本篇是 ARM 嵌入式裸机学习第十二天的打卡笔记,基于 i.MX6ULL(MCIMX6Y2)平台开发。今日学习内容分为两大核心模块:
- 上午:基于 I2C 接口的 GT9147 电容触摸屏驱动开发,覆盖 GPIO 中断配置、芯片复位、坐标读取、中断处理全流程
- 下午:ECSPI(增强型 SPI)裸机驱动实现,包含引脚复用、控制器寄存器配置、全双工收发函数封装 同时结合时钟树底层逻辑,系统梳理了 UART、I2C、SPI 三大经典串行接口的核心区别、时序特性与应用场景,夯实嵌入式通信协议基础。
文章目录
- [一、三大串行通信协议:UART / I2C / SPI 全维度对比](#一、三大串行通信协议:UART / I2C / SPI 全维度对比)
- [1. 核心共同点](#1. 核心共同点)
- [2. 核心差异对比表](#2. 核心差异对比表)
- [3. 典型应用场景](#3. 典型应用场景)
- [4. 核心时序特性](#4. 核心时序特性)
- [二、ECSPI3 裸机驱动开发(i.MX6ULL 平台)](#二、ECSPI3 裸机驱动开发(i.MX6ULL 平台))
- [1. 驱动整体思路](#1. 驱动整体思路)
- [2. 代码逐段解析](#2. 代码逐段解析)
- [3. 关键原理说明](#3. 关键原理说明)
- [三、GT9147 电容触摸屏驱动(I2C 接口)](#三、GT9147 电容触摸屏驱动(I2C 接口))
- [1. 驱动架构](#1. 驱动架构)
- [2. 核心代码解析](#2. 核心代码解析)
- [3. 关键设计要点](#3. 关键设计要点)
- 四、底层支撑:时钟树与外设配置逻辑
- [1. 外设时钟配置通用四步法](#1. 外设时钟配置通用四步法)
- [2. 核心原则](#2. 核心原则)
- 五、学习总结与面试考点梳理
- [1. 今日核心收获](#1. 今日核心收获)
- [2. 高频面试考点](#2. 高频面试考点)
一、三大串行通信协议:UART / I2C / SPI 全维度对比
UART、I2C、SPI 是嵌入式开发中最核心的三类板级串行通信接口,几乎所有 MCU/SoC 都标配这三类外设,也是面试的必考点。
1. 核心共同点
- 均为串行通信:数据按位依次传输,相比并行通信引脚少、布线简单,专为芯片间短距离通信设计
- 均为数字电平通信:直接使用 TTL/CMOS 电平(3.3V/5V),板级通信无需额外电平转换芯片
- 都属于中低速外设接口:主要面向传感器、存储、显示等外设的数据交互,区别于以太网、PCIe 等高速总线
2. 核心差异对比表
| 对比维度 | UART | I2C | SPI |
|---|---|---|---|
| 通信类型 | 异步串行 | 同步串行 | 同步串行 |
| 信号线数量 | 2 根(TX、RX),硬件流控共 4 根 | 2 根(SDA、SCL) | 标准 4 根(SCLK、MOSI、MISO、CS) |
| 架构 | 对等通信,不分主从 | 一主多从,支持多主机总线仲裁 | 一主多从,仅支持单主机 |
| 通信方式 | 全双工 | 半双工(数据线分时复用) | 全双工(收发同时完成) |
| 设备寻址 | 无地址,仅点对点通信 | 7 位 / 10 位从机地址寻址 | 独立 CS 片选信号逐个选中 |
| 典型速率 | 9600bps ~ 3Mbps,常用 115200bps | 标准 100kbps / 快速 400kbps / 高速 3.4Mbps | 数 MHz ~ 几十 MHz,QSPI 可达百 MHz |
| 同步方式 | 双方约定波特率,起始位 + 停止位帧同步 | 主机产生 SCL 时钟同步 | 主机产生 SCLK 时钟同步 |
| 校验应答 | 奇偶校验、停止位校验 | 每字节强制 ACK 应答 | 无内置应答与校验机制 |
| 硬件特性 | 推挽输出,无需上拉 | 开漏输出,必须接上拉电阻 | 推挽输出,无需上拉 |
| 多设备扩展性 | 差,仅支持点对点 | 好,单总线最多挂载 127 个设备 | 一般,每加一个设备多一根 CS 线 |
3. 典型应用场景
- UART:设备调试串口、蓝牙 / WiFi/4G 模组通信、工业 RS232/RS485 总线、双机低速数据交互
- I2C:低速传感器(温湿度、加速度计)、EEPROM、RTC 时钟、OLED 小屏、电容触摸芯片、IO 扩展芯片(适合设备多、数据量小、追求引脚极简的场景)
- SPI:SPI Flash、SD 卡、TFT 液晶屏、高速 ADC/DAC、以太网控制器、高速摄像头(适合速率要求高、数据量大的场景)
4. 核心时序特性
- UART:异步帧协议,依靠起始位(低电平)+ 停止位(高电平)实现帧同步,数据低位在前,接收端按波特率在每位中间采样
- I2C:同步总线,SCL 高电平时 SDA 必须稳定,起始信号为 SCL 高时 SDA 下降沿,停止信号为 SCL 高时 SDA 上升沿,每字节后跟随 ACK 应答
- SPI:同步主从协议,依靠 CPOL(时钟极性)和 CPHA(时钟相位)组合为 4 种工作模式,CS 拉低启动传输,一个时钟周期完成 1 位收发,收发同步进行
二、ECSPI3 裸机驱动开发(i.MX6ULL 平台)
i.MX6ULL 的 ECSPI 是增强型 SPI 控制器,支持主从模式、多通道、可配置时钟分频与工作模式。本次实现 ECSPI3 主机模式驱动,采用 GPIO 模拟片选信号。
1. 驱动整体思路
SPI 驱动开发分为四步:引脚复用配置 → 电气特性配置 → 控制器寄存器配置 → 收发函数封装,底层依赖 CCM 时钟树提供工作时钟。
2. 代码逐段解析
#include "spi.h"
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
void ecspi3_init(void)
{
//========== 1. 引脚功能复用 ==========
// 将UART2相关引脚复用为ECSPI3功能:SCLK、MOSI、MISO
IOMUXC_SetPinMux(IOMUXC_UART2_RX_DATA_ECSPI3_SCLK, 0);
IOMUXC_SetPinMux(IOMUXC_UART2_CTS_B_ECSPI3_MOSI, 0);
IOMUXC_SetPinMux(IOMUXC_UART2_RTS_B_ECSPI3_MISO, 0);
// 片选CS用GPIO1_IO20软件模拟,不使用硬件CS
IOMUXC_SetPinMux(IOMUXC_UART2_TX_DATA_GPIO1_IO20, 0);
//========== 2. 引脚电气特性配置 ==========
// 配置驱动能力、速率、上下拉,0x10B1为常用推挽输出配置
IOMUXC_SetPinConfig(IOMUXC_UART2_RX_DATA_ECSPI3_SCLK, 0x10B1);
IOMUXC_SetPinConfig(IOMUXC_UART2_CTS_B_ECSPI3_MOSI, 0x10B1);
IOMUXC_SetPinConfig(IOMUXC_UART2_RTS_B_ECSPI3_MISO, 0x10B1);
IOMUXC_SetPinConfig(IOMUXC_UART2_TX_DATA_GPIO1_IO20, 0x10B1);
//========== 3. 软件片选初始化 ==========
// 设置GPIO1_IO20为输出方向,空闲时输出高电平(SPI片选通常低电平有效)
GPIO1->GDIR |= (1 << 20);
GPIO1->DR |= (1 << 20);
//========== 4. ECSPI控制器寄存器配置 ==========
// 先清零控制寄存器
ECSPI3->CONREG = 0;
// 配置:通道选择、时钟分频、突发长度、主模式
// (7<<20)通道选择、(14<<12)后分频、(2<<8)突发长度、(1<<4)接收模式、(1<<3)主机模式
ECSPI3->CONREG |= (7 << 20) | (14 << 12) | (2 << 8) | (1 << 4) | (1 << 3);
// 配置寄存器:设置时钟极性、相位等工作模式
ECSPI3->CONFIGREG = 0;
ECSPI3->CONFIGREG |= (1 << 20) | (1 << 4) | (1 << 0);
// 最后使能ECSPI控制器
ECSPI3->CONREG |= (1 << 0);
}
//========== 5. 全双工收发函数 ==========
// SPI特性:发送1字节的同时必然接收1字节
unsigned int ecspi3_write_and_read(unsigned int data)
{
unsigned int ret = 0;
// 等待发送FIFO为空,才可写入数据
while((ECSPI3->STATREG & (1 << 0)) == 0);
ECSPI3->TXDATA = data;
// 等待接收FIFO有数据,发送完成即接收完成
while((ECSPI3->STATREG & (1 << 3)) == 0);
ret = ECSPI3->RXDATA;
return ret;
}
3. 关键原理说明
- 软件片选:使用 GPIO 模拟 CS 的优势是灵活,不受硬件通道限制,操作时拉低 CS、传输完成拉高即可
- 全双工本质:SPI 主从两端各有一个移位寄存器,时钟驱动下逐位交换数据,因此写操作和读操作是同时完成的
- 寄存器配置顺序:先配置控制参数,最后使能外设,避免配置过程中外设工作在不稳定状态
三、GT9147 电容触摸屏驱动(I2C 接口)
GT9147 是汇顶科技的多点电容触控芯片,采用 I2C 接口与主机通信,通过中断引脚通知主机触摸事件,支持 5 点触控。
1. 驱动架构
驱动分为三层:
- 底层:I2C 控制器驱动(提供初始化、数据传输接口)
- 芯片层:GT9147 寄存器读写封装、初始化、复位
- 逻辑层:GPIO 中断处理、坐标解析、状态上报
2. 核心代码解析
#include "gt9147.h"
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "i2c.h"
#include "gpt.h"
#include "stdio.h"
#include "interrupt.h"
struct GT9147_dev _gt9147; // 触摸设备结构体
// 5个触摸点的坐标寄存器地址
unsigned short pointer_add[5] = {0x8150, 0x8158, 0x8160, 0x8168, 0x8170};
//========== 1. GT9147寄存器读封装 ==========
void gt9147_read(unsigned short reg_addr, unsigned char *data, int data_len)
{
struct I2C_Msg _gt9147 = {
.dev_addr = 0x14, // GT9147从机地址
.reg_addr = reg_addr,// 寄存器地址
.reg_len = 2, // 寄存器地址长度2字节
.data = data,
.data_len = data_len,
.dir = I2C_read
};
transfer(I2C2, &_gt9147); // 调用I2C通用传输函数
}
//========== 2. GT9147寄存器写封装 ==========
void gt9147_write(unsigned short reg_addr, unsigned char *data, int data_len)
{
struct I2C_Msg _gt9147 = {
.dev_addr = 0x14,
.reg_addr = reg_addr,
.reg_len = 2,
.data = data,
.data_len = data_len,
.dir = I2C_write
};
transfer(I2C2, &_gt9147);
}
//========== 3. 触摸坐标打印 ==========
void show_pointer(void)
{
if ((_gt9147.num != 0) && (_gt9147.vail == 1))
{
printf("================pointer (%d)=================\n", _gt9147.num);
for (int i = 0; i < _gt9147.num; i++)
{
printf("P%d (%05d, %05d)\n", i, _gt9147.x[i], _gt9147.y[i]);
}
_gt9147.vail = 0;
printf("====================end=====================\n");
}
}
//========== 4. 触摸中断处理函数 ==========
void touch_irq_hangdler(void)
{
if ((GPIO1->ISR & (1 << 9)) != 0) // 判断是否是引脚9触发的中断
{
unsigned char buffer[64] = {0};
gt9147_read(0x814E, buffer, 1); // 读取状态寄存器,获取触摸点数
unsigned char num = buffer[0] & 0x0F;
if (_gt9147.num != num) // 触摸点数变化时更新坐标
{
_gt9147.num = num;
for (int i = 0; i < num; i++)
{
gt9147_read(pointer_add[i], buffer, 4); // 读取每个点的4字节坐标
// 坐标解析:低字节在前,高字节在后,组合为16位坐标
_gt9147.x[i] = (buffer[1] << 8) | (buffer[0] << 0);
_gt9147.y[i] = (buffer[3] << 8) | (buffer[2] << 0);
}
_gt9147.vail = 1;
show_pointer();
}
// 清除芯片中断标志
buffer[0] = 0;
gt9147_write(0x814E, buffer, 1);
// 清除GPIO中断标志位
GPIO1->ISR |= (1 << 9);
}
}
//========== 5. GT9147初始化 ==========
void gt9147_init(void)
{
i2c_init(I2C2); // 初始化I2C2控制器
// 引脚复用配置:中断引脚CT_INT、复位引脚CT_RST
IOMUXC_SetPinMux(IOMUXC_GPIO1_IO09_GPIO1_IO09, 0); // CT_INT
IOMUXC_SetPinMux(IOMUXC_SNVS_SNVS_TAMPER9_GPIO5_IO09, 0); // CT_RST
// 引脚电气配置
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO09_GPIO1_IO09, 0x10B0);
IOMUXC_SetPinConfig(IOMUXC_SNVS_SNVS_TAMPER9_GPIO5_IO09, 0x10B0);
// 复位引脚配置为输出,执行硬件复位
GPIO1->GDIR |= (1 << 9);
GPIO5->GDIR |= (1 << 9);
GPIO1->DR &= ~(1 << 9);
GPIO5->DR &= ~(1 << 9); // 拉低复位引脚
delay_ms(50);
GPIO5->DR |= (1 << 9); // 拉高复位引脚
delay_ms(100); // 等待芯片启动稳定
// 读取芯片ID,验证通信正常
unsigned char buffer[128] = {0};
gt9147_read(0x8140, buffer, 4);
printf("Gesture ID = %s\n", buffer);
// 读取中断触发方式配置
gt9147_read(0x8056, buffer, 1);
printf("Gesture Int way = %d\n", buffer[0] & 0x03);
//========== GPIO中断配置 ==========
// 1. 修改中断引脚为开漏输入模式
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO09_GPIO1_IO09, 0x0800);
// 2. 设置引脚为输入方向
GPIO1->GDIR &= ~(1 << 9);
// 3. 配置中断触发方式
GPIO1->ICR1 |= (3 << 18);
// 4. 解除中断屏蔽
GPIO1->IMR |= (1 << 9);
// 配置GIC中断控制器:设置优先级、注册处理函数、使能中断
GIC_SetPriority(GPIO1_Combined_0_15_IRQn, 0);
system_interrupt_register(GPIO1_Combined_0_15_IRQn, touch_irq_hangdler);
GIC_EnableIRQ(GPIO1_Combined_0_15_IRQn);
// 配置芯片进入正常工作模式
buffer[0] = 0;
gt9147_write(0x8040, buffer, 1);
}
3. 关键设计要点
- 中断驱动:采用中断方式而非轮询读取,大大降低 CPU 占用率,只有触摸事件发生时才进入中断读取坐标
- 硬件复位:通过复位引脚对芯片做硬复位,保证芯片上电后处于确定状态
- 中断处理原则:中断函数内只做数据读取与标志清除,耗时操作(如打印)尽量简化,保证快进快出
四、底层支撑:时钟树与外设配置逻辑
不管是 SPI 还是 I2C,所有外设工作的前提都是时钟,配置外设的本质就是操作 CCM 时钟控制器,这也是之前学习时钟树的核心意义。
1. 外设时钟配置通用四步法
- 定位终点 :在时钟树中找到目标外设对应的时钟根节点(如
ECSPI_CLK_ROOT、I2C_CLK_ROOT) - 回溯选路:从终点向左倒推,选择合适的 PLL 时钟源,根据目标频率计算各级分频系数
- 顺向配置:从上游 PLL 开始,逐级配置 PFD 分频、MUX 选择、外设分频器,必须从上到下配置,避免时钟毛刺
- 开门控:最后打开外设的时钟门控(LPCG),外设寄存器才能正常读写
2. 核心原则
- PLL 配置完成后必须等待锁定(LOCK),未稳定的时钟会导致外设通信异常
- 时钟切换必须保证上游时钟稳定后再操作下游 MUX
- 时钟门控是芯片低功耗的核心,不用的外设一定要关闭时钟
五、学习总结与面试考点梳理
1. 今日核心收获
- 建立了**"时钟 + 引脚复用 + 寄存器配置 + 业务逻辑"**的裸机外设驱动通用开发框架
- 掌握了 SPI 全双工通信的本质与驱动编写方法,理解了收发同步的硬件原理
- 掌握了 I2C 接口外设的驱动套路:芯片复位、寄存器读写、中断处理、数据解析
- 理清了三大串行协议的核心差异,能够根据业务场景合理选型
2. 高频面试考点
- SPI、I2C、UART 的区别(信号线、同步异步、速率、主从架构)
- SPI 的四种工作模式(CPOL 与 CPHA 的定义与组合)
- I2C 的起始信号、停止信号、ACK 应答的时序
- 为什么 SPI 的传输速率比 I2C 高
- SoC 时钟树的作用,外设时钟的配置顺序
- 中断处理函数的编写注意事项(快进快出、不可阻塞、清除标志位)
第十二天打卡完成,裸机外设驱动部分逐步推进,下一步将继续学习更多外设驱动与底层移植知识。