自己写了一个小华的时钟校准补偿工具

开源如下

png转ico:https://www.toolhelper.cn/Image/ImageToIco
生成ico:https://www.iconfont.cn/search/index?searchType=icon&q=%E6%97%B6%E9%92%9F%E6%A0%A1%E5%87%86
ico转base64:https://www.lddgo.net/convert/imagebasesix

import warnings
warnings.filterwarnings('ignore', category=DeprecationWarning)

from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import sys
import serial
import serial.tools.list_ports
import base64

# RTC 校准工具图标 (base64 编码的 ICO 文件)
ICON_BASE64 = "AAABAAEAEBAAAAAAIADoAQAAFgAAAIlQTkcNChoKAAAADUlIRFIAAAAQAAAAEAgGAAAAH/P/YQAAAARnQU1BAACxjwv8YQUAAAABc1JHQgCuzhzpAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAFmSURBVDiNjVK9SkMxGD1fvIJDhw7iC6iDUHC0LoKLzgVxufnuZt/BSWdB8AFcbDIU8gQ6uDgJTqKTdXETXerSixfv55LIbWiKWUIO5+c7SQgzltZ6RESrMW6MoRibArTW10S0BwAicmmtPQKAoih2ReR2nhGYWZhZAKiA5Xne9dgsHrTWJ8oTD4JzURQbDf5WlKNCOjMLEZ1mAKCUckS06Ud/iqs10n9ExIjIJxEtA4Dq9/uLADAYDB6T/RrdiWgpiAEgm0wm3ylBwuRwqpPf7+LL8tUuElX+uMoYQ8aYnXmpWuuzlFGyb0ysqqozHA6f4ymoOU5Zlm3n3Pi/piLymgVARN6cc2NmltRLeJERkSsAsNauZV68TkQvAEBEnVSqMWYBQO2NbgBAlWXZttaOROSDmaWu6/MgyPP8q1mRmY/D2Vq7D0Q/rtfrrbRarfdGrQcAFRFtN7CutfY+nJOvEP+L1L38AuZduMnlP1yGAAAAAElFTkSuQmCC"


def load_icon_from_base64(base64_string):
    """从 base64 字符串加载图标"""
    try:
        icon_data = base64.b64decode(base64_string)

        # 创建临时文件保存图标数据
        import tempfile
        with tempfile.NamedTemporaryFile(delete=False, suffix='.ico') as tmp_file:
            tmp_file.write(icon_data)
            tmp_file_path = tmp_file.name

        # 加载图标
        icon = QIcon(tmp_file_path)
        return icon
    except Exception as e:
        print(f"加载图标失败:{e}")
        return QIcon()


def to_q3_5_format(num):
    """转换为 Q3.5 格式的 9 位补码"""
    if num >= 0:
        return num & 0x1FF
    else:
        return (512 + num) & 0x1FF


def calculate_rtc_compensation(ppm):
    """
    计算 RTC 补偿设定值
    公式:CR[8:0] = [(ppm × 2^15) / 10^6 × 32](Q3.5 补码) + 0x20
    """
    raw_value = round((ppm * (2 ** 15) / 1000000) * 32)
    
    if raw_value < 0:
        raw_value = (512 + raw_value) & 0x1FF
    else:
        raw_value &= 0x1FF
    
    return (raw_value + 0x20) & 0x1FF


def ppm_to_register(ppm):
    """将 ppm 转换为寄存器值 (二进制、十六进制、十进制)"""
    value = calculate_rtc_compensation(ppm)
    return format(value, '09b'), f"0x{value:02X}", value


def calculate_checksum(data):
    """计算累加和校验"""
    return sum(data) & 0xFF


def build_calibration_frame(compensation_value):
    """
    构建校准命令帧
    补偿值的每个字节加上 0x33,高位在前
    格式:68 99 99 99 99 99 99 68 20 05 33 XX XX XX XX CS 16
    其中 XX XX XX XX 为补偿值各字节加 0x33 后的结果

    例如:
    - 补偿值 0x6E -> (0x6E+0x33)=0xA1, 0x33, 0x33, 0x33 -> A1 33 33 33
    - 补偿值 0x100 -> (0x00+0x33)=0x33, (0x01+0x33)=0x34, 0x33, 0x33 -> 33 34 33 33
    - 补偿值 0x101 -> (0x01+0x33)=0x34, (0x01+0x33)=0x34, 0x33, 0x33 -> 34 34 33 33
    """
    # 将补偿值拆分为 4 个字节(低位到高位)
    bytes_list = []
    temp = compensation_value
    for i in range(4):
        byte_val = (temp >> (i * 8)) & 0xFF
        # 每个字节加上 0x33
        adjusted_byte = (byte_val + 0x33) & 0xFF
        bytes_list.append(adjusted_byte)
    
    # 构建帧
    header = bytes([0x68, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x68, 0x20, 0x05, 0x33])
    data_bytes = bytes(bytes_list)
    checksum = calculate_checksum(header + data_bytes)
    footer = bytes([0x16])
    
    frame = header + data_bytes + bytes([checksum]) + footer
    return frame


