目录

2018年山东省机器人竞赛灭火机器人参赛心得

2018年的山东省机器人大赛也如火如荼的结束了。在自己第二次参加这个比赛之际,我也终于拿到了省一等奖。然而,这一次比赛依然是存在着不完美,在此将我参赛期间对比赛的理解、经验写出来,希望可以帮助到需要的人。

比赛规则

首先我们来看一下官方的规则。由于官方规则比较冗长,我这里摘录几条比较重要的,来逐条分析。

  • 比赛场地的墙壁高33cm,由木头做成。墙壁刷成白色。比赛场地的地板是被漆成黑色的光滑木制板。

这一条主要是告诉我们场地的特点。墙壁的高度主要决定了小车能安装传感器的最高高度。事实上,一般的小车都不会超过33cm,因此传感器的安装高度可以不用太在意。墙壁为刷成白色的木头,反光性能比较强,使用光电距离传感器的效果会非常不错。地板是黑色的,对于识别地板上白色的指示线也可以使用光电距离传感器。

  • 比赛场地的房间共4个,房间的走廊和门口的开口宽度都是46cm,将会有一个2.5cm宽的白色带子或白漆印迹表示房间入口。每个房间分别有半径为30cm左右的蜡烛放置区3个(编号为A,B,C,其中4号房间的位置 A未设置)。蜡烛放置于该圆弧的圆心附近位置,具体位置有裁判根据场地确定。 为了实现竞赛的真正目的,机器人在试图扑灭火焰前必须到距火焰30cm以内。在距离火焰30cm的圆上有一条2.5cm宽的白线,机器人在扑灭火焰前必须有一部分在圆圈内,但此时机器人不能碰倒蜡烛。

比赛总共是有四个房间,每个房间内均会有一根蜡烛等待我们去吹灭,但是蜡烛具体的位置是随机的,由现场抽签决定。每个房间门口都有一条白线指示,蜡烛前也有白线,这两条白线是我们识别的重点。

  • 场地照明比赛场地周围的照明等级在比赛时才能确定。参赛者在比赛期间有时间了解周围的灯光等级及标定机器人。在第一天调试设定后,比赛的照明将不会再调整来满足个别参赛者的要求。比赛的挑战之一就是要求机器人能够在一个不确定照明、阴影、散光等实际情况的环境中进行。

场地光线不确定确实给我们造成了很大的识别困难,因为我们使用的火焰传感器,本质上是一个红外线传感器,强烈的阳光也会包含大量的红外线,导致识别错误,小车逻辑直接出错。

  • 机器人在运行过程中可以碰撞或接触墙壁,但不能标记和破坏墙壁,如果碰到墙壁将会被扣分。

就是字面意思,小车不能撞墙,撞一次加罚1秒,刮墙2cm加罚1秒

  • 蜡烛火焰的底部离地面15cm~20cm高,这高度包括支持蜡烛的木质基座。蜡烛是直径大约为2cm粗的白蜡烛。

这条规则告诉我们火焰的高度,可以据此调整传感器高度。

  • 声音启动:这种模式下,机器人不是由人工按按钮来启动,而是接收到3.0~4.0kHz的声音信号后启动。一旦机器人电源打开,只有发出声音机器人才会启动。如果没有接收到声音时机器人就启动,或错误地检测到周围环境的噪声(即使是其他赛场用于启动机器人的声音)而启动,么本轮比赛仍然有效,但机器人不能作为声音启动模式来计分。声音启动模式的得分分数是0.95。

使用声音启动,可以让时间乘以0.95。这个功能还是比较好实现的,是一个很好的加分项。

  • 回家模式:机器人熄灭蜡烛后回到代表起始位置的圆圈内。这里不要求按原路返回及选择最优路径,只要回来就行了,但在回家路上不能进到房间里。如果机器人的任何一部分进入代表起始位置的30cm白圈内,就认为机器人回到了家中,而不必和刚开始的位置一样。回家模式的得分系数是 0.8。

