1. 初识LIS3DH与SPI通信

大家好,今天我来分享一下如何使用STM32的HAL库通过SPI接口驱动LIS3DH加速度计。LIS3DH是ST公司推出的一款超低功耗三轴数字加速度计,广泛应用于各种嵌入式系统中。它支持IIC和SPI两种通信方式,在实际项目中,SPI方式因为通信速率高、抗干扰能力强,经常被选用。

我在最近的一个项目中就遇到了需要驱动LIS3DH的情况。IIC方式很快就调通了,但SPI方式却折腾了我整整两天时间。主要问题出在SPI的时序配置和寄存器读写细节上。经过反复试验和查阅资料,终于找到了正确的配置方法,现在把这些经验分享给大家,希望能帮助遇到同样问题的朋友少走弯路。

SPI通信需要四根线:SCK(时钟线)、MOSI(主设备数据输出)、MISO(主设备数据输入)和CS(片选信号)。与IIC相比,SPI的硬件连接稍微复杂一些,但通信速率更高,适合对实时性要求较高的应用场景。LIS3DH的SPI接口支持标准4线制和3线制两种模式,我们这里主要讨论常用的4线制SPI通信。

2. 硬件连接与SPI基础配置

2.1 硬件连接要点

在我的项目中,使用的是STM32L051C8T6芯片,SPI1接口。具体引脚分配如下:PA4作为片选信号CS,PA5作为时钟线SCK,PA6作为数据输出MISO,PA7作为数据输入MOSI。这里需要注意的是,CS引脚可以任意选择普通的GPIO口,不一定要使用SPI硬件自带的NSS引脚,这样在软件控制上更加灵活。

硬件连接时要注意电平匹配问题。LIS3DH的工作电压是1.8V到3.6V,而STM32的IO口通常是3.3V电平,所以直接连接没有问题。如果使用其他电压等级的MCU,可能需要电平转换电路。另外,SPI信号的走线要尽量短,避免过长的走线引入干扰,特别是在高速通信时更要注意这一点。

2.2 SPI模式配置关键

SPI通信中最关键的就是CPOL和CPHA这两个参数的配置。CPOL决定时钟空闲时的电平状态:CPOL=0表示空闲时SCK为低电平,CPOL=1表示空闲时SCK为高电平。CPHA决定数据采样的时刻:CPHA=0表示在第一个时钟边沿采样数据,CPHA=1表示在第二个时钟边沿采样数据。

查阅LIS3DH的数据手册可以发现,它要求CPOL=1,CPHA=1,也就是SPI模式3。这个配置非常重要,如果配置错误,通信就无法正常进行。在STM32CubeMX中,我们需要将Clock Polarity设置为High,Clock Phase设置为2 Edge,这样就正确配置为模式3了。

除了模式配置,还需要设置SPI的时钟分频。LIS3DH支持最高10MHz的SPI时钟频率,我们可以根据实际需求设置合适的时钟分频系数。在我的项目中,系统时钟是32MHz,我将SPI时钟设置为4分频,即8MHz,这个速率既保证了通信速度,又留有一定的余量。

3. CubeMX配置详解

3.1 引脚功能配置

打开STM32CubeMX,首先配置SPI1接口。在Pinout视图中,找到SPI1,选择Full-Duplex Master模式。这样就会自动分配PA5、PA6、PA7分别作为SPI1的SCK、MISO和MOSI引脚。需要注意的是,这里我们使用软件控制片选信号,所以不需要使能硬件NSS功能。

接下来配置CS引脚。我选择PA4作为片选信号,在Pinout视图中将PA4配置为GPIO_Output模式。这样我们就可以在软件中通过控制PA4的电平来控制片选信号了。片选信号的低电平有效,当CS为低电平时,LIS3DH被选中,可以开始通信;当CS为高电平时,LIS3DH处于未选中状态。

