
简介本资源为多圈绝对值编码器STM32测试程序面向嵌入式开发工程师、工业自动化与机器人控制方向的学习者解决基于SSI协议读取多圈绝对值编码器位置信息并完成断电位置保持的实践问题。压缩包共163个文件约3.2MB以46个h头文件与46个c源文件为核心涵盖STM32外设驱动与SSI通信逻辑另含16个d、16个o、15个crf等编译中间文件以及uvprojx工程文件、hex可执行文件、map映射文件和bat编译脚本构成可直接导入Keil运行的完整工程。目前已有3106人学习下载。工程包含初始化配置、SSI收发函数、编码器协议解析、串口日志输出与错误处理等模块读者可据此理解时钟线与数据线的同步时序、数据位宽与极性相位设置并在此基础上修改适配不同型号编码器快速搭建位置检测与调试环境。1. 多圈绝对值编码器STM32测试程序从SSI协议到位置解算的完整落地拿到一个多圈绝对值编码器第一件事不是写代码而是确认它到底怎么把圈数和单圈角度传出来。这个项目就是干这个的用STM32驱动多圈绝对值编码器通过SSI协议读取位置数据解析出当前圈数和圈内角度最终拼出完整的多圈绝对位置。适合正在做伺服电机、机器人关节、升降平台或者任何需要断电记忆位置的嵌入式开发者。你手里如果有一块STM32板子和一个SSI接口的绝对值编码器这份代码能让你在半小时内看到串口打印出真实位置值而不是对着示波器猜时序。2. 硬件接口与SSI协议编码器到底在说什么2.1 多圈绝对值编码器的数据帧长什么样多圈绝对值编码器输出的不是脉冲而是一帧固定长度的二进制数据。常见的有25位、26位、29位等格式具体取决于单圈分辨率和圈数位数。比如一个25位编码器可能是13位单圈12位多圈也可能是12位单圈13位多圈这个顺序必须查手册确认搞反了位置会跳变。SSI协议本质上是同步串行通信编码器作为从机STM32作为主机提供时钟。一帧数据通常包含起始位、数据位、错误位和停止位。以25位数据为例帧结构可能是1个起始位固定为0或1 25个数据位 1个错误位 1个停止位总共28个时钟周期。有些编码器还会在数据位中间插入奇偶校验位这个必须在手册里看清楚。数据位的排列顺序也有讲究。大多数编码器是MSB先出但个别型号是LSB先出。更坑的是多圈数据和单圈数据的拼接顺序不同厂家不一样。我见过一个编码器前12位是圈数后13位是单圈角度但另一个型号正好反过来。所以第一步永远是拿手册确认帧格式别凭经验猜。2.2 STM32端SSI接口的硬件连接方式SSI是单向通信STM32只需要输出时钟和接收数据两根线。时钟线接编码器的CLK数据线接编码器的DATA。如果是差分输出RS422还需要接对应的负端STM32端用差分接收器转成单端再进GPIO或SPI。常见做法是用STM32的SPI外设来模拟SSI时序。SPI的SCK输出时钟MISO接收数据NSS可以不用或者用来控制帧间隔。但SPI有个问题它的时钟极性和相位要跟SSI匹配。SSI通常在时钟上升沿输出数据下降沿采样对应SPI的CPOL0、CPHA1或者CPOL1、CPHA0具体要看编码器手册。如果不想用SPI也可以用GPIO加定时器手动翻转时钟。这种方式更灵活能精确控制时钟频率和帧间隔但代码量大一些。我一般先用SPI跑通遇到时序问题再换GPIO方案。// SSI接口初始化使用SPI1主机模式时钟空闲高第二个边沿采样 void SSI_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; SPI_HandleTypeDef hspi1 {0}; // 使能GPIO和SPI时钟 __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_SPI1_CLK_ENABLE(); // PA5-SCK, PA6-MISO GPIO_InitStruct.Pin GPIO_PIN_5 | GPIO_PIN_6; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; GPIO_InitStruct.Alternate GPIO_AF5_SPI1; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // SPI参数主机8位数据CPOL1CPHA1预分频256 hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_HIGH; hspi1.Init.CLKPhase SPI_PHASE_2EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_256; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; HAL_SPI_Init(hspi1); }这段代码把SPI1配置成SSI主机模式。关键参数是CLKPolarity和CLKPhase它们决定了时钟空闲电平和采样边沿。BaudRatePrescaler设成256是为了把时钟降到编码器能接受的频率一般SSI编码器最高支持1MHz左右STM32主频72MHz时256分频约281kHz比较安全。FirstBit设成MSB因为绝大多数编码器都是高位先出。2.3 时钟频率与帧间隔的取舍SSI编码器的时钟频率不能太高否则内部处理跟不上。手册里一般会写最大时钟频率比如500kHz或1MHz。但实际用的时候我建议先降到100kHz到200kHz跑通再逐步提高。因为时钟太快会导致数据位还没稳定就被采样读出来全是0或者全是1。帧间隔也很重要。连续读的时候两帧之间要留足够时间让编码器准备下一帧数据。有些编码器要求帧间隔至少20微秒有些要求100微秒以上。如果读得太快第二帧数据可能是上一帧的重复或者直接出错。我一般会在两帧之间加一个延时用HAL_Delay或者定时器都行具体延时长度查手册。还有一个坑SSI是单向通信STM32只发时钟不收时钟所以不需要考虑时钟同步问题。但编码器的数据输出有建立时间和保持时间要求如果STM32的采样点太靠前或太靠后都会读错。用SPI的话采样点由CPHA决定用GPIO的话自己在时钟边沿后加几个NOP再采样。3. 位置解算与代码实现从原始位到物理角度3.1 原始数据读取与位拼接SSI一帧读回来的是几个字节的原始数据需要先拼成一个完整的整数再按位段拆出圈数和单圈角度。假设编码器是25位数据STM32用SPI读4个字节32位其中高25位有效低7位是填充或错误位。// 读取SSI原始数据并解析为多圈位置 uint32_t SSI_ReadRaw(void) { uint8_t rx_buf[4] {0}; uint32_t raw 0; // 拉低片选启动一帧传输 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); HAL_SPI_Receive(hspi1, rx_buf, 4, 100); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // 拼接4字节为32位整数 raw ((uint32_t)rx_buf[0] 24) | ((uint32_t)rx_buf[1] 16) | ((uint32_t)rx_buf[2] 8) | ((uint32_t)rx_buf[3]); return raw; } // 从原始数据中提取圈数和单圈角度 void SSI_ParsePosition(uint32_t raw, uint16_t *turns, uint16_t *angle) { // 假设帧格式高12位圈数低13位单圈角度共25位 // 原始数据右移7位对齐到低25位 uint32_t data raw 7; *turns (data 13) 0x0FFF; // 取高12位 *angle data 0x1FFF; // 取低13位 }SSI_ReadRaw函数通过SPI接收4个字节拼成32位整数。注意这里用的是HAL_SPI_Receive而不是HAL_SPI_TransmitReceive因为SSI只需要接收数据时钟由SPI外设自动产生。片选信号用GPIO手动控制拉低开始传输拉高结束。SSI_ParsePosition函数做位段提取。先右移7位把有效数据对齐到最低位然后分别取高12位和低13位。这里的12和13是假设值实际要根据编码器手册修改。如果编码器是29位可能是13位圈数16位单圈角度移位和掩码都要相应调整。3.2 多圈位置合成与溢出处理圈数和单圈角度拼在一起才是完整的多圈绝对位置。但这里有个细节圈数是循环计数的比如12位圈数范围是0到4095超过4095会回绕到0。如果设备需要连续运行很多圈就要在软件里做溢出处理维护一个虚拟的高位圈数。// 多圈位置合成处理圈数溢出 int64_t SSI_GetMultiTurnPosition(uint16_t turns, uint16_t angle, uint16_t max_turns) { static int64_t virtual_turns 0; static uint16_t last_turns 0; int32_t delta 0; // 计算圈数变化量处理回绕 delta (int32_t)turns - (int32_t)last_turns; if (delta (int32_t)(max_turns / 2)) { delta - max_turns; // 正向回绕 } else if (delta -(int32_t)(max_turns / 2)) { delta max_turns; // 反向回绕 } virtual_turns delta; last_turns turns; // 合成完整位置虚拟圈数 * 单圈分辨率 单圈角度 return virtual_turns * (int64_t)(angle 1) angle; }这个函数用static变量保存上一次的圈数和虚拟圈数。每次读取时计算圈数变化量delta如果delta超过max_turns的一半就认为是回绕加上或减去max_turns修正。然后把delta累加到virtual_turns上得到实际圈数。最后用virtual_turns乘以单圈分辨率加上单圈角度得到完整位置。注意单圈分辨率是angle的最大值加1比如13位角度是0到8191分辨率就是8192。这里用angle1是简化写法实际应该用常量表示。3.3 串口输出与位置验证代码跑通后最直观的验证方式是把位置值通过串口打印出来。用printf重定向到USART然后在主循环里定时读取并打印。// 主循环每100ms读取一次位置并打印 int main(void) { HAL_Init(); SystemClock_Config(); SSI_Init(); UART_Init(115200); uint16_t turns 0, angle 0; int64_t position 0; while (1) { uint32_t raw SSI_ReadRaw(); SSI_ParsePosition(raw, turns, angle); position SSI_GetMultiTurnPosition(turns, angle, 4096); printf(Turns: %4d, Angle: %5d, Position: %lld\r\n, turns, angle, position); HAL_Delay(100); } }主循环每100毫秒读一次编码器解析后打印圈数、单圈角度和合成位置。串口波特率115200用USB转TTL接到电脑上就能看数据。验证时手动转动编码器轴观察圈数和角度是否连续变化。如果角度在0到8191之间循环圈数在0到4095之间递增说明解析正确。如果数据跳变或者不变先检查SPI时钟频率是否太高再检查帧格式是否匹配。可以用逻辑分析仪抓SPI的SCK和MISO波形对照编码器手册看数据位是否正确。4. 避坑与常见问题SSI调试中容易翻车的地方4.1 读出来全是0或全是1现象串口打印的原始数据始终是0x00000000或0xFFFFFFFF转动编码器轴也没变化。原因SPI的时钟极性或相位跟编码器不匹配导致采样点落在数据跳变沿上。或者MISO线没接对STM32根本没收到数据。解决先用示波器或逻辑分析仪看SCK和MISO波形。如果SCK有输出但MISO一直是高或低检查接线和编码器供电。如果MISO有波形但数据不对调整SPI的CPOL和CPHA组合四种组合都试一遍。常见做法是CPOL1、CPHA1或者CPOL0、CPHA1。4.2 数据偶尔跳变或出现固定偏移现象大部分数据正常但偶尔跳出一个异常值或者所有数据都偏了一个固定值。原因帧间隔太短编码器还没准备好下一帧数据就被读了。或者起始位和停止位没对齐导致数据整体移位。解决增加帧间隔延时从100微秒开始试逐步减小到稳定为止。检查原始数据的最高位和最低位是否符合手册的起始位和停止位定义。如果整体偏移可能是SPI读的字节数不对比如应该读3个字节却读了4个多出来的位把数据挤偏了。4.3 圈数回绕时位置突变现象编码器转过零点时合成位置突然跳变一个很大的值。原因圈数溢出处理逻辑有误delta计算时没有正确判断回绕方向。或者max_turns参数设错了跟实际圈数位数不匹配。解决确认圈数位数12位圈数max_turns是409613位是8192。检查delta的判断条件正向回绕和反向回绕的阈值要对称。可以在回绕点附近打印delta和virtual_turns观察变化过程。4.4 单圈角度分辨率不对现象角度值范围不是0到8191而是0到4095或者0到16383。原因单圈角度位数搞错了或者移位和掩码参数跟实际帧格式不匹配。解决查手册确认单圈分辨率和圈数位数重新计算移位量和掩码。如果手册写的是“单圈13位”那角度范围就是0到8191掩码是0x1FFF。如果写的是“单圈12位”掩码就是0x0FFF。4.5 多圈数据不更新现象单圈角度正常变化但圈数始终不变。原因多圈数据在帧中的位置搞错了或者编码器的多圈功能没使能。有些编码器需要外部电池或超级电容维持多圈计数断电后圈数会丢失。解决确认帧格式中多圈数据的位置用逻辑分析仪抓原始数据手动解析看圈数位是否变化。检查编码器是否需要外部电源维持多圈计数如果需要确保电池或电容已接好。5. 二次开发与改造把测试程序变成产品代码5.1 从轮询到中断或DMA测试程序用轮询方式读SSI简单但占CPU。产品代码里我一般会改成定时器触发DMA传输或者用SPI中断。定时器每1毫秒触发一次SPI接收DMA自动把数据搬到缓冲区主循环只负责解析。这样CPU占用率从100%降到不到5%还能腾出时间跑其他任务。改造步骤把SSI_ReadRaw里的HAL_SPI_Receive换成HAL_SPI_Receive_DMA在DMA完成回调里置一个标志位。主循环检查标志位有数据就解析。定时器配置成1kHz在更新中断里启动SPI传输。注意DMA缓冲区和解析缓冲区要分开避免数据竞争。5.2 位置数据的滤波与校准原始位置值会有抖动尤其是编码器安装在振动环境里。我通常会在解析后加一个滑动平均滤波窗口大小5到10。如果对实时性要求高用一阶低通滤波截止频率设成10Hz左右。校准方面编码器安装后需要找零点。常见做法是手动转到机械零点记录当时的单圈角度值存到Flash里。之后每次读位置都减去这个偏移量得到相对于机械零点的绝对位置。多圈零点也要校准把当前虚拟圈数设为0。// 一阶低通滤波 float LowPassFilter(float new_val, float old_val, float alpha) { return alpha * new_val (1.0f - alpha) * old_val; } // 零点校准 void CalibrateZero(int64_t current_pos) { zero_offset current_pos; // 存到Flash Flash_Write(ADDR_ZERO_OFFSET, zero_offset, sizeof(zero_offset)); }LowPassFilter的alpha决定滤波强度0.1到0.3之间比较合适。CalibrateZero把当前位置存为零点偏移之后读位置时减去这个偏移。Flash写入要注意擦除和写入时序别在中断里调用。5.3 多编码器同步读取有些设备需要同时读多个编码器比如双关节机器人。这时候SPI外设不够用可以用GPIO模拟SSI时序每个编码器分配一组GPIO。或者用SPI加多路复用器切换片选。GPIO模拟的代码量不大关键是时序要准。用定时器产生时钟在定时器中断里翻转SCK并采样MISO。时钟频率设成100kHz中断频率200kHzSTM32F103跑72MHz完全能应付。每个编码器用一个状态机轮流读取。我一般会先写一个编码器驱动封装成结构体包含GPIO端口、引脚、帧格式参数。然后实例化多个对象用统一的接口读取。这样增加编码器只需要改配置不用改代码逻辑。5.4 验证方法与调试技巧改造完成后验证分三步第一步用串口打印原始数据和解析结果确认跟测试程序一致。第二步用逻辑分析仪抓SPI波形确认时序没变。第三步跑长时间测试连续读24小时看有没有丢帧或跳变。调试时我习惯在关键位置加计数器比如SPI传输次数、解析成功次数、错误次数。通过串口定期打印这些计数器能快速定位问题。如果错误次数增长很快说明时序或帧格式有问题。如果错误次数为0但数据不对说明解析逻辑有bug。从那以后我每次改SSI驱动都强制走一遍“原始数据打印→波形抓取→长时间跑”的流程再急也不跳过。希望帮到你。本文还有配套的精品资源点击获取