完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
1)实验平台:正点原子STM32mini 开发板
2)摘自《正点原子STM32 不完全手册(HAL 库版)》关注官方微信号公众号,获取更 第二十三章 DMA 实验 本章我们将向大家介绍 STM32 的 DMA。在本章中,我们将利用 STM32 的 DMA 来实 现串口数据传送,并在 TFTLCD 模块上显示当前的传送进度。本章分为如下几个部分: 23.1 STM32 DMA 简介 23.2 硬件设计 23.3 软件设计 23.4 下载验证 23.1 STM32 DMA 简介 DMA,全称为:Direct Memory Access,即直接存储器访问。DMA 传输方式无需 CPU 直接 控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为 RAM 与 I/O 设备 开辟一条直接传送数据的通路,能使 CPU 的效率大为提高。 STM32 最多有 2 个 DMA 控制器(DMA2 仅存在大容量产品中),DMA1 有 7 个通道。DMA2 有 5 个通道。每个通道专门用来管理来自于一个或多个外设对存储器访问的请求。还有一个仲裁起 来协调各个 DMA 请求的优先权。 STM32 的 DMA 有以下一些特性: ●每个通道都直接连接专用的硬件 DMA 请求,每个通道都同样支持软件触发。这些功能 通过软件来配置。 ●在七个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),假如 在相等优先权时由硬件决定(请求 0 优先于请求 1,依此类推) 。 ●独立的源和目标数据区的传输宽度(字节、半字、全字),模拟打包和拆包的过程。源和 目标地址必须按数据传输宽度对齐。 ●支持循环的缓冲器管理 ●每个通道都有 3 个事件标志(DMA 半传输,DMA 传输完成和 DMA 传输出错),这 3 个 事件标志逻辑或成为一个单独的中断请求。 ●存储器和存储器间的传输 ●外设和存储器,存储器和外设的传输 ●闪存、SRAM、外设的 SRAM、APB1 APB2 和 AHB 外设均可作为访问的源和目标。 ●可编程的数据传输数目:最大为 65536 STM32F103RCT6 有两个 DMA 控制器,DMA1 和 DMA2,本章,我们仅针对 DMA1 进行 介绍。 从外设(tiMx、ADC、SPIx、I2Cx 和 USARTx)产生的 DMA 请求,通过逻辑或输入到 DMA 控制器,这就意味着同时只能有一个请求有效。外设的 DMA 请求,可以通过设置相应的 外设寄存器中的控制位,被独立地开启或关闭。 表 23.1.1 是 DMA1 各通道一览表: 表 23.1.1 DMA1 各通道一览表 这里解释一下上面说的逻辑或,例如通道 1 的几个 DMA1 请求(ADC1、TIM2_CH3、TIM4_CH1), 这几个是通过逻辑或到通道 1 的,这样我们在同一时间,就只能使用其中的一个。其他通道也 是类似的。 这里我们要使用的是串口 1 的 DMA 传送,也就是要用到通道 4。接下来,我们介绍一下 DMA 设置相关的几个寄存器。 第一个是 DMA 中断状态寄存器(DMA_ISR)。该寄存器的各位描述如图 23.1.1 所示: 图 23.1.1 DMA_ISR 寄存器各位描述 我们如果开启了 DMA_ISR 中这些中断,在达到条件后就会跳到中断服务函数里面去,即使 没开启,我们也可以通过查询这些位来获得当前 DMA 传输的状态。这里我们常用的是 TCIFx, 即通道 DMA 传输完成与否的标志。注意此寄存器为只读寄存器,所以在这些位被置位之后,只 能通过其他的操作来清除。 第二个是 DMA 中断标志清除寄存器(DMA_IFCR)。该寄存器的各位描述如图 23.1.2 所示: 图 23.1.2 DMA_IFCR 寄存器各位描述 DMA_IFCR 的各位就是用来清除 DMA_ISR 的对应位的,通过写 0 清除。在 DMA_ISR 被置位后, 我们必须通过向该位寄存器对应的位写入 0 来清除。 第三个是 DMA 通道 x 配置寄存器(DMA_CCRx)(x=1~7,下同)。该寄存器的我们在这里就不 贴出来了,见《STM32 参考手册》第 150 页 10.4.3 一节。该寄存器控制着 DMA 的很多相关信息, 包括数据宽度、外设及存储器的宽度、通道优先级、增量模式、传输方向、中断允许、使能等 都是通过该寄存器来设置的。所以 DMA_CCRx 是 DMA 传输的核心控制寄存器。 第四个是 DMA 通道 x 传输数据量寄存器(DMA_CNDTRx)。这个寄存器控制 DMA 通道 x 的每次 传输所要传输的数据量。其设置范围为 0~65535。并且该寄存器的值会随着传输的进行而减少, 当该寄存器的值为 0 的时候就代表此次数据传输已经全部发送完成了。所以可以通过这个寄存 器的值来知道当前 DMA 传输的进度。 第五个是 DMA 通道 x 的外设地址寄存器(DMA_CPARx)。该寄存器用来存储 STM32 外设的地 址,比如我们使用串口 1,那么该寄存器必须写入 0x40013804(其实就是&USART1_DR)。如果使 用其他外设,就修改成相应外设的地址就行了。 最后一个是 DMA 通道 x 的存储器地址寄存器(DMA_CMARx),该寄存器和 DMA_CPARx 差不多, 但是是用来放存储器的地址的。比如我们使用 SendBuf[5200]数组来做存储器,那么我们在 DMA_CMARx 中写入&SendBuff 就可以了。 DMA 相关寄存器就为大家介绍到这里,此节我们要用到串口 1 的发送,属于 DMA1 的通道 4, 接下来我们就介绍下 DMA1 通道 4 的配置步骤: 1)使能 DMA1 时钟。 DMA 的时钟使能是通过 AHB1ENR 寄存器来控制的,这里我们要先使能时钟,才可以配置 DMA 相关寄存器。HAL 库方法为:__HAL_RCC_DMA1_CLK_ENABLE();//DMA1 时钟使能 2) 初始化 DMA1 数据流 4,包括配置通道,外设地址,存储器地址,传输数据量等。 DMA 的某个数据流各种配置参数初始化是通过 HAL_DMA_Init 函数实现的,该函数声明为: HAL_StatusTypeDef HAL_DMA_Init(DMA_HandleTypeDef *hdma); 该函数只有一个 DMA_HandleTypeDef 结构体指针类型入口参数,结构体定义为: typedef struct __DMA_HandleTypeDef { DMA_Stream_TypeDef *Instance; DMA_InitTypeDef Init; HAL_LockTypeDef Lock; __IO HAL_DMA_StateTypeDef State; void *Parent; void (* XferCpltCallback)( struct __DMA_HandleTypeDef * hdma); void (* XferHalfCpltCallback)( struct __DMA_HandleTypeDef * hdma); void (* XferM1CpltCallback)( struct __DMA_HandleTypeDef * hdma); void (* XferErrorCallback)( struct __DMA_HandleTypeDef * hdma); __IO uint32_t ErrorCode; uint32_t StreamBaseAddress; uint32_t StreamIndex; }DMA_HandleTypeDef; 成员变量 Instance 是用来设置寄存器基地址,例如要设置为 DMA1 的通道 4,那么取值 为 DMA1_Channel4。 成员变量 Parent 是 HAL 库处理中间变量,用来指向 DMA 通道外设句柄。 成员变量 XferCpltCallback(传输完成回调函数), XferHalfCpltCallback(半传输完成 回调函数), XferM1CpltCallback(Memory1 传输完成回调函数)和 XferErrorCallback(传输错 误回调函数)是四个函数指针,用来指向回调函数入口地址。 成员变量 StreamBaseAddress 和 StreamIndex 是数据流基地址和索引号,这个是 HAL 库处 理的时候会自动计算,用户无需设置。 其他成员变量 HAL 库处理过程状态标识变量,这里就不做过多讲解。接下来我们着重看看 成员变量 Init,它是 DMA_InitTypeDef 结构体类型,该结构体定义为: typedef struct { 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 优先级:低/中/高/非常高 }DMA_InitTypeDef; 该结构体成员变量非常多,但是每个成员变量配置的基本都是 DMA_SxCR 寄存器和 DMA_SxFCR 寄存器的相应位。我们把结构体各个成员变量的含义都通过注释的方式列出来了。 例如本实验我们要用到 DMA2_ DMA1_Channel4,把内存中数组的值发送到串口外设发送寄存器 DR,所以方向为存储器到外设 DMA_MEMORY_TO_PERIPH,一个一个字节发送,需要数字索引自动 增加,所以是存储器增量模式 DMA_MINC_ENABLE,存储器和外设的字宽都是字节 8 位。具体配 置如下: DMA_HandleTypeDef UART1TxDMA_Handler; //DMA 句柄 UART1TxDMA_Handler.Instance= DMA1_Channel4; //通道选择 UART1TxDMA_Handler.Init.Direction=DMA_MEMORY_TO_PERIPH; //存储器到外设 UART1TxDMA_Handler.Init.PeriphInc=DMA_PINC_DISABLE; //外设非增量模式 UART1TxDMA_Handler.Init.MemInc=DMA_MINC_ENABLE; //存储器增量模式 UART1TxDMA_Handler.Init.PeriphDataAlignment=DMA_PDATAALIGN_BYTE; //外设数据长度:8 位 UART1TxDMA_Handler.Init.MemDataAlignment=DMA_MDATAALIGN_BYTE; //存储器数据长度:8 位 UART1TxDMA_Handler.Init.Mode=DMA_NORMAL; //外设普通模式 UART1TxDMA_Handler.Init.Priority=DMA_PRIORITY_MEDIUM; //中等优先级 这里大家要注意,HAL 库为了处理各类外设的 DMA 请求,在调用相关函数之前,需要调用 一个宏定义标识符,来连接 DMA 和外设句柄。例如要使用串口 DMA 发送,所以方式为: __HAL_LINKDMA(&UART1_Handler,hdmatx,UART1TxDMA_Handler); 其中 UART1_Handler 是串口初始化句柄,我们在 usart.c 中定义过了。UART1TxDMA_Handler 是 DMA 初始化句柄。hdmatx 是外设句柄结构体的成员变量,在这里实际就是 UART1_Handler 的 成员变量。在 HAL 库中,任何一个可以使用 DMA 的外设,它的初始化结构体句柄都会有一个 DMA_HandleTypeDef 指针类型的成员变量,是 HAL 库用来做相关指向的。Hdmatx 就是 DMA_HandleTypeDef 结构体指针类型。 这 句 话 的 含 义 就 是 把 UART1_Handler 句 柄 的 成 员 变 量 hdmatx 和 DMA 句 柄 UART1TxDMA_Handler 连接起来,是纯软件处理,没有任何硬件操作。 这里我们就点到为止,如果大家要详细了解 HAL 库指向关系,请查看本实验宏定义标识符 __HAL_LINKDMA 的定义和调用方法,就会很清楚了。 3)使能串口 1 的 DMA 发送 在实验中,开启一次 DMA 传输传输函数如下: //开启一次 DMA 传输 //huart:串口句柄 //pData:传输的数据指针 //Size:传输的数据量 void MYDMA_USART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) { HAL_DMA_Start(huart->hdmatx, (u32)pData, (uint32_t)&huart->Instance->DR, Size); //开启 DMA 传输 huart->Instance->CR3 |= USART_CR3_DMAT;//使能串口 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);//恢复 这些函数使用方法这里我们就不累赘了。 4)使能 DMA1 数据流 4,启动传输。 使能 DMA 数据流的函数为: HAL_StatusTypeDef HAL_DMA_Start(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength); 这个函数比较好理解,第一个参数是 DMA 句柄,第二个是传输源地址,第三个是传输目标 地址,第四个是传输的数据长度。 通过以上 4 步设置,我们就可以启动一次 USART1 的 DMA 传输了。 5)查询 DMA 传输状态 在 DMA 传输过程中,我们要查询 DMA 传输通道的状态,使用的方法是: __HAL_DMA_GET_FLAG(&UART1TxDMA_Handler,DMA_FLAG_TCIF3_7); 获取当前传输剩余数据量: __HAL_DMA_GET_COUNTER(&UART1TxDMA_Handler); DMA 相关的库函数我们就讲解到这里,大家可以查看固件库中文手册详细了解。 6)DMA 中断使用方法 DMA 中断对于每个流都有一个中断服务函数,比如 DMA1_Channel4 的中断服务函数为 DMA1_Channel4_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 开启,使能数据流,启动传输,这些步骤,如果使用了中断,可以直接调 用 HAL 库函数 HAL_USART_Transmit_DMA,该函数声明如下: HAL_StatusTypeDef HAL_USART_Transmit_DMA(USART_HandleTypeDef *husart, uint8_t *pTxData, uint16_t Size); DMA 相关的库函数我们就讲解到这里,大家可以查看 HAL 库手册详细了解。 23.2 硬件设计 所以本章用到的硬件资源有: 1) 指示灯 DS0 2) KEY0 按键 3) 串口 4) TFTLCD 模块 5) DMA 本章我们将利用外部按键 KEY0 来控制 DMA 的传送,每按一次 KEY0,DMA 就传送一次数据到 USART1,然后在 TFTLCD 模块上显示进度等信息。DS0 还是用来做为程序运行的指示灯。 本章实验需要注意 P4 口的 RXD 和 TXD 是否和旁边的 PA9 和 PA10 连接上了,如果没有,请先连接。 23.3 软件设计 打开本章的实验工程可以看到,我们在 FWLIB 分组下面增加了 DMA 支持文件 stm32f1xx_hal_dma.c,同时引入了 stm32f1xx_hal_dma.h 头文件支持。在 HARDWARE 分组下 面我们新增了 dma.c 以及对应头文件 dma.h 用来存放 dma 相关的函数和定义。 打开 dma.c 文件,代码如下: DMA_HandleTypeDef UART1TxDMA_Handler; //DMA 句柄 //DMA1 的各通道配置 //这里的传输形式是固定的,这点要根据不同的情况来修改 //从存储器->外设模式/8 位数据宽度/存储器增量模式 //chx:DMA 通道选择,DMA1_Channel1~DMA1_Channel7 void MYDMA_Config(DMA_Channel_TypeDef *chx) { __HAL_RCC_DMA1_CLK_ENABLE(); //DMA1 时钟使能 __HAL_LINKDMA(&UART1_Handler,hdmatx,UART1TxDMA_Handler); //将 DMA 与 USART1 联系起来(发送 DMA) //Tx DMA 配置 UART1TxDMA_Handler.Instance=chx; //通道选择 UART1TxDMA_Handler.Init.Direction=DMA_MEMORY_TO_PERIPH; //存储器到外设 UART1TxDMA_Handler.Init.PeriphInc=DMA_PINC_DISABLE; //外设非增量模式 UART1TxDMA_Handler.Init.MemInc=DMA_MINC_ENABLE; //存储器增量模式 UART1TxDMA_Handler.Init.PeriphDataAlignment=DMA_PDATAALIGN_BYTE; //外设数据长度:8 位 UART1TxDMA_Handler.Init.MemDataAlignment=DMA_MDATAALIGN_BYTE; //存储器数据长度:8 位 UART1TxDMA_Handler.Init.Mode=DMA_NORMAL; //外设普通模式 UART1TxDMA_Handler.Init.Priority=DMA_PRIORITY_MEDIUM; //中等优先级 HAL_DMA_DeInit(&UART1TxDMA_Handler); HAL_DMA_Init(&UART1TxDMA_Handler); } //开启一次 DMA 传输 //huart:串口句柄 //pData:传输的数据指针 //Size:传输的数据量 void MYDMA_USART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) { HAL_DMA_Start(huart->hdmatx, (u32)pData, (uint32_t)&huart->Instance->DR, Size); //开启 DMA 传输 huart->Instance->CR3 |= USART_CR3_DMAT;//使能串口 DMA 发送 } 该部分代码仅仅 2 个函数,MYDMA_Config 函数,基本上就是按照我们上面介绍的步骤来初 始化 DMA 的,该函数在外部只能修改通道、源地址、目标地址和传输数据量等几个参数,更多 的其他设置只能在该函数内部修改。 MYDMA_Enable 函数用来产生一次 DMA 传输,该函数每执行一次,DMA 就发送一次。 dma.h 头文件内容比较简单,主要是函数申明,这里我们不细说。 接下来我们看看那 main 函数如下: #define SEND_BUF_SIZE 7000 //发送数据长度,最好等于 sizeof(TEXT_TO_SEND)+2 的整数倍. u8 SendBuff[SEND_BUF_SIZE]; //发送数据缓冲区 const u8 TEXT_TO_SEND[]={"ALIENTEK Mini STM32 DMA 串口实验"}; int main(void) { u16 i; u8 t=0; u8 j,mask=0; float pro=0; HAL_Init(); //初始化 HAL 库 Stm32_Clock_Init(RCC_PLL_MUL9); //设置时钟,72M delay_init(72); //初始化延时函数 uart_init(115200); //初始化串口 usmart_dev.init(84); //初始化 USMART LED_Init(); //初始化 LED KEY_Init(); //初始化按键 LCD_Init(); //初始化 LCD MYDMA_Config(DMA1_Channel4); //初始化 DMA1 通道 4 POINT_COLOR=RED; LCD_ShowString(30,50,200,16,16,"Mini STM32"); LCD_ShowString(30,70,200,16,16,"DMA TEST"); LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(30,110,200,16,16,"2019/11/15"); LCD_ShowString(30,130,200,16,16,"KEY0:Start"); POINT_COLOR=BLUE;//设置字体为蓝色 //显示提示信息 j=sizeof(TEXT_TO_SEND); for(i=0;i if(t>=j)//加入换行符 { if(mask) { SendBuff=0x0a; t=0; }else { SendBuff=0x0d; mask++; } }else//复制 TEXT_TO_SEND 语句 { mask=0; SendBuff=TEXT_TO_SEND[t]; t++; } } POINT_COLOR=BLUE;//设置字体为蓝色 i=0; while(1) { t=KEY_Scan(0); if(t==KEY0_PRES) //KEY0 按下 { printf("rnDMA DATA:rn"); LCD_ShowString(30,150,200,16,16,"Start Transimit...."); LCD_ShowString(30,170,200,16,16," %") ; //显示百分号 HAL_UART_Transmit_DMA(&UART1_Handler,SendBuff,SEND_BUF_SIZE); //启动传输 //使能串口 1 的 DMA 发送 //等待 DMA 传输完成, //此时我们来做另外一些事,点灯 //实际应用中,传输数据期间,可以执行另外的任务 while(1) { if(__HAL_DMA_GET_FLAG(&UART1TxDMA_Handler,DMA_FLAG_TC4)) //等待 DMA1 通道 4 传输完成 { __HAL_DMA_CLEAR_FLAG(&UART1TxDMA_Handler,DMA_FLAG_TC4); //清除 DMA1 通道 4 传输完成标志 HAL_UART_DMAStop(&UART1_Handler); //传输完成以后关闭串口 DMA break; } pro=__HAL_DMA_GET_COUNTER(&UART1TxDMA_Handler); //得到当前还剩余多少个数据 pro=1-pro/SEND_BUF_SIZE; //得到百分比 pro*=100; //扩大 100 倍 LCD_ShowNum(30,170,pro,3,16); } LCD_ShowNum(30,170,100,3,16);//显示 100% LCD_ShowString(30,150,200,16,16,"Transimit Finished!");//提示传送完成 } i++; delay_ms(10); if(i==20) { LED0=!LED0;//提示系统正在运行 i=0; } } } main 函数的流程大致是:先初始化内存 SendBuff 的值,然后通过 KEY0 开启串口 DMA 发送, 在发送过程中,通过__HAL_DMA_GET_COUNTER()函数获取当前还剩余的数据量来计算传输百分比, 最后在传输结束之后清除相应标志位,提示已经传输完成。这里还有一点要注意,因为是使用 的串口 1 DMA 发送,所以代码中使用 HAL_UART_Transmit_DMA 函数开启串口的 DMA 发送: 至此,DMA 串口传输的软件设计就完成了。 23.4 下载验证 在代码编译成功之后,我们通过串口下载代码到 ALIENTEK MiniSTM32 开发板上,可以 看到 DS0 开始闪烁,同时 LCD 显示一些信息,然后我们按 KEY0 按键,开发板就开始通过 DMA 发送数据到串口,并在 TFTLCD 上显示进度等信息,如图 23.4.1 所示: 图 23.4.1 DMA 实验测试图 打开串口调试助手,可以看到串口显示如图 23.4.2 所示的内容: 图 23.4.2 串口收到的数据内容 从上图可以看出,我们收到了来自开发板的串口数据。至此,我们整个 DMA 实验就结束 了,希望大家通过本章的学习,掌握 STM32 的 DMA 使用。DMA 是个非常好的功能,它不但 能减轻 CPU 负担,还能提高数据传输速度,合理的应用 DMA,往往能让你的程序设计变得简单。 |
|
相关推荐
|
|
STM32进行DMA数据转运,为什么只有A组最后一位数据被转运到B组去?
1265 浏览 2 评论
1585 浏览 3 评论
1206 浏览 0 评论
嵌入式开发 Win11安装ST-Link 提示 设备描述符请求失败 怎么处理?
1339 浏览 0 评论
1632 浏览 1 评论
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-8-18 06:35 , Processed in 0.615058 second(s), Total 63, Slave 48 queries .
Powered by 电子发烧友网
© 2015 www.ws-dc.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号