安康市网站建设_网站建设公司_定制开发_seo优化
2025/12/30 17:07:46 网站建设 项目流程

引言

前面介绍了DMA的相关基础知识以及常用寄存器,本次就来借助一个案例巩固一下DMA的理论知识,DMA案例实际上相比前面学习的定时器会简单不少,当然大家需要比较熟悉DMA的相关内容,所以如果忘记了可参考上一篇介绍的DMA基础知识进行回顾,然后再来看本次的案例。

DMA_基础知识、基本原理与相关寄存器详解-CSDN博客https://blog.csdn.net/2301_79475128/article/details/156388808?spm=1001.2014.3001.5501


一、需求描述

使用寄存器以及HAL库操作把ROM中的数据通过DMA传输到RAM,然后把数据通过printf发送到串口验证是否正确。

二、硬件电路设计

由于本案例的ROM使用的是芯片自身的flash,RAM使用的是芯片自身的SRAM,因此本需求无需硬件接线

三、需求分析

根据需求描述可知:

本次案例需要实现的核心内容是:把ROM(也就是flash)中的数据传输到RAM中,并利用串口重定向打印验证。在这里我们更主要的即ROM到RAM的数据传输

开发方式规定为:寄存器和HAL库方式实现

数据传输方式规定:利用DMA进行传输

四、软件设计

4.1 思路分析

根据前面对需求的分析可知:

首先我们需要基于寄存器以及HAL库方式编写;

其次,“ROM到RAM的数据传输”相当于是存储器到存储器的数据传输,同时需要使用DMA,因此DMA上传输前的配置必然需要选择存储器到存储器模式,且传输方向应该是从外设读(因为前面介绍寄存器的时候说过,存储器到存储器模式下外设一般相当于ROM),同时DMA传输不涉及外设,所以通道可以随便选。本次我们就选择DMA1的通道1。最后传输完成后当然需要停止DMA传输,因此这里我选择使用中断实现,且可以设置一个标志位,传输完成是置位后串口打印数据;

然后,“利用串口打印验证”意味着需要我们产生一个之前和过去的变化效果,因此可以展示传输前和传输后记录的内容,即专门定义进行存储,比如定义存储ROM中的数据的量,然后定义初始化为0的量,这俩分别存在于ROM和RAM,然后经过DMA传输前打印这俩的内容,传输完成后打印存储于RAM的量即可验证

这其中除了DMA传输外还有一个关键问题:如何准备存储在RAM和ROM的量?根据RAM和ROM中存储的数据可知,RAM(也就是flash)一般自行添加的是烧录的程序以及定义的常量(const uint8 xxx类似),RAM中一段存储的是平时程序中定义的变量(uint8 xxx类似)。因此,我们可以通过创建常量存储一些内容,然后定义变量默认记录0,然后根据DMA原理将这俩的地址分别传入外设地址寄存器和存储器地址寄存器中,然后经过传输后在打印变量中的数据即可知道DMA传输是否成功了。

最后关于串口打印的话,可以直接看笔者之前对串口重定向的介绍,这部分比较简单,就不再赘述了。STM32调试手段:重定向printf串口_stm32 printf重定向-CSDN博客https://blog.csdn.net/2301_79475128/article/details/145305160

因此,我们整理一下分析得到的核心实现思路:

1、DMA底层配置:重点配置好传输模式和方向(存储器到存储器、从外设读)、源地址和目的地址配置(对应ROM数据的地址和RAM数据的地址),停止DMA传输通过配置中断,在中断处理程序中实现。其余内容正常按照前面文章介绍的寄存器进行配置即可。

2、RAM和ROM数据构建:定义常量存储一些数据,表示这是ROM中的数据;定义变量存储一些数据,表示这是RAM中的数据。为方便索取地址,咱直接定义成数组,数组名即数组首地址。

3、初始化DMA并开始DMA传输:后续定义相关函数并调用。

4、串口打印数据:当数据传输完成时,触发中断将指定标志位置位时执行串口打印逻辑。


