Toggle navigation
首页
问答
文章
积分商城
专家
专区
更多专区...
文档中心
返回主站
搜索
提问
会员
中心
登录
注册
串口
原创征文
serial_V2
串口设备框架serial_v2源码分析--阻塞模式
发布于 2023-09-13 22:26:58 浏览:1150
订阅该版
[tocm] ## 硬件工作模式选择分析 在serial_v2中,串口设备以应用层视角,即阻塞模式或非阻塞模式来作为该串口设备的开启标志. ``` // 使用阻塞接收阻塞发送模式开启uart_dev设备 rt_device_open(uart_dev,RT_DEVICE_FLAG_TX_BLOCKING|RT_DEVICE_FLAG_TX_BLOCKING); ``` 以下为所有的设备开启模式选择: ``` #define RT_DEVICE_FLAG_RX_BLOCKING #define RT_DEVICE_FLAG_RX_NON_BLOCKING #define RT_DEVICE_FLAG_TX_BLOCKING #define RT_DEVICE_FLAG_TX_NON_BLOCKING ``` 对于其在底层传输中到底是采用轮询,中断,还是DMA,则取决于在rtconfig中对发送与接收缓冲区以及DMA相关的宏进行配置. ``` //开启DMA #define RT_SERIAL_USING_DMA ``` ``` //将串口1的发送缓冲区与接收缓冲区的大小设置为128 #define BSP_UART1_RX_BUFSIZE 128 #define BSP_UART1_TX_BUFSIZE 128 ``` serial_v2串口设备框架对底层硬件工作模式的选择原则大致可总结为: (该原则对于TX与RX通用 ) 1. 缓冲区大小被设置为0,则使用轮询模式. 2. 若开启DMA,缓冲区大小不为0,则优先使用DMA模式。 3. 若未开启DMA,且缓存又并非为0,则使用中断模式。 接下来,笔者将详细分析,串口设备框架与驱动是如何选择硬件工作模式的. 在RT-Thread中,如果我们想使用串口,那我们的第一步将会是: ``` //假设以阻塞接收和阻塞发送模式打开串口 rt_device_open(uart_dev,RT_DEVICE_RX_BLOCKING|RT_DEVICE_TX_BLOCKING) ``` 阻塞模式作为一种应用层的视角,其底层可以由轮询,中断,或DMA中任意一种硬件工作模式实现. 在serial_v2中,rt_device_open()函数将会调用框架层的rt_serial_init()与rt_serial_open()函数,从而根据已有的配置,从三种硬件工作模式中选择其中一种。 ``` //串口初始化函数 //先将发送与接收缓冲区初始化为NULL, 然后对波特率等参数进行设置 static rt_err_t rt_serial_init(struct rt_device * dev) { ... //先发送缓冲区与接收缓缓冲区赋值为空,方便日后初始化 serial->serial_rx = RT_NULL; serial->serial_tx = RT_NULL; ... if (serial->ops->configure) //此次跳转到驱动层的函数,对波特率等参数进行配置 result = serial->ops->configure(serial, &serial->config); } ``` ``` //串口开启函数, 主要调用rt_serial_rx_enable与rt_serial_tx_enable函数会对硬件工作模式做出选择 static rt_err_t rt_serial_open(struct rt_device *dev, rt_uint16_t oflag) { //根据设备开启模式,对RX进行初始化 if (serial->serial_rx == RT_NULL) rt_serial_rx_enable(dev, dev->open_flag &(RT_SERIAL_RX_BLOCKINGRT_SERIAL_RX_NON_BLOCKING)); //根据设备开启模式,对TX进行初始化 if (serial->serial_tx == RT_NULL) rt_serial_tx_enable(dev, dev->open_flag & (RT_SERIAL_TX_BLOCKING|RT_SERIAL_TX_NON_BLOCKING)); } ``` 接下来我们先具体分析一下在rt_serial_open()中调用的rt_serial_rx_enable()如何选择RX硬件工作模式 ``` static rt_err_t rt_serial_rx_enable(struct rt_device * dev, rt_uint16_t rx_oflag) { // 第一种选择:如果接收缓冲区的大小为0,则首先使用轮询接收 if (serial->config.rx_bufsz == 0) { ... //将设备的接收函数指针指向轮询接收 dev->read = _serial_poll_rx; dev->open_flag |= RT_SERIAL_RX_BLOCKING; ... } //为接收缓冲区分配内存,其缓冲区大小为在rtconfig中进行设置 rx_fifo = (struct rt_serial_rx_fifo * ) rt_malloc (sizeof(struct rt_serial_rx_fifo) + serial->config.rx_bufsz); //初始化完成量,用于阻塞等待 rt_completion_init(&(rx_fifo->rx_cpt)); //调用 control(),根据已有的配置,决定使用DMA或中断 serial->ops->control(serial,RT_DEVICE_CTRL_CONFIG, (void *) RT_SERIAL_RX_BLOCKING ) } ``` serial->ops->control会调用驱动层的stm32_control(),对DMA或中断进行选择,笔者仅将关键部分列出 : ``` static rt_err_t stm32_control(struct rt_serial_device *serial, int cmd, void *arg) { //RX模式下的工作模式选择 if(ctrl_arg&( RT_DEVICE_FLAG_RX_BLOCKING | RT_DEVICE_FLAG_RX_NON_BLOCKING)) { ... //先判断DMA相关的宏是否被定义,如果是则将ctrl_arg赋值为DMA相关标志,为下文初始化做准备 if (uart->uart_dma_flag & RT_DEVICE_FLAG_DMA_RX) ctrl_arg = RT_DEVICE_FLAG_DMA_RX; else ctrl_arg = RT_DEVICE_FLAG_INT_RX; ... } ... //如果ctrl_arg被赋予了DMA标志相关的值,则选择DMA模式,对DMA进行初始化 if (ctrl_arg & (RT_DEVICE_FLAG_DMA_RX | RT_DEVICE_FLAG_DMA_TX)) { #ifdef RT_SERIAL_USING_DMA //具体分析见下文 stm32_dma_config(serial, ctrl_arg); } else { //如果未开启DMA.则选择中断模式,则对中断相关的函数进行初始化 //具体分析见下文 stm32_control(serial, RT_DEVICE_CTRL_SET_INT, (void*)ctrl_arg); break; } } ``` stm32_control()对中断接收进行初始化: ``` static rt_err_t stm32_control(struct rt_serial_device *serial, int cmd, void *arg) { ... case RT_DEVICE_CTRL_SET_INT: //设置单个中断抢占优先级和响应优先级 HAL_NVIC_SetPriority(uart->config->irq_type, 1, 0); //设置使能中断通道 HAL_NVIC_EnableIRQ(uart->config->irq_type); //使能RXNE相关中断 if (ctrl_arg == RT_DEVICE_FLAG_INT_RX) __HAL_UART_ENABLE_IT(&(uart->handle), UART_IT_RXNE); break; } ``` stm32_dma_config()对DMA接收进行初始化 ``` static void stm32_dma_config(struct rt_serial_device *serial, rt_ubase_t flag) { ... if (RT_DEVICE_FLAG_DMA_RX == flag) { //设置DMA接收数据所用的RX缓冲区 DMA_Handle = &uart->dma_rx.handle; dma_config = uart->config->dma_rx; ... if (RT_DEVICE_FLAG_DMA_RX == flag) { __HAL_LINKDMA(&(uart->handle), hdmarx, uart->dma_rx.handle); } } ... //使能DMA相关中断 if (flag == RT_DEVICE_FLAG_DMA_RX) { rx_fifo = (struct rt_serial_rx_fifo *)serial->serial_rx; RT_ASSERT(rx_fifo != RT_NULL); /* Start DMA transfer */ if (HAL_UART_Receive_DMA(&(uart->handle), rx_fifo->buffer, serial->config.rx_bufsz) != HAL_OK) { /* Transfer error in reception process */ RT_ASSERT(0); } CLEAR_BIT(uart->handle.Instance->CR3, USART_CR3_EIE); __HAL_UART_ENABLE_IT(&(uart->handle), UART_IT_IDLE); } ... //此处省略其他的初始化函数 } ``` 以上就是RX的硬件工作模式选择,与对应硬件初始化的部分代码 接下来分析一下rt_serial_tx_enable()如何选择硬件工作模式与初始化硬件: ``` static rt_err_t rt_serial_tx_enable(struct rt_device*dev, rt_uint16_t rx_oflag){ // 第一种情况:如果发送缓冲区的大小为0,则使用轮询发送 if (serial->config.tx_bufsz == 0) { ... dev->read = _serial_poll_tx; dev->open_flag |= RT_SERIAL_TX_BLOCKING; ... } //如果是阻塞模式,接下来就将调用底层control函数,选择DMA或中断 if (tx_oflag == RT_SERIAL_TX_BLOCKING) { optmode = serial->ops->control(serial,RT_DEVICE_CHECK_OPTMODE, (void *)RT_DEVICE_FLAG_TX_BLOCKING); *************************************************** *补充 : serial->ops->control()函数的源码 *即如何在DMA与中断中做出选择 *RT_DEVICE_CHECK_OPTMODE: * { * //如果开启DMA,那么优先使用DMA * if (ctrl_arg & RT_DEVICE_FLAG_DMA_TX) * return RT_SERIAL_TX_BLOCKING_NO_BUFFER; * else * //未开启则使用中断 * return RT_SERIAL_TX_BLOCKING_BUFFER; * } *************************************************** //optmode的值,即上述调用函数的返回值有两种情况: //RT_SERIAL_TX_BLOCKING_BUFFER,使用框架层缓冲区,即中断发送, //RT_SERIAL_TX_BLOCKING_NO_BUFFER,不使用框架层缓冲区,即使用DMA发送 //中断与DMA发送在软件层面的一大区别是中断发送需要使用框架层的发送缓冲区而DMA发送直接使用应用层缓冲区,不需要框架层分配缓冲区 if (optmode == RT_SERIAL_TX_BLOCKING_BUFFER){ //使用中断发送 //则首先为tx缓冲区分配有tx_bufsz大小的空间 tx_fifo = (struct rt_serial_tx_fifo *) rt_malloc (sizeof(struct rt_serial_tx_fifo)+serial->config.tx_bufsz); //将设备的写函数指针指向中断发送函数 dev->write = _serial_fifo_tx_blocking_buf; ... } else { //使用DMA模式发送,此处缓冲区没有分配tx_bufsz大小的空间 //DMA使用的是应用层的buffer tx_fifo = (struct rt_serial_tx_fifo*) rt_malloc (sizeof(struct rt_serial_tx_fifo)); //初始化DMA serial->ops->control(serial,RT_DEVICE_CTRL_CONFIG, (void *)RT_SERIAL_TX_BLOCKING); //因为使用不使用框架层接收缓冲区,因此将其设置为NULL rt_memset(&tx_fifo->rb, RT_NULL, sizeof(tx_fifo->rb)); ... } //将激活标识设置为false,激活标识用于标记发送缓存是否被使用 tx_fifo->activated = RT_FALSE; //阻塞发送需要初始化完成量 rt_completion_init(&(tx_fifo->tx_cpt)); ... } } ``` stm32_control对中断发送进行初始化: ``` static rt_err_t stm32_control(struct rt_serial_device *serial, int cmd, void *arg) { ... case RT_DEVICE_CTRL_SET_INT: //设置单个中断抢占优先级和响应优先级 HAL_NVIC_SetPriority(uart->config->irq_type, 1, 0); //设置使能中断通道 HAL_NVIC_EnableIRQ(uart->config->irq_type); //使能TXE相关中断 if (ctrl_arg == RT_DEVICE_FLAG_INT_TX) __HAL_UART_ENABLE_IT(&(uart->handle), UART_IT_TXE); break; } ``` stm32_dma_config对DMA发送进行初始化: ``` static void stm32_dma_config(struct rt_serial_device *serial, rt_ubase_t flag) { ... else //RT_DEVICE_FLAG_DMA_TX == flag { //对相关句柄进行赋值 DMA_Handle = &uart->dma_tx.handle; dma_config = uart->config->dma_tx; } ... if (RT_DEVICE_FLAG_DMA_TX == flag) { //设置好DMA的发送缓冲区 __HAL_LINKDMA(&(uart->handle), hdmatx, uart->dma_tx.handle); } ... //此处省略其他的初始化函数 } ``` 目前通过分析,我们理清楚了串口设备框架是如何与驱动合作,对底层的硬件工作方式,根据已有的配置做出选择,并对选择的硬件工作方式进行相关的初始化工作,接下来,我们将分析serial_v2在阻塞模式下的具体工作流程. ## 阻塞模式 ### 阻塞模式下的数据接收 #### 数据接收函数分析 ``` static rt_ssize_t rt_serial_read(struct rt_device *dev, rt_off_t pos, void *buffer, rt_size_t size) { ... //如果设置了接收缓冲区,则调用_serial_fifo_rx(),即使用中断或DMA模式接收 if (serial->config.rx_bufsz) { return _serial_fifo_rx(dev, pos, buffer, size); } //否则缓冲区大小为0,使用轮询模式,此处结论与上文一致 return _serial_poll_rx(dev, pos, buffer, size); } ``` #### 轮询 轮询,就是CPU通过不断地查询某个外部设备的状态,如果外部设备准备好,就可以向其发送数据或者读取数据。然而,这种方式由于CPU不断查询总线,导致指令执行受到影响,效率较低。 以下是serial_v2中轮询_serial_poll_rx()函数的部分源码. ``` //驱动框架层函数 rt_ssize_t _serial_poll_rx(struct rt_device *dev, rt_off_t pos, void *buffer, rt_size_t size) { ... while(size) { //调用驱动层函数去读一个数据 getc_element = serial->ops->getc(serial); if (getc_element == -1) break; *getc_buffer = getc_element; ++ getc_buffer; -- size; } return getc_size - size; } ``` 其中serial->ops->getc(serial)在底层驱动中调用stm32_getc() 以下为stm32_getc()部分源码 ``` //驱动层函数 static int stm32_getc(struct rt_serial_device *serial){ ... ch = -1; if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_RXNE) != RESET) ch = UART_GET_RDR(&uart->handle,stm32_uart_get_mask(...)); return ch; } ``` 通过以上代码,我们可以发现,轮询的底层实现就是就是不断检查RXNE(接收数据寄存器非空标志位),然后从RDR(接收数据寄存器)中读取数据,如果某一次RXNE寄存器被复位,那么_serial_poll_rx()函数会直接返回已经读到的数据的数量,结束轮询接收。 #### 中断 中断方式克服了CPU轮询外部设备的缺点,正常情况下,CPU执行指令,不会主动去检查外部设备的状态。外部设备准备好之后,向CPU发送中断信号,CPU收到中断信号后停止当前的工作,会根据中断信号指定的设备号处理相应的设备。这种处理方式既不影响CPU的工作,也能保证外部设备的数据得到及时处理,工作效率很高. 由上文rt_serial_read()函数分析可知,中断接收是由_serial_fifo_rx(dev, pos, buffer, size)实现的。 ``` //驱动框架层中断接收部分代码 static rt_ssize_t _serial_fifo_rx(struct rt_device *dev, rt_off_t pos, void *buffer, rt_size_t size) { if (dev->open_flag & RT_SERIAL_RX_BLOCKING) { ... //此处可以注意到,阻塞接收函数第一步是检查应用层想读的数据量是否大于缓存, //如果大于,函数将不工作,直接返回,并提示需要改变缓冲区大小设置 if (size > serial->config.rx_bufsz) { LOG_W("(%s) serial device received data:[%d] larger than " "rx_bufsz:[%d], please increase the BSP_UARTx_RX_BUFSIZE option",dev->parent.name, size, serial->config.rx_bufsz); return 0; } //计算中断接收到缓冲区的字节数量 recv_len = rt_ringbuffer_data_len(&(rx_fifo->rb)); //如果收到的数据小于应用层要求的数据,那么调用rt_completion_wait陷入等待 //直至有足够的数据才退出 if (recv_len < size) { rx_fifo->rx_cpt_index = size; //与下文中断服务函数中完成量的唤醒遥相呼应,从而在应用层视角看是以阻塞模式进行数据接收 rt_completion_wait(&(rx_fifo->rx_cpt),RT_WAITING_FOREVER); } //关中断,确保多线程下不会出现竞态条件 level = rt_hw_interrupt_disable(); //将缓冲区的数据转移到应用层缓冲区 recv_len = rt_ringbuffer_get(&(rx_fifo->rb), buffer, size); rt_hw_interrupt_enable(level); } } ``` 看到这里可能有人会问,怎么数据突然就跑到接收缓冲区了,数据是在什么时候被放进去的,详细的流程是什么?接下来,笔者就通过一个场景以及相关的代码大致来说明中断接收的整个流程. 说明: RDR: RX Data Register 接收数据寄存器 RXNE: RX Data Register Not Empty 接收数据寄存器为空 uart_isr() : 驱动层中断服务函数 rt_hw_serial_isr() : 驱动框架层中断服务函数 rx_fifo: 接收缓冲区 1. 假设接收缓冲区大小为128,即在etconfig中设置#define BSP_UART1_RX_BUFSIZE 128 2. 调用rt_device_open(uart_dev, RT_SERIAL_RX_BLOCKING),与rt_device_read(uart_dev,buffer,128),即应用层一次读取128个数据 3. 用户输入一段字符 4. 设备收到字符触发RXNE中断 5. 驱动层中断服务函数uart_isr()接收RXNE中断,将字符传入缓冲区,并调用rt_hw_serial_isr() 6. 框架层中断服务函数rt_hw_serial_isr()根据rx_fifo接收的数据量,来判断阻塞接收是否结束,若接收足够的数据则唤醒完成量,并设置回调函数的参数 7. 若rx_fifo中未获取足够的数据,重复4 - 6步,直至接收完成,此时rt_device_read()函数结束,返回接收字节数量. ``` //驱动层中断服务函数 static void uart_isr(struct rt_serial_device *serial) { ... //如果RXNE未被复位,且接收到RXNE中断,则从RDR寄存器中取数据 if ((__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_RXNE) != RESET) && (__HAL_UART_GET_IT_SOURCE(&(uart->handle), UART_IT_RXNE) != RESET)) { //调用环形缓冲区ringbuff的putchar(),将读取的一个字节放入ringbuffer rt_ringbuffer_putchar(&(rx_fifo->rb), UART_GET_RDR(& uart->handle, stm32_uart_get_mask(...))); //调用设备框架层中断服务函数,做后续的处理,决定读取是否完成,见下一个代码块 rt_hw_serial_isr(serial, RT_SERIAL_EVENT_RX_IND); } ... } ``` ``` //框架层中断服务函数 void rt_hw_serial_isr(struct rt_serial_device *serial, int event) { ... case RT_SERIAL_EVENT_RX_IND: //计算RX缓冲区的数据量 rx_length = rt_ringbuffer_data_len(&rx_fifo->rb); if (serial->parent.open_flag & RT_SERIAL_RX_BLOCKING) { //如果ringbuffer缓冲区中有足够的数据,那么可以调用完成量的唤醒函数 //唤醒上文中rt_serial_read()中等待的完成量 if (rx_fifo->rx_cpt_index && rx_length >= rx_fifo->rx_cpt_index) { rx_fifo->rx_cpt_index = 0; //代表此次数据接收完成 //此处的唤醒与上文完成量的等待遥相呼应 rt_completion_done(&(rx_fifo->rx_cpt)); } if (serial->parent.rx_indicate != RT_NULL) //为回调函数函数设置参数 serial->parent.rx_indicate(&(serial->parent), rx_length); } } ``` 总结,驱动层保证数据从RDR寄存器到rx_fifo,即接收缓冲区,框架层则统计tx_fifo中的数据是否符合应用层要求,,如果符合要求,则将数据复制到应用层缓冲区,二者有机结合,形成阻塞读取. #### DMA 中断方式效率虽然很高,但是对于大量数据的传输就显得力不从心。大量的中断会导致CPU忙于处理中断而减少对指令的处理,效率会变的很低。对于大量的数据传输可以不通过CPU而直接传送到内存,这种方式叫做DMA(DIrect Memory Access)。使用DMA方式,外部设备在数据准备好之后只需向DMA控制器发送一个命令,把数据的地址和大小传送过去,由DMA控制器负责把数据从外部设备直接存放到内存因此,DMA方式适合处理大量的数据。 由上文rt_serial_read()函数分析可知,DMA接收是由_serial_fifo_rx(dev, pos, buffer, size)实现的.虽然DMA模式接收的串口设备框架代码与中断接收一致,但是在中断服务函数部分还是有很多不同. ``` //驱动框架层函数 static rt_ssize_t _serial_fifo_rx(struct rt_device *dev, rt_off_t pos, void *buffer, rt_size_t size) { //中断接收部分代码 if (dev->open_flag & RT_SERIAL_RX_BLOCKING) { ... //此处可以注意到,阻塞接收的函数第一步是检查应用层想读的数据量是否大于缓存, //如果大于,函数将直接返回,并提示需要改变缓冲区大小设置 if (size > serial->config.rx_bufsz) { LOG_W("(%s) serial device received data:[%d] larger than " "rx_bufsz:[%d], please increase the BSP_UARTx_RX_BUFSIZE option",dev->parent.name, size, serial->config.rx_bufsz); return 0; } //计算中断接收到缓冲区的字节数量 recv_len = rt_ringbuffer_data_len(&(rx_fifo->rb)); //如果收到的数据小于应用层要求的数据,那么调用rt_completion_wait陷入等待 //直至有足够的数据才退出 if (recv_len < size) { rx_fifo->rx_cpt_index = size; //与下文中断服务函数中完成量的唤醒遥相呼应,从而在应用层视角看是以阻塞模式进行数据接收 rt_completion_wait(&(rx_fifo->rx_cpt), RT_WAITING_FOREVER); } //关中断,确保多线程下不会出现竞态条件 level = rt_hw_interrupt_disable(); //将缓冲区的数据转移到应用层缓冲区 recv_len = rt_ringbuffer_get(&(rx_fifo->rb), buffer, size); rt_hw_interrupt_enable(level); } } ``` 同样,我们再次通过一个场景以及相关的代码大致来说明DMA接收的整个流程 说明: IDLE: 检测到总线空闲中断 uart_isr() : 驱动层中断服务函数 dma_recv_isr() : 驱动层DMA接收中断处理函数 rt_hw_serial_isr() : 驱动框架层中断服务函数 rx_fifo: 接收缓冲区 1. 调rt_device_open(),rt_device_read() 2. DMA外设读取用户输入的一批字符 3. DMA将字符放入缓冲区后触发IDLE中断 4. 中断服务函数uart_isr()接收IDLE中断,调用dma_recv_isr()计算接收字符数量 5. dma_recv_isr()最后调用rt_hw_serial_isr(),根据缓冲区数据数量来决定是否唤醒完成量,即如果数据不够则不唤醒,继续接收数据,最后设置回调函数的参数 6. 重复2 - 5步, 直至接收完成,此时rt_device_read()中的完成量被唤醒,读取函数结束,返回接收字节数量 ``` static void uart_isr(struct rt_serial_device *serial) { #ifdef RT_SERIAL_USING_DMA //查看IDLE中断是否发生 if ((uart->uart_dma_flag)&&(__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_IDLE) != RESET)&& (__HAL_UART_GET_IT_SOURCE(&(uart->handle), UART_IT_IDLE) != RESET)) { //调用设备框架层中断处理函数 dma_recv_isr(serial, UART_RX_DMA_IT_IDLE_FLAG); __HAL_UART_CLEAR_IDLE_FLAG(&uart->handle); } #endif } static void dma_recv_isr(struct rt_serial_device *serial, rt_uint8_t isr_flag) { ... //省略计算接收数据量的过程 if (recv_len) { //将接收的数据量一同发往串口设备框架层 uart->dma_rx.remaining_cnt = counter; rt_hw_serial_isr(serial, RT_SERIAL_EVENT_RX_DMADONE | (recv_len << 8)); } ... } ``` ``` //串口设备框架层中断服务函数 void rt_hw_serial_isr(struct rt_serial_device * serial, int event) { //获取DMA接收的数据量 rx_length = (event & (~0xff)) >> 8; if (rx_length) { //关中断,避免并发bug level = rt_hw_interrupt_disable(); //更新环形缓冲区的索引 rt_serial_update_write_index(&(rx_fifo->rb),rx_length); //更新完成后再开中断 rt_hw_interrupt_enable(level); } if (serial->parent.open_flag & RT_SERIAL_RX_BLOCKING) { //如果缓冲区数据足够则唤醒完成量 if (rx_fifo->rx_cpt_index && rx_length >= rx_fifo->rx_cpt_index ) { rx_fifo->rx_cpt_index = 0; //唤醒完成量 rt_completion_done(&(rx_fifo->rx_cpt)); } } } ``` 总结,DMA接收与中断接收很多代码是复用的,其思路与中断接收及其相似,但在接收数据方面,DMA独立自行接收数据,不需要像中断接收一样,每次读取数据都占用CPU,从RDR寄存器拿到数据,再放入缓冲区中.在DMA模式下,中断处理程序接收到TC中断时,数据已然在接收缓冲区中.因此DMA接收的效率比中断更高. ### 阻塞模式下的数据发送 #### 数据发送函数分析 数据发送也遵循最开始提出的硬件工作模式选择,即缓冲区为0则使用轮询,开启DMA则优先使用DMA,否则使用中断. ``` static rt_ssize_t rt_serial_write(struct rt_device *dev, rt_off_t pos, const void *buffer, rt_size_t size) { ... if (serial->config.tx_bufsz == 0) { //发送缓冲区为0则,使用轮询 return _serial_poll_tx(dev, pos, buffer, size); } if (dev->open_flag & RT_SERIAL_TX_BLOCKING) { //如果tx缓冲区为空指针,则使用DMA发送 //因为DMA发送只需要应用层缓冲区,不需要框架层缓冲区 if ((tx_fifo->rb.buffer_ptr) == RT_NULL) { return _serial_fifo_tx_blocking_nbuf(dev, pos, buffer, size); } //需要用到框架层缓冲区,则使用中断发送 return _serial_fifo_tx_blocking_buf(dev, pos, buffer, size); } ... } ``` #### 轮询 ``` //框架层轮询发送函数 rt_ssize_t _serial_poll_tx(struct rt_device *dev, rt_off_t pos, const void *buffer, rt_size_t size) { ... //核心代码 while (size) { //调用驱动层函数,一次将一个字节发送出去,详解见下文 serial->ops->putc(serial, *putc_buffer); ++ putc_buffer; -- size; } return putc_size - size; } ``` ``` //驱动层函数 //serial->ops->putc实际为串口驱动中的stm32_putc函数: static int stm32_putc(struct rt_serial_device *serial, char c) { //重置传输完成中断标志(ISR寄存器中) UART_INSTANCE_CLEAR_FUNCTION(&(uart->handle), UART_FLAG_TC); //将一个字节的数据放入TDR寄存器中 UART_SET_TDR(&uart->handle, c); //陷入等待,直到获取到传输完成标志位(TC),此处体现了阻塞发送,即未发送完成则一直等待 while (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_TC) == RESET); return 1; } ``` #### 中断 ``` //框架层中断发送函数 static rt_ssize_t _serial_fifo_tx_blocking_buf( struct rt_device *dev, rt_off_t pos, const void *buffer, rt_size_t size) { ... //当激活标识为TRUE,代表tx_fifo即发送缓冲区正在被使用,因此直接返回0,不发送数据 if (tx_fifo->activated == RT_TRUE) return 0; //之前tx_fifo未被使用,则现在将其设置为TRUE,表示被当前发送线程中的中断发送占用 tx_fifo->activated = RT_TRUE; //阻塞发送的核心代码 while (size) { //将应用层中的数据先复制一份到发送缓冲区中 tx_fifo->put_size = rt_ringbuffer_put(&(tx_fifo->rb), (rt_uint8_t *)buffer + offset,size); //调用底层驱动的传输函数,发送数据 serial->ops->transmit(serial, (rt_uint8_t *)buffer + offset, tx_fifo->put_size, RT_SERIAL_TX_BLOCKING); //更新已经发送的数据量 offset += tx_fifo->put_size; size -= tx_fifo->put_size; //等待传输完成 rt_completion_wait(&(tx_fifo->tx_cpt), RT_WAITING_FOREVER); } return length; } ``` 关于上文的函数调用: 1. serial->ops->transmit()函数会调用驱动层的stm32_transmit() 2. stm_32transmit()又直接会调用驱动层的stm32_control()对中断进行初始化.,从而进行发送 我们直接看stm32_control(serial, RT_DEVICE_CTRL_SET_INT, (void * )tx_flag); ``` //驱动层中断发送函数 static rt_err_t stm32_control(struct rt_serial_device *serial, int cmd, void *arg) { ... case RT_DEVICE_CTRL_SET_INT: //设置单个中断抢占优先级和响应优先级 HAL_NVIC_SetPriority(uart->config->irq_type, 1, 0); //设置使能中断通道 HAL_NVIC_EnableIRQ(uart->config->irq_type); if (ctrl_arg == RT_DEVICE_FLAG_INT_TX) //开启TXE中断 __HAL_UART_ENABLE_IT(&(uart->handle), UART_IT_TXE); break; ... } ``` 此处,我们结合中断,来梳理中断发送的大致流程: 说明: TDR: TX Data Register 发送数据寄存器 TXE: TX Data Register Empty 发送数据寄存器为空 TC: Transmission Complete 发送完成 uart_isr() : 驱动层中断服务函数 rt_hw_serial_isr() : 驱动框架层中断服务函数 tx_fifo: 发送缓冲区 1. 假设有1024字节的数据通过串口发送,发送缓冲区设置为128; 2. 用户调用rt_device_open(uart_dev,RT_SERIAL_TX_BLOCKING)与rt_device_write(uart_dev,buffer,1024); 3. 1024字节中先有128个字节先被复制到缓冲区,调用驱动层stm32_transmit(),初始化中断相关函数,启动发送 4. 中断服务函数uart_isr()检测到TXE中断,将一个字节放入TDR中 (在TDR中的数据后续会被转移至移位寄存器,TDR因此又会空出来) 5. 中断服务函数uart_isr()会接收后续的TXE中断,从而将tx_fifo中的数据都被依次送往TDR寄存器 6. 当缓冲区中没有可发送的数据时,关闭TXE中断,开启TC中断 7. 如果检测到TC中断,则代表移位寄存器中所有的数据都被发送完,此时关闭TC中断,驱动层中断服务函数uart_isr()调用应用层中断服务函数rt_hw_serial_isr() 8. 在rt_hw_serial_isr()中,检测tx_fifo是否为空,为空唤醒完成量,rt_device_write()进行新一轮的组阻塞发送 9. 当所有1024个数据被分8次发送完成,rt_device_write()返回,否则重复3-8步 ``` static void uart_isr(struct rt_serial_device *serial) { //TXE标志位被设立,且检测到TXE中断 if ((__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_TXE) != RESET) &&(__HAL_UART_GET_IT_SOURCE(&(uart->handle), UART_IT_TXE)) != RESET) { ... //如果能够从tx_fifo中读到一个字节的数据,则将其发送到TDR中, if (rt_ringbuffer_getchar(&(tx_fifo->rb), &put_char)) { UART_SET_TDR(&uart->handle, put_char); } else { //如果没有数据可以发送,则关TXE中断,开TC中断 __HAL_UART_DISABLE_IT(&(uart->handle), UART_IT_TXE); __HAL_UART_ENABLE_IT(&(uart->handle), UART_IT_TC); } } ... //TC标志位被设立,且检测到TC中断 if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_TC) && (__HAL_UART_GET_IT_SOURCE(&(uart->handle), UART_IT_TC) != RESET)) { //传输完成,则关闭TC中断,调用驱动框架层中断服务函数rt_hw_serial_isr() __HAL_UART_DISABLE_IT(&(uart->handle), UART_IT_TC); rt_hw_serial_isr(serial, RT_SERIAL_EVENT_TX_DONE); } } ``` ``` void rt_hw_serial_isr(struct rt_serial_device *serial, int event) { case RT_SERIAL_EVENT_TX_DONE: { //获取tx_fifo中的数据长度 tx_length = rt_ringbuffer_data_len(&tx_fifo->rb); if (tx_length == 0) { tx_fifo->activated = RT_FALSE; //tx_fifo中的数据全部发送完,则唤醒完成量,进行下一次发送 if (serial->parent.tx_complete != RT_NULL) serial->parent.tx_complete(&serial->parent, RT_NULL); //设置回调函数 if (serial->parent.open_flag & RT_SERIAL_TX_BLOCKING) rt_completion_done(&(tx_fifo->tx_cpt)); break; } } } ``` 一个可能存在的问题: 因为中断发送应用层数据量可能会大于发送缓冲区大小,因此可能需要多次发送,例如上文发送1024个数据需要将数据分8次拷贝到缓存中,然而,在上述代码中我们可以发现,在一次发送完,tx_fifo的激活标识就被设置为flase了,这意味者,还在工作的tx_fifo被标注为不工作,那么在多线程环境下,这很可能会导致恶性的bug,针对此问题,笔者已经提了相关的pr(#7997). ``` //有问题的代码 if (tx_length == 0) { tx_fifo->activated = RT_FALSE; ... } ``` 总结,中断发送跟轮询发送明显的区别在于,中断发送是直接将数据放入RDR(RX Data Register),然后返回,而轮询发送,有用while死等TC中断这一过程.因此中断发送相较于轮询发送,减少了对于CPU的占用率. ``` //中断发送 UART_SET_TDR(&uart->handle, put_char); ``` ``` //轮询发送 UART_SET_TDR(&uart->handle, c); while (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_TC) == RESET); ``` #### DMA ``` //DMA发送 框架层函数 static rt_ssize_t _serial_fifo_tx_blocking_nbuf( struct rt_device *dev, rt_off_t pos, const void *buffer, rt_size_t size) { if (tx_fifo->activated == RT_TRUE) return 0; //将激活标识设置为TRUE tx_fifo->activated = RT_TRUE; //开启DMA发送 rst = serial->ops->transmit(serial, (rt_uint8_t *)buffer, size, RT_SERIAL_TX_BLOCKING); //等待完成量的唤醒 rt_completion_wait(&(tx_fifo->tx_cpt), RT_WAITING_FOREVER); return rst; } ``` ``` //DMA发送 驱动层函数 static rt_ssize_t stm32_transmit(struct rt_serial_device *serial, rt_uint8_t *buf, rt_size_t size, rt_uint32_t tx_flag) { if (uart->uart_dma_flag & RT_DEVICE_FLAG_DMA_TX) { //调用DMA传输 HAL_UART_Transmit_DMA(&uart->handle, buf, size); return size; } } ``` DMA的大致传输流程: 说明: TC: Transmission Complete 1. 假设有256字节的数据通过串口发送,发送缓冲区设置为256 2. 用户调用rt_device_open(uart_dev,RT_SERIAL_TX_BLOCKING)与rt_device_write(uart_dev,buffer,256); 3. 驱动层调用HAL_UART_Transmit_DMA(),开启DMA数据发送 4. 数据发送完成后,驱动层中断服务函数uart_isr()会接收TC中断,继而调用HAL_UART_TxCpltCallback函数中的rt_hw_serial_isr().在驱动框架层的中断服务函数中,会唤醒完成量,结束一次DMA发送 ``` //DMA发送 驱动层函数 static void uart_isr(struct rt_serial_device *serial) { ... //接收到TC中断 if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_TC) && (__HAL_UART_GET_IT_SOURCE(&(uart->handle), UART_IT_TC) != RESET)) { if (uart->uart_dma_flag & RT_DEVICE_FLAG_DMA_TX) { //HAL_UART_TxCpltCallback()将被调用 HAL_UART_IRQHandler(&(uart->handle)); } } } ``` ``` //DMA发送 驱动层函数 void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { //计算发送数量 level = rt_hw_interrupt_disable(); trans_total_index = __HAL_DMA_GET_COUNTER(&(uart->dma_tx.handle)); rt_hw_interrupt_enable(level); if (trans_total_index) return; //调用驱动层中断服务函数 rt_hw_serial_isr(serial, RT_SERIAL_EVENT_TX_DMADONE); } ``` ``` //DMA发送驱动框架层函数 void rt_hw_serial_isr(struct rt_serial_device *serial, int event) { case RT_SERIAL_EVENT_TX_DMADONE: { //将激活标志设置为FALSE,其他线程可以使用串口发送数据 tx_fifo->activated = RT_FALSE; //设置回调函数的实参 if (serial->parent.tx_complete != RT_NULL) serial->parent.tx_complete(&serial->parent, RT_NULL); if (serial->parent.open_flag & RT_SERIAL_TX_BLOCKING) { //唤醒完成量 rt_completion_done(&(tx_fifo->tx_cpt)); break; } } } ``` DMA发送只需要调用HAL_UART_Transmit_DMA(),开启DMA发送,然后等待TC中断,发送的具体过程都由DMA自行处理,不需要像中断一样,还需要进行持续接收TXE中断,将数据放入TDR寄存器,以及关闭TXE中断开启TC中断等一系列操作,因此进一步降低了CPU的占用率,节省系统资源. ## 写在最后 以上就是对于串口设备框架serial_v2的源码分析,鉴于篇幅,只分析了阻塞模式,非阻塞模式的整体框架与设置模式是相同的,有兴趣的读者可以结合这篇文章自行分析.因为其中的大部分内容都是笔者在大二暑假参加开源之夏时整理的,鉴于实际工作经验不足以及能力有限,很多描述与用词都不太准确,恳请各位前辈批评指正. 最后的最后,非常感谢RT-thread社区能够向广大学生提供参与开源的机会,非常感谢导师与社区负责人的倾心指导,希望日后能继续为社区做贡献. 衷心祝愿RT-Thread社区能不断发展壮大,在国产嵌入式操作系统之路上砥砺前行!
1
条评论
默认排序
按发布时间排序
登录
注册新账号
关于作者
ZOCO_9333
这家伙很懒,什么也没写!
文章
1
回答
0
被采纳
0
关注TA
发私信
相关文章
1
串口DMA发送数据时,数据被覆盖
2
关于串口DMA模式下rt_device_close问题
3
利用stm32f427实现usb转串口,电脑端什么也没有识别到
4
finsh 控制台 适配 RS 485请大神指点????
5
uart_sample.c 中,读串口设备时偏移量pos要设置为-1而不是0?
6
【结贴】at_device软件包中对串口接收数据缺少判断导致数据接收异常
7
串口无法接受数据,但可以发送
8
串口如何有效的清除掉接收缓冲,而不必一个一个的去读取
9
串口接收使用方式问题
10
雅特力FINSH问题
推荐文章
1
RT-Thread应用项目汇总
2
玩转RT-Thread系列教程
3
国产MCU移植系列教程汇总,欢迎查看!
4
机器人操作系统 (ROS2) 和 RT-Thread 通信
5
五分钟玩转RT-Thread新社区
6
【技术三千问】之《玩转ART-Pi》,看这篇就够了!干货汇总
7
关于STM32H7开发板上使用SDIO接口驱动SD卡挂载文件系统的问题总结
8
STM32的“GPU”——DMA2D实例详解
9
RT-Thread隐藏的宝藏之completion
10
【ART-PI】RT-Thread 开启RTC 与 Alarm组件
热门标签
RT-Thread Studio
串口
Env
LWIP
SPI
AT
Bootloader
Hardfault
CAN总线
FinSH
ART-Pi
USB
DMA
文件系统
RT-Thread
SCons
RT-Thread Nano
线程
MQTT
STM32
RTC
FAL
rt-smart
ESP8266
I2C_IIC
UART
WIZnet_W5500
ota在线升级
PWM
cubemx
flash
freemodbus
BSP
packages_软件包
潘多拉开发板_Pandora
定时器
ADC
GD32
flashDB
socket
中断
编译报错
Debug
rt_mq_消息队列_msg_queue
SFUD
msh
keil_MDK
ulog
MicroPython
C++_cpp
本月问答贡献
出出啊
1517
个答案
342
次被采纳
小小李sunny
1444
个答案
290
次被采纳
张世争
812
个答案
177
次被采纳
crystal266
547
个答案
161
次被采纳
whj467467222
1222
个答案
148
次被采纳
本月文章贡献
出出啊
1
篇文章
2
次点赞
小小李sunny
1
篇文章
1
次点赞
张世争
1
篇文章
2
次点赞
crystal266
2
篇文章
2
次点赞
whj467467222
2
篇文章
2
次点赞
回到
顶部
发布
问题
投诉
建议
回到
底部