尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32F4通过SPI读取ICM20648六轴IMU的完整实现与调试

STM32F4通过SPI读取ICM20648六轴IMU的完整实现与调试 简介面向STM32F4嵌入式开发者的一套ICM-20648六轴IMU驱动工程聚焦通过SPI接口完成传感器通信与原始数据读取。工程适用于无人机、机器人、可穿戴等运动检测场景也适合刚接触SPI协议或惯性传感器的学习者对照参考。压缩包共379个文件约19.25MB以C和H源码为主体配齐Keil工程文件、链接脚本以及axf/hex/map编译输出便于直接打开工程查看或重新编译。另有HAL库相关驱动、测试配置和调试信息文件可辅助理解stm32f4xx_hal_i2c、tim、cryp等外设的工程组织方式。目前已有609人浏览学习。包内含完整的SPI引脚与寄存器配置、ICM-20648加速度与陀螺仪数据读取示例以及工程模板和排错参考从初始化到读取数据均有代码支撑可快速移植到实际项目中。1. 不用 I2C 的六轴方案STM32F4 走 SPI 读 ICM20648 到底好在哪做姿态采集第一反应是 I2C 接线省两根线但真到了需要把陀螺仪数据喂给滤波算法、或者采样率推到 1kHz 以上的场景I2C 的 400kHz 标准模式就会变成瓶颈。ICM20648 这颗六轴 IMU 同时开放了 I2C 和 SPI 接口手册上 SPI 时钟最高能跑到 24MHz是 I2C 的几十倍而且 SPI 全双工机制天然适合先发寄存器地址、后收数据这类传感器读操作。这个项目包给的就是一条完整的 STM32F4 SPI 读取 ICM20648 的 HAL 库工程包含 Keil 工程文件、标准外设库文件和一个清理脚本。它已经把 WHO_AM_I 校验、寄存器初始化、六轴数据突发读取这几件最关键的事跑通了适合正在从 MPU6050 迁移过来的开发者也适合项目里 I2C 总线已经挂满设备、需要把 IMU 独立到一条 SPI 总线上的情况。拿到工程后可以直接改引脚配置接入自己的板子不需要再从零翻数据手册。2. SPI 时序与通信帧设计ICM20648 与 MPU6500 系寄存器读写的关键差异2.1 SPI 模式选择和 STM32F4 硬件资源映射ICM20648 支持标准 4 线 SPI信号线为 SCLK、MOSI、MISO、CS。数据手册要求 SPI 模式为 0 或 3即 CPOL0/CPHA0 或 CPOL1/CPHA1这两种模式下数据都在时钟边沿稳定采样。STM32F4 的 SPI 外设通过配置寄存器可以覆盖全部四种模式下面这张表是四种模式与 ICM20648 的匹配情况模式 0 和模式 3 都可以用但工程里配置成模式 0 的居多因为大多数逻辑分析仪抓时序时默认按模式 0 解析。SPI 模式CPOLCPHA数据采样边沿与 ICM20648 匹配Mode 000上升沿匹配Mode 101下降沿不匹配Mode 210下降沿不匹配Mode 311上升沿匹配STM32F4 上有多个 SPI 外设工程默认使用的是 SPI1对应引脚为 PA5SCK、PA6MISO、PA7MOSI、PA4CS。选择 SPI1 的理由很直接它挂在 APB2 总线上时钟频率比 APB1 上的 SPI2/SPI3 高一倍理论上可以跑到更高的 SPI 时钟。虽然 ICM20648 支持 24MHz但实际布线如果用了杜邦线建议先降到 10.5MHz 以下跑通功能再往上提这个点后面调试章节会展开。2.2 寄存器寻址的两种帧格式ICM20648 的数据手册定义了一组和其他 MPU 系列不太一样的帧结构。写单个寄存器时帧格式是 1 字节寄存器地址 1 字节数据没有额外的命令位。读操作则要复杂一些第一个字节的高 7 位是寄存器地址最低位是读标志位置 1 表示读第二个字节的高 7 位是突发读取的长度最低位是突发读取使能标志。这就意味着在发送读地址之后、读取数据之前主机需要额外发送一个长度字节很多从 MPU6050 迁移过来的代码在这里会踩坑。下面是工程里实际使用的写寄存器函数走的是 HAL 库标准接口uint8_t ICM20648_WriteReg(uint8_t reg, uint8_t data) { uint8_t tx_buf[2]; uint8_t rx_buf[2]; tx_buf[0] reg; tx_buf[1] data; HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_TransmitReceive(hspi1, tx_buf, rx_buf, 2, 10); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); return 0; }这段代码的逻辑是先把片选拉低然后用HAL_SPI_TransmitReceive以全双工方式一次性发出寄存器地址和数据字节。使用TransmitReceive而不是Transmit的原因是 SPI 是移位寄存器结构主机发送的同时也在接收从机移位出来的数据全双工一次调用就能完成总线交换避免收发分离可能导致的时钟间隙。第二个参数10是超时时间单位是毫秒正常情况下整个传输在微秒级完成这个值只要给到能覆盖最慢的 SPI 时钟即可不必过大。2.3 突发读帧与 CS 低电平保持读六轴数据是性能关键路径绝不能一个寄存器一个寄存器地读那样每次都要重新拉低 CS、发地址、等响应时序开销全部浪费在帧间隙上。正确做法是一次 CS 周期内完成所有寄存器的连续读取。ICM20648 的数据寄存器从ACCEL_XOUT_H0x2D开始到GYRO_ZOUT_L0x36结束共 14 个字节布局上做了连续编址就是为了支持突发读。uint8_t ICM20648_ReadBytes(uint8_t reg, uint8_t *buf, uint8_t len) { uint8_t addr (reg 1) | 0x80; // 最高位为1表示读操作 uint8_t len_byte ((len - 1) 1) | 0x01; // 高7位为长度-1bit0为使能位 HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(hspi1, addr, 1, 10); HAL_SPI_Transmit(hspi1, len_byte, 1, 10); HAL_SPI_Receive(hspi1, buf, len, 10); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); return 0; }这里addr的计算方式是(reg 1) | 0x80因为 ICM20648 的读地址是把寄存器地址左移一位、最低位补 1这和 MPU6050 直接地址加 0x80的做法不同是最容易写错的地方。len_byte的规则也是左移一位后最低位置 1表示使能突发模式高 7 位是len - 1比如读 14 字节就是(13 1) | 0x01 0x1B。整个过程中 CS 保持低电平SPI 从机在此期间持续按时钟输出数据主机侧HAL_SPI_Receive接收的每一字节就是当前地址递增后的寄存器内容。3. 初始化流程与关键寄存器配置从 WHO_AM_I 到量程滤波的完整链路3.1 软复位和基本校验ICM20648 上电后寄存器处于默认状态但保险起见第一步还是做一次软复位。电源管理寄存器 1 的地址是 0x06bit 0 是设备复位位写 1 触发复位后芯片会回到上电默认状态。需要注意复位操作之后该位会自动清零不需要手动再写一遍。等待至少 50ms 让内部稳压器稳定然后读取 WHO_AM_I 寄存器0x00确认 SPI 链路通没通。uint8_t ICM20648_Init(void) { uint8_t who_am_i 0; // 软复位 ICM20648_WriteReg(0x06, 0x01); HAL_Delay(100); // 读取芯片IDICM20648固定返回0xE0 ICM20648_ReadBytes(0x00, who_am_i, 1); if (who_am_i ! 0xE0) { return 1; // 返回失败SPI链路或芯片供电有问题 } return 0; }WHO_AM_I 固定返回 0xE0如果读到这个值说明 SPI 的 MOSI、MISO、SCK、CS 四根线都正确且芯片供电正常。读到 0xFF 通常是 MISO 没接好或者 CS 极性配反读到 0x00 则基本可以断定 MOSI 方向有问题。这里有个细节读 WHO_AM_I 虽然只有一个字节仍然要按突发读格式发长度字节即先发0x80再发0x01然后收数据不能省掉中间那个长度字节。3.2 电源管理和传感器使能复位之后关键的一步是配置电源管理寄存器 20x07这个寄存器控制加速度计和陀螺仪各个通道的使能状态。默认值会把所有传感器都置于待机模式如果直接读数据会得到全零。下面是配置代码// 配置电源管理寄存器2bit0是陀螺仪待机bit1是陀螺仪禁用 // bit2是加速度计待机bit3是加速度计禁用 // 写入0x00表示所有传感器正常工作 ICM20648_WriteReg(0x07, 0x00);这里容易忽略的是待机和禁用是两回事待机模式下传感器不采样但寄存器还能访问禁用模式则连寄存器访问都可能异常。有些例程只清了禁用位没有清待机位导致陀螺仪输出一直为零。另外还需要确认寄存器 0x06 的 bit 6 是低电平该位为 1 时强制进入休眠模式软复位之后不会自动变回 0需要检查或者直接再写一次 0x00 覆盖掉。3.3 量程、采样率和数字滤波配置传感器量程决定了原始值的缩放比例。ICM20648 的陀螺仪可选 ±250、±500、±1000、±2000 dps 四档加速度计可选 ±2、±4、±8、±16 g 四档。工程里按默认配置走即陀螺仪 ±2000 dps、加速度计 ±16 g这两个量程是分辨率最低但测量范围最大的配置适合前期验证阶段防止数据溢出。配置寄存器分别为陀螺仪配置0x14和加速度计配置0x15。采样率由寄存器 0x13 控制写法是1 该寄存器值单位 kHz。写入 0x07 得到 1kHz 内部采样率这个频率下陀螺仪和加速度计的原始数据都以 1kHz 速率更新。注意这个采样率和 SPI 读取频率不是一回事SPI 可以按照自己的节奏去读最新的数据但如果 SPI 时钟太慢读不过来传感器内部 FIFO 会覆盖旧数据。数字低通滤波配置在寄存器 0x14 的低三位以陀螺仪为例DLPF 配置为 2 时通带带宽约 92Hz这个值对于姿态解算场景是够用的能滤掉大部分机械振动噪声。也可以配置成 0 关闭滤波获得最大带宽代价是输出数据中会混入高频噪声后续算法需要自己做低通处理。下面是一个完整的初始化序列uint8_t ICM20648_Init_Full(void) { uint8_t who_am_i 0; uint8_t config[][2] { {0x06, 0x00}, // 电源管理1退出休眠 {0x07, 0x00}, // 电源管理2全部传感器使能 {0x13, 0x07}, // 采样率1kHz {0x14, 0x02}, // 陀螺仪±2000dpsDLPF 92Hz {0x15, 0x02}, // 加速度计±16gDLPF 92Hz {0x16, 0x00}, // 加速度计量程配置 {0x17, 0x00} // 加速度计数字滤波配置 }; uint8_t i; ICM20648_WriteReg(0x06, 0x01); // 软复位 HAL_Delay(100); ICM20648_ReadBytes(0x00, who_am_i, 1); if (who_am_i ! 0xE0) { return 1; } for (i 0; i sizeof(config) / 2; i) { ICM20648_WriteReg(config[i][0], config[i][1]); } return 0; }这段初始化代码的逻辑是先在软复位后重新写一次电源管理寄存器 1确保休眠位被清掉然后再配置采样率和滤波。寄存器 0x15 的低四位同时包含量程和滤波模式0x02对应 ±16g 和 DLPF 开启。如果后续姿态解算需要更高分辨率把这两个寄存器改成0x00对应 ±250dps 和 ±2g即可注意量程变化后换算灵敏度也要同步修改。4. 六轴原始数据读取与换算14 字节突发读和量程灵敏度对照4.1 加速度计和陀螺仪寄存器布局ICM20648 的六轴数据寄存器在地址上做了线性排列0x2D 到 0x30 是加速度计三个轴的 X、Y、Z 高八位和低八位0x33 到 0x36 是陀螺仪三个轴的对应数据0x31 和 0x32 是温度传感器数据。读的时候直接从 0x2D 开始连续读 14 字节把温度也一起带出来避免下一次重复开启 CS。寄存器分布如表中所示地址内容地址内容0x2DACCEL_XOUT_H0x32TEMP_OUT_L0x2EACCEL_XOUT_L0x33GYRO_XOUT_H0x2FACCEL_YOUT_H0x34GYRO_XOUT_L0x30ACCEL_YOUT_L0x35GYRO_YOUT_H0x31ACCEL_ZOUT_H0x36GYRO_YOUT_L这里埋了一个小坑加速度计的 Z 轴高字节是 0x31而 0x32 是温度低字节也就是说温度寄存器夹在加速度计和陀螺仪之间。如果只读 12 字节只覆盖加速度计和陀螺仪读到的数据会错位因为长度字节设置为14 - 1才能保证从 0x33 开始读到陀螺仪 X 轴。4.2 完整读取代码和字节拼装typedef struct { int16_t accel_x; int16_t accel_y; int16_t accel_z; int16_t temp; int16_t gyro_x; int16_t gyro_y; int16_t gyro_z; } imu_raw_t; void ICM20648_ReadAll(imu_raw_t *imu) { uint8_t buf[14]; ICM20648_ReadBytes(0x2D, buf, 14); imu-accel_x (int16_t)((buf[0] 8) | buf[1]); imu-accel_y (int16_t)((buf[2] 8) | buf[3]); imu-accel_z (int16_t)((buf[4] 8) | buf[5]); imu-temp (int16_t)((buf[6] 8) | buf[7]); imu-gyro_x (int16_t)((buf[8] 8) | buf[9]); imu-gyro_y (int16_t)((buf[10] 8) | buf[11]); imu-gyro_z (int16_t)((buf[12] 8) | buf[13]); }注意这里int16_t的转换是必须的。比如加速度计 X 轴原始值0xFFFF表示满量程负向如果直接赋给uint16_t后续计算时会得到 65535 而不是 -1所有换算全部失效。用(int16_t)强制转换后高字节左移 8 位和低字节相或符号位被保留在最高位补码表示天然正确。温度值的换算是温度 原始值 / 333.87 21单位摄氏度这个值只做参考不能用来校准系统时钟。4.3 原始值转物理量的灵敏度和实际验证方法六轴原始数据本身是整型要和物理世界对应必须乘灵敏度系数。这个系数取决于第 3 章配置的量程工程默认配置下的灵敏度见下表数据类型量程灵敏度满量程输出加速度计±16g2048 LSB/g±32768加速度计±2g16384 LSB/g±32768陀螺仪±2000dps16.4 LSB/(°/s)±32768陀螺仪±250dps131 LSB/(°/s)±32768对应工程默认配置的换算代码如下float accel_x_g (float)imu.accel_x / 2048.0f; float gyro_x_dps (float)imu.gyro_x / 16.4f;验证数据是否合理的技巧是让板子静止放在桌面上加速度计 Z 轴应接近 1.0gX 和 Y 轴接近 0g陀螺仪三轴都接近 0dps。如果 Z 轴读数是 -1g 而 X 轴是 0说明芯片的安装朝向和坐标系定义不同这不影响数据正确性但后续姿态解算时需要做坐标轴映射。如果加速度计三轴都有明显偏置而陀螺仪正常先检查是不是供电纹波太大或者板子有振动在排除硬件问题后做零偏校准静止采集 200 个样本取平均作为后续算法中的偏置补偿值。5. 移植到 Cubemx 工程和坏数据排查技巧从 Keil 工程到自建项目的落地方法5.1 资源包内的工程文件说明和 Cubemx 配置对齐资源包里的文件结构包含SPI.uvguix.*工程配置、Template.axf编译输出、keilkilll.bat清理脚本以及一批stm32f4xx_hal_*.c标准库源文件。keilkilll.bat的作用是删除编译产生的中间文件和临时文件方便打包工程时保持目录干净。从stm32f4xx_hal_i2c.c和stm32f4xx_hal_fmpi2c.c的存在可以推测工程模板是从一个同时包含 I2C 和 SPI 库的模板生成的实际只用到了 SPI 相关部分裁剪时可以把多余的.c文件从 Keil 工程中移除缩短编译时间。如果不用现有 Keil 工程而是用 Cubemx 重新生成SPI1 的参数要和这个工程保持一致全双工主机模式、8 位数据结构、MSB 先行、CPOL 为 Low、CPHA 为 1 Edge。CS 引脚配置为普通 GPIO 输出推挽模式、无上拉下拉初始电平设为高。这个配置对应 Cubemx 中 SPI Mode 选择 Full-Duplex Master参数面板里 Clock Polarity 选 LowClock Phase 选 1st Edge。用 Cubemx 自动生成的代码会在MX_SPI1_Init()里完成这些设置和手工初始化效果一致。5.2 SPI 速率上限和信号完整性的判断方法工程里如果直接把 SPI 时钟配置成最高分频可能出现两种情况一种是完全收不到数据另一种是数据偶发错位且表现为某个轴偶尔跳变。前者是时序完全不满足后者是建立保持时间裕量不足数据手册上 ICM20648 在 1.8V 和 3.3V 下的最大 SPI 时钟不同3.3V 供电时 24MHz 可以跑但杜邦线加上板间寄生电容后实际裕量很小。排查步骤如下先用RCC-APB2ENR确认 SPI1 时钟源来自 PLL确保系统时钟已稳定不要在内部 RC 振荡器下测试。将 SPI 波特率预分频配置降为 32 分频或 64 分频此时 SPI 时钟约为 1.05MHz 或 0.52MHz这个速率下任何寄生参数都不至于破坏时序。逐个字节读回 WHO_AM_I反复读取 100 次如果全都返回 0xE0 说明链路稳定再逐步提高分频系数。数据跳变时在中断里读取错误标志在 HAL 回调函数中处理__HAL_SPI_GET_FLAG状态优先查看 MODF、OVR 标志位。// SPI错误标志检查示例 if (__HAL_SPI_GET_FLAG(hspi1, SPI_FLAG_OVR)) { __HAL_SPI_CLEAR_OVRFLAG(hspi1); // 溢出时先清标志再重新发起下一次读取 } ICM20648_ReadAll(imu);这段代码的逻辑是每次读数据前先检查溢出标志如果上一次读操作还没完成而新的数据已经到达OVR 会被置位此时需要先清标志再读。在循环读取的 while(1) 主循环里加上这个检查可以避免数据错位后一直累积错误状态。5.3 基于原始数据的应用延伸和实时性说明ICM20648 的原始数据在 1kHz 内部采样率下可以支撑多种后处理链路。如果是做离线分析把 14 字节原始数据通过串口以二进制帧输出到 PC 端做 FFT 频域分析SPI 的带宽优势就体现出来了SPI 时钟 10.5MHz 时读 14 字节耗时约 13.3 微秒加上 CS 切换开销不到 20 微秒这意味着理论上每秒可以完成 5 万次完整读取实际瓶颈完全在主循环调度和算法处理上SPI 本身的耗时可以忽略。对比 I2C 在 400kHz 下读同样 14 字节需要 280 微秒以上差距是一个数量级。在常规定时器调度中建议把数据读取放在定时器中断中执行中断频率根据实际需求决定常见的做法是 500Hz 读取陀螺仪和加速度计每两次读取之间 2ms 留给姿态解算和其他任务。中断处理函数中只做数据拷贝和标志位置位换算和滤波放到主循环去做避免在中断里做浮点运算。5.4 FIFO 未生效的常见原因ICM20648 的 FIFO 默认是关闭的要在寄存器 0x1A 里单独使能。工程里如果只是按照常规初始化而没有开 FIFO读到的数据就是当前时刻的最新采样值不会累积历史数据。这在低读取频率下会出现数据跳变比如主循环 100Hz 读取而传感器 1kHz 采样两次读取之间传感器已经更新了 9 次读到的总是最新的那次值看起来数据跳得厉害。要平滑数据除了开 FIFO也可以在初始化时把采样率调低到和读取频率一致或者读取频率提高之后做滑动平均滤波。开 FIFO 的方法是把 0x1A 寄存器配置为对应传感器使能位加速度计使能写 0x18陀螺仪使能写 0x0E两者都开写 0x1E。不过开了 FIFO 之后读取流程就变了需要先读 0x3A 的 FIFO 计数寄存器判断有多少字节可用再决定一次性读取还是分批读取复杂度会明显增加。本文还有配套的精品资源点击获取
返回列表