Last active
November 6, 2023 22:08
-
-
Save ddrcode/c35caa6c63e1104dcfff74d64424b18a to your computer and use it in GitHub Desktop.
Rust: electronic circuit model. Questions about design and architecture
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Electronic circuit model (PoC) | |
| // by ddrcode | |
| // | |
| // PROBLEM | |
| // The code below is my attempt to build a model of electronic circuits. | |
| // It tries to model a real world, where components (chips) are connected | |
| // with each other via pins: each change on output pins triggers an action | |
| // on corresponding input pins. | |
| // | |
| // APPROACH | |
| // The model is built with the concept of "reactive graph", where | |
| // graph nodes represent componets, and graph edges - connections between | |
| // pins. Each state change on output pin, triggers invocation of | |
| // on_pin_state_change method on the input pin and then on the corresponding | |
| // component. That component may then produce some output via its own output pins, | |
| // making the state change to propagate further. | |
| // | |
| // ARCHITECTURE | |
| // Instead of building actual graph, I represent the graph with three hashmaps: | |
| // one for all components, another one for all pins, and the final one - for | |
| // connections between pins. To identify components and pins I use string keys | |
| // (similarly as component/pin names on circuit schematics). | |
| // For simplicity of this ghist I use plenty of unwraps and there is no error | |
| // handling. This is a PoC, not a final code. | |
| // | |
| // EXAMPLE | |
| // The code below (main function) demonstrates a circuit that consist of two | |
| // components (clock [X1] and cpu [U1]) with a single connection between them | |
| // (pin names "out" abd "phi2"). Graph: | |
| // [Clock]: Out ----> Phi2: [Cpu] | |
| // | |
| // ISSUES / QUESTIONS | |
| // 1. The solution utilizes some form of observer pattern. In the result, the | |
| // callback function passed to every pin, must contain a reference to | |
| // the entire circuit. The conseqence of that is the use of Rc (from Circuit) | |
| // and RefCell (for components map). I'm curious whether there it's possible | |
| // to avoid either the callback or, at least, the Rc's and Refcell's | |
| // 2. Perhaps the reactive graph could be implemented with a help of some of existing | |
| // Rx-like crates for React. In general, I'd be interested in something | |
| // similar to reactive graph in R language (see here for details: | |
| // https://mastering-shiny.org/reactive-graph.html) | |
| // 3. I'd appreciate any any redesign suggestions and comments. | |
| use std::{ | |
| cell::{OnceCell, RefCell}, | |
| collections::HashMap, | |
| rc::Rc, | |
| }; | |
| // -------------------------------------------------------------------- | |
| // Model of a Pin (of electronic component) | |
| #[derive(Debug, PartialEq, Copy, Clone)] | |
| pub enum PinDirection { | |
| Input, | |
| Output, | |
| } | |
| #[derive(Clone)] | |
| pub struct Pin { | |
| name: String, | |
| value: RefCell<bool>, | |
| direction: RefCell<PinDirection>, | |
| handler: OnceCell<Rc<RefCell<dyn PinStateChange>>>, | |
| inner_id: OnceCell<u32>, | |
| } | |
| impl Pin { | |
| pub fn new(name: &str, direction: PinDirection) -> Self { | |
| Pin { | |
| name: name.to_string(), | |
| value: RefCell::new(false), | |
| direction: RefCell::new(direction), | |
| handler: OnceCell::new(), | |
| inner_id: OnceCell::new(), | |
| } | |
| } | |
| pub fn input(name: &str) -> Self { | |
| Pin::new(name, PinDirection::Input) | |
| } | |
| pub fn output(name: &str) -> Self { | |
| Pin::new(name, PinDirection::Output) | |
| } | |
| pub fn read(&self) -> bool { | |
| self.state() | |
| } | |
| pub fn val(&self) -> u8 { | |
| self.state().into() | |
| } | |
| pub fn state(&self) -> bool { | |
| *self.value.borrow() | |
| } | |
| pub fn direction(&self) -> PinDirection { | |
| *self.direction.borrow() | |
| } | |
| pub fn write(&self, val: bool) { | |
| if self.is_output() { | |
| *self.value.borrow_mut() = val; | |
| if let Some(handler) = self.handler.get() { | |
| handler.borrow_mut().on_pin_state_change(self); | |
| } | |
| } | |
| } | |
| pub fn set_val(&self, val: bool) { | |
| if !self.is_output() { | |
| *self.value.borrow_mut() = val; | |
| } | |
| } | |
| pub fn name(&self) -> &str { | |
| &self.name | |
| } | |
| pub fn is_output(&self) -> bool { | |
| self.direction() == PinDirection::Output | |
| } | |
| pub fn toggle(&self) { | |
| if self.is_output() { | |
| let v = self.state(); | |
| self.write(!v); | |
| } | |
| } | |
| pub(crate) fn set_inner_id(&self, id: u32) { | |
| let _ = self.inner_id.set(id); | |
| } | |
| pub(crate) fn set_handler(&self, handler: Rc<RefCell<dyn PinStateChange>>) { | |
| let _ = self | |
| .handler | |
| .set(handler) | |
| .map_err(|_| panic!("Handler already defined")); | |
| } | |
| } | |
| pub trait PinStateChange { | |
| fn on_pin_state_change(&mut self, pin: &Pin); | |
| } | |
| // -------------------------------------------------------------------- | |
| // Electronic component | |
| pub trait Component: PinStateChange { | |
| fn get_pin(&self, name: &str) -> Option<&Pin>; | |
| } | |
| // -------------------------------------------------------------------- | |
| // Componet example - clock with single output pin | |
| pub struct Clock { | |
| out: Pin, | |
| } | |
| impl Clock { | |
| pub fn new() -> Self { | |
| Clock { | |
| out: Pin::output("out"), | |
| } | |
| } | |
| pub fn tick(&self) { | |
| self.out.toggle(); | |
| } | |
| } | |
| impl Component for Clock { | |
| fn get_pin(&self, name: &str) -> Option<&Pin> { | |
| match name { | |
| "out" => Some(&self.out), | |
| _ => None, | |
| } | |
| } | |
| } | |
| impl PinStateChange for Clock { | |
| fn on_pin_state_change(&mut self, pin: &Pin) { | |
| todo!() | |
| } | |
| } | |
| // -------------------------------------------------------------------- | |
| // Componet example - CPU with a single input pin | |
| pub struct Cpu { | |
| phi2: Pin, | |
| } | |
| impl Cpu { | |
| pub fn new() -> Self { | |
| Cpu { | |
| phi2: Pin::output("phi2"), | |
| } | |
| } | |
| pub fn tick(&mut self) { | |
| println!("CPU is ticking!"); | |
| } | |
| } | |
| impl Component for Cpu { | |
| fn get_pin(&self, name: &str) -> Option<&Pin> { | |
| match name { | |
| "phi2" => Some(&self.phi2), | |
| _ => None, | |
| } | |
| } | |
| } | |
| impl PinStateChange for Cpu { | |
| fn on_pin_state_change(&mut self, pin: &Pin) { | |
| self.tick(); | |
| } | |
| } | |
| // -------------------------------------------------------------------- | |
| // Circuit | |
| // It owns all components and connections between them | |
| pub struct Circuit { | |
| pins: HashMap<u32, (String, String)>, | |
| connections: HashMap<u32, u32>, | |
| // QUESTION #1 | |
| // I use RefCell, because I need a mutable reference to | |
| // components - to call on_pin_state_change. | |
| // Are there any other ways to avoid it? | |
| components: HashMap<String, RefCell<Box<dyn Component>>>, | |
| } | |
| impl Circuit { | |
| pub fn component(&self, name: &str) -> &RefCell<Box<dyn Component>> { | |
| &self.components[name] | |
| } | |
| } | |
| // -------------------------------------------------------------------- | |
| // Circuit Pin handler | |
| // This is where the magic happens - it makes the circuit "reactive" | |
| // for every pin state change. | |
| struct CircuitPinHandler(Rc<Circuit>); | |
| impl PinStateChange for CircuitPinHandler { | |
| fn on_pin_state_change(&mut self, pin: &Pin) { | |
| let id = pin.inner_id.get().unwrap(); | |
| let (component_id, reader_pin_name) = { | |
| let circuit = &self.0; | |
| println!("Changing pin: {}, {}", pin.name, id); | |
| let reader_id = circuit.connections[id]; | |
| circuit.pins[&reader_id].clone() | |
| }; | |
| let rpin = { | |
| let c = self.0.components[&component_id].borrow(); | |
| let p = c.get_pin(&reader_pin_name).unwrap(); | |
| p.set_val(pin.state()); | |
| p.clone() | |
| }; | |
| // Would be great if I could use hasmap's get_mut here, | |
| // but I can't, because self.0 is a Rc<Circuit> | |
| // QUESTION #2: is there any option to fix it? | |
| let mut component = self.0.components[&component_id].borrow_mut(); | |
| println!( | |
| "Reader pin: {}, {}", | |
| rpin.name(), | |
| rpin.inner_id.get().unwrap() | |
| ); | |
| println!("Updating compoent {}", component_id); | |
| component.on_pin_state_change(&rpin); | |
| } | |
| } | |
| // -------------------------------------------------------------------- | |
| // CircuitBuilder | |
| // Helps building circuits | |
| struct CircuitBuilder { | |
| components: Option<HashMap<String, RefCell<Box<dyn Component>>>>, | |
| pins: HashMap<u32, (String, String)>, | |
| last_pin_id: u32, | |
| connections: HashMap<u32, u32>, | |
| } | |
| impl CircuitBuilder { | |
| pub fn new() -> Self { | |
| CircuitBuilder { | |
| components: Some(HashMap::new()), | |
| pins: HashMap::new(), | |
| last_pin_id: 0, | |
| connections: HashMap::new(), | |
| } | |
| } | |
| pub fn add_component(&mut self, name: &str, cmp: impl Component + 'static) -> &mut Self { | |
| self.components | |
| .as_mut() | |
| .unwrap() | |
| .insert(name.to_string(), RefCell::new(Box::new(cmp))); | |
| self | |
| } | |
| fn add_pin(&mut self, component_name: &str, pin_name: &str) -> u32 { | |
| self.pins.insert( | |
| self.last_pin_id, | |
| (component_name.to_string(), pin_name.to_string()), | |
| ); | |
| self.last_pin_id += 1; | |
| self.last_pin_id - 1 | |
| } | |
| fn add_connection(&mut self, writer_id: u32, reader_id: u32) { | |
| self.connections.insert(writer_id, reader_id); | |
| } | |
| pub fn link( | |
| &mut self, | |
| writer_name: &str, | |
| writer_pin_name: &str, | |
| reader_name: &str, | |
| reader_pin_name: &str, | |
| ) -> &mut Self { | |
| let writer_id = self.add_pin(writer_name, writer_pin_name); | |
| let reader_id = self.add_pin(reader_name, reader_pin_name); | |
| self.add_connection(writer_id, reader_id); | |
| self | |
| } | |
| pub fn build(&mut self) -> Rc<Circuit> { | |
| let c = Circuit { | |
| pins: self.pins.clone(), | |
| connections: self.connections.clone(), | |
| components: self.components.take().unwrap(), | |
| }; | |
| // QUESTION #3 | |
| // Is there a way to make it more elegant? | |
| // I need to use Rc, as I need to pass the reference to the entire Circuit | |
| // to the CircuitPinHandler handler. And there is RefCell, as | |
| // on_pin_state_change requires mutable self | |
| let cref = Rc::new(c); | |
| let handler = Rc::new(RefCell::new(CircuitPinHandler(Rc::clone(&cref)))); | |
| cref.connections.iter().for_each(|(key, rkey)| { | |
| let data = &cref.pins[key]; | |
| let component = cref.components[&data.0].borrow(); | |
| let pin = component.get_pin(&data.1).unwrap(); | |
| // "injecting" handler to all pins | |
| // QUESTION #4 | |
| // Achieving the same functionality without callback would, most likely, | |
| // result in a cleaner code. But is there an alternative to it? | |
| pin.set_handler(Rc::clone(&handler) as Rc<RefCell<dyn PinStateChange>>); | |
| pin.set_inner_id(*key); | |
| let data = &cref.pins[rkey]; | |
| let component = cref.components[&data.0].borrow(); | |
| let pin = component.get_pin(&data.1).unwrap(); | |
| pin.set_inner_id(*rkey); | |
| }); | |
| cref | |
| } | |
| } | |
| // -------------------------------------------------------------------- | |
| fn main() { | |
| let mut c = CircuitBuilder::new(); | |
| c.add_component("X1", Clock::new()) | |
| .add_component("U1", Cpu::new()) | |
| .link("X1", "out", "U1", "phi2"); | |
| let circuit = c.build(); | |
| // the code below enforces signal change on clock's "out" pin. | |
| // in the result CPU input pin changes it's state | |
| // and in the consequence - CPU .tick method is called | |
| circuit | |
| .component("X1") | |
| .borrow() | |
| .get_pin("out") | |
| .unwrap() | |
| .toggle(); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment