☰
STM32F1移植MPU9250:I2C时序与DMP避坑指南
2026/10/26 6:40:25 网站建设 项目流程

简介:源码例程聚焦 MPU9250 九轴传感器在 STM32F4 与 STM32F1 之间的驱动迁移,适合已具备单片机基础、正在做嵌入式运动检测或导航应用的开发者参考。它围绕 GPIO 引脚重映射、I²C/SPI 通信时序、中断向量表调整、时钟与电源配置、RAM/Flash 受限时的代码裁剪,以及 HAL 与 LL 库差异等实际问题展开,能帮助读者理解从高性能平台转向低成本平台时的适配路径。压缩包共 219 个文件,约 4.48MB,以 68 个 .h 头文件、46 个 .c 源文件为核心,并包含 Keil 工程文件、编译产物与少量脚本、说明文件,目录覆盖驱动层、传感器初始化与 DMP 运动数据处理模块。已有 2143 人学习下载。读者可从中获得较完整的工程对照样本,用于梳理接口移植、驱动调用关系和常见编译排错思路。

1. 为什么F4能跑的MPU9250例程搬到F1就“装死”:从时钟到I2C的连锁反应

把一份在STM32F4上跑通的MPU9250源码例程搬到STM32F1,最常见的翻车不是传感器坏了,而是工程换完芯片型号后串口一直吐0。标题里的“移植”不是改几个宏,它要同时处理F1与F4的时钟、I2C时序、DMA通道和浮点能力差异,还要把MPU9250的寄存器访问从原例程的总线层里剥出来。适合手里已有F4例程、想让F1板子继续读九轴数据的人;也适合刚接手一套祖传源码、需要判断该改驱动还是该重写初始化的人。后面按“拆边界—换总线—排故障—调参数—做验证”走,每一步都给出可对照的寄存器与参数。

2. 移植前先拆边界:MPU9250总线时序与F1/F4外设差异

先把“传感器逻辑”和“芯片外设”分开看。MPU9250本身只关心两件事:你用什么总线访问它的寄存器,以及你给它的电源、上拉和延时是否够。F4例程里那些HAL_I2C_Mem_Read、DMA通道、__FPU_PRESENT宏,都是芯片侧的东西。移植到F1时,如果直接把工程器件型号一换就编译,最常见的结果是能过编译、跑起来读回全0或全1。下面先把差异摊开,再决定哪些代码保留、哪些重写。

对比项F4例程常见形态F1落地要确认的点
内核与浮点Cortex-M4F,带FPUCortex-M3,无FPU,浮点运算靠软件模拟
系统主频常见168MHz常见72MHz,延时循环和超时要重算
I2C时钟源APB1频率较高APB1常见36MHz,CCR/TRISE要按36MHz算
DMA映射DMA2通道DMA1通道,请求映射表不同
库风格HAL/LL常见标准外设库或F1版HAL,别混用头文件
中断优先级分组可能不同确认NVIC优先级分组与DMP中断不冲突

2.1 MPU9250寄存器模型:为什么先读WHO_AM_I再谈驱动

MPU9250内部不是一颗“单芯片”,而是加速度计/陀螺仪 die 加一颗AK8963磁力计 die。加速度计和陀螺仪挂在其I2C从地址上,AD0引脚决定7位地址是0x68还是0x69;磁力计默认在0x0C,但通常不直接飞到外部总线,而是通过MPU9250的旁路模式或主机模式访问。很多F4例程把这两条路径封装在同一个MPU9250_ReadReg里,移植时先别改这个函数,先让它能读回两个身份寄存器。

目标7位地址寄存器预期值说明
MPU92500x68/0x690x750x71WHO_AM_I,确认总线通
AK89630x0C0x000x48磁力计身份,需旁路或主机模式
电源管理0x680x6B读回可配置先退出睡眠再读数据
中断配置0x680x37bit1置1开旁路直接访问磁力计常用

我一般会先写一个只做“读一个字节”的最小函数,不碰DMP、不碰姿态解算、不碰DMA。这个函数能稳定读回0x71和0x48,后面的初始化序列才有意义。读不回时不要先怀疑传感器模块,先看地址是否左移、上拉是否焊、F1的I2C是否卡在BUSY。这个顺序能省掉很多玄学排查。

