STM32智能小车学习笔记(避障、循迹、跟随)

news2025/1/21 2:56:22

我们使用的是STM32CubeMX软件和MDK5

芯片使用的是STM32F103C8T6

完成对STM32CubeMX的初始化后开始我们的第一步点亮一个LED灯

一、点亮LED灯

点亮PC13连接的灯

打开STM32CubeMX软件,pc13设置为输出模式

然后按照这样配置,user label 设置成这个IO口代表名字即可

点击这个生成代码

STM32CubeMX给我们每一个引脚都在main.h里面设置以宏的形式,我们写的代码要放在BEGIN 和END之间。

添加以下代码

    HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
    HAL_Delay(100);

编译结束0警告0错误说明我们的格式没出现错误,表示我们成功使用STM32CubeMX配置点亮LED灯。

二、按键控制

原理图

KEY1--PB4         上升沿触发         下拉输入

KEY2--PA12        下降沿触发        上拉输入

 PB4和PA12按照这样进行配置

 使能外部中断,生成代码

 

 重定义中断回调函数,并把点亮LED灯的代码给注释掉。

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if(GPIO_Pin == KEY1_Pin)//判断一下那个引脚触发中断
    {
        HAL_Delay(10);//延时消抖
        HAL_GPIO_TogglePin(KEY1_GPIO_Port, KEY1_Pin);
    }
    if(GPIO_Pin == KEY2_Pin)//判断一下那个引脚触发中断
    {
        HAL_Delay(10);//延时消抖
        HAL_GPIO_TogglePin(KEY2_GPIO_Port, KEY2_Pin);
    }
}

三、OLED使用

本实验使用的是优信电子--0.96寸OLED显示液晶屏模块 IIC液晶屏 四引脚

把中景园电子0.96OLED显示屏_STM32F103C8_IIC_V1.0文件里面的OLED文件添加到到我们的工程分组里面并修改一些错误。

 把上面这些部分换成下面的样式

#define OLED_SCLK_Clr() HAL_GPIO_WritePin(OLED_SCL_GPIO_Port, OLED_SCL_Pin, GPIO_PIN_RESET)//设置SCL低电平
#define OLED_SCLK_Set() HAL_GPIO_WritePin(OLED_SCL_GPIO_Port, OLED_SCL_Pin, GPIO_PIN_SET)//设置SCL高电平

#define OLED_SDIN_Clr() HAL_GPIO_WritePin(OLED_SDA_GPIO_Port,OLED_SDA_Pin,GPIO_PIN_RESET)//设置SDA低电平
#define OLED_SDIN_Set() HAL_GPIO_WritePin(OLED_SDA_GPIO_Port,OLED_SDA_Pin,GPIO_PIN_SET)//设置SDA高电平

SDA-PB12        SCL-PA15

初始化IO口为输出模式--上拉输出模式(这个OLED是IIC协议,模拟IIC控制OLED的)

        

在main.c中加入以下代码 测试OLED显示屏

    OLED_Init();     //初始化OLED          
    OLED_Clear(); 
    OLED_ShowCHinese(0,0,0);//中
    OLED_ShowCHinese(18,0,1);//景
    OLED_ShowCHinese(36,0,2);//园
    OLED_ShowCHinese(54,0,3);//电
    OLED_ShowCHinese(72,0,4);//子
    OLED_ShowCHinese(90,0,5);//科
    OLED_ShowCHinese(108,0,6);//技

四、串口实验

用cubemx软件配置,选择USART1        Mode配置为asynchronous(异步)其他的不用修改,生成代码。

在usart.c中重定向printf

/**
 * @brief 重定向printf (重定向fputc),
    使用时候记得勾选上魔法棒->Target->UseMicro LIB 
    可能需要在C文件加typedef struct __FILE FILE;
    包含这个文件#include "stdio.h"
 * @param 
* @return 
*/
 int fputc(int ch,FILE *stream)
 {
     HAL_UART_Transmit(&huart1,( uint8_t *)&ch,1,0xFFFF);
     return ch;
 }

如果有报错添加        typedef struct __FILE FILE;

在main.c中添加        #include "stdio.h"

用printf函数测试一下是否有误

 五、PWM控制电机

PWMA--PA11        PA11、PA8设置成pwm输出

PWMB--PA8

 由参考手册可知TIM1_CH1和TIM1_CH4复用功能重映射到PA8和PA11

 cudemx软件配置生成代码

预分频值设置为1440-1        自动重装载值设置为100-1

 脉冲时长设置为50(也就是占空比为50%)

因为Cude在生成代码时,有很多外设初始化完成后默认是关闭的。需要我们手动开启。

    HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);//开启定时器1 通道1 PWM输出
    HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_4);//开启定时器1 通道4 PWM输出

 启动软件仿真

 下图中d表示的是一个周期的时间2.00433ms(0.002S)那么频率为1/0.002 = 500HZ

 使用这个修改占空比

__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 40);

六、电机驱动和PWM

实验所用电机为A4950

 

 PA11--PWMA、PA8--PWMB 设置成pwm输出,上一步已经设置好了。

PB3--BIN1        输出模式

PB13--AIN1        输出模式

CudeMx配置生成代码

创建motor.c和.h文件

 小车正方向走电平为低电平反方向走电平为高电平(由A4950电机驱动模块使用手册可知正转接低电平)

motor.c

#include "motor.h"
#include "tim.h"
/*******************
*  @brief 设置两个电机的转速和方向
*  @param motor1:输入1-100,对应控制电机正方向速度在1%-100%、输入-1-(-100)对应控制电机反方向速度在1%-100%
            motor2 原理一样
*  @return 无
*
 *******************/
