☰
高通MSM串口驱动msm_serial.c深度解析:从tty框架到设备树调试实战
2026/10/14 16:59:56 网站建设 项目流程

1. 为什么值得花时间啃 msm_serial.c

做过 Android 底层的人大概都有这个体会:上层应用写得再花哨,一旦串口不通,整块板子就是块砖。而高通平台上的串口驱动,绕来绕去最终都会落到msm_serial.c这个文件上。它是高通 MSM 系列 SoC 串口控制器的核心驱动实现,负责把 Linux 的 tty 框架和硬件 UART 寄存器桥接起来,让上层能通过/dev/ttyHSLx这类设备节点收发数据。

这个文件能做什么?简单说,它管着串口的初始化、波特率配置、FIFO 读写、中断处理、DMA 传输、流控以及电源管理。你调试内核日志、接 GPS 模块、连蓝牙 HCI、跑工厂测试,背后大概率都有它在干活。适合谁来参考?一是刚接触 Android BSP 的驱动工程师,二是需要排查串口丢数据、乱码、打不开设备节点的系统集成人员,三是想理解 Linux tty 驱动模型怎么落到具体硬件上的学习者。

我自己的经历是,第一次看这个文件时被里面一堆msm_port、uart_port、msm_serial_pops绕得头晕,后来把 tty 驱动分层模型理清楚,再回头看就顺了。这篇就把我踩过的坑、理出来的逻辑、以及实际调试中真正管用的手段,完整地摊开讲一遍。

2. 串口驱动整体设计与 msm_serial.c 的定位

2.1 Linux tty 驱动分层模型回顾

要读懂msm_serial.c,先得知道它在整个 tty 体系里站在哪一层。Linux 的串口驱动是典型的分层结构,从上到下大致是:用户空间通过/dev/ttyHSLx访问,进入 tty 核心层(tty_io.c、n_tty.c),再往下是 tty 线路规程(line discipline),然后是串口核心层(serial_core.c),最后才是具体硬件的驱动,也就是msm_serial.c这一层。

serial_core.c定义了一套uart_ops结构体,把打开、关闭、发送、接收、设置波特率、流控等操作抽象成函数指针。msm_serial.c的工作就是实现这套uart_ops,把通用操作翻译成对 MSM UART 寄存器的读写。这种设计的好处是,换一个 SoC 只需要换最底层驱动,上面的 tty 框架、线路规程、用户接口全都不用动。

理解这一点很关键,因为很多人看msm_serial.c时搞不清某个函数到底是被谁调用的。比如msm_serial_startup不是用户直接调的,而是serial_core在打开设备时通过uart_ops回调进来的。把调用链理清楚,读代码就不会迷路。

2.2 高通 MSM UART 硬件特性概览

高通 MSM 系列 SoC 上的 UART 控制器,和标准 8250 有相似之处,但寄存器布局和 FIFO 行为有自己的特点。它通常支持多个 UART 实例,每个实例有独立的寄存器基地址、时钟、中断号和 GPIO 配置。硬件上一般具备发送和接收 FIFO,支持可编程的触发阈值,支持硬件流控(RTS/CTS),部分实例还支持 DMA 传输以降低 CPU 占用。

在msm_serial.c里,这些硬件特性通过struct msm_port来管理,它内嵌了一个struct uart_port,再额外挂上 MSM 特有的字段,比如 DMA 通道、时钟句柄、GPIO 状态等。这种“内嵌通用结构 + 扩展私有数据”的写法在 Linux 驱动里非常常见,好处是既能被通用框架识别,又能保存硬件私有信息。

需要特别注意的是,MSM UART 的 FIFO 深度和触发阈值直接影响到中断频率和吞吐。如果阈值设得太低,每个字节都触发中断,CPU 会被打断到崩溃;设得太高,又可能因为 FIFO 溢出丢数据。这个平衡点在不同波特率下是不一样的,后面讲参数配置时会细说。

2.3 为什么选择 platform driver 模型

msm_serial.c采用的是 platform driver 模型,而不是 PCI 或 USB 那种可热插拔的总线模型。原因很直接:MSM UART 是 SoC 内部集成的外设,地址、中断、时钟这些资源在设备树里静态描述,不存在运行时枚举的问题。platform driver 的 probe 流程由设备树匹配触发,拿到资源后初始化硬件,注册 uart_port,整个过程清晰可控。

用 platform 模型还有个好处是电源管理好做。SoC 内部外设的时钟和电源域通常和系统休眠强相关,platform driver 可以很方便地挂到 runtime PM 框架上,在串口空闲时关时钟省电,有数据时再唤醒。这在移动设备上尤其重要,毕竟电池续航是硬指标。

