深入解析STM32 ADC的多通道转换与中断处理机制

发布时间:2026/7/3 2:35:13

深入解析STM32 ADC的多通道转换与中断处理机制 1. 从单通道到多通道为什么我们需要更复杂的ADC配置刚开始玩STM32的时候相信很多朋友和我一样都是从单通道ADC采集入门的。找个空闲的引脚接个电位器或者光敏电阻配置好一个通道开启转换然后在主循环里或者中断里把数据读出来看着串口助手打印的电压值变化那种成就感还是挺足的。我最早做的几个小项目比如电池电压检测、环境光强度感应都是这么干的简单直接代码也清爽。但是现实中的项目往往没这么“单纯”。我记得有一次接了个小活要做一个简单的数据采集模块需要同时监测一路温度传感器热敏电阻、一路电流采样通过运放、还有一路电源电压。这时候如果还沿用单通道那套“轮询”或者“单次中断”的方法代码就会变得非常臃肿和低效。你可能会想那我轮流初始化三个不同的ADC通道不就行了理论上可以但每次切换通道都需要重新配置、触发、等待转换这中间的时间开销和CPU占用率就上去了而且时序很难做精确。这就是多通道ADC转换要解决的问题核心如何高效、有序、可靠地采集多路模拟信号。STM32的ADC模块设计得非常强大它允许我们一次性配置好多个通道然后按照我们设定的顺序自动进行转换转换完成后再统一通知我们。这就像请了一个专门的“数据采集助理”你只需要告诉它“按A、B、C的顺序去测量这三个点的电压测完了喊我一声”它就能自动完成解放了CPU去处理其他更重要的任务。对于STM32F1系列这类资源不算特别丰富的芯片来说用好ADC的多通道和中断功能是提升系统实时性和效率的关键一步。它能让你在资源有限的情况下依然能流畅地处理多路模拟信号比如在电机控制中同时采样三相电流、在电源管理中监控多路电压、在物联网节点中采集温湿度光照等多种传感器数据。接下来我就结合自己踩过的坑和积累的经验带你深入STM32 ADC的多通道与中断世界让你从“会用”变成“精通”。2. 庖丁解牛深入理解ADC多通道转换的机制要想玩转多通道不能只停留在调用库函数的层面必须得搞清楚STM32 ADC内部是怎么运作的。这就像开车只知道踩油门和刹车也能开但懂了发动机和变速箱的原理你就能开得更顺、更省油出了问题也知道大概从哪排查。2.1 规则通道与注入通道谁是主力谁是“插队者”原始文章里提到了规则通道和注入通道这个概念非常重要但刚开始接触时容易迷糊。我用一个更生活的例子来解释一下。想象一下你在银行排队办业务。规则通道就像普通的取号排队。你取了号配置了转换顺序然后就在队伍里等着柜员ADC模块按叫号顺序SQR寄存器设定的顺序依次为每个客户模拟通道服务。这个队伍最长可以排16个人16个规则通道。那注入通道是什么呢它好比银行的“VIP客户”或者“紧急业务”通道。当规则通道的队伍正在有序办理时突然来了一个VIP客户注入通道转换请求他有权限立即中断当前正在办理的普通业务优先为他服务。等服务完了柜员再回到刚才被中断的普通客户那里继续办理。STM32的注入通道最多有4个JL[1:0]位控制实际使用的数量优先级比规则通道高。在实际项目中规则通道是绝对的主力95%以上的多通道采集场景都用它。比如你需要周期性采集8路传感器数据那把这8个通道配置成规则序列就行了。而注入通道则用在一些需要快速响应的特殊场景。举个例子我在做一个电机驱动板时用规则通道循环采样三相电流做控制算法但同时用了一个注入通道来监控直流母线电压。平时它不参与转换一旦母线电压出现过压或欠压的苗头通过模拟看门狗设置阈值就立即触发注入通道转换快速获取精确电压值进行保护判断确保系统安全。它就像一个时刻待命的“消防员”。2.2 转换顺序的编排SQR寄存器的编程艺术多通道转换顺序是关键。这个顺序完全由我们编程决定通过一组叫做SQRSequence Register的寄存器来配置。对于规则通道是SQR1、SQR2、SQR3对于注入通道是JSQR。很多新手会在这里犯错以为配置了通道就行了顺序是默认的。其实不然STM32需要我们明确地告诉它“第一个转谁第二个转谁……”。SQR1的L[3:0]位决定了本次转换序列总共有几个通道1到16个。然后SQ1[4:0]到SQ16[4:0]这些位分别用来存放第1到第16个要转换的通道编号比如ADC_Channel_0对应0x00。配置时有个细节要注意通道编号和转换顺序是解耦的。你可以让通道0PA0第一个转换也可以让它最后一个转换。这给了我们很大的灵活性。比如项目中如果某个通道的信号变化最快、最需要及时采样我们就可以把它放在序列的第一位。在代码里ST的库函数ADC_RegularChannelConfig帮我们封装好了这个操作// 将ADC1的通道5PA5设置为规则序列中的第2个进行转换采样时间为55.5个周期 ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 2, ADC_SampleTime_55Cycles5); // 将ADC1的通道11PC1设置为规则序列中的第1个进行转换 ADC_RegularChannelConfig(ADC1, ADC_Channel_11, 1, ADC_SampleTime_55Cycles5);上面两行代码就设定了一个两通道的转换序列先转换通道11再转换通道5。这里Rank参数例子中的1和2就是转换顺序非常重要。2.3 扫描模式与连续模式让ADC自动跑起来单通道时我们可能用单次转换触发一次转换一次。多通道时必须引入两个关键模式扫描模式Scan Mode和连续模式Continuous Mode。这两个模式是相辅相成的。扫描模式ADC_ScanConvMode这个模式是针对多通道的。当它被启用ENABLE时ADC会按照SQR寄存器里设定的顺序自动完成整个序列所有通道的一次转换。如果禁用DISABLE那么即使你在SQR里配了多个通道ADC也只会转换序列中的第一个通道即Rank1的那个。所以只要是多通道转换扫描模式必须开启。连续模式ADC_ContinuousConvMode这个模式决定了一次扫描完成后ADC是否自动开始下一次扫描。如果开启连续模式ADC在完成整个序列的转换后会自动从头开始下一轮转换周而复始形成一个连续的采集流水线。如果关闭连续模式那么ADC完成一次序列扫描后就会停止需要再次由软件或硬件触发才能开始下一次扫描。最常用的组合是扫描模式开启 连续模式开启。这样你只需要触发一次比如软件触发一次或者使能一个定时器触发ADC就会像开了自动驾驶一样不停地、循环地按照你设定的顺序采集所有通道的数据。这对于需要实时监控多路信号的系统来说是最高效的方式。3. 中断处理机制如何优雅地“接收”数据数据在ADC那里转换好了我们怎么知道并取回来呢最简单的办法是“轮询”Polling就是不断地去查询状态寄存器ADC_SR的EOCEnd Of Conversion标志位。这种方法在单通道或者对实时性要求不高的场合勉强能用但在多通道连续采集时非常糟糕因为CPU会被长期占用在查询状态上啥也干不了。更优雅、更高效的方式就是使用中断Interrupt。让ADC转换完成后主动“通知”CPUCPU再去处理数据。这样CPU在等待转换期间可以执行其他任务大大提高了系统效率。3.1 ADC有哪些中断源STM32的ADC提供了几个重要的中断事件我们可以根据需求选择启用哪一个规则通道转换结束中断ADC_IT_EOC这是最常用的。当规则通道序列中每一个通道转换完成时都会产生一次这个中断。注意是每个通道转换完都产生一次如果你配置了5个规则通道那么每完成一轮扫描你会收到5次EOC中断。注入通道转换结束中断ADC_IT_JEOC当注入通道序列中所有通道转换完成时产生一次中断。注入通道是作为一个组来处理的。模拟看门狗中断ADC_IT_AWD当被监视的ADC通道可以设置规则或注入通道的模拟电压值超出你预设的高/低阈值时会产生此中断。用于电压监控和报警非常方便。对于多通道规则转换我们主要打交道的就是ADC_IT_EOC中断。这里有一个非常重要的点需要理解在多通道扫描模式下每次单个通道转换完成就会置位EOC标志并可能产生中断如果中断使能了。而不是等整个序列转换完才产生一个中断。这个特性决定了我们中断服务程序ISR的写法。3.2 中断服务程序怎么写数据放哪里这是多通道ADC编程的核心也是容易出问题的地方。因为EOC中断来得太频繁了每个通道转换完都来一次我们需要一个高效、可靠的方法来读取和存储数据。一个经典的“坑”直接在EOC中断里用ADC_GetConversionValue读取数据然后可能做些计算。如果只有两三个通道问题不大。但如果通道多了或者采样率很高频繁进入中断会消耗大量CPU资源甚至可能因为中断处理太慢导致错过下一次转换完成的中断。更优的方案是结合DMA直接存储器访问。但这里我们先讲纯中断的方式理解其机制。假设我们不使用DMA那么在EOC中断服务程序中我们需要做以下几件事判断中断来源检查是哪个ADCADC1/2/3和哪个事件EOC/JEOC/AWD触发的中断。读取数据调用ADC_GetConversionValue(ADCx)读取ADC数据寄存器ADC_DR的值。这个值就是刚刚转换完成的那个通道的结果。关键一步知道读的是哪个通道的数据这是难点。因为ADC_DR寄存器不会告诉你这个数据来自通道5还是通道11。所以我们必须在程序里自己维护一个“通道索引”或者通过转换顺序来推断。一种常见做法是在中断里根据一个循环递增的索引将数据存放到一个数组的对应位置。清除中断标志调用ADC_ClearITPendingBit清除相应的中断标志位否则会不断进入中断。下面是一个简化的示例框架假设我们按顺序转换3个通道Ch11, Ch5, Ch8#define ADC_CH_NUM 3 uint16_t ADC_ConvertedValue[ADC_CH_NUM]; // 存储转换结果的数组 uint8_t current_channel_index 0; // 当前通道索引 void ADC1_2_IRQHandler(void) { if (ADC_GetITStatus(ADC1, ADC_IT_EOC) SET) { // 1. 读取当前转换值 uint16_t raw_value ADC_GetConversionValue(ADC1); // 2. 根据当前索引将值存入数组对应位置 // 注意这里假设中断顺序严格对应通道转换顺序。更稳健的做法是结合状态机或DMA。 ADC_ConvertedValue[current_channel_index] raw_value; current_channel_index; if (current_channel_index ADC_CH_NUM) { current_channel_index 0; // 一轮采集完成复位索引 // 可以在这里设置一个标志位通知主循环一轮数据已就绪 g_adc_round_complete 1; } // 3. 清除中断标志 ADC_ClearITPendingBit(ADC1, ADC_IT_EOC); } }这个例子展示了基本思路但在实际复杂应用中单纯依靠中断索引可能会因为中断丢失或响应不及时而出错。因此对于要求严格顺序和完整性的多通道高速采集强烈推荐使用DMA。DMA可以自动将ADC_DR寄存器里的数据搬运到你指定的内存数组中完全不需要CPU干预只在整轮或半轮传输完成时产生一个中断通知CPU效率极高数据顺序也有保证。4. 实战演练配置一个四通道轮询采集系统光说不练假把式我们动手配置一个实例。目标使用ADC1以扫描连续模式循环采集通道11PC1、通道5PA5、通道8PB0、通道0PA0四路电压使用EOC中断读取数据并将每轮采集完成的四组数据通过串口打印出来。4.1 硬件与引脚准备首先确认你的开发板或自制电路上这四个引脚PC1, PA5, PB0, PA0已经连接了待测的模拟信号源比如电位器分压。STM32的ADC通道与GPIO的对应关系是固定的需要查数据手册。确保这些GPIO引脚被初始化为模拟输入模式GPIO_Mode_AIN这是ADC采集所必需的可以最大程度减少引脚本身对模拟信号的干扰。void ADC_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); // 配置PC1 (ADC123_IN11) GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOC, GPIO_InitStructure); // 配置PA5 (ADC12_IN5) 和 PA0 (ADC123_IN0) GPIO_InitStructure.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_0; GPIO_Init(GPIOA, GPIO_InitStructure); // 配置PB0 (ADC12_IN8) GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_Init(GPIOB, GPIO_InitStructure); }4.2 ADC与中断配置这是核心配置部分。我们将启用扫描模式和连续模式配置四个通道的转换顺序并使能EOC中断。void ADC_Mode_Config(void) { ADC_InitTypeDef ADC_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; // 1. 开启ADC1和GPIO的时钟GPIO时钟前面已开 RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); // 2. 配置ADC时钟PCLK272M8分频得到9MHz RCC_ADCCLKConfig(RCC_PCLK2_Div8); // 3. 初始化ADC1参数 ADC_InitStructure.ADC_Mode ADC_Mode_Independent; // 独立模式单ADC工作 ADC_InitStructure.ADC_ScanConvMode ENABLE; // 启用扫描模式多通道必须 ADC_InitStructure.ADC_ContinuousConvMode ENABLE; // 启用连续转换模式 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; // 软件触发 ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; // 数据右对齐 ADC_InitStructure.ADC_NbrOfChannel 4; // 规则通道序列长度为4 ADC_Init(ADC1, ADC_InitStructure); // 4. 配置规则通道的转换顺序和采样时间 // 参数ADCx, 通道, 转换顺序(1~16), 采样时间 ADC_RegularChannelConfig(ADC1, ADC_Channel_11, 1, ADC_SampleTime_55Cycles5); // 第1个转换 ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 2, ADC_SampleTime_55Cycles5); // 第2个转换 ADC_RegularChannelConfig(ADC1, ADC_Channel_8, 3, ADC_SampleTime_55Cycles5); // 第3个转换 ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 4, ADC_SampleTime_55Cycles5); // 第4个转换 // 5. 配置ADC中断EOC ADC_ITConfig(ADC1, ADC_IT_EOC, ENABLE); // 使能规则通道转换结束中断 // 6. 配置NVIC嵌套向量中断控制器 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); // 设置优先级分组 NVIC_InitStructure.NVIC_IRQChannel ADC1_2_IRQn; // ADC1和ADC2共享中断 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; // 抢占优先级 NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; // 子优先级 NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); // 7. 使能ADC并进行校准校准很重要能减少零点误差和增益误差 ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); // 等待校准寄存器复位完成 ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); // 等待校准完成 // 8. 软件触发开始第一次转换之后由于连续模式会自动循环 ADC_SoftwareStartConvCmd(ADC1, ENABLE); }4.3 中断服务程序与数据管理我们使用一个全局数组adc_values来存储四个通道的数据并用一个索引adc_index来跟踪当前存储位置。同时设置一个标志adc_ready当一轮四个数据都采集完成后置位该标志通知主程序。volatile uint16_t adc_values[4] {0}; // 存储四个通道的结果 volatile uint8_t adc_index 0; // 当前存储索引 volatile uint8_t adc_ready 0; // 一轮采集完成标志 void ADC1_2_IRQHandler(void) { // 判断是否是ADC1的EOC中断 if (ADC_GetITStatus(ADC1, ADC_IT_EOC) ! RESET) { // 读取当前转换值 uint16_t raw_data ADC_GetConversionValue(ADC1); // 将数据存入数组 adc_values[adc_index] raw_data; adc_index; // 如果四个通道都采集完了 if (adc_index 4) { adc_index 0; // 重置索引准备下一轮 adc_ready 1; // 设置标志通知主循环 } // 清除中断标志 ADC_ClearITPendingBit(ADC1, ADC_IT_EOC); } }4.4 主程序逻辑主程序初始化外设后就在循环中检查adc_ready标志。一旦发现标志为1就读取adc_values数组中的四个值将其转换为电压并打印然后清除标志。#include stm32f10x.h #include stdio.h // 用于printf #include usart.h // 假设你有串口初始化代码 extern volatile uint16_t adc_values[4]; extern volatile uint8_t adc_ready; int main(void) { float voltage[4]; // 初始化系统时钟、串口用于打印、ADC等 SystemInit(); USART1_Config(); // 你的串口初始化函数 ADC_GPIO_Config(); ADC_Mode_Config(); printf(四通道ADC循环采集系统启动...\r\n); while(1) { if (adc_ready 1) { adc_ready 0; // 清除标志 // 计算电压值 (Vref 3.3V, 12位ADC) for (int i 0; i 4; i) { voltage[i] (float)adc_values[i] * 3.3f / 4096.0f; } // 通过串口打印结果 printf(Ch11: %.3fV, Ch5: %.3fV, Ch8: %.3fV, Ch0: %.3fV\r\n, voltage[0], voltage[1], voltage[2], voltage[3]); } // 这里可以执行其他任务ADC采集在后台由中断自动处理 // 例如LED闪烁、按键扫描、算法计算等 } }通过这个完整的例子你应该能清晰地看到多通道ADC配置、中断处理和数据流转的整个流程。在实际项目中你可能还需要考虑数字滤波如滑动平均、数据校验、低功耗触发模式等更高级的主题。但掌握了这个基础框架你就已经具备了处理大多数多通道模拟信号采集任务的能力。

相关新闻