目录
前言
一、 系统总体架构设计
二、 硬件架构设计与电源隔离
三、 双MCU协同控制与视觉闭环实现
1. 底盘寻迹控制(MSPM0)
2. 瞄准云台与视觉闭环(STM32F401)
3. 激光延时与状态机控制
四、 难点攻坚与工程思考
五、核心代码
六、PCB设计
STM32F104RCT6功能板(双板)
MSPM0系列功能板
七、项目展示
前言
在本次全国大学生电子设计竞赛中,我担任硬件设计与嵌入式控制负责人,带队完成了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更新中断标志 } }六、PCB设计
STM32F104RCT6功能板(双板)
注:由于电赛题目不确定,选择使用一块核心板配双功能板的形式,确保各个模块都能涵盖。
MSPM0系列功能板
七、项目展示
简易瞄准系统3