嵌入式DMA驱动实战:串口、SPI、ADC与缓存一致性排查指南

发布时间:2026/10/28 9:13:43

嵌入式DMA驱动实战:串口、SPI、ADC与缓存一致性排查指南 做嵌入式驱动这几年我发现自己跟 DMA 打交道的次数比跟任何外设寄存器打交道的次数都多。串口收发、SPI 刷屏、ADC 连续采样、定时器触发批量搬数背后全是这个“硬件搬运工”在干活。如果你还在用中断一个字节一个字节地收数据那这篇第 6 期经验整理大概率能帮你把 CPU 占用率降一个数量级。内容适合正在调串口、SPI、ADC 驱动的新手也适合被缓存一致性、中断标志、通道冲突这些坑折磨过的老手。我会把 DMA 驱动的设计思路、配置流程、代码模板以及我踩过的坑一次讲清楚。1. 先看整体DMA 驱动开发的设计思路与选型判断1.1 DMA 的本质是一个“不占 CPU 的搬运工”讲 DMA 之前先想一个问题CPU 是干什么的除了做运算它还得当“快递员”——把外设寄存器的数据搬到内存再把内存数据搬回外设。这个搬运动作本身不产生任何计算价值纯粹是开销。中断方式下每个字节来一次都要打断 CPU现场保存、压栈出栈、判断标志、拷贝数据代价极高。DMA 干的事就是把这个“快递员”的角色交给一个硬件专用通道它自己完成源地址、目的地址、搬运长度的配置搬完触发中断告诉 CPU 一声就行。这意味着CPU 在搬运期间可以继续干正事。为什么要强调这个因为很多新手在驱动开发时容易陷入一个误区能跑就行不关心中断频率。等到系统任务多了CPU 在 115200 波特率的串口上被中断打爆才发现问题严重。以一个波特率 115200、8N1 格式的串口为例每秒约有 11520 字节如果用中断方式每字节进一次中断每秒要被中断打断一万多次再算上每一次中断的进出场开销CPU 的有效利用率会明显下降。而用 DMA 搬运把一整包数据一次性搬进内存CPU 全程不参与搬运只处理整包数据到达的事件效果天差地别。这其实也回答了“什么时候该用 DMA”的问题只要你有“大量数据需要在外设和内存之间搬移”的场景都应该优先考虑 DMA。典型的包括串口不定长帧接收和高速发送SPI 与外部 Flash、显示屏、SD 卡之间的批量读写ADC 多通道连续采样定时器触发 DMA 批量更新多个 PWM 通道的占空比。反之如果只是偶尔搬几个字节、实时性要求也不高强行上 DMA 反而会增加配置复杂度和调试成本。判断标准很简单单位时间内数据量越大中断频率越高DMA 越值得上。1.2 驱动开发的整体决策流程我做 DMA 驱动时一般不会一上来就抄代码而是按下面的顺序过一遍先看数据手册里的 DMA 请求映射表确认用的外设事件对应哪条 DMA 通道/流避免两条外设抢同一通道。确定搬运方向外设到内存、内存到外设、还是内存到内存。确定模式单次传输还是循环模式。循环模式适合 ADC 连续采样这类永远不停的外设单次模式适合一次性搬完一包数据就停的场景。确定数据宽度字节、半字16 位、还是字32 位与外设寄存器宽度、FIFO 配置必须匹配。选择中断传输完成中断、半传输中断、错误中断分别怎么用以及中断优先级要不要高于某些任务。考虑缓冲区管理和缓存一致性尤其在 Cortex-M7 或 Cortex-A 这类带 Cache 的平台上。测试验证先小包测试再压到满负荷用示波器或逻辑分析仪量时间戳确认实际吞吐和中断负载。这套流程看着繁琐但它能挡住绝大多数运行期才暴露的鬼问题。下面几节我会结合串口、SPI、ADC、定时器这几个最常见场景把每一步拆开讲。2. 串口 DMA 接收从“莫名丢包”到“稳定收包”的关键细节2.1 不定长数据空闲中断才是串口 DMA 的灵魂串口 DMA 接收严格说不难难在“怎么知道一帧数据结束了”。串口协议千奇百怪有固定长度的有带帧头帧尾的也有纯不定长的。对于不定长数据工程上最实用的方案就是“DMA 循环接收 空闲中断”。数据到达时DMA 负责把字节从串口的接收寄存器搬进缓冲区CPU 完全不被打扰当总线上出现一个字节间隔即空闲时串口会产生空闲中断此时 CPU 才去读 DMA 剩余计数器算出真正接收到的字节数然后处理这一帧。这个方案我在 STM32F1、F4、H7 以及 ESP32 上都验证过核心逻辑一致。以 STM32 HAL 库为例代码骨架是这样#define RX_BUF_SIZE 512 static uint8_t uart_rx_buf[RX_BUF_SIZE]; void uart_dma_rx_start(UART_HandleTypeDef *huart) { // 开启 DMA 循环接收CPU 不参与搬运 HAL_UART_Receive_DMA(huart, uart_rx_buf, RX_BUF_SIZE); // 开启空闲中断 __HAL_UART_ENABLE_IT(huart, UART_IT_IDLE); } // 串口中断里判断空闲标志 void uart_irq_handler(UART_HandleTypeDef *huart) { if (__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE)) { // 先清空闲标志注意 HAL 库不同版本清法不同 __HAL_UART_CLEAR_IDLEFLAG(huart); // DMA 剩余计数器的值就是缓冲区里尚未被搬走的字节数 uint16_t remained __HAL_DMA_GET_COUNTER(huart-hdmarx); uint16_t received RX_BUF_SIZE - remained; // 此时 uart_rx_buf[0..received-1] 就是一帧完整数据 handle_frame(uart_rx_buf, received); } HAL_UART_IRQHandler(huart); }注意几个容易翻车的细节。第一空闲中断必须在 DMA 接收开启之后再去使能。如果反过来数据还没开始接收空闲中断却被置位你会在中断里读到一个长度为 0 的“假帧”。第二清空闲标志的方式跟芯片系列相关。有些系列直接清 IDLE 标志位就行但有些系列需要先读状态寄存器再读数据寄存器才能清干净建议查对应参考手册别把一个系列的工程代码复制到另一个系列就不管了。第三计数器是 DMA 的“剩余数”不是“已接收数”。很多人第一次看会搞反导致每帧长度都算错。用缓冲区总长度减去剩余计数才是本次实际收到的字节数。第四如果一帧数据正好填满整个缓冲区空闲中断还没来得及产生DMA 计数器已经归零这一帧就会因为“没有空闲”而不被处理。解决办法是把缓冲区设得比实际最大帧长更大或者配合半传输中断做乒乓切换。我的习惯是缓冲区长度设为最大帧长的 2 倍以上必要时再加一个“DMA 传输完成中断”做兜底。2.2 乒乓缓冲让数据处理和 DMA 搬运并行起来这里顺便聊聊乒乓缓冲。如果你认真做串口 DMA一定会遇到这个问题DMA 在往里写缓冲区而你在中断里读同一个缓冲区两个动作在不该碰头的地方碰了头数据就花了。乒乓缓冲的思路是准备两块缓冲区A 和 B。DMA 搬数据时CPU 处理另一块等 DMA 把当前这块搬满了或半满中断触发就切换CPU 处理刚刚被搬满的那块DMA 继续往另一块写。两块缓冲区轮流被读写就像两个房间轮流住人永远不会互相踩脚。具体到串口场景最简单的实现就是用 DMA 的半传输中断和传输完成中断分别标记前半段/后半段已经填满。假设缓冲区大小 1024 字节DMA 写满前 512 字节触发半传输中断CPU 处理 Buf[0..511]DMA 写满后 512 字节触发传输完成中断CPU 处理 Buf[512..1023]。DMA 是循环模式所以这个流程会不断重复。这样即使一帧的数据长度小于 512 字节也能被及时处理不会因为等待“空闲中断”而延迟一整个帧周期。这里给一个简化版的乒乓处理骨架#define BUF_SIZE 1024 #define HALF_SIZE 512 static uint8_t rx_buf[BUF_SIZE]; void uart_dma_rx_start(void) { HAL_UART_Receive_DMA(huart1, rx_buf, BUF_SIZE); // 使能半传输中断和传输完成中断 __HAL_DMA_ENABLE_IT(huart1.hdmarx, DMA_IT_HT); __HAL_DMA_ENABLE_IT(huart1.hdmarx, DMA_IT_TC); } void dma_rx_irq_handler(void) { if (__HAL_DMA_GET_FLAG(huart1.hdmarx, DMA_FLAG_HT)) { __HAL_DMA_CLEAR_FLAG(huart1.hdmarx, DMA_FLAG_HT); process_data(rx_buf, HALF_SIZE); // 前半段已就绪 } if (__HAL_DMA_GET_FLAG(huart1.hdmarx, DMA_FLAG_TC)) { __HAL_DMA_CLEAR_FLAG(huart1.hdmarx, DMA_FLAG_TC); process_data(rx_buf HALF_SIZE, HALF_SIZE); // 后半段已就绪 } }但提醒一句乒乓缓冲的“帧边界”不一定跟缓冲区边界对齐。如果你在跑 Modbus、自组协议这类有明确帧结构的应用不能直接拿半满标志当帧边界得配合帧头帧尾或超时判断。乒乓解决的是“数据并发覆盖”问题不是“帧协议解析”问题。我在实际项目里常用方案是数据到达时先按乒乓策略写入一个大环形队列协议解析单独跑一个任务从环形队列里消费两个问题解耦非常好用。2.3 ESP32 串口 DMA 的差异点热词里提到 ESP32 的串口 DMA 接收这里单独说两句。ESP32 的 UART 外设本身带有硬件 FIFO官方推荐的 DMA 接收写法跟 STM32 的循环 DMA 不完全一样。ESP-IDF 的uart_driver_install里可以配置UART_RX_DMA模式驱动内部会维护环形缓冲区应用层直接调用uart_read_bytes读走数据。它更类似“驱动帮你做好了缓冲管理和事件通知”你不需要像 STM32 那样手工清 DMA 标志和计算计数器。用 ESP32 时有一个跟发送端相关的坑接收端启用 DMA 后发送端并不需要改变发送时序正常按照原协议发送即可DMA 驱动会按字节流把数据搬进环形缓冲。但如果发送端连续发送的数据包很大而你这边 DMA 缓冲太小数据就可能被截断或丢弃。解决办法是合理设置uart_config里的缓冲区大小并且在事件循环中及时读取数据让环形缓冲腾出空间。不少人拿 STM32 的习惯去写 ESP32结果被“DMA 接收不到数据”卡住很久其实不是 DMA 坏了是 IDF 的事件处理和缓冲策略没配对。3. SPI DMA 与 ADC DMA高速采样的两个“刚需”场景3.1 SPI DMA刷屏、读写 Flash、驱动 ADC 都靠它SPI 是嵌入式里最常用的高频外设接口它的特点就是“快”。很多 MCU 的 SPI 时钟能跑到 18MHz、36MHz、甚至上百 MHz。这时候如果用“发送一个字节等发送完成中断再发下一个字节”的轮询或中断方式你会发现大部分时间都浪费在等待上外设吞吐根本跑不满。DMA 一出场直接把一组待发送的数据交给 DMA 控制器SPI 移位寄存器每准备好一个数据DMA 就自动填充下一个发送完成后再给一个完成中断CPU 全程零参与。以用 SPI DMA 驱动一个 LCD 屏为例刷一屏 240x320、16 位色深的图片数据量是 2403202约 150KB。如果普通方式在主循环里一个字节一个字节往 SPI 数据寄存器写几帧下来主循环基本没法干别的。用 DMACPU 只需要把一包颜色的起始地址和数据长度告诉 DMA然后就可以去跑其他任务搬完了收到完成事件再收尾。刷屏帧率能提升 30%~50%关键是 CPU 占用率大幅下降系统整体响应更快。写 SPI DMA 驱动时几个要点通信前先确认 SPI 的 DMA 请求映射到哪条通道/流。STM32F103 这类 Cortex-M3 的 DMA1/DMA2 和外设的映射关系在参考手册里有一张大表比如 SPI1_RX 固定走 DMA1_Channel2、SPI1_TX 固定走 DMA1_Channel3。两个外设不能同时占用同一条通道所以设计外设分配时要提前查表。发送方向要设置成内存到外设源地址是内存缓冲区、目的地址是 SPI 的数据寄存器地址而且目的地址要固定不自动增加源地址按数据宽度递增。很多人配置反了导致发出去的全是同一个字节。如果 SPI 外设和 DMA 都支持 FIFO可以合理配置 FIFO 阈值减少总线占用。不同芯片差异很大但大方向是 FIFO 能缓冲一定深度容忍总线仲裁延迟避免 SPI 因等待数据出现欠载。大块数据传输时还要注意 DMA 传输长度限制。有些 DMA 的计数寄存器只有 16 位最大单次传输 65535 个数据单元超过需要拆包或者用循环模式。下面是一个用 HAL 库发送数据的典型代码// 发送前先把数据准备好 uint8_t spi_tx_buf[1024]; // 调用 DMA 发送CPU 立即返回 HAL_SPI_Transmit_DMA(hspi1, spi_tx_buf, len); // 在发送完成回调里做后续动作 void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { // 发送完成可以去处理下一包或拉高片选 } }有人会问SPI DMA 发送时片选信号怎么处理正常做法是发送前拉低片选发送完成回调里拉高片选而且要确保片选拉高的动作发生在最后一个字节真正移出 SPI 之后。用HAL_SPI_Transmit_DMA时完成回调在最后一个数据被送入 SPI 移位寄存器时会触发但移位寄存器可能还在往外送所以有的场景下需要加一个极小延时或查询 SPI 忙标志再拉高片选否则最后一个字节容易丢。这个坑我踩过不止一次。3.2 ADC 连续采样为什么必须 DMAADC 连续采样可以理解成一个“永不间断的测量仪”。要以 1kHz、10kHz 甚至更高的采样率连续采集电压每次采样结果都进中断让 CPU 搬运中断负载会高到不可接受。DMA 在这里的价值是ADC 每转换完一个通道触发 DMA 请求DMA 把结果自动搬到内存数组里数组可能存了几百上千个采样点CPU 按自己的节奏去读这批数据做滤波、分析、显示。多通道 ADC 的情况更明显。比如 STM32 的 ADC1 用扫描模式采集 8 个通道转换顺序由规则组定义DMA 按规则把每个通道的结果依次放到目标数组。你只需要把数组大小设为“通道数 × 采样次数”比如 8 通道采集 100 轮就是 800 个半字长度的数组DMA 放进来的顺序天然对应通道顺序。这里要特别注意 DMA 循环模式和数据对应关系。假设你配置了 ADC 扫描 8 个通道目标数组长度 8DMA 用循环模式那么每转换完一轮8 个通道的数据就依次刷新一遍。如果你把数组长度设成 8×NDMA 就会把 N 轮数据连续存放直到存满或循环覆盖。读取时要知道第 i 个采样点对应第 i%8 个通道。这种对应关系不提前想清楚滤波算法全都会因为数据错位而跑飞。安富莱 AD7606 之类的板卡和模块也是同样的逻辑。AD7606 是 8 通道同步采样的高速 ADC外部通常用并行接口或 SPI 接口跟 MCU 通信强调同步采样和高速吞吐。如果走 SPIMCU 端一般就是 SPI DMA 接收AD7606 转换完成后通过 BUSY 引脚通知 MCUMCU 再发起 SPI 读取DMA 把 8 个通道的数据一次性搬进内存。驱动里 BUSY 引脚的中断优先级、SPI 时钟速度、以及 DMA 缓冲区大小是三个关键设计点。BUSY 中断要够快才能保证采样同步精度SPI 时钟要够高才能在采样间隔内搬完数据DMA 缓冲区要够大才能缓存多次采样结果。我做这类采集时会先用一个固定频率的方波源接在其中一个通道上验证 DMA 搬运的数据顺序是否正确再用逻辑分析仪看波形确认最终结果。3.3 Continuous Requests 与 TIM DMA Burst定时器触发批量搬运的玩法热词里的“DMA continuous requests”和“TIM DMA Burst”属于 DMA 的高级玩法很多人在基本收发调通之后才开始碰。先说 continuous requests在很多 MCU 里外设触发 DMA 的请求模式可以配置成单次请求或连续请求。单次请求模式下外设发一次请求DMA 搬一个数据就停了连续请求模式下外设持续保持请求有效DMA 会一直搬运直到预设的传输计数耗尽。对于 ADC 这类持续产生数据的外设用 continuous requests 可以保证数据不断被搬走不会因为请求模式的差异而卡住。TIM DMA Burst 是另一个很实用的用法。定时器不仅能产生 PWM还能通过 DMA 批量更新自己的多个寄存器。最典型的应用是多路电机或多路 LED 控制你需要同时更新 4 路 PWM 的占空比传统做法是连续往 CCR1、CCR2、CCR3、CCR4 寄存器里写值中间要间隔、要判断更新速率上不去。DMA Burst 模式把这一串寄存器写入变成一个突发传输内存里放一个数组比如[1500, 3000, 4500, 6000]定时器溢出事件触发 DMADMA 按地址递增把数组依次写入 CCR1、CCR2、CCR3、CCR4一气呵成占空比更新延迟极低而且所有通道是同一时刻生效的。使用 TIM DMA Burst 有个关键点DMA 的目的地址是“当前突发需要更新的那个寄存器”它不会写在程序里写死的某个 CCR 上而是通过 DMA 的突发配置和地址递增规则访问定时器的寄存器空间。因此你得仔细看定时器寄存器地址映射和 DMA 突发长度配置确保“起始地址 突发长度”所覆盖的范围就是你想更新的那批寄存器。配置错位是最常见的失败原因明明开启了 Burst寄存器却没更新或者更新了不该更新的寄存器导致 PWM 输出异常。标准参考做法是先只更新一个寄存器验证 Burst 基本流程再扩展到多寄存器别一上来就整 4 路。4. DMA 测速与验证怎么证明你的驱动真的快了4.1 从计数器和时间戳读吞吐说完场景聊聊热词里的“DMA 测速”。很多人把 DMA 配置好能收能发就以为万事大吉但实际吞吐到底达到没有、CPU 有没有真正被解放必须通过测速来验证。DMA 测速最直接的办法就是测实际搬运时间。以串口 DMA 接收为例你设定了 1Mbps 波特率理论上每秒最多接收约 100KB 数据实际能不能达到最简单的方法是让上位机以固定频率发送一大包数据你在收到完整数据包之后读系统 tick计算包间的平均间隔。还可以利用 DMA 的剩余计数器做一个“瞬时接收速率”估算收到 N 字节的数据记录开始时间和结束时间以 1 秒为窗口统计总量就能算出真实吞吐。比如上位机连续发 100 包每包 1024 字节统计这批数据的接收耗时用字节数除以时间就是实际接收速率。SPI DMA 测速同样可以做。在 SPI 从设备端用 GPIO 翻转法开始发送前置高一个测试引脚发送完成回调里拉低用逻辑分析仪测量这个高电平的脉宽。这个脉宽就是 DMA 搬完一包数据的实际时间。举个例子SPI 时钟 36MHz单字节需要 8 个时钟周期理论上发送 1024 字节最快约 1024*8/36MHz约 227.6μs。如果你的 DMA 传输完成脉宽测出来是 300μs差距 30% 以上那就要查 SPI 配置、DMA 数据宽度、是否有额外的等待或中断开销了。理论值和实测值对照是驱动性能调优最靠谱的方法。4.2 用示波器和逻辑分析仪抓细节我自己做驱动验证时很少只依赖代码里的统计变量因为软件统计本身也可能有偏差。更可靠的办法是用一个复用引脚配合 GPIO 翻转来标记关键事件DMA 开始搬运时拉高一脚DMA 完成中断里拉低一脚双通道示波器同时看 SPI 时钟和这个标记引脚就能清楚看到“搬运期间有没有时钟空洞”“片选是不是在最后一个字节之后才拉高”。这些细节在高速下非常关键。再说一个常见误判DMA 测速失败并不总代表 DMA 配置错。有时候是上位机发送节奏太快、缓冲区溢出有时候是中断优先级太低DMA 完成中断迟迟得不到响应有时候是缓存一致性问题导致 DMA 写的数据 CPU 读不到测速程序等待超时。所以遇到测速失败先不要急着改 DMA 配置按“缓冲是否溢出 - 中断是否响应 - 数据是否可见 - 配置是否正确”的顺序排查往往更快。我后来做了一套通用的“GPIO 标记 逻辑分析仪”验证模板大多数 DMA 问题都能在 10 分钟内定位到具体环节。5. 这几个 DMA 问题够你排查一晚上5.1 缓存一致性导致 DMA 数据“看不见”在带 Cache 的芯片上比如 Cortex-M7 的 STM32F7/H7、Cortex-A 系列DMA 访问内存是绕过 CPU 缓存的这就引出一个非常经典的问题DMA 往内存写数据CPU 因为 Cache 命中去读内存里的旧值或者 CPU 往内存写数据DMA 搬运时搬走的却是 Cache 里的旧内容。这就是缓存一致性问题。解决思路分两步发送前要把待发送的缓冲区数据从 Cache 写回内存用SCB_CleanDCache或SCB_CleanDCache_by_Addr接收后要告诉 CPU 这片区域已经被外部更新需要作废缓存条目重新从内存读用SCB_InvalidateDCache_by_Addr。而且 DMA 缓冲区必须按 32 字节对齐不同内核要求的对齐长度不同M7 一般是 32 字节否则失效操作可能波及相邻数据。国产很多 M7 内核芯片的坑也集中在这里配置时一定要看芯片厂商的 Cache 操作建议。5.2 DMA 通道冲突STM32F103C8T6 这类芯片的 DMA 通道是固定的某个外设事件可能和另一个外设的事件在同一个通道上竞争。热词里“stm32f103c8t6 dma 同道”指的就是这个问题USART1_TX 走 DMA1_Channel4而某个定时器的更新事件也可能映射到 DMA1_Channel4 之类的通道一旦多个外设映射到同一个 DMA 通道就会出现“改了 A 外设的配置B 外设反而不工作了”的灵异现象。解决方法没有捷径设计外设资源时查清楚请求映射表保证每个 DMA 通道只被一个外设占用。如果通道实在不够可以研究一下 DMA 空闲时隙复用的思路但复杂度会显著上升除非项目必要否则调整外设资源或换引脚更划算。5.3 方向配置错误导致数据“全是同一个字节”DMA 方向配置错了表现非常诡异。比如 SPI 发送你把源地址设成了 SPI 数据寄存器目的地址设成了内存DMA 以为自己是接收结果发送出去的全是同一个值内存里还会被写入一堆随机数据。排查时要先把源地址、目的地址和递增/固定模式一项项过外设到内存时外设寄存器地址固定不变内存地址递增内存到外设时反过来内存源地址递增外设目的地址固定。很多人栽在“把源地址设成固定”上一次性把同一个值发几万个字节。5.4 传输完成标志被误读DMA 的标志位跟外设标志不是一回事。比如 DMA 传输完成标志是 TCIF而串口的发送完成标志是 TC名字很像作用完全不同。有人在串口中断里读到串口 TC 标志后去清 DMA 的 TCIF清完发现标志还在或者把 0 误判成完成导致逻辑错乱。建议写驱动时把不同外设的标志位名写清楚别省那几个字符。5.5 半传输中断和传输完成中断没配合好前面提到乒乓缓冲依赖半传输和传输完成两个中断。这俩中断的触发时机很关键半传输中断是 DMA 搬运了总长度一半时触发传输完成中断是搬完所有数据后触发。如果你只开了完成中断却需要搬运完成前就处理前一半数据就会浪费等待时间如果你只开了半传输中断后面一半数据永远不会被处理。新手最常见的错误是循环模式下传输完成标志不断循环触发但没清理标志或没重新使能 DMA第二次循环就卡在第一个触发点。正确做法是在中断服务里清理对应标志、必要时重新使能传输保证 DMA 处于“搬运-中断-清理-继续搬运”的正常循环中。5.6 初始化顺序不对导致第一包正常、后续错乱DMA 驱动的初始化顺序也有讲究。我见过一个很怪的问题SPI DMA 发送第一包数据正常第二包开始数据错乱查了半天原因是 SPI 的 DMA 请求还没使能程序就把 DMA 配置好了导致 DMA 的第一次请求发生在错误的时间点。正确顺序一般是先初始化外设和对应的 DMA 请求映射再启动 DMA 传输。在 HAL 库里通常是调用HAL_UART_Receive_DMA、HAL_SPI_Transmit_DMA这类启动函数由它内部完成外设和 DMA 的联动配置不要在它之前手动乱开 DMA。5.7 常见问题速查表现象可能原因排查顺序数据字节错位数据宽度、地址递增配置不对查 DMA 数据宽度和递增设置只收到一帧后再也不收循环模式未开或传输完成标志未清查控制寄存器 CIRC 位、中断清标志CPU 读到的数据是旧值缓存一致性问题Clean/Invalidate DCache对齐缓冲区外设之间互相干扰DMA 通道冲突查请求映射表发送的一直是同一个字节方向配置或目的地址固定设置错误查方向、递增模式一帧满缓冲区时不触发处理缓冲区长度等于帧长空闲中断未产生扩大缓冲区或加完成中断兜底这些问题我都实际遇到过而且大概率你也会遇到。记住一个原则先怀疑自己的配置再用最小复现排除配置之外的因素。把问题拆成一个最小工程往往比在一堆业务代码里瞎猜快得多。6. 写在最后DMA 驱动开发我觉得最重要的是这件事做 DMA 驱动这些年我最大的体会是DMA 本身不难难的是你跟整个系统的配合。你不仅要会配寄存器、写回调、清标志还要理解数据流、中断时序、缓存一致性甚至要会看逻辑分析仪上的波形。真正的高手不是把 DMA 用得多花哨而是能在一开始就预估到传输方向、缓冲策略、中断优先级这些架构级问题。第 6 期聊到这里下一期我大概率会写中断嵌套和任务调度相关的内容毕竟 DMA 搬完数据后真正的业务竞争才刚开始。如果你也在调 DMA 卡了很久试试把问题拆成“数据从哪来、到哪去、谁搬、搬完谁处理、何时处理”五步很多问题自己就清晰了。
延伸阅读

