setVCD 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- setVCD/__init__.py +51 -0
- setVCD/exceptions.py +101 -0
- setVCD/py.typed +2 -0
- setVCD/setVCD.py +453 -0
- setVCD/types.py +185 -0
- setvcd-0.2.0.dist-info/METADATA +337 -0
- setvcd-0.2.0.dist-info/RECORD +10 -0
- setvcd-0.2.0.dist-info/WHEEL +5 -0
- setvcd-0.2.0.dist-info/licenses/LICENSE +21 -0
- setvcd-0.2.0.dist-info/top_level.txt +1 -0
setVCD/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""SetVCD - Convert VCD signals to sets of time points."""
|
|
2
|
+
|
|
3
|
+
from .exceptions import (
|
|
4
|
+
ClockSignalError,
|
|
5
|
+
EmptyVCDError,
|
|
6
|
+
InvalidInputError,
|
|
7
|
+
InvalidSignalConditionError,
|
|
8
|
+
InvalidTimeRangeError,
|
|
9
|
+
SignalNotFoundError,
|
|
10
|
+
VCDFileNotFoundError,
|
|
11
|
+
VCDParseError,
|
|
12
|
+
VCDSetError,
|
|
13
|
+
)
|
|
14
|
+
from .setVCD import SetVCD
|
|
15
|
+
from .types import (
|
|
16
|
+
FP,
|
|
17
|
+
Raw,
|
|
18
|
+
SignalCondition,
|
|
19
|
+
String,
|
|
20
|
+
Time,
|
|
21
|
+
TimeValue,
|
|
22
|
+
Value,
|
|
23
|
+
ValueType,
|
|
24
|
+
VCDInput,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__version__ = "0.2.0"
|
|
28
|
+
__all__ = [
|
|
29
|
+
"SetVCD",
|
|
30
|
+
# Exceptions
|
|
31
|
+
"VCDSetError",
|
|
32
|
+
"VCDFileNotFoundError",
|
|
33
|
+
"VCDParseError",
|
|
34
|
+
"ClockSignalError",
|
|
35
|
+
"SignalNotFoundError",
|
|
36
|
+
"EmptyVCDError",
|
|
37
|
+
"InvalidInputError",
|
|
38
|
+
"InvalidSignalConditionError",
|
|
39
|
+
"InvalidTimeRangeError",
|
|
40
|
+
# Types
|
|
41
|
+
"Time",
|
|
42
|
+
"Value",
|
|
43
|
+
"TimeValue",
|
|
44
|
+
"SignalCondition",
|
|
45
|
+
"VCDInput",
|
|
46
|
+
# ValueTypes
|
|
47
|
+
"ValueType",
|
|
48
|
+
"Raw",
|
|
49
|
+
"String",
|
|
50
|
+
"FP",
|
|
51
|
+
]
|
setVCD/exceptions.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Exception classes for VCD2Set package."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class VCDSetError(Exception):
|
|
5
|
+
"""Base exception for all VCD2Set errors.
|
|
6
|
+
|
|
7
|
+
All exceptions raised by VCD2Set inherit from this class,
|
|
8
|
+
making it easy to catch all VCD2Set-specific errors.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class VCDFileNotFoundError(VCDSetError):
|
|
15
|
+
"""Raised when a VCD file cannot be found.
|
|
16
|
+
|
|
17
|
+
This occurs when a filename is passed to VCDSet but the file
|
|
18
|
+
doesn't exist at the specified path.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class VCDParseError(VCDSetError):
|
|
25
|
+
"""Raised when a VCD file cannot be parsed.
|
|
26
|
+
|
|
27
|
+
This can occur due to:
|
|
28
|
+
- Malformed VCD file
|
|
29
|
+
- Unsupported VCD format
|
|
30
|
+
- vcdvcd library errors during parsing
|
|
31
|
+
- Signal access errors
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ClockSignalError(VCDSetError):
|
|
38
|
+
"""Raised when clock signal is not found or ambiguous.
|
|
39
|
+
|
|
40
|
+
This occurs when:
|
|
41
|
+
- Specified clock signal doesn't exist in VCD
|
|
42
|
+
- Clock signal name is misspelled
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SignalNotFoundError(VCDSetError):
|
|
49
|
+
"""Raised when requested signal is not in VCD.
|
|
50
|
+
|
|
51
|
+
This occurs in the get() method when signal_name doesn't
|
|
52
|
+
match any signal in the VCD file.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class EmptyVCDError(VCDSetError):
|
|
59
|
+
"""Raised when VCD file has no signals or no time data.
|
|
60
|
+
|
|
61
|
+
This can occur when:
|
|
62
|
+
- VCD file is empty or has no signals
|
|
63
|
+
- Clock signal has no time/value pairs
|
|
64
|
+
- VCD has signals but no actual data
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class InvalidInputError(VCDSetError):
|
|
71
|
+
"""Raised when input type is invalid.
|
|
72
|
+
|
|
73
|
+
This occurs when the vcd parameter is neither:
|
|
74
|
+
- A string filename
|
|
75
|
+
- A Path object
|
|
76
|
+
- A vcdvcd.VCDVCD object
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class InvalidSignalConditionError(VCDSetError):
|
|
83
|
+
"""Raised when signal_condition is not callable or raises exception.
|
|
84
|
+
|
|
85
|
+
This occurs when:
|
|
86
|
+
- signal_condition is not a callable object
|
|
87
|
+
- signal_condition raises an exception during evaluation
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class InvalidTimeRangeError(VCDSetError):
|
|
94
|
+
"""Raised when time range is invalid.
|
|
95
|
+
|
|
96
|
+
This occurs when:
|
|
97
|
+
- Clock signal has negative timestamps
|
|
98
|
+
- Time iteration encounters invalid bounds
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
pass
|
setVCD/py.typed
ADDED
setVCD/setVCD.py
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
"""SetVCD - Convert VCD signals to sets of time points based on conditions."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Dict, List, Optional, Set, Tuple, Union
|
|
6
|
+
|
|
7
|
+
import vcdvcd
|
|
8
|
+
|
|
9
|
+
from .exceptions import (
|
|
10
|
+
ClockSignalError,
|
|
11
|
+
EmptyVCDError,
|
|
12
|
+
InvalidInputError,
|
|
13
|
+
InvalidSignalConditionError,
|
|
14
|
+
SignalNotFoundError,
|
|
15
|
+
VCDFileNotFoundError,
|
|
16
|
+
VCDParseError,
|
|
17
|
+
)
|
|
18
|
+
from .types import (
|
|
19
|
+
FP,
|
|
20
|
+
AnyValue,
|
|
21
|
+
FPValue,
|
|
22
|
+
Raw,
|
|
23
|
+
RawValue,
|
|
24
|
+
SignalConditionProtocol,
|
|
25
|
+
SignalProtocol,
|
|
26
|
+
String,
|
|
27
|
+
StringValue,
|
|
28
|
+
Time,
|
|
29
|
+
ValueType,
|
|
30
|
+
VCDInput,
|
|
31
|
+
VCDVCDProtocol,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _convert_to_int(value: str) -> Optional[int]:
|
|
36
|
+
"""Convert vcdvcd binary string to integer (Raw conversion).
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
value: Binary string from vcdvcd (e.g., "1010", "xxxx", "z")
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Integer value for valid binary strings, None for x/z or malformed strings.
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
>>> _convert_to_int("1010") # Binary to decimal
|
|
46
|
+
10
|
|
47
|
+
>>> _convert_to_int("xxxx") # X/Z values
|
|
48
|
+
None
|
|
49
|
+
"""
|
|
50
|
+
# Case-insensitive check for unknown/high-impedance
|
|
51
|
+
value_lower = value.lower()
|
|
52
|
+
if "x" in value_lower or "z" in value_lower:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
# Binary to decimal conversion
|
|
56
|
+
try:
|
|
57
|
+
return int(value, 2)
|
|
58
|
+
except ValueError:
|
|
59
|
+
# Defensive: malformed VCD value
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _convert_to_string(value: str) -> Optional[str]:
|
|
64
|
+
"""Convert vcdvcd string to string (String conversion - passthrough)."""
|
|
65
|
+
# Return as-is, only return None for truly invalid input
|
|
66
|
+
if value is None or value == "":
|
|
67
|
+
return None
|
|
68
|
+
return value
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _convert_to_fp(value: str, frac: int, signed: bool) -> Optional[float]:
|
|
72
|
+
"""Convert vcdvcd binary string to fixed-point float (FP conversion)."""
|
|
73
|
+
# Validate frac parameter
|
|
74
|
+
if frac < 0:
|
|
75
|
+
raise ValueError(f"frac must be >= 0, got {frac}")
|
|
76
|
+
|
|
77
|
+
# Check for x/z - return NaN per requirements
|
|
78
|
+
value_lower = value.lower()
|
|
79
|
+
if "x" in value_lower or "z" in value_lower:
|
|
80
|
+
return float("nan")
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
# Convert binary string to integer (unsigned initially)
|
|
84
|
+
int_value = int(value, 2)
|
|
85
|
+
total_bits = len(value)
|
|
86
|
+
|
|
87
|
+
# Handle signed values (two's complement)
|
|
88
|
+
if signed and total_bits > 0:
|
|
89
|
+
sign_bit = 1 << (total_bits - 1)
|
|
90
|
+
if int_value & sign_bit:
|
|
91
|
+
# Negative number in two's complement
|
|
92
|
+
int_value = int_value - (1 << total_bits)
|
|
93
|
+
|
|
94
|
+
# Apply fractional scaling: divide by 2^frac
|
|
95
|
+
float_value = int_value / (1 << frac)
|
|
96
|
+
|
|
97
|
+
return float_value
|
|
98
|
+
|
|
99
|
+
except ValueError:
|
|
100
|
+
# Malformed binary string
|
|
101
|
+
return float("nan")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _convert_value(value_str: str, value_type: ValueType) -> AnyValue:
|
|
105
|
+
"""Dispatch to appropriate converter based on ValueType."""
|
|
106
|
+
if isinstance(value_type, Raw):
|
|
107
|
+
return _convert_to_int(value_str)
|
|
108
|
+
elif isinstance(value_type, String):
|
|
109
|
+
return _convert_to_string(value_str)
|
|
110
|
+
elif isinstance(value_type, FP):
|
|
111
|
+
return _convert_to_fp(value_str, value_type.frac, value_type.signed)
|
|
112
|
+
else:
|
|
113
|
+
# Should never happen with proper typing
|
|
114
|
+
raise ValueError(f"Unknown ValueType: {type(value_type)}")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class SetVCD:
|
|
118
|
+
"""
|
|
119
|
+
Query VCD signals with functionally, and combine them with set theory operators.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(self, vcd: VCDInput, clock: str = "clk") -> None:
|
|
123
|
+
"""
|
|
124
|
+
Args:
|
|
125
|
+
vcd: Either a filename (str/Path) to a VCD file, or an already-parsed
|
|
126
|
+
vcdvcd.VCDVCD object. If a filename is provided, it will be loaded
|
|
127
|
+
and parsed automatically.
|
|
128
|
+
clock: Exact name of the clock signal to use as time reference. This
|
|
129
|
+
signal's last transition determines the iteration range. Must match
|
|
130
|
+
a signal name exactly (case-sensitive).
|
|
131
|
+
"""
|
|
132
|
+
# Load VCD object
|
|
133
|
+
if isinstance(vcd, (str, Path)):
|
|
134
|
+
vcd_path = Path(vcd)
|
|
135
|
+
if not vcd_path.exists():
|
|
136
|
+
raise VCDFileNotFoundError(f"VCD file not found: {vcd_path}")
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
self.wave: VCDVCDProtocol = vcdvcd.VCDVCD(str(vcd_path))
|
|
140
|
+
except Exception as e:
|
|
141
|
+
raise VCDParseError(f"Failed to parse VCD file: {e}") from e
|
|
142
|
+
elif hasattr(vcd, "get_signals") and hasattr(vcd, "__getitem__"):
|
|
143
|
+
# Duck typing check for VCDVCD-like object
|
|
144
|
+
self.wave = vcd
|
|
145
|
+
else:
|
|
146
|
+
raise InvalidInputError(
|
|
147
|
+
f"vcd must be a filename (str/Path) or VCDVCD object, "
|
|
148
|
+
f"got {type(vcd).__name__}"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Validate VCD has signals
|
|
152
|
+
try:
|
|
153
|
+
all_signals = self.wave.get_signals()
|
|
154
|
+
except Exception as e:
|
|
155
|
+
raise VCDParseError(f"Failed to retrieve signals from VCD: {e}") from e
|
|
156
|
+
|
|
157
|
+
if not all_signals:
|
|
158
|
+
raise EmptyVCDError("VCD file contains no signals")
|
|
159
|
+
|
|
160
|
+
# Initialize signal storage
|
|
161
|
+
self.sigs: Dict[str, SignalProtocol] = {}
|
|
162
|
+
|
|
163
|
+
# Find clock signal - EXACT match only
|
|
164
|
+
if clock not in all_signals:
|
|
165
|
+
# Provide helpful error message with similar signals using fuzzy matching
|
|
166
|
+
# Split the search term into parts and find signals containing those parts
|
|
167
|
+
search_parts = [p for p in clock.lower().split(".") if p]
|
|
168
|
+
similar = []
|
|
169
|
+
|
|
170
|
+
# Score each signal based on how many parts match
|
|
171
|
+
scored_signals = []
|
|
172
|
+
for sig in all_signals:
|
|
173
|
+
sig_lower = sig.lower()
|
|
174
|
+
matches = sum(1 for part in search_parts if part in sig_lower)
|
|
175
|
+
if matches > 0:
|
|
176
|
+
scored_signals.append((matches, sig))
|
|
177
|
+
|
|
178
|
+
# Sort by number of matches (descending) and take top matches
|
|
179
|
+
scored_signals.sort(reverse=True, key=lambda x: x[0])
|
|
180
|
+
similar = [sig for _, sig in scored_signals[:5]]
|
|
181
|
+
|
|
182
|
+
error_msg = f"Clock signal '{clock}' not found in VCD."
|
|
183
|
+
if similar:
|
|
184
|
+
error_msg += f" Did you mean one of: {similar}?"
|
|
185
|
+
else:
|
|
186
|
+
error_msg += f" Available signals: {all_signals[:10]}..."
|
|
187
|
+
raise ClockSignalError(error_msg)
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
self.sigs["clock"] = self.wave[clock]
|
|
191
|
+
except Exception as e:
|
|
192
|
+
raise VCDParseError(f"Failed to access clock signal '{clock}': {e}") from e
|
|
193
|
+
|
|
194
|
+
# Verify clock has data
|
|
195
|
+
if not self.sigs["clock"].tv:
|
|
196
|
+
raise EmptyVCDError(f"Clock signal '{clock}' has no time/value data")
|
|
197
|
+
|
|
198
|
+
# Get last clock timestamp
|
|
199
|
+
try:
|
|
200
|
+
self.last_clock: Time = self.sigs["clock"].tv[-1][0]
|
|
201
|
+
except (IndexError, TypeError) as e:
|
|
202
|
+
raise EmptyVCDError(
|
|
203
|
+
f"Failed to get last timestamp from clock signal: {e}"
|
|
204
|
+
) from e
|
|
205
|
+
|
|
206
|
+
def search(self, search_regex: str = "") -> List[str]:
|
|
207
|
+
"""Search for signals matching a regex pattern.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
search_regex: Regular expression pattern to match signal names.
|
|
211
|
+
Empty string returns all signals.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
List of signal names matching the pattern.
|
|
215
|
+
|
|
216
|
+
Example:
|
|
217
|
+
>>> vs = SetVCD("sim.vcd", clock="TOP.clk")
|
|
218
|
+
>>> output_signals = vs.search("output")
|
|
219
|
+
>>> accelerator_signals = vs.search("Accelerator.*valid")
|
|
220
|
+
"""
|
|
221
|
+
signals = self.wave.get_signals()
|
|
222
|
+
searched = [s for s in signals if re.search(search_regex, s)]
|
|
223
|
+
return searched
|
|
224
|
+
|
|
225
|
+
def get(
|
|
226
|
+
self,
|
|
227
|
+
signal_name: str,
|
|
228
|
+
signal_condition: Union[
|
|
229
|
+
SignalConditionProtocol[int],
|
|
230
|
+
SignalConditionProtocol[str],
|
|
231
|
+
SignalConditionProtocol[float],
|
|
232
|
+
],
|
|
233
|
+
value_type: Optional[ValueType] = None,
|
|
234
|
+
) -> Set[Time]:
|
|
235
|
+
"""Filter time points based on signal condition.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
signal_name: Exact name of signal (case-sensitive). Must exist in VCD file.
|
|
239
|
+
signal_condition: Function (sm1, s, sp1) -> bool that evaluates signal values.
|
|
240
|
+
The value types passed depend on value_type parameter:
|
|
241
|
+
- Raw(): receives Optional[int] values
|
|
242
|
+
- String(): receives Optional[str] values
|
|
243
|
+
- FP(): receives Optional[float] values
|
|
244
|
+
value_type: Value conversion type (default: Raw() for backward compatibility).
|
|
245
|
+
- Raw(): Binary to int, x/z become None
|
|
246
|
+
- String(): Keep raw strings including x/z as literals
|
|
247
|
+
- FP(frac, signed): Fixed-point to float, x/z become NaN
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Set of timesteps where signal_condition returns True.
|
|
251
|
+
|
|
252
|
+
Examples:
|
|
253
|
+
>>> # Integer comparison (default Raw)
|
|
254
|
+
>>> rising = vs.get("clk", lambda sm1, s, sp1: sm1 == 0 and s == 1)
|
|
255
|
+
>>>
|
|
256
|
+
>>> # String matching to detect x/z
|
|
257
|
+
>>> has_x = vs.get("data", lambda sm1, s, sp1: s is not None and 'x' in s,
|
|
258
|
+
... value_type=String())
|
|
259
|
+
>>>
|
|
260
|
+
>>> # Fixed-point comparison
|
|
261
|
+
>>> above_threshold = vs.get("temp",
|
|
262
|
+
... lambda sm1, s, sp1: s is not None and s > 25.5,
|
|
263
|
+
... value_type=FP(frac=8, signed=True))
|
|
264
|
+
"""
|
|
265
|
+
# Default to Raw() if not specified
|
|
266
|
+
if value_type is None:
|
|
267
|
+
value_type = Raw()
|
|
268
|
+
|
|
269
|
+
# Validate signal exists
|
|
270
|
+
try:
|
|
271
|
+
all_signals = self.wave.get_signals()
|
|
272
|
+
except Exception as e:
|
|
273
|
+
raise VCDParseError(f"Failed to retrieve signals: {e}") from e
|
|
274
|
+
|
|
275
|
+
if signal_name not in all_signals:
|
|
276
|
+
# Provide helpful error with similar signals using fuzzy matching
|
|
277
|
+
# Split the search term into parts and find signals containing those parts
|
|
278
|
+
search_parts = [p for p in signal_name.lower().split(".") if p]
|
|
279
|
+
similar = []
|
|
280
|
+
|
|
281
|
+
# Score each signal based on how many parts match
|
|
282
|
+
scored_signals = []
|
|
283
|
+
for sig in all_signals:
|
|
284
|
+
sig_lower = sig.lower()
|
|
285
|
+
matches = sum(1 for part in search_parts if part in sig_lower)
|
|
286
|
+
if matches > 0:
|
|
287
|
+
scored_signals.append((matches, sig))
|
|
288
|
+
|
|
289
|
+
# Sort by number of matches (descending) and take top matches
|
|
290
|
+
scored_signals.sort(reverse=True, key=lambda x: x[0])
|
|
291
|
+
similar = [sig for _, sig in scored_signals[:5]]
|
|
292
|
+
|
|
293
|
+
error_msg = f"Signal '{signal_name}' not found in VCD."
|
|
294
|
+
if similar:
|
|
295
|
+
error_msg += f" Did you mean one of: {similar}?"
|
|
296
|
+
else:
|
|
297
|
+
error_msg += f" Available signals: {all_signals[:10]}..."
|
|
298
|
+
raise SignalNotFoundError(error_msg)
|
|
299
|
+
|
|
300
|
+
# Validate signal_condition is callable
|
|
301
|
+
if not callable(signal_condition):
|
|
302
|
+
raise InvalidSignalConditionError(
|
|
303
|
+
f"signal_condition must be callable, got {type(signal_condition).__name__}"
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# Get signal object
|
|
307
|
+
try:
|
|
308
|
+
signal_obj = self.wave[signal_name]
|
|
309
|
+
except Exception as e:
|
|
310
|
+
raise VCDParseError(f"Failed to access signal '{signal_name}': {e}") from e
|
|
311
|
+
|
|
312
|
+
# Iterate through ALL time steps (not just deltas)
|
|
313
|
+
out: Set[Time] = set()
|
|
314
|
+
|
|
315
|
+
for time in range(0, self.last_clock + 1):
|
|
316
|
+
try:
|
|
317
|
+
# Get raw string values from vcdvcd
|
|
318
|
+
sm1_str: Optional[str] = signal_obj[time - 1] if time > 0 else None
|
|
319
|
+
s_str: str = signal_obj[time]
|
|
320
|
+
sp1_str: Optional[str] = (
|
|
321
|
+
signal_obj[time + 1] if time < self.last_clock else None
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Convert values using specified ValueType (None for boundaries)
|
|
325
|
+
sm1: AnyValue = (
|
|
326
|
+
_convert_value(sm1_str, value_type) if sm1_str is not None else None
|
|
327
|
+
)
|
|
328
|
+
s: AnyValue = _convert_value(s_str, value_type)
|
|
329
|
+
sp1: AnyValue = (
|
|
330
|
+
_convert_value(sp1_str, value_type) if sp1_str is not None else None
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# Evaluate user's condition
|
|
334
|
+
try:
|
|
335
|
+
# Tell pyright to ignore this because we have dependent types.
|
|
336
|
+
check = signal_condition(sm1, s, sp1) # type: ignore[arg-type]
|
|
337
|
+
except Exception as e:
|
|
338
|
+
raise InvalidSignalConditionError(
|
|
339
|
+
f"signal_condition raised exception at time {time}: {e}. "
|
|
340
|
+
f"Note: signal values can be None (for x/z values or boundaries)."
|
|
341
|
+
) from e
|
|
342
|
+
|
|
343
|
+
# Add time to result set if condition is True
|
|
344
|
+
if check:
|
|
345
|
+
out.add(time)
|
|
346
|
+
|
|
347
|
+
except InvalidSignalConditionError:
|
|
348
|
+
# Re-raise our own exceptions
|
|
349
|
+
raise
|
|
350
|
+
except Exception as e:
|
|
351
|
+
# Wrap any other errors
|
|
352
|
+
raise VCDParseError(
|
|
353
|
+
f"Failed to access signal '{signal_name}' at time {time}: {e}"
|
|
354
|
+
) from e
|
|
355
|
+
|
|
356
|
+
return out
|
|
357
|
+
|
|
358
|
+
def get_values(
|
|
359
|
+
self,
|
|
360
|
+
signal_name: str,
|
|
361
|
+
timesteps: Set[Time],
|
|
362
|
+
value_type: Optional[ValueType] = None,
|
|
363
|
+
) -> Union[
|
|
364
|
+
List[Tuple[Time, RawValue]],
|
|
365
|
+
List[Tuple[Time, StringValue]],
|
|
366
|
+
List[Tuple[Time, FPValue]],
|
|
367
|
+
]:
|
|
368
|
+
"""Get signal values at specific timesteps.
|
|
369
|
+
|
|
370
|
+
This method takes a set of timesteps (typically from get()) and returns
|
|
371
|
+
the signal values at those times as a sorted list of (time, value) tuples.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
signal_name: Exact name of the signal to query (case-sensitive).
|
|
375
|
+
Must exist in the VCD file.
|
|
376
|
+
timesteps: Set of integer timesteps to query. Can be empty.
|
|
377
|
+
value_type: Value conversion type (default: Raw() for backward compatibility).
|
|
378
|
+
- Raw(): Binary to int, x/z become None → List[Tuple[Time, Optional[int]]]
|
|
379
|
+
- String(): Keep raw strings including x/z → List[Tuple[Time, Optional[str]]]
|
|
380
|
+
- FP(frac, signed): Fixed-point to float, x/z → NaN → List[Tuple[Time, Optional[float]]]
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
Sorted list of (time, value) tuples. Value type depends on value_type parameter.
|
|
384
|
+
|
|
385
|
+
Examples:
|
|
386
|
+
>>> handshakes = valid_times & ready_times
|
|
387
|
+
>>>
|
|
388
|
+
>>> # Get as integers (default)
|
|
389
|
+
>>> int_values = vs.get_values("counter", handshakes)
|
|
390
|
+
>>>
|
|
391
|
+
>>> # Get as strings to see x/z
|
|
392
|
+
>>> str_values = vs.get_values("data_bus", handshakes, String())
|
|
393
|
+
>>>
|
|
394
|
+
>>> # Get as fixed-point floats
|
|
395
|
+
>>> fp_values = vs.get_values("voltage", handshakes, FP(frac=12, signed=False))
|
|
396
|
+
"""
|
|
397
|
+
# Default to Raw() if not specified
|
|
398
|
+
if value_type is None:
|
|
399
|
+
value_type = Raw()
|
|
400
|
+
|
|
401
|
+
# Validate signal exists
|
|
402
|
+
try:
|
|
403
|
+
all_signals = self.wave.get_signals()
|
|
404
|
+
except Exception as e:
|
|
405
|
+
raise VCDParseError(f"Failed to retrieve signals: {e}") from e
|
|
406
|
+
|
|
407
|
+
if signal_name not in all_signals:
|
|
408
|
+
# Provide helpful error with similar signals using fuzzy matching
|
|
409
|
+
search_parts = [p for p in signal_name.lower().split(".") if p]
|
|
410
|
+
similar = []
|
|
411
|
+
|
|
412
|
+
# Score each signal based on how many parts match
|
|
413
|
+
scored_signals = []
|
|
414
|
+
for sig in all_signals:
|
|
415
|
+
sig_lower = sig.lower()
|
|
416
|
+
matches = sum(1 for part in search_parts if part in sig_lower)
|
|
417
|
+
if matches > 0:
|
|
418
|
+
scored_signals.append((matches, sig))
|
|
419
|
+
|
|
420
|
+
# Sort by number of matches (descending) and take top matches
|
|
421
|
+
scored_signals.sort(reverse=True, key=lambda x: x[0])
|
|
422
|
+
similar = [sig for _, sig in scored_signals[:5]]
|
|
423
|
+
|
|
424
|
+
error_msg = f"Signal '{signal_name}' not found in VCD."
|
|
425
|
+
if similar:
|
|
426
|
+
error_msg += f" Did you mean one of: {similar}?"
|
|
427
|
+
else:
|
|
428
|
+
error_msg += f" Available signals: {all_signals[:10]}..."
|
|
429
|
+
raise SignalNotFoundError(error_msg)
|
|
430
|
+
|
|
431
|
+
# Get signal object
|
|
432
|
+
try:
|
|
433
|
+
signal_obj = self.wave[signal_name]
|
|
434
|
+
except Exception as e:
|
|
435
|
+
raise VCDParseError(f"Failed to access signal '{signal_name}': {e}") from e
|
|
436
|
+
|
|
437
|
+
# Get values at each timestep and sort by time
|
|
438
|
+
result: List[Tuple[Time, AnyValue]] = []
|
|
439
|
+
for time in timesteps:
|
|
440
|
+
try:
|
|
441
|
+
value_str: str = signal_obj[time]
|
|
442
|
+
value_converted: AnyValue = _convert_value(value_str, value_type)
|
|
443
|
+
result.append((time, value_converted))
|
|
444
|
+
except Exception as e:
|
|
445
|
+
raise VCDParseError(
|
|
446
|
+
f"Failed to access signal '{signal_name}' at time {time}: {e}"
|
|
447
|
+
) from e
|
|
448
|
+
|
|
449
|
+
# Sort by time
|
|
450
|
+
result.sort(key=lambda x: x[0])
|
|
451
|
+
|
|
452
|
+
# Tell pyright to ignore this because we have dependent types.
|
|
453
|
+
return result # type: ignore[return-value]
|
setVCD/types.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Type definitions and protocols for setVCD package."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Callable, List, Optional, Protocol, Tuple, TypeVar, Union
|
|
6
|
+
|
|
7
|
+
# Type aliases for clarity and documentation
|
|
8
|
+
Time = int
|
|
9
|
+
"""Integer timestamp in VCD time units."""
|
|
10
|
+
|
|
11
|
+
Value = Optional[int]
|
|
12
|
+
"""Signal value as integer (binary conversion) or None (for x/z/boundaries)."""
|
|
13
|
+
|
|
14
|
+
TimeValue = Tuple[Time, Value]
|
|
15
|
+
"""Tuple of (time, value) representing a signal transition."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SignalProtocol(Protocol):
|
|
19
|
+
"""Protocol describing the interface of a vcdvcd Signal object.
|
|
20
|
+
|
|
21
|
+
This protocol documents the expected interface without requiring
|
|
22
|
+
an explicit dependency on vcdvcd for type checking.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
tv: List[TimeValue]
|
|
26
|
+
"""List of (time, value) tuples representing signal transitions."""
|
|
27
|
+
|
|
28
|
+
def __getitem__(self, time: Time) -> str:
|
|
29
|
+
"""Random access to get signal value at specific time.
|
|
30
|
+
|
|
31
|
+
Returns RAW string value from vcdvcd - will be converted by SetVCD layer.
|
|
32
|
+
Uses binary search to interpolate value at any time point,
|
|
33
|
+
even between transitions.
|
|
34
|
+
"""
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class VCDVCDProtocol(Protocol):
|
|
39
|
+
"""Protocol describing the interface of a vcdvcd.VCDVCD object.
|
|
40
|
+
|
|
41
|
+
This protocol documents the expected interface without requiring
|
|
42
|
+
an explicit dependency on vcdvcd for type checking.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def get_signals(self) -> List[str]:
|
|
46
|
+
"""Returns list of all signal names in the VCD file."""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
def __getitem__(self, signal_name: str) -> SignalProtocol:
|
|
50
|
+
"""Returns Signal object for the given signal name.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
signal_name: Exact signal name (case-sensitive).
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Signal object with tv attribute and random access.
|
|
57
|
+
"""
|
|
58
|
+
...
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
SignalCondition = Callable[[Optional[int], Optional[int], Optional[int]], bool]
|
|
62
|
+
"""Type for signal condition callbacks.
|
|
63
|
+
|
|
64
|
+
A SignalCondition is a function that takes:
|
|
65
|
+
- sm1: Signal value at time-1 (None if at time 0 or if value is x/z)
|
|
66
|
+
- s: Signal value at current time (None if value is x/z)
|
|
67
|
+
- sp1: Signal value at time+1 (None if at last time or if value is x/z)
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
- True if this time point should be included in the result set
|
|
71
|
+
- False otherwise
|
|
72
|
+
|
|
73
|
+
Examples:
|
|
74
|
+
>>> # Rising edge detector
|
|
75
|
+
>>> rising_edge: SignalCondition = lambda sm1, s, sp1: sm1 == 0 and s == 1
|
|
76
|
+
>>>
|
|
77
|
+
>>> # High level detector
|
|
78
|
+
>>> is_high: SignalCondition = lambda sm1, s, sp1: s == 1
|
|
79
|
+
>>>
|
|
80
|
+
>>> # Change detector
|
|
81
|
+
>>> changed: SignalCondition = lambda sm1, s, sp1: sm1 is not None and sm1 != s
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
VCDInput = Union[str, Path, VCDVCDProtocol]
|
|
85
|
+
"""Type for VCD input to SetVCD.
|
|
86
|
+
|
|
87
|
+
Can be:
|
|
88
|
+
- str: Filename path to VCD file
|
|
89
|
+
- Path: Pathlib Path object to VCD file
|
|
90
|
+
- VCDVCDProtocol: Already-parsed vcdvcd.VCDVCD object
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ValueType classes for controlling signal value conversion
|
|
95
|
+
@dataclass(frozen=True)
|
|
96
|
+
class Raw:
|
|
97
|
+
"""Represent signal bits as integers (default).
|
|
98
|
+
|
|
99
|
+
X and Z values get turned to None.
|
|
100
|
+
|
|
101
|
+
Example:
|
|
102
|
+
>>> # Default behavior - binary to decimal
|
|
103
|
+
>>> vs.get("data[3:0]", lambda sm1, s, sp1: s == 10, value_type=setvcd.Raw) # "1010" → 10
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True)
|
|
110
|
+
class String:
|
|
111
|
+
"""Represent signal bits as a string.
|
|
112
|
+
|
|
113
|
+
X and Z values stay in the string.
|
|
114
|
+
|
|
115
|
+
Example:
|
|
116
|
+
>>> # Detect x/z values in signal
|
|
117
|
+
>>> vs.get("data", lambda sm1, s, sp1: s is not None and 'x' in s,
|
|
118
|
+
... value_type=String())
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True)
|
|
125
|
+
class FP:
|
|
126
|
+
"""Represent signal bits as floating point, by assuming its fixed point.
|
|
127
|
+
|
|
128
|
+
X and Z values get turned to None.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
frac: Number of fractional bits (LSBs). Must be >= 0.
|
|
132
|
+
signed: Whether value has a sign bit (MSB in two's complement).
|
|
133
|
+
Default is False (unsigned).
|
|
134
|
+
|
|
135
|
+
Examples:
|
|
136
|
+
>>> # Temperature sensor with 8 fractional bits, unsigned
|
|
137
|
+
>>> vs.get("temp", lambda sm1, s, sp1: s is not None and s > 25.5,
|
|
138
|
+
... value_type=FP(frac=8, signed=False))
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
frac: int
|
|
142
|
+
signed: bool = False
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# Union type for all value types
|
|
146
|
+
ValueType = Union[Raw, String, FP]
|
|
147
|
+
"""Union of all ValueType options for signal value conversion."""
|
|
148
|
+
|
|
149
|
+
# Polymorphic value type aliases
|
|
150
|
+
RawValue = Optional[int]
|
|
151
|
+
"""Signal value as integer (binary conversion) or None (for x/z/boundaries)."""
|
|
152
|
+
|
|
153
|
+
StringValue = Optional[str]
|
|
154
|
+
"""Signal value as string (preserved from vcdvcd) or None (for boundaries)."""
|
|
155
|
+
|
|
156
|
+
FPValue = Optional[float]
|
|
157
|
+
"""Signal value as float (fixed-point conversion) or None."""
|
|
158
|
+
|
|
159
|
+
AnyValue = Union[RawValue, StringValue, FPValue]
|
|
160
|
+
"""Any signal value type (int, str, or float, or None)."""
|
|
161
|
+
|
|
162
|
+
# Generic type variable for signal conditions
|
|
163
|
+
T = TypeVar("T", int, str, float, contravariant=True)
|
|
164
|
+
"""Type variable for signal value types (contravariant for function parameters)."""
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class SignalConditionProtocol(Protocol[T]):
|
|
168
|
+
"""Protocol for value-type-aware signal condition functions.
|
|
169
|
+
|
|
170
|
+
A generic protocol that accepts signal values of a specific type (int, str, or float)
|
|
171
|
+
and returns a boolean indicating whether the condition is met.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
def __call__(self, sm1: Optional[T], s: Optional[T], sp1: Optional[T]) -> bool:
|
|
175
|
+
"""Evaluate condition on three consecutive signal values.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
sm1: Signal value at time-1 (None if at time 0 or x/z for Raw)
|
|
179
|
+
s: Signal value at current time (None if x/z for Raw)
|
|
180
|
+
sp1: Signal value at time+1 (None if at last time or x/z for Raw)
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
True if this time point should be included in the result set
|
|
184
|
+
"""
|
|
185
|
+
...
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: setVCD
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Convert VCD signals to sets of time points based on conditions
|
|
5
|
+
Author-email: Michail Rontionov <mrontionov@mront.io>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/mrontio/setVCD
|
|
8
|
+
Project-URL: Repository, https://github.com/mrontio/setVCD
|
|
9
|
+
Project-URL: Issues, https://github.com/mrontio/setVCD/issues
|
|
10
|
+
Keywords: vcd,verilog,waveform,signal,testing,verification
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Software Development :: Testing
|
|
14
|
+
Classifier: Topic :: System :: Hardware
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: vcdvcd>=2.0.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
30
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
31
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
32
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
33
|
+
Requires-Dist: isort>=5.0; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# SetVCD
|
|
37
|
+
Programmatically inspect hardware VCD signals using a high-level functional interface.
|
|
38
|
+
|
|
39
|
+
Higher-order programming constructs and set operations are a natural fit for inspecting VCD signals, and this Python library allows you to easily specify, in text, what simulation timesteps matter to functional correctness.
|
|
40
|
+
|
|
41
|
+
## Motivating Example
|
|
42
|
+
Say you are debugging a streaming interface, you often only care about the values of the data at timesteps meeting the following condition:
|
|
43
|
+
|
|
44
|
+
$\text{Rising edge} \land \text{Reset is 0} \land \text{ready} \land \text{valid}$
|
|
45
|
+
|
|
46
|
+

|
|
47
|
+
|
|
48
|
+
You can filter through an individual signal with a filter function of this signature:
|
|
49
|
+
|
|
50
|
+
$(\text{Bits}, \text{Bits}, \text{Bits}) \rightarrow \text{Bool}$
|
|
51
|
+
|
|
52
|
+
with the left-hand tuple representing *values* at timestep $t$: $(t-1, t, t+1)$.
|
|
53
|
+
|
|
54
|
+
We then define our `get` method, which takes the name of the signal (as a String), a function with the above signature, and returns a set of *timesteps*:
|
|
55
|
+
|
|
56
|
+
$\texttt{get}: (\text{String}, ((\text{Bits}, \text{Bits}, \text{Bits}) \rightarrow \text{Bool})) \rightarrow \text{Set(Timestep)}$
|
|
57
|
+
|
|
58
|
+
As what is returned is a set, you can then use [set operations](https://en.wikipedia.org/wiki/Set_(mathematics)#Basic_operations) to manipulate them as needed, and finally extract the values from your desired signal using our `get_value` function:
|
|
59
|
+
|
|
60
|
+
$\texttt{get-value}: (\text{String}, \text{Set(Timestep)}) \rightarrow \text{List((Timestep, Bits))}$
|
|
61
|
+
|
|
62
|
+
Here's an example of finding the rising edges of the clock signal `TOP.clk` of our test wavefile `wave.vcd`:
|
|
63
|
+
```python
|
|
64
|
+
from setVCD import SetVCD
|
|
65
|
+
|
|
66
|
+
# Load VCD file
|
|
67
|
+
vcd_path = "./tests/fixtures/wave.vcd"
|
|
68
|
+
sv = SetVCD(vcd_path, clock="TOP.clk")
|
|
69
|
+
|
|
70
|
+
rising_edges = sv.get("TOP.clk", lambda tm1, t, tp1: tm1 == 0 and t == 1)
|
|
71
|
+
print(rising_edges)
|
|
72
|
+
# {34, 36, 38, 40, 42, 44, ...}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Because `rising_edges` is returned as a set, we can use set operations to combine it with other signals:
|
|
76
|
+
```python
|
|
77
|
+
# Get times when the reset signal is 0
|
|
78
|
+
reset_is_0 = sv.get("TOP.reset", lambda tm1, t, tp1: t == 0)
|
|
79
|
+
# Use set intersection to get valid clock updates.
|
|
80
|
+
clock_update = rising_edges & reset_is_0
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Finally, you can search the wavefile with a regex (e.g. "output"), and apply the same operations to it:
|
|
84
|
+
```python
|
|
85
|
+
# Find VCD signals relating to keyword "output"
|
|
86
|
+
sv.search("output")
|
|
87
|
+
|
|
88
|
+
# Get times when output_valid and output_ready are asserted.
|
|
89
|
+
out_valid = sv.get("TOP.Accelerator.io_output_valid", lambda tm1, t, tp1: t == 1)
|
|
90
|
+
out_ready = sv.get("TOP.Accelerator.io_output_ready", lambda tm1, t, tp1: t == 1)
|
|
91
|
+
|
|
92
|
+
# Get timesteps of valid outputs
|
|
93
|
+
valid_output_timesteps = rising_edges & reset0 & out_ready & out_valid
|
|
94
|
+
|
|
95
|
+
# Get the values of the Stream `value` signal (the data) at timesteps when it is valid
|
|
96
|
+
outputs = sv.get_values("TOP.Accelerator.io_output_payload_fragment_value_0[0:0]", valid_output_timesteps)
|
|
97
|
+
print(outputs)
|
|
98
|
+
# [(52, 0), (62, 0), (72, 1), ...] # Integer values
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Overview
|
|
102
|
+
|
|
103
|
+
SetVCD is a Python package for analyzing Verilog VCD files and extracting time points where specific signal conditions are met. It provides a simple, type-safe interface for working with simulation waveforms using set-based operations.
|
|
104
|
+
|
|
105
|
+
## Installation
|
|
106
|
+
The package is available in PyPI:
|
|
107
|
+
```bash
|
|
108
|
+
pip install setVCD
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
## Usage
|
|
113
|
+
### Initialization
|
|
114
|
+
|
|
115
|
+
You can initialize SetVCD with either a filename or a vcdvcd object:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
import setVCD
|
|
119
|
+
from pathlib import Path
|
|
120
|
+
|
|
121
|
+
# From string filename
|
|
122
|
+
sv = SetVCD("simulation.vcd", clock="clk")
|
|
123
|
+
|
|
124
|
+
# From Path object
|
|
125
|
+
sv = SetVCD(Path("simulation.vcd"), clock="clk")
|
|
126
|
+
|
|
127
|
+
# From vcdvcd object
|
|
128
|
+
import vcdvcd
|
|
129
|
+
vcd = vcdvcd.VCDVCD("simulation.vcd")
|
|
130
|
+
sv = SetVCD(vcd, clock="clk")
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The `clock` parameter must be the exact name of the clock signal in your VCD file (case-sensitive). This signal determines the time range for queries.
|
|
134
|
+
|
|
135
|
+
### Signal Conditions
|
|
136
|
+
|
|
137
|
+
The `signal_condition` callback receives three arguments representing the signal value at three consecutive time points:
|
|
138
|
+
|
|
139
|
+
- `sm1`: Signal value at time-1 (None at time 0 or if value is x/z)
|
|
140
|
+
- `s`: Signal value at current time (None if value is x/z)
|
|
141
|
+
- `sp1`: Signal value at time+1 (None at last time or if value is x/z)
|
|
142
|
+
|
|
143
|
+
Signal values are `Optional[int]`:
|
|
144
|
+
- Integers: Binary values converted to decimal (e.g., "1010" → 10)
|
|
145
|
+
- None: Represents x/z values or boundary conditions (t-1 at time 0, t+1 at last time)
|
|
146
|
+
|
|
147
|
+
The callback should return `True` to include that time point in the result set.
|
|
148
|
+
|
|
149
|
+
### Examples
|
|
150
|
+
|
|
151
|
+
#### Basic Signal Detection
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
# Rising edge: 0 -> 1 transition
|
|
155
|
+
rising = sv.get("clk", lambda sm1, s, sp1: sm1 == 0 and s == 1)
|
|
156
|
+
|
|
157
|
+
# Falling edge: 1 -> 0 transition
|
|
158
|
+
falling = sv.get("clk", lambda sm1, s, sp1: sm1 == 1 and s == 0)
|
|
159
|
+
|
|
160
|
+
# Any edge: value changed
|
|
161
|
+
edges = sv.get("data", lambda sm1, s, sp1: sm1 is not None and sm1 != s)
|
|
162
|
+
|
|
163
|
+
# Level high
|
|
164
|
+
high = sv.get("enable", lambda sm1, s, sp1: s == 1)
|
|
165
|
+
|
|
166
|
+
# Level low
|
|
167
|
+
low = sv.get("reset", lambda sm1, s, sp1: s == 0)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
#### Multi-bit Signals
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
# Specific pattern on a bus (binary "1010" = decimal 10)
|
|
174
|
+
pattern = sv.get("bus[3:0]", lambda sm1, s, sp1: s == 10)
|
|
175
|
+
|
|
176
|
+
# Bus is non-zero
|
|
177
|
+
active = sv.get("data[7:0]", lambda sm1, s, sp1: s != 0)
|
|
178
|
+
|
|
179
|
+
# Bus transition detection
|
|
180
|
+
bus_changed = sv.get("addr[15:0]", lambda sm1, s, sp1: sm1 is not None and sm1 != s)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
#### Complex Queries with Set Operations
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
# Rising clock edges when enable is high
|
|
187
|
+
clk_rising = sv.get("clk", lambda sm1, s, sp1: sm1 == 0 and s == 1)
|
|
188
|
+
enable_high = sv.get("enable", lambda sm1, s, sp1: s == 1)
|
|
189
|
+
valid_clocks = clk_rising & enable_high
|
|
190
|
+
|
|
191
|
+
# Data changes while not in reset
|
|
192
|
+
data_changes = sv.get("data", lambda sm1, s, sp1: sm1 is not None and sm1 != s)
|
|
193
|
+
not_reset = sv.get("reset", lambda sm1, s, sp1: s == 0)
|
|
194
|
+
valid_changes = data_changes & not_reset
|
|
195
|
+
|
|
196
|
+
# Either signal is high
|
|
197
|
+
sig1_high = sv.get("sig1", lambda sm1, s, sp1: s == 1)
|
|
198
|
+
sig2_high = sv.get("sig2", lambda sm1, s, sp1: s == 1)
|
|
199
|
+
either_high = sig1_high | sig2_high
|
|
200
|
+
|
|
201
|
+
# Exclusive high (one but not both)
|
|
202
|
+
exclusive_high = sig1_high ^ sig2_high
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
#### Advanced Pattern Detection
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
# Detect setup violation: data changes right before clock edge
|
|
209
|
+
data_change = sv.get("data", lambda sm1, s, sp1: sm1 is not None and sm1 != s)
|
|
210
|
+
clk_about_to_rise = sv.get("clk", lambda sm1, s, sp1: s == 0 and sp1 == 1)
|
|
211
|
+
setup_violations = data_change & clk_about_to_rise
|
|
212
|
+
|
|
213
|
+
# Handshake protocol: valid and ready both high
|
|
214
|
+
valid_high = sv.get("valid", lambda sm1, s, sp1: s == 1)
|
|
215
|
+
ready_high = sv.get("ready", lambda sm1, s, sp1: s == 1)
|
|
216
|
+
handshake_times = valid_high & ready_high
|
|
217
|
+
|
|
218
|
+
# State machine transitions (binary "00" = 0, "01" = 1)
|
|
219
|
+
state_a = sv.get("state[1:0]", lambda sm1, s, sp1: s == 0)
|
|
220
|
+
state_b = sv.get("state[1:0]", lambda sm1, s, sp1: s == 1)
|
|
221
|
+
# Times when transitioning from state A to state B
|
|
222
|
+
transition = sv.get("state[1:0]", lambda sm1, s, sp1: sm1 == 0 and s == 1)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Value Types
|
|
226
|
+
|
|
227
|
+
SetVCD supports three ValueType options to control how signal values are converted before being passed to your condition lambdas:
|
|
228
|
+
|
|
229
|
+
### Raw() - Integer Conversion (Default)
|
|
230
|
+
|
|
231
|
+
Converts binary strings to decimal integers. X/Z values become `None`. This is the default behavior.
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
from setVCD import SetVCD, Raw
|
|
235
|
+
|
|
236
|
+
sv = SetVCD("simulation.vcd", clock="clk")
|
|
237
|
+
|
|
238
|
+
# Default behavior (Raw is implicit)
|
|
239
|
+
rising = sv.get("data[7:0]", lambda sm1, s, sp1: sm1 is not None and sm1 < s)
|
|
240
|
+
|
|
241
|
+
# Explicit Raw (same as above)
|
|
242
|
+
rising = sv.get("data[7:0]", lambda sm1, s, sp1: sm1 is not None and sm1 < s, value_type=Raw())
|
|
243
|
+
|
|
244
|
+
# Multi-bit signals converted to decimal
|
|
245
|
+
# Binary "00001010" → integer 10
|
|
246
|
+
high_values = sv.get("bus[7:0]", lambda sm1, s, sp1: s is not None and s > 128)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### String() - Preserve Raw Strings
|
|
250
|
+
|
|
251
|
+
Keeps vcdvcd's raw string representation, including X/Z values as literal strings. Useful for detecting unknown states.
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
from setVCD import SetVCD, String
|
|
255
|
+
|
|
256
|
+
sv = SetVCD("simulation.vcd", clock="clk")
|
|
257
|
+
|
|
258
|
+
# Detect X/Z values in data bus
|
|
259
|
+
has_x = sv.get("data[7:0]",
|
|
260
|
+
lambda sm1, s, sp1: s is not None and 'x' in s.lower(),
|
|
261
|
+
value_type=String())
|
|
262
|
+
|
|
263
|
+
# String pattern matching
|
|
264
|
+
all_ones = sv.get("bus[3:0]",
|
|
265
|
+
lambda sm1, s, sp1: s == "1111",
|
|
266
|
+
value_type=String())
|
|
267
|
+
|
|
268
|
+
# Get string values
|
|
269
|
+
values = sv.get_values("data", timesteps, value_type=String())
|
|
270
|
+
# Returns: [(50, "1010"), (60, "1111"), (70, "xxxx"), ...]
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**X/Z Handling:** X and Z values are preserved as strings (`"x"`, `"z"`, `"xxxx"`, etc.)
|
|
274
|
+
|
|
275
|
+
### FP() - Fixed-Point to Float
|
|
276
|
+
|
|
277
|
+
Converts binary strings to floating-point by interpreting them as fixed-point numbers with configurable fractional bits and optional sign bit.
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
from setVCD import SetVCD, FP
|
|
281
|
+
|
|
282
|
+
sv = SetVCD("simulation.vcd", clock="clk")
|
|
283
|
+
|
|
284
|
+
# Temperature sensor with 8 fractional bits (Q8.8 format)
|
|
285
|
+
# Binary "0001100100000000" → 25.0 degrees
|
|
286
|
+
above_threshold = sv.get("temp_sensor[15:0]",
|
|
287
|
+
lambda sm1, s, sp1: s is not None and s > 25.5,
|
|
288
|
+
value_type=FP(frac=8, signed=False))
|
|
289
|
+
|
|
290
|
+
# Signed fixed-point (Q3.4 format - 1 sign bit, 3 integer bits, 4 fractional bits)
|
|
291
|
+
# Binary "11111110" → -0.125 (two's complement)
|
|
292
|
+
negative_values = sv.get("signed_value[7:0]",
|
|
293
|
+
lambda sm1, s, sp1: s is not None and s < 0,
|
|
294
|
+
value_type=FP(frac=4, signed=True))
|
|
295
|
+
|
|
296
|
+
# Get fixed-point values
|
|
297
|
+
voltages = sv.get_values("adc_reading[11:0]", timesteps,
|
|
298
|
+
value_type=FP(frac=12, signed=False))
|
|
299
|
+
# Returns: [(50, 1.2), (60, 2.5), (70, 3.8), ...]
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**X/Z Handling:** X and Z values become `float('nan')`. Use `math.isnan()` to detect them:
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
import math
|
|
306
|
+
|
|
307
|
+
# Filter out NaN values
|
|
308
|
+
valid_readings = sv.get("sensor",
|
|
309
|
+
lambda sm1, s, sp1: s is not None and not math.isnan(s),
|
|
310
|
+
value_type=FP(frac=8, signed=False))
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**Fixed-Point Formula:**
|
|
314
|
+
- Unsigned: `value = int_value / (2^frac)`
|
|
315
|
+
- Signed: Two's complement, then divide by `2^frac`
|
|
316
|
+
|
|
317
|
+
**Examples:**
|
|
318
|
+
- `"00001010"` with `frac=4, signed=False` → `10 / 16 = 0.625`
|
|
319
|
+
- `"11111110"` with `frac=4, signed=True` → `-2 / 16 = -0.125`
|
|
320
|
+
- `"00010000"` with `frac=0, signed=False` → `16 / 1 = 16.0` (integer)
|
|
321
|
+
|
|
322
|
+
### Hardware Use Cases
|
|
323
|
+
|
|
324
|
+
**Raw (Default):** Most general-purpose verification - state machines, counters, addresses, data comparisons
|
|
325
|
+
|
|
326
|
+
**String:** Debugging X/Z propagation, detecting uninitialized signals, bit-pattern analysis
|
|
327
|
+
|
|
328
|
+
**FP:** Analog interfaces (ADC/DAC), sensor data, fixed-point DSP verification, power/temperature monitors
|
|
329
|
+
|
|
330
|
+
## Future Enhancements
|
|
331
|
+
|
|
332
|
+
Planned for future versions:
|
|
333
|
+
|
|
334
|
+
- Higher-order operations for signal conditions
|
|
335
|
+
- Performance optimization for large VCD files
|
|
336
|
+
- Streaming interface for very large files
|
|
337
|
+
- MCP (Model Context Protocol) integration
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
setVCD/__init__.py,sha256=9XD5kj1dA6Ow_oI0TiEO3T9CKC4j3TuXRs25ZpOLA7E,923
|
|
2
|
+
setVCD/exceptions.py,sha256=0hXqETlYsmZ4sx8X2wJPIYOXA7OI82Nozq6wcgpae2Y,2149
|
|
3
|
+
setVCD/py.typed,sha256=uPBAYYlQuNeFaWWlnXX8ALU2iHi41MrB_u1ZSa3U4lA,81
|
|
4
|
+
setVCD/setVCD.py,sha256=yIRaYauPjXln5n6GuOHBcUgPbr-xbcj37CJfNLJQBpI,17139
|
|
5
|
+
setVCD/types.py,sha256=OZLJszYveOr6pxU36O7qhub_GrJLcefkfZUXiwsL3IM,5644
|
|
6
|
+
setvcd-0.2.0.dist-info/licenses/LICENSE,sha256=rHs-K10ejk3J6IkzDG_iJseZllEVzrR48gEwyqehK_g,1077
|
|
7
|
+
setvcd-0.2.0.dist-info/METADATA,sha256=8WOqJrH2QIhh1puZWnMooa4iGy0v0tiQMsw_b47cdqw,12142
|
|
8
|
+
setvcd-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
+
setvcd-0.2.0.dist-info/top_level.txt,sha256=CRByAnG-0SIIj8FXsocVOHmwoInY5FQFuRHnyCqS6mo,7
|
|
10
|
+
setvcd-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 VCD2Set Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
setVCD
|