【RTT-Studio】详细使用教程十二:UART的分析和使用

文章目录

    • 一、简介
      • 1.串口发送模式
      • 2.串口接收模式
    • 二、串口配置
    • 三、串口发送
    • 四、串口接收

一、简介

  本文主要阐述STM32串口的几种工作中使用的工作模式和编程思路。串口通常情况下使用的是:1个起始位,8个数据位,无奇偶校验,1位停止位,每传输1个字节的数据相当于传输10bit,在波特率:9600的情况下,1字节需要1.04ms,如果传输波特率:115200传输1字节数据需要0.087ms。

1.串口发送模式

串口发送方式主要分为3种:轮询、中断、DMA发送。下面简述一下各个模式优缺点:

模式 优点 缺点
轮询 代码最简单 会导致代码阻塞,会被任何中断,在带os的系统中还容易被高优先级任务打断
中断 将发送优先级提高到该串口中断级别,不会被任务打断,只会被更高优先级中断打断 占用cpu和中断
DMA 不需要cpu参与数据发送,发送也不会被打断 代码复杂

(1)轮询发送
  轮询发送是将需要发送的数据写到串口的数据(DR)寄存器中,然后通过循环等待发送完成TC标致位置位判断数据是否发送结束。一般串口在115200波特率下发送1字节数据时间大概0.087ms,在没有os的情况下,程序需要阻塞89ms,只会被中断打断。在有os的情况下还需要考虑到任务切换和高优先级任务的使用,串口发送的一包数据就会是断断续续的,时间更长。

(2)中断发送
  中断发送是对轮询发送的一种改进,其目的是连续发送一包数据,不被低优先级中断和任务打断。其思路是开启串口发送中断,在应用层调用驱动层发送函数时,将中断发送标志位置1/或直接发送第一字节,触发串口中断,在中断中发送剩余数据。期间每次传输完成时进一次中断,其余时间cpu可以运行别的任务,而且减少了查询发送完成标志位的时间。对于没有os的系统来说做到了时间片处理非阻塞发送。

(3)DMA发送
  该模式需要根据手册提前配置DMA通道模式等信息,通过HAL库提供的发送函数,DMA会自动将内存中的参数搬运到串口外设中,CPU执行完发送命令就可以执行别的任务了。在dma发送结束时会产生DMA中断告诉用户数据是否发送结束。

2.串口接收模式

串口接收也有轮询模式,主要是中断和DMA的方式。下面简述一下各个模式优缺点:

模式 优点 缺点
中断 代码简单 需要多次进入中断
DMA 不需要cpu参与每字节接收,仅产生少量的中断 需要配合空闲中断处理

(1)中断接收
  中断接收是一种常见的接收方式,适合波特率较低(9600)或任务不复杂的情况下使用。9600传输数据时,使用串口中断接收数据大约1.04ms进一次中断,在要求不高的情况下使用中断接收也能满足设计要求,尤其对裸机基于时间片的代码框架来说影响不大,代码也简洁。但是随着波特率的提高此方法则不太适用,当波特率很高(115200以上),若使用串口中断接收意味着每87us会进入一次中断,那么对主程序的时间片影响将会很大,故推荐使用DMA。

(2)DMA接收+空闲中断接收
  需要提前配置DMA通道,DMA模式,使用DMA搬运数据,串口空闲中断判断驱动层的帧结束。当空闲中断产生时将DMA映射的数据拷贝到应用层的接收缓存区。与中断接收区别是不用每个字节进入一次中断,拷贝数据也是将一帧数据拷贝到缓存区。发送1K数据则会在89ms内进1000次中断,若再提高波特率,对裸机时间片的系统来说影响就很大了。


二、串口配置

  UART(Universal Asynchronous Receiver/Transmitter)通用异步收发传输器,UART 作为异步串口通信协议的一种,工作原理是将传输数据的每个字符一位接一位地传输,是在应用程序开发过程中使用频率最高的数据总线。

RT-Thread串口配置步骤:
在这里插入图片描述
(1)通过串口名字找到串口句柄

#define SAMPLE_UART_NAME       "uart2"

static rt_device_t serial;
serial = rt_device_find(SAMPLE_UART_NAME);

(2)配置串口参数
使用rt_device_control()函数修改串口配置参数

/* 初始化配置参数 */
struct serial_configure uart2_config = RT_SERIAL_CONFIG_DEFAULT;  

/* step2:修改串口配置参数 */
uart2_config.baud_rate = BAUD_RATE_115200;      //修改波特率为 9600
uart2_config.data_bits = DATA_BITS_8;           //数据位 8
uart2_config.stop_bits = STOP_BITS_1;           //停止位 1
uart2_config.bufsz     = 128;                   //修改缓冲区 buff size 为 128
uart2_config.parity    = PARITY_NONE;           //无奇偶校验位

rt_device_control(serial, RT_DEVICE_CTRL_CONFIG, &uart2_config);

(3)设置串口接收回调函数
当发生中断(接收中断或者空闲中断)时,会调用注册的回调函数。用户可以在回调函数中写一些无阻塞的功能。虽然回调函数是在中断中执行,所以不能有阻塞和延时。

/* 接收数据回调函数 */
static rt_err_t uart_input(rt_device_t dev, rt_size_t size)
{
    /* 串口接收到数据后产生中断,调用此回调函数,然后发送接收信号量 */
    rt_sem_release(&rx_sem);
	cnt++;
    return RT_EOK;
}

/* 设置接收回调函数 */
rt_device_set_rx_indicate(serial, uart_input);

(4)打开串口设备
打开设备时可以对串口的模式进行配置,rt-thread提供三种模式:中断模式、轮询模式、DMA 模式。发送和接收只能从中选一个,发送和接收可以配置的不一致。默认使用中断接收和轮询发送。

/* 以中断接收及轮询发送模式打开串口设备 */
rt_err_t rt_device_open(rt_device_t dev, rt_uint16_t oflag)

oflag支持模式参数为下列几个:

#define RT_DEVICE_FLAG_INT_RX           0x100           /**< INT mode on Rx 中断接收 */
#define RT_DEVICE_FLAG_DMA_RX           0x200           /**< DMA mode on Rx DMA接收 */
#define RT_DEVICE_FLAG_INT_TX           0x400           /**< INT mode on Tx 中断发送 */
#define RT_DEVICE_FLAG_DMA_TX           0x800           /**< DMA mode on Tx DMA接收 */

rt_device_open函数中初始化DMA模式会进行参数检查,需要确认使用串口DMA宏被定义。如:BSP_UART2_RX_USING_DMA,否则会rt_serial_open()返回错误。当对应串口DMA宏在rtconfig.h中被定义后,通过宏定义设置到对应串口对象的uart_dma_flag参数中,在串口初始化阶段通过rt_hw_usart_init()中的rt_hw_serial_register()函数会把对应uart_dma_flag设置到串口dev->flag中,后续串口open时只是检查一下参数。

完整的串口配置例程代码如下:

/*
 * 程序清单:这是一个 串口 设备使用例程
 * 例程导出了 uart_sample 命令到控制终端
 * 命令调用格式:uart_sample uart2
 * 命令解释:命令第二个参数是要使用的串口设备名称,为空则使用默认的串口设备
 * 程序功能:通过串口输出字符串"hello RT-Thread!",然后错位输出输入的字符
*/
#include 
#include "module_uart.h"
#include "drv_usart.h"

#define SAMPLE_UART_NAME       "uart2"

/* 初始化配置参数 */
struct serial_configure uart2_config = RT_SERIAL_CONFIG_DEFAULT; 

/* 用于接收消息的信号量 */
static struct rt_semaphore rx_sem;
static rt_device_t serial;
uint16_t cnt = 0;

/* 接收数据回调函数 */
static rt_err_t uart_input(rt_device_t dev, rt_size_t size)
{
    /* 串口接收到数据后产生中断,调用此回调函数,然后发送接收信号量 */
    rt_sem_release(&rx_sem);
	cnt++;
    return RT_EOK;
}

static void serial_thread_entry(void *parameter)
{
    char ch;

    while (1)
    {
        /* 从串口读取一个字节的数据,没有读取到则等待接收信号量 */
        while (rt_device_read(serial, -1, &ch, 1) != 1)
        {
            /* 阻塞等待接收信号量,等到信号量后再次读取数据 */
            rt_sem_take(&rx_sem, RT_WAITING_FOREVER);
        }
		 /* 读取到的数据通过串口输出 */
        rt_device_write(serial, 0, &ch, 1);
    }
}

