☰
MPU6500六轴IMU驱动与姿态解算工程实践
2026/10/24 4:38:53 网站建设 项目流程

简介:MPU6500驱动算法是一套面向六轴IMU开发者的驱动与姿态解算参考资料,围绕加速度计、陀螺仪的数据采集与处理展开,适合消费电子、机器人、无人机及物联网运动控制领域的嵌入式开发者,用于解决传感器初始化、原始数据校准、多传感融合与姿态输出等问题。压缩包共157个文件、约730KB,以C源文件和头文件为主体,配合Makefile、工程配置、编译中间产物及少量Python脚本与说明文档,形成较完整的嵌入式工程结构。已有1043人学习下载。资料重点呈现DMP运动驱动、底层寄存器访问和测试入口等模块,读者可据此梳理从配置寄存器、读取原始数据到互补滤波、卡尔曼或Madgwick融合,再到欧拉角与四元数解算的链路,并理解万向锁与连续旋转场景下的表示差异,为移植调试提供可复用的代码组织思路。

1. 一颗六轴 IMU 的驱动,难点从来不在“把数读出来”

很多开发者第一次写 MPU6500 驱动,心里都有一个默认前提:初始化几个寄存器,然后从 0x3B 开始连读 14 个字节,换算成 g 和 °/s,任务就完成了。等真正把它装到设备上,问题才开始出现——同型号换一块板子数据就飘,静止放着航向角自己缓慢旋转,设备一动加速度传感器算出来的倾角就抖得像噪声。原因在于:驱动算法真正处理的从来不是“读寄存器”,而是在噪声、零偏和时间之间反复做取舍。零点几度每秒的残余零偏,积分一分钟就是几十度;一个没算对的采样间隔,能让所有滤波器的系数失效。这些取舍决定了后面姿态解算到底能不能用,也决定了一份驱动代码能长期稳定运行,还是只能勉强跑通。

2. 寄存器地图:配置顺序本身就是一份时序约束

大多数人把寄存器配置理解成“往一张表里填值”,写完了事。实际上这些寄存器之间存在依赖关系:复位后必须等,等待结束后才能改时钟源,改了时钟源 DLPF 的带宽表才成立,带宽定了采样率分频才有意义。顺序错一步,不会报错,只会让后面的数据看起来“有点怪”。

2.1 WHO_AM_I 与复位:为什么第一件事是等

驱动入口第一步永远是读身份寄存器。MPU6500 的身份寄存器在 0x75,正常应返回 0x70。这一步不是形式主义:SPI 接线虚焊、CS 相位错、器件其实没上电,都会在这里暴露成 0x00 或 0xFF。如果跳过它直接读数据,后面所有异常都会混在一起,排查成本瞬间翻倍。

身份确认后是软复位:往 0x6B 写 0x80,把 DEVICE_RESET 位置 1。手册给出的稳定时间是 100 ms 量级,这一百毫秒不能省。复位后芯片默认进入睡眠态并且时钟源切到内部 RC 振荡器,必须再写一次 0x6B 把 SLEEP 清掉。这里有个容易被忽略的细节:时钟源选择位 CLKSEL 建议配置成 1,也就是用 X 轴陀螺的 PLL 作为参考时钟。内部 RC 振荡器的频率会随温度漂移,它漂多少,采样周期就偏多少,积分出来的角度就跟着偏——这种误差非常隐蔽,因为它不会让数据“看起来不对”,只会让长时间积分的结果慢慢偏掉。

提示:软复位之后不要立刻读数据寄存器,也不要紧接着写配置,两帧 SPI 之间留出足够间隔。曾经遇到过主控 SPI 跑到几兆赫兹,复位命令刚发完就发下一条,配置里有一半写了进去、一半没写,现象是“陀螺正常、加速度计一直满量程”,找了很久。

2.2 DLPF 与采样率分频:两个寄存器,一个带宽

