尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

STM32 ADC与DMA高效数据采集:从原理到双缓冲实战

STM32 ADC与DMA高效数据采集:从原理到双缓冲实战 1. 项目概述与核心价值最近在做一个基于STM32的环境监测设备核心需求是同时采集多个传感器的模拟信号比如温度、光照和电池电压。最开始我用的是HAL库里最基础的HAL_ADC_Start和HAL_ADC_PollForConversion轮询方式代码写起来是简单但实际一跑起来问题就暴露了。主循环被ADC采样严重阻塞导致整个系统的响应变得非常迟钝更别提还想在后台做些数据处理或者通信了。这显然不行对于需要实时或多任务处理的嵌入式项目高效的ADC数据采集是基本功。于是很自然地就想到了ADC的DMA传输模式。这个组合堪称STM32数据采集的“黄金搭档”。ADC负责专心致志地把模拟信号转换成数字量而DMA直接存储器访问则像一个不知疲倦的搬运工自动把转换好的数据从ADC的数据寄存器“搬”到我们指定的内存数组中。整个过程完全不需要CPU的干预CPU被彻底解放出来可以去执行其他更重要的任务比如运行控制算法、处理用户界面或者通过串口发送数据。这种“采集-搬运”分离的设计极大地提升了系统的整体效率和实时性。无论是采集单个传感器信号还是需要轮询采集多个通道ADCDMA的方案都能优雅地解决。对于单通道你可以设置DMA进行连续搬运实现一个“数据流”对于多通道你可以利用ADC的扫描模式配合DMA一次触发就能按顺序自动采集所有通道数据在内存中整齐排列。这次我就结合实际的STM32CubeMX配置和HAL库代码把单通道连续采集和多通道扫描采集这两种最常用的模式从原理到代码再到调试中踩过的坑给大家彻底讲清楚。2. 核心硬件与原理剖析2.1 ADC与DMA协同工作机制要玩转ADCDMA首先得在脑子里把它们的协作流程图画清楚。我们以多通道扫描采集为例其核心工作流程是这样的触发启动程序通过软件调用HAL_ADC_Start_DMA()或者配置一个定时器触发ADC。这是整个采集过程的“发令枪”。ADC采样与转换ADC收到启动信号后根据预设的序列规则通道组依次对每个通道进行采样和AD转换。这个阶段ADC外设独自忙碌。DMA传输请求每当ADC完成一个通道的数据转换并将结果存入ADC_DR数据寄存器时它就会向DMA控制器发出一个传输请求。DMA自动搬运DMA控制器响应请求在不通知CPU的情况下将ADC_DR中的数值按照我们预先配置的“目的地址”通常是一个数组、“数据宽度”比如半字和“传输模式”搬运到内存中。循环与完成对于多通道扫描ADC会周而复始地进行“采样-转换-触发DMA”这个过程。DMA则持续搬运当搬运的数据量达到我们预设的数目比如扫描一轮所有通道的数据时DMA会产生一个传输完成中断如果使能了的话通知CPU“这一批数据准备好了”。这里的关键在于“双缓冲”或“乒乓缓冲”思想。我们通常会定义两个大小相同的数组adc_buffer_a和adc_buffer_b。DMA正在往数组A里写数据时CPU可以安全地读取和处理数组B里的上一批数据。当DMA写满数组A并触发中断时我们在中断服务程序里快速切换DMA的目的地址到数组B同时CPU开始处理数组A。如此循环实现了数据采集和处理的完全并行几乎没有数据丢失的风险。2.2 关键参数配置深度解析在STM32CubeMX中配置ADCDMA有几个参数的理解至关重要配置错了轻则数据不对重则程序卡死。ADC侧关键参数Scan Conversion Mode扫描模式多通道采集必须启用Enable。启用后ADC会按照Rank次序中配置的通道顺序自动进行转换。Continuous Conversion Mode连续转换模式如果希望ADC一旦启动就不停地循环采集则启用。如果希望每次都由软件或硬件触发一次采集序列则禁用。通常与DMA搭配时如果使用定时器触发这里禁用如果希望上电后自动连续采集这里启用。DMA Continuous RequestsDMA连续请求这个参数非常关键当使能DMA后建议将此参数设为Enable。它的作用是保证在DMA传输期间即使ADC转换被短暂暂停例如由于高优先级中断DMA请求也能被保持确保数据流的连续性。如果禁用可能在复杂中断环境下丢失DMA请求导致数据缺失。End Of Conversion Selection转换结束选择选择EOC转换结束标志在何时置位。对于多通道扫描DMA通常选择“EOC after each conversion”每次转换后这样每个通道转换完都会触发一次DMA请求。如果选择“EOC after sequence”序列结束后则必须配合DMA的“单次”模式并在整个序列转换完成后才触发一次DMA搬运多个数据配置更复杂。Rank次序这里决定了通道的采样顺序。每个Rank需要指定ChannelADC输入通道如IN0,IN1和Sampling Time采样时间。采样时间需要根据信号源阻抗来调整阻抗越大需要的采样时间越长否则采样值会不准。通常对于传感器设置Cycles为239.5或更高可以获得更稳定的结果。DMA侧关键参数Mode模式分为Normal正常和Circular循环两种。Circular模式这是最常用的模式。DMA在传输完指定数据量后会自动重置传输计数器并从头开始循环传输形成一个连续不断的数据流。非常适合持续数据采集。Normal模式DMA只传输一次指定数据量然后停止需要软件重新启动。适用于需要精确控制采集批次的场景。Data Width数据宽度需要匹配ADC的数据分辨率如12位和内存的数据类型。通常Peripheral外设即ADC侧和Memory内存侧都选择Half Word半字16位因为STM32的ADC数据寄存器是16位对齐的12位数据存放在低12位。Increment Address地址自增Peripheral侧必须设为Disable因为ADC的数据寄存器地址是固定的。Memory侧必须设为Enable这样DMA每次搬运后目的地址会自动递增数据才能依次存放到数组的各个元素中。注意一个常见的误区是DMA传输次数和ADC通道数的关系。DMA的Data Number传输数据量配置的是“总共要搬运多少个数据单元”。对于多通道扫描如果你希望DMA在ADC每扫描完一轮所有通道后就把这一轮的所有通道数据都搬走那么Data Number就应该等于通道数。如果你定义了更大的缓冲区比如每个通道存10次扫描的结果那么Data Number就等于通道数 * 扫描轮数。3. 单通道ADC连续采集DMA实现单通道连续采集是最基础的场景目标是让ADC不停地采集某一个引脚上的电压并通过DMA实时更新到一个变量或数组中。3.1 CubeMX配置步骤选择ADC通道在Analog-ADC1下为你需要使用的引脚例如PA0对应IN0选择IN0通道。ADC参数配置在Parameter Settings选项卡因为只有一个通道所以Scan Conversion Mode可以保持Disable。Continuous Conversion Mode设为Enable实现连续采集。DMA Continuous Requests设为Enable。在Rank里确保你的通道被添加进来并设置合适的采样时间。DMA配置在DMA Settings点击Add选择ADC1。Mode选择Circular循环模式实现持续搬运。Data Width都选Half Word。Increment Address外设侧Disable内存侧Enable。Data Number这里设置为1。因为我们只需要一个变量来存放最新的ADC值。如果你想做一个缓冲区比如用于滤波可以设置为缓冲区大小。3.2 代码实现与解析在main.c的/* USER CODE BEGIN PV */区域定义变量volatile uint16_t adc_single_value 0; // volatile防止编译器优化在main()函数的初始化部分/* USER CODE BEGIN 2 */启动ADC和DMA// 启动ADC并通过DMA将数据传送到 adc_single_value 传输长度为1 if (HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_single_value, 1) ! HAL_OK) { Error_Handler(); }之后adc_single_value这个变量就会被DMA自动、持续地更新为PA0引脚上的最新ADC转换值。你在主循环中可以直接读取它完全不需要等待。实操心得单通道模式Scan Mode禁用时Rank列表里只有一个通道是有效的。即使你添加了多个RankADC也只会转换第一个。对于单通道使用DMA看起来有点“杀鸡用牛刀”但在需要极高采样率或主循环非常繁忙的系统中它仍然是解放CPU的最佳选择。你可以把Data Number设为更大的值比如100创建一个缓冲区然后在DMA传输完成中断里处理这批历史数据实现简单的数字滤波如均值滤波。4. 多通道ADC扫描采集DMA实现多通道扫描是更实用的场景。假设我们需要按顺序采集PA0通道0、PA1通道1、PA2通道2三个传感器的值。4.1 CubeMX配置步骤选择ADC通道在Analog-ADC1下为PA0、PA1、PA2分别选择IN0、IN1、IN2。ADC参数配置Scan Conversion Mode必须设为Enable。Continuous Conversion Mode根据需求选择。如果希望定时触发如用定时器这里选Disable如果希望上电后自动连续扫描选Enable。本例以连续扫描为例。DMA Continuous RequestsEnable。End Of Conversion Selection选择EOC after each conversion。Rank配置这是关键步骤。在Rank选项卡你会看到Rank1、Rank2、Rank3...。你需要手动将三个通道添加进来并调整它们的顺序。例如Rank1:Channel 0(PA0), Sampling time 239.5 CyclesRank2:Channel 1(PA1), Sampling time 239.5 CyclesRank3:Channel 2(PA2), Sampling time 239.5 Cycles这个顺序决定了ADC扫描转换的顺序。DMA配置添加ADC1的DMA流。Mode:Circular。Data Width:Half Word。Increment Address: PeripheralDisable, MemoryEnable。Data Number: 这里设置为3。因为我们有三个通道DMA每搬运3个数据一轮扫描的结果后在循环模式下会回到数组开头继续搬运。4.2 代码实现与双缓冲技巧在main.c中定义缓冲区#define ADC_CHANNEL_NUM 3 volatile uint16_t adc_buffer[ADC_CHANNEL_NUM]; // 用于存放一轮扫描的三个值在main()初始化部分启动if (HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_buffer, ADC_CHANNEL_NUM) ! HAL_OK) { Error_Handler(); }启动后adc_buffer[0]、adc_buffer[1]、adc_buffer[2]将分别自动对应PA0、PA1、PA2的实时ADC值。高级技巧实现双缓冲 为了实现采集与处理的无缝衔接避免处理数据时缓冲区被覆盖我们可以使用双缓冲。#define BUFFER_SIZE (ADC_CHANNEL_NUM * 10) // 假设每个通道缓存10次扫描 volatile uint16_t dma_buffer[2][BUFFER_SIZE]; // 双缓冲 volatile uint8_t current_buffer 0; // 当前DMA正在写入的缓冲区索引 volatile uint8_t buffer_ready 0; // 缓冲区就绪标志 uint16_t processing_buffer[BUFFER_SIZE]; // 用于CPU处理的缓冲区在DMA传输完成中断回调函数HAL_ADC_ConvCpltCallback中需在main.c中重写void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { // 切换DMA目标到另一个缓冲区 current_buffer 1 - current_buffer; HAL_ADC_Stop_DMA(hadc); // 先停止在Circular模式下重新启动前需停止 // 重新启动DMA指向新的缓冲区 if (HAL_ADC_Start_DMA(hadc, (uint32_t*)dma_buffer[current_buffer], BUFFER_SIZE) ! HAL_OK) { Error_Handler(); } // 设置标志通知主循环另一个缓冲区已满可以处理 buffer_ready 1; }在主循环中if(buffer_ready) { buffer_ready 0; uint8_t buffer_to_process 1 - current_buffer; // 处理非当前写入的缓冲区 // 复制数据到处理缓冲区为了快速退出中断不在中断内处理复杂数据 memcpy(processing_buffer, (void*)dma_buffer[buffer_to_process], BUFFER_SIZE * sizeof(uint16_t)); // 现在可以安全地处理 processing_buffer 中的数据了 // ... 你的数据处理代码 ... }这种方法确保了数据处理的完整性是工业级采集程序的常用手法。5. 常见问题排查与实战调试心得即使配置看起来正确实际调试中还是会遇到各种问题。下面是我总结的一些典型问题和解决方法。5.1 数据错位或全为0症状多通道采集时数组里的数据顺序不对或者全部是0。排查检查DMA内存地址自增确保Memory Increment是Enable的。如果禁用所有通道的数据都会被DMA搬运到同一个内存地址数组的第一个元素导致数据被覆盖。检查缓冲区大小与DMA传输数量确保HAL_ADC_Start_DMA函数调用中的Length参数即DMA的Data Number等于或大于你实际需要搬运的数据单元个数。对于简单的多通道扫描它应等于通道数。检查数组定义和链接确保用于存储ADC值的数组是全局变量或静态变量且没有被编译器优化掉。使用volatile关键字修饰是很好的实践。检查ADC的Rank顺序在CubeMX中仔细核对Rank列表里的Channel顺序它必须与你期望的数据在数组中的排列顺序一致。5.2 DMA传输不启动或只传输一次症状调用HAL_ADC_Start_DMA后数组数据不更新或者只更新一次后就停止了。排查检查DMA模式如果你需要持续传输Mode必须设置为Circular循环。如果设成了Normal正常DMA在传输完指定次数后就会停止。检查ADC连续转换模式如果Continuous Conversion Mode是Disable那么ADC在完成一轮扫描后就会停止需要等待下一次触发。如果你希望ADC自动连续运行此处需Enable。检查NVIC中断优先级虽然DMA本身不占用CPU但如果系统中存在更高优先级的中断长时间阻塞可能会影响DMA请求的响应。确保DMA和ADC的中断优先级设置合理通常不需要设置得特别高。验证时钟配置确保ADC和DMA所在的总线时钟如APB2已正确使能。时钟是外设工作的基础。5.3 ADC采样值不稳定、跳动大症状即使输入电压稳定ADC读数值也在最后几位不停跳动。排查与解决增加采样时间这是最有效的方法之一。在CubeMX的Rank配置里增大Sampling Time。对于高阻抗信号源如电位器、某些传感器需要更长的采样时间让内部采样保持电容充放电稳定。可以尝试设置为239.5 Cycles或480.5 Cycles。硬件滤波在ADC输入引脚对地添加一个0.1uF的陶瓷电容可以滤除高频噪声。对于低频噪声可以考虑加入RC低通滤波电路。软件滤波在软件中对连续采集的多个值进行滤波。最简单的就是均值滤波连续采样N次然后取平均值。更高级的可以用中值滤波去除脉冲噪声或一阶低通数字滤波。检查电源和参考电压ADC的精度极度依赖参考电压的稳定性。确保VDDA和VSSA模拟电源干净、稳定。如果芯片有独立的VREF和VREF-引脚请确保它们连接了高质量、低噪声的参考源。在PCB布局上模拟部分和数字部分的电源最好用磁珠或0欧电阻隔离。5.4 使用定时器触发ADC采集在很多应用里我们需要精确的采样间隔比如每秒采样1000次。这时就需要用到定时器触发。CubeMX配置配置一个定时器如TIM2在Parameter Settings中设置Prescaler和Counter Period使其产生所需频率的更新事件Update Event。例如72MHz主频分频后1MHz计数计数器周期设为999即可产生1kHz的触发频率。在ADC的Trigger配置中选择Timer 2 Trigger Out event作为External Trigger Conversion Source。将ADC的Continuous Conversion Mode设为Disable因为我们将由定时器来周期性触发转换。代码修改启动定时器HAL_TIM_Base_Start(htim2);启动ADC的DMA传输HAL_ADC_Start_DMA(hadc1, ...)注意此时ADC处于等待触发状态。这样定时器每次溢出更新就会自动触发ADC进行一次扫描转换并由DMA搬运数据。采样间隔由定时器精确控制。调试心得在调试DMA传输时善用断点和变量实时查看窗口Live Watch比串口打印更高效。你可以观察ADC缓冲区数组的值是否在持续、有规律地变化。另外STM32CubeIDE的System Viewer窗口可以实时监控外设寄存器的状态对于确认ADC是否在转换、DMA传输计数器是否在递减非常有帮助。最后关于HAL库的__weak回调函数比如HAL_ADC_ConvCpltCallback你需要在main.c的用户代码区重新实现它而不是修改stm32xx_hal_adc.c文件。这是HAL库框架的一个特点提供了灵活的接口供用户覆盖。
返回列表