1. 单通道ADC采集实验:从原理到工程实现

在嵌入式系统开发中,模数转换器(ADC)是连接物理世界与数字处理单元的关键桥梁。无论是读取电位器电压、监测电池电量,还是采集传感器信号,ADC的配置精度与稳定性直接决定了整个系统的感知能力。本节以STM32F103系列微控制器为平台,聚焦单通道ADC采集这一最基础也最常被忽视的典型场景,深入剖析其背后的技术逻辑与工程实现细节。不同于泛泛而谈的API调用说明,本文将从芯片级寄存器行为出发,还原每一个配置项背后的硬件约束与设计权衡,帮助工程师建立可迁移、可调试、可优化的ADC工程思维。

1.1 实验功能定义与硬件映射

本实验的核心目标明确而具体:通过STM32F103的ADC1外设,采集开发板板载电位器(通常为10kΩ线性电位器)滑动端输出的模拟电压,并将转换结果以数字量及对应的实际电压值形式显示在LCD屏幕上。该功能看似简单,却完整覆盖了ADC应用的全部关键环节——信号接入、参考基准设定、时序配置、数据读取与标定计算。

硬件层面,该电位器一端接VDDA(模拟电源,通常为3.3V),另一端接地,滑动端连接至MCU的GPIOA_Pin0引脚。根据STM32F103的数据手册,GPIOA_Pin0在复用功能下对应ADC1的通道0(ADC1_IN0)。这一映射关系是配置的起点,任何后续的寄存器操作都必须基于此物理连接进行。值得注意的是,VDDA必须与VDD(数字电源)保持良好去耦,且在高精度应用中,应避免使用VDDA作为ADC参考电压(VREF+),而应优先选用独立的、低噪声的外部基准源或内部高精度基准(如STM32F103的1.2V内部基准)。本实验采用VDDA=3.3V作为VREF+,这既是开发板的默认设计,也是理解基准电压对转换精度影响的最佳切入点。

1.2 分辨率与量化精度:最小刻度的物理意义

ADC的分辨率决定了其区分输入电压微小变化的能力,其本质是将参考电压范围划分为若干等份。STM32F103的ADC为固定12位分辨率,这意味着其数字输出范围为0x0000至0x0FFF(即0至4095),共4096个离散等级。因此,其理论最小可分辨电压(LSB,Least Significant Bit)由下式决定:

$$
LSB = \frac{V_{REF+} - V_{REF-}}{2^{N}} = \frac{3.3V - 0V}{4096} \approx 0.8057mV
$$

这个0.8057mV并非一个抽象的数学概念,而是具有明确物理意义的工程约束。它意味着,当输入电压变化小于约0.8mV时,ADC的数字输出值将不会发生改变;反之,任何大于此值的变化,理论上都能被检测到。在实际项目中,若需测量毫伏级的微弱信号,仅靠提高分辨率是不够的,还需考虑前端运放的增益与噪声、PCB布局的干扰抑制以及ADC自身的积分非线性(INL)和微分非线性(DNL)误差。对于本实验的电位器应用,0.8mV的精度已远超需求,但理解其来源是进行任何精度分析的基础。

需要强调的是,分辨率并非一成不变的“性能参数”。在STM32F4/F7/H7等更高阶系列中,ADC分辨率可通过寄存器配置为8/10/12/14/16位,此时分母将变为$2^8$、$2^{10}$、$2^{12}$、$2^{14}$或$2^{16}$(即256、1024、4096、16384、65536)。例如,在STM32H743上配置为16位模式时,若VREF+仍为3.3V,则LSB将缩小至约50.3µV。这种灵活性带来了精度提升,但也伴随着采样时间延长、功耗增加等代价,工程师必须在系统需求与资源消耗间做出权衡。

1.3 转换时序:采样时间与ADC时钟的协同

