Retro Z80: CPU Test Circuit

There is this simple test circuit that allows you to test your Z80 CPU in a very basic way. There are several incarnations and I have found several YouTube videos, but I will link in the Z80.info site.

So I took out the bread board and set to work and got the NE555 running with a nice enough clock pulse. I put in a pot to be able to vary the clock frequency a bit. Then I plugged in the Z80 and connected all the wires and resistors. I used a segmented LED bar for visualizing the address lines, but initially only connected 3 address lines.

Then I turned it on and no lights… Doh! I always forget that the outer power tracks are divided in two on this bread board. So after placing a jumper wire, it worked.

I thought it would be cool to use all the LEDs in the LED bar so I connected the other address lines (A0-A9) too.

That worked nice and you could see the binary counting of the address lines… But wait, why is A7/LED 8 and higher blinking!?

Here is a YouTube video I made of my test circuit where you can see in action.

First it starts slow, then there’s a short segment that’s really  fast, then at the end, you can see the blinking.

So why do you think that is?

Remember that the Z80 also does DRAM refresh. For the NOPs we’re executing 2 clock cycles are spent using the address bus to access memory, and the last two cycles are used to refresh the DRAM. During the refresh the lower 7 bits of the address bus (A0-A6) is set using the contents of the R register. The other address lines are reset to zero (this is actually the I register and the I register is zeroed out on reset). The R register is incremented at every instruction (M1 cycle, the first 2 clock cycles) and so, in this case, are synchronous to the counting of the address lines.

The blinking red led on the left side on the video is on when the REFRESH line is active (low). It is perhaps a bit hard to see on the video, but if you slow it down, you can see the upper address lines (A7-A9) are on when REFRESH is off and visa versa.

You don’t realize that immediately (at least I didn’t) when you see the circuit and the video’s other people made. They usually have only 3 LEDs on there.

Learned something today.

 

Advertisements
Published in: on December 22, 2015 at 3:51 pm  Leave a Comment  

Retro Z80: Debug facilities

When building a computer from scratch you are bound to run into bugs and problems. Also for software development (the bios for instance) you will want some way to debug your code. There are no fancy integrated development environments for the Z80 (as far as I know) – so this will be roughing it.

So what can we do to make life a little easier?

Hardware

There are hardware related facilities as well as software related facilities. Lets look at the things we can do to or for the hardware to make debugging easier.

Clock Speed

We have a 20MHz Z80 CPU which at full speed goes pretty fast. It would be difficult to see anything actually happen. The use of a logic analyzer may help of course but only if the sample rate is fast enough.

It sometimes helps to slow things down to a crawl and actually see the state changes in front of your eyes – then usually it clicks in your brain what the problem is.

So having a clock that can be very slow or even hand-operated pulsing clocks with a button is very handy. Also a medium speed clock will come in useful to flesh out any hardware timing related issues the design may have. And of course a full speed clock at the intended CPU operating speed.

If you really want to make it spiffy, then a single shot instruction clock would be nice. This clock would monitor the M1 line of the Z80 that indicates the start cycle of a new instruction. The Z80 instruction usually take multiple clock cycles to fully complete – or even multiple machine (M1) cycles. I haven’t seen any circuit diagrams for this anywhere, but I’m sure we can think of something.

Bus Spy

Spying the signals on the bus of the computer can be a good way to understand what is going on. This can be as simple as attaching LEDs to each bus signal (buffered as not to load the bus too much) and running at low clock speeds or single step clock mode.

At the other end of the spectrum is the option of having an intelligent bus spy that can decode the address and instructions etc. I am sure something like this exists (I am not the first to build a computer), I just haven’t looked for it. Do you know any good/cheap ones? Please leave a link in the comments.

General Purpose Visualization

We could attach a module to the system bus that visualizes data – the simplest being several LEDs. The software could latch data into the registers of this module to turn LEDs on and off – visualizing the data.

Other visualizations could include a 7-segment display or even a LCD display (HD44780).

Test Routines

