概述

程序思路

源代码

1. 概述

数电实验最终需要做一个 PC 示波器,不知道为啥要自己写 PC 端程序…

要求如下

将待测波形通过 ADC 模块进行数据采集,FPGA 开发板通过串口将采集到的数据传送给上位机,在上位机上显示波形,同时显示被测波形频率和峰峰值等参数。待测波形为方波、锯齿波及正弦波。上位机推荐使用 PC 机,建议在上位机上对接收到的数据进行处理。上位机界面可以使用学过的 c#或 vc++编写,也可以采用其他软件如 MATLAB、Labview 等软件进行数据处理及波形和参数的显示。

还是 python 用着舒服…

运行截图

波形数据产生源码

github 项目地址

2. 程序思路

总体思路

  1. 检测端口接入,最多同时支持 3 个串行接口,当检测到端口的时候,可以在菜单中进行选择,只能在程序启动的时候检测端口,也就是说端口必须在程序启动前接入 PC
  2. 设置一个按钮,用来开启和暂停接受串口数据
  3. 设置一个缓冲区,用来接受串口数据,当缓冲区满的时候刷新屏幕并将缓冲区清空
  4. 使用 QT 中的 QPainter 来绘图显示波形,将数据看作无符号 7 位整数,范围为 0~127
  5. 根据收到的数据计算频率和峰峰值

部分细节

  1. 串行数据格式:

    波特率:9600
    校验位:无
    停止位:1 位
    数据位:7 位(数字大小为 0~127)

  2. 可以通过调节缩放比例来调整缓冲区大小,间接调整缩放比例

  3. 计算频率是根据图形穿过中点的次数来计算的,计算峰峰值是最大值减最小值

  4. 测试图片是使用 arduino nano 产生数字周期信号进行测试的

局限

只能限制缓冲区大小来进行缩放,如果缓冲区太小的话刷新速度会很快,画面会闪动

即使波形一样,由于缓冲区只是简单接受,刷新前后的图形可能会有很大变化(波形一样,相位不同)

依赖

pyqt,pyserial,python3

3. 源代码

main.py

1
2
3
4
5
6
7
8
9
10
from fpga_gui import *
from sys import exit, argv

if __name__ == "__main__":
app = QtWidgets.QApplication(argv)
MainWindow = QtWidgets.QMainWindow()
ui = Ui_MainWindow()
ui.setupUi(MainWindow)
MainWindow.show()
exit(app.exec_())

PaintBoard.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121

from PyQt5.QtWidgets import QWidget
from PyQt5.Qt import QPixmap, QPainter, QPoint, QPaintEvent, QMouseEvent, QPen, \
QColor, QSize
from PyQt5.QtCore import Qt


class PaintBoard(QWidget):

def __init__(self, Parent=None):
'''
Constructor
'''
super().__init__(Parent)

self.__InitData() # 先初始化数据,再初始化界面
self.__InitView()

def __InitData(self):
self.WIDTH=500
self.HEIGHT=350
self.__size = QSize(self.WIDTH, self.HEIGHT)

# 新建QPixmap作为画板,尺寸为__size
self.__board = QPixmap(self.__size)
self.__board.fill(Qt.black) # 用白色填充画板

self.__IsEmpty = True # 默认为空画板
self.EraserMode = False # 默认为禁用橡皮擦模式

self.__lastPos = QPoint(0, 0) # 上一次鼠标位置
self.__currentPos = QPoint(0, 0) # 当前的鼠标位置

self.__painter = QPainter() # 新建绘图工具

self.__thickness = 1 # 默认画笔粗细为1px
self.__penColor = QColor("white") # 设置默认画笔颜色为黑色
self.__colorList = QColor.colorNames() # 获取颜色列表

def __InitView(self):
# 设置界面的尺寸为__size
self.setFixedSize(self.__size)

def Clear(self):
# 清空画板
self.__board.fill(Qt.black)
self.update()
self.__IsEmpty = True

def ChangePenColor(self, color="white"):
# 改变画笔颜色
self.__penColor = QColor(color)

def ChangePenThickness(self, thickness=1):
# 改变画笔粗细
self.__thickness = thickness

def IsEmpty(self):
# 返回画板是否为空
return self.__IsEmpty

def GetContentAsQImage(self):
# 获取画板内容(返回QImage)
image = self.__board.toImage()
return image

