Skip to content

Instantly share code, notes, and snippets.

@ddrcode
Last active November 6, 2023 22:08
Show Gist options
  • Select an option

  • Save ddrcode/c35caa6c63e1104dcfff74d64424b18a to your computer and use it in GitHub Desktop.

Select an option

Save ddrcode/c35caa6c63e1104dcfff74d64424b18a to your computer and use it in GitHub Desktop.
Rust: electronic circuit model. Questions about design and architecture
// 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