news 2026/4/18 10:02:06

ARM之多点触控与SPI

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ARM之多点触控与SPI

一、SPI 总线基础与 IMX6ULL ECSPI 控制器

SPI(Serial Peripheral Interface,串行外设接口)由 Motorola 率先定义,是一种高速、全双工、同步的串行通信总线,仅需 4 根线即可实现通信,广泛应用于 EEPROM、FLASH、传感器、AD 转换器等外设,因引脚少、布局方便、协议简单的特性被大量芯片集成。

1.1 标准 SPI 总线引脚定义

标准 SPI 为四线制全双工通信,核心引脚功能如下,若仅需单工通信可省略 MOSI 或 MISO 实现三线 SPI(如仅需写数据的 OLED):

  • SCLK:时钟信号线,由主机提供,所有设备通过时钟同步通信;
  • MOSI:主出从入,主机的数据发送线;
  • MISO:主入从出,主机的数据接收线;
  • CS:片选信号,SPI 无从机地址,通过 CS 选择当前通信外设,低电平有效,同一时刻总线仅一个外设被选中。

1.2 SPI 通信时序核心:CPOL 与 CPHA

SPI 的通信时序由时钟极性(CPOL)和时钟相位(CPHA)决定,二者均针对 SCLK 信号线,组合后形成 4 种通信模式,具体使用哪种由外设厂家规定,开发前需熟读外设手册。

  • CPOL:定义 SCLK 空闲时的电平,CPOL=0 为低电平,CPOL=1 为高电平;
  • CPHA:定义数据采样的时钟沿,CPHA=0 在时钟第一个跳变沿采样,CPHA=1 在第二个跳变沿采样。

通用规则:无论哪种模式,SPI 均采用 MSB 先行(最高位先传输);全双工通信中,主机发送 1 字节的同时会接收 1 字节,因此 “发送即接收” 是 SPI 的核心通信逻辑。

1.3 IMX6ULL 的 ECSPI 控制器

IMX6ULL 自带的 SPI 外设为 ECSPI(Enhanced Configurable Serial Peripheral Interface),本质是增强型 SPI,核心优势为配备 64*32 的接收 FIFO(RXFIFO)和发送 FIFO(TXFIFO),提升大数据传输效率,其核心特性如下:

关键逻辑:SPI 全双工特性决定 “发送即接收”,向 TXDATA 写入 1 字节的同时,RXDATA 会接收到外设返回的 1 字节(即使是 Dummy 数据 0xFF)。

  • 共 4 个 ECSPI 控制器,每个支持 4 个硬件片选信号,即一个 ECSPI 可驱动 4 个 SPI 外设;
  • 支持主 / 从模式,开发中通常使用主模式;
  • ECSPI 时钟源为 pll3_sw_clk=480MHz,经 8 分频静态分频后为 60MHz,再通过寄存器配置二级分频得到最终 SCLK 频率;
  • 核心操作通过寄存器完成,包含控制、配置、周期、状态、数据寄存器五大类,是 ECSPI 初始化和数据收发的关键。
    ECSPIx_CONREG(控制寄存器,x=1~4)

    该寄存器是 ECSPI 初始化的核心,用于配置传输长度、通道、分频、主从模式等核心参数:

    位段名称功能说明实验配置(ADXL345)
    bit31:20BURST_LENGTH突发传输数据长度,0X000~0XFFF 对应 1~2^12 bit,实验设为 7(8bit/1 字节)7(0x007)
    bit19:18CHANNEL_SELECTSPI 通道选择(0~3 对应 SS0~SS3),实验用 ECSPI3 通道 00(0x0)
    bit17:16DRCTLSPI_RDY 信号控制,0 = 不关心,1 = 边沿触发,2 = 电平触发0
    bit15:12PRE_DIVIDER预分频(1~16 分频),基础时钟 60MHz,实验设为 0(1 分频)0(0x0)
    bit11:8POST_DIVIDER二级分频,分频值 = 2^POST_DIVIDER,实验设为 3(8 分频,60/8=7.5MHz)3(0x3)
    bit7:4CHANNEL_MODE通道主 / 从模式(0 = 从,1 = 主),仅对应位生效,实验设通道 0 为主模式0x01
    bit3SMC开始模式控制,0 = 通过 XCH 启动传输,1 = 写 TXFIFO 即启动1
    bit2XCH传输启动位(仅 SMC=0 时生效)0
    bit1HTHT 模式使能(I.MX6ULL 不支持)0
    bit0ENSPI 使能位,1 = 使能,0 = 关闭1
    ECSPIx_CONFIGREG(配置寄存器)

    用于配置 SPI 时序(CPOL/CPHA)、片选极性、空闲电平:

    位段名称功能说明实验配置(ADXL345)
    bit28:24HT_LENGTHHT 模式消息长度(I.MX6ULL 不支持)0
    bit23:20SCLK_CTLSCLK 空闲电平(0 = 低,1 = 高),对应通道 3~00x1(通道 0 高电平)
    bit19:16DATA_CTLDATA 空闲电平(0 = 高,1 = 低),对应通道 3~00x0
    bit15:12SS_POL片选极性(0 = 低有效,1 = 高有效),对应通道 3~00x0(低电平有效)
    bit7:4SCLK_POLCPOL(0 = 空闲低,1 = 空闲高),对应通道 3~0,ADXL345 需设为 10x1(通道 0=1)
    bit3:0SCLK_PHACPHA(0 = 第一个沿采样,1 = 第二个沿采样),对应通道 3~0,ADXL345 需设为 10x1(通道 0=1)

    关键:ADXL345 采用 SPI 模式 3(CPOL=1、CPHA=1),需通过 SCLK_POL/SCLK_PHA 配置通道 0 为 1。

    ECSPIx_PERIODREG(周期寄存器)

    用于配置片选延时、时钟源、采样周期:

    位段名称功能说明实验配置
    bit21:16CSD_CTL片选到第一个时钟的延时(0~63 个周期)0
    bit15CSRC时钟源选择(0=60MHz SPI CLK,1=32.768KHz 晶振)0
    bit14:0SAMPLE_PERIO采样周期(0~32767 个周期)0

    时钟源说明:ECSPI 基础时钟 60MHz 来自 pll3_sw_clk (480MHz) 经 8 分频,CSCDR2 寄存器的 ECSPI_CLK_SEL 选通 60MHz,ECSPI_CLK_PODF 设为 0(不分频)。

    ECSPIx_STATREG(状态寄存器)

    用于判断 FIFO 状态、传输进度,是数据收发的核心判断依据:

    名称功能说明
    bit7TC传输完成标志(0 = 传输中,1 = 完成)
    bit6RORXFIFO 溢出标志(0 = 无溢出,1 = 溢出)
    bit5RFRXFIFO 空标志(0 = 非空,1 = 空)
    bit4RDRRXFIFO 数据请求(0 = 数据≤阈值,1 = 数据>阈值)
    bit3RRRXFIFO 就绪(0 = 无数据,1 = 至少 1 个字数据)
    bit2TFTXFIFO 满标志(0 = 非满,1 = 满)
    bit1TDRTXFIFO 数据请求(0 = 数据>阈值,1 = 数据≤阈值)
    bit0TETXFIFO 空标志(0 = 至少 1 个字数据,1 = 空)

    核心用法:

    1. 发送数据前检查 TF 位,为 0 时向 TXDATA 写数据;
    2. 接收数据前检查 RR 位,为 1 时从 RXDATA 读数据;
    3. 传输完成后检查 TC 位,确认数据收发完成。
    ECSPIx_TXDATA/ECSPIx_RXDATA(数据寄存器)

    均为 32 位寄存器,是 SPI 数据收发的最终载体:

  • ECSPIx_TXDATA:写入需发送的数据(实验中仅用低 8 位,对应 8bit 传输);
  • ECSPIx_RXDATA:读取接收到的数据(同样仅取低 8 位)。