We could write some software routines the exercise specific parts of the hardware in some way. Think of these as test scripts. The simplest one is a thorough memory test routine that checks if all memory addresses can be written to and read form with the correct value. Main interest here are the memory boundaries but a full sweep of all memory addresses may even catch more types of faults.

Debugging

As the software is being developed, it will become necessary to have some form of debug support. I assume that the Z80 system is connected to a PC over a dedicated serial debug port and that the PC runs software that interacts with our on-board debug facility.

This also implies that we have a separate entity on the system that controls all this without involving the CPU – lets call it a system controller. For now the idea is to put an MCU with an external bus interface on the system to provide the debug services as well as provide I/O (SPI, I2C, UART etc). At a later stage this may be swapped out for an FPGA. But because I am an FPGA beginner – learning along the way- I’d thought we’d start (relatively) simple.

Catch stray execution

It probably would be beneficial to catch bad jumps due to programming errors or bad data in a vector address table. The Z80 could fly off into nothingness, never to be seen again. So I have been thinking if you could catch something like that. I have come up with a simple trick that just might be enough.

At startup, initialize all memory locations with the value for one of the RST instructions (0xFF for RST38). Then load the program at the specific address and jump to it. Now if the execution flow spins out of control it may jump to a memory location that has the RST instruction written and the (debug) handler there, can even retrieve the jump address from the stack and report the problem over the debug serial link.

Further stack analysis may reveal more info on the execution flow previous to the fault. That analysis can be done by the software on the PC.

Breakpoints

I though it would be awesome to support debug breakpoints for the Z80. That would certainly help with software development. But how?

There is one instruction on the Z80 that came to my mind immediately: HALT. This makes the CPU execute NOPs and signals the !HALT line on the CPU. This is excellent for hooking in a breakpoint mechanism.

The HALT instruction is only one byte and therefor easy to fit in between existing code. The breakpoint have to be compiled in (for now) so you cannot set them dynamically. Allowing to do so would require in-memory modifying the code, which brings all kinds of nasty (re)addressing issues to the -already full- table.

A Non-Maskable Interrupt is issued to start the program again and continue where we left off.

One thing we need to watch out for is to disable and interrupts on a hardware level when the HALT signal is active. This is because any kind of interrupt will exit the HALT condition.

Wrap up

These are just some ideas I came up with and not all of them will probably work right of the bat. It is a start anyway and perhaps even inspire the reader.

 

Published in: on December 21, 2015 at 9:53 am  Leave a Comment  

Retro Z80: Deciding on Memory design

I am trying to decide how to organize the memory layout for the Z80 retro computer project I am doing. The Z80 only has 16 address lines which allows it to access 64k of linear address space. If you want more memory than that – and we do – you have to organize the memory in parallel banks and switch between them, also know as (memory) bank switching. There are many flavors of how to design bank switching and there are many factors that would favor one over the other. Lets look at some of them.

I write this mainly for myself because I find that writing things down forces you to organize your thoughts and reveals weak spots in your thinking.

Bank Switching

With bank switching you add multiple banks of memory (in parallel) for a specific address space and determine through a latch which one of the banks is active. There can be only one active bank at a time for one address space. It is good to realize that these memory banks do not have to reside in separate physical chips. So say you have a 128k RAM chip, that would allow you to implement 2 banks of 64k by controlling the A16 line of the chip. If you do have separate chips, you usually control them with the Chip-Enable (CE) line to make sure the data lines are in a high impedance state and no data is written into the chip when the chip is not active.

So bank switching involves some address decoding to control what (part of the) chip(s) is/are active when others are not.

Video Memory

Another aspect of managing memory is accessing video memory. Video memory is a dedicated memory address space (or bank) that will contain the graphics to display on the (VGA) screen. Dependent on the screen resolution and color depth a specific amount of memory is required. This adds up quickly.

Contention will occur between the processor wanting to write into the video memory to update the display and the VGA controller that continuously need to read the video memory to output it to the monitor. Usually the VGA controller takes precedence over the CPU access to avoid glitches in the image on the display.

