前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >驱动大全之UART子系统

驱动大全之UART子系统

作者头像
韦东山
发布2022-05-09 20:46:11
1.1K0
发布2022-05-09 20:46:11
举报
文章被收录于专栏:韦东山嵌入式韦东山嵌入式

百问网韦东山的UART学习笔记

0. 说明

有些同学想知道我是怎么分析驱动的,我正要研究UART子系统,所以写了这个笔记。 笔记并不是完整的教程,前后可能也没有关联,只是笔记,不要期望太多。

1. 从哪里入手? 思路是怎样的?

我们录制的驱动大全,进入到UART子系统了。我们基于IMX6ULL的内核进行分析,从\Linux-4.9.88\drivers\tty\serial\imx.c开始阅读代码。

代码语言:javascript
复制
imx_serial_init
	uart_register_driver(&imx_reg);
		struct tty_driver *normal;
		normal = alloc_tty_driver(drv->nr);
		normal->driver_state    = drv;  // uart_driver

上述代码涉及两个概念:tty_driver、uart_driver,它们有什么关系?继续阅读代码前,需要搞清楚。

我分析驱动时,思路是:

  • 先弄清楚数据流向:APP open/read/write会导致哪些驱动被调用
  • 数据从哪里来?从中断得来,就从中断分析数据流向

2. TTY/Terminal/UART/Console等概念

请参考解密TTY 请仔细阅读此文章,里面有一个图总结得非常好。

在这里插入图片描述
在这里插入图片描述

3. 各类数据结构

tty_driver、uart_driver互相指定对方,这2个结构体都没有硬件的操作函数。

从uart_register_driver开始分析:

代码语言:javascript
复制
struct uart_driver *drv;
struct tty_driver *normal;

normal->driver_name	= drv->driver_name;
normal->name		= drv->dev_name;
normal->major		= drv->major;
normal->minor_start	= drv->minor;

drv->tty_driver = normal;
normal->driver_state    = drv;  // uart_driver

还要给tty设置操作函数:

代码语言:javascript
复制
tty_set_operations(normal, &uart_ops);
	normal->ops = &uart_ops;

一个uart_driver可以支持多个port,在uart_driver中有一个uart_state数组,每个数组项对应一个port:

代码语言:javascript
复制
	/*
	 * Initialise the UART state(s).
	 */
	for (i = 0; i < drv->nr; i++) {
		struct uart_state *state = drv->state + i;
		struct tty_port *port = &state->port;

		tty_port_init(port);  // 初始化环形buffer等
		port->ops = &uart_port_ops;
	}

注册tty_driver:

代码语言:javascript
复制
	retval = tty_register_driver(normal);
		error = register_chrdev_region(dev, driver->num, driver->name);
		d = tty_register_device(driver, i, NULL);
				tty_register_device_attr(driver, index, device, NULL, NULL);
					retval = tty_cdev_add(driver, devt, index, 1);
								driver->cdevs[index]->ops = &tty_fops;
								err = cdev_add(driver->cdevs[index], dev, count);

4. tty_open

文件:drivers\tty\tty_io.c

多个进程共用一个tty?一个tty只有一个ldisc?一个ldisc中只有一个buffer?

