SPI驱动ADXL345加速度计
一、SPI协议核心原理
SPI是全双工、同步、主从式的通信总线,想要驱动外设,先吃透物理层连线和时序规则。
1.1 物理层:四线制连接逻辑
SPI标准四线制的每根线都有明确分工,IMX6ULL(主设备)与ADXL345(从设备)的具体连线如下:
| 信号线 | 功能说明 | IMX6ULL引脚(ECSPI3) | ADXL345引脚 |
|---|---|---|---|
| SCLK | 串行时钟,主设备产生 | UART2_RX_DATA | SCLK |
| MOSI | 主发从收 | UART2_CTS_B | MOSI |
| MISO | 主收从发 | UART2_RTS_B | MISO |
| CS/SS | 从机选择(低电平有效,GPIO模拟) | UART2_TX_DATA(GPIO1_IO20) | CS |
注:IMX6ULL的ECSPI3本身自带硬件CS,但本文用GPIO模拟CS,更灵活,也便于新手理解"片选"的核心逻辑。
1.2 时序规则:CPOL与CPHA决定通信模式
SPI的时序由两个关键参数定义:CPOL(时钟极性) 和 CPHA(时钟相位) ,组合出4种通信模式,ADXL345推荐使用模式3(CPOL=1,CPHA=1)。
| 参数 | 含义 | 模式3配置 |
|---|---|---|
| CPOL | 时钟闲置时的电平:0=低电平闲置,1=高电平闲置 | 1 |
| CPHA | 数据采样边沿:0=时钟第1个边沿采样,1=时钟第2个边沿采样 | 1 |
模式3的时序逻辑:
- SCLK空闲时保持高电平;
- 主设备在SCLK的下降沿 发送数据,从设备在SCLK的上升沿采样数据(第二个边沿)。
二、IMX6ULL ECSPI控制器:从引脚到寄存器配置
IMX6ULL的SPI控制器称为ECSPI(增强型可配置SPI),我们以ECSPI3为例,完成底层硬件初始化。
2.1 引脚复用配置
IMX6ULL的引脚是多功能的,需要先将对应引脚复用为ECSPI3功能,并配置电气特性(上拉、速率等)。
c
#include "imx6ull.h"
// 引脚复用配置函数(底层寄存器封装,无需修改)
void IOMUXC_SetPinMux(unsigned int mux_register, unsigned int mux_mode);
void IOMUXC_SetPinConfig(unsigned int config_register, unsigned int config_value);
// ECSPI3引脚初始化
void spi3_pin_init(void)
{
// 1. MISO:UART2_RTS_B复用为ECSPI3_MISO
IOMUXC_SetPinMux(IOMUXC_UART2_RTS_B_ECSPI3_MISO, 0);
IOMUXC_SetPinConfig(IOMUXC_UART2_RTS_B_ECSPI3_MISO, 0x10B1); // 上拉、速率100MHz
// 2. MOSI:UART2_CTS_B复用为ECSPI3_MOSI
IOMUXC_SetPinMux(IOMUXC_UART2_CTS_B_ECSPI3_MOSI, 0);
IOMUXC_SetPinConfig(IOMUXC_UART2_CTS_B_ECSPI3_MOSI, 0x10B1);
// 3. SCLK:UART2_RX_DATA复用为ECSPI3_SCLK
IOMUXC_SetPinMux(IOMUXC_UART2_RX_DATA_ECSPI3_SCLK, 0);
IOMUXC_SetPinConfig(IOMUXC_UART2_RX_DATA_ECSPI3_SCLK, 0x10B1);
// 4. CS:UART2_TX_DATA复用为GPIO1_IO20
IOMUXC_SetPinMux(IOMUXC_UART2_TX_DATA_GPIO1_IO20, 0);
IOMUXC_SetPinConfig(IOMUXC_UART2_TX_DATA_GPIO1_IO20, 0x10B1);
// GPIO1_IO20设为输出,初始高电平(未选中ADXL345)
GPIO1->GDIR |= (1 << 20);
GPIO1->DR |= (1 << 20);
}
2.2 ECSPI3寄存器核心配置
IMX6ULL的ECSPI控制器核心配置在CONREG(控制寄存器)和CONFIGREG(配置寄存器),重点是匹配ADXL345的模式3时序,同时设置主机模式、数据长度、时钟分频。
c
// ECSPI3初始化:配置时序、模式、数据长度
void spi3_init(void)
{
// 1. 先初始化引脚
spi3_pin_init();
// 2. 复位CONREG寄存器
ECSPI3->CONREG = 0;
/* CONREG参数解析:
* 7<<20:BURST_LENGTH=7 → 数据长度8位(0~7共8位)
* 0x0E<<12:CLK_CTL=14 → 时钟分频(IPG_CLK=66MHz → 66/(14+1)=4.4MHz,适配ADXL345)
* 2<<8:CHIP_SELECT=2 → 选择通道0(ECSPI3_CH0)
* 1<<4:MODE=1 → 主机模式
* 1<<3:SCLK_CTL=1 → 时钟由CONREG的CLK_CTL控制
* 1<<0:ENABLE=1 → 使能ECSPI3
*/
ECSPI3->CONREG |= (7 << 20) | (0x0E << 12) | (2 << 8) | (1 << 4) | (1 << 3) | (1 << 0);
// 3. 复位CONFIGREG寄存器
ECSPI3->CONFIGREG = 0;
/* CONFIGREG参数解析:
* 1<<20:SCLK_POL=1 → CPOL=1(时钟高电平闲置)
* 1<<4:MOSI_POL=0 → MOSI引脚极性正常(无需翻转)
* 1<<0:PHASE=1 → CPHA=1(第二个边沿采样)
*/
ECSPI3->CONFIGREG |= (1 << 20) | (1 << 4) | (1 << 0);
}
三、ADXL345驱动实现:SPI读写+传感器控制
ADXL345的核心操作是"读写寄存器"------通过SPI向传感器寄存器写配置、读数据,先实现SPI底层读写,再封装传感器专属接口。
3.1 SPI全双工读写基础函数
SPI是全双工通信:发送1个字节的同时,必然会接收1个字节。因此读写函数的核心是"发数据+等接收",利用ECSPI的TX/RX FIFO完成数据传输。
c
// SPI3通道0读写函数:发送1字节,同时接收1字节(全双工)
unsigned int spi3_ch0_write_and_read(unsigned int data)
{
// 等待发送FIFO为空(准备好接收新数据)
while((ECSPI3->STATREG & (1 << 0)) == 0);
// 往TXDATA写入要发送的数据
ECSPI3->TXDATA = data;
// 等待接收FIFO有数据(数据已接收完成)
while((ECSPI3->STATREG & (1 << 3)) == 0);
// 返回接收到的数据
return ECSPI3->RXDATA;
}
3.2 ADXL345寄存器读写封装
ADXL345的寄存器读写有固定规则:
- 读操作:寄存器地址最高位置1(
reg_addr | 0x80),发送地址后发0xFF(伪数据)触发时钟,读取返回值; - 写操作:寄存器地址最高位清0,发送地址后发送要写入的数据。
c
// ADXL345读寄存器:reg_addr=寄存器地址,返回寄存器值
unsigned char adxl345_read(unsigned char reg_addr)
{
unsigned char ret = 0;
// 1. 拉低CS,选中ADXL345
GPIO1->DR &= ~(1 << 20);
// 2. 发送读指令(地址最高位1)
spi3_ch0_write_and_read(reg_addr | 0x80);
// 3. 发送伪数据0xFF,读取传感器返回值
ret = spi3_ch0_write_and_read(0xFF);
// 4. 拉高CS,结束通信
GPIO1->DR |= (1 << 20);
return ret;
}
// ADXL345写寄存器:reg_addr=寄存器地址,data=要写入的值
void adxl345_write(unsigned char reg_addr, unsigned char data)
{
// 1. 拉低CS,选中ADXL345
GPIO1->DR &= ~(1 << 20);
// 2. 发送写指令(地址最高位0)
spi3_ch0_write_and_read(reg_addr & ~0x80);
// 3. 发送要写入的数据
spi3_ch0_write_and_read(data);
// 4. 拉高CS,结束通信
GPIO1->DR |= (1 << 20);
}
3.3 ADXL345初始化与数据采集
ADXL345使用前需要初始化(配置量程、进入测量模式),然后读取三轴加速度数据(16位,低位在前)。
c
// 定义ADXL345数据结构体:存储X/Y/Z三轴加速度值
typedef struct {
short x; // X轴加速度
short y; // Y轴加速度
short z; // Z轴加速度
} ADXL345_Data;
// ADXL345初始化:验证ID+配置量程+进入测量模式
int adxl345_init(void)
{
unsigned char dev_id;
// 1. 读取设备ID(DEVID寄存器地址0x00,固定值0xE5)
dev_id = adxl345_read(0x00);
if(dev_id != 0xE5) {
return -1; // ID错误,初始化失败
}
// 2. 配置DATA_FORMAT(0x31):±16g量程(0x0F)
adxl345_write(0x31, 0x0F);
// 3. 配置POWER_CTL(0x2D):0x08 → 进入测量模式
adxl345_write(0x2D, 0x08);
return 0; // 初始化成功
}
// 读取ADXL345三轴加速度数据
ADXL345_Data adxl345_read_data(void)
{
ADXL345_Data data;
// X轴:低字节(0x32) + 高字节(0x33)
data.x = adxl345_read(0x32);
data.x |= (short)(adxl345_read(0x33) << 8);
// Y轴:低字节(0x34) + 高字节(0x35)
data.y = adxl345_read(0x34);
data.y |= (short)(adxl345_read(0x35) << 8);
// Z轴:低字节(0x36) + 高字节(0x37)
data.z = adxl345_read(0x36);
data.z |= (short)(adxl345_read(0x37) << 8);
return data;
}
四、主函数测试:验证SPI驱动是否正常
最后写主函数,初始化硬件后循环读取加速度数据,可通过串口打印(需提前实现串口初始化)。
c
int main(void)
{
ADXL345_Data accel_data;
// 1. 初始化SPI3
spi3_init();
// 2. 初始化ADXL345
if(adxl345_init() != 0) {
// 串口打印ID错误(需实现uart_printf)
uart_printf("ADXL345 init failed! ID error\r\n");
while(1);
}
uart_printf("ADXL345 init success!\r\n");
// 3. 循环读取三轴数据
while(1) {
accel_data = adxl345_read_data();
// 打印X/Y/Z轴数据
uart_printf("X: %d, Y: %d, Z: %d\r\n", accel_data.x, accel_data.y, accel_data.z);
// 简单延时
for(int i=0; i<1000000; i++);
}
return 0;
}
五、实战调试:常见问题排查
新手最容易遇到"ID读取为0/255"或"数据乱跳",按以下步骤排查:
5.1 ID读取异常(0/255)
- 硬件连线:检查SCLK/MOSI/MISO/CS的引脚是否接反、虚焊;
- CS电平:用万用表测GPIO1_IO20,读寄存器时是否拉低,读完是否拉高;
- SPI模式:确认CONFIGREG的CPOL(1<<20)和CPHA(1<<0)是否配置正确(模式3);
- 时钟分频:ECSPI3的时钟不能超过ADXL345的最大速率(5MHz),本文分频后4.4MHz,符合要求;
- 逻辑分析仪抓波 :观察MOSI是否发送了
0x80(读ID指令),MISO是否返回0xE5。
5.2 数据乱跳
- 电源稳定性:ADXL345需3.3V供电,确保电源纹波小;
- 接地:传感器的GND与IMX6ULL的GND共地,避免干扰;
- 量程配置:确认DATA_FORMAT寄存器配置的量程与数据解析匹配(如±16g对应分辨率13mg/LSB)。
六、总结
本文从SPI协议原理出发,完成了IMX6ULL ECSPI3的底层配置,再到ADXL345的寄存器读写封装,最终实现了三轴加速度数据的采集。核心要点:
- SPI的关键是时序匹配(CPOL/CPHA),必须与外设一致;
- IMX6ULL的ECSPI配置重点是"主机模式+数据长度+时钟分频";
- ADXL345的核心是"寄存器读写规则"(读地址最高位1,写地址最高位0);
- 裸机开发中,"等待FIFO""CS电平控制"是SPI通信的关键细节。