STM32 自平衡小车:标准库 / HAL 库 / FreeRTOS 三版本源码与实现
源码地址:https://github.com/uiuiiii1/balance-car
基于 STM32F103C8T6 的自平衡小车(双轮)固件源码,提供三个版本:STM32 标准外设库裸机版、CubeMX + HAL 库裸机版、HAL + FreeRTOS 多任务版。三者硬件平台与算法骨架一致,区别在于 MCU 软件库与运行架构,便于对比学习。
致谢:本项目主要参考了江科大的 STM32 入门教程。标准库版本主要参考该教程的寄存器写法与外设驱动;HAL 库版与 FreeRTOS 版虽然实现方式不完全相同,但整体设计思路与算法同样参考了该教程。感谢江科大的开源教学。
一、项目概述
自平衡小车是嵌入式入门的经典项目:双轮结构没有支撑脚,必须依靠传感器实时反馈 + 控制算法不断调整电机 PWM,才能让车身保持竖直平衡。本项目用 STM32F103C8T6 实现,硬件与算法固定,提供三个不同软件架构的版本,目的是让你看清同一个项目在不同软件范式下的取舍。
| 版本 | 目录 | 库 | 架构 | 定位 |
|—|—|—|—|—|
| 标准库版 | standard-lib/ | STM32 标准外设库 | 裸机单循环 | 入门,理解底层寄存器 |
| HAL 库版 | hal-lib/ | STM32 HAL 库(CubeMX 生成) | 裸机单循环 | 工程主流,跨型号移植友好 |
| FreeRTOS 版 | freertos-hal/ | HAL 库 + FreeRTOS | 多任务 | 工程化、模块化、学习实时操作系统 |
源码地址:https://github.com/uiuiiii1/balance-car
二、硬件平台
-
主控:STM32F103C8T6(72MHz,20KB RAM,64KB Flash)
-
姿态传感器:MPU6050(I2C 接口,加速度 + 陀螺仪,互补滤波融合)
-
驱动:STM32 内置定时器 PWM 驱动直流电机,支持正反转
-
测速:电机编码器 + 定时器输入捕获
-
人机交互:OLED 显示屏、按键、USART 串口(可蓝牙透传,支持在线调参)
三版本共用同一份原理图(仓库根目录 schematic.pdf)。
三、控制算法:级联 PID
小车平衡的核心是级联 PID 控制环:
-
角度环(内环):以 MPU6050 的角度(加速度 + 陀螺仪互补滤波)为反馈,输出期望速度,保证姿态稳定。
-
速度环(中环):以编码器速度为反馈,输出左右轮差速,保证小车不前倒后倒。
-
转向环(外环):左右轮速差控制转向。
三个环嵌套工作:角度环决定"应该往哪边动",速度环决定"动多快",转向环决定"往哪边偏"。
PID 核心实现
三个版本共用同一套 PID 结构,下面是标准库版的 PID.c(其他版本基本一致):
void PID_Update(PID_t *p)
{
p->Error1 = p->Error0;
p->Error0 = p->Target - p->Actual;
// 积分项(带限幅,防止积分饱和)
if (p->Ki != 0)
{
p->ErrorInt += p->Error0;
if(p->ErrorInt > p->ErrorIntMax) p->ErrorInt = p->ErrorIntMax;
if(p->ErrorInt < p->ErrorIntMin) p->ErrorInt = p->ErrorIntMin;
}
else
{
p->ErrorInt = 0;
}
// P + I + D 计算
// 微分项用"输出微分"形式(用 Actual 差分,而非 Error 差分)
// 这样可以避免目标值突变时微分项剧烈跳变
p->Out = p->Kp * p->Error0
+ p->Ki * p->ErrorInt
- p->Kd * (p->Actual - p->Actual1);
// 死区偏置:输出为正时加偏置,为负时减偏置
// 用于抵消电机启停死区,提升低速响应
if(p->Out > 0) p->Out += p->OutOffset;
if(p->Out < 0) p->Out -= p->OutOffset;
// 输出限幅
if (p->Out > p->OutMax) p->Out = p->OutMax;
if (p->Out < p->OutMin) p->Out = p->OutMin;
p->Actual1 = p->Actual;
}
几个值得注意的设计点:
-
微分用"输出微分"(
Actual差分而非Error差分):当目标值(Target)突变时,如果用 Error 微分,微分项会瞬间跳变,导致输出抖动。改用 Actual 微分可以规避这个问题,对平衡车这种目标值相对稳定的场景特别合适。 -
积分限幅(
ErrorIntMax/Min):防止误差长时间累积导致积分饱和,小车重新稳定后不会因为"历史欠账"而冲过头。 -
死区偏置(
OutOffset):直流电机在低速时有死区(PWM 太小带不动),通过给非零输出加偏置来抵消,提升低速响应。
三个 PID 的初始参数
标准库版 main.c 中定义了三个 PID 的初始参数:
PID_t AnglePID = { .Kp=5, .Ki=0.1, .Kd=4, .OutMax=100, .OutMin=-100, ... };
PID_t SpeedPID = { .Kp=1.0, .Ki=0.05, .Kd=0, .OutMax=20, .OutMin=-20, ... };
PID_t TurnPID = { .Kp=5, .Ki=5, .Kd=0, .OutMax=50, .OutMin=-50, ... };
参数需要按硬件调试,不能照搬。蓝牙串口调参的协议见 Serial.c。
四、标准库版:裸机单循环
标准库版用 STM32 标准外设库,寄存器操作直观,适合理解底层。主循环把所有功能塞进一个 while(1):
int main()
{
OLED_Init();
MPU6050_Init();
Serial_Init();
Moter_Init();
LED_Init();
Key_Init();
Encoder_Init();
Encoder_Init2();
Moter_Init();
Timer_Init();
while(1)
{
if(RunFlag) LED_ON(); else LED_OFF();
// 按键启停
if(Key_Check(KEY_SINGLE))
{
if(RunFlag == 0)
{
PID_Init(&AnglePID);
PID_Init(&SpeedPID);
PID_Init(&TurnPID);
RunFlag = 1;
}
else
{
RunFlag = 0;
}
}
// OLED 显示角度、速度、转向三环的状态
OLED_Clear();
OLED_Printf(0, 0, OLED_6X8, " Angle", AX);
OLED_Printf(0, 8, OLED_6X8, "P:%05.2f", AnglePID.Kp);
// ... 更多显示代码 ...
OLED_Update();
// 串口调参
if (Serial_RxFlag == 1)
{
// 解析蓝牙传来的 PID 参数
Serial_RxFlag = 0;
}
}
}
特点:
-
代码结构直观,初学者容易理解每个外设的初始化与调用顺序
-
所有逻辑在同一个循环里串行执行,谁慢谁拖累全局(OLED 刷屏、蓝牙解析、控制逻辑互不影响彼此)
-
控制循环周期由定时器中断触发(
Timer_Init配置),PWM 输出准点更新 -
单文件
main.c直观,但功能越多while(1)越臃肿
五、HAL 库版:CubeMX 生成 + 工程化写法
HAL 库版用 STM32CubeMX 生成初始化代码,外设寄存器封装在 HAL 驱动里,跨型号移植更友好。工程结构由 CubeMX 生成:
hal-lib/
├── Core/ # CubeMX 生成的核心文件
│ ├── Inc/ # 头文件(main.h、stm32f1xx_hal_conf.h)
│ └── Src/ # 源文件(main.c、stm32f1xx_it.c、usart.c、gpio.c)
├── Drivers/ # 外设驱动
│ ├── CMSIS/ # ARM 内核与设备头文件
│ ├── STM32F1xx_HAL_Driver/ # STM32 HAL 驱动库
│ ├── Hardware/ # 电机、编码器、MPU6050、PWM、I2C、按键、LED
│ ├── OLED/ # OLED 驱动
│ ├── SERIAL/ # 串口驱动
│ └── User/ # PID 等用户模块
└── MDK-ARM/ # Keil MDK 工程文件
与标准库版的对比:
-
CubeMX 图形化配置外设,生成的初始化代码不用手写,省心且不易出错
-
HAL 驱动封装了寄存器细节,写
HAL_GPIO_WritePin而不是直接操作GPIOB->ODR -
跨 STM32 型号移植时,只需换型号重新生成,业务代码不用大改
-
代价是代码层级更深,调试时要穿透 HAL 封装才能看到寄存器状态
六、FreeRTOS 版:多任务架构
FreeRTOS 版在 HAL 基础上引入实时操作系统,把功能拆成独立任务,控制环置于最高优先级。
任务划分
| 任务 | 职责 | 优先级 | 调度 |
|—|—|—|—|
| control_task | 控制环(角度/速度/转向 PID 计算、PWM 输出) | 最高(4) | 10ms 周期 |
| comm_task | 串口通信(蓝牙调参协议解析) | 中 | 信号量唤醒 |
| display_task | OLED 显示刷新 | 中低 | 延时阻塞 |
| key_task | 按键扫描与处理 | 中低 | 延时阻塞 |
关键配置(FreeRTOSConfig.h)
#define configCPU_CLOCK_HZ 72000000
#define configTICK_RATE_HZ 1000 // 系统 tick 1kHz
#define configMAX_PRIORITIES 5
#define configMINIMAL_STACK_SIZE 128 // 最小任务栈 128 字
#define configMAX_SYSCALL_INTERRUPT_PRIORITY \
( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << ( 8 - configPRIO_BITS ) )
FreeRTOS 版 main.c 中关于控制环的说明
// TIM4 仅作为 HAL 时基(控制逻辑已迁移至 control_task 任务)
HAL_IncTick();
控制逻辑从"定时器中断"迁到了"独立任务",这是裸机版和 FreeRTOS 版最大的架构差异。
七、FreeRTOS 版的优缺点(实战总结)
这部分是本项目实际验证过的经验,详细对比见仓库 docs/平衡车FreeRTOS优缺点总结.docx。
优点
-
模块解耦,各功能互不阻塞:OLED 显示、蓝牙解析、按键扫描各自独立,OLED 刷屏不再拖累控制环。实测蓝牙实时调 PID 与 OLED 显示、按键启停能同时正常工作。
-
实时性有明确保障:控制环置于最高优先级(10ms 周期),不会被显示/通信任务打断。
-
通信机制规范:串口收帧用信号量唤醒
comm_task(无数据时任务睡眠,不空转占 CPU);PID 参数用互斥锁保护(蓝牙写与控制环读不冲突)。 -
可扩展性强:以后加传感器(超声波)、加任务(避障)、加模式,只需新增任务,不动其他逻辑。
缺点
-
学习成本和调试难度陡增:踩过
configMAX_SYSCALL_INTERRUPT_PRIORITY需左移 4 位、SVC/PendSV/SysTick 宏重命名、TIM4 兼任 HAL 时基、静态/动态内存选择等坑。写错就是configASSERT卡死。 -
任务切换有开销和抖动:控制环从"定时器中断"(准点触发)搬到"任务"后,存在微秒级调度延迟和周期抖动。搬完后需要微调 PID(采样时刻变了),裸机版调好的参数不能直接搬。
-
RAM 占用增加:每个任务需要独立的栈 + TCB(静态任务栈 128~256 字 × 多个),加上内核对象。裸机版一个 main 栈搞定,FreeRTOS 版 ZI-data 从几千涨到 1.7 万+。F103C8 只有 20KB RAM,任务开多会吃紧(为此删掉了软件定时器来省内存)。
-
中断使用受限:优先级低于
configMAX_SYSCALL_INTERRUPT_PRIORITY的中断不能调用 RTOS API。为此把 USART2 优先级从 0 改到 5。中断里只能调用FromISR后缀的函数。 -
代码量/复杂度增加:
AppTasks.c+FreeRTOSConfig.h+ 各种钩子函数,远比裸机复杂。
对比一览
| 对比项 | 裸机版 | FreeRTOS 版 |
|—|—|—|
| 控制实时性 | 更优(中断准点触发) | 够用但略逊(任务调度有抖动) |
| 代码可读性 | 简单直观 | 复杂,需理解 RTOS 概念 |
| 模块扩展 | 加功能会臃肿 | 加任务即扩展,清晰 |
| 多任务并行 | 单循环互阻塞 | 各自独立并行 |
| 通信机制 | 全局变量 + 标志位 | 信号量 / 互斥锁 / 队列 |
| RAM 占用 | 小 | 大(栈 + TCB + 内核对象) |
| 调试难度 | 低 | 高(配置陷阱多) |
结论
对"单纯让小车更稳",裸机版更强;对"工程化、多模块、可扩展、学习 RTOS",FreeRTOS 是正确选择。本项目保留两个版本互相备份:裸机版是性能最优参考,FreeRTOS 版是工程化/学习版。若未来追求最高控制实时性,可在 FreeRTOS 版中将核心控制环保留在定时器中断中,其余外围逻辑放任务,兼顾实时性与模块化。
八、获取源码与编译
源码地址:https://github.com/uiuiiii1/balance-car
三个版本均包含 Keil MDK-ARM 工程文件(.uvprojx),用 Keil uVision5 打开对应项目即可编译下载:
-
安装 Keil uVision5(含 STM32F1 支持包)
-
打开对应版本的
.uvprojx工程文件(这个没改千万别用) -
编译(F7 → Rebuild),下载(F8)到 STM32F103C8T6 最小系统板
-
按原理图
schematic.pdf接好电机、MPU6050、OLED、串口等外设
九、参考
-
江科大自化协 / 江科大众志 — STM32 系列教程(本项目主要参考)
-
STM32F103C8T6 官方参考手册与数据手册
-
FreeRTOS 官方文档(嵌入式实时操作系统)
本仓库源码用于学习与交流。原理图与代码的原始著作权归原作者所有,参考的江科大教程版权归原作者所有。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)