瑞芯微RK3568芯片是一款定位中高端的通用型SOC,采用22nm制程工艺,搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码,支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU,可用于轻量级人工智能应用。RK3568 支持安卓 11 和 linux 系统,主要面向物联网网关、NVR 存储、工控平板、工业检测、工控盒、卡拉 OK、云终端、车载中控等行业。
【公众号】迅为电子
【粉丝群】258811263(加群获取驱动文档+例程)
【视频观看】嵌入式学习之Linux驱动(第十七篇 串口_全新升级)_基于RK3568
【购买链接】迅为RK3568开发板瑞芯微Linux安卓鸿蒙ARM核心板人工智能AI主板
第201章 端口注册流程分析
在上一节中,我们深入研究了 uart_driver 的注册流程,了解了如何将 UART 驱动程序注册到 Linux 内核中。接下来,我们将进一步探讨端口注册流程的分析。
迅为提供的SDK源码设备树中默认打开了串口9,打开kernel/arch/arm64/boot/dts/rockchip/rk3568.dtsi设备树文件,串口9控制器的设备树节点如下所示:
uart9: serial@fe6d0000 {
compatible = "rockchip,rk3568-uart", "snps,dw-apb-uart";
reg = <0x0 0xfe6d0000 0x0 0x100>;
interrupts = <GIC_SPI 125 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru SCLK_UART9>, <&cru PCLK_UART9>;
clock-names = "baudclk", "apb_pclk";
reg-shift = <2>;
reg-io-width = <4>;
dmas = <&dmac0 18>, <&dmac0 19>;
pinctrl-names = "default";
pinctrl-0 = <&uart9m0_xfer>;
status = "disabled";
};
重点看一下第 2 行 compatible 属性值为“snps,dw-apb-uart”。在 Linux 源码中搜索这个值即可找到对应的 UART 驱动文件,此文件为 drivers/tty/serial/8250/8250_dw.c,在此文件中可以找到如下内容:
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 */ }
};
......
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);
可以看出瑞芯微的 UART 本质上是一个 platform 驱动,当节点匹配成功之后,执行dw_probe函数,函数内容如下所示:
static int dw8250_probe(struct platform_device *pdev)
{
struct uart_8250_port uart = {}; // 初始化一个uart_8250_port结构体
struct resource *regs = platform_get_resource(pdev, IORESOURCE_MEM, 0); // 获取设备资源信息
int irq = platform_get_irq(pdev, 0); // 获取中断号
struct uart_port *p = &uart.port; // 获取uart_port指针
struct device *dev = &pdev->dev; // 获取设备指针
struct dw8250_data *data; // 定义dw8250_data结构体指针
int err;
u32 val;
// 检查是否获取到了设备资源信息
if (!regs) {
dev_err(dev, "no registers defined\n");
return -EINVAL;
}
// 检查是否获取到了中断号
if (irq < 0) {
if (irq != -EPROBE_DEFER)
dev_err(dev, "cannot get irq\n");
return irq;
}
spin_lock_init(&p->lock); // 初始化锁
p->mapbase = regs->start; // 设置设备的物理地址
p->irq = irq; // 设置设备的中断号
p->handle_irq = dw8250_handle_irq; // 设置中断处理函数
p->pm = dw8250_do_pm; // 设置设备的电源管理函数
p->type = PORT_8250; // 设置设备类型
p->flags = UPF_SHARE_IRQ | UPF_FIXED_PORT; // 设置设备标志
p->dev = dev; // 设置设备指针
p->iotype = UPIO_MEM; // 设置IO类型
p->serial_in = dw8250_serial_in; // 设置读函数
p->serial_out = dw8250_serial_out; // 设置写函数
p->set_ldisc = dw8250_set_ldisc; // 设置行规则函数
p->set_termios = dw8250_set_termios; // 设置终端参数函数
// 将设备的物理地址映射到内存空间
p->membase = devm_ioremap(dev, regs->start, resource_size(regs));
if (!p->membase)
return -ENOMEM;
// 分配dw8250_data结构体内存空间
data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->dma.fn = dw8250_fallback_dma_filter; // 设置DMA函数指针
data->usr_reg = DW_UART_USR; // 设置UART状态寄存器地址
#ifdef CONFIG_ARCH_ROCKCHIP
data->irq = irq; // 设置中断号
#endif
p->private_data = data; // 设置设备的私有数据指针
// 读取设备属性"snps,uart-16550-compatible",判断是否兼容16550
data->uart_16550_compatible = device_property_read_bool(dev, "snps,uart-16550-compatible");
// 读取设备属性"reg-shift",获取地址偏移值
err = device_property_read_u32(dev, "reg-shift", &val);
if (!err)
p->regshift = val;
// 读取设备属性"reg-io-width",获取IO宽度
err = device_property_read_u32(dev, "reg-io-width", &val);
if (!err && val == 4) {
p->iotype = UPIO_MEM32; // 设置IO类型为32位
p->serial_in = dw8250_serial_in32; // 设置读函数为32位
p->serial_out = dw8250_serial_out32; // 设置写函数为32位
}
// 如果属性"dcd-override"存在,则始终将DCD状态设置为活动状态
if (device_property_read_bool(dev, "dcd-override")) {
data->msr_mask_on |= UART_MSR_DCD;
data->msr_mask_off |= UART_MSR_DDCD;
}
// 如果属性"dsr-override"存在,则始终将DSR状态设置为活动状态
if (device_property_read_bool(dev, "dsr-override")) {
data->msr_mask_on |= UART_MSR_DSR;
data->msr_mask_off |= UART_MSR_DDSR;
}
// 如果属性"cts-override"存在,则始终将CTS状态设置为活动状态
if (device_property_read_bool(dev, "cts-override")) {
data->msr_mask_on |= UART_MSR_CTS;
data->msr_mask_off |= UART_MSR_DCTS;
}
// 如果属性"ri-override"存在,则始终将RI状态设置为非活动状态
if (device_property_read_bool(dev, "ri-override")) {
data->msr_mask_off |= UART_MSR_RI;
data->msr_mask_off |= UART_MSR_TERI;
}
#ifdef CONFIG_ARCH_ROCKCHIP
// 如果属性"wakeup-source"存在,则使能唤醒功能
if (device_property_read_bool(p->dev, "wakeup-source"))
data->enable_wakeup = 1;
else
data->enable_wakeup = 0;
#endif
// 读取属性"clock-frequency",获取时钟频率
device_property_read_u32(dev, "clock-frequency", &p->uartclk);
// 如果存在"baudclk"时钟,则从中获取时钟频率
data->clk = devm_clk_get(dev, "baudclk");
if (IS_ERR(data->clk) && PTR_ERR(data->clk) != -EPROBE_DEFER)
data->clk = devm_clk_get(dev, NULL);
if (IS_ERR(data->clk) && PTR_ERR(data->clk) == -EPROBE_DEFER)
return -EPROBE_DEFER;
if (!IS_ERR_OR_NULL(data->clk)) {
err = clk_prepare_enable(data->clk);
if (err)
dev_warn(dev, "could not enable optional baudclk: %d\n",
err);
else
p->uartclk = clk_get_rate(data->clk);
}
// 如果没有定义时钟频率,则失败
if (!p->uartclk) {
dev_err(dev, "clock rate not defined\n");
err = -EINVAL;
goto err_clk;
}
// 获取APB时钟
data->pclk = devm_clk_get(dev, "apb_pclk");
if (IS_ERR(data->pclk) && PTR_ERR(data->pclk) == -EPROBE_DEFER) {
err = -EPROBE_DEFER;
goto err_clk;
}
if (!IS_ERR(data->pclk)) {
err = clk_prepare_enable(data->pclk);
if (err) {
dev_err(dev, "could not enable apb_pclk\n");
goto err_clk;
}
}
// 获取复位控制
data->rst = devm_reset_control_get_optional_exclusive(dev, NULL);
if (IS_ERR(data->rst)) {
err = PTR_ERR(data->rst);
goto err_pclk;
}
reset_control_deassert(data->rst);
// 应用特定的quirks
dw8250_quirks(p, data);
// 如果设备不兼容16550,则不处理忙标志
if (data->uart_16550_compatible)
p->handle_irq = NULL;
// 如果不跳过自动配置,则进行端口配置
if (!data->skip_autocfg)
dw8250_setup_port(p);
// 如果有有效的FIFO大小,则尝试连接DMA
if (p->fifosize) {
data->dma.rxconf.src_maxburst = p->fifosize / 4;
data->dma.txconf.dst_maxburst = p->fifosize / 4;
uart.dma = &data->dma;
}
// 注册8250端口
data->line = serial8250_register_8250_port(&uart);
if (data->line < 0) {
err = data->line;
goto err_reset;
}
#ifdef CONFIG_ARCH_ROCKCHIP
// 如果使能唤醒功能,则初始化唤醒
if (data->enable_wakeup)
device_init_wakeup(&pdev->dev, true);
#endif
// 设置平台设备的私有数据
platform_set_drvdata(pdev, data);
// 设置电源管理为激活状态
pm_runtime_set_active(dev);
pm_runtime_enable(dev);
return 0;
err_reset:
reset_control_assert(data->rst);
err_pclk:
if (!IS_ERR(data->pclk))
clk_disable_unprepare(data->pclk);
err_clk:
if (!IS_ERR(data->clk))
clk_disable_unprepare(data->clk);
return err;
}
其中170-175行代码使用serial8250_register_8250_port函数注册了8250端口,serial8250_register_8250_port函数内容如下所示:
int serial8250_register_8250_port(struct uart_8250_port *up)
{
struct uart_8250_port *uart;
int ret = -ENOSPC;
if (up->port.uartclk == 0)
return -EINVAL;
mutex_lock(&serial_mutex);
uart = serial8250_find_match_or_unused(&up->port);
if (uart && uart->port.type != PORT_8250_CIR)
.............
serial8250_apply_quirks(uart);
ret = uart_add_one_port(&serial8250_reg,&uart->port);// 向tty核心层注册一个UART端口
}
其中serial8250_find_match_or_unused函数内容如下所示:
static struct uart_8250_port *serial8250_find_match_or_unused(struct uart_port *port)
{
int i;
/*
* 首先,查找一个匹配的端口条目。
*/
for (i = 0; i < nr_uarts; i++)
if (uart_match_port(&serial8250_ports[i].port, port))
return &serial8250_ports[i];
/* 如果还有空闲的端口号,尝试使用 */
i = port->line;
if (i < nr_uarts && serial8250_ports[i].port.type == PORT_UNKNOWN &&
serial8250_ports[i].port.iobase == 0)
return &serial8250_ports[i];
/*
* 如果没有找到匹配的条目,则查找第一个空闲的条目。
* 我们搜索一个以前未使用过的条目(iobase为0表示未使用)。
*/
for (i = 0; i < nr_uarts; i++)
if (serial8250_ports[i].port.type == PORT_UNKNOWN &&
serial8250_ports[i].port.iobase == 0)
return &serial8250_ports[i];
/*
* 如果仍然没有找到,最后的尝试是找到一个没有实际端口相关联的条目。
*/
for (i = 0; i < nr_uarts; i++)
if (serial8250_ports[i].port.type == PORT_UNKNOWN)
return &serial8250_ports[i];
return NULL;
}
其中第15行uart_add_one_port函数向tty核心层注册一个UART端口,uart_add_one_port函数内容如下所示:
int uart_add_one_port(struct uart_driver *drv, struct uart_port *uport)
{
struct uart_state *state;
struct tty_port *port;
int ret = 0;
struct device *tty_dev;
int num_groups;
.........
tty_dev = tty_port_register_device_attr_serdev(port, drv->tty_driver,
uport->line, uport->dev, port, uport->tty_groups);
.........
return ret;
}
tty_port_register_device_attr_serdev函数内容如下所示:
/*
* 注册一个tty设备到tty核心层,如果注册的设备是serdev设备,则不创建cdev。
*/
struct device *tty_port_register_device_attr_serdev(struct tty_port *port,
struct tty_driver *driver, unsigned index,
struct device *device, void *drvdata,
const struct attribute_group **attr_grp)
{
struct device *dev;
// 将tty端口链接到设备
tty_port_link_device(port, driver, index);
// 注册serdev设备
dev = serdev_tty_port_register(port, device, driver, index);
if (PTR_ERR(dev) != -ENODEV) {
/* 如果注册的是serdev设备,则不创建cdev */
return dev;
}
// 如果注册的不是serdev设备,则创建cdev
return tty_register_device_attr(driver, index, device, drvdata,
attr_grp);
}
EXPORT_SYMBOL_GPL(tty_port_register_device_attr_serdev);
该函数用于注册一个tty设备到tty核心层,如果注册的设备是serdev设备,则不创建cdev。首先,它将tty端口链接到设备,然后尝试注册serdev设备。如果注册的是serdev设备,则直接返回注册结果;否则,通过调用 tty_register_device_attr() 函数创建cdev并注册该设备到tty核心层。tty_register_device_attr函数内容如下所示:
/*
* 注册一个tty设备到tty核心层,包括创建cdev,并添加属性组。
*/
struct device *tty_register_device_attr(struct tty_driver *driver,
unsigned index, struct device *device,
void *drvdata,
const struct attribute_group **attr_grp)
{
char name[64]; // 设备名称缓冲区
dev_t devt = MKDEV(driver->major, driver->minor_start) + index; // 计算设备号
struct ktermios *tp;
struct device *dev;
int retval;
// 检查索引是否超出范围
if (index >= driver->num) {
pr_err("%s: Attempt to register invalid tty line number (%d)\n",
driver->name, index);
return ERR_PTR(-EINVAL);
}
// 根据驱动类型生成设备名称
if (driver->type == TTY_DRIVER_TYPE_PTY)
pty_line_name(driver, index, name);
else
tty_line_name(driver, index, name);
// 分配设备结构体内存空间
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
if (!dev)
return ERR_PTR(-ENOMEM);
// 设置设备结构体的各个属性
dev->devt = devt;
dev->class = tty_class;
dev->parent = device;
dev->release = tty_device_create_release;
dev_set_name(dev, "%s", name); // 设置设备名称
dev->groups = attr_grp; // 设置属性组
dev_set_drvdata(dev, drvdata);
dev_set_uevent_suppress(dev, 1); // 设置抑制uevent
// 注册设备到内核
retval = device_register(dev);
if (retval)
goto err_put;
if (!(driver->flags & TTY_DRIVER_DYNAMIC_ALLOC)) {
/*
* 如果驱动不是动态分配的,则释放任何保存的终端参数数据,
* 这样当重用一个次设备号时,终端参数状态将被重置。
*/
tp = driver->termios[index];
if (tp) {
driver->termios[index] = NULL;
kfree(tp);
}
// 添加cdev到tty核心层
retval = tty_cdev_add(driver, devt, index, 1);
if (retval)
goto err_del;
}
dev_set_uevent_suppress(dev, 0); // 取消抑制uevent
kobject_uevent(&dev->kobj, KOBJ_ADD); // 发送uevent通知设备已添加
return dev;
err_del:
device_del(dev); // 删除设备
err_put:
put_device(dev); // 释放设备结构体内存空间
return ERR_PTR(retval);
}
EXPORT_SYMBOL_GPL(tty_register_device_attr);
该函数用于注册一个tty设备到tty核心层,包括创建cdev,并添加属性组。首先,它会根据驱动类型生成设备名称,并分配设备结构体内存空间,并设置设备的各个属性。然后,它注册设备到内核,并根据驱动是否动态分配来决定是否添加cdev到tty核心层。最后,取消抑制uevent并发送uevent通知设备已添加,并返回设备结构体指针。
添加cdev到tty核心层使用tty_cdev_add函数,内容如下所示:
/*
* 向tty核心层添加cdev。
*/
static int tty_cdev_add(struct tty_driver *driver, dev_t dev,
unsigned int index, unsigned int count)
{
int err;
/* 在这里初始化,因为重用的cdev会导致崩溃 */
driver->cdevs[index] = cdev_alloc(); // 分配一个cdev结构体
if (!driver->cdevs[index]) // 如果分配失败则返回错误
return -ENOMEM;
driver->cdevs[index]->ops = &tty_fops; // 设置cdev的操作
driver->cdevs[index]->owner = driver->owner; // 设置cdev的所有者
err = cdev_add(driver->cdevs[index], dev, count); // 添加cdev到内核
if (err)
kobject_put(&driver->cdevs[index]->kobj); // 添加失败时释放资源
return err;
}
该函数用于向tty核心层添加cdev。首先,它分配一个cdev结构体,并设置其操作和所有者。然后,它通过调用 cdev_add() 函数将cdev添加到内核中。如果添加失败,则释放已分配的资源。
在上面代码第13行中,tty_fops 是 tty 驱动中的文件操作结构体,它定义了 tty 设备文件的操作函数。这些函数包括了对 tty 设备文件进行打开、关闭、读取、写入、控制等操作的实现。一般来说,这些函数会调用相应的 tty 核心层函数来完成对底层 tty 设备的操作。 tty_fops结构体如下所示:
static const struct file_operations tty_fops = {
.llseek = no_llseek,
.read = tty_read,
.write = tty_write,
.poll = tty_poll,
.unlocked_ioctl = tty_ioctl,
.compat_ioctl = tty_compat_ioctl,
.open = tty_open,
.release = tty_release,
.fasync = tty_fasync,
.show_fdinfo = tty_show_fdinfo,
};
当用户空间对 tty 设备文件进行操作时,实际上是在调用对应的 tty_fops 中的操作函数。下面是一个简单的示例,展示了如何在用户空间对 tty 设备文件进行操作
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd;
char buffer[128];
// 打开 tty 设备文件
fd = open("/dev/ttyS0", O_RDWR);
if (fd < 0) {
perror("Failed to open tty device");
return -1;
}
// 向 tty 设备文件写入数据
write(fd, "Hello, tty device!", 18);
// 从 tty 设备文件读取数据
read(fd, buffer, sizeof(buffer));
printf("Received data: %s\n", buffer);
// 关闭 tty 设备文件
close(fd);
return 0;
}
现在端口注册流程我们已经分析完毕了,下个章节我们学习应用串口编程。