STM32 DMA传输配置详解:数据宽度与传输方向设置指南

一、前言

在STM32嵌入式开发中,DMA(直接内存访问)是提高系统性能的关键技术。正确配置DMA的数据宽度和传输方向对于确保数据传输的准确性和效率至关重要。本文将深入探讨STM32 DMA传输中这两个关键参数的配置方法,并提供实际应用示例。

二、DMA基础概念回顾

2.1 DMA工作原理

DMA允许外设与内存之间直接传输数据,无需CPU干预,从而解放CPU资源。

2.2 STM32 DMA主要特性

  • 支持内存到内存、内存到外设、外设到内存传输

  • 可编程的数据宽度(8位、16位、32位)

  • 循环模式、单次传输模式

  • 中断支持(传输完成、半传输完成、传输错误)

三、数据宽度配置详解

3.1 数据宽度参数定义

在STM32中,DMA支持三种数据宽度:

数据宽度 对应寄存器位 应用场景
8位 DMA_SxCR_PSIZE_0 / DMA_SxCR_MSIZE_0 字节数据传输
16位 DMA_SxCR_PSIZE_1 / DMA_SxCR_MSIZE_1 半字数据传输
32位 DMA_SxCR_PSIZE / DMA_SxCR_MSIZE 字数据传输

3.2 数据宽度配置寄存器

3.2.1 外设数据宽度(PSIZE)
cs 复制代码
// DMA_SxCR寄存器中的PSIZE位定义
#define DMA_SxCR_PSIZE_0    (1U << 11)  // PSIZE[1:0] = 01: 8位
#define DMA_SxCR_PSIZE_1    (1U << 12)  // PSIZE[1:0] = 10: 16位
#define DMA_SxCR_PSIZE      (3U << 11)  // PSIZE[1:0] = 11: 32位(两位都置1)
3.2.2 内存数据宽度(MSIZE)
cs 复制代码
// DMA_SxCR寄存器中的MSIZE位定义
#define DMA_SxCR_MSIZE_0    (1U << 13)  // MSIZE[1:0] = 01: 8位
#define DMA_SxCR_MSIZE_1    (1U << 14)  // MSIZE[1:0] = 10: 16位
#define DMA_SxCR_MSIZE      (3U << 13)  // MSIZE[1:0] = 11: 32位(两位都置1)

3.3 数据宽度配置示例

示例1:配置8位数据宽度(字节传输)
cs 复制代码
// 使用标准外设库
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;

// 直接寄存器操作
DMA1_Stream0->CR &= ~(DMA_SxCR_PSIZE | DMA_SxCR_MSIZE);  // 清除位
DMA1_Stream0->CR |= (DMA_SxCR_PSIZE_0 | DMA_SxCR_MSIZE_0);  // 设置为8位
示例2:配置16位数据宽度(半字传输)
cs 复制代码
// 使用HAL库
hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;

// 直接寄存器操作
DMA1_Stream0->CR &= ~(DMA_SxCR_PSIZE | DMA_SxCR_MSIZE);
DMA1_Stream0->CR |= (DMA_SxCR_PSIZE_1 | DMA_SxCR_MSIZE_1);  // 设置为16位
示例3:配置32位数据宽度(字传输)
cs 复制代码
// 使用标准外设库
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;

// 直接寄存器操作
DMA1_Stream0->CR &= ~(DMA_SxCR_PSIZE | DMA_SxCR_MSIZE);
DMA1_Stream0->CR |= DMA_SxCR_PSIZE;  // PSIZE[1:0]都置1
DMA1_Stream0->CR |= DMA_SxCR_MSIZE;  // MSIZE[1:0]都置1

3.4 数据宽度配置注意事项

规则1:数据宽度对齐要求
cs 复制代码
// 示例:32位数据宽度要求4字节对齐
uint32_t buffer[100] __attribute__((aligned(4)));  // 4字节对齐

// 检查对齐的函数
bool is_aligned(void* ptr, size_t alignment) {
    return ((uintptr_t)ptr & (alignment - 1)) == 0;
}

