STM32两轮平衡小车原理详解(开源)
一、引言
关于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=1<<5; //清除LINE5上的中断标志位 mpu_dmp_get_data(&pitch,&roll,&yaw); //得到欧拉角(姿态角)的数据MPU_Get_Gyroscope(&gyrox,&gyroy,&gyroz); //得到陀螺仪数据Encoder_Left=Read_Encoder(2); //读取编码器的值,保证输出极性一致Encoder_Right=-Read_Encoder(3); //读取编码器的值Led_Flash(100);Voltage_Counter++;if(Voltage_Counter==20) //100ms读取一次电压{Voltage_Counter=0;Voltage=Get_battery_volt(); //读取电池电压}if(KEY_Press(100)) //长按按键切换模式并触发模式切换初始化{if(++CTRL_MODE>=101) CTRL_MODE=97;Mode_Change=1;}Get_RC();Target_Speed=Target_Speed>SPEED_Y?SPEED_Y:(Target_Speed<-SPEED_Y?(-SPEED_Y):Target_Speed);//限幅Turn_Speed=Turn_Speed>SPEED_Z?SPEED_Z:(Turn_Speed<-SPEED_Z?(-SPEED_Z):Turn_Speed);//限幅( (20*100) * 100)Balance_Pwm =balance_UP(pitch,Mechanical_angle,gyroy); //===直立环PID控制 Velocity_Pwm=velocity(Encoder_Left,Encoder_Right,Target_Speed); //===速度环PID控制 Turn_Pwm =Turn_UP(gyroz,Turn_Speed); //===转向环PID控制Moto1=Balance_Pwm-Velocity_Pwm+Turn_Pwm; //===计算左轮电机最终PWMMoto2=Balance_Pwm-Velocity_Pwm-Turn_Pwm; //===计算右轮电机最终PWMXianfu_Pwm(); //===PWM限幅Turn_Off(pitch,12); //===检查角度以及电压是否正常Set_Pwm(Moto1,Moto2); //===赋值给PWM寄存器 }
}/**************************************************************************
函数功能:直立PD控制
入口参数:角度、机械平衡角度(机械中值)、角速度
返回 值:直立控制PWM
**************************************************************************/
int balance_UP(float Angle,float Mechanical_balance,float Gyro)
{ float Bias;int balance;Bias=Angle-Mechanical_balance; //===求出平衡的角度中值和机械相关balance=balance_UP_KP*Bias+balance_UP_KD*Gyro; //===计算平衡控制的电机PWM PD控制 kp是P系数 kd是D系数 return balance;
}/**************************************************************************
函数功能:速度PI控制
入口参数:电机编码器的值
返回 值:速度控制PWM
**************************************************************************/
int velocity(int encoder_left,int encoder_right,int Target_Speed)
{ static float Velocity,Encoder_Least,Encoder;static float Encoder_Integral;//=============速度PI控制器=======================// Encoder_Least =(Encoder_Left+Encoder_Right);//-target; //===获取最新速度偏差==测量速度(左右编码器之和)-目标速度 Encoder *= 0.8; //===一阶低通滤波器 Encoder += Encoder_Least*0.2; //===一阶低通滤波器 Encoder_Integral +=Encoder; //===积分出位移 积分时间:10msEncoder_Integral=Encoder_Integral - Target_Speed; //===接收遥控器数据,控制前进后退if(Encoder_Integral>10000) Encoder_Integral=10000; //===积分限幅if(Encoder_Integral<-10000) Encoder_Integral=-10000; //===积分限幅 Velocity=Encoder*velocity_KP+Encoder_Integral*velocity_KI; //===速度控制 if(pitch<-40||pitch>40) Encoder_Integral=0; //===电机关闭后清除积分return Velocity;
}
/**************************************************************************
函数功能:转向PD控制
入口参数:电机编码器的值、Z轴角速度
返回 值:转向控制PWM
**************************************************************************/int Turn_UP(int gyro_Z, int RC)
{int PWM_out;/*转向约束*/if(RC==0)Turn_Kd=TURN_KD; //若无左右转向指令,则开启转向约束else Turn_Kd=0; //若左右转向指令接收到,则去掉转向约束PWM_out=Turn_Kd*gyro_Z + Turn_KP*RC;return PWM_out;
}void Tracking()
{TkSensor=0;TkSensor+=(C1<<3);TkSensor+=(C2<<2);TkSensor+=(C3<<1);TkSensor+=C4;
}
void Get_RC()
{static u8 SR04_Counter =0;static float RATE_VEL = 1;float RATE_TURN = 1.6;float LY,RX; //PS2手柄控制变量int Yuzhi=2; //PS2控制防抖阈值switch(CTRL_MODE){case 97:SR04_Counter++;if(SR04_Counter>=20) //100ms读取一次超声波的数据{SR04_Counter=0;SR04_StartMeasure(); //读取超声波的值}if(SR04_Distance<=30) {Target_Speed=0,Turn_Speed=40;}else{Target_Speed=30,Turn_Speed=0;}break;case 98://蓝牙模式if((Fore==0)&&(Back==0))Target_Speed=0;//未接受到前进后退指令-->速度清零,稳在原地if(Fore==1)Target_Speed--;//前进1标志位拉高-->需要前进if(Back==1)Target_Speed++;///*左右*/if((Left==0)&&(Right==0))Turn_Speed=0;if(Left==1)Turn_Speed-=30; //左转if(Right==1)Turn_Speed+=30; //右转break;case 99://循迹模式Tracking();switch(TkSensor){case 15:Target_Speed=0;Turn_Speed=0;break;case 9:Target_Speed--;Turn_Speed=0;break;case 2://向右转Target_Speed--;Turn_Speed=15;break;case 4://向左转Target_Speed--;Turn_Speed=-15;break;case 8:Target_Speed=-10;Turn_Speed=-80;break;case 1:Target_Speed=-10;Turn_Speed=80;break;}break;case 100://PS2手柄遥控if(PS2_Plugin){LY=PS2_LY-128; //获取偏差RX=PS2_RX-128; //获取偏差if(LY>-Yuzhi&&LY<Yuzhi)LY=0; //设置小角度的死区if(RX>-Yuzhi&&RX<Yuzhi)RX=0; //设置小角度的死区if(Target_Speed>-LY/RATE_VEL) Target_Speed--;else if(Target_Speed<-LY/RATE_VEL) Target_Speed++;Turn_Speed=RX/RATE_TURN;}else{Target_Speed=0,Turn_Speed=0;}break;}
}
2、编码电机编码值采集
#include "encoder.h"/**************************************************************************
函数功能:把TIM2初始化为编码器接口模式
入口参数:无
返回 值:无
**************************************************************************/
void Encoder_Init_TIM2(void)
{TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; GPIO_InitTypeDef GPIO_InitStructure;RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);//使能定时器4的时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//使能PB端口时钟GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1; //端口配置GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入GPIO_Init(GPIOA, &GPIO_InitStructure); //根据设定参数初始化GPIOBTIM_TimeBaseStructInit(&TIM_TimeBaseStructure);TIM_TimeBaseStructure.TIM_Prescaler = 0x0; // 预分频器 TIM_TimeBaseStructure.TIM_Period = ENCODER_TIM_PERIOD; //设定计数器自动重装值TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;//选择时钟分频:不分频TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;TIM向上计数 TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);//使用编码器模式3TIM_ICStructInit(&TIM_ICInitStructure);TIM_ICInitStructure.TIM_ICFilter = 10;TIM_ICInit(TIM2, &TIM_ICInitStructure);TIM_ClearFlag(TIM2, TIM_FLAG_Update);//清除TIM的更新标志位TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);//Reset counterTIM_SetCounter(TIM2,0);TIM_Cmd(TIM2, ENABLE);
}
/**************************************************************************
函数功能:把TIM3初始化为编码器接口模式
入口参数:无
返回 值:无
**************************************************************************/
void Encoder_Init_TIM3(void)
{TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; GPIO_InitTypeDef GPIO_InitStructure;RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);//使能定时器4的时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//使能PB端口时钟GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7; //端口配置GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入GPIO_Init(GPIOA, &GPIO_InitStructure); //根据设定参数初始化GPIOBTIM_TimeBaseStructInit(&TIM_TimeBaseStructure);TIM_TimeBaseStructure.TIM_Prescaler = 0x0; // 预分频器 TIM_TimeBaseStructure.TIM_Period = ENCODER_TIM_PERIOD; //设定计数器自动重装值TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;//选择时钟分频:不分频TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;TIM向上计数 TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12,TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);//使用编码器模式3TIM_ICStructInit(&TIM_ICInitStructure);TIM_ICInitStructure.TIM_ICFilter = 10;TIM_ICInit(TIM3, &TIM_ICInitStructure);TIM_ClearFlag(TIM3, TIM_FLAG_Update);//清除TIM的更新标志位TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);//Reset counterTIM_SetCounter(TIM3,0);TIM_Cmd(TIM3, ENABLE);
}/**************************************************************************
函数功能:单位时间读取编码器计数
入口参数:定时器
返回 值:速度值
**************************************************************************/
int Read_Encoder(u8 TIMX)
{int Encoder_TIM; switch(TIMX){case 2: Encoder_TIM= (short)TIM2 -> CNT; TIM2 -> CNT=0;break;case 3: Encoder_TIM= (short)TIM3 -> CNT; TIM3 -> CNT=0;break; default: Encoder_TIM=0;}return Encoder_TIM;
}
3、PWM配置
#include "pwm.h"//PWM输出初始化
//arr:自动重装值
//psc:时钟预分频数
//TIM1_PWM_Init(7199,0);//PWM频率=72000/(7199+1)=10Khzvoid TIM1_PWM_Init(u16 arr,u16 psc)
{ GPIO_InitTypeDef GPIO_InitStructure;TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;TIM_OCInitTypeDef TIM_OCInitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);// RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA , ENABLE); //使能GPIO外设时钟使能//设置该引脚为复用输出功能,输出TIM1 CH1 CH4的PWM脉冲波形GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8|GPIO_Pin_11; //TIM_CH1 //TIM_CH4GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值 TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为TIMx时钟频率除数的预分频值 不分频TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_timTIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //选择定时器模式:TIM脉冲宽度调制模式1TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能TIM_OCInitStructure.TIM_Pulse = 0; //设置待装入捕获比较寄存器的脉冲值TIM_OCInitStructure.TIM_Pulse = arr >> 1;TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性:TIM输出比较极性高TIM_OC1Init(TIM1, &TIM_OCInitStructure); //根据TIM_OCInitStruct中指定的参数初始化外设TIMxTIM_OC4Init(TIM1, &TIM_OCInitStructure); //根据TIM_OCInitStruct中指定的参数初始化外设TIMxTIM_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; //PA2GPIO_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;//PA3GPIO_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); //初始化串口2USART_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<len; i++)Uart2SendByte(*buf++);
}
void Uart2SendStr(char *str)
{u16 i,len;len = strlen(str);for(i=0; i<len; i++)Uart2SendByte(*str++);
}
5、中断处理函数
void EXTI9_5_IRQHandler(void)
{static u8 Voltage_Counter=0;if(PBin(5)==0){EXTI->PR=1<<5; //清除LINE5上的中断标志位 mpu_dmp_get_data(&pitch,&roll,&yaw); //得到欧拉角(姿态角)的数据MPU_Get_Gyroscope(&gyrox,&gyroy,&gyroz); //得到陀螺仪数据Encoder_Left=Read_Encoder(2); //读取编码器的值,保证输出极性一致Encoder_Right=-Read_Encoder(3); //读取编码器的值Led_Flash(100);Voltage_Counter++;if(Voltage_Counter==20) //100ms读取一次电压{Voltage_Counter=0;Voltage=Get_battery_volt(); //读取电池电压}if(KEY_Press(100)) //长按按键切换模式并触发模式切换初始化{if(++CTRL_MODE>=101) CTRL_MODE=97;Mode_Change=1;}Get_RC();Target_Speed=Target_Speed>SPEED_Y?SPEED_Y:(Target_Speed<-SPEED_Y?(-SPEED_Y):Target_Speed);//限幅Turn_Speed=Turn_Speed>SPEED_Z?SPEED_Z:(Turn_Speed<-SPEED_Z?(-SPEED_Z):Turn_Speed);//限幅( (20*100) * 100)Balance_Pwm =balance_UP(pitch,Mechanical_angle,gyroy); //===直立环PID控制 Velocity_Pwm=velocity(Encoder_Left,Encoder_Right,Target_Speed); //===速度环PID控制 Turn_Pwm =Turn_UP(gyroz,Turn_Speed); //===转向环PID控制Moto1=Balance_Pwm-Velocity_Pwm+Turn_Pwm; //===计算左轮电机最终PWMMoto2=Balance_Pwm-Velocity_Pwm-Turn_Pwm; //===计算右轮电机最终PWMXianfu_Pwm(); //===PWM限幅Turn_Off(pitch,12); //===检查角度以及电压是否正常Set_Pwm(Moto1,Moto2); //===赋值给PWM寄存器 }
}
七、PCB板设计
八、代码开源
1、寄存器版本
链接:https://pan.baidu.com/s/1NlMHsgMF2Cu8sz955n27Eg?pwd=zxf1
提取码:zxf1
--来自百度网盘超级会员V2的分享
2、HAL库版本
链接:https://pan.baidu.com/s/1rW5M7Dz-TK4IWJxNp57mBw?pwd=zxf1
提取码:zxf1
--来自百度网盘超级会员V2的分享
相关文章:

STM32两轮平衡小车原理详解(开源)
一、引言 关于STM32两轮平衡车的设计,我想在读者阅读本文之前应该已经有所了解,所以本文的重点是代码的分享和分析。至于具体的原理,我觉得读者不必阅读长篇大论的文章,只需按照本文分享的代码自己亲手制作一辆平衡车,…...
区间内的真素数问题(C#)
题目:区间内的真素数 找出正整数 M 和 N 之间(N 不⼩于 M)的所有真素数。真素数的定义:如果⼀个正整数P 为素数,且其反序也为素数,那么 P 就为真素数。例如,11,13 均为真素数&#…...

eclipse安装lombok插件
lombok插件下载:Download 下载完成,lombok.jar放到eclipse根目录,双击jar运行 运行界面,点击Install安装。 安装完成,重启IDE,rebuild 项目。 rebuild 项目...

故障演练 | 微服务架构下如何做好故障演练
前言 微服务架构场景中,应用系统复杂切分散。长期运行时,局部出现故障时不可避免的。如果发生故障时不能进行有效反应,系统的可用性将极大地降低。 什么是故障演练 故障演练是指模拟生产环境中可能出现的故障,测试系统或应用在…...

Python爬虫-获取汽车之家车家号
前言 本文是该专栏的第9篇,后面会持续分享python爬虫案例干货,记得关注。 地址:aHR0cHM6Ly9jaGVqaWFoYW8uYXV0b2hvbWUuY29tLmNuL0F1dGhvcnMjcHZhcmVhaWQ9MjgwODEwNA== 需求:获取汽车之家车家号数据 笔者将在正文中介绍详细的思路以及采集方法,废话不多说,跟着笔者直接往…...

No195.精选前端面试题,享受每天的挑战和学习
🤍 前端开发工程师(主业)、技术博主(副业)、已过CET6 🍨 阿珊和她的猫_CSDN个人主页 🕠 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 🍚 蓝桥云课签约作者、已在蓝桥云课上架的前后端实战课程《Vue.js 和 Egg.js 开发企业级健康管理项目》、《带你从入…...
pytest与testNg自动化框架
一.pytest 1.安装pytest: pip install pytest 2.编写用例 - 收集用例 - 执行用例 - 生成报告 3.pytest如何自动识别用例 识别规则如下: 1、搜索根目录:默认从当前目录中搜集测试用例,即在哪个目录下运行pytest命令,…...

数据库安全:Hadoop 未授权访问-命令执行漏洞.
数据库安全:Hadoop 未授权访问-命令执行漏洞. Hadoop 未授权访问主要是因为 Hadoop YARN 资源管理系统配置不当,导致可以未经授权进行访问,从而被攻击者恶意利用。攻击者无需认证即可通过 RESTAPI 部署任务来执行任意指令,最终完…...

前端---认识HTML
文章目录 什么是HTML?HTML的读取、运行HTML的标签注释标签标题标签段落标签换行标签格式化标签图片标签a标签表格标签列表标签表单标签form标签input标签文本框单选框复选框普通按钮提交按钮文件选择框 select标签textarea标签特殊标签div标签span标签 什么是HTML&a…...

竞赛 题目:基于FP-Growth的新闻挖掘算法系统的设计与实现
文章目录 0 前言1 项目背景2 算法架构3 FP-Growth算法原理3.1 FP树3.2 算法过程3.3 算法实现3.3.1 构建FP树 3.4 从FP树中挖掘频繁项集 4 系统设计展示5 最后 0 前言 🔥 优质竞赛项目系列,今天要分享的是 基于FP-Growth的新闻挖掘算法系统的设计与实现…...

保姆级jupyter lab配置清单
❤️觉得内容不错的话,欢迎点赞收藏加关注😊😊😊,后续会继续输入更多优质内容❤️ 👉有问题欢迎大家加关注私戳或者评论(包括但不限于NLP算法相关,linux学习相关,读研读博…...

数据结构预算法--链表(单链表,双向链表)
1.链表 目录 1.链表 1.1链表的概念及结构 1.2 链表的分类 2.单链表的实现(不带哨兵位) 2.1接口函数 2.2函数的实现 3.双向链表的实现(带哨兵位) 3.1接口函数 3.2函数的实现 1.1链表的概念及结构 概念:链表是一种物理存储结…...

数据结构线性表——栈
前言:哈喽小伙伴们,今天我们将一起进入数据结构线性表的第四篇章——栈的讲解,栈还是比较简单的哦,跟紧博主的思路,不要掉队哦。 目录 一.什么是栈 二.如何实现栈 三.栈的实现 栈的初始化 四.栈的操作 1.数据入栈…...
自定义 springboot 启动器 starter 与自动装配原理
Maven 依赖 classpath 类路径管理 Maven 项目中的类路径添加来源分为三类 自定义 springboot starter starter 启动器定义的规则自定义 starter 示例 自动装配 文章链接...

16 _ 二分查找(下):如何快速定位IP对应的省份地址?
通过IP地址来查找IP归属地的功能,不知道你有没有用过?没用过也没关系,你现在可以打开百度,在搜索框里随便输一个IP地址,就会看到它的归属地。 这个功能并不复杂,它是通过维护一个很大的IP地址库来实现的。地址库中包括IP地址范围和归属地的对应关系。 当我们想要查询202…...
vb.net圣经带快捷键,用原装的数据库
Imports System.Data.SqlServerCe Imports System.Text.RegularExpressions Imports System.Data.OleDbPublic Class Form1Dim jiuyue As String() {"创", "出", "利", "民", "申", "书", "士", "…...

Unity中Shader的雾效
文章目录 前言一、Unity中的雾效在哪开启二、Unity中不同种类雾的区别1、线性雾2、指数雾1(推荐用这个,兼具效果和性能)3、指数雾2(效果更真实,性能消耗多) 三、在我们自己的Shader中实现判断,是…...

企业微信开发教程一:添加企微应用流程图解以及常见问题图文说明
最近在前辈的基础上新添加了一个企微应用,过程中遇到了一些卡点,这里一一通过图片标注与注释的方式记录一下,希望能给后来人提供一些清晰明了的帮助,话不多说,大家直接看图吧。 (文中包括一些本项目独有的配…...
【LeetCode】67. 二进制求和
67. 二进制求和 难度:简单 题目 给你两个二进制字符串 a 和 b ,以二进制字符串的形式返回它们的和。 示例 1: 输入:a "11", b "1" 输出:"100"示例 2: 输入:a "…...

【LeetCode刷题笔记】二叉树(一)
102. 二叉树的层序遍历 解题思路: 1. BFS广度优先遍历 ,使用队列,按层访问 解题思路: 2. 前序遍历 , 递归 ,在递归方法参数中,将 层索引...

地震勘探——干扰波识别、井中地震时距曲线特点
目录 干扰波识别反射波地震勘探的干扰波 井中地震时距曲线特点 干扰波识别 有效波:可以用来解决所提出的地质任务的波;干扰波:所有妨碍辨认、追踪有效波的其他波。 地震勘探中,有效波和干扰波是相对的。例如,在反射波…...

基于ASP.NET+ SQL Server实现(Web)医院信息管理系统
医院信息管理系统 1. 课程设计内容 在 visual studio 2017 平台上,开发一个“医院信息管理系统”Web 程序。 2. 课程设计目的 综合运用 c#.net 知识,在 vs 2017 平台上,进行 ASP.NET 应用程序和简易网站的开发;初步熟悉开发一…...

【第二十一章 SDIO接口(SDIO)】
第二十一章 SDIO接口 目录 第二十一章 SDIO接口(SDIO) 1 SDIO 主要功能 2 SDIO 总线拓扑 3 SDIO 功能描述 3.1 SDIO 适配器 3.2 SDIOAHB 接口 4 卡功能描述 4.1 卡识别模式 4.2 卡复位 4.3 操作电压范围确认 4.4 卡识别过程 4.5 写数据块 4.6 读数据块 4.7 数据流…...

WordPress插件:AI多语言写作与智能配图、免费AI模型、SEO文章生成
厌倦手动写WordPress文章?AI自动生成,效率提升10倍! 支持多语言、自动配图、定时发布,让内容创作更轻松! AI内容生成 → 不想每天写文章?AI一键生成高质量内容!多语言支持 → 跨境电商必备&am…...
【HTML-16】深入理解HTML中的块元素与行内元素
HTML元素根据其显示特性可以分为两大类:块元素(Block-level Elements)和行内元素(Inline Elements)。理解这两者的区别对于构建良好的网页布局至关重要。本文将全面解析这两种元素的特性、区别以及实际应用场景。 1. 块元素(Block-level Elements) 1.1 基本特性 …...

【Java_EE】Spring MVC
目录 Spring Web MVC 编辑注解 RestController RequestMapping RequestParam RequestParam RequestBody PathVariable RequestPart 参数传递 注意事项 编辑参数重命名 RequestParam 编辑编辑传递集合 RequestParam 传递JSON数据 编辑RequestBody …...

【OSG学习笔记】Day 16: 骨骼动画与蒙皮(osgAnimation)
骨骼动画基础 骨骼动画是 3D 计算机图形中常用的技术,它通过以下两个主要组件实现角色动画。 骨骼系统 (Skeleton):由层级结构的骨头组成,类似于人体骨骼蒙皮 (Mesh Skinning):将模型网格顶点绑定到骨骼上,使骨骼移动…...
uniapp中使用aixos 报错
问题: 在uniapp中使用aixos,运行后报如下错误: AxiosError: There is no suitable adapter to dispatch the request since : - adapter xhr is not supported by the environment - adapter http is not available in the build 解决方案&…...

在WSL2的Ubuntu镜像中安装Docker
Docker官网链接: https://docs.docker.com/engine/install/ubuntu/ 1、运行以下命令卸载所有冲突的软件包: for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done2、设置Docker…...

使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台
🎯 使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台 📌 项目背景 随着大语言模型(LLM)的广泛应用,开发者常面临多个挑战: 各大模型(OpenAI、Claude、Gemini、Ollama)接口风格不统一;缺乏一个统一平台进行模型调用与测试;本地模型 Ollama 的集成与前…...