一、前言
在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的数据宽度和传输方向对于确保系统性能和稳定性至关重要:
-
数据宽度配置要点:
-
根据外设和内存特性选择合适的数据宽度
-
确保数据对齐
-
考虑数据宽度转换需求
-
-
传输方向配置要点:
-
明确数据流向(内存↔外设或内存↔内存)
-
正确设置源和目标的地址递增
-
考虑FIFO和突发传输配置
-
-
最佳实践:
-
始终验证配置的寄存器值
-
使用库函数提高可移植性
-
添加错误检查和调试支持
-
通过合理配置这些参数,可以充分发挥STM32 DMA的性能优势,构建高效可靠的嵌入式系统。