两轮自平衡小车-资料整理

2023-10-25 00:21

本文主要是介绍两轮自平衡小车-资料整理,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、需求分析

1. 需要实现的功能

  (1)小车自平衡;

  (2)遥控两轮自平衡车移动行驶;

  (3)显示电池电压、平衡倾角、角速度等小车状态信息。

2. 如何实现需求

  (1)最首要的一点,小车得能动起来,所以要用PWM波控制电机驱动轮子;

  (2)要实现平衡,首先得获取小车的姿态,并进行姿态解算,才能进行对应的校正;

  (3)关于平衡控制,本项目使用PID算法调整PWM波来实现;

  (4)使用PID算法控制电机实现平衡,要求获取电机转速作为反馈,所以需要编码器测量电机转速;

  (5)通过蓝牙无线传输技术使用手机APP蓝牙遥控操纵小车的行驶;

  (6)使用OLED显示屏显示小车状态信息。

二、物料清单

  1. 主控芯片:STM32F103C8T6
  2. 姿态检测:MPU6050(通信接口:I2C)
  3. 直流电机驱动:TB6612(直流电机属于大功率器件,GPIO口无法直接驱动,所以需要接电机驱动芯片)
  4. 带霍尔编码器的直流电机(型号:MG513,额定电压:12V)
  5. 12V锂电池
  6. 稳压芯片:LM2596(12V的电压环境对单片机、各传感器和各模块来说电压太高,直接供电会烧毁各模块,所以还需要对电压进行降压处理)
  7. 蓝牙模块:JDY-31(通信接口:USART)
  8. 0.96寸OLED(I2C接口)
  9. 小车车架,轮子等

三、用到的STM32单片机的片上资源

1. NVIC(Nested vectoredinterrupt controller,嵌套向量中断控制器)

    用于管理中断

2. RCC(Reset and clock control,复位与时钟控制)

3. GPIO(General-purpose input/output,通用IO口)

4. AFIO(Alternatefunction I/O,复用IO口)

    用于引脚重定义与中断引脚选择

5.EXTI(External Interrupts,外部中断)

   小车的控制动作基本上都放在了由MPU6050触发的外部中断函数中。

6.TIM(定时器)

    使用输出比较产生PWM波形,使用编码器接口获取电机转速

7. ADC(Analog-to-Digital Converter,模数转换)

     将电池电压信息转换为数字信息

8. USART(Universal Synchronous/Asynchronous Receiver/Transmitter,通用同步/异步收发器)

      用于与蓝牙模块通信

9. I2C(Inter IC Bus,I2C通信)

       用于与MPU6050通信

四、PID算法

1. P , I , D 这三个控制器的简介

k时刻的偏差e(k)=期望值-当前值

P控制器(比例):eq?Kp*e%28k%29

是缩小当前值和期望值之间偏差的主要手段,但无法消除稳态误差。

 I控制器(积分):eq?Ki*%5Csum%20e%28k%29

可消除稳态误差,但容易造成振荡。

D控制器(微分):eq?Kd*%5Be%28k%29-e%28k-1%29%5D

当前偏差减去上一时刻的偏差,偏差的微分可反映偏差的变化趋势。D控制器能根据偏差的变化趋势进行超前调节,抑制振荡,防止系统出现超调,但有可能造成调节周期过长,也会放大噪声。

这方面更详细的讲解可看以下视频,讲得很简单易懂

https://www.bilibili.com/video/av382551634

2. 直立环

作用:让小车角度趋近0

一个很直观的现象是当人要往前倒时,得往前移动才能恢复平衡。控制小车平衡也是一样的,直立环的调节过程就是当小车往前倒时,驱动轮子,让小车往前动,当小车往后倒时,则让小车往后动。

直立环使用PD控制:eq?Out%3DKp*e%28k%29+Kd*%5Be%28k%29-e%28k-1%29%5D,e(k)为k时刻小车目标倾角与当前倾角的差值

为什么直立环使用PD控制?

因为小车存在惯性,所以当小车角度为0时还会存在角速度,所以需要加入D控制器,预测下一时刻的偏差,进行超前调节。这里也可以把D控制器的作用理解为阻尼力,能抑制振荡。

那么为什么直立环不用积分呢?是因为当比例系数很大的时候系统几乎没有稳态误差。平衡小车直立控制需要快速性,所以P是很大的,积分的作用是消除稳态误差,而稳态误差对于直立控制的影响很小,所以也就不需要积分了。

3. 速度环

作用:让电机速度趋近0

之所以需要速度环,是因为直立环只保证了小车能够直立,但没有保证它静止不动。而且小车立起来之后,那就自然会有向前移动和向后移动的趋势,因为电机的移动速度不受控。如果加上速度环配合直立环一起工作,那么就可以让小车在直立起来的同时,还能够抑制小车前后移动。

速度环使用PI控制:eq?Out%3DKp*e%28k%29+Ki*%5Csum%20e%28k%29,e(k)为k时刻小车目标速度与当前速度的差值

为什么速度环使用 PI 控制?

对于速度控制当然是越精确越好,平衡小车当然希望速度一直为0。但是光有P控制器是不够的,因为P控制器的主要缺点是无法消除稳态误差。那么就需要用I积分控制器来消除。

虽然D控制器可以预测未来,提前响应,但是微分控制的缺点是会放大噪声。而速度环的偏差是由电机运动引起的,噪声很大,所以不加入微分控制。
 

4. 串级控制

本项目的串级控制即把直立环和速度环串联起来。控制框图如下:

57d0049bed6d481daa51d47fa08f9943.jpeg

