ADXL345三轴驱动与计步算法实战:软件模拟IIC与波峰检测
发布时间:2026/11/3 11:46:51 锦皓数字建站

简介这份资源面向嵌入式开发初学者与需要快速接入三轴加速度传感器的工程师提供ADXL345驱动代码的完整实现与知识点梳理可用于运动检测、跌倒检测与计步等场景的二次开发。压缩包内共1个doc文件约63KB以文档形式集中呈现驱动源码与函数说明便于对照阅读和移植。已有474人学习下载具备一定的参考基础。内容围绕IIC通信协议展开逐一讲解IIC_Init、IIC_Start、IIC_Stop、IIC_Wait_Ack、IIC_Ack、IIC_NAck、IIC_Send_Byte、IIC_Read_Byte等底层函数的实现细节并给出AD_Wtite_One_Byte、AD_Write_Multi_Byte等寄存器读写封装同时涵盖计步算法的思路说明与实际应用测试结论。读者可据此理解GPIO推挽输出配置、时序延时控制与应答超时处理等关键点快速搭建可稳定运行的ADXL345驱动框架。1. 从一块焊歪的 ADXL345 说起这套驱动到底能干什么去年帮朋友调一个手环原型板子焊完上电串口里加速度三个轴全是 0示波器一量 SCL 有波形、SDA 死活不动。翻来覆去查了两小时最后发现是软件模拟 IIC 的引脚宏定义和实际飞线对不上。那次之后我对“三轴加速度计驱动”这类代码有了个基本判断能不能跑八成不取决于算法多花哨而取决于 IIC 时序和寄存器配置这两块地基有没有打牢。这份 ADXL345 三轴驱动代码就是典型的“地基型”资源它把软件模拟 IIC 的完整时序、ADXL345 的寄存器读写封装、以及一套基于波峰检测的计步逻辑全塞进了一个adxl.c里配合main.h里的引脚宏就能在 STM32 这类 Cortex-M 平台上跑起来。适合谁正在做可穿戴、运动检测、跌倒报警这类小项目的开发者尤其是手里有 ADXL345 模块、想快速拿到三轴原始数据和步数、又不想从零啃数据手册的人。它不依赖硬件 IIC 外设GPIO 推挽输出就能驱动移植成本低这是它最实在的价值。2. 代码结构拆解一个 adxl.c 里藏着的三层职责2.1 文件组织与依赖关系这份代码的物理结构非常扁平核心就两个文件adxl.c和它 include 的main.h。main.h承担了所有“平台相关”的东西——引脚宏、位带操作、延时函数声明、标准库头文件。adxl.c则从下往上叠了三层最底层是软件模拟 IIC 的时序函数中间层是 ADXL345 的寄存器读写封装最上层是数据读取、滤波和计步算法。这种分层在裸机项目里很常见好处是换 MCU 时只需要改main.h里的宏和IIC_Init里的时钟使能上层逻辑一行不用动。引脚定义这块代码里用的是 PB10 和 PB11通过宏映射到 SDA 和 SCL。我一般会把这类宏集中放在main.h顶部并且加上注释标明实际接线避免后面飞线改了忘了同步。位带操作是这套代码的一个隐含依赖IIC_SDA、READ_SDA这些宏大概率是基于 Cortex-M 的位带别名实现的这样单根引脚读写不用读改写整个端口寄存器时序更干净。如果你换到没有位带的平台比如某些 RISC-V这部分得改成HAL_GPIO_ReadPin之类的调用延时也要重新标定。/* main.h 中典型的引脚宏与位带定义按代码推断补全 */ #define IIC_SCL PBout(11) /* SCL 接 PB11 */ #define IIC_SDA PBout(10) /* SDA 输出接 PB10 */ #define READ_SDA PBin(10) /* SDA 输入读 PB10 */ #define SDA_IN() {GPIOB-CRH 0XFFFFF0FF; GPIOB-CRH | (u32)88;} #define SDA_OUT() {GPIOB-CRH 0XFFFFF0FF; GPIOB-CRH | (u32)38;}上面这段是SDA_IN/SDA_OUT的典型写法通过改 GPIOB 的 CRH 寄存器把 PB10 在输入和推挽输出之间切换。参数说明0xFFFFF0FF是清掉 PB10 对应的 4 个配置位88表示上拉/下拉输入模式38表示 50MHz 推挽输出。这里有个细节代码里IIC_Init把 PB10、PB11 都设成了推挽输出并置高但 SDA 在读取时必须切输入所以SDA_IN/SDA_OUT这对宏是整套 IIC 能工作的前提缺一不可。2.2 软件模拟 IIC 的时序实现软件模拟 IIC 的核心就四件事起始、停止、应答、读写字节。代码里IIC_Start的做法是 SDA 先拉高、SCL 拉高、延时 4us然后 SDA 拉低产生下降沿再拉低 SCL 钳住总线。这个顺序不能反因为 IIC 协议规定起始条件是 SCL 高电平期间 SDA 由高变低。IIC_Stop则相反SCL 低时先拉低 SDA再拉高 SCL最后拉高 SDA 产生上升沿。void IIC_Start(void) { SDA_OUT(); IIC_SDA 1; IIC_SCL 1; delay_us(4); IIC_SDA 0; /* SCL 高期间 SDA 下降沿 起始 */ delay_us(4); IIC_SCL 0; /* 钳住总线 */ }延时 4us 对应的是 100kHz 左右的 IIC 速率ADXL345 支持 400kHz但软件模拟留点余量更稳。IIC_Wait_Ack里有个 250 次的超时计数超时就调IIC_Stop并返回 1这个设计很关键——没有超时保护的模拟 IIC 一旦从机没响应就会死等整个主循环卡死。IIC_Send_Byte是高位先行每次把txd的最高位送到 SDA然后左移一位8 次循环发完。IIC_Read_Byte则在 SCL 高电平期间采样 SDA先左移再累加最后根据ack参数决定发 ACK 还是 NACK。提示IIC_Send_Byte的注释里提到“对 TEA5767 这三个延时都是必须的”这是从别的项目抄过来没改干净的痕迹不影响功能但说明这段 IIC 代码是经过多个项目复用的老代码时序参数相对保守。2.3 ADXL345 寄存器读写封装中间层的四个函数AD_Wtite_One_Byte、AD_Write_Multi_Byte、AD_Read_One_Byte、AD_Read_Multi_Byte把 IIC 时序和 ADXL345 的器件地址、寄存器地址拼在一起。写操作的流程是起始 → 发写命令WRITE_CMD通常是 0xA6→ 等 ACK → 发寄存器地址 → 等 ACK → 发数据 → 等 ACK → 停止。读操作稍微绕一点要先发写命令和寄存器地址“告诉芯片我要读哪个寄存器”然后重新起始、发读命令READ_CMD0xA7再读数据。uint8 AD_Read_One_Byte(uint8 reg) { uint8 data 0; IIC_Start(); IIC_Send_Byte(WRITE_CMD); /* 0xA6写方向 */ IIC_Wait_Ack(); IIC_Send_Byte(reg); /* 指定要读的寄存器 */ IIC_Wait_Ack(); IIC_Start(); /* 重复起始 */ IIC_Send_Byte(READ_CMD); /* 0xA7读方向 */ IIC_Wait_Ack(); data IIC_Read_Byte(0); /* 单字节读回 NACK */ IIC_Stop(); return data; }AD_Read_Multi_Byte是读三轴数据的主力它利用 ADXL345 寄存器地址自增的特性从DATAX0开始连续读 6 个字节正好覆盖 X、Y、Z 的低字节和高字节。这里最后一个字节必须回 NACK否则从机会继续输出数据总线状态就乱了。参数len是uint8类型意味着一次最多读 255 字节对 ADXL345 的 6 字节读取绰绰有余。2.4 初始化流程与关键寄存器AD_Initial是整个驱动的入口它先调IIC_Init然后读0x00寄存器判断返回值是不是0xE5——这是 ADXL345 的器件 ID读不到就返回 1 表示失败。这个“读 ID 验活”的做法值得抄比盲目写寄存器然后发现没反应要高效得多。验活通过后依次写六个寄存器DATA_FORMAT写0x69±4g 量程、全分辨率 11 位、BW_RATE写0x0950Hz 输出速率、POWER_CTL写0x28进入测量模式、INT_ENABLE和INT_MAP写0x80关中断、三个偏移寄存器写 0。寄存器写入值作用DATA_FORMAT0x69±4g全分辨率11 位模式BW_RATE0x09输出数据速率 50HzPOWER_CTL0x28使能测量进入正常工作INT_ENABLE0x80关闭 DATA_READY 中断OFSX/Y/Z0x00三轴偏移清零0x69这个值拆开看bit3 是 FULL_RES 置 1bit1:0 是 01 对应 ±4g其余位保留。±4g 量程对应灵敏度约 256 LSB/g全分辨率下 3.9mg/LSB这个量程对计步够用对跌落检测可能偏小后面改造时会提到。POWER_CTL写0x28是置 bit3Measure和 bit5Link让芯片开始采样。初始化返回 0 表示成功返回 1 表示 ID 校验失败调用方根据这个返回值决定是否重试或报错。3. 数据读取与计步算法从原始加速度到步数3.1 三轴数据合成与符号处理Read_Axis负责把 6 个字节拼成三个int16。ADXL345 的数据是 16 位补码小端格式低字节在前所以buf[1]8 | buf[0]再强转int16就能得到带符号的加速度值。这里有个容易翻车的点buf是uint8数组左移时如果先转u16再移位能避免符号扩展问题代码里((u16)buf[1]8)buf[0]正是这么做的。如果直接buf[1]8在某些编译器上uint8会被提升成int高位字节为 0x80 以上时结果就错了。void Read_Axis(int16 *data) { uint8 buf[6] {0}; AD_Read_Multi_Byte(DATAX0, buf, 6); *data (int16)(((u16)buf[1] 8) buf[0]); /* X */ *(data1) (int16)(((u16)buf[3] 8) buf[2]); /* Y */ *(data2) (int16)(((u16)buf[5] 8) buf[4]); /* Z */ }参数data是调用方传进来的三元素数组首地址函数内部用指针偏移写回。这种“传数组首地址 内部偏移”的写法在裸机里很常见省去返回值拷贝但调用方必须保证数组至少 3 个int16的空间否则就是内存越界。3.2 波峰检测计步的状态机计步逻辑集中在Filter_AD_Data和Filter_AD_Data_1两个函数里后者是前者的增强版多了波峰上升沿/下降沿的细分状态。核心思路是对每个轴维护一个Step_P_Flag状态0 表示找最小值非 0 表示找最大值当加速度从最小值爬升越过阈值再回落到最大值以下时判定为一个波峰计算峰峰值Step_P2P再结合阈值Axis_Threhold和时间窗Sample_Timer决定是否计一步。if (Step_P_Flag[i] 0) { /* 找最小值阶段 */ if (Axis_ResultA[i] Step_Min[i]) Step_Min[i] Axis_ResultA[i]; else { Step_Max[i] Axis_ResultA[i]; Step_P_Flag[i] ^ 0xff; /* 翻转到找最大值 */ } } else { /* 找最大值阶段 */ if (Axis_ResultA[i] Step_Max[i]) Step_Max[i] Axis_ResultA[i]; else { Step_P2P[i] Cal_Abs_Value(Step_Max[i], Step_Min[i]); /* 幅值 门限 时间窗三重判定 */ if (Step_P2P[i] Axis_Precision[i] Step_Min[i] Axis_Threhold[i] Axis_Threhold[i] Step_Max[i] Sample_Timer 10) Judge_Show(); Step_Min[i] Axis_ResultA[i]; Step_P_Flag[i] ^ 0xff; } }这段状态机的关键参数有三个Axis_Precision是幅值精度门限峰峰值小于它认为是噪声Axis_Threhold是过零门限要求最小值在门限以下、最大值在门限以上相当于确认信号穿过了基线Sample_Timer是时间窗至少 10 个采样周期才允许计步防止高频抖动被误判。Judge_Show是实际累加步数的钩子函数代码里没给实现需要自己补。Select_Axis负责选当前波动最大的轴作为计步参考轴这样手机横放竖放都能工作。3.3 滑动窗口自适应阈值Judge_Extreme_Ad、Cal_Threhold_Precision、Exchage_Extrem_Reg这三个函数配合Decide_Sample_No计数器实现了一个 50 个采样周期的滑动窗口。每 50 个周期更新一次最大值和最小值寄存器然后Cal_Threhold_Precision根据窗口内的极值算出新的Axis_Threhold和Axis_PrecisionExchage_Extrem_Reg交换新旧极值以便下个周期继续统计。这套自适应机制解决的是不同人步幅、步频差异大的问题——固定阈值要么对慢走不敏感要么对快跑误判。Filter_Axis_Value是另一层滤波它把当前读数和上一次读数做差三个轴差值都小于 10 就认为是噪声返回 0否则返回 1 表示有大变化。这个函数更像是一个“运动/静止”判别器适合做屏幕唤醒或低功耗切换和计步状态机是并行的两条线。实际用的时候我一般把Filter_Axis_Value的返回值接到主循环里静止超过一定时间就降采样率省电。注意Filter_AD_Data_1里int16 abs[2][3]这个变量名和标准库abs()函数重名虽然局部变量会遮蔽函数名、编译能过但可读性差二次开发时建议改成delta之类。另外Step_P_Flag[i] ^ 0xff这种翻转写法在uint8上等价于0 - 255但不如直接赋 0/1 直观接手的人容易看懵。4. 避坑与排查上板后最容易翻车的五个点4.1 读 ID 返回 0x00 或 0xFF现象AD_Initial一直返回 1串口打印器件 ID 不是0xE5。原因通常是三类接线错SDA/SCL 反了或没接上拉电阻、引脚宏和实际飞线不一致、SDA_IN/SDA_OUT的 CRH 位操作写错了端口。解决顺序是先量电压——空闲时 SDA 和 SCL 都应该是高电平如果 SDA 一直是低说明总线被拉死或宏定义反了再查上拉ADXL345 模块一般自带 4.7k 上拉但如果自己画板忘了加模拟 IIC 的推挽输出在切换输入后会浮空。最后用示波器抓起始信号确认 SCL 高期间 SDA 有下降沿。4.2 三轴数据恒定不变或全是 0现象ID 读到了初始化也过了但Read_Axis读出来三个值一直不变。原因多半是POWER_CTL没真正进入测量模式或者DATA_FORMAT的位写错导致数据寄存器不更新。ADXL345 上电默认是待机模式必须写POWER_CTL的 Measure 位。另一个常见原因是读的寄存器地址偏了——DATAX0宏如果定义成0x31而不是0x32读出来的就是别的东西。排查方法单独读POWER_CTL回读确认写入生效再手动读DATAX0和DATAX1看是否有变化。4.3 计步数疯涨或永远为 0现象静止放着步数也在涨或者怎么走都不计步。疯涨一般是Axis_Precision太小或Sample_Timer门限太低噪声被当成波峰永远为 0 则可能是Axis_Threhold设得比实际加速度幅值还大信号穿不过门限。解决先把Read_Axis的原始值打印出来人走几步看峰峰值大概多少再反推阈值。±4g 量程下正常走路单轴峰峰值大概在 2000~6000 LSB 之间阈值设在这个区间的一半左右比较稳。另外Select_Axis选错轴也会导致计步失效比如手环戴在手腕上主要波动轴可能不是 Z。4.4 模拟 IIC 在高速 MCU 上时序错乱现象在 72MHz 的 STM32 上跑得好好的换到 168MHz 的板子就读写失败。原因是delay_us的实现依赖循环计数主频变了延时就不准SCL 周期被压缩到 ADXL345 跟不上的程度。解决把delay_us改成基于 SysTick 或定时器的精确延时或者在 IIC 函数里加__NOP()撑时间。血泪经验是别用for空循环当延时编译器优化等级一改循环直接被优化掉时序全乱。4.5 多字节读最后一个字节没回 NACK现象单字节读写正常连续读 6 个字节时偶尔读到错位数据或总线卡死。原因在AD_Read_Multi_Byte的循环里最后一个字节必须调IIC_Read_Byte(0)发 NACK如果误写成IIC_Read_Byte(1)从机会以为主机还要数据继续占用 SDA后续IIC_Stop产生不了正确的停止条件。排查时重点看循环里i ! (len-1)这个判断有没有写反。这个坑我在两个项目里各踩过一次后来养成了读多字节必先单步看最后一个字节 ACK 状态的习惯。5. 二次开发把这份驱动改成能落地的运动检测模块拿到这份代码直接烧进去能出数据但要真做成产品至少得动三处。第一处是量程和速率。DATA_FORMAT的0x69是 ±4g做跌倒检测时瞬时冲击可能超过 4g 导致削顶我一般改成0x6D±16g代价是分辨率降到 3.9mg/LSB 变 15.6mg/LSB计步精度略降但跌落不漏。BW_RATE的0x09是 50Hz计步够用做手势识别得提到 100Hz 以上对应写0x0A或0x0B。第二处是中断。代码里把INT_ENABLE和INT_MAP都写成0x80关掉了中断实际产品里应该打开DATA_READY中断让 MCU 在数据就绪时才醒来读而不是主循环死等。改法是把INT_ENABLE写0x80DATA_READY 使能INT_MAP写0x00映射到 INT1 引脚然后在 EXTI 中断里调Read_Axis。这样平均功耗能降一个数量级对手环类项目是刚需。第三处是计步状态机的可调参数外置。现在Axis_Precision、Axis_Threhold、Sample_Timer的门限都散在代码里二次开发时建议抽成一个结构体通过串口或 Flash 配置方便不同佩戴位置快速标定。typedef struct { int16 precision[3]; /* 三轴幅值门限 */ int16 threshold[3]; /* 三轴过零门限 */ uint8 time_window; /* 最小计步间隔单位采样周期 */ uint8 axis_sel; /* 0自动选轴 1X 2Y 3Z */ } StepConfig; /* 验证方法串口打印原始三轴 当前步数走 100 步对比误差 */验证改造是否有效我习惯用“已知步数对照法”让测试者走 100 步串口同时输出原始加速度和累计步数事后把原始数据导出来在 PC 上重跑一遍算法看误差是出在采样还是判定。如果原始数据里波峰清晰但步数偏少就是门限太高如果波峰本身糊成一团那是采样率或滤波的问题调算法没用得先解决数据质量。从那以后我每次移植这类传感器驱动都强制先跑一遍“读 ID 打印原始三轴 静止看噪声”这三步确认地基没问题再碰算法省下来的调试时间比什么都值。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。