├── .gitignore ├── imgs └── sequence.png ├── Cargo.lock ├── Cargo.toml ├── src └── main.rs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /imgs/sequence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcoury/emulator-bus-minimal/master/imgs/sequence.png -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "cyclic_dependency" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "cyclic_dependency" 4 | version = "0.1.0" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, rc::Rc}; 2 | 3 | enum Message { 4 | CpuStep, 5 | ReadByte(u16), 6 | ReadByteResponse(u8), 7 | VdpEnableLineInterrupt, 8 | CpuEnableInterrupt, 9 | } 10 | 11 | struct Machine { 12 | bus: Bus, 13 | } 14 | 15 | impl Machine { 16 | fn new() -> Self { 17 | let queue = Rc::new(RefCell::new(Vec::new())); 18 | let cpu = Cpu::new(queue.clone()); 19 | let vdp = Vdp::new(queue.clone()); 20 | let ppi = Ppi {}; 21 | let bus = Bus::new(cpu, vdp, ppi, queue); 22 | Self { bus } 23 | } 24 | 25 | fn step(&mut self) { 26 | self.bus.step(); 27 | } 28 | } 29 | 30 | struct Bus { 31 | cpu: Cpu, 32 | vdp: Vdp, 33 | ppi: Ppi, 34 | queue: Rc>>, 35 | } 36 | 37 | impl Bus { 38 | fn new(cpu: Cpu, vdp: Vdp, ppi: Ppi, queue: Rc>>) -> Self { 39 | Self { 40 | cpu, 41 | vdp, 42 | ppi, 43 | queue, 44 | } 45 | } 46 | 47 | fn step(&mut self) { 48 | println!("Bus step"); 49 | self.queue.borrow_mut().push(Message::CpuStep); 50 | 51 | loop { 52 | let Some(message) = self.queue.borrow_mut().pop() else { 53 | break; 54 | }; 55 | 56 | match message { 57 | Message::CpuStep => { 58 | self.cpu.step(); 59 | } 60 | Message::ReadByte(addr) => { 61 | let data = self.ppi.read_byte(addr); 62 | self.queue 63 | .borrow_mut() 64 | .push(Message::ReadByteResponse(data)); 65 | } 66 | Message::ReadByteResponse(data) => { 67 | // How do I get the data to the External CPU? 68 | } 69 | Message::VdpEnableLineInterrupt => { 70 | self.vdp.enable_line_interrupt(); 71 | } 72 | Message::CpuEnableInterrupt => { 73 | self.cpu.enable_interrupt(); 74 | } 75 | } 76 | } 77 | } 78 | } 79 | 80 | struct Cpu { 81 | ext_cpu: ExtCpu, 82 | } 83 | 84 | impl Cpu { 85 | fn new(queue: Rc>>) -> Self { 86 | let io = Io::new(queue); 87 | let ext_cpu = ExtCpu { io }; 88 | Self { ext_cpu } 89 | } 90 | 91 | fn step(&mut self) { 92 | println!("Cpu step"); 93 | self.ext_cpu.step(); 94 | } 95 | 96 | fn enable_interrupt(&self) { 97 | println!("Cpu enable_interrupt"); 98 | self.ext_cpu.enable_interrupt(); 99 | } 100 | } 101 | 102 | struct Ppi; 103 | 104 | impl Ppi { 105 | fn read_byte(&self, addr: u16) -> u8 { 106 | println!("Ppi read_byte {:x}", addr); 107 | 0xfe 108 | } 109 | } 110 | 111 | struct Vdp { 112 | queue: Rc>>, 113 | } 114 | 115 | impl Vdp { 116 | fn new(queue: Rc>>) -> Self { 117 | Self { queue } 118 | } 119 | 120 | fn enable_line_interrupt(&self) { 121 | println!("Vdp enable_line_interrupt"); 122 | self.queue.borrow_mut().push(Message::CpuEnableInterrupt); 123 | } 124 | } 125 | 126 | trait ExtCpuIo { 127 | fn read(&self, addr: u16) -> u8; 128 | fn write(&self, addr: u16, data: u8); 129 | fn read_port(&self, port: u8) -> u8; 130 | fn write_port(&self, port: u8, data: u8); 131 | } 132 | 133 | struct ExtCpu { 134 | io: ExtCpuIo, 135 | } 136 | 137 | impl ExtCpu { 138 | fn step(&mut self) { 139 | println!("ExtCpu step"); 140 | if self.io.read(0) == 0 { 141 | self.io.write_port(0, 1); 142 | } 143 | } 144 | 145 | fn enable_interrupt(&self) { 146 | println!("ExtCpu enable_interrupt"); 147 | self.io.write_port(1, 1); 148 | } 149 | } 150 | 151 | struct Io { 152 | queue: Rc>>, 153 | } 154 | 155 | impl Io { 156 | fn new(queue: Rc>>) -> Self { 157 | Self { queue } 158 | } 159 | } 160 | 161 | impl ExtCpuIo for Io { 162 | fn read(&self, addr: u16) -> u8 { 163 | println!("ExtCpuIo read: {:x}", addr); 164 | self.queue.borrow_mut().push(Message::ReadByte(0x0042)); 165 | todo!("How do I get the u8 to respond the External CPU?") 166 | } 167 | 168 | fn write(&self, addr: u16, data: u8) { 169 | println!("write: {:x} {:x}", addr, data); 170 | } 171 | 172 | fn read_port(&self, port: u8) -> u8 { 173 | println!("ExtCpuIo read_port: {:x}", port); 174 | todo!() 175 | } 176 | 177 | fn write_port(&self, port: u8, data: u8) { 178 | println!("ExtCpuIo write_port: {:x} {:x}", port, data); 179 | if port == 0x00 { 180 | self.queue 181 | .borrow_mut() 182 | .push(Message::VdpEnableLineInterrupt); 183 | } 184 | } 185 | } 186 | 187 | fn main() { 188 | let mut machine = Machine::new(); 189 | machine.step(); 190 | } 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Modeling Bus and its components in Rust emulator 2 | 3 | [Rust Forum Link](https://users.rust-lang.org/t/modeling-bus-and-its-components-in-rust-emulator/92583) 4 | 5 | I am writing an emulator and am having a hard time modeling a solution for this problem. Below is what I have so far. I am sure there's a better way to model this problem, and that's exactly what I am looking for. 6 | 7 | I have published the example code here: https://github.com/fcoury/emulator-bus-minimal 8 | And there's a [Rust playground link](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=6c1e5f19cf62bfe6aece4f27ef0beeb9) as well. 9 | 10 | I am trying to model the following scenario: 11 | 12 | ## Structs 13 | 14 | - Machine: is the high level abstraction of the hardware, talks via the BUS 15 | - BUS: the orchestrator, talks to the CPU via stepping to next instruction and to the VDP via writing to a given port 16 | - CPU: the CPU, talks to memory via the BUS (not represented here) and to the VDP via the BUS by writing to a given I/O port 17 | - VDP: the graphics chip with VRAM and registers, needs to talk to the CPU to enable/disable interrupts 18 | - PPI: the Programmable Peripheral Interface, talks to the CPU via the BUS 19 | 20 | ### Flow 21 | 22 | - Machine asks CPU to step to next instruction via the BUS 23 | - CPU reads a given memory address and writes to a given VDP port via the BUS 24 | - VDP receives the write and enables interrupts on CPU via the BUS 25 | 26 | Here's a sequence diagram illustrating the complete flow: 27 | 28 | ![Sequence Diagram](imgs/sequence.png) 29 | 30 | ### Problem 31 | 32 | I have one component after the CPU (Z80 implementation) that I don't control. This component needs to read memory via the bus, and for that I need a reentrant lock to the bus. 33 | 34 | Back when I was using Rc>, the flow that manifestates the problem was somewhat like below: 35 | 36 | ``` 37 | Machine.step -> 38 | Bus.step [borrows Bus mutably] -> 39 | Cpu.step [borrows CPU mutably] -> 40 | .step -> 41 | Io.read_byte [borrows BUS mutably] -> 42 | panic 43 | ``` 44 | 45 | And I was getting this error: 46 | 47 | ``` 48 | thread 'main' panicked at 'already borrowed: BorrowMutError', src/main.rs:61:18 49 | ``` 50 | 51 | ```rust 52 | fn step(&mut self) { 53 | println!("CPU step"); 54 | self.bus.borrow_mut().write_port(); // <- here 55 | } 56 | ``` 57 | 58 | ### What I tried so far: 59 | 60 | - Weak BUS references to CPU/VDP, returning the concrete implementation to the Machine, but the strong reference ended up being dropped 61 | - Returning and keeping the Rc> to the Machine, and we follow on this reentrant problem 62 | - Refactoring the code to use what **semicoleon** suggested [here](https://users.rust-lang.org/t/modeling-bus-and-its-components-in-rust-emulator/92583/3) 63 | 64 | > Well one simple option that doesn't really fix your bigger design question is to just not borrow Bus mutably. It's entire purpose is to be a shared communication channel, so it would likely make more sense to have the Cpu and Vdp in the Bus be inside RefCells instead of the Bus. You're much less likely to have a legitimate reason to borrow Cpu/Vdp mutably twice in the same call stack. 65 | > One alternate way to model a system like this is via message passing. Instead of making calls directly you send messages between components. In some ways that's closer to how the actual hardware functions, though it can be quite annoying to write code that way. Here's a sketch of how that might work based on that sample code 66 | 67 | And [here](https://users.rust-lang.org/t/modeling-bus-and-its-components-in-rust-emulator/92583/8): 68 | 69 | > Responses would be most easily modeled by sending a new message back to the original component. 70 | > You could use something like a response channel, but unless each component has a thread and is waiting on new messages, that doesn't really help you much. You still need to get back to the point where the component checks the channel. 71 | 72 | ### What I think I need 73 | 74 | Change the `ExtCpuIo.read` method to be two-phased: 75 | 76 | ```rust 77 | fn read(&self, addr: u16) -> u8 { 78 | println!("ExtCpuIo read: {:x}", addr); 79 | self.queue.borrow_mut().push(Message::ReadByte(0x0042)); 80 | let reply = self.queue.process(); // how!? :-) 81 | if let Message::ReadByteReply(reply) = reply { 82 | reply 83 | } else { 84 | panic!("Unexpected reply"); 85 | } 86 | } 87 | ``` 88 | 89 | Here, `queue.process` would have to talk to the bus and wait for a reply. I am not sure how to model this. 90 | 91 | ```rust 92 | fn process(&self) -> Message { 93 | let msg = self.pop().unwrap(); 94 | match msg { 95 | Message::ReadByte(addr) => { 96 | // ??? 97 | let reply = self.bus.borrow_mut().read_byte(addr); 98 | Message::ReadByteReply(reply) 99 | } 100 | _ => panic!("Unexpected message"), 101 | } 102 | } 103 | ``` 104 | 105 | The code as I have it so far is in this repo, be more than welcome to open a PR, or any way you find suitable. Thank you! 106 | --------------------------------------------------------------------------------