基于Linux的UART驱动框架源码分析笔记

2023-05-16

文章目录

  • 前言
  • 一、I.MX6ULL串口接收和发送方式
    • 1.非DMA方式
      • 1.1.接收方式
      • 1.2 发送方式
    • 2.DMA方式
      • 2.1.接收方式
      • 2.2 发送方式
  • 二、UART驱动注册
    • 1.uart_register_driver()函数解析
    • 2.serial_imx_probe()函数解析
    • 3.uart_add_one_port()函数解析
    • 4.注册流程总结
  • 三、打开设备(open操作)
    • 1.uart_open()函数解析
    • 2.imx_uart_dma_init()函数解析
    • 3.uart_change_speed()函数解析
      • 3.1 uart_update_timeout()函数解析
      • 3.2 start_rx_dma()函数解析
    • 4.使用到的回调函数
    • 5.设备打开流程总结
  • 四、数据发送流程(write操作)
    • 1.uart_write()函数解析
    • 2.dma_tx_work()函数解析
    • 3.dma_tx_callback()函数解析
    • 4.参考链接
  • 五、数据接收流程(read操作)
    • 1.dma_rx_callback()函数解析
    • 2.dma_rx_work()函数解析
    • 3.dma_rx_push_data()函数解析
    • 4.接收buffer的理解
  • 六、关闭设备(close操作)
    • 1.uart_close()函数解析
  • 总结


前言

    最近分析了I.MX6ULL的UART驱动源码,查了很多资料,终于算是了解了其中大部分内容,所以想在这里记下笔记分享给大家。但是由于自身能力有限,难免会有一些地方错误,希望大家谅解。如果有不对的地方,大家可以在评论区指出,我会虚心改正。最后,在分析这份源码时,重点参考了一口Linux公众号的文章,这份笔记用的图也是该公众号的图,大家可以去看下,里面很多文章都写的很棒。


一、I.MX6ULL串口接收和发送方式

    UART驱动框架最核心的工作是实现串口的接收和发送,而串口的接收和发送无非跟硬件的底层相关。所以,先了解下I.MX6ULL底层串口实现接发收方式对整个源码理解是有很大帮助的。而串口实现接收和发送主要有两种方式,一种是非DMA方式,一种是DMA方式。可能看了下面这两种方式有点迷迷糊糊,但是没关系,等后面分析了源码再回来看,就比较清晰了。

1.非DMA方式

1.1.接收方式

    1、设置uart的rxfifo中断方式,即rxfifo接收到一定数量的字节数据后产生一次USR1_RRDY中断。在imx_setup_ufcr()函数中完成设置。
     2、设置uart的rxfifo空闲中断方式,即rxfifo已空闲8个字符长度的时间没有接收到数据就会产生一次USR1_AGTIM中断。(这个例程没有采取这种方式)

1.2 发送方式

    1、设置uart的txfifo中断方式,即txfifo少于一定数量的字节数据后产生一次USR1_TRDY中断。在imx_setup_ufcr()函数中完成设置。但该方式还需要使能UCR1_TXMPTYEN(txfifo空中断)位。

    在以上方式中,接收和发送都会产生串口中断,调用imx_int中断函数,对于接收中断,执行imx_rxint()函数完成数据接收;对于发送中断,执行imx_txint()函数完成数据发送。

2.DMA方式

2.1.接收方式

    1、初始化串口接收DMA。首先初始化DMA接收通道,再初始化串口接收缓冲区。在imx_uart_dma_init()函数中完成。
    2、串口DMA使能。使能接收DMA请求,DMA空闲状态检测和DMA空闲中断。即RX DMA buffer存在数据且超过32帧时间没有再接收到数据就会产生一次DMA请求中断。在imx_enable_dma()函数中完成。
     3、串口DMA接收启动。设置串口DMA接收完成回调函数和回调函数触发方式。回调函数触发方式有两种,一种是DMA空闲中断(AGTIM DMA请求),一种是DMA传输一定数量数据后,触发一次回调函数(接收DMA请求)。在start_rx_dma()函数中完成。

2.2 发送方式

    1、初始化串口DMA。初始化DMA发送通道。在imx_uart_dma_init()函数中完成。
    2、串口DMA使能。使能发送DMA请求。在imx_enable_dma()函数中完成。
    3、串口DMA发送启动。设置串口DMA发送完成回调函数。将发送缓冲区映射到连续的虚拟地址,再通过串口DMA进行传输,传输完成后产生DMA发送请求中断,最后会调用该回调函数。在dma_tx_work()函数中完成。

二、UART驱动注册

    我用的是正点原子提供的内核源码。I.MX6ULL的驱动源码路径在drivers\tty\serial\imx.c下。源码分析时,我会把重点的函数和重点的内容讲解,否则整个讲解下来太繁琐。建议大家能对着源码看。注册流程如下图所示:

1.uart_register_driver()函数解析

    代码如下:(我只把程序重点部分提取了出来,主要是为了告诉这个函数主要做了什么工作,注释都在每行代码后面写清楚了,后面的函数解析也是一样)

int uart_register_driver(struct uart_driver *drv)
{
	/* 分配内存空间 */
	drv->state = kzalloc(sizeof(struct uart_state) * drv->nr, GFP_KERNEL);		//申请nr个uart_state空间	
	normal = alloc_tty_driver(drv->nr);											//分配并初始化tty driver
	
	drv->tty_driver = normal;													//将tty_driver赋值给drv->tty_driver进行管理	

	/* tty driver初始化 */
	normal->driver_name	= drv->driver_name;										//tty driver名设置为("IMX-uart")
	normal->name		= drv->dev_name;										//tty driver名设置为("ttymxc")
	normal->major		= drv->major;											//tty driver主设备号设置为(207)								
	normal->minor_start	= drv->minor;											//tty driver次设备号起始设置为(16)	
	normal->type		= TTY_DRIVER_TYPE_SERIAL;								//tty driver类型设置为TTY_DRIVER_TYPE_SERIAL
	normal->subtype		= SERIAL_TYPE_NORMAL;									//tty driver子类型设置为SERIAL_TYPE_NORMAL
	normal->init_termios	= tty_std_termios;	
	normal->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL | CLOCAL;		//波特率9600|字符长度8|使用接收器|关闭设备时挂起|忽略调制解调器线路状态
	normal->init_termios.c_ispeed = normal->init_termios.c_ospeed = 9600;		//c_ispeed=0
	normal->flags		= TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV;			//|动态设备管理
	normal->driver_state    = drv;												//tty driver和uart driver互相绑定
	tty_set_operations(normal, &uart_ops);										//设置tty driver的操作集(该操作集由tty字符设备操作集调用)

	/* 初始化tty port */
	tty_port_init(port);														//初始化tty port,tty port处于uart_state中			
	port->ops = &uart_port_ops;													//设置tty port的操作集	
	
	/* 注册tty driver */
	retval = tty_register_driver(normal);										//注册tty_driver字符设备,并将新注册的tty driver添加到tty_drivers链表上
}

    1、 init_termios的结构类型为ktermios ,该结构体我的理解为是与控制终端的输入输出有关,关于该结构体各个参数的介绍可以参考链接:参考链接
    2、 tty_register_driver函数完成了字符设备的创建,该字符设备使用的文件操作集如下:

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,
	};	

同时该函数将创建的tty_driver挂接到tty_drivers全局链表上进行管理。
    3、 操作集调用顺序,如应用层向串口写数据,先是在应用层调用write函数,再在write函数中调用tty_fops操作集中的tty_write函数,再在tty_write函数调用uart_ops操作集中的uart_write函数,最后再调用imx_pops操作集中的相关控制串口的函数。

2.serial_imx_probe()函数解析

static int serial_imx_probe(struct imx_port *sport, struct platform_device *pdev)
{
	/* serial_imx_probe_dt()函数中完成 */
	sport->port.line = ret;								//记录该设备节点的串口设备号
	sport->have_rtscts = 0;								//imx6ul没有RTS和CTS, 所以此处为0	
	sport->dte_mode = 0;								//不支持数据终端设备,所以此处为0
	sport->devdata = of_id->data;						//将匹配表兼容的那一项附带的数据交给devdata(也就是测试寄存器偏移地址和串口类型)
	
	/* uart port初始化 */
	sport->port.dev = &pdev->dev;								//将平台设备的device结构体赋值给uart_port->dev
	sport->port.mapbase = res->start;							//将串口的起始地址(物理地址)赋值给uart_port->mapbase
	sport->port.membase = base;									//将串口的起始地址(虚拟地址)赋值给uart_port->membase
	sport->port.type = PORT_IMX,								//表示该串口类型为IMX端口
	sport->port.iotype = UPIO_MEM;								//串口接口寄存器的地址类型为8位的内存地址
	sport->port.irq = rxirq;									//记录串口的fifo溢出中断号
	sport->port.fifosize = 32;									//串口接收发送fifo大小为32
	sport->port.ops = &imx_pops;								//该串口设备使用的操作设备集为imx_pops,	
	sport->port.rs485_config = imx_rs485_config;				//485接口配置函数
	sport->port.rs485.flags = SER_RS485_RTS_ON_SEND | SER_RS485_RX_DURING_TX;
	sport->port.flags = UPF_BOOT_AUTOCONF;						//自动探测串口类型
	init_timer(&sport->timer);									//初始化定时器
	sport->timer.function = imx_timeout;						//定时器超时后调用的函数
	sport->timer.data     = (unsigned long)sport;				//定时器超时函数的传入参数
	
	sport->clk_ipg = devm_clk_get(&pdev->dev, "ipg");			//解析设备树,得到串口的ipg时钟
	sport->clk_per = devm_clk_get(&pdev->dev, "per");			//解析设备树,得到串口的per时钟
	
	sport->port.uartclk = clk_get_rate(sport->clk_per);			//得到串口时钟频率
	
	devm_request_irq(&pdev->dev, rxirq, imx_int, 0, dev_name(&pdev->dev), sport);   //注册串口中断函数imx_int
	
	return uart_add_one_port(&imx_reg, &sport->port);				//sport->port就代表了一个串口设备
} 

3.uart_add_one_port()函数解析

int uart_add_one_port(struct uart_driver *drv, struct uart_port *uport)
{
	uport->minor = drv->tty_driver->minor_start + uport->line;			//设置该串口设备的次设备号
	
	/* 1、串口类型设置为PORT_IMX
	   2、取消流控制和串口自检测	*/
	uart_configure_port(drv, state, uport);								//串口配置
	
	/*sysfs文件系统创建和字符设备创建 */
	uport->tty_groups = kcalloc(num_groups, sizeof(*uport->tty_groups), GFP_KERNEL);
	uport->tty_groups[0] = &tty_dev_attr_group;							//sysfs文件系统属性操作集
	tty_dev = tty_port_register_device_attr(port, drv->tty_driver, uport->line, uport->dev, port, uport->tty_groups); //完成字符设备注册和sysfs文件系统注册
	
	uport->flags &= ~UPF_DEAD;											//表示串口可以开始工作
}

4.注册流程总结

    1、uart_register_driver()函数中完成了tty driver字符设备的注册。
    2、serial_imx_probe()函数中完成一些对串口参数的设置和填充,并注册了定时器中断回调函数imx_timeout和串口中断回调函数imx_int。
    3、uart_add_one_port()函数中完成关于串口的sysfs文件系统创建。(看了代码,个人认为在这一步才在完成字符设备创建,在1中,并没有注册)
    4、以上函数执行过后,uart_driver结构体各个参数如下所示(只把重要的写了出来):

struct uart_driver imx_reg =
{
	.driver_name = "IMX-uart";									/*驱动串口名,串口设备名以驱动名为基础*/
	.name = "ttymxc";											/*串口设备名*/
	.nr = 8;													/*该uart_driver支持的串口数*/
	.tty_driver tty_driver[8] = 
	{
		.major = 207;											/*tty driver主设备号设置为(207)*/
		.minor_start = 16;										/*tty driver次设备号起始设置为(16)*/
		.flags	= TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV;	/* |动态设备管理*/
		.tty_operations uart_ops = &uart_ops;					/*tty driver的操作集(该操作集由tty字符设备操作集调用)*/
	}
	.uart_state state[8] =
	{
		.tty_port port[8] =
		{
			.tty_port_operations ops = &uart_port_ops;			/*tty port的操作集*/		
		}
		.uart_port uart_port[8]=
		{
			.type = PORT_IMX;
			.state = uart_state;
			.mapbase =											/*串口寄存器起始物理地址*/	
			.membase =											/*串口寄存器起始虚拟地址*/	
			.irq = platform_get_irq(pdev, 0);					/*串口fifo溢出中断号*/
			.fifosize = 32;
			.uart_ops ops = &imx_pops;							/*串口设备使用的操作设备集*/			
			.uartclk = clk_get_rate(sport->clk_per);			/*串口时钟频率*/
			.attribute_group tty_groups =  &tty_dev_attr_group;	/*sysfs文件系统属性操作集*/
			.timeout = (HZ * bits) / baud + HZ/50;			//计算串口发送以上所有字符长度时间再加上0.02s的时间间隙,当定时器发生中断,表示数据已经接收完成
		}
		.circ_buf xmit
		{
			.buf = get_zeroed_page(GFP_KERNEL);					//串口发送缓冲区
		}
	}
}	

三、打开设备(open操作)

    uart_open函数的源码路径在drivers\tty\serial\serial_core.c下。open流程如下图所示:
在这里插入图片描述

1.uart_open()函数解析

static int uart_open(struct tty_struct *tty, struct file *filp)
{
	/*uart_port_startup()函数中完成*/
	page = get_zeroed_page(GFP_KERNEL);						//开辟发送缓冲区
	state->xmit.buf = (unsigned char *) page;				//创建串口发送缓冲区		
	
	/* imx_startup()函数中完成 */
	retval = clk_prepare_enable(sport->clk_per);			//使能per时钟
	retval = clk_prepare_enable(sport->clk_ipg);			//使能ipg时钟
	
	imx_setup_ufcr(sport, 0);								//设置uart的fifo中断方式:当txfifo字节数小于等于2时产生一次fifo发送中断,rxfifo字节数为1时或者为32时,产生一次fifo接收中断
	imx_uart_dma_init(sport);								//初始化uart_dma
	INIT_DELAYED_WORK(&sport->tsk_dma_tx, dma_tx_work);		//初始化工作队列,DMA传输函数dma_tx_work
	
	temp |= UCR1_UARTEN;									//UART使能
	temp |= (UCR2_RXEN | UCR2_TXEN);						//使能串口接收和发送功能
	
	imx_enable_ms(&sport->port);							//启动定时器,立刻执行定时器中断函数imx_timeout
	
	uart_change_speed(tty, state, NULL);					//设置串口波特率等属性
}

