linux串口驱动之三发送

目录

用户层接口

tty内存分配及数据拷贝

获取的同步信号

分配数据块的大小

写入过程

ldisc写入过程

获取同步信号

等待队列

驱动写入同步信号

两种模式判断

启用时(加工模式)

禁用时(原始模式)

通用驱动层的写入

获取同步信号

核心写入过程

芯片驱动层的写入(8250)

使能发送中断及发送控制

发送数据

驱动缓存

  判空

剩余字符数目

到尾部的字符数目

剩余空间

到尾部的剩余空间

初始化

数据在buffer中流程

tail指针的并发访问

head与tail的并发

head写入过程

串口读取tail过程

进程唤醒

ldisc层唤醒

总结

缓存大小

并发访问、调度及休眠唤醒


用户层接口

 struct kiocb *iocb, struct iov_iter *from

static ssize_t tty_write(struct kiocb *iocb, struct iov_iter *from)
{
    return file_tty_write(iocb->ki_filp, iocb, from);
}

tty内存分配及数据拷贝

\kernel\drivers\tty\tty_io.c

获取的同步信号

ret = tty_write_lock(tty, file->f_flags & O_NDELAY);

struct mutex atomic_write_lock;

static int tty_write_lock(struct tty_struct *tty, int ndelay)
{
	if (!mutex_trylock(&tty->atomic_write_lock)) {
		if (ndelay)
			return -EAGAIN;
		if (mutex_lock_interruptible(&tty->atomic_write_lock))
			return -ERESTARTSYS;
	}
	return 0;
}

分配数据块的大小

chunk = 2048;
	if (test_bit(TTY_NO_WRITE_SPLIT, &tty->flags))
		chunk = 65536;
	if (count < chunk)
		chunk = count;

	/* write_buf/write_cnt is protected by the atomic_write_lock mutex */
	if (tty->write_cnt < chunk) {
		unsigned char *buf_chunk;

		if (chunk < 1024)
			chunk = 1024;

1) 块大小模式是2KB.。写入数据分割成临时缓冲区进行处理。这极大地简化了底层驱动的实现,因为它们不需要处理锁的问题和用户模式访问。

2) 当检测有 TTY_NO_WRITE_SPLIT,块大小默认为64KB 

  1. 保证数据完整性 - 防止协议数据包被分割

  2. 提高性能 - 减少系统调用次

3)  1和2中为单次发送的最大数据块,如果发送的数据块长度小于这些值,则按照实际的发送。

写入过程

tty的写入比较简单,即将数据分 数据块大小(2K 或者64K)传递给下层ldisc

do_tty_write(ld->ops->write, tty, file, from);

if (signal_pending(current))
			break;
		cond_resched();

1) 当前写入的进程如果有待处理的信号,则停止写入。此次未写入的数据就丢失了。

2) cond_resched() 的作用是在长时间运行的内核代码路径中主动让出CPU,提供调度机会。

假如用户写入大量的数据,导致写入过程一直循环,导致其他进程得不到调度。

计算写入耗时时,并不仅仅考虑硬件的速率,此处还有写入进程被调度走的时间

ldisc写入过程

\kernel\drivers\tty\n_tty.c

第一步tty分配内存,写入数据后,调用ldisc层的写入接口继续下发数据。

获取同步信号

down_read(&tty->termios_rwsem);

struct rw_semaphore termios_rwsem;

等待队列

DEFINE_WAIT_FUNC(wait, woken_wake_function);
add_wait_queue(&tty->write_wait, &wait);

将当前进程加入到 TTY 写入等待队列中,以便在条件满足时被唤醒

  • 当 TTY 写入条件变得可用时(如缓冲区有空间),内核可以通过这个队列找到并唤醒所有等待的进程

  • 为后续的唤醒操作提供目标列表

wait_woken(&wait, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT); 的作用是让当前进程进入可中断的睡眠状态,直到被显式唤醒或收到信号

分为两种情况:

1)阻塞模式。当底层驱动的buffer已经满了,则将进程休眠。

2)非阻塞模式,直接返回

驱动写入同步信号

struct mutex output_lock;
mutex_lock(&ldata->output_lock);

两种模式判断

启用时(加工模式)

调用驱动层 flush_chars接口

  • 适用于交互式终端会话

  • 提供用户友好的输出格式

  • 自动处理终端控制序列

禁用时(原始模式)

  调用驱动层write接口

  • 适用于二进制数据传输

  • 保持数据的原始格式不变

  • 用于串口通信、文件重定向等场景

这一层的buffer依旧来源于上一层。

通用驱动层的写入

\kernel\drivers\tty\serial\serial_core.c

获取同步信号

端口的spinlock

spinlock_t		lock;			/* port lock */

#define uart_port_lock(state, flags)					\
	({								\
		struct uart_port *__uport = uart_port_ref(state);	\
		if (__uport)						\
			spin_lock_irqsave(&__uport->lock, flags);	\
		__uport;						\
	})

