1use celox_state_layout::get_byte_size;
2use num_bigint::BigUint;
3use std::fs::File;
4use std::io::{BufWriter, Write};
5use std::path::Path;
6
7#[derive(Clone, Debug)]
13pub struct VcdSignalDesc {
14 pub scope: String,
16 pub name: String,
18 pub offset: usize,
20 pub width: usize,
22 pub is_4state: bool,
24}
25
26#[derive(Clone, Debug)]
29pub struct VcdExternalSignalDesc {
30 pub scope: String,
31 pub name: String,
32 pub width: usize,
33}
34
35#[derive(Clone, Copy)]
36enum VcdWriterSource {
37 Memory { offset: usize, is_4state: bool },
38 External { index: usize },
39}
40
41struct VcdWriterSignal {
42 vcd_id: String,
43 scope: String,
44 name: String,
45 width: usize,
46 source: VcdWriterSource,
47}
48
49pub struct VcdWriter {
50 writer: BufWriter<File>,
51 signals: Vec<VcdWriterSignal>,
52 last_values: Vec<Option<(BigUint, BigUint)>>,
53 timestamp: u64,
54 header_written: bool,
55 external_count: usize,
56}
57
58impl VcdWriter {
59 pub fn new<P: AsRef<Path>>(path: P, descs: &[VcdSignalDesc]) -> std::io::Result<Self> {
60 let file = File::create(path)?;
61 let writer = BufWriter::new(file);
62 let signals = descs
63 .iter()
64 .map(|desc| VcdWriterSignal {
65 vcd_id: String::new(),
66 scope: desc.scope.clone(),
67 name: desc.name.clone(),
68 width: desc.width,
69 source: VcdWriterSource::Memory {
70 offset: desc.offset,
71 is_4state: desc.is_4state,
72 },
73 })
74 .collect::<Vec<_>>();
75
76 let last_values = vec![None; signals.len()];
77
78 Ok(Self {
79 writer,
80 signals,
81 last_values,
82 timestamp: 0,
83 header_written: false,
84 external_count: 0,
85 })
86 }
87
88 pub fn add_external_signals(&mut self, descs: &[VcdExternalSignalDesc]) -> std::io::Result<()> {
91 if descs.is_empty() {
92 return Ok(());
93 }
94 if self.external_count != 0 {
95 let existing = self
96 .signals
97 .iter()
98 .filter(|signal| matches!(signal.source, VcdWriterSource::External { .. }))
99 .zip(descs)
100 .all(|(signal, desc)| {
101 signal.scope == desc.scope
102 && signal.name == desc.name
103 && signal.width == desc.width
104 });
105 if existing && self.external_count == descs.len() {
106 return Ok(());
107 }
108 }
109 if self.header_written {
110 return Err(std::io::Error::new(
111 std::io::ErrorKind::InvalidInput,
112 "cannot add external VCD signals after the first dump",
113 ));
114 }
115 for desc in descs {
116 let index = self.external_count;
117 self.external_count += 1;
118 self.signals.push(VcdWriterSignal {
119 vcd_id: String::new(),
120 scope: desc.scope.clone(),
121 name: desc.name.clone(),
122 width: desc.width,
123 source: VcdWriterSource::External { index },
124 });
125 self.last_values.push(None);
126 }
127 Ok(())
128 }
129
130 fn write_header(&mut self) -> std::io::Result<()> {
131 if self.header_written {
132 return Ok(());
133 }
134 writeln!(self.writer, "$date")?;
135 writeln!(
136 self.writer,
137 " {}",
138 chrono::Local::now().format("%Y-%m-%d %H:%M:%S")
139 )?;
140 writeln!(self.writer, "$end")?;
141 writeln!(self.writer, "$version")?;
142 writeln!(self.writer, " celox")?;
143 writeln!(self.writer, "$end")?;
144 writeln!(self.writer, "$timescale 1ns $end")?;
145
146 let mut scope_order = Vec::<String>::new();
147 let mut scope_groups = Vec::<Vec<usize>>::new();
148 let mut scope_idx = fxhash::FxHashMap::<String, usize>::default();
149 for (signal_index, signal) in self.signals.iter().enumerate() {
150 if let Some(index) = scope_idx.get(&signal.scope).copied() {
151 scope_groups[index].push(signal_index);
152 } else {
153 let index = scope_order.len();
154 scope_idx.insert(signal.scope.clone(), index);
155 scope_order.push(signal.scope.clone());
156 scope_groups.push(vec![signal_index]);
157 }
158 }
159 let mut next_id = 0;
160 for (scope, group) in scope_order.iter().zip(scope_groups) {
161 writeln!(self.writer, "$scope module {} $end", scope)?;
162 for signal_index in group {
163 let signal = &mut self.signals[signal_index];
164 signal.vcd_id = Self::generate_vcd_id(next_id);
165 next_id += 1;
166 writeln!(
167 self.writer,
168 "$var wire {} {} {} $end",
169 signal.width, signal.vcd_id, signal.name
170 )?;
171 }
172 writeln!(self.writer, "$upscope $end")?;
173 }
174 writeln!(self.writer, "$enddefinitions $end")?;
175 writeln!(self.writer, "$dumpvars")?;
176 writeln!(self.writer, "$end")?;
177 self.header_written = true;
178 Ok(())
179 }
180
181 fn generate_vcd_id(num: usize) -> String {
182 let mut id = String::new();
183 let mut n = num;
184 loop {
185 let char = ((n % 94) + 33) as u8 as char;
186 id.push(char);
187 if n < 94 {
188 break;
189 }
190 n = (n / 94) - 1;
191 }
192 id.chars().rev().collect()
193 }
194
195 fn read_value(memory: &[u8], offset: usize, width: usize) -> BigUint {
197 let byte_size = get_byte_size(width);
198 let slice = &memory[offset..offset + byte_size];
199 let mut val = BigUint::from_bytes_le(slice);
200 let extra_bits = byte_size * 8 - width;
201 if extra_bits > 0 {
202 let mask = (BigUint::from(1u32) << width) - 1u32;
203 val &= mask;
204 }
205 val
206 }
207
208 fn mask_to_width(mut value: BigUint, width: usize) -> BigUint {
209 if value.bits() > width as u64 {
210 value &= (BigUint::from(1u8) << width) - 1u8;
211 }
212 value
213 }
214
215 pub fn dump(&mut self, timestamp: u64, memory: &[u8]) -> std::io::Result<()> {
219 self.dump_with_external(timestamp, memory, &[])
220 }
221
222 pub fn dump_with_external(
224 &mut self,
225 timestamp: u64,
226 memory: &[u8],
227 external: &[(BigUint, BigUint)],
228 ) -> std::io::Result<()> {
229 if external.len() != self.external_count {
230 return Err(std::io::Error::new(
231 std::io::ErrorKind::InvalidInput,
232 format!(
233 "expected {} external VCD values, got {}",
234 self.external_count,
235 external.len()
236 ),
237 ));
238 }
239 self.write_header()?;
240 if timestamp > self.timestamp || timestamp == 0 {
241 writeln!(self.writer, "#{}", timestamp)?;
242 self.timestamp = timestamp;
243 }
244
245 for (i, sig) in self.signals.iter().enumerate() {
246 let (current_val, current_mask, is_4state) = match sig.source {
247 VcdWriterSource::Memory { offset, is_4state } => {
248 let byte_size = get_byte_size(sig.width);
249 let value = Self::read_value(memory, offset, sig.width);
250 let mask = if is_4state {
251 Self::read_value(memory, offset + byte_size, sig.width)
252 } else {
253 BigUint::from(0u32)
254 };
255 (value, mask, is_4state)
256 }
257 VcdWriterSource::External { index } => {
258 let (value, mask) = &external[index];
259 let value = Self::mask_to_width(value.clone(), sig.width);
260 let mask = Self::mask_to_width(mask.clone(), sig.width);
261 let is_4state = mask != BigUint::default();
262 (value, mask, is_4state)
263 }
264 };
265
266 let prev = &self.last_values[i];
267 let changed = match prev {
268 Some((pv, pm)) => pv != ¤t_val || pm != ¤t_mask,
269 None => true,
270 };
271
272 if changed {
273 if is_4state && current_mask != BigUint::from(0u32) {
274 Self::write_four_state_value(
275 &mut self.writer,
276 sig.width,
277 ¤t_val,
278 ¤t_mask,
279 &sig.vcd_id,
280 )?;
281 } else if sig.width == 1 {
282 writeln!(self.writer, "{}{}", current_val, sig.vcd_id)?;
283 } else {
284 writeln!(
285 self.writer,
286 "b{} {}",
287 current_val.to_str_radix(2),
288 sig.vcd_id
289 )?;
290 }
291 self.last_values[i] = Some((current_val, current_mask));
292 }
293 }
294 self.writer.flush()?;
295 Ok(())
296 }
297
298 fn write_four_state_value(
299 writer: &mut BufWriter<File>,
300 width: usize,
301 value: &BigUint,
302 mask: &BigUint,
303 vcd_id: &str,
304 ) -> std::io::Result<()> {
305 if width == 1 {
306 let m = mask.bit(0);
307 let v = value.bit(0);
308 let ch = match (m, v) {
309 (false, false) => '0',
310 (false, true) => '1',
311 (true, false) => 'z',
312 (true, true) => 'x',
313 };
314 writeln!(writer, "{}{}", ch, vcd_id)
315 } else {
316 write!(writer, "b")?;
317 for i in (0..width).rev() {
318 let m = mask.bit(i as u64);
319 let v = value.bit(i as u64);
320 let ch = match (m, v) {
321 (false, false) => '0',
322 (false, true) => '1',
323 (true, false) => 'z',
324 (true, true) => 'x',
325 };
326 write!(writer, "{}", ch)?;
327 }
328 writeln!(writer, " {}", vcd_id)
329 }
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336
337 #[test]
338 fn external_values_are_masked_to_their_declared_width() {
339 let dir = tempfile::tempdir().unwrap();
340 let path = dir.path().join("external-width.vcd");
341 let mut writer = VcdWriter::new(&path, &[]).unwrap();
342 writer
343 .add_external_signals(&[VcdExternalSignalDesc {
344 scope: "component".into(),
345 name: "state".into(),
346 width: 8,
347 }])
348 .unwrap();
349
350 writer
351 .dump_with_external(
352 0,
353 &[],
354 &[(BigUint::from(0x1ffu16), BigUint::from(0x100u16))],
355 )
356 .unwrap();
357 writer
358 .dump_with_external(1, &[], &[(BigUint::from(0xffu8), BigUint::default())])
359 .unwrap();
360
361 let dump = std::fs::read_to_string(path).unwrap();
362 assert!(!dump.contains("b111111111"), "{dump}");
363 assert_eq!(dump.matches("b11111111 !").count(), 1, "{dump}");
364 }
365}