【2022年电赛】有人开摆,有人跑路,有人5秒不识数

前言:该作品是2022年四川省电子设计竞赛一等奖作品,其能稳定完成全部四个问题,但存在停车距离的精度问题。该文章将会介绍该作品的整体设计思路,关键控制算法等技术相关问题,也会给出工程的下载链接。同时本人参加过2021年电赛、十七届智能车竞赛、2022年电赛,文末会给出一些我个人的经验。当然各位小伙伴也可以直接私信我(哔站同名),我有时间一定会回复。

目录

一、整体思路设计

1.1主、从车设计框架

1.2整个作品设计思路

二、各功能块具体实现

2.1模块间通信

2.2小车的程序结构

2.3速度闭环操作

2.4openMV场地元素识别

2.5 巡线实现

三、任务实现思路

四、个人经验

一、整体思路设计

1.1主、从车设计框架

        话不多说,直接上图。图中圆角方框表示模块,箭头表示信号方向(包括供电),各颜色箭头对应含义见图中,图中均为单箭头,即信号流向都是单向的。蜂鸣器与led灯图中未体现。

 图1 主车设计框图

        图1所示为主车的设计框图,采用msp430f5529作为主控制器,openMV用于场地元素识别,其将图像处理后的数据打包发送给主控制器,主控制器会处理接收到的openMV数据,然后改变PWM占空比输出给tb6612电机驱动模块,驱动模块将控制信号功率放大驱动电机旋转,同时主控制器通过蓝牙向从车发送相关信号。电池采用3S\1100mAh电池,采用开关电源模块提供稳定的5V电源。

图2 从车设计框图

        图2所示为从车的设计框图,与主车类似,不同在于从车是被动从蓝牙模块接收主车发送的数据,并且增加了激光测距模块以实现两车间的距离要求。

        注意:该框图中没有考虑模块间的通信电压,一是因为很多模块自带稳压芯片,二是因为各模块间工作电压存在差异,设计时一定要根据实际情况有所考虑。同时ti的板子特别容易烧,建议在进行电路设计时引脚与模块间添加几k到十几k的电阻(我使用的4.7k电阻)用于限流。

        在机械结构方面,两辆车均为三轮车,采用差速方式控制小车转向。

图3 小车底盘示意图

 

图4 小车实物图

        不要在意车长得潦草,我是做智能车的,根本没准备电赛,这两辆车的元器件基本都是捡的和白嫖别人的。 

1.2整个作品设计思路

        作品整体设计思路很简单,两辆车单独进行巡线,主车仅在特定时刻给从车发送指令,配合两车各自的控制逻辑即可完成所有题目。具体一点,首先对主从两车各自进行速度闭环,这样两车就可以实现以匀速且可准确控制的速度运动,然后通过openMV识别场地元素,并将处理后的数据发送给主控制器,这时两辆车即可完成基本的巡线任务,接下来就是将两车结合起来,即主车在特定时刻给从车发送信息,让它们俩协同巡线,再结合具体控制逻辑即可完成各个题目。

二、各功能块具体实现

        前面已经介绍了作品的硬件设计框架,以及整个作品的设计思路,那么接下来就将实现细节逐步完善,包括模块间的通信、小车的程序结构、速度闭环操作、openMV场地元素识别、巡线实现。内容较多,可在文章顶部通过目录跳转至感兴趣区域。

2.1模块间通信

        为什么要先介绍模块间的通信呢,因为它很重要,可以说模块间通信解决好了可以事半功倍。而本作品很重要的一点在于所有信号都是单向的,openMV只给主控制器发送数据而主控制器不给openMV发送数据,主车只给从车发送数据而从车不给主车发送数据。至于原因,当然是这样简单,有人可能会觉得会出现数据错误,这个问题大可放心,别对通信协议这么不信任。

图5 主要模块间的通信

        参照上图,接下来我就简单介绍各模块间通信干了啥,主要有四个(其实只有两个,后两个单纯用来充数的):

        1.主车与从车之间的通信,采用的是串口通信协议(配对蓝牙串口),主车给从车每次发送一个字节的数据,包括八条指令,1-4表示接下来该执行的任务(四个任务嘛),5表示从车启动,6表示从车停止,7表示从车暂停(第四个任务用),8表示从车暂停后启动(同样第个任务用)。

