RTT小师弟 · 2021年07月23日

RTT串口V1版本的使用分析及问题排查指南(三)

本文由RT-Thread论坛用户123原创发布,原文:https://club.rt-thread.org/as...

应用层的串口问题

应用层使用串口时,需要先指定以何种方式(轮询、中断或者DMA)去打开串口,然后再进行串口的操作。这部分相关的内容在第一章已经说过,这里主要用来总结串口使用时遇到的一些问题。

串口注册时候的默认规则慢慢道来:

串口注册的默认规则

我们都知道,串口设备需要先注册,才能通过rt_device_open API 去访问,那么串口注册时候的默认规则是什么呢?看串口驱动的注册代码如下所示:

int rt_hw_usart_init(void)
{
    rt_size_t obj_num = sizeof(uart_obj) / sizeof(struct stm32_uart);
    struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT;
    rt_err_t result = 0;

    stm32_uart_get_dma_config();                                     /* (1)*/

    for (int i = 0; i < obj_num; i++)
    {
        /* init UART object */
        uart_obj[i].config = &uart_config[i];
        uart_obj[i].serial.ops    = &stm32_uart_ops;
        uart_obj[i].serial.config = config;                          /* (2) */

        /* register UART device */
        result = rt_hw_serial_register(&uart_obj[i].serial, uart_obj[i].config->name,
                                       RT_DEVICE_FLAG_RDWR
                                       | RT_DEVICE_FLAG_INT_RX
                                       | RT_DEVICE_FLAG_INT_TX
                                       | uart_obj[i].uart_dma_flag
                                       , NULL);                     /* (3) */
        RT_ASSERT(result == RT_EOK);
    }

    return result;
}

第(1)点:获取串口对应的dma配置,当配置串口支持dma发送或者dma接收时,在这个函数里面将会记录串口的模式配置,在后边初始化串口设备的时候,再对串口进行DMA配置。

第(2)点:配置默认的参数信息,例如波特率,停止位,奇偶校验等,这里选择的是统一配置默认的参数RT_SERIAL_CONFIG_DEFAULT,该参数在serial.h文件中定义,默认配置如下,在这里,其中有一个参数RT_SERIAL_RB_BUFSZ,这个参数的意思是设置串口的接收缓冲区大小,它的默认值是64字节,需要注意的是,串口参数里面没有发送缓冲区的设置

/* Default config for serial_configure structure */
#define RT_SERIAL_CONFIG_DEFAULT           \
{                                          \
    BAUD_RATE_115200, /* 115200 bits/s */  \
    DATA_BITS_8,      /* 8 databits */     \
    STOP_BITS_1,      /* 1 stopbit */      \
    PARITY_NONE,      /* No parity  */     \
    BIT_ORDER_LSB,    /* LSB first sent */ \
    NRZ_NORMAL,       /* Normal mode */    \
    RT_SERIAL_RB_BUFSZ, /* Buffer size */  \
    0                                      \
}

第(3)点:注册串口设备,这里主要关注的是FLAG标志,可以看到串口注册的时候,默认加上了中断发送和中断接收的支持,又由于串口隐性支持轮询发送和轮询接收,因此只需要配置DMA相关即可,也就是uart_obj[i].uart_dma_flag

总结

串口注册时候的默认规则:

1. 串口参数配置按照RT_SERIAL_CONFIG_DEFAULT ,其中串口接收缓冲区大小由RT_SERIAL_RB_BUFSZ决定,默认大小为64字节,串口没有发送缓冲区大小的设置。

2. 串口默认支持中断和轮询的收发模式。

串口打开的默认规则:

串口打开的时候使用rt_device_open(),其中参数oflags支持下列取值 (可以采用或的方式支持多种取值):

#define RT_DEVICE_FLAG_STREAM       0x040     /* 流模式      */
/* 接收模式参数 */
#define RT_DEVICE_FLAG_INT_RX       0x100     /* 中断接收模式 */
#define RT_DEVICE_FLAG_DMA_RX       0x200     /* DMA 接收模式 */
/* 发送模式参数 */
#define RT_DEVICE_FLAG_INT_TX       0x400     /* 中断发送模式 */
#define RT_DEVICE_FLAG_DMA_TX       0x800     /* DMA 发送模式 */

