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

开源如下
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_())
更多推荐



所有评论(0)