1. ADXL345传感器与STM32的硬件连接
ADXL345是一款低功耗、高分辨率的三轴加速度传感器,测量范围可达±16g。它支持I2C和SPI两种通信协议,在实际项目中我更喜欢使用I2C接口,因为接线简单且占用IO口少。传感器的工作电压范围是2.0V-3.6V,与STM32的3.3V电平完美匹配。
硬件连接时需要注意几个关键点:首先确保VCC引脚连接3.3V而非5V,否则可能损坏传感器。其次,I2C总线的上拉电阻必不可少,通常使用4.7kΩ电阻将SDA和SCL线拉高。在我的项目中,具体接线如下:
- ADXL345的VCC接STM32的3.3V
- GND接共地
- SDA接PA5(可根据实际需求更改)
- SCL接PA6
- CS引脚接高电平选择I2C模式
实际调试时遇到过I2C通信失败的问题,后来发现是上拉电阻值过大导致信号上升沿不够陡峭。将10kΩ上拉电阻换成4.7kΩ后问题解决。建议在PCB设计时就将上拉电阻预留为可更换的0805封装。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. I2C通信协议实现细节
STM32的硬件I2C外设虽然性能优越,但在某些情况下软件模拟I2C反而更稳定。下面分享我在项目中验证过的可靠实现方案:
首先是GPIO初始化,需要将SDA和SCL引脚配置为开漏输出模式。开漏输出的特点是能够实现"线与"逻辑,这是I2C总线仲裁的基础。关键代码如下:
c复制GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
时序控制是I2C通信的核心。通过逻辑分析仪实测发现,在72MHz主频下,循环次数为10时SCL频率约为205KHz。实际项目中可以根据需要调整延时函数:
c复制void IIC_Delay(void)
{
uint8_t i;
for(i=0; i<10; i++);
}
完整的I2C协议需要实现起始条件、停止条件、应答检测等基本操作。其中最容易出错的是应答检测环节,需要在发送完地址或数据后及时将SDA切换为输入模式:
c复制uint8_t IIC_wait_ack()
{
uint8_t time = 0;
Set_IIC_SDA_IN(); // SDA设置为输入
IIC_SDA_OUT_1; // 释放SDA总线
IIC_Delay();
IIC_SCL_OUT_1; // 拉高SCL
IIC_Delay();
while(IIC_SDA_IN()) // 检测SDA是否为低电平
{
time++;
if(time > 250)
{
IIC_stop();
return 1;
}
}
IIC_SCL_OUT_0; // 拉低SCL完成应答位
IIC_Delay();
return 0;
}
3. ADXL345的寄存器配置技巧
ADXL345有丰富的配置寄存器,合理的设置可以显著提升测量精度。首先是器件ID检测,这是验证通信是否成功的首要步骤:
c复制if(ADXL345_read_reg(DEVICE_ID) != 0xE5)
{
printf("ADXL345检测失败!\r\n");
while(1);
}
DATA_FORMAT寄存器(0x31)的配置尤为关键,它决定了测量范围和分辨率。我的常用配置是0x0B,对应±16g量程和13位全分辨率模式:
c复制ADXL345_write_reg(DATA_FORMAT, 0x0B);
BW_RATE寄存器(0x2C)控制输出数据速率和功耗。对于大多数应用场景,100Hz的输出速率已经足够:
c复制ADXL345_write_reg(BW_RATE, 0x0E); // 100Hz
POWER_CTL寄存器(0x2D)需要特别注意测量模式使能位。很多新手会忘记开启这个位导致读不到数据:
c复制ADXL345_write_reg(POWER_CTL, 0x08); // 测量模式
校准是提高精度的关键步骤。通过OFSX/OFSY/OFSZ偏移寄存器可以补偿零偏误差。我的经验是先采集100次静止状态下的数据取平均,然后计算补偿值:
c复制short x_offset = -sum_x/100;
short y_offset = -sum_y/100;
short z_offset = -(sum_z/100 - 256); // 重力加速度补偿
ADXL345_write_reg(OFSX, x_offset/4);
ADXL345_write_reg(OFSY, y_offset/4);
ADXL345_write_reg(OFSZ, z_offset/4);
4. 数据采集与滤波处理
原始加速度数据通常包含噪声,需要进行滤波处理。我常用的方法是移动平均滤波结合低通滤波。首先实现一个简单的移动平均:
c复制#define FILTER_NUM 10
short filter_buf[3][FILTER_NUM];
u8 filter_cnt = 0;
void filter_data(short *x, short *y, short *z)
{
static short sum[3] = {0};
// 减去最旧的数据
sum[0] -= filter_buf[0][filter_cnt];
sum[1] -= filter_buf[1][filter_cnt];
sum[2] -= filter_buf[2][filter_cnt];
// 存入新数据
filter_buf[0][filter_cnt] = *x;
filter_buf[1][filter_cnt] = *y;
filter_buf[2][filter_cnt] = *z;
// 加上最新的数据
sum[0] += *x;
sum[1] += *y;
sum[2] += *z;
// 计算平均值
*x = sum[0] / FILTER_NUM;
*y = sum[1] / FILTER_NUM;
*z = sum[2] / FILTER_NUM;
filter_cnt = (filter_cnt + 1) % FILTER_NUM;
}
对于需要快速响应的应用,一阶低通滤波是更好的选择。下面是一个优化过的实现:
c复制#define ALPHA 0.2f // 滤波系数
void low_pass_filter(short *x, short *y, short *z)
{
static short last_x = 0, last_y = 0, last_z = 0;
last_x = ALPHA * (*x) + (1-ALPHA) * last_x;
last_y = ALPHA * (*y) + (1-ALPHA) * last_y;
last_z = ALPHA * (*z) + (1-ALPHA) * last_z;
*x = last_x;
*y = last_y;
*z = last_z;
}
在实际项目中,我通常会将两种滤波方法结合使用:先用移动平均滤除高频噪声,再用低通滤波平滑数据。滤波后的数据稳定性明显提升,特别适合用于姿态解算。
5. 姿态解算算法实现
ADXL345最常用的功能就是倾斜检测和姿态估计。通过加速度数据计算倾角的原理是基于重力加速度在各轴上的分量分布。基本公式如下:
c复制void get_angle(float *x_angle, float *y_angle, float *z_angle)
{
short ax, ay, az;
ADXL345_read_average(&ax, &ay, &az, 10);
// 计算X轴倾角(俯仰角)
*x_angle = atan2(ax, sqrt(ay*ay + az*az)) * 180 / PI;
// 计算Y轴倾角(横滚角)
*y_angle = atan2(ay, sqrt(ax*ax + az*az)) * 180 / PI;
// 计算Z轴倾角(偏航角)
*z_angle = atan2(sqrt(ax*ax + ay*ay), az) * 180 / PI;
}
这里有几个优化点值得注意:使用atan2函数替代atan可以避免除零错误;将弧度转换为角度更符合直观理解;sqrt函数计算可以预先优化。
在平衡小车项目中,我发现直接使用角度值会导致控制抖动。更好的做法是引入角度变化率作为微分项:
c复制float last_angle = 0;
float angle_rate = 0;
u32 last_time = 0;
void get_angle_rate(float angle)
{
u32 current_time = HAL_GetTick();
float dt = (current_time - last_time) / 1000.0f;
if(dt > 0)
{
angle_rate = (angle - last_angle) / dt;
last_angle = angle;
last_time = current_time;
}
}
对于需要全姿态解算的应用,可以结合陀螺仪数据做传感器融合。虽然ADXL345单独使用时存在动态响应差的问题,但在低速或静态场景下完全够用。
6. 实际应用中的问题排查
在调试ADXL345时遇到过几个典型问题,这里分享解决方案:
问题1:读数不稳定,波动大
- 检查电源是否干净,建议在VCC附近加0.1uF去耦电容
- 确认传感器是否固定牢固,机械振动会导致数据波动
- 适当增加滤波采样次数,但要注意实时性要求
问题2:角度计算不准确
- 确保设备水平放置时进行校准
- 检查量程设置是否合适,±2g比±16g分辨率更高
- 验证atan2函数实现是否正确
问题3:I2C通信时好时坏
- 用示波器检查SCL/SDA信号质量
- 缩短I2C总线长度,一般不超过20cm
- 尝试降低I2C时钟频率
问题4:功耗偏高
- 检查POWER_CTL寄存器设置
- 考虑使用低功耗模式(设置LOW_POWER位)
- 降低数据输出速率
一个实用的调试技巧是实时输出原始数据:
c复制while(1)
{
short x,y,z;
ADXL345_read_data(&x,&y,&z);
printf("X:%d Y:%d Z:%d\r\n",x,y,z);
HAL_Delay(100);
}
通过观察原始数据可以快速定位是硬件问题还是算法问题。如果原始数据就不稳定,首先检查硬件连接;如果原始数据稳定但角度计算不准,则需要优化算法。
7. 进阶应用:手势识别实现
除了基本的倾斜检测,ADXL345还可以实现简单的手势识别。下面是一个识别上下左右晃动的实现方案:
c复制#define GESTURE_NONE 0
#define GESTURE_UP 1
#define GESTURE_DOWN 2
#define GESTURE_LEFT 3
#define GESTURE_RIGHT 4
u8 gesture_detection(short x, short y, short z)
{
static short last_x = 0, last_y = 0;
static u32 last_time = 0;
u8 gesture = GESTURE_NONE;
u32 current_time = HAL_GetTick();
float dt = (current_time - last_time) / 1000.0f;
if(dt > 0.1f && dt < 1.0f) // 有效时间窗口
{
float x_speed = (x - last_x) / dt;
float y_speed = (y - last_y) / dt;
if(fabs(x_speed) > 1000) // 阈值可调
{
gesture = (x_speed > 0) ? GESTURE_RIGHT : GESTURE_LEFT;
}
else if(fabs(y_speed) > 1000)
{
gesture = (y_speed > 0) ? GESTURE_UP : GESTURE_DOWN;
}
}
last_x = x;
last_y = y;
last_time = current_time;
return gesture;
}
在实际测试中,这种基于速度阈值的方法可以识别90%以上的基本手势。为了提高识别率,可以引入机器学习算法,但需要考虑STM32的计算能力限制。
另一个实用功能是跌落检测,通过判断加速度矢量和是否接近零:
c复制#define FALL_THRESHOLD 500 // 可调阈值
u8 fall_detection(short x, short y, short z)
{
static u8 fall_state = 0;
static u32 fall_time = 0;
float accel = sqrt(x*x + y*y + z*z);
if(accel < FALL_THRESHOLD)
{
if(fall_state == 0)
{
fall_state = 1;
fall_time = HAL_GetTick();
}
else if(HAL_GetTick() - fall_time > 100) // 持续100ms
{
fall_state = 0;
return 1;
}
}
else
{
fall_state = 0;
}
return 0;
}
这些进阶功能展示了ADXL345的应用潜力,开发者可以根据具体需求进行扩展。在资源有限的嵌入式系统中,合理利用传感器特性往往能实现出乎意料的效果。
