目录
“愿大家都少走弯路,在迷茫时看到希望!”
一.2023年E题完整设计
<1>选择方案
任务一:实现按键按下复位(基础部分)
方法①:识别四顶点位置->连接对角线得到中心点->PID调节使激光点与中心点重合
方法②:识别四顶点位置->对角顶点坐标求平均值得中心点位置->PID调节使重合
方法③:固定所有器件位置,保证各点PWM值不变,得到中心点PWM固定值,开环设定
任务二:实现激光点绕边框一周(基础部分)
步骤I:激光点由中心点到达边线左上角
步骤II:顺时针绕一圈
方法①:两点定线,先确定两点坐标,连线确定等分点,使用PID算法在等分点间移动
方法②:不使用PID,利用与目标点坐标差计算移动方向,每次移动距离为舵机最小精度值
方法③:求PWM和坐标(x,y)的函数关系(近似线性),直接设定PWM值到达指定点
任务三:实现激光点绕A4纸边缘一周(基础部分)
(与任务二区别:矩形放置角度可以倾斜;要区分两矩形宽度以识别A4纸)
任务四:实现绿色激光追踪红色激光(发挥部分)
方法①:区分红绿色激光并得到坐标->PID直接跟踪
<2>任务分配
将上述任务分解成多个要完成的技术,以便分工:
1.硬件平台搭建
2.stm32控制算法:
①PID控制激光点移动到目标点算法(核心)
②舵机以最小分度值移动算法(细微调节)
③给定两点以及等分数计算所有等分点算法(线上移动减少偏差)
④在PID寻点时获取基本点(矩形顶点及中心)PWM值算法
⑤stm32和jetson nano的通信规则设计与数据互传
3.OpenCV识别算法
①识别铅笔线边框:灰度图转换->阈值分割成二值图->霍夫直线变换得到直线上两点(非端点)->从得到的多条直线中筛选去重->编写“已知两直线上两点求直线交点”算法->求得四端点
②识别A4纸边框:阈值分割后利用Harris角点检测出A4框的8个顶点->编写“从8个顶点中识别两两相邻顶点”算法->求得框中心线4顶点
③区分红绿激光点算法:转换到Hsv色彩空间->分别设置阈值,在Hsv空间中二值化图像提取红绿色区域以得到激光点坐标
4.主函数(程序流程)设计
5.电赛报告书写
<3>代码分析
1.stm32上关键源码分析
I.基本部分
(1)引脚使用说明
//引脚使用说明
/*
oled.h GPIOA PIN0/1
bluetooth.h GPIOA PIN2/3
joystick.h GPIOA PIN4/5 ADC1_CH4/5 GPIOB PIN11/12/13 EXTI12/13
Pwm.h GPIOA PIN8/11 TIM1_CH1/4 50hz
usart.h GPIOA PIN9/10 TX/RX Black/White
beep.h GPIOB PIN14
led.h GPIOB PIN15
Timer.h TIM2/3
*/
(2)头文件声明
//头文件声明
#include “public.h” //公用引用函数封装
//#include “bluetooth.h” //蓝牙模块
#include “oled.h” //OLED显示屏模块
#include “Pwm.h” //PWM波生成模块
#include “servo_motor.h” //云台控制函数模块
#include “joystick.h” //摇杆控制模块
#include “string.h”
#include “Delay.h”
#include “Timer.h” //定时器模块
#include “usart.h” //uart通信模块
#include “beep.h” //蜂鸣器模块
#include “led.h” //led灯模块
#include “dma.h” //dma数据转存模块
(3)全局变量和宏定义声明
//全局变量和宏定义声明
//#define OpenLoop_OL //开环实现功能执行
#define CloseLoop_CL //闭环实现功能执行
extern float Voltage[2]; //ad测量电压值[0.3.3] //ad.c
extern char USART_RX_INFO[USART_REC_LEN]; //uart接收数据 //usart.c
extern int x,y; //激光当前坐标 //servo_motor.c
extern int Vertex[4][2]; //四顶点位置 //servo_motor.c
extern int Vertex_Peak_Pos[4][2];
extern int Vertex_A4[4][2];
extern Pwm Center_Pwm;
extern Pwm Peak_Pwm[4];
extern Pwm A4_Pwm[4];
int Programme_Progress=0; //比赛程序进度
int order=0; //蓝牙接收到的命令
int Main_Wait_Stop_Sign =1; //主程序等待标志位
extern int JoyStick_Control_Stop_Sign; //摇杆控制程序结束标志位
int Get_Depend_Point_Pos_Stop_Sign=1;
int Get_A4_Point_Pos_Stop_Sign=1;
extern int Follow_Track_Stop_Sign; //矩形寻迹结束标志位
extern int Follow_Point_Stop_Sign; //绿激光跟随红激光结束标志位
II.模块代码
(1)Timer——定时器延时函数模块
#include “Timer.h”
//TIM2/3
void Timer_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
TIM_InternalClockConfig(TIM2);
TIM_InternalClockConfig(TIM3);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 60000 - 1; //分辨率1us,最大60ms
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
}
void Timer_delay_us(int xus)
{
TIM_Cmd(TIM2,ENABLE); //启动定时器
while(TIM2->CNT < xus);
TIM2->CNT = 0;
TIM_Cmd(TIM2,DISABLE); //关闭定时器
}
void Timer_delay_ms(int xms)
{
int i=0;
for(i=0;i<xms;i++)Timer_delay_us(1000);
}
//外部中断专用延时函数
void EXIT_LINE_Timer_delay_us(int xus)
{
TIM_Cmd(TIM3,ENABLE); //启动定时器
while(TIM3->CNT < xus);
TIM3->CNT = 0;
TIM_Cmd(TIM3,DISABLE); //关闭定时器
}
void EXIT_LINE_Timer_delay_ms(int xms)
{
int i=0;
for(i=0;i<xms;i++)EXIT_LINE_Timer_delay_us(1000);
}
说明:
在Timer_Init()中开启了两个定时器TIM1/2,由Timer_delay_us()和EXIT_LINE_Timer_delay_us()分别使用,分别在中断函数内外使用,避免重复调用冲突
(2)servo_motor——云台舵机控制模块
①控制舵机的旋转
int Oc_Lp[4]={750,750,750,750};
int Oc_Vp[4]={763,763,763,763};
/*********************************************************
函数功能:云台水平方向旋转
*********************************************************/
void Spinnig_Level(int diff)
{
if(diff<0)
{
Oc_Lp[0]=Oc_L=(Oc_L+diff)<660?660:(Oc_L+diff);
}
else if(diff>0)
{
Oc_Lp[0]=Oc_L=(Oc_L+diff)>840?840:(Oc_L+diff);
}
TIM_SetCompare1(TIM1,Oc_L);
int i;
for(i=3;i>0;i–)Oc_Lp[i]=Oc_Lp[i-1];
}
说明:
这里的Oc_Lp存储的是控制舵机的pwm波参数中的OC寄存器中的值,作为舵机运动最基本的函数,
舵机的控制通过改变pwm波参数中的OC寄存器中的值实现
。这里定义数组实现记忆功能,可存储前三次的OC值。并通过三元运算符设定上下限,将最终的OC值通过TIM_SetCompare1()设定。
②控制激光点到达某一像素点
/*********************************************************
函数功能:云台控制激光点到达某一点
函数参数:目标点的坐标
*********************************************************/
int x=360,y=360; //跟随点当前坐标
int Reach_Pos_CL_Stop_Sign=1;
//云台水平方向旋转PID值
float Level_Kp=0.06;
float Level_Ki=0.02;
float Level_Kd=0.01;
//云台竖直方向旋转PID值
float Vert_Kp=0.06;
float Vert_Ki=0.02;
float Vert_Kd=0.01;
void Reach_Pos_CL(int Target_X,int Target_Y,int Reach_Pos_CL_MODE)
{
int Sign(int num);
void Get_Point_Pos(void);
int near(int Target_X,int Target_Y);
int diff_x,diff_y;
while(Reach_Pos_CL_Stop_Sign)
{
Timer_delay_ms(30);
Get_Point_Pos();
if(near(Target_X,Target_Y)<=6)
{
Beep_Times(10,1,NORMAL_MODE);
break;
}
if(Reach_Pos_CL_MODE==PID_MODE && near(Target_X,Target_Y)>60) //用pid计算舵机单位数
{
diff_x=Pid_Control(Level_Kp,Level_Ki,Level_Kd,Target_X,x,PID_REALIZE);
diff_y=Pid_Control(Vert_Kp,Vert_Ki,Vert_Kd,Target_Y,y,PID_REALIZE);
}
else if(Reach_Pos_CL_MODE==MINMIZE_MODE) //以舵机最小分辨率为单位
{
diff_x=-Sign(x-Target_X);
diff_y=-Sign(y-Target_Y);
}
else if(Reach_Pos_CL_MODE==PID_MODE && near(Target_X,Target_Y)<=60) //用pid计算舵机单位数
{
diff_x=-Sign(x-Target_X);
diff_y=-Sign(y-Target_Y);
Timer_delay_ms(30);
}
Spinnig_Level(X_DIRdiff_x);
Spinnig_Vert(Y_DIRdiff_y);
Timer_delay_ms(20);
}
}
int Sign(int num)
{
if(num>5)return 1;
else if(num<-5)return -1;
else return 0;
}
int my_abs(int a,int b)
{
return a-b>0?a-b:b-a;
}
int near(int Target_X,int Target_Y)
{
return my_abs(Target_X,x)+my_abs(Target_Y,y);
}
说明:
输入参数:目标点像素坐标;追踪模式(PID【PID与最小精度混合】模式和最小精度值模式)
追踪过程:
——得到当前激光点坐标:Get_Point_Pos()
——如果接近目标点则蜂鸣器鸣叫并退出【near(Target_X,Target_Y)<=6,说明当前坐标与目标横纵坐标差之和{“距离”}小于6个像素】
——如果使用PID模式:
——“距离”大于60时采用PID算法快速靠近,计算出OC变化值diff_x、diff_y
——“距离”小于60时使用最小精度模式缓慢靠近,利用“符号函数sign()”计算diff
——调用Spinnig_Level()、Spinnig_Level()进行水平和垂直舵机的旋转
③与上位机jetson nano通讯接收点坐标
a.激光点坐标的实时接收
/*********************************************************
函数功能:stm32获取当前激光坐标
*********************************************************/
void Get_Point_Pos(void)
{
if(USART_RX_INFO[0]==’x’) //检查数据定位是否正确(上位机发送信息为:x123y456)
{
x=(USART_RX_INFO[1]-‘0’)*100+(USART_RX_INFO[2]-‘0’)*10+USART_RX_INFO[3]-‘0’;
}
if(USART_RX_INFO[4]==’y’) //检查数据定位是否正确(上位机发送信息为:x123y456)
{
y=(USART_RX_INFO[5]-‘0’)*100+(USART_RX_INFO[6]-‘0’)*10+USART_RX_INFO[7]-‘0’;
}
}
说明:
规定上位机每次发送数据格式为:以#开头,以$结尾;stm32usart模块对接收数据进行解析
上位机坐标数据格式为:x123y456;123、456代表三位坐标值,字符’x’、’y’起定位作用
stm32对接收到的字符坐标进行解析如上
b.特殊坐标接收
//高级控制函数(CloseLoop–CL)
int Vertex_Peak_Pos[4][2];
int Center_Pos[2];
Pwm Center_Pwm;
Pwm Peak_Pwm[4];
Pwm A4_Pwm[4];
//获取重要点坐标
void Get_Point_5(void)
{
int i,j;
while(1)
{
for(i=0;i<8;i++)
{
if(USART_RX_INFO[4i]==’a’+i)continue;
else break;
}
if(i==8)
{
for(i=0;i<4;i++)
{
for(j=0;j<2;j++)Vertex_Peak_Pos[i][j]=(USART_RX_INFO[4(2i+j)+1]-‘0’)100+(USART_RX_INFO[4(2i+j)+2]-‘0’)10+(USART_RX_INFO[4(2*i+j)+3]-‘0’);
}
break;
}
}
while(!(USART_RX_INFO[0]==’i’&&USART_RX_INFO[4]==’j’));
Center_Pos[0]=(USART_RX_INFO[1]-‘0’)*100+(USART_RX_INFO[2]-‘0’)*10+USART_RX_INFO[3]-‘0’;
Center_Pos[1]=(USART_RX_INFO[5]-‘0’)*100+(USART_RX_INFO[6]-‘0’)*10+USART_RX_INFO[7]-‘0’;
Beep_Times(50,5,NORMAL_MODE);
}
说明:
这里接收的是铅笔线框四个顶点的坐标和中心点坐标,但是一次发送的数据长度不能太长,这里拆分成两部分接收(数据格式为:axxxbxxxcxxx…hxxx共8组值四个坐标),
关键在于两部分的衔接
while(!(USART_RX_INFO[0]==’i’&&USART_RX_INFO[4]==’j’));确保收到四个顶点坐标后持续等待中心点坐标的发送
④得到一点坐标对应的舵机pwm波OC值
int sum_num(int *num,int n)
{
int i,sum;
for(i=sum=0;i<n;i++)sum+=num[i];
return sum;
}
//获取目标点pwm值
void Get_Pwm(int px,int py,Pwm *target_pwm,int n)
{
Reach_Pos_CL(px,py,PID_MODE);
target_pwm->level=sum_num(Oc_Lp,n)/n;
target_pwm->vert=sum_num(Oc_Vp,n)/n;
}
说明:
通过②控制函数控制激光点到达指定点后记录目标点pwm值并返回;Pwm结构体定义如下
typedef struct Pwm{
int level;
int vert;
}Pwm;
可以通过改变参数n的值选择是否滤波,4>n>1时进行滤波,取前几次OC值的平均值,不建议滤波
⑤控制激光点沿四边形巡线
//巡线
void Follow_Track(int Vertex[4][2],int divide_num)
{
int i,j;
float sub_l,sub_v;
Pwm Vertex_Pwm[4];
for(i=0;i<4;i++)Get_Pwm(Vertex[i][0],Vertex[i][1],&Vertex_Pwm[i],1);
for(i=0;i<4;i++)
{
sub_l=(Vertex_Pwm[(i+1)%4].level-Vertex_Pwm[i].level); //下一个顶点与当前顶点pwm之差
sub_v=(Vertex_Pwm[(i+1)%4].vert-Vertex_Pwm[i].vert); //下一个顶点与当前顶点纵坐标之差
for(j=0;j<divide_num;j++)
{
Reach_Pos_OL(Vertex_Pwm[i].level+jsub_l/divide_num,Vertex_Pwm[i].vert+jsub_v/divide_num);
Timer_delay_ms(200);
}
Reach_Pos_OL(Vertex_Pwm[(i+1)%4].level,Vertex_Pwm[(i+1)%4].vert);
Timer_delay_ms(300);
}
Beep_Times(50,5,NORMAL_MODE);
}
说明:
输入参数:四边形顺时针顺序顶点坐标、每段等分数divide_num
巡线过程:
——得到四个顶点坐标对应的水平、数值舵机OC值
——在for循环内依次经过四个顶点,视作四个大任务
——内部使用for循环分解小任务,根据等分段数divide_num计算等分点横纵pwm值并移动至
——任务结束鸣叫示意
III.主函数与中断函数部分
(1)红色激光云台
//主函数部分
//重新重启初值还原设置
void Programme_Reset(void)
{
Beep_Times(1000,1,NORMAL_MODE);
Led_Times(1000,1,NORMAL_MODE);
Programme_Progress=0;
Main_Wait_Stop_Sign=1;
JoyStick_Control_Stop_Sign=1;
Follow_Track_Stop_Sign=1;
Get_A4_Point_Pos_Stop_Sign=1;
Get_Depend_Point_Pos_Stop_Sign=1;
}
int main(void)
{
//********************初始化程序********************
Timer_Init(); //定时器初始化
// BlueToothInit(9600,USART_Parity_No,USART_StopBits_1,USART_WordLength_8b); //蓝牙初始化
OLED_Init(); //oled初始化
Beep_Init(); //蜂鸣器初始化
Led_Init(); //led灯初始化
TIM1_PWM_Init(9999,143); //一周期20ms,分辨率20ms/10000)
TIM_SetCompare1(TIM1,750); //对齐角度为90度(1.5ms)
TIM_SetCompare4(TIM1,763); //对齐角度为90度(1.5ms)
uart_init(115200); //uart1初始化
JoyStick_Init(); //JoyStick摇杆初始化
//*************************比赛程序部分*************************
while(1)
{
int i;
//重新重启初值还原设置
Programme_Reset();
// Reach_Pos_CL(50,50,PID_MODE);
Axes_Init();
// Follow_Track(Vertex_Peak_Pos,1);
while(Main_Wait_Stop_Sign);
//摇杆控制
JoyStick_Control();
//#ifdef OpenLoop_OL
// Follow_Track_OL();
//#endif
//#ifdef CloseLoop_CL
// //等待上位机发送初始坐标
// Get_Depend_Point_Pos();
// //环绕正方形顺时针旋转一周
// while(Get_Depend_Point_Pos_Stop_Sign);
//Follow_Track_CL(Vertex_Peak_Pos,2,PID_MODE);
//#endif
Pwm_Track(Peak_Pwm,1);
while(Follow_Track_Stop_Sign);
Get_A4_Point_Pos();
Timer_delay_ms(2000);
// Follow_Track_CL(Vertex_A4,4,MINMIZE_MODE);
// Follow_Track(Vertex_A4,4);
for(i=0;i<4;i++)Get_Pwm(Vertex_A4[i][0],Vertex_A4[i][1],&A4_Pwm[i],1);
Pwm_Track(A4_Pwm,6);
while(Get_A4_Point_Pos_Stop_Sign);
}
}
//中断函数部分
//按键中断函数
void EXTI15_10_IRQHandler()
{
if (EXTI_GetITStatus(EXTI_Line11) == SET)
{
EXIT_LINE_Timer_delay_ms(10);
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11)==0) //软件防抖
{
Beep_Times(50,2,EXIT_LINE_MODE);
Reach_Pos_OL(Oc_L,Oc_V); //保持激光当前指向位置
while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11)==0); //等待按键松开
//再次按下才退出
EXIT_LINE_Timer_delay_ms(10);
while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11)==1);
EXIT_LINE_Timer_delay_ms(10);
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11)==0); //软件防抖
while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11==1)); //等待按键松开
Beep_Times(50,2,EXIT_LINE_MODE);
EXTI_ClearITPendingBit(EXTI_Line11);
}
}
else if (EXTI_GetITStatus(EXTI_Line12) == SET)
{
EXIT_LINE_Timer_delay_ms(10);
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_12)==0) //软件防抖
{
Programme_Progress++;
Beep_Times(500,1,EXIT_LINE_MODE);
if(Programme_Progress==1)
{
Main_Wait_Stop_Sign=0;
}
else if(Programme_Progress==2)
{
JoyStick_Control_Stop_Sign=0;
}
else if(Programme_Progress==3)
{
// Get_Depend_Point_Pos_Stop_Sign=0;
Follow_Track_Stop_Sign=0;
}
else if(Programme_Progress==4)
{
Get_A4_Point_Pos_Stop_Sign=0;
// Follow_Track_Stop_Sign=0;
}
else if(Programme_Progress==5)
{
// Get_A4_Point_Pos_Stop_Sign=0;
}
else if(Programme_Progress==6)
{
;
}
else if(Programme_Progress==7)
{
;
}
else
{
Programme_Reset();
}
while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_12)==0); //等待按键松开
EXTI_ClearITPendingBit(EXTI_Line12);
}
}
else if (EXTI_GetITStatus(EXTI_Line13) == SET)
{
EXIT_LINE_Timer_delay_ms(10);
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_13)==0) //软件防抖
{
Beep_Times(50,3,EXIT_LINE_MODE);
Reach_Pos_OL(Center_Pwm.level,Center_Pwm.vert);
while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_13)==0); //等待按键松开
//再次按下才退出
EXIT_LINE_Timer_delay_ms(10);
while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_13)==1);
EXIT_LINE_Timer_delay_ms(10);
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_13)==0); //软件防抖
while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_13==1)); //等待按键松开
Beep_Times(50,3,EXIT_LINE_MODE);
EXTI_ClearITPendingBit(EXTI_Line13);
}
}
}
说明:
主函数与中断函数相辅相成,程序整体使用外部中断推进以及实现一些特殊功能(立即复位);
由于主函数内小功能函数都借助while()循环实现,设置循环标志位Stop_Sign和程序阶段标志位Programme_Progress来推进主函数;
按下GPIOB,GPIO_Pin_12的按键触发中断,Programme_Progress++以及相应的Stop_Sign=0,以控制目前运行小功能停止并进入下一阶段;
在程序开始和结束处执行Programme_Reset()函数,重置各标志位实现程序重新运行;
注意:
在中断函数内部涉及到的延时函数统统使用EXIT_LINE_Timer_delay_ms()函数,区别外部使用的Timer_delay_ms()函数,防止调用冲突程序卡死
(2)绿色激光云台
//绿车
int r_x=0,r_y=0;
void Get_RaG_Point_Pos(void)
{
if(USART_RX_INFO[0]==’g’&& USART_RX_INFO[8]==’r’&& USART_RX_INFO[4]==’y’&& USART_RX_INFO[12]==’y’) //检查数据定位是否正确(上位机发送信息为:x123y456)
{
x=(USART_RX_INFO[1]-‘0’)*100+(USART_RX_INFO[2]-‘0’)*10+USART_RX_INFO[3]-‘0’;
y=(USART_RX_INFO[5]-‘0’)*100+(USART_RX_INFO[6]-‘0’)*10+USART_RX_INFO[7]-‘0’;
r_x=(USART_RX_INFO[9]-‘0’)100+(USART_RX_INFO[10]-‘0’)10+USART_RX_INFO[11]-‘0’;
r_y=(USART_RX_INFO[13]-‘0’)100+(USART_RX_INFO[14]-‘0’)10+USART_RX_INFO[15]-‘0’;
}
}
void G_Follow_R(int Reach_Pos_CL_MODE)
{
int Sign(int num);
void Get_RaG_Point_Pos(void);
int near(int r_x,int Target_Y);
int diff_x,diff_y,dis;
while(Reach_Pos_CL_Stop_Sign)
{
if(x==0&&y==0)Reach_Pos_OL(750,750);
Get_RaG_Point_Pos();
dis=near(r_x,r_y);
if(dis<=20)
{
Beep_Times(300,1,NORMAL_MODE);
Led_Times(300,1,NORMAL_MODE);
continue;
}
if(Reach_Pos_CL_MODE==PID_MODE && dis>60) //用pid计算舵机单位数
{
Get_RaG_Point_Pos();
diff_x=Pid_Control(Level_Kp,Level_Ki,Level_Kd,r_x,x,PID_REALIZE);
diff_y=Pid_Control(Vert_Kp,Vert_Ki,Vert_Kd,r_y,y,PID_REALIZE);
}
else if(Reach_Pos_CL_MODE==MINMIZE_MODE) //以舵机最小分辨率为单位
{
Get_RaG_Point_Pos();
diff_x=-0.5Sign(x-r_x);
diff_y=-0.5Sign(y-r_y);
}
else if(Reach_Pos_CL_MODE==PID_MODE && dis<=60) //用pid计算舵机单位数
{
Get_RaG_Point_Pos();
diff_x=-0.4Sign(x-r_x);
diff_y=-0.4Sign(y-r_y);
}
Spinnig_Level(X_DIR*diff_x);
Spinnig_Vert(Y_DIR*diff_y);
Timer_delay_ms(20);
}
}
说明:
上位机数据格式为:g123y123r123y123,实时传输红绿激光点两个坐标;
执行点到点的跟踪即可,在主函数中不断重复即可,即while(1)G_Follow_R(PID_MODE);
2.jetson nano上关键源码分析
文件说明:
mian_10、main_11、mian_12是测试函数,分别测试铅笔线识别效果 、A4纸识别效果 、红绿激光分别识别效果
。设置了滑动条供调参 使用,确定好参数
q_1、q_2、q_3即为三个问题对应的程序,分别实现发送铅笔线顶点和中心坐标后实时传输红色激光点坐标
、发送A4纸顶点坐标后实时传输红色激光点坐标 、实时传输红色和绿色激光点坐标
I.相机参数的调整
string gstreamer_pipeline(int capture_width, int capture_height, int display_width, int display_height, int framerate, int flip_method)
{
return “nvarguscamerasrc exposurecompensation=1 ! video/x-raw(memory:NVMM), width=(int)” + to_string(capture_width) + “, height=(int)” +
to_string(capture_height) + “, format=(string)NV12, framerate=(fraction)” + to_string(framerate) +
“/1 ! nvvidconv flip-method=” + to_string(flip_method) + “ ! video/x-raw, width=(int)” + to_string(display_width) + “, height=(int)” +
to_string(display_height) + “, format=(string)BGRx ! videoconvert ! video/x-raw, format=(string)BGR ! appsink”;
}
这里设置好管道参数,主要调整曝光和饱和度,方便之后线条的检测以及红绿激光的区分
可以参考:NVIDIA Jetson Nano 2GB 系列文章(9):调节 CSI
图像质量
II.关键识别算法
(1)铅笔线识别及顶点的计算
变量解析:
int Find = 0, l_x = 0, l_y = 0, r_x = 0, r_y = 0;
int l[2][2],r[2][2],u[2][2],d[2][2];
int ul[2],ur[2],dl[2],dr[2],ce[2];
Find有效个数标志位,表示找到了几组有效的边上两点;
l_x、l_y、r_x、r_y寻找标志位,为1则分别表示上下左右边未找到有效值的两点值
l[2][2]、r[2][2]、u[2][2]、d[2][2]分别存储上下左右边上两点坐标
ul[2]、ur[2]、dl[2]、dr[2]、ce[2]分别存储最终的顶点和中心点坐标
过程:
——转换成灰度图->阈值划分成二制图->霍夫直线检测得到直线并输出直线上两点坐标
——设计算法过滤筛选重复直线并存储两点坐标
for (size_t i = 0; i < linesPPHT.size(); i++) {
x1 = linesPPHT[i][0], y1 = linesPPHT[i][1], x2 = linesPPHT[i][2], y2 = linesPPHT[i][3];
line(image, Point(x1, y1), Point(x2, y2), Scalar(0), 1, 8);
if (x1 < 150 && x2 < 150 && myabs(x2 - x1) < 3 && !l_x){Find++;l_x = (x2 + x1) / 2;l[0][0]=x1;l[0][1]=y1;l[1][0]=x2;l[1][1]=y2;}
else if (y1 < 150 && y2 < 150 && myabs(y1 - y2) < 3 && !l_y){Find++;l_y = (y1 + y2) / 2;u[0][0]=x1;u[0][1]=y1;u[1][0]=x2;u[1][1]=y2;}
else if (x1 > 570 && x2 > 570 && myabs(x2 - x1) < 3 && !r_x){Find++;r_x = (x2 + x1) / 2;r[0][0]=x1;r[0][1]=y1;r[1][0]=x2;r[1][1]=y2;}
else if (y1 > 570 && y2 > 570 && myabs(y1 - y2) < 3 && !r_y){Find++;r_y = (y1 + y2) / 2;d[0][0]=x1;d[0][1]=y1;d[1][0]=x2;d[1][1]=y2;}
}
linesPPHT是霍夫直线检测函数的输出,linesPPHT.size()表示检测到直线的条数;这里根据直线上两点坐标值大小判断属于四条边的那一条;属于其中一条且之前未存储(标志位为1)(见if语句中的判断)则存储并将找点标志位Find+1;Find==4时即寻找结束
——由于霍夫直线检测算法得到的并非顶点而是直线上两点,设计求两直线交点函数
void crossline(int x1,int y1,int x2,int y2,int x3,int y3,int x4,int y4,int cross[2])
{
cross[0]=(y3x4x2-y4x3x2-y3x4x1+y4x3x1-y1x2x4+y2x1x4+y1x2x3-y2x1x3)/(x4y2-x4y1-x3y2+x3y1-x2y4+x2y3+x1y4-x1y3);
cross[1]=(-y3x4y2+y4x3y2+y3x4y1-y4x3y1+y1x2y4-y1x2y3-y2x1y4+y2x1y3)/(y4x2-y4x1-y3x2+x1y3-y2x4+y2x3+y1x4-y1x3);
}
输入的(x1,y1)~(x4,y4)是两条直线上四点坐标,输出交点坐标并赋值给cross;
crossline(l[0][0],l[0][1],l[1][0],l[1][1],u[0][0],u[0][1],u[1][0],u[1][1],ul);
crossline(r[0][0],r[0][1],r[1][0],r[1][1],u[0][0],u[0][1],u[1][0],u[1][1],ur);
crossline(l[0][0],l[0][1],l[1][0],l[1][1],d[0][0],d[0][1],d[1][0],d[1][1],dl);
crossline(r[0][0],r[0][1],r[1][0],r[1][1],d[0][0],d[0][1],d[1][0],d[1][1],dr);
crossline(ul[0],ul[1],dr[0],dr[1],ur[0],ur[1],dl[0],dl[1],ce);
输入之前得到的坐标计算四个顶点值和中心坐标
——向下位机stm32输出坐标
sprintf(m,”#a%03db%03dc%03dd%03de%03df%03dg%03dh%03d$\n”,ul[0],ul[1],ur[0],ur[1],dr[0],dr[1],dl[0],dl[1]);
uart.sendUart(m);
usleep(50000);
sprintf(m,”#i%03dj%03d$\n”,ce[0],ce[1]);
uart.sendUart(m);
(2)A4纸顶点识别及巡线顶点的计算
过程:
——灰度图->二值化->角点检测得到角点坐标CornerImg
——设计算法过滤筛选得到八个顶点P[8][2](绝缘胶布内外边形成两个矩形)
#define MAX_DIS 20
int Is_Exit(int i, int j)
{
int k = 0;
for (k = 0; k < Find; k++)
{
if (myabs(P[k][0]-i)+ myabs(P[k][1]-j)<MAX_DIS)return 1;
}
return 0;
}
int P[8][2] = { 0 };
int Find = 0;
for (int j = 0; j < CornerImg.rows; j++) {
for (int i = 0; i < CornerImg.cols; i++) {
if (CornerImg.at
if (!Is_Exit(i, j))
{
P[Find][0] = i;
P[Find][1] = j;
Find++;
}
}
}
}
Is_Exit()函数遍历已经视作有效的点,如果与当前坐标(i,j)接近则不存储;找到八个有效点退出
——设计根据八个顶点P[8][2]求得巡线四边形的顶点Vertex[4][2](同一个角的内外顶点的中点)
int Vertex[4][2] = { 0 };
int sign[8] = { 0 };
int i,j,k,dis,min = 1000;
int temp1, temp2;
for (k=0,i = 0; i < 8; i++)
{
if (sign[i])continue;
min = 2000;
for (j = 0; j < 8; j++)
{
if (i == j||sign[j])continue;
dis = myabs(P[i][0] - P[j][0]) + myabs(P[i][1] - P[j][1]);
if (dis< min)
{
min = dis;
temp1 = i;
temp2 = j;
}
}
sign[temp1] = 1;
sign[temp2] = 1;
Vertex[k][0] = (P[temp1][0] + P[temp2][0])/2;
Vertex[k][1] = (P[temp1][1] + P[temp2][1])/2;
k++;
}
这里使用for循环遍历P[8][2]中顶点,将距离最近的两点视为A4纸一个角内外两边的两个顶点,求其中点存储在Vertex[4][2]中
——设计算法使巡线的四个端点按照顺时针传输给下位机,否则巡线顺序错误
int temp;
//先整体按y值大小排序
for(i=0;i<4;i++)
{
for(min=Vertex[i][1],j=k=i;j<4;j++)
{
if(Vertex[j][1]<=min)k=j;
}
temp=Vertex[k][0];
Vertex[k][0]=Vertex[i][0];
Vertex[i][0]=temp;
temp=Vertex[k][1];
Vertex[k][1]=Vertex[i][1];
Vertex[i][1]=temp;
}
//y值中等的两点按x值排序
if(Vertex[1][0]<Vertex[2][0])
{
temp=Vertex[1][0];
Vertex[1][0]=Vertex[2][0];
Vertex[2][0]=temp;
temp=Vertex[2][1];
Vertex[2][1]=Vertex[1][1];
Vertex[1][1]=temp;
}
if(Vertex[0][0]&& Vertex[0][1]&&Vertex[1][0]&&Vertex[1][1]&&Vertex[3][0]&& Vertex[3][1]&&Vertex[2][0]&& Vertex[2][1])
{
sprintf(m,”#k%03dl%03dm%03dn%03do%03dp%03dq%03dr%03d$\n”, Vertex[0][0], Vertex[0][1],Vertex[1][0], Vertex[1][1],Vertex[3][0], Vertex[3][1],Vertex[2][0], Vertex[2][1]);
u.sendUart(m);
}
观察任意矩形顶点坐标规律,要顺时针发送,可将y值最小的作为第一个发送,y值最大的第三个发送,介于中间的两点按x值大小判断,x小的最后发送,大的第二个发送
即先整体按y值大小排序,y值中等的两点按x值排序->排序后按013~2的顺序 发送坐标
(3)区分红绿激光
过程如下:
Point color_recognite(Mat image, Scalar Low, Scalar High)
{
vector<vector
vector
vector
double maxarea = 0;
int maxAreaIdx = 0;
Mat g_grayImage, hsv, g_cannyMat_output;
cvtColor(image, hsv, COLOR_BGR2HSV);
split(hsv, hsvSplit);
equalizeHist(hsvSplit[2], hsvSplit[2]);
merge(hsvSplit, hsv);
inRange(hsv, Low, High, g_grayImage);//二值化识别颜色
//开操作 (去除一些噪点)
Mat element = getStructuringElement(MORPH_RECT, Size(2, 2));
morphologyEx(g_grayImage, g_grayImage, MORPH_OPEN, element);
//闭操作 (连接一些连通域)
morphologyEx(g_grayImage, g_grayImage, MORPH_CLOSE, element);
// Canny(g_grayImage, g_cannyMat_output, 80, 80 * 2, 3);
// 寻找轮廓
findContours(g_grayImage, g_vContours, g_vHierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0));
//假设contours是用findContours函数所得出的边缘点集
RotatedRect box;
Point centre;
if (g_vContours.size() != 0)
{
for (int index = 0; index < g_vContours.size(); index++)
{
double tmparea = fabs(contourArea(g_vContours[index]));
if (tmparea > maxarea)
{
maxarea = tmparea;
maxAreaIdx = index;//记录最大轮廓的索引号
}
}
box = minAreaRect(g_vContours[maxAreaIdx]);
rectangle(image, box.boundingRect(), Scalar(0, 0, 255), 2);
centre = box.center;
}
return centre;
}
关键是调用inRange()函数HSV色彩空间二值化的阈值上下限设置
可以参考:OpenCV学习笔记-
inRange()阈值操作函数怎么用_cv.inrange函数
并设置滑动条调整参数获得经验值
最终评判标准:
激光在绝缘胶布上是能否识别(黑色胶布吸光;通过提高曝光,调参,增大激光功率等可以解决)
红绿激光靠近时能否区分(红绿在HSC空间互斥,更亮的会掩盖另一个;调inRange()参数解决)
二.学习资料分享
<1>学习笔记
OpenCV学习笔记——《基于OpenCV的数字图像处理》_switch_swq的博客-
CSDN博客
图像识别小车(电源部分)——电赛学习笔记(1)_switch_swq的博客-
CSDN博客
图像识别小车(电机部分)——电赛学习笔记(2)_switch_swq的博客-
CSDN博客
图像识别小车(jetson nano部分)——电赛学习笔记(3)_switch_swq的博客-
CSDN博客
图像识别小车(PCB设计)——电赛学习笔记(4)_switch_swq的博客-
CSDN博客
PID控制算法理解_switch_swq的博客-
CSDN博客
<2>其他资料
1.唐老师讲电赛的个人空间-唐老师讲电赛个人主页-
哔哩哔哩视频
2.电赛资料:电赛资料_免费高速下载|百度网盘-分享无限制
(baidu.com)提取码:1234
3.我的“电赛”、“VS Studio”、“cmake”、“opencv”、“makefile”、“linux、操作系统”、“stm32”收藏夹
3.以及上面笔记中所包含信息
三.备赛阶段记录
7.2.2023
- 问题
- Nano板子供电5v4A,需求电流较大
- 实验室现有开关电源模块似乎调不了5v,需自己设计电源模块
- 作为底层主要负责,如何为项目打下坚实基础
- 硬件使用有明确目标,如何学习
- 解决
- 先不考虑电源模块,用适配器及直流电源供电
- 先把电机控制写好,提供友善接口
- 先用简单硬件过度,后期转高级的。如电机先用直流后用伺服。先拼起来,再细化雕琢
- 收获
- 用vscode远程开发jetson,下载remote-ssh插件,ssh jetson@IP地址;快捷键ctrl+o调出要打开页面
7.3.2023
- 问题
- Stm32很多知识忘记,如定时器和A/D、D/A;是否需要复习,因复习耗时且不一定需要stm32
- 目前目标尚不明确
- 解决
- 先将stm32相关知识看完,stm32作为保底
- 先搭一个蓝牙遥控小车!
- 收获
- 修好了学长的小车,看到了PID实现双轮平衡小车的现象
- 学会了MG995型号舵机控制(控制脉冲占空比实现角度控制)
- 搭建了测试平台(OLED屏、蓝牙、i2c通讯)
- 搭建了简单的两轮遥控小车,采用直流电机控制,未加入PWM波调速,实现简单的前进、倒退、转弯。
7.4.2023
- 问题
- 昨天做的小车电源直接冒烟,因为电源采用两节3.7V锂电池供电,buck升压到12V以匹配LM298N,电流过大
- 解决
- 暂时给l298n提供5V电压,驱动能力下降,但系统可以运行。以后电源模块之后重新设计或使用小功率电机
- 收获
- 主要将昨天搭好的小车完善,并加入了测压模块(利用STM32的ADC外设)
- 复习了stm32相关知识(外部中断,定制器TIM设置,定时器比较OC产生PWM波)
- 打开了jetson nano的摄像头,它睁开了眼
7.5.2023
- 问题
- 编码器旋转无响应,电机也不动了
- 烧了一个stm32板子,当时接的自制稳压模块,之前都是好的,不知道什么原因
- 解决
- 重新测试电机是否能正常工作
- 在小车到达前有时间学习MPU6050,相关姿态轨迹传输算法
- 收获
- 在jetson nano上跑了例程及自己上传的几张图片
- 学会了linux的vim的使用
- 复习了stm32相关知识(定时器IC输入捕获模式)
- 学会了超声波测距CS100A模块和红外传感模块以及电机编码器部分
7.6.2023
- 问题
- 如何解决电机编码器输出波形峰值小,stm32无法接收
- Pwm波和电源需供电,不然波形失真
- 解决
- 昨天电机不转是因为接线不紧,编码器不行是因为输出电压太小,只有0.5v左右
- 考虑IO口输出模式,不行加电压比较器,ref=0.33V
- 要重新系统性设计电源了,所有信号共地!
- 收获
- 拼好了大车,发现了诸多问题,舵机控制程序完成
- 浅浅学了PCB绘制流程
7.7.2023
- 问题
- 控制函数太过简陋,后续仍需不断升级
- 图像识别进度为零
- 解决
- 《基于opencv的数字图像处理技术》
- 收获
- 用洞洞板搭建好了电源系统(12V-5V-3.7V),系统完全移植到大车
- 新车编码器输出足够大,无需放大器,编码器计数正常
7.8.2023
- 收获
- 学会在Windows配置OpenCV环境,掌握OpenCV图像视频基本操作以及一些基础知识
- PCB绘制进展
7.9.2023
- 收获
- 学会在linux中运行调用OpenCV的c++文件(cmake的使用)
- 学习OpenCV基本数据结构和类的使用
- 进一步了解VS studio上编译选项配置以及debug和release的区别
7.10.2023
- 收获
- 学会OpenCV灰度变换、直方图、边缘检测、霍夫检测直线和圆
7.11.2023
- 问题
- 源码在linux上无法运行(OpenCV调用摄像头出问题,采用CMake方法编译)
- 解决
- 今天下午加晚上未解决
- 收获
- 学会阈值分割(图像二值化方法)
7.12.2023
- 问题
- C++无法编译成功,Mat类未定义引用(QT上编译)
- 解决
- 使用python编写运行成功
- 收获
- 看完特征提取和目标检测(HOG特征+SVM基本流程;LBP特征+级联分类器)
7.13.2023
- 问题
- 依旧无法运行以C++运行OpenCV代码
- 可以运行的OpenCV代码不能直接以videocapture capture(0)的方法获取视频流
- 解决
- 重新系统性安装OpenCV库并重走CMake流程
- 将视频流通过管道gstreamer传输
- 收获
- 学会CMake以及基本编译链接流程
- 重新安装配置OpenCV4.8.0,成功在jetson nano上运行OpenCV代码
7.14.2023
- 问题
- 但运行自己编写的直线检测程序过于卡顿,一秒一帧
- 蓝牙模块无法正常工作
- 解决
- 霍夫直线检测运算量大,不使用该算法
- 调整视频大小及帧率
- 经检测应是蓝牙模块问题,重新购买
- 收获
- 使用画好的pcb搭建小车,将全部器件搭载在小车上
7.15.2023
- 问题
- 电机控制出错,一边电机不受控制
- 目前控制算法学的太少,但图像识别进展不够
- 解决
- GPIO口选到了下载口JTDI/O,换GPIO口控制
- 先用超声波模块、MPU6050、红外传感等模块写避障、路径记录、寻迹等功能
- 收获
- 学会jetson nano上的GPIO使用(基本和树莓派一样)
- 解决了电机的基本控制问题并将电机的四控制线改成了两根
- jetson使用电池供电(器件全供地);实现stm32与jetson nano的usart通信(照搬蓝牙)
7.16.2023
- 问题
- 超声波测距模块中断代码写的不好,拔下模块进入while循环等待,系统卡住
- 拉肚子
- 解决
- 使用static变量,进入中断模式改为EXTI_Trigger_Rising_Falling…
- 休息一天(今日中午至明天中午)
7.17.2023
- 问题
- 欲添加mpu6050模块,但其与oled、蓝牙、超声波模块冲突(非引脚分配问题)
- 解决
- 更改方案,debug试试。仍不行
7.18.2023
- 问题
- 昨天问题仍然存在
- 多个中断之间不协调,影响超声波测距精度。以及测角度过于耗时
- 解决
- 使用江科大自动化的例程代码,简洁明了,解决冲突
- 更改各个中断优先级,控制mpu6050的使用
- 收获
7.19.2023
- 收获
- 学会PID算法
- 重新绘制PCB,解决若干问题
7.20.2023
- 问题
- SysTick定时器冲突问题(外部和中断同时调用delay_us函数会卡死)
- 解决
- 避免了0.1s定时器中断(数据刷新)的SysTick定时函数
- 收获
- 使用编码器利用PID编写行驶给定长度函数及测速
7.21.2023
- 问题
- 后退时编码器反向计数,上限不明确,速度测算出现问题
- 解决
- 通过TIM_EncoderInterfaceConfig设置编码器反转依旧向上计数
- 收获
- 编写小车倒车定长距离
7.22.2023
- 收获
- 编写小车以恒定速度行驶和拐弯90度算法
7.23.2023
- 收获
- 焊好新到的板子
7.24.2023
- 问题
- Jetson配置难,yolo难跑通
- 要求设计完整程序,在jetson开机时自动执行
- 解决
- 学习OpenCV备用
- 学习python或c++可执行文件Linux开机自动执行方法
7.25.2023
- 问题
- 采用硬盘直接克隆方式克隆SD卡依旧无法启动jetson nano系统
- 解决
- 烧录官方镜像文件,成功还原系统。并发现python和C++环境已经配好,之前不会用。解决yolov5摄像头实时检测问题,方案参考亚博论坛。C++也是,g++编译时加上一个参数就行
7.26.2023
- 收获
- 看今年电赛器件清单,简单编写完云台代码,购买K210等器材
7.27.2023
- 收获
- 简单编写完红外寻迹功能
- 解决nano开机启动python文件
7.28.2023
- 问题
- 分析电赛清单,云台摄像头加激光笔应该涉及到动态物体追踪
- 解决
- 学习视频目标跟踪
7.29.2023
- 问题
- Stm32定时器资源有限无法满足云台的加入
- 解决
- 使用pca9685驱动
- 收获
- 采用stm32管脚重定义解决pca9685驱动的使用问题
7.30.2023
- 问题
- Jetson nano的C++库不包含串口uart相关内容
- 解决
- 使用其设备/dev/tthTSH1,研究网上代码
- 收获
- 编写以及pca9685控制云台函数
7.31.2023
- 问题
- 霍夫圆检测一定也不稳定
- 解决
- 调整参数或使用深度学习识别物体的方法
- 收获
- 终于解决串口通信问题,实现C++语言的nano和电脑以及stm32通信
- 完善霍夫圆检测代码,加入uart传输圆心坐标
- 学会nano开机自启动程序方法
- 编写stm32的PID点跟踪函数,实现点跟踪
8.1.2023
- 问题
- 走定长不精确,大约是设定5cm行驶6cm这个比例
- 收获
- 编写摇杆控制云台程序
- 发现之前使用的pid算法全犯了低级错误,本应用float定义PID值结果用了int,修改后大范围应用,各个控制加入PID平稳精确了很多
四.电赛总结及经验教训
<1>本次比赛作品的不足、改进之处
1.stm32和jetson
nano通讯不稳定(可能原因:杜邦线传输能力差、波特率可能设置高了【但低了影响系统处理速度】)(实际原因:while写成了if,导致时机很难对上,通信规则设计失误!)
2.stm32主函数设计不行,没花时间改进,想要重复运行某个程序只能重启,人机交互也不友好
3.比赛报告没有在头脑风暴之后就开始写,导致后期书写太急,不够规范
4.linux操作不熟,开机自启动程序出现问题,且jetson nano上的程序设计缺乏系统性结构性(每问都写了一个程序,而不是整合成一个大的测试程序)
5.所有任务完成太晚,没有留下时间仔细调试调参找问题。而且全流程过一遍后立马就要封箱了,急急忙忙乱改代码导致出现了意想不到的错误!再给一天就刚好了啊!!!
<2>本次比赛的经验教训
1.器件准备很重要:比赛发布器件清单后要备齐,最好每个器件都多买几个。以满足比赛器件需求并防止比赛时器件损坏!(本次比赛oled屏、舵机都反复坏过)
2.器件精度很重要:比赛前统计自己所有器件清单,并实测是否可以使用?精度如何?硬件精度不足会直接导致结果无法满足!(本次比赛刚开始使用的舵机为20kg大扭矩低精度,调了一晚PID参数舵机仍然运动不准,最后才发现是精度问题)
本文转自 https://blog.csdn.net/qq_32971095/article/details/132150694,如有侵权,请联系删除。