if (!is_aligned(buffer, 4)) {
    // 处理对齐错误
}
规则2:数据宽度与外设匹配
cs 复制代码
// SPI数据寄存器通常是16位
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;

// ADC数据寄存器(通常是12位,但按16位处理)
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;

// USART数据寄存器(通常是8位或9位)
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;

四、传输方向配置详解

4.1 传输方向参数定义

STM32 DMA支持三种传输方向:

传输方向 描述 应用场景
内存到外设 从内存读取数据写入外设 串口发送、SPI发送
外设到内存 从外设读取数据写入内存 ADC采集、串口接收
内存到内存 在两个内存区域间传输数据 数据拷贝、缓冲区处理

4.2 传输方向配置寄存器

4.2.1 DIR位配置
cs 复制代码
// DMA_SxCR寄存器中的DIR位定义
#define DMA_SxCR_DIR_0      (1U << 6)   // DIR[1:0] = 01: 内存到外设
#define DMA_SxCR_DIR_1      (1U << 7)   // DIR[1:0] = 10: 外设到内存
// 内存到内存传输需要额外配置

4.3 传输方向配置示例

示例1:内存到外设传输(USART发送)
cs 复制代码
// 使用HAL库配置
hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;

// 使用标准外设库
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralDST;  // 外设作为目标

// 直接寄存器操作
DMA1_Stream6->CR &= ~(DMA_SxCR_DIR_1 | DMA_SxCR_DIR_0);  // 清除方向位
DMA1_Stream6->CR |= DMA_SxCR_DIR_0;  // 设置为内存到外设
示例2:外设到内存传输(ADC采集)
cs 复制代码
// 使用HAL库配置
hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;

// 使用标准外设库
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;  // 外设作为源

// 直接寄存器操作
DMA1_Stream0->CR &= ~(DMA_SxCR_DIR_1 | DMA_SxCR_DIR_0);
DMA1_Stream0->CR |= DMA_SxCR_DIR_1;  // 设置为外设到内存
示例3:内存到内存传输
cs 复制代码
// 使用HAL库配置(需要特殊处理)
hdma_memtomem.Init.Direction = DMA_MEMORY_TO_MEMORY;
hdma_memtomem.Init.PeriphInc = DMA_PINC_ENABLE;  // 外设地址递增
hdma_memtomem.Init.MemInc = DMA_MINC_ENABLE;     // 内存地址递增

// 使用标准外设库
DMA_InitStruct.DMA_DIR = DMA_DIR_MemoryToMemory;
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;  // 内存到内存通常使用普通模式

// 直接寄存器操作
DMA1_Stream0->CR &= ~(DMA_SxCR_DIR_1 | DMA_SxCR_DIR_0);
// 内存到内存:DIR[1:0] = 00,但需要其他配置
DMA1_Stream0->CR |= DMA_SxCR_MINC | DMA_SxCR_PINC;  // 使能地址递增

4.4 完整配置示例