注意一点,角度反馈是基于MPU6050,而速度环的速度反馈是基于电机的编码器。

速度环输入:1.给定速度。2.速度反馈。
输出:角度值(直立环的期望速度输入)

直立环输入:1.给定角度(速度环输出)。2.角度反馈
输出:PWM(直接控制小车)

5. 转向环

作用:令小车进行前/后/左/右的位移

要向左转,则让左轮反转,右轮正转。要向右转,则让右轮反转,左轮正转。

五、程序设计

1. 编码器

当电机旋转时, 编码器会输出A相和B相相差90度的正交信号脉冲。在本项目中,我们使用编码器接口模式 3,即上升沿、下降沿均计数。把A相和B相的所有边沿作为计数器的计数时钟,根据另一相的状态判定是计数自增还是自减。如当A相为上升沿时,若B相为低电平,则计数自增,可知此时电机正转,若B相为高电平,则计数自减,可知电机反转。

3476714a2c934bec93ed64af9c1ed5d0.png

1.1 初始化

第一步,开启RCC时钟,包括GPIO和TIM的时钟

第二步,配置GPIO

第三步,配置时基单元

第四步,配置输入捕获单元,只需配置滤波器和极性

第五步,配置编码器接口模式

第六步,启动定时器

void Encoder_Init_TIM2(void)
{RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); //开启时钟,TIM2挂载在APB1总线RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);/*配置GPIO*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU ; //上拉输入GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0|GPIO_Pin_1 ;GPIO_Init(GPIOA,&GPIO_InitStructure);/*配置时基单元*/TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; //定义TIM结构体TIM_TimeBaseStructInit(&TIM_TimeBaseInitStructure); //赋初始值/*采样频率可由内部时钟加一个时钟分频而来,这里就是指定这个时钟分频的,可见此参数与时基单元关系并不大*/TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1; TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up; //配置计数模式为向上计数/*PWM频率=72MHz/(PSC+1)/(ARR+1)PWM占空比=CCR/(ARR+1)PWM分辨率=1/(ARR+1)*/TIM_TimeBaseInitStructure.TIM_Period=65536-1;  //满量程计数,即使用最大计数范围,也方便换算为负数TIM_TimeBaseInitStructure.TIM_Prescaler=1-1;  //不分频TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure); /*配置输入捕获单元*///STM32会直接占用整个TIMx定时器通道,编码器引脚也是固定的ch1和ch2,通道已被硬件固化,不需配置TIM_ICInitTypeDef Tim_ICInitStructure;TIM_ICStructInit(&Tim_ICInitStructure); //赋初始值Tim_ICInitStructure.TIM_ICFilter=0xf;  //配置滤波器采样频率,采样频率f=fDTS/32,N=8 TIM_ICInit(TIM2,&Tim_ICInitStructure);/*配置编码器接口*/TIM_EncoderInterfaceConfig(TIM2,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising);//写在后面的会覆盖前面的,所以配置编码器接口的函数要放在配置输入捕获单元的函数后面TIM_ClearFlag(TIM2,TIM_FLAG_Update);TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE); //配置中断类型为溢出更新TIM_SetCounter(TIM2,0);TIM_Cmd(TIM2,ENABLE);
}

1.2 编码器速度读取函数

在单位时间内读取编码器脉冲数可得到速度,实现方法就是每隔一段固定的时间触发一次定时中断,采集编码器计数值并保存,再将定时器的计数值清零。

为什么要在TIM_GetCounter前加上(short)进行强制类型转换呢?

猜想是因为函数TIM_GetCounter的返回值为uint16_t(unsigned short int)数据类型,即无符号数,而我们的项目在测速方面需要的是有符号数,所以得通过强制类型转换为有符号数。

