让STM32频率计在1Hz输入下给出可信读数
一块频率计在 1MHz 输入下显示得很漂亮,换成每秒只跳一次的信号,读数却开始犹豫。把小数位补成三位,问题依然存在。对于低速转轴、间歇脉冲和慢速时序,先弄清“这个数字由哪两条边沿算出来”,往往比刷新屏幕更有用。
这次给 STM32F103C8T6 频率计补一个独立的低频档。范围定为 0.5~20Hz,使用输入捕获测周期,遇到捕获覆盖或断线超时就撤掉旧读数。这个范围是本次模块的设计边界,不代表整板已经完成量程认证。

图1 两种测量方法的时序示意。下方的 60000、4464 是用于验证回卷处理的合成捕获值。
给低频信号换一种计时方式
固定一秒闸门统计脉冲,关系是 f = N / Tgate。当输入只有 1Hz,一个边界脉冲就对应约 1Hz 的计数分辨率;窗口与信号的相位、信号轻微频偏及软件闸门抖动,都可能让低频读数发生明显跳变。不能据此断言每个精确 1Hz 信号都会跳。
测周期则记录相邻上升沿。定时器每秒走 10000 格,每格 100µs,两个捕获值之差为 D,频率就是 10000 / D。1Hz 对应 10000 格,0.5Hz 对应 20000 格,20Hz 对应 500 格。
代价也很清楚。0.5Hz 至少等两秒才能得到一个完整周期,上电或恢复后还要先取得第一条边沿。若按一格的不确定度估算,仅计时刻度这一项在 1Hz 附近约为 100ppm,在 20Hz 附近约为 0.2%;晶振误差、比较器边沿抖动和输入波形带来的误差还要另算。显示三位小数不会消除它们。
先沿真实电路找到入口

图2 从对应 EDA 工程重新导出的原理图总览,图框为 V1.0。总览用于认路,关键连接放大见下两图。
信号从 U7 的 FREQ_VIN 进入,经 U5 TLV3201 比较器整形,再进入 U6 SN74LVC1G3157 模拟开关。U6 的 B1 接 HIGH_FREQ,B2 接 LOW_FREQ;两条线服务不同的测量方式,并不是两路同时输入。

图3 真实 EDA 局部。R11/R12 均为 10kΩ,C17/C18 均为 22nF;R13 为 10kΩ,R14 为 100kΩ。
这次使用 0~3.3V、共地的低频方波作为接入条件。TIM2 在 PA15 输出 20kHz、50% 占空比的 PWM,经两级 RC 形成约半电源参考。比较器的正反馈形成迟滞,输入必须实际跨过上下门限;它不是任意幅值的通用频率输入,也不能直接接负电压、5V 或市电信号。PCB 上没有为这类输入提供完整的衰减和保护网络。
TI 真值表规定选择端为高电平时接通 B2,因此 PA8 的 CON 输出高电平,选择 LOW_FREQ。不能把“切换到低频”只写在界面标签上,却仍在读高频通道。

图4 LOW_FREQ 接 PB0,即默认映射的 TIM3_CH3;HIGH_FREQ 接 PA12,PWM 接 PA15,CON 接 PA8。
核对 DS5319 的引脚表后,低频捕获使用 PB0/TIM3_CH3。TIM3 不做重映射。代码独占 TIM2、TIM3,TIM2_CH1 部分重映射到 PA15,并关闭 JTAG、保留 SWD。如果现有程序把这些定时器分配给其他功能,需要先解决资源冲突。

图5 同版 PCB 顶面走线视图。右侧连接器、U5/U6 与 MCU 的位置可用于检查输入路径;这是 EDA 视图,不是焊接照片或 DRC 合格证明。

图6 同版 PCB 底层走线视图,按从顶面向下观察的坐标展示,未镜像成实物背面。调试接线仍以网络名和引脚号为准。
让计数回卷成为正常情况
16 位定时器装不下一直增长的时间。这里让它自由运行,ARR = 65535,每 6.5536 秒回卷一次。相邻捕获差用 16 位无符号减法计算,跨过零点也不必清零计数器。
例如前一次捕获是 60000,后一次是 4464,直接按有符号数相减会得到负数。转成 16 位无符号差,结果恰好是 10000 格,也就是一秒。
这个方法有条件。被接受的周期最多两秒,必须短于计数器一圈;主循环还要持续处理捕获并检查超时。如果系统停了很久,不能拿模运算后的“漂亮结果”继续显示。代码同时用毫秒时钟限制相邻处理间隔,并在超时后重建锚点。