这一条规则要求我们能够在进行完一轮比赛之后,能够再次回到起点而不进入各个房间,这样得分可以乘以0.8,获得一个比较大的加成。

  • 灭火模式:由于使用风扇灭火在现实世界中并不实用,因此,如果参赛者不使用吹风灭火的方式将火灭掉,会有0.85的系数,使用吹风灭火机器人的得分系数为1.0。

这一条规则是在鼓励我们去寻找除了风扇之外其他的灭火方式,然而根据往年的经验,由于风扇的风是非指向性的,可以吹很大的一个区域,灭火效果最好,其他方式例如水流都会出现类似于射歪的问题。

  • 除最小系统板外,其他电路板(不论厂家制作还是自己雕刻的)需要在覆铜层(即TopLayer或BottomLayer)上加学校名称、队伍名称和年份。若使用标准面包板自己焊接的电路则不受该项限制,但也要在机器人的某个部位通过如黏贴、刻画等手段,清晰的显示学校名称、队伍名称和年份,以便甄别。

这一条规则规定我们PCB必须是自制的,或者使用万能板自己焊接。不过往年来看。使用万能板的少之又少,一是因为占地大,二是因为可靠性差。

硬件方案

通过上面的规则分析,现在我们对于规则就有了一个较为详细的理解。接下来我们分析方案实施。

小车底盘方案

适用于这种机器人竞赛的小车底盘所使用的电机,一般都是直流有刷电机,我们也不需要过多的去选择。我们从网上购买到的小车底盘,一般都是一块亚克力或者金属的底板,加上直流电机和与之配套的轮子与支架。这里需要我们选择的主要就是小车的驱动模式。常见的驱动模式主要有以下三种:

  • 后轮驱动,前面使用一个万向轮或者牛眼轮来被动转向,后面两个轮子差速来前进、转向
  • 四轮驱动,四个轮子分别使用一个直流电机驱动,用差速来实现转向
  • 后轮驱动,前轮使用舵机来转向

实际情况中,第三种方案速度快,转弯半径大,车体比较稳定,适合直行偏多的场合,例如飞思卡尔智能车比赛等。第二种方案与第一种方案最大的区别在于转弯灵活性。由于灭火小车的场地里面转弯尤其是直角转弯偏多,四轮驱动在转大弯的时候的转弯半径较大,因此论灵活性来说肯定是两驱的更加适合。当然,如果使用麦克纳姆轮这样的小种方案,四轮驱动就变成了必须。我们的小车也选择的两驱方案,选择两驱方案还有驱动容易这个优点,在后续电路设计的时候可以体现出来。此外,我们还选择了带有正交编码器的直流电机,这在后面闭环时候起到了关键作用。

传感器方案

小车使用的传感器主要分为三类:接近传感器、巡线传感器、火焰传感器、声音传感器。下面是这些传感器安装在灭火小车上的位置。

https://img.yuanze.wang/posts/2018-robot-contest/sensors.jpg
传感器安装位置

接近传感器

接近传感器主要是用来实现小车寻右前进的。由于我们的小车不仅仅寻右,还要实现前面有墙壁的时候掉头,因此总共需要安装三个接近传感器,它们分别安装在小车右前、右后以及前方。右后接近传感器主要检测小车尾部离墙壁的距离,防止小车偏离墙壁距离过大;右前传感器主要用来实现让小车车体整体与墙壁保持一个相对稳定的距离,前面的传感器就像前面所说的,用来检测前方墙壁并掉头或者拐直角弯。 值得注意的是,对于这三个接近传感器,它们的传感器距离都是可以调整的,调整的时候,请一定要使用场地实际的墙壁进行测试,因为不同材质、粗糙度的墙壁对传感器的灵敏度影响很大。我们需要注意的地方如下:

  • 对于前面中间的接近传感器,距离不宜调的过近,否则会导致急向左转开始的较晚,容易在转弯的时候撞墙;同时距离也不宜调的过远,否则容易出现在直行时被别的房间的墙壁干纷导致误判断。
  • 对于右面的两个传感器,后面传感器的检测距离应该比前面的要远一些,让前面的传感器灵敏度高一些,使后面两条中描述的小车轻微震荡过程容易发生。
  • 小车在右边两个传感器均检测到墙壁时,略微向左直行,让小车不至于在直行的过程中挂到墙。
  • 在只有后面传感器检测到墙壁时,向右转弯,这个转弯主要是用于寻右拐弯,因此幅度可以设置的大一些,同时与前面的向左转实现一个小车的略微震荡,让小车可以略微摇摆着向前进,不至于撞到墙的同时,也可以及时转弯。
  • 在后面传感器检测不到墙壁的时候(不管前面传感器能不能检测得到),说明小车离墙壁距离过远,偏离了正常运行时的轻微震荡,需要向右面以一个更大的角度转弯来贴靠墙壁,实现右寻。但是要注意这个地方还是要前进着往前走,而不是让小车原地打转,否则容易陷入死循环,无法贴紧墙壁。

巡线传感器

由于我们必须知道小车当前是处于房间内、房间外并且需要知道小车是不是已经到达了火焰处,因此地面的白线也是我们必须要检测的对象之一。这里我们就用到了循迹小车常用的巡线传感器。一般的寻迹小车是检测和车身运行方向平行的直线,但是这里我们检测的都是与x车身运行方向垂直的直线,因此传感器的安装还需要注意。为了增加检测的准确性与稳定性,小车上总共安装了一左一右共计两个巡线传感器。我们使用的巡线传感器如下图所示。

https://img.yuanze.wang/posts/2018-robot-contest/line-sensor.jpg
巡线传感器

对于巡线传感器,我们需要调节的参数主要是灵敏度。由于线是白色的,底板是黑色的,对于这种光电传感器来说,非常容易识别。我们只需要将其调整到在有白线时输出1,无白线时输出0即可。

火焰传感器

火焰传感器用来检测火焰。我们使用的火焰传感器如下图所示。

https://img.yuanze.wang/posts/2018-robot-contest/fire-sensor.jpg
火焰传感器

这种火焰传感器灵敏度很高,但是存在一个致命的缺陷:只能识别红外线,太阳光如果很强也会被误识别,但是一般的日光灯就不会。所以会出现在调试的时候因为是在室内或者是在晚上调试而寻找火焰时没有任何问题,但是到了比赛场地就误动作的问题。为了解决这个问题,第一个就是要求组委会将比赛场地安排在室内并挡上窗帘,这是本质的解决方法。第二个就是我们可以通过在传感器探头上套一个热缩管,让探头对红外线的识别具有一定的指向性,避免环境辐射来的红外线导致误动作。这一方法非常有用,但是也无法从根本解决问题。

声音传感器

声音传感器本质就是一个麦克风加一个比较器。当麦克风收到的声音超过比较器阈值的时候,就输出一个高电平的数字信号,指示小车开始运行。实际比赛中,为了防止误动作,在按下按键之后,接收到较大音量的时候,才会启动小车。

硬件设计方案

硬件设计方面,由于之前参赛使用手工焊接的万能板所出现的种种问题,这一次我自己制作并焊接了PCB。PCB上主要的电路,我将在下面一一讲解。

https://img.yuanze.wang/posts/2018-robot-contest/pcb-3d-1.png
PCB3D图

https://img.yuanze.wang/posts/2018-robot-contest/pcb-3d-2.png
PCB3D图侧面

https://img.yuanze.wang/posts/2018-robot-contest/pcb-2d.png
删除正面敷铜的PCB布线

单片机最小系统

