送药小车2021年电赛F题目
一些前言废话:送药小车可以说是我的启蒙项目了,我的pid学习和理解,以及项目练习都是从这个项目开始有很大的进步的,多级pid的使用更是让我受益匪浅,大家可以作为电赛练手项目,并且从项目中学习,可以开始制作自己的stm32万能pcb拓展板为之后的项目使用,只可惜曾经的自己不是很爱记录,只断断续续的找到了大部分资料给大家拼凑出一个较为完整的学习和使用过程
Github项目开源代码:https://github.com/dawn-qvq/21-f-
文章目录
前言
背景介绍:2021年全国大学生电子设计竞赛F题(智能送药小车)的任务要求(识别数字、十字路口转向、送药与原路返回)。
项目成果:小车最终能达到的效果(丝滑转弯,完美归位)。
开源声明:本文将分享底盘主控(STM32F103C8T6)的核心控制逻辑与源码,供大家学习交流。
一、系统硬件组成
-
模块 型号 作用 主控 STM32F103C8T6 系统控制核心 摄像头 K210 数字识别、路线识别 灰度模块 八路灰度传感器 黑线检测、循迹 电机驱动 TB6612FNG 双路直流电机驱动 电机 带编码器减速电机 小车运动 编码器 AB相霍尔编码器 速度及位置反馈 蓝牙 HC-05 参数调试 显示 OLED(IIC) 实时状态显示 按键 3个独立按键 调试控制
二、硬件系统设计
2.1 控制核心
- 主控芯片:STM32F103C8T6
引脚 功能 PA0 TIM2_CH1 左PWM PA1 TIM2_CH2 右PWM PB12 左电机IN1 PB13 左电机IN2 PB14 右电机IN1 PB15 右电机IN2 PA6 左编码器A PA7 左编码器B PB6 右编码器A PB7 右编码器B PA9 USART1_TX(K210) PA10 USART1_RX(K210) PA2 USART2_TX(HC05) PA3 USART2_RX(HC05) PB1 KEY1 PC13 KEY2 PA4 KEY3 PC14 Gray1 PC15 Gray2 PA5 Gray3 PB0 Gray4 PB5 Gray5 PA12 Gray6 PA11 Gray7 PA8 Gray8
2.2 定时器模块
-
定时器 功能 TIM1 10ms控制中断 TIM2 PWM输出 TIM3 左编码器 TIM4 右编码器
2.3 串口模块
USART1
连接:
STM32->K210
我们当时使用的是亚博k210模块
波特率:
115200
作用:
- 接收路径编号
- 请求摄像头识别
- 接收数字识别结果
核心代码
USART_InitStructure.USART_BaudRate = 115200;
USART_Init(USART1, &USART_InitStructure);
发送
Serial_SendNumber(4,1);
表示:
请求K210重新识别路线
接收
real_value=Serial_RxData-'0';
USART2
连接
STM32->HC05
波特率
9600
作用:
蓝牙调试
核心代码
USART_InitStructure.USART_BaudRate=9600;
2.4 灰度传感模块
-
使用8个GPIO读取。
对应关系
通道 IO Gray1 PC14 Gray2 PC15 Gray3 PA5 Gray4 PB0 Gray5 PB5 Gray6 PA12 Gray7 PA11 Gray8 PA8 权值
-7 -5 -3 -1 1 3 5 7核心算法
error=Σ(Weight×Gray)/ΣGray代码
numerator+=GrayValue[i]*GrayWeight[i]; denominator+=GrayValue[i];最终
lastError=(float)numerator/denominator;
2.5 电机驱动(TB6612)
采用TB6612双路驱动。
TB6612 H桥工作原理
TB6612FNG 内部集成了两组 H 桥驱动电路,每一路 H 桥均可独立驱动一路直流电机。H 桥由四个 MOSFET 开关组成,通过改变桥臂导通方式,实现电机两端电压极性的切换,从而控制电机正转或反转。
本系统采用一组 PWM 信号和两路方向控制信号共同控制电机运行。
电机方向由 AIN1、AIN2(或 BIN1、BIN2)的高低电平组合决定。
| IN1 | IN2 | 电机状态 |
| 1 | 0 | 正转 |
| 0 | 1 | 反转 |
| 1 | 1 | 短刹车 |
| 0 | 0 | 自由停止(空转) |
因此,只需控制方向引脚的电平组合,即可实现电机的正反转控制,而 PWM 信号则决定电机转速。
控制接口
| 引脚 | 功能 |
|---|---|
| PB12 | 左IN1 |
| PB13 | 左IN2 |
| PB14 | 右IN1 |
| PB15 | 右IN2 |
| PA0 | 左PWM |
| PA1 | 右PWM |
PWM由TIM2输出。
核心代码
Motor_SetPWM_left()
if(PWM>=0)
{
GPIO_SetBits(GPIOB,GPIO_Pin_12);
GPIO_ResetBits(GPIOB,GPIO_Pin_13);
}
反转
GPIO_ResetBits(GPIOB,GPIO_Pin_12);
GPIO_SetBits(GPIOB,GPIO_Pin_13);
2.6 PWM模块
PWM频率
PSC
36-1
ARR
100-1
因此PWM占空比范围
0~100
核心代码
TIM_OC1Init(TIM2,&TIM_OCInitStructure);
TIM_OC2Init(TIM2,&TIM_OCInitStructure);
修改PWM
PWM_SetCompare1()
PWM_SetCompare2()
2.6 编码器模块
采用TIM编码器模式。
左轮
TIM3
右轮
TIM4
初始化
TIM_EncoderInterfaceConfig(
TIM3,
TIM_EncoderMode_TI12,
TIM_ICPolarity_Rising,
TIM_ICPolarity_Falling);
读取
Encoder1_Get();
本质
读取CNT
↓
清零CNT
↓
返回增量
核心代码
int16_t value=TIM_GetCounter(TIM3);
TIM_SetCounter(TIM3,0);
return value;
2.7 人机交互
- OLED显示屏(128×64)
- SPI通信驱动
- 实时状态显示(路线、步骤、速度)
- 按键输入(3键选择)
用于项目调试时自行安排使用
三个按键
按键 IO KEY1 PB1 KEY2 PC13 KEY3 PA4 采用
20ms软件消抖核心代码
Key_Tick();按键释放检测
if(CurrState==0&&PrevState!=0) - LED指示灯(送药状态)
三、软件架构设计
-
为了提高程序的可维护性和模块化程度,本项目采用自底向上的分层设计思想,将整个系统划分为驱动层、功能层、控制层以及应用层四个层次,各模块之间职责明确,通过接口进行数据交互,使程序具有良好的扩展性。
3.1 软件分层架构
整个软件架构如下图所示:

各层职责如下:
层次 主要模块 功能 应用层 main.c 路径规划、状态机调度、任务管理 控制层 control.c、PID.c 串级PID控制、运动控制、转向控制 功能层 Gray、Encoder、Serial、HC05、OLED 数据采集、通信、显示 驱动层 GPIO、TIM、USART、PWM STM32底层外设驱动 整个程序采用模块化设计,每个功能均封装为独立文件,降低了模块之间的耦合程度,提高了代码的可读性和维护效率。
3.2 系统运行流程
系统上电后首先完成各硬件模块初始化,包括 OLED、按键、电机驱动、编码器、串口、蓝牙、灰度传感器以及定时器等外设。
初始化完成后,STM32 向 K210 发送识别请求,获取当前目标路线。随后进入主循环,根据视觉识别结果进入不同的路径规划流程。
系统整体运行流程如下:
-

整个运行过程中,位置控制、速度控制以及循迹控制均在 TIM1 定时中断内完成,实现实时闭环控制。
3.3 状态机设计
为了适应电赛 F 题复杂的路网结构,本系统采用**有限状态机(Finite State Machine,FSM)**进行路径调度。
系统主要由 Step 和 led 两个变量共同控制运行状态。
其中:
- led = 0:送药阶段
- led = 1:返回阶段
- 程序逻辑更加清晰;
- 每一步动作互不影响;
- 便于增加新的路线;
- 调试更加方便。
- 外环(位置环)控制行驶距离;
- 内环(速度环)控制电机转速。
- R:轮半径;
- RATIO:减速比;
- RESOLUTION:编码器分辨率;
- HARMOIC:倍频系数。
- 左转 90°
- 右转 90°
- 原地 180° 掉头
- 左轮反转;
- 右轮正转;
Step 变量表示当前执行步骤,每完成一个动作便自动进入下一状态。
例如一路送药流程如下:

