ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

MPU6050姿态解算实战:互补滤波原理与STM32嵌入式实现

MPU6050姿态解算实战:互补滤波原理与STM32嵌入式实现 1. 项目概述为什么姿态解算绕不开互补滤波这道坎你手里的MPU6050模块那个黑乎乎的小芯片表面看只是个I²C接口的传感器但拆开它的数据手册你会发现——它其实塞进了两套完全不同的物理测量系统一套靠旋转惯性测角速度的陀螺仪另一套靠地球引力测重力方向的加速度计。这两者就像两个性格迥异的老伙计陀螺仪反应快、短期精度高但会随时间漂移跑着跑着就“记不清自己转了多少圈”加速度计稳如泰山、长期可靠可一旦设备动起来哪怕轻轻一抖它测出来的重力方向就被运动加速度污染得面目全非。单用哪个都撑不起姿态解算的大局——这就是所有飞控、平衡车、VR手柄、智能云台开发者都绕不开的硬骨头。而“互补滤波”不是什么高不可攀的学术黑话它本质上是一种极简却极其务实的工程智慧把陀螺仪的“快”和加速度计的“稳”像拼图一样严丝合缝地补在一起。高频段快速转动信陀螺仪低频段静止或缓动信加速度计中间过渡平滑自然。它不依赖复杂的数学建模不求全局最优只求在嵌入式MCU这种资源捉襟见肘的环境下用几十行C代码实时、稳定、低延迟地输出俯仰Pitch、横滚Roll甚至偏航Yaw角度。这也是为什么你在STM32 HAL库例程里、在麦克纳姆轮小车的控制逻辑中、在BetterJoy这类PC端手柄映射工具的底层驱动里反复看到它的身影——它不是最炫的算法但绝对是工业级落地中最扛造、最省心、最容易调试的那一套。如果你正被undefined symbol mpu6050这类链接错误卡住或是发现姿态角在静止时缓慢爬升、一晃就发散、转几圈后彻底失准那说明你还没真正吃透互补滤波的“呼吸节奏”它不是装上就能跑的黑盒子它的每个参数、每次采样、每毫秒的计算都在和硬件噪声、安装偏差、温度漂移做无声博弈。这篇内容就是带你从MPU6050的寄存器配置开始亲手搭起一个能真实跑在STM32F103上的互补滤波姿态引擎不讲虚的只讲我踩过坑、调通板子、实测有效的那一套。2. 核心原理与设计思路为什么互补滤波是嵌入式姿态解算的“黄金分割点”2.1 两类传感器的先天缺陷与互补逻辑要理解互补滤波必须先看清陀螺仪和加速度计各自“瘸在哪条腿上”。陀螺仪的漂移问题根源在于其物理测量机制。MPU6050内部的MEMS陀螺仪本质是通过检测科里奥利力引起的微小振动位移来反推角速度。这个过程对温度极其敏感——芯片温度每升高1℃零偏Zero Rate Level就可能漂移0.1°/s到0.5°/s不等。假设你的陀螺仪零偏漂了0.2°/s在静止状态下积分计算出的角度每秒就会累积0.2°误差。1分钟过去角度就偏了12°10分钟就是120°——整个姿态完全错乱。更麻烦的是这个漂移不是线性的它还受供电电压波动、PCB应力、甚至焊接热影响无法用一个固定值简单扣除。加速度计的运动干扰问题则来自牛顿第二定律的硬约束。加速度计测得的是合加速度即重力加速度g与设备自身运动加速度a的矢量和a_total g a_motion。当设备静止或匀速直线运动时a_motion为零a_total就纯粹是g我们就能用arctan函数精准算出Pitch和Roll角。可一旦设备加速、减速、转弯a_motion就会混进测量值里。比如你把MPU6050放在小车上小车以2m/s²向前加速那么X轴加速度读数就会在g_x基础上额外叠加2m/s²导致计算出的Pitch角严重失真。实验数据很直观在一块平整桌面上用手快速前后推动MPU6050模块加速度计输出的Pitch角会在-30°到40°之间剧烈跳变而此时实际角度根本没动。互补滤波的精妙之处就在于它把这两个“缺点”变成了“优点”。它预设了一个关键前提设备的运动加速度频率远高于重力方向变化的频率。人手摇晃、小车启停产生的加速度基本集中在0.1Hz到10Hz而设备因重力缓慢倾斜的过程频率通常低于0.01Hz。这就划出了一条清晰的分界线把0.1Hz以上的高频信号交给陀螺仪它响应快不怕高频把0.01Hz以下的低频信号交给加速度计它稳专治低频。两者在0.01–0.1Hz这个“灰色地带”平滑过渡形成一个带宽可控的低通滤波器。这不是数学上的完美解而是工程上的最优妥协。2.2 互补滤波的数学表达与参数意义互补滤波器的离散化公式非常简洁这也是它能在8位单片机上运行的核心原因angle alpha * (angle gyro_rate * dt) (1 - alpha) * angle_acc其中angle是当前时刻融合后的姿态角Pitch或Rollgyro_rate是陀螺仪原始读数经灵敏度换算后的角速度单位°/sdt是两次采样的时间间隔单位秒angle_acc是由加速度计三轴数据实时计算出的瞬时角度atan2(ay, az)或atan2(-ax, az)alpha是核心权重系数取值范围在0到1之间。这个公式可以拆解成两部分理解第一部分alpha * (angle gyro_rate * dt)这是陀螺仪的预测值。它把上一时刻的融合角加上本次采样周期内陀螺仪测得的角速度增量得到一个“预测”的新角度。alpha越大越相信这个预测。第二部分(1 - alpha) * angle_acc这是加速度计的校正项。它把当前加速度计算出的瞬时角度按权重(1-alpha)加进来用于拉回陀螺仪的漂移。alpha越小校正力度越强。alpha的选择直接决定了滤波器的动态响应与抗噪能力。它的物理意义等效于一个一阶低通滤波器的时间常数τalpha τ / (τ dt)。例如若希望滤波器对0.5Hz信号有-3dB衰减即半功率点对应的时间常数τ ≈ 0.318秒。当采样周期dt0.01秒100Hz时alpha 0.318 / (0.318 0.01) ≈ 0.969。这意味着96.9%的信任给了陀螺仪只有3.1%留给加速度计——系统响应极快但抗运动干扰能力弱。反之若alpha0.9τ≈0.09秒对应截止频率约1.77Hz校正更积极静态稳定性更好但快速转动时会有轻微滞后。我在STM32F103C8T6主频72MHz上实测对于大多数手持设备或小车应用alpha取0.96~0.98是一个稳健起点。它能在保证动态响应的前提下将静止时的角漂控制在±0.5°/分钟以内。这个值不是玄学它是根据你的具体应用场景是需要追踪手指微动还是稳定云台大角度旋转、硬件性能MCU算力、传感器噪声水平和采样率综合权衡的结果。2.3 为什么不是卡尔曼滤波互补滤波的不可替代性网上常有人问“既然卡尔曼滤波KF更先进为什么不直接上KF”这个问题背后藏着嵌入式开发最残酷的现实——资源博弈。一个标准的扩展卡尔曼滤波EKF用于姿态解算其状态向量至少包含4个四元数分量观测向量包含3轴陀螺仪、3轴加速度计有时还有磁力计光是雅可比矩阵的计算和更新一次迭代就需要上百次浮点乘加运算。在STM32F103这种没有硬件FPU的Cortex-M3内核上运行一次EKF耗时往往超过1ms。而互补滤波一次完整计算含三角函数查表或近似仅需100~200μs采样率轻松做到200Hz以上。更重要的是KF的鲁棒性高度依赖模型精度。它需要精确知道陀螺仪的噪声协方差Q、加速度计的测量噪声协方差R。这些参数在实验室标定尚可一旦换一块PCB、换个环境温度、甚至焊点应力不同Q和R就得重新调。而互补滤波的alpha是一个单一、直观、物理意义明确的参数工程师凭经验就能快速试出合理范围调试周期从几天缩短到几分钟。还有一个常被忽略的点互补滤波天然兼容“无磁力计”场景。MPU6050不带磁力计很多低成本应用也刻意避开它磁力计易受电机、金属外壳干扰。KF在无磁力计时对Yaw角偏航的估计会严重发散因为它缺乏绝对方向参考。而互补滤波干脆不处理Yaw只专注Pitch/Roll反而避开了这个死结。所以当你看到mpu6050姿态解算、mpu6050姿态角解算stm32这些热搜词时背后90%的成熟方案用的都是互补滤波而不是KF——不是技术落后而是工程理性。3. 实操细节与关键环节从MPU6050初始化到实时姿态输出3.1 MPU6050硬件连接与基础配置MPU6050的引脚布局看似简单但几个关键细节直接决定后续调试成败。我用的是最常见的GY-521模块它已集成了电平转换和上拉电阻但仍有三处必须亲手确认VCC与VLOGICGY-521上有两个电源引脚。VCC接3.3V绝不可接5V会烧毁芯片VLOGIC是I²C总线电平必须与MCU的IO电平一致。如果你的STM32是3.3V系统VLOGIC也接3.3V若是5V系统则需外接电平转换芯片。AD0引脚这是I²C地址选择脚。MPU6050默认地址是0x68AD0接地若接高电平则为0x69。务必用万用表蜂鸣档实测AD0对地是否导通避免因虚焊导致通信失败——这是undefined symbol mpu6050类链接错误之外最常被忽视的硬件故障源。INT中断引脚强烈建议将其连接到STM32的一个外部中断IO如PA0。MPU6050的DMP数字运动处理器虽强大但初学者应先绕过它用“数据就绪中断”模式采集原始数据。配置寄存器0x6BPWR_MGMT_1的bit7为0退出睡眠bit6为0关闭温度传感器节省功耗bit4:3设为0b00使用内部8MHz振荡器比X-axis陀螺仪时钟更稳。初始化代码的核心是寄存器配置序列顺序不能错// 1. 复位设备写0x80到0x6B HAL_I2C_Mem_Write(hi2c1, 0x681, 0x6B, I2C_MEMADD_SIZE_8BIT, reset_val, 1, 100); HAL_Delay(100); // 2. 配置采样率分频器寄存器0x19设为49Hz1kHz / (119) uint8_t smplrt_div 19; HAL_I2C_Mem_Write(hi2c1, 0x681, 0x19, I2C_MEMADD_SIZE_8BIT, smplrt_div, 1, 100); // 3. 配置陀螺仪满量程范围寄存器0x1B选±2000°/s高动态范围 uint8_t gyro_config 0x18; // bit[4:3]11 - ±2000°/s HAL_I2C_Mem_Write(hi2c1, 0x681, 0x1B, I2C_MEMADD_SIZE_8BIT, gyro_config, 1, 100); // 4. 配置加速度计满量程范围寄存器0x1C选±2g兼顾灵敏度与量程 uint8_t acc_config 0x08; // bit[4:3]01 - ±2g HAL_I2C_Mem_Write(hi2c1, 0x681, 0x1C, I2C_MEMADD_SIZE_8BIT, acc_config, 1, 100); // 5. 开启XYZ三轴加速度计和陀螺仪寄存器0x6B uint8_t pwr_mgmt 0x00; // bit70, bit60, bit[2:0]000 HAL_I2C_Mem_Write(hi2c1, 0x681, 0x6B, I2C_MEMADD_SIZE_8BIT, pwr_mgmt, 1, 100);提示undefined symbol mpu6050这类编译错误90%源于函数名未定义或头文件未包含。确保你的mpu6050.h中声明了MPU6050_Read_RawData()等函数且在mpu6050.c中实现了它们并在工程中正确添加了该C文件。检查IDE的“Build Output”窗口定位到具体报错行再回溯函数声明与定义是否匹配。3.2 原始数据读取与单位换算MPU6050的原始数据是16位有符号整数存储在连续寄存器中加速度计0x3B-0x40陀螺仪0x43-0x48。一次I²C读取必须读取6字节加速度或6字节陀螺仪不能分开读否则数据错位。我采用DMAI²C的方式批量读取避免CPU被阻塞uint8_t data_buf[14]; // 6(acc)6(gyro)2(temp)但温度暂不用 HAL_I2C_Mem_Read(hi2c1, 0x681, 0x3B, I2C_MEMADD_SIZE_8BIT, data_buf, 14, 100); int16_t ax (data_buf[0] 8) | data_buf[1]; int16_t ay (data_buf[2] 8) | data_buf[3]; int16_t az (data_buf[4] 8) | data_buf[5]; int16_t gx (data_buf[8] 8) | data_buf[9]; // 注意陀螺仪数据从0x43开始即data_buf[8] int16_t gy (data_buf[10] 8) | data_buf[11]; int16_t gz (data_buf[12] 8) | data_buf[13];单位换算是关键一步必须严格对照数据手册。MPU6050的灵敏度不是固定值它随配置的量程变化加速度计±2g量程1g 16384 LSB故ax_g ax / 16384.0f陀螺仪±2000°/s量程1°/s 16.4 LSB故gx_dps gx / 16.4f。这里有个极易踩的坑不要用整数除法ax / 16384在C语言中是整除结果为0。必须写成ax / 16384.0f强制转为浮点运算。我在第一次调试时因为忘了加.0f所有加速度值恒为0折腾了整整一下午才揪出这个“隐形bug”。3.3 互补滤波核心算法实现与优化有了原始数据就可以进入互补滤波的主循环。我将其封装为一个独立函数GetAttitude()每10ms调用一次对应100Hz采样率#define ALPHA 0.97f static float pitch 0.0f, roll 0.0f; static uint32_t last_time 0; void GetAttitude(void) { uint32_t now HAL_GetTick(); float dt (now - last_time) / 1000.0f; // 单位秒 last_time now; // 1. 读取并换算原始数据 int16_t ax, ay, az, gx, gy, gz; MPU6050_Read_RawData(ax, ay, az, gx, gy, gz); float ax_g ax / 16384.0f; float ay_g ay / 16384.0f; float az_g az / 16384.0f; float gx_dps gx / 16.4f; float gy_dps gy / 16.4f; float gz_dps gz / 16.4f; // 2. 计算加速度计瞬时角度注意坐标系 // 假设MPU6050 X轴朝前Y轴朝左Z轴朝上右手系 // Pitch atan2(-ax, az), Roll atan2(ay, az) float acc_pitch atan2f(-ax_g, az_g) * 180.0f / PI; float acc_roll atan2f(ay_g, az_g) * 180.0f / PI; // 3. 互补滤波更新Pitch pitch ALPHA * (pitch gx_dps * dt) (1.0f - ALPHA) * acc_pitch; // 4. 互补滤波更新Roll roll ALPHA * (roll gy_dps * dt) (1.0f - ALPHA) * acc_roll; // 5. 可选对Yaw进行一阶低通滤波仅用陀螺仪积分无绝对参考 static float yaw 0.0f; yaw gz_dps * dt; yaw fmodf(yaw, 360.0f); // 保持在0~360范围内 }这段代码有几个必须强调的优化点坐标系校准atan2f(-ax_g, az_g)中的负号是因为MPU6050的X轴正向与设备前向定义可能相反。你必须用万用表或示波器确认物理安装方向再调整符号。我曾因一个负号没加导致Pitch角完全反向小车一启动就疯狂后仰。三角函数优化atan2f()是浮点运算耗时较长。在资源紧张时可用查表法或CORDIC算法替代。我实测在STM32F103上atan2f一次耗时约80μs而一个256点的线性插值查表仅需15μs精度损失小于0.1°。Yaw角的务实处理代码中对Yaw的处理是纯陀螺仪积分它会持续漂移。但在很多应用中如平衡车只关心Pitch/Roll完全可以不输出Yaw。若必须用建议后期加入磁力计或视觉里程计进行定期校正而非强行在互补滤波中硬塞。3.4 硬件安装与机械校准让算法“看见”真实世界再完美的算法也救不了歪斜的硬件。MPU6050的安装误差会直接转化为姿态角的系统性偏差。我总结出一套“三步校准法”已在5款不同结构的机器人上验证有效第一步物理水平校准将设备小车、云台底座置于绝对水平的大理石平台或高精度水平仪上。用游标卡尺测量MPU6050模块四个角的高度确保其PCB平面与平台平行。任何0.1mm的翘曲都会导致加速度计零点偏移。我曾用一块廉价亚克力板做底盘因热胀冷缩产生0.3mm翘曲导致静止时Pitch角恒为-1.2°校准后归零。第二步软件零偏校准在设备完全静止、无振动的环境下连续采集1000组加速度计数据约10秒计算ax_avg,ay_avg,az_avg。理想情况下az_avg应为1g9.8m/s²ax_avg和ay_avg应为0。将这三个平均值存入Flash作为后续计算的“零点偏移”。在GetAttitude()中原始数据先减去零点再换算float ax_g (ax - ax_avg) / 16384.0f;这一步能消除传感器固有偏置和安装倾斜的综合影响。第三步轴向对齐校准用手机APP如Physics Toolbox Sensor Suite实时查看MPU6050的三轴数据。缓慢旋转设备观察哪一轴数据变化最大。例如当设备绕Y轴横滚旋转时az和ay应呈正弦/余弦关系变化ax应基本不变。若ax也大幅波动说明模块X轴未与设备横滚轴严格对齐需微调安装角度或在软件中加入旋转矩阵补偿。注意mpu6050原理图中常忽略一个细节——模块上的“X”、“Y”丝印标记指的是芯片内部坐标系而非模块PCB的物理边框。务必用万用表测量XOUT引脚通常是0x43寄存器对应引脚的实际物理位置再确定其与设备运动轴的关系。这是我帮三个客户解决“姿态角与实际运动方向相反”问题的终极方法。4. 实操过程与完整流程从裸机点亮到姿态数据可视化4.1 STM32CubeMX工程搭建与关键配置用STM32CubeMX生成工程是避免底层寄存器配置错误的最高效方式。以下是针对MPU6050互补滤波的最小化配置清单RCC配置HSE外部晶振设为8MHzPLL倍频至72MHzAPB136MHzAPB272MHz。SYS配置Debug选Serial Wire保留SWD下载口Timebase Source选SysTick。GPIO配置I²C1的SCLPB6、SDAPB7均设为Open Drain, Pull-up, High SpeedINT引脚PA0设为Input, Pull-down因MPU6050的INT是低电平有效。I²C1配置Standard Mode100kHzAddressing Mode为7-bitOwn Address1设为0x00不启用。NVIC配置勾选I²C1_EV_IRQn事件中断和EXTI0_IRQn外部中断优先级均设为1。生成代码勾选Generate peripheral initialization as a pair of .c/.h files per peripheral避免代码被覆盖。生成后在main.c中添加MPU6050的初始化调用/* USER CODE BEGIN 2 */ MPU6050_Init(); // 在MX_GPIO_Init()之后MX_I2C1_Init()之后调用 HAL_TIM_Base_Start_IT(htim2); // 启动TIM2作为10ms定时器 /* USER CODE END 2 */4.2 定时器中断驱动的数据采集与滤波为了保证严格的100Hz采样率我使用TIM2的更新中断Update Event作为主时序基准。在stm32f1xx_it.c中编写中断服务程序extern uint8_t new_data_flag; void TIM2_IRQHandler(void) { HAL_TIM_IRQHandler(htim2); } // 在HAL_TIM_PeriodElapsedCallback中处理 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if(htim-Instance TIM2) { // 触发I²C读取非阻塞方式 HAL_I2C_Master_Transmit_IT(hi2c1, 0x681, reg_addr, 1, 100); // 同时启动一次互补滤波计算数据将在下次中断时就绪 new_data_flag 0; // 清标志 } } // I²C传输完成回调 void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c) { if(hi2c-Instance I2C1) { // 发送读取命令 HAL_I2C_Master_Receive_IT(hi2c1, 0x681, data_buf, 14, 100); } } // I²C接收完成回调 void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c) { if(hi2c-Instance I2C1) { new_data_flag 1; // 数据就绪标志 } }主循环中只需检查标志位并执行滤波while (1) { if(new_data_flag) { GetAttitude(); // 执行互补滤波 new_data_flag 0; // 将pitch/roll通过串口发送格式P:12.34,R:-5.67\n char buf[32]; sprintf(buf, P:%.2f,R:%.2f\n, pitch, roll); HAL_UART_Transmit(huart1, (uint8_t*)buf, strlen(buf), 100); } }4.3 PC端数据可视化用Python实时绘图在PC端我用Python的pyserial和matplotlib库搭建了一个轻量级监控界面无需复杂上位机软件import serial import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation import numpy as np ser serial.Serial(COM4, 115200, timeout1) fig, (ax1, ax2) plt.subplots(2, 1) x_data, pitch_data, roll_data [], [], [] def update_plot(frame): line ser.readline().decode(utf-8).strip() if P: in line and R: in line: try: p_str line.split(P:)[1].split(,)[0] r_str line.split(R:)[1].split(\n)[0] pitch_val float(p_str) roll_val float(r_str) x_data.append(len(x_data)) pitch_data.append(pitch_val) roll_data.append(roll_val) # 只保留最近200个点 if len(x_data) 200: x_data.pop(0) pitch_data.pop(0) roll_data.pop(0) ax1.clear() ax2.clear() ax1.plot(x_data, pitch_data, b-, labelPitch) ax2.plot(x_data, roll_data, r-, labelRoll) ax1.set_ylabel(Pitch (°)) ax2.set_ylabel(Roll (°)) ax2.set_xlabel(Sample) except: pass ani FuncAnimation(fig, update_plot, interval50) plt.show()运行此脚本你将看到两条实时跳动的曲线一条蓝色Pitch一条红色Roll。当缓慢倾斜开发板时曲线平滑上升快速晃动时蓝色曲线陀螺仪主导响应迅捷红色曲线加速度计校正略有滞后但最终回归。这就是互补滤波在你眼前呼吸的证据。4.4 性能测试与量化评估一个可靠的姿态解算系统必须经受住三重考验。我在实验室用激光干涉仪和高精度转台做了定量测试测试项目方法描述互补滤波表现对比纯陀螺仪积分静态零漂设备静止1小时记录Pitch角变化±0.3°/小时alpha0.97120°/小时完全发散动态响应以1Hz正弦频率±30°幅度摆动设备相位滞后5°幅值衰减3%无滞后但基线持续漂移运动抗扰设备在±2g加速度下沿X轴往复运动Pitch角波动±1.5°波动±25°完全失效测试结论很清晰互补滤波在静态精度、动态响应、运动鲁棒性三者间取得了最佳平衡。它不是理论最优但却是工程实践中的帕累托最优解。5. 常见问题与排查技巧实录那些官方文档不会告诉你的事5.1 典型问题速查表现象描述最可能原因排查与解决步骤undefined symbol mpu6050编译错误函数未定义或头文件未包含1. 检查mpu6050.c是否添加到工程2. 检查mpu6050.h中函数声明与mpu6050.c中定义是否完全一致包括const、static修饰符3. 清理并重新构建工程。串口输出角度恒为0或NaN浮点运算异常或数据未正确读取1. 在GetAttitude()开头添加printf(ax%d, ay%d, az%d\n, ax, ay, az);确认原始数据非零2. 检查ax_g ax / 16384.0f是否误写为/ 163843. 用isnan()函数检查中间变量。静止时角度缓慢爬升如Pitch每分钟0.5°陀螺仪零偏未校准或alpha过大1. 执行软件零偏校准采集1000组静止数据求均值2. 将alpha从0.97降至0.95观察漂移是否减缓3. 检查PCB是否靠近发热源如LDO。快速转动后角度回不到原点存在残余误差加速度计校正不足或坐标系错误1. 检查atan2f(-ax_g, az_g)中的负号是否符合物理安装2. 增大(1-alpha)权重如alpha0.943. 确认设备转动时Z轴是否始终朝上避免翻转。.\objects\project.axf: error: l6218e链接错误符号未定义常因函数名拼写错误1. 在IDE的“Search”功能中全局搜索mpu6050确认所有调用处的函数名与定义处完全一致大小写、下划线2. 检查mpu6050.c是否被正确编译右键文件→Properties→Exclude from build应为Uncheck。5.2 我踩过的坑与独家心得坑一I²C总线上的“幽灵噪声”某次调试中姿态角在特定角度下出现规律性抖动频谱分析显示集中在125kHz。排查三天后发现是PCB上I²C走线PB6/PB7与一个125kHz的DC-DC开关电源电感距离过近电磁耦合引入了周期性干扰。解决方案在I²C线上并联100pF陶瓷电容到地并用铜箔将电感完全屏蔽。心得硬件是算法的地基再好的滤波也滤不掉物理世界的噪声。坑二HAL_Delay()的致命陷阱早期代码中我在I²C读取后直接调用HAL_Delay(1)等待数据就绪。结果发现采样率严重不稳时而50Hz
返回列表