2025寒假练 - 基于STM32G031实现虚拟仪器
该项目使用了STM32G031、C和C#语言,实现了虚拟仪器的设计,它的主要功能为:双通道信号采集和单通道信号发生,将采集到的信号进行FFT,得到其频谱图。除了可以使用单片机的按键,还可以使用上位机进行功能交互。
标签
嵌入式系统
STM32
USB
FFT
上位机
xianyv
更新2025-03-13
南昌大学
207

1.项目介绍

本项目是2025年硬禾学堂“寒假一起练——基于stm32的双通道简易示波器”的板卡硬件实现的具有双通道,电平触发,FFT变换,峰峰值、并且可以与上位机实现功能交互,在上位机显示波形图和FFT后的频谱图的一个主从机虚拟仪器系统。

2.硬件介绍

项目基于STM32G031的口袋仪器训练平台,采用Arm Cortex M0+内核,主频最高可达64MHz。使用一个光电旋转编码器用于控制输入操作,实现采用时间,电平触发,单元格幅值切换等操作。SPI接口的128*128分辨率OLED显示屏可以实现简单的波形显示和参数显示,用12bits ADC的双通道采集两路信号。设有PWM输出口实现单通道的信号输出。

3.方案框图和项目设计思路介绍

(1)方案框图

3.1方案框图

(2)项目设计思路

  • 任务要求在完成简易示波器的基础上,实现与上位机的交互,在PC端观察波形参数。所以任务要求我们不仅要在cubeide等嵌入式开放平台编程,还需要学习掌握部分上位机开发的编程。所以总的来看,项目需要分别完成从机和主机的代码编写,在调试过程中,利用串口通信实现不断修改调整。
  • 在单片机方面,项目需要利用到多个定时器(内部时钟,PWM输出,触发信号),ADC(信号采集),SPI(OLED),UART(串口通信),DMA(保证ADC数据的快速传输),Key(输入控制),Buzzer(功能响应提示)等等。
  • 在上位机方面,项目需要将单片机同PC连接起来,利用PC的数据处理能力和大屏幕显示,实现更好的测试测量效果。具体需要完成:a.串口数据接收.b.多线程执行及调用.c.滤波处理及双通道波形显示.d.FFT快速傅里叶变换及频谱显示.e.信号发生器界面显示.f.上位机窗口参数设置等等。

4.软件流程图和关键代码介绍

(1)软件流程图

4.1软件流程框图

如图所示,本项目的软件流程分为从机(单片机)和上位机(PC)两个部分,两者之间利用串口进行通信和交互。

(2)关键代码

A.单片机(CubeIDE-C语言)