class UARTAssistant(QWidget):
    def __init__(self):
        super().__init__()
        self.serial_port = None
        self.recv_thread = None
        self.worker = None

        # 加载并设置窗口图标
        window_icon = load_icon_from_base64(ICON_BASE64)
        self.setWindowIcon(window_icon)

        # 固定命令
        # 固定命令
        self.CMD_INIT_CLOCK = bytes([0x68, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x68,
                                     0x20, 0x05, 0x36, 0x33, 0x33, 0x33, 0x33, 0x8D, 0x16])
        self.CMD_OPEN_PPS = bytes([0x68, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x68,
                                   0x20, 0x05, 0x34, 0x33, 0x33, 0x33, 0x33, 0x8B, 0x16])
        self.CMD_CLOSE_PPS = bytes([0x68, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x68,
                                    0x20, 0x05, 0x35, 0x33, 0x33, 0x33, 0x33, 0x8C, 0x16])

        self.init_ui()
        self.setup_signals()

    def init_ui(self):
        self.setWindowTitle('RTC 校准工具')
        self.setGeometry(100, 100, 700, 500)
        
        main_layout = QVBoxLayout()
        
        # 串口配置区域(简化)
        config_group = QGroupBox('串口配置')
        config_layout = QHBoxLayout()
        
        config_layout.addWidget(QLabel('端口:'))
        self.port_combo = QComboBox()
        self.refresh_ports()
        config_layout.addWidget(self.port_combo)
        
        self.refresh_btn = QPushButton('刷新')
        self.refresh_btn.setFixedWidth(60)
        config_layout.addWidget(self.refresh_btn)
        
        config_layout.addWidget(QLabel('  波特率:9600  8N1'))
        config_layout.addStretch()
        
        config_group.setLayout(config_layout)
        main_layout.addWidget(config_group)
        
        # 固定命令控制区域
        cmd_group = QGroupBox('秒脉冲控制')
        cmd_layout = QVBoxLayout()

        # 初始化时钟
        init_layout = QHBoxLayout()
        init_label = QLabel('初始化时钟:')
        init_label.setFixedWidth(80)
        init_layout.addWidget(init_label)

        self.init_cmd_text = QLineEdit()
        self.init_cmd_text.setText(self.bytes_to_hex(self.CMD_INIT_CLOCK))
        self.init_cmd_text.setReadOnly(True)
        self.init_cmd_text.setFont(QFont('Consolas', 10))
        init_layout.addWidget(self.init_cmd_text)

        self.init_btn = QPushButton('发送')
        self.init_btn.setFixedWidth(80)
        self.init_btn.setStyleSheet('background-color: #FF9800; color: white;')
        init_layout.addWidget(self.init_btn)

        cmd_layout.addLayout(init_layout)

        # 打开秒脉冲
        open_layout = QHBoxLayout()
        open_label = QLabel('打开秒脉冲:')
        open_label.setFixedWidth(80)
        open_layout.addWidget(open_label)
        
        self.open_cmd_text = QLineEdit()
        self.open_cmd_text.setText(self.bytes_to_hex(self.CMD_OPEN_PPS))
        self.open_cmd_text.setReadOnly(True)
        self.open_cmd_text.setFont(QFont('Consolas', 10))
        open_layout.addWidget(self.open_cmd_text)
        
        self.open_btn = QPushButton('发送')
        self.open_btn.setFixedWidth(80)
        self.open_btn.setStyleSheet('background-color: #4CAF50; color: white;')
        open_layout.addWidget(self.open_btn)
        
        cmd_layout.addLayout(open_layout)
        
        # 关闭秒脉冲
        close_layout = QHBoxLayout()
        close_label = QLabel('关闭秒脉冲:')
        close_label.setFixedWidth(80)
        close_layout.addWidget(close_label)
        
        self.close_cmd_text = QLineEdit()
        self.close_cmd_text.setText(self.bytes_to_hex(self.CMD_CLOSE_PPS))
        self.close_cmd_text.setReadOnly(True)
        self.close_cmd_text.setFont(QFont('Consolas', 10))
        close_layout.addWidget(self.close_cmd_text)
        
        self.close_btn = QPushButton('发送')
        self.close_btn.setFixedWidth(80)
        self.close_btn.setStyleSheet('background-color: #f44336; color: white;')
        close_layout.addWidget(self.close_btn)
        
        cmd_layout.addLayout(close_layout)
        cmd_group.setLayout(cmd_layout)
        main_layout.addWidget(cmd_group)
        
        # 日计时误差校准区域
        calibration_group = QGroupBox('日计时误差校准')
        calibration_layout = QHBoxLayout()
        
        calibration_label = QLabel('日计时误差 (s):')
        calibration_label.setFixedWidth(100)
        calibration_layout.addWidget(calibration_label)
        
        self.error_input = QLineEdit()
        self.error_input.setPlaceholderText('输入日计时误差,如 0.5 或 -0.3')
        self.error_input.setFont(QFont('Consolas', 10))
        calibration_layout.addWidget(self.error_input)
        
        self.calc_btn = QPushButton('计算并发送')
        self.calc_btn.setFixedWidth(100)
        self.calc_btn.setStyleSheet('background-color: #2196F3; color: white;')
        calibration_layout.addWidget(self.calc_btn)
        
        calibration_group.setLayout(calibration_layout)
        main_layout.addWidget(calibration_group)
        
        # 校准命令预览
        preview_group = QGroupBox('校准命令预览')
        preview_layout = QHBoxLayout()
        
        preview_label = QLabel('命令帧:')
        preview_label.setFixedWidth(60)
        preview_layout.addWidget(preview_label)
        
        self.preview_text = QLineEdit()
        self.preview_text.setReadOnly(True)
        self.preview_text.setFont(QFont('Consolas', 10))
        self.preview_text.setPlaceholderText('计算后的命令帧将显示在此处')
        preview_layout.addWidget(self.preview_text)
        
        preview_group.setLayout(preview_layout)
        main_layout.addWidget(preview_group)
        
        # 控制按钮区域
        control_layout = QHBoxLayout()
        
        self.open_close_btn = QPushButton('打开串口')
        self.open_close_btn.setFixedWidth(100)
        control_layout.addWidget(self.open_close_btn)
        
        self.clear_btn = QPushButton('清空接收')
        self.clear_btn.setFixedWidth(100)
        control_layout.addWidget(self.clear_btn)
        
        control_layout.addStretch()
        
        control_layout.addWidget(QLabel('HEX 显示:'))
        self.hex_recv_check = QCheckBox()
        control_layout.addWidget(self.hex_recv_check)
        
        main_layout.addLayout(control_layout)
        
        # 接收区域
        self.recv_text = QTextEdit()
        self.recv_text.setReadOnly(True)
        self.recv_text.setFont(QFont('Consolas', 10))
        main_layout.addWidget(QLabel('接收区:'))
        main_layout.addWidget(self.recv_text)
        
        # 状态栏
        self.status_label = QLabel('未连接')
        self.status_label.setStyleSheet('color: gray;')
        main_layout.addWidget(self.status_label)
        
        self.setLayout(main_layout)
        
    def setup_signals(self):
        self.refresh_btn.clicked.connect(self.refresh_ports)
        self.open_close_btn.clicked.connect(self.toggle_serial)
        self.clear_btn.clicked.connect(self.recv_text.clear)
        self.init_btn.clicked.connect(self.send_init_clock)
        self.open_btn.clicked.connect(self.send_open_pps)
        self.close_btn.clicked.connect(self.send_close_pps)
        self.calc_btn.clicked.connect(self.send_calibration)
        
    def bytes_to_hex(self, data):
        """将字节转换为十六进制字符串"""
        return ' '.join(f'{b:02X}' for b in data)
        
    def refresh_ports(self):
        """刷新串口列表"""
        self.port_combo.clear()
        ports = serial.tools.list_ports.comports()
        for port in ports:
            self.port_combo.addItem(port.device)
            
        if len(ports) == 0:
            if hasattr(self, 'status_label'):
                self.status_label.setText('未找到串口')
        else:
            if hasattr(self, 'status_label'):
                self.status_label.setText(f'找到 {len(ports)} 个串口')
            
    def toggle_serial(self):
        """打开/关闭串口"""
        if self.serial_port is None or not self.serial_port.is_open:
            try:
                self.serial_port = serial.Serial(
                    port=self.port_combo.currentText(),
                    baudrate=9600,
                    bytesize=serial.EIGHTBITS,
                    parity=serial.PARITY_NONE,
                    stopbits=serial.STOPBITS_ONE,
                    timeout=0.5
                )
                
                self.open_close_btn.setText('关闭串口')
                self.status_label.setText(f'已连接:{self.port_combo.currentText()} @ 9600 8N1')
                self.status_label.setStyleSheet('color: green;')
                
                # 启动接收线程
                self.recv_thread = QThread()
                self.worker = SerialWorker(self.serial_port)
                self.worker.moveToThread(self.recv_thread)
                self.worker.data_received.connect(self.display_data)
                self.recv_thread.started.connect(self.worker.read_data)
                self.recv_thread.start()
                
            except Exception as e:
                QMessageBox.critical(self, '错误', f'打开串口失败:{str(e)}')
                self.status_label.setText('打开失败')
                self.status_label.setStyleSheet('color: red;')
        else:
            self.close_serial()
            
    def close_serial(self):
        """关闭串口"""
        if self.recv_thread and self.recv_thread.isRunning():
            self.worker.stop()
            self.recv_thread.quit()
            self.recv_thread.wait()
        
        if self.serial_port and self.serial_port.is_open:
            self.serial_port.close()
        
        self.open_close_btn.setText('打开串口')
        self.status_label.setText('未连接')
        self.status_label.setStyleSheet('color: gray;')
        
    def send_open_pps(self):
        """发送打开秒脉冲命令"""
        if not self.serial_port or not self.serial_port.is_open:
            QMessageBox.warning(self, '警告', '串口未打开')
            return
        
        try:
            self.serial_port.write(self.CMD_OPEN_PPS)
            self.recv_text.append(f'[已发送] 打开秒脉冲:{self.bytes_to_hex(self.CMD_OPEN_PPS)}')
        except Exception as e:
            QMessageBox.critical(self, '错误', f'发送失败:{str(e)}')
            
    def send_close_pps(self):
        """发送关闭秒脉冲命令"""
        if not self.serial_port or not self.serial_port.is_open:
            QMessageBox.warning(self, '警告', '串口未打开')
            return
        
        try:
            self.serial_port.write(self.CMD_CLOSE_PPS)
            self.recv_text.append(f'[已发送] 关闭秒脉冲:{self.bytes_to_hex(self.CMD_CLOSE_PPS)}')
        except Exception as e:
            QMessageBox.critical(self, '错误', f'发送失败:{str(e)}')
    
    def send_calibration(self):
        """根据日计时误差计算并发送校准命令"""
        if not self.serial_port or not self.serial_port.is_open:
            QMessageBox.warning(self, '警告', '串口未打开')
            return
        
        try:
            error = float(self.error_input.text())
            
            # 计算 ppm:日计时误差/3600/24*1000000
            ppm = error / 3600 / 24 * 1000000
            
            # 计算补偿值
            _, _, comp_value = ppm_to_register(ppm)
            
            # 构建校准命令帧
            frame = build_calibration_frame(comp_value)
            
            # 发送
            self.serial_port.write(frame)
            self.recv_text.append(f'[已发送] 校准命令 (误差={error}s, ppm={ppm:.2f}, 补偿值=0x{comp_value:02X}): {self.bytes_to_hex(frame)}')
            
            # 更新预览
            self.preview_text.setText(self.bytes_to_hex(frame))
            
        except ValueError:
            QMessageBox.critical(self, '错误', '请输入有效的日计时误差值')
        except Exception as e:
            QMessageBox.critical(self, '错误', f'发送失败:{str(e)}')

    def send_init_clock(self):
        """发送初始化时钟命令"""
        if not self.serial_port or not self.serial_port.is_open:
            QMessageBox.warning(self, '警告', '串口未打开')
            return

        try:
            self.serial_port.write(self.CMD_INIT_CLOCK)
            self.recv_text.append(f'[已发送] 初始化时钟:{self.bytes_to_hex(self.CMD_INIT_CLOCK)}')
        except Exception as e:
            QMessageBox.critical(self, '错误', f'发送失败:{str(e)}')

    def display_data(self, data):
        """显示接收到的数据"""
        if self.hex_recv_check.isChecked():
            hex_data = ' '.join(f'{b:02X}' for b in data)
            self.recv_text.append(f'[接收] {hex_data}')
        else:
            try:
                text = data.decode('utf-8')
                self.recv_text.append(f'[接收] {text}')
            except:
                hex_data = ' '.join(f'{b:02X}' for b in data)
                self.recv_text.append(f'[接收] (非 UTF-8) {hex_data}')
        
        scrollbar = self.recv_text.verticalScrollBar()
        scrollbar.setValue(scrollbar.maximum())


class SerialWorker(QObject):
    """串口工作线程"""
    data_received = pyqtSignal(bytes)
    
    def __init__(self, serial_port):
        super().__init__()
        self.serial_port = serial_port
        self.running = True
        
    def read_data(self):
        """循环读取串口数据"""
        while self.running:
            if self.serial_port.in_waiting > 0:
                data = self.serial_port.read(self.serial_port.in_waiting)
                self.data_received.emit(data)
            QThread.msleep(10)
            
    def stop(self):
        """停止读取"""
        self.running = False


if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = UARTAssistant()
    window.show()
    sys.exit(app.exec_())

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