2.2 F4到F1的四个硬差异:主频、I2C时序、DMA和FPU

第一个差异是主频。F4上delay_ms(1)的循环次数搬到F1会变长,传感器上电后的稳定延时、复位后的等待、AK8963模式切换后的等待都会跟着变。不要用“大概延时”糊过去,F1上先把SysTick配成1ms,再用示波器或翻转GPIO确认延时。第二个差异是I2C时序。F1的I2C1挂在APB1,常见PCLK1为36MHz,标准模式100kHz下CCR按PCLK1/(2*100k)算,TRISE按(1000ns/T_PCLK1)+1算。快速模式400kHz的占空比和CCR公式也不同,不能把F4的初始化结构体直接抄过来。

参数100kHz标准模式400kHz快速模式备注
PCLK136MHz36MHz先确认RCC配置
CCR18030(占空比2:1)按F1参考手册公式
TRISE37121000ns或300ns上限
上拉4.7kΩ左右2.2kΩ~4.7kΩ模块自带也要实测

第三个差异是DMA。F4例程可能用DMA2做I2C收发,F1的I2C1_TX/RX对应DMA1的不同通道,而且通道优先级、传输方向、外设地址递增配置都要改。传感器寄存器读取数据量很小,我通常先关DMA,用阻塞式读写把链路跑通,再决定要不要开DMA。第四个差异是FPU。F1没有硬件浮点,原例程里如果用了大量float做姿态融合,F1上会明显变慢,甚至因为中断里做浮点导致采样周期抖动。DMP输出四元数是定点格式,反而更适合F1;如果不用DMP,融合周期要重新算。

提示:F1的I2C BUSY卡死时,先断电或把SCL翻转9个时钟恢复总线,不要反复复位I2C外设。

2.3 源码例程分层:把芯片相关代码和传感器逻辑切开

移植时最怕在mpu9250.c里直接看到I2C1->DR或HAL_I2C_Master_Transmit。传感器层只应该知道“读哪个寄存器、读几个字节、返回什么”,总线层负责地址左移、起始停止、等待标志。这样F4换F1时只改总线层,传感器初始化序列和寄存器表不动。目录可以按下面拆,名字不重要,边界要清楚。

/* bsp_i2c_f1.h:只暴露总线读写,不暴露I2C寄存器 */ #ifndef BSP_I2C_F1_H #define BSP_I2C_F1_H #include <stdint.h> uint8_t bus_i2c_write(uint8_t dev7, uint8_t reg, const uint8_t *buf, uint16_t len); uint8_t bus_i2c_read(uint8_t dev7, uint8_t reg, uint8_t *buf, uint16_t len); void bus_i2c_init(void); #endif
/* mpu9250.c:传感器层只调bus_xxx,不碰具体外设 */ uint8_t mpu9250_init(void) { uint8_t who = 0; if (bus_i2c_read(0x68, 0x75, &who, 1) != 0) { return 1; /* 总线错误 */ } if (who != 0x71) { return 2; /* 身份不对,先查地址和上拉 */ } /* 退出睡眠,选择PLL时钟,具体值按寄存器表核对 */ uint8_t pwr = 0x01; bus_i2c_write(0x68, 0x6B, &pwr, 1); return 0; }

上面代码的关键不是0x75和0x71,而是bus_i2c_read的返回值必须能区分“总线没应答”和“读到了但值不对”。很多例程把返回值吞掉,最后只看到全0,排查时像黑匣子。参数上,dev7传7位地址,总线层内部再左移;reg是要访问的寄存器;len是连续读的字节数。F1标准库下,连续读要处理I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED和最后一个字节的ACK/NACK,不能只发地址就读。

2.4 建立最小可编译工程:替换启动文件与时钟树

