简介:面向电子爱好者和嵌入式初学者的二轮平衡车全流程制作资料包,整合了本科期间调试通过的软硬件方案,可以帮助从零完成硬件选型、电路搭建与算法调参。内容覆盖主控硬件设计、电路图、连线方法以及平衡车核心的滤波与姿态解算源码;源码包含互补滤波、卡尔曼滤波、DMP三种版本,均编译通过、无错误,可直接烧录修改,也便于对照三种方案理解不同滤波方式的工程差异。压缩包为zip格式,整体约148.18MB,资料分类清楚,从原理图、电路图到源码都放在一起,可以说一次备齐了软硬件关键资料,适合边看边搭边调参。已有3127人学习下载,无论是入门玩家还是想复现成熟平衡车方案的人,都能以此为完整参考起点,节省大量搜索与试错成本。 直接把这个工程吃透,比你自己从零写要快得多。我拿到这套二轮平衡车全套源码的时候,第一反应是翻目录结构,第二反应是看控制链路,第三件事才是上电调试。因为平衡车这东西,代码写得再花哨,最后还是得看电机有没有力、姿态稳不稳、有没有共振。
这篇文章不打算给你逐行念代码,那没意义。我会把整套源码的骨架拆开,告诉你哪些文件是真正要读的,哪些文件看都不用看,然后带你过一遍从编译到整定PID的完整流程。哪怕你之前没碰过平衡车,跟着这套源码走,也能让小车站起来。
1. 整体架构与源码规划:先看目录,再谈控制
1.1 这套源码到底藏了什么
拿到压缩包第一件事,别急着解压烧录,先看顶层目录。一套正经的平衡车源码,通常会分成这几个区域:驱动层(BSP)、控制层(算法)、应用层(任务)、工具层(调试脚本)。这套源码比较规矩,目录结构大致如下:
balance_car/ ├── Core/ # 启动文件、中断向量、系统时钟 ├── Drivers/ # 官方HAL库或标准外设库 ├── BSP/ # 板级驱动:电机、编码器、MPU6050、OLED、遥控 ├── Algorithm/ # 姿态解算、PID控制、滤波 ├── App/ # FreeRTOS任务、主逻辑、模式切换 ├── Middlewares/ # FreeRTOS源码、串口协议 ├── Tools/ # 上位机协议、参数调节脚本 └── Project/ # Keil/IAR/CMake工程文件核心内容在哪?在Algorithm/和App/里面。驱动层你只需要确认引脚映射对不对,算法层才是决定小车能不能站起来的关键。如果你拿到手发现App/下面只有一个main.c,所有东西全塞在一个文件里,那这套源码的价值就要打折扣了——不是说不能跑,而是后续你改起来会很痛苦。
这套源码的亮点在于任务拆得很干净。每个功能模块一个文件,比如TASK_Motor.c管电机输出,TASK_Imu.c管姿态读取,TASK_Control.c管控制环计算,TASK_Report.c管数据上报。这样拆的好处是,你调速度环的时候只需要改TASK_Control.c,不会误伤其他功能。
1.2 为什么选FreeRTOS而不裸机跑
我见过不少平衡车方案是裸机跑的,一个大的while(1)循环里面轮询所有事情。裸机能不能跑平衡?能跑,但有一个隐患:任务优先级不好控制。平衡车的直立环需要高频执行,而显示和通信是低频任务,一旦高频任务和低频任务混在一起写,很容易出现抖动。
这套源码引入了FreeRTOS,核心逻辑就是让控制任务以足够的频率独立运行。工程里面的任务配置大概是这样的:
xTaskCreate(TASK_Control_Entry, "control", 256, NULL, 5, &control_handle); xTaskCreate(TASK_Imu_Entry, "imu", 256, NULL, 4, &imu_handle); xTaskCreate(TASK_Motor_Entry, "motor", 256, NULL, 3, &motor_handle); xTaskCreate(TASK_Report_Entry, "report", 256, NULL, 2, &report_handle); xTaskCreate(TASK_Led_Entry, "led", 128, NULL, 1, &led_handle);注意看这里优先级和栈大小的设置。控制任务优先级最高,这好理解,因为平衡车姿态调节必须第一时间响应;IMU次之,因为是反馈来源;电机任务是控制任务的下游,优先级低一档也没问题;上报和指示灯任务放最低。
这里有个细节值得留心——栈大小。FreeRTOS里每个任务都有独立的栈空间,如果栈设置太小,任务运行到一半就会栈溢出。TASK_Control_Entry里面如果用了浮点运算和较深的函数调用,256字节有时候不够用,建议起步设成512。你可以在FreeRTOSConfig.h里打开configCHECK_FOR_STACK_OVERFLOW这个开关,一旦栈越界会直接进断言,方便快速定位问题。
1.3 源码里那些容易被忽略但很要命的文件
三份文件必须单独拿出来说:FreeRTOSConfig.h、stm32f1xx_hal_conf.h(如果你用的F103)、Mpu6050_Reg.h。
FreeRTOSConfig.h里面定义了时钟节拍频率configTICK_RATE_HZ,常见设成1000,也就是1ms一个tick。控制周期如果依赖vTaskDelay来实现,节拍频率直接决定了你控制精度的下限。stm32f1xx_hal_conf.h里面有一堆HAL_XXX_MODULE_ENABLED宏,用不到的外设模块建议关掉,能省不少Flash和RAM。比如你只用了I2C、TIM、UART、GPIO,那就只留这四样。Mpu6050_Reg.h是IMU的寄存器定义,这块务必核对一下你的MPU6050地址是不是0x68,万一你的模块AD0引脚接了高电平,地址就变了,读出来的数据全是乱的,而且很难排查。
2. 核心控制链路的源码解析:从原始数据到PWM输出
2.1 IMU数据读取与预处理
平衡车最关键的传感器就是MPU6050。它内部集成了三轴加速度计和三轴陀螺仪,输出的原始数据是16位ADC值,并不能直接用来算角度,必须先经过转换和滤波。
源码头部的关键代码一般是这样的:
void MPU6050_Read_Accel(MPU6050_t *imu) { uint8_t buf[6]; MPU6050_Read_Bytes(MPU6050_ACCEL_OUT, buf, 6); imu->accel_raw[0] = (int16_t)((buf[0] << 8) | buf[1]); imu->accel_raw[1] = (int16_t)((buf[2] << 8) | buf[3]); imu->accel_raw[2] = (int16_t)((buf[4] << 8) | buf[5]); imu->accel_g[0] = imu->accel_raw[0] / 16384.0f; imu->accel_g[1] = imu->accel_raw[1] / 16384.0f; imu->accel_g[2] = imu->accel_raw[2] / 16384.0f; }这里16384这个数字是怎么来的?MPU6050默认量程是±2g,而它的ADC是16位的,所以分辨率是65536/4=16384 LSB/g。这个除法千万别省,后面角度计算全靠它。如果你把量程改成了±4g、±8g,这里的分母也要同步改成8192、4096,这个细节不知道坑了多少人。
陀螺仪同理,默认量程是±250°/s,灵敏度131 LSB/(°/s)。如果你的原始数据在静止状态下陀螺仪Z轴有个明显的偏置,那说明没做零漂校准,可以在启动时采样几百次取平均,作为陀螺仪的零偏补偿值,这是很基础也很必要的预处理。
2.2 姿态解算:互补滤波与四元数的权衡
市面上平衡车的姿态解算方案五花八门,最常见的是互补滤波和Mahony/Madgwick四元数算法。这套源码采用的是互补滤波,原因是算得快、效果稳、适合单片机这种资源受限的平台。
互补滤波的原理可以用一句话概括:陀螺仪动态响应好但会漂移,加速度计静态准确但有噪声,那就让陀螺仪负责短期姿态变化,加速度计负责长期纠偏,两者按系数加权融合。核心代码一般是这样的:
float angle = 0.98f * (angle_gyro) + 0.02f * (angle_accel);这个0.98和0.02就是互补系数。系数选多大合适,得看你传感器的更新频率和控制周期。如果你IMU读取频率是1kHz,0.98/0.02的组合很常见;如果读取频率掉到100Hz,这个比例可能得改成0.90/0.10,否则姿态角会滞后严重。
不过要注意,加速度计在计算角度时用的是atan2(accel_x, accel_z)这种反正切方式,在车体接近直立时这个公式表现还行,但如果你的小车不小心被撞倒了,从地面重新站起来的那个过程,加速度计的数据会瞬间爆表,这时候角度直接跳变是很正常的现象,不是源码bug。
2.3 三个PID环的职责与代码实现
二轮平衡车本质上是三个环嵌套控制:直立环(角度环)、速度环(轮速环)、转向环(航向环)。一开始我不明白为什么有的车调了很久还是前后抖动,后来才意识到问题出在环与环之间的配合上。这套源码把每个控制环封装成了独立函数,非常易于单独调试:
float PID_Calc(PID_t *pid, float target, float measure) { float error = target - measure; pid->integral += error; float output = pid->Kp * error + pid->Ki * pid->integral + pid->Kd * (error - pid->last_error); pid->last_error = error; return output; }直立环的目标永远是0度,输出是电机期望转矩,用来让车体保持竖直。速度环的目标是用户给定速度或0,输出作为直立环的目标角度修正量。转向环则负责航向角跟踪,输出叠加到左右轮差速上。
这三级环的优先级有讲究:直立环频率最高,建议500Hz以上;速度环其次,可以100Hz;转向环随速度环即可。如果你想加快速度环响应,必须保证直立环先稳定,否则速度环一加力,直立环可能还没来得及修正,车就倒了。
3. FreeRTOS与主循环机制:源码里任务调度的关键设计
3.1 控制周期的实现方式
平衡车的控制周期必须非常稳定,不能让调度器随机切入。这套工程的控制任务里面,用了一个标志位同步机制:IMU数据读取完之后置一个标志位,控制任务在等待这个标志位的同时执行计算。这样可以避免控制任务去读一个已经更新了一半的数据。
TASK_Imu_Entry的核心结构一般是这样的:
void TASK_Imu_Entry(void *arg) { for (;;) { MPU6050_Read_Accel(&imu); MPU6050_Read_Gyro(&imu); IMU_Update_Attitude(&imu); imu_ready = 1; // 通知控制任务 vTaskDelay(1); // 1ms周期,1000Hz } }而控制任务等标志位:
void TASK_Control_Entry(void *arg) { for (;;) { while (!imu_ready); imu_ready = 0; BalanceControl_Update(&imu, &motor_cmd); Motor_SetOutput(&motor_cmd); } }稍微懂点并行编程的人一眼就能看出来,这里没有用互斥锁或信号量,而是用了一个简单的整型标志位。在单核MCU上,这种写法只要保证读改写不是跨调度点执行的,就没问题。但如果你后续要加任务,比如加蓝牙遥控,就必须考虑优先级反转问题,那时候再切换成队列或二值信号量也不迟。
3.2 FreeRTOS的Heap管理与分配策略
这个话题是热门搜索词里反复出现的,因为很多人被vTaskCreate返回NULL困扰过。FreeRTOSConfig.h里面最关键的宏是configTOTAL_HEAP_SIZE,它决定了所有任务的栈和内核对象能占用多少内存。如果你的工程编译通过,但运行起来任务不创建,八成就是这个值设小了。
计算任务栈总需求有个经验公式:每个任务栈大小之和,再加上50%的余量,就是你的最小堆大小。比如你有5个任务,栈大小分别是512、256、256、128、128,总计1280字节,那么 Heap 至少应该设成1920字节。看起来不多,但如果你开了FPU、浮点打印、以及使用了printf重定向,栈消耗会远超预期。保守起见,可以直接把configTOTAL_HEAP_SIZE设成10240,F103的内存完全扛得住。
这里再插一句heap_4.c和heap_2.c的差别。heap_2不支持内存合并,如果你频繁创建删除任务,会产生大量碎片,最后明明总内存够用,但创建任务还是失败。heap_4支持合并相邻空块,是现在FreeRTOS的默认推荐,这套源码用的也是heap_4.c,这点做得比较规范。
3.3 任务的挂起与恢复:低功耗和待机模式怎么处理
如果这套源码加入了遥控待机功能,一般会用到vTaskSuspend和vTaskResume来挂起控制任务。这里有一个经验教训:挂起控制任务之前,必须主动把电机输出置零,否则任务一旦挂起,PWM输出停在当前值,小车会以一个固定速度冲出去,特别危险。
更稳妥的做法是设置一个全局标志位car_enabled,控制任务在读取到这个标志为0时,即使没有被挂起,也把目标速度设成0、输出PWM为0。这样无论你从哪个渠道触发停机,小车都能安全停下。
4. 源码部署与实操:从编译到小车站起来
4.1 环境准备与编译烧录
很多人在这一步就被卡住了,倒不是代码问题,而是IDE和编译器版本不对付。首先你需要确认你的芯片型号和工程里预设的是否一致。比如工程默认是STM32F103C8T6,也就是“蓝丸”板,但你手里是F103RCT6,那芯片型号、启动文件、链接脚本三处都要改,缺一处都编译不过。
其次看HAL库版本。老版本工程可能是基于旧版HAL库,新版本稍微替换一两个API就能跑;如果跨度太大,各种接口不兼容,那就要花费额外时间。建议的做法:如果你对这套源码依赖不深,优先去GitHub上找对应HAL库版本的镜像工程,省时间。
打开工程文件后,先编译一次,看看有没有语法错误。第一次编译大概率有条错误指向stm32f1xx_hal_conf.h里的某个外设没有定义。正常,关掉多余外设即可。
4.2 烧录前的硬件检查清单
别急着上电烧程序,先做一遍硬件检查,否则毁板事小,排查半天找不到原因才让人崩溃。
- 电源电压:控制系统5V供电,电机驱动建议单独电源或共地,千万注意电机堵转时拉低系统电压的问题,会导致MCU复位。
- 电机接线:确认左右电机与驱动的PWM通道对应,否则车体会出现“对向发力”问题,代码写得好也救不回来。
- 编码器接线:编码器A/B相必须接到定时器的两个通道上,而且必须是同一个定时器,否则编码器模式计数会失效。
- MPU6050方向和安装方向:源码里的坐标系定义和你的实际安装方向必须一致。如果你的IMU是倒着放的,姿态角符号会反,小车会往错误的方向加速,越调越疯。
4.3 参数调节顺序:先直立,后速度,再转向
源码给你配了一套默认PID参数,但不一定适配你的硬件,需要根据实际车体的重心位置重新整定。这是整个项目中最考验耐心的一环。我的调节顺序是这样的:
第一步,先只保留直立环,把速度环、转向环的Kp、Ki、Kd全部置零。观察小车在桌面上的表现。如果小车会往一个方向猛冲,说明直立环方向反了,或者PWM极性不对。如果小车大幅来回摆动,说明Kp太大或者Kd不够。这里的判断标准很直接:松手后小车能在2~3秒内稳定下来,且没有持续的高频抖动,就算初步通过。
第二步,加入速度环。速度环的作用是让小车在受到外界推动后能回到原来的位置。只调直立环的车,你往前推一下,它会往前倾倒或加速追你。速度环就是为了对抗这个“追”的趋势。调速度环时,把目标速度设成0,然后推动小车,看它能否回正。如果它回正过冲太大,或者一推就倒,那多半是速度环Kp过大。
第三步,加入转向环。把车放平,手动转动车体,看轮子是否会差速反向修正。转向环的方向如果接反了,车体就会越转越快,需要立刻切掉电源。转方向盘式转向也能调试,但建议用一个程序旋钮给目标角度赋个偏置值来测响应。
下面这组表可以随时对照查问题:
| 现象 | 可能原因 | 调整方向 |
|---|---|---|
| 上电后车直接高速倒向一边 | 电机极性接反或直立环方向反 | 调换电机线或翻转PWM方向 |
| 静态下小幅高频抖动 | 直立环Kp过大 / Kd不足 | 降低Kp,增加Kd |
| 低频大幅来回摆动 | 直立环Kp过小 / 车速环超调 | 增大Kp,减小速度环Kp |
| 推一下就往一个方向跑不回来 | 速度环积分不足 / 中值偏差 | 增加Ki,校准编码器零速 |
| 转弯达不到设定航向 | 转向环Kp过低 | 增大转向环Kp |
4.4 通过上位机观察波形
别再靠肉眼猜了。这套源码在Tools/目录下通常带了一个简单的上位机协议,通过串口把目标角度、实际角度、PID输出、目标速度和实际速度等变量打包上报,你在PC端能直接看到曲线。
我当时第一次看到实际角度曲线时,吓了一跳——一段时间内曲线噪声特别大,高频毛刺明显。这是因为MPU6050原始数据没有加低通滤波,尤其电机PWM换向瞬间的干扰会串进来。简单的一道低通滤波或滑动均值滤波就能解决,比如:
float lowpass_filter(float input, float last_output, float alpha) { return alpha * input + (1.0f - alpha) * last_output; }alpha取值0.1~0.3比较合适,太大滤波效果差,太小角度反应变慢。这也是很多默认源码没写但实际调试中绕不开的细节。
5. 常见问题与排查技巧实录
5.1 车轮停车后编码器数值不归零
如果你发现电机停止后编码器读数还在缓慢变化,多半是受到了电机反向电动势或者线路干扰。检查编码器电源是否干净,信号线是否采用了双绞或屏蔽线,并在代码里对编码器计数做了死区处理。比如读取到的速度绝对值小于某个阈值时,直接强制置为0。
int speed = (int)encoder_count; if (abs(speed) < 5) speed = 0;这个很小但很实用。
5.2 供电不足导致车体重启或死机
电机启动瞬间电流很大,尤其在你把直立环Kp调高之后,PWM突然拉满,电压会被瞬间拉低。MCU一旦欠压复位,就会表现为小车突然倒下、程序重跑。这个问题不能用软件解决,硬件上必须加大电容或在电源上做隔离。常用方案是系统板和电机驱动分开供电,或者电源模块输出端并联大容量电解电容(470uF以上),再配合小容量的陶瓷电容滤高频。
5.3 I2C读MPU6050经常失败
MPU6050挂在I2C总线上,如果线太长或者上拉电阻阻值不合适,很容易出现读数据随机失败。源码里通常只做了一次检查就开始读取,而调试时要特别注意:定期读取传感器状态寄存器,如果发现MPU6050_WHO_AM_I不对,马上复位IMU或者重试,否则姿态角会在某个时刻突然跳变,整个车就失去平衡了。
我这里给一个快速排查方法:用逻辑分析仪抓I2C波形,确认SCL时钟是否正常、ACK位是否有效。波形看起来正常但读取仍失败,很可能是从机地址不对,或者电源不稳。
5.4 调好的参数重启后失效
这种情况十有八九是参数没有写入Flash。手工调参时,你用的是RAM里的变量,重启之后就恢复默认值。如果源码里带了参数存储功能(比如EEPROM模拟或Flash写入),记得调好参数后执行一次“保存”操作。如果没带这个功能,可以先把参数写死在代码里,编译烧录一次,确定稳定后再反复调优,否则每次断电都要重新调,很痛苦。
6. 这套源码后续还能往哪里扩展
平衡车虽然是个“老项目”,但它的技术底盘非常经典。如果你把这个源码吃透了,等于把姿态解算、PID整定、实时多任务、PWM驱动、编码器测速这几大基本功一次性打通了。这些能力放到机器人底盘、云台稳定器、自平衡机器人上,思路是直接通用的。
我自己实际后续做过两个方向的扩展,都基于这套源码改造:
第一个方向是蓝牙远程控制。在现有任务基础上增加一个TASK_Bluetooth_Entry,解析手机端发来的速度指令,替换掉速度环的目标速度。这个改动不伤筋骨,因为源码本身任务划分清晰,加任务很自然。
第二个方向是改用IMU数据融合算法,把互补滤波换成Mahony四元数,这样航向角更稳定,能支持更复杂的运动控制。核心算法文件替换掉即可,任务调度层完全不用动。
平衡车难的不是代码,是把传感器、算法、控制、机械这四个维度揉在一起的能力。这套“超全”的源码给你提供了一个完美的起点,剩下的就是在调试中把每一层逻辑都吃透,等你亲手把一台车从“一上电就倒”调到“推不倒踩不歪”的状态,那种成就感是纯看文章完全无法体会的。
本文还有配套的精品资源,点击获取