2.imx_uart_dma_init()函数解析

static int imx_uart_dma_init(struct imx_port *sport)
{
	struct dma_slave_config slave_config = {};		//大多数slave DMA使用到的通用信息都在结构体dma_slave_config中。它允许客户端对外设指定DMA的方向、DMA地址、总线宽度、DMA突发长度等等。
	
	/* DMA接收通道配置 */
	sport->dma_chan_rx = dma_request_slave_channel(dev, "rx");			//得到串口接收的dma通道
	slave_config.direction = DMA_DEV_TO_MEM;							//方向为设备到内存						
	slave_config.src_addr = sport->port.mapbase + URXD0;				//源地址为串口接收寄存器
	slave_config.src_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;			//源地址总线宽度为8bit
	slave_config.src_maxburst = RXTL_UART;								//增量突发模式为16
	ret = dmaengine_slave_config(sport->dma_chan_rx, &slave_config);	//设置slave和controller特定的参数
	
	/* 串口接收缓冲区初始化配置 */
	sport->rx_buf.buf = dma_alloc_coherent(NULL, IMX_RXBD_NUM * RX_BUF_SIZE,	//为DMA接收缓冲分配空间,并将首地址转换为虚拟地址,sport->rx_buf.dmaaddr是返回的内存物理地址,dma就可以用。
					&sport->rx_buf.dmaaddr, GFP_KERNEL);
					
	for (i = 0; i < IMX_RXBD_NUM; i++) {
		sport->rx_buf.buf_info[i].rx_bytes = 0;							//初始化当前内存空间接收字节数为0
		sport->rx_buf.buf_info[i].filled = false;						//初始化当前内存空间没被使用
	}
	
	/* DMA发送通道配置 */
	sport->dma_chan_tx = dma_request_slave_channel(dev, "tx");			//得到串口发送的dma通道
	slave_config.direction = DMA_MEM_TO_DEV;							//方向为内存到设备
	slave_config.dst_addr = sport->port.mapbase + URTX0;				//目标地址为串口发送寄存器
	slave_config.dst_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;			//目标地址总线宽度为8bit
	slave_config.dst_maxburst = TXTL;									//增量突发模式为2
	ret = dmaengine_slave_config(sport->dma_chan_tx, &slave_config);	//设置slave和controller特定的参数
	
	sport->dma_is_inited = 1;											//表示dma初始化成功
}

3.uart_change_speed()函数解析

static void uart_change_speed(struct tty_struct *tty, struct uart_state *state, struct ktermios *old_termios)
{
	/* imx_set_termios()函数中完成 */
	ucr2 = UCR2_WS | UCR2_SRST | UCR2_IRTS;					//8位发送和接收字符长度|重置发送、接收状态机和所有FIFO,并注册USR1,USR2,UBIR,UBMR,UBRC,URXD,UTXD和UTS [6-3]|忽略RTS引脚
	del_timer_sync(&sport->timer);							//删除定时器
	
	baud = uart_get_baud_rate(port, termios, old, 50, port->uartclk / 16);		//通过解码termios结构体来获取指定串口的波特率
	quot = uart_get_divisor(port, baud);										//用于计算某一波特率的串口时钟分频数(串口波特率除数)
	
	uart_update_timeout(port, termios->c_cflag, baud);							//用于更新(设置)串口FIFO超出时间

	div = sport->port.uartclk / (baud * 16);							//imx6ul波特率公式:BaudRate=Ref Freq/(16*(UBMR+1)/(UBIR+1)),这一步是在计算(UBMR+1)/(UBIR+1)的比值

	rational_best_approximation(16 * div * baud, sport->port.uartclk,
		1 << 16, 1 << 16, &num, &denom);								//计算波特率的分频值,即denom代表UBMR的值,num代表UBIR的值
		
	ufcr = (ufcr & (~UFCR_RFDIV)) | UFCR_RFDIV_REG(div);				//设置分频
	writel(ufcr, sport->port.membase + UFCR);
	
	writel(num, sport->port.membase + UBIR);							//设置UBIR
	writel(denom, sport->port.membase + UBMR);							//设置UBMR
	
	/* 1、使能接收DMA请求|使能发送DMA请求|使能AGTIM DMA请求|空闲状态检测(超过32帧时间)
	   2、DMA空闲中断使能    
	   3、sport->dma_is_enabled=1    */
	imx_enable_dma(sport);	
	/* 1、设置DMA传输完成回调函数dma_rx_callback
	   2、设置DMA传输完成回调函数触发方式    
	   3、sport->dma_is_rxing = 1   */	
	start_rx_dma(sport);
}