int Read_Encoder(int TIMx) 
{int value_1;switch(TIMx){case 2:value_1=(short)TIM_GetCounter(TIM2);TIM_SetCounter(TIM2,0);break; 
//if是编码器2,1.采集编码器计数值并保存;2.将定时器的计数值清零case 4:value_1=(short)TIM_GetCounter(TIM4);TIM_SetCounter(TIM4,0);break;default:value_1=0;}return value_1;
}void TIM2_IRQHandler(void) 
{if(TIM_GetITStatus(TIM2,TIM_IT_Update)==SET) //检查中断标志位{TIM_ClearITPendingBit(TIM2,TIM_IT_Update); //清除中断标志位}
}void TIM4_IRQHandler(void) 
{if(TIM_GetITStatus(TIM4,TIM_IT_Update)==SET) //检查中断标志位{TIM_ClearITPendingBit(TIM4,TIM_IT_Update); //清除中断标志位}
}

2. PWM

PWM(Pulsw Width Modulation),脉冲宽度调制,在具有惯性的系统中,可通过对一系列脉冲宽度进行调制,来等效获得所需模拟参量。

数字量由0和1组成,是离散的。模拟量则是在一定范围内连续变化的量。用模拟量点灯可以通过调节电位器让灯呈现不同的亮度。而用数字量点灯,按理说LED只有亮或灭两种状态。比如说给的是5V电压,那就呈现5V的亮度。但只要以很快的频率让LED不断点亮、熄灭、点亮、熄灭,由于视觉暂留,人眼就看不出灯在闪烁了,灯可以呈现出0~5V之间的一个亮度。通过脉冲宽度调制,调节高低电平的时间比例,就能让LED呈现出不同的亮度级别,实现和用模拟量点灯一样的效果。

电机调速也是一样的,由于电机转动具有惯性,所以只要脉冲的频率够快,电机就能维持在一个速度,调节高低电平的时间比例,就能调节电机的转速。

PWM参数:
频率=1/一个周期的时间,占空比=高电平时间/一个周期的时间,分辨率=占空比变化步距

PWM频率越快,等效模拟的信号就越平稳,但性能开销也越大。

占空比越大,等效模拟的电压就趋近于高电平,占空比越小,等效模拟的电压就越趋近于低电平。

分辨率指占空比变化的精细程度,如1%分辨率就是1%→2%→3%,这样以1%的步距跳变,0.1%就是以0.1%的步距跳变。

我们可以使用STM32单片机的输出比较通道来输出PWM波,本项目的输出比较模式为PWM模式1,向上计数时,若CNT<CCR,置有效电平,CNT>=CCR,置无效电平。

我们设置PWM波,设定CCR值即可,CNT会自增。以下是涉及到的公式:

        PWM频率=72MHz/(PSC+1)/(ARR+1) (STM32F103C8T6的主频为72MHz)
        PWM占空比=CCR/(ARR+1)
         PWM分辨率=1/(ARR+1)

2.1 初始化

第一步,开启RCC时钟,包括GPIO和TIM的时钟

第二步,配置GPIO

第三步,配置时基单元

第四步,配置输出比较单元,设置CCR、输出比较模式、极性选择和输出使能

第五步,主输出使能

第六步,使能OC和ARR的预装载寄存器

第七步,启动定时器

void MiniBalance_PWM_Init(uint16_t Arr,uint16_t Psc)
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_TIM1,ENABLE); //开启时钟,TIM1挂载在APB2总线/*配置GPIO*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP ; //复用推挽输出GPIO_InitStructure.GPIO_Pin=GPIO_Pin_8 | GPIO_Pin_11 ;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStructure);/*配置时基单元*/TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; //定义TIM结构体TIM_TimeBaseStructInit(&TIM_TimeBaseInitStructure); //赋初始值/*采样频率可由内部时钟加一个时钟分频而来,这里就是指定这个时钟分频的,可见此参数与时基单元关系并不大*/TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1; TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up; //配置计数模式为向上计数/*PWM频率=72MHz/(PSC+1)/(ARR+1)PWM占空比=CCR/(ARR+1)PWM分辨率=1/(ARR+1)*/TIM_TimeBaseInitStructure.TIM_Period=Arr-1;  //设定ARR值,取值范围是0~65535TIM_TimeBaseInitStructure.TIM_Prescaler=Psc-1;  //设定PSC值,取值范围是0~65535TIM_TimeBaseInit(TIM1,&TIM_TimeBaseInitStructure); /*配置输出比较单元*/TIM_OCInitTypeDef Tim_OcInitStructure;TIM_OCStructInit(&Tim_OcInitStructure); //赋初始值Tim_OcInitStructure.TIM_OCMode=TIM_OCMode_PWM1; //设置输出比较模式为PWM模式1Tim_OcInitStructure.TIM_OCPolarity=TIM_OCPolarity_High; //设置为高极性,即极性不翻转,REF波形直接输出,或者说有效电平为高电平Tim_OcInitStructure.TIM_OutputState=TIM_OutputState_Enable; //输出使能Tim_OcInitStructure.TIM_Pulse=0; //设定CCR值TIM_OC1Init(TIM1,&Tim_OcInitStructure);TIM_OC4Init(TIM1,&Tim_OcInitStructure);TIM_CtrlPWMOutputs(TIM1,ENABLE); //TIM1是高级定时器,必须得主输出使能才可以输出PWM波形TIM_OC1PreloadConfig(TIM1,TIM_OCPreload_Enable);	//使能OC1预装载寄存器TIM_OC4PreloadConfig(TIM1,TIM_OCPreload_Enable);	//使能OC4预装载寄存器TIM_ARRPreloadConfig(TIM1,ENABLE);	//使能TIM1在ARR上的预装载寄存器TIM_Cmd(TIM1,ENABLE); //使能计数器
}

3. 电机驱动


 

340f4157a03149d7af5252c2fef92d57.png

PWMB、PWMA 分别用于控制左右电机的转速。BIN1、BIN2 用于控制左电机的正反转,BO1、BO2驱动左电机。AIN1、AIN2 用于控制右电机的正反转,AO1、AO2驱动右电机。注意,必须要有 PWM 的输入才会有电机线 O1 和 O2 的输出,才能驱动电机转动。PWM 的大小对应电机转速大小。

3.1 初始化

第一步,开启GPIO的RCC时钟

第二步,配置GPIO

void MiniBalance_Motor_Init(void)
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); //开启时钟/*配置GPIO*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP ; //推挽输出GPIO_InitStructure.GPIO_Pin=GPIO_Pin_12 | GPIO_Pin_13 |GPIO_Pin_14 | GPIO_Pin_15;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOB,&GPIO_InitStructure);
}

3.2 赋值函数

PWM频率=72MHz/(PSC+1)/(ARR+1),频率为10KHz。

MiniBalance_PWM_Init(7200,1);   //初始化PWM 10KHZ与电机硬件接口,用于驱动电机
#define PWMB   TIM1->CCR4  
#define PWMA   TIM1->CCR1  

赋值给CCR,占空比值即PWM值。

void Set_Pwm(int motor_left,int motor_right)
{if(motor_left>0){BIN1=0;BIN2=1; //前进 }else  {BIN1=1;BIN2=0; //后退}		PWMB=myabs(motor_left);if(motor_right>0){AIN2=0;AIN1=1;	//前进  }else{AIN2=1;AIN1=0; //后退}		PWMA=myabs(motor_right);
}

3.3 绝对值函数

赋给CCR的值不能为负数,所以需要在赋值之前进行绝对值处理。

int myabs(int a)
{ 		   int temp;if(a<0)  temp=-a;  else temp=a;return temp;
}

3.4 限幅函数

防止电机转速过大,需要有限幅函数。之所以设置为-7000~7000,是因为PWM占空比=CCR/(ARR+1),PWM值的范围为0~7200,因此要将幅值限制在-7200~7200。又因为最大转速容易损伤电机,所以设置为-7000~7000。

//PWM限幅
Motor_Left=PWM_Limit(Motor_Left,7000,-7000);
Motor_Right=PWM_Limit(Motor_Right,7000,-7000);			    
int PWM_Limit(int IN,int max,int min)
{int OUT = IN;if(OUT>max) OUT = max;if(OUT<min) OUT = min;return OUT;
}

4.EXTI

EXTI基本结构:

834c93027e9d451a9ed1f69695caeb65.png

4.1 初始化

第一步,开启RCC时钟,包括GPIO和AFIO的时钟,EXTI和NVIC的时钟是一直开着的,不需要我们来开

第二步,配置GPIO

第三步,配置AFIO,即调用GPIO_EXTILineConfig函数选择中断引脚,该函数虽然名字里带有GPIO,实际上与GPIO无关,而是与AFIO有关

第四步,配置EXTI,选择触发响应模式和边沿触发方式

第五步,配置NVIC,给中断选择优先级

void MPU6050_EXTI_Init(void)
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO,ENABLE); //开启时钟/*配置GPIO*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU; //上拉输入GPIO_InitStructure.GPIO_Pin=GPIO_Pin_12 ;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStructure);/*中断引脚选择,该函数与AFIO相关*/GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource12);EXTI_InitTypeDef EXTI_InitStructure;EXTI_InitStructure.EXTI_Line=EXTI_Line12; //将EXTI的12号线路配置为中断线EXTI_InitStructure.EXTI_LineCmd=ENABLE;   //开启中断EXTI_InitStructure.EXTI_Mode=EXTI_Mode_Interrupt; //指定触发响应模式,这里选择中断响应EXTI_InitStructure.EXTI_Trigger=EXTI_Trigger_Falling; //下降沿触发EXTI_Init(&EXTI_InitStructure);}

5. I2C通信

使用MPU6050需要先编写I2C通信层代码。

I2C是一种同步、半双工,带数据应答的通用数据总线,有两根通信线:SCL(时钟线)、SDA(数据线)。硬件电路为挂载的所有设备的SCL连接在一起,SDA连接在一起,SCL和SDA各添加一个上拉电阻。

02333c23618d4f6aa449ed43eaaa05cb.png

5.1时序基本单元

起始条件:SCL高电平期间,SDA从高电平切换到低电平

d789a6b52ef3437d941cc685a9e48f11.png

终止条件:SCL高电平期间,SDA从低电平切换到高电平

a95cc52d6c844b12a740ce6664573f7f.png

发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL(即主机放开对SCL的控制,让其恢复高电平),从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节。

5ffa89f16e0249088dd7b4a5f59b6866.png

接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)。

871a20efe4a5483d84622bd79ea7804f.png

发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答。

e18187175341468391fe5e83c0722e74.png

接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)。

a4b43bc7fc414f45b553e022285b0f59.png

发送完数据需要调接收应答,接收完数据需要调发送应答。

关于应答,我是这么理解的:以接收应答为例,SDA默认高电平,主机和从机都可以使其变为低电平,若主机释放SDA,SDA仍为低电平,说明从机在使其为低电平,那么主机就知道从机有应答了。若主机释放SDA,SDA恢复为高电平,说明从机没有动作,即没有应答。

5.2 I2C时序

指定地址写:

504c77150a5242ca8170910cb7a1f968.jpeg

指定地址读:

ce14f42421d843d1b30ed852088a1487.jpeg

5.3 初始化

开启时钟及配置GPIO

void IIC_Init(void)
{			RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启时钟GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8|GPIO_Pin_9;	//端口配置GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;      //推挽输出GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;    GPIO_Init(GPIOB, &GPIO_InitStructure);				
}

5.4 引脚操作的封装和改名

void MyI2C_W_SCL(uint8_t BitValue)
{GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue); 
//BitAction可将操作对象转换为位域,或者说位变量Delay_us(10);
}void MyI2C_W_SDA(uint8_t BitValue)
{GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);Delay_us(10);
}uint8_t MyI2C_R_SDA(void)
{uint8_t BitValue;BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);Delay_us(10);return BitValue;
}

5.5 模拟I2C时序基本单元

5.5.1 起始条件

void MyI2C_Start(void)
{MyI2C_W_SDA(1); //为了兼容起始条件和重复起始条件,先释放SDA再释放SCLMyI2C_W_SCL(1);MyI2C_W_SDA(0);MyI2C_W_SCL(0);
}

5.5.2 I2C终止条件

void MyI2C_Stop(void)
{MyI2C_W_SDA(0); //为了确保之后释放SDA能产生上升沿,要先拉低SDA,然后释放SCL,再释放SDAMyI2C_W_SCL(1);MyI2C_W_SDA(1);
}

5.5.3 发送一个字节

