CH32串口接收方案(IDLE+DMA+FreeRTOS+NOTIFY)
为大家分享一种串口不定长数据接收方案,CPU占用极低,响应速度极快,避免了各种无意义的消耗,可以应用在任何其他的单片机中,这是我在实际项目当中琢磨出来的,目前网上好像没怎么看到这种做法

首先介绍一下该方案所需的基础知识:
1. IDLE中断
单片机的串口Idle中断是一种特殊的中断,它会在串口接收完数据并进入空闲状态时触发,单片机就可以及时处理接收到的数据。这个中断可以在串口通信中避免单片机一直等待数据接收而导致系统停止响应的情况。原理是单片机在接收串口数据时,设置一个计时器,当串口接收完数据后,会触发计时器,计时器计时结束后就会触发Idle中断,单片机就可以立即停止接收数据,并执行相应的中断服务程序,从而实现对接收数据的及时处理
2. DMA
DMA(Direct Memory Access,直接内存访问)是一种通过硬件控制数据传输的技术。使用DMA可以使单片机在数据传输过程中腾出CPU的时间和资源,提高系统的效率和性能。在传统的单片机系统中,数据传输通常是通过CPU来实现的,CPU需要不停地从外设中读取数据,然后再将数据写入内存或者反过来,这样会消耗大量的CPU时间和资源。而使用DMA技术可以避免这种情况,因为DMA可以通过硬件控制数据传输,而不需要CPU的干预。当数据传输完成后,DMA会向CPU发送一个中断请求,通知CPU数据已经传输完成,CPU可以继续执行其他的任务,从而提高了系统的效率和性能。
3. RTOS
RTOS(Real-Time Operating System,实时操作系统)是一种专门用于实时应用的操作系统。它提供了一些重要的功能,例如任务调度、中断处理、内存管理、进程间通信等,我使用的是FreeRTOS实时操作系统,其他的操作系统例如RTT,UCOS都是一样的
4. Notify
FreeRTOS中的任务通知是一种轻量级的进程间通信机制,它可以用于不同任务之间的通信和同步。任务通知可以让任务之间更加灵活地进行信息传递,从而实现更加复杂的系统行为。任务通知是由发送者向接收者发送信号的一种机制。当发送者需要向接收者发送一个信号时,它可以使用任务通知API将一个通知值发送给接收者。接收者可以在任何时候等待通知值,当通知值到达时,接收者就会被唤醒,并且可以根据通知值来执行相应的操作。任务通知可以在一定程度上代替二值信号量或者计数信号量,并且任务通知更加快速轻便,官方称任务通知比二值信号量快了至少45%(不清楚其他的操作系统有没有任务通知,但是原理都是类似的,本质就是实现一个二值信号量的效果)

下面是整个方案运行的思路:
首先配置好串口的收发,完成串口的初始化,并且启用idle空闲中断,然后配置dma,为串口外设指定dma通道(注意不要使能dma的中断),创建一个串口数据处理任务,在任务的死循环中等待通知,当串口接收到一帧数据后触发中断,在中断处理函数中需要做几件事情:停止dma通道、复位中断标志位、计算数据的大小、发送任务通知、复位dma计数、重新使能dma通道,做完这些事之后中断结束,串口数据处理任务接到通知,开始直接处理数据
(由于发送任务通知的时候,从dma中得到了数据长度传递给了任务,这带来了超多好处
1.只有触发中断的时候计算一次,运算次数少,常规做法需要在中断中不断累加计数
2.数据处理任务可以直接根据数据长度进行数据处理,无需其他额外的计算步骤
3. 接收数据的缓冲数组不需要每次用完都memset清空,因为得到了准确的数据大小,不存在操作越界的问题
对于CPU来说,就像做梦一样,突然有人叫了你一下,醒来发现数据都喂到嘴巴里了)

代码实现:
1.操作系统的移植
STM32可以使用CUBE直接生成(真好),CH32部分系列可以使用官方移植好的完整工程(真好),GD32可以参考网上教程自行移植 (笑)
2. 串口配置
这段代码是用来初始化USART1串口模块,主要包含以下几个部分:
定义并初始化用于GPIO、USART和NVIC初始化的结构体变量
GPIO_InitStructure
、USART_InitStructure
和NVIC_InitStructure
。使能 GPIOA 和 USART1 的时钟。
配置 GPIOA 的第 9 个引脚为 USART1 的发送引脚,配置 GPIOA 的第 10 个引脚为 USART1 的接收引脚。
配置 USART1 的波特率、数据位长度、停止位、校验位、硬件流控制以及发送和接收模式。
使能 USART1 的空闲中断。
配置 USART1 的中断优先级和使能中断。
使能 DMA1 的通道 5 和通道 4,用于 USART1 的接收和发送。(但是实际上没用到通道4)
使能 USART1 模块。
3.DMA通道配置
这段代码是用来初始化DMA1通道5,定义并初始化用于DMA初始化的结构体变量 DMA_InitStructure,
最后初始化 DMA1 的通道5。
4.中断处理函数
首先在文件顶部申明中断处理函数,这是申明格式,后面的__attribute__申明一定要加,不然GCC编译器不知道这是个中断函数,其他单片机的申明方法不一样,需要自行查明
函数实现,首先失能了dma通道,然后读取串口的发送和接收寄存器,这一步是为了复位中断标志位,如果不这么做中断标志位就无法被复位,会无限进中断,下一步发送任务通知,
使用xTaskNotifyFromISR函数
参数分别为(目标任务句柄,通知值(这里发送数据大小),指定通知如何更新任务的通知,NULL)
不过即使发送了通知任务也没法执行,因为现在还卡在中断里(笑)
然后重新设置dma计数(比如一开始设定dma传输数据大小为20个,接收到3个数据后触发中断,此时计数值为17,所以每次需要将计数值恢复到原始大小,不然会导致下次计算出错)
最后重新打开dma通道(这一步是为了复位内存地址,否则数据无法放在正确的内存里,会导致严重的内存越界问题)
5. 数据处理任务
首先定义一个变量用于存放任务通知值(串口数据大小),然后使能串口dma接口,随后在死循环中用xTaskNotifyWait函数做任务阻塞,这个任务会一直卡在这里不执行
参数为(进入通知时对通知值的操作(这里给0,不操作),退出通知时通知值的操作(给0,不操作),接收通知值的变量地址,超时时间(最大超时))
当接收到数据后,由中断发送通知,这里会被解除阻塞状态,后面的代码得以执行,这里就直接放自己需要的数据出来代码就可以了,我这里只是简单的把接收到的数据原样打印出来

后记:
这个方法虽好,但是其实稍微有点复杂,首先是基于操作系统的,如果是裸机程序则无法使用,其次大部分单片机并没有st那样的代码生成工具(CUBE),各种配置过程繁琐复杂,很容易出错,如果用stm32过程会轻松很多
操作系统提供了多线程的方便,但也会带来操作系统的问题,在中断里面使用操作系统的API一定要使用带FromISR结尾的,否则会造成各种死机问题,同时还需要保证该中断的优先级低于操作系统的可屏蔽中断的优先级,否则会出现错误的中嵌套,导致死机
下面给出完整的代码,仅供参考:
感谢阅读(≧∇≦)ノ
