简介:本资源是一套面向嵌入式开发者与STM32初学者的ADXL355三轴加速度传感器驱动例程,聚焦低功耗物联网、可穿戴设备及运动检测等典型应用场景。资源完整实现STM32通过SPI接口与ADXL355通信的核心功能,涵盖传感器初始化、测量配置、加速度数据读取与校验解析全流程,解决外设驱动开发中时序匹配、寄存器操作与协议健壮性等关键问题。压缩包共5个文件(3个C源文件+2个头文件),总大小仅5KB,其中SPI.c负责STM32 SPI主模式底层驱动,ADXL355.c封装传感器寄存器配置与数据解码逻辑,Communication.c提供带错误检查的数据包解析机制,结构清晰、模块职责分明,便于理解移植与二次开发。目前已有1764人学习下载,读者可直接复用该框架快速接入ADXL355,亦可基于其SPI交互范式拓展至其他SPI传感器开发。 这些年我帮人调试过不少传感器,ADXL355是我觉得“例程写得再好,也得自己重新读一遍芯片手册”的典型。网上流传的《ADXL355例程源码》版本很多,有STM32的、有FPGA的、有Arduino的,甚至还有Python上位机脚本,但真正能拿过来直接编译通过的几乎没有。问题往往不是源码本身写错,而是你没理解它背后隐藏的硬件假设。比如SPI还是I2C、片选接没接、电源噪声大不大、ODR配置和读取节奏是否匹配——这些因素都会让同一份例程在一套板子上跑得飞起,在另一套板子上却连设备ID都读不到。
如果你正在找ADXL355的例程和源码,我建议你先别急着复制粘贴。这篇文章我会从一份典型例程能拆出哪些部分开始,把硬件连接、寄存器初始化、数据拼接、移植适配、故障排查整个链路都过一遍。内容不挑具体平台,但你拿到任何一份ADXL355源码,都能按这个思路快速判断它能不能用,以及怎么改成你自己的工程。
1. 一份ADXL355例程源码,真正该读的是这四块
很多人拿到例程的习惯是先打开main.c,找到初始化函数,然后复制粘贴。但ADXL355的例程源码通常不是给你“抄”的,而是给你“拆”的。我一般会先把整个工程按功能切成四块:驱动层、平台抽象层、应用逻辑、上位机工具。搞明白这四块的边界,比读懂每一行代码更重要。
1.1 传感器驱动层与平台抽象层的边界
驱动层是直接和ADXL355寄存器打交道的代码,比如读DEVID_AD、写POWER_CTL、读XDATA3这些操作。这部分理论上不依赖具体MCU,只要提供“读字节”“写字节”“读多字节”这几个基础函数就够了。
平台抽象层则是把那几个基础函数实现到具体的硬件上。用STM32就是HAL库的SPI读写,用ESP32就是esp-idf的spi_master接口,用Arduino就是Wire或SPI库。这个区分非常关键,因为大多数例程不能直接用,原因就是平台抽象层写死了。
我见过不少“ADXL355例程源码”把SPI的引脚定义、读写时序、甚至延时函数全部揉在一个文件里。如果你换一块板子,就要改几十处。正确做法是把adxl355的驱动文件里通过函数指针或者宏定义,把底层的SPI/I2C操作甩给平台层实现。这样驱动层完全不用动,你只需要重写平台层那四五个函数。
1.2 配置、读取、中断、FIFO四条主线的组织方式
一份结构良好的ADXL355例程,你会看到四条清晰的主线。
第一条是初始化配置:从软复位、等待稳定、读取ID、设置量程、设置ODR和滤波器、到最后切到测量模式。这条线通常在main函数开头或者sensor_init里。
第二条是基础读取:查询状态寄存器里的DRDY标志,或者直接用延时估算数据就绪时间,然后读三轴数据寄存器,拼接成20位无符号数再转成有符号数,最后换算成g值。
第三条是中断处理:把DRDY或FIFO水印中断映射到INT1或INT2引脚,MCU在中断回调里读取数据。这种写法适合高数据率场景,能保证每个样本都不丢。
第四条是FIFO批量读取:配置FIFO水印和FIFO采样数,当FIFO存够一定量的数据后触发中断,MCU一次性把一堆数据全读出来。这种写法适合低功耗场景,MCU大部分时间可以睡着,攒一批数据再处理一次。
你拿到一份源码后,先找到这四条主线在哪里实现,再判断它覆盖了哪几条。很多所谓的例程其实只覆盖了第二条,比如轮询读三轴数据,这就够跑通基本功能,但如果你想做低功耗或者连续高采样率的项目,就得自己补中断和FIFO的部分。
1.3 例程不能直接用的原因:平台差异
除开平台层接口不同,例程不能直接用还有一个隐蔽原因:时序假设。ADXL355上电后需要一段时间才能完成内部校准,软复位后也需要等待。有的例程里延时很短,在某个特定板子上碰巧能用,换一块上电稍慢的板子就初始化失败。
另外,不同MCU的SPI时钟极性、相位支持不一样。ADXL355支持SPI Mode 0和Mode 3,但如果你用的例程写死了一种模式,而你的MCU在另一种模式下反而更稳定,就要主动改。我第一次用ESP32移植STM32例程时就遇到过,STM32那边用Mode 0没问题,ESP32的SPI从机时序在高速下有点怪,改成Mode 3后一切正常。
所以记住这句话:例程是参考实现,不是标准答案。你真正要读的是驱动层逻辑,要重写的是平台层接口,要理解的是寄存器配置背后的意图。
2. 上电之前:硬件连接里的三个细节
软件调试遇到诡异问题,我第一反应是回头量硬件。ADXL355这个芯片尤其如此,它对电源、参考电压、通信引脚的电平都很敏感。很多例程跑不通,不是代码问题,是硬件就没给代码发挥的机会。
2.1 电源去耦与参考电压引脚
ADXL355的工作电压是2.25V到3.6V,但这不是说你可以随便从一个DC-DC输出直接怼上去。它内部是MEMS传感器加ADC加数字逻辑,电源纹波会直接影响输出噪声。我见过有人用面包板飞线,结果静止状态下三轴输出跳动达到几十毫g,根本不是芯片真实水平。
正确做法是至少放一颗0.1uF和一颗1uF或10uF的电容靠近VDD引脚。如果板子上有模拟电源和数字电源分离的条件,建议给模拟电源单独加一个LDO或者LC滤波。另外,VDDIO引脚是IO电平参考,它决定SPI/I2C接口的电平范围,要保证和MCU的IO电平一致。如果你的MCU是3.3V,VDDIO就接3.3V;如果是5V MCU但传感器供电是3.3V,VDDIO不能直接接5V,否则会损坏芯片。
还有一个经常被忽略的引脚是VSSP,这是内部MEMS的参考地,一般直接接地就行。但layout上要让VSSP和VSS的连接路径尽量短、尽量粗,不要绕到很远的地方再过孔。
2.2 SPI/I2C模式怎么选,CS引脚的隐藏作用
ADXL355的CS引脚不只是片选,它还决定了通信模式。CS接高电平时,芯片走I2C;CS接低电平时,芯片走SPI。这个设计很多人不知道,导致例程里明明配置的SPI,但物理上CS一直悬空或者被拉高,芯片实际在I2C模式,自然读不到数据。
如果你的工程用SPI,CS必须由MCU的GPIO主动控制,初始化时保持高电平,通信时拉低。如果你的工程用I2C,CS就必须直接接VDDIO,不能靠GPIO,因为一旦GPIO输出低电平把CS拉低,芯片会瞬间切到SPI模式。
另外我建议CS不要用硬件SPI自动片选,尤其是有多个SPI从机的场景。ADXL355的工作方式比较传统,手动控制CS更稳,时序也更可控。
2.3 地址引脚与电平匹配
ADXL355在I2C模式下的7位设备地址是0x1D,对应的8位写地址是0x3A、读地址是0x3B。这个地址是固定的,没有地址引脚可以改,所以I2C总线上只能挂一颗ADXL355,想挂多颗只能换SPI模式,用不同的CS引脚区分。
电平匹配方面,如果MCU是5V,而ADXL355的VDDIO是3.3V,那么SPI的SCLK、MOSI这些输入引脚不能直接接5V高电平。需要加电平转换或者用开漏加外部上拉到3.3V的方式。I2C模式因为本身是开漏结构,靠上拉电阻决定电平,所以接到5V MCU时,上拉电阻接3.3V即可,通信引脚两侧都能接受。
我调试时习惯先在原理图阶段就把VDDIO的接法标清楚,避免软件工程师和硬件工程师在那里互相甩锅。
3. 初始化源码拆解:从器件ID校验到测量模式
当硬件连接确认无误后,再来看初始化代码,你会发现每一步都有明确目的。ADXL355的初始化流程其实不算复杂,但顺序很重要。我先把我在例程里最常用的一套初始化流程列出来,然后一步步解释为什么这么写。
// 1. 软复位 adxl355_write_reg(0x2F, 0x52); delay_ms(20); // 2. 读取设备ID uint8_t id = adxl355_read_reg(0x00); if (id != 0xAD) { // 链路错误,需要排查 return ADXL355_ERR_ID; } // 3. 设置量程,例如 ±4g adxl355_write_reg(0x2C, 0x01); // 4. 设置ODR和滤波器,具体值请对照手册FILTER_SETUP寄存器编码 adxl355_write_reg(0x28, 0x02); // 5. 进入测量模式 adxl355_write_reg(0x2D, 0x01);3.1 第一步先读DEVID_AD,确认SPI/I2C链路正常
读设备ID这一句看起来简单,但它是整个驱动里最重要的自检步骤。寄存器0x00是DEVID_AD,读出来应该是0xAD。这个值是由芯片内部固化的,读不到或者读出来不对,说明你的通信链路有问题,后面做再多配置都没意义。
很多例程把读ID放在软复位之后,这是一个好习惯。因为芯片在上电后可能处于不确定状态,先软复位再读ID,能保证你读出的是稳定状态下的ID。如果复位之前读,偶尔会上电时序和SPI时序打架,返回全0或者全FF,容易误导你以为是SPI引脚接错。
我调试时会把这一步单独做成一个测试函数,上电后只做这一件事。如果连续读十次DEVID_AD都稳定返回0xAD,再继续往下走。这个习惯帮我过滤掉了大量低级接线错误。
3.2 软复位、量程、ODR和滤波器的配置顺序
软复位寄存器是0x2F,写入0x52表示复位。写完以后不能马上配置其他寄存器,需要等一段时间,通常是20毫秒甚至更长。我习惯等50毫秒,稳妥一点。
量程寄存器是0x2C,低两位决定量程:00对应±2g,01对应±4g,10对应±8g。这个选择直接影响后面的数据换算系数,所以要在初始化的时候就固定下来,不要在运行中反复切换,因为切换量程后芯片需要一点时间重新稳定。
ODR和滤波器配置在0x28这个FILTER_SETUP寄存器里。ADXL355的ODR可以设置成从4000Hz往下到几赫兹,具体编码要对照你手里那版数据手册来查,不同版本手册的表格编号可能不一样。配置ODR时要考虑你的MCU读取速度能不能跟上,如果MCU主频很低或者总线上还挂了别的传感器,建议先选一个较低的ODR,比如250Hz或125Hz,跑通后再逐步提高。
滤波器这里有个关键点:ADXL355内部有低通滤波器,而且低通截止频率通常是跟随ODR变化的,你选ODR的同时等于也选了低通的带宽范围。不要期望能配一个4000Hz的ODR然后把低通截止压到1Hz,这两者是关联的。
3.3 中断与FIFO的配置逻辑
如果你的例程用到了中断和FIFO,初始化代码里还会多出几行。FIFO控制寄存器(0x2A)用来配置FIFO的工作模式,FIFO_SAMPLES寄存器(0x2B)用来设置水印。比如你希望FIFO里攒到32个样本再中断一次,就往FIFO_SAMPLES写32。
中断相关的配置要仔细读例程里的注释。ADXL355支持把多种中断事件映射到INT1或INT2引脚,映射关系通过中断映射寄存器配置。有的例程把DRDY中断映射到INT1,有的把FIFO水印中断映射到INT1,这没有绝对的对错,但你要确保MCU的外部中断引脚和配置保持一致。
我见过最典型的错误是:例程里把FIFO水印中断映射到了INT1,但硬件上INT1没接到MCU,MCU却监听的是INT2,于是数据永远不触发中断。拿到例程先查中断映射寄存器,再看原理图,两边对不上就改一边。
4. 数据读取的完整链路:拼接、换算、验证
初始化只是热身,真正有含金量的是数据读取。ADXL355的三轴输出是20位有效数据,以24位形式存放在三个寄存器里。如果你只是把高16位读出来,精度虽然也够用,但浪费了这颗芯片最大的优势——低噪声高分辨率。
4.1 三轴20位数据的拼接与符号扩展
每个轴的三个字节从高到低分别是DATA3、DATA2、DATA1,起始地址从0x08开始是X轴。比如X轴的高字节在0x08,中字节在0x09,低字节在0x0A。我建议一次连续读三个字节,不要分三次单字节读,因为在三次读取之间数据可能已经更新,导致三个字节来自不同时刻的采样,拼接出来的值会跳变。
拼接的C代码大概长这样:
int32_t adxl355_axis_to_signed(uint8_t h, uint8_t m, uint8_t l) { uint32_t raw = ((uint32_t)h << 16) | ((uint32_t)m << 8) | (uint32_t)l; // 20位有符号数,最高扩展符号位 if (raw & 0x00080000UL) { raw |= 0xFFF00000UL; } return (int32_t)raw; }注意上面的判断看的是第20位,也就是0x00080000这一位。如果这一位是1,说明是负数,要把高12位全部补1,否则返回的int32_t是一个正数,后面换算全乱。这个细节是例程源码里最容易抄错的地方,很多人拼完忘了符号扩展,静止时Z轴读数反而变成一大正数。
4.2 量程换算成g值,以及噪声水平的直观认识
拿到有符号的raw值以后,要换算成g。换算公式是:
g = raw / 2^20 * 满量程范围
其中2^20约等于1048576。如果量程是±2g,满量程范围是4g;如果是±4g,满量程范围是8g;±8g对应16g。举例来说,±2g量程下,1个LSB等于4/1048576,约等于3.8微g。所以ADXL355的20位分辨率不是噱头,它真的能把微g级别的变化反映出来。
那你可能问,噪声水平能到多少?ADXL355在典型配置下的噪声密度大约在25微g/√Hz附近,这意味着在1Hz带宽下,噪声大约在几十微g量级。实测静止放置时,三轴输出的峰峰值抖动通常在零点几毫g以内,具体和电源质量、layout、ODR都有关系。如果你静止时看到几十毫g的跳动,先别怪芯片,大概率是你电源或者接线的问题。
4.3 静止测试和翻转测试:如何确认例程真正跑通
初始化做完、数据拼接也对,怎么确认整个链路是通的?我习惯做两个测试。
第一个是静止测试。把板子平放在桌面上,Z轴应该接近1g,X轴和Y轴接近0g。如果你的板子是立着放的,那对应轴上的读数会不同,但总的矢量模长应该接近1g。计算sqrt(x² + y² + z²),在静止时应该大约等于1g,误差在几十毫g以内都算正常。
第二个是翻转测试。把板子转180度,原本接近1g的轴读数会变成接近-1g。这个测试能验证符号处理是否正确,也能暴露量程配置是否偏了。如果翻转后读数绝对值不是1g而是0.5g,那你的拼接或换算系数大概率有bug。
这两个测试虽然简单,但比盯着屏幕看寄存器值直观得多。我每次移植完ADXL355驱动,都先跑这两个测试,通过了才去做后面的滤波、姿态解算等高级功能。
5. 从STM32到ESP32的移植要点
ADXL355例程里最常见的是STM32版本,但很多人实际用的MCU是ESP32、GD32、瑞萨、或者国产的RISC-V芯片。移植的过程中,最花时间的不是寄存器配置,而是把平台抽象层重写干净。
5.1 平台抽象层该写成什么样
一个合格的平台抽象层,至少应该提供这几个函数:
- 初始化通信接口(SPI或I2C的初始化)
- 写单字节寄存器
- 读单字节寄存器
- 连续读多字节寄存器
- 毫秒级延时
- 片选控制(如果用SPI)
在C语言里,可以用函数指针结构体来抽象,也可以用宏定义。如果你只在一个平台上跑,直接定义成宏也行,但建议还是分文件放,比如adxl355_platform.c和adxl355_platform.h。驱动层的adxl355.c里不要出现任何HAL_、gpio_、Wire这种平台相关调用,把所有平台相关的东西都塞到platform文件里。
我见过最舒服的一种写法是,在adxl355.h里暴露一个结构体:
typedef struct { int (*read_reg)(uint8_t reg, uint8_t *buf, uint16_t len); int (*write_reg)(uint8_t reg, const uint8_t *buf, uint16_t len); void (*delay_ms)(uint32_t ms); } adxl355_bus_t;然后初始化函数接收这个结构体指针。换平台的时候只需要重新实现这三个函数,驱动层代码一行都不用改。这算是小而美的接口设计。
5.2 延迟、超时和临界区处理
移植时最容易出问题的是延时和中断。ADXL355的软复位等待、上电稳定等待都依赖延时函数。STM32例程里可能用的是HAL_Delay,ESP32里是vTaskDelay,Arduino里是delay。这些函数本身差别不大,但注意在裸机环境里要用阻塞延时,在RTOS环境里如果延时期间有更高优先级任务抢占,可能导致读取时序被拉长。对于ADXL355这种几十kHz级别的通信来说,偶尔被抢占几毫秒问题不大,但如果你在FIFO模式下,读取频率和数据率配合得比较紧,就要注意临界区保护。
我建议在读取一组数据的函数里,加上任务锁或者关中断保护,防止读三轴时被其他任务打断。虽然SPI本身有CS片选保护,但多字节读取过程中如果被抢占,CS的状态和缓冲区的数据可能混乱。ESP32的SPI驱动是线程安全的,但如果你自己用GPIO模拟SPI,就得手动加锁。
5.3 我实际移植中踩过的坑
有一次我从STM32F103的例程移植到ESP32-S3,其他部分都很顺利,偏偏数据率一高就偶尔出现某个轴跳变。排查了半天,最后发现是ESP32的SPI时钟默认跑得太快,STM32例程里SPI时钟是2.25MHz,而我直接用了ESP32默认的40MHz SPI时钟,ADXL355虽然标称SPI最高支持到10MHz,但在40MHz下显然不行。把SPI时钟降到5MHz后,问题彻底消失。
还有一个坑是片选引脚。STM32的硬件SPI会自动控制NSS,而ESP32的SPI主设备默认片选是软件控制的,如果你沿用STM32例程里那种“等SPI发送完再拉高CS”的写法,也要确认GPIO输出模式配置正确。有的GPIO默认是输入模式,直接当成片选用,拉低了也没反应。
6. 调试ADXL355时的高频故障排查
最后这部分,我整理一下这些年调试ADXL355遇到的高频问题。你可以直接把这节当临场速查表用。
6.1 一直读不到DEVID_AD:链路还是配置问题
读寄存器0x00拿不到0xAD,先不要怀疑芯片坏了,按顺序排查。第一步确认供电电压,VDD和VDDIO都要量。第二步确认CS引脚电平,它决定通信模式。第三步用示波器或者逻辑分析仪看SCLK、MOSI、MISO上有没有波形,发送读命令后MISO上有没有数据返回。
如果MISO一直低电平或者高电平,大概率是CS方向反了或者电平不匹配。如果波形有,但数据不对,就要检查SPI模式是不是匹配。ADXL355支持Mode 0和Mode 3,你可以在例程里切换CPOL和CPHA试一下,很多时序问题就是这两种模式选错。
6.2 数据全0或全FF:片选与复用引脚冲突
数据全0,通常是MISO被拉死了,或者芯片没有正常回应。数据全FF,通常是MISO悬空在高电平。这两个现象指向的排查方向略有不同。全FF要重点查片选引脚是不是真的拉低了,如果CS一直高,芯片处于I2C模式,SPI总线上什么都不会响应,MISO被上拉电阻拉到高电平,读出来就是0xFF。
还有一个容易踩的坑是引脚复用冲突。有些MCU的某个引脚既支持SPI又支持UART,如果初始化代码里没有把GPIO完全配置成SPI复用功能,就会出现一种很奇怪的现象:第一次读ID正常,第二次全FF。这是因为第一次通信时GPIO还保持默认状态,后面被其他外设重新配置了。解决办法是在初始化SPI之前,严格设置GPIO复用功能,并且不要让多个外设同时占用同一引脚。
6.3 输出漂移和噪声异常:从电源和ODR找原因
静止时数据漂移大,最常见的三个原因:电源纹波、ODR和低通带宽没配对、数据拼接有符号扩展问题。电源问题用示波器看VDD纹波,如果超过几十毫伏就要加滤波。ODR的问题比较隐蔽,比如你配置了4000Hz的ODR,但低通截止频率也跟着设得很高,高频噪声全进来了,静止数据自然抖得厉害。把ODR降下来或者把低通带宽压低,数据会明显变稳。
如果数据在某个方向上长期缓慢漂移,则是温度影响。ADXL355虽然有内部温度补偿,但在温度变化剧烈的环境里,零偏还是会缓慢变化。这种情况需要做温度标定,至少要在你的目标温度范围内测一遍零偏曲线。
6.4 上位机读取时DLL初始化失败的排查思路
很多ADXL355例程配套的上位机是Python脚本,通过USB转SPI或者USB转I2C适配器读数据。如果你在Windows上运行这类脚本时遇到类似“OSError: [WinError 1114] 动态链接库(DLL)初始化例程失败”的报错,不用慌,这是Python加载某个DLL时出了问题。常见的坑有三个。
第一个是DLL位数不匹配,64位Python不能加载32位的DLL,反之亦然。先确认你的Python解释器位数,再去找对应位数的驱动库。第二个是缺少C运行时依赖,DLL本身在,但它依赖的MSVC运行库没装。装一下对应的Visual C++ Redistributable通常能解决。第三个是路径问题,DLL路径不要放在带中文或者空格的目录里,Python用ctypes或pyd加载DLL时对路径比较挑剔。
我遇到过最离谱的一次,是杀毒软件把DLL隔离了,文件还在但实际内容被替换成空壳,加载时直接报1114。把DLL加白名单后一切正常。所以遇到1114,先看依赖、再看位数、三看路径、四看杀毒。
调试到这一步,ADXL355这颗芯片你也算是真正吃透了。当初我拿到第一份ADXL355例程时,也觉得“这不就是读三轴数据吗”,但越往后做越觉得,例程只是给你画了一条路,路边的坑都得自己踩一遍才知道在哪。现在再看到有人直接复制例程跑不通就来问,我一般就让他先开一个逻辑分析仪,把时序拍下来发给我,十有八九问题出在CS或者SPI模式上。如果你也卡在某个奇怪的现象里,不妨也先量波形,再改寄存器,别一上来就怀疑所谓“源码本身有bug”。
本文还有配套的精品资源,点击获取