ADXL345三线SPI非库驱动:寄存器配置、时序与排坑实战 简介面向STM32开发者与嵌入式初学者的3线非库SPI驱动ADXL345加速度计工程包解决在不调用特定库文件的前提下用最少引脚完成三轴加速度数据读取的问题适用于需要节省IO引脚、降低存储占用的STM32项目。资源共22个文件压缩包仅69KB解压后可直接获得MAIN.C、SPI.C源文件与xl345.h、COMMON.H头文件以及对应的Keil工程文件.Uv2/.opt/.plg和编译产物.hex、.elf、.map、.obj、.lst方便在MDK环境中打开即用或直接烧录验证。内含3线SPI的GPIO配置、寄存器读写命令、数据输出时序及16位补码转工程单位等关键实现并涉及0x31电源管理寄存器、0x32数据输出地址等具体操作同时以.s启动文件及.bak备份文件保留工程原始状态。这套精简实现省去库文件依赖适合在资源受限的STM32项目中移植也可作为理解SPI协议与传感器驱动的入门参考。目前已有173人学习下载代码结构紧凑且便于阅读适合对照参考或二次开发。 最近在调一块板子硬件SPI1被屏占掉了剩下的引脚刚好能凑出CS、时钟和一根数据线于是就用3线非库SPI方式把ADXL345接了起来——不挂HAL库、不调标准库的SPI驱动直接对着寄存器写时序。折腾完回头一看这个过程其实把ADXL345的三线SPI时序和常见坑都捋清楚了比直接调库函数收获大得多。这篇就记录我实际用的接线方式、驱动思路和调试排错过程给同样手头引脚紧张、又不甘心被库函数牵着走的朋友做个参考。1. 为什么把SPI缩成3线还要绕开驱动库1.1 4线SPI和3线SPI到底差在哪ADXL345本身支持两种SPI模式标准4线CS、SCLK、SDI、SDO和三线SPICS、SCLK、SDIO。4线模式里数据输入输出各走一根线全双工3线模式把SDI和SDO合并成一根SDIO线收发靠方向切换来完成半双工。项目4线SPI3线SPI数据线MOSI MISO 独立SDIO 单线双向数据流向全双工半双工SDO引脚接MCU的MISO悬空或接地适合场景高速、大吞吐省引脚、已有半双工外设有些朋友会问既然要省线直接用I2C不是更省吗I2C确实只要两根线但当时板子上SCL和SDA这两个IO已经被占用反倒是SPI的SCK和MOSI引脚空着只缺一个MISO。这种情况下用3线SPI最合适CS、SCLK、SDIO三根线正好凑齐SDO直接悬空不接。另外3线模式省下来的那个引脚可以拿去接ADXL345的INT1或者INT2做运动检测、自由落体中断都方便。实际项目里省引脚的意义往往不止是少焊一根线而是能把IO让给更重要的功能。1.2 “非库”不是炫技是真有需求很多人一听“非库”就觉得是在自讨苦吃其实不是。用库函数确实快HAL_SPI_TransmitReceive一调就完事但遇到三线半双工这种场景不少库的封装反而会让人卡住。首先三线模式在硬件上需要SPI外设切换到单线双向模式BIDIMODEHAL库里经常没有直接对口的API最后还是得回去翻寄存器。其次库的版本差异很大同一个传感器代码从单片机A移植到单片机B往往要重新适配一遍HAL版本而寄存器代码基本可以平移。最实际的一点是GPIO模拟的三线SPI可以工作在没有任何SPI外设的MCU上哪怕只是几千块钱成本的8位单片机也能跑。我这次选择“非库”还有一个原因调试时需要亲眼看到时钟边沿和数据线方向切换的过程。用GPIO模拟的话每一步都在自己手里示波器一抓就知道哪里不对用库函数的话出错时只能在封装外面猜。2. 动手前先把ADXL345的3线SPI时序读透2.1 命令字节的格式读和写根本不是一回事ADXL345的SPI通信是主从模式主机发一个命令字节再产生时钟让从机返回数据或者接收数据。命令字节8位结构是最高位是读写标志1读0写第6位是多字节标志MB剩下6位是寄存器地址。举例来说读DEVID寄存器地址0x00命令字节 0x80写POWER_CTL寄存器地址0x2D命令字节 0x2D从DATAX0开始连续读6个字节命令字节 0x80 | 0x40 | 0x32 0xF2第2条写寄存器时命令字节后面还要跟一个数据字节。第3条连续读时MB位置1后内部地址会自动递增主机只需要连续给6个时钟周期就能把DATAX0到DATAZ1全部读回来。这个机制在读取三轴加速度数据时非常常用比一个寄存器一个寄存器地读省一半时间。2.2 时钟极性和相位Mode 3才是标准答案ADXL345数据手册明确写了SPI时序要求空闲时SCLK保持高电平数据在SCLK下降沿被从机更新在上升沿被主机采样。这对应SPI Mode 3也就是CPOL1、CPHA1。实际调试中如果先用了Mode 0能读到数据也别觉得没问题。Mode 0的采样边沿和ADXL345的输出边沿差了一个相位数据可能刚好在电平跳变的瞬间被采样高低温或电压波动一上来就容易偶发错误。老实按Mode 3配置这是最不容易出问题的方案。SCLK速率方面ADXL345最高支持5MHz但3线模式因为是半双工方向切换需要额外的时间我建议先把分频调大一点比如1MHz以下跑通通信再逐步提速。我调试时从128分频开始确认DEVID正确后才加快。2.3 三线模式的方向切换是跟4线最大的区别3线模式下SDIO这根线一会儿是主机输出一会儿是主机输入。发送命令字节的阶段SDIO是输出方向命令字节发完之后如果是要读数据SDIO必须马上切换成输入方向然后主机继续产生时钟从机才能把数据放到这根线上。这里的核心逻辑是读操作需要的时钟还是主机提供但数据方向变了。写操作就简单命令字节和数据字节都是输出不需要切换方向。GPIO模拟的时候方向切换就是改写CRL寄存器的一个模式位而硬件SPI半双工模式需要操作BIDIOE位。这个细节没处理好读回来的数据就是错乱的。3. 不依赖库的实操寄存器配置与收发函数3.1 GPIO模拟三线SPI最通用的方案GPIO模拟的好处是不挑MCU逻辑也完全透明。下面这段代码以STM32F103为例PA4接CSPA5接SCLKPA7接SDIO。#define CS_LOW() GPIOA-BRR GPIO_Pin_4 #define CS_HIGH() GPIOA-BSRR GPIO_Pin_4 #define SCLK_LOW() GPIOA-BRR GPIO_Pin_5 #define SCLK_HIGH() GPIOA-BSRR GPIO_Pin_5 #define SDIO_OUT() GPIOA-CRL (GPIOA-CRL ~(0xF 28)) | (0x2 28) #define SDIO_IN() GPIOA-CRL (GPIOA-CRL ~(0xF 28)) | (0x4 28) #define SDIO_READ() ((GPIOA-IDR GPIO_Pin_7) ! 0) #define SDIO_HIGH() GPIOA-BSRR GPIO_Pin_7 #define SDIO_LOW() GPIOA-BRR GPIO_Pin_7 static void spi3_write_byte(uint8_t dat) { uint8_t i; for (i 0; i 8; i) { SCLK_LOW(); if (dat 0x80) SDIO_HIGH(); else SDIO_LOW(); SCLK_HIGH(); dat 1; } SCLK_LOW(); } static uint8_t spi3_read_byte(void) { uint8_t i, dat 0; SDIO_IN(); for (i 0; i 8; i) { dat 1; SCLK_LOW(); SCLK_HIGH(); if (SDIO_READ()) dat | 0x01; } SCLK_LOW(); SDIO_OUT(); return dat; } uint8_t ADXL345_ReadReg(uint8_t reg) { uint8_t val; CS_LOW(); spi3_write_byte(0x80 | reg); val spi3_read_byte(); CS_HIGH(); return val; } void ADXL345_WriteReg(uint8_t reg, uint8_t val) { CS_LOW(); spi3_write_byte(reg); spi3_write_byte(val); CS_HIGH(); } void ADXL345_ReadMulti(uint8_t reg, uint8_t *buf, uint8_t len) { uint8_t i; CS_LOW(); spi3_write_byte(0xC0 | reg); for (i 0; i len; i) { buf[i] spi3_read_byte(); } CS_HIGH(); }读字节函数里先切到输入模式然后拉低SCLK让从机更新数据拉高SCLK时采样数据正好对应Mode 3的时序要求。读写字节后都要把SDIO切回输出模式否则下一次写命令时数据线还是高阻状态命令根本发不出去。3.2 硬件SPI半双工模式寄存器怎么配如果MCU的SPI外设支持单线双向模式也可以不模拟直接用硬件。还是以STM32F103为例SPI1的PA5做SCKPA7做SDIO。void SPI1_3Wire_Init(void) { RCC-APB2ENR | RCC_APB2ENR_IOPAEN | RCC_APB2ENR_SPI1EN; GPIOA-CRL ~(0xFFFF 16); GPIOA-CRL | (0x3 16) | (0xB 20) | (0xB 28); SPI1-CR1 SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_MSTR | SPI_CR1_CPOL | SPI_CR1_CPHA | SPI_CR1_BIDIMODE | SPI_CR1_SPE; } uint8_t SPI1_ReadByte(uint8_t cmd) { uint8_t val; SPI1-CR1 | SPI_CR1_BIDIOE; while (!(SPI1-SR SPI_SR_TXE)); SPI1-DR cmd; while (!(SPI1-SR SPI_SR_RXNE)); (void)SPI1-DR; SPI1-CR1 ~SPI_CR1_BIDIOE; while (!(SPI1-SR SPI_SR_TXE)); SPI1-DR 0xFF; while (!(SPI1-SR SPI_SR_RXNE)); val SPI1-DR; return val; }这里有几处必须注意。第一CR1里必须同时置SSM和SSI用软件管理NSS否则硬件NSS被外部拉低时主机会自动退出主模式SPE位被硬件清零后续通信全部失效。第二发送完命令字节后RXNE会置位那个读到的数据是命令字节自身不是有效数据必须先读一次DR清掉标志再切换方向。第三BIDIOE位切换方向的时机一定要在发送完成之后最好等到TXE标志置位否则方向切换过早命令还没发完整根线就变成输入了从机收不到命令。3.3 从DEVID验证到输出加速度值通信函数就位后初始化流程其实很短。我最开始做的是读DEVID地址0x00正常返回0xE5。这一步通过硬件接线和基本时序就基本没问题了。void ADXL345_Init(void) { ADXL345_WriteReg(0x2D, 0x08); // POWER_CTL测量模式 ADXL345_WriteReg(0x31, 0x0B); // DATA_FORMAT全分辨率±16g ADXL345_WriteReg(0x2C, 0x0A); // BW_RATE100Hz } void ADXL345_ReadXYZ(float *xg, float *yg, float *zg) { uint8_t buf[6]; ADXL345_ReadMulti(0x32, buf, 6); int16_t x (int16_t)((buf[1] 8) | buf[0]); int16_t y (int16_t)((buf[3] 8) | buf[2]); int16_t z (int16_t)((buf[5] 8) | buf[4]); *xg x * 3.9f / 1000.0f; *yg y * 3.9f / 1000.0f; *zg z * 3.9f / 1000.0f; }寄存器地址分别是POWER_CTL 0x2D、DATA_FORMAT 0x31、BW_RATE 0x2C。DATA_FORMAT配成0x0B表示全分辨率模式加±16g量程此时灵敏度统一为3.9mg/LSB不管量程怎么变都按这个系数换算。BW_RATE配0x0A就是100Hz输出。读数据时从DATAX0开始连读6个字节低字节在前拼成int16再乘以3.9mg就是g值注意最后除以1000是把毫克换成克。4. GPIO模拟和硬件SPI两条路怎么选维度GPIO模拟硬件SPI半双工代码量少逻辑透明寄存器多但省CPU时序精度依赖延时误差大硬件保证稳定可移植性任意MCU近乎无脑移植需对照参考手册改寄存器速率上限受GPIO翻转速度限制可达MHz级学习价值高能彻底理解时序中需懂外设寄存器实际项目里如果MCU支持半双工SPI且引脚映射没问题我倾向于直接上硬件SPI时序稳定主循环不用卡在字节收发上。但如果只是验证传感器能不能通、数据正不正常GPIO模拟反而是首选原因很简单排查方便任意引脚都可以充当SDIO不用看引脚复用表。还有个折中做法先用GPIO模拟把整个流程跑通确认寄存器配置和数据结构都没问题后再切到硬件SPI半双工。这样即使后来出问题也能确定问题在外设配置而不是传感器本身。我这次就是这么干的GPIO版调通后硬件版只花了几分钟就验证完DEVID。ADXL345本身数据率不高最高也就3200HzGPIO模拟的速率完全够用。除非你把SPI速率拉到几MHz去刷数据否则GPIO模拟不会成为瓶颈。通信频率这块还有个经验如果是飞线连接SCLK频率建议压到1MHz以下线间距太近时还能降低串扰概率。5. 实测中绕不开的坑与排查记录5.1 DEVID读回来不是0xE5先查CS引脚DEVID是第一个排查关口读不到0xE5基本就是接线或模式问题。最常见的原因是CS没有被拉低ADXL345直接进入了I2C模式SPI时序自然全部作废。不少模块的CS默认通过上拉电阻拉高如果你在初始化函数里漏了拉低CS只靠命令字节里的地址信息是没用的SPI模式就是CS低电平选中I2C模式才会看地址。还有一种情况是SDO引脚悬空导致电平不确定在三线模式下这个引脚不参与数据但有些模块的PCB上SDO和SDIO之间有跳线或排阻默认状态可能让芯片认为自己在4线模式。如果怎么查都不对把SDO直接接地试一次可以排除这个变量。5.2 数据一直不更新或者固定在一个值如果DEVID读对了但加速度数据半天不动大概率是没有进入测量模式。POWER_CTL寄存器默认值是0x00这就是待机模式传感器不采样。我在第一次调试时就犯了这毛病初始化只配了DATA_FORMAT和BW_RATE忘了写POWER_CTL结果读到的一直是0。另一个容易被忽略的问题是BW_RATE设得太低比如0x07对应12.5Hz变化本来就慢拿手晃半天才更新一次会误以为数据没更新。建议调试时先设0x0A也就是100Hz确认数据变化后再降速。5.3 硬件NSS自动片选引发的诡异丢数据用硬件SPI半双工时如果NSS引脚恰好被外部拉低SPI主机会自动让出总线SPE位被清零。这种情况下最典型的特征是通信偶尔正常偶尔全部失败而且失败时读寄存器全是0xFF重新初始化后又活过来。原因是外部干扰或别的外设复用了NSS引脚把它拉低了。解决办法是CR1里同时置SSM和SSI让SPI完全使用软件片选不再检测NSS引脚状态。既然CS已经由普通GPIO控制硬件NSS这个功能就彻底屏蔽掉这也是很多例程里都写上这两位的原因。5.4 三线方向切换时机最容易踩的坑硬件SPI半双工模式下命令字节发送完毕后RXNE会先置位一次此时读DR读到的是自己刚发出去的命令字节。我一开始没清这个标志直接切方向然后发哑字节结果读回来的第一个数据其实是从机的真实数据后面再读就全部错位。排查半天才发现是把这次无效的RXNE当成了有效接收。GPIO模拟就没有这个问题因为软件完全掌控方向切换的时机。硬件SPI则一定要记住发送完命令后先读一次DR清掉RXNE再切方向。另外命令发送完到切换BIDIOE之间最好等到BSY标志清零再做方向切换否则可能出现最后一个bit还卡在线路上、数据线已经被切成输入的情况表现在数据上就是最高位丢了一位。我的经验是遇到三线通信数据对不上先拿示波器抓SDIO线的波形命令字节之后数据线上有没有出现从机的输出如果有再看方向和边沿对不对如果没有问题基本还在时序模式或CS控制上。这套排查链路走一遍90%的问题都能定位。本文还有配套的精品资源点击获取