1. 项目概述
在嵌入式系统开发中,ADC(模数转换器)是最基础也最关键的模块之一。今天我要分享的是基于STM32F1系列MCU的单通道ADC采集实现,特别是不使用DMA的裸机驱动方案。这个方案非常适合初学者理解ADC的工作原理,也适用于对实时性要求不高的简单数据采集场景。
我选择STM32F103C8T6作为硬件平台,它内置12位精度的ADC模块,采样速率最高可达1MHz。虽然现在STM32家族已经有了性能更强的F4/H7系列,但F1系列依然是学习嵌入式开发的经典选择,其HAL库的封装也相对简洁明了。
2. 硬件设计与原理
2.1 ADC基础概念
ADC的本质是将连续的模拟信号转换为离散的数字量。STM32的ADC模块有几个关键参数需要理解:
- 分辨率:F1系列是12位ADC,意味着输出范围是0-4095(2^12-1)
- 参考电压:通常接VREF+(3.3V)和VREF-(GND)
- 采样时间:每个样本的转换时间,由时钟分频和采样周期数决定
重要提示:实际工程中,VREF+的稳定性直接影响ADC精度。如果使用板载LDO供电,建议在VREF+引脚添加0.1uF去耦电容。
2.2 硬件连接方案
本实验采用最简单的单端输入方式:
code复制信号源 → PA1(ADC1_IN1)
│
└── 10kΩ电阻 → 0.1uF电容 → GND(硬件滤波)
这种RC滤波组合可以抑制高频噪声,时间常数τ=1ms,适合采样率低于1kHz的信号。如果信号源阻抗较高(>10kΩ),需要在前面增加电压跟随器。
3. 软件实现详解
3.1 初始化配置流程
完整的ADC初始化包含以下步骤:
- 时钟配置:
c复制RCC_PeriphCLKInitTypeDef adc_clk_init = {0};
adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC;
adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6; // 72MHz/6=12MHz
HAL_RCCEx_PeriphCLKConfig(&adc_clk_init);
ADC时钟最高14MHz,这里选择12MHz是安全值。超频会导致精度下降。
- GPIO模式设置:
c复制GPIO_InitTypeDef gpio_init = {0};
gpio_init.Pin = GPIO_PIN_1;
gpio_init.Mode = GPIO_MODE_ANALOG; // 必须设为模拟模式
HAL_GPIO_Init(GPIOA, &gpio_init);
- ADC参数初始化:
c复制ADC_HandleTypeDef hadc;
hadc.Instance = ADC1;
hadc.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc.Init.ScanConvMode = ADC_SCAN_DISABLE;
hadc.Init.ContinuousConvMode = DISABLE;
hadc.Init.NbrOfConversion = 1;
hadc.Init.ExternalTrigConv = ADC_SOFTWARE_START;
HAL_ADC_Init(&hadc);
- 通道配置:
c复制ADC_ChannelConfTypeDef sConfig = {0};
sConfig.Channel = ADC_CHANNEL_1;
sConfig.Rank = ADC_REGULAR_RANK_1;
sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;
HAL_ADC_ConfigChannel(&hadc, &sConfig);
采样时间选择239.5周期,在12MHz时钟下约20μs,适合信号源阻抗<50kΩ的情况。
3.2 校准机制解析
STM32的ADC校准是很多人忽略的关键步骤:
c复制HAL_ADCEx_Calibration_Start(&hadc);
这个函数内部完成了:
- 复位校准寄存器
- 启动偏移校准(约10个ADC时钟周期)
- 将校准值存入ADC_DR寄存器
实测发现:上电后不执行校准,零点误差可能达到30-50LSB。校准后通常能控制在±3LSB以内。
4. 数据采集实现
4.1 轮询模式采集流程
不使用DMA时的标准采集序列:
c复制HAL_ADC_Start(&hadc); // 启动转换
if(HAL_ADC_PollForConversion(&hadc, 10) == HAL_OK) {
uint16_t val = HAL_ADC_GetValue(&hadc);
float voltage = val * 3.3f / 4095.0f;
}
4.2 时序特性分析
以12MHz ADC时钟、239.5周期采样时间为例:
- 采样时间:239.5 / 12MHz ≈ 20μs
- 12位转换时间:12.5周期 ≈ 1μs
- 总转换时间:21.5μs → 最大采样率约46kSPS
实际测试发现,由于HAL库函数调用开销,连续采集时实际采样率会降至约30kSPS。如果需要更高效率,建议直接操作寄存器或使用DMA。
5. 精度优化技巧
5.1 参考电压处理
开发板通常使用3.3V LDO作为VREF,实际电压可能在3.28-3.32V之间波动。两种解决方案:
- 使用精密基准源(如REF3030)
- 实时测量VREF电压(通过内部VREFINT通道)
5.2 软件滤波算法
简单的移动平均滤波实现:
c复制#define SAMPLE_COUNT 16
uint16_t adc_filter(void) {
static uint16_t buf[SAMPLE_COUNT];
static uint8_t idx = 0;
uint32_t sum = 0;
buf[idx++] = adc_get_result();
if(idx >= SAMPLE_COUNT) idx = 0;
for(int i=0; i<SAMPLE_COUNT; i++) {
sum += buf[i];
}
return sum / SAMPLE_COUNT;
}
5.3 温度补偿
ADC精度受温度影响明显。对于精密测量,可以:
- 启用内部温度传感器
- 建立温度-误差查找表
- 实时补偿
6. 常见问题排查
6.1 采样值跳动大
可能原因及解决方案:
- 电源噪声 → 增加LC滤波
- 信号源阻抗高 → 加入电压跟随器
- 地线干扰 → 采用星型接地
- 采样时间不足 → 增加SamplingTime
6.2 转换结果始终为0
检查清单:
- GPIO是否配置为模拟模式
- ADC时钟是否使能
- 通道配置的Rank是否≥1
- 是否调用了校准函数
6.3 电压计算偏差大
验证步骤:
- 测量实际VREF电压
- 检查计算公式是否正确
- 确认信号源输出是否准确
- 测试0V和VREF输入时的ADC值
7. 进阶应用方向
虽然本文演示的是基础单通道采集,但可以扩展为:
- 多通道轮询:修改NbrOfConversion和Rank参数
- 定时器触发:改用TIMx_TRGO作为触发源
- 注入通道:处理高优先级信号
- 差分输入:利用ADC1_IN0/1等差分通道对
我在实际项目中发现,对于缓慢变化的信号(如温度传感器),配合定时器触发+中断读取的方式,既能保证定时采集的准确性,又能降低CPU负载。一个典型的配置示例:
c复制// 在TIM6中断中触发ADC
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if(htim->Instance == TIM6) {
HAL_ADC_Start_IT(&hadc);
}
}
// ADC转换完成回调
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
uint16_t val = HAL_ADC_GetValue(hadc);
// 处理数据...
}
这种方案比纯轮询方式更节省资源,又避免了DMA的复杂性,是裸机系统中的折中优选。
