1. 问题现象与背景分析
最近在调试STM32的SPI DMA传输时遇到了一个奇怪的现象:使用DMA_PeripheralDataSize_HalfWord配置从内存向SPI外设传输16位数据时,LCD屏幕显示的颜色总是异常。经过逻辑分析仪抓包发现,实际只有数据的低8位被传输,高8位完全丢失。这个问题在嵌入式开发中相当典型,特别是当我们需要通过SPI接口驱动彩色TFT-LCD时。
SPI接口的DMA传输在嵌入式图形显示中非常常见,它能有效减轻CPU负担。正常情况下,配置为16位传输时,DMA控制器应该一次性搬运两个字节。但实际表现却像是只配置了8位传输,这直接导致显示颜色失真——因为16位RGB565颜色值的高字节丢失后,显示效果完全偏离预期。
2. DMA传输机制深度解析
2.1 DMA数据宽度配置原理
在STM32的DMA控制器中,PeripheralDataSize和MemoryDataSize这两个参数的匹配至关重要。当我们将PeripheralDataSize设置为HalfWord(16位)时,意味着:
- 每次外设访问将操作16位数据
- 地址增量按2字节计算
- DMA控制器会一次性从内存读取16位数据
但问题在于,这个配置必须与外设的实际数据宽度严格匹配。如果SPI外设本身的数据寄存器只有8位宽度,那么即使DMA配置为16位传输,外设也只能接收8位数据。
2.2 SPI外设的数据宽度特性
STM32的SPI_DR寄存器有一个关键特性容易被忽略:
- 在8位数据模式下:DR寄存器是8位宽的
- 在16位数据模式下:DR寄存器才真正扩展为16位
这意味着,如果我们没有正确配置SPI本身的数据宽度,即使DMA配置为16位传输,SPI外设也只会接收低8位数据。这就是导致颜色显示异常的根源。
3. 完整解决方案与配置步骤
3.1 关键配置修改
要使16位DMA传输正常工作,必须同时修改两处配置:
- SPI数据宽度配置:
c复制SPI_InitStructure.SPI_DataSize = SPI_DataSize_16b;
- DMA传输配置:
c复制DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
3.2 完整初始化代码示例
以下是针对STM32F4系列的完整配置示例:
c复制// SPI配置
SPI_InitTypeDef SPI_InitStructure;
SPI_InitStructure.SPI_Direction = SPI_Direction_1Line_Tx;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
SPI_InitStructure.SPI_DataSize = SPI_DataSize_16b; // 关键配置
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_Init(SPI1, &SPI_InitStructure);
// DMA配置
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_Channel = DMA_Channel_3;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR;
DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)buffer;
DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;
DMA_InitStructure.DMA_BufferSize = length;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 16位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; // 16位
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
DMA_Init(DMA2_Stream3, &DMA_InitStructure);
4. 常见问题排查与调试技巧
4.1 典型问题排查流程
当遇到DMA传输数据不完整时,建议按照以下步骤排查:
- 检查逻辑分析仪或示波器信号,确认实际传输的数据位数
- 核对DMA配置中的PeripheralDataSize和MemoryDataSize是否匹配
- 确认外设本身支持的数据宽度(查阅参考手册)
- 检查外设时钟是否使能(特别是SPI的时钟)
- 验证内存缓冲区地址是否对齐(16位传输最好2字节对齐)
4.2 调试技巧与注意事项
- 地址对齐问题:
对于16位传输,内存地址最好2字节对齐。可以使用__align(2)指定缓冲区对齐方式:
c复制__align(2) uint16_t buffer[1024];
- 字节序问题:
STM32采用小端模式,而有些LCD控制器可能期望大端数据。如果颜色仍然异常,可以尝试交换字节:
c复制uint16_t swapped = (original << 8) | (original >> 8);
-
SPI时钟相位配置:
不正确的CPOL/CPHA配置可能导致数据采样错误,虽然不会导致数据截断,但会影响数据有效性。 -
DMA中断使用:
建议启用DMA传输完成中断,用于检测传输状态:
c复制DMA_ITConfig(DMA2_Stream3, DMA_IT_TC, ENABLE);
5. 深入理解数据传输机制
5.1 DMA与SPI的协同工作原理
当DMA和SPI都配置为16位模式时,数据传输的实际过程如下:
- DMA控制器从内存读取16位数据
- 将完整16位数据写入SPI_DR寄存器
- SPI外设自动开始时钟输出,依次移出16位数据
- 传输完成后,DMA控制器自动递增内存地址(+2字节)
- 重复上述过程直到完成指定长度的传输
5.2 性能优化建议
-
使用双缓冲技术:可以准备两个缓冲区,当一个缓冲区正在传输时,CPU可以填充另一个缓冲区,提高效率。
-
合理设置DMA优先级:如果系统中有多个DMA流,为显示数据传输设置较高优先级,避免出现显示撕裂。
-
考虑使用内存到内存的DMA传输:对于需要频繁更新的显示数据,可以先用DMA在内存中准备好完整帧缓冲区,再一次性传输到SPI。
6. 扩展应用与思考
6.1 其他外设的类似问题
这种数据宽度不匹配的问题不仅限于SPI接口,在以下外设中也需要注意:
- USART:当使用DMA传输9位数据时
- I2S:音频数据通常需要特定对齐
- ADC:多通道采样时的数据排列
6.2 32位传输的配置要点
如果需要配置32位传输,需要确保:
- 内存地址4字节对齐
- 外设支持32位访问
- 缓冲区长度是4的倍数
c复制__align(4) uint32_t buffer[1024];
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
在实际项目中遇到DMA传输数据异常时,首先要建立系统的调试思路:从硬件信号观测入手,逐步检查软件配置,特别注意外设与DMA控制器之间的参数匹配。这种问题往往不是DMA本身的问题,而是整个数据传输链路中某个环节的配置不协调导致的。
