16. DMA—直接存储区访问

本章参考资料:《dm00327659-stm32mp157-advanced-armbased-32bit-mpus-stmicroelectronics》DMA控制器章节。

学习本章时,配合《dm00327659-stm32mp157-advanced-armbased-32bit-mpus-stmicroelectronics》DMA控制器章节一起阅读, 效果会更佳,特别是涉及到寄存器说明的部分。 本章内容专业名称较多,内容丰富也较难理解,但非常有必要细读研究。

特别说明,本章内容是以STM32MP157系列资源讲解。

16.1. DMA简介

DMA(Direct Memory Access,直接存储区访问)为实现数据高速在外设寄存器与存储器之间或者存储器与存储器之间传输提供了高效的方法。 之所以称之为高效, 是因为DMA传输实现高速数据移动过程无需任何CPU操作控制。从硬件层次上来说,DMA控制器是独立于内核的, 有点类似GPIO、USART外设一般,只是DMA的功能是可以快速移动内存数据。

STM32MP157的DMA功能齐全,工作模式众多,适合不同编程环境要求。STM32MP1xx系列的DMA支持外设到存储器传输、 存储器到外设传输和存储器到存储器传输三种传输模式。 这里的外设一般指外设的数据寄存器,比如ADC、SPI、I2C、DCMI等等外设的数据寄存器, 存储器一般是指片内SRAM、外部存储器等。

外设到存储器传输就是把外设数据寄存器内容转移到指定的内存空间。 比如进行ADC采集时我们可以利用DMA传输把AD转换数据转移到我们定义的存储区中, 这样对于多通道采集、采样频率高、连续输出数据的AD采集是非常高效的处理方法。

存储区到外设传输就是把特定存储区内容转移至外设的数据寄存器中,这种多用于外设的发送通信。

存储器到存储器传输就是把一个指定的存储区内容拷贝到另一个存储区空间。 功能类似于C语言内存拷贝函数memcpy,利用DMA传输可以达到更高的传输效率, 特别是DMA传输是不占用CPU的,可以节省很多CPU资源。

16.2. DMA功能框图

STM32MP157的DMA可以实现外设寄存器与存储器之间或者存储器与存储器之间传输三种模式, 这要得益于DMA控制器是采用AHB主总线的,可以控制AHB总线矩阵来启动AHB事务。 图 DMA框图 为DMA控制器的框图。

DMA框图

16.2.1. ①外设通道选择

STM32MP157系列资源丰富,具有两个DMA控制器, 同时外设繁多,为实现正常传输, DMA需要通道选择控制。DMA控制器具有8个数据流, 每个数据流可以提供多达一百多个外设请求,映射情况参考表格 DMA请求来源 。 外设通道选择要解决的主要问题是决定哪一个外设作为该数据流的源地址或者目标地址。

DMA请求来源

每一个数据流都支持上述中的DMA请求,比如SPI3_RX请求,即SPI3数据发送请求, 可以选择使用DMA1的数据流0,数据流1,数据流2等等。

16.2.2. ②仲裁器

一个DMA控制器对应8个数据流,数据流包含要传输数据的源地址、目标地址、数据等等信息。 如果我们需要同时使用同一个DMA控制器(DMA1或DMA2)多个外设请求时, 那必然需要同时使用多个数据流,那究竟哪一个数据流具有优先传输的权利呢? 这就需要仲裁器来管理判断了。

仲裁器管理数据流方法分为两个阶段。第一阶段属于软件阶段,我们在配置数据流时可以通过寄存器设定它的优先级别, 具体配置DMA_SxCR寄存器PL[1:0]位,可以设置为非常高、高、中和低四个级别。 第二阶段属于硬件阶段,如果两个或以上数据流软件设置优先级一样,则他们优先级取决于数据流编号, 编号越低越具有优先权,比如数据流2优先级高于数据流3。

16.2.3. ③FIFO

每个数据流都独立拥有四级32位FIFO(先进先出存储器缓冲区)。DMA传输具有FIFO模式和直接模式。

