i.MX6ULL 裸机 ECSPI 驱动开发详解:

在嵌入式裸机开发中,SPI(串行外设接口)是最常用的高速同步串行总线之一,广泛用于连接 Flash、加速度传感器、ADC、OLED 屏等外设。i.MX6ULL 作为 Cortex-A7 内核的工业级 MPU,内置了 4 路增强型可配置 SPI 外设(ECSPI),支持主机 / 从机模式、最高 60MHz 总线时钟能满足绝大多数 SPI 外设的驱动需求。

本文基于正点原子 i.MX6ULL-MINI 开发板,从零实现 ECSPI3 主机模式的裸机驱动,结合寄存器原理逐行拆解代码,详细讲解 SPI 驱动开发的核心逻辑与注意事项,最终实现通用的 SPI 全双工读写能力。

一、SPI 总线核心原理回顾

在讲解 i.MX6ULL 的 ECSPI 外设前,先明确 SPI 总线的核心特性,这是驱动代码设计的基础:

  1. 四线制硬件架构标准 SPI 采用 4 根信号线通信,主从机的连接关系固定:

    • SCLK:串行时钟线,主机输出,用于同步收发数据,时钟频率由主机配置;
    • MOSI:主发从收线,主机发送数据、从机接收数据;
    • MISO:主收从发线,从机发送数据、主机接收数据;
    • CS:片选线,主机输出,有效电平与外设有关,同一时刻只能拉低一个从机的 CS 引脚,确保总线只有一个从机处于工作状态。
  2. 全双工通信特性 SPI 是全双工总线,发送和接收过程完全同步:主机在 SCLK 的驱动下,通过 MOSI 向从机移位发送 1 个 bit 的同时,会通过 MISO 从从机移位接收 1 个 bit。也就是说,每完成 8 个时钟周期(1 字节)的发送,必然同时完成 1 字节的接收,这也是 SPI 驱动通常只用一个读写函数就能完成收发的核心原因。

  3. 4 种 SPI 时序模式SPI 的时序由两个核心参数决定,必须和从机设备手册严格匹配,否则会出现采样错误:

    • CPOL(时钟极性):决定总线空闲时 SCLK 的电平状态。CPOL=0,空闲时 SCLK 为低电平;CPOL=1,空闲时 SCLK 为高电平。
    • CPHA(时钟相位):决定数据采样的时钟沿。CPHA=0,在 SCLK 的第一个跳变沿采样数据;CPHA=1,在 SCLK 的第二个跳变沿采样数据。

    两个参数组合出 4 种 SPI 模式,本文驱动默认配置为模式 3(CPOL=1,CPHA=1),适配 ADXL345 等绝大多数常用 SPI 传感器。

二、i.MX6ULL ECSPI 外设核心寄存器

i.MX6ULL 的 ECSPI 外设配置完全基于寄存器实现,驱动代码的本质就是对寄存器的正确配置。本文只讲解代码中用到的核心寄存器,完整寄存器说明可参考《i.MX6ULL 参考手册》第 20 章。

2.1 控制寄存器(CONREG)

该寄存器是 ECSPI 的核心配置寄存器,32 位有效,决定了外设使能、主从模式、时钟分频、突发长度等核心参数,代码中重点配置的位段如下:

表格

位段 位宽 代码配置 功能说明
BURST_LENGTH 31-20 7<<20 突发传输长度,值为 N 表示一次传输 N+1 个 bit,配置为 7 即一次传输 8bit(1 字节)
PRE_DIVIDER 15-12 14<<12 预分频器,值为 N 对应 N+1 分频,配置为 14 即 15 分频
POST_DIVIDER 11-8 2<<8 后分频器,值为 N 对应 2^N 分频,配置为 2 即 4 分频
CHANNEL_MODE 7-4 1<<4 通道主从模式,bit0 对应通道 0,置 1 表示通道 0 配置为主机模式
SMC 3 1<<3 传输启动模式,置 1 表示向 TXFIFO 写入数据时自动启动 SPI 传输
EN 0 1<<0 ECSPI 外设使能位,置 1 开启外设,置 0 关闭外设

