资讯动态

触摸屏驱动与串口驱动:BSP开发中的核心搭档

发布时间:2026/10/15 11:59:00 来源:尧图企业网站定制
简介面向嵌入式系统、工控设备及自助终端开发者的触摸屏驱动与串口驱动资料包系统讲解两类驱动的工作原理、通信协议及系统集成方法。压缩包共37个文件大小632KB包含C源码、头文件、汇编文件及链接脚本等其中.c和.h文件便于驱动逻辑分析.o与.axf文件可对照编译产物适合驱动开发初学者或排查底层通信问题的工程师。压缩包内部按启动代码、发布版本等模块组织目录结构清晰便于按功能检索。已有1420人学习下载内容从电阻式、电容式触摸屏的原理差异到串口波特率、奇偶校验、中断与DMA配置均有涉及并结合Linux内核模块与Windows WDK开发场景展开帮助读者建立从硬件接口到操作系统驱动的完整认知也可为实际项目中的驱动移植、性能优化与错误处理提供直接参考。1. 触摸屏驱动加串口驱动为什么这两个名字总出现在同一张 BSP 清单里触摸屏驱动加串口驱动乍看是两个不相干的外设驱动一个管手指坐标怎么变成内核事件一个管字节流怎么进出设备。真正在产线上守过板子的人反而清楚这两个名字往往同时出现在一张 BSP 驱动的开工清单里。触摸驱动决定产品能不能“点得准”串口驱动决定你能不能“看得见”——系统起不来、坐标偏了、固件没法升级最后都要靠那条串口把日志送出来。这篇文章把一个标准嵌入式 Linux 平台上的电容触摸驱动和 UART 驱动拆开讲清楚再演示它们如何在一个产品里配合工作适合刚开始做 BSP、或者被触摸 IC 与调试串口反复折磨的应用层工程师。2. 触摸屏驱动怎么做一边注册 input 设备一边把坐标报出去2.1 驱动入口i2c_client 与 platform 注册的差别大多数电容触摸屏控制器都挂在 I2C 或者 SPI 上所以驱动入口一般是i2c_driver而不是platform_driver。这两个入口的差别很实际platform_driver靠设备树里的compatible匹配platform_device而i2c_driver靠设备树子节点里的compatible匹配到 I2C 总线上扫描出来的i2c_clientprobe 函数的第一个参数也从struct platform_device *换成了struct i2c_client *。我一般先写一个最小的 i2c 驱动骨架把设备名占住确认/sys/bus/i2c/devices/0-0038这个节点真的出现再往里面补 input 子系统相关的代码。先跑通“驱动被 probe、设备节点出现”这一小步后面所有问题都好定位。#include linux/i2c.h #include linux/module.h #include linux/of.h static int my_ts_probe(struct i2c_client *client) { /* 一个合格的 probe 至少要走完这几步 1. 检查 client-irq 是否有效 2. 申请 GPIO 复位引脚并拉高 3. 初始化 input 设备 4. request_irq 挂中断 失败时尽量用 dev_err 打出具体原因 */ if (client-irq 0) { dev_err(client-dev, no irq for touch controller\n); return -ENODEV; } return 0; } static const struct of_device_id my_ts_of_match[] { { .compatible my,touch-ic, }, { } }; MODULE_DEVICE_TABLE(of, my_ts_of_match); static struct i2c_driver my_ts_driver { .probe my_ts_probe, .driver { .name my_touch, .of_match_table my_ts_of_match, }, }; module_i2c_driver(my_ts_driver);这里module_i2c_driver是一个封装宏展开后就是标准的 module_init 和 module_exit不需要再手写i2c_add_driver。设备树里touch38这个子节点会被 I2C 核心自动匹配到my,touch-ic然后创建一个i2c_client地址 0x38 会显示在总线目录里。如果 probe 没执行先查compatible字符串和reg地址是否和设备树完全一致这是我从接触 I2C 驱动到现在遇到次数最多的“玄学”问题。2.2 坐标上报的正确姿势input_report_abs 与 input_sync触摸屏驱动在内核里的角色很简单触摸控制器准备好坐标后驱动把坐标变成标准 input 事件交给 input 核心。事件类型主要是两个BTN_TOUCH表示手指有没有按下ABS_X和ABS_Y表示坐标。坐标读出来后必须调用input_sync否则应用层拿到的是一堆没有边界的事件片段。很多人第一次写触摸驱动时上报坐标前后顺序错了或者漏了 sync表现就是手势断断续续、单击被识别成双击。static irqreturn_t my_ts_irq_handler(int irq, void *dev_id) { u8 status; u16 x, y; struct i2c_client *client dev_id; /* 第一步读状态寄存器判断当前是否有有效触点。 不同 IC 的寄存器布局差别很大这一步要以 对应 IC 数据手册为准我这里用 0x80/0x81/0x83 作示例 */ status i2c_smbus_read_byte_data(client, 0x80); if (status 0x80) { x i2c_smbus_read_word_data(client, 0x81); y i2c_smbus_read_word_data(client, 0x83); input_report_key(ts_input, BTN_TOUCH, 1); input_report_abs(ts_input, ABS_X, x); input_report_abs(ts_input, ABS_Y, y); } else { input_report_key(ts_input, BTN_TOUCH, 0); } input_sync(ts_input); /* 不少控制器在读完坐标后需要显式写寄存器清中断标志 否则 INT 引脚一直保持有效电平中断风暴随之而来。 这一句同样要根据具体 IC 手册决定要不要写、写什么值 */ i2c_smbus_write_byte_data(client, 0x84, 0x00); return IRQ_HANDLED; }这段代码里最需要注意的是i2c_smbus_*系列函数。默认 I2C 传输是阻塞的触摸控制器单个寄存器读取通常一两百微秒完成放在中断上下文里勉强可接受但遇到 I2C 总线繁忙重试时就会拉长中断处理时间。稳妥的做法是中断里只置一个标志位然后调用schedule_work让工作队列去读坐标。我实际做过的方案里工作队列方式对后续调试更友好坐标数据也不会因为总线重试而丢失。input 设备初始化的关键参数是坐标范围。注意input_set_abs_params的四个参数除了 min 和 max还有 fuzz 和 flat触摸屏一般写 0 就行。ts_input devm_input_allocate_device(client-dev); if (!ts_input) return -ENOMEM; ts_input-name my-touchscreen; ts_input-id.bustype BUS_I2C; __set_bit(EV_KEY, ts_input-evbit); __set_bit(EV_ABS, ts_input-evbit); __set_bit(BTN_TOUCH, ts_input-keybit); input_set_abs_params(ts_input, ABS_X, 0, 1023, 0, 0); input_set_abs_params(ts_input, ABS_Y, 0, 767, 0, 0); input_register_device(ts_input);__set_bit(EV_ABS, ts_input-evbit)这行决定了 input 子系统会为ABS_X和ABS_Y创建对应的绝对轴信息应用层ioctl查轴范围时拿到的是input_set_abs_params里填的数字。坐标范围不对是触摸不准的第一大来源驱动里写 1023实际控制器上报 12bit 数据触摸点就会被压缩到屏幕四分之一区域。先查ABS_X的 max 值再回到寄存器确认控制器位宽不看寄存器就改设备树坐标范围属于瞎猜。2.3 设备树里最值得较真的触摸参数设备树里触摸控制器节点有几个参数直接影响用户体验中断触发方式、坐标范围、坐标是否翻转交换。中断触发方式出问题表现是不触摸时中断引脚空闲电平不对整个系统中断风暴坐标参数出问题表现是触摸点位置对不上。i2c0 { status okay; pinctrl-0 i2c0_pin; pinctrl-names default; touch38 { compatible my,touch-ic; reg 0x38; interrupt-parent gpio1; interrupts 11 IRQ_TYPE_EDGE_FALLING; reset-gpios gpio1 12 GPIO_ACTIVE_LOW; touchscreen-size-x 768; touchscreen-size-y 1024; touchscreen-inverted-y; }; };touchscreen-size-x和touchscreen-size-y是内核触摸屏 binding 的通用属性很多触摸驱动会读这两个值来初始化input_set_abs_params比我上面硬编码的1023/767更规范。touchscreen-inverted-y表示 Y 轴镜像翻转如果驱动代码里已经处理过翻转设备树里再写一次就会造成双重反转。选型时先确认触摸 IC 的驱动源码处理了哪些通用属性再判断该不该在设备树里补参数这个优先级顺序能省掉大量对坐标的排查时间。提示如果你手上驱动只认它自己的私有属性不认touchscreen-前缀那就以驱动实际解析的属性为准。设备树是给特定驱动用的配置语言不是通用的数据表。3. 串口驱动怎么做核心是 uart_driver、uart_port 和 uart_ops3.1 tty 架构下串口驱动到底在实现什么内核里串口不是一个孤立的驱动它挂在 tty 体系下。应用层打开/dev/ttyS1写字节时数据先经过 tty 核心和 line discipline再由 tty 驱动分发给具体的 uart 驱动最后落到硬件 TX FIFO 和引脚上。对写串口驱动的人来说真正要关心的是三个数据结构struct uart_driver负责向系统登记一批同类型的串口并生成ttySx设备节点struct uart_port描述某个物理串口的地址、中断号和当前状态struct uart_ops是具体硬件操作函数的集合收发字节全都通过它间接调用。很多新手第一次打开 8250 驱动的源码会被大段宏定义淹没但其实把三个结构的职责掰开看就清晰了。uart_driver是整个串口家族的“户口本”uart_port是其中一个孩子的身份证uart_ops是这个孩子会做的一组动作比如启动、收数据、发数据、改参数。写新串口驱动时不需要从零实现 tty 层只实现uart_ops里的函数就够tty 核心会替你处理 open、read、write 的大部分流程。3.2 uart_ops 收发流程与三个必调参数uart_ops的函数很多一个能跑起来的串口驱动至少要实现request_port、config_port、startup、shutdown、set_termios、start_tx、stop_tx、tx_empty。startup里申请中断并使能 RX FIFOset_termios里计算波特率除数并配置数据位校验位start_tx里把缓冲区数据写入硬件 TX 寄存器。这三者几乎决定了串口能不能通、通得稳不稳。static void my_uart_start_tx(struct uart_port *port) { struct circ_buf *xmit port-state-xmit; /* 循环从环形缓冲区取字节写入 TX 寄存器。 uart_circ_empty 判断缓冲区是否取空取空就停 */ while (!uart_circ_empty(xmit)) { writel(xmit-buf[xmit-tail], port-membase UART_TX); xmit-tail (xmit-tail 1) (UART_XMIT_SIZE - 1); port-icount.tx; } /* 数据发完关掉 TX 中断避免做无用功 */ if (uart_circ_empty(xmit)) uart_port_unlock(port); }start_tx是典型的“被动触发”函数tty 层写数据时并不会直接调它而是通过uart_write_wakeup机制唤醒发送。所以很多人第一次写串口驱动时发现上层 write 返回了字节数但 TX 引脚上什么波形都没有原因就是start_tx没被正确注册到uart_ops.start_tx。set_termios里的波特率控制是另一个高频翻车点不同平台的 UART 时钟源频率不同同一个波特率算出来的分频系数可能不一样直接把 8250 驱动里的baud * 16公式照搬出来的实际波特率可能偏了百分之几长线上丢字符不可避免。3.3 注册一条串口从 uart_driver 到 /dev/ttySx 的命令注册串口驱动主要分两步先uart_register_driver注册家族再为每个物理端口uart_add_one_port绑定硬件资源。注册之后系统会在/dev下创建对应节点具体名字看dev_name和端口编号的组合。static struct uart_driver my_uart_drv { .owner THIS_MODULE, .driver_name my_uart, .dev_name ttyS, .nr 4, }; static struct uart_port my_ports[4]; static int my_uart_probe(struct platform_device *pdev) { struct uart_port *port my_ports[0]; /* 每个 port 必须设置 ops、iotype、irq、线号和寄存器映射 否则 uart_add_one_port 会因为缺字段拒绝注册 */ port-ops my_uart_ops; port-iotype UPIO_MEM; port-irq platform_get_irq(pdev, 0); port-fifosize 64; port-line 0; if (uart_register_driver(my_uart_drv)) return -EINVAL; return uart_add_one_port(my_uart_drv, port); }uart_register_driver只需要调用一次而uart_add_one_port每个物理串口都要调一次。dev_name填成ttyS、nr填 4 就会生成/dev/ttyS0到/dev/ttyS3。我在调试时习惯注册完立刻执行一次ls -l /dev/ttyS*如果节点不出现先看uart_add_one_port返回值再看内核日志里有没有port-ops NULL之类提示这些信息往往比臆测“驱动没编译进内核”来得快。注意串口驱动 probe 失败多半不是硬件没接好而是uart_port初始化字段不完整。iotype、membase或mapbase、irq、line这四个字段缺一不可先补齐再谈硬件问题。4. 把触摸坐标从串口打出去一条最小可复现的联合数据通路4.1 两个驱动怎么合作合流点其实在应用层触摸屏驱动和串口驱动在内核里并不会直接发生数据交换。触摸驱动只负责把坐标变成 input 事件串口驱动只负责把字节流搬到 UART 引脚上它们之间没有共享缓冲也没有互相调用的接口。真正把两者连接起来的是应用层应用程序读/dev/input/eventX拿到触摸事件解析成坐标文本再写入/dev/ttySx发出去。这个“通过应用层合流”的设计在嵌入式 Linux 里极其常见却被不少刚接触 BSP 的人误解他们以为要写一个同时操作触摸 IC 和 UART 控制器的超级驱动。我见过有人试图在驱动里直接读触摸寄存器再往串口寄存器写绕开 input 和 tty 两层结果就是硬件一重置就锁死、升级内核固件就崩。标准做法是让每个驱动只做自己最擅长的事中间放一个几百行的应用脚本或者一个小守护进程这样即使触摸屏换了型号只需改内核驱动串口通路完全不动反过来串口换引脚复用应用层也无感知。4.2 一个能跑通的最小 python 打通脚本这个脚本负责把/dev/input/event2的触摸事件转成可读文本写到/dev/ttyS1。先要确认event2到底是不是触摸屏不要照抄设备路径。#!/usr/bin/env python3 import struct EV_ABS 0x03 EV_SYN 0x00 ABS_X 0x00 ABS_Y 0x01 def parse_event(path, out_path): with open(path, rb) as ev: # 关闭用户态缓冲保证坐标不超过一个调度周期就发出去 with open(out_path, wb, buffering0) as ser: while True: raw ev.read(24) # input_event 固定 24 字节 if not raw: break tmp struct.unpack(IIhhi, raw) # 小端机器上原生 unpack 就是小端大端平台要加 # sec, usec, type, code, value if tmp[2] EV_ABS and tmp[3] ABS_X: ser.write(bX:%d\n % tmp[4]) elif tmp[2] EV_ABS and tmp[3] ABS_Y: ser.write(bY:%d\n % tmp[4]) elif tmp[2] EV_SYN: ser.write(b---\n) if __name__ __main__: parse_event(/dev/input/event2, /dev/ttyS1)input_event结构是struct timeval加type/code/value总共 24 字节用struct.unpack(IIhhi)解析正好对上 44224 的布局。buffering0这行是给串口发送的关键Python 写文件默认带缓冲如果不开无缓冲模式几十条坐标会攒成一个大的写请求触摸手感明显发涩。用X:100、Y:200这种纯文本格式是为了让对端设备或者 PC 串口助手直接可读实际项目里如果对端是 MCU改成二进制结构体会更紧凑。4.3 联合配置的设备树触摸中断、串口波特率与流控把两个驱动挂到同一个根节点下的设备树配置最值得关注的是中断号不能重叠以及串口波特率是否被 bootloader 改过。i2c0 { status okay; touch38 { compatible my,touch-ic; reg 0x38; interrupt-parent gpio1; interrupts 11 IRQ_TYPE_EDGE_FALLING; reset-gpios gpio1 12 GPIO_ACTIVE_LOW; touchscreen-size-x 768; touchscreen-size-y 1024; }; }; uart1 { status okay; pinctrl-0 uart1_txd uart1_rxd; pinctrl-names default; /* 波特率通常由应用层 stty 设置设备树一般不写 但有些平台允许通过 current-speed 指定初始值 */ current-speed 115200; };串口节点的pinctrl名称里不要包含ctsrts对应的引脚配置除非你确定硬件接了流控线。很多调试串口是三线制只接 TX、RX、GND如果驱动默认使能硬件流控发送端会一直等 RTS 信号表现就是“能收不能发”或者“开机首字节丢失”。触摸中断和串口中断通常来自不同 GPIO 域冲突概率不高但一旦冲突request_irq会返回-EBUSY直接查/proc/interrupts就能看出哪个驱动先占了那个中断号。提示同一套设备树点不亮串口时先看 bootloader 阶段是否能从这个串口打印字符。能打印说明引脚复用正确问题在驱动不能打印说明引脚方向或者电平已经错了后面改驱动也救不回来。5. 排查与避坑触摸翻车现场和串口丢字符的常见场景5.1 点上半屏光标跑下半屏坐标翻转先看设备树现象手指点屏幕上半部分系统里的光标却跑到下半部分左右也呈镜像或者横竖方向对调。 原因面板出线方向和驱动默认坐标轴方向不一致Y 轴被反向输出或 X/Y 轴发生了交换。这类问题不是硬件坏了而是触摸 IC 自身坐标系与屏幕显示坐标系没有对齐。 解决先在设备树里加touchscreen-inverted-y重启看效果仍然不对就touchscreen-inverted-x方向乱到完全对不上尝试touchscreen-swapped-x-y。我习惯一次只改一个属性改完测一次三个属性一起改会叠加出更诡异的结果到时候根本分不清哪一步造成的。如果这几个通用属性驱动不支持就只能回驱动源码里改坐标映射函数一般就是一行x max_x - x的事。5.2 手指按上去中断不触发开漏 INT 忘了上拉现象触摸屏驱动 probe 成功/proc/interrupts里能看到中断号但手指怎么按中断计数都不增加。 原因很多触摸控制器的 INT 引脚是开漏输出需要一个外部或者 SoC 引脚内部的上拉电阻才能正确产生下降沿。板子上没有外部上拉、pinctrl 又没开内部上拉时引脚电平浮在门限附近中断自然不来。 解决先在/sys/kernel/debug/gpio里确认该 GPIO 的当前状态再回到设备树 touch 节点所在的 pinctrl 配置里加上内部上拉。不同 SoC 的 pinctrl 写法差异较大但基本原理都是bias-pull-up。如果是自己画的板子量一下 INT 引脚空闲电平高电平正常低电平说明上拉缺失这比反复换驱动版本快得多。5.3 串口开机丢第一字节先关 RTS/CTS 再看看分频现象每次系统启动后往串口写数据第一个字符总丢后面的数据都正常或者连续长报文偶尔丢一个字节。 原因最常见的是串口驱动默认开了硬件流控三线制接法下 RTS/CTS 悬空发送端等不到 CTS 有效就推迟发送第二个常见原因是波特率时钟分频不准一个字节约 8.7 微秒的偏差在长包上会积累成断字。 解决先用stty -F /dev/ttyS1 -crtscts关掉流控再看首字符是否恢复。如果关掉流控仍然丢字把波特率换成 9600 试试误差会随波特率下降而缩小以此判断是不是分频公式问题。最后一步才是查 SoC 的 UART 时钟源确认不再使用 bootloader 遗留的错误时钟配置。5.4 两个驱动抢同一个 GPIO 中断号现象触摸驱动在request_irq时返回-EBUSY或者串口在某次系统启动后中断不再触发/proc/interrupts里那个中断号的驱动名完全对不上硬件。 原因两个驱动通过不同设备树节点分别申请了同一个 GPIO 编号。有些 SoC 的 GPIO bank 非常大一个 bank 里中断号是线性叠加的复用引脚配置时很容易把 pin 编号算错。 解决先把两个节点里interrupts的编号逐一换算成实际的 bank pin 号用/sys/kernel/debug/gpio或者 SoC 手册里的 GPIO 编号公式核对。我的经验是不要相信两个驱动各自注释里的“GPIO1_11”这种说法直接看数据手册的 bank 和 pin 对照表换算成最终中断号再检查 pinctrl 是否复用冲突两边都改对之后重新编译设备树问题自然消失。5.5 休眠唤醒之后触摸失效复位时序必须重放现象系统入睡再唤醒后触摸屏无响应串口打印正常内核日志也没有报错/proc/interrupts里触摸中断计数不再增长。 原因触摸控制器进了低功耗模式而驱动 resume 的时候没有重新初始化它或者只是把 GPIO 重新配了中断但没有给控制器发送唤醒命令。触摸 IC 不像串口那样醒来就能收发它的内部状态机可能停在睡眠态。 解决在驱动的 resume 回调里重新拉一次reset-gpios先低后高保持一定延时然后重放 probe 时的初始化寄存器序列最后重新enable_irq。很多驱动的 sleep 回调只做了disable_irqresume 却忘了enable_irq这类一眼看不出来的问题最快定位方式是在 resume 里加dev_info打印确认 resume 有没有被调用再确认中断有没有重新使能。6. 不用猜把触摸和串口的驱动状态直接“看”出来6.1 触摸侧evtest 看事件流没有 evtest 用 hexdump设备树、驱动源码都看不出触摸驱动到底有没有报对坐标唯一可靠的是看事件流。有evtest时evtest /dev/input/event2会实时把 event 的类型、code、value 打出来按一下屏幕能看到坐标值变化手感不对时也能直观看到是 X 轴跳变还是丢点。目标根文件系统没有evtest时用hexdump /dev/input/event2也能看到原始二进制事件每 24 字节一行type 为 03 的就是绝对轴事件type 为 00 的是同步事件。通过观察连续两条同步事件之间的坐标跳变就能判断驱动上报的坐标是否平滑。6.2 串口侧stty 与回环测试三板斧串口驱动调得好不好不要用逻辑分析仪先用回环线测试。把该 UART 的 TX 和 RX 引脚短接然后执行stty -F /dev/ttyS1 115200 raw -crtscts cs8再开两个终端一个cat /dev/ttyS1一个echo test /dev/ttyS1。能看到test返回说明收发路径基本通。三板斧分别是关流控、看波特率、查/proc/tty/driver/my_uart里的发送和接收计数。计数在增长但回环收不到重点检查外部收发器方向控制计数不动问题回到驱动或 pinctrl。6.3 中断和内核日志两个驱动是否健康的最后佐证/proc/interrupts是我每次排查驱动问题的第一站。触摸驱动正常工作时按一次屏幕对应中断计数会跳几次串口正常工作时收发数据时对应中断计数也要增加。两个计数都不动而应用层又没反应说明中断根本没进来后面所有上报和收发流程都不会执行。内核日志里request_irq如果返回-EBUSY说明中断号被占用这个错误信息里通常带着占用者的驱动名顺着名字去查它的设备树节点就能找到冲突根源。以前我把触摸和串口当作两个独立问题分开追后来学会把/proc/interrupts、evtest和stty三个工具配合起来十分钟之内就能判断是“驱动没活着”“中断没进来”还是“数据通路没接上”不用再靠猜和反复编译验证。调试触摸屏驱动加串口驱动组合时先确认两条通路各自健康再谈上下层数据对不对这个顺序帮我少走了很多弯路希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