一、引言
关于STM32两轮平衡车的设计,我想在读者阅读本文之前应该已经有所了解,所以本文的重点是代码的分享和分析。至于具体的原理,我觉得读者不必阅读长篇大论的文章,只需按照本文分享的代码自己亲手制作一辆平衡车,其原理并不言而喻了。源完整代码工程在文章末尾百度网盘链接,请需要的读者自行下载即可。
另外,由于平衡车的精髓在于PID算法的运用,有需要了解PID算法的读者可以参考以下两篇文章:
PID算法详解(代码详解篇),位置式PID、增量式PID(通用)_pid 代码-CSDN博客
PID算法详解(精华知识汇总)_小小_扫地僧的博客-CSDN博客
二、所需材料
1、STM32F03C8T6
2、MPU6050
3、蓝牙模块
4、编码电机
5、TB6612
6、电源+稳压模块
7、OLED显示模块
三、接线强调
1、TB6612接线
2、蓝牙模块与单片机之间
单片机 蓝牙模块
TX ——> RX
RX ——> TX
3、MPU6050
使用IIC通信,所以对照代码接SDA、SCL、GND、VCC、IN(中断触发线)
四、功能介绍
1、两轮平衡直立
2、蓝牙APP控制运动状态
3、遥控手柄控制
4、超声波避障
五、关键算法
PID算法对编码电机的控制
1.位置闭环控制
位置闭环控制就是根据编码器的脉冲累加测量电机的位置信息,并与目标值进行比较,得到控制偏差,然后通过对偏差的比例、积分、微分进行控制,使偏差趋向于零的过程。 位置闭环控制就是根据编码器的脉冲累加测量电机的位置信息,并与目标值进行比较,得到控制偏差,然后通过对偏差的比例、积分、微分进行控制,使偏差趋向于零的过程.
1.1理论分析
1.2控制原理图
1.3C语言实现
int Position_PID (int Encoder, int Target) { static float Bias, Pwm,Integral_bias,Last_Bias; Bias=Encoder-Target;//计算偏差 Integral_bias+=Bias; //求出偏差的积分 Pwm=Position_KP*Bias+Position_KI*Integral_bias+Position_KD*(Bias-Last_Bias);Last_Bias=Bias; //保存上一次偏差 return Pwm; //输出 }
入口参数为编码器的位置测量值和位置控制的目标值,返回值为电机控制PWM(现在再看一下上面的控制原理图是不是更加容易明白了)。
第一行是相关内部变量的定义。
第二行是求出速度偏差,由测量值减去目标值。第三行通过累加求出偏差的积分。
第四行使用位置式PID控制器求出电机 PWM。第五行保存上一次偏差,便于下次调用。最后一行是返回。
然后,在定时中断服务函数里面调用该函数实现我们的控制目标:Moto=Position_PID(Encoder, Target_Position);
Set_Pwm(Moto) ;//===赋值给PWM寄存器
2、速度闭环控制
速度闭环控制就是根据单位时间获取的脉冲数(这里使用了M法测速)测量电机的速度信息,并与目标值进行比较,得到控制偏差,然后通过对偏差的比例、积分、微分进行控制,使偏差趋向于零的过程。
一些PID的要点在位置控制中已经有讲解,这里不再赘叙。
需要说明的是,这里速度控制20ms一次,一般建议10ms或者5ms,因为在这里电机是使用USB供电,速度比较慢,20ms可以延长获取速度的单位时间,提高编码器的采值。
2.1理论分析
根据增量式离散PID公式 根据增量式离散PID公式
Pwm+=Kp[e(k)-e(k-1)]+Ki*e(k)+Kd[e(k)-2e(k-1)+e(k-2)]
e(k):本次偏差
e(k-1):上一次的偏差e (k-2):上上次的偏差
Pwm 代表增量输出
在我们的速度控制闭环系统里面只使用PI控制,因此对PID控制器可简化为以下公式:
Pwm+=Kp[e(k)-e(k-1)]+Ki*e(k)
2.2 控制原理图
2.3 C语言实现
增量式PI控制器具体通过C语言实现的代码如下:
int Incremental_PI (int Encoder,int Target) { static float Bias, Pwm, Last_bias; Bias=Encoder-Target;//计算偏差 Pwm+=Velocity_KP*(Bias-Last_bias)+Velocity_KI*Bias;//增量式PI控制器 Last_bias=Bias;//保存上一次偏差 return Pwm;//增量输出 }
入口参数为编码器的速度测量值和速度控制的目标值,返回值为电机控制PWM。
第一行是相关内部变量的定义。
第二行是求出速度偏差,由测量值减去目标值。第三行使用增量PI控制器求出电机PWM。
第四行保存上一次偏差,便于下次调用。最后一行是返回。
然后,在定时中断服务函数里面调用该函数实现我们的控制目标:
Moto=Incremental_PI(Encoder, Target_Velocity);Set_Pwm(Moto);//===赋值给对应MCU的PWM寄存器
六、关键代码分析
1、编码电机PID算法控制
#include "control.h" #include "usart2.h" /************************************************************************** 函数功能:所有的控制代码都在这里面 5ms定时中断由MPU6050的INT引脚触发 严格保证采样和数据处理的时间同步 在MPU6050的采样频率设置中,设置成100HZ,即可保证6050的数据是10ms更新一次。 读者可在imv_mpu.h文件第26行的宏定义进行修改(#define DEFAULT_MPU_HZ (100)) **************************************************************************/ #define SPEED_Y 100 //俯仰(前后)最大设定速度 #define SPEED_Z 80//偏航(左右)最大设定速度 int Balance_Pwm,Velocity_Pwm,Turn_Pwm,Turn_Kp; float Mechanical_angle=8; float Target_Speed=0; //期望速度(俯仰)。用于控制小车前进后退及其速度。 float Turn_Speed=0; //期望速度(偏航) //针对不同车型参数,在sys.h内设置define的电机类型 float balance_UP_KP=BLC_KP; // 小车直立环PD参数 float balance_UP_KD=BLC_KD; float velocity_KP=SPD_KP; // 小车速度环PI参数 float velocity_KI=SPD_KI; float Turn_Kd=TURN_KD;//转向环KP、KD float Turn_KP=TURN_KP; void EXTI9_5_IRQHandler(void) { static u8 Voltage_Counter=0; if(PBin(5)==0) { EXTI->PR=1SPEED_Y?SPEED_Y:(Target_SpeedSPEED_Z?SPEED_Z:(Turn_Speed10000) Encoder_Integral=10000; //===积分限幅 if(Encoder_Integral> 1; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性:TIM输出比较极性高 TIM_OC1Init(TIM1, &TIM_OCInitStructure); //根据TIM_OCInitStruct中指定的参数初始化外设TIMx TIM_OC4Init(TIM1, &TIM_OCInitStructure); //根据TIM_OCInitStruct中指定的参数初始化外设TIMx TIM_CtrlPWMOutputs(TIM1,ENABLE); //MOE 主输出使能 TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); //CH1预装载使能 TIM_OC4PreloadConfig(TIM1, TIM_OCPreload_Enable); //CH4预装载使能 TIM_ARRPreloadConfig(TIM1, ENABLE); //使能TIMx在ARR上的预装载寄存器 TIM_Cmd(TIM1, ENABLE); //使能TIM1 }
4、蓝牙控制
#include "usart2.h" /************************************************************************** 函数功能:串口2初始化 入口参数: bound:波特率 返回 值:无 **************************************************************************/ void uart2_init(u32 bound) { //GPIO端口设置 GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能UGPIOB时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); //使能USART2时钟 //USART2_TX GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; //PA2 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出 GPIO_Init(GPIOA, &GPIO_InitStructure); //USART2_RX GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;//PA3 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入 GPIO_Init(GPIOA, &GPIO_InitStructure); //USART 初始化设置 USART_InitStructure.USART_BaudRate = bound;//串口波特率 USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式 USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位 USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位 USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制 USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式 USART_Init(USART2, &USART_InitStructure); //初始化串口2 USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);//开启串口接受中断 USART_Cmd(USART2, ENABLE); //使能串口2 } /************************************************************************** 函数功能:串口2接收中断 入口参数:无 返回 值:无 **************************************************************************/ u8 Fore,Back,Left,Right; void USART2_IRQHandler(void) { int Uart_Receive; if(USART_GetITStatus(USART2,USART_IT_RXNE)!=RESET)//接收中断标志位拉高 { Uart_Receive=USART_ReceiveData(USART2);//保存接收的数据 BluetoothCMD(Uart_Receive); } } void BluetoothCMD(int Uart_Receive) { switch(Uart_Receive) { case 90://停止 Fore=0,Back=0,Left=0,Right=0; break; case 65://前进 Fore=1,Back=0,Left=0,Right=0; break; case 72://左前 Fore=1,Back=0,Left=1,Right=0; break; case 66://右前 Fore=1,Back=0,Left=0,Right=1; break; case 71://左转 Fore=0,Back=0,Left=1,Right=0; break; case 67://右转 Fore=0,Back=0,Left=0,Right=1; break; case 69://后退 Fore=0,Back=1,Left=0,Right=0; break; case 70://左后,向右旋 Fore=0,Back=1,Left=0,Right=1; break; case 68://右后,向左旋 Fore=0,Back=1,Left=1,Right=0; break; default://停止 Fore=0,Back=0,Left=0,Right=0; break; } } void Uart2SendByte(char byte) //串口发送一个字节 { USART_SendData(USART2, byte); //通过库函数 发送数据 while( USART_GetFlagStatus(USART2,USART_FLAG_TC)!= SET); //等待发送完成。 检测 USART_FLAG_TC 是否置1; //见库函数 P359 介绍 } void Uart2SendBuf(char *buf, u16 len) { u16 i; for(i=0; i