先不要动MPU9250代码。新建一个F1工程,选对启动文件,把系统时钟配到72MHz,重定向printf,让串口能打印。然后跑一个LED闪烁和串口输出,确认SysTick、GPIO、USART都正常。再把原F4工程里的传感器文件复制进来,只保留mpu9250.c、mpu9250_reg.h、ak8963.c,删掉F4的总线实现和DMA初始化。编译时如果报HAL_xxx未定义,说明库风格混了,要么全换F1标准库,要么全换F1版HAL,不要两边头文件一起包含。

/* 确认F1时钟:串口打印SYSCLK和PCLK1,I2C时序按PCLK1算 */ RCC_ClocksTypeDef clocks; RCC_GetClocksFreq(&clocks); printf("SYSCLK=%lu PCLK1=%lu\r\n", clocks.SYSCLK_Frequency, clocks.PCLK1_Frequency);

这段代码放在main初始化之后、传感器初始化之前。SYSCLK_Frequency应该是72MHz,PCLK1_Frequency常见36MHz。如果PCLK1不是36MHz,前面表格里的CCR和TRISE要重算。参数说明:RCC_ClocksTypeDef来自F1标准库,打印格式用%lu匹配32位无符号长整型。串口能稳定打印之后,再接上I2C读WHO_AM_I,链路问题会少一半。

3. 把I2C驱动换成F1能跑的版本:从引脚到读写函数

这一章只做一件事:让F1通过I2C读到MPU9250的0x75和AK8963的0x00。硬件I2C能跑通就用硬件,跑不通再用GPIO模拟兜底。顺序不要反,先硬件后软件,因为硬件I2C的时序更准,软件模拟容易在F1上被中断打断。下面给的代码基于F1标准外设库,引脚用I2C1的PB6/PB7,实际板子不同就改引脚和时钟。

3.1 硬件I2C初始化:100kHz下F1的CCR和TRISE怎么算

初始化分三步:开GPIO和I2C时钟,把PB6/PB7配成复用开漏,填I2C_InitTypeDef。注意F1的I2C引脚必须配成GPIO_Mode_AF_OD,外部上拉不能省;模块自带上拉也要用万用表确认,别只看原理图。I2C_ClockSpeed设100000,I2C_DutyCycle在标准模式下填I2C_DutyCycle_2即可,I2C_OwnAddress1填0,I2C_Ack使能,地址模式7位。

void bsp_i2c1_init(void) { GPIO_InitTypeDef gpio; I2C_InitTypeDef i2c; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); gpio.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; gpio.GPIO_Mode = GPIO_Mode_AF_OD; /* I2C必须开漏复用 */ gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &gpio); I2C_DeInit(I2C1); 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 */ I2C_Init(I2C1, &i2c); I2C_Cmd(I2C1, ENABLE); }

这段代码没有直接写CCR和TRISE,标准库会根据I2C_ClockSpeed和PCLK1自动算。但前提是RCC_GetClocksFreq读到的PCLK1正确。如果PCLK1是36MHz,100kHz对应CCR=180、TRISE=37;如果PCLK1是24MHz,参数会变。移植后第一件事是打印PCLK1,不是盲目改I2C_ClockSpeed。另外,F1的I2C1在异常时可能卡在BUSY,初始化前如果I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)一直为真,先做总线恢复,再I2C_DeInit。

3.2 读MPU9250的WHO_AM_I:地址0x68与寄存器0x75

读单寄存器要发两次起始:第一次写寄存器地址,第二次读数据。下面函数把7位地址左移后发I2C_Direction_Transmitter,再发I2C_Direction_Receiver。每一步都等事件,不要用固定延时替代。返回0表示成功,非0表示在哪一步失败。调试时把失败步骤打到串口,比只打印“初始化失败”有用得多。