串口数据接收和发送数据的模式分为 3 种:中断模式、轮询模式、DMA 模式。在使用的时候,这 3 种模式只能若串口的打开参数 oflags 没有指定使用中断模式或者 DMA 模式,则默认使用轮询模式。

总结

串口打开的默认规则:

若串口的打开参数 oflags 没有指定使用中断模式或者 DMA 模式,则默认使用轮询模式。

串口读写的默认规则:

先说串口的读数据

我们一般会选择串口中断接收或者是串口DMA接收(好像没有轮询死等串口数据的场景吧,这里不再讨论这个轮询读的方式),这里比较统一,无论是哪种方式,都是非阻塞的接收模式。即,我们可能会通过一个信号量或者消息队列,当接收到数据的时候,就唤醒当前线程进行数据的读取。这里我们参照串口的文档中心的例子:中断接收DMA接收,这两个demo,一个是使用信号量,一个是使用消息队列,来完成串口数据的读取的。

因此可以统一地说,串口的读数据接口,是按照非阻塞的方式进行接收的,当读取不到数据的时候,会将当前线程挂起,以免浪费CPU资源。

再说串口的写数据

串口的写数据会选择三种模式,轮询、中断、DMA。

串口的的轮询写数据这个模式,是我们用的比较多的一个场景,比如上一章节说的FinSH的输出,或者说是rt_kprintf的输出。因此显而易见的,轮询模式的写数据接口是阻塞的方式进行的。

重点来了:串口的中断模式,这个模式其实是有一些问题的,我们理想中的中断发送,其实是先打开发送的中断使能,然后在中断服务函数内把数据发送出去,等数据发送空中断后再发送下一个字节的数据,依次单字节循环发送即将数据发送完成。串口V1的中断发送是怎么实现的呢?我们来看一下代码:

rt_inline int _serial_int_tx(struct rt_serial_device *serial, const rt_uint8_t *data, int length)
{
    int size;
    size = length;
    while (length)
    {
        /*
         * to be polite with serial console add a line feed
         * to the carriage return character
         */
        if (*data == '\n' && (serial->parent.open_flag & RT_DEVICE_FLAG_STREAM))
        {
            if (serial->ops->putc(serial, '\r') == -1)
            {
                rt_completion_wait(&(tx->completion), RT_WAITING_FOREVER);
                continue;
            }
        }
        if (serial->ops->putc(serial, *(char*)data) == -1)
        {
            rt_completion_wait(&(tx->completion), RT_WAITING_FOREVER);
            continue;
        }
        data ++; length --;
    }

    return size - length;
}

比较清晰的看到,中断发送最终是调用的putc接口,而serial->ops->putc其实代表的就是stm32_putc:

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_STM32G4) || defined(SOC_SERIES_STM32MP1) || defined(SOC_SERIES_STM32WB)
    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;
}

这个函数里面,是相当于将数据发送给了TDR数据发送寄存器,然后通过while(1)死等的方式,直到数据的发送完成。因此串口中断发送模式,其实就是和串口轮询发送模式是一样的,并没有用到中断发送的功能。

最后说串口DMA发送模式。由上面讲解我们知道,串口发送轮询模式和中断模式,其实都是阻塞发送模式,这个DMA发送模式则不是这样的。结合第一章节DMA发送的讲解,我们知道DMA发送最终调用的是stm32_dna_transmit,我们看函数代码:

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

这里是调用了HAL_UART_Transmit_DMA函数,有兴趣的可以再跟踪一下这个函数,我这里直接说结论了,这个函数是将buf数据直接传递给数据发送端,然后就直接返回了,也就是说,当函数返回的时候,其实数据并没有发送完成,这就意味着,串口DMA发送模式,其实是一个非阻塞发送模式。

那么会存在哪些问题呢?

