linux中pl011串口驱动的简单分析
背景
需要用 rust 重写串口驱动,那么第一步就是对其进行分析。
Amba 总线
pl011 作为 arm 的片内外设,可以使用 amba 总线相关的函数进行驱动注册。
static int __init pl011_init(void)
{
printk(KERN_INFO "Serial: AMBA PL011 UART driver\n");
return amba_driver_register(&pl011_driver);
}
static void __exit pl011_exit(void)
{
amba_driver_unregister(&pl011_driver);
}
arch_initcall(pl011_init);
module_exit(pl011_exit);
arch_initcall 表示初始化代码与体系结构相关,确保该 pl011_init 在体系结构特定的初始化代码后运行。
amba_driver结构体
static struct vendor_data vendor_arm = {
.reg_offset = pl011_std_offsets,
.ifls = UART011_IFLS_RX4_8|UART011_IFLS_TX4_8,
.fr_busy = UART01x_FR_BUSY,
.fr_dsr = UART01x_FR_DSR,
.fr_cts = UART01x_FR_CTS,
.fr_ri = UART011_FR_RI,
.oversampling = false,
.dma_threshold = false,
.cts_event_workaround = false,
.always_enabled = false,
.fixed_options = false,
.get_fifosize = get_fifosize_arm,
};
static const struct amba_id pl011_ids[] = {
{
.id = 0x00041011, // 表示设备的 id,与设备树中arm,primecell-periphid所定义的值对应
.mask = 0x000fffff, // kernel 在匹配设备时,仅会注意 mask 二进制位为 1 的位,忽略为 0 的位
.data = &vendor_arm,
},
{ 0, 0 },
};
static struct amba_driver pl011_driver = {
.drv = {
.name = "uart-pl011",
.suppress_bind_attrs = IS_BUILTIN(CONFIG_SERIAL_AMBA_PL011),
},
.id_table = pl011_ids,
.probe = pl011_probe,
.remove = pl011_remove,
};
在 init 中,通过 amba_driver_register 函数将 pl011_driver 结构体注册到了总线上。
在 kernel 发现有与 id_table 相匹配的设备时,将调用 probe 函数。
我们需要在 probe 中进行设备的初始化和 uart_driver 的注册。
probe 函数
// uart_amba_port 存放封装了的 uart_port 串口信息
struct uart_amba_port {
struct uart_port port; // 所对应的串口端口
const u16 *reg_offset; // 寄存器偏移量数组
struct clk *clk; // 时钟结构体
const struct vendor_data *vendor; // 设备相关的数据
unsigned int im; /* interrupt mask */
unsigned int old_status;
unsigned int fifosize; /* vendor-specific */
unsigned int fixed_baud; /* vendor-set fixed baud rate */
char type[12];
bool rs485_tx_started;
unsigned int rs485_tx_drain_interval; /* usecs */
};
static int pl011_probe(struct amba_device *dev, const struct amba_id *id)
{
struct uart_amba_port *uap;
// vendor_data 源于 amba_id 结构体中设置的 data
struct vendor_data *vendor = id->data;
int portnr, ret;
// 在本文件中定义了*uart_amba_port的数组,在这里寻找一个空位
portnr = pl011_find_free_port();
if (portnr < 0)
return portnr;
// 分配内存
uap = devm_kzalloc(&dev->dev, sizeof(struct uart_amba_port),
GFP_KERNEL);
if (!uap)
return -ENOMEM;
// 获取指针
uap->clk = devm_clk_get(&dev->dev, NULL);
if (IS_ERR(uap->clk))
return PTR_ERR(uap->clk);
// 如果设备树中 cts-event-workaround 被设定,在 vendor 中进行相应设置
if (of_property_read_bool(dev->dev.of_node, "cts-event-workaround")) {
vendor->cts_event_workaround = true;
dev_info(&dev->dev, "cts_event_workaround enabled\n");
}
uap->reg_offset = vendor->reg_offset; // 设置寄存器偏移量
uap->vendor = vendor; // 设置设备相关数据
uap->fifosize = vendor->get_fifosize(dev); //设置最大缓冲区大小,其中get_fifosize函数在vendor中定义
uap->port.iotype = vendor->access_32b ? UPIO_MEM32 : UPIO_MEM; //寄存器访问是否是 32 位
uap->port.irq = dev->irq[0]; // 中断号
uap->port.ops = &amba_pl011_pops; //设备使用的相关函数,**需要实现**,详见后
snprintf(uap->type, sizeof(uap->type), "PL011 rev%u", amba_rev(dev)); //设置 uap 的 type
ret = pl011_setup_port(&dev->dev, uap, &dev->res, portnr); // uart_port 内存相关的设置,详细的在后面
if (ret)
return ret;
amba_set_drvdata(dev, uap); // 将设备结构体中的 data 设置为 uap
return pl011_register_port(uap); // 注册串口
}
pl011_setup_port函数
static int pl011_setup_port(struct device *dev, struct uart_amba_port *uap,
struct resource *mmiobase, int index)
{
void __iomem *base;
// 映射资源
base = devm_ioremap_resource(dev, mmiobase);
if (IS_ERR(base))
return PTR_ERR(base);
// 通过设备树别名进行索引探测
index = pl011_probe_dt_alias(index, dev);
uap->port.dev = dev; //设置设备
uap->port.mapbase = mmiobase->start; //设置物理基地址
uap->port.membase = base; // 设置基地址
uap->port.fifosize = uap->fifosize; // 设置最大缓冲区大小
uap->port.has_sysrq = IS_ENABLED(CONFIG_SERIAL_AMBA_PL011_CONSOLE); // 是否启用 SysRq 功能
uap->port.flags = UPF_BOOT_AUTOCONF; // 标志位,自动配置
uap->port.line = index; // 端口索引
amba_ports[index] = uap; // 将 uap 存储到 amba_port 中
return 0;
}
pl011_register_port函数
//控制台结构体
static struct console amba_console = {
.name = "ttyAMA", //控制台名称
.write = pl011_console_write, //控制台写入函数
.device = uart_console_device, //控制台设备,通过console和index获取tty设备
.setup = pl011_console_setup, //控制台设置函数
.match = pl011_console_match, //控制台匹配函数
.flags = CON_PRINTBUFFER | CON_ANYTIME, //CON_PRINTBUFFER表示在缓冲区中打印,CON_ANYTIME 表示控制台在任何时候都可用
.index = -1, //表示当前未指定特定索引
.data = &amba_reg, //指向串口驱动结构体
};
#define AMBA_CONSOLE (&amba_console)
// 串口驱动结构体
static struct uart_driver amba_reg = {
.owner = THIS_MODULE,
.driver_name = "ttyAMA", // 驱动名
.dev_name = "ttyAMA", //设备名
.major = SERIAL_AMBA_MAJOR,
.minor = SERIAL_AMBA_MINOR,
.nr = UART_NR,
.cons = AMBA_CONSOLE, // 控制台
};
static int pl011_register_port(struct uart_amba_port *uap)
{
int ret, i;
/* 确保来自该UART的中断被屏蔽和清除 */
pl011_write(0, uap, REG_IMSC);
pl011_write(0xffff, uap, REG_ICR);
// 如果驱动没有被注册
if (!amba_reg.state) {
ret = uart_register_driver(&amba_reg); // 注册串口驱动
if (ret < 0) {
dev_err(uap->port.dev,
"Failed to register AMBA-PL011 driver\n");
for (i = 0; i < ARRAY_SIZE(amba_ports); i++)
if (amba_ports[i] == uap)
amba_ports[i] = NULL;
return ret;
}
}
// 将串口端口添加到驱动上
ret = uart_add_one_port(&amba_reg, &uap->port);
if (ret)
pl011_unregister_port(uap);
return ret;
}
amba_pl011_pops结构体
工作量最大的地方(
static const struct uart_ops amba_pl011_pops = {
.tx_empty = pl011_tx_empty,
.set_mctrl = pl011_set_mctrl,
.get_mctrl = pl011_get_mctrl,
.stop_tx = pl011_stop_tx,
.start_tx = pl011_start_tx,
.stop_rx = pl011_stop_rx,
.throttle = pl011_throttle_rx,
.unthrottle = pl011_unthrottle_rx,
.enable_ms = pl011_enable_ms,
.break_ctl = pl011_break_ctl,
.startup = pl011_startup,
.shutdown = pl011_shutdown,
.set_termios = pl011_set_termios,
.type = pl011_type,
.config_port = pl011_config_port,
.verify_port = pl011_verify_port,
#ifdef CONFIG_CONSOLE_POLL
.poll_init = pl011_hwinit,
.poll_get_char = pl011_get_poll_char,
.poll_put_char = pl011_put_poll_char,
#endif
};
这个结构体 uart_ops
包含了一组函数指针,用于定义 UART 驱动的操作。每个函数指针对应特定的 UART 操作。下面是每个成员的作用:
基本操作
-
tx_empty
:pl011_tx_empty
- 检查传输缓冲区是否为空。用于确定是否可以发送新的数据。
-
set_mctrl
:pl011_set_mctrl
- 设置调制控制(Modem Control)信号,如 RTS(请求发送)和 DTR(数据终端就绪)。
-
get_mctrl
:pl011_get_mctrl
- 获取当前调制控制信号的状态。
-
stop_tx
:pl011_stop_tx
- 停止数据传输。当不再需要传输数据时调用。
-
start_tx
:pl011_start_tx
- 开始数据传输。准备发送数据时调用。
-
stop_rx
:pl011_stop_rx
- 停止接收数据。当不再需要接收数据时调用。
-
throttle
:pl011_throttle_rx
- 节流接收数据。通常用于流控制(flow control),防止接收端过载。
-
unthrottle
:pl011_unthrottle_rx
- 解除节流,重新允许接收数据。
-
enable_ms
:pl011_enable_ms
- 启用调制信号(Modem Status)中断,以监控调制信号的变化。
-
break_ctl
:pl011_break_ctl
- 控制 UART 的中断信号。通常用于发送 Break 信号。
初始化和配置
-
startup
:pl011_startup
- 启动 UART 端口。在端口打开时调用,进行初始化。
-
shutdown
:pl011_shutdown
- 关闭 UART 端口。在端口关闭时调用,进行清理工作。
-
set_termios
:pl011_set_termios
- 设置端口的串行参数,如波特率、数据位、停止位、校验位等。
-
type
:pl011_type
- 返回 UART 端口的类型。用于识别端口类型。
-
config_port
:pl011_config_port
- 配置 UART 端口。用于端口的特定配置操作。
-
verify_port
:pl011_verify_port
- 验证 UART 端口配置。检查配置是否有效。
控制台轮询(仅在启用 CONFIG_CONSOLE_POLL
时有效)
-
poll_init
:pl011_hwinit
- 初始化轮询模式。在控制台轮询模式下使用。
-
poll_get_char
:pl011_get_poll_char
- 从 UART 端口读取一个字符。在轮询模式下获取输入。
-
poll_put_char
:pl011_put_poll_char
- 向 UART 端口写入一个字符。在轮询模式下输出字符。
这些函数指针定义了 UART 驱动的各个操作,实现了 UART 的初始化、配置、传输控制、状态获取等功能。每个函数对应一个特定的操作,通过这些操作可以全面管理 UART 端口的工作。