1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295
/*
ral1243: Emulator program as an example implementation for the z80emu library.
Copyright (C) 2019-2024 Rafal Michalski
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Author contact information: see Cargo.toml file, section [package.authors].
*/
//! RAL 1243 is a fictional computer brought into its virtual existence to provide
//! an example of how to implement emulators based on the [Z80 CPU emulator].
//!
//! [Z80 CPU emulator]: https://github.com/royaltm/rust-z80emu
#![cfg_attr(not(feature = "std"), no_std)]
pub mod bus;
pub mod clock;
pub mod ctc;
pub mod ctc_trigger;
pub mod debug;
pub mod memory;
pub mod pio;
pub mod pio_device;
mod runner;
#[cfg(feature = "std")]
mod thread;
#[cfg(not(feature = "std"))]
extern crate alloc;
#[cfg(feature = "std")]
pub(crate) use std::{boxed, rc, vec};
#[cfg(not(feature = "std"))]
pub(crate) use alloc::{boxed, rc, vec};
#[cfg(feature = "std")]
use std::path::Path;
#[cfg(feature = "std")]
use std::{io, fs};
#[cfg(feature = "std")]
use io::Read;
use z80emu::{Z80, z80::Flavour, CpuDebug};
use bus::{Bus, Terminator};
use ctc::Ctc;
use ctc_trigger::{CtcActive, CtcPassive};
use memory::{Rom, Memory};
use pio::Pio;
use pio_device::{PioInput, PioOutput};
pub use clock::Ts;
pub use pio_device::{PioStream, PioSink};
pub use runner::FrameRunner;
#[cfg(feature = "std")]
pub use thread::RunnerMsg;
const MAX_PULSES: usize = 4;
/// `PIO Z8420` device definition for `Ral1243`.
pub type PioT<I, O> = Pio<Ts, PioInput<I>, PioOutput<O>, Terminator<Ts>>;
/// `CTC Z8430` device definition for `Ral1243`.
pub type CtcT<I, O> = Ctc<Ts,
CtcActive<MAX_PULSES>,
CtcPassive<MAX_PULSES>,
CtcActive<MAX_PULSES>,
CtcPassive<MAX_PULSES>,
PioT<I, O>>;
/// `Ral1243` memory and peripherals.
pub type BusT<I, O> = Bus<CtcT<I, O>, Memory>;
/// Memory control I/O port mask.
pub const MEMORY_PORT_MASK: u16 = 0b11111111;
/// Memory control I/O port address.
pub const MEMORY_PORT_BITS: u16 = 124;
/// PIO peripheral I/O port mask.
pub const PIO_PORT_MASK: u16 = 0b11111100;
/// PIO peripheral I/O port address.
pub const PIO_PORT_BITS: u16 = 0b00001000; // 8,9,10,11
/// PIO peripheral I/O port mask bit number selecting a PIO channel.
pub const PIO_PORT_CHANNEL_SELECT: u32 = 1;
/// PIO peripheral I/O port mask bit number selecting between data and control access.
pub const PIO_PORT_CONTROL_SELECT: u32 = 0;
/// CTC peripheral I/O port mask.
pub const CTC_PORT_MASK: u16 = 0b11111100;
/// PIO peripheral I/O port address.
pub const CTC_PORT_BITS: u16 = 0b00000100; // 4,5,6,7
/// PIO peripheral I/O port mask MSB bit number selecting a CTC channel.
pub const CTC_PORT_CHANNEL_SELECT1: u32 = 1;
/// PIO peripheral I/O port mask LSB bit number selecting a CTC channel.
pub const CTC_PORT_CHANNEL_SELECT0: u32 = 0;
/// `Ral1243` ROM binary.
pub const ROM: &[u8] = include_bytes!("../rom/rom.bin");
/// `Ral1243` EX-ROM 1 binary.
pub const EX_ROM001: &[u8] = include_bytes!("../exroms/exrom001.bin");
/// `Ral1243` EX-ROM 2 binary.
pub const EX_ROM002: &[u8] = include_bytes!("../exroms/exrom002.bin");
/// The `Ral1243` computer.
///
/// Requires one of `Z80` [`Flavour`]s and implementations of [PioStream] and [PioSink].
///
/// * `EXT_HZ`: external clock frequency (for `CTC`) in Hz.
/// * `FRAME_HZ`: in how many frames per second the emulation will run.
pub struct Ral1243<F: Flavour, I: PioStream, O: PioSink,
const EXT_HZ: u32 = 10_000,
const FRAME_HZ: u32 = 500> {
runner: FrameRunner<EXT_HZ, FRAME_HZ>,
cpu: Z80<F>,
bus: BusT<I, O>,
}
/// Read `EX-ROMS` from a directory. `std` only.
#[cfg(feature = "std")]
pub fn read_exroms<P: AsRef<Path>>(dir: P) -> io::Result<Vec<Rom>> {
let mut vec = Vec::new();
let mut buf = Vec::with_capacity(Memory::ROMSIZE);
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
if let Some(name) = entry.file_name().to_str() {
if name.len() == 12 && name.starts_with("exrom") && name.ends_with(".bin") {
let mut f = fs::File::open(&path)?;
let metadata = f.metadata()?;
if metadata.len() > Memory::ROMSIZE as u64 {
return Err(io::Error::new(io::ErrorKind::Other, format!("Exrom is too big: {:?}", path)));
}
buf.clear();
f.read_to_end(&mut buf)?;
vec.push(Memory::make_rom(&buf));
}
}
}
}
Ok(vec)
}
/// Create an `EX-ROM` from a slice.
pub fn exrom_from_slice(exrom: &[u8]) -> Rom {
Memory::make_rom(exrom)
}
impl<F: Flavour, I: PioStream, O: PioSink,
const EXT_HZ: u32, const FRAME_HZ: u32> Ral1243<F, I, O, EXT_HZ, FRAME_HZ>
{
/// A real-time duration of a single emulated frame.
pub fn frame_duration() -> core::time::Duration {
FrameRunner::<EXT_HZ, FRAME_HZ>::frame_duration()
}
/// Validate whether a given `CPU` clock frequency `clock_hz` can be used with
/// the emulator. `max_clock_hz` provides an upper limit of the clock.
/// Returns an error with a message if the check fails.
///
/// While the upper limit depends on how fast the `Z80` can be emulated on a given
/// hardware, `EX-ROM 1` program will report wrong `CPU` frequency if it's above
/// `40_000_000`.
pub fn check_clock(clock_hz: Ts, max_clock_hz: Ts) -> Result<(), &'static str> {
if !FrameRunner::<EXT_HZ, FRAME_HZ>::clock_is_valid(clock_hz) ||
clock_hz > max_clock_hz
{
return Err("please specify clock within the acceptable range");
}
Ok(())
}
/// Validate whether a given RAM size in kilobytes can be used with the emulator.
/// Returns an error with a message if the check fails.
pub fn check_ram_size(ramsizekb: usize) -> Result<(), &'static str> {
if !(1..=Memory::MAXRAM_KB).contains(&ramsizekb) {
return Err("please specify RAM between 1 and 48");
}
Ok(())
}
/// Return a new instance of the `Ral1243` computer.
///
/// Provide the user RAM size in kilobytes and the `CPU` clock frequency.
/// The frequency must be divisible by `EXT_HZ * 2`.
///
/// Optionally an iterator that yields `EX-ROMS` as [`Rom`] instances can be
/// given. If not provided `EX_ROM001` and `EX_ROM002` will be inserted by default.
///
/// * `pio_stream` should be an instance implementing [`PioStream`].
/// * `pio_sink` should be an instance implementing [`PioSink`].
///
/// **Panics** if `ramsizekb` or `clock_hz` are out of range or otherwise invalid.
pub fn new<T>(
ramsizekb: usize,
clock_hz: Ts,
exroms: Option<T>,
pio_stream: I,
pio_sink: O
) -> Self
where T: IntoIterator<Item=Rom>,
T::IntoIter: ExactSizeIterator
{
assert!((1..=Memory::MAXRAM_KB).contains(&ramsizekb));
assert!(FrameRunner::<EXT_HZ, FRAME_HZ>::clock_is_valid(clock_hz));
let mut cpu = Z80::default();
let mut memory = Memory::new(ROM, ramsizekb);
match exroms {
Some(exroms) => { memory.attach_exroms(exroms); }
None => {
memory.attach_exrom(Memory::make_rom(EX_ROM001));
memory.attach_exrom(Memory::make_rom(EX_ROM002));
}
}
let mut runner = FrameRunner::new(clock_hz);
let pio_input = PioInput::new(pio_stream);
let pio_output = PioOutput::new(pio_sink);
let pio = Pio::new(pio_input, pio_output, Terminator::new())
.with_port_bits(
PIO_PORT_MASK,
PIO_PORT_BITS,
PIO_PORT_CHANNEL_SELECT,
PIO_PORT_CONTROL_SELECT);
let ctc_chain0 = CtcActive::new(runner.external_clock_tstates());
let ctc_chain1 = ctc_chain0.new_passive();
let ctc_chain2 = CtcActive::new(runner.external_clock_tstates());
let ctc_chain3 = ctc_chain2.new_passive();
let ctc = Ctc::new(ctc_chain0, ctc_chain1, ctc_chain2, ctc_chain3, pio)
.with_port_bits(
CTC_PORT_MASK,
CTC_PORT_BITS,
CTC_PORT_CHANNEL_SELECT1,
CTC_PORT_CHANNEL_SELECT0);
let mut bus = Bus::new(ctc, memory).with_port_bits(
MEMORY_PORT_MASK, MEMORY_PORT_BITS);
runner.start(&mut cpu, &mut bus);
Ral1243 { runner, cpu, bus }
}
/// Resets all components and begins a new emulation.
pub fn start(&mut self) {
self.runner.start(&mut self.cpu, &mut self.bus);
}
/// Run emulation for a period of a single frame.
/// Return the duration of a frame in T-states.
pub fn step(&mut self) -> Ts {
self.runner.step(&mut self.cpu, &mut self.bus)
}
/// See [`FrameRunner::debug_preview`].
pub fn debug_preview(&self) -> CpuDebug {
self.runner.debug_preview(&self.cpu, &self.bus)
}
/// See [`FrameRunner::debug_step`].
pub fn debug_step(&mut self) -> (Option<CpuDebug>, Ts) {
self.runner.debug_step(&mut self.cpu, &mut self.bus)
}
/// See [`FrameRunner::debug_runto_int`].
pub fn debug_runto_int(&mut self) -> (Option<CpuDebug>, Ts) {
self.runner.debug_runto_int(&mut self.cpu, &mut self.bus)
}
/// See [`FrameRunner::debug_runto_ret`].
pub fn debug_runto_ret(&mut self) -> (Option<CpuDebug>, Ts) {
self.runner.debug_runto_ret(&mut self.cpu, &mut self.bus)
}
/// See [`FrameRunner::run_until_brkpt`].
pub fn run_until_brkpt(&mut self, brkpts: &[u16]) -> (Option<usize>, Ts) {
self.runner.run_until_brkpt(&mut self.cpu, &mut self.bus, brkpts)
}
/// Reset the computer.
pub fn reset(&mut self) {
self.runner.reset(&mut self.cpu, &mut self.bus)
}
/// Trigger non-maskable interrupt and return a number of T-states
/// that it took on success.
pub fn nmi(&mut self) -> Option<Ts> {
self.runner.nmi(&mut self.cpu, &mut self.bus)
}
}