【嵌入式 ARM 学习第十二天】SPI 裸机驱动 + I2C 触摸屏驱动 + 三大串行协议深度对比

本篇是 ARM 嵌入式裸机学习第十二天的打卡笔记,基于 i.MX6ULL(MCIMX6Y2)平台开发。今日学习内容分为两大核心模块:

  1. 上午:基于 I2C 接口的 GT9147 电容触摸屏驱动开发,覆盖 GPIO 中断配置、芯片复位、坐标读取、中断处理全流程
  2. 下午: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. 驱动架构

驱动分为三层:

  1. 底层:I2C 控制器驱动(提供初始化、数据传输接口)
  2. 芯片层:GT9147 寄存器读写封装、初始化、复位
  3. 逻辑层: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. 外设时钟配置通用四步法

  1. 定位终点 :在时钟树中找到目标外设对应的时钟根节点(如ECSPI_CLK_ROOT、I2C_CLK_ROOT)
  2. 回溯选路:从终点向左倒推,选择合适的 PLL 时钟源,根据目标频率计算各级分频系数
  3. 顺向配置:从上游 PLL 开始,逐级配置 PFD 分频、MUX 选择、外设分频器,必须从上到下配置,避免时钟毛刺
  4. 开门控:最后打开外设的时钟门控(LPCG),外设寄存器才能正常读写

2. 核心原则

  • PLL 配置完成后必须等待锁定(LOCK),未稳定的时钟会导致外设通信异常
  • 时钟切换必须保证上游时钟稳定后再操作下游 MUX
  • 时钟门控是芯片低功耗的核心,不用的外设一定要关闭时钟

五、学习总结与面试考点梳理

1. 今日核心收获

  1. 建立了**"时钟 + 引脚复用 + 寄存器配置 + 业务逻辑"**的裸机外设驱动通用开发框架
  2. 掌握了 SPI 全双工通信的本质与驱动编写方法,理解了收发同步的硬件原理
  3. 掌握了 I2C 接口外设的驱动套路:芯片复位、寄存器读写、中断处理、数据解析
  4. 理清了三大串行协议的核心差异,能够根据业务场景合理选型

2. 高频面试考点

  1. SPI、I2C、UART 的区别(信号线、同步异步、速率、主从架构)
  2. SPI 的四种工作模式(CPOL 与 CPHA 的定义与组合)
  3. I2C 的起始信号、停止信号、ACK 应答的时序
  4. 为什么 SPI 的传输速率比 I2C 高
  5. SoC 时钟树的作用,外设时钟的配置顺序
  6. 中断处理函数的编写注意事项(快进快出、不可阻塞、清除标志位)

第十二天打卡完成,裸机外设驱动部分逐步推进,下一步将继续学习更多外设驱动与底层移植知识。

相关推荐
墨心@2 小时前
第 4 章《工具》学习总结
学习·自然语言处理·prompt·agent·harness
AOI小白新手上路2 小时前
4-1_地址空间_RISC与CISC_学习笔记
笔记·学习
Vcaker2 小时前
Linux学习36-k8s集群升级及kube-vip高可用
linux·运维·学习
ARM&开发(Haidong)2 小时前
ARM DMA硬件和驱动原理
arm开发
probex_2 小时前
学习笔记:知识图谱落地实践
笔记·学习·知识图谱
AOI小白新手上路2 小时前
4-3_ARM汇编_学习笔记
汇编·arm开发·学习
kkkkkkkkkk_Z2 小时前
新手自学嵌入式 | 学习日记:ARM架构初探与基础概念梳理
arm开发·学习·架构
SKH.3 小时前
ARM-lcd屏幕点亮
arm开发
鬓戈3 小时前
开源中文输入法技术调研与自建方案
学习·开源