☰
AURIX TC264旋转编码器软解码实战:从AB相原理到ERU中断驱动
2026/9/28 4:04:36 网站建设 项目流程

做了几年嵌入式,接触过不少编码器应用,但第一次在英飞凌AURIX TC264上做旋转编码器读取时,还是被“方案选择”这件事卡了一下午。AURIX不像STM32那样有一个特别顺手的定时器编码器模式,TC264的GPT12虽然支持旋转计数器,但引脚复用表一翻,发现编码器的A、B两相已经被其他功能占掉,根本没有硬件解码通道可用。最后只能回到最原始也最可控的路线:软件解码,简称软解码。

这篇文章就把我这次在AURIX TC264上实现旋转编码器软解码的完整过程写下来。从编码器工作原理、为什么选软解码、计数逻辑设计、基于ERU外部中断的驱动代码,到速度换算、滤波、实际调试中踩过的坑,全部整理成可直接参考的方案。适合正在用AURIX做电机、手轮、角度反馈、位置闭环的工程师,也适合从STM32生态转过来的朋友——你会发现,只要理解了AB相本质,软解码其实比硬件模块更灵活。

1. 为什么选软解码:AURIX平台上的编码器方案取舍

1.1 AB相与Z相到底在表达什么

增量式旋转编码器最核心的输出就是A、B两路方波。轴转动时,A、B两路会产生相位差为90°的脉冲;正转时A相超前B相90°,反转时B相超前A相90°。这个相位关系就是方向信息。编码器每转一圈还会输出一个Z相信号,通常是一窄脉冲,用来做绝对位置的零点标定。

用生活化一点的语言来说,A、B两相信号很像马路上两个相隔固定距离的传感器:你判断一辆车是往左开还是往右开,靠的是两个传感器被触发的先后顺序。编码器里的方向和位置信息,本质上也是这么来的。

在实际选型中,编码器输出形式大致有三种:集电极开路(OC)、推挽输出和差分线驱动(RS422)。OC输出需要外部上拉电阻,AURIX内部上拉可以作为兜底但更建议外部接2.2k到10k的上拉;推挽输出直接怼进MCU即可;差分输出则要配AM26LV32这类差分接收芯片,先转成单端3.3V方波再进GPIO。这个电气接口问题虽然和软件无关,但接错了一定会在调试阶段浪费大量时间。

1.2 STM32与AURIX解码方案的现实差异

写过STM32的朋友都知道,STM32的高级定时器/通用定时器自带Encoder Mode,把编码器的A、B两相接进TI1和TI2,硬件就能自动完成方向判断和加减计数,CPU完全不参与。这个模式用起来确实舒服,导致很多从ST转过来的工程师,第一反应是去AURIX里找类似外设。

但AURIX系列的情况不太一样。TC2xx/TC3xx中,GPT12模块提供了旋转计数器模式(Rotary Counter Mode),可以把A、B两相分别接入T2IN和T3IN,由硬件完成方向识别。听起来很像STM32的编码器模式,但它对引脚复用有固定要求。我这次翻TC264的引脚复用表,编码器A、B两相被分配到的引脚恰好不属于GPT12候选引脚,硬要接就得改板子。在项目进度面前,改板子显然不现实。

于是只剩下两条路:一是用GTM模块定时采样,二是用ERU外部中断配合GPIO电平判断做软解码。GTM的方式适合低频采样场景,但响应延迟大;ERU中断方式实时性更好,逻辑也更直观。最终选了ERU中断方案。

1.3 软解码的边界在哪里

软解码并不是万能的。它最大的约束是CPU占用率会随着编码器转速线性上升。以2500线的编码器为例,轴转速3000rpm时,A相脉冲频率为125kHz,如果做两倍频,意味着每秒钟要被中断250k次;做四倍频则是500k次。TC264主频200MHz,一次ISR从响应到退出如果消耗100个周期,那么500k次中断就要50M周期,占CPU的25%左右。如果系统里还有复杂的控制算法、通信协议栈,这个占用率就很吓人了。

所以我的判断是:在中低速应用、或者编码器物理分辨率不高的场景下,软解码完全够用且极其灵活;但在高转速、高分辨率、CPU资源紧张的场合,优先考虑GPT12硬件旋转计数器或GTM方案。看清这个边界,后面就不会被“软解码是不是不专业”这种问题困扰。

2. 软解码核心原理:方向判断、倍频与计数逻辑

2.1 方向判断:一只引脚触发,另一只引脚定方向

软解码的方向判断非常直接。以A相作为中断源为例,将A相配置为上升沿和下降沿都触发中断。每次A相边沿到来时,读取B相当前电平:

  • A相上升沿到来时,如果B相为低电平,说明正转,计数值加1;
  • A相上升沿到来时,如果B相为高电平,说明反转,计数值减1;
  • A相下降沿到来时,如果B相为高电平,说明正转,计数值加1;
  • A相下降沿到来时,如果B相为低电平,说明反转,计数值减1。

这个判断逻辑的本质就是:借助AB两相的相位关系,在每次边沿时刻用另一相电平作为方向标志。很多资料上把这称为“单相双沿触发”或“两倍频方案”,因为编码器每输出一个完整脉冲周期,A相会经历上升沿和下降沿各一次,计数值变化两次,也就是脉冲频率的两倍。

如果你用四倍频方案,就是把B相也接进中断,A、B两相四个边沿都参与计数。这样每转一圈的计数个数是物理线数的四倍,分辨率更高。

2.2 两倍频与四倍频的取舍逻辑

两倍频只需要占用一个ERU中断通道,代码简单,CPU中断次数少。四倍频需要占用两个中断通道,代码多一个状态判断,但分辨率翻倍。

实际项目中怎么选?我的建议是:如果编码器本身线数够高,比如2500线以上,两倍频已经能提供每转5000个计数,通常够用;如果编码器只有100线、200线这种低分辨率,又需要比较精细的位置控制,那就上四倍频。四倍频的代价不仅是中断次数翻倍,还引入了更严格的时序要求——两个相邻中断之间的间隔可能是几十微秒甚至几微秒,ISR里绝对不能做任何耗时操作,比如打印、浮点运算、慢速Flash读写。

下面这张表是我在实际选型时常用的对比思路:

方案中断源每转计数CPU压力适用场景
单相单边沿A相上升沿P(物理线数)最小极低速、对分辨率无要求
单相双沿A相上升+下降沿2P小大多数电机反馈、手轮
四倍频A相和B相全部边沿4P大高分辨率位置环、低速高精场景

2.3 计数频率上限的快速估算公式

软解码能承受的最高转速,可以用一个简单公式估算:

最高转速rpm = 允许的中断频率 /(倍频系数 × 编码器线数 / 60)

举个例子:2500线编码器,两倍频,如果设定单边沿中断最大频率为200kHz,那么:

最高转速 = 200000 /(2 × 2500 / 60)= 2400rpm

如果编码器线数降到500线,同样200kHz中断频率下,最高转速能到12000rpm。这个估算在项目选型阶段非常有用,先算清楚再动手,可以少走很多弯路。

3. 在TC264上落地:软解码驱动完整实现

3.1 开发环境与硬件接线

我这边用的是AURIX Development Studio + iLLD库,芯片为TC264,双核TriCore,主频200MHz。AURIX Development Studio是英飞凌官方的免费IDE,基于Eclipse,对个人开发者很友好,工程向导里可以直接生成TC264的iLLD基础工程。

硬件接线很简单:

  • A相接到任意一个普通GPIO,我这段代码示例里用的是P10.2;
  • B相接到另一个GPIO,示例用P10.3;
  • 编码器供电根据手册接3.3V或5V,注意TC264的GPIO不是所有引脚都支持5V耐受,输入最高电压务必以数据手册为准,最好统一用3.3V供电或者加电平转换;
  • Z相如果用不到就先不接,需要零点标定时再接一个普通GPIO。

这里特别提醒一句:如果编码器是OC输出,外部上拉电阻一定要加,否则边沿会变得很“拖沓”,示波器上看是一堆毛刺,软解码会误计数到怀疑人生。

3.2 驱动模块怎么组织

软解码代码我拆成两个文件:

  • encoder_soft.h:对外暴露初始化函数、计数读取函数、速度计算函数;
  • encoder_soft.c:实际GPIO配置、ERU中断配置、ISR实现。

这种模块化的好处是后续切换到GPT12硬件方案时,只要把encoder_soft.c里的底层实现换掉,头文件的接口可以保持不变,上层控制代码几乎不用动。

对外接口设计成三个核心函数:

  • Encoder_Init:初始化引脚、配置ERU中断;
  • Encoder_GetCount:读取当前累计计数值;
  • Encoder_CalculateSpeed:按固定周期计算转速,内部用计数差值换算出RPM。

计数变量用volatile修饰,因为它在中断里被修改、在主循环里被读取,交叉访问必须防止编译器优化。有RTOS的话,读取时建议做临界区保护,或者直接读两次取值相等才算有效,避免读到撕裂值。

3.3 初始化代码:GPIO与ERU中断配置

核心初始化代码如下:

// encoder_soft.h #ifndef ENCODER_SOFT_H #define ENCODER_SOFT_H #include "Ifx_Types.h" #define ENC_A_PORT &MODULE_P10 #define ENC_A_PIN IfxPort_Pin_2 #define ENC_B_PORT &MODULE_P10 #define ENC_B_PIN IfxPort_Pin_3 // 编码器物理线数,根据实际编码器修改 #define ENC_PULSES_PER_REV 2500 #define ENC_COUNTS_PER_REV (ENC_PULSES_PER_REV * 2) // 两倍频 // 最小边沿间隔,单位us,用于软件滤波 #define ENC_MIN_EDGE_INTERVAL_US 20 extern volatile sint32 g_encoderCount; void Encoder_Init(void); sint32 Encoder_GetCount(void); float32 Encoder_CalculateSpeed(uint16 periodMs); #endif
// encoder_soft.c #include "encoder_soft.h" #include "IfxPort.h" #include "IfxStm.h" #include "IfxSrc.h" #include "IfxScuEru.h" volatile sint32 g_encoderCount = 0; static uint32 g_lastEdgeTick = 0; static sint32 g_lastCount = 0; static sint32 g_calcDelta = 0; // 中断服务函数,绑定到CPU0,优先级设为10 IFX_INTERRUPT(EncoderA_ISR, 0, 10); static void Encoder_EruConfig(void) { /* * 这里处理ERU通道配置: * 1. 把A相引脚对应的ERU输入通道选出来; * 2. 配置为上升沿+下降沿都产生事件; * 3. 把事件输出路由到对应的中断源。 * * 不同板卡的引脚映射不同,ERU通道号、SRC编号需要按 * TC26x 用户手册 ERU 章节和当前工程的 IfxScuSrcId 枚举来填。 * iLLD版本不同,底层库函数名会有差异,但配置思路一致。 */ } void Encoder_Init(void) { // 1. 引脚配置:输入模式 + 内部上拉 IfxPort_setPinMode(ENC_A_PORT, ENC_A_PIN, IfxPort_Mode_inputPullUp); IfxPort_setPinMode(ENC_B_PORT, ENC_B_PIN, IfxPort_Mode_inputPullUp); // 2. ERU事件配置 Encoder_EruConfig(); // 3. 中断源注册到CPU0 // 具体SRC编号以所选ERU通道为准,这里示意为IfxScuSrcId_eru0 IfxSrc_init(IfxSrc_getSrc(IfxScuSrcId_eru0), IfxSrc_Tos_cpu0, 10); IfxSrc_enable(IfxSrc_getSrc(IfxScuSrcId_eru0)); // 4. 清计数 g_encoderCount = 0; g_lastEdgeTick = IfxStm_getLower(IfxStm_getTFromModule(&MODULE_STM0)); } void EncoderA_ISR(void) { // 取当前STM时间戳,配合最小间隔做滤波 uint32 nowTick = IfxStm_getLower(IfxStm_getTFromModule(&MODULE_STM0)); // 软件滤波:间隔太短的边沿认为是抖动,直接忽略 uint32 deltaUs = (nowTick - g_lastEdgeTick) / 100; // 以STM频率100MHz为例 if (deltaUs < ENC_MIN_EDGE_INTERVAL_US) { return; } g_lastEdgeTick = nowTick; // 方向判断:A相边沿到来时,读B相电平 if (IfxPort_getPinState(ENC_B_PORT, ENC_B_PIN) == TRUE) { if (IfxPort_getPinState(ENC_A_PORT, ENC_A_PIN) == TRUE) { // A相上升沿,B相为高,反转 g_encoderCount--; } else { // A相下降沿,B相为高,正转 g_encoderCount++; } } else { if (IfxPort_getPinState(ENC_A_PORT, ENC_A_PIN) == TRUE) { // A相上升沿,B相为低,正转 g_encoderCount++; } else { // A相下降沿,B相为低,反转 g_encoderCount--; } } }

这段代码里最容易被忽略的是STM时间戳的频率。我示例中以STM0频率100MHz为例,也就是1个tick等于10ns。如果实际工程里STM被配置成其他频率,deltaUs的计算除数要跟着改,否则滤波时间会差一个数量级。这个“频率核对”习惯,建议在写任何时间相关内容时都先确认一遍。

3.4 速度计算与主循环调用

ISR负责累加计数,速度计算放到主循环或周期性任务里做,不要在中断里做。

float32 Encoder_CalculateSpeed(uint16 periodMs) { sint32 currentCount = g_encoderCount; sint32 delta = currentCount - g_lastCount; g_lastCount = currentCount; g_calcDelta = delta; // 转速 = 计数差值 / 每转计数 / 时间(分钟) // periodMs换算成分钟: periodMs / 60000 float32 rpm = (float32)delta / (float32)ENC_COUNTS_PER_REV / ((float32)periodMs / 60000.0f); return rpm; }

主循环里的典型用法:

while (1) { if (g_1msFlag) { g_1msFlag = 0; // 每10ms计算一次转速 float32 rpm = Encoder_CalculateSpeed(10); // 位置直接用累计计数 sint32 position = Encoder_GetCount(); } }

900行左右的完整驱动代码当然不止这么点,但核心逻辑就是这些。ERU通道配置属于平台相关代码,按自己的板卡和iLLD版本补全即可。ISR里的计数判断逻辑是真正的“软解码灵魂”,这部分可以直接照抄。

4. 工程化细节:滤波、速度换算与中断优先级

4.1 抖动滤波:不能靠中断里的延时

旋转编码器的机械触点、长线传输、电机EMI,都会在A、B信号上叠加毛刺。如果用示波器看,正常边沿应该是干净利落的跳变,但干扰严重时会出现反复抖动,也就是在几个微秒内电平来回跳变好多次。

软解码中最忌讳的做法是:在ISR里调用延时函数去“等电平稳定”。中断里做延时不仅阻塞其他中断,还会让CPU占用率暴涨。正确做法是用时间戳滤波:记录上一次有效边沿的时刻,如果这一次边沿距离上次不足设定阈值,就认为它是抖动,直接丢弃。

我代码里用的ENC_MIN_EDGE_INTERVAL_US就是干这个的。阈值设置多大?太小没效果,太大会把真实高速边沿也滤掉。建议先从20us起步,如果编码器转速高,再往下调到5us;如果现场干扰严重,可以往上调到100us。这个值最终应该是实验调出来的,不是拍脑袋定的。

另外,硬件上的RC滤波也很重要。如果编码器线和MCU距离超过30cm,建议在A、B引脚对地各加一个100pF到1nF的小电容,能有效抑制高频毛刺。软件滤波负责兜底,硬件滤波负责根治,两者不冲突。

4.2 从计数到物理量的换算

编码器计数的最终目的,要么是位置,要么是速度,要么两者都要。位置就是累计计数值本身,但要注意溢出问题。TC264的sint32计数范围约±21亿,按每转5000计数计算,足够覆盖几十万转,基本不用担心溢出;但如果用了64位计数,访问时要注意原子性。

速度换算公式我在代码里已经写了,这里再展开举个例子:

  • 编码器物理线数P = 2500;
  • 软解码两倍频,每转计数 = 5000;
  • 速度计算周期 = 10ms;
  • 在10ms内读到计数差值为50。

那么转速就是:

转速 = 50 / 5000 / (10 / 60000) = 60rpm

也就是说,每10ms计数值变化50,对应60rpm。这个例子里如果你用四倍频,每转计数就变成10000,同样的50个计数只对应30rpm。倍频系数变了,换算公式里分母一定要同步改,否则速度值会翻倍或减半,这也是调试时很容易犯的错。

4.3 中断优先级与CPU核绑定的经验

AURIX是多核芯片,TC264是双核,TC3xx甚至到六核。ERU中断可以绑定到任意一个核,但实际使用中我建议固定放在一个核上。最合理的方式是:把编码器中断放到控制算法所在的那个核,减少跨核数据同步的麻烦。

优先级方面,编码器中断属于硬实时事件,优先级应该设置得比较高,但不能高于系统时钟节拍和紧急故障中断。我这边把编码器中断优先级设为10,系统节拍设为8,串口等慢速外设设为30左右。原则是:中断服务频率越高、实时性要求越严格,优先级越高;但优先级高的ISR尽量不要做耗时操作,否则会饿死低优先级中断。

AURIX的中断优先级和向量绑定,在iLLD里主要通过IFX_INTERRUPT宏和IfxSrc_init完成。IFX_INTERRUPT(EncoderA_ISR, 0, 10)里面的0表示CPU0,10表示优先级。如果工程里用了CPU1做主控,记得把0改成1,否则中断会一直挂在CPU0上,可能出现两个核互相等待的隐性死锁。

5. 实际调试中踩过的坑

5.1 方向反了:不是改线,而是确认判断逻辑

第一次上电,手动转编码器,串口打印的计数值跟预期方向完全相反。很多人第一反应是A、B线接反了,然后开始焊线。其实不用。软解码代码里方向判断只是一个if/else,把正转和反转的加减操作对调一下即可。

我习惯做成一个方向宏:

#define ENC_DIRECTION_NORMAL 1 #define ENC_DIRECTION_REVERSED (-1) #define ENC_DIRECTION ENC_DIRECTION_NORMAL

ISR里把计数更新写成:

g_encoderCount += ENC_DIRECTION * 1;

这样后续系统集成时如果发现机械方向不匹配,改一个宏定义就行,完全不用动硬件。这个习惯帮我省过好几次返工。

5.2 上电瞬间计数乱跳

编码器上电瞬间,A、B引脚如果不确定,MCU内部上拉还没建立稳定,外部编码器可能也还没初始化完成,ISR会被一堆伪边沿触发,计数值随机乱跳。

解决办法是分阶段启用:

  • 芯片上电后先把计数变量清零;
  • 引脚配置成上拉输入;
  • 延时100ms到500ms,等编码器供电稳定;
  • 再使能ERU中断。

如果有Z相,更稳妥的做法是:中断全程使能,但每次Z相到来时把计数值重置为预设的零点。这样即使上电瞬间计入了一些乱数,Z相一到就被修正了。

5.3 高速时丢步,低速时没事

这个问题排查起来最费时间。低速转正常,高速转数值偏差变大,甚至停止时计数值和实际角度对不上。原因通常有三个:

第一,滤波阈值太大。ENC_MIN_EDGE_INTERVAL_US设置成100us时,等于要求编码器输出频率不能超过10kHz,超过的边沿全被当成抖动扔掉了。把阈值调小或者干脆用示波器量最小边沿间隔再设置。

第二,ERU只配置了单边沿触发。比如只在上沿中断,那A相的下沿就完全丢失,高速时空占比变化会导致计数值不稳。软解码的“软”不代表可以少算,必须确保配置的是“上升沿+下降沿”双沿触发。

第三,ISR执行时间太长。比如在ISR里做了浮点运算、打印日志、调用耗时库函数,导致下一个边沿到来时MCU还在处理上一个边沿,自然就丢中断了。ISR里只做整数加减和时间戳判断,其他全部丢到主循环。

还有一次我遇到的情况比较特殊:编码器信号线太长,边沿上叠加了反射振荡,10kHz以下看不到问题,一跑到几十kHz就疯狂误触发。后来在A、B端各加了一个100pF电容,再配合软件滤波,问题立刻消失。这类问题没有示波器基本查不出来,所以做编码器调试,示波器是刚需。

6. 还能怎么扩展:硬件方案与TC3xx迁移

6.1 引脚允许时,试试GPT12硬件旋转计数器

如果项目还在方案阶段,引脚布局没有定死,我会建议优先评估GPT12的Rotary Counter模式。这是TC264自带的硬件正交解码能力,配置正确后A、B相方向判断和计数由外设自动完成,CPU零负担。

iLLD里提供了现成的接口,典型思路是:

  • 把A相接T2IN,B相接T3IN;
  • 初始化T2为旋转计数器模式;
  • 使能方向识别;
  • 读计数值直接用库函数取回。

具体接口名包括IfxGpt12_enableRotaryCounter()和IfxGpt12_getRotaryCounter(),这些在AURIX Development Studio的iLLD库中都能找到。硬件方案的优点是稳定性和CPU利用率高,缺点是引脚复用受限。如果软件方案已经跑通,硬件方案可以作为性能优化选项保留到平台迁移时再应用。

6.2 从TC2xx迁移到TC3xx要注意什么

TC2xx和TC3xx在GPIO、ERU、SRC中断体系上保持了很高的兼容性,软解码代码迁移成本不大。主要注意三点:

第一,时钟频率。TC3xx的STM频率和TC2xx可能不同,ISR里的时间戳换算分母要重新确认。

第二,中断节点映射。TC3xx的中断源编号和TC2xx并非完全一致,ERU通道对应的SRC枚举要查最新的IfxScuSrcId定义。

第三,iLLD版本。AURIX Development Studio升级后,部分库函数签名和枚举名可能有调整,直接复制旧代码时编译器会提醒,这时对照当前版本的库头文件修改即可。

我个人的体会是,软解码的本质是“用代码弥补硬件的灵活性不足”。它虽然在极限性能上不如专用硬件模块,但在中小转速、多路编码器、引脚复用受限的工程场景下,非常实用。而且通过亲手实现方向判断和计数逻辑,你对编码器工作方式的理解会完全不一样。如果你也是被硬件复用问题卡住才点开这篇文章,希望这套方案能帮你把时间省下来,留到真正难啃的控制算法上。

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

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

立即咨询