正点原子 I.MX6ULL MINI 开发板学习笔记:SPI 与 ADXL345 三轴加速度采集

这篇笔记继续记录在正点原子 I.MX6ULL MINI 开发板上的学习过程,主题是 SPI 总线与 ADXL345 三轴加速度采集。内容基于实际工程 /home/zhao/code/2026/7.28SPI/spi 中的验证代码。
1. 什么是 SPI?
SPI(Serial Peripheral Interface,串行外设接口)是一种高速、全双工、同步的串行通信协议。它由 Motorola 提出,广泛用于 MCU/SoC 与各种外设(传感器、Flash、显示屏等)之间的短距离通信。
SPI 没有复杂的寻址机制,主机通过一根专门的片选线(CS)来选择从机,通信时主机提供时钟,数据在时钟边沿同步收发。
2. 四线制 SPI 有哪几根信号线?

- SCK(Serial Clock):串行时钟,由主机产生,用于同步数据传输。
- MOSI(Master Out Slave In):主机输出、从机输入。主机发送数据到从机。
- MISO(Master In Slave Out):主机输入、从机输出。从机发送数据到主机。
- CS/SS(Chip Select / Slave Select):片选信号,低电平有效。主机拉低某从机的 CS,即选中该从机。
因为 MOSI 和 MISO 是独立的,所以 SPI 可以全双工工作:主机发送数据的同时也能接收数据。
3. I2C 和 SPI 有什么区别?

| 特性 | I2C | SPI |
|---|---|---|
| 信号线 | 2 根:SDA、SCL | 4 根:SCK、MOSI、MISO、CS |
| 通信方向 | 半双工 | 全双工 |
| 寻址方式 | 7/10 位从机地址 | 片选线 CS |
| 拓扑结构 | 多主多从 | 通常一主多从 |
| 通信速率 | 标准 100Kbps,快速 400Kbps | 可达数 Mbps 甚至更高 |
| 硬件要求 | 需要上拉电阻 | 无需上拉,线多一根 |
简单记忆:I2C 省线但慢,SPI 费线但快。ADXL345 同时支持 I2C 和 SPI,本实验用 SPI 就是为了更高的刷新率和更简单的时序控制。
4. CPOL 和 CPHA 分别代表什么含义?
SPI 的时钟灵活性就靠这两个参数定义。
- CPOL(Clock Polarity,时钟极性):决定 SCK 在空闲时的电平。
- CPOL = 0:空闲时 SCK 为低电平。
- CPOL = 1:空闲时 SCK 为高电平。
- CPHA(Clock Phase,时钟相位):决定数据在哪个时钟边沿被采样。
- CPHA = 0:第一个边沿采样(第二个边沿改变数据)。
- CPHA = 1:第二个边沿采样(第一个边沿改变数据)。

5. SPI 有几种工作方式,请简述?
由 CPOL 和 CPHA 两两组合,SPI 共有 4 种工作模式:
| 模式 | CPOL | CPHA | 空闲时钟 | 采样边沿 |
|---|---|---|---|---|
| Mode 0 | 0 | 0 | 低 | 上升沿 |
| Mode 1 | 0 | 1 | 低 | 下降沿 |
| Mode 2 | 1 | 0 | 高 | 下降沿 |
| Mode 3 | 1 | 1 | 高 | 上升沿 |
具体用哪种模式,由从机芯片手册决定。ADXL345 的 SPI 仅支持 Mode 3(CPOL=1、CPHA=1,datasheet 明确),本实验代码里通过设置 ECSPI3->CONFIGREG 的 SCLK_CTL=1、SCLK_POL=1、SCLK_PHA=1,使用的是 Mode 3(CPOL=1,CPHA=1)。
6. ADXL345 传感器的分辨率是多少位?量程?供电电压?
ADXL345 是一款常用的三轴数字加速度计,关键参数如下:
- 分辨率:在“满分辨率(Full Resolution)”模式下为 13 位;固定量程模式下为 10 位。本实验代码里通过写寄存器
0x31 = 0x0B开启了满分辨率。 - 量程:可配置为 ±2g / ±4g / ±8g / ±16g。代码里同样由
0x31寄存器决定,本例配置为 ±16g。 - 供电电压:2.0V ~ 3.6V,开发板上通常接 3.3V。
- 灵敏度:满分辨率下,任意量程里 LSB 约为 3.9 mg,所以代码里用
ret.x * 0.0039换算成 g。