代码语言:javascript
复制
tty_fops.open 
    tty_open
    	struct tty_struct *tty;

		// tty属于当前进程的, 多进程可以共用tty吗?
		// 这里tty跟硬件对应,比如/dev/ttySAC0, /dev/ttySAC1?
		// 多个进程可以共用多个tty
		tty = tty_open_current_tty(device, filp); 
		if (!tty)
			tty = tty_open_by_driver(device, inode, filp); // 不涉及硬件操作? tty是软件概念?
					struct tty_struct *tty;
					struct tty_driver *driver = NULL;
					// 根据设备节点找到tty_driver
					driver = tty_lookup_driver(device, filp, &index);
                    /* check whether we're reopening an existing tty */
					// 一个tty_driver可以被多次使用,有多个tty
                    tty = tty_driver_lookup_tty(driver, filp, index);
					if (tty)
                        retval = tty_reopen(tty);
					else
                        tty = tty_init_dev(driver, index);
									tty = alloc_tty_struct(driver, idx);
                                                tty->driver = driver;
                                                tty->ops = driver->ops;
                                                tty->index = idx;
									retval = tty_driver_install_tty(driver, tty);
												// vt.c con_ops跟uart_ops是并列的关系
												// 为每一个APP分配vc(virtual console)
												driver->ops->install(driver, tty) 
                                                或
                                                // uart_ops没那么复杂
                                                tty_standard_install
                                                    driver->ttys[tty->index] = tty;
									retval = tty_ldisc_setup(tty, tty->link);
												tty_ldisc_open(tty, tty->ldisc);
                                                    ret = ld->ops->open(tty);
		if (tty->ops->open)
			// 操作硬件, uart_ops.open, uart_open
            // vt.c con_ops跟uart_ops是并列的关系,它更复杂: 为每一个APP分配vc(virtual console)
            retval = tty->ops->open(tty, filp); 
						tty_port_open
                            int retval = port->ops->activate(port, tty);
		if (filp->f_mode & FMODE_READ)
			__proc_set_tty(tty);
				current->signal->tty = tty_kref_get(tty);

5. tty_read

文件:drivers\tty\tty_io.c

在这里插入图片描述
在这里插入图片描述

5.1 ldisc read

文件:drivers\tty\n_tty.c

函数:n_tty_read

在这里插入图片描述
在这里插入图片描述
代码语言:javascript
复制
copy_from_read_buf
    const unsigned char *from = read_buf_addr(ldata, tail); 
									return &ldata->read_buf[i & (N_TTY_BUF_SIZE - 1)];
	retval = copy_to_user(*b, from, n);

5.2 怎么得到数据的?来自中断?

文件:drivers\tty\serial\imx.c

函数:imx_rxint, 数据存入对应的tty_port,也就是每个tty_port对应一个串口

代码语言:javascript
复制
imx_rxint
    struct imx_port *sport = dev_id;
	struct tty_port *port = &sport->port.state->port;

    tty_insert_flip_char(port, rx, flg) // 只是存入tty_port->buf.tail里
    
    tty_flip_buffer_push(port);  // 通知ldisc处理


// include\linux\tty_flip.h
static inline int tty_insert_flip_char(struct tty_port *port,
					unsigned char ch, char flag)
{
	struct tty_buffer *tb = port->buf.tail;  // Active buffer
                                             // 难道这个buffer会改变?有多个buffer?
	int change;

	change = (tb->flags & TTYB_NORMAL) && (flag != TTY_NORMAL);
	if (!change && tb->used < tb->size) {
		if (~tb->flags & TTYB_NORMAL)
			*flag_buf_ptr(tb, tb->used) = flag;
		*char_buf_ptr(tb, tb->used++) = ch;
		return 1;
	}
	return __tty_insert_flip_char(port, ch, flag);
}


// drivers\tty\tty_buffer.c
/**
 *	tty_flip_buffer_push	-	terminal
 *	@port: tty port to push
 *
 *	Queue a push of the terminal flip buffers to the line discipline.
 *	Can be called from IRQ/atomic context.
 *
 *	In the event of the queue being busy for flipping the work will be
 *	held off and retried later.
 */

void tty_flip_buffer_push(struct tty_port *port)
{
    // 之前INIT_WORK(&buf->work, flush_to_ldisc);
    // 导致flush_to_ldisc被调用
	tty_schedule_flip(port); 
}

// drivers\tty\tty_buffer.c
flush_to_ldisc
    // 示例ldisc的函数处理后,放入ldata->read_buf
    // struct n_tty_data *ldata = tty->disc_data;
    struct tty_port *port = container_of(work, struct tty_port, buf.work);
	struct tty_bufhead *buf = &port->buf;
	struct tty_buffer *head = buf->head;  // 唤醒buffer头部
	
	struct tty_struct *tty;
	struct tty_ldisc *disc;

	tty = READ_ONCE(port->itty);
	disc = tty_ldisc_ref(tty);

	count = receive_buf(disc, head, count);
				unsigned char *p = char_buf_ptr(head, head->read);
				tty_ldisc_receive_buf(ld, p, f, count);
					

// drivers\tty\tty_buffer.c
int tty_ldisc_receive_buf(struct tty_ldisc *ld, unsigned char *p,
			  char *f, int count)
{
	if (ld->ops->receive_buf2)
		count = ld->ops->receive_buf2(ld->tty, p, f, count);
	else {
		count = min_t(int, count, ld->tty->receive_room);
		if (count && ld->ops->receive_buf)
			ld->ops->receive_buf(ld->tty, p, f, count);
	}
	return count;
}

// drivers\tty\n_tty.c
n_tty_receive_buf2  // 从tty_port->buf.head中得到数据,经ldisc处理,放入tty->disc_data->read_buf
    n_tty_receive_buf_common	
    	__receive_buf(tty, cp, fp, n);
			n_tty_receive_buf_standard(tty, cp, fp, count);  // 回显、退格....
				struct n_tty_data *ldata = tty->disc_data;
				n_tty_receive_char_inline(tty, c);
					put_tty_queue(c, ldata);
						*read_buf_addr(ldata, ldata->read_head) = c;
						ldata->read_head++;

一个tty_port->buf.tail确实指向Active buffer,在当前buffer不够用、分配新的buffer时,tail才切换到新buffer。

一个tty_port确实只有一个buffer!

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

6. tty_write

7. 不同的tty设备

/dev/ttymxc0、/dev/tty0、/dev/tty1、/dev/tty这些设备的主、次设备号是:

代码语言:javascript
复制
crw-------    1 root     root        5,   1 Jan  1 00:00 /dev/console
crw-rw-rw-    1 root     tty         5,   0 Jan  1 00:00 /dev/tty
crw--w----    1 root     tty         4,   0 Jan  1 00:00 /dev/tty0
crw--w----    1 root     tty         4,   1 Jan  1 00:00 /dev/tty1
crw-------    1 root     root      207,  16 Jan  1 04:56 /dev/ttymxc0

打开/dev/ttymxc0、/dev/tty0、/dev/tty1、/dev/tty时,对应的都是:tty_fops.open即tty_open

代码语言:javascript
复制
1. open /dev/tty  ((5,0))
// (5,0)对应当前tty,当前tty可能是串口可能是vt    
tty_open
    tty = tty_open_current_tty(device, filp);
                if (device != MKDEV(TTYAUX_MAJOR, 0)) // (5,0)对应当前tty
                    return NULL;
                tty = get_current_tty(); // open /dev/tty前,如果没有open过其他TTY则失败
                if (!tty)
                    return ERR_PTR(-ENXIO);

2. open /dev/tty0 (4,0) 
// vt.c中console_driver->major=4,console_driver->minor_start = 1;
//  (4,0)对应的是console_driver.ttys[fg_console]
//  (4,1)对应的是console_driver.ttys[0]
//  (4,2)对应的是console_driver.ttys[1]
tty_open
    tty = tty_open_by_driver(device, inode, filp);
				struct tty_driver *driver = NULL;
				driver = tty_lookup_driver(device, filp, &index);
                                case MKDEV(TTY_MAJOR, 0): {
                                    extern struct tty_driver *console_driver;
                                    driver = tty_driver_kref_get(console_driver);
                                    *index = fg_console;
                                    break;
                                }

3. open /dev/tty1 (4,1) 
//  (4,1)对应的是console_driver.ttys[0]
//  (4,2)对应的是console_driver.ttys[1]
tty_open
    tty = tty_open_by_driver(device, inode, filp);
				struct tty_driver *driver = NULL;
				driver = tty_lookup_driver(device, filp, &index);
                                get_tty_driver(dev_t device, int *index)
                                    struct tty_driver *p;
									dev_t base = MKDEV(p->major, p->minor_start);
									*index = device - base;