void MyI2C_SendByte(uint8_t Byte)
{uint8_t i;for(i=0;i<8;i++){MyI2C_W_SDA(Byte & (0x80>>i)); //主机依次将Byte的各位数据放到SDAMyI2C_W_SCL(1);    //每次释放SCL,从机都会把放在SDA的数据读走MyI2C_W_SCL(0);    //进入下一个时序单元}	
}

5.5.4 接收一个字节

uint8_t MyI2C_ReceiveByte(void)
{uint8_t i,Byte=0x00;MyI2C_W_SDA(1); //主机在接收之前,要先释放SDAfor(i=0;i<8;i++){MyI2C_W_SCL(1);    //释放SCLif(MyI2C_R_SDA()==1){Byte |= (0x80>>i);} //读SDA,如果读到的值为1,则在Byte的对应位置1,为0,则条件不成立,Byte该位为默认值0MyI2C_W_SCL(0); //进入下一个时序单元}return Byte;
}

5.5.5  发送应答

void MyI2C_SendAck(uint8_t AckBit)
{uint8_t i;MyI2C_W_SDA(AckBit); //主机把AckBit放到SDA MyI2C_W_SCL(1);     //从机读取应答MyI2C_W_SCL(0);     //进入下一个时序单元
}

5.5.6 接收应答

uint8_t MyI2C_ReceiveAck(void)
{uint8_t AckBit;MyI2C_W_SDA(1); 		//主机释放SDA,同时从机把应答位放到SDA上MyI2C_W_SCL(1);    		//释放SCLAckBit=MyI2C_R_SDA(); //主机读取应答。读到0,说明从机把SDA拉低了,有应答;读到1,则说明从机没有应答MyI2C_W_SCL(0);			//进入下一个时序单元return AckBit;
}

5.6 I2C时序

5.6.1 指定地址写

#define MPU6050_ADDRESS 	 0xD0void I2C_Write(uint8_t RegAddress,uint8_t Data)
{MyI2C_Start();MyI2C_SendByte(MPU6050_ADDRESS);MyI2C_ReceiveAck();MyI2C_SendByte(RegAddress);MyI2C_ReceiveAck();MyI2C_SendByte(Data);MyI2C_ReceiveAck();MyI2C_Stop();
}

5.6.2 指定地址读

uint8_t I2C_Read(uint8_t RegAddress)
{uint8_t Data;MyI2C_Start();MyI2C_SendByte(MPU6050_ADDRESS);MyI2C_ReceiveAck();MyI2C_SendByte(RegAddress);MyI2C_ReceiveAck();MyI2C_Start();MyI2C_SendByte(MPU6050_ADDRESS | 0x01);MyI2C_ReceiveAck();Data = MyI2C_ReceiveByte();MyI2C_SendAck(1);MyI2C_Stop();return Data;
}

6. MPU6050

MPU6050是一个六轴姿态传感器,使用I2C通信接口,可用于测量小车X、Y、Z轴的加速度值和角速度参数,通过内置的DMP(数字运动处理器)得到四元数,再在程序中计算出小车的俯仰角、横滚角和偏航角,就能得出小车姿态。

MPU6050的程序,尤其是DMP的代码非常复杂,因此本项目移植了正点原子的MPU6050程序文件。以下为摘选出的两个关键函数。

6.1  初始化

void MPU6050_initialize(void) 
{MPU6050_setClockSource(MPU6050_CLOCK_PLL_YGYRO); //设置时钟MPU6050_setFullScaleGyroRange(MPU6050_GYRO_FS_2000);//陀螺仪量程设置MPU6050_setFullScaleAccelRange(MPU6050_ACCEL_FS_2);	//加速度度最大量程 +-2GMPU6050_setSleepEnabled(0); //进入工作状态MPU6050_setI2CMasterModeEnabled(0);	 //不让MPU6050 控制AUXI2CMPU6050_setI2CBypassEnabled(0);	 //主控制器的I2C与	MPU6050的AUXI2C	直通关闭
}

6.2 读取MPU6050内置DMP的姿态信息

void Read_DMP(void)
{
unsigned long sensor_timestamp;
unsigned char more;
long quat[4];
dmp_read_fifo(gyro, accel, quat, &sensor_timestamp, &sensors, &more);
//读取 DMP 数据
if (sensors & INV_WXYZ_QUAT )
{
q0=quat[0] / q30;
q1=quat[1] / q30;
q2=quat[2] / q30;
q3=quat[3] / q30; 
//读取到的四元数是放大了 2^30 倍的,需要除以2^30,变换为原来的数据
Roll = asin(-2 * q1 * q3 + 2 * q0* q2)* 57.3; //计算出横滚角
Pitch = atan2(2 * q2 * q3 + 2 * q0 * q1, -2 * q1 * q1 - 2 * q2*
q2 + 1)* 57.3; // 计算出俯仰角
Yaw = atan2(2*(q1*q2 + q0*q3),q0*q0+q1*q1-q2*q2-q3*q3) * 57.3;
//计算出偏航角
}
}

7. 蓝牙模块

使用蓝牙模块需要先编写USART通信层代码。

USART是STM32内部集成的串口通信设备。串口通信需要两根数据线,连接发送端TX和接收端RX,TX和RX要交叉连接。

7.1 串口参数及时序

波特率:串口通信的速率

起始位:标志一个数据帧的开始,固定为低电平

数据位:数据帧的有效载荷,1为高电平,0为低电平,低位先行

校验位:用于数据验证,根据数据位计算得来

停止位:用于数据帧间隔,固定为高电平

e94f674be8844ef1afe8349167dd44c2.png

7.2  初始化

第一步,开启RCC时钟,包括GPIO和USART的时钟

第二步,配置GPIO

第三步,配置USART

第四步,需要接收的话,要配置中断,加上ITConfig和NVIC的代码再开启USART。

第五步,开启USART

void uart3_init(u32 bound)
{  	 //GPIO端口设置GPIO_InitTypeDef GPIO_InitStructure;USART_InitTypeDef USART_InitStructure;NVIC_InitTypeDef NVIC_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);	//使能UGPIOB时钟RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE);	//使能USART3时钟//USART3_TX  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; //PB.10GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	//复用推挽输出GPIO_Init(GPIOB, &GPIO_InitStructure);//USART3_RX	  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;//PB11GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入GPIO_Init(GPIOB, &GPIO_InitStructure);//Usart3 NVIC 配置NVIC_InitStructure.NVIC_IRQChannel = USART3_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0 ;//抢占优先级NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;		//子优先级NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//IRQ通道使能NVIC_Init(&NVIC_InitStructure);	//根据指定的参数初始化VIC寄存器//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(USART3, &USART_InitStructure);     //初始化串口3USART_ITConfig(USART3, USART_IT_RXNE, ENABLE);//开启串口接受中断USART_Cmd(USART3, ENABLE);                    //使能串口3 }

7.3 串口3获取APP控制指令

使用中断函数获取

void USART3_IRQHandler(void)

收到数据后就进行判断,操作相应的前、后、左、右、刹标志位

if(uart_receive==0x5A) Flag_front=0,Flag_back=0,Flag_Left=0,Flag_Right=0;//刹车
else if(uart_receive==0x41)	Flag_front=1,Flag_back=0,Flag_Left=0,Flag_Right=0;//前
else if(uart_receive==0x45)	Flag_front=0,Flag_back=1,Flag_Left=0,Flag_Right=0;//后
else if(uart_receive==0x42||uart_receive==0x43||uart_receive==0x44)																 Flag_front=0,Flag_back=0,Flag_Left=0,Flag_Right=1;  //左
else if(uart_receive==0x46||uart_receive==0x47||uart_receive==0x48)	    																	 Flag_front=0,Flag_back=0,Flag_Left=1,Flag_Right=0;  //右
else Flag_front=0,Flag_back=0,Flag_Left=0,Flag_Right=0;//刹车

设置起始位和停止位

if(Usart3_Receive==0x7B) Flag_PID=1;   //APP参数指令起始位
if(Usart3_Receive==0x7D) Flag_PID=2;   //APP参数指令停止位

数据包

if(Flag_PID==1)  //采集数据{Receive[i]=Usart3_Receive;i++;}if(Flag_PID==2)  //采集结束,开始分析数据{if(Receive[3]==0x50) 				 PID_Send=1;else if(Receive[1]!=0x23) {								for(j=i;j>=4;j--){Data+=(Receive[j-1]-48)*pow(10,i-j);}switch(Receive[1]){case 0x30:  Balance_Kp=Data;break;case 0x31:  Balance_Kd=Data;break;case 0x32:  Velocity_Kp=Data;break;case 0x33:  Velocity_Ki=Data;break;case 0x34:  Turn_Kp=Data;break; case 0x35:  Turn_Kd=Data;break; case 0x36:  break; //预留case 0x37:  break; //预留case 0x38:  break; //预留}}				 Flag_PID=0;i=0;j=0;Data=0;memset(Receive, 0, sizeof(u8)*50);//数组清零} }  											 
} 

8.PID算法

8.1 直立环

角度的微分就是角速度,所以D控制=Kd*(期望角速度-当前角速度)

int Balance(float Med,float Angle,float Gyro)
{  float Angle_bias,Gyro_bias;int balance;Angle_bias=Med-Angle; //求出期望角度与当前角度的偏差Gyro_bias=0-Gyro; balance=-Balance_Kp/100*Angle_bias-Gyro_bias*Balance_Kd/100; //计算平衡控制的电机PWM  PD控制   kp是P系数 kd是D系数 return balance;
}

8.2 速度环

int Velocity(int encoder_left,int encoder_right)
{  static float velocity,Encoder_Least,Encoder_bias,Movement;static float Encoder_Integral,Target_Velocity;//================遥控前进后退部分====================// if(Flag_front==1)    	Movement=Target_Velocity/Flag_velocity;	  //收到前进信号else if(Flag_back==1)	Movement=-Target_Velocity/Flag_velocity;  //收到后退信号else  Movement=0;//================速度PI控制器=====================//Encoder_Least =0-(encoder_left+encoder_right); //获取最新速度偏差=目标速度(此处为零)-测量速度(左右编码器之和)Encoder_bias *= 0.86;	                     //一阶低通滤波器       Encoder_bias += Encoder_Least*0.14;	         //一阶低通滤波器,减缓速度变化 Encoder_Integral +=Encoder_bias;             //积分出位移 积分时间:10msEncoder_Integral=Encoder_Integral+Movement;  //接收遥控器数据,控制前进后退if(Encoder_Integral>10000)    Encoder_Integral=10000;             //积分限幅if(Encoder_Integral<-10000)	  Encoder_Integral=-10000;            //积分限幅	velocity=-Encoder_bias*Velocity_Kp/100-Encoder_Integral*Velocity_Ki/100;     //速度控制	if(Turn_Off(Angle_Balance,Voltage)==1||Flag_Stop==1) Encoder_Integral=0;//电机关闭后清除积分return velocity;
}

8.3 转向环

int Turn(float gyro)
{static float Turn_Target,turn,Turn_Amplitude=54;float Kp=Turn_Kp,Kd;	 //修改转向速度,修改Turn_Amplitude即可//===================遥控左右旋转部分=================//if(1==Flag_Left)	        Turn_Target=-Turn_Amplitude/Flag_velocity;else if(1==Flag_Right)	 Turn_Target=Turn_Amplitude/Flag_velocity; else Turn_Target=0;if(1==Flag_front||1==Flag_back)  Kd=Turn_Kd;        else Kd=0;   //转向的时候取消陀螺仪的纠正 有点模糊PID的思想//===================转向PD控制器=================//turn=Turn_Target*Kp/100+gyro*Kd/100;//结合Z轴陀螺仪进行PD控制return turn;		 //转向环PWM右转为正,左转为负
}

8.4 计算PWM值

机械中值:能使小车真正平衡住的角度。由于小车的安装往往存在一定的误差,导致重心会有一定的偏移,所以让小车最能平衡的角度不一定是0度。所以直立环的期望角度=速度环输出+机械中值

Velocity_Out=Velocity(Encoder_Left,Encoder_Right);  //速度环PI控制	
Balance_Out=Balance(Velocity_Out+Middle_angle,Angle_Balance,Gyro_Balance);            
//直立环PD控制,直立环的期望角度=速度环输出+机械中值
Turn_Out=Turn(Gyro_Turn);	     //转向环PID控制     
PWM_Out=Balance_Out;			 //串级PID最终输出量		
Motor_Left=PWM_Out+Turn_Out;     //计算左轮电机最终PWM
Motor_Right=PWM_Out-Turn_Out;    //计算右轮电机最终PWM
//PWM值正数使小车前进,负数使小车后退

9.电池电压检测

电池电压通过 ADC 进行测量。

9.1 初始化

第一步,开启RCC时钟,包括GPIO和ADC的时钟

第二步,配置GPIO

第三步,配置多路开关

第四步,配置ADC转换器

第五步,开关控制,开启ADC

void AD_Init(void)
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);RCC_ADCCLKConfig(RCC_PCLK2_Div6); //ADCCLK=PCLK2(72MHz)/6=12MHz,ADC最大时间不能超过14MHzGPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; //模拟输入GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);ADC_InitTypeDef ADC_InitStructure;ADC_DeInit(ADC1);  //复位ADC1,将外设 ADC1 的全部寄存器重设为缺省值	ADC_InitStructure.ADC_Mode=ADC_Mode_Independent; //独立模式ADC_InitStructure.ADC_DataAlign=ADC_DataAlign_Right ; //右对齐ADC_InitStructure.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None; //不使用外部触发,即使用内部软件触发ADC_InitStructure.ADC_ContinuousConvMode=DISABLE; //单次转换ADC_InitStructure.ADC_ScanConvMode=DISABLE; //非扫描模式ADC_InitStructure.ADC_NbrOfChannel=1; //顺序进行规则转换的ADC通道的数目ADC_Init(ADC1,&ADC_InitStructure);ADC_Cmd(ADC1,ENABLE); //使能指定的ADC1/*ADC校准*/ADC_ResetCalibration(ADC1); //复位校准,把RSTCAL位置1while (ADC_GetResetCalibrationStatus(ADC1)); //复位校准完成后,RSTCAL位清零,然后跳出该循环;ADC_StartCalibration(ADC1); //启动校准while(ADC_GetCalibrationStatus(ADC1)); //校准完成后,校准标志位清零,跳出该循环
}

