Toggle navigation
首页
问答
文章
积分商城
专家
专区
更多专区...
文档中心
返回主站
搜索
提问
会员
中心
登录
注册
serial_V1
串口
RTT串口V1版本的使用分析及问题排查指南(三)
发布于 2021-07-22 22:16:04 浏览:3565
订阅该版
[tocm] ### 应用层的串口问题 应用层使用串口时,需要先指定以何种方式(轮询、中断或者DMA)去打开串口,然后再进行串口的操作。这部分相关的内容在第一章已经说过,这里主要用来总结串口使用时遇到的一些问题。 串口注册时候的默认规则慢慢道来: #### 串口注册的默认规则 我们都知道,串口设备需要先注册,才能通过`rt_device_open` API 去访问,那么串口注册时候的默认规则是什么呢?看串口驱动的注册代码如下所示: ```c 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字节,**需要注意的是,串口参数里面没有发送缓冲区的设置**。 ```c /* 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接收(好像没有轮询死等串口数据的场景吧,这里不再讨论这个轮询读的方式),这里比较统一,无论是哪种方式,都是非阻塞的接收模式。即,我们可能会通过一个信号量或者消息队列,当接收到数据的时候,就唤醒当前线程进行数据的读取。这里我们参照串口的文档中心的例子:[中断接收](https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/device/uart/uart_v1/uart?id=%e4%b8%ad%e6%96%ad%e6%8e%a5%e6%94%b6%e5%8f%8a%e8%bd%ae%e8%af%a2%e5%8f%91%e9%80%81) 和 [DMA接收](https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/device/uart/uart_v1/uart?id=dma-%e6%8e%a5%e6%94%b6%e5%8f%8a%e8%bd%ae%e8%af%a2%e5%8f%91%e9%80%81),这两个demo,一个是使用信号量,一个是使用消息队列,来完成串口数据的读取的。 因此可以统一地说,串口的读数据接口,是按照非阻塞的方式进行接收的,当读取不到数据的时候,会将当前线程挂起,以免浪费CPU资源。 ##### 再说串口的写数据 串口的写数据会选择三种模式,轮询、中断、DMA。 串口的的轮询写数据这个模式,是我们用的比较多的一个场景,比如上一章节说的FinSH的输出,或者说是rt_kprintf的输出。因此显而易见的,轮询模式的写数据接口是阻塞的方式进行的。 重点来了:串口的中断模式,这个模式其实是有一些问题的,我们理想中的中断发送,其实是先打开发送的中断使能,然后在中断服务函数内把数据发送出去,等数据发送空中断后再发送下一个字节的数据,依次单字节循环发送即将数据发送完成。串口V1的中断发送是怎么实现的呢?我们来看一下代码: ```c 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`: ```c 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`,我们看函数代码: ```c 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](https://oss-club.rt-thread.org/uploads/20210722/6e0744ec1f971f8b476732ca8ee718b1.png) 当时说的是,”该缓冲区内容被意外修改了“,为什么会被意外修改呢,就是因为发送接口返回了,而数据并没有发送完成。由于发送接口传递的是缓冲区指针,因此改变数据的内容时,DMA发送的地址并不会变,这样的话就相当于直接修改了DMA发送的数据内容,导致数据发送数据错误、丢包等问题。 ##### 总结 串口的读数据接口:是按照非阻塞的方式进行接收的。 串口的写数据接口:中断模式实质上是轮询模式的一种方式,并未充分发挥中断的优势;中断和轮询都是阻塞发送模式,而DMA模式是非阻塞发送模式。如果使用不当,将会使得发送数据容易错误、丢包等问题。 ### 其他 最后再说一下其他方面的问题: 1. 用户使用串口的时候,有时候开发板并未做到抗干扰的保护措施,因此一定要注意,需要将串口引脚设置为上拉模式。有时候的错误干扰将会导致串口中断行为被破坏,导致串口数据无法正常工作。为了验证这种情况,也可以通过串口的错误标志来判断是否有这样的情况产生。下面给出一段测试代码,感兴趣的可以测试下,当串口为浮空时和串口为上拉时候的抗干扰能力。这段测试代码可以直接放到`drv_usart.c`的`uart_isr`中:[相关ISSUE](https://github.com/RT-Thread/rt-thread/issues/4838) ```c 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)) { ... ... ``` 2. 既然串口V1版本有这些问题,那么怎么修改呢?是的,既然命名为串口V1,那肯定是有[串口V2](https://github.com/RT-Thread/rt-thread/pull/4764)啦,串口V2解决了上述从一些问题,使得用户使用上更加明确(串口V2版本是本人写的)。当前关于串口V2版本的介绍比较少,只有文档中心的[[UART 设备 v2 版本](https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/device/uart/uart_v2/uart?id=uart-设备-v2-版本)],也是本人写的使用教程。后续计划是,整理总结更多的串口V2的文档资料,包括串口V2的原理分析、串口V2版本与V1版本对比、以及串口V2版本适配指南等文档,方便大家一起适配共同使用,结合大家的力量,发挥开源的精神。 更多文章: [RTT串口V1版本的使用分析及问题排查指南(一)](https://club.rt-thread.org/ask/article/2894.html) [RTT串口V1版本的使用分析及问题排查指南(二)](https://club.rt-thread.org/ask/article/2898.html) [RTT串口V1版本的使用分析及问题排查指南(三)](https://club.rt-thread.org/ask/article/2904.html) [串口框架V1和V2版本对比](https://club.rt-thread.org/ask/article/2915.html) [串口 V2 适配指南](https://club.rt-thread.org/ask/article/2920.html)
3
条评论
默认排序
按发布时间排序
登录
注册新账号
关于作者
123
这家伙很懒,什么也没写!
文章
6
回答
309
被采纳
68
关注TA
发私信
相关文章
1
串口DMA发送数据时,数据被覆盖
2
关于串口DMA模式下rt_device_close问题
3
利用stm32f427实现usb转串口,电脑端什么也没有识别到
4
finsh 控制台 适配 RS 485请大神指点????
5
uart_sample.c 中,读串口设备时偏移量pos要设置为-1而不是0?
6
【结贴】at_device软件包中对串口接收数据缺少判断导致数据接收异常
7
串口无法接受数据,但可以发送
8
串口如何有效的清除掉接收缓冲,而不必一个一个的去读取
9
串口接收使用方式问题
10
雅特力FINSH问题
推荐文章
1
RT-Thread应用项目汇总
2
玩转RT-Thread系列教程
3
国产MCU移植系列教程汇总,欢迎查看!
4
机器人操作系统 (ROS2) 和 RT-Thread 通信
5
五分钟玩转RT-Thread新社区
6
【技术三千问】之《玩转ART-Pi》,看这篇就够了!干货汇总
7
关于STM32H7开发板上使用SDIO接口驱动SD卡挂载文件系统的问题总结
8
STM32的“GPU”——DMA2D实例详解
9
RT-Thread隐藏的宝藏之completion
10
【ART-PI】RT-Thread 开启RTC 与 Alarm组件
热门标签
RT-Thread Studio
串口
Env
LWIP
SPI
AT
Bootloader
Hardfault
CAN总线
FinSH
ART-Pi
USB
DMA
文件系统
RT-Thread
SCons
RT-Thread Nano
线程
MQTT
STM32
RTC
FAL
rt-smart
ESP8266
I2C_IIC
WIZnet_W5500
UART
ota在线升级
PWM
cubemx
freemodbus
flash
packages_软件包
BSP
潘多拉开发板_Pandora
定时器
ADC
GD32
flashDB
socket
中断
Debug
编译报错
msh
SFUD
keil_MDK
rt_mq_消息队列_msg_queue
ulog
C++_cpp
at_device
本月问答贡献
踩姑娘的小蘑菇
7
个答案
3
次被采纳
a1012112796
13
个答案
2
次被采纳
张世争
9
个答案
2
次被采纳
rv666
5
个答案
2
次被采纳
用户名由3_15位
11
个答案
1
次被采纳
本月文章贡献
程序员阿伟
8
篇文章
2
次点赞
hhart
3
篇文章
4
次点赞
大龄码农
1
篇文章
3
次点赞
ThinkCode
1
篇文章
1
次点赞
Betrayer
1
篇文章
1
次点赞
回到
顶部
发布
问题
投诉
建议
回到
底部