Skip to content

Commit

Permalink
IO-space debug functions. Some fixes to debugger breakpoint functiona…
Browse files Browse the repository at this point in the history
…lity. Don't pause on emulator startup with --debugger (require --breakpoint 0 for this)
  • Loading branch information
tomm committed Dec 1, 2024
1 parent 460a151 commit 2b5ad80
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 37 deletions.
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
members = ["agon-light-emulator-debugger"]

[workspace.package]
version = "0.9.71"
version = "0.9.72"
edition = "2021"
authors = ["Tom Morton <[email protected]>"]
license = "GPL-3.0"
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,26 @@ fab-agon-emulator -d
At the debugger prompt (which will be in the terminal window you invoked the
emulator from), type `help` for instructions on the use of the debugger.

## Debug IO space

Some IO addresses unused by the EZ80F92 are used by the emulator for debugging
purposes:

| IO addresses | Function |
| ------------- | ------------------------------------- |
| 0x00 | Terminate emulator |
| 0x10-0x1f | Breakpoint (requires --debugger) |
| 0x20-0x2f | Print CPU state (requires --debugger) |

These functions are activated by write (not read), and the upper 8-bits of the
IO address are ignored. ie:

```
out (0),a
```

will shut down the emulator.

## Other command-line options

Read about other command-line options with:
Expand Down
37 changes: 32 additions & 5 deletions agon-ez80-emulator/src/agon_machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ pub struct AgonMachine {
mos_map: mos::MosMap,
hostfs_root_dir: std::path::PathBuf,
mos_current_dir: MosPath,
paused: Arc<std::sync::atomic::AtomicBool>,
soft_reset: Arc<std::sync::atomic::AtomicBool>,
emulator_shutdown: Arc<std::sync::atomic::AtomicBool>,
clockspeed_hz: u64,
prt_timers: [prt_timer::PrtTimer; 6],
gpios: Arc<Mutex<gpio::GpioSet>>,
Expand All @@ -47,7 +49,7 @@ pub struct AgonMachine {
// last_pc and mem_out_of_bounds are used by the debugger
pub last_pc: u32,
pub mem_out_of_bounds: std::cell::Cell<Option<u32>>, // address
pub paused: bool,
pub io_unhandled: std::cell::Cell<Option<u16>>, // address
pub cycle_counter: std::cell::Cell<u32>,
}

Expand Down Expand Up @@ -279,6 +281,7 @@ impl Machine for AgonMachine {
fn port_out(&mut self, address: u16, value: u8) {
self.use_cycles(1);
match address {
// Real ez80f92 peripherals
0x80 => self.prt_timers[0].write_ctl(value),
0x81 => self.prt_timers[0].write_reload_low(value),
0x82 => self.prt_timers[0].write_reload_high(value),
Expand Down Expand Up @@ -457,6 +460,16 @@ impl Machine for AgonMachine {

_ => {
//println!("OUT(${:02X}) = ${:x}", address, value);
// Emulator special functions, mapped in IO space
// Discard high byte of address, so we can use `out (n),a`
// for debugging
if address & 0xff == 0 {
println!("Emulator shutdown triggered by write to IO 0x0");
self.emulator_shutdown
.store(true, std::sync::atomic::Ordering::Relaxed);
} else {
self.io_unhandled.set(Some(address));
}
}
}
}
Expand All @@ -466,6 +479,8 @@ pub struct AgonMachineConfig {
pub uart0_link: Box<dyn uart::SerialLink>,
pub uart1_link: Box<dyn uart::SerialLink>,
pub soft_reset: Arc<std::sync::atomic::AtomicBool>,
pub emulator_shutdown: Arc<std::sync::atomic::AtomicBool>,
pub paused: Arc<std::sync::atomic::AtomicBool>,
pub clockspeed_hz: u64,
pub ram_init: RamInit,
pub mos_bin: std::path::PathBuf,
Expand All @@ -489,6 +504,7 @@ impl AgonMachine {
hostfs_root_dir: std::env::current_dir().unwrap(),
mos_current_dir: MosPath(std::path::PathBuf::new()),
soft_reset: config.soft_reset,
emulator_shutdown: config.emulator_shutdown,
clockspeed_hz: config.clockspeed_hz,
prt_timers: [
prt_timer::PrtTimer::new(),
Expand All @@ -502,8 +518,9 @@ impl AgonMachine {
ram_init: config.ram_init,
last_pc: 0,
mem_out_of_bounds: std::cell::Cell::new(None),
io_unhandled: std::cell::Cell::new(None),
cycle_counter: std::cell::Cell::new(0),
paused: false,
paused: config.paused,
mos_bin: config.mos_bin,
onchip_mem_enable: true,
onchip_mem_segment: 0xff,
Expand All @@ -513,6 +530,15 @@ impl AgonMachine {
}
}

pub fn set_paused(&self, state: bool) {
self.paused
.store(state, std::sync::atomic::Ordering::Relaxed);
}

pub fn is_paused(&self) -> bool {
self.paused.load(std::sync::atomic::Ordering::Relaxed)
}

#[inline]
fn get_rom_address(&self, address: u32) -> Option<u32> {
let a: u32 = address.wrapping_sub((self.flash_addr_u as u32) << 16);
Expand Down Expand Up @@ -1528,7 +1554,6 @@ impl AgonMachine {
let mut cpu = Cpu::new_ez80();

let mut debugger = if debugger_con.is_some() {
self.paused = true;
Some(debugger::DebuggerServer::new(debugger_con.unwrap()))
} else {
None
Expand All @@ -1550,7 +1575,9 @@ impl AgonMachine {
self.load_mos();

cpu.state.set_pc(0);
//cpu.set_trace(true);

// This extra call is needed, or breakpoints at 0 don't work. I don't understand why :)
self.debugger_tick(&mut debugger, &mut cpu);

let cycles_per_ms: u64 = self.clockspeed_hz / 1000;
let mut timeslice_start = std::time::Instant::now();
Expand All @@ -1559,7 +1586,7 @@ impl AgonMachine {
let mut cycle: u64 = 0;
while cycle < cycles_per_ms {
self.debugger_tick(&mut debugger, &mut cpu);
if self.paused {
if self.is_paused() {
break;
}
self.do_interrupts(&mut cpu);
Expand Down
2 changes: 2 additions & 0 deletions agon-ez80-emulator/src/bin/agon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ fn main() {
let (tx_vdp_to_ez80, from_vdp): (Sender<u8>, Receiver<u8>) = mpsc::channel();
let (to_vdp, rx_ez80_to_vdp): (Sender<u8>, Receiver<u8>) = mpsc::channel();
let soft_reset = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let emulator_shutdown = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let gpios = std::sync::Arc::new(std::sync::Mutex::new(gpio::GpioSet::new()));
let gpios_ = gpios.clone();

Expand Down Expand Up @@ -259,6 +260,7 @@ fn main() {
}),
uart1_link: Box::new(DummySerialLink {}),
soft_reset,
emulator_shutdown,
gpios: gpios_,
clockspeed_hz: if unlimited_cpu {
std::u64::MAX
Expand Down
65 changes: 52 additions & 13 deletions agon-ez80-emulator/src/debugger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,22 +100,64 @@ impl DebuggerServer {
self.send_disassembly(machine, cpu, None, machine.last_pc, machine.last_pc + 1);
self.send_state(machine, cpu);

machine.paused = true;
machine.set_paused(true);
true
} else {
false
}
}

fn on_unhandled_io(&mut self, machine: &mut AgonMachine, cpu: &mut ez80::Cpu) {
// An IO-space read or write occurred, that didn't correspond to
// any EZ80F92 peripherals
//
// We implement some debugger functions with these unused IOs
if let Some(address) = machine.io_unhandled.get() {
match address & 0xff {
0x10..=0x1f => {
self.con.tx.send(DebugResp::IsPaused(true)).unwrap();
self.con
.tx
.send(DebugResp::Message(format!(
"Breakpoint triggered by IO 0x{:x} access at PC=${:x}",
address & 0xff,
machine.last_pc
)))
.unwrap();
self.send_disassembly(machine, cpu, None, machine.last_pc, machine.last_pc + 1);
self.send_state(machine, cpu);

machine.set_paused(true);
}
0x20..=0x2f => {
self.con
.tx
.send(DebugResp::Message(format!(
"State dump triggered by IO 0x{:x} access at PC=${:x}",
address & 0xff,
machine.last_pc
)))
.unwrap();
self.send_disassembly(machine, cpu, None, machine.last_pc, machine.last_pc + 1);
self.send_state(machine, cpu);
}
_ => {}
}
}
machine.io_unhandled.set(None);
}

/// Called before each instruction is executed
pub fn tick(&mut self, machine: &mut AgonMachine, cpu: &mut ez80::Cpu) {
let pc = cpu.state.pc();

// catch out of bounds memory accesses
self.on_out_of_bounds(machine, cpu);
// debugger functions triggered by IO read/write
self.on_unhandled_io(machine, cpu);

// check triggers
if !machine.paused {
if !machine.is_paused() {
let to_run: Vec<Trigger> = self
.triggers
.iter()
Expand Down Expand Up @@ -202,7 +244,7 @@ impl DebuggerServer {
DebugCmd::GetState,
],
});
machine.paused = false;
machine.set_paused(false);
self.con.tx.send(DebugResp::IsPaused(false)).unwrap();
}
// CALL instruction at (pc)
Expand All @@ -228,34 +270,31 @@ impl DebuggerServer {
DebugCmd::GetState,
],
});
machine.paused = false;
machine.set_paused(false);
self.con.tx.send(DebugResp::IsPaused(false)).unwrap();
}
// other instructions. just step
_ => {
machine.paused = false;
machine.set_paused(false);
machine.execute_instruction(cpu);
machine.paused = true;
machine.set_paused(true);
self.send_state(machine, cpu);
}
}
}
DebugCmd::Step => {
machine.paused = false;
machine.set_paused(false);
machine.execute_instruction(cpu);
machine.paused = true;
machine.set_paused(true);
self.send_state(machine, cpu);
}
DebugCmd::Pause => {
machine.paused = true;
machine.set_paused(true);
self.con.tx.send(DebugResp::IsPaused(true)).unwrap();
}
DebugCmd::Continue => {
machine.mem_out_of_bounds.set(None);
machine.paused = false;
// force one instruction to be executed, just to
// get over any breakpoint on the current PC
machine.execute_instruction(cpu);
machine.set_paused(false);

if !self.on_out_of_bounds(machine, cpu) {
self.con.tx.send(DebugResp::IsPaused(false)).unwrap();
Expand Down
20 changes: 10 additions & 10 deletions agon-light-emulator-debugger/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ use agon_ez80_emulator::debugger::{DebugCmd, DebugResp, Reg16, Registers};

#[derive(Clone)]
struct EmuState {
pub in_debugger: std::sync::Arc<std::sync::atomic::AtomicBool>,
pub ez80_paused: std::sync::Arc<std::sync::atomic::AtomicBool>,
pub emulator_shutdown: std::sync::Arc<std::sync::atomic::AtomicBool>,
}

impl EmuState {
pub fn is_in_debugger(&self) -> bool {
self.in_debugger.load(std::sync::atomic::Ordering::SeqCst)
self.ez80_paused.load(std::sync::atomic::Ordering::SeqCst)
}

pub fn set_in_debugger(&self, state: bool) {
self.in_debugger
self.ez80_paused
.store(state, std::sync::atomic::Ordering::SeqCst);
}

Expand All @@ -35,9 +35,6 @@ impl EmuState {
}

fn print_help() {
println!("While CPU is running:");
println!("<CTRL-C> Pause Agon CPU and enter debugger");
println!();
println!("While CPU is paused:");
println!("br[eak] <address> Set a breakpoint at the hex address");
println!("c[ontinue] Resume (un-pause) Agon CPU");
Expand All @@ -63,6 +60,9 @@ fn print_help() {
println!("triggers List triggers");
println!();
println!("The previous command can be repeated by pressing return.");
println!();
println!("CPU running. Press <CTRL-C> to pause");
println!();
}

fn do_cmd(cmd: parser::Cmd, tx: &Sender<DebugCmd>, rx: &Receiver<DebugResp>, state: &EmuState) {
Expand Down Expand Up @@ -192,15 +192,14 @@ fn drain_rx(rx: &Receiver<DebugResp>, state: &EmuState) {
}
}

const PAUSE_AT_START: bool = true;

pub fn start(
tx: Sender<DebugCmd>,
rx: Receiver<DebugResp>,
emulator_shutdown: std::sync::Arc<std::sync::atomic::AtomicBool>,
ez80_paused: std::sync::Arc<std::sync::atomic::AtomicBool>,
) {
let state = EmuState {
in_debugger: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(PAUSE_AT_START)),
ez80_paused,
emulator_shutdown,
};
let tx_from_ctrlc = tx.clone();
Expand All @@ -211,7 +210,8 @@ pub fn start(
println!("Agon Light Emulator Debugger");
println!();
print_help();
if PAUSE_AT_START {

if state.is_in_debugger() {
println!("Interrupting execution.");
}

Expand Down
Loading

0 comments on commit 2b5ad80

Please sign in to comment.