【迅为电子】RK3568驱动指南|第十七篇 串口-第201章 端口注册流程分析

瑞芯微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;
}

现在端口注册流程我们已经分析完毕了,下个章节我们学习应用串口编程。

  • 6
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值