转速测量先把传感器信号转换为“每转有多少个被计数的事件”,再决定测两个事件之间的时间,还是在固定时间内累计事件。低速时周期法通常更有分辨率;高速时门控计数法更容易平均;需要方向和位移时使用正交编码器。
本页以 STM32F103、内部定时器时钟 72 MHz 为具体示例。原稿使用 STM32F10x 标准外设库(SPL);下面保留其引脚和配置思路,用 CMSIS 寄存器代码说明修正后的捕获边界。SPL、HAL 和 CMSIS 的初始化代码不能不加区分地混在同一套工程中。
先定义 PPR 与计数倍率
跳转到“先定义 PPR 与计数倍率”设 为每转实际进入本次计算的事件数。单路传感器每转输出一个完整脉冲、只捕获上升沿时,;若每转有 20 个上升沿,。编码器铭牌中的 PPR/CPR 定义并不统一,必须确认它指单相周期、边沿数还是已四倍频的计数。
周期法测得相邻事件间隔 :
门控计数法在实际观测时间 内取得 个事件:
这两个公式中的事件定义必须一致。漏边沿、重复边沿、滤波延迟或错误的 都会改变结果,不能靠最后乘一个修正系数掩盖。
周期捕获:TIM3_CH1 / PA6
跳转到“周期捕获:TIM3_CH1 / PA6”F103 默认映射中可用 PA6 接 TIM3_CH1。原 SPL 思路对应如下配置;具体封装、AFIO 重映射和输入电平仍需检查。
| 项目 | 例子 | 目的 |
|---|---|---|
| 时钟 | GPIOA、TIM3;如需重映射再使能 AFIO | 使引脚和定时器工作 |
| 输入 | PA6,选择符合传感器输出的浮空/上拉/下拉 | 开漏输出需要合适上拉;不能对所有传感器套同一种输入方式 |
| PSC | 71,假定 TIM3 内核72 MHz | 得到1 MHz计数,即1 µs/tick |
| ARR | 65535 | 16位完整计数范围 |
| CH1 | Direct TI、上升沿、捕获预分频DIV1 | 每个选择的输入事件锁存CCR1 |
| 输入滤波 | 例如编码3,须按信号宽度核算 | 抑制短毛刺,同时产生采样延迟 |
| 中断 | CC1和Update,正确NVIC入口 | 捕获时间戳并扩展回绕次数 |
F103 的 TIM3 为16位,1 MHz 时每65.536 ms回绕。仅凭相邻两个16位CCR值、且不累计溢出时,可区分的事件间隔必须小于一圈。对1事件/转,这对应约915.527 RPM的下界,因此原来不记录溢出的代码不能同时覆盖1~3000 RPM。RM0008:TIM2~TIM5及输入捕获
原来的跨回绕表达式 0xffff - last + now 还少了一个计数。若 last=65530、now=5,经过的计数应是11而不是10。一般模数为 ARR+1;只有ARR=65535时,转成 uint16_t 的模减法才自然等同于所需模数。
扩展时间戳所需的边界
跳转到“扩展时间戳所需的边界”下面的 ISR 例子把每次Update累计为65536 µs,从而扩展为32位微秒时间戳。它依赖明确的运行条件:
- TIM3独占,1 MHz、ARR=65535、向上自由运行,URS配置为仅计数溢出/下溢产生更新请求;运行期间不产生UG、不改CNT/PSC/ARR。
- 更新事件必须及时服务,不能漏过一整圈。本例用CCR的高/低半圈区分同时挂起的捕获和更新,要求相关事件的最长未处理时间小于半圈,即32.768 ms。
- 捕获ISR本身应远短于输入间隔。多个捕获覆盖同一CCR时,以CC1OF报告丢失并重新配对,不能把覆盖后的数据当成连续两次测量。
- 32位微秒差值仍有模数;两次有效事件的真实跨度必须小于一圈。本例的65秒重新配对阈值远小于约71.58分钟的32位回绕周期。
如果系统会长时间屏蔽中断,上述软件扩展并不可靠。可以降低计数频率、使用硬件级联,或在实际支持32位定时器的器件上选择合适实例;不能因为其他STM32的TIM2是32位就把F103 TIM2当成32位。
#include <stdbool.h>#include <stdint.h>#include "stm32f103xb.h"
typedef struct { uint32_t last_edge_us; uint32_t period_us; uint32_t generation; uint32_t overcaptures; bool have_edge; bool period_valid;} rpm_capture_t;
static volatile uint32_t tim3_epoch_us;static volatile rpm_capture_t rpm_capture;
/* Requires service latency less than half of the 16-bit timer period. */static uint32_t extended_capture_us(uint32_t epoch, uint16_t captured, bool update_pending){ if (update_pending && captured < 0x8000u) { epoch += 65536u; } return epoch + captured;}
void TIM3_IRQHandler(void){ uint32_t flags = TIM3->SR; uint32_t enabled = TIM3->DIER; bool update = (flags & TIM_SR_UIF) != 0u && (enabled & TIM_DIER_UIE) != 0u; bool capture = (flags & TIM_SR_CC1IF) != 0u && (enabled & TIM_DIER_CC1IE) != 0u; uint32_t epoch = tim3_epoch_us; uint16_t captured = 0u; if (capture) { captured = (uint16_t)TIM3->CCR1; /* reading CCR1 clears CC1IF */ } bool overcapture = ((flags | TIM3->SR) & TIM_SR_CC1OF) != 0u;
if (update) { TIM3->SR = ~TIM_SR_UIF; tim3_epoch_us = epoch + 65536u; } if (overcapture) { TIM3->SR = ~TIM_SR_CC1OF; rpm_capture.overcaptures++; rpm_capture.have_edge = false; rpm_capture.period_valid = false; return; } if (!capture) { return; }
uint32_t timestamp = extended_capture_us(epoch, captured, update); uint32_t interval = timestamp - rpm_capture.last_edge_us; rpm_capture.period_valid = rpm_capture.have_edge && interval != 0u && interval <= 65000000u; rpm_capture.period_us = rpm_capture.period_valid ? interval : 0u; rpm_capture.last_edge_us = timestamp; rpm_capture.have_edge = true; rpm_capture.generation++;}
rpm_capture_t rpm_capture_snapshot(uint32_t *now_us){ uint32_t saved_mask = __get_PRIMASK(); __disable_irq(); rpm_capture_t result = rpm_capture; uint32_t epoch = tim3_epoch_us; uint16_t counter = (uint16_t)TIM3->CNT; bool update = (TIM3->SR & TIM_SR_UIF) != 0u; uint32_t now = extended_capture_us(epoch, counter, update); __set_PRIMASK(saved_mask); if (now_us != 0) { *now_us = now; } return result;}初始化时应在定时器和中断关闭状态清零 tim3_epoch_us 与 rpm_capture,装入PSC/ARR,产生一次初始化UG并清标志,再启用UIE、CC1IE、NVIC和计数器。上面的代码只是捕获/快照模块,没有提供板级时钟、GPIO、串口或启动文件,也没有证明实际中断延迟满足要求。
读取快照采用短暂保留/恢复PRIMASK的临界区,使多个字段来自同一次一致状态;只给变量加 volatile 不具备这个保证。不要在这个临界区中计算浮点数或输出串口。
快照状态应区分:尚无边沿、只有第一个边沿、已有有效周期、新测量到达,以及超过停止超时。generation可判断是否有新事件;“这次没有新数据”不应直接显示成0 RPM。对允许1 RPM、1事件/转的信号,一次正常间隔就是60秒,不可能同时用100 ms无边沿就断定电机停止。
从快照计算转速前,检查 period_valid、非零周期和输入超时,例如 (uint32_t)(now_us-last_edge_us)>65000000u。长时间停转超过32位微秒整圈后,单靠该模差也会产生歧义。应用应在到达超时后锁存“已超时”,记下当时的 generation,至少等到两个新边沿形成新的一对才重新接受周期;不能过一圈后自动恢复旧转速,也不能把停转后的第一个边沿与很久以前的边沿配对。该前台状态还要求持续运行、能在超时后而32位微秒尚未整圈前观察到超时。
门控计数:TIM2_ETR 与100 ms窗口
跳转到“门控计数:TIM2_ETR 与100 ms窗口”原稿使用TIM2的外部时钟模式2,从默认映射PA0的ETR输入累计事件,再由TIM4提供100 ms窗口。假定TIM4内核72 MHz,PSC=7199、ARR=999,更新周期为:
ETR有极性、预分频和滤波配置;输入最高频率还受引脚、电气特性、采样和滤波限制。不能只根据CPU主频声称可以计任意快的脉冲。
“读取CNT,再写CNT=0”之间可能恰好到达一个边沿,导致漏计。更合适的做法是保持自由运行,在连续窗口边界读取快照,并对计数值做模减法。16位计数器要求一个观测窗口内的真实事件数少于65536;这只是避免模数歧义的条件,不是ETR硬件允许频率的保证。
如果边界由CPU在TIM4 ISR中读取,则ISR延迟抖动会改变实际窗口。应测量真实时间间隔,或使用定时器连接、捕获/DMA等硬件方式锁定边界,再按该机制分析同步误差。
计数法的量化也要写进结果。1事件/转、100 ms窗口时,一个计数就对应600 RPM,显然不适合精细测1 RPM。增加每转事件数、延长窗口或切换周期法,才能改善低速分辨率;代价是响应速度或硬件条件变化。
正交编码器:方向与四倍频
跳转到“正交编码器:方向与四倍频”编码器模式由定时器同时处理A/B两相。F1的TI12模式利用两相边沿,可得到每个A相完整周期4个计数;“上升沿”极性配置并不意味着整个TI12模式仅计算每相上升沿。方向还取决于A/B接线、输入反相和模式设置。
假设编码器每转A相有1024个完整周期,TI12得到4096 counts/rev。在100 ms内计数增加2048,则机械转速为:
需要初始化两个输入通道的滤波/极性,不能只调用默认选择CH1的输入结构体初始化后就认为CH2滤波也已设置。还要配置GPIO/TIM时钟、输入映射、ARR和计数模式;索引Z脉冲或回零步骤才可能建立绝对位置参考,A/B累计本身只提供相对位移。
短时间内正反向跨回绕可用有符号模差,但必须保证真实位移绝对值小于半个计数范围。以下函数避免依赖把超范围无符号数直接转换成 int16_t 的实现细节:
#include <stdbool.h>#include <stdint.h>
bool encoder_delta16(uint16_t before, uint16_t after, int32_t *delta){ if (delta == 0) { return false; } uint16_t raw = (uint16_t)(after - before); if (raw == 0x8000u) { return false; /* exactly half range: direction is ambiguous */ } *delta = (raw < 0x8000u) ? (int32_t)raw : (int32_t)raw - 65536; return true;}函数无法从两个16位数检测所有超范围运动。若实际正向移动40000计数,它会与较小的反向位移混淆;采样间隔和最大转速必须先保证半范围条件。
可独立验证的换算函数
跳转到“可独立验证的换算函数”下面的函数只做数值换算。输入事件数/转必须已经包含捕获边沿选择、外部预分频或编码器倍率;不合法输入返回false,不用0 RPM掩盖分母错误。
#include <stdbool.h>#include <stdint.h>
bool rpm_from_period(uint32_t period_ticks, uint32_t tick_hz, uint32_t events_per_turn, double *rpm){ if (period_ticks == 0u || tick_hz == 0u || events_per_turn == 0u || rpm == 0) { return false; } *rpm = 60.0 * (double)tick_hz / ((double)period_ticks * (double)events_per_turn); return true;}
bool rpm_from_gate(uint32_t events, uint32_t gate_us, uint32_t events_per_turn, double *rpm){ if (gate_us == 0u || events_per_turn == 0u || rpm == 0) { return false; } *rpm = 60000000.0 * (double)events / ((double)gate_us * (double)events_per_turn); return true;}将乘法提升到浮点后进行,避免在32位整数中先计算 60*1000000、events*60000000 等时发生溢出。控制系统若需要定点结果,应明确采用的整数位宽、缩放与舍入方式。
验证步骤
跳转到“验证步骤”先用信号发生器或已核对的另一计时输出产生已知频率,再同时观察输入波形、捕获周期、更新次数和过捕获计数。至少覆盖一次回绕、多个回绕、停止后重新起转、最小/最大速度、噪声毛刺和ISR被延后等情况。
周期法应记录计数时钟误差和时间戳量化;门控法应记录窗口时长和边界计数不确定性;编码器应验证正反转与计数倍率。串口打印在主循环/任务中完成,不能让打印开销反过来改变高频中断服务能力。
常见启动文件已调用 SystemInit(),主函数不应在不知道启动流程的情况下重复调用。未给出的串口和延时函数也应作为工程依赖列明。时间基准与延时的其他边界见 STM32 延时设计。