
1. 先把这个问题的来龙去脉说清楚1.1 LIS2HH12的高通滤波器到底拿来干嘛LIS2HH12是ST推出的一款超低功耗三轴加速度计12位分辨率支持±2g/±4g/±8g三档量程。它内部集成了一个数字高通滤波器HPF可以在信号路径上把直流分量直接滤掉。这个功能在不少实际项目里非常有用比如计步和手势识别重力是恒定的直流偏置真正有价值的是身体运动产生的动态加速度。高通滤波后可以直接把重力去掉后面做阈值判断会干净很多。振动监测设备安装在一个固定倾角的表面上传感器输出里始终叠着一个由安装角度带来的重力分量。HPF把这一部分剔除后剩下的才是振动信号。跌落检测自由落体时三轴输出接近0g但静止时Z轴有1g的偏置直接用原始值做阈值会很别扭。高通滤波后静态输出归零检测阈值就容易设了。但在实际使用中很多人会遇到一个困惑要启用这个高通滤波器数据手册里提到了一个REFERENCE寄存器地址0x26然后问题就来了——这个REFERENCE值到底怎么算是不是要把静止时读到的加速度值按比例缩放一下再写进去这个疑问不是个例。去看ST社区、StackExchange或者各大嵌入式论坛问LIS2HH12高通滤波参考值的人不在少数而且答案往往说得不够直白导致很多人卡在这个细节上好几天。1.2 论坛上为什么总有人纠结“缩放”这个问题的麻烦之处在于REFERENCE寄存器只有8位而传感器的输出是16位格式虽然是12位有效数据左对齐。一个8位的寄存器一个16位的读数中间显然需要一个转换。问题就出在这个转换上是直接截取高字节还是按比例缩放还是把两个字节都塞进去有人按“缩放”的思路去处理把16位读数除以某个系数再看结果发现怎么都对不上。有人直接写低字节发现滤波器完全没起作用。还有人把16位值硬塞进8位寄存器结果数据被截断输出乱飘。我之前第一次调LIS2HH12时也在这个地方栽过跟头。当时我的做法是静止时读出Z轴原始值发现是正16384左右心想“这是个16位数REFERENCE才8位那我得给它除以64或者128才能放下吧”。结果写进去之后高通滤波器的输出根本没有以零为中心反而出现了一个很大的直流残留。后来仔细读了数据手册里关于输出数据格式的描述才明白根本不是“缩放”的问题而是字节对齐的问题。这类问题用一句很通俗的话概括就是数据手册写的是“register the high byte of the output value”但很多人把它理解成了“scale the output value to fit the register”。一字之差行为天差地别。2. REFERENCE寄存器的真实含义高字节不是系数2.1 数据输出格式与8位寄存器的对应关系先把LIS2HH12的输出格式讲透。这个传感器的每个轴输出占两个寄存器比如X轴是OUT_X_L0x28和OUT_X_H0x29分别存放低字节和高字节。两个字节拼成一个16位的有符号数但12位有效数据是左对齐的也就是说最低4位恒为0。举个例子在±2g量程下静止时水平放置Z轴读到的原始值大约是1g。根据数据手册给出的灵敏度±2g量程下是0.976mg/LSB12位原始值所以1g对应的12位原始值是1024 LSB左右。左对齐到16位之后这个值变成1024 × 16 16384也就是十六进制的0x4000。现在看REFERENCE寄存器。它在寄存器映射表上的说明很简短大意是“高通滤波器的参考值用户可以把静止时输出数据的高字节写入此寄存器以补偿直流偏移”。也就是说这个8位寄存器期望接收的是你16位输出数据的最高有效字节也就是bit15到bit8这8位。回到刚才的例子X/Y/Z轴中Z轴静止读数0x4000最高有效字节就是0x40十进制64。所以你写入REFERENCE的应该是0x40而不是把16384除以256得到64那样“算”出来的。虽然数值恰好一样但理解方式完全不同——你不需要对原始值做任何增益或比例换算只需要按字节截取。这里还要强调一点这个寄存器只存一个8位值。如果你用I2C读到的原始数据是0x4000写入REFERENCE时传0x00低字节肯定错传0x40高字节才对。如果传0x40后面还带着0x00一起写则会把0x00写到下一个寄存器STATUS上这个坑更隐蔽。2.2 算术右移与除法的一字之差那么高字节怎么取最稳妥的方式是用算术右移8位也就是把16位有符号数整体往右移一个字节int16_t raw_z 16384; // 假设读到Z轴16位原始值 uint8_t ref (uint8_t)(raw_z 8); // 得到0x40这里有一个细节值得多说一句。用 8和用/ 256看起来差不多但对于负数两者的结果可能差1。原因是C语言里的整数除法向零取整而有符号右移是向下取整。比如原始值是-16384算术右移8位得到-64对应十六进制的0xC0但如果写成-16384 / 256结果是-64看起来一样可如果是-16383右移8位得到-64除法得到-63。传感器的输出总是12位左对齐低4位为0所以这个差异几乎不会出现但一旦某个模式下输出不是严格左对齐或者你从寄存器里读到了非对齐的噪声数据就会差出1个LSB。我的建议是统一用带符号右移不要用除法。而且读取原始数据时一定要用int16_t类型如果误用了uint16_t负数会变成很大的正数取出来的高字节会完全不对。这种字节序和符号类型的问题在嵌入式调试中属于高发问题。3. 不同量程下的数值换算我该写多少3.1 ±2g/±4g/±8g参考值速查虽然REFERENCE寄存器的本质是“取高字节”但很多人在代码里跑起来之后还是想心里有个数我到底该看到什么数值以1g重力为基准LIS2HH12在三种量程下静止平放时Z轴的参考值大约是下面这样量程12位灵敏度1g对应12位原始值1g对应16位输出值写入REFERENCE的高字节±2g0.976 mg/LSB102416384 (0x4000)64 (0x40)±4g1.952 mg/LSB5128192 (0x2000)32 (0x20)±8g3.904 mg/LSB2564096 (0x1000)16 (0x10)这个规律很好记量程每扩大一倍灵敏度下降一半因此同样1g对应的参考值减半。±2g下写64±4g下写32±8g下写16。注意这个数字指的是“平放时重力方向上的轴”。如果传感器本身的安装角度不是水平读到的可能是三轴的一个矢量组合这时候要按实际读到的原始值取高字节而不是照搬表格。表格的意义在于给你一个数量级的概念帮你快速判断代码读出来的数据是否合理。3.2 通用换算公式与手算示例如果你不想依赖表格可以把逻辑写成一套通用的代码逻辑。核心就是在量程和ODR配置完成、传感器输出稳定之后读取当前姿态下各轴的16位原始值然后右移8位写入// 伪代码采集基准并写入REFERENCE int16_t raw_x, raw_y, raw_z; lis2hh12_read_xyz(raw_x, raw_y, raw_z); write_reg(0x26, (uint8_t)(raw_x 8));有件事我要先说明白LIS2HH12的寄存器映射里REFERENCE只有一个地址0x26是8位全局参考不是每个轴一个。所以严格来说你没法用这一个寄存器同时把X、Y、Z三个轴各自不同的静止偏置都精确补偿掉。这一点很多人会忽略我放到第5章“实操踩坑”里详细说这里先记住结论这个寄存器适合设一个整体参考电平而不是做逐轴精细校准。那“手算示例”是什么意思假设你在±2g量程下把传感器平放Z轴朝上读到的原始数据是这样X轴-16取高字节-16 8 -10xFFY轴8取高字节0Z轴16352取高字节630x3F注意Z轴不是严格的16384因为传感器本身有零点偏置和噪声。你直接把16352右移8位得到63写进去即可。这里不需要去纠结“理论应该是64实际是63要不要修正”。参考值的意义就是“把当前状态视为零”读到的就是什么值直接写不需要再做任何比例调整。如果需要做精确的逐轴零点校准我建议在应用层自己维护三个轴的基准值每次读完原始数据后在软件里做一次减法。用软件处理灵活得多也比依赖这个全局寄存器可靠。4. 完整初始化流程与代码示例4.1 配置顺序为什么重要很多第一次调这颗芯片的人会踩一个顺序坑先配置了高通滤波器再去读静止基准值然后写REFERENCE。表面看逻辑没问题但实际效果可能不对。原因在于一旦HPF被使能FDS1且CTRL2配置完成输出数据就已经被滤波了。你这时读到的“静止值”是滤波后的值不是包含直流分量的原始值。如果一个传感器平放Z轴经过HPF后理论上会慢慢收敛到0而你把这个0读到后右移8位再写回REFERENCE等于告诉滤波器“参考点是0”这没错但你的原始直流信息已经被滤波器吃掉了这个参考值的校准意义就大打折扣。所以正确的顺序应该是先完成基础配置量程、ODR、使能各轴。等待传感器输出稳定读出静止时的原始数据。再使能HPF并配置截止频率。把第2步读到的原始数据高字节写入REFERENCE。这个顺序保证你写入参考值的时刻传感器还没有开始滤除直流你读到的是一个“未经滤波的静态基准”。整个初始化的意义是告诉高通滤波器“当前这个静态状态就是我要归零的参考点”。4.2 I2C读取与字节序的坑LIS2HH12支持I2C和SPI默认I2C地址是0x19SD0/SA0接高或0x18接低。读加速度数据时建议启用BDU块数据更新位避免在读取过程中数据更新导致高字节和低字节来自不同时刻的采样从而出现毛刺。字节序方面LIS2HH12默认是小端模式也就是低字节在前、高字节在后。读回来的数据要这样拼int16_t raw_x (int16_t)((uint16_t)buf[1] 8 | buf[0]);如果你把CTRL4里的BLE位置1变成大端模式那拼接顺序就要反过来。这个跟REFERENCE取值直接相关如果字节拼接错了得到的高字节实际是低字节写进REFERENCE后滤波器的行为会完全乱套。有一种很好排查的方法静止平放时Z轴16位读数理论上应该接近0x4000±2g量程下。如果你读出来是0x4000说明字节序对如果读出来是0x0040说明高低字节拼反了。这个检查非常快强烈建议在调试HPF之前先做。4.3 完整代码流程下面给出一段可参考的STM32平台上的初始化代码用模拟I2C或硬件I2C皆可重点是顺序和取值逻辑#define LIS2HH12_ADDR 0x19 #define LIS2HH12_CTRL1 0x20 #define LIS2HH12_CTRL2 0x21 #define LIS2HH12_CTRL4 0x23 #define LIS2HH12_REFERENCE 0x26 #define LIS2HH12_STATUS 0x27 #define LIS2HH12_OUT_X_L 0x28 void lis2hh12_init_with_hpf(void) { uint8_t buf[6]; int16_t raw_x, raw_y, raw_z; // 1. 先复位并等待稳定 write_reg(LIS2HH12_CTRL2, 0x80); // BOOT1 delay_ms(20); // 2. 基础配置ODR100Hz使能X/Y/Z // CTRL1 0x4F 表示 100Hz 输出低功耗模式关闭三轴使能 write_reg(LIS2HH12_CTRL1, 0x4F); // 3. 配置量程±2g同时开启BDU // CTRL4 0x80BDU1FS[1:0]00 write_reg(LIS2HH12_CTRL4, 0x80); // 4. 等待输出稳定 delay_ms(50); // 5. 读取静止时的原始数据 read_regs(LIS2HH12_OUT_X_L, buf, 6); 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]); // 6. 选择某个轴或手动选择合适的参考值写入REFERENCE // 这里用Z轴的高字节适合平放场景 uint8_t ref (uint8_t)(raw_z 8); write_reg(LIS2HH12_REFERENCE, ref); // 7. 配置高通滤波器FDS1HPFC设为合适的截止频率 // CTRL2 0x01 启用FDSHPFC[3:0]0000 write_reg(LIS2HH12_CTRL2, 0x01); // 8. 使能HPF后丢弃前N个样本让滤波器收敛 for (int i 0; i 10; i) { read_regs(LIS2HH12_OUT_X_L, buf, 6); delay_ms(10); } }这段代码里有两个地方值得解释一下为什么第6步在配置HPF第7步之前因为我们要把原始直流参考传给滤波器而不是让滤波器先把直流吃掉再去猜参考值。为什么第8步要丢样本数字高通滤波器是有暂态响应的。即使参考值配置正确刚使能的一段时间内输出也会有一个从旧状态向新状态收敛的过程。丢弃前10个样本能避免把这个暂态数据当成有效数据用。关于HPFC截止频率我没有在代码里写死具体值因为它和ODR以及你的信号频段强相关。原则是截止频率要明显低于你感兴趣的信号频率但又要高到能较快滤掉重力偏置。比如ODR100Hz如果要滤掉1Hz以下的漂移可以把截止频率设在1~2Hz附近。具体映射关系以数据手册中的表格为准。5. 实操中的踩坑记录与排查思路5.1 写完REFERENCE输出还是偏怎么回事最开始我调这个问题时按照上文的顺序操作完发现Z轴输出确实归零了但X轴和Y轴的输出存在一个相对较大的偏移大概有几十个LSB。当时我第一反应是“参考值写错了”于是反复尝试不同的缩放系数走了不少弯路。后来把寄存器配置一条条捋下来发现问题出在REFERENCE寄存器是全局参考不是逐轴参考。LIS2HH12的0x26寄存器只有8位芯片内部是把同一个参考值应用到三轴的高通滤波支路里。这意味着如果你平放Z轴有1g分量X/Y轴接近0g你写的是Z轴的高字节0x40那么X/Y轴相当于被减掉了一个0.25g左右的偏置。这种情况下X/Y轴的输出当然不可能是理想的零。解决办法有两种如果应用场景是动态监测对X/Y轴的绝对零点不敏感那就不用管它反正HPF会滤掉直流。如果确实需要三个轴都精确归零那不要依赖REFERENCE寄存器改为在软件里做逐轴校准各轴存一个自己的基准值每次读完后在软件里减掉。第二种方式在我的项目里验证过效果比依赖硬件寄存器稳定得多。毕竟软件校准可以随时更新不占用寄存器资源遇到温度漂移也能灵活重采。5.2 温度漂移与运行中更新参考值另一个容易忽略的问题是温度漂移。MEMS加速度计的零点会随温度变化而且变化量远比很多人想象中大。你刚开机时静态读到的参考值在设备运行半小时、温度升高十几度之后可能已经偏了几十甚至上百个LSB。对于只做一次性初始化的代码这会导致一个现象刚上电时HPF输出很干净跑一段时间后输出慢慢出现一个直流分量。这个直流分量不是来自外部加速度而是来自传感器自身零点的温度漂移。处理办法有两种思路在设备进入静止状态时周期性刷新REFERENCE值。这要求应用层有能力判断“当前是否静止”。可以用原始数据方差判断连续几百毫秒方差都很小就认为设备静止然后重新采样基准并更新寄存器。放弃硬件HPF的参考校准改用软件高通滤波。软件实现一个一阶高通滤波器非常简单而且可以随时修改参数不受寄存器数量限制。我做过的几个项目里对温度变化比较敏感的精密测量场景最终都转向了软件滤波。硬件HPF适合快速原型验证软件的灵活性和可维护性要好得多。5.3 与自检、BDU等功能的联动陷阱最后提一个比较隐蔽的坑自检Self-Test功能和REFERENCE之间的关系。LIS2HH12的自检功能会往输出里注入一个已知的偏移量用来验证传感器是否正常工作。如果你在自检前后的静止状态下都去更新REFERENCE那么自检状态下测出的参考值会比正常状态大很多通常是几百个LSB的水平如果你不知道当前开了自检直接把这个值写入REFERENCE等自检关闭后整个滤波器的零点就错了。所以任何涉及REFERENCE的校准逻辑都要确保在校准期间关闭自检并且等待传感器输出稳定。同理BDU位也要先配好。如果BDU没开启你读取16位数据时可能恰好在高低字节之间发生更新拼出来的值一个来自第N次采样一个来自第N1次采样这种脏数据取出来的高字节偶尔会差出几个LSB表现就是滤波后的零点偶尔会跳一下。我的建议是把REFERENCE的更新做成一个独立函数执行前明确做好三件事——关闭自检、开启BDU、等待数据就绪。这样无论后续怎么改配置都不会因为联动关系把参考值搞坏。以上这些坑我都是一步一步踩过来再填回去的。如果你照着文中的顺序和思路去调LIS2HH12的高通滤波大概率能比当年的我少走很多弯路。至于REFERENCE到底要不要缩放现在你应该已经有明确答案了它不是幅度缩放的问题是字节截取的问题。搞清楚这一点剩下的就是按部就班地配置和采样了。