本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:在嵌入式Linux系统中,触摸屏作为重要的人机交互设备,其I2C驱动的实现对系统功能至关重要。本文聚焦于基于全志A33、A83T等系列芯片的嵌入式平台,详细讲解如何编写和集成触摸屏的I2C驱动程序。内容涵盖驱动注册、I2C设备探测、input子系统初始化、中断处理机制、电源管理支持及设备节点创建等核心环节。通过分析wdt87xx_i2c.c源码,帮助开发者掌握从硬件通信到内核事件上报的完整流程,适用于平板电脑、智能终端等设备的驱动开发与调试。

1. 嵌入式Linux驱动开发概述

嵌入式Linux系统中的设备驱动是连接硬件与操作系统的核心桥梁,尤其在触摸屏等外设控制中起着至关重要的作用。本章将从整体视角出发,阐述嵌入式Linux驱动的基本架构、分类及其在内核中的运行机制。

1.1 嵌入式Linux驱动的基本架构

Linux内核采用分层设计思想,驱动程序位于用户空间与硬件之间,通过 设备模型 (device、driver、bus)实现统一管理。该模型基于面向对象的设计理念,其中:
- struct device 描述一个物理设备;
- struct driver 表示对应设备的驱动逻辑;
- struct bus_type 定义总线规范(如I2C、SPI、Platform等),负责设备与驱动的匹配。

// 示例:总线上的设备与驱动匹配过程(简化)
static int my_bus_match(struct device *dev, struct device_driver *drv)
{
    return !strcmp(dev->init_name, drv->name); // 名称匹配触发probe
}

该机制支持热插拔和动态加载,提升系统的可扩展性。

1.2 驱动分类与运行机制

Linux驱动主要分为三类:

类型 特点 典型应用
字符设备 提供字节流访问,通过 /dev 节点操作 触摸屏、传感器
平台设备(Platform) 不具备自动探测能力,依赖设备树或板级信息 SoC内部模块
I2C设备 挂载于I2C总线,由适配器统一调度 外接触摸控制器

以触摸屏为例,其通常通过I2C接口连接主控芯片,属于 I2C客户端设备 ,需注册至I2C子系统,并借助 input 子系统上报坐标事件。

1.3 I2C与Input子系统协同工作机制

为何选择I2C作为触摸屏通信接口?原因在于:
- 布线简洁 :仅需SDA/SCL两根线;
- 多设备共存 :支持多个从设备挂载在同一总线上;
- 低速但可靠 :适合周期性读取触摸数据的场景。

input 子系统则是人机交互的关键支撑框架。它将键盘、鼠标、触摸屏等输入设备抽象为统一接口,向上层应用提供标准化事件(如 EV_ABS EV_KEY )。

// 设置绝对坐标范围示例
input_set_abs_params(input_dev, ABS_X, 0, 800, 0, 0);
input_set_abs_params(input_dev, ABS_Y, 0, 480, 0, 0);

当驱动检测到触摸动作时,调用 input_report_abs() 等函数,事件经 /dev/input/eventX 传递至用户空间,被Qt、Android等图形系统捕获处理。

1.4 设备模型注册与匹配原理

Linux内核通过 总线匹配机制 自动绑定设备与驱动。以I2C为例,流程如下:

graph TD
    A[I2C控制器初始化] --> B[i2c_add_adapter注册适配器]
    B --> C[解析设备树I2C节点]
    C --> D[创建i2c_client设备实例]
    D --> E[查找i2c_driver中的of_match_table]
    E --> F{名称匹配成功?}
    F -->|是| G[调用probe函数完成初始化]
    F -->|否| H[保持未绑定状态]

关键结构体定义如下:

static const struct of_device_id wdt87xx_of_match[] = {
    { .compatible = "weidongtai,wdt87xx", },
    { }
};
MODULE_DEVICE_TABLE(of, wdt87xx_of_match);

static struct i2c_driver wdt87xx_ts_driver = {
    .probe = wdt87xx_ts_probe,
    .remove = wdt87xx_ts_remove,
    .driver = {
        .name = "wdt87xx_ts",
        .of_match_table = wdt87xx_of_match,
    },
};

只有设备树中节点的 compatible 属性与驱动中 of_match_table 一致,才会触发 probe() 调用,进而启动驱动初始化流程。

1.5 本章小结

本章构建了嵌入式Linux驱动开发的整体认知框架,明确了设备模型三大核心组件的作用及交互方式,剖析了字符设备、平台设备与I2C设备之间的层次关系,并揭示了I2C总线如何承载触摸屏这类外设通信。同时,强调了 input 子系统在事件上报中的枢纽地位。这些理论基础为后续深入分析I2C协议、全志平台适配以及触摸屏驱动实现提供了坚实支撑。

2. I2C通信协议原理与应用

2.1 I2C协议基本原理

2.1.1 I2C总线结构与信号时序

I²C(Inter-Integrated Circuit)是一种由Philips公司于1980年代初推出的串行通信总线标准,广泛应用于嵌入式系统中低速外设的互联。其核心优势在于仅需两根信号线即可实现多设备间的双向通信: SDA (Serial Data Line)用于传输数据, SCL (Serial Clock Line)提供同步时钟。这种双线制设计极大地简化了PCB布线复杂度,并支持在同一总线上挂载多个从设备。

在物理层面上,I²C总线采用开漏输出(Open-Drain)结构,所有连接到SDA和SCL的设备都只能将线路拉低或释放为高阻态,因此必须通过外部上拉电阻将两条线拉至高电平。典型的上拉电阻值在4.7kΩ左右,具体数值取决于总线电容、工作频率以及电源电压。当没有任何设备驱动总线时,上拉电阻确保总线处于逻辑“高”状态;而任一设备要发送“0”时,则主动将线路接地。

I²C通信遵循严格的时序规范。整个通信过程由主设备发起并控制SCL时钟信号。数据在SCL为低电平时可以改变,在SCL为高电平时必须保持稳定——这是为了避免误采样。每一个时钟周期传送一位数据,典型的工作速率包括标准模式(100 kbps)、快速模式(400 kbps)、快速模式+(1 Mbps),以及高速模式(3.4 Mbps)。现代SoC通常内置I2C控制器,能够自动处理大部分底层时序细节,但仍需开发者理解这些机制以进行调试。

下图展示了I2C的基本数据传输时序流程:

sequenceDiagram
    participant Master
    participant Slave
    Master->>Master: Start Condition (SCL=H, SDA:Falling Edge)
    Master->>Slave: Send 7-bit Address + R/W bit
    Slave-->>Master: ACK (Pull SDA Low)
    alt Write Operation
        loop For each byte
            Master->>Slave: Send Data Byte
            Slave-->>Master: ACK
        end
    else Read Operation
        Slave->>Master: Send Data Byte
        Master-->>Slave: ACK/NACK (Last byte: NACK)
    end
    Master->>Master: Stop Condition (SCL=H, SDA:Rising Edge)

该流程图清晰地呈现了从起始条件到停止条件之间的完整交互过程,尤其强调了地址传输后的应答机制和每次字节交换后是否需要确认的重要性。

为了更直观地对比不同工作模式下的电气特性,以下表格列出了常见I2C模式的关键参数:

模式 最大时钟频率 典型上拉电阻 总线电容限制 应用场景
标准模式(Standard-mode) 100 kHz 4.7 kΩ ≤400 pF 通用传感器通信
快速模式(Fast-mode) 400 kHz 2.2 kΩ ≤400 pF 中等速率外设
快速模式+(Fast-mode Plus) 1 MHz 1.5–2.2 kΩ ≤550 pF 高性能触摸屏
高速模式(High-speed mode) 3.4 MHz 2.2 kΩ(主设备专用) ≤100 pF 极低延迟需求

值得注意的是,高速模式引入了额外的主设备驱动能力和仲裁机制,且通常与其他模式共存时需使用特定切换序列,实际应用较少见于普通嵌入式平台。

2.1.2 起始/停止条件、应答机制与数据传输格式

I2C通信的启动与终止依赖于特殊的电平跳变组合,称为 起始条件(Start Condition) 停止条件(Stop Condition) 。它们并不依赖额外的控制线,而是通过对SDA和SCL的状态变化定义而成。

  • 起始条件 :当SCL保持高电平时,SDA出现一个下降沿(从高到低),表示一次新的通信开始。
  • 停止条件 :当SCL保持高电平时,SDA出现一个上升沿(从低到高),表示本次通信结束。

