尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32+MPU6050+OpenMV自平衡循迹小车:串级PID与姿态解算实战

STM32+MPU6050+OpenMV自平衡循迹小车:串级PID与姿态解算实战 简介这是一套面向嵌入式初学者的基于C语言、STM32F103C8T6、MPU6050与OpenMV的自平衡循迹小车完整资料涵盖源码与说明文档通过姿态融合与视觉识别实现自平衡与自动循迹适合毕业设计、课程设计及项目开发。资源共249个文件压缩包约67.34MB以C语言头文件与源文件为主另有Keil工程文件、Python脚本、Hex固件及Markdown说明文档其中Keil工程可直接编译烧录Python脚本对应OpenMV端视觉处理配合CubeMX配置工程便于二次开发与移植。项目包含速度闭环与直立闭环、循迹、通信格式、算法与控制等模块直立环与速度环采用PID控制MPU6050姿态数据经I2C读取后参与直立控制OpenMV识别路径后回传指令整体逻辑完整、可直接运行。源码已经过严格测试能够稳定运行并预留扩展接口附有展示与未来改进说明适合以此为基础拓展更多功能。目前已有368人学习这套方案可作为STM32综合应用与机器人控制方向的参考案例。1. 为什么自平衡小车要用 STM32F103C8T6 MPU6050 OpenMV 这个组合两轮自平衡循迹小车是毕业设计和嵌入式课程设计里出现频率很高的一类题目它既要有直立平衡的姿态环又要有速度环和转向环还要让视觉模块完成赛道识别。很多第一次动手做的人会把它拆成“平衡”和“循迹”两个部分分别实现真正合到一起时才发现真正的问题不是某个模块不会用而是几个任务在同一个 MCU 上同时跑时中断、I2C 读取、PWM 输出之间互相抢占时序导致小车站起来之后稍微一动就倒下。这个项目用 STM32F103C8T6 做主控、MPU6050 测姿态、OpenMV 做视觉恰好把一个典型的分工做清楚了C 语言负责底层控制MPU6050 提供高频率姿态数据OpenMV 通过串口只回传偏差值而不是原始图像主控端不用处理视觉算法实时性压力大幅降低。这套方案的定位是给有单片机基础、但没有真正做过带闭环控制系统的开发者准备的覆盖从传感器选型、姿态解算、PID 串联到软硬件联调的完整路径做完之后对嵌入式实时控制的理解会比单纯跑例程要深得多。2. MPU6050 姿态解算从原始数据到可用的角度2.1 硬件连接与 I2C 初始化MPU6050 是一颗集成了三轴陀螺仪和三轴加速度计的传感器内部还带一个 DMP 数字运动处理器。它与主控通信的标准方式是 I2C设备地址由 AD0 引脚决定接地时为 0x68接高电平时为 0x69。STM32F103C8T6 最小系统板上没有专门的传感器接口直接从 PB8SCL和 PB9SDA引出即可也可以用软件模拟 I2C 接任意两个 GPIO但考虑到姿态解算需要较高的读取频率建议使用硬件 I2C1配 400kHz 快速模式。在使用 HAL 库的情况下初始化过程大致为void MPU6050_Init(void) { uint8_t pwr_data 0x00; // 清除休眠位让传感器进入工作状态 uint8_t sample_rate 0x05; // 采样率分频此处配置为 500Hz / (1 5) ≈ 83Hz uint8_t config_data 0x03; // 数字低通滤波 DLPF 配置带宽约 44Hz uint8_t gyro_config 0x08; // 陀螺仪量程 ±500°/s uint8_t accel_config 0x00; // 加速度计量程 ±2g HAL_I2C_Mem_Write(hi2c1, 0x68 1, 0x6B, 1, pwr_data, 1, 100); HAL_I2C_Mem_Write(hi2c1, 0x68 1, 0x19, 1, sample_rate, 1, 100); HAL_I2C_Mem_Write(hi2c1, 0x68 1, 0x1A, 1, config_data, 1, 100); HAL_I2C_Mem_Write(hi2c1, 0x68 1, 0x1B, 1, gyro_config, 1, 100); HAL_I2C_Mem_Write(hi2c1, 0x68 1, 0x1C, 1, accel_config, 1, 100); HAL_Delay(50); }这段代码的重点是寄存器 0x6B 的电源管理位必须清零否则芯片保持在休眠模式读回来的数据全是 0。0x1A 的 DLPF 配置很多教程不写但它直接影响陀螺仪噪声水平对自平衡这种需要高频响应的场景很关键。需要说明的是如果你用的是国产替代型号比如一些标称兼容 MPU6050 的六轴芯片寄存器地址和量程配置通常一致但零偏特性会有差异初始化后做静置校准是必须步骤。2.2 原始数据的读取与滤波初始化完成后从 0x3B 寄存器开始连续读取 14 个字节可以一次拿到加速度计三轴、温度、陀螺仪三轴的原始值。读取代码如下typedef struct { int16_t ax, ay, az; int16_t gx, gy, gz; } MPU6050_Data_t; MPU6050_Data_t mpu_data; void MPU6050_Read_All(void) { uint8_t buf[14]; HAL_I2C_Mem_Read(hi2c1, 0x68 1, 0x3B, 1, buf, 14, 100); mpu_data.ax (buf[0] 8) | buf[1]; mpu_data.ay (buf[2] 8) | buf[3]; mpu_data.az (buf[4] 8) | buf[5]; mpu_data.gx (buf[8] 8) | buf[9]; mpu_data.gy (buf[10] 8) | buf[11]; mpu_data.gz (buf[12] 8) | buf[13]; }读回来的原始值是带符号的 16 位整数下一步是把它转换成物理单位。加速度计直接除以对应量程的灵敏度即可±2g 量程时灵敏度是 16384 LSB/g陀螺仪在 ±500°/s 量程下灵敏度是 65.5 LSB/(°/s)。这里有一个常见的换算误区很多人直接把原始值当作角度使用但加速度计测的是比力而不是角度陀螺仪积分才能得到角度但会随时间漂移这两者必须融合。2.3 互补滤波与 DMP 两条技术路线姿态解算最稳妥的方案是开启 MPU6050 内部的 DMP直接读出四元数再转欧拉角。DMP 的优势是姿态解算在传感器内部完成不占主控运算时间输出频率可以稳定在 100Hz 以上。缺点是需要移植 InvenSense 官方的 DMP 固件库代码体积大初学阶段出了错不好定位。另一种方案是主控端做互补滤波公式简洁且依赖的数学只有高中三角函数适合课程设计答辩时讲解。#define ALPHA 0.96f float angle 0.0f; // 当前融合角度 float dt 0.01f; // 控制周期 10ms void Complement_Filter(float acc_angle, float gyro_rate, float* angle_out) { *angle_out ALPHA * (*angle_out gyro_rate * dt) (1.0f - ALPHA) * acc_angle; }互补滤波的思想是低速时相信陀螺仪积分高频好、低频漂移高速变化时相信加速度计计算出的静态角度低频好、高频噪声大。ALPHA 越大陀螺仪占比越高角度越平滑但跟随变慢取 0.96 是直立小车轮子颠簸场景下比较常用的折中值。直接由加速度计计算角度时用 atan2 更稳单纯 arctan 会在某个轴接近 90° 时出现奇点。2.4 零偏校准的可靠做法MPU6050 上电后陀螺仪的零偏不是 0用手按住车体静止时读出的角速度可能有几度每秒的偏差这个偏差积分两秒就会让角度漂移十几度直接导致小车往一个方向加速倒下。校准方法如下上电后延时 2 秒连续读取 200 次陀螺仪数据取平均值作为零偏之后每次读到的原始角速度都减去它。加速度计同理把静止时的三轴读数作为参考基值取 z 轴的偏移用于修正安装不完全水平带来的固定角度误差。常见做法是把校准结果打印到串口观察零偏是否在 ±0.5°/s 以内再开始平衡控制否则先检查供电电压和 I2C 线缆长度。MVP 阶段用杜邦线连接传感器时线长超过 20cm 就很容易在电机启动瞬间出现数据跳动此时优先考虑换双绞线或者在 MPU6050 的 VCC 与 GND 之间并一个 100nF 陶瓷电容。3. 自平衡控制的关键串级 PID 直立环、速度环与转向环3.1 三环控制架构与参数分工两轮自平衡车的本质是一个一级倒立摆控制目标是让车身倾角稳定在 0° 附近同时底盘位置受控。用一个单环 PID 直接输出 PWM 很难同时满足“站得稳”和“不漂移”两个目标常规做法是把控制分成三个串在一起的回路直立环输出一个角加速度修正量速度环修正因重心偏移和轮子打滑积累的位置误差转向环让车身绕竖直轴转动来调整朝向。三个环的优先级是直立环最高速度环其次转向环最弱中断里执行顺序也按这个先后。直立环使用 PD 控制P 项回应车体倾斜产生的重力分量D 项提供阻尼抑制震荡速度环使用 PI 控制I 项消除长时间的漂移转向环只需要比例控制因为小车对转向速度没有稳态精度要求。typedef struct { float Kp, Ki, Kd; float integral; float last_error; float output_limit; float dead_zone; } PID_Controller_t; float PID_Calculate(PID_Controller_t* pid, float target, float measurement, float dt) { float error target - measurement; float output 0.0f; // 死区处理避免传感器噪声在平衡点附近造成电机频繁抖动 if (fabsf(error) pid-dead_zone) { error 0.0f; } pid-integral error * dt; pid-integral CLAMP(pid-integral, -pid-output_limit, pid-output_limit); output pid-Kp * error pid-Ki * pid-integral pid-Kd * (error - pid-last_error) / dt; pid-last_error error; return CLAMP(output, -pid-output_limit, pid-output_limit); }3.2 直立环与速度环的串联输出实际控制量不是三个 PID 独立输出后相加而是速度环的输出作为直立环的目标角度。这个主从结构是理解自平衡的核心速度环检测到轮子位置偏离目标后不是直接给电机一个 PWM而是命令车体倾斜一个小角度让重力拉着车往目标方向走。直立环的任务则是把实际角度锁定在速度环给出的目标角度上。代码如下void Control_Loop(void) // 10ms 定时器中断调用 { float target_angle_from_speed Speed_PID_Calculate(); float balance_output Balance_PID_Calculate(target_angle_from_speed, fused_angle); float steering_output Steering_PID_Calculate(setpoint_yaw, fused_yaw); float left_pwm balance_output steering_output; float right_pwm balance_output - steering_output; Set_Motor_PWM(left_pwm, right_pwm); }速度环输出需要限制幅值常见问题是让目标角度过大——车体倾斜超过 15° 时轮子加速旋转的速度已经赶不上重心倒下速度反而加剧倾倒。所以速度环输出限制在 ±2° 到 ±3° 是合理的调参时先把速度环输出限幅调到最小直立站住后再逐步放开。转向环的输出加在左右电机上一正一负与平衡输出叠加时要注意正负号与电机方向的一致性。3.3 PID 参数的工程整定顺序PID 参数整定是自平衡项目最劝退人的环节按正确的顺序做会快很多。倒立摆的参数整定顺序是先调直立环的 P 项P 太小车体会震荡后倒下P 太大电机会发出尖锐电流声直立环 P 初步能站稳 3 秒以上后加 D 项D 项增加阻尼防止高频抖动随后加入速度环的 P 和 I 项目标是让小车在平地上缓慢匀速前进不后退最后调转向环的 P 项。下面是常见减速比电机如 1:30 带霍尔编码器的 N20 电机搭配 65mm 轮子时的参考初值控制环KpKiKd输出限幅说明直立环300~60000.5~1.5±1000先不加 I只靠 PD速度环0.5~1.50.1~0.30±100输出作为直立环目标角度转向环1.0~3.000±200转向环不追求跟踪精度这些参数来自个人调试过的同类小车只能作为起点。判断参数是否合适的快速办法是看电机电流从静止开始小车能保持直立连续 10 秒以上且没有明显来回推磨说明直立环基本合格。如果车身一直朝一个方向加速倒不考虑机械重心偏移的前提下先检查陀螺仪零偏是否校干净而不是继续调 PID。注意如果使用了带编码器的直流减速电机速度环反馈直接用编码器脉冲数计算不需要额外滤波而平衡环的反馈角度必须使用滤波后的融合角度不能使用加速度计直接算出的角度。3.4 常见调参失败现象与对策车身高频颤抖通常是直立环 P 太大或 D 太小先把 P 降低 20% 再观察。车身低频来回摇摆通常是速度环 I 值过大导致目标角度反复横跳此时积分限幅要收紧。车子立起来后慢慢往一个方向加速倒先排除陀螺仪零偏、电机死区和两只电机转速不一致三个机械问题最后才怀疑速度环方向接反。速度环方向接反是本项目最隐蔽的坑直立环几乎不受影响但车子会一直往一边快走直到保护机制断电。验证速度环方向的正确方法是把小车抬起悬空倾斜车身手动旋转轮子观察电机是否以抵抗偏心的方式自行修正。这套思路在 DMP 解算和互补滤波解算下都适用区别只是送入控制环的角度来源不同。实际在 STM32F103C8T6 上安排中断优先级也同样重要定时器触发的控制中断必须设为最高抢占优先级串口接收中断次之避免 OpenMV 串口数据进来时打断控制时序导致直立环抖一下然后倒掉。4. OpenMV 循迹实现帧头帧尾协议与串口通信调试4.1 OpenMV 端图像识别与偏差计算OpenMV 是一块带摄像头和 MicroPython 解释器的视觉开发板常见的型号是 M7 和 M4。对循迹小车而言识别方法不需要上神经网络用传统的颜色阈值分割和坐标计算就能完成。OpenMV 拍摄灰度图像后通过阈值提取深色线条赛道通常是黑色或红色计算色块的中心横坐标减去画面中心 160得到的偏差就是小车的横向位置误差。这个值通过串口发给 STM32主控根据偏差控制转向环。import sensor import time from pyb import UART sensor.reset() sensor.set_pixformat(sensor.GRAYSCALE) sensor.set_framesize(sensor.QQVGA) # 160x120帧率更高 sensor.skip_frames(30) sensor.set_auto_gain(False) sensor.set_auto_whitebal(False) uart UART(3, 115200, timeout_char200) threshold (0, 60) # 灰度阈值赛道线条偏黑则小于60 while True: img sensor.snapshot() blobs img.find_blobs([threshold], pixels_threshold80, area_threshold80) if blobs: largest_blob max(blobs, keylambda b: b.pixels()) x_error largest_blob.cx() - 80 # 中心偏差范围约 -80 ~ 80 else: x_error 0 # 帧头 有符号偏差 帧尾格式简单可靠 data bytearray([0xA5, min(max(x_error 0xFF, 0), 255), 0x5A]) uart.write(data)这段代码里关键点是x_error是有符号数 0xFF把它转成一个字节min/max防止越界。设计成单字节传输是因为循迹只需要一个偏差量没必要用多条协议能少一帧丢一帧的概率。pixels_threshold和area_threshold是过滤噪点的参数OpenMV 的find_blobs会把所有符合条件的连通域都找出来取面积最大的那个即可避免把反光误判成赛道线。QQVGA 分辨率下帧率能达到 50 帧以上对车速 0.5m/s 以下的小车足够。4.2 STM32 端串口接收与协议解析STM32F103C8T6 用串口 2 接收 OpenMV 的数据使用空闲中断加 DMA 的方式效率最高。最简单的可用方案是在串口接收中断里逐个字节接收状态机判断帧头帧尾uint8_t rx_buf[3]; uint8_t rx_index 0; uint8_t valid_frame 0; int8_t line_error 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef* huart) { if (huart-Instance USART2) { rx_buf[rx_index] received_byte; if (rx_index 1 rx_buf[0] ! 0xA5) { rx_index 0; // 帧头不匹配重新开始 } if (rx_index 3 rx_buf[2] 0x5A) { line_error (int8_t)rx_buf[1]; // 转换成有符号偏差 valid_frame 1; rx_index 0; } HAL_UART_Receive_IT(huart2, received_byte, 1); } }串口中断方案的问题在于每条数据都打断主循环一次当主控已经在频繁执行 PID 计算时丢帧率会上升。更可靠的做法是开启串口空闲中断用 DMA 接收一批数据后在空闲中断里统一解析避免逐字节进出中断。热词检索里出现的“stm32串口中断接收只收一次”问题常常就发生在这个阶段根源多数是没在每次接收完成后重新调用HAL_UART_Receive_IT或者 DMA 缓冲没有正确清空。代码里的HAL_UART_Receive_IT(huart2, received_byte, 1)就是每次处理完一字节后重新挂载中断。OpenMV 与 STM32 的通信有个接地问题需要留意如果 OpenMV 的供电来自小车主电源的 DCDC 输出和 STM32 共地是必须的否则串口 RX/TX 电平参考点不一致数据会出现乱码表现为帧头经常校验失败且无规律。排查时用示波器看两块板子的 TX 波形幅度如果幅值明显偏低优先检查两端 GND 是否连通和转接板上的电平转换芯片是否被反向电压打坏。4.3 数据帧设计与错误处理两个 MCU 之间的协议越简单越不容易出问题。常用的帧结构是帧头1 字节 数据区1~N 字节 校验可选 帧尾1 字节。本项目的偏差值是单字节不需要校验和但如果后续要同时传输赛道丢失标志和状态位可以扩展成如下格式字段长度字节值/说明帧头1固定 0xA5数据区2第 1 字节偏差量第 2 字节标志位校验和1两个数据字节之和的低 8 位帧尾1固定 0x5A加校验和的价值是如果 OpenMV 断电或摄像头被遮挡STM32 收到的数据可能全是 0x00 或任意噪声校验失败后主控可以进入“寻线失败”的降级状态——让小车减速并原地小角度摆动搜索赛道而不是以全速冲出去。这个逻辑在赛道有断线或者过弯道丢线时尤其重要不管线程检测逻辑怎么调优都建议在数据帧之外单独加一个“丢线标志”供主控控制救车策略。5. 系统集成与上电调试顺序、常见坑与验证技巧5.1 硬件组装与供电设计容易忽略的问题自平衡小车本体建议用带编码器的 N20 减速电机配 65mm 直轮两轮间距在 15cm 左右重心尽量低。MPU6050 的安装位置越低车体在水平方向运动时因加速度引入的姿态误差越小。底盘选择亚克力板或 3D 打印件都可以关键是电机安装孔位精度要保证两只轮子与地面完全平行——否则调好的平衡参数会因为某个轮子摩擦力矩不同而表现不对称小车总朝一个方向偏转初学者会误以为是 PID 参数问题而浪费大量时间。供电方案上最常见的错误是给小主控板、电机驱动和 MPU6050、OpenMV 共用一个锂电池组不做任何降压滤波。电机启动瞬间的浪涌电流会让电压跌落MPU6050 的模拟供电受干扰后读数跳变陀螺仪数据出现毛刺平衡环被虚假的角速度吓到表现就是车突然抖动然后倒塌。推荐的做法是电机驱动单独用 7.4V 锂电池供电STM32 和传感器经过稳压到 3.3V 或 5V 供电二者共地但不共电源。MPU6050 使用 3.3V 供电时SDA/SCL 引脚也是 3.3V 容忍不需要额外电平转换但要确保 STM32 的 I2C 引脚配置为开漏输出且上拉电阻不小于 4.7kΩ。5.2 完整的串级调试顺序很多人拿到一套代码后直接把小车放地上跑结果必然是翻车。推荐的上电调试顺序是不装电池用手转动轮子确认编码器计数方向一致且在两个方向都正常计数。只给传感器上电用串口打印 MPU6050 的角度静置时角度变化不超过 ±0.5°旋转壳体时角度能正确响应。此阶段容易暴露 I2C 通信不稳定问题打印数据如果出现随机跳变就应该检查接线和供电。给电机通电用固定的小 PWM 值测试两个电机转速方向确保“左轮正转”对应的控制量方向与 PV 输出一致。把小车放在能自由倾倒的软垫上方比如床垫上只启用直立环参数从小到大试直到它能站立一秒以上。此阶段最关键的是人不能跟车抢把注意力放在观察振荡趋势上。加入速度环和转向环观察低速下的定点稳定性再逐步提高到实际赛道速度。这个过程每过快一步后面查错的时间都是前面的几倍。自行车的直线循迹能不能跑好一多半取决于第 4 步和第 5 步的直立质量OpenMV 的转向控制只是叠加在平衡之上的一个小的修正量。5.3 用虚拟示波器观察控制量的可视化验证自平衡系统只有车动起来才知道控制效果但车倒了之后数据就没了调试效率非常低。一个补强验证办法是把调试阶段的关键变量通过串口实时导出到上位机绘图软件比如 VOFA、匿名上位机或者 Serial Studio。导出时建议包含三个变量融合角度、直立环输出、目标速度。在车体用手扶着左右摆动时观察波形是否呈现“角度增大→输出增大→角度回落”的正反馈反制关系。如果角度已经恢复到 0° 但输出仍停留在高位说明速度环 I 积分饱和了检查速度环输出限幅是否太紧。如果用串口导出数据的波特率是 115200每 10ms 打印一次每个变量固定 3 个字符带宽完全够用不要盲目提到 460800波特率越高越容易受电机干扰导致串口数据断裂。5.4 典型故障速查表与收尾技巧现象可能原因优先级上电后小车向一个方向加速倒陀螺仪零偏没校准先查站立时车身高频晃动直立环 P 过大或 D 过小次查小车立住了但沿直线越走越快速度环输出方向反了重点查转弯时小车失去平衡转向环输出限幅过大叠加后超出直立环调节范围降低转向环 P 和限幅OpenMV 数据偶尔丢失共地不良或接线过长硬件层查地板不同区域表现差异大轮胎摩擦力不同速度环参数需要重新考量换地面重测最后一个实际项目中常常被忽略的技巧是参数固化。调试时会发现一组比较好的参数顺手改完代码之后一定要在程序里留一个PARAM_SET_ID宏把每次调好的参数组合与对应的车况电池电压、地面类型记录下来。没有这个习惯的话电池充满和快没电时参数表现差异很大容易出现昨天活着、今天一动就倒又找不出改动过什么的情况。控制参数保存在 STM32 的 Flash 末尾扇区可以在断电后恢复最后一组有效参数这是量产级小车代码里常见的做法也适合直接写进课程设计报告的系统可靠性设计部分。本文还有配套的精品资源点击获取
返回列表