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:20 BURST_LENGTH 突发传输数据长度,0X000~0XFFF 对应 1~2^12 bit,实验设为 7(8bit/1 字节) 7(0x007)
    bit19:18 CHANNEL_SELECT SPI 通道选择(0~3 对应 SS0~SS3),实验用 ECSPI3 通道 0 0(0x0)
    bit17:16 DRCTL SPI_RDY 信号控制,0 = 不关心,1 = 边沿触发,2 = 电平触发 0
    bit15:12 PRE_DIVIDER 预分频(1~16 分频),基础时钟 60MHz,实验设为 0(1 分频) 0(0x0)
    bit11:8 POST_DIVIDER 二级分频,分频值 = 2^POST_DIVIDER,实验设为 3(8 分频,60/8=7.5MHz) 3(0x3)
    bit7:4 CHANNEL_MODE 通道主 / 从模式(0 = 从,1 = 主),仅对应位生效,实验设通道 0 为主模式 0x01
    bit3 SMC 开始模式控制,0 = 通过 XCH 启动传输,1 = 写 TXFIFO 即启动 1
    bit2 XCH 传输启动位(仅 SMC=0 时生效) 0
    bit1 HT HT 模式使能(I.MX6ULL 不支持) 0
    bit0 EN SPI 使能位,1 = 使能,0 = 关闭 1
    ECSPIx_CONFIGREG(配置寄存器)

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

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

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

    ECSPIx_PERIODREG(周期寄存器)

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

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

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

    ECSPIx_STATREG(状态寄存器)

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

    名称 功能说明
    bit7 TC 传输完成标志(0 = 传输中,1 = 完成)
    bit6 RO RXFIFO 溢出标志(0 = 无溢出,1 = 溢出)
    bit5 RF RXFIFO 空标志(0 = 非空,1 = 空)
    bit4 RDR RXFIFO 数据请求(0 = 数据≤阈值,1 = 数据>阈值)
    bit3 RR RXFIFO 就绪(0 = 无数据,1 = 至少 1 个字数据)
    bit2 TF TXFIFO 满标志(0 = 非满,1 = 满)
    bit1 TDR TXFIFO 数据请求(0 = 数据>阈值,1 = 数据≤阈值)
    bit0 TE TXFIFO 空标志(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. 工程实践要点:总线与设备解耦开发、通信校验、异常处理、模块化分层是嵌入式驱动开发的通用准则,可大幅提升代码复用性和可靠性。
相关推荐
YJlio8 小时前
1.7 通过 Sysinternals Live 在线运行工具:不下载也能用的“云端工具箱”
c语言·网络·python·数码相机·ios·django·iphone
不做无法实现的梦~10 小时前
ros2实现路径规划---nav2部分
linux·stm32·嵌入式硬件·机器人·自动驾驶
陌上花开缓缓归以10 小时前
W25N01KVZEIR flash烧写
arm开发
Lbs_gemini060314 小时前
01-01-01 C++编程知识 C++入门 工具安装
c语言·开发语言·c++·学习·算法
熊猫_豆豆14 小时前
同步整流 Buck 降压变换器
单片机·嵌入式硬件·matlab
shihui200315 小时前
两个8*8点阵流水屏
c语言·51单片机·proteus
ghx_echo16 小时前
c/c++结构体对齐,extern “C”与关键字const
c语言·c++
IvanCodes16 小时前
五、C语言数组
c语言·开发语言
chenchen0000000018 小时前
49元能否买到四核性能?HZ-RK3506G2_MiniEVM开发板评测:MCU+三核CPU带来的超高性价比
单片机·嵌入式硬件
雪域迷影19 小时前
MacOS下源码安装SDL3并运行hello.c示例程序
c语言·开发语言·macos·sdl3