ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

DMA从地址+偏移处启动:串口环形缓冲与STM32实战

DMA从地址+偏移处启动:串口环形缓冲与STM32实战 做嵌入式有一类需求看着不起眼真上手却能把人绕晕DMA传输时不从内存数组的头部开始而是要从第N个元素的位置开始。也就是标题里这句话Start transfer from address offset。我在一个串口DMA环形缓冲的项目里第一次认真体会到它的价值。串口收包是连续数据流DMA负责把数据搬到内存缓冲但处理完一包数据后下一包必须接着上一次写入的位置继续搬而不是从头覆盖。如果结构设计成永远从缓冲头部启动DMA那只能先停掉DMA、清空缓冲、再重新启动中间任何一个字节进来都会丢。换成从地址偏移的位置启动整个设计就顺了。这篇文章会把DMA 地址偏移这个功能拆开讲它背后的硬件机制是什么、不同MCU上怎么配置代码、以及我实际跑下来踩过哪些坑。文章主要面向正在用STM32、GD32这类MCU做ADC多通道采集、串口DMA收发、SPI/I2C DMA传输的开发者偏实战代码可以直接参考。1. 为什么要从地址偏移处启动DMA传输1.1 串口环形缓冲偏移不是可选项而是必需品先说最简单的场景。串口DMA接收最流行的方案是空闲中断DMADMA常开数据不断进入内存缓冲。缓冲通常设计成环形一个数组头尾相接读写指针各自维护。但问题来了DMA启动的时候你给它的内存目标地址必须是一个具体的数组下标不能是环形逻辑上的下一个位置。第一次启动还好直接给数组首地址。处理完一包数据后如果下次启动还给数组首地址前面没读走的数据就被覆盖了。正确做法是在每次重新启动DMA之前计算当前写指针在数组中的位置然后把DMA目标地址配置成数组基地址当前写指针偏移。这就是地址偏移的经典用途。#define RX_BUF_SIZE 256 uint8_t rx_buf[RX_BUF_SIZE]; volatile uint16_t write_index 0; // 当前写入位置 // 每处理完一包数据后用当前偏移重新启动DMA void uart_dma_restart(void) { // 关闭DMA通道 DMA_Cmd(DMA1_Channel5, DISABLE); // 让DMA从 write_index 处开始继续写入 DMA_SetCurrDataCounter(DMA1_Channel5, RX_BUF_SIZE - write_index); DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)rx_buf[write_index]; DMA_Init(DMA1_Channel5, DMA_InitStructure); // 重新使能 DMA_Cmd(DMA1_Channel5, ENABLE); }注意这里有一个关键点DMA_BufferSize也是根据偏移动态调整的。如果当前写指针在数组的100号位置那么这轮可用的空间是RX_BUF_SIZE - 100而不是整个数组长度。很多人只改地址忘了改传输长度结果DMA越过数组末尾写到了别的变量上这种内存践踏问题在调试时非常隐蔽。1.2 ADC多通道采样偏移能做什么不能做什么热搜词里stm32 adc多通道扫描循环采样dma出现频率很高我再展开讲讲这个场景。ADC多通道扫描模式下DMA会把每次转换结果依次搬运到内存数组。假设使能了8个通道DMA每轮会依次写入adc_values[0]到adc_values[7]。这时如果业务只需要后4个通道的数据能不能直接用DMA地址偏移来跳过前4个通道答案是不能。这里必须先纠正一个常见误区DMA不关心你正在转换的是第几个通道它只是被动地等待外设触发每次触发就搬一次数据。前4个通道的转换结果一定会产生DMA请求DMA就一定会搬运除非你在ADC侧做配置。所以从地址offset启动不是用来跳过ADC通道的它改变的是数据在内存中的落点而不是外设的数据产生节奏。ADC场景下的正确用法有两种。一种是把数据落到数组中间位置比如你要让adc_values[8]到adc_values[15]作为本周期数据前8个位置留给另一个DMA流使用那么HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_values[8], 8);另一种是分段采集。比如ADC扫描16个通道DMA一次搬不完或者你想把结果分开存到两个缓冲区这时可以分两次配置DMA第一次目标地址是buf_a传输8个半字第二次目标地址是buf_b再传输8个半字。第二次启动时目标地址相对于全局内存而言就是任意偏移。我在实际项目里用这种方式做过多路传感器数据分块效果很稳。但要提醒一句ADCDMA分段采集的时序比较紧如果ADC速度高两次DMA配置之间可能出现漏数据建议根据ADC采样频率和中断处理耗时评估一下必要时加大缓冲数组或者直接用循环模式搬一整片再由软件做切片。场景DMA地址偏移能解决吗推荐做法串口环形缓冲续传能动态计算偏移并重配地址ADC跳过前几个通道不能在ADC侧关通道或软件切片ADC数据分块存储能分次配置DMA目标地址SPI/I2C DMA到指定内存区域能起始地址直接设为目标下标2. 地址偏移的硬件实质寄存器里没有单独的offset框2.1 DMA地址寄存器的真面目很多人拿到芯片手册看到DMA有内存地址寄存器、外设地址寄存器、传输计数寄存器就一直以为硬件里有一个独立的偏移量字段。实际上绝大多数MCU的DMA并没有。你在代码里写的rx_buf[write_index]最终是CPU计算好完整地址后写入DMA的内存地址寄存器。DMA硬件从头到尾只知道一个绝对目标地址它不会去关心这个地址是不是某个数组的第几个元素。拿STM32F407举例DMA相关的关键寄存器只有这几个DMA_SxPAR外设地址寄存器存外设寄存器的地址比如(USART1-DR)或(ADC1-DR)。DMA_SxM0AR内存地址寄存器0存目标内存的绝对地址。DMA_SxNDTR传输数据个数单位是数据宽度字节/半字/字。你在代码里写了(uint32_t)rx_buf[write_index]编译后的结果就是rx_buf的基地址 write_index这个值被直接塞进DMA_SxM0AR。所以地址偏移这个操作本质上发生在CPU侧而不是DMA侧。DMA硬件就像一台只知道起点和距离的传送带你把起点坐标告诉它就行。从这里可以推导出一个实操结论只要你会算指针DMA就能从任意内存位置开始搬运不存在DMA只支持数组头部这种限制。同理传输长度也要跟着改因为DMA的NDTR是从起点开始的元素个数不是整个数组的长度。2.2 为什么硬件不帮你算偏移理解了寄存器结构之后自然会问既然偏移这么常用为什么芯片厂商不把偏移做进硬件我的理解是这是成本和通用性的权衡。DMA的核心职责是搬运它把CPU从数据搬迁中解放出来但地址计算本身并不复杂CPU算一下花不了多少时间。如果给DMA增加一个专门做偏移的寄存器意味着每个DMA通道都要多一个配置项除了增加硅片面积还会让寄存器配置逻辑变复杂。还不如让CPU把最终地址算好DMA保持专注搬运的纯粹性。当然也不是所有DMA都没有偏移寄存器。瑞萨的DMAC、部分专用DMA控制器确实有独立的地址偏移寄存器每次传输后除了可以选择递增/不变还可以配置递增多少。这类硬件更适合数据拼接、矩阵搬运等场景。但对于STM32、GD32这类通用MCU默认方案就是CPU算好最终地址DMA直接使用。这也解释了一个现象在使用HAL库时HAL_ADC_Start_DMA的缓冲区参数类型是uint32_t *但ADC采样值通常是uint16_t数组。底层其实是把你传的指针强转成内存地址它不关心你传的数组类型。所以你在传adc_values[8]时库函数内部并不会检查下标是否越界也不会帮你做对齐这些都得自己保证。2.3 偏移与传输计数的联动规则这里有一条非常重要的规则我称之为偏移守恒原则起始地址传输次数必须落在合法区域内。举个例子。你有一个uint16_t buf[32]要从buf[12]开始搬运20个元素。起始地址是buf[12]传输次数是20最后一处落点是buf[31]恰好合法。如果把传输次数写成32DMA就会从buf[12]一路写到buf[43]后面那12个元素已经超出数组边界会覆盖相邻内存中的其他变量。这在代码审查时很难发现因为从逻辑上看你只配置了缓冲区的一部分。我在给一个运动控制项目做代码评审时就见过这种bugDMA从pulse_buffer[8]启动但传输长度忘了从总长度中减去偏移量8结果每次运行时缓冲区后面的标志位都被DMA踩掉伺服使能信号时有时无排查了整整两天。所以我的习惯是任何带偏移的DMA配置代码里都加上一个注释或断言// 起始地址: buf[12]长度: 20落点范围: buf[12] ~ buf[31] #define DMA_BUF_LEN 32 #define DMA_START_OFFSET 12 #define DMA_TRANSFER_CNT (DMA_BUF_LEN - DMA_START_OFFSET) assert(DMA_START_OFFSET DMA_TRANSFER_CNT DMA_BUF_LEN);用宏把偏移量和传输次数的关系固定下来一目了然也避免后续维护时只改一处忘了另一处。3. STM32与GD32上的配置代码与验证过程3.1 标准库下的串口DMA环形缓冲实现先给一套基于STM32标准库的完整示例这个代码我在F103和F407上都跑过可以直接抄。#define RX_BUF_SIZE 256 uint8_t rx_buf[RX_BUF_SIZE]; volatile uint16_t rx_write_index 0; // 当前DMA下一次写入的位置 volatile uint8_t rx_new_data 0; // 有数据可处理标志 void uart_dma_init(void) { USART_InitTypeDef USART_InitStructure; DMA_InitTypeDef DMA_InitStructure; /* 配置串口为DMA模式使用空闲中断 */ USART_InitStructure.USART_BaudRate 115200; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_Mode USART_Mode_RX | USART_Mode_TX; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_Init(USART1, USART_InitStructure); USART_DMACmd(USART1, USART_DMAReq_RX, ENABLE); /* DMA1 通道5外设到内存 */ DMA_DeInit(DMA1_Channel5); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)USART1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)rx_buf[rx_write_index]; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize RX_BUF_SIZE - rx_write_index; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode DMA_Mode_Normal; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel5, DMA_InitStructure); DMA_ITConfig(DMA1_Channel5, DMA_IT_TC, ENABLE); DMA_Cmd(DMA1_Channel5, ENABLE); /* 串口空闲中断 */ USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); }这里DMA模式用的是DMA_Mode_Normal不是循环模式。原因我在后面说正常模式更适合手动控制偏移。串口空闲中断里核心逻辑是把DMA的当前落点记录到rx_write_index然后重配DMA地址void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_IDLE) ! RESET) { USART_ClearITPendingBit(USART1, USART_IT_IDLE); // 先停DMA读取当前剩余计数算出已收到的字节数 DMA_Cmd(DMA1_Channel5, DISABLE); uint16_t remain DMA_GetCurrDataCounter(DMA1_Channel5); uint16_t received (RX_BUF_SIZE - rx_write_index) - remain; // 更新写指针预留一个字节给下一次空闲判断 rx_write_index (rx_write_index received) % RX_BUF_SIZE; // 重配地址和长度 DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)rx_buf[rx_write_index]; DMA_InitStructure.DMA_BufferSize RX_BUF_SIZE - rx_write_index; DMA_Init(DMA1_Channel5, DMA_InitStructure); DMA_Cmd(DMA1_Channel5, ENABLE); rx_new_data 1; } }这套代码的关键点在于rx_write_index是动态变化的偏移每次从空闲中断退出后DMA的内存地址都指向当前环形缓冲区的实际可写位置。接收再多数据也不会覆盖未处理的数据。3.2 HAL库下的写法与指针类型陷阱使用HAL库时配置思路一样但有一个容易踩的坑HAL库的DMA接收函数对缓冲区指针类型和传输长度有隐藏要求。以HAL_UART_Receive_DMA为例HAL_UART_Receive_DMA(huart1, rx_buf, RX_BUF_SIZE);如果要把起始地址设为rx_buf[write_index]直接写HAL_UART_Receive_DMA(huart1, rx_buf[write_index], RX_BUF_SIZE - write_index);看起来挺正常但HAL库内部会检查Size 0的情况。如果write_index等于RX_BUF_SIZE也就是写指针刚好在缓冲区末尾RX_BUF_SIZE - write_index等于0HAL库会直接返回错误。这种边界情况很容易忽略。另外HAL_UART_Receive_DMA内部会把传入的uint8_t *缓冲区地址直接转给DMA配置。在某些HAL库版本中还会对地址做一次__HAL_DMA_RESET_HANDLE_STATE之类的清理。如果DMA上一次传输还没结束直接调用HAL_UART_Receive_DMA重新配置地址可能导致上一次传输的完成中断和这一次的配置逻辑打架。我的建议是在重配之前先HAL_UART_DMAStop再重新启动。虽然多两行代码但稳定很多。HAL_UART_DMAStop(huart1); HAL_UART_Receive_DMA(huart1, rx_buf[write_index], RX_BUF_SIZE - write_index);还有一个细节HAL_UART_Receive_DMA的缓冲区参数类型是uint8_t *而ADC那块用的是uint32_t *。很多人从串口DMA迁移到ADC多通道DMA时直接把uint16_t数组传进去编译报警告也不管最后跑起来数据全乱。这其实不是DMA的问题是类型宽度和存储字节序的匹配问题。HAL库最终取的只是一块连续内存地址数据宽度取决于DMA的配置而不是C语言指针类型。所以务必保证指针类型和DMA数据宽度一致避免编译器做了奇怪的优化。3.3 GD32上的差异与兼容性GD32的DMA整体结构和STM32很像尤其是GD32F30x、GD32F4xx系列寄存器基本兼容但有几个细节不太一样。第一GD32F30x的DMA通道和外部请求映射关系与STM32F103并不完全一致。虽然代码迁移时绝大部分能直接跑但如果你同时用到多个外设的DMA通道建议对照GD32的参考手册核对一下外设请求映射表。我在一个项目里把STM32F103的代码迁移到GD32F303UART2的DMA通道在STM32上是DMA1_Channel5在GD32上也是DMA1_Channel5看起来一致但DMA请求映射的触发方式略有差异导致偶发丢数据。第二GD32F4xx系列对DMA的模式配置增加了循环模式和普通模式以外的乒乓模式。这里的乒乓模式本质上就是双缓冲自动交替它内部会维护两个内存地址传输完成自动切换。如果你的需求是两段缓冲区交替接收用乒乓模式会比手动改偏移更省心。但要注意乒乓模式下两个缓冲区的地址必须分别配置而且DMA_BufferSize的计数值和偏移量的关系要重新理解。第三GD32E230这类M23内核的MCU也有DMA但通道数量较少地址寄存器的位宽和对齐要求参考Cortex-M23的规格。这种低配MCU上做地址偏移完全没问题但别指望有多强大的FIFO或突发传输支持。4. 循环模式下的回绕陷阱与Cache一致性坑4.1 循环传输的地址回绕为什么偏移在循环模式会坑人我前面特意把DMA模式设为DMA_Mode_Normal是因为循环模式和地址偏移放在一起有一个非常隐蔽的坑。循环模式下DMA传输完一轮后内存地址会自动回绕到初始配置值。如果你初始配置的是buf[8]那每轮回绕点都是buf[8]而不是buf[0]。这意味着buf[0]到buf[7]这8个元素永远轮不到DMA来写它们一直是初始值或者上一轮残留值。这个行为在某些场景下是好事比如你要让DMA始终填充数组中段的一块窗口每轮刷新回绕点正好是窗口起点。但大多数情况下这属于配置偏移时的意外副作用。我在做ADC多通道循环采样时就踩过一次我把DMA内存地址设成了adc_values[2]本意是让前两个元素留给软件写标志位结果发现每次循环启动数据只落在下标2到9之间数组前两个元素永远是零。查了一圈不是DMA没工作而是回绕逻辑就是这样设计的。所以使用循环模式时偏移的意义不仅是从哪里开始第一次搬还决定了每一轮从哪里重新开始。如果业务逻辑是整片数组都被DMA周期性刷新那么初始地址应该就是数组首地址偏移量必须是0。如果业务逻辑是DMA只负责刷新数组中某一段窗口那可以把初始地址设成窗口起点传输长度设成窗口长度回绕逻辑反而成了免费福利。4.2 地址对齐一次HardFault的教训DMA对地址对齐的要求常常被忽视尤其是用指针偏移的时候。STM32F4/F7/H7的DMA控制器在32位传输宽度下源地址和目标地址必须4字节对齐16位传输宽度下必须2字节对齐。8位传输没有对齐要求。举个实际例子。假如你定义了一个uint8_t byte_buf[64]想从下标5开始搬运32字节DMA配置为32位宽度。byte_buf[5]这个地址的十六进制表示如果末两位不是00就满足不了4字节对齐结果是直接进HardFault连错误中断都来不及处理。有一次我就是这样新接手一个旧项目接收缓冲区从uint8_t改成uint16_t但源码里用偏移的地方还保留着_index * 1的逻辑算出来的偏移量是奇数DMA跑起来直接硬件错误。当时的错误号是BUSE查了一圈才发现是地址对齐问题。如果你必须用非对齐地址有两个方案。第一降低DMA数据宽度到8位传输效率会下降一点但功能正常。第二调整缓冲区定义让偏移量等于偶数。对于ADC多通道采样缓冲区用uint16_t数组偏移量按元素个数算天然2字节对齐。如果要32位对齐就把数组定义成uint32_t元素偏移量对应4字节步进。4.3 带Cache的MCU上DMA和Cache的一致性STM32F7、H7这类带D-Cache的MCUDMA直接访问内存CPU读写数据时可能命中Cache两边看到的数据会不一致。这是一个平时不炸、一炸就怪的坑。场景是这样的CPU往tx_buf里写了一帧数据然后启动DMA发送。如果tx_buf的内容还在D-Cache里还没有写回到物理RAMDMA去物理RAM里搬数据大概率搬到的还是旧数据。反过来DMA接收数据到rx_buf后CPU再去读rx_buf如果Cache里留存着同一地址的旧数据CPU读到的就是Cache中的旧值。解决方式是使用CMSIS提供的Cache维护函数// 发送前把Cache里的数据写回RAM SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, len); // 接收后让Cache里的旧数据失效下一次读取直接从RAM加载 SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, len);地址偏移场景下这两个函数的首参数要格外小心。如果你从tx_buf[offset]开始发送SCB_CleanDCache_by_Addr的起始地址也必须从tx_buf[offset]算起长度对应实际传输长度。如果只Clean了tx_buf头部而实际DMA发送的是偏移之后的位置那Cache你还是没刷干净发送出去的内容依然是脏数据。我当时在H743上做双Buffer交替采集DMA地址在两个缓冲区间切换Cache清理函数一开始只做了整片缓冲的Clean。后来改成按实际DMA起始地址和传输长度做精确Clean/Invalidate问题才根除。4.4 实测踩坑记录一次偶发数据错乱的排查再说一个真实的坑它把上面几个因素全串起来了。项目用STM32F407采集三个通道的ADCDMA从adc_values[1]开始搬运因为下标0被一个保护变量占用。运行了一段时间后偶发出现ADC数据整体错乱偶尔还伴随系统跑飞。排查链路大概是这样的先怀疑ADC配置重新校准问题仍然随机出现。用调试器看adc_values数组发现从下标1开始的6个值每轮都正常但下标0的变量偶发被改掉。检查代码里所有对下标0的写操作没有异常。最后把DMA起始地址改回adc_values[0]保护变量移到另一个数组问题消失。原因其实是DMA的内存地址寄存器在某些异常时序下会多搬运一个数据起始地址是adc_values[1]多搬一个就会踩到下标0。DMA从偏移位置启动时如果传输完成中断和下一次ADC触发撞在一起NDTR计数可能和实际搬运数量出现偏差。F407的DMA在特定优先级下这个偏差确实可能发生。虽然概率低但在工业现场就是致命问题。这个经历让我养成一个习惯只要DMA搬运的数据会旁路写一些关键变量尽量不要让保护变量紧挨在DMA缓冲区的旁边或者给缓冲区前后各留一个哨兵空间DMA越界时第一时间能从哨兵的变化里发现。5. 动态偏移、替代方案与多平台延伸5.1 动态修改地址寄存器实现任意起点续传环形缓冲里的偏移常常不是固定值而是运行时动态变化的。这时需要先停DMA、再改地址、最后重新使能三步走。为什么必须先停因为DMA控制器在搬运过程中内部地址寄存器的值由硬件维护你如果直接写入新地址有的芯片会忽略写入有的芯片会在当前传输结束后才生效行为不可预期。三步走的标准代码以STM32标准库为例void dma_set_start_offset(DMA_Channel_TypeDef *ch, uint32_t base_addr, uint32_t offset, uint32_t transfer_len) { DMA_Cmd(ch, DISABLE); // 第一步停DMA while (DMA_GetCmdStatus(ch) ! DISABLE); // 等真正停住 // 第二步改地址和长度 DMA_InitStructure.DMA_MemoryBaseAddr base_addr offset; DMA_InitStructure.DMA_BufferSize transfer_len; DMA_Init(ch, DMA_InitStructure); // 第三步重新使能 DMA_Cmd(ch, ENABLE); }注意transfer_len这里是从偏移位置开始的长度不是整段缓冲区长度。如果偏移发生在环形缓冲的尾部附近还要考虑跨过末尾后回绕的情况。我常用的做法是把环形缓冲拆成两段第一段从当前位置到缓冲区末尾第二段从缓冲区头部到数据结束。DMA一次只能配置一段如果收的数据跨了绕回点就在中断里配置两次DMA。这样灵活但中断里的操作时间要严格控制。5.2 低配MCU上的替代思路没有DMA或者DMA通道不够用不是所有MCU都有足够的DMA通道也不是每个项目都愿意把DMA花在地址偏移这种操作上。低配MCU上的替代方案实际上是在CPU吞吐量和内存占用之间做取舍。最直接的办法是用中断指针偏移。外设每收到一个字节就进一次中断处理函数里把数据写到buf[write_index]写完自动write_index到末尾回0。这种方案的CPU占用高但代码逻辑直白适合数据速率不高的场景。如果数据速率稍高但DMA通道确实不够可以用定时器触发手动搬运的思路。先把外设数据在中断里放进一个小块临时缓冲攒够一定数量后再用memcpy搬到目标位置。这里的memcpy目标地址同样可以是目标数组基地址偏移只是这个偏移由软件维护。另外还有一种思路是双缓冲乒乓操作虽然它不算真正的地址偏移但在很多场景下能替代偏移需求。它让两个DMA缓冲区交替接收一个缓冲区在接收的同时另一个缓冲区可以被CPU安全处理。和动态地址偏移相比乒乓缓冲的缺点是内存占用翻倍优点是不用频繁停启DMA。5.3 从MCU到FPGAAXI DMA的地址描述符带来的启发聊到DMA地址偏移就不能不提Xilinx的AXI DMA。AXI DMA是FPGA里常用的DMA控制器它和MCU的DMA有个本质区别传输参数不是保存在一组寄存器里而是放在内存中的Buffer DescriptorBD里每个BD包含Buffer Address和Buffer Length。这相当于把地址偏移这件事彻底放开了。要搬运内存在base_addr offset处的数据最简单的做法是修改BD里的Buffer Address把offset加进去更新BD的控制位然后交给DMA硬件去执行。如果搬运多个不连续的内存块AXI DMA的Scatter-Gather模式还能通过一个BD链表把各段内存串起来一次性搬运完。这个机制带来的启发是DMA的灵活性上限取决于它的描述能力。MCU的DMA用寄存器描述传输所以偏移必须CPU算好再填进去AXI DMA用BD描述传输天然支持更复杂的内存布局。理解了这一点再看MCU上的地址偏移配置就不会觉得简陋了它只是这种通用机制的一个子集。我的建议是如果你在工作中遇到特别复杂的DMA传输需求比如多段拼接、步进搬运可以跳出MCU DMA的既有思维参考AXI DMA的BD设计思路在软件层自己维护一个传输描述符结构体把地址、偏移、长度、下一段指针都封装起来配合DMA中断逐段执行。这样即使MCU的DMA很傻你的代码也能做出很灵活的传输调度。结尾DMA从地址偏移处启动这个操作代码上往往只有一行但背后牵扯到DMA控制器的工作方式、地址对齐、Cache一致性、循环回绕、传输计数联动等因素。我在调试中最大的体会是不要只看DMA能做什么还要看它的边界在哪里。偏移改变的是数据落点不会改变外设产生数据的节奏偏移同时要求传输长度同步调整否则越界循环模式下偏移还决定了每一轮的上限位置。搞清楚这三条再上手写代码基本不会出大问题。最后分享一个实用的小习惯我每次配置带偏移的DMA后都会在调试器里把内存地址和传输结束后的末端地址打出来看一眼确认没有越界。跑几轮之后再查一下预期偏移位置的数据是否被正确刷新。这个习惯帮我挡掉了不少隐蔽的坑也希望对你有所帮助。
返回列表