main.rsannotatedmain.rssource159 lines · 3.5 KB · raw
1//! Test runner for running tests assembled for CP/M, specifically ZEXDOC and ZEXALL.
2
3use env_logger::Env;
4use std::error::Error;
5use std::io::Write;
6use std::path::Path;
7use std::{env, fs, io, process};
8use z80_emu::Z80;
9use z80_emu::debug::DummyZ80Debugger;
10use z80_emu::traits::{BusInterface, InterruptLine};
11
12// This is an I/O routine that emulates the CP/M's $05 syscall, which does the following:
13//
14// If C=2, treat the contents of the E register as an ASCII code and print it
15// If C=9, treat the contents of DE as a memory address, and continuously print ASCII characters
16// starting from that address until a '$' character (36d / 0x24) is encountered
17//
18// Pure assembly version:
19//
20// PUSH AF
21// PUSH DE
22// LD A, C
23// CP 2
24// JR Z, singlechar
25// CP 9
26// JR Z, multichar
27//
28// return:
29//   POP DE
30//   POP AF
31//   RET
32//
33// singlechar:
34//   LD A, E
35//   OUT (0), A
36//   JR return
37//
38// multichar:
39//   LD A, (DE)
40//   CP 36
41//   JR Z, return
42//   OUT (0), A
43//   INC DE
44//   JR multichar
45const CPM_IO_ROUTINE: &str = concat!(
46    "F5",   // PUSH AF
47    "D5",   // PUSH DE
48    "79",   // LD A, C
49    "FE02", // CP 2
50    "2807", // JR Z, 7
51    "FE09", // CP 9
52    "2808", // JR Z, 8
53    "D1",   // POP DE
54    "F1",   // POP AF
55    "C9",   // RET
56    "7B",   // LD A, E
57    "D300", // OUT (0), A
58    "18F8", // JR -8
59    "1A",   // LD A, (DE)
60    "FE24", // CP 36
61    "28F3", // JR Z, -13
62    "D300", // OUT (0), A
63    "13",   // INC DE
64    "18F6", // JR -10
65);
66
67fn hex_to_bytes(s: &str) -> Vec<u8> {
68    let mut bytes = Vec::new();
69    for i in (0..s.len()).step_by(2) {
70        let hex_char = &s[i..i + 2];
71        let byte = u8::from_str_radix(hex_char, 16).unwrap();
72        bytes.push(byte);
73    }
74    bytes
75}
76
77struct FullyWritableBus {
78    memory: Box<[u8; 0x10000]>,
79    io_ports: [u8; 0x100],
80}
81
82impl FullyWritableBus {
83    fn new() -> Self {
84        Self {
85            memory: vec![0; 0x10000].into_boxed_slice().try_into().unwrap(),
86            io_ports: [0; 0x100],
87        }
88    }
89}
90
91impl BusInterface for FullyWritableBus {
92    type DebugView<'a>
93        = DummyZ80Debugger
94    where
95        Self: 'a;
96
97    fn read_memory(&mut self, address: u16) -> u8 {
98        self.memory[address as usize]
99    }
100
101    fn write_memory(&mut self, address: u16, value: u8) {
102        self.memory[address as usize] = value;
103    }
104
105    fn read_io(&mut self, address: u16) -> u8 {
106        self.io_ports[(address & 0xFF) as usize]
107    }
108
109    fn write_io(&mut self, address: u16, value: u8) {
110        self.io_ports[(address & 0xFF) as usize] = value;
111        print!("{}", value as char);
112    }
113
114    fn nmi(&self) -> InterruptLine {
115        InterruptLine::High
116    }
117
118    fn int(&self) -> InterruptLine {
119        InterruptLine::High
120    }
121
122    fn busreq(&self) -> bool {
123        false
124    }
125
126    fn reset(&self) -> bool {
127        false
128    }
129}
130
131fn main() -> Result<(), Box<dyn Error>> {
132    env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
133
134    let mut args = env::args();
135    if args.len() < 2 {
136        io::stderr().write_all("ARGS: <filename>\n".as_bytes())?;
137        process::exit(1);
138    }
139    args.next();
140
141    let filename = args.next().unwrap();
142
143    let bytes = fs::read(Path::new(&filename))?;
144
145    let mut bus = FullyWritableBus::new();
146    bus.memory[0x100..bytes.len() + 0x100].copy_from_slice(&bytes);
147
148    let io_routine = hex_to_bytes(CPM_IO_ROUTINE);
149    bus.memory[0x05..io_routine.len() + 0x05].copy_from_slice(&io_routine);
150
151    let mut z80 = Z80::new();
152    z80.set_pc(0x100);
153
154    while z80.pc() != 0 {
155        z80.execute_instruction(&mut bus);
156    }
157
158    Ok(())
159}