除了SPI相关的引脚,还需要配置系统时钟。根据实际使用的晶振频率,在Clock Configuration中设置正确的时钟树。确保系统时钟、APB总线时钟等都正确配置,特别是SPI的时钟源要正确选择。在我的配置中,使用内部HSI 16MHz时钟作为系统时钟源,经过PLL倍频到32MHz。

3.2 SPI参数详细设置

在Configuration标签页中,点击SPI1进入参数配置界面。首先设置Basic Parameters:Mode选择Full-Duplex Master,这是最常用的SPI主设备全双工模式。Data Size选择8 bits,因为LIS3DH的寄存器读写都是按8位进行的。

Clock Parameters中,First Bit选择MSB First,这是SPI通信的标准配置。Baud Rate设置时钟分频,我选择4分频,得到8MHz的SPI时钟。Clock Polarity选择High,Clock Phase选择2 Edge,这就是前面说的SPI模式3的配置。

在Advanced Parameters中,CRC Calculation选择Disable,因为我们不需要CRC校验。NSS Signal Type选择Software,这样就是软件控制片选信号。其他参数保持默认值即可。配置完成后,点击Generate Code生成工程代码。

4. HAL库SPI驱动实现

4.1 初始化代码分析

生成的代码中,SPI的初始化主要在spi.c文件中。我们来看看关键的初始化函数:

void MX_SPI1_Init(void)
{
  hspi1.Instance = SPI1;
  hspi1.Init.Mode = SPI_MODE_MASTER;
  hspi1.Init.Direction = SPI_DIRECTION_2LINES;
  hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
  hspi1.Init.CLKPolarity = SPI_POLARITY_HIGH;
  hspi1.Init.CLKPhase = SPI_PHASE_2EDGE;
  hspi1.Init.NSS = SPI_NSS_SOFT;
  hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4;
  hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
  hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
  hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
  hspi1.Init.CRCPolynomial = 7;
  if (HAL_SPI_Init(&hspi1) != HAL_OK)
  {
    Error_Handler();
  }
}

这个函数配置了SPI1为主设备模式、全双工、8位数据、模式3、软件NSS控制、4分频等参数,与我们在CubeMX中的配置完全一致。HAL_SPI_Init函数会根据这些配置初始化SPI外设。

GPIO的初始化在HAL_SPI_MspInit函数中,这个函数由HAL库自动调用。它配置了SPI引脚的复用功能和GPIO参数:

void HAL_SPI_MspInit(SPI_HandleTypeDef* hspi)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if(hspi->Instance==SPI1)
  {
    __HAL_RCC_SPI1_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();
    
    GPIO_InitStruct.Pin = GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF0_SPI1;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
  }
}

4.2 片选信号控制

由于我们使用软件控制片选,需要自己编写CS引脚的控制函数。我在main.h中定义了片选控制的宏:

#define LIS3DH_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET)
#define LIS3DH_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET)

在实际使用中,每次SPI通信前拉低CS,通信完成后拉高CS。注意CS信号的控制要严格遵循时序要求,特别是在高速通信时,CS信号的建立时间和保持时间都要满足芯片要求。

5. LIS3DH寄存器读写实战

5.1 读写函数实现

LIS3DH的SPI通信有特定的时序要求。写寄存器时,先发送寄存器地址(最高位为0表示写),然后发送要写入的数据。读寄存器时,先发送寄存器地址(最高位为1表示读),然后读取返回的数据。

我编写了读写单个寄存器的函数:

uint8_t LIS3DH_ReadReg(uint8_t reg)
{
  uint8_t tx_data[2] = {reg | 0x80, 0x00};  // 设置最高位为1表示读
  uint8_t rx_data[2] = {0};
  
  LIS3DH_CS_LOW();
  HAL_SPI_TransmitReceive(&hspi1, tx_data, rx_data, 2, 100);
  LIS3DH_CS_HIGH();
  
  return rx_data[1];
}

