【2024年电赛H题自动行驶小车】省一学长手把手从零教你学MSPM0

news2024/9/25 17:13:30

一、前前言

第十二届浙江省大学生电子设计竞赛终于落下帷幕了,名单已公示,几家欢喜几家愁?我觉得每一位经历过电赛的朋友都称得上英雄,我想我们所学到的并非是“省一等”或“成功参赛奖”一个头衔能囊括的,相信真正有价值的东西会在人生的道路上生根发芽。

我写这篇博客的最初目的,也只是想用代码的方式记录这段伟大的时光。

二、前言

2024年省赛题目如下:

不难看出,题目的难点只有两个:

1、如何熟练使用TI MSPM0系列的板子?

2、无黑线部分如何尽可能保持直行?

对于第一个问题,我先给出我的解决方法:

1、【2024电赛】TI MSPM0快速入门课

2、LP-MSPM03507学习资料汇总

第一个链接是B站UP主Torris-Yin的基础教学视频,我觉得讲的很好,故上附链接推荐给各位。

第二个链接是我自己整理的纯代码形式的G3507资料,仅供参考!

第二个问题,我的解决方法是购买高精度的陀螺仪模块——JY61P、MPU6050。(不推荐使用MPU6050,需要软件去零漂)

好了,难点都解决了,现在发现其实拿下省一也没难度吧!

三、整体框架

基本思路就是:有线巡线,无线靠巡航角

接下来进入正题,我会尽量按顺序一一介绍各模块的配置与使用,然后将各模块整合一块。

源码下附全开源,可根据需求跳转查看相应部分。

功底不深,多多包涵!

四、驱动电机

1、PWM配置

PWM Period Count:自动重装载值,相当于CUBEMX中的ARR

PWM Frequency:    PWM输出频率,建议选择15k ~ 20kHz

2、PWM输通道及占空比配置

Channel Update Mode:死区PWM宽度设置,此处不设置死区影响不大。

3、正反转IO口配置

此四口仅配置为普通输出IO口即可

4、代码填写

AIN1、2控制左马达的正反转:AIN1为高,AIN2为低,则为正转;反之相反。

BIN1、2同理。

/* 全局宏定义 */
#define AIN1_High       DL_GPIO_setPins(DIR_PORT, DIR_AIN1_PIN)
#define AIN1_Low        DL_GPIO_clearPins(DIR_PORT, DIR_AIN1_PIN)
#define AIN2_High       DL_GPIO_setPins(DIR_PORT, DIR_AIN2_PIN)
#define AIN2_Low        DL_GPIO_clearPins(DIR_PORT, DIR_AIN2_PIN)
#define BIN1_High       DL_GPIO_setPins(DIR_PORT, DIR_BIN1_PIN)
#define BIN1_Low        DL_GPIO_clearPins(DIR_PORT, DIR_BIN1_PIN)
#define BIN2_High       DL_GPIO_setPins(DIR_PORT, DIR_BIN2_PIN)
#define BIN2_Low        DL_GPIO_clearPins(DIR_PORT, DIR_BIN2_PIN)



/*
    * 初始化
*/
void Init(void)
{
    /* 关闭所有输出 */
    AIN1_Low;
    AIN2_Low;
    BIN1_Low;
    BIN2_Low;

    /* 使能PWM波输出 */
    DL_Timer_startCounter(PWM_0_INST);
}

至此,驱动电机部分完成。

五、读取编码器数值

此部分包含两个部分:配置IO口读取编码器配置、配置定时器定时读取编码值(此时的编码值相当于小车速度值)

编码器Port配置:GPIO_EncoderA\B

1、编码器IO口初始化配置

2、配置IO口及中断

Internal Resistor:上下拉电阻配置,由于读取的编码器有效电平为高电平,故选择配置为下拉电阻

Input Filter:           输入电平滤波,降低误读率

其余三个IO口配置同理。

3、代码填写

此代码选择的是四分频的编码器度数方式,精度更高。

想对编码器模式了解更多?详见【STM32+HAL】直流电机PID控制

