1. STM32 ADC扫描模式实战:从原理到波形显示
在嵌入式开发中,模拟信号采集是基础却至关重要的功能。STM32的ADC模块提供了多种工作模式,其中扫描模式配合注入通道的使用,能够实现多通道的高效采样。最近我在一个工业传感器项目中,就遇到了需要同时采集两路模拟信号的需求。经过反复调试和优化,最终实现了稳定可靠的采集方案。本文将分享整个实现过程,包括硬件连接、寄存器配置、数据处理等关键环节。
2. 硬件设计与核心思路
2.1 需求分析与方案选型
项目需要实时采集两路模拟信号(0-3.3V范围),并将转换结果通过串口发送到上位机显示波形。考虑到采样精度和实时性要求,我选择了STM32F103C8T6作为主控,其内置的12位ADC完全满足需求。关键设计要点包括:
- 使用ADC1的注入通道实现两路信号交替采样
- 采用定时器1触发采样,确保固定采样间隔
- 通过DMA传输减轻CPU负担(本示例为简化采用直接读取)
- 串口波特率设置为115200以保证数据传输速度
2.2 硬件连接示意图
实际硬件连接非常简单:
- PA0(ADC1_IN0) - 连接第一路模拟信号
- PA1(ADC1_IN1) - 连接第二路模拟信号
- PA9(USART1_TX) - 连接串口模块RX
- 共地连接确保参考电平一致
注意:模拟输入信号幅度不得超过VDDA(通常3.3V),否则可能损坏ADC模块。对于工业现场的高压信号,必须使用分压电路或隔离运放进行调理。
3. 软件实现详解
3.1 系统初始化流程
整个系统的初始化遵循外设依赖关系,按以下顺序进行:
- USART1初始化 - 用于数据传输
- TIM1初始化 - 提供采样时钟
- ADC1初始化 - 核心采集功能
这种顺序确保了当ADC需要定时器触发时,定时器已经准备就绪;当需要发送数据时,串口也已初始化完成。
3.2 关键代码解析
3.2.1 定时器配置
定时器1配置为向上计数模式,产生1kHz的触发信号:
c复制void APP_TIM1_Init(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct = {0};
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStruct.TIM_Period = 999; // 自动重装载值
TIM_TimeBaseInitStruct.TIM_Prescaler = 71; // 72分频(72MHz/72=1MHz)
// 定时频率 = 1MHz/(999+1) = 1kHz
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseInitStruct);
TIM_SelectOutputTrigger(TIM1, TIM_TRGOSource_Update);
TIM_Cmd(TIM1, ENABLE);
}
计算说明:
- STM32F103主频72MHz,定时器时钟为72MHz
- 预分频71→实际分频系数=71+1=72
- 计数频率=72MHz/72=1MHz
- 自动重装载值999→溢出频率=1MHz/(999+1)=1kHz
3.2.2 ADC注入通道配置
ADC配置为外部触发注入模式,关键参数解析:
c复制void APP_ADC1_Init(void){
// GPIO初始化(省略...)
RCC_ADCCLKConfig(RCC_PCLK2_Div6); // ADC时钟=72MHz/6=12MHz
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
ADC_InitTypeDef ADC_InitStruct = {0};
ADC_InitStruct.ADC_ContinuousConvMode = DISABLE; // 非连续转换
ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right; // 数据右对齐
ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStruct.ADC_Mode = ADC_Mode_Independent; // 独立模式
ADC_InitStruct.ADC_NbrOfChannel = 1;
ADC_InitStruct.ADC_ScanConvMode = DISABLE; // 禁用扫描模式(使用注入序列)
ADC_Init(ADC1, &ADC_InitStruct);
// 注入序列配置
ADC_InjectedSequencerLengthConfig(ADC1, 2); // 2个注入通道
ADC_InjectedChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_13Cycles5);
ADC_InjectedChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_13Cycles5);
ADC_ExternalTrigInjectedConvConfig(ADC1, ADC_ExternalTrigInjecConv_T1_TRGO);
ADC_ExternalTrigInjectedConvCmd(ADC1, ENABLE);
ADC_Cmd(ADC1, ENABLE);
}
关键点说明:
- 采样时间选择13.5周期,在12MHz ADC时钟下约1.125μs
- 注入序列不同于规则序列,可以打断当前转换立即执行
- 两个通道分别配置为序列位置1和2
4. 数据采集与处理
4.1 主循环实现
主程序不断查询转换完成标志,读取并处理数据:
c复制while(1){
while(ADC_GetFlagStatus(ADC1, ADC_FLAG_JEOC)== RESET); // 等待转换完成
uint16_t jdr1 = ADC_GetInjectedConversionValue(ADC1, ADC_InjectedChannel_1);
uint16_t jdr2 = ADC_GetInjectedConversionValue(ADC1, ADC_InjectedChannel_2);
// 转换为实际电压值(12位ADC,3.3V参考)
float v1 = jdr1 * (3.3f / 4095);
float v2 = jdr2 * (3.3f / 4095);
My_USART_Printf(USART1, "%.3f,%.3f\n", v1 , v2); // 发送到串口
}
电压转换公式解析:
- 12位ADC满量程值为4095(2^12-1)
- 参考电压3.3V时,LSB=3.3V/4095≈0.000806V
- 实际电压=ADC值×LSB
4.2 串口输出格式设计
为方便上位机解析,采用CSV格式:
code复制1.234,2.345\n
1.235,2.346\n
...
每行包含两路电压值,逗号分隔,以换行符结束。这种格式可直接被多数串口绘图工具识别。
5. 性能优化与问题排查
5.1 常见问题及解决方案
-
采样值跳动大
- 检查电源稳定性,ADC参考电压需干净稳定
- 增加采样时间(调整ADC_SampleTime参数)
- 在输入端添加0.1uF滤波电容
-
转换速度不达标
- 确认ADC时钟配置正确(不超过14MHz)
- 减少采样周期数(平衡速度和精度)
- 检查定时器触发频率是否匹配预期
-
数据发送丢失
- 降低串口波特率或优化发送代码
- 增加发送缓冲区或使用DMA
- 检查硬件连接是否可靠
5.2 实测性能数据
在72MHz系统时钟下,测得:
- 单次转换时间:约15μs(含采样时间)
- 最大采样率:约66kSPS(单通道)
- 实际应用采样率:1kSPS(两通道)
经验分享:当需要更高采样率时,可考虑使用规则组配合DMA,同时启用扫描模式。但注入通道的优势在于可以随时中断常规转换,适合处理紧急或高优先级信号。
6. 扩展应用与进阶技巧
6.1 多通道扩展方案
如需采集更多信号,可采用以下方法:
- 规则组+注入组组合使用
- 启用扫描模式配合DMA
- 使用多ADC并行采集(仅限支持此功能的型号)
6.2 校准与精度提升
STM32 ADC内置自校准功能,上电后应执行:
c复制ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1));
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1));
其他精度优化措施:
- 保持VDDA和VREF+干净稳定
- 避免高频数字信号干扰模拟部分
- 适当预热后使用(温度影响精度)
6.3 上位机波形显示
推荐使用以下工具可视化串口数据:
- SerialPlot:轻量级开源工具,支持实时绘图
- CoolTerm:功能丰富的串口终端,可保存数据
- 自定义Python程序:使用matplotlib实现灵活显示
一个简单的Python显示示例:
python复制import serial
import matplotlib.pyplot as plt
ser = serial.Serial('COM3', 115200)
plt.ion()
fig, ax = plt.subplots()
data = [[], []]
while True:
line = ser.readline().decode().strip()
v1, v2 = map(float, line.split(','))
data[0].append(v1)
data[1].append(v2)
ax.clear()
ax.plot(data[0], label='Channel 0')
ax.plot(data[1], label='Channel 1')
ax.legend()
plt.pause(0.01)
7. 关键参数调整指南
7.1 采样时间选择
STM32F1系列提供以下采样时间选项:
| 周期数 | 12MHz下时间 | 适用场景 |
|---|---|---|
| 1.5 | 0.125μs | 高速低阻抗信号 |
| 7.5 | 0.625μs | 一般信号 |
| 13.5 | 1.125μs | 高阻抗信号 |
| 28.5 | 2.375μs | 高精度测量 |
| 41.5 | 3.458μs | 极高阻抗 |
| 55.5 | 4.625μs | 温度传感器 |
| 71.5 | 5.958μs | 最严苛条件 |
选择原则:在保证精度的前提下选择最短时间,提高采样率。
7.2 触发频率计算
定时器触发频率公式:
code复制f_trigger = f_TIM / (PSC + 1) / (ARR + 1)
其中:
- f_TIM:定时器时钟频率(72MHz)
- PSC:预分频值
- ARR:自动重装载值
例如:
- PSC=71,ARR=999 → 1kHz
- PSC=0,ARR=719 → 100kHz(接近ADC极限)
8. 实际项目经验总结
在工业现场部署时,发现了几个值得注意的问题:
-
地环路干扰:传感器与控制器间的地电位差导致测量不准,解决方案:
- 使用隔离变送器
- 采用差分输入(如果ADC支持)
- 确保单点接地
-
电磁干扰:变频器等设备导致ADC值异常跳动,应对措施:
- 使用屏蔽双绞线
- 在ADC输入端添加RC滤波
- 软件端添加数字滤波(如移动平均)
-
温度漂移:长期运行后精度下降,解决方法:
- 定期执行ADC校准
- 避免PCB靠近热源
- 考虑温度补偿算法
经过这些优化后,系统实现了±0.5%的测量精度,完全满足工业级应用要求。这个项目让我深刻体会到,ADC性能不仅取决于芯片本身,还与整体硬件设计和软件处理密切相关。