1.串口重定向:通过串口重定向,之后发送数据可以使用printf()函数,像c语言打印数据一样的格式像串口发送数据,即printf(“对应的打印符",变量);

  • ptr指向要发送的数据的指针。
  • len要发送的数据的长度。
  • 0xffff发送超时时间,单位为毫秒。这里设置为 0xffff 表示无限等待,直到数据发送完成。

下面为串口重定向的声明和使用处的部分代码。

extern UART_HandleTypeDef hlpuart1;

__attribute__((weak)) int _write(int file, char *ptr, int len)
{
if(HAL_UART_Transmit(&hlpuart1,ptr,len,0xffff) != HAL_OK)
{
Error_Handler();
}
}

串口重定向(位置...\core\Inc\uart.h)

for (uint8_t k = 0; k < SCOPE_CHANNEL_NUM; k++) {
float voltage = toVoltage(sample->data[sample->sp + j_int][k]);
uint16_t val = Voltage_To_Coordinate(voltage);
if(k == 0){printf("C1%d\r\n",val);}
if(k == 1){printf("C2%d\r\n",val);}
if (j_int != 0)
OLED_DrawLine(SCOPE_X_MIN + last_i[k], last_val[k], SCOPE_X_MIN + i, val, 1);
last_i[k] = i;
last_val[k] = val;
}

利用printf()函数像上位机发送两个通道采集的数据(位置...\core\App\Scope\UI.c)


2.触发位置调整:为了确保波形显示的美观,同时保证波形显示效果稳定,此处利用下降沿触发计算与方差中心最小的点,以该点作为触发点,显示采集到的波形。

  • edges_cnt[!scope_tri_edge] 表示非当前触发边沿的边沿数量。由于输入反相,输入上升沿对应数据下降沿,所以使用 !scope_tri_edge 来访问边沿数组。
  • 对于每个边沿位置 edges[!scope_tri_edge][i],计算其与样本中心位置的差值 diff
  • 如果 diff 小于当前的 min_diff,则更新 min_diff  min_diff_p
uint16_t min_diff = UINT16_MAX, min_diff_p = 0;
for (uint16_t i = 0; i < edges_cnt[!scope_tri_edge]; i++) { // 因输入反相,输入上升沿是数据下降沿
int diff = abs((int) edges[!scope_tri_edge][i] * 2 - SCOPE_SAMPLE_NUM);
if (diff < min_diff) {
min_diff = diff;
min_diff_p = edges[!scope_tri_edge][i];
}
}
uint16_t tri_p = min_diff_p;

调整显示波形触发点(位置...\Core\App\Scope\sample.c)

B.上位机(Visual Studio窗体应用编程-C#语言)

1.FFT频谱图

此处使用了MathNet.Numerics.IntegralTransforms库中的Fourier.Forward()函数,将complexData复数数组内的数据自动进行FFT变换。

  • data.Average()计算了一段数据点内元素的平均值。这段平均值可以看做是直流分量的大小。
  • ToList() 方法将经过转换后的元素收集到一个新的 List<double> 中,这个新列表 dataWithoutDC 就是去除了直流分量的数据。
  • Complex 是 .NET 中用于表示复数的类型,它的构造函数 new Complex(x, 0) 表示创建一个实部为 x,虚部为 0 的复数。使用 Select 方法对 dataWithoutDC 中的每个元素进行转换,将其转换为对应的复数。
  • ToArray() 方法将转换后的复数元素收集到一个 Complex 类型的数组 complexData 中,因为 FFT 算法通常需要处理复数形式的数据。
            // 计算直流分量
double dcComponent = data.Average();
// 去除直流分量
List<double> dataWithoutDC = data.Select(x => (double)x - dcComponent).ToList();
// 将输入数据转换为复数数组
Complex[] complexData = dataWithoutDC.Select(x => new Complex(x, 0)).ToArray();
// 执行 FFT 变换
Fourier.Forward(complexData, FourierOptions.NoScaling);
// 计算采样率和频率分辨率
double frequencyResolution = currentSampleRate * 8 / complexData.Length;
// 清空之前的 FFT 数据点
fftSeries.Points.Clear();

部分FFT变换代码

2.组合滤波器

由于传输过程中存在不可避免的噪声干扰,波形时常会有尖波和混叠的情况产生,于是我引入了组合滤波器对波形数据进行滤波。

  • 初始化输出列表:创建一个空的列表 filteredData 用于存储滤波后的结果。
  • 遍历输入数据 i 大于等于 MedianWindowSize - 1 时,从输入数据中选取长度为 MedianWindowSize 的窗口数据,依次应用中值滤波器和自适应阈值滤波器,并将滤波后的结果添加到 filteredData 中。 i 小于 MedianWindowSize - 1 时,直接将输入数据的元素添加到 filteredData 中。
  • 返回滤波结果:返回 filteredData
private List<int> ApplyCombinedFilter(List<int> data)
{
List<int> filteredData = new List<int>();
for (int i = 0; i < data.Count; i++)
{
if (i >= MedianWindowSize - 1)
{
var window = data.Skip(i - MedianWindowSize + 1).Take(MedianWindowSize).ToList();
int medianFiltered = MedianFilter(window);
int adaptiveFiltered = AdaptiveThresholdFilter(window, medianFiltered);
filteredData.Add(adaptiveFiltered);
}
else
{
filteredData.Add(data[i]);
}
}
return filteredData;
}

组合滤波器遍历数据

5.功能展示图及说明

(单片机部分)

单片机OLED显示波形

在时间刻度为5ms时,通道一采集 在时间刻度为1ms时,通道二采集

转动旋钮按键,可以切换OLED显示界面的时间刻度。按下旋钮按键可以切换触发方式(上升沿或者下降沿)。

OLED参数界面,左下角的两个,从上而下分别是通道一和通道二的峰峰值,中间的两个分别是其中的直流分量大小。最右边两个分别是两个通道的频率(对于大于200Hz的频率,频率检查效果很差,图中的频率为200Hz)。

(上位机部分)

通道一的波形和频谱图显示(输入波形为方波,频率为200Hz)

通道二的波形和频谱图显示(输入波形为三角波,频率为200Hz)

信号发生器显示界面

6.项目中遇到的难题和解决方法

本次项目遇到了不少的困难与挑战。尽管有些问题仍然没有被很好的解决,但是大部分的问题都在不断地尝试中被攻克。

例如在波形显示的过程中,起初上位机显示的波形效果很糟糕,存在很多的杂波参数测量也很差。我第一时间想到的是串口通信的数据接收有问题,但尽管我将串口发送的代码不断优化,最终接收到的数据难免会有不少的噪声干扰。其噪声程度随发送速率的增大而增大,但是降低发送频率又会显著影响上位机的示波效果FFT,容易造成上位机与单片机的数据滞后。在我百思不得其解的时候,我想到了最近爆火的DeepSeek等AI模型,希望从AI上找到解决办法。AI的回答是利用滤波模型,对波形的噪声进行衰减,例如滑动窗口滤波、中值滤波等等。再经过不断地组合调试后,现在波形显示的噪声明显减少了,大大降低了波形的失真。

7.尚未实现或有瑕疵的功能

1.PWM的单通道信号输出:一开始我是用定时器3作为ADC采集的触发事件,因为PWM需要使用定时器三,所以我切换为了定时器二,但是结果证明,当定时器二作为ADC采集且定时器开启了PWM的通道三之后,我的程序运行会变得很卡,同时像按键的触发也基本失灵,截止报告提交时,这个问题暂时未解决。

2.周期的检测不灵敏:周期的检查刷新很慢,而且不是很准确。未来的解决思路是

  • 用数组对一段数据进行储存,单独处理并分析一段数据,得出周期的结果再释放。
  • 优化周期的检测机制,研究新的判断方法。
  • 进一步优化滤波模型,降低噪声对周期计算的影响。

8.对本次活动的心得体会

本次活动给我留下了深刻且难忘的印象。通过完成本次活动,我的嵌入式开发能力的不仅得到了提升,同时还促使学会了上位机编程等新的技术,可谓是受益良多。伴随着虚拟仪器的一步步构建,我的内心也获得了很大的满足感和成就感。希望今后还能多多参加这类的活动。


软硬件
元器件
STM32G031G8U6
主流Arm Cortex-M0+ MCU,具有64 KB Flash存储器、8 KB RAM、64 MHz CPU、2x USART、定时器、ADC和通信接口,1.7-3.6V
附件下载
Windows_chart_serial.zip
上位机基于VS编写的窗体应用工程压缩包
Windows_chart_serial.exe
窗体应用程序(上位机程序)
YinHexuetang.zip
STM32G031基于CubeIDE的项目工程文件
YinHexuetang.bin
STM32G031烧录用的二进制文件
团队介绍
潘杨政。南昌大学,大二,测控技术与仪器专业。
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号