乐于分享
好东西不私藏

【从零开始撸内核驱动源码】:ttyserial之8250串口驱动总体架构剖析

【从零开始撸内核驱动源码】:ttyserial之8250串口驱动总体架构剖析

Hello,大家好,我是程序媛MM。

本文约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,设备名称基于物理地址0xfdf02000    compatible = "arm,pl011""arm,primecell"// 驱动兼容性:内核优先匹配arm,pl011串口驱动,其次匹配arm,primecell通用总线驱动    reg = <0x0 0xfdf02000 0x0 0x1000>;        // 寄存器映射:起始地址0xfdf0200064位地址,高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, 0sizeof(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);                        // 直接写入FCR    serial_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;                 // 最大读取次数,防止长时间占用CPU    do {        // 从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->硬件。

以上为全文内容。

往期文章(欢迎订阅技术分享栏目全部文章):

【从零开始撸内核驱动源码】:以ttyserial(串口驱动)为例,串联字符设备驱动基础知识点的学习计划
Linux内核源码顶层 Makefile分析并单独编译调试内核自带的驱动
【从零开始撸内核驱动源码】:ttynull驱动
Linux内核驱动安装失败问题调试及解决方法
Linux内核驱动源码走读之编译内核及外部驱动实操指南

谢谢你看到这里

这里是女程序员的笔记本

 15年+嵌入式软件工程师兼二胎宝妈

分享读书心得、工作经验,自我成长和生活方式。

希望我的文字能对你有所帮助

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 【从零开始撸内核驱动源码】:ttyserial之8250串口驱动总体架构剖析

评论 抢沙发

9 + 2 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