本文由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发送的地址并不会变,这样的话就相当于直接修改了DMA发送的数据内容,导致数据发送数据错误、丢包等问题。
总结
串口的读数据接口:是按照非阻塞的方式进行接收的。
串口的写数据接口:中断模式实质上是轮询模式的一种方式,并未充分发挥中断的优势;中断和轮询都是阻塞发送模式,而DMA模式是非阻塞发送模式。如果使用不当,将会使得发送数据容易错误、丢包等问题。
其他
最后再说一下其他方面的问题:
用户使用串口的时候,有时候开发板并未做到抗干扰的保护措施,因此一定要注意,需要将串口引脚设置为上拉模式。有时候的错误干扰将会导致串口中断行为被破坏,导致串口数据无法正常工作。为了验证这种情况,也可以通过串口的错误标志来判断是否有这样的情况产生。下面给出一段测试代码,感兴趣的可以测试下,当串口为浮空时和串口为上拉时候的抗干扰能力。这段测试代码可以直接放到
drv_usart.c
的uart_isr
中:相关ISSUEstatic 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版本的使用分析及问题排查指南(三)