目录
一、前言
2024年全国电赛也是落下帷幕了,各省获奖名单陆续出炉,不知道大家有没有拿到满意的成绩。电赛总是充满遗憾的,但只要有所收获,就不算白来。电赛的故事不是三言两语能概括的,但我始终相信学无止境,因而在此将参赛方案整理并开源,也希望能和各位读者共同探讨,共同进步。
二、赛题分析
2024年H题自动行驶小车,题目如下:
相较于往年的小车题,今年的题目其实是偏简单的,难点主要有两个:
1、限定使用TI MSPM0系列的板子
2、在不使用摄像头的情况下实现白色区域的自动寻路
对于第一个问题,我向大家推荐两个解决方案:
链接一是B站UP主Torris的入门视频,讲的非常清晰,推荐给大家
链接二是南极熊学长整理的M0资料,非常全面,后续我也会整理一份G3507的学习笔记供大家交流学习
对于第二个问题,我使用的解决方案是使用高精度陀螺仪模块(MPU6050、JY61P、GY25等),通过巡航角来判断小车的行驶方向。
解决了这两个最大的难题之后,就可以根据题目出一个大致方案了。
三、方案框架
这里我使用的陀螺仪是JY61P,不推荐大家使用MPU6050,零漂太严重,想用的话需要用算法去零漂,这里就不过多介绍了。
思路很简单,有线靠灰度寻线,没线靠巡航角找角度。
接下来我会对各个模块的代码进行简单的整理讲解,文章本意是与大家交流,如有不足之处也希望大家多多包涵。
文末代码开源,可根据代码分析学习。
四、电机驱动
1、PWM波配置
其中,PWM Period Count就是定时器计数周期的计数值,也就是重装载值,相当于32中的ARR。
Calculated PWM Frequency为系统计算出的PWM实际输出频率,驱动电机建议在15k~20k之间。
我们需要两个PWM波,所以开启了两个通道,计数模式对我们影响不大,这里选择向下计数。
通道1设置与通道0完全一致。
2、IN1、IN2配置
其余三个引脚配置均相同,仅需配置IO口名称及模式。
3、自定义函数用以修改PWM波占空比
/********************************************
函数功能:修改PWM波占空比
参数: duty:占空比0~100
channel:通道选择
返回值:无
********************************************/
void set_Duty(uint8_t duty,uint8_t channel)
{
uint32_t CompareValue;
CompareValue = 2500 - 2500/100*duty; //占空比转换
if(channel == 0)
{
DL_Timer_setCaptureCompareValue(PWM_0_INST,CompareValue,DL_TIMER_CC_0_INDEX);
}
else if(channel == 1)
{
DL_Timer_setCaptureCompareValue(PWM_0_INST,CompareValue,DL_TIMER_CC_1_INDEX);
}
}
这里我选择直接直接输入占空比,将其转换为Counter Compare Value。
注意,占空比转换公式中的2500须等于PWM波配置中的PWM Period Count值。
4、自定义函数用以驱动电机
/********************************************
函数功能:输入PWM波占空比驱动电机
参数: motor_left:左轮目标占空比
motor_right:右轮目标占空比
返回值:无
********************************************/
void Set_Pwm(int motor_left,int motor_right)
{
if(motor_left > 0) //前进
{
AIN10;
AIN21;
}
else //后退
{
AIN11;
AIN20;
}
set_Duty(myabs(motor_left),1);
if(motor_right > 0) //前进
{
BIN10;
BIN21;
}
else //后退
{
BIN11;
BIN20;
}
set_Duty(myabs(motor_right),0);
}
其中AIN11、AIN10等均为宏定义,就是根据输入转速值的正负将引脚置高或置低。
这样电机驱动部分就完成了,现在就可以让小车跑起来了。
五、编码器读取
编码器的数值的读取是小车控制中最重要的一环,我们需要根据编码器的返回值判断电机是否到达了我们需要的转速。
1、编码器IO口配置
两个电机各须两个引脚,因此我们开启两个引脚组分别读取两个电机的编码器数值。
这里开启引脚的外部中断用以读取编码器电平,其余三个引脚同理。
2、定时中断配置
完成引脚配置后,我们只能读取编码器高低电平的转换,因此我们还需要一个定时器用以读取一段时间内的编码器数值。
这里我们选择50ms的计数周期,建议周期设置为20~50ms。
3、中断读取编码器
这里使用四分频的编码器读取方式,对四分频读取不了解的同学可以看南极熊学长的博客
/********************************************
函数功能:中断读取编码器
参数:无
返回值:无
********************************************/
void GROUP1_IRQHandler(void) //中断服务函数
{
/**********************编码器读取***********************/
gpioA = DL_GPIO_getEnabledInterruptStatus(GPIOA,GPIO_EncoderA_PIN_0_PIN | GPIO_EncoderA_PIN_1_PIN);
gpioB = DL_GPIO_getEnabledInterruptStatus(GPIOB,GPIO_EncoderB_PIN_2_PIN | GPIO_EncoderB_PIN_3_PIN);
if((gpioA & GPIO_EncoderA_PIN_0_PIN) == GPIO_EncoderA_PIN_0_PIN)
{
//Pin0上升沿
if(!DL_GPIO_readPins(GPIOA,GPIO_EncoderA_PIN_1_PIN))//P1为高电平
{
gEncoderCount_left--;
}
else//P1为低电平
{
gEncoderCount_left++;
}
}
else if((gpioA & GPIO_EncoderA_PIN_1_PIN) == GPIO_EncoderA_PIN_1_PIN)
{
//Pin1上升沿
if(!DL_GPIO_readPins(GPIOA,GPIO_EncoderA_PIN_0_PIN))//P0为高电平
{
gEncoderCount_left++;
}
else//P1为低电平
{
gEncoderCount_left--;
}
}
if((gpioB & GPIO_EncoderB_PIN_2_PIN) != 0)
{
if(!DL_GPIO_readPins(GPIOB,GPIO_EncoderB_PIN_3_PIN))
{
gEncoderCount_right--;
}
else
{
gEncoderCount_right++;
}
}
else if((gpioB & GPIO_EncoderB_PIN_3_PIN) != 0)
{
if(!DL_GPIO_readPins(GPIOB,GPIO_EncoderB_PIN_2_PIN))
{
gEncoderCount_right++;
}
else
{
gEncoderCount_right--;
}
}
DL_GPIO_clearInterruptStatus(GPIOA, GPIO_EncoderA_PIN_0_PIN|GPIO_EncoderA_PIN_1_PIN);
DL_GPIO_clearInterruptStatus(GPIOB, GPIO_EncoderB_PIN_2_PIN|GPIO_EncoderB_PIN_3_PIN);
}
其中,gEncoderCount_left及gEncoderCount_right均为全局变量,用以计数编码器数值。
4、定时器定时读取编码器数值
每50ms读取一次外部中断中的编码器计数值并将计数值清零
我们得到了电机的转速,接下来就可以对小车进行控制了。
六、陀螺仪巡航角获取
我们使用的陀螺仪JY61P可以直接通过串口获取返回值,这里我们选择用串口中断,想提高传输效率也可以选择使用DMA。
代码用的是官方提供的例程,这里就不贴了,大家可以直接在工程中查看。
七、八路灰度返回值读取
灰度返回值为高低电平,直接用IO口读取即可。
这里我们直接使用宏定义读取返回值。
#define P1 DL_GPIO_readPins(GPIO_Gray_PIN_Gray_1_PORT,GPIO_Gray_PIN_Gray_1_PIN)
#define P2 DL_GPIO_readPins(GPIO_Gray_PIN_Gray_2_PORT,GPIO_Gray_PIN_Gray_2_PIN)
#define P3 DL_GPIO_readPins(GPIO_Gray_PIN_Gray_3_PORT,GPIO_Gray_PIN_Gray_3_PIN)
#define P4 DL_GPIO_readPins(GPIO_Gray_PIN_Gray_4_PORT,GPIO_Gray_PIN_Gray_4_PIN)
#define P5 DL_GPIO_readPins(GPIO_Gray_PIN_Gray_5_PORT,GPIO_Gray_PIN_Gray_5_PIN)
#define P6 DL_GPIO_readPins(GPIO_Gray_PIN_Gray_6_PORT,GPIO_Gray_PIN_Gray_6_PIN)
#define P7 DL_GPIO_readPins(GPIO_Gray_PIN_Gray_7_PORT,GPIO_Gray_PIN_Gray_7_PIN)
#define P8 DL_GPIO_readPins(GPIO_Gray_PIN_Gray_8_PORT,GPIO_Gray_PIN_Gray_8_PIN)
那么我们就得到了八路灰度的返回值,这也是巡线的基础。
八、PID速度环与差速环
1、速度环
速度环的作用是使电机到达指定转速,在实际中,占空比相同的PWM波在不同电机上的转速可能会有差异,速度环可以减小电机本身带来的影响。
这只是最基础的PID用法,本工程也只使用到了P环,这里就不详细解释了。
2、差速环
有了速度环,我们还需要一个差速环来给左右电机一个差速,这样才能让小车转向。
灰度循迹的差速环是根据检测到黑色的位置给电机差速,比如最右侧的光电检测到了黑色,说明小车偏左,那么就给左轮一个正向差速,给右轮一个反向差速,让小车回到轨道上。
/********************************************
函数功能:差速环返回值
参数: 无
返回值:差速值
********************************************/
int xunji(void) //输出差速,左轮速度为middle+x,右轮速度为middle-x
{
if(P4!=0) return -8;
else if(P5!=0) return 8;
else if(P3!=0) return -12;
else if(P6!=0) return 12;
else if(P2!=0) return -16;
else if(P7!=0) return 16;
else if(P1!=0) return -28;
else if(P8!=0) return 28;
return 0;
}
陀螺仪的差速环我选择直接使用巡航角的数值,因为陀螺仪的返回值是-180~180,所以在0附近时巡航角本身就存在正负,同时因为我选择的PWM改变范围是0~100,所以我们不需要对偏差值进行转换,直接作为差速输入给速度环即可,下面是代码示例。
if (n%2==0)
{
if(Yaw<0) bias = 180 - myabs(Yaw); //当走过白色区域为偶数时,须将+180和-180转化为0+和0-
else bias = Yaw - 180;
}
else if(n%2==1)
{
bias = Yaw; //当走过白色区域为奇数时,不需要转化
}
3、将差速环加入速度环
Speed_Middle为基准速度值,也就是走直线时的速度,在基准上加上差速环返回值,在将目标值targetA和targetB输入速度环,从而控制小车循迹及走直线。
targetA = Speed_Middle+bias;
targetB = Speed_Middle-bias;
CurrentA = (float)gEncoderVal_left/3; //left
CurrentB = (float)gEncoderVal_right/3; //right
Motor_Left = (int)PWM_Limit(PID_A(CurrentA,targetA),Limit, -Limit);
Motor_Right = (int)PWM_Limit(PID_B(CurrentB,targetB),Limit, -Limit); //PWM限幅
Set_Pwm(Motor_Left, Motor_Right);
速度环及差速环的完成,意味着小车已经可以巡线和走直线了,那么接下来就可以按照题目要求让小车动起来了。
九、运动模式整合
1、模式一(AB)
第一问走直线,只需要用到陀螺仪。
其中ledbegin及ledflag用于声光模块,使用定时中断使声光运行500ms。
void Control_AB(void) //模式一
{
int Motor_Left, Motor_Right; //电机赋值
float bias; //差速值
bias = Yaw; //直接将陀螺仪偏差值作为差速值
if ((P1!=0 || P2!=0 || P3!=0 || P4!=0 || P5!=0 || P6!=0 || P7!=0 || P8!=0)&&a==0) //任一光电检测到黑色就停止
{
DL_GPIO_clearPins(GPIO_STBY_PORT,GPIO_STBY_PIN_STBY_PIN);
ledbegin=1;
a=1; //停止标志位,判断是否已经停止,使此if仅进入一次
}
if (ledflag==1) DL_GPIO_setPins(GPIO_LED_PORT,GPIO_LED_PIN_LED_PIN); //声光模块驱动判断
else DL_GPIO_clearPins(GPIO_LED_PORT,GPIO_LED_PIN_LED_PIN);
targetA = Speed_Middle+bias; //基于基准值给电机差速
targetB = Speed_Middle-bias;
CurrentA = (float)gEncoderVal_left/3; //左轮当前转速
CurrentB = (float)gEncoderVal_right/3; //右轮当前转速
Motor_Left = (int)PWM_Limit(PID_A(CurrentA,targetA),Limit, -Limit);
Motor_Right = (int)PWM_Limit(PID_B(CurrentB,targetB),Limit, -Limit); //PWM限幅
Set_Pwm(Motor_Left, Motor_Right);
}
2、模式二(ABCDA)
第二问需要结合灰度和陀螺仪,根据返回值切换模式并寻路。
void Control_ABCDA(void) //模式二
{
int Motor_Left, Motor_Right; //电机赋值
float bias; //差速值
if(m==6) //m为模式切换计数值,用于判断是否跑完全程
{
DL_GPIO_clearPins(GPIO_STBY_PORT,GPIO_STBY_PIN_STBY_PIN);
}
if (ledflag==1) DL_GPIO_setPins(GPIO_LED_PORT,GPIO_LED_PIN_LED_PIN); //声光模块驱动判断
else DL_GPIO_clearPins(GPIO_LED_PORT,GPIO_LED_PIN_LED_PIN);
if (P1==0 && P2==0 && P3==0 && P4==0 && P5==0 && P6==0 && P7==0 && P8==0) timebegin=1; //给小车一个出弯延时,使转向时小车中心位于点上
else whiteflag=0;
if (whiteflag==1) //在白色区域时,小车根据陀螺仪返回值走直线
{
ledflag2=0;
if(ledflag1==0)
{
ledflag1=1;
ledbegin=1;
m++;
}
if (n%2==0)
{
if(Yaw<0) bias = 180 - myabs(Yaw); //当走过白色区域为偶数时,须将+180和-180转化为0+和0-
else bias = Yaw - 180;
}
else if(n%2==1)
{
bias = Yaw; //当走过白色区域为奇数时,不需要转化
}
flag=0;
}
else if(whiteflag==0) //在黑线区域小车根据灰度返回值循迹
{
ledflag1=0;
if(ledflag2==0)
{
ledflag2=1;
ledbegin=1;
m++;
}
if(flag==0)
{
flag=1;
n=n+1;
}
bias = xunji(); //差速为灰度返回值
whiteflag=0;
}
targetA = Speed_Middle+bias;
targetB = Speed_Middle-bias;
CurrentA = (float)gEncoderVal_left/3; //左轮当前转速
CurrentB = (float)gEncoderVal_right/3; //右轮当前转速
Motor_Left = (int)PWM_Limit(PID_A(CurrentA,targetA),Limit, -Limit);
Motor_Right = (int)PWM_Limit(PID_B(CurrentB,targetB),Limit, -Limit); //PWM限幅
Set_Pwm(Motor_Left, Motor_Right);
}
3、模式三(ACBDA)
第三问的难点在于在出弯时如何让小车精准的转向,这里我们使用陀螺仪使小车走固定角度,通过出弯时快速转向来提高准确性。
同时,因为我们使用八路灰度进行入弯判断,在入弯时,小车一旦向内偏离就会使外侧光电先识别到,从而直接漂出轨道,如图:
对此我的解决方案是,在模式三及模式四中,我们只使用半边的灰度来进行循迹,从A到C时使用左半边,从B到D时使用右半边。
void Control_ACBDA(void) //模式三
{
int Motor_Left, Motor_Right; //电机赋值
float bias; //差速值
if(m==6) //m为模式切换计数值,用于判断是否跑完全程
{
DL_GPIO_clearPins(GPIO_STBY_PORT,GPIO_STBY_PIN_STBY_PIN);
}
if (ledflag==1) DL_GPIO_setPins(GPIO_LED_PORT,GPIO_LED_PIN_LED_PIN);
else DL_GPIO_clearPins(GPIO_LED_PORT,GPIO_LED_PIN_LED_PIN);
if (P1==0 && P2==0 && P3==0 && P4==0 && P5==0 && P6==0 && P7==0 && P8==0) timebegin1=1;
else whiteflag1=0;
if (whiteflag1==1)
{
ledflag2=0;
if(ledflag1==0)
{
ledflag1=1;
ledbegin=1;
m++;
}
if (n%2==0)
{
bias = Yaw + 105; //以A到C的方向为0,B到D的巡航角为-103~-106,具体数值根据实际自行调整
}
else if(n%2==1)
{
bias = Yaw;
}
flag=0;
}
else if(whiteflag1==0)
{
ledflag1=0;
if(ledflag2==0)
{
ledflag2=1;
ledbegin=1;
m++;
}
if(flag==0)
{
flag=1;
n=n+1;
}
if(n%2==1) bias = xunji_right(); //从B到D时使用右半边灰度
else if(n%2==0) bias = xunji_left(); //从A到C时使用左半边灰度
whiteflag1=0;
}
targetA = Speed_Middle+bias;
targetB = Speed_Middle-bias;
CurrentA = (float)gEncoderVal_left/3; //left
CurrentB = (float)gEncoderVal_right/3; //right
Motor_Left = (int)PWM_Limit(PID_A(CurrentA,targetA),Limit, -Limit);
Motor_Right = (int)PWM_Limit(PID_B(CurrentB,targetB),Limit, -Limit); //PWM限幅
Set_Pwm(Motor_Left, Motor_Right);
}
4、模式四(ACBDAx4)
模式四与模式三差别不大,只需要参数微调,就不放了,大家可以直接去工程中对比学习。
十、按键等其余外设配置
1、按键
2、声光模块
3、电机驱动板使能
4、OLED
我们将当前模式、当前巡航角显示在了OLED上,使用软件I2C进行通信。
十一、整体主控(main函数)
本工程中的整体控制是放在了main函数中,包括电机控制函数也放在main函数中,这样的好处是控制周期短,但也容易被打断,大家也可以再开一个定时器用于调用控制函数,当然定时器周期需尽可能短。
extern float Yaw;
int main(void)
{
uint8_t a=5;
int mode=0; //小车运动模式
int begin=0; //小车启动标志位
SYSCFG_DL_init();
NVIC_EnableIRQ(TIMER_Encoder_Read_INST_INT_IRQN); //开启定时器
NVIC_EnableIRQ(GPIO_EncoderA_INT_IRQN); //开启编码器外部中断
NVIC_EnableIRQ(GPIO_EncoderB_INT_IRQN);
DL_Timer_startCounter(TIMER_Encoder_Read_INST); //开启定时中断用于读取编码器数据
DL_Timer_startCounter(PWM_0_INST); //pwm定时器初始化
OLED_Init(); //初始化OLED
NVIC_EnableIRQ(UART_JY61P_INST_INT_IRQN); //使能中断
Serial_JY61P_Zero_Yaw(); //复位陀螺仪,使上电时位置为0
while (1)
{
OLED_ClearArea(0,0,79,40); //刷新oled
if(!DL_GPIO_readPins(GPIO_Key_PIN_S2_PORT, GPIO_Key_PIN_S2_PIN)) //模式切换
{
Delay_ms(10);
if(!DL_GPIO_readPins(GPIO_Key_PIN_S2_PORT, GPIO_Key_PIN_S2_PIN))
{
mode = (mode + 1)%4;
}
while(!DL_GPIO_readPins(GPIO_Key_PIN_S2_PORT, GPIO_Key_PIN_S2_PIN));
}
else if(!DL_GPIO_readPins(GPIO_Key_PIN_S1_PORT, GPIO_Key_PIN_S1_PIN)) //小车启动
{
Delay_ms(10);
if(!DL_GPIO_readPins(GPIO_Key_PIN_S1_PORT, GPIO_Key_PIN_S1_PIN))
{
Delay_ms(2000); //启动延时
DL_GPIO_setPins(GPIO_STBY_PORT,GPIO_STBY_PIN_STBY_PIN);
begin=1;
}
while(!DL_GPIO_readPins(GPIO_Key_PIN_S1_PORT, GPIO_Key_PIN_S1_PIN));
}
if (mode==0)
{
OLED_ShowString(0, 0, "AB",OLED_8X16); //在OLED上显示当前模式
OLED_Printf(0, 40, OLED_6X8, "Yaw:%5.2f",Yaw); //显示当前巡航角
OLED_Update();
if (begin==1)
{
Control_AB();
}
}
else if(mode==1)
{
OLED_ShowString(0, 0, "ABCDA",OLED_8X16);
OLED_Printf(0, 40, OLED_6X8, "Yaw:%5.2f",Yaw);
OLED_Update();
if (begin==1)
{
Control_ABCDA();
}
}
else if(mode==2)
{
OLED_ShowString(0, 0, "ACBDA",OLED_8X16);
OLED_Printf(0, 40, OLED_6X8, "Yaw:%5.2f",Yaw);
OLED_Update();
if (begin==1)
{
Control_ACBDA();
}
}
else if(mode==3)
{
OLED_ShowString(0, 0, "ACBDAx4",OLED_8X16);
OLED_Printf(0, 40, OLED_6X8, "Yaw:%5.2f",Yaw);
OLED_Update();
if (begin==1)
{
Control_ACBDAx4();
}
}
}
}
十二、源码分享
百度网盘:提取码:qs65
CSDN:【2024电赛H题】自动行驶小车
十三、结语
学无止境,希望每一位读者都能够有所收获,有不足之处也欢迎大家在评论区留言或者私信。
大家也可以去看看南极熊学长的方案,方案无好坏,各有优缺,希望大家融会贯通,精益求精。
如有疑问或想交流学习心得,欢迎加入交流群751950234,群内不定期更新代码。