1. STM32 DMA基础概念与工作模式
DMA(Direct Memory Access)直接存储器访问是STM32微控制器中一个非常重要的外设模块。它允许数据在外设和存储器之间或者存储器与存储器之间直接传输,而无需CPU的干预。这种机制可以显著提高系统性能,特别是在需要高速数据传输的场景中。
1.1 DMA的核心优势
DMA控制器的主要优势体现在三个方面:
-
解放CPU资源:传统的数据传输需要CPU参与每个字节的搬运工作,而DMA可以在后台自动完成这些操作,让CPU专注于其他计算任务。例如,在进行ADC采样时,使用DMA可以将采样数据直接存储到指定内存区域,而不需要CPU频繁中断来处理每个采样点。
-
高速数据传输:DMA控制器采用专门的硬件通道进行数据传输,速度远高于CPU通过软件搬运数据。在STM32F4系列中,DMA控制器通过AHB总线矩阵直接访问存储器,可以实现高达168MHz的工作频率。
-
精确的传输控制:DMA提供了丰富的控制选项,包括传输长度、数据宽度、地址自动递增、循环模式等,可以满足各种复杂的数据传输需求。
1.2 STM32 DMA的三种工作模式
STM32的DMA控制器支持三种基本工作模式:
-
外设到存储器模式:将外设数据寄存器中的内容传输到指定的内存空间。这种模式常用于数据采集场景,比如ADC转换结果的自动存储。
-
存储器到外设模式:将特定存储区内容传输至外设的数据寄存器。这种模式常用于通信外设的数据发送,如USART、SPI等。
-
存储器到存储器模式:将一个存储区内容拷贝到另一个存储区。功能类似于C语言的memcpy函数,但效率更高,因为完全由硬件实现。
注意:在STM32F4系列中,只有DMA2控制器支持存储器到存储器模式,DMA1不支持此功能。这是因为DMA1的外设端口没有连接到总线矩阵,只能访问APB1外设。
2. DMA存储器到存储器模式详解
2.1 硬件架构与配置要点
存储器到存储器模式是DMA最基础的应用之一,它实现了内存数据的快速拷贝。在STM32F4系列中,这一功能由DMA2控制器提供。
2.1.1 关键寄存器配置
实现存储器到存储器传输需要配置以下几个关键寄存器:
-
DMA_SxCR(数据流x配置寄存器):
- DIR[1:0]位:设置为10表示存储器到存储器模式
- MINC/PINC位:控制存储器/外设地址是否自动递增
- MSIZE/PSIZE位:设置存储器/外设数据宽度(字节、半字或字)
- CIRC位:循环模式使能
- EN位:使能数据流
-
DMA_SxPAR:在外设地址寄存器中存放源存储器地址(在存储器到存储器模式下)
-
DMA_SxM0AR:在存储器0地址寄存器中存放目标存储器地址
-
DMA_SxNDTR:设置要传输的数据项数目
2.1.2 配置流程示例
以下是配置DMA存储器到存储器模式的标准流程:
- 使能DMA2时钟:
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE) - 复位DMA数据流:
DMA_DeInit(DMA2_Stream0) - 等待复位完成:检查
DMA_GetCmdStatus()返回值 - 配置DMA初始化结构体:
c复制DMA_InitStructure.DMA_Channel = DMA_Channel_0; DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)srcBuffer; DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)destBuffer; DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToMemory; DMA_InitStructure.DMA_BufferSize = bufferSize; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; - 初始化DMA:
DMA_Init(DMA2_Stream0, &DMA_InitStructure) - 使能DMA数据流:
DMA_Cmd(DMA2_Stream0, ENABLE)
2.2 实际应用案例
2.2.1 数据块快速拷贝
存储器到存储器模式最常见的应用就是数据块的快速拷贝。下面是一个完整的示例代码:
c复制#define BUFFER_SIZE 32
const uint32_t srcBuffer[BUFFER_SIZE] = {
0x01020304, 0x05060708, /* ... 其他数据 ... */
};
uint32_t destBuffer[BUFFER_SIZE];
void DMA_MemCopy_Config(void)
{
DMA_InitTypeDef DMA_InitStructure;
/* 使能DMA2时钟 */
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);
/* 复位DMA流0 */
DMA_DeInit(DMA2_Stream0);
while(DMA_GetCmdStatus(DMA2_Stream0) != DISABLE);
/* 配置DMA参数 */
DMA_InitStructure.DMA_Channel = DMA_Channel_0;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)srcBuffer;
DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)destBuffer;
DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToMemory;
DMA_InitStructure.DMA_BufferSize = BUFFER_SIZE;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
DMA_Init(DMA2_Stream0, &DMA_InitStructure);
/* 清除传输完成标志并启动DMA */
DMA_ClearFlag(DMA2_Stream0, DMA_FLAG_TCIF0);
DMA_Cmd(DMA2_Stream0, ENABLE);
}
2.2.2 数据传输验证
为确保DMA传输的正确性,我们需要对源数据和目标数据进行比对:
c复制uint8_t Buffercmp(const uint32_t* pSrc, const uint32_t* pDest, uint16_t length)
{
while(length--) {
if(*pSrc++ != *pDest++) {
return 0; // 数据不匹配
}
}
return 1; // 所有数据匹配
}
void Verify_Data(void)
{
if(Buffercmp(srcBuffer, destBuffer, BUFFER_SIZE)) {
printf("DMA传输验证成功!\r\n");
} else {
printf("DMA传输验证失败!\r\n");
}
}
3. DMA配置中的关键参数解析
3.1 数据宽度与地址递增
在DMA配置中,数据宽度和地址递增是两个非常重要的参数,它们直接影响数据传输的正确性和效率。
3.1.1 数据宽度配置
DMA支持三种数据宽度:
- 字节(8位):
DMA_PeripheralDataSize_Byte/DMA_MemoryDataSize_Byte - 半字(16位):
DMA_PeripheralDataSize_HalfWord/DMA_MemoryDataSize_HalfWord - 字(32位):
DMA_PeripheralDataSize_Word/DMA_MemoryDataSize_Word
重要提示:在直接模式(不使用FIFO)下,外设和存储器的数据宽度必须保持一致,否则会导致传输错误。只有在使用FIFO时,才允许源和目标数据宽度不同。
3.1.2 地址递增控制
地址递增功能通过以下参数控制:
DMA_PeripheralInc:外设地址是否自动递增DMA_MemoryInc:存储器地址是否自动递增
对于存储器到存储器传输,通常需要使能两者的地址递增功能。递增的步长由对应的数据宽度决定:
- 字节宽度:每次递增1字节
- 半字宽度:每次递增2字节
- 字宽度:每次递增4字节
3.2 传输模式选择
DMA支持两种基本的传输模式:
-
普通模式(Normal):
- 传输完指定数量的数据后自动停止
- 需要重新使能才能进行下一次传输
- 适用于单次数据传输任务
-
循环模式(Circular):
- 传输完成后自动重新开始
- 适用于持续数据传输场景
- 注意:存储器到存储器模式不支持循环模式
配置示例:
c复制/* 普通模式配置 */
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
/* 循环模式配置 */
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
3.3 FIFO与突发传输
3.3.1 FIFO配置
DMA控制器为每个数据流提供了4字(16字节)的FIFO缓冲区,可用于:
- 数据宽度转换(如字节到字的打包)
- 缓冲突发传输数据
- 平滑数据流
FIFO相关配置参数:
c复制DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Enable; // 使能FIFO
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull; // 阈值设置
3.3.2 突发传输
突发传输可以显著提高大数据量传输的效率,它允许DMA在短时间内占用总线进行连续传输。STM32 DMA支持以下突发大小:
- 单次传输(无突发)
- 4节拍增量突发
- 8节拍增量突发
- 16节拍增量突发
配置示例:
c复制/* 存储器突发传输配置 */
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_INC4;
/* 外设突发传输配置 */
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_INC4;
注意:突发传输必须与FIFO配合使用,且FIFO阈值必须是突发数据量的整数倍。
4. DMA中断与双缓冲技术
4.1 DMA中断配置与应用
DMA控制器提供了丰富的中断源,可以在特定事件发生时触发中断:
- 传输完成中断(TCIF):当DMA完成全部数据传输时触发
- 半传输中断(HTIF):当DMA完成一半数据传输时触发
- 传输错误中断(TEIF):传输过程中发生错误时触发
- FIFO错误中断(FEIF):FIFO上溢或下溢时触发
- 直接模式错误中断(DMEIF):直接模式错误时触发
中断配置示例:
c复制/* 使能传输完成和半传输中断 */
DMA_ITConfig(DMA2_Stream0, DMA_IT_TC | DMA_IT_HT, ENABLE);
/* 配置NVIC */
NVIC_InitStructure.NVIC_IRQChannel = DMA2_Stream0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
中断服务例程示例:
c复制void DMA2_Stream0_IRQHandler(void)
{
if(DMA_GetITStatus(DMA2_Stream0, DMA_IT_TCIF0)) {
/* 传输完成处理 */
DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_TCIF0);
}
if(DMA_GetITStatus(DMA2_Stream0, DMA_IT_HTIF0)) {
/* 半传输处理 */
DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_HTIF0);
}
}
4.2 双缓冲技术详解
双缓冲是DMA的一个高级功能,它使用两个存储区交替工作,非常适合需要连续处理数据的应用场景。
4.2.1 双缓冲工作原理
- DMA控制器在两个存储区(M0AR和M1AR)之间自动切换
- 当一个存储区正在被DMA访问时,CPU可以处理另一个存储区的数据
- 每次缓冲区切换时会触发传输完成中断
4.2.2 双缓冲配置
配置双缓冲模式需要注意以下几点:
- 必须使能循环模式(自动激活)
- 需要配置两个存储器地址(M0AR和M1AR)
- 不能用于存储器到存储器传输
配置示例:
c复制DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)buffer0;
DMA_InitStructure.DMA_Memory1BaseAddr = (uint32_t)buffer1;
DMA_InitStructure.DMA_BufferSize = BUFFER_SIZE;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory; // 或其它方向
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Enable;
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
DMA_Init(DMA2_Stream0, &DMA_InitStructure);
/* 使能双缓冲模式 */
DMA_DoubleBufferModeConfig(DMA2_Stream0, (uint32_t)buffer1, DMA_Memory_1);
DMA_DoubleBufferModeCmd(DMA2_Stream0, ENABLE);
4.2.3 双缓冲应用实例
双缓冲非常适合音频处理、图像采集等场景。例如,在音频播放应用中:
- DMA正在从buffer0读取数据发送到DAC
- CPU同时向buffer1填充新的音频数据
- 当buffer0发送完成,DMA自动切换到buffer1
- CPU转而处理buffer0,填充下一段音频数据
- 如此循环,实现无缝音频播放
5. 常见问题与调试技巧
5.1 DMA配置常见问题
-
数据传输不启动:
- 检查DMA时钟是否使能
- 确认DMA数据流已使能(EN=1)
- 对于外设模式,检查外设的DMA请求是否使能
-
数据传输不完整:
- 检查NDTR寄存器值是否正确
- 确认缓冲区大小足够
- 检查地址递增设置是否合理
-
数据错位或损坏:
- 检查源和目标数据宽度是否匹配(直接模式下)
- 确认地址递增设置正确
- 检查是否有其他总线主设备在访问同一内存区域
5.2 调试技巧与工具
-
使用断点观察DMA寄存器:
- 在DMA启动前设置断点,检查所有配置寄存器
- 在传输完成后检查NDTR值是否为0
-
利用中断调试:
- 使能传输完成和错误中断
- 在中断服务程序中设置标志变量
-
内存查看工具:
- 使用调试器的内存查看功能比较源和目标数据
- 检查关键变量和缓冲区的值
-
DMA标志检查:
c复制if(DMA_GetFlagStatus(DMA2_Stream0, DMA_FLAG_TCIF0)) { /* 传输完成 */ } if(DMA_GetFlagStatus(DMA2_Stream0, DMA_FLAG_TEIF0)) { /* 传输错误 */ }
5.3 性能优化建议
-
合理选择数据宽度:
- 32位传输效率最高,但需要内存对齐
- 对于非对齐数据,使用字节或半字传输
-
使用突发传输:
- 大数据量传输时启用突发模式
- 配合FIFO使用效果更佳
-
内存布局优化:
- 将DMA缓冲区放在CCM内存(如果可用)或SRAM1中
- 避免DMA缓冲区跨越内存页边界
-
总线仲裁优先级:
- 对实时性要求高的DMA流设置更高优先级
- 通过DMA_SxCR寄存器的PL[1:0]位设置
6. 进阶应用:DMA与存储器到存储器模式的实际案例
6.1 图像处理中的DMA应用
在图像处理应用中,DMA的存储器到存储器模式可以高效地实现图像数据的搬运和预处理。例如:
c复制// 图像灰度化处理
void Image_Grayscale(uint16_t *src, uint16_t *dest, uint32_t size)
{
// 第一步:使用DMA拷贝原始数据
DMA_MemCopy_Config(src, dest, size);
while(DMA_GetFlagStatus(DMA_FLAG_TC) == RESET);
// 第二步:CPU进行灰度化处理
for(uint32_t i=0; i<size; i++) {
uint16_t pixel = dest[i];
uint8_t r = (pixel >> 11) & 0x1F;
uint8_t g = (pixel >> 5) & 0x3F;
uint8_t b = pixel & 0x1F;
uint8_t gray = (r * 77 + g * 150 + b * 29) >> 8;
dest[i] = (gray << 11) | (gray << 5) | gray;
}
}
6.2 双缓冲实现数据流处理
结合双缓冲和存储器到存储器模式,可以实现高效的数据流处理:
c复制#define BUF_SIZE 1024
uint32_t buf1[BUF_SIZE], buf2[BUF_SIZE];
volatile uint8_t current_buf = 0;
void DMA1_Stream0_IRQHandler(void)
{
if(DMA_GetITStatus(DMA1_Stream0, DMA_IT_TCIF0)) {
// 切换缓冲区
current_buf ^= 1;
// 处理非当前缓冲区数据
Process_Data(current_buf ? buf1 : buf2);
DMA_ClearITPendingBit(DMA1_Stream0, DMA_IT_TCIF0);
}
}
void Init_DoubleBuffer_DMA(void)
{
DMA_InitTypeDef DMA_InitStructure;
// ... 初始化DMA ...
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)buf1;
DMA_InitStructure.DMA_Memory1BaseAddr = (uint32_t)buf2;
DMA_InitStructure.DMA_BufferSize = BUF_SIZE;
DMA_DoubleBufferModeConfig(DMA1_Stream0, (uint32_t)buf2, DMA_Memory_1);
DMA_DoubleBufferModeCmd(DMA1_Stream0, ENABLE);
// 使能传输完成中断
DMA_ITConfig(DMA1_Stream0, DMA_IT_TC, ENABLE);
DMA_Cmd(DMA1_Stream0, ENABLE);
}
6.3 与其它外设协同工作
DMA可以与其他外设协同工作,构建高效的数据处理流水线。例如,结合ADC和DMA实现高速数据采集:
- ADC配置为连续转换模式
- DMA配置为循环模式,从ADC数据寄存器读取到内存
- 使用半传输和传输完成中断处理数据
- 在内存中对采集的数据进行处理和分析
这种设计可以实现极高的采样率,同时保持很低的CPU占用率。