uint8_t mpu9250_read_reg(uint8_t dev7, uint8_t reg, uint8_t *val) { uint32_t timeout = 100000; while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)) { if (--timeout == 0) return 1; /* 总线BUSY超时 */ } I2C_GenerateSTART(I2C1, ENABLE); timeout = 100000; while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)) { if (--timeout == 0) return 2; } I2C_Send7bitAddress(I2C1, dev7 << 1, I2C_Direction_Transmitter); timeout = 100000; while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)) { if (--timeout == 0) return 3; /* 地址无应答 */ } I2C_SendData(I2C1, reg); timeout = 100000; while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)) { if (--timeout == 0) return 4; } I2C_GenerateSTART(I2C1, ENABLE); /* 重复起始 */ timeout = 100000; while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)) { if (--timeout == 0) return 5; } I2C_Send7bitAddress(I2C1, dev7 << 1, I2C_Direction_Receiver); timeout = 100000; while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)) { if (--timeout == 0) return 6; } I2C_AcknowledgeConfig(I2C1, DISABLE); /* 最后一个字节NACK */ I2C_GenerateSTOP(I2C1, ENABLE); while (I2C_GetFlagStatus(I2C1, I2C_FLAG_RXNE) == RESET); *val = I2C_ReceiveData(I2C1); I2C_AcknowledgeConfig(I2C1, ENABLE); return 0; }

参数说明:dev7是7位地址,MPU9250传0x68,AK8963传0x0C;reg是寄存器地址;val是输出。返回值1到6对应BUSY、起始、地址、寄存器发送、重复起始、接收地址六个阶段。实际调试时先读0x75,返回值应该是0,*val应该是0x71。如果返回3,查地址和上拉;返回1,查总线是否被从机拉死。这个函数不处理连续读,连续读要另写,不能直接循环调用单字节读,否则磁力计数据会不连续。

3.3 读AK8963的WHO_AM_I:地址0x0C与寄存器0x00

AK8963默认不是直接挂在外部总线上的。常见做法是写MPU9250的INT_PIN_CFG寄存器0x37,把bit1(BYPASS_EN)置1,让磁力计旁路到主I2C总线。写完之后要延时,再读0x0C的0x00。如果读不到0x48,先确认0x37是否写成功,再确认AK8963是否上电。有些模块把磁力计的电源和MPU9250分开,原理图上多一路使能,代码里没拉高就永远读不到。

uint8_t ak8963_who_am_i(uint8_t *who) { uint8_t cfg = 0x02; /* BYPASS_EN=1 */ if (mpu9250_write_reg(0x68, 0x37, cfg) != 0) return 1; delay_ms(10); /* 等旁路稳定 */ return mpu9250_read_reg(0x0C, 0x00, who); }

这段代码里mpu9250_write_reg可以按上一节的读函数改一个写版本,注意写单字节不需要重复起始,发完数据直接STOP。delay_ms(10)不是随便给的,旁路切换后给磁力计一点时间;如果读回0x00,先把延时加到50ms试一次,再查硬件。读回0x48之后,还要配置AK8963的CNTL1寄存器0x0A为0x16,进入16位输出连续测量模式。读数据前看ST1寄存器0x02的DRDY位,读完6字节再读ST2寄存器0x09释放,不然数据会锁死。

3.4 软件I2C兜底:GPIO模拟时序与上拉电阻

硬件I2C调不通时,软件模拟是最快的兜底。把PB6/PB7配成开漏输出,SCL和SDA都带上拉,用GPIO_WriteBit翻转。软件I2C的坑在延时和时钟拉伸:F1跑72MHz,delay_us(2)在100kHz下差不多,但中断一打断就会变慢。所以软件I2C期间最好关总中断,或者保证没有高优先级中断。下面只给起始和写字节的核心,读字节按同样节奏在SDA上采样。

#define SCL_H() GPIO_SetBits(GPIOB, GPIO_Pin_6) #define SCL_L() GPIO_ResetBits(GPIOB, GPIO_Pin_6) #define SDA_H() GPIO_SetBits(GPIOB, GPIO_Pin_7) #define SDA_L() GPIO_ResetBits(GPIOB, GPIO_Pin_7) #define SDA_IN() GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7) static void i2c_delay(void) { delay_us(3); } void soft_i2c_start(void) { SDA_H(); SCL_H(); i2c_delay(); SDA_L(); i2c_delay(); SCL_L(); i2c_delay(); } uint8_t soft_i2c_write_byte(uint8_t b) { for (uint8_t i = 0; i < 8; i++) { if (b & 0x80) SDA_H(); else SDA_L(); b <<= 1; i2c_delay(); SCL_H(); i2c_delay(); SCL_L(); i2c_delay(); } SDA_H(); i2c_delay(); SCL_H(); i2c_delay(); uint8_t ack = (SDA_IN() == 0) ? 0 : 1; SCL_L(); i2c_delay(); return ack; }

