简介:这份资源面向嵌入式初学者与智能小车爱好者,围绕STM32微控制器与MPU6050六轴惯性测量单元,解决小车直线行驶与避障中的姿态解算与运动控制问题。包内共186个文件,以45个h头文件、42个c源文件为核心,配合26个d依赖文件、26个o目标文件及25个crf编译中间文件,另有uvprojx工程、hex固件、map映射与sct分散加载等配置,压缩包约5.02MB,构成一套可直接编译的Keil工程。内容涉及I2C读取陀螺仪与加速度计数据、低通与互补滤波去噪、PID参数整定调节电机转速,以及超声波或红外传感器配合路径规划实现避障,并兼顾实时性、功耗与安全阈值保护。目前已有7721人学习下载,适合通过动手实践理解传感器应用与机器人控制策略,为后续复杂项目打下基础。
1. 从 MPU6050 到走直线:一个被低估的底层能力
很多人第一次拿到 MPU6050 模块,第一反应是接上 Arduino 读一下三轴加速度和角速度,看到串口有数就以为搞定了。但真正把这块六轴传感器用在小车上做「走直线」,才会发现事情没那么简单——零漂、温漂、积分误差、轮速差、地面摩擦,每一个都能让小车跑出弧线。这份资源包的核心价值,就是给出了一套从 MPU6050 原始数据到小车直线行驶的完整实现,包含陀螺仪数据读取、偏航角计算、PID 闭环控制以及避障逻辑的整合。它适合正在做单片机智能小车、两轮差速底盘、STM32 或 Arduino 平台课程设计的从业者,尤其是那些已经能让小车动起来、但一跑就歪、调 PID 调到怀疑人生的朋友。走直线是所有路径规划的地基,地基不稳,后面加雷达动态避障、ROS 自主导航仿真都是空中楼阁。
2. MPU6050 偏航角解算:从寄存器到可信角度
2.1 为什么原始陀螺仪数据不能直接用
MPU6050 输出的是角速度,单位是度每秒。要得到角度,必须对角速度做时间积分。问题在于,陀螺仪存在零偏,静止时输出并不是严格的 0,可能是 0.5 度每秒、1.2 度每秒,甚至随温度变化。直接积分的话,哪怕零偏只有 0.1 度每秒,60 秒后角度误差就累积到 6 度,小车早就跑偏了。常见做法是上电后先静止采样几百次,求平均值作为零偏,后续每次读取都减去这个值。但零偏会随温度漂移,所以更稳妥的方案是结合加速度计做互补滤波——加速度计在静止时能提供绝对角度参考,虽然动态响应差、噪声大,但长期不漂。互补滤波的思路就是:高频信任陀螺仪积分,低频信任加速度计解算,用一个系数把两者融合。
资源包里通常会有MPU6050_GetData这样的函数,读取加速度和角速度原始值,然后通过ComplementaryFilter或KalmanFilter得到稳定的偏航角。如果你拿到的代码只有陀螺仪积分,没有滤波,那走直线时间一长必翻车。
2.2 寄存器配置与数据读取的关键参数
MPU6050 的配置直接决定后续解算的精度和稳定性。以下是一组经过验证的初始化参数,适用于小车走直线场景:
| 寄存器 | 地址 | 推荐值 | 说明 |
|---|---|---|---|
| PWR_MGMT_1 | 0x6B | 0x00 | 解除休眠,使用内部 8MHz 时钟 |
| SMPLRT_DIV | 0x19 | 0x09 | 采样率 = 1kHz / (1+9) = 100Hz |
| CONFIG | 0x1A | 0x03 | 数字低通滤波 44Hz,抑制电机振动噪声 |
| GYRO_CONFIG | 0x1B | 0x00 | 陀螺仪量程 ±250°/s,分辨率最高 |
| ACCEL_CONFIG | 0x1C | 0x00 | 加速度计量程 ±2g,适合小车低速场景 |
配置完成后,从 0x3B 开始连续读取 14 个字节,依次是加速度 X/Y/Z、温度、陀螺仪 X/Y/Z。注意 MPU6050 输出的是大端格式,需要把高字节和低字节拼成 int16。
// MPU6050 读取原始数据并转换为物理量 // 假设 I2C 读函数为 MPU6050_ReadBytes #define MPU6050_ADDR 0xD0 #define GYRO_SCALE 131.0f // ±250°/s 对应 131 LSB/(°/s) #define ACCEL_SCALE 16384.0f // ±2g 对应 16384 LSB/g void MPU6050_ReadRaw(int16_t *accel, int16_t *gyro) { uint8_t buf[14]; MPU6050_ReadBytes(MPU6050_ADDR, 0x3B, buf, 14); accel[0] = (int16_t)((buf[0] << 8) | buf[1]); accel[1] = (int16_t)((buf[2] << 8) | buf[3]); accel[2] = (int16_t)((buf[4] << 8) | buf[5]); gyro[0] = (int16_t)((buf[8] << 8) | buf[9]); gyro[1] = (int16_t)((buf[10] << 8) | buf[11]); gyro[2] = (int16_t)((buf[12] << 8) | buf[13]); } float GetGyroZ_DegPerSec(int16_t gyro_z_raw) { return (float)gyro_z_raw / GYRO_SCALE; }这段代码里GYRO_SCALE取 131.0 是因为量程设为 ±250°/s,这是 MPU6050 数据手册给出的灵敏度。如果你改成 ±500°/s,这个值要变成 65.5。很多新手直接抄代码但量程配置对不上,读出来的角速度差一倍,PID 参数怎么调都不对。MPU6050_ReadBytes是底层 I2C 读函数,需要根据你用的平台实现,STM32 用 HAL 库的话就是HAL_I2C_Mem_Read。
2.3 互补滤波实现与参数整定
拿到角速度和加速度后,偏航角的解算可以用互补滤波。加速度计解算偏航角的公式是atan2(accel_y, accel_x),但注意小车在水平面上运动时,Z 轴加速度接近重力加速度,X/Y 轴变化不大,所以加速度计对偏航角的分辨率其实很低。更常见的做法是:只用陀螺仪 Z 轴积分得到偏航角,用加速度计辅助判断小车是否静止,静止时自动更新零偏。
float yaw = 0.0f; float gyro_z_offset = 0.0f; float alpha = 0.98f; // 互补滤波系数 // 上电静止校准,采集 500 次求零偏 void MPU6050_CalibrateGyro(void) { int32_t sum = 0; int16_t accel[3], gyro[3]; for (int i = 0; i < 500; i++) { MPU6050_ReadRaw(accel, gyro); sum += gyro[2]; HAL_Delay(2); } gyro_z_offset = (float)sum / 500.0f; } // 每 10ms 调用一次,更新偏航角 void UpdateYaw(float dt) { int16_t accel[3], gyro[3]; MPU6050_ReadRaw(accel, gyro); float gyro_z = ((float)gyro[2] - gyro_z_offset) / GYRO_SCALE; yaw += gyro_z * dt; // 积分得到角度 }alpha在这个简化版本里没用到,因为加速度计对偏航角参考价值有限。如果你要做完整姿态解算,可以用四元数或 Mahony 滤波,但对于小车走直线,Z 轴角速度积分加静止零偏校准已经够用。关键是dt要准,建议用定时器中断固定 10ms 调用一次,不要用HAL_Delay在主线程序里凑时间,否则积分误差会很大。
3. 串级 PID 控制走直线:从角度偏差到电机输出
3.1 单环还是串级:控制结构选型
走直线的本质是让偏航角保持在初始值附近。最简单的是单环 PID:输入是目标角度 0 度,反馈是当前偏航角,输出直接给电机差速。但实际调试会发现,单环响应慢,小车已经偏了才纠正,走起来像喝醉酒。更稳的方案是串级 PID:外环角度环,输入角度偏差,输出目标角速度;内环角速度环,输入目标角速度和当前角速度,输出电机 PWM 差速。内环响应快,能及时抑制扰动,外环保证稳态精度。
资源包里如果只给了单环,你可以自己扩成串级。内环用陀螺仪 Z 轴角速度做反馈,外环用积分后的偏航角做反馈。注意内环的采样周期要比外环快,通常内环 5ms,外环 20ms。
3.2 位置式 PID 代码实现与参数含义
typedef struct { float Kp, Ki, Kd; float integral; float prev_error; float output_max; float integral_max; } PID_Controller; float PID_Compute(PID_Controller *pid, float target, float actual, float dt) { float error = target - actual; pid->integral += error * dt; // 积分限幅,防止饱和 if (pid->integral > pid->integral_max) pid->integral = pid->integral_max; if (pid->integral < -pid->integral_max) pid->integral = -pid->integral_max; float derivative = (error - pid->prev_error) / dt; pid->prev_error = error; float output = pid->Kp * error + pid->Ki * pid->integral + pid->Kd * derivative; if (output > pid->output_max) output = pid->output_max; if (output < -pid->output_max) output = -pid->output_max; return output; } // 外环:角度环,输出目标角速度 PID_Controller angle_pid = { .Kp = 2.5f, .Ki = 0.02f, .Kd = 0.8f, .output_max = 100.0f, .integral_max = 50.0f }; // 内环:角速度环,输出 PWM 差速 PID_Controller gyro_pid = { .Kp = 1.2f, .Ki = 0.01f, .Kd = 0.05f, .output_max = 300.0f, .integral_max = 100.0f }; void Motor_Control(float target_yaw, float current_yaw, float current_gyro_z, float dt) { float target_gyro = PID_Compute(&angle_pid, target_yaw, current_yaw, dt); float diff = PID_Compute(&gyro_pid, target_gyro, current_gyro_z, dt); int base_pwm = 400; // 基础速度 int left_pwm = base_pwm - (int)diff; int right_pwm = base_pwm + (int)diff; // 限幅后写入电机驱动 if (left_pwm > 1000) left_pwm = 1000; if (left_pwm < 0) left_pwm = 0; if (right_pwm > 1000) right_pwm = 1000; if (right_pwm < 0) right_pwm = 0; Motor_SetPWM(left_pwm, right_pwm); }Kp是比例项,决定纠正力度,太大会震荡,太小会反应迟钝。Ki消除稳态误差,但太大会积分饱和,小车会画龙。Kd抑制超调,但对噪声敏感,如果陀螺仪数据抖动大,Kd 要小甚至为 0。integral_max和output_max是必须加的限幅,否则一旦偏差大,积分项会累积到爆炸,小车直接原地打转。我一般先把 Ki 和 Kd 置零,只调 Kp 到小车能大致回正但有点震荡,然后加 Kd 压震荡,最后加一点点 Ki 消除静差。
3.3 电机差速与死区补偿
两轮差速小车在低速时,电机有死区——PWM 低于某个值电机根本不转。如果 PID 输出很小,小车可能一动不动,积分项继续累积,等累积够了突然猛冲。常见做法是在 PWM 输出上加死区补偿:如果计算出的 PWM 大于 0 但小于死区阈值,直接给死区阈值;如果等于 0 就保持 0。另外左右电机的死区可能不一致,需要分别标定。
#define MOTOR_DEADZONE 80 int CompensateDeadzone(int pwm) { if (pwm > 0 && pwm < MOTOR_DEADZONE) return MOTOR_DEADZONE; if (pwm < 0 && pwm > -MOTOR_DEADZONE) return -MOTOR_DEADZONE; return pwm; }这个函数在Motor_SetPWM之前调用。注意死区值因电机和驱动板而异,TB6612 和 L298N 就不一样,电池电压下降时死区也会变。建议在电池满电和快没电时各测一次,取一个折中值。
4. 避障逻辑与走直线的协同:别让避障毁了直线
4.1 避障触发时的角度保持策略
资源包标题里带了「避障」,说明这份代码不只是走直线,还集成了避障。但避障和走直线是有冲突的:避障时小车要转向,转向就意味着偏航角变了。如果避障结束后直接切回走直线,PID 会把当前角度当作目标角度,小车就沿着避障后的新方向走,而不是原来的直线方向。正确的做法是:避障前记录目标偏航角,避障过程中不更新目标角度,避障结束后让 PID 把小车拉回原来的目标角度。
float target_yaw = 0.0f; // 全局目标偏航角,上电时校准 void Avoidance_Routine(void) { // 检测到障碍物,执行转向 Motor_SetPWM(-300, 300); // 原地右转 HAL_Delay(300); Motor_SetPWM(400, 400); // 前进 HAL_Delay(500); // 避障结束,不修改 target_yaw,PID 会自动把车拉回原方向 }这里target_yaw始终是上电时的初始角度,避障动作只是临时改变电机输出,不改变目标值。这样避障结束后,角度环会发现当前偏航角偏离了target_yaw,自动输出纠正量。但要注意,如果避障转向太大,角度环输出会饱和,小车可能先猛转再慢慢回正,体验不好。可以在避障结束后加一个过渡:先让小车低速前进,等角度误差小于某个阈值再恢复基础速度。
4.2 超声波或红外避障的集成要点
常见的避障传感器是 HC-SR04 超声波和红外光电。超声波测距范围广但刷新慢,红外响应快但受环境光影响。资源包里如果用的是红外,注意安装高度和角度,地面反光会导致误触发。超声波的话,触发信号和回响信号要用定时器捕获,不要用pulseIn阻塞主循环,否则 PID 的dt就不准了。
// 超声波非阻塞测距,基于定时器输入捕获 volatile float distance_cm = 0.0f; volatile uint32_t echo_start = 0, echo_end = 0; void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) { if (echo_start == 0) { echo_start = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); } else { echo_end = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); uint32_t diff = (echo_end > echo_start) ? (echo_end - echo_start) : (0xFFFF - echo_start + echo_end); distance_cm = diff * 0.034f / 2.0f; // 声速 340m/s,往返除以 2 echo_start = 0; } } }0.034是声速换算成厘米每微秒的值。定时器频率如果是 1MHz,一个计数就是 1 微秒。这段代码放在中断里,不阻塞主循环,PID 的定时器中断可以正常运行。如果避障逻辑和 PID 都在主循环里跑,建议用状态机:STATE_LINE_FOLLOW、STATE_AVOID、STATE_RECOVER,每个状态里做对应的事,避免HAL_Delay卡死整个系统。
5. 避坑与排查:走直线翻车实录
5.1 小车静止时角度缓慢漂移
现象:小车放在桌上不动,串口打印的偏航角每秒增加 0.5 到 2 度。原因:陀螺仪零偏未校准或校准不充分,或者校准后温度变化导致零偏漂移。解决:上电后必须静止校准,采集至少 500 次求平均;如果长时间运行,可以每隔一段时间检测加速度计输出,判断小车是否静止,静止时重新校准零偏。另外检查GYRO_SCALE是否和GYRO_CONFIG寄存器配置一致,量程对不上会导致积分速度错误。
5.2 PID 输出震荡,小车画龙
现象:小车走直线时左右摇摆,轨迹像蛇形。原因:Kp太大或Kd太小,系统欠阻尼;也可能是陀螺仪数据噪声大,Kd放大了噪声。解决:先减小Kp到小车能回正但响应稍慢,然后增大Kd直到震荡消失。如果Kd已经很大还是震荡,检查 MPU6050 的CONFIG寄存器是否开启了低通滤波,电机振动会耦合到陀螺仪数据里。常见做法是把低通滤波设到 44Hz 或 21Hz,牺牲一点响应速度换稳定性。
5.3 避障后小车不回到原方向
现象:小车避障转向后,沿着新方向一直走,不再回到原来的直线。原因:避障逻辑里修改了target_yaw,或者避障结束后重新初始化了 PID 积分项。解决:确保target_yaw只在初始化时设定一次,避障过程中和结束后都不修改。如果避障动作导致角度误差太大,可以在避障结束后先让小车停止,等角度环把车拉回target_yaw附近再恢复前进。
5.4 电池电压下降后走直线变差
现象:刚充满电时走得挺直,跑十几分钟后开始偏。原因:电池电压下降导致电机转速变化,同样的 PWM 对应的实际转速变了,PID 参数需要重新整定。解决:如果电机驱动支持编码器,可以加轮速环做串级 PID 的最内环,这样电压变化时轮速闭环能自动补偿。没有编码器的话,至少要在电池电压低于阈值时降低基础速度,或者重新整定 PID。常见做法是用分压电阻采集电池电压,在软件里做低压降速。
5.5 I2C 通信失败导致角度突变
现象:串口偶尔打印角度跳变,小车突然猛转。原因:MPU6050 的 I2C 通信受电机干扰,读数据失败返回全 0 或全 1,积分后角度突变。解决:I2C 读取加超时和重试,如果连续失败多次,保持上一次的角度值不更新,同时让小车减速。硬件上 I2C 上拉电阻用 4.7k 到 10k,走线远离电机线,MPU6050 模块尽量靠近主控。
6. 进阶:用编码器做轮速闭环,让走直线不挑地面
走直线只靠 MPU6050 角度环,在地毯、瓷砖、木地板上表现差异很大,因为不同地面的摩擦系数不同,同样的 PWM 差速产生的转向效果不一样。更稳的方案是加编码器做轮速闭环,形成「角度环 → 轮速环 → 电机」的三环控制。角度环输出目标左右轮速差,轮速环用编码器反馈保证左右轮实际转速差跟目标一致。这样即使地面摩擦变化,轮速环也能快速补偿。
// 轮速环 PID,输入目标轮速和实际轮速(编码器脉冲/周期) PID_Controller left_speed_pid = { .Kp = 3.0f, .Ki = 0.5f, .Kd = 0.0f, .output_max = 1000.0f, .integral_max = 500.0f }; PID_Controller right_speed_pid = { .Kp = 3.0f, .Ki = 0.5f, .Kd = 0.0f, .output_max = 1000.0f, .integral_max = 500.0f }; void Speed_Loop(float target_left, float target_right, float actual_left, float actual_right, float dt) { float left_pwm = PID_Compute(&left_speed_pid, target_left, actual_left, dt); float right_pwm = PID_Compute(&right_speed_pid, target_right, actual_right, dt); Motor_SetPWM((int)left_pwm, (int)right_pwm); }编码器测速常用 M 法:固定周期读编码器脉冲数,除以周期得到速度。周期太短脉冲少,分辨率不够;周期太长响应慢。我一般用 20ms 测速周期,配合 500 线编码器,低速时也能有足够分辨率。轮速环的Kp可以给大一点,因为编码器反馈比陀螺仪更直接、噪声更小。
验证三环是否生效,可以做一个简单测试:用手按住一边轮子,观察另一边轮子的 PWM 是否自动增大以维持目标轮速差。如果按住了没反应,说明轮速环没起作用,检查编码器读数是否正确、PID 计算是否在正确的定时器中断里调用。另一个测试是让小车在粗糙地面和光滑地面各跑一次,看直线偏差是否明显减小。
从那以后我每次调走直线,都强制先单独验证 MPU6050 零偏校准和角度积分,再验证电机死区,最后才上 PID。跳过任何一步,后面都会用更奇怪的现象还回来。希望帮到你。
本文还有配套的精品资源,点击获取