3、加速度计原理与数据读取:MEMS加速度计工作原理、I2C/SPI接口读取、原始数据校准与滤波
好,咱们今天聊聊加速度计。这东西在嵌入式PDA里太常见了,但说实话,真正用好它的人不多。我见过不少项目,加速度计数据读出来就直接用,结果系统动不动就抽风。嗯,这章咱们就把它的底裤扒干净。
3.1 MEMS加速度计工作原理
MEMS加速度计,说白了就是一个微型的弹簧-质量块系统。你想想看,芯片里面有个微小的质量块,用硅悬臂梁挂着。当芯片加速时,质量块会惯性运动,导致电容变化。测量这个电容变化,就能算出加速度。
为什么会这样?核心原理就是牛顿第二定律:F=ma。质量块受到加速度作用产生位移,位移改变电容极板间距,电容值就变了。然后通过ASIC芯片把电容转成电压,再经过ADC变成数字量。
我在项目中遇到过一个问题:某款加速度计在静止时输出值总在跳。后来发现是芯片封装应力导致的。嗯,这里要注意,MEMS器件的封装应力对零偏影响很大,选型时得留个心眼。
关键参数速查表:
| 参数 | 典型值 | 说明 |
|---|---|---|
| 量程 | ±2g / ±4g / ±8g / ±16g | 根据应用场景选择,PDA常用±2g |
| 灵敏度 | 16384 LSB/g (±2g时) | 数字输出分辨率 |
| 零偏 | ±40 mg | 静止时输出偏差,需校准 |
| 噪声密度 | 150 μg/√Hz | 影响滤波设计 |
3.2 I2C/SPI接口读取
读取加速度计数据,主流接口就两种:I2C和SPI。我个人习惯在PDA上用I2C,因为引脚少。但如果你对速率有要求,比如要做高频振动检测,那还是SPI靠谱。
以经典的MPU6050为例,咱们看看I2C怎么读。这芯片我用了不下十次,每次都得翻数据手册确认寄存器地址,后来干脆写了个通用驱动库。
// MPU6050 I2C读取示例
#define MPU6050_ADDR 0x68
#define ACCEL_XOUT_H 0x3B
void read_accel_raw(int16_t *ax, int16_t *ay, int16_t *az) {
uint8_t buf[6];
// 设置寄存器地址
i2c_start(MPU6050_ADDR << 1 | 0); // 写模式
i2c_write(ACCEL_XOUT_H);
i2c_stop();
// 连续读取6字节
i2c_start(MPU6050_ADDR << 1 | 1); // 读模式
for(int i = 0; i < 6; i++) {
buf[i] = i2c_read(i < 5 ? 1 : 0); // 最后字节发NACK
}
i2c_stop();
// 合并高低字节
*ax = (buf[0] << 8) | buf[1];
*ay = (buf[2] << 8) | buf[3];
*az = (buf[4] << 8) | buf[5];
}
个人经验:I2C读取时,连续读比单次读快得多。因为每次重新发起始信号和寄存器地址,开销很大。我一般用连续读模式,一次读完所有轴的数据。
SPI接口呢?速率更高,但要多用3根线。我建议在PDA上这样选:如果加速度计和陀螺仪共用,用I2C省引脚;如果单独用加速度计做高频采集,用SPI。
// SPI读取示例(ADXL345)
#define ADXL345_CS PB0
#define ADXL345_ADDR 0x32 // 0x32 = 0x1D << 1
uint8_t read_reg_spi(uint8_t reg) {
uint8_t data;
CS_LOW();
spi_transfer(reg | 0x80); // 读操作,最高位置1
data = spi_transfer(0x00); // 发送哑字节,接收数据
CS_HIGH();
return data;
}
void read_accel_spi(int16_t *x, int16_t *y, int16_t *z) {
uint8_t buf[6];
CS_LOW();
spi_transfer(0x32 | 0x80 | 0x40); // 多字节读,bit6置1
for(int i = 0; i < 6; i++) {
buf[i] = spi_transfer(0x00);
}
CS_HIGH();
*x = (buf[1] << 8) | buf[0]; // ADXL345是小端模式
*y = (buf[3] << 8) | buf[2];
*z = (buf[5] << 8) | buf[4];
}
注意:不同厂家的加速度计,字节序可能不同。MPU6050是大端,ADXL345是小端。我曾经因为没注意这个,读出来的数据符号反了,排查了半天。
3.3 原始数据校准
原始数据直接能用吗?别闹。每个加速度计都有零偏和刻度误差。你把它平放,理论上Z轴应该是1g,X和Y是0g。但实际读出来呢?Z轴可能是1.02g,X轴可能是0.05g。这就是零偏。
校准方法其实不复杂。我常用的方法是六位置法:
- 把加速度计平放(Z轴朝上),采集N组数据取平均
- 翻转180度(Z轴朝下),再采集N组数据取平均
- 同理,X轴朝上/朝下,Y轴朝上/朝下
- 用这6组数据解算出零偏和刻度因子
嗯,这里要注意,采集数据时一定要让传感器完全静止。我一般采100组数据,去掉最大最小值,再取平均。这样能滤掉随机噪声。
// 简易校准代码
typedef struct {
float bias[3]; // 零偏
float scale[3]; // 刻度因子
} accel_calib_t;
accel_calib_t calibrate_accel(void) {
accel_calib_t calib;
float raw_pos[3], raw_neg[3];
// 采集6个位置的数据(伪代码)
// pos_z_up: Z轴朝上
// pos_z_down: Z轴朝下
// ...
// 计算零偏
calib.bias[0] = (raw_pos[0] + raw_neg[0]) / 2.0f;
calib.bias[1] = (raw_pos[1] + raw_neg[1]) / 2.0f;
calib.bias[2] = (raw_pos[2] + raw_neg[2]) / 2.0f;
// 计算刻度因子
calib.scale[0] = 2.0f / (raw_pos[0] - raw_neg[0]);
calib.scale[1] = 2.0f / (raw_pos[1] - raw_neg[1]);
calib.scale[2] = 2.0f / (raw_pos[2] - raw_neg[2]);
return calib;
}
// 应用校准
void apply_calib(int16_t raw[3], float calibed[3], accel_calib_t *cal) {
for(int i = 0; i < 3; i++) {
calibed[i] = (raw[i] - cal->bias[i]) * cal->scale[i];
}
}
避坑指南:我曾经在量产时发现,同一批次的加速度计零偏差异很大。后来加了出厂校准流程,每个模块单独标定。虽然多了道工序,但产品一致性好了很多。
3.4 滤波处理
校准完的数据就能用了吗?还不行。你看看原始数据的波形,高频噪声一大堆。特别是PDA这种手持设备,手抖一下,数据就跳得飞起。
滤波方法我常用两种:
- 滑动平均滤波:简单粗暴,适合低频应用。取最近N个数据平均,N越大越平滑,但延迟也越大。
- 一阶低通滤波:计算量小,适合实时系统。公式:y[n] = α * x[n] + (1-α) * y[n-1]
我个人习惯用一阶低通。为什么?因为它只需要一个状态变量,内存占用小。在PDA这种资源受限的系统里,能省则省。
// 一阶低通滤波实现
typedef struct {
float alpha; // 滤波系数 0~1
float filtered; // 滤波后的值
} lowpass_filter_t;
void filter_init(lowpass_filter_t *f, float alpha) {
f->alpha = alpha;
f->filtered = 0.0f;
}
float filter_update(lowpass_filter_t *f, float raw) {
f->filtered = f->alpha * raw + (1.0f - f->alpha) * f->filtered;
return f->filtered;
}
// 使用示例
lowpass_filter_t filter_x, filter_y, filter_z;
filter_init(&filter_x, 0.2f); // α=0.2,截止频率约 0.03 * 采样率
filter_init(&filter_y, 0.2f);
filter_init(&filter_z, 0.2f);
// 每次读取后滤波
float fx = filter_update(&filter_x, calibed[0]);
float fy = filter_update(&filter_y, calibed[1]);
float fz = filter_update(&filter_z, calibed[2]);
α值怎么选?我一般这样定:采样率100Hz时,α取0.1~0.3。α越小,滤波越强,但延迟越大。做姿态解算时,α取0.2左右比较平衡。如果只是检测倾斜,α可以取0.05,更平滑。
还有一种情况要注意:如果你要做步数检测或者手势识别,滤波太强会把有用信号也滤掉。这时候我建议用互补滤波,或者干脆不滤波,用原始数据做特征提取。
嗯,说到这,我想起一个项目。当时做PDA的计步器功能,用了滑动平均滤波,结果步数总是少计。后来发现是滤波把步态的峰值削平了。改成中值滤波后,效果好了很多。所以滤波方法没有银弹,得根据应用场景来选。
最后总结一下这章的核心:
- MEMS加速度计本质是电容式微机械结构,输出受封装应力影响
- I2C适合引脚受限的场景,SPI适合高速采集
- 原始数据必须校准,六位置法是最实用的方法
- 滤波要权衡平滑度和延迟,一阶低通是性价比最高的选择
下一章咱们聊陀螺仪,那个更刺激。陀螺仪的零偏漂移问题,我当年可是被它折磨得不轻。到时候给你讲讲怎么用艾伦方差分析噪声特性。