3.1 uart_update_timeout()函数解析

void uart_update_timeout(struct uart_port *port, unsigned int cflag, unsigned int baud)
{
	bits = 10;											//1个起始位+8个字符长度+1个停止位
	bits = bits * port->fifosize;						//fifo大小为32,一个fifo存10个bit,所以一共接收320bit
	port->timeout = (HZ * bits) / baud + HZ/50;			//计算串口发送以上所有字符长度时间再加上0.02s的时间间隙,当定时器发生中断,表示数据已经接收完成
}

3.2 start_rx_dma()函数解析

static int start_rx_dma(struct imx_port *sport)
{
	sport->rx_buf.periods = IMX_RXBD_NUM;									//接收buffer数量									
	sport->rx_buf.period_len = RX_BUF_SIZE;									//每个buffer大小,DMA每传输period_len长度就会调用一次回调函数				
	sport->rx_buf.buf_len = IMX_RXBD_NUM * RX_BUF_SIZE;						//dma总传输长度
	sport->rx_buf.cur_idx = 0;												
	sport->rx_buf.last_completed_idx = -1;
	
	desc = dmaengine_prep_dma_cyclic(chan, sport->rx_buf.dmaaddr,			//获取一个接收描述符
	sport->rx_buf.buf_len, sport->rx_buf.period_len,						//rx_buf.buf_len为dma总传输长度,rx_buf.period_len为dma每传输period_len长度就会调用一次回调函数
	DMA_DEV_TO_MEM, DMA_PREP_INTERRUPT);
	
	desc->callback = dma_rx_callback;										//设置dma接收完成回调函数
	desc->callback_param = sport;
	
	sport->rx_buf.cookie = dmaengine_submit(desc);							//提交传输描述符,把传输描述符加入到DMA engine驱动的等待队列返回值是一个cookie,主要用来检查DMA engine活动的状态过程
	dma_async_issue_pending(chan);											//发起等待的请求并等待回调通知

	sport->dma_is_rxing = 1;												//表示dma处于接收状态
}

4.使用到的回调函数

以上函数使用到的回调函数:
    imx_timeout; //定时器超时中断回调函数
    imx_int //串口中断回调函数
    dma_tx_work //启动串口DMA发送回调函数
    dma_tx_callback //串口DMA发送完成回调函数
    dma_rx_callback //串口DMA接收完成回调函数
    关于imx_timeout函数,个人理解为是与流控控制相关的代码,而我没用过流控的串口设备,所以也就不清楚这部分原理。

5.设备打开流程总结

    1、使能串口时钟,初始化发送缓冲区。
    2、设置uart的fifo中断方式.
    3、在imx_uart_dma_init()函数中初始化串口DMA。
    4、初始化串口DMA发送工作队列。
    5、串口使能,串口接收使能和串口发送使能。
    6、串口波特率设置。
    7、串口接收DMA使能。
    8、串口DMA接收启动。

四、数据发送流程(write操作)

    uart_write函数的源码路径在drivers\tty\serial\serial_core.c下。write流程如下图所示:
在这里插入图片描述

1.uart_write()函数解析

static int uart_write(struct tty_struct *tty,const unsigned char *buf, int count)
{
	circ = &state->xmit;
	c = CIRC_SPACE_TO_END(circ->head, circ->tail, UART_XMIT_SIZE);			//计算发送缓冲区的空闲空间	
	memcpy(circ->buf + circ->head, buf, c);									//将应用层传来的数据拷贝到发送缓冲区空间中	

	/* imx_start_tx()函数中完成 */
	schedule_delayed_work(&sport->tsk_dma_tx, 0);		//调用串口DMA发送回调函数(dma_tx_work),延时时间为0,即立即执行	
}

2.dma_tx_work()函数解析

static void dma_tx_work(struct work_struct *w)
{
	if (xmit->tail > xmit->head && xmit->head > 0) {
		sport->dma_tx_nents = 2;								//表示有两个scatterlist
		sg_init_table(sgl, 2);									//初始化SG table,相当于此函数可以初始化多个scatterlist
		sg_set_buf(sgl, xmit->buf + xmit->tail,UART_XMIT_SIZE - xmit->tail);		//将xmit->tail以后的缓冲区buffer赋给第一个scatterlist
				
		sg_set_buf(sgl + 1, xmit->buf, xmit->head);				//将0到xmit->head之间的缓冲区buffer赋给第二个scatterlist
	} else {
		sport->dma_tx_nents = 1;								//表示有1个scatterlist
		sg_init_one(sgl, xmit->buf + xmit->tail, sport->tx_bytes);	//初始化1个scatterlist,并将xmit->tail到xmit->tail+sport->tx_bytes之间的缓冲buffer赋给scatterlist
	}
	
	ret = dma_map_sg(dev, sgl, sport->dma_tx_nents, DMA_TO_DEVICE);		//将不连续nents个物理内存区域的sgl映射到连续的虚拟地址中,其中DMA_TO_DEVICE为方向,表示dma传输方向为发送
	
	desc = dmaengine_prep_slave_sg(chan, sgl, sport->dma_tx_nents,		//获取一个传输描述符
						DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT);
	desc->callback = dma_tx_callback;					//设置DMA传输完成回调函数
	desc->callback_param = sport;	

	sport->dma_is_txing = 1;					//表示dma正在发送
	
	dmaengine_submit(desc);						//提交传输描述符,把传输描述符加入到DMA engine驱动的等待队列(仅仅提交描述符到DMA engine的等待队列,它不会启动DMA操作)
	dma_async_issue_pending(chan);				//发起等待的请求并等待回调通知	
}