更多相关文章

2026/10/28 9:08:42

C# WinForm 嵌入 CefSharp:.NET 4.0 老项目实战避坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/28 9:08:42

SAR影像变化检测中的4种差异图原理与实战选型

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/28 9:08:42

STM32F103硬件I2C驱动AT24C02实战:从时序到页写避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/28 12:25:48

企业AI培训实战指南:精准匹配与高效落地

1. 企业AI培训的现状与痛点最近两年,AI技术培训在企业内部如火如荼地展开,但真正取得实效的却寥寥无几。作为一位参与过十余家企业数字化转型的顾问,我见过太多"为了培训而培训"的失败案例。某制造业客户曾花费200万采购AI课程&…

2026/10/28 12:25:48

ANP协议:基于HTTP/TLS与DID的智能体可信通信标准

1. 这份白皮书到底在解决什么问题?——从“Agent Network Protocol”这个命名开始拆解“Agent Network Protocol”(ANP)这个名称本身就是一个强信号:它不是在讲单个智能体(Agent)怎么思考,而是在…

2026/10/28 12:25:48

MPAndroidChart滑动柱状图:视口配置、手势冲突与性能优化

简介:面向Android开发者的MPAndroidChart滑动柱状图实战工程,聚焦图表在有限屏幕内展示大量分类数据时的横向浏览需求。工程围绕柱状图拖拽、缩放与视口范围控制展开,涉及手势监听、X轴显示区间调整以及按可视范围动态加载数据等实现思路&…

2026/10/28 12:25:48

飞书文档复制受限怎么解决?权限模型与八种方案全解析

同事甩过来一个飞书文档链接,说“第三部分的数据你直接复制到周报里就行”。你点开、选中、CtrlC、切到编辑器、CtrlV,粘出来一片空白,或者只剩一个标题。换个浏览器、换台电脑、退出重进,还是不行。这时候大部分人的第一反应是“…

2026/10/28 12:25:47

B+树索引原理与数据库查询优化实战

1. 穿透索引的本质与B树结构解析当我们在数据库查询中听到"索引穿透"这个词时,实际上是在讨论查询执行过程中如何与存储引擎的索引结构交互。B树作为现代关系型数据库最常用的索引数据结构,其独特的叶节点与枝节点设计直接决定了查询效率。B树…

2026/10/28 12:20:47

电商SEO与用户体验优化的核心关联与实践

1. 电商SEO与用户体验的本质关联在电商行业摸爬滚打多年后,我发现一个被许多新手运营忽略的真相:SEO优化和用户体验优化(UX)从来就不是割裂的两个部门工作,而是同一枚硬币的正反面。去年我们团队操盘的一个家居品类独立…

2026/10/27 14:24:03

全国计算机二级C语言真题PDF高效刷题指南

简介:这份PDF是全国计算机二级C语言考试的试题与答案解析,专为备考二级C语言的考生准备。内容涵盖C语言特点与设计理念、程序基本组成单位、main函数执行流程、函数嵌套调用规则、标识符命名规范、整型实型字符型数据类型、八进制十六进制数合法形式、运…