直接模式在每个外设请求都立即启动对存储器传输。在直接模式下,如果DMA配置为存储器到外设传输那DMA会见一个数据存放在FIFO内, 如果外设启动DMA传输请求就可以马上将数据传输过去。

FIFO用于在源数据传输到目标地址之前临时存放这些数据。 可以通过DMA数据流xFIFO控制寄存器DMA_SxFCR的FTH[1:0]位来控制FIFO的阈值, 分别为1/4、1/2、3/4和满。如果数据存储量达到阈值级别时,FIFO内容将传输到目标中。

FIFO对于要求源地址和目标地址数据宽度不同时非常有用,比如源数据是源源不断的字节数据, 而目标地址要求输出字宽度的数据,即在实现数据传输时同时把原来4个8位字节的数据拼凑成一个32位字数据。 此时使用FIFO功能先把数据缓存起来,分别根据需要输出数据。

FIFO另外一个作用使用于突发(burst)传输。

16.2.4. ④存储器端口、⑤外设端口

DMA控制器实现双AHB主接口,更好利用总线矩阵和并行传输。 DMA控制器通过存储器端口和外设端口与存储器和外设进行数据传输,

DMA控制器的功能是快速转移内存数据,需要一个连接至源数据地址的端口和一个连接至目标地址的端口。

DMA2(DMA控制器2)的存储器端口和外设端口都是连接到AHB总线矩阵,可以使用AHB总线矩阵功能。 DMA2存储器和外设端口可以访问相关的内存地址,包括有内部Flash、内部SRAM、AHB1外设、 AHB2外设、APB2外设和外部存储器空间。

16.2.5. ⑥编程端口

AHB从器件编程端口是连接至AHB外设的。AHB外设在使用DMA传输时需要相关控制信号。

16.3. DMA数据配置

DMA工作模式多样,具有多种可能工作模式,具体可能配置见表 DMA配置可能情况

DMA配置可能情况

16.3.1. DMA传输模式

DMA支持三种传输模式, 模式选择可以通过DMA_SxCR寄存器的DIR[1:0]位控制, 进而将DMA_SxCR寄存器的EN位置1就可以使能DMA传输。

在DMA_SxCR寄存器的PSIZE[1:0]和MSIZE[1:0]位分别指定外设和存储器数据宽度大小, 可以指定为字节(8位)、半字(16位)和字(32位),我们可以根据实际情况设置。 直接模式要求外设和存储器数据宽度大小一样,实际上在这种模式下DMA数据流直接使用PSIZE,MSIZE不被使用。

16.3.2. 源地址和目标地址

DMA数据流x外设地址DMA_SxPAR(x为0~7)寄存器用来指定外设地址,它是一个32位数据有效寄存器。 DMA数据流x存储器0地址DMA_SxM0AR(x为0~7) 寄存器和DMA数据流x存储器1地址DMA_SxM1AR(x为0~7)寄存器用来存放存储器地址, 其中DMA_SxM1AR只用于双缓冲模式,DMA_SxM0AR和DMA_SxM1AR都是32位数据有效的。

当选择外设到存储器模式时,即设置DMA_SxCR寄存器的DIR[1:0] 位为“00”,DMA_SxPAR寄存器为外设地址, 也是传输的源地址,DMA_SxM0AR寄存器为存储器地址,也是传输的目标地址。 对于存储器到存储器传输模式,即设置DIR[1:0]位为“10”时,采用与外设到存储器模式相同配置。而对于存储器到外设, 即设置DIR[1:0]位为“01”时,DMA_SxM0AR寄存器作为为源地址,DMA_SxPAR寄存器作为目标地址。

16.3.3. 流控制器