1.4 IMX6ULL ECSPI3 底层驱动实现(spi.c/spi.h)

补充核心的 SPI 通用驱动代码,这是 ADXL345 驱动的基础:

// spi.h #ifndef __SPI_H__ #define __SPI_H__ #include "stdint.h" // ECSPI3 初始化函数 void spi3_init(void); // ECSPI3 通道0 读写函数(SPI核心:发送1字节同时接收1字节) unsigned char spi3_ch0_write_and_read(unsigned char data); #endif // spi.c #include "spi.h" #include "MCIMX6Y2.h" #include "fsl_iomuxc.h" /** * @brief ECSPI3 引脚初始化(ECSPI3_SCLK/ECSPI3_MOSI/ECSPI3_MISO + GPIO1_IO20(CS)) */ static void spi3_pin_init(void) { // 1. 配置ECSPI3引脚复用 IOMUXC_SetPinMux(IOMUXC_ECSPI3_SCLK_ECSPI3_SCLK, 0); // SCLK IOMUXC_SetPinMux(IOMUXC_ECSPI3_MOSI_ECSPI3_MOSI, 0); // MOSI IOMUXC_SetPinMux(IOMUXC_ECSPI3_MISO_ECSPI3_MISO, 0); // MISO IOMUXC_SetPinMux(IOMUXC_GPIO1_IO20_GPIO1_IO20, 0); // CS(GPIO1_IO20) // 2. 配置引脚电气特性(上拉、速率、驱动能力) IOMUXC_SetPinConfig(IOMUXC_ECSPI3_SCLK_ECSPI3_SCLK, 0x10B0); IOMUXC_SetPinConfig(IOMUXC_ECSPI3_MOSI_ECSPI3_MOSI, 0x10B0); IOMUXC_SetPinConfig(IOMUXC_ECSPI3_MISO_ECSPI3_MISO, 0x10B0); IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO20_GPIO1_IO20, 0x10B0); // 3. CS引脚设为输出,默认拉高(未选中设备) GPIO1->GDIR |= (1 << 20); GPIO1->DR |= (1 << 20); } /** * @brief ECSPI3 控制器初始化(模式3、主模式、时钟分频配置) */ void spi3_init(void) { spi3_pin_init(); // 1. 使能ECSPI3时钟(CCM寄存器) CCM->CCGR1 |= (3 << 28); // CCM_CCGR1[29:28] = 11, 使能ECSPI3时钟 // 2. 复位ECSPI3控制器 ECSPI3->CONREG &= ~(1 << 0); // 关闭ECSPI ECSPI3->CONREG |= (1 << 1); // 软件复位 while((ECSPI3->CONREG & (1 << 1)) != 0); // 等待复位完成 // 3. 配置ECSPI3_CONREG寄存器 ECSPI3->CONREG = 0; ECSPI3->CONREG |= (1 << 0); // 使能ECSPI ECSPI3->CONREG |= (0 << 1); // 主模式 ECSPI3->CONREG |= (3 << 2); // 通道0 ECSPI3->CONREG |= (0 << 4); // 无硬件片选,使用软件CS ECSPI3->CONREG |= (1 << 6); // CPOL=1(模式3) ECSPI3->CONREG |= (1 << 7); // CPHA=1(模式3) ECSPI3->CONREG |= (0 << 8); // MSB先行 ECSPI3->CONREG |= (7 << 10); // 数据长度8bit ECSPI3->CONREG |= (0 << 17); // 忽略片选极性 // 4. 配置时钟分频(SCLK = 60MHz / (1 + 7) = 7.5MHz) ECSPI3->PERIODREG = 7; // PERIOD[7:0] = 7, 分频系数=7+1=8 } /** * @brief ECSPI3 通道0 读写函数(SPI核心逻辑:发送1字节同时接收1字节) * @param data: 要发送的字节 * @retval 接收的字节 */ unsigned char spi3_ch0_write_and_read(unsigned char data) { // 1. 等待发送FIFO为空 while((ECSPI3->STATREG & (1 << 1)) == 0); // 2. 写入发送数据(8bit) ECSPI3->TXDATA = data & 0xFF; // 3. 等待接收FIFO有数据 while((ECSPI3->STATREG & (1 << 0)) == 0); // 4. 读取接收数据并返回 return ECSPI3->RXDATA & 0xFF; }