void Motor_Set(int motor1, int motor2)
{
    //根据参数正负 设置选择方向
    if(motor1 < 0) BIN1_SET;
        else    BIN1_RESET;
    if(motor2 < 0) AIN1_SET;
        else    AIN1_RESET;
    
    if(motor1 < 0)
    {
        if(motor1 < -99) motor1 = -99; //超过PWM幅值
        __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, (100+motor1));//修改定时器1 通道1 Pulse改变占空比
    }
    else
    {
        if(motor1 > 99) motor1 = 99;
         __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, motor1);//修改定时器1 通道1 Pulse改变占空比
    }
    if(motor2 < 0)
    {
        if(motor2 < -99) motor2 = -99;//超过PWM幅值
        __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_4, (100+motor2));//修改定时器1 通道4 Pulse改变占空比
    }
    else
    {
        if(motor2 > 99) motor2 = 99;
         __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_4, motor2);//修改定时器1 通道4 Pulse改变占空比
    }     
}

motor.h

#ifndef MOTOR_H_
#define MOTOR_H_

#include "main.h"

#define AIN1_RESET HAL_GPIO_WritePin(AIN1_GPIO_Port, AIN1_Pin, GPIO_PIN_RESET)//设置AIN1 PB13为低电平
#define AIN1_SET HAL_GPIO_WritePin(AIN1_GPIO_Port, AIN1_Pin, GPIO_PIN_SET)//设置AIN1 PB13为高电平

#define BIN1_RESET HAL_GPIO_WritePin(BIN1_GPIO_Port, BIN1_Pin, GPIO_PIN_RESET)//设置BIN1 PB3为低电平
#define BIN1_SET HAL_GPIO_WritePin(BIN1_GPIO_Port, BIN1_Pin, GPIO_PIN_SET)//设置BIN1 PB3为高电平

void Motor_Set(int motor1, int motor2);

#endif


 进行测试

HAL_Delay(500);
 Motor_Set(0,0);

七、编码器测速

这里我们选择TI1和TI2上计数(四倍频)

 由原理图可知AO_A,AO_B以及BO_A,BO_B所连引脚分别为PA0、PA1、PB6、PB7

设置CubeMx

1、设置编码器模式 2、自动重装载值设置为65535 3、TI1 TI2都计数

3、TI1 TI2都计数 4、两个滤波器设置为6

5、打开全局中断 同理设置TI4

6、GPIO引脚设置为上拉 生成代码

 定时器中断定时测速

使用定时器1、2ms进入一次中断,使用中断回调函数

1、设置内部时钟源 

2、使能自动重装载

3、开启更新中断

开启定时器以及中断

  HAL_TIM_Encoder_Start(&htim2, TIM_CHANNEL_ALL);//开启定时器2
  HAL_TIM_Encoder_Start(&htim4, TIM_CHANNEL_ALL);//开启定时器4
  HAL_TIM_Base_Start_IT(&htim2);                //开启定时器2 中断    
  HAL_TIM_Base_Start_IT(&htim4);                //开启定时器4 中断
  HAL_TIM_Base_Start_IT(&htim1);                //开启定时器1 中断

定义两个变量保存编码器计数数值以及两个变量表示速度

  short Encoder1Count = 0;//编码器计数器数值
  short Encoder2Count = 0;
  float Motor1Speed = 0.00;
  float Motor2Speed = 0.00;
  uint16_t TimerCount = 0;

定时器溢出时间计算公式 

 

/*******************
*  @brief 定时器1回调函数
*  @param  ARR == 99 PSC == 1439
*  @return  根据定时器溢出时间计算公式可得0.002s溢出一次
*
 *******************/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if(htim == &htim1)//htim1 500HZ  2ms 中断一次
    {
        TimerCount++;
        if(TimerCount % 5 == 0)//每10ms执行一次
        {
            Encoder1Count = (short)__HAL_TIM_GET_COUNTER(&htim4);
            Encoder2Count = (short)__HAL_TIM_GET_COUNTER(&htim2);
            __HAL_TIM_SET_COUNTER(&htim4,0);
            __HAL_TIM_SET_COUNTER(&htim2,0);
            
            Motor1Speed = (float)Encoder1Count*100/9.6/11/4;
            Motor2Speed = (float)Encoder2Count*100/9.6/11/4;
            
            TimerCount = 0;
        }
    }
}

在main.c声明

 extern float Motor1Speed ;
 extern float Motor2Speed ;

在main.c中输出速度即可 

      printf("Motor1Speed:%.2f\r\n",Motor1Speed);
      printf("Motor2Speed:%.2f\r\n",Motor2Speed);

八、PID速度控制

【PID算法 - 从入门到实战!】https://www.bilibili.com/video/BV1iP411x71X?vd_source=20e2569dfbc86cd3178a9555d0dd7ac2

使用匿名上位机曲线显示速度波形方便观察数据 

niming.c

#include "niming.h"
#include "main.h"
#include "usart.h"
uint8_t data_to_send[100];