流控制器主要涉及到一个控制DMA传输停止问题。DMA传输在DMA_SxCR寄存器的EN位被置1后就进入准备传输状态, 如果有外设请求DMA传输就可以进行数据传输。很多情况下,我们明确知道传输数据的数目, 比如要传1000个或者2000个数据,这样我们就可以在传输之前设置DMA_SxNDTR寄存器为要传输数目值, DMA控制器在传输完这么多数目数据后就可以控制DMA停止传输。

DMA数据流x数据项数DMA_SxNDTR(x为0~7)寄存器用来记录当前仍需要传输数目, 它是一个16位数据有效寄存器,即最大值为65535,这个值在程序设计是非常有用也是需要注意的地方。 我们在编程时一般都会明确指定一个传输数量,在完成一次数目传输后DMA_SxNDTR计数值就会自减, 当达到零时就说明传输完成。

如果某些情况下在传输之前我们无法确定数据的数目,那DMA就无法自动控制传输停止了, 此时需要外设通过硬件通信向DMA控制器发送停止传输信号。 这里有一个大前提就是外设必须是可以发出这个停止传输信号,只有SDIO才有这个功能, 其他外设不具备此功能。

16.3.4. 循环模式

循环模式相对应于一次模式。一次模式就是传输一次就停止传输,下一次传输需要手动控制, 而循环模式在传输一次后会自动按照相同配置重新传输,周而复始直至被控制停止或传输发生错误。

通过DMA_SxCR寄存器的CIRC位可以使能循环模式。

16.3.5. 传输类型

DMA传输类型有单次(Single)传输和突发(Burst)传输。突发传输就是用非常短时间结合非常高数据信号率传输数据, 相对正常传输速度,突发传输就是在传输阶段把速度瞬间提高,实现高速传输,在数据传输完成后恢复正常速度, 有点类似达到数据块“秒传”效果。为达到这个效果突发传输过程要占用AHB总线, 保证要求每个数据项在传输过程不被分割,这样一次性把数据全部传输完才释放AHB总线; 而单次传输时必须通过AHB的总线仲裁多次控制才传输完成。

单次和突发传输数据使用具体情况参考表 DMA传输类型 。 其中PBURST[1:0]和MBURST[1:0]位是位于DMA_SxCR寄存器中的, 用于分别设置外设和存储器不同节拍数的突发传输, 对应为单次传输、4个节拍增量传输、8个节拍增量传输和16个节拍增量传输。 PINC位和MINC位是寄存器DMA_SxCR寄存器的第9和第10位,如果位被置1则在每次数据传输后数据地址指针自动递增, 其增量由PSIZE和MSIZE值决定,比如,设置PSIZE为半字大小,那么下一次传输地址将是前一次地址递增2。

DMA传输类型

突发传输与FIFO密切相关,突发传输需要结合FIFO使用,具体要求FIFO阈值一定要是内存突发传输数据量的整数倍。 FIFO阈值选择和存储器突发大小必须配合使用,具体参考表 FIFO阈值配置

FIFO阈值配置

16.3.6. 直接模式

默认情况下,DMA工作在直接模式,不使能FIFO阈值级别。

直接模式在每个外设请求都立即启动对存储器传输的单次传输。直接模式要求源地址和目标地址的数据宽度必须一致, 所以只有PSIZE控制,而MSIZE值被忽略。突发传输是基于FIFO的所以直接模式不被支持。 另外直接模式不能用于存储器到存储器传输。

在直接模式下,如果DMA配置为存储器到外设传输那DMA会见一个数据存放在FIFO内, 如果外设启动DMA传输请求就可以马上将数据传输过去。

16.3.7. 双缓冲模式

设置DMA_SxCR寄存器的DBM位为1可启动双缓冲传输模式,并自动激活循环模式。 双缓冲不应用与存储器到存储器的传输。双缓冲模式下,两个存储器地址指针都有效, 即DMA_SxM1AR寄存器将被激活使用。开始传输使用DMA_SxM0AR寄存器的地址指针所对应的存储区, 当这个存储区数据传输完DMA控制器会自动切换至DMA_SxM1AR寄存器的地址指针所对应的另一块存储区, 如果这一块也传输完成就再切换至DMA_SxM0AR寄存器的地址指针所对应的存储区,这样循环调用。