提示:读msm_serial.c时,先找到of_device_id匹配表,看清楚它匹配的是设备树里哪个 compatible 字符串,再顺着 probe 函数往下看,这样能快速建立整体印象。

3. 核心数据结构与关键函数拆解

3.1 msm_port 结构体的字段含义

struct msm_port是整个驱动的核心数据结构,理解它每个字段的作用,等于拿到了读代码的钥匙。它内嵌struct uart_port uart,这是给serial_core用的通用部分;然后是struct msm_uart_dma dma,管理 DMA 相关资源;还有struct clk *clk、struct clk *pclk这类时钟句柄,以及int irq、void __iomem *base等。

我建议读的时候拿张纸,把每个字段和它在哪里被赋值、在哪里被读取列出来。比如msm_port->uart.membase在 probe 里通过devm_ioremap_resource拿到,之后所有寄存器读写都基于它。再比如msm_port->dma只有在启用 DMA 模式时才会被初始化,普通中断模式下它是空的。把字段的生命周期搞清楚,代码逻辑自然就通了。

有个容易忽略的点是msm_port和uart_port之间的转换。驱动里到处是to_msm_port(port)这样的宏,本质是container_of,通过内嵌的uart_port反推出外层的msm_port。这个技巧在 Linux 内核里遍地都是,不熟悉的话会觉得莫名其妙,熟悉了就知道这是实现“继承”效果的标准手法。

3.2 uart_ops 回调函数的实现要点

msm_serial_pops这个uart_ops结构体里定义了一堆回调,每个都对应一个具体操作。挑几个最关键的说说。

msm_serial_startup在设备被打开时调用,负责申请中断、初始化硬件、启动接收。这里有个细节:中断申请用的是request_irq还是devm_request_irq,直接影响出错时要不要手动释放。现在主流写法都用devm_系列,省心。

msm_serial_set_termios是配置波特率、数据位、停止位、校验位的地方。这个函数比较长,因为它要处理各种参数组合,还要考虑硬件流控的开关。实际调试中波特率不对导致乱码,十有八九是这里的分频计算出了问题。

msm_serial_handle_rx和msm_serial_handle_tx是中断处理的核心,分别处理接收和发送。接收时要判断是 FIFO 触发还是超时,发送时要判断 FIFO 是否还有空间。这两个函数的效率直接决定串口的吞吐能力。

msm_serial_shutdown是关闭时的清理函数,和 startup 对称,释放中断、关时钟、复位硬件。写驱动时对称性很重要,startup 里申请了什么,shutdown 里就要释放什么,否则反复打开关闭会资源泄漏。

3.3 中断处理与 DMA 传输的取舍

MSM UART 支持两种数据传输方式:中断模式和 DMA 模式。中断模式下,每收到一批数据或发送 FIFO 有空位就触发中断,CPU 逐字节或逐块搬运。DMA 模式下,数据在 FIFO 和内存之间直接搬,CPU 只在传输完成时被通知一次。

怎么选?看数据量和实时性要求。低波特率、小数据量、对延迟敏感的场合,中断模式更合适,因为 DMA 有建立传输的开销,小包反而更慢。高波特率、大数据量、CPU 负载重的场合,DMA 能显著降低 CPU 占用。msm_serial.c里通常通过设备树属性或模块参数来控制是否启用 DMA。

实际项目中我遇到过一种情况:默认开了 DMA,但某个外设发的是不定长小包,结果每次 DMA 传输都要等超时才能结束,延迟反而比中断模式大。后来改成中断模式,问题消失。所以这个取舍不能想当然,要结合具体外设的行为来定。

4. 实操:从设备树到设备节点的完整链路

4.1 设备树节点的关键属性配置

在高通平台上,UART 的配置信息写在设备树里。一个典型的 UART 节点会包含compatible、reg、interrupts、clocks、clock-names、pinctrl等属性。compatible用来和驱动匹配,reg描述寄存器地址范围,interrupts描述中断号,clocks和clock-names描述时钟源。

这里有个坑:clock-names的顺序和驱动里clk_get的顺序必须一致,否则拿到的时钟就错了。我曾经因为把core和iface两个时钟的顺序写反,导致串口能打开但一发数据就卡死,查了半天才发现是时钟搞混了。

另外pinctrl配置也很关键,TX、RX、RTS、CTS 四个引脚的复用功能要设对。如果引脚复用没配对,寄存器读写都正常,但物理线上就是没信号。这种问题用示波器一量就清楚,但纯看代码很难发现。

4.2 probe 函数的执行流程

probe 函数是驱动的入口,执行流程大致是:匹配设备树、获取寄存器资源并映射、获取中断、获取并使能时钟、配置引脚、初始化msm_port、注册uart_port。