9.2 AD采样

uint16_t Get_Adc(uint8_t ADC_Channel)
{ADC_RegularChannelConfig(ADC1,ADC_Channel,1,ADC_SampleTime_239Cycles5);//ADC1,ADC通道,采样时间为239.5周期ADC_SoftwareStartConvCmd(ADC1,ENABLE); //软件触发转换while(ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC)==RESET); //转换完成后,转换结束位会置1return ADC_GetConversionValue(ADC1); //获取转换值
}

9.3 读取电池电压

int Get_battery_volt(void)   
{  int Volt;//电池电压Volt=Get_Adc(Battery_Ch)*3.3*67/4096*10;	//电阻分压,具体根据原理图简单分析可以得到	return Volt;
}

10. main函数

主函数主要做一些初始化工作,完成后进入一个死循环。主要控制功能在外部中断函数里面,5ms 定时中断由 MPU6050 的 INT 引脚触发。我 们把比较耗时的 OLED 显示和 APP 发送数据等函数放在主函数里面,因为 5ms 定时中断由 MPU6050 的 INT 引脚触发,需严格保证采样和数据处理时间的同步。

六、PID调参

1. 确定小车的机械中值

先从后往前绕电机轴轻推小车,直到小车向前倒,记录倒下时的角度。然后从前往后绕电机轴轻推小车,直到小车向后倒,记录倒下时的角度。取两次角度的中间值作为机械中值。

