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