1、DMA基础概念:什么是DMA、DMA与CPU搬运数据的区别、DMA在游戏机中的作用
1.1 什么是DMA?——说白了就是“数据搬运工”
DMA,全称Direct Memory Access,直接存储器访问。
我习惯把它理解成一个“专职的数据搬运工”。CPU是大脑,负责思考、运算、决策。而DMA呢?它就是个只会干苦力活的家伙——从A点搬数据到B点,搬完就歇着,绝不思考。
你想想看,在游戏机里,数据搬运有多频繁?
- 从卡带/光盘把纹理数据搬到显存
- 从内存把音频流搬到音频芯片
- 从CPU缓存把渲染指令搬到GPU
这些操作如果全让CPU干,那CPU就别干别的了。DMA就是来解放CPU的。
核心要点:DMA是一个硬件模块,它可以在不占用CPU的情况下,在内存和外设之间、或者内存的不同区域之间,自动完成数据搬运。
1.2 DMA与CPU搬运数据的区别——谁更划算?
我刚开始做嵌入式时,总觉得“CPU自己搬数据也没多慢啊,何必多搞个DMA?”直到有一次,我在一个掌机项目里,CPU要同时处理游戏逻辑、音频解码、还有屏幕刷新。结果画面一卡一卡的,音频也断断续续。
后来一分析,CPU有将近40%的时间花在了搬运数据上。加了DMA之后,CPU占用率直接降到15%。嗯,从那以后我再也不敢小看DMA了。
咱们来对比一下:
| 对比项 | CPU搬运 | DMA搬运 |
|---|---|---|
| 执行方式 | 逐字节/逐字搬运,每条指令都要取指、译码、执行 | 硬件自动完成,一次配置,批量搬运 |
| CPU占用 | 100%占用CPU,无法做其他事 | 几乎不占用CPU,仅配置时需CPU介入 |
| 搬运速度 | 受限于CPU指令执行速度 | 通常更快,可达到总线带宽上限 |
| 适用场景 | 小数据量、非频繁搬运 | 大数据量、周期性搬运、实时性要求高 |
| 灵活性 | 高,可随时修改搬运逻辑 | 低,需预先配置好传输参数 |
说白了,CPU搬数据就像让一个大学教授去搬砖——不是不能干,但太浪费了。DMA就是那个专门搬砖的工人,教授该去搞科研。
我的经验:判断是否该用DMA,就看数据量。如果一次搬运超过64字节,或者每秒搬运超过1000次,我建议你认真考虑DMA。否则,CPU自己搬可能更简单。
1.3 DMA在游戏机中的作用——没有DMA,游戏机跑不动
游戏机对实时性的要求有多高?你想想看,60帧每秒的游戏,每帧只有16.6毫秒。这16.6毫秒里,CPU要处理物理碰撞、AI逻辑、输入响应,GPU要渲染一帧画面,音频要持续输出。
如果CPU还要分心去搬运纹理、顶点数据、音频流……那画面卡顿是必然的。
DMA在游戏机里的典型应用场景:
- 纹理加载:从ROM或内存把纹理数据搬到显存。我做过一个PSP模拟器项目,纹理搬运占了CPU 30%的开销,改用DMA后降到5%以下。
- 音频流传输:音频数据需要持续不断地送到音频DAC。用DMA做环形缓冲区,CPU只需要往缓冲区里写数据,DMA自动把数据送到音频芯片。
- 显示刷新:帧缓冲区数据从内存搬运到显示控制器。很多老游戏机(比如GBA)就是靠DMA来实现双缓冲和图层混合的。
- 外设数据交换:手柄输入、存储卡读写、网络数据包收发。这些都可以用DMA来减轻CPU负担。
避坑指南:我曾经在一个项目里,DMA和CPU同时访问同一块内存区域,结果数据被覆盖了,画面出现撕裂。后来加了同步机制——DMA传输完成时触发中断,CPU再处理数据。记住:DMA和CPU共享内存时,一定要做好同步。
1.4 一个简单的DMA配置示例
以STM32为例,配置一次DMA传输其实很简单:
// 配置DMA从内存搬运数据到外设(比如UART)
DMA_InitTypeDef DMA_InitStruct;
// 使能DMA时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 配置DMA通道
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; // 外设地址
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)txBuffer; // 内存地址
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralDST; // 内存到外设
DMA_InitStruct.DMA_BufferSize = 256; // 传输大小
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不变
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址递增
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal; // 单次传输
DMA_InitStruct.DMA_Priority = DMA_Priority_High;
DMA_InitStruct.DMA_M2M = DMA_M2M_Disable; // 非内存到内存
DMA_Init(DMA1_Channel4, &DMA_InitStruct);
// 使能DMA通道
DMA_Cmd(DMA1_Channel4, ENABLE);
你看,配置完这些,CPU就可以去干别的事了。DMA会自动把txBuffer里的256字节数据,一个一个地送到UART发送寄存器里。传输完成后,DMA会触发一个中断,告诉CPU“我干完了”。
小技巧:我习惯把DMA配置封装成一个函数,参数包括源地址、目标地址、数据大小、传输模式。这样在项目里调用起来特别方便,也容易维护。
1.5 小结
DMA不是什么神秘的东西。它就是一块硬件,专门负责数据搬运。在游戏机里,DMA几乎是标配——没有它,CPU会被数据搬运拖死,游戏根本跑不流畅。
记住三点:
- DMA是“专职搬运工”,CPU是“大脑”,各司其职
- 大数据量、高频次的数据搬运,用DMA准没错
- DMA和CPU共享内存时,一定要做好同步,否则数据会乱
下一章,我会讲讲DMA的几种传输模式——单次传输、循环传输、突发传输,以及它们各自适合什么场景。到时候我会分享一个我踩过的坑,关于循环传输的缓冲区大小设置问题……嗯,那是个让人头大的bug。