二、SPI 实战:驱动 ADXL345 三轴加速度传感器

本次实验将 ADXL345 连接到 IMX6ULL 的 ECSPI3 通道 0,采用四线制 SPI 通信,实现三轴加速度数据的读取。开发核心思路为:先实现 ECSPI 的通用初始化和读写函数,再基于 SPI 封装 ADXL345 的设备操作函数,实现总线与设备的解耦。

2.1 ADXL345 传感器核心特性

ADXL345 是一款低功耗、高精度的三轴加速度传感器,完美适配 SPI/I2C 总线,核心特性如下:

  • 测量范围:±2g/±4g/±8g/±16g 可编程;
  • 分辨率:最高 13 位(3.9mg/LSB),可精准检测静态重力和动态振动;
  • 工作电压:2.0V~3.6V,测量模式功耗仅 0.23mA,待机模式 40μA;
  • 接口支持:3 线 / 4 线 SPI、I2C,本次采用 4 线 SPI;
  • 核心功能:三轴加速度测量、自由落体检测、单击 / 双击检测、活动 / 非活动监测,内置 32 级 FIFO 缓冲。

2.2 ADXL345 的 SPI 通信规则

ADXL345 的 SPI 通信采用模式 3(CPOL=1,CPHA=1),即 SCLK 空闲为高电平,在第二个时钟跳变沿采样数据,同时有一个关键的地址规则:

  • 写寄存器:寄存器地址最高位为 0,直接发送地址 + 待写数据;
  • 读寄存器:寄存器地址最高位为 1,发送带最高位 1 的地址 + 空数据(Dummy,如 0xFF),同时接收传感器返回的寄存器数据。

例如:写 0x20 寄存器先发 0x20,再发数据;读 0x20 寄存器先发 0xA0(0x20 | 0x80),再发 0xFF,同时读取返回值。

2.3 ADXL345 完整驱动代码(adxl345.c/adxl345.h)

// adxl345.h #ifndef __ADXL345_H__ #define __ADXL345_H__ #include "stdint.h" // 三轴加速度数据结构体 typedef struct { int16_t x; // X轴加速度 int16_t y; // Y轴加速度 int16_t z; // Z轴加速度 } ADXL345_Data; // 函数声明 unsigned char adxl345_read(unsigned char reg_addr); void adxl345_write(unsigned char reg_addr, unsigned char data); void adxl345_init(void); ADXL345_Data adxl345_read_data(void); // 新增:数据转换为实际加速度值(g) float adxl345_convert_to_g(int16_t raw_data); #endif // adxl345.c #include "adxl345.h" #include "spi.h" #include "MCIMX6Y2.h" #include "fsl_iomuxc.h" #include "stdio.h" /** * @brief 读取ADXL345指定寄存器的值 * @param reg_addr: 要读取的寄存器地址 * @retval 寄存器返回值 * @note CS引脚为GPIO1_IO20,低电平有效 */ unsigned char adxl345_read(unsigned char reg_addr) { unsigned char ret = 0; // 拉低CS引脚,选中ADXL345 GPIO1->DR &= ~(1 << 20); // 发送带读标志的寄存器地址(最高位置1) spi3_ch0_write_and_read(reg_addr | 0x80); // 发送Dummy数据0xFF,同时接收传感器返回值 ret = spi3_ch0_write_and_read(0xFF); // 拉高CS引脚,释放总线 GPIO1->DR |= (1 << 20); return ret; } /** * @brief 向ADXL345指定寄存器写入数据 * @param reg_addr: 要写入的寄存器地址 * @param data: 要写入的数据 * @retval 无 */ void adxl345_write(unsigned char reg_addr, unsigned char data) { // 拉低CS引脚,选中ADXL345 GPIO1->DR &= ~(1 << 20); // 发送寄存器地址(最高位置0,写操作) spi3_ch0_write_and_read(reg_addr); // 发送要写入的数据 spi3_ch0_write_and_read(data); // 拉高CS引脚,释放总线 GPIO1->DR |= (1 << 20); } /** * @brief ADXL345初始化函数 * @param 无 * @retval 无 * @note 配置测量范围、采样率、使能测量模式,增加通信校验 */ void adxl345_init(void) { // 读取设备ID寄存器(0x00),验证通信是否正常(ADXL345 ID为0xE5) unsigned char dev_id = adxl345_read(0x00); if (dev_id != 0xE5) { printf("ADXL345通信异常!ID=0x%02X(预期0xE5)\n", dev_id); while(1); // 通信失败则卡死,便于调试 } printf("ADXL345 ID = 0x%02X(正常)\n", dev_id); // 0x2E: 中断控制寄存器,禁用所有中断 adxl345_write(0x2E, 0x08); // 0x31: 数据格式寄存器,0x0B表示±16g量程、13位分辨率 adxl345_write(0x31, 0x0B); // 0x2C: 功耗控制寄存器,0x08表示采样率12.5Hz adxl345_write(0x2C, 0x08); // 0x2D: 电源控制寄存器,0x08表示使能测量模式(修正原代码0x0B,避免休眠) adxl345_write(0x2D, 0x08); } /** * @brief 读取ADXL345三轴加速度原始数据 * @param 无 * @retval ADXL345_Data结构体,包含X/Y/Z三轴16位数据 */ ADXL345_Data adxl345_read_data(void) { ADXL345_Data ret; // 读取X轴数据(低字节0x32,高字节0x33) ret.x = adxl345_read(0x32); ret.x |= (int16_t)(adxl345_read(0x33) << 8); // 读取Y轴数据(低字节0x34,高字节0x35) ret.y = adxl345_read(0x34); ret.y |= (int16_t)(adxl345_read(0x35) << 8); // 读取Z轴数据(低字节0x36,高字节0x37) ret.z = adxl345_read(0x36); ret.z |= (int16_t)(adxl345_read(0x37) << 8); return ret; } /** * @brief 将原始数据转换为实际加速度值(单位:g) * @param raw_data: 原始16位数据 * @retval 实际加速度值(g) * @note ±16g量程下,1LSB = 3.9mg = 0.0039g */ float adxl345_convert_to_g(int16_t raw_data) { return raw_data * 0.0039f; }

