串口DMA驱动实现分析
1.1 DMA控制器基本原理
DMA控制器用于实现各类存储介质间的数据搬移,存储介质包括内存以及各类外设的寄存器空间;
基本的工作原理是,配置好DMA控制器后,通过软件或DMA请求线的方式启动DMA传输,搬移数据时,独自控制系统总线,不需要CPU干预;
DMA在完成数据搬移完成后,通过中断等方式通知CPU;
DMA控制器能请求和释放总线控制权;能给出访问内存所需的地址信息;能够自动修改地址指针;能够设定和修改传送的字节数;
能够向存储器和外设发出相应的读/写控制信号
1.2 Itop4412串口DMA驱动实现分析
如上图所示,itop4412开发板使用的DMA控制器是PL330,PL330包括3个独立的中断控制器,其中DMA_mem用于内存间的数据搬移,DMA_peri1和DMA_peri2用于内存与外设间的数据搬移;
PL330控制器的驱动在内核中已经实现,对应驱动文件drivers\dma\pl330.c,外设驱动只需要调用内核DMA子系统提供的DMA接口,就可以使用pl330.c提供的DMA功能;
如下,是itop4412串口驱动的dts节点,在dts节点中指定串口使用DMA_peri1控制器的第17和18通道
serial_3: serial@13830000 { compatible = "samsung,exynos4210-uart"; reg = <0x13830000 0x100>; interrupts = <GIC_SPI 55 IRQ_TYPE_LEVEL_HIGH>; clocks = <&clock CLK_UART3>, <&clock CLK_SCLK_UART3>; clock-names = "uart", "clk_uart_baud0"; dmas = <&pdma1 17>, <&pdma1 18>; dma-names = "rx", "tx"; status = "disabled"; };
1.2.1 初始化部分
串口驱动初始化过程,申请了两个DMA通道,一个用于数据包接收,一个用于数据包发送,并配置了DMA通道的参数;
对于数据包接收通道,传输方向是外设到内存,数据宽度是1个字节,源地址是串口的数据接收寄存器S3C2410_URXH,burst设为cache的大小16字节,然后申请一片内存作为接收buffer,
使用dma_map_single函数获取内存对应的总线地址;
对于数据包发送通道,传输方向是内存到外设,数据宽度是1个字节,目的地址是串口的数据发送寄存器S3C2410_UTXH,burst设为cache的大小16字节,发送buffer就是tty层的xmit;
/* * 串口DMA驱动的初始化 * 1、申请DMA通道,包括数据接收和数据发送 * 2、并且配置DMA通道的参数 * 3、申请接收buffer,并获取总线地址 * 4、对发送buffer进行DMA映射 */ /* * 1、申请DMA通道 */ /* 申请并配置收包通道 */ dma->rx_conf.direction = DMA_DEV_TO_MEM; --- 外设到内存 dma->rx_conf.src_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE; --- 数据宽度为1个字节 dma->rx_conf.src_addr = p->port.mapbase + S3C2410_URXH; --- 源地址为串口接收数据寄存器 dma->rx_conf.src_maxburst = 16; --- DMA内部接收缓存为16字节 dma->rx_chan = dma_request_chan(p->port.dev, "rx"); dmaengine_slave_config(dma->rx_chan, &dma->rx_conf); /* 申请并配置发包通道 */ dma->tx_conf.direction = DMA_MEM_TO_DEV; --- 内存到外设 dma->tx_conf.dst_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE; --- 数据宽度为1个字节 dma->tx_conf.dst_addr = p->port.mapbase + S3C2410_UTXH; -- 目的地址为串口的发送数据寄存器 dma->tx_conf.dst_maxburst = 16; --- DMA内部发送缓存为16字节 dma->tx_chan = dma_request_chan(p->port.dev, "tx"); dmaengine_slave_config(dma->tx_chan, &dma->tx_conf); /* 申请一片普通内存,然后进行DMA映射 */ dma->rx_size = PAGE_SIZE; dma->rx_buf = kmalloc(dma->rx_size, GFP_KERNEL); dma->rx_addr = dma_map_single(p->port.dev, dma->rx_buf, dma->rx_size, DMA_FROM_DEVICE); --- DMA_FROM_DEVICE:由这个标志,接口内会将该内存的cache置无效 dma->tx_addr = dma_map_single(p->port.dev, p->port.state->xmit.buf, UART_XMIT_SIZE, DMA_TO_DEVICE); --- DMA_TO_DEVICE:保证cpu将数据刷到内存
1.2.2 数据包发送
启动一次DMA发送数据包,需要设置两个部分:串口和DMA发送通道;
串口部分需要通过寄存器配置DMA发送模式;
DMA发送通道部分,需要在发送通道中申请发包描述符,申请描述符时还需要传入发送的字节数以及源地址,并且注册一个传输完成时的回调函数,然后,将描述符提交给DMA控制器驱动,
并且启动该描述符描述的传输;
DMA传输完成后,DMA控制器驱动会调用回调函数通知我们,在回调函数中可以通过接口获取传输结果
/* 注意:为了效率,DMA一次只传输cache字节(16字节),因此需要中断发包和DMA发包相结合的方式 */ /* * 数据包发送 * 1、设置控制寄存器0x4的bit位使能DMA发送模式 * 2、设置发送字节数和内存源地址 * 3、申请发送描述符,启动DMA传输 * 4、DMA传输完成回调函数处理 */ /* 将0x38的bit[2]置1以屏蔽发包中断 */ s3c24xx_set_bit(port, S3C64XX_UINTM_TXD, S3C64XX_UINTM); /* Enable tx dma mode */ /* * 设置控制寄存器0x4的bit位使能DMA发送模式 * 1、bit[22:20]设置为011,使得TXBURST为16字节 * 2、bit[3:2]设置为10,使能串口的DMA发送模式,这两个标志位决定了哪种方式可以写数据到串口的发送buffer */ ucon = rd_regl(port, S3C2410_UCON); ucon &= ~(S3C64XX_UCON_TXBURST_MASK | S3C64XX_UCON_TXMODE_MASK); ucon |= (dma_get_cache_alignment() >= 16) ? S3C64XX_UCON_TXBURST_16 : S3C64XX_UCON_TXBURST_1; ucon |= S3C64XX_UCON_TXMODE_DMA; wr_regl(port, S3C2410_UCON, ucon); /* * 设置发送字节数和内存源地址 */ /* 设置发送字节数为cache缓存行的整数倍 */ dma->tx_size = count & ~(dma_get_cache_alignment() - 1); --- 如果发送字节小于16,那么count为0,DMA就不发送? dma->tx_transfer_addr = dma->tx_addr + xmit->tail; /* 将cache的数据刷到内存 */ dma_sync_single_for_device(ourport->port.dev, dma->tx_transfer_addr, dma->tx_size, DMA_TO_DEVICE); /* * 申请发送描述符,然后提交给pl330控制器驱动,然后,启动DMA传输 */ dma->tx_desc = dmaengine_prep_slave_single(dma->tx_chan, dma->tx_transfer_addr, dma->tx_size, DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT); dma->tx_desc->callback = s3c24xx_serial_tx_dma_complete; --- 传输完成回调函数 dma->tx_desc->callback_param = ourport; dma->tx_bytes_requested = dma->tx_size; dma->tx_cookie = dmaengine_submit(dma->tx_desc); --- 提交DMA发送描述符 dma_async_issue_pending(dma->tx_chan); --- 启动DMA传输 /* * DMA传输完成回调函数处理 */ /* 从发送状态获取成功发送的字节数 */ dmaengine_tx_status(dma->tx_chan, dma->tx_cookie, &state); count = dma->tx_bytes_requested - state.residue; /* 调用如下接口,使cpu看到内存的最新数据,因为该内存原先由DMA操作 */ dma_sync_single_for_cpu(ourport->port.dev, dma->tx_transfer_addr, dma->tx_size, DMA_TO_DEVICE); /* 从tty层请求更多数据 */ spin_lock_irqsave(&port->lock, flags); xmit->tail = (xmit->tail + count) & (UART_XMIT_SIZE - 1); port->icount.tx += count; ourport->tx_in_progress = 0; if (uart_circ_chars_pending(xmit) < WAKEUP_CHARS) uart_write_wakeup(port); --- 从tty层请求更多数据 /* 根据数据大小,决定是否启动DMA传输,如果启动,则直接申请描述符,启动DMA传输 */ s3c24xx_serial_start_next_tx(ourport); spin_unlock_irqrestore(&port->lock, flags);
停止DMA发送的接口
/* * 停止DMA传输 */ dmaengine_pause(dma->tx_chan); dmaengine_tx_status(dma->tx_chan, dma->tx_cookie, &state); dmaengine_terminate_all(dma->tx_chan); dma_sync_single_for_cpu(ourport->port.dev, dma->tx_transfer_addr, dma->tx_size, DMA_TO_DEVICE); async_tx_ack(dma->tx_desc); count = dma->tx_bytes_requested - state.residue; xmit->tail = (xmit->tail + count) & (UART_XMIT_SIZE - 1); port->icount.tx += count;
1.2.3 数据包接收
DMA收包是将数据从串口的数据寄存器搬移到内存,DMA的启动配置和DMA发包的配置大体上一致,也是需要配置串口和DMA接收通道两个部分;
串口部分需要通过寄存器配置寄存器使串口工作于DMA Rx Mode;
DMA接收通道需要申请一个收包描述符,申请描述符时需要传入收包内存的总线地址和本次需要搬移的数据长度,并且注册一个传输完成时的回调函数,然后将描述符提交给DMA控制器驱动并启动该描述符的传输;
/* * 串口驱动初始化时使能收包中断,在收包中断检测到Rx Timeout(寄存器0x10的bit3被置1表示),则启动Rx Dma传输 * Rx DMA流程启动 * 1、提交DMA描述符,启动DMA传输 * 2、设置UART的寄存器,使UART工作于DMA Rx Mode */ /* 把收包内存置无效,把收包内存交给DMA控制器使用 */ dma_sync_single_for_device(ourport->port.dev, dma->rx_addr, dma->rx_size, DMA_FROM_DEVICE); --- 内存这一块还不熟悉 /* 申请DMA描述符 */ dma->rx_desc = dmaengine_prep_slave_single(dma->rx_chan, dma->rx_addr, dma->rx_size, DMA_DEV_TO_MEM, DMA_PREP_INTERRUPT); dma->rx_desc->callback = s3c24xx_serial_rx_dma_complete; dma->rx_desc->callback_param = ourport; dma->rx_bytes_requested = dma->rx_size; /* 提交描述符,启动DMA */ dma->rx_cookie = dmaengine_submit(dma->rx_desc); dma_async_issue_pending(dma->rx_chan); /* * 设置UART的0x4寄存器,使UART工作于DMA Rx Mode * 0x4寄存器:[18:16]设置为011,Rx Dma Burst Size为16字节 * 0x4寄存器:[15:12]设置为1111,没有数据时,经过最大帧周期产生收包中断 * 0x4寄存器:bit[7]和bit[11]置1,使能当收包FIFO为空时,收包超时中断 * 0x4寄存器:[1:0]设置为10,设置UART为DMA Rx模式 */ ucon = rd_regl(port, S3C2410_UCON); ucon &= ~(S3C64XX_UCON_RXBURST_MASK | S3C64XX_UCON_TIMEOUT_MASK | S3C64XX_UCON_EMPTYINT_EN | S3C64XX_UCON_DMASUS_EN | S3C64XX_UCON_TIMEOUT_EN | S3C64XX_UCON_RXMODE_MASK); ucon |= S3C64XX_UCON_RXBURST_16 | 0xf << S3C64XX_UCON_TIMEOUT_SHIFT | S3C64XX_UCON_EMPTYINT_EN | S3C64XX_UCON_TIMEOUT_EN | S3C64XX_UCON_RXMODE_DMA; wr_regl(port, S3C2410_UCON, ucon); /* * DMA完成回调函数处理 * s3c24xx_serial_rx_dma_complete */ /* 读取接收状态获取DMA传输的数据字节数,并向DMA控制器确认描述符 */ dmaengine_tx_status(dma->rx_chan, dma->rx_cookie, &state); received = dma->rx_bytes_requested - state.residue; async_tx_ack(dma->rx_desc); /* * 接收数据处理 * 将收包内存交给CPU处理 * 把数据上送tty层 */ dma_sync_single_for_cpu(ourport->port.dev, dma->rx_addr, dma->rx_size, DMA_FROM_DEVICE); ourport->port.icount.rx += count; copied = tty_insert_flip_string(tty, ((unsigned char *)(ourport->dma->rx_buf)), count); tty_flip_buffer_push(t); tty_kref_put(tty);