//通过F1帧发送4个uint16类型的数据
void ANO_DT_Send_F1(uint16_t _a, uint16_t _b, uint16_t _c, uint16_t _d)
{
    uint8_t _cnt = 0;		//计数值
    uint8_t sumcheck = 0;  //和校验
    uint8_t addcheck = 0; //附加和校验
    uint8_t i = 0;
	data_to_send[_cnt++] = 0xAA;//帧头
    data_to_send[_cnt++] = 0xFF;//目标地址
    data_to_send[_cnt++] = 0xF1;//功能码
    data_to_send[_cnt++] = 8; //数据长度
	//单片机为小端模式-低地址存放低位数据,匿名上位机要求先发低位数据,所以先发低地址
	data_to_send[_cnt++] = BYTE0(_a);       
    data_to_send[_cnt++] = BYTE1(_a);
	
    data_to_send[_cnt++] = BYTE0(_b);
    data_to_send[_cnt++] = BYTE1(_b);
	
    data_to_send[_cnt++] = BYTE0(_c);
    data_to_send[_cnt++] = BYTE1(_c);
	
    data_to_send[_cnt++] = BYTE0(_d);
    data_to_send[_cnt++] = BYTE1(_d);
	 for ( i = 0; i < data_to_send[3]+4; i++)
    {
        sumcheck += data_to_send[i];//和校验
        addcheck += sumcheck;//附加校验
    }
    data_to_send[_cnt++] = sumcheck;
    data_to_send[_cnt++] = addcheck;
	HAL_UART_Transmit(&huart1,data_to_send,_cnt,0xFFFF);//这里是串口发送函数
}
//,通过F2帧发送4个int16类型的数据
void ANO_DT_Send_F2(int16_t _a, int16_t _b, int16_t _c, int16_t _d)   //F2帧  4个  int16 参数
{
    uint8_t _cnt = 0;
    uint8_t sumcheck = 0; //和校验
    uint8_t addcheck = 0; //附加和校验
    uint8_t i=0;
   data_to_send[_cnt++] = 0xAA;
    data_to_send[_cnt++] = 0xFF;
    data_to_send[_cnt++] = 0xF2;
    data_to_send[_cnt++] = 8; //数据长度
	//单片机为小端模式-低地址存放低位数据,匿名上位机要求先发低位数据,所以先发低地址
    data_to_send[_cnt++] = BYTE0(_a);
    data_to_send[_cnt++] = BYTE1(_a);
	
    data_to_send[_cnt++] = BYTE0(_b);
    data_to_send[_cnt++] = BYTE1(_b);
	
    data_to_send[_cnt++] = BYTE0(_c);
    data_to_send[_cnt++] = BYTE1(_c);
	
    data_to_send[_cnt++] = BYTE0(_d);
    data_to_send[_cnt++] = BYTE1(_d);
	
	  for ( i = 0; i < data_to_send[3]+4; i++)
    {
        sumcheck += data_to_send[i];
        addcheck += sumcheck;
    }

    data_to_send[_cnt++] = sumcheck;
    data_to_send[_cnt++] = addcheck;
	
	HAL_UART_Transmit(&huart1,data_to_send,_cnt,0xFFFF);//这里是串口发送函数
}
//通过F3帧发送2个int16类型和1个int32类型的数据
void ANO_DT_Send_F3(int16_t _a, int16_t _b, int32_t _c )   //F3帧  2个  int16 参数   1个  int32  参数
{
    uint8_t _cnt = 0;
    uint8_t sumcheck = 0; //和校验
    uint8_t addcheck = 0; //附加和校验
    uint8_t i=0;
    data_to_send[_cnt++] = 0xAA;
    data_to_send[_cnt++] = 0xFF;
    data_to_send[_cnt++] = 0xF3;
    data_to_send[_cnt++] = 8; //数据长度
	//单片机为小端模式-低地址存放低位数据,匿名上位机要求先发低位数据,所以先发低地址
    data_to_send[_cnt++] = BYTE0(_a);
    data_to_send[_cnt++] = BYTE1(_a);
	
    data_to_send[_cnt++] = BYTE0(_b);
    data_to_send[_cnt++] = BYTE1(_b);
	
    data_to_send[_cnt++] = BYTE0(_c);
    data_to_send[_cnt++] = BYTE1(_c);
    data_to_send[_cnt++] = BYTE2(_c);
    data_to_send[_cnt++] = BYTE3(_c);
	
	  for ( i = 0; i < data_to_send[3]+4; i++)
    {
        sumcheck += data_to_send[i];
        addcheck += sumcheck;
    }

    data_to_send[_cnt++] = sumcheck;
    data_to_send[_cnt++] = addcheck;

	HAL_UART_Transmit(&huart1,data_to_send,_cnt,0xFFFF);//这里是串口发送函数
}

niming.h

#ifndef  NIMING_H
#define  NIMING_H
#include "main.h"
//需要发送16位,32位数据,对数据拆分,之后每次发送单个字节
//拆分过程:对变量dwTemp 去地址然后将其转化成char类型指针,最后再取出指针所指向的内容
#define BYTE0(dwTemp)  (*(char *)(&dwTemp))
#define BYTE1(dwTemp)  (*((char *)(&dwTemp) + 1))
#define BYTE2(dwTemp)  (*((char *)(&dwTemp) + 2))
#define BYTE3(dwTemp)  (*((char *)(&dwTemp) + 3))


void ANO_DT_Send_F1(uint16_t, uint16_t _b, uint16_t _c, uint16_t _d);
void ANO_DT_Send_F2(int16_t _a, int16_t _b, int16_t _c, int16_t _d);
void ANO_DT_Send_F3(int16_t _a, int16_t _b, int32_t _c );

#endif 


添加测试代码

    if(Motor1Speed>3.1) Motor1Pwm--;
    if(Motor1Speed<2.9) Motor1Pwm++;
    if(Motor2Speed>3.1) Motor2Pwm--;
    if(Motor2Speed<2.9) Motor2Pwm++;
    Motor_Set(Motor1Pwm,Motor2Pwm);
    printf("Motor1Speed:%.2f Motor1Pwm:%d\r\n",Motor1Speed,Motor1Pwm);
    printf("Motor2Speed:%.2f Motor2Pwm:%d\r\n",Motor2Speed,Motor2Pwm);
    HAL_Delay(10);
    
    //电机速度等信息发送到上位机
    //注意上位机不支持浮点数,所以要乘100
    ANO_DT_Send_F2(Motor1Speed*100, 3.0*100,Motor2Speed*100,3.0*100);

PID代码

pid.c

#include "pid.h"

