STM32的DMA解释

一句话解释:

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;
}
相关推荐
Zevalin爱灰灰34 分钟前
编程技巧(基于STM32)第一章 定时器实现非阻塞式程序——按键控制LED灯闪烁模式
stm32·单片机·嵌入式硬件
红花与香菇2____2 小时前
【学习笔记】Cadence电子设计全流程(二)原理图库的创建与设计(上)
笔记·嵌入式硬件·学习·pcb设计·cadence·pcb工艺
Jerry.yl3 小时前
关于 BK3633 上电时受串口 UART2 影响而无法启动的问题说明
嵌入式硬件·物联网·bk3633
苏慕TRYACE4 小时前
RT-Thread+STM32L475VET6实现红外遥控实验
stm32·单片机·嵌入式硬件·rt-thread
小幽余生不加糖5 小时前
deepseek帮我设计物理量采集单片机口保护电路方案
单片机·嵌入式硬件
Ronin-Lotus7 小时前
蓝桥杯篇---IAP15F2K61S2串口
单片机·嵌入式硬件·职场和发展·蓝桥杯·c·iap15f2k61s2
xiaohai@Linux7 小时前
ESP32 在IDF_V5.3.1版本下实现AP无线热点模式!(带WIFI事件处理)
c语言·嵌入式硬件·tcp/ip·wifi·esp32
yyqzjw8 小时前
【STM32】外部时钟|红外反射光电开关
stm32·单片机·嵌入式硬件
charlie1145141918 小时前
(萌新入门)如何从起步阶段开始学习STM32 —— 0.碎碎念
c语言·stm32·单片机·嵌入式硬件·学习·教程
苏慕TRYACE8 小时前
RT-Thread+STM32L475VET6——ADC采集电压
stm32·单片机·嵌入式硬件·rt-thread