1. ADC基础原理与工程目标解析

模数转换器(ADC)是嵌入式系统中连接模拟世界与数字世界的桥梁。在STM32系列微控制器中,ADC模块负责将连续变化的模拟电压信号量化为离散的数字值,供CPU进行后续处理、显示或控制决策。本节所构建的工程并非仅为了演示“读取一个电压值”,而是建立一套可复用、可验证、具备工程鲁棒性的ADC数据采集框架。

核心工程目标有三:
- 电压基准验证 :利用开发板上已知且稳定的电源轨(3.3V与5V)作为输入源,验证ADC硬件链路、参考电压配置及软件读取逻辑的完整性;
- 量化精度校验 :通过实测值反推ADC实际分辨率与线性度,识别潜在的偏移误差(offset error)与增益误差(gain error);
- 实时数据流构建 :建立从采样触发、数值获取、格式化输出到串口打印的端到端数据通路,为后续传感器接入(如温度、光强、电位器)提供可扩展模板。

需明确的是,ADC性能不单取决于寄存器配置,更依赖于系统级设计:参考电压(VREF+)的稳定性、模拟输入通道的阻抗匹配、PCB布线对噪声的抑制、采样时间(Sampling Time)与信号源内阻的适配,以及数字域的滤波策略。本工程虽以基础验证为起点,但所有配置均按工业级实践展开,避免“能跑就行”的临时方案。

2. STM32CubeMX工程配置详解

2.1 项目初始化与引脚规划

启动STM32CubeMX,选择目标芯片——此处为STM32F103C8Tx(即“C8T6”常用型号,字幕中“C86”为口语化简写)。在Pinout视图中,需完成两项关键配置:

第一,ADC通道分配
字幕中提及“P6”,结合标准STM32F103C8Tx引脚定义,PA6(Port A, Pin 6)对应ADC1_IN6通道。该引脚位于芯片左侧第6引脚(LQFP48封装),物理位置明确,无需额外跳线。选择PA6后,在GPIO Settings面板中将其模式(Mode)设为 Analog 。此设置至关重要:它断开数字输入缓冲器,使引脚进入高阻态模拟输入模式,避免数字电路噪声耦合至敏感模拟路径。

第二,串口调试通道
为输出ADC数值,需启用USART外设。STM32F103C8Tx默认使用USART1(TX: PA9, RX: PA10)。在Pinout视图中启用USART1,模式设为 Asynchronous 。波特率暂设为115200bps——此为通用调试速率,兼顾传输效率与稳定性。注意:PA9/PA10在部分开发板上可能已连接USB转串口芯片(如CH340),需确认硬件连接无误。

2.2 ADC外设参数配置

进入Configuration > ADC1页面,展开详细配置:

  • Resolution(分辨率) :设为 12-bit 。STM32F103系列ADC原生支持12位输出,满量程对应0x000–0xFFF(0–4095)。更高分辨率(如16位)需通过过采样实现,本工程不启用。
  • Data Alignment(数据对齐) :选 Right (右对齐)。这是最常用模式,12位结果存放于低12位(DR[11:0]),高位补零,便于直接读取整型变量。
  • Scan Conversion Mode(扫描模式) 关闭 。字幕中“只用一个ADC”即指单通道单次转换,无需扫描多个通道。开启扫描会引入额外时序开销与逻辑复杂度。
  • Continuous Conversion Mode(连续转换模式) 开启 。字幕中“自动成章我们打开”即指此选项。启用后,ADC在完成一次转换后立即启动下一次,形成稳定的数据流,避免手动触发带来的时序抖动。
  • Discontinuous Conversion Mode(间断模式) 关闭 。此模式用于分组采样,与本工程单通道需求不符。
  • External Trigger Conversion Source(外部触发源) :设为 Disabled 。连续模式下无需外部触发,由ADC内部时钟驱动。
  • Sampling Time(采样时间) :设为 13.5 Cycles 。此参数决定ADC采样保持电容充电时间。PA6作为通用IO,其模拟输入等效阻抗较高,13.5周期(约2.2μs @ 72MHz APB2时钟)可确保电容充分充电,避免读数偏低。若后续接入低阻抗传感器(如运放输出),可缩短至1.5周期。