//定义一个结构体类型变量
tpid pidMotor1Speed;
//给结构体类型变量赋初值
void PID_init(void)
{
    pidMotor1Speed.actual_val = 0.0;
    pidMotor1Speed.target_val = 0.00;
    pidMotor1Speed.err = 0.0;
    pidMotor1Speed.err_last = 0.0;
    pidMotor1Speed.err_sum  = 0.0;
    pidMotor1Speed.kp = 0;
    pidMotor1Speed.ki = 0;
    pidMotor1Speed.kd = 0;
}
//比例p调节控制函数
float P_realize(tpid * pid, float actual_val)
{
    pid->actual_val = actual_val; //传递真实值
    pid->err = pid->target_val - pid->actual_val; //当前误差=目标值-真实值
    //比例控制调节    输出=Kp*当前误差
    pid->actual_val = pid->kp*pid->err;
    return pid->actual_val;
}
//比例P 积分I 控制函数
float PI_realize(tpid * pid, float actual_val)
{
    pid->actual_val = actual_val; //传递真实值
    pid->err = pid->target_val - pid->actual_val; //当前误差=目标值-真实值
    pid->err_sum += pid->err;//误差累计值 = 当前误差累计和
    //使用PI控制 输出=Kp*当前误差+Ki*误差累计值
    pid->actual_val = pid->kp*pid->err + pid->ki*pid->err_sum;
    return pid->actual_val;
}
// PID控制函数
float PID_realize(tpid * pid, float actual_val)
{
    pid->actual_val = actual_val; //传递真实值
    pid->err = pid->target_val - pid->actual_val; //当前误差=目标值-真实值
    pid->err_sum += pid->err;//误差累计值 = 当前误差累计和
    //使用PID控制 输出 = Kp*当前误差  +  Ki*误差累计值 + Kd*(当前误差-上次误差)
    pid->actual_val = pid->kp*pid->err + pid->ki*pid->err_sum + pid->kd*(pid->err - pid->err_last);
    //保存上次误差: 这次误差赋值给上次误差
    pid->err_last = pid->err;
    
    return pid->actual_val;
}

pid.h

#ifndef __PID_H
#define __PID_H

//声明一个结构体类型
typedef struct
{
    float target_val;//目标值
    float actual_val;//实际值
    float err;       //当前偏差
    float err_last;  //上次偏差
    float err_sum;   //误差累计值
    float kp,ki,kd;  //比例 积分 微分系数

} tpid;
//声明函数
void PID_init(void);
float P_realize(tpid * pid, float actual_val);
float PI_realize(tpid * pid, float actual_val);
float PID_realize(tpid * pid, float actual_val);


#endif




然后在main函数中调用PID_init()函数,别忘把头文件给包含进去。

使用cJSON方便调参

1、调大堆栈都改为0x800

2、开启串口一的全局中断

__HAL_UART_ENABLE_IT(&huart1,UART_IT_RXNE);    
//开启串口1接收中断

中断回调函数

uint8_t Usart1_ReadBuf[256];    //串口1 缓冲数组
uint8_t Usart1_ReadCount = 0;   //串口1 接收字节计数
if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_RXNE))//判断huart1 是否读到字节
{
     if(Usart1_ReadCount >= 255) Usart1_ReadCount = 0;
     HAL_UART_Receive(&huart1,&Usart1_ReadBuf[Usart1_ReadCount++],1,1000);
 }

编写函数用于判断串口是否发送完一帧数据

 //判断否接收完一帧数据
uint8_t Usart_WaitReasFinish(void)
{
    static uint16_t Usart_LastReadCount = 0; //记录上次的计数值
    if(Usart1_ReadCount == 0)
    {
        Usart_LastReadCount = 0;
        return 1;//表示没有在接收数据
    }
    if(Usart1_ReadCount == Usart_LastReadCount)
    {
        Usart1_ReadCount = 0;
        Usart_LastReadCount = 0;
        return 0;//已经接收完成了
    }
    Usart_LastReadCount = Usart1_ReadCount;
    return 2;//表示正在接受中
}

把cJSON的.c.h放到工程中去,并在main函数中加入以下代码

#include "cJSON.h"
 #include <string.h>
cJSON *cJsonData ,*cJsonVlaue;

    if(Usart_WaitReasFinish() == 0)//是否接收完毕
    {
        cJsonData  = cJSON_Parse((const char *)Usart1_ReadBuf);
        if(cJSON_GetObjectItem(cJsonData,"p") !=NULL)
        {
            cJsonVlaue = cJSON_GetObjectItem(cJsonData,"p");
            p = cJsonVlaue->valuedouble;
            pidMotor1Speed.kp = p;
        }
        if(cJSON_GetObjectItem(cJsonData,"i") !=NULL)
        {
            cJsonVlaue = cJSON_GetObjectItem(cJsonData,"i");
            i = cJsonVlaue->valuedouble;
            pidMotor1Speed.ki = i;
        }
        if(cJSON_GetObjectItem(cJsonData,"d") !=NULL)
        {
            cJsonVlaue = cJSON_GetObjectItem(cJsonData,"d");
            d = cJsonVlaue->valuedouble;
            pidMotor1Speed.kd = d;
        }
        if(cJSON_GetObjectItem(cJsonData,"a") !=NULL)
        {
            cJsonVlaue = cJSON_GetObjectItem(cJsonData,"a");
            a = cJsonVlaue->valuedouble;
            pidMotor1Speed.target_val =a;
        }
        if(cJsonData != NULL)
        {
            cJSON_Delete(cJsonData);//释放空间、但是不能删除cJsonVlaue不然会 出现异常错误
        }
        memset(Usart1_ReadBuf,0,255);//清空接收buf,注意这里不能使用strlen
    
    }

     printf("P:%.3f  I:%.3f  D:%.3f A:%.3f\r\n",p,i,d,a);

九、PID整定方法

把PID控制函数放到中断里面循环调用定时执行

if(TimerCount % 10 == 0)//每20ms执行一次
        {
            Motor_Set(PID_realize(&pid1_speed, Motor1Speed), PID_realize(&pid2_speed, Motor2Speed));
            TimerCount = 0;
        }

//定义一个结构体类型变量
tpid pidMotor1Speed;
tpid pidMotor2Speed;
//给结构体类型变量赋初值
void PID_init(void)
{
    pidMotor1Speed.actual_val=0.0;
    pidMotor1Speed.target_val=0.00;
    pidMotor1Speed.err=0.0;
    pidMotor1Speed.err_last=0.0;
    pidMotor1Speed.err_sum=0.0;
    pidMotor1Speed.kp=0;
    pidMotor1Speed.ki=0;
    pidMotor1Speed.kd=0;

    pidMotor2Speed.actual_val=0.0;
    pidMotor2Speed.target_val=0.00;
    pidMotor2Speed.err=0.0;
    pidMotor2Speed.err_last=0.0;
    pidMotor2Speed.err_sum=0.0;
    pidMotor2Speed.kp=0;
    pidMotor2Speed.ki=0;
    pidMotor2Speed.kd=0;
}

