Linux UART驱动分析
1. 介绍
8250是IBM PC及兼容机使用的一种串口芯片; 16550是一种带先进先出(FIFO)功能的8250系列串口芯片; 16550A则是16550的升级版本, 修复了FIFO相关BUG, 也是目前比较常见的串口芯片.
本文介绍的是Xilinx UART 驱动分析, 因为没有找到其datasheet, 硬件操作部分分析16550的实现.
Xilinx UART驱动主要由drivers/tty/serial/xilinx_uartps.c来实现
其相关配置和基本信息可参考
2. 结构体
uart_driver和console结构变量, 以及实现了uart_ops函数操作集定义如下图所示
static struct uart_driver cdns_uart_uart_driver = { .owner = THIS_MODULE, .driver_name = CDNS_UART_NAME, /* xuartps */ .dev_name = CDNS_UART_TTY_NAME, /* ttyPS */ .major = CDNS_UART_MAJOR, /* 0, 注册时动态分配 */ .minor = CDNS_UART_MINOR, /* 0, 注册时动态分配*/ .nr = CDNS_UART_NR_PORTS, /* 2 */ #ifdef CONFIG_SERIAL_XILINX_PS_UART_CONSOLE .cons = &cdns_uart_console, /* ttyPS */ #endif }; static struct console cdns_uart_console = { .name = CDNS_UART_TTY_NAME, /* ttyPS */ .write = cdns_uart_console_write, .device = uart_console_device, .setup = cdns_uart_console_setup, .flags = CON_PRINTBUFFER, .index = -1, /* 由cmdline指定(e.g. console=ttyPS ) */ .data = &cdns_uart_uart_driver, }; static const struct uart_ops cdns_uart_ops = { .set_mctrl = cdns_uart_set_mctrl, .get_mctrl = cdns_uart_get_mctrl, .start_tx = cdns_uart_start_tx, .stop_tx = cdns_uart_stop_tx, .stop_rx = cdns_uart_stop_rx, .tx_empty = cdns_uart_tx_empty, .break_ctl = cdns_uart_break_ctl, .set_termios = cdns_uart_set_termios, .startup = cdns_uart_startup, .shutdown = cdns_uart_shutdown, .pm = cdns_uart_pm, .type = cdns_uart_type, .verify_port = cdns_uart_verify_port, .request_port = cdns_uart_request_port, .release_port = cdns_uart_release_port, .config_port = cdns_uart_config_port, #ifdef CONFIG_CONSOLE_POLL .poll_get_char = cdns_uart_poll_get_char, .poll_put_char = cdns_uart_poll_put_char, #endif };
3. 初始化
模块入口为cdns_uart_init
首先注册UART驱动
uart_register_driver(&cdns_uart_uart_driver);
随后又注册platform驱动
platform_driver_register(&cdns_uart_platform_driver);
其中cdns_uart_platform_driver定义如下
static const struct of_device_id cdns_uart_of_match[] = { { .compatible = "xlnx,xuartps", }, { .compatible = "cdns,uart-r1p8", }, { .compatible = "cdns,uart-r1p12", .data = &zynqmp_uart_def }, { .compatible = "xlnx,zynqmp-uart", .data = &zynqmp_uart_def }, {} }; static struct platform_driver cdns_uart_platform_driver = { .probe = cdns_uart_probe, .remove = cdns_uart_remove, .driver = { .name = CDNS_UART_NAME, .of_match_table = cdns_uart_of_match, .pm = &cdns_uart_dev_pm_ops, }, };
而在arch/arm/boot/dts/zynq-7000.dtsi中, 定义了uart设备树相关信息
uart0: serial@e0000000 { compatible = "xlnx,xuartps", "cdns,uart-r1p8"; status = "disabled"; clocks = <&clkc 23>, <&clkc 40>; clock-names = "uart_clk", "pclk"; reg = <0xE0000000 0x1000>; interrupts = <0 27 4>; }; uart1: serial@e0001000 { compatible = "xlnx,xuartps", "cdns,uart-r1p8"; status = "disabled"; clocks = <&clkc 24>, <&clkc 41>; clock-names = "uart_clk", "pclk"; reg = <0xE0001000 0x1000>; interrupts = <0 50 4>; };
关于设备树, 可参考
从文章中我们知道内核会将设备树解析为platform_device, 匹配后则会调用cdns_uart_probe
下面以uart0驱动probe分析一下该函数
int cdns_uart_probe(struct platform_device *pdev) { int id, irq; struct uart_port *port; struct resource *res; struct cdns_uart *cdns_uart_data; /* 分配驱动私有数据结构体 */ cdns_uart_data = devm_kzalloc(&pdev->dev, sizeof(*cdns_uart_data), GFP_KERNEL); /* 从dts获取时钟(clocks), pclk=40, uart_clk=23 */ cdns_uart_data->pclk = devm_clk_get(&pdev->dev, "pclk"); cdns_uart_data->uartclk = devm_clk_get(&pdev->dev, "uart_clk"); /* 准备时钟源 */ clk_prepare(cdns_uart_data->pclk); clk_prepare(cdns_uart_data->uartclk); /* 从dts获取编址(reg), start=0xE0000000,end=0xE0001000 */ platform_get_resource(pdev, IORESOURCE_MEM, 0); /* 从dts获取中断(interrupts), 中断号为27 !!! */ platform_get_irq(pdev, 0); /* 获取设备编号, 此处为0 */ id = of_alias_get_id(pdev->dev.of_node, "serial"); /* 初始化uart端口 */ port = cdns_uart_get_port(id); /* 设置uart端口硬件相关参数 */ port->mapbase = res->start; port->irq = irq; port->dev = &pdev->dev; port->uartclk = clk_get_rate(cdns_uart_data->uartclk); port->private_data = cdns_uart_data; cdns_uart_data->port = port; platform_set_drvdata(pdev, port); /* 添加uart端口 */ uart_add_one_port(&cdns_uart_uart_driver, port); } static struct uart_port cdns_uart_port[CDNS_UART_NR_PORTS]; /* 2 */ struct uart_port *cdns_uart_get_port(int id) { struct uart_port *port; /* 获取本地定义的uart_port结构体变量 */ port = &cdns_uart_port[id]; spin_lock_init(&port->lock); port->membase = NULL; port->irq = 0; port->type = PORT_UNKNOWN; /* 会在config_port中设置为PORT_XUARTPS */ port->iotype = UPIO_MEM32; /* 串口接口寄存器的地址类型 */ port->flags = UPF_BOOT_AUTOCONF; /* 该标志会使uart_add_one_port调用config_port */ port->ops = &cdns_uart_ops; /* 即前面定义的uart_ops函数操作集 */ port->fifosize = CDNS_UART_FIFO_SIZE; /* 64 */ port->line = id; /* 0 */ port->dev = NULL; return port; }
4. 16550介绍
16550寄存器信息如下
RBF定义如下
THR定义如下
IER定义如下
IIR定义如下
FCR定义如下
LCR定义如下
MCR定义如下
LSR定义如下
MSR定义如下
SCR定义如下
5. 硬件操作实现
这里分析8250/16550对uart_ops的实现serial8250_pops
主要代码位于drivers/tty/serial/8250/8250_port.c
tx_empty: serial8250_tx_empty
读取并判断LSR的第THRE、TEMT位是否为1
set_mctrl: serial8250_set_mctrl
将位设置(RTS、DTR、OUT1、OUT2、LOOP)写入MCR
get_mctrl: serial8250_get_mctrl
读取MSR, 即Modem Interface的当前状态
stop_tx: serial8250_stop_tx
禁用IER的THRI/ETBEI位
start_tx: serial8250_start_tx
启用IER的THRI/ETBEI位; 当LSR的THRE位为1, 通过操作THR将circ_buf的数据搬运至UART
stop_rx: serial8250_stop_rx
禁用IER的RLSI/ELSI和RDI/ERBFI位
enable_ms: serial8250_enable_ms
启用IER的MSI/EDSSI
break_ctl: serial8250_break_ctl
启动或者禁用LCR的SBC/SetBreak位
startup: serial8250_startup
1. 设置FCR清空FIFO缓冲区, 清空中断寄存器(LSR、RX、IIR、MSR), 初始化相关寄存器
2. 调用uart_8250_ops::setup_irq(univ8250_setup_irq)
3. 设置MCR寄存器
4. 为TX/RX请求DMA通道
univ8250_setup_irq serial_link_irq_chain request_irq serial8250_interrupt dw8250_handle_irq /* 即uart_port::handle_irq */ serial8250_handle_irq handle_rx_dma(Running here???) serial8250_rx_dma /* uart_8250_port::uart_8250_dma::rx_dma */ __dma_rx_complete tty_insert_flip_string /* 将数据插入接收数据缓冲区 */ tty_flip_buffer_push /* 将数据搬至线路规程层 */ tty_schedule_flip flush_to_ldisc serial8250_rx_chars serial8250_read_char uart_insert_char tty_insert_flip_char /* 将数据插入接收数据缓冲区 */ tty_flip_buffer_push /* 将数据搬至线路规程层 */ tty_schedule_flip flush_to_ldisc
shutdown: serial8250_shutdown
初始化寄存器(...), 注销中断处理程序(???)
set_termios: serial8250_set_termios
设置相关寄存器(...)
set_ldisc: serial8250_set_ldisc
如果没有设置了Modem状态, 则禁用IER的MSI位
pm: serial8250_pm
休眠(???)
type: serial8250_type
获取硬件名称
release_port: serial8250_release_port
释放端口占用物理资源, 如Memory, I/O
request_port: serial8250_request_port
请求物理资源
config_port: serial8250_config_port
按照传入参数配置端口
verify_port: serial8250_verify_port
校验端口配置是否有效
参考: