【开源飞控】匿名飞控TI版解析(1)
准备电赛的飞控题,买来了匿名的飞控学习一下,这里整理了一下匿名飞控中比较关键的几部分,学习了一下原理,然后代码解读都写注释里了,篇幅较长。### 一、遥控器信号接收
匿名的飞控提供了ppm和sbus两种遥控器信号的接收,在这里我只使用了ppm,从程序里看应该是最多可以接收7个通道。
#### 1.代码解读
##### (1)ppm/sbus初始化
首先是在 Drv_BspInit() 中调用了 Remote_Control_Init() ,
```
void Remote_Control_Init()
{
//
RC_IN_MODE = Ano_Parame.set.pwmInMode;///在参数初始化设置中默认设置为ppm模式
//
if(RC_IN_MODE == SBUS)
{
Drv_SbusInit();
}
else
{
Drv_PpmInit();
// PWM_IN_Init(RC_IN_MODE);
}
}
```
在 Drv_PpmInit() 中初始化输入捕获,开启中断,这里是配置成向上计时模式,即捕获到上升沿后,进入中断,并以80M的频率计数,计数区间为0~0xffffff。
```
void Drv_PpmInit(void)
{
ROM_SysCtlPeripheralEnable(PPM_SYSCTL);
ROM_SysCtlPeripheralEnable(SYSCTL_PERIPH_WTIMER1);
/*GPIOC配置为定时器捕获模式*/
ROM_GPIOPinTypeTimer(PPM_PORTS, PPM_PIN);
ROM_GPIOPinConfigure(PPM_FUNCTION);
/*配置定时器5B为捕获上升沿*/
ROM_TimerConfigure( WTIMER1_BASE ,TIMER_CFG_SPLIT_PAIR | TIMER_CFG_B_CAP_TIME_UP );
ROM_TimerControlEvent(WTIMER1_BASE,TIMER_B,TIMER_EVENT_POS_EDGE);
ROM_TimerLoadSet( WTIMER1_BASE , TIMER_B , 0xffff );
ROM_TimerPrescaleSet( WTIMER1_BASE , TIMER_B , 0xff );
///这里是配置成向上计时模式,即捕获到上升沿后,进入中断,并以80M的频率计数,计数区间为0~0xffffff
/*开启定时器中断*/
TimerIntRegister(WTIMER1_BASE,TIMER_B , PPM_Decode);
ROM_IntPrioritySet( INT_WTIMER1B , USER_INT6);
ROM_TimerIntEnable( WTIMER1_BASE , TIMER_CAPB_EVENT);
ROM_TimerEnable( WTIMER1_BASE, TIMER_B );
ROM_IntEnable( INT_WTIMER1B );
}
```
##### (2)中断中进行数据解析
当PPM输入引脚捕获到高电平脉冲上升沿时触发中断, PPM_Decode(void) 为中断服务函数,调用PPM_Cal(PulseHigh) 计算出当前脉冲宽度。脉冲宽度在1000us-2000us之间,一帧数据结束标志位脉冲宽度大于5000us,即留出了5ms作为空闲判断的标志。
```
static void PPM_Cal(uint32_tPulseHigh)
{
static uint8_t Chan = 0;
/*脉宽高于一定值说明一帧数据已经结束*/
if(PulseHigh > 5000)
{
/*一帧数据解析完成*/
Chan = 0;
}
else
{
/*脉冲高度正常*/
if (PulseHigh > PULSE_MIN && PulseHigh < PULSE_MAX)
{
if(Chan < 16)
{
ch_watch_dog_feed(Chan);
RC_PPM.Captures = PulseHigh;
}
}
}
}
static void PPM_Decode(void)
{
static uint32_t PeriodVal1,PeriodVal2 = 0;
static uint32_t PulseHigh;
/*清除中断标志*/
ROM_TimerIntClear( WTIMER1_BASE , TIMER_CAPB_EVENT );
/*获取捕获值*/
PeriodVal1 = ROM_TimerValueGet( WTIMER1_BASE , TIMER_B );
if( PeriodVal1 > PeriodVal2 )
PulseHigh =(PeriodVal1 - PeriodVal2) /80;
else
PulseHigh =(PeriodVal1- PeriodVal2 + 0xffffff)/80;
PeriodVal2 = PeriodVal1;
PPM_Cal(PulseHigh);
}
```
##### (3)遥控器任务
读出脉冲宽度后,调用下面的函数,先把脉冲宽度信息处理成合适的杆量信息。然后检测遥感状态,是否触发解锁、传感器校准等特殊动作。
```
void RC_duty_task(u8 dT_ms) //建议2ms调用一次
{
if(flag.start_ok)
{
/获得通道数据
// if(RC_IN_MODE == PWM)
// {
// for(u8 i=0;i<CH_NUM;i++)
// {
// if(chn_en_bit & (1<<i))//(Rc_Pwm_In!=0)//该通道有值,==0说明该通道未插线(PWM)
// {
// CH_N = 1.25f *((s16)Rc_Pwm_In - 1500); //1100 -- 1900us,处理成大约+-500摇杆量
// }
// else
// {
// CH_N = 0;
// }
// CH_N = LIMIT(CH_N,-500,500);//限制到+—500
// }
// }
// else if(RC_IN_MODE == PPM)
if(RC_IN_MODE == PPM || RC_IN_MODE == PWM)
{
for(u8 i=0;i<CH_NUM;i++)
{
if(chn_en_bit & (1<<i))//(Rc_Ppm_In!=0)//该通道有值
{
CH_N = ((s16)RC_PPM.Captures - 1500); //1000 -- 2000us,处理成大约+-500摇杆量
}
else
{
CH_N = 0;
}
CH_N = LIMIT(CH_N,-500,500);//限制到+—500
}
}
else//sbus
{
for(u8 i=0;i<CH_NUM;i++)
{
if(chn_en_bit & (1<<i))//该通道有值
{
CH_N = 0.65f *((s16)Rc_Sbus_In - 1024); //248 --1024 --1800,处理成大约+-500摇杆量
}
else
{
CH_N = 0;
}
CH_N = LIMIT(CH_N,-500,500);//限制到+—500
}
}
///
//解锁监测
unlock(dT_ms);
//摇杆触发功能监测
stick_function(dT_ms);
//通道看门狗
ch_watch_dog(dT_ms);
//失控保护检查
fail_safe_check(dT_ms);//3ms
}
}
```
#### 2.ppm原理
ppm的原理其实比较简单,下面图基本可以说清楚,
其实就是是把多路PWM波压缩成一路ppm信号,通常20ms为一个周期,用一系列高电平脉冲之间的间隔时间表示每一路PWM波的脉宽,各通道的高电平信号是一个挨着一个的,最大的时间是2ms,而不是每个通道分配2ms的时间,但实际可能出现不是紧挨着的,但是对接码没影响。
这部分相关内容也可以参考:
- https://blog.csdn.net/wxc971231/article/details/95983705
- https://blog.csdn.net/nicekwell/article/details/53866204
#### 二、传感器数据读取
匿名使用的icm20602、ak8975、spl06都是SPI通信,通过cs脚进行片选,占用资源少且速度快。另外匿名飞控上还有一点spi flash,不过看了一下代码感觉是没有用上。对于用户自己加传感器,匿名预留了5个串口,其中1是gps,2是数传,3空的,4是光流,5是空的。
#### 1.SPI代码和原理
##### (1)以icm20602读取为例
我们平时使用的spi在TM4C123GH6PM中即使ssi外设,该芯片中包括四个同步串行接口(SSI)模块,是ti公司定义的一种高速、全双工、同步串行接口协议,兼容spi。首先是spi初始化,初始化clk、mosi、miso三个脚,读写函数。
```
void Drv_Spi0Init(void)
{
ROM_SysCtlPeripheralEnable( SYSCTL_PERIPH_SSI0 );
ROM_SysCtlPeripheralEnable(SPI0_SYSCTL);
/*配置IO口*/
ROM_GPIOPinTypeSSI(SPI0_PROT,SPI0_CLK_PIN|SPI0_RX_PIN|SPI0_TX_PIN);
ROM_GPIOPinConfigure(SPI0_CLK);
ROM_GPIOPinConfigure(SPI0_RX);
ROM_GPIOPinConfigure(SPI0_TX);
/* SSI配置 模式3(Polarity = 1 Phase = 1) 主设备模式 速率1MHz 数据长度8位*/
ROM_SSIConfigSetExpClk(SPI0_BASE, ROM_SysCtlClockGet(), SSI_FRF_MOTO_MODE_3,SSI_MODE_MASTER, 10000000,8);
/*开启SSI0*/
ROM_SSIEnable(SPI0_BASE);
}
/* SPI读写函数 */
uint8_t Drv_Spi0SingleWirteAndRead(uint8_t SendData)
{
uint32_t ui_TempData;
uint8_t uc_ReceiveData;
/* 向SSI FIFO写入数据 */
ROM_SSIDataPut(SPI0_BASE, SendData);
/* 等待SSI不忙 */
while(ROM_SSIBusy(SPI0_BASE));
/* 从FIFO读取数据 */
ROM_SSIDataGet(SPI0_BASE, &ui_TempData);
/* 截取数据的低八位 */
uc_ReceiveData = ui_TempData & 0xff;
return uc_ReceiveData;
}
void Drv_Spi0Transmit(uint8_t *ucp_Data, uint16_t us_Size)
{
uint16_t i = 0;
/* 连续写入数据 */
for(i = 0; i < us_Size; i++)
{
Drv_Spi0SingleWirteAndRead(ucp_Data);
}
}
void Drv_Spi0Receive(uint8_t *ucp_Data, uint16_t us_Size)
{
uint16_t i = 0;
/* 连续读取数据 */
for(i = 0; i < us_Size; i++)
{
ucp_Data = Drv_Spi0SingleWirteAndRead(0xFF);
}
}
icm20602的cs脚初始化,并通过拉高拉低进行片选,
void Drv_Icm20602CSPinInit(void)
{
ROM_SysCtlPeripheralEnable(ICM_CSPIN_SYSCTL);
ROM_GPIOPinTypeGPIOOutput(ICM20602_CS_PORT,ICM20602_CS_PIN);
ROM_GPIOPinWrite(ICM20602_CS_PORT, ICM20602_CS_PIN,ICM20602_CS_PIN);///先拉高
}
static void icm20602_enable(u8 ena)
{
if(ena)
ROM_GPIOPinWrite(ICM20602_CS_PORT, ICM20602_CS_PIN,0);//拉低
else
ROM_GPIOPinWrite(ICM20602_CS_PORT, ICM20602_CS_PIN,ICM20602_CS_PIN);
}
static void icm20602_readbuf(u8 reg, u8 length, u8 *data)
{
icm20602_enable(1);
Drv_Spi0SingleWirteAndRead(reg|0x80);
Drv_Spi0Receive(data,length);
icm20602_enable(0);
}
static u8 icm20602_writebyte(u8 reg, u8 data)
{
u8 status;
icm20602_enable(1);
status = Drv_Spi0SingleWirteAndRead(reg);
Drv_Spi0SingleWirteAndRead(data);
icm20602_enable(0);
return status;
}
```
##### (2)spi原理
SPI, Serial Perripheral Interface, 串行外围设备接口, 是 Motorola 公司推出的一种同步串行接口技术. SPI 总线在物理上是通过接在外围设备微控制器(PICmicro) 上面的微处理控制单元 (MCU) 上叫作同步串行端口(Synchronous Serial Port) 的模块(Module)来实现的, 它允许 MCU 以全双工的同步串行方式, 与各种外围设备进行高速数据通信.
**①四线**
- SCK, Serial Clock, 主要的作用是 Master 设备往 Slave 设备传输时钟信号, 控制数据交换的时机以及速率;
- SS/CS, Slave Select/Chip Select, 用于 Master 设备片选 Slave 设备, 使被选中的 Slave 设备能够被 Master 设备所访问;
- SDO/MOSI, Serial Data Output/Master Out Slave In, 在 Master 上面也被称为 Tx-Channel, 作为数据的出口, 主要用于 SPI 设备发送数据;
- SDI/MISO, Serial Data Input/Master In Slave Out, 在 Master 上面也被称为 Rx-Channel, 作为数据的入口, 主要用于SPI 设备接收数据;
**②时序**
- CPOL=0,CPHA=0:此时空闲态时,SCLK处于低电平,数据采样是在第1个边沿,也就是SCLK由低电平到高电平的跳变,所以数据采样是在上升沿,数据发送是在下降沿。
- CPOL=0,CPHA=1:此时空闲态时,SCLK处于低电平,数据发送是在第2个边沿,也就是 SCLK由低电平到高电平的跳变,所以数据采样是在下降沿,数据发送是在上升沿。
- CPOL=1,CPHA=0:此时空闲态时,SCLK处于高电平,数据采集是在第1个边沿,也就是 SCLK由高电平到低电平的跳变,所以数据采集是在下降沿,数据发送是在上升沿。
- CPOL=1,CPHA=1:此时空闲态时,SCLK处于高电平,数据发送是在第2个边沿,也就是 SCLK由高电平到低电平的跳变,所以数据采集是在上升沿,数据发送是在下降沿。
**③内部机制**
主要参考:http://bbs.chinaunix.net/thread-1916003-1-1.html
#### 2.UART代码及原理
##### (1)以自己加的omv读取为例
用的底板上空闲的串口5,读取openmv的数据,首先是在初始化程序中调用 Drv_Uart5Init(115200) ,设置好波特率。在这里我用omv发送到数据是一帧12字节,用fifo6/8刚好接收一帧进一次中断。使用fifo就可以减少进入中断的次数,防止因频繁进入中断影响mcu效率。
```
void Drv_Uart5Init(uint32_t baudrate)
{
ROM_SysCtlPeripheralEnable(SYSCTL_PERIPH_UART2);
ROM_SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOD);
/*PD7解锁操作*/
HWREG(UART2_PORT + GPIO_O_LOCK) = GPIO_LOCK_KEY;
HWREG(UART2_PORT + GPIO_O_CR) = UART2_PIN_TX;
HWREG(UART2_PORT + GPIO_O_LOCK) = 0x00;
/*GPIO的UART模式配置*/
ROM_GPIOPinConfigure(UART2_RX);
ROM_GPIOPinConfigure(UART2_TX);
ROM_GPIOPinTypeUART(UART2_PORT, UART2_PIN_TX | UART2_PIN_RX);
/*配置串口号波特率和时钟源*/
ROM_UARTConfigSetExpClk(UART2_BASE, ROM_SysCtlClockGet(), baudrate,(UART_CONFIG_WLEN_8 | UART_CONFIG_STOP_ONE | UART_CONFIG_PAR_NONE));
///8个数据位、一个停止位、无校验位
/*FIFO设置*/
ROM_UARTFIFOLevelSet(UART2_BASE,UART_FIFO_TX6_8,UART_FIFO_RX6_8);///设置为6/8满时触发中断
ROM_UARTFIFOEnable(UART2_BASE);
/*使能串口*/
ROM_UARTEnable( UART2_BASE );
/*使能UART0接收中断*/
UARTIntRegister(UART2_BASE,UART5_IRQHandler);
ROM_IntPrioritySet(INT_UART2, USER_INT2);
ROM_UARTTxIntModeSet(UART2_BASE,UART_TXINT_MODE_EOT);
ROM_UARTIntEnable(UART2_BASE,UART_INT_RX | UART_INT_RT | UART_INT_TX);///使能收、发、接收超时中断
}
在中断中循环读出fifo中的12个字节,
void UART5_IRQHandler(void)
{
uint8_t com_data;
/*获取中断标志 原始中断状态 不屏蔽中断标志*/
uint32_t flag = ROM_UARTIntStatus(UART2_BASE,1);
/*清除中断标志*/
ROM_UARTIntClear(UART2_BASE,flag);
/*判断FIFO是否还有数据*/
while(ROM_UARTCharsAvail(UART2_BASE))
{
com_data=ROM_UARTCharGet(UART2_BASE);///读一个字节
Drv_omvGetOneByte(com_data);
}
if(flag & UART_INT_TX)
{
Drv_Uart5TxCheck();
}
}
```
然后就是一个字节一个字节读取,这里写了一个简单的协议,防止数据发送出现问题。
```
void Drv_omvGetOneByte(u8 data)
{
if (omv_get_cnt == 0)
{
if (data == 0x68) //帧头
{
omv_data_buff = data;
omv_get_cnt = 1;
}
}
else if (omv_get_cnt == 1)
{
if (data == 0x69) //id
{
omv_data_buff = data;
omv_get_cnt = 2;
}
else
{
omv_get_cnt = 0;
}
}
else
{
omv_data_buff = data;
omv_get_cnt++;
if (omv_get_cnt >= 12)
{
omv_get_cnt = 0;
omv_data_analysis(); ///数据解析
if (omv_data_check(omv_data_buff)) ///数据校验
{
omv_data_out();
}
}
}
}
```
##### (2)uart通信原理
**①字符帧相关**
- 起始位:先发出一个逻辑”0”的信号,表示传输字符的开始。
- 数据位:紧接着起始位之后。资料位的个数可以是4、5、6、7、8等,构成一个字符。通常采用ASCII码。从最低位开始传送,靠时钟定位。
- 奇偶校验位:资料位加上这一位后,使得“1”的位数应为偶数(偶校验)或奇数(奇校验),以此来校验资料传送的正确性。
- 停止位:它是一个字符数据的结束标志。可以是1位、1.5位、2位的高电平。 由于数据是在传输线上定时的,并且每一个设备有其自己的时钟,很可能在通信中两台设备间出现了小小的不同步。因此停止位不仅仅是表示传输的结束,并且提供 计算机校正时钟同步的机会。适用于停止位的位数越多,不同时钟同步的容忍程度越大,但是数据传输率同时也越慢。
- 空闲位:处于逻辑“1”状态,表示当前线路上没有资料传送。
- 波特率:是衡量资料传送速率的指标。表示每秒钟传送的二进制位数。
**②通信过程**
*收发寄存器:*
- 输出缓冲寄存器,它接收CPU从数据总线上送来的并行数据,并加以保存。
- 输出移位寄存器,它接收从输出缓冲器送来的并行数据,以发送时钟的速率把数据逐位移出,即将并行数据转换为串行。
- 输入移位寄存器,它以接收时钟的速率把出现在串行数据输入线上的数据逐位移入,当数据装满后,并行送往输入缓冲寄存器,即将串行数据转换成并行数据。
- 输入缓冲寄存器,它从输入移位寄存器中接收并行数据,然后由CPU取走。
发送时,数据被写入发送FIFO。如果UART 被使能,则会按照预先设置好的参数(波特率、数据位、停止位、校验位等)开始发送数据,一直到发送FIFO 中没有数据。一旦向发送FIFO 写数据(如果FIFO 未空),UART 的忙标志位BUSY 就有效,并且在发送数据期间一直保持有效。BUSY 位仅在发送FIFO 为空,且已从移位寄存器发送最后一个字符,包括停止位时才变无效。即 UART 不再使能,它也可以指示忙状态。
在UART 接收器空闲时,如果数据输入变成“低电平”,即接收到了起始位,则接收计数器开始运行,并且数据在Baud16 的第8 个周期被采样。如果Rx 在Baud16 的第8 周期仍然为低电平,则起始位有效,否则会被认为是错误的起始位并将其忽略。如果起始位有效,则根据数据字符被编程的长度,在 Baud16 的每第 16 个周期对连续的数据位(即一个位周期之后)进行采样。如果奇偶校验模式使能,则还会检测奇偶校验位。
最后,如果Rx 为高电平,则有效的停止位被确认,否则发生帧错误。当接收到一个完整的字符时,将数据存放在接收FIFO 中。
### 三、加速度计、陀螺仪数据处理
直接从传感器中读出的数据和可用的数据还是有区别的,主要体现在:不在中心位置、旋转加速度对加速度的影响、校准值等,所以要对原始数据进行一系列处理。下面这个函数是放在1ms中断任务里面进行的,主要就是对六轴原始数据的处理。
```
void Sensor_Data_Prepare(u8 dT_ms)
{
float hz = 0 ;
if(dT_ms != 0) hz = 1000/dT_ms;
// MPU6050_Read();
// sensor_rotate_func(dT);
/*静止检测*/
motionless_check(dT_ms);
MPU6050_Data_Offset(); //校准函数///开机首先对陀螺仪进行自动校准,但是对加速度计不校准,加速度可用上位机或者摇杆指令校准
/*得出校准后的数据*/
for(u8 i=0;i<3;i++)
{
sensor_val = sensor.Acc_Original ;
sensor_val = sensor.Gyro_Original - save.gyro_offset ;
//sensor_val = (sensor_val >>2) <<2;
}
/*可将整个传感器坐标进行旋转*/
// for(u8 j=0;j<3;j++)
// {
// float t = 0;
//
// for(u8 i=0;i<3;i++)
// {
//
// t += sensor_val *wh_matrix;
// }
//
// sensor_val_rot = t;
// }
// for(u8 j=0;j<3;j++)
// {
// float t = 0;
//
// for(u8 i=0;i<3;i++)
// {
//
// t += sensor_val *wh_matrix;
// }
//
// sensor_val_rot = t;
// }
/*赋值*/
for(u8 i = 0;i<6;i++)
{
sensor_val_rot = sensor_val;
}
/*数据坐标转90度*/ ///这里的旋转是因为icm20602的安装导致和机身坐标系不一样,绕机身z轴转90度即可
sensor_val_ref =sensor_val_rot ;
sensor_val_ref = -sensor_val_rot ;
sensor_val_ref =sensor_val_rot;
sensor_val_ref =(sensor_val_rot - save.acc_offset ) ;
sensor_val_ref = -(sensor_val_rot - save.acc_offset ) ;
sensor_val_ref =(sensor_val_rot - save.acc_offset ) ;
/*单独校准z轴模长*/
mpu_auto_az();
//======================================================================
/*软件低通滤波*/
for(u8 i=0;i<3;i++)
{
//
gyr_f = (sensor_val_ref );
acc_f = (sensor_val_ref );
//
for(u8 j=3;j>0;j--)
{
//
gyr_f += GYR_ACC_FILTER *(gyr_f - gyr_f);
acc_f += GYR_ACC_FILTER *(acc_f - acc_f);
}
// LPF_1_(100,dT_ms*1e-3f,sensor_val_ref,sensor.Gyro);
// LPF_1_(100,dT_ms*1e-3f,sensor_val_ref,sensor.Acc);
}
/*旋转加速度补偿*/
//======================================================================
for(u8 i=0;i<3;i++)///这部分是,如果icm安装的偏心,则测量的加速度会受到旋转的角加速度的影响,需要补偿。如果安装不对正,则要先去设置center_pos
{
center_pos.gyro_rad_old = center_pos.gyro_rad;
center_pos.gyro_rad =gyr_f *RANGE_PN2000_TO_RAD;//0.001065f;
center_pos.gyro_rad_acc = hz *(center_pos.gyro_rad - center_pos.gyro_rad_old);
}
center_pos.linear_acc = +center_pos.gyro_rad_acc *center_pos.center_pos_cm - center_pos.gyro_rad_acc *center_pos.center_pos_cm;
center_pos.linear_acc = -center_pos.gyro_rad_acc *center_pos.center_pos_cm + center_pos.gyro_rad_acc *center_pos.center_pos_cm;
center_pos.linear_acc = +center_pos.gyro_rad_acc *center_pos.center_pos_cm - center_pos.gyro_rad_acc *center_pos.center_pos_cm;
//======================================================================
/*赋值*/
for(u8 i=0;i<3;i++)
{
sensor.Gyro = gyr_f;//sensor_val_ref;
sensor.Acc = acc_f - center_pos.linear_acc/RANGE_PN16G_TO_CMSS;//sensor_val_ref;//
}
/*转换单位*/
for(u8 i =0 ;i<3;i++)
{
/*陀螺仪转换到度每秒,量程+-2000度*/
sensor.Gyro_deg = sensor.Gyro *0.061036f ;///65535 * 4000; +-2000度 0.061
/*陀螺仪转换到弧度度每秒,量程+-2000度*/
sensor.Gyro_rad = sensor.Gyro_deg *0.01745f;//sensor.Gyro *RANGE_PN2000_TO_RAD ;//0.001065264436f //微调值 0.0010652f
/*加速度计转换到厘米每平方秒,量程+-8G*/
sensor.Acc_cmss = (sensor.Acc *RANGE_PN16G_TO_CMSS );// /65535 * 16*981; +-8G
}
}
```
### 四、姿态解算
姿态解算可用说是飞控中比较核心的部分,即将测得的陀螺仪、加速度计、磁力计的数据进行融合解算,方法有很多,匿名这里其实采用的是比较基础的一种方法,是一种融合磁力计的互补滤波。在1ms任务中调用 IMU_Update_Task(1) 如下,
```
void IMU_Update_Task(u8 dT_ms)
{
/*如果准备飞行,复位重力复位标记和磁力计复位标记*/
if(flag.unlock_sta )
{
imu_state.G_reset = imu_state.M_reset = 0;///即重力和磁力正常修正
reset_imu_f = 0;
}
else
{
if(flag.motionless == 0)
{
// imu_state.G_reset = 1;//自动复位
//sensor.gyr_CALIBRATE = 2;
}
if(reset_imu_f==0 )//&& flag.motionless == 1)
{
imu_state.G_reset = 1;//自动复位
sensor.gyr_CALIBRATE = 2;//校准陀螺仪,不保存
reset_imu_f = 1; //已经置位复位标记
}
}
if(0)
{
imu_state.gkp = 0.0f;
imu_state.gki = 0.0f;
}
else
{
if(0)
{
imu_state.gkp = 0.2f;
}
else
{
/*设置重力互补融合修正kp系数*/
imu_state.gkp = 0.2f;//0.4f;
}
/*设置重力互补融合修正ki系数*/
imu_state.gki = 0.01f;
/*设置罗盘互补融合修正ki系数*/ 这里应该是kp系数
imu_state.mkp = 0.1f;
}
imu_state.M_fix_en = sens_hd_check.mag_ok; //磁力计修正使能 ///这里始终是使能的
/*姿态计算,更新,融合*/ ///核心部分
IMU_update(dT_ms *1e-3f, &imu_state,sensor.Gyro_rad, sensor.Acc_cmss, mag.val,&imu_data);//x3_dT_1 * 0.000001f
//
}
```
在一些修正标志使能、并设置修正系数之后,调用imu更新的函数。这一部分内容比较多,只来看几个关键部分,其他理论内容可以参考我之前的文章:https://blog.csdn.net/GWH_98/article/details/86811091
至于互补滤波修正,我想再总结一下。之前也没写过有关用磁力计修正的内容,这里也补充一下。加速计和磁力计都是用来修正陀螺仪的,其实在姿态解算中,陀螺仪积分置信度通常是最高的。加速计可以修正xoy平面,而磁力计可以修正绕z轴的转动,这样就可以对三个轴都进行修正。
加速计修正时,是将世界系重力分量成旋转矩阵到机体系然后与加计测得的(注意,这里是没有横向加速度时测得,即只有重力加速度分量,这是惯导的要求。但一般是有其他加速度的,所以总还是有误差的。)计算叉积,得到偏差。这里要搞清楚:**世界系标准量**是,旋转矩阵是需要修正的,**存在误差量**是,机体系标准量是。
磁力计修正时,也是同样的思路。如果让机头x轴对准正北,则**世界系标准量**应该是这样一个值。但是这个值我们无法获得,不想加计直接是。所以要从**机体系标准值**推,即乘旋转矩阵得到,此时这个值和我们的**世界系标准量**存在一个关系,即hx2+hy2=bx2,这是由我们已经通过加计校准过xoy平面决定的,即此时只存在绕z轴误差。这样我们得到**世界系标准量**就和加速度计原理一样修正就好了。
匿名中加计的修正部分和mahony算法大致相同,但磁力计部分做了一些简化。
```
//水平面方向向量 ///这里是计算水平面内航向坐标系下的单位向量,实际上就是偏航角的cos和sin
float hx_vec_reci = my_sqrt_reciprocal(my_pow(att_matrix) + my_pow(att_matrix));
imu->hx_vec = att_matrix *hx_vec_reci;
imu->hx_vec = att_matrix *hx_vec_reci;
// 计算载体坐标下的运动加速度。(与姿态解算无关)
for(u8 i = 0;i<3;i++)
{
imu->a_acc = (s32)(acc - 981 *imu->z_vec);
}
//计算世界坐标下的运动加速度。坐标系为北西天
for(u8 i = 0;i<3;i++)
{
s32 temp = 0;
for(u8 j = 0;j<3;j++)
{
temp += imu->a_acc *att_matrix;
}
imu->w_acc = temp;
}
///这里通过上面计算出的单位向量进行绕z轴转动的坐标变化,即将世界系转换到航向系,因为按照我们的操作习惯都是有头模式的遥控。
w2h_2d_trans(imu->w_acc,imu_data.hx_vec,imu->h_acc);
```
```
//误差积分 ///pi修正中I项
vec_err_i +=LIMIT(vec_err,-0.1f,0.1f) *dT *ki_use;
// 构造增量旋转(含融合纠正)。
// d_angle = (gyr + (vec_err+ vec_err_i) * kp_use - mag_yaw_err *imu->z_vec *kmp_use *RAD_PER_DEG) * dT / 2 ;
// d_angle = (gyr + (vec_err+ vec_err_i) * kp_use - mag_yaw_err *imu->z_vec *kmp_use *RAD_PER_DEG) * dT / 2 ;
// d_angle = (gyr + (vec_err+ vec_err_i) * kp_use - mag_yaw_err *imu->z_vec *kmp_use *RAD_PER_DEG) * dT / 2 ;
#ifdef USE_MAG///这里感觉写的有点问题,I的修正项应该不需要再乘以kp了
d_angle = (gyr + (vec_err+ vec_err_i) * kp_use + mag_yaw_err *imu->z_vec *mkp_use) * dT / 2 ;
#else
d_angle = (gyr + (vec_err+ vec_err_i) * kp_use ) * dT / 2 ;
#endif
}
// 计算姿态。///龙格库塔更新四元数
imu->w = imu->w - imu->x*d_angle - imu->y*d_angle - imu->z*d_angle;
imu->x = imu->w*d_angle + imu->x + imu->y*d_angle - imu->z*d_angle;
imu->y = imu->w*d_angle - imu->x*d_angle + imu->y + imu->z*d_angle;
imu->z = imu->w*d_angle + imu->x*d_angle - imu->y*d_angle + imu->z;
q_norm_l = my_sqrt_reciprocal(imu->w*imu->w + imu->x*imu->x + imu->y*imu->y + imu->z*imu->z);
imu->w *= q_norm_l;
imu->x *= q_norm_l;
imu->y *= q_norm_l;
imu->z *= q_norm_l;
```
### 五、高度数据融合
匿名飞控中有气压计和tof都可以测量高度,匿名的操作是,存在tof的时候就选择tof与惯导加速度进行一个融合计算;不存在tof时则用气压计与惯导加速度进行融合运算。用了一段时间感觉匿名的定高好像并不是很准确,而且也有点飘,不知道是不是自己菜的问题,听说acfly用气压计就很稳,但好像是半开源,有时间也想改一改匿名的定高部分吧。
下面这个函数是对高度数据的处理,由 WCZ_Fus_Task(11) 经过选择tof或气压计之后调用。
```
void WCZ_Data_Calc(u8 dT_ms,u8 wcz_f_pause,s32 wcz_acc_get,s32 ref_height)
{
static u8 cyc_xn;
float hz,ntimes_hz;
hz = safe_div(1000,dT_ms,0);
ntimes_hz = hz/N_TIMES;
wcz_ref_height = ref_height; ///tof或者是气压计的值
wcz_acc = wcz_acc_get; ///加速度计的值,低通简单滤过波
/
wcz_acc_deadzone = LIMIT(5 *(0.996f - imu_data.z_vec *imu_data.z_vec),0,1) *10;
// roll_acc_fix = (ABS(sensor.Gyro_deg) + ABS(sensor.Gyro_deg) - 20) *0.1f;
// roll_acc_fix = LIMIT(roll_acc_fix,0,5);
//
// wz_acc_comp = roll_acc_fix ;//- LIMIT(5 *(0.996f - imu_data.z_vec *imu_data.z_vec),0,1) *5 ;
cyc_xn ++;
cyc_xn %= N_TIMES;
if(cyc_xn == 0)
{
wcz_ref_speed = (wcz_ref_height - ref_height_old) *ntimes_hz; ///由tof或者气压计直接微分算速度和加速度
wcz_ref_acc = (wcz_ref_speed - ref_speed_old) *ntimes_hz;
ref_height_old = wcz_ref_height;
ref_speed_old = wcz_ref_speed;
}
///低通滤波+积分
wcz_acc_fus.fix_ki = 0.05f;
wcz_acc_fus.in_est = wcz_acc;
wcz_acc_fus.in_obs = wcz_ref_acc;
wcz_acc_fus.ei_limit = 200;
inte_fix_filter(dT_ms*1e-3f,&wcz_acc_fus);
wcz_spe_fus.fix_kp = 0.5f;
wcz_spe_fus.in_est_d = my_deadzone(wcz_acc_fus.out,0,wcz_acc_deadzone);
wcz_spe_fus.in_obs = wcz_ref_speed;
wcz_spe_fus.e_limit = 200;
fix_inte_filter(dT_ms*1e-3f,&wcz_spe_fus);
wcz_hei_fus.fix_kp = 0.3f;
wcz_hei_fus.in_est_d = wcz_spe_fus.out;
wcz_hei_fus.in_obs = ref_height;
//wcz_hei_fus.e_limit = 200;
fix_inte_filter(dT_ms*1e-3f,&wcz_hei_fus);
///
}
```
### 六、PID控制
匿名飞控用的是经典控制pid算法,高度是有两个环的,位置控制值做到位置速度环,然后后面是角度环和角速度环,具体不展开讲了,这里主要说一下匿名用的pid算法。传统的pid算法想必应该都已经很熟悉了,匿名在这里其实也没有做太多特殊处理。主要就是家里前馈和微分先行。
- 前馈控制:就是按扰动量进行补偿,但用的话就是开环的,一般要和反馈控制结合使用。
- 微分先行pid:发生突变,而被调量的变化,通常总是比较缓慢的。这种输出量先行微分控制适合于给定值频繁变化的场合,可以避免给定值变化时可能引起的系统振荡,明显地改善了系统的动态特性。其实就是d参数对于期望值和反馈值的比重不同。分为d_exp和d_fb,如果这两个值相等,和普通的pid就一样了。
```
float PID_calculate( float dT_s, //周期(单位:秒)
float in_ff, //前馈值
float expect, //期望值(设定值)
float feedback, //反馈值()
PID_arg_st *pid_arg, //PID参数结构体
PID_val_st *pid_val, //PID数据结构体
float inte_d_lim,//积分误差限幅
float inte_lim //integration limit,积分限幅
)
{
float differential,hz;
hz = safe_div(1.0f,dT_s,0);
// pid_arg->k_inc_d_norm = LIMIT(pid_arg->k_inc_d_norm,0,1);
pid_val->exp_d = (expect - pid_val->exp_old) *hz;///期望的差值
if(pid_arg->fb_d_mode == 0)
{
pid_val->fb_d = (feedback - pid_val->feedback_old) *hz;///反馈的差值
}
else
{
pid_val->fb_d = pid_val->fb_d_ex;
}
differential = (pid_arg->kd_ex *pid_val->exp_d - pid_arg->kd_fb *pid_val->fb_d);
pid_val->err = (expect - feedback);
pid_val->err_i += pid_arg->ki *LIMIT((pid_val->err ),-inte_d_lim,inte_d_lim )*dT_s;//)*T;//+ differential/pid_arg->kp
//pid_val->err_i += pid_arg->ki *(pid_val->err )*T;//)*T;//+ pid_arg->k_pre_d *pid_val->feedback_d
pid_val->err_i = LIMIT(pid_val->err_i,-inte_lim,inte_lim);
pid_val->out = pid_arg->k_ff *in_ff ///前馈值*前馈系数直接加进来
+ pid_arg->kp *pid_val->err
+differential
// + pid_arg->k_inc_d_norm *pid_val->err_d_lpf + (1.0f-pid_arg->k_inc_d_norm) *differential
+ pid_val->err_i;
pid_val->feedback_old = feedback;
pid_val->exp_old = expect;
return (pid_val->out);
}
```
————————————————
版权声明:本文为CSDN博主「AngeloG」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/GWH_98/article/details/97509669
页:
[1]