这两个条件具有全局意义,所有挂接在总线上的设备都会检测到,并据此判断当前是否正在被寻址或通信是否已结束。

在每一次字节传输完成后(无论是地址还是数据),接收方必须返回一个 应答位(ACK) 非应答位(NACK) 。具体方式如下:
- 若接收方成功接收到一个字节,它会在第9个时钟周期将SDA拉低(ACK),表示“我收到了,请继续”;
- 若接收方无法接收更多数据(如缓冲区满、设备忙或地址不匹配),则让SDA保持高电平(NACK),主设备可根据此信号决定是否重试或终止通信。

每个数据帧由若干字节构成,每字节包含8位数据加1位应答位。完整的传输结构通常如下所示:

[Start] → [Slave_Address + R/W] → [ACK] → [Data_Byte_1] → [ACK] → ... → [Data_Byte_n] → [ACK/NACK] → [Stop]

对于写操作,主设备发送目标从机地址(7位或10位)后紧跟一个“0”表示写方向;随后发送一系列数据。对于读操作,主设备发送地址后跟“1”,然后由从设备逐字节回传数据。

下面是一个典型的I2C写寄存器操作的伪代码示例:

// 向从设备0x5A写入寄存器0x10的值0xFF
i2c_start();
i2c_write(0x5A << 1 | 0);  // 地址左移+写标志
if (!i2c_read_ack()) {
    printk("Device not responding\n");
    goto error;
}
i2c_write(0x10);           // 寄存器地址
i2c_read_ack();
i2c_write(0xFF);           // 写入的数据
i2c_read_ack();
i2c_stop();
逻辑分析与参数说明:
  • i2c_start() :触发起始条件,使总线进入活动状态。其实现依赖于对GPIO的精确时序控制或调用硬件控制器API。
  • (0x5A << 1) | 0 :将7位从机地址左移一位,最低位设置为0表示写操作。若为读操作则设为1。
  • i2c_read_ack() :读取第9个时钟周期内SDA的状态。若为低电平说明从设备已正确响应。
  • i2c_write(byte) :逐位发送一个字节,每位在SCL低电平时设置SDA,在SCL高电平时采样。
  • i2c_stop() :释放总线,发出停止条件,允许其他主设备访问。

上述代码虽然适用于裸机环境下的模拟I2C(bit-banging),但在Linux内核中应优先使用 i2c_transfer() 接口封装,避免直接操作引脚。

此外,I2C还支持 重复起始条件(Repeated Start) ,即在一个通信流中不释放总线而直接发起新的地址传输。这常用于先写寄存器地址再立即读取其内容的操作,例如:

[Start] → [Addr+W] → [Reg] → [ReStart] → [Addr+R] → [Read Data] → [Stop]

这种方式保证了两次操作之间不会被其他主设备打断,提高了原子性和可靠性。

2.1.3 地址寻址模式(7位与10位)及多设备共存机制

I2C支持两种地址长度: 7位地址模式 10位地址模式 ,其中7位最为常见。两者的主要区别体现在地址字段的编码方式和兼容性处理上。

在7位寻址中,地址占用7个比特,加上第8位作为读/写标志,形成一个字节的地址帧。例如,常见触摸芯片FT6X36的I2C地址为0x38(7位),则写操作地址为0x70(0x38<<1),读操作为0x71。

而在10位寻址中,地址扩展为10位,其传输分为两个阶段:
1. 第一阶段:发送特殊前缀 11110XX ,其中 XX 是10位地址的最高两位;
2. 第二阶段:发送剩余8位地址。

由于10位模式较为复杂且多数设备仍使用7位地址,Linux内核默认以7位模式为主。

多个设备可以在同一I2C总线上共存,前提是它们拥有不同的地址。然而,实践中常遇到地址冲突问题,例如多个EEPROM芯片出厂预设相同地址。解决方案包括:
- 使用可配置地址引脚(如A0、A1)更改设备物理地址;
- 添加I2C多路复用器(如PCA9548)分时切换通道;
- 在软件层面通过设备树明确指定每个节点的reg属性,防止重复注册。

Linux内核通过 struct i2c_client 中的 addr 字段记录每个设备的7位地址(无需包含读写位),并在探测过程中验证其是否存在。

综上所述,深入理解I2C的信号时序、应答机制和地址规则,不仅是开发可靠驱动的前提,也是定位通信故障的基础。接下来章节将进一步剖析Linux如何抽象和管理这一协议栈。

2.2 Linux内核中的I2C子系统架构

2.2.1 I2C核心层(i2c-core)、适配器层(i2c-adaptor)与客户端驱动层(i2c-client/driver)

Linux内核将I2C子系统划分为三个逻辑层次: 核心层(i2c-core) 适配器层(i2c-adapter) 客户驱动层(i2c-client/i2c-driver) ,体现了典型的分层抽象思想。这种设计不仅提升了代码复用率,也增强了系统的可扩展性和可维护性。

  • i2c-core :位于 drivers/i2c/i2c-core.c ,是整个I2C框架的核心中枢。它提供了统一的注册接口、设备匹配机制、总线管理功能,并向上层驱动暴露标准化的API(如 i2c_transfer i2c_smbus_read_byte_data 等)。同时负责协调适配器与客户端之间的绑定关系。

  • i2c-adapter :代表具体的I2C控制器硬件(如全志H3的TWI模块),封装了底层通信能力。每个适配器对应一个物理I2C总线实例(如i2c-0、i2c-1),并通过 struct i2c_adapter 描述。该结构体继承自 struct device ,具备设备模型属性。

  • i2c-client i2c-driver :分别表示挂载在总线上的从设备及其驱动程序。 i2c_client 描述一个实际存在的I2C外设(如触摸屏芯片wdt87xx),而 i2c_driver 则是对应的驱动逻辑,包含probe、remove等回调函数。

三者之间的关系可通过如下mermaid类图表示:

classDiagram
    class i2c_core {
        +i2c_add_adapter()
        +i2c_del_adapter()
        +i2c_register_driver()
        +i2c_transfer()
    }
    class i2c_adapter {
        -name: char*
        -algo: i2c_algorithm*
        -dev: struct device
    }
    class i2c_client {
        -addr: u16
        -adapter: i2c_adapter*
        -dev: struct device
        -driver: i2c_driver*
    }
    class i2c_driver {
        -probe: function()
        -remove: function()
        -id_table: const struct i2c_device_id*
        -driver.of_match_table: const struct of_device_id*
    }

    i2c_core <|-- i2c_adapter : registers
    i2c_core <|-- i2c_driver : registers
    i2c_adapter "1" --> "n" i2c_client : controls
    i2c_driver "1" --> "n" i2c_client : binds to

该图揭示了各组件之间的依赖与协作机制。例如, i2c_adapter 由平台驱动创建并注册至核心层; i2c_client 可在设备树中静态声明或动态添加; i2c_driver 通过 i2c_register_driver() 向核心注册,之后由核心自动尝试与现有或未来出现的client匹配。

以下是 struct i2c_adapter 的关键字段说明:

字段 类型 说明
name char* 用户可见的适配器名称,如 “sunxi_i2c.0”
algo struct i2c_algorithm* 指向底层算法结构,定义如何执行数据传输
dev struct device 继承设备模型,支持sysfs属性展示
nr int 总线编号,-1表示动态分配

i2c_algorithm 结构体则定义了底层通信的具体实现:

struct i2c_algorithm {
    int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);
    int (*smbus_xfer)(struct i2c_adapter *adap, u16 addr, unsigned short flags,
                      char read_write, u8 command, int size, union i2c_smbus_data *data);
    u32 (*functionality)(struct i2c_adapter *);
};

其中 master_xfer 是最关键的函数指针,用于执行一组 i2c_msg 消息的传输。每个 i2c_msg 代表一个独立的数据帧,包含地址、标志、长度和数据缓冲区。

2.2.2 总线驱动与设备驱动分离设计思想