/*
    * 编码器读取函数
    * 编码器A、B分别对应编码器A、B的引脚
    * EncoderA_CNT、EncoderB_CNT    分别对应编码器A、B的计数值
    * EncoderA_Port、EncoderB_Port  分别对应编码器A、B的端口
*/
void Encodering(void)
{
    EncoderA_Port = DL_GPIO_getEnabledInterruptStatus(GPIO_EncoderA_PORT, GPIO_EncoderA_PIN_0_PIN | GPIO_EncoderA_PIN_1_PIN);
    EncoderB_Port = DL_GPIO_getEnabledInterruptStatus(GPIO_EncoderB_PORT, GPIO_EncoderB_PIN_2_PIN | GPIO_EncoderB_PIN_3_PIN);
    
    /* 编码器A */
    if((EncoderA_Port & GPIO_EncoderA_PIN_0_PIN) == GPIO_EncoderA_PIN_0_PIN)        //编码器A-Pin0
    {
        if(!DL_GPIO_readPins(GPIO_EncoderA_PORT,GPIO_EncoderA_PIN_1_PIN))   EncoderA_CNT--;
        else                                                                EncoderA_CNT++;
    }
    else if((EncoderA_Port & GPIO_EncoderA_PIN_1_PIN) == GPIO_EncoderA_PIN_1_PIN)   //编码器A-Pin1
    {
        if(!DL_GPIO_readPins(GPIO_EncoderA_PORT,GPIO_EncoderA_PIN_0_PIN))   EncoderA_CNT++;
        else                                                                EncoderA_CNT--;
    }
    DL_GPIO_clearInterruptStatus(GPIO_EncoderA_PORT, GPIO_EncoderA_PIN_0_PIN|GPIO_EncoderA_PIN_1_PIN);


    /* 编码器B */
    if((EncoderB_Port & GPIO_EncoderB_PIN_2_PIN) == GPIO_EncoderB_PIN_2_PIN)
    {
        if(!DL_GPIO_readPins(GPIO_EncoderB_PORT,GPIO_EncoderB_PIN_3_PIN))   EncoderB_CNT--;
        else                                                                EncoderB_CNT++;
    }
    else if((EncoderB_Port & GPIO_EncoderB_PIN_3_PIN) == GPIO_EncoderB_PIN_3_PIN)
    {
        if(!DL_GPIO_readPins(GPIO_EncoderB_PORT,GPIO_EncoderB_PIN_2_PIN))   EncoderB_CNT++;
        else                                                                EncoderB_CNT--;
    }
    DL_GPIO_clearInterruptStatus(GPIO_EncoderB_PORT, GPIO_EncoderB_PIN_2_PIN|GPIO_EncoderB_PIN_3_PIN);
}





/**************************************************************************
Function: Read encoder count per unit time
Input   : TIMX:Timer
Output  : none
函数功能:单位时间读取编码器计数
入口参数:TIMX:定时器
返回  值:速度值
**************************************************************************/
void Read_Encoder(void)
{
    EncoderA_VEL = EncoderA_CNT;
    EncoderA_CNT = 0;
    EncoderB_VEL = EncoderB_CNT;
    EncoderB_CNT = 0;
}

 

编码器定时器配置:TIMER_Encoder_Read

1、定时器初始化配置

2、配置定时器周期及中断

因配置为周期向下计数模式,故中断对应的选择Zero event,即定时器计数到0触发中断。

3、代码填写

TIMER_Encoder_Read_INST_IRQHandler:定时器中断服务函数,每20ms进入中断读取编码值,并对速度变量赋值。

/*
    * 编码器初始化
*/
void Encoder_Init(void)
{
    /* 关闭所有输出 */
    AIN1_Low;
    AIN2_Low;
    BIN1_Low;
    BIN2_Low;

    /* 使能编码器定时器、GPIO中断 */
    NVIC_EnableIRQ(GPIO_EncoderA_INT_IRQN);
    NVIC_EnableIRQ(GPIO_EncoderB_INT_IRQN);
    NVIC_EnableIRQ(TIMER_Encoder_Read_INST_INT_IRQN);
    DL_Timer_startCounter(TIMER_Encoder_Read_INST);

    /* 使能PWM波输出 */
    DL_Timer_startCounter(PWM_0_INST);
}




/*
    * 编码器读取中断服务函数
    * 读取编码器数值
*/
void TIMER_Encoder_Read_INST_IRQHandler(void)
{
    switch (DL_TimerG_getPendingInterrupt(TIMER_Encoder_Read_INST)){
        case DL_TIMER_IIDX_ZERO:
            Read_Encoder();            //赋值编码器数值
            // printf("%d,%d\r\n",EncoderA_VEL,EncoderB_VEL);
            break;
        default:
            break;
    }
}

 至此,我们实现了读取马达的速度值。

六、八路灰度传感器模块

1、IO口配置

普通输入IO口配置即可。

因为灰度传感器输出电压一般大于3.3V,建议不要配置上下拉电阻。

2、全局宏定义

检测到黑线时,输出高电平,变量置1。

#define P1          DL_GPIO_readPins(Sensor_P1_PORT,Sensor_P1_PIN)
#define P2          DL_GPIO_readPins(Sensor_P2_PORT,Sensor_P2_PIN)
#define P3          DL_GPIO_readPins(Sensor_P3_PORT,Sensor_P3_PIN)
#define P4          DL_GPIO_readPins(Sensor_P4_PORT,Sensor_P4_PIN)
#define P5          DL_GPIO_readPins(Sensor_P5_PORT,Sensor_P5_PIN)
#define P6          DL_GPIO_readPins(Sensor_P6_PORT,Sensor_P6_PIN)
#define P7          DL_GPIO_readPins(Sensor_P7_PORT,Sensor_P7_PIN)
#define P8          DL_GPIO_readPins(Sensor_P8_PORT,Sensor_P8_PIN)

至此,完成八路灰度传感器基础配置完毕。

七、串口获取巡航角

 1、串口配置

根据JY61P数据使用手册,配置为普通串口中断配置即可。(模块波特率需通过上位机修改,这里不赘述)