参数说明:i2c_delay里的3微秒对应大约100kHz出头,实际用逻辑分析仪抓SCL周期再调;上拉电阻4.7kΩ在3.3V下常见,线长或模块多时降到2.2kΩ;soft_i2c_write_byte返回0表示从机应答,返回1表示无应答。软件I2C读MPU9250时,地址仍然左移,读寄存器也要重复起始。写完这套后,把总线层bus_i2c_read/write切到软件实现,传感器层一行不改,这就是分层的好处。

4. 移植避坑记录:不通信、数据跳变和磁力计读不到

移植F1最花时间的不是写代码,是定位“哪一层断了”。下面五条是血泪经验里出现频率最高的现象,每条按“现象—原因—解决”写。排查时先备一个逻辑分析仪或示波器,没有仪器就用GPIO翻转和串口打印缩小范围。不要一上来改DMP,先把WHO_AM_I读通。

现象先查快速验证
WHO_AM_I读回0x00/0xFF地址、上拉、BUSY换软件I2C读同一寄存器
加速度计数据满量程跳量程配置、字节序只读6字节原始值打印
磁力计读不到0x48旁路、电源、CNTL1读0x37确认bit1
DMP进HardFaultRAM、栈、对齐先关DMP读原始数据
姿态角漂移零偏、DLPF、周期静止采样求均值

4.1 现象:WHO_AM_I读回0x00或0xFF

现象是串口打印WHO=0x00或WHO=0xFF,I2C函数返回值可能是3或6。原因通常有三类:地址没有左移,I2C_Send7bitAddress收到的是0x68而不是0xD0;模块没有上拉,SDA一直被拉低或浮空;F1的I2C卡在BUSY,后续起始发不出去。先看返回值,返回3说明地址阶段无应答,返回6说明接收地址阶段失败。用万用表量SDA/SCL对3.3V的电压,空闲时应接近3.3V;如果只有1V左右,上拉不够或从机拉死。

解决顺序:先用软件I2C读0x75,如果能读回0x71,硬件I2C的时序或BUSY恢复有问题;如果软件I2C也不行,查地址左移、供电和焊接。硬件I2C卡BUSY时,把SCL配成普通GPIO,手动翻转9个时钟,再发STOP,最后重新初始化。这个恢复动作我一般放在bsp_i2c_init开头,避免上一次调试留下的BUSY带到下一次上电。

4.2 现象:加速度计数据满量程跳变

现象是静止放置时,加速度计某轴在±2g和±16g之间乱跳,或者原始值一直是0x7FFF。原因常见是量程寄存器和解析代码不匹配:初始化写的是±16g,解析按±2g算灵敏度;或者读的是6字节,但结构体对齐导致高低字节拼错。MPU9250的加速度计数据从0x3B开始,高字节在前,低字节在后,读出来要(int16_t)((hi<<8)|lo)。如果先读低字节再读高字节,数据会像噪声。

解决:先把量程固定为±2g,寄存器0x1C写0x00,灵敏度按16384 LSB/g算;静止时Z轴应该接近1g,即16384左右。确认原始值稳定后,再改量程。陀螺仪同理,0x1B写0x00是±250dps,灵敏度131 LSB/(°/s)。不要一边改量程一边看姿态角,先把原始三轴打印成十进制,确认符号和量级都对。

4.3 现象:磁力计始终读不到0x48

现象是MPU9250的0x75能读0x71,但0x0C的0x00读回0x00或0xFF。原因通常是旁路没开、AK8963没上电、或者模式没切。先写0x37=0x02,延时10ms以上,再读0x0C。如果还是不行,查模块原理图上的磁力计电源使能引脚,有些模块用GPIO控制,代码里不拉高就一直断电。旁路开了之后,0x0C应该能应答;不应答就是硬件或电源问题。

