2025寒假练 - 基于STM32实现采集外部声音信号并生成伯特图
一、项目介绍
本项目旨在构建一个基于STM32的简易示波器/频谱仪/信号发生器学习平台,实现外部声音信号的实时采集与频域分析。项目中,通过STM32的ADC模块采集外部麦克风信号,经由USB转串口芯片将采集到的数字数据传送到个人电脑。PC端利用Python程序实时处理数据,计算FFT后生成波特图(幅频特性图)以及相频特性图,以便直观展示音频信号的频谱分布和相位响应。该平台不仅为嵌入式信号采集与数据处理提供了实践平台,同时也为音频信号处理、滤波器设计、控制系统分析等领域奠定基础。
二、硬件平台简介
主要硬件说明
- MCU:STM32G031G8Ux
- 采用STM32G031G8Ux微控制器作为核心处理单元,具备高速ADC功能,支持PA0引脚(ADC1_IN0)进行外部信号采集,系统主频设定为64MHz,既满足实时采样要求,也保证了数据处理的稳定性。
- CH340 USB转串口芯片
- 该芯片实现了MCU与PC之间的高速串口通信。在本项目中,波特率设为1500000,能够高速传输采集到的音频数据,确保PC端获得足够的数据以实现准确的FFT分析。
- 外部麦克风信号采集电路
- 电路设计包括前置放大、滤波等环节,将来自麦克风的弱小音频信号放大到合适的电平,并滤除不必要的噪声,为ADC采集提供干净的信号源。
- 个人电脑
- PC端使用Python编写数据处理与图形显示程序,利用Matplotlib进行实时绘图,实现波特图(幅频特性图)和相频图的动态显示,为信号分析提供直观的可视化效果。
附上信号采集电路的仿真链接:传送门
三、方案框图和项目设计思路
方案框图
设计思路
- 信号采集
利用STM32的ADC功能对连接于PA0引脚的外部麦克风信号进行采样。为了保证采样精度和稳定性,项目采用STM32CubeMX进行初始代码生成,设定系统主频为64MHz,并配置USART1(实际使用的是MX_USART2_UART_Init,需注意具体硬件连线)以实现高速数据传输。 - 数据传输
采集到的数据通过CH340 USB转串口芯片传输至PC。数据格式采用简单的ASCII字符串格式,每个采样值后跟逗号分隔,便于PC端解析。 - 数据处理与FFT分析
PC端利用Python实时接收串口数据,进行数据缓冲、时间戳分配、清洗等预处理。通过NumPy库计算FFT,获得信号在频域内的幅值与相位数据。为了更准确地反映实际采样率,程序对一次读取内的多个数据采用均匀分配时间戳的方法进行处理。 - 波特图生成
将FFT结果中的幅值转换为dB(公式:20×log10(幅值)),并采用对数坐标绘制频率轴,设置幅频图显示范围为下限 -20 dB,上限 90 dB。相频图同样采用对数坐标显示频率,反映信号相位分布情况。
四、软件流程图和关键代码解析
软件流程图
关键代码解析
1. PC端数据处理与波特图生成代码
PC端的Python代码主要负责接收串口数据、处理数据、计算FFT并生成图表。以下为关键部分解析:
- 数据处理与时间戳分配
def process_data(raw_data):
该函数的核心在于通过字符串操作将原始数据分割成独立采样值,并利用
global data_buffer, timestamps, values, last_time
current_time = time.time() - start_time
try:
data_buffer += raw_data.decode('ascii', errors='replace')
samples = []
# 分割数据:以逗号为分隔符得到每个采样点
while "," in data_buffer:
comma_pos = data_buffer.find(",")
value_str = data_buffer[:comma_pos]
data_buffer = data_buffer[comma_pos+1:]
if value_str.isdigit():
value = int(value_str)
if 0 <= value <= 4095:
samples.append(value)
else:
logger.warning(f"数据超范围: {value}")
else:
logger.warning(f"无效数据格式: {value_str}")
# 多个样本均匀分配时间戳,确保采样率计算准确
if samples:
n = len(samples)
sample_times = np.linspace(last_time, current_time, n, endpoint=False) if n > 1 else [current_time]
timestamps.extend(sample_times)
values.extend(samples)
last_time = current_time
except Exception as e:
logger.error(f"数据处理异常: {e}")np.linspace
函数在上次读取时间与当前时间之间均匀分配时间戳,从而保证后续采样率计算的准确性。 - FFT计算与波特图更新
def update(frame):
该部分通过FFT变换获取频域数据,再将幅值转换为dB,利用Matplotlib实现波特图和相频图的实时更新。为了兼顾数据可视化,频率轴采用对数刻度,并对幅频图的上下限进行了固定设置。
try:
while ser.in_waiting > 0:
raw_data = ser.read(ser.in_waiting)
process_data(raw_data)
except serial.SerialException as e:
logger.error(f"串口读取错误: {e}")
current_time = time.time() - start_time
cutoff = current_time - MAX_DURATION
while timestamps and timestamps[0] < cutoff:
timestamps.pop(0)
values.pop(0)
if len(values) > 1 and len(timestamps) > 1:
dt = np.diff(timestamps)
fs = 1 / np.mean(dt) # 根据时间间隔计算采样率
fft_y = np.fft.rfft(np.array(values) - np.mean(values))
fft_freq = np.fft.rfftfreq(len(values), d=1/fs)
fft_magnitude = np.abs(fft_y)
fft_magnitude_db = 20 * np.log10(np.maximum(fft_magnitude, 1e-12))
fft_phase = np.angle(fft_y, deg=True)
# 更新波特图:幅值采用dB单位,显示范围固定为 -20 到 90 dB
line_freq.set_data(fft_freq, fft_magnitude_db)
ax_freq.set_xlim(max(1, fft_freq[1]), fs / 2)
ax_freq.set_ylim(-20, 90)
# 更新相频图
line_phase.set_data(fft_freq, fft_phase)
ax_phase.set_xlim(max(1, fft_freq[1]), fs / 2)
ax_phase.set_ylim(-180, 180)
return line_freq, line_phase
2. 单片机端代码解析
单片机端程序采用STM32CubeMX生成初始代码,并在KAIL_V5中进行主函数的编辑。以下是关键代码及其解析:
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_ADC1_Init();
MX_USART2_UART_Init();
/* USER CODE BEGIN 2 */
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
uint8_t str[3]; // 用于存放转换后的字符串数据
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
// 启动ADC采样
HAL_ADC_Start(&hadc1);
uint16_t value;
// 轮询等待ADC转换完成,超时设为100ms
if(HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK)
value = HAL_ADC_GetValue(&hadc1);
// 停止ADC采样,避免连续转换冲突
HAL_ADC_Stop(&hadc1);
// 将采集到的ADC数值转换为字符串,格式为“数值,”
int len = sprintf((char *)str, "%d,", value);
// 通过USART将数据发送至PC,等待超时100ms
HAL_UART_Transmit(&huart2, (uint8_t*)&str, len, 100);
}
/* USER CODE END 3 */
}
关键点解析:
- 系统初始化
代码首先调用HAL_Init()
函数初始化HAL库,然后调用SystemClock_Config()
配置系统时钟(64MHz)。后续调用MX_GPIO_Init()
、MX_ADC1_Init()
及MX_USART2_UART_Init()
分别初始化GPIO、ADC和串口外设,这些配置均由STM32CubeMX自动生成。 - ADC采样与数据发送
在无限循环中,程序通过HAL_ADC_Start()
启动ADC采样,并调用HAL_ADC_PollForConversion()
等待ADC转换完成(超时设为100ms),获取转换后的数值。采样结束后,调用HAL_ADC_Stop()
停止ADC以节省功耗。 - 数据格式化与串口传输
采集到的ADC数值通过sprintf()
函数转换为字符串格式,并在每个数值后添加逗号分隔符,便于PC端按照约定格式解析。最后,通过HAL_UART_Transmit()
函数将数据发送出去。
五、功能展示图及说明
在实际运行中,PC端会同时弹出两个图表窗口:
- 波特图(幅频图)
- 横轴为频率(Hz),采用对数刻度显示,下限设置为1Hz(避免对数坐标从0开始),上限为Nyquist频率(实际采样率的一半)。
- 纵轴显示信号幅值,单位为dB,范围固定在-20 dB至90 dB,直观展示各频率分量的增益情况。
- 相频图
- 同样采用对数坐标的频率轴,纵轴显示相位信息(单位为度),范围设定为-180°到180°,反映信号各频率点的相位响应。
六、项目中遇到的难题和解决方法
1. 数据时间戳分配与采样率计算
问题描述:
最初版本中,由于所有采样值在一次串口读取中使用相同时间戳,导致实际采样率计算偏低,进而影响FFT结果与频率显示范围。
解决方法:
采用numpy.linspace
在上次读取与当前读取时间间均匀分配时间戳,确保每个样本有独立时间标记,从而准确计算采样率,进而保证FFT计算的正确性。
2. 串口数据稳定性
问题描述:
在高速串口通信下,可能出现数据丢失或缓冲区溢出的问题,导致PC端数据处理异常。
解决方法:
在数据读取过程中增加异常捕捉机制,使用serial.SerialException
捕获错误,并通过日志记录异常情况。同时,优化数据缓冲区处理,确保每次读取数据完整后再进行分割与处理。
3. 波特图幅频范围设定
问题描述:
实际信号幅值变化较大,初期设定的动态范围难以同时兼顾弱信号和强信号,图表显示不够直观。
解决方法:
经过多次调试,将幅频图的显示范围固定为下限-20 dB,上限90 dB,既保证了较弱信号的显示,又不会因强信号过高而导致整体图表失衡。
4. 单片机与PC端通信协议的简化
问题描述:
如何设计简单且高效的数据传输协议,使得PC端能够快速、准确地解析单片机传输的连续数据。
解决方法:
采用逗号分隔的ASCII字符串作为数据格式,简单明了且便于解析,同时利用HAL库提供的UART函数实现高速传输,确保数据格式统一和传输稳定。
七、项目心得体会
通过本次“2025寒假练”项目,我对嵌入式系统与信号处理有了更深刻的理解。从硬件采集、数据传输到PC端数据处理与可视化,每一个环节都要求工程师对软硬件协同设计有较高的把控能力。具体体会如下:
- 硬件设计与调试
在STM32CubeMX的辅助下,硬件初始化及外设配置变得十分简便,但实际调试过程中仍需要关注ADC采样的精度与串口通信的稳定性。通过本项目,我深刻认识到硬件调试与系统集成的重要性。 - 数据处理与算法实现
PC端Python程序的实现过程使我体验到数据预处理和FFT算法在实时信号处理中的关键作用。尤其是时间戳分配和采样率计算问题,直接关系到FFT结果的准确性,这为我今后在信号处理领域的学习提供了宝贵经验。 - 系统集成与可视化
将硬件采集的数据实时传输、处理,并通过Matplotlib生成波特图和相频图,不仅锻炼了我的编程能力,也加深了我对数字信号处理和数据可视化的理解。直观的图表显示有助于快速定位问题,对实际工程调试具有重要意义。 - 问题解决与持续改进
项目中遇到的数据丢失、采样率计算不准、通信异常等问题,通过不断调试和优化得到了有效解决。整个过程强化了我对问题分析、方案设计与实施改进的能力,也让我体会到工程实践中的细节决定成败。
总之,本项目不仅提升了我的嵌入式系统开发技能,也让我在信号处理和数据可视化方面积累了宝贵经验。未来,我将继续探索更高效、更精确的音频信号处理方法,为实现更复杂的系统打下坚实基础。