4. open /dev/console (5,1)
对应console_fops.open, 还是tty_open
tty_open
    tty = tty_open_by_driver(device, inode, filp);
				struct tty_driver *driver = NULL;
				driver = tty_lookup_driver(device, filp, &index);
                                case MKDEV(TTYAUX_MAJOR, 1): {
                                    struct tty_driver *console_driver = console_device(index);
                                    if (console_driver) {
                                        driver = tty_driver_kref_get(console_driver);
                                        if (driver) {
                                            /* Don't let /dev/console block */
                                            filp->f_flags |= O_NONBLOCK;
                                            break;
                                        }
                                    }
                                    return ERR_PTR(-ENODEV);
                                }

// 重点在于:console_device
// kernel\printk\printk.c
/*
 * Return the console tty driver structure and its associated index
 */
struct tty_driver *console_device(int *index)
{
	struct console *c;
	struct tty_driver *driver = NULL;

	console_lock();
	for_each_console(c) {
		if (!c->device)
			continue;
		driver = c->device(c, index); // imx_console/vt_console_driver都会成功,是谁?
		if (driver)
			break;
	}
	console_unlock();
	return driver;
}

// 问题: register_console注册了那么多的console,/dev/console对应谁?
// bootargs中可以有多个:console=/dev/tty console=/dev/ttySAC0
// 我们在APP中使用/dev/console时,就是去console_drivers链表中找到tty_driver
// console_drivers链表中有那么多tty_driver,选谁?选第1个!第1个是谁?
// register_console时,谁排在console_drivers链表的第1位?
// 处理bootargs中的信息时,最后一个"console=xxx"就排在console_drivers链表第1位
//  /dev/console对应的是bootargs中最后一个"console=xxx"

8. register_console

register_console是理解printk、/dev/console的关键。

串口、vt.c,都会register_console,那么谁放在console_drivers链表的第1位?

文件:kernel\printk\printk.c

