STM32定时器编码器模式详解:替代外部中断的可靠电机测速方案
2026/9/7 6:11:36 网站建设 项目流程

简介:面向STM32F103嵌入式开发者的编码器程序工程,主要解决增量式编码器位置与速度采集问题,适合电机控制、机器人定位等运动控制场景。压缩包共934个文件,约9.99MB,以C源文件、头文件为主,并包含启动文件、链接脚本、工程配置文件及文本说明,便于在IAR或STM32CubeMX环境中直接打开与移植。程序覆盖TIM2/TIM3/TIM4编码器模式配置、A/B相脉冲计数、方向判断及中断服务,同时给出滤波与倍频处理思路,可帮助开发者快速搭建高精度运动控制基础。已有2329人学习本资源,对刚接触编码器接口或需要工程参考的开发者具备较好借鉴价值。 大家做电机测速、小车里程计或者云台角度反馈的时候,最容易想到的方案就是“外部中断数脉冲”——AB相各接一个中断引脚,来一个上升沿计一次数。我之前在调一个闭环项目时也这么干过,电机转速稍微拉上去一点,逻辑分析仪上就明显看到丢脉冲,转速一高计数器根本反应不过来。后来换成了STM32F103定时器自带的编码器接口模式,才算真正把这个问题解决掉。这篇文章就把我用标准外设库和CubeMX两种方式配置编码器测速的完整过程写清楚,包括寄存器原理、参数计算、测速公式、以及我在实际项目中踩过的几个坑,给正在做stm32f103编码器程序的朋友一份可以直接抄作业的参考。

1. 为什么编码器测速首选定时器接口模式,而不是外部中断数脉冲

1.1 外部中断为什么跟不上:从硬件机制说起

先别急着写代码,我们得搞清楚一个核心问题:为什么外部中断数脉冲在高速场景下不靠谱。

STM32F103的外部中断每条IO线都有对应的EXTI线,但中断服务程序执行是有开销的——压栈、判断标志位、读电平、清标志、出栈,一套下来几十个周期就没了。假如编码器是1000线,4倍频后每圈产生4000个脉冲,电机转速3000转/分时每秒就是20万个脉冲,平均5微秒一个脉冲。如果中断服务函数稍微写长一点,或者系统里还有别的中断在抢占优先级,丢脉冲几乎是必然的。

更麻烦的是方向判断。用外部中断数脉冲时,你得在A相上升沿去读B相电平,来判断正转还是反转。这个“读”的动作本身就有时序风险:如果读B相的时候正好赶上B相也在跳变,方向就判错了。我实测下来,低速还好,转速一高方向误判的比例明显上升。

1.2 编码器接口模式的核心:硬件自动判向与加减速

STM32F103的定时器编码器接口模式(Encoder Interface Mode)解决的就是这个问题。它把AB两相接入定时器的CH1和CH2引脚,由硬件内部的输入滤波、边沿检测和方向判断逻辑直接驱动计数器。AB相的相位关系决定了计数器的方向位(DIR),计数器自动加一或者减一,全程不需要CPU干预。

这个机制相当于把“数脉冲+判方向”这件最耗时的事情从软件搬到了硬件里。CPU要做的只是定期读取CNT寄存器,算出这一段时间内走了多少脉冲。我在项目里用定时器编码器模式接1000线编码器,电机跑到4000转/分,计数器纹丝不乱,逻辑分析仪抓波形也看不到丢步。

1.3 TI1和TI2的映射关系与4倍频来源

定时器编码器模式有三种配置:只计数TI1(A相)、只计数TI2(B相)、同时计数TI1和TI2。第三种模式下,AB相每个边沿都会触发计数,也就是通常说的4倍频——A上升沿、A下降沿、B上升沿、B下降沿各计一次。

4倍频的意义不只是分辨率翻四倍,更重要的是方向判断的可靠性。单倍频模式下,你只能在一个相位的边沿采样另一个相位的电平,误判风险高;4倍频模式下,每四分之一个周期就做一次方向确认,抗干扰能力强得多。所以在实际项目里,我基本都是用TIM_EncoderMode_TI12,也就是同时计数两相的模式。

2. CubeMX配置编码器模式:参数计算与最容易忽略的引脚设置

2.1 时钟使能与GPIO配置:上拉不是可选项

先说GPIO配置。编码器AB两相接到定时器的CH1和CH2引脚,比如TIM3的PA6和PA7,或者TIM4的PB6和PB7。这里有一个很多人会忽略的细节:GPIO必须配置为上拉输入

为什么必须上拉?因为增量编码器的输出通常是开漏或者推挽,如果是开漏输出,没有上拉电阻时信号高电平是浮空的,定时器采到的就是随机电平。即使你的编码器是推挽输出,我也建议接上拉——上拉能提升信号边沿的陡峭度,尤其是在线缆比较长的时候,对减少误计数有帮助。

在标准外设库里的配置长这样:

GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure);

注意,这里GPIO_Speed其实对输入模式没有实际意义,但保留它可以让代码风格统一,不影响功能。

2.2 CubeMX中编码器模式的参数填写逻辑

如果你用CubeMX生成HAL库代码,参数配置界面里选择定时器后,把Combined Channels设置为Encoder Mode,然后会看到几个关键参数:

  • Input Filter:输入滤波,这个值后面细说。我一般先填0x0F,后面根据实际情况调。
  • Polarity:TI1和TI2的极性。常用的是Rising,意思是两个边沿都计数。如果你发现方向反了,不需要改代码,把其中一个极性改成Falling就能反转方向。
  • Prescaler:预分频,测速场景填0,让计数器直接数编码器脉冲,否则会降低分辨率。
  • Period(ARR):自动重载值。这个参数是编码器模式最容易踩坑的地方,后面专门讲。

计数器溢出时间计算公式是:溢出时间 = ARR / 最大脉冲频率。假设你设ARR为65535,编码器4倍频后最高输出脉冲频率是100kHz,那65535/100000约等于0.65秒,也就是说最多0.65秒计数就会溢出。明白了这个关系,你就能根据自己项目的最大转速来选ARR,而不是随手填个65535就完事了。

2.3 滤波器设置与机械抖动的关系

编码器信号的抖动主要来自机械振动和接触不良。光学编码器还好,如果是自制码盘或者滑轨上的光栅,信号边沿经常带毛刺,毛刺会让计数器多计数。

硬件层面的对策是定时器的输入滤波。STM32的输入滤波本质是一个数字低通滤波器:连续采样N个时钟周期,如果电平保持一致才认为是有效跳变。ICFilter的取值范围是0到15,数值越大滤波越强,但也会增加信号延迟,导致高速时跟不上。

我实测的经验是:低频场景(电机转速不高、编码器线数少)滤波设大一点没问题;高频场景滤波设太大会丢脉冲。如果你不确定,先用0x0F跑一遍,再用逻辑分析仪看计数是否准确,逐步减小。

3. 标准库代码:编码器定时器的初始化与测速逻辑

3.1 基于标准外设库的完整初始化代码

我用的是STM32F103标准外设库V3.5,这版库在项目里还算常见。下面这段代码是经过实际项目验证的,可以直接抄:

void Encoder_Init_TIM3(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period = 65535; TIM_TimeBaseStructure.TIM_Prescaler = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_ICStructInit(&TIM_ICInitStructure); TIM_ICInitStructure.TIM_ICFilter = 0x0F; TIM_ICInit(TIM3, &TIM_ICInitStructure); TIM_ClearFlag(TIM3, TIM_FLAG_Update); TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); TIM3->CNT = 0; TIM_Cmd(TIM3, ENABLE); }

这段代码有几点要说明。TIM_CounterMode_Up在编码器模式下其实会被硬件覆盖,计数器方向由编码器相位自动决定,但保留这句可以让代码结构完整,也不会出错。TIM_EncoderInterfaceConfig里的极性如果设成Rising,就是4倍频计数;如果只想单倍频,可以配合TIM_ICInit配置只在上升沿计数。

3.2 溢出处理:方向与符号的坑

编码器模式下的溢出和普通定时器溢出不一样。普通定时器固定向上计数,溢出只发生在ARR处;编码器模式下计数器可能向上也可能向下,所以既可能从ARR溢出回0,也可能从0下溢到ARR。如果处理不对,测出来的位移就是错的。

中断处理函数里要判断方向位DIR:

void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); // 读取方向位:bit4是DIR if ((TIM3->CR1 & TIM_CR1_DIR) != 0) { overflow_count--; // 反转,溢出是向下溢出 } else { overflow_count++; // 正转,溢出是向上溢出 } } }

