RT-Thread串口发送篇详解

系列文章目录

rt-thread 之 fal移植
rt-thread 之 生成工程模板
STM32——串口理论篇
rt-thread——串口V1版本(一)配置
rt-thread——串口V1版本(三)接收篇

文章目录

  • 系列文章目录
  • 前言
  • 轮询发送
  • 中断发送
  • DMA发送

  • 前言

    这篇文章主要讲一下rt-thread的串口的三种发送,轮询、中断、DMA发送。rt-thread提供的串口驱动V1版本中断发送有问题,这边只提供中断的发送思路,或者大家可以去研究一下V2版本。后续会有V2版本的文章更新。首先需要提出两个概念阻塞非阻塞,阻塞顾名思义程序会在这里等待到阻塞结束。非阻塞则会立马执行结束跑到后面的程序。


    下面是发送程序对应用层的接口:

    rt_size_t rt_device_write(rt_device_t dev,
                              rt_off_t    pos,
                              const void *buffer,
                              rt_size_t   size)
    

    里面本质上会调用rt_serial_write()函数往串口设备写数据,里面又细分了三个函数是这篇文章的主角:_serial_int_tx()(中断发送) _serial_dma_tx()(dma发送)_serial_poll_tx()轮询发送。

    轮询发送

    轮询发送是最简单的发送模式,rt-thread提供的是阻塞的发送函数。程序会将需要发送的数据一字节一字节的写到串口的DR寄存器,然后查询发送完成标志,直到数据完全发送结束。优点:程序简单易懂,缺点:浪费cpu资源,容易被高优先级任务打断,一帧数据发送断断续续的。
    _serial_poll_tx()的具体实现(省略流处理代码):

    rt_inline int _serial_poll_tx(struct rt_serial_device *serial, const rt_uint8_t *data, int length)
    {
        int size;
        RT_ASSERT(serial != RT_NULL);
        size = length;
        while (length)
        {
            serial->ops->putc(serial, *data);
            ++ data;
            -- length;
        }
        return size - length;
    }
    

    很简单就是每次发送1字节数据循环length长度。再看看putc函数的实现,我这边代码是stm32的所以是以下这个函数:

    static int stm32_putc(struct rt_serial_device *serial, char c)
    {
        struct stm32_uart *uart;
        RT_ASSERT(serial != RT_NULL);
    
        uart = rt_container_of(serial, struct stm32_uart, serial);
        UART_INSTANCE_CLEAR_FUNCTION(&(uart->handle), UART_FLAG_TC);
    #if defined(SOC_SERIES_STM32L4) || defined(SOC_SERIES_STM32WL) || defined(SOC_SERIES_STM32F7) || defined(SOC_SERIES_STM32F0) \
        || defined(SOC_SERIES_STM32L0) || defined(SOC_SERIES_STM32G0) || defined(SOC_SERIES_STM32H7) || defined(SOC_SERIES_STM32L5) \
        || defined(SOC_SERIES_STM32G4) || defined(SOC_SERIES_STM32MP1) || defined(SOC_SERIES_STM32WB) || defined(SOC_SERIES_STM32F3)  \
        || defined(SOC_SERIES_STM32U5)
        uart->handle.Instance->TDR = c;
    #else
        uart->handle.Instance->DR = c;
    #endif
        while (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_TC) == RESET);
        return 1;
    }
    

    代码很简单,根据单片机型号将数据写入DR或者TDR寄存器中,然后等待发送完成标志。按照这种方式,代码需要在这阻塞到一帧数据完全发送结束才会执行后面的代码。再此期间代码会被高优先级任务和中断打断。像modbus这种没有没有帧长度协议靠字符间间隔区分帧结束的情况不是很推荐这种写法。当然这种写法配合RS485拉低拉高使能引脚就特别简单。

    中断发送

    中断发送函数:

    rt_inline int _serial_int_tx(struct rt_serial_device *serial, const rt_uint8_t *data, int length)
    {
        int size;
        struct rt_serial_tx_fifo *tx;
        RT_ASSERT(serial != RT_NULL);
        size = length;
        tx = (struct rt_serial_tx_fifo*) serial->serial_tx;
        RT_ASSERT(tx != RT_NULL);
        while (length)
        {
            if (serial->ops->putc(serial, *(char*)data) == -1)
            {
                rt_completion_wait(&(tx->completion), RT_WAITING_FOREVER);
                continue;
            }
            data ++; length --;
        }
        return size - length;
    }
    

    与轮询发送区别是多了一个等待完成信号量的内容。bug是里面字符发送函数与轮询发送函数一样,永远返回1,所以等待完成函数不会被触发。rt-thread bspV1版本这边是有问题的,感兴趣的同学可以看下V2版本的串口驱动函数。
    一般中断思路是发送第一字节,然后进入中断函数,在中断函数中将剩余字节发送完成。优点不会出现轮询发送中的缺点期间代码会被高优先级任务和低优先级中断打断,可用于modbus这类无长度的协议。这种模式也存在缺点,发送期间全程会平凡进入中断,若波特率特别高则使用DMA发送更加合理。

    DMA发送

    dma发送函数:

    rt_inline int _serial_dma_tx(struct rt_serial_device *serial, const rt_uint8_t *data, int length)
    {
        rt_base_t level;
        rt_err_t result;
        struct rt_serial_tx_dma *tx_dma;
        tx_dma = (struct rt_serial_tx_dma*)(serial->serial_tx);
        result = rt_data_queue_push(&(tx_dma->data_queue), data, length, RT_WAITING_FOREVER);
        if (result == RT_EOK)
        {
            level = rt_hw_interrupt_disable();
            if (tx_dma->activated != RT_TRUE)
            {
                tx_dma->activated = RT_TRUE;
                rt_hw_interrupt_enable(level);
                /* make a DMA transfer */
                serial->ops->dma_transmit(serial, (rt_uint8_t *)data, length, RT_SERIAL_DMA_TX);
            }
            else
            {
                rt_hw_interrupt_enable(level);
            }
            return length;
        }
        else
        {
            rt_set_errno(result);
            return 0;
        }
    }
    

    其中比较重要的是以下两个函数:
    rt_data_queue_push()
    将数据放入dma数据队列中,这里需要特别注意一下直接是应用层写的数据,没有发送缓存区,若是局部变量,在dma发送是可能已经释放掉,无法发送正确数据。其次在发送过程中修改应用层数据,dma会直接发送修改后的数据,这点也需要特别注意。
    serial->ops->dma_transmit(serial, (rt_uint8_t *)data, length, RT_SERIAL_DMA_TX);
    在函数指针背后调用的是对hal库分装一层的dma传输函数,其代码如下(有删减):

    static rt_size_t stm32_dma_transmit(struct rt_serial_device *serial, rt_uint8_t *buf, rt_size_t size, int direction)
    {
        if (RT_SERIAL_DMA_TX == direction)
        {
            if (HAL_UART_Transmit_DMA(&uart->handle, buf, size) == HAL_OK)
            {
                return size;
            }
            else
            {
                return 0;
            }
        }
        return 0;
    }
    

    特别注意HAL_UART_Transmit_DMA()是个无阻塞函数,也就是执行完这一行时,数据开始发送,但是不会等到数据完全发送结束。若rs485的串口从轮询发送切换成dma发送需要特别注意这一点,否则只能发送1-2字节。那么如何优雅的控制485的使能信号呢?在dma中断中有个回调函数 hdma->XferCpltCallback(hdma);DMA中断代码如下(有删减):

    void HAL_DMA_IRQHandler(DMA_HandleTypeDef *hdma)
    {
      /* Transfer Complete Interrupt management ***********************************/
      else if (((flag_it & (DMA_FLAG_TC1 << hdma->ChannelIndex)) != RESET) && ((source_it & DMA_IT_TC) != RESET))
      {
        if((hdma->Instance->CCR & DMA_CCR_CIRC) == 0U)
        {
          /* Disable the transfer complete and error interrupt */
          __HAL_DMA_DISABLE_IT(hdma, DMA_IT_TE | DMA_IT_TC);  
          /* Change the DMA state */
          hdma->State = HAL_DMA_STATE_READY;
        }
        /* Clear the transfer complete flag */
          __HAL_DMA_CLEAR_FLAG(hdma, __HAL_DMA_GET_TC_FLAG_INDEX(hdma));
        /* Process Unlocked */
        __HAL_UNLOCK(hdma);
    
        if(hdma->XferCpltCallback != NULL)
        {
          /* Transfer complete callback */
          hdma->XferCpltCallback(hdma);
        }
      }
    
    }
    

    hdma->XferCpltCallback(hdma);回调函数本质上会调用这个函数static void UART_DMATransmitCplt(DMA_HandleTypeDef *hdma),其中
    /* Enable the UART Transmit Complete Interrupt */
    SET_BIT(huart->Instance->CR1, USART_CR1_TCIE);
    会通过软件触发串口的完成中断。

    static void UART_DMATransmitCplt(DMA_HandleTypeDef *hdma)
    {
      UART_HandleTypeDef *huart = (UART_HandleTypeDef *)((DMA_HandleTypeDef *)hdma)->Parent;
      /* DMA Normal mode*/
      if ((hdma->Instance->CCR & DMA_CCR_CIRC) == 0U)
      {
        huart->TxXferCount = 0x00U;
        /* Disable the DMA transfer for transmit request by setting the DMAT bit
           in the UART CR3 register */
        CLEAR_BIT(huart->Instance->CR3, USART_CR3_DMAT);
    
        /* Enable the UART Transmit Complete Interrupt */
        SET_BIT(huart->Instance->CR1, USART_CR1_TCIE);
    
      }
    }
    

    发送完成去拉485使能引脚可以在串口中断中的回调函数中实现,也可以在回调函数中通过信号量的释放完成阻塞的DMA发送。但是阻塞指的是任务阻塞,cpu资源会被该任务释放,执行其他任务。

    物联沃分享整理
    物联沃-IOTWORD物联网 » RT-Thread串口发送篇详解

    发表评论