十、实现小车前后左右运动

//motorPidSetSpeed(1,2);//向右转弯
//motorPidSetSpeed(2,1);//向左转弯
//motorPidSetSpeed(1,1);//前进
//motorPidSetSpeed(-1,-1);//后退
//motorPidSetSpeed(0,0);//停止
//motorPidSetSpeed(-1,1);//右原地旋转
//motorPidSetSpeed(1,-1);//左原地旋转

void motorPidSetSpeed(float Motor1SetSpeed,float Motor2SetSpeed)
{
    //改变电机PID参数的目标速度
    pidMotor1Speed.target_val = Motor1SetSpeed;
    pidMotor2Speed.target_val = Motor2SetSpeed;
    //根据PID计算 输出作用于电机
    Motor_Set(PID_realize(&pidMotor1Speed,Motor1Speed),PID_realize(&pidMotor2Speed,Motor2Speed));
}
//向前加速函数
void motorSpeedUp(void)
{
    static float MotorSetSpeedUp = 0.5;//静态变量 函数结束变量不会销毁
    if(MotorSetSpeedUp <= MAX_SPEED_UP) MotorSetSpeedUp += 0.5; //如果没有超过最大值就增加0.5
    motorPidSetSpeed(MotorSetSpeedUp, MotorSetSpeedUp);//设置到电机
}
//向前减速函数
void motorSpeedCut(void)
{
    static float MotorSetSpeedCut = 3;//静态变量 函数结束变量不会销毁
    if(MotorSetSpeedCut >= 0.5) MotorSetSpeedCut -= 0.5;
    motorPidSetSpeed(MotorSetSpeedCut, MotorSetSpeedCut);
}

在main函数写以下代码其中一个就能实现对小车的控制

//motorPidSetSpeed(1,2);//向右转弯
//motorPidSetSpeed(2,1);//向左转弯
//motorPidSetSpeed(1,1);//前进
//motorPidSetSpeed(-1,-1);//后退
//motorPidSetSpeed(0,0);//停止
//motorPidSetSpeed(-1,1);//右原地旋转
//motorPidSetSpeed(1,-1);//左原地旋转

十一、OLED显示速度与历程

/*里程数(cm) += 时间周期(s)*车轮转速(转/s)*车轮周长(cm)*/
            Mileage += 0.02*Motor1Speed*22;

extern float Mileage;//里程数
uint8_t OledString[20];
/*******************
*  sprintf 函数说明   函数sprintf()用来作格式化的输出
*  函数sprintf()的用法和printf()函数一样,只是sprintf()函数给出第一个参数string(一般为字符数组)
*  一定要在调用sprintf之前分配足够大的空间给buf。
*
 *******************/
    sprintf((char*)OledString, "V1:%.2fV2:%.2f", Motor1Speed, Motor2Speed);//显示两个电机的速度
    OLED_ShowString(0, 0, OledString, 12);//这个是oled驱动里面的,是显示位置的一个函数
    
    sprintf((char*)OledString, "Mileage:%.2f", Mileage);//显示里程数
    OLED_ShowString(0, 1, OledString, 12);
    

十二、OLED显示ADC采集电压

原理图

ADC连接PA4引脚

adc.c

别忘在adc.h中声明电池电压函数

/*******************
*  @brief  电池电压测量函数
*  @param
*  @return  小车电池电压
*
 *******************/
float adcGetBatteryVoltage(void)
{
    HAL_ADC_Start(&hadc2);//启动ADC转化
    if(HAL_OK == HAL_ADC_PollForConversion(&hadc2,50))//等待转化完成、超时时间50ms
        return (float)HAL_ADC_GetValue(&hadc2)/4096*3.3*5;//计算电池电压
    return -1;
}

在main函数中加

    sprintf((char*)OledString, "U:%.2fV", adcGetBatteryVoltage());
    OLED_ShowString(0,2,OledString,12);

十三、PID循迹

DO 高电平->有黑线 小灯灭

DO低电平->没有黑线 小灯亮

原理图

OUT_1-PA5、OUT_2-PA7、OUT_3-PB0、OUT_4-PB1

#define READ_HW_OUT_1 HAL_GPIO_ReadPin(HW_OUT_1_GPIO_Port, HW_OUT_1_Pin)
//读取红外对管连接的GPIO电平
#define READ_HW_OUT_2 HAL_GPIO_ReadPin(HW_OUT_2_GPIO_Port, HW_OUT_2_Pin)
#define READ_HW_OUT_3 HAL_GPIO_ReadPin(HW_OUT_3_GPIO_Port, HW_OUT_3_Pin)
#define READ_HW_OUT_4 HAL_GPIO_ReadPin(HW_OUT_4_GPIO_Port, HW_OUT_4_Pin)

 在pid.c中加一下代码

    tPid pidHW_Tracking;//红外循迹的PID    


    pidHW_Tracking.actual_val = 0.0;
    pidHW_Tracking.target_val = 0.00;//红外循迹PID 的目标值为0
    pidHW_Tracking.err = 0.0;
    pidHW_Tracking.err_last = 0.0;
    pidHW_Tracking.err_sum = 0.0;
    pidHW_Tracking.Kp = -1.50;
    pidHW_Tracking.Ki = 0;
    pidHW_Tracking.Kd = 0.80;

在main函数中加一下代码

extern tPid pidHW_Tracking;//红外循迹的PID
 uint8_t g_ucaHW_Read[4] = {0};//保存红外对管电平的数组
int8_t g_cThisState = 0;//这次状态
int8_t g_cLastState = 0; //上次状态
float g_fHW_PID_Out;//红外对管PID计算输出速度
float g_fHW_PID_Out1;//电机1的最后循迹PID控制速度
float g_fHW_PID_Out2;//电机2的最后循迹PID控制速度