配置寄存器 0x1A 的低三位是 DLPF_CFG,它决定芯片内部的数字低通滤波带宽;0x19 是 SMPLRT_DIV,决定输出采样率。很多人只调后者,因为它直观——“我要 100 Hz,那就把分频设成对应值”。但分频只是抽取,它不做抗混叠。信号里高于新采样率一半的频率分量,会被原封不动地折叠进低频,伪装成“低频漂移”。

举个例子:设备上有个 190 Hz 的振动源(电机、风扇、结构共振都可能),采样率设成 200 Hz,Nyquist 是 100 Hz,190 Hz 会折叠到 10 Hz。你看到的是一条 10 Hz 的“慢晃”,用任何软件滤波都滤不掉,因为它已经变成了真实信号的一部分。DLPF 存在的意义就是在抽取之前先把这个带外能量削掉。

DLPF_CFG加速度计带宽陀螺带宽陀螺延迟陀螺输出率
0460 Hz250 Hz0.97 ms8 kHz
1184 Hz184 Hz2.9 ms1 kHz
292 Hz92 Hz3.9 ms1 kHz
341 Hz41 Hz5.9 ms1 kHz
420 Hz20 Hz9.9 ms1 kHz
510 Hz10 Hz17.85 ms1 kHz
65 Hz5 Hz33.48 ms1 kHz

这张表最该看的是最后一列。DLPF_CFG 为 0 时陀螺内部输出率是 8 kHz,此时 SMPLRT_DIV 的分母基数是 8000;一旦打开任意一档滤波,基数就变成 1 kHz。也就是说,改 DLPF 会顺带改掉采样率的计算基准。这属于典型的“配置耦合”——两个寄存器写在不同函数里,改一个另一个就失效了。实际工程里我习惯把这两个值打包成一个结构体,一次算完再一次性下发。

2.3 量程与灵敏度:加速度传感器的 LSB 怎么变成角度误差

量程寄存器 0x1B(陀螺)和 0x1C(加速度计)决定的是量化和饱和之间的平衡点。量程越小,单个 LSB 代表的物理量越小,分辨率越高;但一旦超过量程,输出就会削顶,姿态瞬间跳变且无法恢复。

量程灵敏度1 LSB 对应
陀螺 ±250 °/s131 LSB/(°/s)0.0076 °/s
陀螺 ±500 °/s65.5 LSB/(°/s)0.0153 °/s
陀螺 ±1000 °/s32.8 LSB/(°/s)0.0305 °/s
陀螺 ±2000 °/s16.4 LSB/(°/s)0.0610 °/s
加速度 ±2 g16384 LSB/g61 µg
加速度 ±4 g8192 LSB/g122 µg
加速度 ±8 g4096 LSB/g244 µg
加速度 ±16 g2048 LSB/g488 µg

很多人下意识选最大量程“保险”,这是个反直觉的坑。加速度量程从 ±2 g 换到 ±16 g,分辨率直接掉 8 倍。而加速度传感器在姿态解算里的作用是提供长期基准,它对低频误差极其敏感——0.01 g 的偏差,在静止倾角计算里就是约 0.57° 的固定偏差,而且这个偏差会被互补滤波或 Mahony 当成“真值”持续吸收进去。所以量程的选择逻辑应该是:先用实测数据统计运动过程中的峰值加速度,再留 1.5~2 倍余量,而不是直接拉到顶。

还有一个更隐蔽的点:表里的灵敏度是标称值,实际器件存在百分之几的增益误差和零点偏置。这不是精度问题,是系统误差,必须靠标定消掉,后面第 4 章会展开。

上电初始化时,需要按顺序配置这些寄存器,顺序本身也有约束。

