☰
STM32F103编码器程序:定时器正交解码实现位置与速度测量
2026/10/15 17:17:36 网站建设 项目流程

简介:STM32F103编码器程序是一份面向嵌入式开发者的工程源码包,以增量式编码器为核心,解决信号采集、方向判读与位置计数问题。代码基于STM32标准外设库编写,覆盖定时器输入捕获配置、编码器模式初始化、中断服务函数及滤波思路,适合电机控制、机器人定位和自动化设备等场景。资源共934个文件,以C源文件和H头文件为主,辅以汇编启动文件、工程配置、脚本、链接文件与说明文档,RAR压缩包约9.99MB,结构清晰便于查阅。目前已有2330人学习下载,适合有一定单片机基础、正在调试编码器读数或希望提升运动控制精度的开发者。通过阅读这套工程,可获得硬件连接参考、配置示例和调试要点,结合自身硬件调整分频、倍频与滤波策略,能有效减少丢步和抖动干扰。

1. STM32F103 编码器程序:从正交信号到位置与速度的一条最短路径

做小车、云台或转台时,编码器读数忽正忽负、高速丢脉冲、反转计数错乱——这类问题在STM32F103上九成不是编码器坏了,而是定时器的编码器接口模式没有用对。F103 的 TIM2/TIM3/TIM4 自带硬件正交解码,A/B 两相直接接定时器通道,配置成编码器模式后,方向由硬件判,计数由硬件记,CPU 只负责定时读 CNT 和做溢出扩展。这套资源就是一份最小可跑的编码器工程:从引脚映射到初始化代码,从 16 位 CNT 回绕到 32 位位置扩展,再到 M 法测速和常见翻车场景。适合做电机闭环的开发者、控制类课设复现,也适合从串口读数阶段往闭环阶段走的入门者照着抄。

2. 编码器模式原理与选型:为什么用 TIM 而不是 EXTI

2.1 正交解码的硬件逻辑:边沿计数与方向判定

增量式编码器输出 A 相和 B 相,两路方波相位差 90 度。正转时 A 超前 B,反转时 B 超前 A。要解出"有没有走一步"和"往哪个方向走",只需要抓一个组合:某一相出现有效边沿时,另一相的电平是高还是低。

这个组合逻辑用外部中断做,就是每次 A 相上升沿进中断,在中断服务程序里读一次 B 相电平,再决定位置加一还是减一。低速演示没问题,转速一起来,中断响应延迟和沿抖动会叠加出错。我在早期项目里就遇到过一次:手转读数正常,电机一带负载,正转偶尔变成负数,查了半天发现是 EXTI 优先级跟毛刺在互相干扰。从那以后编码器计数我优先走定时器硬件,不再用 EXTI 做正交判相。

STM32F103 的 TIM2/TIM3/TIM4 内置编码器接口,CH1 输入映射为 TI1,CH2 输入映射为 TI2。编码器模式下,硬件逻辑在每个有效边沿自动比较另一通道电平,CNT 寄存器随之加一或减一,不进中断、不占 CPU。数据手册里这个机制叫"编码器接口模式",对应 TIM_SMCR 寄存器的 SMS[2:0] 字段,一共有三种计数沿配置:

SMS 值计数来源分辨率倍数工程里什么时候用
001TI1 边沿1 倍频只需要方向、不追求分辨率
010TI2 边沿1 倍频同上,换了一路输入
011TI1 与 TI2 全部边沿4 倍频默认推荐,分辨率最高

工程里默认用 011 即 4 倍频。同样是 360 线编码器,1 倍频一圈 360 个计数,4 倍频一圈 1440 个计数,分辨率直接乘 4,而且四个沿都参与计数,对 A/B 占空比不敏感。代价是计数频率翻四倍,16 位 CNT 回绕更快,溢出扩展必须跟上,这部分在第 4 章展开。

标准库里选模式就一行调用:

TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12, // 011: 双沿计数, 4倍频 TIM_ICPolarity_Rising, // TI1 极性 TIM_ICPolarity_Rising // TI2 极性 );

逻辑说明:TIM_EncoderMode_TI12 对应 SMS=011,使能双沿计数。后两个参数是输入捕获极性,默认 Rising。如果 A/B 两相实际接反了,把这两个参数都改成 TIM_ICPolarity_Falling,计数方向就会反过来,等效于交换 A/B 两根线,不用改硬件。