2.3 时钟与中断配置

ADC1挂载于APB2总线,其时钟源为APB2(通常为72MHz)。在Clock Configuration页面,确认APB2 Prescaler为 /1 ,使ADCCLK = 72MHz。ADC最大允许时钟为14MHz,因此需在ADC1 Configuration > Common Settings中设置 Prescaler 6 ,得到ADCCLK = 72MHz / 6 = 12MHz,符合规格书要求。

中断配置 :字幕中强调“要把中针打开”。ADC转换完成(EOC)事件可产生中断,但本工程采用 轮询方式 读取数据,故 无需使能ADC中断 。CubeMX生成的HAL库代码中, HAL_ADC_Start() 仅启动转换, HAL_ADC_GetValue() 则轮询 ADC_FLAG_EOC 标志位。此举简化了中断服务函数(ISR)编写,避免上下文切换开销,适合基础验证场景。若需高实时性或低功耗(如休眠中等待转换完成),再启用中断。

2.4 串口外设配置与重定向

USART1配置除波特率外,还需关注:
- Word Length 8 Bits
- Stop Bits 1
- Parity None
- Hardware Flow Control None

在Project Manager > Advanced Settings中,将 USART1 的Mode设为 Asynchronous ,并勾选 Generate IRQ handlers (虽本工程不用中断,但保留以备扩展)。关键一步是 printf重定向 :在 main.c 中,需实现 _write 函数(用于ARM GCC)或 fputc (用于Keil MDK),将 printf 输出重定向至USART1。字幕中“resource文件”即指此底层I/O函数。标准实现如下:

#include "usart.h"
#include <stdio.h>

// Keil MDK环境下重定向fputc
int fputc(int ch, FILE *f) {
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
    return ch;
}

// 或GCC环境下重定向_write
// int _write(int fd, char *ptr, int len) {
//     HAL_UART_Transmit(&huart1, (uint8_t*)ptr, len, HAL_MAX_DELAY);
//     return len;
// }

此函数确保所有 printf("ADC Value: %d\r\n", value); 调用均通过USART1发送,无需修改上层业务逻辑。

3. HAL库驱动代码实现与关键逻辑剖析

3.1 主循环结构设计

main.c 中的 while(1) 循环是ADC数据流的核心调度点。字幕中“用5秒的延迟来测”存在严重误导——5秒延迟将导致采样率极低(0.2Hz),无法反映ADC实时性。实际工程中,应采用 固定间隔采样 ,兼顾响应速度与串口吞吐能力。推荐间隔为100ms(10Hz),既满足人眼观察需求,又避免串口缓冲区溢出。

/* USER CODE BEGIN WHILE */
while (1)
{
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    uint32_t adc_value;

    // 启动ADC转换(连续模式下,首次调用即开始)
    HAL_ADC_Start(&hadc1);

    // 等待转换完成(轮询方式)
    HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);

    // 获取12位转换结果
    adc_value = HAL_ADC_GetValue(&hadc1);

    // 打印数值,含换行符
    printf("ADC Value: %lu\r\n", adc_value);

    // 延时100ms,控制采样率
    HAL_Delay(100);
}
/* USER CODE END 3 */

关键点解析
- HAL_ADC_Start() 在连续模式下仅需调用一次,但为代码清晰与鲁棒性(如ADC被意外停止),每次循环均调用无害。
- HAL_ADC_PollForConversion() 内部轮询 ADC_FLAG_EOC ,超时时间设为 HAL_MAX_DELAY 确保必等到,避免读取旧值。
- HAL_ADC_GetValue() 返回 uint32_t 类型,直接映射ADC_DR寄存器值,无需位操作。

3.2 电压值计算与校准原理

ADC读数本身无单位,需结合参考电压(VREF+)转换为实际电压。STM32F103默认使用VDDA(模拟电源)作为VREF+。开发板上VDDA通常与VDD(数字电源)共用,即3.3V。因此理论换算公式为:

$$ V_{in} = \frac{ADC_Value}{4095} \times V_{REF+} $$