完整示例:USART DMA发送配置
cs 复制代码
void USART1_DMA_TX_Config(void)
{
    DMA_HandleTypeDef hdma_usart1_tx;
    
    // 使能DMA时钟
    __HAL_RCC_DMA2_CLK_ENABLE();
    
    // 配置DMA
    hdma_usart1_tx.Instance = DMA2_Stream7;
    hdma_usart1_tx.Init.Channel = DMA_CHANNEL_4;
    hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;  // 传输方向:内存到外设
    hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;      // 外设地址不递增
    hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE;          // 内存地址递增
    hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;  // 外设数据宽度:8位
    hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;     // 内存数据宽度:8位
    hdma_usart1_tx.Init.Mode = DMA_NORMAL;                 // 普通模式(非循环)
    hdma_usart1_tx.Init.Priority = DMA_PRIORITY_HIGH;      // 高优先级
    hdma_usart1_tx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;   // 禁用FIFO
    hdma_usart1_tx.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL;
    hdma_usart1_tx.Init.MemBurst = DMA_MBURST_SINGLE;
    hdma_usart1_tx.Init.PeriphBurst = DMA_PBURST_SINGLE;
    
    // 初始化DMA
    HAL_DMA_Init(&hdma_usart1_tx);
    
    // 关联到USART1
    __HAL_LINKDMA(&huart1, hdmatx, hdma_usart1_tx);
}
完整示例:ADC DMA采集配置
cs 复制代码
void ADC1_DMA_Config(void)
{
    DMA_HandleTypeDef hdma_adc1;
    
    // 使能DMA时钟
    __HAL_RCC_DMA2_CLK_ENABLE();
    
    // 配置DMA
    hdma_adc1.Instance = DMA2_Stream0;
    hdma_adc1.Init.Channel = DMA_CHANNEL_0;
    hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;       // 传输方向:外设到内存
    hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;          // 外设地址不递增
    hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;              // 内存地址递增
    hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;  // 外设数据宽度:16位
    hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;     // 内存数据宽度:16位
    hdma_adc1.Init.Mode = DMA_CIRCULAR;                   // 循环模式
    hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH;          // 高优先级
    hdma_adc1.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
    
    // 初始化DMA
    HAL_DMA_Init(&hdma_adc1);
    
    // 关联到ADC1
    __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);
}

五、高级配置技巧

5.1 数据宽度不匹配处理

当外设和内存数据宽度不匹配时,需要特别注意:

cs 复制代码
// 示例:16位外设数据存储到32位内存数组
uint32_t adc_buffer[100];  // 32位数组存储16位ADC数据

// 配置DMA
hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;  // 外设:16位
hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;         // 内存:32位

// 注意:传输数量需要根据实际数据量调整
// 如果外设是16位,内存是32位,传输100个数据需要设置传输数量为100
hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;

5.2 双向DMA配置

某些应用需要双向DMA传输:

cs 复制代码
void SPI_DMA_Bidirectional_Config(void)
{
    // 发送DMA配置(内存到外设)
    hdma_spi1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
    hdma_spi1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_spi1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    
    // 接收DMA配置(外设到内存)
    hdma_spi1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_spi1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_spi1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    
    // 同时使能发送和接收DMA
    HAL_SPI_Transmit_DMA(&hspi1, tx_buffer, TX_SIZE);
    HAL_SPI_Receive_DMA(&hspi1, rx_buffer, RX_SIZE);
}

5.3 使用FIFO的数据宽度转换

STM32 DMA的FIFO支持数据宽度转换:

cs 复制代码
void DMA_Config_With_FIFO(void)
{
    hdma_usart1.Init.FIFOMode = DMA_FIFOMODE_ENABLE;           // 启用FIFO
    hdma_usart1.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL;  // FIFO阈值
    hdma_usart1.Init.MemBurst = DMA_MBURST_INC4;               // 内存突发传输
    hdma_usart1.Init.PeriphBurst = DMA_PBURST_SINGLE;          // 外设单次传输
    
    // FIFO可以在内部处理数据宽度转换
    // 例如:从32位内存读取,8位外设发送
}

六、调试技巧与常见问题

6.1 调试技巧

cs 复制代码
// 1. 检查寄存器配置
void DMA_Debug_Check(DMA_Stream_TypeDef* DMA_Stream)
{
    printf("CR: 0x%08X\n", DMA_Stream->CR);
    printf("NDTR: %u\n", DMA_Stream->NDTR);
    printf("PAR: 0x%08X\n", DMA_Stream->PAR);
    printf("M0AR: 0x%08X\n", DMA_Stream->M0AR);
    
    // 检查数据宽度配置
    uint32_t psize = (DMA_Stream->CR & DMA_SxCR_PSIZE) >> 11;
    uint32_t msize = (DMA_Stream->CR & DMA_SxCR_MSIZE) >> 13;
    printf("外设数据宽度: %lu, 内存数据宽度: %lu\n", psize, msize);
}

// 2. 使用断点和观察点
__attribute__((always_inline)) 
static inline void DMA_Wait_Complete(DMA_Stream_TypeDef* stream)
{
    while(!(stream->CR & DMA_SxCR_TCIF));
}

