从零打造智能小车:STM32F103C8T6与L298N的PID电机控制实战
1. 项目概述与核心组件选型
智能小车作为嵌入式学习的经典项目,融合了硬件设计、电机控制、传感器集成和无线通信等多领域技术。本项目采用STM32F103C8T6作为主控芯片,配合L298N电机驱动模块和蓝牙通信,实现带PID调速功能的智能小车系统。
核心组件特性对比:
| 组件 | 型号 | 关键参数 | 项目中的作用 |
|---|---|---|---|
| 主控芯片 | STM32F103C8T6 | Cortex-M3内核,72MHz主频,64KB Flash | 系统控制核心,运行PID算法 |
| 电机驱动 | L298N | 最大46V/2A双路输出,逻辑电压5V | 驱动直流电机,支持PWM调速 |
| 蓝牙模块 | JDY-31 | 蓝牙4.2,串口透传,10米传输距离 | 无线控制指令传输 |
| 直流电机 | 25GA370 | 12V,370转/分,带霍尔编码器 | 提供动力并反馈转速 |
提示:初学者建议选择带有编码器的直流电机,便于实现闭环速度控制。无编码器的开环控制难以达到精确调速效果。
2. 硬件系统搭建与电路设计
2.1 电源系统设计
智能小车的电源系统需要为不同组件提供合适的电压:
- 12V锂电池:直接为L298N和电机供电
- 5V稳压电路:为STM32和蓝牙模块供电
- 3.3V LDO:为编码器等外围器件供电
典型接线步骤:
- 将锂电池正负极接入L298N的12V和GND端子
- 从L298N的5V输出端引出电源线到STM32的5V引脚
- 确保所有模块的GND共地连接
- 为STM32添加100μF电容滤波,防止电机干扰
2.2 电机驱动接口配置
L298N模块的电机控制逻辑如下表所示:
| IN1 | IN2 | ENA | 电机A状态 |
|---|---|---|---|
| 0 | 0 | X | 停止 |
| 1 | 0 | PWM | 正转(PWM调速) |
| 0 | 1 | PWM | 反转(PWM调速) |
| 1 | 1 | X | 刹车 |
// STM32 GPIO初始化示例 void Motor_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; // 使能GPIO时钟 __HAL_RCC_GPIOA_CLK_ENABLE(); // 配置IN1(PA4), IN2(PA5)为输出 GPIO_InitStruct.Pin = GPIO_PIN_4 | GPIO_PIN_5; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 初始化状态:电机停止 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); }2.3 编码器接口设计
带霍尔编码器的电机可提供转速反馈,是实现PID控制的关键。编码器输出两路正交信号,可通过STM32的定时器编码器模式读取:
// 定时器编码器模式配置 void Encoder_TIM_Init(void) { TIM_Encoder_InitTypeDef sConfig = {0}; TIM_MasterConfigTypeDef sMasterConfig = {0}; htim2.Instance = TIM2; htim2.Init.Prescaler = 0; htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 65535; htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; sConfig.EncoderMode = TIM_ENCODERMODE_TI12; sConfig.IC1Polarity = TIM_ICPOLARITY_RISING; sConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI; sConfig.IC1Prescaler = TIM_ICPSC_DIV1; sConfig.IC1Filter = 0; sConfig.IC2Polarity = TIM_ICPOLARITY_RISING; sConfig.IC2Selection = TIM_ICSELECTION_DIRECTTI; sConfig.IC2Prescaler = TIM_ICPSC_DIV1; sConfig.IC2Filter = 0; HAL_TIM_Encoder_Init(&htim2, &sConfig); HAL_TIM_Encoder_Start(&htim2, TIM_CHANNEL_ALL); }3. PID控制算法实现与调参
3.1 PID算法原理
PID控制器由比例(P)、积分(I)、微分(D)三部分组成,其离散形式可表示为:
u(k) = Kp*e(k) + Ki*∑e(j) + Kd*[e(k)-e(k-1)]其中:
- e(k) = 目标速度 - 实际速度
- Kp:比例系数,决定响应速度
- Ki:积分系数,消除稳态误差
- Kd:微分系数,抑制超调
3.2 增量式PID实现
// 增量式PID控制器 int PID_Controller(int Target, int Current) { static int last_error = 0, prev_error = 0; static int output = 0; int error = Target - Current; // 增量计算 int delta = Kp*(error - last_error) + Ki*error + Kd*(error - 2*last_error + prev_error); output += delta; // 输出限幅 if(output > MAX_PWM) output = MAX_PWM; if(output < -MAX_PWM) output = -MAX_PWM; // 更新误差记录 prev_error = last_error; last_error = error; return output; }3.3 PID参数整定方法
手动调参步骤:
- 先将Ki和Kd设为0,逐渐增大Kp直到系统出现持续振荡
- 取振荡时Kp值的50%作为初始比例系数
- 逐渐增加Ki,消除稳态误差但避免积分饱和
- 最后加入Kd抑制超调,通常为Kp的10%-20%
注意:实际调试时应先确保电机能正常启停和转向,再逐步加入PID控制。调试过程中建议通过OLED实时显示转速和PWM占空比。
4. 蓝牙遥控与系统集成
4.1 蓝牙通信协议设计
JDY-31模块通过串口与STM32通信,可定义简单协议格式:
帧头(1B) | 命令类型(1B) | 数据(2B) | 校验(1B)典型控制命令示例:
- 0xA1:设置目标速度
- 0xB1:紧急停止
- 0xC1:PID参数调整
// 蓝牙数据接收处理 void Bluetooth_Process(uint8_t* data) { if(data[0] == 0xA1) { // 速度设置命令 int speed = (data[1] << 8) | data[2]; Set_Target_Speed(speed); } else if(data[0] == 0xB1) { // 停止命令 Motor_Stop(); } }4.2 手机APP设计要点
使用MIT App Inventor或Android Studio开发控制APP时需注意:
- 提供速度滑块控制(0-100%)
- 添加方向控制按钮(前进/后退/停止)
- 实现PID参数调节界面
- 确保发送数据格式与下位机协议一致
4.3 系统整合与测试
完整的控制流程如下:
- 手机APP发送速度指令
- STM32通过蓝牙接收并解析指令
- 编码器实时反馈当前转速
- PID控制器计算PWM输出
- L298N驱动电机达到目标速度
常见问题排查:
- 电机不转:检查电源连接和使能信号
- 转速波动大:调整PID参数或检查编码器接线
- 蓝牙连接不稳定:确保模块供电充足,避开2.4G干扰源
5. 进阶优化方向
5.1 速度滤波算法
原始编码器数据存在噪声,可添加滑动平均滤波:
#define FILTER_SIZE 5 int speed_filter[FILTER_SIZE] = {0}; int Moving_Average_Filter(int new_speed) { static int index = 0; int sum = 0; speed_filter[index] = new_speed; index = (index + 1) % FILTER_SIZE; for(int i=0; i<FILTER_SIZE; i++) { sum += speed_filter[i]; } return sum / FILTER_SIZE; }5.2 自适应PID控制
根据系统状态动态调整PID参数:
void Adaptive_PID(int error) { if(abs(error) > 100) { // 大误差范围 Kp = 8.0; Ki = 0.5; Kd = 0.1; } else if(abs(error) > 30) { // 中等误差 Kp = 5.0; Ki = 1.0; Kd = 0.5; } else { // 小误差范围 Kp = 3.0; Ki = 2.0; Kd = 1.0; } }5.3 能量回收与制动
利用PWM斩波实现电子制动,将动能转化为电能回充电容:
void Motor_Brake(void) { // 设置H桥为短路制动模式 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // 短暂保持后释放 HAL_Delay(50); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); }在实际项目中,我发现电机的机械特性对PID控制影响很大。相同PID参数在不同负载下表现差异明显,因此建议在目标负载条件下进行最终调参。另外,L298N的发热问题不容忽视,长时间运行时最好加装散热片。