
这次我们来看 STM32C5A3R 上定时器输入捕获的完整用法目标很明确测量外部方波信号的频率。很多朋友刚接触 STM32 定时器时都是从 PWM 输出开始的但“测频率”在项目里出现得更早——测电机转速、测传感器输出、测外部时钟源本质上都是先要知道一个信号的频率。用定时器输入捕获做频率测量最大优势是硬件自动捕获边沿不需要 CPU 一直去读引脚电平。定时器在检测到上升沿或下降沿时会把当前计数器值锁存到捕获寄存器同时触发中断。我们在中断回调里拿到两次连续边沿的计数值差值就能算出周期再换算成频率。这篇文章不会停在概念层面而是按“CubeMX 图形化配置 - 生成代码 - 写中断回调 - 串口打印频率”的完整流程走一遍并给出频率和占空比同时测量的思路、多通道扩展方案、资源占用观察方法和常见问题排查清单。如果你现在正在做 STM32 的定时器输入捕获或者想给项目加一个频率测量功能这篇文章可以直接收藏。1. 核心能力速览先用一张表把 STM32C5A3R 定时器输入捕获测频率的关键信息列出来方便快速判断这个方案适不适合你。能力项说明功能定位定时器输入捕获测量外部方波信号频率可扩展到占空比测量开发环境STM32CubeMX HAL 库其他开发方式原理相同测量原理捕获相邻边沿的计数器值用定时器时钟频率计算信号周期硬件需求一块 STM32C5A3R 开发板或最小系统信号源串口调试工具中断开销边沿触发中断中只读取捕获寄存器主循环负责计算和输出CPU 占用整体较低适合在实时系统中作为后台测量任务扩展方向频率占空比同测、多通道多路信号、DMA 批量捕获、串口指令交互输出方式串口打印、调试器变量观察、外部显示或上报给上位机这个方案的核心价值在于定时器自己完成信号边沿的时间记录代码逻辑简单而且 CubeMX 配置完成后需要在用户代码里做的事很少。2. 适用场景与使用边界定时器输入捕获测频率在嵌入式项目里非常常见但也不是所有场景都适合用这个方法。先看适用场景PWM 信号测量。比如采集另一个 MCU 输出的 PWM确认频率和占空比是否符合预期。电机转速反馈。霍尔传感器或编码器输出的脉冲频率可以换算成 RPM。超声波测距回波宽度。回波高电平时间虽然不完全是“频率”但同样用输入捕获测高电平时间方法几乎一样。红外遥控解码。NEC 协议等红外信号本质上就是一组不同脉宽的时间序列。外部时钟校准。用标准频率源测量本地振荡器的频率偏差。再看使用边界信号电平必须落在 MCU 引脚的输入电压范围内超出范围需要加电平转换或隔离电路。输入信号需要用示波器或逻辑分析仪确认是干净的方波。如果边沿抖动很严重建议先加硬件 RC 滤波或使用定时器自带的输入滤波器。频率测量范围受计数器位宽和定时器时钟限制。频率太高两个边沿间隔太短计数差值分辨率不够频率太低计数器可能溢出需要额外处理溢出计数。中断频率和 CPU 负载要平衡。如果测量一个很高频的信号每个边沿都进中断系统大部分时间都会耗在中断处理上。另外提醒一句如果要把这个功能用来做产品功能频率测量误差需要结合实际信号源做标定不能只看理论计算值。3. 环境准备与前置条件3.1 硬件准备建议准备以下硬件一块 STM32C5A3R 开发板或基于该芯片的自制最小系统板。一个可以输出方波信号的来源。最简单的办法是再找一块 STM32 开发板输出 PWM或者使用信号发生器。没有信号发生器时可以用另一个定时器输出固定频率方波作为测试信号。一个 ST-Link 或 J-Link 调试器用于下载和调试。一个 USB 转串口模块方便查看串口打印的频率数据。杜邦线若干用于连接信号输出和定时器输入引脚。3.2 软件准备需要安装以下软件版本以当前最新稳定版为准STM32CubeMX。用于图形化配置芯片引脚、时钟树、定时器外设。STM32CubeIDE或 Keil MDK。两者都可以编译调试本文以通用流程描述实际菜单名称可能有差异。ST-Link 驱动。如果是 ST 原厂开发板一般插上就能识别。串口调试助手用于观察串口输出。3.3 固件包检查STM32C5A3R 属于 STM32C5 系列打开 STM32CubeMX 新建工程时如果型号列表里找不到该芯片需要先更新 STM32CubeMX并在 CubeMX 中安装对应的 STM32C5 系列固件包。固件包安装路径一般在 CubeMX 的 Help - Manage embedded software packages 中管理具体版本以你安装时能获取到的版本为准。3.4 通用检查清单在开始配置之前建议先确认以下几点开发板能正常下载程序点灯工程可以跑通。串口能够正常输出连接无误。信号源输出正常可以用示波器看到方波。准备测试的低频、中频、高频三组信号便于验证测量结果。4. 安装部署与工程配置这一节是整个流程的核心部分我们从头开始创建一个 STM32C5A3R 的定时器输入捕获工程。4.1 新建工程并选择芯片打开 STM32CubeMX点击 New Project在芯片搜索框中输入 STM32C5A3R选中对应型号点击 Start Project。如果搜索不到按第 3.3 节的方式先更新固件包。4.2 配置 RCC 和时钟树在 System Core - RCC 中根据开发板实际晶振配置 HSE。如果开发板没有外部晶振可以继续使用内部时钟。关键点在这里建议先到 Clock Configuration 页面确认定时器时钟。CubeMX 会根据系统时钟自动计算出各个定时器总线的时钟频率。这个频率决定了后续 PSC 和 ARR 怎么算所以不要略过这一步。定时器时钟频率是计算频率的关键参数。实际数值取决于你的时钟树配置不同板子、不同时钟源结果可能不一样请以 CubeMX 的 Clock Configuration 页面显示值为准。后续代码中我们用timer_clock这个变量来表示它。4.3 配置定时器输入捕获在左侧 Pinout Configuration 面板中找到要使用的定时器。这里以 TIM3 为例实际操作时选择哪个定时器取决于你的引脚映射和项目资源。在 TIM3 的 Mode 配置中Clock Source 选择 Internal Clock。Channel1 选择 Input Capture direct mode也就是 TI1 直接输入到捕获通道 1。Channel2 如果也要用可以类似配置。本文先以单通道测量频率为例。在 Configuration 面板中Parameter Settings 的关键参数如下PrescalerPSC预分频值。置 0 表示计数器时钟等于定时器时钟分辨率最高。如果信号频率偏低可以适当增大 PSC降低计数频率。Counter PeriodARR自动重装值。这个值决定了计数器能计多少个数。测量频率时它应该大于预期信号一个周期对应的计数个数。Counter ModeUp向上计数模式。Internal Clock Division通常选 1。Trigger Selection如果只是普通捕获可以不配触发源。Input Filter如果信号有毛刺可以设一个滤波值。默认 0 表示不滤波。Input Prescaler可对输入信号做分频用于高频信号测量。默认不分频。需要注意的是PSC 和 ARR 的最终值必须和你的定时器时钟、待测信号频率匹配。下面给出一个计算示例。假设定时器时钟频率为 80 MHz待测信号频率为 1 kHz一个信号周期的计数个数为 80 MHz / 1 kHz 80000 也就是说计数器从 0 计到 80000 对应一个完整的信号周期。那么 ARR 至少要大于 80000才能保证一次计数不会溢出。如果 ARR 最大值 65535不够用就要增大 PSC。比如 PSC1 时计数频率降为 40 MHz一个周期计 40000 个ARR65535 就够了。再举高频例子。如果待测信号是 10 MHz定时器时钟 80 MHz一个周期只计 8 个计数分辨率 12.5%。这时分辨率不足测量误差会很明显。要么提高定时器时钟要么换用其他测频方案比如外部计数器加闸门时间测量。4.4 配置 NVIC 中断在 NVIC Settings 中勾选 TIM3 global interrupt开启定时器全局中断。输入捕获中断回调依赖这个中断才能触发。4.5 配置串口如果想把测量结果打印出来还需要配置一个串口。在左侧 Connectivity - USART 中把对应串口设为 Asynchronous 模式波特率建议 115200这个值可以根据需要调整。串口引脚分配保持 CubeMX 默认即可。4.6 生成工程代码在 Project Manager 中设置工程名称、路径和 IDE。点击 Generate Code 生成代码。生成代码后打开工程核心的定时器和串口初始化代码已经由 CubeMX 写好了。我们需要在用户代码区添加输入捕获启动逻辑、中断回调和频率计算逻辑。5. 功能测试与效果验证5.1 启动输入捕获定时器初始化完成后默认不会立即开始捕获。需要在 main 函数中手动启动。在 main.c 的/* USER CODE BEGIN 2 */区域添加/* USER CODE BEGIN 2 */ HAL_TIM_IC_Start_IT(htim3, TIM_CHANNEL_1); printf(Input Capture Start\r\n); /* USER CODE END 2 */HAL_TIM_IC_Start_IT的作用是启动输入捕获并使能中断。启动后每次检测到通道 1 的选定边沿就会进入中断回调。注意如果你使用的是 printf 重定向需要确保串口重定向已经配置好。这里不再展开 printf 重定向的细节。5.2 编写中断回调输入捕获的中断回调函数是HAL_TIM_IC_CaptureCallback。CubeMX 生成的代码中可能没有这个函数需要我们自己实现。先实现一个最简单的“测量频率”版本/* USER CODE BEGIN 4 */ uint16_t g_capture_diff 0; uint8_t g_capture_flag 0; void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM3) { if (htim-Channel HAL_TIM_ACTIVE_CHANNEL_1) { static uint16_t last_capture 0; uint16_t current_capture __HAL_TIM_GET_COMPARE(htim, TIM_CHANNEL_1); // 计算两次捕获之间的计数值差值 g_capture_diff current_capture - last_capture; last_capture current_capture; g_capture_flag 1; } } } /* USER CODE END 4 */这个回调的逻辑每次输入捕获中断发生读取当前捕获寄存器值。和上一次捕获值做差得到两次上升沿之间的计数值差。把这个差值保存到全局变量并置标志位告诉主循环“有新数据了”。这里使用uint16_t类型是因为常见的通用定时器是 16 位。如果实际使用的是 32 位定时器需要改为uint32_t。用无符号减法计算差值有一个好处当计数器回绕时低 16 位差值依然能算对但前提是两次捕获间隔不能超过计数器最大值。如果被测信号周期太长导致中间发生多次计数器溢出就必须额外处理溢出次数。中断回调里只做“保存计数值差值”这件事不做频率浮点运算。原因很简单浮点运算会占很多时间放在主循环里做更安全也方便调试。5.3 主循环计算并打印频率在主循环中发现标志位置 1 后读取差值并计算频率。假设定时器时钟频率为timer_clock则信号周期对应的计数值为g_capture_diff信号频率为freq timer_clock / g_capture_diff注意当 PSC 不为 0 时真正的计数器时钟是timer_clock / (PSC 1)计算时要用修正后的计数时钟。主循环代码示例/* USER CODE BEGIN WHILE */ while (1) { if (g_capture_flag 1) { g_capture_flag 0; if (g_capture_diff 0) { // 这里的 timer_clock 需要替换为实际定时器计数时钟频率 float freq (float)timer_clock / (float)g_capture_diff; printf(Freq: %.2f Hz, diff: %d\r\n, freq, g_capture_diff); } } /* USER CODE END WHILE */ }把工程编译下载到开发板后用信号源向 TIM3 通道 1 对应引脚输入一个 1 kHz 方波串口助手应该能看到类似输出Input Capture Start Freq: 1000.00 Hz, diff: 80000 Freq: 999.98 Hz, diff: 80001不同时钟配置和测试条件下diff 值会不同频率值应接近信号源设定值。如果串口打印出 0 或者错误数值按第 8 节排查。5.4 同时测量频率和占空比很多场景不只要求频率还想知道高电平时间或占空比。这时要用两个通道或同一个通道配置双沿捕获。比较常用的方法是使用定时器的两个捕获通道一个捕获上升沿一个捕获下降沿。简单做法是配置通道 1 为上升沿捕获通道 2 为下降沿捕获。代码如下void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM3) { if (htim-Channel HAL_TIM_ACTIVE_CHANNEL_1) { // 上升沿记录周期 static uint16_t last_rising 0; uint16_t rising __HAL_TIM_GET_COMPARE(htim, TIM_CHANNEL_1); g_period_diff rising - last_rising; last_rising rising; g_period_flag 1; } else if (htim-Channel HAL_TIM_ACTIVE_CHANNEL_2) { // 下降沿记录高电平时间 uint16_t falling __HAL_TIM_GET_COMPARE(htim, TIM_CHANNEL_2); static uint16_t last_falling 0; g_duty_diff falling - last_falling; last_falling falling; g_duty_flag 1; } } }计算占空比时用高电平计数值差值除以整个周期计数值差值即可。需要注意的是双通道配置时必须保证两个通道的输入来自同一个信号且要正确处理边沿顺序。实际项目中占空比测量还需要考虑上升沿和下降沿的对应关系避免出现“先下降后上升”导致高电平时间算错。5.5 验证判断标准怎么判断输入捕获测频率是否成功频率输出是否稳定。1 kHz 信号在普通时钟精度下输出应该稳定在 999.xx Hz 到 1000.xx Hz 之间不会出现几十 Hz 的跳变。频率是否随输入变化。把信号源改为 10 kHz、50 kHz、100 kHz输出应该能跟随变化。改变 PSC 后频率结果是否仍一致。PSC 改了计数值会变但换算出的频率应该不变。如果不一致说明计算时用的计数时钟没有对应修正。用示波器同时观察输入信号和串口打印结果确认两者吻合。6. 接口扩展与批量采集思路STM32 单片机本身没有 Web API 这种东西但我们可以把“测量功能”做成串口指令的形式方便在调试阶段快速交互也可以把多通道采集做成批量任务。6.1 串口指令交互一个简单的做法是串口收到read_freq\r\n指令时返回Freq: xxx Hz。这样可以脱离 IDE 调试器直接在串口助手里下指令读取当前频率。示例伪代码uint8_t rx_buffer[64]; uint32_t rx_len 0; void SerialCommand_Handle(void) { if (strncmp((char *)rx_buffer, read_freq, 9) 0) { printf(Freq: %.2f Hz\r\n, current_freq); } else if (strncmp((char *)rx_buffer, start_cap, 9) 0) { HAL_TIM_IC_Start_IT(htim3, TIM_CHANNEL_1); printf(Capture started\r\n); } }这种指令交互方式特别适合原型验证不用一改参数就重新编译下载固件。6.2 多通道批量测量如果同时需要测量多路信号可以将多个定时器通道分别配置为输入捕获。CubeMX 中每个通道独立配置捕获边沿代码中根据中断回调的htim-Channel区分是哪个通道的事件。批量采集的思路为每路信号维护独立的捕获差值和标志位。主循环统一轮询所有标志位。每路数据在输出时加上通道标识比如CH1: 1000 Hz, CH2: 2000 Hz。实际使用中要注意多个通道共享同一个中断回调入口回调里的判断逻辑会变长尽量保持回调简短只保存数据不处理浮点运算。6.3 多次测量取平均单次测量的结果波动会比较大尤其是信号边沿存在抖动时。建议加入多次采集取平均的逻辑比如连续测量 10 次去掉最大值和最小值再取平均。这种做法对频率稳定性判断很有帮助。uint32_t freq_sum 0; uint32_t sample_count 0; 在回调或主循环中 freq_sum current_freq; sample_count; if (sample_count 10) { float avg_freq (float)freq_sum / 10.0f; printf(Avg Freq: %.2f Hz\r\n, avg_freq); sample_count 0; freq_sum 0; }取平均能明显减少显示抖动但代价是实时性下降。对实时性要求高的场景可以减小采样次数到 3 到 5 次。7. 资源占用与性能观察定时器输入捕获是硬件外设行为计数器计数完全由硬件完成CPU 不需要参与。这一点比 GPIO 模拟测量方波要节省大量 CPU 时间。7.1 中断开销每次捕获触发一次中断中断里做的事情是读取捕获寄存器。做一次减法。保存标志位。这个操作在几十个时钟周期内能完成对大部分系统来说负担很小。但要注意如果被测信号频率很高比如 100 kHz每 10 微秒触发一次中断虽然单次开销小但频繁打断主循环会影响其他实时任务。如果发现系统被测量任务拖累可以考虑增大分频器降低捕获频率比如每 4 个边沿捕获一次。换用 DMA 模式多个捕获值由 DMA 搬运CPU 在 DMA 完成中断中一次性处理。把计算逻辑放到低优先级任务或空闲时间处理。7.2 计数分辨率和测量精度计数分辨率等于计数时钟的倒数。计数时钟越高两个边沿之间能计到的数越多分辨率越高。举例定时器时钟 80 MHz计数时钟 80 MHz两个上升沿间隔 1 ms相差 80000。分辨率是 1/80000也就是 0.00125%。如果 PSC 设为 79计数时钟变成 1 MHz同样 1 ms 信号只差 1000分辨率变成 0.1%。所以测高频信号时PSC 要尽量小。测低频信号时限制条件变成了计数器溢出。1 kHz 信号在 80 MHz 计数时钟下一个周期计数 8000016 位计数器最大值 65535不够用。这时必须增大 PSC或者使用 32 位定时器。如果你在做一个低频测量场景比如 50 Hz 交流信号就更要注意 ARR 和溢出计数的问题。7.3 观察方式调试阶段可以用三种方式观察资源占用在调试器中打开变量窗口实时查看g_capture_diff和current_freq。用逻辑分析仪抓取一个 GPIO 翻转信号测量中断处理函数从开始到结束的耗时。对比打开和关闭输入捕获功能时系统主循环执行其他任务的时间差。这些方法能帮你判断当前配置是否会在系统中造成性能瓶颈。8. 常见问题与排查方法输入捕获测频率看起来很直观但实际操作中容易踩坑。下面把常见问题整理成表格方便对照排查。问题现象可能原因排查方式解决方案捕获中断不触发定时器通道未启动或 NVIC 未使能检查HAL_TIM_IC_Start_IT是否调用检查 NVIC 配置在主函数中调用HAL_TIM_IC_Start_IT中断触发但频率始终为 0捕获值差值为 0或计数时钟计算错误在回调中打印原始捕获值检查 PSC 和 ARR 配置确认计数时钟频率频率显示值比实际值小ARR 太小计数器溢出观察 diff 值是否接近 ARR增大 ARR或使用 32 位定时器频率显示值不稳定跳变明显信号边沿毛刺接触不良用示波器观察输入信号开启输入滤波器检查接线或加 RC 滤波高频信号测量误差大计数分辨率不足检查 PSC 是否过大将 PSC 设为 0提高计数时钟串口没有打印输出串口未配置或 printf 重定向失败单独测试串口发送数据确认串口初始化检查重定向函数引脚不能识别输入信号GPIO 复用配置错误检查 CubeMX 引脚是否映射到定时器通道在 Pinout 页面检查复用功能分配编译报错找不到定时器相关 HAL 函数固件包或 HAL 库配置缺失检查 CubeMX 生成的工程是否包含定时器模块在 CubeMX 中确认定时器外设已被选中并重新生成代码双通道测量时频率和占空比错位上升沿和下降沿匹配错误检查回调中通道判断逻辑用标志位区分上升沿和下降沿状态确保周期和高电平时序匹配最值得说的是第一条。很多朋友配置好工程后发现程序不进入中断回调直接怀疑定时器初始化有问题。实际上HAL_TIM_IC_Start_IT这一步被漏掉的概率非常高。CubeMX 生成的MX_TIM3_Init只做了外设参数初始化并不会自动启动捕获。记得在主函数中手动启动对应通道的输入捕获中断。第二条也比较常见。g_capture_diff是uint16_t如果使用 32 位定时器读取捕获值要改成uint32_t否则高 16 位被截断计算出来的频率会严重错误。另外提醒一点如果被测信号没有共地也就是信号源和开发板的地线没有接到一起捕获结果可能完全随机甚至不触发中断。排查时先确认共地。9. 最佳实践与使用建议9.1 第一次先跑最小验证不要一上来就搞多通道、占空比、DMA 这些扩展功能。第一次做输入捕获测频率时先用一个固定频率的方波信号验证整个链路。建议从 1 kHz 开始因为这个频率在计数分辨率和 ARR 之间比较容易平衡。验证通过了再逐步增加功能。这样做的好处是一旦出现问题可以确定是基础配置的问题还是扩展功能引入的问题。9.2 把计数值和频率分开处理中断回调里只保存计数值差值不要做浮点运算。主循环中再计算频率。这样可以避免两个问题浮点运算在中断里占用太长时间影响其他中断响应。调试时可以直接查看原始计数值判断是捕获问题还是计算问题。9.3 工程和素材分类管理虽然是嵌入式工程但同样建议把 CubeMX 工程、源码、测试脚本分目录管理。比如project/ ├── CubeMX/ # CubeMX 工程文件 ├── Core/ # 源码 ├── Test/ # 测试脚本、信号源配置记录 └── Docs/ # 项目文档和笔记这样在后续维护时可以快速找到 CubeMX 配置和源码的对应关系避免改了一处忘记另一处。9.4 批量采集时加标志和日志多通道批量测量时建议为每路信号打上独立标签并记录测量时间戳。这样即使某一路信号异常也能从日志里快速定位。记录格式可以参考printf([%lu ms] CH1: %.2f Hz, CH2: %.2f Hz\r\n, (unsigned long)HAL_GetTick(), freq1, freq2);9.5 串口调试注意安全用 USB 转串口调试时不要带电插拔杜邦线最好使用隔离型 USB 转串口模块。连接时先确认好 TX 和 RX 方向避免误接导致通信异常。9.6 信号源合规使用如果你用信号发生器、另一块开发板或者其他设备产生测试信号确保这些信号来源合法、来源明确。在项目开发过程中不要对未授权的、高电压的或来源不明的信号做直接测量避免损坏开发板或造成安全隐患。9.7 商用前标定如果要把频率测量功能放到产品中不能只靠理论计算。需要配合标准频率源做全量程标定记录不同频率点的测量误差形成标定表。这样才能确认在不同温度和电压下测量误差是否在允许范围内。10. 总结与下一步STM32C5A3R 的定时器输入捕获做频率测量从 CubeMX 配置到串口打印出结果整个过程并不复杂。核心就是两件事一是把定时器时钟和分频关系算清楚二是在中断回调里正确保存捕获差值。最值得先验证的是单通道 1 kHz 方波测量。把这一条链路跑通再往占空比、多通道、DMA 方向扩展就顺理成章。最容易踩的坑有两个一个是忘记调用HAL_TIM_IC_Start_IT导致中断不触发另一个是 PSC 和 ARR 没算好低频溢出不处理高频分辨率不足。下一步可以做的方向很多。如果你想继续深入建议按这个顺序尝试用双通道测量频率和占空比对比原始计数值验证高电平时间是否正确。加入 DMA 批量采集模式降低高频信号下的中断频率。加入串口指令解析让上位机可以实时查询测量结果。在 RTOS 环境下把测量任务放到独立线程中观察任务调度对其他实时任务的影响。建议把这篇配置流程保存好尤其是第 8 节的问题排查表。实际开发中遇到频率显示不对的情况回来看这个表能少走不少弯路。