15

My little Z80 breadboard project got stuck in the mud where I am pulling my hair out. It looks like the CPU is making a calculation error on a relative jump. Here is my code, and I don't think ZX Spectrum or things like this. I don't have any I/O devices, only an Arduino Nano and I'm running at a dozen Hz of a 555 astable clock.

So all the I/O I have is the Arduino interrupting on Z80's /RD CHANGE and on /WD FALLING. Then /IOREQ is sampled to determine if it's an I/O request for us or memory activity. If it's an IOREQ right now it asserts the /WAIT state and I can type a response on the serial console. Otherwise, I just observe memory activity, and it works pretty nicely. I see the code come by my monitor as it is executed. Here is my program:

0000                main
0000  3E 3E                LD A, '>'    ; output a "prompt"
0002  D3 00                OUT (0), A
0004  DB 00                IN  A, (0)   ; input a byte
0006  57                   LD  D, A
0007  CA 27 00             JP  Z, halte ; by accident I left JP and it works
000A  DB 00                IN  A, (0)
000C  15                   DEC D
000D  28 1B                JR  Z, echo
000F  6F                   LD  L, A
0010  DB 00                IN  A, (0)
0012  67                   LD  H, A
0013  15                   DEC D
0014  28 18                JR Z, exec
...
0027                halte
0027                 ; just stop
0027  76                   HALT
0028  18 D6                JR main
002A                echo
002A                  ; write the same byte out that was just read
002A  D3 00                OUT (0), A
002C  18 D2                JR main
002E                exec
002E  E9                   JP (HL) ; I wish I could push PC to (SP)
002F                       ; bye bye, no return
...
0045  18 B9                JR main ; last instruction

I wrote this into my EEPROM by

  1. erasing everything with $FF
  2. filling $0000 - $0200 with $00
  3. then writing the code in
3E 3E D3 00 DB 00 57 CA  27 00 DB 00 15 28 1B 6F
DB 00 67 15 28 18 DB 00  4F DB 00 47 15 28 10 15
28 15 15 28 18 18 D9 76  18 D6 D3 00 18 D2 E9 DB
00 77 23 10 FA 18 C9 7E  23 10 FC 18 C3 DB 00 5F
DB 00 57 ED B0 18 B9 00  00 00 00 00 00 00 00 00

This will be useful for what's happening next.

I see system reach HALT state if I respond with $00 on the console. Here is a transcript of what my Arduino thing shows (I make comments with ;)

0000 > 3e
0001 > 3e
0002 > d3
0003 > 00
IO: 0000 < 00 ; sampling the data lines worked well with monitoring MREQ but with IOREQ it fails :(, should be 3E not 00
0004 > db
0005 > 00
IO: 0000 > .. ; here my /WAIT lamp comes on and CPU is waiting
g 00          ; I say "give 00" and it continues
0006 > 57
0007 > ca
0008 > 27
0009 > 00
0027 > 76     ; the HALT makes the /HALT LED come on
0028 > 18     ; and that is what happens during HALT, we keep reading 18 from the next opcode but never proceeding
0028 > 18
0028 > 18

This made me very happy as it is my first Z80 program actually running on my breadboard.

But when I respond with 01 or 02, something very strange happens!

0000 > 3e
0001 > 3e
0002 > d3
0003 > 00
IO: 0000 < f0  ; no idea why this time the data lines were sampled as $F0, but you see for sure that memory is read well through those same wires.
0004 > db
0005 > 00
IO: 0000 > ..  ; /WAIT state asserted
> g 01         ; this time I give not 0 but 1
0006 > 57
0007 > ca
0008 > 27
0009 > 00      ; JP halte not taken
000b > 00      ; here Arduino skipped a beat 000a DB, but we can see that it is just a sampling failure, CPU marches on:
000c > 15
000d > 28      ; JR
000e > 1b      ; 1B should get us to $0f + $1b = $2a, yes
0036 > 00      ; but WTF! Why are we at $36 now??
0037 > 00      ; and it's all 00
0038 > 00
0039 > 00
003a > 00

I need to explain here that I only sample the address lines A0 - A5, that means if I see $36 comes out as 00, it could only be $76, $B6, $F6, $136, $176, $1B6, $1F6, remembering that my EEPROM doesn't have 0 until after address $0046 and then only until $0200, after which it's all $FF. With a relative jump, however, from $000F I could not reach past $008f, so $76 is the only possible address where we jumped to.

Why would we jump to this weird address?

The only reason I could imagine is I have twisted some data lines (the Z80 pin-out is crazy that way), but the chance for that is pretty low, since it reads all the other instructions correctly.

I tried to find some flipped bit, but when I take the next code 02, it gets stranger even:

0000 > 3e
0001 > 3e
0002 > d3
0003 > 00
IO: 0000 < ff ; it must be a timing issue that I can't sample the data lines during IOREQ with /WR low (active)
0004 > db
0005 > 00
IO: 0000 ..   ; /WAIT is asserted
g 02          ; I reply
0006 > 57
0007 > ca
0008 > 27
0009 > 00
000b > 00     ; we skipped the exact same beat again $000A with $DB
              ; oops, weren't we supposed to get the IN IOREQ now? missed it :/
000c > 15     ; since we took 000B as NOP, we are at the DEC D, now
000d > 28
000e > 1b
0010 > db     ; and we didn't skip the IN IOREQ here
0011 > 00
IO: 0000 ..   ; /WAIT is asserted
g 00          ; I respond with 00 (that's an operand, the function code is still being decremented in D
0012 > 67     ;
0013 > 15     ; DEC D to zero and
0014 > 28     ; JR
0015 > 18     ; to echo but ...
0037 > 00     ; DAMMIT!
0038 > 00
0039 > 00

So again the JR is going off into the open pastures. And there is no discernible constant error here. In fact, while the last time we jumped to 36, now we jump to 37.

Surely I still have timing issues, but I can be pretty certain that the 28 1b and 28 18 JR instructions have been read as have all the others, and het they go off into the wilderness.

UPDATE: I see I already got an answer, but I just found a very peculiar case that I want to keep as it may be symptomatic and indicative of the problem.

0006 > 57
0007 > ca      ; skipped 3 beats here!!
000b > 00
000c > 15
000e > 1b
000f > 6f
0010 > db      ; we jumped mid-instruction over 3 bytes!!!
0014 > 28      ; and we took this as the argument to the IN instruction!!!
IO: 0028 > ..  ; here is the proof. DB ... 28 was executed
g ff           ; now I give FF as the result of the IN
0015 > 18      ; but now we are off and take the address of JR Z, +18 as a JR unconditional
0016 > db      ; with DB as the increment, negative!
0016 > 00
0017 > 00
0018 > 00

And we are back in the territory between 0046 and 0200. How can we possibly get here with a backward relative jump!?

What can make a PC skip three beats between M1 and the operand read? It's like a button bouncing. Noise? And why does $000A get consistently skipped over?

As for the other ideas, all good. But right now I have all inputs tied to high with a 1k resistor. So /IRQ and /NMI are held high.

5
  • 1
    Way back when, when I was writing production Z80 code for embedded systems (early 80s), we had a Z-80 in-circuit emulator. It allowed you to single step (instruction by instruction) through the code. At the time, they cost a fortune (and ours was fought over by three teams). But, they were very handy for figuring out oddball issues (though we never ran the clock other than what the system called for). I expect that, if they still exist, they cost a lot less
    – Flydog57
    Aug 16 at 15:36
  • 2
    Here is another similar project clocked at 7 Hz (also 555): Why when outputting registers' value the result is a wrong fluctuating value in a Z80 CPU?. What is your chip exactly? An original NMOS one? A "static" CMOS one (e.g., Z84C0008)? Aug 17 at 2:27
  • @PeterMortensen apparently an original NMOS. Aug 17 at 5:32
  • 1
    go through your CPU datasheet and look for clock ratings some chips has minimal frequency in order to work properly and can not be run at low speed ... IIRC original NMOS needs at least 250KHz. For those if you really need to be slow you could block the CPU like DMA chip does ... using IIRC BUSAK,BUSRQ pins also I think you could use WAIT too but that would need more circuitry I think
    – Spektre
    Aug 17 at 6:51
  • Is a Z80 programming question on-topic for Stack Overflow given that people presumably still write Z80 code?
    – qwr
    Aug 17 at 7:28

3 Answers 3

18

I thank Raffzahn for his teaching me some Z80 basics, which indeed I don't have.

But on the main issue that I was pulling my hair out over, I think I found the problem. There appears to be an issue with clocking the Z80 too slow, which is somehow indeed causing some fluctuations or noise inside the CPU.

So, when I turned up the clock, I didn't get these problems any more.

I think it is known from 6502 NMOS technology too, they are not static, so you can't just stop the clock (or reduce the frequency under a certain threshold) and expect the registers not to get messed up.

And I have a confirmed solution for extremely slow clocking simply by keeping the clock line high most of the time and having fixed downward spikes. According to this picture:

enter image description here

(forget that diode D1 here, not using that)

Now I have Rb fixed to 1 kOhm and C is 1 uF. So we have the low phase a fixed 1 ms, hmm, that is 500 times longer than the maximum 2 us said it should be, but I don't have a problem with it. Don't have a scope either to actually measure it. My clock LED I removed because it looks like constant on, but I plugged the LED on the side of an inverter of the clock signal, and I see very brief flashing. I suppose 1 us I would not see, but 1 ms (or longer) I can see. (A homebrew computer needs blinkenlights after all.)

Anyway the Ra I have now adjustable between 0 and 1 MOhm. So it can be run really slow, like 1 Hz, and probably I could make it even slower to even stop the clock in this state and it will continue working reliably.

5
  • 3
    Thanks. The Z80 is not complicated, it's just a different way of thinking, so forget your 6502 knowledge and learn to love the 80s way:)) The according to the 1981 data book the NMOS CPUs are defined as max 2µs low and static high. This means, using a symetric clock that 250 kHz is the minimum frequency. Using asymmetric clock (longer high) is in theory unlimited, but the manual warns to go past 200µs. So minimum clock is ~5 kHz is possible (2µs low 200µs high).Then again, using a CMOS version should solve that for any speed as it's static.
    – Raffzahn
    Aug 15 at 18:52
  • So I need to configure my 555 timer so that it does quick low phases and extends any period in the high state. I noticed that this uneven duty cycle happens with 555 timers. Just now I don't have a scope to verify that. Aug 15 at 19:31
  • 5
    Jup, that's one way. Another would be to have the Arduino do the clocking. Software rules (isn't that the 6502 way?) Then again, just gift yourself a CMOS and you're able to do any speed :))
    – Raffzahn
    Aug 15 at 19:35
  • 1
    wouldn't this apply here: minimum clock rate 250 kHz
    – dlatikay
    Aug 16 at 10:47
  • 2
    Note that this isn't a case of "it happens not to work at low clock speeds... must be a bug". Almost all microprocessors /require/ a certain minimum clock speed, I think that the 1802 was one of the very few exceptions. Aug 16 at 18:35
14

I'm not sure, but this does look a bit like the typical trap programmers fall for when coming from CPUs that set flags according to data handled:

0004  DB 00                IN  A, (0)   ; input a byte
0006  57                   LD  D, A
0007  CA 27 00             JP  Z, halte ; by accident I left JP and it works

68xx or 65xx CPUs do set flags according to any data moved, x80 CPUs don't. Flags are only updated after ALU operations. To see if the byte read is zero, an ALU operation needs to be performed:

    IN  A, (0)   ; input a byte
    AND A
    LD  D, A
    JP  Z, halte ; Jump if byte was zero

Now the jump will be taken if the byte read is zero. Before the Z flag was, within the context of the I/O operation, undefined. It showed whatever prior operation did leave there.


P.S.: That JP shows as well nicely the use of an absolute address for conditional jumps. One that needs relocation if you want to move that code around.


P.P.S.: Do not start code direct at 0000h, always put a JUMP there and have your code start after the vectors (0040h), it's a great way to insert random problems as soon as interrupts come into play.

So even if you do not use interrupts or any of the RST instructions right now, keep the first 64 bytes for vectors, best is to populate the first with a jump to your reset routine (JMP main) and fill all other with a jump to some error handling like

   ORG   8      ;8/16/24/32/40/48/56
   LD    A,'$'  ;Show a $-bomb'
   JMP   ECHO

Or maybe a different symbol for each vector. Avoids errors and helps a lot in debugging later on.

2
  • I agree that the flag testing thing is a problem, but I do not think it is the full story. Why does inputting $00 consistently work but everything else fail in the same way?
    – JeremyP
    Aug 15 at 15:45
  • 2
    @JeremyP Doesn't make much sense to search ahead, especially as his 'exec' features a jump to nirvana. Lets eliminate the first problem and then continue to see what happens. Even more as he has described quite a lot, but never said what he expects the happen with the input given (yes, I can read code, but code is not Spec, Code is Documentation).
    – Raffzahn
    Aug 15 at 15:57
1

Not an answer to the question, but a suggestion: You said a couple of times that you don't have a scope. Well, get one. You can buy tiny DS0138 digital scope kit for literally pocket change, AND it fits in your pocket! Here's an Amazon link to one for $26. A nice case is a few $ more.

https://www.amazon.com/DS0138-Digital-Oscilloscope-Unsoldered-Workshop/dp/B01MT4NZRR

1
  • 3
    I live on 3 different continents and did not expect to be in India for so long, so I did not take my little handheld scope with me that I bought last time I was in USA. In India I get chips cheap, but didn't find the cheap DSO handheld scope or an EEPROM programmer (hand-wired that one on a breadboard, haha). Aug 16 at 19:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.