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}