当其中一个存储区传输完成时都会把传输完成中断标志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双缓冲模式传输数据就可以非常好的解决这个问题,达到解码和输出音频数据到音频电路同步进行的效果。

16.3.8. DMA中断

每个DMA数据流可以在发送以下事件时产生中断:

1) 达到半传输: DMA数据传输达到一半时HTIF标志位被置1,如果使能HTIE中断控制位将产生达到半传输中断;

2) 传输完成: DMA数据传输完成时TCIF标志位被置1,如果使能TCIE中断控制位将产生传输完成中断;

3) 传输错误: DMA访问总线发生错误或者在双缓冲模式下试图访问“受限”存储器地址寄存器时TEIF标志位被置1,如果使能TEIE中断控制位将产生传输错误中断;

4) FIFO错误: 发生FIFO下溢或者上溢时FEIF标志位被置1,如果使能FEIE中断控制位将产生FIFO错误中断;

5) 直接模式错误:在外设到存储器的直接模式下,因为存储器总线没得到授权,使得先前数据没有完成被传输到存储器空间上,此时DMEIF标志位被置1, 如果使能DMEIE中断控制位将产生直接模式错误中断。

16.4. 软件设计

本次实验在原来串口实验的基础上修改,将原来串口发送部分替换成使用DMA进行传输,当收到一帧数据之后启用DMA将数据再次发送出去。

在原来的串口的基础上增加DMA外设相关配置,DMA1默认分配给A7内核因此,M4内核默认只能使用DMA2。选择add添加DMA请求,此处选择USART3_TX。

DMA相关配置1

接下来进行一步设置,整个设置的界面如下所示。我们可在这里配置DMA的优先,数据宽度以及传输模式等,这里都选择默认即可。

DMA相关配置2

在NVIC中已经默认开启DMA中断,如下图所示

DMA相关配置3

到这里关于DMA的串口传输已经设置完毕。

16.4.1. DMA相关结构体

DMA在STM32CubeIDE上的设置很简单,但我们也应该了解DMA外设相关的结构体,知道各个成员的 意义,以便能够运用自如地使用各个外设。

DMA_HandleTypeDef外设管理结构体(stm32mp1xx_hal_dma.h文件)

stm32mp1xx_hal_dma.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 typedef struct __DMA_HandleTypeDef {
     DMA_Stream_TypeDef              *Instance; /*!< 外设寄存器基地址*/
     DMA_InitTypeDef                 Init;   /*!< DMA初始化结构体*/
     HAL_LockTypeDef                 Lock;   /*!< DMA锁资源*/
     __IO HAL_DMA_StateTypeDef       State;  /*!< DMA工作状态*/
     void *Parent;/*!<父对象*/
     /*!< DMA传输完成回调函数*/
     void (* XferCpltCallback)( struct __DMA_HandleTypeDef * hdma);
     /*!< DMA半传输回调函数*/
     void (* XferHalfCpltCallback)( struct __DMA_HandleTypeDef * hdma);
     /*!< DMA缓冲区1完成传输完成回调函数*/
     void (* XferM1CpltCallback)( struct __DMA_HandleTypeDef * hdma);
     /*!< DMA缓冲区1半传输回调函数*/
     void (* XferM1HalfCpltCallback)( struct __DMA_HandleTypeDef * hdma);
     /*!< DMA传输错误回调函数*/
     void (* XferErrorCallback)( struct __DMA_HandleTypeDef * hdma);
     /*!< DMA传输终止回调函数*/
     void (* XferAbortCallback)( struct __DMA_HandleTypeDef * hdma);
     __IO uint32_t               ErrorCode;/*!< DMA错误执行操作返回值*/
     uint32_t                    StreamBaseAddress;/*!< DMA数据流基地址*/
     uint32_t                    StreamIndex;      /*!< DMA数据流的下标*/
     DMAMUX_Channel_TypeDef      *DMAmuxChannel;   /*!< DMAMUX通道基地址*/
                                         /*!< DMAMUX通道状态寄存器地址*/
     DMAMUX_ChannelStatus_TypeDef *DMAmuxChannelStatus;
     uint32_t                    DMAmuxChannelStatusMask;/*!< DMAMUX通道状态值掩码*/
     DMAMUX_RequestGen_TypeDef   *DMAmuxRequestGen; /*!< DMAMUX请求生成器外设基地址 */
     DMAMUX_RequestGenStatus_TypeDef  *DMAmuxRequestGenStatus;/*!< DMAMUX请求生成器状态寄存器基地址*/
     uint32_t                     DMAmuxRequestGenStatusMask; /*!< DMAMUX请求生成器状态值掩码*/
 } DMA_HandleTypeDef;

