| 1 | //! A debugging session: the simulator, the program loaded into it, |
| 2 | //! breakpoints and run state. Front ends own the source text and decide |
| 3 | //! when to build; everything else about running a program lives here. |
| 4 | |
| 5 | use std::collections::{HashMap, HashSet}; |
| 6 | use std::path::Path; |
| 7 | |
| 8 | use crate::asm::{self, AsmOutput, Diagnostic}; |
| 9 | use crate::sim::{Sim, Stop}; |
| 10 | |
| 11 | /// Result of the last successful assembly, which the simulator runs. |
| 12 | pub struct Build { |
| 13 | pub source: Vec<String>, |
| 14 | pub out: AsmOutput, |
| 15 | pub addr_to_line: HashMap<u16, usize>, |
| 16 | } |
| 17 | |
| 18 | impl Build { |
| 19 | /// First address emitted by `line`, if any. |
| 20 | pub fn line_addr(&self, line: usize) -> Option<u16> { |
| 21 | self.out |
| 22 | .lines |
| 23 | .get(line) |
| 24 | .filter(|l| !l.bytes.is_empty()) |
| 25 | .and_then(|l| l.addr) |
| 26 | } |
| 27 | } |
| 28 | |
| 29 | /// Instructions executed per tick (one tick ≈ 33 ms) for each speed setting. |
| 30 | pub const SPEEDS: [u32; 8] = [1, 5, 25, 100, 500, 2_000, 10_000, 40_000]; |
| 31 | |
| 32 | /// Instruction budget for step over / step out / run to cursor. |
| 33 | const STEP_BUDGET: u32 = 5_000_000; |
| 34 | |
| 35 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] |
| 36 | pub enum MemSpace { |
| 37 | Iram, |
| 38 | Sfr, |
| 39 | Xram, |
| 40 | Code, |
| 41 | } |
| 42 | |
| 43 | impl MemSpace { |
| 44 | pub const ALL: [MemSpace; 4] = [ |
| 45 | MemSpace::Iram, |
| 46 | MemSpace::Sfr, |
| 47 | MemSpace::Xram, |
| 48 | MemSpace::Code, |
| 49 | ]; |
| 50 | |
| 51 | pub fn name(self) -> &'static str { |
| 52 | match self { |
| 53 | MemSpace::Iram => "IRAM", |
| 54 | MemSpace::Sfr => "SFR", |
| 55 | MemSpace::Xram => "XRAM", |
| 56 | MemSpace::Code => "CODE", |
| 57 | } |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | pub struct Session { |
| 62 | pub sim: Sim, |
| 63 | /// Last assembler result, even if it had errors (for diagnostics). |
| 64 | pub last_asm: Option<(String, AsmOutput)>, |
| 65 | /// Last error-free build, loaded into the simulator. |
| 66 | pub build: Option<Build>, |
| 67 | /// Breakpoints by source line. |
| 68 | pub breakpoints: HashSet<usize>, |
| 69 | pub running: bool, |
| 70 | /// The next run chunk starts a fresh run and may leave a breakpoint. |
| 71 | resume: bool, |
| 72 | /// Index into `SPEEDS`. |
| 73 | pub speed: usize, |
| 74 | pub clock_mhz: f64, |
| 75 | /// Memory before the last step or run chunk, to highlight changes. |
| 76 | pub prev_iram: [u8; 256], |
| 77 | pub prev_sfr: [u8; 128], |
| 78 | /// Outcome of the last command, for the status line. |
| 79 | pub status: String, |
| 80 | } |
| 81 | |
| 82 | impl Session { |
| 83 | pub fn new(clock_mhz: f64) -> Self { |
| 84 | Self { |
| 85 | sim: Sim::new(), |
| 86 | last_asm: None, |
| 87 | build: None, |
| 88 | breakpoints: HashSet::new(), |
| 89 | running: false, |
| 90 | resume: false, |
| 91 | speed: 5, |
| 92 | clock_mhz, |
| 93 | prev_iram: [0; 256], |
| 94 | prev_sfr: [0; 128], |
| 95 | status: String::new(), |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | /// Forgets the program and breakpoints, for a newly loaded source. |
| 100 | pub fn clear(&mut self) { |
| 101 | self.breakpoints.clear(); |
| 102 | self.build = None; |
| 103 | self.last_asm = None; |
| 104 | self.running = false; |
| 105 | } |
| 106 | |
| 107 | /// Whether `lines` differ from the source the simulator is running. |
| 108 | pub fn is_stale<'a>(&self, lines: impl IntoIterator<Item = &'a str>) -> bool { |
| 109 | self.build |
| 110 | .as_ref() |
| 111 | .is_none_or(|b| !b.source.iter().map(String::as_str).eq(lines)) |
| 112 | } |
| 113 | |
| 114 | /// Diagnostics of the last assembly, if it was of exactly `source`. |
| 115 | pub fn diagnostics_for(&self, source: &str) -> &[Diagnostic] { |
| 116 | match &self.last_asm { |
| 117 | Some((src, out)) if src == source => &out.diagnostics, |
| 118 | _ => &[], |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | // ---- Build ---- |
| 123 | |
| 124 | /// Assembles `source`. On success the program is loaded into a freshly |
| 125 | /// reset simulator and, if `path` (the saved source file) is given, |
| 126 | /// `.lst` and `.hex` files are written next to it. |
| 127 | pub fn assemble(&mut self, source: &str, path: Option<&Path>) -> bool { |
| 128 | let out = asm::assemble(source); |
| 129 | let ok = out.ok(); |
| 130 | if ok { |
| 131 | self.sim.load_code(out.bytes()); |
| 132 | self.sim.reset(); |
| 133 | self.running = false; |
| 134 | self.status = format!("assembled: {} bytes, 0 errors", out.code_size()); |
| 135 | if let Some(path) = path { |
| 136 | self.write_outputs(&out, source, path); |
| 137 | } |
| 138 | self.build = Some(Build { |
| 139 | source: source.split('\n').map(String::from).collect(), |
| 140 | addr_to_line: out.addr_to_line(), |
| 141 | out: out.clone(), |
| 142 | }); |
| 143 | self.snapshot(); |
| 144 | } else { |
| 145 | let first = &out.diagnostics[0]; |
| 146 | self.status = format!( |
| 147 | "{} error(s); line {}: {}", |
| 148 | out.diagnostics.len(), |
| 149 | first.line + 1, |
| 150 | first.msg |
| 151 | ); |
| 152 | } |
| 153 | self.last_asm = Some((source.to_string(), out)); |
| 154 | ok |
| 155 | } |
| 156 | |
| 157 | /// Makes sure a build of `source` is loaded; returns false on errors. |
| 158 | pub fn ensure_built(&mut self, source: &str, path: Option<&Path>) -> bool { |
| 159 | !self.is_stale(source.split('\n')) || self.assemble(source, path) |
| 160 | } |
| 161 | |
| 162 | fn write_outputs(&mut self, out: &AsmOutput, source: &str, path: &Path) { |
| 163 | let res = std::fs::write(path.with_extension("lst"), out.listing(source)) |
| 164 | .and_then(|_| std::fs::write(path.with_extension("hex"), out.intel_hex())); |
| 165 | if let Err(e) = res { |
| 166 | self.status = format!("assembled, but writing .lst/.hex failed: {e}"); |
| 167 | } |
| 168 | } |
| 169 | |
| 170 | // ---- Breakpoints ---- |
| 171 | |
| 172 | pub fn toggle_breakpoint(&mut self, line: usize) { |
| 173 | if !self.breakpoints.remove(&line) { |
| 174 | self.breakpoints.insert(line); |
| 175 | } |
| 176 | } |
| 177 | |
| 178 | pub fn bp_addrs(&self) -> HashSet<u16> { |
| 179 | let Some(b) = &self.build else { |
| 180 | return HashSet::new(); |
| 181 | }; |
| 182 | self.breakpoints |
| 183 | .iter() |
| 184 | .filter_map(|&l| b.line_addr(l)) |
| 185 | .collect() |
| 186 | } |
| 187 | |
| 188 | // ---- Execution ---- |
| 189 | |
| 190 | pub fn snapshot(&mut self) { |
| 191 | self.prev_iram = self.sim.iram; |
| 192 | self.prev_sfr = self.sim.sfr; |
| 193 | } |
| 194 | |
| 195 | /// Source line of the instruction at the PC. |
| 196 | pub fn pc_line(&self) -> Option<usize> { |
| 197 | self.build.as_ref()?.addr_to_line.get(&self.sim.pc).copied() |
| 198 | } |
| 199 | |
| 200 | /// Stops running and describes why in `status`. |
| 201 | pub fn report(&mut self, stop: Stop) { |
| 202 | self.running = false; |
| 203 | self.status = match stop { |
| 204 | Stop::Breakpoint(pc) => format!("breakpoint at {pc:04X}h"), |
| 205 | Stop::ReservedOpcode { pc, opcode } => { |
| 206 | format!("reserved opcode {opcode:02X}h at {pc:04X}h — stopped") |
| 207 | } |
| 208 | Stop::PowerDown => "CPU in power-down mode (PCON.PD) — reset to continue".into(), |
| 209 | Stop::Target | Stop::Budget => format!("stopped at {:04X}h", self.sim.pc), |
| 210 | }; |
| 211 | } |
| 212 | |
| 213 | fn step_with(&mut self, f: impl FnOnce(&mut Sim, &HashSet<u16>) -> Stop) { |
| 214 | self.running = false; |
| 215 | self.snapshot(); |
| 216 | let bps = self.bp_addrs(); |
| 217 | match f(&mut self.sim, &bps) { |
| 218 | Stop::Target => self.status = format!("PC = {:04X}h", self.sim.pc), |
| 219 | s => self.report(s), |
| 220 | } |
| 221 | } |
| 222 | |
| 223 | pub fn step_into(&mut self) { |
| 224 | self.step_with(|s, _| match s.step() { |
| 225 | Ok(()) => Stop::Target, |
| 226 | Err(e) => e, |
| 227 | }); |
| 228 | } |
| 229 | |
| 230 | pub fn step_over(&mut self) { |
| 231 | self.step_with(|s, b| s.step_over(STEP_BUDGET, b)); |
| 232 | } |
| 233 | |
| 234 | pub fn step_out(&mut self) { |
| 235 | self.step_with(|s, b| s.step_out(STEP_BUDGET, b)); |
| 236 | } |
| 237 | |
| 238 | /// Runs until the first instruction of `line`, or a breakpoint. |
| 239 | /// Returns false if the line holds no code. |
| 240 | pub fn run_to_line(&mut self, line: usize) -> bool { |
| 241 | let Some(target) = self.build.as_ref().and_then(|b| b.line_addr(line)) else { |
| 242 | self.status = "no code on that line".into(); |
| 243 | return false; |
| 244 | }; |
| 245 | self.step_with(|s, bps| { |
| 246 | let mut bps = bps.clone(); |
| 247 | bps.insert(target); |
| 248 | match s.run(STEP_BUDGET, &bps, true) { |
| 249 | Stop::Breakpoint(pc) if pc == target => Stop::Target, |
| 250 | other => other, |
| 251 | } |
| 252 | }); |
| 253 | true |
| 254 | } |
| 255 | |
| 256 | /// Starts free running; `tick` then advances the program. Needs a build. |
| 257 | pub fn start(&mut self) { |
| 258 | if self.build.is_none() { |
| 259 | return; |
| 260 | } |
| 261 | self.running = true; |
| 262 | self.resume = true; |
| 263 | self.status = "running…".into(); |
| 264 | } |
| 265 | |
| 266 | pub fn pause(&mut self) { |
| 267 | self.running = false; |
| 268 | self.status = format!("paused at {:04X}h", self.sim.pc); |
| 269 | } |
| 270 | |
| 271 | /// Runs one chunk of `SPEEDS[speed]` instructions if running. Front ends |
| 272 | /// call this every ~33 ms. |
| 273 | pub fn tick(&mut self) { |
| 274 | if !self.running { |
| 275 | return; |
| 276 | } |
| 277 | self.snapshot(); |
| 278 | let bps = self.bp_addrs(); |
| 279 | let resume = std::mem::take(&mut self.resume); |
| 280 | match self.sim.run(SPEEDS[self.speed], &bps, resume) { |
| 281 | Stop::Budget => {} |
| 282 | s => self.report(s), |
| 283 | } |
| 284 | } |
| 285 | |
| 286 | pub fn reset(&mut self) { |
| 287 | self.sim.reset(); |
| 288 | self.running = false; |
| 289 | self.snapshot(); |
| 290 | self.status = "reset".into(); |
| 291 | } |
| 292 | |
| 293 | /// Writes `value` at `addr` in `space`. Returns false if out of range. |
| 294 | pub fn poke(&mut self, space: MemSpace, addr: u32, value: u8) -> bool { |
| 295 | let ok = match space { |
| 296 | MemSpace::Iram if addr <= 0xFF => { |
| 297 | self.sim.iram[addr as usize] = value; |
| 298 | true |
| 299 | } |
| 300 | MemSpace::Sfr if (0x80..=0xFF).contains(&addr) => { |
| 301 | self.sim.set_sfr(addr as u8, value); |
| 302 | true |
| 303 | } |
| 304 | MemSpace::Xram if addr <= 0xFFFF => { |
| 305 | self.sim.xram[addr as usize] = value; |
| 306 | true |
| 307 | } |
| 308 | MemSpace::Code if addr <= 0xFFFF => { |
| 309 | self.sim.code[addr as usize] = value; |
| 310 | true |
| 311 | } |
| 312 | _ => false, |
| 313 | }; |
| 314 | self.status = if ok { |
| 315 | format!("wrote {value:02X}h at {addr:X}h") |
| 316 | } else { |
| 317 | "address out of range".into() |
| 318 | }; |
| 319 | ok |
| 320 | } |
| 321 | |
| 322 | /// Simulated time since reset at `clock_mhz` (12 clocks per machine cycle). |
| 323 | pub fn elapsed(&self) -> String { |
| 324 | let micros = self.sim.cycles as f64 * 12.0 / self.clock_mhz; |
| 325 | if micros >= 1_000_000.0 { |
| 326 | format!("{:.3} s", micros / 1e6) |
| 327 | } else if micros >= 1000.0 { |
| 328 | format!("{:.3} ms", micros / 1e3) |
| 329 | } else { |
| 330 | format!("{micros:.2} µs") |
| 331 | } |
| 332 | } |
| 333 | } |
| 334 | |
| 335 | /// Name of the interrupt source served at vector `v`. |
| 336 | pub fn vector_name(v: u16) -> &'static str { |
| 337 | match v { |
| 338 | 0x03 => "INT0", |
| 339 | 0x0B => "T0", |
| 340 | 0x13 => "INT1", |
| 341 | 0x1B => "T1", |
| 342 | 0x23 => "UART", |
| 343 | _ => "?", |
| 344 | } |
| 345 | } |
| 346 | |
| 347 | #[cfg(test)] |
| 348 | mod tests { |
| 349 | use super::{MemSpace, Session}; |
| 350 | |
| 351 | const PROG: &str = "\ |
| 352 | ORG 0 |
| 353 | LJMP main |
| 354 | ORG 30h |
| 355 | main: MOV R0,#0 |
| 356 | loop: ACALL bump |
| 357 | SJMP loop |
| 358 | bump: INC R0 |
| 359 | NOP |
| 360 | RET |
| 361 | END"; |
| 362 | |
| 363 | fn session() -> Session { |
| 364 | let mut s = Session::new(12.0); |
| 365 | assert!(s.assemble(PROG, None), "{}", s.status); |
| 366 | s |
| 367 | } |
| 368 | |
| 369 | #[test] |
| 370 | fn breakpoints_steps_and_run_to_line() { |
| 371 | let mut s = session(); |
| 372 | s.toggle_breakpoint(6); // bump: INC R0 |
| 373 | s.start(); |
| 374 | s.tick(); |
| 375 | assert!(!s.running); |
| 376 | assert_eq!(s.pc_line(), Some(6)); |
| 377 | assert_eq!(s.status, format!("breakpoint at {:04X}h", s.sim.pc)); |
| 378 | |
| 379 | s.step_into(); // INC R0 |
| 380 | assert_eq!(s.sim.reg(0), 1); |
| 381 | assert_eq!(s.pc_line(), Some(7)); |
| 382 | s.step_out(); // back after ACALL |
| 383 | assert_eq!(s.pc_line(), Some(5)); |
| 384 | |
| 385 | s.toggle_breakpoint(6); |
| 386 | s.step_into(); // SJMP loop |
| 387 | assert_eq!(s.pc_line(), Some(4)); |
| 388 | s.step_over(); // whole call |
| 389 | assert_eq!((s.pc_line(), s.sim.reg(0)), (Some(5), 2)); |
| 390 | |
| 391 | assert!(s.run_to_line(7)); |
| 392 | assert_eq!((s.pc_line(), s.sim.reg(0)), (Some(7), 3)); |
| 393 | assert!(!s.run_to_line(0)); // ORG: no code |
| 394 | } |
| 395 | |
| 396 | #[test] |
| 397 | fn staleness_errors_and_poke() { |
| 398 | let mut s = session(); |
| 399 | assert!(!s.is_stale(PROG.split('\n'))); |
| 400 | let edited = PROG.replace("NOP", "NOP\n NOP"); |
| 401 | assert!(s.is_stale(edited.split('\n'))); |
| 402 | assert!(s.ensure_built(&edited, None)); |
| 403 | assert!(!s.is_stale(edited.split('\n'))); |
| 404 | |
| 405 | let bad = " MOV A,\n FOO\n"; |
| 406 | assert!(!s.assemble(bad, None)); |
| 407 | assert_eq!(s.diagnostics_for(bad).len(), 2); |
| 408 | assert!(s.diagnostics_for(PROG).is_empty()); |
| 409 | assert!(s.build.is_some(), "a failed build keeps the last good one"); |
| 410 | |
| 411 | assert!(s.poke(MemSpace::Iram, 0x30, 0xAB)); |
| 412 | assert_eq!(s.sim.iram[0x30], 0xAB); |
| 413 | assert!(s.poke(MemSpace::Sfr, 0x90, 0x0F)); |
| 414 | assert!(!s.poke(MemSpace::Sfr, 0x20, 1)); |
| 415 | assert_eq!(s.status, "address out of range"); |
| 416 | } |
| 417 | } |