项目描述:
巡线小车是我作为新手入手的第一个项目,基本巡线功能是使用红外传感器循迹模块判断黑线的路径来确定转向方向,同时控制单片机配置PWM占空比波控制小车前进的L298N电机模块,实现前后退,左右转(差速转)来巡线。利用HC-05蓝牙模块来使用USART串口发送指令,实现小车的遥控功能;利用mpu6050陀螺仪模块感应偏航角与俯仰角,控制小车转向和加减速;

需求分析:
任务1:能够完成遥控功能
能够通过手机或电脑远程或其他远程控制端启动小车按要求通过图1轨道,轨道用宽约1cm的黑色电工胶制作。
在这里插入图片描述

图1

1、小车车头放置在A点,远程启动小车,启动后小车匀速行驶,行驶过程中不能碾压左右两边边界线,第30s时车头经过B点,时间误差绝对值不超过1s。
2、小车车头放置在A点,远程启动小车,启动后小车匀速行驶,行驶过程中不能碾压左右两边边界线,小车行驶第10s是车头经过B点,时间误差绝对值不超过1s。
3、小车车头放置在A点,远程启动小车,启动后小车匀速行驶,行驶过程中不能碾压左右两边边界线,小车行驶第30s时车头停在B点,时间误差绝对值不超过1s,误差绝对值不超过3cm。

任务2:能够完成以下巡线功能

能够通过手机或电脑或其他远程控制端远程启动小车按要求通过图2轨道,轨道用宽约1cm的黑色电工胶制作。

在这里插入图片描述

图2

小车车头放置在A点,通过远程控制启动小车巡线前进,在前进过程中识别到路灯放置处为红色或其他颜色则在路灯停止线停下来,识别到路灯放置处为绿色则继续前进,小车到达B点时自动停止并且蜂鸣器发出声音提示。在整个前进过程中小车走重复路段和小车垂直投影偏离轨道视为失败。

任务3:能够通过远端设备体感控制小车

能够通过远程控制端倾斜来控制小车运动,远程控制端倾斜方向和程度控制小车运动方向和速度。

在这里插入图片描述

图3

假设远程控制端为图3中纸飞机,纸飞机的机头对应远程控制端的正前方。 当远程控制端沿pitch方向出现角度A时,小车后退。
当远程控制端沿pitch反方向出现角度A时,小车前进。
当远程控制端沿roll方向出现角度B时,小车向右转弯(时持续转弯,不是转到某个角度就不转了)。
当远程控制端沿roll反方向出现角度B时,小车向左转弯。
角度A与小车速度成正比,角度A变化区间为10°40°时,速度变化区间为0300mm/s;角度A变化区间为-10°-40°时,速度变化区间为0-300mm/s
角度B与小车方向成正比,角度B变化区间为10°40°时,小车方向变化区间为90°135°;角度B变化区间为-10°-40°时,小车方向变化区间为90°45°(转向角度参考图4)

在这里插入图片描述

图4

设计思路
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

开发过程:

1.让小车动起来:控制电机转动

第一步选择电机,我们选用两个直流无刷电机控制一左一右两个轮胎的转动,但是要同时驱动两个电机需要10V以上的电源供电,所以我们外接了一个12V的电池,但是一个电机大概能够接受5V的电压,所以需要转换12V到5V,网上搜索怎么办:
在这里插入图片描述
在这里插入图片描述

由资料显示,我们有两种方案,但是第一种方案发热问题严重,我们用第二套方案;电路图如下:
在这里插入图片描述

有了5V的电源还不够,电机只要接上电源就能转动,但是不能控制速度,正反转,所以需要专门控制电机的电机驱动TB6612FNG模块:
查询相关数据

TB6612FNG引脚图
TB6612FNG是东芝半导体公司生产的一款直流电机驱动器件,它具有大电流MOSFET-H桥结构,双通道电路输出,可同时驱动2个电机。
TB6612FNG每通道输出最高1.2 A的连续驱动电流,启动峰值电流达2A/3.2
A(连续脉冲/单脉冲);4种电机控制模式:正转/反转/制动/停止;PWM支持频率高达100
kHz;待机状态;片内低压检测电路与热停机保护电路;工作温度:-20~85℃;SSOP24小型贴片封装。

