J-Link RTT日志增强:用Python脚本实现时间戳与文件轮转
1. 为什么需要增强J-Link RTT日志功能
嵌入式开发过程中,调试是最让人头疼的环节之一。我做过不少基于STM32的项目,发现J-Link的RTT(Real Time Transfer)功能确实是个好东西——它不需要额外的硬件接口,就能实现调试信息的实时输出。但用久了就会发现两个明显的痛点:一是日志没有时间戳,二是长时间运行后日志文件会变得巨大无比。
先说时间戳问题。上周调试一个传感器采集程序时,设备突然卡死,查看日志发现有一堆错误信息,但根本不知道这些错误是在什么时间点发生的。更糟的是,当多个任务并行输出日志时,没有时间戳的日志就像一锅乱炖,完全理不清事件发生的先后顺序。
文件大小问题更让人崩溃。有一次设备跑了三天三夜,生成的日志文件直接飙到2GB,用普通文本编辑器根本打不开。想分析问题?先等十分钟让文件加载完再说。更可怕的是,如果这时候程序崩溃,整个日志文件可能因为没及时保存而丢失。
2. 环境准备与基础配置
2.1 安装正确的库和驱动
这里有个大坑我踩过——Python有两个pylink库。一个是pylink,另一个是pylink-square。前者已经停止维护,我们要用的是后者。安装命令很简单:
pip install pylink-square但安装完库只是第一步。J-Link的DLL文件必须放在正确位置,否则会报"找不到JLinkARM.dll"的错误。根据我的经验,最稳妥的做法是把SEGGER安装目录下的这两个文件:
- JLink_x64.dll
- JLinkARM.dll
直接拷贝到你的Python脚本同级目录。我试过设置系统PATH环境变量,但在某些Windows版本上仍然会加载失败。
2.2 基础连接测试
在写复杂脚本前,建议先用简单代码测试RTT功能是否正常:
import pylink jlink = pylink.JLink() jlink.open() # 自动检测连接的J-Link jlink.set_tif(pylink.enums.JLinkInterfaces.SWD) # 根据实际选择JTAG/SWD jlink.connect('STM32F407VG') # 填写你的芯片型号 jlink.rtt_start() try: while True: data = jlink.rtt_read(0, 1024) # 读取0号上行通道 if data: print(bytes(data).decode('ascii', errors='ignore'), end='') except KeyboardInterrupt: jlink.rtt_stop() jlink.close()这段代码能跑通,说明基础环境没问题。注意connect()里的芯片型号一定要写对,我曾经因为写错型号(STM32F407VE写成STM32F407VG)折腾了一下午。
3. 实现时间戳功能
3.1 基本时间戳实现
原始RTT输出的日志就像没戴手表的人——永远不知道现在几点。给日志添加时间戳其实很简单,核心就是time.strftime()函数:
from datetime import datetime def add_timestamp(log): return f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')}] {log}"但直接这么用会有个隐藏问题:时间戳的获取时间与实际日志产生时间存在微小延迟。对于高频日志(比如每毫秒一条),这个延迟会导致时间戳不准。我的解决方案是获取时间戳后立即读取RTT:
timestamp = time.strftime("[%Y-%m-%d %H:%M:%S] ", time.localtime()) data = jlink.rtt_read(0, 1024) if data: log = timestamp + bytes(data).decode('ascii', errors='ignore')3.2 处理多行日志的特殊情况
实际使用中发现一个棘手问题:有些日志本身包含换行符。如果简单地在每行前加时间戳,会出现这种情况:
[2023-08-15 14:00:00] 第一行日志 第二行日志 # 缺少时间戳我的处理方法是先按换行符分割,再给每一行添加时间戳:
raw_data = bytes(data).decode('ascii', errors='ignore') logs = raw_data.split('\n') for log in logs[:-1]: # 最后一个是空字符串 if log: # 过滤空行 fp.write(f"{timestamp}{log}\n")注意这里要用logs[:-1],因为split()会在最后多生成一个空字符串。这也是为什么很多人的脚本会出现末尾多空行的问题。
4. 文件轮转机制实现
4.1 基于大小的轮转策略
200MB是我的经验值——足够大以避免频繁切换文件,又足够小能被大多数编辑器流畅打开。实现逻辑很简单:
MAX_SIZE = 200 * 1024 * 1024 # 200MB if os.path.getsize(current_file) > MAX_SIZE: close_current_file() current_file = create_new_file()但实际编码时要考虑文件正在被写入的状态。我推荐用os.stat()获取文件大小,比os.path.getsize()更可靠:
file_stat = os.stat(file_name) if file_stat.st_size > MAX_FILE_SIZE: fp.close() file_name = generate_new_filename() fp = open(file_name, 'a')4.2 文件名生成策略
好的文件名应该包含足够的信息量。我习惯用这种格式:
def generate_filename(): return time.strftime("RTT_%Y%m%d_%H%M%S_") + str(int(time.time()*1000)%1000) + ".log"其中%H%M%S是时分秒,后面的毫秒级时间戳可以避免1秒内创建多个文件时的命名冲突。曾经因为忽略毫秒部分,导致高频率日志时文件被覆盖。
5. 异常处理与稳定性增强
5.1 处理RTT通道切换问题
当固件中调用SEGGER_RTT_SetTerminal()时,Python端会收到一个特殊字符(通常是0xFF)。如果不处理,会导致解码错误。我的解决方案是:
try: decoded = data.decode('ascii') except UnicodeDecodeError: if len(data) > 1 and data[0] == 0xFF: decoded = bytes(data[1:]).decode('ascii', errors='ignore') else: decoded = ''5.2 自动重连机制
长时间运行中,J-Link可能会因为各种原因断开。我封装了一个带自动重连的读取函数:
def safe_rtt_read(jlink, retries=3): for _ in range(retries): try: return jlink.rtt_read(0, 1024) except pylink.errors.JLinkException: jlink.rtt_stop() time.sleep(0.5) jlink.rtt_start() return []这个机制帮我解决了不少半夜断连导致日志丢失的问题。重试次数建议设为3-5次,间隔0.5秒比较合适。
6. 完整代码实现
结合所有优化点,最终的脚本大概长这样:
import pylink import time import os from datetime import datetime MAX_FILE_SIZE = 200 * 1024 * 1024 # 200MB BUFFER_SIZE = 1024 class RTTLogger: def __init__(self, chip_name): self.jlink = pylink.JLink() self.jlink.open() self.jlink.set_tif(pylink.enums.JLinkInterfaces.SWD) self.jlink.connect(chip_name) self.jlink.rtt_start() self.current_file = self._new_file() def _new_file(self): filename = datetime.now().strftime("RTT_%Y%m%d_%H%M%S") + ".log" return open(filename, 'a', newline='\n') def _rotate_file(self): self.current_file.close() self.current_file = self._new_file() def run(self): try: while True: timestamp = datetime.now().strftime("[%Y-%m-%d %H:%M:%S.%f] ") data = self.jlink.rtt_read(0, BUFFER_SIZE) if not data: time.sleep(0.01) continue try: log = bytes(data).decode('ascii') except UnicodeDecodeError: if len(data) > 1 and data[0] == 0xFF: log = bytes(data[1:]).decode('ascii', errors='ignore') else: continue for line in log.split('\n')[:-1]: if line: self.current_file.write(f"{timestamp}{line}\n") if os.stat(self.current_file.name).st_size > MAX_FILE_SIZE: self._rotate_file() except KeyboardInterrupt: self.current_file.close() self.jlink.rtt_stop() self.jlink.close() if __name__ == "__main__": logger = RTTLogger("STM32F407VG") logger.run()这个版本已经处理了大多数常见问题。使用时只需修改芯片型号即可。我还习惯添加一个命令行参数解析,方便动态设置芯片型号和日志大小限制。
7. 高级技巧与优化建议
7.1 多通道日志分离
RTT支持多个上行通道(通常0通道用于普通日志,1通道用于错误日志)。可以扩展我们的脚本同时监听多个通道:
channels = {0: 'info', 1: 'error'} for channel, prefix in channels.items(): data = jlink.rtt_read(channel, BUFFER_SIZE) if data: log = f"[{prefix.upper()}] {decode_data(data)}"这样在分析日志时,可以快速过滤不同级别的信息。
7.2 日志压缩归档
对于需要长期保存的日志,建议添加自动压缩功能。这里有个简单的实现思路:
import gzip def compress_old_logs(): for file in os.listdir('.'): if file.endswith('.log') and not file.endswith('.gz'): with open(file, 'rb') as f_in: with gzip.open(f"{file}.gz", 'wb') as f_out: f_out.writelines(f_in) os.remove(file)可以设置一个定时任务,每天凌晨压缩旧的日志文件。注意要处理好文件占用问题,避免压缩正在写入的日志。
7.3 网络日志传输
对于远程设备,可以通过Socket将日志实时传输到服务器:
import socket class NetworkHandler: def __init__(self, host, port): self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect((host, port)) def send(self, message): try: self.sock.sendall(message.encode('utf-8')) except: self.reconnect() def reconnect(self): self.sock.close() self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect((host, port))这个功能特别适合现场设备调试,可以实时查看日志而不需要物理接触设备。
