一句话解释:
DMA的特点就是无需CPU的参与就可以直接访问内存(可以直接读取内存的数据,也可以直接传数据给内存)
这个内存一般指的是片内SRAM、片内Flash

我举个例子:
有一个温度传感器,它以较高的频率(例如每秒1000次)采样温度数据,并通过SPI(Serial Peripheral Interface)接口将数据发送到STM32。你需要将这些数据存储到内存中,以便后续进行数据分析或处理。
如果用CPU的话,CPU需要频繁的从SPI接口读取数据并写入内存,会占用大量的CPU时间影响其它任务的执行。另外,CPU在数据搬运上的效率低。
那怎么办呢?
我们可以不通过CPU,让DMA当这个中间人,外设读到什么数据就直接写入到内存。让CPU做别的事情去,不会影响到CPU正常处理。

步入正题
STM32F103有 2 个 DMA 控制器,分别是DMA1和DMA2
DMA1 有 7 个通道、DMA2 有 5个通道
每个通道专门用来管理来自于一个或多个外设对存储器访问的请求
在同一个DMA模块上,多个请求间的优先权可以通过软件设置。
如果2个请求有相同的软件优先级,则较低编号的通道比较高编号的通道有较高的优先权。举个例子,通道2优先于通道4
代码设计
定义全局变量
cpp
#define SPI_RX_BUFFER_SIZE 1000 // 缓冲区大小,假设每秒采样1000次
uint8_t SPI_RxBuffer[SPI_RX_BUFFER_SIZE]; // 存储接收到的数据
volatile uint8_t DMA_TcFlag = 0; // DMA传输完成标志
初始化结构体
cpp
typedef struct
{
uint32_t DMA_PeripheralBaseAddr; // 外设地址
uint32_t DMA_MemoryBaseAddr; // 存储器地址
uint32_t DMA_DIR; // 传输方向
uint32_t DMA_BufferSize; // 传输数目
uint32_t DMA_PeripheralInc; // 外设地址增量模式
uint32_t DMA_MemoryInc; // 存储器地址增量模式
uint32_t DMA_PeripheralDataSize; // 外设数据宽度
uint32_t DMA_MemoryDataSize; // 存储器数据宽度
uint32_t DMA_Mode; // 模式选择
uint32_t DMA_Priority; // 通道优先级
uint32_t DMA_M2M; // 存储器到存储器模式
} DMA_InitTypeDef;
DMA初始化函数
cpp
void My_DMA_Init(void)
{
// 1. 打开DMA1控制器时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);
// 2. 配置DMA2通道3作为SPI1接收 : 外设 -> 内存
DMA_InitTypeDef DMA_Config;
DMA_Config.DMA_Channel = DMA_Channel_3; // SPI1_RX
DMA_Config.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR; // SPI1数据寄存器地址
DMA_Config.DMA_MemoryBaseAddr = (uint32_t)SPI_RxBuffer; // 内存缓冲区地址
DMA_Config.DMA_DIR = DMA_DIR_PeripheralSRC; // 外设 -> 内存
DMA_Config.DMA_BufferSize = SPI_RX_BUFFER_SIZE; // 缓冲区大小
DMA_Config.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不递增
DMA_Config.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址递增
DMA_Config.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // 数据大小为字节
DMA_Config.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_Config.DMA_Mode = DMA_Mode_Normal; // 普通模式
DMA_Config.DMA_Priority = DMA_Priority_High; // 高优先级
DMA_Config.DMA_FIFOMode = DMA_FIFOMode_Disable; // 禁用FIFO
DMA_Config.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
DMA_Config.DMA_MemoryBurst = DMA_MemoryBurst_Single;
DMA_Config.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
DMA_Init(DMA2_Stream3, &DMA_Config);
// 3. 配置DMA2通道3支持DMA_IT_TC中断
DMA_ITConfig(DMA2_Stream3, DMA_IT_TC, ENABLE);
// 4. 配置NVIC支持DMA2通道3中断
NVIC_InitTypeDef NVIC_Config;
NVIC_Config.NVIC_IRQChannel = DMA2_Stream3_IRQn; // DMA2通道3中断
NVIC_Config.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_Config.NVIC_IRQChannelSubPriority = 0;
NVIC_Config.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_Config);
}
有一些参数可以更改:





SPI初始化函数
cpp
void My_SPI_Init(void)
{
// 1. 打开SPI1控制器时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
// 2. 配置SPI1
SPI_InitTypeDef SPI_Config;
SPI_Config.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //表示SPI使用两条线(MOSI和MISO)进行全双工通信
//表示SPI1工作在主模式(Master Mode)。主模式下,SPI1可以主动发送数据并控制时钟线(SCK)。
SPI_Config.SPI_Mode = SPI_Mode_Master;
SPI_Config.SPI_DataSize = SPI_DataSize_8b;
SPI_Config.SPI_CPOL = SPI_CPOL_High;
SPI_Config.SPI_CPHA = SPI_CPHA_2Edge;
SPI_Config.SPI_NSS = SPI_NSS_Soft;
SPI_Config.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;
SPI_Config.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_Config.SPI_CRCPolynomial = 7;
SPI_Init(SPI1, &SPI_Config);
// 3. 启用SPI1
SPI_Cmd(SPI1, ENABLE);
// 4. 配置SPI1支持DMA接收。
//这使得SPI1可以在接收到数据时自动触发DMA传输,将数据存储到内存。
SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Rx, ENABLE);
}
DMA中断处理函数
cpp
void DMA2_Stream3_IRQHandler(void)
{
// 1. 判断是否为传输完成中断
if(DMA_GetITStatus(DMA2_Stream3, DMA_IT_TCIF3) != RESET)
{
// 2. 清除中断标志
DMA_ClearITPendingBit(DMA2_Stream3, DMA_IT_TCIF3);
// 3. 关闭DMA通道
DMA_Cmd(DMA2_Stream3, DISABLE);
// 4. 设置传输完成标志
DMA_TcFlag = 1;
}
}
测试函数
cpp
void SPI_DMA_Rx_Test(void)
{
// 1. 初始化缓冲区。清空接收缓冲区,确保没有残留数据。
memset(SPI_RxBuffer, 0, SPI_RX_BUFFER_SIZE);
// 2. 关闭DMA通道
DMA_Cmd(DMA2_Stream3, DISABLE);
// 3. 设置DMA传输长度
DMA_SetCurrDataCounter(DMA2_Stream3, SPI_RX_BUFFER_SIZE);
// 4. 启动DMA接收
SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Rx, ENABLE);
DMA_Cmd(DMA2_Stream3, ENABLE);
// 5. 等待DMA传输完成
while(!DMA_TcFlag)
{
// 可以在这里执行其他任务
}
// 6. 遍历缓冲区,打印接收到的数据
for(int i = 0; i < SPI_RX_BUFFER_SIZE; i++)
{
printf("Data[%d]: %d\n", i, SPI_RxBuffer[i]);
}
// 7. 清除标志
DMA_TcFlag = 0;
}