openMSX fake I/O


Enlighted (6067)

NYYRIKKI's picture

26-10-2022, 20:04

Every once in a while it comes to mind that for debug purposes it would be great to use openMSX to emulate some device that possibly exists only in your head... For simplicity, I started from some latch that implements 1-byte of RAM on I/O port 8...

Now writing to such device is easy and straight forward, as you can for example give command:

debug set_watchpoint write_io 0x8 {} {set ::MyValue $::wp_last_value}

the problem starts if you want to read the value back... After experimenting I came up with this custom command:

wp_after_input 0x8 {} {wp_set_input $::MyValue}

... in theory it works well enough for my purpose (INIR & INDR broken), but I just wonder am I going totally to wrong direction as this solution just feels very fragile and weird? Yes, I realize the code is ATM not practically usable as it uses only global variables etc. etc. but those are relatively straight forward things to fix. I just wonder if there is some similar trick for input that there is for output that has simply skipped my eyes? ...or maybe someone has already found a better way that is not quite this messed up? (ie. no need to step back in time to step forward)

In total my current piece of s**t code looks like this: (Can be copy/pasted to openMSX console)

proc wp_after_input {MyPort MyCond MyCMD} {
  set ::MyPort $MyPort
  set ::MyCond $MyCond
  set ::MyCMD $MyCMD
  set ::MyWP [debug set_watchpoint read_io $::MyPort $::MyCond {
	set ::MyBP [expr [reg pc]+1]
	debug set_bp -once $::MyBP {} {
		eval $::MyCMD
		wp_after_input $::MyPort $::MyCond $::MyCMD
	debug remove_watchpoint $::MyWP

proc wp_set_input x {
 switch [peek [expr [reg pc]-2]] {
 219 {reg a $x}
 237 {
	switch [peek [expr [reg pc]-1]] {
	 120 {reg a $x}
	  64 {reg b $x}
	  72 {reg c $x}
	  80 {reg d $x}
	  88 {reg e $x}
	  96 {reg h $x}
	 104 {reg l $x}
	 162 {poke [expr [reg hl]-1] $x}
	 178 {poke [expr [reg hl]-1] $x} ;# BROKEN!
	 170 {poke [expr [reg hl]+1] $x}
	 186 {poke [expr [reg hl]+1] $x} ;# BROKEN!
	 default {debug break; error "No input command at $[format %X [expr [reg pc]-2]]"}
 default {debug break; error "No input command at $[format %X [expr [reg pc]-2]]"}

set MyValue 8
debug set_watchpoint write_io 0x8 {} {set ::MyValue $::wp_last_value}
wp_after_input 0x8 {} {wp_set_input $::MyValue}
Login or register to post comments

By wouter_

Hero (526)

wouter_'s picture

27-10-2022, 17:55

Hi Nyyrikki,

The current watchpoints in openMSX are indeed ill suited for what you're trying to do. Let me give some some background information.

Currently for writes (memory or IO), we first execute the (Z80) write operation and then call the Tcl proc.
For reads (memory or IO) we instead first call the Tcl proc and only then execute the (Z80) read operation.

It's been like this for +16 years, so I don't remember the exact reasoning. But I think it was something like: in the read-proc you first might want to alter the stored value in RAM before the Z80 reads it (I think the cheat-system makes use of this mechanism). And in a write-callback-proc you might want to overwrite the value that the Z80 just wrote with some other value.

So this order makes sense for some scenarios (e.g. altering the behavior of existing RAM-like MSX devices). But in your scenario it might be more useful to have a Tcl read-callback that triggers AFTER the actual Z80 read-operation (then in your Tcl script you can see and alter the read value). Unfortunately that's not possible today.

I fully understand why you might want to create "fake" MSX devices. I've created several myself. Though as an openMSX developer my opinion might be biased, but I think it's easier to implement such "fake" devices in openMSX itself. So in C++ rather than as a Tcl. Obviously this requires that you re-build the openMSX executable yourself, but that's not too difficult.