代码已更新到github:wuyansama111/tianwen_imu: aoerospace_project_imu
前段时间对组合导航比较感兴趣,于是想搞一个惯性导航来用。思来想去还是没选择MPU6050,太老了,于是选择了一款比较新的惯导:icm45686
icm45686对比MPU6050,在网上资料相对较少,且比较零碎,博主根据官方程序和芯片手册跑通了通信,在此分享出来,并对一些关键程序进行解释,如有忽略请多包涵。
一、IIC通信配置
iic_gpio_config非常标准的iic初始化配置,这里我选取了PB8和PB9作为SCL和SDA进行通信。
void iic_gpio_config(void)//PB8==SCL PB9==SDA
{
/* 使能时钟 */
rcu_periph_clock_enable(RCU_SCL);
rcu_periph_clock_enable(RCU_SDA);
/* 配置SCL为输出模式 */
gpio_mode_set(PORT_SCL,GPIO_MODE_OUTPUT,GPIO_PUPD_PULLUP,GPIO_SCL);
/* 配置为开漏输出 50MHZ */
gpio_output_options_set(PORT_SCL,GPIO_OTYPE_OD,GPIO_OSPEED_50MHZ,GPIO_SCL);
/* 配置SDA为输出模式 */
gpio_mode_set(PORT_SDA,GPIO_MODE_OUTPUT,GPIO_PUPD_PULLUP,GPIO_SDA);
/* 配置为开漏输出 50MHZ */
gpio_output_options_set(PORT_SDA,GPIO_OTYPE_OD,GPIO_OSPEED_50MHZ,GPIO_SDA);
}
IIC_Start,IIC_Stop,IIC_NAck,IIC_Ack,IIC_Wait_Ack,这五个函数采用的是比较通用的iic程序,分别对应着起始信号,停止信号,发送非应答,发送应答和等待应答。需要额外注意的是,iic通信需要的延时函数使用的是FreeRTOS的延时函数,因此该函数最好在线程内部运行。
延时函数已替换成正确的微秒级延时,此前采用的延时为毫秒级,github最新代码已更改
//起始信号
void IIC_Start(void)
{
SDA_OUT();
SDA(1);
SCL(1);
delay_us(4);
SDA(0);
delay_us(4);
SCL(0);
}
//停止信号
void IIC_Stop(void)
{
SDA_OUT();
SCL(0);
SDA(0);
delay_us(4);
SCL(1);
SDA(1);
delay_us(4);
}
//发送非应答
void IIC_NAck(void)
{
SCL(0);
SDA_OUT();
SDA(1);
delay_us(2);
SCL(1);
delay_us(2);
SCL(0);
}
//发送应答
void IIC_Ack(void)
{
SCL(0);
SDA_OUT();
SDA(0);
delay_us(2);
SCL(1);
delay_us(2);
SCL(0);
}
uint8_t IIC_Wait_Ack(void)
{
uint8_t ucErrTime=0;
SDA_IN(); //SDA设置为输入
SDA(1);delay_us(1);
SCL(1);delay_us(1);
while(SDA_GET())
{
ucErrTime++;
if(ucErrTime>250)
{
IIC_Stop();
return 1;
}
}
SCL(0);//时钟输出0
return 0;
}
在有上述五个函数后就可以实现通过iic发送一个字节与接收一个字节了,即IIC_Send_Byte和IIC_Read_Byte
//发送一个字节
void IIC_Send_Byte(uint8_t txd)
{
uint8_t t;
SDA_OUT();
SCL(0);//拉低时钟开始数据传输
for(t=0;t<8;t++)
{
SDA((txd&0x80)>>7);
txd<<=1;
delay_us(2);
SCL(1);
delay_us(2);
SCL(0);
delay_us(2);
}
}
uint8_t IIC_Read_Byte(unsigned char ack)
{
unsigned char i,receive=0;
SDA_IN();//SDA设置为输入
for(i=0;i<8;i++ )
{
SCL(0);
delay_us(2);
SCL(1);
receive<<=1;
if(SDA_GET())receive++;
delay_us(1);
}
if (!ack)
IIC_NAck();//发送nACK
else
IIC_Ack(); //发送ACK
return receive;
}
实际上IIC通信一般还存在器件地址与寄存器地址,所以单独使用上述两个函数实现正确读取还是比较困难的,因此进行进一步的封装,可有IIC_Write_1Byte和IIC_Read_1Byte
uint8_t IIC_Write_1Byte(uint8_t SlaveAddress,uint8_t REG_Address,uint8_t REG_data)
{
IIC_Start();
IIC_Send_Byte((SlaveAddress<<1)|0);//发送器件地址+写命令
if(IIC_Wait_Ack()) //等待应答
{
IIC_Stop();
return 1;
}
IIC_Send_Byte(REG_Address); //写寄存器地址
IIC_Wait_Ack(); //等待应答
IIC_Send_Byte(REG_data);//发送数据
if(IIC_Wait_Ack()) //等待ACK
{
IIC_Stop();
return 1;
}
IIC_Stop();
return 0;
}
uint8_t IIC_Read_1Byte(uint8_t SlaveAddress,uint8_t REG_Address)
{
uint8_t data;
IIC_Start();
IIC_Send_Byte((SlaveAddress<<1)|0);//发送器件地址+写命令
IIC_Wait_Ack(); //等待应答
IIC_Send_Byte(REG_Address); //写寄存器地址
IIC_Wait_Ack(); //等待应答
IIC_Start();
IIC_Send_Byte((SlaveAddress<<1)|1);//发送器件地址+读命令
IIC_Wait_Ack(); //等待应答
data=IIC_Read_Byte(0);//读取数据,发送nACK
IIC_Stop(); //产生一个停止条件
return data;
}
有了这两个函数后可以实现对相应寄存器的读取了,在这可以试试单独读取器件地址0x69,寄存器地址0x72(who am i),看看是否可以读取到0xE9。实际上单独读取一个寄存器还是不满足需求,需要一个可以连续读取多个字节的函数,进一步封装:IIC_Read_nByte和IIC_Write_nByte。
uint8_t IIC_Write_nByte(uint8_t SlaveAddress, uint8_t REG_Address, uint16_t len, const uint8_t *buf)
{
IIC_Start();
IIC_Send_Byte(SlaveAddress<<1);
if(IIC_Wait_Ack())
{
IIC_Stop();
return 1;
}
IIC_Send_Byte(REG_Address);
IIC_Wait_Ack();
while(len--)
{
IIC_Send_Byte(*buf++);
IIC_Wait_Ack();
}
IIC_Stop();
return 0;
}
// IIC读n字节数据
uint8_t IIC_Read_nByte(uint8_t SlaveAddress, uint8_t REG_Address, uint16_t len, uint8_t *buf)
{
IIC_Start();
IIC_Send_Byte(SlaveAddress<<1);
if(IIC_Wait_Ack())
{
IIC_Stop();
return 1;
}
IIC_Send_Byte(REG_Address);
IIC_Wait_Ack();
IIC_Start();
IIC_Send_Byte(SlaveAddress<<1 | 0x01);
IIC_Wait_Ack();
while(len)
{
if(len == 1)
{
*buf = IIC_Read_Byte(0);
}
else
{
*buf = IIC_Read_Byte(1);
}
buf++;
len--;
}
IIC_Stop();
return 0;
}
到此IIC通信上的工作结束了,接下来就是针对icm45686的工作了
二、icm45686配置
icm45686的AD0如果 处于悬空状态,那该引脚为高电平,因此器件地址为0x69,主要寄存器地址及其功能见下表,详细内容请见官方配置手册,本文资料会附带中英文双版手册(中文为机翻)
寄存器地址 | 寄存器名称 | 主要功能 |
---|---|---|
0x7F | REG_MISC2 | 复位 |
0x32 | DRIVE_CONFIG0 | IIC和SPI模式选择 |
0x18 | INT1_CONFIG2 | int1中断配置 |
0x16 | INT1_CONFIG0 | int1中断配置 |
0x1B | ACCEL_CONFIG0 | 加速度传感器配置 |
0x1C | GYRO_CONFIG0 | 陀螺仪传感器配置 |
0x10 | PWR_MGMT0.1 | 陀螺仪与加速度传感器工作模式配置 |
由此可有imu初始化函数imu_init
void imu_init(void)//初始化函数,应在线程函数中初始化
{
IIC_Write_1Byte(0x69,0x7F,0x02);//初始化
IIC_Write_1Byte(0x69,0x32,0x10);//配置iic接口
IIC_Write_1Byte(0x69,0x18,0x01);//
IIC_Write_1Byte(0x69,0x16,0x04);//配置中断引脚
IIC_Write_1Byte(0x69,0x1B,0x38);//设置加速度范围±4g
IIC_Write_1Byte(0x69,0x1C,0x38);//设置角速率范围±500deg
IIC_Write_1Byte(0x69,0x10,0x0F);//开启陀螺仪加速度计处于低噪音模式
}
为了防止数据被意外情况污染,因此在读取寄存器时要判断数据是否有效 ,因此可有:read_dreg
static int read_dreg(uint8_t slave ,uint8_t reg, uint32_t len, uint8_t *buf)//读取函数
{
if (IIC_Read_nByte(slave,reg,len,buf)!=0)
return INV_IMU_ERROR_TRANSPORT;
return INV_IMU_OK;
}
由于icm45686为大端通信,因此读取到的数据需要整合一下,因此有inv_imu_get_register_data
int inv_imu_get_register_data(icm45686_sensor_data *data)//解包大端通信
{
int status = INV_IMU_OK;
uint8_t buf[sizeof(icm45686_sensor_data)];
status |= read_dreg(icm45686_slave_adress, icm45686_reg_adress, sizeof(icm45686_sensor_data),buf);
data->accel_data[0]=(int16_t)buf[0]|(buf[1]<<8);
data->accel_data[1]=(int16_t)buf[2]|(buf[3]<<8);
data->accel_data[2]=(int16_t)buf[4]|(buf[5]<<8);
data->gyro_data [0]=(int16_t)buf[6]|(buf[7]<<8);
data->gyro_data [1]=(int16_t)buf[8]|(buf[9]<<8);
data->gyro_data [2]=(int16_t)buf[10]|(buf[11]<<8);
data->temp_data =(int16_t)buf[12]|(buf[13]<<8);
return status;
}
读取数据的寄存器及内容见下表
寄存器地址 | 寄存器名称 | 主要内容 |
0x00 | ACCEL_DATA_X1_UI | ACCEL_DATA_X_UI[15:8] |
0x01 | ACCEL_DATA_X0_UI | ACCEL_DATA_X_UI[7:0] |
0x02 | ACCEL_DATA_Y1_UI | ACCEL_DATA_Y_UI[15:8] |
0x03 | ACCEL_DATA_Y0_UI | ACCEL_DATA_Y_UI[7:0] |
0x04 | ACCEL_DATA_Z1_UI | ACCEL_DATA_Z_UI[15:8] |
0x05 | ACCEL_DATA_Z0_UI | ACCEL_DATA_Z_UI[7:0] |
0x06 | GYRO_DATA_X1_UI | GYRO_DATA_X_UI[15:8] |
0x07 | GYRO_DATA_X0_UI | GYRO_DATA_X_UI[7:0] |
0x08 | GYRO_DATA_Y1_UI | GYRO_DATA_Y_UI[15:8] |
0x09 | GYRO_DATA_Y0_UI | GYRO_DATA_Y_UI[7:0] |
0x0A | GYRO_DATA_Z1_UI | GYRO_DATA_Z_UI[15:8] |
0x0B | GYRO_DATA_Z0_UI | GYRO_DATA_Z_UI[7:0] |
0x0C | TEMP_DATA1_UI | TEMP_DATA_UI[15:8] |
0x0D | TEMP_DATA0_UI | TEMP_DATA_UI[7:0] |
在读取数据后,因为数据本身是ICM45686进行AD转换得来的,因此需要计算得到正确值
计算方法:65535/2/目标最大值,温度传感器标定公式为官方手册提供,可有bsp_IcmGetRawData
int bsp_IcmGetRawData(float accel_mg[3], float gyro_dps[3], float *temp_degc)//数值处理函数
{
int rc = 0;
icm45686_sensor_data d;
rc |= inv_imu_get_register_data(&d);
//SI_CHECK_RC(rc);
accel_mg[0] = (float)(d.accel_data[0])/8192.0f*9.8f;//加速度单位为g,因此我又简单补了一下
accel_mg[1] = (float)(d.accel_data[1])/8192.0f*9.8f;
accel_mg[2] = (float)(d.accel_data[2])/8192.0f*9.8f;
gyro_dps[0] = (float)(d.gyro_data[0])/65.0f;
gyro_dps[1] = (float)(d.gyro_data[1])/65.0f;
gyro_dps[2] = (float)(d.gyro_data[2])/65.0f;
*temp_degc = (float)(25+((d.temp_data ))/128.0f);
//计算方法:65535/2/目标最大值,温度传感器标定公式为官方手册提供
return 0;
}
到此icm45686的加速度传感器和陀螺仪数据都被读了出来。
三、效果呈现
读取来的数据通过VOFA+显示,所以需要串口通信,通信格式为firewater格式数据 "<any>:ch0,ch1,ch2,...,chN\n"
该数据为直接从icm45686读取得到的,我未进行滤波处理。