前言:打算给小巡洋舰搞点花活,顺便做个飞控,记录一下其中最为关键的惯性导航(IMU)。本次项目中使用的是TDK出的最新MEMS传感器以提高项目的生命周期,因此本文将先简单介绍ICM42688这个片子,接着探讨SPI驱动方法,最后记录六轴MEMS传感器的工作原理。
ICM42688简介
这款6轴MEMS运动跟踪传感器内有一个三轴陀螺仪和一个三轴加速度计,通信方面支持I3C接口、常规的IIC、SPI通信和2kB的FIFO,并且按照TDK的老传统这个片子里也是带数字处理单元的。这个片子目前算是消费电子里的顶流了,参数如下:
陀螺仪噪声密度:$0.0028°/s/\sqrt{Hz}$
加速度计噪声密度:$70\mu g/\sqrt{Hz}$
8档陀螺仪量程:$\pm15.6 \sim 2000dps$
4档加速度计量程:2/4/8/16g
前端ADC精度:16bits
SPI通信速率达24MHz
最后,温漂参数也是非常不错:
陀螺仪SSF-Temp为$\pm 0.005 % /° C$,ZRO-Temp为$\pm 0.005 °/s/° C$
加速度计SSF-Temp为$\pm 0.005 % /°C$,ZGO-Temp为$\pm 0.15 mg/° C$
至于内置的APEX数字运动处理器也是非常优秀了,可以说是针对消费电子设计的,支持检测拿起、记录步数、倾斜测试等。这些功能在机器人应用上虽然犹如鸡肋,但是应该可以做一些花活。
SPI总线
SPI总线在高速板载外设的通信中,极为常见。标准的4线SPI的信号线有:
片选信号线NSS (或CS、CSB):SPI标准允许一套总线上挂载多个设备,但是通信时只能有一个设备与主机通信,也就是片选被使能(拉低)的外设;
同步时钟线SCLK (或SCK):在下面将会看到,数据线上的信号将在时钟线的上下边沿被采集或放置,多个从机时该线共享;
主机收从机发数据线MISO (或SDO):全称Master Input Slave Output,作用很显然了,串行发至主机的数据线,多个从机时该线共享;
主机发从机收数据线MOSI (或SDI):全称Master Output Slave Input,类似的,串行发至从机的数据线,多个从机时该线共享;
⚠️SPI总线属于有时钟的同步总线,因此在布线时应该严格按等长线设计 ;但一般无需做阻抗匹配
在STM32等MCU上,SPI外设的配置十分灵活,如上所示可以配置出最少4种类型的SPI通信方式,并由参数CPOL 和CPHA 配置:
CPOL(Clock Polarity)表示时钟线SCLK 在空闲状态时的信号电平。
CPHA(Clock Phase)表示数据线MOSI 和MISO 在哪个时钟沿捕获数据。如上图所示,当该参数为1Edge
时,设备在第一个时钟跳变沿采样数据;当该参数为2Edge
时,设备在第二个时钟跳变沿采样数据。
ICM42688的SPI标准
上图为截取自42688数据手册的SPI时序图,显然片选信号CS 和时钟信号SCLK 在空闲时都为高电平,数据线SDI 和SDO 在每个周期的第一个边沿放置数据及第二个边沿采样数据。因此CPOL=High(1
),CPHA=2Edge(1
)。
底层驱动代码
本文接下来开发的驱动程序都是基于上原理图实现的硬件进行配置,为FOSH
计划中的GUAV
通用无人空中飞行器项目。采样标准4线SPI通信的方式,并使用1个中断线和一个外部32.768kHz的标准时钟(来自MCU的内部时钟共享,以提高数据同步性),另外添加PWM控制的加热恒温功能以防止低温环境带来的误差。
在ICM42688P的数据手册上,我们看到该片的SPI使用时为:
先发高位(MSB first)
上升沿采样(与上面SPI小节中传输时序一致)
读写时都是主机先发(MOSI)的第一个字节为地址信息,且该字节的最高位表示读写指令(读是1,写是0)
根据这些信息来配置MCU,笔者使用的为STM32H750VBT6,使用官方工具CubeMX配置SPI参数具体如下:
在CubeMX生成的项目中,可以编写如下代码读写ICM42688P的寄存器,并测试连续读写功能:
基于该测试代码和上面的原理图(等长布线),经实际上板测试,在225kHz~22.5MHz的时钟(波特率)范围内都可以稳定通信(SPI官方最高速率指标为24MHz)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 COPY #include "bsp.h" #include "string.h" #define ICM42xxx_HSPI hspi2 #define ICM42xxx_BUFFER_LEN 16 #define ICM42xxx_SPI_NSS_HIGH() HAL_GPIO_WritePin(SPI2_NSS4IMU_GPIO_Port, SPI2_NSS4IMU_Pin, GPIO_PIN_SET) #define ICM42xxx_SPI_NSS_LOW() HAL_GPIO_WritePin(SPI2_NSS4IMU_GPIO_Port, SPI2_NSS4IMU_Pin, GPIO_PIN_RESET) uint8_t icm42xxx_spi_tx [ICM42xxx_BUFFER_LEN] ={0xff };uint8_t icm42xxx_spi_rx [ICM42xxx_BUFFER_LEN] ={0xff };uint8_t testCache[4 ];uint32_t icm42xxx_spi_master_write_register (uint8_t reg_addr, uint8_t value) { ICM42xxx_SPI_NSS_LOW(); icm42xxx_spi_tx[0 ] = reg_addr & 0x7F ; icm42xxx_spi_tx[1 ] = value; HAL_SPI_TransmitReceive(&ICM42xxx_HSPI, &icm42xxx_spi_tx[0 ], &icm42xxx_spi_rx[0 ], 2 , 0XFF ); ICM42xxx_SPI_NSS_HIGH(); return 0 ; } uint32_t icm42xxx_spi_master_read_register (uint8_t reg_addr, uint8_t * pData, uint8_t len) { if (len>ICM42xxx_BUFFER_LEN-1 ) return 1 ; ICM42xxx_SPI_NSS_LOW(); icm42xxx_spi_tx[0 ] = reg_addr | 0x80 ; memset (icm42xxx_spi_tx+1 ,0x00 ,len); if ( HAL_SPI_TransmitReceive(&ICM42xxx_HSPI, &icm42xxx_spi_tx[0 ], &icm42xxx_spi_rx[0 ], len+1 , 0XFF ) != HAL_OK) { return 2 ; } ICM42xxx_SPI_NSS_HIGH(); memcpy (pData,icm42xxx_spi_rx+1 ,len); return 0 ; } uint32_t icm42xxx_spi_master_communicate_test (void ) { #define ICM42XXX_REGADDR_WHO_AM_I (0x75) #define ICM42XXX_REGVAL_WHO_AM_I (0x47) #define ICM42XXX_REGADDR_ACCEL_WOM_Y_THR (0x4B) icm42xxx_spi_master_read_register(ICM42XXX_REGADDR_WHO_AM_I, testCache, 1 ); if (testCache[0 ] != ICM42XXX_REGVAL_WHO_AM_I) return 1 ; icm42xxx_spi_master_write_register(0x11 ,0x01 ); HAL_Delay(2 ); icm42xxx_spi_master_read_register(ICM42XXX_REGADDR_ACCEL_WOM_Y_THR, testCache, 2 ); for (int i = 0 ; i < 2 ;i++) { testCache[i] += 0x15 ; HAL_Delay(1 ); icm42xxx_spi_master_write_register((ICM42XXX_REGADDR_ACCEL_WOM_Y_THR + i), testCache[i]); } HAL_Delay(1 ); icm42xxx_spi_master_read_register(ICM42XXX_REGADDR_ACCEL_WOM_Y_THR, testCache+2 , 2 ); for (int i = 0 ; i < 2 ;i++) { if (testCache[i] != testCache[i+2 ]) return 2 ; } return 0 ; }
姿态解算算法 一般用直观的欧拉角 来表示机体姿态,但是解算过程中,由于欧拉角的万向锁问题,人们使用四元数 作为主要数学工具。
四元数 这是类似于复数的工具,但虚部是$\mathbb R^3$空间向量$\mathbf q_v=[q_1,q_2,q_3]^T$,一般写为:
$$ \mathbf q \triangleq \begin{bmatrix} q_0 \\ \mathbf q_v \end{bmatrix} $$
具有性质:
加减法(显然满足交换律) $$ \mathbf{p} \pm \mathbf{q} = \begin{bmatrix} p_0 \\ \mathbf p_v \end{bmatrix} \pm \begin{bmatrix} q_0 \\ \mathbf q_v \end{bmatrix} = \begin{bmatrix} p_0\pm q_0 \\ \mathbf p_v\pm \mathbf q_v \end{bmatrix} $$
乘法(不能交换次序,但有分配律和结合律) $$ \mathbf p\otimes \mathbf q = \begin{bmatrix} p_0 \\ \mathbf p_v \end{bmatrix} \otimes \begin{bmatrix} q_0 \\ \mathbf q_v \end{bmatrix} = \begin{bmatrix} p_0q_0-\mathbf q_v^T \mathbf p_v \\ \mathbf p_v\times \mathbf q_v + p_0 \mathbf q_v + q_0 \mathbf p_v \end{bmatrix} \\ \mathbf q \otimes (\mathbf r+\mathbf m) = \mathbf q\otimes \mathbf r + \mathbf q \otimes \mathbf m \\ \mathbf q \otimes \mathbf r \otimes \mathbf m = (\mathbf q \otimes \mathbf r) \otimes \mathbf m =\mathbf q \otimes (\mathbf r \otimes \mathbf m ) $$
数乘 $$ s\mathbf q = \mathbf q s = \begin{bmatrix} sq_0 \\ s\mathbf q_v \end{bmatrix} $$
共轭(和复数类似,都是虚部反) $$ \mathbf q^* = \begin{bmatrix} q_0 \\ -\mathbf q_v \end{bmatrix} \\ (\mathbf q \otimes \mathbf p)^* = \mathbf p^* \otimes \mathbf q^* $$
范数 $$ \Vert \mathbf q \Vert = \sqrt{q_0^2 + \mathbf q_v^T\mathbf q_v} = \sqrt{\Vert \mathbf q \otimes \mathbf q^*\Vert} $$
逆 $$ \mathbf q \otimes \mathbf q^{-1} = \begin{bmatrix} 1 \\ \mathbf 0_{3\times 1} \end{bmatrix} \\ \mathbf q^{-1} = \frac{\mathbf q^*}{\Vert \mathbf q \Vert} $$
以上就是四元数的基本性质,接下来我们将这个工具往需要的姿态解算上带。首先,从四元数的范数计算,我们类似单位向量一般的,定义单位四元数$\Vert \mathbf q\Vert = 1$,这是因为单位化之后的四元数们具有优秀的计算性质:
$$ \forall . \Vert \mathbf q \Vert =\Vert \mathbf p \Vert =1 \\ \Rightarrow \Vert \mathbf q \otimes \mathbf p \Vert = 1 \\ \mathbf q^{-1} = \mathbf q^{*} $$
然后我们观察到四元数$\mathbf q$是由一个标量$q_0$和一个空间向量$\mathbf q_v$组成,如果把一个旋转过程记录为绕$\mathbf v$轴(右手螺旋系)旋转$\theta_0$角度,那么看起来恰好可以用四元数来表示🧐但是人们发现直接这样带进去的话,算起来还是不好用!然后就有人发现,可以把旋转过程和四元数对应为:
$$ \mathbf q = \begin{bmatrix} \cos\frac \theta 2 \\ \mathbf v \sin\frac \theta 2 \end{bmatrix}, \Vert \mathbf v \Vert =1 $$
首先,这样映射的话就成了单位四元数啦,求逆很方便!然后将空间方向向量$\mathbf v_1$用一个四元数表示: $$ \mathbf p = \begin{bmatrix} 0 \\ \mathbf v_1 \end{bmatrix} $$
则该空间向量经过一个$\mathbf q$的旋转过程(绕$\mathbf v$转$\theta$角),经证明,可采用下式计算旋转后的空间姿态$\mathbf v_1’$:
$$ \begin{bmatrix} 0 \\ \mathbf v_1’ \end{bmatrix} = \mathbf q \otimes \mathbf p \otimes \mathbf q^{-1} = \begin{bmatrix} \cos\frac \theta 2 \\ \mathbf v \sin\frac \theta 2 \end{bmatrix} \otimes \begin{bmatrix} 0 \\ \mathbf v_1 \end{bmatrix} \otimes \begin{bmatrix} \cos\frac \theta 2 \\ -\mathbf v \sin\frac \theta 2 \end{bmatrix} $$
读者可以直接记下该重要公式 ,有余力可结合文末的推荐参考文献自行推导具体过程。
四元数旋转计算过程矩阵化 除了四元数外,这里也介绍一下它的好兄弟——旋转矩阵:
$$ \mathbf v_e = \mathbf R ^e_b \cdot \mathbf v_b \\ \mathbf R^e_b \in SO(3),SO(3) = { \mathbf R | \mathbf R^T \mathbf R = \mathbf I_3, det(\mathbf R) = 1,\mathbf R \in \mathbb R_{3\times 3} } $$
虽然用不上,但是这里为啥要介绍旋转矩阵呢,因为优秀的先人就是从这个旋转矩阵的变换中,观察到四元数空间旋转计算过程和旋转矩阵计算极为像!前提是我们要把四元数旋转计算过程进行矩阵化(毕竟不矩阵化,计算机也算不了这玩意),四元数乘法计算可以转为:
$$ \mathbf p\otimes \mathbf q = \mathbf p^+ \cdot \mathbf q = \mathbf q^- \cdot \mathbf p $$
上式矩阵乘法中,将一个四元数直接作为$\mathbb R_{4}$向量,另一个四元数与四元数乘法符号一起转换为一个$\mathbb R_{4\times 4}$等效矩阵:
$$ \mathbf p^{+} = p_0 \mathbf I_4 + \begin{bmatrix} 0 & -\mathbf p_v^T \\ \mathbf p_v & [\mathbf p_v]_\times \end{bmatrix} $$ $$ \mathbf q^{-} = q_0 \mathbf I_4 + \begin{bmatrix} 0 & -\mathbf q_v^T \\ \mathbf q_v & - [\mathbf q_v]_\times \end{bmatrix} $$
其中,$[\mathbf p_v]_\times$是$\mathbf p_v$叉乘计算的矩阵点乘表示:
$$ \mathbf p_v \times \mathbf a = [\mathbf p_v]_\times \mathbf a,因此 [\mathbf p_v]_\times = \begin{bmatrix} 0 & -p_3 & p_2 \\ p_3 & 0 & -p_1 \\ -p_2 & p_1 & 0 \end{bmatrix} $$
因此四元数旋转计算过程可以矩阵化为:
$$ \begin{bmatrix} 0 \\ \mathbf v_{1}’ \end{bmatrix} = \mathbf q \otimes \begin{bmatrix} 0 \\ \mathbf v_{1} \end{bmatrix} \otimes \mathbf q^* = \mathbf q^{+} \cdot \mathbf q^{* -} \cdot \begin{bmatrix} 0 \\ \mathbf v_{1} \end{bmatrix} $$ $$ \Rightarrow \begin{bmatrix} 0 \\ \mathbf v_1’ \end{bmatrix} = \begin{bmatrix} q_0 & -\mathbf q_v^T \\ \mathbf q_v & q_0\mathbf I_3 + [\mathbf q_v]_\times \end{bmatrix} \begin{bmatrix} q_0 & +\mathbf q_v^T \\ -\mathbf q_v & q_0\mathbf I_3 + [\mathbf q_v]_\times \end{bmatrix} \begin{bmatrix} 0 \\ \mathbf v_1 \end{bmatrix} $$
在这个过程中,由于变换对象和结果对象四元数的实数部分都是0,所以如下我们可以把与0操作的部分直接去掉,则得到如下类似旋转矩阵的变换形式:
$$ \mathbf v_1’ = \begin{bmatrix} q_0^2+q_1^2-q_2^2-q_3^2 & 2(q_1q_2-q_0q_3) & 2(q_1q_3+q_0q_2) \\ 2(q_1q_2+q_0q_3) & q_0^2-q_1^2+q_2^2-q_3^2 &2(q_2q_3-q_0q_1) \\ 2(q_1q_3-q_0q_2) & 2(q_2q_3+q_0q_1) & q_0^2-q_1^2-q_2^2+q_3^2 \end{bmatrix} \cdot \mathbf v_1 $$
六轴数据→四元数→姿态估计 姿态估计算法的最终目标 通常就是欧拉角 ,因为欧拉角的物理概念很直观很好理解,方便设计多旋翼等类型的控制系统。而中间计算过程,上面通过对四元数的介绍,我们知道肯定是用这种优秀的工具。当机体停在平地上开机,接着开始运动,我们可以分别从3轴陀螺仪、3轴加速度计获得两套姿态:
陀螺仪数据获得姿态
这里我们将传感器返回的3轴陀螺仪数据记为: $$ \omega = \begin{bmatrix} \omega_x \\ \omega_y \\ \omega_z \end{bmatrix} $$
显然,对角速度进行积分就可以直接得到姿态,但从长远来看,我们需要把积分过程用四元数来算才行。从四元数的性质推导得到四元数的微分方程:
$$ \dot{\mathbf q} = \frac 12 \mathbf q \otimes \begin{bmatrix} 0 \\ \omega \end{bmatrix} $$
所以陀螺仪估计姿态,在MCU中的离散式计算方程为:
$$ \mathbf q_t = \mathbf q_{t-1} + \frac 12 \mathbf q_{t-1} \otimes \begin{bmatrix} 0 \\ \omega \end{bmatrix} \Delta t $$
式中$\Delta t$为定采样/计算迭代的周期时长。一个纯积分的系统,都必然是会发散的!这也是用陀螺仪测量的问题,如果一直迭代上式,就算是静止放着,角速度测量时的噪声、误差等都会不断累积,也就是大家常说的“漂移”。
加速度计数据获得姿态
类似的,我们把传感器得到的3轴加速度计数据记为:
$$ \mathbf a = \begin{bmatrix} a_x \\ a_y \\ a_z \end{bmatrix} $$
当我们把传感器静止 放在一个水平面 上时,显然受到重力的作用,测量的结果应该是当地的重力加速度$\mathbf g = [0,0,g]^T$;而在其他姿态静止 情况下,我们通过勾股定理可以得到:
$$ a_x = -g \sin \theta \\ a_y = g \cos \theta \sin \phi \\ a_z = g \cos \theta \cos \phi $$
多么美妙方便呀,可以直接得到两个姿态角$\theta = \arcsin(\frac {a_x}{-g})$和$\phi = \arctan(\frac{a_y}{a_z})$。是的,非常遗憾,得不到偏航角!并且还有其他问题,比如载体运动时测量结果带有运动加速度、加速度噪声等。
通过上面直接用原始数据求解姿态,我们就得到了一句流传于世的经典名言:陀螺仪测量结果高频特性好,加速度计测量结果低频特性好。展开来说呢,就是陀螺仪因为积分漂移的问题,看总体的话测量结果会越来越差,但是局部测量的姿态(角速度)可靠性高;而加速度恰恰相反,没有漂移问题,但是局部测量结果估计的姿态存在较大干扰和噪声。所以,工程师们达成了一个共识,陀螺仪和加速计配合起来用就叫做一个完美!(当然,还有个缺陷就是偏航角测量问题,得加地磁计)那么就要设计一套多传感器数据融合估计姿态算法了,接下来开始介绍一套常用的6轴传感器估计姿态算法。
这里接下来讨论的其实是Mahony算法 !还有很多其他优秀的姿态估计算法,只是本文写不下而已!
图中$_b \vec a$和$_b\vec \omega$分别表示传感器测量得到的加速度和角速度,由于传感器是安装在机体上的,所以这里使用前下角标“$_b$”进行注明,并考虑图片分辨率问题使用头戴箭头的方式替代本文中的粗体来表示矢量。
这个算法融合的原理简单来说,就是用PI控制器去调整四元数离散微分方程中的角速度,补偿的依据(误差)是加速度计测量得到的向量与估计姿态下的重力方向做叉乘,这个误差的定义也就是这个算法的精髓。可以看出算法框图中是一个闭环结构,所以最后算法收敛的目标就是陀螺仪估计得到的姿态应该接近/等于加速度计估计的姿态。
Mahony姿态估计实现代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 COPY #define sampleFreq 1000.0f #define twoKpDef (2.0f * 0.5f) #define twoKiDef (2.0f * 0.0f) volatile float twoKp = twoKpDef; volatile float twoKi = twoKiDef; volatile float integralFBx = 0.0f , integralFBy = 0.0f , integralFBz = 0.0f ; float inv_sqrt (float x) { float halfx = 0.5f * x; float y = x; long i = *(long *)&y; i = 0x5f3759df - (i >> 1 ); y = *(float *)&i; y = y * (1.5f - (halfx * y * y)); return y; } void MahonyFilter_6Axis (float q[4 ], float gx, float gy, float gz, float ax, float ay, float az) { float normFactor; float halfvx, halfvy, halfvz; float halfex, halfey, halfez; float q0 = q[0 ],q1 = q[1 ], q2 = q[2 ], q3 = q[3 ]; float q0q0 = q[0 ]*q[0 ]; float q0q1 = q[0 ]*q[1 ]; float q0q2 = q[0 ]*q[2 ]; float q0q3 = q[0 ]*q[3 ]; float q1q1 = q[1 ]*q[1 ]; float q1q2 = q[1 ]*q[2 ]; float q1q3 = q[1 ]*q[3 ]; float q2q2 = q[2 ]*q[2 ]; float q2q3 = q[2 ]*q[3 ]; float q3q3 = q[3 ]*q[3 ]; if ((ax != 0.0f ) || (ay != 0.0f ) || (az != 0.0f )) { normFactor = inv_sqrt(ax * ax + ay * ay + az * az); ax *= normFactor; ay *= normFactor; az *= normFactor; halfvx = q1q3 - q0q2; halfvy = q0q1 + q2q3; halfvz = q0q0 - 0.5f + q3q3; halfex = (ay * halfvz - az * halfvy); halfey = (az * halfvx - ax * halfvz); halfez = (ax * halfvy - ay * halfvx); if (twoKi > 0.0f ) { integralFBx += twoKi * halfex * (1.0f / sampleFreq); integralFBy += twoKi * halfey * (1.0f / sampleFreq); integralFBz += twoKi * halfez * (1.0f / sampleFreq); gx += integralFBx; gy += integralFBy; gz += integralFBz; } else { integralFBx = 0.0f ; integralFBy = 0.0f ; integralFBz = 0.0f ; } gx += twoKp * halfex; gy += twoKp * halfey; gz += twoKp * halfez; } gx *= (0.5f * (1.0f / sampleFreq)); gy *= (0.5f * (1.0f / sampleFreq)); gz *= (0.5f * (1.0f / sampleFreq)); q[0 ] += (-q1 * gx - q2 * gy - q3 * gz); q[1 ] += (q0 * gx + q2 * gz - q3 * gy); q[2 ] += (q0 * gy - q1 * gz + q3 * gx); q[3 ] += (q0 * gz + q1 * gy - q2 * gx); normFactor = inv_sqrt(q[0 ] * q[0 ] + q[1 ] * q[1 ] + q[2 ] * q[2 ] + q[3 ] * q[3 ]); q[0 ] *= normFactor; q[1 ] *= normFactor; q[2 ] *= normFactor; q[3 ] *= normFactor; }
迭代得到的四元数,可以需要时用来解欧拉角:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 COPY void trans_quaternionToEulerAngle (imu_rawData_t* pIMU) { float q[4 ]; for (int i =0 ; i <4 ; i++) { q[i] = pIMU->quaternion_body2Earth[i]; } pIMU->yaw = -atan2 (2 *q[1 ]*q[2 ] + 2 *q[0 ]*q[3 ], -2 *q[2 ]*q[2 ] - 2 *q[3 ]*q[3 ] + 1 )* 57.3 ; pIMU->pitch = -asin (-2 *q[1 ]*q[3 ] + 2 *q[0 ]*q[2 ])* 57.3 ; pIMU->roll = atan2 (2 *q[2 ]*q[3 ] + 2 *q[0 ]*q[1 ], -2 *q[1 ]*q[1 ] - 2 *q[2 ]*q[2 ] + 1 )* 57.3 ; }
MEMS陀螺仪工作原理
首先介绍物理学中的科里奥利力(Coriolis force),是对旋转体系中进行直线运动的质点由于惯性相对于旋转体系产生的直线运动的偏移的一种描述。科里奥利力来自于物体运动所具有的惯性。说白了,其实就是射击游戏里的打提前量,但是目标是旋转的 。这玩意的发现来源于炮弹射不准问题,因为地球是有自转的(旋转系),所以在某个时刻瞄准目标打炮时,炮弹只会直着飞出去(相对于宇宙这个静止惯性系),然而在飞的过程中目标跟着地球转走了,就打不准;从地面上受害者的角度来看,就是炮弹自己慢慢拐弯走了???🤣聪明的炮兵就因此就请出了科里奥利力来算射击提前量:
$$ \vec F_c = -2m(\vec \omega \times \vec v) $$ 式中,$m$是炮弹的质量,$\vec v$是炮弹的飞行速度,$\vec \omega$是受害者所在旋转系的角速度。
MEMS陀螺仪也是这么个道理,内部有一个微机电(Micro-Electro-Mechanical)谐振腔,腔内有一个小振子受到交替电压驱动作振动,当外壳带着谐振腔旋转时,里面的振子因为科里奥利力导致振动方向偏移,测量这个方向偏移就可以得到旋转速度 了。
MEMS加速度计原理 加速度计的不同类型原理有:
压电式:里面有个质量块以及用来测量的压电元件,加减速时由于压力不同导致压电元件产生的电压不同: $$ F = ma \\ V = kF $$
电容式:内部一样有一个质量块,加减速时拉动电容的极板,通过测量电容量即可
热感式:没有任何质量块,它的中央有一个加热体,周边是温度传感器,里面是密闭的气腔,工作时在加热体的作用下,气体在内部形成一个热气团,热气团的比重和周围的冷气是有差异的,通过惯性热气团的移动形成的热场变化让感应器感应到加速度值
初始化(信号链配置)详解 在上面我们给出了使用ICM42688
芯片的应用硬件电路和高速SPI
驱动的相关工作,以及将陀螺仪数据与加速度计数据进行融合的入门级Mahony
算法。在最后补充对ICM42688的初始化过程,在上电/复位后需要依次初始化的有:
软件复位
设置外部时钟源与PLL
配置陀螺仪信号链
配置加速度计信号链
配置中断
配置FIFO
使能供电
所有功能的描述在数据手册上都能找到,这里着重讲解信号链(下图)的配置,因为这关系到采集数据的有效性和可靠性问题。陀螺仪由于上方讨论的振子结构问题,所以输出信号的噪声比较大;因此相比于加速度计的信号链,陀螺仪加入了一级“限波器”(Notch Filter),用来消除振子的工作频率噪声。随后是两级通用的滤波器:抗混叠滤波器(Anti-Alias Filter)和用户接口滤波器(UI Filter),中间夹着一个硬件的偏置补偿。
UIF是由输出数据率(ODR:output data rate)、阶数(Order)、带宽(Bandwidth)或群延迟(Group Delay)三个指标进行配置,主要作用是输出系统需要的数据到寄存器中;因为MEMS传感器的带宽非常大,所以根据奈归斯特采样定理,需要将进入UIF的信号带宽限制为ODR的一半,因此AAF的作用就是这个了。
下图是使用AAF前后的信号波形情况,可以看出对于陀螺仪来说效果非常明显,而加速度计本身噪声就比较少。
以下是初始化的实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 COPY void icm426xx_deviceInit (void ) { #ifndef _RAWSIGNAL_ANALYSIS_ICM426XX_ uint16_t gyro_aaf_delta = 2 ; uint16_t gyro_aaf_deltasqr = gyro_aaf_delta*gyro_aaf_delta; uint8_t gyro_aaf_bitshift = 13 ; uint16_t accel_aaf_delta = 2 ; uint16_t accel_aaf_deltasqr = gyro_aaf_delta*gyro_aaf_delta; uint8_t accel_aaf_bitshift = 13 ; #endif imuStatus = ICM426XX_STATUS_NOREADY; imu.quaternion_body2Earth[0 ] = 1 ; imu.quaternion_body2Earth[1 ] = 0 ; imu.quaternion_body2Earth[2 ] = 0 ; imu.quaternion_body2Earth[3 ] = 0 ; HAL_Delay(3 ); icm426xx_spi_master_write_register(MPUREG_REG_BANK_SEL, 0 ); icm426xx_spi_master_write_register(MPUREG_DEVICE_CONFIG, ICM426XX_DEVICE_CONFIG_RESET_EN); HAL_Delay(2 ); icm426xx_spi_master_write_register(MPUREG_REG_BANK_SEL,1 ); icm426xx_spi_master_write_register(MPUREG_INTF_CONFIG5_B1, ICM426XX_INTF_CONFIG5_PIN9_asCLKIN ); icm426xx_spi_master_write_register(MPUREG_REG_BANK_SEL,0 ); icm426xx_spi_master_write_register(MPUREG_INTF_CONFIG1, (ICM426XX_INTF_CONFIG1_ACCEL_LP_CLK_RCOSC | \ ICM426XX_INTF_CONFIG1_RTC_MODE_EN | \ 0x01 ) ); icm426xx_spi_master_write_register(MPUREG_TMST_CONFIG, (ICM426XX_TMST_CONFIG_TMST_TO_REGS_EN | \ ICM426XX_TMST_CONFIG_RESOL_16us | \ ICM426XX_TMST_CONFIG_TMST_FSYNC_EN | \ ICM426XX_TMST_CONFIG_TMST_EN ) ); icm426xx_spi_master_write_register(MPUREG_REG_BANK_SEL,1 ); #ifndef _RAWSIGNAL_ANALYSIS_ICM426XX_ icm426xx_spi_master_write_register(MPUREG_GYRO_CONFIG_STATIC3_B1, (gyro_aaf_delta & 0x3f ) ); icm426xx_spi_master_write_register(MPUREG_GYRO_CONFIG_STATIC4_B1, (gyro_aaf_deltasqr & 0xff ) ); icm426xx_spi_master_write_register(MPUREG_GYRO_CONFIG_STATIC5_B1, ( ((gyro_aaf_deltasqr>>8 ) & 0x0f ) | (gyro_aaf_bitshift<<4 ) ) ); #ifdef _DONT_USE_NOTCHFILTER_ICM426XX_ icm426xx_spi_master_write_register(MPUREG_GYRO_CONFIG_STATIC2_B1, (ICM426XX_GYRO_AAF_EN | ICM426XX_GYRO_NF_DIS) ); #else icm426xx_spi_master_write_register(MPUREG_GYRO_CONFIG_STATIC2_B1, (ICM426XX_GYRO_AAF_EN | ICM426XX_GYRO_NF_EN) ); #endif #else icm426xx_spi_master_write_register(MPUREG_GYRO_CONFIG_STATIC2_B1, (ICM426XX_GYRO_AAF_DIS | ICM426XX_GYRO_NF_DIS) ); #endif icm426xx_spi_master_write_register(MPUREG_REG_BANK_SEL, 0 ); icm426xx_spi_master_write_register(MPUREG_GYRO_CONFIG0, (_USER_GYRO_FULLSCALE_ | ICM426XX_GYRO_CONFIG0_ODR_200_HZ) ); icm426xx_spi_master_write_register(MPUREG_GYRO_CONFIG1, (ICM426XX_GYRO_CONFIG_GYRO_UI_FILT_ORD_3RD_ORDER | (2 <<1 )) ); icm426xx_spi_master_write_register(MPUREG_ACCEL_GYRO_CONFIG0, (ICM426XX_GYRO_ACCEL_CONFIG0_ACCEL_FILT_BW_14 | ICM426XX_GYRO_ACCEL_CONFIG0_GYRO_FILT_BW_14) ); icm426xx_spi_master_write_register(MPUREG_REG_BANK_SEL,2 ); #ifndef _RAWSIGNAL_ANALYSIS_ICM426XX_ icm426xx_spi_master_write_register(MPUREG_ACCEL_CONFIG_STATIC2_B2, ((accel_aaf_delta << BIT_ACCEL_AAF_DELT_POS) | \ ICM426XX_ACCEL_AAF_EN) ); icm426xx_spi_master_write_register(MPUREG_ACCEL_CONFIG_STATIC3_B2, (accel_aaf_deltasqr & 0xff ) ); icm426xx_spi_master_write_register(MPUREG_ACCEL_CONFIG_STATIC4_B2, ( ((accel_aaf_deltasqr>>8 ) & 0x0f ) | (accel_aaf_bitshift<<4 ) ) ); #else icm426xx_spi_master_write_register(MPUREG_ACCEL_CONFIG_STATIC2_B2, ICM426XX_ACCEL_AAF_DIS); #endif icm426xx_spi_master_write_register(MPUREG_REG_BANK_SEL, 0 ); icm426xx_spi_master_write_register(MPUREG_ACCEL_CONFIG0, (_USER_ACCL_FULLSCALE_ | ICM426XX_ACCEL_CONFIG0_ODR_200_HZ) ); icm426xx_spi_master_write_register(MPUREG_ACCEL_CONFIG1, (ICM426XX_ACCEL_CONFIG_ACCEL_UI_FILT_ORD_3RD_ORDER | (2 <<1 )) ); icm426xx_spi_master_write_register(MPUREG_REG_BANK_SEL, 0 ); icm426xx_spi_master_write_register(MPUREG_INT_CONFIG, (ICM426XX_INT_CONFIG_INT1_DRIVE_CIRCUIT_PP | \ ICM426XX_INT_CONFIG_INT1_POLARITY_HIGH) ); #ifdef _USER_USE_ICM426XX_FIFO_ icm426xx_spi_master_write_register(MPUREG_INT_SOURCE0, (BIT_INT_SOURCE0_FIFO_THS_INT1_EN | \ BIT_INT_SOURCE0_FIFO_FULL_INT1_EN ) ); #else icm426xx_spi_master_write_register(MPUREG_INT_SOURCE0, BIT_INT_SOURCE0_UI_DRDY_INT1_EN ); #endif icm426xx_spi_master_write_register(MPUREG_INT_CONFIG0, (ICM426XX_UI_DRDY_INT_CLEAR_ON_STATUS_READ | \ ICM426XX_FIFO_THS_INT_CLEAR_ON_STATUS_READ | \ ICM426XX_FIFO_FULL_INT_CLEAR_ON_STATUS_READ ) ); icm426xx_spi_master_write_register(MPUREG_INT_CONFIG1, (ICM426XX_INT_TPULSE_DURATION_100_US | \ ICM426XX_INT_TDEASSERT_ENABLED | \ ICM426XX_INT_CONFIG1_ASY_RST_ENABLED) ); icm426xx_spi_master_write_register(MPUREG_REG_BANK_SEL, 0 ); icm426xx_spi_master_write_register(MPUREG_FIFO_CONFIG, ICM426XX_FIFO_CONFIG_MODE_BYPASS ); #ifdef _USER_USE_ICM426XX_FIFO_ icm426xx_spi_master_write_register(MPUREG_INTF_CONFIG0, (ICM426XX_INTF_CONFIG0_FIFO_SREG_INVALID_IND_EN | \ ICM426XX_INTF_CONFIG0_FIFO_COUNT_REC_RECORD | \ ICM426XX_INTF_CONFIG0_FIFO_COUNT_LITTLE_ENDIAN | \ ICM426XX_INTF_CONFIG0_DATA_LITTLE_ENDIAN ) ); icm426xx_spi_master_write_register(MPUREG_FIFO_CONFIG1, (ICM426XX_FIFO_CONFIG1_RESUME_PARTIAL_RD_EN | \ ICM426XX_FIFO_CONFIG1_WM_GT_TH_EN | \ ICM426XX_FIFO_CONFIG1_HIRES_DIS | \ ICM426XX_FIFO_CONFIG1_TMST_FSYNC_EN | \ ICM426XX_FIFO_CONFIG1_TEMP_EN | \ ICM426XX_FIFO_CONFIG1_GYRO_EN | \ ICM426XX_FIFO_CONFIG1_ACCEL_EN) ); icm426xx_spi_master_write_register(MPUREG_FIFO_CONFIG2, 4 ); icm426xx_spi_master_write_register(MPUREG_FIFO_CONFIG2+1 , 0 ); icm426xx_spi_master_write_register(MPUREG_FIFO_CONFIG, ICM426XX_FIFO_CONFIG_MODE_STOP_ON_FULL ); #endif icm426xx_spi_master_write_register(MPUREG_REG_BANK_SEL, 0 ); icm426xx_spi_master_write_register(MPUREG_PWR_MGMT_0, (ICM426XX_PWR_MGMT_0_TEMP_EN | \ ICM426XX_PWR_MGMT_0_IDLE_EN | \ ICM426XX_PWR_MGMT_0_GYRO_MODE_LN | \ ICM426XX_PWR_MGMT_0_ACCEL_MODE_LN ) ); HAL_Delay(50 ); icm426xx_spi_master_write_register(MPUREG_REG_BANK_SEL, 0 ); imuStatus = ICM426XX_STATUS_NORMAL; return ; }
考虑到系统在工作的过程中,环境因素的变化,因此将加载偏置的接口单独放在初始化接口的外部:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 COPY void icm426xx_load6AxisOffset (uint8_t pOffset[9 ]) { icm426xx_spi_master_write_register(MPUREG_REG_BANK_SEL, 4 ); for (int addr=0 ; addr < 9 ; addr ++) { icm426xx_spi_master_write_register(MPUREG_OFFSET_USER_0_B4 + addr, pOffset[addr]); } icm426xx_spi_master_write_register(MPUREG_REG_BANK_SEL, 0 ); } void icm426xx_getAverageOffset (uint8_t pOffset[9 ]) { for (int addr =0 ; addr < 3 ; addr++) { gyroAxisSum[addr] = gyroAxisSum[addr] >>9 ; accelAxisSum[addr] = accelAxisSum[addr] >>9 ; gyroAxisSum[addr] = -(gyroAxisSum[addr] * 500 ) >> (6 +4 ); } accelAxisSum[0 ] = -(accelAxisSum[0 ] * 4 ) >> 4 ; accelAxisSum[1 ] = -(accelAxisSum[1 ] * 4 ) >> 4 ; accelAxisSum[2 ] = -((accelAxisSum[2 ] * 4 ) >> 4 ) + 1000 ; pOffset[0 ] = gyroAxisSum[0 ] & 0x00ff ; pOffset[1 ] = ((gyroAxisSum[1 ]& 0x0f00 ) >> 4 ) | ((gyroAxisSum[0 ]&0x0f00 ) >> 8 ); pOffset[2 ] = gyroAxisSum[1 ] & 0x00ff ; pOffset[3 ] = gyroAxisSum[2 ] & 0x00ff ; pOffset[4 ] = ((accelAxisSum[0 ]& 0x0f00 ) >> 4 ) | \ ((gyroAxisSum[2 ] & 0x0f00 ) >> 8 ); pOffset[5 ] = accelAxisSum[0 ] & 0x00ff ; pOffset[6 ] = accelAxisSum[1 ] & 0x00ff ; pOffset[7 ] = ((accelAxisSum[2 ]& 0x0f00 ) >> 4 ) | ((accelAxisSum[1 ]&0x0f00 ) >> 8 ); pOffset[8 ] = accelAxisSum[2 ] & 0x00ff ; }
若使用了中断引脚,则可以按如下进行中断处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 COPY void icm426xx_callback_interrupt1 (void ) { uint8_t regVal_int_status; if (imuStatus != ICM426XX_STATUS_NORMAL) return ; icm426xx_spi_master_read_register(MPUREG_INT_STATUS, ®Val_int_status, 1 ); #ifdef _USER_USE_ICM426XX_FIFO_ if (regVal_int_status & BIT_INT_STATUS_FIFO_FULL) { icm426xx_readFIFOPacket(NULL ); } else if (regVal_int_status & BIT_INT_STATUS_FIFO_THS) { icm426xx_readFIFOPacket(NULL ); } else #endif if (regVal_int_status & BIT_INT_STATUS_DRDY) { icm426xx_getRawData(&imuData); if (sumNum < 512 ) { transCache.byte[0 ] = imuData.gyro_x_L; transCache.byte[1 ] = imuData.gyro_x_H; gyroAxisSum[0 ] += transCache.getSign; transCache.byte[0 ] = imuData.gyro_y_L; transCache.byte[1 ] = imuData.gyro_y_H; gyroAxisSum[1 ] += transCache.getSign; transCache.byte[0 ] = imuData.gyro_z_L; transCache.byte[1 ] = imuData.gyro_z_H; gyroAxisSum[2 ] += transCache.getSign; transCache.byte[0 ] = imuData.accel_x_L; transCache.byte[1 ] = imuData.accel_x_H; accelAxisSum[0 ] += transCache.getSign; transCache.byte[0 ] = imuData.accel_y_L; transCache.byte[1 ] = imuData.accel_y_H; accelAxisSum[1 ] += transCache.getSign; transCache.byte[0 ] = imuData.accel_z_L; transCache.byte[1 ] = imuData.accel_z_H; accelAxisSum[2 ] += transCache.getSign; sumNum ++; if (sumNum >= 512 ) { icm426xx_getAverageOffset(Offset6Axis); icm426xx_load6AxisOffset(Offset6Axis); } } else { icm426xx_trans_RawDataToStandardData(&imuData, &imu); MahonyFilter_6Axis(imu.quaternion_body2Earth, imu.gyro_x, imu.gyro_y, imu.gyro_z, imu.accel_x, imu.accel_y, imu.accel_z); trans_quaternionToEulerAngle(&imu); print_AHRS_data(&imu); } #ifdef _RAWSIGNAL_ANALYSIS_ICM426XX_ #endif } if (regVal_int_status & BIT_INT_STATUS_RESET_DONE) { } if (regVal_int_status & BIT_INT_STATUS_PLL_RDY) { } if (regVal_int_status & BIT_INT_STATUS_AGC_RDY) { } }
参考文献