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;
}
相关推荐
逼子格1 小时前
五种音频器件综合对比——《器件手册--音频器件》
嵌入式硬件·音视频·硬件工程师·硬件测试·电子器件·硬件笔试真题·音频器件
niuTaylor3 小时前
STM32平衡车开发实战教程:从零基础到项目精通
stm32·单片机·嵌入式硬件
可待电子单片机设计定制(论文)12 小时前
【STM32设计】基于STM32的智能门禁管理系统(指纹+密码+刷卡+蜂鸣器报警)(代码+资料+论文)
stm32·单片机·嵌入式硬件
不可思议迷宫13 小时前
Verilog编程实现一个分秒计数器
单片机·嵌入式硬件·fpga开发
life_yangzi15 小时前
关于单片机IAP升级的那点事儿|智能设置中断向量表
单片机·嵌入式硬件
了一li17 小时前
STM32实现一个简单电灯
stm32·单片机·嵌入式硬件
可待电子单片机设计定制(论文)19 小时前
【STM32设计】数控直流稳压电源的设计与实现(实物+资料+论文)
stm32·嵌入式硬件·mongodb
march_birds20 小时前
FreeRTOS 与 RT-Thread 事件组对比分析
c语言·单片机·算法·系统架构
小麦嵌入式20 小时前
Linux驱动开发实战(十一):GPIO子系统深度解析与RGB LED驱动实践
linux·c语言·驱动开发·stm32·嵌入式硬件·物联网·ubuntu
触角010100011 天前
STM32F103低功耗模式深度解析:从理论到应用实践(上) | 零基础入门STM32第九十二步
驱动开发·stm32·单片机·嵌入式硬件·物联网