# serialX **Repository Path**: djx_rt_thread/serialX ## Basic Information - **Project Name**: serialX - **Description**: 基于 rt-thread 平台的串口框架 - **Primary Language**: C - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 18 - **Created**: 2024-03-26 - **Last Updated**: 2024-03-26 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # serialX ## 前言 “serialX” 我起的名字,起个名字想破头。 在前一篇文章里,大致提出了我的串口驱动框架理论。里面做了一些对串口驱动特性的幻想。也在 NUC970 芯片下通过了中断模式的实践验证。但是,因为 NUC970 的 uart 自带 fifo 。用它测试效果好,并不能真正说明驱动框架通过验证了。 然后,紧接着笔者在 STM32F429 完成了中断和 DMA 两种模式。今天,我把一些测试结果和移植说明发出来,征求全网公测。 测试配置:DMA 二级缓存 32 个字节,串口收发缓存各 512 字节。 注:本串口驱动工作特性请参阅前一篇文章[rt-thread 驱动篇 之 串口驱动框架剖析性能提升](https://club.rt-thread.org/ask/article/3335.html) ### STM32 中断模式测试 以下是三组连续发收测试: 1. 定时间隔20ms,发送250字节数据,持续发送2600w,接收发送数据量相等 ![250-20ms-2600w.png](https://gitee.com/thewon/serialX/raw/master/test/250-20ms-2600w.png) 2. 定时间隔50ms,发送250字节数据,持续发送600w,接收发送数据量相等 ![250-50ms-600w.png](https://gitee.com/thewon/serialX/raw/master/test/250-20ms-2600w.png) 3. 定时间隔80ms,发送1000字节数据,持续发送600w,接收发送数据量相等 ![1000-80ms-610w.png](https://gitee.com/thewon/serialX/raw/master/test/1000-80ms-610w.png) 注:刚刚跟我们小伙伴求证了一下,串口调试助手的定时间隔是固定周期。如果是这样的,以上测试是有意义的,如果不是,那就没达到串口带宽上限。 ### STM32 DMA模式测试 1. 读写测试,串口调试助手定时 10ms ,发送40字节数据,持续发送129w ![dma-40-10ms-129w.png](https://gitee.com/thewon/serialX/raw/master/test/dma-40-10ms-129w.png) 2. 串口调试助手定时 50ms ,发送500字节数据,持续发送527w ![dma-500-50ms-527w.png](https://gitee.com/thewon/serialX/raw/master/test/dma-500-50ms-527w.png) 3. 串口调试助手定时 40ms ,发送500字节数据,持续发送261w ![dma-500-40ms-261w.png](https://gitee.com/thewon/serialX/raw/master/test/dma-500-40ms-261w.png) 4. 串口调试助手定时 40ms ,发送1000字节数据,持续发送262w ![dma-1000-40ms-262w.png](https://gitee.com/thewon/serialX/raw/master/test/dma-1000-40ms-262w.png) 串口调试助手上发送和接收数量不相等,接着我在代码中添加了个断点,单独发送了一个字节 ‘Z’ 。 ![dma-1000-40ms-262w-rw.png](https://gitee.com/thewon/serialX/raw/master/test/dma-1000-40ms-262w-rw.png) 代码中接收和发送数量相等,都等于串口调试助手的接收量。这个缺少的部分是串口调试助手发送失败数量,还是串口驱动接收丢失了? 接下来,修改成中断接收发送模式,其它不做修改进行相同的测试,也是有数量差。进一步检查串口驱动里,接收缓存有溢出现象。应用层没来得及把数据取走,就删掉了最旧的数据。 ### 接口详解及移植说明 #### rtdef.h 添加几个宏定义 添加一个强制 inline 宏定义 `#define rt_forceinline static __attribute__((always_inline))` 添加阻塞打开相关标志() ``` #define RT_DEVICE_OFLAG_BLOCKING 0x000 /**< blocking io mode */ #define RT_DEVICE_OFLAG_NONBLOCKING 0x004 /**< non-blocking io mode */ ... #define RT_DEVICE_CTRL_BLOCKING 0x05 /**< blocking io */ ``` #### serialX.h 添加串口驱动缓存和 DMA 二级缓存大小定义(放弃使用 `RT_SERIAL_RB_BUFSZ`): ``` #ifndef RT_SERIAL_FIFO_BUFSZ #define RT_SERIAL_FIFO_BUFSZ 512 #endif #ifndef RT_SERIAL_DMA_BUFSZ #define RT_SERIAL_DMA_BUFSZ 32 #endif ``` 串口接收和发送使用的缓存大小是一样的,如果想改变串口缓存大小,请修改 `RT_SERIAL_FIFO_BUFSZ` 的值。 如果想改变 DMA 二级缓存大小,请修改 `RT_SERIAL_DMA_BUFSZ` 的值。 定义一个收发通用 fifo: ``` struct rt_serial_fifo { rt_uint32_t buf_sz; /* software fifo */ rt_uint8_t *buffer; rt_uint16_t put_index, get_index; rt_bool_t is_full; }; ``` 重新定义 `rt_serial_device` 定义: ``` struct rt_serial_device { struct rt_device parent; const struct rt_uart_ops *ops; struct serial_configure config; void *serial_rx; // 串口接收缓存 void *serial_tx; // 串口发送缓存 #ifdef RT_SERIAL_USING_DMA // 串口收发缓存和 DMA 使用的二级缓存分开 rt_size_t dma_idx_rx; rt_uint8_t serial_dma_rx[RT_SERIAL_DMA_BUFSZ]; // DMA 接收缓存 rt_uint8_t serial_dma_tx[RT_SERIAL_DMA_BUFSZ]; // DMA 发送缓存 #endif cb_serial_tx _cb_tx; // 写过程回调函数指针 cb_serial_rx _cb_rx; // 读过程回调函数指针 struct rt_completion completion_tx; // 发送完成 struct rt_completion completion_rx; // 接收到新数据 笔者注:这里虽然用的是完成量,但是这样有个漏洞,见下面详细分析说明 }; typedef struct rt_serial_device rt_serial_t; ``` 串口驱动通用框架和硬件底层接口定义 ``` struct rt_uart_ops { // 用于配置外设寄存器,引脚功能复用,启用外设等等 rt_err_t (*configure)(struct rt_serial_device *serial, struct serial_configure *cfg); // 用于使能禁用中断,初始配置 DMA rt_err_t (*control)(struct rt_serial_device *serial, int cmd, void *arg); // 串口外设写数据寄存器*为空*,把数据放入写数据寄存器。*不为空*,死等 int (*putc)(struct rt_serial_device *serial, char c); // 串口外设读数据寄存器*不为空*,读出读数据寄存器的值。*为空*,返回 -1 int (*getc)(struct rt_serial_device *serial); // 启动发送,多数是开启串口外设发送寄存器空中断 void (*start_tx)(struct rt_serial_device *serial); // 结束发送,多数是关闭串口外设发送寄存器空中断 void (*stop_tx)(struct rt_serial_device *serial); #ifdef RT_SERIAL_USING_DMA // 判断 DMA 是否在发送过程中,就像上一篇里笔者多次提示的,必须有效检测 DMA 是否在发送数据中 rt_bool_t (*is_dma_txing)(struct rt_serial_device *serial); // 启动 DMA 发送 void (*start_dma_tx)(struct rt_serial_device *serial, rt_uint8_t *buf, rt_size_t size); // 停止 DMA 发送 void (*stop_dma_tx)(struct rt_serial_device *serial); #endif // 使能串口外设中断 void (*enable_interrupt)(struct rt_serial_device *serial); // 禁用串口外设中断 void (*disable_interrupt)(struct rt_serial_device *serial); }; ``` 移植 serialX 到新芯片上,必须按照 `rt_uart_ops` 的定义实现上述几个接口。函数功能不能随意更改。 #### `rt_hw_serial_isr` 这个中断只接收 `RT_SERIAL_EVENT_RX_IND` `RT_SERIAL_EVENT_RX_IND` `RT_SERIAL_EVENT_RX_DMADONE` `RT_SERIAL_EVENT_TX_DMADONE` 四种中断状态。 - `RT_SERIAL_EVENT_RX_IND` 接收寄存器不空中断 - `RT_SERIAL_EVENT_TX_DONE` 发送寄存器空中断,为了兼容自带 fifo 的芯片,event 参数的高三字节代表 fifo 容量 - `RT_SERIAL_EVENT_RX_DMADONE` 串口接收 DMA 中断。 这个可以兼容接收半传输和全传输等多种中断。event 参数的高三字节代表 DMA fifo 接收数据数量(1-RT_SERIAL_DMA_BUFSZ)。 - `RT_SERIAL_EVENT_TX_DMADONE` 串口发送 DMA 中断。这个应该保证 DMA 发送完本次 DMA 缓存中的所有数据,也就是对于 stm32 芯片是 DMA 计数达到 0。 ### 使用注意 - `RT_SERIAL_FIFO_BUFSZ` `RT_SERIAL_DMA_BUFSZ` 两个的定义和实际是否合适,小数据量通信可以定义小点儿,数据量大的情况适当调整这两个值。 - `rt_uart_ops` 接口定义,功能实现必须匹配。 - 阻塞模式,收发是一致的。默认是阻塞模式。想使用非阻塞模式请 open 的时候添加 `RT_DEVICE_OFLAG_NONBLOCKING` flag。 - 使用 `RT_DEVICE_FLAG_INT` `RT_DEVICE_FLAG_DMA_RX` `RT_DEVICE_FLAG_INT_TX` `RT_DEVICE_FLAG_DMA_TX` 四个 open flag 指定收发模式,是用中断还是 DMA。 - **特别提醒**,非阻塞模式下,read 可能返回 0。write 返回值可能不是目标写入 size。read/write 还可能返回 `RT_EXXX` 错误值。 - **特别提醒**,阻塞模式下,read 返回值可能不是期望数据量 size。笔者也曾经提供过可靠处理流数据的方案,详见 [rt-thread 使用宝典(2021-1210更新)](https://club.rt-thread.org/ask/article/3186.html) #### 使用完成量进入阻塞漏洞分析 串口驱动里有几个阻塞点,进入阻塞都使用的 `rt_complition` ,如下代码: ``` serial->ops->enable_interrupt(serial); ret = rt_completion_wait(&(serial->completion_rx), RT_WAITING_FOREVER);// 或者 serial->completion_tx ``` 首先开中断,调用 `rt_completion_wait` 等待完成量进入阻塞。这样是有个漏洞的,当开中断后有个串口中断,中断处理函数里调用 `rt_completion_done` 是没有任何反应的,`rt_completion_done` 直接返回退出。 进而回到原线程才执行 `rt_completion_wait`。之后,如果有第二次接收(或发送)中断发生时才会结束上一次的阻塞。但是,第二次什么时候出现也就是个未知数了。即便前一次可能已经收全了全部想要的数据,但是会不定期阻塞下去。 解决方法有两个:一、不用永久阻塞,换成 10ms 或者几 ms 等待;二、用二值信号量替代。 但是!!!我没有用上述方法中的任何一个进行改进,原因是: 第一种方法无疑要引入一个循环,`rt_completion_wait` 超时返回的时候循环继续阻塞。还有就是等待时间没有理论支持。最重要的是用循环方式补漏洞的方式不美观。 没使用二值信号量的原因是,rt-thread 的信号量实现没有真正的“二值”,如果中断已经多次 release 了,然后应用层才来一次 take,之后还可能成功 take 多次(虽然应该是要阻塞的,但是实际不阻塞,反而会循环 take 多次)。 因此,暂时继续使用完成量。年后信号量的真正的二值性实现后,再换成信号量方式。 ## 新增适配芯片 #### N32G45 - 已在 N32G45XVL-STB 开发板上测试,芯片 N32G457VEL7; - 已添加适配 USART 1/2/3/4 ,可以自行添加其他串口外设; - 支持中断和 DMA (收发可任意选择)。 2022/06/06 #### AB32VG1 - 已在 AB32VG1 开发板上测试,芯片 N32G457VEL7; - 已添加适配 USART 0/1/2 ,可以自行添加其他串口外设; - 支持中断收发(不支持轮询发送,因为把发送完成判断去掉了)。 2022/06/08 #### RA6M4 - 已在 RA6M4 开发板上测试,芯片 R7FA6M4AF3CFB; - 已添加适配 USART 1/2/3/4 ,可以自行添加其他串口外设; - 支持中断接收。 - 本想改成中断发送,但是失败了,连续发送多个字节数据会不响应发送完成中断(望有熟悉这个芯片的大佬指点迷津)。 2022/06/06 #### GD32F4 - 已在 GD32450i-EVAL 开发板上测试,芯片 GD32F450IK; - 已添加适配 USART 0-7 ,可以自行添加其他串口外设; - 支持中断收发 (收发可任意选择)。 - 支持轮询收发。 - 未支持 DMA 收发。 2022/06/08 #### AT32 2023/04/19 - 只是编译通过,并未进行测试。 #### 其他芯片 下一步计划中。。。敬请期待! ## 2023/02/15 修改记录 1. 修改 `struct serial_configure` 结构体,去掉 `rt_uint32_t bufsz` 成员元素,并将其移动到 `struct rt_serial_device` 内部。 2. `struct rt_uart_ops` 添加 `init` 接口。 `init` 和 `configure` 进行功能划分, - `configure` 只进行配置串口外设的“波特率、数据位长度、停止位长度、校验位以及其它可能存在的流控”等配置。 - `init` 除了包含 `configure` 的功能外,还可能包含“引脚复用设置、串口外设时钟配置、串口外设使能以及开启串口外设中断总开关”。 **强烈建议:**使能串口外设之前,先给串口外设配置一组默认的“波特率、数据位长度”等。这组默认值,或者是 `rt_hw_serial_register` 初始化的,或者是上次使用串口设备留下来的。 ### 解决的问题 `init` 和 `configure` 功能分离之后,可以使 `configure` 的操作在 `open` 之后或者之前, ``` uart_dev = rt_device_find(SERIAL_DEV_NAME); rt_device_control(uart_dev, RT_DEVICE_CTRL_CONFIG, &uart_conf); rt_device_open(uart_dev, RT_DEVICE_OFLAG_RDWR); ``` 和 ``` uart_dev = rt_device_find(SERIAL_DEV_NAME); rt_device_open(uart_dev, RT_DEVICE_OFLAG_RDWR); rt_device_control(uart_dev, RT_DEVICE_CTRL_CONFIG, &uart_conf); ``` 这两种操作结果都是一样的。 > 因为,在开启 posix 之后,使用 termios 配置串口波特率只能在 open 之后。 ### 需要注意的地方 很多芯片厂商提供的开发包里,串口外设使能和配置波特率是同一个函数,并没有单独的配置波特率的函数! 比如 stm32 只有一个 `HAL_UART_Init` 初始化函数,它的功能是配置串口外设寄存器然后使能串口,这个函数内部调用了静态函数 `UART_SetConfig` 配置波特率等寄存器。并没有外部接口版本的 `UART_SetConfig` 。 作为对比,gd32 提供的接口就丰富多了,不仅有 `usart_baudrate_set` 还有 `usart_word_length_set` `usart_stop_bit_set` `usart_parity_config` ,每一个都是可以单独设置的。 因此,结论是,**`init` 和 `configure` 功能分离之后,stm32 并没有改进**。除非我们修改 HAL 的源码,把 `UART_SetConfig` 修改成全局函数并提供外部引用声明(这可能要触动很多人的神经了), `configure` 里只调用 `UART_SetConfig` 。 ## 2023/08/19 修改记录 添加阻塞超时特性 ``` rt_tick_t timeout = 50; rt_device_control(serial_dev, RT_DEVICE_CTRL_TIMEOUT, &timeout); ``` 设置读写超时时间,在阻塞模式下等待读/写超过一定时间后从读/写等待返回 ## 结束语 现笔者将打码开放出来 gitee 仓库 [serialX](https://gitee.com/thewon/serialX),求全论坛公测。期待各位大佬提出各种测试方案,它最不怕的就是考验。 有问题可以在仓库里提 [issue](https://gitee.com/thewon/serialX/issues) ,或者到 rt-thread 官方论坛上进行讨论。