能读到0x48但读不到数据时,查CNTL1寄存器0x0A是否写了0x16,查ST1的DRDY位是否置1,读6字节后是否读了ST2。AK8963一次数据读取是HXL、HXH、HYL、HYH、HZL、HZH、ST2,少读ST2会导致下一次数据不更新。这个坑很隐蔽,现象是第一次有数,后面一直不变。

4.4 现象:F1上跑DMP进HardFault

现象是初始化DMP后一调度就进HardFault,或者卡在while(1)。原因可能是F1的RAM比F4小,DMP固件和栈叠在一起;也可能是DMP固件数组的地址对齐不满足,或者中断优先级里调用了浮点。F1没有FPU,DMP中断里如果做了浮点运算,会走软件浮点,栈开销变大。先看.map文件里DMP相关段和栈顶的距离,栈至少留1KB以上做验证。

解决:先关DMP,只用原始数据读通;再逐步打开DMP,每次只加一个中断源。DMP固件写入前确认目标地址对齐,写完后回读校验。中断里只置标志,姿态解算放主循环。HardFault时用栈回溯或打印LR/PC,比猜有效。F1上如果Flash紧张,先裁掉没用到的传感器分支,不要先裁栈。

4.5 现象:串口有数据但姿态角漂移

现象是原始数据看起来正常,姿态角几分钟漂几度。原因通常是零偏没标定、DLPF带宽太宽、融合周期和采样率不匹配。F1上主循环慢,如果融合周期按F4的5ms写,实际可能变成8ms,积分项就会偏。先打印每次融合的时间戳,确认周期稳定。加速度计和陀螺仪的零偏在静止时采样1000点求均值,上电后扣除。

解决:把DLPF设到44Hz左右,采样率降到100Hz或50Hz,融合周期用定时器中断给。磁力计数据要先做硬铁校准,至少做平面旋转取min/max。不要用串口每帧都打印浮点,F1上printf很慢,会拖累周期;改成二进制帧或降低打印频率。

5. 让MPU9250在F1上稳定输出:采样率、量程与DMP取舍

读通寄存器只是开始,F1上要稳定输出,得把采样率、DLPF、量程和融合周期一起定下来。MPU9250的陀螺仪输出率默认1kHz,SMPLRT_DIV决定分频,CONFIG的DLPF决定带宽。采样率越高,F1的中断和打印压力越大;采样率太低,姿态融合会滞后。我的习惯是先定100Hz,跑稳后再试200Hz,不要一上来就1kHz。

5.1 采样率与DLPF:50Hz和200Hz的配置差异

SMPLRT_DIV寄存器0x19,采样率=陀螺输出率/(1+DIV)。DLPF关掉时陀螺输出率8kHz,开DLPF时1kHz。常见配置:100Hz采样、DLPF=3(约44Hz带宽),DIV=9;200Hz采样、DLPF=3,DIV=4;50Hz采样、DLPF=4(约21Hz带宽),DIV=19。DLPF越低,噪声越小,但延迟越大。F1做平衡或慢速姿态,50Hz~100Hz够用;做快速动作,200Hz更合适。

目标采样率SMPLRT_DIV(0x19)CONFIG(0x1A)说明
50Hz190x04带宽约21Hz,噪声低
100Hz90x03常用折中
200Hz40x03注意F1主循环负载
500Hz10x02串口打印要降频

配置顺序:先写0x6B退出睡眠,再写0x19、0x1A、0x1B、0x1C,最后写0x6B选时钟源。每次写完延时几毫秒,别连续写不等待。读数据时先读0x3B开始的14字节,加速度6字节、温度2字节、陀螺6字节,一次读完保证同一采样点。

5.2 加速度计/陀螺仪零偏标定:静止采样与均值扣除

零偏标定不复杂,但要在静止、水平、温度稳定后做。上电后延时1秒,连续读1000组原始值,去掉前100组,剩下的求平均。加速度计Z轴减去16384(±2g量程),X/Y减0;陀螺仪三轴直接减均值。标定值存到Flash或每次上电重算,产品里一般存Flash,调试时每次重算方便。