g_ucaHW_Read[0] = READ_HW_OUT_1;//读取红外对管状态、这样相比于写在if里面更高效
g_ucaHW_Read[1] = READ_HW_OUT_2;
g_ucaHW_Read[2] = READ_HW_OUT_3;
g_ucaHW_Read[3] = READ_HW_OUT_4;

if(g_ucaHW_Read[0] == 0 && g_ucaHW_Read[1] == 0 && g_ucaHW_Read[2] == 0 && g_ucaHW_Read[3] == 0)
{
    g_cThisState = 0;//前进
}
else if(g_ucaHW_Read[0] == 0 && g_ucaHW_Read[1] == 1 && g_ucaHW_Read[2] == 0 && g_ucaHW_Read[3] == 0)
{
     g_cThisState = -1;//右转
}
else if(g_ucaHW_Read[0] == 1 && g_ucaHW_Read[1] == 0 && g_ucaHW_Read[2] == 0 && g_ucaHW_Read[3] == 0)
{
     g_cThisState = -2;//快速右转
}
else if(g_ucaHW_Read[0] == 1 && g_ucaHW_Read[1] == 1 && g_ucaHW_Read[2] == 0 && g_ucaHW_Read[3] == 0)
{
    g_cThisState = -3;//快速右转
}
else if(g_ucaHW_Read[0] == 0 && g_ucaHW_Read[1] == 0 && g_ucaHW_Read[2] == 1 && g_ucaHW_Read[3] == 0)
{
    g_cThisState = 1;//左转
}
else if(g_ucaHW_Read[0] == 0 && g_ucaHW_Read[1] == 0 && g_ucaHW_Read[2] == 0 && g_ucaHW_Read[3] == 1)
{
    g_cThisState = 2;//快速左转
}
else if(g_ucaHW_Read[0] == 0 && g_ucaHW_Read[1] == 0 && g_ucaHW_Read[2] == 1 && g_ucaHW_Read[3] == 1)
{
    g_cThisState = 3;//快速左转
}
g_fHW_PID_OUT = PID_realize(&pidHW_Tracking, g_cThisState);//PID计算输出目标速度 这个速度,会和基础速度加减

g_fHW_PID_OUT1 = 3 + g_fHW_PID_OUT;//电机1速度=基础速度+循迹PID输出速度
g_fHW_PID_OUT2 = 3 - g_fHW_PID_OUT;//电机1速度=基础速度-循迹PID输出速度
if(g_fHW_PID_OUT1 > 5) g_fHW_PID_OUT1 = 5;//进行限幅 限幅速度在0-5之间
if(g_fHW_PID_OUT1 < 0) g_fHW_PID_OUT1 = 0;
if(g_fHW_PID_OUT2 > 5) g_fHW_PID_OUT2 = 5;
if(g_fHW_PID_OUT2 < 0) g_fHW_PID_OUT2 = 0;
if(g_cThisState != g_cLastState)//如何这次状态不等于上次状态、就进行改变目标速度和控制电机、在定时器中依旧定时控制电机
{
    motorPidSetSpeed(g_fHW_PID_OUT1, g_fHW_PID_OUT2);//通过计算的速度控制电机
}
g_cLastState = g_cThisState;//保存上次红外对管状态

十四、手机遥控

原理图

CubeMx配置

1、点击USART3模式选择异步通信

2、打开串口三全局中断

打开串口接收数据

  HAL_UART_Receive_IT(&huart3, &g_ucUsart3ReceiveData, 1);  //串口三接收数据

重定义串口回调函数

uint8_t g_ucUsart3ReceiveData;  //保存串口三接收的数据
//串口接收回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if( huart == &huart3)//判断中断源
    {
        if(g_ucUsart3ReceiveData == 'A') motorPidSetSpeed(1,1);//前运动
        if(g_ucUsart3ReceiveData == 'B') motorPidSetSpeed(-1,-1);//后运动
        if(g_ucUsart3ReceiveData == 'C') motorPidSetSpeed(0,0);//停止
        if(g_ucUsart3ReceiveData == 'D') motorPidSetSpeed(1,2);//右边运动   
        if(g_ucUsart3ReceiveData == 'E') motorPidSetSpeed(2,1);//左边运动
        if(g_ucUsart3ReceiveData == 'F') motorSpeedUp();//加速
        if(g_ucUsart3ReceiveData == 'G') motorSpeedCut();//减速
        
        HAL_UART_Receive_IT( &huart3, &g_ucUsart3ReceiveData, 1);//继续进行中断接收
    }
}

十五、超声波避障

GPIO工作模式

原理图

具体问题可参考HC_SR04工作原理

Trig(PB5)我们配置为GPIO输出

Echo(PA6)我们配置GPIO输入功能

CubeMx配置

HC_SR04.c

#include "HC_SR04.h"

//因为我们不适用定时器所以我们需要自己写一个us级延时函数
/*******************
 *  @brief  us级延时
*  @param  usdelay:要延时的us时间
*  @return  
*
*******************/
void HC_SR04_Delayus(uint32_t usdelay)
{
    __IO uint32_t Delay = usdelay * (SystemCoreClock / 8U / 1000U / 1000);//SystemCoreClock:系统频率
    do
    {
        __NOP();
    }
    while(Delay --);
}
 /*******************
*  @brief  HC_SR04读取超声波距离
*  @param  无
*  @return 障碍物距离单位:cm (静止表面平整精度更高) 
*注意:两个HC_SR04_Read()函数调用的时间间隔要2ms及以上,测量范围更大 精度更高 
*******************/
float HC_SR04_Read(void)
{
    uint32_t i = 0;
    float Distance;
    HAL_GPIO_WritePin(HC_SR04_Ting_GPIO_Port, HC_SR04_Ting_Pin, GPIO_PIN_SET);//输出15us高电平
    HC_SR04_Delayus(15);
    HAL_GPIO_WritePin(HC_SR04_Ting_GPIO_Port, HC_SR04_Ting_Pin, GPIO_PIN_RESET);//高电平输出结束,设置为低电平
    
    while(HAL_GPIO_ReadPin(HC_SR04_Echo_GPIO_Port, HC_SR04_Echo_Pin) == GPIO_PIN_RESET)//等待回响高电平
    {
        i++;
        HC_SR04_Delayus(1);
        if(i>10000) return -1;//超时退出循环、防止程序卡死这里
    }
    i = 0;
    while(HAL_GPIO_ReadPin(HC_SR04_Echo_GPIO_Port, HC_SR04_Echo_Pin) == GPIO_PIN_SET)//下面循环是2us
    {
        i = i+1;
        HC_SR04_Delayus(1);//1us 延时,但是整个循环大概是2us左右(因为延时1us 42-44行代码跑也需要一定的时间)
        if(i>10000) return -2;//超时退出循环
    }
    Distance = i*2*0.033/2;//这里乘2的原因是上面的2us
    return Distance;
}