图7 有效性状态机。LOST 指捕获寄存器被覆盖,不是对信号源全部丢脉冲情况的检测。
还有一个容易忽略的标志 CC3OF。它表示上一次捕获还没被消费,新的捕获已经覆盖寄存器。此时两条“相邻边沿”的前提已经失效,继续相减可能得到错误频率。处理办法是丢掉锚点,等待两条新的有效边沿。源端本身漏发了一个脉冲而硬件正常捕获,软件无法凭空知道,不能把这个机制宣传为万能丢脉冲检测。
完整的低频捕获模块
下面是本次独立实现的 lowfreq.c。采用 C11,仅依赖标准整数类型;STM32 寄存器地址和状态位依据 RM0008。lf_begin 要求 TIM2/TIM3 的实际输入时钟均为 72MHz,代码不会替调用者验证时钟树。常见配置是 HCLK 72MHz、APB1 36MHz,再由定时器的倍频规则得到 72MHz。
/* SPDX-License-Identifier: MIT
* Independently written low-frequency capture module, version 1.0.0.
* STM32F103C8T6: PB0/TIM3_CH3; PA8 selects U6 B2 (LOW_FREQ).
*/
#include <stdint.h>
enum { LF_WAIT, LF_VALID, LF_RANGE, LF_LOST, LF_TIMEOUT };
typedef struct {
uint32_t last_ms;
uint16_t last_cap, ticks;
uint8_t armed, status;
} LowFreq;
void lf_reset(LowFreq *s) { *s = (LowFreq){0}; }
void lf_timeout(LowFreq *s, uint32_t ms) {
if (s->armed && (uint32_t)(ms - s->last_ms) > 2200u) {
s->armed = 0;
s->status = LF_TIMEOUT;
s->ticks = 0;
}
}
void lf_edge(LowFreq *s, uint16_t cap, uint32_t ms, int lost) {
lf_timeout(s, ms);
if (lost) {
s->armed = 0;
s->ticks = 0;
s->status = LF_LOST;
return;
}
if (s->armed) {
uint16_t dt = (uint16_t)(cap - s->last_cap);
s->ticks = (dt >= 500u && dt <= 20000u) ? dt : 0;
s->status = s->ticks ? LF_VALID : LF_RANGE;
} else {
s->ticks = 0;
s->status = LF_WAIT;
}
s->last_cap = cap;
s->last_ms = ms;
s->armed = 1;
}
/* Integer millihertz; call only for status == LF_VALID. */
uint32_t lf_millihz(const LowFreq *s) {
return s->status == LF_VALID
? (10000000u + s->ticks / 2u) / s->ticks : 0u;
}
#ifndef LOWFREQ_TEST
#define R32(a) (*(volatile uint32_t *)(uintptr_t)(a))
#define T3(o) R32(0x40000400u + (o))
#define CC3IF (1u << 3)
#define CC3OF (1u << 11)
/* Owns TIM2 and TIM3. Caller must configure both APB1 timer clocks to 72 MHz. */
int lf_begin(LowFreq *s, uint32_t tim3_clock_hz) {
if (tim3_clock_hz != 72000000u) return 0;
R32(0x40021018u) |= (1u << 0) | (1u << 2) | (1u << 3);
uint32_t map = R32(0x40010004u);
if ((map & (3u << 10)) != 0u) return 0;
/* Explicit SWJ_CFG avoids relying on its unspecified readback. */
map = (map & ~((7u << 24) | (3u << 8))) | (2u << 24) | (1u << 8);
R32(0x40010004u) = map; /* JTAG off, SWD kept; TIM2_CH1 on PA15 */
R32(0x4002101cu) |= 3u;
R32(0x40021010u) |= 3u;
R32(0x40021010u) &= ~3u;
R32(0x40010804u) = (R32(0x40010804u) & ~0xf0000000u) | 0xa0000000u;
R32(0x40000028u) = 0u;
R32(0x4000002cu) = 3599u;
R32(0x40000034u) = 1800u;
R32(0x40000018u) = 0x68u; /* PWM1, CCR1 preload */
R32(0x40000020u) = 1u;
R32(0x40000014u) = 1u;
R32(0x40000000u) = 0x81u; /* 20 kHz, 50% reference, ARPE+CEN */
R32(0x40010c00u) = (R32(0x40010c00u) & ~15u) | 4u;
R32(0x40010810u) = 1u << 8; /* PA8 latch high before output */
R32(0x40010804u) = (R32(0x40010804u) & ~15u) | 2u;
T3(0x28u) = tim3_clock_hz / 10000u - 1u;
T3(0x2cu) = 65535u;
T3(0x1cu) = 1u; /* CC3S=01, TI3 direct, no filter/prescaler */
T3(0x20u) = 1u << 8; /* CC3E=1, rising edge */
T3(0x14u) = 1u; /* UG loads PSC */
T3(0x10u) = 0u; /* clear initialization flags */
lf_reset(s);
T3(0x00u) = 1u; /* CEN, upcount; DIER remains reset */
return 1;
}
/* Call from one main-loop context at least every 10 ms; no TIM3 ISR. */
void lf_poll(LowFreq *s, uint32_t ms) {
uint32_t before = T3(0x10u);
if (before & (CC3IF | CC3OF)) {
uint16_t cap = (uint16_t)T3(0x3cu); /* reading CCR3 clears CC3IF */
uint32_t after = T3(0x10u);
int lost = ((before | after) & CC3OF) != 0u;
if (lost) T3(0x10u) = 0x165fu; /* rc_w0; keep new IF, reserved bits 0 */
lf_edge(s, cap, ms, lost);
} else {
lf_timeout(s, ms);
}
}
#endif
读取 CCR3 会清除输入捕获标志,CC3OF 则按手册写零清除。代码在读取前后检查覆盖标志;清除时只给已定义的其他可清零状态位写一,保留可能刚到达的新捕获标志,保留位写零。该模块使用主循环轮询,不安装 TIM3 中断,也不在显示任务里清零 CNT。
把它接入现有工程时,先完成 HAL、系统时钟和显示初始化,再启动低频模块。删除原有 TIM2/TIM3 的重复初始化与启动;进入低频档后停用原来的高频读数显示,避免两套数据争用同一区域。原先一次等待数百毫秒的峰值扫描也应停用,本档采用固定参考和上述方波输入条件。
#include "lowfreq.c"
/* HAL_Init / SystemClock_Config / TFT initialization run first.
* HCLK=72 MHz, APB1=36 MHz, TIM2/TIM3 clock=72 MHz.
* This replaces the old TIM2/TIM3 setup and blocking peak scan.
*/
extern uint32_t HAL_GetTick(void);
extern void HAL_Delay(uint32_t ms);
static LowFreq meter;
int meter_start(void) {
if (!lf_begin(&meter, 72000000u)) return 0;
HAL_Delay(10); /* allow the two RC stages to settle */
return 1;
}
/* Main loop: call within 10 ms. Display only when LF_VALID.
* Print LF_WAIT/LF_RANGE/LF_LOST/LF_TIMEOUT as status text.
*/
void meter_task(void) {
lf_poll(&meter, HAL_GetTick());
/* if (meter.status == LF_VALID) use lf_millihz(&meter); */
}
meter_task 的调用间隔应保持在 10ms 以内,屏幕刷新分片调度,不要在循环里再放 600ms 扫描或长时间阻塞发送。捕获时间由硬件锁存,轮询不会用软件到达时间替代 CCR3;一旦处理不过来,覆盖标志会让读数失效。
LF_VALID 时才使用 lf_millihz,单位为毫赫兹。返回零不等于测到了 0Hz,必须同时查看状态。其他状态在屏幕上显示“等待边沿”“超出本档”“捕获覆盖”或“信号超时”,不要继续保留上一次的 Hz 数字。两级 RC 在启动后预留 10ms 稳定时间,该延时只发生一次。
把边界问题写成可重复检查
本次使用 Zig 0.13.0 所带 C 编译器。模块以 -target thumb-freestanding-eabi -mcpu=cortex_m3 -ffreestanding -std=c11 -Os -Wall -Wextra -Werror 编译为 Cortex-M3 目标文件;同一份核心代码在主机上执行断言测试。