3.dma_tx_callback()函数解析

static void dma_tx_callback(void *data)
{
	dma_unmap_sg(sport->port.dev, sgl, sport->dma_tx_nents, DMA_TO_DEVICE);			//解除映射关系

	sport->dma_is_txing = 0;														//表示此时dma传输完成
	
	xmit->tail = (xmit->tail + sport->tx_bytes) & (UART_XMIT_SIZE - 1);				//调整发送缓冲区的xmit->tail,xmit->tail指向已发送空间的末尾
	sport->port.icount.tx += sport->tx_bytes;										//记录此uart_port已经发送多少个字节
	
	uart_write_wakeup(&sport->port);												//唤醒tty写等待工作队列	,唤醒上层因串口端口写数据而堵塞的进程,通常在串口发送中断处理函数中调用该函数									

	schedule_delayed_work(&sport->tsk_dma_tx, msecs_to_jiffies(1));					//如果一次dma传输没有将串口数据发送完,将继续调用dma_tx_work函数,延时时间为1ms

	if (waitqueue_active(&sport->dma_wait)) {										//判断一个等待队列是否为空,不为空if成立
		wake_up(&sport->dma_wait);													//唤醒dma等待工作队列,比如在关闭串口时需要等待dma传输完成,完成后,再唤醒由于dma正在传输而休眠的任务
	}
}

4.参考链接

1、关于scatterlist介绍和学习,可参考这篇文章:链接地址
2、关于dmaengine介绍和学习,可参考这篇文章:链接地址
3、关于函数dma_map_sg()介绍和学习,可参考这篇文章:链接地址

五、数据接收流程(read操作)

    uart_read函数的源码路径在drivers\tty\serial\serial_core.c下。read流程如下图所示:
在这里插入图片描述

1.dma_rx_callback()函数解析

static void dma_rx_callback(void *data)
{
	status = dmaengine_tx_status(chan, sport->rx_buf.cookie, &state);			//获取当前DMA状态(完成|停止|正在传输)和得到DMA还剩多少数据没有传输
	count = RX_BUF_SIZE - state.residue;										//得到DMA已经传输了多少个字节
	
	sport->rx_buf.buf_info[sport->rx_buf.cur_idx].filled = true;				//表示该buffer已经被使用
	sport->rx_buf.buf_info[sport->rx_buf.cur_idx].rx_bytes = count;				//记录该buffer接收了多少数据
	sport->rx_buf.cur_idx++;													//记录当当前接收数据的buffer的编号
	sport->rx_buf.cur_idx %= IMX_RXBD_NUM;
	
	dma_rx_work(sport);															//上传数据给应用层
}

2.dma_rx_work()函数解析

static void dma_rx_work(struct imx_port *sport)
{
	if (sport->rx_buf.last_completed_idx < cur_idx) {							//last_completed_idx初始值为-1,因为第0个内存块的上一个就是-1
		dma_rx_push_data(sport, tty, sport->rx_buf.last_completed_idx + 1, cur_idx);
	} else if (sport->rx_buf.last_completed_idx == (IMX_RXBD_NUM - 1)) {		//cur_idx<last_completed_idx,且last_completed_idx=19,则读取将0到cur_idx内存块
		dma_rx_push_data(sport, tty, 0, cur_idx);
	} else {																	//cur_idx<last_completed_idx,且last_completed_idx<19
		dma_rx_push_data(sport, tty, sport->rx_buf.last_completed_idx + 1,IMX_RXBD_NUM);
		dma_rx_push_data(sport, tty, 0, cur_idx);
	}	
}

3.dma_rx_push_data()函数解析

