我在复习计算机组成原理I/O这一章的时候,前三种控制方式——程序查询、中断、DMA——本来是顺下来的,但到了DMA这里,很多人第一次卡住。卡住的原因很有意思:前面两种方式都是CPU亲力亲为,DMA突然说CPU可以不干活了,于是“到底谁在搬运数据”“CPU什么时候被闲置”“为什么传送完了又要中断一次”这类问题全冒出来了。尤其这几年408真题里,DMA几乎成了I/O部分的固定考点,选择题会抠细节、大题会结合中断和总线带宽一起考,光背结论根本不够用。
这篇内容我会把DMA从设计动机、控制器结构、完整传送流程、访存冲突仲裁,到考研真题的典型陷阱一次讲透,最后还用STM32和PC内核里的DMA保护做个现实对照。不管是正在备考408的考生,还是准备做嵌入式开发、想搞懂DMA IP核怎么用的工程师,按这个思路过一遍,基本能把DMA这块焊死。
1. 从查询到DMA:I/O控制方式演进中“效率”是怎么一步步逼出来的
很多教材喜欢按“程序查询→中断→DMA→通道”的顺序铺开讲,看起来像是在罗列知识点,其实这四条路线的背后是一条清晰的主线——怎么把CPU从繁重的I/O搬运工作中解放出来。不理解这条主线,后面的寄存器、时序全都是在背天书。
1.1 程序查询方式:CPU成了外设的贴身保姆
程序查询方式的核心逻辑很简单:CPU不断读取外设的状态寄存器,判断外设是否准备好,准备好了就传一个数据,没准备好就继续循环等待。
// 程序查询方式的伪代码示意 while (1) { if (STATUS_REG == READY) { DATA_REG = buffer[i++]; // 传一个数据 } // 否则空转等待 }这种方式最大的问题在于:外设的速度远慢于CPU。磁盘转一圈、串口收一个字节,可能都要几千甚至几万个时钟周期,CPU在这段时间里只能空转。也就是说,CPU把大量本该用来执行程序的时间,浪费在了“等外设”上。这也是为什么教材里画程序查询流程时,永远有一个循环判断的框——那个判断过程的时间开销,基本全是损耗。
1.2 中断方式:节省了等待,但没省掉单字节搬运
中断方式的改进是在“等待”这个环节:外设准备好了就主动拉一个中断请求线,CPU收到后再响应,而不是傻乎乎地轮询。这一点确实把CPU从“陪等”中解放了出来,外设在准备数据的时候,CPU可以跑其他程序。
但问题还在。中断方式一次只能传送一个字节(或一个字)。CPU收到中断后,要执行一整套动作:保存现场、识别中断源、跳转到中断服务程序、完成数据读写、恢复现场、返回断点。这一套下来几百几十个时钟周期就没了,结果只搬了一个字节。如果外设的数据率很高,比如磁盘每秒钟要传几百万字节,CPU的时间几乎全被这一次次的“中断欢迎仪式”吃掉了。
所以中断方式适合“数据量小、不频繁”的场景,比如键盘按键;但遇到磁盘、显示器这类“大批量连续数据”的外设,中断方式就是杯水车薪。DMA的诞生,就是冲着这个痛处来的。
1.3 DMA的设计思路:把“搬运工”从CPU里独立出来
DMA全称Direct Memory Access,直接存储器存取。它的核心思想是:在I/O设备和主存之间,建立一条不经过CPU内部寄存器的直接数据通路,由DMA控制器这个专门的硬件来接管数据搬运工作。CPU只负责在传送开始前设置参数,以及在整块数据传送完成后处理一次收尾中断。
你可以把CPU想象成一个公司老板:程序查询方式是老板亲自盯着生产线,中断方式是有事才叫老板,但一件事就要老板跑一趟。DMA则是老板雇了一个专职物流主管,把整批货从A仓库搬到B仓库这件事,一次性打包交代给主管,主管自己搬完,搬完再向老板汇报一声。老板从头到尾不用搬任何一件货。
这个思路带来的变化是结构性的:DMA控制器不再像中断那样只能被动响应,它要能主动接管总线,发出主存地址、控制读写时序,所以在硬件上它必须是“总线主设备”。这一点后面讲数据通路时还会细说。
2. DMA控制器的内部构造与数据通路:寄存器各司其职
DMA不是凭空变出来的法术,它是由一堆寄存器加控制逻辑组成的硬件模块。只有把这些寄存器各自的职责搞清楚,你才能真正看懂“DMA能搬数据”这件事是怎么发生、怎么结束的。
2.1 核心寄存器的职责与初始化
不同教材、不同芯片厂商对DMA控制器寄存器的命名会有差异,但核心的几个职责是固定的,我把通用叫法和职责对应起来:
| 寄存器 | 常见别名 | 核心职责 |
|---|---|---|
| 主存地址寄存器(MAR) | 内存地址寄存器、SAR | 存放本次要访问的主存起始地址,每传送一个数据自动加1(或按需不变) |
| 外设地址寄存器(DAR) | 设备地址寄存器 | 存放外设接口中数据寄存器的地址,或外设设备的标识 |
| 字计数器(WC) | 传送计数器 | 记录还需传送的数据个数,每传一个减1,减到0表示整块传送完成 |
| 数据缓冲寄存器(DBR) | 数据暂存寄存器 | 暂存从外设读到或即将写入外设的数据 |
| 控制/状态寄存器 | CSR | 存放传送方向、中断允许位、DMA启动位等控制信息,以及DMA忙闲状态 |
这里要特别留意一个点:DAR在很多408教材里被写成“外设地址寄存器”,但在ARM芯片的DMA外设里,它对应的往往是“外设基地址”,比如USART的数据寄存器地址。MAR对应的是“内存缓冲区的首地址”。每次DMA传送后,MAR会自增(内存地址递增),而DAR通常保持不变(因为外设的数据寄存器地址是固定的)。这一增一不变,正是“外设到内存连续搬运”的本质。
预处理阶段做的事情,用一句话说就是:CPU向DMA控制器的这些寄存器“写初值”。比如从磁盘读入内存,CPU先把主存缓冲区首地址写入MAR,把磁盘设备地址和操作命令写入DAR/控制寄存器,把传送字节数写入WC,然后置启动位。之后DMA控制器就按照这些参数自己跑。
2.2 数据通路:谁把数据从磁盘送到了内存
数据通路的理解,建议沿着“外设→主存”这个方向梳理:
- 外设准备好一个数据后,通过DMA请求线向DMA控制器发出请求。
- DMA控制器向CPU发出总线请求(HRQ/BUSREQ),争取总线的使用权。
- CPU在合适的时机让出总线,DMA控制器获得总线控制权后,直接向地址总线发出主存地址(来自MAR),同时向外设接口发出读信号,把数据从外设数据寄存器读到DMA内部的数据缓冲寄存器DBR。
- DMA控制器再向主存发出写信号,把DBR中的数据写入MAR指向的存储单元。
- 写完一个数据,MAR加1,WC减1,如果WC不为0,继续下一轮;如果WC为0,DMA发出中断请求通知CPU传送完毕。
注意,整个过程中数据确实经过了DMA控制器的DBR,但DBR只是一个中转站,并不需要CPU的ALU参与运算,也不需要CPU寄存器中转。所以它比“中断方式里CPU亲手搬运”快得多,因为省掉了保护现场、恢复现场这一整套软件开销。
2.3 为什么DMA控制器必须能当总线主设备
这是很多考生会卡住的点:DMA控制器凭什么能让主存听它的?答案是:它必须具有总线主设备能力。
普通的外设接口是总线从设备,只能被动接受CPU发来的地址和控制信号。DMA控制器在获得总线控制权后,会变成总线上的主设备,主动向地址总线发送地址、向控制总线发出读/写信号。所以DMA控制器内部必须有“地址输出”和“控制信号输出”的能力。这也是它和普通外设接口最本质的区别。
理解了这个,你就能解释为什么DMA控制器需要和CPU做“总线仲裁”:因为总线同一时刻只能有一个主设备,DMA要接管总线,就必须经过请求—应答—让出—使用的完整握手过程。这也是下一章要讲的访存冲突仲裁问题的硬件根源。
3. 一次DMA传输的完整生命周期:从预处理到后处理
DMA的一次完整传送,严格来说分为预处理、数据传送、后处理三个阶段。很多同学只盯着中间的数据传送阶段,忽略了前后的软件开销,做计算题时就会漏项。
3.1 预处理阶段:CPU把“传送任务单”交给DMA
预处理阶段由CPU执行程序完成。CPU通过执行一条I/O指令(或对DMA控制器端口的一组写操作),把下列信息写入DMA控制器:
- 传送方向:是外设→内存(读/输入),还是内存→外设(写/输出),写入控制寄存器。
- 主存首地址:写入MAR。
- 传送的数据个数:写入WC。
- 外设地址及启动命令:写入DAR和控制寄存器。
预处理完成后,DMA控制器进入“待命/传送状态”。这时候CPU可以继续执行自己的程序,比如做计算、跑指令,而外设一旦准备好数据,DMA就会启动传送。预处理是软件和硬件的交接点,它的代价是若干个时钟周期,通常按“块”计,与块大小无关。
3.2 数据传送阶段:总线上的“闯红灯”细节
数据传送阶段全由DMA控制器硬件自动完成,每传送一个数据的典型步骤如下:
- 外设通过DMA请求线发出“数据就绪”信号。
- DMA控制器接到请求后,向CPU发出总线请求。
- CPU在当前总线周期结束后让出总线(具体让出时机取决于仲裁策略,见第4章)。
- DMA控制器接管总线,把MAR中的地址送上地址总线,并发出相应的读写控制信号。
- 数据经DBR中转(或在外设与主存间直通)完成一次读写。
- MAR加1(或保持不变),WC减1。
- 若WC不为0,回到第1步继续;若WC为0,传送阶段结束。
这一阶段最容易被误解的地方是“CPU完全被闲着”。在成组连续传送方式下确实CPU不能访存,但在周期挪用方式下CPU只是暂时让出一个存储周期,绝大部分时间仍在执行程序。所以“DMA传送时CPU一定不能干活”这句话,严格说是错的,要看仲裁策略。
3.3 后处理阶段:传完了为什么还要中断一次
当WC的值为0时,说明整块数据已经传送完成,DMA控制器向CPU发出中断请求。CPU响应该中断后执行中断服务程序,做后处理工作:
- 检查数据传送过程中是否有错误(比如校验失败)。
- 判断是否还有下一块数据需要传送,如果有则重新初始化寄存器,启动下一次DMA。
- 记录或更新状态信息,返回原程序继续执行。
这就是很多人常说“DMA传送完成后会中断CPU一次”的原因。但注意,这个“一次”是针对“一整块数据”而言的,不是每个字节都中断。对比中断方式“每个字节都中断”,DMA的巨大优势就体现在这里。
3.4 和中断方式逐项对比:别再混淆这两条路线
我把两种方式的重要差异用一张表列出来,做选择题时可以直接对着判断:
| 对比项 | 中断方式 | DMA方式 |
|---|---|---|
| 数据传送单位 | 一个字节/字 | 一个数据块 |
| 每次中断次数 | 每传一个数据中断一次 | 每传完一块数据中断一次 |
| 数据传送执行者 | CPU执行中断服务程序搬运 | DMA控制器硬件自动搬运 |
| 传送期间CPU状态 | 中断处理中,被占用于传送 | 大部分时间可执行其他程序(取决于仲裁) |
| 响应过程 | 需要保护/恢复现场 | 数据传送阶段不需要 |
| 适用场景 | 低速、少量数据 | 高速、大批量数据 |
一个很典型的题目是这样问的:“DMA方式和中断方式相比,主要优势是什么?”答案核心就是:用硬件代替软件,把一次一字节的搬运变成一次一整块的搬运,从而大幅降低CPU参与I/O的时间开销。
4. DMA与CPU访存冲突的三种仲裁策略:性能和代价怎么权衡
DMA要访问主存,CPU也要访问主存,但主存只有一个,总线也只有一套。怎么协调双方的使用权?教材里讲了三种基本策略,本质上是“效率”和“复杂度”之间的权衡。
4.1 停止CPU访存(成组连续传送):简单但霸道
这种策略的做法是:DMA一旦获得总线控制权,就在整个数据块传送期间一直占着总线,CPU在此期间不能访问主存。
优点是控制逻辑最简单,DMA传送的连续性和完整性最好,适合磁盘这类需要高速连续传送数据的设备。缺点也很明显:CPU被长时间剥夺访存权。
但注意,这里说的是“不能访存”,不是“完全不能工作”。CPU内部寄存器操作、运算器内部运算等不访问主存的工作理论上仍可进行,但因为指令在执行时要不断取指令、读写操作数,长期不能访存,CPU实际执行会处于停顿状态。所以在成组连续传送下,系统整体吞吐率会明显下降。
4.2 周期挪用(周期窃取):见缝插针的折中方案
周期挪用(也叫周期窃取)是目前应用最广的思路:DMA在CPU不访问主存的时间间隙里,“偷偷”挪用一两个存储周期来完成一次数据传送。
现代CPU执行一条指令通常需要多个存储周期,比如取指周期、取操作数周期。在这些周期之外,CPU可能正在进行内部运算,并不访问主存,DMA就在这种间隙插入自己的访存操作。
所以周期挪用最大的好处就是“偷时间”,对CPU执行程序的影响远小于停止CPU访存。它也有代价:如果外设数据率很高,DMA请求频繁,可能出现“DMA每次还没传完,新的请求就来了”的情况,导致传送被拉长;而且DMA不一定能恰好赶上CPU的访存间隙,可能要多等一会儿,这叫“挪用等待”。
从408考题角度,周期挪用经常作为单选题里“CPU影响最小/折中方案”的答案出现,要能跟另外两种区分开。
4.3 交替分时访问:硬件换并发
交替分时访问(也叫透明DMA)是一种更理想化的方案:把CPU的一个存取周期一分为二,前半段给CPU访存,后半段给DMA访存。这样两级互不干扰,从CPU看来完全不需要等待,也不需要DMA请求总线、仲裁让权,控制反而简单。
但天下没有免费的午餐,它要求主存的工作速度至少提高一倍,而且CPU周期本身要能拆。现代很多高性能系统里,类似的思想会以“多端口存储器”或“总线矩阵分时”的形式存在,但纯透明的交替分时在实际工程中很少单独出现,更多是理论上的一种“极限方案”。
4.4 三种方式一张表理清
| 仲裁策略 | 总线使用权分配 | 对CPU的影响 | 硬件复杂度 | 适合场景 |
|---|---|---|---|---|
| 停止CPU访存 | DMA独占总线直到整块传完 | 大,CPU长时间不能访存 | 简单 | 高速大批量连续存储设备 |
| 周期挪用 | DMA在CPU访存间隙插入访问 | 小,每次仅挪用1~2个周期 | 中等 | 多数实际系统 |
| 交替分时访问 | CPU和DMA按固定节拍分时访问 | 几乎无影响 | 依赖高速主存 | 理想化/高端设计 |
考试时遇到“某系统采用周期挪用方式”,要能画出时间线:CPU访存周期、DMA插入周期交替出现。遇到“某设备要求连续传送”,则优先考虑停止CPU访存方式,因为周期挪用可能因等待间隙而破坏连续性。
5. 考研408里DMA的高频考法与典型陷阱
DMA在408里几乎是I/O控制方式中分值最重的点。选择题考概念和计算,大题常结合中断、地址计算、总线带宽一起出。历年比如2024年的真题,已经把DMA放到更综合的IO+Cache场景里考了;这类题的本质仍然是DMA基本流程与访存冲突分配,但题干信息更多,更需要把原理吃透。
5.1 概念类陷阱:状态位、流程顺序与“谁在搬数据”
概念类陷阱集中在几个经常被想当然的地方:
- “DMA期间CPU被完全占用”——错。周期挪用方式下CPU大部分时间照常执行程序。
- “DMA传送结束后不需要中断”——错。后处理阶段CPU必须响应一次中断做收尾。
- “DMA是软件实现”——错。DMA是硬件控制器,预处理和后处理才需要软件配合。
- “DMA传送的数据要经过CPU寄存器”——错。数据经DMA控制器的DBR,不进CPU通用寄存器。
- “DMA请求=中断请求”——本质不同。DMA请求是“请求总线搬数据”,在传送过程中多次产生;中断请求是“请求CPU处理I/O完成事件”,在整块传送完后产生一次。
考试里经常把这几种说法排列组合成选项,抓住“谁搬数据、搬多大一块、搬完谁收尾、什么时候CPU闲”这四条线,基本能排除掉所有错误项。
5.2 计算类题型:数据率、总线宽度、占用比例
计算题的核心是对准“每次DMA传送”和“每块传送”的开销口径。我做一个教学示例,参数自己设的,不带真题数字,但思路是完全一样的:
假设某外设数据率为200MB/s,总线宽度32位,总线时钟100MHz。每传送一个32位数据需要1个时钟周期。DMA每次成组连续传送128KB数据,完成一块传送后,CPU的预处理+后处理共需5000个时钟周期。求DMA方式下,总线被数据传送和DMA处理器开销占用的时间占比。
先算每秒传送次数:200MB/s ÷ 128KB/块 = 1600块/s(按1MB=1024KB,128KB=131072B,200MB=200×1024×1024B,除下来约1600)。
块级开销:1600 × 5000 = 8000000周期/s(800万周期/s)。
数据级传送开销:每秒要传的32位数个数 = 200MB ÷ 4B = 50M次/s,每个数占1个总线周期,所以是50M周期/s。
总占用周期 = 50M + 8M = 58M周期/s。总线总共100M周期/s,占用率=58%。
也就是说,在这样的速率和块大小下,系统仍然有约42%的总线余量可用。如果把块降为32KB,块数变成4倍,块级开销也会变成4倍(32M周期/s),占用率就会升到82%左右。这就是为什么DMA块大小选得越大,CPU和总线I/O开销占比越低。
5.3 综合题思路:DMA在完整I/O流程中的定位
近年大题喜欢把DMA和Cache、中断优先级甚至流水线串起来考。拿到这类题,我一般建议在草稿纸上先画两条线:一条是CPU执行主线,一条是DMA传送线。然后在时间轴上标出“预处理”“多轮数据传送”“后处理中断”三个区段,再看题目问的是哪一段的代价。
比如题目说“采用DMA从磁盘读入数据到内存,完成后触发一次中断”,它实际上在描述“数据传送阶段由DMA执行,后处理阶段由CPU执行”的完整故事。只要把“CPU时间”和“DMA时间”从流程图上拆开,就不会漏算。
6. 章节之外的现实:从试卷走向STM32与PC内核
学408的时候容易产生一种错觉,觉得DMA是课本里的“理想模型”。实际上你随便打开一个STM32工程,DMA就在那里等你配置;PC上还在用IOMMU做DMA防护。理论模型和工程实现的差异,恰恰能反过来帮你加深对概念的理解。
6.1 单片机里的DMA:外设到内存的直接通道
在STM32里,DMA外设做的事和408教材描述的逻辑几乎完全一致。配置一个USART接收DMA,核心参数是:
DMA_InitTypeDef DMA_InitStructure; // 外设地址:串口数据寄存器,固定不变 DMA_InitStructure.PeriphBaseAddr = (uint32_t)&USART1->DR; // 内存地址:接收缓冲区,可递增 DMA_InitStructure.MemoryBaseAddr = (uint32_t)rx_buffer; // 方向:外设 -> 内存 DMA_InitStructure.Direction = DMA_DIR_PeripheralToMemory; // 要接收的数据个数,等价于408里的WC初值 DMA_InitStructure.BufferSize = 64; // 外设地址不自增,内存地址自增 DMA_InitStructure.PeriphInc = DMA_PInc_Disable; DMA_InitStructure.MemoryInc = DMA_MInc_Enable; // 数据宽度、模式、优先级 DMA_InitStructure.PeriphDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.Mode = DMA_Mode_Circular; // 循环模式 DMA_InitStructure.Priority = DMA_Priority_High; DMA_Init(DMA1_Channel5, &DMA_InitStructure); DMA_Cmd(DMA1_Channel5, ENABLE);对照着看就很清晰:PeriphBaseAddr对应DAR,MemoryBaseAddr对应MAR,BufferSize对应WC,外设地址不自增对应“DAR不变”,内存地址自增对应“MAR自动加1”,传输完成中断对应后处理阶段。你在课本上背了半天“MAR、DAR、WC”,到这里就是结构体的三个字段而已。我当年就是在STM32上跑了一回UART的DMA接收,突然把课本里的抽象图看懂了。
6.2 内核DMA保护:理论模型在系统安全中的延伸
PC上有个常见蓝屏项叫“Driver Verifier DMA Violation”,本质就是DMA在绕过CPU运送数据的过程中,也绕过了常规的内存权限检查。DMA控制器只认物理地址,如果驱动程序配错了缓冲区,DMA就可能写到不该写的内存区域。
所以现代系统里引入了IOMMU(SMMU)做DMA重映射:DMA发出的地址先过一个“翻译+权限检查”的关卡,再把物理地址放到总线上。可以理解为给DMA这位“物流主管”加了门禁系统,货可以照搬,但哪个仓库能搬、哪个不能搬,由门禁统一管控。这正好说明,DMA不是“免费超能力”,它在获得高带宽的同时,也需要额外的保护机制来保证安全边界。
6.3 我对学DMA的一点体会
我个人实际操作中的体会是:备考和工程实践别互相割裂。你是考研党,不一定要会写STM32代码,但哪怕只是看一眼MCU参考手册里的DMA框图,书上的MAR、DAR、WC立刻就会具象化。你是嵌入式开发者,如果当年没认真学过计组,也可以回头翻翻唐朔飞或者王道书里“访存冲突仲裁”的那一页,因为MCU里外设什么时候能发DMA请求、什么时候会被总线仲裁卡住,书里讲得比芯片手册更直白。
DMA这个知识点,考试考的是流程对不对、开销算不算得清、概念分不分得开;工程上考的是缓冲区配置准不准、总线带宽扛不扛得住、内存保护开没开。两边的底层逻辑是同一个:让专用硬件去搬数据,CPU只做决策和验收。把这个核心逻辑想明白了,408的大题和嵌入式的中断调试,都能少走很多弯路。