这里假设需求为使用GPIO模拟SPI获取板子传感器ICM20608的数据。
相较于I2C半双工的同步串行总线,SPI是全双工的同步串行总线,理论有更高的传输带宽,但SPI占用的引脚更多,通常需要四根,分别是提供时钟的SCLK,提供数据输出的MOSI(MasterOut Slave In),提供数据输入的MISO(Master In SlaveOut)和提供片选信号的CS。同一时刻只能有一个SPI设备处于工作状态,即多个CS信号中某时间只能有一个有效。为了适配不同的外设,SPI支持通过寄存器来配置片选信号和时钟信号的极性和相位。
SPI的速度也不是很高,通信协议也不是很复杂,因此在实际工程中,也不少使用GPIO模拟SPI的场景,关于SPI协议的详细分析,这里也不过多描述,读者参考后面的模拟SPI代码和其它资料。
ICM-20608-G是InvenSense面向大众市场的一款六轴姿态传感器,包含三轴加速度传感器、三轴角速度传感器和温度传感器。三轴加速度传感器(Accelerometer、G-Sensor)也叫重力感应器,可以感知任意方向上的加速度(重力加速度则只是地表垂直方向加速度),加速度传感器主要用于测量设备的受力情况,相对外部参考物的运动。三轴角速度传感器(Gyroscope、GYRO-Sensor)也叫陀螺仪,通过测量三维坐标系内陀螺转子的垂直轴与设备之间的夹角,并计算角速度,角速度传感器主要用于测量设备的自身的旋转运动,擅长感知自身运动。通过六轴数据,基本可以计算出物体的运动状态,常用手机、手环、无人机、云台等领域。
ICM-20608-G的内部框图如图2.18.1所示,六轴和温度传感器经过ADC转换保存到内部寄存器,最后通过I2C或SPI向外提供数据。

ICM-20608-G在底板的电路图,如图2.18.2所示。接在了SPI5上,这里使用GPIO模拟,可知PH6模拟作为SPI的SCK信号,PF11作为SPI MOSI信号, PH7作为SPI MISO信号,PH5作为SPI NSS片选信号。

先设置好MCU时钟、串口,接着设置PH6、PF11、PH7、PH5作为普通GPIO,注意PH7是MISO,数据输入引脚,因此要设置为输入模式,其它为输出模式。