for example:

    USCI_A_UART_transmitData(USCI_A0_BASE,work);//向从机发送work,work是任务序号
    delay_ms(500);
    USCI_A_UART_transmitData(USCI_A0_BASE,5);//向从机发送开始信号
    delay_ms(100);

        2.openMV与主控制器之间的通信,采用串口通信协议,openMV只给主控制器发送数据,每次五个字节,格式为0x2c 0x12 data1 data2 data3,0x2c与0x12为帧头,用于同步数据传送,data1表示场地元素,包括六种:0正常巡线,1正常停车,2岔路口进,3岔路口出,4非正常停车, 5转弯,data2与data3表示黑线的中心值,为什么是两条呢,正常情况确实只有一条,但小车处于进出三岔时会有两条黑线,此时就需要将两条黑线中心横坐标都发送给主控制器。

openMV发送数据函数:

# 通信协议
def send_data_packet(x1,y1,z1):
    data =struct.pack("<bbbbb",                #格式为五个字符
                   0x2C,                       #帧头1
                   0x12,                       #帧头2
                   int(x1), # up sample by 4   #数据1,道路元素
                   int(y1), # up sample by 4   #数据2,黑线横坐标
                   int(z1),                    #数据3,另一条黑线横坐标,只有一条线时为0
                   )
    uart.write(data)

也附上单片机接收程序:

/************************************openMV数据接收程序*****************************************/
unsigned char openmv_data[3]={0,0,0};
#pragma vector=USCI_A1_VECTOR
__interrupt void USCI_A1_ISR (void)
{
    uint8_t receivedData = 0;//接收原始数据
    static char state=0;//因为要进行帧同步,所以需要记录字节顺序
    switch (__even_in_range(UCA1IV,4))
    {
        case 2:
            receivedData = USCI_A_UART_receiveData(USCI_A1_BASE);//接收字节数据
            if(receivedData==0x2c&&state==0)//判断是不是第一个帧头
                state++;
            else if(receivedData==0x12&&state==1)//判断是不是第二个帧头
                state++;
            else if(state>1)//帧头判断无误就开始接收数据
            {
                openmv_data[state-2]=receivedData;
                if(state==4)//只有三个数据,加上帧头共五个字节,所以state就到4
                    state=0;
                state++;
            }
            break;
        default:
            break;
    }
}

       前面提到应该信任通信协议,其实串口在115200波特率下单字节误码率很低很低,几乎可以忽略,但当我们需要在同一次数据发送多种信息时,就需要用到帧同步,也就是加上两个帧头,为啥是两个?因为一个帧头的同步错误率是1/256(忽略传输过程错误),还是比较可能出错的,两个帧头同步错误率则是1/65536,几乎不可能出错,至于三个及其以上也可以,但没必要。

        3.从车与激光测距模块间的通信,采用的是IIC,这里主控制器仅仅从激光测距模块读出原始数据,解算代码我也不会写,抄的某宝店铺里的。你可能会问,为啥不用超声波模块,为啥不用串口通信,好吧,我太菜了,超声波模块代码不会移植,至于串口,emmm,msp430f5529只有两个串口。。。。

        4.控制器与tb6612之间的通信,PWM,至于PWM是啥,怎么用它控制电机,可以看这篇博客http://t.csdn.cn/rkT8p

        多说几句:对于电赛,或者说对于单片机来说,常用的通信协议就三种串口、IIC、SPI,前者通常都是单片机硬件实现,后两者软硬实现都可以(硬实现指的它有对应实物电路,软实现指的无实物电路,但可以用代码模拟),后两者考虑到灵活性一般软件实现。将这三种搞明白就行了,知道原理,懂得移植代码即可,至于CAN口(大疆M2006使用的通信协议)、RS485啥的,了解了解就行了,用到了再说。

2.2小车的程序结构

 图6 小车程序结构图

        程序结构图如图4所示,该处仅给出主程序结构,未考虑openMV的程序与作用。对于跟随小车系统,在蓝牙连接后可用过按键选择任务,并通过oled显示,确定好任务后主车会通过蓝牙向从车发送选择任务和发车指令,主车发送发车指令后以及从车接收发车指令后都会根据任务选择速度(这里的速度指的速度闭环后的目标速度,后面会详细介绍),接着就会根据选择的任务执行对应的程序,最后在主车识别停车后两车都停止(控制周期为20ms)。

以下是两车的程序框架代码:

主车程序框架:

#include "driverlib.h"
char work=1;//选择第几题
unsigned int diatance=0;
int main(void)
{
/**************************************初始化部分*************************************/
    char txt[30];
    WDT_A_hold(WDT_A_BASE);
    SystemClock_Init();//时钟初始化
    led_init();//led初始化
    key_init();//按键初始化
    OLED_Init();//oled初始化
    OLED_Clear();//清屏
    UART_Init(USCI_A0_BASE,115200);//蓝牙初始化
    UART_Init(USCI_A1_BASE,115200);//openMV初始化
    motor_init();//电机初始化
    encoder_init();//编码器初始化
    Timer_A_Init(1);//定时器中断初始化
    __bis_SR_register(GIE);
/**************************************按键选择题目部分*************************************/
    while(1)//选择题目并显示参数
    {
        if(!GPIO_getInputPinValue(GPIO_PORT_P1, GPIO_PIN1))//按下开始运行
            break;
        if(!GPIO_getInputPinValue(GPIO_PORT_P2, GPIO_PIN1))//按下选择题目
        {
            delay_ms(15);//消抖
            if(!GPIO_getInputPinValue(GPIO_PORT_P2, GPIO_PIN1))
                work++;
        }
        if(work==5)//循环选择
            work=1;
        sprintf(txt,"work:%d    ",(int)work);//显示题目
        OLED_ShowString(0,0, (unsigned char *)txt,8);
        sprintf(txt,"middle:%d    ",(int)openmv_data[0]);//显示openMV中线数据
        OLED_ShowString(0,2, (unsigned char *)txt,8);
    }
/**************************************发送题目与发车指令部分*************************************/
    USCI_A_UART_transmitData(USCI_A0_BASE,work);//向从机发送work
    delay_ms(500);
    USCI_A_UART_transmitData(USCI_A0_BASE,5);//向从机发送开始信号
    delay_ms(100);
/**************************************根据题目选择速度部分*************************************/
    switch(work)//根据题目选择速度选择
    {
    case 1:aim_speed=11.05;break;//题目一
    case 2:aim_speed=18.35;break;//题目二
    case 3:aim_speed=20;break;//题目三
    case 4:aim_speed=18.35*2;break;//题目四
    default:while(1);
    }
    OLED_Clear();
/**************************************根据题目选择执行代码部分*************************************/
    while(1)
    {
        if(timer_20ms)//主控制函数,定时器中断20ms
        {
            switch(work)//题目选择
            {
            case 1:control_1();break;//题目一
            case 2:control_2();break;//题目二
            case 3:control_3();break;//题目三
            case 4:control_4();break;//题目四
            }
            GPIO_toggleOutputOnPin(GPIO_PORT_P4, GPIO_PIN7);//指示灯,程序正常运行可以看到灯闪烁
            timer_20ms=0;//清空标志位
        }
    }
}

从车程序框架:

