简介
小车包含如下功能:PWM驱动电机、编码器测速、PID速度控制、OLED显示、循迹、手机遥控、超声波避障和跟随、MPU6050指定轨迹、扩展排针(方便后续扩展)、摄像头模块(可以搭配openmv)、FreeRTOS定时器(我们保留着,之后我会扩展)。
硬件部分
材料选购
电源
1、12v锂电池组18650充电带保护板大容量电瓶通用移动电源便携蓄电池。
小车重要结构类似三轮或四轮结构均可
1、常用M3螺丝包 螺母 3*5 3*8 3*10 3*12 3*12 圆头平头螺丝。
2、常用M3 铜柱 包 M3*5 6 8 10 12 15 +6 双通 M3螺母 螺丝。
3、STM32小车车体亚克力板。(亚克力板要能安装对应电机和万向轮)
4、JGA25-370直流减速电机。
5、真空W420 万向轮或者是牛眼轮滚珠万向球CY-15-25A型号:CY-12A(全碳钢)
线材
1、XH2.54 3P 单双头电子线 红白排线长20CM。
2、XH2.54 4P 单头电子线 红白排线 长20CM。
3、杜邦线 母对母 公对母公对公40P彩色排线连接线10/15/30/20/40CM。
PCB元件
1、XH2.54接插件 2.54mm 。
2、XH2.54接插件 2.54mm 。
3、单排针 间距2.54MM 。(买长的,短的直接锯)
4、间距2.54MM 单排直插母座。(买长的,短的直接锯)
5、DC-005电源座 DC电源插座5.5*2.1MM DC2.1。
6、滑动开关 插件大电流拨动开关 SS-12D10L2.5。
7、0603封装20K电阻和0603封装10K电阻。
8、跳线帽 短路帽 间距2.54MM。
电子元件
1、USB转TTL CH340模块 USB转串口 .
2、ST-LINK V2 STM8/STM32仿真器编程器。
3、DC-DC 3A迷你降压模块 车载电源6V9V12V-30V转3.3V/5V输出。
4、A4950直流有刷电机驱动板模块 双路电机驱动模块。
5、STM32f103c8t6最小系统板。
6、OLED显示屏模块 0.96寸 IIC/SPI 。
7、HC-SR04 超声波测距模块。
8、寻迹传感器 TCRT5000红外反射传感器。
9、HC-05 主从机一体蓝牙串口透传模块。
10、MPU-6050模块 三轴加速度陀螺仪6DOF GY-521
原理图以及PCB
结构与组装
焊接电子元件
插上各个模块
将板子用铜柱固定在小车底盘,并把模块的线接好
软件部分
按键
原理图
开始配置
主要代码
void HAL_GPIO_EXTI_Callback ( uint16_t GPIO_Pin ){if ( GPIO_Pin == KEY1_Pin ){ // 判断一下那个引脚触发中断//这里编写触发中断后要执行的程序HAL_GPIO_TogglePin ( LED_GPIO_Port , LED_Pin ); // 切换 LED GPIO 状态}if ( GPIO_Pin == KEY2_Pin ){ // 判断一下那个引脚触发中断//这里编写触发中断后要执行的程序HAL_GPIO_TogglePin ( LED_GPIO_Port , LED_Pin ); // 切换 LED GPIO 状态}}
PWM控制电机
认识PWM
原理图
PWM配置
主要代码
HAL_TIM_PWM_Start ( & htim1 , TIM_CHANNEL_1 ); // 开启定时器 1 通道 1 PWM 输出HAL_TIM_PWM_Start ( & htim1 , TIM_CHANNEL_4 ); // 开启定时器 1 通道 4 PWM 输出
我们可以使用这个宏来修改占空比
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 40);
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_4, 40);
电机驱动和PWM
认识电机驱动
项目使用电机驱动芯片为A4950、下面是电机驱动的相关介绍
使用电机驱动(独立工程)
主要代码
电机正反转测试
HAL_GPIO_WritePin ( AIN1_GPIO_Port , AIN1_Pin , GPIO_PIN_RESET ); // 设置 AIN1 PB13 为 低电平HAL_GPIO_WritePin ( BIN1_GPIO_Port , BIN1_Pin , GPIO_PIN_SET ); // 设置 BIN1 PB3 为高电平HAL_Delay ( 1000 );// 两次会使得电机反向。HAL_GPIO_WritePin ( AIN1_GPIO_Port , AIN1_Pin , GPIO_PIN_SET ); // 设置 AIN1 PB13 为 高电平HAL_GPIO_WritePin ( BIN1_GPIO_Port , BIN1_Pin , GPIO_PIN_RESET ); // 设置 BIN1 PB3 为低电平
/******************** @brief 设置两个电机转速和方向* @param motor1: 电机 B 设置参数、 motor2: 设置参数* @param motor1: 输入 1~100 对应控制 B 电机正方向速度在 1%-100% 、输入 -1~-100 对应控制 B 电机反方向速度在 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 ;//motor1 设置电机 B 的转速if ( motor1 < 0 ){if ( motor1 < - 99 ) motor1 = - 99 ; // 超过 PWM 幅值//负的时候绝对值越小 PWM 占空比越大//现在的motor1 -1 -99//给寄存器或者函数 99 1__HAL_TIM_SET_COMPARE ( & htim1 , TIM_CHANNEL_1 , ( 100 + motor1 )); // 修改定时器 1通道1 PA8 Pulse改变占空比}else{if ( motor1 > 99 ) motor1 = 99 ;//现在是 0 1 99//我们赋值 0 1 99__HAL_TIM_SET_COMPARE ( & htim1 , TIM_CHANNEL_1 , motor1 ); // 修改定时器 1 通道 1 PA8 Pulse改变占空比}//motor2 设置电机 A 的转速if ( motor2 < 0 ){if ( motor2 < - 99 ) motor2 = - 99 ; // 超过 PWM 幅值//负的时候绝对值越小 PWM 占空比越大//现在的motor2 -1 -99//给寄存器或者函数 99 1__HAL_TIM_SET_COMPARE ( & htim1 , TIM_CHANNEL_4 , ( 100 + motor2 )); // 修改定时器 1 通道4 PA11 Pulse 改变占空比}else{if ( motor2 > 99 ) motor2 = 99 ;//现在是 0 1 99//我们赋值 0 1 99__HAL_TIM_SET_COMPARE ( & htim1 , TIM_CHANNEL_4 , motor2 ); // 修改定时器 1 通道 4 PA11 Pulse改变占空比}}
HAL_Delay ( 500 );Motor_Set ( 0 , 0 );
编码器测速
认识编码器
单片机定时器的编码器功能
获得单位时间计数器值变化量
这里很巧妙,因为如果编码器的方向如果是反的,那么16位数的CNT计数器就会变成63335,之后不断的减1,然后变成short类型的数据之后,那么就会变成-1,我们就只要转换一下数据类型,就可以知道编码器的方向。(但是切记,不能溢出,但是也不需要考虑,电机不可能转的能这么快,超过32768)
同理设置TIM4。
生成代码
HAL_TIM_Encoder_Start ( & htim2 , TIM_CHANNEL_ALL ); // 开启定时器 2HAL_TIM_Encoder_Start ( & htim4 , TIM_CHANNEL_ALL ); // 开启定时器 4HAL_TIM_Base_Start_IT ( & htim2 ); // 开启定时器 2 中断HAL_TIM_Base_Start_IT ( & htim4 ); // 开启定时器 4 中断
short Encoder1Count = 0 ; // 编码器计数器值short Encoder2Count = 0 ;
Motor_Set ( 0 , 0 );//1. 保存计数器值Encoder1Count = ( short ) __HAL_TIM_GET_COUNTER ( & htim4 );Encoder2Count = ( short ) __HAL_TIM_GET_COUNTER ( & htim2 );//2. 清零计数器值__HAL_TIM_SET_COUNTER ( & htim4 , 0 );__HAL_TIM_SET_COUNTER ( & htim2 , 0 );printf ( "Encoder1Count:%d\r\n" , Encoder1Count );printf ( "Encoder2Count:%d\r\n" , Encoder2Count );HAL_Delay ( 2 );
//定义两个 float 变量float Motor1Speed = 0.00 ;float Motor2Speed = 0.00 ;// 下面是代码 ( 一定要把主函数没有用的删除掉 )// 计算速度Motor1Speed = ( float ) Encode1Count * 100 / 9.6 / 11 / 4 ;Motor2Speed = ( float ) Encode2Count * 100 / 9.6 / 11 / 4 ;printf ( "Motor1Speed:%.2f\r\n" , Motor1Speed );printf ( "Motor2Speed:%.2f\r\n" , Motor2Speed );
定时器中断定时测量速度
开启定义更新中断
代码开启定时器1 中断
HAL_TIM_Base_Start_IT(&htim1); //开启定时器1 中断
定时器回调函数中添加 速度计算内容
/******************** @brief 定时器回调函数* @param* @return********************/void HAL_TIM_PeriodElapsedCallback ( TIM_HandleTypeDef * htim ){if ( htim == & htim1 ) //htim1 500HZ 2ms 中断一次{TimerCount ++ ;if ( TimerCount % 5 == 0 ) // 每 10ms 执行一次{Encode1Count = ( short ) __HAL_TIM_GET_COUNTER ( & htim4 );Encode2Count = ( short ) __HAL_TIM_GET_COUNTER ( & htim2 );__HAL_TIM_SET_COUNTER ( & htim4 , 0 );__HAL_TIM_SET_COUNTER ( & htim2 , 0 );Motor1Speed = ( float ) Encode1Count * 100 / 9.6 / 11 / 4 ;Motor2Speed = ( float ) Encode2Count * 100 / 9.6 / 11 / 4 ;TimerCount = 0 ;}}}
主函数就输出速度大小就可以了
printf ( "Motor1Speed:%.2f\r\n" , Motor1Speed );printf ( "Motor2Speed:%.2f\r\n" , Motor2Speed );
然后打开串口助手
PID-速度控制
准备工作-匿名上位机曲线显示速度波形方便观察数据
// 需要发送 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))
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);//这里是串口发送函数
}
添加测试代码
// 电机速度等信息发送到上位机// 注意上位机不支持浮点数,所以要乘 100ANO_DT_Send_F2 ( Motor1Speed * 100 , 3.0 * 100 , Motor2Speed * 100 , 3.0 * 100 );
P I D 逐个参数理解
一句话就是,我们会根据我们需求,然后结合现在的状态,不断调试出合适的PID参数,看看哪些参数最适合我们的要求。
开始写PID
#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 ;// 声明函数float P_realize ( tPid * pid , float actual_val );void PID_init ( void );float PI_realize ( tPid * pid , float actual_val );float PID_realize ( tPid * pid , float actual_val );#endif
//给结构体类型变量赋初值
void PID_init()
{
pid1_speed.actual_val=0.0;
pid1_speed.target_val=0.00;
pid1_speed.err=0.0;
pid1_speed.err_last=0.0;
pid1_speed.err_sum=0.0;
pid1_speed.Kp=15;
pid1_speed.Ki=5;
pid1_speed.Kd=0;
pid2_speed.actual_val=0.0;
pid2_speed.target_val=0.00;
pid2_speed.err=0.0;
pid2_speed.err_last=0.0;
pid2_speed.err_sum=0.0;
pid2_speed.Kp=15;
pid2_speed.Ki=5;
pid2_speed.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;
}
①PI算法:
特点:从时域上看,只要存在偏差,积分就会不停对偏差积累,因此稳态时误差一定为零
不足:比例与积分动作都是对过去控制误差进行操作, 不对未来控制误差进行预测,限制了控制性能。
PI调节将比列调节的快速反应与积分调节消除静差的特点结合,实现好的调节效果。
PI调节适用于控制通道滞后较小、负荷变化不大、 工艺参数不允许有静差的系统。
②PD算法:
适用于舵机快速响应。对于惯性较大的对象,常常希望能加快控制速度, 此时可增加微分作用。
特点:
比例控制对于惯性较大对象,控制过程缓慢,控制品质不佳。比例微分控制可提高控制速度,对惯性较大对象,可改善控制质量,减小偏差,缩短控制时间。
理想微分作用持续时间太短, 执行器来不及响应。实际使用中,一般加以惯性延迟,称为实际微分。
PD 调节以比例调节为主,微分调节为辅,PD调节是有差调节。
PD 调节具有提高系统稳定性、抑制过渡过程最大动态偏差的作用。
PD 调节有利于提高系统响应速度。
PD 调节抗干扰能力差,一般只能应用于被调参数 变化平稳的生产过程。
微分作用太强时,容易造成系统振荡。
③PID算法:
将比例、积分、微分三种调节规律结合在一起, 只要三项作用的强度配合适当,既能快速调节,又能消除余差,可得到满意的控制效果。特点:
PID 控制作用中,比例作用是基础控制;微分作用是 用于加快系统控制速度;积分作用是用于消除静差。
只要比例、积分、微分三种控制规律强度配合适当, 既能快速调节,又能消除余差,可得到满意控制效果。
Kp 较小时,系统对微分和积分环节的引入较为敏感,积分会引起超调,微分可能会引起振荡,而振荡剧烈的时候超调也会增加。
Kp 增大时,积分环节由于滞后产生的超调逐渐减小,此时如果想要继续减少超调可以适当引入微分环节。继续增大 Kp 系统可能会不太稳定,因此在增加 Kp 的同时引入 Kd 减小超调,可以保证在 Kp 不是很大的情况下也能取得较好的稳态特性和动态性能。
Kp 较小时,积分环节不宜过大,Kp 较大时积分环节也不宜过小(否则调节时间会非常地长),当使用分段PID ,在恰当的条件下分离积分,可以取得更好的控制效果。原因在于在稳态误差即将满足要求时,消除了系统的滞后。因此系统超调会明显减少。
PID_init ();
// 比例 p 调节控制函数float P_realize ( tPid * pid , float actual_val ){pid -> actual_val = actual_val ; // 传递真实值pid -> err = pid -> target_val - pid -> actual_val ; // 目标值减去实际值等于误差值//比例控制调节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 控制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 控制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控制,就是要根据理论,每个参数具体起什么作用,然后结合自己实际情况,不断调试,直到选出最好的参数,这就是我们所谓的PID调控。
加入cJSON方便上位机调参
__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 );}
extern uint8_t Usart1_ReadBuf [ 255 ]; // 串口 1 缓冲数组extern uint8_t Usart1_ReadCount ; // 串口 1 接收字节计数// 判断否接收完一帧数据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 ; // 表示正在接受中}
#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控制中,P、I、D各个参数分别代表比例(Proportional)、积分(Integral)、微分(Derivative),它们分别起到以下作用:
1.比例控制(P):
2.比例项的作用是根据当前偏差的大小来产生一个控制输出。P参数用于调节系统的稳定性和响应速度。增大P参数可以减小超调量,但可能会加大稳定时间。
3.P参数的作用是使系统对误差的响应更为迅速,但会增加系统对误差的抖动和震荡。
4.积分控制(I):
5.积分项的作用是减小稳态误差,通过对积分累积的偏差进行补偿来消除静差。I参数用于调节系统的静态稳定性,确保系统最终稳定在目标值附近。
6.I参数的作用是消除系统的稳态误差,对于存在恒定偏差的系统,采用I参数可以更好地调节系统。
7.微分控制(D):
8.微分项的作用是根据误差变化率来预测未来的系统状态,通过预测系统的趋势来提前调整控制输出。D参数用于抑制超调和稳定系统的过程。
9.D参数的作用是稳定系统的过渡过程,减小超调量,减缓系统的振荡速度,提高系统的稳定性。综合来看,P、I、D三个参数在PID控制中协同工作,通过不同比例的调节,可以使系统达到稳定、响应迅速且准确的控制效果。在实际应用中,根据系统的特性和要求,需要合理调节这三个参数以实现最佳的控制效果。
tPid pid1_speed ; // 电机 1 的转速控制tPid pid2_speed ; // 电机 2 的转速控制// 初始化 PID 参数void PID_init (){pid1_speed . actual_val = 0.0 ; // 初始化电机 1 转速 PID 结构体pid1_speed . target_val = 0.0 ;pid1_speed . err = 0.0 ;pid1_speed . err_last = 0.0 ;pid1_speed . err_sum = 0.0 ;pid1_speed . Kp = 0.0 ;pid1_speed . Ki = 0.0 ;pid1_speed . Kd = 0.0 ;pid2_speed . actual_val = 0.0 ; // 初始化电机 2 转速 PID 结构体pid2_speed . target_val = 0.0 ;pid2_speed . err = 0.0 ;pid2_speed . err_last = 0.0 ;pid2_speed . err_sum = 0.0 ;pid2_speed . Kp = 0.0 ;pid2_speed . Ki = 0.0 ;pid2_speed . Kd = 0.0 ;}
if(Timer2Count %10 ==0)//每20ms一次
{ Motor_Set(PID_realize(&pid1_speed,Motor1Speed),PID_realize(&pid2_speed,Motor2Speed));
Timer2Count=0;
}
以上是入门篇
小车跑一跑
如何实现小车的前、后、左、右、停
/******************** @brief 通过 PID 控制电机转速* @param Motor1Speed: 电机 1 目标速度、 Motor2Speed: 电机 2 目标速度* @return 无********************/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 ));}
// motorPidSetSpeed(1,2);// 向右转弯// motorPidSetSpeed(2,1);// 向左转弯// motorPidSetSpeed(1,1);// 前进// motorPidSetSpeed(-1,-1);// 后退// motorPidSetSpeed(0,0);// 停止
加速减速函数
// 向前加速函数void motorSpeedUp ( void ){static float MotorSetSpeedUp = 0.5 ; // 静态变量 函数结束 变量不会销毁if ( MotorSetSpeedUp <= MAX_SPEED_UP ) MotorSetSpeedUp += 0.5 ; // 如果没有超过最大值就增加0.5motorPidSetSpeed ( MotorSetSpeedUp , MotorSetSpeedUp ); // 设置到电机}// 向前减速函数void motorSpeedCut ( void ){static float MotorSetSpeedCut = 3 ; // 静态变量 函数结束 变量不会销毁if ( MotorSetSpeedCut >= 0.5 ) MotorSetSpeedCut -= 0.5 ; // 判断是否速度太小motorPidSetSpeed ( MotorSetSpeedCut , MotorSetSpeedCut ); // 设置到电机}
OLED速度与历程显示
/* 里程数 (cm) += 时间周期( s ) * 车轮转速 ( 转 /s)* 车轮周长 (cm)*/Mileage += 0.02 * Motor1Speed * 22 ;
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采集电压和显示
/******************** @brief 电池电压测量计算函数* @param 无* @return 小车电池电压********************/float adcGetBatteryVoltage ( void ){HAL_ADC_Start ( & hadc2 ); // 启动 ADC 转化if ( HAL_OK == HAL_ADC_PollForConversion ( & hadc2 , 50 )) // 等待转化完成、超时时间 50msreturn ( float ) HAL_ADC_GetValue ( & hadc2 ) / 4096 * 3.3 * 5 ; // 计算电池电压return - 1 ;}
sprintf (( char* ) OledString , "U:%.2fV" , adcGetBatteryVoltage ());OLED_ShowString ( 0 , 2 , OledString , 12 ); // 这个是 oled 驱动里面的,是显示位置的一个函数,
循迹功能
非PID循迹功能完成
#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)
if ( READ_HW_OUT_1 == 0 && READ_HW_OUT_2 == 0 && READ_HW_OUT_3 == 0 && READ_HW_OUT_4 == 0 ){printf ( " 应该前进 \r\n" );motorPidSetSpeed ( 1 , 1 ); // 前运动}if ( READ_HW_OUT_1 == 0 && READ_HW_OUT_2 == 1 && READ_HW_OUT_3 == 0 && READ_HW_OUT_4 == 0 ){printf ( " 应该右转 \r\n" );motorPidSetSpeed ( 0.5 , 2 ); // 右边运动}if ( READ_HW_OUT_1 == 1 && READ_HW_OUT_2 == 0 && READ_HW_OUT_3 == 0 && READ_HW_OUT_4 == 0 ){printf ( " 快速右转 \r\n" );motorPidSetSpeed ( 0.5 , 2.5 ); // 快速右转}if ( READ_HW_OUT_1 == 0 && READ_HW_OUT_2 == 0 && READ_HW_OUT_3 == 1 && READ_HW_OUT_4 == 0 ){printf ( " 应该左转 \r\n" );motorPidSetSpeed ( 2 , 0.5 ); // 左边运动}if ( READ_HW_OUT_1 == 0 && READ_HW_OUT_2 == 0 && READ_HW_OUT_3 == 0 && READ_HW_OUT_4 == 1 ){printf ( " 快速左转 \r\n" );motorPidSetSpeed ( 2.5 , 0.5 ); // 快速左转}
加入循迹PID
我们这里规定小车位于循迹路上有5个位置(因为我们有四个红外对管),这样子我们可以生成七个状态。(快速左转x2、快速右转x2、左转、右转、前进)
extern tPid pidHW_Tracking ; // 红外循迹的 PIDuint8_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 )//使用else if更加合理高效
{
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;//进行限幅 限幅速度在0-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;//保存上次红外对管状态
pidHW_Tracking . actual_val = 0.0 ;pidHW_Tracking . target_val = 0.00 ; // 红外循迹 PID 的目标值为 0pidHW_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 ;
手机遥控功能
电脑控制小车
uint8_t g_ucUsart3ReceiveData ; // 保存串口三接收的数据
HAL_UART_Receive_IT ( & huart3 , & g_ucUsart3ReceiveData , 1 ); // 串口三接收数据
// 串口接收回调函数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' ) motorPidSpeedUp (); // 加速if ( g_ucUsart3ReceiveData == 'G' ) motorPidSpeedCut (); // 减速HAL_UART_Receive_IT ( & huart3 , & g_ucUsart3ReceiveData , 1 ); // 继续进行中断接收}}
sprintf (( char* ) Usart3String , "V1:%.2fV2:%.2f\r\n" , Motor1Speed , Motor2Speed ); // 显示两个电机转速 单位:转 / 秒HAL_UART_Transmit ( & huart3 ,( uint8_t * ) Usart3String , strlen (( const char* ) Usart3String ), 50 ); // 阻塞式发送通过串口三输出字符 strlen: 计算字符串大小sprintf (( char * ) Usart3String , "Mileage%.2f\r\n" , Mileage ); // 计算小车里程 单位 cmHAL_UART_Transmit ( & huart3 ,( uint8_t * ) Usart3String , strlen (( const char* ) Usart3String ), 50 ); // 阻塞式发送通过串口三输出字符 strlen: 计算字符串大小sprintf (( char * ) Usart3String , "U:%.2fV\r\n" , adcGetBatteryVoltage ()); // 显示电池电压HAL_UART_Transmit ( & huart3 ,( uint8_t * ) Usart3String , strlen (( const char* ) Usart3String ), 50 ); // 阻塞式发送通过串口三输出字符 strlen: 计算字符串大小HAL_Delay ( 5 );
手机蓝牙控制小车
先调试蓝牙模块-设置波特率
如图先把蓝牙模块通过USB-TTL模块相连接,然后
蓝牙模块连接单片机
超声波避障功能
超声波测距
/******************** @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 (静止表面平整精度更高)
*
*******************/
float HC_SR04_Read(void)
{
uint32_t i = 0;
float Distance;
HAL_GPIO_WritePin(HC_SR04_Trig_GPIO_Port,HC_SR04_Trig_Pin,GPIO_PIN_SET);//输出15us高电平
HC_SR04_Delayus(15);
HAL_GPIO_WritePin(HC_SR04_Trig_GPIO_Port,HC_SR04_Trig_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>100000) 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左右
if(i >100000) return -2;//超时退出循环
}
Distance = i*2*0.033/2;//这里乘2的原因是上面是2微妙
return Distance ;
}
sprintf (( char * ) Usart3String , "HC_SR04:%.2fcm\r\n" , HC_SR04_Read ()); // 显示超声波数据HAL_UART_Transmit ( & huart3 ,( uint8_t * ) Usart3String , strlen (( const char* ) Usart3String ), 0xFFFF ); // 通过串口三输出字符 strlen: 计算字符串大小
避障逻辑编写
//************** 避障功能 ********************//// 避障逻辑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跟随功能
// 超声波跟随if ( HC_SR04_Read () > 25 ){motorForward (); // 前进HAL_Delay ( 100 );}if ( HC_SR04_Read () < 20 ){motorBackward (); // 后退HAL_Delay ( 100 );}
PID跟随功能
在pid.c中定义一组PID参数
tPid pidFollow ; // 定距离跟随 PIDpidFollow . actual_val = 0.0 ;pidFollow . target_val = 22.50 ; // 定距离跟随 目标距离 22.5cmpidFollow . err = 0.0 ;pidFollow . err_last = 0.0 ;pidFollow . err_sum = 0.0 ;pidFollow . Kp =- 0.5 ; // 定距离跟随的 Kp 大小通过估算 PID 输入输出数据,确定大概大小,然后在调试pidFollow . Ki =- 0.001 ; //Ki 小一些pidFollow . Kd = 0 ;
//**********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 ); // 读取超声波传感器不能过快
最后可以明显发现,PID跟随功能明显跟随的更加快,不容易被一下子贴近碰到以及一下子距离拉大而跟丢。
用MPU6050走直线和转弯90度
使用软件初始化两个引脚
//IO 方向设置 设置 SDA-PB9 为输入或者输出#define MPU_SDA_IN() {GPIOB->CRH&=0XFFFFFF0F;GPIOB->CRH|=8<<4;}#define MPU_SDA_OUT() {GPIOB->CRH&=0XFFFFFF0F;GPIOB->CRH|=3<<4;}
//IO 操作函数#define MPU_IIC_SCL_HigeHAL_GPIO_WritePin(SCL_6050_GPIO_Port,SCL_6050_Pin,GPIO_PIN_SET) // 设置 SCL 高电平#define MPU_IIC_SCL_LowHAL_GPIO_WritePin(SCL_6050_GPIO_Port,SCL_6050_Pin,GPIO_PIN_RESET) // 设置 SCL 低电平#define MPU_IIC_SDA_HigeHAL_GPIO_WritePin(SDA_6050_GPIO_Port,SDA_6050_Pin,GPIO_PIN_SET) // 设置 SDA 高电平#define MPU_IIC_SDA_LowHAL_GPIO_WritePin(SDA_6050_GPIO_Port,SDA_6050_Pin,GPIO_PIN_RESET) // 设置 SDA 低电平#define MPU_READ_SDA HAL_GPIO_ReadPin(SDA_6050_GPIO_Port,SDA_6050_Pin)// 读 SDA 电平
当然,大家应该对I2C协议应该很了解了,我们这里用GPIO来模拟通信,封装了写数据和读数据的函数,这里就不去详细列举了。
uint8_t MPU_Init(void); //初始化MPU6050
uint8_t MPU_Write_Len(uint8_t addr,uint8_t reg,uint8_t len,uint8_t *buf);//IIC连续写
uint8_t MPU_Read_Len(uint8_t addr,uint8_t reg,uint8_t len,uint8_t *buf); //IIC连续读
uint8_t MPU_Write_Byte(uint8_t reg,uint8_t data); //IIC写一个字节
uint8_t MPU_Read_Byte(uint8_t reg); //IIC读一个字节uint8_t MPU_Set_Gyro_Fsr(uint8_t fsr);
uint8_t MPU_Set_Accel_Fsr(uint8_t fsr);
uint8_t MPU_Set_LPF(uint16_t lpf);
uint8_t MPU_Set_Rate(uint16_t rate);
uint8_t MPU_Set_Fifo(uint8_t sens);
short MPU_Get_Temperature(void);
uint8_t MPU_Get_Gyroscope(short *gx,short *gy,short *gz);
uint8_t MPU_Get_Accelerometer(short *ax,short *ay,short *az);
float pitch , roll , yaw ; // 俯仰角 横滚角 航向角#include "mpu6050.h"#include "inv_mpu.h"#include "inv_mpu_dmp_motion_driver.h"
初始化6050
HAL_Delay ( 500 ); // 延时 0.5 秒 6050 上电稳定后初始化MPU_Init (); // 初始化 MPU6050while ( MPU_Init () != 0 );while ( mpu_dmp_init () != 0 )
sprintf (( char * ) Usart3String , "pitch:%.2f roll:%.2fyaw:%.2f\r\n" , pitch , roll , yaw ); // 显示 6050 数据HAL_UART_Transmit ( & huart3 ,( uint8_t * ) Usart3String , strlen (( const char* ) Usart3String ), 0xFFFF ); // 通过串口三输出字符 strlen: 计算字符串大小//mpu_dmp_get_data(&pitch,&roll,&yaw);// 返回值 :0,DMP 成功解出欧拉角while ( mpu_dmp_get_data ( & pitch , & roll , & yaw ) != 0 ){} // 这个可以解决经常读不出数据的问题
利用6050直线和90度(有代码)
走直线(控制朝一个方向运动)
tPid pidMPU6050YawMovement ; // 利用 6050 偏航角 进行姿态控制的 PID
pidMPU6050YawMovement . actual_val = 0.0 ;pidMPU6050YawMovement . target_val = 0.00 ; // 设定姿态目标值pidMPU6050YawMovement . err = 0.0 ;pidMPU6050YawMovement . err_last = 0.0 ;pidMPU6050YawMovement . err_sum = 0.0 ;pidMPU6050YawMovement . Kp = 2 ; // 定距离跟随的 Kp 大小通过估算 PID 输入输出数据,确定大概大小,然后在调试pidMPU6050YawMovement . Ki = 0 ;pidMPU6050YawMovement . Kd = 0 ;
float g_fMPU6050YawMovePidOut = 0.00f ; // 姿态 PID 运算输出float g_fMPU6050YawMovePidOut1 = 0.00f ; // 第一个电机控制输出float g_fMPU6050YawMovePidOut2 = 0.00f ; // 第一个电机控制输出
//*************MPU6050 航向角 PID 转向控制 *****************//sprintf (( char * ) Usart3String , "pitch:%.2f roll:%.2fyaw:%.2f\r\n" , pitch , roll , yaw ); // 显示 6050 数据 俯仰角 横滚角 航向角HAL_UART_Transmit ( & huart3 ,( uint8_t * ) Usart3String , strlen (( const char* ) Usart3String ), 0xFFFF ); // 通过串口三输出字符 strlen: 计算字符串大小//mpu_dmp_get_data(&pitch,&roll,&yaw);// 返回值 :0,DMP 成功解出欧拉角while ( mpu_dmp_get_data ( & pitch , & roll , & yaw ) != 0 ){} // 这个可以解决经常读不出数据的问题g_fMPU6050YawMovePidOut = PID_realize ( & pidMPU6050YawMovement , yaw ); //PID 计算输出目标速度 这个速度,会和基础速度加减g_fMPU6050YawMovePidOut1 = 1.5 + g_fMPU6050YawMovePidOut ; // 基础速度加减 PID 输出速度g_fMPU6050YawMovePidOut2 = 1.5 - g_fMPU6050YawMovePidOut ;if ( g_fMPU6050YawMovePidOut1 > 3.5 ) g_fMPU6050YawMovePidOut1 = 3.5 ; // 进行限幅if ( g_fMPU6050YawMovePidOut1 < 0 ) g_fMPU6050YawMovePidOut1 = 0 ;if ( g_fMPU6050YawMovePidOut2 > 3.5 ) g_fMPU6050YawMovePidOut2 = 3.5 ;if ( g_fMPU6050YawMovePidOut2 < 0 ) g_fMPU6050YawMovePidOut2 = 0 ;motorPidSetSpeed ( g_fMPU6050YawMovePidOut1 , g_fMPU6050YawMovePidOut2 );
pidMPU6050YawMovement . Kp = 0.02 ; //6050 航向角 PID 运动控制pidMPU6050YawMovement . Ki = 0 ;pidMPU6050YawMovement . Kd = 0.1 ;
转弯90度功能(控制转弯角度)
extern tPid pidMPU6050YawMovement ; // 利用 6050 偏航角 进行姿态控制的 PID
if ( g_ucUsart3ReceiveData == 'H' ) // 转向 90 度{if ( pidMPU6050YawMovement . target_val <= 180 )pidMPU6050YawMovement . target_val += 90 ; // 目标值}if ( g_ucUsart3ReceiveData == 'I' ) // 转回 90 度{if ( pidMPU6050YawMovement . target_val >= - 180 )pidMPU6050YawMovement . target_val -= 90 ; // 目标值}
按键和APP按钮切换功能
uint8_t g_ucMode = 0 ;// 小车运动模式标志位 0: 显示功能、 1:PID 循迹模式、 2: 手机遥控普通运动模式、 3. 超声波避障模式、4:PID 跟随模式、 5: 遥控角度闭环
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
MX_TIM1_Init();
MX_TIM2_Init();
MX_TIM4_Init();
MX_ADC2_Init();
OLED_Init(); //初始化OLED
OLED_Clear();
Motor_init();/*pwm波生成*/
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 中断,计时2ms
__HAL_UART_ENABLE_IT(&huart1,UART_IT_RXNE);//开启串口接收中断
HAL_UART_Receive_IT(&huart3,&g_ucUsart3ReceiveData,1); //串口三接收数据
/*初始化mpu6050*/
HAL_Delay(500);//延时0.5秒 6050上电稳定后初始化
MPU_Init(); //初始化MPU6050
while(MPU_Init()!=0);
while(mpu_dmp_init()!=0);
/*pid初始化*/
PID_init();
while (1)
{
sprintf((char *)OledString," g_ucMode:%d",g_ucMode);//显示g_ucMode 当前模式
OLED_ShowString(0,6,OledString,12); //显示在OLED上
sprintf((char *)Usart3String," g_ucMode:%d",g_ucMode);//蓝牙APP显示
HAL_UART_Transmit(&huart3,( uint8_t *)Usart3String,strlen(( const char *)Usart3String),50);//阻塞式发送通过串口三输出字符 strlen:计算字符串大小
if(g_ucMode == 0)//0LED显示功能
{
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驱动里面的,是显示位置的一个函数,
sprintf((char*)OledString, "U:%.2fV", adcGetBatteryVoltage());//显示电池电压
OLED_ShowString(0,2,OledString,12);//这个是oled驱动里面的,是显示位置的一个函数,
sprintf((char *)OledString,"HC_SR04:%.2fcm\r\n",HC_SR04_Read());//显示超声波数据
OLED_ShowString(0,3,OledString,12);//这个是oled驱动里面的,是显示位置的一个函数,
sprintf((char *)OledString,"p:%.2f r:%.2f \r\n",pitch,roll);//显示6050数据 俯仰角 横滚角
OLED_ShowString(0,4,OledString,12);//这个是oled驱动里面的,是显示位置的一个函数,
sprintf((char *)OledString,"y:%.2f \r\n",yaw);//显示6050数据 航向角
OLED_ShowString(0,5,OledString,12);//这个是oled驱动里面的,是显示位置的一个函数,
//蓝牙APP显示
sprintf((char*)Usart3String, "V1:%.2fV2:%.2f", Motor1Speed,Motor2Speed);//显示速度
HAL_UART_Transmit(&huart3,( uint8_t *)Usart3String,strlen(( const char *)Usart3String),50);//阻塞式发送通过串口三输出字符 strlen:计算字符串大小
//阻塞方式发送可以保证数据发送完毕,中断发送不一定可以保证数据已经发送完毕才启动下一次发送
sprintf((char*)Usart3String, "Mileage:%.2f", Mileage);//显示里程
HAL_UART_Transmit(&huart3,( uint8_t *)Usart3String,strlen(( const char *)Usart3String),50);//阻塞式发送通过串口三输出字符 strlen:计算字符串大小
sprintf((char*)Usart3String, "U:%.2fV", adcGetBatteryVoltage());//显示电池电压
HAL_UART_Transmit(&huart3,( uint8_t *)Usart3String,strlen(( const char *)Usart3String),50);//阻塞式发送通过串口三输出字符 strlen:计算字符串大小
sprintf((char *)Usart3String,"HC_SR04:%.2fcm\r\n",HC_SR04_Read());//显示超声波数据
HAL_UART_Transmit(&huart3,( uint8_t *)Usart3String,strlen(( const char *)Usart3String),50);//阻塞式发送通过串口三输出字符 strlen:计算字符串大小
sprintf((char *)Usart3String,"p:%.2f r:%.2f \r\n",pitch,roll);//显示6050数据 俯仰角 横滚角
HAL_UART_Transmit(&huart3,( uint8_t *)Usart3String,strlen(( const char *)Usart3String),50);//阻塞式发送通过串口三输出字符 strlen:计算字符串大小
sprintf((char *)Usart3String,"y:%.2f \r\n",yaw);//显示6050数据 航向角
HAL_UART_Transmit(&huart3,( uint8_t *)Usart3String,strlen(( const char *)Usart3String),50);//阻塞式发送通过串口三输出字符 strlen:计算字符串大小
//获得6050数据
while(mpu_dmp_get_data(&pitch,&roll,&yaw)!=0){} //这个可以解决经常读不出数据的问题
//在显示模式电机停转 设置小车速度为0
motorPidSetSpeed(0,0);
}
if(g_ucMode == 1)///**** 红外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 )//使用else if更加合理高效
{
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;//进行限幅 限幅速度在0-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;//保存上次红外对管状态
}
if(g_ucMode == 2)//***************遥控模式***********************//
{
//遥控模式的控制在串口三的中断里面
}
if(g_ucMode == 3) //******超声波避障模式*********************//
{
//避障逻辑
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);
}
}
}
}
if(g_ucMode == 4) //**********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);//读取超声波传感器不能过快
}
if(g_ucMode == 5)//*************MPU6050航向角 PID转向控制*****************//
{sprintf((char *)Usart3String,"pitch:%.2f roll:%.2f yaw:%.2f\r\n",pitch,roll,yaw);//显示6050数据 俯仰角 横滚角 航向角
HAL_UART_Transmit(&huart3,( uint8_t *)Usart3String,strlen(( const char *)Usart3String),0xFFFF);//通过串口三输出字符 strlen:计算字符串大小
//mpu_dmp_get_data(&pitch,&roll,&yaw);//返回值:0,DMP成功解出欧拉角
while(mpu_dmp_get_data(&pitch,&roll,&yaw)!=0){} //这个可以解决经常读不出数据的问题
g_fMPU6050YawMovePidOut = PID_realize(&pidMPU6050YawMovement,yaw);//PID计算输出目标速度 这个速度,会和基础速度加减g_fMPU6050YawMovePidOut1 = 1.5 + g_fMPU6050YawMovePidOut;//基础速度加减PID输出速度
g_fMPU6050YawMovePidOut2 = 1.5 - g_fMPU6050YawMovePidOut;
if(g_fMPU6050YawMovePidOut1 >3.5) g_fMPU6050YawMovePidOut1 =3.5;//进行限幅
if(g_fMPU6050YawMovePidOut1 <0) g_fMPU6050YawMovePidOut1 =0;
if(g_fMPU6050YawMovePidOut2 >3.5) g_fMPU6050YawMovePidOut2 =3.5;//进行限幅
if(g_fMPU6050YawMovePidOut2 <0) g_fMPU6050YawMovePidOut2 =0;
motorPidSetSpeed(g_fMPU6050YawMovePidOut1,g_fMPU6050YawMovePidOut2);//将最后计算的目标速度 通过motorPidSetSpeed控制电机
}/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
我们添加一个通过蓝牙APP按钮切换模式代码
if ( g_ucUsart3ReceiveData == 'J' ) // 改变模式{if ( g_ucMode == 5 ) g_ucMode = 1 ; //g_ucMode 模式是 0 1 2 3 4 5else{g_ucMode += 1 ;}}if ( g_ucUsart3ReceiveData == 'K' ) g_ucMode = 0 ; // 设置为显示模式
然后对应APP也要添加 按钮设置
总结:以上就是整个小车的所有功能了,(按键、PWM驱动电机、编码器测速、PID速度控制、OLED显示、循迹、手机遥控、超声波避障和跟随、MPU6050指定轨迹)我从原理到代码全部都和大家讲解了一遍,只要大家按照步骤一步步来,我相信只要你有心,那么复刻这个小车简直易如反掌。