若想进一步提高通讯效率,详见文章中的串口DMA传输LP-MSPM03507学习资料汇总

2、代码填写
#include "GYRO.h"

// 定义接收变量
uint8_t RollL, RollH, PitchL, PitchH, YawL, YawH, VL, VH, SUM;
float Pitch,Roll,Yaw;

// 串口接收状态标识
#define WAIT_HEADER1 0
#define WAIT_HEADER2 1
#define RECEIVE_DATA 2

uint8_t RxState = WAIT_HEADER1;
uint8_t receivedData[9];
uint8_t dataIndex = 0;


//发送置偏航角置零命令
void Serial_JY61P_Zero_Yaw(void)
{
    DL_UART_Main_transmitDataBlocking(UART_JY61P_INST,0XFF);
	DL_UART_Main_transmitDataBlocking(UART_JY61P_INST,0XAA);
	DL_UART_Main_transmitDataBlocking(UART_JY61P_INST,0X69);
	DL_UART_Main_transmitDataBlocking(UART_JY61P_INST,0X88);
	DL_UART_Main_transmitDataBlocking(UART_JY61P_INST,0XB5);
	delay_ms(100);
	DL_UART_Main_transmitDataBlocking(UART_JY61P_INST,0XFF);
	DL_UART_Main_transmitDataBlocking(UART_JY61P_INST,0XAA);
	DL_UART_Main_transmitDataBlocking(UART_JY61P_INST,0X76);
	DL_UART_Main_transmitDataBlocking(UART_JY61P_INST,0X00);
	DL_UART_Main_transmitDataBlocking(UART_JY61P_INST,0X00);
	delay_ms(100);
	DL_UART_Main_transmitDataBlocking(UART_JY61P_INST,0XFF);
	DL_UART_Main_transmitDataBlocking(UART_JY61P_INST,0XAA);
	DL_UART_Main_transmitDataBlocking(UART_JY61P_INST,0X00);
	DL_UART_Main_transmitDataBlocking(UART_JY61P_INST,0X00);
	DL_UART_Main_transmitDataBlocking(UART_JY61P_INST,0X00);
    delay_ms(500);
}



// 串口中断处理函数
/*
    * 串口接收数据,并解析出角度
    * 接收到的数据格式为:0x55 0x53 0xXX 0xXX 0xXX 0xXX 0xXX 0xXX 0xXX 0xXX
    * 其中0xXX为角度数据,单位为0.1度
    * 角度数据为16位,高8位在前,低8位在后
    * 角度数据为正数表示顺时针旋转,负数表示逆时针旋转
    * 角度数据为0表示水平
    * 角度数据为180表示垂直
    * 角度数据为90表示垂直向上
    * 角度数据为-90表示垂直向下
    * 角度数据为360表示水平向右
    * 角度数据为-360表示水平向左
*/
void UART_JY61P_INST_IRQHandler(void) 
{
    uint8_t uartdata = DL_UART_Main_receiveData(UART_JY61P_INST);   // 接收一个uint8_t数据

    switch (RxState) {
    case WAIT_HEADER1:
        if (uartdata == 0x55) {
            RxState = WAIT_HEADER2;
        }
        break;
    case WAIT_HEADER2:
        if (uartdata == 0x53) {
            RxState = RECEIVE_DATA;
            dataIndex = 0;
        } else {
            RxState = WAIT_HEADER1;                                 // 如果不是期望的第二个头,重置状态
        }
        break;
    case RECEIVE_DATA:
        receivedData[dataIndex++] = uartdata;
        if (dataIndex == 9) {
            // 数据接收完毕,分配给具体的变量
            RollL = receivedData[0];
            RollH = receivedData[1];
            PitchL = receivedData[2];
            PitchH = receivedData[3];
            YawL = receivedData[4];
            YawH = receivedData[5];
            VL = receivedData[6];
            VH = receivedData[7];
            SUM = receivedData[8];

            // 校验SUM是否正确
            uint8_t calculatedSum = 0x55 + 0x53 + RollH + RollL + PitchH + PitchL + YawH + YawL + VH + VL;
            if (calculatedSum == SUM) {
                // 校验成功,可以进行后续处理
                if((float)(((uint16_t)RollH << 8) | RollL)/32768*180>180){
                    Roll = (float)(((uint16_t)RollH << 8) | RollL)/32768*180 - 360;
                }else{
                    Roll = (float)(((uint16_t)RollH << 8) | RollL)/32768*180;
                }

                if((float)(((uint16_t)PitchH << 8) | PitchL)/32768*180>180){
                    Pitch = (float)(((uint16_t)PitchH << 8) | PitchL)/32768*180 - 360;
                }else{
                    Pitch = (float)(((uint16_t)PitchH << 8) | PitchL)/32768*180;
                }

                if((float)(((uint16_t)YawH << 8) | YawL)/32768*180 >180){
                    Yaw = (float)(((uint16_t)YawH << 8) | YawL)/32768*180 - 360;
                }else{
                    Yaw = (float)(((uint16_t)YawH << 8) | YawL)/32768*180;
                }
                
            } else {
                // 校验失败,处理错误
            }
            RxState = WAIT_HEADER1;                                 // 重置状态以等待下一个数据包
        }
        break;
    }
}