2.4 ADXL345 调试与异常处理

  • 通信校验:初始化时读取设备 ID,若与预期(0xE5)不符则打印错误并卡死,快速定位硬件接线 / 驱动问题;
  • 数据有效性:原始数据为有符号数,转换为实际 g 值后更易理解(如水平放置时 Z 轴约 1g,X/Y 轴接近 0g);
  • 常见问题
    1. ID 读取错误:检查 CS/SCLK/MOSI/MISO 接线、SPI 模式(必须模式 3)、时钟频率(不宜超过 10MHz);
    2. 数据恒为 0:检查电源控制寄存器(0x2D)是否配置为测量模式(0x08),而非休眠模式;
    3. 数据波动大:增加软件滤波(如滑动平均)
    // 滑动平均滤波(取10次采样平均值) ADXL345_Data adxl345_filter_data(void) { ADXL345_Data sum = {0, 0, 0}; for (int i = 0; i < 10; i++) { ADXL345_Data tmp = adxl345_read_data(); sum.x += tmp.x; sum.y += tmp.y; sum.z += tmp.z; delay_ms(1); } sum.x /= 10; sum.y /= 10; sum.z /= 10; return sum; }

三、I2C 总线基础与 IMX6ULL I2C 控制器

3.1 I2C 总线核心特性

I2C(Inter-Integrated Circuit)是由飞利浦定义的半双工串行总线,仅需 SCL(时钟)和 SDA(数据)两根线即可实现多设备通信,核心特性:

  • 主从架构:单主机多从机,从机通过 7 位 / 10 位地址区分;
  • 总线仲裁:多主机场景下避免总线冲突(IMX6ULL 开发中极少用到);
  • 应答机制:从机接收 / 发送数据后需发送 ACK/NACK 确认,是通信可靠性的关键;
  • 速率等级:标准模式(100Kbps)、快速模式(400Kbps)、高速模式(3.4Mbps),GT9147 采用 400Kbps。

3.2 IMX6ULL I2C2 底层驱动实现(i2c.c/i2c.h)

补充 I2C 通用驱动,支撑 GT9147 驱动开发:

// i2c.h #ifndef __I2C_H__ #define __I2C_H__ #include "stdint.h" // I2C传输方向枚举 typedef enum { I2C_Write = 0, I2C_Read } I2C_Dir; // I2C消息结构体 typedef struct { uint8_t dev_addr; // 设备地址(7位) uint16_t reg_addr; // 寄存器地址 uint8_t reg_len; // 寄存器地址长度(1/2字节) I2C_Dir dir; // 传输方向 uint8_t *data; // 数据缓冲区 uint32_t len; // 数据长度 } I2C_Msg; // 函数声明 void i2c2_init(void); void transfer(uint8_t i2c_num, I2C_Msg *msg); // 延时函数声明(需实现) void delay_us(uint32_t us); #endif // i2c.c #include "i2c.h" #include "MCIMX6Y2.h" #include "fsl_iomuxc.h" /** * @brief I2C2 引脚初始化(SCL/SDA) */ static void i2c2_pin_init(void) { // 1. 配置I2C2引脚复用 IOMUXC_SetPinMux(IOMUXC_I2C2_SCL_I2C2_SCL, 1); // 复用为I2C2_SCL IOMUXC_SetPinMux(IOMUXC_I2C2_SDA_I2C2_SDA, 1); // 复用为I2C2_SDA // 2. 配置引脚电气特性(开漏输出、上拉、400Kbps速率) IOMUXC_SetPinConfig(IOMUXC_I2C2_SCL_I2C2_SCL, 0x70B0); IOMUXC_SetPinConfig(IOMUXC_I2C2_SDA_I2C2_SDA, 0x70B0); } /** * @brief I2C2 控制器初始化(400Kbps、主模式) */ void i2c2_init(void) { i2c2_pin_init(); // 1. 使能I2C2时钟 CCM->CCGR1 |= (3 << 12); // CCM_CCGR1[13:12] = 11 // 2. 复位I2C2 I2C2->I2C_CR = 0; // 关闭I2C I2C2->I2C_CR |= (1 << 15); // 软件复位 while((I2C2->I2C_CR & (1 << 15)) != 0); // 3. 配置I2C频率(400Kbps) // I2C时钟源=24MHz,公式:ICR = (24MHz / (2 * 400Kbps)) - 1 = 29 I2C2->I2C_FDR = 0x181B; // ICR=29 (0x1D),配置FDR寄存器 // 4. 使能I2C2 I2C2->I2C_CR |= (1 << 0); } /** * @brief I2C发送起始信号 */ static void i2c_start(I2C_Type *base) { base->I2C_CR |= (1 << 5); // 发送START while((base->I2C_SR & (1 << 5)) == 0); // 等待START发送完成 base->I2C_SR &= ~(1 << 5); // 清除标志 } /** * @brief I2C发送停止信号 */ static void i2c_stop(I2C_Type *base) { base->I2C_CR |= (1 << 6); // 发送STOP while((base->I2C_SR & (1 << 6)) == 0); // 等待STOP发送完成 base->I2C_SR &= ~(1 << 6); // 清除标志 } /** * @brief I2C发送1字节数据 * @param data: 要发送的字节 * @retval 0-成功,1-失败(无ACK) */ static int i2c_send_byte(I2C_Type *base, uint8_t data) { while((base->I2C_SR & (1 << 4)) == 0); // 等待发送缓冲区空 base->I2C_TXDR = data; while((base->I2C_SR & (1 << 1)) == 0); // 等待发送完成 if (base->I2C_SR & (1 << 0)) // 检查ACK { base->I2C_SR &= ~(1 << 0); return 1; // 无ACK } return 0; } /** * @brief I2C接收1字节数据 * @param ack: 1-发送ACK,0-发送NACK * @retval 接收的字节 */ static uint8_t i2c_recv_byte(I2C_Type *base, uint8_t ack) { while((base->I2C_SR & (1 << 3)) == 0); // 等待接收缓冲区满 uint8_t data = base->I2C_RXDR; // 发送ACK/NACK if (ack) base->I2C_CR &= ~(1 << 4); // ACK else base->I2C_CR |= (1 << 4); // NACK delay_us(10); return data; } /** * @brief I2C通用传输函数(核心) * @param i2c_num: I2C控制器编号(2=I2C2) * @param msg: I2C消息结构体 */ void transfer(uint8_t i2c_num, I2C_Msg *msg) { I2C_Type *base = (i2c_num == 2) ? I2C2 : NULL; if (base == NULL) return; // 1. 发送起始信号 + 设备地址(写) i2c_start(base); uint8_t dev_addr = (msg->dev_addr << 1) | I2C_Write; if (i2c_send_byte(base, dev_addr) != 0) { printf("I2C设备地址0x%02X无应答!\n", msg->dev_addr); i2c_stop(base); return; } // 2. 发送寄存器地址 if (msg->reg_len == 2) // 16位寄存器地址(GT9147) { i2c_send_byte(base, (msg->reg_addr >> 8) & 0xFF); // 高字节 i2c_send_byte(base, msg->reg_addr & 0xFF); // 低字节 } else if (msg->reg_len == 1) // 8位寄存器地址 { i2c_send_byte(base, msg->reg_addr & 0xFF); } if (msg->dir == I2C_Read) { // 3. 重复起始信号 + 设备地址(读) i2c_start(base); dev_addr = (msg->dev_addr << 1) | I2C_Read; if (i2c_send_byte(base, dev_addr) != 0) { printf("I2C读地址0x%02X无应答!\n", msg->dev_addr); i2c_stop(base); return; } // 4. 接收数据 for (uint32_t i = 0; i < msg->len; i++) { // 最后1字节发送NACK,其余发送ACK msg->data[i] = i2c_recv_byte(base, (i == msg->len - 1) ? 0 : 1); } } else // I2C_Write { // 3. 发送数据 for (uint32_t i = 0; i < msg->len; i++) { i2c_send_byte(base, msg->data[i]); } } // 5. 发送停止信号 i2c_stop(base); }

四、I2C 总线实战:驱动 GT9147 多点电容触摸 IC

电容触摸屏是嵌入式人机交互的核心设备,本次实验基于 IMX6ULL 的 I2C2 控制器,驱动 GT9147 多点电容触摸 IC(搭载在 ATK-7016RGB LCD 屏上),采用中断方式获取触摸坐标,补充了异常处理、坐标校准、多触点防抖等关键内容。

4.1 GT9147 触摸 IC 核心特性

GT9147 是一款高性能的多点电容触摸控制 IC,广泛应用于中小尺寸 LCD 屏,核心特性如下:

  • 支持最大 5 点触摸,采用 15*28 的驱动感应结构,触摸精度高;
  • 通信接口:I2C(SCL/SDA),配套 RST(复位)、INT(中断)引脚;
  • 中断方式:触摸按下 / 松开时,INT 引脚产生电平变化,通知主机读取数据;
  • 寄存器操作:所有配置和数据读取均通过 I2C 读写寄存器完成;
  • 可配置参数:触摸灵敏度、采样频率、中断触发方式(上升沿 / 下降沿 / 电平)。

4.2 GT9147 完整驱动代码(gt9147.c/gt9147.h)

