简介:面向嵌入式开发者的ST系列传感器标准C驱动Demo包,覆盖LISDS12、LISDH12、LISDW12、LPS22HB、LPS25HB、LSM6DSM等常用型号,旨在帮助硬件工程师和物联网开发者快速完成加速度、压力、陀螺仪数据的读取与集成。压缩包共104个文件,以51个C源码、26个头文件和26个说明文本为主,另有1个Markdown文档,整体仅383KB,目录结构清晰。代码涵盖驱动库接口、I2C/SPI通信协议、低通滤波、中断处理、计步器算法、功耗管理与错误检查等关键知识点,并附有与各芯片对应的寄存器映射文件,便于对照手册做底层开发或二次移植。所有源码均采用标准C编写,不依赖特定IDE,注释和配置较为清晰,适合迁移到不同MCU平台。已有约1100人学习下载,适合有一定单片机基础、正着手集成ST传感器驱动或关注低功耗运动检测实现的开发者参考。 做了这么多年嵌入式传感器驱动,我手里过了不少ST的加速度计,像LIS2DH12、LIS2DW12、LIS2DS12这仨,真的是从可穿戴到工业监测都能碰到。这几颗芯片驱动本身不难,但如果你不是用STM32平台,或者嫌官方HAL库太重,想用纯标准C写一套能跑在任意MCU上的demo,坑还是有一些的。我把我实际整理、调试这套LISDS12/LISDH12/LISDW12标准C实现demo的过程记录下来,包括寄存器配置、分层设计、量程换算和踩过的坑,希望能帮正在接这几颗芯片的人省点时间。
1. 这套标准C demo到底解决什么问题
先说痛点。ST官方其实提供了丰富的驱动库,但大多数代码是绑定在STM32平台上的,依赖HAL库或者SPL库。你的主控如果是GD32、N32、ESP32、瑞萨、甚至一颗简单的8位机,要把官方驱动裁出来,工作量比你自己写一个还大。更麻烦的是,官方驱动为了覆盖所有型号,做了大量条件编译和回调函数指针,阅读和二次修改都很头疼。
我当时的需求很直接:一块自研的低功耗传感器板上,主控是某国产Cortex-M0内核MCU,I2C和SPI都要支持,芯片在LIS2DH12和LIS2DW12之间选了后者,后续可能还要兼容LIS2DS12。我希望代码做到:
- 纯标准C(C89/C99风格),不依赖任何厂商SDK
- 不动态分配内存,全部静态变量和栈上操作
- I2C/SPI底层通过函数指针或弱函数与平台解耦
- 三颗芯片共用一套API,切换芯片时只改配置头和宏定义
这套demo做出来之后,放到新板子上基本就是改个I2C读函数的事。如果你也是这种需求,可以直接照着我下面这套分层结构和初始化流程来做;如果你是第一次接触ST加速度计,我也把原理和关键寄存器讲清楚,方便你后续扩展。
2. 三颗芯片的共性与差异:选型对照
2.1 它们到底是什么芯片
LIS2DH12、LIS2DW12、LIS2DS12都是意法半导体(ST)的三轴MEMS加速度计,封装多为2x2mm的LGA-12,极小,适合穿戴和空间受限的场景。它们都支持I2C和SPI两种接口,输出都是16位,量程都能做到±2g/±4g/±8g/±16g,内部都有FIFO、中断引脚和温度传感器(LIS2DH12是否带温度需看具体型号,通常有温度通道,LIS2DW12带有温度传感器,LIS2DS12也有)。
我第一次拿到这仨芯片时,第一反应是:寄存器是不是完全一样?结果并不是。它们的中断配置寄存器、FIFO配置寄存器、低功耗控制位都有差异,尤其是LIS2DS12还带了机器学习处理单元,和另外两颗的寄存器布局差别更明显。
2.2 三颗芯片怎么选
| 芯片型号 | WHO_AM_I值 | 核心特点 | 典型场景 |
|---|---|---|---|
| LIS2DH12 | 0x33 | 通用型,性能均衡,高分辨率模式选择灵活 | 运动检测、倾角测量、振动监测 |
| LIS2DW12 | 0x44 | 超低功耗,休眠电流极低,抗冲击能力强 | 电池供电穿戴设备、长期睡眠唤醒 |
| LIS2DS12 | 0x43 | 带机器学习处理、更多内置数据处理能力 | 运动识别、姿势分类、边缘智能 |
我的个人建议是:做低功耗产品优先用LIS2DW12,它的睡眠模式和唤醒中断比LIS2DH12更省事;做通用采集板,LIS2DH12是“万金油”选择,资料多、使用广泛;如果要在MCU端做简单的运动识别,想让传感器本身分担一部分计算,LIS2DS12更合适。
2.3 Demo如何做到三颗芯片统一接口
代码层面我不建议为每颗芯片单独写一套完全独立的驱动,而是抽一个公共的lis2xxx框架,内部通过两个东西区分型号:
- 编译宏:例如
#define ACC_CHIP_LIS2DH12/#define ACC_CHIP_LIS2DW12/#define ACC_CHIP_LIS2DS12 - 型号相关参数结构体:把默认地址、量程配置掩码、FIFO配置寄存器地址、状态寄存器地址等放在一个const结构体里
这样上层的acc_init()、acc_read_raw()、acc_read_g()对应用层完全不可见差异。做法上类似一个简化版的设备抽象层,但又不至于像Linux内核那样复杂。
3. 驱动代码的分层结构与移植思路
3.1 分层的架构
我最终把demo分成四层:
- 应用层:只调用
acc_init()、acc_read_raw()、acc_read_g()、acc_get_temp()这几个接口 - 传感器驱动层:处理寄存器地址、初始化流程、数据合成、量程换算
- 平台抽象层:提供
plat_i2c_write()、plat_i2c_read()、plat_spi_write_read()、plat_delay_ms() - 硬件底层:由你当前MCU的SDK实现平台抽象层的函数
这样做最大的好处是,我可以先在电脑上用软件模拟I2C把整个驱动逻辑跑通,再烧到板子上。如果你要在不同项目间复用,也只需要重写平台抽象层那几个函数。
3.2 目录结构示例
lis2xxx_demo/ ├── app/ │ └── main.c ├── driver/ │ ├── lis2xxx.h │ ├── lis2xxx.c │ ├── lis2dh12.h │ ├── lis2dw12.h │ ├── lis2ds12.h │ └── acc_api.h ├── platform/ │ ├── platform.h │ ├── platform_i2c.c │ └── platform_spi.c └── build/3.3 为什么这样分层
很多初学者喜欢把寄存器读写直接写在main.c里,测试没问题后就完事了。但等你要换芯片、换平台或者加一个新功能时,就会发现耦合的代码很难拆。我在demo里把平台抽象层单独放,甚至在底层同时提供了“硬件I2C”和“GPIO模拟I2C”两套实现,方便调试时对比。
如果你是裸机环境,没有RTOS,建议平台抽象层的延时函数直接使用阻塞延时,不要用vTaskDelay之类的接口。如果后续要上RTOS,用函数指针注册方式替换掉延时实现即可。这个我在LIS2DW12低功耗唤醒测试时就这么干过,改动量很小。
4. 核心读写实现:从I2C时序到初始化配置
4.1 平台接口定义
下面是我在platform.h里定义的接口,这套demo的驱动层只依赖这几个函数:
#ifndef PLATFORM_H #define PLATFORM_H #include <stdint.h> /* I2C 写寄存器,返回0成功,非0失败 */ int8_t plat_i2c_write_reg(uint8_t dev_addr, uint8_t reg, uint8_t *data, uint16_t len); /* I2C 读寄存器,返回0成功,非0失败 */ int8_t plat_i2c_read_reg(uint8_t dev_addr, uint8_t reg, uint8_t *data, uint16_t len); /* SPI 写读,返回0成功,非0失败 */ int8_t plat_spi_write_read(uint8_t *tx, uint8_t *rx, uint16_t len); /* 毫秒延时 */ void plat_delay_ms(uint32_t ms); #endif4.2 读WHO_AM_I校验通信
I2C和SPI第一次通信前,应该先读WHO_AM_I寄存器,确认器件地址、通信时序都是对的。LIS2DH12的WHO_AM_I是0x0F,期望值0x33;LIS2DW12对应0x44;LIS2DS12对应0x43。
读写函数实现很简单,核心是I2C发送寄存器地址后连续读N个字节。SPI模式下要注意,ST的加速度计SPI读指令最高位必须是1,写指令最高位是0,否则通信无效。
int8_t lis2xxx_check_who_am_i(uint8_t dev_addr, uint8_t expect) { uint8_t val = 0; if (plat_i2c_read_reg(dev_addr, LIS2XXX_WHO_AM_I, &val, 1) != 0) { return -1; } if (val != expect) { return -2; } return 0; }4.3 初始化配置流程
以LIS2DH12为例,我常用的初始化流程是:
- 软复位(如果有)
- 配置CTRL_REG1:使能X/Y/Z轴,设置ODR
- 配置CTRL_REG4:量程FS、BDU、HR
- 配置CTRL_REG5:FIFO使能或关断
- 读取STATUS_REG清除未处理数据
- 自检相关配置(可选)
LIS2DW12和LIS2DS12的寄存器名有所不同,比如LIS2DW12的控制寄存器是CTRL1到CTRL6,量程位在CTRL6的FUS位里,而LIS2DS12的控制寄存器布局更接近LIS2DH12但多出了一些用于机器学习内核的寄存器和功能块。
当时我用LIS2DW12做低功耗方案时,配置ODR为25Hz、低功耗模式1,静止电流能到微安级别。唤醒后切到100Hz高分辨率模式,数据质量完全够用。
4.4 BDU为什么必须置位
BDU(Block Data Update)是数据块更新锁存。如果不置位,你在读高字节和低字节的间隙,传感器内部刚好更新了数据,就会出现高低字节来自不同时刻的情况,导致原始值跳变非常明显。特别是量程选±16g时,一个跳变可能就是几百个LSB。
所以我在初始化时都会强制把BDU位置1:
/* LIS2DH12 CTRL_REG4: BDU=1, FS=00(±2g), HR=1 */ uint8_t ctrl4 = (1 << 7) | (0 << 4) | (1 << 3); lis2xxx_write_reg(dev_addr, LIS2DH12_REG_CTRL4, ctrl4);这个习惯让我少排查了很多“数据飘”的问题。
5. 数据读取、量程换算与滤波处理
5.1 读取原始值
加速度输出寄存器是连续的6个字节,顺序是OUT_X_L、OUT_X_H、OUT_Y_L、OUT_Y_H、OUT_Z_L、OUT_Z_H。最稳妥的读法是一次性读6字节:
typedef struct { int16_t x; int16_t y; int16_t z; } acc_raw_t; int8_t lis2xxx_read_raw(uint8_t dev_addr, acc_raw_t *raw) { uint8_t buf[6]; if (plat_i2c_read_reg(dev_addr, LIS2XXX_OUT_X_L, buf, 6) != 0) { return -1; } raw->x = (int16_t)((uint16_t)buf[1] << 8 | buf[0]); raw->y = (int16_t)((uint16_t)buf[3] << 8 | buf[2]); raw->z = (int16_t)((uint16_t)buf[5] << 8 | buf[4]); return 0; }这里有一个所有ST加速度计都通用的关键点:输出寄存器是补码格式,低字节在前,高字节在后。如果你直接把两个字节拼成uint16_t再强转int16_t,方向是对的,但要注意C语言中左移一个有符号数的行为可能因编译器而异。我在GCC和ARMCC下都验证过,先转成uint16_t再右移填充,结果最稳。
5.2 换算成物理量g
传感器的LSB灵敏度取决于量程。以±2g为例,16位输出,全量程范围是-32768到+32767,对应的物理量是-2g到+2g,所以换算公式是:
acc_g = raw / 32768.0f * full_scale_g代码里不要写死一个比例系数。因为量程是可配置的,我在初始化时就把当前量程保存到一个全局结构体里,换算时候直接取用:
float lis2xxx_raw_to_g(int16_t raw, uint8_t full_scale_g) { return (float)raw / 32768.0f * (float)full_scale_g; }实际测试中,芯片平放时Z轴读数大约0.98g到1.02g之间,X/Y轴在±0.05g以内,这个精度在大多数应用里够用了。如果你的应用需要精确到mg级别,必须做零点偏移校准。
5.3 零点偏移校准
MEMS加速度计出厂会有零点偏移,一般在几十毫g之内。最简单的校准方法:将传感器水平静止放置,采集1000个样本求平均,把这个平均值作为偏移量,之后每次读数都减去它。
要注意:校准时的姿态和环境会影响结果,不要在电机振动平台附近校准。我当时用mbed开发板做了个简单校准程序,静态采集200个点,算出三个轴的平均值,然后写到Flash里作为校正参数。这个操作让倾角模块的精度从±1.5°提升到±0.3°左右。
5.4 滤波处理
加速度计原始数据对振动很敏感。如果是做倾角测量,需要低通滤波;如果是做振动检测,反而可以保留高频分量。最常用的滑动平均滤波实现如下:
#define FILTER_N 8 static int16_t filter_buf[3][FILTER_N]; static uint8_t filter_idx = 0; void acc_filter_push(acc_raw_t *raw) { filter_buf[0][filter_idx] = raw->x; filter_buf[1][filter_idx] = raw->y; filter_buf[2][filter_idx] = raw->z; filter_idx = (filter_idx + 1) % FILTER_N; } acc_raw_t acc_filter_get_average(void) { acc_raw_t avg = {0}; int32_t sum[3] = {0, 0, 0}; for (int i = 0; i < FILTER_N; i++) { sum[0] += filter_buf[0][i]; sum[1] += filter_buf[1][i]; sum[2] += filter_buf[2][i]; } avg.x = (int16_t)(sum[0] / FILTER_N); avg.y = (int16_t)(sum[1] / FILTER_N); avg.z = (int16_t)(sum[2] / FILTER_N); return avg; }这个滤波器简单但有效。注意采样率和滤波窗口要匹配:如果ODR是100Hz,窗口8相当于1秒内做2次更新,实时性会稍差;如果ODR是200Hz以上,窗口8的延迟基本可接受。
6. 常见问题排查与避坑记录
6.1 I2C地址速查
ST加速度计的I2C地址由SA0引脚电平决定。以LIS2DH12为例,7位地址是0x18(SA0=0)或0x19(SA0=1),换算成8位读写地址就是0x30/0x32(写)和0x31/0x33(读)。很多新手在这里出错,因为有些例程使用7位地址,有些使用8位地址。
| 芯片 | SA0=0 | SA0=1 |
|---|---|---|
| LIS2DH12 | 0x18 (7bit) / 0x30 (8bit写) | 0x19 (7bit) / 0x32 (8bit写) |
| LIS2DW12 | 0x18 (7bit) | 0x19 (7bit) |
| LIS2DS12 | 0x18 (7bit) | 0x19 (7bit) |
如果你的I2C总线上还有其他器件,最好用I2C扫描工具先确认实际地址,别猜。
6.2 WHO_AM_I读不对
我碰到过三种情况:
- 用的SPI模式但读指令最高位没拉高,读回来全是0xFF
- 供电电压不足,芯片处于欠压复位状态
- PCB焊接问题,LGA封装太小,虚焊导致某个供电脚或通信脚接触不良
另外,LIS2DH12和LIS2DW12的WHO_AM_I值不一样,如果你拿LIS2DH12的驱动去读LIS2DW12,期望值对不上,函数直接报错,别慌。
6.3 数据全0或全FF
数据全0通常是因为X/Y/Z轴输出没有使能。CTRL_REG1(LIS2DH12)里低三位是X/Y/Z enable,必须都置1。数据全FF则是通信异常,多半是读时序不对或者MOSI/MISO接反了,尤其SPI模式下要重点检查SDI和SDO是否接反。
6.4 高低字节撕裂问题
这个问题我前面提过,BDU没置位时,偶尔会读到很大的跳变,比如实际1g的Z轴突然变成-2g,然后又恢复。排查方法很简单:看跳变是不是有规律、是不是某个固定轴频繁出现,如果是,先置位BDU再试。
6.5 移植官方库和多工具链的坑
有段时间我在非ST平台上直接用ST官网下载的驱动包,编译时到处报错。典型的报错有:
.\libraries\cmsis\cm3\devicesupport\st\stm32f10x\stm32f10x.h(298): error,这是路径和芯片型号宏不对,CMSIS头文件匹配不上#define shell_cmd(n, d, h)这类宏展开时和自定义符号冲突,通常是官方固件库里集成了shell命令行模块,你的工程里又定义同名符号could not verify st device!,这个通常不是代码问题,而是调试工具和芯片连接异常,常见原因是ST-Link/J-Link的SWD线序不对、目标板供电不足
工具链方面,我试过把工程迁移到别处编译,环境变量没配好时会出现类似python was not found或者warning: retrying ...的网络重试提示。这时候不用怀疑代码,先把依赖的工具链和Python环境装齐,再重新编译。烧录调试时,用ST官方工具或者ST Visual Programmer都能识别出这三颗传感器周边的MCU,但识别不到或校验失败往往是因为芯片上的读保护开启了,先解除读保护再烧。
6.6 低功耗模式下的唤醒漏检
用LIS2DW12做低功耗唤醒时,唤醒阈值和持续时间要匹配。如果你唤醒事件设置得太灵敏,振动噪声就会频繁唤醒MCU;太迟钝,又可能漏检真实动作。我实测下来的建议是:唤醒阈值先设到128mg左右,持续采样3个样本确认,这样误触发率明显降低。这个参数我后面在多个板子上复用,效果都不错。
7. 经验总结与扩展方向
整套标准C demo做完,我最深的体会是:传感器驱动本质上就是对寄存器表的清晰理解和稳定的通信抽象。不管是LIS2DH12、LIS2DW12还是LIS2DS12,只要把平台层接口写好,驱动部分就是查数据手册、配置寄存器、读数换算这套流程。
最后再分享一个扩展思路。这套demo写好后,我在它上面做了两个实际功能:
- 步数检测:用LIS2DH12的100Hz输出,对加速度幅值做阈值判断和峰谷计数,在MCU上跑得很稳,不依赖传感器内置计步器
- 倾角传感器:用LIS2DW12的低功耗模式,配合零点校准和低通滤波,输出了稳定的Roll和Pitch角,带载情况下精度在0.5°以内
如果后续你需要在产品里加入自由落体检测或6D方向识别,ST这三颗芯片都有对应的内置功能寄存器,直接在现有demo上添加功能函数就可以,不需要重写底层通信。这是我目前用得最顺的一套传感器驱动结构,建议你也按这个思路整理,后面会省不少事。
本文还有配套的精品资源,点击获取