HNU-RFID与传感器原理实验

一、实验目的

了解ISO14443协议以及RFID的各个组成部分,基于STC单片机与RC522模块实现一个RFID的简易应用。

二、开发工具与环境

1.上位机应用程序:

开发软件:vscode

语言:python

UI:基于pyside6

2.下位机MCU:

开发软件:keil5

语言:c语言

三、智能门禁系统

(一)功能简介

1.门禁:根据用户RFID卡的ID进行身份验证

2.添加/删除用户ID

3.紧急模式:使门一直处于开启状态(如遇到紧急情况时,管理员可以开启)

(二)具体实现---------STC-B

以上所有功能底层实现其实都是在MCU里完成的,我将其分为4种模式mode:

Mode=1:门禁。

Mode=2:添加用户

Mode=3:删除用户

Mode=4:紧急模式

所以我只需要改变mode的值进行功能切换。而阅读器每100ms检测一次,将其放在100ms回调函数里面。代码如下:

1.四种模式实现

Mode=1时,检测周围有无RFID,若有读取ID并写到card_number里,然后用check()函数检查该ID是否在我的ID数据库(后续有具体实现)进行身份验证,若成功,则开门,并启动计数器KeepOpen_3S=0

在1s回调函数若发现计数器被启用了,则开始计数,若等于3s,则会关门:

Mode=2和3时,也是先检测周围RFID并,若有RFID,则调用add_user()/delete_user()在数据库里将该ID添加或删除:

Mode=4时,则使门一直开启:

2.ID数据库管理实现

每个用户都有唯一的ID身份,那么需要对每个"湖大"的学生ID存储起来,这时需要一个数据库进行存储,由于数据库一般是在上位机存储,并且需要学习一些数据库的知识,所以我用下位机MCU来存储每个用户ID数据

考虑到用户ID数据需要永久存储,即断电时不会时数据丢失,将其存储在非易失存储器M24C02中,BSP中也有M24C02模块:

M24C02提供256字节,RFID的ID总共4字节。我设定M24C02的第一个字节(0x00)存储用户ID个数。从第2字节(0x01)到最后存储每个用户的ID。总共可以存储最大63个用户。

注意到M24C02有"写"寿命限制(每一单元大约"写"寿命为10万次量级寿命)。所以上电后先将M24C02中的用户数据全部读到RAM中,只有当用户ID实现添加/删除功能时才将其写入M24C02中

创建两个模块manageID.h、manageID.c

(1)LoadUsersFromNVM

分别加载M24C02存储器的用户ID数和用户ID数据到userCount和数组userCashe中

(2)对某个地址写入4字节的用户ID
(3)添加用户add_user(const unsigned char *newID)

当添加用户时,写入到下一个可用位置,并增加用户数量。

(4)删除用户delete_user(const unsigned char *delID)

每次删除时,将最后一个用户移动到被删除的位置,并减少用户数量,这样只需两次写入(被删除的位置和最后一个位置)。

(5)身份验证check(const unsigned char *cardID)

3.与UI程序交互

规定UI程序可以通过发送指令来改变mode的值,达到切换功能,帧结构如下:

帧头(AA)+指令+data长度(0x01)+data(mode值)+校验和
(其中校验和=指令+ data长度+data)

目前只有一个指令0x20,作用是改变mode值,如:

让mode=2:AA 20 01 02 03 06

那么我需要时刻接收UI程序发送的5字节的帧,做出反应:

用BSP中uart1模块的接收回调函数


而在mode=1时,若有用户开门时,也会发生帧给UI程序,动态显示门的状态:

此时发给UI程序的指令时0x01。Data为0x01代表开门;0x00代表关门

3s后关门:

4.main函数

(三)具体实现------UI程序

1.准备工作(python环境)

(1)安装anaconda

使用anaconda配置虚拟环境能避免一些依赖问题,处理起来很麻烦。

我看的b站视频安装的:Anaconda安装

(2)学习python Qt并安装pyside6,在base虚拟环境下安装python、pyside6等环境

学习资料:白月黑羽

(3)vscode选择虚拟环境下的python解释器

我就用的base下的虚拟环境:

2.智能门禁系统UI具体实现

(1)串口通信线程模块

a) 创建串口线程,并一直循环运行串口检测

根据这些信号值的改变,动态更新UI界面

