ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

LIS3DH三轴加速度计驱动实战:I2C与SPI双接口配置及避坑指南

LIS3DH三轴加速度计驱动实战:I2C与SPI双接口配置及避坑指南

简介:这份资源面向嵌入式开发初学者与需要快速集成三轴加速度计的工程师,围绕LIS3DH传感器提供完整的软硬件驱动方案,重点解决I2C与SPI两种总线接口下的初始化、读写与数据采集问题。压缩包共139个文件,约6.05MB,以35个h头文件与35个c源文件为核心,辅以o、cout、pbi等编译中间文件,以及pdf、txt技术文档和hex固件,覆盖驱动源码、工程配置与说明资料。资源中已包含IIC2_Init、IIC2_Start、IIC2_Send_Byte、IIC2_Read_Byte、IIC2_Wait_Ack等完整I2C操作函数,并配套lis3dh_driver.c与lis3dh_driver_spi.c等SPI驱动实现,便于读者直接移植到STM8等平台。目前已有506人学习下载,适合需要快速验证传感器通信、理解总线时序与寄存器配置的开发者参考。

1. 从一颗 LIS3DH 说起:为什么 I2C 和 SPI 两套驱动都得攥在手里

前阵子帮朋友调一块三轴加速度计板子,现象很邪门:I2C 能读出 WHO_AM_I 是 0x33,可一读加速度数据就全是 0,换成 SPI 接法立马正常。折腾半天才发现是 I2C 上拉电阻选得不对,波形上升沿塌成了台阶。这件事让我重新翻出硬盘里那份 LIS3DH 驱动资料包——它同时给了 I2C 和 SPI 两套源码,还有 STM8S 的定时器文件,正好能把这类玄学问题一次讲透。LIS3DH 是 ST 家的经典三轴加速度传感器,量程可配 ±2g 到 ±16g,支持 I2C 和 SPI 两种数字接口,低功耗模式下电流只有微安级,在计步、姿态检测、振动监测里出镜率极高。这份资源包的价值不在于代码多长,而在于它把两种接口的底层时序都摊开给你看,还附带了 STM8S 平台的工程文件,适合刚接触嵌入式传感器驱动的新手照着复现,也适合老手拿来对照排查时序问题。下面我就按「这是什么 → 怎么用 → 坑在哪」的顺序,把这份资料拆开讲。

2. LIS3DH 驱动包的骨架:文件构成与接口选型逻辑

2.1 压缩包里到底有什么

先把包里的东西理清楚,不然拿到一堆 .c 和 .h 容易懵。从项目正文看,核心文件分三类:LIS3DH 驱动层(lis3dh_driver.c、lis3dh_driver_spi.c)、STM8S 外设层(stm8s_tim1.c、stm8s_tim5.c)、以及工程调试残留文件(SPI_Flash.Debug.cspy.bat、SPI_Flash.pbd.browse 这类)。驱动层是重点,它把寄存器操作和接口时序分开了:lis3dh_driver.c 负责芯片本身的初始化、量程配置、数据读取,lis3dh_driver_spi.c 则专门处理 SPI 的字节收发。这种分层写法很实用,换平台时只改接口文件,寄存器逻辑不用动。

文件作用是否需移植
lis3dh_driver.c寄存器读写、量程/速率配置、数据解析否,逻辑通用
lis3dh_driver_spi.cSPI 字节收发、片选控制是,依赖 MCU
stm8s_tim1.c / tim5.c定时器,用于周期采样是,平台相关
SPI_Flash.*.bat / .browse调试工程残留可删

注意:那些 .bat 和 .browse 是 IDE 生成的临时文件,不影响驱动逻辑,移植时直接忽略。真正要盯的是驱动层里对 WHO_AM_I(0x0F)的读取,这是判断通信是否打通的第一道关卡。

2.2 I2C 还是 SPI:先看板子再选

