问题总览
1、电机带负载所引起的死区补偿问题
2、利用MPU6050传感器进行某一轴的角度测量问题
3、stm32串口通讯得到的数据用DMA转运问题
一、电机带负载所引起的死区补偿问题
我选用的电机为直流减速电机,型号为GA25-370,是12V的电机。
电机运行分为带负载和不带负载的两种情况:不带负载的电机转速呈现饱和特性,带负载的电机转速呈现有死区的饱和特性。制作平衡车的时候不会让电机满速运行,所以我们可以近似地看做为线性特性和死区特性。平衡车一定是带负载的,那我们如何补偿呢?这里分享我的方法。
直流电机调速方案比较简单,就是用PWM波进行调速。一个最简单粗暴的方法就是直接在PID控制的结果中,加上一个数,这个数就是死区的△值。这样的话测量死区△值也很简单,只需将小车平放在地上,失能PID运算,并逐渐提高PWM值,小车轮胎即将运动的时刻所输出的PWM占空比值即为我们所要的死区△的值,记录并写进程序即可。但上述办法有一个弊端,电机的死区主要来源是轮胎的摩擦力(其他的力比如空气阻力可忽略不计),轮胎的摩擦力并不是一成不变的,也就是说△的值是一个时变的量,这种方法补偿的实际效果是小车有一个低频抖动且无法通过改变PID值进行消除。
第二种方法比较复杂,需要修改直立环的程序逻辑,我们在用第一种方法得到死区△的值后,减去适量的数a进行补偿(此时补偿值为△-a),或者不补偿也可(此时补偿值为0)。之后在直立环的函数里,当角度偏差小的时候(比如±3°),kp选用较大的数,同时结合补偿(△-a或者0)保证小幅偏差可以让小车启动;当角度偏差大的时候,kp选用较小的数,同时结合补偿(△)能保证小车回正即可。这种方法补偿的实际效果可以减轻上一个方法的低频抖动问题,但是小车的抗干扰能力不会太强。
二、利用MPU6050传感器进行某一轴的角度测量问题
我们计算偏角普遍是用两种方法:一种是获得重力加速度在x轴和z轴(根据传感器安装不同,选取的两个轴也会有所不同)上分量,通过反正切函数获得,我们设为angle1;另一种方法是获得y轴角速度,对角速度积分获得角度,我们设为angle2。但这两种方法各有利弊:第一种方法适合在静止的时候使用,第二种方法适合在运动的时候使用。
针对上述两种方法的利弊,我们有两种滤波算法可以使用。
// 第一种算法为一阶滤波算法,在平衡小车上应用就是:k1*angle1 + (1-k1) *angle2,k1为滤波系数,k1∈[0,1]。对于k1的值,我们通常选用0.03,也就是说我们更偏向于angle2所计算的角度,而不是angle1。为什么呢,是因为小车在平衡的过程中,是不断运动着的,angle2精确度比angle1要高,所以我们更置信于angle2所计算的结果。当然,如果你在编程调试并未使用电机时(也就是说只是看程序运行结果还未上实物),这个滤波算法肯定是不准的,因为它只适合运动的时候进行计算。
// 第二种算法为卡尔曼滤波算法,这个算法较为复杂,我这里不展开解释其算法过程,只简单介绍原理。卡尔曼滤波算法其实本质上就是猜,是一种有根据地猜,如果你的小车在几乎静止的状态下,卡尔曼滤波更置信于angle1,你的小车在运动的情况下,卡尔曼滤波更置信于angle2,也就是说,卡尔曼滤波在小车的应用上算是更先进的一阶滤波算法,因为它可以根据实际情况调整k1的值。
根据我的实际应用,这两种算法在平衡小车的上结果几乎一样,分不出哪一个更优劣。不过卡尔曼滤波在只进行数据调试的时候更好用一些。
三、stm32串口通讯得到的数据用DMA转运问题
stm32的DMA外设可以一定程度上减少CPU的运算量,避免中断过多而引起的bug,这里我分享我的USART3的DMA配置(库函数)。
#include "stm32f10x.h" // Device header
char Usart_DRData; //串口数据接收变量
void MyUSART_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStruture;
GPIO_InitStruture.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruture.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStruture.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStruture);
GPIO_InitStruture.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStruture.GPIO_Pin = GPIO_Pin_11;
GPIO_InitStruture.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStruture); //选用USART3的RX、TX接口
USART_InitTypeDef USART_InitStruture;
USART_InitStruture.USART_BaudRate = 9600;
USART_InitStruture.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStruture.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
//或运算可以同时配置RX与TX
USART_InitStruture.USART_Parity = USART_Parity_No;
USART_InitStruture.USART_StopBits = USART_StopBits_1;
USART_InitStruture.USART_WordLength = USART_WordLength_8b;
USART_Init(USART3,&USART_InitStruture);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
DMA_InitTypeDef DMA_InitStruture;
DMA_InitStruture.DMA_PeripheralBaseAddr = (uint32_t)&USART3 -> DR;//外设接收寄存器地址
DMA_InitStruture.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStruture.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStruture.DMA_MemoryBaseAddr = (uint32_t)(&Usart_DRData); //传输目标寄存器地址
DMA_InitStruture.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStruture.DMA_MemoryInc = DMA_MemoryInc_Disable;
DMA_InitStruture.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStruture.DMA_BufferSize = 1;
//转运的数的个数,我这里就转一个数,所以这里的值可以任意写,如果你转运带包头包尾的数,
//这个就不能任意写,而且传输目标寄存器地址就得是数组变量,然后DMA_MemoryInc为Enable
//也就是地址自增,数组的地址都是紧挨着的
DMA_InitStruture.DMA_Mode = DMA_Mode_Circular; //循环模式
DMA_InitStruture.DMA_M2M = DMA_M2M_Disable;
DMA_InitStruture.DMA_Priority = DMA_Priority_Medium;
//优先级,这里就一个DMA通道,可以任意选
DMA_Init(DMA1_Channel3,&DMA_InitStruture); //USART3的RX的DMA通道为DMA1的3通道
//可以通过查找数据手册的DMA1请求映像表得到
DMA_Cmd(DMA1_Channel3,ENABLE);
USART_DMACmd(USART3, USART_DMAReq_Rx, ENABLE); //使能USART3的RX的DMA转运
USART_Cmd(USART3,ENABLE);
}
/* 之后Usart_DRData的值即为串口通讯的值 */