在这里插入图片描述

如图1所示,TB6612FNG的主要引脚功能:AINl/AIN2、BIN1/BIN2、PWMA/PWMB为控制信号输入端;AO1/A02、B01/B02为2路电机控制输出端;STBY为正常工作/待机状态控制引脚;VM(4.5~15
V)和VCC(2.7~5.5 V)分别为电机驱动电压输入和逻辑电平输入端。
TB6612FNG是基于MOSFET的H桥集成电路,其效率高于晶体管H桥驱动器。相比L293D每通道平均600 mA的驱动电流和1.2
A的脉冲峰值电流,它的输出负载能力提高了一倍。相比L298N的热耗性和外围二极管续流电路,它无需外加散热片,外围电路简单,只需外接电源滤波电容就可以直接驱动电机,利于减小系统尺寸。对于PWM信号,它支持高达100
kHz的频率,相对以上2款芯片的5 kHz和40 kHz也具有较大优势。

根据网上找到的资料,我们使用一AINl/AIN2和PWMA来控制左电机、BIN1/BIN2和PWMB来控制右电机,再根据PWMA和PWMB接口需要有时钟复用功能绘制出原理图就行了:
在这里插入图片描述
(主芯片部分)
在这里插入图片描述
(电机驱动模块)
这样就能写驱动代码了,初始化IO口(略),配置时钟:

1
//初始化定时器(右轮的) TIM_TimeBaseStructure.TIM_Period = arr; TIM_TimeBaseStructure.TIM_Prescaler = psc; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseInit(TIM4, & TIM_TimeBaseStructure); //初始化定时器通道 TIMOCInitStructure.TIM_OCMode=TIM_OCMode_PWM1;//输出模式 TIMOCInitStructure.TIM_Pulse=0; TIMOCInitStructure.TIM_OCPolarity=TIM_OCPolarity_High ;//极性 TIMOCInitStructure.TIM_OutputState=ENABLE; TIM_OC3Init(TIM4,&TIMOCInitStructure); //其他配置 TIM_ARRPreloadConfig(TIM4,ENABLE); TIM_OC3PreloadConfig(TIM4,TIM_OCPreload_Enable); //启动定时器 TIM_Cmd(TIM4,ENABLE); /*---------------------------------------------------------------------------------------------*/

这的话我们给定时器4周期设置为5ms,根据TIM4挂载APB1总线,时钟频率72MHz,由公式0.005=arr*psc/72,000,000,我们假定arr为1000,速度0-1000就对应arr的0-1000(方便不用计算),所以psc为2,在初始化时配置即可;左边轮子的电机同理;
这样我们就能在单片机的IO口上配置高低电平来控制转动了,比如AIN1接PB1,AIN2接PB0,PB1置高,PB0置低就能正转,反之反转;
同时加上我们控制TIM4产生的PWM波,设置一个期望速度来设置高电平占空比:

1
void Moter_RightControl(int speed){ int temp =speed; //限幅 if(temp<-1000) temp=-1000; if(temp> 1000) temp= 1000; if(temp<0) { AIN1=1; AIN2=0; temp=-temp; } else if(temp>0){ AIN1=0; AIN2=1; } else{ AIN1=0; AIN2=0; } TIM_SetCompare3(TIM4,(u16)temp); }

这样我们调用这个函数参数为速度就能控制电机速度和转向了;

2.能够转向:控制舵机转动

查询MG995舵机工作原理

> 1.MG995舵机简介

产品型号 MG995 产品尺寸 40.7_19.7_42.9mm 产品重量 55g 工作扭矩 13KG/cm 反应转速
53-62R/M 使用温度 -30~+60° 死区设定 4微秒 插头类型 JR、FUTABA通用 转动角度 最大180度
舵机类型 模拟舵机 工作电流 100mA 使用电压 3-7.2V 结构材质 金属铜齿、空心杯电机、双滚珠轴承 无负载 操作速度
0.17秒/60度(4.8V);0.13秒/60度(6.0V) 附件包含 舵盘、线长 30CM、固定螺钉、减振胶套及铝套等附件 适用范围 1:10和1:8平跑车、越野车、卡车、大脚车、攀爬车、双足机器人、机械手、遥控船,适合50级-90级甲醇固定翼飞机以及26cc-50cc汽油固定翼飞机等模型。

在这里插入图片描述

2.舵机接线
舵机上有三根线,分别为VCC、GND、信号线。控制信号一般要求周期为20ms的PWM信号。VCC、GND需要另外接驱动给舵机供电,而且得和开发板共地。
在这里插入图片描述

中间的永远是电源正极。

3.控制原理*

舵机的控制一般需要一个20ms的时基脉冲,该脉冲的高电平部分一般为0.5ms~2.5ms范围内的角度控制脉冲部分。以180度角度舵机为例,那么对应的控制关系是这样的:

0.5ms————–0度; 1.0ms————45度; 1.5ms————90度;
2.0ms———–135度; 2.5ms———–180度;

由资料显示,舵机的接线方式很简单,只需要一个周期为20ms的PWM信号接口就行,我们也找一个有定时器复用功能的IO口初始化一下,配置周期20ms:0.02=arr*psc/72,000,000,先设置arr为10000,算出psc;由舵机的对应的控制关系:
0度的占空比为0.5/20=0.025;90度的占空比为1.5/20=0.075;
180度的占空比为2.5/20=0.125;所以0度-180度对应arr的250-1250,其他值不能用(TIM_SetCompare2(TIM2,temp);中的temp不能等于这个范围以外的值)。

1
//初始化定时器 TIM_InitStructure.TIM_Period = arr-1; TIM_InitStructure.TIM_Prescaler = psc-1; TIM_InitStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_InitStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2,&TIM_InitStructure); //定时器通道 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInitStructure.TIM_Pulse = 750; //1.5ms 0-10000 --->250-1250 TIM_OCInitStructure.TIM_OutputState = ENABLE; TIM_OC2Init(TIM2,&TIM_OCInitStructure); //使能预装载 TIM_ARRPreloadConfig(TIM2,ENABLE); //在定时器工作时改变预分频器的值 时基模块 TIM_OC2PreloadConfig(TIM2,TIM_OCPreload_Enable); //通道 //使能定时器 TIM_Cmd(TIM2,ENABLE); } //舵机角度控制 //param : 0~180 void Servo_setAngle(u8 angle) { u16 temp = 0; temp = angle; //限幅 if(temp<0) temp = 0; if(temp>180) temp = 180; temp = (1250-250)/180*temp+250; TIM_SetCompare2(TIM2,temp); } /*==========================================================*/

配置好舵机,我们就能使用角度控制函数了。

来到主函数,初始化

1
Servo_Init(10000,144);//周期20ms//舵机初始化 Moter_RightInit(1000,2);//电机初始化

调用

1
Servo_setAngle(90) Moter_RightControl(200); Moter_LeftControl(200);

小车就能以200的速度在90度方向行驶;
*

3.让小车自动巡线:解析红外模块数据*

选择模块:红外循迹模块有很多种,这里我们选用最多的五路来控制,实物图如下:
在这里插入图片描述

参数搜得到: 在这里插入图片描述

可以看到他有7个引脚,一个vcc一个gnd,5个传感器,我们只需要在单片机上设置5个IO口读取这五个传感器返回的值即可;选5个IO口为传感器配置引脚为输入模式,读取数据,如果输入为0,则代表识别到黑线,根据这个我们就能写出方向控制函数:

1
u8 direction=GO_STRAIGHT; //返回应该的转弯方向 u8 LineFollow(void){ if((L1==0&&R1==0)||(L2 == 0 || L1 == 0)) { direction = GO_LEFT_2; return GO_LEFT_2; } else if(R1 == 0 || R2 == 0){ direction = GO_RIGHT_2; return GO_RIGHT_2; } else if(M0 == 0 &&L1==1&&R1==1&&L2==1&&R2==1) { direction=GO_STRAIGHT; return GO_STRAIGHT; } else { return direction; } return direction; } /*-=----------------------------------------------------------------*/

在主函数解析返回值,进而设置方向即可;

1
switch(LineFollow()){ case 0: Servo_setAngle(170); break; case 1: Servo_setAngle(150); break; case 2: Servo_setAngle(90); break; case 3: Servo_setAngle(40); break; case 4: Servo_setAngle(30); break; }

以上,我们已经能够让小车实现最基本的自动巡线。接下来加上附加功能,用串口发送指令,让小车按指令前进。

4.遥控小车:HC-05蓝牙通信技术

同样的网上先看看怎么用的:

一、说明
蓝牙传输模块一般通过串口进行通信,即RS232(设备1)<—>蓝牙模块<—>蓝牙模块<—>RS232(设备2)。因此,使用蓝牙模块需要配置的参数有串口通信参数和蓝牙通信参数。HC05蓝牙模块采用的AT配置命令进行配置。
二、数据格式 HC-05只支持一种数据格式: 数据位8 位,停止位1 位,无校验位,无流控制。波特率要选择正确,
原始模式是38400和正常模式是9600。AT命令后面需要换行,然后点发送命令才有效。 三、AT命令配置方法
按住按键或EN脚拉高,此时灯是慢闪,进入AT命令模式,默认波特率是38400。原始模式下一直处于AT命令模式状态。
四、AT主要的命令AT+RESET HC-05复位 AT+VERSION? 返回HC-05的软件版本号 AT+UART? 返回蓝牙波特率
AT+UART=115200,1,2 设置串口波特率115200,2位停止位,偶校验 AT+NAME=BLUE 修改蓝牙模块的名字为BLUE
AT+ORGL 恢复出厂默认设置 AT+NAME? 返回HC-05的名字 AT+PSWD? 查询配对密码
AT+PSWD=”1234” 设置密码1234 AT+ROLE? AT+ROLE=1 ?: 查询主从状态
=1:设置成主
=0:设置成从
=2:设置成回环 波特率设置的规则如下: AT+UART=,, param1: 波特率 param2: 停止位, 0=1位,1=2位 param3: 校验位, 0=无校验,1=奇校验,2=偶校验 默认设置为9600,0,0

可以得知蓝牙通信需要配置串口,将蓝牙TX,RX连接到单片机串口的RX,TX上就可以通信了;在这之前要配置蓝牙:
用下面的ttl_usb转换模块连接上蓝牙,
在这里插入图片描述

长按蓝牙上的按钮然后再上电(进入AT模式),打开串口助手,发送指令
主:AT+ROLE=1 AT+PSWD=”xxxx” AT+UART=115200,0,0
从:AT+ROLE=0 AT+PSWD=”xxxx” AT+UART=115200,0,0
(主要配置这三个,其他的配置这里可以不用)
分别配置好一主一从两个蓝牙后,然后给他们配置单片机上的IO口,找到有串口复用功能的引脚,配置USART:
在这里插入图片描述

我们选择串口一进行配置:

1
void Usart1_init(u32 bound) { //GPIO端口设置 GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1| RCC_APB2Periph_GPIOA, ENABLE); //USART1_TX GPIOA.9 GPIO_InitStructure.GPIO_Pin=GPIO_Pin_9; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); //Usart1 NVIC 配置 NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); //USART 初始化设置 USART_InitStructure.USART_BaudRate=bound; USART_InitStructure.USART_WordLength=USART_WordLength_8b; USART_InitStructure.USART_StopBits=USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, &USART_InitStructure); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); USART_Cmd(USART1, ENABLE); }

在这里我们要思考当串口发送什么数据的时候小车才能响应,因此我们要做一个简单的通信协议,如下:
串口接收或发送数据的格式为: CAR:[控制命令?]=[小车模式],[方向],[速度]
声明如下变量:

1
float CarMode =2;//小车模式 0遥控模式 1巡线模式 2定时模式 float Direction =90;//舵机方向 float Speed_USART =0;//串口设置的速度

这样,若要控制小车运动,模式为遥控,方向90,速度100,该指令为:

CAR:SPO=0,90,100

(若要控制小车PID,p为3,i为1,d为0,该指令为:CAR:PID=3,1,0 )
串口通信需要给他配置中断,写出如下中断函数和接收数据的函数:

1
char USART_ReadBuff[30]; u8 USART_ReadOK=0; char opv[2]; void USART1_IRQHandler(void) //串口1中断服务程序 { u8 temp=0; static u8 count =0; if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断 { temp= USART_ReceiveData(USART1); //读取接收到的数据 //USART_SendData(USART1,temp); USART_ReadBuff[count++]=temp; USART_ITConfig(USART1, USART_IT_IDLE,ENABLE); } if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) //空闲帧 { USART_ReadOK=1; count=0; //USART_SendData(USART1,'A'); USART_ClearITPendingBit(USART1, USART_IT_IDLE); USART_ITConfig(USART1, USART_IT_IDLE,DISABLE); } } u8 USART_GetData(char* cmd,float* D1,float* D2,float* D3){ u8 flag=0; if(USART_ReadOK==1){ if(sscanf(USART_ReadBuff,"CAR:%3s=%f,%f,%f",cmd,D1,D2,D3)>=2){ flag=1; } USART_ReadOK=0;//清除接收完成标志 memset(USART_ReadBuff,0,sizeof(USART_ReadBuff));//清除数组 } return flag; } void USART1_SendByte(u8* addr,int size){ while(size--) { while(USART_GetFlagStatus(USART1,USART_FLAG_TC) == RESET);//等待发送缓存区完成 USART_SendData(USART1,*addr); addr++; } }

在主函数判断USART_GetData(buff,&data1,&data2,&data3)==1是否接收到数据,如果=1,解析数据:

1
char buff[6]={0}; float data1,data2,data3; if(USART_GetData(buff,&data1,&data2,&data3)==1){ if(strcmp(buff,"PID")==0){ .......... } else if(strcmp(buff,"SPO")==0){ CarMode=data1; Direction=data2; Speed_USART=data3; } }

做完这些,小车的主要功能就都有了,接下来就是加上辅助性的功能了;

5.小车速度精准控制:编码器数据采集

要想精准控制小车的速度,就必须知道速度的实际值,本小车编码器采用的编码器原理图:

在这里插入图片描述

同样网上查询资料编码器怎么用(我随便复制的,肯定还有更合适的):

编码器(encoder)是将信号(如比特流)或数据进行编制、转换为可用以通讯、传输和存储的信号形式的设备。
光电编码器如果按信号原理来分类的话,可以分为增量型编码器和绝对型编码器。旋转编码器是一种光电式旋转测量装置,它将被测的角位移直接转换成数字信号(高速脉冲信号)。因此可将旋转编码器的输出脉冲信号直接输入给PLC,利用PLC的高速计数器对其脉冲信号进行计数,以获得测量结果。
  编码器接线原理
  我们通常用的是增量型编码器,可将旋转编码器的输出脉冲信号直接输入给PLC,利用PLC的高速计数器对其脉冲信号进行计数,以获得测量结果。不同型号的旋转编码器,其输出脉冲的相数也不同,有的旋转编码器输出A、B、Z三相脉冲,有的只有A、B相两相,最简单的只有A相。
  编码器有5条引线,其中3条是脉冲输出线,1条是COM端线,1条是电源线(OC门输出型)。编码器的电源可以是外接电源,也可直接使用PLC的DC24V电源。电源“-”端要与编码器的COM端连接,“+
”与编码器的电源端连接。编码器的COM端与PLC输入COM端连接,A、B、Z两相脉冲输出线直接与PLC的输入端连接,A、B为相差90度的脉冲,Z相信号在编码器旋转一圈只有一个脉冲,通常用来做零点的依据,连接时要注意PLC输入的响应时间。旋转编码器还有一条屏蔽线,使用时要将屏蔽线接地,提高抗干扰性。
  编码器———–PLC   A—————–X0   B—————–X1
  Z——————X2   +24V————+24V   COM————-
-24V———–COM   增量式编码器转轴旋转时,有相应的脉冲输出,其计数起点任意设定,可实现多圈无限累加和测量。编码器轴转一圈会输出固定的脉冲,脉冲数由编码器光栅的线数决定。需要提高分辩率时,可利用
90 度相位差的 A、B 两路信号进行倍频或更换高分辩率编码器。   绝对式光电编码器与单片机怎么接线
  绝对式光电编码器有很多种接口,现在比较常见的是串行同步接口,也就是符合RS422电平标准的时钟数据接口,其时钟线通常有+,-
一组,数据线+,-
一组,如与单片机连接的话,最好是选用带有SPI功能的单片机,把单片机的SPI的时钟输出和数据输入分别用422电平转换芯片转换成差分信号后与编码器连接,当然也可以用普通单片机IO口模拟SPI时序,不过这样做的话程序上处理相当麻烦,最好不用。
  NPN开路输出,又叫OC输出。   需要在A、B端分别外接一个电阻,电阻上端的电压由你的电路决定:
  单片机接5V,PLC接24V,使用就很方便了。
  检测A、B信号就是(1)检测脉冲数量;(2)A、B谁在前,谁在后。A相上升沿在前(出现高电平)表示编码器正转;反之B在前,表示反转。
  至于45°,就看编码器一周有多少脉冲,自己分配了。   PLC与旋转编码器的接线图
  旋转编码器是一种光电式旋转测量装置,它将被测的角位移直接转换成数字信号(高速脉冲信号)。因此可将旋转编码器的输出脉冲信号直接输入给plc,利用PLC的高速计数器对其脉冲信号进行计数,欧姆龙触摸屏,以获得测量结果。

在这里插入图片描述

(旋转编码器与plc的链接图)   如图所示是输出两相脉冲的旋转编码器与FX2N系列PLC的连接示意图。
  编码器有4条引线,其中2条是脉冲输出线,1条是COM端线,1条是电源线。
  编码器的电源可以是外接电源,也可直接使用PLC的DC24V电源。电源“-”端要与编码器的COM端连接,“+ ”与编码器的电源端连接。
  编码器的COM端与PLC输入COM端连接,A、B两相脉冲输出线直接与PLC的输入端连接,连接时要注意PLC输入的响应时间。有的旋转编码器还有一条屏蔽线,使用时要将屏蔽线接地。
不同型号的旋转编码器,其输出脉冲的相数也不同,有的旋转编码器输出A、B、Z三相脉冲,有的只有A、B相两相,最简单的只有A相。

上面的资料笔者也没有看懂所有,根据本小车需要的,是AB双相脉冲的,当转动轮子时,示波器波形大概如下:
在这里插入图片描述

正转 在这里插入图片描述
反转
根据正转波形分析,当A相下降沿时,B相是低电平,当A相上升沿时,B相是高电平,两个上升/下降沿刚好隔着一个周期,电机每转一圈转化为A、B相出现一次上升/下降沿。反转正好相反;
因此如果我们规定A相上升沿时,判断B相的电平为高电平还是低电平就能知道轮子是正转还是反转;并且跳变一次就是轮子转一圈,由此还可以获得小车位移的长度(根据轮子周长)。
对单片机来说,每次跳变就要记一次数,所以需要配置中断来计数,初始化如下:

1
void Encode_Init(void){ GPIO_InitTypeDef GPIO_InitStructure; EXTI_InitTypeDef EXTI_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA| RCC_APB2Periph_GPIOB,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); //编码器AB相 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_Init(GPIOB, &GPIO_InitStructure); //映射中断线到IO //左轮A相 PA7 右轮A相 PB6 GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource6); GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource7); EXTI_InitStructure.EXTI_Line=EXTI_Line6; EXTI_InitStructure.EXTI_Mode=EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger=EXTI_Trigger_Falling; EXTI_InitStructure.EXTI_LineCmd=ENABLE; EXTI_Init(&EXTI_InitStructure); EXTI_InitStructure.EXTI_Line=EXTI_Line7; EXTI_Init(&EXTI_InitStructure); NVIC_InitStructure.NVIC_IRQChannel=EXTI9_5_IRQn ; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0; NVIC_InitStructure.NVIC_IRQChannelSubPriority=0; NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE; NVIC_Init(&NVIC_InitStructure); } int Encode_Right=0; int Encode_Left=0; void EXTI9_5_IRQHandler(void){ if(EXTI_GetFlagStatus(EXTI_Line6)!=RESET){ if(PAin(7)==0) Encode_Left--; //反转 else Encode_Left++;//正转 EXTI_ClearITPendingBit(EXTI_Line6); } if(EXTI_GetFlagStatus(EXTI_Line7)!=RESET){ if(PBin(6)==0) Encode_Right--; else Encode_Right++; EXTI_ClearITPendingBit(EXTI_Line7); } }

获取到编码器记下的圈数后,还需要把他们转换成位移和速度我们才能用:

1
void Encode_GetCode(int *LC,int *RC) { *LC = Encode_Left; *RC = Encode_Right; } // 知道轮子转一圈的编码值 EX = 15000 // 知道轮子的周长 C 225mm // encode / EX *C = position 99999/15000*225 void Encode_GetPosition(float *LP,float *RP) { *LP = (float)Encode_Left/15000*201.061929; // 单位mm *RP = (float)Encode_Right/15000*201.061929; // 单位mm }

计算速度光有位移还不行,还需要一定时间,所以我们需要一个定时器定时计算速度,这里先作为参数使用上;在配置好后加进去。

1
void Encode_GetSpeed(float *LS,float *RS,float time){ static float lastleft_s=0,lastright_s=0; float left_s,right_s; Encode_GetPosition(&left_s,&right_s); *LS = (left_s-lastleft_s)/time; // mm/s *RS = (right_s-lastright_s)/time; // mm/s lastleft_s = left_s; lastright_s = right_s; }

现在来配置一个定时器,周期我们也配置成20ms,这样和之前舵机一样不用再计算而且这个周期也很合理;(代码略)
在定时器中断函数中就是需要计算出定时器响应的时候速度值了:

1
void TIM3_IRQHandler(void){ if(TIM_GetITStatus(TIM3,TIM_IT_Update)!=RESET){ //功能函数 Encode_GetSpeed(&Speed_MeasureL,&Speed_MeasureR,0.02); //printf("Speed_MeasureL=%f,Speed_MeasureR=%f\r\n",Speed_MeasureL,Speed_MeasureR); TIM_ClearITPendingBit(TIM3,TIM_IT_Update); } }

以上获取了实际的速度。
因为有摩擦力,惯性等因素的存在,实际速度与期望的速度肯定不相同,要想把一个期望速度在实际中体现出来,就需要用到PID算法。

6.小车速度精准控制:PID算法

什是pid算法?
PID:输入一个期望值,一个实际测量的值,PID会产生一个控制量让系统达到期望值。输出的值作用到PWM波(对小车来说:pwm→加速度→速度→产生位移)。
由V=V0+(1/2)_a_t*,这里的pwm→加速度 的过程不知道具体关系,所以简化成pwm→速度→产生位移

在这里插入图片描述

(绿色部分为按位移算,这里没用到)

在这里插入图片描述

Ev :期望速度 Mv:测量速度 Err=Mv-Ev:误差 参数P*误差 I 误差的累加 D(误差-上一次误差)

输出值out=P*误差 + I 误差的累加 + D(误差-上一次误差)

在这里插入图片描述

p与out值成正比,p越大启动越快(太快会震荡),确定一个P值后输出值到一定值会产生稳态误差,比如:
当p稳定时,输出值稳定,产生的加速度稳定,理想状态下加速度为零时匀速运动,但摩擦力在所难免,算出的加速度0=a=(ma-f)/m,此时实际速度没有达到期望值也不变。

原理大概如上,可能有些表达不清楚,具体些需要详细学了PID算法才能说明了;
该小车的PID算法如下:

1
typedef struct { float p; float i; float d; float ErrLimit; float IntegLimit; float OutputLimit; float ErrInteg; float LastErr; }_PID; float PID(_PID *pid,float expect,float measure){ float Err=expect-measure; float output=0; //误差限幅 if(Err < -pid->ErrLimit) Err = - pid->ErrLimit; else if(Err > pid->ErrLimit) Err = pid->ErrLimit; output=pid ->p * Err;//比例相 pid->ErrInteg+=Err;//误差积分 //积分限幅 if(pid->ErrInteg<-pid->IntegLimit){ pid->ErrInteg = -pid->IntegLimit; } if(pid->ErrInteg>pid->IntegLimit){ pid->ErrInteg = pid->IntegLimit; } output+=pid ->i*pid ->ErrInteg;//积分相 output+=pid ->d*(Err-pid ->LastErr);//微分相 //输出限幅 if(output < -pid->OutputLimit){ output = -pid ->OutputLimit; } if(output > pid ->OutputLimit){ output = pid ->OutputLimit; } pid->LastErr = Err;//保存误差值,用于下一次计算 return output; } //由于笔者对PID还是第一次接触,多的还不懂。

小尝试一

7.小车遥控升级版:mpu6650陀螺仪

在我们生活中,有一些赛车游戏可以感知手机的俯仰,横移,翻滚来控制游戏里的小车进行转向,加速等操作,他们用到的技术就是陀螺仪,先来了解下:

在这里插入图片描述

要做到这个还需要另外一块开发板,将陀螺仪固定在上面,做一个类似的遥控装置,我们还是使用蓝牙通信,先给开发板初始化并配置串口,让他可以发送数据即可;初始化陀螺仪接口…关于IIC协议我只知道一点原理,具体使用在这里并没有体现,使用的正点原子提供的MPU6050整个文件包,哈哈(我不讲了不讲了讲不了)。
不过这个人讲的挺好的:https://blog.csdn.net/muchunpeng/article/details/98311161

小尝试二

8.小车巡线升级版:openmv摄像头识别

我们让小车智能起来,能够识别交通灯,在openmv星瞳科技官网查看openmv相关信息:https://singtown.com/openmv/
在这里插入图片描述

这里我们还是初始化小车的开发版的USART3串口,用串口输出来实现。连线如上图。用之前的TTL转USB模块将它连接到电脑,下载它的程序还需要上位机软件OPENMV IDE 同样在上面的网站有,安装后会有很多例程学习可以看(https://book.openmv.cc/quick-starter.html),本次我们就利用颜色识别的例程来实现,他在这里: 在这里插入图片描述

点击copy,我们进入IDE把代码放进去,现在只需要开启他的串口并且发送我们小车能够识别的信息(我们自定义的那个协议)就行了。(我是这样整的,其实还可以传别的参数,我为方便解析)。由于这个IDE是python编程写的,所以需要一点相关知识。

我改成了这样:

1
import sensor, image, time, math import json import time from pyb import UART #导入包 uart = UART(3,115200)#配置uart对象 uart.init(115200, bits=8, parity=None, stop=1) thresholds_red = [(30, 100, 15, 127, 15, 127)] #红色色域识别,这个可以专门测出来实际想要的那个颜色,我这里直接用的例程的红色 thresholds_blue=[(30, 100, -64, -8, -32, 32)] sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.skip_frames(time = 2000) sensor.set_auto_gain(False) sensor.set_auto_whitebal(False) clock = time.clock() while(True): clock.tick() img = sensor.snapshot() for blob in img.find_blobs(thresholds_red, pixels_threshold=200, area_threshold=200): uart.write("CAR:OPV=0,90,100")#直接传一整条指令 time.sleep(1.5)#延时的原因是防止串口一直发送数据,处理数据的串口进不了中断,没法实现。 for blob in img.find_blobs(thresholds_blue, pixels_threshold=200, area_threshold=200): uart.write("CAR:OPV=1,90,100") time.sleep(1.5)

在小车单片机上加上解析命令并做出反应的相关代码即可;

这个博客主要还是想理清楚这个小项目的开发思路。提炼出这当中涉及到的技术:

项目经验与收获: 学会了HC-05蓝牙模块的调试/无线遥控技术; 编码器数据采集/编码器数据转换成位移和速度;
学会了红外传感器循迹模块的使用/红外巡线技术; 了解了PID算法实现,调试,舵机控制方法; 了解了mpu6050陀螺仪的使用方法;
理解程序设计技巧思路;

使用的硬件: STM32F103RCT6; 电机驱动:TB6612FNG模块; 舵机:MG995 TTL转USB模块 陀螺仪:MPU6050
GY521 OPENMV 其他:电机,小车模型等