STM32F103 ADC多通道连续采样工程实践
ADC(模数转换器)是嵌入式系统中实现模拟信号数字化的核心外设,其性能直接受参考电压、采样时间、时钟配置及通道切换机制影响。理解SAR型ADC的工作原理与硬件约束,如RC采样时间匹配、VREF+稳定性、ADCCLK频率上限(≤14MHz)等,是保障12位精度的基础。在工程实践中,HAL库配置需协同RCC时钟分频、GPIO模拟输入模式、DMA循环搬运与双缓冲机制,以支撑多通道连续采样场景。典型应用涵
11. ADC:从寄存器映射到多通道连续采样工程实践
在嵌入式系统中,模拟信号数字化是传感器数据采集、电源监控、环境参数测量等场景的基础环节。STM32F103C8T6作为一款成熟可靠的主流MCU,其内置的12位逐次逼近型(SAR)ADC模块具备良好的精度、灵活的触发机制和丰富的数据管理能力。但实际工程中,ADC配置远非“使能时钟→设置分辨率→启动转换”三步所能概括。本章将脱离抽象理论,聚焦于真实项目中必须面对的细节:参考电压选择对量程的实际约束、采样时间与信号源阻抗的匹配关系、规则组与注入组的职责划分、DMA搬运过程中地址对齐引发的数据错位、以及多通道连续采样下如何规避通道切换引入的瞬态误差。所有内容均基于STM32F103C8T6数据手册Rev12及HAL库v1.8.4实现,不依赖任何第三方抽象层。
11.1 ADC硬件架构与关键参数解析
STM32F103C8T6集成单个12位ADC,支持最多18个外部输入通道(CH0–CH17),其中CH0–CH15对应GPIO引脚,CH16–CH17为内部温度传感器与VREFINT基准电压。该ADC并非独立外设,其性能直接受制于三个核心硬件参数:供电电压VDDA、参考电压VREF+、以及ADC时钟(ADCCLK)频率。
VDDA与VREF+的关系决定有效量程
VDDA为ADC模拟部分供电引脚,必须稳定在2.4V–3.6V范围内。VREF+则作为ADC转换的上限基准。当VREF+直接连接VDDA时(典型应用),输入信号范围为0–VDDA;若外部接入精密基准(如2.5V),则量程缩为0–2.5V,但可提升绝对精度。需特别注意:VDDA与VCC(数字供电)之间必须通过100nF陶瓷电容去耦,且VREF+引脚需外接100nF电容至地,否则高频噪声将直接调制转换结果。实测中,若省略VREF+去耦电容,12位结果的最低3位将呈现随机跳变。
ADCCLK频率约束转换精度与速度
ADCCLK由APB2总线时钟(PCLK2)经预分频器产生。根据数据手册Table 59,当ADCCLK > 14MHz时,由于内部采样电容充电时间不足,信噪比(SNR)将显著劣化。因此,即使PCLK2为72MHz,也必须通过ADC预分频器(RCC_CFGR位ADCPRE[1:0])将其降至≤14MHz。常见配置为PCLK2=72MHz → ADCCLK=72MHz/6=12MHz。此频率下,单次转换时间为12.5个ADCCLK周期(1.04μs),满足绝大多数传感器响应需求。
采样时间(Sampling Time)的本质是RC时间常数匹配
ADC内部采样保持电路等效为一个开关、一个采样电容(Csamp≈8pF)与一个导通电阻(Ron≈5kΩ)。当外部信号源存在输出阻抗(Rs)时,采样阶段实际构成RC充电回路,时间常数τ=(Rs+Ron)×Csamp。若采样时间设置过短,Csamp无法充至输入电压,导致转换值偏低。数据手册明确要求:对于Rs≤10kΩ的信号源,最小采样时间为1.5周期;Rs在10kΩ–100kΩ间需7.5周期;Rs>100kΩ则需23.5周期。例如,读取电位器(Rs≈10kΩ)时,若误用1.5周期采样时间,实测结果比理论值低约12LSB;改用7.5周期后误差收敛至±1LSB内。
11.2 HAL库ADC初始化:从时钟使能到结构体填充
HAL库将ADC配置解耦为时钟控制、GPIO配置、ADC外设初始化三个层级。此解耦虽提升可移植性,但也隐藏了底层依赖关系,需工程师主动厘清。
第一步:APB2总线时钟使能与ADCCLK分频
ADC挂载于APB2总线,其时钟使能位位于RCC_APB2ENR寄存器bit9(ADC1EN)。但仅使能时钟不够——必须同步配置ADCCLK分频比。此操作在 HAL_RCC_ADCCLKConfig() 函数中完成,本质是修改RCC_CFGR寄存器的ADCPRE字段。错误实践是仅调用 __HAL_RCC_ADC1_CLK_ENABLE() 而忽略分频配置,此时ADCCLK等于PCLK2(72MHz),超出14MHz限值,导致转换结果不可靠。
// 正确的时钟初始化顺序
__HAL_RCC_ADC1_CLK_ENABLE(); // 使能ADC1时钟
HAL_RCC_ADCCLKConfig(RCC_ADCPCLK2_DIV6); // 配置ADCCLK = PCLK2 / 6 = 12MHz
第二步:ADC通道引脚复用配置
以PA0(ADC1_IN0)为例,其配置需满足三重条件:
1. GPIOA时钟使能(RCC_APB2ENR bit2)
2. PA0模式设为模拟输入(GPIOA_MODER bit[1:0]=0b11), 禁止上拉/下拉 (GPIOA_PUPDR bit[1:0]=0b00),因上拉电阻会与信号源形成分压,改变实际输入电压
3. 复用功能选择(GPIOA_AFRL bit[3:0]=0b0000,对应AF0即ADC功能)
HAL库中通过 GPIO_InitTypeDef 结构体实现:
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; // 关键:必须为ANALOG模式
GPIO_InitStruct.Pull = GPIO_NOPULL; // 关键:禁止上下拉
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
第三步:ADC初始化结构体关键字段解析 ADC_HandleTypeDef 结构体中的 Init 成员包含决定ADC行为的核心参数,其设置逻辑需严格遵循数据手册约束:
| 字段 | 典型值 | 工程目的 | 原理说明 |
|---|---|---|---|
ClockPrescaler |
ADC_CLOCK_SYNC_PCLK_DIV4 |
控制ADCCLK分频 | 此处为二次分频,与RCC_CFGR的ADCPRE共同决定最终ADCCLK |
Resolution |
ADC_RESOLUTION_12B |
设置转换精度 | 12位模式下,DR寄存器低12位有效;6/8/10位模式会右移并补零 |
DataAlign |
ADC_DATAALIGN_RIGHT |
数据对齐方式 | 右对齐时12位结果存于DR[11:0],左对齐则存于DR[15:4],影响后续数据处理逻辑 |
ScanConvMode |
ENABLE |
启用扫描模式 | 多通道采样必需,否则仅转换第一个通道 |
EOCSelection |
ADC_EOC_SEQ_CONV |
中断触发条件 | SEQ_CONV 表示整组转换结束触发中断, SINGLE_CONV 为单次转换结束 |
LowPowerAutoWait |
DISABLE |
自动延时等待 | 启用后ADC在转换间隙自动进入低功耗,但会增加通道切换延迟,实时性敏感场景禁用 |
完整初始化代码示例:
ADC_HandleTypeDef hadc1;
ADC_ChannelConfTypeDef sConfig = {0};
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; // ADCCLK = 72MHz/4 = 18MHz? 错!需先经RCC_CFGR分频
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.ScanConvMode = ENABLE; // 多通道必需
hadc1.Init.EOCSelection = ADC_EOC_SEQ_CONV; // 整组结束触发
hadc1.Init.LowPowerAutoWait = DISABLE;
hadc1.Init.ContinuousConvMode = ENABLE; // 连续转换模式
hadc1.Init.NbrOfConversion = 2; // 规则组含2个通道
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; // 软件触发
hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
hadc1.Init.DMAContinuousRequests = ENABLE; // DMA连续请求
hadc1.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN; // 溢出时覆盖旧数据
HAL_ADC_Init(&hadc1);
// 配置通道0(PA0)
sConfig.Channel = ADC_CHANNEL_0;
sConfig.Rank = 1; // 在规则组中排第1位
sConfig.SamplingTime = ADC_SAMPLETIME_7CYCLES_5; // 匹配10kΩ信号源
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
// 配置通道1(PA1)
sConfig.Channel = ADC_CHANNEL_1;
sConfig.Rank = 2; // 在规则组中排第2位
sConfig.SamplingTime = ADC_SAMPLETIME_7CYCLES_5;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
11.3 规则组与注入组:双轨数据采集策略
ADC的规则组(Regular Group)与注入组(Injected Group)并非简单的“主备”关系,而是针对不同实时性与优先级需求设计的并行转换引擎。理解其差异是构建可靠采集系统的关键。
规则组:高吞吐量、周期性数据流
规则组专为持续、规律的传感器数据采集设计。其特性包括:
- 支持1–16个通道按序扫描,转换结果按序存入ADC_DR寄存器(单通道)或DMA缓冲区(多通道)
- 可配置为单次/连续/扫描模式,连续模式下自动重复整组转换
- EOC(End of Conversion)标志在整组转换完成后置位,适合批量数据处理
注入组:高优先级、事件驱动的紧急采样
注入组设计初衷是响应突发事件(如过压告警),其特性包括:
- 最多支持4个通道,可随时抢占规则组转换(需启用 ADC_INJECTED_AUTO )
- 转换结果存入专用寄存器JDR1–JDR4,避免与规则组数据混淆
- 支持四种触发源:外部引脚、定时器、看门狗,或软件强制触发
工程场景选择指南
- 温湿度传感器(DHT22) :采用规则组连续扫描。因DHT22为数字传感器,ADC不适用;此处指代模拟输出的SHT30等。其数据更新率低(1–10Hz),规则组连续模式配合DMA搬运可实现零CPU干预。
- 电机电流检测(Shunt Resistor) :必须使用注入组。当FOC算法检测到过流风险时,需在微秒级内强制触发电流采样,此时注入组可立即中断当前规则组转换,确保采样时刻精准。
- 电池电压监控 :规则组与注入组协同。常规周期性监测走规则组;当系统进入低功耗模式前,通过软件触发注入组快速采样一次,避免唤醒整个系统。
注入组配置要点
注入组通道配置独立于规则组,需单独调用 HAL_ADCEx_InjectedConfigChannel() 。关键参数 InjectedNbrOfConversion 指定注入序列长度, InjectedChannel 指定通道号, InjectedRank 指定序列位置。若启用自动注入( ADC_INJECTED_AUTO ),则每次规则组转换结束时自动启动注入序列,此模式适用于需要严格时间间隔的差分采样(如CH0-CH1)。
11.4 DMA搬运:解决CPU瓶颈与数据一致性
在12位ADC以10kHz速率采样时,每秒产生20KB原始数据。若依赖中断逐字节读取ADC_DR,CPU将100%占用。DMA(Direct Memory Access)是唯一可行方案,但其配置存在易被忽视的陷阱。
DMA通道与请求映射
ADC1的规则组转换完成事件(EOC)映射至DMA1通道1,注入组映射至DMA1通道2。需在 MX_DMA_Init() 中配置:
- hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY
- hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE (外设地址固定)
- hdma_adc1.Init.MemInc = DMA_MINC_ENABLE (内存地址递增)
- hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD (ADC_DR为16位寄存器)
- hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD
- hdma_adc1.Init.Mode = DMA_CIRCULAR (循环模式,避免DMA传输完成中断)
- hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH
双缓冲机制规避数据覆盖
循环模式下,DMA持续向同一缓冲区写入。若CPU处理速度慢于采样速率,新数据将覆盖未处理的旧数据。HAL库提供双缓冲(Double Buffer)模式:DMA在缓冲区A填满后自动切换至B,并触发 HAL_ADC_ConvCpltCallback() 回调;CPU在回调中获取当前满缓冲区指针,处理完毕后调用 HAL_ADC_Start_DMA() 重新指定缓冲区。此机制确保无数据丢失,但需额外RAM开销。
uint16_t adc_buffer_a[1024];
uint16_t adc_buffer_b[1024];
uint16_t *active_buffer = adc_buffer_a;
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
if (hadc->Instance == ADC1) {
// 切换缓冲区指针
if (active_buffer == adc_buffer_a) {
active_buffer = adc_buffer_b;
} else {
active_buffer = adc_buffer_a;
}
// 通知处理任务有新数据
xQueueSendFromISR(adc_data_queue, &active_buffer, NULL);
}
}
地址对齐陷阱:为何数据总是错位?
ADC_DR寄存器地址为0x40012400,为偶数地址。当DMA配置为半字(16位)传输时,内存目标地址必须为偶数,否则触发HardFault。实践中,若定义 uint16_t adc_buffer[1024] 并取其地址,编译器通常保证对齐;但若定义为 uint8_t raw_buffer[2048] 再强制转换,则可能因编译器优化导致奇地址,必须显式对齐:
// 强制16位对齐
uint16_t __attribute__((aligned(2))) adc_buffer[1024];
11.5 多通道连续采样:消除通道切换瞬态误差
当规则组配置为扫描多个通道(如CH0→CH1→CH2)时,ADC内部模拟多路复用器(MUX)切换会产生瞬态响应。若前一通道输入电压高(如3.3V),后一通道输入电压低(如0.1V),MUX切换后采样电容需时间放电,导致首个采样点严重失真。此现象在高速采样中尤为突出。
硬件级解决方案:添加采样保持电路(Sample-and-Hold)
在信号链前端增加专用S/H芯片(如LF398),其工作原理是在ADC启动转换瞬间“冻结”输入电压,为ADC提供稳定采样点。此方案成本增加约¥2,但可彻底消除瞬态误差,适用于医疗设备等高精度场景。
软件级解决方案:丢弃首N个采样点
更经济的做法是在每个通道序列开始后,主动丢弃前2–4个转换结果。HAL库无内置丢弃功能,需在DMA回调中实现:
static uint8_t discard_count = 0;
static uint16_t *buffer_ptr;
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
if (hadc->Instance == ADC1) {
if (discard_count < 4) {
discard_count++;
return; // 丢弃本次数据
}
// 正常处理逻辑
process_adc_data(buffer_ptr);
buffer_ptr += 2; // CH0与CH1各1个值
}
}
校准级解决方案:通道间偏移补偿
ADC各通道存在微小增益与偏移差异。可通过测量已知电压(如VREFINT=1.2V)计算校准系数。例如,CH0测得VREFINT为1245(理想1200),则CH0增益校正因子为1200/1245≈0.964;CH1测得为1190,则校正因子为1200/1190≈1.008。此系数需在数据处理层应用,而非ADC配置层。
11.6 实际项目调试经验:从波形异常到根因定位
在开发一款基于STM32F103C8T6的工业温度采集终端时,曾遇到ADC读数周期性跳变问题。示波器捕获PA0引脚电压稳定在1.85V,但MCU读取的12位值在0x760–0x78F间波动(理论值应为0x770)。排查过程如下:
步骤1:验证参考电压稳定性
万用表测量VREF+引脚电压为3.28V(正常),但示波器观察到100mVpp高频噪声叠加在VREF+上。根源在于VREF+去耦电容虚焊。补焊100nF X7R电容后,噪声消失,ADC值稳定在0x770±1。
步骤2:检查信号源阻抗匹配
温度传感器输出阻抗标称为5kΩ,但实测PCB走线引入额外1kΩ寄生电阻。原配置采样时间为1.5周期,导致Csamp充电不足。将 SamplingTime 改为 ADC_SAMPLETIME_13CYCLES_5 后,波动幅度收窄至±0.5LSB。
步骤3:分析DMA传输完整性
启用DMA传输完成中断,发现中断频率与预期不符。追踪发现 HAL_ADC_Start_DMA() 被重复调用,导致DMA通道重配置冲突。修正为仅在初始化时调用一次,并在 HAL_ADC_ConvCpltCallback() 中不重启DMA,问题解决。
步骤4:确认时钟树配置
最后验证ADCCLK实际频率:使用MCO(Microcontroller Clock Output)引脚输出ADCCLK至示波器,实测为12.002MHz,符合 RCC_CFGR 与 ADC_Init.ClockPrescaler 联合配置,排除时钟误差。
此类问题凸显ADC调试的系统性——它横跨硬件设计(去耦、布线)、时钟配置、外设初始化、DMA调度、甚至PCB工艺。工程师必须建立完整的故障树,而非孤立看待某一层级。
11.7 温度传感器与VREFINT:片上资源的精确利用
STM32F103C8T6内置两个关键模拟资源:温度传感器(TS)与内部参考电压(VREFINT),二者共享ADC通道CH16与CH17。合理利用它们可省去外部元件,但需深入理解其特性。
温度传感器(TS)的校准方法
TS输出电压与温度呈线性关系:Vts = V25 + (T - 25) × Avg_Slope,其中V25为25°C时的典型输出(1.43V),Avg_Slope为平均斜率(4.3mV/°C)。芯片出厂时在V25和V110两点校准,校准值存储于系统存储器(0x1FFFF7E8与0x1FFFF7EA)。实际使用需读取这两个点并线性插值:
// 读取校准值
uint16_t V25_CAL = *(uint16_t*)0x1FFFF7E8;
uint16_t V110_CAL = *(uint16_t*)0x1FFFF7EA;
// 采集CH16(TS)电压
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);
uint16_t adc_ts = HAL_ADC_GetValue(&hadc1);
// 计算实际电压(假设VREF+ = 3.3V)
float vts = (adc_ts * 3.3f) / 4095.0f;
// 计算温度
float temperature = 25.0f + ((vts - (V25_CAL * 3.3f / 4095.0f)) / (V110_CAL - V25_CAL)) * 85.0f;
注意 :此计算假设VREF+精确为3.3V。若VREF+存在偏差,需先用VREFINT校准VREF+。
VREFINT的精确测量与VREF+校准
VREFINT标称值为1.20V,但个体差异可达±10%。其真实值可通过测量VREFINT通道(CH17)获得,并反推VREF+:
// 测量CH17(VREFINT)
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);
uint16_t adc_vref = HAL_ADC_GetValue(&hadc1);
// 计算VREF+实际值:VREF+ = VREFINT_cal * 4095 / adc_vref
// VREFINT_cal为芯片手册给出的校准值(如1.21V)
float vref_actual = 1.21f * 4095.0f / adc_vref;
获得VREF+实际值后,所有ADC读数均可据此重新标定,将系统级误差从±10%降至±1%以内。此步骤在量产校准工装中必不可少。
我曾在一款环境监测设备中忽略VREFINT校准,导致全批次产品温度读数整体偏高2.3°C。返厂通过ISP更新固件加入VREFINT校准流程后,精度恢复至±0.5°C。这印证了一个事实:片上模拟资源的价值不在于“能用”,而在于“精确可控”。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐



所有评论(0)