HC_SR04.h

#ifndef __HC_SR04_H
#define __HC_SR04_H
#include "main.h"

void HC_SR04_Delayus(uint32_t usdelay);
float HC_SR04_Read(void);


#endif


main函数中加

//避障逻辑
if(HC_SR04_Read() > 25)//前方无障碍
{
    motorPidSetSpeed(1,1);//前运动
    HAL_Delay(100);
}
else//前方有障碍物
{
    motorPidSetSpeed(-1,1);//向右原地转
    HAL_Delay(500);
    if(HC_SR04_Read() > 25)//右边无障碍
    {
        motorPidSetSpeed(1,1);//前运动
        HAL_Delay(100);
    }
    else//右边有障碍
    {
        motorPidSetSpeed(1,-1);//向左原地转
        HAL_Delay(1000);
        if(HC_SR04_Read() > 25)//左边无障碍
        {
            motorPidSetSpeed(1,1);//前运动
            HAL_Delay(100);
        }
        else
        {
            motorPidSetSpeed(-1,-1);//后退
            HAL_Delay(1000);
            motorPidSetSpeed(-1,1);//右运动
            HAL_Delay(50);
        }
    }
}

十六、超声波(PID)跟随

 pid.c中加以下代码

tPid pidFollow;//定距离跟随PID



    pidFollow.actual_val=0.0;
    pidFollow.target_val=22.50;
    pidFollow.err=0.0;
    pidFollow.err_last=0.0;
    pidFollow.err_sum=0.0;
    pidFollow.kp=-0.5; //定距离跟随的Kp大小通过估算PID输入输出数据,确定大概大小,然后在调试
    pidFollow.ki=-0.001;
    pidFollow.kd=0;

main.c中加以下代码

 extern tpid pidFollow;

float g_fHC_SR04_Read;//超声波传感器读取障碍物数据
float g_fFollow_PID_OUT;//定距离跟随PID计算输出速度




    g_fHC_SR04_Read = HC_SR04_Read();//读取前方障碍物距离
    if(g_fHC_SR04_Read < 60)//如果前60cm 有东西就启动跟随
    {
        g_fFollow_PID_OUT = PID_realize(&pidFollow,g_fHC_SR04_Read);//PID计算输出目标速度 这个速度,会和基础速度加减
        if(g_fFollow_PID_OUT > 6) g_fFollow_PID_OUT = 6;//对输出速度限幅
        if(g_fFollow_PID_OUT < -6) g_fFollow_PID_OUT = -6;
        motorPidSetSpeed(g_fFollow_PID_OUT, g_fFollow_PID_OUT);//速度作用与电机上
    }
    else motorPidSetSpeed(0,0);//如果前面60cm 没有东西就停止
    HAL_Delay(10);//读取超声波传感器不能过快

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

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

相关文章

Go微服务: 关于消息队列的选择和分类以及使用场景

消息队列概述 在分布式系统和微服务架构中&#xff0c;消息队列&#xff08;Message Queue&#xff09;是一个核心组件&#xff0c;用于在不同的应用程序或服务之间异步传递消息在 Go 语言中&#xff0c;有多种实现消息队列的方式&#xff0c;包括使用开源的消息队列服务&…

2024 年最全的 21 款数据恢复工具软件汇总

使用其中任何一款免费数据恢复工具&#xff0c;您都可以找回那些您认为已经永远消失的文件。我根据这些程序对我而言的易用性和它们提供的功能对这些程序进行了排名。 这些应用程序从您的硬盘、USB 驱动器、媒体卡等恢复文档、视频、图像、音乐等。我建议每个计算机所有者都安装…

附录二-nmap基本用法

参考 黑客工具—Nmap的使用_哔哩哔哩_bilibili nmap是扫描IP和端口的&#xff0c;相当于攻击前的索敌步骤。不止网络安全方面会用到&#xff0c;平时运维的时候也会用到nmap 1 下载nmap nmap官网 https://nmap.org/ 点击下载&#xff0c;然后点你用的平台就行了 往下滚可以…

[职场] 硬件研发是什么职业 #职场发展#其他

硬件研发是什么职业 硬件研发工程师需要具备丰富的电子电路知识、熟练掌握各种电子元器件的性能及应用、具备一定的机械结构设计能力&#xff0c;同时还要具备良好的团队协作和沟通能力。本文会进行详细介绍。 一、硬件研发是什么 硬件研发&#xff0c;全称硬件研发工程师&am…

一文搞懂Python的异常

人生之事,不如意者十之有九。 在编程中亦是如此。异常(Exception),遍布于程序各个角落,开发工作的大部分coding,都是为了应对和解决它。 概念 异常,简而言之,是程序在执行期间发生的非预期的、非正常的事件或情况。 举个实际生活的例子: 你周末出门买大龙虾,但当你…

Vue-App桌面程序列表

