视觉循迹云台自瞄准小车(25年电赛控制题目)
提示:该篇为25年电赛e题,简易自行瞄准装置
前言
在智能车、嵌入式控制、机器视觉快速普及的当下,传统单单片机小车普遍存在任务卡顿、控制延迟高、视觉与运动冲突、稳定性差等问题。为解决以上痛点,本项目采用 MSPM0G3507 + STM32 双 MCU 异构架构,搭建了一套集视觉识别、二维云台随动、高精度自主循迹于一体的智能小车系统。
通过双芯片分工协作,将数据解析、视觉处理与实时运动控制进行解耦,极大提升系统实时性与稳定性。本文将从硬件架构设计、PCB 布局、底层驱动开发、八路循迹算法、云台 PID 控制、MaxiCam 视觉数据处理等方面,完整记录整套项目的开发思路与实战细节,全程干货、无废话,适合嵌入式、智能车初学者学习参考。
本项目所有资料开源免费,希望能帮助更多同学快速入门嵌入式智能控制项目。
一、项目概述与技术架构
1.1 项目背景与需求分析
- 项目目标:实现自主循迹、目标识别、精准控制的智能小车


- 核心难点:多传感器融合、实时控制、精度要求(误差≤2cm、响应<2s)
- 技术方案选型依据
1.2 双MCU架构设计
-
为什么选择双MCU?
- 任务隔离:主MCU(STM32)负责算法运算,从MCU(MSPM0G3507)负责底层驱动
- 实时性保证:避免高层算法阻塞驱动层
- 可靠性提升:某个MCU故障不影响整体运行
二、硬件设计与PCB实现
2.1 原理图设计要点
在本项目中,为了提高硬件的复用性和后续开发效率,并没有针对本次比赛单独设计专用控制板,而是分别设计了 MSPM0G3507 通用扩展板 和 STM32F103C8T6 通用扩展板。两块扩展板均采用模块化设计思路,将核心控制器与各类外设接口统一引出,使其不仅能够满足本项目需求,也能够直接应用于后续其他电子设计竞赛、课程设计以及个人项目开发。
该扩展板预留了大量I/O资源,后续仅需根据不同项目连接对应外设即可,无需重新绘制原理图和PCB,大幅缩短了项目开发周期。
由于所有常用外设均采用标准接口设计,因此后续无论是更换视觉模块、增加传感器,还是接入无线通信模块,都可以直接复用该扩展板,无需重新设计硬件。
- MSPM0G3507部分扩展板
MSPM0G3507扩展板主要定位为底层控制核心板,负责电机驱动、传感器采集以及实时控制等任务。
设计过程中主要引出了以下资源:电机驱动接口(TB6612),编码器接口,灰度循迹接口,按键接口,UART通信接口,电源管理模块,GPIO扩展接口
- STM32F103C8T6部分拓展板

STM32扩展板主要作为上层控制核心板,负责视觉处理、云台控制以及多模块通信等功能。
板载统一引出了:UART通信接口,PWM输出接口,I2C接口,SPI接口,舵机控制接口,摄像头(MaxiCam)接口,电源接口,GPIO扩展接口
2.2 PCB布局布线经验

