
简介面向嵌入式开发者与传感器应用工程师这份压缩包围绕ST LIS3DH三轴加速度计提供基于IIC与SPI两种接口的驱动源码和配套技术文档可帮助快速完成驱动移植与数据采集验证。IIC模式包含初始化、起始/停止、发送字节、读取字节与应答等待等完整底层操作函数SPI独立驱动可完成寄存器读写与数据读取便于按硬件接线灵活选择。包内共139个文件包含35个头文件与35个C源文件覆盖驱动主体、MCU相关初始化及示例工程另有PDF文档与TXT说明辅助理解寄存器配置和调试流程以及IAR工程配置、Hex文件等便于直接烧录测试。压缩包整体约6.05MB体积小巧但结构完整IIC/SPI驱动、平台层与文档资料分区清晰适合不同工程环境选用。资源已有505人学习下载对正在调试LIS3DH或准备在STM8等平台上接入加速度传感器的开发者是一份有参考价值的落地资料。1. 为什么 LIS3DH 到现在还是 I2C/SPI 驱动入门的首选芯片LIS3DH 是 2010 年前后的三轴加速度计但今天在运动检测、计步、倾角测量和振动监测里依然大量出现。原因很简单功耗极低正常模式约 2µA低功耗模式甚至可以到 0.5µA量程可选 ±2g/±4g/±8g/±16g而且同时支持 I2C 和 SPI。对嵌入式工程师来说这套驱动源码的价值不是“拿来就能跑”而是能把 I2C 时序、寄存器位操作、SPI 片选和连续读取这些基本功一次看明白。适合正在学 stm8/stm32 外设驱动的人也适合需要把旧方案移植到新 MCU 上做快速对照的老手。下面从寄存器到驱动函数按实际读写路径拆解。2. LIS3DH 寄存器地图与驱动设计写代码前先确定通信边界2.1 器件地址、SA0 引脚和 I2C/SPI 总线怎么选LIS3DH 的 I2C 地址由 SA0 引脚决定。SA0 接 GND 时 7 位地址是 0x18接 VDD 时是 0x19。在 8 位读写的代码里这个地址左移一位作为首字节读操作加 0x01。SPI 模式下 SA0 的功能被 CS 引脚替代所以同一块板子只要把驱动接口层换掉上层应用完全不用动。很多开源板默认把 SA0 接地例程里直接写#define LIS3DH_ADDR 0x18如果你手上的模块是 0x19第一批读出数据会全是 0xFF。选择依据一般是板级资源和实时性。I2C 只需要两根线能挂多个器件但时钟频率受上拉电阻和总线电容限制通常 400kHz 下读一次完整加速度数据需要 250µs 左右。SPI 用四根线独占 CS没有开漏上拉问题时钟可以跑到 5MHz 甚至 10MHz读取延迟更低。要接 OLED、气压计、陀螺仪等多个设备时优先 I2C数据量大、要求响应快的场景直接选 SPI。STM8 的 I2C 外设坑比较多很多人干脆用 GPIO 模拟 I2C这就是下面驱动里IIC2_函数组出现的原因。2.2 控制寄存器 1 到 6ODR、量程和块更新设置LIS3DH 的核心配置集中在 CTRL_REG1 到 CTRL_REG6驱动初始化就是向这些寄存器写入下面这张表里的关键位。寄存器地址关键位作用CTRL_REG10x20ODR[3:0], LPen, Zen, Yen, Xen数据输出速率、低功耗模式、各轴使能CTRL_REG20x21HP[2:0], HPM[1:0]高通滤波配置CTRL_REG40x23BDU, BLE, FS[1:0], HR块更新、字节序、量程、高分辨率CTRL_REG50x24FIFO_EN, LIR_INT1FIFO 使能和中断锁存CTRL_REG60x25I2_IF选择 I2C 还是 SPI默认 I2CSTATUS_REG0x27ZYXDA, ZYXOR数据就绪和溢出标志量程通过 CTRL_REG4 的 FS[1:0] 设置00 对应 ±2g01 对应 ±4g10 对应 ±8g11 对应 ±16g。ODR 在 CTRL_REG1 的 ODR[3:0] 设置比如 0x07 对应 100Hz0x09 对应 400Hz。做倾角测量时选 ±2g 和 100Hz 够用做振动分析再考虑 ±8g/±16g 和高 ODR。注意 HR 位决定数据分辨率默认 12 位置 1 后进入高分辨率模式功耗也会上升电池设备要权衡。2.3 初始化与自检先复位再写配置最后读 WHO_AM_I驱动里LIS3DH_Data_Init的常见做法是先软复位再依次写控制寄存器最后回读 WHO_AM_I 确认通信链路。参考实现void LIS3DH_Data_Init(void) { IIC2_Init(); // 初始化 I2C GPIO LIS3DH_WriteReg(CTRL_REG5, 0x80); // BOOT 位清 0 LIS3DH_WriteReg(CTRL_REG1, 0x57); // ODR100Hz, 使能 X/Y/Z LIS3DH_WriteReg(CTRL_REG4, 0x80); // BDU1, 量程±2g LIS3DH_WriteReg(CTRL_REG6, 0x00); // 默认 I2C 接口 uint8_t id LIS3DH_ReadReg(WHO_AM_I); // 固定 0x33 if (id ! 0x33) { while (1); // 或者置错误标志 } }CTRL_REG5的 0x80 位对应 BOOT 位置 1 会重启芯片内部逻辑并恢复默认寄存器值所以初始化第一件事是把它写 0。0x57的二进制是 0101 0111ODR[3:0]0101 对应 100Hz低三位 Zen/Yen/Xen 全为 1。CTRL_REG4的 BDU1 表示读高/低字节时同步锁存避免读取过程中数据更新导致高低字节错位。WHO_AM_I 寄存器地址是 0x0F固定返回 0x33回读不对时优先查 SA0 电平和 I2C 地址是否匹配而不是怀疑芯片坏了。如果发现初始化后读数一直是 0先查STATUS_REG的低三位ZYXDA这个位为 1 才表示三个轴都有新数据。驱动里读 STATUS 再读数据寄存器就是为了避免读回上一次的旧数据。芯片刚上电时内部 RC 振荡器需要一点时间稳定初始化后加 10ms 延时再开始读比反复重建 I2C 通信更有效。3. IIC 驱动源码拆解从 SCL/SDA 时序到 LIS3DH 寄存器读写3.1 可移植的 IIC2 底层函数组这套驱动用两个普通 GPIO 模拟 I2C所以IIC2_Init做的就是配置 SCL、SDA 为开漏输出带上拉然后空闲状态保持 SCL1、SDA1。真正决定兼容性的不是代码而是时序里每个延迟的取值。常规做法是延时 5µsSCL 高电平时间不能小于 4µs。I2C 标准模式要求 4.7µs快速模式只要 1.3µs用 5µs 能兼顾大多数 MCU 和传感器STM8 主频 16MHz 时一个delay_us(5)大约 80 个 NOP移植到其他平台时要把这个宏按实际主频重新调否则时序过慢或过快都会导致 ACK 异常。void IIC2_Start(void) { SDA_H; // SDA 先拉高 SCL_H; // 确保 SCL 高 delay_us(5); SDA_L; // 在 SCL 高电平期间 SDA 下降沿 START delay_us(5); SCL_L; // 拉低 SCL为传输数据位做准备 } void IIC2_Stop(void) { SCL_L; // 先保证 SCL 低 SDA_L; // SDA 拉低 delay_us(5); SCL_H; // SCL 拉高 delay_us(5); SDA_H; // 在 SCL 高电平期间 SDA 上升沿 STOP }START 条件是 SCL 高电平时 SDA 产生下降沿STOP 条件是 SCL 高电平时 SDA 产生上升沿。代码里先操作 SCL 再操作 SDA顺序很重要。若先拉 SDA 再拉 SCL总线可能被识别成其他状态若拉高 SCL 后立即拉高 SDA则可能意外产生停止条件。所以标准实现必须按 SCL 变化、延迟、SDA 变化的顺序执行。用逻辑分析仪抓时序时可以对比标准 I2C 波形SCL 高电平期间 SDA 只能有一个边沿两个边沿都发生在 SCL 高电平就是错误。3.2 字节收发、ACK 和 NACK 的配合发送字节和读取字节是同一个位移循环区别只是 SDA 方向。IIC2_Send_Byte在 SCL 低电平时把数据位放到 SDA再拉高 SCL 让从机采样。读取时反过来在 SCL 高电平时从 SDA 上采样MCU 作为主机不需要驱动 SDA所以要把 SDA 设为输入模式或开漏释放。void IIC2_Send_Byte(uint8_t txd) { uint8_t i; for (i 0; i 8; i) { if (txd 0x80) SDA_H; else SDA_L; txd 1; SCL_H; delay_us(5); SCL_L; delay_us(5); } } uint8_t IIC2_Read_Byte(unsigned char ack) { uint8_t i, dat 0; SDA_IN; // 释放 SDA改为输入 for (i 0; i 8; i) { SCL_H; delay_us(5); dat 1; if (SDA_READ) dat | 0x01; SCL_L; delay_us(5); } if (ack) IIC2_Ack(); else IIC2_NAck(); return dat; }IIC2_Read_Byte的参数 ack 表示收到这一字节后是否回复 ACK。读最后一个字节前要回 NACK告诉从机不用再发下一字节读中间字节回 ACK从机才会继续准备下一字节。SDA_IN切换方向是软件 I2C 容易漏的一步如果一直保持推挽输出SDA 会被主机拉死从机数据发不出来。ACK 相关三个函数看起来简单但对硬件 I2C 和软件模拟都适用IIC2_Wait_Ack在主机发送地址或寄存器地址后读取从机的响应位如果 SDA 在第九个时钟后仍是高电平说明从机没应答。常见原因是器件地址配置错或者 SA0 接法软件没对应其次是芯片还在复位或电源没稳定。调试时在IIC2_Wait_Ack里返回错误码而不是直接退出可以定位是地址阶段失败还是寄存器阶段失败。3.3 寄存器读写的完整包装和多字节读把底层字节函数封装成LIS3DH_ReadReg和LIS3DH_WriteReg上层代码就不需要关心 START、地址、ACK 这些细节。写寄存器时主机发出 START依次发器件地址 8 位含写位、寄存器地址、数据然后 STOP。读寄存器时要先写寄存器地址完成“写地址”阶段再发一次 START 或重复 START最后连续读两个字节顺序是低字节在前。对于加速度数据最好一次读六个寄存器而不是分开读三个两字节下面这个函数把 0x28 开始的数据连续读出void LIS3DH_ReadAccel(int16_t *ax, int16_t *ay, int16_t *az) { uint8_t buf[6]; IIC2_Start(); IIC2_Send_Byte(LIS3DH_ADDR 1); IIC2_Wait_Ack(); IIC2_Send_Byte(0x28); // OUT_X_L IIC2_Wait_Ack(); IIC2_Start(); // 重复 START IIC2_Send_Byte((LIS3DH_ADDR 1) | 0x01); IIC2_Wait_Ack(); for (int i 0; i 6; i) { buf[i] IIC2_Read_Byte(i 5 ? 1 : 0); } IIC2_Stop(); *ax (int16_t)((buf[1] 8) | buf[0]); *ay (int16_t)((buf[3] 8) | buf[2]); *az (int16_t)((buf[5] 8) | buf[4]); }LIS3DH_ReadAccel里的循环只对最后一个字节回 NACK前五个字节都回 ACK这是从机继续输出下一字节的必要条件。如果连续读数发现 X/Y/Z 三个轴的数值互相错位多半是 ACK/NACK 顺序写反或者是读取时寄存器地址没有从 0x28 开始而是跳到了别的地址。重复 START 可以直接跟在 ACK 信号后面不需要先发 STOP这让总线在两次事务之间不会被其他主机抢走。3.4 上拉电阻、GPIO 模式和 IIC 波形观察软件 I2C 最常见的现象是 SCL 波形正常但 SDA 地址后无 ACK或者读到的数据全是 0xFF。先量 SDA/SCL 静态电平正常应该是高电平如果为低说明 SDA 或 SCL 漏接上拉或者 GPIO 被配置成了推挽输出且初始输出低。上拉电阻一般取 4.7kΩ3.3V 供电时 10kΩ 以下都能工作5V 供电可以取 2.2kΩ但总线电容大时上升沿会拉长最好用逻辑分析仪看波形。抓 I2C 地址字节时0x18 左移一位是 0x30最低位为 0 表示写如果抓到的是 0x60说明代码里多移了一位或地址定义错误。SCL 频率也不能太高软件模拟 I2C 时每个 bit 至少留 2.5µs 低电平和 2.5µs 高电平过快的时钟在长线上会产生振铃导致从机把 1 读成 0。4. SPI 接口驱动从 I2C 迁移到 SPI 的代码级替换4.1 引脚映射和 SPI 模式差异LIS3DH 的 SPI 信号在数据手册里叫 SPC、SDI、SDO/SA0、CS。SPC 是 SPI 时钟SDI 是 MOSISDO/SA0 是 MISOCS 是片选。I2C 模式下的 SA0 地址引脚在 SPI 模式下变成 MISO所以硬件设计要确认这个引脚没有接死到 GND否则 SPI 读不到数据。CTRL_REG6 的 I2_IF 位默认 0 表示 I2C 使能要切到 SPI 需要对这个位写 1但芯片上电默认是 I2C 使能SPI 模式下初始化时也要通过 SPI 总线写这个寄存器即使默认 I2C 使能SPI 写入依然有效。反过来如果初始化代码里误把 CTRL_REG6 的 I2_IF 写成 1I2C 通信会立刻中断。SPI 通信格式是 8 位数据、SPI 模式 0CPOL0, CPHA0或模式 2CPOL1, CPHA1手册上写的是数据在时钟上升沿采样。CS 为低电平时芯片被选中读操作在发送地址字节后由从机驱动 MISO 输出数据不需要像 I2C 那样切换方向。引脚对应关系功能I2C 模式SPI 模式时钟SCLSPCSCL数据输入SDA双向SDIMOSI数据输出同上SDO/SA0MISO片选无CS地址选择SA0 引脚无复用为 MISO4.2 SPI 读写函数实现和 R/W 位处理lis3dh_driver_spi.c里的读写和 I2C 版本差别不大只是把开始/停止/ACK 换成了 CS 拉低和拉高一个字节的收发用 SPI 外设或模拟移位完成。硬件 SPI 速度高软件模拟则要看时序下面以模拟 SPI 为例更接近原驱动结构void LIS3DH_SPI_WriteReg(uint8_t reg, uint8_t val) { CS_L; SPI_SendByte(reg 0x3F); // 最高位 W/R0, 次高位 MS0 SPI_SendByte(val); CS_H; } uint8_t LIS3DH_SPI_ReadReg(uint8_t reg) { uint8_t val; CS_L; SPI_SendByte((reg 0x3F) | 0x80); // W/R1 val SPI_SendByte(0x00); // 同时读取 MISO CS_H; return val; }reg 0x3F是保留地址低 6 位最高位是 R/W 位次高位是 MS 位。MS 位为 1 表示 SPI 地址自增模式一般不用在单寄存器读写里读取多字节时设置 MS1地址可以在连续字节间自动增加。所以多字节读的地址字节应写成(reg 0x3F) | 0x80 | 0x40。如果忘了置 MS虽然读单个寄存器没问题但连续读六个数据寄存器时会一直返回同一个地址的数据。4.3 硬件 SPI 和模拟 SPI 的迁移要点如果 MCU 自带 SPI 外设建议先按 1MHz 时钟测试再逐步提频。LIS3DH 的 SPI 最高时钟在 5MHz 或 10MHz 左右取决于 VDD但连线长时高频容易读回 0xFF。用硬件 SPI 时要特别注意字节顺序是 MSB first。用软件模拟 SPI 时SPI_SendByte可以用同一个位移循环发送一个字节的同时把 MISO 电平移入接收变量参考实现uint8_t SPI_SendByte(uint8_t dat) { uint8_t i, recv 0; for (i 0; i 8; i) { if (dat 0x80) MOSI_H; else MOSI_L; dat 1; SCLK_H; delay_us(2); recv 1; if (MISO_READ) recv | 0x01; SCLK_L; delay_us(2); } return recv; }这里MOSI_H/MOSI_L是 SDI 引脚MISO_READ是 SDO 引脚输入。关键是 SCLK 的相位模式 0 要求数据在 SCLK 上升沿被采样所以发送端要先在 SCLK 拉低时改 SDI拉高后保持稳定接收端则在 SCLK 拉高后读取 MISO。如果读到的数据整体偏移一位通常是 CPHA 设错或这几行顺序不对。4.4 从 I2C 迁移到 SPI 的工程坑改驱动时最容易踩的坑是寄存器地址的位宽。I2C 地址字节是 8 位寄存器地址直接发送SPI 地址字节第 7 位是 R/W第 6 位是 MS 位寄存器地址只有 6 位。所以0x28OUT_X_L在 I2C 里直接发0x28在 SPI 里写地址0x28 0x3F读地址(0x28 0x3F) | 0x80。很多人把 I2C 的寄存器地址原封不动发给 SPI高位和 R/W 位重叠芯片根本不响应。另一个坑是 CS 拉高的时机。CS 必须在最后一个时钟沿结束之后再拉高不能在一个字节传完立即拉高要给从机内部锁存留点时间。如果用硬件 SPI 且 MCU 没有自动片选写完数据寄存器后要加delay_us(1)再拉高 CS否则最后一位经常丢。这个细节用示波器看只有几十纳秒但足以让加速度数据出现周期性跳变。片选信号还应该在 SPI 时钟空闲状态为低电平时保持高不能让 CS 在字节间隙抖动否则芯片会误判为多次片选切换内部 FIFO 指针也容易错乱。5. 把 LIS3DH 数据调到能用中断、FIFO 和现场校准技巧5.1 用 INT1 数据就绪中断减少轮询开销初始化后不断读 STATUS_REG 是可行的但 CPU 会被占满。更好的做法是用 INT1 引脚将 CTRL_REG3 的 INT1_DRDY 位置 1LIS3DH_WriteReg(CTRL_REG3, 0x10); // INT1_DRDY 1 LIS3DH_WriteReg(CTRL_REG5, 0x08); // LIR_INT1 1CTRL_REG3的 bit4 对应 INT1_DRDY每次新数据准备好就会在 INT1 脚输出脉冲。CTRL_REG5的 LIR_INT1 置 1 后中断请求会被锁存直到读取 STATUS_REG 才清除。实际使用时把 INT1 接到 MCU 外部中断在中断服务函数里置一个标志位主循环再读 OUT_X_L 到 OUT_Z_H 一组数据。读取频率严格跟随 ODR不会读到重复旧值。5.2 FIFO 批量读取把六个寄存器一次读回LIS3DH 内置 32 级 FIFO通过 CTRL_REG5 的 FIFO_EN 和 FIFO_CTRL_REG0x2E配置。常用流水线模式存新覆盖旧LIS3DH_WriteReg(CTRL_REG5, 0x40); // FIFO_EN 1 LIS3DH_WriteReg(FIFO_CTRL_REG, 0x40 | 0x1F); // 流水线模式阈值 31FIFO 样本数达到FTH[4:0]阈值时FIFO_SRC_REG会产生标志。中断里一次读取 6 字节主循环攒够阈值再处理能明显减少 I2C 事务次数。用地址自增模式时只需给一个起始地址。5.3 量程换算、零偏校准和滑动滤波的落地方式±2g 量程下原始数据是 12 位左对齐右移 4 位后按 16384 LSB/g 换算float acc_x_g (int16_t)(raw_x 4) / 16384.0f;±4g 是 8192±8g 是 4096±16g 是 2048。零偏用六点法让每个轴分别朝上和朝下读静止值取平均作为零偏偏移量。现场没有水平台时放静止平面取 200 组平均也能消掉初始温漂。滤波用窗口 4 或 8 的滑动平均ODR100Hz 时窗口 8 等效截止频率约 12Hz能滤掉多数机械振动。校准后看静止状态三个轴均方差Z 轴稳定在 1g 附近、X/Y 接近 0 且波动小于 ±20 LSB驱动初始化就算验证通过。换板子时先查 SA0 和 CS 两个引脚的默认电平它们决定 LIS3DH 上电时是否误入未知接口模式这是最容易让人查半天启动流程的问题。本文还有配套的精品资源点击获取