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

RTT小师弟

本文由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模式是非阻塞发送模式。如果使用不当,将会使得发送数据容易错误、丢包等问题。
其他

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

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

既然串口V1版本有这些问题,那么怎么修改呢?是的,既然命名为串口V1,那肯定是有串口V2啦,串口V2解决了上述从一些问题,使得用户使用上更加明确(串口V2版本是本人写的)。当前关于串口V2版本的介绍比较少,只有文档中心的[UART 设备 v2 版本],也是本人写的使用教程。后续计划是,整理总结更多的串口V2的文档资料,包括串口V2的原理分析、串口V2版本与V1版本对比、以及串口V2版本适配指南等文档,方便大家一起适配共同使用,结合大家的力量,发挥开源的精神。

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

阅读 186

小而美的物联网操作系统,RT-Thread 已经拥有一个国内最大的嵌入式开源社区,同时被广泛应用于能源、车载...

1 声望
2 粉丝
0 条评论
你知道吗?

小而美的物联网操作系统,RT-Thread 已经拥有一个国内最大的嵌入式开源社区,同时被广泛应用于能源、车载...

1 声望
2 粉丝
宣传栏