typedef struct { int32_t ax, ay, az; int32_t gx, gy, gz; } bias_t; void calibrate_gyro(bias_t *b) { int32_t sx = 0, sy = 0, sz = 0; int16_t ax, ay, az, gx, gy, gz; for (int i = 0; i < 1000; i++) { mpu9250_read_raw(&ax, &ay, &az, &gx, &gy, &gz); if (i >= 100) { sx += gx; sy += gy; sz += gz; } delay_ms(2); } b->gx = sx / 900; /* 900组有效样本 */ b->gy = sy / 900; b->gz = sz / 900; }

这段代码里delay_ms(2)对应约500Hz采样,1000组约2秒。除以900是因为去掉了前100组。实际用的时候把b->gx等存起来,后面读陀螺减掉。加速度计零偏同理,但Z轴要减重力。注意量程变了灵敏度要跟着变,±4g时Z轴约8192,不能还用16384。

5.3 DMP在F1上的资源账:Flash、RAM和中断频率

DMP的好处是输出四元数,F1不用跑浮点融合;坏处是占Flash和RAM,固件加载和中断调试麻烦。F1常见64KB~128KB Flash、20KB RAM,DMP固件和栈要提前算账。先编译一个不开DMP的原始数据版本,看.map里Flash和RAM占用;再开DMP,对比增量。如果RAM只剩不到2KB,栈很容易在中断嵌套时溢出。

中断频率也要算。DMP输出率配100Hz,中断里只置标志,主循环读FIFO。不要在中断里printf,也不要浮点。F1的NVIC优先级分组先设好,DMP中断优先级低于串口接收,避免丢数据。如果DMP加载后HardFault,先查固件写入地址是否落在合法RAM区,再查栈大小。

5.4 串口验证帧:原始数据与姿态角对照

调试时不要只打印姿态角,要把原始六轴和姿态角放同一帧,才能判断是传感器问题还是融合问题。帧头用0xAA 0x55,后面跟ax,ay,az,gx,gy,gz的int16,再跟roll,pitch,yaw的int16(放大100倍),最后加校验和。F1串口波特率115200,100Hz帧约2KB/s,够用。

typedef struct { uint8_t head[2]; int16_t acc[3]; int16_t gyro[3]; int16_t angle[3]; /* 度×100 */ uint8_t checksum; } debug_frame_t; uint8_t frame_checksum(const uint8_t *p, uint16_t len) { uint8_t sum = 0; while (len--) sum += *p++; return sum; }

发送前把结构体按字节打包,注意F1是小端,上位机按小端解析。angle放大100倍避免浮点打印。校验和覆盖帧头到角度,接收端算完对比。这个帧格式不复杂,但能让你在串口助手里一眼看出哪轴异常,比只打印“初始化成功”有用。

6. 验证移植是否干净:用寄存器快照和回环测试收尾

移植完成后别急着写“完工”。我一般做三步验证:第一步,寄存器快照,把0x75、0x00、0x6B、0x19、0x1A、0x1B、0x1C、0x37读一遍,和写入值对照;第二步,总线回环,用软件I2C和硬件I2C读同一个寄存器,值必须一致;第三步,示波器看SCL/SDA,确认100kHz周期、上升沿和ACK位。三步都过,才认为链路干净。

验证项通过标准失败先看
WHO_AM_I快照0x71和0x48稳定地址、旁路、上拉
配置回读写入值与回读一致写函数是否发STOP
双总线对比软硬I2C结果一致硬件时序、BUSY
波形100kHz、ACK正常上拉、时钟源
连续读1000帧无错帧缓冲区、校验

寄存器快照建议做成一个函数,每次上电打印一次。配置回读时注意有些寄存器只读或自清零,别把不可写位当成错误。双总线对比很能说明问题:软件I2C能读、硬件I2C不能读,基本就是时序或BUSY恢复;两边都不能读,查硬件。连续读1000帧看校验错误率,错误率高先降采样率或关DMA。

我自己的习惯是保留一个“最小读寄存器”函数,永远不删。每次改时钟、改引脚、改库,先跑它读0x75。这个函数通过,再谈DMP和姿态。移植F1的后悔药不多,提前留一个能定位到阶段的返回值,比事后猜强。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询