☰
STM32驱动多圈绝对值编码器:SSI协议解析与位置解算实战
2026/10/19 14:01:51 网站建设 项目流程

简介:本资源为多圈绝对值编码器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的CPOL=0、CPHA=1或者CPOL=1、CPHA=0,具体要看编码器手册。

如果不想用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位数据,CPOL=1,CPHA=1,预分频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。这里用angle+1是简化写法,实际应该用常量表示。

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组合,四种组合都试一遍。常见做法是CPOL=1、CPHA=1或者CPOL=0、CPHA=1。

4.2 数据偶尔跳变或出现固定偏移

现象:大部分数据正常,但偶尔跳出一个异常值,或者所有数据都偏了一个固定值。

原因:帧间隔太短,编码器还没准备好下一帧数据就被读了。或者起始位和停止位没对齐,导致数据整体移位。

解决:增加帧间隔延时,从100微秒开始试,逐步减小到稳定为止。检查原始数据的最高位和最低位是否符合手册的起始位和停止位定义。如果整体偏移,可能是SPI读的字节数不对,比如应该读3个字节却读了4个,多出来的位把数据挤偏了。

4.3 圈数回绕时位置突变

现象:编码器转过零点时,合成位置突然跳变一个很大的值。

原因:圈数溢出处理逻辑有误,delta计算时没有正确判断回绕方向。或者max_turns参数设错了,跟实际圈数位数不匹配。

解决:确认圈数位数,12位圈数max_turns是4096,13位是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,中断频率200kHz,STM32F103跑72MHz完全能应付。每个编码器用一个状态机,轮流读取。

我一般会先写一个编码器驱动,封装成结构体,包含GPIO端口、引脚、帧格式参数。然后实例化多个对象,用统一的接口读取。这样增加编码器只需要改配置,不用改代码逻辑。

5.4 验证方法与调试技巧

改造完成后,验证分三步:第一步用串口打印原始数据和解析结果,确认跟测试程序一致。第二步用逻辑分析仪抓SPI波形,确认时序没变。第三步跑长时间测试,连续读24小时,看有没有丢帧或跳变。

调试时我习惯在关键位置加计数器,比如SPI传输次数、解析成功次数、错误次数。通过串口定期打印这些计数器,能快速定位问题。如果错误次数增长很快,说明时序或帧格式有问题。如果错误次数为0但数据不对,说明解析逻辑有bug。

从那以后我每次改SSI驱动,都强制走一遍“原始数据打印→波形抓取→长时间跑”的流程,再急也不跳过。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询