每一步都可能出错,所以好的驱动会在每步失败时打印明确的错误信息并正确回滚。读代码时可以重点关注错误处理路径,看看它是不是在失败时把之前申请的资源都释放了。有些老代码错误处理不完整,probe 失败后残留资源,导致下次加载驱动失败,这种问题很隐蔽。

注册uart_port用的是uart_add_one_port,成功后serial_core会创建对应的 tty 设备节点。节点名字通常由uart_port的line字段决定,比如 line 为 0 就是ttyHSL0。如果设备节点没出现,先确认uart_add_one_port的返回值,再看serial_core的日志。

4.3 波特率计算与寄存器配置实例

波特率的计算是串口驱动里最容易出错的地方之一。MSM UART 的波特率由时钟频率和分频系数决定,公式大致是:波特率 = 时钟频率 / (分频系数)。分频系数又分整数部分和小数部分,写进不同的寄存器。

假设时钟是 7.3728 MHz,目标波特率是 115200,那么分频系数 = 7372800 / (16 × 115200) ≈ 4.0。这里 16 是过采样倍数,MSM UART 通常用 16 倍过采样。算出整数部分是 4,小数部分是 0,写进对应寄存器即可。

但实际时钟往往不是理想值,比如 7.3728 MHz 换成 7.68 MHz,算出来分频系数是 4.1666...,小数部分就要取近似值。近似带来的误差如果超过 2%,通信就可能不稳定。所以选时钟源时,尽量选能让常用波特率整除的频率。这个细节在硬件设计阶段就要考虑,软件阶段只能尽量补偿。

注意:改波特率后一定要用示波器或逻辑分析仪量一下实际波形,确认误差在可接受范围内。光看代码算对了不算数,硬件上跑对才算数。

5. 常见问题与排查技巧实录

5.1 串口打不开或设备节点缺失

设备节点缺失是最常见的问题。排查顺序是:先看驱动有没有加载成功,dmesg里搜msm_serial相关日志;再看设备树匹配有没有成功,probe 有没有被调用;然后看uart_add_one_port返回值;最后看serial_core有没有报错。

如果 probe 根本没被调用,多半是compatible字符串不匹配,或者设备树节点状态是disabled。如果 probe 被调用了但失败了,看错误码,-EPROBE_DEFER说明依赖的资源还没准备好,比如时钟或引脚控制器还没初始化,这种情况驱动会自动重试,不用慌。

还有一种情况是节点出现了但打不开,报Permission denied。这通常是权限问题,检查/dev/ttyHSLx的属主和权限位,必要时在uevent规则里加一条。

5.2 数据乱码与波特率偏差

乱码基本可以锁定是波特率问题。先确认两端波特率设置一致,再确认驱动里算出来的分频系数对不对。可以在set_termios里加打印,把时钟频率、目标波特率、算出的分频系数都打出来,和手册里的公式对一遍。

如果分频系数算对了还是乱码,检查时钟源实际频率是不是和预期一致。有些平台的时钟会被其他模块分频或门控,实际频率和标称值不一样。用clk_get_rate读一下实际频率,别想当然。

还有一种乱码是数据位、停止位、校验位不匹配导致的。这种乱码有特征,比如固定位置出错,或者每帧都错。用逻辑分析仪抓几帧波形,一眼就能看出来。

5.3 丢数据与 FIFO 溢出

丢数据通常和 FIFO 阈值、中断延迟、CPU 负载有关。如果接收 FIFO 阈值设得太高,数据在 FIFO 里等太久,新数据来了就溢出。如果中断处理太慢,FIFO 满了还没被读走,也会溢出。

排查时先看 FIFO 阈值配置,适当调低接收阈值,让中断更早触发。然后看中断处理函数里有没有耗时操作,比如打印日志、加锁等待。中断上下文里绝对不能做耗时的事,该放到工作队列的就放过去。

高波特率下丢数据,考虑启用 DMA 或硬件流控。硬件流控能让发送方在接收方来不及处理时暂停,从根源上避免溢出。但流控需要硬件支持,引脚也要接对,不是所有场景都能用。

5.4 常见问题速查表

现象可能原因排查手段解决方向
设备节点缺失驱动未加载/设备树不匹配dmesg 查日志修 compatible 或加载驱动
打不开设备权限不足/资源被占查权限位和占用进程改权限或释放占用
数据乱码波特率/帧格式不匹配逻辑分析仪抓波形统一两端配置
丢数据FIFO 溢出/中断延迟查阈值和中断耗时调阈值或启用 DMA
发送卡死时钟配置错误查时钟频率和顺序修正时钟配置
休眠后失效电源管理未恢复查 runtime PM 回调补全恢复逻辑

6. 调试手段与性能优化经验

6.1 用 ftrace 和动态调试跟踪调用链

