提示,无有效眼图中心点数据,检查下哪里错了。如果发现问题,直接重头到位新生成一个软件吧,不要怕长。不要删除原有注释,并看看没注释的地方加一下注释。
# =====================================================
# 导入必要的库模块 - 详细解释每个模块的作用
# =====================================================
import numpy as np
"""
NumPy (Numerical Python) - 科学计算基础库
主要功能:
1. 提供高效的N维数组对象(ndarray)
2. 支持广播功能函数
3. 提供线性代数、傅里叶变换、随机数生成等功能
在本代码中主要用于:
- 数值计算和数组操作
- 科学计算支持
"""
import matplotlib.pyplot as plt
"""
Matplotlib - Python中最强大的绘图库
主要功能:
1. 创建静态、动态和交互式图表
2. 支持多种图表类型(线图、散点图、柱状图等)
3. 高度可定制化(颜色、线型、标签等)
在本代码中主要用于:
- 绘制眼图
- 可视化DDR校准数据
- 创建图表和图形界面
"""
import re
"""
re (Regular Expression) - 正则表达式模块
主要功能:
1. 文本搜索和模式匹配
2. 文本替换
3. 复杂字符串处理
在本代码中主要用于:
- 解析日志文件中的关键数据
- 提取VREF、偏移量、数据点等信息
- 处理复杂的文本匹配任务
"""
import datetime
"""
datetime - 日期和时间处理模块
主要功能:
1. 日期和时间的表示
2. 日期和时间的计算
3. 日期和时间的格式化
在本代码中主要用于:
- 生成时间戳
- 创建带时间戳的文件名
- 记录报告生成时间
"""
from matplotlib.lines import Line2D
"""
Line2D - 用于创建二维线条对象
主要功能:
1. 表示二维坐标系中的线条
2. 控制线条属性(颜色、线宽、样式等)
在本代码中主要用于:
- 创建图例元素
- 自定义图表中的线条样式
"""
import os
"""
os (Operating System) - 操作系统接口模块
主要功能:
1. 文件和目录操作
2. 路径操作
3. 进程管理
在本代码中主要用于:
- 文件路径处理
- 目录创建
- 文件存在性检查
"""
from collections import defaultdict
"""
defaultdict - 带默认值的字典
主要功能:
1. 当访问不存在的键时返回默认值
2. 避免KeyError异常
在本代码中主要用于:
- 存储电压-窗口映射关系
- 简化字典初始化操作
"""
# =====================================================
# 健壮的文件读取函数 - 详细解释每个编程概念
# =====================================================
def robust_read_file(file_path):
"""
健壮的文件读取函数,处理不同编码的文件
参数:
file_path - 文件在电脑上的完整路径(字符串)
编程概念详解:
1. 函数定义:def关键字用于定义函数,函数是一段可重复使用的代码块
2. 参数传递:file_path是形式参数,调用时传入实际文件路径
3. 异常处理:try-except结构用于捕获和处理运行时错误
4. 上下文管理器:with语句用于资源管理,确保文件正确关闭
5. 编码处理:不同文件可能使用不同编码(UTF-8, Latin-1等)
6. 正则表达式:用于过滤控制字符
"""
##########################################################
# try-except 异常处理结构
# try: 尝试执行可能出错的代码块
##########################################################
try:
##########################################################
# with 上下文管理器
# 语法:with expression [as variable]:
# 特点:自动管理资源(如文件),确保资源正确释放
# open() 函数:打开文件
# 'r':只读模式
# encoding='utf-8':指定UTF-8编码
##########################################################
with open(file_path, 'r', encoding='utf-8') as f:
# 文件对象方法:f.read()读取整个文件内容
return f.read()
##########################################################
# except 捕获特定异常
# UnicodeDecodeError:当文件编码不匹配时抛出
##########################################################
except UnicodeDecodeError:
try:
# 尝试使用Latin-1编码(支持所有256个字节值)
with open(file_path, 'r', encoding='latin-1') as f:
content = f.read()
##########################################################
# 正则表达式详解:r'[\x00-\x1F]+'
# 用途:匹配并删除控制字符(ASCII 0-31)
# 分解:
# r'':原始字符串,避免转义字符处理
# []:字符类,匹配括号内任意字符
# \x00-\x1F:十六进制范围,表示ASCII控制字符(0-31)
# +:量词,匹配1次或多次
# re.sub():替换匹配项
# 参数1:模式
# 参数2:替换内容(空字符串)
# 参数3:输入字符串
##########################################################
return re.sub(r'[\x00-\x1F]+', '', content)
##########################################################
# Exception 捕获所有异常
# 通用异常处理,打印错误信息
##########################################################
except Exception as e:
# 格式化字符串:f-string (Python 3.6+)
print(f"文件解码错误: {e}")
return None
# =====================================================
# 日志解析函数 - 重点讲解正则表达式
# =====================================================
def parse_log_file(log_content, normalization_point):
"""
解析DDR校准日志文件,提取关键数据
参数:
log_content - 日志文件的内容(字符串)
normalization_point - 归一化点(十六进制整数)
数据结构说明:
data = {
vref: {
dq_index: {
'read': (min, max, window),
'write': (min, max, window)
}
}
}
raw_data = {
vref: {
dq_index: {
'read': {'min': min_val, 'max': max_val},
'write': {'min': min_val, 'max': max_val}
}
}
}
"""
# 初始化数据结构
data = {} # 主数据结构,存储解析后的数据
current_vref = None # 当前处理的vref值
pending_data = {} # 临时存储待处理的数据(字典)
current_offset = None # 当前偏移量
raw_data = {} # 存储原始数据(偏移前)
# 按行处理日志内容
# 字符串方法:split('\n') 按换行符分割字符串
for line in log_content.split('\n'):
# 字符串方法:strip() 移除首尾空白字符
line = line.strip()
# 空行检查
if not line:
continue # 跳过空行
##########################################################
# 正则表达式1:匹配VREF行
# 模式:r'.*vref:\s*0x([0-9a-fA-F]+)'
# 目标示例: "Setting vref: 0x1A3"
#
# 详细分解:
# .* - 匹配任意字符(除换行符外)0次或多次(贪婪匹配)
# vref: - 匹配字面字符串 "vref:"
# \s* - 匹配0个或多个空白字符(空格、制表符等)
# 0x - 匹配字面字符串 "0x"
# ( - 开始捕获组
# [0-9a-fA-F] - 字符类,匹配十六进制字符(0-9, a-f, A-F)
# + - 匹配前面的元素1次或多次
# ) - 结束捕获组
#
# 匹配过程:
# "Setting vref: 0x1A3" -> 匹配整个字符串
# 捕获组1: "1A3"
##########################################################
vref_match = re.match(r'.*vref:\s*0x([0-9a-fA-F]+)', line)
if vref_match:
# 获取捕获组内容
hex_str = vref_match.group(1)
# int()函数:字符串转整数
# 参数1:字符串
# 参数2:基数(16表示十六进制)
current_vref = int(hex_str, 16)
# 字典初始化
data[current_vref] = {} # 嵌套字典初始化
raw_data[current_vref] = {}
pending_data = {} # 重置临时数据
current_offset = None
continue # 跳过后续处理
##########################################################
# 正则表达式2:匹配偏移量行
# 模式:r'.*0x38c:\s*(?:0x)?([0-9a-fA-F]+)'
# 目标示例: "Offset 0x38c: 0x25" 或 "0x38c: 25"
#
# 详细分解:
# .* - 匹配任意字符0次或多次
# 0x38c: - 匹配字面字符串 "0x38c:"
# \s* - 匹配0个或多个空白字符
# (?: - 开始非捕获组
# 0x - 匹配字面字符串 "0x"
# )? - 非捕获组出现0次或1次
# ( - 开始捕获组
# [0-9a-fA-F]+ - 匹配1个或多个十六进制字符
# ) - 结束捕获组
#
# 特殊说明:
# (?:...) 是非捕获组,匹配但不捕获内容
# 用于处理可选前缀而不创建额外捕获组
##########################################################
offset_match = re.match(r'.*0x38c:\s*(?:0x)?([0-9a-fA-F]+)', line)
if offset_match and current_vref is not None:
try:
hex_str = offset_match.group(1)
offset_value = int(hex_str, 16)
# 计算偏移量:归一化点 - 读取值
current_offset = normalization_point - offset_value
except ValueError:
# 异常处理:打印警告信息
print(f"警告: 无法解析偏移量: {offset_match.group(1)}")
current_offset = None
continue
##########################################################
# 正则表达式3:匹配最大值点
# 模式:r'.*dq(\d+)\s+max_(\w+)_point\s*:\s*(-?\d+)'
# 目标示例: "dq5 max_read_point: 120"
#
# 详细分解:
# .* - 匹配任意字符0次或多次
# dq - 匹配字面字符串 "dq"
# (\d+) - 捕获组1:匹配1个或多个数字(DQ索引)
# \s+ - 匹配1个或多个空白字符
# max_ - 匹配字面字符串 "max_"
# (\w+) - 捕获组2:匹配1个或多个单词字符(方向:read/write)
# _point - 匹配字面字符串 "_point"
# \s*:\s* - 匹配冒号前后任意空白
# (-?\d+) - 捕获组3:匹配可选负号后跟1个或多个数字
#
# 捕获组说明:
# 组1: DQ索引 (如 "5")
# 组2: 方向 (如 "read")
# 组3: 最大值 (如 "120")
##########################################################
max_match = re.match(r'.*dq(\d+)\s+max_(\w+)_point\s*:\s*(-?\d+)', line)
if max_match and current_vref is not None:
# 提取捕获组内容
dq_index = int(max_match.group(1)) # 转换为整数
direction = max_match.group(2) # 字符串
max_val = int(max_match.group(3)) # 转换为整数
# 字典操作:检查键是否存在并初始化
if current_vref not in raw_data:
# 字典设置默认值
raw_data[current_vref] = {}
if dq_index not in raw_data[current_vref]:
raw_data[current_vref][dq_index] = {}
if direction not in raw_data[current_vref][dq_index]:
# 嵌套字典初始化
raw_data[current_vref][dq_index][direction] = {'min': None, 'max': None}
# 存储原始值(不应用偏移)
raw_data[current_vref][dq_index][direction]['max'] = max_val
# 只有读方向应用偏移
if direction == 'read' and current_offset is not None:
# 应用偏移
max_val += current_offset
# 存储到临时数据字典
key = (dq_index, direction) # 元组作为字典键
pending_data[key] = {'max': max_val} # 字典值也是字典
continue
##########################################################
# 正则表达式4:匹配最小值点(结构类似最大值匹配)
# 模式:r'.*dq(\d+)\s+min_(\w+)_point\s*:\s*(-?\d+)'
# 目标示例: "dq5 min_read_point: 32"
##########################################################
min_match = re.match(r'.*dq(\d+)\s+min_(\w+)_point\s*:\s*(-?\d+)', line)
if min_match and current_vref is not None:
dq_index = int(min_match.group(1))
direction = min_match.group(2)
min_val = int(min_match.group(3))
key = (dq_index, direction)
# 存储原始值(类似最大值处理)
if current_vref not in raw_data:
raw_data[current_vref] = {}
if dq_index not in raw_data[current_vref]:
raw_data[current_vref][dq_index] = {}
if direction not in raw_data[current_vref][dq_index]:
raw_data[current_vref][dq_index][direction] = {'min': None, 'max': None}
raw_data[current_vref][dq_index][direction]['min'] = min_val
# 只有读方向应用偏移
if direction == 'read' and current_offset is not None:
min_val += current_offset
# 更新临时数据
if key in pending_data:
# 字典更新操作
pending_data[key]['min'] = min_val
continue
##########################################################
# 正则表达式5:匹配窗口行
# 模式:r'.*dq(\d+)\s+(\w+)_windows\s*:\s*(-?\d+)'
# 目标示例: "dq5 read_windows: 88"
#
# 详细分解:
# .* - 匹配任意字符0次或多次
# dq - 匹配字面字符串 "dq"
# (\d+) - 捕获组1:匹配1个或多个数字(DQ索引)
# \s+ - 匹配1个或多个空白字符
# (\w+) - 捕获组2:匹配1个或多个单词字符(方向)
# _windows - 匹配字面字符串 "_windows"
# \s*:\s* - 匹配冒号前后任意空白
# (-?\d+) - 捕获组3:匹配可选负号后跟1个或多个数字
##########################################################
win_match = re.match(r'.*dq(\d+)\s+(\w+)_windows\s*:\s*(-?\d+)', line)
if win_match and current_vref is not None:
dq_index = int(win_match.group(1))
direction = win_match.group(2)
windows = int(win_match.group(3))
key = (dq_index, direction)
# 检查是否已收集最小值和最大值
if key in pending_data and 'min' in pending_data[key] and 'max' in pending_data[key]:
min_val = pending_data[key]['min']
max_val = pending_data[key]['max']
# 确定最大延迟值(读0x7F=127,写0xFF=255)
max_delay = 0x7F if direction == 'read' else 0xFF
# 确保值在有效范围内
min_val = max(0, min_val) # 最小值不小于0
max_val = min(max_delay, max_val) # 最大值不超过最大延迟
# 检查数据有效性
if min_val > max_val or windows < 0:
result = None # 无效数据
else:
# 计算窗口大小
window_size = max_val - min_val + 1
result = (min_val, max_val, window_size)
# 存储到最终数据结构
if dq_index not in data[current_vref]:
# 初始化嵌套字典
data[current_vref][dq_index] = {}
data[current_vref][dq_index][direction] = result
# 从临时数据中移除
del pending_data[key] # 删除字典键
# 返回解析结果
return data, raw_data
# =====================================================
# 眼图指标计算函数 - 算法详解
# =====================================================
def calculate_eye_metrics(data, avddq, dq_index, direction):
"""
计算眼图的最大宽度和最大高度(含公共延迟位置)
返回:
max_eye_width: 最大眼宽 (UI)
max_eye_height: 最大眼高 (V)
best_vref: 最大眼宽对应的VREF
best_window: 最大眼宽对应的窗口(min, max, size)
eye_height_range: 最大眼高对应的电压范围(min_v, max_v)
common_delay: 在连续电压范围内都有效的公共延迟位置
"""
max_eye_width = 0.0
max_eye_height = 0.0
best_vref = None
best_window = None
eye_height_range = (0, 0)
common_delay = None
# 存储每个电压点的窗口信息
voltage_data = {}
# 遍历所有VREF值
for vref, dq_data in data.items():
# 计算实际电压
voltage = (vref / 0x1FF) * avddq
# 获取当前DQ和方向的数据
dq_info = dq_data.get(dq_index, {}).get(direction)
if dq_info is None:
continue
# 解包元组
min_point, max_point, window_size = dq_info
# 确定UI范围(读2UI,写4UI)
max_delay = 0x7F if direction == 'read' else 0xFF
ui_range = 2 if direction == 'read' else 4
# 计算窗口大小(UI单位)
window_ui = (window_size / max_delay) * ui_range
# 更新最大眼宽
if window_ui > max_eye_width:
max_eye_width = window_ui
best_vref = vref
best_window = (min_point, max_point, window_size)
# 存储电压点信息
voltage_data[voltage] = {
'vref': vref,
'min_delay': min_point,
'max_delay': max_point,
'window_ui': window_ui
}
# 1. 找到所有连续电压范围
sorted_voltages = sorted(voltage_data.keys())
voltage_ranges = []
if sorted_voltages:
current_start = sorted_voltages[0]
current_end = sorted_voltages[0]
for i in range(1, len(sorted_voltages)):
# 检查电压是否连续(考虑浮点精度)
if abs(sorted_voltages[i] - current_end) < 1e-6 or abs(sorted_voltages[i] - sorted_voltages[i-1]) < 1e-6:
current_end = sorted_voltages[i]
else:
# 保存当前连续范围
voltage_ranges.append((current_start, current_end))
current_start = sorted_voltages[i]
current_end = sorted_voltages[i]
# 添加最后一个范围
voltage_ranges.append((current_start, current_end))
# 2. 找出最大眼高的连续电压范围及其公共延迟位置
max_height = 0.0
best_range = (0, 0)
for v_start, v_end in voltage_ranges:
# 计算当前范围的高度
height = v_end - v_start
if height > max_height:
max_height = height
# 3. 找出该范围内的所有电压点
voltages_in_range = [v for v in sorted_voltages if v_start <= v <= v_end]
if voltages_in_range:
# 4. 找出所有电压点都有效的公共延迟位置
# 初始化公共延迟范围为第一个电压点的范围
common_min = voltage_data[voltages_in_range[0]]['min_delay']
common_max = voltage_data[voltages_in_range[0]]['max_delay']
# 遍历范围内的所有电压点,找到公共延迟范围
for v in voltages_in_range[1:]:
data_point = voltage_data[v]
# 更新公共最小延迟(取最大值)
common_min = max(common_min, data_point['min_delay'])
# 更新公共最大延迟(取最小值)
common_max = min(common_max, data_point['max_delay'])
# 检查是否存在公共延迟位置
if common_min <= common_max:
# 保存最大眼高范围及其公共延迟位置
max_eye_height = height
best_range = (v_start, v_end)
# 取公共延迟范围的中间值作为公共延迟位置
common_delay = (common_min + common_max) // 2
eye_height_range = best_range
return max_eye_width, max_eye_height, best_vref, best_window, eye_height_range, common_delay
def plot_eye_center(ax, direction, dq_index, eye_width, eye_height,
best_vref, best_window, eye_height_range, common_delay,
avddq, normalization_point, ui_range):
"""
基于最大眼宽和最大眼高精确计算眼图中心点
参数:
ax: 坐标轴对象
direction: 方向 ('write' 或 'read')
dq_index: DQ索引
eye_width: 最大眼宽 (UI单位)
eye_height: 最大眼高 (电压单位)
best_vref: 最大眼宽对应的VREF
best_window: 最大眼宽对应的窗口(min, max, size)
eye_height_range: 最大眼高对应的电压范围(min_v, max_v)
common_delay: 在连续电压范围内都有效的公共延迟位置
avddq: AVDDQ电压
normalization_point: 归一化点
ui_range: UI范围 (写为4,读为2)
"""
if not best_window or not eye_height_range or common_delay is None:
return None
# 1. 提取最大眼宽对应的数据
min_point, max_point, _ = best_window
max_delay = 0xFF if direction == 'write' else 0x7F
# 转换为UI单位
min_ui = (min_point / max_delay) * ui_range
max_ui = (max_point / max_delay) * ui_range
# 2. 最大眼宽对应的电压(精确值)
voltage_best = (best_vref / 0x1FF) * avddq
# 3. 提取最大眼高对应的电压范围
min_voltage, max_voltage = eye_height_range
# 4. 计算公共延迟位置(UI单位)
common_ui = (common_delay / max_delay) * ui_range
# 5. 计算原始延迟值(考虑0x38c偏移)
center_delay = int(common_delay)
# 6. 计算原始Vref值
center_vref = best_vref
# 7. 计算原始日志值(减去0x38c偏移)
base_offset = normalization_point - 0x38C
log_delay = center_delay - base_offset
# 8. 绘制眼宽横线(在最佳电压位置)
ax.hlines(
y=voltage_best,
xmin=min_ui,
xmax=max_ui,
colors='blue',
linestyles='-',
linewidth=1,
zorder=3
)
# 9. 绘制眼高竖线(在公共延迟位置)
ax.vlines(
x=common_ui,
ymin=min_voltage,
ymax=max_voltage,
colors='blue',
linestyles='-',
linewidth=1,
zorder=3
)
# 10. 计算交点(眼图中心)
center_x = common_ui
center_y = voltage_best
# 11. 标记中心点
ax.scatter(center_x, center_y, s=100, c='gold', marker='*', edgecolors='black', zorder=4)
# 12. 添加坐标信息标注
info_text = (f"眼图中心: ({center_x:.2f}UI, {center_y:.3f}V)\n"
f"原始值: Vref=0x{center_vref:03X}, Delay={log_delay}\n"
f"换算值: Vref=0x{center_vref:03X}, Delay={center_delay}")
ax.annotate(info_text,
xy=(center_x, center_y),
xytext=(center_x + 0.1, center_y + 0.05),
arrowprops=dict(arrowstyle='->', color='black'),
fontsize=8,
bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
return (center_vref, log_delay)
# =====================================================
# 眼图数据生成函数 - 详细解释算法
# =====================================================
def generate_eye_diagram(data, avddq, ui_ps, dq_index, direction):
"""
生成眼图数据点
参数:
data - 解析后的日志数据(字典)
avddq - AVDDQ电压值(浮点数)
ui_ps - 每个UI的时间(皮秒)
dq_index - DQ索引(0-15,整数)
direction - 方向('read'或'write',字符串)
算法说明:
1. 遍历所有VREF值
2. 计算实际电压 = (vref / 0x1FF) * avddq
3. 遍历所有可能的延迟值
4. 将延迟值转换为UI单位
5. 根据数据有效性标记为通过点或失败点
"""
pass_points = [] # 存储通过点(绿色)
fail_points = [] # 存储失败点(红色)
# 确定最大延迟值(读0x7F=127,写0xFF=255)
max_delay = 0x7F if direction == 'read' else 0xFF
# 确定UI范围(读2UI,写4UI)
ui_range = 2 if direction == 'read' else 4
# 遍历所有VREF值
for vref, dq_data in data.items():
# 计算实际电压
voltage = (vref / 0x1FF) * avddq
# 获取当前DQ和方向的数据
dq_info = dq_data.get(dq_index, {}).get(direction)
# 遍历所有可能的延迟值
for delay in range(0, max_delay + 1):
# 将延迟值转换为UI单位
ui_value = (delay / max_delay) * ui_range
# 如果没有有效数据,标记为失败点
if dq_info is None:
fail_points.append((ui_value, voltage))
else:
# 解包元组
min_point, max_point, _ = dq_info
# 检查当前延迟是否在有效范围内
if min_point <= delay <= max_point:
pass_points.append((ui_value, voltage))
else:
fail_points.append((ui_value, voltage))
return pass_points, fail_points
# =====================================================
# 输出原始数据到新日志 - 文件操作详解
# =====================================================
def export_raw_data(raw_data, normalization_point, log_path):
"""
输出原始数据到新日志文件(按DQ划分)
参数:
raw_data - 原始数据(偏移前)
normalization_point - 归一化点
log_path - 原始日志文件路径
文件操作详解:
1. 创建输出目录:os.makedirs()
2. 构建文件路径:os.path.join()
3. 写入文件:open()配合write()
4. 格式化输出:f-string
"""
# 获取当前时间戳
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
# 获取日志文件名(不含扩展名)
log_filename = os.path.basename(log_path)
if '.' in log_filename:
# rsplit() 从右边分割字符串,maxsplit=1表示只分割一次
log_name = log_filename.rsplit('.', 1)[0]
else:
log_name = log_filename
# 创建输出目录
log_dir = os.path.dirname(log_path) or os.getcwd() # 获取目录或当前工作目录
output_dir = os.path.join(log_dir, "raw_data_export") # 创建输出目录路径
##########################################################
# os.makedirs() 创建目录(如果不存在)
# exist_ok=True 表示目录已存在时不报错
##########################################################
os.makedirs(output_dir, exist_ok=True)
# 创建输出文件路径
output_file = os.path.join(output_dir, f"{log_name}_raw_data_{timestamp}.txt")
# 写入原始数据
with open(output_file, 'w', encoding='utf-8') as f:
# 写入标题信息
f.write("=" * 80 + "\n")
f.write(f"DDR校准原始数据报告 (归一化点: 0x{normalization_point:X})\n")
f.write(f"生成时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write(f"原始日志: {log_path}\n")
f.write("=" * 80 + "\n\n")
# 按vref排序
sorted_vrefs = sorted(raw_data.keys())
for vref in sorted_vrefs:
# 写入vref标题
f.write(f"VREF: 0x{vref:03X}\n") # :03X表示3位十六进制大写,不足补0
f.write("-" * 60 + "\n")
# 按DQ索引排序
sorted_dq = sorted(raw_data[vref].keys())
for dq_index in sorted_dq:
# 写入DQ标题
f.write(f" DQ{dq_index}:\n")
# 处理读方向数据
if 'read' in raw_data[vref][dq_index]:
rd = raw_data[vref][dq_index]['read']
f.write(f" 读方向:\n")
f.write(f" 原始最小值: {rd['min']}\n")
f.write(f" 原始最大值: {rd['max']}\n")
# 计算并写入窗口大小
window_size = rd['max'] - rd['min'] + 1
f.write(f" 窗口大小: {window_size}\n")
# 处理写方向数据
if 'write' in raw_data[vref][dq_index]:
wr = raw_data[vref][dq_index]['write']
f.write(f" 写方向:\n")
f.write(f" 原始最小值: {wr['min']}\n")
f.write(f" 原始最大值: {wr['max']}\n")
# 计算并写入窗口大小
window_size = wr['max'] - wr['min'] + 1
f.write(f" 窗口大小: {window_size}\n")
f.write("\n") # DQ间空行
f.write("\n") # VREF间空行
print(f"原始数据已导出至: {output_file}")
return output_file
# =====================================================
# 眼图绘制函数 - 数据可视化详解
# =====================================================
def plot_eye_diagrams(log_content, data_rate, avddq, log_path, normalization_point):
"""
绘制DDR眼图(修复版)
修复问题:
1. 数据解析一致性:确保窗口大小计算一致
2. 眼图中心点:基于最大眼宽和最大眼高计算
3. 保留原有的最大眼宽/眼高标注
4. 文件名添加时间戳
"""
# 设置中文字体支持
plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'Microsoft YaHei', 'WenQuanYi Micro Hei']
plt.rcParams['axes.unicode_minus'] = False
# 计算UI时间(皮秒)
ui_ps = (1 / (data_rate * 1e6)) * 1e12
# 解析日志文件
data, raw_data = parse_log_file(log_content, normalization_point)
# 导出原始数据到新日志
raw_data_file = export_raw_data(raw_data, normalization_point, log_path)
# 检查是否解析到有效数据
if not data:
print("错误: 无法从日志中解析出有效数据")
return None, None, None
# 创建图表对象
fig_write, axes_write = plt.subplots(4, 4, figsize=(22, 24))
fig_read, axes_read = plt.subplots(4, 4, figsize=(22, 24))
# 获取带时间戳的文件名
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
log_dir = os.path.dirname(log_path) or os.getcwd()
log_filename = os.path.basename(log_path)
log_name = log_filename.rsplit('.', 1)[0] if '.' in log_filename else log_filename
# 设置标题(包含原始数据文件路径)
norm_title = f" (Normalized to 0x{normalization_point:X}, Raw Data: {os.path.basename(raw_data_file)})"
fig_write.suptitle(f'DDR Write Eye Diagram (Data Rate: {data_rate} Mbps, UI: {ui_ps:.2f} ps){norm_title}', fontsize=18)
fig_read.suptitle(f'DDR Read Eye Diagram (Data Rate: {data_rate} Mbps, UI: {ui_ps:.2f} ps){norm_title}', fontsize=18)
# 展平坐标轴数组
axes_write = axes_write.flatten()
axes_read = axes_read.flatten()
# 创建图例元素
legend_elements = [
Line2D([0], [0], marker='o', color='w', label='Pass', markerfacecolor='green', markersize=10),
Line2D([0], [0], marker='o', color='w', label='Fail', markerfacecolor='red', markersize=10),
Line2D([0], [0], color='blue', linestyle='-', label='眼宽眼高连线', linewidth=1),
Line2D([0], [0], marker='*', color='w', label='眼图中心', markerfacecolor='gold', markersize=10)
]
# 存储所有中心点信息
all_center_points_write = []
all_center_points_read = []
# 遍历16个DQ通道
for dq_index in range(16):
# 计算写眼图指标(现在返回6个值)
write_width, write_height, best_vref_write, best_window_write, eye_height_range_write, common_delay_write = calculate_eye_metrics(
data, avddq, dq_index, 'write'
)
write_pass, write_fail = generate_eye_diagram(data, avddq, ui_ps, dq_index, 'write')
# 计算读眼图指标(现在返回6个值)
read_width, read_height, best_vref_read, best_window_read, eye_height_range_read, common_delay_read = calculate_eye_metrics(
data, avddq, dq_index, 'read'
)
read_pass, read_fail = generate_eye_diagram(data, avddq, ui_ps, dq_index, 'read')
# 绘制写眼图
if write_fail:
x_fail, y_fail = zip(*write_fail)
axes_write[dq_index].scatter(x_fail, y_fail, s=1, c='red', alpha=0.1, zorder=1)
if write_pass:
x_pass, y_pass = zip(*write_pass)
axes_write[dq_index].scatter(x_pass, y_pass, s=1, c='green', alpha=0.5, zorder=2)
# 绘制读眼图
if read_fail:
x_fail, y_fail = zip(*read_fail)
axes_read[dq_index].scatter(x_fail, y_fail, s=1, c='red', alpha=0.1, zorder=1)
if read_pass:
x_pass, y_pass = zip(*read_pass)
axes_read[dq_index].scatter(x_pass, y_pass, s=1, c='green', alpha=0.5, zorder=2)
# 添加原有的眼宽眼高标注
write_text = f"Max Eye Width: {write_width:.3f} UI\nMax Eye Height: {write_height:.3f} V"
axes_write[dq_index].annotate(
write_text,
xy=(0.98, 0.02),
xycoords='axes fraction',
fontsize=9,
ha='right',
va='bottom',
bbox=dict(boxstyle='round', facecolor='white', alpha=0.8)
)
read_text = f"Max Eye Width: {read_width:.3f} UI\nMax Eye Height: {read_height:.3f} V"
axes_read[dq_index].annotate(
read_text,
xy=(0.98, 0.02),
xycoords='axes fraction',
fontsize=9,
ha='right',
va='bottom',
bbox=dict(boxstyle='round', facecolor='white', alpha=0.8)
)
# === 任务一:基于最大眼宽和最大眼高的眼图中心点计算 ===
# 使用修正后的plot_eye_center函数
center_point = plot_eye_center(
axes_write[dq_index],
'write',
dq_index,
write_width,
write_height,
best_vref_write,
best_window_write,
eye_height_range_write,
common_delay_write, # 新增:公共延迟位置
avddq,
normalization_point,
ui_range=4
)
if center_point:
all_center_points_write.append(center_point)
# 处理读方向眼图中心
center_point = plot_eye_center(
axes_read[dq_index],
'read',
dq_index,
read_width,
read_height,
best_vref_read,
best_window_read,
eye_height_range_read,
common_delay_read, # 新增:公共延迟位置
avddq,
normalization_point,
ui_range=2
)
if center_point:
all_center_points_read.append(center_point)
# 设置公共轴属性
for ax, direction in [(axes_write[dq_index], 'Write'), (axes_read[dq_index], 'Read')]:
ax.set_title(f'DQ{dq_index} {direction} Eye', fontsize=12)
ax.set_xlabel('Delay (UI)', fontsize=10)
ax.set_ylabel('Voltage (V)', fontsize=10)
ax.set_xlim(0, 4 if direction == 'Write' else 2)
ax.set_ylim(0, avddq)
ax.grid(True, linestyle='--', alpha=0.6)
ax.legend(handles=legend_elements, loc='upper right', fontsize=9)
ax.tick_params(axis='both', which='major', labelsize=9)
# 计算统计信息
def calculate_stats(center_points, direction):
"""计算并返回统计信息字符串"""
if not center_points:
return f"{direction}方向: 无有效眼图中心点数据"
# 分离Vref和Delay值
vrefs = [cp[0] for cp in center_points]
delays = [cp[1] for cp in center_points]
# 计算平均值
avg_vref = sum(vrefs) / len(vrefs)
avg_delay = sum(delays) / len(delays)
# 计算中位数
sorted_vrefs = sorted(vrefs)
sorted_delays = sorted(delays)
median_vref = sorted_vrefs[len(sorted_vrefs) // 2]
median_delay = sorted_delays[len(sorted_delays) // 2]
# 格式化统计信息
stats = (f"{direction}方向统计 (共{len(center_points)}个DQ):\n"
f"平均Vref: 0x{int(avg_vref):03X} (十六进制), "
f"平均Delay: {avg_delay:.1f} (十进制)\n"
f"中位数Vref: 0x{int(median_vref):03X} (十六进制), "
f"中位数Delay: {median_delay:.1f} (十进制)")
return stats
# 计算统计信息
write_stats = calculate_stats(all_center_points_write, "写")
read_stats = calculate_stats(all_center_points_read, "读")
# 添加统计信息到图表底部
fig_write.text(0.5, 0.01, write_stats,
ha='center', va='bottom', fontsize=12,
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8))
fig_read.text(0.5, 0.01, read_stats,
ha='center', va='bottom', fontsize=12,
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8))
# 调整布局
fig_write.tight_layout(rect=[0, 0.03, 1, 0.97])
fig_read.tight_layout(rect=[0, 0.03, 1, 0.97])
# 构建带时间戳的输出文件路径
write_filename = os.path.join(log_dir, f"{log_name}_{timestamp}_ddr_write_eye.png")
read_filename = os.path.join(log_dir, f"{log_name}_{timestamp}_ddr_read_eye.png")
# 保存图像
fig_write.savefig(write_filename, dpi=300, bbox_inches='tight')
fig_read.savefig(read_filename, dpi=300, bbox_inches='tight')
# 关闭图像释放内存
plt.close(fig_write)
plt.close(fig_read)
# 打印结果
print(f"写眼图已保存至: {write_filename}")
print(f"读眼图已保存至: {read_filename}")
return write_filename, read_filename, raw_data_file
# =====================================================
# 主函数 - 程序入口点详解
# =====================================================
def main():
"""
主函数,程序入口点
功能:
1. 获取用户输入
2. 读取日志文件
3. 解析数据
4. 生成眼图
5. 导出结果
用户交互详解:
1. 使用input()获取用户输入
2. 使用循环处理无效输入
3. 使用try-except捕获异常
"""
# 打印欢迎信息
print("=" * 50)
print("DDR眼图生成器(带原始数据导出)")
print("=" * 50)
# 用户输入DataRate(带异常处理)
while True:
try:
data_rate = float(input("请输入DataRate (Mbps/Pin): "))
break
except ValueError:
print("错误: 请输入有效的数字")
# 用户输入AVDDQ电压(带异常处理)
while True:
try:
avddq = float(input("请输入AVDDQ电压值 (V): "))
break
except ValueError:
print("错误: 请输入有效的数字")
# 归一化点输入处理(带错误检查)
while True:
norm_input = input("请输入归一化点(十六进制值,如0x40或40): ").strip()
if not norm_input:
print("错误: 输入不能为空,请重新输入")
continue
try:
# 处理十六进制前缀
if norm_input.startswith(("0x", "0X")):
hex_str = norm_input[2:]
else:
hex_str = norm_input
# 字符串转整数(16进制)
normalization_point = int(hex_str, 16)
break
except ValueError:
print(f"错误: '{norm_input}' 不是有效的十六进制数,请重新输入")
# 日志文件路径输入(带文件存在检查)
while True:
log_path = input("请输入日志文件路径: ").strip()
# 检查文件是否存在
# os.path.exists() 判断路径是否存在
if not os.path.exists(log_path):
print(f"错误: 文件 '{log_path}' 不存在,请重新输入")
else:
# 获取绝对路径
log_path = os.path.abspath(log_path)
break
# 读取文件内容
log_content = robust_read_file(log_path)
if log_content is None:
print("无法读取日志文件")
return
# 尝试生成眼图(带异常处理)
try:
# 调用眼图生成函数(返回三个值)
write_file, read_file, raw_data_file = plot_eye_diagrams(
log_content, data_rate, avddq, log_path, normalization_point
)
print("\n眼图生成成功!")
print(f"原始数据文件: {raw_data_file}")
except Exception as e:
# 捕获所有异常并打印错误信息
print(f"眼图生成失败: {e}")
# 异常对象:e.args 获取异常参数
print(f"错误详情: {e.args}")
# =====================================================
# Python特殊检查 - 模块执行控制
# =====================================================
if __name__ == "__main__":
"""
__name__ 是Python的内置变量
当脚本直接运行时,__name__ 等于 "__main__"
当脚本被导入时,__name__ 等于模块名
这种结构允许:
1. 直接运行脚本时执行测试代码
2. 作为模块导入时不执行测试代码
"""
main() # 调用主函数