
1. 从转轮上的几根线说起正交编码器到底解决了什么问题做电机控制、小车底盘、云台角度反馈的朋友多半都遇到过同一个困惑怎么准确知道电机现在转了多少圈、转向是正还是反用霍尔传感器只能测个大概用光码盘配合普通IO读又得盯着电平边沿数脉冲稍微一转快了程序就乱套。直到你接触了正交编码器Quadrature Encoder再配合STM32定时器自带的编码器模式这件事才真正变得优雅起来。正交编码器本质上就是一种通过两组相位相差90度的方波信号来传递位置和方向信息的传感器。A相和B相各自输出脉冲电机每转过一个固定角度就产生一个边沿。因为两相信号之间有90度的相位差所以控制器只需要判断A相和B相谁先跳变、谁后跳变就能知道轴是正转还是反转。更关键的是利用A、B两相的共同边沿可以把一圈的物理分辨率直接翻四倍这就是常说的4倍频。比如编码器一圈输出500个脉冲4倍频之后控制器每圈能分辨2000个计数单位。STM32的定时器外设在设计上就内置了编码器接口模式。也就是说硬件层面已经帮你把A、B两相的判向和计数逻辑做完了不需要外部判向电路也不需要占用CPU中断去逐个边沿判断。你只需要把编码器的A、B相接在定时器的两个通道输入上配置好工作模式计数器就会自动加一或减一整个过程零CPU负担。对工程实践来说这意味着三件事接线简单、实时性好、分辨率高。接线方面只需两根信号线加电源地实时性方面计数由硬件完成不会因为中断响应延迟而丢脉冲分辨率方面天然支持4倍频不用额外算法就能拿到更精细的位置信息。我最早接触这个功能是在做一个双轮差速小车的时候。当时用的是500线增量式编码器加直流减速电机MCU是STM32F103RCT6。最初的方案是用外部中断读A相再用GPIO判断B相电平来定方向结果车速一快中断频繁触发主循环直接卡死测速数据还时不时跳变。后来换用定时器的编码器模式整个测速链路瞬间轻松了。本文就围绕STM32CubeMX中的正交编码器配置完整讲一遍原理、配置步骤、代码集成和实际踩坑经验给正准备做运动控制的朋友一个可直接参考的路径。2. 硬件层的关键判断编码器类型、接口电压和定时器选型2.1 增量式编码器与绝对式编码器的区别谈到正交编码器第一件事是分清增量式和绝对式。STM32定时器的编码器模式主要配合增量式编码器使用。增量式编码器输出A、B两相方波可能还有Z相零位脉冲它的特点是只输出相对位置变化量断电后无法记忆绝对位置。绝对式编码器直接输出当前角度的二进制编码值一般走SPI、SSI或者并口不走定时器编码器模式这条路。我自己项目里用过两类增量式编码器一种是光电式码盘上刻着密密麻麻的栅格通过光栅透射产生方波信号另一种是磁式利用磁阻传感器感知磁铁旋转角度变化输出也同样是A、B正交信号。光电式精度高但怕尘土油污磁式抗污染能力强价格相对便宜在小型机器人关节和电机尾轴上很常见。STM32的编码器模式对这两类都适用因为归根到底它只识别电平跳变和相位关系不在乎信号是光还是磁产生的。2.2 信号电平匹配3.3V与5V编码器的处理编码器供电电压常见的有5V和3.3V两种。许多工业级编码器都是5V供电输出高电平是5V这个电压直接进STM32的GPIO是有风险的。STM32F1系列的IO容忍5V输入F4系列的部分IO也标注了FTFive-volt tolerant属性但不是所有引脚都支持。稳妥的做法是在CubeMX里查看引脚定义表确认选择的通道引脚带FT标识否则串电阻分压之后再进MCU。我在一个项目中用的是欧姆龙E6B2-CWZ6C额定供电5V到24V输出是开集电极方式必须外部加上拉电阻才能有明确的高电平。那时候为了省事直接把输出信号引到F103的PA0和PA1上实测能用但波形边沿比较缓后来在信号线上并联了100pF电容去毛刺波形立刻干净了不少。如果你的编码器是推挽输出边沿会比较陡直接接3.3V供电的编码器则最简单完全不用考虑电平转换。下表是我判断编码器接口适配性时常用的参考编码器输出类型输出电平是否可直接接STM32 F1是否可直接接STM32 F4处理方式推挽输出3.3V0~3.3V是是直接连接推挽输出5V0~5V多数引脚可仅FT引脚可确认FT或电平转换开集电极输出需外部上拉视上拉到3.3V或5V视上拉到3.3V或5V上拉到3.3V最安全差分输出RS-422差分信号否否需要差分转单端电路2.3 定时器选型为什么优先选带编码器模式的通用定时器STM32家族里高级定时器TIM1、TIM8通用定时器TIM2、TIM3、TIM4、TIM5等都支持编码器接口模式。选型时主要看三点可用的通道引脚是否和你的PCB走线冲突、计数器位宽、以及是否有其他外设抢占同一引脚。F103这种老经典TIM2和TIM3的通道引脚映射很常用。TIM2的CH1和CH2可以映射到PA0、PA1或者通过重映射到PA15、PB3TIM3的CH1和CH2可以映射到PA6、PA7或者重映射到PB4、PB5。如果你用了JTAG调试口PA15、PB3、PB4这些引脚默认被调试功能占用需要先关闭JTAG才能当普通IO用。不要小看这一步很多人配置了半天引脚就是没反应最后发现是JTAG占用的问题。计数器位宽方面F103的TIM2和TIM5是32位计数器TIM3和TIM4是16位。高速旋转时16位计数器在极高分辨率下可能很快溢出需要额外处理溢出中断。32位计数器则宽裕得多。同样是500线4倍频电机最高转速3000转/分钟每秒产生的脉冲数是500 × 4 × 50 10000016位计数器65535的上限在一秒内一定会溢出。虽然可以通过溢出中断配合扩展位数但如果你同时做多路电机控制溢出中断会时不时打断主流程增加复杂度。这也是我后来做四轮小车时优先选TIM2的原因32位计数器让位置计数变得非常省心。另外CubeMX的引脚分配视图里可以直接搜索TIM2_CH1、TIM2_CH2这样的复用功能标识谁和谁共用一个引脚会高亮显示底层引脚冲突一目了然。选定时器时我习惯先在CubeMX里把引脚分配方案拖一遍确认没有冲突再回去画PCB能省掉后面不少飞线改板的麻烦。3. 定时器编码器模式的内部机制计数方向判定的硬核逻辑3.1 为什么硬件判向比中断判向更可靠软件判向的思路通常是A相触发外部中断进中断后读B相电平高电平代表正转低电平代表反转。这个方法在小车慢速运行时没问题但转速起来之后中断的响应延迟会引入判断误差。更麻烦的是如果A相毛刺较多可能在极短时间内触发多次中断造成计数错误。硬件编码器模式没有这个问题。STM32的编码器接口逻辑同时监视两个输入信号在每个信号边沿处进行一次采样综合两个信号的电平状态判断方向。因为判断逻辑完全由硬件组合电路实现不依赖指令执行速度所以只要信号频率在定时器时钟能处理的范围内就不会出错。判断方向和计数的本质逻辑可以理解成一个两输入的状态机。A、B两相各有两个电平状态高、低组合起来有四种状态。编码器每转过一个最小角度单位组合状态就变化一次变化的方向则决定了计数器是加一还是减一。这有点像我们判断一个人往哪边走看到脚印从左边挪到右边就知道他往右走了。硬件在每个边沿看的是上一个状态和当前状态的关系而不是单看一路信号。3.2 三种计数模式为什么不选X1和X2直接上X4STM32的编码器模式可以在CubeMX里配置三种计数方式X1模式只在一个通道的边沿计数X2模式在两个通道的边沿计数X4模式在四个边沿计数也就是每个完整信号周期产生四个计数脉冲。三种模式的差异直接反映在分辨率上。X1模式一圈计500个数以500线编码器为例X2模式一圈计1000个数X4模式一圈计2000个数。很多人会纠结选哪个好我的建议是只要计数器位数够用直接X4模式。X4模式不光分辨率最高而且每个边沿都在更新计数器角度变化信息更平滑低速时的测速表现也更细腻。代价仅仅是计数器溢出更快但在32位计数器面前这个代价基本可以忽略。CubeMX中的配置项叫做Encoder Mode下拉里会出现Encoder Mode TI1、Encoder Mode TI2、Encoder Mode TI1 and TI2三个选项。第一个对应X1模式第二个对应X1模式只是计数沿选择的通道不同第三个才是X4模式。填的时候注意别选错我早期就误选过Encoder Mode TI1结果分辨率只到了一半查了半天资料才发现是这里的问题。3.3 计数方向、溢出中断和Counter Period的配合逻辑编码器模式下计数器是一个双向计数器。有人说编码器模式下定时器的ARPE自动重装载预装载和重装载值没有意义这句话是不准确的。编码器模式下计数器会在0和自动重装载值之间来回计数上溢和下溢都会触发更新事件。如果你把重装载值设为默认的65535计数范围就是0到65535如果你设成2000计数范围就变成0到2000计数器到2000后继续正转就回0反转从0往下就跳到2000。这个特性非常有用。假设你的编码器一圈4倍频之后正好是2000个脉冲把Counter Period设为2000那么读回来的CNT值就直接对应当前角度在0到2000之间的位置根本不需要额外做取模运算。做云台和机械臂角度控制时这个特性可以省掉大量运算代码。需要注意的一件事是CubeMX里Counter Period填的值对应的是ARR寄存器它决定计数上限。有人把Encoder Mode配好了但Counter Period还沿用默认值运行起来发现计数到65535就跳变还以为是程序问题。这其实是重装载值没有按编码器一圈的脉冲数来设计的典型表现。下面我用一个表格梳理三种模式下的关键参数建议方便你配置时对照检查配置项X1模式X2模式X4模式一圈计数脉冲500线50010002000Counter Period建议值499或500999或10001999或2000低速测速细腻度一般较好最佳溢出中断频率最低中等最高3.4 初始化时清零计数的两种方式项目启动时位置必须从一个已知的状态开始。清零计数器常见有两种做法一种是调用__HAL_TIM_SET_COUNTER(htim, 0)直接把计数器写入0另一种是开启定时器后向TIM_SR寄存器的UIF位写0等待一次更新事件完成清零。实际上最方便的还是第一种因为HAL库提供现成宏一条语句搞定。如果你接的编码器有Z相零位脉冲还可以把Z相接在另一个IO上通过外部中断或者轮询方式检测到零位信号时清零计数器这样每次上电后机器都能自动找到机械原点。对于不带Z相的编码器只能采用上电找限位开关的方式归零或者在机械结构上做一个固定参考点让电机转到参考点时执行一次清零操作。4. STM32CubeMX配置实操一步步点出一份可用的工程4.1 新建工程与时钟树设置打开STM32CubeMX新建工程选择你的具体芯片型号。这里以STM32F103RCT6为例如果你用的是其他型号流程大同小异。先到Pinout Configuration界面左侧Categories栏里找到SYS把Debug设为Serial Wire。这一步很重要否则默认的JTAG会占用部分引脚后面配置编码器输入时可能出现引脚灰色不可选的情况。接着配置RCCHSE选择Crystal/Ceramic Resonator。然后进Clock Configuration把系统时钟设为72MHz。F103的编码器定时器时钟来自APB1APB1的最高频率是36MHz但定时器时钟会自动倍频到72MHz这个细节不用手动干预。不过你需要知道编码器模式下定时器能处理的最高信号频率和这个时钟有关72MHz的定时器时钟足以覆盖绝大多数编码器的信号频率。4.2 TIM2的Encoder Mode配置细节在Categories里找到TIM2点击进去在Mode一栏把Combined Channels设为Encoder Mode。此时下方会出现两个输入通道的配置选项Channel1和Channel2都会自动变成Input Capture Indirect Mode之类的显示不需要手动修改CubeMX会自动关联。接着在Configuration一栏里设置参数Counter Period根据上一章的公式计算如果500线编码器用X4模式填1999。填1999的意思是从0数到1999共2000个值正好对应一圈。Encoder Mode选Encoder Mode TI1 and TI2。Polarity默认的Rising Edge即可不需要改动。只有在编码器信号接反、方向反了的时候才考虑通过取反极性来修正方向。有个小技巧在配置完TIM2后到右侧的Pinout视图确认一下TIM2_CH1和TIM2_CH2映射到了哪个引脚。如果不满意默认映射可以在芯片视图里直接点击引脚旁边的复用功能文字下拉重新选择。这样不用改PCB布局也能灵活调整。4.3 打开中断还是DMA实际场景的选择依据编码器模式单独工作不需要中断也不需要DMA。计数器是硬件自己跑的你想读位置时直接读CNT寄存器就行。但从工程实用性角度建议开启更新中断尤其是需要做测速或者位置溢出管理的时候。在NVIC Settings标签页里勾选TIM2 global interrupt然后在代码里实现定时器周期中断回调就能实现定期采集位置值。如果你只是偶尔读取一下当前位置比如操作员按键触发的机械臂点动就不必开中断轮询读取CNT足够。开了中断反而会频繁打断主循环增加功耗和复杂度。选择标准只有一个你是否需要在计数溢出边界进行处理或者需要恒定周期采集数据。需要就开不需要就不开。DMA在编码器场景下的用途不大典型的DMA用法是配合ADC连续采样模拟量或是配合串口收发数据编码器计数本身没有DMA的需求。如果你看到别人在编码器项目里用DMA多半是用DMA搬运周期采样的位置数据到内存数组做的是后级数据处理不是编码器模式本身的要求。4.4 生成代码前的最后检查清单点击Project Manager设置好工程名称、存储路径和工具链。这里有几个容易忽略的检查点需要确认IDE选项要和本机安装的版本对应MDK-ARM选对应版本或者选STM32CubeIDE。代码生成选项里Generated files框下勾选Generate peripheral initialization as a pair of .c/.h files per peripheral这样每个外设单独生成文件和头文件后续维护方便。如果勾选了Initialize all peripherals with their default settings可能会把没配置的外设也生成初始化代码增加编译时间建议不勾。确认无误后点击GENERATE CODE。生成的工程放在你指定的路径下打开MDK或者CubeIDE就能直接编译。注意第一次编译之前在魔术棒或项目属性里检查C/C编译器选项是否选中了Use MicroLIB。STM32标准库全功能时需要较少栈空间也会影响printf之类的重定向这些虽然不是编码器专属配置但能让后续调试顺畅不少。5. 生成代码解读与业务代码集成怎么把CNT变成有用的速度5.1 HAL库初始化流程里发生了什么打开生成的tim.c文件你会看到MX_TIM2_Init函数里面有几段关键代码。第一段是会话语句和结构体赋值配置Prescaler、CounterMode、Period和ClockDivision。需要特别注意的是编码器模式下ClockDivision位置和采样控制有关一般默认值即可不要去调整预分频值。HAL_TIM_Encoder_Init这个函数比普通定时器初始化多做了什么它会调用定时器底层初始化函数把编码器模式写入到定时器的控制寄存器里并配置两个输入通道为映射模式。更重要的是它设置了输入滤波器的相关参数。在CubeMX界面的Input Filter选项里你可以填0到15的数字。这个滤波器是数字信号滤波器用于滤除信号线上的窄毛刺。实际工程中如果编码器线缆较长或者电机电磁干扰较重把输入滤波调到2到4左右能大幅减少误计数。调太高会让有效信号的最小脉宽变长影响极限转速下的计数准确度。5.2 启动编码器接口HAL_TIM_Encoder_Start和Start_IT的区别代码生成后不会自动启动定时器。你需要在主程序的初始化阶段调用HAL_TIM_Encoder_Start来启动编码器接口。这里有一个常见的坑有人用了编码器模式但忘了启动定时器CNT寄存器永远是0怎么转电机都没反应查了老半天。如果开启了更新中断则要调用HAL_TIM_Encoder_Start_IT这样才能在计数器溢出时触发中断回调。不带_IT的版本永远进不了中断。不要记混这两个接口的名字我在一个四轴机械臂的项目里就吃过这个亏只调了Start没调Start_IT中断服务函数里怎么调试都不进最后对着手册逐行查才发现是调用错了API。启动编码器之后读位置的接口是__HAL_TIM_GET_COUNTER(htim2)。这个宏返回当前计数器的值也就是编码器从启动或上次清零到现在的位置变化量。如果当前方向是反转读回的值会从计数上限往下递减你不需要判断正负CNT值本身就是有方向性的位置信息。5.3 周期测速两次采样差值与M法测速编码器最常见的应用场景是测速度。M法测速的思路是在固定时间窗口内统计脉冲数用计数差值除以时间得到速度。实现起来非常简单开启一个基础定时器比如TIM6配置成1ms中断在中断回调里读取CNT值减去上一次的CNT值差值就是1ms内的脉冲变化量。用这个差值乘以一个换算系数就能得到转速。换算系数怎么算举个例子编码器500线4倍频后一圈2000个脉冲1ms采到100个脉冲那么每秒就是100000个脉冲除以2000就是50圈/秒也就是3000转/分钟。代码里可以写成 speed_rpm (delta_count * 60) / (2000 * 0.001) / 60简化之后其实就是按比例算。为了防止计数器溢出带来的跳变如果两次差值绝对值超过计数范围的一半就做一次补偿修正。比如Counter Period设为1999如果这次读到100上次读到1900直接算差值是-1800但实际可能是正转了200个脉冲发生了回绕需要判断差值大于半周期就减掉一个满周期。这个判断逻辑是测速代码里最容易出错的地方务必处理。5.4 和PWM控制配合一个位置闭环的最简样例有了速度和位置信息就能做最简单的闭环控制。比如你要让电机转到设定位置就可以写一个比例控制器PID里只保留P项int32_t target 1000; // 目标位置 int32_t current __HAL_TIM_GET_COUNTER(htim2); int32_t error target - current; int32_t output Kp * error; // 输出限幅到PWM占空比范围 __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, output);这里的Kp需要实际调试调小了响应慢调大了会振荡。通常的做法是先让电机在一个固定占空比下运行观察编码器读数是否稳定再逐步加大Kp观察响应曲线。完整的PID还要加上积分项消除静差和微分项抑制超调这里只是演示闭环控制的框架足以说明编码器数据如何注入到控制链路中。6. 实测踩坑记录从波形毛刺到方向反了我遇到过的五个典型问题6.1 A、B相接反导致计数方向反转这是新手最容易碰到的问题。STM32CubeMX中默认的计数方向是A相超前B相为正转如果你的编码器线序恰巧反了电机正转时计数器的值反而在减小。排查这个问题很简单手动缓慢正转电机打开调试器看CNT值的变化方向。如果发现反了有两个解决办法一是在硬件上把A、B两相信号对调二是在CubeMX的Polarity设置里把其中一个通道的极性设为Falling Edge。从项目维护角度看改软件极性更省事因为不用动接线。需要注意的是改极性不仅会影响计数方向还会影响计数边沿的位置本质上等于把采样点平移了90度。如果你同时还用了Z相零位零位脉冲的位置和计数器的匹配关系可能会因此偏一位不过对大多数应用没有影响。6.2 布线太长导致波形边沿抖动数字滤波器的正确设置编码器线缆超过30厘米又没有采用双绞屏蔽线时A、B相波形边沿容易出现振铃和毛刺。毛刺导致的结果是异常计数有时正转一下计数器跳好几个数有时反转时计数反而增加。逐一对硬件进行改动可能比较麻烦但STM32定时器自带的输入滤波器能在软件层面解决大部分问题。在CubeMX的Input Filter字段中填入合适值就可以开启滤波。这个滤波器的原理是只有当信号连续稳定N个采样周期后才确认电平变化。Filter值越大抗干扰越强同时对真实信号的延迟也越大。编码器转速不高时直接把滤波值设到8都行但高速场合需要核算一下最大可接受延迟。以72MHz定时器时钟为例Filter值为8时最短可识别的信号脉宽大约是8个采样周期即大约110纳秒的脉冲宽度要求。如果编码器最高转速下信号脉宽小于这个值就会丢脉冲。6.3 编码器零点漂移Z相清零与CNT自动回绕的区别很多编码器带有Z相也叫Index信号每圈出一个脉冲。利用Z相可以精确确定机械零位。有人直接把Z相接在普通IO上在中断里用软件清零CNT。另一种实现方式是利用定时器的编码器接口和Z相输入的特性在硬件层面自动复位计数器但STM32的编码器模式并不内置Z相清零逻辑除非额外使用定时器的外部触发复位功能。如果你需要Z相清零最常规的做法就是外部中断加软件清零。不过要小心Z相脉冲宽度和A相脉宽相关如果Z相信号在电机高速旋转时变得很窄外部中断可能会漏触发。我用的E6B2-CWZ6C编码器Z相脉宽是A相脉宽的四分之一低速完全没问题高速时最好用输入捕获来测量脉宽避免漏掉。6.4 计数器溢出处理在CubeMX里留足余量的设计思路如果你用的是16位定时器计数器溢出是一个绕不开的问题。以500线编码器X4模式为例Counter Period设为1999那么它范围只有0到1999远小于65535这种情况下计数器每隔一圈就会溢出一次。每次溢出都需要在中断里把溢出计数值加到一个软件扩展变量里保证位置读数的连续性。实际上处理溢出最简单的方式是NVIC开启更新中断在中断回调里判断更新事件的方向。如果读取SR中的DIR标志位为0表示向下计数即反转溢出为1表示向上计数即正转溢出。根据方向给软件计数变量加一或减一最终位置值等于扩展变量乘以(Period 1)再加上当前CNT值。这块逻辑虽然不复杂但涉及临界段保护的问题要在中断里操作共享变量时注意加临界区保护防止主循环读数据时读到一半被中断改写造成高字节和低字节属于不同时刻的快照。6.5 机械安装误差造成的轻微零速抖动最后说一种比较隐蔽的情况电机静止时计数器读数却在小幅跳动。这种情况多半是机械振动带动编码器码盘微小摆动A、B相在临界边沿附近轻微颤抖导致硬件捕捉到多次跳变。解决方法和毛刺类似适当增大输入滤波值同时在应用层做一个小范围的死区判断连续多次读到同一位置才认为电机静止。这一类问题不会影响大范围运动控制但如果你在做云台增稳或精密定位就非常致命。我在做小型云台时零速抖动导致画面缓慢漂移排查了很久才发现是电机减速箱的微小回程间隙带动编码器码盘抖动最后通过调高输入滤波器值和增加死区阈值解决了。这类经验很难在芯片手册里找到只能靠现场调试总结。7. 数据链路之外为什么我强烈建议你把编码器数据流单独封装7.1 底层寄存器操作与上层控制逻辑解耦写编码器相关代码时我强烈建议把编码器的读取和处理封装成独立模块不要在主循环里直接到处调用__HAL_TIM_GET_COUNTER。原因很简单第一如果以后换了MCU平台只需要修改底层封装上层控制逻辑完全不用动第二相同的位置数据会在不同地方用到比如显示、闭环控制、日志如果直接散落在各处代码改起来苦不堪言。我自己习惯的封装方式是这样的单独建一个encoder.c和encoder.h提供Encoder_Init、Encoder_GetPosition、Encoder_GetSpeed、Encoder_Clear等接口。在上层代码里所有模块都只调用这几个接口看不到任何硬件寄存器操作。调试的时候也只需要在这几个接口里加入日志打印就能看到全系统的位置变化情况。7.2 一种更可靠的位置累加方法定时采样配合溢出补偿对于需要长时间记录位置的应用单纯依赖CNT寄存器是不够的因为溢出清零会丢数据。推荐的做法是用固定周期定时器中断比如1kHz读取一次CNT值并累加到软件位置变量里同时处理溢出回绕。这样即使主程序被其他任务阻塞位置也不会丢。举个具体实现例子static int32_t last_count 0; static int32_t position_sum 0; #define COUNTER_PERIOD 1999 void Encoder_Tick_1ms(void) { int32_t cur __HAL_TIM_GET_COUNTER(htim2); int32_t delta cur - last_count; if (delta (COUNTER_PERIOD 1) / 2) delta - (COUNTER_PERIOD 1); else if (delta -(COUNTER_PERIOD 1) / 2) delta (COUNTER_PERIOD 1); position_sum delta; last_count cur; } int32_t Encoder_GetPosition(void) { return position_sum; }这个方法把溢出的判断条件统一封装在一个中断函数里主程序在任何时刻读取的都是连续的软件累加位置彻底摆脱了计数回绕的困扰。而且position_sum是32位有符号变量即便长时间运行也不容易溢出实际项目中一年半载跑下来都没问题。7.3 使用场景扩展从单轴测速到多轴同步如果你的项目不只是一个电机而是像机械臂、四轮小车这样多个电机协同工作每个电机一个定时器测速频率要保持一致控制周期也要尽量同步。最简单的方法是利用同一个基础定时器触发所有电机的采样中断在同一个中断回调里完成所有编码器的数据采集和速度解算。这样不同轴之间的数据天然同步控制解算不容易出现因为时间戳不一致导致的偏差。多轴系统里的另一个注意点是所有编码器共用一个电源时多个电机同时启动会造成电源电压跌落可能让编码器输出波形畸变。解决方法是每个编码器电源附近都加一个去耦电容并且在MCU端的信号线上加RC滤波或施密特触发器缓冲。这个细节是我们在做四轮小车时发现的当时只给主控供电一个电机启动瞬间编码器读数就乱跳排查了很久才发现是电源轨受到干扰。8. 从配置到稳定运行的最后一段路我最想让你记住的三件事回头再看整个STM32CubeMX正交编码器方案其实主要就三件事。第一原理上要搞懂A、B两相正交信号如何通过相位关系产生位置和方向信息硬件编码器模式为什么能替代中断判向方案。第二配置上要选对定时器、选对引脚、选对Encoder Mode和Counter Period这三样决定了物理层能不能正常计数。第三工程上要做好溢出处理、信号滤波和模块封装这三件事决定了系统长时间跑下来稳不稳定。最后的建议是不要一上来就追求花哨的算法先把编码器数据读准、读稳、读连续再谈后面的PID和运动规划。你把一个编码器通道吃透了其他通道都是复制粘贴的事情。我自己第一次调通编码器模式时看到电机转一圈读数从0走到2000再归零那种踏实感是单纯写GPIO中断判向完全体会不到的。希望这篇内容能帮你少走几个弯路一次就把编码器这块做利索。