2. 注意事项

在调试【速度环参数极性】时:需要去掉(注释掉) 【直立环运算】

在调试【速度环参数大小】时:再次引入(取消注释) 【直立环运算】

【转向环运算】始终是去掉(注释) 的一个状态。若转向环已提前将参数调试好,则未注释也影响不大。

3. 直立环参数整定

确定Kp极性
极性错误:小车往哪边倒,车轮就往反方向开,会使得小车加速倒下。

极性正确:小车往哪边倒,车轮就往哪边开,以保证小车有直立的趋势。

确定Kp大小

一直增加Kp,直到出现大幅低频震荡。

确定Kd大小

极性错误:拿起小车绕电机轴旋转,车轮反向转动,无跟随。

极性正确:拿起小车绕电机轴旋转,车轮同向转动,有跟随。

确定Kd大小

Kd一直增加,直到出现高频震荡。

直立环调试完毕后,对所有确立的参数乘以0.6作为最终参数。
原因:之前得到的参数都是Kp、Kd 最大值,根据工程经验平衡小车的理想参数为最大参数乘以0.6求得。

结果:乘以0.6后,小车的抖动消失,但同时直立效果也变差。待下面加入速度环就能得到更好的性能。

4. 速度环参数整定

Kp&Ki:

线性关系、Ki=(1/200)*Kp、仅调Kp即可。