图8 实际测试输出。它证明逻辑与这些输入条件匹配,不能替代板上波形、时基准确度或抗干扰实测。
测试包含 1Hz 跨回卷、0.5Hz 和 20Hz 边界、499/20001 格拒绝、捕获覆盖后重建、2200ms 超时、32 位毫秒回卷,以及一次可能伪装成 1Hz 的长时间旧捕获。另将 1~65535 格与 256 个起始相位组合,检查了 16776960 组周期/相位输入。
这一步没有把整板固件链接、烧录或校准,也没有验证比较器实物波形。后续上板宜先用独立信号源输入 1Hz 和 20Hz,检查 PB0 边沿及 50% PWM 滤波参考,再拔掉输入确认旧读数撤销。刻意延长一次主循环来触发捕获覆盖,确认恢复过程确实需要新的两条边沿;不要只测试正常路径。
板上的 SI5351 可用于功能联调,不能因为互测读数接近就宣布绝对准确。是否能校准,要看两套时基的误差和参考来源。低频档首先解决的是测量方法和数据有效性,精度还需要独立参考与实测证据。
资料与验证范围
工程采用 EDA课程案例团队的“stm32-简易频率计”,许可 GPL 3.0,V1.0 原理图更新于 2025-10-31,PCB1 来自同一工程。工程入口及上游代码说明可访问,完整原始固件下载需站点登录;本文模块独立实现,不依赖该下载附件,不提供未经验证的整板替换固件。
项目及工程入口 https://oshwhub.com/course-examples/stm32-jian-yi-pin-lv-ji
上游代码说明 https://wiki.lceda.cn/zh-hans/course-projects/mcu32/32-freq/code-introduce.html
引脚表 DS5319 https://www.st.com/resource/en/datasheet/stm32f103c8.pdf
定时器参考手册 RM0008 第15章 https://www.st.com/resource/en/reference_manual/rm0008-stm32f101xx-stm32f102xx-stm32f103xx-stm32f105xx-and-stm32f107xx-advanced-armbased-32bit-mcus-stmicroelectronics.pdf
开关真值表 https://www.ti.com/document-viewer/SN74LVC1G3157/datasheet
电路图与 PCB 图从实际 EDA 工程重新导出并裁剪缩放,复用部分保留 GPL 3.0 归属;新增模块采用 MIT,完整代码已列于正文。示意图明确标注为示意,验证范围为主机逻辑测试和 Cortex-M3 目标编译,未做整板实测。
技术交流,欢迎关注微信公众号:美男子玩编程。