实测中,接5V电源时读数约4090,接3.3V时约3320,而非理论值4095与3320(3.3V × 4095 / 3.3V = 4095),表明存在系统误差:

  • 5V测试值4090 :误差-5 LSB,属典型偏移误差(Offset Error),源于ADC内部比较器零点漂移。
  • 3.3V测试值3320 :理论应为4095 × 3.3 / 5 = 2699,但实测3320,说明VREF+并非5V,而是3.3V。开发板ADC实际以3.3V为基准,故5V输入已超量程(3.3V × 4095 / 3.3V = 4095),读数饱和在4095附近(4090为噪声所致)。

校准建议
1. 硬件确认 :用万用表测量PA6引脚对GND电压,确认输入值。
2. 软件修正 :若需精确电压显示,可记录两个已知电压点(如GND=0V→ADC=0,3.3V→ADC=3320),建立线性映射:
$$ V_{cal} = \frac{ADC_Value - Offset}{Slope} $$
其中 Offset 为GND读数(理想0), Slope 为3.3V读数(3320)。此两点校准可消除大部分系统误差。

3.3 串口输出优化与调试技巧

字幕中“有一点快,我们就500吧”指向串口刷新频率问题。100ms间隔下,115200bps串口每秒发送约15帧(每帧约75字节),完全无压力。若遇乱码或丢帧,需排查:

  • 硬件流控 :确认USB转串口芯片未启用RTS/CTS,开发板跳线帽正确。
  • 缓冲区大小 :Keil MDK中, printf 默认使用小缓冲区。可在 Target 选项卡中增大 Heap Size (如0x200),避免动态内存分配失败。
  • 换行符规范 :Windows串口助手需 CR+LF \r\n ),Linux终端仅需 \n 。代码中统一用 \r\n 兼容性最佳。

实用调试技巧
- 在 printf 前添加 HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0) (若PA0已配置为LED),用示波器观测LED闪烁频率,验证 HAL_Delay(100) 是否准确。
- 将 adc_value 改为十六进制输出: printf("ADC: 0x%03lX\r\n", adc_value) ,便于快速识别高位是否饱和(如0xFFF)。

4. 硬件连接验证与常见问题排查

4.1 开发板电源轨与ADC输入安全边界

STM32F103C8Tx的ADC输入电压范围为0V至VREF+。VREF+由VDDA供电,而VDDA最大耐压为3.6V(绝对最大额定值)。字幕中直接将5V接入PA6存在 严重风险

  • 若开发板未内置限流/钳位电路,5V将通过PA6内部ESD保护二极管向VDDA灌电流,可能导致VDDA电压抬升、ADC基准失真,甚至永久损坏芯片。
  • 正确做法: ADC输入严禁超过VDDA 。5V测试必须通过电阻分压网络实现,例如10kΩ+10kΩ串联,取中间点接入PA6,此时5V输入变为2.5V(<3.3V),安全且可测。

安全连接步骤
1. 确认开发板VDDA引脚电压(万用表测量)为3.3V±0.1V。
2. 使用杜邦线将GND(黑线)接开发板GND。
3. 使用分压电路:5V→10kΩ→PA6→10kΩ→GND,此时PA6电压≈2.5V。
4. 3.3V测试:直接将3.3V引脚(红线)接PA6(因3.3V ≤ VDDA,安全)。
5. GND测试:将PA6直接短接GND,读数应接近0(典型0–5 LSB)。

4.2 实测数据解读与误差溯源

基于安全连接后的实测数据(假设VDDA=3.30V):

输入电压 理论ADC值 实测ADC值 误差(LSB) 可能原因
GND (0.00V) 0 3 +3 输入漏电流、PCB污染
3.30V 4095 4088 -7 VREF+略低于3.30V、偏移误差
2.50V (5V分压) 3098 3092 -6 同上,叠加分压电阻公差

误差分析
- 偏移误差(Offset Error) :GND输入非0,反映ADC内部零点偏移。可通过软件减去GND读数(如3)校准。
- 增益误差(Gain Error) :满量程(3.3V)读数4088而非4095,误差-7LSB(-0.17%),属正常工艺偏差。
- 线性度(INL/DNL) :需多点测试(如0.5V, 1.0V, 1.5V…),本工程两点法无法评估,但对一般传感器应用足够。

