
目录
[1. DMA概述](#1. DMA概述)
[1.1 简介](#1.1 简介)
[1.2 存储器映像](#1.2 存储器映像)
[1.3 DMA框图](#1.3 DMA框图)
[1.4 基本结构](#1.4 基本结构)
[1.5 触发源选择](#1.5 触发源选择)
[1.6 数据宽度与对齐](#1.6 数据宽度与对齐)
[2. USART实现数据发送](#2. USART实现数据发送)
[3. DMA实现发送数据转运](#3. DMA实现发送数据转运)
[3.1 DMA初始化](#3.1 DMA初始化)
[3.1.1 传输方向](#3.1.1 传输方向)
[3.1.2 外设与存储器参数配置](#3.1.2 外设与存储器参数配置)
[3.1.2.1 起始地址](#3.1.2.1 起始地址)
[3.1.2.2 数据宽度](#3.1.2.2 数据宽度)
[3.1.2.3 地址是否自增](#3.1.2.3 地址是否自增)
[3.1.3 传输数据的大小](#3.1.3 传输数据的大小)
[3.1.4 模式选择](#3.1.4 模式选择)
[3.1.5 存储器到存储器配置(M2M)](#3.1.5 存储器到存储器配置(M2M))
[3.1.5.1 模式1:存储器到外设(M2P)](#3.1.5.1 模式1:存储器到外设(M2P))
[3.1.5.2 模式2:外设到存储器(P2M)](#3.1.5.2 模式2:外设到存储器(P2M))
[3.1.5.3 模式3:存储器到存储器(M2M)](#3.1.5.3 模式3:存储器到存储器(M2M))
[3.1.6 优先级配置](#3.1.6 优先级配置)
[3.1.7 DMA通道配置](#3.1.7 DMA通道配置)
[3.1.8 DMA中断配置](#3.1.8 DMA中断配置)
[3.2 DMA数据发送](#3.2 DMA数据发送)
[3.3 主函数代码](#3.3 主函数代码)
1. DMA概述
1.1 简介
DMA,全称Direct Memory Access,即直接存储器访问。
DMA传输将数据从一个地址空间复制到另一个地址空间,提供在外设和存储器之间或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源。

如果没有不通过DMA,CPU传输数据还要以内核作为中转站,例如将ADC采集的数据转移到SRAM中。
而如果通过DMA的话,DMA控制器将获取到的外设数据存储到DMA通道中,然后通过DMA总线与DMA总线矩阵协调,将数据传输到SRAM中,期间不需内核参与。
主要特征:
- 同一个DMA模块上,多个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),优先权设置相等时由硬件决定(请求0优先于请求1,依此类推);
- 独立数据源和目标数据区的传输宽度(字节、半字、全字);
- 可编程的数据传输数目:最大为65535;
- 对于大容量的STM32芯片有2个DMA控制器 两个DMA控制器,DMA1有7个通道,DMA2有5个通道。
1.2 存储器映像
计算机系统的五大组成部分:运算器、控制器、存储器、输入设备和输出设备。
其中运算器和控制器合在一起叫CPU。
STM32所有类型的存储器:
|--------|-------------|------------|---------------------|
| 类型 | 起始地址 | 存储器 | 用途 |
| ROM | 0x0800 0000 | 程序存储器Flash | 存储C语言编译后的程序代码 |
| ROM | 0x1FFF F000 | 系统存储器 | 存储BootLoader,用于串口下载 |
| ROM | 0x1FFF F800 | 选项字节 | 存储一些独立于程序代码的配置参数 |
| RAM | 0x2000 0000 | 运行内存SRAM | 存储运行过程中的临时变量 |
| RAM | 0x4000 0000 | 外设寄存器 | 存储各个外设的配置参数 |
| RAM | 0xE000 0000 | 内核外设寄存器 | 存储内核各个外设的配置参数 |
1.3 DMA框图
在看之前我们先需要搞懂一个概念什么是寄存器,寄存器是一种特殊的存储器,寄存器的每一位后面连接着一根导线,可以操作外设电平的状态,完成如操作引脚电平,开关的打开或者关闭,切换数据选择器,当做计数器等的操作:

所以寄存器可以说是连接软件和硬件的桥梁,软件读写寄存器就相当于在控制硬件的执行。
下面我们来看看DMA的框图:
①:DMA总线访问各个存储器;
②:DMA内部的多个通道进行独立的数据转运;
③:仲裁器用于管理多个通道,防止冲突;
④:DMA从设备用于配置DMA参数;
⑤:DMA请求用于硬件触发DMA的数据转运;

1.4 基本结构
上面的图看不懂,没关系,我们总结一下:

我们拆分一下,先看这部分:
可以看出DMA的转运是后方向的,可以外设到内存,也可以内存到外设,我们可以通过函数进行控制:

cpp
#define DMA_DIR_PeripheralDST ((uint32_t)0x00000010)
#define DMA_DIR_PeripheralSRC ((uint32_t)0x00000000)
#define IS_DMA_DIR(DIR) (((DIR) == DMA_DIR_PeripheralDST) || \
((DIR) == DMA_DIR_PeripheralSRC))
然后再来看看二者所需的数据:

首先是基地址,也就是两者的起始地址,这两个参数决定数据从哪里来到哪里去,所需函数:
cpp
//外设
uint32_t DMA_PeripheralBaseAddr; /*!< Specifies the peripheral base address for DMAy Channelx. */
//存储器
uint32_t DMA_MemoryBaseAddr; /*!< Specifies the memory base address for DMAy Channelx. */
然后是数据宽度,其作用计时指定一次转运要按多大的数据宽度来进行,其可以选择字节(uint8_t),半字(uint16_t),字(uint32_t):
cpp
#define DMA_PeripheralDataSize_Byte ((uint32_t)0x00000000)
#define DMA_PeripheralDataSize_HalfWord ((uint32_t)0x00000100)
#define DMA_PeripheralDataSize_Word ((uint32_t)0x00000200)
#define IS_DMA_PERIPHERAL_DATA_SIZE(SIZE) (((SIZE) == DMA_PeripheralDataSize_Byte) || \
((SIZE) == DMA_PeripheralDataSize_HalfWord) || \
((SIZE) == DMA_PeripheralDataSize_Word))
其函数是:
cpp
//外设
uint32_t DMA_PeripheralDataSize; /*!< Specifies the Peripheral data width.
This parameter can be a value of @ref DMA_peripheral_data_size */
//存储器
uint32_t DMA_MemoryDataSize; /*!< Specifies the Memory data width.
This parameter can be a value of @ref DMA_memory_data_size */
地址是否自增,作用是决定下次转运是不是要把地址移到下一个位置去,其参数可以选择使能或者失能:
cpp
#define DMA_PeripheralInc_Enable ((uint32_t)0x00000040)
#define DMA_PeripheralInc_Disable ((uint32_t)0x00000000)
#define IS_DMA_PERIPHERAL_INC_STATE(STATE) (((STATE) == DMA_PeripheralInc_Enable) || \
((STATE) == DMA_PeripheralInc_Disable))
其函数是:
cpp
//外设
uint32_t DMA_PeripheralInc; /*!< Specifies whether the Peripheral address register is incremented or not.
This parameter can be a value of @ref DMA_peripheral_incremented_mode */
//存储器
uint32_t DMA_MemoryInc; /*!< Specifies whether the memory address register is incremented or not.
This parameter can be a value of @ref DMA_memory_incremented_mode */
然后我们看看另一个参数:传输计数器,这个值表示DMA需要转运几次,你可以将其理解为他是一个自减计数器,假如你初始化的值为5,那么每次转运一次计数减1,当减到0的时候,DMA就不会在进行转运了,并且当其减到0,之前自增的地址又会回到起始地址,方便新一轮的转换:

cpp
uint32_t DMA_BufferSize; /*!< Specifies the buffer size, in data unit, of the specified Channel.
The data unit is equal to the configuration set in DMA_PeripheralDataSize
or DMA_MemoryDataSize members depending in the transfer direction. */
那么他是怎么进行新一轮的转换呢?这就要靠自动重装器,其作用就是当转运次数归零后,询问是否将转运次数回到最初值,这样如果我们配置为循环模式,DMA计数归零回到起始地址,而自动重装器又将DMA的数据恢复,这样就可以循环:
cpp
#define DMA_Mode_Circular ((uint32_t)0x00000020)
#define DMA_Mode_Normal ((uint32_t)0x00000000)
#define IS_DMA_MODE(MODE) (((MODE) == DMA_Mode_Circular) || ((MODE) == DMA_Mode_Normal))
函数:
cpp
uint32_t DMA_Mode; /*!< Specifies the operation mode of the DMAy Channelx.
This parameter can be a value of @ref DMA_circular_normal_mode.
@note: The circular buffer mode cannot be used if the memory-to-memory
data transfer is configured on the selected Channel */
然后就是触发机制,主要配置其使能或者失能,使能软件触发,失能硬件触发:

cpp
#define DMA_M2M_Enable ((uint32_t)0x00004000)
#define DMA_M2M_Disable ((uint32_t)0x00000000)
#define IS_DMA_M2M_STATE(STATE) (((STATE) == DMA_M2M_Enable) || ((STATE) == DMA_M2M_Disable))
这里需要注意一点软件触发不能和循环一起使用。
因为软件触发就是想将传输计数器清零,但是循环模式我们上面也说了清零后会进行自动重装,因此不能一起使用。
最后就是,使能DMA,也就是开启DMA:

1.5 触发源选择
对于硬件触发我们需要根据不同的触发源,选择不同的通道,软件触发就随便了:

其不同的外设,需要对应不同的通道,可以参考上下图:

这里我们需要考虑一个问题:当多个DMA请求同时到来时,是如何工作呢?
STM32的DMA仲裁器处理多请求时,其优先级判定分为两个明确的阶段:
软件优先级:这是你可以在程序中配置的。每个数据流(Stream)或通道(Channel)都可以被设置为以下四个等级之一:
- 非常高(Very High)
- 高(High)
- 中(Medium)
- 低(Low)
硬件优先级: 当两个或更多请求的软件优先级相同时,仲裁器会转而依靠硬件规则来裁决:编号较小的数据流或通道,拥有更高的优先级。例如,DMA1的优先级高于DMA2。
1.6 数据宽度与对齐
根据下表,简单来说就是右对齐,要是目标宽度不够,取最低位(可以参考第四行),要是目标宽度比源端宽度大则高位补零(可以参考第二或者三行):

2. USART实现数据发送
对于串口的原理这里不进行过多表述,这里为了方便寻找资源,我直接使用江协/江科大的串口代码,在这里演示一下不使用DMA转运出现的效果。
程序设计:通过USART1发送5000个A的数据,同时通过GPIO0口电亮一颗LED灯进行循环亮灭,看看会出现什么情况。其中工程模版,我用的之前移植好的ZET6的代码:
首先对于串口,可以自己去复制江协的代码,这里我对初始化函数名称进行了一个简单的修改,其他的都没有动:
cpp
#include "stm32f10x.h"
#include <stdio.h>
#include <stdarg.h>
uint8_t Serial_TxPacket[4]; //定义发送数据包数组,数据包格式:FF 01 02 03 04 FE
uint8_t Serial_RxPacket[4]; //定义接收数据包数组
uint8_t Serial_RxFlag; //定义接收数据包标志位
void Usart_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //开启USART1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA9引脚初始化为复用推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA10引脚初始化为上拉输入
/*USART初始化*/
USART_InitTypeDef USART_InitStructure; //定义结构体变量
USART_InitStructure.USART_BaudRate = 9600; //波特率
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制,不需要
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; //模式,发送模式和接收模式均选择
USART_InitStructure.USART_Parity = USART_Parity_No; //奇偶校验,不需要
USART_InitStructure.USART_StopBits = USART_StopBits_1; //停止位,选择1位
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长,选择8位
USART_Init(USART1, &USART_InitStructure); //将结构体变量交给USART_Init,配置USART1
/*中断输出配置*/
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //开启串口接收数据的中断
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //选择配置NVIC的USART1线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC线路的抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
/*USART使能*/
USART_Cmd(USART1, ENABLE); //使能USART1,串口开始运行
}
/**
* 函 数:串口发送一个字节
* 参 数:Byte 要发送的一个字节
* 返 回 值:无
*/
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART1, Byte); //将字节数据写入数据寄存器,写入后USART自动生成时序波形
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); //等待发送完成
/*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/
}
/**
* 函 数:串口发送一个数组
* 参 数:Array 要发送数组的首地址
* 参 数:Length 要发送数组的长度
* 返 回 值:无
*/
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
uint16_t i;
for (i = 0; i < Length; i ++) //遍历数组
{
Serial_SendByte(Array[i]); //依次调用Serial_SendByte发送每个字节数据
}
}
/**
* 函 数:串口发送一个字符串
* 参 数:String 要发送字符串的首地址
* 返 回 值:无
*/
void Serial_SendString(char *String)
{
uint8_t i;
for (i = 0; String[i] != '\0'; i ++)//遍历字符数组(字符串),遇到字符串结束标志位后停止
{
Serial_SendByte(String[i]); //依次调用Serial_SendByte发送每个字节数据
}
}
/**
* 函 数:次方函数(内部使用)
* 返 回 值:返回值等于X的Y次方
*/
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1; //设置结果初值为1
while (Y --) //执行Y次
{
Result *= X; //将X累乘到结果
}
return Result;
}
/**
* 函 数:串口发送数字
* 参 数:Number 要发送的数字,范围:0~4294967295
* 参 数:Length 要发送数字的长度,范围:0~10
* 返 回 值:无
*/
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i ++) //根据数字长度遍历数字的每一位
{
Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0'); //依次调用Serial_SendByte发送每位数字
}
}
/**
* 函 数:使用printf需要重定向的底层函数
* 参 数:保持原始格式即可,无需变动
* 返 回 值:保持原始格式即可,无需变动
*/
int fputc(int ch, FILE *f)
{
Serial_SendByte(ch); //将printf的底层重定向到自己的发送字节函数
return ch;
}
/**
* 函 数:自己封装的prinf函数
* 参 数:format 格式化字符串
* 参 数:... 可变的参数列表
* 返 回 值:无
*/
void Serial_Printf(char *format, ...)
{
char String[100]; //定义字符数组
va_list arg; //定义可变参数列表数据类型的变量arg
va_start(arg, format); //从format开始,接收参数列表到arg变量
vsprintf(String, format, arg); //使用vsprintf打印格式化字符串和参数列表到字符数组中
va_end(arg); //结束变量arg
Serial_SendString(String); //串口发送字符数组(字符串)
}
/**
* 函 数:串口发送数据包
* 参 数:无
* 返 回 值:无
* 说 明:调用此函数后,Serial_TxPacket数组的内容将加上包头(FF)包尾(FE)后,作为数据包发送出去
*/
void Serial_SendPacket(void)
{
Serial_SendByte(0xFF);
Serial_SendArray(Serial_TxPacket, 4);
Serial_SendByte(0xFE);
}
/**
* 函 数:获取串口接收数据包标志位
* 参 数:无
* 返 回 值:串口接收数据包标志位,范围:0~1,接收到数据包后,标志位置1,读取后标志位自动清零
*/
uint8_t Serial_GetRxFlag(void)
{
if (Serial_RxFlag == 1) //如果标志位为1
{
Serial_RxFlag = 0;
return 1; //则返回1,并自动清零标志位
}
return 0; //如果标志位为0,则返回0
}
/**
* 函 数:USART1中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void USART1_IRQHandler(void)
{
static uint8_t RxState = 0; //定义表示当前状态机状态的静态变量
static uint8_t pRxPacket = 0; //定义表示当前接收数据位置的静态变量
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) //判断是否是USART1的接收事件触发的中断
{
uint8_t RxData = USART_ReceiveData(USART1); //读取数据寄存器,存放在接收的数据变量
/*使用状态机的思路,依次处理数据包的不同部分*/
/*当前状态为0,接收数据包包头*/
if (RxState == 0)
{
if (RxData == 0xFF) //如果数据确实是包头
{
RxState = 1; //置下一个状态
pRxPacket = 0; //数据包的位置归零
}
}
/*当前状态为1,接收数据包数据*/
else if (RxState == 1)
{
Serial_RxPacket[pRxPacket] = RxData; //将数据存入数据包数组的指定位置
pRxPacket ++; //数据包的位置自增
if (pRxPacket >= 4) //如果收够4个数据
{
RxState = 2; //置下一个状态
}
}
/*当前状态为2,接收数据包包尾*/
else if (RxState == 2)
{
if (RxData == 0xFE) //如果数据确实是包尾部
{
RxState = 0; //状态归0
Serial_RxFlag = 1; //接收数据包标志位置1,成功接收一个数据包
}
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE); //清除标志位
}
}
cpp
#ifndef __SERIAL_H
#define __SERIAL_H
#include <stdio.h>
extern uint8_t Serial_TxPacket[];
extern uint8_t Serial_RxPacket[];
void Usart_Init(void);
void Serial_SendByte(uint8_t Byte);
void Serial_SendArray(uint8_t *Array, uint16_t Length);
void Serial_SendString(char *String);
void Serial_SendNumber(uint32_t Number, uint8_t Length);
void Serial_Printf(char *format, ...);
void Serial_SendPacket(void);
uint8_t Serial_GetRxFlag(void);
#endif
来到主函数,我们创建一个宏定义,循环在串口发送缓冲区写入5000个A,然后进行发送,为了方便观察实验效果,我将该函数写在while循环之外,这样发送完成后LED灯就能正常工作,前后可以做个对比:
cpp
#define Sum_Duff_Size 5000
for(int i = 0;i<Sum_Duff_Size;i++)
{
Serial_TxPacket[Sum_Duff_Size] = 'P';
}
Serial_SendArray(Serial_TxPacket, Sum_Duff_Size);
完整main函数:
cpp
#include "stm32f10x.h"
#include "LED.h"
#include "Usart_DMA.h"
#include "Delay.h"
#define Sum_Duff_Size 5000
int main(void)
{
LED_GPIO_Config();
Usart_Init();
for(int i = 0;i<Sum_Duff_Size;i++)
{
Serial_TxPacket[i] = 'P';
}
Serial_SendArray(Serial_TxPacket, Sum_Duff_Size);
while (1)
{
GPIO_SetBits(GPIOB,GPIO_Pin_0);
Delay_ms(500);
GPIO_ResetBits(GPIOB,GPIO_Pin_0);
Delay_ms(500);
}
}
此时我们下载程序,会发现串口持续打印数据,但是此时LED灯被卡着无法进行闪烁,需要等待数据发送完成才能进行下一步的操作:

由于每发送或接收一个字节,串口都会产生一个中断。CPU需要停下当前的工作,保存现场,跳转到中断服务程序,处理这一个字节的数据(例如,将数据从接收缓冲区复制到用户数组,或从用户数组加载下一个要发送的字节),然后恢复现场,继续之前的工作。这么做就会导致CPU大量时间浪费在数据的传输上,阻塞后续功能的实现,并且对于115200的波特率,每秒就有115200个字节,意味着CPU每秒要被中断11.5万次,负担极重。
为了减轻CPU的负担,我们引入DMA进行数据转运。DMA模式下CPU只需要在传输开始前,对DMA控制器进行配置,告诉它数据的源地址、目标地址和传输长度。之后,整个数据块的传输(比如1024个字节)就完全由DMA控制器接管。在整个块传输完成或达到半传输、传输完成时,DMA才会产生一个中断通知CPU。CPU在此期间可以完全不受打扰地执行其他任务,或者进入低功耗模式。
3. DMA实现发送数据转运
在开始编写代码前我们进行一些准备工作,首先找到这张图,我们根据这张图进行DMA代码的配置:

为了方便代码的阅读,我们将上方串口的代码部分,除了串口初始化的代码,其余全部删除:
cpp
#include "stm32f10x.h"
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
void Usart_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //开启USART1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA9引脚初始化为复用推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA10引脚初始化为上拉输入
/*USART初始化*/
USART_InitTypeDef USART_InitStructure; //定义结构体变量
USART_InitStructure.USART_BaudRate = 9600; //波特率
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制,不需要
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; //模式,发送模式和接收模式均选择
USART_InitStructure.USART_Parity = USART_Parity_No; //奇偶校验,不需要
USART_InitStructure.USART_StopBits = USART_StopBits_1; //停止位,选择1位
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长,选择8位
USART_Init(USART1, &USART_InitStructure); //将结构体变量交给USART_Init,配置USART1
/*中断输出配置*/
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //开启串口接收数据的中断
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //选择配置NVIC的USART1线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC线路的抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
/*USART使能*/
USART_Cmd(USART1, ENABLE); //使能USART1,串口开始运行
}
添加头文件:
cpp
#include "Usart_DMA.h"
3.1 DMA初始化
首先常规的初始化配置,开启DMA的时钟,定义结构体变量:
cpp
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 开启DMA时钟
DMA_InitTypeDef DMA_InitStructure; // 定义结构体变量
对于结构体变量的参数:
cpp
/**
* @brief DMA 初始化结构体定义
*/
typedef struct
{
uint32_t DMA_PeripheralBaseAddr; /*!< 指定DMAy通道x的外设基地址 */
uint32_t DMA_MemoryBaseAddr; /*!< 指定DMAy通道x的存储器基地址 */
uint32_t DMA_DIR; /*!< 指定外设是源还是目的地。
此参数可以是 @ref DMA_data_transfer_direction 中的值 */
uint32_t DMA_BufferSize; /*!< 指定通道的缓冲区大小,以数据单元为单位。
数据单元等于在DMA_PeripheralDataSize或DMA_MemoryDataSize
成员中设置的配置,具体取决于传输方向 */
uint32_t DMA_PeripheralInc; /*!< 指定外设地址寄存器是否递增。
此参数可以是 @ref DMA_peripheral_incremented_mode 中的值 */
uint32_t DMA_MemoryInc; /*!< 指定存储器地址寄存器是否递增。
此参数可以是 @ref DMA_memory_incremented_mode 中的值 */
uint32_t DMA_PeripheralDataSize; /*!< 指定外设数据宽度。
此参数可以是 @ref DMA_peripheral_data_size 中的值 */
uint32_t DMA_MemoryDataSize; /*!< 指定存储器数据宽度。
此参数可以是 @ref DMA_memory_data_size 中的值 */
uint32_t DMA_Mode; /*!< 指定DMAy通道x的操作模式。
此参数可以是 @ref DMA_circular_normal_mode 中的值。
@注意:如果在所选通道上配置了存储器到存储器数据传输,
则不能使用循环缓冲区模式 */
uint32_t DMA_Priority; /*!< 指定DMAy通道x的软件优先级。
此参数可以是 @ref DMA_priority_level 中的值 */
uint32_t DMA_M2M; /*!< 指定DMAy通道x是否将用于存储器到存储器传输。
此参数可以是 @ref DMA_memory_to_memory 中的值 */
}DMA_InitTypeDef;
3.1.1 传输方向
根据框图我们知道,DMA数据转运的方向有三种:外设到存储器、存储器到外设、存储器到存储器。

而我们想要通过芯片向电脑传输数据,就是存储器到外设,因此配备:
cpp
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; // 数据传输方向:存储器到外设
对于DMA_DIR的参数,可以参考:
cpp
/** @defgroup DMA_data_transfer_direction
* @{
*/
#define DMA_DIR_PeripheralDST ((uint32_t)0x00000010)//存储器→外设
#define DMA_DIR_PeripheralSRC ((uint32_t)0x00000000)//外设→存储器
#define IS_DMA_DIR(DIR) (((DIR) == DMA_DIR_PeripheralDST) || \
((DIR) == DMA_DIR_PeripheralSRC))
对于存储器到存储器的设置,又一个具体的参数下面会涉及到,可以通过目录直接跳转过去。
3.1.2 外设与存储器参数配置
我们需要对如下参数进行配置:

3.1.2.1 起始地址
首先对于起始地址,对于外设的起始地址,由于我们使用的是串口1进行数据传输,所以这里我们需要采用其数据寄存器(DR)的地址,这里我们创建一个宏定义,使串口1指向DR寄存器:
cpp
#define USART_DR_ADDRESS ((uint32_t)&(USART1->DR))
对于存储器的起始地址,我们创建一个发送缓冲区(也就是一个数组即可):
cpp
#define USART_TX_BUFFER_SIZE 5000 // 定义发送缓冲区大小
uint8_t Usart_Tx_Buf[USART_TX_BUFFER_SIZE];
此时的起始地址配置:
cpp
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)USART_DR_ADDRESS; // 外设基地址:串口1数据寄存器
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)Usart_Tx_Buf; // 存储器基地址:发送缓冲区
3.1.2.2 数据宽度
对于数据宽度的取值,可以分为:
cpp
/** @defgroup DMA_peripheral_data_size
* @{
*/
#define DMA_PeripheralDataSize_Byte ((uint32_t)0x00000000)// 字节(8位)
#define DMA_PeripheralDataSize_HalfWord ((uint32_t)0x00000100)// 半字(16位)
#define DMA_PeripheralDataSize_Word ((uint32_t)0x00000200)// 字(32位)
#define IS_DMA_PERIPHERAL_DATA_SIZE(SIZE) (((SIZE) == DMA_PeripheralDataSize_Byte) || \
((SIZE) == DMA_PeripheralDataSize_HalfWord) || \
((SIZE) == DMA_PeripheralDataSize_Word))
| 宏定义 | 数据宽度 | 字节数 | 适用场景 |
|---|---|---|---|
| DMA_PeripheralDataSize_Byte | 8位 | 1字节 | 串口、SPI字节传输 |
| DMA_PeripheralDataSize_HalfWord | 16位 | 2字节 | ADC12位数据、16位外设 |
| DMA_PeripheralDataSize_Word | 32位 | 4字节 | 32位外设、内存批量操作 |
这里全设成字节:
cpp
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // 外设数据宽度:字节
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; // 存储器数据宽度:字节
3.1.2.3 地址是否自增
这个可以简单的看出,Enable使能自增,Disable禁止自增:
cpp
/** @defgroup DMA_peripheral_incremented_mode
* @{
*/
#define DMA_PeripheralInc_Enable ((uint32_t)0x00000040)
#define DMA_PeripheralInc_Disable ((uint32_t)0x00000000)
#define IS_DMA_PERIPHERAL_INC_STATE(STATE) (((STATE) == DMA_PeripheralInc_Enable) || \
((STATE) == DMA_PeripheralInc_Disable))
首先对于外设地址,前面我们也提到了,我们使用的是串口的数据寄存器(DR),所有数据都要写入到同一个USART数据寄存器,串口硬件会自动将数据移出,不需要改变寄存器地址:
cpp
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不自增
对于存储器的数据,如果不自增,所有数据都会写入同一个内存位置,会进行数据的覆盖,导致数据丢失,因此需要自增:
cpp
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 存储器地址自增
3.1.3 传输数据的大小
也就是一次性传输数据的量,我们前面举例传输的是5000字节,那么这里也设置为5000:
cpp
#define SENDBUFF_SIZE 5000 // 一次发送的数据量
DMA_InitStructure.DMA_BufferSize = SENDBUFF_SIZE; // 传输数据大小
注意,这个量不是代表的字节数,而是而是数据项的数量,例如:
cpp
// 示例1:字节传输
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_BufferSize = 100; // 传输100个字节(100 * 1字节)
// 示例2:半字传输
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_BufferSize = 100; // 传输200个字节(100 * 2字节)
// 示例3:字传输
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;
DMA_InitStructure.DMA_BufferSize = 100; // 传输400个字节(100 * 4字节)
3.1.4 模式选择
模式选择可以进行单次模式或者循环模式进行数据传输:
cpp
#define DMA_Mode_Circular ((uint32_t)0x00000020) // 循环模式
#define DMA_Mode_Normal ((uint32_t)0x00000000) // 普通模式
#define IS_DMA_MODE(MODE) (((MODE) == DMA_Mode_Circular) || ((MODE) == DMA_Mode_Normal))
这里我们只进行单次传输:
cpp
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // 普通模式(发送完成停止)
3.1.5 存储器到存储器配置(M2M)
对于存储器到存储器的配置有专门的参数配置:
cpp
uint32_t DMA_M2M; /*!< Specifies if the DMAy Channelx will be used in memory-to-memory transfer.
This parameter can be a value of @ref DMA_memory_to_memory */
其参数如下:
cpp
/** @defgroup DMA_memory_to_memory
* @{
*/
#define DMA_M2M_Enable ((uint32_t)0x00004000)
#define DMA_M2M_Disable ((uint32_t)0x00000000)
#define IS_DMA_M2M_STATE(STATE) (((STATE) == DMA_M2M_Enable) || ((STATE) == DMA_M2M_Disable))
常见的配置有:
3.1.5.1 模式1:存储器到外设(M2P)
cpp
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 禁用M2M模式
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; // 存储器→外设
3.1.5.2 模式2:外设到存储器(P2M)
cpp
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 禁用M2M模式
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 外设→存储器
3.1.5.3 模式3:存储器到存储器(M2M)
cpp
DMA_InitStructure.DMA_M2M = DMA_M2M_Enable; // 使能M2M模式
// 注意:M2M模式下DMA_DIR参数通常被忽略
3.1.6 优先级配置
对于优先级的配置,我们上面介绍的时候知道其优先级分为四级:
cpp
#define DMA_Priority_VeryHigh ((uint32_t)0x00003000) // 非常高优先级
#define DMA_Priority_High ((uint32_t)0x00002000) // 高优先级
#define DMA_Priority_Medium ((uint32_t)0x00001000) // 中等优先级
#define DMA_Priority_Low ((uint32_t)0x00000000) // 低优先级
#define IS_DMA_PRIORITY(PRIORITY) (((PRIORITY) == DMA_Priority_VeryHigh) || \
((PRIORITY) == DMA_Priority_High) || \
((PRIORITY) == DMA_Priority_Medium) || \
((PRIORITY) == DMA_Priority_Low))
不同优先级的适用场景:
| 优先级级别 | 数值 | 适用场景 |
|---|---|---|
| VeryHigh | 最高 | 实时性要求极高的数据传输 |
| High | 高 | 重要外设,如USB、以太网 |
| Medium | 中 | 一般外设,如SPI、I2C |
| Low | 低 | 不重要的后台传输 |
cpp
DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh; // 最高优先级
3.1.7 DMA通道配置
由于我们使用的是USART1_TX,因此需要初始化通道4:

cpp
DMA_Init(DMA1_Channel4, &DMA_InitStructure);// 初始化DMA通道 - USART1_TX使用通道4
同时为了确保DMA从干净的状态开始工作,避免误触发中断或错误状态,这里我们还需要清除所有旧的状态标志位:
cpp
DMA_ClearFlag(DMA1_FLAG_GL4 | DMA1_FLAG_TC4 | DMA1_FLAG_HT4 | DMA1_FLAG_TE4);// 清除DMA通道4的所有标志位
对于这几个标志位的含义:
| 标志位 | 含义 | 说明 |
|---|---|---|
| DMA1_FLAG_GL4 | 全局标志 | 通道4的全局中断标志 |
| DMA1_FLAG_TC4 | 传输完成 | 数据全部传输完成时置位 |
| DMA1_FLAG_HT4 | 半传输完成 | 传输完成一半数据时置位 |
| DMA1_FLAG_TE4 | 传输错误 | 传输过程中发生错误时置位 |
对于DMA1的各通道标志位,其实就更改一下后缀的数字:
| 通道 | 全局标志 | 传输完成 | 半传输 | 传输错误 |
|---|---|---|---|---|
| Channel1 | DMA1_FLAG_GL1 | DMA1_FLAG_TC1 | DMA1_FLAG_HT1 | DMA1_FLAG_TE1 |
| Channel2 | DMA1_FLAG_GL2 | DMA1_FLAG_TC2 | DMA1_FLAG_HT2 | DMA1_FLAG_TE2 |
| Channel3 | DMA1_FLAG_GL3 | DMA1_FLAG_TC3 | DMA1_FLAG_HT3 | DMA1_FLAG_TE3 |
| Channel4 | DMA1_FLAG_GL4 | DMA1_FLAG_TC4 | DMA1_FLAG_HT4 | DMA1_FLAG_TE4 |
| Channel5 | DMA1_FLAG_GL5 | DMA1_FLAG_TC5 | DMA1_FLAG_HT5 | DMA1_FLAG_TE5 |
| Channel6 | DMA1_FLAG_GL6 | DMA1_FLAG_TC6 | DMA1_FLAG_HT6 | DMA1_FLAG_TE6 |
| Channel7 | DMA1_FLAG_GL7 | DMA1_FLAG_TC7 | DMA1_FLAG_HT7 | DMA1_FLAG_TE7 |
3.1.8 DMA中断配置
首先使能传输完成中断:
cpp
DMA_ITConfig(DMA1_Channel4, DMA_IT_TC, ENABLE);// 使能传输完成中断
其中对于DMA的中断类型:
cpp
DMA_IT_TC // Transfer Complete - 传输完成中断
DMA_IT_HT // Half Transfer - 半传输完成中断
DMA_IT_TE // Transfer Error - 传输错误中断
配置NVIC:
cpp
// 配置DMA传输完成中断
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel4_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
初始不使能DMA,等待数据准备好后再使能:
cpp
DMA_Cmd(DMA1_Channel4, DISABLE);
3.2 DMA数据发送
首先创建一个数据发送标志位,注意使用volatile声明,防止编译器优化:
cpp
volatile uint8_t dma_busy = 0; // DMA发送状态标志
设置一个while循环,根据标志位,等待DMA数据发送完成:
cpp
void USART_WaitDMAFinish(void)
{
while (dma_busy)
{
// 等待DMA发送完成
}
}
首先判断一下传输的数据是否为空,或者超出缓冲区范围,如果是则直接返回,不进行数据传输,若是满足,则通过wait函数等待上一次的数据完成,完成后复制本次数据到缓冲区,配置DMA传输数据长度,然后清除标志位,设置忙状态,然后使能DMA发送中断:
cpp
void USART_SendDataDMA(uint8_t *data, uint16_t len)
{
//若是数据为0,或者超范围直接返回,不进行数据转运
if (len == 0 || len > USART_TX_BUFFER_SIZE)
return;
USART_WaitDMAFinish();// 等待上一次DMA传输完成
memcpy(Usart_Tx_Buf, data, len);// 复制数据到发送缓冲区
DMA_SetCurrDataCounter(DMA1_Channel4, len);// 配置DMA传输数据长度
DMA_ClearFlag(DMA1_FLAG_GL4 | DMA1_FLAG_TC4 | DMA1_FLAG_HT4 | DMA1_FLAG_TE4); // 清除所有DMA标志
dma_busy = 1;// 设置忙标志
DMA_Cmd(DMA1_Channel4, ENABLE); // 使能DMA
USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);// 使能USART的DMA发送
}
然后回进入到中断当中,如果数据传输完成标志位置0,进行下一次数据传输,这里我们只进行发送,不进行后续处理,因此没有后续代码,想要进行一些处理的可以自行添加:
cpp
// DMA传输完成中断服务函数
void DMA1_Channel4_IRQHandler(void)
{
if (DMA_GetITStatus(DMA1_IT_TC4))
{
DMA_ClearITPendingBit(DMA1_IT_TC4); // 清除中断标志
DMA_Cmd(DMA1_Channel4, DISABLE); // 禁用DMA
dma_busy = 0; // 清除忙标志
// 可以在这里添加发送完成回调函数
}
}
3.3 主函数代码
主函数部分就是将刚刚的串口发送,更改为DMA的发送:
cpp
#include "stm32f10x.h"
#include "LED.h"
#include "Usart_DMA.h"
#include "Delay.h"
#define Sum_Duff_Size 5000
extern uint8_t Usart_Tx_Buf[USART_TX_BUFFER_SIZE];
int main(void)
{
LED_GPIO_Config();
Usart_Init();
DMA_Config();
for(int i = 0;i<Sum_Duff_Size;i++)
{
Usart_Tx_Buf[i] = 'A';
}
USART_SendDataDMA(Usart_Tx_Buf,Sum_Duff_Size);
while (1)
{
GPIO_SetBits(GPIOB,GPIO_Pin_0);
Delay_ms(500);
GPIO_ResetBits(GPIOB,GPIO_Pin_0);
Delay_ms(500);
}
}
我们可以运行看一下效果:

运行后可以发现,串口在正常打印数据的同时,LED灯也在正常的闪烁(这里没法放视频,就不进行演示了,可以自行看一下效果)。
完整工程:

STM32学习笔记_时光の尘的博客-CSDN博客
FreeRTOS实战(四)·USART串口实现DMA数据转运(江协/江科大代码移植)_freertos dma-CSDN博客
