Skip to content

Commit 1d7b95c

Browse files
committed
tests: Add ti_dac38j8x_eyescan test
Signed-off-by: Kamil Rakoczy <krakoczy@antmicro.com>
1 parent 785cc40 commit 1d7b95c

File tree

6 files changed

+398
-0
lines changed

6 files changed

+398
-0
lines changed

protoplaster/conf/parser.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from protoplaster.tests.adc.LTC2493.test import __file__ as LTC2493_test
1616
from protoplaster.tests.adc.LTC2499.test import __file__ as LTC2499_test
1717
from protoplaster.tests.dac.dac.test import __file__ as dac_test
18+
from protoplaster.tests.dac.ti_dac38j8x_eyescan.test import __file__ as ti_dac38j8x_eyescan_test
1819
from protoplaster.tests.memtester.test import __file__ as mem_test
1920
from protoplaster.tests.dac.LTC2655.test import __file__ as LTC2655_test
2021
from protoplaster.tests.dac.LTC2657.test import __file__ as LTC2657_test
@@ -47,6 +48,7 @@
4748
"UCD90320U": UCD90320U_test,
4849
"TCA9548A": TCA9548A_test,
4950
"simple": simple_test,
51+
"ti_dac38j8x_eyescan": ti_dac38j8x_eyescan_test,
5052
}
5153

5254

protoplaster/tests/dac/ti_dac38j8x_eyescan/__init__.py

