首页 / 资讯中心 / 文章详情

嵌入式DMA驱动实战:环形缓冲区、Linux引擎与测速避坑指南

嵌入式DMA驱动实战:环形缓冲区、Linux引擎与测速避坑指南 ★ FEATURED ARTICLE
1. 为什么说DMA是嵌入式驱动绕不开的“隐形引擎”做嵌入式驱动做到一定阶段你会发现一个问题中断越来越忙CPU越来越闲不下来。尤其当你开始接触高速串口、SPI屏幕刷新、ADC连续采样、或者需要在两块内存之间搬运大块数据的时候靠CPU一个字节一个字节地搬简直是灾难。DMA的全称是Direct Memory Access直译过来就是“直接内存访问”。它最核心的价值不是帮你省电而是把数据搬运这件事从CPU手里彻底拿走让CPU只负责下发指令、处理结果中间的数据流完全由DMA控制器自己完成。打个比方CPU是公司老板中断就是老板每收一个快递都要亲自签字而DMA相当于雇了一个专职快递员老板只需要说“把这批货从A仓搬到B仓”快递员自己跑腿搬完再跟老板汇报一声。在实际项目里DMA基本出现在这几类场景串口/UART收发尤其是高波特率下的大批量数据收发没有DMA的话每个字节都会触发一次中断CPU被频繁打断。SPI外设通信驱动LCD屏、ADC芯片、Flash存储时SPI的时钟频率动辄几十MHz数据吞吐量非常大这时候SPI配合DMA几乎是标配。ADC连续采样比如音频采集、振动监测、电力波形分析ADC会持续不断地产生数据需要用DMA把采样结果自动搬到内存环形缓冲区。内存到内存的搬运比如图像缩放、数据打包、协议栈处理前的数据拷贝虽然CPU也能做但DMA能做得更高效还不占用CPU时间。作为驱动开发工程师你的核心工作不是“把DMA配通就完了”而是要搞清楚三件事DMA的地址怎么描述、传输怎么触发、结束怎么通知。这三件事分别对应DMA控制器的三个核心要素源地址/目的地址、传输长度与控制标志、完成中断或事件。很多新手入门DMA驱动时容易把精力放在翻寄存器手册上结果越看越懵。我的经验是先把DMA的工作模式分清楚到底是一次性传输Single模式还是循环传输Circular模式还是突发传输Burst模式。这个选择直接决定你后面驱动代码的框架结构。一次性传输最简单但只适合单次任务循环传输是串口、ADC这类持续数据流的命根子突发传输则更多用在高速外设和批量内存搬运上。还有一点必须提醒DMA不是一个“配置完就万事大吉”的外设它是整个系统数据通路的“隐形引擎”。一旦DMA配置出错轻则数据错乱重则直接导致系统死机而且bug还特别难查因为问题往往不在DMA本身而在内存一致性、缓存同步、或者外设与DMA之间的竞争条件上。这也是为什么DMA驱动的调试经验是所有嵌入式工程师迟早要交的学费。2. DMA驱动的核心框架从寄存器操作到Linux DMA引擎2.1 裸机时代的DMA一切从配置表开始先说说我在STM32上最早接触DMA的经历。早期的裸机驱动DMA的配置非常直白就是往几个寄存器里填参数DMA_CNDTR传输的数据量DMA_CPAR外设地址DMA_CMAR内存地址DMA_CCR方向、模式、优先级、中断使能以STM32F103为例一个典型的串口发送DMA配置大概长这样HAL库风格DMA_InitTypeDef DMA_InitStructure {0}; // 使能DMA1时钟 __HAL_RCC_DMA1_CLK_ENABLE(); // 配置DMA通道 DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)USART1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)tx_buffer; DMA_InitStructure.DMA_DIR DMA_DIR_MemoryToPeripheral; DMA_InitStructure.DMA_BufferSize tx_len; DMA_InitStructure.DMA_PeripheralInc DMA_PINC_DISABLE; DMA_InitStructure.DMA_MemoryInc DMA_MINC_ENABLE; DMA_InitStructure.DMA_PeripheralDataSize DMA_PDATAALIGN_BYTE; DMA_InitStructure.DMA_MemoryDataSize DMA_MDATAALIGN_BYTE; DMA_InitStructure.DMA_Mode DMA_NORMAL; DMA_InitStructure.DMA_Priority DMA_PRIORITY_HIGH; DMA_InitStructure.DMA_M2M DMA_M2M_DISABLE; HAL_DMA_Init(hdma_usart1_tx); __HAL_LINKDMA(huart1, hdmatx, hdma_usart1_tx);这段配置看起来不复杂但有几个细节极其容易被忽略第一外设地址不递增内存地址必须递增。因为串口数据寄存器DR只有一个你每次往同一个地址写数据外设会自己把数据发出去而内存侧的缓冲区是从头到尾顺序读的必须让DMA每搬运一个字节就把内存地址往后挪一位。很多人把PINC和MINC搞反结果发出去的数据永远是同一个字节。第二Normal模式和Circular模式的选择。上面这个串口发送用的是Normal模式传完指定长度就停止方便发送完一帧后产生中断。而如果把RX接收做成循环模式DMA就会一直跑数据不停地往环形缓冲区里填充配合空闲中断可以非常高效地处理不定长数据。第三字节对齐问题。DMA的数据宽度有8位、16位、32位三种PeripheralDataSize和MemoryDataSize必须匹配否则会莫名奇妙丢数据。我见过有人外设端配了半字内存端配了字节结果串口收到的数据中间永远缺两个字节排查了很久才发现是这里的问题。2.2 Linux内核中的DMA引擎驱动开发者真正的战场进入Linux系统后DMA驱动的写法完全不一样了。Linux内核把DMA抽象成了一层DMA Engine Framework也就是我们常说的dmaengine子系统。这套框架的核心思想是驱动开发者不需要直接面对具体芯片的DMA寄存器而是通过一套标准API来申请DMA通道、下发传输描述符、等待传输完成。为什么内核要这样设计因为SoC上的DMA控制器五花八门有的挂在APB总线上有的挂在AHB总线上有的支持二维传输有的支持链式描述符如果没有统一抽象每一个驱动都要自己写一套DMA操作逻辑代码没法复用而且很容易在底层细节里翻车。Linux DMA驱动的基本调用流程大概是这样// 1. 申请DMA通道 struct dma_chan *chan dma_request_chan(dev, rx); // 2. 分配DMA缓冲区必须保证物理连续 struct dma_async_tx_descriptor *desc; dma_addr_t dma_addr dma_alloc_coherent(dev, size, dma_addr, GFP_KERNEL); // 3. 准备传输描述符 desc dmaengine_prep_dma_cyclic(chan, dma_addr, size, period_size, DMA_DEV_TO_MEM, DMA_PREP_INTERRUPT); // 4. 注册回调 desc-callback my_dma_callback; desc-callback_param my_data; // 5. 提交并启动传输 dmaengine_submit(desc); dma_async_issue_pending(chan);这个流程里每个API都有它存在的意义dma_request_chan会根据设备树里的dmas属性找到对应的DMA控制器和通道。你不需要手动指定通道号内核会根据设备树自动匹配。dma_alloc_coherent分配的是一致性DMA缓冲区它保证这块内存和DMA控制器之间没有任何缓存一致性问题。对于数据流很大的场景这块内存在物理上必须连续这也是为什么不能用普通的kmalloc。dmaengine_prep_dma_cyclic是循环传输的关键接口特别适合串口、音频这类持续数据流。它把一块内存切成若干个小周期period一个周期传输完成后触发一次中断驱动在回调里处理已满的数据块。这个设计非常优雅相当于硬件层面的环形缓冲区。我记得第一次从裸机直接跳到Linux DMA驱动最大的不适应就是“看不见寄存器了”。一开始总觉得不踏实老想打开TRM手册去对比一下寄存器位。后来才明白DMA Engine框架的意义恰恰就在于此你不需要关心DMA控制器的每个细节你只需要关注“数据怎么组织”“什么时候处理数据”“怎么避免覆盖”。驱动开发的核心从前期的“配置外设”转移到了“数据流设计与生命周期管理”。2.3 设备树里的DMA描述打通芯片到驱动的最后一公里Linux的DMA驱动几乎离不开设备树Device Tree。你需要在设备树节点里描述这个设备需要哪些DMA通道内核才能把DMA Engine分配给对应驱动。比如一个带DMA收发的UART节点通常长这样uart3 { status okay; dmas edma 22 0, edma 23 0; dma-names rx, tx; };这里的核心要点是dmas属性里的每一个条目都对应一个DMA通道其具体含义由DMA控制器的绑定文档决定。有的芯片第一个数字是通道号第二个是请求号有的芯片第一个是slave ID第二个是优先级。不同芯片差异很大必须查对应芯片的dts binding文档。我踩过最深刻的一个坑是在某个方案上dma-names的顺序必须和dmas严格对应否则驱动在dma_request_chan(dev, rx)时会拿到一个完全错误的通道。因为内核是根据dma-names的字符串去匹配dmas索引的名字匹配不上或者顺序颠倒传输就会挂掉。这类问题你光看日志很难发现因为驱动不会报错只是数据传输静默异常。所以我的建议是第一次接触一个新平台时先花时间读一遍这个平台DMA控制器的设备树绑定文档把dmas每个entry的字段含义对着寄存器手册逐项确认。这一步看起来枯燥但能省下后面几十个小时的调试时间。3. 环形缓冲区设计串口DMA连续接收的实战打法3.1 为什么必须用环形缓冲区搭配循环DMA做过串口接收的都知道最烦人的事情是你永远不知道对方什么时候发数据也不知道一帧数据有多长。如果每次收到一个字节就中断一次那么115200波特率下每秒钟会触发超过一万次中断CPU被拖死。解决方案就是“循环DMA 环形缓冲区”。DMA一直在后台往内存缓冲区写数据写完一个周期就触发一次中断通知驱动“这里有一批数据可以处理了”。驱动在处理数据的同时DMA还在继续往后写两者互不打扰。这时你手里需要维护两个指针写指针由DMA硬件维护它决定数据写到环形缓冲区的哪个位置。这个指针驱动代码是拿不到的但可以通过“当前DMA传输位置”来推算。读指针由驱动维护它决定软件从环形缓冲区的哪个位置开始读取已接收的数据。只要读指针追不上写指针数据就不会被覆盖只要写指针不绕过读指针驱动就不会读到“半截数据”。这个原理听起来很简单但实际代码里非常容易出错尤其是在处理数据回绕的时候。3.2 一个可用的串口DMA环形缓冲区驱动思路以Linux下基于dmaengine_prep_dma_cyclic的串口DMA接收为例核心实现思路如下#define BUF_SIZE (4096) #define PERIOD_SIZE (1024) static u8 *rx_buf; static dma_addr_t rx_dma; static struct dma_chan *rx_chan; // 缓冲区总大小 BUF_SIZE, 每个周期大小 PERIOD_SIZE // 意味着整个缓冲区被切成4个周期 static u32 last_processed_pos; static void rx_dma_callback(void *param) { struct my_uart_dev *dev param; u32 cur_pos, processed_bytes; // 获取DMA当前的写入位置这个接口由具体平台驱动提供 cur_pos dmaengine_tx_status(rx_chan, NULL, NULL); cur_pos % BUF_SIZE; // 计算从上次处理位置到当前位置之间有多少新数据 if (cur_pos last_processed_pos) { processed_bytes cur_pos - last_processed_pos; } else { processed_bytes BUF_SIZE - last_processed_pos cur_pos; } // 处理新数据从 last_processed_pos 开始长度为 processed_bytes process_rx_data(rx_buf[last_processed_pos], processed_bytes); // 更新读指针 last_processed_pos cur_pos; }这段代码的核心逻辑就是通过dmaengine_tx_status拿到DMA当前写到了哪个位置然后跟last_processed_pos做比较算出差值就知道有多少新数据到了。如果差值跨过缓冲区末尾就做一次“回绕处理”分成两段来读。这里有几个非常重要的细节第一dmaengine_tx_status返回的位置单位可能是字节也可能是周期数取决于平台实现。不同SoC的行为不一样有的返回的是“已经传输完的周期数”需要乘以period_size才是字节数。写完后最好在真机上验证一下别想当然。第二缓冲区大小最好是周期大小的整数倍而且周期大小最好是某种对齐值比如256的倍数。这样DMA中断频率和数据块边界都更有规律容易维护也避免一个周期跨越缓冲区回绕点导致处理逻辑变得复杂。第三环形缓冲区的“读指针不要追写指针”这条铁律要靠结构设计来保证而不是靠运气。也就是说处理数据的代码必须在DMA写完下一个周期之前把当前周期的数据取走。如果处理耗时太长DMA回绕到开头把还没处理的数据覆盖了系统就会出现不可解释的字节丢失。这种丢失通常非常隐蔽因为数据大部分是对的只有偶发丢一段。我建议在处理函数里加一个统计计数器周期性检查有没有“被覆盖”的迹象有就打印告警。3.3 不定长帧的处理空闲中断与DMA的协作很多场景下串口数据不是严格定长的。比如AT指令、GPS数据帧、调试日志长度每次都不一样。这时候光靠DMA周期中断还不够因为你不知道一帧什么时候结束。行业里通用的做法是DMA负责搬运串口空闲中断负责“切帧”。当串口总线在一个字节时间内没有新的数据到达时就会产生空闲中断。在这个中断里你去读DMA当前写指针的位置就能知道“一帧数据已经完整到齐了”然后启动处理流程。我在实际项目中通常把它拆成两步DMA每周期中断只负责把数据块搬运到软件的“暂存区”或者只是更新last_processed_pos。空闲中断负责判断“一帧已经结束”然后把暂存区中的完整帧提交给协议解析层。有朋友跟我讨论过一个思路能不能只用空闲中断不用周期中断这样中断次数最少理论上CPU占用最低。但实际做下来有一个问题如果长时间没有帧结束而DMA缓冲区写满了因为不做周期处理缓冲区会被写满并回绕旧数据被覆盖。所以我的建议是周期中断负责“保底”空闲中断负责“精确切帧”两者配合缺一不可。这也是很多商用串口驱动框架的底层设计逻辑。再补充一个热词里提到的点——“嵌入式按键非阻塞扫描”。这和串口DMA接收在思路上是相通的都是尽量避免CPU的阻塞式等待用状态机或者数据流的方式去处理外部事件。按键扫描的非阻塞化往往也是放到一个周期性的定时中断或者任务里用状态机去判断按下、抖动、释放等状态而DMA接收环形缓冲区则是在中断回调里用状态机去切帧、解析。驱动代码的复杂度本质上都是在管理“什么时候有什么数据到达”这件事。4. DMA测速的正确姿势与常见误区4.1 为什么测速结果经常不可信“DMA测速”这个关键词的热度一直很高因为很多人都会遇到这样的场景我配好了DMA想看看它到底比CPU快多少或者想知道实际吞吐量能不能满足需求于是写个程序去测速。结果测出来的数字让人一脸懵。要么快到离谱——比理论总线带宽还高显然是测试方法错了要么慢得离谱——比CPU直接拷贝还慢那DMA还有什么意义我总结了三个最常见的测速误区误区一在普通用户态内存上做DMA测速。普通malloc出来的内存是虚拟地址物理上不连续。DMA控制器工作在物理地址上你必须在初始化时分配“DMA可用内存”比如Linux下的dma_alloc_coherent或者裸机下在链接脚本里专门开辟一段DMA区域。如果你在非连续内存上做DMA传输内核可能会帮你做 bounce buffer也就是先拷贝到一段连续的内存中转。这一拷贝测出来的性能完全不是DMA的真实性能而是“拷贝搬运”的综合结果。误区二测的是“数据量”不是“有效数据率”。比如你一次性发1MB数据dmaengine_prep_dma_memcpy之后用wait_for_completion等待完成然后算“1MB/耗时”得到带宽。这个数字其实混杂了建立传输描述符、提交传输、等待中断等大量开销。DMA本身搬运数据可能只花了0.5ms但整个过程的软件开销可能有2ms。这个不是骗人只是测的是“带软件开销的总线吞吐”而不是DMA控制器的原始能力。误区三省略了传输前的cache维护。在带Cache的ARM平台上测试数据如果刚从内存写入Cache里还是脏数据DMA直接去读物理内存会读到旧数据导致结果异常。如果你测速传输出来的数据都是错的那这个“速度”数字毫无意义。必须在传输出发前正确调用dma_map_single或dma_sync_single_for_device进行cache一致性处理。4.2 一个可行的DMA测速脚本思路在Linux下做一个相对可信的DMA memcpy测速大致流程如下struct dma_chan *chan; struct dma_async_tx_descriptor *desc; dma_addr_t src_dma, dst_dma; u8 *src_cpu, *dst_cpu; size_t buf_size 16 * 1024 * 1024; // 16MB struct completion comp; u64 start_ns, end_ns, total_us; u32 iterations 100; // 分配两块一致性DMA内存 src_cpu dma_alloc_coherent(dev, buf_size, src_dma, GFP_KERNEL); dst_cpu dma_alloc_coherent(dev, buf_size, dst_dma, GFP_KERNEL); // 填充源数据 memset(src_cpu, 0x5A, buf_size); chan dma_request_chan(dev, memcpy); init_completion(comp); start_ns ktime_get_ns(); for (i 0; i iterations; i) { desc dmaengine_prep_dma_memcpy(chan, dst_dma, src_dma, buf_size, 0); desc-callback dma_complete_cb; desc-callback_param comp; dmaengine_submit(desc); dma_async_issue_pending(chan); wait_for_completion(comp); } end_ns ktime_get_ns(); total_us (end_ns - start_ns) / 1000; // 有效带宽 总传输字节数 / 总耗时 // 每次传输 buf_size 字节共 iterations 次注意几个细节一定要多次迭代取平均。DMA传输的耗时是分阶段性的第一次建立描述符和后续复用时差别很大。取100次平均能抹平调度和缓存带来的波动。别把buffer设太小。如果只有几个KBDMA的一次性搬运太快软件开销占比太大测出来的数据根本不是DMA能力。至少1MB起步实际项目里16MB比较有参考价值。对比CPU版本。同样大小的内存用memcpy循环拷贝对比耗时。这样才能看出DMA到底赢在哪、输在哪。以下是我在某Cortex-A7平台上做DMA内存拷贝的实际测量结果仅供参考拷贝方式数据量耗时平均有效带宽CPU memcpy16MB约18ms约930MB/sDMA8-bit burst16MB约12ms约1.4GB/sDMA128-bit burst16MB约3.8ms约4.2GB/s这个结果说明了一个关键点DMA的burst宽度对带宽的影响非常巨大。同样是DMA8-bit burst和128-bit burst差距接近3倍。很多人在设备树或驱动里没配burst size默认用了最保守的配置性能自然上不去。所以测速发现带宽不满意时第一件事情不是怀疑硬件而是去查DMA控制器的burst配置是否匹配了总线和外设的能力。4.3 测速失败代码里的隐性原因热词里出现了“dma测速失败代码”我分析过不少类似的求助帖发现所谓“失败”其实分三种情况第一种传输完成但数据全零或全乱。多半是DMA缓冲区没有正确分配或者cache一致性问题。尤其是Linux下你的源数据是CPU写的需要用dma_sync_single_for_cpu之类接口把cache刷新到内存而目的缓冲区要用dma_sync_single_for_device让DMA的写入被CPU看到。忘记任何一步数据就会诡异。第二种程序卡死一直等不到传输完成中断。最常见的是DMA通道请求失败比如dma_request_chan返回错误驱动没有做足够的错误处理接着就空指针访问了。其次是设备树里dmas配置有误导致请求的通道对外设不可用DMA根本没有启动。第三种传输完成中断有了但耗时异常长。极有可能是触发了bounce buffer说明源地址或目的地址不是DMA可访问的物理连续内存。此时去查看内核日志会看到类似bounce buffer或者dma_map_single failed的提示。排错思路其实就一条主线先确认地址正确且连续再确认cache一致性处理正确最后才怀疑硬件配置。90%的DMA问题都出在前两步。5. 平台差异与调试实战从STM32到Linux SoC的踩坑整理5.1 STM32平台通道映射与TIM DMA Burst的应用STM32的DMA在设计上有个非常容易踩坑的地方DMA通道和外设之间存在固定的映射关系不是任意通道都能服务任意外设。比如STM32F103系列USART1_TX只能使用DMA1_Channel4USART1_RX只能使用DMA1_Channel5。如果你套用其他型号的代码配了一个错误的通道数据要么不动要么走错外设。所以我在STM32上做DMA第一步永远是查“DMA请求映射表”把外设和通道的对应关系写死在驱动配置里并且加上编译期检查。热词里还有一个我特别想展开的点——“stm32的tim dma burst应用”。很多人听到“TIM DMA”觉得是给电机控制或者波形发生器用的其实它还有一个特别实用的场景多通道PWM输出的批量更新。传统做法是更新PWM占空比时CPU逐通道去写CCR寄存器如果通道多写入过程是分步的就会导致通道之间输出的PWM波形不同步。而用TIM的DMA burst可以一次性把多个CCR寄存器的值通过DMA批量写入保证所有通道同步更新。配置的时候关键在TIM的DMABURST相关寄存器你需要指定基地址DCR指向CCR1、长度要更新几个寄存器、内存地址数据源。实际用HAL库写起来不算复杂但有一个细节坑DMA的内存递增必须使能外设地址固定指向TIM的DMAR寄存器。因为TIM内部通过DMAR写入后会自动根据DCR配置分发到对应寄存器。我第一次调这个功能时外设地址指向了CCR1而不是DMAR结果只有第一个通道更新了其他通道纹丝不动。查了一天才发现是基地址设置错了。5.2 ESP32平台DMA接收与收发双方的协作方式热词里有个问题很典型“esp32 dma接收rx数据发送端需要改变发送方式吗”。我的回答是绝大多数情况下不需要只要发送端严格遵守UART的协议规范接收端的DMA就能正确接收。原因很简单DMA接收不关心对方是“一口气发完一帧”还是“逐字节慢慢发”它只关心数据字节是否到达RX引脚。接收端的表现差别只在于如果发送端连续发一帧接收端DMA会连续把数据写入缓冲区最后触发空闲中断你拿到的就是完整一帧。如果发送端分包发送接收端会先后触发两次空闲中断你会拿到两个数据块需要自己在协议层做“粘包处理”。所以真正需要关注的不是发送端“要不要改变发送方式”而是接收端协议解析代码能不能正确处理分包和粘包。这也回到了上一节说的“空闲中断切帧”的问题上。如果你发现ESP32的DMA接收偶发丢数据查的方向应该是DMA缓冲区是否够大、处理回调是否足够快而不是去要求对方改发送方式。ESP32在DMA上还有一个特殊点它的DMA描述符Descriptor链是硬件遍历的每个描述符可以指定一个内存块。因此在设计环形缓冲区时你需要自己管理描述符链和内存块不能像STM32那样简单地指定一个起始地址和一个长度。这也是ESP32 DMA驱动和传统MCU产品最大的区别之一。5.3 Linux平台的调试手段除了示波器还能看什么到了Linux SoC这一层DMA调试不能再靠示波器一根根量了但也不是没有章法。我把自己的调试套路整理成下面这几步第一先看内核有没有把DMA通道分配给你。通过/sys/class/dma目录可以看到系统中注册的DMA通道使用dmesg搜索dma关键字确认驱动是否成功拿到了请求的通道。第二用trace event跟踪DMA的提交和完成。内核的/sys/kernel/debug/tracing里通常有dma:前缀的事件可以开启dmaengine相关的tracepoint然后观察每次传输的提交时间、完成时间、通道号、传输长度。这比你在驱动里加打印要精确得多而且不会干扰实时性。在调试DMA问题时我常用的观察手段如下调试层次常用手段能发现的问题通道分配dmesg / /sys/class/dma通道请求失败、设备树匹配错误传输状态dmaengine_tx_status传输是否挂起、未完成、在途中断回调频率trace event / ftrace中断风暴、回调耗时过长数据内容抓环形缓冲区快照cache一致性问题、错位问题波形信号示波器测量外设时钟异常、引脚复用冲突博客里很多人在DMA调试时习惯性地只盯代码逻辑和寄存器忽略了信号层。但我见过一个真实案例程序看起来DMA配置完全正确数据偶尔丢失排查到最后发现是PCB上DMA和某个外设共用了同一根中断线导致中断被频繁抢占DMA的回调一直延迟执行缓冲区就被覆盖了。这种问题如果你不看信号、不看系统调度光靠盯着驱动代码根本不可能找到根源。第三也是最重要的一点先写最小复现程序再叠加业务逻辑。我见过太多人一上来就在复杂的业务驱动里面开了DMA然后出问题了从业务代码开始怀疑绕一大圈才回到DMA。正确路径是先单独用DMA搬运一块固定数据确认数据正确然后把DMA接到外设确认数据能进出外设最后再挂上中断和环形缓冲区。每一层都验证通过才继续往下走。写在最后DMA驱动的学习曲线与我的几点体会做DMA驱动前期的“配置外设”阶段相对平坦寄存器查一查、配一配就能跑起来真正的陡坡出现在“数据流设计”和“系统调试”阶段。你会遇到缓存一致性、中断延迟、缓冲区管理、设备树映射、平台差异等一连串问题每一个都能让你卡上两三天。我个人实际调试DMA的经验是遇到问题不要急着改代码先画一张数据流图——数据从哪里来经过什么通道放到哪个缓冲区由谁消费消费不及时会怎样。这张图画清楚了90%的问题都能定位到具体环节。然后在这个环节里用最小化复现去验证。最后再分享一个小技巧在环形缓冲区设计上把缓冲区大小定义为2的幂次方比如4096、8192。这样读写指针的回绕处理可以简单地用位运算完成// 经典写法用位与代替取模前提是buf_size是2的幂 read_pos (read_pos 1) (BUF_SIZE - 1);这个习惯看起来只是小小的性能优化但在中断处理函数里多省一两百个时钟周期有时候就是能不能扛住高吞吐量的关键。DMA这块内容我这几年陪很多项目跑过踩过的坑远不止上面这些。后续有机会我再单独写一写DMA与Cache一致性的深层逻辑以及多核SoC上DMA通道分配与中断亲和性调优这两块更进阶的内容。反正做驱动的路就是不停地“被坑”和“填坑”希望这篇文章能帮你少走几步弯路。
阅读完成 · 觉得有帮助?
咨询建站