ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32标准库驱动IMU601六轴传感器:I2C配置、姿态解算与避坑指南

STM32标准库驱动IMU601六轴传感器:I2C配置、姿态解算与避坑指南 简介IMU601六轴陀螺仪标准库代码包专为使用STM32标准库的开发者设计解决正点原子官方资料以HAL库为主导致标准库用户难以直接移植的问题。资源共208个文件以C源码和头文件为主包含工程配置文件、链接脚本以及烧录用的hex文件同时附带有编译辅助脚本压缩包整体仅7.32MB结构清晰便于按模块引用。目前已有807人学习下载。作者将原有HAL库代码彻底改写为标准库写法并保留LCD显示、定时器、ADC采集、I2C通信及CAN总线等常用外设驱动开发项目时无需重新编写底层接口直接调用封装好的参数即可快速获取六轴姿态数据可显著减少标准库项目的移植与调试工作量。资源适用于智能车、平衡小车、机械臂姿态反馈等常见嵌入式场景对正在学习标准库的初学者也有较好的参考价值。1. 项目概述与整体设计思路IMU601这种六轴模块说穿了就是把三轴加速度计和三轴陀螺仪焊在一小块PCB上再用I2C总线把数据吐给主控。很多人觉得这类传感器驱动难写其实真正难的部分不是那几十个寄存器而是底层的I2C时序、量程换算和后期滤波。这篇文章我以STM32F103C8T6配合标准外设库v3.5为平台给出一套从硬件接线、I2C初始化、寄存器配置到原始数据读取、物理量换算和姿态角解算的IMU601标准库代码并把我实际调试时踩过的坑一并写出来。选择标准库而不是HAL库在当前很多人看来好像有点复古。但标准库最大的优势就是离寄存器近代码执行路径非常透明。读取IMU601本质上就是设置寄存器、读寄存器、拼字节、算数据这么几件事用HAL库反而要在初始化结构体和回调函数之间绕来绕去。标准库写法直接、占用Flash小也符合大多数老工程师的代码习惯。适合用这套代码的人包括正在学STM32的在校生、给竞赛小车写底层驱动的同学以及维护老项目需要快速集成传感器的工程师。1.1 IMU601六轴模块能做什么六轴IMU能同时输出三轴加速度和三轴角速度。加速度计用于测量物体受到的加速度静态时可以推算出重力方向也就是倾斜角陀螺仪用于测量旋转角速度对角速度积分可以得到角度的变化量。两者结合就可以估算出物体的俯仰角和横滚角。实际场景包括自平衡小车的姿态环、两轮或四轮机器人的倾倒检测、云台的增稳控制、机械臂末端抖动分析以及各种可穿戴跌倒检测。要注意它没有磁力计所以无法得到一个绝对稳定的航向角偏航角会随时间漂移这也是六轴和九轴IMU的一个重要区别。如果你的项目需要精确的偏航角要么外接磁力计要么用编码盘或者UWB等其他传感器做补偿。1.2 标准库方案的两个核心优点第一是代码透明。标准库本质上是把寄存器操作封装成函数你调用I2C_GenerateSTART之后还能清楚看到硬件I2C状态机的每一步。这对理解I2C协议本身帮助很大排查总线问题时能顺着函数一路找到物理寄存器。第二是工程结构简洁。一个完整的IMU601驱动刨掉底层I2C核心代码量通常不超过一百行编译出来的固件体积也很小这对Flash只有64KB的STM32F103C8T6来说非常友好。2. 硬件连接与标准库工程准备2.1 IMU601模块引脚与接线表常见的IMU601模块虽然厂家不同但引脚定义基本是VCC、GND、SCL、SDA这四个部分会多一个地址选择脚AD0。这里使用STM32F103的硬件I2C1对应引脚是PB6和PB7。IMU601引脚功能STM32F103引脚VCC电源正3.3VGND电源地GNDSCLI2C时钟PB6SDAI2C数据PB7AD0地址选择GND地址0x68接线时有一个容易被忽视的细节I2C总线需要上拉电阻。很多模块板上其实已经自带但如果你用的是面包板加杜邦线飞线或者模块上明确没赶上拉就要在SCL和SDA上各接一个4.7k电阻到3.3V。没有上拉电阻的话通信大概率失败而且表现出的现象很诡异有时候能读有时候卡死。2.2 标准库v3.5工程搭建的几个关键点工程的搭建思路是标准套路我重点说三个特别容易出问题的地方。首先编译之前必须在Keil的C/C选项卡里定义两个宏USE_STDPERIPH_DRIVER和STM32F10X_MD。前者告诉标准库使用外设驱动后者告诉标准库当前芯片的容量等级。STM32F103C8T6属于中等容量所以用STM32F10X_MD。漏掉任何一个宏编译期都会出现一堆库函数未定义的报错很多新手会卡在这一步很久。其次启动文件要选startup_stm32f10x_md.s别把HD的启动文件拷进来。MD和HD的启动文件虽然长得像但在中断向量表大小和内存初始化上是有区别的选错后程序能编译能下载运行时却可能莫名跑飞。最后关于时钟。默认情况下标准外设库的系统时钟初始化代码会把STM32F103的PLL配置为9倍频前提是你外接8MHz晶振这样得到72MHz主频。如果你的板上晶振不是8MHz就要修改system_stm32f10x.c里的PLL参数。实测中最直观的检查方法是看延时精度如果1ms的延时函数实际差别很大多半就是主频配置不对而不是延时代码写错了。3. 驱动代码实现从I2C底层到寄存器配置3.1 I2C引脚初始化和400kHz快速模式先看I2C1的引脚初始化。这里要把PB6和PB7配置为开漏复用输出同时使能I2C1和GPIOB的时钟。void IMU601_I2C_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; I2C_InitTypeDef I2C_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_OD; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1, ENABLE); RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1, DISABLE); I2C_InitStructure.I2C_Mode I2C_Mode_I2C; I2C_InitStructure.I2C_ClockSpeed 400000; I2C_InitStructure.I2C_DutyCycle I2C_DutyCycle_2; I2C_InitStructure.I2C_Ack I2C_Ack_Enable; I2C_InitStructure.I2C_AcknowledgedAddress I2C_AcknowledgedAddress_7bit; I2C_Init(I2C1, I2C_InitStructure); I2C_Cmd(I2C1, ENABLE); }注意中间那两句关于外设复位的代码。为什么要复位一次再释放因为旧工程在调试时可能把I2C外设的状态机停在某个非正常状态复位一下能确保寄存器回到默认值。这是我用了很长时间硬件I2C之后养成的习惯。时钟速度我直接用400kHz快速模式不过如果你的连线和上拉质量一般建议改成100000通信稳定性会明显提升传感器读取频率并不会低到不可用。3.2 单字节写入和多字节读取的完整实现接下来是与IMU601通信的核心。我在这里约定下面函数传入的设备地址都是7位地址0x68而ST标准库的I2C_Send7bitAddress期望接收的是8位地址形式所以在函数内部统一做了一次左移一位也就是devAddr 1。如果不搞清楚这个转换很多初学者会直接在传参时写0xD0然后又移位一次结果地址变成0xA0通信就彻底对不上了。写一个字节比较简单uint8_t IMU601_WriteByte(uint8_t devAddr, uint8_t regAddr, uint8_t data) { while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); I2C_GenerateSTART(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, devAddr 1, I2C_Direction_Transmitter); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); I2C_SendData(I2C1, regAddr); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_SendData(I2C1, data); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_GenerateSTOP(I2C1, ENABLE); return 0; }读取多字节要稍微复杂一点因为中间需要一次重复起始信号而且最后一个字节必须发送非应答信号NAK然后再停止。如果最后一个字节仍然返回ACK从机会认为主机还要继续读数据总线时序就会乱掉。uint8_t IMU601_ReadBytes(uint8_t devAddr, uint8_t regAddr, uint8_t *buf, uint8_t len) { uint8_t i; while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); I2C_GenerateSTART(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, devAddr 1, I2C_Direction_Transmitter); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); I2C_SendData(I2C1, regAddr); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_GenerateSTART(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, devAddr 1, I2C_Direction_Receiver); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); for (i 0; i len; i) { if (i len - 1) { I2C_AcknowledgeConfig(I2C1, DISABLE); } while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); buf[i] I2C_ReceiveData(I2C1); } I2C_AcknowledgeConfig(I2C1, ENABLE); I2C_GenerateSTOP(I2C1, ENABLE); return 0; }这段代码每一个CheckEvent都是等硬件状态机走到那一步再继续一步都不能省略。实际调试中如果发现卡住八成是上拉电阻缺失或者模块地址不对我把排查方法放到第五部分展开。3.3 IMU601寄存器配置与初始化函数IMU601这类六轴模块的寄存器布局和MPU6050系列基本一致核心寄存器就是下面这几个。我手头模块的厂家手册也是这样排列的如果你的模块寄存器地址不同务必以手册为准。寄存器地址寄存器名称配置值作用0x6BPWR_MGMT_10x00退出睡眠模式并选择内部时钟0x19SMPLRT_DIV0x07采样率设置为 1kHz/(17)125Hz0x1ACONFIG0x04开启DLPF截止频率约21Hz0x1BGYRO_CONFIG0x08陀螺仪满量程 ±500°/s0x1CACCEL_CONFIG0x00加速度计满量程 ±2g在代码文件开头先定义设备地址#define IMU601_ADDR 0x68 // 7位从机地址初始化函数如下void IMU601_Init(void) { IMU601_WriteByte(IMU601_ADDR, 0x6B, 0x80); // 先复位 delay_ms(100); IMU601_WriteByte(IMU601_ADDR, 0x6B, 0x00); // 唤醒 IMU601_WriteByte(IMU601_ADDR, 0x19, 0x07); // 采样率分频 IMU601_WriteByte(IMU601_ADDR, 0x1A, 0x04); // 开启低通滤波 IMU601_WriteByte(IMU601_ADDR, 0x1B, 0x08); // 陀螺仪 ±500°/s IMU601_WriteByte(IMU601_ADDR, 0x1C, 0x00); // 加速度计 ±2g }第一行的0x80是复位命令之后延时100ms再唤醒。有人嫌这100ms太长把它缩短到10ms偶尔也能工作但没有必要省这个时间。传感器上电后内部需要稳定特别是陀螺仪过早操作容易出现读出数据跳变。4. 数据读取、物理量换算和滤波4.1 原始数据读取函数加速度计和陀螺仪的数据寄存器在0x3B到0x48每个通道16位高字节在前。可以直接从0x3B开始连续读取14个字节把加速度计、温度、陀螺仪一次取回来。typedef struct { int16_t ax, ay, az; int16_t gx, gy, gz; } imu601_raw_t; void IMU601_ReadRaw(imu601_raw_t *raw) { uint8_t buf[14]; IMU601_ReadBytes(IMU601_ADDR, 0x3B, buf, 14); raw-ax (int16_t)((buf[0] 8) | buf[1]); raw-ay (int16_t)((buf[2] 8) | buf[3]); raw-az (int16_t)((buf[4] 8) | buf[5]); // buf[6], buf[7] 是温度 raw-gx (int16_t)((buf[8] 8) | buf[9]); raw-gy (int16_t)((buf[10] 8) | buf[11]); raw-gz (int16_t)((buf[12] 8) | buf[13]); }这里必须用int16_t不能用uint16_t。加速度计和陀螺仪输出的原始值是二进制补码形式的带符号数如果用了无符号类型你看到的负值就会变成很大的正数后面换算全部出错。4.2 原始数据换算成g和度每秒换算比例取决于你在初始化里选择的量程。我使用的±2g和±500°/s对应下面两组参数。初始化配置加速度计量程加速度计灵敏度陀螺仪量程陀螺仪灵敏度ACCEL_CONFIG0x00±2g16384 LSB/g无无GYRO_CONFIG0x08无无±500°/s65.5 LSB/(°/s)换算代码很简单float ax_g (float)raw-ax / 16384.0f; float gx_dps (float)raw-gx / 65.5f;为什么要用浮点因为换算结果要参与后面的角度运算。如果直接用整数除法接近0的小角度会被直接截成0姿态角就看不到平滑变化了。如果项目里资源紧张也可以用定点数但那时要额外维护小数位代码可读性会下降。4.3 陀螺仪零偏校准陀螺仪静止时输出并不是严格为0而是存在一个固定的零偏通常表现为几十到几百个LSB。如果不去掉零偏就积分角度会一直朝一个方向漂移哪怕放在桌面上不动解算出来的角度也会越跑越偏。最简单的标定方法是上电后先静止采集几百个样本把平均值当作零偏存下来后续每次读取后减去它。int32_t gx_offset 0, gy_offset 0, gz_offset 0; void IMU601_Calibration(void) { int i; imu601_raw_t tmp; gx_offset gy_offset gz_offset 0; for (i 0; i 500; i) { IMU601_ReadRaw(tmp); gx_offset tmp.gx; gy_offset tmp.gy; gz_offset tmp.gz; delay_ms(2); } gx_offset / 500; gy_offset / 500; gz_offset / 500; }标定完成后每次读陀螺仪的有效角速度是float gx_comp (float)(raw-gx - gx_offset) / 65.5f;这个环节看着笨但对姿态解算的稳定性提升非常大。凡是做过自平衡小车的人基本都有体会陀螺仪零偏没校准好后期无论怎么调PID都稳不下来。4.4 姿态角解算与互补滤波俯仰角Pitch和横滚角Roll可以先用加速度计算一版静态角度。常见的公式是float pitch_acc atan2(-raw-ay, raw-az) * 57.29578f; float roll_acc atan2(raw-ax, raw-az) * 57.29578f;注意一点这里的符号和轴方向直接取决于IMU601在板子上的安装方向。如果传感器旋转90度安装公式里的符号就要相应调整不能全套照搬。加速度计优点是长期稳定缺点是对振动敏感动态下噪声大陀螺仪优点是动态响应快缺点是有零偏和积分漂移。互补滤波就是把两者按比例融合。#define FILTER_K 0.98f #define SAMPLE_DT 0.008f float pitch_angle 0.0f, roll_angle 0.0f; void IMU601_UpdateAngle(void) { imu601_raw_t raw; IMU601_ReadRaw(raw); float gx (float)(raw.gx - gx_offset) / 65.5f; float gy (float)(raw.gy - gy_offset) / 65.5f; float pitch_acc atan2(-raw.ay, raw.az) * 57.29578f; float roll_acc atan2(raw.ax, raw.az) * 57.29578f; pitch_angle FILTER_K * (pitch_angle gy * SAMPLE_DT) (1.0f - FILTER_K) * pitch_acc; roll_angle FILTER_K * (roll_angle gx * SAMPLE_DT) (1.0f - FILTER_K) * roll_acc; }0.98这个系数表示陀螺仪权重占98%加速度计占2%。系数越大越信任陀螺仪角度越平滑但跟踪越慢系数越小越信任加速度计响应越快但噪声越大。另外这里的SAMPLE_DT固定写为8ms只适合在采样率稳定为125Hz时使用。更稳妥的做法是用定时器或者系统节拍计算每次读取的实际时间差否则程序里稍有阻塞滤波结果就会因为时间不准确而失真。5. 常见问题排查与避坑记录5.1 I2C总线忙或者卡死的排查顺序硬件I2C调试过程中最烦人的问题就是卡在while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY))。正常的排查顺序是第一测SCL和SDA的静态电平正常应该被上拉到高电平第二确认模块有没有自带上拉电阻如果没有就外接4.7k第三检查用电情况杜邦线接触不良或者模块供电不稳会出现偶发卡死第四如果以上都没问题则把400kHz降成100kHz再试。如果时间紧还有一个绕过问题的方法就是干脆用GPIO模拟I2C。标准库下模拟I2C只有几十行代码时序完全由软件控制出问题更好定位。很多产品代码反而更喜欢模拟I2C因为不依赖硬件外设状态机。5.2 读到全0x00或者全0xFF这两种现象的原因不一样。全0x00通常表示主控发出了地址但从机没有正确应答之后总线被拉死。常见原因是设备地址不对或者AD0引脚接错。全0xFF通常是SDA信号根本没传出去多半是引脚配置成了推挽复用而不是开漏复用或者模块没有供电。在调试时可以读取0x75寄存器也就是WHO_AM_I正确值一般返回0x68。如果读出来是这个值说明底层I2C是通的问题只出在寄存器配置或数据解析上。这个判断方法非常高效我每次换新IMU模块都会先读它。5.3 角度值跳变和漂移角度跳变先看是不是加速度计原始值里混了很多毛刺。此时把DLPF截止频率调低一点比如把CONFIG寄存器的0x04改成0x06截止频率会从约21Hz降到约5Hz噪声会明显改善代价是角度响应变慢。至于漂移主要查两个方面陀螺仪零偏没校准或者SAMPLE_DT和真实采样周期不一致。这两条排查完绝大多数漂移都能解决。5.4 标准库编译报错速查报错现象常见原因解决办法找不到stm32f10x.h头文件路径没加在Include Path里加标准库目录大量函数无定义没有定义USE_STDPERIPH_DRIVER在宏定义里加上RCC_APB2Periph_GPIOB未声明芯片宏选错定义STM32F10X_MD程序能烧录但运行异常启动文件选错换startup_stm32f10x_md.s延时时间差一倍晶振频率和PLL配置不匹配修改system_stm32f10x.c6. 实测结果与个人体会6.1 实测数据看什么把这段代码烧进板子模块水平放置时三轴加速度接近(0, 0, 1g)这从原始LSB上看就是x轴和y轴都接近0z轴接近16000左右。陀螺仪三轴静态零偏我手头这块模块大概在20到40个LSB之间经过500次采样平均后解算出来的Pitch和Roll能稳定在0.1度以内。用手旋转模块角度能快速跟随停止后基本不漂。如果你手头有串口屏或者虚拟示波器建议把滤波前的原始数据和滤波后的角度一起打出来。这样能直观看到振动带来的噪声到底有多大、低通滤波到底压制了多少。没有可视化工具的时候很多问题只能靠猜有了图形界面接线对不对、配置对不对、滤波参数合不合理一眼就能判断。6.2 给后来者的几个小建议我自己的体会是标准库驱动IMU601真的不像很多人想的那么神秘核心就是I2C读写加寄存器配置。但我建议第一次做的朋友不要直接上400kHz先用100kHz把整个流程跑通再逐步提速。另外代码里凡是涉及设备地址的地方最好全部用宏定义统一管理比如#define IMU601_ADDR 0x68这样以后换模块或者改成I2C2时只需要改一处不用翻遍代码去改裸数字。还有一个小技巧调试阶段把原始数据和换算后的数据都通过串口发到上位机固定格式打印出来比如ax, ay, az, gx, gy, gz。这样观察数据能直接看出是底层问题还是算法问题。等到平台稳定了再决定是否去掉打印语句。我就是靠这个方式快速定位了最开始寄存器配置写错顺序的问题。希望这套代码和排查思路能帮你在IMU601上少走几个弯路。本文还有配套的精品资源点击获取
返回列表