功能模块分区布局
PCB布局时遵循"先布局、后布线"的原则,根据电路功能划分不同区域,主要分为:
- MCU核心控制区
- 电源管理区
- 电机驱动区
- 通信接口区
- 外设扩展区
布局时尽量保证同一功能模块集中放置,减少不同模块之间信号交叉。例如,将TB6612电机驱动器靠近电机接口放置,缩短大电流走线长度;串口接口统一放置在PCB边缘,方便后续连接MaxiCam、蓝牙等外设。
电源布线优先
由于项目中同时驱动电机和控制器,电源质量直接影响整个系统稳定性,因此优先完成电源网络布线。
设计过程中主要采用以下措施:
- 电源线宽明显大于普通信号线,提高载流能力;
- MCU附近放置0.1μF去耦电容和10μF滤波电容,降低高频噪声;
- 电机驱动芯片附近增加大容量储能电容,减小电机启动时造成的电压跌落;
- 电源输入端预留TVS和反接保护接口,提高整板可靠性。
经过实际测试,即使电机频繁启停,MCU供电仍保持稳定,没有出现复位现象。
数字信号与大电流分离
电机工作时会产生较大的电磁干扰,因此布线时尽量将:
- PWM驱动线
- 电机供电线
与:
- UART通信
- 编码器信号
- 灰度传感器信号
保持一定距离,避免平行长距离走线。
同时尽量减少高速信号跨越大电流区域,降低串扰,提高通信稳定性。
去耦与接地设计
每个MCU电源引脚附近均放置0.1μF去耦电容,并尽可能靠近芯片引脚布局。
PCB底层采用大面积铺地,提高接地完整性,同时通过多个过孔连接上下层GND,降低接地阻抗。
对于电机驱动、电源接口等大电流区域,增加多个接地过孔,提高散热能力和电流回流能力。
接口统一规范
为了方便后续项目复用,所有扩展接口均采用统一排针方式引出,包括:
- UART
- PWM
- I2C
- SPI
- GPIO
- 编码器
- 灰度接口
接口排列统一采用"VCC-GND-Signal"或"GND-VCC-Signal"的形式,减少接线错误,提高调试效率。
预留调试接口
考虑到后期程序下载和故障排查,在PCB设计阶段预留了:
- SWD下载接口
- UART调试接口
- 电源测试点
- GPIO测试点
这样在硬件调试过程中,无需拆卸整板即可快速进行程序烧录、串口调试以及示波器测量,大幅提高了开发效率。
三、小车循迹部分底层驱动开发
3.1 电机驱动
-
电机控制原理
TB6612每个电机均有两个方向控制引脚(AIN1、AIN2 / BIN1、BIN2)以及一个PWM输入引脚。
控制逻辑如下:
程序中的
Motor_Send()函数根据速度正负自动切换方向。Motor_Send(150,150); //左右轮正转 Motor_Send(-150,-150); //左右轮反转 Motor_Send(150,-150); //原地左转 Motor_Send(-150,150); //原地右转其中PWM占空比决定电机输出功率,占空比越大,电机转速越高。
整个控制流程如下:
目标速度 │ ▼ Motor_Send() │ GPIO控制方向 PWM控制速度 │ ▼ TB6612驱动 │ ▼ 直流减速电机 -
代码框架
void Motor_Send(int Aoutput ,int Boutput) { if(Aoutput > 0) //正转 { DL_GPIO_setPins(TB6612_AIN1_PORT,TB6612_AIN1_PIN); DL_GPIO_clearPins(TB6612_AIN2_PORT,TB6612_AIN2_PIN); DL_Timer_setCaptureCompareValue(PWM_0_INST,(uint32_t)(Aoutput),GPIO_PWM_0_C0_IDX); } else //反转 { DL_GPIO_setPins(TB6612_AIN2_PORT,TB6612_AIN2_PIN); DL_GPIO_clearPins(TB6612_AIN1_PORT,TB6612_AIN1_PIN); DL_Timer_setCaptureCompareValue(PWM_0_INST,(uint32_t)(-Aoutput),GPIO_PWM_0_C0_IDX); } if(Boutput > 0) //正转 { DL_GPIO_setPins(TB6612_BIN1_PORT,TB6612_BIN1_PIN); DL_GPIO_clearPins(TB6612_BIN2_PORT,TB6612_BIN2_PIN); DL_Timer_setCaptureCompareValue(PWM_0_INST,(uint32_t)(Boutput),GPIO_PWM_0_C1_IDX); } else //反转 { DL_GPIO_setPins(TB6612_BIN2_PORT,TB6612_BIN2_PIN); DL_GPIO_clearPins(TB6612_BIN1_PORT,TB6612_BIN1_PIN); DL_Timer_setCaptureCompareValue(PWM_0_INST,(uint32_t)(-Boutput),GPIO_PWM_0_C1_IDX); } } float my_abs_float(float a) { if(a>0) return a; else return -a; } int my_abs_int(int a) { if(a>0) return a; else return -a; }
3.2 编码测速
为了实现电机闭环控制,小车两侧电机均安装了AB相增量式编码器,用于实时检测车轮转速和旋转方向。
1. AB相编码器原理
编码器输出两路相位相差90°的方波信号(A相、B相)。
例如:
A相: ──▁▁────▁▁────
B相: ▁▁────▁▁──────
由于两相信号存在固定相位差,因此不仅能够统计脉冲数量,还可以判断旋转方向。
- A相领先B相 → 电机正转
- B相领先A相 → 电机反转
因此只需要检测A、B两相信号的变化关系,就可以完成测速和方向判断。
2. 外部中断计数
本项目没有使用MCU自带编码器接口,而是采用GPIO外部中断模拟编码器计数。
当A相或B相信号发生电平变化时,会立即进入中断函数:
GROUP1_IRQHandler()
程序读取另一相信号的电平状态,即可判断当前旋转方向。
例如:
if(A相产生中断)
{
if(B相为低)
编码器计数--;
else
编码器计数++;
}
左右两个编码器均采用相同方式进行计数,因此分别维护:
Get_Encoder_countA
Get_Encoder_countB
两个变量作为左右轮累计脉冲数。
整个过程无需CPU不断轮询,提高了测速精度,也降低了系统资源占用。
3. 编码器的作用
编码器不仅用于获取车轮速度,还为后续控制算法提供反馈数据,是实现闭环控制的重要基础。
主要作用包括:
- 实时测量左右轮转速;
- 判断电机旋转方向;
- 为PID速度控制提供反馈;
- 统计小车运行距离;
- 提高循迹过程中的运动精度。
整个反馈流程如下:
PWM输出
│
▼
电机转动
│
▼
编码器产生AB相信号
│
▼
GPIO外部中断计数
│
▼
获得左右轮速度
│
▼
PID控制器修正PWM
通过这种"PWM驱动 + 编码器反馈"的闭环控制方式,小车能够根据实际运行状态不断修正电机输出,提高速度控制精度,使循迹更加平稳、转弯更加准确,也为后续加入速度环PID和串级PID控制奠定了基础。
-
/******************************************************* 函数功能:外部中断模拟编码器信号 入口函数:无 返回 值:无 ***********************************************************/ void GROUP1_IRQHandler(void) { gpio_interrup = DL_GPIO_getEnabledInterruptStatus(GPIOA,ENCODER_E1A_PIN|ENCODER_E1B_PIN|ENCODER_E2A_PIN|ENCODER_E2B_PIN); //encoderA if((gpio_interrup & ENCODER_E1A_PIN)==ENCODER_E1A_PIN) { if(!DL_GPIO_readPins(GPIOA,ENCODER_E1B_PIN)) { Get_Encoder_countA--; } else { Get_Encoder_countA++; } } else if((gpio_interrup & ENCODER_E1B_PIN)==ENCODER_E1B_PIN) { if(!DL_GPIO_readPins(GPIOA,ENCODER_E1A_PIN)) { Get_Encoder_countA++; } else { Get_Encoder_countA--; } } //encoderB if((gpio_interrup & ENCODER_E2A_PIN)==ENCODER_E2A_PIN) { if(!DL_GPIO_readPins(GPIOA,ENCODER_E2B_PIN)) { Get_Encoder_countB--; } else { Get_Encoder_countB++; } } else if((gpio_interrup & ENCODER_E2B_PIN)==ENCODER_E2B_PIN) { if(!DL_GPIO_readPins(GPIOA,ENCODER_E2A_PIN)) { Get_Encoder_countB++; } else { Get_Encoder_countB--; } } DL_GPIO_clearInterruptStatus(GPIOA,ENCODER_E1A_PIN|ENCODER_E1B_PIN|ENCODER_E2A_PIN|ENCODER_E2B_PIN); }
3.3 八路灰度
1. 工作原理
灰度传感器利用红外发射与接收原理工作。
- 白色地面反射红外光较强;
- 黑色胶带吸收红外光较多。
因此,当传感器检测到黑线和白底时,会输出不同的数字电平,MCU只需读取GPIO电平即可判断当前位置。
2. 数据采集
程序中的 sensor() 函数负责读取8路GPIO输入,并将其转换成8个状态变量:
L4、L3、L2、L1
R1、R2、R3、R4
每一路传感器只有两种状态:
- 1:检测到黑线(或满足设定阈值)
- 0:未检测到黑线
每次进入循迹循环时,都会先调用 sensor() 更新8路传感器状态,为后续偏差计算提供实时数据。
3. 在循迹中的作用
获取8路灰度数据后,程序会根据哪些传感器检测到黑线,计算出小车相对于赛道中心的偏移量(Error)。
例如:
L1 R1 检测到黑线 → Error = 0(位于赛道中心)
L2、L3 检测到黑线 → Error > 0(偏左)
R2、R3 检测到黑线 → Error < 0(偏右)
随后根据误差调整左右电机PWM,实现自动修正方向,使小车始终沿黑线稳定运行。
整个数据处理流程如下:
灰度传感器
│
▼
读取8路GPIO状态
│
▼
得到L4~R4状态
│
▼
计算循迹误差Error
│
▼
调整左右轮PWM
│
▼
小车完成自主循迹
该模块是整个循迹系统的"感知层",负责实时获取赛道信息,为后续误差计算和运动控制提供基础数据。
代码部分
-
int L4,L3,L2,L1,R1,R2,R3,R4; void sensor() { if( (sensor_L4_PIN&DL_GPIO_readPins(sensor_L4_PORT,sensor_L4_PIN))==sensor_L4_PIN) { L4=1; } else L4=0; if( (sensor_L3_PIN&DL_GPIO_readPins(sensor_L3_PORT,sensor_L3_PIN))==sensor_L3_PIN) { L3=1; } else L3=0; if( (sensor_L2_PIN&DL_GPIO_readPins(sensor_L2_PORT,sensor_L2_PIN))==sensor_L2_PIN) { L2=1; } else L2=0; if( (sensor_L1_PIN&DL_GPIO_readPins(sensor_L1_PORT,sensor_L1_PIN))==sensor_L1_PIN) { L1=1; } else L1=0; if( (sensor_R1_PIN&DL_GPIO_readPins(sensor_R1_PORT,sensor_R1_PIN))==sensor_R1_PIN) { R1=1; } else R1=0; if( (sensor_R2_PIN&DL_GPIO_readPins(sensor_R2_PORT,sensor_R2_PIN))==sensor_R2_PIN) { R2=1; } else R2=0; if( (sensor_R3_PIN&DL_GPIO_readPins(sensor_R3_PORT,sensor_R3_PIN))==sensor_R3_PIN) { R3=1; } else R3=0; if( (sensor_R4_PIN&DL_GPIO_readPins(sensor_R4_PORT,sensor_R4_PIN))==sensor_R4_PIN) { R4=1; } else R4=0; }
3.4 主函数部分
主函数(main())作为整个系统的核心控制入口,负责完成硬件初始化、用户交互、自主循迹以及终点停车等功能,其整体流程如下:
系统初始化
│
▼
按键选择循迹圈数
│
▼
确认开始运行
│
▼
实时采集灰度数据
│
▼
计算循迹误差
│
▼
调整左右轮PWM
│
▼
检测是否经过计数点
│
▼
达到目标圈数?
│ │
│否 │是
▼ ▼
继续循迹 停止电机
1. 系统初始化
程序开始后,首先完成MCU外设初始化,包括GPIO、PWM、UART、定时器以及中断配置,同时完成电机驱动模块初始化。
初始化完成后启动PWM输出,为后续电机控制做好准备。
2. 按键选择循迹圈数
为了提高程序灵活性,本项目增加了圈数选择功能。
- KEY2:循环选择需要完成的循迹圈数(1~5圈);
- LED:通过闪烁次数反馈当前选择的圈数;
- KEY3:确认当前圈数并开始循迹。
例如:
| LED闪烁次数 | 对应圈数 |
|---|---|
| 1次 | 1圈 |
| 2次 | 2圈 |
| 3次 | 3圈 |
| 4次 | 4圈 |
| 5次 | 5圈 |
这种交互方式无需增加显示屏,即可实现简单的人机交互。
3. 八路灰度循迹
开始运行后,程序循环调用 sensor() 函数读取8路灰度传感器数据。
根据左右传感器检测到黑线的位置,计算小车相对于赛道中心的偏移量(Error)。
例如:
- 黑线位于中心 → Error = 0
- 黑线偏左 → Error > 0
- 黑线偏右 → Error < 0
误差范围设计为 -8~+8,误差越大,说明偏离赛道中心越严重。
4. 差速控制
得到误差后,程序采用简单的比例控制(P控制)调整左右轮速度:
左轮速度 = 基础速度 - K × Error
右轮速度 = 基础速度 + K × Error
其中:
- 基础速度为185;
- K为比例系数(程序中取10)。
当小车偏向左侧时,左轮减速、右轮加速;当偏向右侧时,则执行相反操作,从而实现自动修正方向。
此外,当所有灰度传感器均未检测到黑线时,程序会根据上一时刻记录的循迹方向(HD_Last)继续向原方向寻找赛道,提高丢线后的恢复能力。
5. 圈数统计
为了实现自动停车,程序采用状态机方式统计经过起终点的次数,而不是简单检测一次黑线。
每次完整经过标志区域,需要依次经历:
- 进入标志区;
- 离开标志区;
- 回到正常循迹。
程序利用 Flag 控制状态转换,避免同一标志线被重复计数,提高计数准确性。
达到设定圈数后,立即停止电机运行。
6. 自动停车
程序不断比较当前计数值与目标圈数。
当完成用户设定的循迹圈数后,调用:
Motor_Send(0,0);
关闭左右电机PWM输出,使小车平稳停止,完成整个循迹任务。
代码部分
int32_t Get_Encoder_countA = 0, encoderA_cnt, PWMA, Get_Encoder_countB = 0, encoderB_cnt, PWMB;
int flag = 0;
volatile int i = 0, key = 0;
int ang, km;
float error = 0, HD_Last = 1;
int k = 10;
uint8_t Num1, Flag, LED_NUM, LED_flag = 0;
float pitch = 0, roll = 0, yaw = 0;
float initial_angle = 0;
uint8_t Quanshu = 0;
extern enum Mode mode;
extern Motor motor;
extern int L4,L3,L2,L1,R1,R2,R3,R4;
int main(void)
{
int i = 0;
Motor_Init();
motor.AtargetSpeed = 0;
motor.BtargetSpeed = 0;
uint8_t buffer1[32] = {0};
uint8_t buffer2[32] = {0};
SYSCFG_DL_init();
DL_Timer_startCounter(PWM_0_INST);
NVIC_ClearPendingIRQ(UART_0_INST_INT_IRQN);
NVIC_ClearPendingIRQ(ENCODER_INT_IRQN);
NVIC_ClearPendingIRQ(TIMER_0_INST_INT_IRQN);
NVIC_EnableIRQ(UART_0_INST_INT_IRQN);
NVIC_EnableIRQ(ENCODER_INT_IRQN);
NVIC_EnableIRQ(TIMER_0_INST_INT_IRQN);
Motor_start();
while(1)
{
//Motor_Send(300,300);
if(!DL_GPIO_readPins(KEY_PORT, KEY_PIN_2_PIN))
{
delay_ms(10);
while(!DL_GPIO_readPins(KEY_PORT, KEY_PIN_2_PIN));
delay_ms(10);
LED_NUM++;
if(LED_NUM > 5)LED_NUM = 1;
LED_flag = 1;
}
if(LED_flag)
{
LED_flag = 0;
for(uint8_t i = 0; i < LED_NUM; i++)
{
DL_GPIO_setPins(LED_PORT, LED_PIN_16_PIN);
delay_ms(100);
DL_GPIO_clearPins(LED_PORT, LED_PIN_16_PIN);
delay_ms(200);
}
}
if(!DL_GPIO_readPins(KEY_PORT, KEY_PIN_3_PIN))
{
delay_ms(10);
while(!DL_GPIO_readPins(KEY_PORT, KEY_PIN_3_PIN));
delay_ms(10);
Quanshu = LED_NUM;
break;
}
}
while(Quanshu)
{
sensor();
if(L1 && R1)error = 0;
else if(L1 && !L2)error = 1;
else if(R1 && !R2)error = -1;
else if(L1 && L2)error = 2;
else if(R1 && R2)error = -2;
else if(L2 && !L3)error = 3;
else if(R2 && !R3)error = -3;
else if(L2 && L3)error = 4;
else if(R2 && R3)error = -4;
else if(L3 && !L4)error = 5;
else if(R3 && !R4)error = -5;
else if(L3 && L4)error = 6;
else if(R3 && R4)error = -6;
else if(!L3 && L4)error = 7;
else if(!R3 && R4)error = -7;
else
{
if(HD_Last == 1)error = 8;
else if(HD_Last == -1)error = -8;
}
if(L3)HD_Last = 1;
else if(R3)HD_Last = -1;
if(L4 && L3 && L2 && Flag == 0)
{
Num1++;
Flag = 1;
}else if(!L1 && !L2 && !L3 && !L4 && !R1 && Flag == 1)
{
Num1++;
Flag = 2;
}else if(L1 && R1 && Flag == 2)
{
Num1++;
Flag = 0;
}
if(Num1 >= 12 * Quanshu - 1)
{
Flag = 3;
Motor_Send(0, 0);
}else
{
Motor_Send(185 - k * error, 185 + k * error);
}
}
}
注意:这里不是直接记录圈数。
而是:每经过起终点的一部分都会+1。
所以最后:
Num1 >= 12*Quanshu-1
才代表真正完成了指定圈数。
四、二维云台
4.1 MaxiCam
为了实现目标自动跟踪,本项目采用 MaxiCam作为视觉处理单元,负责目标识别,STM32负责运动控制。
1. 工作原理
MaxiCam完成图像采集和目标识别后,将目标中心坐标通过串口发送给STM32。
例如:
160,120
其中:
- 第一个数据表示目标X坐标
- 第二个数据表示目标Y坐标
STM32串口接收到完整的一帧数据后,调用 ParseFrame() 函数进行解析,得到目标的实时坐标。
整个通信流程如下:
MaxiCam识别目标
│
▼
获取目标中心坐标
│
▼
串口发送 "X,Y"
│
▼
STM32串口中断接收
│
▼
解析坐标数据
│
▼
PID控制云台
采用串口中断 + 缓冲区接收的方式,相比轮询方式实时性更高,不会阻塞主程序运行。
4.2 PID算法
获取目标坐标后,需要控制云台不断调整姿态,使目标始终位于画面中心。
1. 控制原理
首先设定图像中心作为目标位置:
图像宽度:320
图像高度:240
目标中心:
(160,120)
然后计算目标当前位置与图像中心之间的误差:
ErrorX = 160 - BallX
ErrorY = 120 - BallY
误差越大,说明目标偏离中心越远,需要调整角度越多。
2. PID控制
项目分别建立了两个独立PID控制器:
- Pan PID(水平轴)
- Tilt PID(俯仰轴)
PID控制器根据当前位置不断计算输出:
输出 = Kp×误差 + Ki×误差积分 + Kd×误差变化率
其中:
- P(比例):快速响应当前位置误差;
- I(积分):消除长期存在的静态误差;
- D(微分):预测误差变化趋势,减小超调,提高稳定性。
程序不断计算新的控制量,使目标逐渐移动至画面中心,实现闭环跟踪。
// PID控制器初始化函数
void PID_Init(PID_Controller *pid, float kp, float ki, float kd)
{
pid->Setpoint = 0.0f;
pid->Input = 0.0f;
pid->Output = 0.0f;
pid->Kp = kp;
pid->Ki = ki;
pid->Kd = kd;
pid->LastError = 0.0f;
pid->Integral = 0.0f;
}
// PID计算函数
float PID_Compute(PID_Controller *pid, float input)
{
float error = pid->Setpoint - input;
pid->Integral += error;
float derivative = error - pid->LastError;
pid->Output = pid->Kp * error + pid->Ki * pid->Integral + pid->Kd * derivative;
pid->LastError = error;
return pid->Output;
}
// 更新PID输入的函数
void PID_SetInput(PID_Controller *pid, float input)
{
pid->Input = input;
}
// 设置PID设定值的函数
void PID_SetSetpoint(PID_Controller *pid, float setpoint)
{
pid->Setpoint = setpoint;
}
4.3 二维云台控制模块
本项目采用"步进电机 + 舵机"组成二维云台:
- X轴(水平方向):步进电机控制左右旋转;
- Y轴(俯仰方向):舵机控制上下运动。
1. 水平控制
PID计算得到X轴控制量后:
目标偏左
↓
步进电机左转
↓
目标回到中心
为了防止频繁抖动,程序设置了死区(Dead Zone):
|输出| < 1
↓
不控制电机
同时限制单次最大步数,避免目标快速移动导致云台剧烈摆动,提高跟踪稳定性。
2. 俯仰控制
Y轴采用PWM控制舵机角度。
PID输出作为角度增量:
当前角度
+
PID输出
↓
新的舵机角度
程序还加入了角度限位:
MIN_ANGLE_Y
~
MAX_ANGLE_Y
避免舵机超出机械结构允许范围,提高系统安全性。
整个视觉闭环控制流程
整个自动跟踪过程采用闭环控制,其工作流程如下:
摄像头采集图像
│
▼
OpenMV目标识别
│
▼
计算目标中心坐标
│
▼
UART串口发送坐标
│
▼
STM32串口中断接收
│
▼
解析目标位置
│
▼
计算X、Y方向误差
│
▼
Pan/Tilt双PID运算
│
▼
控制步进电机和舵机
│
▼
云台调整姿态
│
▼
目标重新回到画面中心
// 摄像头图像尺寸
#define CAMERA_WIDTH 320
#define CAMERA_HEIGHT 240
// 假设的激光位置
float ballX = CAMERA_WIDTH / 2; // 在图像中的x位置
float ballY = CAMERA_HEIGHT / 2; // 在图像中的y位置
float x,y;
//uint8_t KeyNum; //定义用于接收键码的变量
float Angle; //定义角度变量
PID_Controller panPID;
PID_Controller tiltPID;
float servo_rotation_value=0.0;// 舵机1(水平轴)初始角度
float servo_pitch_value=0.0; // 舵机2(垂直轴)初始角度
uint8_t servo_rotation_direction=0; // 舵机1方向翻转标志
uint8_t servo_pitch_direction=1; // 舵机2方向翻转标志
int main(void)
{
/*模块初始化*/
Servo_Init(); //舵机初始化
board_init();//步进电机初始化
//Key_Init(); //按键初始化
OLED_Init();
Serial_Init();
// 初始化PID控制器
PID_Init(&panPID, 0.005, 0.0f, 0.0f); // 水平轴PID参数需要根据实际情况调整
PID_Init(&tiltPID, 0.005f, 0.0f, 0.003f); // 垂直轴PID参数需要根据实际情况调整
PID_SetSetpoint(&panPID,CAMERA_WIDTH/2);
PID_SetSetpoint(&tiltPID,CAMERA_HEIGHT/2);
Angle=90;
while (1)
{
OLED_ShowNum(3, 0 ,y,3);
// OLED_ShowSignedNum(1,0,ballX,3);
// OLED_ShowSignedNum(2,0,ballX,3);
// 假设这里更新了小球的位置
if (FrameReady)
{
FrameReady = 0; // 清除标志位
ParseFrame(); // 调用解析函数
OLED_ShowNum(1, 0 ,last_num1,3);
OLED_ShowNum(2, 0, last_num2,3);
data1=(uint8_t)last_num1;
data2=(uint8_t)last_num2;
// 在这里您就可以使用 last_num1 和 last_num2 了
// 例如,可以再通过串口打印出来进行验证
// Serial_Printf("Last two numbers are: %d, %d\r\n", last_num1, last_num2);
}
// ...
// 计算舵机应该调整的角度
ballX=(uint8_t)data1;
ballY=(uint8_t)data2;
float deltaX = ballX - CAMERA_WIDTH / 2;
float deltaY = ballY - CAMERA_HEIGHT / 2;
if(ballX==0){
ballX=160;
ballY=120;
}
//ballX = (ballX > MAX_X) ? MAX_X : ((ballX < MIN_X) ? MIN_X : ballX);
// 使用PID计算误差(图像像素)
x=PID_Compute(&panPID, ballX);
y=PID_Compute(&tiltPID, ballY);
//x=(float)(deltaX)*0.005;
//y=(float)(deltaY)*0.005;
// if (!servo_rotation_direction) { // 如果servo_rotation_direction为false
// x = -x; // x取反
// }
if (!servo_pitch_direction) { // 如果servo_pitch_direction为false
y = y; // y取反
}
//在限位内,驱动X轴,Y轴移动
// if (MIN_ANGLE_X < servo_rotation_value + x && servo_rotation_value + x < MAX_ANGLE_X) {
// servo_rotation_value += x; // 更新旋转值
//// Servo_SetAngle1(90+servo_rotation_value); // 控制水平轴舵机
// }
if (fabs(x) >= 1.0f) // 加入死区限制
{
if(x<-4)
{
x=-4;
}
if(x>4)
{
x=4;
}
step_motor_rotate(x); // 控制水平步进电机
}
if (MIN_ANGLE_Y < servo_pitch_value + y && servo_pitch_value + y < MAX_ANGLE_Y) { // 假设俯仰值也有类似的有效范围检查
servo_pitch_value += y; // 更新俯仰值
Servo_SetAngle2(servo_pitch_value); // 控制水平轴舵机
}
Delay_ms(5);
}
}
五、总结
本项目完成了一套基于 MSPM0G3507+STM32 双 MCU 架构的智能视觉循迹小车系统。项目全程主导硬件整体方案设计、器件选型、原理图绘制与 PCB 布局打样,完成整机焊接与软硬件联合调试。
硬件层面,采用双 MCU 异构分工架构,充分发挥 STM32 数据处理优势与 MSPM0 实时控制优势,解决了单芯片任务拥堵、实时性差的问题,并通过合理的 PCB 分区布局、强弱电分离设计,有效降低电磁干扰,提升系统稳定性。
软件层面,完成了 GPIO、PWM、UART 等全套底层驱动开发,实现八路高精度循迹算法,使小车具备稳定自主循迹行驶能力;针对二维云台设计并优化 PID 闭环控制算法,经过多轮参数整定,最终实现云台响应时间<2s、定位误差≤2cm,跟随效果稳定。同时完成 MaxiCam 视觉模块的数据解析、滤波运算与串口传输优化,有效降低系统延迟、提升目标识别准确率与实时性,实现视觉识别与运动控制的高效联动。
通过本项目,完整掌握了从硬件架构设计、PCB 开发、底层驱动编程到控制算法调试、视觉数据处理的全流程嵌入式开发能力,具备独立完成智能运动控制系统的设计与优化能力。
项目展望
本次项目整体功能完成度较高,但仍有优化空间。后续可进一步升级完善:
- 算法升级:将传统 PID 替换为增量 PID、模糊 PID,进一步提升云台动态响应速度与抗干扰能力,消除小幅抖动;
- 视觉优化:引入帧数据滤波、动态阈值校准,提升复杂光线场景下的识别稳定性;
- 智能策略优化:结合视觉识别信息实现自动变速、弯道预判,提升小车行驶流畅度;
- 硬件迭代:优化电源与走线布局,进一步降低噪声干扰,提升整机集成度与便携性;
- 功能拓展:可增加姿态检测、无线调参、上位机可视化等功能,使系统更加智能化、模块化。
以上就是本次双 MCU 智能视觉循迹小车项目的全部内容,感谢大家耐心观看与阅读!
本项目所有源码、算法思路以及硬件设计资料完全免费开源,大家可以直接下载学习、参考与使用,欢迎各位基于本项目继续优化与拓展功能。
祝愿大家在嵌入式学习道路上不断进步、学有所成,学业顺利、生活愉快,都能做出更优秀的项目!
更多推荐

所有评论(0)