void LIS3DH_WriteReg(uint8_t reg, uint8_t value)
{
  uint8_t tx_data[2] = {reg & 0x7F, value};  // 设置最高位为0表示写
  
  LIS3DH_CS_LOW();
  HAL_SPI_Transmit(&hspi1, tx_data, 2, 100);
  LIS3DH_CS_HIGH();
}

这里使用了HAL_SPI_TransmitReceive函数来实现读写同时进行。注意读操作时,第一个字节是发送的寄存器地址(最高位置1),第二个字节是 dummy 数据,用于产生时钟信号;返回的数据中,第一个字节是无效的,第二个字节才是真正读取的数据。

5.2 设备ID读取与验证

读取LIS3DH的WHO_AM_I寄存器是验证通信是否正常的最直接方法。这个寄存器的地址是0x0F,固定返回值是0x33。

uint8_t who_am_i = LIS3DH_ReadReg(0x0F);
if(who_am_i == 0x33)
{
  printf("LIS3DH detected successfully!\n");
}
else
{
  printf("LIS3DH detection failed, read: 0x%02X\n", who_am_i);
}

如果读出的值不是0x33,而是0xFF或其他值,说明SPI通信有问题。常见的原因包括:SPI模式配置错误、片选信号控制不当、硬件连接问题等。这时候需要仔细检查配置和连接。

6. 加速度计功能配置

6.1 工作模式设置

LIS3DH有多种工作模式可供选择,包括低功耗模式、正常模式、高分辨率模式等。通过CTRL_REG1(0x20)寄存器可以配置数据速率和功耗模式。

我通常这样配置:选择正常模式,输出数据速率100Hz,使能所有轴:

// 设置CTRL_REG1:正常模式,100Hz,XYZ轴使能
LIS3DH_WriteReg(0x20, 0x57);

CTRL_REG4(0x23)寄存器用于设置量程和分辨率。LIS3DH支持±2g、±4g、±8g、±16g四个量程,根据实际应用需求选择合适的量程:

// 设置CTRL_REG4:±2g量程,高分辨率模式
LIS3DH_WriteReg(0x23, 0x08);

6.2 数据读取处理

加速度数据存储在OUT_X_L(0x28)、OUT_X_H(0x29)、OUT_Y_L(0x2A)、OUT_Y_H(0x2B)、OUT_Z_L(0x2C)、OUT_Z_H(0x2D)这六个寄存器中。由于数据是16位有符号数,需要将高低字节组合起来:

int16_t LIS3DH_ReadAccelX(void)
{
  uint8_t xlow = LIS3DH_ReadReg(0x28);
  uint8_t xhigh = LIS3DH_ReadReg(0x29);
  return (int16_t)((xhigh << 8) | xlow);
}

// Y轴和Z轴的读取函数类似

为了提高效率,也可以一次性读取所有三个轴的数据:

void LIS3DH_ReadAccelAll(int16_t *x, int16_t *y, int16_t *z)
{
  uint8_t data[6];
  uint8_t reg = 0x28 | 0x80 | 0x40;  // 读操作,多字节读取
  
  LIS3DH_CS_LOW();
  HAL_SPI_Transmit(&hspi1, &reg, 1, 100);
  HAL_SPI_Receive(&hspi1, data, 6, 100);
  LIS3DH_CS_HIGH();
  
  *x = (int16_t)((data[1] << 8) | data[0]);
  *y = (int16_t)((data[3] << 8) | data[2]);
  *z = (int16_t)((data[5] << 8) | data[4]);
}

这里使用了多字节读取功能,通过设置地址字节的 bit 6(0x40)来启用地址自动递增,这样可以一次性读取连续的多个寄存器。

7. 原始数据到物理量转换

7.1 量程换算

读取到的原始加速度数据是16位有符号整数,需要根据设置的量程转换为实际的物理量(g值)。换算公式如下:

实际加速度值 = (原始数据 × 量程) / 32768

例如,当量程设置为±2g时,灵敏度为16384 LSB/g,换算公式为:

加速度值(g) = 原始数据 / 16384.0

代码实现:

float LIS3DH_ConvertToG(int16_t raw, uint8_t range)
{
  float sensitivity;
  switch(range)
  {
    case 0: // ±2g
      sensitivity = 16384.0;
      break;
    case 1: // ±4g
      sensitivity = 8192.0;
      break;
    case 2: // ±8g
      sensitivity = 4096.0;
      break;
    case 3: // ±16g
      sensitivity = 2048.0;
      break;
    default:
      sensitivity = 16384.0;
  }
  return raw / sensitivity;
}

7.2 角度计算应用

加速度计的一个重要应用是倾角测量。通过三个轴的加速度值,可以计算出设备相对于重力方向的倾斜角度。以X轴为例,倾斜角度的计算公式为:

角度 = atan2(X, sqrt(YY + ZZ)) × 180 / π

代码实现:

#include <math.h>

float LIS3DH_CalculatePitch(int16_t x, int16_t y, int16_t z, uint8_t range)
{
  float fx = LIS3DH_ConvertToG(x, range);
  float fy = LIS3DH_ConvertToG(y, range);
  float fz = LIS3DH_ConvertToG(z, range);
  
  return atan2(fx, sqrt(fy*fy + fz*fz)) * 180.0 / M_PI;
}

float LIS3DH_CalculateRoll(int16_t x, int16_t y, int16_t z, uint8_t range)
{
  float fx = LIS3DH_ConvertToG(x, range);
  float fy = LIS3DH_ConvertToG(y, range);
  float fz = LIS3DH_ConvertToG(z, range);
  
  return atan2(fy, sqrt(fx*fx + fz*fz)) * 180.0 / M_PI;
}

需要注意的是,这种计算方法在设备静止时比较准确,如果设备在运动,由于存在线性加速度,计算出的角度会有误差。

8. 调试技巧与常见问题

8.1 调试工具使用

在调试SPI通信时,逻辑分析仪是非常有用的工具。通过逻辑分析仪可以直观地看到SPI的时序波形,包括时钟极性、相位、数据内容等,帮助快速定位问题。

如果没有逻辑分析仪,也可以使用STM32的串口输出调试信息。比如在关键位置添加打印语句,输出读取的寄存器值、状态信息等:

printf("WHO_AM_I value: 0x%02X\n", LIS3DH_ReadReg(0x0F));

另一个有用的调试方法是回环测试。将MOSI和MISO短接,然后发送数据并接收,看是否能正确回传。这样可以排除外设本身的问题,专注于SPI配置的调试。

8.2 常见问题解决

问题1:读取的值始终是0xFF 这通常表示SPI通信完全没有建立。可能的原因包括:片选信号没有正确控制、SPI模式配置错误、硬件连接问题等。建议检查CS信号的电平变化,确认SPI模式与LIS3DH要求的一致。

问题2:读取的值不正确但非0xFF 这可能表示时序或相位问题。检查CPOL和CPHA的设置是否正确,时钟频率是否过高。可以尝试降低SPI时钟频率,看看问题是否改善。

问题3:数据跳动较大 加速度计数据本身会有一定的噪声,这是正常现象。可以通过软件滤波来平滑数据,比如移动平均滤波或卡尔曼滤波。也可以调整LIS3DH内置的滤波器设置。

问题4:功耗过高 如果使用电池供电,需要注意功耗问题。在不需要高速数据采集时,可以降低输出数据速率,或让LIS3DH进入低功耗模式。

我在实际项目中就遇到过数据跳动大的问题,后来发现是电源噪声导致的。在加速度计的电源引脚增加了滤波电容后,数据稳定性明显改善。另一个常见问题是地线干扰,确保数字地和模拟地良好连接,必要时使用磁珠隔离。

调试SPI设备需要有耐心,一步一步排除问题。先从最简单的读取设备ID开始,确保基本通信正常,然后再逐步配置更复杂的功能。遇到问题时,不要急于修改多个参数,应该每次只修改一个参数,观察效果,这样才能准确找到问题根源。

更多推荐