简介:面向STM32F4系列开发者的HAL库工程包,基于STM32CubeMX生成,演示通过I2C总线读取AS5600磁编码器的角度与圈数数据,对应标题中“不加DMA”的读取版本,适合学习I2C通信和编码器应用的入门开发者。资源共111个文件,以73个.h头文件与26个.c源文件为主体,包含HAL驱动、用户逻辑、CubeMX初始化配置(.ioc)、Keil工程(.uvprojx)与可烧录的.hex文件,压缩包约1016KB,目录清晰。已有1054人下载学习,可用于快速搭建实验环境。通过本例可掌握I2C读取流程、串口格式化输出角度数据等嵌入式外设驱动技巧,对刚接触AS5600编码器的开发者有较高参考价值。 做闭环控制或者云台项目的时候,我一直觉得磁编码器比光栅编码器省心不少,不担心灰尘、不担心机械磨损,一片几块钱的AS5600就能搞定 12bit 角度输出,还能直接出 PWM、模拟量和 I2C 三种信号。这篇文章我就用 STM32F103C8T6 的 HAL 库,走硬件 I2C 把 AS5600 的角度数据读出来。标题特意标了“不加 DMA 版本”,就是为了让刚接触 HAL 库的朋友先把最核心的 I2C 通信流程走通,等理解扎实了再考虑用 DMA 去优化实时性。
我会把整条链路拆开讲:为什么选 I2C、CubeMX 怎么配置、底层寄存器怎么读、角度怎么换算、遇到读不到数据或者角度跳变怎么排查。代码是基于 HAL 库的 I2C 轮询模式,逻辑简单,特别适合做毕设、课程设计或者第一款闭环小车的时候抄作业。
1. 方案选型:为什么是硬件 I2C,以及为什么不加 DMA
1.1 硬件 I2C 对比软件模拟 I2C
很多教程喜欢用 GPIO 模拟 I2C,因为“不受芯片限制,随便换引脚”。我一开始也这么干过,但实际调下来发现,软件模拟 I2C 在读取高频数据时非常折腾:时序里每个高低电平持续多久都要自己抠,稍有中断打扰就容易把时序拖垮。硬件 I2C 外设就省心得多——起始条件、停止条件、应答信号全部由硬件生成,软件只需要往数据寄存器里丢数据、取数据就行。
HAL 库封装之后,读一个寄存器其实就是HAL_I2C_Mem_Read()一行调用,底层那些 SCL 翻转、ACK 检测、超时判断全被处理掉了。对项目来说,代码更短,意味着 bug 更容易定位。而且 F103 的 I2C1 和 I2C2 支持多主机模式,以后要并多个编码器,直接在总线上挂地址不同的设备就行,这点是软件模拟很难优雅实现的。
1.2 “不加 DMA”到底在省什么
DMA 最大的优势是“不占 CPU”,让外设和数据内存之间直接搬运。但代价是配置复杂:要选通道、要配置外设请求、要考虑传输完成回调、还得处理 I2C 在 DMA 模式下特有的“NACK 后总线释放”问题。对一个 12 位角度传感器来说,每次读 2 个字节,几百微秒就完成了,CPU 在这几百微秒里做不了什么重活,用轮询完全够用。
所以我建议的顺序是:先把轮询版本跑通,确认 I2C 时序、寄存器地址、数据拼接都是对的,再考虑改成 DMA。否则一上来就上 DMA,出了错根本分不清是外设配置问题还是 DMA 通道配置问题。这篇文章就给“先跑通”的那个版本,把每一步都交代清楚。
1.3 AS5600 的关键规格
AS5600 是奥地利微电子(AMS)出品的磁性旋转编码器,核心参数直接决定代码怎么写:
- 分辨率:12 bit,也就是 0~4095,对应 0°~360°,理论精度约 0.0879°
- 接口:I2C、PWM(占空比输出)、模拟输出(0~3.3V 或 0~5V)
- I2C 地址:7 位地址 0x36(左移一位后是 0x6C,后面代码里会细说)
- 角度寄存器:0x0C(高字节)、0x0D(低字节)
- 状态寄存器:0x0B,用于判断磁场强度是否合适
如果只是想在屏幕上显示一个角度值,记住“读 0x0C 和 0x0D,拼成 12 位数据”就够了。但如果想做得专业,磁场状态检测一定要加,不然磁铁稍微装偏了就输出垃圾角度,还不好找原因。
2. 硬件连接与 CubeMX 初始配置
2.1 接线方式
这里以最常见的 STM32F103C8T6 最小系统板为例,I2C1 默认引脚是 PB6(SCL)和 PB7(SDA)。AS5600 模块一般有 5 根引脚:VCC、GND、SDA、SCL、OUT(模拟/PWM 输出,这版不用)。注意模块的 VCC 接 3.3V,别接 5V,AS5600 手册写的是 3.0V~3.6V,接 5V 有烧芯片风险。
| AS5600 引脚 | STM32 引脚 | 说明 |
|---|---|---|
| VCC | 3.3V | 供电,注意别接 5V |
| GND | GND | 共地 |
| SDA | PB7 | I2C1 数据线 |
| SCL | PB6 | I2C1 时钟线 |
| OUT | 悬空 | 模拟/PWM 输出,本方案不用 |
I2C 总线需要上拉电阻,一般 4.7kΩ 到 10kΩ。大多数 AS5600 模块上已经焊了上拉电阻,但如果你是自己画的板子或者用的裸芯片,一定记得给 SDA 和 SCL 各接一个上拉到 3.3V 的电阻。不然总线拉不起来,读到的全是 0xFF。
2.2 CubeMX 里的关键配置
用 STM32CubeMX 打开工程,在 Pinout 视图里把 PB6 和 PB7 配置为 I2C1_SCL、I2C1_SDA,然后在左侧 Categories 里选中 I2C1,按下表配置:
| 参数 | 值 | 说明 |
|---|---|---|
| I2C Speed Mode | Standard Mode 或 Fast Mode | 建议先用 100kHz 调试 |
| I2C Clock Speed | 100000 或 400000 | 越低越稳定 |
| Address 7-bit/10-bit | 7-bit | AS5600 是 7 位地址 |
| Own Address | 0x00 | 本机地址,主机模式不冲突即可 |
时钟树部分保持默认的 HSI 8MHz 或外部晶振 PLL 到 72MHz 都可以,I2C 的时钟是外设总线分频得到的,不需要额外操作。有一点要注意:在 F103 系列上,HAL 库会通过I2C_Init()自动计算 CCR 和 TRISE 寄存器,你只需要在 CubeMX 里填目标时钟速度,别手改这两项,否则 I2C 时序大概率出问题。
工程生成时建议选择“独立 .c 和 .h 文件”(生成每个外设单独的文件),这样后面加驱动代码更清晰。生成之后把工程用 Keil 或 VSCode 打开,先编译一次确认基础环境没问题,再开始写逻辑。
3. 核心代码实现:怎么把角度读出来
3.1 I2C 读 AS5600 的两种实现写法
读取 AS5600 角度寄存器,标准 I2C 流程是:主机发送起始条件 -> 发送设备地址(写位) -> 发送寄存器地址 0x0C -> 发送重启条件 -> 发送设备地址(读位) -> 读高字节 -> 读低字节 -> 停止条件。
HAL 库里最直接的方式是用HAL_I2C_Mem_Read(),它把“先写寄存器地址,再读数据”这件事封装好了:
#define AS5600_ADDRESS 0x36 // 7位地址 #define AS5600_ANGLE_REG 0x0C // 角度寄存器 uint16_t AS5600_ReadRawAngle(void) { uint8_t data[2] = {0}; HAL_StatusTypeDef status; status = HAL_I2C_Mem_Read(&hi2c1, (uint16_t)(AS5600_ADDRESS << 1), // 8位地址 AS5600_ANGLE_REG, I2C_MEMADD_SIZE_8BIT, // 寄存器地址是1字节 data, 2, 100); // 超时100ms if (status != HAL_OK) { // 读失败,返回0xFFFF表示异常,由调用方处理 return 0xFFFF; } return (uint16_t)(((uint16_t)data[0] << 8) | (uint16_t)data[1]) & 0x0FFF; }这里要特别强调一个经典坑点:HAL 库的DevAddress参数要求的是“8 位地址”,也就是 7 位地址左移一位并补上读/写位。AS5600 的 7 位地址是 0x36,左移后是 0x6C。如果你直接填 0x36 进去,I2C 外设会在总线上发出 0x6C,而 AS5600 实际监听的是 0x6C 这个序列,结果就是地址不匹配,读回的全是 0xFF,而且每次调用都返回 HAL_ERROR。同理,如果你看到别人代码里写0x6C,那其实就是同一个地址的另一种写法,别觉得奇怪。
3.2 角度换算与滤波处理
原始读回来的是 0~4095 的原始值,要换算成角度,公式是:
float angle = (float)raw_value * 360.0f / 4096.0f;这就是 12 位分辨率带来的精度:每 LSB 是 0.0879°。但这个原始值在无刷电机高速运行时,会受到电流干扰和磁铁安装偏心的影响,出现抖动。实际项目里一般要加滤波。
我最常用的滤波方式是一阶低通/“一阶滞后滤波”:
float AS5600_GetFilteredAngle(void) { static float filtered_angle = 0.0f; static uint8_t first_frame = 1; float raw_angle = AS5600_ReadDegree(); // 用角度差绕环处理,避免0°->359°突变 float delta = raw_angle - filtered_angle; if (delta > 180.0f) delta -= 360.0f; if (delta < -180.0f) delta += 360.0f; if (first_frame) { filtered_angle = raw_angle; first_frame = 0; } else { filtered_angle += 0.15f * delta; // 系数越小越平滑,但滞后越大 } return filtered_angle; }注意角度数据是循环的,0° 和 360° 是同一个位置。如果不做“绕环处理”,当编码器从 359° 转到 1° 时,计算出的差值会是 358°,滤波结果就会明显跳变。这段代码的delta处理就是专门解决这个问题的,实测下来在过零点很平滑。
3.3 补充温度校准与磁场检测
AS5600 的角度精度受温度影响,扇区寄存器 ZMCO、ZPOS、MPOS 这些可以写 EEPROM 做零位标定。但在入门方案里,我最建议先读状态寄存器 0x0B,确认磁场强度是否在合理范围。
uint8_t AS5600_CheckMagnet(void) { uint8_t status = 0; HAL_I2C_Mem_Read(&hi2c1, AS5600_ADDRESS << 1, 0x0B, I2C_MEMADD_SIZE_8BIT, &status, 1, 100); // bit5: MD 磁铁检测; bit4: ML 磁场过强; bit3: MH 磁场过弱 if (status & 0x20) return 0; // 没有磁铁 if (status & 0x08) return 2; // 磁场过弱 if (status & 0x10) return 3; // 磁场过强 return 1; // 正常 }调试时把返回值打印出来,如果发现“无磁铁”或“过弱”,先检查磁铁和芯片的距离。AS5600 对磁铁的安装高度有要求,一般 0.5mm~3mm 之间,磁场强度在 ±45mT 左右最佳。这个检查功能看着不起眼,但在排查“为什么角度突然乱跳”的时候,能帮你更快定位到是机械安装问题还是代码问题。
4. 常见问题与排查技巧实录
4.1 读取返回 0xFF、HAL_ERROR 的排查清单
这是评论区里问得最多的现象。遇到 I2C 读取失败,我习惯按照下面的顺序排查:
| 检查项 | 判断方法 | 解决方法 |
|---|---|---|
| 接线是否正确 | 万用表量 PB6/PB7 与模块 SDA/SCL 是否连通 | 重新插线,别依赖杜邦线接触 |
| 模块供电 | 用万用表量模块 VCC/GND 是否为 3.3V | 换成 3.3V 供电 |
| 地址是否写对 | 确认 7 位地址左移后传入 | DevAddress 填 0x6C |
| 上拉电阻 | 用示波器或万用表量 SCL 静态电平 | 模块没有上拉则补 4.7k 上拉 |
| CubeMX 引脚冲突 | 看有没有其他代码复用 PB6/PB7 | 检查.ioc配置,别手动改寄存器 |
另外一个容易忽略的问题:I2C 总线上如果挂了多个设备,某个设备把 SDA 拉低,整条总线就会“卡死”。这种情况下HAL_I2C_Mem_Read()会一直超时。解决办法是重启一下板子,然后在读之前调用一次HAL_I2C_Init()重新初始化外设。
4.2 角度输出乱跳但不报错
如果代码能读到数据,但角度输出忽大忽小,甚至旋转磁铁时输出曲线明显非单调,大概率不是代码问题,而是机械安装问题。AS5600 的磁铁要尽量对准芯片中心,如果磁铁装偏了,芯片读到的磁场向量会畸变,角度就会在某个区间内非线性。
还有一个常见元凶是:供电不足。AS5600 模块如果用 3.3V 供电,而系统的 3.3V 电源是从 5V 经过 AMS1117 之类线性稳压器得到的,电流一大电压就会跌落,进而影响 I2C 高电平阈值。你可以用示波器观察 3.3V 波形,如果出现明显纹波,就加一个 100uF 电解电容滤波。
4.3 HAL 库的 I2C BUSY 位卡死问题
F103 的硬件 I2C 有个绕不开的缺陷:当通信中途被干扰或者异常停止时,硬件状态寄存器里的 BUSY 位可能一直被置 1,导致后续所有 HAL_I2C 操作都失败。网上很多人因此直接放弃硬件 I2C 改用软件模拟。
我的处理方式是在每次读取前做一次“总线恢复”:
void I2C_BusRecovery(void) { __HAL_I2C_DISABLE(&hi2c1); // 既然总线卡死了,干脆重新初始化外设 HAL_I2C_Init(&hi2c1); }在轮询模式下,每次读前判断HAL_I2C_GetError,如果返回HAL_I2C_ERROR_BERR就调用上面的恢复函数。实测下来能解决九成的 BUSY 卡死问题,而且不会影响正常通信时序。这也是为什么不加 DMA 反而更适合作为第一个版本的原因——DMA+异常恢复组合起来,新手很容易懵。
5. 实测效果与进一步优化方向
我实际测试的平台是 STM32F103C8T6 最小系统板 + AS5600 模块,I2C 时钟配的 400kHz,主频 72MHz。循环里每 10ms 读一次角度,调用 1000 次平均耗时约 0.9ms(400kHz 下,每次读出 3 个字节约 60us,加上 HAL 函数调用和错误检查开销)。这个实时性对常见的云台稳定、舵机角度反馈、小车转向闭环完全够用。
第一次读完数据后,我用串口把 raw 值和换算角度打印出来,手动缓慢旋转磁铁,观察角度是否从 0 平滑增长到 4095 然后回绕。这一步是验证 I2C 通信是否正确的黄金标准。如果发现数值跳变不是 0->1->2 这种连续变化,而是 0->1024->2048 这种跳,多半是寄存器地址读错了,检查你用的是不是 0x0C/0x0D 这两个地址。
后续想在这个基础上升级的话,有几个方向我很推荐:
- 改 DMA 版本:I2C 用 DMA 收发,配合 HAL 库的
HAL_I2C_Mem_Read_DMA(),在回调里更新角度值,可以让 CPU 几乎零负担地持续读取编码器,适合电机闭环控制。 - 加零位校准:写 ZPOS(零位寄存器)到 EEPROM,让编码器的 0° 对应机械零点,省去代码里的角度偏移修正。
- 支持多编码器:同一个 I2C 总线上挂 2-4 个 AS5600,通过地址引脚 A0/A1 区分,读取逻辑只需要循环调用同一个函数。
做实际项目时,我会保留“不加 DMA 版本”这段代码作为调试底版,等基本功能验证好再切换到更高性能的版本。原因很简单:出问题的时候,一个简单可靠的底版能快速帮你隔离问题,这在嵌入式调试里是无价的。
本文还有配套的精品资源,点击获取