/* MPU6500 上电初始化:每一步都在等前一步稳定 */ static int mpu6500_init(spi_dev_t *dev) { uint8_t who = 0; /* 1. 先确认身份,读不到 0x70 就说明链路有问题,不必往下走 */ if (spi_read_reg(dev, 0x75, &who) || who != 0x70) return -1; /* 2. 软复位,之后留足启动时间 */ spi_write_reg(dev, 0x6B, 0x80); delay_ms(100); /* 3. 唤醒并切到陀螺 PLL 时钟:内部 RC 温漂大,会让零偏随温度跑 */ spi_write_reg(dev, 0x6B, 0x01); /* 4. 陀螺 ±500 dps(FY_SEL=1),加速度计 ±4 g(AFS_SEL=1) */ spi_write_reg(dev, 0x1B, 0x08); spi_write_reg(dev, 0x1C, 0x08); /* 5. DLPF_CFG = 3:陀螺与加速度计带宽都是 41 Hz,延迟约 6 ms * 注意这一写会把陀螺内部输出率从 8 kHz 切到 1 kHz */ spi_write_reg(dev, 0x1A, 0x03); /* 6. 采样率 = 1000 / (1 + 4) = 200 Hz,必须在第 5 步之后算 */ spi_write_reg(dev, 0x19, 0x04); return 0; }

这段代码里真正值得说的是第 5 步和第 6 步的先后关系。如果把采样率写在 DLPF 之前,分母基数还是 8000,算出来的实际采样率会是 1600 Hz,比预期快 8 倍——数据不会报错,只是滤波器的时间常数全部对不上,表现为“滤波效果和仿真对不上”。第 3 步的时钟源选择同样不是可选项:用内部振荡器时,采样周期本身就带温漂,任何基于固定 dt 的积分都会引入缓慢累积的误差。

3. 数据通路:SPI 突发读、FIFO 与补码对齐

寄存器配置正确,只代表芯片在按预期采样。数据怎么从芯片搬到主控内存,决定了这些样本的时间属性有没有被破坏。这一段是最容易被写成“能用就行”的地方,也是长期精度差异最大的地方。

3.1 SPI 读写的第 7 位与突发读

MPU6500 的 SPI 帧结构很简单:第一个字节是地址,最高位表示读写方向,1 为读、0 为写,低 7 位是寄存器地址;后续字节是数据。单字节读一次要两帧,读 14 字节就变成 28 帧,中间还可能被中断打断。所以实际驱动几乎都实现突发读——给一个起始地址,芯片在内部自动递增地址,连续吐出多个字节。

这里有个隐含约束:突发读的过程中 CS 必须一直保持低电平,一旦在中间拉高,芯片会认为传输结束,剩下的数据就丢了。同时两个字节之间不能有太长的空闲,否则内部状态机会复位。时钟极性和相位方面,模式 0 和模式 3 都能工作,但必须和主控配置一致;如果发现读出来的数据整体右移一位,八成是 CPHA 配错了,数据在错误的边沿被采样。

/* 从 0x3B 起连读 14 字节:加速度 6 + 温度 2 + 陀螺 6 */ int mpu6500_read_raw(spi_dev_t *dev, int16_t out[7]) { uint8_t buf[14]; /* 一次 CS 拉低读完,中途不能释放 */ if (spi_read_burst(dev, 0x3B, buf, sizeof(buf))) return -1; for (int i = 0; i < 7; i++) { /* 高字节在前;先拼成无符号再转有符号,避免左移溢出 */ uint16_t u = ((uint16_t)buf[2 * i] << 8) | buf[2 * i + 1]; out[i] = (int16_t)u; } return 0; }

out数组的下标是 0~2 加速度、3 温度、4~6 陀螺,这个顺序由寄存器地址的物理排列决定,不能靠“感觉”去猜。温度那两字节夹在中间,如果按 6+6 去读,陀螺数据会整体错位两个字节,现象是陀螺值和加速度值“看起来都很正常,但完全对不上物理方向”。

3.2 FIFO:把轮询换成事件

