简介面向意法半导体LIS2DW12三轴加速度计的驱动源码与示例集合适合嵌入式、物联网开发者快速掌握传感器驱动开发尤其适用于低功耗运动监测、健康设备与移动终端等场景。压缩包为ZIP格式共10个文件全部为C源码大小约30KB轻量而实用内容综合示例覆盖单次读取、FIFO批量读取、自由落体、单击/双击、唤醒、自检、方向检测、活动识别等典型功能从底层寄存器配置到上层应用调用层次分明可系统学习I2C与SPI通信时序、寄存器读写、原始数据解析和中断唤醒机制等核心知识点。已有1508人学习下载实践参考价值得到较多开发者认可。代码结构清晰可直接借鉴驱动初始化与数据读取流程根据实际硬件平台稍作配置修改即可集成使用能有效降低LIS2DW12驱动开发门槛、缩短项目移植周期。 最近在做一块低功耗运动检测板子传感器选的是 ST 的 LIS2DW12。这颗芯片的名字经常出现在各种 BSP 里可真要自己从头写 lis2dw12 驱动的时候你会发现网上大部分能搜到的例子都是 LIS3DH 或 LIS2DH12 的代码寄存器名字看着像实际位定义差不少直接套用十有八九会翻车。我前后折腾了几天把一个带 FIFO 和中断的驱动跑通中间踩了几个值得记下来的坑。这篇文章不打算贴一个超大文件而是把从芯片原理、寄存器、初始化、读数、FIFO 到 Linux 接入的完整链路讲清楚并提供可以直接改用的驱动骨架和小例子。适合正在做可穿戴设备、电池供电传感器节点以及需要在 Linux 下接入这颗加速度计的朋友参考。1. LIS2DW12 这颗芯片到底强在哪里1.1 超低功耗不只是口号LIS2DW12 是 ST 面向超低功耗市场推出的三轴加速度计封装做到了 2mm x 2mm x 0.7mm非常适合手环、耳机充电盒、智能标签这类空间极度紧张的产品。它的典型待机电流只有几十纳安低功耗模式下 1.6Hz 采样时平均电流能压在 1 微安以内用一颗 200mAh 的纽扣电池跑一年是完全现实的事情。这颗芯片的功耗指标直接决定了驱动代码的写法。如果只是在开发板上做姿态检测轮询读数据完全够了但如果目标是电池供电产品加速度计大部分时间必须待在低功耗模式由内部运动检测中断去唤醒 MCU而不是让 MCU 不停查询传感器。这个需求差异会让驱动程序的架构变得很不一样后面讲 FIFO 和中断配置时还会再提到。1.2 写驱动前先精读数据手册里这几页拿到 LIS2DW12 数据手册很多朋友第一反应是去翻 register map然后一头扎进代码里。我建议反过来先花半小时把下面三部分看明白后面调驱动会快很多功能框图弄清楚寄存器组、传感器核心、FIFO、中断控制器之间的数据流向。尤其是 FIFO 在哪个位置这会影响你对批量读取的理解。寄存器映射表地址、默认值和读写属性。写代码时要频繁对照最好打印一份放在手边。芯片特性表ODR 范围、量程和灵敏度、噪声密度、功耗数据。这些参数在换算加速度值和诊断异常数据时都要用到。1.3 别把 LIS2DW12 当 LIS3DH 用LIS3DH 是很多人的入门传感器网上教程和代码多到爆炸。LIS2DW12 虽然看起来和它长得很像引脚也兼容但它是重新设计的低功耗产品线不是 LIS3DH 的马甲。WHO_AM_I 的返回值不同CTRL1、CTRL4 这些寄存器的位定义也有差别FIFO 的行为更加不一样。我见过不止一个项目直接把 LIS3DH 的驱动文件改个名字就上板子结果读出来的数据完全不对最后排查了半天才发现问题出在寄存器配置上。所以我的建议是凡是遇到 ST 的加速度计第一件事确认 WHO_AM_I第二件事对照你手上那版数据手册的位描述不要盲目相信网上的代码注释。2. 驱动编写前必须吃透的寄存器地图2.1 I2C 还是 SPI接线差别很大LIS2DW12 支持 I2C 和 SPI 两种接口最常用的是 I2C两根线就能搞定适合大多数 MCU。SPI 适合 I2C 总线已经比较拥挤、或者对数据吞吐率有要求的场景。I2C 从机地址由 SA0 引脚决定接地时是 0x18接 VDD 时是 0x19。注意这里说的是 7 位地址很多平台的 I2C 驱动接口里需要把地址左移一位再去读写这是新手最容易踩的坑之一。接线时除了 VDD、GND、SCL、SDA还有一个细节容易被忽略LIS2DW12 的 CS 引脚在 I2C 模式下必须接高电平否则芯片可能进入 SPI 模式I2C 地址怎么发都不对。上电时如果 CS 悬空还可能受干扰导致 WHO_AM_I 读出来是 0xFF。2.2 核心寄存器一览我在开发过程中把最常用的几个寄存器整理成了表格处理大部分调试场景都够用。更完整的位描述还是要回到数据手册去查。寄存器地址作用WHO_AM_I0x0F芯片 ID固定返回 0x44CTRL10x20ODR 配置、低功耗模式选择CTRL20x21软复位、BOOT、滤波相关CTRL30x22中断引脚配置、地址自增CTRL40x23量程选择±2g/±4g/±8g/±16gCTRL60x25低噪声模式等STATUS0x27数据就绪标志、FIFO 状态OUT_X_L ~ OUT_Z_H0x28 ~ 0x2D三轴加速度原始值FIFO_CTRL0x2EFIFO 模式和水位设置FIFO_SAMPLES0x2FFIFO 中已存样本数2.3 读加速度的两种姿势第一种是轮询。每次读数据前先看 STATUS 寄存器的 ZYXDA 位这一位变成 1 说明三个轴的新数据已经准备好了然后连续读 6 个字节。这种写法最简单但 MCU 要一直醒着等数据在低功耗场景里很吃亏。第二种是 FIFO 加中断。传感器内置了一个 FIFO可以先把多次采样结果存起来存到指定水位之后通过 INT1 引脚通知 MCU。MCU 被唤醒后一次性把一批数据搬走然后再回去睡觉。这种方案能显著降低系统平均功耗也是这颗芯片的正确打开方式。3. 一个能直接改用的驱动代码骨架3.1 先把底层读写接口封好不管用 I2C 还是 SPI第一步都是抽象底层通信接口。我习惯定义两个函数指针初始化时把对应平台的代码填进去。这样驱动主体完全不依赖具体 MCU换平台时只改一层小接口。typedef struct { int (*read_reg)(uint8_t reg, uint8_t *buf, uint16_t len); int (*write_reg)(uint8_t reg, const uint8_t *buf, uint16_t len); } lis2dw12_bus_t; typedef struct { lis2dw12_bus_t bus; uint8_t i2c_addr; } lis2dw12_dev_t;实际在 STM32 上read_reg 里多半是 HAL_I2C_Mem_ReadLinux 用户态程序里则是 ioctl 加读缓冲。把这一层封装好后面所有代码都是操作寄存器清爽很多。3.2 初始化流程配置顺序有讲究初始化第一步永远是读 WHO_AM_I确认芯片在线也顺便确认 I2C 地址和接线没有问题。然后才去写 CTRL1、CTRL4 这些控制寄存器。我习惯先把 CTRL1 里的 ODR 和模式配置好再设置量程最后如果需要软复位会再补一次复位操作。int lis2dw12_init(lis2dw12_dev_t *dev) { uint8_t id; uint8_t ctrl1; uint8_t ctrl4; /* 第一步确认芯片 ID */ dev-bus.read_reg(dev-i2c_addr, LIS2DW12_WHO_AM_I, id, 1); if (id ! 0x44) { return -1; } /* 第二步配置 CTRL1ODR 和模式。 * 这里先给出一个示意实际 ODR 编码请对照手册表格。 */ dev-bus.read_reg(dev-i2c_addr, LIS2DW12_CTRL1, ctrl1, 1); ctrl1 ~0xF0; ctrl1 | (0x07 4); /* 100Hz 示例 */ dev-bus.write_reg(dev-i2c_addr, LIS2DW12_CTRL1, ctrl1, 1); /* 第三步配置 CTRL4 的量程±2g 示例 */ dev-bus.read_reg(dev-i2c_addr, LIS2DW12_CTRL4, ctrl4, 1); ctrl4 ~0x30; dev-bus.write_reg(dev-i2c_addr, LIS2DW12_CTRL4, ctrl4, 1); return 0; }配置顺序的重点是CTRL1 的 ODR 一定要先写再写量程。有些项目先把量程配好再回头写 ODR结果传感器会因为配置不完整进入一种“半工作”状态表现为数据一直不更新或者读出随机值。3.3 读取三轴原始值并换算成 g读数据时我先检查 STATUS 的 ZYXDA 位然后一次读 6 个字节。多字节读要保证地址自增已经打开否则会一直在读同一个寄存器。芯片手册里把这个功能叫做 IF_ADD_INC一般在 CTRL3 里控制。int lis2dw12_read_xyz(lis2dw12_dev_t *dev, int16_t *x, int16_t *y, int16_t *z) { uint8_t status; uint8_t buf[6]; dev-bus.read_reg(dev-i2c_addr, LIS2DW12_STATUS, status, 1); if (!(status 0x01)) { return -EAGAIN; } dev-bus.read_reg(dev-i2c_addr, LIS2DW12_OUT_X_L, buf, 6); *x (int16_t)((buf[1] 8) | buf[0]); *y (int16_t)((buf[3] 8) | buf[2]); *z (int16_t)((buf[5] 8) | buf[4]); return 0; }原始值和物理量之间的换算要看量程和分辨率。LIS2DW12 在不同模式下输出位数不一样正常模式下 14 位低功耗模式下可能会降到 12 位甚至 8 位。以 ±2g 量程、14 位输出为例1g 大约对应 4096 LSB所以换算公式是g raw / 4096.0。如果你把量程改成了 ±16g这个系数就完全不同务必按手册里的灵敏度重新算。3.4 FIFO 批量读取把系统功耗真正降下来FIFO 的用法是把多个采样结果先缓存到芯片内部然后一次读出来。我常用的模式是连续模式传感器按 ODR 持续采样一直往 FIFO 里写满了之后由硬件设置水位中断MCU 过来搬数据。FIFO 深度是 32 级也就是最多缓存 32 个三轴样本。配置 FIFO 时我先写 FIFO_CTRL 设置工作模式和水位再把中断打开。FIFO 水位中断被触发后我读取 FIFO_SAMPLES 寄存器的低 5 位得到当前有效样本数然后循环读取 6 字节数据块直到把存量样本搬完。int lis2dw12_fifo_read(lis2dw12_dev_t *dev, int16_t samples[][3], uint8_t max_num) { uint8_t n; uint8_t buf[6]; uint8_t i; dev-bus.read_reg(dev-i2c_addr, LIS2DW12_FIFO_SAMPLES, n, 1); n 0x1F; if (n max_num) { n max_num; } for (i 0; i n; i) { dev-bus.read_reg(dev-i2c_addr, LIS2DW12_OUT_X_L, buf, 6); samples[i][0] (int16_t)((buf[1] 8) | buf[0]); samples[i][1] (int16_t)((buf[3] 8) | buf[2]); samples[i][2] (int16_t)((buf[5] 8) | buf[4]); } return n; }用 FIFO 有个好处MCU 可以睡上一阵子等传感器存满一批数据再醒来平均功耗低很多。但要注意 FIFO 水位不要设成满深度留一点余量否则中断响应稍微慢一点就会丢数据。4. Linux 下的接入方式用户态调试与内核驱动两手抓4.1 用户态 I2C 操作验证传感器最快的方式拿到一块板子先别急着写内核驱动。先把 I2C 总线上能不能看到这颗芯片搞清楚。Linux 下用 i2c-tools 可以快速探测i2cdetect -y 1 i2cget -y 1 0x18 0x0f如果i2cdetect在 0x18 或 0x19 位置看到了设备再用i2cget读 WHO_AM_I能得到 0x44说明接线和地址都没问题。这时候可以接着写一小段 Python 脚本配合 smbus2 库快速验证基础功能from smbus2 import SMBus bus SMBus(1) bus.write_byte_data(0x18, 0x20, 0x70) # CTRL1: 100Hz 示例 data bus.read_i2c_block_data(0x18, 0x28, 6) x (data[1] 8) | data[0] y (data[3] 8) | data[2] z (data[5] 8) | data[4] print(x, y, z)这一步看着不起眼但在写内核驱动之前能把硬件链路确认干净后期排错会省非常多时间。4.2 内核 IIO 驱动产品级做法如果要做产品正经的接入方式是走内核的 IIO 子系统。主线内核里已经有 st_lis2dw12 这个驱动位于 drivers/iio/accel/ 目录下设备树里把器件挂到 I2C 总线上声明 compatible 为st,lis2dw12并配置中断引脚i2c1 { lis2dw1218 { compatible st,lis2dw12; reg 0x18; interrupt-parent gpioa; interrupts 1 IRQ_TYPE_EDGE_RISING; }; };驱动加载成功以后IIO 框架会在/sys/bus/iio/devices/iio:deviceX下生成节点读原始值可以直接用 sysfs 接口cat /sys/bus/iio/devices/iio:deviceX/in_accel_x_raw不过不同内核版本的 IIO 驱动对设备树属性的支持程度有差异有些属性只在新版本里才生效。我的建议是先用当前内核文档核对一遍不要直接照抄旧项目的设备树。4.3 两种接入方式怎么选用户态 I2C 适合调试和快速验证Python 脚本改起来快图形化也方便。内核 IIO 驱动适合正式产品sysfs 接口方便上层应用读取也让传感器管理和电源管理更容易统一交给内核。如果只是在 MCU 上做开发完全用不上内核驱动直接在 RTOS 或裸机环境里用第三节的代码骨架就行。5. 实战避坑数据手册写了但容易看漏的细节5.1 WHO_AM_I 读出来是 0xFF 或 0x00这个现象十有八九是接线或接口模式问题按下面顺序排查用万用表确认 VDD 和 GND 电压正常芯片供电范围一般是 1.62V 到 3.6V不要给到 5V。确认 CS 引脚在 I2C 模式下确实拉高。CS 悬空是 I2C 模式下最容易犯的错。确认 I2C 地址。0x18 是 7 位地址如果你用的是 ioctl 方式要把地址左移一位变成 0x30 才能发给内核。用逻辑分析仪抓一下 SCL 和 SDA看主机是不是真的把起始条件和从机地址发出去了。我遇到过一次非常隐蔽的问题板子上拉了 4.7k 上拉但主控的 I2C 引脚配置成了开漏输出之后又被复用成别的功能导致 SCL 一直拉不高。这类问题不从波形上看很难定位。5.2 读出来数据全是 0或者长时间不更新先看 STATUS 的 ZYXDA 位有没有变化。如果永远为 0说明传感器根本没有产生新的采样结果。这时候优先检查 ODR 配置是否写进去了很多库函数在初始化时会把 CTRL1 清零如果后面没有再设置 ODR芯片就一直处于 power-down 状态。另外一种常见情况是读函数用了单字节读但没有开地址自增。结果每次读到 OUT_X_L然后总线自动又回到同一个地址6 个字节读出来全是同一个值。这个现象很有迷惑性很容易让人以为是传感器坏了。还有一点提醒一下有些传感器在配置完寄存器之后需要等一段时间才能输出第一组有效数据不能指望写完初始化立刻就有数据。如果逻辑分析仪上能看到 I2C 事务正常但 STATUS 一直是 0可以加一个几十毫秒的延时再查。5.3 FIFO 水位中断不触发中断不触发先从硬件到软件一层层查。我最常遇到的是中断引脚配置没对上。LIS2DW12 的 INT1 和 INT2 是可以配置不同中断源的如果代码里把 FIFO 满中断配到 INT2但硬件上只连了 INT1自然永远等不到中断。另一个容易被忽略的点是中断触发方式。有些驱动配置的是电平触发但中断引脚没有正确清标志位导致中断一直挂着有些场景适合边沿触发需要读一次 STATUS 或 FIFO_SAMPLES 来清标志。每次调试中断问题我都会先写一个最简单的轮询版本来确认 FIFO 数据本身没问题再去查中断链路这样能把“传感器异常”和“中断路径异常”快速区分开。5.4 低功耗模式下ODR 和带宽不一定能随意配LIS2DW12 虽然有很丰富的低功耗档位但并不是所有组合都能自由选择。某些低功耗模式下支持的最高 ODR 会降低如果代码里选了手册不允许的组合可能读到的数据更新率完全不符合预期。选型时如果项目需要 400Hz 采样就要确保工作在支持这个 ODR 的模式下否则数据曲线会“断断续续”看起来像是驱动有 bug实际是模式配置本身就不合法。5.5 调完这块芯片以后我给自己留的一条笔记最后一个建议来自我这次实际调完之后的一点体会ST 传感器的寄存器位定义在不同子型号之间差异不小无论从哪个例程开始改第一步永远是把 WHO_AM_I 和手册版本确认好。把寄存器配置写成一个清晰的结构体或配置表不要散落在代码里到处都是 magic number。这样即使换了芯片型号也能很快比对差异不至于重新踩一遍同一个坑。本文还有配套的精品资源点击获取