STM32 DMA学习日记
写于2024/9/28晚
文章目录
- STM32 DMA学习日记
- 1. DMA简介
- 2. I/O方式
- 2.1 程序查询方式
- 2.2 程序中断方式
- 2.3 DMA方式
- 3.DMA框图
- 4. 相关寄存器
- 4.1 DMA中断状态寄存器(DMA_ISR)
- 4.2 DMA中断标志清除寄存器(DMA_IFCR)
- 4.3 DMA通道x传输数量寄存器(DMA_CNDTRx)
- 4.4 DMA通道x配置寄存器(DMA_CCRx)
- 4.5 DMA通道x外设地址寄存器(DMA_CPARx)
- 4.6 DMA通道x存储器地址寄存器(DMA_CMARx)
- 5.例程解析
- 5.1 DMA相关HAL库驱动介绍
1. DMA简介
DMA,全称为:Direct Memory Access,即直接存储器访问。DMA 传输方式无需 CPU 直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为 RAM 与 I/O 设备开辟一条直接传送数据的通路,能使 CPU 的效率大为提高。是计算机的4种I/O方式中的一种。
2. I/O方式
输入/输出系统实现主机与I/O设备之间的数据传送,可以采用不同的控制方式,各种方式在代价、性能、解决问题的着重点等方面各不相同,常用的I/O方式有程序查询、程序中断、DMA和通道等,其中前两种方式更依赖于CPU中程序指令的执行。下面我们来简介一下计算机组成原理中的4中I/O方式中的前三种。
2.1 程序查询方式
信息交换的控制完全由CPU执行程序实现,程序查询方式接口中设置一个数据缓冲寄存器(数据端口)和一个设备状态寄存器(状态端口)。主机进行I/O操作时,先发出询问信号,读取设备的状态并根据设备状态决定下一步操作究竟是进行数据传送还是等待。
程序查询方式的工作流程如下(见图7.2):
- ①CPU执行初始化程序,并预置传送参数。
- ②向I/O接口发出命令字,启动I/O设备。
- ③从外设接口读取其状态信息。
- ④CPU不断查询I/O设备状态,直到外设准备就绪。
- ⑤传送一次数据。
- ⑥修改地址和计数器参数。
- ⑦判断传送是否结束,若未结束转第③步,直到计数器为0
在这种控制方式下,CPU一旦启动I/O,就必须停止现行程序的运行,并在现行程序中插入一段程序。程序查询方式的主要特点是CPU有“踏步”等待现象,CPU与I/O串行工作。这种方式的接口设计简单、设备量少,但CPU在信息传送过程中要花费很多时间来查询和等待,而且在一段时间内只能和一台外设交换信息,效率大大降低。
2.2 程序中断方式
程序中断方式的思想:CPU在程序中安排好在某个时机启动某台外设,然后CPU继续执行当前的程序,不需要像查询方式那样一直等待外设准备就绪。一旦外设完成数据传送的准备工作,就主动向CPU发出中断请求,请求CPU为自己服务。在可以响应中断的条件下,CPU暂时中止正在执行的程序,转去执行中断服务程序为外设服务,在中断服务程序中完成一次主机与外设之间的数据传送,传送完成后,CPU返回原来的程序,如图7.3所示。
2.3 DMA方式
DMA方式是一种完全由硬件进行成组信息传送的控制方式,它具有程序中断方式的优点,即在数据准备阶段,CPU与外设并行工作。DMA方式在外设与内存之间开辟一条“直接数据通道”,信息传送不再经过CPU,降低了CPU在传送数据时的开销,因此称为直接存储器存取方式。
由于数据传送不经过CPU,也就不需要保护、恢复CPU现场等烦琐操作。
这种方式适用于磁盘、显卡、声卡、网卡等高速设备大批量数据的传送,它的硬件开销比较大。在DMA方式中,中断的作用仅限于故障和正常传送结束时的处理。
DMA控制器的组成
在DMA方式中,对数据传送过程进行控制的硬件称为DMA控制器(DMA接口)。当I/O设备需要进行数据传送时,通过DMA控制器向CPU提出DMA传送请求,CPU响应之后将让出系统总线,由DMA控制器接管总线进行数据传送。其主要功能如下:
- 接受外设发出的DMA请求,并向CPU发出总线请求。
- CPU响应并发出总线响应信号,DMA接管总线控制权,进入DMA操作周期。
- 确定传送数据的主存单元地址及长度,并自动修改主存地址计数和传送长度计数。
- 规定数据在主存和外设间的传送方向,发出读写等控制信号,执行数据传送操作。
- 向CPU报告DMA操作结束。
DMA方式和中断方式的区别
DMA方式和中断方式的重要区别如下:
①中断方式是程序的切换,需要保护和恢复现场;而DMA方式不中断现行程序,无需保护现场,除了预处理和后处理,其他时候不占用任何CPU资源。
②对中断请求的响应只能发生在每条指令执行结束时(执行周期后);而对DMA请求的响应可以发生在任意一个机器周期结束时(取指、间址、执行周期后均可)。
③中断传送过程需要CPU的干预;而DMA传送过程不需要CPU的干预,因此数据传输率非常高,适合于高速外设的成组数据传送。④DMA请求的优先级高于中断请求。
⑤中断方式具有处理异常事件的能力,而DMA方式仅局限于大批数据的传送。
⑥从数据传送来看,中断方式靠程序传送,DMA方式靠硬件传送。
3.DMA框图
STM32F103ZET6 有两个 DMA 控制器,DMA1 和 DMA2,本章,我们仅针对 DMA1 进行介绍。
下面先来学习 DMA 控制器框图,通过学习 DMA 控制器框图会有一个很好的整体掌握,同时对之后的编程也会有一个清晰的思路。
图中,我们标记了 3 处位置,起作用分别是:
① DMA 请求
如果外设想要通过 DMA 来传输数据,必须先给 DMA 控制器发送 DMA 请求,DMA 收到请求信号之后,控制器会给外设一个应答信号,当外设应答后且 DMA 控制器收到应答信号之后,就会启动 DMA 的传输,直到传输完毕。
STM32F103 共有 DMA1 和 DMA2 两个控制器,DMA1 有 7 个通道,DMA2 有 5 个通道,不同的 DMA 控制器的通道对应着不同的外设请求,这决定了我们在软件编程上该怎么设置,具体见表 29.1.1.1DMA 请求映像表。
② 通道
DMA 具有 12 个独立可编程的通道,其中 DMA1 有 7 个通道,DMA2 有 5 个通道,每个通道对应不同的外设的 DMA 请求。虽然每个通道可以接收多个外设的请求,但是同一时间只能接收一个,不能同时接收多个。
③ 仲裁器
当发生多个 DMA 通道请求时,就意味着有先后响应处理的顺序问题,这个就由仲裁器管理。仲裁器管理 DMA 通道请求分为两个阶段。第一阶段属于软件阶段,可以在 DMA_CCRx寄存器中设置,有 4 个等级:非常高,高,中和低四个优先级。第二阶段属于硬件阶段,如果两个或以上的 DMA 通道请求设置的优先级一样,则他们优先级取决于通道编号,编号越低优先权越高,比如通道 0 高于通道 1。在大容量产品和互联型产品中,DMA1 控制器拥有高于 DMA2 控制器的优先级。
4. 相关寄存器
4.1 DMA中断状态寄存器(DMA_ISR)
该寄存器是查询当前 DMA 传输的状态,我们常用的是 TCIFx 位,即通道 DMA 传输完成与否的标志。注意此寄存器为只读寄存器,所以在这些位被置位之后,只能通过其他的操作来清除。
4.2 DMA中断标志清除寄存器(DMA_IFCR)
该寄存器是用来清除 DMA_ISR 的对应位的,通过写 0 清除。在 DMA_ISR 被置位后,我们必须通过向该寄存器对应的位写 1 来清除。
4.3 DMA通道x传输数量寄存器(DMA_CNDTRx)
4.4 DMA通道x配置寄存器(DMA_CCRx)
该寄存器控制着 DMA 很多相关信息,包括数据宽度、外设及存储器宽度、通道优先级、增量模式、传输方向、中断允许、使能等,所以说 DMA_CCRx 是 DMA 传输的核心控制寄存器。
4.5 DMA通道x外设地址寄存器(DMA_CPARx)
该寄存器是用来存储 STM32 外设的地址,比如我们平常使用串口 1,那么该寄存器必须写入 0x40013804(其实就是&USART1_DR)。其他外设就可以修改成其他对应外设地址就好了。
4.6 DMA通道x存储器地址寄存器(DMA_CMARx)
DMA通道x存储器地址寄存器用来存放存储器的地址,该寄存器和 DMA_CPARx差不多,所以就不列出来了。举个应用的例子,在程序中,我们使用到一个 g_sendbuf[5200]数组来做存储器,那么我们在 DMA_CMARx 中写入&g_sendbuf 即可。
5.例程解析
5.1 DMA相关HAL库驱动介绍
驱动函数 | 关联寄存器 | 功能描述 |
---|---|---|
__HAL_RCC_DMAx_CLK_ENABLE(…) | RCC_AHBENR | 使能DMAx时钟 |
HAL_DMA_Init(…) | DMA_CCR | 初始化DMA |
HAL_DMA_Start_IT(…) | DMA_CCR/CPAR/CMAR/CNDTR | 开始DMA传输 |
__HAL_LINKDMA(…) | 用来连接DMA和外设句柄 | |
HAL_UART_Transmit_DMA(…) | CCR/CPAR/CMAR/CNDTR/USART_CR3 | 使能DMA发送,启动传输 |
__HAL_DMA_GET_FLAG(…) | DMA_ISR | 查询DMA传输通道的状态 |
__HAL_DMA_ENABLE(…) | DMA_CCR(EN) | 使能DMA外设 |
__HAL_DMA_DISABLE(…) | DMA_CCR(EN) | 失能DMA外设 |
DMA外设相关结构体:DMA_HandleTypeDef 和 DMA_InitTypeDef
typedef struct __DMA_HandleTypeDef
{
DMA_Channel_TypeDef *Instance; /*!< Register base address 寄存器基地址 */
DMA_InitTypeDef Init; /*!< DMA communication parameters DMA参数 */
} DMA_HandleTypeDef;
typedef struct
{
uint32_t Direction /* DMA传输方向 */
uint32_t PeriphInc /* 外设地址(非)增量 */
uint32_t MemInc /* 存储器地址(非)增量*/
uint32_t PeriphDataAlignment /* 外设数据宽度 */
uint32_t MemDataAlignment /* 存储器数据宽度 */
uint32_t Mode /* 操作模式 */
uint32_t Priority /* DMA通道优先级 */
} DMA_InitTypeDef;
以DMA方式传输串口数据配置步骤
- 使能DMA时钟:
__HAL_RCC_DMA1_CLK_ENABLE
- 初始化DMA:
HAL_DMA_Init
函数初始化DMA相关参数__HAL_LINKDMA
函数连接DMA和外设 - 使能串口的DMA发送,启动传输:
HAL_UART_Transmit_DMA
- 查询DMA传输状态:
__HAL_DMA_GET_FLAG
查询通道传输状态__ HAL_DMA_GET_COUNTER
获取当前传输剩余数据量 - DMA中断使用:
HAL_NVIC_EnableIRQ
HAL_NVIC_SetPriority
编写中断服务函数xxx_IRQHandler
DMA_HandleTypeDef g_dma_handle; /* DMA句柄 */
extern UART_HandleTypeDef g_uart1_handle; /* UART句柄 */
/**
* @brief 串口TX DMA初始化函数
* @note 这里的传输形式是固定的, 这点要根据不同的情况来修改
* 从存储器 -> 外设模式/8位数据宽度/存储器增量模式
*
* @param dmax_chy : DMA的通道, DMA1_Channel1 ~ DMA1_Channel7, DMA2_Channel1 ~ DMA2_Channel5
* 某个外设对应哪个DMA, 哪个通道, 请参考<<STM32中文参考手册 V10>> 10.3.7节
* 必须设置正确的DMA及通道, 才能正常使用!
* @retval 无
*/
void dma_init(DMA_Channel_TypeDef* DMAx_CHx)
{
if ((uint32_t)DMAx_CHx > (uint32_t)DMA1_Channel7) /* 大于DMA1_Channel7, 则为DMA2的通道了 */
{
__HAL_RCC_DMA2_CLK_ENABLE(); /* DMA2时钟使能 */
}
else
{
__HAL_RCC_DMA1_CLK_ENABLE(); /* DMA1时钟使能 */
}
__HAL_LINKDMA(&g_uart1_handle, hdmatx, g_dma_handle); /* 将DMA与USART1联系起来(发送DMA) */
/* Tx DMA配置 */
g_dma_handle.Instance = DMAx_CHx; /* USART1_TX使用的DMA通道为: DMA1_Channel4 */
g_dma_handle.Init.Direction = DMA_MEMORY_TO_PERIPH; /* DIR = 1 , 存储器到外设模式 */
g_dma_handle.Init.PeriphInc = DMA_PINC_DISABLE; /* 外设非增量模式 */
g_dma_handle.Init.MemInc = DMA_MINC_ENABLE; /* 存储器增量模式 */
g_dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; /* 外设数据长度:8位 */
g_dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; /* 存储器数据长度:8位 */
g_dma_handle.Init.Mode = DMA_NORMAL; /* 外设流控模式 */
g_dma_handle.Init.Priority = DMA_PRIORITY_MEDIUM; /* 中等优先级 */
HAL_DMA_Init(&g_dma_handle);
}
main.c
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./USMART/usmart.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/KEY/key.h"
#include "./BSP/DMA/dma.h"
const uint8_t TEXT_TO_SEND[] = {"正点原子 STM32 DMA 串口实验"}; /* 要循环发送的字符串 */
#define SEND_BUF_SIZE (sizeof(TEXT_TO_SEND) + 2) * 200 /* 发送数据长度, 等于sizeof(TEXT_TO_SEND) + 2的200倍. */
uint8_t g_sendbuf[SEND_BUF_SIZE]; /* 发送数据缓冲区 */
extern DMA_HandleTypeDef g_dma_handle; /* DMA句柄 */
extern UART_HandleTypeDef g_uart1_handle; /* UART句柄 */
int main(void)
{
uint8_t key = 0;
uint16_t i, k;
uint16_t len;
uint8_t mask = 0;
float pro = 0; /* 进度 */
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
delay_init(72); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
led_init(); /* 初始化LED */
lcd_init(); /* 初始化LCD */
key_init(); /* 初始化按键 */
dma_init(DMA1_Channel4); /* 初始化串口1 TX DMA */
lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
lcd_show_string(30, 70, 200, 16, 16, "DMA TEST", RED);
lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
lcd_show_string(30, 110, 200, 16, 16, "KEY0:Start", RED);
len = sizeof(TEXT_TO_SEND);
k = 0;
for (i = 0; i < SEND_BUF_SIZE; i++) /* 填充ASCII字符集数据 */
{
if (k >= len) /* 入换行符 */
{
if (mask)
{
g_sendbuf[i] = 0x0a;
k = 0;
}
else
{
g_sendbuf[i] = 0x0d;
mask++;
}
}
else /* 复制TEXT_TO_SEND语句 */
{
mask = 0;
g_sendbuf[i] = TEXT_TO_SEND[k];
k++;
}
}
i = 0;
while (1)
{
key = key_scan(0);
if (key == KEY0_PRES) /* KEY0按下 */
{
printf("\r\nDMA DATA:\r\n");
lcd_show_string(30, 130, 200, 16, 16, "Start Transimit....", BLUE);
lcd_show_string(30, 150, 200, 16, 16, " %", BLUE); /* 显示百分号 */
HAL_UART_Transmit_DMA(&g_uart1_handle, g_sendbuf, SEND_BUF_SIZE);
/* 等待DMA传输完成,此时我们来做另外一些事情,比如点灯
* 实际应用中,传输数据期间,可以执行另外的任务
*/
while (1)
{
if ( __HAL_DMA_GET_FLAG(&g_dma_handle, DMA_FLAG_TC4)) /* 等待 DMA1_Channel4 传输完成 */
{
__HAL_DMA_CLEAR_FLAG(&g_dma_handle, DMA_FLAG_TC4);
HAL_UART_DMAStop(&g_uart1_handle); /* 传输完成以后关闭串口DMA */
break;
}
pro = DMA1_Channel4->CNDTR; /* 得到当前还剩余多少个数据 */
len = SEND_BUF_SIZE; /* 总长度 */
pro = 1 - (pro / len); /* 得到百分比 */
pro *= 100; /* 扩大100倍 */
lcd_show_num(30, 150, pro, 3, 16, BLUE);
}
lcd_show_num(30, 150, 100, 3, 16, BLUE); /* 显示100% */
lcd_show_string(30, 130, 200, 16, 16, "Transimit Finished!", BLUE); /* 提示传送完成 */
}
i++;
delay_ms(10);
if (i == 20)
{
LED0_TOGGLE(); /* LED0闪烁,提示系统正在运行 */
i = 0;
}
}
}