简介:这份资源面向使用STM32 HAL库进行编码器开发的嵌入式工程师与学生,聚焦于通过I2C总线读取AS5600磁编码器角度数据,且采用非DMA的阻塞式读取方式,适合需要理解I2C底层时序、不依赖DMA中断的入门与进阶场景。压缩包共111个文件,以73个.h头文件与26个.c源文件为核心,涵盖STM32F4xx HAL驱动、I2C与定时器外设配置,另含.ioc工程配置、.uvprojx与.uvoptx Keil工程文件、.hex烧录固件及.bat辅助脚本,整体约1016KB,结构完整可直接编译。代码中已封装AS5600_Read_DMA接口与角度、圈数打印逻辑,读者可据此快速验证传感器通信、理解寄存器读取流程,并在此基础上扩展多圈计数或闭环控制。目前已有1071人学习下载,适合作为I2C编码器采集的参考工程。
1. 磁编码器选型与 I2C 轮询方案的适用边界
AS5600 这颗 12 位磁旋转编码器在电机控制、旋钮交互、云台角度反馈里出现频率很高,原因很直接:非接触、体积小、成本低,输出 0~4095 的绝对角度,掉电后位置不丢。但很多人第一次把它接到 STM32 上,用 HAL 库的 I2C 轮询去读,会发现角度跳变、读数偶尔卡死、甚至总线直接锁住。问题往往不在芯片,而在「不加 DMA 的轮询读法」这件事本身的边界没搞清楚。
这篇笔记只讲一件事:用 STM32 HAL 库,纯轮询方式通过 I2C 读取 AS5600 的原始角度和状态寄存器,不引入 DMA、不引入中断回调,把最小可复现的读法、寄存器配置、时序参数和踩坑点讲透。适合两类人:一是刚上手磁编码器、想先把数据读稳的新手;二是已经在用 DMA 或中断、但想搞清楚底层寄存器交互到底发生了什么的老手。轮询方案在低速采样(几十到几百赫兹)下完全够用,代价是占用 CPU,收益是逻辑简单、没有并发问题、调试时不会因为 DMA 搬运和主循环抢资源而出现玄学现象。
2. AS5600 寄存器映射与 HAL I2C 轮询读的底层逻辑
2.1 先搞清楚要读哪几个寄存器
AS5600 的 I2C 从机地址是 0x36(7 位地址,HAL 里要左移一位变成 0x6C)。它内部有一组配置寄存器和一组输出寄存器,轮询读角度只需要碰输出区。核心寄存器如下:
| 寄存器地址 | 名称 | 含义 | 读法 |
|---|---|---|---|
| 0x0C | RAW_ANGLE_H | 原始角度高 8 位 | 连续读 2 字节 |
| 0x0D | RAW_ANGLE_L | 原始角度低 4 位有效 | 同上 |
| 0x0E | ANGLE_H | 经过 ZPOS/MPOS 处理后的角度高 8 位 | 连续读 2 字节 |
| 0x0F | ANGLE_L | 处理后角度低 4 位有效 | 同上 |
| 0x0B | STATUS | 磁铁状态标志 | 单字节读 |
| 0x16 | AGC | 自动增益值 | 单字节读 |
RAW_ANGLE 是传感器直接输出的 0~4095 原始值,ANGLE 是经过零点(ZPOS)和最大角度(MPOS)映射后的值。如果你不做零点标定,两者数值一样。STATUS 寄存器里 MD 位表示检测到磁铁,ML 位表示磁铁太弱,MH 位表示磁铁太强,这三个位是排查「读出来一直是 0 或一直是 4095」的第一手线索。
2.2 HAL_I2C_Mem_Read 的调用方式和参数含义
HAL 库读寄存器最顺手的是HAL_I2C_Mem_Read,它把「写寄存器地址 + 重复起始 + 读数据」这套时序封装好了。最小读角度函数如下:
#include "stm32f1xx_hal.h" // 按实际芯片系列替换头文件 #define AS5600_ADDR (0x36 << 1) // HAL 使用 8 位地址,7 位地址左移一位 #define AS5600_RAW_H 0x0C #define AS5600_STATUS 0x0B I2C_HandleTypeDef hi2c1; // 假设 I2C1 已由 CubeMX 初始化 /* 读取 12 位原始角度,返回 0~4095,失败返回 0xFFFF */ uint16_t AS5600_ReadRawAngle(void) { uint8_t buf[2] = {0}; HAL_StatusTypeDef st; // 从 0x0C 开始连续读 2 字节,AS5600 支持地址自增 st = HAL_I2C_Mem_Read(&hi2c1, AS5600_ADDR, AS5600_RAW_H, I2C_MEMADD_SIZE_8BIT, buf, 2, 10); if (st != HAL_OK) { return 0xFFFF; // 用非法值标记失败,调用方自行处理 } // 高 8 位在 buf[0],低 4 位在 buf[1] 的高半字节 return ((uint16_t)buf[0] << 8) | (buf[1] & 0x0F); } /* 读取 STATUS 寄存器,判断磁铁状态 */ uint8_t AS5600_ReadStatus(void) { uint8_t s = 0; if (HAL_I2C_Mem_Read(&hi2c1, AS5600_ADDR, AS5600_STATUS, I2C_MEMADD_SIZE_8BIT, &s, 1, 10) != HAL_OK) { return 0xFF; } return s; }逻辑说明:HAL_I2C_Mem_Read的第二个参数是 8 位从机地址,第三个参数是寄存器起始地址,第四个参数说明寄存器地址宽度是 8 位,第五个是接收缓冲区,第六个是字节数,第七个是超时毫秒数。AS5600 的寄存器地址会自动递增,所以从 0x0C 读 2 字节就能一次拿到高 8 位和低 4 位,不需要分两次读。低字节只有低 4 位有效,必须用& 0x0F屏蔽掉高 4 位的无效数据,否则角度会出现 16 的倍数跳变。
参数说明:超时值 10ms 是轮询场景下的经验值。I2C 标准模式 100kHz 下读 2 字节大约 0.3ms,快速模式 400kHz 下更短,10ms 足够覆盖绝大多数情况。如果总线上挂了多个从机或者走线较长,可以放宽到 20~50ms,但不要设成HAL_MAX_DELAY,否则总线异常时主循环会被永久阻塞,这是轮询方案最典型的翻车方式。
2.3 轮询周期和 I2C 速率的匹配关系
不加 DMA 意味着每次读角度都要 CPU 全程参与。AS5600 内部采样率典型值是 150Hz 左右(不同滤波配置下会变),也就是说传感器每 6~7ms 才更新一次内部角度值。如果你的主循环 1ms 读一次,会读到大量重复值,白白占用总线;如果 100ms 读一次,又会丢失快速旋转时的角度变化。
常见做法是把读取周期设在 5~20ms,对应 50~200Hz 的采样率,和传感器内部更新率大致匹配。I2C 速率建议用 400kHz 快速模式,CubeMX 里 I2C 的 Clock Speed 直接填 400000。100kHz 也能用,但读一次的时间会拉长到 1ms 量级,在高频轮询时 CPU 占用会明显上升。
提示:轮询周期不要用
HAL_Delay硬等,那会把整个主循环卡死。用HAL_GetTick()做非阻塞时间片判断,主循环里其他任务才能正常跑。
3. 从 CubeMX 配置到读角度的完整落地步骤
3.1 CubeMX 里 I2C 参数怎么填
在 CubeMX 里选中 I2C1,模式选 I2C,Speed Mode 选 Fast Mode,Clock Speed 填 400000。GPIO 会自动分配到 PB6/PB7 或 PB8/PB9,取决于芯片型号。上拉电阻这块要注意:AS5600 模块板上通常已经带了 4.7k~10k 上拉,如果模块自带,CubeMX 里 GPIO 的上拉就不需要再开,否则并联后阻值偏低,上升沿变陡但功耗增加,长走线时反而容易振铃。
时钟配置里确认 I2C 的时钟源频率,F1 系列是 APB1,F4 系列也是 APB1,确保分频后能得到接近 400kHz 的实际速率。CubeMX 会自动算,但如果你手动改了 PCLK1,要回头检查 I2C 的 Timing 寄存器值有没有跟着变。
3.2 初始化顺序和第一个读通的最小验证
生成代码后,先别急着写业务逻辑,用下面这段最小验证确认总线通了:
void AS5600_Test(void) { uint8_t status = AS5600_ReadStatus(); uint16_t angle = AS5600_ReadRawAngle(); if (status == 0xFF) { printf("I2C no response\r\n"); // 总线没通,查接线和上拉 return; } if (!(status & 0x20)) { printf("No magnet detected, status=0x%02X\r\n", status); return; } printf("Angle=%u, status=0x%02X\r\n", angle, status); }逻辑说明:先读 STATUS,如果返回 0xFF 说明HAL_I2C_Mem_Read失败,大概率是从机没应答,检查 SDA/SCL 有没有接反、上拉有没有、地址有没有写错。STATUS 的 bit5 是 MD 位,为 1 表示检测到磁铁。如果 MD 为 0,角度寄存器读出来会是无效值,这时候转磁铁或者调磁铁距离才有意义。
参数说明:磁铁和芯片表面的推荐气隙是 0.5~3mm,太远 MD 不置位,太近 MH 置位、AGC 饱和,角度线性度会变差。轴向充磁的圆磁铁效果最好,直径 6mm 左右、厚度 2~3mm 是常见规格。
3.3 把角度读进主循环并做简单滤波
确认能读到数之后,把读取逻辑放进主循环的时间片里:
uint32_t last_tick = 0; uint16_t angle_buf[4] = {0}; uint8_t buf_idx = 0; while (1) { if (HAL_GetTick() - last_tick >= 10) { // 10ms 周期,100Hz last_tick = HAL_GetTick(); uint16_t a = AS5600_ReadRawAngle(); if (a != 0xFFFF) { angle_buf[buf_idx++ & 0x03] = a; } // 四点滑动平均,抑制单次读数抖动 uint32_t sum = 0; for (int i = 0; i < 4; i++) sum += angle_buf[i]; uint16_t angle_avg = sum >> 2; // angle_avg 就是可用的角度值,0~4095 对应 0~360 度 } // 其他任务继续跑 }逻辑说明:用HAL_GetTick()做 10ms 时间片,读失败时保留上一次的缓冲区数据不更新,避免 0xFFFF 污染平均值。四点滑动平均对磁编码器的白噪声抑制效果明显,代价是引入约 2 个周期的相位延迟,在 100Hz 采样下约 20ms,对大多数角度反馈场景可以接受。
参数说明:滑动平均的窗口长度按噪声水平调。窗口 4 适合噪声 ±3LSB 以内的情况;如果抖动超过 ±10LSB,先查磁铁安装偏心、气隙过大或电源纹波,而不是一味加大窗口,窗口越大动态响应越差。角度转度数用angle * 360.0f / 4096.0f,注意用浮点或者先乘后除避免整数截断。
4. 不加 DMA 时最容易翻车的几个点
4.1 现象:读数偶尔返回 0xFFFF,重试又好了
原因:I2C 总线在电磁干扰下出现仲裁丢失或从机忙,HAL_I2C_Mem_Read返回非 HAL_OK。轮询方案没有中断兜底,一次失败就是一次失败。
解决:在读取函数里加一次重试,重试前先调用HAL_I2C_DeInit和HAL_I2C_Init复位 I2C 外设,或者用HAL_I2C_IsDeviceReady确认从机在线再读。更彻底的做法是在 SDA/SCL 上加 100nF 到地的滤波电容,缩短走线,远离电机驱动线。
4.2 现象:角度在某个位置固定跳变 16 的倍数
原因:低字节的高 4 位无效数据没屏蔽。AS5600 的 ANGLE_L 寄存器只有低 4 位有效,高 4 位是保留位,某些批次会读出随机值。
解决:读回来之后强制& 0x0F,这一点在 2.2 的代码里已经处理。如果还有跳变,检查是不是把 RAW_ANGLE 和 ANGLE 两个寄存器搞混了,两者地址只差 2。
4.3 现象:主循环卡死,看门狗复位
原因:HAL_I2C_Mem_Read的超时设成了HAL_MAX_DELAY,总线被拉低时函数永远不返回。
解决:所有轮询 I2C 调用的超时必须是有限值,10~50ms 之间。同时在主循环里喂看门狗,一旦 I2C 卡住超过看门狗周期,系统能自动复位恢复。
4.4 现象:角度线性度差,两端压缩中间拉伸
原因:磁铁没对中,或者气隙太大导致磁场边缘非线性。AGC 值可以量化这个问题,AGC 正常范围在 0x40~0x80 之间,太小说明磁场太强,太大说明太弱。
解决:读 0x16 寄存器的 AGC 值,调整磁铁位置让 AGC 落在中间范围。同时确认磁铁是轴向充磁、芯片在磁铁正下方,偏心超过 1mm 线性度就会明显恶化。
4.5 现象:多个 AS5600 挂同一条 I2C 读不出来
原因:AS5600 的 I2C 地址固定为 0x36,不可通过引脚改变,同一条总线上挂两个就会地址冲突。
解决:要么用 I2C 多路复用器分路,要么用两条独立 I2C 总线,要么改用 SPI 接口的同类编码器。这是选型阶段就要确认的事,不要等画完板子才发现。
5. 用 STATUS 和 AGC 做在线自检的进阶技巧
轮询方案虽然简单,但可以顺手把 AS5600 的诊断寄存器用起来,做成一个在线自检机制,这在长期运行的设备里比读角度本身更有价值。具体做法是每读 N 次角度,穿插读一次 STATUS 和 AGC,把 MD、ML、MH 三个标志和 AGC 值一起判断,一旦异常就置位一个错误标志,供上层逻辑决定是否降级运行或报警。
typedef struct { uint16_t angle; uint8_t agc; uint8_t magnet_ok; uint8_t error; } AS5600_State; AS5600_State as_state; void AS5600_Update(void) { uint8_t st = AS5600_ReadStatus(); uint8_t agc = 0; if (st == 0xFF) { as_state.error = 1; // I2C 通信失败 return; } as_state.magnet_ok = (st & 0x20) ? 1 : 0; as_state.error = 0; // 磁铁过弱或过强都标记异常 if ((st & 0x08) || (st & 0x10)) { // ML 或 MH 置位 as_state.error = 2; } HAL_I2C_Mem_Read(&hi2c1, AS5600_ADDR, 0x16, I2C_MEMADD_SIZE_8BIT, &agc, 1, 10); as_state.agc = agc; // AGC 超出合理范围也标记异常 if (agc < 0x40 || agc > 0x80) { as_state.error = 3; } uint16_t a = AS5600_ReadRawAngle(); if (a != 0xFFFF) { as_state.angle = a; } }逻辑说明:STATUS 的 bit3 是 ML(磁铁太弱),bit4 是 MH(磁铁太强),bit5 是 MD(检测到磁铁)。三个位组合起来能区分「没磁铁」「磁铁太远」「磁铁太近」三种安装问题。AGC 是自动增益控制值,反映内部放大器的增益档位,磁铁越远增益越高、AGC 值越大。把 AGC 的正常范围卡在 0x40~0x80,超出就说明安装状态在漂移,可能是磁铁松动或者温度导致气隙变化。
参数说明:自检的调用频率不用和角度读取一样高,每 100ms 跑一次足够,因为磁铁状态和 AGC 是慢变量。这样既不影响角度采样率,又能持续监控传感器健康度。错误码分级也有讲究:error=1 是通信层问题,优先查硬件;error=2 是安装问题,查磁铁位置;error=3 是裕量问题,系统还能跑但精度在下降,可以提前预警。
我自己的习惯是,任何用磁编码器的项目,第一版固件一定先把 STATUS 和 AGC 打印出来,盯着这两个值调磁铁位置,调到 AGC 落在 0x50~0x70 之间再开始调角度逻辑。这个习惯帮我省掉了大量「角度不对就怀疑代码」的来回折腾——十次里有七次是磁铁没装好,代码本身没问题。轮询方案的价值就在于它足够透明,每一步读写在示波器上都能对上,出问题时不会在 DMA 和中断的层层封装里迷路。希望帮到你。
本文还有配套的精品资源,点击获取