简介:MPU9250从STM32F4移植到F1源码例程,是一份面向嵌入式开发者的完整移植工程,重点解决在资源更有限的STM32F1平台上驱动九轴传感器MPU9250的问题。资料涵盖GPIO映射、中断向量调整、时钟配置、内存优化等关键环节,适合需要将现有F4代码迁移到F1的工程师学习参考。压缩包共含219个文件,大小4.48MB,以.c源文件、.h头文件为主,另附Keil工程文件(.uvprojx)、编译生成文件(.axf、.hex)及调试辅助文件,便于直接对照工程结构理解移植思路。已有2143人学习下载。通过这份例程,读者可掌握从F4到F1的底层差异处理,并获取可复用的驱动代码与配置模板,缩短同类项目的开发周期。
1. 这份资源解决什么:F4能跑的九轴例程,F1板上为什么全乱
很多玩 STM32 的开发者手里都有一块老 F1 板子,比如 103C8T6 这种最小系统板。想跑 MPU9250 九轴传感器,网上搜到的例程十有八九是 F4 的,直接拖进工程、改个芯片型号就用,结果就是串口打印出来的加速度、陀螺仪数据不是全零就是乱跳,磁力计干脆没反应。这份源码例程解决的问题就是:把一套能在 F4 上正常出数的 MPU9250 驱动,完整地移植到 F1 平台,让 I2C 时序、时钟分频、DMP 固件加载都能在 72MHz 主频下稳定工作。适合两类人:一类是手头只有 F1 板子但想调九轴姿态的初学者,另一类是从 F4 换到 F1 做低成本方案、需要快速上手的开发者。资源里带完整工程源码和接线说明,拿过来改个引脚就能出数据。
2. 移植前先看差异:F1 和 F4 的时钟、I2C 与工程结构对比
2.1 F4 例程为什么不能直接烧进 F1
MPU9250 本身的寄存器读写逻辑不分芯片平台,读加速度、读陀螺仪、读磁力计,都是通过 I2C 或 SPI 对寄存器地址做读写。但 F1 和 F4 的外设时钟树差异很大,导致同一份代码在不同平台上的表现完全不同。主流 F4 芯片主频能到 168MHz,APB1 外设时钟是 42MHz;F1 的 103 系列主频最高 72MHz,APB1 只有 36MHz。I2C 外设的时序参数是按外设时钟算出来的,F4 例程里写死的 CCR 分频值拿到 F1 上,I2C 时钟频率直接超限,传感器要么不应答,要么应答错乱。
另一个隐蔽差异是中断向量和启动文件。F4 例程用的是startup_stm32f40x.s,中断向量表偏移和 F1 的startup_stm32f10x.s不同,特别是 SysTick 和 I2C 中断向量位置不一样。直接拿 F4 工程改芯片型号,链接阶段未必报错,但上电后中断服务函数根本进不来,DMP 固件加载流程里需要等 FIFO 中断的步骤会一直卡在while循环里。
还有一点是 GPIO 复用功能的配置方式。标准外设库时代,F4 用GPIO_PinAFConfig配置复用功能,F1 直接GPIO_Mode_AF_OD就能把引脚设为复用开漏。两套库的函数名和参数定义完全不同,代码里这些部分不改,编译是过不去的。下面这个表格总结了移植时最需要关注的差异点:
| 对比项 | STM32F4 | STM32F1 | 移植影响 |
|---|---|---|---|
| 主频 | 168MHz | 72MHz | I2C 分频、延时函数要重算 |
| APB1 时钟 | 42MHz | 36MHz | I2C 时序参数完全不同 |
| 启动文件 | startup_stm32f40x.s | startup_stm32f10x.s | 中断向量偏移不一致 |
| GPIO 复用 | PinAFConfig | 直接复用开漏 | 初始化代码函数不同 |
| 硬件 I2C | 相对稳定 | 有总线锁死风险 | F1 建议用软件 I2C |
2.2 源码目录结构与硬件连接方案
这套例程的目录结构是典型的 F1 标准外设库工程布局,main.c 负责初始化和主循环,mpu9250.c 封装了传感器驱动,i2c_soft.c 实现了软件 I2C 时序,inv_mpu.c 和 dmp 相关文件用于姿态解算。拿到源码包以后,重点看User目录下的 main.c 和mpu9250目录下的驱动文件,这两个地方是移植的核心。
硬件连接是典型的四线 I2C,加上 MPU9250 的 AD0 引脚用来选择 I2C 器件地址。例程里默认用的是 PB8 接 SCL、PB9 接 SDA,这两个引脚在 F1 上可以复用为 I2C1 的硬件接口,但这套例程实际走的是软件模拟 I2C,所以 GPIO 配置成普通开漏输出加外部上拉就行。VCC 接 3.3V,GND 接地,INT 引脚接 PB6,用来给 F1 提供数据就绪中断。
接线本身没什么玄学,但有个细节值得注意:MPU9250 模块上的上拉电阻一般已经焊好了,如果你用的是自己画的板子,SCL 和 SDA 上必须各加一个 4.7kΩ 上拉电阻到 VCC,否则软件 I2C 读出来的数据是随机数。我一般还会在 VCC 和 GND 之间加一个 100nF 的陶瓷电容,给传感器滤掉高频噪声——F1 板子如果用 USB 供电,板上纹波往往比 F4 开发板的 LDO 输出大不少,电源不稳会直接体现成陀螺仪零漂偏大。
2.3 为什么这套例程选择软件 I2C 而不是硬件 I2C
F1 的硬件 I2C 外设出了名的难用。标准外设库的 I2C 驱动里,EV5、EV6、EV7 这几个事件标志的判断顺序稍有偏差,总线就会卡在一个莫名其妙的状态,SCL 拉不低也放不开,必须复位外设甚至断电重启才能恢复。在 MPU9250 这种需要连续读多字节数据的场景里,硬件 I2C 的 NACK 处理和总线错误恢复更是折磨人。F4 的硬件 I2C 改进过,用起来相对省心,F1 平台就果断切软件 I2C,用 GPIO 模拟时序。
软件 I2C 的本质就是延时翻转电平。MPU9250 的标准 I2C 时钟频率是 400kHz,一个周期就是 2.5μs,F1 主频 72MHz 的情况下,一个空循环大约几个纳秒,所以延时函数用微秒级就够。例程里的i2c_delay()用的是 DWT 或 SysTick 做微秒延时,不用HAL_Delay这种毫秒级接口。
软件 I2C 的优势不只是躲开硬件外设的坑,还有一个实际好处:引脚任意。你想把 SCL 换到 PA5、SDA 换到 PA7,只需要改 GPIO 初始化里两个宏定义,不用去翻数据手册查复用功能映射表。对于经常改板子的人来说,这个自由度比那一点性能损耗值得多。MPU9250 的采样率上限是 1kHz,I2C 跑 400kHz 完全够用,软件模拟的性能不会成为瓶颈。
3. 核心代码拆解:从初始化到姿态解算的完整链路
3.1 引脚的宏定义与 GPIO 初始化
移植的第一步改引脚宏定义。例程里在i2c_soft.h中把 SCL 和 SDA 的引脚、端口、时钟全部集中定义成宏,方便换板子时只改一处。这套思路值得沿用,因为 F1 不同型号的引脚兼容性差异比 F4 大,换一个封装就要重新核对引脚复用。
#define I2C_SCL_PORT GPIOB #define I2C_SCL_PIN GPIO_Pin_8 #define I2C_SCL_CLK_ENABLE() RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE) #define I2C_SDA_PORT GPIOB #define I2C_SDA_PIN GPIO_Pin_9 #define I2C_SDA_CLK_ENABLE() RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE)GPIO 初始化的要点是:SCL 和 SDA 都要配置成开漏输出模式。开漏输出的好处是,引脚拉低时是真正的低电平,释放时靠外部上拉电阻回到高电平,这是 I2C 协议要求的电平逻辑。如果配成推挽输出,两个器件同时驱动总线时可能造成短路电流,这在多设备 I2C 总线上是隐患。
void i2c_soft_init(void) { GPIO_InitTypeDef GPIO_InitStructure; I2C_SCL_CLK_ENABLE(); I2C_SDA_CLK_ENABLE(); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Pin = I2C_SCL_PIN; GPIO_Init(I2C_SCL_PORT, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = I2C_SDA_PIN; GPIO_Init(I2C_SDA_PORT, &GPIO_InitStructure); I2C_SCL_HIGH(); I2C_SDA_HIGH(); }这里的GPIO_Speed_50MHz在 F1 上不是必须的,但设成最高速度会让电平翻转沿更陡峭,在总线电容较大的场景下能改善信号质量。初始化完成后把 SCL 和 SDA 都拉高,表示总线处于空闲状态。如果在这里用示波器量到某个引脚一直是低电平,优先检查外部上拉电阻有没有焊,焊了的话检查阻值是否过大——10kΩ 在 400kHz 下可能拉不回来,信号沿变圆导致时序超时。
3.2 I2C 时序实现与超时保护
软件 I2C 的时序核心就是四个函数:起始信号、停止信号、发送字节、接收字节。这套例程实现得比较规整,每个函数都带超时计数,这是从 F4 移植到 F1 时必须加的东西。F1 主频比 F4 低一半以上,如果直接用 while 循环等 SCL 释放、而不加超时,一旦传感器没应答,整个 MCU 就死在这个循环里,中断全废。
uint8_t i2c_soft_start(void) { uint32_t timeout = 1000; I2C_SDA_HIGH(); I2C_SCL_HIGH(); while (I2C_SCL_READ() == 0 && timeout--) ; if (timeout == 0) return I2C_ERR_TIMEOUT; I2C_SDA_LOW(); I2C_SCL_LOW(); return I2C_OK; }起始信号的时序要求是:SCL 在高电平期间,SDA 产生一个下降沿。代码里先拉高 SDA 和 SCL,确认 SCL 真的被外部拉高(I2C_SCL_READ),然后拉低 SDA,再拉低 SCL 准备传数据。I2C_SCL_READ这里读的是输入寄存器,不是输出寄存器,因为开漏输出模式下引脚电平由外部上拉决定,读输出寄存器没意义。
超时保护这套例程里每个时序步骤都做了,原因是 MPU9250 内部有 EEPROM 和 FIFO,极端情况下传感器会拉低 SCL 做时钟延长(clock stretching),主机必须等它释放。如果等不到,说明传感器处于异常状态,此时返回超时错误比卡死循环更能让主程序有机会做恢复处理。我后来在自己项目里还加了一个重试机制:连续超时两次就执行一次软件复位,复位不成就报告错误,而不是让主循环一直卡着。
3.3 MPU9250 初始化:寄存器配置的关键参数
MPU9250 上电后先要复位,然后按顺序配置电源管理、加速度计量程、陀螺仪量程、采样率分频和数字低通滤波器。这套例程的初始化顺序完全按数据手册来,移植的时候不要调整顺序,特别是PWR_MGMT_1寄存器——从 F4 搬到 F1 后,有人图省事把复位寄存器那行注释掉,结果 DMP 加载永远失败。
void mpu9250_init(void) { // 软件复位 write_byte(MPU9250_ADDR, PWR_MGMT_1, 0x80); delay_ms(100); // 唤醒并选择时钟源为PLL write_byte(MPU9250_ADDR, PWR_MGMT_1, 0x01); delay_ms(10); // 配置加速度计量程为 ±8g write_byte(MPU9250_ADDR, ACCEL_CONFIG, 0x10); // 配置陀螺仪量程为 ±2000dps write_byte(MPU9250_ADDR, GYRO_CONFIG, 0x18); // 采样率分频:1kHz / (1 + 0) = 1kHz write_byte(MPU9250_ADDR, SMPLRT_DIV, 0x00); // 数字低通滤波:184Hz write_byte(MPU9250_ADDR, CONFIG, 0x02); }量程配置说明:ACCEL_CONFIG的低四位是量程位,0x10对应 ±8g,满量程输出值是 4096 LSB/g;GYRO_CONFIG的0x18对应 ±2000dps,满量程输出是 16.4 LSB/dps。这两个值直接影响后续把原始值换算成物理量时的缩放系数,改动之前必须同步改换算函数里的除数。采样率分频SMPLRT_DIV是 0,表示输出频率等于内部 1kHz 基础频率除以 1,就是 1kHz;如果设成 4,就是 1kHz / 5 = 200Hz。F1 主频不高,DMP 姿态解算输出如果设到 1kHz,串口打印会占用大量 CPU 时间,例程里实际 main 循环用的是 100Hz 输出,在SMPLRT_DIV里配成 9 更合理。
3.4 数据读取:原始值拼接与换算
MPU9250 的加速度和陀螺仪数据是 16 位有符号数,高字节和低字节分两个寄存器存放,读取时先读高字节再读低字节。这套例程用一个read_byte连续读两个地址,然后做位拼接。
int16_t mpu9250_read_accel_x(void) { uint8_t buf[2]; if (i2c_soft_read_bytes(MPU9250_ADDR, ACCEL_XOUT_H, 2, buf) != I2C_OK) return 0; return (int16_t)((buf[0] << 8) | buf[1]); }这里有个容易翻车的细节:(buf[0] << 8) | buf[1]的结果类型是无符号整型,直接赋给int16_t时,超过 32767 的值会被无符号转有符号处理成负数——这其实是预期行为,陀螺仪数据本来就是有符号的。但如果buf的类型是uint8_t而读函数出错时没有初始化缓冲区,返回的可能是上一次的残留值,跟超时返回的 0 混在一起难以排查。我的习惯是读失败时返回一个特定的异常值比如INT16_MIN,而不是 0,因为 0 在传感器数据里是合法的静止值,容易和错误值混淆。
换算成物理量时注意整型和浮点型的坑。原始值除以 16.4(陀螺仪 2000dps 量程的情况)才能得到每秒度数,如果写成raw / 16.4而 raw 是整型,某些编译器会先做整型除法再做浮点转换,结果全部是整数。必须写成raw / 16.4f或(float)raw / 16.4f。这套例程里换算函数写的是标准写法,但如果你自己改过量程范围,记得同步改这里的系数。
磁力计的读取路径稍微不同,AK8963 是挂在 MPU9250 内部的 I2C 总线上,要先设置I2C_MST_CTRL寄存器启用 I2C 主模式,然后通过EXT_SENS_DATA寄存器读它的输出。从 F4 移植到这里时,磁力计没数据是最常见的问题,多半是没开旁路模式或者没等数据就绪位拉高。例程里用了一个while等 MAG_DATA_RDY 位,F1 上如果主频不够高,这个循环里也要加超时,不然传感器断线就是死等。
4. 移植避坑总结:5 条最常见的翻车现场与排查思路
4.1 数据全是 0 或固定值:I2C 地址搞错了
现象:串口打印加速度和陀螺仪全部是 0,用逻辑分析仪抓 I2C 波形,发现传感器对地址字节没有 ACK 响应。
原因与解决:MPU9250 的 I2C 地址由 AD0 引脚决定。AD0 接地时地址是 0x68,接高电平是 0x69。很多 F4 例程里默认 0x68,而你的模块上 AD0 被拉高,地址对不上。F1 移植时不要只改芯片平台,传感器模块的地址也要重新核对。用 I2C 扫描程序在总线上扫一圈,能直接看到器件在哪个地址响应。
4.2 I2C 忙等卡死:while 循环等事件标志无超时
现象:程序运行到 I2C 读写的某个 while 循环处停住,调试器暂停后 PC 指针落在等待 SCL 释放的循环里出不来。
原因与解决:F1 主频比 F4 低,同样的 while 循环条件更容易撞上传感器时钟延长的窗口期。如果传感器内部正在进行 NVM 写操作或 FIFO 切换,SCL 会一直拉低。标准外设库的 I2C 驱动里,EV6 等事件位的等待循环尤其危险。我在移植时把所有 while 等待都改成了带超时的版本,超时后返回错误码并重试一次,重试仍失败就复位传感器。这个改动之后,长时间跑挂的概率明显下降。
4.3 DMP 固件加载失败:等待 FIFO 中断无响应
现象:load DMP 固件的步骤卡住,dmp_load_motion_firmware返回值一直是负数,F4 上跑得好好的代码到了 F1 就不行。
原因与解决:DMP 固件加载流程里,MPU9250 写固件到内部 RAM 之后要等 FIFO 计数变化才能确认加载成功。F1 的串口打印速度如果很慢,或者调试器打断了时序,传感器的 FIFO 中断会在还没配置好之前就触发。解决方法是先按数据手册建议的时序,在加载固件之前先把 FIFO 重置,然后加一段延时(我一般加 50ms)等传感器内部稳定,再开始写固件。还有一个容易忽略的点:DMP 固件加载要求传感器处于非睡眠状态,PWR_MGMT_1的 SLEEP 位必须清零。
4.4 陀螺仪零漂过大:电源纹波和引脚悬空
现象:静止放置时陀螺仪输出有几百 dps 的漂移,用手摸一下模块数据就变,看起来完全不能用。
原因与解决:MPU9250 对电源噪声敏感,F1 开发板如果是 USB 直接供电,5V 转 3.3V 通常用的是 AMS1117,高频纹波不小。传感器模块和 F1 板之间如果用了过长的杜邦线,还会引入天线效应。我的做法是:VCC 和 GND 之间并联 10μF 和 100nF 两个电容,I2C 连线控制在 10cm 以内,并且把 INT 引脚配置成内部上拉输入(如果例程没配,这个引脚悬空会拾取噪声干扰触发误中断)。
4.5 串口数据乱码:波特率配置受外部晶振影响
现象:MPU9250 输出正常,但串口打印的中文注释全是乱码,数字还能看个大概。
原因与解决:很多 F1 最小系统板用的是 8MHz 外部晶振,也有部分板子用 25MHz 的,两种晶振下串口波特率的BRR计算值完全不同。标准外设库的SystemInit会按宏定义的时钟频率配置,如果你从 F4 工程搬过来忘了改HSE_VALUE,时钟配置错乱,串口波特率自然不对。查这个问题的捷径是:先调小波特率到 9600 看看乱码是否消失,如果消失,大概率是时钟宏定义和实际晶振不匹配——检查stm32f10x.h里的HSE_VALUE和system_stm32f10x.c里的 PLL 倍频配置就行。
5. 二次开发改造:换引脚、换传感器、提频率的三个动手方向
拿到这套例程,第一步建议做的是把引脚宏定义改成你自己板子上的实际连接。比如我把 SCL 从 PB8 换到 PA5、SDA 从 PB9 换到 PA7,只需要改第 3.1 节里四个宏的端口和引脚编号,GPIO 初始化不用动。但注意 F1 的 PA5 和 PA7 在有些板子上接了板载 LED 或其他外设,要确认没有冲突。改完引脚,用i2c_soft_scan()扫一遍总线地址确认传感器能应答,再跑主循环看数据是否更新。
第二个方向是把输出频率调高。例程默认配置适合调试,但如果你要做姿态控制或动作识别,1kHz 的原始采样率更合适。此时需要在 main 循环里把 DMP 读取函数和串口打印解耦——用定时器每 1ms 触发一次数据读取,把姿态结果存到全局变量,主循环只负责把最新值发出去。我就是这么干的,开始时打印函数耗时太长,导致定时器回调被串口阻塞占用,后来把打印改成了 DMA 发送,问题才解决。
第三个方向是灵敏度调整。如果你的应用需要更灵敏的加速度范围,把ACCEL_CONFIG改成0x08(±4g),对应的缩放系数从 4096 改成 8192;陀螺仪量程改成0x08(±500dps),缩放系数从 16.4 改成 65.5。改完量程记得重新校零——F1 平台的移植里这步经常被跳过,结果就是数据范围看着正常,实际换算出来的物理量偏差很大。
验证移植是否成功的标准很简单:传感器静止时加速度 Z 轴读数约等于 1g,水平放置时 X、Y 轴接近 0,陀螺仪三轴静止时零漂在 ±3dps 以内,转动模块时数据随方向实时变化。从那以后我每次换芯片平台,都强制自己先跑一遍总线扫描和静止数据检查再做后续开发,顺便说一句:这套例程是我用过的 MPU9250 移植工程里结构最清晰的一个,希望帮到你。
本文还有配套的精品资源,点击获取