选接口不是拍脑袋,得看三个现实条件。第一是引脚数:I2C 只要 SCL、SDA 两根线,SPI 至少要 SCLK、MOSI、MISO、CS 四根,引脚紧张时 I2C 占优。第二是速率:LIS3DH 的 I2C 最高 400kHz,SPI 能到 10MHz,要高频采样(比如振动分析)SPI 更稳。第三是总线冲突:I2C 是共享总线,挂多个器件时地址不能撞,LIS3DH 的 SAD 引脚决定地址是 0x18 还是 0x19;SPI 每片一个 CS,互不干扰。

我一般这么定:慢速姿态检测、引脚少,走 I2C;高速数据采集、总线上还有别的 SPI 器件,走 SPI。这份资料两套都给,正好可以按项目切换。下面分别讲两套接口的落地步骤。

3. I2C 接口驱动落地:从 IO 模拟到数据读取

3.1 软件模拟 I2C 的时序骨架

资料里的 I2C 部分用的是 IO 模拟,摘要里列了一串函数:IIC2_Init、IIC2_Start、IIC2_Stop、IIC2_Send_Byte、IIC2_Read_Byte、IIC2_Wait_Ack、IIC2_Ack、IIC2_NAck。这套函数是标准软件 I2C 的骨架,好处是不挑 MCU,任何有普通 IO 的芯片都能跑。先看初始化和起始信号:

// 初始化 I2C 的 IO 口,配置为开漏输出 void IIC2_Init(void) { GPIO_Init(GPIOB, GPIO_PIN_6 | GPIO_PIN_7, GPIO_MODE_OUT_OD_HIZ_FAST); // SCL=PB6, SDA=PB7 } // 发送 I2C 起始信号:SCL 高时 SDA 由高变低 void IIC2_Start(void) { SDA_HIGH(); // 先拉高数据线 SCL_HIGH(); // 再拉高时钟线 delay_us(2); SDA_LOW(); // SCL 保持高时拉低 SDA,产生起始条件 delay_us(2); SCL_LOW(); // 钳住总线,准备传输 }

逻辑说明:I2C 的起始条件是 SCL 为高时 SDA 出现下降沿,停止条件相反。delay_us(2) 是给信号建立时间,具体值看 MCU 主频,太快会导致从机来不及响应。参数上,SCL 和 SDA 必须配成开漏输出,否则无法实现线与,多设备挂载时会烧引脚。

3.2 字节收发与 ACK 处理

字节收发是 I2C 最容易翻车的地方,核心是「高位先发、每字节跟一个 ACK」。发送一个字节的写法:

// I2C 发送一个字节,高位先发 void IIC2_Send_Byte(uint8_t txd) { uint8_t t; SDA_OUT(); // 切换 SDA 为输出 SCL_LOW(); // 拉低时钟,准备放数据 for (t = 0; t < 8; t++) { if (txd & 0x80) SDA_HIGH(); else SDA_LOW(); txd <<= 1; // 左移,准备下一位 delay_us(2); SCL_HIGH(); // 时钟拉高,从机采样 delay_us(2); SCL_LOW(); // 时钟拉低,准备下一位 delay_us(2); } }

逻辑说明:循环 8 次,每次先放数据再拉高 SCL,从机在 SCL 高电平期间采样。发送完 8 位后要调用 IIC2_Wait_Ack 等从机拉低 SDA 应答。如果 Wait_Ack 返回 1,说明从机没应答,常见原因是地址写错或器件没上电。参数上,txd 是要发的字节,移位方向必须是左移,因为 I2C 规定 MSB 先行。

3.3 读 LIS3DH 加速度数据的完整流程

通信打通后,读数据就三步:写寄存器地址、重启始条件、读字节。以读 WHO_AM_I 和 OUT_X_L 为例:

// 读 LIS3DH 单个寄存器 uint8_t LIS3DH_Read_Reg(uint8_t reg) { uint8_t val; IIC2_Start(); IIC2_Send_Byte(LIS3DH_ADDR << 1); // 写地址 IIC2_Wait_Ack(); IIC2_Send_Byte(reg); // 寄存器地址 IIC2_Wait_Ack(); IIC2_Start(); // 重启始条件 IIC2_Send_Byte((LIS3DH_ADDR << 1) | 0x01); // 读地址 IIC2_Wait_Ack(); val = IIC2_Read_Byte(0); // 读一个字节,NACK 结束 IIC2_Stop(); return val; }