2026/10/27 9:34:54

requests+BeautifulSoup:爬虫抓取h1标题的入门实战指南

简介:一份以实战为导向的Python爬虫入门资源,面向具备基本编程基础、希望快速掌握网络数据抓取的初学者,可解决从零编写简单爬虫的常见困惑。实例以requests库发送HTTP请求、BeautifulSoup解析HTML文档,完整演示从网页中提取 标题…

2026/10/28 3:32:29

递推最小二乘参数辨识MATLAB编程:二阶系统与渐消记忆实现

简介:这是一篇面向控制系统、信号处理与机器学习等领域学习者的最小二乘参数辨识程序说明文档。资源共包含一个docx格式文档,压缩包大小七十八KB,内容围绕递推最小二乘算法和渐消记忆最小二乘算法展开:先构造一个极点位于单位圆内…

2026/10/28 0:02:13

Python股票自动交易系统源码解析:从行情获取到风控的完整链路

简介:这份资源是面向高校计算机、金融相关专业毕业设计场景的完整项目源码包,主题为基于Python的股票自动交易系统,适合需要完成毕设选题、课程设计或自学金融量化交易开发的学生与开发者参考。压缩包共收录687个文件,整体约14.93…

2026/10/28 0:02:13

视酷酷信IM源码部署指南:Java 17+MySQL 8.0.33实战配置

简介:这是一套2024年亲测可部署的视酷酷信IM即时通讯系统源码,面向具备Java、OC、C#及Vue开发能力的技术人员,解决私有化即时通讯平台快速搭建与二次定制需求,尤其适用于需集成商城、保障数据主权的中小型企业或独立开发者。资源共…

2026/10/28 0:02:13

Python爬取百度迁徙数据:接口解析、批量采集与避坑指南

简介:这份资源是一个用Python爬取百度迁徙数据的脚本压缩包,面向具备Python基础、希望入门网络爬虫与数据解析的开发者,解决按日期获取全国热门城市人口迁徙数据的问题。包内共1个文件,为单个py脚本,压缩包约1KB&#…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