第一次用MSPM0G3507做小车测速的时候,我想都没想就打算用外部中断数脉冲,结果折腾到半夜:毛刺误触发、正反转判断麻烦、电机转速一高中断还丢计数。后来翻数据手册才意识到,这颗芯片的TimerA0里藏着一个硬件正交编码器接口QEI,A/B两相脉冲直接接进去,方向判断、计数、滤波全部由外设自动完成,CPU想读的时候读一下寄存器就行。这篇是MSPM0G3507系列教程的第七篇,前面六篇把时钟、GPIO、UART、定时器、PWM这些基础外设都过了一遍,这次集中讲编码器(TIMER-QEI),覆盖硬件接线、SysConfig配置、代码读取、测速换算和实战中能遇到的典型坑。适合正在用这颗芯片做小车测速、机械臂角度反馈或者转台位置检测的开发者,也适合第一次接触正交编码器、分不清A相B相Z相的新手。
1. 为什么测速要单独用一个外设:硬件QEI和外部中断的差距
1.1 用外部中断数脉冲的痛
很多初学者做电机测速的第一反应就是"把编码器A相接外部中断,每次上升沿触发计数",我也这么干过。单看想法好像没问题,仔细一算就露馅了。拿常见的7线霍尔编码器举例,PPR是7,X4倍频后每转一圈产生28个计数脉冲。假设电机输出3000rpm,也就是每秒50圈,那每秒要处理50乘以28等于1400次外部中断。每次中断都要进出栈、读电平、判断方向、更新变量,CPU大部分时间都耗在这上面。
如果只是转速低还好说,换成500线的光电编码器,乘以4倍频后每圈2000个脉冲,3000rpm时每秒就是10万次中断,普通单片机在这个量级下基本什么都干不了。更麻烦的是毛刺问题,电机启动瞬间电磁干扰串到信号线上,外部中断分不清真脉冲还是噪声,计数就乱跳。方向判断更是要额外写代码,在A相中断里读B相电平才能知道正转反转,稍微不注意时序就判断错误。
硬件QEI解决的恰恰就是这几个问题:解码逻辑固化在外设里,不占CPU;内置输入滤波器可以直接吃掉窄毛刺;方向由硬件根据A/B相位的先后关系自动判断。同样是3000rpm的场景,CPU只需要在需要读数的时候一次读走位置计数器,和外部中断方案的负载完全不在一个量级。
1.2 MSPM0G3507上的QEI资源在哪
MSPM0G3507这个型号里,能直接跑正交编码器接口的外设只有一个,就是Timer A0。这是高级控制定时器,不仅有QEI,还能输出三相互补PWM,很多用这颗芯片做电机控制的方案都是围着它转的。G系列里另外几个TimerG是通用定时器,不支持QEI模式,不要在TimerG上找编码器选项,找不到的。
在SysConfig里左侧Timers面板添加外设时,只有Timer A0的Mode列表里会出现Quadrature Encoder这个选项。用的时候注意,如果是G系列的其它型号,比如MSPM0G3107,外设资源配置不一样,不一定带TimerA0,需要先对着数据手册确认。确认方法很简单,在芯片数据手册的"Block Diagram"页面看到"Timer A0 (Advanced Control Timer)"字样,就说明支持;如果没有,就得换方案,比如用两个GPIO外部中断自己实现正交解码,或者换一颗有QEI的型号。
1.3 QEI外设是怎么干活的:从滤波到计数
QEI模块内部大致分为三段:输入滤波器、正交解码器、位置计数器。输入滤波器的作用是把信号线上小于一定宽度的毛刺直接滤掉,相当于给编码器信号安了一道闸门,避免噪声脉冲进入后续电路。正交解码器负责分析A相和B相的相位关系——A相超前B相90度就是正转,反过来就是反转——然后产生方向信号和计数时钟。位置计数器根据方向信号自动加一减一,正转累加、反转递减。
这里要注意X1、X2、X4三种解码率的区别。X1模式只在A相的上升沿计数,X2模式在A相的上升沿和下降沿都计数,X4模式则是A相和B相的四个边沿全部计数。同样的编码器,X4模式下位置分辨率是X1的四倍。比如7PPR的编码器配X4模式,转一圈位置计数器变化28;配X1模式就只有7。实际项目里默认用X4,除非信号频率太高、后面还有带宽瓶颈才考虑降档。
2. 硬件准备:编码器接入前的三个检查
2.1 电平匹配:5V编码器别直接进3.3V引脚
MSPM0G3507的GPIO耐压上限是3.6V,而市面上很多编码器模块是5V供电的,输出高电平也是5V,直接怼进单片机引脚轻则误触发,重则损坏引脚。接之前先查清楚编码器模块的原理图或数据手册:如果模块上带了电平转换芯片,比如TXB0104、SN74LVC,那输出已经是3.3V电平,可以直接接;如果只是简单的开漏输出加上拉电阻到5V,那高电平就是5V,必须加一级分压电阻或者用双向电平转换模块。
最省事的办法是选3.3V供电的编码器模块,或者那种VCC和逻辑电平分开的型号。实在只能用5V编码器,可以在信号线上串一个1k电阻再加一个2.2k电阻到地分压,两个信号各分一组,简单可靠。
2.2 上拉和滤波:读数乱跳的隐形元凶
很多霍尔编码器和光电编码器的信号输出是开漏结构,引脚只能在低电平有效,高电平要靠外部上拉电阻拉起来。如果模块上没有集成上拉,A相B相在悬空状态时会受环境干扰电平漂移,编码器不转的时候计数值也自己乱走。这种情况在SysConfig里开启GPIO内部上拉可以缓解,但内部上拉阻值通常几十千欧,抗干扰能力有限,更稳妥的是在信号线对3.3V之间加4.7k到10k的上拉电阻。
电源滤波也不要忽略。编码器供电在电机启动瞬间容易跌落,会导致脉冲丢失甚至输出乱码。我给编码器的VCC和GND之间并了一颗100uF电解电容加一颗0.1uF瓷片电容,实测电机全速启停时计数稳定了很多。信号线上如果能串一个100欧左右的电阻,起到一定的限流和阻尼作用,也有帮助。
2.3 引脚和布线:SysConfig PinMux里先确认
MSPM0G3507的TimerA0 QEI信号最终落在哪两个引脚上,不是固定的,不同封装、不同开发板的布局都不一样。在SysConfig的PinMux视图里选好QEI功能后,界面上会列出所有可用的引脚组合,有的要求A相B相必须落在同一组通道上。务必对照你的开发板原理图,确认选的引脚没有被板载LED、按键、USB接口这些外设占用,再动手接线。
走线方面,编码器信号线尽量短,不要和电机驱动输出的PWM线、大电流导线走在一起,更不能捆成一股。实在避免不了交叉,就让信号线垂直跨过动力线,减少平行段的耦合长度。对于低速小车这类干扰不极端的场景,做到前两点基本就能稳定读数了。
3. SysConfig配置编码器(TIMER-QEI)逐项详解
3.1 配置入口:在SysConfig里加一个Timer A0
在CCS里打开工程的.syscfg文件,左侧找到Timers,点ADD,选择Timer A0。随后在Mode的下拉菜单里选择Quadrature Encoder,有些版本的SDK显示的选项名是QEI。保存之后CCS会自动生成初始化代码和对应的头文件宏。
这里容易踩一个坑:有些人在SysConfig里只是把Timer A0添加了,但没在Mode里切换成Quadrature Encoder,默认还是General Purpose,生成出来的代码根本不是QEI初始化,编译能过,程序跑起来编码器计数自然一动不动。配置完保存后再看一眼生成的ti_msp_dl_config.c,里面至少有DL_TimerA_initQuadratureEncoderMode这样的调用,才说明配置真正生效了。
3.2 关键选项怎么选:解码率、位宽、滤波
SysConfig里QEI相关的选项在不同SDK版本里叫法略有差异,但核心就几个。
解码率一般选X4,分辨率最高。如果后续发现中断频率太高或信号质量不好,再降回X2或X1。
位置计数器位宽优先选32位。拿7PPR编码器加X4模式算,一圈28个计数,3000rpm也就是每秒1400个计数,16位计数范围是负32768到正32767,大约运行46秒就会溢出。你要是做位置控制,正转一段时间再反转回来,溢出的符号处理能把你绕晕。32位计数器在绝大多数小车上跑上几天几夜都不会溢出,省心太多。
输入滤波器是另一个值得花时间设置的参数。QEI的输入滤波器本质上是一个采样判决器,用某个采样时钟对引脚电平连续采样,连续多次采到高电平才认为引脚确实变高了。这样能滤掉窄毛刺,但如果采样时钟分频系数太大,滤波器窗口太宽,真正的脉冲边沿也会被滤掉,表现在现象上就是高速时计数大幅减少。选多大合适,要根据实际脉冲频率反推,滤波器窗口至少要小于一个脉冲宽度的几分之一。比如7PPR编码器配X4模式、电机轴3000rpm时,每秒边沿1400个,单个边沿间隔约0.7毫秒,滤波器窗口设在十几微秒量级就绰绰有余,别盲目往大了调。
Index信号也就是Z相,如果编码器没有第三路输出,或者你不需要找绝对零点,可以让它保持禁用。需要做回零校准的场合再接上,通常还伴随一个Index中断用来清零位置计数器。
3.3 生成代码后的检查点
配置完保存后,打开ti_msp_dl_config.h看一眼,确认存在类似TIMER_QEI_INST这样的宏定义。这个宏在写代码时会用到,不同人给外设起的名字不同,生成的宏名也会跟着变,别瞎猜,打开头文件直接看。另外检查生成的引脚配置函数,确保A相B相所在的GPIO口被配置成了TimerA0的替代功能,而不是普通GPIO。如果发现引脚没有生效,回到SysConfig的PinMux视图,看有没有被其他外设占用,这是新配置不生效的最常见原因。
4. 把编码器读数变成转速:代码和换算
4.1 最简单的读取代码:先确认计数值能动
SysConfig配置完成后,读取编码器位置计数值只需要一行接口调用。下面是最小可运行示例。
#include "ti_msp_dl_config.h" int main(void) { SYSCFG_DL_init(); while (1) { int32_t qei_count = DL_TimerA_getTimerCount(TIMER_QEI_INST); // 把 qei_count 用串口发出来,或者调试器里看这个变量 } }注意返回值用int32_t而不是uint32_t。QEI位置计数器是有符号的,正转加、反转减,用无符号读出来方向信息就丢了一半。拿到这个值后,手动旋转编码器,观察数值是否跟着正反变化,这一步能验证接线和配置是否正确,比直接闷头写测速公式靠谱得多。
4.2 测速换算:把计数值变成rpm
知道了位置计数值,测速本质上就是"单位时间内的计数增量"换算成圈数,再换算成转速。
公式是:
转速rpm = (当前计数 - 上次计数) / (编码器PPR × 解码倍频) / 采样时间(秒) × 60拿7PPR编码器、X4模式举例,采样周期100毫秒,测到计数增量delta为28:
- 每秒计数增量 = 28 × 10 = 280
- 每秒转数 = 280 / (7 × 4) = 10圈/秒
- 每分钟转数 = 10 × 60 = 600rpm
如果电机带了减速箱,而编码器在电机轴那一侧,算出来的是电机轴转速,要得到输出轴转速,还要除以减速比。比如减速比30比1的输出轴转速是600除以30等于20rpm。很多人在这一步犯了迷糊,以为传感器直接给了负载端的转速,结果PID参数怎么调都不对。
4.3 方向、清零和溢出:几个小细节
方向问题不用在代码里额外判断,QEI的位置计数值本身就是带符号的。正转时数值往上走,反转时往下走,直接拿符号位做判断就行。
清零位置计数器有两种场景。一种是做回零,比如机械臂开机先找原点,找到后把当前位置设为0。另一种是测速,测速本质只需要增量,不一定要清零,但为了防止长时间运行后32位计数器在极端情况下溢出,用增量差值的写法天然免疫溢出问题,只要采样周期内不会超过计数范围就行。
SDK里清零接口的准确名字在不同版本里略有差异,直接读取头文件里TimerA相关的函数声明,找一个带set或者reset字样的接口,或者干脆往位置计数器寄存器里写0,同样能达到目的。
4.4 定时中断采样:让速度数据稳定输出
主循环轮询读取的缺点是采样间隔不固定,主循环里如果还有串口打印、屏幕刷新这种事情,算出来的速度就会时快时慢。稳定做法是配一个TimerG定时器,每10毫秒产生一次中断,在中断里读计数、算增量、更新全局速度变量。
#define PPR 7 #define SAMPLE_MS 10 #define COUNTS_PER_REV 28 // PPR * 4,X4模式 volatile int32_t last_count = 0; volatile int16_t motor_rpm = 0; void TIMER_TICK_IRQHandler(void) { int32_t cur_count = DL_TimerA_getTimerCount(TIMER_QEI_INST); int32_t delta = cur_count - last_count; last_count = cur_count; // 每秒计数 = delta / 0.01 = delta * 100 // 每秒转数 = 每秒计数 / COUNTS_PER_REV // rpm = 每秒转数 * 60 motor_rpm = (int16_t)(delta * 100 * 60 / COUNTS_PER_REV); DL_TimerG_clearInterruptStatus(TIMER_TICK_INST, DL_TIMER_INTERRUPT_PERIOD_EVENT); }这个写法故意避免了浮点数,全用整数运算,中断里执行时间很短。delta为负代表反转,motor_rpm为负值,正好符合控制逻辑的习惯。需要注意的是中断里别做串口发送这类耗时操作,把速度数据存到全局变量,主循环里想用再读。
5. 实测心得:最容易踩的坑和排查链路
5.1 现象一:编码器转了,计数值纹丝不动
这是最常见的故障,排查思路按步骤走基本三分钟内定位。
先确认接线方向有没有接反,A相接的确实是TimerA0对应的通道引脚,B相同理。很多板子的排针丝印印得不清楚,插反了位置计数器自然不动。接着看有没有共地,编码器地和MCU地必须连在一起,信号才有统一的参考电平。然后打开SysConfig重新看一眼Mode是不是Quadrature Encoder,保存后重编译,确认生成代码里有QEI初始化函数。
如果上面都正常,用一个最笨但最有效的方法:把这两个引脚在SysConfig里临时配置成普通GPIO输入,用手拨编码器,在主循环里读GPIO电平变化。如果GPIO模式下能看到电平翻转,说明编码器和接线都没问题,问题就出在QEI配置或引脚复用上;如果GPIO模式也读不到变化,那就是编码器本身没输出了,用万用表量VCC和GND,看供电有没有到位。
5.2 现象二:计数乱跳,正负反复横跳
编码器静止时计数还在动,十有八九是信号质量问题。先把示波器或者逻辑分析仪挂到A相和B相上,慢慢转动编码器看波形。波形边沿如果是慢慢爬坡而不是陡峭的方波,那就是上拉电阻没接或者阻值太大,引脚在高低电平之间徘徊,触发无效计数。解决办法是加4.7k上拉电阻到3.3V,或者确认SysConfig里内部上拉已经打开。
还有一种情况是编码器供电在电机启停瞬间跌落,导致输出波形畸变。这时候示波器看的是编码器VCC引脚,电压在电机启动时会掉几百毫伏甚至更多。在供电端并联大电容,或者把编码器供电和电机供电彻底分开,就能解决。
5.3 现象三:方向反了,正转变反转
方向反了的本质很简单,A相和B相接换了。在物理上对调A/B两条线,或者在SysConfig里找位置极性选项做反相,两种方式效果一样,我一般优先改配置,省得拆线焊线。注意确认一下判断标准,正转的定义是人为定的,反了不一定是硬件错,符合实际机械约定就行。
5.4 现象四:高速时丢脉冲
低速计数正常,转速一高计数就明显偏小,重点查输入滤波器参数。滤波窗口太宽会把相邻很近的有效边沿吞掉,7PPR编码器配X4模式高速时每秒上千个边沿,窗口设得过大直接废掉。把滤波器分频调小几个档位试试,如果丢掉的比例大幅下降,基本就是它的问题。
另外,如果QEI信号线上接了比较长的杜邦线,高速时信号反射和串扰也会导致丢脉冲。尽量缩短信号线长度,实在不行可以试一下把信号线双绞,和地线绞在一起能明显改善高速传输质量。
5.5 一个容易被忽视的坑:QEI引脚被板载外设占用
我遇到过最隐蔽的问题,是PinMux里QEI配置看着是选上了,但实际上选的引脚和板载LED、按键的GPIO复用了。平时跑LED闪烁程序时看起来正常,接上编码器后计数疯狂跳动,因为LED的PWM信号直接串到了编码器输入路径上。排查方法不复杂,接编码器之前先在PinMux界面把所有引脚的功能过一遍,尤其检查和板载LED共用的那些引脚。出现跳数的现象时,除了查编码器自身信号质量,也检查一下旁边有没有在跑的PWM或者高频外设。
6. 进阶:让QEI数据真正好用
6.1 配合TimerG定时中断做10毫秒速度采样
前面第4.4节给了一个定时中断框架,实际工程里我通常直接在这个基础上扩展。一个TimerG做速度采样中断,PWM输出用另一路定时器,控制环路跑在同一个中断里,读编码器、算误差、更新占空比,形成一个简单的闭环。MSPM0G3507片上资源完全够用,TimerA0做QEI,TimerG0做采样中断,TimerA0的PWM通道做电机驱动,刚好组成一套不冲突的最小系统。
6.2 校准真实PPR:别被减速比绕晕
编码器标称的PPR不一定完全准确,或者减速比标称值和实际有微小偏差。装好机械结构之后,做一次实测校准:手动把输出轴转N圈,记录位置计数器的变化量delta,那么真实每圈计数就是delta除以N。如果只方便转电机轴,那除以的圈数也按电机轴来算,然后根据减速比折算到输出轴。这个校准值记录下来,写进程序里当常量用,远比迷信标称参数靠谱。
6.3 下一步:把QEI数据喂给PID闭环
编码器数据本身只是反馈量,它真正价值体现在闭环控制里。用这个数据可以做速度环,也可以做位置环,甚至两者串级。刚开始做PID的同学,先用上一节的10毫秒采样中断把数据测准,再在纸上把目标转速和实测转速的误差手算一遍,理解P、I、D三项各自在纠正什么,再往工程里加参数。有时候看起来是PID参数没调好,实际是编码器数据本身就有问题,这就是为什么先把QEI读数稳定住比什么都重要。
写到最后说点实在的。我每次拿到一块新板子,第一件事就是把编码器接上去,用手转一圈看计数值是不是干净稳定地跟着走。这个土办法比任何调试手段都管用,读数稳定、方向正确、数值符合预期,后面写算法就放心了;一旦读数乱跳,不要急着改代码,先回去查上拉、查地线、查PinMux。编码器是整个控制环的地基,地基歪了,上面的控制算法写得再漂亮也白搭。