跳转到内容
新建笔记

STM32F103 转速测量:周期捕获、门控计数与编码器

转速测量先把传感器信号转换为“每转有多少个被计数的事件”,再决定测两个事件之间的时间,还是在固定时间内累计事件。低速时周期法通常更有分辨率;高速时门控计数法更容易平均;需要方向和位移时使用正交编码器。

本页以 STM32F103、内部定时器时钟 72 MHz 为具体示例。原稿使用 STM32F10x 标准外设库(SPL);下面保留其引脚和配置思路,用 CMSIS 寄存器代码说明修正后的捕获边界。SPL、HAL 和 CMSIS 的初始化代码不能不加区分地混在同一套工程中。

设 PP 为每转实际进入本次计算的事件数。单路传感器每转输出一个完整脉冲、只捕获上升沿时,P=1P=1;若每转有 20 个上升沿,P=20P=20。编码器铭牌中的 PPR/CPR 定义并不统一,必须确认它指单相周期、边沿数还是已四倍频的计数。

周期法测得相邻事件间隔 Δt\Delta t:

n=60PΔt=60fCNTPΔCRPM.n=\frac{60}{P\Delta t} =\frac{60f_{\mathrm{CNT}}}{P\Delta C}\quad\mathrm{RPM}.

门控计数法在实际观测时间 TgT_g 内取得 NN 个事件:

n=60NPTgRPM.n=\frac{60N}{PT_g}\quad\mathrm{RPM}.

这两个公式中的事件定义必须一致。漏边沿、重复边沿、滤波延迟或错误的 PP 都会改变结果,不能靠最后乘一个修正系数掩盖。

F103 默认映射中可用 PA6 接 TIM3_CH1。原 SPL 思路对应如下配置;具体封装、AFIO 重映射和输入电平仍需检查。

项目例子目的
时钟GPIOA、TIM3;如需重映射再使能 AFIO使引脚和定时器工作
输入PA6,选择符合传感器输出的浮空/上拉/下拉开漏输出需要合适上拉;不能对所有传感器套同一种输入方式
PSC71,假定 TIM3 内核72 MHz得到1 MHz计数,即1 µs/tick
ARR6553516位完整计数范围
CH1Direct 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,更新周期为:

7200×100072×106=0.1 s.\frac{7200\times1000}{72\times10^6}=0.1\ \mathrm{s}.

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,则机械转速为:

n=60×20484096×0.1=300 RPM.n=\frac{60\times2048}{4096\times0.1}=300\ \mathrm{RPM}.

需要初始化两个输入通道的滤波/极性,不能只调用默认选择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 延时设计。