Linux内核坚持“总线-设备-驱动”模型,I2C子系统完美践行了这一理念。其核心思想是: 将硬件平台相关的控制器驱动(Bus Driver)与外设功能驱动(Device Driver)彻底解耦 ,使得同一款传感器驱动可在不同SoC平台上复用,只要适配器层实现了标准接口。

例如,一款基于Goodix GT911的触摸屏驱动无需关心它是接在全志H6的I2C2上还是瑞芯微RK3399的I2C5上,只需声明compatible字符串并与之匹配即可。这种松耦合设计极大提升了驱动生态的可移植性。

这种分离带来的另一个好处是支持 热插拔与动态探测 。当一个新的I2C设备被添加(如通过设备树或用户空间工具),核心层会遍历所有已注册的 i2c_driver ,检查其 of_match_table id_table 是否匹配新设备的属性。一旦匹配成功,立即调用该驱动的 probe() 函数完成初始化。

反之,若设备被移除(如断电或卸载模块),则调用 remove() 清理资源。整个过程由内核自动调度,无需应用程序干预。

此外,该架构支持多种设备描述方式:
- 静态声明 :在设备树中定义 i2c@1c2ac00 节点及其子节点;
- 动态注册 :通过 i2c_new_client_device() 手动创建client;
- ACPI枚举 :在x86平台中使用ACPI表描述设备。

这种灵活性使得I2C子系统既能服务于传统的嵌入式ARM平台,也能兼容桌面级系统。

2.2.3 i2c_add_adapter与i2c_new_client_device接口详解

在平台驱动初始化过程中,通常需要调用 i2c_add_adapter() 将一个 i2c_adapter 实例注册到内核。该函数原型如下:

int i2c_add_adapter(struct i2c_adapter *adapter);

其主要作用包括:
- 分配唯一的总线编号(若 nr == -1 );
- 注册到全局适配器链表;
- 创建对应的 /sys/class/i2c-adapter/i2c-X 目录;
- 触发uevent通知用户空间;
- 尝试匹配已存在的i2c_driver。

示例代码片段:

static int sunxi_i2c_probe(struct platform_device *pdev)
{
    struct i2c_adapter *adap;
    int ret;

    adap = devm_kzalloc(&pdev->dev, sizeof(*adap), GFP_KERNEL);
    if (!adap)
        return -ENOMEM;

    adap->owner = THIS_MODULE;
    adap->class = I2C_CLASS_HWMON;
    adap->dev.parent = &pdev->dev;
    adap->algo = &sunxi_i2c_algo;  // 指向自定义传输函数
    adap->retries = 3;
    strscpy(adap->name, "sunxi_i2c", sizeof(adap->name));

    platform_set_drvdata(pdev, adap);

    ret = i2c_add_adapter(adap);
    if (ret)
        return ret;

    dev_info(&pdev->dev, "I2C adapter %s registered\n", adap->name);
    return 0;
}
参数说明与逻辑分析:
  • devm_kzalloc :使用设备资源管理机制分配内存,设备卸载时自动释放;
  • adap->algo :指向实现 master_xfer 等函数的算法结构,通常由SoC厂商提供;
  • i2c_add_adapter :若成功返回0,失败返回负错误码(如-EINVAL、-ENODEV);
  • 注册后可通过 ls /sys/class/i2c-adapter/ 查看新设备。

另一方面, i2c_new_client_device() 用于在运行时动态创建一个I2C客户端设备。常见于某些无法通过设备树描述的场景,或用于测试目的。

struct i2c_client *
i2c_new_client_device(struct i2c_adapter *adap, struct i2c_board_info const *info);

其中 i2c_board_info 描述设备基本信息:

struct i2c_board_info {
    char type[I2C_NAME_SIZE];     // 设备类型名,如"wdt87xx"
    __u16 addr;                   // 7位从机地址
    void *platform_data;          // 私有数据指针
    struct dev_archdata *archdata;
    int irq;                      // 中断号
};

调用示例:

static void __init test_i2c_device_init(void)
{
    struct i2c_board_info info = {
        I2C_BOARD_INFO("test_eeprom", 0x50),
        .irq = gpio_to_irq(IRQ_GPIO_PIN),
    };
    struct i2c_client *client;

    client = i2c_new_probed_device(i2c_get_adapter(1), &info, NULL, NULL);
    if (!client)
        pr_err("Failed to register test EEPROM\n");
}

注意: i2c_new_probed_device 会在注册前尝试探测设备是否存在(发送地址并等待ACK),提高健壮性。

综上,掌握 i2c_add_adapter i2c_new_client_device 的使用方法,是构建完整I2C驱动生态的关键技能。

3. 全志平台硬件架构与驱动适配

全志科技(Allwinner Technology)作为国内领先的嵌入式SoC设计厂商,其H3、H6、R329等系列芯片广泛应用于智能电视盒、工业控制终端、教育平板及物联网设备中。这些SoC集成了丰富的外设控制器,其中I2C总线模块在连接触摸屏、传感器、音频编解码器等低速外设方面扮演着关键角色。然而,不同系列芯片的I2C控制器在寄存器布局、时钟源选择、中断机制等方面存在差异,给驱动开发带来了适配挑战。本章将深入剖析全志平台I2C控制器的硬件特性,结合设备树(Device Tree)机制详解如何正确描述和配置触摸屏设备,并通过平台驱动模型实现资源获取与初始化流程的精准控制。

3.1 全志SoC平台I2C控制器特性

全志SoC中的I2C控制器并非统一设计,而是根据产品定位进行了差异化优化。以主流的H3、H6和R329为例,它们均支持标准I2C协议,但在功能扩展、性能参数及内核支持上各有侧重。理解这些差异是编写可移植性强、稳定性高的驱动程序的前提。

3.1.1 全志H3/H6/R329等系列芯片I2C模块功能差异

芯片型号 I2C通道数 最高工作频率 DMA支持 中断类型 典型应用场景
H3 4 400 kHz 不支持 边沿触发 普通外设通信(如TP、RTC)
H6 6 1 MHz 支持 电平/边沿可配 高速传感器、多点触控屏
R329 5 400 kHz 支持 边沿触发 AI语音设备、带触摸交互的产品

从表中可见,H6系列提供了更高的传输速率和DMA能力,适合需要批量读取触摸数据的场景;而H3虽无DMA,但因其成本优势仍被大量使用于入门级设备中。R329则针对AIoT应用做了优化,在音频子系统之外也保留了完整的I2C接口用于人机交互。

以H6为例,其I2C控制器具备以下特点:
- 支持主模式下的快速模式+(Fast-mode Plus),即最高1Mbps通信速率;
- 内置FIFO缓冲区(通常为8字节),减少CPU中断负担;
- 可配置中断触发方式(高电平或上升沿),便于与外部设备协调;
- 使用独立的时钟门控(Clock Gating),可在空闲时关闭时钟节省功耗。

相比之下,H3的I2C控制器结构更为简单,仅提供基本的起始/停止信号生成、地址发送和单字节收发功能,所有数据传输依赖CPU轮询或短时间中断处理。因此,在高刷新率触摸屏应用中容易造成CPU负载过高。

// 示例:根据芯片类型判断是否启用DMA模式
static bool should_use_dma(struct i2c_adapter *adap)
{
    struct sunxi_i2c *i2c = i2c_get_adapdata(adap);

    if (of_device_is_compatible(i2c->dev->of_node, "allwinner,sun50i-h6-i2c"))
        return true;  // H6支持DMA
    else if (of_device_is_compatible(i2c->dev->of_node, "allwinner,sun8i-h3-i2c"))
        return false; // H3不支持DMA
    return false;
}

代码逻辑逐行解析:
1. i2c_get_adapdata(adap) :获取绑定到该I2C适配器的私有数据结构指针,通常是 struct sunxi_i2c *
2. of_device_is_compatible() :利用设备树节点中的 compatible 属性进行匹配,判断当前运行的是哪个SoC平台。
3. 若为H6,则返回 true 表示可以启用DMA传输路径;否则返回 false ,采用传统中断方式处理。

此函数可用于驱动内部动态决策数据传输策略,提升跨平台兼容性。

3.1.2 寄存器布局与时钟配置机制

全志I2C控制器的寄存器映射遵循统一风格,通常位于内存映射的专用区域(如H3中为 0x01c20800 )。主要寄存器包括:

偏移地址 名称 功能说明
0x00 I2C_CTL 控制寄存器:启停、中断使能、主从模式设置
0x04 I2C_STATUS 状态寄存器:反映当前总线状态(如忙、错误)
0x08 I2C_ADDR 本地从地址(仅从机模式使用)
0x0C I2C_XADDR 扩展地址(用于10位寻址)
0x10 I2C_DATA 数据寄存器:读写操作的数据缓存
0x14 I2C_CLKM 时钟分频系数M值
0x18 I2C_CLKN 时钟分频系数N值

时钟配置的核心在于计算合适的 CLKM CLKN 值,使得输出SCL频率满足目标速率。公式如下:

f_{SCL} = \frac{f_{input}}{2^{N} \times (M + 1)}

其中 $ f_{input} $ 是输入时钟频率(例如24MHz),$ N \in [0,7] $,$ M \in [0,15] $。驱动需根据用户请求的速率(如400kHz)自动求解最优参数组合。

static int sunxi_i2c_set_clock(struct sunxi_i2c *i2c, unsigned int rate)
{
    unsigned long input_clk = clk_get_rate(i2c->clk);
    int n, m;
    int best_n = 0, best_m = 0;
    unsigned int best_diff = UINT_MAX;

    for (n = 0; n < 8; n++) {
        for (m = 0; m < 16; m++) {
            unsigned int scl = input_clk / ((1 << n) * (m + 1));
            int diff = abs(scl - rate);
            if (diff < best_diff) {
                best_diff = diff;
                best_n = n;
                best_m = m;
            }
        }
    }

    writel(best_m, i2c->base + I2C_CLKM);
    writel(best_n, i2c->base + I2C_CLKN);

    return 0;
}

参数说明:
- rate :期望的SCL频率(单位Hz)
- input_clk :通过 clk_get_rate() 获取的实际输入时钟频率
- 循环遍历所有可能的M/N组合,寻找最接近目标频率的解

该算法确保即使在不同晶振环境下也能自适应调整,避免硬编码带来的移植问题。

3.1.3 DMA支持与中断处理机制

在H6等高端平台上,I2C控制器可通过DMA实现高效数据搬运,尤其适用于连续读取多字节触摸数据包的场景。启用DMA的基本流程如下:

graph TD
    A[开始I2C传输] --> B{是否启用DMA?}
    B -- 是 --> C[配置DMA通道]
    C --> D[启动DMA传输]
    D --> E[等待DMA完成中断]
    E --> F[处理I2C事务完成]
    B -- 否 --> G[进入中断轮询模式]
    G --> H[每次中断读写一个字节]
    H --> I{是否完成?}
    I -- 否 --> H
    I -- 是 --> F

当启用DMA时,驱动需提前申请DMA通道并准备SG(Scatter-Gather)列表。Linux内核中常用 dmaengine 接口完成此类操作:

static int sunxi_i2c_dma_xfer(struct sunxi_i2c *i2c, struct i2c_msg *msg)
{
    struct dma_async_tx_descriptor *txd;
    dma_cookie_t cookie;

    if (msg->flags & I2C_M_RD) {
        txd = dmaengine_prep_slave_single(i2c->rx_chan,
                                          msg->addr, msg->len,
                                          DMA_DEV_TO_MEM,
                                          DMA_PREP_INTERRUPT);
    } else {
        txd = dmaengine_prep_slave_single(i2c->tx_chan,
                                          msg->addr, msg->len,
                                          DMA_MEM_TO_DEV,
                                          DMA_PREP_INTERRUPT);
    }

    if (!txd)
        return -ENOMEM;

    txd->callback = sunxi_i2c_dma_complete;
    txd->callback_param = i2c;
    cookie = dmaengine_submit(txd);
    dma_async_issue_pending(i2c->rx_chan); // 或 tx_chan

    return 0;
}

