A => .gitignore +1 -0
A => Cargo.lock +122 -0
@@ 1,122 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "ansi_term"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "clap"
+version = "2.33.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
+dependencies = [
+ "ansi_term",
+ "atty",
+ "bitflags",
+ "strsim",
+ "textwrap",
+ "unicode-width",
+ "vec_map",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6"
+
+[[package]]
+name = "strsim"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+
+[[package]]
+name = "textwrap"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "unicode-width"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
+
+[[package]]
+name = "vcd"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84a5ed196124bf3bb32418e5f79cd7548a455500bba37e094f7ef4cfe91161fc"
+
+[[package]]
+name = "vcd2v"
+version = "0.1.0"
+dependencies = [
+ "clap",
+ "vcd",
+]
+
+[[package]]
+name = "vec_map"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
A => Cargo.toml +11 -0
@@ 1,11 @@
+[package]
+name = "vcd2v"
+version = "0.1.0"
+authors = [ "Andrew Kay <projects@ajk.me>" ]
+edition = "2018"
+description = "Simple utility for generating Verilog stimulus from VCD input"
+license = "ISC"
+
+[dependencies]
+clap = "2"
+vcd = "0.6.1"
A => LICENSE +13 -0
@@ 1,13 @@
+Copyright (c) 2021, Andrew Kay
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
A => README.md +51 -0
@@ 1,51 @@
+# vcd2v
+
+Simple utility for generating Verilog stimulus from [VCD](https://en.wikipedia.org/wiki/Value_change_dump) input.
+
+## Usage
+
+So, you have a VCD file containing signals (`libsigrok.D1` and `libsigrok.D3`), you want to convert these to a series of Verilog delay and assignment statements:
+
+```
+cat capture.vcd | vcd2v libsigrok.D1 libsigrok.D3
+```
+
+This will result in something like:
+
+```
+D1 = 0;
+D3 = 1;
+#375;
+D1 = 1;
+#500;
+D1 = 0;
+#125;
+D1 = 1;
+D3 = 0;
+...
+```
+
+You can rename the Verilog variables used for the signals:
+
+```
+cat capture.vcd | vcd2v a=libsigrok.D1 b=libsigrok.D3
+```
+
+This will result in something like:
+
+```
+a = 0;
+b = 1;
+#375;
+a = 1;
+#500;
+a = 0;
+#125;
+a = 1;
+b = 0;
+...
+```
+
+You can export only a portion of the VCD file using the `--time` argument.
+
+You can apply a multiplier to the delays using the `--scale` argument.
A => src/lib.rs +345 -0
@@ 1,345 @@
+use std::collections::HashMap;
+use std::io;
+use std::io::{BufWriter, Read, Write};
+
+use vcd::Command::{ChangeScalar, Timestamp};
+use vcd::{Header, IdCode, Parser, Var, VarType};
+
+pub fn run<R: Read>(
+ input: &mut R,
+ selections: &[String],
+ start_time: Option<u64>,
+ end_time: Option<u64>,
+ scale: Option<f32>,
+) -> Result<(), String> {
+ let mut parser = Parser::new(input);
+
+ let header = parser.parse_header().map_err(|e| e.to_string())?;
+
+ let signals = get_signal_map(&header, selections)?;
+
+ let mut output = BufWriter::new(io::stdout());
+
+ generate(
+ &mut output,
+ &mut parser,
+ &signals,
+ start_time,
+ end_time,
+ scale.unwrap_or(1.0),
+ )
+ .map_err(|e| e.to_string())
+}
+
+fn parse_selection(selection: &str) -> Result<(Option<&str>, &str), String> {
+ if selection.is_empty() {
+ return Err("empty signal selection".into());
+ }
+
+ let elements: Vec<&str> = selection.split('=').collect();
+
+ match elements.len() {
+ 1 => Ok((None, elements[0])),
+ 2 => {
+ if elements[0].is_empty() || elements[1].is_empty() {
+ return Err(format!("invalid signal selection: {}", selection));
+ }
+
+ Ok((Some(elements[0]), elements[1]))
+ }
+ _ => Err(format!("invalid signal selection: {}", selection)),
+ }
+}
+
+#[cfg(test)]
+mod parse_selection_tests {
+ use super::*;
+
+ #[test]
+ fn test_valid() {
+ assert_eq!(parse_selection("a.b.x"), Ok((None, "a.b.x")));
+ }
+
+ #[test]
+ fn test_valid_reassignment() {
+ assert_eq!(parse_selection("q=a.b.x"), Ok((Some("q"), "a.b.x")));
+ }
+
+ #[test]
+ fn test_invalid() {
+ assert!(parse_selection("").is_err());
+ assert!(parse_selection("=").is_err());
+ assert!(parse_selection("q=").is_err());
+ assert!(parse_selection("=a.b.x").is_err());
+ }
+}
+
+fn find_vcd_var<'a>(header: &'a Header, path: &str) -> Option<&'a Var> {
+ let path: Vec<&str> = path.split('.').collect();
+
+ header.find_var(&path)
+}
+
+#[cfg(test)]
+mod find_vcd_var_tests {
+ use super::*;
+
+ #[test]
+ fn test_valid() {
+ let mut parser = vcd::Parser::new(
+ &b"
+ $scope module a $end
+ $scope module b $end
+ $var wire 1 ! x $end
+ $var wire 1 # y $end
+ $var wire 8 $ z $end
+ $upscope $end
+ $upscope $end
+ $enddefinitions $end
+ "[..],
+ );
+
+ let header = parser.parse_header().unwrap();
+
+ let vcd_var = find_vcd_var(&header, "a.b.x").unwrap();
+
+ assert_eq!(vcd_var.reference, "x");
+ }
+
+ #[test]
+ fn test_invalid() {
+ let mut parser = vcd::Parser::new(
+ &b"
+ $scope module a $end
+ $scope module b $end
+ $var wire 1 ! x $end
+ $var wire 1 # y $end
+ $var wire 8 $ z $end
+ $upscope $end
+ $upscope $end
+ $enddefinitions $end
+ "[..],
+ );
+
+ let header = parser.parse_header().unwrap();
+
+ assert!(find_vcd_var(&header, "a.b.c").is_none());
+ assert!(find_vcd_var(&header, "a.c.z").is_none());
+ }
+}
+
+fn get_signal_map(
+ header: &Header,
+ selections: &[String],
+) -> Result<HashMap<IdCode, String>, String> {
+ let mut signals = HashMap::new();
+
+ for selection in selections {
+ let (verilog_name, vcd_path) = parse_selection(selection)?;
+
+ let vcd_var = find_vcd_var(header, vcd_path)
+ .ok_or_else(|| format!("invalid VCD path: {}", vcd_path))?;
+
+ if vcd_var.var_type != VarType::Wire || vcd_var.size != 1 {
+ return Err(format!("signal must be 1-bit wide wire: {}", vcd_path));
+ }
+
+ let verilog_name = verilog_name.unwrap_or(&vcd_var.reference);
+
+ signals.insert(vcd_var.code, verilog_name.to_string());
+ }
+
+ Ok(signals)
+}
+
+#[cfg(test)]
+mod get_signal_map_tests {
+ use std::str::FromStr;
+
+ use super::*;
+
+ #[test]
+ fn test_valid() {
+ let mut parser = vcd::Parser::new(
+ &b"
+ $scope module a $end
+ $scope module b $end
+ $var wire 1 ! x $end
+ $var wire 1 # y $end
+ $var wire 8 $ z $end
+ $upscope $end
+ $upscope $end
+ $enddefinitions $end
+ "[..],
+ );
+
+ let header = parser.parse_header().unwrap();
+
+ let signals =
+ get_signal_map(&header, &["q=a.b.x".to_string(), "a.b.y".to_string()]).unwrap();
+
+ assert_eq!(signals.get(&IdCode::from_str("!").unwrap()).unwrap(), "q");
+ assert_eq!(signals.get(&IdCode::from_str("#").unwrap()).unwrap(), "y");
+ }
+
+ #[test]
+ fn test_invalid() {
+ let mut parser = vcd::Parser::new(
+ &b"
+ $scope module a $end
+ $scope module b $end
+ $var wire 1 ! x $end
+ $var wire 1 # y $end
+ $var wire 8 $ z $end
+ $upscope $end
+ $upscope $end
+ $enddefinitions $end
+ "[..],
+ );
+
+ let header = parser.parse_header().unwrap();
+
+ assert!(get_signal_map(&header, &["a.b.c".to_string()]).is_err());
+ assert!(get_signal_map(&header, &["a.b.z".to_string()]).is_err());
+ }
+}
+
+fn generate<R: Read, W: Write>(
+ output: &mut BufWriter<W>,
+ parser: &mut Parser<R>,
+ signals: &HashMap<IdCode, String>,
+ start_time: Option<u64>,
+ end_time: Option<u64>,
+ scale: f32,
+) -> io::Result<()> {
+ let mut time = 0;
+ let mut last_time = start_time.unwrap_or(0);
+
+ for command_result in parser {
+ let command = command_result?;
+
+ match command {
+ Timestamp(t) => time = t,
+ ChangeScalar(i, v) if signals.contains_key(&i) => {
+ if start_time.map_or(false, |start_time| time < start_time) {
+ continue;
+ }
+
+ if end_time.map_or(false, |end_time| time > end_time) {
+ break;
+ }
+
+ let delay = time - last_time;
+
+ if delay > 0 {
+ let delay = delay as f32 * scale;
+
+ writeln!(output, "#{};", delay)?;
+ }
+
+ let verilog_name = signals.get(&i).unwrap();
+
+ writeln!(output, "{} = {};", verilog_name, v)?;
+
+ last_time = time;
+ }
+ _ => (),
+ }
+ }
+
+ Ok(())
+}
+
+#[cfg(test)]
+mod generate_tests {
+ use super::*;
+
+ #[test]
+ fn test_no_time_range() {
+ let mut parser = vcd::Parser::new(
+ &b"
+ $scope module a $end
+ $scope module b $end
+ $var wire 1 ! x $end
+ $var wire 1 # y $end
+ $var wire 8 $ z $end
+ $upscope $end
+ $upscope $end
+ $enddefinitions $end
+ #100
+ 1!
+ 1#
+ #110
+ 0!
+ #120
+ 0#
+ #130
+ 1!
+ 1#
+ "[..],
+ );
+
+ let header = parser.parse_header().unwrap();
+
+ let signals =
+ get_signal_map(&header, &["q=a.b.x".to_string(), "a.b.y".to_string()]).unwrap();
+
+ let mut output = BufWriter::new(Vec::new());
+
+ assert!(generate(&mut output, &mut parser, &signals, None, None, 1.0).is_ok());
+
+ let output = String::from_utf8(output.into_inner().unwrap()).unwrap();
+
+ assert_eq!(
+ output,
+ "#100;\nq = 1;\ny = 1;\n#10;\nq = 0;\n#10;\ny = 0;\n#10;\nq = 1;\ny = 1;\n"
+ );
+ }
+
+ #[test]
+ fn test_with_time_range() {
+ let mut parser = vcd::Parser::new(
+ &b"
+ $scope module a $end
+ $scope module b $end
+ $var wire 1 ! x $end
+ $var wire 1 # y $end
+ $var wire 8 $ z $end
+ $upscope $end
+ $upscope $end
+ $enddefinitions $end
+ #100
+ 1!
+ 1#
+ #110
+ 0!
+ #120
+ 0#
+ #130
+ 1!
+ 1#
+ "[..],
+ );
+
+ let header = parser.parse_header().unwrap();
+
+ let signals =
+ get_signal_map(&header, &["q=a.b.x".to_string(), "a.b.y".to_string()]).unwrap();
+
+ let mut output = BufWriter::new(Vec::new());
+
+ assert!(generate(
+ &mut output,
+ &mut parser,
+ &signals,
+ Some(105),
+ Some(125),
+ 1.0
+ )
+ .is_ok());
+
+ let output = String::from_utf8(output.into_inner().unwrap()).unwrap();
+
+ assert_eq!(output, "#5;\nq = 0;\n#10;\ny = 0;\n");
+ }
+}
A => src/main.rs +119 -0
@@ 1,119 @@
+use std::io;
+use std::io::BufReader;
+use std::process;
+use std::str::FromStr;
+
+use clap::{App, Arg};
+
+fn main() {
+ let (selections, start_time, end_time, scale) = parse_args();
+
+ let mut input = BufReader::new(io::stdin());
+
+ if let Err(e) = vcd2v::run(&mut input, &selections, start_time, end_time, scale) {
+ eprintln!("{}", e);
+ process::exit(1);
+ }
+}
+
+fn parse_args() -> (Vec<String>, Option<u64>, Option<u64>, Option<f32>) {
+ let matches = App::new("vcd2v")
+ .arg(
+ Arg::with_name("time")
+ .long("time")
+ .short("t")
+ .takes_value(true)
+ .value_name("[START][:END]"),
+ )
+ .arg(
+ Arg::with_name("scale")
+ .long("scale")
+ .short("s")
+ .takes_value(true)
+ .value_name("SCALE"),
+ )
+ .arg(Arg::with_name("selection").multiple(true).required(true))
+ .get_matches();
+
+ let selections: Vec<String> = matches
+ .values_of("selection")
+ .unwrap()
+ .map(|v| v.to_string())
+ .collect();
+
+ let time_range = matches
+ .value_of("time")
+ .map(|s| parse_time_range(s))
+ .transpose()
+ .expect("invalid time argument")
+ .unwrap_or((None, None));
+
+ let scale = matches
+ .value_of("scale")
+ .map(|s| f32::from_str(s))
+ .transpose()
+ .expect("invalid scale argument");
+
+ (selections, time_range.0, time_range.1, scale)
+}
+
+fn parse_time_range(value: &str) -> Result<(Option<u64>, Option<u64>), String> {
+ if value.is_empty() {
+ return Err("empty time range".into());
+ }
+
+ let elements: Vec<&str> = value.split(':').collect();
+
+ let (start_time, end_time) = match elements.len() {
+ 1 => (Some(elements[0]), None),
+ 2 => (Some(elements[0]), Some(elements[1])),
+ _ => return Err("invalid time range format".into()),
+ };
+
+ let parse_optional_time = |s: Option<&str>| {
+ s.filter(|s| !s.is_empty())
+ .map(|s| u64::from_str(s))
+ .transpose()
+ .map_err(|e| e.to_string())
+ };
+
+ let start_time = parse_optional_time(start_time)?;
+ let end_time = parse_optional_time(end_time)?;
+
+ if let (Some(s), Some(e)) = (start_time, end_time) {
+ if s >= e {
+ return Err("invalid time range: start time must be less than end time".into());
+ }
+ }
+
+ Ok((start_time, end_time))
+}
+
+#[cfg(test)]
+mod parse_time_range_tests {
+ use super::*;
+
+ #[test]
+ fn test_start_time_only() {
+ assert_eq!(parse_time_range("1"), Ok((Some(1), None)));
+ assert_eq!(parse_time_range("1:"), Ok((Some(1), None)));
+ }
+
+ #[test]
+ fn test_end_time_only() {
+ assert_eq!(parse_time_range(":9"), Ok((None, Some(9))));
+ }
+
+ #[test]
+ fn test_start_and_end_time() {
+ assert_eq!(parse_time_range("1:9"), Ok((Some(1), Some(9))));
+ }
+
+ #[test]
+ fn test_invalid_range() {
+ assert!(parse_time_range("").is_err());
+ assert!(parse_time_range("a").is_err());
+ assert!(parse_time_range("1:a").is_err());
+ assert!(parse_time_range("9:1").is_err());
+ }
+}