Whitespace-only changes.
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
{% block content %}
2+
<html>
3+
<body>
4+
<h2>Eye diagram</h2>
5+
6+
<div class="opts">
7+
<div>Lane</div>
8+
<div>Bit Number</div>
9+
<select onchange="updateEyeDiagram()" name="lanes" id="lanes"></select>
10+
<select onchange="updateEyeDiagram()" name="bits" id="bits">
11+
<option>Average All Bits</option>
12+
</select>
13+
</div>
14+
15+
<div>
16+
<canvas height="570px" width="680px" id="eye_diagram"></canvas>
17+
</div>
18+
<div class="stats">
19+
<div>Eye width:</div> <span id="width">0</span>
20+
<div>Eye height:</div> <span id="height">0</span>
21+
</div>
22+
</body>
23+
24+
<style>
25+
canvas {
26+
margin-top: 15px;
27+
}
28+
29+
.opts {
30+
display: grid;
31+
width: 10em;
32+
grid-template-columns: 1fr 1fr;
33+
gap: 0.3em;
34+
}
35+
36+
.stats {
37+
display: grid;
38+
width: 15em;
39+
grid-template-columns: 1fr 1fr;
40+
margin-left: 3em
41+
}
42+
</style>
43+
44+
<script>
45+
const chartData = {{ samples|tojson }};
46+
47+
const ctx = document.getElementById("eye_diagram").getContext("2d");
48+
49+
// Configuration
50+
const nLanes = 8;
51+
const nBits = 20;
52+
const avgAmplitude = 2048;
53+
const axisPadding = { left: 55, right: 90, top: 10, bottom: 35 };
54+
const gridSize = { x: 32, y: 64 };
55+
const translateOrigin = { x: gridSize.x / 2, y: gridSize.y / 2 };
56+
const ticksCommon = {
57+
length: 9,
58+
padding: { grid: 1, text: 2 },
59+
step: 2,
60+
};
61+
const ticks = { x: { ...ticksCommon }, y: { ...ticksCommon } };
62+
const axisMultiplier = {{ axis_multiplier|tojson }};
63+
64+
const cell = {
65+
width: Math.floor((ctx.canvas.width - axisPadding.left - axisPadding.right) / gridSize.x),
66+
height: Math.floor((ctx.canvas.height - axisPadding.top - axisPadding.bottom) / gridSize.y),
67+
};
68+
69+
// Styles
70+
ctx.strokeStyle = "black";
71+
ctx.font = "12px Arial";
72+
73+
// Y Axis label
74+
ctx.textBaseline = "top";
75+
ctx.rotate(-Math.PI / 2);
76+
ctx.fillText("Voltage Offset (mV)", - ctx.canvas.width / 2, 10);
77+
ctx.rotate(Math.PI / 2);
78+
79+
// X Axis label
80+
ctx.textAlign = "center";
81+
ctx.textBaseline = "bottom";
82+
ctx.fillText("Phase offset (1/32 UI)", axisPadding.left + cell.width * gridSize.x / 2, ctx.canvas.height - 10);
83+
84+
// Legend
85+
{
86+
const label = "Amplitude";
87+
const width = 30;
88+
const height = 120;
89+
const padding = {
90+
grid: 10,
91+
label: 15,
92+
};
93+
94+
const x = axisPadding.left + (gridSize.x + 1) * cell.width + padding.grid;
95+
const y = axisPadding.top;
96+
97+
// Border
98+
const borderWidth = 1;
99+
ctx.fillStyle = "black";
100+
ctx.fillRect(x - borderWidth, y - borderWidth, width + borderWidth * 2, height + borderWidth * 2);
101+
102+
// Gradient
103+
const gradient = ctx.createLinearGradient(0, y, 0, y + height);
104+
gradient.addColorStop(0, "black");
105+
gradient.addColorStop(0.5, "red");
106+
gradient.addColorStop(1, "blue");
107+
ctx.fillStyle = gradient;
108+
ctx.fillRect(x, y, width, height);
109+
110+
// Ticks
111+
const font = ctx.font; // save font
112+
ctx.font = "10px Arial";
113+
ctx.textAlign = "left";
114+
ctx.textBaseline = "middle";
115+
ctx.fillStyle = "black";
116+
const ticksLength = 6;
117+
const maxValue = chartData
118+
.flat(1)
119+
.flatMap((pixel) => pixel.amp)
120+
.reduce((a, b) => Math.max(a, b), -Infinity); // Math.max(...values) causes stack overflow
121+
const ticksValues = [0, avgAmplitude, maxValue];
122+
ticksValues.forEach((value) => {
123+
const xPos = x + width + borderWidth
124+
const yPos = y + height - value / maxValue * height;
125+
ctx.moveTo(xPos + 1, yPos);
126+
ctx.lineTo(xPos + 1 + ticksLength, yPos);
127+
ctx.fillText(value, xPos + ticksLength + 3, yPos + ctx.lineWidth / 4);
128+
});
129+
const maxTicksWidth = Math.max(...ticksValues.map((value) => ctx.measureText(value).width));
130+
ctx.font = font; // restore font
131+
132+
// Label
133+
ctx.textAlign = "center";
134+
ctx.textBaseline = "bottom";
135+
ctx.rotate(Math.PI / 2);
136+
ctx.fillText(label, axisPadding.top + height / 2, - (x + width + padding.label + maxTicksWidth));
137+
ctx.rotate(-Math.PI / 2);
138+
}
139+
140+
function updateEyeDiagram() {
141+
const laneNumber = document.getElementById("lanes").selectedIndex;
142+
const bitNumber = document.getElementById("bits").selectedIndex - 1;
143+
144+
// Calculate current amplitude grid
145+
const gridData = chartData[laneNumber]
146+
.map((pixel) => {
147+
const amp = bitNumber == -1
148+
? pixel.amp.reduce((a, b) => a + b, 0) / pixel.amp.length
149+
: pixel.amp[bitNumber];
150+
return { ...pixel, amp };
151+
});
152+
153+
// Calculate width and height
154+
const maxValue = Math.max(...gridData.map(({ amp }) => amp));
155+
const eyePixels = gridData.filter(({ amp }) => amp !== maxValue);
156+
const width = (Math.max(...eyePixels.map(({ x }) => x)) - Math.min(...eyePixels.map(({ x }) => x))) * axisMultiplier.x;
157+
const height = (Math.max(...eyePixels.map(({ y }) => y)) - Math.min(...eyePixels.map(({ y }) => y))) * axisMultiplier.y;
158+
document.getElementById("width").innerText = width;
159+
document.getElementById("height").innerText = height;
160+
161+
// Fill grid
162+
for (const pixel of gridData) {
163+
const xPos = (translateOrigin.x + pixel.x) * cell.width + axisPadding.left;
164+
const yPos = (translateOrigin.y - pixel.y - 1) * cell.height + axisPadding.top;
165+
166+
const fillStyle = pixel.amp >= 0 && pixel.amp <= avgAmplitude
167+
? `rgb(${(pixel.amp / avgAmplitude) * 255}, 0, ${(1 - pixel.amp / avgAmplitude) * 255})`
168+
: `rgb(${(1 - ((pixel.amp - avgAmplitude) / avgAmplitude)) * 255}, 0, 0)`;
169+
170+
ctx.fillStyle = fillStyle;
171+
ctx.fillRect(xPos, yPos, cell.width, cell.height);
172+
}
173+
174+
// X Axis ticks
175+
ctx.textAlign = "right";
176+
ctx.textBaseline = "middle";
177+
const xTicksBase = axisPadding.left - ticks.y.padding.grid - ticks.y.length;
178+
for (let i = 0; i <= gridSize.y; i += ticks.y.step) {
179+
const y = axisPadding.top + i * cell.height;
180+
ctx.moveTo(xTicksBase, y);
181+
ctx.lineTo(xTicksBase + ticks.y.length, y);
182+
ctx.stroke();
183+
184+
const text = ((translateOrigin.y - i) * axisMultiplier.y).toString();
185+
ctx.fillText(text, xTicksBase - ticks.y.padding.text, y + ctx.lineWidth / 4);
186+
}
187+
188+
// Y Axis ticks
189+
ctx.textAlign = "center";
190+
ctx.textBaseline = "top";
191+
const yTicksBase = axisPadding.top + gridSize.y * cell.height + ticks.x.padding.grid + ticks.x.length;
192+
for (let i = 0; i <= gridSize.x; i = i += ticks.x.step) {
193+
const x = axisPadding.left + i * cell.width;
194+
ctx.moveTo(x, yTicksBase - ticks.x.length);
195+
ctx.lineTo(x, yTicksBase);
196+
ctx.stroke();
197+
const text = ((i - translateOrigin.x) * axisMultiplier.x).toString();
198+
ctx.fillText(text, x, yTicksBase + ticks.x.padding.text);
199+
}
200+
}
201+
202+
window.addEventListener("DOMContentLoaded", () => {
203+
const selectLanes = document.getElementById("lanes");
204+
for (var i = 0; i < nLanes; i++) {
205+
const option = document.createElement("option");
206+
option.text = i;
207+
selectLanes.add(option);
208+
}
209+
210+
const selectBits = document.getElementById("bits");
211+
for (var i = 0; i < nBits; i++) {
212+
const option = document.createElement("option");
213+
option.text = i;
214+
selectBits.add(new Option(i));
215+
}
216+
217+
updateEyeDiagram();
218+
})
219+
220+
</script>
221+
222+
</html>
223+
{% endblock %}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from collections import defaultdict
2+
import csv
3+
import os
4+
import tempfile
5+
from typing import Callable
6+
7+
from jinja2 import Environment, DictLoader
8+
9+
TI_DAC38J8X_EYESCAN_LIBRARY = False
10+
try:
11+
from eyescan.eyescan import perform_eyescan
12+
TI_DAC38J8X_EYESCAN_LIBRARY = True
13+
except OSError:
14+
print(
15+
"Warning: TI DAC38J8X eyescan library is not available. Disabling eyescan module tests."
16+
)
17+
18+
19+
class EyeScan:
20+
21+
def __init__(self, ftdi_dev: int, bit: int) -> None:
22+
self.eyescan_file = tempfile.TemporaryFile()
23+
self.axis_multiplier = {"x": 1, "y": 10}
24+
25+
# Check if the eyescan library is available
26+
assert TI_DAC38J8X_EYESCAN_LIBRARY, "TI DAC38J8X eyescan library is not available."
27+
28+
perform_eyescan(ftdi_dev, self.eyescan_file, bit)
29+
30+
def parse_file(self) -> list[dict]:
31+
samples_by_lane = defaultdict(lambda: defaultdict(list))
32+
with open(self.data_path) as file:
33+
for row in csv.reader(file, delimiter="\t"):
34+
lane, bit, y, x, amp = map(int, row)
35+
samples_by_lane[lane][(y, x)].append((bit, amp))
36+
return [lane for _, lane in sorted(samples_by_lane.items())]
37+
38+
def aggregate_samples(self,
39+
samples: list[dict],
40+
agg_lane_bits: Callable = lambda x: x) -> list[list]:
41+
return [[{
42+
"y": k[0],
43+
"x": k[1],
44+
"amp": agg_lane_bits([i[1] for i in sorted(lane[k])])
45+
} for k in lane] for lane in samples]
46+
47+
def read_diagram_template(self) -> str:
48+
with open(f"{os.path.dirname(__file__)}/eye_diagram.html") as file:
49+
return file.read()
50+
51+
def render_template(self, html_template: str, **kwargs) -> str:
52+
environment = Environment(
53+
loader=DictLoader({"template": html_template}))
54+
template = environment.get_template("template")
55+
return template.render(**kwargs)
56+
57+
def render_diagram(self) -> str:
58+
samples = self.parse_file()
59+
samples = self.aggregate_samples(samples)
60+
diagram_template = self.read_diagram_template()
61+
return self.render_template(diagram_template,
62+
samples=samples,
63+
axis_multiplier=self.axis_multiplier)
64+
65+
def get_eyescan_file_path(self) -> str:
66+
return self.eyescan_file.path
67+
68+
def get_eye_size(self, sample: list[dict]) -> tuple[int, int]:
69+
max_value = max(pixel["amp"] for pixel in sample)
70+
eye_pixels = [pixel for pixel in sample if pixel["amp"] != max_value]
71+
x_values = [pixel["x"] for pixel in eye_pixels]
72+
y_values = [pixel["y"] for pixel in eye_pixels]
73+
if len(x_values):
74+
width = max(x_values) - min(x_values)
75+
else:
76+
width = 0
77+
if len(y_values):
78+
height = max(y_values) - min(y_values)
79+
else:
80+
height = 0
81+
return (width * self.axis_multiplier["x"],
82+
height * self.axis_multiplier["y"])

0 commit comments

Comments
 (0)