2.2 配置寄存器(CONFIGREG)

该寄存器主要用于配置 SPI 时序模式、片选极性等参数,和从设备时序匹配的核心配置都在这个寄存器中:

表格

位段 位宽 代码配置 功能说明
SCLK_POL 7-4 1<<4 时钟极性配置,bit0 对应通道 0,置 1 表示 CPOL=1,空闲时 SCLK 为高电平
SCLK_PHA 3-0 1<<0 时钟相位配置,bit0 对应通道 0,置 1 表示 CPHA=1,第二个时钟沿采样数据

2.3 状态寄存器(STATREG)

该寄存器是只读寄存器,用于获取 ECSPI 的工作状态,驱动中通过该寄存器判断收发是否完成,重点用到两个位:

  • TE(bit0):TXFIFO 空标志位,置 1 表示发送 FIFO 为空,可写入新的发送数据;
  • RR(bit3):RXFIFO 就绪标志位,置 1 表示接收 FIFO 中有有效数据,可读取。

2.4 数据寄存器

  • TXDATA:发送数据寄存器,向该寄存器写入数据,会将数据压入 TXFIFO,在 SMC=1 的模式下会自动启动 SPI 传输;
  • RXDATA:接收数据寄存器,从该寄存器读取数据,会取出 RXFIFO 中的有效数据,同时自动清除 RR 标志位。

三、硬件设计与工程结构

3.1 硬件引脚复用

本文使用 i.MX6ULL 的 ECSPI3 外设,开发板上该外设的引脚与 UART2 引脚复用,具体映射关系如下,同时采用软件片选的方式,使用 GPIO1_IO20 作为 CS 引脚,比硬件片选更灵活:

表格

功能 芯片引脚 复用配置宏定义
ECSPI3_MOSI UART2_CTS_B IOMUXC_UART2_CTS_B_ECSPI3_MOSI
ECSPI3_MISO UART2_RTS_B IOMUXC_UART2_RTS_B_ECSPI3_MISO
ECSPI3_SCLK UART2_RX_DATA IOMUXC_UART2_RX_DATA_ECSPI3_SCLK
SPI_CS (软件) UART2_TX_DATA IOMUXC_UART2_TX_DATA_GPIO1_IO20

3.2 工程文件结构

驱动工程分为两个文件,结构极简,便于移植和复用:

  • spi.h:头文件,驱动函数的外部声明;
  • spi.c:源文件,ECSPI 初始化、核心读写函数的实现。

四、驱动代码逐行深度解析

4.1 头文件 spi.h

头文件仅做函数声明,对外暴露两个核心接口,符合 SPI 驱动的最小化设计原则:

c

运行

复制代码
#ifndef  __SPI_H__
#define __SPI_H__
// ECSPI初始化函数,完成引脚、时钟、模式的全部配置
extern void spi_init(void);
// SPI全双工读写函数,发送1个数据的同时接收1个数据
extern unsigned int spi_write_read(unsigned int data);
#endif // ! __SPI_H__

这里的spi_write_read是 SPI 驱动的核心,基于 SPI 全双工特性,一个函数即可完成发送、接收两种操作:

  • 仅发送数据:调用函数后忽略返回值即可;
  • 仅接收数据:向函数传入 0xFF(空数据),接收返回值即可。

4.2 初始化函数 spi_init ()

初始化函数分为引脚复用与电气属性配置软件片选 GPIO 配置ECSPI 核心寄存器配置三个部分,逐行解析如下:

c

运行

复制代码
#include "fsl_common.h"
#include "fsl_iomuxc.h"
#include "MCIMX6Y2.h"
#include "spi.h"

