Linux中tty与8250-uart的虐恋(包括双中断发送接收机制)
串口通用驱动文件在哪里?
drivers/tty/serial/
哪一个是正确的
compatible
?
arch/arm64/boot/dts/rockchip/rk3568.dtsi
uart3: serial@fe670000 {compatible = "rockchip,rk3568-uart", "snps,dw-apb-uart";reg = <0x0 0xfe670000 0x0 0x100>;interrupts = <GIC_SPI 119 IRQ_TYPE_LEVEL_HIGH>;clocks = <&cru SCLK_UART3>, <&cru PCLK_UART3>;clock-names = "baudclk", "apb_pclk";reg-shift = <2>;reg-io-width = <4>;dmas = <&dmac0 6>, <&dmac0 7>;pinctrl-names = "default";pinctrl-0 = <&uart3m0_xfer>;status = "disabled";
};
static const struct of_device_id dw8250_of_match[] = {{ .compatible = "snps,dw-apb-uart" },{ .compatible = "cavium,octeon-3860-uart" },{ .compatible = "marvell,armada-38x-uart" },{ .compatible = "renesas,rzn1-uart" },{ /* Sentinel */ }
};
MODULE_DEVICE_TABLE(of, dw8250_of_match);
dw8250_platform_driver
属于platform_driver
,相当于drv->probe
static struct platform_driver dw8250_platform_driver = {.driver = {.name = "dw-apb-uart",.pm = &dw8250_pm_ops,.of_match_table = dw8250_of_match,.acpi_match_table = dw8250_acpi_match,},.probe = dw8250_probe,.remove = dw8250_remove,
};module_platform_driver(dw8250_platform_driver)
dw8250_probe
uart_add_one_port
把一个 已经初始化好的uart_port
结构体 绑定到 已经注册好的uart_driver
上,从而让它成为Linux
内核可见的一个串口设备(/dev/ttySx
、/dev/ttyAMA0
等)
dw8250_probeuart_8250_port uartresource *regs = platform_get_resource//从设备树中获取 8250 控制器寄存器物理基地址uart_port *p = &uart.portdw8250_data *datap->handle_irq = dw8250_handle_irqp->serial_in = dw8250_serial_in;p->serial_out = dw8250_serial_out;p->membase = devm_ioremap(dev, regs->start, resource_size(regs))//内存映射得在Linux用虚拟地址device_property_read_u32device_property_read_booldevm_clk_getdw8250_quirks(p, data)//特定平台配置和兼容性问题of_alias_get_id(np, "serial")p->serial_inp->serial_outdata->line = serial8250_register_8250_port(&uart)uart_add_one_portplatform_set_drvdata(pdev, data)
serial8250_init
uart_register_driver
先注册 driveruart_register_driver
“先填申报户口资料”,uart_add_one_port
是“依照资料给每块 UART 上户口”;只有上完户口,内核里才会出现对应的/dev/tty*
设备tty_register_driver
创建 tty 设备节点(/dev/ttyS0
、ttyS1
…)
module_init(serial8250_init)serial8250_isa_init_ports//实现请求端口、添加和注册串口端口serial8250_init_portuart_8250_port port->ops = &serial8250_popsuart_register_driver(&serial8250_reg)//uart_driver 转 tty_driver 注册ttynormal = alloc_tty_driver(drv->nr)tty_set_operations(normal, &uart_ops)port->ops = &uart_port_ops//tty_port *port = &uart_state->porttty_register_driver(normal)put_tty_driver(normal)serial8250_isa_devs = platform_device_alloc("serial8250",PLAT8250_DEV_LEGACY)platform_device_add(serial8250_isa_devs)serial8250_register_ports(&serial8250_reg, &serial8250_isa_devs->dev);platform_driver_register(&serial8250_isa_driver)
uart_driverstruct uart_state *state;struct tty_port portstruct uart_port *uart_portstruct tty_driver *tty_driver;
static struct uart_driver serial8250_reg = {.owner = THIS_MODULE,.driver_name = "serial",.dev_name = "ttyS",.major = TTY_MAJOR,.minor = 64,.cons = SERIAL8250_CONSOLE,
};
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,.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
};
static const struct tty_port_operations uart_port_ops = {.carrier_raised = uart_carrier_raised,.dtr_rts = uart_dtr_rts,.activate = uart_port_activate,.shutdown = uart_tty_port_shutdown,
};
uart_ops
serial8250_pops
和uart_port
什么区别
uart_8250_portuart_8250_dma *dmauart_8250_ops *opsuart_port port(*serial_in)(*serial_out)(*set_termios)(*set_ldisc)(*handle_irq)uart_state *statetty_port portuart_port *uart_portuart_ops *ops(*startup)(*start_tx)(*stop_tx)(*stop_rx)(*ioctl)
static const struct uart_ops serial8250_pops = {.tx_empty = serial8250_tx_empty,.set_mctrl = serial8250_set_mctrl,.get_mctrl = serial8250_get_mctrl,.stop_tx = serial8250_stop_tx,.start_tx = serial8250_start_tx,.throttle = serial8250_throttle,.unthrottle = serial8250_unthrottle,.stop_rx = serial8250_stop_rx,.enable_ms = serial8250_enable_ms,.break_ctl = serial8250_break_ctl,.startup = serial8250_startup,.shutdown = serial8250_shutdown,.set_termios = serial8250_set_termios,.set_ldisc = serial8250_set_ldisc,.pm = serial8250_pm,.type = serial8250_type,.release_port = serial8250_release_port,.request_port = serial8250_request_port,.config_port = serial8250_config_port,.verify_port = serial8250_verify_port,
#ifdef CONFIG_CONSOLE_POLL.poll_get_char = serial8250_get_poll_char,.poll_put_char = serial8250_put_poll_char,
#endif
};
univ8250_setup_irq
硬件中断号都没有(port->irq == 0
,常见于早期板级代码或虚拟端口),则完全采用 纯轮询- 如果硬件中断号有效,就调用
serial_link_irq_chain()
把普通中断处理链挂到该IRQ
上,包括注册serial8250_interrupt()
static const struct uart_8250_ops univ8250_driver_ops = {.setup_irq = univ8250_setup_irq,.release_irq = univ8250_release_irq,
};
serial_in
serial_out
读取发送 如何 实现的?
ALTERNATIVE
是内核提供的一个宏,利用ELF .altinstructions
段 机制- 在内核启动早期 会扫描这个段,根据
ARM64_WORKAROUND_DEVICE_LOAD_ACQUIRE
的布尔值,把ldrb
原地patch
成ldarb
,或者在不需要时保持ldrb
,保证 读操作之前的所有读写都不能重排到它之后,用于解决Device-nGRE/Device-nGnRE
映射上可能出现的加载乱序或重试bug
serial_in(struct uart_8250_port *up, int offset)up->port.serial_in(&up->port, offset);dw8250_serial_inreadb(p->membase + (offset << p->regshift))readb_relaxedasm volatile(ALTERNATIVE("ldrb %w0, [%1]","ldarb %w0,[%1]",ARM64_WORKAROUND_DEVICE_LOAD_ACQUIRE): "=r" (val) : "r" (addr));serial_out(struct uart_8250_port *up, int offset, int value)up->port.serial_out(&up->port, offset, value);dw8250_serial_outwriteb(value, p->membase + (offset << p->regshift))writeb_relaxedasm volatile("strb %w0, [%1]" : : "rZ" (val), "r" (addr))
dw8250_handle_irq
->serial8250_handle_irq
serial8250_handle_irq
真正处理函数
univ8250_setup_irqrequest_irqserial8250_interruptp->handle_irq = dw8250_handle_irqdw8250_handle_irqserial8250_handle_irqif (iir & UART_IIR_NO_INT) // 把假中断挡在门外serial_port_in(port, UART_LSR) //一次读出所有线路/错误状态位skip_rx = true //自动 RTS/CTS 硬件流控把 RX 堵住//只要有数据 (`DR`) 或 Break (`BI`) 并且未被上一阶段跳过,就处理接收dma_err = handle_rx_dma(up, iir) //先尝试 DMA 接收 status = serial8250_rx_chars(up, status) //DMA 失败或无 DMA落到 PIO 接收serial8250_modem_status(up)//读取 `UART_MSR` 寄存器,检测 CTS/DSR/RI/DCD 变化 //CTS 变高/低、载波丢失等事件上报 TTYserial8250_tx_chars(up)//serial8250_tx_chars把 TTY 环形缓冲区里的字节写入UART TXFIFOpr_err //现 Overrun/Parity/Frame/Break 错误,就打印一行提示
UART_LSR 是什么寄存器?
- Line Status Register(线路状态寄存器)一次读即可得到发送/接收单元当前状态
#define UART_LSR 5 /* In: Line Status Register */
bit7 UART_LSR_FIFOE FIFO 数据错误
bit6 UART_LSR_TEMT 发送移位器+发送保持器都空
bit5 UART_LSR_THRE 发送保持器 空(可继续写下一个字节)
bit4 UART_LSR_BI Break 中断
bit3 UART_LSR_FE 帧错误
bit2 UART_LSR_PE 奇偶错误
bit1 UART_LSR_OE Overrun 错误
bit0 UART_LSR_DR 接收数据就绪
up->dma->txchan
是什么?
- 发送
DMA
通道:由dma_request_slave_channel_compat()
向DMA Engine
子系统 申请得到 - 不为
NULL
→ 用DMA
搬数据到UART
- 为
NULL
→ 退回到传统PIO
(CPU
轮询)方式发送
UART_IIR_THRI
是什么?
UART_IIR_THRI
=“Transmit Holding Register Empty Interrupt”
- 当发送
FIFO
(或保持寄存器)里的最后一个字节被移位器取走后,硬件置位THRE
(UART_LSR bit5
),并同时把UART_IIR_THRI
填进IIR
,表示“你可以继续写下一个字节了”
为什么
p->handle_irq
dw8250_handle_irq
要兼顾DesignWare UART
“假超时”和“BUSY”处理?什么是DesignWare UART
?
DesignWare UART
(官方文档中称为DW_apb_uart
)是Synopsys
公司推出的、基于AMBA APB
总线的可综合 IP 核,目标是给 SoC 提供一套 兼容传统16550/8250
的通用异步串行收发器(UART
)dw8250_handle_irq
复用了 8250 核心逻辑,又补上了芯片特有的“假超时”和“BUSY”处理
阶段 | 作用 |
---|---|
读取 IIR | 获知当前中断类型 |
DesignWare UART RX TIMEHOUT 假中断检查 | 读 USR/LSR/RFL ,必要时空读 UART_RX 清假超时 |
标准中断 | 交给 serial8250_handle_irq() |
DesignWare UART BUSY 中断 | 读 USR 清 BUSY状态 |
结束 | 返回已处理标志 |
自动
RTS/CTS
流控是什么?
自动 RTS/CTS
流控 = 硬件级双向握手,用两根线 RTS(Request To Send)
和 CTS(Clear To Send)
自动启停对方的发送,防止 FIFO
溢出。
RTS
由 本地UART
输出:
“我准备好接收了,你可以发”。CTS
由 对方UART
输出:
“我准备好了,你可以发”。
工作方式(全双工场景):
-
本地接收侧
- 当本地
RX FIFO
快满(可编程阈值)时,UART
自动把RTS
拉高 → 对方看到RTS=1
就 暂停发送 FIFO
被读空后,RTS
自动拉低 → 对方继续发。
- 当本地
-
本地发送侧
- 只有检测到
CTS=0
(对方准备好)时,UART
才会把TX FIFO
里的数据真正发出; CTS=1
则硬件暂停发送
- 只有检测到
dma初始化位置在哪里?两个函数是什么?
serial8250_startupserial8250_do_startupserial8250_request_dmadma->txchan = dma_request_slave_channel_compatdma->tx_addr = dma_map_single
serial8250_initserial8250_isa_init_portsserial8250_set_defaultsup->dma->tx_dma = serial8250_tx_dmaup->dma->rx_dma = serial8250_rx_dma
serial8250_tx_dma
struct uart_8250_dma *dma = p->dma; // 拿到 DMA 私有结构
struct circ_buf *xmit = &p->port.state->xmit; // 指向 TTY 层的发送环形缓冲区
struct dma_async_tx_descriptor *desc; // DMA 描述符
int ret; // 返回值if (dma->tx_running) // 如果上一次 DMA 还没完成,直接退出return 0;if (uart_tx_stopped(&p->port) || uart_circ_empty(xmit)) { // 上层已暂停或缓冲区为空serial8250_rpm_put_tx(p); // 发送端置空闲,降低功耗return 0;
}dma->tx_size = CIRC_CNT_TO_END(xmit->head, xmit->tail, UART_XMIT_SIZE); // 计算本次可搬的字节数
#ifdef CONFIG_ARCH_ROCKCHIP
if (dma->tx_size < MAX_TX_BYTES) { // Rockchip:字节数太少就别 DMA,用 PIOret = -EBUSY;goto err;
}
#endifdesc = dmaengine_prep_slave_single( // 构造 DMA 描述符dma->txchan, // 发送通道dma->tx_addr + xmit->tail, // 源地址:环形缓冲区尾指针dma->tx_size, // 长度DMA_MEM_TO_DEV, // 方向:内存 -> 设备DMA_PREP_INTERRUPT | DMA_CTRL_ACK); // 完成后中断 + 立即应答
if (!desc) { // 描述符申请失败ret = -EBUSY;goto err;
}dma->tx_running = 1; // 置标志:DMA 正在跑
desc->callback = __dma_tx_complete; // DMA 完成回调
desc->callback_param = p; // 回调参数:uart_8250_port 指针dma->tx_cookie = dmaengine_submit(desc); // 把描述符提交给 DMA 引擎dma_sync_single_for_device( // 刷新 cache,确保 DMA 看到最新数据dma->txchan->device->dev,dma->tx_addr, UART_XMIT_SIZE, DMA_TO_DEVICE);dma_async_issue_pending(dma->txchan); // 真正启动 DMA 传输if (dma->tx_err) { // 如果之前出过错dma->tx_err = 0; // 清错误if (p->ier & UART_IER_THRI) { // 关 THRE 中断,避免 PIO 与 DMA 冲突p->ier &= ~UART_IER_THRI;
#ifdef CONFIG_ARCH_ROCKCHIPp->ier &= ~UART_IER_PTIME;
#endifserial_out(p, UART_IER, p->ier);}
}
return 0; // 成功启动 DMAerr:
dma->tx_err = 1; // 记录错误,下次退到 PIO
return ret;
serial8250_rx_dma
unsigned int rfl, i = 0, fcr = 0, cur_index = 0; // 局部变量
unsigned char buf[MAX_FIFO_SIZE]; // 临时缓存,放本次接收字节
struct uart_port *port = &p->port; // 方便引用
struct tty_port *tty_port = &p->port.state->port; // TTY 端口
struct dma_tx_state state; // DMA 状态结构
struct uart_8250_dma *dma = p->dma; // DMA 私有结构fcr = UART_FCR_ENABLE_FIFO | UART_FCR_T_TRIG_10 | UART_FCR_R_TRIG_11; // 开 FIFO,设高触发
serial_port_out(port, UART_FCR, fcr); // 立即写到硬件do { // 轮询直到 DMA 余量是整 burst 倍数dmaengine_tx_status(dma->rxchan, dma->rx_cookie, &state);cur_index = dma->rx_size - state.residue; // 已搬运字节
} while (cur_index % dma->rxconf.src_maxburst); // 对齐到 burst 大小rfl = serial_port_in(port, UART_RFL_16550A); // 读出 FIFO 当前字节数
while (i < rfl) // 把 FIFO 里剩余字节全部读出buf[i++] = serial_port_in(port, UART_RX);__dma_rx_complete(p); // 复位 DMA 描述符,准备下一轮tty_insert_flip_string(tty_port, buf, i); // 把 i 个字节塞进 TTY flip buffer
p->port.icount.rx += i; // 统计接收字节
tty_flip_buffer_push(tty_port); // 通知上层“有数据可读”if (fcr) // 恢复原始 FCR 设置serial_port_out(port, UART_FCR, p->fcr);
return 0;
为什么叫“8250”驱动?
- 历史原因:
Intel
在1981
年推出了NMOS 8250 UART
芯片,用来给早期IBM PC/XT
提供串口COM1/COM2
- 后续
16450
、16550A
、16750
、16850
等芯片都把 寄存器布局 做成了8250
的超集,于是 Linux 就把支持这一系列芯片的驱动统称为8250
驱动
tty
与8250
驱动是什么关系?
- 用户空间对 tty 设备文件进行操作时,实际上是在调用对应的
tty_fops
中的操作函数
应用层│├── read()/write()/ioctl()│
tty 层(tty_io.c, n_tty.c, tty_port.c)│ ① 负责线路规程、回显、缓冲、termios├── tty_struct├── tty_port└── tty_ldisc│
serial_core(serial_core.c)│ ② 把“tty 端口”与“uart_driver”粘合在一起└── uart_driver / uart_port│
8250 驱动族(8250_core.c, 8250_port.c, 8250_dw.c …)│ ③ 真正把字节写进 UART 寄存器└── struct uart_ops│
硬件 UART(16550, DesignWare, QCOM …)
tty
如何调用8250
open("/dev/ttyS0", …)tty_open()tty_port_open(struct tty_port *port, struct tty_struct *tty,struct file *filp)port->ops->activateuart_port_activateuart_startupretval = uport->ops->startup(uport);uart_state->uart_port->ops->startupuart_ops.startupserial8250_startup
cat /proc/interrupts
cat /proc/tty/drivers
cat /proc/tty/ldiscs
- 行规程
有了
early boot
of_platform_default_populate_init
,为什么serial8250_init
需要platform_device_add
?
-
因为
serial8250_isa_devs
并不是“从设备树里解析出来的普通串口”,而是8250/16550
驱动为了兼容ISA/Legacy UART
专门创建的一个“虚拟”平台设备 -
给“非设备树、非
ACPI
”的老式串口一个统一的platform_device
句柄 -
那些固定地址(0x3F8、0x2F8…)的
ISA UART
在设备树里往往根本没有节点,或者节点被禁用 -
驱动必须手动把它们包装成一个
platform_device
,才能继续沿用platform_driver
的probe
流程 -
驱动注册时如果找不到任何
device
就会无事可做;因此驱动自己先platform_device_alloc()
/platform_device_add()
创建一个名字为"serial8250"
、ID 为PLAT8250_DEV_LEGACY
的设备,确保serial8250_isa_driver
能匹配到它并进入probe
platform_device_add
调用bus
匹配,drv
进行probe
serial8250_init
→platform_device_add()
→device_add()
→bus_probe_device()
→__device_attach()
→bus_for_each_drv()
→__device_attach_driver()
→driver_match_device()
→bus_type->match()
__device_attach_driver()
→driver_probe_device
->really_probe
->drv->probe
__device_attach_driverdriver_match_devicebus_type->match()driver_probe_devicedrv->probe
-
参考文档43
module_platform_driver 与 module_init_module platform driver-CSDN博客 -
参考文档44
uart驱动学习-CSDN博客 -
参考45
深入浅出UART驱动开发与调试:从基础调试到虚拟驱动实现-CSDN博客