~ajk/vcd2v

1951895a3fc357642f48ddba001116e35ef05ba8 — Andrew Kay 3 years ago
Initial commit
7 files changed, 662 insertions(+), 0 deletions(-)

A .gitignore
A Cargo.lock
A Cargo.toml
A LICENSE
A README.md
A src/lib.rs
A src/main.rs
A  => .gitignore +1 -0
@@ 1,1 @@
/target

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());
    }
}