int uart2_init(void)
{
	rt_err_t ret;
    /* 查找系统中的串口设备 */
    serial = rt_device_find(SAMPLE_UART_NAME);
    if (!serial)
    {
        rt_kprintf("find %s failed!\n", SAMPLE_UART_NAME);
        return RT_ERROR;
    }
    
	/* step2:修改串口配置参数 */
	uart2_config.baud_rate = BAUD_RATE_115200;        //修改波特率为 9600
	uart2_config.data_bits = DATA_BITS_8;           //数据位 8
	uart2_config.stop_bits = STOP_BITS_1;           //停止位 1
	uart2_config.bufsz     = 128;                   //修改缓冲区 buff size 为 128
	uart2_config.parity    = PARITY_NONE;           //无奇偶校验位
	rt_device_control(serial, RT_DEVICE_CTRL_CONFIG, &uart2_config);
	
    /* 初始化信号量 */
    rt_sem_init(&rx_sem, "rx_sem", 0, RT_IPC_FLAG_FIFO);
    /* 以中断接收及轮询发送模式打开串口设备 */
    rt_device_open(serial, RT_DEVICE_FLAG_INT_RX);
    /* 设置接收回调函数 */
    rt_device_set_rx_indicate(serial, uart_input);

    /* 创建 serial 线程 */
    rt_thread_t thread = rt_thread_create("serial", serial_thread_entry, RT_NULL, 1024, 25, 10);
    /* 创建成功则启动线程 */
    if (thread != RT_NULL)
    {
        rt_thread_startup(thread);
    }
    else
    {
        ret = RT_ERROR;
    }

    return ret;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(uart_sample, uart device sample);

三、串口发送

  串口的三种发送:轮询、中断、DMA发送。下面是发送程序对应用层的接口:

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()轮询发送。

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

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

putc函数的实现:根据单片机型号将数据写入DR或者TDR寄存器中,然后等待发送完成标志。按照这种方式,代码需要在这阻塞到一帧数据完全发送结束才会执行后面的代码。再此期间代码会被高优先级任务和中断打断。

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
    if(uart->config->is485){
        HAL_GPIO_WritePin(uart->config->de_port,uart->config->de_pin, GPIO_PIN_RESET);
    }
    uart->handle.Instance->DR = c;
#endif
    while (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_TC) == RESET);
    if(uart->config->is485){
            HAL_GPIO_WritePin(uart->config->de_port,uart->config->de_pin, GPIO_PIN_SET);
        }
    return 1;
}

2.中断发送
与轮询发送区别是多了一个等待完成信号量的内容。bug是里面字符发送函数与轮询发送函数一样,永远返回1,所以等待完成函数不会被触发。

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

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

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

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

四、串口接收

  串口接收常见的分中断接收和DMA接收。在RTT串口配置中设置了一个ringbuf,其作用是用来接收数据的。无论使用中断接收还是DMA接收数据都会存入ringbuf中。

1.中断接收
在RTT中,中断接收的流程如下:
【RTT-Studio】详细使用教程十二:UART的分析和使用_第1张图片
中断接收无论如何都会触发单片机中断向量表中的中断函数,只需要去查找中断函数中具体实现即可。如果是接收中断则进入rt_hw_serial_isr函数否则则去清除一些错误标志位。

void USART2_IRQHandler(void)
{
    /* enter interrupt */
    rt_interrupt_enter();
    uart_isr(&(uart_obj[UART2_INDEX].serial));
    /* leave interrupt */
    rt_interrupt_leave();
}

static void uart_isr(struct rt_serial_device *serial)
{
	...
	/* UART in mode Receiver -------------------------------------------------*/
	if ((__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_RXNE) != RESET) &&
	        (__HAL_UART_GET_IT_SOURCE(&(uart->handle), UART_IT_RXNE) != RESET))
	{
	    rt_hw_serial_isr(serial, RT_SERIAL_EVENT_RX_IND);
	}
...
    else
    {
        if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_ORE) != RESET)
        {
            __HAL_UART_CLEAR_OREFLAG(&uart->handle);
        }
        if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_NE) != RESET)
        {
            __HAL_UART_CLEAR_NEFLAG(&uart->handle);
        }
        if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_FE) != RESET)
        {
            __HAL_UART_CLEAR_FEFLAG(&uart->handle);
        }
        if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_PE) != RESET)
        {
            __HAL_UART_CLEAR_PEFLAG(&uart->handle);
        }
    }
}

rt_hw_serial_isr()可以分成两部分读取数据和回调函数调用部分。读取数据部分是负责把串口接收数据读取到ringbuf中。

