STM32F103 ADC实战:从硬件搭建到波形可视化的全流程解析
在嵌入式开发中,ADC(模数转换器)是将模拟世界与数字系统连接的关键桥梁。许多开发者虽然掌握了ADC的基本原理,却苦于无法将其转化为实际项目中的有效工具。本文将带你从零开始,使用STM32F103C8T6(BluePill开发板常见型号)构建一个完整的电压采集与可视化系统,涵盖CubeMX配置、DMA传输优化、串口协议设计以及Python实时波形显示的全套解决方案。
1. 硬件准备与CubeMX工程创建
在开始编码前,正确的硬件连接和开发环境配置是项目成功的基础。我们需要准备以下硬件组件:
- STM32F103C8T6最小系统板(BluePill)
- 10kΩ电位器(用于模拟电压输入)
- USB转TTL串口模块(如CH340G)
- 杜邦线若干
硬件连接示意图:
| STM32引脚 | 连接目标 | 备注 |
|---|---|---|
| PA0 | 电位器中间引脚 | ADC1通道0输入 |
| 3.3V | 电位器一端 | 供电电压 |
| GND | 电位器另一端 | 接地 |
| PA9(TX) | USB-TTL模块RX | USART1发送端 |
| PA10(RX) | USB-TTL模块TX | USART1接收端 |
| 3.3V | USB-TTL模块VCC | 可选,部分模块需供电 |
| GND | USB-TTL模块GND | 共地 |
打开STM32CubeMX,按照以下步骤创建工程:
- 选择MCU型号:STM32F103C8T6
- 系统核心配置:
- SYS: Serial Wire (用于ST-Link调试)
- RCC: High Speed Clock (HSE) 选择Crystal/Ceramic Resonator
- ADC1配置:
- 启用IN0通道(对应PA0)
- 参数设置:
Resolution = 12Bits Data Alignment = Right Scan Conversion Mode = Disabled Continuous Conversion Mode = Enabled Discontinuous Conversion Mode = Disabled DMA Continuous Requests = Enabled End Of Conversion Selection = EOC flag at the end of single conversion
- USART1配置:
- Mode: Asynchronous
- Baud Rate: 115200
- Word Length: 8Bits
- Parity: None
- Stop Bits: 1
- DMA配置:
- 添加DMA通道:ADC1
- Mode: Circular
- Priority: High
- Memory Increment: Enable
- 时钟树配置:
- 确保PCLK2不超过72MHz(ADC时钟不超过14MHz)
- 推荐配置:
HCLK = 72MHz PCLK1 = 36MHz PCLK2 = 72MHz ADC Prescaler = PCLK2/6 = 12MHz
- 生成代码:
- Toolchain/IDE选择适合你的开发环境(MDK-ARM/IAR/STM32CubeIDE)
提示:CubeMX生成的HAL库代码已经包含了ADC和USART的初始化,但我们需要在用户代码区域添加应用逻辑。
2. ADC采集与DMA传输优化
传统轮询方式采集ADC会占用大量CPU资源,而中断方式在高采样率时也会导致频繁中断。使用DMA可以直接将ADC转换结果搬运到内存缓冲区,完全解放CPU资源。
DMA缓冲区定义与初始化:
在main.c文件中添加以下全局变量:
#define ADC_BUF_SIZE 256 uint16_t adcBuffer[ADC_BUF_SIZE]; float voltageBuffer[ADC_BUF_SIZE];在main()函数中的/* USER CODE BEGIN 2 */部分添加DMA启动代码:
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adcBuffer, ADC_BUF_SIZE);ADC值到实际电压的转换:
STM32F103的ADC为12位分辨率,参考电压通常为3.3V。创建转换函数:
void ADC_ConvertToVoltage(uint16_t* adcBuf, float* voltBuf, uint32_t size) { for(uint32_t i = 0; i < size; i++) { voltBuf[i] = (float)adcBuf[i] * 3.3f / 4095.0f; } }定时采样控制:
为了实现固定采样率,我们可以使用定时器触发ADC转换。在CubeMX中配置TIM2:
- 时钟源:Internal Clock
- 预分频器:72-1 (1MHz)
- 计数器周期:1000-1 (1kHz采样率)
- 触发输出(TRGO):Update Event
然后在ADC配置中:
- 选择"Timer 2 Trigger Out event"作为外部触发源
- 外部触发边沿:上升沿触发
注意:DMA循环模式会持续覆盖缓冲区数据,因此需要合理设计数据处理节奏,避免数据丢失。
3. 串口数据传输协议设计
直接将原始ADC数据通过串口发送效率低下且难以解析。我们需要设计一个简单高效的通信协议。
协议帧结构设计:
| 字节位置 | 内容 | 说明 |
|---|---|---|
| 0 | 0xAA | 帧头,用于同步 |
| 1 | 0x55 | 帧头第二字节 |
| 2 | 数据长度N | 后续数据字节数 |
| 3~N+2 | 数据内容 | 实际传输的数据 |
| N+3 | 校验和 | 前面所有字节的和的低8位 |
数据打包函数实现:
void USART_SendDataPacket(uint8_t* data, uint16_t size) { uint8_t packet[256 + 4]; // 最大256字节数据+4字节协议头尾 uint8_t checksum = 0; packet[0] = 0xAA; packet[1] = 0x55; packet[2] = size; for(int i = 0; i < size; i++) { packet[3 + i] = data[i]; } // 计算校验和 for(int i = 0; i < size + 3; i++) { checksum += packet[i]; } packet[size + 3] = checksum; HAL_UART_Transmit(&huart1, packet, size + 4, HAL_MAX_DELAY); }电压数据发送实现:
在主循环中定期发送电压数据:
/* USER CODE BEGIN WHILE */ while (1) { static uint32_t lastTick = 0; if(HAL_GetTick() - lastTick >= 100) { // 每100ms发送一次数据 lastTick = HAL_GetTick(); ADC_ConvertToVoltage(adcBuffer, voltageBuffer, ADC_BUF_SIZE); USART_SendDataPacket((uint8_t*)voltageBuffer, ADC_BUF_SIZE * sizeof(float)); } /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */4. Python上位机波形显示
使用Python的pySerial和Matplotlib库可以快速构建一个实时波形显示工具。
Python端代码实现:
import serial import struct import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation from collections import deque # 串口配置 ser = serial.Serial('COM3', 115200, timeout=1) # 替换为你的串口号 plt.style.use('ggplot') # 显示配置 MAX_POINTS = 500 x_data = deque(maxlen=MAX_POINTS) y_data = deque(maxlen=MAX_POINTS) fig, ax = plt.subplots() line, = ax.plot([], [], 'b-') ax.set_xlim(0, MAX_POINTS/10) ax.set_ylim(0, 3.3) ax.set_xlabel('Sample Point') ax.set_ylabel('Voltage (V)') ax.set_title('STM32 ADC Real-time Waveform') def parse_packet(data): if len(data) < 4 or data[0] != 0xAA or data[1] != 0x55: return None length = data[2] if len(data) < length + 4: return None checksum = sum(data[:-1]) & 0xFF if checksum != data[-1]: print(f"Checksum error: {checksum} != {data[-1]}") return None return data[3:3+length] def update(frame): # 读取串口数据 while ser.in_waiting > 0: header = ser.read(1) if header == b'\xAA': rest = ser.read(3) # 读取协议头剩余部分 if len(rest) == 3 and rest[0] == 0x55: length = rest[1] data = ser.read(length + 1) # 数据+校验和 packet = b'\xAA' + rest + data payload = parse_packet(packet) if payload: # 解析浮点数据 num_floats = len(payload) // 4 voltages = struct.unpack(f'<{num_floats}f', payload) # 更新显示数据 for i, v in enumerate(voltages): x_data.append(len(x_data)) y_data.append(v) # 更新曲线 line.set_data(x_data, y_data) # 调整X轴范围 if len(x_data) > MAX_POINTS/10: ax.set_xlim(x_data[-1] - MAX_POINTS/10, x_data[-1]) return line, ani = FuncAnimation(fig, update, blit=True, interval=50) plt.show()功能增强建议:
- 多通道支持:修改协议格式,包含通道信息
- 数据保存:添加按钮将当前数据保存为CSV
- 采样率显示:计算实际采样率并显示
- 触发功能:添加边沿触发功能稳定波形显示
5. 系统优化与调试技巧
一个健壮的数据采集系统需要考虑多方面因素。以下是几个关键优化点:
电源噪声抑制:
- 在ADC参考电压引脚添加10μF和0.1μF去耦电容
- 使用独立的LDO为模拟部分供电
- 避免数字信号线与模拟信号线平行走线
软件滤波算法:
- 移动平均滤波(简单有效)
#define FILTER_WINDOW 5 float movingAverageFilter(float newValue) { static float buffer[FILTER_WINDOW] = {0}; static uint8_t index = 0; static float sum = 0; sum -= buffer[index]; buffer[index] = newValue; sum += buffer[index]; index = (index + 1) % FILTER_WINDOW; return sum / FILTER_WINDOW; } - 中值滤波(适合消除脉冲噪声)
- 卡尔曼滤波(动态系统最优估计)
DMA双缓冲技术:当处理速度跟不上采集速度时,可以使用双缓冲技术:
#define BUF_SIZE 256 uint16_t adcBuffer1[BUF_SIZE]; uint16_t adcBuffer2[BUF_SIZE]; volatile uint8_t activeBuffer = 0; // 0 for buffer1, 1 for buffer2 // 在DMA完成中断中切换缓冲区 void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc) { activeBuffer = 1; // 处理adcBuffer1 } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { activeBuffer = 0; // 处理adcBuffer2 }常见问题排查:
无数据或数据全零:
- 检查CubeMX中ADC通道配置是否正确
- 测量实际输入电压是否在0-3.3V范围内
- 确认DMA配置为循环模式
数据不稳定:
- 检查电源稳定性
- 适当增加采样时间(ADC_SMPR寄存器)
- 添加软件滤波
串口数据错乱:
- 确认双方波特率一致
- 检查地线连接
- 降低传输速率或缩短数据包长度