在嵌入式系统开发中,精确的角度测量是一个常见需求。AS5600是一款非接触式磁性角度传感器,通过I2C接口输出12位分辨率的角度数据。本文将详细介绍如何在STM32平台上通过软件模拟I2C协议实现AS5600的角度读取。
这个方案特别适合那些硬件I2C资源紧张或者需要灵活配置I2C时序的场景。通过GPIO模拟I2C协议,我们可以完全掌控通信时序,解决硬件I2C可能遇到的兼容性问题。我在多个机器人关节控制项目中都采用了这种方案,实测角度读取稳定可靠。
I2C协议有两个基本时序规则需要严格遵守:
唯一的例外是起始和停止信号,它们打破了上述规则,这也是I2C协议中识别总线起始和结束的特殊方式。
在STM32上模拟I2C需要特别注意GPIO的模式切换。以下是核心宏定义:
c复制#define SDA_IN() {GPIOB->CRH&=0xFFFF0FFF;GPIOB->CRH|=0x00008000;} // 配置PB11为输入模式
#define SDA_OUT() {GPIOB->CRH&=0xFFFF0FFF;GPIOB->CRH|=0x00003000;} // 配置PB11为输出模式
#define READ_SDA (GPIOB->IDR&(1<<11)) // 读取PB11引脚电平状态
#define IIC_SCL_1 GPIO_SetBits(GPIOB,GPIO_Pin_10) // 设置PB10(SCL)为高电平
#define IIC_SCL_0 GPIO_ResetBits(GPIOB,GPIO_Pin_10) // 设置PB10(SCL)为低电平
#define IIC_SDA_1 GPIO_SetBits(GPIOB,GPIO_Pin_11) // 设置PB11(SDA)为高电平
#define IIC_SDA_0 GPIO_ResetBits(GPIOB,GPIO_Pin_11) // 设置PB11(SDA)为低电平
注意:在读取数据时,必须先将SDA切换为输入模式;发送数据时再切换回输出模式。这个细节在实际调试中最容易被忽视。
AS5600有几个关键寄存器我们需要关注:
c复制#define RAW_Angle_Hi 0x0C // 原始角度值高位寄存器地址
#define RAW_Angle_Lo 0x0D // 原始角度值低位寄存器地址
#define AS5600_Address 0x36 // AS5600的I2C设备地址
AS5600的I2C地址固定为0x36,这个地址在硬件设计时通过芯片引脚确定,无法更改。
以下是读取AS5600单个寄存器的函数实现:
c复制u8 AS5600_ReadOneByte(u8 addr) {
u8 temp;
IIC_Start(); // 发送起始信号
IIC_Send_Byte(AS5600_Address<<1); // 发送设备地址+写位
IIC_Wait_Ack(); // 等待应答
IIC_Send_Byte(addr); // 发送寄存器地址
IIC_Wait_Ack(); // 等待应答
IIC_Start(); // 发送重复起始信号
IIC_Send_Byte((AS5600_Address<<1)+1); // 发送设备地址+读位
IIC_Wait_Ack(); // 等待应答
temp=IIC_Read_Byte(0); // 读取数据,发送非应答
IIC_Stop(); // 发送停止信号
return temp; // 返回读取到的数据
}
AS5600的原始角度值是12位分辨率(0-4095),但通过I2C接口读取时分为高8位和低8位两个寄存器:
c复制u16 AS5600_ReadRawAngleTwo(void) {
u8 dh,dl;
IIC_Start(); // 发送起始信号
IIC_Send_Byte(AS5600_Address<<1); // 发送设备地址+写位
IIC_Wait_Ack(); // 等待应答
IIC_Send_Byte(RAW_Angle_Hi); // 发送角度高位寄存器地址
IIC_Wait_Ack(); // 等待应答
IIC_Start(); // 发送重复起始信号
IIC_Send_Byte((AS5600_Address<<1)+1); // 发送设备地址+读位
IIC_Wait_Ack(); // 等待应答
dh=IIC_Read_Byte(1); // 读取高位,发送应答
dl=IIC_Read_Byte(0); // 读取低位,发送非应答
IIC_Stop(); // 发送停止信号
return ((dh<<8)+dl); // 组合高低位返回16位值
}
实际使用时,我们需要将读取到的12位原始值(0-4095)转换为实际角度(0-360°),转换公式为:实际角度 = 原始值 × (360/4096) ≈ 原始值 × 0.08789
为了实现周期性角度读取,我们配置TIM2定时器产生1ms中断:
c复制void TIM2_Init(u16 arr,u16 psc) {
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); // 使能TIM2时钟
NVIC_InitStructure.NVIC_IRQChannel=TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority=0;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_Init(&NVIC_InitStructure);
TIM_TimeBaseInitStructure.TIM_Period = arr;
TIM_TimeBaseInitStructure.TIM_Prescaler=psc;
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE);
TIM_Cmd(TIM2,ENABLE);
}
参数计算:对于72MHz的系统时钟,设置arr=999,psc=71,定时周期为:
(999+1)×(71+1)/72MHz = 1000us = 1ms
主程序整合了所有功能模块:
c复制int main(void) {
GPIO_Config(); // 初始化GPIO
uart_init(115200); // 初始化串口
delay_s(0x5fffff); // 延时等待系统稳定
printf("Initial OK!\r\n"); // 打印初始化信息
TIM2_Init(999,71); // 初始化TIM2定时器
while(1) {
if(time1_cntr>=200) { // 每200ms
time1_cntr=0;
LED_blink; // LED闪烁
}
if(time2_cntr>=200) { // 每200ms
time2_cntr=0;
// 读取并打印角度值
printf("Angle_I2C=%.4f\r\n",AS5600_ReadRawAngleTwo()*0.08789);
}
}
}
在实际项目中,我发现AS5600对磁铁的位置非常敏感。最佳实践是使用专门的安装支架固定磁铁,保持与传感器垂直距离恒定。另外,温度变化会影响磁性材料的特性,在精度要求高的场合需要考虑温度补偿。