一句话解释:
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
代码设计
定义全局变量
#define SPI_RX_BUFFER_SIZE 1000 // 缓冲区大小,假设每秒采样1000次
uint8_t SPI_RxBuffer[SPI_RX_BUFFER_SIZE]; // 存储接收到的数据
volatile uint8_t DMA_TcFlag = 0; // DMA传输完成标志
初始化结构体
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初始化函数
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_RXDMA_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; // 禁用FIFODMA_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初始化函数
void My_SPI_Init(void)
{// 1. 打开SPI1控制器时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);// 2. 配置SPI1SPI_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. 启用SPI1SPI_Cmd(SPI1, ENABLE);// 4. 配置SPI1支持DMA接收。//这使得SPI1可以在接收到数据时自动触发DMA传输,将数据存储到内存。SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Rx, ENABLE);
}
DMA中断处理函数
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;}
}
测试函数
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;
}