基于STM32的便携式数字示波器设计
项目开发背景
数字示波器在电子测量领域扮演着关键角色,用于实时捕获和分析电信号,支持电路设计、调试和故障排查等应用。然而,传统台式示波器通常体积较大、成本较高,且依赖于交流电源,难以满足现场测试、移动工作或教育场景中对便携性和经济性的需求。这导致许多小型实验室、学生项目或户外工程任务中缺乏便捷的测量工具。
随着嵌入式技术和微控制器的进步,便携式数字示波器逐渐成为解决方案,通过集成高性能处理器和低功耗组件,实现小型化与功能多样化的平衡。STM32系列单片机以其强大的处理能力、丰富的外设接口和低成本优势,为开发此类设备提供了可靠平台,能够有效驱动实时数据采集和显示系统。
本项目聚焦于设计一款基于STM32的便携式数字示波器,旨在整合信号调理、高速模数转换和用户交互模块,实现对0-200kHz带宽信号的实时采集与分析。通过提供自动触发、电压频率测量、波形缩放平移以及FFT频域分析等功能,该设备力求以紧凑形态满足专业测量需求,为工程师、爱好者和教育机构提供一种灵活实用的工具,以填补市场对高性能便携示波器的空白。
设计实现的功能
(1)实现对0-200kHz带宽内输入信号的实时采集与波形显示。
(2)提供自动、常规、单次三种触发模式,并支持触发电平与边沿设置。
(3)具备电压幅值(Vpp,Vrms)与信号频率/周期的手动与自动测量功能。
(4)支持波形显示画面的缩放与平移操作。
(5)具备FFT频域分析功能,可将时域波形转换为频谱显示。
项目硬件模块组成
(1)主控与显示模块:采用STM32F103ZET6单片机驱动3.5寸TFT-LCD电阻触摸屏。
(2)信号调理模块:由OPA2350运放构成的衰减/放大电路,将输入信号调理至0-3.3V的ADC量程内。
(3)模数转换模块:直接使用STM32内置的12位ADC,配合DMA实现最高1Msps的采样率。
(4)用户输入模块:采用旋转编码器与独立按键组合,进行参数调节与功能切换。
(5)电源模块:采用MP1584EN DC-DC降压芯片将外部5V输入转换为系统所需的3.3V。
设计意义
该便携式数字示波器设计基于STM32平台,实现了对0-200kHz带宽内信号的实时采集与显示,结合触发、测量和频域分析功能,具有显著的实际应用价值。其小型化硬件构成,包括STM32F103ZET6主控、3.5寸TFT-LCD触摸屏和紧凑的电源模块,使得设备便于携带和现场使用,适合电子工程师、学生或爱好者在移动环境中进行信号调试和测试,克服了传统示波器体积庞大、不便移动的局限性。
该设计体现了嵌入式系统在仪器仪表领域的有效应用,通过利用STM32内置ADC和DMA技术实现最高1Msps采样率,展示了低成本硬件实现高速数据采集的可行性。这为嵌入式学习和项目开发提供了实践案例,有助于深入理解数字信号处理、实时系统设计和用户界面编程,推动技术教育和创新。
设计采用通用组件如OPA2350运放进行信号调理和旋转编码器进行交互,在保证功能完整性的同时控制了成本。这使得设备适用于预算有限的场景,如实验室教学、初创企业或个人项目,同时通过自动测量和FFT分析功能,扩展了其应用范围至频域分析,增强了实用性和灵活性。
整体而言,该设计通过集成实时波形显示、触发模式和测量功能,满足了基本示波器的需求,并为便携式电子测试工具的发展提供了参考。它强调实际应用,以STM32为核心实现了功能与便携性的平衡,有助于促进小型化、智能化测试仪器的普及和进步。
设计思路
该便携式数字示波器的设计核心在于利用STM32F103ZET6作为主控,通过高效的硬件模块协同工作,实现对输入信号的实时采集、处理与显示。系统首先通过信号调理模块将外部输入信号进行衰减或放大,确保其幅度适配STM32内置ADC的0-3.3V量程,从而安全且精确地处理0-200kHz带宽内的信号。这一调理过程由OPA2350运放构成的电路完成,它能有效保持信号完整性,为后续模数转换奠定基础。
模数转换模块直接使用STM32的12位ADC,配合DMA技术实现最高1Msps的采样率,确保对200kHz信号的充分采样,满足奈奎斯特定理要求。ADC采集的数据通过DMA直接传输到内存,减少了CPU负担,实现了高效的实时数据流处理。这为波形显示和后续分析提供了原始数据来源,支持自动、常规和单次三种触发模式,触发电平与边沿设置通过软件算法实现,基于采集数据与用户设定阈值的比较来稳定波形显示。
主控与显示模块由STM32驱动3.5寸TFT-LCD电阻触摸屏,负责波形渲染和用户界面管理。通过嵌入式图形库,系统能够实时绘制采集到的波形,并支持触摸和物理输入进行缩放与平移操作,方便用户观察信号细节。用户输入模块结合旋转编码器和独立按键,允许灵活调节参数如时间基、电压档位和触发设置,增强了仪器的交互性和便携性。
测量功能通过软件算法实现,包括电压幅值(如Vpp和Vrms)与信号频率/周期的计算,支持手动与自动模式,基于采集数据进行数字处理,确保测量精度。同时,系统集成FFT频域分析功能,利用STM32的运算能力将时域波形转换为频谱显示,帮助用户分析信号的频率成分,扩展了示波器的应用范围。
电源模块采用MP1584EN DC-DC降压芯片,将外部5V输入稳定转换为系统所需的3.3V,为整个硬件提供可靠供电,确保了便携式设计的稳定运行。整个系统通过软件固件整合各模块,优化资源分配,以实现功能需求的同时,保持低功耗和紧凑结构,适合现场使用。
框架图
系统框架图:
+-----------------------------+
| 输入信号 |
| (0-200kHz) |
+-----------------------------+
|
v
+-----------------------------+
| 信号调理模块 |
| (OPA2350运放电路) |
+-----------------------------+
|
v
+-----------------------------+
| ADC模块 |
| (内置12位ADC, DMA, 1Msps) |
+-----------------------------+
|
v
+-----------------------------+ +-----------------------------+
| 用户输入模块 | | 主控单元 |
| (旋转编码器, 独立按键) |---->| (STM32F103ZET6单片机) |
+-----------------------------+ +-----------------------------+
|
v
+-----------------------------+
| 显示单元 |
| (3.5寸TFT-LCD触摸屏) |
+-----------------------------+
^
|
+-----------------------------+
| 电源模块 |
| (MP1584EN DC-DC, 5V转3.3V)|
+-----------------------------+
系统总体设计
该系统总体设计基于STM32F103ZET6单片机为核心,实现便携式数字示波器的功能。系统从外部信号输入开始,通过信号调理模块将输入信号进行衰减或放大,确保其电压范围适配到0-3.3V以内,以匹配STM32内置ADC的量程要求。这一调理过程由OPA2350运放构成的电路完成,能够处理0-200kHz带宽内的信号,为后续采集提供稳定基础。
模数转换模块利用STM32的内置12位ADC,配合DMA技术实现高速数据采集,最高采样率可达1Msps,以满足实时采集与波形显示的需求。采集到的数据直接存储到内存中,由主控单元进行实时处理,确保波形能够及时更新并显示在3.5寸TFT-LCD电阻触摸屏上。LCD屏幕不仅用于波形可视化,还通过触摸功能辅助用户交互,但主要控制依赖于硬件输入模块。
用户输入模块采用旋转编码器与独立按键的组合,允许用户灵活调节参数,如触发电平、边沿设置以及显示缩放与平移。这些输入直接与主控交互,实现触发模式的切换,包括自动、常规和单次模式,确保波形捕获的精确性。同时,系统支持电压幅值测量,如Vpp和Vrms,以及信号频率与周期的手动与自动计算,测量结果实时显示在屏幕上,增强实用性。
系统还集成了FFT频域分析功能,主控单元对采集的时域波形数据进行快速傅里叶变换,将结果转换为频谱显示在LCD上,提供频域视角以辅助信号分析。整个系统由电源模块供电,MP1584EN DC-DC降压芯片将外部5V输入转换为稳定的3.3V电源,为各个硬件模块提供所需电压,确保系统在便携环境下可靠运行。
系统功能总结
| 系统功能 | 描述 |
|---|---|
| 信号采集与调理 | 实现对0-200kHz带宽内输入信号的实时采集,通过OPA2350运放构成的衰减/放大电路将信号调理至0-3.3V ADC量程,使用STM32内置12位ADC配合DMA实现最高1Msps采样率。 |
| 波形显示与操作 | 在3.5寸TFT-LCD电阻触摸屏上显示波形,支持缩放与平移操作。 |
| 触发功能 | 提供自动、常规、单次三种触发模式,并支持触发电平与边沿设置。 |
| 测量功能 | 具备电压幅值(Vpp, Vrms)与信号频率/周期的手动与自动测量功能。 |
| 频域分析功能 | 具备FFT频域分析功能,可将时域波形转换为频谱显示。 |
| 用户输入 | 采用旋转编码器与独立按键组合,进行参数调节与功能切换。 |
| 电源供应 | 采用MP1584EN DC-DC降压芯片将外部5V输入转换为系统所需的3.3V。 |
设计的各个功能模块描述
主控与显示模块采用STM32F103ZET6单片机作为核心控制器,驱动3.5寸TFT-LCD电阻触摸屏实现波形实时显示、界面交互以及参数设置,支持波形缩放、平移操作,并整合触发模式、测量功能和FFT频谱显示等控制逻辑。
信号调理模块由OPA2350运放构成的衰减与放大电路组成,负责将输入信号在0-200kHz带宽内进行调理,确保信号电压范围适配到0-3.3V以内,以供后续模数转换模块进行准确采集。
模数转换模块直接利用STM32内置的12位ADC,配合DMA技术实现最高1Msps的采样率,实现对输入信号的高速实时采集,满足波形显示和FFT频域分析的数据需求。
用户输入模块采用旋转编码器与独立按键的组合,允许用户进行触发电平、边沿设置、电压幅值与频率测量等参数调节,以及功能切换和操作控制。
电源模块采用MP1584EN DC-DC降压芯片,将外部输入的5V电压转换为系统所需的稳定3.3V电源,为各个硬件模块提供可靠的电能供应。
上位机代码设计
由于篇幅限制,我将提供一个精简但完整的基于Qt C++的示波器上位机软件框架代码。这个代码包含串口通信、波形显示、FFT分析、数据测量等核心功能。
// main.cpp
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
// mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QtCharts>
#include <QSerialPort>
#include <QSerialPortInfo>
#include <QTimer>
QT_CHARTS_USE_NAMESPACE
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void onConnectClicked();
void onDisconnectClicked();
void onStartStopClicked();
void onSerialReadyRead();
void updateWaveform();
void processData(const QByteArray &data);
void calculateFFT();
void updateMeasurements();
void onTriggerChanged();
void onTimebaseChanged();
void onVoltageScaleChanged();
void onSaveDataClicked();
void onLoadDataClicked();
void onAutoScaleClicked();
private:
Ui::MainWindow *ui;
QSerialPort *serialPort;
QTimer *dataTimer;
QChart *timeDomainChart;
QChart *freqDomainChart;
QLineSeries *waveSeries;
QLineSeries *fftSeries;
QValueAxis *timeAxis;
QValueAxis *voltageAxis;
QValueAxis *freqAxis;
QValueAxis *magnitudeAxis;
// 数据缓冲区
QVector<double> timeData;
QVector<double> voltageData;
QVector<double> fftData;
QVector<double> freqBins;
// 测量参数
double vpp, vrms, frequency, period;
double sampleRate;
double timebase;
double voltageScale;
int triggerMode;
double triggerLevel;
bool triggerEdge;
// 通信参数
QByteArray buffer;
bool isStreaming;
void setupUI();
void setupCharts();
void setupSerialPort();
void initializeVariables();
void parseDataPacket(const QByteArray &packet);
void applyTrigger();
void performFFT();
void calculateMeasurements();
};
#endif // MAINWINDOW_H
// mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QMessageBox>
#include <QFileDialog>
#include <QFile>
#include <QTextStream>
#include <QDebug>
#include <complex>
#include <vector>
#include <algorithm>
#include <cmath>
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow),
serialPort(new QSerialPort(this)),
dataTimer(new QTimer(this)),
timeDomainChart(new QChart()),
freqDomainChart(new QChart()),
waveSeries(new QLineSeries()),
fftSeries(new QLineSeries())
{
ui->setupUi(this);
initializeVariables();
setupUI();
setupCharts();
setupSerialPort();
// 连接信号槽
connect(dataTimer, &QTimer::timeout, this, &MainWindow::updateWaveform);
connect(serialPort, &QSerialPort::readyRead, this, &MainWindow::onSerialReadyRead);
// 界面控件信号连接
connect(ui->connectButton, &QPushButton::clicked, this, &MainWindow::onConnectClicked);
connect(ui->disconnectButton, &QPushButton::clicked, this, &MainWindow::onDisconnectClicked);
connect(ui->startStopButton, &QPushButton::clicked, this, &MainWindow::onStartStopClicked);
connect(ui->timebaseCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &MainWindow::onTimebaseChanged);
connect(ui->voltageScaleCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &MainWindow::onVoltageScaleChanged);
connect(ui->triggerCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &MainWindow::onTriggerChanged);
connect(ui->saveButton, &QPushButton::clicked, this, &MainWindow::onSaveDataClicked);
connect(ui->loadButton, &QPushButton::clicked, this, &MainWindow::onLoadDataClicked);
connect(ui->autoScaleButton, &QPushButton::clicked, this, &MainWindow::onAutoScaleClicked);
}
MainWindow::~MainWindow()
{
if(serialPort->isOpen())
serialPort->close();
delete ui;
}
void MainWindow::initializeVariables()
{
vpp = vrms = frequency = period = 0;
sampleRate = 1000000; // 1Msps
timebase = 0.001; // 1ms/div
voltageScale = 1.0; // 1V/div
triggerMode = 0; // 自动触发
triggerLevel = 1.65; // 中间电平
triggerEdge = true; // 上升沿
isStreaming = false;
// 初始化数据缓冲区
timeData.resize(1024);
voltageData.resize(1024);
fftData.resize(512);
freqBins.resize(512);
// 初始化时间轴
for(int i = 0; i < 1024; i++) {
timeData[i] = i * (1.0/sampleRate);
}
}
void MainWindow::setupUI()
{
// 设置串口列表
foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts()) {
ui->portCombo->addItem(info.portName());
}
// 设置波特率
ui->baudCombo->addItems({"9600", "19200", "38400", "57600", "115200", "230400", "460800", "921600"});
ui->baudCombo->setCurrentText("115200");
// 设置时基选项
QStringList timebases;
timebases << "1us/div" << "2us/div" << "5us/div" << "10us/div" << "20us/div"
<< "50us/div" << "100us/div" << "200us/div" << "500us/div" << "1ms/div"
<< "2ms/div" << "5ms/div" << "10ms/div" << "20ms/div" << "50ms/div"
<< "100ms/div" << "200ms/div" << "500ms/div" << "1s/div";
ui->timebaseCombo->addItems(timebases);
ui->timebaseCombo->setCurrentIndex(9); // 1ms/div
// 设置电压档位
QStringList voltageScales;
voltageScales << "10mV/div" << "20mV/div" << "50mV/div" << "100mV/div" << "200mV/div"
<< "500mV/div" << "1V/div" << "2V/div" << "5V/div";
ui->voltageScaleCombo->addItems(voltageScales);
ui->voltageScaleCombo->setCurrentIndex(6); // 1V/div
// 设置触发模式
ui->triggerCombo->addItems({"自动", "常规", "单次"});
// 初始化显示
ui->vppLabel->setText("Vpp: --");
ui->vrmsLabel->setText("Vrms: --");
ui->freqLabel->setText("频率: --");
ui->periodLabel->setText("周期: --");
}
void MainWindow::setupCharts()
{
// 时域图设置
waveSeries->setName("波形");
waveSeries->setColor(Qt::blue);
waveSeries->setUseOpenGL(true);
timeDomainChart->addSeries(waveSeries);
timeDomainChart->setTitle("时域波形");
timeDomainChart->setAnimationOptions(QChart::NoAnimation);
timeAxis = new QValueAxis();
timeAxis->setTitleText("时间 (s)");
timeAxis->setRange(0, timebase * 10); // 10格
timeAxis->setLabelFormat("%.6f");
voltageAxis = new QValueAxis();
voltageAxis->setTitleText("电压 (V)");
voltageAxis->setRange(-5, 5); // ±5V范围
voltageAxis->setLabelFormat("%.3f");
timeDomainChart->addAxis(timeAxis, Qt::AlignBottom);
timeDomainChart->addAxis(voltageAxis, Qt::AlignLeft);
waveSeries->attachAxis(timeAxis);
waveSeries->attachAxis(voltageAxis);
// 频域图设置
fftSeries->setName("频谱");
fftSeries->setColor(Qt::red);
freqDomainChart->addSeries(fftSeries);
freqDomainChart->setTitle("频域分析");
freqDomainChart->setAnimationOptions(QChart::NoAnimation);
freqAxis = new QValueAxis();
freqAxis->setTitleText("频率 (Hz)");
freqAxis->setRange(0, 50000); // 0-50kHz
freqAxis->setLabelFormat("%.0f");
magnitudeAxis = new QValueAxis();
magnitudeAxis->setTitleText("幅度 (dB)");
magnitudeAxis->setRange(-100, 0);
freqDomainChart->addAxis(freqAxis, Qt::AlignBottom);
freqDomainChart->addAxis(magnitudeAxis, Qt::AlignLeft);
fftSeries->attachAxis(freqAxis);
fftSeries->attachAxis(magnitudeAxis);
// 设置到GraphicsView
ui->timeChartView->setChart(timeDomainChart);
ui->timeChartView->setRenderHint(QPainter::Antialiasing);
ui->freqChartView->setChart(freqDomainChart);
ui->freqChartView->setRenderHint(QPainter::Antialiasing);
}
void MainWindow::setupSerialPort()
{
serialPort->setDataBits(QSerialPort::Data8);
serialPort->setParity(QSerialPort::NoParity);
serialPort->setStopBits(QSerialPort::OneStop);
serialPort->setFlowControl(QSerialPort::NoFlowControl);
}
void MainWindow::onConnectClicked()
{
if(ui->portCombo->currentText().isEmpty()) {
QMessageBox::warning(this, "错误", "没有可用的串口!");
return;
}
serialPort->setPortName(ui->portCombo->currentText());
serialPort->setBaudRate(ui->baudCombo->currentText().toInt());
if(serialPort->open(QIODevice::ReadWrite)) {
ui->statusLabel->setText("已连接: " + ui->portCombo->currentText());
ui->connectButton->setEnabled(false);
ui->disconnectButton->setEnabled(true);
} else {
QMessageBox::critical(this, "错误", "无法打开串口!");
}
}
void MainWindow::onDisconnectClicked()
{
if(serialPort->isOpen()) {
serialPort->close();
ui->statusLabel->setText("已断开连接");
ui->connectButton->setEnabled(true);
ui->disconnectButton->setEnabled(false);
if(isStreaming) {
onStartStopClicked();
}
}
}
void MainWindow::onStartStopClicked()
{
if(!serialPort->isOpen()) {
QMessageBox::warning(this, "警告", "请先连接串口!");
return;
}
if(!isStreaming) {
// 开始采集
QByteArray cmd = "START\r\n";
serialPort->write(cmd);
dataTimer->start(50); // 20Hz更新
ui->startStopButton->setText("停止");
ui->statusLabel->setText("正在采集数据...");
isStreaming = true;
} else {
// 停止采集
QByteArray cmd = "STOP\r\n";
serialPort->write(cmd);
dataTimer->stop();
ui->startStopButton->setText("开始");
ui->statusLabel->setText("已停止采集");
isStreaming = false;
}
}
void MainWindow::onSerialReadyRead()
{
buffer.append(serialPort->readAll());
// 查找完整的数据包(以换行符结束)
while(buffer.contains('\n')) {
int endIndex = buffer.indexOf('\n');
QByteArray packet = buffer.left(endIndex);
buffer = buffer.mid(endIndex + 1);
if(packet.startsWith("DATA:")) {
parseDataPacket(packet.mid(5)); // 去掉"DATA:"
}
}
}
void MainWindow::parseDataPacket(const QByteArray &packet)
{
QList<QByteArray> values = packet.split(',');
if(values.size() >= 1024) {
voltageData.clear();
for(int i = 0; i < 1024; i++) {
// 转换为电压值(假设ADC参考电压3.3V,12位)
double voltage = values[i].toDouble() * (3.3 / 4096.0);
voltageData.append(voltage);
}
applyTrigger();
updateMeasurements();
calculateFFT();
}
}
void MainWindow::applyTrigger()
{
// 简单的触发处理
if(triggerMode == 2) { // 单次触发
static bool triggered = false;
if(!triggered) {
for(int i = 1; i < voltageData.size(); i++) {
if(triggerEdge) { // 上升沿
if(voltageData[i-1] < triggerLevel && voltageData[i] >= triggerLevel) {
triggered = true;
break;
}
} else { // 下降沿
if(voltageData[i-1] > triggerLevel && voltageData[i] <= triggerLevel) {
triggered = true;
break;
}
}
}
}
if(!triggered) {
voltageData.fill(0);
}
}
}
void MainWindow::updateWaveform()
{
waveSeries->clear();
// 更新波形显示
double maxVoltage = -9999, minVoltage = 9999;
for(int i = 0; i < voltageData.size(); i++) {
waveSeries->append(timeData[i], voltageData[i]);
if(voltageData[i] > maxVoltage) maxVoltage = voltageData[i];
if(voltageData[i] < minVoltage) minVoltage = voltageData[i];
}
// 自动调整Y轴范围
if(ui->autoScaleCheck->isChecked()) {
double range = maxVoltage - minVoltage;
double center = (maxVoltage + minVoltage) / 2;
voltageAxis->setRange(center - range * 0.7, center + range * 0.7);
}
// 更新FFT显示
fftSeries->clear();
for(int i = 0; i < fftData.size(); i++) {
fftSeries->append(freqBins[i], fftData[i]);
}
}
void MainWindow::calculateFFT()
{
// 简单FFT实现(实际应使用FFTW或KissFFT等库)
int N = voltageData.size();
std::vector<std::complex<double>> x(N);
// 转换为复数并应用窗函数
for(int i = 0; i < N; i++) {
// 汉宁窗
double window = 0.5 * (1 - cos(2 * M_PI * i / (N - 1)));
x[i] = std::complex<double>(voltageData[i] * window, 0);
}
// 简单DFT实现(性能较低,实际应用中应使用FFT算法)
std::vector<std::complex<double>> X(N/2);
fftData.clear();
freqBins.clear();
for(int k = 0; k < N/2; k++) {
std::complex<double> sum(0, 0);
for(int n = 0; n < N; n++) {
double angle = -2 * M_PI * k * n / N;
sum += x[n] * std::complex<double>(cos(angle), sin(angle));
}
X[k] = sum;
// 转换为dB
double magnitude = 20 * log10(abs(sum) / (N/2) + 1e-10);
fftData.append(magnitude);
freqBins.append(k * sampleRate / N);
}
}
void MainWindow::updateMeasurements()
{
// 计算Vpp
double max = *std::max_element(voltageData.begin(), voltageData.end());
double min = *std::min_element(voltageData.begin(), voltageData.end());
vpp = max - min;
// 计算Vrms
double sumSquares = 0;
for(double v : voltageData) {
sumSquares += v * v;
}
vrms = sqrt(sumSquares / voltageData.size());
// 简单频率测量(过零检测)
int zeroCrossings = 0;
for(int i = 1; i < voltageData.size(); i++) {
if((voltageData[i-1] < 0 && voltageData[i] >= 0) ||
(voltageData[i-1] > 0 && voltageData[i] <= 0)) {
zeroCrossings++;
}
}
frequency = zeroCrossings * sampleRate / (2.0 * voltageData.size());
period = 1.0 / frequency;
// 更新显示
ui->vppLabel->setText(QString("Vpp: %1 V").arg(vpp, 0, 'f', 3));
ui->vrmsLabel->setText(QString("Vrms: %1 V").arg(vrms, 0, 'f', 3));
ui->freqLabel->setText(QString("频率: %1 Hz").arg(frequency, 0, 'f', 1));
ui->periodLabel->setText(QString("周期: %1 ms").arg(period * 1000, 0, 'f', 3));
}
void MainWindow::onTimebaseChanged()
{
QString timebaseStr = ui->timebaseCombo->currentText();
timebaseStr.remove("s/div");
if(timebaseStr.contains('u')) {
timebaseStr.remove('u');
timebase = timebaseStr.toDouble() * 1e-6;
} else if(timebaseStr.contains('m')) {
timebaseStr.remove('m');
timebase = timebaseStr.toDouble() * 1e-3;
} else {
timebase = timebaseStr.toDouble();
}
// 计算采样点数
int points = 10 * timebase * sampleRate; // 10格
points = qMin(points, 10000); // 限制最大点数
points = qMax(points, 100); // 限制最小点数
// 更新X轴范围
timeAxis->setRange(0, timebase * 10);
// 发送设置到下位机
if(serialPort->isOpen() && isStreaming) {
QByteArray cmd = QString("TIMEBASE:%1\r\n").arg(points).toLatin1();
serialPort->write(cmd);
}
}
void MainWindow::onVoltageScaleChanged()
{
QString scaleStr = ui->voltageScaleCombo->currentText();
scaleStr.remove("V/div");
if(scaleStr.contains('m')) {
scaleStr.remove('m');
voltageScale = scaleStr.toDouble() * 1e-3;
} else {
voltageScale = scaleStr.toDouble();
}
// 发送设置到下位机
if(serialPort->isOpen() && isStreaming) {
QByteArray cmd = QString("VSCALE:%1\r\n").arg(voltageScale).toLatin1();
serialPort->write(cmd);
}
}
void MainWindow::onTriggerChanged()
{
triggerMode = ui->triggerCombo->currentIndex();
// 发送触发设置
if(serialPort->isOpen() && isStreaming) {
QByteArray cmd = QString("TRIGGER:%1,%2,%3\r\n")
.arg(triggerMode)
.arg(triggerLevel)
.arg(triggerEdge ? "1" : "0").toLatin1();
serialPort->write(cmd);
}
}
void MainWindow::onSaveDataClicked()
{
QString fileName = QFileDialog::getSaveFileName(this, "保存数据", "", "CSV文件 (*.csv);;文本文件 (*.txt)");
if(fileName.isEmpty()) return;
QFile file(fileName);
if(file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream stream(&file);
stream << "时间(s),电压(V)\n";
for(int i = 0; i < voltageData.size(); i++) {
stream << QString("%1,%2\n").arg(timeData[i], 0, 'f', 9).arg(voltageData[i], 0, 'f', 6);
}
file.close();
QMessageBox::information(this, "成功", "数据保存成功!");
}
}
void MainWindow::onLoadDataClicked()
{
QString fileName = QFileDialog::getOpenFileName(this, "加载数据", "", "CSV文件 (*.csv);;文本文件 (*.txt)");
if(fileName.isEmpty()) return;
QFile file(fileName);
if(file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QTextStream stream(&file);
stream.readLine(); // 跳过标题行
voltageData.clear();
timeData.clear();
int index = 0;
while(!stream.atEnd()) {
QString line = stream.readLine();
QStringList values = line.split(',');
if(values.size() >= 2) {
timeData.append(values[0].toDouble());
voltageData.append(values[1].toDouble());
index++;
}
}
file.close();
// 更新显示
updateMeasurements();
calculateFFT();
updateWaveform();
QMessageBox::information(this, "成功", "数据加载成功!");
}
}
void MainWindow::onAutoScaleClicked()
{
if(voltageData.isEmpty()) return;
double max = *std::max_element(voltageData.begin(), voltageData.end());
double min = *std::min_element(voltageData.begin(), voltageData.end());
// 添加10%的边距
double margin = (max - min) * 0.1;
voltageAxis->setRange(min - margin, max + margin);
// 自动调整时间轴
if(!timeData.isEmpty()) {
timeAxis->setRange(timeData.first(), timeData.last());
}
}
// osciloscope.pro (Qt项目文件)
QT += core gui serialport charts
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
TARGET = Oscilloscope
TEMPLATE = app
DEFINES += QT_DEPRECATED_WARNINGS
SOURCES += \
main.cpp \
mainwindow.cpp
HEADERS += \
mainwindow.h
FORMS += \
mainwindow.ui
这个示波器上位机软件包含以下主要功能:
- 串口通信:连接STM32下位机,接收波形数据
- 波形显示:实时显示时域波形
- 频域分析:FFT频谱显示
- 测量功能:Vpp、Vrms、频率、周期测量
- 参数设置:时基、电压档位、触发设置
- 数据存储:保存和加载波形数据
- 自动调节:自动调整显示范围
注意:实际应用中需要根据STM32下位机的具体通信协议修改数据解析部分,并可能需要使用更高效的FFT库(如FFTW)进行频域分析。
模块代码设计
STM32模块代码设计(寄存器方式)
1. 系统时钟配置
// system_clock.c
void SystemClock_Config(void) {
// 使能外部8MHz晶振
RCC->CR |= RCC_CR_HSEON;
while(!(RCC->CR & RCC_CR_HSERDY));
// FLASH预取指缓存和等待周期
FLASH->ACR = FLASH_ACR_PRFTBE | FLASH_ACR_LATENCY_2;
// AHB、APB1、APB2预分频
RCC->CFGR |= RCC_CFGR_HPRE_DIV1; // HCLK = SYSCLK = 72MHz
RCC->CFGR |= RCC_CFGR_PPRE1_DIV2; // PCLK1 = HCLK/2 = 36MHz
RCC->CFGR |= RCC_CFGR_PPRE2_DIV1; // PCLK2 = HCLK = 72MHz
// PLL配置:HSE * 9 = 72MHz
RCC->CFGR &= ~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE | RCC_CFGR_PLLMULL);
RCC->CFGR |= RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9;
// 使能PLL
RCC->CR |= RCC_CR_PLLON;
while(!(RCC->CR & RCC_CR_PLLRDY));
// 选择PLL作为系统时钟源
RCC->CFGR &= ~RCC_CFGR_SW;
RCC->CFGR |= RCC_CFGR_SW_PLL;
while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
}
2. ADC模块配置(PA0通道)
// adc.c
#define ADC1_DR_Address ((uint32_t)0x4001244C)
#define BUFFER_SIZE 1024
volatile uint16_t adc_buffer[BUFFER_SIZE];
volatile uint32_t adc_index = 0;
void ADC1_Init(void) {
// 使能ADC1时钟
RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
// 配置PA0为模拟输入
GPIOA->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0);
GPIOA->CRL |= GPIO_CRL_CNF0_0; // 模拟输入模式
// ADC复位
RCC->APB2RSTR |= RCC_APB2RSTR_ADC1RST;
RCC->APB2RSTR &= ~RCC_APB2RSTR_ADC1RST;
// ADC校准
ADC1->CR2 |= ADC_CR2_ADON; // 开启ADC
delay_us(1);
ADC1->CR2 |= ADC_CR2_CAL; // 开始校准
while(ADC1->CR2 & ADC_CR2_CAL); // 等待校准完成
// 配置ADC
ADC1->CR1 = 0;
ADC1->CR2 = 0;
// 独立模式,数据右对齐
ADC1->CR2 |= ADC_CR2_CONT; // 连续转换模式
ADC1->CR2 &= ~ADC_CR2_ALIGN; // 右对齐
// 采样时间:239.5周期(对应1Msps)
ADC1->SMPR2 |= ADC_SMPR2_SMP0_0 | ADC_SMPR2_SMP0_1 | ADC_SMPR2_SMP0_2;
// 规则序列:通道0,序列1
ADC1->SQR1 = 0;
ADC1->SQR3 = ADC_SQR3_SQ1_0; // 通道0在序列1
// 使能扫描模式
ADC1->CR1 |= ADC_CR1_SCAN;
// 使能ADC并开始转换
ADC1->CR2 |= ADC_CR2_ADON;
delay_us(1);
ADC1->CR2 |= ADC_CR2_ADON; // 第二次开启开始转换
}
void ADC1_DMA_Init(void) {
// 使能DMA1时钟
RCC->AHBENR |= RCC_AHBENR_DMA1EN;
// 配置DMA1通道1(ADC1)
DMA1_Channel1->CCR = 0;
DMA1_Channel1->CCR |= DMA_CCR1_CIRC; // 循环模式
DMA1_Channel1->CCR |= DMA_CCR1_MINC; // 存储器地址递增
DMA1_Channel1->CCR &= ~DMA_CCR1_PINC; // 外设地址不递增
DMA1_Channel1->CCR &= ~DMA_CCR1_PSIZE; // 外设数据宽度:16位
DMA1_Channel1->CCR &= ~DMA_CCR1_MSIZE; // 存储器数据宽度:16位
DMA1_Channel1->CCR &= ~DMA_CCR1_DIR; // 外设到存储器
// 设置数据数量
DMA1_Channel1->CNDTR = BUFFER_SIZE;
// 设置外设地址(ADC数据寄存器)
DMA1_Channel1->CPAR = ADC1_DR_Address;
// 设置存储器地址
DMA1_Channel1->CMAR = (uint32_t)adc_buffer;
// 使能DMA传输完成中断
DMA1_Channel1->CCR |= DMA_CCR1_TCIE;
// 配置NVIC
NVIC_EnableIRQ(DMA1_Channel1_IRQn);
NVIC_SetPriority(DMA1_Channel1_IRQn, 0);
// 使能DMA
DMA1_Channel1->CCR |= DMA_CCR1_EN;
// 使能ADC的DMA请求
ADC1->CR2 |= ADC_CR2_DMA;
}
// DMA1通道1中断服务函数
void DMA1_Channel1_IRQHandler(void) {
if(DMA1->ISR & DMA_ISR_TCIF1) {
// 传输完成
DMA1->IFCR |= DMA_IFCR_CTCIF1;
adc_index = BUFFER_SIZE;
}
}
// 读取ADC值
uint16_t ADC1_Read(void) {
ADC1->CR2 |= ADC_CR2_SWSTART; // 开始转换
while(!(ADC1->SR & ADC_SR_EOC)); // 等待转换完成
return ADC1->DR;
}
3. 旋转编码器配置(PA6, PA7)
// encoder.c
volatile int32_t encoder_count = 0;
volatile uint8_t encoder_switch = 0;
static uint8_t last_state = 0;
void Encoder_Init(void) {
// 使能GPIOA时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
// PA6, PA7配置为上拉输入
GPIOA->CRL &= ~(GPIO_CRL_CNF6 | GPIO_CRL_MODE6 |
GPIO_CRL_CNF7 | GPIO_CRL_MODE7);
GPIOA->CRL |= GPIO_CRL_CNF6_1 | GPIO_CRL_CNF7_1; // 上拉/下拉输入
GPIOA->ODR |= GPIO_ODR_ODR6 | GPIO_ODR_ODR7; // 上拉
// 初始化状态
last_state = (GPIOA->IDR >> 6) & 0x03;
}
void Encoder_Process(void) {
uint8_t current_state = (GPIOA->IDR >> 6) & 0x03;
// 状态变化检测
static const int8_t state_table[] = {0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0};
// 计算状态索引
uint8_t index = (last_state << 2) | current_state;
// 更新计数
encoder_count += state_table[index];
// 保存当前状态
last_state = current_state;
}
// 获取编码器计数值
int32_t Encoder_GetCount(void) {
int32_t count;
__disable_irq();
count = encoder_count;
encoder_count = 0; // 读取后清零
__enable_irq();
return count;
}
4. 独立按键配置(PA8, PA9, PA10)
// buttons.c
#define BUTTON_COUNT 3
volatile uint8_t button_state[BUTTON_COUNT] = {0};
volatile uint32_t button_press_time[BUTTON_COUNT] = {0};
void Buttons_Init(void) {
// 使能GPIOA时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
// PA8, PA9, PA10配置为上拉输入
GPIOA->CRH &= ~(GPIO_CRH_CNF8 | GPIO_CRH_MODE8 |
GPIO_CRH_CNF9 | GPIO_CRH_MODE9 |
GPIO_CRH_CNF10 | GPIO_CRH_MODE10);
GPIOA->CRH |= GPIO_CRH_CNF8_1 | GPIO_CRH_CNF9_1 | GPIO_CRH_CNF10_1; // 上拉/下拉输入
GPIOA->ODR |= GPIO_ODR_ODR8 | GPIO_ODR_ODR9 | GPIO_ODR_ODR10; // 上拉
}
void Buttons_Scan(void) {
static uint32_t last_time = 0;
uint32_t current_time = SysTick_GetTick();
// 10ms扫描一次
if(current_time - last_time < 10) return;
last_time = current_time;
// 读取按键状态(按下为0)
uint8_t raw_state = ~(GPIOA->IDR >> 8) & 0x07;
for(int i = 0; i < BUTTON_COUNT; i++) {
if(raw_state & (1 << i)) {
// 按键按下
if(button_state[i] == 0) {
button_state[i] = 1; // 按下标记
button_press_time[i] = current_time;
} else {
// 长按检测(超过1秒)
if((current_time - button_press_time[i]) > 1000) {
button_state[i] = 2; // 长按标记
}
}
} else {
// 按键释放
if(button_state[i] == 1) {
// 短按
button_state[i] = 3; // 短按事件
} else if(button_state[i] == 2) {
// 长按释放
button_state[i] = 4; // 长按释放事件
} else {
button_state[i] = 0; // 无按键
}
}
}
}
// 获取按键事件
uint8_t Button_GetEvent(uint8_t button_num) {
if(button_num >= BUTTON_COUNT) return 0;
uint8_t event = button_state[button_num];
// 清除事件标记(除按下状态外)
if(event == 3 || event == 4) {
button_state[button_num] = 0;
}
return event;
}
5. 定时器配置(用于采样率控制)
// timer.c
void TIM2_Init(uint32_t frequency) {
// 使能TIM2时钟
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
// 定时器复位
RCC->APB1RSTR |= RCC_APB1RSTR_TIM2RST;
RCC->APB1RSTR &= ~RCC_APB1RSTR_TIM2RST;
// 计算ARR和PSC值
uint32_t arr_value = (72000000 / frequency) - 1;
// 配置预分频器
TIM2->PSC = 0; // 不分频
// 配置自动重装载值
TIM2->ARR = arr_value;
// 配置更新中断
TIM2->DIER |= TIM_DIER_UIE;
// 配置NVIC
NVIC_EnableIRQ(TIM2_IRQn);
NVIC_SetPriority(TIM2_IRQn, 1);
// 使能定时器
TIM2->CR1 |= TIM_CR1_CEN;
}
void TIM2_SetFrequency(uint32_t frequency) {
// 禁用定时器
TIM2->CR1 &= ~TIM_CR1_CEN;
// 计算新的ARR值
uint32_t arr_value = (72000000 / frequency) - 1;
// 更新ARR
TIM2->ARR = arr_value;
// 重新使能定时器
TIM2->CR1 |= TIM_CR1_CEN;
}
// TIM2中断服务函数
void TIM2_IRQHandler(void) {
if(TIM2->SR & TIM_SR_UIF) {
TIM2->SR &= ~TIM_SR_UIF;
// 触发ADC采样
ADC1->CR2 |= ADC_CR2_SWSTART;
}
}
6. 系统滴答定时器
// systick.c
volatile uint32_t systick_counter = 0;
void SysTick_Init(void) {
// 配置SysTick为1ms中断
SysTick->LOAD = 72000 - 1; // 72MHz/1000 = 72000
SysTick->VAL = 0;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk;
// 配置NVIC
NVIC_SetPriority(SysTick_IRQn, 0);
}
uint32_t SysTick_GetTick(void) {
return systick_counter;
}
void delay_ms(uint32_t ms) {
uint32_t start = systick_counter;
while((systick_counter - start) < ms);
}
void SysTick_Handler(void) {
systick_counter++;
}
7. 主程序框架
// main.c
#include "stm32f10x.h"
// 函数声明
void SystemClock_Config(void);
void GPIO_Init(void);
void ADC1_Init(void);
void ADC1_DMA_Init(void);
void Encoder_Init(void);
void Buttons_Init(void);
void TIM2_Init(uint32_t freq);
void SysTick_Init(void);
// 波形处理缓冲区
#define WAVE_BUFFER_SIZE 1024
uint16_t wave_buffer[WAVE_BUFFER_SIZE];
volatile uint8_t data_ready = 0;
int main(void) {
// 系统初始化
SystemClock_Config();
SysTick_Init();
GPIO_Init();
// 外设初始化
ADC1_Init();
ADC1_DMA_Init();
Encoder_Init();
Buttons_Init();
TIM2_Init(100000); // 初始100kHz采样率
// LCD初始化(简化)
LCD_Init();
LCD_Clear(0x0000);
// 主循环
while(1) {
// 处理按键
Buttons_Scan();
// 处理编码器
Encoder_Process();
int32_t encoder_change = Encoder_GetCount();
if(encoder_change != 0) {
// 调整参数(如触发电平、时基等)
AdjustParameter(encoder_change);
}
// 检查按键事件
for(int i = 0; i < 3; i++) {
uint8_t event = Button_GetEvent(i);
if(event == 3) { // 短按
HandleButtonPress(i);
} else if(event == 4) { // 长按
HandleButtonLongPress(i);
}
}
// 处理ADC数据
if(data_ready) {
ProcessWaveData();
DisplayWaveform();
data_ready = 0;
}
// 执行FFT分析(按需)
if(fft_request) {
PerformFFT();
DisplaySpectrum();
fft_request = 0;
}
}
}
// 波形数据处理函数
void ProcessWaveData(void) {
// 复制DMA缓冲区数据
for(int i = 0; i < BUFFER_SIZE; i++) {
wave_buffer[i] = adc_buffer[i];
}
// 触发检测
if(trigger_mode != TRIGGER_NONE) {
ApplyTrigger(wave_buffer, BUFFER_SIZE);
}
// 电压测量
CalculateVoltage(wave_buffer, BUFFER_SIZE);
// 频率测量
CalculateFrequency(wave_buffer, BUFFER_SIZE);
}
// LCD显示函数(简化框架)
void DisplayWaveform(void) {
// 清屏
LCD_ClearArea(0, 0, 320, 240, 0x0000);
// 绘制网格
DrawGrid();
// 绘制波形
for(int i = 0; i < WAVE_BUFFER_SIZE - 1; i++) {
int x1 = i * 320 / WAVE_BUFFER_SIZE;
int y1 = 240 - (wave_buffer[i] * 240 / 4096);
int x2 = (i + 1) * 320 / WAVE_BUFFER_SIZE;
int y2 = 240 - (wave_buffer[i + 1] * 240 / 4096);
LCD_DrawLine(x1, y1, x2, y2, 0x07E0); // 绿色波形
}
// 显示测量结果
DisplayMeasurements();
}
// GPIO初始化
void GPIO_Init(void) {
// 使能所有GPIO时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN |
RCC_APB2ENR_IOPCEN | RCC_APB2ENR_IOPDEN |
RCC_APB2ENR_IOPEEN;
}
8. 测量功能实现
// measurements.c
typedef struct {
float vpp; // 峰峰值
float vrms; // RMS值
float freq; // 频率
float period; // 周期
} Measurement_t;
Measurement_t measurements;
void CalculateVoltage(uint16_t *buffer, uint32_t size) {
uint16_t min = 4095, max = 0;
uint32_t sum = 0;
uint32_t sum_sq = 0;
for(uint32_t i = 0; i < size; i++) {
uint16_t value = buffer[i];
// 查找最大值和最小值
if(value < min) min = value;
if(value > max) max = value;
// 累加用于计算平均值和RMS
sum += value;
sum_sq += value * value;
}
// 计算Vpp(假设3.3V参考电压)
measurements.vpp = (max - min) * 3.3f / 4096.0f;
// 计算Vrms
float avg = (float)sum / size;
float avg_sq = (float)sum_sq / size;
float variance = avg_sq - (avg * avg);
measurements.vrms = sqrt(variance) * 3.3f / 4096.0f;
}
void CalculateFrequency(uint16_t *buffer, uint32_t size) {
// 寻找过零点
uint32_t zero_crossings[50];
uint32_t cross_count = 0;
for(uint32_t i = 1; i < size && cross_count < 50; i++) {
// 检测从负到正的过零点(以2048为零点)
if(buffer[i-1] < 2048 && buffer[i] >= 2048) {
zero_crossings[cross_count++] = i;
}
}
if(cross_count >= 2) {
// 计算平均周期(采样点)
uint32_t total_samples = 0;
for(uint32_t i = 1; i < cross_count; i++) {
total_samples += zero_crossings[i] - zero_crossings[i-1];
}
float avg_period_samples = (float)total_samples / (cross_count - 1);
// 转换为时间和频率
measurements.period = avg_period_samples / current_sample_rate;
measurements.freq = 1.0f / measurements.period;
} else {
measurements.period = 0;
measurements.freq = 0;
}
}
9. FFT实现(简化)
// fft.c
#include <math.h>
#define FFT_SIZE 256
#define PI 3.14159265358979323846f
typedef struct {
float real;
float imag;
} Complex_t;
Complex_t fft_input[FFT_SIZE];
float fft_output[FFT_SIZE/2];
void PerformFFT(void) {
// 准备输入数据(汉宁窗)
for(int i = 0; i < FFT_SIZE; i++) {
float sample = wave_buffer[i] * 3.3f / 4096.0f;
float window = 0.5f * (1 - cos(2 * PI * i / (FFT_SIZE - 1)));
fft_input[i].real = sample * window;
fft_input[i].imag = 0;
}
// 执行基2 FFT
FFT_Base2(fft_input, FFT_SIZE);
// 计算幅度谱
for(int i = 0; i < FFT_SIZE/2; i++) {
float real = fft_input[i].real;
float imag = fft_input[i].imag;
fft_output[i] = sqrt(real*real + imag*imag);
}
}
// 基2 FFT算法
void FFT_Base2(Complex_t *data, int n) {
int i, j, k, m;
Complex_t temp, w, wn;
float angle;
// 位反转置换
j = 0;
for(i = 0; i < n-1; i++) {
if(i < j) {
temp = data[i];
data[i] = data[j];
data[j] = temp;
}
k = n >> 1;
while(k <= j) {
j -= k;
k >>= 1;
}
j += k;
}
// 蝶形运算
for(m = 2; m <= n; m <<= 1) {
angle = -2 * PI / m;
wn.real = cos(angle);
wn.imag = sin(angle);
for(k = 0; k < n; k += m) {
w.real = 1;
w.imag = 0;
for(j = 0; j < m/2; j++) {
Complex_t t, u;
u.real = w.real * data[k+j+m/2].real - w.imag * data[k+j+m/2].imag;
u.imag = w.real * data[k+j+m/2].imag + w.imag * data[k+j+m/2].real;
t.real = data[k+j].real;
t.imag = data[k+j].imag;
data[k+j].real = t.real + u.real;
data[k+j].imag = t.imag + u.imag;
data[k+j+m/2].real = t.real - u.real;
data[k+j+m/2].imag = t.imag - u.imag;
// 更新旋转因子
temp.real = w.real * wn.real - w.imag * wn.imag;
temp.imag = w.real * wn.imag + w.imag * wn.real;
w = temp;
}
}
}
}
10. 头文件
// stm32f10x_reg.h
#ifndef __STM32F10X_REG_H
#define __STM32F10X_REG_H
// 寄存器基地址
#define PERIPH_BASE ((uint32_t)0x40000000)
#define APB1PERIPH_BASE (PERIPH_BASE + 0x00000)
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
#define AHBPERIPH_BASE (PERIPH_BASE + 0x20000)
// GPIO寄存器结构
typedef struct {
volatile uint32_t CRL;
volatile uint32_t CRH;
volatile uint32_t IDR;
volatile uint32_t ODR;
volatile uint32_t BSRR;
volatile uint32_t BRR;
volatile uint32_t LCKR;
} GPIO_TypeDef;
// RCC寄存器结构
typedef struct {
volatile uint32_t CR;
volatile uint32_t CFGR;
volatile uint32_t CIR;
volatile uint32_t APB2RSTR;
volatile uint32_t APB1RSTR;
volatile uint32_t AHBENR;
volatile uint32_t APB2ENR;
volatile uint32_t APB1ENR;
volatile uint32_t BDCR;
volatile uint32_t CSR;
} RCC_TypeDef;
// ADC寄存器结构
typedef struct {
volatile uint32_t SR;
volatile uint32_t CR1;
volatile uint32_t CR2;
volatile uint32_t SMPR1;
volatile uint32_t SMPR2;
volatile uint32_t JOFR1;
volatile uint32_t JOFR2;
volatile uint32_t JOFR3;
volatile uint32_t JOFR4;
volatile uint32_t HTR;
volatile uint32_t LTR;
volatile uint32_t SQR1;
volatile uint32_t SQR2;
volatile uint32_t SQR3;
volatile uint32_t JSQR;
volatile uint32_t JDR1;
volatile uint32_t JDR2;
volatile uint32_t JDR3;
volatile uint32_t JDR4;
volatile uint32_t DR;
} ADC_TypeDef;
// DMA通道寄存器结构
typedef struct {
volatile uint32_t CCR;
volatile uint32_t CNDTR;
volatile uint32_t CPAR;
volatile uint32_t CMAR;
} DMA_Channel_TypeDef;
// 外设基地址
#define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)
#define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)
#define GPIOC_BASE (APB2PERIPH_BASE + 0x1000)
#define GPIOD_BASE (APB2PERIPH_BASE + 0x1400)
#define GPIOE_BASE (APB2PERIPH_BASE + 0x1800)
#define RCC_BASE (AHBPERIPH_BASE + 0x1000)
#define ADC1_BASE (APB2PERIPH_BASE + 0x2400)
#define DMA1_BASE (AHBPERIPH_BASE + 0x0000)
// 外设声明
#define GPIOA ((GPIO_TypeDef *)GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *)GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef *)GPIOC_BASE)
#define GPIOD ((GPIO_TypeDef *)GPIOD_BASE)
#define GPIOE ((GPIO_TypeDef *)GPIOE_BASE)
#define RCC ((RCC_TypeDef *)RCC_BASE)
#define ADC1 ((ADC_TypeDef *)ADC1_BASE)
#define DMA1_Channel1 ((DMA_Channel_TypeDef *)(DMA1_BASE + 0x08))
#endif /* __STM32F10X_REG_H */
此代码提供了完整的STM32F103ZET6寄存器级开发框架,实现了数字示波器的核心功能。注意:LCD驱动部分需要根据具体使用的TFT-LCD控制器进行相应调整。
项目核心代码
/* 基于STM32的便携式数字示波器 - main.c */
#include "stm32f10x.h"
#include "lcd.h"
#include "adc_dma.h"
#include "signal_processing.h"
#include "ui.h"
#include "encoder.h"
#include "buttons.h"
#include "fft.h"
/* 全局变量定义 */
volatile uint8_t trigger_flag = 0;
volatile uint8_t data_ready = 0;
uint16_t adc_buffer[BUFFER_SIZE];
float voltage_buffer[BUFFER_SIZE];
WaveformInfo waveform = {
.trigger_mode = AUTO,
.trigger_edge = RISING,
.trigger_level = 1.65f,
.timebase = 10.0f, /* us/div */
.voltage_scale = 1.0f, /* V/div */
.offset_x = 0,
.offset_y = 0
};
/* 函数声明 */
static void System_Init(void);
static void Display_Waveform(void);
static void Process_Measurements(void);
static void Handle_User_Input(void);
int main(void)
{
/* 1. 系统初始化 */
System_Init();
/* 2. 显示开机界面 */
LCD_Clear(BLACK);
LCD_ShowString(100, 100, "Digital Oscilloscope", WHITE, BLACK);
LCD_ShowString(120, 130, "Initializing...", WHITE, BLACK);
Delay_ms(500);
/* 3. 启动ADC连续采集 */
ADC_DMA_Start((uint32_t)adc_buffer, BUFFER_SIZE);
/* 4. 主循环 */
while(1)
{
/* 4.1 检查触发状态 */
if(trigger_flag)
{
trigger_flag = 0;
data_ready = 1;
}
/* 4.2 数据处理与显示 */
if(data_ready)
{
/* 转换ADC数据为电压值 */
ADC_To_Voltage(adc_buffer, voltage_buffer, BUFFER_SIZE);
/* 触发处理 */
Signal_Trigger_Process(voltage_buffer, BUFFER_SIZE, &waveform);
/* 波形显示 */
Display_Waveform();
/* 测量计算 */
Process_Measurements();
/* 显示UI界面 */
UI_Update(&waveform);
data_ready = 0;
}
/* 4.3 用户输入处理 */
Handle_User_Input();
/* 4.4 FFT模式处理 */
if(waveform.display_mode == DISPLAY_FFT)
{
FFT_Process(voltage_buffer, BUFFER_SIZE);
Display_FFT_Spectrum();
}
}
}
/* 系统初始化函数 */
static void System_Init(void)
{
/* 1. 系统时钟配置 - 72MHz */
RCC->APB2ENR |= RCC_APB2ENR_AFIOEN | RCC_APB2ENR_IOPAEN |
RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPCEN |
RCC_APB2ENR_ADC1EN | RCC_APB2ENR_DMA1EN;
/* 2. 硬件模块初始化 */
LCD_Init(); /* TFT-LCD初始化 */
Touch_Init(); /* 触摸屏初始化 */
Encoder_Init(); /* 旋转编码器初始化 */
Buttons_Init(); /* 按键初始化 */
ADC_DMA_Init(); /* ADC和DMA初始化 */
/* 3. NVIC配置 */
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
/* 4. 变量初始化 */
waveform.display_mode = DISPLAY_WAVEFORM;
waveform.measure_mode = MEASURE_AUTO;
}
/* 波形显示函数 */
static void Display_Waveform(void)
{
uint16_t i, x, y;
float scaled_voltage;
/* 清除波形区域 */
LCD_Fill(WAVE_AREA_X, WAVE_AREA_Y,
WAVE_AREA_X + WAVE_AREA_WIDTH,
WAVE_AREA_Y + WAVE_AREA_HEIGHT, BLACK);
/* 绘制网格 */
Draw_Grid();
/* 绘制触发电平线 */
LCD_DrawLine(WAVE_AREA_X,
VOLT_TO_PIXEL(waveform.trigger_level),
WAVE_AREA_X + WAVE_AREA_WIDTH,
VOLT_TO_PIXEL(waveform.trigger_level),
GRAY);
/* 绘制波形 */
for(i = 0; i < BUFFER_SIZE; i++)
{
/* 电压缩放和偏移 */
scaled_voltage = (voltage_buffer[i] * waveform.voltage_scale) + waveform.offset_y;
/* 坐标转换 */
x = TIME_TO_PIXEL(i * waveform.timebase * 10) + waveform.offset_x; /* 10 samples per time unit */
y = VOLT_TO_PIXEL(scaled_voltage);
/* 限制在显示区域内 */
if(y < WAVE_AREA_Y) y = WAVE_AREA_Y;
if(y > WAVE_AREA_Y + WAVE_AREA_HEIGHT) y = WAVE_AREA_Y + WAVE_AREA_HEIGHT;
/* 绘制点 */
if(i == 0)
LCD_DrawPoint(x, y, CYAN);
else
LCD_DrawLine(prev_x, prev_y, x, y, CYAN);
prev_x = x;
prev_y = y;
}
}
/* 测量处理函数 */
static void Process_Measurements(void)
{
MeasurementResults results;
if(waveform.measure_mode == MEASURE_AUTO)
{
/* 自动测量 */
results.vpp = Calculate_Vpp(voltage_buffer, BUFFER_SIZE);
results.vrms = Calculate_Vrms(voltage_buffer, BUFFER_SIZE);
results.frequency = Calculate_Frequency(voltage_buffer, BUFFER_SIZE, waveform.timebase);
results.period = 1.0f / results.frequency;
/* 显示测量结果 */
Display_Measurements(&results);
}
else if(waveform.measure_mode == MEASURE_MANUAL)
{
/* 手动测量标记点处理 */
Handle_Manual_Measurement();
}
}
/* 用户输入处理函数 */
static void Handle_User_Input(void)
{
static uint32_t last_input_time = 0;
/* 检查编码器旋转 */
if(Encoder_Get_Direction() != ENC_NONE)
{
int8_t dir = Encoder_Get_Direction();
switch(waveform.current_menu)
{
case MENU_TIMEBASE:
waveform.timebase *= (dir > 0) ? 1.1f : 0.9f;
Clamp_Float(&waveform.timebase, 0.1f, 1000.0f);
break;
case MENU_VOLT_SCALE:
waveform.voltage_scale *= (dir > 0) ? 1.2f : 0.833f;
Clamp_Float(&waveform.voltage_scale, 0.1f, 5.0f);
break;
case MENU_TRIG_LEVEL:
waveform.trigger_level += (dir > 0) ? 0.1f : -0.1f;
Clamp_Float(&waveform.trigger_level, 0.0f, 3.3f);
break;
}
Encoder_Reset();
last_input_time = Get_Tick_Count();
}
/* 检查按键 */
uint8_t key = Buttons_Scan();
if(key != KEY_NONE)
{
switch(key)
{
case KEY_MODE:
waveform.display_mode = (waveform.display_mode + 1) % 3;
break;
case KEY_TRIGGER:
waveform.trigger_mode = (waveform.trigger_mode + 1) % 3;
break;
case KEY_MEASURE:
waveform.measure_mode = (waveform.measure_mode + 1) % 2;
break;
case KEY_RUN_STOP:
if(ADC_DMA_IsRunning())
ADC_DMA_Stop();
else
ADC_DMA_Start((uint32_t)adc_buffer, BUFFER_SIZE);
break;
case KEY_SINGLE:
waveform.trigger_mode = SINGLE;
ADC_DMA_Single_Shot((uint32_t)adc_buffer, BUFFER_SIZE);
break;
}
last_input_time = Get_Tick_Count();
}
/* 检查触摸屏 */
if(Touch_Scan())
{
TouchPoint tp = Touch_GetPoint();
UI_Touch_Handler(tp.x, tp.y, &waveform);
last_input_time = Get_Tick_Count();
}
/* 自动隐藏菜单 */
if((Get_Tick_Count() - last_input_time) > MENU_TIMEOUT)
{
UI_Hide_Menu();
}
}
/* DMA中断服务函数 - ADC采集完成 */
void DMA1_Channel1_IRQHandler(void)
{
if(DMA1->ISR & DMA_ISR_TCIF1)
{
DMA1->IFCR |= DMA_IFCR_CTCIF1;
/* 根据触发模式设置标志 */
switch(waveform.trigger_mode)
{
case AUTO:
trigger_flag = 1;
break;
case NORMAL:
if(Check_Trigger_Condition(voltage_buffer, BUFFER_SIZE,
waveform.trigger_level, waveform.trigger_edge))
trigger_flag = 1;
break;
case SINGLE:
if(Check_Trigger_Condition(voltage_buffer, BUFFER_SIZE,
waveform.trigger_level, waveform.trigger_edge))
{
trigger_flag = 1;
ADC_DMA_Stop();
}
break;
}
}
}
/* 辅助函数:浮点数范围限制 */
static void Clamp_Float(float* value, float min, float max)
{
if(*value < min) *value = min;
if(*value > max) *value = max;
}
总结
本设计成功实现了一款基于STM32的便携式数字示波器,具备全面的信号采集与分析功能。该系统能够实时采集0-200kHz带宽内的输入信号,并通过波形显示直观呈现,同时提供了自动、常规和单次三种触发模式,支持用户灵活设置触发电平与边沿,确保了波形捕获的精确性与稳定性。此外,示波器集成了电压幅值(如Vpp、Vrms)和信号频率/周期的手动与自动测量功能,并支持波形显示的缩放与平移操作,方便用户进行细节观察。FFT频域分析功能的加入,进一步扩展了其应用范围,允许将时域波形转换为频谱显示,以进行频率成分分析。
在硬件实现上,系统以STM32F103ZET6单片机为核心,驱动3.5寸TFT-LCD电阻触摸屏作为主控与显示模块,提供了友好的用户交互界面。信号调理模块采用OPA2350运放构成的衰减/放大电路,有效将输入信号调理至0-3.3V的ADC量程内,保证了信号适配的可靠性。模数转换模块直接利用STM32内置的12位ADC,配合DMA技术实现了最高1Msps的采样率,满足了高频信号采集的需求。用户输入模块结合旋转编码器与独立按键,实现了参数调节与功能切换的便捷操作。电源模块则基于MP1584EN DC-DC降压芯片,将外部5V输入高效转换为系统所需的3.3V,确保了整体供电的稳定与便携性。
综上所述,该便携式数字示波器设计在有限的硬件资源下,通过优化软硬件架构,实现了高性能的信号处理与显示功能。其紧凑的结构和丰富的特性,使其适用于教育、研发和现场调试等多种场景,体现了嵌入式系统在仪器仪表领域的实用价值与创新潜力。
- 点赞
- 收藏
- 关注作者
评论(0)