串口通用驅動文件在哪里?
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 TIMEHO?UT 假中斷檢查 | 讀 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博客