这款基于STM32F1和FreeRTOS的双轮平衡车项目,是我最近完成的一个嵌入式系统综合实践案例。作为一名长期从事嵌入式开发的工程师,我发现这个项目完美融合了实时控制、传感器融合和任务调度等核心技术要点,特别适合想要提升RTOS实战能力的朋友学习参考。
项目硬件采用轮趣科技的标准平衡车套件,我在原厂提供的裸机代码基础上进行了FreeRTOS移植和功能扩展。整个系统最核心的创新点在于:通过合理的任务划分和优先级设计,在资源有限的STM32F103C8T6上实现了稳定的10ms控制周期,同时还能兼顾人机交互和无线通信功能。
主控芯片选用STM32F103C8T6(俗称"蓝莓派"),这款72MHz的Cortex-M3内核MCU在成本与性能之间取得了很好的平衡。硬件资源分配经过精心设计,确保各外设互不冲突:
c复制// 关键外设引脚定义(部分示例)
#define MPU6050_INT_PIN GPIO_Pin_12 // PA12
#define ENCODER_LEFT_A GPIO_Pin_0 // PA0 (TIM2_CH1)
#define MOTOR_LEFT_PWM GPIO_Pin_8 // PA8 (TIM1_CH1)
特别要注意的是PWM输出和编码器接口都需要特定的定时器通道,我在原理图设计阶段就做好了规划:
MPU6050作为核心传感器,其I²C接口通过软件模拟实现(PB8/PB9),这样设计有两个好处:
实际调试中发现,某些廉价MPU6050模块需要降低I²C时钟速度(从400kHz降到100kHz)才能稳定工作。这是通过修改
i2c_delay()函数实现的。
编码器选用AB相增量式光电编码器(500线),配合STM32的编码器接口模式,可以实现高精度的转速测量。这里有个细节:编码器电源最好单独用LDO供电,避免电机干扰导致计数异常。
整个系统按照"实时性要求"划分为三个层级:
关键实时任务(控制环相关)
人机交互任务
后台任务
任务优先级设置遵循"关键任务优先"原则:
c复制#define TASK4_PRIO (tskIDLE_PRIORITY + 5) // 最高优先级
#define TASK3_PRIO (tskIDLE_PRIORITY + 4)
#define TASK5_PRIO (tskIDLE_PRIORITY + 3)
#define TASK1_PRIO (tskIDLE_PRIORITY + 2)
#define TASK2_PRIO (tskIDLE_PRIORITY + 2)
这个任务通过MPU6050的DRDY中断触发(PA12配置为外部中断),采用任务通知机制唤醒:
c复制void EXTI15_10_IRQHandler(void) {
if(EXTI_GetITStatus(EXTI_Line12) != RESET) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
vTaskNotifyGiveFromISR(xHandleTask3, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
EXTI_ClearITPendingBit(EXTI_Line12);
}
}
任务主体采用vTaskDelayUntil()实现精确的5ms周期:
c复制void vTask3(void *pvParameters) {
TickType_t xLastWakeTime = xTaskGetTickCount();
const TickType_t xFrequency = pdMS_TO_TICKS(5);
for(;;) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// 获取传感器数据(互斥量保护)
xSemaphoreTake(xMutex, portMAX_DELAY);
Get_Angle(&pitch, &roll); // 姿态解算
left_vel = Get_Velocity_Form_Encoder(ENCODER_LEFT);
distance = Read_Distance();
xSemaphoreGive(xMutex);
// 通知控制任务
xTaskNotifyGive(xHandleTask4);
vTaskDelayUntil(&xLastWakeTime, xFrequency);
}
}
这是系统的核心,实现三环PID控制:
c复制void vTask4(void *pvParameters) {
for(;;) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
xSemaphoreTake(xMutex, portMAX_DELAY);
Balance(&balance_out); // 平衡环
Velocity(&velocity_out); // 速度环
Turn(&turn_out); // 转向环
// PWM合成
motor_left = balance_out + velocity_out + turn_out;
motor_right = balance_out + velocity_out - turn_out;
PWM_Limit(&motor_left, &motor_right);
Set_Pwm(motor_left, motor_right);
xSemaphoreGive(xMutex);
}
}
PID参数整定是个技术活,我的经验是:
原厂代码使用的是标准卡尔曼滤波,我在实践中发现两个改进点:
c复制// 根据加速度计方差动态调整R
if(fabs(accel_lpf - accel_raw) > 0.2f) {
R *= 2.0f; // 运动状态增加观测噪声
}
平衡环采用PD控制:
c复制void Balance(float *out) {
static float err, last_err;
err = target_angle - pitch;
*out = balance_kp * err + balance_kd * (err - last_err);
last_err = err;
}
速度环需要特别注意积分饱和问题:
c复制// 速度环积分项处理
if(fabs(velocity_err) < 50) { // 死区
velocity_i_sum += velocity_err * velocity_ki;
velocity_i_sum = constrain(velocity_i_sum, -100, 100); // 抗饱和
}
电机抖动严重
平衡角度偏移
FreeRTOS任务卡死
uxTaskGetStackHighWaterMark()监控堆栈使用c复制// 将PID参数放大1000倍存储为整数
int32_t kp = (int32_t)(balance_kp * 1000);
// 计算时先做乘法后除法
output = (error * kp) / 1000;
任务通知替代队列:
对于简单的任务间通信,使用任务通知比队列效率高3-5倍。
关键代码放在RAM执行:
将PID计算等关键函数放到RAM中运行(通过__attribute__((section(".ramfunc")))实现),可以避免Flash访问延迟。
这个项目最让我有成就感的是,通过合理的RTOS任务设计,在72MHz的主频下实现了媲美裸机程序的实时性能。特别是在加入超声波避障功能后,整个系统仍然能保持稳定的10ms控制周期,这充分证明了FreeRTOS在嵌入式控制领域的实用价值。