Watching a Z80 Bus from an RP2350 – Part 2
Having gone through the basics in my previous post: Watching a Z80 from an RP2350, I’m now turning to the memory of the Z80 to see what I can do there.
As I mentioned last time, there is nothing particularly unique about doing this, and a number of projects have been here before.
Once again, I’m using my PGA2350 breakout to monitor my RC2014.
Z80 Bus Memory Writes
As described in part 1, there are a number of different bus control signals, creating different bus cycles. What I need to watch out for are memory writes. The Z80 CPU User Manual shows the timing diagram:
This appears relatively straight forward – we can see that a valid write will have the following:
/MREQ = 0 (active)/RD = 1 (inactive)
/WR = 0 (active)
/IORQ = 1 (inactive)
/M1 = 1 (inactive)
At this point in the cycle, the address and data buses will have valid data. There is one additional complication however – memory refresh. But I believe that during a memory refresh cycle, as well as /RFSH = 0 (active), I don’t think either of /RD or /WR will be active, so I think it’s probably ok.
Memory accesses are generally 3 T-states (clock cycles) long, so at 10MHz, that will be around 300nS for one write, which isn’t very long. But if a RP2350 is running at 150MHz, that would mean there are around 45 clock cycles within which to detect it.
The Z80 CPU card I’m using in my RC2014 for this is the SC149 which has a built-in clock module running with a 7.3728MHz crystal.
RP2350 Updated Code
In order to be able to see what is going on, I want some means of feeding back what has been pulled off the bus. So the plan is as follows:
- Create a 64K memory buffer on the RP2350.
- Watch for /WR and /MREQ and pull the address and data off the bus.
- Use the address as the index into the 64K buffer and store the byte of data.
- Use both cores:
- core 1 will be running just the watching and storing code, nothing more.
- core 0 will be running some IO code that allows the memory buffer to be interrogated.
With the unofficial Arduino RP2040 core, the simplest form of multicore processing is to define two setup() and loop() functions, as described here: https://arduino-pico.readthedocs.io/en/latest/multicore.html
I’ve implemented a simple memory dump command. The serial monitor can be used to enter a 4-digit hex address, between 0 and FFFF and 16 bytes of memory will be dumped to the serial console.
The basic logic is as follows:
ram [65536]setup()
Initialise serial port
loop()
Wait for serial input
Read characters as a hex string
print out the line of memory
setup1()
Clear ram[] buffer
Initialise the GPIO for address and data reading
loop1()
IF /WR and /MREQ
Read address and data lines
ram[address] = data
First use
The RC2014 SC monitor has memory dump, edit and fill commands. The RC2014 memory map is as follows:
- 0000-7FFF ROM
- 8000-FFFF RAM
When running against simple writes, the memory writes are successfully detected and the data stored in the correct places.
When performing a memory fill command however, it would appear that some writes are missed.
RC2014 Console:
*helpSmall Computer Monitor by Stephen C Cousins (www.scc.me.uk)
Configuration S8 20221216, Monitor 1.3.1, SCZ80 BIOS 1.3.1
Monitor commands:
A [<address>] = Assemble | D [<address>] = Disassemble
M [<address>] = Memory display | E [<address>] = Edit memory
R [<name>] = Registers/edit | F [<name>] = Flags/edit
B [<address>] = Breakpoint | S [<address>] = Single step
I <port> = Input from port | O <port> <data> = Output to port
G [<address>] = Go to program
BAUD <device> <rate> | CONSOLE <device>
FILL <start> <end> <byte> | API <function> [<A>] [<DE>]
DEVICES, DIR, HELP, RESET
BASIC Grant Searle's adaptation of Microsoft BASIC
WBASIC Warm start BASIC (retains BASIC program)
CPM Load CP/M from Compact Flash (requires prepared CF card)
*fill 8000 8100 a5
*m 8000
8000: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 ................
8010: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 ................
8020: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 ................
8030: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 ................
8040: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 ................
8050: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 ................
8060: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 ................
8070: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 ................
8080: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 ................
8090: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 ................
80A0: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 ................
80B0: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 ................
80C0: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 ................
80D0: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 ................
80E0: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 ................
80F0: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 ................
8100: A5 D9 BA E8 B2 C3 FA 0B 20 0A AC F3 38 0A 89 A6 ........ ...8...
8110: CA 8B C0 92 A0 34 AE 67 2B 82 AB 8E EB A3 F8 DE .....4.g+.......
Arduino serial monitor:
8000: A5 A5 A5 A5 00 00 00 00 A5 A5 A5 A5 00 00 00 00 ................8010: A5 A5 A5 A5 00 00 00 00 A5 A5 A5 A5 00 00 00 00 ................
8020: A5 A5 A5 A5 00 00 00 00 A5 A5 A5 A5 00 00 00 00 ................
8030: A5 A5 A5 A5 00 00 00 00 A5 A5 A5 A5 00 00 00 00 ................
8040: A5 A5 A5 A5 00 00 00 00 A5 A5 A5 A5 00 00 00 00 ................
8050: A5 A5 A5 A5 00 00 00 00 A5 A5 A5 A5 00 00 00 00 ................
8060: A5 A5 A5 A5 00 00 00 00 A5 A5 A5 A5 00 00 00 00 ................
8070: A5 A5 A5 A5 00 00 00 00 A5 A5 A5 A5 00 00 00 00 ................
8080: A5 A5 A5 A5 00 00 00 00 A5 A5 A5 A5 00 00 00 00 ................
8090: A5 A5 A5 A5 00 00 00 00 A5 A5 A5 A5 00 00 00 00 ................
80A0: A5 A5 A5 A5 00 00 00 00 A5 A5 A5 A5 00 00 00 00 ................
80B0: A5 A5 A5 A5 00 00 00 00 A5 A5 A5 A5 00 00 00 00 ................
80C0: A5 A5 A5 A5 00 00 00 00 A5 A5 A5 A5 00 00 00 00 ................
80D0: A5 A5 A5 A5 00 00 00 00 A5 A5 A5 A5 00 00 00 00 ................
80E0: A5 A5 A5 A5 00 00 00 00 A5 A5 A5 A5 00 00 00 00 ................
80F0: A5 A5 A5 A5 00 00 00 00 A5 A5 A5 A5 00 00 00 00 ................
8100: A5 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
8110: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
As part of the address reading code I was still updating some debug LEDs to represent the data being accessed. Removing the updating of the LEDs, fared better, but still missed the odd byte:
8000: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 00 ................8010: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 ................
8020: A5 A5 A5 00 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 ................
8030: A5 A5 A5 A5 A5 A5 A5 00 A5 A5 A5 A5 A5 A5 A5 A5 ................
8040: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 00 A5 A5 A5 A5 ................
8050: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 00 ................
8060: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 ................
8070: A5 A5 A5 00 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 ................
8080: A5 A5 A5 A5 A5 A5 A5 00 A5 A5 A5 A5 A5 A5 A5 A5 ................
8090: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 00 A5 A5 A5 A5 ................
I also never seem able to see any writes that happen to memory as part of the SC monitor starting up and running. The additional data that is seen above is probably just random data kicking around in the SRAM from a previous session.
So the basic principle seems sound, but there are naturally performance issues to consider. Some options include:
- Overclocking the RP2350.
- Slowing down the Z80.
- Using hand-optimised assembler in the loop.
- Switch to using the PIO and DMA to automatically respond to memory writes.
It turns out that overclocking the RP2350 is trivial when using the unofficial Arduino core – just select the CPU speed as one of the Arduino board options:
When running at 200MHz I seem to be able to capture all the writes from the SC monitor “fill” command running on a Z80 clocked at 7.3728MHz:
*fill 8000 8fff a5*m 8000
8000: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 ................
8010: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 ................
8020: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 ................
8030: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 ................
8040: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 ................
8050: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 ................
8060: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 ................
8070: A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 A5 ................
Paging through on the Arduino, all RAM in the range 8000-8FFF is indeed set to A5. It remains to be seen if this will be quick enough for some actual code running on the Z80, but this is very promising.
There are options to overclock up to 300MHz, but I don’t want to push things. Yet.
But really, the way to do it will be PIO.
Conclusion
This feels like some significant progress has now been made. It is really great to know I can grab writes off the Z80 bus and mirror them in memory.
I will need to perform some kind of extended test, but really that requires some running code on the Z80 now rather than commands in the SC monitor.
And for robustness I really do need to implement a PIO and DMA autonomous memory grabber such as that used on OneROM.
I am also thinking I’d like a jumper-configurable clock module for my RC2014. Something along the lines of that used with a Z80 CPU Tester where a LS193 binary counter or similar allows for subdivided clocks. This would be really useful for experimenting.
But for now, this will do nicely and might even be enough to allow me to go to the next stage of what I’d like to do.
Kevin
#pga2350 #rc2014 #rp2350 #z80






