让STM32频率计在1Hz输入下给出可信读数

2026-11-063

一块频率计在 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%;晶振误差、比较器边沿抖动和输入波形带来的误差还要另算。显示三位小数不会消除它们。

先沿真实电路找到入口

频率计实际EDA原理图总览

图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。如果现有程序把这些定时器分配给其他功能,需要先解决资源冲突。

频率计PCB顶面高清走线视图

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

频率计PCB底层高清走线视图

图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 目标编译,未做整板实测。

技术交流,欢迎关注微信公众号:美男子玩编程。

创作许可协议

本项目采用 CC BY(署名) 进行许可。

评论(0)
- 没有更多了 -