创建“DemoDriver”目录,将前面的“driver_tim.c”复制到该目录,然后新建“driver_spi.c”实现模拟SPI协议,创建“icm_20608g.c”实现对ICM_20608_G的控制。
“driver_spi.c”实现了模拟SPI协议,代码如下:
/*
* 函数名:void SPI_Init(void)
* 输入参数:
* 输出参数:无
* 返回值:无
* 函数作用:初始化SPI的四根引脚
*/
void SPI_Init(void)
{
SPI_CS(1); // CS初始化高
SPI_CLK(0); // CLK初始化高
}
/*
* 函数名:void SPI_WriteByte(uint8_t data)
* 输入参数:data -> 要写的数据
* 输出参数:无
* 返回值:无
* 函数作用:模拟SPI写一个字节
*/
void SPI_WriteByte(uint8_t data)
{
uint8_t i = 0;
uint8_t temp = 0;
for(i=0; i<8; i++)
{
temp = data&0x80;
data = data<<1;
SPI_CLK(0);
SPI_MOSI(temp);
SPI_Delay();
SPI_CLK(1);
SPI_Delay();
}
}
/*
* 函数名:uint8_t SPI_ReadByte(void)
* 输入参数:
* 输出参数:无
* 返回值:读到的数据
* 函数作用:模拟SPI读一个字节
*/
uint8_t SPI_ReadByte(void)
{
uint8_t i = 0;
uint8_t read_data = 0xFF;
for(i=0; i<8; i++)
{
read_data = read_data << 1;
SPI_CLK(0);
SPI_Delay();
SPI_CLK(1);
SPI_Delay();
if(SPI_MISO()==1)
{
read_data = read_data + 1;
}
}
SPI_CLK(0);
return read_data;
}
/*
* 函数名:uint8_t SPI_WriteReadByte(uint8_t pdata)
* 输入参数:pdata -> 要写的一个字节数据
* 输出参数:无
* 返回值:读到的数据
* 函数作用:模拟SPI读写一个字节
*/
uint16_t SPI_WriteReadByte(uint16_t pdata)
{
uint8_t i = 0;
uint8_t temp = 0;
uint16_t read_data = 0xFFFF;
for(i=0;i<16;i++)
{
temp = ((pdata&0x8000)==0x8000)? 1:0;
pdata = pdata<<1;
read_data = read_data<<1;
SPI_CLK(0);
SPI_MOSI(temp);
SPI_Delay();
SPI_CLK(1);
SPI_Delay();
if(SPI_MISO()==1)
{
read_data = read_data + 1;
}
}
return read_data;
}
接着就是利用SPI读写函数,对ICM_20608_G进行操作。在读取数据前,需要先初始化、配置ICM_20608_G。初始化的内容比较多,依次为复位、选择时钟、获取ID测试、设置六轴详细参数(分辨率、量程、是否使用滤波器等)、使能、关闭低功耗模式和FIFO等。每个寄存器的功能、设置的值含义,读者可以参考芯片手册的寄存器对应理解,这里不再赘述。
void ICM_Init(void)
{
uint8_t ret = 0;
// 复位ICM
ICM_RW_Register(ICM_PWM_MGMT_1 | ICM_WRITE, 0x80);
HAL_Delay(50);
// 选择时钟
ICM_RW_Register(ICM_PWM_MGMT_1 | ICM_WRITE, 0x01);
HAL_Delay(50);
// 获取设备ID
ret = ICM_RW_Register(ICM_WHO_AM_I | ICM_READ, Dummy_Byte);
printf("\rICM ID: 0x%x\n", ret);
ICM_CS_ENABLE();
// 输出速率:内部采样率
ICM_RW_Register(ICM_SMPLRT_DIV | ICM_WRITE, 0x00);
// 陀螺仪 2000dps
ICM_RW_Register(ICM_GYRO_CONFIG | ICM_WRITE, 0x18);
// 加速度16G量程
ICM_RW_Register(ICM_ACCEL_CONFIG | ICM_WRITE, 0x18);
// 陀螺仪低通滤波 20Hz
ICM_RW_Register(ICM_CONFIG | ICM_WRITE, 0x04);
// 加速度低通滤波 21.2Hz
ICM_RW_Register(ICM_ACCEL_CONFIG_2 | ICM_WRITE, 0x04);
// 打开加速度计和陀螺仪所有轴
ICM_RW_Register(ICM_PWM_MGMT_1 | ICM_WRITE, 0x00);
// 关闭低功耗
ICM_RW_Register(ICM_LP_MODE_CFG | ICM_WRITE, 0x00);
// 关闭FIFO
ICM_RW_Register(ICM_FIFO_EN | ICM_WRITE, 0x00);
}
初始化完后,就可以读取数据,每个数据占两位,读取完后需要高低字节拼接处理。获取到寄存器原始值后,还需要做换算处理,比如温度,手册上写的换算公式为:
而“Room TemperatureOffset”可从手册查到典型值为25℃,“Temp_Sensitivity”典型值为326.8LSB/℃。
对于角速度,前面初始化的时候将量程设置了为±2000dps,读出来的原始数据是带符号的16位,则范围为-32767~+32767,+32767对应+2000,即灵敏度为16.4。
对于加速度,前面初始化的时候将量程设置了为±16g,出来的原始数据是带符号的16位,则范围为-32767~+32767,+32767对应+16,即灵敏度为2048。
void ICM_ReadGyroAccel(void)
{
int16_t temp = 0;
float temp_value = 0;
uint8_t temp_h = 0, temp_l = 0;
int16_t xa = 0, ya = 0, za = 0;
float xa_act = 0, ya_act = 0, za_act = 0;
uint8_t xa_l = 0, ya_l = 0, za_l = 0;
uint8_t xa_h = 0, ya_h = 0, za_h = 0;
int16_t xg = 0, yg = 0, zg = 0;
float xg_act = 0, yg_act = 0, zg_act = 0;
uint8_t xg_l = 0, yg_l = 0, zg_l = 0;
uint8_t xg_h = 0, yg_h = 0, zg_h = 0;
temp_l = ICM_RW_Register(ICM_TEMP_OUT_L | ICM_READ, Dummy_Byte);
temp_h = ICM_RW_Register(ICM_TEMP_OUT_H | ICM_READ, Dummy_Byte);
temp = (temp_h<<8) | temp_l;
temp_value = (temp - 25)/326.8 + 25;
xa_l = ICM_RW_Register(ICM_ACCEL_XOUT_L | ICM_READ, Dummy_Byte);
xa_h = ICM_RW_Register(ICM_ACCEL_XOUT_H | ICM_READ, Dummy_Byte);
ya_l = ICM_RW_Register(ICM_ACCEL_YOUT_L | ICM_READ, Dummy_Byte);
ya_h = ICM_RW_Register(ICM_ACCEL_YOUT_H | ICM_READ, Dummy_Byte);
za_l = ICM_RW_Register(ICM_ACCEL_ZOUT_L | ICM_READ, Dummy_Byte);
za_h = ICM_RW_Register(ICM_ACCEL_ZOUT_H | ICM_READ, Dummy_Byte);
xa = xa_l + (xa_h<<8); xa_act = xa / 2048.0;
ya = ya_l + (ya_h<<8); ya_act = ya / 2048.0;
za = za_l + (za_h<<8); za_act = za / 2048.0;
xg_l = ICM_RW_Register(ICM_GYRO_XOUT_L | ICM_READ, Dummy_Byte);
xg_h = ICM_RW_Register(ICM_GYRO_XOUT_H | ICM_READ, Dummy_Byte);
yg_l = ICM_RW_Register(ICM_GYRO_YOUT_L | ICM_READ, Dummy_Byte);
yg_h = ICM_RW_Register(ICM_GYRO_YOUT_H | ICM_READ, Dummy_Byte);
zg_l = ICM_RW_Register(ICM_GYRO_ZOUT_L | ICM_READ, Dummy_Byte);
zg_h = ICM_RW_Register(ICM_GYRO_ZOUT_H | ICM_READ, Dummy_Byte);
xg = xg_l + (xg_h<<8); xg_act = xg / 16.4;
yg = yg_l + (yg_h<<8); yg_act = yg / 16.4;
zg = zg_l + (zg_h<<8); zg_act = zg / 16.4;
printf("\r-----------------------------------\n");
printf("\rTemperature Original value: 0x%x \n", temp);
printf("\rTemperature Converted value: %.2f\n", temp_value);
printf("\r\n");
printf("\rAccelerometer Original value: xa:0x%x ya:0x%x za:0x%x \n", xa, ya, za);
printf("\rAccelerometer Converted value: xa_act:%.2fg ya_act:%.2fg za_act:%.2fg \n", xa_act, ya_act, za_act);
printf("\r\n");
printf("\rGyroscope Original value: xg:0x%x yg:0x%x zg:0x%x \n", xg, yg, zg);
printf("\rGyroscope Converted value: xg_act:%.2fdeg/s yg_act:%.2fdeg/s zg_act:%.2fdeg/s \n", xg_act, yg_act, zg_act);
printf("\r-----------------------------------\n");
}
最后主函数调用“ICM_ReadGyroAccel()”打印结果。
调试运行,串口输出如图2.18.4所示。先打印出ICM_20608_G的ID为0xAF,再打印温度原始数据和转换数据。接着是三轴加速度传感器(重力感应器)的原始数据和转换数据,其中Z轴的值为1g,即受重力影响,大小为1g,其它两个方向无受力,数值为0,用户将板子立放将会看到数值变化。最后是三轴角速度传感器(陀螺仪)的原始数据和转换数据,开发板目前静置,三个轴方向数值为0,用户晃动开发板,将看到值对应变化。