至此,陀螺仪模块配置完毕。

八、剩余外设配置

1、按键配置

按键一端接IO口,另一端接地即可,建议串联一个1kΩ左右电阻

按键EN配置同理

 

2、声光提示IO口

题目中要求每经过一个点需声光报警,我们配置为普通输出IO口即可。

3、代码填写
#define LED_High        DL_GPIO_setPins(LED_PORT, LED_PIN_LED_PIN)
#define LED_Low         DL_GPIO_clearPins(LED_PORT, LED_PIN_LED_PIN)


while(1)
{    
    /* 按键扫描 */
    Key_Scan();

    /* LED闪烁 */
    LED_Sound();
}



/*
    * 按键扫描函数
    * 按键按下时,切换mode
    * 按键按下时,切换flag_en确认使能
*/
void Key_Scan(void)
{
    /* 模式切换按键 */
    if(DL_GPIO_readPins(KEY_PORT, KEY_S2_PIN) == 0)
    {
        delay_ms(10);
        if(DL_GPIO_readPins(KEY_PORT, KEY_S2_PIN) == 0)
        {
            while(!DL_GPIO_readPins(KEY_PORT, KEY_S2_PIN));
            mode = (mode + 1) % 5;
        }
    }

    /* 使能按键 */
    if(DL_GPIO_readPins(KEY_PORT, KEY_EN_PIN) == 0)
    {
        delay_ms(10);
        if(DL_GPIO_readPins(KEY_PORT, KEY_EN_PIN) == 0)
        {
            while(!DL_GPIO_readPins(KEY_PORT, KEY_EN_PIN));
            flag_en = 1 - flag_en;
        }
    }
}



/*
    * LED闪烁函数
*/
void LED_Sound(void)
{
    if(flag_LED)
    {
        LED_High;
        LED_CNT  = LED_CNT + 0.1;
        if(LED_CNT >= 800)         //闪烁延时
        {
            LED_Low;
            LED_CNT = 0;
            flag_LED = 0;
        }
    }
}

4、控制定时器TIMER_0

控制函数需要保证尽可能的不被打断,所以放在定时器里是一个很好的选择。

而放在while里的优点是执行周期短,故考虑两者可尽量降低定时器周期

1)设置定时器周期500us

2)开启中断

九、速度环、差速环实现

速度环

速度环的目标值为设定目标速度,测试参数时需先关闭差速环,仅测量速度环,并对目标速度Motor_Left赋初值,通过上位机查看当前速度与目标速度的数值曲线来调节PID参数。

//速度环PID
#define   Kp1   	40
#define   Ki1     	0.13
#define   Kd1  		0.1


/*=================== 增量式PID控制设计 ===================*/
//左A轮PID
float PID_A(float Encoder,float Target)
{
	static float Bias, Last_bias, Last2_bias, Pwm;
	Bias = Target - Encoder;
	Pwm += Kp1 * (Bias - Last_bias) + Ki1 * Bias + Kd1 * (Bias - 2 * Last_bias + Last2_bias);

	Last_bias = Bias;
	Last2_bias = Last_bias;
	return Pwm;
}


//右B轮PID
float PID_B(float Encoder,float Target)
{
	static float Bias, Last_bias, Last2_bias, Pwm;
	Bias = Target-Encoder;
	Pwm += Kp1 * (Bias - Last_bias) + Ki1 * Bias + Kd1 * (Bias - 2 * Last_bias + Last2_bias);

	Last_bias = Bias;
	Last2_bias = Last_bias;
	return Pwm;
}

差速环

差速环的目标值为控制量的目标值,即陀螺仪的差速环GYRO_Control的目标值应始终为0(直行),输入为当前巡航角,输出为应偏移数值(bias)

巡线的差速环Incremental_Quantity略有不同,可以这么理解:当所有光电都返回为识别到白色,则两轮差速值为0;若离车体中心的两光电检测到黑线,则返回一个较小的偏差值;距离越远,返回的偏差值越大。

差速环的参数调节应在速度环之后。

//陀螺仪PID
#define   Kp3       1
#define   Ki3       0
#define   Kd3  	    0

/*
    函数功能:角度PID
    IN: 当前值,目标值
    OUT: 输出值
*/
float GYRO_Control(float now,float target)
{
	static float Bias, Last_bias, Last2_bias, Pwm;
	Bias = target-now;
	Pwm += Kp3 * (Bias - Last_bias) + Ki3 * Bias + Kd3 * (Bias - 2 * Last_bias + Last2_bias);

	Last_bias = Bias;
	Last2_bias = Last_bias;
	return Pwm;
}



/*
    函数:Incremental_Quantity
    功能:普通直行巡线模式
    参数:无
*/
int Incremental_Quantity(void)
{
    int value = 0;
    if(!P1) //检测到最右端
        value += 12;
    if(!P2) 
        value += 9;
    if(!P3)
        value += 7;
    if(!P4)
        value += 3;
    if(!P5)
        value -= 3;
    if(!P6)
        value -= 7;
    if(!P7)
        value -= 9;
    if(!P8)
        value -= 12;
    return value;
}

