ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32绝对值编码器驱动实践:SSI协议解析与回零方案

STM32绝对值编码器驱动实践:SSI协议解析与回零方案 简介面向需要在STM32平台上实现绝对值编码器读取与电机定位的嵌入式开发者这是一份完整的C/C工程源码覆盖编码器接口配置、SSI等数字协议解析、中断处理、位置计算与误差校正等关键环节可直接应用到工业自动化与机器人控制场景。压缩包共78个文件以33个.c源码与35个.h头文件为主体另含示例工程、启动文件、编译脚本、hex固件及说明文档整体仅323KB工程内HARDWARE、SYSTEM、APP等目录划分清晰便于按模块翻阅和迁移。已有2087人学习适合正在学习STM32 HAL库或从事工业控制、机器人项目的初中级工程师参考。通过研究代码可快速理解绝对值编码器数据解析思路、定时器捕获与中断服务程序写法同时示例代码还包含初始化配置与位置计算等可直接复用的核心逻辑能显著缩短从零搭建的开发周期。1. 绝对值编码器在 STM32 上能省掉回零但协议得自己写伺服驱动器上电后如果没有位置反馈机械手就得先撞限位再找零产线每次重启都要重复一遍标定流程。换成绝对值编码器位置数据保存在编码器内部断电前后都能直接读回真实角度这是它在转台、机械臂和精密设备上被普遍采用的主要原因。STM32 读这类编码器通常走 SSI 三线协议硬件上任意 GPIO 或 SPI 外设都能接真正的开发量集中在协议时序、字节序和多圈解析这几个地方处理不好代码换个型号就废。下面按接口选型、驱动实现、数据解析、应用实例和调试验证的顺序给出一套可以直接抄进工程 C/C 代码的做法。2. STM32 选接口SSI、SPI、PWM 与绝对式编码器的匹配2.1 三种接口选型SSI 是三根线里性价比最高的绝对值编码器输出类型很多STM32 侧常见的是下面这几种接法接口类型信号线典型分辨率STM32 外设适用场景SSICLK、DAT、CS单圈 13~17 bit多圈 6~12 bitSPI 或 GPIO工业编码器最通用性价比最高SPISCK、MISO、MOSI、CS14~20 bit部分带内部寄存器SPI传感器级编码器适合短距离板载PWM电源 PWM 输出通常 12~16 bitTIM 输入捕获线少但要持续接收脉冲占 CPU模拟量0~10 V 或 4~20 mA10~12 bitADC成本最低线性度和噪声都一般实际项目里我会优先看 SSI。原因很简单三根线加一根供电就能工作不依赖专用解码芯片帧长和位宽都可以在软件里配置。17 bit 单圈加 6 bit 多圈一共 23 bit对绝大多数转台和关节轴都够用。PWM 型布线最少但它的位置信息分散在一整个占空比周期里想拿到稳定的角度要持续占用定时器捕获通道控制周期一紧张就容易丢数据。2.2 SSI 帧怎么排单圈、多圈、校验位和时钟沿SSI 数据帧的结构通常是这样高字节在前MSB 先发一帧里先排单圈数据再排多圈圈数最后是状态位或校验位。时钟空闲时为低电平编码器在时钟下降沿把数据放到 DAT 线上主站建议在上升沿采样。需要注意不同厂商对边沿定义存在差异代码里必须把采样点做成可调整的不能写死。一个典型配置单圈 17 bit、多圈 6 bit、1 bit 偶校验总帧长 24 bit。CS 拉低表示一帧开始CLK 每产生一个脉冲就读出一位位序从最高位排到最低位CS 拉高一帧结束。有些编码器在 CS 有效边沿就会把 MSB 放到 DAT 线上如果代码里上来就先打时钟会把第一位漏掉表现是角度最大值只有理论值的一半或数据整体偏移一位。2.3 先定义ssi_encoder_cfg_t把格式变成可配置参数把帧格式做成配置结构体驱动部分不掺型号相关的常量换编码器时只改结构体内容/* 单圈17bit 多圈6bit 1个偶校验位总长24bit */ typedef struct { uint8_t single_bits; /* 单圈角度数据位宽 */ uint8_t multi_bits; /* 多圈圈数位宽没有则为0 */ uint8_t parity_bits; /* 校验位数量 */ uint8_t total_bits; /* 总帧长 single multi parity */ uint8_t check_parity; /* 1: 偶校验; 0: 不校验 */ } ssi_encoder_cfg_t; static const ssi_encoder_cfg_t ssi_cfg { .single_bits 17, .multi_bits 6, .parity_bits 1, .total_bits 24, .check_parity 1, };这段结构体的核心作用是把协议相关信息收拢到一处。single_bits 决定角度掩码怎么算multi_bits 决定圈数范围total_bits 决定接收缓冲区和模式check_parity 让后续校验代码按需通过。驱动函数全部接收该结构体指针切勿用一堆宏散落在各个文件里。3. 手写 for STM32 的 SSI 驱动GPIO 模拟和 SPI 读取两种代码3.1 GPIO 模拟 SSI 时序最小可读代码与延时设置先用 GPIO 方式把整个时序跑通逻辑最清楚排错也最容易。CubeMX 生成的引脚宏直接拿来做位操作#include main.h /* 对应 CubeMX 生成的引脚宏按实际接线修改 */ #define SSI_CLK_PORT GPIOC #define SSI_CLK_PIN GPIO_PIN_6 #define SSI_DAT_PORT GPIOC #define SSI_DAT_PIN GPIO_PIN_7 #define SSI_NSS_PORT GPIOC #define SSI_NSS_PIN GPIO_PIN_5 static uint32_t ssi_read_gpio(const ssi_encoder_cfg_t *cfg) { uint32_t raw_frame 0; uint32_t bit_mask 1UL (cfg-total_bits - 1); /* CS 拉低一帧开始 */ HAL_GPIO_WritePin(SSI_NSS_PORT, SSI_NSS_PIN, GPIO_PIN_RESET); for (volatile int i 0; i 8; i); /* 等待编码器准备好 */ for (int i 0; i cfg-total_bits; i) { HAL_GPIO_WritePin(SSI_CLK_PORT, SSI_CLK_PIN, GPIO_PIN_SET); for (volatile int j 0; j 4; j); /* 时钟高电平保持 */ if (HAL_GPIO_ReadPin(SSI_DAT_PORT, SSI_DAT_PIN) GPIO_PIN_SET) { raw_frame | bit_mask; } bit_mask 1; HAL_GPIO_WritePin(SSI_CLK_PORT, SSI_CLK_PIN, GPIO_PIN_RESET); for (volatile int j 0; j 4; j); /* 时钟低电平保持 */ } HAL_GPIO_WritePin(SSI_NSS_PORT, SSI_NSS_PIN, GPIO_PIN_SET); return raw_frame; }代码逻辑是先把 CS 拉低启动帧然后逐位用上升沿采 DAT同时用 bit_mask 从最高位往最低位填。延时循环决定时钟频率168MHz 主频下每次循环约几十纳秒整帧 24 bit 读下来几微秒足够转台和关节控制使用。如果编码器离 STM32 较远线缆电容会拖慢信号边沿把两个延时循环同时放大到几十次时钟降到几百 kHz 再观察波形。3.2 用 SPI 外设读 SSIHAL_SPI_Receive 一条路径收完GPIO 模拟适合调试量产后一般改用 SPI 外设把 CPU 从位翻转里解放出来。SSI 的 CLK 接 STM32 的 SCKDAT 接 MISOCS 仍用普通 GPIO 控制static HAL_StatusTypeDef ssi_read_spi(SPI_HandleTypeDef *hspi, const ssi_encoder_cfg_t *cfg, uint8_t *rx_buf, uint8_t rx_len) { HAL_StatusTypeDef st; HAL_GPIO_WritePin(SSI_NSS_PORT, SSI_NSS_PIN, GPIO_PIN_RESET); st HAL_SPI_Receive(hspi, rx_buf, rx_len, 50); HAL_GPIO_WritePin(SSI_NSS_PORT, SSI_NSS_PIN, GPIO_PIN_SET); return st; }HAL_SPI_Receive 在主机模式下发送 dummy 字节并同步接收 MISOrx_len 按 total_bits 向上取整24 bit 传 3。SPI 初始化时设为 MSB 优先、8 bit 数据时钟极性 CPOL0、相位 CPHA0 先用遇到数据整体错一位就切 CPHA1。需要留意的是HAL 库会一直翻转 SCK 直到收满指定字节而标准 SSI 允许时钟中途暂停如果你用的编码器对时钟连续性和最后一个位有特殊要求就回到 GPIO 方式自己控制脉冲数。3.3 字节序和第一次读数不对时的检查顺序第一次调通时先不要急着算角度把 rx_buf 里三个字节的 hex 通过串口打印出来手动缓慢顺时针转动编码器。最高位附近的字节应该按 0x00、0x01、0x02 这样递增如果看到数据整体左移或右移一位基本是采样边沿选错。帧整体多一位total_bits 把校验位也当数据位了减少 1。最低位始终为 0前一位读的是校验位single_bits 配置多算了一位。转速快时数值乱跳CS 高电平后 CLK 仍有残留脉冲检查 SPI 关闭时 CS 和 SCK 的电平状态。DAT 悬空时读到随机噪声MISO 加一个 10kΩ 上拉电阻到 3.3V。提示原始帧通过串口打印时曲线应当是平滑跳变的看到突变先查帧再怀疑控制逻辑。4. 数据解析与 C/C 封装格雷码、多圈和角度偏移4.1 格雷码转二进制的 C 函数与溢出保护很多 SSI 编码器内部使用格雷码出厂参数表里会写明。格雷码相邻数值只有一位变化直接按二进制用会得到错误的角度必须先转换uint32_t gray_to_binary(uint32_t gray, uint8_t bits) { uint32_t bin gray; while (gray 1) { bin ^ gray; } bin (1UL bits) - 1; return bin; }转换原理是格雷码从高位往低位逐位异或quality 出二进制原值。最后的掩码会把高于 bits 的残留位清掉防止多圈数据或校验位混进单圈角度。以 17 bit 单圈为例掩码是 0x1FFFF二进制角度范围 0 到 131071对应 0° 到 359.997°。4.2 把原始帧拆成单圈角度和多圈圈数原始帧里单圈数据占低位多圈数据在它左边解析时用位域偏移做干净利落typedef struct { uint32_t raw_frame; /* 原始24位帧 */ uint16_t single_raw; /* 单圈原始值未做格雷码转换 */ int16_t turns; /* 多圈圈数按有符号解释 */ } ssi_position_t; void ssi_parse_frame(uint32_t raw_frame, const ssi_encoder_cfg_t *cfg, ssi_position_t *pos) { uint32_t single_mask (1UL cfg-single_bits) - 1; pos-raw_frame raw_frame; pos-single_raw (uint16_t)(raw_frame single_mask); pos-turns (int16_t)((raw_frame cfg-single_bits) ((1UL cfg-multi_bits) - 1)); }右移 single_bits 后圈数刚好落在最低的 multi_bits 位里。强转 int16_t 是为了把 6 bit 无符号圈数解释成 -32 到 31 的有符号值便于处理正反转。如果你的编码器手册把高位圈数定义为无符号就不要用 int16_t直接用 uint8_t 接住再乘圈数倍率。4.3 C 封装后的读取接口raw_、angleDeg() 与零点偏置C 代码适合小而快的嵌入式工程C 封装则把位运算和控制逻辑隔离得更干净位置环程序只跟对象打交道class SsiEncoder { public: SsiEncoder(SPI_HandleTypeDef *hspi, const ssi_encoder_cfg_t cfg) : hspi_(hspi), cfg_(cfg), zero_offset_(0.0f) {} bool read() { uint8_t buf[4] {0}; uint8_t len (cfg_.total_bits 7) / 8; if (HAL_SPI_Receive(hspi_, buf, len, 50) ! HAL_OK) { return false; } uint32_t raw 0; for (int i 0; i len; i) { raw (raw 8) | buf[i]; } uint32_t single raw ((1UL cfg_.single_bits) - 1); single gray_to_binary(single, cfg_.single_bits); angle_raw_ 360.0f * (float)single / (float)(1UL cfg_.single_bits); return true; } float angleDeg() const { float a angle_raw_ - zero_offset_; if (a 0.0f) a 360.0f; if (a 360.0f) a - 360.0f; return a; } void setZeroOffset(float offset) { zero_offset_ offset; } private: SPI_HandleTypeDef *hspi_; ssi_encoder_cfg_t cfg_; float zero_offset_; float angle_raw_; };read 函数把字节拼成整帧取出单圈值并转成角度angleDeg 返回减去零点偏置后的真实角度范围 0 到 360。零点偏置的含义是机械零位对应的编码器原始角度首次安装时标定一次之后每次上电读一次原始值就能直接得出真实坐标不需要再次回零。5. 绝对值编码器应用实例关节回零与电机位置环5.1 机械臂/转台回零绝对角度和备份寄存器一起用绝对值编码器虽然上电不丢角度但机械装配时有一个真正的零点位置需要把编码器读数跟机械零点对齐。常见的标定流程是手动把转台转到机械零位附近用百分表打平然后在程序里读取当前角度存为零点偏置void calibrateZero(SsiEncoder enc) { enc.read(); float raw_angle enc.angleRaw(); /* 需要暴露一个读取原始角度的接口 */ uint32_t hex; memcpy(hex, raw_angle, sizeof(hex)); HAL_RTCEx_BKUPWrite(hrtc, RTC_BKP_DR1, hex); }启动时从 RTC 备份寄存器读回 float通过 memcpy 还原设置到 setZeroOffset 里。这样上电后零点偏置直接生效机械臂各关节不需要做归零动作就能知道当前姿态。工业现场仍然会保留断电检修后的零点校验因为多圈编码器超量程后会回卷不能无限制依赖内部计圈值。5.2 电机位置环里怎么用绝对角度值位置环的误差计算可以直接用目标角度减实际角度PID 的微分项建议加低通滤波防止编码器末位抖动被放大typedef struct { float kp, ki, kd; float integral; float prev_error; float filtered_d; float lp_coeff; /* 低通系数0.1 较常用 */ } pid_ctrl_t; float pid_run(pid_ctrl_t *pid, float target, float actual, float dt) { float err target - actual; float d_err; pid-integral err * dt; d_err (err - pid-prev_error) / dt; pid-filtered_d (d_err - pid-filtered_d) * pid-lp_coeff; float out pid-kp * err pid-ki * pid-integral pid-kd * pid-filtered_d; pid-prev_error err; return out; /* 输出给速度环或 PWM */ }位置环输出进入速度环或电流环这里只写了外环部分。积分项必须限幅否则转台长时间被外力阻挡时积分会越积越大故障恢复的一瞬间猛冲。用绝对值编码器做反馈有个好处控制器启动时 prev_error 和 integral 可以直接清零真实位置已经知道不用从上次的估算值继续。5.3 多圈读数的失效处理重试还是信任缓存SSI 编码器在强干扰或瞬间掉电时可能读到坏帧直接把坏值送进位置环会让电机抖动一下。一般的处理方式是连续两次读取结果做合理性判断差值超出预期就丢弃重读static int ssi_validate_frame(uint32_t raw, uint32_t prev, uint32_t change_mask) { uint32_t diff (raw ^ prev) change_mask; /* 角度低位一直在动高位突变才可疑 */ return diff 8; }change_mask 通常是单圈部分的低位若干位因为正常转动时角度变化是连续的高位不会瞬间跳变。连续重读三次仍然失败应当直接停电机并上报错误而不是让 PID 继续用旧值维持输出。更严格的防护是下一章要讲的奇偶校验。6. 验证与调试逻辑分析仪抓帧、奇偶校验与断电零点恢复6.1 逻辑分析仪验证 SSI 帧长和时钟沿把 CLK、DAT、CS 三根线接到逻辑分析仪采样率不低于 25MHz触发条件设为 CS 下降沿。抓到的波形要确认两点一是 CS 高电平时 CLK 必须保持稳定低电平不少库里初始化时钟的代码会在 CS 拉低后才执行导致帧前多出毛刺二是整帧时钟沿数量必须等于 total_bits多一个或少一个都会让解析整体移位。6.2 用偶校验位给每帧数据把关SSI 编码器多采用偶校验即一帧内数据位加校验位总数为偶数。通过折叠异或把 32 bit 压缩成一位判断校验位是否匹配uint32_t parity_count(uint32_t v) { v ^ v 16; v ^ v 8; v ^ v 4; v ^ v 2; v ^ v 1; return v 1U; } uint8_t ssi_parity_check(uint32_t raw, uint8_t total_bits) { uint32_t data_bits raw ((1UL (total_bits - 1)) - 1); uint32_t stored (raw (total_bits - 1)) 1U; return parity_count(data_bits) stored; }校验通过才更新位置缓存失败则重读一帧连续失败 N 次后关闭 PWM 输出通道。这个方法能拦截大部分偶发干扰但没法修正数据位本身损坏因此重读策略仍然保留。6.3 把零点偏置连同校验值一起存入 RTC 备份区备份寄存器在电池供电下能长期保存但存在电池耗尽或被异常写坏的可能。更稳妥的做法是同时存一份浮点值和它的取反读取时交叉验证void zero_store(float offset) { uint32_t word; memcpy(word, offset, sizeof(word)); HAL_RTCEx_BKUPWrite(hrtc, RTC_BKP_DR1, word); HAL_RTCEx_BKUPWrite(hrtc, RTC_BKP_DR2, ~word); } int zero_load(float *offset) { uint32_t w1 HAL_RTCEx_BKUPRead(hrtc, RTC_BKP_DR1); uint32_t w2 HAL_RTCEx_BKUPRead(hrtc, RTC_BKP_DR2); if (w1 0xFFFFFFFFu || (w1 ^ w2) ! 0xFFFFFFFFu) return -1; memcpy(offset, w1, sizeof(w1)); return 0; }写入前先调用 HAL_PWR_EnableBKPDataStorage() 并打开 PWR 时钟否则备份寄存器写入不生效。读取时对 w1 和 w2 做异或判断两者互为取反才认为数据有效返回 -1 时走重新校准分支手动回零一次即可恢复。本文还有配套的精品资源点击获取
返回列表