目录
DMA简介
DMA(Direct Memory Access,直接存储区访问)为实现数据高速在外设寄存器与存储器之间或者存储器与存储器之间传输提供了高效的方法。之所以称之为高效,是因为DMA 传输实现高速数据移动过程无需任何CPU 操作控制。从硬件层次上来说,DMA 控制器是独立于Cortex-M4 内核的,有点类似GPIO、USART 外设一般,只是DMA 的功能是可以快速移动内存数据。
DMA数据传输的四个要素
- 传输源:DMA数据传输的来源
- 传输目标:DMA数据传输的目的
- 传输数量:DMA数据传输的数量
- 触发信号:启动一次DMA数据传输的动作
STM32F4xx系列的DMA特点
STM32F4xx 系列的DMA 功能齐全,工作模式众多,适合不同编程环境要求。
STM32F4xx 系列的DMA 支持外设到存储器传输、存储器到外设传输和存储器到存储器传输三种传输模式。这里的外设一般指外设的数据寄存器,比如ADC、SPI、I2C、DCMI 等等外设的数据寄存器,存储器一般是指片内SRAM、外部存储器、片内Flash 等等。
外设到存储器传输就是把外设数据寄存器内容转移到指定的内存空间。比如进行ADC采集时我们可以利用DMA 传输把AD 转换数据转移到我们定义的存储区中,这样对于多通道采集、采样频率高、连续输出数据的AD 采集是非常高效的处理方法。
存储区到外设传输就是把特定存储区内容转移至外设的数据寄存器中,这种多用于外设的发送通信。
存储器到存储器传输就是把一个指定的存储区内容拷贝到另一个存储区空间。功能类似于C 语言内存拷贝函数memcpy,利用DMA 传输可以达到更高的传输效率,特别是DMA 传输是不占用CPU 的,可以节省很多CPU 资源。
一、STM32的DMA框图
STM32F4xx系列的DMA可以实现三种传输方式,这主要是因为DMA控制器是基于AHB总线的,可以控制总线矩阵来启动AHB事务。
DMA 控制器提供两个 AHB 主端口:AHB 存储器端口(用于连接存储器)和 AHB 外设端口(用于连接外设)。但是,要执行存储器到存储器的传输,AHB 外设端口必须也能访问存储器。
AHB 从端口用于对 DMA 控制器进行编程(它仅支持 32 位访问)。
DMA1 控制器 AHB 外设端口与 DMA2 控制器的情况不同,不连接到总线矩阵,因此,仅 DMA2 数据流能够执行存储器到存储器的传输。
1.外设通道选择
STM32F4xx系列资源丰富,具有两个DMA控制器,同时外设繁多,为实现正常传输,DMA 需要通道选择控制。每个DMA控制器具有8个数据流,每个数据流对应8个外设请求。每个数据流都与一个 DMA 请求相关联,此 DMA 请求可以从 8 个可能的通道请求中选出。
外设通道选择要解决的主要问题是决定哪一个外设作为该数据流的源地址或者目标地址。
2.仲裁器
一个DMA 控制器对应8 个数据流,数据流包含要传输数据的源地址、目标地址、数据等等信息。如果我们需要同时使用同一个DMA 控制器(DMA1 或DMA2)多个外设请求时,那必然需要同时使用多个数据流,那究竟哪一个数据流具有优先传输的权利呢?这就需要仲裁器来管理判断了。
仲裁器管理数据流方法分为两个阶段。第一阶段属于软件阶段,我们在配置数据流时可以通过寄存器设定它的优先级别,具体配置DMA_SxCR 寄存器PL[1:0]位,可以设置为非常高、高、中和低四个级别。第二阶段属于硬件阶段,如果两个或以上数据流软件设置优先级一样,则他们优先级取决于数据流编号,编号越低越具有优先权,比如数据流2 优先级高于数据流3。
3.FIFO
DMA 传输具有FIFO模式和直接模式。
直接模式在每个外设请求都立即启动对存储器传输。在直接模式下,如果DMA配置为存储器到外设传输,DMA会将一个数据从存储器预加载在FIFO 内,从而确保一旦外设触发DMA传输请求就可以马上将数据传输过去。为了避免FIFO饱和,建议使用高优先级配置相应数据流。当实现存储器到存储器传输时不得使用直接模式。
FIFO用于在源数据传输到目标之前临时存储这些数据。每个数据流都有一个独立的4字FIFO,阈值级别可由软件配置为1/4、1/2、3/4或满。如果数据存储量达到阈值级别时,FIFO内容将传输到目标。
FIFO 对于要求源地址和目标地址数据宽度不同时非常有用,比如源数据是源源不断的字节数据,而目标地址要求输出字宽度的数据,即在实现数据传输时同时把原来4 个8 位字节的数据拼凑成一个32 位字数据。此时使用FIFO 功能先把数据缓存起来,分别根据需要输出数据。
FIFO 另外一个作用使用于突发(burst)传输。
4.端口
存储器端口、外设端口
DMA控制器实现双AHB主接口,更好利用总线矩阵和并行传输。DMA控制器通过存储器端口和外设端口与存储器和外设进行数据传输,DMA控制器的功能是快速转移内存数据,需要一个连接至源数据地址的端口和一个连接至目标地址的端口。
DMA2(DMA 控制器2)的存储器端口和外设端口都是连接到AHB 总线矩阵,可以使用AHB 总线矩阵功能。DMA2 存储器和外设端口可以访问相关的内存地址,包括有内部Flash、内部SRAM、AHB1 外设、AHB2 外设、APB2 外设和外部存储器空间。
DMA1 的存储区端口相比DMA2 的要减少AHB2 外设的访问权,同时DMA1 外设端口是没有连接至总线矩阵的,只有连接到APB1 外设,所以DMA1 不能实现存储器到存储器传输。
编程端口
AHB从器件编程端口是连接至AHB2外设的。AHB2外设在使用DMA传输时需要相关控制信号。
二、DMA数据配置
DMA 工作模式多样,具有多种可能工作模式,具体可能配置见图
8 个 DMA 控制器数据流都能够提供源和目标之间的单向传输链路,每个数据流可以配置为不同的传输源和传输目标,这些传输源和目标称为通道
每个数据流配置后都可以执行:
● 常规类型事务:存储器到外设、外设到存储器或存储器到存储器的传输。
● 双缓冲区类型事务:使用存储器的两个存储器指针的双缓冲区传输(当 DMA 正在进行自/至缓冲区的读/写操作时,应用程序可以进行至/自其它缓冲区的写/读操作)。
要传输的数据量(多达 65535)可以编程,并与连接到外设 AHB 端口的外设(请求DMA传输)的源宽度相关。每个事务完成后,包含要传输的数据项总量的寄存器都会递减。
1.源、目标和传输模式
源传输和目标传输在整个 4 GB 区域(地址在 0x0000 0000 和 0xFFFF FFFF 之间)都可以 寻址外设和存储器。
DMA2 支持全部三种传输模式,而DMA1 只有外设到存储器和存储器到外设两种模式。传输方向使用 DMA_SxCR 寄存器中的 DIR[1:0] 位进行配置。
注意:使用存储器到存储器模式时,不允许循环模式和直接模式。只有 DMA2 控制器能够执行存储器到存储器的传输。
在DMA_SxCR 寄存器的PSIZE[1:0]和MSIZE[1:0]位分别指定外设和存储器数据宽度
大小,可以指定为字节(8 位)、半字(16 位)和字(32 位),我们可以根据实际情况设置。直接
模式要求外设和存储器数据宽度大小一样,实际上在这种模式下DMA 数据流直接使用
PSIZE,MSIZE 不被使用。
2.指针递增
根据 DMA_SxCR 寄存器中 PINC 和 MINC 位的状态,外设和存储器指针在每次传输后可以 自动向后递增或保持常量。
通过单个寄存器访问外设源或目标数据时,禁止递增模式十分有用。
如果使能了递增模式,则根据在 DMA_SxCR 寄存器 PSIZE 或 MSIZE 位中编程的数据宽度,下一次传输的地址将是前一次传输的地址递增 1(对于字节)、2(对于半字)或 4(对 于字)。
为了优化封装操作,可以不管 AHB 外设端口上传输的数据的大小,将外设地址的增量偏移大小固定下来。DMA_SxCR 寄存器中的 PINCOS 位用于将增量偏移大小与外设 AHB 端口或 32 位地址(此时地址递增 4)上的数据大小对齐。PINCOS 位仅对 AHB 外设端口有影响。
如果将 PINCOS 位置 1,则不论 PSIZE 值是多少,下一次传输的地址总是前一次传输的地址递增 4(自动与 32 位地址对齐)。但是,AHB 存储器端口不受此操作影响。
如果 AHB 外设端口或 AHB 存储器端口分别请求突发事务,为了满足 AMBA 协议(在固定地址模式下不允许突发事务),则需要将 PINC 或 MINC 位置 1。
3.流控制器
控制要传输的数据数目的实体称为流控制器。此流控制器使用 DMA_SxCR 寄存器中的 PFCTRL 位针对每个数据流独立配置。
流控制器主要涉及到一个控制DMA 传输停止问题。DMA 传输在DMA_SxCR 寄存器的EN 位被置1 后就进入准备传输状态,如果有外设请求DMA 传输就可以进行数据传输。
很多情况下,我们明确知道传输数据的数目,比如要传1000 个或者2000 个数据,这样我们就可以在传输之前设置DMA_SxNDTR 寄存器为要传输数目值,DMA 控制器在传输完这么多数目数据后就可以控制DMA 停止传输。
- DMA控制器:DMA 数据流x 数据项数DMA_SxNDTR(x 为0~7)寄存器用来记录当前仍需要传输数目,它是一个16 位数据有效寄存器,即最大值为65535,这个值在程序设计是非常有用也是需要注意的地方。我们在编程时一般都会明确指定一个传输数量,在完成一次数目传输后DMA_SxNDTR 计数值就会自减,当达到零时就说明传输完成。
- 外设源或目标:如果某些情况下在传输之前我们无法确定数据的数目,那DMA 就无法自动控制传输停止了,此时需要外设通过硬件通信向DMA 控制器发送停止传输信号。这里有一个大前提就是外设必须是可以发出这个停止传输信号,只有SDIO 才有这个功能,其他外设不具备此功能。
- 注意:使用存储器到存储器模式时,DMA 始终是流控制器,而 PFCTRL 位由硬件强制置为 0。在外设流控制器模式下禁止循环模式。
4.循环模式
循环模式相对应于一次模式。一次模式就是传输一次就停止传输,下一次传输需要手动控制,而循环模式在传输一次后会自动按照相同配置重新传输,周而复始直至被控制停止或传输发生错误。
循环模式可用于处理循环缓冲区和连续数据流(例如 ADC 扫描模式)。可以使用 DMA_SxCR寄存器中的 CIRC 位使能此特性。当激活循环模式时,要传输的数据项的数目在数据流配置阶段自动用设置的初始值进行加载,并继续响应 DMA 请求。
5.传输类型
DMA 传输类型有单次(Single)传输和突发(Burst)传输。
DMA 控制器可以产生单次传输或 4 个、8 个和 16 个节拍的增量突发传输。
突发大小通过软件针对两个 AHB 端口独立配置,配置时使用 DMA_SxCR 寄存器中的 MBURST[1:0] 和 PBURST[1:0] 位。突发大小指示突发中的节拍数,而不是传输的字节数。
突发传输就是用非常短时间结合非常高数据信号率传输数据,相对正常传输速度,突发传输就是在传输阶段把速度瞬间提高,实现高速传输,在数据传输完成后恢复正常速度,有点类似达到数据块“秒传”效果。为达到这个效果突发传输过程要占用AHB 总线,保证要求每个数据项在传输过程不被分割,这样一次性把数据全部传输完才释放AHB 总线;而单次传输时必须通过AHB 的总线仲裁多次控制才传输完成。
注意:仅在使能指针递增模式时允许突发模式:
- 当 PINC 位为“0”时,也应将 PBURST 位清为“00”
- 当 MINC 位为“0”时,也应将 MBURST 位清为“00”
6.直接模式
默认情况下,DMA 工作在直接模式,不使能FIFO 阈值级别。
直接模式在每个外设请求都立即启动对存储器传输的单次传输。直接模式要求源地址和目标地址的数据宽度必须一致,所以只有PSIZE 控制,而MSIZE 值被忽略。突发传输是基于FIFO 的所以直接模式不被支持。另外直接模式不能用于存储器到存储器传输。
在直接模式下,如果DMA 配置为存储器到外设传输,那DMA将会预加载一个数据到FIFO 内,如果外设启动DMA传输请求就可以马上将数据传输过去。
7.双缓冲模式
此模式可用于所有 DMA1 和 DMA2 数据流。设置DMA_SxCR 寄存器的DBM位为1 可启动双缓冲传输模式。除了有两个存储器指针之外,双缓冲区数据流的工作方式与常规(单缓冲区)数据流的一样。使能双缓冲区模式时,将自动使能循环模式(DMA_SxCR 中的 CIRC 位的状态是“无关”),并在每次事务结束时交换存储器指针。
在此模式下,每次事务结束时,DMA 控制器都从一个存储器目标交换为另一个存储器目标。 这样,软件在处理一个存储器区域的同时,DMA 传输还可以填充/使用第二个存储器区域。双缓冲区数据流可以双向工作(存储器既可以是源也可以是目标)。
当其中一个存储区传输完成时都会把传输完成中断标志TCIF 位置1,如果我们使能了DMA_SxCR 寄存器的传输完成中断,则可以产生中断信号,这个对我们编程非常有用。另外一个非常有用的信息是DMA_SxCR 寄存器的CT 位,当DMA 控制器是在访问使用DMA_SxM0AR 时CT=0,此时CPU 不能访问DMA_SxM0AR,但可以向DMA_SxM1AR填充或者读取数据;当DMA控制器是在访问使用DMA_SxM1AR 时CT=1,此时CPU 不能访问DMA_SxM1AR,但可以向DMA_SxM0AR 填充或者读取数据。另外在未使能DMA 数据流传输时,可以直接写CT 位,改变开始传输的目标存储区。
双缓冲模式应用在需要解码程序的地方是非常有效的。比如MP3 格式音频解码播放,MP3 是被压缩的文件格式,我们需要特定的解码库程序来解码文件才能得到可以播放的PCM信号,解码需要一定的实际,按照常规方法是读取一段原始数据到缓冲区,然后对缓冲区内容进行解码,解码后才输出到音频播放电路,这种流程对CPU 运算速度要求高,很容易出现播放不流畅现象。如果我们使用DMA 双缓冲模式传输数据就可以非常好的解决这个问题,达到解码和输出音频数据到音频电路同步进行的效果。
8.DMA中断
对于每个 DMA 数据流,可在发生以下事件时产生中断:
- 达到半传输:DMA 数据传输达到一半时HTIF 标志位被置1,如果使能HTIE 中断控制位将产生达到半传输中断;
- 传输完成:DMA 数据传输完成时TCIF 标志位被置1,如果使能TCIE 中断控制位将产生传输完成中断;
- 传输错误:DMA 访问总线发生错误或者在双缓冲模式下试图访问“受限”存储器地址寄存器时TEIF 标志位被置1,如果使能TEIE 中断控制位将产生传输错误中断;
- FIFO 错误(上溢、下溢或 FIFO 级别错误):发生FIFO 下溢或者上溢时FEIF 标志位被置1,如果使能FEIE 中断控制位将产生FIFO 错误中断;
- 直接模式错误:在外设到存储器的直接模式下,因为存储器总线没得到授权,使得先前数据没有完成被传输到存储器空间上,此时DMEIF 标志位被置1,如果使能DMEIE 中断控制位将产生直接模式错误中断。
注意:在将使能控制位置‘1’前,应将相应的事件标志清零,否则会立即产生中断。
三、HAL库中的DMA
1.DMA_InitTypeDef初始化结构体
/**
* @brief DMA Configuration Structure definition
*/
typedef struct
{
uint32_t Channel; /*通道选择*/
uint32_t Direction; /*传输方向*/
uint32_t PeriphInc; //外设递增
uint32_t MemInc; /*存储器递增*/
uint32_t PeriphDataAlignment; /*外设数据宽度*/
uint32_t MemDataAlignment; /*存储器数据宽度*/
uint32_t Mode; /*模式选择 */
uint32_t Priority; /*优先级*/
uint32_t FIFOMode; /*FIFO模式*/
uint32_t FIFOThreshold; /*FIFO阈值*/
uint32_t MemBurst; /*存储器突发传输数据大小 */
uint32_t PeriphBurst; /*外设突发传输数据大小 */
}DMA_InitTypeDef;
- 1)Chanel:DMA 请求通道选择,可选通道0 至通道7,每个外设对应固定的通道,具体设置值需要查阅芯片参考手册的DMA请求映射表;它设定DMA_SxCR 寄存器的CHSEL[2:0]位的值。例如,我们使用模拟数字转换器ADC规则采集4 个输入通道的电压数据,需要查表得到使用的通道。
- 2)Direction:传输方向选择,可选外设到存储器、存储器到外设以及存储器到存储器。它设定DMA_SxCR 寄存器的DIR[1:0]位的值。ADC 采集显然使用外设到存储器模式。
- 3)PeriphInc:如果配置为PeriphInc_Enable,使能外设地址自动递增功能,它设定DMA_SxCR 寄存器的PINC 位的值;一般外设都是只有一个数据寄存器,所以一般不会使能该位。ADC的数据寄存器地址是固定并且只有一个所以不使能外设地址递增。
- 4)MemInc:如果配置为MemInc_Enable,使能存储器地址自动递增功能,它设定DMA_SxCR 寄存器的MINC 位的值;我们自定义的存储区一般都是存放多个数据的,所以使能存储器地址自动递增功能。我们可以定义一个包含4个元素的数组用来存放数据,使能存储区地址递增功能,自动把每个通道数据存放到对应数组元素内。
- 5)PeriphDataAlignment:外设数据宽度,可选字节(8 位)、半字(16 位)和字(32 位),它设定DMA_SxCR 寄存器的PSIZE[1:0]位的值。ADC 数据寄存器只有低16 位数据有效,使用半字数据宽度。
- 6)MemDataAlignment:存储器数据宽度,可选字节(8 位)、半字(16 位)和字(32 位),它设定DMA_SxCR 寄存器的MSIZE[1:0]位的值。保存ADC 转换数据也要使用半字数据宽度,这跟我们定义的数组是相对应的。
- 7)Mode:DMA 传输模式选择,可选一次传输或者循环传输,它设定DMA_SxCR 寄存器的CIRC 位的值。我们希望ADC 采集是持续循环进行的,所以使用循环传输模式。
- 8)Priority:软件设置数据流的优先级,有4 个可选优先级分别为非常高、高、中和低,它设定DMA_SxCR 寄存器的PL[1:0]位的值。DMA 优先级只有在多个DMA数据流同时使用时才有意义,这里我们使用默认设置。
- 9)FIFOMode:FIFO 模式使能,如果设置为DMA_FIFOMode_Enable 表示使能FIFO模式功能;它设定DMA_SxFCR 寄存器的DMDIS 位。ADC 采集传输使用直接传输模式即可,不需要使用FIFO 模式。
- 10)FIFOThreshold:FIFO 阈值选择,可选4 种状态分别为FIFO 容量的1/4、1/2、3/4和满;它设定DMA_SxFCR 寄存器的FTH[1:0]位; DMA_FIFOMode 设置为DMA_FIFOMode_Disable,那DMA_FIFOThreshold 值无效。ADC 采集传输不使用FIFO 模式,设置该值无效。
- 11)MemBurst:存储器突发模式选择,可选单次模式、4 节拍的增量突发模式、8 节拍的增量突发模式或16 节拍的增量突发模式,它设定DMA_SxCR 寄存器的MBURST[1:0]位的值。ADC 采集传输是直接模式,要求使用单次模式。
- 12)PeriphBurst:外设突发模式选择,可选单次模式、4 节拍的增量突发模式、8 节拍的增量突发模式或16 节拍的增量突发模式,它设定DMA_SxCR 寄存器的PBURST[1:0]位的值。ADC 采集传输是直接模式,要求使用单次模式。
2.DMA_HandleTypeDef 初始化结构体
/**
* @brief DMA handle Structure definition
*/
typedef struct __DMA_HandleTypeDef
{
DMA_Stream_TypeDef *Instance; /*数据流句柄实例*/
DMA_InitTypeDef Init; /* DMA参数初始化 */
HAL_LockTypeDef Lock; /*DMA锁定对象 */
__IO HAL_DMA_StateTypeDef State; /* DMA传输状态 */
void *Parent; /*父类指针 */
void (* XferCpltCallback)( struct __DMA_HandleTypeDef * hdma); /*DMA传输完成回调函数*/
void (* XferHalfCpltCallback)( struct __DMA_HandleTypeDef * hdma); /*DMA半传输完成回调函数*/
void (* XferM1CpltCallback)( struct __DMA_HandleTypeDef * hdma); /*Memory1 DMA传输完成回调函数*/
void (* XferM1HalfCpltCallback)( struct __DMA_HandleTypeDef * hdma); /*Memory1 DMA半传输完成回调函数*/
void (* XferErrorCallback)( struct __DMA_HandleTypeDef * hdma); /*DMA传输错误回调函数*/
void (* XferAbortCallback)( struct __DMA_HandleTypeDef * hdma); /*DMA传输中断回调函数*/
__IO uint32_t ErrorCode; /*!< DMA错误码 */
uint32_t StreamBaseAddress; /*!< DMA数据流基地址*/
uint32_t StreamIndex; /*!< DMA数据流索引 */
}DMA_HandleTypeDef;
- 1)*Instance: 指向DMA 数据流基地址的指针,即指定使用哪个DMA 数据流。可选数据流0 至数据流7。
- 2)Init:这里包含上面介绍DMA_InitTypeDef 结构体的所有参数的初始化。
- 3)Lock:DMA 锁定对象。DMA 进程锁,通常都在DMA 传输设置开始前锁上进程锁,设置完毕后释放进程锁。
- 4)State:DMA 传输状态。它包含六种状态,1、复位状态,尚未初始化或者禁能。2、就绪状态,已经完成初始化,随时可以传输数据。3、传输忙,DMA 传输进程正在进行。4、传输超时状态。5、传输错误状态。6、传输中止状态。
- 5)*Parent:父类指针。只要将该指针指向一些ADC、UART 等外设的handle 类,就等于完成了继承。
- 6)DMA 传输过程中的回调函数。包括传输完成,传输完成一半,传输错误,传输中止回调函数。这些回调函数中可以加入用户的处理代码。
- 7)ErrorCode:DMA 错误码,包含无错误:HAL_DMA_ERROR_NONE,传输错误HAL_DMA_ERROR_TE,FIFO 错误HAL_DMA_ERROR_FE,直接模式错误:HAL_DMA_ERROR_DME,超时错误:HAL_DMA_ERROR_TIMEOUT,参数错误:HAL_DMA_ERROR_PARAM,没有回调函数正在执行退出请求错误:HAL_DMA_ERROR_NO_XFER,不支持模式错误:HAL_DMA_ERROR_NOT_SUPPORTED。。
- 8)StreamBaseAddress:DMA 数据流基地址,用来根据定义句柄计算数据流的基地址。
- 9)StreamIndex:DMA 数据流索引,根据数据流的序号来确定数据流的偏移地址。
3.DMA常用接口函数
stm32f4xx_hal_dma.h
文件中定义的DMA接口函数
常用的宏定义函数
/* @brief Enable the specified DMA Stream.*/
#define __HAL_DMA_ENABLE(__HANDLE__) ((__HANDLE__)->Instance->CR |= DMA_SxCR_EN)
/* @brief Disable the specified DMA Stream. */
#define __HAL_DMA_DISABLE(__HANDLE__) ((__HANDLE__)->Instance->CR &= ~DMA_SxCR_EN)
//Returns the number of remaining data units in the current DMAy Streamx transfer.
#define __HAL_DMA_GET_COUNTER(__HANDLE__) ((__HANDLE__)->Instance->NDTR)
// Enable the specified DMA Stream interrupts.
#define __HAL_DMA_ENABLE_IT(__HANDLE__, __INTERRUPT__) (((__INTERRUPT__) != DMA_IT_FE)? \
((__HANDLE__)->Instance->CR |= (__INTERRUPT__)) : ((__HANDLE__)->Instance->FCR |= (__INTERRUPT__)))
//Clear the DMA Stream pending flags.
#define __HAL_DMA_CLEAR_FLAG(__HANDLE__, __FLAG__) \
(((uint32_t)((__HANDLE__)->Instance) > (uint32_t)DMA2_Stream3)? (DMA2->HIFCR = (__FLAG__)) :\
((uint32_t)((__HANDLE__)->Instance) > (uint32_t)DMA1_Stream7)? (DMA2->LIFCR = (__FLAG__)) :\
((uint32_t)((__HANDLE__)->Instance) > (uint32_t)DMA1_Stream3)? (DMA1->HIFCR = (__FLAG__)) : (DMA1->LIFCR = (__FLAG__)))
常用的C函数
HAL_StatusTypeDef HAL_DMA_Init(DMA_HandleTypeDef *hdma);
HAL_StatusTypeDef HAL_DMA_DeInit(DMA_HandleTypeDef *hdma);
HAL_StatusTypeDef HAL_DMA_Start(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength);
HAL_StatusTypeDef HAL_DMA_Start_IT(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength);
HAL_StatusTypeDef HAL_DMA_Abort(DMA_HandleTypeDef *hdma);
HAL_StatusTypeDef HAL_DMA_Abort_IT(DMA_HandleTypeDef *hdma);
void HAL_DMA_IRQHandler(DMA_HandleTypeDef *hdma);
stm32f4xx_hal_uart.h
文件中定义的串口DMA操作函数
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_DMAPause(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_UART_DMAResume(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_UART_DMAStop(UART_HandleTypeDef *huart);
- HAL_UART_Transmit_DMA函数:该函数将启动DMA方式的串口数据发送,发送完成后,触发DMA中断,调用中断回调函数 HAL_UART_TxCpltCallback进行后续处理
- HAL_UART_Receive_DMA函数:该函数将启动DMA方式的串口数据接收,接收完成后,触发DMA中断,调用中断回调函数 HAL_UART_RxCpltCallback进行后续处理
示例
使用中断的时候实现了可变长的数据帧报的发送。我们也用DMA实现下。
1.空闲中断
- 在一帧数据传输结束后,通信线路将会维持高电平,这个状态称为空闲状态;
- 当CPU 检测到通信线路处于空闲状态时,且空闲状态持续时间大于一个字节传输时间时,空闲状态标志IDLE将由硬件置 1。如果串口控制寄存器 CR1中的 IDLEIE位为 1,将会触发空闲中断(IDLE中断);
- 由于空闲标志是在一帧数据传输完成后才置位,在有效数据传输过程中不会置位,因此借助空闲中断,可以实现不定长数据的收发。
时序图
将中断实现的帧头帧尾作为一帧数据,即0xaa、0x55两个字符的帧数据,其传输时序图如下:
从图中可以看到,在发送该帧数据之前和之后,数据线为高电平,处于 IDLE 状态。在该帧有效数据发送过程中,即 0xaa 字节与 0x55 字节之间,没有 IDLE 状态出现,这样利用 IDLE 状态就可以判断不定长数据的发送结束。
2.设计思路
- 使能 IDLE 中断,在串口的中断服务程序 UART1_IRQHandler 中添加对 IDLE 中断的判断,该函数位于 stm32f4xx_it.c 文件;
- 设置传输模式为普通模式,启动 DMA 传输。串口一旦接收到数据, 则触发 DMA 操作,将数据存放到用户定义的接收缓冲区;
- 当一帧数据发送完成后,线路处于 IDLE 状态,将触发 IDLE中断,调用IDLE中断回调函数,设置数据接收完成标志;
- 主程序检测到接收完成标志置位后,将接收的一帧数据原样发回PC ,并禁能 DMA ,以触发 DMA 中断。 DMA 中断将调用接收中断回调函数,在回调函数中重新启动 DMA 传输。
3.使用CubeMX配置
- 选择目标芯片进入配置界面,将基础配置完成后,进入串口配置,选择异步模式,进入参数配置,添加串口接收和串口发送的DMA数据流
- 设置串口接收发送的DMA参数
通道、方向以及优先级由MX自动配置
传输方式使用普通模式;地址递增外设不递增,存储器递增;FIFO不使能;数据宽度为字节。
除了自动配置的,发送与接收的DMA参数相同。
- 使能串口中断与DMA中断
DMA中断由MX自动使能,我们需要手动使能串口中断。 - 生成工程代码
4.编写用户代码
- 添加空闲中断处理程序
在stm32l4xx_it.c
文件中的串口1中断服务函数中添加空闲中断判断
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
//添加IDLE中断处理
if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE) != RESET) //是否发送IDLE
{
__HAL_UART_CLEAR_IDLEFLAG(&huart1);//清除IDLE中断标志
HAL_UART_IdleCpltCallback(&huart1);//调用自行实现的IDLE中断回调函数
}
/* USER CODE END USART1_IRQn 1 */
}
- 定义用户变量与添加用户函数
在main.c
文件中定义如下变量
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
#define LENGTH 100 //接收缓冲区大小,该值为帧数据总字符数的阈值
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
extern DMA_HandleTypeDef hdma_usart1_rx;
uint8_t RxBuffer[LENGTH];//接收缓冲区
uint8_t Recount = 0;//接收数据个数
uint8_t RxFlag = 0;//接收完成标志:0未完成,1完成
/* USER CODE END PV */
- 在
main.h
文件中添加IDLE中断回调函数声明
/* USER CODE BEGIN EFP */
void HAL_UART_IdleCpltCallback(UART_HandleTypeDef *huart);
/* USER CODE END EFP */
- 添加如下初始化代码
/* USER CODE BEGIN 2 */
printf("******** UART communication using IDLE IT +DMA**********\r\n");
__HAL_UART_ENABLE_IT(&huart1,UART_IT_IDLE);//使能IDLE中断
HAL_UART_Receive_DMA(&huart1,(uint8_t*)RxBuffer,LENGTH);//启动DMA接收
/* USER CODE END 2 */
- 后台while循环中程序处理
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if(RxFlag == 1)
{
RxFlag =0;// 清除标志位
//发生空闲中断时,已接受数据个数等于数据总量减去DMA数据流中待接收的数据个数
Recount = LENGTH - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
//采用DMA方式原样发回PC
HAL_UART_Transmit_DMA(&huart1,(uint8_t*)RxBuffer,Recount);
Recount = 0;
//设置DMA Disable,触发DMA中断,调用接收中断回调函数来重新启动下一次DMA接收
__HAL_DMA_DISABLE(&hdma_usart1_rx);
}
}
/* USER CODE END 3 */
- 编写中断回调函数
/* USER CODE BEGIN 4 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
HAL_UART_Receive_DMA(&huart1,(uint8_t*)RxBuffer,LENGTH);//启动DMA接收
}
}
void HAL_UART_IdleCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
RxFlag = 1;//置位接收完成标志
HAL_UART_DMAStop(&huart1);
}
}
/* USER CODE END 4 */