逻辑分析:
- 根据消息方向(读/写)选择对应的DMA通道( rx_chan tx_chan
- 调用 dmaengine_prep_slave_single 准备一次单次DMA传输
- 设置回调函数 sunxi_i2c_dma_complete ,用于通知I2C核心层传输完成
- 提交任务并触发DMA引擎执行

相比传统中断方式每字节触发一次中断,DMA显著降低了中断频率,提升了系统整体响应能力。

3.2 设备树在全志平台中的实现

设备树(Device Tree)是现代Linux内核中描述硬件拓扑的标准机制,尤其在全志平台中发挥着核心作用。它不仅定义了I2C总线的存在与否,还精确刻画了挂载在其上的具体设备及其物理连接关系。

3.2.1 sunxi设备树源文件组织结构

全志平台的设备树源文件( .dtsi .dts )通常存放于内核源码目录 arch/arm/boot/dts/ 下,命名规则为 sunXi-xxxx.dts[i] 。例如:

  • sun8i-h3.dtsi :H3 SoC通用定义
  • sun50i-h6.dtsi :H6 SoC通用定义
  • myboard-h3.dts :基于H3的定制开发板描述

这些文件采用包含关系构建层级结构:

// myboard-h3.dts
#include "sun8i-h3.dtsi"

/ {
    model = "My Custom Board with H3";
    compatible = "mycompany,myboard-h3", "allwinner,sun8i-h3";

    chosen {
        stdout-path = "serial0:115200n8";
    };

    fragment@0 {
        target = <&i2c1>;
        __overlay__ {
            status = "okay";
            touchscreen: wdt87xx@3b {
                compatible = "weidongshan,wdt87xx";
                reg = <0x3b>;
                interrupt-parent = <&pio>;
                interrupts = <RK_PD1 IRQ_TYPE_EDGE_FALLING>;
                interrupt-controller;
                #address-cells = <1>;
                #size-cells = <0>;
                vddio-supply = <&reg_dc1sw>;
                pinctrl-names = "default";
                pinctrl-0 = <&ts_int_pin &ts_rst_pin>;
            };
        };
    };
};

上述片段展示了如何在设备树中添加一个I2C触摸屏设备节点。关键字段解释如下:

属性名 说明
compatible 驱动匹配依据,格式为“制造商,型号”
reg I2C设备地址(此处为0x3B)
interrupts 中断引脚编号及触发方式
vddio-supply 引用供电 regulator,确保电源先于probe开启
pinctrl-0 引脚复用配置,防止GPIO冲突

3.2.2 添加I2C触摸屏设备节点(compatible、reg、interrupts属性设置)

为了使内核能够正确识别并加载wdt87xx触摸屏驱动,必须在设备树中正确定义相关属性。

&i2c1 {
    clock-frequency = <400000>; /* 设置I2C总线速率为400kHz */
    status = "okay";

    wdt87xx_ts: wdt87xx@3b {
        compatible = "weidongshan,wdt87xx";
        reg = <0x3b>;
        interrupt-parent = <&pio>;
        interrupts = <RK_PD1 2>; /* PD1引脚,下降沿触发 */
        pinctrl-names = "default";
        pinctrl-0 = <&touch_int &touch_rst>;
        vddio-supply = <&reg_usb0_vbus>;
        wakeup-source; /* 支持唤醒系统 */
    };
};

参数说明:
- clock-frequency :若未指定,默认可能为100kHz,影响响应速度
- wakeup-source :允许该设备在休眠状态下唤醒系统(如轻触唤醒屏幕)

驱动侧通过 of_match_table 进行匹配:

static const struct of_device_id wdt87xx_of_match[] = {
    { .compatible = "weidongshan,wdt87xx", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, wdt87xx_of_match);

只有 compatible 完全一致时, probe() 函数才会被调用。

3.2.3 Pinctrl引脚复用配置与供电引脚控制(vddio-supply)

全志SoC的引脚具有多种复用功能,必须通过pinctrl子系统将其配置为I2C或GPIO模式。

&pio {
    touch_int: touch-int-pin {
        pins = "PD1";
        function = "gpio_in";
        bias-pull-up;
    };

    touch_rst: touch-rst-pin {
        pins = "PD2";
        function = "gpio_out";
    };
};

此外,许多触摸芯片需要独立IO电压(如VDDIO=2.8V),应通过 regulator 机制管理:

&regulators {
    usb0_vbus: usb0-vbus-regulator {
        compatible = "regulator-fixed";
        regulator-name = "vcc-usb0";
        regulator-min-microvolt = <5000000>;
        regulator-max-microvolt = <5000000>;
        gpio = <&pio PD3 0>;
        enable-active-high;
    };
};

驱动中可通过 devm_regulator_get() 获取并使能电源:

struct regulator *vddio;
vddio = devm_regulator_get(&client->dev, "vddio");
if (IS_ERR(vddio))
    return PTR_ERR(vddio);
regulator_enable(vddio);

这保证了在 probe() 执行前电源已稳定,避免因供电不足导致探测失败。

3.3 平台驱动与设备匹配过程

Linux内核采用platform bus机制管理SoC内部集成的非即插即用设备,I2C控制器正是典型代表。掌握platform driver与device的绑定流程,是调试驱动加载异常的关键。

3.3.1 platform_driver与platform_device绑定机制

在设备树被解析后,内核会为每个 status="okay" 的节点创建 platform_device ,并与注册的 platform_driver 进行匹配。

static struct platform_driver sunxi_i2c_driver = {
    .probe = sunxi_i2c_probe,
    .remove = sunxi_i2c_remove,
    .driver = {
        .name = "sunxi-i2c",
        .of_match_table = sunxi_i2c_of_match,
        .pm = &sunxi_i2c_pm_ops,
    },
};
module_platform_driver(sunxi_i2c_driver);

当设备树中有节点 compatible = "allwinner,sun8i-h3-i2c" 时, of_match_table 匹配成功,触发 probe() 调用。

3.3.2 clk、reset、pinctrl资源获取(devm_clk_get、devm_reset_control_get)

probe() 函数中,必须依次获取各项硬件资源:

static int sunxi_i2c_probe(struct platform_device *pdev)
{
    struct resource *res;
    struct sunxi_i2c *i2c;
    int ret;

    i2c = devm_kzalloc(&pdev->dev, sizeof(*i2c), GFP_KERNEL);
    i2c->dev = &pdev->dev;

    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    i2c->base = devm_ioremap_resource(&pdev->dev, res);

    i2c->clk = devm_clk_get(&pdev->dev, NULL);
    clk_prepare_enable(i2c->clk);

    i2c->rst = devm_reset_control_get_shared(&pdev->dev, NULL);
    reset_control_deassert(i2c->rst);

    i2c->pinctrl = devm_pinctrl_get(&pdev->dev);
    pinctrl_select_state(i2c->pinctrl, pinctrl_lookup_state(i2c->pinctrl, "default"));

    i2c_set_adapdata(&i2c->adapter, i2c);
    ret = i2c_add_adapter(&i2c->adapter);
    if (ret)
        return ret;

    platform_set_drvdata(pdev, i2c);
    return 0;
}

资源获取说明:
- devm_clk_get :获取门控时钟, devm_ 前缀表示设备移除时自动释放
- reset_control_deassert :解除复位状态,使模块进入工作模式
- pinctrl_select_state :激活默认引脚配置,防止功能错乱

3.3.3 GPIO中断引脚申请与边沿触发配置(request_threaded_irq)

触摸屏通常使用专用中断引脚通知主机有触摸事件发生。驱动需注册中断服务例程:

static int wdt87xx_request_irq(struct i2c_client *client)
{
    struct wdt87xx_data *ts = i2c_get_clientdata(client);
    int irq;

    irq = client->irq;
    return devm_request_threaded_irq(&client->dev, irq,
                                     wdt87xx_irq_handler, // 上半部
                                     wdt87xx_irq_thread,  // 下半部
                                     IRQF_TRIGGER_FALLING,
                                     client->name, ts);
}

采用 threaded_irq 的原因是I2C读取属于睡眠型操作(不能在原子上下文中调用),必须由内核线程上下文执行。

3.4 实践:全志平台上电初始化流程调试

驱动开发中最常见的问题是 probe() 失败,往往源于电源、时钟或引脚配置错误。

3.4.1 利用printk和ftrace跟踪驱动加载顺序

在关键位置插入 dev_info() 输出日志:

dev_info(&client->dev, "Probing wdt87xx touchscreen...\n");

配合 ftrace 工具可查看完整调用栈:

echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
dmesg | grep wdt87xx
cat /sys/kernel/debug/tracing/trace_pipe

3.4.2 使用示波器观测I2C SDA/SCL波形验证通信启动

若怀疑I2C未发出起始信号,可用示波器探头连接SDA/SCL引脚,观察是否有预期波形。正常情况下,在 i2c_transfer() 调用后应看到:

  • 起始条件(SCL高→SDA由高变低)
  • 发送设备地址+写标志(0x3B << 1 | 0)
  • 接收ACK信号(SDA被拉低)

3.4.3 解决因电源未使能导致的probe失败问题

常见错误日志:

wdt87xx i2c-3b: Failed to read chip ID: -110 (timeout)

原因往往是 vddio-supply 未正确配置或regulator未enable。解决方案:

  1. 检查设备树中 vddio-supply = <&xxx> 引用是否存在
  2. 确认regulator节点已定义且 status="okay"
  3. probe() 中显式调用 regulator_enable()

最终确保硬件加电早于任何I2C操作,方可顺利完成设备识别与初始化。

4. 触摸屏驱动核心逻辑设计与实现

在嵌入式Linux系统中,触摸屏作为人机交互的核心外设之一,其驱动程序不仅要完成硬件通信的建立,还需将原始触控数据转换为操作系统可理解的输入事件。本章围绕WDT87XX系列电容式触摸控制器,深入剖析基于I2C总线和input子系统的驱动开发全过程。从模块注册、设备初始化到中断处理、多点触控上报机制,再到电源管理与调试接口的设计,逐步构建一个稳定、高效且符合内核规范的触摸屏驱动框架。重点聚焦于驱动如何通过I2C协议获取触摸状态,并借助input子系统向用户空间传递精准的坐标信息,同时兼顾低功耗场景下的suspend/resume行为控制。

4.1 驱动模块注册与初始化

驱动程序的入口是整个模块运行的起点,决定了内核何时加载该驱动以及如何与其他子系统进行交互。在Linux内核中,设备驱动通常以模块形式存在,通过 module_init() 宏指定初始化函数,由内核调度执行。对于I2C设备驱动而言,必须遵循I2C子系统的注册机制,正确填充 i2c_driver 结构体并实现关键回调函数。

4.1.1 module_init宏展开与initcall_level选择

module_init() 宏用于声明驱动模块的初始化入口函数。其本质是对 __initcall() 宏的封装,最终将函数指针注册到特定的.initcall段中,由内核启动时按优先级顺序调用。不同的 initcall_level 决定了驱动加载时机,常见级别包括:

级别 宏定义 加载阶段 适用场景
pure_initcall pure_initcall(fn) 内核最早期 极少使用,仅用于核心基础设施
core_initcall core_initcall(fn) 核心子系统初始化后 总线控制器(如I2C适配器)
postcore_initcall postcore_initcall(fn) core之后 平台总线相关驱动
arch_initcall arch_initcall(fn) 架构特定初始化 SoC平台驱动
subsys_initcall subsys_initcall(fn) 子系统准备就绪 I2C、SPI等核心子系统驱动
fs_initcall fs_initcall(fn) 文件系统挂载前 设备节点依赖文件系统的驱动
device_initcall device_initcall(fn) 设备模型可用后 多数设备驱动推荐使用
late_initcall late_initcall(fn) 用户空间启动前 热插拔或延迟初始化设备

对于触摸屏这类依赖I2C总线和input子系统的外设驱动,应选择 subsys_initcall 或更晚的级别。但由于现代内核普遍采用设备树动态匹配机制,实际常用 module_init() 默认绑定至 device_initcall 层级即可确保所有底层资源已准备完毕。

static int __init wdt87xx_ts_init(void)
{
    return i2c_add_driver(&wdt87xx_i2c_driver);
}
module_init(wdt87xx_ts_init);

static void __exit wdt87xx_ts_exit(void)
{
    i2c_del_driver(&wdt87xx_i2c_driver);
}
module_exit(wdt87xx_ts_exit);

代码逻辑逐行分析
- __init :表示该函数仅在初始化阶段使用,之后释放内存。
- i2c_add_driver() :向I2C子系统注册驱动,触发设备匹配流程。
- module_init() :将 wdt87xx_ts_init 注册为模块初始化入口。
- __exit :标记退出函数,模块卸载时调用。
- i2c_del_driver() :注销驱动,释放资源。

此机制保证了驱动在系统运行后期被加载,避免因I2C总线未就绪导致probe失败。

4.1.2 i2c_driver结构体定义

i2c_driver 是I2C客户端驱动的核心数据结构,包含探测、移除、设备匹配等关键字段。以下是WDT87XX驱动中的典型定义:

static const struct of_device_id wdt87xx_of_match[] = {
    { .compatible = "weidongtech,wdt87xx", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, wdt87xx_of_match);

static const struct i2c_device_id wdt87xx_id[] = {
    { "wdt87xx", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, wdt87xx_id);

static struct i2c_driver wdt87xx_i2c_driver = {
    .driver = {
        .name   = "wdt87xx_ts",
        .of_match_table = wdt87xx_of_match,
        .owner  = THIS_MODULE,
    },
    .probe_new  = wdt87xx_ts_probe,
    .remove     = wdt87xx_ts_remove,
    .id_table   = wdt87xx_id,
};

参数说明
- .name :驱动名称,需唯一标识。
- .of_match_table :用于设备树匹配,决定是否调用probe。
- .owner :模块拥有者,防止过早卸载。
- .probe_new :新式探测函数(替代旧probe),接收 struct i2c_client * const struct i2c_device_id *
- .id_table :传统ID表,兼容非设备树环境。
- MODULE_DEVICE_TABLE() :生成模块别名,支持自动加载。

当设备树中存在 compatible = "weidongtech,wdt87xx" 节点时,内核会尝试将其与 .of_match_table 匹配,成功则调用 .probe_new 函数。

4.1.3 动态分配struct wdt87xx_data私有数据结构

为了管理驱动运行时状态,需为每个设备实例分配私有数据结构。该结构通常包含I2C客户端指针、input设备指针、锁、工作队列及硬件配置信息。

struct wdt87xx_data {
    struct i2c_client *client;
    struct input_dev *input_dev;
    struct mutex lock;
    struct work_struct work;
    bool suspended;
    u8 firmware_ver;
    int max_x, max_y;
    int irq_gpio;
};

probe 函数中动态分配:

static int wdt87xx_ts_probe(struct i2c_client *client,
                           const struct i2c_device_id *id)
{
    struct wdt87xx_data *data;
    struct input_dev *input_dev;

    data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;

    input_dev = devm_input_allocate_device(&client->dev);
    if (!input_dev)
        return -ENOMEM;

    data->client = client;
    data->input_dev = input_dev;
    mutex_init(&data->lock);
    INIT_WORK(&data->work, wdt87xx_work_handler);

    i2c_set_clientdata(client, data);

    /* 其他初始化省略 */
    return 0;
}

逻辑分析
- devm_kzalloc() :带资源管理的内存分配,设备卸载时自动释放。
- devm_input_allocate_device() :安全申请input设备。
- mutex_init() :初始化互斥锁,保护并发访问。
- INIT_WORK() :初始化工作队列,用于底半部处理。
- i2c_set_clientdata() :绑定私有数据与i2c_client,后续可通过 i2c_get_clientdata() 获取。

graph TD
    A[module_init] --> B[i2c_add_driver]
    B --> C{I2C设备存在?}
    C -->|Yes| D[匹配of_match_table]
    D --> E[调用probe_new]
    E --> F[分配wdt87xx_data]
    F --> G[申请input_dev]
    G --> H[注册input设备]
    H --> I[请求中断]
    I --> J[驱动就绪]

上述流程清晰展示了从模块加载到驱动初始化完成的关键路径,体现了Linux设备驱动“分离与匹配”的设计理念。

4.2 input子系统集成

input子系统是Linux中统一处理输入事件(按键、鼠标、触摸等)的核心框架。它屏蔽了底层硬件差异,向上层提供标准化的事件接口(/dev/input/eventX)。触摸屏驱动必须正确配置input设备属性,才能使GUI系统准确识别触控行为。

4.2.1 struct input_dev申请与生命周期管理

struct input_dev 代表一个输入设备,需通过 input_allocate_device() 创建,并在不再需要时由 input_free_device() 释放。推荐使用 devm_ 系列函数实现自动资源管理:

input_dev = devm_input_allocate_device(&client->dev);
if (!input_dev) {
    dev_err(&client->dev, "Failed to allocate input device\n");
    return -ENOMEM;
}

设置基本属性:

input_dev->name = "wdt87xx-touchscreen";
input_dev->id.bustype = BUS_I2C;
input_dev->id.vendor = 0x1234;
input_dev->id.product = 0x0087;
input_dev->id.version = 0x0100;

参数说明
- .name :设备名称,出现在 /proc/bus/input/devices 中。
- .bustype :总线类型,I2C设备设为 BUS_I2C
- .vendor/product/version :用于udev规则匹配或应用程序识别。

最后调用 input_register_device() 完成注册:

error = input_register_device(data->input_dev);
if (error) {
    dev_err(&client->dev, "Unable to register input device: %d\n", error);
    return error;
}

一旦注册成功,内核会自动生成对应的event节点(如 /dev/input/event0 ),供用户空间读取。

4.2.2 设置输入事件类型与绝对坐标范围

触摸屏属于绝对坐标设备,需启用 EV_ABS 事件类型,并设定X/Y轴的最大最小值:

__set_bit(EV_ABS, input_dev->evbit);
__set_bit(EV_KEY, input_dev->evbit);
__set_bit(BTN_TOUCH, input_dev->keybit);

input_set_abs_params(input_dev, ABS_X, 0, 4095, 0, 0);
input_set_abs_params(input_dev, ABS_Y, 0, 4095, 0, 0);

函数解析
- __set_bit(EV_ABS, evbit) :声明支持绝对坐标事件。
- __set_bit(BTN_TOUCH, keybit) :模拟按下事件,某些应用依赖此键判断触摸状态。
- input_set_abs_params() :配置ABS轴参数,原型如下:

c void input_set_abs_params(struct input_dev *dev, unsigned int axis, int min, int max, int fuzz, int flat);

参数 含义
axis 坐标轴(ABS_X/ABS_Y)
min/max 最小/最大值(像素或原始ADC值)
fuzz 抗抖阈值,微小变化不触发上报
flat 中心死区宽度(用于操纵杆)

若屏幕分辨率为800x480,但传感器输出为4096级,则可在应用层进行映射。

4.2.3 多点触摸协议支持

现代电容屏支持多点触控,Linux提供了两种MT协议: MT Legacy (Type A)和 MT Protocol B (Type B)。后者更高效,推荐使用。

启用MT-B协议:

__set_bit(INPUT_PROP_DIRECT, input_dev->propbit); // 直接输入设备(非指针)
__set_bit(EV_ABS, input_dev->evbit);

input_set_abs_params(input_dev, ABS_MT_POSITION_X, 0, 4095, 0, 0);
input_set_abs_params(input_dev, ABS_MT_POSITION_Y, 0, 4095, 0, 0);
input_set_abs_params(input_dev, ABS_MT_TRACKING_ID, 0, 65535, 0, 0);
input_set_abs_params(input_dev, ABS_MT_WIDTH_MAJOR, 0, 255, 0, 0);
input_set_abs_params(input_dev, ABS_MT_PRESSURE, 0, 255, 0, 0);

关键ABS_MT事件说明
- ABS_MT_POSITION_X/Y :单个触点坐标。
- ABS_MT_TRACKING_ID :触点唯一ID,-1表示释放。
- ABS_MT_WIDTH_MAJOR :接触面积大小。
- ABS_MT_PRESSURE :压力值。

上报示例:

void wdt87xx_report_touch(struct wdt87xx_data *data, int id, int x, int y, int pressure)
{
    input_mt_slot(data->input_dev, id);
    input_mt_report_slot_state(data->input_dev, MT_TOOL_FINGER, true);
    input_report_abs(data->input_dev, ABS_MT_POSITION_X, x);
    input_report_abs(data->input_dev, ABS_MT_POSITION_Y, y);
    input_report_abs(data->input_dev, ABS_MT_PRESSURE, pressure);
}

每帧结束调用:

input_mt_sync_frame(data->input_dev);
input_sync(data->input_dev);

input_mt_sync_frame() :标记当前帧MT数据结束,清除未更新的slot。

sequenceDiagram
    participant Kernel
    participant Driver
    participant App
    Driver->>Kernel: input_report_abs(x,y)
    Driver->>Kernel: input_mt_sync_frame()
    Kernel->>Kernel: 缓存事件
    Kernel->>App: read(/dev/input/event0) 返回struct input_event

该流程确保多点触控事件有序传递至用户空间。

4.3 中断处理与数据上报

触摸事件具有突发性和高频特性,直接在中断上下文处理I2C读取可能导致系统延迟。因此需采用“顶半部+底半部”机制解耦响应与处理。

4.3.1 顶半部与底半部机制

顶半部负责快速响应中断,仅做必要操作(如禁用中断、调度底半部);底半部在进程上下文中完成耗时任务(如I2C读取、数据解析、事件上报)。

使用工作队列实现:

static irqreturn_t wdt87xx_irq_handler(int irq, void *dev_id)
{
    struct wdt87xx_data *data = dev_id;

    if (data->suspended)
        return IRQ_HANDLED;

    disable_irq_nosync(irq);
    schedule_work(&data->work);
    return IRQ_HANDLED;
}

static void wdt87xx_work_handler(struct work_struct *work)
{
    struct wdt87xx_data *data =
        container_of(work, struct wdt87xx_data, work);

    wdt87xx_read_touch_data(data); // 实际读取与上报
    enable_irq(data->client->irq);
}

优势对比

机制 是否可睡眠 适用场景
Tasklet 快速软中断处理
工作队列 涉及I/O、内存分配等

此处选择工作队列,因其允许调用 i2c_smbus_read_i2c_block_data() 等可能休眠的函数。

4.3.2 I2C读取触摸寄存器数据包格式解析

假设WDT87XX使用专有协议,状态寄存器位于0x02,数据格式如下:

字节偏移 含义
0x02 触摸点数(0~5)
0x03 状态标志
0x04~0x07 Point0: X低、X高、Y低、Y高

读取函数:

#define WDT87XX_REG_NUM_POINTS  0x02
#define WDT87XX_POINT_SIZE      4
#define MAX_TOUCH_POINTS        5

static int wdt87xx_read_touch_data(struct wdt87xx_data *data)
{
    u8 buf[1 + MAX_TOUCH_POINTS * WDT87XX_POINT_SIZE];
    int points, i, ret;

    ret = i2c_smbus_read_i2c_block_data(data->client,
                                        WDT87XX_REG_NUM_POINTS,
                                        sizeof(buf), buf);
    if (ret < 0) {
        dev_err(&data->client->dev, "Failed to read touch data: %d\n", ret);
        return ret;
    }

    points = buf[0] & 0x0F;
    if (points > MAX_TOUCH_POINTS)
        points = MAX_TOUCH_POINTS;

    for (i = 0; i < points; i++) {
        u8 *p = &buf[4 + i * 4];
        int x = (p[1] << 8) | p[0];
        int y = (p[3] << 8) | p[2];

        wdt87xx_report_touch(data, i, x, y, 100);
    }

    if (points == 0)
        wdt87xx_report_touch(data, 0, 0, 0, 0); // 所有点抬起

    input_mt_sync_frame(data->input_dev);
    input_sync(data->input_dev);

    return 0;
}

参数说明
- i2c_smbus_read_i2c_block_data() :读取连续多个字节。
- buf[0] & 0x0F :提取低4位作为有效触点数。
- (p[1] << 8) | p[0] :合成16位坐标值。
- 循环遍历每个触点并上报。

4.3.3 input_report_abs调用链示例

完整的事件上报链涉及多个层次:

input_report_abs(dev, ABS_MT_POSITION_X, x);
 ↓
input_handle_event() → handler->event()
 ↓
evdev_event() → wake_up_interruptible(&evdev->wait)
 ↓
用户空间read()返回

这一机制保障了事件从驱动直达应用的低延迟传输。

4.4 电源管理与设备热插拔支持

移动设备对功耗敏感,触摸屏应在系统进入休眠时关闭,唤醒时恢复工作状态。

4.4.1 实现suspend/resume回调函数

i2c_driver 中添加PM回调:

#ifdef CONFIG_PM
static int wdt87xx_ts_suspend(struct device *dev)
{
    struct i2c_client *client = to_i2c_client(dev);
    struct wdt87xx_data *data = i2c_get_clientdata(client);

    mutex_lock(&data->lock);
    if (data->suspended)
        goto out_unlock;

    disable_irq(client->irq);
    cancel_work_sync(&data->work);
    data->suspended = true;

    // 可选:发送命令让芯片进入低功耗模式
    i2c_smbus_write_byte_data(client, 0x01, 0x01);

out_unlock:
    mutex_unlock(&data->lock);
    return 0;
}

static int wdt87xx_ts_resume(struct device *dev)
{
    struct i2c_client *client = to_i2c_client(dev);
    struct wdt87xx_data *data = i2c_get_clientdata(client);

    mutex_lock(&data->lock);
    if (!data->suspended)
        goto out_unlock;

    // 唤醒芯片
    i2c_smbus_write_byte(client, 0x00);
    msleep(5);

    data->suspended = false;
    enable_irq(client->irq);

out_unlock:
    mutex_unlock(&data->lock);
    return 0;
}

static const struct dev_pm_ops wdt87xx_pm_ops = {
    .suspend = wdt87xx_ts_suspend,
    .resume  = wdt87xx_ts_resume,
};

#define WDT87XX_PM_OPS (&wdt87xx_pm_ops)
#else
#define WDT87XX_PM_OPS NULL
#endif

// 在i2c_driver中引用:
.driver = {
    .name = "wdt87xx_ts",
    .of_match_table = wdt87xx_of_match,
    .pm = WDT87XX_PM_OPS,
},

4.4.2 设备状态保存与恢复策略

  • disable_irq() :防止休眠期间产生中断。
  • cancel_work_sync() :等待正在运行的工作完成。
  • msleep(5) :给予芯片足够唤醒时间。

4.4.3 sysfs接口创建

暴露调试信息至用户空间:

static ssize_t debug_dump_show(struct device *dev,
                              struct device_attribute *attr, char *buf)
{
    struct wdt87xx_data *data = dev_get_drvdata(dev);
    // 读取并格式化寄存器内容
    return snprintf(buf, PAGE_SIZE, "IRQ: %d, Suspended: %d\n",
                    data->client->irq, data->suspended);
}

static DEVICE_ATTR_RO(debug_dump);

static int wdt87xx_ts_probe(struct i2c_client *client, ...)
{
    ...
    error = device_create_file(&client->dev, &dev_attr_debug_dump);
    if (error)
        dev_warn(&client->dev, "Failed to create sysfs file\n");
    ...
}

用户可通过 cat /sys/devices/.../debug_dump 查看状态。

| 调试文件 | 路径 | 用途 |
|--------|------|-----|
| debug_dump | /sys/.../debug_dump | 查看驱动内部状态 |
| power/control | /sys/.../power/control | 控制自动挂起行为 |

5. 驱动编译、部署与实战调试

5.1 驱动编译方式选择

在嵌入式Linux开发中,设备驱动的编译方式直接影响其灵活性与系统集成度。针对 wdt87xx_i2c.c 这类触摸屏驱动,常见的编译策略包括模块化编译和静态编译两种。

模块化编译(obj-m) 是最灵活的方式,适用于调试阶段。通过在驱动目录下的 Makefile 中设置:

obj-m += wdt87xx_i2c.o

KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

default:
    $(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
    $(MAKE) -C $(KDIR) M=$(PWD) clean

该配置利用内核构建系统完成模块编译,生成 .ko 文件。随后可通过 insmod wdt87xx_i2c.ko 动态加载, rmmod wdt87xx_i2c 卸载,便于快速迭代。

静态编译进内核 则需将驱动纳入内核源码树,并修改 drivers/input/touchscreen/Kconfig Makefile

config TOUCHSCREEN_WDT87XX
    tristate "WDT87XX I2C Touchscreen support"
    depends on I2C
    help
      Say Y here if you have a WDT87XX based touchscreen controller.
obj-$(CONFIG_TOUCHSCREEN_WDT87XX) += wdt87xx_i2c.o

启用 CONFIG_TOUCHSCREEN_WDT87XX=y/m 后,在执行 make menuconfig 时可选中驱动,最终随内核镜像烧录固化。

对于全志平台等ARM架构设备,必须使用 交叉编译工具链

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- \
     -C /path/to/kernel/source \
     M=$(PWD) modules

此命令确保生成的二进制兼容目标硬件指令集,是部署前的关键步骤。

编译方式 灵活性 调试便利性 启动速度 适用场景
模块化 (obj-m) 开发、调试阶段
静态 (built-in) 产品发布、稳定版本
交叉编译 必须 可配合模块 - 所有嵌入式目标平台

跨平台编译环境搭建还需注意头文件路径一致性、内核版本匹配等问题,建议使用与目标系统一致的 kernel headers。

5.2 wdt87xx_i2c.c源码深度解析

以实际驱动文件 wdt87xx_i2c.c 为例,其核心逻辑贯穿从注册到数据上报全过程。

5.2.1 初始化流程梳理

驱动入口由 module_init(wdt87xx_init) 触发,注册 i2c_driver 结构体:

static int __init wdt87xx_init(void)
{
    return i2c_add_driver(&wdt87xx_i2c_driver);
}

当设备树中存在 compatible 匹配项时,内核自动调用 .probe = wdt87xx_ts_probe 函数。整个初始化链条如下:

flowchart TD
    A[module_init] --> B[i2c_add_driver]
    B --> C{Device Tree 匹配}
    C -->|compatible="sunxi,wdt87xx"| D[wdt87xx_ts_probe]
    D --> E[资源申请: clk/pinctrl/irq]
    E --> F[input_dev 注册]
    F --> G[中断线程化 request_threaded_irq]
    G --> H[启动触摸芯片]

5.2.2 关键函数分析

wdt87xx_ts_probe() 执行关键资源配置:

static int wdt87xx_ts_probe(struct i2c_client *client,
                            const struct i2c_device_id *id)
{
    struct wdt87xx_data *ts;
    int error;

    ts = devm_kzalloc(&client->dev, sizeof(*ts), GFP_KERNEL);
    if (!ts)
        return -ENOMEM;

    ts->client = client;
    i2c_set_clientdata(client, ts);

    ts->input = devm_input_allocate_device(&client->dev);
    if (!ts->input)
        return -ENOMEM;

    /* 设置输入类型 */
    __set_bit(EV_ABS, ts->input->evbit);
    __set_bit(EV_KEY, ts->input->evbit);
    __set_bit(BTN_TOUCH, ts->input->keybit);

    input_set_abs_params(ts->input, ABS_X, 0, SCREEN_WIDTH, 0, 0);
    input_set_abs_params(ts->input, ABS_Y, 0, SCREEN_HEIGHT, 0, 0);
    input_mt_init_slots(ts->input, MAX_CONTACTS, INPUT_MT_DIRECT);

    error = devm_request_threaded_irq(&client->dev, client->irq,
                                      NULL, wdt87xx_irq_handler,
                                      IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
                                      "wdt87xx", ts);
    if (error) {
        dev_err(&client->dev, "Failed to request IRQ\n");
        goto err_free_mem;
    }

    error = input_register_device(ts->input);
    if (error)
        goto err_free_irq;

    return 0;

err_free_irq:
    devm_free_irq(&client->dev, client->irq, ts);
err_free_mem:
    return error;
}

上述代码采用 devm_* 系列资源管理函数,实现自动释放,避免内存泄漏。错误处理采用 goto 标签跳转,保证清理路径唯一且完整。

wdt87xx_irq_handler() 作为中断服务例程,读取寄存器并调度工作队列进行数据解析:

static irqreturn_t wdt87xx_irq_handler(int irq, void *dev_id)
{
    struct wdt87xx_data *ts = dev_id;
    queue_work(ts->workqueue, &ts->work);
    return IRQ_HANDLED;
}

wdt87xx_input_report() 完成多点坐标解析与上报:

void wdt87xx_input_report(struct wdt87xx_data *ts, u8 *data)
{
    int i, x, y, touch;
    for (i = 0; i < MAX_CONTACTS; i++) {
        touch = data[TOUCH_STATUS + i];
        if (touch) {
            x = get_unaligned_le16(&data[X_POS + i*COORD_SIZE]);
            y = get_unaligned_le16(&data[Y_POS + i*COORD_SIZE]);

            input_mt_slot(ts->input, i);
            input_mt_report_slot_state(ts->input, MT_TOOL_FINGER, true);
            input_report_abs(ts->input, ABS_MT_POSITION_X, x);
            input_report_abs(ts->input, ABS_MT_POSITION_Y, y);
        } else {
            input_mt_report_slot_inactive(ts->input, i);
        }
    }
    input_sync(ts->input); // 提交帧结束标记
}

5.3 调试手段与问题定位

5.3.1 内核日志分析

使用 dmesg | grep wdt87xx 可捕获驱动运行状态:

[  123.456789] wdt87xx_ts_probe: Found device at address 0x5d
[  123.457123] wdt87xx_ts_probe: Request IRQ success, irq=42
[  123.457456] input: wdt87xx as /devices/virtual/input/input5
[  124.123456] wdt87xx_irq_handler: Received interrupt, reporting touch

常见错误模式如:
- i2c-nack : 设备未响应 → 检查地址、电源、上拉电阻
- Unable to request IRQ : GPIO冲突或设备树中断未定义
- No input device registered : probe中途失败导致 input_dev 未完成注册

5.3.2 用户空间行为跟踪

结合 strace 监控应用层对 /dev/input/eventX 的访问:

strace -e trace=read,ioctl,write -p $(pidof weston)

输出示例:

read(4, "\x1b\xea\x8cM\t\0\0\x1\x00\x0\x00\x0\x0\x0\x0\x0\x1", 16) = 16

验证事件是否正常传递至用户空间。

5.3.3 模拟触摸注入测试

借助 evtest input_event 工具反向注入事件,验证事件链完整性:

# 获取设备节点
ls /dev/input/by-path/*touch*
# 注入一次点击
input_event /dev/input/event2 3 0 400  # ABS_X
input_event /dev/input/event2 3 1 600  # ABS_Y
input_event /dev/input/event2 1 330 1  # BTN_TOUCH press
input_event /dev/input/event2 0 0 0    # sync

若GUI产生响应,则说明 input 子系统链路通畅。

5.4 完整驱动上线流程总结

5.4.1 实机验证多点触控准确性

在真实设备运行 evtest /dev/input/eventX ,滑动手势记录原始数据:

Time(us) Type Code Value
123456 3 57 0
123457 3 53 412
123458 3 54 621
123459 1 330 1
123460 0 0 0
125000 3 57 -1
125001 1 330 0
125002 0 0 0

观察 ABS_MT_SLOT 切换与坐标变化是否符合预期轨迹。

5.4.2 性能优化建议

  • 减少轮询 :禁用轮询机制,完全依赖中断触发。
  • 批量上报 :合并多个触点在一个 input_sync() 前提交。
  • 中断去抖 :添加软件滤波或硬件 RC 滤波电路,防止误触发。
  • 功耗控制 :在 suspend 中关闭芯片电源,resume 时重新初始化。

5.4.3 提交流程准备

提交至 Linux 内核社区前,执行代码规范检查:

./scripts/checkpatch.pl --strict wdt87xx_i2c.c

修复警告如:
- 不推荐使用 printk ,应替换为 dev_dbg/dev_err
- 添加 SPDX 许可标识: MODULE_LICENSE("GPL");
- 编写 Documentation/devicetree/bindings/input/wdt87xx.yaml`

同时提供完整测试报告与性能基准数据,确保可维护性与兼容性。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:在嵌入式Linux系统中,触摸屏作为重要的人机交互设备,其I2C驱动的实现对系统功能至关重要。本文聚焦于基于全志A33、A83T等系列芯片的嵌入式平台,详细讲解如何编写和集成触摸屏的I2C驱动程序。内容涵盖驱动注册、I2C设备探测、input子系统初始化、中断处理机制、电源管理支持及设备节点创建等核心环节。通过分析wdt87xx_i2c.c源码,帮助开发者掌握从硬件通信到内核事件上报的完整流程,适用于平板电脑、智能终端等设备的驱动开发与调试。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