STM32CubeMX实战:ADXL345传感器数据采集与自动校准全流程(附源码)
从零到一:基于STM32CubeMX与ADXL345构建高精度运动感知系统
最近在做一个智能穿戴设备的原型,需要实时监测设备的姿态和运动轨迹。选型时,ADXL345这款三轴加速度传感器以其高集成度和数字接口脱颖而出。但真正上手时,我发现网上很多资料要么过于零散,只讲寄存器配置;要么就是代码片段堆砌,缺乏从硬件连接到数据处理、再到误差校准的完整链路。对于需要快速将传感器用起来的工程师或初学者来说,这种割裂感让人头疼。
这篇文章,我想和你分享的,不仅仅是如何用STM32CubeMX配置I2C读取几个数据,而是一套从项目视角出发的完整解决方案。我们会一起搭建硬件环境,编写稳健的驱动,实现数据滤波,并深入探讨一个常被忽略但至关重要的环节——传感器自动校准。校准做得好,数据的可信度能提升一个数量级,这对于姿态检测、计步等应用来说是基石。整个过程我会附上可复用的源码和踩坑经验,目标是让你看完就能动手,做出稳定可靠的运动感知模块。
1. 项目蓝图:硬件选型与STM32CubeMX工程初始化
在写第一行代码之前,合理的硬件布局和软件框架规划能避免后期大量返工。我们这次的核心是STM32F103C8T6(Blue Pill板很常见)和ADXL345模块。ADXL345模块通常已经集成了必要的上拉电阻和滤波电容,我们只需要关心四根线:VCC、GND、SCL、SDA。
注意:市面上常见的ADXL345模块,其I2C地址选择引脚(SDO/ALT ADDRESS)通常已通过焊盘选择接地或接VCC。务必在购买或焊接前确认,这决定了后续编程中的设备地址是0x53还是0x1D。
使用STM32CubeMX初始化项目,能极大节省底层配置时间。打开软件,选择你的STM32型号后,我们按步骤进行关键配置:
- 系统核心(SYS):在
Debug栏选择Serial Wire,这是ST-Link调试所必需的。 - 时钟(RCC):
High Speed Clock (HSE)选择Crystal/Ceramic Resonator,为系统提供精准时钟源。 - I2C接口:根据你的硬件连接,启用
I2C1或I2C2。模式选择I2C,参数通常保持默认的Standard Mode(100kHz)即可。如果后续需要高速传输,可以在这里提升速率。 - GPIO(可选但推荐):配置一个LED引脚(如PC13)为
Output,用于指示系统状态;配置一个按键引脚(如PA0)为Input,设置为Pull-down,用于触发我们后续的校准功能。 - 时钟树配置:这是CubeMX的精华。将HSE输入后,通过PLL倍频,将系统时钟(
SYSCLK)设置到芯片允许的最高频率(例如72MHz),以获得最佳性能。
完成这些后,给你的工程起个名字,选择好IDE(Keil MDK或STM32CubeIDE等),关键一步是在Project Manager的Code Generator选项卡中,勾选“Generate peripheral initialization as a pair of ‘.c/.h’ files per IP”。这会让每个外设(如I2C、GPIO)的代码独立成对的文件,结构清晰,方便管理。
点击GENERATE CODE,一个包含所有初始化代码的工程就创建好了。这个工程已经具备了I2C通信的硬件基础,接下来我们要做的,就是让ADXL345“活”起来。
2. 驱动层构建:与ADXL345的稳健对话
生成的工程直接打开,我们的主战场在main.c和即将创建的传感器驱动文件。我习惯为每个重要的外设传感器创建独立的.[c|h]文件,比如adxl345.c和adxl345.h。这符合模块化编程思想,方便移植和复用。
首先,在adxl345.h中定义核心的寄存器地址、设备地址和函数接口。这里有个细节:ADXL345的很多功能寄存器是只读或需要特定序列写入的,仔细查阅数据手册能避免很多奇怪的问题。
// adxl345.h 部分关键定义
#define ADXL345_ADDR_WRITE 0x3A // 假设SDO接地,写地址
#define ADXL345_ADDR_READ 0x3B // 读地址
// 重要寄存器地址
#define ADXL345_DEVID 0x00 // 器件ID,固定为0xE5
#define ADXL345_DATA_FORMAT 0x31 // 数据格式控制(量程、分辨率等)
#define ADXL345_BW_RATE 0x2C // 数据速率与带宽
#define ADXL345_POWER_CTL 0x2D // 电源控制
#define ADXL345_DATAX0 0x32 // X轴数据低字节
#define ADXL345_OFSX 0x1E // X轴偏移校准寄存器
// ... 其他寄存器定义
// 函数声明
uint8_t ADXL345_Init(I2C_HandleTypeDef *hi2c);
void ADXL345_ReadRawData(I2C_HandleTypeDef *hi2c, int16_t *x, int16_t *y, int16_t *z);
驱动函数的核心是ADXL345_Init。它的任务不仅仅是让传感器工作,更是将其配置到适合我们应用的状态。比如,对于运动检测,我们可能关心较高的数据输出速率(如100Hz);而对于静态倾角测量,则可能选择较低的速率和更宽的滤波带宽来抑制噪声。
// adxl345.c 中的初始化函数
uint8_t ADXL345_Init(I2C_HandleTypeDef *hi2c) {
uint8_t check_id, config_data;
// 1. 验证器件ID,确认通信正常
HAL_I2C_Mem_Read(hi2c, ADXL345_ADDR_READ, ADXL345_DEVID, 1, &check_id, 1, HAL_MAX_DELAY);
if (check_id != 0xE5) {
return 1; // 通信失败或器件错误
}
// 2. 配置数据格式:±4g量程,13位全分辨率,数据右对齐
config_data = 0x01; // 对应DATA_FORMAT寄存器,Range bits = 01 (±4g)
HAL_I2C_Mem_Write(hi2c, ADXL345_ADDR_WRITE, ADXL345_DATA_FORMAT, 1, &config_data, 1, HAL_MAX_DELAY);
// 3. 配置输出数据速率:100 Hz
config_data = 0x0A; // Rate code = 0x0A (100 Hz)
HAL_I2C_Mem_Write(hi2c, ADXL345_ADDR_WRITE, ADXL345_BW_RATE, 1, &config_data, 1, HAL_MAX_DELAY);
// 4. 配置电源模式:进入测量模式
config_data = 0x08; // Measure bit = 1
HAL_I2C_Mem_Write(hi2c, ADXL345_ADDR_WRITE, ADXL345_POWER_CTL, 1, &config_data, 1, HAL_MAX_DELAY);
// 5. 初始清零偏移寄存器(校准前)
config_data = 0;
HAL_I2C_Mem_Write(hi2c, ADXL345_ADDR_WRITE, ADXL345_OFSX, 1, &config_data, 1, HAL_MAX_DELAY);
HAL_I2C_Mem_Write(hi2c, ADXL345_ADDR_WRITE, ADXL345_OFSY, 1, &config_data, 1, HAL_MAX_DELAY);
HAL_I2C_Mem_Write(hi2c, ADXL345_ADDR_WRITE, ADXL345_OFSZ, 1, &config_data, 1, HAL_MAX_DELAY);
HAL_Delay(10); // 等待传感器稳定
return 0; // 初始化成功
}
数据读取函数ADXL345_ReadRawData则利用HAL库的HAL_I2C_Mem_Read连续读取多个寄存器。这里有一个效率上的小优化:ADXL345的数据寄存器(X0, X1, Y0, Y1, Z0, Z1)是连续地址,我们可以用一次多字节读取操作获取全部6个字节的数据,而不是分6次读取,这能显著减少I2C总线开销。
void ADXL345_ReadRawData(I2C_HandleTypeDef *hi2c, int16_t *x, int16_t *y, int16_t *z) {
uint8_t data_buf[6];
// 一次性读取从DATAX0开始的6个连续寄存器
HAL_I2C_Mem_Read(hi2c, ADXL345_ADDR_READ, ADXL345_DATAX0, 1, data_buf, 6, HAL_MAX_DELAY);
// 组合数据(注意字节序,ADXL345为小端,即低字节在前)
*x = ((int16_t)data_buf[1] << 8) | data_buf[0];
*y = ((int16_t)data_buf[3] << 8) | data_buf[2];
*z = ((int16_t)data_buf[5] << 8) | data_buf[4];
}
至此,驱动层搭建完毕。在main.c中初始化I2C并调用ADXL345_Init后,你就能在循环中不断读取到原始的加速度数据了。但这些数据是“毛糙”的,直接使用往往噪声很大,接下来我们就来处理它。
3. 信号调理:从原始数据到可信信息
直接从传感器读出的数据通常包含两种“干扰”:一种是高频的随机噪声,另一种是传感器本身的零偏(即使静止时输出也不为零)。前者我们用软件滤波处理,后者则通过校准来解决。
软件滤波是嵌入式信号处理的基本功。对于加速度计,常用的有均值滤波、滑动平均滤波和一阶低通滤波。选择哪种,取决于你的应用场景和对实时性的要求。
- 均值滤波:最简单,采集N个样本后求平均。能有效抑制随机噪声,但会引入N/2个采样周期的延迟,适合对实时性要求不高的数据上报场景。
void ADXL345_ReadAverage(I2C_HandleTypeDef *hi2c, int16_t *x_avg, int16_t *y_avg, int16_t *z_avg, uint8_t times) { int32_t sum_x = 0, sum_y = 0, sum_z = 0; int16_t x_temp, y_temp, z_temp; for(uint8_t i=0; i<times; i++) { ADXL345_ReadRawData(hi2c, &x_temp, &y_temp, &z_temp); sum_x += x_temp; sum_y += y_temp; sum_z += z_temp; HAL_Delay(5); // 根据数据速率适当延时,避免读取过快 } *x_avg = (int16_t)(sum_x / times); *y_avg = (int16_t)(sum_y / times); *z_avg = (int16_t)(sum_z / times); } - 一阶低通滤波(指数加权平均):更优雅,计算量小,能平滑数据的同时保持较好的实时性。其公式为
filtered_data = α * new_data + (1-α) * old_filtered_data。α是滤波系数(0<α≤1),α越大,对新数据响应越快,但平滑效果越弱。// 全局变量存储滤波后的值 float filtered_x = 0, filtered_y = 0, filtered_z = 0; #define ALPHA 0.2f // 滤波系数,可调整 void ADXL345_LowPassFilter(I2C_HandleTypeDef *hi2c) { int16_t x_raw, y_raw, z_raw; ADXL345_ReadRawData(hi2c, &x_raw, &y_raw, &z_raw); filtered_x = ALPHA * x_raw + (1 - ALPHA) * filtered_x; filtered_y = ALPHA * y_raw + (1 - ALPHA) * filtered_y; filtered_z = ALPHA * z_raw + (1 - ALPHA) * filtered_z; }
为了让你更直观地感受不同滤波方法的效果,我整理了一个简单的对比表格:
| 滤波方法 | 原理 | 实时性 | 平滑效果 | 适用场景 | 代码/计算复杂度 |
|---|---|---|---|---|---|
| 均值滤波 | 取N次采样算术平均 | 差(延迟大) | 好 | 静态测量、数据上报 | 低 |
| 滑动平均 | 维护一个长度为N的队列,每次更新平均值 | 中 | 好 | 实时性要求稍高的数据流 | 中 |
| 一阶低通 | 指数加权递归平均 | 好 | 中 | 需要快速响应的动态检测 | 低 |
在我的运动检测项目中,我选择了一阶低通滤波。因为设备需要快速响应姿态变化,同时又希望读数看起来稳定不跳动。经过反复测试,将ALPHA设置为0.2到0.3之间,在响应速度和平滑度之间取得了不错的平衡。
滤波解决了随机噪声,但传感器固有的零偏和灵敏度误差则需要校准。ADXL345内部提供了三个专用的偏移寄存器(OFSX, OFSY, OFSZ),允许我们写入补偿值。接下来的章节,我们将深入自动校准的逻辑。
4. 自动校准实战:消除零偏与提升精度
校准是提升传感器实用精度的关键一步。想象一下,你的设备平放在桌面上,理想情况下Z轴应该输出1g(约256 LSB,在±2g量程下),X和Y轴输出0。但实际由于安装应力、芯片本身差异等原因,输出会有偏差。自动校准的目的,就是通过算法计算出这个偏差值,并自动写入偏移寄存器。
校准的前提是已知姿态。最常用的方法是六面校准法:将传感器依次以六个不同的正交面朝下静止放置,分别记录每个面的输出,理论上,每个轴的正反两个方向的输出应该大小相等、符号相反。通过计算,可以解算出零偏和比例因子。
但对于很多集成在设备内部、不便拆卸的传感器,我们更常用的是静态水平面校准。假设设备只有一个固定的安装姿态(例如,电路板水平放置),我们只需要校准这个姿态下的零偏。其核心思想是:在静止状态下,传感器感受到的合力应该只有重力加速度。
基于这个原理,自动校准函数ADXL345_AutoCalibrate的工作流程如下:
- 准备阶段:确保传感器处于稳定的测量模式,并清空旧的偏移值。
- 数据采集:在设备保持水平静止的状态下,连续采集多组数据(如100次)并求平均,得到静止状态下的原始输出
(X_raw, Y_raw, Z_raw)。 - 理论值计算:在水平静止时,理想输出应为
(0, 0, +1g)。1g对应的LSB值取决于你设置的量程(Range)。例如,在±2g量程、13位分辨率下,1g ≈ 256 LSB。 - 偏差计算:
- X轴零偏 = 0 - X_raw_avg
- Y轴零偏 = 0 - Y_raw_avg
- Z轴零偏 = (+1g的LSB值) - Z_raw_avg
- 写入寄存器:将计算出的零偏值(注意寄存器格式,通常是补码且分辨率是LSB的1/4或1/2,需查阅数据手册)写入OFSX, OFSY, OFSZ寄存器。
下面是一个简化版的自动校准函数实现,它假设设备已水平放置,并通过一个按键(如KEY_UP)来触发:
void ADXL345_AutoCalibrate(I2C_HandleTypeDef *hi2c) {
int32_t sum_x = 0, sum_y = 0, sum_z = 0;
int16_t x_temp, y_temp, z_temp;
uint8_t samples = 50;
int16_t offset_x, offset_y, offset_z;
uint8_t reg_val;
printf("开始校准,请保持设备水平静止...\r\n");
HAL_Delay(2000); // 给用户时间准备
// 采集静止状态下的样本
for(uint8_t i=0; i<samples; i++) {
ADXL345_ReadRawData(hi2c, &x_temp, &y_temp, &z_temp);
sum_x += x_temp;
sum_y += y_temp;
sum_z += z_temp;
HAL_Delay(20); // 50Hz采样率
}
// 计算平均值
int16_t avg_x = sum_x / samples;
int16_t avg_y = sum_y / samples;
int16_t avg_z = sum_z / samples;
// 计算偏移量。假设量程为±2g,则1g = 256 LSB。
// OFS寄存器分辨率是15.6 mg/LSB (≈ 1/64 of 1g),所以偏移量需要除以4。
offset_x = (0 - avg_x) / 4;
offset_y = (0 - avg_y) / 4;
offset_z = (256 - avg_z) / 4; // 期望Z轴输出+1g (256 LSB)
// 将偏移量写入传感器(注意转换为uint8_t,寄存器为8位有符号补码)
reg_val = (uint8_t)(offset_x & 0xFF);
HAL_I2C_Mem_Write(hi2c, ADXL345_ADDR_WRITE, ADXL345_OFSX, 1, ®_val, 1, HAL_MAX_DELAY);
reg_val = (uint8_t)(offset_y & 0xFF);
HAL_I2C_Mem_Write(hi2c, ADXL345_ADDR_WRITE, ADXL345_OFSY, 1, ®_val, 1, HAL_MAX_DELAY);
reg_val = (uint8_t)(offset_z & 0xFF);
HAL_I2C_Mem_Write(hi2c, ADXL345_ADDR_WRITE, ADXL345_OFSZ, 1, ®_val, 1, HAL_MAX_DELAY);
printf("校准完成。偏移量: X=%d, Y=%d, Z=%d\r\n", offset_x, offset_y, offset_z);
}
提示:校准的精度极度依赖于设备在校准过程中的静止程度。务必将其放在一个稳固、水平的表面上。如果条件允许,进行多次校准取平均,或实现更复杂的六面校准算法,精度会更高。
校准完成后,再次读取数据,你会发现X和Y轴在水平静止时非常接近0,Z轴则稳定在+1g附近。这个简单的自动校准流程,能将传感器的静态精度提升一个档次,为后续的姿态解算等应用打下坚实基础。
5. 数据应用:从加速度到姿态角
获得干净、校准后的加速度数据后,我们就可以进行一些有趣的应用了。最常见的就是倾角测量。当传感器静止时,加速度计测得的唯一力就是重力加速度,其在三个轴上的分量与传感器的倾斜角度有直接的三角函数关系。
例如,计算传感器X轴与水平面的夹角(俯仰角): pitch = arctan( X / sqrt(Y^2 + Z^2) ) * (180 / π)
同理,Y轴对应的滚转角: roll = arctan( Y / sqrt(X^2 + Z^2) ) * (180 / π)
在C语言中,我们可以这样实现:
#include <math.h> // 需要链接数学库,在Keil中勾选“Use MicroLIB”或添加-lm
float ADXL345_GetPitch(int16_t x, int16_t y, int16_t z) {
// 将LSB值转换为重力加速度g
float g_x = (float)x / 256.0; // 假设±2g量程,灵敏度256 LSB/g
float g_y = (float)y / 256.0;
float g_z = (float)z / 256.0;
// 计算俯仰角,单位度
float pitch = atan2(g_x, sqrt(g_y*g_y + g_z*g_z)) * 180.0 / M_PI;
return pitch;
}
float ADXL345_GetRoll(int16_t x, int16_t y, int16_t z) {
float g_x = (float)x / 256.0;
float g_y = (float)y / 256.0;
float g_z = (float)z / 256.0;
// 计算滚转角,单位度
float roll = atan2(g_y, sqrt(g_x*g_x + g_z*g_z)) * 180.0 / M_PI;
return roll;
}
注意:
atan2(y, x)函数比atan(y/x)更安全,它能正确处理所有象限的角度。另外,这种基于重力分量的姿态解算只适用于静态或准静态情况。一旦传感器本身存在线性加速度(比如在移动的车上),这种方法就会产生很大误差。对于动态姿态跟踪,需要融合陀螺仪的数据,使用互补滤波或卡尔曼滤波等算法。
最后,将这些功能整合到主循环中。一个典型的应用框架是:初始化后进入主循环,循环内读取滤波后的数据,计算角度,并通过串口打印或显示屏显示。同时,不断检测校准按键,当按键按下时,执行自动校准流程。
// main.c 主循环示例
int main(void) {
// HAL初始化、时钟、外设初始化...
ADXL345_Init(&hi2c1); // 初始化传感器
printf("System & ADXL345 Ready.\r\n");
int16_t acc_x, acc_y, acc_z;
float pitch, roll;
while (1) {
// 1. 读取并滤波
ADXL345_ReadAverage(&hi2c1, &acc_x, &acc_y, &acc_z, 5);
// 2. 计算姿态角
pitch = ADXL345_GetPitch(acc_x, acc_y, acc_z);
roll = ADXL345_GetRoll(acc_x, acc_y, acc_z);
// 3. 输出(通过串口)
printf("Acc: X=%6d, Y=%6d, Z=%6d | Pitch=%5.2f°, Roll=%5.2f°\r\n",
acc_x, acc_y, acc_z, pitch, roll);
// 4. 检查校准按键
if(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_SET) {
HAL_Delay(50); // 简单消抖
if(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_SET) {
ADXL345_AutoCalibrate(&hi2c1);
while(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_SET); // 等待按键释放
}
}
HAL_Delay(100); // 控制循环周期
}
}
在实际部署时,你可能会遇到I2C通信偶尔失败的情况。一个健壮的做法是在驱动层增加重试机制,比如连续读取器件ID三次,只有全部失败才报错。另外,对于需要低功耗的应用,别忘了在不需要测量时,通过写POWER_CTL寄存器将ADXL345设置为待机模式。
更多推荐



所有评论(0)