ADC(模数转换器)是嵌入式系统中非常重要的外设模块,它负责将模拟信号转换为数字信号。STM32F103VET6内置了3个12位精度的ADC模块,每个ADC最多支持16个外部通道。在实际项目中,我经常用它来采集传感器信号、电池电压等模拟量。
这个芯片的ADC性能相当不错:转换时间最快可达1μs(72MHz主频时),输入电压范围0-3.3V,支持单次/连续转换、扫描模式、中断/DMA传输等多种工作方式。第一次使用时,我被它的丰富功能震撼到了,但配置起来也确实需要花些功夫理解。
ADC的时钟源来自PCLK2,最大不能超过14MHz。这里有个容易踩的坑:如果系统时钟配置为72MHz,PCLK2默认也是72MHz,必须通过分频器将ADC时钟降到14MHz以下。我建议初学者直接使用8分频(9MHz),这样既安全又容易计算采样时间。
在开始编程前,硬件设计要特别注意几点:
我做过一个温度采集项目,因为VREF+没接滤波电容,导致采集值波动很大。后来在VREF+对地加了0.1μF和10μF电容后,稳定性明显改善。这个小细节希望大家注意。
以下是配置PC1为ADC输入引脚的代码:
c复制void ADC_GPIO_Config(void) {
GPIO_InitTypeDef GPIO_InitStructure;
// 使能GPIOC时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
// 配置PC1为模拟输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOC, &GPIO_InitStructure);
}
ADC的配置需要仔细设置多个参数。下面是我常用的初始化函数:
c复制void ADC_Mode_Config(void) {
ADC_InitTypeDef ADC_InitStructure;
// 使能ADC2时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC2, ENABLE);
// ADC基本参数配置
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; // 独立模式
ADC_InitStructure.ADC_ScanConvMode = DISABLE; // 单通道禁用扫描
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 连续转换
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 软件触发
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // 数据右对齐
ADC_InitStructure.ADC_NbrOfChannel = 1; // 1个转换通道
ADC_Init(ADC2, &ADC_InitStructure);
// 设置ADC时钟为PCLK2的8分频(9MHz)
RCC_ADCCLKConfig(RCC_PCLK2_Div8);
// 配置通道11的采样时间为55.5周期
ADC_RegularChannelConfig(ADC2, ADC_Channel_11, 1, ADC_SampleTime_55Cycles5);
// 使能EOC中断
ADC_ITConfig(ADC2, ADC_IT_EOC, ENABLE);
// 校准ADC
ADC_Cmd(ADC2, ENABLE);
ADC_ResetCalibration(ADC2);
while(ADC_GetResetCalibrationStatus(ADC2));
ADC_StartCalibration(ADC2);
while(ADC_GetCalibrationStatus(ADC2));
// 开始转换
ADC_SoftwareStartConvCmd(ADC2, ENABLE);
}
采样时间直接影响转换精度。计算公式为:
总转换时间 = 采样时间 + 12.5个周期
以9MHz时钟为例:
对于慢变信号(如温度),可以使用更长的采样时间;对快速信号则需要缩短采样时间。我在电机控制项目中,为了采集电流信号,不得不将采样时间降到28.5周期,牺牲了一些精度换取速度。
配置NVIC和中断服务函数:
c复制// 中断配置
void ADC_NVIC_Config(void) {
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
NVIC_InitStructure.NVIC_IRQChannel = ADC1_2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
// 中断服务函数
uint16_t ADC_ConvertedValue = 0;
void ADC1_2_IRQHandler(void) {
if(ADC_GetITStatus(ADC2, ADC_IT_EOC) == SET) {
ADC_ConvertedValue = ADC_GetConversionValue(ADC2);
ADC_ClearITPendingBit(ADC2, ADC_IT_EOC);
}
}
将ADC原始值转换为实际电压:
c复制float voltage = (float)ADC_ConvertedValue / 4095 * 3.3f;
这里有个细节:理论上12位ADC最大值是4095(2^12-1),但实际使用时发现读取值偶尔会达到4096。我建议在代码中加入限幅处理:
c复制uint16_t raw_value = ADC_ConvertedValue > 4095 ? 4095 : ADC_ConvertedValue;
float voltage = (float)raw_value / 4095 * 3.3f;
ADC采集难免会有噪声,简单的软件滤波可以显著改善数据质量:
c复制#define FILTER_LEN 10
uint16_t filter_buf[FILTER_LEN];
uint8_t filter_index = 0;
// 滑动平均滤波
uint16_t adc_filter(uint16_t new_value) {
static uint32_t sum = 0;
sum = sum - filter_buf[filter_index] + new_value;
filter_buf[filter_index] = new_value;
filter_index = (filter_index + 1) % FILTER_LEN;
return sum / FILTER_LEN;
}
完整的ADC采集系统初始化应该按照以下顺序:
c复制void ADC_Init(void) {
ADC_GPIO_Config();
ADC_NVIC_Config();
ADC_Mode_Config();
}
主程序中可以定期读取和显示电压值:
c复制int main(void) {
USART_Config(); // 初始化串口
ADC_Init(); // 初始化ADC
printf("ADC Voltage Measurement Demo\r\n");
while(1) {
float voltage = (float)adc_filter(ADC_ConvertedValue) / 4095 * 3.3f;
printf("Voltage: %.2fV\r\n", voltage);
Delay(500000); // 简单延时
}
}
在实际项目中,我遇到过这些问题:
有个特别隐蔽的bug:当同时使用多个外设时,ADC采集会偶尔出错。后来发现是其他外设的时钟配置影响了PCLK2,导致ADC时钟超限。建议在复杂系统中明确检查各总线时钟频率。