2025寒假练-基于STM32G031的测试测量训练平台完成虚拟仪器实现
该项目使用了QT、STM32G031,实现了虚拟仪器的设计,它的主要功能为:显示波形以及基本的参数,参数包括被测信号的峰峰值和平均值、被测信号的周期、FFT后的频谱。。
标签
嵌入式系统
测试
显示
开发板
USB
HexZuo
更新2025-03-19
中国科学技术大学
43

2025寒假练-基于STM32G031的测试测量训练平台完成虚拟仪器实现

项目介绍

本项目使用硬禾学堂STM32G031简易示波器学习板,完成了基于STM32的虚拟仪器实现任务。主要实现了以下功能:

使用STM32G031片上ADC进行数据采集,利用串口发送到上位机虚拟仪器,虚拟仪器使用QT编程实现,对采样数据进行处理,包括显示波形、计算峰峰值、平均值、频率、频谱等。


硬件部分使用STM32G031平台进行数据采集,使用板上的OLED显示采集状态,利用板上的串口,通过USB同PC进行数据传输。

软件部分使用QT编写上位机,编写简单的仪器控制界面,实现通道示波器的的功能。 上位机主要实现了以下功能:显示波形以及基本的参数,参数包括被测信号的峰峰值和平均值、被测信号的周期、FFT后的频谱。
项目的详细介绍可以参考视频链接:

硬件介绍

项目的方案硬件框图如图所示:

image.png


项目整体使用cubeMX进行配置,整体配置如下所示:

image.png


  1. ADC转换驱动
    使用定时器TIM2驱动ADC进行转换,TIM2的配置如下:
    image.png
    ADC的配置如下:
    image.png
    16MHz的时钟,分频后得到1MHz,ARR = 100,得到10kHz的采样频率;所以采集的模拟数据的频率应该低于5kHz,所以此时有效的频谱的范围在0-5KHz之内。相邻两次采样的时间间隔为:0.1ms。
    考虑到采样率还有进一步提升的空间,将ARR设置为50,采样的时间间隔变为0.05ms。

image.png


ADC将采集到的数据通过DMA方式保存在数组中,同时开启连续转换模式,只用在转换完成设定次数后,才会触发ADC转换完成的中断回调函数。

  1. ADC中断回调函数
    ADC中断回调函数的处理逻辑如下:
void  HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc){
if( hadc->Instance == ADC1 ){
HAL_ADC_Stop_DMA( &hadc1);
// HAL_UART_Transmit(&huart2, &tx_frame ,sizeof(ADCFrame) , HAL_MAX_DELAY);
if(adc_conv_cnt< CONV_DATA_LEN ){
tx_frame.adc_value[adc_conv_cnt]= calc_average(adc_conv_data, CONV_DATA_AVG);
adc_conv_cnt++;
}
else{
adc_conv_cnt = 0;
HAL_UART_Transmit(&huart2, &tx_frame ,sizeof(ADCFrame) , HAL_MAX_DELAY);
}
HAL_ADC_Start_DMA( &hadc1, adc_conv_data , CONV_DATA_AVG);
// HAL_ADC_Start_DMA( &hadc1, tx_frame.adc_value , CONV_DATA_LEN);
}
}

其中的几个重要参数解释:
#define CONV_DATA_LEN 256 通过串口每次向上位机发送的转换数据的个数。
#define CONV_DATA_AVG 32,也就是DMA在获得CONV_DATA_AVG次ADC的转换结果之后,进入ADC的中断回调函数。
回调函数首先关闭ADC转换,统计已经获得的转换结果的次数。如果次数没有达到CONV_DATA_LEN,就计算转换结果的平均值,写入到数组中,转换次数加1。如果达到了,就通过串口将转换结果发送到上位机。发送结果封装为帧,帧的结构定义为:

typedef struct {
uint16_t header; // 0x55AA
uint16_t adc_value[CONV_DATA_LEN];
uint16_t footer; // 0xAA55
} ADCFrame;
extern ADCFrame tx_frame;

通过帧头与帧尾识别数据传输的开始与结束。
也就是说,收集32个数据取平均值,收集满256个数据后发送一段数据帧,每个数据帧代表 322560.05 = 409.6ms时间段内的数据。

如果要更加精细的显示更高频率的波形,可以通过修改TIM2的时钟改变采样率,减少CONV_DATA_AVG的值即可。但是每一帧数据在上位机的展示范围也会减小。

  1. ADC的校准
    控制ADC零漂,也就是ADC在没有输入时,其输出应该也是0,而且在噪声干扰下,转换结果也应该维持在零点上下。根据视频的教程,根据原理图计算
    image.png

    可以看出输入反相,二级管接1M电阻时,反馈回路中并联的电阻总阻值为0.5M欧姆,计算公式:
    $$
    可以计算得到,如果满足,需要,根据PWM的原理,输出1.1直流的电压,配置应该设置为;
    image.png

    实际的测试证明,ARR控制PWM的频率,该频率过低时,将不能保证输出的信号稳定在1.1V.

为了验证以上的结论,编写了一下代码进行测试,PWM的测试代码如下:

		uint32_t adc_data = HAL_ADC_GetValue(&hadc1);
uint16_t ccr = __HAL_TIM_GET_COMPARE(&htim3, TIM_CHANNEL_4);
if( adc_data < 2048 ) ccr++ ;
else ccr--;
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_4, ccr);
sprintf(tx_uart,"%d %d",adc_data,ccr);
OLED_ShowString(2, 40, tx_uart, 12, 1);
OLED_Refresh();