参数说明:模式参数还有 TIM_EncoderMode_TI1 和 TIM_EncoderMode_TI2,对应 1 倍频,只在特殊分辨率需求下用。极性参数是软件层面解决接线反相的后悔药,比起拆电机线、剥线头,改这一行快得多。

2.2 倍频与线数的匹配:360 线、512 线、1000 线怎么配

先澄清一个常见误解:CNT 里存的只是脉冲累计值,不是圈数,更不是角度。物理单位要靠软件换算,换算公式是:

一圈增量 = 编码器线数 P/R × 倍频倍数 × 减速比

常用编码器线数在不同倍频下每圈计数如下:

编码器线数1 倍频(每圈计数)2 倍频4 倍频
360 线3607201440
512 线51210242048
1000 线100020004000

带减速箱的电机还要再乘减速比。比如电机本体 1000 线、减速比 1:30、4 倍频下,输出轴转一圈需要 1000 × 4 × 30 = 120000 个计数。这个数字直接决定溢出扩展的频度:每秒转一圈就产生 12 万次计数,16 位 CNT 半秒多就回绕一次,不做 32 位扩展根本没法用。

我的习惯是把这些换算常量收敛在头文件里,不要散写在多处。换编码器、换减速比时只改一处:

#define ENCODER_PPR 1000U // 编码器线数 #define ENCODER_MULT 4U // 4倍频 #define GEAR_RATIO 30U // 减速比 #define COUNTS_PER_REV (ENCODER_PPR * ENCODER_MULT * GEAR_RATIO) #define DEG_PER_COUNT (360.0f / (float)COUNTS_PER_REV)

逻辑说明:COUNTS_PER_REV 是输出轴转一圈对应的总计数,DEG_PER_COUNT 是每个计数对应的角度增量。角度换算时直接用计数值乘以 DEG_PER_COUNT 即可,避免每处都写除法。

参数说明:ENCODER_PPR 是编码器厂商标称线数,不是倍频后的数。ENCODER_MULT 与第 2.1 节 TIM_EncoderMode_TI12 对应,如果改成 1 倍频,这里同步改成 1。GEAR_RATIO 是输出轴与电机轴的减速比,不带减速箱写 1。

2.3 EXTI 软判相:只适合低速演示,不适合闭环

既然说 EXTI 容易踩坑,就把反面代码摆出来对比着看。网上流传的"最简单编码器程序"大多是这种写法:

// 不推荐的做法:A相上升沿触发 EXTI,B相电平判方向 void EXTI0_IRQHandler(void) { if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1)) // 读B相电平 { position++; // B相为高, 判定正转 } else { position--; // B相为低, 判定反转 } }

逻辑说明:这个逻辑在低速下是正确的,A 相上升沿时 B 相电平正好反映方向。问题在于 A 相上升沿附近如果 B 相存在毛刺,或者中断响应延迟导致读到的电平已经是边沿后的下一拍,方向就会判反。转速越高、中断优先级越低,出错概率越大。

参数说明:这个方案里 position 必须用带符号类型,否则反转时减一变成 65535,串口打印出来就是一个疯狂增大的正数。另一点是边沿选择:用上升沿还是下降沿只影响采样点位置,不改变方向判定的本质缺陷。所以这个方案我只建议在手动旋转的演示项目里用,电机闭环直接上 TIM 编码器模式。

3. 最小可跑工程:GPIO 映射、初始化与位置读取

3.1 引脚映射、JTAG 复用与上拉选择

STM32F103 的编码器模式优先用默认引脚映射,避免重映射引入额外配置:

定时器CH1(A 相)CH2(B 相)总线
TIM2PA0PA1APB1
TIM3PA6PA7APB1
TIM4PB6PB7APB1

这三个组合里最常用 TIM2,原因很实际:PA0/PA1 没有 JTAG 占用,不需要关 SWJ 映射,拿来就能用。TIM4 的 PB6/PB7 也不涉及 JTAG,做电机板时经常和 PWM 输出引脚排在一起,布线方便。

要避开的是 PA15、PB3、PB4 这几个脚。PA15 是 JTDI,PB3 是 JTDO,PB4 是 JNTRST,默认被 JTAG 占用,直接当普通 IO 用会失灵。如果板子已经把编码器接到了这些引脚,使用前要关闭 JTAG:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // 重映射功能时钟 GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); // 关闭JTAG, 保留SWD