// gt9147.h #ifndef __GT9147_H__ #define __GT9147_H__ #include "stdint.h" // I2C传输方向枚举 typedef enum { I2C_Read = 0, I2C_Write } I2C_Dir; // I2C消息结构体 typedef struct { uint8_t dev_addr; // 设备地址(7位) uint16_t reg_addr; // 寄存器地址 uint8_t reg_len; // 寄存器地址长度 I2C_Dir dir; // 传输方向 uint8_t *data; // 数据缓冲区 uint32_t len; // 数据长度 } I2C_Msg; // 触摸设备结构体 typedef struct { uint8_t num; // 触摸点个数(0~5) uint8_t available; // 触摸数据有效标志(1-有效,0-无效) uint16_t x[5]; // 5个触摸点X坐标 uint16_t y[5]; // 5个触摸点Y坐标 uint8_t id[5]; // 触摸点ID(区分不同触点) } GT9147_Device; // 函数声明 void gt9147_read(unsigned short reg_addr, unsigned char *data, unsigned int len); void gt9147_write(unsigned short reg_addr, unsigned char *data, unsigned int len); void gt9147_init(void); // 新增:坐标校准函数 void gt9147_calibrate(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t screen_w, uint16_t screen_h); // 外部函数声明 void transfer(uint8_t i2c_num, struct I2C_Msg *msg); void delay_ms(uint32_t ms); void delay_us(uint32_t us); void system_interrupt_register(uint32_t irq_num, void (*handler)(void)); void GIC_SetPriority(uint32_t irq_num, uint8_t priority); void GIC_EnableIRQ(uint32_t irq_num); #endif // gt9147.c #include "gt9147.h" #include "MCIMX6Y2.h" #include "fsl_iomuxc.h" #include "interrupt.h" #include "stdio.h" // 触摸设备结构体实例 GT9147_Device touch_dev; // 5个触摸点的坐标寄存器地址(实测修正版,手册标注为0x8158开始) unsigned short point_addr[5] = {0x8150, 0x8158, 0x8160, 0x8168, 0x8170}; // 坐标校准参数 static int16_t calib_x_offset = 0; static int16_t calib_y_offset = 0; static float calib_x_scale = 1.0f; static float calib_y_scale = 1.0f; /** * @brief 从GT9147指定寄存器读取数据 * @param reg_addr: 寄存器地址(16位) * @param data: 数据接收缓冲区 * @param len: 要读取的字节数 * @retval 无 */ void gt9147_read(unsigned short reg_addr, unsigned char *data, unsigned int len) { // GT9147设备地址为0x14(7位地址,实际I2C传输时左移1位) I2C_Msg _gt9147 = { .dev_addr = 0x14, .reg_addr = reg_addr, .reg_len = 2, // 寄存器地址长度为2字节(16位) .dir = I2C_Read, // 读操作 .data = data, .len = len }; transfer(I2C2, &_gt9147); } /** * @brief 向GT9147指定寄存器写入数据 * @param reg_addr: 寄存器地址(16位) * @param data: 要写入的数据缓冲区 * @param len: 要写入的字节数 * @retval 无 */ void gt9147_write(unsigned short reg_addr, unsigned char *data, unsigned int len) { I2C_Msg _gt9147 = { .dev_addr = 0x14, .reg_addr = reg_addr, .reg_len = 2, .dir = I2C_Write, // 写操作 .data = data, .len = len }; transfer(I2C2, &_gt9147); } /** * @brief 触摸坐标校准(解决屏幕显示与触摸坐标不匹配问题) * @param x0/y0: 校准点1原始坐标 * @param x1/y1: 校准点2原始坐标 * @param screen_w/screen_h: 屏幕分辨率 */ void gt9147_calibrate(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t screen_w, uint16_t screen_h) { calib_x_offset = -x0; calib_y_offset = -y0; calib_x_scale = (float)screen_w / (x1 - x0); calib_y_scale = (float)screen_h / (y1 - y0); printf("GT9147校准完成:X偏移=%d, Y偏移=%d, X缩放=%.2f, Y缩放=%.2f\n", calib_x_offset, calib_y_offset, calib_x_scale, calib_y_scale); } /** * @brief 校准触摸坐标 * @param x/y: 原始坐标 * @retval 校准后的坐标 */ static void gt9147_calib_coords(uint16_t *x, uint16_t *y) { *x = (uint16_t)((*x + calib_x_offset) * calib_x_scale); *y = (uint16_t)((*y + calib_y_offset) * calib_y_scale); // 边界检查,避免坐标超出屏幕 if (*x > 800) *x = 800; if (*y > 480) *y = 480; } /** * @brief 打印触摸点坐标(带校准) */ void show_points(void) { if (touch_dev.available == 1) { for (int i = 0; i < touch_dev.num; i++) { // 校准坐标 uint16_t x = touch_dev.x[i]; uint16_t y = touch_dev.y[i]; gt9147_calib_coords(&x, &y); printf("[%d] ID=%d: x = %d, y = %d\n", i, touch_dev.id[i], x, y); } touch_dev.available = 0; // 清除有效标志 } } /** * @brief 触摸中断服务函数(防抖+高效处理) * @note GPIO1_IO09为触摸中断引脚,下降沿触发 */ void touch_interrupt_handler(void) { // 检查GPIO1_IO09中断标志位 if ((GPIO1->ISR & (1 << 9)) != 0) { // 防抖延时(100us),避免机械抖动导致重复触发 delay_us(100); if ((GPIO1->DR & (1 << 9)) != 0) // 再次检查电平 { GPIO1->ISR |= (1 << 9); // 清除中断标志 return; } unsigned char buffer[128] = {0}; // 读取触摸点个数寄存器(0x814E) gt9147_read(0x814E, buffer, 1); int num = buffer[0] & 0x0F; // 低4位为有效触摸点个数(0~5) // 写入0清除触摸标志,避免重复触发中断 buffer[0] = 0; gt9147_write(0x814E, buffer, 1); if (num != 0 && num <= 5) // 校验触摸点个数有效性 { touch_dev.num = num; touch_dev.available = 1; printf("触摸点数 = %d\n", num); for (int i = 0; i < num; i++) { // 读取每个触摸点的4字节坐标数据 gt9147_read(point_addr[i], buffer, 4); // 解析X坐标(低字节buffer[0],高字节buffer[1]) touch_dev.x[i] = (buffer[1] << 8) | buffer[0]; // 解析Y坐标(低字节buffer[2],高字节buffer[3]) touch_dev.y[i] = (buffer[3] << 8) | buffer[2]; // 解析触摸点ID(buffer[4],扩展读取1字节) gt9147_read(point_addr[i] + 4, buffer, 1); touch_dev.id[i] = buffer[0] & 0x0F; } show_points(); // 打印坐标 } // 清除中断标志位 GPIO1->ISR |= (1 << 9); } } /** * @brief GT9147初始化函数(增加通信校验+参数配置) */ void gt9147_init(void) { // 1. 配置引脚复用 IOMUXC_SetPinMux(IOMUXC_GPIO1_IO09_GPIO1_IO09, 0); // INT引脚(GPIO1_IO09) IOMUXC_SetPinMux(IOMUXC_SNVS_SNVS_TAMPER9_GPIO5_IO09, 0); // RST引脚(GPIO5_IO09) // 2. 配置引脚电气特性 IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO09_GPIO1_IO09, 0x10B0); IOMUXC_SetPinConfig(IOMUXC_SNVS_SNVS_TAMPER9_GPIO5_IO09, 0x10B0); // 3. 设置引脚为输出模式(用于复位) GPIO1->GDIR |= (1 << 9); // INT引脚设为输出 GPIO5->GDIR |= (1 << 9); // RST引脚设为输出 // 4. 复位GT9147 GPIO1->DR &= ~(1 << 9); // INT拉低 GPIO5->DR &= ~(1 << 9); // RST拉低 delay_ms(10); // 延时10ms GPIO5->DR |= (1 << 9); // 释放复位 delay_ms(100); // 等待GT9147初始化 // 5. 读取设备ID(0x8140~0x8143),验证I2C通信(正常为"9147"或"1158") unsigned char buffer[32] = {0}; gt9147_read(0x8140, buffer, 4); printf("GT9147 ID = %s\n", (char *)buffer); // 通信校验 if (buffer[0] != '9' && buffer[0] != '1') { printf("GT9147通信异常!\n"); while(1); } // 6. 配置触摸灵敏度(0x8047寄存器,值越小越灵敏) buffer[0] = 0x05; // 高灵敏度 gt9147_write(0x8047, buffer, 1); // 7. 重新配置INT引脚为输入模式(中断检测) IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO09_GPIO1_IO09, 0x800); // 使能Keeper属性 GPIO1->GDIR &= ~(1 << 9); // 改为输入模式 // 8. 配置GPIO中断(下降沿触发) GPIO1->ICR1 |= (3 << 18); // ICR1[19:18]配置GPIO1_IO09为下降沿触发 GPIO1->IMR |= (1 << 9); // 解除中断屏蔽 // 9. 注册并配置中断 system_interrupt_register(GPIO1_Combined_0_15_IRQn, touch_interrupt_handler); GIC_SetPriority(GPIO1_Combined_0_15_IRQn, 0); GIC_EnableIRQ(GPIO1_Combined_0_15_IRQn); // 10. 设置GT9147为正常读坐标模式(0x8040) buffer[0] = 0; gt9147_write(0x8040, buffer, 1); // 默认校准(适配800x480屏幕) gt9147_calibrate(0, 0, 800, 480, 800, 480); }

