STM32 ADC连续转换模式:原理、配置与实战应用

📅 2026/7/31 4:28:41
STM32 ADC连续转换模式:原理、配置与实战应用
1. 项目概述为什么需要ADC连续转换模式如果你用过STM32的HAL库做过ADC采集大概率是从轮询或者中断模式入门的。启动一次转换一次读一次值流程清晰但效率低下。当你的项目需要实时监控一个快速变化的模拟信号比如电机电流、音频波形或者电源纹波时这种“单次触发”的模式就显得力不从心了。你可能会想到用DMA这确实是个好办法但DMA通常配合定时器触发配置起来稍显复杂而且对于一些只需要中等速度、连续不断地读取单个通道数据的场景有点“杀鸡用牛刀”的感觉。这时候ADC的连续转换模式Continuous Conversion Mode就该登场了。这个模式的核心思想就一句话一旦启动ADC就会像上了发条一样永不停歇地自动进行转换转换结果会源源不断地填充到数据寄存器里等着你来读取。它完美填补了单次转换和DMA传输之间的空白地带。我最初在做一个电池电压监控项目时需要以大约1kHz的频率读取电压值用中断模式太浪费CPU用DMA又觉得小题大做连续转换模式就成了最优雅的解决方案。简单来说连续转换模式解决了“持续、自动、低开销采样”的核心需求。它特别适合那些对采样时序要求不是极端严格比如不需要精确的等间隔采样但又需要比手动轮询高得多的数据更新率的应用场景。接下来我们就深入拆解这个模式的里里外外。2. 核心原理与工作流程拆解要玩转连续转换模式不能只停留在调用HAL_ADC_Start(hadc1)这个层面必须得清楚它内部的“引擎”是怎么转起来的。这能帮你避开很多坑比如数据覆盖、启动失败、采样率算不准等等。2.1 连续转换模式的引擎从启动到永动在单次转换模式下ADC完成一次转换后会自动停止并将EOC转换结束标志位置位。你需要手动清除这个标志并再次触发转换。而在连续转换模式下这个逻辑被改变了。启动引擎当你调用HAL_ADC_Start()或HAL_ADC_Start_IT()中断模式时ADC的转换引擎被首次触发。首次转换ADC开始对选定的通道进行第一次模数转换。自动重载关键步骤来了。在连续转换模式下一旦一次转换完成硬件会自动清除EOC标志并立即开始下一次转换中间没有任何软件干预的停顿。这个过程是硬件自动完成的形成了一个闭环。数据流转每次转换完成的结果都会写入到同一个数据寄存器ADCx-DR中。这意味着新的数据会覆盖旧的数据。如果你不及时读取之前的数据就丢失了。永动循环只要不调用HAL_ADC_Stop()这个“转换-完成-覆盖-再转换”的循环就会一直持续下去直到天荒地老或断电。这里有一个非常重要的细节在HAL库中即使你使用HAL_ADC_Start_IT()以中断方式启动连续转换也只在第一次转换完成时产生一个中断。后续的连续转换不会再产生EOC中断这是很多人的误解。HAL库这样设计是为了避免在连续高速转换时中断频率过高导致CPU被完全拖垮。那么怎么读取数据呢通常有两种方式在首次中断里启动一个定时器定期来轮询数据寄存器或者更常见的直接采用轮询方式启动连续转换然后在主循环或某个定时器中断里定期去读取ADCx-DR的值。2.2 与单次、扫描、DMA模式的关联与区别连续转换模式常常和其他模式组合使用理解它们的组合关系至关重要。连续转换 vs. 单次转换这是最根本的区别一个“永动”一个“单发”。在CubeMX里就是一个复选框Continuous Conversion Mode的差别。与扫描模式Scan Mode的组合这是功能强大的组合。扫描模式是指ADC按顺序自动转换多个通道。如果同时开启扫描模式和连续转换模式那么ADC会连续不断地、循环地转换你序列里的所有通道。例如你设置了通道1、2、3那么转换顺序就是1-2-3-1-2-3……永不停歇。数据寄存器里会按顺序依次更新为通道1、2、3的结果。读取时必须清楚当前数据对应哪个通道。与DMA模式的协作这是处理高速、多通道数据的黄金搭档。当使能DMA时每次转换完成EOC都会触发DMA请求将数据从ADC-DR自动搬运到你指定的内存数组里。连续转换模式 DMA实现了真正的“后台自动采样”CPU完全被解放。数据在内存中按顺序排列不会丢失是音频采集、电机三相电流同步采样等应用的标配。触发源选择连续转换模式可以和不同的触发源配合。默认是软件触发SWSTART。你也可以将其设置为由定时器触发TIMx_TRGO。这时连续转换模式的“连续”指的是在每次定时器触发事件到来时ADC会自动完成一次或一个序列的转换并且准备好迎接下一次触发而不是不受触发源控制地疯狂转换。这种组合常用于需要精确等间隔采样的场合。注意在CubeMX配置中如果你选择了定时器触发通常需要将Continuous Conversion Mode禁用因为定时器本身已经提供了周期性的触发信号。如果同时使能ADC可能会在定时器触发间隔内自行开始额外的转换导致采样时序混乱。具体需参考芯片参考手册。3. 基于CubeMX与HAL库的配置实战光说不练假把式我们以一个最典型的场景为例用STM32F4的ADC1采集单个通道例如通道5对应某个GPIO的电压使用连续转换模式在主循环中轮询读取。我会把每个配置项背后的“为什么”讲清楚。3.1 CubeMX图形化配置详解打开CubeMX选择你的芯片型号开始配置ADC。选择ADC实例在Analog下拉菜单中找到ADC1点击它。配置时钟ADC的时钟ADCCLK非常关键它决定了转换速度。STM32F4的ADC时钟通常由APB2分频而来。在Clock Configuration标签页确保APB2时钟PCLK2是你期望的频率比如84MHz。然后回到ADC配置页在ADC_Settings的Clock Prescaler中选择一个分频系数使得ADCCLK不超过芯片手册规定的最大值对于STM32F4通常是36MHz。例如PCLK284MHz选择4分频得到ADCCLK21MHz这是安全的。配置分辨率与对齐方式Resolution选择12-bit。这是最常用的精度。更高的精度如16位过采样会降低转换速度。Data Alignment选择Right alignment右对齐。这样12位的结果存放在16位数据寄存器的低12位读取后直接与0xFFF掩码即可处理起来最直观。左对齐在某些计算场景下有用但较少用。开启连续转换模式找到Continuous Conversion Mode将其设置为Enabled。这是本章节的核心。配置扫描模式与通道数因为我们只采一个通道所以将Scan Conversion Mode设置为Disabled。如果使能ADC会期待一个序列即使你只配了一个通道行为也可能有差异为简化问题单通道时建议关闭。Number Of Conversion设置为1。配置采样时间在ADC_Regular_ConversionMode的Rank列表中点击Channel选择你的具体通道比如Channel 5。然后在Sampling Time中选择一个采样周期例如Cycles 84。这个值需要根据你的信号源阻抗来定。阻抗越大需要的采样时间越长以便让ADC内部的采样保持电容充放电到稳定。对于直接连接MCU GPIO的低阻抗信号如分压电阻Cycles 3或Cycles 15可能就够了。选择更长的周期可以提高精度但会降低转换速率。这里选84是一个比较保守的通用值。配置触发方式在External Trigger Conversion Source中选择Regular Conversion launched by software。这意味着我们将通过调用HAL_ADC_Start()来启动转换引擎。生成代码配置好GPIO等其他外设后生成代码。3.2 生成的代码分析与关键函数解读CubeMX会生成adc.c和adc.h。我们重点关注adc.c中的初始化函数MX_ADC1_Init(void)。static void MX_ADC1_Init(void) { ADC_ChannelConfTypeDef sConfig {0}; hadc1.Instance ADC1; hadc1.Init.ClockPrescaler ADC_CLOCK_SYNC_PCLK_DIV4; hadc1.Init.Resolution ADC_RESOLUTION_12B; hadc1.Init.ScanConvMode DISABLE; hadc1.Init.ContinuousConvMode ENABLE; // 看这里连续模式已使能 hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.ExternalTrigConvEdge ADC_EXTERNALTRIGCONVEDGE_NONE; hadc1.Init.ExternalTrigConv ADC_SOFTWARE_START; hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion 1; hadc1.Init.DMAContinuousRequests DISABLE; hadc1.Init.EOCSelection ADC_EOC_SINGLE_CONV; if (HAL_ADC_Init(hadc1) ! HAL_OK) { Error_Handler(); } sConfig.Channel ADC_CHANNEL_5; sConfig.Rank 1; sConfig.SamplingTime ADC_SAMPLETIME_84CYCLES; if (HAL_ADC_ConfigChannel(hadc1, sConfig) ! HAL_OK) { Error_Handler(); } }关键点在于hadc1.Init.ContinuousConvMode ENABLE;。这行代码将连续转换模式的配置写入了ADC的硬件寄存器。3.3 应用层代码编写启动与数据读取在main.c的用户代码区我们编写应用逻辑。// 在文件顶部定义变量 ADC_HandleTypeDef hadc1; uint32_t adc_value 0; float voltage 0.0f; // 在main()函数的初始化部分之后 HAL_ADC_Start(hadc1); // 启动ADC连续转换 while (1) { // 方式一简单的轮询读取非阻塞但可能读到“中间值” // adc_value HAL_ADC_GetValue(hadc1); // 直接读取寄存器任何时候都可能读 // 方式二可靠的轮询读取推荐 if (HAL_ADC_PollForConversion(hadc1, 10) HAL_OK) // 等待一次转换完成超时10ms { adc_value HAL_ADC_GetValue(hadc1); voltage (float)adc_value * 3.3f / 4095.0f; // 假设Vref3.3V12位分辨率 // 在这里使用voltage例如打印、控制等 printf(ADC Value: %lu, Voltage: %.3f V\r\n, adc_value, voltage); } else { // 处理超时错误 } HAL_Delay(100); // 延迟100ms控制读取频率。注意这并不影响ADC自身的转换频率 }这里有一个至关重要的坑需要解释HAL_Delay(100)控制的是我们读取ADC结果的频率是100ms读一次。但ADC自身的转换是连续不停的它的转换频率由ADC时钟ADCCLK和采样时间SamplingTime决定。我们来算一下ADC的实际转换速率假设ADCCLK 21 MHz。采样时间 84个周期。12位转换时间对于STM32F4固定为12.5个周期具体值查数据手册。一次转换的总时间 采样时间 转换时间 84 12.5 96.5 个ADC时钟周期。转换速率 ADCCLK / 总周期 21,000,000 Hz / 96.5 ≈217.6 kHz。这意味着ADC正在以每秒21.76万次的速度疯狂转换并覆盖数据寄存器。我们的主循环每100ms即10Hz才去读一次在这100ms内数据寄存器已经被覆盖了大约21760次我们读到的只是这2万多次转换中最后一次完成的那个值。这带来了连续转换模式最典型的一个特性你读到的永远是最“新鲜”的瞬时值。这对于监控实时变化的信号是优点但对于需要稳定读数的场景如读取稳定的电源电压可能会因为噪声而看到数值跳动。解决方法通常是在软件侧进行滑动平均滤波。实操心得HAL_ADC_PollForConversion函数在连续转换模式下依然有效。它会等待EOC标志置位。由于转换是连续的EOC会周期性地置位。这个函数能确保我们读取到的是一个“完整的、刚转换好”的数据而不是在转换中途去读寄存器导致的错误值。虽然直接读DR寄存器更快但在可靠性要求高的场合建议使用这个轮询等待函数。4. 进阶应用结合DMA实现自动搬运当需要处理多个通道或更高频率的数据流时轮询读取就不够用了因为CPU会被频繁占用且容易丢失数据。DMA直接存储器访问是完美的搭档。4.1 CubeMX中配置ADC连续转换DMA在DMA Settings标签页点击Add为ADC1添加一个DMA流Stream或通道Channel根据不同系列。模式选择Circular循环模式。这样DMA会在搬运完一轮数据后自动回到数组开头重新开始形成一个环形缓冲区实现不间断的数据流。数据宽度都选择Word32位因为ADC数据寄存器是32位的尽管数据只占低16位。回到ADC配置使能DMA Continuous Requests如果CubeMX有该选项。这确保在连续转换模式下每次转换完成都发出DMA请求。将Scan Conversion Mode设置为Enabled并在Rank中配置多个你需要转换的通道和顺序。4.2 代码实现与缓冲区管理// 定义缓冲区 #define ADC_BUFF_SIZE 1024 uint32_t adc_dma_buffer[ADC_BUFF_SIZE] {0}; // 在main初始化部分 HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_dma_buffer, ADC_BUFF_SIZE); // 之后ADC和DMA就会全自动工作。数据会自动填充到adc_dma_buffer数组中。 // 你需要通过半传输完成HT或传输完成TC中断来知道数据已经准备好了一段。在DMA循环模式下缓冲区被一分为二的思想是经典用法。你可以使能DMA的半传输完成中断和传输完成中断。当DMA填满半缓冲时产生HT中断你可以处理前半部分数据当填满整个缓冲时产生TC中断你可以处理后半部分数据。这样实现了“乒乓缓冲”处理数据的同时ADC和DMA可以无缝地向另一半缓冲区继续写入几乎没有数据丢失的风险。关键配置解析DMA Continuous Requests这个必须使能。在连续转换模式下ADC会以最高速率连续产生数据。如果DMA请求不是连续的DMA在完成一次搬运后就会停止而ADC还在不断产生新数据并覆盖DR寄存器后续的数据就无法被DMA捕获导致缓冲区里的数据不连续。数据对齐我们之前配置的是右对齐12位。DMA搬运的是整个32位DR寄存器的值。所以adc_dma_buffer里的每个元素其有效数据在低12位。处理时需要buffer[i] 0xFFF。5. 常见问题、调试技巧与性能优化在实际项目中我踩过不少坑这里总结一下。5.1 数据跳动噪声问题现象即使输入一个稳定的直流电压读取的ADC值也在最后几位不停跳动。原因与解决电源与参考电压噪声这是首要怀疑对象。确保模拟部分VDDA、VSSA的电源干净最好使用LC滤波。如果MCU有独立的VREF引脚务必连接一个稳定、干净的参考电压源。采样时间不足如果信号源阻抗较高比如用了很大的分压电阻ADC的采样保持电容没有足够时间充电到稳定值。增加Sampling Time采样周期数是最直接的解决办法。可以尝试增加到Cycles 480看看效果。数字电路干扰高速数字IO如时钟线、PWM输出会对模拟电路产生耦合干扰。布线时让模拟走线远离数字走线在ADC输入引脚就近添加一个对地的小电容如10nF~100nF进行滤波。软件滤波硬件无法完全解决时必须上软件滤波。滑动平均滤波简单有效#define FILTER_LEN 16 uint32_t adc_filter_buf[FILTER_LEN] {0}; uint8_t filter_index 0; uint32_t filter_sum 0; // 每次获取新值adc_raw后 filter_sum - adc_filter_buf[filter_index]; // 减去最旧的值 adc_filter_buf[filter_index] adc_raw; // 存入新值 filter_sum adc_raw; // 加上新值 filter_index (filter_index 1) % FILTER_LEN; // 更新索引 uint32_t adc_filtered filter_sum / FILTER_LEN; // 计算平均值5.2 转换速率不达预期或计算错误问题实测的采样率远低于理论计算值。排查步骤确认时钟源用示波器或逻辑分析仪检查ADC输入引脚的实际信号频率或者通过翻转一个GPIO在每次转换完成时来间接测量。确保ADCCLK的计算是正确的。检查Clock Configuration中APB2和ADC分频器的设置。检查触发间隔如果你使用了定时器触发并且使能了连续转换模式可能会导致冲突。在定时器触发场景下通常应禁用连续转换模式让定时器作为唯一的触发源。DMA开销在DMA模式下如果缓冲区很小DMA传输完成中断非常频繁CPU处理中断的开销可能成为瓶颈导致你无法及时取走数据虽然ADC本身速度没问题但系统吞吐量上不去。增大DMA缓冲区并使用半传输中断来降低中断频率。5.3 HAL_ADC_Start()失败或无法启动连续转换可能原因ADC未正确初始化检查MX_ADC_Init()是否被调用且HAL_ADC_Init()返回值是否为HAL_OK。ADC状态机错误ADC可能处于错误状态如溢出、过载。在HAL_ADC_Start()前调用HAL_ADC_DeInit()再HAL_ADC_Init()进行复位或者直接调用__HAL_ADC_CLEAR_FLAG(hadc1, ADC_FLAG_ALL)清除所有状态标志试试。DMA冲突如果之前使能过DMA需要先停止DMAHAL_ADC_Stop_DMA()才能用HAL_ADC_Start()。5.4 多通道连续扫描时的数据对应问题当使能扫描模式连续转换时数据寄存器会循环更新多个通道的值。解决方案使用DMA这是最推荐的方式。DMA会按照转换顺序将各通道数据依次存入数组。buffer[0]是通道1buffer[1]是通道2以此类推。不使用DMA你需要知道当前数据寄存器里是哪个通道的结果。在扫描模式下这很困难因为转换是硬件自动循环的软件很难同步。通常需要结合定时器触发和禁用连续转换模式在每次定时器触发后用轮询或中断读取一个序列的数据这样才能保证通道顺序的确定性。5.5 低功耗应用下的考量连续转换模式意味着ADC内核始终处于活动状态功耗会比单次模式高。在电池供电设备中如果需要间歇性采样更好的策略是使用单次转换模式由一个低功耗定时器如LPTIM定期唤醒MCU并触发一次ADC转换。转换完成后MCU读取数据处理然后再次进入低功耗模式。这样可以最大限度地节省电量。最后分享一个调试小技巧如果你不确定ADC是否真的在以连续模式工作可以初始化一个GPIO引脚在ADCx_IRQHandler中断服务函数里如果你开了中断或者在主循环的读取点用HAL_GPIO_TogglePin()翻转这个引脚。然后用示波器观察这个引脚的电平翻转频率它应该等于你读取ADC数据的频率轮询方式或者转换完成中断的频率。这能直观地验证你的软件逻辑是否按预期运行。