速度环+差速环

为方便理解,这里暂时使用了部分伪代码,基本思想为:括号里为判断当前是否落在黑线上语句,若有一个在黑线上,则bias赋值为bias1,即巡线差速值;若全不在黑线上,bias赋值为bias2,即陀螺仪差速值。然后在中值速度Speed_Middle的基础上加减bias,自此实现了差速环控制。

#define  Limit		3800			//PWM波限幅
/*************************************/
float Speed_Middle = 30;			                    //中值速度
int Motor_Left, Motor_Right;		                    //左右马达占空比	


/**************************************************************************
Function: Control function
Input   : none
Output  : none
函数功能:控制小车巡线
入口参数:无
返回  值:无
**************************************************************************/
void Control(void)
{
    float TargetA, TargetB;                         //目标速度
    float bias1, bias2, bias;                       //巡线、陀螺仪偏差

    bias1 = Incremental_Quantity();
    bias2 = GYRO_Control(Yaw, 0);
    bias = (是否检查到黑线) ? bias1 : bias2;

    TargetA = Speed_Middle - bias;
    TargetB = Speed_Middle + bias;

    Motor_Left  = (int)PWM_Limit(PID_A(EncoderA_VEL,TargetA), Limit, -Limit);    //占空比限幅
    Motor_Right = (int)PWM_Limit(PID_B(EncoderB_VEL,TargetB), Limit, -Limit);
    Set_Pwm(Motor_Left, Motor_Right);
}




/*
    函数功能:设置PWM比较值
    IN: 左马达占空比,右马达占空比
    OUT: 无
*/
void Set_Pwm(int Left, int Right)
{
    if(Left > 0)    {AIN1_High;AIN2_Low;}      //左正转
    else            {AIN1_Low;AIN2_High;}      //左反转
    DL_Timer_setCaptureCompareValue(PWM_0_INST, myabs(Left), DL_TIMER_CC_0_INDEX);

    if(Right > 0)   {BIN1_High;BIN2_Low;}      //右正转
    else            {BIN1_Low;BIN2_High;}      //右反转
    DL_Timer_setCaptureCompareValue(PWM_0_INST, myabs(Right), DL_TIMER_CC_1_INDEX);
}

十、四种巡线模式

1、整体代码

由于此部分函数过于冗长,故先采用伪代码形式呈现供大家理解。代码虽长,逻辑简单。

基本思路为:通过按键切换巡线模式mode后,小车进入不同的if语句块,每个语句块放置各个模式的顺序执行代码。

#include "Sensor.h"

/*
    函数功能:路径控制总函数
    参数:无
*/
int Follow_Route(void)
{
    static int cnt = 0;
    /* 模式一 */
    if(mode == 1)
    {
        Follow_Route_Mode1();     //模式一时,仅需判断一次黑线
        return 1;
    }

    /* 模式二 */
    else if(mode == 2)
    {
        Follow_Route_Mode2();    //模式二时,通过改变flag标志位实现四段路径的切换
        return 2;
    }

    /* 模式三 */
    else if(mode == 3)
    {
        Follow_Route_Mode3(0);      //仅进行一次八字绕圈
        return 3;
    }

    /* 模式四 */
    else if(mode == 4)
    {
        Follow_Route_Mode4();      //循环绕四圈
        return 4;
    }
    return 0;
}

2、全部代码

逻辑不难,大家自行理解吧!

#include "Sensor.h"

#define Black_CNT1          30                               //mode = 1 黑线持续次数
#define Black_CNT2          30                               //mode = 2 黑线持续次数
#define Black_CNT3          50                               //mode = 3、4 黑线持续次数

#define White_CNT           1000                              //白线持续次数  
#define White_CNT2          5000                              //mode = 2 白线持续次数
#define White_CNT3          7000                              //mode = 3、4 白线持续次数


extern volatile int flag;                                  //flag状态位:0-暂停;1-AB;2-BC;3-CD;4-DA;
extern volatile bool flag_LED;
extern volatile int mode;                                  //mode状态位:0-暂停;