4.3 GT9147 调试与性能优化

  • 防抖处理:中断服务函数中增加 100us 延时防抖,避免触摸机械抖动导致的重复触发;
  • 坐标校准:解决触摸坐标与屏幕显示坐标不匹配的问题,适配不同分辨率屏幕;
  • 中断优先级:设置为最高优先级(0),保证触摸响应的实时性;
  • 常见问题
    1. 无中断触发:检查 INT 引脚接线、中断配置(下降沿 / 上升沿)、GT9147 中断使能寄存器;
    2. 坐标乱跳:调整触摸灵敏度寄存器(0x8047)、增加软件滤波、检查电源纹波;
    3. 多触点识别异常:解析触摸点 ID(buffer [4]),区分不同触点。

五、驱动使用

5.1 ADXL345 使用

#include "adxl345.h" #include "spi.h" #include "delay.h" #include "stdio.h" int main(void) { // 初始化串口(用于打印) uart_init(); // 初始化ECSPI3 spi3_init(); // 初始化ADXL345 adxl345_init(); while(1) { // 读取三轴加速度原始数据 ADXL345_Data data = adxl345_read_data(); // 转换为g值 float x_g = adxl345_convert_to_g(data.x); float y_g = adxl345_convert_to_g(data.y); float z_g = adxl345_convert_to_g(data.z); // 打印数据(原始值+实际g值) printf("X: %d (%.2fg), Y: %d (%.2fg), Z: %d (%.2fg)\n", data.x, x_g, data.y, y_g, data.z, z_g); // 延时500ms delay_ms(500); } return 0; }