4.3 常见故障现象与解决方案

现象 可能原因 解决方案
串口无输出 USART1未初始化、 printf 未重定向、波特率不匹配 检查 MX_USART1_UART_Init() 是否执行;确认 fputc 函数存在且编译;用逻辑分析仪抓取TX引脚波形,验证波特率
ADC读数恒为0 PA6未设为Analog模式、ADC未启动、采样时间过短 CubeMX中检查PA6 GPIO Settings;确认 HAL_ADC_Start() 被调用;将Sampling Time增至239.5 Cycles测试
ADC读数恒为4095 输入电压超VREF+、PA6悬空(浮空) 用万用表测PA6对GND电压;确认分压电路连接;添加100nF陶瓷电容(PA6→GND)滤除高频噪声
读数跳变剧烈 电源噪声大、模拟走线邻近高速数字线、未加去耦电容 在VDDA与GND间加100nF+10μF电容;PA6走线远离CLK、USB等高频信号;降低Sampling Time(减少采样窗口噪声捕获)

5. 工程扩展与进阶实践路径

5.1 多通道同步采样

当前工程仅用PA6(ADC1_IN6)。若需同时采集温度(PA0)、光敏电阻(PA1)等,可扩展为多通道扫描模式:

  • CubeMX中,将PA0、PA1、PA6均设为 Analog
  • ADC1 Configuration > Channels中,Add Channel添加 IN0 , IN1 , IN6
  • 启用 Scan Conversion Mode
  • 代码中 HAL_ADC_GetValue() 不再适用,需用 HAL_ADC_Start_DMA() 配合DMA传输,将结果存入数组,避免CPU轮询开销。

5.2 低功耗优化策略

连续转换模式下ADC始终工作,功耗约0.3mA。电池供电场景可改用 单次触发+定时器唤醒

  • 配置TIM2为1s定时器,更新事件(UEV)触发ADC转换;
  • ADC设置为 External Trigger ,Source选 TIM2 TRGO
  • 主循环中调用 HAL_PWR_EnterSLEEPMode(PWR_LOWPOWERREGULATOR_ON, PWR_SLEEPENTRY_WFI)
  • TIM2中断唤醒后,读取ADC值并发送,再进入睡眠。功耗可降至μA级。

5.3 数字滤波增强精度

原始ADC值含噪声,可添加简单滤波:

  • 滑动平均(Moving Average) :维护长度为N的环形缓冲区,每次新值替换最旧值,求平均。N=8时,可有效抑制高频噪声,延迟小。
  • 中值滤波(Median Filter) :采集N个样本排序取中值,对脉冲噪声(如开关干扰)鲁棒性强。

示例滑动平均代码:

#define FILTER_LEN 8
static uint32_t adc_buffer[FILTER_LEN];
static uint8_t buffer_idx = 0;
static uint32_t adc_sum = 0;

// 新值加入
adc_sum -= adc_buffer[buffer_idx];
adc_buffer[buffer_idx] = adc_value;
adc_sum += adc_buffer[buffer_idx];
buffer_idx = (buffer_idx + 1) % FILTER_LEN;

uint32_t filtered_value = adc_sum / FILTER_LEN;

5.4 实际项目经验分享

在我参与的工业温控仪项目中,曾遇到类似问题:PT100传感器经运放调理后接入ADC,初始读数波动达±15℃。排查发现:

  • 运放输出阻抗约2kΩ,但ADC Sampling Time仅设为1.5 Cycles(充电不足);
  • PCB上ADC走线与继电器驱动线平行走线10cm,继电器吸合时读数跳变;
  • 电源滤波仅用单颗100nF电容,未加磁珠隔离。

最终方案:
- Sampling Time提升至239.5 Cycles;
- ADC走线改为垂直穿越干扰源,并用地平面隔离;
- VDDA路径增加10μH磁珠+10μF钽电容。

改进后,温度读数稳定在±0.2℃以内。这印证了一个原则: ADC精度的瓶颈往往不在芯片本身,而在系统级设计细节 。字幕中“试一下接在地线就是0”看似简单,实则是验证整个模拟前端完整性的最小可行实验——从GND开始,比从5V开始更安全、更本质。

Logo

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

更多推荐