FEATURED · 精选文章

STM32+MPU6050+OpenMV自平衡循迹小车:数据对齐与调参实战

发布时间 / 2026/9/13 14:18:17
来源 / 创域科博编辑部
栏目 / 资讯中心
STM32+MPU6050+OpenMV自平衡循迹小车:数据对齐与调参实战 简介这是一份面向毕业设计、课程设计及嵌入式项目开发的完整工程资料基于C语言、STM32F103C8T6主控、MPU6050姿态传感器与OpenMV视觉模块实现自平衡与循迹两大核心功能。内容覆盖速度闭环、直立闭环、循迹算法、通信格式与展示模块工程源码经过严格测试适合需要快速上手或二次扩展的电子、自动化、计算机等相关专业学生。资源包共249个文件压缩包大小约67.34MB文件类型以h头文件和c源文件为主另含Keil工程配置、OpenMV Python脚本、原理图/文档及编译生成文件可满足从代码阅读、编译下载到实车调试的完整流程。项目结构按算法、控制、闭环、循迹等模块划分便于对照学习与功能定制。目前已有368人学习浏览适合在现有方案基础上做算法优化或功能延伸是一份兼顾参考价值与实用性的综合项目资源。1. 基于C语言的STM32F103C8T6MPU6050OpenMV自平衡循迹小车为什么难在数据对齐本科毕设和课程设计里基于C语言、以STM32F103C8T6最小系统板为主控的自平衡循迹小车长期霸占推荐榜单。表面上看这是把两台小车拼在一起先让两轮车保持直立再让它沿黑线走。真正入手会发现MPU6050、驱动电机和OpenMV分别在输出三个因果链而这三个链在时间上完全不同步。倾角每5毫秒变一次PWM每20毫秒修一次OpenMV每帧要几十毫秒哪一环没对齐车就会先抖再倒最后冲线。这篇内容按“接线、读MPU6050、解算、直立PD、OpenMV循迹、联调”的顺序展开适合拿来复现也适合答辩时把控制思路讲清楚。2. STM32F103C8T6最小系统板的引脚规划与C工程骨架先别急着写控制逻辑把引脚钉死否则后边大概率会为了挪一根线重画大半块板子。F103C8T6只有48个引脚PA、PB、PC三组口看着够用等接上MPU6050、OpenMV、TB6612电机驱动和电池电量检测之后冗余不超过10个脚。分配原则是带复用的接口固定在硬件外设上普通IO留给电机方向PWM选同一个定时器输出尽量避免和调试串口抢通道。2.1 MPU6050、OpenMV与电机驱动的连接方式MPU6050只吃3.3V不要在5V电源上并联。它走I2C总线地址由AD0引脚决定接GND时7位地址是0x68接3.3V时是0x69。OpenMV通过串口把图像处理结果发给STM32两端必须共地。电机驱动我一般用TB6612比L298N轻逻辑电压3.3V就能驱动正好匹配STM32F103C8T6的IO电平。模块STM32F103C8T6引脚说明MPU6050 SCLPB6I2C1_SCLMPU6050 SDAPB7I2C1_SDAMPU6050 AD0GND选地址0x68MPU6050 VCC3V3严禁接5V左电机IN1/IN2PA8/PA9TB6612方向输入右电机IN1/IN2PA10/PA11TB6612方向输入左PWMPA0TIM2_CH1右PWMPA1TIM2_CH2OpenMV TXPB11STM32的USART3_RXOpenMV RXPB10STM32的USART3_TX注意PA2和PA3是USART2串口很多人习惯拿它接OpenMV但PA0到PA3又是TIM2的四个PWM通道。如果PWM、OpenMV、调试串口全挤在前8个脚里CubeMX分分钟给你标红。我建议OpenMV走USART3PB10/PB11调试串口保留USART1的PA9/PA10TIM2只开CH1和CH2这样资源不冲突。另外检查你的最小系统板原理图。某宝上便宜的F103C8T6板子有的把PA11/PA12引到了USB座有的把PB2设成了BOOT1引脚。电机方向信号不推荐放在PA11/PA12和PB2上USB复用和启动引脚容易在上电瞬间产生意外电平。2.2 CubeMX下的资源分配与电机驱动代码骨架用CubeMX生成HAL库工程是现在最常见的做法。芯片选STM32F103C8T6RCC里打开HSE外部晶振SYS里开启Serial Wire调试口否则烧录一次后第二次可能连不上SWD。时钟树直接拉满72MHz外设配置如下表。外设模式关键参数I2C1I2CStandard模式100kHzUSART3Asynchronous1152008N1开中断TIM2Internal ClockPSC71-1ARR99-1CH1/CH2输出PWMGPIOAOutput PPPA8到PA11速度HIGHPSC71-1和ARR99-1表示定时器时钟被分频到1MHzPWM周期1kHz算一下就明白72MHz除72等于1MHz再计数100次输出频率为10kHz。对直流减速电机来说10kHz既能避开人的听觉敏感区也不会让驱动芯片发热严重。占空比范围是0到99对应PWM比较值。电机驱动的C代码按下边这个骨架写#include main.h #include stdlib.h void Motor_Init(void) { GPIO_InitTypeDef gpio {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); gpio.Pin GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11; gpio.Mode GPIO_MODE_OUTPUT_PP; gpio.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, gpio); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_2); } void Motor_SetSpeed(int16_t left, int16_t right) { left left 99 ? 99 : (left -99 ? -99 : left); right right 99 ? 99 : (right -99 ? -99 : right); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, left 0 ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_9, left 0 ? GPIO_PIN_RESET : GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, abs(left)); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_10, right 0 ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_11, right 0 ? GPIO_PIN_RESET : GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_2, abs(right)); }代码里left的正负代表方向绝对值代表PWM占空比。PA8/PA9控制左电机PA10/PA11控制右电机正好四根方向线。上面这个映射是按“IN11IN20时正转”写的如果你的TB6612接线相反把代码里PA8和PA9的SET/RESET对调即可不需要动硬件。3. MPU6050姿态解算与自平衡PID控制代码自平衡的核心不是让小车永远停在0度而是让倾角收敛在平衡点附近。MPU6050里有三轴加速度计和三轴陀螺仪加速度计测的是比力静态时能从重力分解算出倾角但运动加速度会污染结果陀螺仪测的是角速度短期很准积分后会漂移。二者互补才能得到平稳倾角。对毕设来说互补滤波比卡尔曼更直观也足够满足循迹小车的需求。3.1 用HAL库读取MPU6050原始数据MPU6050的寄存器地址从0x3B开始连续存放加速度、温度、角速度数据一次读14个字节最方便。写入配置时先把设备唤醒再设置采样率、陀螺仪量程和加速度计量程。#define MPU6050_ADDR 0xD0 // 0x68左移1位后的8位地址 static void MPU_WriteReg(uint8_t reg, uint8_t val) { HAL_I2C_Mem_Write(hi2c1, MPU6050_ADDR, reg, I2C_MEMADD_SIZE_8BIT, val, 1, 100); } static void MPU_ReadReg(uint8_t reg, uint8_t *buf, uint16_t len) { HAL_I2C_Mem_Read(hi2c1, MPU6050_ADDR, reg, I2C_MEMADD_SIZE_8BIT, buf, len, 100); } void MPU_Init(void) { MPU_WriteReg(0x6B, 0x00); // 退出睡眠模式 MPU_WriteReg(0x19, 0x07); // 采样率分频约125Hz MPU_WriteReg(0x1B, 0x08); // 陀螺仪量程 ±500dps MPU_WriteReg(0x1C, 0x00); // 加速度计量程 ±2g } void MPU_GetData(float *ax, float *ay, float *az, float *gx, float *gy, float *gz) { uint8_t buf[14]; int16_t raw[7]; MPU_ReadReg(0x3B, buf, 14); for (uint8_t i 0; i 7; i) { raw[i] (int16_t)((buf[i * 2] 8) | buf[i * 2 1]); } *ax raw[0] / 16384.0f; *ay raw[1] / 16384.0f; *az raw[2] / 16384.0f; *gx raw[4] / 65.5f; *gy raw[5] / 65.5f; *gz raw[6] / 65.5f; }这里量程对应的灵敏度必须写对。±2g时加速度计每LSB代表16384分之一g±500dps时陀螺仪每LSB代表65.5分之一度每秒。采样率分频设为7实际输出约125Hz对平衡车足够了。如果主循环里还要跑串口和检测125Hz能留下足够的时间余量。读到数据后先别急着算角度把六路浮点数通过串口打印出来观察数值是否随板子方向变化。如果某个轴符号总反就在后续代码里统一取反不要改硬件。3.2 互补滤波得到车体倾角车体倾角通常定义为竖直方向与车身竖轴的夹角。加速度计用z轴和某一水平轴算atan2输出的是“重力方向”没有漂移但有噪声陀螺仪负责短时间内的变化量这么互补float balance_angle 0.0f; float balance_gyro 0.0f; void Balance_Update(float ax, float ay, float az, float gx, float gy, float gz) { static uint32_t last_tick 0; float dt; float acc_angle; uint32_t now HAL_GetTick(); dt (float)(now - last_tick) / 1000.0f; if (dt 0.0001f || dt 0.02f) { dt 0.008f; // 异常时间戳保护 } last_tick now; acc_angle atan2f(ax, az) * 57.29578f; balance_gyro gy; // 安装方向若反这里需要取负 balance_angle 0.96f * (balance_angle balance_gyro * dt) 0.04f * acc_angle; }系数0.96和0.04表示相信陀螺仪积分更多加速度计只用来校正长期漂移。dt是两次调用之间的实际时间和采样率分频有关。不能假设HAL_GetTick每5ms一定被调用一次因为主循环里可能有串口阻塞所以每次都要算dt。如果角度方向反了把balance_gyro gy改成balance_gyro -gy然后上车验证用手向前推车头串口角度应为负。3.3 直立PD与速度环的参数表有了角度和角速度直立控制用PD就够了。P项给回复力矩D项抑制振荡。速度环不一定每个型号都做但自平衡循迹小车跑起来后会持续向一个方向漂移必须让车在目标速度为0时保持静止所以速度环用PI把积分项当成PWM偏移。参数建议初值调节现象与做法Kp_balance150到250车能明显回正但会低频前后摆动Kd_balance0.6到1.2低频摆动消失车身变硬Kp_speed3到6车被推一下后能回到原位Ki_speed0.05到0.15消除持续漂移积分要限幅PWM输出上限70避免堵转和超调留30%余量直立PWM计算的代码通常长这样int16_t Balance_GetPWM(void) { int16_t pwm; pwm (int16_t)(Kp_balance * balance_angle Kd_balance * balance_gyro); if (pwm 70) pwm 70; if (pwm -70) pwm -70; return pwm; }为什么上限是70而不是99因为小车还需要余量去响应倾斜突变和循迹差速。如果主控制输出已经到99左右轮就无法再差值视觉循迹会失效。限幅不是简单截止它会让控制律进入非线性区所以实际调参时要确保大部分时间PWM不超过上限。4. OpenMV循迹识别与STM32串口通信OpenMV在自平衡小车里主要负责一件事找到地面黑线的横向偏移量然后发给STM32。它不需要发送整幅图像也不需要用PWM或GPIO做模拟信号一条115200波特率的串口线就能把结果包传过去。图像处理任务放在OpenMV自带的MicroPython环境里跑STM32只做运动控制这条思路既降低STM32负荷也让答辩时能单独演示视觉部分。4.1 OpenMV灰度阈值识别并计算偏移量OpenMV的find_blobs函数默认用LAB颜色空间黑色胶带对应的L通道很低阈值可以写成(0, 30, -20, 20, -20, 20)。取ROI时避开画面上部因为远处黑线角度变化大容易干扰计算。摄像头固定在车身前方稍向下倾斜视野里保留近处一段黑线即可。import sensor, image, time from pyb import UART uart UART(3, 115200) sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QQVGA) # 160x120 sensor.set_vflip(True) sensor.skip_frames(2000) thresholds [(0, 30, -20, 20, -20, 20)] roi (0, 60, 160, 60) def make_frame(err): hisb (err 8) 0xFF losb err 0xFF ck 0xAA ^ hisb ^ losb return bytearray([0xAA, hisb, losb, ck, 0x55]) while True: img sensor.snapshot() blobs img.find_blobs(thresholds, roiroi, pixels_threshold30, area_threshold30, mergeTrue) if blobs: max_blob max(blobs, keylambda b: b.area()) err max_blob.cx() - img.width() // 2 uart.write(make_frame(err)) time.sleep_ms(20)err范围是-80到80负数表示黑线在画面中心左侧正数在右侧。每次只发一帧数据帧格式是帧头、两个偏移字节、校验字节、帧尾。这里校验用XOR简单但够用因为控制环路每20ms就在纠错一次偶尔丢一帧不会让车失控。make_frame里的hish字节和los字节把err拆成高8位和低8位即使偏移超过255也能传输。如果赛道是白底黑线阈值编辑器里把L通道范围选在10到50之间即可。如果用的是浅色地板上贴黑色电工胶带不建议调高sensor亮度反而应该把曝光固定住避免环境光闪烁让阈值来回跳。4.2 STM32串口中断接收循迹数据包STM32端用USART3接收串口中断一次只收一个字节再用状态机组帧。这样OpenMV就算中间乱发一两个字节状态机也会退回等待帧头不会造成整个缓冲区解析错位。static uint8_t uart_rx_byte 0; static uint8_t rx_state 0; static int16_t rx_line_err 0; static int16_t line_err 0; static uint8_t rx_xor 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart ! huart3) { return; } switch (rx_state) { case 0: if (uart_rx_byte 0xAA) { rx_state 1; rx_xor 0xAA; } break; case 1: rx_line_err (int16_t)(uart_rx_byte 8); rx_xor ^ uart_rx_byte; rx_state 2; break; case 2: rx_line_err | uart_rx_byte; rx_xor ^ uart_rx_byte; rx_state 3; break; case 3: rx_state (uart_rx_byte rx_xor) ? 4 : 0; break; case 4: if (uart_rx_byte 0x55) { line_err rx_line_err; } rx_state 0; break; } HAL_UART_Receive_IT(huart3, uart_rx_byte, 1); }在主函数里要先启动一次HAL_UART_Receive_IT(huart3, uart_rx_byte, 1)之后每次回调结束时再补启动一次。状态机的思路很简单case 0等0xAAcase 1收高字节case 2收低字节case 3校验case 4收帧尾。只有校验通过后才把rx_line_err赋值给全局变量line_err。得到line_err之后在直立控制主循环里做差速int16_t left_pwm balance_pwm line_err * K_line; int16_t right_pwm balance_pwm - line_err * K_line; K_line 0.6f;K_line就是循迹灵敏度。初始值取0.4到0.8数值太大会让车子过冲黑线摆动激烈。注意这个公式是在balance_pwm已经稳定的前提下叠加的如果车身还在低频晃动先把直立PD调稳再增大K_line。5. 自平衡循迹小车联调顺序与运行中热调PID真正联调不是一次把代码全下进去就能跑通常按倒序验证先让OpenMV的数据沿串口流动再单独调直立最后合起来跑循迹。顺序弄反了车倒在地上你分不清是角度反了、Kd太小还是OpenMV把转向信号灌进了平衡环。第一步在STM32上屏蔽电机输出只把balance_angle和balance_gyro打印出来。水平放置时角度应接近0因为互补滤波会收敛到加速度计读数。第二步把车拿在手上绕电机轴转动观察角度是否连续变化如果数字跳变或卡住多半是I2C偶尔失败需要在MPU_ReadReg后加错误重试。第三步架空电机给PWM一个很小的固定值确认左右轮转向和Motor_SetSpeed里的正负关系一致。现象原因方向调整方法车身低频前后摆动最终倒下Kp太小放大Kp_balance直到车能拉回高频颤动电机噪声尖锐Kd过大或电源不稳减小Kd电机电源加100uF电容直立稳定但往一个方向漂移速度积分不够加大Ki_speed并检查轮子是否打滑循迹时蛇形前进K_line过大减小K_line或者降低期望速度调试中最有用的技巧是运行中热调参。把Kp、Kd、Ki保存在普通全局变量里通过USART1发字符串B200,0.8,0.05STM32解析后直接修改参数不必每次改代码重新编译。解析代码可以放在main函数的主循环里从DMA接收缓冲区取出整行再sscanfchar cmd[32]; if (sscanf(cmd, B%d,%f,%f, kp_tmp, kd_tmp, ki_tmp) 3) { Kp_balance (float)kp_tmp; Kd_balance kd_tmp; Ki_speed ki_tmp; }这样的热调参方式还能和最后的文档配合你把一组表现最好的参数记在Keil工程注释里同时把串口命令写入OpenMV侧的测试脚本答辩演示时先恢复默认参数再手动输入整定后的参数就能展示出“参数可配置”和“控制可复现”两个加分项。调完参数后用STM32内置Flash的最后一页保存这组值下次上电优先读取再通过串口命令覆盖才算把自平衡循迹小车的调试闭环走完。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