轮询读数据的最大问题不是浪费 CPU,而是采样间隔不均匀。主循环里如果还做了别的事,两次读取之间的间隔可能是 4 ms,也可能是 9 ms。对于陀螺积分来说,dt 抖动直接变成角度误差;对于任何数字滤波器来说,系数都是按固定 dt 推的,dt 一变,截止频率就偏移。

FIFO 解决的就是这个问题:芯片按固定速率往内部队列里塞样本,主控什么时候有空什么时候来取,取出来的一批数据仍然是等间隔的。配置上,0x23 决定哪些通道进 FIFO,0x6A 的 FIFO_EN 位使能、FIFO_RST 位清空,0x72 读当前字节数,0x74 逐字节读出。

包长要算清楚:加速度 6 字节 + 陀螺 6 字节 = 12 字节;如果把温度也打开就是 14 字节。FIFO 深度是 512 字节,也就是 12 字节约 42 个样本。按 200 Hz 算,42 个样本只够撑 210 ms,所以读取周期必须明显小于这个值,否则溢出。溢出的标志在中断状态寄存器里,一旦发生,队列里的数据就不可信了——不是丢一两个样本,而是整个时间轴错位。

注意:这一系列芯片里,有的型号提供 FIFO 水位(watermark)中断,有的只有溢出中断。如果手上这颗没有水位中断,常见做法是用数据就绪中断触发读取,每次读之前先查 0x72 的字节数,按整数个包取走,余数留着下次。别按“读固定长度”硬取,那样会把一个包从中间截断。

3.3 补码与符号扩展:一行代码的翻车现场

所有测量值都是 16 位二进制补码,高字节在前。这个描述看起来毫无歧义,但真正写起来,出问题的地方特别多。

第一种是符号处理错误。用(buf[0] << 8) | buf[1]直接算,得到的是 0~65535 的无符号数,负值全部变成接近 65535 的大数。这种错误在静止时特别难发现——三个轴里总有一两个是正的,看起来“有数”,只有当设备翻转到某个特定姿态,某个轴突然跳到 65000 多,才会暴露。

第二种是结构体对齐。有人喜欢把 14 字节缓冲区直接memcpy到一个结构体里,然后按int16_t访问。如果结构体没有packed属性,编译器会按 4 字节对齐插入填充,字段位置全错;即使加了packed,在某些架构上非对齐访问会触发异常或降速。稳妥的写法还是逐字节拼接。

第三种是 Python 侧。用 struct 解包时格式串写成'>h'才对,写成'>H'就又回到无符号;用 numpy 时frombuffer默认按本机字节序,而传感器是大端,不指定dtype='>i2'就会得到完全错乱的数。

3.4 时间戳:驱动里最容易被省略的字段

一个只返回原始数据的驱动接口,用起来会很不方便。原因在于调用方拿不到准确的 dt,要么自己用系统时钟算,要么假设一个固定周期。前者受调度抖动影响,后者在负载变化时直接失效。

正确的做法是让驱动在读到一批数据时同时返回时间戳,用单调时钟(不受系统时间调整影响),并且这个时间戳要标注清楚它对应的是“读取时刻”还是“采样时刻”。如果用了 FIFO,队列里的样本是过去一段时间采集的,第一个样本的时刻应该是当前时刻 - (包数-1) * 采样周期,而不是读取时刻。这个差别在 200 Hz、42 个包的场景下能达到 200 ms,做多传感器融合时这个误差足以让两个传感器的数据对不齐。

4. 从零偏到四元数:标定残差如何一路放大成姿态漂移

到这里,原始数据已经能稳定、等间隔地拿到了。接下来的问题是:这些数据里含多少系统性误差,它们会以什么方式进入姿态解算。这一章的核心逻辑是一条链路——零偏不消,积分就漂;标定不准,倾角就偏;融合权重给错,前面两步的成果全部白费。

4.1 陀螺零偏:静态均值法能压到什么程度

最常用的零偏估计方法是静止采样求均值:设备不动,采 N 个样本,取平均作为零偏,后续每帧减去。这个方法的有效性取决于两个前提——噪声是零均值的,且采样期间设备真的静止。

从统计上看,N 个独立样本的均值标准差是单样本标准差的 1/√N。假设陀螺噪声密度对应到 200 Hz 采样下每轴标准差 0.1 °/s,采 2000 个样本(10 秒),均值标准差约 0.0022 °/s。这个量级积分一小时才 8°,看起来够用。

但真正麻烦的问题是零偏不稳定,也就是零偏本身随温度和时间的慢变,而不是白噪声。芯片从室温启动,工作十分钟后内部温升可能十几度,零偏随之漂移零点几到几度每秒。这时候启动时标定的那个常数就完全失效了——它不但没帮上忙,反而变成了一个固定偏差被持续积分。

所以工程上的做法通常有两层:一是用静止检测(三轴角速度模长小于阈值、加速度模长接近 1 g)在运行中持续更新零偏;二是靠加速度计做长期校正,也就是后面互补滤波和 Mahony 里那个积分项的作用。只靠启动标定,注定会遇到温漂问题。

import numpy as np def estimate_gyro_bias(samples, fs, still_thresh=0.05): """samples: (N, 3) 角速度,单位 rad/s still_thresh: 判定静止的角速度模长阈值 返回 (bias, 标准差, 是否可信) """ norm = np.linalg.norm(samples, axis=1) # 只保留明显静止的样本,运动中采到的均值毫无意义 still = samples[norm < still_thresh] if len(still) < fs: # 有效样本不足 1 秒,直接判不可信 return np.zeros(3), np.zeros(3), False bias = still.mean(axis=0) std = still.std(axis=0) / np.sqrt(len(still)) return bias, std, bool(np.all(std < 0.005))

这段代码里真正起作用的不是mean,而是still那个筛选条件和最后的标准差检查。如果采到的样本里混进了运动片段,均值会被拉偏,而且偏得很隐蔽——因为运动往往是双向的,正负抵消后看起来“差不多”。标准差检查是最后一道防线:如果均值标准差错过了阈值,说明这段数据不够“静”,宁可不用,也不要写进零偏。

4.2 加速度计六面标定与最小二乘拟合

加速度传感器的误差模型比陀螺复杂,因为它同时存在零偏、增益误差和轴间不正交。最简单的标定是六面法:把设备依次摆成六个朝向,每个朝向静止采集,取均值,得到每个轴的正负两个极值。

如果假设各轴独立,模型就是raw = S * true + b,那么b = (max + min) / 2,S = (max - min) / 2g。这个方法简单、产线友好,能消掉大部分零偏和增益误差,但消不掉轴间耦合——当某个轴倾斜时,重力分量会串到另一个轴上。

更完整的方法是把三个轴的误差放在一个 3×3 矩阵里一起拟合:

import numpy as np def fit_accel(samples): """samples: [(raw_xyz, g_unit_xyz), ...] 每个静止朝向一组 模型 raw = M @ g + b,M 是 3x3(含增益与轴间耦合),b 是零偏 未知量 12 个,用最小二乘一次解出 """ A, y = [], [] for raw, g in samples: for i in range(3): row = np.zeros(12) row[i * 3:(i + 1) * 3] = g # M 的第 i 行乘重力方向 row[9 + i] = 1.0 # 第 i 轴的零偏项 A.append(row) y.append(raw[i]) x, *_ = np.linalg.lstsq(np.asarray(A), np.asarray(y), rcond=None) M = x[:9].reshape(3, 3) b = x[9:] return M, b # 使用时先求逆:g_est = inv(M) @ (raw - b),再归一化得到方向

这段的核心是把“每个朝向的重力方向已知”这个先验变成方程。g是单位向量,六个朝向理论上给出 ±1 和 0 的组合,但实际操作中很难摆得完全水平,所以通常会多采十几个任意姿态,让最小二乘自己去拟合。未知量 12 个,至少需要 4 个不共面的姿态;姿态越多、分布越均匀,M的条件数越好,解的稳定性越高。

验证方法很直接:把标定参数代回去,计算每个样本的|g_est|,应该接近 1 g。残差的标准差如果超过 0.01 g,说明采样姿态分布不够或者有某个朝向的数据被污染了,需要重采。这一步不能省——标定参数错了,后面的姿态解算再精细也是在错误的基础上叠加。

4.3 互补滤波:截止频率其实是信任分配

有了标定后的数据,就可以进入姿态解算。最直观的想法是直接用加速度计算倾角:roll = atan2(ay, az),pitch = atan2(-ax, sqrt(ay²+az²))。静止时这个结果很准,但设备一有线性加速度(走路、加减速、振动),重力方向就被污染了,算出来的角度会剧烈跳动。

陀螺的优势正好相反:短时间积分非常平滑,但零偏会持续累积。互补滤波的本质就是把两者的频域特性拼起来——让加速度计负责低频,陀螺负责高频,中间用一个截止频率切开。

import math class Complementary: def __init__(self, fc=1.0): # 截止频率决定"多快开始相信陀螺" self.tau = 1.0 / (2 * math.pi * fc) self.roll = 0.0 self.pitch = 0.0 def update(self, ax, ay, az, gx, gy, gz, dt): # 加速度计给出的倾角,只在高频噪声之外才可信 acc_roll = math.atan2(ay, az) acc_pitch = math.atan2(-ax, math.hypot(ay, az)) # 系数由 tau 和 dt 共同决定,dt 变了系数必须跟着变 alpha = self.tau / (self.tau + dt) # 陀螺积分走高频,加速度计走低频 self.roll = alpha * (self.roll + gx * dt) + (1 - alpha) * acc_roll self.pitch = alpha * (self.pitch + gy * dt) + (1 - alpha) * acc_pitch return self.roll, self.pitch

gx、gy必须是弧度每秒,如果前面按度每秒算的,这里要先转换,否则积分结果会大 57 倍。alpha的表达式里同时出现了tau和dt,很多人图省事直接写死 0.98,结果采样率一改(比如从 200 Hz 换到 100 Hz),等效截止频率就变了,滤波特性跟着变,表现为“同样的参数,换了主控板效果就不一样”。

截止频率的选择是个纯工程取舍。取 0.5~1 Hz,抗线性加速度能力强,但姿态跟随会变慢,快速转动时会有明显滞后;取 2~5 Hz,跟随快,但振动和线性加速度容易漏进来。我一般会先看应用的运动频谱:手持设备取 1 Hz 左右比较稳,车载或云台可以到 2~3 Hz。

4.4 Mahony 的 PI 校正与四元数积分

互补滤波只能处理两个角度,遇到偏航就无能为力——加速度计测不到绕重力轴的旋转。真正通用的做法是用四元数表示姿态,用加速度计观测到的重力方向去校正陀螺积分。

Mahony 滤波器的思路可以用一句话概括:把“当前四元数推算出的重力方向”和“加速度计实测的重力方向”做叉积,叉积结果就是误差轴;用这个误差去修正陀螺的角速度,修正量由比例项和积分项组成。

