目录
[一、 系统总体架构设计](#一、 系统总体架构设计)
[二、 硬件架构设计与电源隔离](#二、 硬件架构设计与电源隔离)
[三、 双MCU协同控制与视觉闭环实现](#三、 双MCU协同控制与视觉闭环实现)
[1. 底盘寻迹控制(MSPM0)](#1. 底盘寻迹控制(MSPM0))
[2. 瞄准云台与视觉闭环(STM32F401)](#2. 瞄准云台与视觉闭环(STM32F401))
[3. 激光延时与状态机控制](#3. 激光延时与状态机控制)
[四、 难点攻坚与工程思考](#四、 难点攻坚与工程思考)
前言
在本次全国大学生电子设计竞赛中,我担任硬件设计与嵌入式控制负责人,带队完成了E题《简易自行瞄准装置》的设计与制作。该项目要求系统能够在移动或静止状态下,通过视觉反馈驱动二维云台,实现激光的精准打靶与圆弧画圆。
本项目采用了**"双MCU+视觉反馈+闭环控制"** 的架构。通过双路独立电源隔离设计降低电机干扰,底盘寻迹与云台瞄准分别由不同主控独立处理,最终成功完成了赛题基本要求,并获得省级三等奖。本文将从系统架构、硬件设计、双核协同控制及视觉闭环算法等维度进行硬核复盘。
一、 系统总体架构设计
为满足赛题要求并保证系统的实时性与稳定性,系统整体采用了双MCU协同控制架构:
-
底盘控制端(TI MSPM0系列MCU):基于CCS开发环境,负责自动寻迹小车底盘的电机驱动、循迹算法与运动控制。
-
瞄准控制端(STM32F401RCT6):基于Keil 5开发环境,负责二维云台的瞄准模块、外设驱动、视觉坐标解析与激光控制。
-
视觉模块:作为上位机,通过串口(UART)将靶心与光斑的坐标实时发送给瞄准控制端。
二、 硬件架构设计与电源隔离
作为硬件负责人,我使用嘉立创EDA独立完成了整机的原理图与PCB Layout,并负责整机的机械搭建与走线。
-
双路独立供电隔离系统 :
赛题严格要求主控与瞄准模块独立开关控制。更重要的是,小车的直流电机在启动和变速时会产生极大的瞬态电流,引发严重的电压波动与电磁干扰。为此,我设计了双路独立LDO电源树,将电机驱动电源、底盘MCU电源与STM32云台控制电源进行物理隔离,有效阻断了电机噪声对视觉反馈与云台微步进控制的干扰,确保了激光打靶的稳定性。
-
结构与抗干扰布线 :
在紧凑的尺寸限制(25cm×15cm×25cm)内,合理规划了PCB布局,将大电流电机走线与弱信号(UART通信、步进电机驱动)走线严格分离,并进行了地线敷铜隔离处理。
三、 双MCU协同控制与视觉闭环实现
系统的核心在于"运动"与"瞄准"的独立解耦与高效协同。
1. 底盘寻迹控制(MSPM0)
底盘采用TI MSPM0系列MCU进行实时控制。利用其定时器资源与PWM模块,结合红外循迹传感器的数组,实现了基于偏差PID的电机差速控制。得益于该MCU的硬件资源调配,小车能够平稳、高速地在100cm×100cm的黑色边线正方形上逆时针稳定寻迹。
2. 瞄准云台与视觉闭环(STM32F401)
瞄准模块的核心在于将视觉坐标系映射到步进电机的物理坐标系。
-
串口通信与坐标解析:STM32通过UART高速接收视觉模块发送的X/Y坐标偏差。利用状态机处理串口数据帧的接收与校验,确保数据不丢包。
-
闭环控制算法:将解析出的靶心与光斑X/Y轴偏差,通过比例控制算法转化为二维云台(两相步进电机)的步进脉冲数量与方向。云台快速调整激光笔姿态,直到偏差逼近靶心。
-
响应时间优化 :赛题要求2s/4s内击中靶心。我利用STM32的定时器中断构建了系统状态机逻辑,通过非阻塞式延时替代了传统的
delay()死等,极大提高了系统的响应速度与控制精度,光斑精准落点在2cm误差范围内。
3. 激光延时与状态机控制
在赛题中,激光的开启和关闭时机极其重要。通过定时器中断精确控制激光引脚的输出电平,实现打靶时的瞬时点亮与关断,避免多余光斑在感光纸上留下痕迹影响判分。
四、 难点攻坚与工程思考
-
多机通信的数据粘包与同步 :
在视觉模块与STM32的串口通信中,容易受到外界环境光与电机干扰。通过自定义数据包头与校验位,并在STM32中利用空闲中断配合DMA进行数据接收,彻底解决了串口数据丢包与错位的问题。
-
步进电机的失步与共振 :
二维云台需要微步进调整以达到高精度瞄准。在硬件上通过调整驱动器细分电流;在软件上通过梯形加减速算法控制步进脉冲频率,有效避免了云台启动/停止时的机械抖动与失步现象,保证了光斑的稳定驻留。
五、核心代码
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "board.h"
#include "Delay.h"
#include "usart.h"
#include "Emm_V5.h"
#include "key.h"
#include "math.h"
#include "Serial Screen.h"
#include "usart3.h"
#include "JiGuang.h"
#include "tip3.h"
#include "Time.h"
#include "usart4.h"
uint8_t KeyNum = 0;
uint8_t KeyFalg = 0;
uint32_t distance = 0;
int32_t x,y;
uint8_t Flag_XY = 0;
uint8_t Three_Flag = 0;
uint8_t Four_Flag = 0;
uint8_t Two_Flag = 0;
uint16_t flag_4s = 0;
uint16_t flag_2s = 0;
uint8_t UpFlag = 0;
uint8_t up = 0;
extern char flag_4,a;
int main(void)
{
board_init(); //串行电机初始化
Key_Init();
OLED_Init();
Serial_Screen_Init();
usart3_Init();
Jiguang_Init();
Timer_Init();
usart4_Init();
UpdateDisplay();
//上电延时2秒等待Emm_V5.0闭环初始化完毕
Delay_ms(2000);
while (1)
{
/**********************串口屏调整步进电机姿态**********************/
Screen_Return0();
if(Return0_Set == 1)
{
Emm_V5_Origin_Set_O(1, 1);
Return0_Set = 0;
}
else if(Return0_Set == 2)
{
Emm_V5_Origin_Set_O(2, 1);
Return0_Set = 0;
}
if(Serial_RxFlag == 1)
{
Control_actuator(1, Serial_RxPacket1[0], (uint16_t)Serial_RxPacket1[1], Serial_RxPacket1[2], (uint32_t)Serial_RxPacket1[3]);
Serial_RxFlag = 0;
}
else if(Serial_RxFlag == 2)
{
Control_actuator(2, Serial_RxPacket2[0], (uint16_t)Serial_RxPacket2[1], Serial_RxPacket2[2], (uint32_t)Serial_RxPacket2[3]);
Serial_RxFlag = 0;
}
/******************************************************************/
/*****************************第二题*******************************/
if(Two_Flag == 1)
{
Emm_V5_Origin_Trigger_Return(1,0,0);
Delay_ms(200);
Emm_V5_Origin_Trigger_Return(2,0,0);
Delay_ms(200);
Two_Flag = 0;
//
}
/******************************************************************/
/*****************************第三题*******************************/
if(Three_Flag == 1)
{
if(UpFlag == 0)
{
UpFlag = 1;
Control_actuator(2, 1, 10, 0, 200);
Delay_ms(200);
}
actrual_get();
if(RotationAngle!=0)
{
Control_actuator(1, (uint8_t)RotationDir, 30, 0, (RotationAngle*3200)/360);
RotationAngle = 0;
}
else if(Flag == 1 && Flag_XY == 0)
{
//计算水平差值
x = actual_x - 115;
if(x <= 1 && x>= -1)
{
Emm_V5_Stop_Now(1, 0);
Flag_XY = 1;
}
else if(x <= -1) //逆时针
{
Control_actuator(1, 0, 10, 0, 1);
}
else//顺时针
{
Control_actuator(1, 1, 10, 0, 1);
}
}
else if(Flag == 1 && Flag_XY == 1)
{
//102
//98
//90
//计算垂直差值
y = actual_y - FixedValues[CurrentFixedIndex];
if(y <= 1 && y>= -1)
{
Emm_V5_Stop_Now(2, 0);
Three_Flag = 0;
}
else if(y <= -1) //向上
{
Control_actuator(2, 1, 10, 0, 1);
}
else//向下
{
Control_actuator(2, 0, 10, 0, 1);
}
Flag_XY = 0;
}
Flag = 0;
}
/******************************************************************/
/*****************************第四题*******************************/
if(Four_Flag == 1)
{
if(up == 0)
{
Control_actuator(2, 1, 10, 0, 165);
Delay_ms(200);
up = 1;
}
actrual_get();
if(Flag == 1)
{
//计算水平差值
x = actual_x - 112;
if(x <= 2 && x>= -2)
{
Emm_V5_Stop_Now(1, 0);
}
if(a==1)
{
Control_actuator(1, 0, 20, 0, 800);
a=0;
}
else if(x <= -2&&flag_4==0) //逆时针
{
Control_actuator(1, 0, 20, 0, 3);
}
else if (x >= 2&&flag_4==0) //顺时针
{
Control_actuator(1, 1, 20, 0, 3);
}
else if(x <= -2&&flag_4==2) //逆时针
{
Control_actuator(1, 0, 20, 0, 3);
}
else if (x >= 2&&flag_4==2) //顺时针
{
Control_actuator(1, 1, 20, 0, 3);
}
else if(x <= -2&&(flag_4==1||flag_4==3)) //逆时针
{
Control_actuator(1, 0, 25, 0, 3);
}
else if(x >= 2&&(flag_4==1||flag_4==3))//顺时针
{
Control_actuator(1, 1, 25, 0, 3);
}
else if(x <= -2&&flag_4==4) //逆时针
{
Control_actuator(1, 0, 20, 0, 5);
}
else if(x >= 2&&flag_4==4)//顺时针
{
Control_actuator(1, 1, 20, 0, 5);
}
}
Flag = 0;
}
//
/******************************************************************/
/*****************************按键检测*****************************/
KeyNum = Key_GetNum();
if(KeyNum == 1)
{
KeyFalg = 1;
}
if(KeyNum == 2)
{
KeyFalg = 2;
}
if(KeyNum == 3)
{
KeyFalg = 3;
}
if(KeyNum == 4)
{
KeyFalg = 4;
}
if(KeyNum == 5)
{
KeyFalg = 5;
}
if(KeyNum == 6)
{
KeyFalg = 6;
}
if(KeyNum == 7)
{
KeyFalg = 7;
}
if(KeyNum == 8)
{
KeyFalg = 8;
}
/******************************************************************/
/*****************************按键菜单*****************************/
switch(KeyFalg)
{
case 1: // 按键1 - 启动第三题
Three_Flag = 1;
KeyFalg = 0;
flag_4s = 1;
break;
case 2: // 按键2 - 增加角度
RotationAngle += 15;
if(RotationAngle > 180)
{
RotationAngle = 0;
}
UpdateDisplay();
KeyFalg = 0;
break;
case 3: // 按键3 - 切换定值
CurrentFixedIndex = (CurrentFixedIndex + 1) % 3;
UpdateDisplay();
KeyFalg = 0;
break;
case 4: // 按键4 - 切换方向
RotationDir = !RotationDir;
UpdateDisplay();
KeyFalg = 0;
break;
case 5: // 按键5 - 开启第二题
Two_Flag = 1;
flag_2s = 1;
KeyFalg = 0;
break;
case 6: // 按键6 -
// Four_Flag = 1;
// Jiguang_ON();
KeyFalg = 0;
break;
case 7: // 按键7 - 开始
// Jiguang_OFF();
Four_Flag = 1;
KeyFalg = 0;
break;
case 8: // 按键8 - 开启激光
Jiguang_ON();
KeyFalg = 0;
break;
}
/******************************************************************/
}
}
void TIM1_UP_IRQHandler(void) //10ms读取一次编码器的数据 即编码器速度
{
if (TIM_GetITStatus(TIM1, TIM_IT_Update) != RESET) //检查TIM1更新中断发生与否
{
if(flag_4s != 0)
{
flag_4s++;
if(flag_4s > 380)
{
flag_4s = 0;
Jiguang_ON();
}
}
if(flag_2s != 0)
{
flag_2s++;
if(flag_2s > 180)
{
flag_2s = 0;
Jiguang_ON();
}
}
//中断时需要做的事情
TIM_ClearITPendingBit(TIM1, TIM_IT_Update ); //清除TIMx更新中断标志
}
}
六、项目展示
简易瞄准系统3