简介:这是面向STM32F103RCT6的AS5600角度编码器硬件I2C驱动源码,适合正在学习I2C通信或需要读取角度数据的单片机开发者。程序基于标准外设库实现,包含I2C1初始化、设备地址宏定义、角度寄存器读写函数封装,并配合定时器1ms调度、串口打印和LED闪烁指示,可直观观察模块工作状态。压缩包共197个文件,以C语言源文件、头文件和Keil工程文件为主,同时附带编译生成的.o/.axf/.hex等输出文件,整体约5.38MB,目录内容完整,便于参考或直接移植。已有506人学习,适合快速验证AS5600基本功能,也可作为STM32硬件I2C时序与多文件工程组织的示例。对于入门者,这套源码能省去查手册和调试时序的重复工作,直接关注角度数据应用。
1. 用 STM32F103RCT6 硬件 I2C 读 AS5600,先从 0x36 地址和 12 位数据说起
AS5600 角度编码器输出 12 位角度数据,单圈分辨率约 0.088°,通过 I2C 或 PWM 都能读回磁铁旋转位置;STM32F103RCT6 自带两个硬件 I2C 外设,完全可以直接把 AS5600 的寄存器内容读出来,不必像很多工程那样改成 GPIO 模拟 I2C。实际项目里把硬件 I2C 写废掉的情况,多半是 F1 的事件标志清理顺序不对,或者在读多字节时 NACK/STOP 的时机没卡准,问题并不在外设本身。这篇按照 AS5600 的寄存器映射给出一套能直接改用的驱动程序源码,同时把从机地址、上拉电阻、总线复位这些交叉点讲透,适合云台、关节电机、旋钮输入这类需要先把角度读准再谈闭环控制的项目。
2. AS5600 寄存器映射与 I2C 多字节读取时序:0x36 从机地址和 12 位角度
2.1 默认 I2C 地址 0x36 和角度寄存器选型
AS5600 的 7 位从机地址出厂固定为 0x36,I2C 总线上实际发送的字节要把地址左移一位:写方向是0x36 << 1 | 0,也就是 0x6C;读方向是0x36 << 1 | 1,也就是 0x6D。调试 ACK 失败时,最常见的原因是直接把 0x36 塞进数据寄存器,少做了一次左移,从机自然不应答。
角度相关的寄存器主要有三个,表格里列出的是最常用的一组:
| 寄存器地址 | 名称 | 位宽 | 作用 |
|---|---|---|---|
| 0x0B | STATUS | 8 | 磁场强度指示,MH/ML/MD 三个状态位 |
| 0x0C~0x0D | RAWANGLE | 12 | AGC 补偿后的原始角度 |
| 0x0E~0x0F | ANGLE | 12 | 经过 ZPOS/MPOS 映射后的角度,通常直接用于控制 |
| 0x07~0x08 | CONF | 14 | 输出模式、方向、零位使能等配置 |
读取时寄存器地址放在第一个数据字节之后,AS5600 内部地址指针会自动递增,所以从 0x0C 开始连续读 4 个字节能得到 RAWANGLE 和 ANGLE 两个值。12 位数据存在两个字节里,读回来的 16 位数值要& 0x0FFF才能去掉高位无效位。
RAWANGLE 和 ANGLE 的区别容易被忽略:0x0C 是不做映射的原始位置,0x0E 会先应用 ZPOS 零点位置和 MPOS 最大位置,再输出映射后的角度。不写这两个寄存器时二者数值一致,但闭环控制里建议固定读 0x0E,这样换向和限位逻辑都建立在一个稳定语义上。
2.2 一次读多个字节的完整时序:START、寄存器地址、重复开始
用文本把一次两字节读的完整时序展开,方便和逻辑分析仪波形对照:
S 0x6C A 0x0E A Sr 0x6D A [高字节] A [低字节] N PS 表示起始位,Sr 表示重复起始位,A 表示 ACK 应答,N 表示主设备在第 2 个字节前关闭 ACK,P 表示停止位。先发写方向地址和寄存器地址 0x0E,等这两个字节都得到 ACK 后,再发一次 START 并把方向切成读,AS5600 才会把 ANGLE 的高字节和低字节依次送到总线上。
注意:重复起始必须在写寄存器地址的 TXE 事件产生后再发,不能上一个字节还没移出去就拉 START,否则 F1 的硬件 I2C 会进入非法状态。
读两个字节时,主设备收到第 1 个字节后要主动把 ACK 清掉,并从此刻准备 STOP;第 2 个字节到达 RXNE 后立刻读走数据,再完成 STOP。好多从 GPIO 模拟 I2C 转过来的人习惯读完两个字节才拉 STOP,这在硬件 I2C 上会多收一个字节,且总线状态容易错乱。
3. STM32F103RCT6 硬件 I2C 初始化:引脚、上拉和 100kHz/400kHz 参数
3.1 引脚分配与重映射
STM32F103RCT6 的 I2C1 默认映射在 PB6/PB7,其中 PB6 是 SCL,PB7 是 SDA;如果 PCB 把线布到了 PB8/PB9,需要设置 AFIO 重映射位。I2C2 固定在 PB10/PB11,没有重映射选项。具体对应关系如下:
| 外设 | 默认引脚 | 重映射后 | 重映射开关 |
|---|---|---|---|
| I2C1 | PB6=SCL,PB7=SDA | PB8=SCL,PB9=SDA | AFIO->MAPR 的 I2C1_REMAP 位 |
| I2C2 | PB10=SCL,PB11=SDA | 无 | 无 |
GPIO 要配置成复用开漏输出,因为 I2C 总线靠外部上拉电阻把电平拉高,开漏结构才能实现“线与”和应答传输。F1 内部虽然有上拉,但开漏输出模式下内部上拉并不能提供足够的驱动强度,稳妥做法是在 PCB 上给 SCL 和 SDA 各放一颗 4.7kΩ 到 10kΩ 的上拉电阻;100kHz 标准模式用 10kΩ 就行,400kHz 快速模式且总线电容较大时换 4.7kΩ 或 2.2kΩ。
3.2 寄存器版初始化代码
下面是一段寄存器级初始化,不依赖 HAL 库,逻辑顺序是开时钟、配引脚、复位外设、设速度、使能。
void AS5600_I2C1_Init(void) { /* 开 GPIOB、AFIO、I2C1 的时钟 */ RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN; RCC->APB1ENR |= RCC_APB1ENR_I2C1EN; /* PB6=SCL, PB7=SDA,复用开漏输出,速度 10MHz */ GPIOB->CRL &= ~(0xFFUL << 24); GPIOB->CRL |= (0xDDUL << 24); /* 每个端口字段: CNF=11 复用开漏, MODE=01 10MHz */ /* 硬件复位 I2C1 外设,清掉上电后的不确定状态 */ I2C1->CR1 |= I2C_CR1_SWRST; I2C1->CR1 &= ~I2C_CR1_SWRST; /* PCLK1 是 36MHz,保持 I2C1->CR2 和外设实际时钟一致 */ I2C1->CR2 = 36; /* 100kHz: CCR = 36MHz / (2 * 100kHz) = 180 */ I2C1->CCR = 180; /* TRISE = 上升时间(us) * PCLK1(MHz) + 1 = 1 * 36 + 1 = 37 */ I2C1->TRISE = 37; /* 使能 I2C 外设 */ I2C1->CR1 |= I2C_CR1_PE; }代码里的 SWRST 不是必须每次上电都做,但保留它能避开一种现场问题:主控复位后从机状态机还在等数据,I2C 外设认为总线忙,这时直接发 START 会卡在 BUSY 判断上。先复位外设、再配置、再使能,是更干净的上电路径。
MODE 选 10MHz 而不是 50MHz,是因为 I2C 是开漏总线,引脚斜率由外部上拉电阻决定,MCU 输出驱动能力快了反而容易引入振铃;10MHz 挡位在大多数 F103 板子上比 50MHz 更稳。
3.3 PCLK1 与 CCR/TRISE 参数表
F103 的系统时钟一般是 72MHz,APB1 最大 36MHz,I2C 挂在这条总线上。CCR 的取值直接决定 SCL 频率,标准模式公式是CCR = PCLK1 / (2 * fSCL),TRISE 表示 SCL 上升沿允许的最长时间,单位是 I2C 时钟周期加 1。
| 目标 SCL | CCR 计算方式 | 36MHz 下的 CCR | TRISE 计算 |
|---|---|---|---|
| 100kHz 标准模式 | PCLK1 / (2 * fSCL) | 180 | 1us * 36 + 1 = 37 |
| 400kHz 快速模式 | PCLK1 / (2 * fSCL) | 45 | 0.3us * 36 + 1 = 12 |
| 400kHz 快速模式,DUTY=1 | PCLK1 / (3 * fSCL) | 30 | 同上 |
DUTY 位改成 1 后,SCL 低电平时间和高电平时间变成 2:1,适合低频段附近对占空比有要求的从机。AS5600 数据手册给出的 I2C 最大速率是 1MHz,F1 硬件 I2C 在快速模式下跑到 400kHz 是常规用法,再往上就得靠 DUTY 和高驱动能力的电阻了。
4. 读写 AS5600 的驱动源码:应答处理与两字节读取
4.1 基础读函数:单字节状态读取
先把单字节读函数写出来,寄存器地址由参数传入,读取 STATUS 和 CONF 时都会用到。
#define AS5600_ADDR 0x36 #define AS5600_STATUS 0x0B #define AS5600_RAWANGLE 0x0C #define AS5600_ANGLE 0x0E uint8_t I2C1_ReadReg(uint8_t reg) { uint8_t val; /* 总线忙则等待,避免外设还挂着上一次未完成的事务 */ while (I2C1->SR2 & I2C_SR2_BUSY); /* 起始条件,等待 EV5 */ I2C1->CR1 |= I2C_CR1_START; while (!(I2C1->SR1 & I2C_SR1_SB)); /* 写方向地址 0x6C,等待 EV6 */ I2C1->DR = (AS5600_ADDR << 1); while (!(I2C1->SR1 & I2C_SR1_ADDR)); /* 读 SR2 清除 ADDR 标志 */ (void)I2C1->SR2; /* 写寄存器地址,等待 EV8 */ while (!(I2C1->SR1 & I2C_SR1_TXE)); I2C1->DR = reg; while (!(I2C1->SR1 & I2C_SR1_TXE)); /* 重复起始,切到读方向 */ I2C1->CR1 |= I2C_CR1_START; while (!(I2C1->SR1 & I2C_SR1_SB)); I2C1->DR = (AS5600_ADDR << 1) | 0x01; while (!(I2C1->SR1 & I2C_SR1_ADDR)); (void)I2C1->SR2; /* 单字节接收:先关 ACK、先发 STOP,再等 RXNE */ I2C1->CR1 &= ~I2C_CR1_ACK; I2C1->CR1 |= I2C_CR1_STOP; while (!(I2C1->SR1 & I2C_SR1_RXNE)); val = I2C1->DR; /* 恢复 ACK,为下一次多字节读做准备 */ I2C1->CR1 |= I2C_CR1_ACK; return val; }这段代码每跑完一次函数,I2C 外设会被正确收回到空闲状态,下一次进入前 BUSY 判断基本不会生效。ADDR 标志必须通过读 SR2 清除,如果只读 SR1 不清 SR2,后续 EV6 会一直触发,表现为收发数据错位。
单字节接收里两个顺序很关键:先关 ACK、先发 STOP,再等 RXNE。要是反过来等到了数据才发 STOP,停止条件晚了一个位周期,从机可能把下一个字节也送出来。
4.2 读 ANGLE 两字节:NACK 和 POS 的时序位置
角度数据是 12 位,读 0x0E 和 0x0F 两个连续地址才能取回完整角度。两字节收发的到来主要在最后两个字节,处理方式和单字节略有区别:
uint16_t AS5600_ReadAngle(void) { uint8_t hi, lo; uint16_t angle; /* 先走写方向,把寄存器指针定位到 0x0E */ while (I2C1->SR2 & I2C_SR2_BUSY); I2C1->CR1 |= I2C_CR1_START; while (!(I2C1->SR1 & I2C_SR1_SB)); I2C1->DR = (AS5600_ADDR << 1); while (!(I2C1->SR1 & I2C_SR1_ADDR)); (void)I2C1->SR2; I2C1->DR = AS5600_ANGLE; while (!(I2C1->SR1 & I2C_SR1_TXE)); /* 重复起始,切读方向 */ I2C1->CR1 |= I2C_CR1_START; while (!(I2C1->SR1 & I2C_SR1_SB)); I2C1->DR = (AS5600_ADDR << 1) | 0x01; while (!(I2C1->SR1 & I2C_SR1_ADDR)); (void)I2C1->SR2; /* 关键参数:ACK=0 且 POS=1,硬件在第 1 个数据字节后自动产生 NACK */ I2C1->CR1 &= ~I2C_CR1_ACK; I2C1->CR1 |= I2C_CR1_POS; while (!(I2C1->SR1 & I2C_SR1_RXNE)); hi = I2C1->DR; I2C1->CR1 |= I2C_CR1_STOP; I2C1->CR1 &= ~I2C_CR1_POS; while (!(I2C1->SR1 & I2C_SR1_RXNE)); lo = I2C1->DR; angle = ((uint16_t)hi << 8) | lo; return angle & 0x0FFF; }POS 位是 STM32F1 硬件 I2C 处理两字节接收的特殊机制。置位后,硬件在第一个接收字节结束时不立即回 NACK,而是等两个字节都接收完,在第二个字节的应答位上回 NACK,这样从机刚好送完两个字节,主控也能在第 2 个字节前把 STOP 发出去。手动 GPIO 模拟 I2C 没有这个位,得自己控制每一位,这就是很多移植代码在硬件 I2C 上读 12 位角度读不对的根因。
读完最后把结果& 0x0FFF,因为 AS5600 的 ANGLE 寄存器只有低 12 位有意义,高 4 位会在某些批次芯片上读到不确定值。
4.3 写 CONF 配置寄存器,只改 RAM 不改 EEPROM
写方向驱动要处理的是 CONF、ZPOS、MPOS 这几个寄存器,它们写进 RAM 立即生效,掉电丢失。对 CONF 寄存器做一次写配置如下:
void AS5600_WriteConf(uint16_t conf) { /* 地址 0x07 是 CONF 高字节地址,0x08 是低字节地址 */ while (I2C1->SR2 & I2C_SR2_BUSY); I2C1->CR1 |= I2C_CR1_START; while (!(I2C1->SR1 & I2C_SR1_SB)); I2C1->DR = (AS5600_ADDR << 1); while (!(I2C1->SR1 & I2C_SR1_ADDR)); (void)I2C1->SR2; I2C1->DR = 0x07; while (!(I2C1->SR1 & I2C_SR1_TXE)); /* 先后写高字节、低字节,ASCII 段是 CONF[13:8] 和 CONF[7:0] */ I2C1->DR = (uint8_t)(conf >> 8); while (!(I2C1->SR1 & I2C_SR1_TXE)); I2C1->DR = (uint8_t)(conf & 0xFF); while (!(I2C1->SR1 & I2C_SR1_TXE)); I2C1->CR1 |= I2C_CR1_STOP; }这里只改 RAM 里的配置值,适合方向、停止位这些控制位需要频繁切换的场景。如果需要永久保存,AS5600 还有专门的烧写流程,数据手册里的 ZMCO 和烧写命令就是干这个用的。量产产品不要在电机运行中反复烧写 EEPROM,寿命和时序都不允许。
5. 硬件 I2C 总线卡死与 ACK 失败:定位方法和多从机场景
5.1 从现象区分原因,先别怀疑芯片
读 AS5600 遇到 ACK 失败,先分清楚是“从机没响应”还是“总线被拉死”。用示波器或万用表量 SCL/SDA 静态电平,按下面表格对照:
| 现象 | SCL/SDA 电平 | 大概率原因 |
|---|---|---|
| 发 0x6C 后一直没有 ADDR 事件 | 总线空闲时 SCL 高、SDA 高 | 地址方向写错、AS5600 供电没起来 |
| SDA 常低 | SCL 正常翻转,SDA 低 | 从机状态机卡住,或 PC 端设备把总线锁死 |
| 偶发 NACK,不是每次都出现 | 电平正常,速率过快 | 上拉电阻偏大、总线电容大、线太长 |
地址方向写错最典型:用 0x36 直接写 DR,总线变成 0x6C 的右移值,从机地址对不上。供电问题也常见,AS5600 的 VDD 引脚纹波大时会间歇性不应答,建议在 VDD 管脚附近放 100nF 去耦电容。
5.2 AS5600 地址固定,一条总线上怎么挂多路
AS5600 的 7 位地址固定是 0x36,不像 AT24C02 那样有 A0/A1/A2 地址脚,所以两颗 AS5600 不能直接并联在同一个硬件 I2C 接口上。常见的做法是按需切换总线域,或者用带地址通道的开关把多颗芯片隔离在不同总线段上;另外同一系列的 AS5600L 支持可配地址,新设计可以直接换料。
I2C 扩展器接在 F103 和 AS5600 之间时,要注意 AS5600 挂在扩展器下游,地址不会冲突,但读时序要经过两级,速度会打折扣。实际调试中我把 SCL 频率降到 100kHz 后,扩展器场景下的 ACK 失败率明显下降。
5.3 SCL 九个脉冲复位从机状态机
如果 SDA 被从机拉低,常见做法是先切 GPIO 手动恢复总线,再切回硬件 I2C。具体方法是把 PB6/PB7 临时配成推挽输出,手动翻转 SCL 9 次,让从机内部的位计数器回到帧起始位置:
void I2C1_BusRecover(void) { /* 先把两个脚切成推挽输出,速度 50MHz */ GPIOB->CRL &= ~(0xFFUL << 24); GPIOB->CRL |= (0x33UL << 24); /* CNF=00 通用推挽, MODE=11 */ /* SCL 和 SDA 都先释放为高 */ GPIOB->BSRR = GPIO_BSRR_BS6 | GPIO_BSRR_BS7; /* 手动拉 9 个 SCL 脉冲,让从机复位 */ for (int i = 0; i < 9; i++) { GPIOB->BRR = GPIO_BRR_BR6; for (volatile int d = 0; d < 100; d++); GPIOB->BSRR = GPIO_BSRR_BS6; for (volatile int d = 0; d < 100; d++); } /* 恢复成复用开漏配置 */ GPIOB->CRL &= ~(0xFFUL << 24); GPIOB->CRL |= (0xDDUL << 24); }这段代码在每次初始化 I2C 前调用一次,能消化绝大多数的总线卡死。恢复正常后,还要确认 AS5600 的 VDD 和 GND 单点接地,部分不稳定现象是共地电阻造成的地弹,而不是协议问题。
6. AS5600 角度进闭环控制系统前:滤波、过零和零点校准
6.1 处理 4095→0 跳变,一阶低通滤波
AS5600 的 12 位角度在一个完整旋转中从 4095 回到 0,如果在差值计算时不处理回绕,普通减法会得到 4095 的假跳变,闭环里就是一次巨大的位置阶跃。常用做法是把两个 uint16 相减后按有符号处理,再根据差值大小修正到 ±2048 范围内:
static uint16_t last_angle; static int32_t angle_acc; int16_t Angle_GetDelta(uint16_t current) { int16_t delta = (int16_t)(current - last_angle); last_angle = current; if (delta > 2048) delta -= 4096; else if (delta < -2048) delta += 4096; return delta; }(int16_t)(current - last_angle)这一步利用无符号减法回绕的特性,把 4095→0 的变化解释成 -1,再通过两次边界判断修正,就能得到连续的角度增量。把增量累加到angle_acc上,可以得到多圈累计角度,适合做关节电机这类需要多圈位置的应用。
对单圈位置再做一阶低通时,建议把滤波放在增量之后而不是原始值之上,否则 4095 和 0 两个相邻值会被滤波器拉出中间位置,产生不该有的过渡角度。滤波器系数可以根据控制频率调整,增量路径上用右移代替浮点除法,整型运算在 F103 上更高效。
6.2 磁场强度诊断和零点校准,放在产线流程里做
闭环控制跑起来之前,先读一次 STATUS 寄存器确认磁铁安装位置是否在安全窗口内。STATUS 的位 3 是 MD,表示磁场强度是否达到检测阈值;位 5 是 MH 表示磁场太强,位 4 是 ML 表示磁场太弱。检查逻辑直接放在初始化末尾:
st = I2C1_ReadReg(AS5600_STATUS); if (st & 0x20) { /* 磁场过强,磁铁离芯片太近或直径过大 */ } if (st & 0x10) { /* 磁场过弱,磁铁太远或偏心太大 */ }这两个状态位在生产中很有用,结构件装配偏差、磁铁充磁不足都会先反映在 MH/ML 上。正常运行中我还会周期性读一次 STATUS,一旦 ML 出现,说明硬件已经处于失效边缘,闭环输出不能继续信任。
零点校准的常见做法是:在机械装配到位后,把当前角度作为基准位置,然后写 ZPOS 寄存器将基准点映射成 0。写 ZPOS 的过程和写 CONF 相同,但有一个前提,机械零点必须在磁铁完全固定的状态下标定,不能在电机通电状态下手持磁铁去校零。如果想保存到永久存储,还要按 AS5600 手册的 EEPROM 烧写命令执行一次,这个操作只放在产线工位里跑,运行时逻辑里不要频繁调用。
调试时可以先读到原始角度和 ANGLE 两个值,对比它们之间的差值,如果差值正好等于 ZPOS 的配置值,说明映射逻辑生效了。把读到的 12 位数据换算成角度,用360.0f * raw / 4096.0f验证一下输出,确认硬件 I2C 整个链路已经稳定可用。
本文还有配套的精品资源,点击获取