def paintEvent(self, paintEvent):
# 绘图事件
# 绘图时必须使用QPainter的实例,此处为__painter
# 绘图在begin()函数与end()函数间进行
# begin(param)的参数要指定绘图设备,即把图画在哪里
# drawPixmap用于绘制QPixmap类型的对象
self.__painter.begin(self)
# 0,0为绘图的左上角起点的坐标,__board即要绘制的图
self.__painter.drawPixmap(0, 0, self.__board)
# self.__painter.drawPolyline()
self.__painter.end()

# 保存原本画图功能
# 点击事件不触发刷新
def mousePressEvent(self, mouseEvent):
# 鼠标按下时,获取鼠标的当前位置保存为上一次位置
self.__currentPos = mouseEvent.pos()
self.__lastPos = self.__currentPos

# 移动事件触发刷新
def mouseMoveEvent(self, mouseEvent):
# 鼠标移动时,更新当前位置,并在上一个位置和当前位置间画线
self.__currentPos = mouseEvent.pos()
self.__painter.begin(self.__board)

if self.EraserMode == False:
# 非橡皮擦模式
self.__painter.setPen(QPen(self.__penColor, self.__thickness)) # 设置画笔颜色,粗细
else:
# 橡皮擦模式下画笔为纯白色,粗细为10
self.__painter.setPen(QPen(Qt.white, 10))

# 画线
self.__painter.drawLine(self.__lastPos, self.__currentPos)
self.__painter.end()
self.__lastPos = self.__currentPos

self.update() # 更新显示

def mouseReleaseEvent(self, mouseEvent):
self.__IsEmpty = False # 画板不再为空

# 画图
# 刷新缓冲区时调用这个函数
def draw_wave(self,data):
length=len(data)
points=[QPoint(int(i*self.WIDTH/length),self.HEIGHT-int(data[i]*self.HEIGHT/128)) for i in range(length)]

self.Clear()
self.__painter.begin(self.__board)
self.__painter.setPen(QPen(self.__penColor, self.__thickness)) # 设置画笔颜色,粗细
self.__painter.drawPolyline(*points)
self.__painter.end()
self.update()

fpga_gui.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'fpga_displayui.ui'
#
# Created by: PyQt5 UI code generator 5.11.3
#
# WARNING! All changes made in this file will be lost!

from PyQt5 import QtCore, QtGui, QtWidgets
from PaintBoard import PaintBoard
from serial import Serial,PARITY_NONE,STOPBITS_ONE,SEVENBITS
import serial.tools.list_ports
from time import time


class Ui_MainWindow(object):

def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(720, 400)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")

# 画板
self.board = PaintBoard(self.centralwidget)
self.board.setEnabled(True)
self.board.setGeometry(QtCore.QRect(200, 10, 500, 360))

self.board.setObjectName("widget")

# UI初始化
# 连接状态提示
self.lineEdit = QtWidgets.QLineEdit(self.centralwidget)
self.lineEdit.setGeometry(QtCore.QRect(10, 30, 170, 30))
font = QtGui.QFont()
font.setPointSize(17)
self.lineEdit.setFont(font)
self.lineEdit.setReadOnly(True)

# 缩放比例调整
self.label = QtWidgets.QLabel(self.centralwidget)
self.label.setGeometry(QtCore.QRect(20, 110, 81, 21))
font = QtGui.QFont()
font.setPointSize(12)
self.label.setFont(font)
self.label.setObjectName("label")
self.lineEdit.setObjectName("lineEdit")
self.doubleSpinBox = QtWidgets.QDoubleSpinBox(self.centralwidget)
self.doubleSpinBox.setGeometry(QtCore.QRect(110, 110, 62, 22))
self.doubleSpinBox.setMinimum(10.0)
self.doubleSpinBox.setMaximum(90.0)
self.doubleSpinBox.setProperty("value", 50.0)
self.doubleSpinBox.setObjectName("doubleSpinBox")
self.doubleSpinBox.valueChanged.connect(self.changerate)

# # 纵坐标调整
# self.label = QtWidgets.QLabel(self.centralwidget)
# self.label.setGeometry(QtCore.QRect(20, 90, 81, 21))
# font = QtGui.QFont()
# font.setPointSize(12)
# self.label.setFont(font)
# self.label.setObjectName("label")
# self.lineEdit.setObjectName("lineEdit")
# self.doubleSpinBox = QtWidgets.QDoubleSpinBox(self.centralwidget)
# self.doubleSpinBox.setGeometry(QtCore.QRect(110, 90, 62, 22))
# self.doubleSpinBox.setMinimum(16.0)
# self.doubleSpinBox.setProperty("value", 32.0)
# self.doubleSpinBox.setObjectName("doubleSpinBox")
#
# # 横坐标调整
# self.label_2 = QtWidgets.QLabel(self.centralwidget)
# self.label_2.setGeometry(QtCore.QRect(20, 130, 81, 21))
# font = QtGui.QFont()
# font.setPointSize(12)
# self.label_2.setFont(font)
# self.label_2.setObjectName("label_2")
# self.doubleSpinBox_2 = QtWidgets.QDoubleSpinBox(self.centralwidget)
# self.doubleSpinBox_2.setGeometry(QtCore.QRect(110, 130, 62, 22))
# self.doubleSpinBox_2.setMinimum(50.0)
# self.doubleSpinBox_2.setMaximum(99.0)
# self.doubleSpinBox_2.setObjectName("doubleSpinBox_2")