逻辑说明:LIS3DH 的 I2C 地址由 SAD 引脚决定,接地是 0x18,接 VDD 是 0x19,左移一位后最低位是读写位。读操作要先写寄存器地址,再重启始条件切读模式。读加速度时,OUT_X_L(0x28)到 OUT_Z_H(0x2D)是连续 6 个寄存器,可以用连续读一次取完,效率更高。参数上,IIC2_Read_Byte 的参数 0 表示读完后发 NACK,1 表示发 ACK 继续读。

4. SPI 接口驱动落地:模式配置与高速读取

4.1 SPI 模式 0 与片选控制

LIS3DH 的 SPI 支持模式 0(CPOL=0,CPHA=0)和模式 3(CPOL=1,CPHA=1),资料里 lis3dh_driver_spi.c 默认用模式 0。SPI 的关键是片选 CS 必须由软件控制,每次传输前拉低、传输后拉高。先看字节收发:

// SPI 收发一个字节,模式 0 uint8_t LIS3DH_SPI_ReadWriteByte(uint8_t txd) { uint8_t rx = 0; for (uint8_t i = 0; i < 8; i++) { if (txd & 0x80) MOSI_HIGH(); else MOSI_LOW(); txd <<= 1; SCLK_HIGH(); // 模式 0:上升沿采样 delay_us(1); if (MISO_READ()) rx |= 0x01; // 读 MISO rx <<= 1; SCLK_LOW(); // 下降沿移出下一位 delay_us(1); } return rx; }

逻辑说明:模式 0 下,数据在 SCLK 上升沿被采样,下降沿切换。注意 rx 的移位顺序,先读位再左移,最后补正。如果读出来全是 0xFF 或 0x00,先查 CS 有没有拉低、MISO 有没有接对。参数上,delay_us(1) 决定 SPI 速率,太快时杜邦线会引入振铃,降速往往能救回来。

4.2 多字节读取与地址自动递增

LIS3DH 的 SPI 读操作有个细节:寄存器地址的最高位(MSB)为 1 表示读,为 0 表示写。连续读时地址会自动递增,所以读 6 个加速度寄存器只需一次片选:

// 连续读 LIS3DH 多个寄存器 void LIS3DH_Read_Multi(uint8_t reg, uint8_t *buf, uint8_t len) { CS_LOW(); // 片选拉低 LIS3DH_SPI_ReadWriteByte(reg | 0x80); // 读标志位置 1 for (uint8_t i = 0; i < len; i++) { buf[i] = LIS3DH_SPI_ReadWriteByte(0x00); // 发空字节收数据 } CS_HIGH(); // 片选拉高,结束传输 }

逻辑说明:reg | 0x80 是 SPI 读操作的地址格式,写操作则直接发地址。连续读时芯片内部地址指针自增,所以能一次取完 X/Y/Z 六个字节。参数上,buf 要提前分配好空间,len 别超过寄存器边界,否则会读到保留区。读完后把低字节和高字节拼成 int16_t,再按量程换算成 g 值。

4.3 量程与输出速率配置

读数据前得先配 CTRL_REG1 到 CTRL_REG4。常见配置:CTRL_REG1(0x20)设 ODR 和使能轴,CTRL_REG4(0x23)设量程和分辨率。比如 100Hz 输出、±2g 量程、使能 XYZ:

// 配置 LIS3DH:100Hz,±2g,XYZ 使能 void LIS3DH_Config(void) { LIS3DH_Write_Reg(0x20, 0x57); // ODR=100Hz, LPen=0, XYZ enable LIS3DH_Write_Reg(0x23, 0x00); // BDU=0, BLE=0, FS=±2g, HR=0 }

逻辑说明:0x57 拆开是 0101 0111,ODR 位是 0101 对应 100Hz,低三位 111 使能三轴。0x23 写 0x00 表示连续更新、小端模式、±2g 量程。参数上,量程越大分辨率越低,±2g 时灵敏度是 1mg/digit,±16g 时降到 12mg/digit,按实际加速度范围选。