static void dma_rx_push_data(struct imx_port *sport, struct tty_struct *tty, unsigned int start, unsigned int end)
{
	for (i = start; i < end; i++) {
		if (sport->rx_buf.buf_info[i].filled) {
			tty_insert_flip_string(port, sport->rx_buf.buf + (i * RX_BUF_SIZE), sport->rx_buf.buf_info[i].rx_bytes);		//将读取的数据放到tty buffer中
			tty_flip_buffer_push(port);											//将tty数据块的数据推到线路规程当中,这个函数的作用就类似于通知tty去线路规程获取从串口过来的数据
			sport->rx_buf.buf_info[i].filled = false;							//清除该内存块使用标志
			sport->rx_buf.last_completed_idx++;
			sport->rx_buf.last_completed_idx %= IMX_RXBD_NUM;
			sport->port.icount.rx += sport->rx_buf.buf_info[i].rx_bytes;		//记录一共读取了多少字节的数据
		}
	}
}

4.接收buffer的理解

    关于接收缓冲理解:在创建接收缓冲时,创建的总大小为20*buffer大小,即一共有20个buffer用来接收串口数据。而每个buffer大小恰恰是一次串口DMA传输的大小。关于函数dmaengine_prep_dma_cyclic()实现了一个循环的DMA操作。即一共20个buffer,这20个buffer轮流用来接收串口数据。所以每个buffer都有自己的一个编号,根据last_completed_idx和cur_idx这两个编号,用来确定将那些编号的buffer里面的数据传递给应用层。

六、关闭设备(close操作)

    uart_close函数的源码路径在drivers\tty\serial\serial_core.c下。close流程如下图所示:在这里插入图片描述

1.uart_close()函数解析

static void uart_close(struct tty_struct *tty, struct file *filp)
{
	/* uart_shutdown()函数内实现 */
	ret = wait_event_interruptible_timeout(sport->dma_wait,!sport->dma_is_rxing && !sport->dma_is_txing, msecs_to_jiffies(1));  //等待DMA传输完成,等待时间1ms

	dmaengine_terminate_all(sport->dma_chan_tx);		//串口DMA发送通道的所有活动停止
	dmaengine_terminate_all(sport->dma_chan_rx);		//串口DMA接收通道的所有活动停止
	
	cancel_delayed_work_sync(&sport->tsk_dma_tx);			//取消串口DMA发送回调函数
	
	imx_stop_tx(port);										//禁用串口发送中断			
	imx_stop_rx(port);										//禁用串口接收中断
	imx_disable_dma(sport);									//禁用DMA	
	
	/*imx_uart_dma_exit()函数中完成*/
	dma_release_channel(sport->dma_chan_rx);						//释放串口DMA接收通道
	dma_release_channel(sport->dma_chan_tx);						//释放串口DMA发送通道
	
	dma_free_coherent(NULL, IMX_RXBD_NUM * RX_BUF_SIZE,(void *)sport->rx_buf.buf, sport->rx_buf.dmaaddr);   //释放接收缓冲区
	free_page((unsigned long)state->xmit.buf);																//释放发送缓冲区
	
	temp &= ~(UCR2_TXEN);										//禁用串口发送
	temp &= ~(UCR1_TXMPTYEN | UCR1_RRDYEN | UCR1_RTSDEN | UCR1_UARTEN);	//禁用串口中断和串口
	
	clk_disable_unprepare(sport->clk_per);				//禁用串口per时钟
	clk_disable_unprepare(sport->clk_ipg);				//禁用串口ipg时钟
	
	
	sport->dma_is_rxing = 0;
	sport->dma_is_txing = 0;
	sport->dma_is_enabled = 0;
	sport->dma_is_inited = 0;
}

总结

    这份笔记更多的是对难点的函数进行解析,给大家一个参考的思路。所以,如果有人想真正搞明白源码实现原理,还是得自己从头到尾去分析一遍。另外,UART驱动源码重点就是在实现串口的接收和发送,只要抓住这个重点,就能明白每一个函数的目的是干嘛。所以,再回去看我最开始写的串口接发收方式,就会发现思路更加的清晰了。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

基于Linux的UART驱动框架源码分析笔记 的相关文章

