资讯动态

I/O控制器与DMA控制方式:从原理到工程实践

发布时间:2026/9/12 9:12:34 来源:尧图企业网站定制
课本里讲 I/O 控制器和 DMA 控制方式的时候总是一副“这是计算机组成原理的重点”的架势。真正工作以后才发现不理解这两样东西调驱动的天花板就卡在这里。前阵子有个朋友调 RK3588 的以太网内核反复报 failed to reset the dma另一个朋友在 GD32E230 上做 ADC 四通道 DMA 采集数据一直错位。问题看着八竿子打不着实际上链路都能追回到同一个根子上我们到底有没有想清楚DMA 控制器在替 CPU 办什么事外设那边又是怎么把数据交给 DMA 的。这篇文章把 I/O 控制器和 DMA 控制方式从原理到工程实践重新捋一遍。适合下面这类读者在 MCU 上折腾过 ADC、串口、I2C、PWM 的嵌入式开发者被 DMA 数据错乱折磨过的驱动工程师以及刚学完计算机组成原理、想搞明白“DMA 到底凭什么快”的学生。我会把寄存器结构、总线时序、传输模式这些偏理论的东西和实际工程里的配置顺序、踩坑案例放到一起讲。1. I/O 控制器为什么外设访问非要过一道“代理”1.1 三个寄存器一整套“员工守则”CPU 不能也不应该直接去操作每个外设的引脚和时序。外设种类千差万别时钟节奏也不一样I/O 控制器夹在中间职责就一句话把 CPU 发出的“读/写”意图翻译成外设能理解的时序信号同时把外设的状态反馈给 CPU。一个典型的 I/O 控制器至少包含三类寄存器数据寄存器Data Register暂存 CPU 与外设交换的数据。简单的只有一个字节复杂的会带 FIFO 缓冲。状态寄存器Status Register向外报告“忙/闲/错误/数据就绪”。控制寄存器Control RegisterCPU 向控制器下发工作参数比如使能、波特率、中断屏蔽。以 STM32 的 USART 控制器为例DR 是数据寄存器SR 里放着 TXE/RXNE/TC 这些状态位CR1 用来打开串口、使能接收中断和 DMA 请求。ADC 控制器也类似数据在 DREOC 标志在状态寄存器扫描/连续转换/触发源全写在控制寄存器里。搞清楚这三个寄存器再看新平台的外设驱动几乎都能直接上手。1.2 内存映射 I/O读写寄存器也是内存操作访问寄存器有两种方式。x86 上的独立编址用专门的 IN/OUT 指令ARM 和绝大多数 MCU 走内存映射 I/OMMIO寄存器被映射到一段内存地址C 代码里用一个指针就能访问。MMIO 看起来方便但有一个工程上非常容易踩的坑操作这些地址时编译器和 CPU 可能会做缓存、乱序。MCU 上很多寄存器是用 volatile 修饰的指针在带 Cache 的高性能处理器上还需要加上内存屏障才能保证寄存器访问顺序。DMA 一旦接管数据传输这个“顺序”问题会放大后面说 Cache 一致性时再展开。1.3 从查询到中断再到 DMACPU 越来越“省心”I/O 数据传输方式课本上通常会列四种程序查询、中断驱动、DMA、通道I/O 处理器。这里我更愿意说成三种思路程序查询CPU 不断读状态寄存器等外设准备好吃一次数据。优点是代码简单缺点是等多久完全看外设脸色CPU 大量时间空转。中断驱动外设准备好之后主动通知 CPUCPU 处理完这一字节再回去干活。响应性能提升了但每次传一个字节就要进一次中断保存现场、恢复现场、跳转处理函数这些额外开销并不小。DMA数据传输这件事整体外包给 DMA 控制器CPU 只在传输完成后被通知一次。大批量、高频率、持续的数据流效率最高。控制方式数据交换单位CPU 参与度典型场景程序查询字节/字全程忙等按键扫描、低速调试中断字节/字每次传输进中断串口收发、按键事件DMA数据块只在启动和完成时介入ADC 连续采样、网卡、磁盘通道数据块/整个程序几乎不介入独立处理器大型机外围处理这里的关键变量是“CPU 被占用的粒度”。中断虽然解决了忙等但没解决“每次都要 CPU 亲自搬运一个字节”的问题。DMA 的价值就是把搬运的动作从 CPU 手里拿走让 CPU 专心算数据而不是传数据。2. DMA 工作的底层逻辑数据搬运工如何接管总线2.1 DMA 控制器结构DMA 控制器本质上是一个“能代理 CPU 做内存访问的专用处理器”。它至少需要这几个部件当前地址寄存器源/目的各一个记录数据搬到哪里去。传输计数寄存器还剩多少数据要搬。控制寄存器传输方向、数据宽度、模式、中断使能。状态寄存器忙/空闲、传输是否溢出。外设请求接口接收外设的 DMA 请求信号DREQ。总线请求/应答接口与 CPU 或总线仲裁器打交道。在 MCU 上DMA 一般集成在芯片内部通道数、优先级、FIFO 深浅各有不同。复杂 SoC 上 DMA 可能是独立 IP 核挂到系统总线上比如 AXI DMA、GPDMA 一类。这不影响我们对工作原理的理解只是访问路径和配置接口会有区别。2.2 从 DREQ 到传输完成的完整时序DMA 搬运不是 DMA 控制器自己拍脑袋干的活必须由外设发起。典型流程是这样外设准备好了数据。比如 ADC 转换完成数据已经进 DR 寄存器状态寄存器里 EOC 置位。如果配置了 DMA外设会拉高 DMA 请求信号 DREQ。DMA 控制器收到 DREQ向 CPU 发出总线请求HOLD/BUSREQ。CPU 在执行完当前总线周期后让出总线回一个总线应答HLDA/BUSACK。DMA 控制器接管总线在外设寄存器和内存地址之间做一次读/写。地址寄存器自增传输计数减 1。计数不为 0 则等待下一次 DREQ计数为 0 则拉低请求信号向 CPU 发出传输完成中断。需要强调的是第 6 步里“等待下一次 DREQ”很重要。DMA 不是一口气把一整块数据全部搬完才还总线。对于外部触发的传输它是外设出一个数据单元DMA 就搬一个数据单元。这样设计外设 FIFO 不容易溢出CPU 也不需要被长时间挂起。2.3 周期挪用与突发传输DMA 抢占总线有好几种策略停止 CPUBlock/Burst ModeDMA 一口气占用总线直到整块传输完成中途 CPU 无法访问内存。简单粗暴适合确定性要求高、但允许 CPU 暂停的场景。周期挪用Cycle StealingDMA 只在 CPU 总线周期的空隙里楔一个周期来做传输CPU 几乎感知不到。大多数 MCU 的内置 DMA 都是这种思路。交替访存/透明 DMA把总线时间片分给 CPU 和 DMA两者交替使用适合总线带宽有富余的系统。用生活类比来解释CPU 是厨师DMA 是传菜员。没有 DMA 时厨师做一道菜还得自己去柜台端给客人中断式 I/O 相当于顾客按铃厨师放下炒勺去端一盘菜周期挪用相当于传菜员趁厨师翻锅的间隙端菜突发传输相当于传菜员等攒齐了五六盘菜再一起端出去效率高但需要备菜区FIFO足够宽。热词里那个“dma continuous requests”本质上就是在允许的条件下连续拉高 DMA 请求让 DMA 以接近突发的方式连续搬运常用于有大 FIFO 的外设或高速数据流。3. DMA 的几种传输模式从单次搬运到 Scatter-Gather3.1 Normal 模式与 Circular 模式先看两个基础模式。Normal普通模式DMA 通道收到 N 次请求搬完 N 个数据后停止并且触发一次传输完成事件。后续要再用得重新配置计数和地址。适合一次性的大块数据传输比如从内部 Flash 往 RAM 搬一段代码。Circular循环模式搬完 N 个数据后地址自动回绕到初始位置传输计数自动重装DMA 继续工作。最典型的应用是 ADC 连续采样——ADC 会源源不断地产出数据DMA 把采样结果写进同一个环形缓冲区CPU 可以随时去读最近的一批数据。循环模式往上走一步是双缓冲。硬件提供两个缓冲区当一个缓冲区被 DMA 写满时自动切到另一个同时切入中断给 CPU。CPU 在处理上一个缓冲区数据的时间窗口内DMA 已经在往第二个缓冲区里写两边互不阻塞。音频、波形记录这类对持续性要求高的场合很常见。3.2 三种数据流向配置逻辑完全不同DMA 可以搬三种流向外设到内存源地址固定在外设数据寄存器目的地址在内存地址递增只开目的侧源侧不递增。内存到外设源地址在内存目的地址固定在外设寄存器递增只开源侧。内存到内存两边都是内存地址是否递增可配搬完一块数据再搬下一块。配置时最容易犯错的就是“递增方向没配对”。我在不少代码里看到目的地址递增忘了开结果所有数据都写到同一个位置只留下最后一个值。这类问题查起来表象特别像“数据丢失”或“数据错位”。另外注意内存到内存 DMA 并不是所有芯片都支持配置和外设触发也不一样。像某些 MCU 的 DMA内存到内存模式不能和其他通道的外设触发模式混用因为它的仲裁逻辑不一样。编译环境里即使不报错行为也未必符合预期。3.3 Scatter-Gather为不连续的内存而生离散式 DMAScatter-Gather是 DMA 进阶玩法。它解决的问题很现实上层需要的数据不一定连续。比如网络收包时协议栈给每个报文分配了各自的缓冲区数据散落在多个内存块里。如果没有 Scatter-GatherCPU 只能等 DMA 把整块搬到一个大缓冲区再挨个 memcpy 到目标缓冲区白白浪费时间和内存。Scatter-Gather 的做法是由软件准备一组描述符Descriptor每个描述符里记录三样东西内存起始地址、本段长度、下一个描述符的指针。DMA 硬件按这个链表依次搬送搬完第一段自动顺着指针找到第二段继续搬。全部搬完后再发传输完成中断。CPU 看到的是“一次 DMA 请求数据已经分散/汇聚到位”。现在的硬盘控制器、网卡、USB 控制器普遍用描述符环Descriptor Ring实现 Scatter-Gather。调试驱动时如果遇到这种硬件先画清楚描述符的关系再下手改配置。热词里提到的“离散式 dma scatgather”说的就是它。3.4 直接模式、FIFO 模式与 BURST 参数不少 Cortex-M 的 DMA 支持直接模式和 FIFO 模式两大类工作方式。直接模式外设的请求和 DMA 的内存访问一一对应来一个请求传一个数据。配置简单延迟低但每一次访问都要走完整总线握手总线上小事务很多。FIFO 模式DMA 内部先攒一批数据凑够阈值后再以突发Burst方式一次写到内存/外设。总线事务数量明显减少吞吐率更高但引入了延迟和复杂度。突发长度要跟外设 FIFO 深度、DMA FIFO 阈值、总线位宽三者匹配否则要么外设 FIFO 溢出要么 DMA 忙于等数据。选哪种本质上是在“延迟”和“总线效率”之间做权衡。低速外设用直接模式就好高速、大批量场景走 FIFO突发。4. 工程中的 DMA 落地ADC、串口、I2C、PWM 各有讲究4.1 ADC 多通道 DMA 采集在多通道 ADC 持续采样场景DMA 几乎是标配。ADC 完成一次转换后数据如果不及时拿走会被下一次转换覆盖。在查询/中断模式下同样会有这个问题只是表现有时候是“读到的数据偶尔跳变”。用 STM32CubeMX 配置时套路很固定把 ADC 设置成连续转换 扫描模式规则序列里排好要采的通道DMA 选择 Circular 模式数据宽度和 ADC 数据寄存器对齐通常是半字Memory 地址设成一块本地 uint16_t 数组。关键点是缓冲区大小和通道数的关系。规则组一次扫描产生一组数据DMA 的传输次数通常设成“组数 × 通道数”。例如四个通道连续采样你希望每完成 100 组触发一次回调传输次数就设 400。回调里按通道索引去拆数据第一、第五、第九个元素都是通道 0以此类推。热词里“adc四通道使用dma”的坑很多就出在这个索引换算上。4.2 串口 DMA发送简单接收要有设计串口 DMA 分收发两边。发送相对简单把要发的一包数据通过 DMA 从内存搬到 USART TX 数据寄存器发完触发完成中断。但要注意DMA 发送是“异步”的你在调用发送函数之后立刻修改缓冲区可能正好发到一半。通常的做法是在完成回调里把“忙”标志清掉需要连续发送时先等忙标志再填充缓冲区。接收这边更讲究。UART 的数据是“不可预测、不定长”到达的有的做法是 DMA 配成接收空闲中断IDLE模式DMA 始终在 Circular 模式下等待串口数据每收到一个字节搬一次接收空闲后进入串口空闲中断从 DMA 当前剩余计数算出这次一共收到多少数据。这样既不需要提前知道帧长CPU 也不用频繁进接收中断。要注意一个细节DMA 的当前计数寄存器比如 NDTR会随传输递减计算已接收长度时要拿“配置时的总长度”减去“当前剩余”而不是读一个绝对位置。我第一次写这个逻辑就写反了调试时收到的内容一直莫名多一截或少一截。4.3 I2C 和 DMA未必总是最佳选择I2C 用不用 DMA争议比较大。I2C 一帧通常只有几十上百字节如果都用 DMA光配置和中断处理的开销就占了整个传输时间的一大半。对短数据中断或轮询反而更稳、更简单。但当传输长度上来了比如从 EEPROM 连续读 2KB、从传感器连续刷一长串配置DMA 的价值就出来了CPU 可以在 I2C 搬运期间去做别的事情。此时需要小心的 I2C 特有逻辑ACK/NACK 由硬件自动处理DMA 模式下如果中途出错软件捕获错误的路径会比中断模式绕一般得先关掉 DMA 通道再走错误处理流程。我个人的经验是短数据关闭 DMA长数据开启 DMA同时把超时保护做周全。4.4 PWM DMA用定时器更新事件刷新占空比PWM 加 DMA 是一个比较巧妙的用法常见于呼吸灯、多段曲线控制、步进电机速度规划。它的思路是把一组占空比数据放在内存数组里定时器的更新事件UEV作为 DMA 请求源每个更新事件触发 DMA 从数组搬一个值到比较寄存器 CCx。循环模式下这组占空比就会一次次被执行形成完整的波形。CPU 只需要在最后把整个数组准备好即可。有些协议时序也可以这样发比如 WS2812 这类单线协议把各个颜色位的“0/1 电平时间”场景化成 PWM 周期和占空比用 DMA 持续喂比较寄存器。虽然各家 HAL 库对 PWM DMA 的封装略有出入但核心都是“事件触发 内存到外设 循环”。4.5 一个通用的配置顺序建议综合以上场景我总结了一个通用顺序按这个顺序做踩坑概率会小很多开外设时钟和 DMA 控制器时钟确认时钟源频率。先把外设寄存器复位到默认状态特别是状态寄存器里的残留标志。配置 DMA 通道方向、地址、宽度、递增、模式然后是突发参数。使能 DMA 通道很多芯片这一步只是“准备就绪”不会立刻开始。使能外设的 DMA 触发让外设开始工作。外设产生第一个事件后再去检查 DMA 状态。顺序反了最常见的结果外设先跑起来第一个有效数据已经产生但 DMA 当时还没接管于是第一个样本丢失随后所有样本整体前移一位看起来就是“数据错位”。5. 那些年 DMA 翻车的现场排查链路比答案重要5.1 现象一ADC 四通道数据张冠李戴GD32E230 上做 ADC 四通道 DMA 采集出来的数据乱了套通道顺序对不上、数据奇偶交错。很多人第一反应是“DMA 配置错了”实际上要按这个顺序查先确认 ADC 规则序列的通道排列顺序再看 DMA 缓冲区里每组的排列。两个顺序必须一致。确认 DMA 数据宽度。ADC 数据寄存器多半是半字16 位如果配成字节高字节和低字节会被拆到两个 Buffer 位置数据自然交错。确认内存变量对齐。某些 DMA 要求缓冲区按 4 字节对齐否则高性能模式会报总线错误或搬运时地址错乱。确认 DMA 的地址递增是否只开在内存侧。外设数据寄存器地址必须固定。检查初始化顺序。前面说的先 DMA 后外设否则第一组数据整体错位。热词里“gd32e230 adc dma数据紊乱”多半根源就在这里。这类问题我还真踩过一次最后发现只是字节宽度配错了数据高 8 位和低 8 位被拆开看着像随机数。5.2 现象二外设报 failed to reset the dma问题却在外设本身“failed to reset the dma”这类报错在 RK3588 的以太网等场景出现过。内核启动时驱动尝试复位 MAC 里的 DMA 引擎一直失败。看着是 DMA 的问题但 DMA 接收不到复位命令本质上是外设根本没有正常工作。我的排查思路是这样的先读外设的版本/标识寄存器。如果读到全 0 或全 F说明总线应答不正常外设要么没上电要么时钟没开要么地址整体不对。查时钟树。驱动在 probe 里有没有把对应外设的时钟 gate 打开没有时钟寄存器访问全部无响应。查复位控制器。有些 IP 需要先释放 reset line或者在时钟稳定后才释放顺序错会导致外设起不来。查电源域和 IO 域。复杂的 SoC 上外设所在电源域没上电、引脚被复用错误都可能让外设读不到。如果前面都正常再回去查 DMA 配置看中断路由和总线矩阵是否正常。这类问题很容易让人陷入“反复调 DMA 寄存器”的死循环。实际上只要外设寄存器能正常读DMA 复位大概率不会失败。先确认外设活着再谈 DMA 配置。5.3 现象三DMA 中断不停触发或者触发一次后再也不来中断异常也是高频事故。中断“不停触发”先查中断标志是否在服务函数里及时清除有些 DMA 是硬件自动清标志有些微控制器需要软件写特定位。再查是不是传输完成回调里又重新启动了 DMA导致每次完成都重新发起一轮。中断“只触发一次”多数是 Normal 模式下没有重新设置传输计数。Circular 模式则要注意是不是某个总线错误让 DMA 进入了异常状态常见于缓冲区未对齐或访问了非法地址。另外在带 Cache 的 MCU 上DMA 写完内存后CPU 读到的可能是缓存里的旧数据。解决办法是读之前对目标内存做 cache invalidateDMA 从内存读数据之前如果 CPU 刚改过数据要先 cache clean。这个点在高频刷新场景很容易漏漏一次就偶尔出现“数据不变”的灵异现象。5.4 现象四编译器优化把现场搞得更乱还有一个容易忽略的因素是编译器优化。DMA 修改内存的行为在 CPU 看来是“外部写入”如果编译器不知道这块内存会被硬件改可能把对它的访问优化掉比如寄存器变量优化、读缓存优化。解决方式是用 volatile 修饰由 DMA 填充的缓冲区或者至少用原子读/写。在读取 DMA 数据前插入内存屏障确保数据访问顺序。对于描述符修改后要做一次 cache clean 和屏障再启动 DMA。嵌入式里遇到“有时正常、有时抽风”的 DMA 问题我第一个怀疑的往往是这三个Cache 没同步、变量没对齐、时序顺序不对。6. 我个人的 DMA 选型与调试习惯6.1 什么时候才值得用 DMA我用一个简单标准来判断如果这个数据传输频率超过几百次每秒、单次数据量超过几十字节DMA 值得考虑否则就用中断或者轮询。短数据用 DMA每次配置的寄存器操作比中断本身还多收益是负的。具体场景上连续 ADC、连续串口流、图像/音频采集无脑上 DMA。一次几十字节的 EEPROM 读、几十微秒的短脉冲中断优先。内存到内存的大块拷贝用 DMA 省 CPU但要确认芯片支持且对齐。6.2 调试 DMA 时我最常用的一招先不要接外设触发把 DMA 目标地址设成一个临时变量源地址设成外设寄存器用软件触发一次 DMA看目标地址里是否出现预期的值。这一步能同时验证地址、宽度、递增方向三个要素。如果这步都通不过问题大概率在 DMA 配置先别去查外设时序。验证通过后再接外设触发加一个单步中断或逻辑分析仪观察外部响应是否正确。这样分层定位比一上来就拉大管网查要快得多。6.3 最后分享两个小习惯一个是在设计缓冲区时多留一个“哨兵位”。比如 ADC 缓冲长度设成通道数的整数倍再加 2万一 DMA 多搬了一个数据不会立刻踩到别的变量查起来也更容易看出边界。另一个是给 DMA 相关结构体加上版本注释。我在团队里推行过一个规矩任何 DMA 配置改动必须同步更新注释里的“改动原因 验证平台”。理由很简单DMA 配置看着都像“随便填的寄存器”三个月后回来看代码没有注释根本不知道当时为什么这么设。这两条习惯帮我省了很多重复排查的时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价