如status_updated门状态改变时,会调用handle_status函数进行更新UI:

10ms延时平衡响应速度与CPU占用

b) 连接管理connect_serial/disconnect_serial

使用QMutexLocker实现RAII锁机制

c) 数据发送协议

d) 接收数据

这里若收到MCU传来的门状态数据,就会给status_updated赋值,触发信号

(2)主界面模块(MainWindow)

主要分为三部分:

  1. create_serial_toolbar串口有关的一系列按钮,如设置波特率、连接等等
  2. create_mode_toolbar选择模式按钮(门禁模式、添加用户等等)
  3. init_mode_uis 一个标签。依赖于模式的选择,根据模式来动态显示标签、也会根据MCU的反馈来改变标签
  1. 串口有关的按钮

若点击了连接,则会调用toggle_connection函数

如果当前按钮显示"连接",代表此时未连接,则会调用connect_serial函数进行连接;否则会调用disconnect_serial函数断开连接:

  1. 模式的选择按钮

    当选择模式1、2、3时会调用switch_mode()函数,会发送指令给MCU,使其改变mode的值:

当选择模式4时会调用toggle_emergency_mode()函数,也会给MCU发送指令,使mode=4。该函数会调用update_door_image()进行对标签门状态更新为"打开"

  1. 一个动态显示的标签
    根据mode的不同进行改变,如当处于门禁模式时会显示一张门关闭的图片,当处于添加用户模式时,会显示一句话"请将要添加的校园卡放置在阅读器附近"。
(3)主函数以及界面样式
完整代码
python 复制代码
import sys
from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, QStackedWidget,
                              QVBoxLayout, QLabel, QPushButton,
                              QComboBox, QMessageBox, QToolBar)
from PySide6.QtCore import QThread, Signal, QMutex, Qt, QMutexLocker
from PySide6.QtGui import QPixmap
import serial
from serial.tools import list_ports

# ==================== 串口通信线程类 ====================
class SerialThread(QThread):
    data_received = Signal(bytes)
    status_updated = Signal(int, str)
    error_occurred = Signal(str)
    connection_changed = Signal(bool)

    def __init__(self):
        super().__init__()
        self.mutex = QMutex()
        self.ser = None
        self.running = True
        self.connected_flag = False
        self.current_mode = 1
        self.port = ""
        self.baudrate = 115200

    def run(self):
        while self.running:
            if self.ser and self.ser.is_open and self.connected_flag:
                try:
                    if self.ser.in_waiting:
                        data = self.ser.read_all()
                        self.parse_data(data)
                except Exception as e:
                    self.error_occurred.emit(f"串口读取错误: {str(e)}")
                QThread.msleep(10)

    def connect_serial(self, port, baudrate):
        with QMutexLocker(self.mutex):
            try:
                if self.ser and self.ser.is_open:
                    self.ser.close()
                self.ser = serial.Serial(port=port, baudrate=baudrate,
                                        parity=serial.PARITY_NONE,
                                        stopbits=serial.STOPBITS_ONE,
                                        timeout=0.5)
                self.port = port
                self.baudrate = baudrate
                self.connected_flag = True
                self.connection_changed.emit(True)
                return True
            except Exception as e:
                self.error_occurred.emit(f"连接失败: {str(e)}")
                self.connection_changed.emit(False)
                return False

    def disconnect_serial(self):
        with QMutexLocker(self.mutex):
            if self.ser and self.ser.is_open:
                try:
                    self.ser.close()
                except Exception:
                    pass
            self.connected_flag = False
        self.connection_changed.emit(False)

    def send_mode_command(self, mode_value):
        return self.send_command(0x20, bytes([mode_value]))

    def send_command(self, cmd, data=bytes()):
        with QMutexLocker(self.mutex):
            if not (self.ser and self.ser.is_open):
                self.error_occurred.emit("串口未连接")
                return False
            try:
                frame = bytes([0xAA, cmd, len(data)]) + data
                frame += bytes([sum(frame[1:]) & 0xFF])
                self.ser.write(frame)
                return True
            except Exception as e:
                self.error_occurred.emit(f"发送失败: {str(e)}")
                return False

    def parse_data(self, data):
        if len(data) < 4 or data[0] != 0xAA:
            return
        cmd, length = data[1], data[2]
        payload = data[3:3+length]
        if cmd == 0x01:
            status = "开启" if payload[0] else "关闭"
            self.status_updated.emit(1, status)
        elif cmd == 0x20 and payload and payload[0] == 0x01:
            self.current_mode = payload[1]
            self.status_updated.emit(5, f"模式切换成功:{self.current_mode}")

# ==================== 主窗口类 ====================
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.serial_thread = SerialThread()
        self.emergency_mode = False
        self.is_door_open = False
        self.init_ui()
        self.init_serial()
        self.setWindowTitle("智能门禁控制系统")
        self.resize(600, 400)

    def init_ui(self):
        self.stack = QStackedWidget()
        self.setCentralWidget(self.stack)
        self.create_serial_toolbar()
        self.create_mode_toolbar()
        self.init_mode_uis()

    def create_serial_toolbar(self):
        toolbar = QToolBar()
        self.addToolBar(Qt.TopToolBarArea, toolbar)
        self.port_combo = QComboBox()
        self.refresh_ports()
        self.baud_combo = QComboBox()
        self.baud_combo.addItems(["9600","19200","38400","57600","115200"])
        self.baud_combo.setCurrentText("9600")
        self.connect_btn = QPushButton("连接")
        self.connect_btn.clicked.connect(self.toggle_connection)
        self.status_indicator = QLabel()
        self.status_indicator.setFixedSize(20,20)
        toolbar.addWidget(QLabel("端口:"))
        toolbar.addWidget(self.port_combo)
        toolbar.addWidget(QLabel("波特率:"))
        toolbar.addWidget(self.baud_combo)
        toolbar.addWidget(QPushButton("刷新", clicked=self.refresh_ports))
        toolbar.addWidget(self.connect_btn)
        toolbar.addWidget(self.status_indicator)

    def create_mode_toolbar(self):
        toolbar = QToolBar()
        self.addToolBar(Qt.TopToolBarArea, toolbar)
        self.mode_combo = QComboBox()
        self.mode_combo.addItems(["门禁模式","添加用户","删除用户"])
        self.mode_combo.currentIndexChanged.connect(self.switch_mode)
        self.admin_btn = QPushButton("紧急开门")
        self.admin_btn.clicked.connect(self.toggle_emergency_mode)
        self.admin_btn.setEnabled(False)
        toolbar.addWidget(QLabel("工作模式:"))
        toolbar.addWidget(self.mode_combo); toolbar.addWidget(self.admin_btn)

    def init_mode_uis(self):
        # 门禁模式
        page1 = QWidget()
        l1 = QVBoxLayout(page1)
        self.door_img = QLabel()
        self.door_img.setAlignment(Qt.AlignCenter)
        self.door_img.setScaledContents(True)
        self.door_img.clear()  # 初始时不显示图片
        l1.addWidget(self.door_img)
        # 添加用户
        page2 = QWidget(); l2 = QVBoxLayout(page2)
        label2 = QLabel("请将要添加的校园卡放置在阅读器附近")
        label2.setAlignment(Qt.AlignCenter)
        label2.setStyleSheet("font-size:20px; color:black;")
        l2.addWidget(label2)
        # 删除用户
        page3 = QWidget(); l3 = QVBoxLayout(page3)
        label3 = QLabel("请将要删除的校园卡放置在阅读器附近")
        label3.setAlignment(Qt.AlignCenter)
        label3.setStyleSheet("font-size:20px; color:black;")
        l3.addWidget(label3)
        # 加入stack
        self.stack.addWidget(page1); self.stack.addWidget(page2)
        self.stack.addWidget(page3)

    def init_serial(self):
        self.serial_thread.status_updated.connect(self.handle_status)
        self.serial_thread.error_occurred.connect(self.show_error)
        self.serial_thread.connection_changed.connect(self.update_connection_status)
        self.serial_thread.start()

    def toggle_connection(self):
        if self.connect_btn.text() == "连接": self.connect_serial()
        else: self.disconnect_serial()

    def connect_serial(self):
        port = self.port_combo.currentText().split(" - ")[0]
        baud = int(self.baud_combo.currentText())
        if self.serial_thread.connect_serial(port, baud):
            self.connect_btn.setText("断开") 
            self.status_indicator.setStyleSheet("background:#4CAF50;border-radius:10px;")
            if self.mode_combo.currentIndex() == 0:
                self.is_door_open = False; self.stack.setCurrentIndex(0)
                self.update_door_image(False)
            self.admin_btn.setEnabled(True)

    def disconnect_serial(self):
        self.serial_thread.disconnect_serial()
        self.connect_btn.setText("连接")
        self.status_indicator.setStyleSheet("background:#f44336;border-radius:10px;")
        self.admin_btn.setEnabled(False)
        self.is_door_open = False
        self.stack.setCurrentIndex(0)  # 确保回到门禁界面
        self.update_door_image(None)  # 传递None清除图片

    def toggle_emergency_mode(self):
        if self.mode_combo.currentIndex() != 0: return
        if not self.emergency_mode and self.serial_thread.send_mode_command(4):
            self.emergency_mode=True
            self.is_door_open=True
            self.mode_combo.setEnabled(False)
            self.admin_btn.setText("退出紧急模式")
            self.stack.setCurrentIndex(0)
            self.update_door_image(True)
        elif self.emergency_mode and self.serial_thread.send_mode_command(1):
            self.emergency_mode=False; self.is_door_open=False
            self.mode_combo.setEnabled(True)
            self.admin_btn.setText("紧急开门"); self.stack.setCurrentIndex(0)
            self.update_door_image(False)

    def switch_mode(self, index):
        if self.emergency_mode: return
        if index in (0,1,2) and self.serial_thread.send_mode_command(index+1):
            self.stack.setCurrentIndex(index)
            # 紧急模式按钮状态
            self.admin_btn.setEnabled(self.serial_thread.connected_flag and index==0)
            if index==0: self.update_door_image(self.is_door_open)

    def update_door_image(self, open_status:bool):
        if open_status is None:  # 新增空状态处理
            self.door_img.clear()
        else:
            pix = QPixmap(f"images/door_{'open' if open_status else 'closed'}.png")
            self.door_img.setPixmap(pix)

    def update_connection_status(self, connected):
        col="#4CAF50" if connected else "#f44336"
        self.status_indicator.setStyleSheet(f"background:{col};border-radius:10px;")
        self.connect_btn.setText("断开" if connected else "连接")
        self.admin_btn.setEnabled(connected and self.mode_combo.currentIndex()==0)
        if connected and self.mode_combo.currentIndex()==0:
            self.is_door_open=False; self.stack.setCurrentIndex(0); self.update_door_image(False)

    def handle_status(self, mode, status):
        if mode==1:
            self.is_door_open=(status=="开启")
            if not self.emergency_mode: self.update_door_image(self.is_door_open)
        # elif mode==5:
        #     new_m=int(status.split(":")[1])
        #     if new_m==4: self.emergency_mode=True; self.is_door_open=True; self.stack.setCurrentIndex(0); self.update_door_image(True)
        #     elif new_m==1: self.emergency_mode=False; self.is_door_open=False; self.stack.setCurrentIndex(0); self.update_door_image(False)

    def refresh_ports(self):
        self.port_combo.clear(); self.port_combo.addItems([f"{p.device} - {p.description}" for p in list_ports.comports()])

    def show_error(self, msg):
        QMessageBox.critical(self, "错误", msg)
        if "串口" in msg: self.disconnect_serial()

