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 总线的核心特性这是驱动代码设计的基础四线制硬件架构标准 SPI 采用 4 根信号线通信主从机的连接关系固定SCLK串行时钟线主机输出用于同步收发数据时钟频率由主机配置MOSI主发从收线主机发送数据、从机接收数据MISO主收从发线从机发送数据、主机接收数据CS片选线主机输出有效电平与外设有关同一时刻只能拉低一个从机的 CS 引脚确保总线只有一个从机处于工作状态。全双工通信特性SPI 是全双工总线发送和接收过程完全同步主机在 SCLK 的驱动下通过 MOSI 向从机移位发送 1 个 bit 的同时会通过 MISO 从从机移位接收 1 个 bit。也就是说每完成 8 个时钟周期1 字节的发送必然同时完成 1 字节的接收这也是 SPI 驱动通常只用一个读写函数就能完成收发的核心原因。4 种 SPI 时序模式SPI 的时序由两个核心参数决定必须和从机设备手册严格匹配否则会出现采样错误CPOL时钟极性决定总线空闲时 SCLK 的电平状态。CPOL0空闲时 SCLK 为低电平CPOL1空闲时 SCLK 为高电平。CPHA时钟相位决定数据采样的时钟沿。CPHA0在 SCLK 的第一个跳变沿采样数据CPHA1在 SCLK 的第二个跳变沿采样数据。两个参数组合出 4 种 SPI 模式本文驱动默认配置为模式 3CPOL1CPHA1适配 ADXL345 等绝大多数常用 SPI 传感器。二、i.MX6ULL ECSPI 外设核心寄存器i.MX6ULL 的 ECSPI 外设配置完全基于寄存器实现驱动代码的本质就是对寄存器的正确配置。本文只讲解代码中用到的核心寄存器完整寄存器说明可参考《i.MX6ULL 参考手册》第 20 章。2.1 控制寄存器CONREG该寄存器是 ECSPI 的核心配置寄存器32 位有效决定了外设使能、主从模式、时钟分频、突发长度等核心参数代码中重点配置的位段如下表格位段位宽代码配置功能说明BURST_LENGTH31-20720突发传输长度值为 N 表示一次传输 N1 个 bit配置为 7 即一次传输 8bit1 字节PRE_DIVIDER15-121412预分频器值为 N 对应 N1 分频配置为 14 即 15 分频POST_DIVIDER11-828后分频器值为 N 对应 2^N 分频配置为 2 即 4 分频CHANNEL_MODE7-414通道主从模式bit0 对应通道 0置 1 表示通道 0 配置为主机模式SMC313传输启动模式置 1 表示向 TXFIFO 写入数据时自动启动 SPI 传输EN010ECSPI 外设使能位置 1 开启外设置 0 关闭外设2.2 配置寄存器CONFIGREG该寄存器主要用于配置 SPI 时序模式、片选极性等参数和从设备时序匹配的核心配置都在这个寄存器中表格位段位宽代码配置功能说明SCLK_POL7-414时钟极性配置bit0 对应通道 0置 1 表示 CPOL1空闲时 SCLK 为高电平SCLK_PHA3-010时钟相位配置bit0 对应通道 0置 1 表示 CPHA1第二个时钟沿采样数据2.3 状态寄存器STATREG该寄存器是只读寄存器用于获取 ECSPI 的工作状态驱动中通过该寄存器判断收发是否完成重点用到两个位TE(bit0)TXFIFO 空标志位置 1 表示发送 FIFO 为空可写入新的发送数据RR(bit3)RXFIFO 就绪标志位置 1 表示接收 FIFO 中有有效数据可读取。2.4 数据寄存器TXDATA发送数据寄存器向该寄存器写入数据会将数据压入 TXFIFO在 SMC1 的模式下会自动启动 SPI 传输RXDATA接收数据寄存器从该寄存器读取数据会取出 RXFIFO 中的有效数据同时自动清除 RR 标志位。三、硬件设计与工程结构3.1 硬件引脚复用本文使用 i.MX6ULL 的 ECSPI3 外设开发板上该外设的引脚与 UART2 引脚复用具体映射关系如下同时采用软件片选的方式使用 GPIO1_IO20 作为 CS 引脚比硬件片选更灵活表格功能芯片引脚复用配置宏定义ECSPI3_MOSIUART2_CTS_BIOMUXC_UART2_CTS_B_ECSPI3_MOSIECSPI3_MISOUART2_RTS_BIOMUXC_UART2_RTS_B_ECSPI3_MISOECSPI3_SCLKUART2_RX_DATAIOMUXC_UART2_RX_DATA_ECSPI3_SCLKSPI_CS (软件)UART2_TX_DATAIOMUXC_UART2_TX_DATA_GPIO1_IO203.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模式3CPOL1CPHA1 ECSPI3-CONFIGREG | (1 20) | (1 4) | (1 0); }寄存器清零裸机开发中外设初始化的通用规范先清零寄存器再按需配置避免芯片默认值导致的未知问题。时钟频率计算i.MX6ULL 的 ECSPI 根时钟固定为 60MHz最终 SCLK 频率计算公式为SCLK(PRE_DIVIDER1)×2POST_DIVIDER60MHz本文配置PRE_DIVIDER14、POST_DIVIDER2最终 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; }通道选择CONREG的 bit19-18 是通道选择位这里清零后固定使用通道 0和初始化中的通道配置保持一致避免通道选择错误。发送前等待循环等待TE位置 1确保发送 FIFO 为空上一次的数据已经完全发送再写入新数据防止 FIFO 溢出导致的数据丢失。启动传输向TXDATA写入数据后由于初始化中SMC1ECSPI 会自动启动 SPI 传输在 SCLK 的驱动下完成数据的移位收发。接收等待循环等待RR位置 1确保接收 FIFO 中有有效数据。这里要注意SPI 全双工特性决定了发送完成的同时接收也必然完成等待接收就绪就是等待整个传输流程结束。数据读取读取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 驱动工作完全正常。六、开发中的常见坑与注意事项外设时钟必须提前使能i.MX6ULL 的所有外设都需要 CCM 时钟控制器使能后才能工作若只配置 ECSPI 寄存器而不使能时钟所有寄存器操作都会无效。裸机开发中建议在 main 函数开头先初始化 CCM打开全部外设时钟。SPI 模式必须与从设备严格匹配CPOL 和 CPHA 的配置错误是 SPI 通信失败的最常见原因必须严格按照从设备数据手册的时序要求配置哪怕只差一个参数也会出现采样数据全 0 或全 0xFF 的问题。片选时序的严格控制多字节读写过程中CS 引脚必须全程保持低电平不能中途拉高每次完整的读写操作结束后必须拉高 CS让从机复位传输状态否则从机会出现数据错位。时钟频率不能超过从设备上限不同 SPI 从设备的最高时钟频率差异极大比如 SPI Flash 最高可达 100MHz而部分传感器仅支持 10MHz 以下的时钟。若通信出现偶发错误可先降低 SPI 时钟频率验证。数据位宽的匹配初始化中BURST_LENGTH配置为 7对应 8bit 数据位宽这是绝大多数 SPI 设备的标准配置。若使用 16bit 位宽的设备需将该值改为 1516bit否则会出现数据移位错误。七、总结本文从零实现了 i.MX6ULL ECSPI3 的主机模式裸机驱动基于 SPI 全双工特性设计了通用的读写接口完整覆盖了引脚配置、寄存器时序、实际使用的全流程。