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

我们使用的是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灯的代码给注释掉。

cpp 复制代码
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文件添加到到我们的工程分组里面并修改一些错误。

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

cpp 复制代码
#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显示屏

cpp 复制代码
    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

cpp 复制代码
/**
 * @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在生成代码时,有很多外设初始化完成后默认是关闭的。需要我们手动开启。

cpp 复制代码
    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

使用这个修改占空比

cpp 复制代码
__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

cpp 复制代码
#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

cpp 复制代码
#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

进行测试

cpp 复制代码
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、开启更新中断

开启定时器以及中断

cpp 复制代码
  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 中断

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

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

定时器溢出时间计算公式

cpp 复制代码
/*******************
*  @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声明

cpp 复制代码
 extern float Motor1Speed ;
 extern float Motor2Speed ;

在main.c中输出速度即可

cpp 复制代码
      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

cpp 复制代码
#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

cpp 复制代码
#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 

添加测试代码

cpp 复制代码
    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

cpp 复制代码
#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

cpp 复制代码
#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、开启串口一的全局中断

cpp 复制代码
__HAL_UART_ENABLE_IT(&huart1,UART_IT_RXNE);    
//开启串口1接收中断

中断回调函数

cpp 复制代码
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);
 }

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

cpp 复制代码
 //判断否接收完一帧数据
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函数中加入以下代码

cpp 复制代码
#include "cJSON.h"
 #include <string.h>
cpp 复制代码
cJSON *cJsonData ,*cJsonVlaue;
cpp 复制代码
    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控制函数放到中断里面循环调用定时执行

cpp 复制代码
if(TimerCount % 10 == 0)//每20ms执行一次
        {
            Motor_Set(PID_realize(&pid1_speed, Motor1Speed), PID_realize(&pid2_speed, Motor2Speed));
            TimerCount = 0;
        }
cpp 复制代码
//定义一个结构体类型变量
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;
}

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

cpp 复制代码
//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函数写以下代码其中一个就能实现对小车的控制

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

十一、OLED显示速度与历程

cpp 复制代码
/*里程数(cm) += 时间周期(s)*车轮转速(转/s)*车轮周长(cm)*/
            Mileage += 0.02*Motor1Speed*22;
cpp 复制代码
extern float Mileage;//里程数
uint8_t OledString[20];
cpp 复制代码
/*******************
*  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中声明电池电压函数

cpp 复制代码
/*******************
*  @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函数中加

cpp 复制代码
    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

cpp 复制代码
#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中加一下代码

cpp 复制代码
    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函数中加一下代码

cpp 复制代码
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、打开串口三全局中断

打开串口接收数据

cpp 复制代码
  HAL_UART_Receive_IT(&huart3, &g_ucUsart3ReceiveData, 1);  //串口三接收数据

重定义串口回调函数

cpp 复制代码
uint8_t g_ucUsart3ReceiveData;  //保存串口三接收的数据
cpp 复制代码
//串口接收回调函数
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

cpp 复制代码
#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

cpp 复制代码
#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函数中加

cpp 复制代码
//避障逻辑
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中加以下代码

cpp 复制代码
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中加以下代码

cpp 复制代码
 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);//读取超声波传感器不能过快
相关推荐
国科安芯36 分钟前
ASP4644芯片低功耗设计思路解析
网络·单片机·嵌入式硬件·安全
充哥单片机设计1 小时前
【STM32项目开源】基于STM32的智能厨房火灾燃气监控
stm32·单片机·嵌入式硬件
CiLerLinux8 小时前
第四十九章 ESP32S3 WiFi 路由实验
网络·人工智能·单片机·嵌入式硬件
时光の尘8 小时前
【PCB电路设计】常见元器件简介(电阻、电容、电感、二极管、三极管以及场效应管)
单片机·嵌入式硬件·pcb·二极管·电感·三极管·场效应管
Lu Zelin8 小时前
单片机为什么不能跑Linux
linux·单片机·嵌入式硬件
宁静致远20219 小时前
stm32 freertos下基于hal库的模拟I2C驱动实现
stm32·嵌入式硬件·freertos
Wave84513 小时前
STM32--智能小车
stm32·单片机·嵌入式硬件
wdfk_prog16 小时前
[Linux]学习笔记系列 -- lib/timerqueue.c Timer Queue Management 高精度定时器的有序数据结构
linux·c语言·数据结构·笔记·单片机·学习·安全
helesheng18 小时前
用低成本FPGA实现FSMC接口的多串口(UART)控制器
stm32·fsmc·fpga·uart控制器
充哥单片机设计19 小时前
【STM32项目开源】基于STM32的智能家居环境(空气质量)检测系统
stm32·单片机·嵌入式硬件