源码地址: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 控制环

  1. 角度环(内环):以 MPU6050 的角度(加速度 + 陀螺仪互补滤波)为反馈,输出期望速度,保证姿态稳定。

  2. 速度环(中环):以编码器速度为反馈,输出左右轮差速,保证小车不前倒后倒。

  3. 转向环(外环):左右轮速差控制转向。

三个环嵌套工作:角度环决定"应该往哪边动",速度环决定"动多快",转向环决定"往哪边偏"。

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

优点

  1. 模块解耦,各功能互不阻塞:OLED 显示、蓝牙解析、按键扫描各自独立,OLED 刷屏不再拖累控制环。实测蓝牙实时调 PID 与 OLED 显示、按键启停能同时正常工作。

  2. 实时性有明确保障:控制环置于最高优先级(10ms 周期),不会被显示/通信任务打断。

  3. 通信机制规范:串口收帧用信号量唤醒 comm_task(无数据时任务睡眠,不空转占 CPU);PID 参数用互斥锁保护(蓝牙写与控制环读不冲突)。

  4. 可扩展性强:以后加传感器(超声波)、加任务(避障)、加模式,只需新增任务,不动其他逻辑。

缺点

  1. 学习成本和调试难度陡增:踩过 configMAX_SYSCALL_INTERRUPT_PRIORITY 需左移 4 位、SVC/PendSV/SysTick 宏重命名、TIM4 兼任 HAL 时基、静态/动态内存选择等坑。写错就是 configASSERT 卡死。

  2. 任务切换有开销和抖动:控制环从"定时器中断"(准点触发)搬到"任务"后,存在微秒级调度延迟和周期抖动。搬完后需要微调 PID(采样时刻变了),裸机版调好的参数不能直接搬。

  3. RAM 占用增加:每个任务需要独立的栈 + TCB(静态任务栈 128~256 字 × 多个),加上内核对象。裸机版一个 main 栈搞定,FreeRTOS 版 ZI-data 从几千涨到 1.7 万+。F103C8 只有 20KB RAM,任务开多会吃紧(为此删掉了软件定时器来省内存)。

  4. 中断使用受限:优先级低于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断不能调用 RTOS API。为此把 USART2 优先级从 0 改到 5。中断里只能调用 FromISR 后缀的函数。

  5. 代码量/复杂度增加AppTasks.c + FreeRTOSConfig.h + 各种钩子函数,远比裸机复杂。

对比一览

| 对比项 | 裸机版 | FreeRTOS 版 |

|—|—|—|

| 控制实时性 | 更优(中断准点触发) | 够用但略逊(任务调度有抖动) |

| 代码可读性 | 简单直观 | 复杂,需理解 RTOS 概念 |

| 模块扩展 | 加功能会臃肿 | 加任务即扩展,清晰 |

| 多任务并行 | 单循环互阻塞 | 各自独立并行 |

| 通信机制 | 全局变量 + 标志位 | 信号量 / 互斥锁 / 队列 |

| RAM 占用 | 小 | 大(栈 + TCB + 内核对象) |

| 调试难度 | 低 | 高(配置陷阱多) |

结论

对"单纯让小车更稳",裸机版更强;对"工程化、多模块、可扩展、学习 RTOS",FreeRTOS 是正确选择。本项目保留两个版本互相备份:裸机版是性能最优参考,FreeRTOS 版是工程化/学习版。若未来追求最高控制实时性,可在 FreeRTOS 版中将核心控制环保留在定时器中断中,其余外围逻辑放任务,兼顾实时性与模块化。


八、获取源码与编译

源码地址:https://github.com/uiuiiii1/balance-car

三个版本均包含 Keil MDK-ARM 工程文件(.uvprojx),用 Keil uVision5 打开对应项目即可编译下载:

  1. 安装 Keil uVision5(含 STM32F1 支持包)

  2. 打开对应版本的 .uvprojx 工程文件(这个没改千万别用)

  3. 编译(F7 → Rebuild),下载(F8)到 STM32F103C8T6 最小系统板

  4. 按原理图 schematic.pdf 接好电机、MPU6050、OLED、串口等外设


九、参考

  • 江科大自化协 / 江科大众志 — STM32 系列教程(本项目主要参考)

  • STM32F103C8T6 官方参考手册与数据手册

  • FreeRTOS 官方文档(嵌入式实时操作系统)


本仓库源码用于学习与交流。原理图与代码的原始著作权归原作者所有,参考的江科大教程版权归原作者所有。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