1) Instance:指向DMA数据流基地址的指针, 即指定使用哪个DMA数据流。可选数据流0至数据流7。

2) Init:DMA初始化结构体,用来配置DMA的请求, 以及传输数据的个数等,具体请看下面的介绍。

3) Lock:DMA的锁资源。DMA进程锁, 通常都在DMA传输设置开始前锁上进程锁,设置完毕后释放进程锁。

4) State:DMA传输状态。它包含六种状态,1、复位状态,尚未初始化或者禁能。2、就绪状态,已经完成初始化, 随时可以传输数据。3、传输忙,DMA传输进程正在进行。4、传输超时状态。5、传输错误状态。6、传输中止状态。

5) Parent:父类指针。 只要将该指针指向一些ADC、UART等外设的handle类,就等于完成了继承。

6) XferCpltCallback、XferHalfCpltCallback、XferM1CpltCallback、XferM1HalfCpltCallback、XferErrorCallback、XferAbortCallback: 分别对应着DMA传输完成的回调函数,DMA半传输回调函数,DMA缓冲区1完成传输完成回调函数,DMA缓冲区1半传输回调函数, DMA传输错误回调函数和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数据流索引, 根据数据流的序号来确定数据流的偏移地址。

10) DMAmuxChannel:DMAMUX的外设基地址, DMAMUX主要是用来配置DMA的外设请求。

11) DMAmuxChannelStatus: DMAMUX的状态寄存器基地址。

12) DMAmuxChannelStatusMask: DMAMUX的状态值掩码,用来屏蔽某些无关的数据位。

13) DMAmuxRequestGen: DMAMUX的请求产生寄存器基地址。

14) DMAmuxRequestGenStatus: DMAMUX的请求产生状态寄存器基地址

15) DMAmuxRequestGenStatusMask: DMAMUX的请求产生的状态值掩码,用来屏蔽某些无关的数据位。

DMA_InitTypeDef初始化结构体

stm32mp1xx_hal_dma.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 typedef struct {
     uint32_t Request;               /*!< DMA请求*/
     uint32_t Direction;            /*!< 传输方向*/
     uint32_t PeriphInc;            /*!< 外设地址递增*/
     uint32_t MemInc;               /*!< 存储器地址递增*/
     uint32_t PeriphDataAlignment;  /*!< 外设数据宽度*/
     uint32_t MemDataAlignment;     /*!< 内存数据宽度*/
     uint32_t Mode;                 /*!< DMA模式选择*/
     uint32_t Priority;             /*!< 优先级*/
     uint32_t FIFOMode;             /*!< FIFO模式*/
     uint32_t FIFOThreshold;        /*!< FIFO阈值*/
     uint32_t MemBurst;             /*!< 存储器突发传输*/
     uint32_t PeriphBurst;          /*!< 外设突发传输*/
 } DMA_InitTypeDef;

Request:DMA请求选择,每个外设对应固定的值,具体设置值需要查;它设定DMAmuxChannel的寄存器CCR的SPOL[3:0]位的值。 例如,我们使用模拟数字转换器ADC3规则采集4个输入通道的电压数据,查

