Skip to content

Commit 7962b26

Browse files
committed
[#91106] tests: Add vivado_ibert eyescan test
1 parent ec7af06 commit 7962b26

File tree

5 files changed

+444
-0
lines changed

5 files changed

+444
-0
lines changed

protoplaster/tests/bert/vivado_ibert/__init__.py

Whitespace-only changes.
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
{% extends "base.html" %}
2+
3+
{% block content %}
4+
<html>
5+
<body>
6+
<h2>Eye diagram</h2>
7+
8+
<div>
9+
<canvas height="570px" width="680px" id="eye_diagram"></canvas>
10+
</div>
11+
<div class="stats">
12+
<div>Eye width:</div> <span id="width">0</span>
13+
<div>Eye height:</div> <span id="height">0</span>
14+
</div>
15+
</body>
16+
17+
<style>
18+
canvas {
19+
margin-top: 15px;
20+
}
21+
22+
.stats {
23+
display: grid;
24+
width: 15em;
25+
grid-template-columns: 1fr 1fr;
26+
margin-left: 3em
27+
}
28+
</style>
29+
30+
<script>
31+
const chartData = {{ samples|tojson }};
32+
33+
const ctx = document.getElementById("eye_diagram").getContext("2d");
34+
35+
// Configuration
36+
const xValues = [...new Set(chartData.map(({ x }) => x))].sort((a, b) => a - b);
37+
const yValues = [...new Set(chartData.map(({ y }) => y))].sort((a, b) => a - b);
38+
const stepSize = {
39+
x: xValues[1] - xValues[0],
40+
y: yValues[1] - yValues[0],
41+
};
42+
43+
const avgAmplitude = 0.27;
44+
const axisPadding = { left: 55, right: 120, top: 10, bottom: 35 };
45+
const gridSize = { x: xValues.length, y: yValues.length };
46+
const translateOrigin = { x: gridSize.x / 2, y: gridSize.y / 2 };
47+
const ticksCommon = {
48+
length: 9,
49+
padding: { grid: 1, text: 2 },
50+
step: 2,
51+
};
52+
const ticks = { x: { ...ticksCommon }, y: { ...ticksCommon } };
53+
54+
const cell = {
55+
width: Math.floor((ctx.canvas.width - axisPadding.left - axisPadding.right) / gridSize.x),
56+
height: Math.floor((ctx.canvas.height - axisPadding.top - axisPadding.bottom) / gridSize.y),
57+
};
58+
59+
// Styles
60+
ctx.strokeStyle = "black";
61+
ctx.font = "12px Arial";
62+
63+
// Y Axis label
64+
ctx.textBaseline = "top";
65+
ctx.rotate(-Math.PI / 2);
66+
ctx.fillText("Voltage", -ctx.canvas.height / 2, 10);
67+
ctx.rotate(Math.PI / 2);
68+
69+
// X Axis label
70+
ctx.textAlign = "center";
71+
ctx.textBaseline = "bottom";
72+
ctx.fillText("Unit Interval", axisPadding.left + cell.width * gridSize.x / 2, ctx.canvas.height - 10);
73+
74+
// Legend
75+
{
76+
const label = "Amplitude";
77+
const width = 30;
78+
const height = 120;
79+
const padding = {
80+
grid: 10,
81+
label: 10,
82+
};
83+
84+
const x = axisPadding.left + (gridSize.x + 1) * cell.width + padding.grid;
85+
const y = axisPadding.top;
86+
87+
// Border
88+
const borderWidth = 1;
89+
ctx.fillStyle = "black";
90+
ctx.fillRect(x - borderWidth, y - borderWidth, width + borderWidth * 2, height + borderWidth * 2);
91+
92+
// Gradient
93+
const gradient = ctx.createLinearGradient(0, y, 0, y + height);
94+
gradient.addColorStop(0, "black");
95+
gradient.addColorStop(0.5, "red");
96+
gradient.addColorStop(1, "blue");
97+
ctx.fillStyle = gradient;
98+
ctx.fillRect(x, y, width, height);
99+
100+
// Ticks
101+
const font = ctx.font; // save font
102+
ctx.font = "10px Arial";
103+
ctx.textAlign = "left";
104+
ctx.textBaseline = "middle";
105+
ctx.fillStyle = "black";
106+
const ticksLength = 6;
107+
const maxValue = chartData
108+
.flat(1)
109+
.flatMap((pixel) => pixel.amp)
110+
.reduce((a, b) => Math.max(a, b), -Infinity); // Math.max(...values) causes stack overflow
111+
const ticksValues = [0, avgAmplitude, maxValue];
112+
ticksValues.forEach((value) => {
113+
const xPos = x + width + borderWidth
114+
const yPos = y + height - value / maxValue * height;
115+
ctx.moveTo(xPos + 1, yPos);
116+
ctx.lineTo(xPos + 1 + ticksLength, yPos);
117+
ctx.fillText(value, xPos + ticksLength + 3, yPos + ctx.lineWidth / 4);
118+
});
119+
const maxTicksWidth = Math.max(...ticksValues.map((value) => ctx.measureText(value).width));
120+
ctx.font = font; // restore font
121+
122+
// Label
123+
ctx.textAlign = "center";
124+
ctx.textBaseline = "bottom";
125+
ctx.rotate(Math.PI / 2);
126+
ctx.fillText(label, axisPadding.top + height / 2, - (x + width + padding.label + maxTicksWidth));
127+
ctx.rotate(-Math.PI / 2);
128+
}
129+
130+
function updateEyeDiagram() {
131+
// Calculate eye width and height
132+
const minValue = Math.min(...chartData.map(({ amp }) => amp));
133+
const eyePixels = chartData.filter(({ amp }) => amp === minValue);
134+
const width = (Math.max(...eyePixels.map(({ x }) => x)) - Math.min(...eyePixels.map(({ x }) => x)));
135+
const height = (Math.max(...eyePixels.map(({ y }) => y)) - Math.min(...eyePixels.map(({ y }) => y)));
136+
document.getElementById("width").innerText = width;
137+
document.getElementById("height").innerText = height;
138+
139+
// Fill grid
140+
for (const pixel of chartData) {
141+
const xIndex = pixel.x / stepSize.x;
142+
const yIndex = pixel.y / stepSize.y;
143+
const xPos = (translateOrigin.x + xIndex) * cell.width + axisPadding.left;
144+
const yPos = (translateOrigin.y - yIndex - 1) * cell.height + axisPadding.top;
145+
146+
const fillStyle = pixel.amp >= 0 && pixel.amp <= avgAmplitude
147+
? `rgb(${(pixel.amp / avgAmplitude) * 255}, 0, ${(1 - pixel.amp / avgAmplitude) * 255})`
148+
: `rgb(${(1 - ((pixel.amp - avgAmplitude) / avgAmplitude)) * 255}, 0, 0)`;
149+
150+
ctx.fillStyle = fillStyle;
151+
ctx.fillRect(xPos, yPos, cell.width, cell.height);
152+
}
153+
154+
// X Axis ticks
155+
ctx.textAlign = "right";
156+
ctx.textBaseline = "middle";
157+
const xTicksBase = axisPadding.left - ticks.y.padding.grid - ticks.y.length;
158+
for (let i = 0; i <= gridSize.y; i += ticks.y.step) {
159+
const y = axisPadding.top + i * cell.height;
160+
ctx.moveTo(xTicksBase, y);
161+
ctx.lineTo(xTicksBase + ticks.y.length, y);
162+
ctx.stroke();
163+
164+
const text = ((translateOrigin.y - i) * stepSize.y).toString();
165+
ctx.fillText(text, xTicksBase - ticks.y.padding.text, y + ctx.lineWidth / 4);
166+
}
167+
168+
// Y Axis ticks
169+
ctx.textAlign = "center";
170+
ctx.textBaseline = "top";
171+
const yTicksBase = axisPadding.top + gridSize.y * cell.height + ticks.x.padding.grid + ticks.x.length;
172+
for (let i = 0; i <= gridSize.x; i = i += ticks.x.step) {
173+
const x = axisPadding.left + i * cell.width;
174+
ctx.moveTo(x, yTicksBase - ticks.x.length);
175+
ctx.lineTo(x, yTicksBase);
176+
ctx.stroke();
177+
const text = ((i - translateOrigin.x) * stepSize.x).toString();
178+
ctx.fillText(text, x, yTicksBase + ticks.x.padding.text);
179+
}
180+
}
181+
182+
window.addEventListener("DOMContentLoaded", updateEyeDiagram);
183+
184+
</script>
185+
186+
</html>
187+
{% endblock %}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import csv
2+
import os
3+
import shutil
4+
import tempfile
5+
import subprocess
6+
7+
from jinja2 import DictLoader, Environment
8+
9+
10+
class EyeScan:
11+
12+
def __init__(self, vivado_cmd: str, hw_server: str, serial_number: str,
13+
channel_path: str, prbs_bits: int) -> None:
14+
self.eyescan_file = tempfile.NamedTemporaryFile()
15+
self.hw_server = hw_server
16+
self.serial_number = serial_number
17+
self.channel_path = channel_path
18+
self.prbs_bits = prbs_bits
19+
20+
# Check if Vivado is available
21+
if shutil.which(vivado_cmd) is None:
22+
raise RuntimeError(
23+
f"{vivado_cmd} not found on PATH, or specified absolute path does not exist"
24+
)
25+
26+
# Perform the eye scan
27+
vivado_argv = [
28+
vivado_cmd,
29+
"-mode",
30+
"batch",
31+
"-nolog",
32+
"-nojournal",
33+
"-source",
34+
"ibert_eyescan.tcl",
35+
"-tclargs",
36+
self.eyescan_file.name,
37+
self.hw_server,
38+
self.serial_number,
39+
self.channel_path,
40+
str(self.prbs_bits),
41+
]
42+
res = subprocess.run(vivado_argv,
43+
cwd=os.path.dirname(__file__),
44+
capture_output=True,
45+
text=True)
46+
if res.returncode != 0:
47+
raise RuntimeError("Eye scan failed, Vivado stdout:\n" +
48+
res.stdout + "\nVivado stderr:\n" + res.stderr)
49+
50+
def parse_file(self) -> list[dict]:
51+
samples = []
52+
with open(self.get_eyescan_file_path()) as file:
53+
for line in file:
54+
if line.strip() == "Scan Start":
55+
break
56+
else:
57+
raise ValueError(
58+
"Scan data start marker not found in CSV file")
59+
60+
reader = csv.reader(file)
61+
xs = [int(x) for x in next(reader)[1:]]
62+
for row in reader:
63+
if row == ["Scan End"]:
64+
break
65+
y, *amps = int(row[0]), *map(float, row[1:])
66+
for x, amp in zip(xs, amps, strict=True):
67+
samples.append({"x": x, "y": y, "amp": amp})
68+
else:
69+
raise ValueError("Scan data end marker not found in CSV file")
70+
71+
return samples
72+
73+
def read_diagram_template(self) -> str:
74+
with open(f"{os.path.dirname(__file__)}/eye_diagram.html") as file:
75+
return file.read()
76+
77+
def read_base_template(self) -> str:
78+
with open(
79+
f"{os.path.dirname(__file__)}/../../../webui/templates/base.html"
80+
) as file:
81+
return file.read()
82+
83+
def render_template(self, html_template: str, **kwargs) -> str:
84+
base_template = self.read_base_template()
85+
86+
environment = Environment(loader=DictLoader({
87+
"template": html_template,
88+
"base.html": base_template,
89+
}))
90+
template = environment.get_template("template")
91+
return template.render(**kwargs)
92+
93+
def render_diagram(self) -> str:
94+
samples = self.parse_file()
95+
diagram_template = self.read_diagram_template()
96+
return self.render_template(diagram_template,
97+
samples=samples,
98+
disable_nav=True,
99+
num_bits=self.prbs_bits)
100+
101+
def get_eyescan_file_path(self) -> str:
102+
return self.eyescan_file.name
103+
104+
def get_eye_size(self, sample: list[dict]) -> tuple[int, int]:
105+
min_value = min(pixel["amp"] for pixel in sample)
106+
eye_pixels = [pixel for pixel in sample if pixel["amp"] != min_value]
107+
x_values = [pixel["x"] for pixel in eye_pixels]
108+
y_values = [pixel["y"] for pixel in eye_pixels]
109+
if len(x_values):
110+
width = max(x_values) - min(x_values)
111+
else:
112+
width = 0
113+
if len(y_values):
114+
height = max(y_values) - min(y_values)
115+
else:
116+
height = 0
117+
return (width, height)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
proc perform_eye_scan { outputPath hwServer serialNumber channelPath prbsBits } {
2+
# Connect to the emulator
3+
open_hw_manager
4+
connect_hw_server -url "$hwServer"
5+
open_hw_target "$hwServer/xilinx_tcf/Xilinx/$serialNumber"
6+
# Program and Refresh the device
7+
current_hw_device [lindex [get_hw_devices] 0]
8+
refresh_hw_device -update_hw_probes false [lindex [get_hw_devices] 0]
9+
set fullPath "$hwServer/xilinx_tcf/Xilinx/$serialNumber/$channelPath"
10+
set xil_newLinks [list]
11+
set xil_newLink [create_hw_sio_link -description {Link 0} [lindex [get_hw_sio_txs $fullPath/TX] 0] [lindex [get_hw_sio_rxs $fullPath/RX] 0] ]
12+
lappend xil_newLinks $xil_newLink
13+
set xil_newLinkGroup [create_hw_sio_linkgroup -description {Link Group 0} [get_hw_sio_links $xil_newLinks]]
14+
unset xil_newLinks
15+
# Set link to use PCS Loopback, and write to hardware
16+
set_property LOOPBACK {Far-End PCS} [get_hw_sio_links -of_objects [get_hw_sio_linkgroups {Link_Group_0}]]
17+
commit_hw_sio -non_blocking [get_hw_sio_links -of_objects [get_hw_sio_linkgroups {Link_Group_0}]]
18+
set_property RX_PATTERN "PRBS $prbsBits-bit" [get_hw_sio_links -of_objects [get_hw_sio_linkgroups {Link_Group_0}]]
19+
commit_hw_sio -non_blocking [get_hw_sio_links -of_objects [get_hw_sio_linkgroups {Link_Group_0}]]
20+
set_property TX_PATTERN "PRBS $prbsBits-bit" [get_hw_sio_links -of_objects [get_hw_sio_linkgroups {Link_Group_0}]]
21+
commit_hw_sio -non_blocking [get_hw_sio_links -of_objects [get_hw_sio_linkgroups {Link_Group_0}]]
22+
# Create, run, and save scan
23+
set xil_newScan [create_hw_sio_scan -description {Scan 2} 2d_full_eye [lindex [get_hw_sio_links $fullPath/TX->$fullPath/RX] 0 ]]
24+
run_hw_sio_scan [get_hw_sio_scans $xil_newScan]
25+
wait_on_hw_sio_scan $xil_newScan
26+
write_hw_sio_scan $outputPath $xil_newScan
27+
}
28+
29+
set requiredArgs 5
30+
if { $argc != $requiredArgs } {
31+
puts "Incorrect argument count, got $argc, expected $requiredArgs"
32+
exit 1
33+
}
34+
35+
set outputPath [lindex $argv 0]
36+
set hwServer [lindex $argv 1]
37+
set serialNumber [lindex $argv 2]
38+
set channelPath [lindex $argv 3]
39+
set prbsBits [lindex $argv 4]
40+
41+
perform_eye_scan $outputPath $serialNumber $channelPath $prbsBits

0 commit comments

Comments
 (0)