2025寒假练 - 基于STM32实现采集外部声音信号并生成伯特图
该项目使用了STM32的简易示波器/频谱仪/信号发生器学习平台,实现了ADC采集外部音频信号,将采集后的数据通过串口发送至PC的设计,它的主要功能为:PC接受并处理串口接受的数据,最终将音频信号的幅频特性和相频特性显示在图表上。
标签
ADC
开发板
RombieHHH
更新2025-03-13
北京邮电大学
102

2025寒假练 - 基于STM32实现采集外部声音信号并生成伯特图

一、项目介绍

本项目旨在构建一个基于STM32的简易示波器/频谱仪/信号发生器学习平台,实现外部声音信号的实时采集与频域分析。项目中,通过STM32的ADC模块采集外部麦克风信号,经由USB转串口芯片将采集到的数字数据传送到个人电脑。PC端利用Python程序实时处理数据,计算FFT后生成波特图(幅频特性图)以及相频特性图,以便直观展示音频信号的频谱分布和相位响应。该平台不仅为嵌入式信号采集与数据处理提供了实践平台,同时也为音频信号处理、滤波器设计、控制系统分析等领域奠定基础。

二、硬件平台简介

主要硬件说明

  1. MCU:STM32G031G8Ux
    • 采用STM32G031G8Ux微控制器作为核心处理单元,具备高速ADC功能,支持PA0引脚(ADC1_IN0)进行外部信号采集,系统主频设定为64MHz,既满足实时采样要求,也保证了数据处理的稳定性。
  2. CH340 USB转串口芯片
    • 该芯片实现了MCU与PC之间的高速串口通信。在本项目中,波特率设为1500000,能够高速传输采集到的音频数据,确保PC端获得足够的数据以实现准确的FFT分析。
  3. 外部麦克风信号采集电路
    • 电路设计包括前置放大、滤波等环节,将来自麦克风的弱小音频信号放大到合适的电平,并滤除不必要的噪声,为ADC采集提供干净的信号源。
  4. 个人电脑
    • PC端使用Python编写数据处理与图形显示程序,利用Matplotlib进行实时绘图,实现波特图(幅频特性图)和相频图的动态显示,为信号分析提供直观的可视化效果。

附上信号采集电路的仿真链接:传送门


三、方案框图和项目设计思路

方案框图

设计思路

  1. 信号采集
    利用STM32的ADC功能对连接于PA0引脚的外部麦克风信号进行采样。为了保证采样精度和稳定性,项目采用STM32CubeMX进行初始代码生成,设定系统主频为64MHz,并配置USART1(实际使用的是MX_USART2_UART_Init,需注意具体硬件连线)以实现高速数据传输。
  2. 数据传输
    采集到的数据通过CH340 USB转串口芯片传输至PC。数据格式采用简单的ASCII字符串格式,每个采样值后跟逗号分隔,便于PC端解析。
  3. 数据处理与FFT分析
    PC端利用Python实时接收串口数据,进行数据缓冲、时间戳分配、清洗等预处理。通过NumPy库计算FFT,获得信号在频域内的幅值与相位数据。为了更准确地反映实际采样率,程序对一次读取内的多个数据采用均匀分配时间戳的方法进行处理。
  4. 波特图生成
    将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):
    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单位,显示范围固定为 -2090 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
    该部分通过FFT变换获取频域数据,再将幅值转换为dB,利用Matplotlib实现波特图和相频图的实时更新。为了兼顾数据可视化,频率轴采用对数刻度,并对幅频图的上下限进行了固定设置。

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端会同时弹出两个图表窗口:

  1. 波特图(幅频图)
    • 横轴为频率(Hz),采用对数刻度显示,下限设置为1Hz(避免对数坐标从0开始),上限为Nyquist频率(实际采样率的一半)。
    • 纵轴显示信号幅值,单位为dB,范围固定在-20 dB至90 dB,直观展示各频率分量的增益情况。
  2. 相频图
    • 同样采用对数坐标的频率轴,纵轴显示相位信息(单位为度),范围设定为-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生成波特图和相频图,不仅锻炼了我的编程能力,也加深了我对数字信号处理和数据可视化的理解。直观的图表显示有助于快速定位问题,对实际工程调试具有重要意义。
  • 问题解决与持续改进
    项目中遇到的数据丢失、采样率计算不准、通信异常等问题,通过不断调试和优化得到了有效解决。整个过程强化了我对问题分析、方案设计与实施改进的能力,也让我体会到工程实践中的细节决定成败。

总之,本项目不仅提升了我的嵌入式系统开发技能,也让我在信号处理和数据可视化方面积累了宝贵经验。未来,我将继续探索更高效、更精确的音频信号处理方法,为实现更复杂的系统打下坚实基础。

附件下载
源代码.zip
单片机及上位机源代码(需安装python环境)
团队介绍
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号