内核里调试驱动,printk是最直接的手段,但加多了会影响时序,尤其在中断路径里。更好的办法是用 ftrace 跟踪函数调用,或者用 dynamic debug 按需打开日志。

ftrace 可以跟踪msm_serial_handle_rx这类函数的调用频率和耗时,帮你判断瓶颈在哪。dynamic debug 则可以在运行时开关某条dev_dbg语句,不用重新编译内核。这两个工具配合使用,排查效率比满屏printk高得多。

我习惯在怀疑中断处理有问题时,先用 ftrace 看中断触发频率,如果频率异常高,说明 FIFO 阈值设低了;如果频率正常但数据还是丢,说明处理太慢,得优化处理逻辑。

6.2 降低中断频率的阈值调优

FIFO 阈值调优是个经验活。原则是:在保证不溢出的前提下,尽量提高阈值以降低中断频率。接收阈值一般设为 FIFO 深度的一半到四分之三,发送阈值设为四分之一到一半。

但这不是死规矩。如果外设发的是突发大数据,接收阈值要调低,让中断更早触发,给 CPU 更多时间搬运。如果外设发的是稳定小数据流,接收阈值可以调高,减少中断次数。实际调的时候,一边改阈值一边压测,看丢包率和 CPU 占用,找到平衡点。

6.3 电源管理对串口稳定性的影响

移动设备上,串口驱动必须配合电源管理。空闲时关时钟省电,有数据时开时钟工作。但开关时钟有延迟,如果处理不当,可能在时钟还没稳定时就去读写寄存器,导致数据丢失或硬件异常。

msm_serial.c里通常用 runtime PM 来管理时钟。打开设备时pm_runtime_get_sync,关闭时pm_runtime_put。关键是确保所有寄存器访问都在时钟开启的状态下进行,中断处理函数里也要注意这一点。

我遇到过休眠唤醒后串口失效的问题,最后发现是唤醒时时钟恢复的顺序不对,先访问了寄存器再开时钟。调整顺序后问题解决。这类问题在移动平台很常见,调试时要有耐心。

7. 从 msm_serial.c 延伸出的通用驱动思维

7.1 分层抽象带来的可移植性

msm_serial.c最值得学习的地方,是它如何把硬件相关和硬件无关的代码清晰分开。serial_core处理通用逻辑,msm_serial只处理 MSM 特有的寄存器操作。这种分层让驱动容易移植,也让上层框架稳定。

写其他驱动时也可以借鉴这个思路:先想清楚哪些是通用的,哪些是硬件特有的,把通用部分抽象出来,特有部分单独实现。这样代码清晰,维护成本低,换硬件时改动也小。

7.2 中断上下文的编写纪律

中断处理函数里能做的事很有限:不能睡眠、不能长时间持锁、不能做耗时计算。msm_serial.c的中断处理严格遵守这些纪律,把耗时的事推到工作队列或线程化中断里。

这个纪律说起来简单,做起来容易忘。尤其是在中断里加个printk调试,忘了删,上线后在高频中断下直接把系统拖垮。我的习惯是调试代码用dev_dbg而不是printk,默认不输出,需要时再开。

7.3 资源管理的对称性原则

startup 和 shutdown 对称,probe 和 remove 对称,这是驱动健壮性的基础。msm_serial.c在这方面做得比较规范,申请和释放成对出现。自己写驱动时,可以列个清单,把申请的资源都记下来,确保每个都有对应的释放路径,包括错误处理路径。

错误处理路径最容易被忽略。probe 中途失败时,前面申请的资源要全部释放,否则残留资源会导致下次加载失败。用devm_系列函数能省不少事,但也不是万能的,有些资源还是得手动管理。

8. 我个人在实际调试中的几点体会

调串口驱动这些年,最大的体会是:代码看得再多,不如动手量一次波形。很多问题在代码层面看是通的,硬件上就是不对,这时候逻辑分析仪和示波器比任何调试手段都管用。我现在的习惯是,只要串口行为异常,先抓波形,确认物理层没问题,再往驱动层查。

第二个体会是,设备树配置的错误比驱动代码的错误更常见。驱动代码通常是验证过的,设备树却是每个项目都要重新配的,引脚、时钟、中断这些资源一多,很容易配错。所以排查问题时,我一般先怀疑设备树,再怀疑驱动。

第三个体会是,电源管理相关的问题最隐蔽。系统正常运行时一切正常,一休眠唤醒就出问题,而且不一定每次都复现。这类问题要有耐心,多测几轮,把休眠唤醒的日志打开,一步步看状态恢复的顺序。

最后分享一个小技巧:在msm_serial.c里加一个模块参数,用来在运行时切换中断模式和 DMA 模式。这样不用重新编译内核就能对比两种模式的表现,调优时特别方便。这个参数上线前记得去掉或设默认值,别留在生产版本里。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询