核心写入过程


	while (port) {
		c = CIRC_SPACE_TO_END(circ->head, circ->tail, UART_XMIT_SIZE);
		if (count < c)
			c = count;
		if (c <= 0)
			break;
		memcpy(circ->buf + circ->head, buf, c);
		circ->head = (circ->head + c) & (UART_XMIT_SIZE - 1);
		buf += c;
		count -= c;
		ret += c;
	}

缓存大小 #define UART_XMIT_SIZE    PAGE_SIZE,通常是4K

1)查看驱动层缓存的余量,如果余量大于待写入的数据量count。

2) 如果缓存没有空间,则返回0,即通知上一层进入 休眠的流程。

3) 将数据拷贝到缓存中。

4) 移动缓存的head指针。即这里发送进程作为生产者,采用head指针操控buffer。

这里head的移动并没有加同步信号,还是用了port的同步信号

芯片驱动层的写入(8250)

通用层调用芯片驱动的start_tx进行数据发送

使能发送中断及发送控制

	if (up->ier & UART_IER_THRI)
		return false;
	up->ier |= UART_IER_THRI;
#if defined(CONFIG_ARCH_ROCKCHIP) && defined(CONFIG_NO_GKI)
	up->ier |= UART_IER_PTIME;
#endif
	serial_out(up, UART_IER, up->ier);
	return true;

1) 如果已经使能了发送空中断,则当前已经处于发送数据的流程中,则直接返回false。

2) 如果没有使能发送空中断,则使能。并开始发送数据。

第一次使能发送空中断,理论上芯片里面发送寄存器和发送FIFO都是空的,为何还要软件触发一次主动发送,而非直接通过发送空的中断来触发?

那就是第一次的时候,即使空也不会触发中断,只有发送数据后为空才会触发中断。

发送数据

serial8250_tx_chars

此接口即会在上述总体流程中初始时调用;主要在中断里面会调用。

count = up->tx_loadsz;
	do {
		serial_out(up, UART_TX, xmit->buf[xmit->tail]);
		
		xmit->tail = (xmit->tail + 1) & (UART_XMIT_SIZE - 1);
		port->icount.tx++;
		if (uart_circ_empty(xmit))
			break;
		
	} while (--count > 0);

	if (uart_circ_chars_pending(xmit) < WAKEUP_CHARS)
		uart_write_wakeup(port);

1)获取发送芯片FIFO的大小

2)从circ队列中获取数据写满FIFO

3) 如果circ 队列的字符数据少于 门限值,则唤醒休眠的发送进程。

#define        WAKEUP_CHARS    256    /* hard coded for now    */

驱动缓存

    用户发送的数据,最终落到驱动缓存buffer中。

  判空

   

#define uart_circ_empty(circ)		((circ)->head == (circ)->tail)

剩余字符数目


/* Return count in buffer.  */
#define CIRC_CNT(head,tail,size) (((head) - (tail)) & ((size)-1))

#define uart_circ_chars_pending(circ)	\
	(CIRC_CNT((circ)->head, (circ)->tail, UART_XMIT_SIZE))

到尾部的字符数目

/* Return count up to the end of the buffer.  Carefully avoid
   accessing head and tail more than once, so they can change
   underneath us without returning inconsistent results.  */
#define CIRC_CNT_TO_END(head,tail,size) \
	({int end = (size) - (tail); \
	  int n = ((head) + end) & ((size)-1); \
	  n < end ? n : end;})

剩余空间

/* Return space available, 0..size-1.  We always leave one free char
   as a completely full buffer has head == tail, which is the same as
   empty.  */
#define CIRC_SPACE(head,tail,size) CIRC_CNT((tail),((head)+1),(size))

#define uart_circ_chars_free(circ)	\
	(CIRC_SPACE((circ)->head, (circ)->tail, UART_XMIT_SIZE))

到尾部的剩余空间

/* Return space available up to the end of the buffer.  */
#define CIRC_SPACE_TO_END(head,tail,size) \
	({int end = (size) - 1 - (head); \
	  int n = (end + (tail)) & ((size)-1); \
	  n <= end ? n : end+1;})

tty写入时采用

初始化

在通用串口驱动层startup时,分配发送需要的驱动内存。

	/*
	 * Initialise and allocate the transmit and temporary
	 * buffer.
	 */
	page = get_zeroed_page(GFP_KERNEL);  //这里分配了页大小的缓存
	if (!page)
		return -ENOMEM;

	uart_port_lock(state, flags);
	if (!state->xmit.buf) {
		state->xmit.buf = (unsigned char *) page;
		uart_circ_clear(&state->xmit);  //将头尾都指向0索引
		uart_port_unlock(uport, flags);
	} else {
		uart_port_unlock(uport, flags);
		/*
		 * Do not free() the page under the port lock, see
		 * uart_shutdown().
		 */
		free_page(page);
	}

数据在buffer中流程

1)  串口通用驱动层 ,将上层的数据拷贝head开始的剩余空间中,并移动head。属于用户进程的上下文中。