Vue-App桌面程序列表 文章说明讲解视频核心代码效果展示项目链接 文章说明 采用Vue实现PC端的桌面程序列表&#xff0c;采用HBuilderX将程序转化为5App&#xff0c;实现移动端的适配&#xff1b;支持桌面打开新应用&#xff0c;底部导航展示当前应用列表&#xff0c;可切换或关…

用你熟悉的语言就能开发智能合约,Vara Network 以 WASM 解锁未来应用创新

Vara Network 自推出以来&#xff0c;凭借其基于 Gear Protocol 的独特架构和强大的开发工具&#xff0c;为开发者提供了一个高效、安全的智能合约构建平台。Vara Network 通过采用先进的 Actor 模型、持久内存概念和 WebAssembly 技术&#xff0c;实现了异步消息处理、并行计算…

在线OJ项目测试(selenium+Junit5)

目录 在线OJ项目测试的思维导图 在线OJ的UI自动化测试 测试一&#xff1a;检查未登录时的页面访问以及一些未登录时的非法操作 测试二&#xff1a;测试注册界面 测试三&#xff1a;测试登录界面 测试四&#xff1a;测试题目列表界面 测试五&#xff1a;测试题目详情界面…

Android Kotlin 异步操作回调转换为挂起函数

异步接口回调是一种通过接口将任务的执行和结果处理分离开来的编程设计模式。通常用于网络请求、数据库查询等耗时操作。 挂起函数是 Kotlin 中的一个特性&#xff0c;用于简化异步编程。挂起函数是可以在协程中暂停执行并恢复的函数&#xff0c;避免了回调地狱问题&#xff0…

php质量工具系列之PHPCPD

PHPCPD 用于检测重复代码&#xff0c;直观的说就是复制粘贴再稍微改改 该工具作者已经 停止维护 安装 composer global require --dev sebastian/phpcpd执行 phpcpd --log-pmd phpcpd_result.xml ./app参数介绍 --log-pmd 将结果保存在phpcpd_result.xml 中 ./app 是phpcpd扫…

linux系统PXE自动装机和无人值守

一、PXE 1.PXE&#xff1a;c/s模式&#xff0c;允许客户端通过网络从远程服务器&#xff08;服务端&#xff09;下载引导镜像&#xff0c;加载安装文件&#xff0c;实现自动化安装操作系统。&#xff08;c/s客户端和服务端都可以是多台&#xff09; 2.PXE优点&#xff1a;规模…

【机器学习基础】Python编程08:五个实用练习题的解析与总结

Python是一种广泛使用的高级编程语言,它在机器学习领域中的重要性主要体现在以下几个方面: 简洁易学:Python语法简洁清晰,易于学习,使得初学者能够快速上手机器学习项目。 丰富的库支持:Python拥有大量的机器学习库,如scikit-learn、TensorFlow、Keras和PyTorch等,这些…

基于STM32开发的智能家居监控系统

目录 引言环境准备智能家居监控系统基础代码实现&#xff1a;实现智能家居监控系统 4.1 传感器数据读取4.2 电器设备控制4.3 实时数据监控与分析4.4 用户界面与数据可视化应用场景&#xff1a;家庭安全监控与管理问题解决方案与优化收尾与总结 1. 引言 随着智能家居技术的发…

AI绘画中的图像格式技术

在数字艺术的广阔天地里&#xff0c;AI绘画作为一种新兴的艺术形式&#xff0c;正在逐渐占据一席之地。不同于传统绘画&#xff0c;AI绘画依赖于复杂的算法和机器学习模型来生成图像&#xff0c;而这一切的背后&#xff0c;图像格式技术发挥着至关重要的作用。图像格式不仅关系…

23 二叉搜索树

本节目标 1.内容安排说明 2.二叉搜索树实现 3.应用分析 4.进阶题 1. 内容安排说明 二叉树在c数据结构已经说过了&#xff0c;本节内容是因为&#xff1a; map和set特性需要先铺垫二叉搜索树&#xff0c;而二叉搜索树也是一种树形结构二叉搜索树的特性了解&#xff0c;有助于…

LNMP与动静态网站介绍

Nginx发展 Nginx nginx http server Nginx是俄罗斯人 Igor Sysoev(伊戈尔.塞索耶夫)开发的一款高性能的HTTP和反向代理服务器。 Nginx以高效的epoll.kqueue,eventport作为网络IO模型&#xff0c;在高并发场景下&#xff0c;Nginx能够轻松支持5w并发连接数的响应&#xff0c;并…

OrangePi AIpro Ubuntu 22.04 aarch64 安装MySql 8.0

查看MySQL安装包 接下来可以使用以下命令安装MySQL服务器&#xff1a; 安装MySQL 8.0 # 安装最新版本 sudo apt install -y mysql-server # 安装指定版本 sudo apt install -y mysql-server-8.0初始化配置信息 sudo mysql_secure_installationVALIDATE PASSWORD COMPONENT ca…

算法分析与设计期末考试复习(更新ing)

重点内容&#xff1a; 绪论&#xff1a; 简单的递推方程求解 1.19(1)(2) 、 教材例题 多个函数按照阶的大小排序 1.18 分治法&#xff1a; 分治法解决芯片测试问题 计算a^n的复杂度为logn的算法&#xff08;快速幂&#xff09; 分治法解决平面最近点对问…

SecureCRT[po破] for Mac SSH终端操作工具[解] 安装教程

文章目录 效果一、准备工作二、开始安装1、双击运行软件&#xff0c;将其从左侧拖入右侧文件夹中&#xff0c;等待安装完毕2、 应用程序显示软件图标&#xff0c;表示安装成功 三、输入对应参数1、解决“软件已损坏&#xff0c;无法打开&#xff0c;要移到废纸篓”问题解决步骤…

从零开始实现自己的串口调试助手(8)-循环发送

循环发送 准备 创建槽函数 设置QSpinBox的最大值 注意&#xff1a; // 我们不能在qt的ui线程中延时&#xff0c;否则将导致页面刷新问题 //QThread::msleep(ui->spinBox->text().toInt());//设置下次发送时间间隔 定时器实现 关联信号与槽: //添加自动换行定…