对卡尔曼滤波器的初略了解
3赞(博文来源:http://blog.chinaaet.com/zhaocundang)
写这篇博客只要是记录备战这个暑假7月25号全国电子设计比赛,对自动化控制类项目的了解与认知。这个对非自动化专业的学生要去了解卡尔曼滤波和PID来说,有点难,况且数学就只学了一年就没数学课了。不过我还是耐心的研究了起来,下面就以小白的身份说说卡尔曼滤波器这个东西吧。
什么是卡尔曼滤波器?首先卡尔曼是个人,这个人研究数学很牛逼,就研究出来这个滤波器,所以叫卡尔曼滤波器。
滤波器顾名思义就过滤,就像从沙子里面淘金子那样,把沙子和其他的石子去掉,就要金子。然后从数学角度来看,假如我要测一个角度值,测角度用的传感器是I2C接线的MPU6050(陀螺仪),现在我从陀螺仪里面已经读出一些角度值来了,但是有些角度值会出现漂移,一直增加,范围波动很大,不能稳定下来,所以就用了这个卡尔曼滤波器来过滤一下输出稳定的角度值。
这里使用了MPU6050陀螺仪,采用I2C 2线制,就是 VCC GND SCL SDA这几根线就能驱动陀螺仪。SCL是时钟端,负责时序,SDA是数据端,用来传输数据。
/* 什么是I2C? Inter ingrated cruit 内部集成芯片。
IIC(Inter-Integrated Circuit)总线是一种由PHILIPS公司开发的两线式串行总线,用于连接微控制器及其外围设备。它是由数据线SDA和时钟SCL构成的串行总线,可发送和接收数据。在CPU与被控IC之间、IC与IC之间进行双向传送,高速IIC总线一般可达400kbps以上。
I2C总线在传送数据过程中共有三种类型信号, 它们分别是:开始信号、结束信号和应答信号。
开始信号:SCL为高电平时,SDA由高电平向低电平跳变,开始传送数据。
结束信号:SCL为高电平时,SDA由低电平向高电平跳变,结束传送数据。
应答信号:接收数据的IC在接收到8bit数据后,向发送数据的IC发出特定的低电平脉冲,表示已收到数据。CPU向受控单元发出一个信号后,等待受控单元发出一个应答信号,CPU接收到应答信号后,根据实际情况作出是否继续传递信号的判断。若未收到应答信号,由判断为受控单元出现故障。
*/
IIC驱动陀螺仪,这里简单贴出代码简单一看。
void I2C_Congiguration(void) { GPIO_InitTypeDef GPIO_InitStructure; I2C_InitTypeDef I2C_InitStructure; /* 使能与 I2C1 有关的时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE); /* PB6-I2C1_SCL、PB7-I2C1_SDA*/ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 开漏输出 GPIO_Init(GPIOB, &GPIO_InitStructure); /* I2C 配置 */ I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; I2C_InitStructure.I2C_OwnAddress1 = I2C1_MPU6050; I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; I2C_InitStructure.I2C_ClockSpeed = I2C_Speed; /* 使能 I2C1 */ I2C_Cmd(I2C1, ENABLE); /* I2C1 初始化 */ I2C_Init(I2C1, &I2C_InitStructure); /*允许1字节1应答模式*/ I2C_AcknowledgeConfig(I2C1, ENABLE); } void MPU6050_Init(void) { I2C_Congiguration();//IIC模块初始化 delay_ms(500); I2C_WriteByte(PWR_MGMT_1,0x00); delay_ms(10); I2C_WriteByte(SMPLRT_DIV,0x07); delay_ms(10); I2C_WriteByte(CONFIG,0x06); delay_ms(10); I2C_WriteByte(GYRO_CONFIG,0x18); delay_ms(10); I2C_WriteByte(ACCEL_CONFIG,0x01); delay_ms(1000); } void I2C_WriteByte(uint8_t REG_Address, uint8_t Write_Data) { while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); /* Send STRAT condition */ I2C_GenerateSTART(I2C1, ENABLE); /* Test on EV5 and clear it */ while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); /* Send EEPROM address for write */ I2C_Send7bitAddress(I2C1, I2C1_MPU6050, I2C_Direction_Transmitter); /* Test on EV6 and clear it */ while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); /* Send the EEPROM's internal address to write to */ I2C_SendData(I2C1, REG_Address); /* Test on EV8 and clear it */ while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); /* Send the byte to be written */ I2C_SendData(I2C1, Write_Data); /* Test on EV8 and clear it */ while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); /* Send STOP condition */ I2C_GenerateSTOP(I2C1, ENABLE); I2C_WaitEepromStandbyState(); } uint8_t I2C_ReadByte(uint8_t REG_Address) { uint8_t data_byte; //*((u8 *)0x4001080c) |=0x80; while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); /* Send START condition */ I2C_GenerateSTART(I2C1, ENABLE); //*((u8 *)0x4001080c) &=~0x80; /* Test on EV5 and clear it */ while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); /* Send EEPROM address for write */ I2C_Send7bitAddress(I2C1, I2C1_MPU6050, I2C_Direction_Transmitter); /* Test on EV6 and clear it */ while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); /* Clear EV6 by setting again the PE bit */ I2C_Cmd(I2C1, ENABLE); /* Send the EEPROM's internal address to write to */ I2C_SendData(I2C1, REG_Address); /* Test on EV8 and clear it */ while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); /* Send STRAT condition a second time */ I2C_GenerateSTART(I2C1, ENABLE); /* Test on EV5 and clear it */ while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); /* Send EEPROM address for read */ I2C_Send7bitAddress(I2C1, I2C1_MPU6050, I2C_Direction_Receiver); /* Test on EV6 and clear it */ while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); /* While there is data to be read */ /* Disable Acknowledgement */ I2C_AcknowledgeConfig(I2C1, DISABLE); /* Send STOP Condition */ I2C_GenerateSTOP(I2C1, ENABLE); /* Test on EV7 and clear it */ if(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)) /* Read a byte from the EEPROM */ data_byte = I2C_ReceiveData(I2C1); /* Enable Acknowledgement to be ready for another reception */ I2C_AcknowledgeConfig(I2C1, ENABLE); //I2C_EE_WaitEepromStandbyState(); return data_byte; } void I2C_ReadBuffer(uint8_t* Data_Buffer, uint8_t REG_Address, uint8_t Num_Byte) { //*((u8 *)0x4001080c) |=0x80; while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); /* Send START condition */ I2C_GenerateSTART(I2C1, ENABLE); //*((u8 *)0x4001080c) &=~0x80; /* Test on EV5 and clear it */ while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); /* Send EEPROM address for write */ I2C_Send7bitAddress(I2C1, I2C1_MPU6050, I2C_Direction_Transmitter); /* Test on EV6 and clear it */ while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); /* Clear EV6 by setting again the PE bit */ I2C_Cmd(I2C1, ENABLE); /* Send the EEPROM's internal address to write to */ I2C_SendData(I2C1, REG_Address); /* Test on EV8 and clear it */ while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); /* Send STRAT condition a second time */ I2C_GenerateSTART(I2C1, ENABLE); /* Test on EV5 and clear it */ while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); /* Send EEPROM address for read */ I2C_Send7bitAddress(I2C1, I2C1_MPU6050, I2C_Direction_Receiver); /* Test on EV6 and clear it */ while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); /* While there is data to be read */ while(Num_Byte) { if(Num_Byte == 1) { /* Disable Acknowledgement */ I2C_AcknowledgeConfig(I2C1, DISABLE); /* Send STOP Condition */ I2C_GenerateSTOP(I2C1, ENABLE); } /* Test on EV7 and clear it */ if(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)) { /* Read a byte from the EEPROM */ *Data_Buffer = I2C_ReceiveData(I2C1); /* Point to the next location where the byte read will be saved */ Data_Buffer++; /* Decrement the read bytes counter */ Num_Byte--; } } /* Enable Acknowledgement to be ready for another reception */ I2C_AcknowledgeConfig(I2C1, ENABLE); //I2C_EE_WaitEepromStandbyState(); } void I2C_WaitEepromStandbyState(void) { vu16 SR1_Tmp = 0; do { /* Send START condition */ I2C_GenerateSTART(I2C1, ENABLE); /* Read I2C1 SR1 register */ SR1_Tmp = I2C_ReadRegister(I2C1, I2C_Register_SR1); /* Send EEPROM address for write */ I2C_Send7bitAddress(I2C1, I2C1_MPU6050, I2C_Direction_Transmitter); }while(!(I2C_ReadRegister(I2C1, I2C_Register_SR1) & 0x0002)); /* Clear AF flag */ I2C_ClearFlag(I2C1, I2C_FLAG_AF); /* STOP condition */ I2C_GenerateSTOP(I2C1, ENABLE); }
主要是读写时序:
现在驱动陀螺仪完事了,获取陀螺仪角度。MPU6050陀螺仪有三轴角度和三轴加速度: 横滚角roll, 俯仰角pitch, 航向角yaw,和各个角度的角速度。这里获取了x轴的加速度,换算成弧度再换算成角度,获取了y轴的角速度。
static uint8_t DataBuffer[2]; //数据缓存 /****************************加速度****************************************/ I2C_ReadBuffer(DataBuffer, ACCEL_XOUT_H, 2); Accel_x = (short)((DataBuffer[0]<<8)+DataBuffer[1]);//读取X轴加速度 Angle_ax = (Accel_x +220) /16384; //去除零点偏移,计算得到角度(弧度) Angle_ax = Angle_ax*1.2*180/3.14; //弧度转换为度 /****************************角速度****************************************/ I2C_ReadBuffer(DataBuffer, GYRO_YOUT_H, 2); Gyro_y = (short)((DataBuffer[0]<<8)+DataBuffer[1]); //静止时角速度Y轴输出为-18左右 Gyro_y = (Gyro_y + 18)/16.4; //去除零点偏移,计算角速度值 //Angle_gy = Angle_gy + Gyro_y*0.01; //角速度积分得到倾斜角度,因为卡尔曼计算带有时间dt,所以此处不用积分 /***************************卡尔曼融合*************************************/ Kalman_Filter(Angle_ax,Gyro_y); //卡尔曼滤波计算倾角
卡尔曼滤波器,主要包含5个公式,数学不好,看不太懂,就知道5个公式。(真的是数学问题)可以看看:百度百科卡尔曼滤波器
5个公式是这样的:
/*
X(k|k-1)=A X(k-1|k-1)+B U(k) ……….. (1)
式(1)中,X(k|k-1)是利用上一状态预测的结果,X(k-1|k-1)是上一状态最优的结果,U(k)为现在状态的控制量,如果没有控制量,它可以为0。
到现在为止,我们的系统结果已经更新了,可是,对应于X(k|k-1)的协方差还没更新。我们用P表示协方差(covariance):
P(k|k-1)=A P(k-1|k-1) A’+Q ……… (2)
式(2)中,P(k|k-1)是X(k|k-1)对应的协方差,P(k-1|k-1)是X(k-1|k-1)对应的协方差,A’表示A的转置矩阵,Q是系统过程的协方差。
现在我们有了现在状态的预测结果,然后我们再收集现在状态的测量值。结合预测值和测量值,我们可以得到现在状态(k)的最优化估算值X(k|k):
X(k|k)= X(k|k-1)+Kg(k) (Z(k)-H X(k|k-1)) ……… (3)
其中Kg为卡尔曼增益(Kalman Gain):
Kg(k)= P(k|k-1) H’ / (H P(k|k-1) H’ + R) ……… (4)
到现在为止,我们已经得到了k状态下最优的估算值X(k|k)。但是为了要令卡尔曼滤波器不断的运行下去直到系统过程结束,我们还要更新k状态下X(k|k)的协方差:
P(k|k)=(I-Kg(k) H)P(k|k-1) ……… (5)
*/
卡尔曼滤波器代码:将陀螺仪的x轴角度和y轴角度传参进入卡尔曼滤波器输出稳定的x轴角度和y轴角度。
void Kalman_Filter(float Accel,float Gyro) { Angle+=(Gyro - Q_bias) * dt; //先验估计 Pdot[0]=Q_angle - PP[0][1] - PP[1][0]; // Pk-先验估计误差协方差的微分 Pdot[1]= -PP[1][1]; Pdot[2]= -PP[1][1]; Pdot[3]=Q_gyro; PP[0][0] += Pdot[0] * dt; // Pk-先验估计误差协方差微分的积分 PP[0][1] += Pdot[1] * dt; // =先验估计误差协方差 PP[1][0] += Pdot[2] * dt; PP[1][1] += Pdot[3] * dt; Angle_err = Accel - Angle; //zk-先验估计 PCt_0 = C_0 * PP[0][0]; PCt_1 = C_0 * PP[1][0]; E = R_angle + C_0 * PCt_0; K_0 = PCt_0 / E; K_1 = PCt_1 / E; t_0 = PCt_0; t_1 = C_0 * PP[0][1]; PP[0][0] -= K_0 * t_0; //后验估计误差协方差 PP[0][1] -= K_0 * t_1; PP[1][0] -= K_1 * t_0; PP[1][1] -= K_1 * t_1; Angle += K_0 * Angle_err; //后验估计 Q_bias += K_1 * Angle_err; //后验估计 Gyro_y = Gyro - Q_bias; //输出值(后验估计)的微分=角速度 }
加粗的Angle 和 Gyro_y 是最终输出的稳定的x轴角度 和 y轴角度,这里Z轴角度没用,Z轴是垂直上下,没有用到,所以没有从IIC陀螺仪那里输出。
然后输出的角度就可以用了,角度值也比较稳定。