简介:面向嵌入式系统、工控设备及自助终端开发者的触摸屏驱动与串口驱动资料包,系统讲解两类驱动的工作原理、通信协议及系统集成方法。压缩包共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_client,probe 函数的第一个参数也从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。设备树里touch@38这个子节点会被 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"; touch@38 { 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_ops
3.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 FIFO,set_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", buffering=0) 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(b"X:%d\n" % tmp[4]) elif tmp[2] == EV_ABS and tmp[3] == ABS_Y: ser.write(b"Y:%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")解析正好对上 4+4+2+2+4 的布局。buffering=0这行是给串口发送的关键,Python 写文件默认带缓冲,如果不开无缓冲模式,几十条坐标会攒成一个大的写请求,触摸手感明显发涩。用X:100、Y:200这种纯文本格式是为了让对端设备或者 PC 串口助手直接可读,实际项目里如果对端是 MCU,改成二进制结构体会更紧凑。
4.3 联合配置的设备树:触摸中断、串口波特率与流控
把两个驱动挂到同一个根节点下的设备树配置,最值得关注的是中断号不能重叠,以及串口波特率是否被 bootloader 改过。
&i2c0 { status = "okay"; touch@38 { 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_irq,resume 却忘了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三个工具配合起来,十分钟之内就能判断是“驱动没活着”“中断没进来”还是“数据通路没接上”,不用再靠猜和反复编译验证。调试触摸屏驱动加串口驱动组合时,先确认两条通路各自健康,再谈上下层数据对不对,这个顺序帮我少走了很多弯路,希望帮到你。
本文还有配套的精品资源,点击获取