一、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 = 空) 核心用法:
- 发送数据前检查 TF 位,为 0 时向 TXDATA 写数据;
- 接收数据前检查 RR 位,为 1 时从 RXDATA 读数据;
- 传输完成后检查 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);
- 常见问题 :
-
ID 读取错误:检查 CS/SCLK/MOSI/MISO 接线、SPI 模式(必须模式 3)、时钟频率(不宜超过 10MHz);
-
数据恒为 0:检查电源控制寄存器(0x2D)是否配置为测量模式(0x08),而非休眠模式;
-
数据波动大:增加软件滤波(如滑动平均)
// 滑动平均滤波(取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),保证触摸响应的实时性;
- 常见问题 :
- 无中断触发:检查 INT 引脚接线、中断配置(下降沿 / 上升沿)、GT9147 中断使能寄存器;
- 坐标乱跳:调整触摸灵敏度寄存器(0x8047)、增加软件滤波、检查电源纹波;
- 多触点识别异常:解析触摸点 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 工程化开发要点
- 模块化分层 :
- 底层:总线驱动(spi.c/i2c.c),负责硬件寄存器操作;
- 中层:外设驱动(adxl345.c/gt9147.c),封装外设寄存器和通信规则;
- 应用层:业务逻辑(main.c),调用外设驱动接口。
- 调试优先级 :
- 第一步:验证总线通信(读取设备 ID);
- 第二步:验证外设初始化(寄存器配置);
- 第三步:验证数据读取(传感器数据 / 触摸坐标);
- 第四步:优化性能(滤波、防抖、实时性)。
- 可靠性设计 :
- 通信校验:读取设备 ID、寄存器回读;
- 异常处理:总线无应答、数据超出范围时的容错;
- 资源保护:SPI/I2C 总线操作时禁止中断(避免时序错乱)。
6.3 拓展方向
- SPI 驱动拓展:基于 ECSPI 通用驱动,驱动 SPI FLASH(如 W25Q64)、SPI OLED 屏,掌握 SPI 多设备组网(通过 CS 引脚);
- I2C 驱动拓展:基于 I2C2 通用驱动,驱动 I2C 温湿度传感器(如 AM2320)、I2C EEPROM(如 AT24C02),掌握 I2C 多设备组网(通过设备地址);
- 功能升级 :
- ADXL345:添加自由落体检测、单击 / 双击检测、FIFO 数据读取;
- GT9147:添加手势识别(滑动、缩放)、触摸按键映射;
- Linux 驱动移植:将裸机驱动移植为 Linux 内核驱动(字符设备驱动),实现基于设备文件的外设操作,贴近实际产品开发。
总结
- SPI 核心要点:ADXL345 采用 SPI 模式 3(CPOL=1、CPHA=1),读寄存器地址最高位需置 1,初始化时必须校验设备 ID,原始数据需转换为实际 g 值;
- I2C 核心要点:GT9147 寄存器地址为 16 位,触摸点坐标寄存器起始地址为 0x8150(手册标注有误),中断处理需防抖,坐标需校准适配屏幕;
- 工程实践要点:总线与设备解耦开发、通信校验、异常处理、模块化分层是嵌入式驱动开发的通用准则,可大幅提升代码复用性和可靠性。