5.2 GT9147 使用

#include "gt9147.h" #include "i2c.h" #include "delay.h" #include "stdio.h" int main(void) { // 初始化串口 uart_init(); // 初始化I2C2 i2c2_init(); // 初始化GT9147 gt9147_init(); // 可选:手动校准(根据实际屏幕调整) // gt9147_calibrate(20, 20, 780, 460, 800, 480); while(1) { // 主循环仅做其他业务处理,触摸数据由中断服务函数打印 delay_ms(10); } return 0; }

六、嵌入式串行总线开发通用思想与工程实践

本次 SPI 驱动 ADXL345 和 I2C 驱动 GT9147 的实验,遵循了嵌入式开发的通用核心思想,补充工程实践要点:

6.1 总线与设备解耦

  • 先实现总线(ECSPI/I2C)的通用驱动,包含引脚初始化、控制器初始化、通用读写函数,总线驱动与具体外设无关,可复用;
  • 基于总线通用驱动,封装具体外设的操作函数,仅需关注外设的通信规则和寄存器配置。

6.2 工程化开发要点

  1. 模块化分层
    • 底层:总线驱动(spi.c/i2c.c),负责硬件寄存器操作;
    • 中层:外设驱动(adxl345.c/gt9147.c),封装外设寄存器和通信规则;
    • 应用层:业务逻辑(main.c),调用外设驱动接口。
  2. 调试优先级
    • 第一步:验证总线通信(读取设备 ID);
    • 第二步:验证外设初始化(寄存器配置);
    • 第三步:验证数据读取(传感器数据 / 触摸坐标);
    • 第四步:优化性能(滤波、防抖、实时性)。
  3. 可靠性设计
    • 通信校验:读取设备 ID、寄存器回读;
    • 异常处理:总线无应答、数据超出范围时的容错;
    • 资源保护:SPI/I2C 总线操作时禁止中断(避免时序错乱)。

6.3 拓展方向

  1. SPI 驱动拓展:基于 ECSPI 通用驱动,驱动 SPI FLASH(如 W25Q64)、SPI OLED 屏,掌握 SPI 多设备组网(通过 CS 引脚);
  2. I2C 驱动拓展:基于 I2C2 通用驱动,驱动 I2C 温湿度传感器(如 AM2320)、I2C EEPROM(如 AT24C02),掌握 I2C 多设备组网(通过设备地址);
  3. 功能升级
    • ADXL345:添加自由落体检测、单击 / 双击检测、FIFO 数据读取;
    • GT9147:添加手势识别(滑动、缩放)、触摸按键映射;
  4. Linux 驱动移植:将裸机驱动移植为 Linux 内核驱动(字符设备驱动),实现基于设备文件的外设操作,贴近实际产品开发。

总结

  1. SPI 核心要点:ADXL345 采用 SPI 模式 3(CPOL=1、CPHA=1),读寄存器地址最高位需置 1,初始化时必须校验设备 ID,原始数据需转换为实际 g 值;
  2. I2C 核心要点:GT9147 寄存器地址为 16 位,触摸点坐标寄存器起始地址为 0x8150(手册标注有误),中断处理需防抖,坐标需校准适配屏幕;
  3. 工程实践要点:总线与设备解耦开发、通信校验、异常处理、模块化分层是嵌入式驱动开发的通用准则,可大幅提升代码复用性和可靠性。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/10 22:25:59

Lychee-Rerank在智能客服知识库中的应用:用户问句-FAQ匹配度排序

Lychee-Rerank在智能客服知识库中的应用&#xff1a;用户问句-FAQ匹配度排序 1. 项目背景与价值 在智能客服系统中&#xff0c;如何快速准确地匹配用户问句与知识库中的FAQ条目是一个核心挑战。传统的关键词匹配方法往往无法理解语义相似性&#xff0c;导致大量相关答案被遗漏…

作者头像 李华
网站建设 2026/4/10 18:35:49

QMCDecode全攻略:QQ音乐加密音频转换的高效解决方案

QMCDecode全攻略&#xff1a;QQ音乐加密音频转换的高效解决方案 【免费下载链接】QMCDecode QQ音乐QMC格式转换为普通格式(qmcflac转flac&#xff0c;qmc0,qmc3转mp3, mflac,mflac0等转flac)&#xff0c;仅支持macOS&#xff0c;可自动识别到QQ音乐下载目录&#xff0c;默认转换…

作者头像 李华
网站建设 2026/4/18 6:43:36

云容笔谈GPU算力优化:梯度检查点+FlashAttention-2降低显存峰值45%

云容笔谈GPU算力优化&#xff1a;梯度检查点FlashAttention-2降低显存峰值45% 1. 项目背景与挑战 云容笔谈作为专注于东方审美的高清影像生成平台&#xff0c;面临着GPU显存使用的重大挑战。系统基于Z-Image Turbo核心驱动&#xff0c;需要处理1024x1024分辨率的高清图像生成…

作者头像 李华
网站建设 2026/4/16 18:32:43

7个技巧让PS手柄在PC游戏实现无延迟操控 - 2026实战指南

7个技巧让PS手柄在PC游戏实现无延迟操控 - 2026实战指南 【免费下载链接】DS4Windows Like those other ds4tools, but sexier 项目地址: https://gitcode.com/gh_mirrors/ds/DS4Windows 作为一名多年主机玩家&#xff0c;当我第一次把PS4手柄接到PC上时&#xff0c;满心…

作者头像 李华