void spi_init(void)
{
    // 第一部分:引脚复用配置
    IOMUXC_SetPinMux(IOMUXC_UART2_CTS_B_ECSPI3_MOSI, 0);
    IOMUXC_SetPinMux(IOMUXC_UART2_RTS_B_ECSPI3_MISO, 0);
    IOMUXC_SetPinMux(IOMUXC_UART2_RX_DATA_ECSPI3_SCLK, 0);
    IOMUXC_SetPinMux(IOMUXC_UART2_TX_DATA_GPIO1_IO20, 0);
  • IOMUXC_SetPinMux是 NXP SDK 封装的引脚复用配置函数,第一个参数是引脚复用的宏定义,指定了引脚的功能;第二个参数是 SION 位(软件输入使能),这里配置为 0,关闭强制输入。
  • 前三个引脚分别配置为 ECSPI3 的 MOSI、MISO、SCLK 功能,第四个引脚配置为 GPIO1_IO20,用于软件片选。

c

运行

复制代码
    // 第二部分:引脚电气属性配置
    IOMUXC_SetPinConfig(IOMUXC_UART2_CTS_B_ECSPI3_MOSI, 0x10B1);
    IOMUXC_SetPinConfig(IOMUXC_UART2_RTS_B_ECSPI3_MISO, 0x10B1);
    IOMUXC_SetPinConfig(IOMUXC_UART2_RX_DATA_ECSPI3_SCLK, 0x10B1);
    IOMUXC_SetPinConfig(IOMUXC_UART2_TX_DATA_GPIO1_IO20, 0x10B1);
  • IOMUXC_SetPinConfig用于配置引脚的电气特性,所有 SPI 相关引脚统一配置为0x10B1,该值的二进制为0 0001 0000 1011 0001,对应 PAD 寄存器的 bit16~bit0,关键位拆解如下:
    • bit16(HYS)=0:关闭迟滞比较器,SPI 引脚作为高速输出无需迟滞;
    • bit12(PKE)=1:使能上下拉 / 状态保持器功能;
    • bit11(ODE)=0:关闭开路输出,SPI 采用推挽输出模式;
    • bit7-6(SPEED)=10:配置引脚速度为 100MHz 中速,满足 SPI 高速通信需求;
    • bit5-3(DSE)=110:驱动能力配置为 R0/6,保证 SPI 信号的驱动强度;
    • bit0(SRE)=1:开启高压摆率,加快电平跳变速度,减少高速信号的畸变。

c

运行

复制代码
    // 第三部分:软件片选GPIO配置
    GPIO1->GDIR |= (1 << 20); // GPIO1_IO20配置为输出模式
    GPIO1->DR |= (1 << 20);    // GPIO1_IO20默认输出高电平,不选中从机
  • SPI 片选默认低电平有效,这里初始化 GPIO 为输出模式,默认输出高电平,确保总线空闲时没有从机被选中。
  • 软件片选的优势在于时序控制完全自主,不受 ECSPI 硬件通道限制,可适配任意 CS 引脚的硬件设计。

c

运行

复制代码
    // 第四部分:ECSPI核心寄存器配置
    ECSPI3->CONREG = 0; // 先清零控制寄存器,避免默认值干扰
    ECSPI3->CONREG |= (1 << 0); // 使能ECSPI3外设
    // 配置突发长度、分频系数、主机模式、传输启动模式
    ECSPI3->CONREG |= (7 << 20) | (14 << 12) | (2 << 8) | (1 << 4) | (1 << 3);
    
    ECSPI3->CONFIGREG = 0; // 清零配置寄存器
    // 配置SPI模式3:CPOL=1,CPHA=1
    ECSPI3->CONFIGREG |= (1 << 20) | (1 << 4) | (1 << 0);
}
  • 寄存器清零:裸机开发中外设初始化的通用规范,先清零寄存器,再按需配置,避免芯片默认值导致的未知问题。
  • 时钟频率计算 :i.MX6ULL 的 ECSPI 根时钟固定为 60MHz,最终 SCLK 频率计算公式为:SCLK=(PRE_DIVIDER+1)×2POST_DIVIDER60MHz本文配置PRE_DIVIDER=14POST_DIVIDER=2,最终 SCLK 频率 = 60/(15×4)=1MHz,属于 SPI 通用安全频率,可根据从设备手册调整分频系数修改时钟。
  • 模式配置CONFIGREG的配置最终实现 SPI 模式 3,需和从设备的时序要求严格匹配,若从设备使用模式 0,只需将对应位清零即可。