#include "driverlib.h"
char work=1;//从机工作程序
unsigned int diss=0;//激光测距显示距离
int main(void)
{
/**************************************初始化*************************************/
    char txt[30];
    WDT_A_hold(WDT_A_BASE);
    SystemClock_Init();//时钟初始化
    led_init();//led初始化
    key_init();//按键初始化
    I2C_GPIO_Config();//激光测距
    OLED_Init();//oled初始化
    OLED_Clear();//清屏
    UART_Init(USCI_A0_BASE,115200);//蓝牙初始化
    UART_Init(USCI_A1_BASE,115200);//openMV初始化
    motor_init();//电机初始化
    encoder_init();//编码器初始化
    Timer_A_Init(1);//定时器中断初始化
    __bis_SR_register(GIE);
/**************************************获取题目和发车指令*************************************/
    while(1)
    {
        if(co_data<=4) work=co_data;//获取工作模式
        else if(co_data==5) break;//跳出循环车跑
        takeRangeReading(0Xe0);//读取前方物体距离
        requestRange((0Xe0+1),&diss);
        sprintf(txt,"work:%d    ",work);
        OLED_ShowString(0,0, (unsigned char *)txt,8);
        sprintf(txt,"middle:%d    ",openmv_data[0]);
        OLED_ShowString(0,2, (unsigned char *)txt,8);
        sprintf(txt,"dis:%d    ",diss);
        OLED_ShowString(0,4, (unsigned char *)txt,8);
    }
/**************************************根据题目选择速度*************************************/
    switch(work)//根据题目选择速度选择
     {
         case 1:aim_speed=11.05;break;//题目一
         case 2:aim_speed=26;break;//题目二,追上前26,追上后18.35
         case 3:aim_speed=20;break;//题目三
         case 4:
             {
                 aim_speed=18.35*2-1;//跑快了速度不准,需要校正
                 turn_.kp=0.25;//电机比较辣鸡,1m速度时要重调参数
                 turn_.kd=0.4;
                 break;//题目四
             }
         default:while(1);
     }
    OLED_Clear();
/**************************************根据题目选择执行代码部分*************************************/
    while(1)
    {
        if(timer_20ms)//控制周期20ms
        {
            switch(work)//选择题目
            {
            case 1:control_1();break;//题目一
            case 2:control_2();break;//题目二
            case 3:control_3();break;//题目三
            case 4:control_4();break;//题目四
            }
            GPIO_toggleOutputOnPin(GPIO_PORT_P4, GPIO_PIN7);//指示灯
            timer_20ms=0;//清空标志位
        }
    }
}

        多说几句:在执行控制算法、滤波算法时,控制周期十分重要,具体体现在控制周期的长短以及周期的不变性,理论上控制周期越短越好,但实际上并不是这样,因为控制周期受限于机械的响应频率、传感器、控制器性能等,所以只能在一定范围内控制周期越短越好。

        至于为啥控制周期要不变,通俗地说,参数是就某一个控制周期调试的,周期变了,输出效果达不到预期。这一点在数字滤波器中体现尤为明显,数字滤波器在设计时要考虑截止频率,这其中该滤波器模拟频率f1=采样频率f * 数字频率f2,采样频率即控制周期,它变了模拟截至频率也变了,滤波器效果就直接改变了。

2.3速度闭环操作

        前面提到了速度闭环,至于为啥,说得直白一点便是进行速度闭环后想让小车跑多快就多块,这样你就会发现第一问根本不需要测距,仅仅利用速度闭环就可以搞定,当然这跟你做的速度闭环精度也有关系,做得足够好1、3、4问都可以不用测距。甚至可以再加上一个陀螺仪(非本作品),利用其偏航角和速度闭环(直接用编码器也可以)进行矢量积分,这样就可以得到相对于起点的当前坐标,美其名曰“全场定位”。

        说了速度闭环的作用,接下来就具体谈一谈它的实现。

        在说速度闭环以前,你需要有一定的基础知识,主要是PID相关知识,若有不懂的请自行百度之。说到速度闭环绕不开的两个东西就是电机和编码器,电机是被控对象,输入是PWM的等效电压,输出是转速(别杠我,你说是力矩也没问题),我们需要让转速达到我们期望的值,比如200r/min,有两种方法,一种是建立电机的精确动力学模型,然后根据动力学模型和负载给PWM等效电压,emmm反正我是做不到;另一种就很简单实用,直接使用编码器测出转速,并将其与目标转速做差后得到error,将error输入一个增量式PI控制器,控制器输出作用于tb6612电机驱动模块,tb6612将控制信号功率放大驱动电机,调整控制器参数即可实现速度闭环

图7 速度闭环框图

         控制流程知道了,那么怎么测速呢?这里用到的是霍尔正交编码器,它输出的波形是两条相位相差90°的方波信号,我们称为A相与B相,A相超前B相则正转,反之则反转,其频率正比于速度。

图8 正交编码器波形图 

        那么怎么用单片机读取正交信号呢?靠谱的方式有两种,第一种是部分单片机自带的硬件读取方式,优点是硬件实现,不消耗软件资源、精度高、测速范围广,缺点是占用一个定时器且很多单片机没有该功能;第二种是使用外部中断读取,观察波形可以知道,正转且A相处于上升沿时,B相是高电平,反转且A相处于上升沿时,B相是低电平,这样就可以将A相上升沿设置为外部中断触发源,触发后读取B相电平高低,高则计数加1,低则计数减1,单位时间内的计数值即位速度大小,正负表示方向。该方式的优点是通用性很强,几乎所有单片机都有外部中断功能,缺点是消耗软件资源且转速过快可能导致程序卡死。

程序内计算脉冲数代码:

#pragma vector=PORT2_VECTOR     // P2口中断源
__interrupt
void Port_2 (void)              // 声明一个中断服务程序,名为Port_2()//将其作为A相,用于触发中断
{
    if(GPIO_getInputPinValue(GPIO_PORT_P2, GPIO_PIN5))//读取B相,高电平计数加
        counter_right++;
    else//低电平则计数减
        counter_right--;
    GPIO_clearInterrupt(GPIO_PORT_P2, GPIO_PIN4);
}

多说几句:

        1.编码器种类很多,正交编码器(常见的有霍尔与光电,前者精度低,后者精度很高,不建议用外部中断方式读取光电编码器,因为太消耗算力了)、方向编码器(可以理解为正交编码器AB两相经过D触发器变成了一路脉冲和一路用正负电平表示方向)、磁编码器(常用于无刷电机FOC控制)等等;

        2.前边给出了测速的方法,细心的小伙伴能够发现,该种方式的测速精度是可以更高的,即将B相上升沿也用起来,这样可以得到双倍精度,我在工程中未采用双倍精度方式是为了给msp430节省算力,而事实证明单倍精度完全够用;

        3.电机型号很多种,有条件的建议直接使用成品电机,如大疆的M2006(没打广告,真的好用)。

2.4openMV场地元素识别

        现在来说一说场地元素的识别,这属于视觉的范畴,但不用害怕,电赛用不到高深的视觉知识,基本的知识可直接移步openMV官网。视觉这东西需要现场实际调试,代码受摄像头安装高度、角度等的影响很大,所以此处只给出一种可行的思路,不对本工程代码做过多解释。

        首先我们需要具体情况具体分析,以本次作品为例,其中我们需要作出判断的元素包括进三岔、出三岔、直行、弯道、暂停标识以及停车标识,第一步要做的就是确定摄像头安装位置、高度、角度,且非必要不要轻易改变!!!接着就是对图像进行处理,一般无颜色识别使用灰度图像即可,然后对图像滤波、二值化,这样就获得了黑白分明的二值化图像。

        接下来重点来了,一般的道路识别传统视觉就可以搞定,不要考虑神经网络巡线,神经网络巡线神TM的巡线。一般来说采取在图像中加入窗口的方式来进行元素识别,何为窗口,就是在图像中加入几个框框,然后对每一个框框内的色块分别处理,统计每一个窗口内符合要求的色块数量、大小等特征,结合多个窗口中的色块特征即可综合判断当前处于什么场地元素。如本工程中的5个窗口如下图

 图9 本作品加窗图像

        识别是视觉的核心,也是设计中最灵活的一块。识别做好了很多问题都可以迎刃而解,至于方法,本文提供了一种可行方式,肯定还有其它更好的方式,也欢迎各位小伙伴不吝赐教!最后将处理好的数据打包发送给主控制器即可,发送方式见本文 2.1模块间通信。

附上本作品视觉部分代码:

import sensor, image, time
from pyb import UART
from pyb import LED
import struct

sensor.reset()
sensor.set_pixformat(sensor.GRAYSCALE)
sensor.set_framesize(sensor.QQVGA)
sensor.skip_frames(time = 2000)
black_threshold = (0, 70)#二值化阈值
line_roi = (0, 100, 160, 10)#窗口1
line_roi2 = (0, 50, 160, 10)#窗口2
line_roi3 = (0, 20, 160, 10)#窗口3
left_roi = (25, 0, 8, 120)#窗口4
right_roi = (135, 0, 8, 120)#窗口5
uart = UART(3, 115200)

# 通信协议
def send_data_packet(x1,y1,z1):
    data =struct.pack("<bbbbb",                #格式为五个字符
                   0x2C,                       #帧头1
                   0x12,                       #帧头2
                   int(x1), # up sample by 4   #数据1,道路元素
                   int(y1), # up sample by 4   #数据2,黑线横坐标
                   int(z1),                    #数据3,另一条黑线横坐标,只有一条线时为0
                   )
    uart.write(data)


#求绝对值
def abs_(x1,x2):
    xx = x1-x2
    if(xx>0):
        return xx
    else:
        return -xx

