ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux串口触摸屏接入全解析:从UART配置到输入子系统

Linux串口触摸屏接入全解析:从UART配置到输入子系统 前阵子把一块 7 寸串口触摸屏接到 ARM Linux 主板上时我以为这个活半小时就能搞定。毕竟板子内核里已经带了 8250 串口驱动屏幕模块自己也有触摸功能接上 UART 之后无非就是在应用层发指令、读坐标。结果真正调起来才发现Linux 下做串口屏和单片机上完全是两码事。设备树里串口引脚的 pinctrl 没配好触摸坐标上报到 Linux 输入子系统之后的映射关系也是错的跑一会儿还会丢字节。折腾了两三天最后问题基本都在内核配置和输入子系统接入方式上反而屏幕本身没出什么幺蛾子。这次就把我做 Linux 串口触摸屏的完整设计和排错过程总结一下给后面要做类似方案的朋友一个参考。主题围绕 Linux 内核如何正确支持串口屏、串口触摸数据怎么变成标准 Linux 输入事件以及这个过程中容易踩到的各种坑。1. 串口屏项目的技术边界Linux下不是“接上就能用”1.1 串口屏与普通LCD的本质差异很多人刚接触串口屏时会有一个思维惯性觉得屏幕就是显示设备要么走 HDMI、要么走 RGB/MIPI最多再加个 I2C 触摸芯片。但串口屏的原理完全不同。它本身就是一块带 MCU 的独立显示模组内部有自己的显存、字库和控件库主控只需要通过串口发一条“显示一张图”“设置某个文本控件”之类的指令剩下的渲染工作全部在屏幕内部完成。这带来一个非常大的好处Linux 侧不需要维护 framebuffer也不需要关心分辨率、刷新率、图层叠加这些复杂功能。整个过程看起来就是“打开一个串口设备往里写指令再读取返回”。但也正因为如此Linux 内核侧的工作边界变得很微妙。你不需要去写一个 LCD 控制器驱动但你必须保证串口设备稳定可用、数据不丢、触摸事件能进入 Linux 标准的输入子系统。否则就会出现“屏幕显示正常但触摸点了没反应”这种最尴尬的局面。1.2 Linux侧要解决的三层问题接串口屏时我习惯把问题拆成三层来看。第一层是硬件载体层。串口屏一般走 TTL 电平的 UART少数工业场景会转 RS232 或者 RS485。这一层要做的事情包括确认 SoC 的 UART 引脚是否被其他外设占用、设备树里的串口节点是否使能、波特率和校验位是否和屏幕一致。内核侧主要涉及 pinctrl、串口驱动、设备树 aliases。第二层是数据通道层。串口屏的通讯协议通常是一帧一帧的里面可能有帧头、命令字、长度、负载、校验和、帧尾。Linux 的read()是流式读取不保证一次拿到完整的一帧所以需要一个状态机或者缓冲机制来拼包。这一层可以在用户态做也可以写一个内核模块做 line discipline。第三层是输入事件层。这是 Linux 串口屏和单片机串口屏最大的区别。单片机可以直接把触摸坐标拿去用但 Linux 下各种图形框架Qt、GTK、Flutter默认从/dev/input/eventX读取输入事件所以串口屏上报的触摸数据必须转换成标准的input_event结构。要么通过 uinput 从用户态注入要么在内核里注册一个 input device 驱动。这三层只要有一层没做对整个系统用起来就会很别扭。2. 内核侧支持串口屏UART设备树、驱动与DMA配置2.1 先让tty节点稳定可用的关键配置内核对串口屏的支持核心其实就是“把串口这个基础外设整利索”。以 ARM Linux 上常用的设备树为例一个串口节点往往要打开两样东西status okay和对应的pinctrl。uart3 { pinctrl-0 uart3_tx_pins uart3_rx_pins; pinctrl-names default; status okay; };看起来简单但实际板卡上会有很多隐性冲突。最常见的是这个串口被内核 console 占用了。很多 BSP 默认把consolettyS0,115200写在内核启动参数里而ttyS0恰好就是要接串口屏的串口。这时候即使你的应用能打开/dev/ttyS0内核日志也会时不时地往这个串口上吐数据屏幕就会间歇性乱码。解决方式有三个换一个不冲突的串口节点比如把串口屏接到ttyS1。修改console启动参数让内核日志输出到别的串口。用earlycon或者独立调试串口把普通串口完全释放给业务。另一个我踩过的坑是设备树里串口的时钟和别名。有些平台需要配置clocks属性否则串口波特率计算会偏差很大例如 115200 实际跑出来是 110000 左右屏幕端就会一直乱码。调试时可以先在内核 dmesg 里看串口驱动的初始化信息确认最终的波特率分频系数是否符合预期。2.2 串口驱动选型与调度依赖Linux 下最常见的串口驱动是 8250 系列和 PL011 系列。8250 通常对应兼容 16550A 的 UART很多 x86 板卡和部分 ARM SoC 都走这一套PL011 则是 ARM 平台常见的驱动比如树莓派和一些基于 AMBA 总线的 SoC。这两个驱动在内核 config 里的选项不一样CONFIG_SERIAL_8250y CONFIG_SERIAL_8250_CONSOLEy CONFIG_SERIAL_AMBA_PL011y CONFIG_SERIAL_AMBA_PL011_CONSOLEy如果裁剪内核的时候漏掉了对应选项系统可能连/dev/ttyS0或/dev/ttyAMA0都不会出现。检查的时候不要光看有没有这个设备节点还要确认驱动有没有真正 probe 成功。串口屏对实时性的要求其实不高但触摸体验对“连续上报”很敏感。如果系统负载高用户态读串口的线程被调度器长时间挂起触摸事件就会一阵一阵地卡。比较好的做法是把读取线程设置成高优先级实时线程或者用poll/select加超时的阻塞式读取不要用忙等循环。Linux 的串口驱动内部有等待队列机制poll会在数据到达时唤醒进程这对触摸读取已经足够。2.3 DMA/FIFO的取舍与实测效果内核里的串口驱动为了降低中断频率一般都会打开 FIFO常见 16550A 的 FIFO 是 64 字节。但串口屏的数据量其实很小触摸坐标动静也就几十个字节显示指令一般也不会超过几百字节所以 FIFO 溢出风险并不高。另外很多 BSP 会默认给串口打开 DMA 支持。DMA 能够把数据从 UART FIFO 搬到内存减少 CPU 中断但在某些平台上反而会带来问题DMA 描述符分配失败、缓存一致性问题、或者 DMA 通道和别的驱动冲突。实际测试下来如果只是接一块串口触摸屏关闭 DMA 让数据走传统中断FIFO系统更稳。关闭方式因平台而异有一些是在设备树里配置dmas属性为空或去掉dma-names有些是内核 config 里关闭CONFIG_SERIAL_8250_DMA。关键判断依据是看实测是否丢字节。我曾经遇到一个平台开着 DMA 时串口屏的触摸坐标偶尔多出几个 FF 字节关了 DMA 之后连续点了半个小时都没出现异常。这种问题排查起来很耗费时间不如一开始就按“数据量小就关 DMA”的原则处理。3. 串口协议解析策略内核处理还是用户态处理3.1 串口屏通信数据帧的基本模样串口屏品牌很多协议也各不相同但常见协议的结构往往差不太多。拿市面上主流的几类串口屏来说发送指令一般是“帧头 命令 参数 帧尾”触摸上报则是“帧头 触摸状态 X坐标 Y坐标 帧尾”。假设某串口屏的触摸包格式是这样0xAA 0x01 0x00 0x80 0x01 0x2C 0x00 0xFF 0xFF 0xFF其中0xAA是帧头0x01表示触摸按下后面0x0080是 X 坐标大端0x012C是 Y 坐标0xFF 0xFF 0xFF是结束标志。不同品牌会有不同设计有的用长度字段有的用 CRC16 校验有的用累加和校验。解析时务必先看明白两个问题这条协议是定长的还是变长的有没有校验字段定长协议解析最简单每次读到固定字节数就能处理变长协议需要根据长度字段来循环读。校验字段也不能忽略工业环境下的串口线经常会受干扰没有校验的协议很容易出现“触摸乱跳”。3.2 两种解析路线的对比这里有一个经常被问到的选择串口协议解析到底放在内核里还是用户态我的回答是默认放用户态。理由有三个调试方便。用户态程序可以随便printf可以用脚本模拟串口屏回包不用反复重新编译内核。协议迭代快。串口屏厂商偶尔会出固件升级波协议微调用户态改起来成本低。内核社区不建议在驱动里维护业务协议。内核自带的大量串口驱动只负责数据搬运不解析业务帧。但有一种情况我会考虑写内核模块屏幕触摸事件需要参与系统的休眠唤醒策略。比如系统休眠后串口屏来触摸事件要唤醒系统这时候用户态进程已经被冻结无法及时处理就必须在中断/驱动层做事件检测。另一个场景是触摸数据量极大用户态 read 已经成了性能瓶颈虽然这种情况在串口屏项目里很少出现。同时也要提醒一点有一些想“玩得高级”的人会试图动态 hook tty 设备的file_operations拦截read和write来实现串口数据过滤甚至透明加密。这种思路确实能解决问题但风险很高。内核里随便替换file_operations很容易造成引用计数混乱而且不同内核版本的tty_operations结构体定义不一致升级一次内核就要重新适配。与其这样还不如正儿八经注册一个 line discipline把串口数据流在链路层做自定义处理。3.3 粘包、断包与状态机解析串口本身是字节流不是“一包一包”的。用户态调用read()时有可能一次拿到半包数据也有可能一次拿到两包数据。这就叫断包和粘包。很多初次做串口屏的人吃了这个亏——以为read()返回多少字节就是完整的一帧结果发现坐标偶尔错乱。正确做法是引入一个简单的状态机逐字节处理enum parse_state { ST_IDLE, ST_HEAD, ST_CMD, ST_LEN, ST_DATA, ST_TAIL }; for (size_t i 0; i len; i) { switch (state) { case ST_IDLE: if (buf[i] FRAME_HEAD) state ST_HEAD; break; case ST_HEAD: if (buf[i] FRAME_HEAD) // 连续两个帧头按协议处理 continue; cmd buf[i]; state ST_CMD; break; // ... 其余状态处理 } }另外还需要一个超时机制。如果收到半个包之后就再也没有后续字节一定要在超时后复位状态机否则下一个帧头到来之前会一直卡在中间状态。这个超时的经验值一般取“若干个字节间隔”串口波特率 115200 时5~10ms 超时就已经足够。解析完成之后还可以顺便在/proc或/sys下导出一些统计信息比如收了多少帧、校验错误多少次、解析成功多少次。这样在客户现场出了问题时能快速判断是协议解析问题还是电磁干扰问题。4. 从触摸数据到标准input事件串口触摸屏接入Linux输入子系统4.1 evdev/input子系统的基础Linux 下几乎所有输入设备——键盘、鼠标、触摸屏、遥控器——最终都会通过 input 子系统向上层报事件。用户空间的图形库经常监听/dev/input/eventXevdev就是这一层的内核驱动。串口屏的触摸功能要跟 Linux 生态无缝配合就得把坐标上报到 evdev。接入方式有两种用户态通过 uinput 模拟输入设备或者内核态写一个 input driver。uinput 的优点是极其灵活int fd open(/dev/uinput, O_WRONLY | O_NONBLOCK); ioctl(fd, UI_SET_EVBIT, EV_KEY); ioctl(fd, UI_SET_KEYBIT, BTN_TOUCH); ioctl(fd, UI_SET_EVBIT, EV_ABS); ioctl(fd, UI_SET_ABSBIT, ABS_X); ioctl(fd, UI_SET_ABSBIT, ABS_Y); ioctl(fd, UI_DEV_CREATE); struct input_event ev {0}; ev.type EV_ABS; ev.code ABS_X; ev.value x; write(fd, ev, sizeof(ev)); ev.type EV_SYN; ev.code SYN_REPORT; ev.value 0; write(fd, ev, sizeof(ev));用 uinput 时核心逻辑就是解析串口帧 → 算出真实坐标 → 写一个input_event→ 发送SYN_REPORT。这个链路非常清晰而且不用改内核、不用重新编译设备树适合大多数商业项目。内核态的 input driver 适合对延迟极其敏感、或者需要深度休眠唤醒的场景。实现上大致是注册一个input_allocate_device()创建的设备设置好ABS_X/ABS_Y的最小值和最大值然后在一个内核线程或中断上下文里调input_report_abs()和input_sync()。这种方法要求你对内核模块开发有足够的把握否则一个空指针就能让整个系统 panic。4.2 坐标换算与校准串口屏返回的坐标值取决于屏幕内部触摸面板的 ADC 采样范围。有些屏幕的 X 范围是 0~4095有些是 0~1023Y 范围也可能和屏幕分辨率不是严格对应。但 Linux 输入子系统上报的坐标范围必须是设备描述里声明的absmax否则用户态拿到之后还要二次换算。通常做法是把串口屏上报的原始值线性映射到屏幕的实际分辨率linux_x (raw_x - x_min) * (screen_width - 1) / (x_max - x_min) linux_y (raw_y - y_min) * (screen_height - 1) / (y_max - y_min)这里最容易忽略的一点是触摸面板的零点方向。有些屏幕的 X 坐标从左往右增加有些却是反的Y 轴也是一样。如果映射之后触摸点和显示点仍然是镜像的就要对坐标做反转。校准也不能只做一次。实际项目里屏幕可能被更换、屏幕玻璃面板存在装配误差、或者贴了不同厚度的保护膜都会导致触摸坐标漂移。Linux 桌面环境一般用 libinput 的校准矩阵嵌入式 Qt 环境可以用 tslib 的ts_calibrate生成校准参数按映射公式写进应用配置即可。经验上在显示画面正确的前提下先用evtest查看上报值再用一个小画布程序验证“按下位置和光标位置”是否一致是最快的校准方法。4.3 多点触摸和按键组合上报常见串口屏里带多点触摸的型号也不在少数。Linux 内部对多点触摸使用 MT 协议Type B 是比较主流的实现方式需要用到input_mt_slot和input_mt_report_slot_state。假设屏幕支持两个触摸点上报逻辑大概是input_mt_slot(dev, 0); input_mt_report_slot_state(dev, MT_TOOL_FINGER, true); input_report_abs(dev, ABS_MT_POSITION_X, x0); input_report_abs(dev, ABS_MT_POSITION_Y, y0); input_mt_slot(dev, 1); input_mt_report_slot_state(dev, MT_TOOL_FINGER, true); input_report_abs(dev, ABS_MT_POSITION_X, x1); input_report_abs(dev, ABS_MT_POSITION_Y, y1); input_mt_report_slot_state(dev, MT_TOOL_FINGER, false); input_sync(dev);注意协议解析层要怎么区分“多点”与“单点”。很多串口屏的触摸协议在单点触摸和多点触摸下走的是不同的命令字不能混用。如果协议文档不够详细可以在屏幕上放两个手指慢慢移动直接打印收到的十六进制原始数据来反推。还有一类串口屏是做“控件跳转”的屏幕上的按钮会主动上报“按键编号”而不是绝对坐标。这种场景下更简单——直接把按键编号映射成EV_KEY事件交给上层应用处理。但要注意区分触摸类是单击还是长按否则上层会收到一大批重复按键事件。5. 实测中的问题清单乱码、丢帧、漂移与开机时序5.1 乱码和串口参数不一致乱码是最容易定位、也最常见的问题。大部分串口屏出厂默认波特率是 9600 或者 115200如果你的应用stty设置成了 57600或者内核 console 启动参数里用了别的串口参数就会出现一堆乱码。排错时先做三件事stty -F /dev/ttyS3 115200 raw -echo cat /dev/ttyS3第一确认内核启动阶段没有往这个串口打印日志第二确认上位机的波特率和屏幕固件一致第三确认串口电平匹配。TTL 串口屏接 RS232 电平转换器或者在相反方向接板载 RS232 口都会出现不稳定乱码。这里再顺便提一个容易忽略的校验位问题。很多串口屏默认是8N1也就是 8 个数据位、无校验、1 个停止位。但用老款迪文屏或某些工业屏时可能会是8E1或8O1。如果应用层一直用 8N1 去读偶校验的屏幕偶尔能收到正确数据但整体上是隔三差五乱码。5.2 丢帧与缓冲区配置“丢帧”从表面看是触摸卡顿、指令没反应但底层原因往往在 Linux 的数据链路里。一个常见原因是用户态读取线程被高负载任务拖住串口接收缓冲区溢出后新来的数据被丢弃。Linux tty 驱动本身有接收缓冲区但默认大小有限。如果你确定某路串口数据量比较大或者读取线程确实响应不及时可以尝试调大 tty 的 flip buffer 或者使用TIOCVHANGUP清理缓冲。更常见的做法是提高读取线程优先级struct sched_param param { .sched_priority 50 }; pthread_setschedparam(thread_id, SCHED_FIFO, param);如果平台支持把当前线程绑到和 UART 中断亲和性一致的 CPU 核上也能减少调度延迟。另外一个容易造成“数据看起来丢了”的原因是 DMA 未配置好却开着 DMA。DMA 描述符不够时UART 收到的数据没有及时搬到内存硬件 FIFO 一旦溢出就会丢字节。遇到这种情况直接看/proc/interrupts里对应串口的中断计数增长是否正常和dmesg里有没有报DMA timeout之类的错误。5.3 触摸漂移与坐标零点错误触摸漂移最典型的症状是“点击屏幕右上角光标却出现在左上角附近”。除了第 4 章说的坐标映射和零点方向问题之外还有一种漂移是“越往边缘越偏”这通常说明屏幕触摸面板本身不是线性的。很多串口屏支持在屏幕内部做触摸校准你可以在屏的工程配置里手动校准一次之后再往 Linux 侧输出坐标。这种二次校准的效果往往比纯线性映射更准因为屏幕内部的校准固件能修正非线性误差。如果换了不同批次、不同尺寸的串口屏校准参数务必要重新做一遍。不要为了图省事把上一块屏的校准文件直接拷贝到新设备里尤其是从老型号屏幕换到新型号屏幕时坐标范围可能完全不一样。5.4 上电时序与串口屏的“假故障”最后说一下上电时序。很多工控板是 Linux 系统和串口屏同时上电串口屏内部 MCU 启动可能需要几百毫秒甚至一秒而 Linux 用户态程序可能更早地尝试向串口屏发初始化指令。这时候屏幕还没就绪程序等不到应答就会误判为“串口屏坏了”。解决方式是加一个初始化握手机制。程序打开串口节点后不急着发业务数据先发送一条查询指令等待屏幕返回确定应答连续重试几次都失败才报错。同样如果系统里有独立看门狗喂狗线程应该和串口读取线程分开。因为读串口偶尔会阻塞如果让同一个线程既读串口又喂狗一旦串口屏瞬时无响应看门狗可能先超时复位整个系统造成更严重的问题。6. 选型与工程落地建议从STM32到Linux的迁移经验6.1 串口屏品牌选型时的内核视角市面上常见的串口屏品牌比如淘晶驰、迪文、大彩都有各自的开发生态。选型时除了看屏幕分辨率、色深、UI 组态软件好不好用之外还要特别关注两点。第一串口协议是否公开、文档是否完整。有的屏幕协议只在厂商的组态软件里封装不提供公开指令表这种就很难在 Linux 下稳定扩展。第二触摸上报是否走同一路串口。有些屏幕支持触摸数据通过单独引脚输出有些必须和显示指令共用同一路 UART这会直接影响你的串口资源配置。另外新旧型号切换也要留个心眼。某些品牌的老型号屏幕固件和新型号使用不同的协议格式代码里最好不要把协议解析写死在业务逻辑里而是单独抽象一层方便后面换屏时只改协议适配。6.2 内核裁剪与系统打包时的注意事项如果产品要量产内核裁剪是绕不开的一步。裁剪时最容易犯的错误就是把“看起来没用”的输入子系统给裁掉了。串口屏虽然不走 I2C 触摸芯片但它一旦通过 uinput 注入输入事件就需要CONFIG_INPUT_UINPUT上层图形库读取触摸点则需要CONFIG_INPUT_EVDEV。CONFIG_INPUTy CONFIG_INPUT_EVDEVy CONFIG_INPUT_UINPUTy CONFIG_SERIAL_8250y CONFIG_SERIAL_COREy这几个选项建议直接编进内核而不是编译成模块否则系统启动时如果没有提前insmod/dev/input目录下就会少设备节点。同时还要注意 udev 规则。串口屏设备节点权限如果不做处理普通用户态程序可能打不开/dev/ttySx。在开发板阶段直接跑 root 可能没感觉到了产品要做降权运行时节点权限就会变成一个问题。KERNELttyS[0-9]*, MODE0660, GROUPdialout KERNELuinput, MODE0660, GROUPinput6.3 和裸机开发不同的几个思维转变从 STM32 裸机方案迁移到 Linux最大的差异是多任务环境。裸机里你可以写一个while(1)循环原地等待串口数据完全可控但在 Linux 里会有蓝牙、WiFi、看门狗、业务线程等一大堆进程抢 CPU 和中断资源串口读取线程必须用阻塞式 IO 加超时而不是忙等。另一个差异是调试方式。裸机可以直接用 JTAG 打断点看寄存器Linux 下判断串口问题就应该先用evtest、stty、dmesg、/proc/tty/driver/serial这些工具和接口做快速定位而不是一上来就改内核代码。串口屏项目里绝大多数问题都出在配置不在驱动本身。最后分享一个小经验如果串口屏在开发板上工作正常换到量产主板上出现触摸漂移或乱码先别急着怀疑代码优先检查主板串口电平参考电压和信号完整性。很多小批量主板会把 UART 走线拉得太长又没加 ESD 防护和上拉电阻干扰引起的丢帧比软件问题更难查。在硬件端正、协议解析正确的基础上内核和用户态的配置只要按我前面说的几个原则去处理这套方案基本能稳定跑很久。
返回列表