资讯动态

STM32G431多通道ADC采样:DMA配置、双缓冲实现与性能优化实战

发布时间:2026/8/7 2:59:27 来源:尧图企业网站定制
1. 项目概述为什么DMA是STM32 ADC采样的“王牌”在嵌入式开发里采集模拟信号是家常便饭。无论是监测电池电压、读取传感器数据还是做音频处理都离不开ADC模数转换器。用STM32做过ADC采集的朋友大概率都经历过两种模式一种是轮询在主循环里傻等转换完成标志另一种是中断每次转换完成都跳进中断服务函数里读数据。这两种方式在单通道、低频采样时还能应付但一旦遇到多通道、高频率的连续采样任务CPU就会被频繁打断大量时间浪费在“搬运数据”这种机械劳动上系统实时性大打折扣还容易丢数据。这时候就该DMA直接存储器访问登场了。你可以把它想象成一个极其高效且任劳任怨的“数据搬运工”。它的核心工作逻辑是当ADC转换完成一个数据后会产生一个“货物已备好”的信号DMA请求DMA控制器一收到这个信号就立刻行动起来在完全不需要CPU干预的情况下自动把ADC数据寄存器里的值搬运到你预先指定的内存数组里。CPU在此期间可以安心处理其他更复杂的任务比如算法运算、协议解析或者系统调度。我这次要分享的就是在STM32G431这款性能不错的Cortex-M4内核MCU上实现多通道ADC电压值采集的DMA方案。STM32G4系列是意法半导体主打高性能和数字电源应用的产品线G431更是其中的性价比之选它的ADC通常是ADC1支持多达19个外部通道采样速率快配合DMA使用能轻松构建一个稳定、高效、低CPU占用的数据采集系统。这个项目非常适合需要同时监控多个模拟量比如多路温度、压力、电流的工业控制、能源管理或实验室仪器等场景。2. 核心思路与硬件设计考量2.1 为什么选择STM32G431的ADC1与DMA1STM32G431内部通常集成有多个ADC模块我们最常用的是ADC1。它是一款12位分辨率的逐次逼近型ADC对于大多数精度要求不极端的场景完全够用。它的关键优势在于支持扫描模式和连续转换模式这正是多通道DMA采集的基石。扫描模式ADC会按照你预先配置好的通道序列比如通道0、5、8一个接一个地自动进行转换。连续转换模式当一次扫描序列全部转换完成后ADC不会停止而是立刻从头开始下一次扫描源源不断地产生数据。当这两种模式结合ADC就会像一台不知疲倦的扫描仪循环扫描你关心的那几个通道。而DMA的作用就是在每次单个通道转换完成时及时把数据“搬走”存到数组里。在STM32G431中ADC1的常规转换数据通常对应到DMA1的某个数据流Stream和通道Channel。你需要查阅芯片的参考手册Reference Manual找到准确的映射关系。对于G431ADC1的转换数据通常由DMA1的Stream0或Stream4通过Channel0来请求和传输。配置时务必确认这一点这是整个功能正常工作的硬件前提。2.2 多通道采样的内存布局规划这是使用DMA时一个非常关键且容易出错的点。我们采集的是多个通道数据最终要存到一个内存数组中。这个数组该怎么定义假设我们要循环采集3个通道ADC通道0PA0、通道5PA5、通道8PB0。如果配置DMA为单次传输每次传输半字即16位对应ADC的12位数据并且使能了DMA的循环模式Circular Mode那么DMA会周而复始地把数据搬运到内存。一种直观但错误的想法是定义一个二维数组adc_value[3]希望DMA自动把通道0的数据放到adc_value[0]通道5的放到[1]通道8的放到[2]。但DMA没这么智能它只会按照内存地址递增的顺序依次存放。因此正确的做法是定义一个一维数组其长度等于“一次完整扫描的通道数” × “你希望缓存的数据组数”。例如#define ADC_CHANNEL_NUM 3 // 要采集的通道数量 #define ADC_BUFFER_DEPTH 100 // 每个通道缓存的数据深度组数 volatile uint16_t adc_raw_buffer[ADC_CHANNEL_NUM * ADC_BUFFER_DEPTH];DMA的目标地址就指向这个数组的首地址adc_raw_buffer[0]传输数据量NDTR寄存器设置为ADC_CHANNEL_NUM * ADC_BUFFER_DEPTH。那么数据会如何填充呢假设ADC按顺序扫描通道0、5、8DMA搬运的结果将是adc_raw_buffer[0]- 第1组通道0的值adc_raw_buffer[1]- 第1组通道5的值adc_raw_buffer[2]- 第1组通道8的值adc_raw_buffer[3]- 第2组通道0的值adc_raw_buffer[4]- 第2组通道5的值 ... 以此类推。当DMA传输完所有ADC_CHANNEL_NUM * ADC_BUFFER_DEPTH个数据后由于开启了循环模式它会自动重置传输计数器从数组开头重新开始搬运覆盖旧数据。这样我们就得到了一个环形的缓冲区。应用程序只需要定期例如每采集完N组数据后去这个缓冲区里按规律取出各个通道的最新值即可实现了数据采集与处理的解耦。注意这里使用了volatile关键字声明数组。这是因为DMA控制器是独立于CPU操作这个内存区域的编译器在优化代码时可能会认为这个数组的值没有变化而使用缓存中的旧值。volatile告诉编译器这个变量可能被意想不到地改变每次使用都必须从内存中重新读取确保数据一致性。3. 详细配置步骤与代码实现下面我将基于STM32CubeMX配置工具和HAL库一步步拆解配置过程并给出关键代码。即使你用的是标准外设库或直接寄存器操作思路也是完全相通的。3.1 使用STM32CubeMX进行图形化配置引脚配置在Pinout Configuration标签页找到你需要使用的ADC通道对应的GPIO引脚例如PA0、PA5、PB0。将它们配置为**模拟输入Analog**模式。CubeMX会自动将GPIO初始化为模拟模式关闭上下拉以降低功耗和干扰。ADC1配置在Analog-ADC1中找到Scan Conversion Mode选择Enabled。这是多通道采集的开关。找到Continuous Conversion Mode选择Enabled。让ADC不停歇地工作。找到DMA Continuous Requests选择Enabled。这个选项非常重要它确保在一次DMA传输未完成时ADC产生的新数据能继续触发DMA请求而不是被丢弃。对于连续扫描模式必须开启。在Rank配置表中添加你需要采样的通道。比如Rank1选择通道0Rank2选择通道5Rank3选择通道8。可以设置每个通道的采样时间Sampling Time采样时间越长抗噪声能力越强但转换速率会降低。根据信号源阻抗和精度要求调整对于大多数传感器239.5 Cycles是一个比较稳妥的起点。Resolution保持默认的12位。Data Alignment选择右对齐Right Alignment这样读取到的16位数据有效值就在低12位处理起来最直观。DMA配置在DMA Settings点击Add添加一个DMA请求。DMA Request选择ADC1。Mode选择Circular循环模式这样DMA会无限循环工作。Direction选择Peripheral To Memory从外设到内存。Data WidthPeripheral和Memory都选择Half Word半字16位以匹配ADC数据寄存器16位和我们定义的uint16_t数组。Increment AddressMemory需要设为Enabled让目标内存地址在每次传输后自动递增Peripheral必须设为Disabled因为ADC数据寄存器地址是固定的。Priority可以根据系统需求设置通常High或Very High即可。时钟与中断配置在Clock Configuration标签页确保系统时钟HCLK和ADC时钟通常来自APB2已正确配置。ADC时钟不能超过其最大允许值对于G431通常是60MHz太高的时钟会导致采样精度下降。在NVIC Settings中通常不需要开启ADC的全局中断或转换完成中断因为数据搬运由DMA全权负责。但是强烈建议开启DMA传输完成中断TCIE或半传输完成中断HTIE。这样我们可以在DMA搬运完一半或全部缓冲区时得到通知去处理数据实现“双缓冲”机制能有效避免处理数据时缓冲区被覆盖的问题。3.2 关键代码解析与填充CubeMX生成代码后我们需要在用户代码区添加一些关键逻辑。第一步定义缓冲区并启动ADC/* 在文件顶部全局变量区定义 */ #define ADC_CH_NUM 3 #define ADC_BUF_DEPTH 100 volatile uint16_t adc_dma_buffer[ADC_CH_NUM * ADC_BUF_DEPTH]; /* 在main函数初始化部分启动ADC和DMA */ HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_dma_buffer, ADC_CH_NUM * ADC_BUF_DEPTH);HAL_ADC_Start_DMA这个函数一举三得它启动了ADC的转换启动了关联的DMA传输并设置了DMA的目标地址和传输数据总量。调用之后整个采集系统就开始自动运行了。第二步处理DMA中断实现双缓冲在stm32g4xx_it.c中找到DMA对应的中断服务函数比如DMA1_Channel1_IRQHandler它会调用HAL库的HAL_DMA_IRQHandler。我们需要在用户代码中重写对应的回调函数。首先在main.c的用户代码区声明回调函数/* 当DMA传输完成一半半缓冲区满时调用 */ void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc) { if (hadc-Instance ADC1) { // 此时数据已经安全地存入了 adc_dma_buffer 数组的前半部分 // 可以安全地处理前半部分数据而DMA正在向后半部分写入 process_adc_data(adc_dma_buffer, ADC_CH_NUM * (ADC_BUF_DEPTH/2)); } } /* 当DMA传输全部完成全缓冲区满时调用 */ void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { if (hadc-Instance ADC1) { // 此时数据已经安全地存入了 adc_dma_buffer 数组的后半部分 // 可以安全地处理后半部分数据而DMA已经回到前半部分开始写入 process_adc_data(adc_dma_buffer[ADC_CH_NUM * (ADC_BUF_DEPTH/2)], ADC_CH_NUM * (ADC_BUF_DEPTH/2)); } }第三步编写数据处理函数process_adc_data函数是你应用逻辑的核心。它需要从交错的原始缓冲区中提取出每个通道的数据。void process_adc_data(volatile uint16_t *buffer, uint32_t size) { // size 是本次回调传入的缓冲区长度半缓冲或全缓冲的总数据个数 uint32_t num_of_sets size / ADC_CH_NUM; // 计算有多少“组”完整数据 uint16_t ch0_values[ADC_BUF_DEPTH/2]; // 临时存储通道0的数据 uint16_t ch5_values[ADC_BUF_DEPTH/2]; uint16_t ch8_values[ADC_BUF_DEPTH/2]; for (uint32_t i 0; i num_of_sets; i) { // 从交错的数据中分离各通道 ch0_values[i] buffer[i * ADC_CH_NUM 0]; // 第i组第一个位置是通道0 ch5_values[i] buffer[i * ADC_CH_NUM 1]; // 第i组第二个位置是通道5 ch8_values[i] buffer[i * ADC_CH_NUM 2]; // 第i组第三个位置是通道8 } // 现在你可以对 chX_values 数组进行进一步处理了 // 例如求平均值、滤波、判断阈值、通过串口发送等等。 // 这里只是简单计算最后一个值的电压 float v0 (float)ch0_values[num_of_sets-1] * 3.3f / 4095.0f; // 假设Vref3.3V float v5 (float)ch5_values[num_of_sets-1] * 3.3f / 4095.0f; float v8 (float)ch8_values[num_of_sets-1] * 3.3f / 4095.0f; // ... 你的应用逻辑 ... }通过这种双缓冲机制数据处理和DMA数据搬运在时间上几乎是并行的极大地提高了系统效率和实时性避免了数据竞争。4. 核心参数计算与性能评估配置多通道DMA-ADC时有几个关键参数决定了系统的性能和稳定性必须心里有数。1. 总转换时间与采样率ADC转换一个通道的时间 采样时间逐次逼近转换时间固定如12.5个ADC时钟周期。 对于STM32G431假设ADC时钟ADCCLK为60MHz单个通道采样时间设置为239.5 Cycles。采样阶段时间T_sampling 239.5 / 60MHz ≈ 3.99μs转换阶段时间T_conversion 12.5 / 60MHz ≈ 0.21μs单通道转换时间T_ch 3.99 0.21 4.2μs对于3个通道的扫描序列一次扫描总时间T_scan 3 * T_ch 12.6μs理论最大采样率每个通道F_sample_per_ch 1 / T_scan ≈ 79.4 kHz这意味着在连续扫描模式下每个通道每秒最多能被采样约7.94万次。这是一个理论峰值实际可用采样率还受DMA搬运速度、中断处理时间等因素影响但通常非常接近。2. DMA缓冲区大小与系统延迟缓冲区深度ADC_BUF_DEPTH决定了数据处理任务的“弹性空间”。深度越大CPU处理单次中断的间隔可以越长但带来的数据延迟也越大。数据延迟Latency从信号被ADC采样到被应用程序处理中间的最大时间差。在最坏情况下刚采样完的数据位于缓冲区末尾延迟时间为处理整个缓冲区所需的时间。Max_Latency ADC_BUF_DEPTH * T_scan例如ADC_BUF_DEPTH100则最大延迟 100 * 12.6μs 1.26ms。如果你的控制系统要求响应时间在1ms以内这个缓冲区深度就太大了需要减小到50甚至更小。中断频率DMA半传输/传输完成中断的频率为F_interrupt F_sample_per_ch * ADC_CH_NUM / (ADC_BUF_DEPTH / 2)。缓冲区深度越大中断频率越低CPU负担越轻但延迟越高。需要在实时性和CPU负载之间做权衡。5. 实战避坑指南与高级技巧在实际项目中仅仅调通基础功能是不够的稳定性和可靠性才是关键。下面分享几个我踩过坑才总结出来的经验。坑点一数据错位或跳动现象从缓冲区里解析出来的通道数据是乱的或者数值不稳定。排查与解决检查DMA内存地址自增确保DMA配置中Memory Increment是开启的。如果关闭了所有通道的数据都会堆叠在数组的第一个元素导致数据覆盖和混乱。检查ADC通道序列Rank在CubeMX或代码中确认你添加的Rank顺序1,2,3...是否与你期望的采样顺序一致。DMA会严格按照ADC转换完成的顺序搬运数据。检查参考电压和接地ADC的测量值是基于参考电压VREF的。确保VREF引脚连接了稳定、干净的电源通常是3.3V。模拟地VDDA和数字地VSS之间建议用磁珠或0欧电阻单点连接并在靠近芯片引脚处放置去耦电容如100nF10uF这是抑制数字噪声干扰模拟测量的关键。优化采样时间如果信号源阻抗较高如某些热电偶、光敏电阻过短的采样时间会导致ADC内部的采样电容充电不足读数偏低且跳动。适当增加该通道的采样周期数Sampling Time。坑点二DMA传输不启动或中途停止现象ADC启动了但数组里一直没有新数据或者数据更新一段时间后停止了。排查与解决确认DMA通道映射这是最硬性的错误。必须核对芯片数据手册Datasheet或参考手册确认ADC1的DMA请求对应的是DMA1的哪个Stream和Channel。G431通常是DMA1 Stream0/Channel0或Stream4/Channel0。CubeMX一般会自动配置正确但手动编程时极易出错。开启DMA连续请求在ADC配置中DMA Continuous Requests必须设为Enable。如果禁用DMA在完成一次配置的数据量传输后就会停止即使ADC还在产生数据。检查缓冲区溢出如果开启了DMA传输完成中断但在中断回调函数里处理数据的时间过长超过了DMA填满另一半缓冲区的时间就会发生缓冲区被覆盖而数据丢失。解决方法是优化数据处理算法或者增大缓冲区深度来赢得更多处理时间。高级技巧注入通道与规则通道的混合使用STM32的ADC除了规则通道序列Regular Group还有一个注入通道序列Injected Group。注入通道可以理解为“插队”通道拥有更高的优先级。当触发注入转换时ADC会立即暂停当前的规则序列转换先完成注入通道的转换然后再回到规则序列。 这在一些安全关键场景非常有用。例如你用规则通道DMA循环采集4路普通的温度信号同时配置一个注入通道来采集关键的过流保护信号。当过流信号触发时可以通过外部触发或软件触发ADC立即采样该通道你可以在注入转换完成中断中快速读取并采取保护动作而不需要等待漫长的规则序列扫描完成。规则通道的DMA采集完全不受影响实现了高优先级事件响应与后台连续监测的完美结合。配置注入通道时注意它不能使用DMA进行数据搬运它的数据存储在独立的注入数据寄存器JDRx中需要在注入转换完成中断中手动读取。这恰恰符合其“紧急、快速响应”的定位。6. 调试方法与问题定位实录当系统不工作时有序的调试是快速定位问题的关键。第一步确认基础时钟与电源使用调试器或printf输出系统核心时钟HCLK、APB2时钟ADC时钟源的频率确认它们是否在预期范围内。测量VREF和VDDA引脚电压确保在芯片要求范围内如2.4V-3.6V。第二步验证ADC单次转换在启动复杂的DMA多通道扫描之前先简化问题。配置ADC为单通道、单次转换模式不使用DMA用轮询或中断方式读取一个已知电压如通过电阻分压得到的1.65V的通道。如果这里读数都不对那问题就在更底层引脚、参考电压、时钟。第三步验证DMA基本传输可以写一个简单的测试用DMA从一个内存数组搬运数据到另一个内存数组或者从外设如SPI的DR寄存器搬运数据到内存。先排除DMA控制器本身配置的问题。第四步结合调试器观察这是最强大的手段。在IDE如STM32CubeIDE中将adc_dma_buffer数组添加到Live Watch实时监视窗口。全速运行程序。你应该能看到数组中的数值在快速、有规律地变化。如果数据完全不变说明DMA/ADC没启动如果数据变化但杂乱无章说明配置如通道顺序、内存地址递增有误。在DMA传输完成中断回调函数入口处设置断点。程序应该能周期性地停在这个断点处。如果没有检查NVIC中DMA传输完成中断是否使能以及HAL_ADC_Start_DMA是否成功调用。第五步信号完整性检查如果数值有规律但波动较大可能是模拟信号问题。用示波器探头打到10X档减少对电路的影响直接测量ADC输入引脚上的波形。观察是否有高频毛刺、电源噪声耦合或地电平波动。通常需要在信号输入端添加一个RC低通滤波如1kΩ电阻串联100nF电容对地可以滤除大部分高频噪声。我个人的习惯是在项目初期就会把ADC的某个通道通过分压电阻接到VREF或一个已知的精密基准源上。这样在程序里读取这个通道的值就能实时监控ADC模块本身的工作是否正常、参考电压是否稳定相当于内置了一个自检功能。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价