APP_STYLE = '''
QMainWindow {
    background-image: url(images/background.png);
    background-repeat: no-repeat;
    background-position: center;
}
QToolBar {
    padding:5px; border-bottom:1px solid #bdc3c7;
}
QToolBar QLabel {
    color: black;
}
'''

if __name__=="__main__":
    app=QApplication(sys.argv)
    app.setStyleSheet(APP_STYLE)
    window=MainWindow()
    window.show()
    sys.exit(app.exec())
(4)UI界面效果演示

i. 初始界面(未连接时)

i. 连接以后,初始为门禁模式,由于刚开始门是关的,所以为一个关门的图片

ii. 当有校园卡在阅读器附近时,若身份验证成功,会改变图片,变为开门

iii. 当模式为添加用户/删除用户时,会显示一段提示语,这是校园卡就可以放在阅读器附近进行添加/删除了

iv. 紧急模式下,会给MCU一个改变mode=4的指令,同时标签一直显示开门图片

四、校园卡消费与充值

(一)功能简介

1.消费

2.充值

(二)具体实现---------STC-B

1.实现思路

同样的,用一个变量mode来切换"消费"与"充值"功能。

Mode=1 -> 消费

Mode=2 -> 充值

每个RFID都有一些可以读写的内存,那么我只需要规定某一个"区域"是用来存储"余额"的,消费和充值无非就是对这个"区域"进行加减运算。我们校园卡是S50卡,总共16个扇区,每个扇区都有3个数据块和一个控制块,所以有64个块(0x00-0x3f),每个块都是16字节。

