news 2026/2/2 11:47:32

Linux中serial驱动开发深度剖析与实例解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux中serial驱动开发深度剖析与实例解析

Linux串口驱动开发:从框架到实战的深度拆解

你有没有遇到过这样的场景?
在调试一块嵌入式板子时,minicom里数据乱码、丢包严重;或者刚写完的自定义UART驱动,一cat /dev/ttyS1就卡死系统。更离谱的是,明明硬件接线没问题,但波特率就是对不上——这些看似“玄学”的问题,背后其实都藏着Linux串口驱动机制的影子。

别急着换芯片或重做PCB,先搞清楚内核是怎么和你的UART控制器“对话”的。本文不讲泛泛而谈的API列表,而是带你钻进Linux串口子系统的内核深处,从uart_driver注册开始,一步步走到中断处理、tty流转、用户空间交互,最后用真实代码告诉你:为什么有些寄存器必须在特定时机设置,为什么ISR里不能打印日志,以及如何写出一个稳定可靠的串口驱动。


一个串口设备是如何被“看见”的?

我们常说“打开/dev/ttyS0”,但这背后发生的事远比 fopen 多得多。在Linux中,每个串口设备都不是孤立存在的,它属于一套分层管理的驱动架构——最上层是用户可见的字符设备节点,中间是统一的serial core子系统,底层才是具体的硬件操作逻辑。

这套设计的核心思想很简单:让平台无关的部分由内核统一处理,让开发者只关心自己的硬件差异

举个例子,无论是TI的AM335x还是全志的D1,只要它们有UART控制器,就可以共用同一套 tty 层逻辑(比如回显、流控、termios配置)。你要做的,只是告诉内核:“我这儿有个串口,地址是0x48020000,中断号是72,时钟频率是48MHz。”剩下的创建设备节点、处理read/write调用、管理接收缓冲区等工作,全部交给 serial core 自动完成。

那么,你是怎么“告诉”内核这些信息的呢?答案就在两个关键结构体中:struct uart_driverstruct uart_port


uart_driver:我不是端口,我是“端口工厂”

很多人初学时容易混淆uart_driveruart_port。记住这个比喻:

uart_driver是工厂营业执照,uart_port是流水线上的具体机器。

也就是说,uart_driver不代表任何一个物理串口,而是描述一类串口设备的公共属性。比如你有一块板子上有3个UART控制器,那你就只需要定义一个uart_driver,然后为每个控制器实例化一个uart_port

来看它的核心字段:

字段作用说明
.owner必须设为THIS_MODULE,防止模块卸载时还在被引用
.driver_name驱动名,出现在/proc/tty/drivers列表中
.dev_name设备节点前缀,如 “ttyS” 或 “ttyAMA”
.major/.minor主次设备号范围,通常主设备号固定为4(TTY_MAJOR)
.nr最大支持的端口数量

典型的初始化如下:

static struct uart_driver my_uart_drv = { .owner = THIS_MODULE, .driver_name = "my_uart", .dev_name = "ttyMY", .major = TTY_MAJOR, .minor = 64, .nr = 2, // 支持两个串口 };

注册时只需一行:

ret = uart_register_driver(&my_uart_drv); if (ret) { pr_err("无法注册串口驱动\n"); return ret; }

这一步完成后,你会发现/proc/tty/drivers多了一行输出:

my_uart /dev/ttyMY 4 64-65 serial

注意:此时还没有任何实际的硬件操作,也没有申请中断或映射寄存器。这只是向内核声明:“我可以提供最多2个名为 ttyMY0 和 ttyMY1 的串口设备”。

真正的硬件绑定,要等到uart_port被添加进来。


uart_port:你的硬件身份证

如果说uart_driver是工厂执照,那uart_port就是你每一台设备的“身份证”。它包含了所有与硬件直接相关的运行时信息。

关键字段详解:

字段说明
.iotype访问方式:UPIO_MEM表示内存映射(常见于SoC),UPIO_PORT是x86的I/O端口
.mapbase寄存器物理地址
.membase映射后的虚拟地址(需 ioremap 后赋值)
.irq使用的中断号
.uartclk串口时钟源频率(决定波特率精度)
.fifosizeFIFO深度,影响收发策略
.ops指向struct uart_ops,定义了所有硬件操作函数
.line在当前驱动中的索引号(从0开始)

典型定义:

static struct uart_port my_ports[] = { { .iotype = UPIO_MEM, .mapbase = 0x101f1000, .irq = IRQ_UART0, .uartclk = 14745600, .fifosize = 16, .ops = &my_uart_ops, .flags = UPF_BOOT_AUTOCONF, .line = 0, }, { .iotype = UPIO_MEM, .mapbase = 0x101f2000, .irq = IRQ_UART1, .uartclk = 14745600, .fifosize = 16, .ops = &my_uart_ops, .flags = UPF_BOOT_AUTOCONF, .line = 1, } };

注册流程也很清晰:

for (i = 0; i < ARRAY_SIZE(my_ports); i++) { ret = uart_add_one_port(&my_uart_drv, &my_ports[i]); if (ret) pr_err("添加端口 %d 失败\n", i); }

到这里,设备节点/dev/ttyMY0/dev/ttyMY1才真正可用。但如果你现在就去读它,会发现什么也收不到——因为还没启用中断,也没配置波特率。


中断来了!谁来处理?uart_ops说了算

struct uart_ops是一组函数指针,相当于串口硬件的“操作说明书”。当内核需要打开端口、发送数据、修改波特率时,就会回调这里面的函数。

其中最重要的三个函数是:

  • .startup():端口首次打开时调用,用于申请中断、使能时钟
  • .shutdown():关闭端口时释放资源
  • .set_termios():处理 termios 结构,重新配置波特率、数据位等
  • .handle_irq():可选,自定义中断处理入口

但我们通常不会自己实现.handle_irq(),而是通过 request_irq 注册一个标准中断服务程序(ISR),在里面解析状态寄存器并调用 serial core 提供的 API。

下面是一个典型的中断处理函数:

static irqreturn_t my_uart_isr(int irq, void *dev_id) { struct uart_port *port = dev_id; unsigned long flags; unsigned int status, ch, flag; spin_lock_irqsave(&port->lock, flags); status = readl(port->membase + UART_STATUS_REG); /* 是否有数据到达? */ if (status & RX_INT_PENDING) { while ((readl(port->membase + UART_LSR) & LSR_DR)) { ch = readl(port->membase + UART_RBR); // 读数据 flag = TTY_NORMAL; /* 处理错误标志 */ if (status & (LSR_OE | LSR_PE | LSR_FE)) { if (status & LSR_OE) flag = TTY_OVERRUN; if (status & LSR_PE) flag = TTY_PARITY; if (status & LSR_FE) flag = TTY_FRAME; status &= ~(LSR_OE | LSR_PE | LSR_FE); } /* 将数据送入tty层缓冲区 */ uart_insert_char(port, status, LSR_OE, ch, flag); } /* 告诉上层:有新数据到了! */ tty_flip_buffer_push(port->state->port.tty); } /* 发送完成中断:尝试发送下一个字节 */ if (status & TX_EMPTY_INT) { uart_write_wakeup(port->state->port.tty); } spin_unlock_irqrestore(&port->lock, flags); return IRQ_HANDLED; }

重点来了:你在 ISR 中不能直接把数据拷贝给用户空间!

正确的做法是:
1. 用uart_insert_char()把接收到的数据放入tty flip buffer
2. 调用tty_flip_buffer_push()触发数据上抛
3. 内核会在下半部(softirq)将数据复制到 line discipline 缓冲区
4. 用户调用read()时才会真正拿到数据

这样做的好处是避免在中断上下文中做耗时操作,提升系统响应性。


数据是怎么从硬件流到用户空间的?

整个路径可以简化为一条链路:

硬件 UART → 中断触发 → ISR读取RBR → uart_insert_char() → tty_flip_buffer → tty_ldisc_buffer → 用户 read() 返回

反过来,写数据的流程是:

用户 write() → tty layer 接收 → ldisc 处理(如XON/XOFF) → serial core 调用 port->ops->start_tx() → 启动中断或DMA发送

这种分层模型带来了几个显著优势:

  • 标准化接口:应用层无需关心底层是16550A还是S3C2410
  • 灵活的线路规程:支持 N_TTY(默认)、PPP、HCI(蓝牙)等多种模式
  • 内置流控机制:RTS/CTS 硬件流控、XON/XOFF 软件流控自动生效
  • 异步I/O支持:poll/select/epoll 可监控串口状态变化

你可以用下面这条命令验证是否真的通了:

echo "hello" > /dev/ttyMY0

如果对方串口终端能看到输出,恭喜你,驱动已经跑通!


开发中那些踩过的坑,我都替你试过了

🛑 痛点一:频繁丢包(Overrun)

现象:高波特率下(如 115200bps+)接收数据丢失严重。

原因分析:
- 中断延迟太高(被其他高优先级中断抢占)
- ISR执行时间太长(比如加了大量printk)
- FIFO溢出后未及时清空中断标志

解决方案:
-缩短临界区:只在必要时持锁,避免在ISR中调用复杂函数
-提高中断优先级:使用request_threaded_irq()分离顶半部和底半部
-启用DMA:对于大数据量传输,DMA比中断更高效
-合理设置FIFO触发级别:不要一味追求低延迟

🛑 痛点二:波特率不准

现象:两边都是9600bps,但通信失败。

根本原因:uartclk设置错误,导致分频后的实际波特率偏差过大。

解决方法:
1. 精确填写.uartclk字段(查手册确认时钟源)
2. 实现.set_termios()中的分频算法:

void set_baud_rate(struct uart_port *port, unsigned int baud) { unsigned int quot = (port->uartclk / (16 * baud)); writel(quot & 0xFF, port->membase + DLL); writel((quot >> 8) & 0xFF, port->membase + DLM); }

建议:使用uart_get_baud_rate()uart_update_timeout()辅助函数,它们已集成常见校验逻辑。

🛑 痛点三:并发访问冲突

现象:多线程同时读写串口时系统崩溃或数据错乱。

原因:多个上下文(进程、中断)同时访问寄存器。

正确做法:
- 所有硬件访问必须加锁:使用spin_lock_irqsave(&port->lock)
- 避免在中断中睡眠(不能调用 copy_to_user、kmalloc(GFP_KERNEL) 等)


高阶技巧:让你的驱动更健壮

✅ 支持设备树动态探测

现代Linux倾向于使用设备树(Device Tree)来描述硬件。你可以添加匹配表:

static const struct of_device_id my_uart_dt_ids[] = { { .compatible = "myvendor,my-uart-v1" }, { } }; MODULE_DEVICE_TABLE(of, my_uart_dt_ids);

并在 probe 函数中解析 reg、interrupts 属性,实现自动配置。

✅ 实现电源管理

支持 suspend/resume 很简单:

static void my_uart_pm(struct uart_port *port, unsigned int state, unsigned int oldstate) { if (state == 0) { // resume: 重新使能时钟、恢复寄存器 } else { // suspend: 关闭时钟、保存关键寄存器 } } // 在 ops 中注册 .ops = { .pm = my_uart_pm, // ... };

✅ 调试利器推荐

  • 查看中断统计:cat /proc/interrupts | grep uart
  • 启用控制台输出:配置CONFIG_SERIAL_CORE_CONSOLE=y并绑定 earlycon
  • 添加调试日志:使用dev_dbg()替代 printk,可通过 dynamic_debug 控制开关

写到最后:为什么你还得懂这套老古董机制?

也许你会问:现在都2025年了,谁还用串口?

答案是:几乎所有嵌入式设备都在用

  • 调试通道:uboot、kernel启动日志唯一可靠输出方式
  • 工业现场:PLC、传感器、电表仍广泛使用 RS485/RS232
  • 模组通信:GPS、LoRa、NB-IoT 模块大多通过串口对接
  • 远程维护:即使以太网断开,串口也能连上去救砖

更重要的是,串口驱动是理解Linux字符设备模型的最佳入口。一旦你掌握了uart_port如何与 tty layer 协作、中断如何推动数据流动、用户空间如何通过标准接口访问硬件——再去学 I2C、SPI、甚至网络驱动,都会轻松很多。

所以,下次当你面对一个全新的SoC手册,看到那一排UART控制器寄存器时,不要再一头雾水。你应该想的是:

“好,我要先注册一个 driver,然后为每个 port 填好 mapbase 和 irq,再实现 ops 的 startup 和 set_termios……”

这才是一个合格的嵌入式开发者该有的思维路径。

如果你正在开发一款带有多串口扩展能力的边缘网关,或者需要对接某个奇葩通信协议的工控设备,欢迎在评论区分享你的挑战,我们一起拆解。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/1/30 5:06:12

APA第7版格式生成器:学术写作终极效率工具

APA第7版格式生成器&#xff1a;学术写作终极效率工具 【免费下载链接】APA-7th-Edition Microsoft Word XSD for generating APA 7th edition references 项目地址: https://gitcode.com/gh_mirrors/ap/APA-7th-Edition 还在为学术论文的参考文献格式烦恼吗&#xff1f…

作者头像 李华
网站建设 2026/1/31 20:43:28

StructBERT零样本分类器案例:法律文书自动分类

StructBERT零样本分类器案例&#xff1a;法律文书自动分类 1. 引言&#xff1a;AI 万能分类器的时代来临 在自然语言处理&#xff08;NLP&#xff09;领域&#xff0c;文本分类是构建智能系统的核心能力之一。传统方法依赖大量标注数据进行监督训练&#xff0c;成本高、周期长…

作者头像 李华
网站建设 2026/1/30 9:44:55

RTL8852BE无线网卡驱动:从零开始掌握Linux WiFi 6配置

RTL8852BE无线网卡驱动&#xff1a;从零开始掌握Linux WiFi 6配置 【免费下载链接】rtl8852be Realtek Linux WLAN Driver for RTL8852BE 项目地址: https://gitcode.com/gh_mirrors/rt/rtl8852be RTL8852BE作为一款支持最新WiFi 6标准的Realtek无线网卡芯片&#xff0c…

作者头像 李华
网站建设 2026/1/30 3:23:37

LeagueSkinChanger终极指南:免费解锁英雄联盟全皮肤体验

LeagueSkinChanger终极指南&#xff1a;免费解锁英雄联盟全皮肤体验 【免费下载链接】LeagueSkinChanger Skin changer for League of Legends 项目地址: https://gitcode.com/gh_mirrors/le/LeagueSkinChanger 想要在英雄联盟中免费体验所有精美皮肤&#xff0c;打造专…

作者头像 李华
网站建设 2026/1/30 15:17:04

ArduPilot与BLHeli通信故障排查:系统学习

ArduPilot 与 BLHeli 通信故障排查&#xff1a;从原理到实战的系统性指南 你有没有遇到过这样的情况——飞控已经解锁&#xff0c;遥控器油门推上&#xff0c;但电机毫无反应&#xff1f;或者刚起飞就突然失控&#xff0c;日志里满屏“ESC lost”警告&#xff1f;如果你用的是 …

作者头像 李华
网站建设 2026/1/29 20:22:26

STM32嵌入式开发实战宝典:一站式解决方案助力项目快速落地

STM32嵌入式开发实战宝典&#xff1a;一站式解决方案助力项目快速落地 【免费下载链接】stm32 STM32 stuff 项目地址: https://gitcode.com/gh_mirrors/st/stm32 STM32嵌入式开发项目为开发者提供了一个完整的驱动生态体系&#xff0c;从基础外设到复杂应用&#xff0c;…

作者头像 李华