老读者应该知道,这个系列前面几篇已经把MSPM0G3507的GPIO、定时器、中断这些基础外设过了一遍。今天这篇轮到编码器,准确说是用芯片内部的TIMER模块配置QEI(Quadrature Encoder Interface,正交编码器接口),去接增量式编码器。
做电机控制、小车底盘测速、云台角度反馈、甚至机械臂关节位置检测,编码器都是绕不开的传感器。MSPM0G3507这颗M0+内核的MCU,主频80MHz,片内集成多个TIMG定时器,其中一部分可以配置成Encoder模式来替代外置的解码芯片。这篇就把怎么在CCS里配合SysConfig把QEI跑起来讲透,同时把配置过程里容易踩的坑也一并列出来。
先说清楚适用人群:手里有MSPM0G3507的LaunchPad或自研板,想接带A/B两路正交输出的增量编码器,用来测速度或相对位置,那就非常适合往下看。如果用的是绝对值编码器,走的是SPI/SSI这类通信接口,那QEI模式就用不上了,等下会简单说两句区别。
1. 为什么用TIMER做QEI:增量编码器的读数原理
1.1 正交信号和四倍频的本质
增量式编码器内部是一个码盘加光电或磁感应元件,转动时输出两路方波信号,工程上通常叫A相和B相,有的也叫PHA和PHB。这两路信号的关键特征就是相位差正好是90度,也就是正交。
正转的时候,A相上升沿总是比B相提前四分之一周期;反转的时候反过来,B相领先A相。MCU就是靠检测这两路信号的边沿先后顺序来判断方向,靠统计边沿数量来计算转过的角度或圈数。
这就引出了编码器分辨率的一个重要概念:四倍频。一个完整正交周期里包含四个边沿时刻,分别是A上升沿、B上升沿、A下降沿、B下降沿。如果只数A相的上升沿,一圈下来计数值等于码盘线数PPR;如果四个边沿都数,一圈下来计数值就是4倍的PPR。所以很多编码器标称500线,在四倍频模式下实际一圈能产生2000个计数脉冲,分辨率一下就上去了。
MSPM0的TIMG模块在Encoder模式下,硬件会自动完成这个边沿检测和方向判断,不需要CPU在中断里去比对波形。这也是为什么QEI模式比普通的外部中断+GPIO扫描方案靠谱得多,脉冲再密也不会丢,CPU压力也小。
1.2 TIMG8/QEI模块与独立编码器接口的取舍
有些MCU会带一个专门的正交编码器接口外设,叫QEI或者eQEP,独立于定时器存在。MSPM0G3507这颗芯片的资源布局不太一样,TI的做法是复用TIMG定时器资源,让其中一个定时器工作在Encoder模式,从功能上讲就等效于一个硬件QEI接口。
这么设计的好处是外设资源不浪费。同一颗TIMG既可以做普通定时器、PWM输出、输入捕获,也可以切换成Encoder模式,按需分配。坏处就是配置的时候要稍微注意一下引脚复用和中断资源的占用,比如你用TIMG8做了编码器接口,那TIMG8的捕获比较通道就不能再拿去做PWM输出了,这个要在工程规划阶段就考虑清楚。
从MSPM0G3507的型号命名看,这个系列内部有多个TIMG实例,常见的有TIMG0、TIMG1、TIMG6、TIMG7、TIMG8、TIMG9等。具体哪个TIMG支持Encoder模式,不同封装、不同型号会有差异。稳妥的做法是打开SysConfig之后,看外设列表里哪个TIMG下拉选项里有Encoder Mode可选项,那就是能用的。一般来说TIMG8和TIMG9很多型号都支持,但还是要以你手上芯片的数据手册为准。
如果接的是EC11那一类机械旋转编码器,或者带霍尔传感器的电机编码器,原理都一样,都是A/B两路正交方波,所以下面这套配置流程完全通用。
2. SysConfig一步一步配好QEI
2.1 创建工程和选择TIMG外设
在CCS里新建MSPM0G3507工程后,工程里会有一个.syscfg配置文件,双击打开就能进SysConfig图形化配置界面。这个工具的本质是生成初始化代码,你不用手写寄存器配置那一大坨,但要清楚它帮你做了什么。
先把要用的TIMG外设加进来。在SysConfig左侧的Software组件里找到TIMER,添加一个定时器实例。比如添加Timer_8,对应的是TIMG8。然后在Timer_8的配置页面里,把Mode从默认的PWM模式改成Encoder Mode。
这时候页面会弹出跟编码器相关的配置项,包括输入引脚选择、计数模式、计数方向、Top值、滤波参数等。引脚选择这一步要特别留意,MSPM0G3507引脚复用不是随便选的,必须参考数据手册里的引脚复用表,在SysConfig里也要勾选对应的引脚。通常QEI模式需要两个输入引脚,分别接A相和B相;如果要用Z相做零点复位,还得再选一个Index引脚。
改完引脚后,SysConfig会自动在PINMUX里把引脚复用关系配好。这个环节最容易出的问题就是引脚冲突,比如你之前已经用了PA0做串口或者PWM输出,现在又要拿它做编码器输入,SysConfig会直接标红报错。解决办法就是换引脚或者调整之前的外设分配。
2.2 关键参数:计数模式、方向、Top值和滤波
进入Encoder模式后,有几个参数对实际测量结果影响很大,一定得理解每一项的含义再动手。
第一个是Counting Mode,也就是计数模式。里面有Quadrature 1x、Quadrature 2x、Quadrature 4x这几个选项。1x模式下只在A相上升沿计数,2x模式下A相和B相的上升沿都计数,4x模式下四个边沿都计数。我这里做转速和位置测量,普遍选4x模式,分辨率最高。如果编码器本身线数已经很高,或者CPU后续要处理的运算量比较大,也可以用1x模式降低计数频率,具体情况具体取舍。
第二个是Count Direction,计数方向。一般选Bidirectional,也就是双向计数。正转时计数值增加,反转时计数值减少。这个参数其实由硬件根据A/B相位差自动判断,不需要软件干预。但你要知道,计数方向只代表编码器转动的方向,不等于电机的正转或反转,因为电机接线和编码器安装方式会影响这个对应关系。
第三个是计数器范围。MSPM0的TIMG计数器默认是16位,也就是计数范围从0到65535。在Encoder模式下,你要设置Position Count Bottom和Position Count Top两个值。通常就是Bottom=0,Top=0xFFFF。但这里有个实际问题:如果电机转速很高,或者编码器线数很多,65535个计数可能不够用。比如1000线的编码器在4x模式下,一圈就是4000个计数,65535大概只能代表16.4圈,如果设备行程超过这个范围,计数值就会溢出回绕。
解决溢出问题有两个思路:一是把多个16位计数扩展成32位,在中断里维护软件计数值;二是如果应用场景单一圈位置(比如云台、机械臂关节),可以在Top值上做文章,让计数器转满就停止或者回零。后面第4节会详细讲32位扩展的处理方法。
第四个是Input Filter,输入滤波。编码器信号在工业现场容易受到干扰,电机电刷火花、PWM驱动线缆串扰,都可能在A/B信号上叠加毛刺。如果毛刺被当成有效边沿计数,位置数据就废了。SysConfig里可以给编码器输入配置一个数字滤波器,本质是连续采样输入信号,只有信号稳定超过设定的采样窗口后才认为是一次有效电平变化。
滤波器的采样窗口可以按系统时钟周期数来配置。窗口越大抗干扰能力越强,但同时会增加信号延迟,高速场合下可能导致相位检测滞后。实际项目里要根据编码器最高输出频率来折算,别盲目把滤波窗口调到最大。比如系统时钟80MHz,滤波窗口设成8个时钟周期,对应延迟0.1微秒,对大多数应用来说影响可以忽略。
2.3 中断和事件:什么时候该打断CPU
Encoder模式下有几个事件可以产生中断,最常用的是Load Event,也就是计数值更新事件。可以配置成每检测到一定数量的计数边沿就触发一次中断,比如每次计数匹配某个预设值就通知CPU读取当前计数值。这个机制在做位置采样时特别有用。
另外一个实用的事件是Direction Change,方向改变事件。电机正转突然变成反转的时候触发中断。在位置控制场景里,这个事件可以用来判断运动方向是否发生意外变化,及时停止驱动或者记录换向时刻。
还有一个跟Index信号相关的事件。如果编码器有Z相,每转一圈输出一个脉冲,在Encoder模式下可以配置成检测到Index输入时清零计数器或者触发中断。这个功能在回零、找原点时非常关键。
中断配置在SysConfig里勾选对应选项就行,实际代码里写ISR时再从状态寄存器里判断是哪个事件触发的。不要把多个事件的中断逻辑都塞在一个ISR里然后分不清来源,项目大了会非常难查问题。
3. 代码实现:初始化、读位置、算速度
3.1 初始化代码和寄存器行为
SysConfig配置完成后,CCS会自动生成初始化代码。在main.c里调用SYSCFG_DL_init(),所有外设初始化就都完成了。这时定时器还没开始跑,需要手动调用启动计数函数。
以TIMG8为例,初始化之后启动代码如下:
#include "ti_msp_dl_config.h" int main(void) { SYSCFG_DL_init(); DL_TimerG_startCounter(TIMER_8); while (1) { // 主循环任务 } }DL_TimerG_startCounter()的作用是让编码器接口开始工作,此后A/B引脚上的正交信号就会驱动计数器自动增减。读取当前计数用DL_TimerG_getTimerCount(TIMER_8),返回的就是当前计数值。
这里要讲一个实际使用中的细节:DL_TimerG_getTimerCount()读回来的RAW计数值是无符号16位。如果你在调试窗口里看到数值在0到65535之间来回跳,不代表位置在反复变化,而是计数器本身按16位回绕了。要拿到真实的位置变化量,必须用带符号的差值计算,要靠软件把这个回绕处理好。
还要注意一点:SysConfig生成的代码不要手动改。比如你直接在生成的timer.c里改了某个寄存器的值,下次重新生成代码会被覆盖掉。所有配置都回到.syscfg里去改,然后重新生成。这是SDK开发的基本规矩,新手经常在这里翻车。
3.2 位置累计和方向判别的完整写法
实际项目里我们一般不会直接用16位RAW值当位置数据,太容易溢出了。正确做法是在中断里维护一个32位甚至64位的软件位置计数器,每次读取到新的RAW值后,和上一次的RAW值做带符号差值,再累加到全局位置变量上。
示例代码如下:
volatile int32_t g_position = 0; volatile uint16_t g_last_raw = 0; volatile int8_t g_direction = 1; void TIMG8_IRQHandler(void) { uint32_t int_status = DL_TimerG_getEnabledInterruptStatus(TIMER_8); DL_TimerG_clearInterruptStatus(TIMER_8, int_status); // 读取当前RAW值 uint16_t current_raw = DL_TimerG_getTimerCount(TIMER_8); // 计算差值,处理16位回绕 int16_t delta = (int16_t)(current_raw - g_last_raw); // 累加到32位全局位置 g_position += delta; g_last_raw = current_raw; // 方向判断 if (DL_TimerG_getTimerDirection(TIMER_8) == DL_TIMER_DIRECTION_UP) { g_direction = 1; } else { g_direction = -1; } }这里关键在于(int16_t)(current_raw - g_last_raw)这个处理。只要两次读取之间实际转过的计数不超过32767,这个差值就是准确的,不仅消除了16位回绕问题,还保留了正负号。比如上一次Raw值是65000,这一次变成了100,实际是正向转了516个计数,那么(int16_t)(100-65000)算出来的结果就是516,完美避开了回绕误判。
手动写32位扩展时,要保证ISR执行时间足够短,不能在中断里做串口打印、浮点运算这类耗时操作。中断频率如果很高,比如编码器每分钟几千转,再加上高线数和4x倍频,每秒钟可能产生几十万次计数,每次计数不一定触发中断,要看Load Event的配置方式。通常做法是每累计固定脉冲数(比如每次Load事件)才进一次中断,别让边沿直接驱动中断。
3.3 从计数差值换算转速RPM
有了位置计数,速度计算就顺理成章了。最常用的方法就是固定时间窗口法:每隔固定时间(比如100ms)读取一次位置值,和上次做差,然后用差值、编码器线数、倍频数算转速。
公式很简单:
转速RPM = 计数差值 / (编码器每转总计数 × 采样时间秒) × 60其中编码器每转总计数 = 编码器线数PPR × 倍频数。比如500线的编码器,4x模式下每转总计数是2000。采样周期100ms,两次采样之间计数差值2000,那正好一秒转了20转?不对,100ms转了2000个计数,相当于0.1秒转一圈,所以一秒10转,RPM=600。
写代码时可以直接用整数运算避免浮点开销:
#define ENCODER_PPR 500 #define QUAD_MULTIPLIER 4 #define COUNTS_PER_REV (ENCODER_PPR * QUAD_MULTIPLIER) #define SAMPLE_PERIOD_MS 100 volatile int32_t g_position = 0; volatile int32_t g_last_pos = 0; volatile int32_t g_speed_rpm = 0; void Speed_Task_100ms(void) { int32_t current_pos = g_position; int32_t delta = current_pos - g_last_pos; g_last_pos = current_pos; // 转成RPM g_speed_rpm = (delta * 1000) / SAMPLE_PERIOD_MS * 60 / COUNTS_PER_REV; }实际用的时候,建议先打印原始计数差值验证方向对不对,再套公式。如果正转时速度显示为负,说明接线或者方向配置反了,在SysConfig里把两个输入引脚对调就行,不用改代码。
固定时间窗口法的缺点是在低速时分辨率不够。如果电机一分钟只转几转,100ms窗口内计数差值只有几个脉冲,算出来的速度要么是0要么跳变很大。低速高精度测速可以改用脉冲间隔法,通过输入捕获测量两个边沿之间的时间,再换算出速度,但这个话题展开又是一篇,后面再单独聊。
4. 实测中踩过的坑和排查技巧
4.1 上电丢脉冲和毛刺计数
我最开始接编码器的时候遇到一个很典型的问题:用手慢慢转编码器,串口打印的位置值偶尔会突然跳几十上百个计数,而且方向不固定。用万用表量A/B引脚电平,转动时确实有变化,但波形不稳定。
排查了一圈,发现问题是A/B引脚悬空时电平不确定。因为编码器是开漏输出或者机械触点,驱动能力很弱,如果MCU引脚内部没有使能上拉电阻,引脚电平就会在阈值附近飘,产生毛刺,硬件QEI根本分不清哪些是有效边沿,全都记下来了。
解决方法是把A/B两个输入引脚都配置成内部上拉。在SysConfig里给引脚设置Pull Up,或者在代码里手动配置:
DL_GPIO_setPulls(GPIOA, GPIO_PIN_0, DL_GPIO_PULL_UP);如果编码器线缆比较长,或者现场干扰源多,光靠MCU内部上拉还不够,建议在靠近MCU引脚的地方外加一个10kΩ的上拉电阻到3.3V,同时配合第2节讲的输入滤波器一起用。
另外一个细节:上电瞬间,电机驱动器和编码器可能不是同时供电,MCU会先跑起来而编码器还没稳定输出,上电最初几十毫秒内的计数值是不可信的。工程上一般在上电后延时一下,或者等系统启动完成后再清零位置值,再开始累计。
4.2 方向接反和引脚复用冲突
方向接反是QEI配置里最普遍的问题,特征是:你手动正转,打印的位置值却是负数,或者位置值减小。很多人第一反应是改代码把差值取反,但其实标准做法是回到SysConfig里把A相和B相的引脚对调,或者把编码器插头反过来接。
为什么推荐从硬件层面对调而不是软件取反?因为只要软件里做一次取反,以后别人看代码就得时刻记着这个坑,而且如果需要用到方向改变事件,取反逻辑很容易出遗漏。硬件把A/B对调之后,信号相位关系和软件逻辑就完全一致了,不容易出二次问题。
引脚复用冲突是另一个高频事故。SysConfig里选引脚的时候,如果编码器的A相引脚被其他外设占用,配置界面会直接报错。但有些冲突是隐性的,比如引脚本身支持的功能非常多,你选了一个GPIO输出控制LED,另一个外设也用了同一个引脚,SysConfig能检测到,但如果你用寄存器直接配置或者是旧版本SDK,可能不会提示那么清楚。养成一个好习惯:配置编码器输入引脚之前,先看一遍原理图,确认这个引脚没有被板上的跳线、按键、LED占用。
4.3 16位计数溢出与多圈扩展
第2节提过,TIMG默认是16位计数器,多圈位置累计要靠软件扩展。但在实际ISR里写扩展逻辑时,有一个很容易被忽略的问题:读取CPU死等和中断延迟。
如果主循环里有长临界区,比如关中断操作或者长时间屏蔽中断,那么编码器中断可能不会及时响应,导致两次读取之间实际计数差值超过32767,这时(int16_t)差值计算就会溢出,位置数据直接乱掉。所以用QEI的项目,主循环里尽量不要长时间关中断,尤其不能在高转速下用__disable_irq()包着一大段浮点运算。
如果真的避免不了长时间关中断,那就得提高采样频率或者改用硬件更强的方案。比如把Load Event配置成每256个计数触发一次,中断频率就很低了,主循环关中断几十微秒也不会丢位置,因为QEI计数在硬件层面是实时累加的不受中断影响,只是软件读取时可能错过一些中断而已。
还有人问能不能把TIMG配置成32位模式。MSPM0G3507的部分TIMG支持32位计数模式,但在Encoder模式下能否使用32位计数,要参考芯片参考手册。即使支持,32位计数器起始值、回绕处理方式也和16位完全不同,代码逻辑不一样,别指望直接替换。稳妥方案还是16位计数器+软件32位扩展。
4.4 滤波参数和信号完整性的取舍
调滤波器参数的时候,我试过把滤波窗口设到很大,比如几百个系统时钟周期,结果编码器转快了之后方向判断开始出错。原因很简单:滤波器本质上是个低通环节,窗口越大延迟越大,A相和B相的边沿经过不同延迟处理后,相位关系可能被破坏,QEI硬件就分不清谁先谁后了,方向自然乱跳。
所以滤波器设置的原则是:能滤掉干扰的正确窗口,但不要追求越大越好。一般从8个时钟周期的窗口开始调,如果还有毛刺再往上加,同时配合外部RC滤波电路一起处理。外部在A/B引脚对地各加一个100pF到1nF的小电容也可以滤高频噪声,效果也不错。
如果编码器信号质量本身很差,比如长线传输、接头松动,那软件调参只是权宜之计,最终要查接线和屏蔽。QEI遇到奇怪的方向抖动,先拿示波器或者逻辑分析仪看A/B波形,确认信号干净再回头怀疑软件。
5. 后续扩展:从测速到闭环控制
这篇聊的编码器读取和测速方法,其实是闭环控制的基础。位置值就是PID控制里的位置反馈,速度值就是速度环的反馈,有了这两个量,电机控制就算真正打通了反馈回路。
如果你后面要做速度闭环,我建议速度环的采样周期和电流环尽量协调好,比如速度环跑1kHz,那编码器速度采样也按1ms周期来做,别拖后腿。固定时间窗口法在低速时分辨率不够的问题,闭环控制里会更敏感,这时可以配合M/T法测速,或者用输入捕获去测脉冲间隔,低速效果会好很多。
另外,如果编码器带Z相,回零逻辑一定要做进初始化流程里。设备上电先朝一个方向找Z相脉冲,找到之后清零位置值,这个位置的绝对性和可重复性比单纯靠限位开关更可靠。
做这期内容的时候,我又把小车的编码器电机驱动板翻出来重新测了一遍。给大家一个建议:刚把代码跑通的时候,先别急着写PID或者复杂算法,把编码器正转、反转时打印的RAW值和方向值观察清楚,确认接线、倍频数、方向都和预期一致之后,再往上层加控制逻辑。基础的数据读准了,后面怎么折腾都稳。这套配置流程放到其他型号的MSPM0上也基本通用,理解了Encoder模式的本质,换芯片也就是找引脚、配参数的事。