# 峰峰值标签
self.label_3 = QtWidgets.QLabel(self.centralwidget)
self.label_3.setGeometry(QtCore.QRect(20, 190, 81, 31))
font = QtGui.QFont()
font.setPointSize(17)
self.label_3.setFont(font)
self.label_3.setObjectName("label_3")
self.label_4 = QtWidgets.QLabel(self.centralwidget)
self.label_4.setGeometry(QtCore.QRect(110, 200, 71, 16))
self.label_4.setObjectName("label_4")

# 频率标签
self.label_5 = QtWidgets.QLabel(self.centralwidget)
self.label_5.setGeometry(QtCore.QRect(20, 220, 81, 31))
font = QtGui.QFont()
font.setPointSize(17)
self.label_5.setFont(font)
self.label_5.setObjectName("label_5")
self.label_6 = QtWidgets.QLabel(self.centralwidget)
self.label_6.setGeometry(QtCore.QRect(110, 230, 71, 16))
self.label_6.setObjectName("label_6")

# 控制按钮
self.pushButton = QtWidgets.QPushButton(self.centralwidget)
self.pushButton.setGeometry(QtCore.QRect(30, 300, 140, 50))
font = QtGui.QFont()
font.setPointSize(22)
self.pushButton.setFont(font)
self.pushButton.setObjectName("pushButton")
self.running = False
self.pushButton.clicked.connect(self.switch)

# 菜单
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 718, 23))
self.menubar.setObjectName("menubar")
self.menu = QtWidgets.QMenu(self.menubar)
self.menu.setObjectName("menu")

# 二级菜单标签
# 选择串口标签
self.menu_2 = QtWidgets.QMenu(self.menu)
self.menu_2.setObjectName("menu_2")
MainWindow.setMenuBar(self.menubar)

# 选择串口
self.actionCOM1 = QtWidgets.QAction(MainWindow)
self.actionCOM1.setObjectName("actionCOM1")
self.actionCOM1.triggered.connect(lambda: self.selectCOM(0))
self.actionCOM2 = QtWidgets.QAction(MainWindow)
self.actionCOM2.setObjectName("actionCOM2")
self.actionCOM2.triggered.connect(lambda: self.selectCOM(1))
self.actionCOM3 = QtWidgets.QAction(MainWindow)
self.actionCOM3.setObjectName("actionCOM3")
self.actionCOM3.triggered.connect(lambda: self.selectCOM(2))
self.menu_2.addAction(self.actionCOM1)
self.menu_2.addAction(self.actionCOM2)
self.menu_2.addAction(self.actionCOM3)
self.menu.addAction(self.menu_2.menuAction())
self.menubar.addAction(self.menu.menuAction())



# 定时器接收数据
self.pretime = time()
self.timer = QtCore.QTimer()
self.timer.timeout.connect(self.data_receive)

self.retranslateUi(MainWindow)
self.initdata()
QtCore.QMetaObject.connectSlotsByName(MainWindow)

def retranslateUi(self, MainWindow):

# 文字初始化
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "FPGA_Display"))
self.lineEdit.setText(_translate("MainWindow", "端口未选择"))
self.label.setText(_translate("MainWindow", "缩放比例"))
# self.label.setText(_translate("MainWindow", "纵坐标大小"))
# self.label_2.setText(_translate("MainWindow", "横坐标大小"))
self.label_3.setText(_translate("MainWindow", "峰峰值:"))
self.label_4.setText(_translate("MainWindow", "0"))
self.label_5.setText(_translate("MainWindow", " 频率:"))
self.label_6.setText(_translate("MainWindow", "0"))
self.pushButton.setText(_translate("MainWindow", "开始"))
self.menu.setTitle(_translate("MainWindow", "菜单"))
self.menu_2.setTitle(_translate("MainWindow", "选择端口"))
self.actionCOM1.setText(_translate("MainWindow", "COM1"))
self.actionCOM2.setText(_translate("MainWindow", "COM2"))
self.actionCOM3.setText(_translate("MainWindow", "COM3"))