import math def mahony_update(q, gx, gy, gz, ax, ay, az, dt, kp=1.0, ki=0.05, e_int=None): q0, q1, q2, q3 = q # 由当前四元数推算机体系下的重力方向 vx = 2 * (q1 * q3 - q0 * q2) vy = 2 * (q0 * q1 + q2 * q3) vz = q0 * q0 - q1 * q1 - q2 * q2 + q3 * q3 n = math.sqrt(ax * ax + ay * ay + az * az) if n < 1e-6: return q, e_int # 自由落体或异常值,跳过校正 ax, ay, az = ax / n, ay / n, az / n # 归一化,长度信息被丢掉 # 叉积 = 实测方向 × 估计方向,方向就是需要转动的轴 ex = ay * vz - az * vy ey = az * vx - ax * vz ez = ax * vy - ay * vx # 积分项负责吃掉残余零偏,但要限幅,防止长时间线加速度把积分器撑爆 e_int[0] += ex * ki * dt e_int[1] += ey * ki * dt e_int[2] += ez * ki * dt for i in range(3): e_int[i] = max(-0.5, min(0.5, e_int[i])) gx += kp * ex + e_int[0] gy += kp * ey + e_int[1] gz += kp * ez + e_int[2] # 四元数微分方程 q' = 0.5 * q ⊗ ω,这里用一阶欧拉积分 dq0 = 0.5 * (-q1 * gx - q2 * gy - q3 * gz) dq1 = 0.5 * (q0 * gx + q2 * gz - q3 * gy) dq2 = 0.5 * (q0 * gy - q1 * gz + q3 * gx) dq3 = 0.5 * (q0 * gz + q1 * gy - q2 * gx) q = [q0 + dq0 * dt, q1 + dq1 * dt, q2 + dq2 * dt, q3 + dq3 * dt] # 每步都要归一化,否则数值漂移会让四元数模长慢慢偏离 1 norm = math.sqrt(sum(v * v for v in q)) return [v / norm for v in q], e_int

几个参数的实际影响:kp决定对加速度计的信任程度,取大了响应快但抗噪差,取小了收敛慢;ki负责消除稳态误差,本质上就是在补陀螺的残余零偏,但如果ki给大,在持续线性加速度的场景下(比如一直加速的车辆),积分项会把错误的姿态当成真值吸收进去,表现为“加速一段时间后姿态明显偏了,减速后才慢慢回来”。所以e_int的限幅不是可选项,是必需品。

积分方式也值得说一句。上面用的是一阶欧拉,在 200 Hz、角速度 500 °/s 的情况下单步误差可以忽略;但如果采样率降到 50 Hz 或者角速度更高,一阶积分的误差就会显现出来,这时候需要换成二阶或龙格-库塔。这不是理论洁癖——快速旋转时的姿态误差会直接体现在控制环的稳定性上。

5. 排查清单:五个反复出现的数据异常

下面这些现象,是我在实际项目里反复遇到的。它们的共同点是:代码看起来没错,编译不报错,运行时也不崩溃,只是数据“有点不对”。每一条都按现象、原因、处理方式写清楚。

5.1 通信与上电阶段

现象一:WHO_AM_I 读到 0x00 或 0xFF。原因通常是 SPI 从设备根本没被选中,或者 MISO 悬空。0x00 说明 MISO 一直被拉低,0xFF 说明一直拉高,两者都意味着主控没收到任何有效响应。检查 CS 是否真的拉低、SPI 模式是否匹配、供电是否正常。另一种可能是 CS 拉低太早,在时钟还没稳定时就开始发地址,芯片没识别到起始条件。处理办法是用示波器看 CS 和 SCLK 的时序关系,确认地址字节在 CS 有效之后才发出。

现象二:配置写完,读回来发现某些位没生效。最常见的原因是在软复位之后没有等够时间。芯片内部还在初始化,SPI 接口可能已经能响应,但寄存器写入被忽略。另一个原因是写配置时用了突发写,而某些寄存器不支持连续写。处理方式是逐条写完之后立刻读回校验,把“写完就算完”改成“写完必须验证”,这个习惯能省掉大量排查时间。

现象三:偶尔读到全 0 的一帧数据。如果用的是 FIFO,这通常是读的时候包长度没对齐,从一个包的中间开始读,解出来的值看起来像 0。检查 FIFO_COUNT

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

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

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

立即咨询