5. 避坑与排查:I2C 上拉、SPI 模式、时序错位

5.1 I2C 波形出现台阶,读数据全 0

现象:示波器看 SDA 上升沿有明显台阶,读 WHO_AM_I 正常但读加速度全是 0。原因:上拉电阻太大,总线电容充电慢,高速时电平没到阈值就被采样。解决:把上拉从 10k 换成 4.7k 甚至 2.2k,同时缩短走线。常见做法是 3.3V 系统用 4.7k,5V 系统用 2.2k,具体看总线电容。

5.2 SPI 读出来全是 0xFF

现象:SPI 接法下读任何寄存器都返回 0xFF。原因:MISO 没接或 CS 没拉低,主机读到的是悬空高电平。解决:先量 CS 引脚在传输时是否拉低,再查 MISO 是否接到从机 SDO。如果 CS 用硬件片选,注意有些 MCU 的硬件 CS 在传输间隙会自动拉高,导致多字节读断掉,这时改软件片选。

5.3 加速度数据跳变或卡死

现象:数据偶尔跳变,或者读几次后卡死。原因:I2C 时序里 delay 太短,从机没来得及应答;或者 SPI 速率超过芯片上限。解决:把 delay_us 从 1 加到 2 或 5,SPI 时钟先降到 1MHz 试。另外检查电源去耦,LIS3DH 的 VDD 和 VDD_IO 都要加 100nF 电容,电源纹波大会导致内部逻辑误判。

5.4 WHO_AM_I 读不到 0x33

现象:读 0x0F 寄存器返回不是 0x33。原因:I2C 地址错(SAD 接法不对)或 SPI 模式不匹配。解决:I2C 下确认 SAD 引脚电平,接地 0x18、接 VDD 0x19;SPI 下确认 CPOL/CPHA 是模式 0 还是模式 3,两种都试一遍。如果还是不对,查供电是否到 2.16V 以上,LIS3DH 最低工作电压是 1.71V,但 1.8V 以下时序会变慢。

5.5 移植后编译报错找不到寄存器定义

现象:换平台后编译提示 LIS3DH_CTRL_REG1 未定义。原因:lis3dh_driver.h 里的寄存器宏依赖具体头文件,移植时没包含。解决:把驱动层的 .h 文件路径加进工程,或者把寄存器宏单独抽成一个头文件。那些 .bat 和 .browse 文件不用管,它们不参与编译。

6. 进阶技巧:用 FIFO 和中断把采样做稳

LIS3DH 有个很实用的 FIFO 模式,能缓存 32 组数据,配合中断引脚能大幅降低 MCU 轮询开销。配置 FIFO 用 CTRL_REG5(0x24)和 FIFO_CTRL_REG(0x2E)。比如设成 Stream 模式,FIFO 满 16 组触发水印中断:

// 配置 FIFO:Stream 模式,水印 16 void LIS3DH_FIFO_Config(void) { LIS3DH_Write_Reg(0x2E, 0x80 | 16); // Stream 模式,水印阈值 16 LIS3DH_Write_Reg(0x24, 0x40); // FIFO 使能 }

逻辑说明:0x2E 的高两位是 FIFO 模式,10 表示 Stream,低五位是水印阈值。0x24 的 bit6 是 FIFO_EN。配好后 INT1 引脚会在水印达到时输出高电平,MCU 用外部中断触发读取,一次读 16 组数据。参数上,水印阈值别设满 32,留点余量防止溢出丢数据。

验证方法:先读 FIFO_SRC_REG(0x2F)看 FSS 位,确认存了多少组,再批量读 OUT_X_L。我一般会写个简单的串口打印,把三轴数据按 g 值输出,晃动板子看波形是否连续。如果数据断断续续,先查 ODR 和 FIFO 模式是否匹配,Stream 模式下旧数据会被新数据覆盖,FIFO 模式则会停止更新。

从那以后我每次调 LIS3DH,都强制先读 WHO_AM_I 确认通信,再配量程和 ODR,最后才开 FIFO 和中断,这三步顺序乱了就容易出玄学问题。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进