第一呢,是模式不够统一,其他两个模式都是阻塞发送,这里突然变成了非阻塞发送,这样就会使得用户编写的上层应用在模式改变的情况下,无法做到行为一致。

第二呢,是数据紊乱,当应用程序模式为轮询时正常工作,然后切换成DMA模式后出现数据紊乱的情况。这部分内容我在第一章DMA发送部分也提到过的,

dma.png

当时说的是,”该缓冲区内容被意外修改了“,为什么会被意外修改呢,就是因为发送接口返回了,而数据并没有发送完成。由于发送接口传递的是缓冲区指针,因此改变数据的内容时,DMA发送的地址并不会变,这样的话就相当于直接修改了DMA发送的数据内容,导致数据发送数据错误、丢包等问题。

总结

串口的读数据接口:是按照非阻塞的方式进行接收的。

串口的写数据接口:中断模式实质上是轮询模式的一种方式,并未充分发挥中断的优势;中断和轮询都是阻塞发送模式,而DMA模式是非阻塞发送模式。如果使用不当,将会使得发送数据容易错误、丢包等问题。

其他

最后再说一下其他方面的问题:

  1. 用户使用串口的时候,有时候开发板并未做到抗干扰的保护措施,因此一定要注意,需要将串口引脚设置为上拉模式。有时候的错误干扰将会导致串口中断行为被破坏,导致串口数据无法正常工作。为了验证这种情况,也可以通过串口的错误标志来判断是否有这样的情况产生。下面给出一段测试代码,感兴趣的可以测试下,当串口为浮空时和串口为上拉时候的抗干扰能力。这段测试代码可以直接放到drv_usart.cuart_isr中:相关ISSUE

    static void uart_isr(struct rt_serial_device *serial)
    {
        struct stm32_uart *uart;
    
        RT_ASSERT(serial != RT_NULL);
        uart = rt_container_of(serial, struct stm32_uart, serial);
        if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_ORE) != RESET)
        {
            LOG_E("(%s) serial device Overrun error!", serial->parent.parent.name);
            __HAL_UART_CLEAR_OREFLAG(&uart->handle);
        }
        if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_NE) != RESET)
        {
            LOG_E("(%s) serial device Noise error!", serial->parent.parent.name);
            __HAL_UART_CLEAR_NEFLAG(&uart->handle);
        }
        if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_FE) != RESET)
        {
            LOG_E("(%s) serial device Framing error!", serial->parent.parent.name);
            __HAL_UART_CLEAR_FEFLAG(&uart->handle);
        }
        if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_PE) != RESET)
        {
            LOG_E("(%s) serial device Parity error!", serial->parent.parent.name);
            __HAL_UART_CLEAR_PEFLAG(&uart->handle);
        }
    
        if ((__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_RXNE) != RESET) &&
                (__HAL_UART_GET_IT_SOURCE(&(uart->handle), UART_IT_RXNE) != RESET))
        {
    ... ...
    
  1. 既然串口V1版本有这些问题,那么怎么修改呢?是的,既然命名为串口V1,那肯定是有串口V2啦,串口V2解决了上述从一些问题,使得用户使用上更加明确(串口V2版本是本人写的)。当前关于串口V2版本的介绍比较少,只有文档中心的[UART 设备 v2 版本],也是本人写的使用教程。后续计划是,整理总结更多的串口V2的文档资料,包括串口V2的原理分析、串口V2版本与V1版本对比、以及串口V2版本适配指南等文档,方便大家一起适配共同使用,结合大家的力量,发挥开源的精神。

更多文章:
RTT串口V1版本的使用分析及问题排查指南(一)
RTT串口V1版本的使用分析及问题排查指南(二)
RTT串口V1版本的使用分析及问题排查指南(三)

推荐阅读
关注数
8074
内容数
181
小而美的物联网操作系统,经过14年的累积发展,RT-Thread 已经拥有一个国内最大的嵌入式开源社区,同时被广泛应用于能源、车载、医疗、消费电子等多个行业,累积装机量超过4亿台,成为国人自主开发、国内最成熟稳定和装机量最大的开源 RTOS。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息