def initdata(self):
# 初始化串行端口
# self.currentCOM=0
# self.COMs=[serial.Serial("COM3",9600,timeout=0.5) for i in range(1,3)]
# 获取可用的端口
port_list = list(serial.tools.list_ports.comports())
self.port_num = len(port_list)
for i in port_list:
print(i.device)
self.currentCOM_index = -1
self.currentCOM = None
self.COMs = []

if self.port_num == 0:
print("没有可用端口")
self.pushButton.setEnabled(False)
self.actionCOM1.setEnabled(False)
self.actionCOM2.setEnabled(False)
self.actionCOM3.setEnabled(False)
else:
# 有可用的端口
# 初始化缓存区,缓冲区定义为MAXBUFF字节
self.MAXBUFF=100
self.databuff = []
self.buffremain = self.MAXBUFF

# 初始化端口
self.lineEdit.setText("端口COM1已打开")
self.currentCOM_index = 0
self.COMs = [Serial(port=seri.device, baudrate=9600,
timeout=0.5,parity=PARITY_NONE,
stopbits = STOPBITS_ONE,bytesize=SEVENBITS)
for seri in port_list]
self.currentCOM = self.COMs[0]

if self.port_num == 1:
self.actionCOM2.setEnabled(False)
self.actionCOM3.setEnabled(False)
elif self.port_num == 2:
self.actionCOM3.setEnabled(False)

# 改变缩放比例
def changerate(self):
self.MAXBUFF = int(self.doubleSpinBox.value()*2)
# self.buffremain = self.MAXBUFF
# self.databuff = []

# 打开串口
def port_open(self):
# self.currentCOM.port = self.s1__box_2.currentText()
# self.currentCOM.baudrate = int(self.s1__box_3.currentText())
# self.currentCOM.bytesize = int(self.s1__box_4.currentText())
# self.currentCOM.stopbits = int(self.s1__box_6.currentText())
# self.currentCOM.parity = self.s1__box_5.currentText()
# self.currentCOM.baudrate=9600
# self.currentCOM.bytesize=8
# self.currentCOM.stopbits=1
# self.currentCOM.parity="N"


try:
self.currentCOM.open()
except (Exception, BaseException) as e:
print(e)
print( "Port Error", "此串口不能被打开!")

# 打开串口接收定时器,周期为2ms
self.timer.start(10)

return self.currentCOM.isOpen()

# 关闭串口
def port_close(self):
self.timer.stop()
try:
self.currentCOM.close()
except:
pass
return not self.currentCOM.isOpen()

# 在菜单中选择端口
def selectCOM(self, n):
self.currentCOM_index = n if (n < self.port_num) else 0
self.currentCOM = self.COMs[n]
self.lineEdit.setText("端口COM%d已打开" % (n + 1))
print("OK", n)

# 运行/停止运行按钮
def switch(self):
# 尝试进行切换
result=False
if self.running:
result=self.port_close()
else:
result=self.port_open()

# 根据切换结果进行设置
if result:
self.running = not self.running
if self.running:
self.pushButton.setText("暂停")
else:
self.pushButton.setText("开始")
else:
print("false")

# 接收数据
def data_receive(self):
try:
# 获取缓存区中的个数
num = self.currentCOM.inWaiting()
num = num if num < self.buffremain else self.buffremain
except (Exception, BaseException) as e:
print(e)
exit(-1)
if num > 0:
data = self.currentCOM.read(num)
num = len(data)
print(data)


# 统计接收字符的数量
self.buffremain -= num
# 添加数据到缓冲区中
self.databuff.extend(data)
else:
pass

if self.buffremain <= 0:
self.refresh()

# 缓冲区满,触发刷新
def refresh(self):
fre=0
mid = (max(self.databuff)+min(self.databuff))/2
now = time()
perid = now-self.pretime
self.pretime = time()

# 计算频率
for i in range(len(self.databuff)-2):
if (self.databuff[i]-mid)*(self.databuff[i+2]-mid)<0:
fre+=1

# 计算峰峰值
vvp=max(self.databuff)-min(self.databuff)
self.label_4.setText(str(vvp))
self.label_6.setText(str(round(fre/2/perid,2)))

self.board.draw_wave(self.databuff)
self.buffremain = self.MAXBUFF
self.databuff = []
print("=================refresh!!!!================")