From b39b209b253c60afea5a9fe662a7ea94d59a6c06 Mon Sep 17 00:00:00 2001 From: Seth Pellegrino Date: Sun, 18 Dec 2022 09:59:11 -0800 Subject: [PATCH 1/4] wip: proof-of-concept for "robosim" A "discrete event simulator" model of our circuit(s) so we can ask questions like "which invariants are we violating?" and maybe even "what did we decide to do about the SPI clock? why?" --- .vscode/settings.json | 7 +++ robosim/.gitignore | 2 + robosim/Cargo.lock | 25 +++++++++ robosim/Cargo.toml | 9 ++++ robosim/src/main.rs | 115 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 158 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 robosim/.gitignore create mode 100644 robosim/Cargo.lock create mode 100644 robosim/Cargo.toml create mode 100644 robosim/src/main.rs diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b5854e9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + // see: https://github.com/rust-lang/rust-analyzer/issues/2649 + "rust-analyzer.linkedProjects": [ + // "robogen/Cargo.toml", // shhh it's a secret that this exists in the other branch + "robosim/Cargo.toml", + ] +} diff --git a/robosim/.gitignore b/robosim/.gitignore new file mode 100644 index 0000000..8130c3a --- /dev/null +++ b/robosim/.gitignore @@ -0,0 +1,2 @@ +debug/ +target/ diff --git a/robosim/Cargo.lock b/robosim/Cargo.lock new file mode 100644 index 0000000..444b3bc --- /dev/null +++ b/robosim/Cargo.lock @@ -0,0 +1,25 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "fugit" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab17bb279def6720d058cb6c052249938e7f99260ab534879281a95367a87e5" +dependencies = [ + "gcd", +] + +[[package]] +name = "gcd" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4b1b088ad0a967aa29540456b82fc8903f854775d33f71e9709c4efb3dfbfd2" + +[[package]] +name = "robosim" +version = "0.1.0" +dependencies = [ + "fugit", +] diff --git a/robosim/Cargo.toml b/robosim/Cargo.toml new file mode 100644 index 0000000..96c667f --- /dev/null +++ b/robosim/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "robosim" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +fugit = "0.3.6" diff --git a/robosim/src/main.rs b/robosim/src/main.rs new file mode 100644 index 0000000..a9525f9 --- /dev/null +++ b/robosim/src/main.rs @@ -0,0 +1,115 @@ +use std::fmt::Debug; + +#[derive(Debug, Clone, Copy)] +pub enum Signal { + High, + Low, + Z, +} + +impl Signal { + pub fn invert(&self) -> Signal { + match &self { + Signal::High => Signal::Low, + Signal::Low => Signal::High, + + Signal::Z => todo!("what's the inverse of high-Z? more high-Z probably? Or, do clocks run on a different Signal that can't be high-Z?"), + } + } +} + +#[derive(Debug, Clone)] +pub struct Bus { + lines: Vec, +} + +#[derive(Debug, Clone)] +pub enum State { + Signal(Signal), + Bus(Bus), +} + +// fugit::Instant can only do u32, u64. Do we need signed? idk +// type Instant = fugit::Instant; + +type Instant = i64; // what, femtoseconds? +type Duration = i64; // what, femtoseconds? +type Step = i64; // what, femtoseconds? + +// see https://stackoverflow.com/questions/27886474/recursive-function-type +// TODO: generator? https://doc.rust-lang.org/beta/unstable-book/language-features/generators.html +pub struct StateFn(Box (State, Vec<(Step, StateFn)>)>); + +// TODO: would something like this be better? +#[allow(dead_code)] +pub struct _State { + state: State, + next: Box Vec<(Step, _State)>>, +} + +fn main() { + // big idea: event-based simulator that tracks history, + // which gets mapped to https://github.com/wavedrom/schema/blob/master/WaveJSON.md + + // things to model: + // clock (square wave) + // inverter (non-GAL) + // SIPO (pretending it's not a GAL) + // FIFO + + // An ideal square clock + // (do we need to model rise/fall times?) + fn make_clock(initial_state: Signal, pulse_width: Duration) -> StateFn { + let clock = move || -> (State, Vec<(Step, StateFn)>) { + return ( + State::Signal(initial_state), + vec![(pulse_width, make_clock(initial_state.invert(), pulse_width))], + ); + }; + return StateFn(Box::new(clock)); + } + + let mut total_steps = 6; + let mut simulate = move || -> bool { + total_steps -= 1; + total_steps > 0 + }; + + // simulation state + let mut now: Instant = 0; + // TODO: multiple current states, with labels + let mut clock = make_clock(Signal::High, 12_500_000 /* 80MHz */); + let mut history = vec![]; + while simulate() { + let (state, mut schedule) = clock.0(); + history.push(("clock", now, state)); + + let (step, next) = schedule.pop().unwrap(); + now += step; + clock = next; + } + + for (label, ts, state) in history { + println!("{} t={:9} {:?}", label, ts, state); + } +} + +// some reading: +// - https://en.wikipedia.org/wiki/Discrete-event_simulation +// - https://dev.to/elshize/type-safe-discrete-simulation-in-rust-3n7d + +// more reading: +// - https://en.wikipedia.org/wiki/Hardware_description_language +// - https://en.wikipedia.org/wiki/Register-transfer_level +// - https://www.oreilly.com/library/view/introduction-to-digital/9780470900550/chap4-sec003.html +// - https://en.wikipedia.org/wiki/VHDL#Design +// - https://en.wikipedia.org/wiki/Calendar_queue + +// lots more reading: +// - https://my.eng.utah.edu/~kstevens/docs/vlsid16.pdf +// - https://www.sciencedirect.com/topics/computer-science/timing-verification + +// crates (none of these feel quite right): +// - https://github.com/garro95/desim +// - https://github.com/ndebuhr/sim +// a whole bunch of possibilities from https://lib.rs/simulation From 2c94850669019b8b041146dd5cb24b3c24d5e9f6 Mon Sep 17 00:00:00 2001 From: Seth Pellegrino Date: Tue, 20 Dec 2022 07:43:01 -0800 Subject: [PATCH 2/4] wip(does-not-compile): trying to wire up a network Attempting to connect an inverter that will delay changing its output by 6ns to my 80MHz clock, but Rust does not believe what I'm doing is safe. Maybe it even isn't? --- robosim/src/main.rs | 116 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 96 insertions(+), 20 deletions(-) diff --git a/robosim/src/main.rs b/robosim/src/main.rs index a9525f9..a9d3097 100644 --- a/robosim/src/main.rs +++ b/robosim/src/main.rs @@ -1,4 +1,4 @@ -use std::fmt::Debug; +use std::{fmt::Debug, vec}; #[derive(Debug, Clone, Copy)] pub enum Signal { @@ -18,16 +18,16 @@ impl Signal { } } -#[derive(Debug, Clone)] -pub struct Bus { - lines: Vec, -} +// #[derive(Debug, Clone)] +// pub struct Bus { +// lines: Vec, +// } -#[derive(Debug, Clone)] -pub enum State { - Signal(Signal), - Bus(Bus), -} +// #[derive(Debug, Clone)] +// pub enum State { +// Signal(Signal), +// Bus(Bus), +// } // fugit::Instant can only do u32, u64. Do we need signed? idk // type Instant = fugit::Instant; @@ -38,15 +38,50 @@ type Step = i64; // what, femtoseconds? // see https://stackoverflow.com/questions/27886474/recursive-function-type // TODO: generator? https://doc.rust-lang.org/beta/unstable-book/language-features/generators.html -pub struct StateFn(Box (State, Vec<(Step, StateFn)>)>); +pub struct StateFn { + f: Box (Signal, Vec<(Step, StateFn)>)>, +} // TODO: would something like this be better? #[allow(dead_code)] pub struct _State { - state: State, + state: Signal, next: Box Vec<(Step, _State)>>, } +// TODO: or this? +#[allow(dead_code)] +pub struct PinState { + pub state: Signal, + pub next: Box Vec<(Step, PinState)>>, +} + +pub type Pins = Vec<(String, PinState)>; + +// "wire"s the output into the given inputs, forming a node in the wiring DAG +// like clojure's `->` form +// TODO: circuits, definitionally, are not DAGs? +pub fn wire1( + output: StateFn, + mut inputs: Vec Vec<(Step, StateFn)>>>, +) -> StateFn { + StateFn { + f: Box::new(move || -> (Signal, Vec<(Step, StateFn)>) { + let (signal, steps) = (output.f)(); + + let downstream: Vec<_> = inputs.iter_mut().map(|f| f(signal)).flatten().collect(); + + let mut rewired: Vec<_> = steps + .into_iter() + .map(move |(step, f)| (step, wire1(f, inputs))) + .collect(); + + rewired.extend(downstream); + (signal, rewired) + }), + } +} + fn main() { // big idea: event-based simulator that tracks history, // which gets mapped to https://github.com/wavedrom/schema/blob/master/WaveJSON.md @@ -60,13 +95,50 @@ fn main() { // An ideal square clock // (do we need to model rise/fall times?) fn make_clock(initial_state: Signal, pulse_width: Duration) -> StateFn { - let clock = move || -> (State, Vec<(Step, StateFn)>) { - return ( - State::Signal(initial_state), + let clock = move || -> _ { + ( + initial_state, vec![(pulse_width, make_clock(initial_state.invert(), pulse_width))], - ); + ) }; - return StateFn(Box::new(clock)); + return StateFn { f: Box::new(clock) }; + } + + fn make_clock2(initial_state: Signal, pulse_width: Duration) -> Pins { + fn clock( + state: Signal, + pulse_width: Duration, + ) -> Box Vec<(Step, PinState)>)> { + Box::new(move || { + return vec![( + pulse_width, + PinState { + state: state, + next: Box::new(clock(state.invert(), pulse_width)), + }, + )]; + }) + } + return vec![( + "clock".to_string(), + PinState { + state: initial_state, + next: clock(initial_state.invert(), pulse_width), + }, + )]; + } + + fn make_inverter() -> Box Vec<(Step, StateFn)>> { + Box::new(move |input: Signal| -> _ { + vec![( + 6_000_000, // 6ns gate delay + StateFn { + f: Box::new(move || -> (Signal, Vec<(Step, StateFn)>) { + (input.invert(), vec![]) + }), + }, + )] + }) } let mut total_steps = 6; @@ -78,15 +150,19 @@ fn main() { // simulation state let mut now: Instant = 0; // TODO: multiple current states, with labels - let mut clock = make_clock(Signal::High, 12_500_000 /* 80MHz */); + // TODO: determinism within a run & across multiple runs? + let clock = make_clock(Signal::High, 12_500_000 /* 80MHz */); + + let mut root = wire1(clock, vec![make_inverter()]); + let mut history = vec![]; while simulate() { - let (state, mut schedule) = clock.0(); + let (state, mut schedule) = (root.f)(); history.push(("clock", now, state)); let (step, next) = schedule.pop().unwrap(); now += step; - clock = next; + root = next; } for (label, ts, state) in history { From a967b7edbdb084e4c1ea3fee916226075b7b262e Mon Sep 17 00:00:00 2001 From: Seth Pellegrino Date: Tue, 20 Dec 2022 09:19:11 -0800 Subject: [PATCH 3/4] chore: move experimental (dead) code --- robosim/src/main.rs | 46 ++++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/robosim/src/main.rs b/robosim/src/main.rs index a9d3097..b1190b8 100644 --- a/robosim/src/main.rs +++ b/robosim/src/main.rs @@ -58,6 +58,28 @@ pub struct PinState { pub type Pins = Vec<(String, PinState)>; +#[allow(dead_code)] +fn make_clock2(initial_state: Signal, pulse_width: Duration) -> Pins { + fn clock(state: Signal, pulse_width: Duration) -> Box Vec<(Step, PinState)>)> { + Box::new(move || { + return vec![( + pulse_width, + PinState { + state: state, + next: Box::new(clock(state.invert(), pulse_width)), + }, + )]; + }) + } + return vec![( + "clock".to_string(), + PinState { + state: initial_state, + next: clock(initial_state.invert(), pulse_width), + }, + )]; +} + // "wire"s the output into the given inputs, forming a node in the wiring DAG // like clojure's `->` form // TODO: circuits, definitionally, are not DAGs? @@ -104,30 +126,6 @@ fn main() { return StateFn { f: Box::new(clock) }; } - fn make_clock2(initial_state: Signal, pulse_width: Duration) -> Pins { - fn clock( - state: Signal, - pulse_width: Duration, - ) -> Box Vec<(Step, PinState)>)> { - Box::new(move || { - return vec![( - pulse_width, - PinState { - state: state, - next: Box::new(clock(state.invert(), pulse_width)), - }, - )]; - }) - } - return vec![( - "clock".to_string(), - PinState { - state: initial_state, - next: clock(initial_state.invert(), pulse_width), - }, - )]; - } - fn make_inverter() -> Box Vec<(Step, StateFn)>> { Box::new(move |input: Signal| -> _ { vec![( From 3596fb4aae86ad2e660820fc3b3bf4a2bb4100d4 Mon Sep 17 00:00:00 2001 From: Seth Pellegrino Date: Tue, 20 Dec 2022 09:19:13 -0800 Subject: [PATCH 4/4] wip(fix): restrict wire1 inputs to unitary, poorly In order to achieve the goal of 2c94850 without too much wrangling, we restrict things that are inputs into a node to only produce a single future scheduled event so we can avoid needing to possibly call downstream consumers more than once in the future. Alternatives include: * Possibly restricting consumers to being Fn (vs FnMut), and passing around lots of references with *hand wave* the correct lifetimes * Instead of taking consumers, take a FnMut that creates the consumers, (which can then all be FnOnces again), calling as many times as necessary. Also, it'd probably have to return a copy of itself for "rewiring", too But probably the most fruitful: * Stop holding Rust like it was Haskell, and make `struct Node {...}` that holds the (mutable) state of a node in the graph. (why is it always graphs?) Additionally, and more reason to do the above, the simulator engine now has to recognize the mapping between the schedule (in its entirety) and which nodes are updated at which point for saving into the history. This is probably fine (if not ideal) for a ~5 component graph, but unworkable for a general simulator. --- robosim/src/main.rs | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/robosim/src/main.rs b/robosim/src/main.rs index b1190b8..3e975cf 100644 --- a/robosim/src/main.rs +++ b/robosim/src/main.rs @@ -89,14 +89,17 @@ pub fn wire1( ) -> StateFn { StateFn { f: Box::new(move || -> (Signal, Vec<(Step, StateFn)>) { - let (signal, steps) = (output.f)(); + let (signal, mut steps) = (output.f)(); let downstream: Vec<_> = inputs.iter_mut().map(|f| f(signal)).flatten().collect(); - let mut rewired: Vec<_> = steps - .into_iter() - .map(move |(step, f)| (step, wire1(f, inputs))) - .collect(); + // let mut rewired: Vec<_> = steps + // .into_iter() + // .map(move |(step, f)| (step, wire1(f, inputs))) + // .collect(); + let (delay, clock) = steps.pop().expect("a single clock element"); + + let mut rewired = vec![(delay, wire1(clock, inputs))]; rewired.extend(downstream); (signal, rewired) @@ -148,9 +151,10 @@ fn main() { // simulation state let mut now: Instant = 0; // TODO: multiple current states, with labels - // TODO: determinism within a run & across multiple runs? + // TODO: determinism within a run & across multiple runs (i.e. seeded random)? let clock = make_clock(Signal::High, 12_500_000 /* 80MHz */); + // TODO: what's the inverter's initial state? let mut root = wire1(clock, vec![make_inverter()]); let mut history = vec![]; @@ -158,13 +162,17 @@ fn main() { let (state, mut schedule) = (root.f)(); history.push(("clock", now, state)); - let (step, next) = schedule.pop().unwrap(); - now += step; + let (inv_step, next) = schedule.pop().unwrap(); + let (state, _) = (next.f)(); + history.push(("inv", now + inv_step, state)); + + let (clock_step, next) = schedule.pop().unwrap(); + now += clock_step; root = next; } for (label, ts, state) in history { - println!("{} t={:9} {:?}", label, ts, state); + println!("t={:9} {:8} {:?}", ts, label, state); } }