运行测试代码可以得到,ccr的取值稳定在了333附近,与理论计算的数据接近。

  1. OLED显示
    此部分在教程中有详细的介绍,可以参考网站视频教程,本次工程使用的是硬件SPI驱动OLED屏幕进行显示,驱动函数已经完成。

QT上位机软件设计

  • 软件流程图和关键代码介绍
  1. 上位机的功能如图所示:
    image.png

绘制图形时采用QCustomPlot库绘图,使用QSerialPort库进行串口间的通信。其他使用到的控件包括ComoboBox、PushButton、LCD。这些控件的使用很容易,在ui界面上拖动放在合适位置,编辑控件的名称、z在Mianwindow通过ui->name进行访问与设置,右键点击可以设置控件处理的槽函数。下面介绍一下各个功能的实现:

  1. 串口收发

首先扫描有效串口,在comoBox中显示。QSerialPortInfo::availablePorts()直接返回当前可用串口,可以进行设置。

    foreach (QSerialPortInfo info , QSerialPortInfo::availablePorts() ) {
QString str = info.portName()+":"+info.description();
ui->comobox_serialport->addItem(str );
}

对选中的串口进行设置:

    int i = ui->comobox_serialport->currentIndex();
QSerialPortInfo info = comList.at(i);
comPort.setPort( info );
comPort.setPortName( info.portName() );
comPort.setBaudRate( QSerialPort::Baud115200 );
comPort.setDataBits( QSerialPort::Data8 );
comPort.setStopBits(QSerialPort::OneStop );
comPort.setParity( QSerialPort::NoParity );
comPort.setFlowControl( QSerialPort::NoFlowControl );

使用方法comPort.open( QIODevice::ReadWrite )comPort.readAll()comPort.write(cmd)可以完成串口的打开、收发。

  1. 数据帧接收处理

前面有过数据帧的规定,由于发送的数据帧较长,serialport的缓冲区读取一次可能不能读取完整,所以有:

 if( readByte.startsWith("\x55\xAA") && is_rx_frame_start == false ){
is_rx_frame_start = true;
}
else if( readByte.endsWith("\xAA\x55") && is_rx_frame_start == true ){
···
···
}
else if( readByte.startsWith("\xCC\xAA") && readByte.endsWith("\xAA\xCC") && readByte.size()==8 ){
···
···
}
if( is_rx_frame_start == true ){
rx_buffer.append( readByte);
}

接受到一帧数据后,要去除帧头与帧尾,进行滤波,该上位机提供了两种滤波方式与无滤波处理,方便进行对比。

  • 使用脉冲滤波
    image.png
  • 不采用滤波
    image.png

滤波处理完成后的函数即可计算峰峰值与平均值,在lcd上进行显示。

3.使用Eigen库计算FFT
在QT上添加Eigen库,使C++支持FFT运算。
只需要调用fft.fwd(spectrum, input);方法即可。在计算之前需要去直流、添加汉宁窗:

    Eigen::VectorXcd spectrum(N);
input.array() -= input.mean();//去直流

Eigen::VectorXd window = Eigen::VectorXd::Zero(N);
for(int i = 0; i < N; ++i) {
window[i] = 0.5f * (1 - std::cos(2 * M_PI * i / (N - 1)));
}
input.array() *= window.array();//汉宁窗

FFT加汉宁窗通过抑制截断效应引起的频谱泄漏,平衡主瓣宽度与旁瓣衰减,适用于存在邻近干扰的情况。array()方法表示该数组以逐个元素的形式,而不是矩阵的形式,进行运算。

  1. 统计周期

计算周期可以通过寻找FFT中的最大频率分量得到,方法很简单,但是需要对齐FFT结果的下标与实际的频率分量:

 	plot_spectrum = eigenFFT(raw_num);
int max_index = 0;
double max = -10000.0;
for( int i=0;i<plot_spectrum.size();i++ ){
if( plot_spectrum[i]>max ){
max = plot_spectrum[i];
max_index = i;
}
}

第二种方法是通过计数的方式确定,通过迟滞比较的方法确定上升沿的个数,统计在这一段数据帧中,出现了对少个上升沿,然后除以这一段数据帧的时间上度,就可以得到1s内出现多少次上升沿,也就是周期。

    	const double HYSTERESIS_RATIO = 0.1; // 迟滞比例
double hysteresis = HYSTERESIS_RATIO * (threshold - Vmin);
bool lastState = false;
//double lastEdgeTime = -MIN_PERIOD*2;
for(int i=1; i< calc_period.size(); ++i){
//滞回比较, 上升沿检测
bool currentState = (calc_period[i] > threshold) && (calc_period[i-1] < (threshold - hysteresis));
if(currentState && !lastState){
rise_edge++;
}
lastState = currentState;
}
double step = (1.0/Fs)*2 * 32*256 ;
ui->lcd_period->display( rise_edge/step );


这两种方法切换可以通过调整comoBox的选中项进行。

心得体会

在参与电子森林2025寒假在家练“STM32虚拟仪器设计”活动的过程中,我深刻体会到理论与实践结合的重要性,同时也积累了嵌入式开发与虚拟仪器设计的宝贵经验。通过实践,我认识到虚拟仪器的核心优势在于灵活性:通过软件定义功能,硬件仅作为执行单元。此次活动不仅让我掌握了STM32与虚拟仪器的联合开发技术,更培养了从问题定义到系统实现的完整工程思维。


附件下载
qt-eeTree.zip
上位机文件
STM32-SerialPort.zip
QT程序文件
团队介绍
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号