flag = 0#0正常巡线,1正常停车,2岔路口in,3岔路口out,4非正常停车,5转弯
xx1=0
xx2=0
while(True):
    img = sensor.snapshot()
    img_black=img.binary([threshold])
    img_black.mean(2)
    blobs_low = img.find_blobs([black_threshold], roi = line_roi, pixels_threshold = 150)
    blobs_high = img.find_blobs([black_threshold], roi = line_roi2, pixels_threshold = 150)
    blobs_left = img.find_blobs([black_threshold], roi = left_roi, pixels_threshold = 100)
    blobs_right = img.find_blobs([black_threshold], roi = right_roi, pixels_threshold = 100)
    blobs_high2 = img.find_blobs([black_threshold], roi = line_roi3, pixels_threshold = 150)


    # 基础题停车判定
    # 使用下面的框进行判定,是否能够找到停车标志
    if(blobs_low and blobs_high):
        #max_blob = find_biggest_blobs(blobs_low)
        if(blobs_low[0].w()>0.5*img.width() and len(blobs_high)==1 and blobs_low[0].w()<0.8*img.width()):#停车0.3m下
            print("stop")
            flag = 1
            xx1 = 80
            xx2 = 0
        elif(len(blobs_high2)==0 and blobs_high[0].w()>0.6*img.width()):#提升停车
            print("stopwww")
            flag = 4
            xx1 = 80
            xx2 = 0
        elif(len(blobs_high)==1 and len(blobs_low)==1):#正常巡线
            len_h_l=abs_(blobs_high[0].cx(),blobs_low[0].cx())
            if(len_h_l<5):
                print("line")
                flag = 0
                img.draw_cross(blobs_high[0].cx(), blobs_high[0].cy(), (255,255,255), 30)
                xx1 = blobs_high[0].cx()
                xx2 = 0
            else:
                print("line_wan")
                flag = 5
                img.draw_cross(blobs_high[0].cx(), blobs_high[0].cy(), (255,255,255), 30)
                xx1 = blobs_high[0].cx()
                xx2 = 0
        elif(len(blobs_high)==2 and len(blobs_low)==2):#一类出入三岔
            len_high = abs_(blobs_high[0].cx(),blobs_high[1].cx())
            len_low = abs_(blobs_low[0].cx(),blobs_low[1].cx())
            if(len_high>len_low):
                print("in")
                flag = 2
                img.draw_cross(blobs_high[0].cx(), blobs_high[0].cy(), (255,255,255), 30)
                img.draw_cross(blobs_high[1].cx(), blobs_high[1].cy(), (255,255,255), 30)
                xx1 = blobs_high[0].cx()
                xx2 = blobs_high[1].cx()
            else:
                print("out")
                flag = 3
                img.draw_cross(blobs_high[0].cx(), blobs_high[0].cy(), (255,255,255), 30)
                img.draw_cross(blobs_high[1].cx(), blobs_high[1].cy(), (255,255,255), 30)
                xx1 = blobs_high[0].cx()
                xx2 = blobs_high[1].cx()
        elif(len(blobs_high)==2 and len(blobs_high2)==2):#另一类出入三岔
            len_high = abs_(blobs_high[0].cx(),blobs_high[1].cx())
            len_high2 = abs_(blobs_high2[0].cx(),blobs_high2[1].cx())
            if(len_high<len_high2):
                print("in")
                flag = 2
                img.draw_cross(blobs_high[0].cx(), blobs_high[0].cy(), (255,255,255), 30)
                img.draw_cross(blobs_high[1].cx(), blobs_high[1].cy(), (255,255,255), 30)
                xx1 = blobs_high[0].cx()
                xx2 = blobs_high[1].cx()
            else:
                print("out")
                flag = 3
                img.draw_cross(blobs_high[0].cx(), blobs_high[0].cy(), (255,255,255), 30)
                img.draw_cross(blobs_high[1].cx(), blobs_high[1].cy(), (255,255,255), 30)
                xx1 = blobs_high[0].cx()
                xx2 = blobs_high[1].cx()
        elif(len(blobs_high)==2 and blobs_right ):#出弯
            print("out")
            flag = 3
            img.draw_cross(blobs_high[0].cx(), blobs_high[0].cy(), (255,255,255), 30)
            img.draw_cross(blobs_high[1].cx(), blobs_high[1].cy(), (255,255,255), 30)
            xx1 = blobs_high[0].cx()
            xx2 = blobs_high[1].cx()
        elif(len(blobs_high)==2 and blobs_left):#进弯
            print("in")
            flag = 2
            img.draw_cross(blobs_high[0].cx(), blobs_high[0].cy(), (255,255,255), 30)
            img.draw_cross(blobs_high[1].cx(), blobs_high[1].cy(), (255,255,255), 30)
            xx1 = blobs_high[0].cx()
            xx2 = blobs_high[1].cx()

        send_data_packet(xx1,xx2,flag)

    # 画出一个矩形,便于查看
    #send_data_packet(128,159,2)
    img.draw_rectangle(line_roi)
    img.draw_rectangle(line_roi2)
    img.draw_rectangle(line_roi3)
    img.draw_rectangle(left_roi)
    img.draw_rectangle(right_roi)