1) 可知编号为115。

2) Direction:传输方向选择,可选外设到存储器、存储器到外设以及存储器到存储器。它设定DMA_SxCR寄存器的DIR[1:0]位的值。 ADC采集显然使用外设到存储器模式。

3) PeriphInc:如果配置为PeriphInc_Enable,使能外设地址自动递增功能,它设定DMA_SxCR寄存器的PINC位的值; 一般外设都是只有一个数据寄存器,所以一般不会使能该位。ADC3的数据寄存器地址是固定并且只有一个所以不使能外设地址递增。

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采集传输是直接模式,要求使用单次模式。

16.4.2. DMA初始化函数

MX_DMA_Init函数

在MX_DMA_Init函数中,只开启了DMA相关的时钟以及配置DMA的中断优先级,并没dma初始化相关的代码, 如下所示。

dma.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 void MX_DMA_Init(void)
 {

     __HAL_RCC_DMAMUX_CLK_ENABLE();
     __HAL_RCC_DMA2_CLK_ENABLE();

     HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 0, 0);
     HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);

 }

而是在usart.c文件中初始化了有关于串口传输的dma,相较于之前的串口初始化相关代码,新添加的代码如下

usart.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 DMA_HandleTypeDef hdma_usart3_tx;

 void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
 {
     /* .................省略....................*/

     hdma_usart3_tx.Instance = DMA2_Stream0;                 //DMA通道,也是DMA2的基地址
     hdma_usart3_tx.Init.Request = DMA_REQUEST_USART3_TX;//DMA请求
     hdma_usart3_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;//传输方式
     hdma_usart3_tx.Init.PeriphInc = DMA_PINC_DISABLE;       //外设地址递增
     hdma_usart3_tx.Init.MemInc = DMA_MINC_ENABLE;   //存储器地址递增
     hdma_usart3_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;  //外设地址宽度
     hdma_usart3_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;             //内存数据宽度
     hdma_usart3_tx.Init.Mode = DMA_NORMAL;                  //DMA模式选择
     hdma_usart3_tx.Init.Priority = DMA_PRIORITY_LOW;        //优先级
     hdma_usart3_tx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;//FIFO模式
     if (HAL_DMA_Init(&hdma_usart3_tx) != HAL_OK)    //初始化DMA外设
     {
         Error_Handler();
     }

     __HAL_LINKDMA(uartHandle,hdmatx,hdma_usart3_tx);  //将DMA与串口结构体互相关联

     /* .................省略....................*/
 }

这段代码设置了DMA传输的各种配置,并使用HAL_DMA_Init初始化串口相关的DMA参数。 最后使用__HAL_LINKDMA 将DMA_HandleTypeDef与UART_HandleTypeDef相互关联。

16.4.3. UART DMA常用函数

常用的串口DMA操作函数有以下几个

stm32mp1xx_hal_uart.c
1
2
3
4
5
 HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size); //启用DMA串口发送
 HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);//启用DMA串口接收
 HAL_StatusTypeDef HAL_UART_DMAPause(UART_HandleTypeDef *huart);     //暂停DMA传输
 HAL_StatusTypeDef HAL_UART_DMAResume(UART_HandleTypeDef *huart);    //恢复DMA传输
 HAL_StatusTypeDef HAL_UART_DMAStop(UART_HandleTypeDef *huart);      //停止DMA传输

根据函数的名字不难猜到各个函数的作用,在本次工程上使用DMA将数据传输到串口3上,因此我们会使用到HAL_UART_Transmit_DMA函数。 在HAL_UART_Transmit_DMA设置各种DMA回调函数以及串口发送完成回调函数,当有中断产生时会调用相对应的回调函数。

有以下几种回调函数可供我们编写使用。

stm32mp1xx_hal_uart.c
1
2
3
__weak void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);  //UART DMA传输完成
__weak void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart);/UART DMA传输完成一半
__weak void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)    //串口出错

只要重写上面几个回调函数即可实现我们想要的功能。