Kp&Ki极性:

极性错误:手动转动其中一个车轮,另一车轮会以同样速度反向旋转——典型负反馈。

极性正确:手动转动其中一个车轮,两个车伦会同向加速,直至电机最大速度——典型正反馈。

 Kp&Ki大小:

增加Kp&Ki,直至小车保持平衡的同时,速度接近于零。且回位效果较好。

5. 转向环参数整定

确定Kp极性

极性错误:拿起小车,并将小车绕Z轴旋转,两车轮旋转的趋势与小车旋转趋势一致——典型正反馈。

极性正确:拿起小车,并将小车绕Z轴旋转,两车轮旋转的趋势与小车旋转趋势相反——典型负反馈。

确定Kp大小

加大Kp,直至走直线效果较好,且无剧烈抖动。

 

 

 

 

 

 

这篇关于两轮自平衡小车-资料整理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/278742

相关文章

数论入门整理(updating)

一、gcd lcm 基础中的基础,一般用来处理计算第一步什么的,分数化简之类。 LL gcd(LL a, LL b) { return b ? gcd(b, a % b) : a; } <pre name="code" class="cpp">LL lcm(LL a, LL b){LL c = gcd(a, b);return a / c * b;} 例题:

基于UE5和ROS2的激光雷达+深度RGBD相机小车的仿真指南(五):Blender锥桶建模

前言 本系列教程旨在使用UE5配置一个具备激光雷达+深度摄像机的仿真小车,并使用通过跨平台的方式进行ROS2和UE5仿真的通讯,达到小车自主导航的目的。本教程默认有ROS2导航及其gazebo仿真相关方面基础,Nav2相关的学习教程可以参考本人的其他博客Nav2代价地图实现和原理–Nav2源码解读之CostMap2D(上)-CSDN博客往期教程: 第一期:基于UE5和ROS2的激光雷达+深度RG

rtmp流媒体编程相关整理2013(crtmpserver,rtmpdump,x264,faac)

转自:http://blog.163.com/zhujiatc@126/blog/static/1834638201392335213119/ 相关资料在线版(不定时更新,其实也不会很多,也许一两个月也不会改) http://www.zhujiatc.esy.es/crtmpserver/index.htm 去年在这进行rtmp相关整理,其实内容早有了,只是整理一下看着方

笔记整理—内核!启动!—kernel部分(2)从汇编阶段到start_kernel

kernel起始与ENTRY(stext),和uboot一样,都是从汇编阶段开始的,因为对于kernel而言,还没进行栈的维护,所以无法使用c语言。_HEAD定义了后面代码属于段名为.head .text的段。         内核起始部分代码被解压代码调用,前面关于uboot的文章中有提到过(eg:zImage)。uboot启动是无条件的,只要代码的位置对,上电就工作,kern

JavaScript整理笔记

JavaScript笔记 JavaScriptJavaScript简介快速入门JavaScript用法基础语法注释关键字显示数据输出innerHTML innerText属性返回值的区别调试 数据类型和变量数据类型数字(Number)字符串(String)布尔值(Boolean)null(空值)和undefined(未定义)数组(Array)对象(Object)函数(Function) 变量

关于回调函数和钩子函数基础知识的整理

回调函数:Callback Function 什么是回调函数? 首先做一个形象的比喻:   你有一个任务,但是有一部分你不会做,或者说不愿做,所以我来帮你做这部分,你做你其它的任务工作或者等着我的消息,但是当我完成的时候我要通知你我做好了,你可以用了,我怎么通知你呢?你给我一部手机,让我做完后给你打电话,我就打给你了,你拿到我的成果加到你的工作中,继续完成其它的工作.这就叫回叫,手机

站长常用Shell脚本整理分享(全)

站长常用Shell脚本整理分享 站长常用Shell脚本整理分享1-10 站长常用Shell脚本整理分享11-20 站长常用Shell脚本整理分享21-30 站长常用Shell脚本整理分享31-40 站长常用Shell脚本整理分享41-50 站长常用Shell脚本整理分享51-59 长期更新

基于微信小程序与嵌入式系统的智能小车开发(详细流程)

一、项目概述 本项目旨在开发一款智能小车,结合微信小程序与嵌入式系统,提供实时图像处理与控制功能。用户可以通过微信小程序远程操控小车,并实时接收摄像头采集的图像。该项目解决了传统遥控小车在图像反馈和控制延迟方面的问题,提升了小车的智能化水平,适用于教育、科研和娱乐等多个领域。 二、系统架构 1. 系统架构设计 本项目的系统架构主要分为以下几个部分: 微信小程序:负责用户界面、控制指令的

IEEE会议投稿资料汇总http://cadcg2015.nwpu.edu.cn/index.htm

最近投了篇IEEE的顶级会议文章,一下是比较有用的一些资料,以供参考。 1.会议主页:http://cadcg2015.nwpu.edu.cn/index.htm     (The 14th International Conference on Computer-Aided Design and Computer Graphics (CAD/Graphics 2015)) 2.I

ansible资料

ansible系列教程-强烈推荐看完ansible官方编写的例子ansible_uiJenkins配置ansiblegalaxy官方文档中文教程1中文教程2playbook进阶YAML语法fabric编写的自动化部署