提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
本文小车能稳定实现2021电赛F题基础题目1、2,将在后文分享全流程制作,以下是电赛的基本任务要求和赛道。
题目要求简介:
基础任务就是一辆小车识别数字后前往不同房间,发挥部分就是两辆小车协同前往病房。
文章设计小车运行视频如下
一、整体方案决策
1.题目分析
根据任务文档,我们分析作品所需以下多项基本功能:
1、数字识别功能
2、循迹功能
3、十字识别、T字识别
4、药物检测功能
5、红、黄、绿LED
6、远程通讯功能
2.设计方案
基础功能方案:
采用OPENMV实现数字识别与循迹功能,设计支架(3D打印或DIY)悬于车头正上方;
使用四驱小车配TB6612驱动作为执行机构;
安装于底盘两侧的灰度传感器进行十字或T字识别;
使用行程开关配合容器(3D打印或DIY)实现压力检测药物;
三色LED焊接在PCB上;
通过OLED显示各阶段状态,便于调试BUG;
扩展功能:MPU6050、串口3蓝牙
二、代码方案
1.OPENMV端
OPENMV端程序流程图
##功能:通过串口输入切换模式1:数字识别、模式2:巡线模式
##数字识别:识别到数字后返回数字
##巡线模式:二值化巡线返回斜率和截距
THRESHOLD = (36, 61, 17, 82, 12, 67) # Grayscale threshold for dark things...
import sensor, image, time
from pyb import UART
from image import SEARCH_EX, SEARCH_DS
from pyb import LED
mode = 1
tta0 = 0
tta1 = 0
num = 0
min_degree = 0
max_degree = 179
enable_lens_corr = False # turn on for straighter lines...打开以获得更直的线条…
template1 = image.Image("/1.pgm")
template2 = image.Image("/2.pgm")
template3 = image.Image("/3.pgm")
template4 = image.Image("/4.pgm")
LED(1).on()
LED(2).on()
LED(3).on()
clock = time.clock() # to process a frame sometimes.
uart = UART(3, 115200)#P4、5 TX、RX
while(True):
if uart.any():
mode = int(uart.read())
if mode == 1:
sensor.reset()
sensor.set_contrast(1)
sensor.set_gainceiling(16)
sensor.set_framesize(sensor.QQVGA) #160x120
sensor.set_pixformat(sensor.GRAYSCALE)
while(mode == 1):
if uart.any():
mode = int(uart.read())
clock.tick()
img = sensor.snapshot()
r1 = img.find_template(template1, 0.70, step=4, search=SEARCH_EX) #, roi=(10, 0, 60, 60))
if r1:
img.draw_rectangle(r1)
print(r1)
print("(X1:"+str(r1[0])+")"+'\r\n')
uart.write("(X1:"+str(r1[0])+")"+'\r\n')
r2 = img.find_template(template2, 0.70, step=4, search=SEARCH_EX) #, roi=(10, 0, 60, 60))
if r2:
img.draw_rectangle(r2)
print(2)
print("(X2:"+str(r2[0])+")"+'\r\n')
uart.write("(X2:"+str(r2[0])+")"+'\r\n')
r3 = img.find_template(template3, 0.70, step=4, search=SEARCH_EX) #, roi=(10, 0, 60, 60))
if r3:
img.draw_rectangle(r3)
print(3)
print("(X3:"+str(r3[0])+")"+'\r\n')
uart.write("(X3:"+str(r3[0])+")"+'\r\n')
r4 = img.find_template(template4, 0.70, step=4, search=SEARCH_EX) #, roi=(10, 0, 60, 60))
if r4:
img.draw_rectangle(r4)
print(4)
print("(X4:"+str(r4[0])+")"+'\r\n')
uart.write("(X4:"+str(r4[0])+")"+'\r\n')
print("FPS:"+str(clock.fps()))
################################################################################################
if mode == 2:
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QQQVGA) # 80x60 (4,800 pixels) - O(N^2) max = 2,3040,000. #80x60 w,h
sensor.skip_frames(time = 2000) # WARNING: If you use QQVGA it may take seconds
while(mode == 2):
if uart.any():
mode = int(uart.read())
clock.tick()
#########################识别直线的截距、斜率##################
#
img = sensor.snapshot()
img_1 = img.binary([THRESHOLD])#拍摄图像,将其转变为二进制储存在img变量
line = img_1.get_regression([(100,100)], robust = True)
if (line):
rho_err = abs(line.rho())-img_1.width()/2 #截距偏移
##line.rho()线在最中心时,是±40 在最两边时为0,右边为负,左边为正
##所以rho_err只要偏离中心就都为负数
if line.theta()>90:
theta_err = line.theta()-180 #角度偏移
else:
theta_err = line.theta()
img_1.draw_line(line.line(), color = 127)#画标志线
if line.magnitude()>8:
output_str="[%d,%d]" % (rho_err,theta_err) #方式1
print("[rho_err,theta_err]")
print(output_str)
uart.write(output_str+'\r\n')
OPENMV进行红线循迹与数字识别功能,这两个功能通过stm32单片机发送模式切换指令控制
红线巡线状态下,OPENMV通过串口发送数据给stm32,数据格式为[XXX,XXX]对应[rho,theta]。
数字识别状态下,openmv通过sd卡内提前储存的数字模板与当前图像中的数字进行对比,识别率高则发送识别数据给stm32单片机,以数字1格式为例(X1:XXX),数字2则是(X2:XXX)其中XXX为数字在图像中的X坐标。
XXX由代码中r数组提取出来,r数组为模板识别成功后,得出的相应数据组成的数组,r[0]为其中的第0位数据,代表在摄像头视野图像中的此匹配模板矩形框的左上角X坐标。
由于代码实现部分是通过星瞳科技例程修改而来,非原创部分不做过多解释,需自行学习例程,以下为例程链接。
巡线小车例程
多模板匹配数字识别例程
2.STM32端
32程序整体流程图如下图所示
整体程序分为初始化代码、发车处理代码、题目1、2操作代码。
硬件初始化就为常规处理,因此后文不做过多解释。
1.发车处理代码
以下为发车处理代码的程序流程图
发车处理函数如下
void SetOut_prepare(void)
{
while(XJ_flag==0)
{
OLED_chTS(0);//识别数字
usart1_TX(1);//发送切换循迹指令
SB_flag=1;//识别模式标志位
while(MB_num==0)MB_num=SZ_num;//等待识别出数字,将识别数字赋予MB_num变量
SZ_num=0;//清零
WZ_X[MB_num]=0;//清零
OLED_chTS(1);//数字
OLED_chTS(2);//等待放入药物
SB_flag=0;//关闭数字识别
usart1_TX(2);//发送循迹指令
while(ZH_flag)
{
OLED_ShowString(0,16*3,"Trace after 2s",16);OLED_Refresh();
if(YW_KEY==0)
{
delay_ms(1000);
delay_ms(1000);
if(YW_KEY==0)ZH_flag=0; //等待装货检测
}
}
XJ_flag=1;//打开循迹标志位
OLED_chTS(3);//循迹中
}
}
发车处理函数中涉及到以下多项功能函数与标志位
OLED_chTS(u8 num)//OLED中文提示,按num变量不同,显示设定好的语句
usart1_TX(u8 mode)//串口1发送函数,用于OPENMV切换模式,根据mode变量不同,发送不同指令给OPENMV
OLED_ShowString(u8 x,u8 y,u8 *chr,u8 size1)//OLED显示字符串函数,*chr参数为字符串
OLED_Refresh()//刷新OLED数据,使写入数据显示在OLED上
SB_flag代表识别模式开启,使定时器中断函数内的数字识别相关函数运行
SZ_num变量代表摄像头当前识别到的数字,根据定时器中断函数内的串口接收函数所得,后续由相关讲解
WZ_X[MB_num]后续在串口接收函数做相关讲解,此处仅做清零处理,避免使后续数据污染
ZH_flag为装货标志位,初始为1,使函数进行等待装货循环,当连续两秒检测到货物则清零跳出循环
XJ_flag循迹标志位,用于开启定时器中断函数内与循迹运行的相关函数
void OLED_chTS(u8 num)//中文提示
{
u8 i;
if(num==0)for(i=0;i<4;i++)OLED_ShowChinese(i*16,0,i,16);//识别数字
else if(num==1)OLED_ShowNum(16*4,0,MB_num,1,16);//开头目标数字号
else if(num==2)for(i=0;i<6;i++)OLED_ShowChinese(i*16,16,4+i,16);//等待放入药物
else if(num==3){OLED_Clear();for(i=0;i<3;i++)OLED_ShowChinese(i*16,0,10+i,16);}//循迹中
else if(num==4){OLED_Clear();for(i=0;i<3;i++)OLED_ShowChinese(i*16,0,13+i,16);}//转弯中
else if(num==5){OLED_Clear();for(i=0;i<5;i++)OLED_ShowChinese(i*16,0,16+i,16);}//请拿开药物
else if(num==6){OLED_Clear();for(i=0;i<3;i++)OLED_ShowChinese(i*16,0,21+i,16);}//调头中
else if(num==7){OLED_Clear();for(i=0;i<5;i++)OLED_ShowChinese(i*16,0,24+i,16);}//抵达目的地
else if(num==8)OLED_ShowNum(16*4,0,SZ_num,1,16);//路口目标数字号
else if(num==9)OLED_ShowNum(16*4,16,WZ_X[SZ_num],3,16);//路口目标数字号的X坐标
OLED_Refresh();
}
根据相关注释可看出,对应num参数所代表的中文语句或数字
OLED_ShowChinese显示中文
OLED_ShowNum显示数字
void usart1_TX(u8 mode)//发送切换模式指令
{
USART_PRINTF_FLAG=1;
if(mode==1)printf("1");
else if(mode==2)printf("2");
}
USART_PRINTF_FLAG确定从哪个串口printf函数发出数据,此处为串口1发出
void TIM3_IRQHandler(void) //TIM3中断
{
if(TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) //检查指定的TIM中断发生与否:TIM 中断源
{
TIM_ClearITPendingBit(TIM3, TIM_IT_Update); //清除TIMx的中断待处理位:TIM 中断源
if(CS_flag==0)//
{
if(count_flag==1)JS_num1++;
else if(SBWZ_flag==0)JS_num2--;
if(XJ_flag==1)
{
usart1_RX();//检测循迹数据
output=rho_PID_calc(&rho_pid,rho,8)+theta_PID_calc(&theta_pid,theta,0);//处理循迹数据
if(STOP_flag>20)STOP_flag=20;//连续十次都没有接受到
if(STOP_flag!=20)motor_XZ(output);//电机循迹
else motor_ZFZ(0,0);//停车
}
else if(SB_flag==1)usart1_RX();//检测识别数据
}
else {usart1_RX();usart3_RX(); }
}
}
在定时器中断函数中有几种分支处理,分为测试处理,非测试处理包含(循迹处理,数字识别处理,计时处理,倒计时处理)
测试处理用于组装小车后检测小车硬件状态使用,仅运行usart1_RX();usart3_RX();进行串口接收数据测试
非测试处理则为正常运行情况:
1、通过count_flag、SBWZ_flag这两个标志位处理题目2中测速与控制识别位置的相关数据
2、通过XJ_flag、SB_flag决定摄像头工作模式下的数据处理,数字识别状态下只需要运行usart1_RX()即可,但循迹状态下则还要将接收数据进行PID处理,并通过STOP_flag判断小车是否循迹出界,当STOP_flag满足相关条件则强制停车,STOP_flag的大小由usart1_RX()得出。在未满足出界情况,小车将执行motor_XZ(output)函数,进行循迹前进。
其中的rho_PID_calc()、theta_PID_calc()PID代码出自以下链接,有需要同学请自行了解
从0编写一份PID控制代码
//处理从OPENMV接收到数据
//OPENMV发来的数据是[xxx,xxx]数据
//将括号内第一个数据处理到rho,第二个数据处理到thera
void usart1_RX(void)//接收函数
{
u8 t=0;
u8 flag=0;//位置标志位
u8 z_flag=0;//中间标志位
u8 w_flag=0;//']'结尾标志位
u8 w_flag2=0;//')'结尾标志位
if(USART1_RX_STA&0x8000)
{
rho=0;
theta=0;
/************将接收的数据转化成stm32能使用的数据******************/
if(USART1_RX_BUF[0]==0x5B) //判断是否是"["
{
while(USART1_RX_BUF[flag]!=0x2C)flag++;//判断是否是","
z_flag=flag;
while(USART1_RX_BUF[flag]!=0x5D)flag++;//判断是否是]
w_flag=flag;
for(t=z_flag-1;t>0;t--)
{
if(USART1_RX_BUF[t]!='-')
rho+=(USART1_RX_BUF[t]-'0')*pow(10,z_flag-1 - t);
else
rho=-1*rho;
}
for(t=w_flag-1;t>z_flag;t--)
{
if(USART1_RX_BUF[t]!='-')
theta+=(USART1_RX_BUF[t]-'0')*pow(10,w_flag-1 - t);//运算10的次方
else
theta=-1*theta;
}
}
else if(USART1_RX_BUF[0]==0x28) //判断是否是"("
{
WZ_X[0]=0;
while(USART1_RX_BUF[flag]!=0x29)flag++;//判断是否是")"
w_flag2=flag;
for(t=w_flag2-1;t>3;t--)
{
if(USART1_RX_BUF[t]!='-')
WZ_X[0]+=(USART1_RX_BUF[t]-'0')*pow(10,w_flag2-1 - t);//运算10的次方
else
WZ_X[0]=-1*WZ_X[0];
}
SZ_num=USART1_RX_BUF[2]-'0';
WZ_X[SZ_num]=WZ_X[0];
if((SZ_num==MB_num)&(WZ_X[SZ_num]<80))ZX_flag=1;
else if((SZ_num==MB_num)&(WZ_X[SZ_num]>80))ZX_flag=2;
else if((SZ_num!=MB_num)&(WZ_X[SZ_num]<80))ZX_flag=2;
else if((SZ_num!=MB_num)&(WZ_X[SZ_num]>80))ZX_flag=1;
}
USART1_RX_STA=0;
z_flag=0;
w_flag=0;
flag=0;
STOP_flag=0;//停止传输标志位
}
else STOP_flag++;
}
此部分串口接收处理,需要结合串口接收例程理解,在此只解释原创部分
通过数据格式判断当前接收数据为循迹数据或数字识别数据
循迹数据:根据定义的数据格式,得出各节点在数据中的位置并赋予z_flag、w_flag,得出数据rho、theta的长度,再通过for循环将其中数据进行相加,得出数据rho、theta的大小、正负。
识别数据:根据定义的数据格式,得出末节点在数据中的位置并赋予w_flag,得出识别数字的X坐标,并赋予WZ_X[0],在通过SZ_num与MB_num对比,以及WZ_X[SZ_num]的大小得出ZX_flag的数值(用于题目2决定左转或右转)
此处用意为:当前识别数字与目标数字是否吻合并判断识别X坐标的左右偏向,得出小车转向选择。一共有四种结果。识别数字与目标数字匹配,识别X坐标在左半边,小车左转,X坐标在右,小车右转;数字不匹配,则相对反之。
由于小车循迹过程中,会一直反馈数据,但当小车超界后无法反馈循迹数据,STOP_flag++,连续20次无接收,则通过中断函数判断小车超界并执行motor_ZFZ(0,0)函数进行停车。
2.题目1处理代码
题目1程序流程图
void TM1(void)
{
//根据题目1病房号左转或右转
delay_ms(1000);
delay_ms(1000);
while((GRAY3==0)||(GRAY4==0));//当这两个都检测到,意味着识别到十字路口才执行转弯
XJ_flag=0;//循迹标志位清零
motor_ZFZ(0,0);
delay_ms(1000);
OLED_chTS(4);//转弯中
if(MB_num==1)ZW90_cmd(1);//转弯处理
else ZW90_cmd(2);
delay_ms(1000);
XJ_flag=1;//循迹标志位置1
while(STOP_flag!=20);//只有持续识别不到直线才到终点
XJ_flag=0;
LEDR=1;//卸货指示灯
OLED_chTS(5);//请拿走货物
while(XH_flag)//等待卸货
{
if(YW_KEY==1)
{
delay_ms(1000);
delay_ms(1000);
if(YW_KEY==1)XH_flag=0;
}
}
LEDR=0;
OLED_chTS(6);//调头中
if(MB_num==1)ZW180_cmd(2);
else ZW180_cmd(1);
delay_ms(1000);
OLED_chTS(3);//循迹中
XJ_flag=1;
while((GRAY3==0)||(GRAY4==0));//当这两个都检测到,意味着识别到十字路口才执行转弯
XJ_flag=0;
motor_ZFZ(0,0);
delay_ms(1000);
OLED_chTS(4);//转弯中
if(MB_num==1)F_ZW90_cmd(2);
else F_ZW90_cmd(1);
motor_ZFZ(0,0);
delay_ms(1000);
XJ_flag=1;
while(STOP_flag!=20);//只有持续识别不到直线才到终点
XJ_flag=0;
motor_ZFZ(0,0);
LEDG=1;
OLED_chTS(7);//抵达目的地
while(1);
1、GRAY3、GRAY4为灰度传感器IO口,当传感器没有被触发时为0,感应到红线,即十字路口时为1,跳出while循环。
2、当感应到十字路口后小车将停车一秒后执行转弯操作,ZW90_cmd()为转弯函数,根据题目1的识别数字判断左右转。ZW90_cmd()包含motor_R()、motor_L(),内部参数决定转动时间≈转动角度,此处需要在调试阶段进行微调,因为小车电机动力直连电池,因此在不同电压下,电机转动同等时间,距离角度会不同**(根据实际调试情况,左右转时间可能不一致)**
3、当小车转弯后继续巡线,默认巡线出界时为终点,停车等待取货,取货步骤与前面放入类似,只是标志位改为XH_flag,然后执行掉头程序ZW180_cmd(),与ZW90_cmd()类似,只是时间更长、角度更大。剩下内容与开头类似,F_ZW90_cmd()为返程时的转弯函数,可一一对应理解。
//电机相关函数
void ZW90_cmd(u8 set)
{
if(set==2)motor_R(5);
else if(set==1)motor_L(5);
}
//小车左转程序time为左转时间
void motor_L(u8 time)
{
u8 i;
motor_ZFZ(1,2);
motor_ZFZ(2,2);
motor_ZFZ(3,1);
motor_ZFZ(4,1);
TIM_SetCompare1(TIM2,4000);
TIM_SetCompare2(TIM2,4000);
TIM_SetCompare3(TIM2,4000);
TIM_SetCompare4(TIM2,4000);
for(i=time;i>0;i--)delay_ms(100);
motor_ZFZ(0,0);
}
//小车左转程序time为右转时间
void motor_R(u8 time)
{
u8 i;
motor_ZFZ(1,1);
motor_ZFZ(2,1);
motor_ZFZ(3,2);
motor_ZFZ(4,2);
TIM_SetCompare1(TIM2,4000);
TIM_SetCompare2(TIM2,4000);
TIM_SetCompare3(TIM2,4000);
TIM_SetCompare4(TIM2,4000);
for(i=time;i>0;i--)delay_ms(100);
motor_ZFZ(0,0);
}
//测试电机正反转驱动
void motor_ZFZ(u8 id,u8 cmd)
{
if(id==1)
{
if(cmd==0){A1=0;A2=0;}
else if(cmd==1){A1=1;A2=0;} //
else if(cmd==2){A1=0;A2=1;} //
}
else if(id==2)
{
if(cmd==0){B1=0;B2=0;}
else if(cmd==1){B1=1;B2=0;} //
else if(cmd==2){B1=0;B2=1;} //
}
else if(id==3)
{
if(cmd==0){C1=0;C2=0;}
else if(cmd==1){C1=1;C2=0;} //
else if(cmd==2){C1=0;C2=1;} //
}
else if(id==4)
{
if(cmd==0){D1=0;D2=0;}
else if(cmd==1){D1=1;D2=0;} //
else if(cmd==2){D1=0;D2=1;} //
}
else if(id==0)
{
if(cmd==0){A1=0;A2=0;B1=0;B2=0;C1=0;C2=0;D1=0;D2=0;}
else if(cmd==1){A1=1;A2=0;B1=1;B2=0;C1=1;C2=0;D1=1;D2=0;} //轮子正转测试
else if(cmd==2){A1=0;A2=1;B1=0;B2=1;C1=0;C2=1;D1=0;D2=1;} //轮子反转测试
}
}
3.题目2处理代码
void TM2(void)
{
count_flag=1;//开始计算时间
//根据题目1病房号左转或右转
delay_ms(1000);
delay_ms(1000);
while(GRAY3==0||GRAY4==0);// 初次检测到十字,用于计算小车速度
count_flag=0;//停止计时
V=10*S1/JS_num1;
JS_num2=10*S2/V;// 根据以上公式得出小车抵达识别位置所需时间
SBWZ_flag=0;//等待计时器把数据归零抵达位置
while(SBWZ_flag==0)if(JS_num2==0)SBWZ_flag=1;//等待抵达识别位置
XJ_flag=0;//停止循迹
motor_ZFZ(0,0);//停车
OLED_Clear();
OLED_chTS(0);//识别数字
//开始识别
ZX_flag=0;//清零一次
printf("1");
SB_flag=1;//识别标志位打开
while(ZX_flag==0);// 识别到目标数值并判断方向后,ZX_flag不等于0,跳出循环卡点
SB_flag=0;
OLED_chTS(8);//数字号
OLED_chTS(9);//数字号
printf("2");
// 等待3秒,用于观察OLED现象
delay_ms(1000);
delay_ms(1000);
delay_ms(1000);
XJ_flag=1;//开始循迹
while((GRAY3==0)||(GRAY4==0));//当这两个都检测到,意味着识别到十字路口才执行转弯
XJ_flag=0;
motor_ZFZ(0,0);
delay_ms(1000);
OLED_chTS(4);//转弯中
if(ZX_flag==1)ZW90_cmd(1);//ZX_flag=1 左转 ZX_flag=2 右转
else ZW90_cmd(2);
delay_ms(1000);
XJ_flag=1; //转弯后循迹
while(STOP_flag!=20);//只有持续识别不到直线才到终点
XJ_flag=0;
LEDR=1;//卸货指示灯
OLED_chTS(5);//请拿走货物
while(XH_flag)//等待卸货
{
if(YW_KEY==1)
{
delay_ms(1000);
delay_ms(1000);
if(YW_KEY==1)XH_flag=0;
}
}
LEDR=0;
OLED_chTS(6);//调头中
if(ZX_flag==1)ZW180_cmd(2);
else ZW180_cmd(1);
delay_ms(1000);
OLED_chTS(3);//循迹中
XJ_flag=1;
while((GRAY3==0)||(GRAY4==0));//当这两个都检测到,意味着识别到十字路口才执行转弯
XJ_flag=0;
motor_ZFZ(0,0);
delay_ms(1000);
OLED_chTS(4);//转弯中
if(ZX_flag==1)F_ZW90_cmd(2);
else F_ZW90_cmd(1);
motor_ZFZ(0,0);
delay_ms(1000);
XJ_flag=1;
while(STOP_flag!=20);//只有持续识别不到直线才到终点
XJ_flag=0;
motor_ZFZ(0,0);
LEDG=1;
OLED_chTS(7);//抵达目的地
while(1);
}
题目2的操作与题目1类似,区别在于开头通过JS_num1计时计算出车速V,得出从第一十字到抵达识别位置所需时间,等待小车行走JS_num2到达识别位置,开启摄像头数字识别,从而得出ZX_flag的数值,判断左右转操作,可往前观察usart1_RX()函数内处理。
三、硬件方案
1.原理图与PCB
稳压模块使用了两款AMS1117模块,分别是3.3V、5V的,电源使用11.1V的航模电池,XT60接口。
其他模块对应PCB板子上对应插上即可
2.材料清单
以下是材料清单不完全图片,材料成本约为567.2,清单文件在文末会给出。
总结
1、按照资料制作的小车,只需调试ZW90_cmd、ZW180_cmd、F_ZW90_cmd内,对应的motor_R()、motor_L()中的参数即可实现效果
2、小车预留了MPU6050陀螺仪接口、HC-05蓝牙模块接口,可用于后续扩展功能。
链接:https://pan.baidu.com/s/1NFYKn5YgpUaa1BN3sJAEkQ
提取码:wp0l
–来自百度网盘超级会员V4的分享
欢迎在评论区讨论与咨询问题