这里overflow_count是volatile int32_t类型的全局变量。每次读取位置的时候,把当前CNT和overflow_count整合成一个32位甚至更宽的位置值。注意,读取过程最好先关中断,防止读到一半溢出中断进来把overflow_count改了,导致数据错乱。用临界区保护一下,代码虽然多一点,但可靠性大大提高。

3.3 测速公式怎么选:M法、T法还是M/T法

测速本质上是在一段时间内统计脉冲数,但不同的统计策略适用场景不同。

  • M法(固定时间计数):每隔固定时间读一次CNT差值,除以时间得到速度。在高速场景,时间固定,计数较大,精度高;低速时一个周期内可能只有几个脉冲,量化误差明显。
  • T法(测脉冲周期):测量相邻两个脉冲的时间间隔,用定时器时基计算。低速场景精度高,高速时脉冲间隔太短,误差变大。
  • M/T法:结合两者,适合宽范围测速,但代码复杂度高一些。

我常用的做法是:对一般的电机控制,M法100ms采样周期足够;对需要快速响应的电流环或速度环,采样周期压低到10ms到20ms。用M法测速时,速度计算代码可以封装成这样一个函数:

int32_t Encoder_GetSpeed(int32_t *last_count, uint32_t interval_ms) { int32_t curr_count = (int32_t)((overflow_count << 16) + TIM3->CNT); int32_t delta = curr_count - *last_count; *last_count = curr_count; // 这里根据编码器线数和倍频换算成RPM return delta; }

拿到delta之后,再根据编码器物理线数换算成实际转速。换算公式在下一节详细展开。

4. 四倍频、方向判断与Z相回零:几个容易翻车的细节

4.1 四倍频计算的正确姿势:线数与倍频

增量编码器标称的“线数”指的是码盘一圈的物理刻线数。比如1000线编码器,AB两相每圈各输出1000个脉冲,4倍频后每圈计数是4000。

你把TIM3->CNT读出来,这个数并不是“圈数”,而是“脉冲计数”。转速的换算公式是:

转速(RPM) = 脉冲差值 × 60 / (采样时间秒数 × 每圈脉冲数)

举例:1000线编码器,4倍频,每圈脉冲数4000。100ms采样一次,测到脉冲差值400个,那么转速 = 400 × 60 / (0.1 × 4000) = 60 RPM。

这里容易犯的错是把“线数”直接当“每圈脉冲数”,忘记乘倍频系数。如果你配置成TI12模式(即4倍频),公式里必须写4000而不是1000。还有一种常见的错误是单位混乱——采样时间是毫秒直接代入公式,算出来的转速差了1000倍。建议统一用秒做单位。

4.2 方向判断:什么时候DIR不可信

编码器模式下,TIMx_CR1的DIR位可以实时反映当前旋转方向,但要注意:在两个相位转换的瞬间,DIR可能发生瞬间抖动。尤其是在电机静止但存在微小振动时,DIR会来回翻转,看起来像计数器在来回跳动。

如果你需要稳定的方向信息,我建议不要直接读DIR位,而是自己维护一个方向标志。方法是在定时器中断里记录上一次的CNT值,和当前CNT比较,连续若干次都在增加就认为正向,连续减少就认为反向。这样虽然滞后一点,但方向输出的稳定性好得多。

如果你的系统里同时用到了编码器定时器的Slave模式(比如触发ADC采样),那DIR位的含义和变化时机要查一下参考手册,别想当然。我最初以为DIR位是瞬时方向,后来发现它在特定模式下会延迟一个计数周期才更新,导致方向判断和实际动作对不上,排查了很久。

4.3 Z相回零:前提、接线与软件配合

增量编码器没有绝对位置,重启后计数器归零,机械位置却可能在任何地方。要做绝对定位就得靠Z相——编码器每转一圈输出一个Z脉冲,用来标记机械零位。

Z相该怎么接?如果定时器还有多余的输入捕获通道,可以接上去做捕获中断;更简单的做法是接一个外部中断引脚,比如PA0对应的EXTI0。回零的流程一般是:上电后让电机朝固定方向匀速转,等待Z相中断到来,在中断里把TIM3->CNT清零,同时标记“零位已找到”。之后所有位置计算都以这个零位为基准。

实际操作中有几个细节。第一,Z相必须和AB相同一个电源域,如果编码器是5V供电而单片机是3.3V,Z相输出必须做电平转换或分压,不然可能烧引脚。第二,Z相脉冲宽度通常很短,中断里只做清零和置标志,不要做耗时操作,否则下一个Z脉冲来的时候可能还没处理完。第三,回零完成后记得关闭Z中断或加一个“已回零”标志判断,避免正常工作过程中再次触发Z中断把计数器清了。如果你想利用Z脉冲做圈数统计,那就在中断里让圈数变量加一,而不是清CNT。

5. 实测踩坑记录:抖动计数、初始化误动和24位溢出

5.1 上电瞬间计数器乱跳:为什么初始化顺序很重要

上电瞬间编码器还没有稳定输出,电源电压爬升过程中信号线上会有随机毛刺。如果此时定时器已经开始工作,计数器就会被这些毛刺灌入一大堆无意义的脉冲。我在一次电机调试中就遇到过:刚上电还没发指令,编码器计数就已经是几千了。

解决思路是调整初始化顺序:先让编码器供电,再初始化定时器编码器模式,最后才使能定时器。早期我把初始化和使能写在一起,上电后直接启动,毛刺全部计进去了。改成“先初始化GPIO和编码器配置,延时100ms等编码器供电稳定,再TIM_Cmd使能并清零CNT”之后,问题消失。对要求严格的项目,也可以在使能定时器之前先等待编码器输出有效电平信号。

5.2 信号边沿抖动:滤波器调节的实测经验

有一次在步进电机的项目里,电机静止时计数器偶尔会自己加一减一,位置环输出因此周期性抖动。一开始怀疑是代码逻辑问题,后来用示波器看编码器输出,发现电机静止时信号线上依然有毛刺,幅度接近阈值电平。

我把TIM_ICFilter从0调到0x0F,毛刺被滤掉,计数器不再乱跳。但随后又出现新问题:电机高速运行时,脉冲计数偏低,转速越高偏差越大——典型的高速下滤波器延迟过大的症状。最后我把滤波值调到0x05,低速静止不再误计,高速也能跟上。这个值并不通用,它取决于你的信号质量和编码器最高输出频率,建议在项目调试阶段多试几个值,用示波器和电机实测校准。

5.3 24位计数器溢出:50万脉冲的大范围场景

STM32F103的定时器计数器是16位的,最大也就65535。如果在做长距离位移测量或者减速比很大的结构,一圈下来计数器可能远超65535。比如你用1000线编码器测丝杆行程,丝杆转10圈就是40000个脉冲,16位够用;但如果是50圈、100圈,单靠16位计数器必然溢出。

方法我前面已经提到了:在更新中断里维护一个更高位数的计数变量。把overflow_count和CNT组合成一个32位有符号数,理论上能表达的范围就非常大了。但这里有个很容易被忽略的问题:16位计数器溢出后CNT会从65535回到0,或者从0回到65535,你在读取和合并的时候必须保证原子性。如果你的代码里没有关中断直接读CNT,恰好读到一半发生溢出中断,合并出来的数值可能是错的。

我在实际代码里是这样处理的:

int32_t Encoder_GetPosition(void) { int32_t temp; __disable_irq(); temp = ((int32_t)overflow_count << 16) + (int32_t)TIM3->CNT; __enable_irq(); return temp; }

关中断的时间很短,不会影响系统实时性,却能保证数据一致性。这个习惯我后面一直保留着,也建议你写编码器读取代码时都加上临界区保护。

6. 个人小结与建议

从外部中断数脉冲切换到定时器编码器模式,是我做电机控制项目时一个很关键的转折点。它不只是省了几个CPU周期的问题,而是从根本上解决了高速丢脉冲、方向误判这些会影响闭环稳定性的核心痛点。如果你现在还靠GPIO中断在数编码器脉冲,建议尽早迁移到这个方案上来。

最后再说一点自己的体会:编码器程序的调试,不要只盯着代码看,波形和时序往往才是根源。手边有一台逻辑分析仪或者示波器,能让你排查抖动、毛刺类问题快好几倍。先硬件后软件,先单步后闭环,按这个顺序来,编码器相关的坑基本都能绕过去。

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

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

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

立即咨询