资讯动态

多圈绝对值编码器STM32测试程序:SSI协议驱动与位置解算实战

发布时间:2026/10/19 9:50:49 来源:尧图企业网站定制
简介这份资源是面向嵌入式开发者的多圈绝对值编码器STM32测试工程聚焦SSI串行同步协议的位置传感通信实践。工程基于STM32微控制器完整实现了GPIO接口配置、时钟分频与数据格式设定、中断与轮询收发、编码器协议解析及位置信息输出等关键环节适合工业自动化、机器人控制与位置检测方向的初中级开发者参考移植。压缩包共163个文件约3.2MB以46个h头文件与46个c源文件为核心辅以工程配置、编译脚本、hex烧录文件及map映射文件等目录结构清晰可直接导入开发环境编译运行。目前已有3106人学习下载。读者可借此掌握SSI通信时序、绝对值编码器圈数与位置解析方法并基于现成驱动框架快速搭建测试环境、排查通信异常省去从零编写底层代码的成本。1. 多圈绝对值编码器STM32测试程序从SSI协议到位置解算的完整落地伺服电机上电那一刻如果驱动器不知道转子当前在机械圈的哪个位置就得先来一段找零动作这在多关节机器人或者云台设备上基本不可接受。多圈绝对值编码器解决的正是这个问题——它不光记录单圈内的角度还能记住转了多少圈断电重启后位置依然在。这份STM32测试程序就是围绕这类编码器做的底层驱动验证工程核心工作是用STM32的GPIO模拟SSI协议时序把编码器返回的帧解析成圈数单圈角度的物理量再通过串口打印出来。适合正在做伺服驱动、机器人关节控制、或者第一次接触SSI接口位置传感器的嵌入式开发者尤其是手头有编码器但不知道怎么把数据读出来的那批人。整个工程不依赖复杂中间件裸机跑通改起来门槛低。2. 工程结构与SSI协议时序数据是怎么从编码器挪到STM32里的2.1 目录组织与模块划分这份测试程序的结构是典型的STM32裸机工程布局没有上RTOS所有逻辑围绕一个主循环展开。常见做法是按功能切成几个独立文件方便单独替换project/ ├── Core/ │ ├── Inc/ │ │ ├── ssi_encoder.h // SSI协议引脚定义、帧结构体、API声明 │ │ ├── main.h │ │ └── stm32f1xx_hal_conf.h │ └── Src/ │ ├── ssi_encoder.c // SSI时序模拟、数据接收、校验、解算 │ ├── main.c // 初始化、主循环、串口输出 │ └── stm32f1xx_it.c ├── Drivers/ │ └── STM32F1xx_HAL_Driver/ └── MDK-ARM/ // Keil工程文件ssi_encoder.c是整个工程的核心它对外只暴露三个函数初始化、读一帧、把原始值转成角度。main.c里不直接碰GPIO所有时序细节都封在驱动层这样换引脚或者换编码器型号时只改一个文件。我一般会把编码器的数据位数、时钟频率、超时阈值都做成宏定义放在头文件里编译期就能调整不用满工程找魔法数字。2.2 SSI协议帧格式与时序要点SSISynchronous Serial Interface是一种主从同步串行协议编码器是 slaveSTM32 是 master。STM32 负责发时钟编码器在时钟的下降沿把数据位推出来STM32 在上升沿采样。一帧数据的典型结构是1 个起始位固定为 0 或 1看编码器手册 N 位数据位 1 个校验位数据位里高位在前通常前几位是圈数后面是单圈角度。以常见的 25 位编码器为例1 位起始 12 位圈数 12 位单圈角度 1 位校验总共 26 个时钟。时钟频率不能太高多数编码器支持 100kHz 到 1MHz第一次调试建议从 100kHz 起步稳定后再往上提。时钟高电平和低电平各占一半用HAL_GPIO_WritePin翻转配合HAL_Delay或者简单的空循环做微秒级延时。注意如果用 HAL 库的HAL_Delay最小分辨率是 1ms根本不够用必须自己写delay_us或者用定时器。2.3 核心代码SSI读帧与位置解算下面这段是ssi_encoder.c里读一帧的核心逻辑用 GPIO 模拟时钟和数据采样// ssi_encoder.h 中的关键宏定义 #define SSI_CLK_PIN GPIO_PIN_5 #define SSI_CLK_PORT GPIOA #define SSI_DAT_PIN GPIO_PIN_6 #define SSI_DAT_PORT GPIOA #define SSI_DATA_BITS 25 // 总数据位数不含起始和校验 #define SSI_TOTAL_BITS (SSI_DATA_BITS 2) // 含起始位和校验位 #define SSI_CLK_DELAY_US 5 // 半周期5us对应100kHz时钟 // ssi_encoder.c 读一帧 uint32_t SSI_ReadFrame(void) { uint32_t raw 0; uint8_t i; // 1. 拉低时钟准备起始 HAL_GPIO_WritePin(SSI_CLK_PORT, SSI_CLK_PIN, GPIO_PIN_RESET); delay_us(SSI_CLK_DELAY_US); // 2. 循环读取每一位 for (i 0; i SSI_TOTAL_BITS; i) { // 上升沿编码器输出数据已稳定STM32采样 HAL_GPIO_WritePin(SSI_CLK_PORT, SSI_CLK_PIN, GPIO_PIN_SET); delay_us(SSI_CLK_DELAY_US); // 跳过起始位和校验位只取中间的数据位 if (i 0 i SSI_DATA_BITS) { raw 1; if (HAL_GPIO_ReadPin(SSI_DAT_PORT, SSI_DAT_PIN) GPIO_PIN_SET) { raw | 0x01; } } // 下降沿编码器准备下一位 HAL_GPIO_WritePin(SSI_CLK_PORT, SSI_CLK_PIN, GPIO_PIN_RESET); delay_us(SSI_CLK_DELAY_US); } // 3. 帧结束后保持时钟高电平一段时间编码器需要复位时间 HAL_GPIO_WritePin(SSI_CLK_PORT, SSI_CLK_PIN, GPIO_PIN_SET); delay_us(20); // 典型值10~30us看手册 return raw; }逻辑说明函数返回的是去掉起始位和校验位之后的纯数据位25 位。raw变量在循环里左移再或上当前位最终得到的就是编码器输出的原始二进制值。参数方面SSI_CLK_DELAY_US决定了时钟频率5us 半周期对应 100kHz如果编码器手册标称支持 500kHz可以改成 1us但要注意 STM32 的 GPIO 翻转速度和delay_us的精度。SSI_TOTAL_BITS必须和编码器实际帧长一致多读一位少读一位都会导致数据错位。拿到raw之后需要按编码器的数据格式拆成圈数和单圈角度// 假设高12位是圈数低13位是单圈角度25位数据 void SSI_Decode(uint32_t raw, int32_t *turns, float *angle_deg) { uint32_t turns_raw (raw 13) 0x0FFF; // 取高12位 uint32_t angle_raw raw 0x1FFF; // 取低13位 // 单圈角度13位对应0~8191映射到0~360度 *angle_deg (float)angle_raw * 360.0f / 8192.0f; // 圈数多圈编码器通常用补码表示正反转 // 如果最高位是1说明是负圈数反转 if (turns_raw 0x0800) { *turns (int32_t)turns_raw - 4096; // 12位补码转有符号 } else { *turns (int32_t)turns_raw; } }这里有两个容易翻车的点。第一圈数的位宽和单圈角度的位宽必须严格按编码器手册来不同厂家可能把圈数放低位、角度放高位拆错了数据看起来有变化但物理意义完全不对。第二多圈计数的补码处理很多编码器在反转时圈数会从 0 跳到最大值再递减如果不做补码转换你会看到圈数突然从 0 变成 4095以为是编码器坏了。angle_deg的分母 8192 是 2 的 13 次方如果单圈是 12 位就改成 4096这个必须和实际位数对齐。2.4 主循环与串口输出main.c里的逻辑很直接初始化系统和 SSI 引脚然后在一个 while 循环里周期性读帧、解算、打印。采样周期建议 1ms 到 10ms太快了串口打印会堵太慢了看不到动态变化。int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); SSI_Encoder_Init(); // 配置CLK和DAT引脚为推挽输出/浮空输入 int32_t turns 0; float angle 0.0f; uint32_t raw; while (1) { raw SSI_ReadFrame(); SSI_Decode(raw, turns, angle); printf(raw0x%07X turns%ld angle%.2f\r\n, (unsigned int)raw, (long)turns, angle); HAL_Delay(5); // 5ms采样周期 } }SSI_Encoder_Init()里要把 CLK 配成推挽输出、DAT 配成输入上拉或浮空看编码器输出类型推挽输出就浮空开漏就上拉。串口波特率 115200 足够printf 重定向到 USART1 的huart1就行。如果打印出来全是 0 或者全是 1先查 DAT 引脚有没有接反、编码器供电是否正常、时钟极性对不对。3. 编译烧录与上电验证让数据第一次正确跳出来3.1 开发环境与工程配置这份工程默认是 Keil MDK 的.uvprojx工程芯片型号按 STM32F103C8T6 配置Flash 64KB、RAM 20KB对于这个测试程序绰绰有余。如果你用的是 STM32CubeIDE 或者 IAR把Core/和Drivers/两个目录拖进新工程再把启动文件和链接脚本换成对应芯片的即可。HAL 库版本建议用 F1 系列的 1.8.x太老的版本HAL_GPIO_ReadPin的返回值类型可能不一样。时钟配置里系统主频设成 72MHzAPB2 给 GPIO 和 USART1 供时钟。delay_us的实现可以用 SysTick 或者一个简单的for循环但for循环的延时随编译器优化等级变化建议用 SysTick 做微秒延时精度稳定。我一般会在ssi_encoder.c里放一个delay_us的弱定义方便换平台时重写。3.2 接线与上电检查清单接线就四根编码器电源通常 5V 或 24V看型号、GND、CLK、DAT。CLK 接 STM32 的 PA5DAT 接 PA6这两个引脚在代码里是宏定义的改起来方便。上电前用万用表量一下编码器供电是否正常DAT 线在没有时钟时应该是高电平空闲态如果一直是低电平可能是编码器没供电或者线接反了。上电后先别急着看串口用示波器或者逻辑分析仪抓一下 CLK 和 DAT 的波形。正常的 SSI 时序是CLK 空闲高电平一帧开始时拉低然后连续翻转 26 次对应 26 位DAT 在 CLK 下降沿变化、上升沿稳定。如果 DAT 一直不动检查编码器是不是需要先发一个唤醒时钟有些型号上电后第一帧是无效的要连读两帧取第二帧。3.3 串口数据判读与常见异常串口打印正常的话你会看到turns和angle随着编码器轴转动而变化。用手慢慢转一圈angle应该从 0 平滑走到 360 再回 0turns在跨圈时加 1 或减 1。如果angle跳变不连续比如从 180 直接跳到 200说明数据位对齐有问题检查SSI_DATA_BITS和拆位时的移位量。如果turns一直是 0但angle在变可能是圈数位宽设错了或者编码器本身就是单圈型号。如果raw值每次读都在剧烈跳动先降低时钟频率到 50kHz 试试有些编码器对时钟占空比敏感。还有一点printf在裸机里是阻塞的如果采样周期设成 1ms 而串口打印要 2ms主循环会被拖慢看起来像是编码器响应变慢实际是打印堵住了。4. 避坑与排查SSI调试中最容易翻车的五个地方4.1 现象数据全是0或全是1怎么转都不变原因通常是 DAT 引脚配置错了。如果编码器是推挽输出STM32 的 DAT 引脚配成上拉输入两者会打架读回来永远是高电平。如果编码器是开漏输出STM32 配成浮空输入又没有外部上拉读回来永远是低电平。解决方法是先确认编码器输出类型推挽就配浮空输入开漏就配上拉输入或者干脆在 DAT 上加一个 4.7k 上拉到 3.3V。4.2 现象角度值偶尔跳变圈数乱加这是典型的时序余量不足。SSI_CLK_DELAY_US设得太小比如 1usSTM32 的 GPIO 翻转还没稳定编码器就已经采样了或者编码器数据还没建立 STM32 就采了。把延时加大到 5us 甚至 10us先保证稳定再逐步压缩。另外如果主循环里还有其他中断比如 SysTick 或者串口中断可能在 SSI 读帧过程中打断时序导致某一位读错。读帧函数里最好关中断读完再开。4.3 现象上电第一次读数是错的后面正常很多多圈绝对值编码器上电后需要一段初始化时间内部 DSP 要完成自检和圈数恢复这期间输出的数据无效。常见做法是上电后延时 100ms 到 500ms 再开始读或者连续读三帧丢弃前两帧取第三帧。我在项目里一般会在SSI_Encoder_Init()末尾加一个HAL_Delay(200)省得后面排查半天。4.4 现象圈数在正反转时从0跳到最大值这是补码没处理。多圈编码器的圈数寄存器是有符号的正转加到最大值后溢出到负的最小值反转减到最小值后溢出到正的最大值。如果不做补码转换你会看到 0 突然变成 4095。解决方法是按数据位宽做补码判断12 位圈数就看 bit11是 1 就减 4096。注意不同厂家的圈数位宽可能不是 12 位8 位、10 位、14 位都有补码的阈值要跟着改。4.5 现象串口打印正常但电机一动就丢数据这是电磁干扰。编码器线缆和电机动力线捆在一起走SSI 的时钟和数据线被干扰读回来的帧校验失败或者数据错位。解决方法是编码器线用屏蔽双绞线屏蔽层单端接地走线尽量远离动力线。软件上可以加帧校验SSI 的校验位通常是奇偶校验或者 CRC校验不过就丢弃当前帧用上一帧的有效值。如果干扰严重降低时钟频率也能提高抗干扰能力。5. 二次开发改造从测试程序到产品级驱动的几个关键动作把这份测试程序改成能上产品的驱动核心是加三样东西校验、缓存、异常恢复。校验方面SSI 帧的最后一位通常是奇偶校验读帧时把校验位也取出来和数据位一起算奇偶性不匹配就丢弃。缓存方面维护一个最近有效值的结构体校验失败时返回上一次的有效值而不是 0避免上层看到位置突然归零。异常恢复方面连续 N 帧校验失败就重新初始化编码器拉低时钟一段时间再释放模拟一次上电复位。具体改造时我会把SSI_ReadFrame拆成两层底层SSI_ReadRaw只负责时序和取位上层SSI_GetPosition负责校验、缓存和异常计数。这样底层可以复用到不同型号的编码器上层逻辑统一。下面是一个改造后的接口示例typedef struct { int32_t turns; float angle_deg; uint32_t last_raw; uint8_t error_cnt; uint8_t valid; } SSI_PosHandle; // 返回0表示成功-1表示校验失败但用了缓存值-2表示连续失败 int SSI_GetPosition(SSI_PosHandle *h) { uint32_t raw SSI_ReadRaw(); uint8_t parity SSI_CalcParity(raw); if (parity ! SSI_ReadParityBit()) { h-error_cnt; if (h-error_cnt 5) { SSI_Encoder_Init(); // 重新初始化 h-error_cnt 0; return -2; } return -1; // 用缓存值 } h-error_cnt 0; h-last_raw raw; SSI_Decode(raw, h-turns, h-angle_deg); h-valid 1; return 0; }参数说明error_cnt是连续错误计数超过阈值触发重新初始化valid标志位给上层判断数据是否可信last_raw保留原始值方便调试。这个结构体可以放在全局或者由上层传入看你的架构习惯。另一个改造点是多圈圈数的掉电保存。有些编码器本身带电池或者非易失存储圈数断电不丢有些则需要 STM32 在断电前把圈数写到 Flash 或者 EEPROM 里上电后再恢复。如果编码器不带掉电保存你需要在系统里加一个掉电检测中断在电压跌落到阈值前把turns写进 Flash。这个动作要快通常只有几毫秒窗口所以 Flash 写操作要提前擦好页中断里只做写数据。最后说一个我踩过的坑编码器的圈数位宽和单圈角度位宽加起来不一定等于总数据位中间可能有保留位或者状态位。有一次我按 1213 拆结果角度总是差一点后来翻手册发现 bit12 是多圈溢出标志不参与角度计算实际角度是 11 位。从那以后我每次拿到新编码器都先用逻辑分析仪抓一帧完整数据手动标出每一位的含义再写拆位代码。这个习惯帮我省了很多返工时间。希望这份测试程序能帮你把 SSI 编码器顺利跑起来少走点弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