毫无疑问,这一次使用的单片机也是STM32。因为有比较多的传感器,加上电路比较复杂,芯片的引脚数就成了必须要考虑的问题。通过通用性和引脚数的考量,最终选择了STM32F103RCT6这一款64脚的芯片,可以很好的满足要求。

电机驱动电路

小车底盘上的电机为小功率直流电机,因此选择了TB6612这款MOS驱动H桥的双路电机驱动芯片。与常用的L298相比,它具有发热小、体积小、电路简单、性能更强的优点,是我们替代L298N的良好选择。 除此之外,为了实现某些条件下的特殊要求(例如小车必须走严格的直线),我们还需要进行闭环。因此除了电机驱动部分之外,还加上了编码器接口,一个编码器需要使用STM32的一个定时器。

风扇、蜂鸣器驱动电路

由于我们使用的风扇是刀片服务器上的暴力风扇,一个风扇就有40W以上的功率,加上风扇开关的频率也不高,因此选择了继电器驱动的方案。继电器使用小功率三极管驱动,蜂鸣器也同样使用小功率三极管驱动。

其他接口电路

为了增强拓展性,PCB板上除了有上述电路之外,还有13路传感器的接口、OpenMV接口、蓝牙串口接口、NRF24L01接口、OLED接口以及三个按键。

除此之外,为了提高调车时的体验,这一次的PCB板上还板载了一个J-link,这样在调试的时候,只需要连接上USB线,即可快速下载程序。当然,这个J-Link毋庸置疑是盗版的,充其量也就是一个STM32F103C8T6的最小系统加上一堆电阻和一个USB座。原理图和二进制烧录文件都是从网络收集得来,焊接好之后只需要用一个正常的J-Link将程序下载进板子上的这一块单片机,就可以让它成为J-link。

https://img.yuanze.wang/posts/2018-robot-contest/jlink-sch.png
J-Link原理图

软件设计

小车运行路线规划

按照规则,小车从H处出发,经过四个房间并吹灭蜡烛后,回到H处,即完成比赛。因此,如何设计小车路线能够让小车在最短的时间内既能遍历所有房间,又能以最短的路线回到起点,成了我们考虑的重点。综合往年的方案与自己的方案,我们得出了如下的方案:小车按照①到④的顺序,依次进入四个房间,在每个房间内查找火焰并灭火。除了寻找火焰的过程外,小车均遵循右手定则,即永远贴着右侧墙壁走,遇到墙角则向左转。小车运行路线图如下图所示。其中房间内的旋转箭头即寻找火焰的过程。 需要注意的是,④号房间由于墙壁不和任何一个墙壁相连,因此不能使用常规的右手法则进入。于是,为了能够正常进入④号房间,我们在3号房间出门之后的第一个前方墙壁处,强制小车掉头,即可进入④号房间。

https://img.yuanze.wang/posts/2018-robot-contest/map.png
小车运行路线

底层硬件驱动

硬件初始化主要包含按键蜂鸣器初始化、风扇控制IO口及PWM定时器初始化、传感器输入IO初始化、电池电压检测ADC初始化、编码器、定时器中断等部分。下面是电机驱动及编码器输入的初始化代码。

