共计 3041 个字符,预计需要花费 8 分钟才能阅读完成。
背景痛点
在工业自动化设备中,经常需要对电机编码器或高速脉冲信号进行精确计数。传统做法是用外部中断或轮询 GPIO,但在 1200Hz 及以上频率时,常遇到两个典型问题:

- 信号抖动导致的误计数:机械振动或电磁干扰会让脉冲边沿产生毛刺
- CPU 负载导致的丢脉冲:当系统处理其他任务时,可能错过高速脉冲的跳变沿
我曾在一个包装机项目中使用普通 IO 中断计数,结果实际产量总是比理论值少 3% 左右。通过逻辑分析仪抓包才发现,当传送带高速运行时,部分脉冲被 MCU 漏掉了。
硬件选型
面对高速计数需求,开发者通常有两种选择:
- 外部专用计数芯片(如 HCTL-2022)
- 优点:自带 32 位计数器,不占用 CPU 资源
-
缺点:增加 BOM 成本,需要额外通信接口读取计数值
-
STM32 内置定时器
- 优点:零额外成本,灵活配置计数模式
- 挑战:需要合理配置时钟树和滤波参数
经过实测,STM32F103 的 TIM2 定时器在 72MHz 主频下,配合输入捕获滤波器,完全能稳定处理 1200Hz 信号。下面分享具体实现方法。
核心实现
定时器基础配置
- 时钟配置:确保 APB1 总线时钟达到最高频率(通常 36MHz 经过倍频到 72MHz)
- 分频设置:TIMx_PSC(Prescaler)设为 0,让计数器每个时钟周期都计数
- 重载值:TIMx_ARR(Auto-reload Register)设为 65535(16 位定时器最大值)
// TIM2 基础配置结构体
TIM_HandleTypeDef htim2;
htim2.Instance = TIM2;
htim2.Init.Prescaler = 0; // 无预分频
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 0xFFFF; // ARR 最大值
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&htim2);
输入捕获配置关键点
- 滤波器设置:根据信号特性选择合适采样频率
- 示例:8 次采样中 5 次一致才触发(TIM_ICPSC_DIV8, TIM_IC_FILTER_5)
- 边沿检测:通常选择上升沿触发
TIM_IC_InitTypeDef sConfigIC;
sConfigIC.ICPolarity = TIM_ICPOLARITY_RISING;
sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
sConfigIC.ICPrescaler = TIM_ICPSC_DIV8; // 8 分频采样
sConfigIC.ICFilter = 5; // 5/ 8 滤波
HAL_TIM_IC_ConfigChannel(&htim2, &sConfigIC, TIM_CHANNEL_1);
中断安全保护
在中断服务函数中读取计数值时,必须防止主程序同时修改计数器。推荐做法:
- 使用
__HAL_LOCK(htim)进行硬件锁 - 临界区操作前关闭全局中断
- 对于频繁的中断,考虑使用环形缓冲区暂存数据
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{if(htim->Instance == TIM2)
{__disable_irq(); // 进入临界区
uint16_t count = TIM2->CNT;
__enable_irq(); // 退出临界区
// 处理计数值...
}
}
性能优化
延迟实测对比
使用 1000Hz 方波信号测试不同实现方案的响应延迟(示波器测量从信号跳变到计数器更新的时间差):
| 实现方式 | 平均延迟 | 最大抖动 |
|---|---|---|
| GPIO 轮询 | 15μs | ±8μs |
| 外部中断 | 6μs | ±3μs |
| 定时器输入捕获 | 2μs | ±0.5μs |
GPIO 速度影响
测试不同 GPIO 配置对信号捕捉的影响(信号频率 1200Hz):
| GPIO_Mode | 误触发次数 / 分钟 |
|---|---|
| 低速(10MHz) | 127 |
| 中速(50MHz) | 42 |
| 高速(100MHz) | 3 |
结论:至少配置为 50MHz 以上输出速度
避坑指南
PCB 布局建议
- 编码器信号线走差分对(如可用 A +/A- 信号)
- 在 MCU 引脚附近放置 100nF 去耦电容
- 避免计数信号线与电机电源线平行走线
计数器溢出处理
当脉冲频率×运行时间可能超过 65535 时,需要扩展为 32 位计数器:
volatile uint32_t totalCount = 0;
uint16_t lastCount = 0;
void UpdateCount()
{
uint16_t current = TIM2->CNT;
if(current < lastCount) {totalCount += 65536 - lastCount + current;} else {totalCount += current - lastCount;}
lastCount = current;
}
完整代码示例
// 在 main.c 中添加
void MX_TIM2_Init(void)
{TIM_ClockConfigTypeDef sClockSourceConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
TIM_IC_InitTypeDef sConfigIC = {0};
htim2.Instance = TIM2;
htim2.Init.Prescaler = 0;
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 65535;
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&htim2);
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig);
HAL_TIM_IC_Init(&htim2);
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig);
sConfigIC.ICPolarity = TIM_ICPOLARITY_RISING;
sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
sConfigIC.ICPrescaler = TIM_ICPSC_DIV8;
sConfigIC.ICFilter = 5;
HAL_TIM_IC_ConfigChannel(&htim2, &sConfigIC, TIM_CHANNEL_1);
}
// 启动计数
HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1);
延伸思考
对于更高端的应用场景,可以尝试以下进阶方案:
- 编码器接口模式:
- 配置 TIMx_SMCR 寄存器为编码器模式
-
同时接入 A / B 相信号实现正反转判断
-
DMA 传输计数:
- 将定时器的 CNT 寄存器通过 DMA 周期性传输到内存
-
完全解放 CPU,适合 1MHz 以上超高频信号
-
多定时器级联:
- 用 TIMx 作为从定时器,扩展计数位宽
- 实现 32 位甚至 64 位计数器
通过本文介绍的方法,我们在贴片机项目中将计数误差控制在 0.01% 以内。希望这些实战经验对你有帮助!
正文完
发表至: 未分类
近两天内