6.2 常见问题与解决方案

问题1:数据宽度不匹配导致数据错误

症状 :接收到的数据错位或值不正确。
解决方案

  • 检查外设和内存数据宽度是否匹配

  • 确保地址递增设置正确

  • 验证传输数量是否正确

问题2:传输方向配置错误

症状 :数据传输方向相反或根本没有数据传输。
解决方案

  • 确认DIR位设置正确

  • 检查外设地址和内存地址是否正确

  • 验证外设是否支持DMA传输

问题3:内存对齐问题

症状 :程序崩溃或数据错误。
解决方案

cs 复制代码
// 确保内存对齐
// 对于32位传输
__attribute__((aligned(4))) uint32_t buffer[100];

// 对于16位传输  
__attribute__((aligned(2))) uint16_t buffer[100];

七、性能优化建议

7.1 数据宽度选择优化

cs 复制代码
// 原则:使用与处理器位宽匹配的数据宽度
#if defined(STM32F4) || defined(STM32F7)
// 32位处理器,优先使用32位数据宽度
#define OPTIMAL_DATA_WIDTH  DMA_MDATAALIGN_WORD
#elif defined(STM32F1)
// 某些系列可能更适合16位
#define OPTIMAL_DATA_WIDTH  DMA_MDATAALIGN_HALFWORD
#endif

7.2 传输方向优化

cs 复制代码
// 对于大数据量传输,考虑内存到内存DMA
void optimize_large_data_transfer(void* src, void* dst, size_t size)
{
    if(size > 1024)  // 大数据量使用DMA
    {
        // 配置内存到内存DMA传输
        memcpy_dma(src, dst, size);
    }
    else  // 小数据量使用CPU
    {
        memcpy(dst, src, size);
    }
}

八、总结

正确配置STM32 DMA的数据宽度和传输方向对于确保系统性能和稳定性至关重要:

  1. 数据宽度配置要点

    • 根据外设和内存特性选择合适的数据宽度

    • 确保数据对齐

    • 考虑数据宽度转换需求

  2. 传输方向配置要点

    • 明确数据流向(内存↔外设或内存↔内存)

    • 正确设置源和目标的地址递增

    • 考虑FIFO和突发传输配置

  3. 最佳实践

    • 始终验证配置的寄存器值

    • 使用库函数提高可移植性

    • 添加错误检查和调试支持

通过合理配置这些参数,可以充分发挥STM32 DMA的性能优势,构建高效可靠的嵌入式系统。

相关推荐
清风6666662 小时前
基于单片机的多路热电偶温度监测与报警器
数据库·单片机·mongodb·毕业设计·课程设计·期末大作业
Archie_IT2 小时前
基于STM32F103C8T6标准库的OLED显示屏中文汉字显示实现_资料编号39
stm32·单片机·嵌入式硬件·mcu·硬件工程·信息与通信·信号处理
じ☆冷颜〃2 小时前
二分查找的推广及其在排序与链表结构中的关联
网络·windows·经验分享·笔记·算法·链表
幽络源小助理2 小时前
SpringBoot+Vue智能学习平台系统源码 | 教育类JavaWeb项目免费下载 – 幽络源
vue.js·spring boot·学习
一路往蓝-Anbo2 小时前
STM32单线串口通讯实战(三):协议层设计 —— 帧结构、多机寻址与硬件唤醒
c语言·开发语言·stm32·单片机·嵌入式硬件·物联网
爱吃生蚝的于勒3 小时前
【Linux】零基础深入学习动静态库+深入学习地址
linux·运维·服务器·c语言·数据结构·c++·学习
崎岖Qiu3 小时前
【设计模式笔记26】:深入浅出「观察者模式」
java·笔记·观察者模式·设计模式
Noushiki3 小时前
RabbitMQ 基础 学习笔记1
笔记·学习·rabbitmq
知识分享小能手3 小时前
Ubuntu入门学习教程,从入门到精通, Ubuntu 22.04中的任务计划详解(16)
linux·学习·ubuntu