0%

STM32串口DMA接收的使用姿势

串口接收

串口是一种直接接收串行数据的方法,而通常固定数据帧的结构为多个字节,在处理的时候实际上是针对整帧。因此需要通过某种方法将串行数据转换为相当于同一时刻连续出现的数据处理。评定这种转换方法的优劣有以下方面

  • 正确性
    能正确识别数据帧的帧头和帧尾,总是可以将接收到的各种数据正确转换,不论发送的间隔,发送的时机如何,甚至也可以处理不定长的数据
  • 可靠性
    当发送数据出现不符合协议规定的数据时能够判断出数据错误,并返回错误信息,同时处理该错误时不会影响到接下来的接收过程
  • 完整性
    在保证以上两点的情况下尽可能减少数据丢失

实际上,在STM32的串口中,我们接收数据通常会采用以下两种方法

  • 串口中断接收
    当每接收到一个字节时发生一次中断,处理一个字节,通常会编写一个有限状态机(FSM)去识别和处理一帧数据。这种方法是最原始的,最通用的接收方法,但是每接收一个字节都需要占用内核时间,同时不容易编写发现和处理错误的代码段,因此目前没有被我们使用
  • 串口DMA接收
    DMA是直接内存访问(Direct Memory Access)的缩写。顾名思义,它是一个直接访问处理内存数据的外设,通过预先配置可以让该外设自动传输内存中的数据而无需内核干预。在串口的应用中,在接收到一个字节后DMA将串口数据寄存器中的数据直接复制到指定内存地址中,这个内存地址可以预先设定,并且可以配置DMA在每次完成传输后地址自增1,直到达到预设的数据长度为止。于是我们可以设定一块缓冲区,在正确配置DMA的情况下即可实现将接收到的数据连续填入这个缓冲区内,整个过程由DMA控制器完成。DMA本身可以配置在传输数据过半(完成预先设定的数据长度的一半)或者完成所有传输后发出中断,以便内核及时处理缓冲区内的数据。

串口DMA的接收方案

通常DMA执行的模式有单次传输,循环传输(在完成缓冲区末尾传输后回到开头继续传输),触发内核数据处理的有DMA的传输过半中断,传输完成中断,以及串口空闲中断。所谓串口空闲指的是在串口中一个传输时间内没有数据,这一情况可以作为一个帧的末尾(我们一般认为串口数据总是一个接着一个的,如果出现了空闲就意味着发送结束)的标志,产生中断,此时缓冲区中极有可能出现至少完整的一帧。这几种DMA配置和中断配置可以组合出以下几种实用的方案

单个DMA缓冲(适用于固定帧长,处理一帧的速度远远大于数据速率的情况)

这种方案是将DMA配置为一次传输,缓冲区长度设为一个帧长。在一次传输完成时发出DMA传输完成中断,因此在中断时都会接收到完整一帧长度的数据,此时相比于串口中断的再做处理会更加简单,在中断末尾完成后还需要手动重启DMA,使下一次的数据也能正确接收。这种方案的劣势在于容错性差,主要有以下几个方面的考虑:

  • 在每次接收数据中如果出现多一个字节或者少一个字节的情况这种方案会让数据包中的某个字节丢失。
  • 其次如果存在数据速率突发增大的情况,即出现在DMA完成中断中还没配置启动下一次DMA传输就有数据来的情况会出现数据丢失
  • 在DMA启动恰好是串口发送一帧数据的中间时刻的情况下,不仅在第一次会出现不可避免的数据丢失,而且在接下来DMA传输完成时总是会在一帧数据中间结束,下一次启动时又有可能会在一帧数据发送的中间时刻启动,因此最坏的情况下这种方案总是无法正确接收到一帧(当然,这一点可以通过空闲中断发现错误并重置缓冲区纠正)

DMA循环双缓冲+空闲中断(适用于固定帧长,固定发送频率的改进方案)

这种方案是为了解决上述高速接收数据丢失和同步的问题。该方案将DMA设为循环模式,缓冲区长度设为两倍帧长,通过串口空闲中断(也可以通过DMA传输过半中断判断,只不过依然会出现上面的问题)触发一帧数据处理。在接收到完整一帧后触发串口空闲中断,此时再通过确认接收到的数据长度是否为一帧长度即可及时发现错误,同时两倍缓冲区长度使得在内核处理一帧时,即使第二帧马上发送仍然能够无丢失地接收,因此可以处理突发数据接收。在完成一帧数据处理后不需要重新配置DMA(除非数据出错,需要重置DMA缓冲区接收长度)。DMA循环双缓冲+空闲中断的方式通过增加内存占用,大大提升了容错能力,目前主要作为遥控器串口或图传串口的接收

图片

DMA循环长缓冲+空闲中断(适用于不定长数据,突发数据接收)

通常为了在时间上接收连续会采用DMA循环模式,这种方案就是简单地给出一块足够大的循环缓冲区给DMA不断写入,通过串口空闲中断粗略确定帧结尾后在中断内进一步处理。但是这种方案会带来内存数据上的不连续,即会出现完整的一帧数据因为缓冲区的回绕在内存空间中断开,这一点需要额外的处理。例如用双倍缓冲区长度而只让DMA使用前面一块作为缓冲区,当出现回绕时,把开头的数据复制到后一块未使用的缓冲区内即可。或者按字节为单位处理缓冲区内的数据。劣势同上,在于占用更多内存。