ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

STM32标准库ADC多路采集:规则组与DMA实现及电压校准

STM32标准库ADC多路采集:规则组与DMA实现及电压校准 简介一套基于STM32标准库的ADC采集学习资源面向正在入门STM32嵌入式开发、需要掌握单通道与多通道模拟信号采集的开发者。资源以Keil工程形式呈现围绕1路、2路、多路ADC采集场景给出标准库下的初始化配置、通道序列扫描、DMA或中断读取等工程化实现可直接对照学习或在此基础上修改复用。压缩包共377个文件包含66个c源码、74个h头文件以及大量编译生成的o/d/crf中间文件另有uvprojx工程文件、map/axf/lst链接输出等整体结构是一套完整可编译的STM32标准库例程包大小9.56MB。目前已有382人学习下载。结合内容预览可知工程还涉及定时器、Flash、LCD等模块适合作为综合实验模板通过梳理ADC配置参数、采样时序与多路切换思路可帮助读者快速从单路采集扩展到多路应用减少重复踩坑。1. 标准库视角下的 STM32 ADC 采集从 1 路到多路的关键分界玩标准库的人早晚会卡在 ADC 多路采集上单路轮询明明好使把通道切换代码怼上去读回来的数据要么永远是第一个通道的要么多通道数据互相串。这不是代码敲错而是对 STM32 ADC 的规则组和 DMA 机制理解没到位。下面这套流程不绕理论直接从 STM32 标准库的 ADC 采集入手先把 1 路由规则组连续转换读通再把 2 路、多路接到 DMA 的搬运链上最后解决数据错位、电压换算和采样周期这几件工程里绕不开的事。适合正在用标准库 v3.5 做裸机开发的读者Keil 环境下可以直接跟着改。2. STM32 ADC 的底层机制与标准库初始化骨架2.1 规则组只有一个数据寄存器多路采集的天然瓶颈STM32F103 的 ADC 是 12 位逐次逼近型转换器转换完成后结果放在一个 16 位的数据寄存器里。以 ADC1 为例它最多有 18 个通道其中 16 路来自 GPIO、两路来自芯片内部温度传感器和内部参考电压 Vrefint。这些通道不能同时转换必须按“规则组”里的顺序排队。规则组最多能排 16 个通道转换时从序号 1 开始依次执行每个通道转换完EOC 标志位置位结果写入同一个寄存器 ADC_DR。这里就是新手最常见困惑的来源规则组不是“一个通道一个寄存器”而是“所有通道共用一个寄存器”。如果你在扫描模式下开了 2 路第 1 路转换完把结果写进 DR第 2 路转换完又把结果写进同一个 DR上一次的数据就被覆盖。所谓多路采集本质上要解决的是“在 DR 被覆盖之前把结果搬走”。理解这一点后面看 DMA 配置就不会觉得那些参数是死记硬背。2.2 标准库新建工程与 ADC 时钟链路三个 RCC 别漏标准库 v3.5 新建工程时外设库、启动文件和 Device 头文件的搭法和常规 GPIO 工程一样但 ADC 初始化里 RCC 配置最容易漏。F103 的 ADC 时钟从 APB2 来使用前必须做两件事打开 ADC1 的 APB2 时钟再通过 RCC_ADCCLKConfig 设置分频。GPIO 也要给时钟但要把引脚模式设成 GPIO_Mode_AIN模拟输入模式下推挽、开漏、上下拉全部不生效这是 ADC 和普通 IO 初始化的显著区别。RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure);这段代码的前提是 APB2 已经跑到 72MHz。F103 的 ADC 时钟上限是 14MHz所以 ADCCLK 的分频要从 Div6 起步12MHz 是最常用的一档。把 RCC_PCLK2_Div2 或 Div4 用上去ADC 时钟超限采样值可能完全不正常。下面这张表是 72MHz 主频下的常见分频选择新建工程时可以直接对着选。RCC_ADCCLKConfig 参数实际 ADC 时钟是否可用RCC_PCLK2_Div236MHz超限不可用RCC_PCLK2_Div418MHz超限不可用RCC_PCLK2_Div612MHz常用RCC_PCLK2_Div89MHz可用精度损失可忽略如果你用的是 STM32F407 的 ADC 工程标准库的时钟配置思路一样但分频系数和上限不同F4 的 ADC 时钟允许到 36MHz而且时钟源配置还涉及 PLL 的输出不能直接把这里的 Div6 抄过去。标准库里 ADC_InitTypeDef 结构体的每个成员都对应 ADC_CR1、ADC_CR2 和 ADC_SQR1 的某几个位逐个赋值比直接改寄存器可读性好这也是标准库在调试期的优势。另外在 Keil 里建标准库工程时记得把 stm32f10x_adc.c 和 stm32f10x_dma.c 加进外设库源文件组并把头文件路径补上不然编译时会报 implicit declaration这是标准库新建工程最常见的报错跟 ADC 本身无关但拦住了不少人。2.3 通道要排队ADC_RegularChannelConfig 的 Rank 不是物理编号ADC 引脚确定后还要把通道排进规则组序列标准库的做法是调用 ADC_RegularChannelConfig(ADC1, ch, rank, sampleTime)。ch 是物理通道号比如 ADC_Channel_0 对应 PA0rank 是转换顺序第几个转由 rank 决定。一个通道可以在规则组里出现多次rank 也可以不连续但相同的 rank 同时挂两个物理通道时后配置的那个会覆盖先配置的这个细节在动态切换通道时尤其坑。初始化函数本身要处理的东西不多只有一个 ADC_InitTypeDef 和一次校准。整体顺序我一般这样组织先 RCC再 GPIO然后 ADC_Init开 ADC最后做一次标准校准再启动转换。校准必须等两个标志位完成否则首次采集值会偏。ADC_InitTypeDef ADC_InitStructure; ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode DISABLE; ADC_InitStructure.ADC_ContinuousConvMode DISABLE; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1));参数说明放这里ADC_ScanConvMode 决定能不能一次扫描多个通道单路先关掉ContinuousConvMode 开起来后规则组转完一轮会自动从头再来一遍ExternalTrigConv_None 表示使用软件触发后面用 ADC_SoftwareStartConvCmd 启动转换DataAlign_Right 右对齐12 位有效数据落在寄存器低 12 位。这一套是标准库 ADC 工程的最小骨架单路和多路只差 Scan、通道数和 DMA 三处。3. 标准库实现 ADC 单路采集连续转换与电压换算3.1 轮询读取是最小实现EOC 标志与数据寄存器单路采集不需要 DMA最简单可靠的方式是软件触发加轮询。每次请求一个通道时先重新配置一次规则组序列然后启动转换等待 EOC 标志置位最后从 ADC_GetConversionValue 读值。标准库里这个函数返回的是数据寄存器右对齐后的值直接就能用。u16 adc_read_single(u8 adc_ch) { ADC_RegularChannelConfig(ADC1, adc_ch, 1, ADC_SampleTime_55Cycles5); ADC_SoftwareStartConvCmd(ADC1, ENABLE); while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) RESET); return ADC_GetConversionValue(ADC1); }这里有几个点值得说透。第一每次调用都重新配置 rank 是有意的不是多余操作这样同一个函数想采哪个通道就采哪个不用为每个通道写一套逻辑。第二触发命令只需要在软件触发模式下用如果用了定时器触发要改成等待触发事件标志。第三EOC 标志在读完 ADC_DR 后自动清零如果你在 while 循环里只查标志不读寄存器第一次查完是好的第二次开始就永远等不到新的 EOC。实际项目里我不会在中断或者主循环里频繁调用这个函数做连续采样它的定位是“按需单次采样”。比如按键按下时读一次电池电压、进入某个状态时读一次光敏传感器这种低频场景轮询完全够用。如果要求持续输出就要看下一节的连续模式。3.2 连续模式与单次模式的取舍看场景而不是照抄标准库里 ADC_ContinuousConvMode 一旦打开规则组转换完成一轮后不会停而是自动重新开始下一轮。这个模式看起来省事实际上它对读取方提出了要求EOC 置位后 DR 里的数据只能保持到下一次转换完成如果读取不及时拿到的可能已经是下一通道或者下一轮的数值。轮询读连续模式反而容易因为主循环调度不稳定而丢数据。使用方式单次模式连续模式按需读一次合适功耗低浪费转换停不下来持续输出 轮询需要反复触发有覆盖风险不推荐持续输出 DMA每轮触发一次标准做法推荐结论很明确连续模式基本是给 DMA 准备的。单路场景要连续读我一般也直接把 DMA 方案提前用上而不是靠轮询去追 EOC多路场景更是如此。如果你只在中断里读可以在 EOC 中断回调里立即把 ADC_GetConversionValue 的结果存进全局变量理论上可行但中断频率和主循环读取之间的竞争条件需要自己小心。3.3 电压换算的精度边界别把 3.3V 当成真理ADC 读回来的数本身没有物理意义必须换算成电压。12 位分辨率下满量程是 4096 个分度理论公式是 V adc_val / 4096 * Vref。Vref 在这里是 VDDA不是某个你脑补的 3.3V。很多板子 VDDA 直接接 3.3V但 LDO 输出、电源纹波和 PCB 走线都会让实际 VDDA 偏离标称值所以换算结果天生带着误差。提高换算精度的一个常见做法是用精密基准源反推 Vref。具体操作是把一个已知电压比如 2.048V接到某个 ADC 通道多读几次取平均然后用公式反解 Vref 已知电压 * 4096 / adc_val把算出来的 Vref 固化成宏。这样做不用改硬件适合要求几毫伏级一致性但又没条件上外部基准的场合。后面第 5 章还有基于内部参考电压的修正方案和这个思路互补。4. 标准库实现 ADC 2 路与多路采集规则组扫描与 DMA 搬运4.1 为什么 2 路以上的主流方案一定是 DMA回到第 2 章说的那个瓶颈规则组只有一个数据寄存器多通道转换结果在 DR 里排队等待读取而下一个通道的完成会覆盖之前的数据。理论上你可以用 EOC 中断每次转换完成进中断把 DR 读走实际做 2 路以上时中断频率会变得很高而且中断响应延迟直接决定你能跑多快的采样率。标准库时代做多路 ADC 采集我基本没见有人用“纯中断逐次搬运”主流做法是把 DMA 通道挂到 ADC1 的 DMA 请求上。DMA 解决的是“自动搬运”问题每次规则通道转换完成ADC 会发出 DMA 请求DMA 控制器把 ADC_DR 里的 16 位数据搬到内存数组整个过程不需要 CPU 参与。2 路是多路的最小形态DMA 缓冲区长度设 2 就能跑多路也就是把缓冲区长度和规则组序列同步扩展。理解了规则组和 DMA 的配合2 路和比如 8 路的配置差距只有一个宏。4.2 多路 DMA 初始化以 3 路为例2 路改一个宏下面是完整的多路初始化代码通道采样接口是 PA0、PA1、PA2对应 ADC 的通道 0、1、2。2 路场景把 ADC_CH_NUM 改成 2去掉 PA2 对应的 GPIO 和第三个通道配置即可。#define ADC_CH_NUM 3 u16 g_adc_buf[ADC_CH_NUM]; void ADC_Multi_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; DMA_InitTypeDef DMA_InitStructure; ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); /* ADC1 的 DMA 请求默认挂在 DMA1 的 Channel1 上 */ DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr (u32)((ADC1-DR)); DMA_InitStructure.DMA_MemoryBaseAddr (u32)g_adc_buf; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize ADC_CH_NUM; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel1, DMA_InitStructure); DMA_Cmd(DMA1_Channel1, ENABLE); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; 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 ADC_CH_NUM; ADC_Init(ADC1, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE); }代码里值得画重点的参数有四个。DMA_DIR_PeripheralSRC 表示外设是数据源方向是从 ADC 外设读到内存PeripheralDataSize 和 MemoryDataSize 都配成 HalfWord是因为 ADC_DR 是 16 位寄存器半字正好对应一个通道的采样值内存数组必须用 u16MemoryInc 打开让 DMA 每搬一次自动把目的地址向前移一个单位Circular 环形模式在缓冲区填满后自动回绕到数组开头和 ADC 连续扫描正好配对不需要手动重装。规则组配置里有个关键对应关系务必记牢rank 为 1 的通道转换结果一定落在 g_adc_buf[0]rank 为 2 落在 g_adc_buf[1]依此类推。很多人调完发现数据对不上十有八九是把物理通道号当成数组下标了。比如你把 ADC_Channel_1 配成 rank 1那 PA1 的电压就在 g_adc_buf[0]而不是 g_adc_buf[1]。初始化顺序上DMA 的使能要先于 ADC 转换启动ADC 的 DMA 请求开关ADC_DMACmd要在 ADC_Cmd 打开之前设置好。标准库对顺序没有强约束但先 DMA 后 ADC 这个习惯能避免极少数情况下启动转换后 DMA 请求没人接的问题。校准放在 ADC 使能之后、软件触发之前等待标志位完成再启动首次数据才可信。4.3 读数据不是简单地直接访问数组标志位与临界保护DMA 一直在后台写 g_adc_buf主循环读这个数组时会把一个 16 位值分成两次内存访问而 DMA 写入也可能发生在任意时刻。假如主循环正读到 g_adc_buf[0] 的低字节DMA 刚好把下一次转换结果写进来读出来的就是一个新旧拼接的数。单次偶尔发生看不出来多路扫描下数组越大错位概率越明显。最省事的保护方式是查询 DMA 传输完成标志再读数。在环形模式下缓冲区被填满一次就置一次 TC1 标志读到标志后把它清掉再取数组值能保证这一轮的三个值都是同一个扫描周期产出的。if(DMA_GetFlagStatus(DMA1_FLAG_TC1) SET) { DMA_ClearFlag(DMA1_FLAG_TC1); process_adc_values(g_adc_buf, ADC_CH_NUM); }更稳的做法是开 DMA 的半传输和传输完成中断在中断里把 g_adc_buf 复制到一块独立的内存主循环只处理副本。标准库可以用 DMA_ITConfig(DMA1_IT_HT1 | DMA1_IT_TC1, ENABLE) 开启两个中断复制进缓冲区用 memcpy 即可。这个方案能让主循环完全不阻塞代价是多占一块内存多路数越大越划算。对应中断服务函数是 DMA1_Channel1_IRQHandler在标准库里通过 DMA_GetITStatus(DMA1_IT_HT1) 和 DMA_GetITStatus(DMA1_IT_TC1) 区分半满和全满事件。中断请求宏触发时机适合做什么DMA1_IT_HT1缓冲区前半已填满搬运前半份数据DMA1_IT_TC1整个缓冲区更新完搬运全部数据更新业务值顺带提一句如果你用的是 HAL 库工程ADC 多路 DMA 的配置会把外设句柄和 DMA 句柄关联起来初始化入口不同但底层寄存器位语义和这里完全一致。标准库的好处是寄存器看得见出问题容易定位这也是我建议学习阶段不要直接跳 HAL 的原因。5. 采样周期、Vrefint 校准与多路验证技巧5.1 采样周期不是随便抄和信号源内阻强相关标准库把采样时间做成了枚举参数从 1.5 个周期到 239.5 个周期一档一档往上加。采样时间直接影响输入电容充电的窗口信号源内阻越大需要的采样时间越长。下面这张表是 12MHz ADC 时钟下的时间与适用场景。采样时间参数周期数单通道耗时含 12.5 周期量化适用场景ADC_SampleTime_1Cycles51.5约 1.2us低内阻信号源、快速采样ADC_SampleTime_55Cycles555.5约 5.7us常规传感器输出最常用ADC_SampleTime_239Cycles5239.5约 21us高内阻信号、电池电压多路扫描时每一路都会完整走完“采样量化”总时间基本是单通道时间乘以路数。3 路都用 55.5 周期时一轮完整扫描约 17us换算过来每组数据 58k 次/秒绝大多数传感器场景都够用。如果你把采样周期设到 1.5速度上去了采样电容没充满采样值会在几个 LSB 之间乱跳肉眼看到的现象就是“读数不稳但平均一下又像对的”。5.2 用 Vrefint 修正 VDDA比换精密电源更省事F103 内置了一个 1.2V 左右的参考电压 Vrefint它被接到内部通道 17 上。把 ADC_RegularChannelConfig 的通道参数改成 ADC_Channel_Vrefint 采一次理论上会得到一个接近固定值的 ADC 原始读数。如果 VDDA 实际低于 3.3V同一个 Vrefint 的读数会变大反过来就能推出 VDDA 实际值Vref_actual 1.20 * 4096 / vrefint_val。用这个实测 Vref 去重算第 3 章的电压公式可以消除电源电压偏移带来的系统性误差。注意 Vrefint 的 1.2V 本身有制造偏差严格校准还得查数据手册里的精度指标但做量产前的粗校准已经比写死 3.3V 强很多。内部温度传感器通道 16 也可以参与验证多路采集配置里加一路内部通道能顺便确认规则组配置没有把内部通道的 rank 挤乱。5.3 多路通道错位的终极验证三个已知电压解决争端验证多路配置是否真的按 rank 对应正确与其反复看代码不如接三个已知电压实测一次。比如通道 0 接 1.0V通道 1 接 2.0V通道 2 接 3.3V 的三分之一电压附近通过串口把 g_adc_buf 打出来看比例。如果数组下标和预期电压对不上改 rank 顺序就行。反过来如果电压能对上但数值波动超过十几个 LSB先查采样周期是否太短再查电源地和信号源地之间有没有共地问题。把“接三个已知电压再写校正表”当成多路 ADC 量产的固定动作比事后抓数据错位快得多。本文还有配套的精品资源点击获取
返回列表