提示:该篇为25年电赛e题,简易自行瞄准装置

文章目录

前言

一、项目概述与技术架构

1.1 项目背景与需求分析

1.2 双MCU架构设计

二、硬件设计与PCB实现

2.1 原理图设计要点

2.2 PCB布局布线经验

​编辑​编辑功能模块分区布局

电源布线优先

数字信号与大电流分离

去耦与接地设计

接口统一规范

预留调试接口

三、小车循迹部分底层驱动开发

3.1 电机驱动

电机控制原理

代码框架

3.2 编码测速

为了实现电机闭环控制,小车两侧电机均安装了AB相增量式编码器,用于实时检测车轮转速和旋转方向。

1. AB相编码器原理

2. 外部中断计数

3. 编码器的作用

3.3 八路灰度

1. 工作原理

2. 数据采集

3. 在循迹中的作用

3.4  主函数部分

1. 系统初始化

2. 按键选择循迹圈数

3. 八路灰度循迹

4. 差速控制

5. 圈数统计

6. 自动停车

代码部分

四、二维云台

4.1  MaxiCam

1. 工作原理

4.2  PID算法

1. 控制原理

2. PID控制

4.3  二维云台控制模块

1. 水平控制

2. 俯仰控制

整个视觉闭环控制流程

五、总结

项目展望


GitHub - dawn-qvq/-e-2025: 2025年全国大学生电子设计竞赛——简易自行瞄准装置开源项目。采用STM32+MSPM0G3507双MCU架构,结合MaxiCam视觉识别、二维云台、PID控制及PCB设计,实现目标检测、自动跟踪与稳定瞄准。 CSDN开源链接:https://blog.csdn.net/2301_79957440/article/details/162274628?spm=1001.2014.3001.5501 · GitHub2025年全国大学生电子设计竞赛——简易自行瞄准装置开源项目。采用STM32+MSPM0G3507双MCU架构,结合MaxiCam视觉识别、二维云台、PID控制及PCB设计,实现目标检测、自动跟踪与稳定瞄准。 CSDN开源链接:https://blog.csdn.net/2301_79957440/article/details/162274628?spm=1001.2014.3001.5501 - dawn-qvq/-e-2025https://github.com/dawn-qvq/-e-2025.git


前言

在智能车、嵌入式控制、机器视觉快速普及的当下,传统单单片机小车普遍存在任务卡顿、控制延迟高、视觉与运动冲突、稳定性差等问题。为解决以上痛点,本项目采用 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 开发、底层驱动编程到控制算法调试、视觉数据处理的全流程嵌入式开发能力,具备独立完成智能运动控制系统的设计与优化能力。

项目展望

本次项目整体功能完成度较高,但仍有优化空间。后续可进一步升级完善:

  1. 算法升级:将传统 PID 替换为增量 PID、模糊 PID,进一步提升云台动态响应速度与抗干扰能力,消除小幅抖动;
  2. 视觉优化:引入帧数据滤波、动态阈值校准,提升复杂光线场景下的识别稳定性;
  3. 智能策略优化:结合视觉识别信息实现自动变速、弯道预判,提升小车行驶流畅度;
  4. 硬件迭代:优化电源与走线布局,进一步降低噪声干扰,提升整机集成度与便携性;
  5. 功能拓展:可增加姿态检测、无线调参、上位机可视化等功能,使系统更加智能化、模块化。

    以上就是本次双 MCU 智能视觉循迹小车项目的全部内容,感谢大家耐心观看与阅读!

    本项目所有源码、算法思路以及硬件设计资料完全免费开源,大家可以直接下载学习、参考与使用,欢迎各位基于本项目继续优化与拓展功能。

    祝愿大家在嵌入式学习道路上不断进步、学有所成,学业顺利、生活愉快,都能做出更优秀的项目!

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