2) 某些设备驱动,在用户进程的上下文接口中,会通过tail读取刚才写入的数据。然后写入到FIFO中。例如FIFO 为32字节,则从circ buffer中读取32个字节写入到FIFO中。

3) 而对于circ buffer中其余的数据,例如此次用户进程写入1280个字节,前期32个字节写入到FIFO中,后续的字节则在驱动中断上下文中读取。

tail指针的并发访问

首先对于tail指针的访问,即存在两个上下文,即用户发送及中断,对其访问。

前期的通用层在发送时也获取此锁。即uart_port的锁。

并没有单独对circ buffer进行单独的并发访问控制。

struct uart_port *port,
spin_lock_irqsave(&port->lock, flags);

head与tail的并发

同样采用了uart_port的锁。

head写入过程

	while (port) {
		c = CIRC_SPACE_TO_END(circ->head, circ->tail, UART_XMIT_SIZE);
		if (count < c)
			c = count;
		if (c <= 0)
			break;
		memcpy(circ->buf + circ->head, buf, c);
		circ->head = (circ->head + c) & (UART_XMIT_SIZE - 1);
		buf += c;
		count -= c;
		ret += c;
	}

此缓存是连续的数组,并非链表,这里写入的时候用了些技巧,即每次都获取head到tail之间的空余空间,然后拷贝进去。

实际计算几种情况

#define CIRC_SPACE_TO_END(head,tail,size) \
    ({int end = (size) - 1 - (head); \
      int n = (end + (tail)) & ((size)-1); \
      n <= end ? n : end+1;})

1) head=tail=0,size=4096 即初始态

  • end = 4096 - 1 - 0 = 4095

  • n = (4095 + 0) & 4095=4095

CIRC_SPACE_TO_END(0, 0, 4096) = 4095

2) head=4095,tail=0,size=4096即用户进程已写满后再次想写入

  • end = 4096 - 1 - 4095 = 0

  • n = (0 + 0) & 4095 = 0

  • CIRC_SPACE_TO_END(4095, 0, 4096) = 0

3) head=4095,tail=100,size=4096即用户进程已写满后再次想写入,并且驱动已经发送部分数据

CIRC_SPACE_TO_END(4095, 100, 4096) = 1

  • head 指针在缓冲区的最后一个位置(4095)

  • 从 head 到缓冲区末尾的连续可用空间只有 1 个字节

4)基于3的情况,c=1 head=4095

circ->head = (circ->head + c) & (UART_XMIT_SIZE - 1);

circ->head = 0

这是循环缓冲区设计的经典模式,要求缓冲区大小必须是 2 的幂。

5)head=0,tail=200,size=4096即用户进程已写满后再次想写入,并且驱动已经发送部分数据,head已经回绕了

CIRC_SPACE_TO_END(0, 200, 4096) = 199

即当出现3的情况时,通常需要两次拷贝,将用户数据拷贝到circ buffer中。

串口读取tail过程

xmit->tail = (xmit->tail + 1) & (UART_XMIT_SIZE - 1);

这里和写入的计算完全一样,但是每次只是一个字节。

进程唤醒

const struct tty_port_client_operations tty_port_default_client_ops = {
	.receive_buf = tty_port_default_receive_buf,
	.write_wakeup = tty_port_default_wakeup,
};
void tty_wakeup(struct tty_struct *tty)
{
	struct tty_ldisc *ld;

	if (test_bit(TTY_DO_WRITE_WAKEUP, &tty->flags)) {
		ld = tty_ldisc_ref(tty);
		if (ld) {
			if (ld->ops->write_wakeup)
				ld->ops->write_wakeup(tty);
			tty_ldisc_deref(ld);
		}
	}
	wake_up_interruptible_poll(&tty->write_wait, EPOLLOUT);
}

   此处wake_up 与ldisc写入过程中的等待队列完美对应了。唤醒在等待队列中等待特定事件的可中断睡眠进程,并传递事件掩码信息

ldisc层唤醒

static void n_tty_write_wakeup(struct tty_struct *tty)
{
	clear_bit(TTY_DO_WRITE_WAKEUP, &tty->flags);
	kill_fasync(&tty->fasync, SIGIO, POLL_OUT);
}

向注册了异步 I/O 通知的进程发送输出就绪信号

  • 向通过 fcntl(F_SETFL) 设置了 FASYNC 标志的进程发送通知

  • 这些进程希望在外设状态变化时异步接收信号

  • POLL_OUT 事件表示设备已准备好接收输出数据

  • 通知应用程序现在可以安全地进行写入操作而不会阻塞

总结

缓存大小

tty层缓存驱动层缓存唤醒门限

拆分时 2K

不拆分时 64K

PAGE_SIZE 诸如4K等256字节

并发访问、调度及休眠唤醒

     循环发送数据时适时让出CPU,底层驱动缓存忙时的进程休眠,及驱动缓存到达门限时的唤醒,这些是各种驱动通用。

    驱动并非简单的操作硬件,其还要考虑与上层进程的接口,及系统中其他进程的关系。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

proware

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值