/*
    函数功能:路径控制总函数
    参数:无
*/
int Follow_Route(void)
{
    static int cnt = 0;
    /* 模式一 */
    if(mode == 1)
    {
        if(flag == 1)                                                    //AB段末尾
        {
            if(P1 || P2 || P3 || P4 || P5 || P6 || P7 || P8)        //任一点检测到黑线
            {
                cnt++;
                // printf("%d\r\n", cnt);
                if(cnt > Black_CNT1)                                     //检测到黑线持续5次
                {
                    flag_LED = 1;
                    flag = 0;
                    cnt = 0;
                    return 0;
                }
            }
            else   cnt = 0;                                         //连续检测黑线判定
        }   
    }

    /* 模式二 */
    else if(mode == 2)
    {
        /* B点判定 */
        if(flag == 1)
        {
            if(P1 || P2 || P3 || P4 || P5 || P6 || P7 || P8)        //任一点检测到黑线
            {
                cnt++;
                if(cnt > Black_CNT2)             //检测到黑线持续5次
                {
                    flag_LED = 1;
                    flag = 2;               //进入状态二
                    cnt = 0;
                    return 2;
                }
            }
            else   cnt = 0;
        }
        /* C点判定 */
        else if(flag == 2)
        {
            if(!(P1 || P2 || P3 || P4 || P5 || P6 || P7 || P8))                  //所有点检测到白线
            {
                cnt++;
                if(cnt > White_CNT2)               //检测到白线持续White_CNT2次
                {
                    flag_LED = 1;
                    flag = 3;
                    cnt = 0;
                    return 3;
                }
            }
            else   cnt = 0;
        }
        /* D点判定 */
        else if(flag == 3)
        {
            if(P1 || P2 || P3 || P4)        //任一点检测到黑线
            {
                cnt++;
                if(cnt > Black_CNT2)                //检测到黑线持续Black_CNT2次
                {
                    flag_LED = 1;
                    flag = 4;
                    cnt = 0;
                    return 4;
                }
            }
            else   cnt = 0;
        }
        /* A点(终点)判定 */
        else if(flag == 4)
        {
            if(!(P1 || P2 || P3 || P4 || P5 || P6 || P7 || P8))                  //所有点检测到白线
            {
                cnt++;
                if(cnt > White_CNT4 )                  //检测到白线持续White_CNT4次
                {
                    flag_LED = 1;
                    flag = 0;           //一圈结束
                    cnt = 0;
                    return 0;
                }
            }
            else   cnt = 0;
        }
    }

    /* 模式三 */
    else if(mode == 3)
    {
        Follow_Route_Mode3(0);      //仅进行一次绕圈
        return 3;
    }

    /* 模式四 */
    else if(mode == 4)
    {
        Follow_Route_Mode4();      //循环绕四圈
        return 4;
    }
    return 0;
}




/*
    函数功能:模式三路径控制
    参数:  stop:循环停止标志位
    stop - 1:循环继续
    stop - 0:循环一次
*/
int Follow_Route_Mode3(int stop)
{
    static int cnt3 = 0;
    /* C点判定 */
    if(flag == 1)
    {
        if(P5 || P6 || P7 || P8)        //车体左侧任一点检测到黑线
        {
            cnt3++;
            if(cnt3 > Black_CNT3)               //检测到黑线持续Black_CNT3次
            {
                flag_LED = 1;
                flag = 2;               //进入下一状态
                cnt3 = 0;
                return 2;
            }
        }
        else   cnt3 = 0;
    }

    /* B点判定 */
    else if(flag == 2)
    {
        if(!(P1 || P2 || P3 || P4 || P5 || P6 || P7 || P8))              //所有点检测到白线
        {
            cnt3++;
            if(cnt3 > White_CNT3)                             //检测到白线持续White_CNT3次
            {
                flag_LED = 1;
                flag = 3;
                cnt3 = 0;
                return 3;
            }
        }
    }

    /* D点判定 */
    else if(flag == 3)
    {
        if(P1 || P2 || P3 || P4)        //车体右侧任一点检测到黑线
        {
            cnt3++;
            if(cnt3 > Black_CNT3)                      //检测到黑线持续Black_CNT3次
            {
                flag_LED = 1;
                flag = 4;
                cnt3 = 0;
                return 4;
            }
        }
    }

    /* A点判定 */
    else if(flag == 4)
    {
        if(!(P1 || P2 || P3 || P4 || P5 || P6 || P7 || P8))             //所有点检测到白线
        {
            cnt3++;
            if(cnt3 > White_CNT3)                      //检测到白线持续White_CNT3次
            {
                flag_LED = 1;
                flag = stop;                     //停止
                cnt3 = 0;
                return 0;
            }
        }
    }
    return 0;
}





/*
    函数功能:模式四路径控制
*/
int Follow_Route_Mode4(void)
{
    Follow_Route_Mode3(1);  //循环三圈不停止
    Follow_Route_Mode3(1);
    Follow_Route_Mode3(1);
    Follow_Route_Mode3(0);      //停止
    return 0;
}

十一、模块整合

核心控制代码放在main.c文件中,关键函数接口主要从Sensor.cControl.c两个文件中引出。

main.c通过按键扫描来选择开启哪个模式

Sensor.c包含各模式的巡线流程代码

Control.c包含各模式速度环及差速环的PID函数代码

1、各个全局标志位代码的含义与赋值方式
/* 
按键一:改变mode值以切换当前模式
按键二:使能flag_en以确认当前模式
flag : 状态机思想,每结束一段路径通过软件切换当前模式数
*/


volatile int flag = 1;                                  //flag状态位:0-暂停;1-AB段;2-BC段;3-CD段;4-DA段;
volatile int mode = 0;                                  //mode状态位:0-暂停;1-模式一... ...;
volatile int flag_en = 0;                               //flag_en状态位:0-不使能;1-使能

 

2、初始化配置

用惯了HAL库代码书写习惯,,相信大家能明白我的意思!

    SYSCFG_DL_init();
  /* USER CODE BEGIN 2 */

/* GYRO初始化 */
Serial_JY61P_Zero_Yaw();
NVIC_EnableIRQ(UART_JY61P_INST_INT_IRQN);
delay_ms(500);

/* 编码器及PWM初始化 */
Encoder_Init();

/* 使能控制定时器 */
NVIC_EnableIRQ(TIMER_0_INST_INT_IRQN);
DL_Timer_startCounter(TIMER_0_INST);

  /* USER CODE END 2 */

3、while(1)填写

CPU不断扫描按键状态,当开启使能时开启巡线函数。

    while (1) {

    /* 按键扫描 */
    Key_Scan();

    if(flag_en)         //使能控制开启
    {
        Follow_Route(); //路径控制总函数函数
        /* LED闪烁 */
        LED_Sound();
    }
}
4、定时器控制中断

每隔500us进入控制中断,且只在使能情况下开启控制函数。

/*
    * 定时器0中断服务函数
*/
void TIMER_0_INST_IRQHandler(void)
{
    switch (DL_TimerA_getPendingInterrupt(TIMER_0_INST)) {
    case DL_TIMERA_IIDX_ZERO:
        //flag位判断
        if(flag == 0 || flag_en == 0 || mode == 0)      Set_Pwm(0,0);
        else if(flag_en)                                Control();
    break;
    default:
    break;
    }
}

十二、源码提供

夸克网盘:提取码:miXa

百度网盘:提取码:6666

Gitee:Automatic_Driving

CSDN:Automatic-Driving

十三、结语

本人能力有限,代码未必是最优解,若有可改进之处望在评论区留言。

如有小伙伴想交流学习心得,欢迎加入群聊751950234,群内不定期更新代码,以及提供本人博客所有源码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1992923.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一篇带你了解kickstart无人值守以及pxe实现服务器自动部署

目录 使用背景 pxe原理 一、kickstart无人值守实验 实验环境&#xff1a; kickstart脚本制作&#xff1a; http服务 DCHP服务 定位需要装的机器上&#xff08;已使用光盘&#xff09; 二、PXE实验 实验环境&#xff1a;在上个实验环境的基础上 使用已经做好的dhcp&am…

Type-C取电芯片LDR6500

在当今电子设备日新月异的时代&#xff0c;Type-C接口以其高效、便捷的特点迅速成为市场主流。这一接口不仅支持高速数据传输&#xff0c;还实现了正反插拔的便利性&#xff0c;极大地提升了用户体验。然而&#xff0c;在Type-C接口的广泛应用背后&#xff0c;一个关键的技术组…

数模评价类—【主成分分析法】

目录 文章目录 前言 一、主成分分析法是什么&#xff1f; 二、模型原理 三.实现步骤 1.x标准化处理 2.计算标准化样本后的协方差矩阵/样本相关系数矩阵 3.计算R的特征值&#xff08;入&#xff09;和特征向量 4.计算主成分贡献率和累计贡献率 5.写出主成分 总结 前言…

【物联网设备端开发】使用QEMU模拟ESP硬件运行ESP-IDF

目录 一&#xff0c;开发环境搭建 1.1 安装ESP-IDF 1.2 安装vscode插件 1.3 在ESP-IDF插件配置ESP-IDF开发配置 1.4 下载IOTDeviceSDK 设备端开发代码 1.5 通过ESP-IDF插件编译好镜像 1.6 构建QEMU docker镜像 1.7 使用QEMU容器运行镜像 二&#xff0c;搭建QEMU环境步…

CS531 Virtualization

MLFQ copy and change address translation Segmentation TLB multilevel page

Qt实现中英文切换中QMessageBox中的中文信息怎么处理

有粉丝看了《Qt实现中英文切换》文章后&#xff0c;留言说“中英文切换中QMessageBox中的中文信息怎么处理”&#xff0c;这篇文章就告诉你处理方法。 1&#xff0c;QMessageBox OK或Cancel提示语句 QMessageBox::information(this, QString::fromLocal8Bit("测试")…

达林顿管uln2004a参数及其功耗计算

特点&#xff1a; 单输出集电极电流500mA高电压输出 50V有输出钳位二极管达林顿晶体管阵列。每个由七个组成NPN达林顿对&#xff0c;具有高压输出与共阴极钳位二极管开关感性负载。单个达灵顿对的集电极额定电流为500mA。达林顿对可以并联&#xff0c;以获得更大的电流能力 应…

【LVS】部署NAT模式集群

一、实验环境 每台主机的防火墙和SELinux都要关掉 systemctl stop firewalld setenforce 0 client&#xff08;测试机&#xff09;&#xff1a; ip 172.25.254.50 lvs&#xff08;调度器&#xff09;&#xff1a; vip 172.25.254.100 dip 192.168.0.100 RS1&#xff08;真实服…

2025深圳国际户外棋牌游戏休闲娱乐展览会

