
1. 项目缘起为什么是STM32G431与DMA方式的ADC在嵌入式开发里采集模拟信号是再基础不过的操作。但就是这个基础操作选型和方法的不同带来的开发体验和最终性能天差地别。我最近在一个需要同时监控多路传感器电压的项目里再次用到了STM32G431的ADC并且选择了DMA方式进行数据搬运。之所以这么选背后有几个很实际的考量。首先STM32G431属于STM32G4系列这个系列主打混合信号处理和高性能。它的ADC模数转换器性能相当不错最高能达到4Msps的采样率并且内置了硬件过采样、可编程增益放大器等高级功能对于需要高精度或多通道快速采样的场景非常友好。其次DMA直接存储器访问方式几乎是多通道、连续采集场景下的“标配”。想象一下如果你的主循环里每隔一段时间就要去读取好几路ADC的值CPU会被频繁打断去干“搬运工”的活效率低下不说还可能因为中断响应不及时丢失数据。DMA就是为了解放CPU而生的它能在ADC转换完成后自动把数据从ADC数据寄存器搬到我们指定的内存数组里整个过程无需CPU干预。所以这个标题“STM32G431单片机多通道采集ADC电压值DMA方式”指向的是一个非常经典且实用的嵌入式开发场景如何高效、可靠地利用单片机硬件资源实现多路模拟量的同步或快速轮询采集。这不仅仅是调通一个功能更是对单片机外设理解、系统资源分配和编程思想的一次实践。2. 硬件与工程环境搭建动手之前得先把“战场”布置好。这里的环境搭建不仅仅是打开IDE更包括对硬件资源的规划和理解。2.1 核心器件与引脚分配我手头的开发板是STM32G431RB Nucleo板它核心是一颗STM32G431RBT6。我们需要关注的是它的ADC外设。STM32G431包含3个ADCADC1 ADC2 ADC3它们可以独立工作也可以组成双重或三重模式提高采样率。对于多通道采集我们通常使用一个ADC在其多个通道间进行扫描。假设我们要采集4路电压信号分别来自板载的可变电阻器电位器或外部传感器。在STM32G431上ADC1的通道5、6、7、8分别对应引脚PA0、PA1、PA2、PA3。这几个引脚都支持5V容忍并且位于同一个GPIO端口GPIOA配置起来比较方便。注意STM32的ADC通道编号与引脚名称的对应关系需要查数据手册Datasheet或参考手册Reference Manual的引脚定义表不同型号、不同封装可能不同切忌想当然。在CubeMX或直接编程时我们需要将PA0、PA1、PA2、PA3配置为模拟输入模式Analog Mode。这是最关键的一步如果配置成其他功能如GPIO输出ADC将无法正确读取该引脚电压。使能ADC1的时钟。配置ADC1的工作参数。2.2 开发工具链选择我使用的是STM32CubeIDE。它集成了STM32CubeMX图形化配置工具和基于Eclipse的IDE环境对于快速配置STM32复杂的外设非常高效。当然你也可以用Keil MDK、IAR等传统工具原理是相通的。使用CubeIDE的好处是我们可以先用CubeMX进行可视化配置生成初始化代码框架然后再在生成的代码基础上添加业务逻辑。这能避免很多底层寄存器配置的繁琐和错误。首先在CubeMX中新建一个针对STM32G431RB的项目。在Pinout Configuration视图下找到Analog类别下的ADC1。在ADC1的设置中将IN5、IN6、IN7、IN8对应PA0-PA3的Mode设置为“Single-ended”单端输入最常用。然后进入Parameter Settings标签页这里才是配置ADC和DMA的核心。2.3 ADC关键参数配置详解ADC的配置参数很多每一个都影响着采样行为和结果。以下是针对多通道DMA采集的关键配置及其含义Clock Prescaler时钟分频ADC的工作时钟ADCCLK由系统时钟分频而来。STM32G431的ADC时钟最高可达60MHz。我们需要根据Resolution分辨率和Sampling Time采样时间来计算总转换时间确保ADCCLK不会超频。一个安全的做法是在项目初期先选择一个较大的分频比如PCLK2 divided by 8等所有功能稳定后再尝试优化提速。Resolution分辨率选择12 bits。这是ADC的转换精度12位对应0-4095的数值范围。分辨率越高对微小电压变化的识别能力越强。Data Alignment数据对齐选择Right alignment右对齐。这样转换完成的12位数据会存放在16位数据寄存器的低12位我们读取和处理起来更直观。Scan Conversion Mode扫描转换模式必须启用Enabled。这是多通道采集的灵魂。启用后ADC会按照我们配置的Rank序列顺序自动对多个通道进行转换。Continuous Conversion Mode连续转换模式这里我们选择Disabled禁用。因为我们希望由软件或定时器来触发一组转换而不是让ADC永不停歇地转换。触发一次它就完成一轮对所有选定通道的扫描。Discontinuous Conversion Mode间断转换模式禁用。这个模式用于将一次扫描分组我们不需要。DMA Continuous RequestsDMA连续请求必须启用Enabled。这个选项允许DMA在一次ADC扫描转换多个通道完成后自动请求DMA进行下一次数据传输。如果不启用DMA只搬运一次数据就停止了。End Of Conversion Selection转换结束选择选择EOC after each conversion每次转换后产生EOC信号。这样每个通道转换完成都会有一个标志方便DMA搬运单个通道的数据。如果选择EOC after sequence则是一轮扫描完成后才产生一个标志DMA需要配置成搬运多个数据数组我们采用前者更直观。Low Power Auto Wait低功耗自动等待禁用。Overrun behaviour溢出行为选择Overrun data overwritten覆盖溢出数据。当DMA来不及搬运数据而新的转换数据已经产生时新数据会覆盖旧数据。在调试阶段也可以选择Overrun data preserved并配合中断来检测溢出错误。Conversion Data Management转换数据管理选择DMA Circular ModeDMA循环模式。这是实现连续自动采集的关键ADC转换的数据会通过DMA循环地写入到我们指定的内存数组中形成一个数据缓冲区。接下来是通道配置。在ADC1的Configuration标签下点击Add将IN5IN6IN7IN8依次加入。 对于每个通道需要设置两个关键参数Rank序列号决定通道的转换顺序从1开始编号。我们按顺序设置为1234。Sampling Time采样时间这是ADC内部采样保持电容对输入电压进行充电的时间。时间太短采样不准确时间太长影响转换速度。对于一般阻抗的信号源92.5 cycles或247.5 cycles是一个比较稳妥的起点。如果信号源阻抗较大比如直接接了一个大电阻则需要更长的采样时间。2.4 DMA配置在CubeMX的Analog-ADC1配置页找到DMA Settings点击Add。DMA Request选择ADC1。Mode选择Circular循环模式与ADC配置中的DMA Circular Mode对应。Increment AddressPeripheral外设地址选择Disable因为ADC数据寄存器地址是固定的Memory内存地址选择Enable因为我们要把数据依次存放到数组的不同位置。Data WidthPeripheral和Memory都选择Word字32位。虽然ADC数据是12位但寄存器是32位的我们按字访问更高效。在内存中我们会定义一个uint32_t的数组来接收。配置完成后在Project Manager中设置好工程名、路径、Toolchain/IDE然后生成代码。CubeMX会为我们生成ADC和DMA的初始化代码MX_ADC1_Init()和MX_DMA_Init()以及GPIO的初始化。3. 软件逻辑与代码实现剖析生成了初始化代码只是搭好了舞台演员数据如何上场、下场需要我们来编写剧本。3.1 定义数据缓冲区与全局变量在main.c的顶部用户代码区/* USER CODE BEGIN PV */定义我们需要的变量。/* USER CODE BEGIN PV */ #define ADC_BUFFER_SIZE 4 // 对应4个通道 volatile uint32_t adcBuffer[ADC_BUFFER_SIZE]; // DMA搬运的目标数组 volatile uint8_t adcConversionComplete 0; // 转换完成标志位 /* USER CODE END PV */这里有几个细节volatile关键字至关重要。它告诉编译器这个变量可能被程序之外的实体改变比如DMA控制器禁止编译器对它进行优化例如将变量值缓存到寄存器导致读不到DMA写入的新值。数组大小ADC_BUFFER_SIZE设为4意味着DMA会循环地将通道1到4的数据依次填入adcBuffer[0]到adcBuffer[3]。我们额外定义了一个标志位adcConversionComplete用于在非DMA循环模式或需要同步读取时使用。在纯DMA循环模式下这个标志位可能不是必须的因为数据始终在缓冲区里更新。3.2 启动ADC与DMA传输初始化函数MX_ADC1_Init()已经配置好了ADC和DMA但还没有启动它们。我们需要在main()函数的初始化部分/* USER CODE BEGIN 2 */之后启动DMA和ADC。/* USER CODE BEGIN 2 */ // 启动DMA传输将ADC数据寄存器(外设地址)的数据搬运到adcBuffer(内存地址) // 参数DMA句柄 外设地址 内存地址 数据长度不是字节数是数据项数目 DMA通道 if (HAL_ADC_Start_DMA(hadc1, (uint32_t*)adcBuffer, ADC_BUFFER_SIZE) ! HAL_OK) { Error_Handler(); // 如果启动失败进入错误处理 } /* USER CODE END 2 */HAL_ADC_Start_DMA这个函数完成了三件大事启动ADC的DMA传输功能。配置DMA通道建立从ADC数据寄存器ADC1-DR到我们定义的adcBuffer数组的内存映射。因为之前配置的是Circular模式所以DMA会周而复始地进行数据传输每当ADC转换完一个数据由我们设置的EOC after each conversion触发DMA就将其搬走然后自动更新目标地址指向数组的下一个位置。当填满数组后又回到开头覆盖旧数据。启动了DMAADC本身还没有开始转换。ADC需要一个“触发信号”来启动一次扫描转换。触发源可以是软件触发、定时器触发、外部引脚触发等。这里我们先使用最简单的软件触发。在需要开始采集的地方比如一个定时器中断里或者主循环中调用HAL_ADC_Start(hadc1); // 这个函数在非DMA模式下是启动转换在DMA已启动模式下它使能ADC并等待触发 HAL_ADC_PollForConversion(hadc1, 100); // 轮询等待转换完成超时100ms // 或者使用中断方式 HAL_ADC_Start_IT(hadc1);但更常见的做法是使用定时器触发ADC这样可以实现精确的采样间隔。我们可以在CubeMX中配置一个定时器如TIM2的更新事件作为ADC1的触发源Trigger Source。这样定时器每隔固定时间产生一个触发信号ADC就自动执行一轮多通道扫描DMA则自动搬运数据。这种方式完全解放了CPU是真正的“后台自动采集”。3.3 数据处理与电压值计算数据通过DMA源源不断地写入adcBuffer数组。我们可以在任何需要的时候去读取这个数组。数组里的值是ADC的原始数字量范围是0-409512位分辨率。要得到实际的电压值需要经过换算。公式是电压值 (V) (原始值 / 4095) * 参考电压 (Vref)对于STM32G431Vref通常连接到VDDA模拟电源电压假设我们的开发板VDDA3.3V。那么float get_channel_voltage(uint32_t raw_adc_value) { float vref 3.3f; return ((float)raw_adc_value / 4095.0f) * vref; } // 在主循环或定时任务中读取并转换 void process_adc_data(void) { float voltage_ch1 get_channel_voltage(adcBuffer[0]); // Rank1的通道对应IN5/PA0 float voltage_ch2 get_channel_voltage(adcBuffer[1]); // Rank2的通道对应IN6/PA1 float voltage_ch3 get_channel_voltage(adcBuffer[2]); // Rank3的通道对应IN7/PA2 float voltage_ch4 get_channel_voltage(adcBuffer[3]); // Rank4的通道对应IN8/PA3 // 接下来可以打印、显示、或者用于控制逻辑 // printf(CH1: %.3fV, CH2: %.3fV, CH3: %.3fV, CH4: %.3fV\n, // voltage_ch1, voltage_ch2, voltage_ch3, voltage_ch4); }注意adcBuffer是volatile的直接读取是安全的。但在一些对数据一致性要求极高的场合比如计算4个通道的瞬时和为了防止在读取数组的过程中被DMA更新可以考虑临时关闭ADC或DMA或者使用双缓冲区技术。3.4 利用定时器触发实现精准采样上面提到软件触发不够精确我们配置定时器触发。在CubeMX中配置一个基本定时器比如TIM2。在Parameter Settings中设置Prescaler预分频和Counter Period自动重装载值来计算所需的更新频率。假设系统时钟是170MHz我们希望ADC每秒采样1000次即1kHz。定时器时钟 系统时钟 / (预分频1)。设预分频为16999则定时器时钟 170MHz / 17000 10kHz。计数器周期设为9则定时器更新频率 10kHz / (91) 1kHz。完美。在ADC1的配置中找到Trigger选项。External Trigger Conversion Source选择Timer 2 Update Event。将External Trigger Conversion Edge选择为Rising edge上升沿触发。在代码中我们只需要启动定时器和ADC的DMAADC就会按照1kHz的频率自动进行扫描转换。/* USER CODE BEGIN 2 */ HAL_TIM_Base_Start(htim2); // 启动定时器2 if (HAL_ADC_Start_DMA(hadc1, (uint32_t*)adcBuffer, ADC_BUFFER_SIZE) ! HAL_OK) { Error_Handler(); } // 不需要再调用 HAL_ADC_Start因为定时器会触发它 /* USER CODE END 2 */4. 调试技巧与常见问题排查功能实现了但很可能第一次跑不通。下面是我在调试STM32 ADC DMA时踩过的一些坑和总结的技巧。4.1 调试第一步确认基础配置时钟树检查使用CubeMX的Clock Configuration视图确保给ADC的时钟ADCCLK没有超频STM32G431最高60MHz并且定时器的时钟也计算正确。这是很多异常问题的根源。引脚模式确认在生成的MX_GPIO_Init函数中或者通过调试器查看GPIOA的MODER寄存器确认PA0-PA3被正确设置为模拟模式GPIO_MODE_ANALOG。DMA通道与流STM32的DMA有流Stream和通道Channel的概念。CubeMX通常会自动选择正确的DMA和通道如DMA1 Channel1。如果手动编程务必查阅参考手册的DMA请求映射表确认ADC1对应的DMA请求是哪个。4.2 数据不更新或全为0这是最常见的问题。可能性1DMA或ADC未启动。确保HAL_ADC_Start_DMA调用成功并且如果使用软件触发记得调用HAL_ADC_Start。如果使用定时器触发确保定时器已启动HAL_TIM_Base_Start。可能性2缓冲区数组未加volatile。编译器优化可能导致你永远读到的是数组的初始值。给adcBuffer加上volatile。可能性3采样时间太短。如果输入信号源阻抗高比如直接接了一个1MΩ的电位器ADC内部的采样保持电容来不及充电到稳定电压。尝试大幅增加Sampling Time比如增加到640.5 cycles。可能性4参考电压问题。确保VDDA和VSSA模拟地稳定且干净。在复杂的电路板上模拟电源最好用磁珠或电感与数字电源隔离并加上去耦电容。4.3 数据跳动噪声大ADC读数总是在小范围波动。硬件层面电源噪声用示波器查看VDDA的波形是否平滑。增加滤波电容。信号噪声在ADC输入引脚就近对地加一个小的滤波电容如100nF可以滤除高频噪声。但注意电容太大会影响信号变化速度。地线干扰确保模拟地VSSA和数字地VSS单点连接且走线粗短。软件层面过采样与求平均STM32G431的ADC硬件支持过采样。可以在CubeMX的ADC_Regular_ConversionMode下启用Oversampling设置一个过采样倍数如16倍和右移位数4位。硬件会自动进行多次采样并累加平均输出一个位数更高的结果如12位过采样后输出16位数据再右移回需要的位数能有效提高信噪比。软件滤波在读取adcBuffer后进行软件滤波如滑动平均滤波、中值滤波等。#define FILTER_SIZE 10 uint32_t filter_buffer[FILTER_SIZE] {0}; uint8_t filter_index 0; uint32_t moving_average_filter(uint32_t new_sample) { filter_buffer[filter_index] new_sample; filter_index (filter_index 1) % FILTER_SIZE; uint64_t sum 0; for(int i0; iFILTER_SIZE; i) { sum filter_buffer[i]; } return (uint32_t)(sum / FILTER_SIZE); }4.4 DMA溢出与数据错位在高速采样时如果CPU或总线太忙DMA可能来不及搬运数据而ADC已经产生了新的数据。现象数据出现跳跃、错位或者ADC状态寄存器中的溢出标志OVR被置位。排查在CubeMX中将Overrun behaviour设置为Overrun data preserved并启用ADC的溢出中断ADC_IT_OVR。在溢出中断服务函数中置位一个标志这样就能知道是否发生了溢出。如果发生溢出考虑降低采样率、提高DMA优先级在CubeMX的DMA配置中设置、优化主程序减少总线占用、或者使用双缓冲区Ping-Pong Buffer让DMA在两个缓冲区之间切换给CPU留出更充裕的时间处理一个缓冲区的数据。4.5 使用调试器实时查看内存这是最直接的调试手段。在STM32CubeIDE的调试模式下暂停程序然后在Expressions或Variables视图中添加adcBuffer数组。你可以看到数组中的实时数值。手动改变输入引脚的电压比如用手触摸引入人体感应电压观察对应的数组元素值是否变化。这能快速定位是配置问题还是数据处理问题。5. 性能优化与进阶应用当基础功能跑通后我们可以考虑如何让它跑得更好、更稳。5.1 优化采样率与转换时间ADC的总转换时间 采样时间 逐次逼近转换时间固定为ADC时钟周期的若干倍12位分辨率下通常是12.5个周期。 例如ADCCLK60MHz采样时间设为247.5 cycles转换时间约为 总周期数 247.5 12.5 260 cycles。 单次转换时间 260 / 60MHz ≈ 4.33us。 那么扫描4个通道一轮的时间约为 4.33us * 4 17.32us对应的理论最大采样频率约为57.7kHz对于单个通道的等效采样率如果是4通道轮流则每个通道的更新率是这个值的1/4。优化方向在保证采样精度的前提下减少Sampling Time。可以通过实验确定最小可用的采样时间。提高ADCCLK但注意不要超过60MHz上限。使用ADC的双重/三重模式。STM32G431的ADC1和ADC2可以组成双重模式交替采样同一通道理论上可以将采样率翻倍。但这需要更复杂的触发和DMA配置。5.2 实现双缓冲区Ping-Pong Buffer在高速连续采集且需要软件实时处理数据的场景为了防止处理数据时DMA覆盖缓冲区可以使用双缓冲区。#define BUFFER_SIZE 256 // 每个缓冲区大小 volatile uint32_t adcBufferPing[BUFFER_SIZE]; volatile uint32_t adcBufferPong[BUFFER_SIZE]; volatile uint32_t *activeBuffer adcBufferPing; volatile uint8_t bufferReady 0; // 在DMA半传输完成和传输完成中断中切换缓冲区 void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc) { // 半传输完成意味着Ping缓冲区填满了一半不在双缓冲区模式下我们通常用M2M模式。 // 更常见的做法是使用DMA的双缓冲区模式如果MCU支持或者手动管理两个缓冲区。 } // 一个实用的软件双缓冲思路使用DMA的普通模式非循环长度设为BUFFER_SIZE。 // 当DMA传输完成中断TC触发时意味着一个缓冲区满了。 // 在TC中断里切换DMA的目标内存地址到另一个缓冲区重新启动DMA并设置标志通知主循环处理已满的缓冲区。STM32G4的DMA支持双缓冲区模式Double Buffer Mode可以在CubeMX中直接配置两个内存地址Memory0 Address和Memory1 Address。当对Memory0的传输完成时DMA会自动切换到Memory1并产生传输完成中断此时我们处理Memory0的数据反之亦然。这是最优雅高效的方案。5.3 结合RTOS进行任务调度在复杂的嵌入式系统中ADC数据采集可能只是一个后台任务。我们可以将ADC配置为定时器触发DMA循环采集然后在RTOS如FreeRTOS中创建一个低优先级的任务定期去读取adcBuffer或双缓冲区中已满的那个进行滤波、换算、判断等操作再通过队列、信号量等机制将处理结果发送给其他任务如显示任务、通信任务。这样采集过程完全由硬件自动完成软件任务只负责非实时的数据处理系统响应性和效率更高。5.4 校准ADC以提高精度STM32的ADC出厂时有校准数据但上电后仍需进行一次校准以消除内部电容的误差偏移。 HAL库提供了校准函数。通常在ADC初始化之后启动之前进行。HAL_ADCEx_Calibration_Start(hadc1, ADC_SINGLE_ENDED);对于高精度测量应用执行校准是必要步骤。注意校准过程会稍微耗时且应在模拟部分稳定上电一段时间后再进行。6. 项目总结与核心要点回顾回顾整个“STM32G431多通道ADC DMA采集”的实现过程其核心思想是硬件自动化和数据流管道化。我们通过CubeMX配置搭建了一条从模拟引脚到内存数组的自动化数据流水线定时器作为节拍器发出触发脉冲 - ADC接收到脉冲按序对多个通道进行模数转换 - 每转换完一个数据硬件产生EOC信号 - DMA控制器响应EOC信号将数据从ADC寄存器搬运到指定内存 - 内存数组被循环更新。整个过程中CPU只在最初负责“搭管子”初始化配置之后就可以去处理其他更重要的任务只需在需要的时候来“水管”内存数组里取一瓢“水”数据即可。这种设计极大地提高了系统的效率和实时性。几个让我印象深刻的要点volatile是关键只要涉及DMA、中断等异步修改的变量一定要加volatile这是无数诡异Bug的根源。理解时序ADC的采样时间、总转换时间、定时器触发周期这三者需要匹配。采样时间不足会导致精度差触发周期小于总转换时间会导致ADC还在忙就被触发可能出错。调试从简入繁不要一开始就上多通道、高频率、双缓冲。先从单通道、软件触发、轮询读取开始确保ADC基本功能正常。然后加入DMA观察数据能否自动搬运。再加入多通道扫描。最后才上定时器触发和双缓冲。每一步都验证通过能极大降低排查难度。善用CubeMX与手册CubeMX能可视化地解决80%的配置问题但剩下的20%需要深入参考手册Reference Manual和数据手册Datasheet。比如DMA请求映射、ADC最大时钟、引脚复用功能等手册是最终依据。这个项目虽然基础但涵盖了STM32开发中硬件外设使用、中断/DMA应用、系统资源规划等多个核心概念。把它吃透再面对其他如SPI、I2C、UART等外设的DMA传输时你会发现思路是相通的举一反三会容易很多。在实际产品中这种ADCDMA定时器的组合是构建数据采集子系统最稳定、最可靠的基石。