逻辑说明:GPIO_Remap_SWJ_JTAGDisable 关闭 JTAG 后,PA15/PB3/PB4 恢复普通 IO 功能,同时 SWCLK 和 SWDIO 仍然可用,下载调试不受影响。

参数说明:如果项目里还要用 SWO 调试口,需要改成 GPIO_Remap_SWJ_Disable,这个宏会把 SWO 也释放掉,按需选择。

上拉选择方面,多数增量编码器内部输出是开漏或推挽开路结构,MCU 侧最好启用内部上拉。GPIO 配置成 GPIO_Mode_IPU,省掉外部上拉电阻。长线传输(超过 30cm)建议外部再加 4.7kΩ 上拉到 3.3V,原因在第 5 章避坑里细说。

3.2 标准库初始化代码:从时钟到编码器接口

以 TIM2、PA0/PA1 为例,标准库完整初始化函数如下:

void Encoder_TIM2_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; // 1. 开启时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); // 2. PA0/PA1 配置为输入, 内部上拉 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 3. 时基: 预分频0, 自动重载0xFFFF TIM_TimeBaseStructure.TIM_Period = 0xFFFF; TIM_TimeBaseStructure.TIM_Prescaler = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); // 4. 编码器接口: 双沿计数 TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); // 5. 输入滤波, 滤掉毛刺 TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; TIM_ICInitStructure.TIM_ICFilter = 0x0F; TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; TIM_ICInit(TIM2, &TIM_ICInitStructure); // 6. 清空计数器并启动 TIM_SetCounter(TIM2, 0); TIM_Cmd(TIM2, ENABLE); }

逻辑说明:第 3 步里 TIM_Prescaler 必须为 0,编码器模式下 CNT 直接对有效沿计数,预分频器不参与,改大了只会让溢出事件与 CNT 回绕不同步。TIM_Period 保持 0xFFFF,不要在编码器模式下通过改小 ARR 来加快溢出,那会直接丢计数。第 5 步的 TIM_ICFilter 是输入滤波采样次数,0x0F 表示连续采样 15 个周期确认电平稳定,能滤掉机械抖动引入的毛刺,代价是引入约 0.2 微秒的边沿延迟,对绝大多数编码器场景没有影响。

参数说明:TIM_ICFilter 取值范围 0 到 0x1F,值越大滤波越强。对线数高、转速快的场合建议降低到 0x04 以下,对线数低、现场干扰大的场合提高。这个值属于"按现场调"的参数,没有绝对最优。

初始化完成后 CNT 从 0 开始,正转加计数,反转减计数。编码器模式下 CNT 是 16 位无符号数,减到 0 会跳到 0xFFFF,这是后面要处理的回绕问题。

3.3 读取位置与方向:CNT 与 DIR 位

读取当前原始计数值:

uint16_t Encoder_GetRawCount(void) { return (uint16_t)TIM2->CNT; }

读取方向:

uint8_t Encoder_GetDirection(void) { return (TIM2->CR1 & TIM_CR1_DIR) ? 1 : 0; // 1: 向下计数(反转) 0: 向上计数(正转) }

逻辑说明:编码器模式下 CR1 寄存器的 DIR 位由硬件自动更新,向上计数时为 0,向下计数时为 1。这个位可以用于逻辑判断,比如方向变化时记录换向时刻。注意不推荐用 DIR 位做位置正负修正,位置本身已经通过 CNT 增减体现方向了。

参数说明:DIR 位是 1 位只读状态,初始化后第一次读取可能不稳定,建议在系统上电 1 毫秒后再使用。CNT 是 16 位,调试时用串口打印要小心:直接打印 uint16_t 会显示 0 到 65535,反转时从 0 减到 65535 是正常回绕,不是故障。

3.4 把计数值算成角度和圈数

有了原始计数,换算到物理量是这层:

float Encoder_GetAngle_Deg(void) { int32_t count = Encoder_GetPosition(); // 32位扩展位置, 见第4章 return (float)count * DEG_PER_COUNT; } float Encoder_GetSpeed_Rpm(float delta_sec) { int32_t now = Encoder_GetPosition(); static int32_t last = 0; int32_t delta = now - last; last = now; return (float)delta / (float)COUNTS_PER_REV / delta_sec * 60.0f; }

逻辑说明:角度换算用乘法替代除法,因为 DEG_PER_COUNT 已经预处理好。速度换算通过两次采样位置差除以时间窗口实现,delta 为负数表示反转,这个方式在固定时间窗口下就是后面要讲的 M 法测速。

参数说明:delta_sec 是调用周期,用秒做单位。在定时中断里每 10ms 调用一次就传 0.01f。静态变量 last 是函数级保持,工程上建议改成结构体或全局变量,方便多路编码器复用。

4. 高速不丢脉冲:溢出中断、32 位位置扩展与读取竞态

4.1 16 位 CNT 回绕:转速一高就得面对

CNT 是 16 位计数器,最大计数值 65535。编码器模式下向上计数到 0xFFFF 后下一个沿会溢出回 0,向下计数到 0 后会下溢回 0xFFFF,溢出瞬间产生更新事件。如果中断处理不及时,位置信息就丢了一整段。

回绕速度跟编码器线数、倍频、转速直接相关。举两个实际场景:

  • 360 线、4 倍频、电机 3000 转/分,每秒计数 1440 × 50 = 72000,CNT 约 0.9 秒回绕一次
  • 1000 线、4 倍频、减速比 1:30、输出轴 60 转/分,每秒计数 4000 × 30 × 1 = 120000,CNT 约 0.55 秒回绕一次

第二种场景在云台、机械臂关节里很常见。位置一旦被截断,轻则角度计算跳变,重则 PID 积分直接飞掉。所以只要电机带减速箱,溢出扩展就是必须项。

4.2 溢出中断扩展位置:硬件计数加软件进位

扩展思路跟十进制进位一样:CNT 负责段内计数,软件维护一个 32 位全局变量负责进位。每次溢出时判断方向,向上溢出就加 65536,向下溢出就减 65536。

volatile int32_t g_encoder_count = 0; // 32位累计位置 void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); if (TIM2->CR1 & TIM_CR1_DIR) // 向下计数, 下溢从0跳到0xFFFF { g_encoder_count -= 65536; } else // 向上计数, 溢出从0xFFFF跳到0 { g_encoder_count += 65536; } } }

逻辑说明:中断里不读 CNT,只做加减进位,这是刻意为之。CNT 的回绕由硬件完成,软件在溢出发生的瞬间同步进位,两者天然一致。方向用 DIR 位判断,避免了用计数值符号推断方向带来的边界误差。65536 来自 ARR + 1,因为当前配置 ARR=0xFFFF。

参数说明:g_encoder_count 必须声明为 volatile,防止编译器优化后读取不到最新值。中断服务函数名称 TIM2_IRQHandler 是启动文件里固定好的,不要自己改名。更新事件标志位必须在判断后立即清除,否则中断退出后立刻再次进入,形成死循环。

使能更新中断的代码放在初始化函数末尾:

NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; // 低于电机控制中断 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);

逻辑说明:抢占优先级建议比电机换相/PWM 中断低一级,避免编码器进位打断高实时性控制。进位操作本身只执行几拍,丢一点不影响大局。

参数说明:优先级具体数值要根据项目里已有中断分配来定,原则是编码器中断不能让电机堵转检测这类任务延迟太久。

有了 g_encoder_count 之后,读取真实位置不能直接返回它,因为两次溢出之间的段内增量还留在 CNT 里。合并函数见下一节。

4.3 读取顺序的竞态:先关中断再读

直接返回 g_encoder_count 会漏掉最近一次溢出到当前时刻之间的增量。正确合并方式是 g_encoder_count 加当前 CNT:

int32_t Encoder_GetPosition(void) { int32_t pos; __disable_irq(); // 关中断, 防止读一半发生溢出 pos = g_encoder_count + (int32_t)(uint16_t)TIM2->CNT; __enable_irq(); return pos; }

逻辑说明:g_encoder_count 是已完成进位的粗计数,CNT 是当前段内细计数,相加得到完整的 32 位位置。举例:CNT=60000 时还没有溢出,g_encoder_count=0,返回 60000;溢出发生后 g_encoder_count=65536,CNT=0,返回 65536;再转 1000 计数,返回 66536。连续且无跳变。

参数说明:__disable_irq 是全局关中断,如果在中断上下文调用会导致嵌套中断无法响应,所以这个读取函数只在任务级代码里使用。相比用信号量或原子操作,全局关中断在这个场景最简单,因为临界区只有三四条指令,关中断时间不超过 1 微秒。

这里还有一个隐藏细节:如果读取时 CNT 刚好在 0xFFFF 且下一个沿即将到来,关中断挡住溢出,位置会暂时停在当前值,开中断后溢出中断执行,下一次读取就正确。结果是最多偏差 1 个计数,且不会累积,实际工程完全可接受。

5. 避坑:编码器程序里最常翻车的五个场景

5.1 转一圈计数差几个脉冲:先查机械再查滤波

现象:360 线编码器 4 倍频,手转 20 圈,平均每圈 1438 到 1442,偶尔出现 1430。

原因:这个现象有三种常见来源。机械间隙最常见,联轴器或齿轮回差导致反向瞬间损失计数;其次是信号占空比不是 50%,某些沿间距不均匀,硬件状态机偶尔漏采;最后是 TIM_ICFilter 滤波值设太大,把有效沿当毛刺滤掉了。

解决:按顺序排查。先手动缓慢正反转观察:反向切换瞬间丢几个计数,基本是机械回差,拧紧联轴器。再用示波器看 A/B 波形是否干净、占空比是否 50%,波形畸变就处理编码器供电和安装。最后软件调参,把 TIM_ICFilter 从 0x0F 降到 0x02 试一轮。注意滤波值不是越大越好,过滤波和欠滤波的表现都是计数不准。

5.2 正反转读数都增加:多半是无符号类型在捣鬼

现象:不管向哪边转,串口打印的位置数值都在增大,反转时数值也一路往上冲。

原因:这是最阴间的错误之一。位置变量声明成了 uint16_t 或 uint32_t,反转时 CNT 从 0 变成 0xFFFF,无符号数解释为 65535,打印出来就是"增大了"。本质是负数被无符号类型截断了。

解决:位置相关变量全部改用 int32_t,包括中断里的 g_encoder_count、读取接口的返回值、换算函数的入参。不要在读取后再强转成无符号类型。调试时串口打印用 %ld 而不是 %d,ST 的 printf 重定向默认对 int16_t 截断,踩过这个坑的都知道打印值突然跳到 65535 有多困惑。

5.3 悬空引脚读数乱跳:内部上拉没开

现象:手不碰编码器,串口读数每隔几十毫秒跳变 1 到 3 个计数,方向随机。

原因:编码器内部输出级是开漏结构,MCU 侧 GPIO 配置成浮空输入时,引脚电平没有被稳定钳位,微弱电磁噪声就能触发边沿。特别是编码器线比较长且没有终端处理时,悬空引脚就像一根天线。

解决:GPIO 初始化从 GPIO_Mode_IN_FLOATING 改成 GPIO_Mode_IPU,启用内部上拉。长线场景在编码器端和 MCU 端各加一个 4.7kΩ 上拉到 3.3V。改完再看读数,绝大多数情况下乱跳直接消失。如果还在跳,检查编码器外壳是否接地。

5.4 电机通电后读数乱跳:EMI 干扰要硬件软件一起治

现象:手转编码器读数稳定,电机一上电,读数立刻像抽风一样乱跳。

原因:电机电刷或 PWM 换相产生强烈的电磁干扰,通过电源线、地线或空间辐射耦合进编码器信号线。这个在带直流电机的小车上几乎必现,只是严重程度不同。

解决:分三层处理。第一层硬件,编码器线换屏蔽双绞线,屏蔽层单端接地,编码器供电从电机电源分离出来,单独用 LDO 降到 3.3V。第二层滤波,编码器信号线上加 100nF 电容到地,引脚侧形成低通,同时把 TIM_ICFilter 加大到 0x0F 以上。第三层隔离,如果干扰还是压不住,换差分输出编码器,MCU 侧通过比较器或电平转换芯片接收。注意滤波和速度的平衡:滤波越大,边沿延迟越大,高速场景会丢沿,所以先硬件治理再软件滤波。

5.5 PB3/PB4 不工作还烧录失败:JTAG 占用要先关闭

现象:把编码器 A/B 相接到 PB3/PB4,引脚电平完全不受控制,程序下载偶尔失败,调试器连接不稳定。

原因:PB3 默认是 JTDO,PB4 默认是 JNTRST,JTAG 功能在芯片上电后默认使能,这两个引脚被调试逻辑占用,普通 GPIO 操作无法控制电平。下载失败的典型表现是连接目标时报告 JTAG 通信错误。

解决:在初始化最前面关闭 JTAG 功能:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);

逻辑说明:关闭 JTAG 后 PA15/PB3/PB4 转换回普通 IO,SWD 调试口不受影响。如果你的调试器用的是 SWD 模式,这行代码加上后一切正常。如果用的是 JTAG 模式,关了之后就连不上了,需要用 SWD 下载一次把代码擦掉或改回。

参数说明:GPIO_Remap_SWJ_JTAGDisable 释放 PA15、PB3、PB4;GPIO_Remap_SWJ_Disable 进一步释放 PB3 用作 SWO。只用到普通 IO 时建议用前者,保留 SWO 调试输出。

6. 进阶:M 法测速、断电回零与编码器模式最后的细节

6.1 M 法测速:10ms 窗口读增量

M 法测速本质是固定时间窗口内统计计数增量,适合中高速场景。窗口太短,低速时计数抖动占比大;窗口太长,速度反馈延迟大。工程上 10ms 是折中值。

void Speed_Task_10ms(void) { int32_t now_pos = Encoder_GetPosition(); static int32_t last_pos = 0; int32_t delta = now_pos - last_pos; last_pos = now_pos; // 速度: 计数/秒 float speed_cps = (float)delta * 100.0f; // 再换算成转/分 float speed_rpm = speed_cps / (float)COUNTS_PER_REV * 60.0f; }

逻辑说明:delta 是 10ms 内的位置变化量,乘以 100 得到每秒计数。speed_cps 除以 COUNTS_PER_REV 得到每秒圈数,再乘 60 就是 RPM。delta 为负表示反转,速度值带符号可以直接喂给 PID。

参数说明:这个任务放在定时器中断里调用,或者放在 RTOS 的 10ms 周期任务里。不要用 delay 函数软延时循环,时间抖动会让速度计算带上噪声。低速场景下如果 delta 经常为 0,可以考虑把窗口拉长到 50ms,或者换成 T 法测速。

6.2 断电位置丢失:增量编码器的回零方案

增量编码器只有相对位置,断电后重新上电,当前位置不知道在哪。这不算程序 Bug,是传感器原理决定的天花板。

解决手段按成本递增:第一,用带 Z 相(索引脉冲)的编码器,上电后低速转一圈捕捉 Z 脉冲作为机械零点。第二,在机构上加限位开关或霍尔挡块,上电后先运行到限位位置再清零。第三,换绝对值编码器,位置信息由编码器自身保存。

配套软件上电清零函数:

void Encoder_ResetPosition(void) { __disable_irq(); g_encoder_count = 0; TIM_SetCounter(TIM2, 0); __enable_irq(); }

逻辑说明:清零操作必须在关中断下完成,否则清零过程中溢出中断插入,会把残留计数值带入 g_encoder_count。TIM_SetCounter 在编码器模式下可以直接写 CNT,硬件会在下一个沿恢复计数。

6.3 最后的细节:ARR 保持 0xFFFF,PSC 必须为 0

编码器模式有两个寄存器设置容易被忽略,但它们直接决定 32 位扩展逻辑是否成立。

第一个是 ARR。有开发者想让 CNT 更快溢出以增加进位频率,把 ARR 改成比如 60000。后果是计数到 60000 就"提前翻转"回 0,而硬件根本不认为这是溢出,中断里的 g_encoder_count 不会进位,位置直接丢 6000 段。更隐蔽的是,997 线和 1000 线编码器在这种配置下表现完全不一致,很难排查。ARR 固定在 0xFFFF,让 CNT 走满 16 位自然回绕,才是扩展逻辑的前提。

第二个是 PSC。编码器模式下 CNT 的递增直接由输入沿驱动,预分频器不参与计数,但更新事件周期与 PSC 有关。一旦 PSC 不为 0,更新中断与 CNT 回绕不同步,进位时机错位,位置扩展就出随机跳变。所以 PSC 保持 0。

从那以后我每次配编码器工程,都会强制走一遍检查:先确认 ARR=0xFFFF、PSC=0,再审一遍读取接口有没有关中断,最后用方向盘逻辑手转几圈验证正反向。这三步花不了两分钟,但能拦下九成的位置跳变问题。整套 stm32f103 编码器程序代码就在资源包里,直接下下来对照调试,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询