
简介面向嵌入式开发者和STM32学习者这一MPU9250九轴传感器驱动源码例程完整呈现了从STM32F4平台移植到STM32F1平台的全过程。作者围绕两个平台的硬件差异重点解决普通IO与I2C/SPI接口的引脚映射、中断服务例程适配、时钟树配置、内存占用缩减以及HAL/LL库函数兼容等关键问题并适当适配MPL等中间层库便于快速集成到资源有限的F1工程中。资源包共219个文件压缩后约4.48MB主体为C源文件与头文件包含MPU9250 DMP姿态解算驱动、STM32F10x标准外设库相关源码以及Keil工程文件.uvprojx/.uvoptx和编译生成的hex、axf、map等构建输出可直接打开工程编译调试。目前已有2135人学习使用。通过学习该例程可系统掌握外设驱动跨平台移植的思路与调试方法获得经过适配验证的F1版本驱动显著缩短低成本运动检测或导航应用的开发周期。 上周帮朋友把手头一块F407开发板上的MPU9250例程迁到F103小板上原以为F4和F1同属STM32家族库函数风格都差不多把芯片型号一改最多改改启动文件就能跑。结果从编译报错到上板后加速度数据全为零断断续续折腾了两天中间还一度怀疑传感器硬件被烧了。这篇文章就是把这次MPU9250从STM32F4移植到STM32F1的过程完整复盘一遍把真正要改的地方讲透顺带解释每处改动背后的原因。不管你是用正点原子、野火的F4例程还是从GitHub上捞的工程只要按这个思路梳理都能省掉大把调试时间。1. 为什么F4例程不能直接烧进F1先摸清驱动的依赖边界先说结论MPU9250这颗传感器本身不挑MCU真正挑的是代码和MCU绑得有多深。很多人所谓的“移植”就是把.c文件拖进新工程结果报错一堆那是因为没分清哪些代码属于传感器、哪些代码属于STM32F4。1.1 一个典型MPU9250例程由哪几层组成大多数MPU9250例程源码结构是类似的最底层是IIC初始化、IIC读写函数中间层是mpu9250.c、inv_mpu.c这类传感器驱动负责读写寄存器、初始化配置、加载DMP固件最上层是应用层比如串口打印、OLED显示、姿态解算结果输出。动手之前先做一次“依赖检查”打开例程全局搜索GPIO_、RCC_、SysTick、dprintf、I2C_这些关键词把涉及这些内容的文件标记为“MCU相关”其余标记为“传感器相关”。STM32F4到F1之间真正要动的其实只有那几个MCU相关文件。传感器相关代码像寄存器地址、初始化时序在F1上完全不需要改因为MPU9250芯片本身不关心主控是F407还是F103它只认IIC总线上谁在读它。1.2 MPU9250其实是两颗芯片的合体还有一个很多教程忽略的事实MPU9250内部并不只是一颗惯性传感器而是MPU6500三轴加速度计三轴陀螺仪和AK8963三轴磁力计两颗芯片封装在一起。它们挂在同一条IIC总线上MPU6500的地址通常是0x68或0x69AK8963的地址是0x0C。这个前提对移植很重要。我实际遇到过的现象是陀螺仪和加速度都能正常读出来磁力计却一直没数据就以为是AK8963坏了。其实多半是F1的IIC时序对AK8963的初始化不够稳定或者代码只初始化了MPU6500根本没管AK8963。移植后验证数据时要分开验证这两颗芯片别合在一起当成“一颗九轴传感器”去查。2. F1与F4的底层差异时钟、延时和引脚配置里藏着的坑F407默认主频168MHzF103最大72MHz。这个差距带来的第一个直接后果就是延时函数必须重做。2.1 从168MHz到72MHz延时函数必须重做MPU9250的初始化过程对延时是有依赖的比如上电后至少要等几十毫秒让内部电源稳定写寄存器之间也需要微秒级的等待。如果直接把F407工程里的delay.c复制到F1工程因为系统时钟配置完全不同同样的循环次数产生的延时会严重漂移。更隐蔽的是很多F4例程的delay_us是用DWT数据观察点与跟踪单元计数器实现的而F103没有DWT。这种代码到了F1上要么编译不过要么延时行为完全看编译器优化心情。最稳妥的做法是在F1工程里直接换一套基于SysTick轮询的延时实现不依赖中断也不依赖DWT。我自己常用这个简版static __IO uint32_t g_fac_us 9; void delay_init(void) { SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); g_fac_us SystemCoreClock / 8000000; } void delay_us(uint32_t nus) { uint32_t temp; SysTick-LOAD nus * g_fac_us - 1; SysTick-VAL 0; SysTick-CTRL | SysTick_CTRL_ENABLE_Msk; do { temp SysTick-CTRL; } while ((temp SysTick_CTRL_COUNTFLAG_Msk) 0); SysTick-CTRL ~SysTick_CTRL_ENABLE_Msk; } void delay_ms(uint32_t nms) { while (nms--) { delay_us(1000); } }SysTick在F1上自动以HCLK/8作为计数时钟72MHz主频下就是9MHz一个时钟周期大约0.11微秒。上面把g_fac_us取9LOAD nus * 9 - 1算出来的延时就能和微秒数量级对上。这段延时是轮询方式如果在中断里调用会阻塞中断但用在传感器初始化阶段完全没问题。还有一个坑正点原子F4例程里的delay_ms需要配套SysTick_Handler中断服务函数移植过来时一定要检查F1工程里有没有同步放入这个函数否则会出现while循环卡住的现象。2.2 GPIO配置方式完全不同F1没有MODER寄存器F4的GPIO初始化里有GPIO_Mode_OUT、GPIO_OType、GPIO_PuPd这类字段底层对应的是MODER、OTYPER、PUPDR寄存器。F103的GPIO完全不是这套结构它用的是CRL/CRH寄存器。在标准外设库里的体现就是没有GPIO_PuPd、没有GPIO_OTypeGPIO_Mode取值变成GPIO_Mode_Out_PP、GPIO_Mode_Out_OD、GPIO_Mode_IPU、GPIO_Mode_IN_FLOATING这一套。所以从F4代码往F1代码拷初始化结构体时直接删除GPIO_PuPd和GPIO_OType两行把GPIO_Mode_OUT改成GPIO_Mode_Out_PP一般就能通过编译。如果IIC引脚要配置成开漏输出就写GPIO_Mode_Out_OD。2.3 模拟IIC才是移植最省事的方案F4和F1都有硬件IIC外设但关于STM32硬件IIC的吐槽一直很多。移植时我强烈建议用模拟IIC原因很简单模拟IIC只依赖两个GPIO引脚的翻转和延时换芯片平台时几乎不用改硬件IIC要处理外设时钟、中断、错误标志F1和F4的库函数细节还有差异调试成本明显更高。模拟IIC还有一个额外好处时序完全由软件控制。遇到读不到设备的情况可以直接把IIC时钟放慢比如把每次电平翻转后的延时调大就能排除“时序太快”这个变量。后面调试章节会再提到这个技巧。3. 逐模块替换这份移植清单可以直接照做这里是一份我实际操作用的清单顺序也按这个来能少走很多弯路。3.1 新建F103基础工程再往里面搬传感器代码不要试图把F407工程里的所有文件都搬过来。F4工程的启动文件、system_stm32f4xx.c、头文件路径、目标芯片宏定义和F1完全是两套硬搬只会让编译错误越来越多。正确做法是先新建一个能跑起来的F103标准外设库空工程用V3.5库跑个串口和IO翻转验证环境没问题再把MPU9250相关的源文件复制进来。这里提醒一下Keil里新建工程时如果Device列表里没有STM32F103系列芯片需要先去Pack Installer安装Keil.STM32F1xx_DFP支持包否则建不了工程。复制源文件时还要留意原F4例程是标准库还是HAL库。如果是CubeMX生成的HAL库代码里面大量HAL_GPIO_WritePin、HAL_Delay这样的接口在F1标准库工程里没有不要试图逐行“翻译”直接按功能写到F1的GPIO操作里。我一般会新建一个mpu9250_bsp.c把F4例程里的IIC引脚操作、延时调用都封装进去后续改动只动这一个文件inv_mpu.c这些传感器算法层文件可以原样保留。3.2 把IIC底层改成标准库V3.5风格以PB8SCL、PB9SDA为例F1工程的IIC初始化通常长这样GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_8 | GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_OD; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure);注意RCC这行和F4区别很大F4是RCC_AHB1Periph_GPIOBF1是RCC_APB2Periph_GPIOB两者连总线都不一样。引脚初始化后再写SDA输入/输出切换函数。模拟IIC里SDA一会要输出低电平一会要读回数据所以需要一个切换方向的动作static void SDA_OUT(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_OD; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); } static void SDA_IN(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, GPIO_InitStructure); }SCL引脚始终用开漏输出两个引脚各自配上拉电阻到3.3V。这里有个细节MPU9250模块的VDD必须接3.3V不能接5V否则模块上的电平逻辑会出问题也会导致IIC读不到数据。上拉电阻推荐4.7kΩ到10kΩ太小会拉低信号跳变速度太大则信号沿不陡。3.3 批量替换RCC和外设总线映射移植过程中报错最多的就是RCC开头的宏因为F4和F1的外设挂在不同总线上。最常遇到的几个差异整理成表格| 外设 | STM32F4 | STM32F1 | | GPIOA~GPIOI | AHB1 | APB2 | | USART1 | APB2 | APB2 | | USART2/3 | APB1 | APB1 | | SPI1 | APB2 | APB2 | | SPI2 | APB1 | APB1 | | I2C1 | APB1 | APB1 |改的时候直接用编辑器的全局搜索替换把RCC_AHB1Periph_GPIOx换成RCC_APB2Periph_GPIOx其他外设保持对应总线。替换完编译一遍报错会被引导到剩余文件里继续修就行。3.4 串口打印重定向和波特率校验姿态数据要看到总得用串口打出来。F4例程里的fputc重定向、printf头文件在F1上通常通用但要注意F103的APB1总线最高36MHzAPB2总线最高72MHz如果原F4工程里USART2/3的波特率配置依赖较高的PCLK1到F1上分频后实际波特率可能不对。我调试时先把波特率设成115200如果串口助手收到乱码优先检查时钟配置和波特率寄存器分频值别急着改硬件。4. 编译报错集锦F4代码搬上F1最常见的拦路虎这里把这次移植中碰到的报错集中整理每个问题都按现象、原因、处理来讲方便复现排查思路。4.1 莫名其妙出现GPIOA未定义现象编译报error: GPIOA undeclared或者找不到GPIO_TypeDef。原因F4例程头文件路径还指向stm32f4xx.h而F1工程没有包含stm32f10x.h。这种报错往往不是代码本身的问题而是工程的include路径混进了F4的CMSIS头文件。处理把工程里所有指向stm32f4xx、core_cm4的头文件目录删掉换成F1标准库对应的CMSIS和标准外设库路径。同时确认启动文件用的是startup_stm32f10x_hd.s而不是F4的启动文件。4.2 直接操作寄存器的代码MODER没有CRL/CRH才有现象报error: GPIOA has no member named MODER。原因有些F4例程为了省事直接写GPIOA-MODER、GPIOA-OTYPERF1的GPIO寄存器是CRL/CRH/IDR/ODR/BSRR/BRR根本没有MODER。处理要么改用标准库的GPIO_SetBits、GPIO_ResetBits、GPIO_ReadInputDataBit要么把寄存器操作改写为CRL/CRH。这里特别提醒直接改寄存器比库函数难查错建议优先统一成库函数。4.3 GPIO_Mode_AF这种枚举在F1里不存在现象报GPIO_Mode_AF undeclared或者GPIO_Speed_100MHz undeclared。原因F4的GPIO模式分成GPIO_Mode_IN、OUT、AF再由OType区分推挽开漏F1的GPIO_Mode枚举里写的是GPIO_Mode_AF_PP、GPIO_Mode_AF_OD。速度档位也不同F4没有100MHzF1没有100MHz。处理把GPIO_Mode_AF改成GPIO_Mode_AF_PP或GPIO_Mode_AF_OD把GPIO_Speed_100MHz改成GPIO_Speed_50MHz。4.4 u8/u16重复定义现象报redefinition of typedef u8。原因F4例程为了方便常常自己typedef了u8、u16、u32而F1标准库的stm32f10x.h里也定义了u8、u16、u32两处冲突。处理删掉工程自定义的类型定义统一用标准库的。如果工程里大量文件都用u8来写保留标准库的typedef即可不需要额外再定义。这四条是报错里最有代表性的。如果报错反而集中在inv_mpu.c这类传感器算法文件里先别急着改算法检查一下是不是头文件路径把F4的头文件也加进来了或者某个底层IIC读写函数的签名没有对上。DMP库本身不依赖具体MCU型号只要底层读写接口对了编译通常很快能过。5. 上板调试数据对了才算移植成功编译通过只是开始。我见过很多工程编译零警告上板后数据全乱。下面按调试顺序讲每步都承担验证职责。5.1 第一步先读WHO_AM_I写一个最简单的初始化IIC初始化后直接读MPU6500的WHO_AM_I寄存器0x75预期返回0x71。读不到就回到上一章排查IIC底层不要急着向下调。如果读到0x71说明IIC底层、引脚、供电、上拉都没问题。接下来再读AK8963的WIA寄存器地址0x0C预期返回0x48。两个芯片都读到了再继续配置量程、数字滤波这些。如果IIC读不出来最有效的排查手段是把IIC_Delay加大把SCL每个电平持续的时间从几百纳秒放大到几个微秒然后再试。模拟IIC的好处就在这时序完全是软件控制的。很多AK8963读不到的问题就是IIC速率冲到400kHz以上复位后时序不稳定。5.2 用静止数据验证加速度和陀螺仪把板子平放加速度计量程设为±2g读数X、Y应该接近0Z轴应该接近16384 LSB1g对应的数值。陀螺仪输出应该接近0允许有几十LSB的零偏。如果加速度输出全是0检查电源管理寄存器PWR_MGMT_1有没有把传感器关掉以及加速度量程配置是不是被后续代码覆盖。如果陀螺仪漂移异常大多半是传感器供电纹波或者PCB布局问题和MCU移植关系就不大了。5.3 磁力计读数和常见现象排查磁力计要手动开启AK8963的连接和读模式很多F4例程里这部分被封装得很深移植到F1后只初始化了MPU6500部分所以磁力计一直是0。我习惯把磁力计的验证独立成单独的函数读AK8963的WIA再读一组原始磁场数据看它会不会随板子转动变化。调试过程中的常见现象和排查方法整理成一张表方便直接对照| 现象 | 排查方向 | | IIC一直没应答 | 测SCL/SDA波形查上拉电阻确认传感器供电是3.3V | | 加速度和陀螺仪全0 | 查PWR_MGMT_1、SLEEP位、量程配置寄存器 | | 陀螺仪漂移大 | 查供电纹波、退耦电容、传感器固定方式 | | AK8963一直读不到 | 查CNTL寄存器配置、IIC速率是否太快、总线是否被占用 | | 数据跳动剧烈 | 查IIC时序是否太差、地线是否共地、杜邦线是否过长 |这些都是实际趟过的坑尤其是杜邦线过长导致信号质量下降那条在实验板上很常见。把杜邦线剪短到10cm以内很多“玄学”问题就消失了。6. 移植完成后还值得做的三件事能读到正确数据只是第一步MPU9250的“九轴”应用里姿态角通常要DMP或算法算出来。这里列三个通用扩展点。6.1 把DMP接上姿态解算才能真正用起来MPU9250例程里的DMP固件加载和姿态解算大部分放在inv_mpu.c和inv_mpu_dmp_motion_driver.c里。移植时底层只要把i2c_write、i2c_read函数指针指向F1自己的软件IIC读写函数DMP初始化时该读的固件、该写的寄存器都用这套函数完成。DMP库本身不绑定MCU型号所以通常改动量很小。注意F1里中断引脚如果接了EXTI要和F4一致不要漏掉中断服务函数。6.2 上FreeRTOS之前IIC访问要先上锁如果后面要把这套代码搬到FreeRTOS工程里MPU9250的IIC总线不能多个任务同时读。常见做法是给传感器读写函数加一个互斥锁任务里调mpu9250_read_accel前后取锁放锁。这不是F1特有的问题但很多人在裸机上跑习惯了一到多任务环境就踩并发坑。6.3 IIC改SPI提性能的另一条路MPU9250支持SPI模式F103的SPI1最高18MHz理论上比IIC的400kHz快不少。但要特别注意切到SPI模式后AK8963磁力计不能直接通过SPI访问只能走MPU6500内部的I2C Master通道间接读。如果只需要加速度和陀螺仪SPI是性能方案如果九轴数据都要模拟IIC反而更省心。这个坑我以前踩过移植时别看到“支持SPI”就直接冲。最后再补充一点自己的感受。做这种跨芯片移植最容易乱的地方是把“F4的工程”和“MPU9250的驱动”混成一体来搬。真正顺手的做法是建一个F1空工程把F4例程里的传感器逻辑一层层解耦出来再放进F1的底层里。按这个思路无论你是从F407迁到F103还是以后换GD32、换国产M0内核流程都一样。我也是踩了几天坑才总结出这套顺序希望这篇能帮你把那几天省下来。本文还有配套的精品资源点击获取