内容介绍
内容介绍
1、项目介绍
项目要求如下:
- 使用STM32G031平台进行数据采集及信号产生
- 通过USB同PC进行数据传输
- 在上位机上编写简单的仪器控制界面,实现双通道示波器和单通道信号发生器的功能 - 显示波形以及基本的参数
a.被测信号的峰峰值和平均值
b.被测信号的周期
c.FFT后的频谱
2、简短的使用到的硬件介绍
- 使用了硬件SPI驱动OLED
- 使用了外部中断去检测按键与编码器旋钮
- TIM3去作为PWM信号发生
- 两个GPIO引脚去控制放大器倍数
- TIM14和TIPM16的PWM引脚控制直流偏置
- PA1与PA7为ADC1的两个通道
3、方案框图和项目设计思路介绍
通过对项目要求的思考,为了减少下位机的负担,我觉得仅使用下位机做采集的功能,不适用下位机去进行FFT和频率周期等等参数的运算,通过STM32的ADC功能采集了数据之后,通过串口去发送给上位机,在上位机去完成计算。并且使用上位机去发送信号发生器的数据给下位机。OLED与按键编码器是为了方便调试的时候进行数据的查看的。
4、软件流程图和关键代码介绍
1、串口通信协议
串口通信协议是上位机与下位机交互的主要部分,也就是这一部分将数据互相传输,才能使得上位机正常显示。
//上位机发送部分//
void Widget::Send_Data()
{
QByteArray Data;
//起始位校验//
Data.append(static_cast<uint8_t>(Frame_Header));
//起始位校验//
//00不设置 11方波 22三角波 33正弦波//
if(Wave_Flag == 1)Data.append(static_cast<uint8_t>(0x11));
else Data.append(static_cast<uint8_t>(0x00));
// else if(Wave_Flag == 2)Data.append(static_cast<uint8_t>(0x22));
// else if(Wave_Flag == 3)Data.append(static_cast<uint8_t>(0x33));
//00不设置 11方波 22三角波 33正弦波//
Data.append(static_cast<char>(STM_Duty >> 8));
Data.append(static_cast<char>(STM_Duty & 0xFF));
Data.append(static_cast<char>(STM_Freq >> 8));
Data.append(static_cast<char>(STM_Freq & 0xFF));
//结束位校验//
Data.append(static_cast<uint8_t>(Frame_End));
//结束位校验//
if (Serial_Port->isOpen())
{
qint64 bytesWritten = Serial_Port->write(Data);
if (bytesWritten == -1)qDebug() << "Failed to write to serial port:" << Serial_Port->errorString();
else if (bytesWritten != Data.size())qDebug() << "Incomplete write to serial port.";
else qDebug() << "Data sent successfully, bytes written:" << bytesWritten;
}
}
//上位机发送部分//
//上位机接收部分//
void Widget::Process_Data()
{
int Temp;
//数据过少直接退出//
if(Adc_Buf.size() < 5)
{
qDebug() << "Buffer is less than 5";
Adc_Buf.clear();
return;
}
//数据过少直接退出//
//起始位校验//
Temp = Adc_Buf.indexOf(Frame_Header);
if(Temp == -1)
{
qDebug() << "Frame header not found";
Adc_Buf.clear();
return;
}
else if(Temp > 0)
{
Adc_Buf.remove(0, Temp);
return;
}
//起始位校验//
//获取数据长度//
int Length = static_cast<uint8_t>(Adc_Buf.at(1)) * 2 * 2;
if(Length < 0)
{
qDebug() << "Invalid data length";
Adc_Buf.clear();
return;
}
//获取数据长度//
//数据不完整//
if(Adc_Buf.size() < 3 + Length)
{
qDebug() << "Data is incomplete.";
return;
}
//数据不完整//
//提取数据//
QByteArray Packet = Adc_Buf.mid(2, Length);
//提取数据//
//结束位校验//
Temp = Adc_Buf.size() - 1;
if(Adc_Buf[Temp] == Frame_End)
{
if(Packet.size() % 2 == 0)
{
for(int i = 0;i < Packet.size();i += 4)
{
//解析数据//
uint16_t CH1_High_Byte = static_cast<uint16_t>(static_cast<uint8_t>(Packet[i])) << 8;
uint16_t CH1_Low_Byte = static_cast<uint8_t>(Packet[i + 1]);
uint16_t CH1_Value = CH1_High_Byte | CH1_Low_Byte;
uint16_t CH2_High_Byte = static_cast<uint16_t>(static_cast<uint8_t>(Packet[i+2])) << 8;
uint16_t CH2_Low_Byte = static_cast<uint8_t>(Packet[i + 3]);
uint16_t CH2_Value = CH2_High_Byte | CH2_Low_Byte;
//解析数据//
//存储数据//
Adc_Data_CH1.append(CH1_Value);
Adc_Data_CH2.append(CH2_Value);
//存储数据//
}
}
Adc_Buf.remove(0, 3 + Length);
}
else
{
//qDebug() << "Frame end not found";
Adc_Buf.clear();
return;
}
//结束位校验//
}
//上位机接收部分//
//下位机发送部分//
void Usart_Send_uint16(uint16_t Data)
{
uint8_t Temp;
Temp = (Data >> 8) & 0xFF;
HAL_UART_Transmit(&hlpuart1, &Temp, 1, HAL_MAX_DELAY);
Temp = (Data & 0xff);
HAL_UART_Transmit(&hlpuart1, &Temp, 1, HAL_MAX_DELAY);
}
void Send_Data(uint16_t *Array, uint8_t Array_Length)
{
uint8_t Temp;
//发送起始位//
Temp = Start_Flag;
HAL_UART_Transmit(&hlpuart1, &Temp, 1, HAL_MAX_DELAY);
//发送起始位//
//发送数据长度//
Temp = Array_Length;
HAL_UART_Transmit(&hlpuart1, &Temp, 1, HAL_MAX_DELAY);
//发送数据长度//
//发送数据//
for(uint16_t i = 0;i < Array_Length * 2;i++)
{
//CH1//
if(i % 2 != 0)Usart_Send_uint16(Array[i]);
//CH1//
//CH2//
if(i % 2 == 0)Usart_Send_uint16(Array[i]);
//CH2//
}
//发送数据//
//发送结束位//
Temp = End_Flag;
HAL_UART_Transmit(&hlpuart1, &Temp, 1, HAL_MAX_DELAY);
//发送结束位//
}
//下位机发送部分//
//下位机接收部分//
if(Receive_Flag)
{
Send_Data(Adc_Buf,200);
Duty = (Receive_Buf[2] << 8) | Receive_Buf[3];
Freq = (Receive_Buf[4] << 8) | Receive_Buf[5];
//载入数据//
if(Receive_Buf[1] == 0x11)Set_Pwm(htim3, TIM_CHANNEL_3, Freq, Duty);
//载入数据//
Receive_Flag = 0;
}
//下位机接收部分//
2、波形基本参数计算
通过过零点法去计算出波形的周期、频率、峰峰值、平均值等等参数,并且显示到Qt界面上
void Widget::Calculate_Period_And_Frequency()
{
if(Volatage_Flag == 0 && Adc_Data_CH1.isEmpty() && Adc_Data_CH2.isEmpty())return;
if((Volatage_Flag == 1 || Spectrum_Flag == 1) && Adc_Data_CH1.isEmpty())return;
if((Volatage_Flag == 2 || Spectrum_Flag == 2) && Adc_Data_CH2.isEmpty())return;
uint16_t minValue_CH1 = 4095; //最小值
uint16_t maxValue_CH1 = 0; //最大值
uint32_t sum_CH1 = 0; //用于计算平均值
uint16_t minValue_CH2 = 4095; //最小值
uint16_t maxValue_CH2 = 0; //最大值
uint32_t sum_CH2 = 0; //用于计算平均值
for(int i = 0; i < Adc_Data_CH1.size(); i++)
{
if (Adc_Data_CH1[i] < minValue_CH1) minValue_CH1 = Adc_Data_CH1[i];
if (Adc_Data_CH1[i] > maxValue_CH1) maxValue_CH1 = Adc_Data_CH1[i];
sum_CH1 += Adc_Data_CH1[i];
}
for(int i = 0; i < Adc_Data_CH2.size(); i++)
{
if (Adc_Data_CH2[i] < minValue_CH2) minValue_CH2 = Adc_Data_CH2[i];
if (Adc_Data_CH2[i] > maxValue_CH2) maxValue_CH2 = Adc_Data_CH2[i];
sum_CH2 += Adc_Data_CH2[i];
}
//计算峰峰值//
float Peak_Peak_CH1 = static_cast<float>(maxValue_CH1 - minValue_CH1);
Peak_Peak_CH1 = Peak_Peak_CH1 * 3.3 / 4095;
float Peak_Peak_CH2 = static_cast<float>(maxValue_CH2 - minValue_CH2);
Peak_Peak_CH2 = Peak_Peak_CH2 * 3.3 / 4095;
//计算峰峰值//
//计算平均值//
float Ave_Value_CH1 = static_cast<float>(sum_CH1) / Adc_Data_CH1.size();
Ave_Value_CH1 = Ave_Value_CH1 * 3.3 / 4095;
float Ave_Value_CH2 = static_cast<float>(sum_CH2) / Adc_Data_CH2.size();
Ave_Value_CH2 = Ave_Value_CH2 * 3.3 / 4095;
//计算平均值//
//计算周期和频率//
double Ch1_Rate = 50000; //采样率
double CH1_Period;
int CH1_Frequency;
double Ch2_Rate = 50000; //采样率
double CH2_Period;
int CH2_Frequency;
double Zero_Point_CH1 = static_cast<float>(sum_CH1) / Adc_Data_CH1.size();
double Zero_Point_CH2 = static_cast<float>(sum_CH2) / Adc_Data_CH2.size();
QVector<int> Zero_CH1; //过零点
QVector<int> Zero_CH2; //过零点
for(int i = 1; i < Adc_Data_CH1.size(); i++)
{
if
(
(Adc_Data_CH1[i - 1] < Zero_Point_CH1 && Adc_Data_CH1[i] >= Zero_Point_CH1) || // 从下到上过零
(Adc_Data_CH1[i - 1] > Zero_Point_CH1 && Adc_Data_CH1[i] <= Zero_Point_CH1)
)Zero_CH1.append(i);
}
if(Zero_CH1.size() < 2)
{
CH1_Frequency = 0;
CH1_Period = 0;
}
else
{
double TotalInterval = 0;
for(int i = 1; i < Zero_CH1.size(); i++)TotalInterval += (Zero_CH1[i] - Zero_CH1[i - 1]);
double averageInterval = TotalInterval / (Zero_CH1.size() - 1);
CH1_Period = 2 * (averageInterval / Ch1_Rate);
CH1_Frequency = static_cast<int>(1 / CH1_Period);
}
for(int i = 1; i < Adc_Data_CH2.size(); i++)
{
if
(
(Adc_Data_CH2[i - 1] < Zero_Point_CH2 && Adc_Data_CH2[i] >= Zero_Point_CH2) || // 从下到上过零
(Adc_Data_CH2[i - 1] > Zero_Point_CH2 && Adc_Data_CH2[i] <= Zero_Point_CH2)
)Zero_CH2.append(i);
}
if(Zero_CH2.size() < 2)
{
CH2_Frequency = 0;
CH2_Period = 0;
}
else
{
double TotalInterval = 0;
for(int i = 1; i < Zero_CH2.size(); i++)TotalInterval += (Zero_CH2[i] - Zero_CH2[i - 1]);
double averageInterval = TotalInterval / (Zero_CH2.size() - 1);
CH2_Period = 2 * (averageInterval / Ch2_Rate);
CH2_Frequency = static_cast<int>(1 / CH2_Period);
}
//计算周期和频率//
//ui显示//
ui->One_Ave->setText(QString::number(Ave_Value_CH1));
ui->One_PeakPeak->setText(QString::number(Peak_Peak_CH1));
ui->One_Freq->setText(QString::number(CH1_Frequency));
ui->One_Period->setText(QString::number(CH1_Period, 'f', 6));
ui->Two_Ave->setText(QString::number(Ave_Value_CH2));
ui->Two_PeakPeak->setText(QString::number(Peak_Peak_CH2));
ui->Two_Freq->setText(QString::number(CH2_Frequency));
ui->Two_Period->setText(QString::number(CH2_Period, 'f', 6));
//ui显示//
}
3、电压的计算与显示
通过通信协议获取数据之后将AD值转换为电压,然后显示到Qt图表中
//显示CH1时域//
Series1->clear();
for(int i = 0; i < Adc_Data_CH1.size(); i++)
{
//刷新图表//
double Voltage = Adc_Data_CH1[i] * 3.3 / 4095;
Series1->append(i, Voltage);
Chart->axisX()->setRange(0, 200);
Chart->axisY()->setRange(0,3.3);
//刷新图表//
}
//显示CH1时域//
4、频谱的计算与显示
通过Kiss_FFT库里面的FFT算法。去计算出频谱然后显示到Qt图表中
void Widget::Calculate_Spectrum()
{
//将时域数据填充到FFT输入//
int dataSize;
if(Spectrum_Flag == 1)dataSize = Adc_Data_CH1.size();
if(Spectrum_Flag == 2)dataSize = Adc_Data_CH2.size();
for(int i = 0;i < fftSize;i++)
{
if(i < dataSize)
{
if(Spectrum_Flag == 1)fftInput[i].r = Adc_Data_CH1[i] * 3.3 / 4095; // 实部
if(Spectrum_Flag == 2)fftInput[i].r = Adc_Data_CH2[i] * 3.3 / 4095; // 实部
fftInput[i].i = 0; // 虚部(设为 0)
}
else
{
fftInput[i].r = 0;
fftInput[i].i = 0;
}
}
//将时域数据填充到FFT输入//
//执行FFT//
kiss_fft(fftConfig, fftInput, fftOutput);
//执行FFT//
//计算频谱幅度//
QVector<double> spectrumMagnitude(fftSize / 2);
for(int i = 0; i < fftSize / 2; i++)
{
double magnitude = sqrt(fftOutput[i].r * fftOutput[i].r + fftOutput[i].i * fftOutput[i].i);
spectrumMagnitude[i] = magnitude;
}
//计算频谱幅度//
//更新频谱图表//
Update_Spectrum_Chart(spectrumMagnitude);
//更新频谱图表//
}
void Widget::Update_Spectrum_Chart(const QVector<double>& spectrumMagnitude)
{
if(Spectrum_Flag == 1)Series3->clear();
if(Spectrum_Flag == 2)Series4->clear();
double Rate = 500000; // 采样率
double frequencyResolution = Rate / fftSize;
for (int i = 0; i < spectrumMagnitude.size(); i++)
{
double frequency = i * frequencyResolution;
if(Spectrum_Flag == 1)Series3->append(frequency, spectrumMagnitude[i]);
if(Spectrum_Flag == 2)Series4->append(frequency, spectrumMagnitude[i]);
}
Chart->axisX()->setRange(0, Rate / 2);
Chart->axisY()->setRange(0, *std::max_element(spectrumMagnitude.begin(), spectrumMagnitude.end()));
}
5、功能展示图及说明
- 单通道采集图:
接入板子的PA0接口,通过ADC去获取数值,然后通过串口去发送给上位机处理数据并且显示出来。
2.双通道采集图
接入板子的PA0与PA7接口,通过ADC去获取数值,然后通过串口去发送给上位机处理数据并且显示出来。
3.波形频谱显示
点击波形切换频谱
3.信号发生器设置
6、项目中遇到的难题和解决方法
- 没有了解过FFT,不知道如何实现。解决方法:使用KissFFT轻量化库,在网上找相关的资料去移植。
- 之前只接触过STM32F103系列的单片机,仅了解官方标准库的编程,但是本次STMG031系列的单片机不支持标准库的编程。解决方法:学习HAL库的编程。
- 刷新图表过于频繁,导致卡顿。解决方法:使用定时器,在定时器中的槽函数中去刷新图表。
7、对本次活动的心得体会(包括意见或建议)
- 学习到了上位机的编程,了解了Qt的基本使用方法。
- 学习到了STM32HAL库的编程,以及CUBEIDE的使用。
- 建议以后的活动可以再进一步的升级使用的主控芯片,并且可以详细的出一下系统的上位机学习教程。
软硬件
附件下载
Oscillograph.rar
上位机
Oscillograph.rar
下位机
团队介绍
基于STM32G031 && Qt上位机实现简易示波器、频谱仪、信号发生器
评论
0 / 100
查看更多
猜你喜欢
2022年寒假在家一起练活动平台汇总- 完成任务即返款!精选5个平台,为充分利用寒暑假时间,做更有意义的事,将所学理论知识用到实践中,提升个人技能,并为参加电赛的同学做训练。
Lucia
20279
制作FPGA电子琴1. 存储一段音乐,并可以进行音乐播放,
2. 可以自己通过板上的按键进行弹奏,支持两个按键同时按下(和弦)并且声音不能失真,板上的按键只有13个,可以通过有上方的“上“、”下”两个按键对音程进行扩展
john
1372
WeDesign第4期:基于STM32G031核心模块的扩展板设计基于STM32G031的最小系统模块,运行Arm Cortex M0+内核,工作频率为64MHz,通过USB供电和配置,最多支持18个输入输出,其中6个可以为模拟输入。本期要求大家基于这个小模块,设计一款扩展板,并进行调试。
WeDesign
3203