代码语言:javascript
复制
register_console(struct console *newcon)
    // 1. 如果注册过,就不要重复注册了
	if (console_drivers)
		for_each_console(bcon)
			if (WARN(bcon == newcon,
					"console '%s%d' already registered\n",
					bcon->name, bcon->index))
				return;
    
	// 2. 有两类console:bootconsoles(用于earyly_printk)、real consoles(bootconsole之外)
	// bootconsoles用于早期的打印,可以注册多个bootconsoles
	// 但是一旦注册real console后,就无法再注册bootconsoles,并且会unregister所有的bootconsoles

	/* 如果已经注册了real console,那么就不能注册bootconsole
	 * before we register a new CON_BOOT console, make sure we don't
	 * already have a valid console
	 */
	if (console_drivers && newcon->flags & CON_BOOT) {
		/* find the last or real console */
		for_each_console(bcon) {
			if (!(bcon->flags & CON_BOOT)) {
				pr_info("Too late to register bootconsole %s%d\n",
					newcon->name, newcon->index);
				return;
			}
		}
	}

	// 现在都是boot console ?
	if (console_drivers && console_drivers->flags & CON_BOOT)
		bcon = console_drivers;

	// selected_console对应的是bootargs中最后一个"console=xxx"
	if (preferred_console < 0 || bcon || !console_drivers)
		preferred_console = selected_console;

	// 如果你都没有在bootargs中指定"console=xxx",那我们就使用第1个注册的console
	/*
	 *	See if we want to use this console driver. If we
	 *	didn't select a console we take the first one
	 *	that registers here.
	 */
	if (preferred_console < 0) {
		if (newcon->index < 0)
            // newcon->index本意是:你想用这个tty_driver的哪个port作为console?
            // 没设置的话,就设为0
			newcon->index = 0; 
		if (newcon->setup == NULL ||
		    newcon->setup(newcon, NULL) == 0) {
			newcon->flags |= CON_ENABLED;
			if (newcon->device) {
				newcon->flags |= CON_CONSDEV;
				preferred_console = 0;
			}
		}
	}

	/*
	 *	See if this console matches one we selected on
	 *	the command line.
	 */
	// bootargs中指定"console=/dev/ttySAC0  console=/dev/tty"
	// 注册console时,如果跟bootargs中某项匹配的话,如下设置
			if (newcon->setup &&
			    newcon->setup(newcon, c->options) != 0)  // 初始化
				break;

		newcon->flags |= CON_ENABLED; 
		if (i == selected_console) {  // 如果跟最后一项"console=xxx"匹配的话
			newcon->flags |= CON_CONSDEV;
			preferred_console = selected_console;
		}

	// 对应bootargs中没有使用"console=xxx"指定的console,不注册
	if (!(newcon->flags & CON_ENABLED))
		return;

	/*
	 * If we have a bootconsole, and are switching to a real console,
	 * don't print everything out again, since when the boot console, and
	 * the real console are the same physical device, it's annoying to
	 * see the beginning boot messages twice
	 */
	// 早期打印的信息,就不要在newcon上再打印一次了
	if (bcon && ((newcon->flags & (CON_CONSDEV | CON_BOOT)) == CON_CONSDEV))
		newcon->flags &= ~CON_PRINTBUFFER;

	/*
	 *	Put this console in the list - keep the
	 *	preferred driver at the head of the list.
	 */
	// 如果在bootargs中指定了"console=xxx console=yyy"
	// 1. 第一次注册console时,它当然在console_drivers链表的头部
	// 2. bootargs中最后一个console(flag为CON_CONSDEV)放到console_drivers链表的头部
	console_lock();
	if ((newcon->flags & CON_CONSDEV) || console_drivers == NULL) {
		newcon->next = console_drivers;
		console_drivers = newcon;
		if (newcon->next)
			newcon->next->flags &= ~CON_CONSDEV;
	} else {
		newcon->next = console_drivers->next;
		console_drivers->next = newcon;
	}

	if (newcon->flags & CON_PRINTBUFFER) {
		/*
		 * console_unlock(); will print out the buffered messages
		 * for us.
		 */
		raw_spin_lock_irqsave(&logbuf_lock, flags);
		console_seq = syslog_seq;
		console_idx = syslog_idx;
		console_prev = syslog_prev;
		raw_spin_unlock_irqrestore(&logbuf_lock, flags);
		/*
		 * We're about to replay the log buffer.  Only do this to the
		 * just-registered console to avoid excessive message spam to
		 * the already-registered consoles.
		 */
		exclusive_console = newcon; // 下面的console_unlock用到
	}
	console_unlock(); // 把buffered messages在exclusive_console的console上打印出来
	console_sysfs_notify();

	/*
	 * By unregistering the bootconsoles after we enable the real console
	 * we get the "console xxx enabled" message on all the consoles -
	 * boot consoles, real consoles, etc - this is to ensure that end
	 * users know there might be something in the kernel's log buffer that
	 * went to the bootconsole (that they do not see on the real console)
	 */
	pr_info("%sconsole [%s%d] enabled\n",
		(newcon->flags & CON_BOOT) ? "boot" : "" ,
		newcon->name, newcon->index);

	// 已经注册了real console的话,就unregister bootconsole
	if (bcon &&
	    ((newcon->flags & (CON_CONSDEV | CON_BOOT)) == CON_CONSDEV) &&
	    !keep_bootcon) {
		/* We need to iterate through all boot consoles, to make
		 * sure we print everything out, before we unregister them.
		 */
		for_each_console(bcon)
			if (bcon->flags & CON_BOOT)
				unregister_console(bcon);
	}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021-07-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 百问网韦东山的UART学习笔记
  • 0. 说明
  • 1. 从哪里入手? 思路是怎样的?
  • 2. TTY/Terminal/UART/Console等概念
  • 3. 各类数据结构
  • 4. tty_open
  • 5. tty_read
    • 5.1 ldisc read
      • 5.2 怎么得到数据的?来自中断?
      • 6. tty_write
      • 7. 不同的tty设备
      • 8. register_console
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档