实验里,我用0x3c块进行操作,由于只是模拟,所以我只取这16个字节的后两个字节作为"余额"。

所以还是每100ms检测周围有无RFID卡,根据mode的值再进行相关功能逻辑

2.消费功能

在100ms回调函数里,每次检测有无RFID卡片,若有,则读出0x3c中的后两位的值进行相关转换得到余额,再进行"消费"(余额-value),其中,value是上位机UI程序输入的消费值。


不过还要注意一个细节,当余额小于value时,即余额不足时,MCU给UI发送的是clock(全ff)

3.充值功能

与消费差不多,只不过是对余额的基础上加上value

4.与UI交互

在交互前,双方要规定发送/接收的帧的结构:

Header(0xaa)+命令+data长度+data+校验和sum

总共两个命令:

  1. 0x20:改变mode的值(UI切换功能时)
    有两种data:0x0001和0x0002
  2. 0x40:UI消费/充值确定时发送
    会发送value值,data为两个字节

(三)具体实现------UI程序

1.UI整体设计

从上到下,分为4个部分:

一、 串口配置等一系列按钮

二、 功能选择(消费/充值)

三、 设定消费/充值的值 + 余额显示

四、 日志显示(一系列操作的记录)+清除日志

2.串口模块

点击"检测串口",会调用refresh_ports函数,检测有无可连接的串口:

点击"连接"/"断开",会调用toggle_connection函数,实现串口连接/断开:

3.功能切换

点击"消费模式"或"充值模式"都会调用set_mode函数,不过传的参数分别为1和2。会给MCU发送改变mode值的指令:

4.余额显示以及设置value

点击 "确定"时,会调用发送金额指令改变MCU中value的值:

若MCU检测到校园卡会发送16字节数据给UI,UI收到后会提取后两个字节并处以100,实时显示余额;当校园卡离开后,3s后不再显示:

5.日志显示

在上面的按钮中,如切换模式时、点击确定时、余额不足时都会添加日志消息:


当点击"清除日志"时,会将日志清空:

完整代码

python 复制代码
import sys
from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, QGroupBox, QVBoxLayout,
                              QHBoxLayout, QPushButton, QLineEdit, QLabel,
                              QComboBox, QTextEdit, QMessageBox)
from PySide6.QtCore import QTimer, Qt
import serial
import serial.tools.list_ports
import time

class RFIDApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.ser = None
        self.current_mode = 1  # 1-消费 2-充值
        self.last_seen = None  # 上次检测到卡片的时间戳
        self.init_ui()
        self.init_serial()
        
    def init_ui(self):
        # 主窗口设置
        self.setWindowTitle("校园卡模拟系统")
        self.setGeometry(400, 200, 600, 400)

        # 串口配置区
        serial_group = QGroupBox("串口配置")
        self.port_combo = QComboBox()
        self.refresh_btn = QPushButton("检测串口")
        self.refresh_btn.clicked.connect(self.refresh_ports)
        self.baud_combo = QComboBox()
        self.baud_combo.addItems(["9600", "115200"])
        self.connect_btn = QPushButton("连接")
        self.connect_btn.clicked.connect(self.toggle_connection)
        
        serial_layout = QHBoxLayout()
        serial_layout.addWidget(QLabel("端口:"))
        serial_layout.addWidget(self.port_combo)
        serial_layout.addWidget(self.refresh_btn)
        serial_layout.addWidget(QLabel("波特率:"))
        serial_layout.addWidget(self.baud_combo)
        serial_layout.addWidget(self.connect_btn)
        serial_group.setLayout(serial_layout)

        # 模式切换区
        mode_group = QGroupBox("操作模式")
        self.consume_btn = QPushButton("消费模式")
        self.recharge_btn = QPushButton("充值模式")
        self.consume_btn.setCheckable(True)
        self.recharge_btn.setCheckable(True)
        self.consume_btn.clicked.connect(lambda: self.set_mode(1))
        self.recharge_btn.clicked.connect(lambda: self.set_mode(2))
        
        mode_layout = QHBoxLayout()
        mode_layout.addWidget(self.consume_btn)
        mode_layout.addWidget(self.recharge_btn)
        mode_group.setLayout(mode_layout)

        # 金额输入区
        input_group = QGroupBox("金额操作")
        self.value_input = QLineEdit()
        self.value_input.setPlaceholderText("输入金额(元)")
        self.send_btn = QPushButton("确定")
        self.send_btn.clicked.connect(self.send_command)
        
        input_layout = QHBoxLayout()
        input_layout.addWidget(self.value_input)
        input_layout.addWidget(self.send_btn)
        input_group.setLayout(input_layout)

        # 状态显示区
        self.balance_label = QLabel("当前余额:--.-- 元")
        self.balance_label.setAlignment(Qt.AlignCenter)
        self.balance_label.setStyleSheet("font-size: 20px; color: #2ecc71;")
        
        # 日志显示
        self.log_text = QTextEdit()
        self.log_text.setReadOnly(True)
        # 清除日志按钮
        self.clear_btn = QPushButton("清除日志")
        self.clear_btn.clicked.connect(self.clear_log)

        # 主布局
        main_layout = QVBoxLayout()
        main_layout.addWidget(serial_group)
        main_layout.addWidget(mode_group)
        main_layout.addWidget(input_group)
        main_layout.addWidget(self.balance_label)
        main_layout.addWidget(self.log_text)
        main_layout.addWidget(self.clear_btn)

        container = QWidget()
        container.setLayout(main_layout)
        self.setCentralWidget(container)

    def refresh_ports(self):
        """刷新串口列表"""
        self.init_serial()
        self.log_text.append("串口列表已更新")

    def clear_log(self):
        """清除日志"""
        self.log_text.clear()

    def init_serial(self):
        """初始化串口设备列表"""
        ports = serial.tools.list_ports.comports()
        self.port_combo.clear()
        for port in ports:
            self.port_combo.addItem(port.device)

    def toggle_connection(self):
        """切换串口连接状态"""
        if self.ser and self.ser.is_open:
            self.ser.close()
            self.connect_btn.setText("连接")
            self.log_text.append("连接已断开")
        else:
            try:
                self.ser = serial.Serial(
                    port=self.port_combo.currentText(),
                    baudrate=int(self.baud_combo.currentText()),
                    timeout=1
                )
                QTimer.singleShot(100, self.read_serial)
                self.connect_btn.setText("断开")
                self.log_text.append("串口连接成功")
            except Exception as e:
                self.show_error(f"连接失败: {str(e)}")

    def set_mode(self, mode):
        """设置操作模式"""
        self.current_mode = mode
        self.consume_btn.setChecked(mode == 1)
        self.recharge_btn.setChecked(mode == 2)
        self.log_text.append(f"切换到 {'消费' if mode ==1 else '充值'} 模式")
        self.send_mode_command(mode)

    def send_mode_command(self, mode):
        """发送模式切换命令"""
        if not self.ser or not self.ser.is_open:
            self.show_error("请先连接串口")
            return
        
        cmd = 0x20
        frame = self.build_frame(cmd, mode)
        self.ser.write(frame)
        self.log_text.append(f"发送模式指令: {bytes(frame).hex()}")

    def build_frame(self, cmd, value):
        """构建协议帧"""
        header = 0xAA
        length = 0x02
        
        if cmd == 0x40:  # 金额指令
            # 四舍五入处理,避免浮点截断误差
            cents = int(round(float(value) * 100))
            data_bytes = [(cents >> 8) & 0xFF, cents & 0xFF]
        else:  # 模式指令
            data_bytes = [0x00, value]
        
        frame = [header, cmd, length] + data_bytes
        checksum = sum(frame[1:]) % 256
        frame.append(checksum)
        return bytes(frame)

    def send_command(self):
        """发送金额指令"""
        if not self.validate_input():
            return
        
        cmd = 0x40
        frame = self.build_frame(cmd, self.value_input.text())
        
        try:
            self.ser.write(frame)
            self.log_text.append(f"发送指令: {bytes(frame).hex()}")
            self.value_input.clear()
        except Exception as e:
            self.show_error(f"发送失败: {str(e)}")

    def validate_input(self):
        """验证输入有效性"""
        if not self.ser or not self.ser.is_open:
            self.show_error("请先连接串口")
            return False
        
        try:
            float(self.value_input.text())
            return True
        except ValueError:
            self.show_error("请输入有效的数字金额")
            return False

    def read_serial(self):
        """读取串口数据并处理卡片离开逻辑"""
        if self.ser and self.ser.is_open:
            try:
                if self.ser.in_waiting >= 16:
                    while self.ser.in_waiting >= 16:
                        data = self.ser.read(16)
                        # 余额不足(16字节全0xFF)
                        if self.current_mode == 1 and data == b'\xff' * 16:
                            self.log_text.append("余额不足")
                            continue
                        balance = self.parse_balance(data)
                        self.update_display(balance)
                    self.last_seen = time.time()
                else:
                    if self.last_seen and (time.time() - self.last_seen) > 3:
                        self.balance_label.setText("当前余额:--.-- 元")
                        self.balance_label.setStyleSheet("font-size: 20px; color: #2ecc71;")
                        self.last_seen = None
            except Exception as e:
                self.log_text.append(f"读取错误: {str(e)}")
            
            QTimer.singleShot(100, self.read_serial)

    def parse_balance(self, data):
        """解析余额数据"""
        if len(data) >= 16:
            high_byte = data[14]
            low_byte = data[15]
            balance = (high_byte << 8) | low_byte
            return balance / 100.0
        return 0.0

    def update_display(self, balance):
        """更新界面显示"""
        self.balance_label.setText(f"当前余额:{balance:.2f} 元")
        self.balance_label.setStyleSheet(
            "font-size: 20px; color: #2ecc71;" if balance >= 0 else "font-size: 20px; color: #e74c3c;"
        )

    def show_error(self, message):
        """显示错误信息"""
        QMessageBox.critical(self, "错误", message)

    def closeEvent(self, event):
        """关闭窗口事件处理"""
        if self.ser and self.ser.is_open:
            self.ser.close()
        event.accept()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = RFIDApp()
    window.show()
    sys.exit(app.exec())