This video memory contention complicates matters further and need to be thought through carefully. I see several solutions to this problem.

Dual-Ported Memory

The first obvious solution is to have dual-ported memory. A port is a complete address (for the RAM size of the chip) and data bus. This is a special type of memory that has two ports that can access the memory simultaneously. It has built-in logic to lock out access to the same memory cell by the two ports (semaphore). The granularity is at cell level so there is a high level op parallelism. This is a very easy solution to the problem with the only downside is that this type of memory is not cheap.

There is also two-ported memory that is also suitable. Two-ported memory has one dedicated read port and one dedicated write port. Dual-ported memory has at least one read/write port. Also note that FPGAs often have internal dual ported memory blocks.

Segmented Memory

Another solution would be to use separate physical RAM chips. This would allow simultaneous access to different chips for the two ports (CPU and VGA controller). These ports would require extra logic IC’s like latches. The smaller the size of the memory chips the more of them you need but also the more parallel the two processes can access the video memory. The granularity is determined by the chip memory size and has a low(er) level of parallelism. Because of this you still would need something to halt the CPU when a collision is eminent – there is still a chance that the CPU and VGA controller want to both access the same chip.

Double Buffering

There is also the option to use double buffered video memory. This is a technique that toggles two parallel video memory spaces (banks) between the CPU and the VGA controller. At any time the CPU has access its own video memory bank and the VGA controller has access to the other bank. When the CPU is done writing to the video memory it indicates the at the next appropriate moment the banks can be switched (swapped) presenting the VGA controller with the new video information.

One problem that occurs here is that the CPU’s video memory now contains an old snapshot of the video data. That means if the CPU partially updates the video memory – for instance only updates (bit-blit) one small area on the screen, the rest of the scene is old data from the previous display image. I do not have any ideas how to solve this yet – so leave a comment if you do.

Graphics Processor

An alternate way of doing video is to use a separate graphics processor. This relieves the CPU from the task of creating the graphics. It also opens the door to a different hardware solution where communication with the graphics processor is done through I/O and not shared memory. So there is no contention.

The program would communicate in drawing primitives to the graphics processor which would reduce the amount of data to be exchanged quite a bit.

I need to delve into sprites and this type of graphics processing in general a bit more before making any final decisions.

Software Implications

Getting the hardware to correctly bank switch memory blocks is relatively easy although there are a lot of flavors in varying levels of difficulty. Another question is how is the software going to deal with memory spaces ‘suddenly’ appearing and disappearing in the program? I could not find very much on this on the interweb – so again, if you know any resources, please leave them in a comment.

One obvious implication is the stack pointer (SP). What happens to the stack when you swap out the memory block it is currently located in? Or if you want to jump or call code that is loaded in another memory block? And how do you get back to the calling address (RET)?

This all requires very precise orchestration in the software to not crash the system. It stands to reason to facilitate managing memory block switches by some sort of built-in service routine that knows the details of how to manage this for the program that is running. Using a RST instruction is a very common way to provide service routines that perform this type of important and frequently used operations.

Note that these RST instructions require the lower memory page (high address byte is $00) to be initialized with some sensible code. That means that this lower area of all memory banks in this address space, is off-limits to a any program and needs to be initialized by a boot mechanism. For now I assume that this is all RAM.

Note that other addresses may need to be included in this safe area, such as the NMI Service Routine ($0066) or perhaps an ISR vector table for IM2 (depends on the value of the I register).

Idea: A system controller can perform the boot sequence and initialize all the RAM banks that overlap with this address space. It could then set a bit (latch) to disable writing to this part of the memory (of any bank) at a hardware level. This way that crucial address space cannot be corrupted by buggy programs.

All RST instructions push a return address onto the stack. This can be used to resume the program where the call was made. It also allows the RST functionality to intercept and redirect execution flow as well as retrieve extra parameters from the call address etc.

When execution is transferred from one memory bank to another, there always has to be a ‘common ground’ to make the switch from. It stands to reason to leave the lower memory space static to provide this basis.

Stack Pointer

The stack pointer and the contents of the stack itself are tied to the code that is currently executing, including all the calls and push instructions that may have been performed up until this moment.

So it seems logical to locate the stack in the same memory page as the code is executing.

Jumps and Calls

Jumps and calls are always within the 64k address space. This is not to complicate the instruction set. A separate way of calling out-of-context code is provided with RST service routine. A small structure can be supplied by the caller to indicate what code to execute on what memory page.

We could reuse the notion of a vector table (by index) we use for interrupts and will probably use for public BIOS functions.

Interruspts

The I register is initialized to 1 so a protected version of the ISR addresses is at the bottom of the memory (base page or all banks). A program (or OS) can override by setting I to another value but has to do all management – including bank switches.

BIOS ROM

BIOS contains the bare essentials for interfacing with the hardware (I/O). This includes bank switching but also keyboard, mouse, display, printer, network and storage (disk) access. It does also require a bit of RAM to maintain several structures for its services.

These service routines can be used by any program to help with memory bank/page switching.

Loading Programs

The size of the memory blocks also influences how programs are loaded. What if programs are larger than a memory block. What if they are larger than 64k?

We may have to design a program file layout that is relocatable and publishes entry and exit points. This clearly needs some more thoughts.

Memory Usage

Memory is typically used for several different types of data. One is programs. Another type is data. The system itself requires a reserved region of memory.

These different types of memory usage could be taken into account by the memory manager to enable/disable certain services for those blocks (performance optimization).

Multi-Threading

An OS could use memory banks to execute parallel tasks and use the memory manager to direct the programs to specific parts of the memory space and memory blocks.

Each process (program) can get its own base page. Not sure if that helps or just makes things worse. We’ll have to see.

 

Published in: on December 14, 2015 at 7:44 am  Leave a Comment  

Z80 CPU boot.asm template

I am researching how to build a Z80 retro computer. For this I read about the Z80 and its instruction set. There are a couple of fixed addresses the Z80 CPU uses that you better get right. So I made a template file that can be the starting point of writing your own Z80 boot/bios ROM.

First of all there is !RESET (! means not or active low). The reset vector of the Z80 is address $0000 (hex). So here goes code that initializes your system.

But wait there is more…

Then there are the RST instructions. The first one RST00 jumps to address $0000. Sort of a soft reset. Your code might want to check if the current boot is from a hard reset or a soft reset.
Other RST instructions jump to addresses: $08, $10, $18, $20, $28, $30 and $38 (all on the first memory page, so the high byte of the address is $00). So all these locations should at least contain something to get your system going again. You could also think of these as shortcuts to often used bios functions.

But wait there is more…

In Interrupt Mode 1 and interrupt on the !INT pin of the Z80 will trigger an RST38 (hex). So if you want to allow IM1 you need to take that into account. Interrupt Mode 0 (the old 8080 mode) allows the interrupting device to put an instruction on the databus which the Z80 will execute. To make it easy, devices usually put RST instructions on the data bus. Personally I think this mode is less useful. Interrupt mode 2 provides a very flexible dispatch mechanism. When the !INT pin is put low by the interrupting device, a low address can be put on the data bus. This, together with the I register for the high address bits forms an address that contains a jump table with 16 bit addresses. Each address should be the start of an Interrupt Service Routine (ISR) for that specific interrupt/device. Up to 128 interrupts can be managed in this fashion.

So its important, if you use IM2 to determine a location for the ISR jump table (the value of the I register).

But wait there is more…

Finally there is the Non-Maskable Interrupt. When the !NMI pin is put low the Z80 jumps to address $66. The code that runs there should end with the RETN instruction.

All in all a nice list of fixed addresses you should take into account when writing a Z80 ROM.

 

 

; bios.asm
; Declares fixed addresses and startup routines

;
; Page 0 memory layout
;

; !RESET and RST00
ORG $0000
SEEK $0000
di ; from warm boot (rst00), interrupts may be enabled
jp ResetInit

; RTS08
ORG $0008
SEEK $0008
; di ; if you need interrupts disabled, do that here
jp BiosFn1

; RST10
ORG $0010
SEEK $0010
; di ; if you need interrupts disabled, do that here
jp BiosFn2

; RST18
ORG $0018
SEEK $0018
; di ; if you need interrupts disabled, do that here
jp BiosFn3

; RST20
ORG $0020
SEEK $0018
; di ; if you need interrupts disabled, do that here
jp BiosFn4

; RST28
ORG $0028
SEEK $0028
; di ; if you need interrupts disabled, do that here
jp BiosFn5

; RST30
ORG $0030
SEEK $0030
; di ; if you need interrupts disabled, do that here
jp BiosFn6

; RST38 and IM1
ORG $0038
SEEK $0038
; di ; if you need interrupts disabled, do that here
jp BiosFn7

; !NMI
ORG $0066
SEEK $0066
; The ISR for NMI goes here. Keep it short and sweet.
retn

;
; ISR Table (IM2) of 16-bit jump addresses (I=1) 256 bytes max
;

; ISR Table is located at page 1 ($0100).
ISR_TABLE: EQU 1
; Page 1
ORG $0100
SEEK $0100
; The lo address byte (A0-A7: A0=0) is put on the databus by the interrupting device.
; The hi address byte (A8-A15) is supplied by the I register that is initialized to (page) 1.
;DEFW ISR0 ; Address of ISR
;DEFW ISR1 ; Address of ISR
;DEFW ISR2 ; Address of ISR
;DEFW ISR3 ; Address of ISR
;DEFW ISR4 ; Address of ISR
;DEFW ISR5 ; Address of ISR
;DEFW ISR6 ; Address of ISR
;DEFW ISR7 ; Address of ISR
;DEFW ISR8 ; Address of ISR
;DEFW ISR9 ; Address of ISR
;DEFW ISR10 ; Address of ISR
;DEFW ISR11 ; Address of ISR
;DEFW ISR12 ; Address of ISR
;DEFW ISR13 ; Address of ISR
;DEFW ISR14 ; Address of ISR
;DEFW ISR15 ; Address of ISR
;DEFW ISR16 ; Address of ISR
;DEFW ISR17 ; Address of ISR
;DEFW ISR18 ; Address of ISR
;DEFW ISR19 ; Address of ISR
;DEFW ISR20 ; Address of ISR
;DEFW ISR21 ; Address of ISR
;DEFW ISR22 ; Address of ISR
;DEFW ISR23 ; Address of ISR
;DEFW ISR24 ; Address of ISR
;DEFW ISR25 ; Address of ISR
;DEFW ISR26 ; Address of ISR
;DEFW ISR27 ; Address of ISR
;DEFW ISR28 ; Address of ISR
;DEFW ISR29 ; Address of ISR
;DEFW ISR30 ; Address of ISR
;DEFW ISR31 ; Address of ISR
;
; DEFW ISR127 ; Address of ISR
; !!! Last ISR !!!
;
; Start of bios
;

; Page 2
ORG $0200
SEEK $0200

; Interrupts are disabled.
ResetInit:
ld a, i ; cold boot/hard reset would init I to $00
cp a, $00 ; is I zero?
jr nz, resetWarm ; nope – warm boot

; TODO
; MemoryTest to determine RAM_TOP

; Initialize Interrupt mode
ld a, ISR_TABLE ; load I with page 1 address
ld i, a ; for the ISR jump table at $0100
im 2 ; for IM2
; ResetInit jumps here if it detects a warm reset
resetWarm:
; Initialize SP

ei ; turn on interrupts

BiosFn1:
; bios function #1

BiosFn2:
; bios function #2

BiosFn3:
; bios function #3

BiosFn4:
; bios function #4

BiosFn5:
; bios function #5

BiosFn6:
; bios function #6

BiosFn7:
; bios function #7
; also used for IM1

 

Published in: on December 7, 2015 at 4:36 pm  Leave a Comment