void Motor_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStructure;
  TIM_TimeBaseInitTypeDef TIMBaseInit; 
  TIM_OCInitTypeDef TIMOCInit;

  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOC,ENABLE);//开启GPIOA.B.C时钟
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);//打开定时器2时钟

  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;//PA0.PA1 PWMA.PWMB
  GPIO_Init(GPIOA,&GPIO_InitStructure);

  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_10 | GPIO_Pin_11;//PB0 STDBY,PB10.PB11 BIN1.BIN2
  GPIO_Init(GPIOB,&GPIO_InitStructure);

  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4|GPIO_Pin_5;//PC4.PC5 AIN2.AIN1
  GPIO_Init(GPIOC,&GPIO_InitStructure);

  TIMBaseInit.TIM_ClockDivision = 0;//时钟分频器 用不到 直接赋0
  TIMBaseInit.TIM_CounterMode = TIM_CounterMode_Up;//定时器向上计数模式
  TIMBaseInit.TIM_Period = 7199;//自动重载值 即计数最大值
  TIMBaseInit.TIM_Prescaler = 0;//预分频系数 系统时钟除以次数+1即为计数频率
  TIM_TimeBaseInit(TIM2,&TIMBaseInit);//初始化时基模块

  TIMOCInit.TIM_OCMode = TIM_OCMode_PWM1;//输出比较模块为PWM模式
  TIMOCInit.TIM_OCPolarity = TIM_OCPolarity_High;
  TIMOCInit.TIM_OutputState = TIM_OutputState_Enable;//使能比较输出 与GPIO相连
  TIM_OC1Init(TIM2,&TIMOCInit);//初始化TIM2 CH1 PA0
  TIM_OC2Init(TIM2,&TIMOCInit);//初始化TIM2 CH2 PA1

  TIM_Cmd(TIM2,ENABLE);//开启定时器2

  GPIO_SetBits(GPIOB,GPIO_Pin_0);//STDBY=1 开启电机驱动器
}

void Encoder_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStructure;
  TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
  TIM_ICInitTypeDef TIM_ICInitStructure;

  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOC|RCC_APB2Periph_TIM8,ENABLE);//开启GPIOB时钟
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);

  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;//PB6.PB7 M1A.M1B
  GPIO_Init(GPIOB,&GPIO_InitStructure);

  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;//PC6.PC7 M2A.M2B
  GPIO_Init(GPIOC,&GPIO_InitStructure);

  TIM_DeInit(TIM4);
  TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);
  TIM_TimeBaseStructure.TIM_Period = 0xFFFF;
  TIM_TimeBaseStructure.TIM_Prescaler = 0;
  TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1 ;
  TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; 
  TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure);

  TIM_EncoderInterfaceConfig(TIM4,TIM_EncoderMode_TI12,TIM_ICPolarity_Falling,TIM_ICPolarity_Falling);

  TIM_ICStructInit(&TIM_ICInitStructure);
  TIM_ICInitStructure.TIM_ICFilter = 6;//ICx_FILTER;
  TIM_ICInit(TIM4, &TIM_ICInitStructure);


  TIM_DeInit(TIM8);
  TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);
  TIM_TimeBaseStructure.TIM_Period = 0xFFFF;
  TIM_TimeBaseStructure.TIM_Prescaler = 0;
  TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1 ;
  TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; 
  TIM_TimeBaseInit(TIM8, &TIM_TimeBaseStructure);

  TIM_EncoderInterfaceConfig(TIM8,TIM_EncoderMode_TI12,TIM_ICPolarity_Falling,TIM_ICPolarity_Falling);

  TIM_ICStructInit(&TIM_ICInitStructure);
  TIM_ICInitStructure.TIM_ICFilter = 6;//ICx_FILTER;
  TIM_ICInit(TIM8, &TIM_ICInitStructure);

  TIM_Cmd(TIM4,ENABLE);
  TIM_Cmd(TIM8,ENABLE);
}

在用户程序中,我们只需要调用系统初始化函数,即可完成上述所有部分的初始化工作。系统初始化的函数如下。

void Hardware_Init(void)
{
  NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
  delay_init();
  SetRemap();
  Key_Buzzer_Init();
  Fan_Init();
  Sensor_Init();
  Motor_Init();
  BattVolt_Init();
  Encoder_Init();
  TIM3_IRQ_Init(99,7199);//72000000/7200/100=100Hz,10ms
}

底层硬件接口

为了方便编程,在设计完硬件之后,首先完成的就是完善的底层硬件驱动接口。接口主要分为控制电机、读取编码器、控制风扇、读取按键和传感器输入等。部分接口如下:

void Fan_Set(uint8_t speed);//设置风扇速度 0为关闭
void Motor_Set(int16_t left,int16_t right);

#define GO_FORWARD(turn) Motor_Set((float)speed*(1+(turn)),(float)speed*(1-(turn)))//前进 正为右 负为左
#define GO_BACKWARD(turn) Motor_Set(-(float)speed*(1-(turn)),-(float)speed*(1+(turn)))//后退 正为右 负为左
#define SHARP_TURN_RIGHT(turn) Motor_Set((float)speed*(turn),-(float)speed*(turn))//右急转
#define SHARP_TURN_LEFT(turn) Motor_Set(-(float)speed*(turn),(float)speed*(turn))//左急转
#define SLIGHT_TURN_RIGHT(turn) Motor_Set((float)speed*(turn),-(float)speed*(turn)*0.1)//右略微转
#define SLIGHT_TURN_LEFT(turn) Motor_Set(-(float)speed*(turn)*0.1,(float)speed*(turn))//左略微转
#define STOP() Motor_Set(0,0)//停止

#define A1 PCin(3)
#define A2 PCin(2)
#define A3 PCin(1)
#define A4 PCin(0)
#define D1 PBin(9)
#define D2 PBin(8)
#define D3 PBin(5)
#define D4 PBin(4)
#define D5 PBin(3)
#define D6 PDin(2)
#define D7 PCin(12)
#define D8 PCin(11)
#define D9 PCin(10)
#define D10 PAin(15)

#define FRONT_SENSOR D6
#define RIGHT_FRONT_SENSOR D8
#define RIGHT_SENSOR D7
#define FIRE (D1&&D2&&D4)
#define SOUND D10
#define LINE_LEFT D3
#define LINE_RIGHT D9

小车运行状态分析

小车在运行的过程中,总共存在四种状态:

  • 未启动时的停止状态
  • 不在房间内的正常运行状态,此时小车按照右手法则前进
  • 在房间内的找火状态,此时小车寻找火焰
  • 熄灭火焰,但仍在房间内的状态

在程序中,这三种状态使用状态机进行切换,使用标志变量进行区分。程序框架如下。 小车整体逻辑为,在接收到启动的声音信号后,小车首先进入正常运行模式,并使用右手法则进入一个房间。在检测到房间的白线后,小车急刹车,然后向前运行一段距离(通过程序设置)到达房间的中心,然后缓慢原地旋转寻找火焰(旋转时长通过程序设置),若未找到火焰就离开房间;若找到则慢慢前进直到小车前的巡线传感器接触蜡烛前的白线停止,之后开启风扇吹灭,再进入下一个房间,重复上述过程。

运行中的细节处理

针对上述小车运行的逻辑中的一些细节部分,接下来进行详细的说明。

右手法则

对于右手法则,前面在介绍传感器的时候已经讲了其基本原理。需要注意的是三个传感器的优先级顺序。在程序中,中传感器的优先级是最高的,其次是后传感器,最后是前传感器。值得注意的是,程序中对应前述三种情况时的转弯值,也是调整参数的重要部分之一,同时为了保证响应速度,转弯和正常前进这部分并没有使用闭环控制。右手法则部分的程序如下。

if(!FRONT_SENSOR)
{
 if(roomcnt == 3)
  front_sensor_delay = 60;
 else
  front_sensor_delay = 25;
 /***********************第四个房间之前强行掉头**********************/
 if(roomcnt == 4 && inroom == 0 && already_turned == 0)//将要进入最后一个房间
 {
  SHARP_TURN_LEFT(1.2);
  BUZZER = 1;
  delay_ms(600);
  BUZZER = 0;
  already_turned = 1;
 }
 else//正常右转
 {
  SHARP_TURN_LEFT(1);
 }
}
/***********************正常寻右运行**********************/
else if(!RIGHT_SENSOR && !RIGHT_FRONT_SENSOR)//右边两个都有
{
 GO_FORWARD(-0.15);
}
else if(!RIGHT_SENSOR)//右前没有了
{
 GO_FORWARD(0.15);
}
else if(!RIGHT_FRONT_SENSOR)//只有右前
{
 GO_FORWARD(-0.1);
}
else//都没有了
{
 GO_FORWARD(0.7);
}

小车进房间与在房间内的判断与处理

小车在正常运行并处于房间之外的时候,若前方两个巡线传感器有一个检测到了白线,就说明已经到达了房间口。此时,为了保证小车的车头方向和房间门的方向垂直,采用的方法是令小车急刹车。急刹车的实现主要是基于电机驱动芯片来实现的,当TB6612的A与B均为高电平的时候,H桥将电机的两端导通,进入动力制动模式,小车可以迅速刹在原地。这之后,小车向前直线运行一段通过程序设定的时间之后,便可以达到房间中央。此处程序节选如下。

if(!inroom)
{
 /***********************进房间进入寻火模式**********************/
 if(!LINE_LEFT && global_delay == 0)
 {
  if(roomcnt == 5 && global_delay == 0)//终点线
  {
   Motor_Break();
   BUZZER = 1;
   delay_ms(1000);
   BUZZER = 0;
   start_flag = 0;
   roomcnt = 1;
   return;
  }
  
  inroom = 1;//在房间内
  Motor_Break();//急刹车
  
  /***********************进房间直行**********************/
  if(roomcnt == 1)
   temp = 800;
  else if(roomcnt == 2)
   temp = 800;
  else if(roomcnt == 3)
   temp = 1100;
  else if(roomcnt == 4)
   temp = 400;
  else
   temp = 0;
  go_straight(temp);//直行

在实际测试中,发现小车在向前直行的过程中,总是无法走直线,猜测应该是由于两个电机的特性差异导致的。这个差异在右手法则阶段不明显甚至可以忽略,但是这种需要严格直行的场合下,就变得特别明显。解决方法就是使用编码器,实现一个类似与PID控制的P控制,通过编码器累积走过的值来控制两个电机的相对速度,进而实现一个简单的闭环控制。经过测试,效果良好。相关代码如下。

void go_straight(uint16_t time)//0为寻找白线 否则为向前走的毫秒数
{
 float ratio_l,ratio_r;
 
 if(roomcnt == 2 && !fire_on)//第二个房间 需要向右转
 {
  TIM4->CNT = 0;
  TIM8->CNT = 250;//清空编码器数值
 }
 else
 {
  TIM4->CNT = 0;
  TIM8->CNT = 0;//清空编码器数值
 }
 if(time == 0)
 {
  while(1)
  {
   ratio_r = straight_ratio;
   ratio_l = straight_ratio * (1 - (TIM4->CNT - TIM8->CNT) * 0.01);
   
   Motor_Set(speed*ratio_l,speed*ratio_r);//直行前进
   
   if(!LINE_LEFT || !LINE_RIGHT)
    break;
  }
 }
 else
 {
  while(time --)
  {
   ratio_r = straight_ratio;
   ratio_l = straight_ratio * (1 - (TIM4->CNT - TIM8->CNT) * 0.01);
   
   Motor_Set(speed*ratio_l,speed*ratio_r);//直行前进
   delay_ms(1);
  }
 }
}

在小车行进设定的时间之后,再次急刹车,让小车停到到房屋中间。然后让小车慢速旋转,当火焰传感器检测到火焰的时候,说明当前方向上有火焰,再次调用上面的走直线函数并设置参数为0,即可让小车走到白线的时候停下来,而不是走特定的时间停下。

Motor_Break();//急刹车
fire_on = 1;
find_fire();//找火
fire_on = 0;

void find_fire(void)
{
 uint16_t i;
 
 SHARP_TURN_RIGHT(0.3);//顺时针转 找火
 
 for(i = 0;i < 40000;i ++)
 {
  delay_us(100);
  
  if(!FIRE)//找到火了
  {
   Motor_Break();
   
   go_straight(0);//直行 等待到白线
   Motor_Break();
   RETRY:
   Fan_Set(255);//开风扇
   delay_ms(1000);
   delay_ms(1500);
   Fan_Set(0);
   delay_ms(500);
   if(!FIRE)
    goto RETRY;
   
   break;
  }
 }
}

灭火之后的处理

小车在灭火之后,继续按照右手法则离开房间。但是在室内进行右手法则的时候,会经过墙角蜡烛处的白线从而导致误识别已经出房间了,这显然是我们不希望的。这个问题的解决,是通过安装的两个巡线传感器来实现的。因为蜡烛处的白线是弯的,因此两个巡线传感器检测到白线存在一个先后的时间差,与此同时出房间的白线都是和墙壁垂直的,这样我们就可以利用这个时间差来判断当前是蜡烛的白线还是房间口的白线,配合上一定的延时,误判概率几乎为0。 程序方面,使用定时器中断,在中断中对变量进行计数,通过计数变量判断两个传感器检测到白线的时差并进行延时,进而增强判断的准确性。同时,在检测到出房间的白线,程序也有一个小的延时,防止重复检测导致刚出房间就进房间。部分程序如下。

/***********************延迟不检测白线**********************/
if(roomcnt == 4)
 global_delay = 30;//第4个房间延迟300ms不检测白线
else if(roomcnt == 1)
 global_delay = 80;//第一1个房间
else if(roomcnt == 3)
 global_delay = 110;
else
 global_delay = 90;//2不检测白线

else//在房间内
{
 /***********************退出房间判断条件**********************/
 if(line_valid && global_delay == 0 && front_sensor_delay == 0)//前面没有信号
 {
  inroom = 0;
  roomcnt ++;
  
  global_delay = 50;//延时500ms
  
  BUZZER = 1;//提示出房间
  delay_ms(50);
  BUZZER = 0;
 }
}

void TIM3_IRQHandler(void)
{
 if(TIM_GetITStatus(TIM3,TIM_IT_Update))//如果产生的是更新中断
 {
  if(left_line_cnt)
   left_line_cnt --;
  if(right_line_cnt)
   right_line_cnt --;
  
  if(!LINE_LEFT)
   left_line_cnt = 4;
  if(!LINE_RIGHT)
   right_line_cnt = 4;
  
  if(left_line_cnt && right_line_cnt)
   line_valid = 1;
  else
   line_valid = 0;
  
  if(global_delay)
   global_delay --;
  
  if(front_sensor_delay)
   front_sensor_delay --;
  
  TIM_ClearITPendingBit(TIM3,TIM_IT_Update);
 }
}

④号房间之前的特殊处理

正如前文所述,④号房间不和墙壁相连,因此需要特殊的处理才能正确进入。在④号房之前的墙壁处,强行让小车掉头,即可进入④号房间。在比赛中,我们使用的是延时的方法,让小车转过180度来实现掉头。但是实际上,由于比赛场地与学校里的场地的地面摩擦力不一样,在学校正常的参数,到了场地就显得偏大,导致小车转向超过180度,没有进入④号房间而是按照正常路径回到起点,识别起点白线导致逻辑错乱。正确的改进方法应该是使用编码器,通过设定编码器转过的值,来确定小车转过的角度。此部分的代码如下。

/***********************第四个房间之前强行掉头**********************/
if(roomcnt == 4 && inroom == 0 && already_turned == 0)//将要进入最后一个房间
{
 SHARP_TURN_LEFT(1.2);
 BUZZER = 1;
 delay_ms(600);
 BUZZER = 0;
 already_turned = 1;
}

其他

其他代码包括声控启动等代码,较为简单,可以下载完整代码查看。

 单片机代码

 PCB工程

最终效果