4.3 全双工读写函数 spi_write_read ()

该函数是 SPI 驱动的核心,实现单字节的全双工收发,代码量极少但完全覆盖了 SPI 传输的完整流程,逐行解析如下:

c

运行

复制代码
unsigned int spi_write_read(unsigned int data)
{
    // 清除通道选择位,固定使用ECSPI3的通道0
    ECSPI3->CONREG &= ~(3 << 18); 
    // 等待TXFIFO为空,确保上一次传输完成,避免FIFO溢出
    while ((ECSPI3->STATREG & (1 << 0)) == 0);
    // 写入待发送数据到TXFIFO,自动启动SPI传输
    ECSPI3->TXDATA = data;
    // 等待RXFIFO就绪,确保接收数据完成
    while ((ECSPI3->STATREG & (1 << 3)) == 0);
    // 读取接收数据,同时清除RR标志位
    unsigned int ret = ECSPI3->RXDATA;
    // 返回接收到的数据
    return ret;
}
  1. 通道选择CONREG的 bit19-18 是通道选择位,这里清零后固定使用通道 0,和初始化中的通道配置保持一致,避免通道选择错误。
  2. 发送前等待 :循环等待TE位置 1,确保发送 FIFO 为空,上一次的数据已经完全发送,再写入新数据,防止 FIFO 溢出导致的数据丢失。
  3. 启动传输 :向TXDATA写入数据后,由于初始化中SMC=1,ECSPI 会自动启动 SPI 传输,在 SCLK 的驱动下完成数据的移位收发。
  4. 接收等待 :循环等待RR位置 1,确保接收 FIFO 中有有效数据。这里要注意,SPI 全双工特性决定了发送完成的同时,接收也必然完成,等待接收就绪就是等待整个传输流程结束。
  5. 数据读取 :读取RXDATA寄存器获取接收数据,读操作会自动清除RR标志位,释放 RXFIFO 空间,为下一次传输做准备。

五、驱动使用示例

以 SPI 接口的 ADXL345 三轴加速度传感器为例,演示上述驱动的实际使用方法,该传感器使用 SPI 模式 3,和我们的驱动配置完全匹配。

5.1 片选控制宏定义

首先封装片选的控制宏,SPI 通信的核心规则是:通信前拉低 CS,通信全程保持 CS 低电平,通信结束后拉高 CS

c

运行

复制代码
// 拉低片选,选中ADXL345
#define ADXL345_CS_SELECT()   GPIO1->DR &= ~(1 << 20)
// 拉高片选,取消选中
#define ADXL345_CS_DESELECT() GPIO1->DR |= (1 << 20)

5.2 寄存器读写函数

ADXL345 的 SPI 读写规则:

  • 写寄存器:先发送寄存器地址(最高位为 0),再发送待写入的数据;
  • 读寄存器:先发送寄存器地址(最高位为 1),再发送空数据(0xFF),读取返回的寄存器值。

c

运行

复制代码
// ADXL345写寄存器函数
void adxl345_write_reg(unsigned char addr, unsigned char data)
{
    ADXL345_CS_SELECT();  // 通信前拉低片选
    spi_write_read(addr & 0x7F); // 发送写命令,最高位清0
    spi_write_read(data);        // 发送待写入数据
    ADXL345_CS_DESELECT();// 通信结束拉高片选
}

// ADXL345读寄存器函数
unsigned char adxl345_read_reg(unsigned char addr)
{
    unsigned char reg_val;
    ADXL345_CS_SELECT();
    spi_write_read(addr | 0x80); // 发送读命令,最高位置1
    reg_val = spi_write_read(0xFF); // 发送空数据,读取寄存器值
    ADXL345_CS_DESELECT();
    return reg_val;
}

5.3 设备 ID 读取验证

ADXL345 的设备 ID 寄存器地址为 0x00,固定值为 0xE5,可通过读取该寄存器验证 SPI 驱动是否正常工作:

c

运行

复制代码
#include "stdio.h"

int main(void)
{
    // 必须先初始化系统时钟,打开所有外设时钟
    init_clock();
    // 初始化SPI驱动
    spi_init();
    
    // 读取ADXL345设备ID
    unsigned char dev_id = adxl345_read_reg(0x00);
    printf("ADXL345 Device ID: 0x%X\r\n", dev_id);
    
    while(1)
    {
        // 后续业务逻辑
    }
    return 0;
}

若串口打印出ADXL345 Device ID: 0xE5,说明 SPI 驱动工作完全正常。

六、开发中的常见坑与注意事项

  1. 外设时钟必须提前使能i.MX6ULL 的所有外设都需要 CCM 时钟控制器使能后才能工作,若只配置 ECSPI 寄存器而不使能时钟,所有寄存器操作都会无效。裸机开发中建议在 main 函数开头先初始化 CCM,打开全部外设时钟。

  2. SPI 模式必须与从设备严格匹配CPOL 和 CPHA 的配置错误是 SPI 通信失败的最常见原因,必须严格按照从设备数据手册的时序要求配置,哪怕只差一个参数,也会出现采样数据全 0 或全 0xFF 的问题。

  3. 片选时序的严格控制多字节读写过程中,CS 引脚必须全程保持低电平,不能中途拉高;每次完整的读写操作结束后,必须拉高 CS,让从机复位传输状态,否则从机会出现数据错位。

  4. 时钟频率不能超过从设备上限不同 SPI 从设备的最高时钟频率差异极大,比如 SPI Flash 最高可达 100MHz,而部分传感器仅支持 10MHz 以下的时钟。若通信出现偶发错误,可先降低 SPI 时钟频率验证。

  5. 数据位宽的匹配 初始化中BURST_LENGTH配置为 7,对应 8bit 数据位宽,这是绝大多数 SPI 设备的标准配置。若使用 16bit 位宽的设备,需将该值改为 15(16bit),否则会出现数据移位错误。

七、总结

本文从零实现了 i.MX6ULL ECSPI3 的主机模式裸机驱动,基于 SPI 全双工特性设计了通用的读写接口,完整覆盖了引脚配置、寄存器时序、实际使用的全流程。

相关推荐
进击的小头7 小时前
第7篇:嵌入式芯片运算核心:ALU_MAC_FPU的工作原理与性能差异
单片机·嵌入式硬件
振南的单片机世界7 小时前
RS485组网:一问一答,多个从机不打架
单片机·嵌入式硬件
开源盛世!!7 小时前
4.9-4.11
单片机·嵌入式硬件
路过羊圈的狼8 小时前
STM32使用SFUD (Serial Flash Universal Driver) 串行 Flash 通用驱动库驱动W25Q128
stm32·单片机·嵌入式硬件
LCG元8 小时前
多MCU通信:STM32F1通过I2C/SPI实现数据同步与控制
stm32·单片机·嵌入式硬件
史蒂芬_丁8 小时前
EPWM Global Load
单片机·嵌入式硬件
碎像8 小时前
单片机-数码管显示
单片机·嵌入式硬件
智者知已应修善业8 小时前
【CD4022八进制计数器脉冲分配器】2023-5-31
驱动开发·经验分享·笔记·硬件架构·硬件工程
senijusene8 小时前
IMX6ULL Linux 驱动开发流程:从环境搭建到系统启动与内核编译
linux·运维·驱动开发