1)实验平台:正点原子STM32MP157开发板
2)购买链接:https://item.taobao.com/item.htm?&id=629270721801
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-318813-1-1.html
4)正点原子官方B站:https://space.bilibili.com/394620890
5)正点原子STM32MP157技术交流群:691905614
串口可以通过轮询、中断以及DMA的方式接收数据,在前面串口通信实验章节,我们学习了如何使用UART中断接收数据,本章节,我们将介绍STM32MP157的DMA,我们将利用DMA来实现串口数据传送。
本章分为如下几个小节:
21.1、 DMA简介
21.2、 硬件设计
21.3、 软件设计
21.4、 编译和测试
21.1 DMA简介
系统的核心是CPU,CPU无时无刻在处理着大量的事物,如复制数据、转移数据、计算等等,对于一些事情,可以让CPU以外的其它模块去执行,那么CPU就可以腾出部分资源去处理其它事情,这样可以更好地利用CPU的资源。要处理的这些事物中,例如对于转移数据,其实也可以不需要CPU的参与,可以直接使用DMA来处理。
DMA,全称为:Direct Memory Access,即直接存储器访问。DMA传输将数据从一个地址空间复制到另外一个地址空间,通过硬件为RAM与I/O设备开辟一条直接传送数据的通路,传输动作本身是由 DMA 控制器来实行和完成,没有中断处理方式那样保留现场和恢复现场的过程,无需任何CPU操作就可以实现高速数据移动,这使空余出的CPU资源可专注于处理其他更加实用或者紧急的操作,从而使CPU的效率大大提高了。
图21.1. 1数据传输方式
DMA就是为了解决大量数据转移而过多消耗CPU资源而产生的,DMA的作用就是实现数据的直接传输,去掉了传统的数据传输需要经过CPU处理的环节,如果没有DMA,那么CPU传输还需要以内核作为中转站,特别是在转移大量数据时,占用更多的CPU资源。
以ADC外设采集数据为例,当ADC采集到数据时,ADC外设向DMA控制器发送一个请求信号,DMA收到请求后,触发DMA工作,如果此时DMA控制器接收到多个请求信号,DMA控制器会根据通道的优先权来处理请求,优先权高的优先处理。DMA控制器从AHB外设获取ADC采集到的数据,暂时存储到DMA通道中,然后再通过AHB将DMA通道的数据传送到SRAM中,完成一次数据转移,数据转移的过程不需要CPU的参与。
21.1.1 STM32MP157 DMA简介
图21.1.1. 1图 DMA控制器数据流和通道关系
STM32MP157的双口DMA有以下一些特性:
① 双AHB主总线架构,一个用于存储器访问,另一个用于外设访问;
② 仅支持32位访问的AHB从编程接口
③ 每个DMA控制器有8个数据流,每个数据流有多达116个通道(或称请求)
④ 每个数据流有单独的四级32位先进先出存储器缓冲区(FIFO),可用于FIFO模式或直接模式。
⑤ 通过硬件可以将每个数据流配置为:
1,常规通道,支持外设到内存,内存到外设和内存到内存的传输。
2,支持在存储器方双缓冲的双缓冲区通道
⑥ 8个数据流中的每一个都连接到专用硬件DMA通道(请求);
⑦ DMA 数据流请求之间的优先级可用软件编程(4个级别:非常高、高、中、低),在软件优先级相同的情况下可以通过硬件决定优先级(例如,请求0的优先级高于请求1);
⑧ 每个数据流也支持通过软件触发存储器到存储器的传输;
⑨ 每个数据流的通道请求可以由DMA请求复用器(DMAMUX)选择,可供选择的通道请求数多达116个。此选择可由软件配置DMAMUX,并允许108个外设发起DMA请求;
⑩ 要传输的数据项的数目可以由DMA控制器或外设管理:
1,DMA 流控制器:要传输的数据项的数目是1到65535,可用软件编程
2,外设流控制器:要传输的数据项的数目未知并由源或目标外设控制,这些外设通过硬件发出传输结束的信号
⑪ 独立的源和目标传输宽度(字节、半字、字):源和目标的数据宽度不相等时,DMA自动封装/解封必要的传输数据来优化带宽。这个特性仅在FIFO模式下可用。
⑫ 对源和目标的增量或非增量寻址
⑬ 支持4个、8个和16个节拍的增量突发传输。突发增量的大小可由软件配置,通常等于外设FIFO大小的一半
⑭ 每个数据流都支持循环缓冲区管理
⑮ 5个事件标志(DMA 半传输、DMA 传输完成、DMA 传输错误、DMA FIFO 错误、直接模式错误),进行逻辑或运算,从而产生每个数据流的单个中断请求
21.1.2 DMA框图
图21.1.2. 1 DMA控制器框图
图中,我们标记了6处位置,起作用分别是:
①,DMA控制器的从机编程接口,通过该接口可以对DMA的相关控制寄存器进行设置,从而配置DMA,实现不同的功能。同时,该接口可以输出dma_it[0:7]的中断信号到NVIC(对于M4内核,中断控制器是NVIC;对于A7内核,中断控制器是GIC。本文我们讲解的是M4内核部分),以及dma_tcif[0:7]的信号到MDMA。
②,DMA控制器的外设接口,用于访问相关外设,特别的,当外设接口设置的访问地址是内存地址的时候,DMA就可以工作在内存到内存模式了。
③,DMA控制器的FIFO区,可以实现存储器接口到外设接口之间的数据长度非对齐传输。
每个数据流(总共8个数据流)都有一个独立的FIFO(先进先出存储器缓冲区),FIFO用于临时存放要传输的数据,可通过DMA_SxFCR寄存器来控制控制 FIFO的阈值,如果数据存储量达到阈值时,FIFO中的数据将传输到目标中。
④,DMA控制器的存储器接口,用于访问外部存储器,特别的当存储器地址是外设地址的时候,可以实现类似外设到外设的传输效果。
⑤,DMA控制器的仲裁器,用于仲裁数据流0~7的请求优先级,保证数据有序传输。
⑥,这是DMA控制器数据流0~7的通道请求信号,由DMAMUX的选择,每个数据流有多达116个通道请求可以选择。我们必须根据实际需求来选择对应的通道请求。
2. DMA请求复用器(DMAMUX)
STM32MP157资源丰富,外设众多,为实现正常传输,需要通过DMAMUX对通道资源进行 管理。这里重点介绍一下DMAMUX,其全称是DMA请求复用器,用于管理DMA1和DMA2的通道请求。DMAMUX总共有16个通道,其中通道07对应DMA1的数据流07,通道815对应DMA2的数据流07。DMAMUX框图如下所示:
图21.1.2. 2 DMAMUX控制器框图
整个DMAMUX的功能比较复杂,包括:选择具体的DMA请求通道(源)、同步控制、请求生成、请求计数和中断等,本章我们只用到其最简单的应用:选择具体的DMA请求通道,即通过DMAMUX选择DMA1/DMA2的数据流通道(请求通道)。为了方便说明,我们在图中标出了几个关键点:
①,外设请求输入,即STM32MP157芯片内部外设的请求,这部分总共有p+1个外设请求,对于DMAMUX来说,总共有108个(p=107)。
②,通道选择,通过DMAMUX_CmCR寄存器的DMAREQ_ID[7:0]位来选择DMA的具体请求通道,总共有116个通道,其中①处有108个,还有8个通道来自DMAMUX内部的请求生成器,我们一般用①处的108个请求通道。
③,同步控制,用于同步DMA请求,最终输出给DMA控制器,我们本章不使用同步控制,因此可以关闭同步(通过DMAMUX_CmCR寄存器的SE位设置)。
④,输出到DMA控制器的请求信号,对DMAMUX来说总共有16个输出请求信号(m=15),DMAMUX通道0到7与DMA1通道0到7相连,DMAMUX通道8到15与DMA2通道0到7 相连。
因此,我们一般只需要通过设置DMAMUX通道m的DMAMUX_CmCR寄存器来选择输入通道,即可完成DMA1/DMA2某个数据流的DMA输入请求通道选择。
3. DMA请求映射
DMA通道/请求和外设不是任意连接的,需要根据DMA请求映射表来,DMAMUX的DMA请求资源分配表如下所示(下图只是截图一部分):
图21.1.2. 3 DMA请求资源分配表(部分)
由表可知,DMAMUX的DMA请求输入总共有116个(116之后的是保留的,具体查看参考手册,这里只截图部分,列表见参考手册的Table 110),表中我们省略了一部分内容。
举例来说,假设我们要设置DMA2数据流3的DMA输入请求通道为:串口7的TX(UART7_TX),则:
1,DMA2数据流3对应DMAMUX的通道11(从0开始算起,8~15对应的是DMA2,第4和刚好是通道11)。
2,串口7的TX对应的外设DMA请求编号为:80
因此,我们只需要设置DMAMUX_CxCR的DMAREQ_ID[6:0]位为80即可完成DMA2数据流3的请求来自UART7_TX的设置。
由于DMA1/DMA2数据流0~7的DMA请求全部是来自DMAMUX的设置(16个通道),DMAMUX的所有通道都可以独立设置,因此,对于STM32MP157来说,DMA1/DMA2的任何一个数据流的请求都可以来自116个通道的任意一个,因此相对于STM32其他系列来说,STM32MP157的DMA配置更加灵活,无需固定通道对应固定外设请求,可以随意设置。
21.1.3 DMA寄存器
DMA配置参数包括:通道地址、优先级、数据传输方向、存储器/外设数据宽度、存储器/外设地址、数据传输量等。
图21.1.3. 1 DMAMUX1_CxCR寄存器各位描述(部分)
本章,该寄存器我们只需要关心DMAREQ_ID[6:0]这7个位,其他位全部用不到(设置为0即可),DMAREQ_ID[6:0]用于选择输出通道x的DMA输入请求,我们需要通过这8个位选择DMA1/DMA2数据流的请求源。详细请求列表(DMA请求映射表)见参考手册的Table 110:
图21.1.3. 2 DMA请求资源分配表(部分)
2. DMA中断状态寄存器(DMA_LISR和DMA_HISR)
DMA中断状态寄存器,该寄存器总共有2个:DMA_LISR和DMA_HISR,每个寄存器管理4数据流(总共8个),DMA_LISR寄存器用于管理数据流03,而DMA_HISR用于管理数据流47。这两个寄存器各位描述都完全一模一样,只是管理的数据流不一样。
这里,我们仅以DMA_LISR寄存器为例进行介绍,DMA_LISR各位描述如下图所示:
图21.1.3. 3 DMA_LISR寄存器
如果开启了DMA_LISR中这些位对应的中断,则在达到条件后就会跳到中断服务函数里面去,即使没开启,我们也可以通过查询这些位来获得当前DMA传输的状态。这里我们常用的是TCIFx位,即数据流x的DMA传输完成与否标志。注意此寄存器为只读寄存器,所以在这些位被置位之后,只能通过其他的操作来清除。DMA_HISR寄存器各位描述通DMA_LISR寄存器各位描述完全一样,只是对应数据流4~7,这里我们就不列出来了。
3. DMA中断标志清除寄存器(DMA_LIFCR和DMA_HIFCR)
DMA中断标志清除寄存器, 该寄存器同样有2个:DMA_LIFCR和DMA_HIFCR,同样是每个寄存器控制4个数据流,DMA_LIFCR寄存器用于管理数据流0~3,而DMA_ HIFCR用于管理数据流4~7。这两个寄存器各位描述都完全一模一样,只是管理的数据流不一样。
这里,我们仅以DMA_LIFCR寄存器为例进行介绍,DMA_LIFCR各位描述如下图所示:
图21.1.3. 4 DMA_LIFCR寄存器
DMA_LIFCR的各位就是用来清除DMA_LISR的对应位的,通过写1清除。在DMA_LISR被置位后,我们必须通过向该位寄存器对应的位写入1来清除。DMA_HIFCR的使用同DMA_LIFCR类似,这里就不做介绍了。
第四个是DMA数据流x配置寄存器(DMA_SxCR)(x=0~7,下同)。该寄存器的我们在这里就不贴出来了,见《STM32MP157参考手册》。该寄存器控制着DMA的很多相关信息,包括数据宽度、外设及存储器的宽度、优先级、增量模式、传输方向、中断允许、使能等都是通过该寄存器来设置的。所以DMA_ SxCR是DMA传输的核心控制寄存器。
第五个是DMA数据流x数据项数寄存器(DMA_SxNDTR)。这个寄存器控制DMA数据流x的每次传输所要传输的数据量。其设置范围为0~65535。并且该寄存器的值会随着传输的进行而减少,当该寄存器的值为0的时候就代表此次数据传输已经全部发送完成了。所以可以通过这个寄存器的值来知道当前DMA传输的进度。特别注意,这里是数据项数目,而不是指的字节数。比如设置数据位宽为16位,那么传输一次(一个项)就是2个字节。
第六个是DMA数据流x的外设地址寄存器(DMA_SxPAR)。该寄存器用来存储STM32MP157外设的地址,比如我们使用串口4(UART4),那么该寄存器必须写入0x40010028(其实就是&USART_TDR),即UART4的地址(基地址+偏移地址,基地址= 0x40010000,USART_TDR的偏移地址= 0x28)。如果使用其他外设,就修改成相应外设的地址就行了。
图21.1.3. 5 USART_TDR的地址
最后一个是DMA数据流x的存储器地址寄存器,由于STM32MP157的DMA支持双缓存,所以存储器地址寄存器有两个:DMA_SxM0AR和DMA_SxM1AR,其中DMA_SxM1AR仅在双缓冲模式下,才有效。本章我们没用到双缓冲模式,所以存储器地址寄存器就是:DMA_SxM0AR,该寄存器和DMA_CPARx差不多,但是是用来放存储器的地址的。比如我们使用SendBuf[7800]数组来做存储器,那么我们在DMA_SxM0AR中写入&SendBuff就可以了。
4. DMA数据流x外设地址寄存器(DMA_SxPAR)
DMA数据流x外设地址寄存器如下图所示:
图21.1.3. 6 DMA_SxPAR寄存器
该寄存器存放的是DMA读或者写数据的外设数据寄存器的基址。本实验,我们需要通过DMA读取ADC1转换后存放在ADC1常规数据寄存器 (ADC_DR) 的结果数据。所以我们需要给DMA_SxPAR寄存器写入ADC_DR寄存器的地址。这样配置后,DMA就会从ADC_DR寄存器的地址读取ADC的转换后的数据到某个内存空间。这个内存空间地址需要我们通过DMA_SxM0AR寄存器来设置,比如定义一个变量,把这个变量的地址值写入该寄存器。
注意:DMA_SxPAR寄存器受到写保护,只有DMA_SxCR寄存器中的EN为“0”时才可以写入,即先要禁止数据流传输才可以写入。
5. DMA数据流x存储器0地址寄存器(DMA_SxM0AR)
DMA数据流x存储器0地址寄存器描述如下图所示:
图21.1.3. 7 DMA_SxM0AR寄存器
该寄存器存放的是DMA读或者写数据的存储器的地址。这些位受到写保护,只有当禁止数据流(DMA_SxCR 寄存器中的位 EN=“0”)或使能数据流(DMA_SxCR 寄存器中的 EN=“1”)并且 DMA_SxCR 寄存器中的位 CT =“1”(在双缓冲区模式下)时才可以写入。如果用到双缓冲区模式我们还需要用到DMA_SxM1AR寄存器,本实验我们是用不到的。
6. DMA 数据流 x 配置寄存器 (DMA_SxCR) (x = 0…7)
图21.1.3. 8 DMA_SxCR寄存器
DMA_SxCR寄存器的位比较多,我们先关注几个位,这些位将由软件置 1 和清零,这些位受到保护,只有 EN 为“0”时才可以写入。
PL[1:0]位用于设置优先级:00:低优先级;01:中优先级;10:高优先级;11:非常高优先级。
MSIZE[1:0]用于设置存储器数据大小:00:字节(8 位);01:半字(16 位);10:字(32 位);11:保留。注意:在直接模式下,当位 EN =“1”时,MSIZE 位由硬件强制置为与 PSIZE 相同的值。
PSIZE[1:0]用于外设数据大小:00:字节(8 位);01:半字(16 位);10:字(32 位);11:保留。
DIR[1:0]用于配置数据传输方向:00:外设到存储器;01:存储器到外设;10:存储器到存储器;11:保留。
21.1.4 DMA的HAL库驱动
DMA在HAL库中的驱动代码在stm32mp1xx_hal_dma.c文件(及其头文件)中。UART4相关的HAL库驱动代码在stm32mp1xx_hal_uart.c文件(及其头文件)中。
形参1是DMA_HandleTypeDef结构体类型指针变量,其定义如下:
typedef struct __DMA_HandleTypeDef
{
void *Instance; /* 寄存器基地址 */
DMA_InitTypeDef Init; /* DAM通信参数 */
HAL_LockTypeDef Lock; /* DMA锁对象 */
__IO HAL_DMA_StateTypeDef State; /* DMA传输状态 */
void *Parent; /* 父对象状态,HAL库处理的中间变量 */
void (*XferCpltCallback)( struct __DMA_HandleTypeDef *hdma);/*DMA传输完成回调*/
void (* XferHalfCpltCallback)( struct __DMA_HandleTypeDef * hdma);
/* DMA一半传输完成回调 */
void (* XferM1CpltCallback)( struct __DMA_HandleTypeDef * hdma);
/* DMA传输完整的Memory1回调 */
void (* XferM1HalfCpltCallback)( struct __DMA_HandleTypeDef * hdma);
/* 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数据流索引号 */
DMAMUX_Channel_TypeDef *DMAmuxChannel; /* DMAMUX信道基础地址 */
DMAMUX_ChannelStatus_TypeDef *DMAmuxChannelStatus; /* DMAMUX通道状态基础地址 */
uint32_t DMAmuxChannelStatusMask;/* DMAMUX通道状态掩码 */
DMAMUX_RequestGen_TypeDef *DMAmuxRequestGen;/* DMAMUX请求生成器基地地址 */
DMAMUX_RequestGenStatus_TypeDef *DMAmuxRequestGenStatus;
/* DMAMUX请求生成器状态地址 */
uint32_t DMAmuxRequestGenStatusMask; /* DMAMUX请求生成器状态掩码 */
}DMA_HandleTypeDef;
这个结构体内容比较多,上面已注释中文翻译,下面列出几个成员说明一下。
Instance:是用来设置寄存器基地址,例如要设置为DMA2的数据流7,那么取值为DMA2_Stream7。
Parent:是HAL库处理中间变量,用来指向DMA通道外设句柄。
StreamBaseAddress和StreamIndex是数据流基地址和索引号,这个是HAL库处理的时候会自动计算,用户无需设置。
其他成员变量是HAL库处理过程状态标识变量,这里就不做过多讲解。
接下来我们重点介绍Init,它是DMA_InitTypeDef结构体类型变量,该结构体定义如下:
typedef struct
{
uint32_t Request; /* 请求设置,设置是哪个外设请求的 */
uint32_t Direction; /* 传输方向,例如存储器到外设DMA_MEMORY_TO_PERIPH */
uint32_t PeriphInc; /* 外设(非)增量模式,非增量模式DMA_PINC_DISABLE */
uint32_t MemInc; /* 存储器(非)增量模式,增量模式DMA_MINC_ENABLE */
uint32_t PeriphDataAlignment; /* 外设数据大小:8/16/32位 */
uint32_t MemDataAlignment; /* 存储器数据大小:8/16/32位 */
uint32_t Mode; /* 模式:外设流控模式/循环模式/普通模式 */
uint32_t Priority; /* DMA优先级:低/中/高/非常高 */
uint32_t FIFOMode; /* FIFO模式开启或者禁止 */
uint32_t FIFOThreshold; /* FIFO阈值选择 */
uint32_t MemBurst; /* 存储器突发模式:单次/4个节拍/8个节拍/16个节拍 */
uint32_t PeriphBurst; /* 外设突发模式:单次/4个节拍/8个节拍/16个节拍 */
}DMA_InitTypeDef;
该结构体成员变量非常多,但是每个成员变量配置的基本都是DMA_SxCR寄存器和DMA_SxFCR寄存器的相应位。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
2. 串口的DMA发送函数HAL_UART_Transmit_DMA
函数描述:
使用 DMA 方式发送数据。
函数形参:
形参1是UART_HandleTypeDef结构体类型指针变量;
形参2是要发送的数据地址;
形参3是要发送的数据大小(单位:字节)。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
串口的DMA发送实际是串口控制寄存器USART_CR3的位7来控制的,在HAL库中操作该寄存器来使能串口DMA发送的函数为HAL_UART_Transmit_DMA,该函数声明如下:
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart,
uint8_t *pData, uint16_t Size)
这里大家需要注意,调用该函数后会开启相应的DMA中断。
HAL库还提供了对串口的DMA发送的停止,暂停,继续等操作函数:
HAL_StatusTypeDef HAL_UART_DMAStop(UART_HandleTypeDef *huart); /* 停止 */
HAL_StatusTypeDef HAL_UART_DMAPause(UART_HandleTypeDef *huart); /* 暂停 */
HAL_StatusTypeDef HAL_UART_DMAResume(UART_HandleTypeDef *huart);/* 恢复 */
__HAL_DMA_ENABLE: 使能指定的DMA Stream;
__HAL_DMA_DISABLE: 禁止指定的DMA Stream;
__HAL_DMA_GET_FS: 返回当前DMA Stream FIFO填充情况;
__HAL_DMA_ENABLE_IT: 使能指定的DMA Stream中断;
__HAL_DMA_DISABLE_IT: 禁止指定的DMA Stream中断;
__HAL_DMA_GET_IT_SOURCE: 检查指定的指定的DMA Stream中断是否发送;
__HAL_DMA_GET_COUNTER:获取当前传输剩余数据量;
__HAL_DMA_SET_COUNTER:设置对应的DMA数据流传输的数据量大小。
表21.1.4. 1中断标志位
DMA中断对于每个数据流都有一个中断服务函数,比如DMA2_Stream0的中断服务函数为DMA2_Stream0_IRQHandler。HAL库提供了通用DMA中断处理函数HAL_DMA_IRQHandler,在该函数内部,会对DMA传输状态进行分析,然后调用相应的中断处理回调函数:
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart); /* 发送完成回调函数 */
void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart);/* 发送一半回调函数 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart); /* 接收完成回调函数 */
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart);/* 接收一半回调函数 */
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart); /* 传输出错回调函数 */
注意的是,DMA传输完成回调函数 UART_DMAReceiveCplt会调用HAL_UART_RxCpltCallback函数
21.2 硬件设计
图21.3.1. 1 LED0配置
UART4的两个引脚配置如下:
图21.3.1. 2 UART配置
2. 配置UART4
如下图,采用默认配置,即UART4的波特率为115200Bit/s;字长为8位;无校验位;1位停止位;数据方向为发和收;16倍过采样;Clock Prescaler(时钟预分频器)分频值为1。
图21.3.1. 3 UART4参数配置
实验中我们会用到UART4的中断,如下,开启UART4全局中断:
图21.3.1. 4开启UART4全局中断
3. 配置DMA
我们配置M4内核使用DMA2,从DMA配置处可以看到,而DMA1给A7内核(Cortex-A7 non secure)使用的,DMA1和DMA2可以单独给M4或者A7内核使用:
图21.3.1. 5 DMA配置
点击DMA Settings,再点击Add添加通道:
图21.3.1. 6 添加DMA
DMA2请求选择UART4_RX 和UART4_TX ,数据流可选DMA2 Stream 0~DMA2 Stream 7,这里选DMA2 Stream 0和DMA2 Stream 1,传输方向分别为Peripheral To Memory和Memory To Peripheral,传输速率设置为中速:
图21.3.1. 7配置DMA参数
以上配置项解释如下:
DMA Request 是DMA请求:
即DMA传输的对应外设,这里我们选为UART4的发送端和接收端;
Stream是数据流:
我们前面分析了DMA1和DMA2各有07(共8个)数据流,所以此处可以随意配置数据流07,这里,我们选择DMA2的Stream 0和Stream 1;
Dirction是DMA传输方向,有4种传输方向:
外设到内存 Peripheral To Memory
内存到外设 Memory To Peripheral
内存到内存 Memory To Memory
外设到外设 Peripheral To Peripheral
Priority是传输速度:
可选最高优先级 Very Hight、高优先级 Hight、中等优先级 Medium和低优先级Low;
Mode是DMA的传输模式:
Normal表示正常模式,当一次DMA数据传输完后就停止DMA传送,也就是只传输一次就停止了;
Circular是循环模式,当传输完成后又重新开始继续传输,即不断循环永不停止传输
Increment Address是地址指针递增:
Src Memory 表示外设地址寄存器,设置传输数据的时候外设地址是不变还是递增。如果设置 为递增,那么下一次传输的时候地址加 Data Width个字节。
Dst Memory 表示内存地址寄存器,设置传输数据时候内存地址是否递增。如果设置 为递增,那么下一次传输的时候地址加 Data Width个字节。
串口发送数据是将数据不断存进固定外设地址串口的发送数据寄存器(USARTx_TDR)。所以外设的地址是不递增。而内存储器存储的是要发送的数据,所以地址指针要递增,保证数据依次被发出,串口数据发送寄存器只能存储8bit,每次发送一个字节,所以数据长度选择Byte。
图21.3.1. 8 DMA配置
4. 配置NVIC
UART4的抢占优先级和子优先级我们都设置为3,而DMA是默认开启了中断了,中断优先级默认是0,我们也可以修改此优先级,如下:
图21.3.1. 9 中断优先级配置
5. 配置时钟
时钟可以采用默认的HSI时钟,这里就保持和前面一样的配置,配置PCLK1最大为104.5MHz:
图21.3.1. 10时钟配置
6. 配置生成独立的.c和.h文件
图21.3.1. 11配置生成独立的文件
7. 生成工程
生成工程,本实验会用到LED0的驱动程序,所以将LED0相关的代码也拷贝到工程中:
图21.3.1. 12生成工程
1 #include "dma.h"
2
3 DMA_HandleTypeDef hdma_memtomem_dma2_stream2;/* DMA句柄 */
4
5 /**
6 * @brief DMA初始化函数
7 * @note 使能DMA通道,配置DMA为内存-->内存的模式
8 * @param 无
9 * @retval 无
10 */
11 void MX_DMA_Init(void)
12 {
13 __HAL_RCC_DMAMUX_CLK_ENABLE(); /* DMAMUX时钟使能 */
14 __HAL_RCC_DMA2_CLK_ENABLE(); /* DMA时钟使能 */
15
16 /* Configure DMA request hdma_memtomem_dma2_stream2 on DMA2_Stream2 */
17 /* 数据流选择DMA2_Stream2 */
18 hdma_memtomem_dma2_stream2.Instance = DMA2_Stream2;
19 /* DMAMUX请求,内存到内存传输 */
20 hdma_memtomem_dma2_stream2.Init.Request = DMA_REQUEST_MEM2MEM;
21 /* DMA数据传输方向,内存到内存方向 */
22 hdma_memtomem_dma2_stream2.Init.Direction = DMA_MEMORY_TO_MEMORY;
23 /* DMA外围增量模式启用 */
24 hdma_memtomem_dma2_stream2.Init.PeriphInc = DMA_PINC_ENABLE;
25 /* 存储器增量模式 */
26 hdma_memtomem_dma2_stream2.Init.MemInc = DMA_MINC_ENABLE;
27 /* 外设数据长度:8位 */
28 hdma_memtomem_dma2_stream2.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
29 /* 存储器数据长度:8位 */
30 hdma_memtomem_dma2_stream2.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
31 /* 外设流控模式 */
32 hdma_memtomem_dma2_stream2.Init.Mode = DMA_NORMAL;
33 /* 速度为中等优先级 */
34 hdma_memtomem_dma2_stream2.Init.Priority = DMA_PRIORITY_MEDIUM;
35 /* 关闭FIFO模式 */
36 hdma_memtomem_dma2_stream2.Init.FIFOMode = DMA_FIFOMODE_ENABLE;
37 /* FIFO阈值配置 */
38 hdma_memtomem_dma2_stream2.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL;
39 /* 存储器突发单次传输 */
40 hdma_memtomem_dma2_stream2.Init.MemBurst = DMA_MBURST_SINGLE;
41 /* 外设突发单次传输 */
42 hdma_memtomem_dma2_stream2.Init.PeriphBurst = DMA_PBURST_SINGLE;
43 if (HAL_DMA_Init(&hdma_memtomem_dma2_stream2) != HAL_OK)/* DMA初始化 */
44 {
45 Error_Handler();
46 }
47 /* DMA2_Stream0_IRQn中断配置 */
48 HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 2, 0);
49 HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);
50 /* DMA2_Stream1_IRQn中断配置 */
51 HAL_NVIC_SetPriority(DMA2_Stream1_IRQn, 2, 1);
52 HAL_NVIC_EnableIRQ(DMA2_Stream1_IRQn);
53 }
串口UART4的代码如下:
1 #include "usart.h"
2
3 UART_HandleTypeDef huart4; /* UART4句柄 */
4 DMA_HandleTypeDef hdma_uart4_rx; /* DMA句柄,UART4接收端 */
5 DMA_HandleTypeDef hdma_uart4_tx; /* DMA句柄,UART4发送端 */
6
7 /**
8 * @brief UART4初始化函数
9 * @note 使能DMA通道,配置DMA为内存-->内存的模式
10 * @param 无
11 * @retval 无
12 */
13 void MX_UART4_Init(void)
14 {
15 huart4.Instance = UART4; /* UART4 */
16 huart4.Init.BaudRate = 115200; /* 波特率115200 */
17 huart4.Init.WordLength = UART_WORDLENGTH_8B; /* 字长:8位 */
18 huart4.Init.StopBits = UART_STOPBITS_1; /* 1个停止位 */
19 huart4.Init.Parity = UART_PARITY_NONE; /* 无校验位 */
20 huart4.Init.Mode = UART_MODE_TX_RX; /* 发送和接收模式 */
21 huart4.Init.HwFlowCtl = UART_HWCONTROL_NONE; /* 无硬件流 */
22 huart4.Init.OverSampling = UART_OVERSAMPLING_16; /* 16倍过采样 */
23 /* 没有高级功能初始化 */
24 huart4.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;
25 huart4.Init.ClockPrescaler = UART_PRESCALER_DIV1; /* UART4时钟分频为1 */
26 /* 没有高级功能初始化 */
27 huart4.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;
28 if (HAL_UART_Init(&huart4) != HAL_OK) /* 使用HAL库初始化UART4 */
29 {
30 Error_Handler();
31 }
32 /* FIFO相关的初始化,本届十堰我们没有使用FIFO模式 */
33 if (HAL_UARTEx_SetTxFifoThreshold(&huart4, UART_TXFIFO_THRESHOLD_1_8) != \ HAL_OK)
34 {
35 Error_Handler();
36 }
37 if (HAL_UARTEx_SetRxFifoThreshold(&huart4, UART_RXFIFO_THRESHOLD_1_8) != \ HAL_OK)
38 {
39 Error_Handler();
40 }
41 if (HAL_UARTEx_DisableFifoMode(&huart4) != HAL_OK)
42 {
43 Error_Handler();
44 }
45 }
46 /**
47 * @brief UART4的GPIO引脚初始化函数
48 * @note 使能DMA通道,配置DMA为内存-->内存的模式
49 * @param uartHandle UART指针变量
50 * @retval 无
51 */
52 void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
53 {
54 GPIO_InitTypeDef GPIO_InitStruct = {0};
55 RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
56 if(uartHandle->Instance==UART4)
57 {
58 if(IS_ENGINEERING_BOOT_MODE())
59 {
60 /* 初始化UART4外设时钟 */
61 PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_UART24;
62 PeriphClkInit.Uart24ClockSelection = RCC_UART24CLKSOURCE_PCLK1;
63 if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
64 {
65 Error_Handler();
66 }
67 }
68 __HAL_RCC_UART4_CLK_ENABLE(); /* UART4时钟使能 */
69 __HAL_RCC_GPIOG_CLK_ENABLE(); /* GPIOG时钟使能 */
70 __HAL_RCC_GPIOB_CLK_ENABLE(); /* GPIOB时钟使能 */
71 /**UART4 GPIO Configuration
72 PG11 ------> UART4_TX
73 PB2 ------> UART4_RX
74 */
75 GPIO_InitStruct.Pin = GPIO_PIN_11; /* 引脚11 */
76 GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; /* 复用推挽输出 */
77 GPIO_InitStruct.Pull = GPIO_PULLUP; /* 上拉模式 */
78 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速模式 */
79 GPIO_InitStruct.Alternate = GPIO_AF6_UART4; /* 复用为UART4 */
80 HAL_GPIO_Init(GPIOG, &GPIO_InitStruct); /* 使用HAL库初始化GPIO引脚 */
81
82 GPIO_InitStruct.Pin = GPIO_PIN_2; /* 引脚2 */
83 GPIO_InitStruct.Mode = GPIO_MODE_AF; /* 复用模式 */
84 GPIO_InitStruct.Pull = GPIO_PULLUP; /* 上拉模式 */
85 GPIO_InitStruct.Alternate = GPIO_AF8_UART4; /* 复用为UART4 */
86 HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); /* 使用HAL库初始化GPIO引脚 */
87
88 /**** UART4_RX DMA 初始化部分 ****/
89
90 hdma_uart4_rx.Instance = DMA2_Stream0; /* 数据流选择DMA2_Stream10 */
91 /* DMAMUX请求UART4的接收端 */
92 hdma_uart4_rx.Init.Request = DMA_REQUEST_UART4_RX;
93 /* 数据流方向:外设到内存 */
94 hdma_uart4_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
95 hdma_uart4_rx.Init.PeriphInc = DMA_PINC_DISABLE; /* 外设增量模式禁用 */
96 hdma_uart4_rx.Init.MemInc = DMA_MINC_ENABLE; /*内存增量模式使能 */
97 /* 外设数据长度:8位 */
98 hdma_uart4_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
99 /* 存储器数据长度:8位 */
100 hdma_uart4_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
101 hdma_uart4_rx.Init.Mode = DMA_NORMAL; /* 外设流控模式 */
102 hdma_uart4_rx.Init.Priority = DMA_PRIORITY_MEDIUM; /* 速度为中等优先级 */
103 hdma_uart4_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE; /* 关闭FIFO模式 */
104 /* 使用HAL库初始化UART4的接收端 */
105 if (HAL_DMA_Init(&hdma_uart4_rx) != HAL_OK)
106 {
107 Error_Handler();
108 }
109 /* 使用HAL库初始化UART4的接收端 */
110 __HAL_LINKDMA(uartHandle,hdmarx,hdma_uart4_rx);
111
112 /**** UART4_TX DMA初始化部分 ****/
113 hdma_uart4_tx.Instance = DMA2_Stream1; /* 数据流选择DMA2_Stream1 */
114 /* DMAMUX请求,UART4的发送端 */
115 hdma_uart4_tx.Init.Request = DMA_REQUEST_UART4_TX;
116 /* 数据流方向:内存到外设 */
117 hdma_uart4_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
118 hdma_uart4_tx.Init.PeriphInc = DMA_PINC_DISABLE; /* 外设增量模式禁用 */
119 hdma_uart4_tx.Init.MemInc = DMA_MINC_ENABLE; /*内存增量模式使能 */
120 /* 外设数据长度:8位 */
121 hdma_uart4_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
122 /* 存储器数据长度:8位 */
123 hdma_uart4_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
124 hdma_uart4_tx.Init.Mode = DMA_NORMAL; /* 外设流控模式 */
125 hdma_uart4_tx.Init.Priority = DMA_PRIORITY_MEDIUM; /* 速度为中等优先级 */
126 hdma_uart4_tx.Init.FIFOMode = DMA_FIFOMODE_DISABLE; /* 关闭FIFO模式 */
127 if (HAL_DMA_Init(&hdma_uart4_tx) != HAL_OK)/* 使用HAL库初始化UART4的发送端 */
128 {
129 Error_Handler();
130 }
131 /* 将DMA句柄关联到UART4的接收端 */
132 __HAL_LINKDMA(uartHandle,hdmatx,hdma_uart4_tx);
133 HAL_NVIC_SetPriority(UART4_IRQn, 3, 3); /* UART4的中断优先级设置 */
134 HAL_NVIC_EnableIRQ(UART4_IRQn); /* 使能UART4的中断 */
135 }
136 }
137 /**
138 * @brief UART4的GPIO引脚反初始化(去初始化)函数
139 * @note 如果有必要,可以调用此函数取消UART的初始化
140 * @param uartHandle UART指针变量
141 * @retval 无
142 */
143 void HAL_UART_MspDeInit(UART_HandleTypeDef* uartHandle)
144 {
145 if(uartHandle->Instance==UART4)
146 {
147 __HAL_RCC_UART4_CLK_DISABLE();
148
149 /**UART4 GPIO Configuration
150 PG11 ------> UART4_TX
151 PB2 ------> UART4_RX
152 */
153 HAL_GPIO_DeInit(GPIOG, GPIO_PIN_11); /* 取消初始化PG11 */
154 HAL_GPIO_DeInit(GPIOB, GPIO_PIN_2); /* 取消初始化PB2 */
155
156 HAL_DMA_DeInit(uartHandle->hdmarx); /* DMA取消初始化 */
157 HAL_DMA_DeInit(uartHandle->hdmatx); /* 取消初始化DMA外设 */
158
159 HAL_NVIC_DisableIRQ(UART4_IRQn); /* 取消UART4的中断初始化 */
160 }
161 }
21.3.2 添加用户代码
1 #include "main.h"
2 #include "dma.h"
3 #include "usart.h"
4 #include "gpio.h"
5
6 /* USER CODE BEGIN Includes */
7 #include "./BSP/Include/led.h"
8 /* USER CODE END Includes */
9
10 void SystemClock_Config(void);
11
12 int main(void)
13 {
14 HAL_Init(); /* HAL库初始化 */
15 if(IS_ENGINEERING_BOOT_MODE())
16 {
17 SystemClock_Config(); /* 系统时钟配置 */
18 }
19 MX_GPIO_Init(); /* GPIO初始化 */
20 MX_DMA_Init(); /* DMA初始化 */
21 MX_UART4_Init(); /* UART4初始化 */
22 /* USER CODE BEGIN 2 */
23 uint8_t Senbuff[] ={"WWW.openedv.com\r\n"}; /* 要循环发送的字符串 */
24 /* USER CODE END 2 */
25
26 while (1)
27 {
28 /* USER CODE BEGIN 3 */
29 /* 开始DMA传送 */
30 HAL_UART_Transmit_DMA(&huart4, (uint8_t *)Senbuff, sizeof(Senbuff));
31 LED0_TOGGLE(); /* LED0翻转 */
32 HAL_Delay(1000); /* 延时1s */
33 }
34 /* USER CODE END 3 */
35 }
第23行,定义一个数组,存放一串字符串WWW.openedv.com;
第30行,使用DMA发送字符串,由于开启了UART4全局中断,运行后会看到串口不断打印WWW.openedv.com;
第31和32行,LED0每隔1s的时间闪烁一次,此时CPU是空闲的,可以做别的事情。
/* USER CODE BEGIN 0 */
#include "dma.h"
uint8_t rx_flag = 0; /* 接收标志位 */
uint16_t rx_len = 0; /* 接收的数据长度 */
uint8_t rx_buf[rx_max] = {0}; /* 定义一个数组,用于存放接收的数据 */
/* USER CODE END 0 */
修改usart.h文件:
/* USER CODE BEGIN Private defines */
#define rx_max 1024
extern uint8_t rx_flag;
extern uint16_t rx_len;
extern uint8_t rx_buf[rx_max];
/* USER CODE END Private defines */
修改stm32f1xx_it.c文件:
我们要用到uart4相关的定义,所以先包含usart.h头文件:
/* USER CODE BEGIN Includes */
#include "usart.h"
/* USER CODE END Includes */
UART4_IRQHandler函数修改如下,在标红的字体之间添加代码,其它代码保持不变:
void UART4_IRQHandler(void)
{
/* USER CODE BEGIN UART4_IRQn 0 */
uint32_t temp;
if((__HAL_UART_GET_FLAG(&huart4,UART_FLAG_IDLE) != RESET))
{
/*清除状态寄存器和串口数据寄存器*/
__HAL_UART_CLEAR_IDLEFLAG(&huart4);
HAL_UART_DMAStop(&huart4); /*停止DMA接收*/
temp = huart4.hdmarx->Instance->NDTR; /*接收数据的剩余大小*/
rx_len = rx_max - temp; /*读取接收长度,总大小-剩余大小*/
rx_flag=1; /* 接收标志位置1 */
HAL_UART_Receive_DMA(&huart4,rx_buf,rx_max); /*开启接收DMA接收*/
}
/* USER CODE END UART4_IRQn 0 */
HAL_UART_IRQHandler(&huart4);
/* USER CODE BEGIN UART4_IRQn 1 */
/* USER CODE END UART4_IRQn 1 */
}
修改main.c文件:
如下,标红的字体之间的代码是我们手动添加的:
#include "main.h"
#include "dma.h"
#include "usart.h"
#include "gpio.h"
/* USER CODE BEGIN Includes */
#include "./BSP/Include/led.h"
/* USER CODE END Includes */
void SystemClock_Config(void);
int main(void)
{
HAL_Init();
if(IS_ENGINEERING_BOOT_MODE())
{
SystemClock_Config();
}
MX_GPIO_Init();
MX_DMA_Init();
MX_UART4_Init();
/* USER CODE BEGIN 2 */
HAL_UART_Receive_DMA(&huart4, rx_buf, rx_max); /* 启动DMA接收 */
__HAL_UART_ENABLE_IT(&huart4, UART_IT_IDLE); /* 使能IDLE中断 */
/* USER CODE END 2 */
while (1)
{
/* USER CODE BEGIN 3 */
if(rx_flag) /* 接收到数据 */
{
rx_flag=0; /* 清除标志位,等待下一次接收 */
HAL_UART_Transmit_DMA(&huart4, rx_buf, rx_len); /*启动DMA发送 */
}
LED0_TOGGLE(); /* LED0翻转,CPU空闲时可以做别的事情 */
HAL_Delay(1000); /* 延时100ms */
}
/* USER CODE END 3 */
}
21.4 编译和测试
编译不报错,运行结果如下:
如果是第一种修改方法,只能发送定长的数据,串口会每隔1s打印字符串WWW.openedv.com。
图21.4. 1运行结果
如果是使用DMA+串口接收空闲中断(IDLE)的方法,可以发送不定长的数据:
图21.4. 2运行结果
至此,我们整个DMA实验就结束了,本实验是以一个简单的例子来介绍DMA,希望大家通过本章的学习,再多加实践,掌握STM32MP157的DMA使用。DMA是个非常好的功能,它不但能减轻CPU负担,还能提高数据传输速度,合理的应用DMA,往往能让你的程序设计变得简单。