【从零开始撸内核驱动源码】:ttyserial之8250串口驱动总体架构剖析
本文约2000字,基于 Linux 6.6.123 内核,将从目录架构、分层设计、核心流程、总线适配四个维度,总体走读 8250 串口驱动的实现逻辑,来打通 “硬件-驱动-内核子系统” 的全链路认知,后续再逐个细节去走读各个文件源码。
关注公众号, 即可获得与Linux相关的电子书籍以及常用开发工具,文末有文档清单。

8250/16550 系列 UART 驱动是 Linux 字符设备驱动的经典范式, 本文继续走读8250串口驱动。
一 驱动目录:8250 驱动的文件矩阵
在drivers/tty/serial/8250/目录下(如上图所示) “核心层 – 端口层 – 总线适配层 – 厂商定制层” 分层组织,核心文件及职责如下表:

说明:Linux 内核为串口驱动设计了serial_core抽象层(drivers/tty/serial/serial_core.c),8250 驱动通过uart_driver和uart_ops对接该抽象层,再由serial_core统一实现tty_operations对接 TTY 子系统 —— 这是新版内核的标准化设计,避免各串口驱动重复实现 TTY 接口。
二 核心架构:四层联动的设计哲学
8250 串口驱动的完整架构是 “TTY 子系统->UART 核心层 ->8250 核心层 ->硬件端口层”,每一层的职责与接口如下:
[1]. 顶层:TTY 子系统与 UART 核心层
Linux 内核中,所有串口驱动都需通过serial_core抽象层对接 TTY 子系统:
serial_core.c实现标准的tty_operations(如uart_open/uart_write);
static const struct tty_operations uart_ops = {.install = uart_install,.open = uart_open,.close = uart_close,.write = uart_write,.put_char = uart_put_char,.flush_chars = uart_flush_chars,.write_room = uart_write_room,.chars_in_buffer= uart_chars_in_buffer,.flush_buffer = uart_flush_buffer,.ioctl = uart_ioctl,.throttle = uart_throttle,.unthrottle = uart_unthrottle,.send_xchar = uart_send_xchar,.set_termios = uart_set_termios,.set_ldisc = uart_set_ldisc,.stop = uart_stop,.start = uart_start,.hangup = uart_hangup,.break_ctl = uart_break_ctl,.wait_until_sent= uart_wait_until_sent,#ifdef CONFIG_PROC_FS.proc_show = uart_proc_show,#endif.tiocmget = uart_tiocmget,.tiocmset = uart_tiocmset,.set_serial = uart_set_info_user,.get_serial = uart_get_info_user,.get_icount = uart_get_icount,#ifdef CONFIG_CONSOLE_POLL.poll_init = uart_poll_init,.poll_get_char = uart_poll_get_char,.poll_put_char = uart_poll_put_char,#endif};
8250 驱动只需向serial_core注册uart_driver和uart_ops,即可复用 TTY 适配逻辑;这种设计让 8250 驱动聚焦于 “UART 硬件操作”,而非重复实现 TTY 适配代码。
[2]. 第二层:8250 核心层(8250_core.c)
8250_core.c是 8250 驱动的 “总控中心”,核心职责是向 UART 核心层注册驱动,并管理 8250 串口端口,关键结构体与接口如下:
>>注册 UART 驱动(核心入口)
// 8250_core.c 核心定义static struct uart_driver serial8250_reg = {.owner = THIS_MODULE,.driver_name = "serial", // 驱动名,对应/dev/ttyS*.dev_name = "ttyS", // 设备节点名前缀.major = TTY_MAJOR, // 主设备号.minor = 64, // 次设备号起始值.nr = 32, // 支持的串口数量};// 驱动初始化时注册到UART核心层static int __initserial8250_init(void){int ret;// 1. 注册UART驱动(对接uart_core)ret = uart_register_driver(&serial8250_reg);if (ret)return ret;// 2. 初始化端口链表、中断处理等serial8250_init_ports();return 0;}module_init(serial8250_init);
[3]. 第三层:硬件端口层(8250_port.c)
8250_port.c是驱动的 “硬件操作层”,实现serial8250_ops中所有接口的底层硬件逻辑:
>>实现 UART 操作接口(uart_ops)
8250 驱动通过uart_ops结构体向 UART 核心层暴露硬件操作能力,这是替代直接实现tty_operations的核心接口:
// 8250_port.c中定义的uart_ops(核心硬件操作集)staticconststruct uart_ops serial8250_ops = {.tx_empty = serial8250_tx_empty, // 检查TX FIFO是否为空.set_mctrl = serial8250_set_mctrl, // 设置调制解调器控制信号(RTS/CTS等).get_mctrl = serial8250_get_mctrl, // 获取调制解调器状态.stop_tx = serial8250_stop_tx, // 停止发送.start_tx = serial8250_start_tx, // 启动发送(核心发送入口).stop_rx = serial8250_stop_rx, // 停止接收.enable_ms = serial8250_enable_ms, // 启用调制解调器状态中断.break_ctl = serial8250_break_ctl, // 控制Break信号.startup = serial8250_startup, // 串口打开时的硬件初始化.shutdown = serial8250_shutdown, // 串口关闭时的硬件清理.flush_buffer = serial8250_flush_buffer, // 刷新发送缓冲区.set_termios = serial8250_set_termios, // 配置串口参数(波特率/数据位等).type = serial8250_type, // 返回UART类型(如PORT_16550A).release_port = serial8250_release_port, // 释放端口资源.request_port = serial8250_request_port, // 请求端口资源.config_port = serial8250_config_port, // 配置端口(如自动探测).verify_port = serial8250_verify_port, // 验证端口参数合法性};
说明:
uart_ops是 UART 核心层定义的 “硬件操作抽象接口”,而非 TTY 层的tty_operations;
当用户态调用open(“/dev/ttyS0”)时,流程是:
TTY子系统->serial_core-> serial8250_startup->8250_port.c的硬件初始化;
当用户态调用write时,流程是:
TTY子系统 ->serial_core-> serial8250_start_tx -> 8250_port.c的硬件发送。
serial_port.c的核心职责包括:
>>寄存器读写适配(IO 端口 / 内存映射、不同位宽 / 字节序);
>>端口初始化、波特率配置、FIFO 管理;
>>中断处理、数据收发的底层实现;
>>硬件缺陷规避(如 Freescale 16550A 的 FIFO 长度适配)。
[4]. 第四层:总线适配层(如 8250_of.c)
对接不同硬件总线(Platform/PCI/PnP),解析硬件资源(寄存器物理地址、中断号、时钟频率、寄存器访问位宽 / 端序、FIFO 规格等),完成硬件资源的合法性校验与差异化适配(如大端序、寄存器偏移、厂商私有特性),并将这些资源填充到 uart_8250_port(含 uart_port)结构体中;针对 Platform 总线(设备树场景),还需完成时钟启用、复位释放等嵌入式硬件资源管理,最终通过 serial8250_register_8250_port(或 serial8250_register_port)将端口注册到 8250 核心层,同时关联 8250 核心定义的 uart_ops 操作集,使适配后的端口纳入 8250 驱动的全局管理体系。
三 核心流程走读:从硬件注册到数据收发
以嵌入式平台(设备树 + Platform 总线) 为例,完整走读 8250 串口驱动的 “注册 – 初始化 – 收发” 全流程,接口调用链路:
阶段 1:硬件资源解析与端口注册(8250_of.c)
嵌入式平台的 UART 硬件信息通过设备树描述,8250_of.c作为 Platform 总线适配层,完成 “设备树->8250 端口” 的资源转换。
[1]. 设备树节点(以hi3660.dtsi 为例)
uart0: serial@fdf02000 { // 定义UART0设备节点:标签为uart0,设备名称基于物理地址0xfdf02000compatible = "arm,pl011", "arm,primecell"; // 驱动兼容性:内核优先匹配arm,pl011串口驱动,其次匹配arm,primecell通用总线驱动reg = <0x0 0xfdf02000 0x0 0x1000>; // 寄存器映射:起始地址0xfdf02000(64位地址,高32位为0x0),地址空间大小0x1000(4KB)interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>; // 中断配置:使用GIC中断控制器,SPI(共享外设中断)编号74,高电平触发clocks = <&crg_ctrl HI3660_CLK_MUX_UART0>, // 时钟源引用:第一个时钟来自crg_ctrl节点的HI3660_CLK_MUX_UART0(串口时钟多路选择)<&crg_ctrl HI3660_PCLK>; // 第二个时钟来自crg_ctrl节点的HI3660_PCLK(APB总线时钟)clock-names = "uartclk", "apb_pclk"; // 时钟名称:分别对应驱动中的"uartclk"(核心时钟)和"apb_pclk"(总线时钟)pinctrl-names = "default"; // 引脚控制状态名称列表:当前仅定义"default"(默认)状态pinctrl-0 = <&uart0_pmx_func &uart0_cfg_func>; // 默认状态的引脚配置:引用uart0_pmx_func(引脚复用功能)和uart0_cfg_func(引脚电气参数)status = "disabled"; // 设备状态:禁用,表示该设备默认不加载驱动,可在其他配置中覆盖为"okay"启用};
[2]. Platform 驱动匹配与 Probe
8250_of.c定义 Platform 驱动结构体,匹配设备树节点后执行probe函数:
// 8250_of.c 核心代码static const struct of_device_id of_platform_serial_table[] = {{ .compatible = "ns8250", .data = (void *)PORT_8250, },{ .compatible = "ns16450", .data = (void *)PORT_16450, },{ .compatible = "ns16550a", .data = (void *)PORT_16550A, },...{ /* end of list */ },};MODULE_DEVICE_TABLE(of, of_platform_serial_table);static struct platform_driver of_platform_serial_driver = {.driver = {.name = "of_serial",.of_match_table = of_platform_serial_table,.pm = &of_serial_pm_ops,},.probe = of_platform_serial_probe,.remove = of_platform_serial_remove,};module_platform_driver(of_platform_serial_driver);
[3]. Probe 核心逻辑
of_platform_serial_probe解析设备树资源,初始化 8250 端口,并注册到 8250 核心层:
//串口设备探测函数 - 当设备树节点与驱动匹配时被调用staticintof_platform_serial_probe(struct platform_device *ofdev){struct of_serial_info *info; // 设备私有数据结构struct uart_8250_port port8250; // 8250串口端口结构体unsigned int port_type; // 端口类型标识u32 tx_threshold; // 发送FIFO阈值int ret;// 特殊硬件兼容性排除:BCM7271串口由专用驱动处理if (IS_ENABLED(CONFIG_SERIAL_8250_BCM7271) &&of_device_is_compatible(ofdev->dev.of_node, "brcm,bcm7271-uart"))return -ENODEV;// 从驱动匹配数据中获取端口类型(如PORT_16550A、PORT_16750等)port_type = (unsigned long)of_device_get_match_data(&ofdev->dev);if (port_type == PORT_UNKNOWN) // 未知类型则退出return -EINVAL;// 检查设备是否被RTAS(PowerPC实时抽象层)占用if (of_property_read_bool(ofdev->dev.of_node, "used-by-rtas"))return -EBUSY;// 分配设备私有数据结构内存info = kzalloc(sizeof(*info), GFP_KERNEL);if (info == NULL)return -ENOMEM;// 初始化串口端口结构体memset(&port8250, 0, sizeof(port8250));// 核心设置:从设备树节点解析并配置硬件(寄存器、中断、时钟等)ret = of_platform_serial_setup(ofdev, port_type, &port8250, info);if (ret)goto err_free; // 设置失败则跳转到清理内存// 如果设备支持FIFO,设置FIFO能力标志if (port8250.port.fifosize)port8250.capabilities = UART_CAP_FIFO;// 检查设备树中是否有发送阈值配置,并计算加载大小if ((of_property_read_u32(ofdev->dev.of_node, "tx-threshold",&tx_threshold) == 0) &&(tx_threshold < port8250.port.fifosize))port8250.tx_loadsz = port8250.port.fifosize - tx_threshold;// 检查是否启用自动流控制if (of_property_read_bool(ofdev->dev.of_node, "auto-flow-control"))port8250.capabilities |= UART_CAP_AFE;// 配置过载退避时间(默认为0)if (of_property_read_u32(ofdev->dev.of_node,"overrun-throttle-ms",&port8250.overrun_backoff_time_ms) != 0)port8250.overrun_backoff_time_ms = 0;// 关键步骤:向8250串口子系统注册端口ret = serial8250_register_8250_port(&port8250);if (ret < 0)goto err_dispose; // 注册失败则跳转到资源清理// 注册成功:保存信息并关联设备数据info->type = port_type; // 保存端口类型info->line = ret; // 保存分配的串口线路号platform_set_drvdata(ofdev, info); // 将私有数据绑定到平台设备return 0; // 成功返回// 错误处理路径:资源逆序释放err_dispose:irq_dispose_mapping(port8250.port.irq); // 释放中断映射pm_runtime_put_sync(&ofdev->dev); // 电源管理:减少引用计数pm_runtime_disable(&ofdev->dev); // 禁用电源管理clk_disable_unprepare(info->clk); // 关闭时钟err_free:kfree(info); // 释放私有数据结构内存return ret; // 返回错误码}
阶段 2:硬件端口初始化(8250_port.c)
serial8250_init_port调用8250_port.c的底层接口,完成 UART 硬件的 “上电配置”, 核心步骤:
>>禁用中断:向 IER 寄存器写 0,避免初始化过程中触发异常中断;
>>复位 FIFO:若硬件支持 FIFO,写入UART_FCR_ENABLE_FIFO | UART_FCR_CLEAR_RCVR | UART_FCR_CLEAR_XMIT清空 FIFO;
>>配置线路参数:设置默认 8N1(8 位数据、无校验、1 位停止);
>>硬件存在性检测:读取 LSR 寄存器,若返回 0xFF 则判定为无有效硬件;
>>注册中断处理函数:将serial8250_interrupt绑定到中断控制器(通过request_irq)。
说明:波特率配置是serial8250_set_termios的核心:
// 8250_port.c 中 serial8250_do_set_termios 函数片段voidserial8250_do_set_termios(struct uart_port *port, struct ktermios *termios,const struct ktermios *old){struct uart_8250_port *up = up_to_u8250p(port); // 获取8250特定端口结构unsigned char cval; // 计算得到的LCR(线路控制寄存器)值unsigned long flags; // 中断保存标志unsigned int baud, quot, frac = 0; // 波特率、分频器整数部分、小数部分...// 根据终端设置计算LCR值(数据位、停止位、奇偶校验)cval = serial8250_compute_lcr(up, termios->c_cflag);// 获取新波特率并计算对应的分频器baud = serial8250_get_baud_rate(port, termios, old);quot = serial8250_get_divisor(port, baud, &frac);/** 开始修改端口状态 - 需要禁用中断以保证原子性*/serial8250_rpm_get(up); // 电源管理:增加引用计数spin_lock_irqsave(&port->lock, flags); // 获取端口自旋锁,保存中断状态up->lcr = cval; // 保存计算好的LCR值// FIFO触发阈值配置:低速且无DMA时使用最小触发阈值if (up->capabilities & UART_CAP_FIFO && port->fifosize > 1) {if (baud < 2400 && !up->dma) {up->fcr &= ~UART_FCR_TRIGGER_MASK;up->fcr |= UART_FCR_TRIGGER_1; // 1字节触发阈值}}...serial_port_out(port, UART_IER, up->ier); // 写入IER寄存器...// 设置波特率分频器(最重要的硬件配置之一)serial8250_set_divisor(port, baud, quot, frac);/** 特殊处理16750 FIFO模式:必须在DLAB=1时设置FCR才能启用64字节FIFO*/if (port->type == PORT_16750)serial_port_out(port, UART_FCR, up->fcr); // 直接写入FCRserial_port_out(port, UART_LCR, up->lcr); // 恢复LCR(清除DLAB位)...// 设置调制解调器控制寄存器(MCR)- 包括RTS/DTR等信号serial8250_set_mctrl(port, port->mctrl);spin_unlock_irqrestore(&port->lock, flags); // 释放锁,恢复中断serial8250_rpm_put(up); // 电源管理:减少引用计数// 更新termios结构中的波特率值(如果波特率非0)if (tty_termios_baud_rate(termios))tty_termios_encode_baud_rate(termios, baud, baud);}
阶段 3:数据收发核心流程
8250串口的数据收发采用 “内核缓冲区+硬件 FIFO+中断驱动” 模式,接口链路为:
用户态write ->TTY 子系统->serial_core -> serial8250_start_tx(8250_core.c)-> serial8250_start_tx(8250_port.c)
1. 数据发送流程
// 8250_port.c 中核心发送函数staticvoidserial8250_start_tx(struct uart_port *port){struct uart_8250_port *up = up_to_u8250p(port); // 转换为8250特定结构struct uart_8250_em485 *em485 = up->em485; // RS485模式控制结构/* 端口锁已持有,用于同步UART_IER访问(控制台场景) */lockdep_assert_held_once(&port->lock);// 检查是否有数据需要发送:没有特殊字符且发送缓冲区为空则直接返回if (!port->x_char && uart_circ_empty(&port->state->xmit))return;serial8250_rpm_get_tx(up); // 电源管理:增加发送相关引用计数// RS485模式特殊处理if (em485) {// 如果正在启动定时器或无法开始发送,则返回if ((em485->active_timer == &em485->start_tx_timer) ||!start_tx_rs485(port))return;}__start_tx(port); // 实际启动发送的核心函数}
2. 数据接收流程
接收是纯中断驱动,核心链路:
硬件收数->触发中断->serial8250_interrupt(8250_core.c)
->serial8250_rx_chars(8250_port.c):
// 8250_port.c 中接收数据核心函数u16 serial8250_rx_chars(struct uart_8250_port *up, u16 lsr){struct uart_port *port = &up->port; // 获取通用串口端口结构int max_count = 256; // 最大读取次数,防止长时间占用CPUdo {// 从UART读取一个字符(根据lsr状态处理数据、错误等)serial8250_read_char(up, lsr);// 达到最大读取次数后退出循环(防止中断处理时间过长)if (--max_count == 0)break;// 再次读取线路状态寄存器,检查是否还有数据可读lsr = serial_in(up, UART_LSR);} while (lsr & (UART_LSR_DR | UART_LSR_BI)); // 循环条件:数据就绪或Break中断// 将暂存在tty缓冲区中的数据推送到上层(tty层)处理tty_flip_buffer_push(&port->state->port);return lsr; // 返回最终的状态寄存器值}
阶段 4:中断处理
serial8250_interrupt是中断处理总入口,核心逻辑:
/* 8250串口共享中断处理函数 - 处理多个串口端口共享同一中断线的情况 */staticirqreturn_tserial8250_interrupt(int irq, void *dev_id){struct irq_info *i = dev_id; // 中断信息结构,包含共享该中断的所有串口链表struct list_head *l, *end = NULL; // l:当前遍历节点,end:第一个未处理中断的端口int pass_counter = 0, handled = 0; // pass_counter:循环计数,handled:中断处理标志pr_debug("%s(%d): start\n", __func__, irq); // 调试信息:中断开始spin_lock(&i->lock); // 获取自旋锁,保护共享数据结构l = i->head; // 从链表头部开始遍历do {struct uart_8250_port *up;struct uart_port *port;up = list_entry(l, struct uart_8250_port, list); // 获取链表节点对应的串口端口port = &up->port;// 调用端口特定的中断处理函数if (port->handle_irq(port)) {handled = 1; // 标记至少一个端口处理了中断end = NULL; // 重置end,因为可能有更多端口需要处理} else if (end == NULL) // 如果该端口没有中断,记录第一个"空闲"端口end = l;l = l->next; // 移动到下一个端口// 防无限循环保护:如果遍历完整个链表且超过PASS_LIMIT次,强制退出if (l == i->head && pass_counter++ > PASS_LIMIT)break;} while (l != end); // 循环直到遇到第一个未处理中断的端口spin_unlock(&i->lock); // 释放自旋锁pr_debug("%s(%d): end\n", __func__, irq); // 调试信息:中断结束return IRQ_RETVAL(handled); // 返回中断处理结果}
四 核心设计要点
关键设计亮点如下:
>>UART 核心层解耦:通过uart_ops对接serial_core,而非直接实现tty_operations,符合 Linux 内核标准化设计;
>>资源自动管理:使用devm_系列函数(如devm_ioremap)自动释放资源,避免内存泄漏;
>>并发安全:使用spin_lock_irqsave/spin_unlock_irqrestore保护临界区,支持中断嵌套;
>>错误处理:完整的错误回滚逻辑(如 probe 函数中的 goto 链),符合内核驱动规范;
>>硬件抽象:通过uart_8250_port封装通用属性,通过uart_config数组适配不同 UART 型号。
五 总结
Linux 6.6.123 的 8250 串口驱动,是 “UART 核心层解耦 + 分层设计 + 硬件抽象”的典型实现:
[1].驱动核心逻辑:8250_core.c通过uart_driver和uart_ops对接 UART 核心层,而非直接实现tty_operations;
[2].硬件操作层:8250_port.c实现所有底层硬件逻辑,是驱动的 “手脚”;
[3].总线适配层:8250_of.c/8250_pci.c解析不同总线的硬件资源,完成端口注册;
[4].接口链路:用户态操作->TTY 子系统->serial_core->8250_core->8250_port->硬件。
以上为全文内容。
这里是女程序员的笔记本
15年+嵌入式软件工程师兼二胎宝妈
分享读书心得、工作经验,自我成长和生活方式。
希望我的文字能对你有所帮助
夜雨聆风
