简介:这份资源面向嵌入式开发工程师与传感器驱动学习者,聚焦LIS3DH三轴加速度传感器的软硬件驱动开发,解决I2C与SPI两种总线接口下的初始化、读写与数据采集问题。压缩包共139个文件,约6.05MB,以35个h头文件与35个c源文件为核心,涵盖IIC2_Init、IIC2_Start、IIC2_Send_Byte、IIC2_Wait_Ack等完整I2C操作函数,以及lis3dh_driver.c、lis3dh_driver_spi.c等SPI驱动实现;同时附带pdf技术文档、txt说明、hex固件与工程配置文件,便于对照移植。资源还包含STM8S系列外设驱动与工程模板,可帮助读者理解传感器寄存器配置、通信时序与中断处理流程。目前已有506人学习下载,适合需要快速搭建LIS3DH驱动框架、排查总线通信问题的开发者参考。
1. 从一颗 LIS3DH 说起:为什么 IIC 和 SPI 两套驱动都得会写
LIS3DH 是 ST 的一款三轴 MEMS 加速度计,量程可配 ±2g/±4g/±8g/±16g,内置 10 位 ADC、32 级 FIFO、两个可编程中断引脚,供电 1.71V~3.6V,IIC 最高 400kHz、SPI 最高 10MHz。它便宜、好买、资料全,几乎是每个做可穿戴、计步器、倾角检测、振动采集的工程师绕不开的一颗料。但真正上手你会发现,网上能搜到的 LIS3DH 驱动代码要么只给 IIC、要么只给 SPI,寄存器配置散落在各处,读出来数据不对时连从哪查都不知道。这篇笔记就围绕「加速度传感器 LIS3DH 驱动程序及相关技术文档资料,包括 IIC 和 SPI 接口驱动程序源码」这个方向,把两套接口的驱动从寄存器、时序、代码到排错完整走一遍。适合刚拿到传感器模块、准备在 STM32 或 GD32 上跑通第一帧数据的嵌入式工程师,也适合已经能读数据、但被 FIFO 和中断配置卡住的人。
2. LIS3DH 寄存器与接口选型:先把黑匣子拆开
2.1 关键寄存器地图与上电默认状态
LIS3DH 的寄存器不多,但有几个是必须刻在脑子里的。上电后芯片默认处于掉电模式,ODR 为 0,此时读 WHO_AM_I(0x0F)应该返回 0x33,这是判断 IIC/SPI 通信是否打通的第一道关卡。如果这一步读出来是 0x00 或 0xFF,后面所有配置都是白费。
| 寄存器 | 地址 | 作用 | 常用值 |
|---|---|---|---|
| WHO_AM_I | 0x0F | 器件 ID | 固定 0x33 |
| CTRL_REG1 | 0x20 | ODR、轴使能、低功耗 | 0x57(100Hz,XYZ 使能) |
| CTRL_REG2 | 0x21 | 高通滤波 | 0x00 |
| CTRL_REG3 | 0x22 | 中断引脚路由 | 0x10(AOI1 到 INT1) |
| CTRL_REG4 | 0x23 | 量程、BDU、字节序 | 0x88(±2g,BDU=1) |
| CTRL_REG5 | 0x24 | FIFO 使能 | 0x40 |
| FIFO_CTRL_REG | 0x2E | FIFO 模式 | 0x80(Stream 模式) |
| OUT_X_L | 0x28 | X 轴低字节 | 只读 |
CTRL_REG1 的 ODR 位(bit7~4)决定输出数据率,100Hz 对应 0x5,XYZ 使能位(bit2~0)全置 1,合起来就是 0x57。CTRL_REG4 的 BDU 位(bit7)建议置 1,这样高低字节在读取期间不会被新数据刷新,避免读到撕裂的采样值——这是很多人数据跳变的根因。
2.2 IIC 还是 SPI:三个维度做决策
选接口不是拍脑袋,看三个维度就够了。
第一是引脚预算。IIC 只要 SCL、SDA 两根线,还能挂多个从机;SPI 至少四根(SCK、MOSI、MISO、CS),多片选还要额外片选线。板子空间紧张、传感器多,IIC 更省。
第二是速率。LIS3DH 的 IIC 上限 400kHz,SPI 能到 10MHz。如果你要跑 1kHz 以上 ODR 做振动采集,IIC 的带宽会成为瓶颈,SPI 是唯一选择。
第三是抗干扰。IIC 是开漏输出,靠上拉电阻拉高,长走线或强干扰环境下波形容易出现台阶、上升沿变缓;SPI 是推挽输出,驱动能力强,波形干净。工业现场、电机旁边,优先 SPI。
提示:IIC 上拉电阻取值不是玄学。400kHz 下常用 2.2k~4.7k,100kHz 下 4.7k~10k。总线电容越大,上拉要越小,但太小会增加功耗、拉低高电平裕度。用示波器看上升沿,超过 300ns 就该减小电阻。
2.3 硬件连接与地址确定
IIC 模式下,LIS3DH 的 7 位地址由 SA0 引脚决定:SA0 接 GND 是 0x18,接 VDD 是 0x19。注意这是 7 位地址,HAL 库函数要左移一位变成 0x30 或 0x32。很多人卡在这里,扫描不到设备就是因为地址没移位。
SPI 模式下,CS 拉低使能,SCK 空闲为高(CPOL=1),数据在第二个边沿采样(CPHA=1),即模式 3。LIS3DH 也支持模式 0,但模式 3 更稳。SPI 读操作时,地址最高位要置 1 表示读,写操作最高位为 0。比如读 WHO_AM_I(0x0F),发送的字节是 0x8F | 0x00 = 0x8F;写 CTRL_REG1(0x20),发送 0x20。
3. IIC 驱动实现:从裸机时序到 HAL 库封装
3.1 用 HAL 库跑通最小读取流程
先给一段能直接编译的初始化加读取代码,基于 STM32 HAL 库,IIC 地址按 SA0 接 GND 计算。
#include "stm32f4xx_hal.h" #define LIS3DH_IIC_ADDR (0x18 << 1) // 7位地址左移,HAL库要求 #define WHO_AM_I_REG 0x0F #define CTRL_REG1 0x20 #define CTRL_REG4 0x23 #define OUT_X_L 0x28 I2C_HandleTypeDef hi2c1; uint8_t LIS3DH_Init(void) { uint8_t id = 0; uint8_t data; // 1. 读WHO_AM_I确认通信 HAL_I2C_Mem_Read(&hi2c1, LIS3DH_IIC_ADDR, WHO_AM_I_REG, I2C_MEMADD_SIZE_8BIT, &id, 1, 100); if (id != 0x33) { return 1; // 通信失败 } // 2. CTRL_REG1: 100Hz, XYZ使能 data = 0x57; HAL_I2C_Mem_Write(&hi2c1, LIS3DH_IIC_ADDR, CTRL_REG1, I2C_MEMADD_SIZE_8BIT, &data, 1, 100); // 3. CTRL_REG4: ±2g, BDU=1, 高字节在后 data = 0x88; HAL_I2C_Mem_Write(&hi2c1, LIS3DH_IIC_ADDR, CTRL_REG4, I2C_MEMADD_SIZE_8BIT, &data, 1, 100); HAL_Delay(10); // 等待第一个采样周期 return 0; } void LIS3DH_ReadAccel(int16_t *x, int16_t *y, int16_t *z) { uint8_t buf[6]; // 连续读6字节,地址自动递增 HAL_I2C_Mem_Read(&hi2c1, LIS3DH_IIC_ADDR, OUT_X_L | 0x80, I2C_MEMADD_SIZE_8BIT, buf, 6, 100); *x = (int16_t)((buf[1] << 8) | buf[0]); *y = (int16_t)((buf[3] << 8) | buf[2]); *z = (int16_t)((buf[5] << 8) | buf[4]); }逻辑说明:HAL_I2C_Mem_Read 的第三个参数是寄存器地址,第四个参数指定地址长度 8 位。读多字节时,LIS3DH 要求寄存器地址最高位置 1 才能自动递增,所以 OUT_X_L | 0x80。数据拼接时低字节在前、高字节在后,这是 LIS3DH 默认的 little-endian 输出格式,CTRL_REG4 的 BLE 位可以改,但没必要动。
参数说明:超时时间给 100ms 足够,IIC 400kHz 下读 6 字节不到 200us。如果返回 HAL_TIMEOUT,先查上拉电阻和地址。HAL_Delay(10) 不能省,ODR 刚使能时第一个数据还没准备好,立刻读会得到全 0。
3.2 裸机 IIC 时序:当 HAL 库不够用时
有些项目不能用 HAL,或者 IIC 外设被占用,就得用 GPIO 模拟。核心是四个时序:起始、停止、发送字节、接收字节。
// 软件IIC,SCL和SDA都配置为开漏输出 #define SCL_H() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET) #define SCL_L() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET) #define SDA_H() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET) #define SDA_L() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET) #define SDA_READ() HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_7) void IIC_Start(void) { SDA_H(); SCL_H(); delay_us(2); SDA_L(); delay_us(2); // SCL高时SDA拉低 = 起始 SCL_L(); delay_us(2); } void IIC_Stop(void) { SDA_L(); SCL_H(); delay_us(2); SDA_H(); delay_us(2); // SCL高时SDA拉高 = 停止 } uint8_t IIC_WriteByte(uint8_t byte) { for (int i = 0; i < 8; i++) { if (byte & 0x80) SDA_H(); else SDA_L(); byte <<= 1; delay_us(1); SCL_H(); delay_us(2); SCL_L(); delay_us(1); } SDA_H(); delay_us(1); // 释放SDA等待ACK SCL_H(); delay_us(2); uint8_t ack = SDA_READ(); // 0=ACK, 1=NACK SCL_L(); delay_us(1); return ack; }逻辑说明:开漏输出配合外部上拉,才能实现线与。delay_us 的具体值取决于主频,168MHz 下 2us 对应约 336 个时钟周期,足够 100kHz 时序。如果跑 400kHz,把延时压到 0.5us 左右,但要注意 GPIO 翻转速度。
参数说明:SDA 在 SCL 高电平期间必须保持稳定,变化只能发生在 SCL 低电平期间,这是 IIC 协议的铁律。软件模拟最容易翻车的地方就是延时不够导致时序违例,示波器上看 SCL 和 SDA 的相位关系一目了然。
3.3 IIC 波形排查:台阶和丢 ACK 怎么定位
IIC 波形出现台阶,九成是上拉电阻太大或总线电容太大。上升沿是 RC 充电曲线,R 越大、C 越大,到达高电平阈值的时间越长。400kHz 下如果上升沿超过 300ns,从机可能还没看到高电平就被拉低了,直接丢数据。
排查步骤:先把示波器探头接 SCL,看上升时间。如果超过 300ns,把上拉从 10k 换到 4.7k 或 2.2k。如果换了还不行,查总线上挂了多少设备,每个设备的引脚电容加起来可能超过 200pF。再不行就降低速率到 100kHz,或者换 SPI。
丢 ACK 的另一个常见原因是地址错误。用逻辑分析仪抓一帧,看第一个字节是不是 0x30(写)或 0x31(读)。如果是 0x18 或 0x19,说明忘了左移。
4. SPI 驱动实现:模式、片选与 DMA 加速
4.1 SPI 模式配置与单字节读写
LIS3DH 的 SPI 用模式 3(CPOL=1,CPHA=1),MSB 在前,8 位数据。片选可以用硬件 NSS,也可以用软件 GPIO 控制。硬件片选省事,但多从机时容易冲突;软件片选灵活,推荐用。
#define LIS3DH_CS_L() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define LIS3DH_CS_H() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) uint8_t LIS3DH_SPI_ReadReg(uint8_t reg) { uint8_t tx = reg | 0x80; // 最高位置1表示读 uint8_t rx = 0; LIS3DH_CS_L(); HAL_SPI_TransmitReceive(&hspi1, &tx, &rx, 1, 100); LIS3DH_CS_H(); return rx; } void LIS3DH_SPI_WriteReg(uint8_t reg, uint8_t val) { uint8_t tx[2] = { reg & 0x7F, val }; // 最高位清0表示写 LIS3DH_CS_L(); HAL_SPI_Transmit(&hspi1, tx, 2, 100); LIS3DH_CS_H(); }逻辑说明:读操作时,第一个字节发送地址(最高位置 1),第二个字节发送任意值(通常 0x00),接收到的就是寄存器内容。写操作时,第一个字节是地址(最高位清 0),第二个字节是数据。CS 必须在整个传输期间保持低电平,传输完再拉高。
参数说明:HAL_SPI_TransmitReceive 的超时给 100ms。SPI 时钟预分频要保证不超过 10MHz,STM32F4 的 APB2 是 84MHz,分频 16 得到 5.25MHz,安全。如果通信失败,先查 CPOL/CPHA 是否配成模式 3,再查 CS 是否接对。
4.2 连续读取与 DMA 搬运
读三轴数据要连续读 6 个字节,用 DMA 可以解放 CPU。配置好 DMA 通道后,一次触发搬完。
uint8_t spi_rx_buf[6]; void LIS3DH_ReadAccel_DMA(int16_t *x, int16_t *y, int16_t *z) { uint8_t tx = OUT_X_L | 0x80 | 0x40; // 读+自动递增 LIS3DH_CS_L(); HAL_SPI_Transmit(&hspi1, &tx, 1, 100); // 先发地址 HAL_SPI_Receive_DMA(&hspi1, spi_rx_buf, 6); // DMA收6字节 // 等待DMA完成回调,或在回调里处理数据 } // DMA完成回调 void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { LIS3DH_CS_H(); int16_t x = (int16_t)((spi_rx_buf[1] << 8) | spi_rx_buf[0]); int16_t y = (int16_t)((spi_rx_buf[3] << 8) | spi_rx_buf[2]); int16_t z = (int16_t)((spi_rx_buf[5] << 8) | spi_rx_buf[4]); // 处理数据 }逻辑说明:地址字节的 bit6(0x40)是自动递增使能位,SPI 模式下必须置 1 才能连续读。先发地址再启动 DMA 接收,中间 CS 不能拉高。DMA 完成回调里再拉高 CS,保证时序完整。
参数说明:DMA 优先级建议设中或高,避免被其他中断打断导致 CS 提前拉高。如果数据错位,检查 DMA 的传输方向和外设数据宽度是否都是 8 位。
4.3 软件片选与硬件片选的取舍
硬件片选由 SPI 外设自动控制,时序精准,但一根 NSS 只能对应一个从机。多从机时要么用片选译码器,要么改用软件片选。软件片选就是普通 GPIO,代码里手动拉低拉高,灵活但要注意在传输前后加延时,给从机反应时间。
常见做法是:单从机用硬件片选,多从机用软件片选。如果 SPI 总线上挂了 Flash 和 LIS3DH,Flash 用硬件片选,LIS3DH 用软件片选,互不干扰。
5. 避坑与排查:LIS3DH 驱动最容易翻车的五个地方
5.1 读 WHO_AM_I 返回 0x00 或 0xFF
现象:初始化时读 0x0F 寄存器,得到 0x00 或 0xFF,后续配置全部无效。
原因:IIC 地址没左移、上拉电阻缺失、SPI 模式配错、CS 没拉低。0x00 通常是设备没响应,0xFF 通常是 MISO 一直被拉高。
解决:先用逻辑分析仪抓一帧,看地址字节对不对。IIC 地址 0x18 左移后是 0x30,读操作是 0x31。SPI 检查 CPOL=1、CPHA=1,CS 在传输期间为低。上拉电阻用 4.7k 先试。
5.2 数据跳变、偶尔出现极大值
现象:静止时读出的加速度值偶尔跳到几千甚至上万,换算成 g 超过量程。
原因:CTRL_REG4 的 BDU 位没置 1,读低字节和高字节之间数据被刷新,拼出一个错误值。
解决:把 CTRL_REG4 的 bit7 置 1,即写入 0x88。这样读取期间输出寄存器冻结,读完自动更新。代价是数据更新率略降,但换来稳定性。
5.3 FIFO 溢出或读不到数据
现象:配置了 FIFO 但读出来全是 0,或者 FIFO_SRC 显示溢出。
原因:FIFO_CTRL_REG 模式设错,或者 ODR 和 FIFO 模式不匹配。Stream 模式下 FIFO 满了会覆盖旧数据,FIFO 模式满了就停止。
解决:先用 Bypass 模式确认基本读取正常,再切到 FIFO 模式。FIFO_CTRL_REG 写 0x80 是 Stream,0x40 是 FIFO。读 FIFO 时连续读 OUT_X_L 六字节,FIFO 指针自动递增。
5.4 中断引脚不触发
现象:配置了 AOI1 到 INT1,但运动时 INT1 一直是低。
原因:CTRL_REG3 的路由位没设对,或者 CTRL_REG1 的轴使能没开。中断源要同时满足阈值和持续时间。
解决:CTRL_REG3 写 0x10 把 AOI1 路由到 INT1。INT1_CFG(0x30)设 0x2A 使能 X/Y/Z 高事件。INT1_THS(0x32)设阈值,INT1_DURATION(0x33)设持续时间。先用最小阈值测试,确认中断能触发再调大。
5.5 SPI 高速下数据错位
现象:SPI 时钟超过 5MHz 后,读出的数据偶尔错位或全错。
原因:走线太长、没有阻抗匹配、CS 拉高太早。
解决:降低 SPI 时钟到 1MHz 先验证功能,再逐步提高。CS 拉高前确保最后一个时钟沿已经完成,可以在 HAL_SPI_TransmitReceive 返回后加一个 __NOP() 再拉高。走线超过 10cm 时加串联电阻 22~100 欧姆。
6. 进阶技巧:用 FIFO 加水印中断做低功耗采集
前面把基础驱动跑通了,但真正做产品时,CPU 不能一直轮询读数据,太费电。LIS3DH 的 FIFO 加水印中断就是为低功耗场景设计的:让传感器自己攒数据,攒到一定数量再叫醒 CPU 一次性搬走。
配置思路是这样的。先把 FIFO_CTRL_REG 设为 Stream 模式(0x80),FIFO 深度 32 级。然后设置水印阈值,FIFO_CTRL_REG 的低 5 位就是水印值,比如设 16,写 0x80 | 16 = 0x90。接着把 FIFO 水印中断路由到 INT1,CTRL_REG3 写 0x04(FTH 到 INT1)。最后在 CTRL_REG5 里使能 FIFO(bit6 置 1),写 0x40。
void LIS3DH_ConfigFIFO_Watermark(void) { uint8_t data; // 1. 使能FIFO data = 0x40; LIS3DH_SPI_WriteReg(0x24, data); // CTRL_REG5 // 2. Stream模式,水印16 data = 0x80 | 16; LIS3DH_SPI_WriteReg(0x2E, data); // FIFO_CTRL_REG // 3. 水印中断路由到INT1 data = 0x04; LIS3DH_SPI_WriteReg(0x22, data); // CTRL_REG3 }中断触发后,读 FIFO_SRC(0x2F)看 FSS 位,确认攒了多少个样本。然后连续读 OUT_X_L 六字节乘以样本数,一次性搬完。搬完后 FIFO 自动清空,继续攒下一批。
这个方案的价值在于:100Hz ODR 下,水印 16 意味着每 160ms 才中断一次 CPU,其余时间 MCU 可以进 STOP 模式,平均功耗从毫安级降到微安级。做手环、计步器、资产追踪标签,这是标配。
验证方法:用示波器接 INT1,看中断周期是不是 160ms。如果不对,查 ODR 配置和水印值。再用逻辑分析仪抓 SPI 读 FIFO 的波形,确认一次读了 16×6=96 字节。数据在内存里按 XYZXYZ 排列,解析时每 6 字节一组。
我自己的习惯是,每次调 LIS3DH 都先把 WHO_AM_I 读通,再配 ODR,再读单次数据,最后才上 FIFO 和中断。跳过任何一步,后面出问题都得回头补课。希望帮到你。
本文还有配套的精品资源,点击获取