(四)UI界面效果演示

1.初始界面

2.消费模式

连接MCU后,点击"消费模式",会发现日志会更新

(1)当有校园卡在MCU周围时,会显示余额
(2)消费20.28元时

点击"确定"前:

点击"确定"后:

(3)校园卡离开后3s不再显示余额:
(4)若余额不足时:


3.充值模式

(1)点击充值模式
(2)充值10元:


4.清除日志

五、总结

(一)问题与难点

1.STC-B与RC522模块的接口连接,以及驱动代码的迁移,不过老师给了指导手册,就变得简单多了

2.在编译时data的数据太大了,编译不了:

(1)可以将Mermory Model改为large就可以了

但这样也会出现问题,当调用BSP的一些模块时会编译失败,原因是编写BSP的工程用的small型与当前的large不兼容,所以还是只能用small型

(2)将所有的变量声明前面加上xdata

3.MCU与UI程序的交互,要规定帧的结构

4.UI界面程序的编写,要从0开始学习

六、GitHub工程文件自取

相关推荐
GodKK老神灭1 小时前
FOC中PLL的点乘法
单片机
逐步前行2 小时前
STM32_DMA_寄存器操作
stm32·单片机·嵌入式硬件
计算机安禾3 小时前
【C语言程序设计】第39篇:预处理器与宏定义
c语言·开发语言·c++·vscode·算法·visual studio code·visual studio
本喵是FW3 小时前
C语言手记3
c语言·开发语言
HABuo4 小时前
【linux线程(一)】线程概念、线程控制详细剖析
linux·运维·服务器·c语言·c++·ubuntu·centos
Hello World . .4 小时前
51单片机基础外设:中断、定时器/计数器(PWM控制蜂鸣器、电机)
单片机·嵌入式硬件·51单片机
WangLanguager5 小时前
foc最终要求的是相电压,还是线电压
单片机
LCG元5 小时前
基于STM32CubeMX的HAL库串口通信与DMA传输深度优化
stm32·单片机·嵌入式硬件
C羊驼5 小时前
C语言学习笔记(十一):数据在内存中的存储
c语言·经验分享·笔记·学习