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

分享
四线制 SPI 连接图
四线制 SPI:SCK、MOSI、MISO、CS

这篇笔记继续记录在正点原子 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 有哪几根信号线?

四线制 SPI 信号线
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 对比
I2C 与 SPI 的主要区别
特性I2CSPI
信号线2 根:SDA、SCL4 根: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:第二个边沿采样(第一个边沿改变数据)。
SPI 四种模式时序
CPOL 与 CPHA 组合出 SPI 的四种工作模式

5. SPI 有几种工作方式,请简述?

由 CPOL 和 CPHA 两两组合,SPI 共有 4 种工作模式:

模式CPOLCPHA空闲时钟采样边沿
Mode 000低上升沿
Mode 101低下降沿
Mode 210高下降沿
Mode 311高上升沿

具体用哪种模式,由从机芯片手册决定。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。
ADXL345 三轴
ADXL345 可同时测量 X、Y、Z 三轴加速度

7. ADXL345 三轴加速度采集实验

7.1 硬件连接

本实验使用 I.MX6ULL 的 ECSPI3 接口连接 ADXL345,引脚映射如下:

ADXL345I.MX6ULL 引脚功能
SCLKUART2_RX_DATA / ECSPI3_SCLK时钟
MOSIUART2_CTS_B / ECSPI3_MOSI主机输出
MISOUART2_RTS_B / ECSPI3_MISO主机输入
CSUART2_TX_DATA / GPIO1_IO20片选(软件控制)

7.2 SPI3 初始化流程

SPI3 初始化流程
I.MX6ULL 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 的时序更直观,速率也更高,适合对实时性要求高的传感器或存储设备。

继续阅读

嵌入式开发面试知识手册

嵌入式开发工程师面试知识手册 适用对象:零基础起步、目标是通过嵌入式软件/驱动/BSP 方向面试的读者 编写依据:赵哲简历全部知识点 + 智能车 AGV 控制平台完整项目代码(i.MX6ULL Linux 端 + STM32F407 固件)+ Linux 驱动练习代码包 + embedded-agent 嵌入式开发智能体仓库 + 真实面试手撕代码与追问记录 使用方法:每篇按"通俗解释 → 原理细节 → 命令/API → 代码实例 → 项目结合 → 记忆口诀 → 面试问答"组织;口诀用于快速记忆,问答用于自测。第9篇是讲项目的演练场,第10篇是手撕代码实战题库。 总目录 * 第1篇 C语言与数据结构 * 1.1 编译链接全过程 * 1.2 数据类型与内存

作者:azhezhezhe

正点原子 I.MX6ULL MINI 开发板学习笔记:Linux 驱动架构详解——编译、树内树外与运行时效果

这是 Linux 驱动小系列的总结篇。前三篇把 LED 驱动从内核模块写到 misc 设备,属于“会用”;这篇把背后的架构讲透:驱动到底怎么编译、内核树内和树外两种方式的区别、以及驱动里每个操作在系统跑起来之后到底产生了什么效果。内容整理自我的学习笔记(note.pdf)和实际工程 code/2026/20260803。 1. 驱动代码到底怎么编译的? 不管哪种方式,最终都靠内核的 kbuild 系统。核心就三个文件的关系: * Kconfig:定义 menuconfig 里的选项(如 CONFIG_MYMODULE),三态 y/m/n——y 编进内核,m 编成模块,n 不编; * .config:menuconfig 的选择结果,保存在源码顶层; * Makefile:

作者:azhezhezhe

正点原子 I.MX6ULL MINI 开发板学习笔记:misc 设备——字符设备驱动的快捷方式

上两篇分别用手动 mknod 和 class_create+device_create 注册字符设备,调用链一长串。这篇用 misc(混杂)设备 一步搞定。代码来自实际工程 code/2026/20260803/05_misc_led,已验证可用。 1. 完成什么实验? 用 misc_register 重写 LED 驱动:一步完成设备号分配、字符设备注册、/dev/myled0 节点创建,同时保留上一篇的 sysfs 属性 led_status。 2. 什么是 misc 设备? misc(混杂)设备是 Linux 给“不好归类的小字符设备”

作者:azhezhezhe