随机推荐

  • http请求digest auth认证

    1 post请求 public static String postMethod String url String query String host throws IOException String content 61 null 认
  • npm install 报错? fs 不能被resolve ?

    先看一下报错信息 然后我按照他的提示去 node modules 里面找到对应文件 xff0c 注释掉 fs 相关就ok了 可是这样毕竟不雅 经过苦苦寻找 xff0c 终于找到原因了 在 webpack config js 中添加如下 与
  • 树莓派3B+安装Ubuntu16.04系统(64位)

    目录 系统下载 Ubuntu16 04系统 补丁 刷系统 换源 改DNS网关 无线 U盘挂载 原文 系统下载 感谢公众号 xff1a 树莓派爱好者基地 更多系资源请关注公众号 Ubuntu16 04系统 链接 xff1a https pan
  • 将浏览器请求直接导入到 Postman Apifox 中

    方法就是如下图所示 xff0c 好像不需要过多文字来赘述了 应用场景就是浏览器 F12 debug 到的请求 xff0c 可以直接拷贝为 cURL 请求 xff0c 拷贝的内容 xff0c 包含了 请求头 参数 请求体所有内容 xff0c
  • SMPL-论文解读

    文章目录 创新点算法shape blend shape xff1a pose blend shapeJoint locationSMPL model 训练过程Pose Parameter Trainingjoint regressorSha
  • Jetson Xavier NX 刷机烧录及备份还原

    文章目录 前言一 准备二 详细流程1 准备OS升级包2 主机和Jetson产品连接3 系统安装4 载入镜像5 提取 Jeston Xavier NX 母板镜像5 1 在PC端搭建烧录环境5 2 提取镜像 6 镜像还原 xff08 批量复制烧
  • 第二章.1节 进程基本概念

    xff08 PS xff1a 个人课下整理的操作系统笔记 xff0c OneNote直接拷贝过来的 xff0c 图片看不了就将就一下 xff0c 配合张伟老师的PPT看效果更好 xff09 一 进程定义 1 进程是具有某一个功能的程序 在某
  • 使用bladeRF和树莓派+YateBTS做GSM基站

    个人出于对无线电的好奇加入了gnuradio的学习 本次测试是基于以下链接网址内容进行的相关测试 xff1a https www freebuf com geek 102729 html 我的安装步骤是一个大神给我找的 xff1a http
  • linux中使用crontab添加定任务执行失败,提示Permission denied

    问题 xff1a 在linux中 xff0c 使用普通账户创建定时任务执行shell脚本时 xff0c 任务会自动执行 xff0c 但总执行失败 xff08 shell脚本编写确认无误 xff09 如下 xff1a 使用sudo cront
  • Win10 USB转串口连接Linux主机调试

    先梳理一下Linux里的console和tty这两个概念 xff1a Console 控制台 xff0c 用于接收所有的内核消息 xff0c 告警 xff0c 允许用户log in 只有text模式 xff0c 也就是说 xff0c 用户通
  • FreeRTOS:卡在configASSERT( ( pxQueue ) )问题的解决

    使用二值信号量过程中 xff0c 刚开始程序还是正常运行 xff0c 一段时间后出现了死机 通过仿真发现程序卡在了 xQueueSemaphoreTake 函数的 configASSERT pxQueue 中 通过查看 xQueueSema
  • 记录复现“VIBE”遇见的BUG

    docker配置 xff1a ubuntu18 01 xff0c cuda10 1 输入视频测试demo时 xff0c 依次遇见下列问题 第一个问题 39 EGL span class token punctuation span cann
  • 软件专业大学生常用的刷题、找答案、找资源的网站

    接下来介绍的主要是结合个人和身边同学经常使用的几个网站 xff0c 肯定有很多不全 xff0c 欢迎大佬们查漏补缺 刷题 一 牛客网 xff08 https www nowcoder com xff09 牛客网 是一个专注于程序员的学习和成
  • Linux设备驱动

    Linux设备驱动 介绍 Linux系列文章记录自己学习Linux开发的历程 最近终于把迅为4412开发环境搞定了 xff0c 先吐槽一波迅为 xff0c 版本太 的多了 xff0c 安卓Linux搞在一起 xff0c 还有那个硬件原理图
  • Linux 磁盘坏块修复处理(错误:read error: Input/output error)

    当磁盘出现坏块时 xff0c 你对所关联的文件进行读取时 xff0c 一般会出现 read error Input output error 这样的错误 反过来讲 xff0c 当你看到 read error Input output err
  • docker给运行中的容器添加端口映射

    问题描述 docker上面运行容器 xff0c run p 的时候只映射了一个端口 xff0c 后面对该nginx做扩展 xff0c 需要开放其他端口 当然重新再启一个容器在docker启动参数里多配置几个 p端口映射是能解决的 xff0c
  • Git常用命令符

    1 强制推送 xff08 慎用 xff0c 除非你认为其他冲突等可以丢弃 或者不是很重要 xff09 git push force 2 创建文件等小命令 touch a 创建一个a文件 echo 1234 gt gt a 把1234这个内容
  • 全网最全的 LeetCode 国人大神刷题指南,全部 Go 语言实现

    大家好 xff0c 我是欧盆索思 xff08 opensource xff09 xff0c 每天为你带来优秀的开源项目 xff01 说到 LeetCode xff0c 作为一个程序员来说 xff0c 应该不陌生 xff0c 近几年参加面试都
  • ROS实现串口通信

    虚拟串口的搭建 参考Linux下添加虚拟串口 xff0c 接收和发送数据 com py文件代码如下 xff1a span class token comment usr bin env python span span class toke
  • 基于Linux的UART驱动框架源码分析笔记

    文章目录 前言一 I MX6ULL串口接收和发送方式1 非DMA方式1 1 接收方式1 2 发送方式 2 DMA方式2 1 接收方式2 2 发送方式 二 UART驱动注册1 uart register driver 函数解析2 serial