经典三问
Q: MPU6050 是什么?
A: 一颗把三轴加速度和三轴角速度测出来、通过 I2C 交给单片机的六轴惯性传感器。
Q: 为什么要读它的原始数据?
A: 角度是算出来的,寄存器里的数才是证据。安装方向、量程、零偏里任何一处变了,屏幕上那个角度都会跟着变;手里没有原始数据,就分不清是哪一处变了。
Q: 常见使用场景?
A: 车头朝向、倾角检测、云台稳定、碰撞与跌倒检测、运动记录。
八针的 MPU6050 模块平放在桌上,接四根线,烧进一段程序。串口那边第一眼看到的是一串整数,头六个是原始值:az 是个两千上下的正数,ax 和 ay 在几十到几百之间来回晃。
把模块一侧垫高,ax 开始顺着一个方向长,az 跟着变小。手指敲一下桌面,六个数字同时跳出一个尖峰。
这些就是原始数据,是十六位有符号整数,不是角度,也不是"每秒多少度"。从这堆整数走到屏幕上能读的倾角,中间要做四件事:把芯片叫醒、把量程说清楚、把十四个字节一次搬回来、再决定每一个数字除以多少。
八根针,先只接四根
模块排针上的八个名字是 VCC、GND、SCL、SDA、XDA、XCL、AD0、INT。第一次看见很容易觉得每一根都要接上,其实先接四根就够:
text
STM32 3.3V ---- MPU6050 VCC
STM32 GND ---- MPU6050 GND
STM32 PB8 ---- MPU6050 SCL
STM32 PB9 ---- MPU6050 SDA
剩下四根先放着,各自是什么先说清:
XDA、XCL是芯片内部那个辅助 I2C 主机的两根线,用来让 MPU6050 自己去读一块外部磁力计。这里用不到。AD0决定器件地址的最低位。接地时地址是0x68,接高电平是0x69。同一条总线上要挂两块 MPU6050 时它才有用;一块的时候直接接地。INT是一根输出线,芯片用它告诉单片机"新数据好了"。这里不接,靠节拍定时去读;什么时候非它不可,读十四个字节那一节会说。
PB8、PB9 正是 OLED 用的那两根。这不是冲突,是 I2C 本来的用法 :一条总线上可以挂很多设备,每个设备认自己的地址------OLED 认 0x3C,MPU6050 认 0x68,谁也不理别人。两根线怎么被拉高、模块上的上拉电阻并联之后上升沿更快而灌入电流更大,讲两根线那一回已经说过;手上这块模块写的是多少,以它的说明为准。
供电也按模块说话。芯片本体的工作电压是 2.375V 到 3.46V(产品规格书里的电气参数),所以带稳压的模块可以接 5V,不带稳压的只能给 3.3V。
那几个时序函数不用重写
讲两根线那一回已经把软件 I2C 的时序写全了,这里接着用同一份:SoftI2C_Init() 把 PB8、PB9 配成开漏输出并置空闲,SoftI2C_Start() 和 SoftI2C_Stop() 产生起始和停止条件,SoftI2C_SendByte() 发 8 位并在第 9 拍收回执,SoftI2C_ReadByte() 读 8 位、参数传 1 就回一个应答。它们和芯片没有关系,只和线上那几拍有关系------接第三块、第四块设备时,这几行一个字都不用改。
总线上的第二个地址:先读身份证
MPU6050 不认命令,它认寄存器。给它一个字节的寄存器地址,它就按那个寄存器的规矩收或者给数据。既然总线上一堆设备各认各的地址,第一件事就是确认"我喊的这个地址上,坐着的确实是 MPU6050"。
WHO_AM_I(地址 0x75)就是干这个的,它的复位值固定是 0x68(器件手册的寄存器表里写着)。
读一个寄存器是两段传输拼起来的:先按写的方式告诉它要读哪个寄存器,再用一次重复起始换成读的方向,把数据收回来。收最后一个字节的时候要回一个 NACK,这是告诉对方"我够了,放开数据线",否则它会继续送下一个寄存器 ------ 讲两根线那一回第 9 拍那条规矩,在这里第一次真正用上。
c
/* ---------- 寄存器地址:这里用到的部分 ---------- */
#define MPU6050_ADDRESS 0xD0U /* 0x68 左移一位,末位 0 表示写;读地址是 0xD1 */
#define MPU6050_WHO_AM_I 0x75U
#define MPU6050_PWR_MGMT_1 0x6BU
#define MPU6050_SMPLRT_DIV 0x19U
#define MPU6050_CONFIG 0x1AU
#define MPU6050_GYRO_CONFIG 0x1BU
#define MPU6050_ACCEL_CONFIG 0x1CU
#define MPU6050_ACCEL_XOUT_H 0x3BU
/* 往一个寄存器里写一个字节 */
void MPU6050_WriteReg(uint8_t reg, uint8_t value)
{
SoftI2C_Start();
SoftI2C_SendByte(MPU6050_ADDRESS);
SoftI2C_SendByte(reg);
SoftI2C_SendByte(value);
SoftI2C_Stop();
}
/* 从一个寄存器里读一个字节 */
uint8_t MPU6050_ReadReg(uint8_t reg)
{
uint8_t value;
SoftI2C_Start();
SoftI2C_SendByte(MPU6050_ADDRESS);
SoftI2C_SendByte(reg);
SoftI2C_Start(); /* 重复起始:方向要换了 */
SoftI2C_SendByte((uint8_t)(MPU6050_ADDRESS | 0x01U));
value = SoftI2C_ReadByte(0U); /* 只读一个字节,回 NACK */
SoftI2C_Stop();
return value;
}
地址 0xD0 是 0x68 左移一位、末位留 0;要读的时候把末位置 1,就成了 0xD1。这一点写错,现象和错地址一模一样:没有回执。
把它叫醒:为什么写 0x01 而不是 0x00
PWR_MGMT_1(0x6B)管供电模式和时钟源,上电复位值是 0x40 ,也就是 SLEEP 那一位是 1------芯片出厂状态是睡着的。不管这一位,后面所有寄存器都会读到不动的一堆数,或者干脆没有回执。
把它写成 0x00 也能唤醒:低三位是时钟源选择,0 表示用芯片内部那个 8MHz 振荡器。但手册在这一段里写得很直接:上电默认用内部振荡器,强烈建议改用某个陀螺仪轴(或者外部时钟)作为时钟基准,稳定性更好 。所以这里写 0x01,也就是"唤醒,并且拿 X 轴陀螺仪做时钟基准"。
c
/* 初始化:返回 1 表示这块芯片在,返回 0 表示没找到 */
uint8_t MPU6050_Init(void)
{
SoftI2C_Init();
if (MPU6050_ReadReg(MPU6050_WHO_AM_I) != 0x68U)
{
return 0U; /* 身份对不上,后面的寄存器不用动 */
}
MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01U); /* 唤醒 + 以 X 轴陀螺仪为时钟基准 */
Delay_ms(50); /* 等时钟稳下来:延时 50 毫秒 */
MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09U); /* 采样率 = 1000 / (1 + 9) = 100Hz */
MPU6050_WriteReg(MPU6050_CONFIG, 0x03U); /* 数字低通:陀螺仪 42Hz、加速度 44Hz */
MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18U); /* FS_SEL = 3:±2000°/s */
MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18U); /* AFS_SEL = 3:±16g */
return 1U;
}
Delay_ms() 是前面用过很多次的毫秒延时函数,这里用来等时钟稳定。这个等待有依据:把时钟基准换成 PLL 之后,它需要一点时间才锁得住,手册的电气参数里写着,时钟源选 1、2、3(也就是选陀螺仪的某个轴)时,PLL 的稳定时间是 1 到 10 毫秒。留 50 毫秒是把这个上限放宽了几倍,让上电、时钟切换这些都落定之后再开始读写。开机那一下不等,第一批读数的可信度就只能靠运气。
SMPLRT_DIV 那一行要算一遍。芯片内部的陀螺仪采样快得多,这个寄存器是给它做分频的:采样率 = 陀螺仪输出速率 ÷ (1 + 分频值) ,而陀螺仪输出速率在低通滤波器打开时是 1kHz、关掉时是 8kHz。这里低通开着(CONFIG 写了 0x03),所以 1000 ÷ 10 正好 100Hz ------ 和后面每 10 毫秒读一次刚好对上:读得太快会把同一个采样点读两遍,读得太慢,中间那段运动就直接丢了。
量程回答的是"能测多大"
GYRO_CONFIG 和 ACCEL_CONFIG 里各有两位选量程。写 0x18 就是二进制 0001 1000,两位都落在 11 上,也就是两边的最大档:
| 加速度量程 | 灵敏度 | 陀螺仪量程 | 灵敏度 |
|---|---|---|---|
| ±2g | 16384 LSB/g | ±250°/s | 131 LSB/(°/s) |
| ±4g | 8192 LSB/g | ±500°/s | 65.5 LSB/(°/s) |
| ±8g | 4096 LSB/g | ±1000°/s | 32.8 LSB/(°/s) |
| ±16g | 2048 LSB/g | ±2000°/s | 16.4 LSB/(°/s) |
这张表里的数字全部来自器件手册,它们也是后面所有换算式里唯一每次都要翻看的东西。
量程不是精度,是"能测多大"。 选小档,同样的物理量占用的数字更大、看着更细;但一旦超过量程,读数就被切在最大值上不动了------你想看的那段运动正好在切口外面。小车这边的情况是:电机一转,车架跟着抖;轮子卡住、车头撞上桌腿的那一下,几个 g 的冲击很常见。所以这里取最大档。
代价要认下来:±16g 这一档下,平放时的重力读数只有 2048 左右。这不是传感器坏了,也不是校准没做,而是"1g 在这把尺子上只有这么长"。换到 ±2g 那一档,同一个姿势会读到一万六千多;两个数字讲的是同一件事,除以的常数不一样。
十四个字节,一次读完
数据从 0x3B 开始,连续排着七组、每组两个字节,每组都是高字节在前:
text
0x3B 0x3C 加速度 X 0x41 0x42 温度
0x3D 0x3E 加速度 Y 0x43 0x44 陀螺仪 X
0x3F 0x40 加速度 Z 0x45 0x46 陀螺仪 Y
0x47 0x48 陀螺仪 Z
十四这个数字不能改,温度也不能跳过去。 它夹在中间,跳过它,后面的陀螺仪三组字节就全部对错位。
读法上有个细节,手册专门用一段讲过:这七个测量寄存器其实是两套------芯片内部有一套一直在按采样率更新,外面能读到的是另一套;只有当总线空闲下来的时候,外面这一套才从里面同步一次 。这条设计的意思是:一次连读的十四个字节,必定来自同一个采样时刻,高低字节不会一个来自这一拍、一个来自下一拍。
一次连读也就这么写出来了:起始、说寄存器地址、重复起始、换方向,然后连着收十四个字节,最后一个回 NACK。
c
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;
} MPU6050_Raw;
/* 从 0x3B 连读 14 个字节,拼成七个有符号数 */
void MPU6050_ReadRaw(MPU6050_Raw *raw)
{
uint8_t buffer[14];
uint8_t i;
SoftI2C_Start();
SoftI2C_SendByte(MPU6050_ADDRESS);
SoftI2C_SendByte(MPU6050_ACCEL_XOUT_H);
SoftI2C_Start();
SoftI2C_SendByte((uint8_t)(MPU6050_ADDRESS | 0x01U));
for (i = 0U; i < 13U; i++)
{
buffer[i] = SoftI2C_ReadByte(1U); /* 前 13 个字节:回 ACK,还要继续 */
}
buffer[13] = SoftI2C_ReadByte(0U); /* 最后一个:回 NACK,我够了 */
SoftI2C_Stop();
raw->accel_x = (int16_t)(((uint16_t)buffer[0] << 8) | buffer[1]);
raw->accel_y = (int16_t)(((uint16_t)buffer[2] << 8) | buffer[3]);
raw->accel_z = (int16_t)(((uint16_t)buffer[4] << 8) | buffer[5]);
raw->temp = (int16_t)(((uint16_t)buffer[6] << 8) | buffer[7]);
raw->gyro_x = (int16_t)(((uint16_t)buffer[8] << 8) | buffer[9]);
raw->gyro_y = (int16_t)(((uint16_t)buffer[10] << 8) | buffer[11]);
raw->gyro_z = (int16_t)(((uint16_t)buffer[12] << 8) | buffer[13]);
}
两个字节拼成一个数:高字节先左移八位,再和低字节"或"到一起,最后当成有符号 的十六位整数。强转那一下不是装饰:0xFFFF 当无符号数是六万多,当有符号数是 -1。倾斜到另一边时,传感器就是从 0 往负数走,没有这一步,屏幕上的数字会在零点处跳到六万多。
不连读会怎样
对照写法是把每个字节当成独立的读操作:
c
/* 对照写法:一个字节一个字节读------不建议 */
raw->accel_x = (int16_t)(((uint16_t)MPU6050_ReadReg(0x3BU) << 8) |
MPU6050_ReadReg(0x3CU));
这样写语法没问题,读出来的数大部分时候也像那么回事。但每一次 MPU6050_ReadReg() 都是一整段传输,中间隔着刷屏、发串口、处理按键;而芯片内部那一套寄存器一直按 100Hz 更新。两次独立的读之间跨过一次更新,拼出来的高低字节就来自两个采样时刻,数值在几个数之间无规律地跳,看着像"噪声特别大"。
手册对这种写法给了一句提醒:不用连读的时候,得靠数据就绪中断 自己保证一组单字节读发生在同一个采样时刻。也就是说:不连读,就得把那根 INT 线接上------这里先不接它,是因为连读已经把这件事解决了。
每个数字除以多少
原始数只是"尺子上的刻度"。要变成 g 和度每秒,除以刚才那张表里的灵敏度:
c
/* 当前量程:加速度 ±16g、陀螺仪 ±2000°/s */
#define MPU6050_ACCEL_LSB_PER_G 2048.0f
#define MPU6050_GYRO_LSB_PER_DPS 16.4f
typedef struct
{
float accel_x_g;
float accel_y_g;
float accel_z_g;
float temperature_c;
float gyro_x_dps;
float gyro_y_dps;
float gyro_z_dps;
} MPU6050_Data;
void MPU6050_Convert(const MPU6050_Raw *raw, MPU6050_Data *data)
{
data->accel_x_g = (float)raw->accel_x / MPU6050_ACCEL_LSB_PER_G;
data->accel_y_g = (float)raw->accel_y / MPU6050_ACCEL_LSB_PER_G;
data->accel_z_g = (float)raw->accel_z / MPU6050_ACCEL_LSB_PER_G;
data->temperature_c = (float)raw->temp / 340.0f + 36.53f;
data->gyro_x_dps = (float)raw->gyro_x / MPU6050_GYRO_LSB_PER_DPS;
data->gyro_y_dps = (float)raw->gyro_y / MPU6050_GYRO_LSB_PER_DPS;
data->gyro_z_dps = (float)raw->gyro_z / MPU6050_GYRO_LSB_PER_DPS;
}
温度那一行的两个数字来自产品规格书:温度通道的灵敏度是 340 LSB/°C,35°C 时偏置是 −521 LSB 。代进去算一遍就得到 原始值 ÷ 340 + 36.53。它不是高精度温度计,但拿来当"芯片是不是在正常工作"的旁证很够用:手指按住芯片停半分钟,这个数会往上走一两度。
改量程的时候,要改的就是这两个宏。这两个常数和寄存器里那两位是同一件事的两半,只改一处,屏幕上的数就会差 8 倍。
平放时那个数就是重力
加速度计测的是"它自己感受到的加速度",静止的时候感受到的就是重力。这一点带来一个很好用的自检:不管模块摆成什么姿势,三个加速度的平方和开根号都应该接近 1g。
c
#include <math.h>
/* 加速度向量的长度,静止时应该接近 1 */
float MPU6050_AccelNorm(const MPU6050_Data *data)
{
return sqrtf((data->accel_x_g * data->accel_x_g) +
(data->accel_y_g * data->accel_y_g) +
(data->accel_z_g * data->accel_z_g));
}
F103 里没有硬件浮点单元,开方和三角函数都由软件算出来,比一条整数加法慢得多。放在 200ms 一次的显示任务里不心疼,但它不该出现在中断里------中断只负责搬运,计算留给主循环。
自检怎么做:
text
平放在桌面 三个轴读数分别是多少?(±16g 档下 az 应该在 2048 附近)
平方和开根号 是不是接近 1.000?
把一侧垫高一厘米 哪个轴变了?变多还是变少?
绕垂直轴转一圈 哪几个轴在变?哪几个几乎不动?
前两行是关键:模长明显偏离 1,是传感器自己的偏差;模长对得上而 az 偏小,是模块本身没放平。 这两个结论完全不同,一个属于另一场要单独做的实验,一个只要把桌子垫平。有了这个自检,就不会把"桌子歪了"当成"传感器不准",然后开始乱改代码。
做这个自检之前还有一个前提:模块要固定住。用手按着、让杜邦线吊着它,手和线的抖动会一路传进芯片,模长会跟着晃,量出来的结果不作数。
有了重力,就有两个角
平放的模块绕水平轴一倾,重力就从一个轴分到另一个轴上,分量之间的比例就确定了倾角。用两个反正切就能把它们取出来:
c
/* 由加速度算俯仰角和横滚角,单位:度 */
void MPU6050_GetPitchRoll(const MPU6050_Data *data, float *pitch, float *roll)
{
float ax = data->accel_x_g;
float ay = data->accel_y_g;
float az = data->accel_z_g;
*pitch = atan2f(-ax, sqrtf((ay * ay) + (az * az))) * 57.29578f;
*roll = atan2f(ay, az) * 57.29578f;
}
57.29578 是 180 除以 π,把弧度换成度。俯仰角定义成"绕 Y 轴转了多少",横滚角是"绕 X 轴转了多少"。俯仰角被限制在 −90 到 90 度 之间------它的第二个参数是平方和开根号,恒不为负,所以不可能超过 90;横滚角走的是 atan2(ay, az),az 一翻到背面就变负,它能一直走到 ±180 度------把模块翻到背面,横滚角显示的就是 ±180。
为什么这里用反正切,而不是直接拿 ax 除以 az 再查表?因为那个比值在分母接近 0 的时候没法用 ,而反正切把两个分量一起看,模块转到任何姿态都不会得到无穷大。这一点在模块接近竖直的时候最直观:az 掉到几十,ax 接近 1g,ax ÷ az 成了一个几千的数,比值算法开始乱跳,反正切给出的数还是稳的。
慢慢倾斜模块,屏幕上这两个数跟着走;放回桌面,它们回到 0 附近。
第三个角不在这两个公式里
屏幕上可以放三行,第三行是偏航角。但只用加速度,偏航角根本算不出来 :模块绕垂直轴转一圈,重力的方向相对芯片没有任何变化,三个加速度读数从头到尾一模一样。这不是公式没找对,是信息本身不在这里。
能给出这件事的只有陀螺仪:它测的是"转得多快",绕垂直轴的那个轴就对应偏航的角速度。要把它变成角度,就得把角速度按时间累加------每一小段乘上这一小段的时间,再往前加。这条路要另说一件事:累加用的那个数,静止时也不是 0。
所以这里的屏幕上先放两行角度加一行温度。
芯片里的第一道滤波
CONFIG(0x1A)里有个数字低通滤波器,两端都能用:陀螺仪和加速度的信号先过它,再进数据寄存器。低三位 DLPF_CFG 选档:
DLPF_CFG |
陀螺仪带宽 | 加速度带宽 | 延迟 |
|---|---|---|---|
| 0 | 256Hz | 260Hz | 0.98ms |
| 1 | 188Hz | 184Hz | 1.9ms |
| 2 | 98Hz | 94Hz | 2.8ms |
| 3 | 42Hz | 44Hz | 4.8ms |
| 4 | 20Hz | 21Hz | 8.3ms |
| 5 | 10Hz | 10Hz | 13.4ms |
| 6 | 5Hz | 5Hz | 18.6ms |
这张表的读法是:带宽越小,抖动越少,但延迟越大。 带宽那一列就是它放行的变化有多快:256Hz 这一档几乎什么都放进来,桌面的一点振动都能在数字上看见;5Hz 这一档把大部分抖动挡在外面,代价是任何一个真实动作都要过 18 毫秒才看得清。
它也是芯片里的第一道滤波,发生在我们的代码之前 :带宽和延迟都在这个字节上定下来,代码里再写的任何滤波都是在它之后又叠一层。参数调了半天没效果的时候,先回头看一眼 CONFIG 里写的是哪一档。
另外有一处容易想岔:ACCEL_CONFIG 里那三位高通滤波(ACCEL_HPF)不作用在数据寄存器上 ,它只接给芯片内部的运动检测和自由落体检测------手册在那一节写得很清楚,高通滤波的输出不提供给数据寄存器。想让数据干净,要么动 CONFIG,要么在代码里自己做。
四个故障,改一行就能复现
陀螺仪整组错位,但数字还像那么回事。 把拼装那一段里的温度那一行删掉,让 gyro_x 从 buffer[6]、buffer[7] 开始拼,也就是当作"温度不存在"来解释这十四个字节。转一下模块:gx 几乎不动,它是一个几千上下的负数(室温下温度的原始值就长这样);绕 X 轴转,跟着变的是 gy;绕 Y 轴转,跟着变的是 gz;真正绕 Z 轴的那个角速度读回来了却没被用上。温度占着 0x41、0x42 两个地址,跳过它,后面每一组都往前挪一组。 把温度那一行加回去,三个轴立刻各就各位。
只有一个轴坏掉。 把连读的循环从 14 改成 13,最后一个字节没读回来,它的值是数组里剩下来的东西。表现是三个角速度里有一个每次都在乱跳,另两个完全正常。这一条最容易被误判成"这个轴坏了":芯片好好的,是少读了一个字节。
同一个动作,数字差了 4 倍。 把 ACCEL_CONFIG 从 0x18 改成 0x08(量程从 ±16g 换到 ±4g),而换算常数没动。把模块平放:az 从两千附近变成八千多,用 ÷2048 一算,重力就成了 4g。寄存器那两位和代码里那两个宏必须一起改------它们是一件事的两半。
数字抖得比以前厉害,读数的节奏却没变。 把 CONFIG 从 0x03 改成 0x00,带宽从 44Hz 一下子张到 260Hz,模块平放在桌上不动,读数的摆动幅度能看出一档与另一档的差别。这一改还会顺带动另一个数:低通关掉以后陀螺仪的输出速率从 1kHz 变成 8kHz,采样率跟着从 100Hz 变成 800Hz------但读的节奏仍旧是每 10 毫秒一次,所以屏幕上只是数字更毛,不会更快。想清楚哪些是采样率的事、哪些是带宽的事,这一条就没白做。
一份完整的程序
把上面几段接起来,主循环的形状和前面那些任务一样:短的节拍负责读,慢一点的节拍负责显示。
c
#include "stm32f10x.h"
#include <math.h>
#include <stdio.h>
static MPU6050_Raw g_raw;
static MPU6050_Data g_data;
static uint8_t g_online = 0U;
/* 屏幕上三行:两个角度用整数,温度用一位小数 */
void MPU6050_ShowOnOled(const MPU6050_Data *data, uint8_t online)
{
float pitch;
float roll;
MPU6050_GetPitchRoll(data, &pitch, &roll);
OLED_ShowString(0, 0, "MPU6050", OLED_8X16);
OLED_ShowString(0, 16, "P:", OLED_8X16);
OLED_ShowSignedNum(24, 16, (int32_t)pitch, 3, OLED_8X16);
OLED_ShowString(64, 16, "R:", OLED_8X16);
OLED_ShowSignedNum(88, 16, (int32_t)roll, 3, OLED_8X16);
OLED_ShowString(0, 32, "T:", OLED_8X16);
OLED_ShowFloatNum(24, 32, (double)data->temperature_c, 2, 1, OLED_8X16);
OLED_ShowString(0, 48, (online != 0U) ? "online" : "offline", OLED_8X16);
OLED_Update();
}
几点说明:
OLED_ShowString、OLED_ShowSignedNum、OLED_ShowFloatNum就是那块 OLED 驱动程序里的三个显示函数:写字符串、写带符号的整数、写带小数的数,参数里带上字体大小OLED_8X16。- 角度那一行用带符号的整数函数,温度那一行用小数函数,两个函数的分工本来就不一样。 带符号的那个写整数,负号自己占一格;小数那个会先写一个
+或-号,再把整数部分、小数点、小数部分依次写出来------所以温度那一行显示的是+26.5,留给它的位置也要多算一格符号的宽度。角度取整数就够了,三位数放得下俯仰的 ±90;横滚能到 ±180,-180连负号是四个字符,位置从 88 排到 120,还没出屏。 - 最后那一行写的是开机时的身份检查结果,
MPU6050_Init()返回 1 才是online。
串口那一行也放在显示节拍里,一行看全原始值、角度和模长。小数全部乘成整数再发:用 %f 在老版本工具链配微库时会打不出正常结果,前面把 printf 接到串口那一回已经交代过;乘 100 发整数、收的时候当百分之一读,到哪都好使。
c
int main(void)
{
uint32_t read_last_ms = 0U;
uint32_t show_last_ms = 0U;
OLED_Init();
g_online = MPU6050_Init();
while (1)
{
/* 每 10 毫秒读一次:和芯片 100Hz 的采样率对上 */
if ((uint32_t)(tick_ms - read_last_ms) >= 10U)
{
read_last_ms = tick_ms;
MPU6050_ReadRaw(&g_raw);
MPU6050_Convert(&g_raw, &g_data);
}
/* 每 200 毫秒显示一次:整屏推一次要花几十毫秒,别跟着读数一起刷 */
if ((uint32_t)(tick_ms - show_last_ms) >= 200U)
{
float pitch;
float roll;
float norm;
show_last_ms = tick_ms;
MPU6050_GetPitchRoll(&g_data, &pitch, &roll);
norm = MPU6050_AccelNorm(&g_data);
MPU6050_ShowOnOled(&g_data, g_online);
printf("ax=%d ay=%d az=%d gx=%d gy=%d gz=%d T=%d P=%d R=%d N=%d\r\n",
g_raw.accel_x, g_raw.accel_y, g_raw.accel_z,
g_raw.gyro_x, g_raw.gyro_y, g_raw.gyro_z, g_raw.temp,
(int32_t)(pitch * 100.0f), (int32_t)(roll * 100.0f),
(int32_t)(norm * 1000.0f));
}
}
}
tick_ms 是那条每毫秒加一的时间轴,OLED_Init() 是那块屏的初始化函数,两样前面都已经有了。
两个节拍分开是有原因的:读一次传感器,线上要来回十七个字节(地址、寄存器地址、重复起始、换方向后那个读地址,再加十四字节数据),按 150kHz 那个速率算大约一毫秒;而往屏幕上推一整屏要几十毫秒,软件 I2C 那一档六十毫秒上下。把它们绑在一起,读数就会被刷屏拖着走,采样间隔变得不均匀------屏幕上看着只是数字有点跳,实际是采样的时间点乱了。分开之后,读数永远是 10 毫秒一次,屏幕慢一点没人介意。
到这一步,屏幕上那两个会跟着手变的数,从哪来、除了多少、为什么只有两个,都能答上来了。手里有原始数据,才谈得上后面的事;而那片屏幕现在缺的那一行偏航角,它需要的不是公式,是另一套东西:把角速度按时间累起来------累加这条路要处理的事,和刚才这两个角完全不是一回事。