简介:这份资源面向嵌入式开发初学者与需要快速接入三轴加速度传感器的工程师,提供ADXL345驱动代码的完整实现与知识点梳理,可用于运动检测、跌倒检测与计步等场景的二次开发。压缩包内共1个doc文件,约63KB,以文档形式集中呈现驱动源码与函数说明,便于对照阅读和移植。资源已有474人学习下载,具备一定的参考基础。内容围绕软件IIC时序展开,逐一讲解IIC_Init引脚初始化、Start与Stop起止信号、Wait_Ack应答等待、Ack与NAck应答控制,以及Send_Byte和Read_Byte的逐位收发实现,并给出向ADXL345寄存器写入单字节与多字节的封装函数;同时涉及基于加速度信号的计步算法思路,且代码经过实际应用测试,可稳定运行于多种环境。读者可据此掌握IIC底层时序编写方法,理解传感器寄存器读写流程,并获取可直接复用的驱动框架与计步实现参考。
1. 软件模拟 IIC 驱动 ADXL345:一份能直接跑计步的三轴加速度代码包
拿到一份只有单个adxl.c的驱动代码,很多人第一反应是“就这?”。但真正把它烧进板子、接上 ADXL345、跑出稳定的三轴数据和计步结果,中间隔着的坑比想象中多。这份代码包的核心价值不在于“驱动”两个字,而在于它把软件模拟 IIC 的时序、ADXL345 的寄存器配置、以及一套基于峰峰值判断的计步算法全部揉进了一个文件里,拿来就能编译,改改引脚就能跑。它适合正在做可穿戴设备、运动检测 Demo、或者单纯想搞明白“计步到底怎么算”的嵌入式开发者。三轴加速度计的数据读取本身不复杂,难的是从噪声里把“一步”抠出来,这份代码给了一个可用的起点。
2. 软件模拟 IIC 的时序底子:为什么不用硬件外设
2.1 软件模拟 IIC 的选型理由与引脚约束
ADXL345 支持 IIC 和 SPI 两种接口,这份代码选了 IIC,而且是软件模拟的方式。原因很实际:很多低成本 MCU 的硬件 IIC 外设存在各种玄学问题,比如某些型号的硬件 IIC 在从机拉低时钟时会产生总线死锁,或者多主机场景下仲裁逻辑有 bug。软件模拟 IIC 把时序控制权完全交给代码,引脚随便挑两个普通 GPIO 就能用,移植成本极低。
代码里IIC_Init把 PB10 和 PB11 配置为推挽输出,初始输出高电平。这里有个细节值得注意:推挽输出模式下,引脚既能输出高也能输出低,但 ADXL345 的 SDA 线在应答阶段需要从机把线拉低,所以代码里用SDA_IN()和SDA_OUT()两个宏在输入和输出模式之间切换。如果忘记切回输出模式就发数据,SDA 线会处于高阻态,波形直接翻车。
void IIC_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11); // 空闲态拉高 }这段初始化只做了两件事:开 GPIOB 时钟、把两个引脚设成推挽输出并拉高。参数上唯一需要根据实际硬件改的是GPIO_Pin和对应的端口,如果你把传感器接在 PB6/PB7 上,改这两行就行。GPIO_Speed_50MHz对 IIC 来说绰绰有余,IIC 标准模式才 100kHz,快速模式 400kHz,50MHz 的翻转速度足够覆盖。
2.2 起始、停止与应答:三个最容易写错的时序点
IIC 协议里起始信号的定义是:SCL 为高电平时,SDA 从高变低。停止信号反过来:SCL 为高时,SDA 从低变高。代码里IIC_Start先拉高 SDA 和 SCL,延时 4 微秒,再拉低 SDA,再延时 4 微秒,最后拉低 SCL 钳住总线。这个顺序不能乱,如果先拉低 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; // 钳住总线 }应答等待IIC_Wait_Ack是另一个高频翻车点。它先把 SDA 切成输入模式,释放总线让从机控制,然后拉高 SCL,在 SCL 高电平期间读 SDA。如果读到低电平说明从机应答了,读到高电平说明没应答。代码里加了一个 250 次的超时计数,超时后主动发停止信号并返回 1。这个超时保护很关键,没有它的话,传感器没接好或者地址写错,程序会死循环卡在while(READ_SDA)里,表现为“程序跑飞了但看不出原因”。
提示:
delay_us的具体实现依赖你的系统时钟。如果延时函数不准,比如实际延时远大于标称值,IIC 速率会掉到几 kHz,虽然大概率还能通信,但读取三轴数据的刷新率会明显下降。用示波器看一眼 SCL 波形是最稳妥的验证方式。
2.3 单字节与多字节读写:寄存器地址的两种访问模式
ADXL345 的寄存器访问分单字节和多字节两种。单字节读写的流程是:起始 → 发写命令(WRITE_CMD)→ 等应答 → 发寄存器地址 → 等应答 → 起始 → 发读命令(READ_CMD)→ 等应答 → 读数据 → 发 NACK → 停止。多字节读则是在读完一个字节后发 ACK 而不是 NACK,让从机继续输出下一个寄存器的值,直到最后一个字节才发 NACK。
void AD_Read_Multi_Byte(uint8 reg, uint8 *buf, uint8 len) { uint8 i; IIC_Start(); IIC_Send_Byte(WRITE_CMD); IIC_Wait_Ack(); IIC_Send_Byte(reg); // 起始寄存器地址 IIC_Wait_Ack(); IIC_Start(); // 重复起始 IIC_Send_Byte(READ_CMD); IIC_Wait_Ack(); for(i = 0; i < len; i++) { if(i != (len - 1)) *(buf + i) = IIC_Read_Byte(1); // 非最后一字节,回 ACK else *(buf + i) = IIC_Read_Byte(0); // 最后一字节,回 NACK } IIC_Stop(); }Read_Axis函数就是靠这个多字节读一次性把DATAX0到DATAZ1六个寄存器读出来,然后拼成三个 16 位有符号数。注意拼接顺序是低字节在前、高字节在后,((u16)buf[1] << 8) + buf[0]得到 X 轴原始值。如果高低字节搞反,读出来的数据会大得离谱或者符号完全错误,这是新手最容易犯的错之一。
3. ADXL345 寄存器配置:量程、速率与数据格式的取舍
3.1 初始化流程与器件 ID 校验
AD_Initial函数的第一件事不是写配置,而是读0x00寄存器判断返回值是不是0xE5。这是 ADXL345 的器件 ID,固定值。如果读不到0xE5,说明 IIC 通信根本没通,后面写再多配置都是白费。这个校验步骤看起来简单,但省掉了大量“配置写了怎么没反应”的排查时间。
uint8 AD_Initial(void) { IIC_Init(); if(AD_Read_One_Byte(0) == 0xE5) { AD_Wtite_One_Byte(DATA_FORMAT, 0x69); // ±4g,全分辨率 11 位 AD_Wtite_One_Byte(BW_RATE, 0x09); // 输出速率 50Hz AD_Wtite_One_Byte(POWER_CTL, 0x28); // 测量模式 AD_Wtite_One_Byte(INT_ENABLE, 0x80); // 关中断 AD_Wtite_One_Byte(INT_MAP, 0x80); AD_Wtite_One_Byte(OFSX, 0x00); // X 偏移 AD_Wtite_One_Byte(OFSY, 0x00); // Y 偏移 AD_Wtite_One_Byte(OFSZ, 0x00); // Z 偏移 return 0; } return 1; }DATA_FORMAT写0x69拆开看:bit6 和 bit5 是0b11对应 ±4g 量程,bit3 是1开启全分辨率模式,bit2 是0表示数据右对齐。全分辨率模式下,输出分辨率固定为 11 位,量程越大每 LSB 对应的重力加速度越大。±4g 量程下,每 LSB 约等于 3.9mg,对计步场景够用。如果你做的是跌落检测,可能需要 ±16g;如果做的是极慢速的姿态变化检测,±2g 能换来更细的分辨率。
BW_RATE写0x09对应输出数据速率 50Hz。这个值决定了你多久能读到一次新数据,计步算法里Sample_Timer的计数节奏和它直接相关。50Hz 意味着每 20ms 一个采样点,对于步行这种低频运动(通常 1~3Hz)来说采样率足够,又不会让数据量过大。
POWER_CTL写0x28是让芯片进入测量模式。bit3 置 1 开启测量,bit5 置 1 开启自动休眠,bit2 置 0 关闭休眠。如果这个寄存器没写对,芯片会停留在待机模式,读出来的三轴数据全是零,现象就是“IIC 通了但数据不动”。
3.2 偏移寄存器与数据对齐的实操细节
OFSX、OFSY、OFSZ三个偏移寄存器代码里全写0x00,意思是先不做硬件偏移补偿。实际使用中,传感器焊接在板子上难免有安装倾斜或者零点漂移,静止时读出来的值可能不是理想的(0, 0, 256)附近(Z 轴受重力影响约 256 LSB)。如果发现静止状态下 X 或 Y 轴有几十个 LSB 的固定偏差,可以通过写这三个寄存器做粗调。偏移量的单位是每 LSB 约 15.6mg,写之前最好先采集几百个静止样本取平均,再换算成寄存器值。
注意:偏移寄存器是 8 位有符号数,范围 -128 到 127。如果偏差超过这个范围,说明要么传感器坏了,要么量程选得不对,靠偏移寄存器救不回来。
数据对齐方面,代码用的是右对齐模式,12 位数据放在 16 位寄存器的低 12 位,高 4 位是符号扩展。Read_Axis里直接强转int16就能拿到正确的有符号值。如果改成左对齐,拼接方式要跟着改,否则数值会差 16 倍。
4. 计步算法拆解:峰峰值判断与动态阈值
4.1 三轴选择与动态阈值更新机制
计步算法的核心逻辑在Filter_AD_Data和Filter_AD_Data_1两个函数里。第一步是Select_Axis选出一个当前变化最剧烈的轴作为计步参考轴,因为手机或手环的佩戴方向不固定,不能写死用 Z 轴。选轴之后,算法维护一个 50 次采样的窗口,在窗口内不断更新每个轴的最大值和最小值,窗口结束时调用Cal_Threhold_Precision计算动态阈值和精度门限,然后交换极值寄存器准备下一个窗口。
if(Decide_Sample_No < 49) { Judge_Extreme_Ad(); // 更新窗口内最值 Decide_Sample_No++; } else { Cal_Threhold_Precision(Axis_Threhold, Axis_Precision); Exchage_Extrem_Reg(); // 交换最值,准备下个周期 Decide_Sample_No = 0; }这个动态阈值的思路很实用:人的步频和步幅会变,固定阈值在慢走时漏计、快跑时多计。用最近 50 个采样点的峰峰值来推算当前的运动强度,阈值跟着数据走,适应性好很多。50 次采样在 50Hz 下是 1 秒,相当于每秒更新一次判断基准。
4.2 波峰检测状态机与时间窗约束
每个轴独立维护一个Step_P_Flag状态机,三个状态分别对应“找最小值”“找上升沿最大值”“找下降沿最小值”。当检测到一个完整的波峰-波谷组合后,计算前半段和后半段的幅值差,取变化更大的那一段作为Step_P2P,再和精度门限比较。只有幅值超过门限、且最小值到最大值之间跨过了阈值线,才认为可能是一步。
if(Step_P2P[i] > Axis_Precision[i]) { if((Step_Min[i] < Axis_Threhold[i]) && (Axis_Threhold[i] < Step_Max[i])) { if(Sample_Timer >= 10) // 时间窗约束 { Judge_Show(); } } }Sample_Timer >= 10这个条件是用来防抖的。没有它的话,一次剧烈晃动可能被拆成好几个“步”。10 个采样点在 50Hz 下是 200ms,对应最快步频 5 步/秒,正常人跑步也很难超过这个频率。Z 轴的时间窗更严格,要求Sample_Timer >= 13,因为 Z 轴受重力影响,噪声模式和其他两轴不同。
Sum_Time和Sum_Step配合实现“有效计步间隔”:如果 150 个采样周期(3 秒)内没有新的步被确认,计步累加器清零,重新进入搜索模式。这个机制防止了长时间静止后突然一个动作被误判为连续步伐。
4.3 噪声过滤与大幅变化标记
Filter_Axis_Value函数做了一层简单的差分滤波:把当前读数和上一次读数逐轴求绝对差,如果任一轴的差值超过 10 个 LSB,标记flag = 1表示“大幅变化”,否则标记为噪声。这个 flag 可以给上层逻辑用来判断当前数据是否可信,比如在flag = 1时跳过计步判断,避免把碰撞或甩动当成步伐。
uint8 Filter_Axis_Value(int16 *data, int16 *reg) { uint8 loop_i; uint8 flag = 0; int16 sub[3] = {0}; Read_Axis(data); for(loop_i = 0; loop_i < 3; loop_i++) { sub[loop_i] = Cal_Abs_Value(*(data + loop_i), *(reg + loop_i)); } if((sub[0] > 10) || (sub[1] > 10) || (sub[2] > 10)) flag = 1; // 大幅变化 else flag = 0; // 噪声级别 for(loop_i = 0; loop_i < 3; loop_i++) { *(reg + loop_i) = *(data + loop_i); } return flag; }阈值 10 是个经验值,对应约 39mg 的加速度变化。如果你发现静止时 flag 频繁置 1,说明传感器噪声偏大,可以适当调高;如果轻微走动都触发不了,说明阈值太高,调到 5 左右试试。这个参数没有标准答案,跟具体传感器的焊接质量、电源纹波都有关系。
5. 避坑排查:五个让计步结果对不上的典型问题
5.1 读不到器件 ID 0xE5
现象:AD_Initial返回 1,串口打印初始化失败,后续所有数据为零。原因通常是 IIC 引脚接反、上拉电阻缺失、或者从机地址不对。ADXL345 的 7 位地址是0x53,左移一位后写命令是0xA6,读命令是0xA7。代码里WRITE_CMD和READ_CMD如果定义错了,起始信号发出去从机根本不应答。解决:先用万用表确认 SDA 和 SCL 空闲时都是高电平,再用示波器抓起始信号后的第一个字节,看是不是0xA6。
5.2 三轴数据全是零或恒定不变
现象:ID 读到了0xE5,但Read_Axis读出来的值始终是零或者某个固定值。原因大概率是POWER_CTL没写对,芯片还在待机模式。0x28这个值里 bit3 是测量使能位,如果写成0x20就只开了自动休眠没开测量。另一个可能是DATA_FORMAT的 bit3 没置 1,全分辨率没开,数据寄存器不更新。解决:用AD_Read_One_Byte把POWER_CTL和DATA_FORMAT读回来打印,确认写入生效。
5.3 计步数只增不减或者疯狂累加
现象:静止放在桌上,步数自己往上涨;或者走一步计了五六步。原因分两种:前者是动态阈值算得太低,噪声被当成了步伐,检查Cal_Threhold_Precision里阈值下限有没有设保护;后者是时间窗Sample_Timer判断失效,可能是Sample_Timer的递增位置在中断里被重复调用,导致计数节奏乱了。解决:把Step_P2P、Axis_Threhold、Sample_Timer三个值通过串口打出来,走几步看波形,正常一步应该对应一个明显的峰谷和 10 以上的Sample_Timer。
5.4 换一块板子计步就不准了
现象:同一份代码,A 板子计步正常,B 板子偏差很大。原因是 ADXL345 的安装方向和焊接应力不同,导致静止零点偏移和轴间灵敏度差异。代码里偏移寄存器全写零,没有做个体校准。解决:在初始化后加一段静止采样,每个轴采集 200 个点取平均,把平均值换算成偏移量写入OFSX/OFSY/OFSZ。注意 Z 轴要减去 256(1g 对应的 LSB 数),因为静止时 Z 轴承受重力。
5.5 多字节读数据高低字节错位
现象:X 轴读数在静止时约等于 256,但 Y 和 Z 轴数值异常大或者符号跳变。原因是AD_Read_Multi_Byte读出来的 buf 数组里,高低字节的拼接顺序搞反了。ADXL345 的数据寄存器是低字节在前(DATAX0在低地址),高字节在后。如果写成(buf[0] << 8) + buf[1],数值会完全错乱。解决:对照数据手册确认寄存器地址顺序,DATAX0=0x32、DATAX1=0x33,拼接时buf[1]是高字节。
6. 从能跑到好用:计步参数的整定方法与验证习惯
代码能编译、能读出三轴数据,只是第一步。真正让计步结果可信,靠的是对着真实数据调参数。我一般会先写一个临时测试函数,把Read_Axis读到的原始值、Step_P2P、Axis_Threhold、Axis_Precision四个量以 CSV 格式从串口吐出来,然后用串口助手存成文件,丢进表格里画折线图。走 20 步、跑 20 步、上下楼各一趟,分别看峰峰值分布落在什么区间。
整定的顺序有讲究:先调Axis_Precision的下限,保证静止和轻微晃动不触发;再调Axis_Threhold的系数,让正常步行能跨过阈值线;最后调时间窗Sample_Timer的门限,把一步拆成多步的情况压下去。这三个参数互相耦合,改一个要回头验证另外两个,别想一次调完。
| 参数 | 代码位置 | 典型调整方向 | 验证方法 |
|---|---|---|---|
Axis_Precision下限 | Cal_Threhold_Precision | 静止误计多则调高 | 静置 1 分钟看步数是否为零 |
Axis_Threhold系数 | Cal_Threhold_Precision | 漏计多则调低 | 慢走 20 步对比实际步数 |
Sample_Timer门限 | Filter_AD_Data内 | 一步多计则调高 | 快跑 20 步看是否翻倍 |
| 差分阈值 10 | Filter_Axis_Value | 噪声大则调高 | 静止时 flag 是否频繁置 1 |
还有一个容易被忽略的点:Filter_AD_Data_1和Filter_AD_Data是两套不同的峰检测实现,前者带更细的上升沿/下降沿分段判断,后者是简化的峰峰值比较。代码里两个函数都存在但只调用了其中一个,如果你发现当前使用的版本对慢走不敏感,可以换另一个试试,两者的参数体系是共通的,但状态机行为有差异,切换后要重新整定时间窗。
从那以后我每次拿到这类传感器驱动代码,都强制先做三件事:读 ID 确认通信、打印原始三轴值确认数据在动、用 CSV 记录一段真实运动数据再调算法参数。跳过任何一步,后面调计步都是在猜。希望这份拆解能帮你在接 ADXL345 的时候少走几个弯路。
本文还有配套的精品资源,点击获取