多说几句:

        1.常用的视觉模块一般有openMV和k210,前者比较适合传统视觉,后者比较适合神经网络,它们俩并没有特别大的差距(k210的原配镜头可能暗角),有条件上linux板卡跑opencv当然更好;

        2.视觉最常遇到的问题就是抗干扰能力很差,一方面体现在场地光线亮度,另一方面则是场地反光。前者的解决办法有现场手动调节阈值、使用自动阈值(如大津法),后者到目前为止我未发现特别有效的解决办法,有作用的办法包括调整场地光线,让其尽量均匀、调节摄像头角度,找到效果好些的角度、加入偏振片,但会使图像变暗。

2.5 巡线实现

        前面已经介绍了视觉、速度闭环等,接下来介绍巡线部分的具体实现。该作品小车采取差速转弯方式,openMV将处理好的数据发送给主控制器,主控制器提取出巡线黑线的横坐标,与中点值做差得到误差error,将误差输入位置式PD控制器,控制器输出分别加与减在两只电机的目标速度上,调整控制器参数即可实现巡线。

图10 巡线实现框图

三、任务实现思路

        具体实现就是仁者见仁智者见智了,代码写得太绕,文字不好描述(其实是太久了我也忘了我写的啥)。总结起来就是主车只管自己跑,从车非必要也只管自己跑,必要时测个距离接收下主车的指令。

四、个人经验

        想来我不会再有机会参加大学生电赛了,遗憾没能拿到国奖,也庆幸当初自己选择了电赛。我参加电赛的初衷是觉得自己太菜了,想要变得厉害一点,然后随便拉了俩队友参加2021年国赛,奈何队友压根对电赛不感兴趣,而我又极其焦虑、不自信,最后国赛只拿了省二。今年电赛没有国奖,所以我去参加了智能车竞赛,有幸傍上俩靠谱队友的大腿,混了个国一。西部赛过后参加2022年电赛,边做边玩混了个省一,把去年没吃的零食都给吃回来了。

        电赛让我学会了很多东西,软的、硬的、机械的我都有所学。同时它也让我建立了自信,我不差,只是我给自己的压力太大。

        接下来就是一些我的个人经验,仅仅作为参考:

1.队友很重要,电赛四天三夜,除非很大神的人,不然一带二必寄。找队友就去找靠谱的,宁缺毋滥,也别羞于情面,该踢的人就得踢,纵容混子就是对自己不负责;

2.电赛(仅传统控制)涉及的知识面特别广,所以队内分工很重要,一般的分工是软件、硬件、机械,但最好不要只会自己的分工,更不要队内连沟通都没有。传统控制题在竞赛开始前谁也不知道会是啥,可能80%的任务得软件干,也可能80%的任务得硬件干,所以最好都有所准备,不然就会闲的闲死、累的累死;

3.不要啥也不准备去参加电赛,裸赛就是找虐。电赛对大部分人来说都是困难的,想要四天三夜从0开始很难很难,个人建议一定得做训练题、准备必备的功能模块,也一定要根据原件清单猜题提前准备;

4.别吊死在stm32和PCB板上。单片机只是工具,它们是互通的,一定要有能够很迅速地换款单片机的能力,或者直接使用ti的板子。做硬件的可以不会画PCB,但一定得会焊洞洞板,四天三夜不要指望投板子;

5.电赛四天三夜要把握好节奏,做软件的要有足够睡眠。前两个晚上能不熬夜就不要熬夜,熬了后面会很疲倦,代码都认识就是不知道写了啥那种。第一天硬件、机械就算熬夜也尽量把实物电路和机械搞定(这也是为啥不要指望投PCB);

6.想到了再补充。

工程下载链接:my_project_2022_two_cars: 2022年电赛工程

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
乘风的头像乘风管理团队
上一篇 2023年7月28日
下一篇 2023年7月28日

相关推荐