首页 / 资讯中心 / 文章详情

STM32F427模拟SSI协议读取绝对式编码器位置:定时器PWM+中断实现详解

STM32F427模拟SSI协议读取绝对式编码器位置:定时器PWM+中断实现详解 ★ FEATURED ARTICLE
做运动控制或者非标设备调试的朋友应该都遇到过这样的场景手里拿到了一个绝对式编码器想通过STM32控制器读取它的位置反馈结果翻开手册一看通讯协议不是常见的UART、SPI而是SSI。更麻烦的是主控芯片STM32F427并没有硬件SSI外设。我这次要分享的就是在STM32F427上用模拟方式实现SSI协议完成绝对式编码器位置反馈通信的完整过程。这套方案解决了什么问题呢简单说就是让F427在没有SSI硬件外设的情况下稳定地读出绝对式编码器的位置数据包含单圈位置、多圈圈数和状态位。整个过程不依赖昂贵的外部芯片只用定时器PWM配合GPIO中断就完成了SSI时序的模拟。如果你正准备在STM32F4平台上接SSI协议的绝对值编码器或者想搞清楚SSI协议到底怎么跟SPI区分、怎么用定时器精确模拟这篇文章应该能帮上忙。1. 拿到这个需求我首先把SSI协议拆成了三件事1.1 SSI到底在传什么时钟、数据还有一条“看不见的帧”SSISynchronous Serial Interface看起来跟SPI很像都有CLK和DATA两根线但实际用起来有非常明显的区别。SPI是“主发从收”每次传输有明确的字节长度而且一般有片选信号SSI则是主机产生一串连续时钟脉冲从机也就是编码器在时钟边沿把位置数据一位一位“推”出来。没有片选没有地址没有寄存器操作命令一个完整的SSI帧就是“一串时钟一串数据位”。所以做模拟之前必须先弄清楚三个关键点数据的输出时序多数SSI编码器是在CLK下降沿更新数据位主机在CLK上升沿去采样这样能保证数据稳定。也有一部分编码器是反的所以第一件事永远是查手册。帧结构常见的SSI帧是“高位在前”连续输出多圈位置、单圈位置以及状态位。位宽常见的有13位、17位、21位、25位等。比如一个25位的编码器可能分配17位给单圈位置、8位给圈数或状态具体怎么拆同样要看手册。空闲电平与静默时间SSI在传输开始前CLK和DATA都处于高电平。当主机拉出第一个下降沿时编码器锁存当前位置并开始输出。传输结束后需要一段静默时间通常5-20us让编码器准备下一次通信。把它拆成“时钟产生、数据采样、帧格式解析”三个子问题之后整个实现思路就清晰了。接下来要做的就是找到合适的手段在STM32F427上把这几个动作按顺序做出来。1.2 时序参数要抠到什么程度才算“够用”我第一次实现SSI模拟时犯过的最大错误是把时序参数想得太“宽容”。SSI虽然最高时钟频率有些编码器能到1MHz但更多工业编码器推荐的通信频率是100kHz到500kHz。而且时钟高电平时间、低电平时间、数据建立保持时间每个参数都有下限。以我手头这款编码器为例推荐的最高频率是500kHz那么一个时钟周期就是2us。如果我用定时器PWM输出周期必须精确控制在2us误差最好小于200ns否则长时间运行后可能出现偶发错位。另外数据采样点也很有讲究。最终我选择在上升沿采样数据因为编码器数据是在下降沿更新的到上升沿时刻数据已经稳定了几个百纳秒采样最安全。这里要特别提醒一点很多人觉得模拟协议就是“拉高拉低延时”这个思路在低速场合也许能跑通但放到工业编码器上CPU延时的抖动会导致采样点不稳定偶尔就会出现一位错位。要保证长期稳定必须用定时器硬件来产生时钟把软件介入降到最低。2. 方案选型为什么是STM32F427以及三种模拟思路的取舍2.1 STM32F427的资源底牌STM32F427这颗芯片用的内核是Cortex-M4F主频最高180MHz带有FPU对于需要做运动控制算法的场景来说性能足够价格相对高端MPU也有优势。关键是它的定时器资源非常丰富多个高级定时器和通用定时器都可以产生PWM而且有互补输出、DMA请求、更新中断等机制正好可以用在SSI时钟生成上。另一个选它的理由在于引脚电平兼容性好。F427部分引脚是5V容忍FT引脚在处理一些电平不完全匹配的外部编码器接口时直接连也能工作。当然工业编码器一般是RS422差分输出需要加一个RS422接收器转成单端TTL电平F427只需要负责跟转换芯片对接就行了。2.2 三种模拟SSI的实现路线对比既然芯片本身没有SSI控制器那就要在用软件模拟还是硬件外设“曲线救国”之间做选择。我评估了三条路线各有优劣这里直接把对比列出来方便你根据自己的情况判断。实现方案时序精确度CPU占用复杂度适用场景GPIO翻转延时低受中断和代码执行时间影响大高全程占用CPU最低频率低于100kHz、对稳定性要求不高的场景定时器PWM中断采样高PWM周期由硬件保证中断只做采样中每个时钟周期中断一次中本次选用适合100kHz-1MHz的正常工业通信SPI复用定时器触发DMA高硬件全程参与几乎不占CPU极低高需要把位置读取和处理完全解耦的场景第一条路线最直观但我不推荐作为量产方案。GPIO翻转加延时意味着每个时钟周期都要CPU实时参与一旦有更高优先级的中断插入时序立即抖动。第二条路线用PWM硬件产生时钟中断只负责在正确的沿采数据时序抖动大幅减少。第三条路线是进阶玩法用SPI外设去“扮演”SSI主机通过DMA搬运数据CPU完全解放但配置复杂而且SPI的数据长度是8/16位对齐的处理非8位倍数的SSI帧时比较别扭。综合看下来PWM中断的路线平衡了可靠性和开发效率也是我最后实际采用的方案。3. 实操定时器PWM方案完整实现SSI位置反馈通信3.1 硬件连接从编码器到MCU的信号链路实际接线时绝对式编码器大多是RS422差分接口也就是说CLK和DATA都有正负两根线。我们主机的CLK需要经过RS422发送器转成差分信号给编码器编码器返回的DATA差分信号则需要经过RS422接收器转成单端TTL电平给MCU。我的做法是用一片AM26LV31作为时钟发送器负责把F427输出的PWM时钟转成差分CLK用一片AM26LV32作为数据接收器把编码器返回的差分DATA转成3.3V单端信号直接接到F427的GPIO输入脚。如果你的编码器是单端TTL输出那就简单了只需要注意电平匹配和共地。具体引脚分配上我把定时器PWM输出引脚选在了PA6对应的复用功能是TIM3_CH1。数据输入引脚选在PC4配成上拉输入。这里有个经验数据线虽然本身是推挽输出驱动但建议还是开启内部上拉防止编码器在未上电或复位瞬间把数据线拉低导致误触发。供电方面编码器如果要求的电压是5V最好用独立的稳压源供电不要直接从MCU的3.3V上拉。工业编码器长线传输时的电压跌落比想象中严重我曾经因为供电不足遇到过位置偶发跳变的问题后面会细说。3.2 定时器初始化与PWM时钟生成核心思路是让TIM3产生50%占空比的PWM频率设为500kHz。这样CLK的高电平时间和低电平时间都是1us满足大多数SSI编码器对时钟高低电平的要求。#define SSI_CLK_PORT GPIOA #define SSI_CLK_PIN GPIO_PIN_6 #define SSI_DATA_PORT GPIOC #define SSI_DATA_PIN GPIO_PIN_4 #define SSI_FRAME_BITS 25 #define SSI_CLK_HZ 500000U TIM_HandleTypeDef htim3; void SSI_GPIO_Init(void) { GPIO_InitTypeDef gpio {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOC_CLK_ENABLE(); /* PA6复用为TIM3_CH1推挽输出内部上拉 */ gpio.Pin SSI_CLK_PIN; gpio.Mode GPIO_MODE_AF_PP; gpio.Pull GPIO_PULLUP; gpio.Speed GPIO_SPEED_FREQ_VERY_HIGH; gpio.Alternate GPIO_AF2_TIM3; HAL_GPIO_Init(SSI_CLK_PORT, gpio); /* PC4为数据输入上拉输入 */ gpio.Pin SSI_DATA_PIN; gpio.Mode GPIO_MODE_INPUT; gpio.Pull GPIO_PULLUP; HAL_GPIO_Init(SSI_DATA_PORT, gpio); } void SSI_TIM_PWM_Init(void) { TIM_OC_InitTypeDef oc {0}; __HAL_RCC_TIM3_CLK_ENABLE(); htim3.Instance TIM3; htim3.Init.Prescaler 0; htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period (SystemCoreClock / SSI_CLK_HZ) - 1; htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(htim3); oc.OCMode TIM_OCMODE_PWM1; oc.Pulse (SystemCoreClock / SSI_CLK_HZ) / 2; oc.OCPolarity TIM_OCPOLARITY_HIGH; HAL_TIM_PWM_ConfigChannel(htim3, oc, TIM_CHANNEL_1); }配置里的关键是Period和Pulse计算。F427主频按180MHz算要得到500kHz的PWMARR是359CCR是180这样计数器从0数到359刚好一个完整周期前180个计数输出高电平后180个计数输出低电平。如果你碰到的编码器最高支持1MHz就把SSI_CLK_HZ改成1000000ARR对应变成179CCR变成90逻辑是不变的。3.3 核心读写函数在PWM的中断里“顺手”采样PWM产生时钟之后剩下的问题就是什么时候读数据。前文说了编码器在下降沿更新数据因此我在TIM3的更新中断里读取数据。定时器的更新事件发生在计数器从ARR回0的那个时刻对应PWM刚好从低电平跳变到高电平也就是上升沿。此时数据位已经稳定读取最安全。中断里做的事很简单把PC4的电平读出来移位拼到帧数据里同时记录位计数。当采集完25位后停止PWM输出。volatile uint32_t ssi_frame_raw 0; volatile uint8_t ssi_bit_index 0; volatile uint8_t ssi_frame_ok 0; void TIM3_IRQHandler(void) { HAL_TIM_IRQHandler(htim3); } void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM3) { if (ssi_bit_index SSI_FRAME_BITS) { ssi_frame_raw (ssi_frame_raw 1) | HAL_GPIO_ReadPin(SSI_DATA_PORT, SSI_DATA_PIN); ssi_bit_index; } if (ssi_bit_index SSI_FRAME_BITS) { HAL_TIM_PWM_Stop_IT(htim3, TIM_CHANNEL_1); ssi_frame_ok 1; } } }这块逻辑单独看非常简单但有一个小细节要注意ssi_frame_raw的移位和拼接顺序默认是高位在前帧的第一个bit最终落在最高位。也就是说如果编码器输出顺序是MSB first那么直接对ssi_frame_raw做后续解析就行。完成一帧读取后PWM停止了此时CLK引脚可能会停在高电平或低电平。多数编码器要求通信结束后CLK保持高电平所以我在主循环里或者在停止后手动把PA6拉高。比较稳妥的做法是先把引脚从复用模式切回GPIO输出模式再写高电平。void SSI_ReleaseCLK(void) { GPIO_InitTypeDef gpio {0}; gpio.Pin SSI_CLK_PIN; gpio.Mode GPIO_MODE_OUTPUT_PP; gpio.Pull GPIO_PULLUP; gpio.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(SSI_CLK_PORT, gpio); HAL_GPIO_WritePin(SSI_CLK_PORT, SSI_CLK_PIN, GPIO_PIN_SET); }启动整帧读取的函数就把这些串起来复位帧状态、确保CLK空闲为高、切换成PWM复用、启动定时器中断输出最后等待帧完成标志。uint32_t SSI_ReadFrame(void) { ssi_frame_raw 0; ssi_bit_index 0; ssi_frame_ok 0; /* 确保CLK空闲为高再启动PWM */ SSI_ReleaseCLK(); delay_us(20); /* 切换PA6为TIM3_CH1复用功能 */ GPIO_InitTypeDef gpio {0}; gpio.Pin SSI_CLK_PIN; gpio.Mode GPIO_MODE_AF_PP; gpio.Pull GPIO_PULLUP; gpio.Speed GPIO_SPEED_FREQ_VERY_HIGH; gpio.Alternate GPIO_AF2_TIM3; HAL_GPIO_Init(SSI_CLK_PORT, gpio); HAL_TIM_PWM_Start_IT(htim3, TIM_CHANNEL_1); uint32_t timeout HAL_GetTick(); while (!ssi_frame_ok) { if (HAL_GetTick() - timeout 5) { HAL_TIM_PWM_Stop_IT(htim3, TIM_CHANNEL_1); return 0xFFFFFFFF; } } SSI_ReleaseCLK(); return ssi_frame_raw; }第一次跑通这个函数时我用逻辑分析仪抓了CLK和DATA波形确认CLK一帧刚好25个脉冲DATA在下降沿跳变、上升沿稳定整个帧结构跟编码器手册完全吻合。那种“协议跑通”的成就感说实话还是挺上头的。3.4 从原始帧到真实位置格雷码转换与位域拆解大多数绝对值编码器输出的原始数据是格雷码而不是二进制码这是因为格雷码相邻两个计数之间只有一位变化在工业干扰环境下不容易产生多bit错误。所以拿到25位原始数据后第一步要转成二进制然后再按编码器手册拆分位域。格雷码转二进制的经典算法是从高位到低位逐位异或uint32_t GrayToBinary(uint32_t gray, uint8_t bits) { uint32_t bin gray; while (bits-- 1) { gray 1; bin ^ gray; } return bin; }假设编码器是17位单圈位置加8位圈数/状态位总位宽25位那FOR循环转换完成后bin的高17位是单圈位置低8位是状态和圈数。具体怎么拆要严格跟着编码器手册走不同品牌在高位低位分配上并不统一。还有一个值得注意的点单圈位置值的分辨率跟位数直接相关。17位意味着每圈有131072个计数也就是说编码器一圈被分成了131072份。如果电机轴通过减速器连接最终负载端的位置可能需要把单圈值跟减速比再做一次换算而不是直接用这个值。4. 实测数据与稳定性验证4.1 用逻辑分析仪验证时序协议跑通之后不要急着欢呼先用逻辑分析仪抓一段完整的通信过程。我当时的做法是设置触发条件为CLK下降沿抓取一帧25位数据和前后静默期。正常波形应该是这样的CLK空闲为高第一个下降沿后DATA开始有变化之后每个上升沿时DATA保持稳定。如果发现某个上升沿处DATA正好在跳变说明采样点太靠前了需要想办法把采样时刻往后挪。好在PWM方案里上升沿采样天然满足“数据稳定后再读取”的要求。如果抓到的波形里DATA有一位或几位持续为低先不要怀疑程序检查一下编码器是否正常上电、数据线是否虚接。我有一次排查了很久最后发现是编码器的屏蔽层没有接地导致数据线上叠加了高频干扰波形上一片毛刺。4.2 连续读取的稳定性表现单帧跑通和连续稳定读取完全是两回事。我在调试阶段做了两个压力测试一是让电机以低速匀速旋转连续读取位置并绘制时间序列看有没有跳变二是让电机停止在固定位置长时间静置读取看是否存在“原地抖动”。第一个测试中我遇到了偶发的单bit跳变位置在某一时刻突然多出或减少一个计数。排查下来原因有两个一个是数据线经过长线缆时由于没有用双绞屏蔽线导致差分信号退化为单端信号后引入噪声另一个是中断优先级不够高被其他中断打断时采样点被拖到数据边沿附近。解决办法是改用屏蔽双绞线并且把TIM3中断优先级提到较高档位。第二个测试中“原地抖动”通常意味着编码器本身有噪声或者信号质量差。如果每次重启后位置值都不一样大概率是时序初始化时第一个下降沿之前没有给编码器足够的准备时间。我的解决办法是在启动PWM前把CLK空闲为高的时间延长到20us以上给编码器内部的锁存电路留足响应时间。5. 常见问题与排错手册5.1 六个高频故障现象排查这里整理了一些我在实际项目中遇到或者跟同行交流时总结出来的问题按现象、可能原因、解决办法列出来方便你遇到类似问题时快速排查。故障现象可能原因排查方法与解决办法一直读不到任何数据DATA线一直为高或一直为低编码器未响应检查编码器供电检查RS422接收器输出用万用表量DATA是否随电机转动变化读到的数据偶发跳变一个数采样点靠近数据边沿时序不稳定确认编码器数据更新沿和采样沿必要时降低CLK频率到250kHz读到的数据错位1到2位帧起始判断错误多采或少采了位用逻辑分析仪数清楚每个帧包含的总位数检查是否有起始位格雷码转二进制后位置不连续转换算法位数不正确确认编码器总位宽转换时bits参数是否传对建议打印原始格雷码对照验证电机旋转到某一点后位置跳回0单圈位置回零是正常的但状态位被忽略导致了误解拆包时保留圈数位和状态位把完整多圈数据组合起来看CLK停止后编码器不响应下一帧CLK没有恢复到空闲高电平停止PWM后强制将CLK引脚输出高电平并延时20us再启动下一帧5.2 调试工具与调试顺序建议调试这类模拟协议通信我建议按下面这个顺序来能少走很多弯路。第一步用逻辑分析仪看CLK频率和脉冲个数。不需要接编码器先确认主机侧产生的时钟是准确的500kHz停止位置在25个脉冲后。这一步过了时序基础就有了。第二步接上编码器抓一整帧的CLK和DATA波形。重点看DATA在哪个沿变化、哪个沿稳定然后调整代码里的采样沿。这一步能解决90%的“读出来是乱码”问题。第三步写一个简单的串口打印程序把原始帧和转换后的位置值一起发到上位机。对比码盘转动方向确认位置是否连续、是否和实际机械方向一致。如果反了可能是数据位顺序或格雷码转换方向的问题。5.3 代码健壮性的几个细节核心流程稳定之后我还做了几处健壮性加固这里也一并分享。一是超时保护。SSI_ReadFrame函数里加了5ms超时一旦编码器断线或者通信异常不会让主循环卡死在等待标志上。返回0xFFFFFFFF作为异常值调用方可以据此做故障处理。二是帧状态变量需要加volatile修饰。中断里写的标志主循环里读如果不加volatile开启编译器优化后可能出现标志一直不更新的情况。这也是一个非常隐蔽的坑。三是在解析位置数据之前加一个“数据有效性校验”。有些SSI编码器帧尾会包含校验位或者固定的状态字段可以利用这些信息判断当前帧是否有效避免把异常数据直接用到控制算法里。如果编码器没有校验位可以连续读两帧比较如果位置变化量超过电机最大物理可达速度对应的增量就丢弃当前帧。这个逻辑在低速场合很实用。结束语一个小建议最后说一个我在实际调试中体会最深的事情模拟SSI协议本质上是把硬件能干的事交给定时器去干把只剩“采样”这一步交给CPU去干而不是用CPU去模拟人的手工翻转电平。理解了这一点你不仅能在F427上做好SSI将来遇到任何类似的低速同步协议比如某些光栅尺、角度传感器的私有接口都能用同样的方法论快速落地。把这套定时器PWM中断采样的框架留下来下次接别的编码器改改位宽和采样沿就能复用省下的时间足够你再折腾好几个功能模块。
阅读完成 · 觉得有帮助?
咨询建站