2025深圳国际户外棋牌游戏休闲娱乐展览会 2025 Shenzhen International Outdoor Chess Card Games, Leisure and Entertainment Exhibition 时间&#xff1a;2025年02月27-3月01日 地点&#xff1a;深圳会展中心&#xff08;福田馆&#xff09; 详询主办方陆先生 I38&…

LeetCode.3131.找出与数相加的整数I

题目描述&#xff1a; 给你两个长度相等的数组 nums1 和 nums2。 数组 nums1 中的每个元素都与变量 x 所表示的整数相加。如果 x 为负数&#xff0c;则表现为元素值的减少。 在与 x 相加后&#xff0c;nums1 和 nums2 相等 。当两个数组中包含相同的整数&#xff0c;并且这…

C语言结构体初识(变量定义初始化和传参)

结构体类型的声明 结构体是一些值的集合&#xff0c;这些值被称为成员变量&#xff0c;结构体的每个成员可以是不同类型的变量。而数组却是一些相同类型元素值的集合。 声明&#xff1a; struct tag {member-list; 成员变量的列表 }variable-list; 结构体变量 -》是全局变量…

Python写UI自动化--playwright(输入框操作)

get_by_placeholder 允许我们根据输入框的 placeholder 属性来查找元素。这对于使用占位符文本作为提示的输入框非常有用。 如上图&#xff0c;导航百度一下--登录操作&#xff0c;对账号和密码输入框进行检查&#xff0c;在源码页面会看到类似&#xff1a;placeholder"手…

C口一拖二数据线:解锁数字生活的便捷新篇章LDR6020

添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09; 在科技日新月异的今天&#xff0c;我们的电子设备如同生活中的伴侣&#xff0c;无论是工作、学习还是娱乐&#xff0c;都离不开它们的陪伴。随着Type-C接口逐渐成为众多高端智能手机、平板电脑、笔记本电脑…

Java中的优先级队列(PriorityQueue)(如果想知道Java中有关优先级队列的知识点,那么只看这一篇就足够了!)

前言&#xff1a;优先级队列&#xff08;Priority Queue&#xff09;是一种抽象数据类型&#xff0c;其中每个元素都关联有一个优先级&#xff0c;元素按照优先级顺序进行处理。 ✨✨✨这里是秋刀鱼不做梦的BLOG ✨✨✨想要了解更多内容可以访问我的主页秋刀鱼不做梦-CSDN博客 …

从零开始之AI视频制作篇

从零开始之AI视频制作篇 文章目录 从零开始之AI视频制作篇前言一、工具列表二、成片展示三、制作流程1、获取图片素材2、图片生成视频2.1 Runway操作流程 3、文本生成语音3.1 Fish Audio操作流程 4、视频剪辑4.1 音频素材4.2 字幕生成 四、Runway提示词参考&#xff1a;参考 前…

盘点5个PDF 怎么转换成 Word 的实用技巧

在日常的办公和学习中&#xff0c;要将 PDF 文件转换成 Word 是很常有的事。方便我们编辑、修改内容或者是提取其中的内容。一般都会用到一些工具&#xff1b;下面&#xff0c;我将为大家介绍5种高效且实用的 PDF 转 Word 的方法。 1、PDF365转换软件 直通车&#xff1a;www.…

搜维尔科技:【研究】大屏幕沉浸式系统的优势,视觉冲击强、‌分辨率高、‌画面层次感强以及沉浸式交互性体验好等!

大屏幕沉浸式系统的优势主要体现在视觉冲击强、‌分辨率高、‌画面层次感强以及沉浸式交互性体验好。‌ 视觉冲击强&#xff1a;‌大屏幕沉浸式系统通过使用多台投影机投射画面&#xff0c;‌结合高质量影片&#xff0c;‌营造出场景环境&#xff0c;‌通过视觉艺术直击体验者…

Hanoi(汉诺)塔问题

目录 什么是汉诺塔&#xff1f; 如何分析汉诺塔 代码实现汉诺塔 什么是汉诺塔&#xff1f; 这是一个古典的数学问题&#xff0c;是一个用递归方法解题的典型例子。汉诺塔的故事在这里不做介绍啦&#xff01; 汉诺塔的思想是&#xff1a; 总共有3根柱子&#xff0c;这里假设为…

书生.浦江大模型实战训练营——(三)Git基本操作与分支管理

最近在学习书生.浦江大模型实战训练营&#xff0c;所有课程都免费&#xff0c;以关卡的形式学习&#xff0c;也比较有意思&#xff0c;提供免费的算力实战&#xff0c;真的很不错&#xff08;无广&#xff09;&#xff01;欢迎大家一起学习&#xff0c;打开LLM探索大门&#xf…

趋动科技荣登「AIGC赋能金融创新引领者TOP20」

2023年11月28日&#xff0c;“极新AIGC行业峰会”在北京召开&#xff0c;峰会以“AI落地”为指引&#xff0c;探究AI实践与产业化。 从制造业到金融服务业&#xff0c;从医疗保健到交通运输&#xff0c;从文化娱乐到消费零售&#xff0c;智能客服、数字人直播、智能巡检机器人&…