while (1)
{
    ch = serial->ops->getc(serial);
    if (ch == -1) break;
    /* disable interrupt */
    level = rt_hw_interrupt_disable();

    rx_fifo->buffer[rx_fifo->put_index] = ch;
    rx_fifo->put_index += 1;
    if (rx_fifo->put_index >= serial->config.bufsz) rx_fifo->put_index = 0;

    /* if the next position is read index, discard this 'read char' */
    if (rx_fifo->put_index == rx_fifo->get_index)
    {
        rx_fifo->get_index += 1;
        rx_fifo->is_full = RT_TRUE;
        if (rx_fifo->get_index >= serial->config.bufsz) rx_fifo->get_index = 0;

        _serial_check_buffer_size();
    }
    /* enable interrupt */
    rt_hw_interrupt_enable(level);
}

上述使用serial->ops->getc(serial)函数,看这个名字就能猜到是获取串口接收的一个字节数据。对于STM32来说就是将串口的DR寄存器通过这个函数返回。将DR寄存器中的值存入ringbuf,还对ringbuf满的情况做了一下异常处理,ringbuf is_full标志位会置一,数据会覆盖最早的数据导致数据丢失,所以需要保证应用层及时从ringbuf中取出数据。

static int stm32_getc(struct rt_serial_device *serial)
{
    int ch;
    struct stm32_uart *uart;
    RT_ASSERT(serial != RT_NULL);
    uart = rt_container_of(serial, struct stm32_uart, serial);
    ch = -1;
    if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_RXNE) != RESET)
    {
    ...
        ch = uart->handle.Instance->DR & stm32_uart_get_mask(uart->handle.Init.WordLength, uart->handle.Init.Parity);
	...
    }
    return ch;
}

再看看回调函数部分,若回调函数不为空,且这次就收到数据则调用一次接收回调函数。通常回调函数中释放一个信号量通知应用层程序从ringbuf中读取数据。其实这也看出了中断方式的一个缺陷,每接收一字节放一次ringbuf,调用一次回调函数,应用层也只能一字节一字节接收。为了减CPU的使用率还提供了DMA的方式。

/* 调用回调函数部分 */
/* invoke callback */
if (serial->parent.rx_indicate != RT_NULL)
{
    rt_size_t rx_length;

    /* get rx length */
    level = rt_hw_interrupt_disable();
    rx_length = (rx_fifo->put_index >= rx_fifo->get_index)? (rx_fifo->put_index - rx_fifo->get_index):
        (serial->config.bufsz - (rx_fifo->get_index - rx_fifo->put_index));
    rt_hw_interrupt_enable(level);
    if (rx_length)
    {
        serial->parent.rx_indicate(&serial->parent, rx_length);
    }
}
break;

2.DMA接收
【RTT-Studio】详细使用教程十二:UART的分析和使用_第2张图片
这里触发串口中断的判断变了,变成了DMA半满、满中断和空闲中断触发串口中断,三者是或的关系。接收中断中如果开启DMA则会执行以下代码:

  else 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))
    {
        level = rt_hw_interrupt_disable();
        recv_total_index = serial->config.bufsz - __HAL_DMA_GET_COUNTER(&(uart->dma_rx.handle));
        recv_len = recv_total_index - uart->dma_rx.last_index;
        uart->dma_rx.last_index = recv_total_index;
        rt_hw_interrupt_enable(level);

        if (recv_len)
        {
            rt_hw_serial_isr(serial, RT_SERIAL_EVENT_RX_DMADONE | (recv_len << 8));
        }
        __HAL_UART_CLEAR_IDLEFLAG(&uart->handle);
    }
    else if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_TC) &&
            (__HAL_UART_GET_IT_SOURCE(&(uart->handle), UART_IT_TC) != RESET))
    {
        if ((serial->parent.open_flag & RT_DEVICE_FLAG_DMA_TX) != 0)
        {
            HAL_UART_IRQHandler(&(uart->handle));
        }
        UART_INSTANCE_CLEAR_FUNCTION(&(uart->handle), UART_FLAG_TC);
    }

DMA在处理的过程中也是调用rt_hw_serial_isr与中断一致,接收长度需要根据DMA接收长度增加个小计算得出。后续处理与中断接收也基本一致,将数据更新进ringbuf再调用回调函数。最大的区别是DMA一次会接收一帧或者半帧数据才进一次中断。回调函数处理也会有所不一样,官方例程也不在释放信号量了,通过消息队列传出串口接收数据大小,交给应用层处理。


你可能感兴趣的:(RTT-Studio,单片机,stm32,嵌入式硬件)