简介:面向嵌入式开发者的MPU9250移植实战例程,聚焦把STM32F4平台上的九轴传感器驱动迁移到资源更紧张的STM32F1,解决低成本姿态检测方案中的代码复用与平台适配问题。压缩包共219个文件、约4.48MB,包含68个头文件、46个C源文件及Keil工程配置、编译中间文件与烧录产物,覆盖GPIO/I²C通信、时钟与外设初始化、MPU9250及DMP运动驱动、MPL数据处理等模块,便于对照F4的Cortex-M4F与F1的Cortex-M3差异重构工程,并留意F1的RAM与Flash容量限制。已有2143人学习下载,说明其在传感器移植与课程设计场景中具备一定参考价值。读者可从中获得I/O映射、中断向量、内存占用、库函数替换、通信时序与电源管理等关键问题的处理思路,并借助完整工程快速验证加速度、角速度与磁力计数据采集,适合具备STM32基础、需要做低成本姿态检测或导航应用的中级开发者。
1. 从 F4 搬到 F1,MPU9250 例程最先崩的往往不是传感器
某开发者把一块 F4 板子上跑通的 MPU9250 源码例程搬到 F1 板子,编译零警告,下载后串口只打印一行就不动了。拔掉传感器,程序能跑;接上传感器,卡在 I2C 等待循环。这类现场我见过太多次,问题通常不在 MPU9250 本身,而在 F1 和 F4 的总线时钟、AFIO 重映射、HAL 与标准库接口、EXTI 配置差异。下面围绕“MPU9250 从 STM32F4 移植到 STM32F1 的源码例程”拆开讲:先让总线活过来,再让初始化和数据读取跑通,最后把校准和验证做完。适合手里有一份 F4 例程、想在 F1 上复现加速度、陀螺和磁力计数据的开发者。
2. 总线与时钟先对齐:F1 的 I2C/SPI 和 F4 差在哪
移植第一刀应该砍在总线。MPU9250 支持 I2C 和 SPI,F4 例程常用 I2C,因为接线少、调试直观。F1 的 I2C 外设更老,时序寄存器、GPIO 复用、AFIO 重映射都和 F4 不一样。先别改传感器驱动,先把“读一个寄存器”的函数跑通,后面初始化、磁力计、中断才有意义。总线没通就调 DMP 或滤波,等于在黑匣子里猜。
2.1 为什么 F4 例程里的 while 等待在 F1 上会死循环
F4 的 HAL 库在 I2C 传输里带超时,调用失败会返回HAL_TIMEOUT,程序还能往下走。很多 F1 例程用标准库,写法是while(!I2C_CheckEvent(...)),没有超时。F1 的 I2CBUSY位在异常复位、热插拔、从机把 SDA 拉低之后,可能一直保持 1,外设不释放,循环就死在那里。现场表现是:主循环不跑,串口不再打印,仿真器暂停后看到程序停在I2C_CheckEvent或I2C_GetFlagStatus里。
第二个常见原因是引脚重映射。F1 的 I2C1 默认在 PB6/PB7,换到 PB8/PB9 必须打开 AFIO 重映射;F4 用GPIO_AF4_I2C1这类复用编号,代码直接抄过来,F1 上引脚根本没有接到 I2C 外设。还有 APB1 频率不同:F4 常见 42MHz 或 45MHz,F1 常见 36MHz,CCR和TRISE沿用 F4 的值,SCL 频率偏快或偏慢,轻则偶发 NACK,重则从机完全不响应。把这三件事分开查,比反复换传感器模块有效。
2.2 三个必须先改的时钟与引脚参数
第一步看 RCC。F1 上 I2C1 挂在 APB1,GPIOB 和 AFIO 挂在 APB2,三个时钟都要开;F4 例程里往往只开了 GPIOB 和 I2C1,AFIO 时钟在 F1 上是漏项。第二步看 GPIO 模式。I2C 引脚要配成GPIO_Mode_AF_OD,速度 50MHz,外部上拉 4.7k 到 3.3V;如果模块自带上拉,也要确认上拉电压和 F1 的 IO 电平一致。第三步看 I2C 时序。先把速度降到 100kHz,别一上来就 400kHz。
| 参数 | F4 例程常见写法 | F1 要检查的点 | 建议值 |
|---|---|---|---|
| I2C 时钟源 | APB1 42MHz 或 45MHz | APB1 常见 36MHz,CCR/TRISE要重算 | 先按 36MHz 算 |
标准模式CCR | 直接沿用 F4 | CCR = PCLK1 / (2 * SCL) | 36MHz、100kHz 时约 180 |
TRISE | 直接沿用 F4 | 标准模式约PCLK1/1MHz + 1 | 约 37 |
| GPIO 复用 | GPIO_AF4_I2C1 | F1 用 AFIO 重映射,PB8/PB9 要开 remap | 查GPIO_PinRemapConfig |
| 从机地址 | 7 位地址 0x68 | 标准库传地址时要左移,HAL 也常要左移 | 0x68 << 1 |
| 上拉电阻 | 模块自带或外部 4.7k | 量 SCL/SDA 空闲电平 | 3.3V 上拉 |
注意:F1 的 I2C
BUSY位卡死时,先断电重新上电,再在初始化里加“GPIO 模拟时钟脉冲恢复总线”的兜底。不要只靠I2C_DeInit,有些板子上外设复位后引脚仍被从机拉低。
2.3 最小可跑的寄存器读写代码
下面这段只做一件事:初始化 100kHz I2C,读 MPU9250 的WHO_AM_I。代码按 F1 标准库风格写,HAL 用户把底层换成HAL_I2C_Mem_Read/Write也能用,但地址左移和超时逻辑要保留。
/* 返回值:0 成功,-1 超时。timeout 用递减计数,避免死等。 */ static int i2c_wait(I2C_TypeDef *I2Cx, uint32_t event, uint32_t timeout) { while (!I2C_CheckEvent(I2Cx, event)) { if (--timeout == 0) return -1; } return 0; } void mpu_i2c_init(void) { GPIO_InitTypeDef gpio; I2C_InitTypeDef i2c; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); /* 用 PB8/PB9 时必须打开重映射;用 PB6/PB7 则注释掉这一行 */ GPIO_PinRemapConfig(GPIO_Remap_I2C1, ENABLE); gpio.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9; gpio.GPIO_Mode = GPIO_Mode_AF_OD; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &gpio); i2c.I2C_Mode = I2C_Mode_I2C; i2c.I2C_DutyCycle = I2C_DutyCycle_2; i2c.I2C_OwnAddress1 = 0x00; i2c.I2C_Ack = I2C_Ack_Enable; i2c.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; i2c.I2C_ClockSpeed = 100000; /* 先 100kHz,稳定后再试 400kHz */ I2C_Init(I2C1, &i2c); I2C_Cmd(I2C1, ENABLE); } int mpu_write_reg(uint8_t reg, uint8_t val) { uint32_t timeout = 100000; I2C_GenerateSTART(I2C1, ENABLE); if (i2c_wait(I2C1, I2C_EVENT_MASTER_MODE_SELECT, timeout)) return -1; I2C_Send7bitAddress(I2C1, 0x68 << 1, I2C_Direction_Transmitter); if (i2c_wait(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED, timeout)) return -1; I2C_SendData(I2C1, reg); if (i2c_wait(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING, timeout)) return -1; I2C_SendData(I2C1, val); if (i2c_wait(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED, timeout)) return -1; I2C_GenerateSTOP(I2C1, ENABLE); return 0; } int mpu_read_regs(uint8_t reg, uint8_t *buf, uint16_t len) { uint32_t timeout = 100000; I2C_GenerateSTART(I2C1, ENABLE); if (i2c_wait(I2C1, I2C_EVENT_MASTER_MODE_SELECT, timeout)) return -1; I2C_Send7bitAddress(I2C1, 0x68 << 1, I2C_Direction_Transmitter); if (i2c_wait(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED, timeout)) return -1; I2C_SendData(I2C1, reg); if (i2c_wait(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED, timeout)) return -1; /* 重复起始,切到接收方向 */ I2C_GenerateSTART(I2C1, ENABLE); if (i2c_wait(I2C1, I2C_EVENT_MASTER_MODE_SELECT, timeout)) return -1; I2C_Send7bitAddress(I2C1, 0x68 << 1, I2C_Direction_Receiver); if (i2c_wait(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED, timeout)) return -1; while (len) { if (len == 1) { I2C_AcknowledgeConfig(I2C1, DISABLE); I2C_GenerateSTOP(I2C1, ENABLE); } if (i2c_wait(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED, timeout)) return -1; *buf++ = I2C_ReceiveData(I2C1); len--; } I2C_AcknowledgeConfig(I2C1, ENABLE); return 0; }逻辑说明:i2c_wait把“等事件”变成带超时的轮询,任何一步失败都返回 -1,主程序可以打印错误码,而不是卡死。mpu_read_regs先用写方向发寄存器地址,再发重复起始并切到读方向,最后一个字节前关 ACK 并生成 STOP,这是 F1 标准库读多字节的常规写法。参数说明:timeout取 100000 是经验值,主频越低可以适当加大;0x68 << 1是 7 位地址左移,具体模块如果是 0x69,改成0x69 << 1;I2C_ClockSpeed先固定 100000,等WHO_AM_I能稳定读回 0x71 再考虑提速。读回值打印成十六进制,如果得到 0x00 或 0xFF,先查上拉、地址和重映射,不要急着换芯片。
3. MPU9250 初始化移植:从 HAL 调用到寄存器时序
总线通了之后,第二刀砍在初始化。F4 例程里经常能看到HAL_I2C_Mem_Write直接写寄存器,F1 上可能换成标准库,也可能继续用 HAL,但传感器驱动里到处是 HAL 句柄,移植时改一处漏一处。更稳的做法是先把“写寄存器、读寄存器”抽成一层接口,让 MPU9250 的初始化逻辑只依赖接口,不依赖具体库。这样 F1 用标准库、F4 用 HAL、甚至临时用 GPIO 模拟 I2C,都不需要重写初始化表。
3.1 把读写接口抽成一层,别让传感器代码绑死 HAL
接口不需要复杂,两个函数指针就够:一个写单寄存器,一个读连续寄存器。MPU9250 的初始化、量程配置、磁力计旁路、数据读取都走这两个函数。F1 上标准库实现可以复用第 2 章的mpu_write_reg和mpu_read_regs;F4 上 HAL 实现可以用HAL_I2C_Mem_Write和HAL_I2C_Mem_Read。接口层只负责地址左移、超时和返回值,传感器层不关心底层是哪个库。
typedef struct { int (*write)(uint8_t reg, uint8_t val); int (*read)(uint8_t reg, uint8_t *buf, uint16_t len); } mpu_bus_t; static mpu_bus_t bus; int mpu_init(mpu_bus_t *b) { uint8_t who = 0; bus = *b; /* 绑定总线实现 */ if (bus.write(0x6B, 0x80) != 0) /* PWR_MGMT_1:复位 */ return -1; delay_ms(100); /* 复位后等 100ms,别省 */ if (bus.read(0x75, &who, 1) != 0) /* WHO_AM_I */ return -2; if (who != 0x71) /* MPU9250 的 WHO_AM_I 是 0x71 */ return -3; bus.write(0x6B, 0x01); /* 清睡眠,选 PLL 时钟源 */ bus.write(0x19, 0x04); /* SMPLRT_DIV:1kHz/(1+4)=200Hz */ bus.write(0x1A, 0x03); /* CONFIG:DLPF,按手册表查 */ bus.write(0x1B, 0x00); /* GYRO_CONFIG:±250 dps */ bus.write(0x1C, 0x00); /* ACCEL_CONFIG:±2g */ bus.write(0x1D, 0x03); /* ACCEL_CONFIG2:加速度低通 */ bus.write(0x37, 0x02); /* INT_PIN_CFG:打开旁路,AK8963 可见 */ bus.write(0x38, 0x01); /* INT_ENABLE:数据就绪中断 */ return 0; }逻辑说明:先复位再读WHO_AM_I,是为了排除芯片还在睡眠或 I2C 状态机没复位的情况。复位后延时 100ms 是保守值,F1 主频低也不会出问题。PWR_MGMT_1写 0x01 表示清睡眠并选择 PLL,很多“初始化能过但数据不动”的现场就是只写了 0x00,芯片仍用内部振荡器,数据更新很慢。参数说明:SMPLRT_DIV的 0x04 对应 200Hz,改采样率就改这个值;CONFIG的 0x03 是低通滤波档位,具体截止频率查手册表,不要照搬别的例程;GYRO_CONFIG和ACCEL_CONFIG的 0x00 分别是 ±250dps 和 ±2g,灵敏度常用 131 LSB/(°/s) 和 16384 LSB/g,换量程后换算系数也要换。返回值 -1/-2/-3 分别对应复位写失败、读 ID 失败、ID 不匹配,串口打印出来能快速定位。
3.2 WHO_AM_I、复位与量程配置的顺序
初始化顺序不要随意调。常见可靠顺序是:上电延时、复位、读WHO_AM_I、清睡眠并选时钟、配置采样率和低通、配置陀螺和加速度量程、配置中断引脚、开数据就绪中断。每一步写完后建议读回寄存器,尤其是PWR_MGMT_1和INT_PIN_CFG。F1 的 I2C 在 100kHz 下写寄存器通常没问题,但读回能发现“地址左移错了但写操作碰巧没报错”的情况。
| 步骤 | 寄存器 | 值 | 作用 | 检查点 |
|---|---|---|---|---|
| 1 | 0x6B | 0x80 | 复位 | 延时 100ms |
| 2 | 0x75 | 只读 | 读 WHO_AM_I | 应为 0x71 |
| 3 | 0x6B | 0x01 | 清睡眠、选 PLL | 读回应为 0x01 |
| 4 | 0x19 | 0x04 | 采样率分频 | 200Hz 左右 |
| 5 | 0x1A | 0x03 | 低通滤波 | 按手册查截止频率 |
| 6 | 0x1B | 0x00 | 陀螺 ±250dps | 灵敏度 131 LSB/(°/s) |
| 7 | 0x1C | 0x00 | 加速度 ±2g | 灵敏度 16384 LSB/g |
| 8 | 0x37 | 0x02 | 旁路模式 | 磁力计可见 |
| 9 | 0x38 | 0x01 | 数据就绪中断 | 中断引脚有动作 |
注意:
WHO_AM_I读回 0x71 只说明 I2C 通了,不代表配置生效。复位后马上读 ID 有时会读到旧值,延时不够就误判。我的习惯是复位后延时 100ms,再读两次 ID,两次一致才继续。
3.3 AK8963 磁力计旁路模式在 F1 上的初始化
MPU9250 内部带一个 AK8963 磁力计,但它不在 MPU9250 的寄存器地址空间里直接暴露。常见做法是打开 MPU9250 的旁路模式,让主 I2C 总线直接看到 AK8963 的 7 位地址 0x0C。INT_PIN_CFG的 bit1 写 1 就是旁路使能,对应值 0x02。旁路打开后,F1 的主 I2C 可以直接读 AK8963 的WHO_AM_I(寄存器 0x00),正常返回 0x48。如果读不到,先确认 MPU9250 的USER_CTRL里 I2C master 没有抢总线,再确认旁路位真的写进去了。
AK8963 初始化顺序是:先写CNTL1掉电,再进 Fuse ROM 访问模式读 ASA 调整值,再回掉电,最后写连续测量模式。连续测量模式 2 对应CNTL1=0x16,表示 16 位输出、100Hz 连续测量。读数据时从 0x03 开始连续读 7 个字节,包含 HXL 到 ST2;最后一个 ST2 必须读,否则 AK8963 不会开始下一次测量。这个细节在 F4 例程里经常被封装掉了,搬到 F1 上如果只读 6 个字节,磁力计数据会停更。
/* 这里的 ak_write/ak_read 是直接访问 0x0C 的底层函数, 不是 MPU9250 的寄存器读写。旁路模式打开后才能用。 */ int ak8963_init(void) { uint8_t id = 0, asa[3]; if (ak_read(0x00, &id, 1) != 0) return -1; /* AK8963 WHO_AM_I */ if (id != 0x48) return -2; ak_write(0x0A, 0x00); /* CNTL1:先掉电 */ delay_ms(10); ak_write(0x0A, 0x0F); /* Fuse ROM 访问模式 */ delay_ms(10); ak_read(0x10, asa, 3); /* 读 ASA 调整值,后续做磁力计校准用 */ ak_write(0x0A, 0x00); /* 回掉电 */ delay_ms(10); ak_write(0x0A, 0x16); /* 16 位输出,连续测量模式 2 */ delay_ms(10); return 0; } int ak8963_read(int16_t mag[3]) { uint8_t buf[7]; /* 0x03~0x09:HXL/HXH/HYL/HYH/HZL/HZH/ST2 */ if (ak_read(0x03, buf, 7) != 0) return -1; mag[0] = (int16_t)((buf[1] << 8) | buf[0]); mag[1] = (int16_t)((buf[3] << 8) | buf[2]); mag[2] = (int16_t)((buf[5] << 8) | buf[4]); return 0; }逻辑说明:ak_read(0x00, ...)读的是 AK8963 的 ID,不是 MPU9250 的
本文还有配套的精品资源,点击获取