7. ADXL345 三轴加速度采集实验
7.1 硬件连接
本实验使用 I.MX6ULL 的 ECSPI3 接口连接 ADXL345,引脚映射如下:
| ADXL345 | I.MX6ULL 引脚 | 功能 |
|---|---|---|
| SCLK | UART2_RX_DATA / ECSPI3_SCLK | 时钟 |
| MOSI | UART2_CTS_B / ECSPI3_MOSI | 主机输出 |
| MISO | UART2_RTS_B / ECSPI3_MISO | 主机输入 |
| CS | UART2_TX_DATA / GPIO1_IO20 | 片选(软件控制) |
7.2 SPI3 初始化流程

7.3 真实可用的 SPI/ADXL345 代码
下面是实际工程 /home/zhao/code/2026/7.28SPI/spi 中验证过的核心代码。
spi.h
#ifndef _SPI_H_
#define _SPI_H_
extern void spi3_init(void);
extern unsigned int spi3_read_write(unsigned int data);
#endif
spi.c
#include "MCIMX6Y2.h"
#include "core_ca7.h"
#include "fsl_iomuxc.h"
#include "spi.h"
/* SPI3 初始化 */
void spi3_init(void)
{
/* 1. 引脚复用:UART2 复用为 ECSPI3 */
IOMUXC_SetPinMux(IOMUXC_UART2_RX_DATA_ECSPI3_SCLK, 0);
IOMUXC_SetPinMux(IOMUXC_UART2_RTS_B_ECSPI3_MISO, 0);
IOMUXC_SetPinMux(IOMUXC_UART2_CTS_B_ECSPI3_MOSI, 0);
IOMUXC_SetPinMux(IOMUXC_UART2_TX_DATA_GPIO1_IO20, 0);
/* 2. 电气属性配置 */
IOMUXC_SetPinConfig(IOMUXC_UART2_RX_DATA_ECSPI3_SCLK, 0x10B1);
IOMUXC_SetPinConfig(IOMUXC_UART2_RTS_B_ECSPI3_MISO, 0x10B1);
IOMUXC_SetPinConfig(IOMUXC_UART2_CTS_B_ECSPI3_MOSI, 0x10B1);
IOMUXC_SetPinConfig(IOMUXC_UART2_TX_DATA_GPIO1_IO20, 0x10B1);
/* 3. 软件片选默认拉高 */
GPIO1->GDIR |= (1 << 20);
GPIO1->DR |= (1 << 20);
/* 4. 配置 CONREG:使能 SPI、主模式、通道 0 */
ECSPI3->CONREG = 0;
ECSPI3->CONREG |= (7 << 20) | (0x0E << 12) | (2 << 8) | (1 << 4) | (1 << 3) | (1 << 0);
/* 5. 配置 CONFIGREG:SCLK_CTL=1、SCLK_POL=1、SCLK_PHA=1,对应 Mode 3 */
ECSPI3->CONFIGREG = 0;
ECSPI3->CONFIGREG |= (1 << 20) | (1 << 4) | (1 << 0);
}
/* SPI 全双工读写:发一个字节/字,同时收一个字节/字 */
unsigned int spi3_read_write(unsigned int data)
{
/* 等待 TXFIFO 就绪 */
while ((ECSPI3->STATREG & (1 << 0)) == 0);
ECSPI3->TXDATA = data;
/* 等待 RXFIFO 有数据 */
while ((ECSPI3->STATREG & (1 << 3)) == 0);
return ECSPI3->RXDATA;
}
adxl345.h
#ifndef _ADXL345_H
#define _ADXL345_H
struct ADXL345_Data
{
short x;
short y;
short z;
float xg;
float yg;
float zg;
};
extern void adxl345_write(unsigned char reg_address, unsigned char data);
extern unsigned char adxl345_read(unsigned char reg_address);
extern void adxl345_init(void);
extern struct ADXL345_Data adxl345_get_value(void);
#endif
adxl345.c
#include "adxl345.h"
#include "spi.h"
#include "MCIMX6Y2.h"
/* 向 ADXL345 寄存器写一个字节 */
void adxl345_write(unsigned char reg_address, unsigned char data)
{
GPIO1->DR &= ~(1 << 20); /* CS 拉低 */
spi3_read_write(reg_address); /* 发送寄存器地址 */
spi3_read_write(data); /* 发送数据 */
GPIO1->DR |= (1 << 20); /* CS 拉高 */
}
/* 从 ADXL345 寄存器读一个字节 */
unsigned char adxl345_read(unsigned char reg_address)
{
unsigned char ret = 0;
GPIO1->DR &= ~(1 << 20); /* CS 拉低 */
spi3_read_write(reg_address | 0x80); /* 地址最高位置 1 表示读 */
ret = spi3_read_write(0xFF); /* 发送占位字节,同时接收数据 */
GPIO1->DR |= (1 << 20); /* CS 拉高 */
return ret;
}
/* ADXL345 初始化 */
void adxl345_init(void)
{
adxl345_write(0x2E, 0x08); /* 使能 DATA_READY 中断(0x2E 为 INT_ENABLE 寄存器) */
adxl345_write(0x31, 0x0B); /* 满分辨率 13 位,±16g */
adxl345_write(0x2C, 0x08); /* 输出数据速率 25Hz */
adxl345_write(0x2D, 0x0B); /* 测量模式 */
}
/* 读取三轴加速度并换算成 g */
struct ADXL345_Data adxl345_get_value(void)
{
struct ADXL345_Data ret;
ret.x = adxl345_read(0x32);
ret.x |= adxl345_read(0x33) << 8;
ret.y = adxl345_read(0x34);
ret.y |= adxl345_read(0x35) << 8;
ret.z = adxl345_read(0x36);
ret.z |= adxl345_read(0x37) << 8;
ret.xg = ret.x * 0.0039;
ret.yg = ret.y * 0.0039;
ret.zg = ret.z * 0.0039;
return ret;
}
main.c 中的调用
#include "spi.h"
#include "adxl345.h"
int main(void)
{
/* 板级启动代码完成时钟、中断、UART 等初始化 */
clock_init();
system_interrupt_init();
led_init();
beep_init();
gpt_init();
uart_init();
i2c_init();
spi3_init();
adxl345_init();
led_off();
beep_off();
while (1)
{
struct ADXL345_Data t = adxl345_get_value();
int i = t.xg * 10;
int j = t.yg * 10;
int k = t.zg * 10;
printf("x_accel:%d.%d y_accel:%d.%d z_accel:%d.%d\n",
i / 10, i % 10, j / 10, j % 10, k / 10, k % 10);
delay_ms(1000);
}
return 0;
}
7.4 关键点说明
- 片选软件控制:本例把 CS 接到普通 GPIO(GPIO1_IO20),在收发前后手动拉低/拉高。也可以用 SPI 控制器的硬件片选。
- 读寄存器要置位 0x80:ADXL345 的 SPI 读时序要求寄存器地址最高位为 1,即
reg_address | 0x80。 - 多字节读取:实际工程中可以用连续地址读取一次性把 X、Y、Z 六个字节全部读出,这里为了清晰分成六次单字节读。
- 数值换算:满分辨率下 1 LSB ≈ 3.9 mg,所以乘以 0.0039 得到以 g 为单位的加速度。
- SCLK 频率:ECSPI 时钟根为 60MHz,CONREG 中 PRE_DIVIDER=0(1 分频)、POST_DIVIDER=7(8 分频),最终 SCLK = 60MHz / 8 = 7.5MHz。
7.5 实验现象
开发板静止平放时,Z 轴接近 1g(重力加速度),X、Y 轴接近 0g;倾斜或晃动板子时,三轴数值会相应变化。例如:
x_accel:0.0 y_accel:0.1 z_accel:1.0
x_accel:0.5 y_accel:0.0 z_accel:0.8
x_accel:0.-2 y_accel:0.3 z_accel:0.9
实际数据会有轻微抖动,这是传感器噪声和人体抖动带来的正常波动。注意第三行:示例打印逻辑用 %d.%d 拼接整数位和小数位,-0.2g 时 i/10=0、i%10=-2,所以显示成 0.-2;工程上更稳妥的写法是先判断符号、再打印绝对值。
总结
| 问题 | 答案 |
|---|---|
| 什么是 SPI? | 高速、全双工、同步串行通信协议 |
| I2C 和 SPI 区别? | I2C 两线半双工、省引脚;SPI 四线全双工、速度快 |
| 四线制 SPI 信号线? | SCK、MOSI、MISO、CS |
| CPOL / CPHA? | 时钟极性 / 时钟相位,决定空闲电平和采样边沿 |
| SPI 几种工作方式? | 4 种(Mode 0/1/2/3),由 CPOL 和 CPHA 组合 |
| ADXL345 分辨率? | 满分辨率 13 位 |
| ADXL345 量程? | ±2g / ±4g / ±8g / ±16g(本例 ±16g) |
| ADXL345 供电电压? | 2.0V ~ 3.6V(开发板接 3.3V) |
通过 SPI 读取 ADXL345,让我对 SPI 的全双工、CPOL/CPHA、片选控制有了实际体会。相比 I2C,SPI 的时序更直观,速率也更高,适合对实时性要求高的传感器或存储设备。