代码部分:
while (1)
{
KeyNum = Key_GetNum();
if(KeyNum==1)
{
Car_Spin(LEFT_90);
}
if(KeyNum==3)
{
Car_Spin(SPIN_180);
}
// 接收串口信号,更新line(寻迹路线)
if (Serial_GetRxFlag() == 1)
{
Serial_RxFlag = 0;
line = real_value %10;
if(flag1)
{
line1=line;
flag1=0;
}
}
if(line1!=0)flag2=1;
//第一行小灯,第二行路线,第三行步骤和左右转
OLED_Printf(0, 16, OLED_8X16, "Line:%d", line);
OLED_Update();
OLED_Printf(64, 16, OLED_8X16, "road:%d", roadline);
OLED_Update();
OLED_Printf(0, 32, OLED_8X16, "Step:%d", Step);
OLED_Update();
// for(int i=0;i<8;i++)
// {
// OLED_Printf(i*16, 0, OLED_8X16, "%d", GrayValue[i]);
// }
// OLED_Update();
if (led == 0)
{
if (line1 == 1)
{
roadline=1;
switch (Step)
{
case 0: // 直行
Car_go(60+15-5+3+CARLENGTH);
Step = 1;
break;
case 1: // 左转
if(flag_stop)
{
Car_Spin(LEFT_90);
Step = 2;
}
break;
case 2: // 再直行
if(flag_turn_over)
{
Car_go(40+15-5+CARLENGTH);
Step=3;
}
break;
case 3: // 完成
flag = 0;
OLED_Printf(0, 0, OLED_8X16, "led:%d",led);
OLED_Update();
//给串口发送1,亮灯
Serial_SendNumber(1,1);
Serial_SendString("\r\n");
if(KeyNum == 2)
{
led = 1;
Step = 0;
}
break;
}
}
// line == 2: 右转流程
else if (line1 == 2)
{
roadline=2;
switch (Step)
{
case 0: // 直行
Car_go(60+15-5+3);
Step = 1;
break;
case 1: // 左转
if(flag_stop)
{
Car_Spin(RIGHT_90);
Step = 2;
}
break;
case 2: // 再直行
if(flag_turn_over)
{
Car_go(40+15-5);
Step=3;
}
break;
case 3: // 完成
flag = 0;
OLED_Printf(0, 0, OLED_8X16, "led:%d",led);
OLED_Update();
if(KeyNum == 2)
{
led = 1;
Step = 0;
}
break;
}
}
else if(line1>2)
{
switch (Step)
{
case 0:
Car_go(150+15-5+3+2+2+CARLENGTH);
Step=1;
break;
case 1:
if(flag_stop)
{
if(line==6)//左转
{
OLED_Printf(64, 32, OLED_8X16, "l");
OLED_Update();
roadline=3;
Step=2;
}
else if(line==7)
{
OLED_Printf(64, 32, OLED_8X16, "r");
OLED_Update();
roadline=4;
Step=3;
}
else if(line==5)//直行
{
OLED_Printf(64, 32, OLED_8X16, "s");
OLED_Update();
Step=5;
}
}
break;
case 2:
if(flag_stop)
{
Car_Spin(LEFT_90);
Step=4;
}
break;
case 3:
if(flag_stop)
{
Car_Spin(RIGHT_90);
Step=4;
}
break;
case 4:
if(flag_turn_over)
{
Car_go(40+15-5+3+2+CARLENGTH);
Step=14;
}
break;
case 5:
if(flag_stop)
{
Car_go(15+60+15-5+3+3+2+CARLENGTH);
Step=6;
}
break;
case 6:
if(flag_stop)
{
if(line==6)
{
Step=7;
roadline=5;
}
else if(line==7)
{
Step=8;
roadline=7;
}
}
break;
case 7:
if(flag_stop)
{
Car_Spin(LEFT_90);
Step=9;
}
break;
case 8:
if(flag_stop)
{
Car_Spin(RIGHT_90);
Step=9;
}
break;
case 9:
if(flag_turn_over)
{
Car_go(15+60+15-5+3+2+CARLENGTH);
Step=10;
}
break;
case 10:
if(flag_stop)
{
if(line==6)
{
if(roadline==5)
roadline=5;
else
roadline=8;
Step=11;//左转
}
else if(line==7)
{
if(roadline==5)
roadline=6;
else
roadline=7;
Step=12;//右转
}
}
break;
case 11:
if(flag_stop)
{
Car_Spin(LEFT_90);
Step=13;
}
break;
case 12:
if(flag_stop)
{
Car_Spin(RIGHT_90);
Step=13;
}
break;
case 13:
if(flag_turn_over)
{
Car_go(40+15-5+3+CARLENGTH);
Step=14;
}
break;
case 14:
flag = 0;
OLED_Printf(0, 0, OLED_8X16, "led:%d",led);
OLED_Update();
//给串口发送1,亮灯
Serial_SendNumber(1,1);
Serial_SendString("\r\n");
if(KeyNum == 2)
{
led = 1;
Step = 0;
}
break;
}
}
}
// led == 1: 第二阶段
else if (led == 1)
{
if (roadline == 1)
{
switch (Step)
{
case 0:
Car_Spin(SPIN_180);
Step=1;
break;
case 1:
if(flag_turn_over)
{
Car_go(40+15-5+CARLENGTH);
Step=2;
}
break;
case 2:
if(flag_stop)
{
Car_Spin(RIGHT_90);
Step=3;
}
break;
case 3:
if(flag_turn_over)
{
Car_go(60+15-5+3+CARLENGTH);
Step=4;
}
break;
case 4:
flag = 0;
OLED_Printf(0, 0, OLED_8X16, "led:%d",led);
OLED_Update();
//给串口发送2,亮灯
Serial_SendNumber(2,1);
Serial_SendString("\r\n");
break;
}
}
else if (roadline == 2)
{
switch (Step)
{
case 0:
Car_Spin(SPIN_180);
Step=1;
break;
case 1:
if(flag_turn_over)
{
Car_go(40+15-5+CARLENGTH);
Step=2;
}
break;
case 2:
if(flag_stop)
{
Car_Spin(LEFT_90);
Step=3;
}
break;
case 3:
if(flag_turn_over)
{
Car_go(60+15-5+3+CARLENGTH);
Step=4;
}
break;
case 4:
flag = 0;
OLED_Printf(0, 0, OLED_8X16, "led:%d",led);
OLED_Update();
//给串口发送2,亮灯
Serial_SendNumber(2,1);
Serial_SendString("\r\n");
break;
}
}
else if (roadline == 3)
{
switch (Step)
{
case 0:
Car_Spin(SPIN_180);
Step=1;
break;
case 1:
if(flag_turn_over)
{
Car_go(40+15-5+3+CARLENGTH);
Step=2;
}
break;
case 2:
if(flag_stop)
{
Car_Spin(RIGHT_90);
Step=3;
}
break;
case 3:
if(flag_turn_over)
{
Car_go(150+15-5+3+CARLENGTH);
Step=4;
}
break;
case 4:
flag = 0;
OLED_Printf(0, 0, OLED_8X16, "led:%d",led);
OLED_Update();
//给串口发送2,亮灯
Serial_SendNumber(2,1);
Serial_SendString("\r\n");
break;
}
}
else if (roadline == 4)
{
switch (Step)
{
case 0:
Car_Spin(SPIN_180);
Step=1;
break;
case 1:
if(flag_turn_over)
{
Car_go(40+15-5+3+CARLENGTH);
Step=2;
}
break;
case 2:
if(flag_stop)
{
Car_Spin(LEFT_90);
Step=3;
}
break;
case 3:
if(flag_turn_over)
{
Car_go(150+15-5+3+CARLENGTH);
Step=4;
}
break;
case 4:
flag = 0;
OLED_Printf(0, 0, OLED_8X16, "led:%d",led);
OLED_Update();
//给串口发送2,亮灯
Serial_SendNumber(2,1);
Serial_SendString("\r\n");
break;
}
}
else if (roadline == 5)
{
switch (Step)
{
case 0:
Car_Spin(SPIN_180);
Step=1;
break;
case 1:
if(flag_turn_over)
{
Car_go(40+15-5+3+CARLENGTH);
Step=2;
}
break;
case 2:
if(flag_stop)
{
Car_Spin(RIGHT_90);
Step=3;
}
break;
case 3:
if(flag_turn_over)
{
Car_go(15+60+15-5+3+2+CARLENGTH);
Step=4;
}
break;
case 4:
if(flag_stop)
{
Car_Spin(RIGHT_90);
Step=5;
}
break;
case 5:
if(flag_turn_over)
{
Car_go(15+60+30+60+30+60-5+3+CARLENGTH);
Step=6;
}
break;
case 6:
flag = 0;
OLED_Printf(0, 0, OLED_8X16, "led:%d",led);
OLED_Update();
//给串口发送2,亮灯
Serial_SendNumber(2,1);
Serial_SendString("\r\n");
break;
}
}
else if (roadline == 6)
{
switch (Step)
{
case 0:
Car_Spin(SPIN_180);
Step=1;
break;
case 1:
if(flag_turn_over)
{
Car_go(40+15-5+3+CARLENGTH);
Step=2;
}
break;
case 2:
if(flag_stop)
{
Car_Spin(LEFT_90);
Step=3;
}
break;
case 3:
if(flag_turn_over)
{
Car_go(15+60+15-5+3+2+CARLENGTH);
Step=4;
}
break;
case 4:
if(flag_stop)
{
Car_Spin(RIGHT_90);
Step=5;
}
break;
case 5:
if(flag_turn_over)
{
Car_go(15+60+30+60+30+60-5+3+CARLENGTH);
Step=6;
}
break;
case 6:
flag = 0;
OLED_Printf(0, 0, OLED_8X16, "led:%d",led);
OLED_Update();
//给串口发送2,亮灯
Serial_SendNumber(2,1);
Serial_SendString("\r\n");
break;
}
}
else if (roadline == 7)
{
switch (Step)
{
case 0:
Car_Spin(SPIN_180);
Step=1;
break;
case 1:
if(flag_turn_over)
{
Car_go(40+15-5+3+CARLENGTH);
Step=2;
}
break;
case 2:
if(flag_stop)
{
Car_Spin(LEFT_90);
Step=3;
}
break;
case 3:
if(flag_turn_over)
{
Car_go(15+60+15-5+3+2+CARLENGTH);
Step=4;
}
break;
case 4:
if(flag_stop)
{
Car_Spin(LEFT_90);
Step=5;
}
break;
case 5:
if(flag_turn_over)
{
Car_go(15+60+30+60+30+60-5+3);
Step=6;
}
break;
case 6:
flag = 0;
OLED_Printf(0, 0, OLED_8X16, "led:%d",led);
OLED_Update();
//给串口发送2,亮灯
Serial_SendNumber(2,1);
Serial_SendString("\r\n");
break;
}
}
else if (roadline == 8)
{
switch (Step)
{
case 0:
Car_Spin(SPIN_180);
Step=1;
break;
case 1:
if(flag_turn_over)
{
Car_go(40+15-5+3);
Step=2;
}
break;
case 2:
if(flag_stop)
{
Car_Spin(LEFT_90);
Step=3;
}
break;
case 3:
if(flag_turn_over)
{
Car_go(15+60+15-5+3+2);
Step=4;
}
break;
case 4:
if(flag_stop)
{
Car_Spin(RIGHT_90);
Step=5;
}
break;
case 5:
if(flag_turn_over)
{
Car_go(15+60+30+60+30+60-5+3 );
Step=6;
}
break;
case 6:
flag = 0;
OLED_Printf(0, 0, OLED_8X16, "led:%d",led);
OLED_Update();
//给串口发送2,亮灯
Serial_SendNumber(2,1);
Serial_SendString("\r\n");
break;
}
}
}
}
3.4 路径规划设计
根据 2021 年全国大学生电子设计竞赛 F 题赛道特点,本系统共设计了 8 种路线方案,分别对应变量 roadline = 1~8。
不同路线由 K210 返回的数字决定。
例如:
| 返回值 | 含义 |
| 5 | 十字路口直行 |
| 6 | 十字路口左转 |
| 7 | 十字路口右转 |
对于复杂路网,系统采用多级判断方式。
例如:
第一个路口
│
▼
第二个路口
│
▼
第三个路口
│
▼
目标区域
每经过一个交叉口,状态机会根据新的识别结果更新下一步运动策略,实现动态导航。
3.5 寻迹控制流程
为了保证小车能够稳定沿黑线运行,本系统采用八路灰度传感器获取赛道中心位置,并结合 PID 算法实时调整左右轮速度。
控制流程如下:
八路灰度采集
│
▼
加权计算误差
│
▼
PID计算补偿量
│
▼
左右轮PWM修正
│
▼
电机驱动
整个循迹控制在定时器中断中周期执行,控制周期为 10 ms,能够保证系统具有较高的实时性。
四、关键算法详解
4.1 八路灰度循迹算法
为了提高循迹精度,本系统采用八路灰度传感器进行加权平均计算。
八个传感器对应不同权值:
-7 -5 -3 -1 1 3 5 7
首先统计检测到黑线的传感器数量,然后采用加权平均计算偏离中心的位置。
计算公式如下:
误差值 Error = 所有像素灰度值乘以对应权重的总和 ÷ 所有像素灰度值的总和
当所有传感器均未检测到黑线时,系统保持上一时刻误差值,避免小车因丢线而剧烈摆动。
得到误差后,对左右轮 PWM 进行补偿:
left_PWM = PID_Speed_left.Out - error * 6;
right_PWM = PID_Speed_right.Out + error * 6;
通过差速控制实现平滑循迹。
4.2 串级 PID 控制算法
为了兼顾速度稳定性和位置精度,本系统采用位置环 + 速度环组成串级 PID 控制结构。
控制流程如下:

其中:
核心控制函数为:
Location_Speed();
其主要流程为:

串级控制相比单一 PID 能够有效降低超调,提高停车精度。
代码部分
PID_t PID_Speed_left;
PID_t PID_Location_left ;
PID_t PID_Speed_right ;
PID_t PID_Location_right;
/**
* @brief PID参数初始化
* @note 无
* @retval 无
*/
void PID_Init(void)
{
/* 速度相关初始化参数 */
PID_Speed_left.Actual=0.0;
PID_Speed_left.Target=0.0;
PID_Speed_left.Error0=0.0;
PID_Speed_left.Error1=0.0;
PID_Speed_left.ErrorInt=0.0;
PID_Speed_left.Kp=0.4;
PID_Speed_left.Ki=0.1;
PID_Speed_left.Kd=0.0;
PID_Speed_left.OutMax=100;
PID_Speed_left.OutMin=-100;
/* 位置相关初始化参数 */
PID_Location_left.Actual=0.0;
PID_Location_left.Target=0.0;
PID_Location_left.Error0=0.0;
PID_Location_left.Error1=0.0;
PID_Location_left.ErrorInt=0.0;
PID_Location_left.Kp=0.15;
PID_Location_left.Ki=0.0;
PID_Location_left.Kd=0.35;
PID_Location_left.OutMax=30;
PID_Location_left.OutMin=-30;
/* 速度相关初始化参数 */
PID_Speed_right.Actual=0.0;
PID_Speed_right.Target=0.0;
PID_Speed_right.Error0=0.0;
PID_Speed_right.Error1=0.0;
PID_Speed_right.ErrorInt=0.0;
PID_Speed_right.Kp=0.45;
PID_Speed_right.Ki=0.12;
PID_Speed_right.Kd=0.0;
PID_Speed_right.OutMax=100;
PID_Speed_right.OutMin=-100;
/* 位置相关初始化参数 */
PID_Location_right.Actual=0.0;
PID_Location_right.Target=0.0;
PID_Location_right.Error0=0.0;
PID_Location_right.Error1=0.0;
PID_Location_right.ErrorInt=0.0;
PID_Location_right.Kp=0.15;
PID_Location_right.Kp=0.0;
PID_Location_right.Kp=0.3;
PID_Location_right.OutMax=30;
PID_Location_right.OutMin=-30;
}
float Speed_PID_Update(PID_t *p)
{
p->Error1 = p->Error0;
p->Error0 = p->Target - p->Actual;
if (p->Ki != 0)
{
p->ErrorInt += p->Error0;
}
else
{
p->ErrorInt = 0;
}
//积分限幅
if (p->ErrorInt > 834) {p->ErrorInt = 834;}//上限=p->Out/p->Ki
if (p->ErrorInt < -834) {p->ErrorInt = -834;}
p->Out = p->Kp * p->Error0
+ p->Ki * p->ErrorInt
+ p->Kd * (p->Error0 - p->Error1);
if (p->Out > p->OutMax) {p->Out = p->OutMax;}
if (p->Out < p->OutMin) {p->Out = p->OutMin;}
return p->Out;
}
float Location_PID_Update(PID_t *p)
{
p->Error1 = p->Error0;
p->Error0 = p->Target - p->Actual;
if (p->Ki != 0)
{
p->ErrorInt += p->Error0;
}
else
{
p->ErrorInt = 0;
}
p->Out = p->Kp * p->Error0
+ p->Ki * p->ErrorInt
+ p->Kd * (p->Error0 - p->Error1);
if (p->Out > p->OutMax) {p->Out = p->OutMax;}
if (p->Out < p->OutMin) {p->Out = p->OutMin;}
return p->Out;
}
4.3 距离检测与停车控制
系统利用编码器累计脉冲计算实际行驶距离。
计算公式如下:
行驶距离 Distance =(脉冲数 × 圆周率 × 轮子半径)÷(减速比 × 编码器分辨率 × 谐波系数)
当车辆距离目标点小于车长补偿值(±CARLENGTH)时,系统开始进入减速阶段。
停车控制采用渐进式制动策略,在约 180 ms 内逐步降低速度,当计数达到设定阈值后关闭 PWM 输出,实现平稳停车,避免因惯性导致越线。
4.4 转向控制算法
系统支持三种基本转向动作:
转向控制采用编码器位置闭环实现,通过设置左右轮目标脉冲数,使两侧车轮反向旋转,实现精准转向。
例如左转时:
转向完成后,根据编码器累计脉冲判断是否达到目标角度,并设置 flag_turn_over 标志位,通知状态机继续执行下一步任务。
为了减小地面摩擦及机械误差带来的累计偏差,系统在每次转向结束后均重新进入灰度循迹模式,通过黑线重新校正车辆姿态,提高整体导航精度。
4.5 K210视觉导航算法
K210 负责识别赛道中的数字标志及交叉路口信息,并通过 USART1 与 STM32 实时通信。
STM32 周期性发送识别请求:
STM32 → K210:4
K210 完成图像识别后返回导航结果:
5 → 直行
6 → 左转
7 → 右转
STM32 根据接收到的数字更新路径状态,并结合有限状态机动态规划下一步运动路线,实现视觉识别与循迹控制的协同工作。
五、部分效果展示
去除k210视觉识别只走规定路线展示(方便只调试小车控制部分,以及调节pid参数,循迹路线)
【21年电赛f题送药小车-哔哩哔哩】 https://b23.tv/FUvMT9U
加上k210视觉识别的路线展示
【21年电赛f题送药小车2-哔哩哔哩】 https://b23.tv/BBs6S7u
六、项目总结
本项目基于 STM32F103C8T6 作为主控制器,结合 K210 AI 视觉模块、八路灰度循迹模块、编码器闭环反馈、TB6612 电机驱动、HC-05 蓝牙通信 以及 OLED 实时显示 等硬件,实现了 2021 年全国大学生电子设计竞赛 F 题《智能送药小车》的完整控制系统。
整个系统采用模块化设计思想,将硬件驱动、数据采集、运动控制、路径规划以及视觉导航进行了分层设计,具有良好的可维护性和扩展性。
在控制算法方面,系统采用有限状态机完成复杂路网的任务调度,通过 Step 状态变量实现分阶段控制,并结合 roadline 路径编号完成八种路线的自动切换。相比传统顺序程序,状态机结构逻辑更加清晰,也便于后续增加新的路径和功能。
运动控制方面,引入了位置环 + 速度环串级 PID 控制算法。位置环负责控制目标行驶距离,速度环负责调节电机转速,同时利用八路灰度传感器计算循迹误差,对左右轮 PWM 输出进行实时补偿,使小车能够平稳地沿黑线运行,并在复杂弯道及十字路口保持较好的循迹性能。
视觉导航部分采用 K210 完成数字识别和路口决策,通过串口与 STM32 实时通信,实现了视觉识别与运动控制的协同工作。实验结果表明,系统能够准确识别不同导航标志,并根据识别结果动态规划行驶路线,完成送药和返程任务。
整个项目开发过程中,也遇到了诸多实际问题,例如:
- 电机一致性导致的小车跑偏;
- 编码器累计误差影响停车精度;
- 转向结束后姿态偏差;
- 灰度传感器安装高度对循迹效果的影响;
- 电池电压下降导致 PID 参数发生变化。
更多推荐



所有评论(0)