16.4.4. 中断服务函数

回调函数是在中断服务函数中被调用,在DMA2_Stream0_IRQHandler中断函数服务函数中,调用了 HAL_DMA_IRQHandler通用的DMA中断处理函数,如下所示,

stm32mp1xx_it.c
1
2
3
4
 void DMA2_Stream0_IRQHandler(void)
 {
     HAL_DMA_IRQHandler(&hdma_usart3_tx);
 }

而在HAL_DMA_IRQHandler函数中,根据判断相应的中断标志位,调用相对应的中断处理函数, 在中断处理函数处理完毕(清除中断标志位等)就会调用相应的回调函数。由于整个HAL_DMA_IRQHandler 处理函数较为复杂,需要使用大量的篇幅去介绍,这里只做简单介绍。有兴趣的读者可深入了解整个函数是怎么 实现的。

16.4.5. 添加用户代码

本次实验在原来串口实验的基础上修改,将原来串口发送部分替换成使用DMA进行传输, 因此只需要在原来的基础上做简单修改即可。主要修改bsp_debug_usart.c文件如下

bsp_debug_usart.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
 #include "./usart/bsp_debug_usart.h"

 uint8_t rx_buf[RX_MAX];      //串口3接收数据存储
 uint8_t rx_flag = 0;                 //当一帧数据接收完成之后置1


 //重定义printf函数
 int __io_putchar(int ch)
 {
     HAL_UART_Transmit(&UartHandle, (uint8_t*)&ch, 1, HAL_MAX_DELAY);
     return ch;
 }

 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) //接收完成回调函数
 {

     static uint8_t *ucTemp = rx_buf;
     uint16_t bufSize;

     //接收到字符串结束符时,并重新接收
     if( *ucTemp == '\n')
     {
         bufSize = ucTemp - rx_buf +1;       //接收的字符大小
         ucTemp = rx_buf;
         HAL_UART_Receive_IT(huart, ucTemp, 1);
         rx_flag = 1;

         //启动DMA传输
         HAL_UART_Transmit_DMA(huart, ucTemp, bufSize);
     }
     else
     {
         ucTemp++;
         HAL_UART_Receive_IT(huart, ucTemp, 1);
     }
 }

 void Usart_Config(void)      //配置函数
 {
     //启用串口3接收
     HAL_UART_Receive_IT(&UartHandle, rx_buf, 1);
 }

 //串口发送函数
 void Usart_SendString(uint8_t *str)
 {
     unsigned int k=0;
     while (*(str + k)!='\0')
     {
         HAL_UART_Transmit( &UartHandle,(uint8_t *)(str + k) ,1,HAL_MAX_DELAY);
         k++;
     }
 }

主要修改了HAL_UART_RxCpltCallback串口接收回调函数的内容,当接收到完整的一帧数据之后 启动UART DMA的传输,使得数据从串口中发送。DMA数据的传输是不需要CPU参与的,因此在发送大量数据时并不会占用 CPU资源。

16.4.6. 主函数

main.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 const char *Buf = " --------野火stm32mp157 Cortex-M4内核串口DMA实验--------\n";

 int main(void)
 {

     HAL_Init();

     if(IS_ENGINEERING_BOOT_MODE())
     {
         SystemClock_Config();
     }

     MX_GPIO_Init();
     MX_DMA_Init();
     MX_USART3_UART_Init();

     Main_Config();

     HAL_UART_Transmit_DMA(&UartHandle, (uint8_t *)Buf, strlen(Buf));

     while (1)
     {
         // Main_Task();
     }

 }

main函数的代码内容改动不多,只是多了DMA初始化函数,同时将串口发送函数替换成了使用DMA传输, 并且在while循环中注释掉了Main_Task()函数,在while循环中什么都不做。

16.4.7. 下载验证

将编译好的程序下载到板子运行。在实验现象上和串口实验一模一样, DMA传输适用于频繁的数据传输情况。

实验现象