4.2 程序编写(寄存器方式)

经过前面的介绍,这个案例实现起来应该很简单了,我们就不过多赘述一些内容了。

4.2.1 DMA初始化

我们将DMA底层配置分为两大部分,分别是DMA初始化(配置传输之前的相关内容)和DMA数据传输(配置传输时必要的相关内容)。

首先我们进行DMA初始化的编写。按照前面对DMA基本原理和寄存器介绍以及对该案例的分析,我们应该知道传输之前需要开启时钟、配置传输模式、传输方向、外设数据宽度和存储器数据宽度、外设地址自增模式和存储器地址自增模式以及中断配置(传输完成中断)。

需要注意的是,由于一个DMA控制器包括多个通道,且各个通道的配置使用的寄存器配置都是一样的,因此这里对那些通用的寄存器使用的特殊的DMA通道结构体指针,所以配置寄存器时使用的并非DMA1,而是DMA1_Channal1这种。

DMA1初始化参考代码如下:

void DMA1_Init(void) { // 1. 开启时钟 RCC->AHBENR |= RCC_AHBENR_DMA1EN; // 2. DMA配置 // 2.1 设置数据传输方向:从外设读,MEM2MEM DMA1_Channel1->CCR |= DMA_CCR1_MEM2MEM; DMA1_Channel1->CCR &= ~DMA_CCR1_DIR; // 2.2 设置数据传输宽度:8位 DMA1_Channel1->CCR &= ~DMA_CCR1_MSIZE; DMA1_Channel1->CCR &= ~DMA_CCR1_PSIZE; // 2.3 地址增量模式 DMA1_Channel1->CCR |= DMA_CCR1_MINC; DMA1_Channel1->CCR |= DMA_CCR1_PINC; // 2.4 开启数据传输完成中断标志 DMA1_Channel1->CCR |= DMA_CCR1_TCIE; // 3. NVIC配置 NVIC_SetPriorityGrouping(3); NVIC_SetPriority(DMA1_Channel1_IRQn, 2); NVIC_EnableIRQ(DMA1_Channel1_IRQn); }

4.2.2 DMA数据传输

接着是DMA数据传输的实现,这里毕竟要进行数据传输,因此传输双方的地址肯定需要,即ROM和RAM的地址需要传参进入,其次还需要知道传输的数据量,所以传参还有数据长度。

内部实现的话可想而知,设置外设地址、存储器地址、数据长度(即传输的数据数量),最后开启DMA通道1使能即可。

void DMA1_TransmitData(uint32_t srcAddr, uint32_t desAddr, uint8_t dataLen) { // 1. 数据传输源和目标地址 DMA1_Channel1->CMAR = desAddr; DMA1_Channel1->CPAR = srcAddr; // 2. 数据传输量 DMA1_Channel1->CNDTR = dataLen; // 3. 开启DMA1通道 DMA1_Channel1->CCR |= DMA_CCR1_EN; }

4.2.3 重写中断处理程序

最后是重写一下DMA1通道传输完成的中断处理程序,这个中断处理程序名同理还是到启动汇编中的中断向量表中可以找到,这里不再赘述。

其中编写的即,清除传输完成中断标志位,然后关闭DMA1通道的使能,最后传输完成标志位置位(该标志位可在主程序定义全局变量,这里外部声明后调用)。

需要注意的是,DMA中关于中断的寄存器没有专门再用其他结构体封装,也就是说还是原本的DMA1这种就能访问,同时中断的寄存器有两个,分别是用于读取指定中断状态的ISR寄存器和中断标志清除的IFCR寄存器。因此中断中处理应该是判断相应中断标志位是否置位,若置位则清除,然后执行相应中断处理逻辑

// 中断处理程序 void DMA1_Channel1_IRQHandler(void) { // 判断传输完成中断标志位 if (DMA1->ISR & DMA_ISR_TCIF1) { // 清除传输完成标志位 DMA1->IFCR |= DMA_IFCR_CTCIF1; // 关闭DMA1通道 DMA1_Channel1->CCR &= ~DMA_CCR1_EN; // 完成标志置位(主程序全局定义) isFinished = 1; } }

4.2.4 主程序实现

最后在main.c中定义好存储在ROM中的常量数组以及存储在RAM中的变量数组,然后初始化串口和DMA,若不放心他们存储的位置,可先打印一下他们的地址,然后循环中当传输完成标志位置位时执行串口打印即可。

这里就是往ROM中存储了10 20 30 40这四个字节数据,然后通过DMA1通道1传输到RAM中,如果串口打印des数组显示10 20 30 40,说明传输成功。

#include "usart.h" #include "dma1.h" // 引入传输完成标志 uint8_t isFinished = 0; // 定义全局常量,存至ROM const uint8_t src[] = {10, 20, 30, 40}; // 定义全局变量,放在RAM uint8_t des[] = {0}; int main(void) { // 初始化 USART_Init(); DMA1_Init(); // 打印数据地址 printf("src = %p, des = %p\n", src, des); // DMA1通道将ROM数据搬运到RAM DMA1_TransmitData((uint32_t)src, (uint32_t)des, 4); // 死循环保持状态 while (1) { if (isFinished) { // 传输完毕标志清零 isFinished = 0; // 打印数据 for (uint8_t i = 0; i < 4; i++) { printf("%d\t", des[i]); } } } }

需要注意的是,由于俩数据数组的地址并不是显式定义的32位,进行DMA数据传输时传入源目的地址需要对数据类型进行uint32_t的强转,避免产生警告或者报错。

4.3 程序编写(HAL库方式)

由于HAL库方式关于DMA的配置非常简单,因此这里简单给出关键步骤即可,其他不再赘述。

4.3.1 DMA配置

4.3.2 代码补充

4.3.2.1 基本内容

在主程序编写的实际上与寄存器是非常类似的,也是先定义好常量、变量和标志位。

然后while循环进行串口打印。

4.3.2.2 中断处理

1、基础实现

然后补充一下中断处理程序的内容。这部分其实有两种处理办法。先说基础的处理办法,即直接进入it.c的文件在最底下找到DMA1通道1的中断处理程序,然后直接关闭通道和完成标志位置位就行。

2、进阶回调实现

还有一种进阶实现,也就是注册DMA传输完成的中断回调函数,然后在回调函数中实现中断逻辑。当然这种实现相对复杂,对C语言基础要求较高,涉及到注册回调函数、函数指针的内容,但实际上这种方式用起来会非常方便,而且代码解耦比较好。

那么如何去注册回调函数呢,也就是自己创建回调函数,然后使用。

因此最后我们需要先在main函数调用这个函数。

然后在上方声明这个回调函数,回调函数参数可在这个注册回调的函数形参列表找到。

最后,在最下面补充这个回调函数的实现,也就是中断处理逻辑即可。

注:此时原先中断处理程序中编写的这个的逻辑就可以删除了。

至此,HAL库的实现就完成了。

4.3 测试效果

最后下载程序查看一下串口情况。

首先是寄存器实现的测试效果如下:

显然,这里展示的地址和数据均没有问题。

然后是HAL库方式实现的测试效果如下:

很显然,也没有问题。


五、总结

本文介绍了通过DMA实现STM32芯片内部ROM(Flash)到RAM(SRAM)数据传输的案例。文章详细分析了需求,包括使用寄存器方式操作DMA1通道1,将常量数组从Flash传输到变量数组,并通过串口打印验证。重点讲解了寄存器方式的实现步骤:1) DMA初始化配置时钟、传输模式、中断等;2) DMA传输函数设置源/目的地址和数据长度;3) 中断处理程序实现传输完成标志。同时简要说明了HAL库的实现方法。


以上便是本次文章的所有内容,欢迎各位朋友在评论区讨论,本人也是一名初学小白,愿大家共同努力,一起进步吧!

鉴于笔者能力有限,难免出现一些纰漏和不足,望大家在评论区批评指正,谢谢!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询