STM32 DMA技术详解:从原理到实战,实现高效数据传输与性能优化

发布时间:2026/7/30 6:52:47

STM32 DMA技术详解:从原理到实战,实现高效数据传输与性能优化 1. 项目概述为什么DMA是STM32性能提升的关键如果你正在用STM32做项目尤其是涉及到大量数据传输的场景比如采集传感器数据、驱动屏幕、处理音频或者高速通信那你一定遇到过CPU被数据搬运工作“累趴下”的情况。CPU吭哧吭哧地从外设比如ADC、串口把数据读到内存或者从内存搬到外设比如SPI、DAC宝贵的计算资源都浪费在了这种“搬砖”的体力活上真正需要复杂运算的任务反而被拖慢了。这时候DMADirect Memory Access直接存储器访问就该登场了。它就像一个独立于CPU的“专职搬运工”可以在不打扰CPU的情况下高效地在内存与外设之间、内存与内存之间搬运数据。对于STM32开发者来说掌握DMA不仅仅是学会一个外设更是从“玩具级”应用迈向“工业级”或“高性能”应用的一道分水岭。这篇笔记我就结合自己这些年踩过的坑和积累的经验带你彻底搞懂STM32的DMA从原理到配置从常见应用到疑难杂症手把手让你把DMA用起来、用好。2. DMA核心原理与STM32中的实现架构要理解DMA首先要跳出“CPU中心论”。在传统的编程思维里所有操作都依赖CPU执行指令。但DMA引入了一个新的“执行单元”——DMA控制器。它的工作流程可以类比为你CPU要给朋友外设A寄一箱书数据。没有DMA时你需要自己CPU一本本从书架上内存拿下来打包再跑去邮局外设A寄出全程亲力亲为。有了DMA后你只需要写一张“工作清单”配置DMA通道告诉你的私人助理DMA控制器从书架的这个位置开始取多少本书打包好送到哪个邮局的哪个柜台。然后你就可以去干别的了CPU处理其他任务助理会自动完成所有搬运工作并在完成后通知你一声触发中断。在STM32的体系结构中DMA控制器是一个独立的外设模块。以常见的STM32F1/F4系列为例它们通常有1到2个DMA控制器DMA1 DMA2每个控制器下辖多条“通道”Channel。你可以把通道理解为一条条专用的“传输管道”。每个通道在某一时刻只能服务于一个特定的“传输请求者”这个请求者通常是某个外设如ADC1、USART1_TX、SPI1_RX等。通道的优先级可以配置当多个传输请求同时发生时高优先级的通道会先被服务。DMA传输有三个核心概念源Source、目标Destination和传输计数器Data Number。源和目标可以是外设寄存器地址如USART1-DR或内存地址如一个数组buffer。传输模式主要分两种外设到内存模式典型应用是ADC采集数据存入数组或串口接收数据到缓冲区。内存到外设模式典型应用是从数组发送串口数据或从内存缓冲区传输数据到DAC进行波形输出。内存到内存模式这是DMA2的专属功能在F1系列上可以在两个内存区域间快速拷贝数据效率远高于CPU的memcpy。DMA传输的“粒度”由数据宽度Data Width决定可以是字节8位、半字16位或字32位。传输可以配置为单次传输完指定数量后停止或循环模式Circular Mode后者在缓冲区填满后自动回到开头重新开始是实现“环形缓冲区”Ring Buffer的硬件基础对于连续数据流处理至关重要。注意配置源和目标的数据宽度时必须确保两者一致或者你清楚地知道不一致时硬件的行为某些系列支持宽度不匹配的打包/解包但通常不建议。例如从32位的ADC数据寄存器源传输到16位的内存数组目标如果不做特殊处理可能会丢失高16位数据。3. CubeMX图形化配置DMA的详细步骤与参数解读对于初学者或快速原型开发使用STM32CubeMX进行图形化配置是最高效的方式。它能直观地帮你完成时钟、引脚、外设和DMA的初始化代码生成。下面以STM32F407的ADC1多通道采集并通过DMA存入数组为例详解配置过程。3.1 外设基础配置首先在Pinout Configuration标签页下激活你需要使用的外设。对于本例我们需要在Analog-ADC1中启用IN0、IN1、IN2三个通道假设对应PA0, PA1, PA2。在Parameter Settings中配置ADC为“扫描模式”Scan Conversion Mode: Enabled和“连续转换模式”Continuous Conversion Mode: Enabled。这样ADC会自动按顺序扫描所有启用的通道。设置采样时间Sampling Time根据你的信号频率调整。在DMA Settings选项卡点击Add选择ADC1。此时会自动弹出DMA流/通道的选择对于F4系列是Stream和ChannelF1系列只有Channel。CubeMX通常会帮你选择一个可用的流比如DMA2 Stream0通道Channel 0ADC1对应此通道。3.2 DMA参数深度解析点击添加的DMA行进入详细配置。这里的每一个选项都至关重要Direction:Peripheral To Memory。这定义了数据从ADC数据寄存器流向内存数组。Priority: 可选Low,Medium,High,Very High。如果你的系统中有多个DMA传输在竞争优先级高的会优先得到服务。对于实时性要求高的数据采集可以设为High。Mode:Normal普通模式传输完设定的数据量后DMA传输停止需要软件重新使能才能进行下一次传输。Circular循环模式传输完设定的数据量后DMA自动重置传输计数器并从头开始周而复始。对于连续采集必须选择此模式。Increment Address地址自增Peripheral通常设为Disable。因为ADC的数据寄存器地址是固定的ADC1-DR我们总是从这个固定地址读取数据。Memory必须设为Enable。因为我们希望采集到的数据依次存放到数组的连续元素中内存地址需要自动增加。Data Width数据宽度Peripheral选择Word32位。因为STM32F4的ADC是12位精度数据在数据寄存器中是右对齐或左对齐的16位数据但寄存器本身是32位的。查看数据手册可知ADC_DR寄存器是32位低16位有效。为安全起见通常按外设寄存器宽度配置。Memory也选择Word。这样每次传输搬运32位4字节。如果你的数组定义为uint16_t这里选Half Word16位也是正确的但必须与源宽度匹配或理解其转换规则。Memory Burst和Peripheral Burst通常用于高性能系列如H7用于配置突发传输在F4上一般保持默认Single。3.3 生成代码与关键函数分析配置完成后生成代码。CubeMX会在main.c中生成初始化代码并在stm32f4xx_it.c中生成DMA中断服务函数如果你使能了传输完成中断。你需要手动完成以下几步定义缓冲区在用户代码区定义一个足够大的数组例如uint32_t adc_buffer[BUFFER_SIZE];。BUFFER_SIZE需要是通道数的整数倍因为DMA会按顺序填充所有通道的数据。启动传输在main函数的初始化部分之后while(1)之前调用HAL库函数启动DMA传输。// 启动ADC的DMA采集数据将存入adc_buffer采集 BUFFER_SIZE 个数据后循环模式下会从头开始 HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_buffer, BUFFER_SIZE);处理数据在DMA传输完成中断如果使能或者半传输完成中断中处理数据。更常见的做法是使用循环模式并结合“半传输完成”HT和“传输完成”TC中断来实现“双缓冲”Ping-Pong Buffer机制实现数据处理的零等待。这是高效实时处理的关键技巧我们稍后详细说明。实操心得CubeMX配置DMA时最容易出错的地方是数据宽度和地址自增。务必对照数据手册确认外设数据寄存器的实际有效宽度。一个快速验证方法是先按你认为正确的配置生成代码然后单步调试观察DMA相关寄存器如CNDTR-剩余数据量CMAR-内存地址CPAR-外设地址的值是否符合预期变化。4. 标准外设库与HAL库下DMA的编程模式对比虽然CubeMX和HAL库大大简化了开发但理解底层寄存器操作和标准外设库Standard Peripheral Library, SPL的编程模式能让你在遇到复杂问题或需要极致性能时有更深的掌控力。此外很多遗留项目仍在使用SPL。4.1 标准外设库SPL模式SPL更接近硬件寄存器需要手动配置大量结构体成员。以USART1发送数据到内存为例// 1. 定义DMA初始化结构体 DMA_InitTypeDef DMA_InitStructure; // 2. 开启DMA时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 3. 配置DMA通道参数USART1_TX通常对应DMA1_Channel4 DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)USART1-DR; // 外设地址 DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)tx_buffer; // 内存地址 DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; // 方向内存到外设 DMA_InitStructure.DMA_BufferSize buffer_size; // 传输数据量 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_Channel4, DMA_InitStructure); // 4. 使能DMA通道 DMA_Cmd(DMA1_Channel4, ENABLE); // 5. 使能USART的DMA发送功能 USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);SPL的优势是直接、高效代码量小但可读性稍差且需要开发者对寄存器有较好理解。4.2 HAL库模式HAL库提供了高度抽象和封装的API强调易用性和跨系列兼容性。同样的USART DMA发送// 1. 启动DMA传输假设huart1和hdma_usart1_tx已在CubeMX中初始化 HAL_UART_Transmit_DMA(huart1, tx_buffer, buffer_size); // 2. 传输完成回调函数弱定义需用户重写 void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if(huart-Instance USART1) { // 发送完成处理后续逻辑如点亮LED或准备下一包数据 } }HAL库将底层配置隐藏在MX_DMA_Init()函数中并提供了丰富的回调函数xxxCpltCallback,xxxHalfCpltCallback来处理中断事件。其优点是开发速度快但缺点是代码体积较大执行效率相对SPL略有损耗且其错误处理机制锁机制有时会在复杂中断场景下引入问题。4.3 模式选择建议新手、快速开发、项目复杂度不高强烈推荐HAL库 CubeMX。它能避免大量低级错误让你聚焦业务逻辑。追求极致性能、代码体积、对时序有严苛要求的老手可以考虑SPL甚至直接操作寄存器。存量项目维护遵循项目原有库的选择。 我个人在大多数应用场景下使用HAL库但在对实时性要求极高的核心中断服务函数中有时会混合使用寄存器操作来确保最短延迟。5. 环形缓冲区与双缓冲机制实现连续数据流无丢失处理这是DMA应用中最精华、最体现设计水平的部分。很多初学者配置好DMA后发现数据还是会丢或者处理数据时CPU和DMA会“打架”问题往往出在没有设计好数据缓冲区。5.1 问题场景假设你用ADC以1MHz的速率通过DMA循环模式采集数据缓冲区大小为1000个字。DMA不停地往这个数组里写数据。你的CPU需要读取这些数据并进行滤波等处理。如果你在DMA写入的同时去读取可能会读到“半新半旧”的数据撕裂问题。如果你等DMA写满1000个后再一次性读取那么在读取和处理这1000个数据的期间假设耗时500usDMA又在写入新的数据这500us内新采集的数据就会覆盖掉你尚未读取的旧数据造成丢失。5.2 解决方案双缓冲Ping-Pong Buffer双缓冲利用DMA的“半传输完成”Half Transfer, HT和“传输完成”Transfer Complete, TC中断将一个大缓冲区在逻辑上分成两个等大的“子缓冲区”。你定义一个大小为2*N的缓冲区uint32_t dma_buffer[2*N];。在DMA循环模式下使能HT和TC中断。第一阶段DMA从dma_buffer[0]开始写写满前N个数据后触发HT中断。此时dma_buffer[0]到dma_buffer[N-1]Buffer A是“已满”状态可供CPU安全读取而DMA正在向dma_buffer[N]到dma_buffer[2N-1]Buffer B写入数据。在HT中断服务函数里你通知主程序或任务去处理Buffer A的数据。第二阶段DMA继续写直到写满后N个数据即整个2*N缓冲区触发TC中断。此时Buffer B“已满”可供CPU读取而DMA会自动将传输地址重置回缓冲区开头dma_buffer[0]开始覆盖Buffer A。在TC中断服务函数里你通知主程序去处理Buffer B的数据。如此循环往复CPU和DMA交替使用两个缓冲区实现了“生产者DMA”和“消费者CPU”的完美解耦只要CPU处理一个缓冲区数据的速度快于DMA填满另一个缓冲区的速度数据就永远不会丢失。5.3 代码实现示例基于HAL库#define BUFFER_HALF_SIZE 500 uint32_t adc_dual_buffer[BUFFER_HALF_SIZE * 2]; // 双缓冲 volatile uint8_t buffer_ready 0; // 标志位0:无数据1:半缓冲满2:全缓冲满 // 重写ADC DMA半传输完成回调函数 void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc) { buffer_ready 1; // 标志前半部分缓冲区就绪 } // 重写ADC DMA传输完成回调函数 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { buffer_ready 2; // 标志后半部分缓冲区就绪 } int main(void) { // ... 初始化代码包括启动ADC DMA采集 HAL_ADC_Start_DMA(hadc1, adc_dual_buffer, BUFFER_HALF_SIZE * 2); while(1) { if(buffer_ready 1) { // 处理 adc_dual_buffer[0] 到 adc_dual_buffer[BUFFER_HALF_SIZE-1] process_data(adc_dual_buffer[0], BUFFER_HALF_SIZE); buffer_ready 0; // 处理完清除标志需注意中断重入问题实际可能用更安全的标志 } else if(buffer_ready 2) { // 处理 adc_dual_buffer[BUFFER_HALF_SIZE] 到 adc_dual_buffer[2*BUFFER_HALF_SIZE-1] process_data(adc_dual_buffer[BUFFER_HALF_SIZE], BUFFER_HALF_SIZE); buffer_ready 0; } // ... 执行其他任务 } }重要提示上述代码中的buffer_ready作为在中断和主循环间共享的变量必须考虑临界区保护。在更严谨的工程中应使用原子操作、关中断、或者RTOS的信号量/消息队列来传递缓冲区就绪事件避免竞态条件。6. 常见外设的DMA应用场景与配置要点掌握了核心原理和双缓冲技巧后我们来看看几个最常用外设的DMA应用细节。6.1 ADC多通道扫描 DMA这是DMA最经典的应用。关键在于理解ADC的“扫描序列”如何映射到DMA的“内存地址递增”。配置如前所述使能ADC扫描模式和连续转换模式配置好通道序列和采样时间。数据对齐ADC结果可以左对齐或右对齐。右对齐时12位数据存放在寄存器的低12位左对齐时存放在高12位。DMA传输的数据宽度需要与此匹配否则会取错数据位。缓冲区大小如果你的缓冲区大小N不是通道数M的整数倍那么DMA写入的数据序列会错位。例如3个通道缓冲区大小为100。DMA写入顺序是Ch1, Ch2, Ch3, Ch1, Ch2, Ch3...。当你读取时必须知道当前下标对应哪个通道的数据。通常建议设置N M * KK为整数这样每次读取K个完整的扫描周期数据处理起来更规整。6.2 UART串口收发 DMA串口DMA能极大解放CPU特别是在高速或不定长数据通信时。发送UART TX DMA配置为内存到外设模式。将待发送数据的数组地址和长度配置给DMA启动传输即可。CPU可以立即去干别的DMA会“悄无声息”地发完所有数据。注意在发送完成中断TC触发前不要修改发送缓冲区的内容否则可能发送出错误数据。接收UART RX DMA配置为外设到内存模式并使用循环模式。这是实现不定长数据接收的“神器”。你只需要开辟一个足够大的环形缓冲区比如256字节并启动DMA循环接收。数据会源源不断地写入缓冲区并自动覆盖旧数据。你需要额外维护一个软件读指针。当需要读取数据时根据DMA当前写入位置通过__HAL_DMA_GET_COUNTER宏获取剩余传输计数可以反算出写指针位置和读指针计算出未读数据的长度和位置进行处理。空闲中断IDLE配合DMA接收这是更高级的用法。使能串口的空闲线路中断IDLE Line Interrupt。当DMA在循环接收数据时如果总线上一段时间没有新数据产生IDLE中断就意味着一帧数据结束了。在IDLE中断服务函数中你可以根据DMA的写指针位置精确地提取出刚刚接收到的完整一帧数据效率极高。6.3 SPI/I2S音频传输 DMA驱动音频编解码器或数字麦克风时需要持续不断的音频数据流。DMA是必选项。双缓冲是标配音频数据对实时性要求极高必须使用双缓冲机制。一个缓冲区正在通过DMA发送给I2S外设播放另一个缓冲区由CPU或解码器填充下一段音频数据。数据宽度与对齐I2S通常是16位或24位数据。注意STM32的I2S外设数据寄存器可能是16位或32位需要根据数据手册和音频设备的要求仔细配置DMA的数据宽度和传输模式。24位数据有时需要打包成32位进行传输。主从模式与时钟确保SPI/I2S的主从模式、时钟极性和相位配置正确否则DMA传输的数据无法被正确识别。7. DMA使用中的高级话题与疑难杂症排查当你开始深入使用DMA一定会遇到一些棘手的问题。这里分享几个典型的“坑”和排查思路。7.1 内存对齐问题Alignment Fault这是一个隐蔽且常见的崩溃原因。Cortex-M内核尤其是M3/M4/M7对内存访问有对齐要求。例如访问一个32位4字节数据其地址必须是4的倍数。如果你定义了一个uint8_t数组然后将其地址强制转换为uint32_t*并配置给DMA数据宽度为Word而该数组的起始地址不是4字节对齐的那么当DMA试图以32位方式访问时就会触发硬件错误HardFault。解决方案使用编译器指令来保证缓冲区的对齐。例如在GCC或ARM Compiler中// 定义一个32位对齐的缓冲区 __attribute__((aligned(4))) uint8_t buffer[1024]; // 或者 uint32_t buffer[256]; // 本身就是4字节对齐的7.2 缓存一致性问题Cache Coherency这个问题在带有数据缓存D-Cache的高性能MCU如STM32H7系列中极为突出。CPU和DMA共享同一片内存SRAM。CPU读写数据时会经过缓存而DMA直接访问物理内存绕过缓存。这就可能导致CPU写DMA读CPU修改了缓冲区数据写在了Cache里但未写回内存Write-Back。DMA读走的是内存中的旧数据。DMA写CPU读DMA将新数据写入了内存但CPU的Cache中还是旧数据导致CPU读到的是旧数据。解决方案手动维护缓存在DMA传输开始前或结束后使用SCB_CleanDCache_by_Addr清理将Cache数据写回内存或SCB_InvalidateDCache_by_Addr无效化丢弃Cache数据从内存重新加载函数来维护缓存一致性。这是最常用和灵活的方法。使用非缓存内存区域将DMA缓冲区定义在SRAM中不被缓存Non-Cacheable的区域如D3域的SRAM或通过MPU配置。这牺牲了性能换来了简单性。使用带缓存维护的DMA API一些HAL库的DMA驱动特别是H7系列在内部已经处理了部分缓存维护但并非所有情况都覆盖需要仔细阅读手册。7.3 DMA中断与CPU任务竞争在双缓冲应用中中断服务函数ISR中设置标志位主循环中处理数据。如果数据处理非常耗时可能会发生DMA已经写满了下一个缓冲区并再次触发中断但主循环还没处理完上一个缓冲区。这会导致缓冲区被覆盖数据丢失。排查与解决优化处理算法减少单次数据处理时间。增大缓冲区给CPU更长的处理时间窗口。使用RTOS将数据处理放在一个独立的高优先级任务中并使用消息队列或信号量从ISR快速传递事件让任务调度器来管理执行顺序。使用三缓冲甚至多缓冲提供更多的缓冲余地但这会增加内存和逻辑复杂度。7.4 DMA传输未启动或中途停止检查列表时钟DMA控制器在AHB总线上的时钟是否使能__HAL_RCC_DMA1_CLK_ENABLE()或__HAL_RCC_DMA2_CLK_ENABLE()。外设DMA请求是否使能了外设的DMA请求例如对于UART发送除了配置DMA还需要调用__HAL_UART_ENABLE_IT(huart1, UART_IT_DMATX)HAL库通常在HAL_UART_Transmit_DMA内部处理了。NVIC中断如果使用了中断NVIC是否配置并使能传输计数器CNDTR在启动传输前这个寄存器必须被写入一个大于0的值。HAL库的启动函数会做这件事。如果传输中途停止可以检查这个寄存器是否变成了0。外设状态确保外设本身已正确初始化并处于就绪状态如UART已使能ADC已校准并启动。7.5 内存到内存传输的陷阱内存到内存传输在某些系列上如F1是DMA2的专属功能。使用时需注意它没有外设端的“流控制”传输速度极快会占用大量总线带宽可能影响其他核心外设的访问。源和目标的地址不能指向同一块内存区域的重叠部分结果将是未定义的。如果需要移动重叠区域的数据应使用CPU的memmove函数。8. 实战构建一个基于DMA的实时数据采集与处理系统框架最后我们整合以上所有知识点设计一个简单的、可扩展的实时数据采集系统框架。该系统使用ADC多通道扫描、DMA双缓冲、实时处理并考虑在RTOS环境下的应用。8.1 系统设计目标ADC以固定频率如10kHz采集4个通道的信号。使用DMA循环模式双缓冲确保数据无丢失。在主循环或RTOS任务中对采集到的数据进行实时滤波如移动平均和计算如有效值。处理后的数据可通过串口DMA发送至上位机。8.2 关键组件定义// 定义双缓冲大小每个缓冲区容纳10个完整的ADC扫描周期4通道*1040个数据 #define ADC_CHANNELS 4 #define SCAN_CYCLES_PER_BUFFER 10 #define ADC_BUFFER_HALF_SIZE (ADC_CHANNELS * SCAN_CYCLES_PER_BUFFER) // 40 // 双缓冲区和状态标志 __attribute__((aligned(4))) uint16_t adc_dma_buffer[ADC_BUFFER_HALF_SIZE * 2]; volatile uint8_t adc_buffer_index 0; // 0: 前半部分就绪 1: 后半部分就绪 // 处理后的数据缓冲区 float processed_data[ADC_CHANNELS];8.3 DMA与ADC配置CubeMXHALADC配置扫描模式、连续转换模式、使能DMA连续请求、配置好4个通道的采样顺序和时间。DMA配置外设到内存、循环模式、高优先级、外设地址不自增固定为ADC-DR、内存地址自增、数据宽度均为半字16位对应ADC 12位数据右对齐。使能DMA的HT和TC中断。8.4 中断回调与数据处理任务// ADC DMA半传输完成回调 void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc) { adc_buffer_index 0; // 标志前半缓冲区就绪 // 如果使用RTOS这里可以释放一个信号量或发送消息给处理任务 osSemaphoreRelease(adcDataReadySemaphore); // 示例 } // ADC DMA传输完成回调 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { adc_buffer_index 1; // 标志后半缓冲区就绪 osSemaphoreRelease(adcDataReadySemaphore); // 示例 } // 数据处理任务函数 (RTOS环境示例) void adc_data_process_task(void const *argument) { uint16_t *buffer_to_process; while(1) { // 等待信号量表示有缓冲区就绪 osSemaphoreWait(adcDataReadySemaphore, osWaitForever); // 根据标志确定处理哪个缓冲区 if(adc_buffer_index 0) { buffer_to_process adc_dma_buffer[0]; } else { buffer_to_process adc_dma_buffer[ADC_BUFFER_HALF_SIZE]; } // 处理一个完整缓冲区的数据40个数据即10组扫描 for(int cycle 0; cycle SCAN_CYCLES_PER_BUFFER; cycle) { for(int ch 0; ch ADC_CHANNELS; ch) { uint16_t raw_value buffer_to_process[cycle * ADC_CHANNELS ch]; // 这里进行你的数据处理例如累加用于后续平均 // sum[ch] raw_value; } } // 完成10组扫描后计算各通道的平均值 for(int ch 0; ch ADC_CHANNELS; ch) { processed_data[ch] (float)sum[ch] / SCAN_CYCLES_PER_BUFFER * 3.3f / 4095.0f; // 转换为电压 sum[ch] 0; // 清零累加器 } // 可以在这里将 processed_data 通过串口DMA发送出去 // HAL_UART_Transmit_DMA(huart1, (uint8_t*)processed_data, sizeof(processed_data)); } }8.5 系统启动在main函数中初始化所有外设和RTOS后启动ADC的DMA采集并创建数据处理任务。// 启动ADC DMA循环采集 HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_dma_buffer, ADC_BUFFER_HALF_SIZE * 2); // 创建信号量和任务 adcDataReadySemaphore osSemaphoreNew(1, 0, NULL); osThreadNew(adc_data_process_task, NULL, thread_attr);这个框架清晰地分离了数据采集DMA中断驱动和数据处理任务通过双缓冲和信号量机制实现了安全、高效的数据流。你可以在此基础上增加更复杂的算法、通信协议或用户接口。DMA的掌握最终是为了让你能设计出这样优雅、高效的系统让CPU专注于它该做的“思考”工作而不是“搬运”工作。

相关新闻