ADC的转换过程绝非瞬时完成,它是一个包含“采样”与“转换”两个阶段的严格时序过程。其总转换时间(T CONV )由两部分构成:
1. 采样时间(T SAMP :ADC内部采样保持电路(S&H)对输入引脚电压进行“捕获”并稳定所需的时间。
2. 转换周期(T CLK :ADC核心执行逐次逼近(SAR)算法,将采样到的模拟电压量化为数字量所需的时间,以ADC时钟周期(ADCCLK)为单位。

对于STM32F103,其转换周期固定为12.5个ADCCLK周期。因此,总转换时间公式为:
$$
T_{CONV} = T_{SAMP} + 12.5 \times T_{CLK}
$$

其中,$T_{CLK} = \frac{1}{f_{ADCCLK}}$。本实验中,系统时钟(SYSCLK)配置为72MHz,通过RCC预分频器将ADCCLK设置为12MHz($f_{ADCCLK} = \frac{72MHz}{6} = 12MHz$),故$T_{CLK} \approx 83.33ns$。

采样时间则由软件配置,其可选值在STM32F103中为1.5/7.5/13.5/28.5/41.5/55.5/71.5/239.5个ADCCLK周期。选择239.5周期作为最大值,其物理意义在于:更长的采样时间允许采样电容有更充分的时间充电至输入电压,从而在输入阻抗较高(如本实验的电位器滑动端)或存在较长走线电容的情况下,确保采样精度。将239.5代入公式:
$$
T_{CONV} = 239.5 \times 83.33ns + 12.5 \times 83.33ns = 252 \times 83.33ns \approx 21\mu s
$$

这个21微秒的转换时间,是系统实时性的基本约束。它意味着,若采用轮询方式等待转换完成,CPU在此期间将无法响应其他高优先级任务;若采用中断方式,则需确保中断服务程序(ISR)的执行时间远小于此值,否则将导致后续转换被覆盖。在实际项目中,若系统要求10kHz以上的采样率(即周期<100µs),则21µs的转换时间完全满足;但若要求100kHz采样率(周期<10µs),则必须降低采样时间或提高ADCCLK频率(需确保不超过ADC最大工作频率72MHz/6=12MHz的限制),这往往以牺牲精度为代价。

1.4 工作模式选择:单次、非扫描与软件触发

针对本实验“仅采集一个通道”的单一需求,ADC的工作模式应被精简至最高效的状态,避免任何不必要的硬件开销与软件复杂度。这体现在三个关键配置上:

  • 禁用扫描模式(Scan Mode Disabled) :扫描模式用于按预设序列自动转换多个通道。当仅需转换一个通道时,启用扫描模式不仅浪费时钟周期(因需配置序列长度、顺序等),还会引入额外的通道切换延迟。因此,必须将 ADC_CR1 寄存器中的 SCAN 位清零。
  • 选择单次转换模式(Single Conversion Mode) :连续模式(Continuous Mode)会使ADC在一次转换完成后立即启动下一次,形成不间断的转换流。这对于需要高速、持续采样的场景(如音频采集)是必要的,但对于本实验的手动电位器调节场景,它毫无意义且会持续占用ADC资源。因此,应将 ADC_CR2 寄存器中的 CONT 位清零。
  • 采用软件触发(Software Trigger) :触发源决定了ADC何时开始转换。硬件触发(如定时器溢出、外部引脚事件)适用于需要严格同步的场合。本实验无需外部同步,最简单、最可控的方式即是软件触发。通过向 ADC_CR2 寄存器的 SWSTART 位置1,即可手动发起一次转换。这种方式赋予了应用程序完全的控制权,可在LCD刷新、用户按键等任意逻辑点精确发起采样。

这三个模式的组合——单次、非扫描、软件触发——构成了最轻量级的ADC工作状态,其寄存器配置简洁明了,是所有ADC应用的起点与基石。

2. ADC寄存器级配置详解

HAL库封装了底层寄存器操作,但其本质仍是通过对特定内存地址(即寄存器)的读写来控制硬件。要真正掌握ADC,必须理解这些寄存器的功能与相互关系。以下将结合本实验需求,逐一解析关键寄存器的配置逻辑。

2.1 控制寄存器(CR1 & CR2):ADC的“指挥中枢”

ADC_CR1 (Control Register 1)与 ADC_CR2 (Control Register 2)共同构成了ADC的主控中心,几乎所有核心功能都由其位域定义。

  • ADC_CR1 关键位域

    • SCAN (bit 8) :扫描模式使能位。本实验设为 0 ,禁用扫描。
    • AWDIE (bit 6) :模拟看门狗中断使能。本实验未使用模拟看门狗功能,保持默认 0
    • JAWDEN (bit 5) :注入通道模拟看门狗使能。同上,不使用, 0
    • AWDEN (bit 4) :规则通道模拟看门狗使能。同上, 0
    • DISCEN (bit 3) :不连续模式使能。该模式用于在扫描序列中对选定通道进行分组转换,本实验无多通道, 0
    • JDISCEN (bit 2) :注入通道不连续模式使能。 0
    • JAUTO (bit 1) :自动注入使能。 0
    • AWDCH[4:0] (bits 0-4) :模拟看门狗监控通道选择。因看门狗未启用,此字段无关紧要。
  • ADC_CR2 关键位域

    • ADON (bit 0) :ADC使能位。这是ADC工作的总开关,必须在所有配置完成后置1才能启动ADC。 切记,此位应在最后一步设置,否则在配置过程中ADC可能处于不稳定状态。
    • CONT (bit 1) :连续转换模式使能。本实验设为 0 ,选择单次模式。
    • CAL (bit 2) :校准启动位。这是一个只写位,向其写 1 将启动ADC校准流程。校准是ADC上电后的必要步骤,必须在首次转换前执行。
    • RSWSTART (bit 22) :规则通道软件启动位。本实验通过向此位置 1 来触发单次转换。注意,该位为“写1清除”(Write-One-to-Clear, W1C)类型,即写 1 后硬件会自动将其清零,表示启动指令已接收。
    • EXTSEL[2:0] (bits 21-19) :外部触发源选择。本实验使用软件触发,因此将此字段设为 0b111 SWSTART )。
    • EXTTRIG (bit 20) :外部触发使能。本实验禁用外部触发,设为 0
    • ALIGN (bit 11) :数据对齐方式。 0 表示右对齐(推荐), 1 表示左对齐。右对齐时,12位数据位于16位寄存器的低12位(bits 11:0),高位补0,读取时可直接使用;左对齐时,数据位于高12位(bits 15:4),需右移4位才能得到正确数值。本实验采用右对齐,设为 0
    • DMA (bit 8) :DMA请求使能。本实验不使用DMA,设为 0
    • TSVREFE (bit 23) :温度传感器与VREFINT使能。本实验未使用片上温度传感器或内部参考电压,保持 0

2.2 采样时间寄存器(SMPR1 & SMPR2):为信号“留足时间”

ADC的采样精度高度依赖于输入信号在采样电容上的稳定程度。 ADC_SMPR1 ADC_SMPR2 (Sample Time Register 1 & 2)分别负责配置不同通道的采样时间,体现了STM32 ADC设计的灵活性。

  • ADC_SMPR2 :管理通道0至通道9。每个通道分配3位(bits 2:0 for CH0, bits 5:3 for CH1, …, bits 29:27 for CH9),共10组。本实验使用通道0(CH0),因此需配置 SMPR2[2:0]
  • ADC_SMPR1 :管理通道10至通道17。同样,每个通道3位。本实验不涉及这些通道,相关位域可保持默认。

对于CH0,其3位采样时间配置( SMPR2[2:0] )的编码如下:
| SMPR2[2:0] | 采样时间 (ADCCLK cycles) |
| :--------- | :------------------------ |
| 000 | 1.5 |
| 001 | 7.5 |
| 010 | 13.5 |
| 011 | 28.5 |
| 100 | 41.5 |
| 101 | 55.5 |
| 110 | 71.5 |
| 111 | 239.5 |

本实验选择最大值239.5,故将 SMPR2[2:0] 设为 0b111 。这一选择的工程考量在于:电位器滑动端的输出阻抗并非理想零欧姆,其戴维南等效电阻在滑动过程中会变化,最高可达数千欧姆。过短的采样时间可能导致采样电容无法在规定周期内充分充电,从而引入显著的转换误差。239.5周期提供了充足的裕量,确保了在各种电位器位置下的采样精度。

2.3 规则序列寄存器(SQR1-SQR3)与数据寄存器(DR):定义“做什么”与“结果在哪”

ADC_SQRx (Sequence Register)系列寄存器定义了规则通道转换序列的长度与各通道的转换顺序。由于本实验仅使用一个通道,配置极为简化。

  • ADC_SQR1 :该寄存器的高4位( L[15:12] )定义了规则序列的长度(1-16)。本实验只需1次转换,故将 L[15:12] 设为 0b0000 (即0,代表1次转换)。
  • ADC_SQR3 :该寄存器的低5位( SQ1[4:0] )定义了序列中第一个(也是唯一一个)转换的通道号。本实验为通道0,故将 SQ1[4:0] 设为 0b00000

当ADC完成一次转换后,12位的转换结果将被写入 ADC_DR (Data Register)的低12位(bits 11:0)。由于我们已将 ALIGN 位设为 0 (右对齐),读取 ADC_DR 即可直接获得0-4095范围内的原始数字量,无需任何位移运算。这是最直观、最不易出错的数据获取方式。

2.4 状态寄存器(SR):与ADC“对话”的窗口

ADC_SR (Status Register)是应用程序与ADC硬件进行状态同步的唯一接口。它包含了多个标志位,用于指示ADC当前所处的状态。

  • EOC (bit 1) :End of Conversion,转换结束标志。当一次规则通道转换完成时,此位被硬件置 1 。应用程序可通过轮询此位或使能其对应的中断( EOCIE )来获知转换完成。
  • ADON (bit 0) :ADC使能状态位。此位为只读,反映 CR2 ADON 位的实际状态,可用于确认ADC是否已成功启动。

在本实验的轮询模式中,核心逻辑即为:

// 启动转换
SET_BIT(ADC1->CR2, ADC_CR2_SWSTART);

// 等待转换完成
while (RESET_BIT(ADC1->SR, ADC_SR_EOC)) {
    // 空循环等待
}

// 读取结果
uint16_t raw_value = ADC1->DR;

这段代码的本质,就是通过反复读取 ADC_SR 寄存器并检查 EOC 位,来实现与ADC硬件的同步。理解 SR 寄存器的作用,是编写可靠、健壮ADC驱动的第一步。

3. HAL库驱动框架与关键函数剖析

HAL库极大地简化了ADC的初始化与使用流程,但其内部逻辑依然严格遵循前述寄存器配置原理。理解其API的设计意图,有助于在更高层次上驾驭ADC。

3.1 初始化流程:从时钟使能到校准

ADC的初始化是一个严格的、不可逆序的流程,其步骤环环相扣:

  1. 使能ADC时钟与GPIO时钟 :通过 __HAL_RCC_ADC1_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE() 宏,确保ADC外设与GPIO端口的时钟已开启。这是所有外设操作的前提。
  2. 配置GPIO引脚 :将GPIOA_Pin0配置为模拟输入模式( GPIO_MODE_ANALOG )。此步骤至关重要,它断开了GPIO的数字输入/输出缓冲器,防止数字电路噪声耦合至敏感的模拟输入引脚,是保证ADC精度的物理基础。
  3. 调用 HAL_ADC_Init() :该函数是ADC初始化的入口。其核心任务是:
    • 复位ADC(通过向 CR2 0 )。
    • 配置 CR1 CR2 寄存器,包括 SCAN CONT ALIGN EXTSEL 等位。
    • 配置 SMPR1 SMPR2 ,设置各通道采样时间。
    • 最关键一步:执行ADC校准 。校准是消除ADC内部偏移误差(Offset Error)的必要过程。 HAL_ADC_Init() 内部会调用 HAL_ADCEx_Calibration_Start() ,该函数首先置位 CR2 CAL 位启动校准,然后轮询 CR2 CAL 位直至其被硬件清零,表明校准完成。 未经校准的ADC,其转换结果将存在一个固定的、不可忽略的偏移量,导致所有读数系统性偏差。

3.2 通道配置与启动: HAL_ADC_ConfigChannel() HAL_ADC_Start()

HAL_ADC_ConfigChannel() 函数负责配置具体的ADC通道参数,其主要操作对象是 SQRx 寄存器。对于本实验,它将 SQR1 L 字段设为 0 (1次转换),并将 SQR3 SQ1 字段设为 0 (通道0)。

HAL_ADC_Start() 函数则负责启动ADC转换。其核心操作是向 CR2 寄存器的 SWSTART 位置 1 ,发出软件触发指令。该函数本身并不等待转换完成,它只是发出了一个“开始”的命令,体现了HAL库对异步操作的支持。

3.3 数据获取:轮询、中断与DMA的抉择

获取转换结果有三种主流模式,其选择取决于应用的具体需求:

  • 轮询模式( HAL_ADC_PollForConversion() :这是最简单、最易理解的模式。该函数内部即执行前述 while (RESET_BIT(ADC1->SR, ADC_SR_EOC)) 的循环,并在 EOC 置位后读取 DR 寄存器。其优点是逻辑清晰、无中断开销;缺点是CPU在等待期间完全被阻塞,降低了系统整体效率。适用于对实时性要求不高、或作为初学者理解原理的场景。
  • 中断模式( HAL_ADC_Start_IT() :通过使能 EOCIE 中断,当转换完成时,ADC会触发一个中断请求(IRQ)。在中断服务函数(ISR)中,调用 HAL_ADC_IRQHandler() ,该函数会自动清除 EOC 标志并调用用户注册的回调函数 HAL_ADC_ConvCpltCallback() 。此模式解放了CPU,使其可在等待期间执行其他任务,是大多数实时应用的首选。
  • DMA模式( HAL_ADC_Start_DMA() :对于需要高速、大批量数据采集的场景(如波形记录),DMA是最优解。它允许ADC在转换完成后,自动将 DR 寄存器的内容搬运至指定的内存缓冲区,全程无需CPU干预。本实验虽未使用,但理解其存在是为未来扩展打下基础。

4. 电压值计算与工程实践要点

获取到0-4095范围内的原始数字量( raw_value )仅仅是第一步。将其转化为具有物理意义的电压值( voltage ),并最终在LCD上显示,是整个数据链路的终点。

4.1 电压计算:从数字量到物理量

根据ADC的基本原理,转换结果与输入电压呈线性关系:
$$
voltage = \frac{raw_value}{4095} \times V_{REF+}
$$

其中, V_REF+ 即为参考电压。本实验中, V_REF+ = VDDA ≈ 3.3V 。因此,计算公式为:
$$
voltage = \frac{raw_value \times 3.3}{4095}
$$

在嵌入式C语言中,为避免浮点运算带来的性能开销与代码体积膨胀,通常采用定点数运算。一个高效的实现是:

// 将3.3V * 1000 = 3300mV,结果单位为毫伏(mV)
uint32_t voltage_mV = (raw_value * 3300UL) / 4095UL;

此计算将结果放大1000倍,以整数形式表达毫伏值,既保证了精度(约0.8mV的LSB),又规避了浮点运算。最终显示时,再将 voltage_mV 拆分为整数部分与小数部分进行格式化输出。

4.2 工程实践中的关键注意事项

在真实的项目开发中,仅仅完成上述配置远不足以保证ADC的长期稳定运行。以下是几个极易被忽视、却至关重要的工程实践要点:

  • 电源与地的分离 :模拟电源(VDDA/VSSA)与数字电源(VDD/VSS)必须在PCB上通过磁珠或0Ω电阻进行物理隔离,并在靠近MCU的模拟电源引脚处放置高质量的去耦电容(通常为100nF陶瓷电容+10µF电解电容)。这是抑制数字开关噪声窜入模拟电路的最根本措施。我曾在一个工业传感器项目中,因VDDA与VDD共用同一块铜箔,导致ADC读数在电机启动时出现高达50mV的跳变,最终通过严格分割模拟/数字地平面并优化去耦才得以解决。
  • 输入信号的调理 :直接将电位器等高阻抗信号源接入ADC引脚是危险的。理想情况下,应在ADC输入前加入一个由运放构成的电压跟随器(Buffer),其输入阻抗极高(GΩ级),输出阻抗极低(Ω级),能完美隔离信号源与ADC采样电容,彻底消除因采样电容充放电引起的信号拖尾与失真。对于本实验的电位器,虽然239.5的采样时间已提供足够裕量,但在更高要求的应用中,Buffer是必不可少的。
  • 软件滤波的必要性 :即使硬件设计完美,环境电磁干扰(EMI)仍可能导致ADC读数出现随机抖动。简单的均值滤波(如连续采样16次,舍弃最大最小值后取平均)或一阶IIR滤波( filtered = filtered * 0.9 + raw * 0.1 )能显著平滑数据,提升用户体验。在电位器应用中,这能有效消除因手指轻微抖动引起的LCD数值“闪烁”。

综上所述,单通道ADC采集实验绝非一个简单的“点亮LED”式入门练习。它是一扇通往嵌入式模拟世界的大门,其背后交织着精密的时序逻辑、严谨的电气特性与丰富的工程经验。唯有将寄存器配置、HAL库调用、硬件设计与软件算法融会贯通,才能真正驾驭这一核心外设,在纷繁复杂的实际项目中游刃有余。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