ADC(Analog-to-Digital Converter)模数转换是嵌入式系统中最基础也最重要的外设功能之一。在普中STM32F1xx开发板上实现ADC功能,意味着我们可以将现实世界中的模拟信号(如温度、光照、电压等)转换为数字信号供MCU处理。这个实验看似简单,但涉及硬件电路设计、时钟配置、采样精度控制等多个关键技术点。
我在工业控制领域使用STM32的ADC功能已有7年经验,从早期的F1系列到现在的H7系列都用过。实测发现,虽然F1的ADC性能比不上新款芯片,但通过合理的配置和软件处理,完全能满足大多数应用场景的需求。本实验将基于STM32F103ZET6芯片,使用标准库完成ADC单通道电压采集,并分享几个提升采集精度的实用技巧。
普中开发板上的ADC输入电路设计直接影响测量精度。板载的电位器通过PA1引脚(ADC1通道1)连接MCU,这个设计虽然简单,但有几点需要注意:
输入阻抗匹配:STM32F1的ADC输入阻抗典型值为50kΩ,当信号源阻抗过大时会导致采样电容充电不足。建议在信号源和ADC输入之间加入电压跟随器(如OPAMP)进行阻抗变换。
参考电压选择:开发板使用3.3V作为VDDA和VREF+,这意味着ADC的测量范围是0-3.3V。对于精密测量,建议使用外部基准源(如REF3025提供2.5V基准)。
滤波电路:开发板原理图上可以看到一个0.1μF的滤波电容,这对于滤除高频噪声很有帮助。在实际项目中,我通常会额外增加一个RC低通滤波器(如1kΩ+0.01μF)。
STM32F103的ADC是12位逐次逼近型,主要特性包括:
这里有个关键点容易被忽略:ADC时钟不能超过14MHz。在标准库中,我们需要通过RCC配置将APB2时钟分频,确保ADC时钟在安全范围内。我通常选择PCLK2 6分频(72MHz/6=12MHz),这样既满足速度要求又留有余量。
使用标准库配置ADC需要遵循以下步骤,每个步骤都有其技术考量:
c复制RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE);
c复制GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
c复制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; // 1个转换通道
ADC_Init(ADC1, &ADC_InitStructure);
c复制ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1));
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1));
重要提示:校准必须在ADC使能后、开始转换前进行。校准值会存储在ADC的校准寄存器中,每次上电都需要重新校准。
ADC_SampleTime配置直接影响转换精度,STM32F1提供7种采样时间选择(1.5~239.5个周期)。根据信号源阻抗,应遵循以下原则:
对于开发板上的电位器(约10kΩ),我推荐配置为28.5周期:
c复制ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_28Cycles5);
标准的ADC采集流程如下,但实际应用中需要考虑更多细节:
c复制uint16_t Get_ADC_Value(uint8_t ch) {
ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_28Cycles5);
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC));
return ADC_GetConversionValue(ADC1);
}
这个基础函数有几个可以优化的地方:
在实际项目中,我通常会采用以下两种方法来提高ADC稳定性:
c复制#define SAMPLE_TIMES 16
uint16_t ADC_AvgRead(uint8_t ch) {
uint32_t sum = 0;
for(uint8_t i=0; i<SAMPLE_TIMES; i++) {
sum += Get_ADC_Value(ch);
delay_us(10); // 适当延时
}
return sum / SAMPLE_TIMES;
}
c复制uint16_t ADC_AdvancedRead(uint8_t ch) {
uint16_t buf[5];
for(uint8_t i=0; i<5; i++) {
buf[i] = Get_ADC_Value(ch);
}
Bubble_Sort(buf, 5); // 排序取中值
uint32_t sum = buf[2];
for(uint8_t j=0; j<3; j++) {
sum += Get_ADC_Value(ch);
}
return sum / 4;
}
将ADC值转换为实际电压需要考虑参考电压精度:
c复制float ADC_ToVoltage(uint16_t adc_val) {
float voltage = (float)adc_val * 3.3f / 4095;
// 可选:根据实际基准电压校准
// voltage *= 3.3 / actual_vref;
return voltage;
}
对于精密测量,我建议在代码中加入校准系数。比如实测发现3.3V电源实际为3.28V时:
c复制#define VREF_CALIBRATION 0.9924f // 3.28/3.3
float calibrated_voltage = ADC_ToVoltage(adc_val) * VREF_CALIBRATION;
现象:ADC值在输入不变时仍有较大波动
可能原因及解决方案:
通过多年项目经验,我总结出以下提升ADC精度的有效方法:
c复制// 利用内部温度传感器和Vrefint校准
void ADC_EnableInternalRef(void) {
ADC_TempSensorVrefintCmd(ENABLE);
delay_ms(10); // 等待传感器稳定
}
float Get_SupplyVoltage(void) {
uint16_t vrefint = Get_ADC_Value(ADC_Channel_Vrefint);
return (1.20f * 4095) / vrefint; // Vrefint典型值1.20V
}
当扩展到多通道采集时,需要特别注意:
c复制ADC_InitStructure.ADC_ScanConvMode = ENABLE;
ADC_InitStructure.ADC_NbrOfChannel = channel_count;
c复制DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)adc_buffer;
DMA_InitStructure.DMA_BufferSize = channel_count;
c复制ADC_RegularChannelConfig(ADC1, next_ch, 1, ADC_SampleTime_28Cycles5);
delay_us(3); // 通道切换稳定时间
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
将ADC与光敏电阻结合,可以实现环境光检测。电路连接如下:
code复制VCC ---[10kΩ]---+
|
[LDR]
|
PA1(ADC)
|
GND
软件处理时需要注意到光敏电阻的非线性特性,建议采用查表法或分段线性化处理:
c复制const uint16_t lux_table[] = {0, 10, 50, 100, 200, 500, 1000, 2000};
const uint16_t adc_table[] = {4095, 3500, 2500, 1800, 1000, 500, 200, 50};
uint16_t Get_Lux_Value(void) {
uint16_t adc_val = ADC_AvgRead(ADC_Channel_1);
// 简单查表法
for(uint8_t i=0; i<8; i++) {
if(adc_val >= adc_table[i]) {
return lux_table[i];
}
}
return 0;
}
利用STM32F1的ADC可以实现电阻触摸屏驱动,需要4个ADC通道(XP, XM, YP, YM)。关键实现步骤:
c复制// 设置XP为上拉,XM为下拉,YP和YM为高阻
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_SetBits(GPIOX, XP_Pin);
GPIO_ResetBits(GPIOX, XM_Pin);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOY, &GPIO_InitStructure);
uint16_t x_pos = Get_ADC_Value(YP_Channel);
c复制// 设置YP为上拉,YM为下拉,XP和XM为高阻
GPIO_SetBits(GPIOY, YP_Pin);
GPIO_ResetBits(GPIOY, YM_Pin);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOX, &GPIO_InitStructure);
uint16_t y_pos = Get_ADC_Value(XP_Channel);
对于电池供电设备,通常需要监测电池电压。由于ADC量程限制(0-3.3V),需要使用电阻分压:
code复制VBAT ---[R1 100k]---+
|
[R2 100k]
|
PA1(ADC)
|
GND
软件处理时需要考虑分压比和ADC参考电压:
c复制#define VOLTAGE_DIVIDER_RATIO 2.0f // (R1+R2)/R2
float Get_BatteryVoltage(void) {
float adc_voltage = ADC_ToVoltage(ADC_AvgRead(ADC_Channel_1));
return adc_voltage * VOLTAGE_DIVIDER_RATIO;
}
为提高精度,可以在代码中校准分压电阻的实际值:
c复制// 用已知电源校准分压比
#define CALIB_VOLTAGE 3.300f // 校准用标准电压
#define CALIB_ADC_VAL 1650 // 校准时测得的ADC值
float actual_ratio = CALIB_VOLTAGE / (CALIB_ADC_VAL * 3.3f / 4095);
在电池供电场景中,合理使用低功耗模式可以大幅延长设备续航。STM32F1的ADC在低功耗模式下有几个关键注意事项:
c复制ADC_Cmd(ADC1, DISABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, DISABLE);
c复制RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
ADC_Init(ADC1, &ADC_InitStructure);
ADC_Cmd(ADC1, ENABLE);
ADC_ResetCalibration(ADC1);
// ...校准流程
一个典型的低功耗数据采集流程:
c复制void Enter_ADC_LowPowerMode(void) {
// 配置定时器触发ADC
TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update);
ADC_ExternalTrigConvCmd(ADC1, ENABLE);
// 配置唤醒中断
ADC_ITConfig(ADC1, ADC_IT_EOC, ENABLE);
// 进入停止模式
PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI);
// 唤醒后继续执行
SystemInit(); // 需要重新配置时钟
}
为确保ADC工作正常,建议进行以下测试:
线性度测试:使用精密可调电源,从0V到3.3V以0.1V为步进,记录ADC读数并计算误差。
噪声测试:输入固定电压(如1.65V),连续采集1000次,计算标准差:
c复制float ADC_NoiseTest(uint8_t ch) {
uint32_t sum = 0, sum_sq = 0;
for(uint16_t i=0; i<1000; i++) {
uint16_t val = Get_ADC_Value(ch);
sum += val;
sum_sq += val * val;
}
float mean = (float)sum / 1000;
float variance = (float)sum_sq/1000 - mean*mean;
return sqrt(variance);
}
根据我的测试数据,STM32F1的ADC在室温下通常能达到:
要达到最佳性能,建议: