© James R. Strickland 2016

James R. Strickland, Junk Box Arduino, 10.1007/978-1-4842-1425-1_11

11. Z80 Explorer

James R. Strickland

(1)Highlands Ranch, Colorado, USA

Well, here we are. The last chapter. This one’s the payoff, the big one, the microprocessor chapter. This is the chapter where we finally go inside a system not too different from the Cestino’s ATmega1284P microcontroller and really see what goes on behind the curtain. We’re going to build the Z80 explorer, shown in Figure 11-1.

A340964_1_En_11_Fig1_HTML.jpg
Figure 11-1. The Z80 Explorer

In previous chapters, like Chapter 10, where I talked about interrupts at some length, you’ve had to take it on faith that I’m right. Certainly the code based on that understanding works. You’ve also had to take on faith that somewhere between the C++ Arduino sketch and the “bare metal” of the ATmega1284P, our intent was translated, reasonably, into machine instructions. It certainly seems to work, but there’s a magic-y feel to the process, and I’ve said from the beginning that there is no magic in electronics and computers. (If you dig down far enough, there’s quantum mechanics, but that’s not actually magic either. It just seems that way .)

In this project, we’re going to hook an ancient type of microprocessor, the Zilog Z80, to the Cestino, and program the Cestino to emulate all the other hardware that would normally go with the microprocessor. We’re going to run the microprocessor’s clock very slowly, so we can look into memory in real time. To make the Z80 work, we’ll hand-assemble some programs (more on this later). You won’t have to take on faith how interrupts work. You’ll be able to see them go.

So let’s get started. We have a lot of ground to cover.

The Stuff You Need

For as big as this project is, its part count is very low. There are three. Two of which you’ve used in other projects.

New Parts

  • 1 10kΩ resistor.

  • 1 0.1μF capacitor. Tantalum, like the ones we used to bypass the power supply on the ATmega and the oscillator in chapter 2, and the 74xx00 that is our logic probe in chapter 7. Ceramic disk caps will work, but tantalums are easier to find room for.

New or Used Parts

  • One Z80 CPU. Sadly, not all Z80s will work. The oldest types, the NMOS types, won’t accept a clock as slow as the one we’ll be giving it. “Modern” Z80s in the Z8400 or Z84C00 series will do just fine.

  • There’s some question where the NEC clone, the μPD780 will. Its datasheet says it is effectively a static design, meaning it will accept a clock with a speed of zero, but that below a few hundred kHz is not guaranteed. I have one of these, and I think I developed the original prototype of this project with it, but as my μPD780 has since died, all I can say now is look up your specific IC’s datasheet online, and look in the AC characteristics. If, as the Z8400/Z84C00 models from Zilog do, it lists no upper limit to the clock period, this means it will accept a static clock. They’ll often mention a fully static design in the datasheet as well. You can also just plug your Z80 in and see if it works. It’s junk, right?

  • If you’re ordering a new one (which might be the safest approach) any of the Zilog Z84C00 family, like the 20MHz Z84C0020, I used) will work. They differ only in their maximum clock frequency. As always, make sure it is the 40 pin through-hole DIP version, as the Z80 has been around a long time and comes in a lot of other packages.

  • A tactile button. As with Chapter 10, this button needs to be a momentary switch, normally off. After that, use whatever button you like. Be advised that some tactile buttons (like mine) don’t like to stay in the breadboard, and that they may switch adjacent terminals rather than opposite terminals like you’d expect. If they really give you trouble, you can solder them to a piece of protoboard, add some pin headers, make some connections, and use the pin headers to connect the switches to your breadboard. If there were any projects in this book after this one, I’d probably do that .

Z80 Microprocessor Anatomy

As microprocessors go, the Z80 is a simple beast. At their hearts, modern CPUs are nearly as simple, but they’re complicated by multiple cores, vast, 64 bit busses, caches, complex memory read and write cycles, one or (frequently) more floating point units, vector units, caches, and so on. Nearly all these additions are designed to make the processor do more per clock cycle than the Z80 can. For a given speed in megahertz or gigahertz, a limitation of the very transistors from which the processor is made (among other things), a more efficient processor will get more done, simple as that. All the complexity does make them harder to understand. Even the ATmega1284P, heart of the Cestino, is more complex, despite being an eight bit processor itself. The addition of internal RAM, flash, and all the various I/O modules and functionality that are built in make the ATmega powerful and useful in the Cestino, but the complicate understanding it.

The Z80 is different. It is an early design, when simply making a microprocessor for a reasonable price was the great technical challenge. Its internal data bus is only eight bits, which we’re already familiar with from the Cestino. It handles eight bits at a time, has no caches, no floating point units, no vector units, no nothing. All that’s left is the processor, the bare minimum needed to be a functionally complete CPU.

You’ll notice as we dig deep into the Z80’s innards, especially if you’ve had formal programming training, that a lot of what you find here are classic computing data structures. Variables, stacks, pointers; they’re all here. This is not an accident. A CPU is essentially a program itself, with various functions called by instructions with or without data. If you always thought of yourself as a software person like I did, this may be a bit of a shock. There’s no there there. There’s no intelligence in the CPU. It’s a program written in transistors for running other programs. You already know everything you need to understand what goes on in the CPU .

Busses

We’ll start with the busses. There are four of them in the Z80, three of which have direct connections to the outside world.

Memory Address

The memory address bus, despite being a 16 bit bus, is the simplest conceptually. It’s a series of output lines not unlike a pair of the ATmega’s ports in output mode. The state of the output lines together is controlled by logic in the CPU which connects them to one of several different registers, depending on what the CPU is doing. At the beginning of an instruction fetch cycle, the address bus is connected to the PC, or program counter register. This register will hold the address of the instruction being fetched. (More on that later in the Registers section.)

If an instruction requires data, as the cycle continues, the Program Counter increases by one for each byte the instruction requires. This is handled by the control logic, which is also discussed later.

If an instruction demands that the CPU read an unrelated area of memory the address pins can be connected to one of several other 16 bit registers, depending on which instruction is used.

Data

The data bus is even more like an ATmega port than the memory address bus. It’s 8 bits wide, and it’s bidirectional, which means you can read from it and write to it. The big difference is that you can do almost nothing “in the background” with the CPU without touching the data bus. The ATmega 1284P has an 8 bit data bus too, but it’s not connected directly to the outside world. Our ports are, themselves, peripherals.

The Z80’s data bus is where nearly all the communication with the outside world really happens. Any data the CPU sends to or gets from memory goes through here. Any data the CPU sends to a peripheral device goes through here. If you’ve noticed that many of the devices we’ve tinkered with in other chapters have wanted an 8 bit bus, it’s because they were designed to be connected to the 8 bit data bus of an eight bit CPU. Even ATA, which was designed in the days of the sixteen bit ISA bus, is still controlled entirely with 8 bit commands.

Control

The control bus is kind of a hodge-podge of inputs and outputs, as will become (painfully) obvious when we connect it to the ATmega and write the sketch to control it. There are 12 control signals, only about half of which we’ll actually use.

System Control

The system control registers are all outputs. They’re there so the CPU can tell the rest of whatever system it’s connected to what it’s doing, and what it wants.

/M1

The /M1 signal tells the system that the CPU is in the instruction fetch part of a machine cycle. Whatever memory it’s fetching from, it’s fetching an instruction, as opposed to data. This can be important for timing purposes. /M1 is turned on once per machine cycle, as we only read one instruction per machine cycle.

/M1 could allow us to turn the Z80 into something like the ATmega, where instructions are read from one memory source, like Flash or EPROM, and data is read from RAM. We could connect some logic to the /M1 signal, probably combined with /MREQ, to chose which chip-select signal you switch on to turn RAM off and Flash on, for example. Don’t worry. Our use for the /M1 signal in this project is very simple: tell the sketch when the Z80 is reading data or fetching an instruction.

/MREQ

/MREQ tells the system that the Z80 needs to use the memory system. Specifically, it says that the CPU has put a valid address on the Memory Address bus. /MREQ is pulsed active (low) several times in a machine cycle, and is active low.

/IORQ

/IORQ has two functions. First, it tells the system that the CPU is trying to transfer data to an IO device, and that the lower half of the address bus has a valid address for an IO device.

/IORQ is also generated during the M1 phase of the machine cycle when the M1 signal is active) during an interrupt. This is the CPU telling the system that if you’re using a mode 2 interrupt, you can put the interrupt vector on the data bus and select where the interrupt takes you. This takes some external logic. Unsurprisingly Zilog had a series of companion ICs to the Z80 that provided timers that could interrupt the system (not that different from the ones on the ATmega) and interrupt controllers that could provide for many different interrupt-using devices and pass an address accordingly .

/RD and /WR

The /RD and /WR signals are active low, and tell the system that the CPU is trying to read or write (as the abbreviations suggest) to RAM. /RD may be pulsed active (low) up to twice per machine cycle, once for the instruction fetch and once for a data read, whereas /WR will only pulse on in the write phase of a machine cycle. These signals are used to put whatever RAM is connected to the CPU in the right state to receive data from the CPU (/WR) or transmit data to the CPU (/WR).

/RFSH

The /RFSH (refresh) signal takes some explaining. We’re not using it at all in this project, so if you’re not interested in the care and feeding of DRAM, you can probably skip down to the CPU Control signals later in this chapter .

You’ve undoubtedly heard of DRAM, (Dynamic RAM). It’s the kind of ram your desktop computer uses. You might know that, unlike SRAM (Static RAM), dynamic RAM requires refreshing. Here’s what that means.

If you look at Dennard’s original 1966 patent, you can see that a cell of DRAM is essentially a FET and a capacitor to hold its state either conducting or not conducting. The patent also covered a way of addressing lots and lots of these cells by rows and columns, and the technique for manufacturing them on silicon wafers. The important point is that the FET will hold its settings only as long as the capacitors remain charged, and the capacitors are very, very small.

DRAM in those days was accessed by splitting the row and column address into two parts, allowing the DRAM chip to multiplex its address pins, and also allowing the DRAM to save a pin, since row 0 and column 0 were valid. Thus, a 16k DRAM IC like the 4116 (very common in Z80-based systems) could have seven address pins, and one data pin (each IC only held one bit. You needed eight of them for 16kb of RAM.) In 1980 they went for about U.S.$44 each. You wired them in parallel except that each one would be connected to a different line of the data bus to the CPU .

In order to access the DRAM, you pulled the /RAS (row address strobe) line low, sent the row address (the lower seven bits on the cpu’s memory address bus) then pulled /CAS (column address strobe) low as well and sent the column address from the next seven bits up. If you cared to use page mode, you could pull /CAS low multiple times and send multiple column addresses.

Why am I telling you this? Because every time you pull /RAS low, that row of bits’ capacitors were recharged. The row was refreshed. If you accessed all the bytes in your RAM array before the capacitors discharged (about 2ms on the 4116 datasheet I’m looking at, you wouldn’t have to refresh it at all. In fact, the Apple II computer did exactly this, using its video circuitry to scan every row of DRAM bytes in the system. Steve Wozniak was and is a very clever man .

For most computers, perhaps lacking video circuitry at all (the earliest Z80 computers required a separate “dumb” terminal like an ADM3A or a DEC VT100), and certainly lacking Steve Wozniak, refreshing meant that at some point you had to stop the computer doing its business, generate row addresses and pull the /RAS signal low repeatedly to make sure that all the rows were refreshed before they ran out of time.

The Z80 provides the /RFSH line to tell the system (particularly memory) that the address bus’s lower 7 bits are set to the next memory row in line to be refreshed, and it can now refresh that row while the CPU is decoding the instruction it just loaded and therefore doesn’t need RAM at all. With only a little extra circuitry, the Z80 gave you dram refreshing for free.

This, more than any other single feature, is why the Z80 became so much more popular than the Intel 8080 whose instruction set it copied exactly. RAM was expensive, and logic wasn’t cheap either. Having the CPU do this for you without making the CPU wait for RAM was a big deal.

DRAM refresh timing is a tricky business, and it still is today. When you read overclocking sites talking about CAS timing, they’re talking about the latency between when the /CAS signal goes low and when the data shows up on the DATA signal of the IC. Also, on more modern DRAM ICs, if you pull the /CAS signal low before /RAS, the DRAM will generate the next row address that needs refreshing for itself. This is called /CAS before /RAS refreshing .

Now you know.

CPU Control

The CPU control signals of the Z80 are a mixture of one output and four inputs. With the exception of /HALT, these signals all control some aspect of the Z80’s operation.

/HALT

/HALT is the oddball of the control group, in that it’s an output. The CPU tells the rest of the system that it’s halted through this signal, and the only thing that will get it to resume is an interrupt. If you add an external driver and an LED (the outputs on the Z80 can only sink a couple of mA) you could drive a HALT led from this signal. You could also plug the logic probe in there. We don’t use the /HALT signal for anything in this project.

/WAIT

/WAIT is an input, and a very important one to know what it’s doing. Wait tells the CPU to wait for IO. Back in the day, we used to talk about low/no wait-state memory. This meant that the RAM wouldn’t tell the CPU to wait, via the /WAIT line. In other words, RAM could keep up with the CPU. We use /WAIT extensively because we will be simulating RAM with the ATmega using the ATmega’s memory and a couple of interrupts to get the job done promptly. It’s still nowhere near as fast as hardware RAM. Wait lets us keep the CPU from reading at the wrong time and getting really, really confused.

/INT

/INT is one of two interrupt signals on the Z80. Remember interrupts from the last chapter? We’re going to be masters of interrupts by the end of this one, using them both on the Z80 and the ATmega. Whereas the ATmega has three INT pins, the Z80 has just two, of which this is the more flexible. Pulling this signal low causes the Z80 to stop what it’s doing at the end of the machine cycle it’s in and do one of several things, depending on how the interrupt is configured, which I’ll cover at some length down in the programming section of this chapter. In any case, this is the pin that triggers it. Unlike the ATmega’s interrupts, we get no choices whether this interrupt fires high or low or on the edges. It is active low. Like the ATmega, the /INT signal is disabled (masked) by default, but can be enabled from software .

We will use this signal, so it’s wired to the button with a pull-up resistor, switched to the - bus .

/NMI

/NMI stands for Non-Maskable Interrupt. When /NMI is pulled low, the Z80 will interrupt at the end of the current instruction cycle and will restart and fetch its next instruction from address 0x0066. This works exactly the same way as the mode 1 interrupts we’ll be using with the /INT line down in the program section, save that the address is different. I’ll explain in detail there.

We don’t use this signal, and it’s very important to keep it out of mischief, so we pin it to the + bus.

/RESET

/RESET resets the CPU. The program counter goes to zero, all signals are reset to their default states, normal interrupts are disabled, and the interrupt mode is set back to 0. (More on that down below in the interrupt program). We’re handling this signal from the ATmega.

Bus Control
/BUSRQ and /BUSACK

/BUSRQ and /BUSACK are control lines for the bus of the system. In fully fledged computer systems, often another device besides the CPU needs to talk to memory (usually) or another IO device. The CPU is slow. It takes several clock cycles for a single machine cycle, and if we don’t have to use the CPU for something because we’re just moving a large amount of data from device A to device B, it’s possible to tell the CPU to pause and let the external device control the address bus, data bus, and the /MREQ, /IORQ, /RD and /WR signals so the external device can control the bus. /BUSRQ requests that this happen, and /BUSACK is the CPU’s acknowledgement that it’s waiting to be allowed to talk to the bus again. We don’t use these signals, but if you do, you should know that /BUSRQ blocks the CPU’s access to the RAM, so if you’re using CPU refreshing, you won’t be while the CPU doesn’t control the bus.

Internal

The internal bus is the last one we’ll talk about. As the name suggests it has no direct connections with the outside world. This is the bus we use when we move data from the data bus to a register, or from one register to another, or from a register to the ALU (arithmetic/logic unit). This is the bus that controls the data bus. The internal data bus on the Z80 is eight bits wide, and it’s this bus which determines that the CPU is an eight bit processor. We’ll use this bus a lot, but we don’t get a lot of say what happens with it .

Registers

Registers are memory, if you like, inside the CPU. You can also think of them as variables.

We’ve talked about registers before discussing the port registers of the ATmega, and these registers are very much in that same vein. Most hold one byte each. Some are paired and can be used with their pair-mate as a sixteen bit register, or as two eight bit registers. Some, as they are primarily for holding memory addresses, must be used as sixteen bit registers. Some are modified automatically by the CPU as the ATmega’s port registers are, and some are just a place to put data. Registers are at the heart of programming.

Program Counter

The Program Counter (PC ) register is a sixteen bit register that keeps track of the address in memory we’re at in this machine cycle. The program counter generates the address on the address bus when the Z80 requests memory access. The program counter is what gets changed when an interrupt happens, reset when the Z80 is reset, and so on. Memory holds instructions and parameters to those instructions. The program counter is what ensures that those instructions get read and executed in the order they were put in memory.

You can’t read the PC register directly, although you can set it with a JP (jump) command. If you’ve ever programmed in BASIC, and JP sounds like GOTO to you? Well, it is, exactly.

Accumulator and Flag Registers

The accumulator (A) register is where eight bit math goes. To do math on a Z80, you load the accumulator, then load another register with another value, then issue an instruction to add that other register. The accumulator is where the result lands.

Closely tied to the accumulator register is the Flag (F) register. The various bits of this register have different values. In the usual 0b order, from bit seven to bit zero, these are: S, Z,(not used), H, (not used), P/V, N, and C.

S: Sign Flag

The Sign flag is equal to the most significant bit in the Accumulator. Because the Z80’s ALU assumes signed integers, this bit will be the sign bit.

If there’s one thing that is a lot more complicated at this level of computing than with higher level languages like the C/C++ we’ve been using for sketches, it’s math. It gets worse when you try to do floating point math, as we will in the sketch for this project (but not in assembly on the Z80.) For CPUs with no floating point unit, the floating point math functions are part of the program. Floating point math is expensive in compute time on systems with no FPU.

Z: Zero Flag

The Zero flag is set if the byte in the Accumulator is Zero as the result of a calculation. It’s set if the math unit is doing a compare and the two values compared are equal. It’s also set (one) if a bit is being tested in a register if the bit tested is zero, and reset (zero) if the bit tested is one.

H: Half Carry Flag

The Half Carry flag is used by the Decimal Adjust Accumulator instruction to correct the decimal point when dealing with packed BCD notation digits.

That’s great, what does it mean?

Simply this: Because computers are very often in the business of dealing with decimal numbers even though in the most literal sense they can’t, many programs take advantage of the fact that 0x0 to 0x9 can represent decimal digits as well, so each byte of memory can store two digits of decimal represented numbers, zero to 99, as we did in the Dice Device’s displays, if not in memory. This wastes half the permutations of a byte, but for decimal operations it simplifies things. The half-carry flag is set when bits get borrowed from the high digit to the low digit and from the low digit to the high digit .

P/V: Parity/Overflow Flag

The Parity/Overflow Flag has a number of different functions. If your math operation generates a value greater than 127 or less than -128, you’ve overflowed your 8 bit Accumulator, so this flag will be set so you can tell.

The flag is also used to determine the parity of the byte in the Accumulator if you’ve done logical operations on it or rotated it. Parity is odd (P/V is zero) if there are an odd number of 1 bits in the byte, and it’s even (P/V is 1) if there are an even number. Parity is used, among other things, to verify that a byte has been sent or received correctly.

N: Add/Subtract Flag

The Add/Subtract flag is exactly what it sounds like. If it’s set, it means we’re doing a subtraction, which may be important for interpreting the results. There’s more about this flag in the datasheet.

C: Carry Flag

The Carry flag is set when a math operation generates a value bigger than the one byte in the accumulator can handle, either through addition or subtraction. (Carry can also mean borrow if you’re subtracting.) It’s reset by any ADD or SUBtract that does not generate a carry/borrow, or any logical operation—AND, OR, or XOR. During the rotate instructions (RRCA, RRC, SRA, and SRL, if you wondered) it will hold the final bit at whichever end of the byte you’re rotating toward when the final value is shifted out. If you rotate one of the registers, or the accumulator until it’s empty in either direction, the Carry flag will hold the last bit.

Index Register

There are two index registers in the Z80, IX and IY. Both of them are 16 bit registers. This is for indirectly accessing memory.

Wondering what that means? It’s simpler than it sounds. Moving 16 bit values around in an 8 bit computer is compute-expensive, so if all your data for the next few instructions are within 255 bytes of each other, you can set the address of the first byte in IX, and then access the rest of the bytes with an instruction that takes an 8 bit offset to that 16 bit address.

General Purpose

There are six general purpose registers in the Z80, in three pairs of two: BC, DE, and HL. These registers can be used individually as 8 bit registers (B,C,D,E,H, and L) or with their partners as 16 bit registers (BC, DE, and HL.) As you go through the list of instructions, you’ll find that some instructions don’t work with some registers. HL, for example, is clearly intended as a pointer to memory. (More about pointers a few sections down. We’ll be dealing with them a lot in this project.) D is the register used for the offset when doing indirect addressing (index + offset - IX or IY+D). The IX+D pair is also most often used as a pointer.

There are quite a few instructions for accessing a given memory location at HL and remarkably few for other general purpose registers.

General purpose registers contain only what you put in them. They are variables, in the truest sense.

Stack Pointer

SP, the Stack Pointer . Remember how I mentioned pointers? The Stack Pointer is a 16 bit register dedicated to the Z80’s stack.

The Z80’s stack is called a “Software stack.” This means that rather than have memory inside the Z80 for the stack, which is a standard part of microprocessor anatomy, the Z80’s stack uses external memory. When you set the stack up, you point the stack pointer to the highest memory address it’s allowed to use, and the Z80 stack will expand toward the lowest memory address. If it intersects with your data or your code, the stack system will happily overwrite both, just like you can overwrite the stack with data. One thing about machine language/assembly language: there’s very little protection from yourself at this level. I’ll mention this again later .

In any case, the stack is used by a number of different systems in the Z80, not least of which is the interrupt system and the call system. If you call another address in code, the first thing that happens is that the value of the PC (program counter) is pushed onto the stack. When your code issues the RET instruction, the call system pops the PC’s value back off the stack, sets the PC to it, and away you go, assuming you haven’t pushed anything else to the stack that you’ve not subsequently popped between the two .

Evil Twin Registers

If you’ve looked at the Z80 datasheet, you may have noticed a whole second set of the general purpose registers, and the accumulator and flag registers. They’re real. You can swap the two sets of registers with a single instruction, and it’s a very fast operation. If, for example, you have an interrupt handler (an ISR, in ATmega parlance) that uses the accumulator and HL, you can push the values of both onto the stack when the ISR is called, then pop them off when the ISR is done before it returns. Or you could execute one command, switch registers, and have a set of registers all to yourself for the ISR. When you’re done, just switch back, call RETI (return from Interrupt) and let the CPU handle popping the PC register off the stack.

There’s a lot of that kind of tweaky optimizing funcitonality in the Z80. It’s a CISC (Complex Instruction Set) processor designed for assembly language programming, unlike the ATmega. It will reward hand-optimization like I just described far more than modern RISC instruction sets will, which instead use all those transistors to speed up all the instructions. Different philosophies, different times.

The ALU

The Z80’s Arithmetic/Logic Unit (ALU) is a pretty limited beast. It can Add, Subtract, AND, OR, XOR, Compare, Increment and Decrement integers. That’s it. Need multiplication and division? Write a program that does that. Need floating point math? Write a program that does that. Do you pay a huge penalty in performance using software instead of the ALU’s built-in functions? Yes, absolutely. Zilog actually fixed the multiplication/division problem with the Z180, and while they, like their older Z80 brethren are cheap and plentiful, they aren’t available in quantities of less than 120 in DIP format. If you were building a Z80-based system today, there are floating point units based on programmable DSPs, such as the Micromega uM-FPU64. You might also be able to use such an FPU for fast integer multiplication and division, although it would be worth figuring out whether the extra steps of communicating with these devices over SPI, I2c, or UART (TTL level RS232) would be slower than having the Z80 multiply by adding over and over again.

The ALU of the Z80 breaks its operations into two groups: 8 bit math and 16 bit math.

Eight Bit Math

There are 17 8 bit math instructions on the Z80, but they can be grouped into eight groups: ADD, SUBtract, AND, OR, XOR, CP (compare), INCrement, and DECrement.

The ADD Family

The ADD family of instructions add various operands to the value stored in the Accumulator. This is how math is done on the Z80: values are stored in the Accumulator (A register) and modified mathematically.

So ADD A, r adds the contents of one of the eight bit general purpose registers to the value stored in the Accumulator.

ADD A, n adds the value n (an eight bit value) to the value stored in the accumulator.

ADD A, (HL) is tricky. In assembly, when a register or a memory address is enclosed in parens, it means that this is a pointer. HL, in this case, would be set to a sixteen bit address in memory, but (HL) means the contents of that memory cell, which will be an 8 bit number. This is the same with ADD A, (IX+D) and ADD A, (IY+D). IX and IY are index registers which will hold a 16 bit value, and the D register is used as the 8 bit offset from that address. The whole shebang is an address in memory, and is enclosed in parens, making it a pointer. What you get from the (IX+D) pointer will be the contents of that memory address, not the value of IX+D.

ADC is actually part of the ADD family, save that it is add with carry. ADC will add the value of the carry flag (remember the carry flag in the F (flag) register?), which is zero or one, plus whatever operand you give it to the value in the accumulator. If you actually are doing a software multiply, you’ll probably need this. (Don’t worry. We won’t.)

Which Opcode is Which

We’ve seen the assembly instructions for the various adds, but one thing that the Z80 datasheet makes a little confusing is how those instructions translate into opcodes. When you look at ADD A,r; add the contents of register r to the Accumulator; there’s no opcode listed. When you hand-assemble code, you have to put the opcode together yourself.

ADD A,r has seven permutations, depending on which register you want to add to A. To get the opcode you need, you have to figure out which register you’re adding, then get the fixed bits of the command, and add the bits of the register you want to it.

Suppose you want to add general purpose eight bit register B to the Accumulator. That’s the ADD A,r instruction. If you look on the table in the datasheet, you’ll see that the B object code is 0b000.

The ADD A,r instruction’s value is 0b10000___, where the last three bits are the object code of the register you want. If we fill 000 in for the last three bits, we get 0b10000000, or 0x80. This is the opcode for ADD A,B.

If you want C instead of B, no problem. C’s object code is 0b001. We put 001 in for the last three bits of the opcode, and get 0b10000001, or 0x81. And so on. The three bit object codes for all the 8 bit registers you’re allowed to touch stay the same for every 8 bit instruction. For reasons known only to themselves, Zilog’s engineers did not assign an object code for pointers like (HL) or (IX+D). They just list the opcodes directly in those instructions .

The SUB Family

The SUB and SBC families has the same members as the ADD family, but they’re grouped together differently. SUB is listed as SUB s, where the s operand can be any register, a number, or a register used as a pointer. The object codes are the usual suspects too.

Compare

CP, compare, is a little different. It compares the value in the accumulator with the value of the other operand. Sure, the other operand can be all the usual suspects—explicit values, other registers, pointers to memory like (HL) or (IX+D), but it’s a weirdo in how it returns its results. The results are not set in the Accumulator. They come up in the flags attribute. If the compare is true, the Z (zero) flag is set to 1. If it’s not, the Zero flag is set to zero. Compare also sets the H flag the P/V flag, the N flag, and the C flag as appropriate. The Z flag is the most useful, though .

Increment and Decrement

A lot of times what you really want to add or subtract to a given register is one. Consider the PC (Program Counter) The Z80 increments this register at least once every machine cycle, and often two or three times. If you guessed that the Z80 probably uses the very same hardware for that as for the INC instruction, you’re probably right. INC is much faster than setting a register to one and adding it to another register.

DEC does the same thing in the other direction. Where INC adds one to the register or memory location, DEC subtracts one, with all the same advantages.

INC and DEC do not use the accumulator unless told to. You can increment or decrement any eight bit register you’re allowed to touch (including the Accumulator) as well as values in memory referenced by the usual pointers.

The usual system of opcode building where you add the three bits for the given object code into the rest of the instruction is also used, save that the object code goes in the middle of the instruction. Like this.

If you want to INC C, you look up INC r, and discover that the opcode is 0b00___100. The object code for C is 001, so you put those three bits, right to left, in the missing slots, and get 0b00001100, or 0x0C (or simply 0xC, but it’s easier to match up with a full byte using both hex digits.) If you want to increment the Accumulator (INC A), you apply 111 to the opcode and get 0b00111100, or 0x3C.

Pointers like (HL), (IX+D) and (IY+D) have their own opcodes, as is often the case.

INC and DEC set the usual flags .

If you’re doing a counting loop, it should be obvious there are two ways to do it. Set the value of a register, INC it, and compare it, useful if you’re using the register as part of an address (as in indirect memory addressing using (IX+D) where you’re incrementing D) or set your register to the maximum value of the loop and DEC it until the Zero flag is set. When we do some simple programs for the Z80, we’ll do both.

Sixteen Bit Math

The 16 bit Arithmetic group is similar to the 8 bit group, save that while there are fewer registers it can access (as we have to use pairs of registers instead of single 8 bit registers), more of them can have math performed on them. You can’t use the accumulator at all, since it’s an 8 bit register, and the F (flags) register is not directly user settable. Sixteen bit Arithmetic instructions do use the flags in the F register pretty much the same way their 8 bit counterparts do.

The other thing to note is that the object codes for 16 bit registers are only two bits long.

There are four of the usual families of math instructions in the 16 bit world: ADD/ADC, SUB/SBC, INC, and DEC. All of them are designed to deal with addresses or other 16 bit values in registers. None of them deal in pointers like (HL), as the value stored at any given address in memory must be 8 bits long.

ADD/ADC/SBC

The ADD/ADC and SBC families of instructions access specific registers.

ADD HL,ss works pretty much like ADD A, s, save that HL is just a general purpose register (usually used as a pointer to memory), and ss denotes another general purpose 16 bit register. You build the opcode the usual way, too .

If you want to add the value of a register to HL, for example, you look up the opcode for 16 bit ADD HL,ss, and discover it’s 0b00__1001. Looking up the object code of the register pair HL, it turns out to be 0b10, so as usual we assemble the opcode to be 0b00101001, or 0x29.

If we place 0x29, 0x10, and 0x00 in three bytes of memory, in that order we’ve instructed the Z80 to add 0x0010 (decimal 16) to whatever is in HL already. You saw that, right? Little-endian means our lowest significant byte comes first in RAM, so 0x10 before 0x00 comes out equalling 0x0010. If you wonder why I insist on putting the leading zeros on hex values, here’s where it pays off in clarity.

Always make sure you know what value is in a register (or the Accumulator for that matter) before you use it in math. Also, if you are programming within an operating system, the operating system may well be using any given register, so it behooves you to either store that register and restore it when you're done, or know what registers you're allowed to use.

The 16 bit ADD family also includes the ability to add to the IX and IY pointers, but not to ADC them.

ADC works pretty much like ADD, except that it carries.

The 16 bit SBC family is even more limited. There are instructions to subtract any given pair of 16 bit registers from HL, and all 16 bit subtractions use the carry flag. The 16 bit SBC is also a rare 16 bit instruction: there are two bytes in a row used to indicate it.

So if you want to subtract the value of BC from HL, you look up the opcode. The first byte of the opcode is 0xED. The second byte is the one you have to build. The skeleton is 0b01__0010. Register pair BC’s object code is 00, so the second byte becomes 0b01000010, or 0x42. To subtract a given number from whatever is in HL, using BC, you’d first have to load that number into BC, then put the opcodes 0xED,0x42 into memory in that order. There’s no other way to do sixteen bit subtraction, and you will be using the carry flag, whether you like it or not.

Why is 16 bit subtraction so limited? The Z80 was designed when you didn’t get many transistors on a given IC. The original Z80 had only 8500 transistors total, whereas the CPU in my monster mac has 1.4 billion transistors. Every function they added to the Z80 cost transistors, and if an instruction wasn’t used often, like 16 bit subtraction (in an 8 bit processor) it didn’t make the cut .

INC/DEC

For all that the ADD/ADC and SBC arithmetic family is limited in the Z80, the INC and DEC families are full-featured. You can increment or decrement any general purpose register, as well as IX and IY.

INC ss, the increment instruction for any general purpose register, is a single byte instruction, with the usual object codes for 16 bit registers.

As always, the IX and IY registers have their own opcodes. In this case they are sixteen bit opcodes.

Logic, Rotation, and Bit Twiddling

There are other functions of the ALU. It deals, as the name suggests, with logic, also with bit rotation (which is similar to but not the same as bit shifting) and setting, resetting, and testing single bits.

Logic: AND, OR, and XOR

Boolean Logic

AND, OR, and XOR all work the same way. Set the Accumulator to the value of itself AND, OR, or XOR the s operand, which can be any general purpose register, an explicit value, or the usual pointers. Once again, for some permutations, you build the opcode yourself by putting the object code together with the prefix just as you do with ADD, and other opcodes are simply given outright.

Rotation

The RL and RR families of instructions do rotate left, rotate left with carry, rotate right, and rotate right with carry on a variety of registers.

The Z80’s rotate instructions are quite different from the Arduino bit shift operators. They only shift the byte by one bit per call, and unlike the Arduino bit shift, the Z80 rotates do wrap bits back around to the other end of the byte. They also interact with the carry flag, depending on which rotate you call.

RLCA (0x07), rotates the byte in the Accumulator (register A) left one bit. Bit 7, the leftmost bit, is stored in the carry flag and is also set on bit 0 of the accumulator.

RRCA (0x0F) does the same thing in the other direction. All the bits of the accumulator are rotated right one bit, and the right most (lowest) bit of the byte goes to the carry flag and also is placed in bit 7.

RLA (0x17) rotates all the bits in A left. Whatever was in the carry flag moves into the rightmost (lowest) bit of A, and whatever was in the leftmost (highest) bit of A is copied into the carry bit.

RRA (0x0F) rotates all the bits of the accumulator right one bit. The carry flag is copied into the leftmost (highest) bit of A, and the rightmost (lowest) bit of A is then copied into the carry flag.

The rest of the RL family of instructions work the same way on different registers. They’re two-byte (sixteen bit) instructions, so they will take longer to execute. Some of them also act on sixteen bit registers.

Bit Twiddling: Set, Reset, and Test

The BIT instruction tests one bit in a given register. If it’s zero, the Z flag is set, otherwise the Z flag is reset to zero.

The BIT instruction is a two byte instruction. The first byte is 0xCB. The second byte contains a prefix code, 0b01, three bits to select which bit you want to look at, where 0b001 would be bit 1, second from the right, and 0b110 would be bit 6, second from the left. The remaining three bits select the usual object codes for registers. So if you want to see if bit 3 of register D is set, you’d use the first byte, 0xCB, and your second byte would be 01 for the prefix, 011 for bit 3, and 010 for register D. Put together it would be 0b01011010, or 0x5A. Then you’d check the flag register to see if the zero bit is set. If it is, your bit was set to zero .

There are versions of BIT to test memory locations with the usual array of memory pointers, too.

SET and RESET work exactly the same way, with the same types of parametersZ80 EXPLORER:ALU:.

Instruction Decoding and Control Logic

In order to do its job, the Z80 has to take instructions in and carry them out. If you wondered previously how it gets from ADD A,0x12 to actually doing that, you’ve already seen part of the answer.

The Z80 doesn’t know anything about ADD A, 0x12. Add A is a mnemonic for humans. In fact, when Zilog first knocked the Z80’s instruction set off from Intel’s 8080, the first thing they did was change all the mnemonics to escape Intel’s copyright. The two CPUs remained compatible, however, because the opcodes didn’t change.

You’ve already seen some opcodes. You’ve built some, if you followed along as we talked about the ALU and its instructions. There are lots more, and no, I’m not going to get into them all. They’re all in the Z80’s mostly excellent datasheet, although as we’ve seen some construction is required for a lot of them.

Opcodes are what the CPU does understand. It may not know anything about ADD A,0x12 but put 0xC6 in a location in memory and 0x12 in the next location down, and the Z80 knows exactly what to do.

Unless told otherwise, the Z80 assumes anything it comes across in memory is an instruction. So when it hits 0xC6, the instruction decoder looks up that number, reads in the next value for data (0x12). The control logic connects the accumulator to the ALU, and passes 0x12 on the internal bus to the ALU, then triggers the ALU’s add function. That’s how it happens. It sounds like calling functions from what amounts to a menu because it is very much like calling functions from a menu. CPUs are like programs, as I said .

Putting It All Together: Operations

Let’s walk through an entire instruction cycle on the Z80, soup to nuts, so we know how the thing works. This is discussed at some length in the datasheet, so some terms need defining.

A clock cycle or T (time) cycle is one full cycle of the clock, that is, one positive pulse, and one zero pulse.

A machine cycle is a section of the instruction cycle, a given phase of the work to be done. Each machine cycle can take from three to six clock (T) cycles.

An instruction cycle is the entire process from the time an instruction is read in as an opcode until the time the CPU is ready for the next opcode.

We’ll look at an LD A, load the Accumulator, with a value of 0x10.

LD r,n is one of those opcodes like ADD A,r that we have to construct. We look up the opcode skeleton and find it’s 0b00___110. We look up the Accumulator’s object code, and it’s 0b111 Put the two together and we have 0b00111110, or 0x3E. So our instruction and data, one after the other, will be 0x3E, 0x10.

The first step of an instruction cycle is to read the the opcode from memory. The CPU puts the Program Counter’s value onto the memory bus, pulls /MREQ low, pulls /RD low, and pulls /M1 low.

/MREQ and /RD tell the memory system it’s needed, that there is a valid address on its address bus, and that it will be read rather than written to. /M1 tells the system this is an instruction fetch, machine cycle 1, the start of an instruction cycle. The CPU stays in this state for three clock (T) cycles while it waits for the value on its data lines to settle. The opcode is read during the third clock (T) cycle. The CPU has taken up 0x3E. It will spend clock cycles 4, 5, and 6 decoding the instruction and, presumably, the control logic will spend that time moving data on the internal bus and connecting the right sections of the CPU together to actually carry out the instruction.

Meanwhile, for the rest of the M1 cycle, /M1 is high, /RD is high, and /MREQ goes high briefly (for one clock cycle) then goes low again, along with the /RFSH signal, to tell memory that not only are we not using it presently, but to go refresh the row at the bottom of the memory address bus. If needed. Be thankful we’re not dealing with DRAM in this project.

Note that the M1 cycle, like all machine cycles, can be stretched if the /WAIT signal is low. This is so that slow memory (like what we’ll be using) has time to do its job before the CPU starts asking for another address.

The LD r,n instruction is listed as requiring two M cycles. One to fetch the instruction, one to read the data from memory, so we have another M cycle to go through. Since we’re reading data, this will be a memory read cycle .

The control logic is set by the decoded instruction, 0x3E, to increment the Program Counter (PC) so the next value can be read as data and not as an opcode later. As with the opcode fetch cycle, the memory read cycle begins with the memory address being set on the address bus, /MREQ going low, and /RD going low. /M1 does not go low, since this is the M2 cycle.

/MREQ and /RD stay low for the second half of the first clock cycle, all the way through the second, and halfway through the third. The data from the memory bus is actually read roughly at the start of the third clock cycle. Like the opcode fetch M1 cycle, the memory read cycle can be lengthened by pulling the /WAIT signal low.

The CPU now has the data it needs. The /MREQ and /RD signals go high, and no refresh activity happens.

The control logic has set the multiplexor so it’s looking at the correct register (A, the accumulator) and it has data on the internal data bus to put there, so the value 0x10 is set in the accumulator.

And we’re done.

LD A,n is a pretty simple instruction. There can be a lot of other M cycles involved: interrupt cycles, bus arbitration cycles, resets, and so on, but you can see from this example how things are broken down, how the opcode gets read and executed, and what happens behind the curtain.

Objects and Classes , Revisited

Let’s pause a moment and pull back from the microscopic detail level we’ve been looking at in the guts of the Z80 and talk about objects and classes. We haven’t done much object oriented C++ programming in this book, and my personal opinion is that when it’s not needed, it adds complexity without a purpose.

Our sketches are starting to get complicated enough that we have a reason to use objects in this one. Even in the Dice Device, last chapter, you might have noticed that there were an awful lot of global variables, and at times it made the sketch messy and hard to read, and it sometimes got difficult to tell what code exactly accessed a given global variable.

Objects, by contrast, group the code with the variables. If a variable (or a function) is set private, it cannot be touched from outside the object. Only the object’s member functions can touch it.

If a variable or a function is set public, then it can be touched from outside the object.

An object also has a constructor and a destructor. If we don’t put one of each in explicitly, C++ will put a generic one in for us. Most of the time, that’s fine. These are listed as member functions of the same name as the object for the constructor, and the same name again with a tilde (∼) in front of it, for the destructor.

Here’s an example:

class cpu {
  private:
    uint8_t a = 0;
    const uint8_t M1_MASK = 0b00010000;
  public:
    cpu() { // constructor
      Serial.println("CPU: Object created.");
      a = 0x51;
    }
    boolean M1() { //member function
      return (a & M1_MASK);
    }
   uint8_t my_uint=0;
};

This class doesn’t do much, and it’s not very useful. If it looks like I might have cut it down from a class in the sketch, you have a good eye .

This class is named cpu. There are two private variables: a uint8_t (unsigned eight bit integer) called a, and a constant uint8_t called M1_MASK, both of which are set as soon as they’re declared. But watch out, there’s a catch there that I’ll cover very shortly.

Both those variables are private. The rest of the code can’t touch them, or even see them. If you try, your sketch won’t compile.

However, because the M1() function is a member of the class, it can touch those variables, AND them together, and return the result.

You should also notice the constructor. It throws a Serial.println() message and resets a to 51.

When exactly does that occur?

Good question. Declaring a class, by itself, does nothing. It defines only what an object of that class would have in it, if it existed. To actually make an object of that class (it’s called instantiating that object), we have to declare one, just like a variable.

Here’s the declaration that makes an object of the class cpu called Z80 .

cpu Z80; // declare our CPU object.

You do it just like uint8_t a;

The object Z80, is of the class cpu, and the moment we declared it, the constructor ran. Notice that while we called the constructor, this particular one takes no parameters, so we don’t put the parameters field in the call. If our constructor took parameters, for example:

    cpu(String a_string) { // constructor
      Serial.println("CPU: Object created." + a_string);
    }

we’d be obliged to call it like this:

  cpu Z80("hello");

To call other member functions, only the public ones, you use this syntax:

mybool=Z80.M1();

This tells C++ we want to call the M1() member function of the object Z80, and we want the results in the variable my_bool. Unlike constructors and destructors, calling member functions is done with standard function call syntax.

Want to access the public variable my_uint? You do it the same way.

Z80.my_uint=4;

Want to access the private variable a?

You can’t.

So now that we’ve covered member functions and variables, what about that destructor function? When does it get called?

In this case, never. Z80 is a global object, declared at run time, and this sketch, like all sketches, has no graceful exit. When you’re done, you unplug the Cestino.

There are times we’ll want to make objects go away and thus fire their destructors, but to get into those, we need to talk about pointers .

Pointers

Pointers are one of those things that confuse new programmers endlessly. We’ve already seen them in their simplest form while we were discussing the guts of the Z80, and that may be the easiest way to understand them. So, a little review.

I said that the HL register in the Z80 was often used as a pointer to memory. This means that HL is often set to a memory address. You almost never want to access that address itself. What you really want is what’s in memory at that address. In assembly language, this is shown like this. (HL). (HL) refers not to the memory address stored in HL, but to the value stored at that location in memory. With me so far? Good.

C and C++, like most higher level languages, also have pointers, and they’re incredibly useful, but the syntax for getting at them is awful.

This is a C++ pointer’s declaration: uint8_t* a_pointer;

Instead of declaring it as a regular uint8_t variable like we normally would, we declared it as a pointer. It’s declared, but doesn’t point to anything yet.

Pointer declaration syntax has undergone some semantic drift between C and C++. In C, pointers are traditionally declared uint8_t *a; that is, a is a pointer to a uint8_t. In C++ they are traditionally declared uint8_t* a; It doesn't matter which you use in C++. What does matter is when you try to declare more than one pointer at a time. Each pointer must have its own dereferencing asterisk. Eg: uint8_t *a, *b; If you declared uint8_t* a,b; you'll find that b is a uint8_t and not a pointer at all. It's better and much safer to put pointer declarations on individual lines.

Cannonically, in C++, you have to use the new command before your pointer points to anything. Eg: mem_sim = new uint8_t; The new command allocates memory for whatever type you put after it returns the address of the memory space it’s allocated for you and the result is put in your pointer variable .

Still with me?

Okay. Here’s where C++ causes major headaches for everyone sooner or later. In C++, you can ask for the address of any existing variable with the & character. Here’s an example:

uint8_t *b;
uint8_t c;
b=&c;

See what I’ve done there? I’ve set the pointer b to equal the address of the uint8_t variable c. This works. This is fine, except if c is inside a function and b is not, when the function terminates, the memory that used to be the variable c is automatically deallocated. If I try to access b, the results are... undefined. That’s never good. Worse, sometimes it will work because nothing else has allocated the memory yet.

In C++, you use pointers when a function needs to change one of the variables passed in to it: a variable that is neither a global nor (in the Arduino’s case) a register, so the function can’t see the variable at all. Cannonically, functions can’t do this. However, if one of the parameters of your function is a pointer, your function can dereference the pointer with a *, modify the value stored in that location of memory, and go on its merry way.

Here’s an example:

void my_function(uint8_t* pointer_to_c) {
  ++*pointer_to_c;
}


void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);


  uint8_t *b;
  uint8_t c;
  b = &c;
  c = 10;


  Serial.println(c);
  my_function(&c);
  Serial.println(c);
  my_function(b);
  Serial.println(c);


}

Our function takes a pointer to a uint8_t called pointer_to_c. When we call it, we ask C++ to give us whatever pointer_to_c points to (hence the astarisk at the beginning of the pointer name) and increment that.

The first thing that happens in setup() is that We create a pointer to a uint8_t called b. Then we created a uint8_t called c. We set b to the address of c, and set c to 10.

Next, we Serial.println c. This should give us the 10 we put in it.

Next, we call the function with the address of c (hence the & sign in front of c). The function runs, and we Serial.println c again. Because our function takes a pointer as a parameter, and we’ve passed the address that would be in that pointer directly, the function works as advertised, looks up the memory address stored in pointer_to_c, and increments that uint8_t. When we Serial.println c again, we’ll find that c is 11.

Next, we call my_function again, but this time with b which, you will recall, is a pointer to c. Notice well that there is no asterisk around b in this call. We’re passing the value of b, which is a memory address, into my_function(), which expects a memory address. My_function() takes that address into its own pointer variable, pointer_to_c, dereferences it with a preceding asterisk, so C++ knows to look up that memory address and add 1 to the value stored there.

Nothing stops you from adding to the address stored in b. In fact, this is fairly common practice in C and C++, and a way of life in assembly language. Be careful, though. You can wind up with pointers pointed at the wrong thing very, very easily doing pointer arithmetic. Programs that mishandle pointers will compile and run, right up until the pointer is accessed, when the program will crash, access the wrong thing, or any number of other odious failures. Make sure you know what you’re doing.

In case you wondered, here are the actual results from the pointer demo code.

10
11
12

Okay. I think we’re up to speed on basic pointers in C++. Here’s the rub. Pointers can point to objects, too.

To declare a pointer to the class we talked about in objects and classes, we’d do this:

cpu* Z80 = NULL;

The NULL should be a big clue. We have declared a pointer to an object, but it does not yet point to anything. In fact, it points to NULL, which is a convenient value we can test for, and also keeps the pointer out of trouble. Z80 has not been instantiated, and the constructor has not fired .

Let’s make all that happen.

Z80= new cpu();

Now there actually is an object. Now the constructor has fired. Now Z80 points to an object in memory that we can work with.

Now is it time to talk about destructors in classes? Yes it is.

If we want to get rid of the object cpu() and fire its destructor function , we use the command “delete”. Like this:

delete Z80;
Z80=NULL;

I try to always follow a delete up with setting the pointer to NULL.

Can you guess what happens if you forget to delete the object before you set its pointer to NULL?

Nothing. The object still exists, somewhere in memory, but our pointer doesn’t point to it anymore. Nor does anything else. It will sit there wasting memory until the system reboots. This is a problem for pointers to regular variables too. When you hear about memory leaks in software, frequently sloppy pointer handling is the reason.

If you delete a pointer pointing to NULL, what happens? Nothing. When I talk about setting your pointers to a safe value, this is what I’m talking about. If you happened to delete a pointer that pointed to some random location in memory, that location is getting freed, whatever was in it. Not a good idea. Again, sloppy pointer handling causes an awful lot of the software problems in the world .

We will be using pointers and classes like the ones we’ve just talked about in the sketch, as well as in the programs we’ll run on the Z80. I’ll try to keep it simple and straightforward .

Function Prototypes

Up until now, when we’ve used functions, we’ve been careful to define them before they were called. This way, the GCC compiler knows that when you call function repeat(), it returns a String object, for example.

For smaller sketches (and programs in general) our approach works fine. When functions begin to call each other, however, the programming gets complicated. When some of the functions involved are members of a C++ class, your work gets worse. We’ll be doing exactly that in this sketch. We’ll be creating functions thate are members of a C++ class.

Fortunately C, and by extension C++, lets you create a function prototype. This prototype looks like a function declaration, except that it has no code, only the types and variable names. Here’s an example.

String repeat(int number, char character);

This is the prototype for the function repeat, which takes an integer and a character parameter and returns a String object.

The compiler assumes (correctly) that we’ll declare the actual function someplace else, anywhere else, like this:

String repeat(int number, char character) {
  String temp = "";
  for (int c = 0; c < number; c++) {
    temp = temp + String(character);
  }
  return temp;
}

Notice that the first line of the function has exactly the same variables, types, and variable order as the function prototype. If they don’t, the sketch won’t compile .

Function prototypes are a good idea generally. They completely free you from sorting out what functions call what and what order they’re declared in. In fact, classic C programming usually has the prototypes first, and the functions after main() (the equivalent of loop() more or less,) after all the other code. It’s such a good idea that up until Arduino 1.6.6, the Arduino IDE built your function prototypes for you. They reasoned that it can be a little tricky for new programmers to make sure that a function’s prototype and its declaration are always the same .

For reasons known only to themselves, the Arduino maintainers broke this functionality in Arduino 1.6.6 and above, so moving forward, as protection from IDE chaos, it’s probably a good idea to make your own.

Build the Z80 Explorer

For all the complexity of the sketch, the build is pretty straightforward. As you can see in Figure 11-2, we’re using all four ports of the ATmega. Ports A and B are the address bus’s least significant bits (LSB) and most significant bits (MSB). Port C is the data port. These can be changed around fairly easily in the sketch if you want to. The port that absolutely must remain the same is port D, which is wired to 6 of the 14 signals in the Z80’s control bus. It’s essential that this port be wired as shown, because we’re using OC1A (PD5, aka pin 19) to generate the Z80’s clock, and we’re using INT1 (PD3, aka pin 17) and INT0 (PD2, aka pin 16) as interrupt signals for the ATmega to process the Z80’s memory requests.

A340964_1_En_11_Fig2_HTML.jpg
Figure 11-2. The Z80 Explorer Schematic

Install the Z80

Like the ATmega1284P, the Z80 is a 40 pin DIP IC, so I recommend installing it starting with its pin 1 on line 11 of your breadboard, oriented the same way as the ATmega. It makes for a neater looking breadboard and keeps the connecting wires shorter. For high speed signals, shorter is better.

Power Circuits

First, in case you haven’t been doing this all along, unplug the Cestino from USB power.

Wire the Z80’s pin 11 (the pins on the schematic in Figure 11-2 are not in order) and pin 29 to the + and - busses, respectively, then wire the 0.1μF capacitor from pin 11 to the - bus. Nothing you haven’t seen here before. We’re giving the Z80 a bypass cap of its own to keep noise out of its power supply.

Data Bus

Let’s go ahead and wire up the data bus first. Why? Because all the data bus pins are on the left side of the Z80 as we’ve mounted it, and they all connect to port C, which is on the bottom right of the ATmega. This means that our data bus wires will be under many of the control bus wires, so its easiest to do them now .

The data bus pins are, sadly, all over the left side of the Z80 and not in any particular order. In signal order, that is, D0-D7, they are Z80 pins 14, 15, 12, 8, 7, 9, 10, 13. Wire these pins to ATmega port C, from PC0 (ATmega pin 22) to PC7 (ATmega pin 29) in that order. As usual, I hanked these wires up once I had them in place .

Control Bus

The Z80 has a lot of control signals. Of these, we use six, so let’s start by connecting the input signals we’re not using to the + bus to keep them out of trouble. Connect pins 17 and 25 on the Z80 to the + bus. These are /NMI, nonmaskable interrupt (Z80 pin 17), and /BUSRQ which requests the Z80 stop talking to the memory bus so other devices can talk to it. (Z80 pin 25).

The Clock

Ordinarily, the Z80’s clock would be like the ATmega’s, driven by an external oscillator. For this project, however, we’ll be using an ATmega timer to generate the clock signal for the Z80, so we can control the Z80’s processing speed from the sketch. We dealt with timers in chapter ten, but this time we’ll use the timer’s output pin to generate the Z80’s clock directly, instead of interrupting the ATmega. Theoretically, this gives us up to a 20MHz clock, equal to the Cestino’s own system clock (but isolated from it by the ATmega’s internal electronics.). Realistically we won’t be running the Z80’s clock anywhere near that fast, even if you do have the 20MHz version of the Z80.

Connect pin 6 on the Z80, /CLK, to the OC1A pin on the ATmega’s pin 19.

The Interrupt Button

One of the things I wanted to show with the Z80 explorer is how interrupts really work, what really happens, and what precisely an interrupt vector is. It can be a tough concept, but once you see it happen, it’s completely clear. To do that, we need to be able to generate an interrupt on the Z80.

The Z80 has two interrupt lines , really, the /INT signal, and the /NMI signal. We pinned /NMI to the + bus to keep it from ever being activated. /NMI, as the name Non-Maskable Interrupt suggests, can’t be turned off. If we have a button problem or an accidental push of the interrupt button during a program we don’t want interrupts in, /NMI can send the Z80 off on a non-existent interrupt vector that goes nowhere, and from which we can’t recover.

/INT, by contrast, is much better behaved. We’ll wire it to a tactile button.

Once you get the button in your breadboard, wire one side of its switch to the - bus, and wire the 10kΩ resistor from the other side to the + bus. By now you know that we can safely short the + bus to the - bus through a 10kΩ resistor .

The 10kΩ resistor is a pullup for the interrupt circuit, so wire from the junction of the switch and the 10kΩ resistor to pin 16 of the Z80, the /INT pin. When you push the button, it will lower the voltage on the /INT circuit to zero, and since /INT is active low, this will send the Z80 an interrupt signal. Until we turn interrupts on, the button won’t do anything, and that’s fine.

Memory Read/Write Signals

We’ll be using the ATmega to simulate RAM for the Z80. This will be slow. In order to make it as fast as possible, we’ll do it with a pair of ISRs. This is why /RD and /WR need to be tied to two of our three INT pins on the ATmega.

Connect the /RD signal on the Z80 to INT1 on the ATmega (Z80 pin 21 to ATmega pin 17). Connect the Z80’s /WR (pin 22) to the ATmega’s INT0 (pin 16).

Reset and Wait Signals

Our simulated memory will be slow, as I said. To make sure the Z80 stays in sync with such a slow memory, we’ll need the /WAIT line. /WAIT is on Z80 pin 24. Connect it to PD7 (ATmega pin 21, over on the other side of the ATmega from the rest of port D.)

We’ll also need to be able to reset the Z80 (a lot). We could wire the Z80 to the ATmega’s reset circuit, but this would mean we’d have to restart the sketch on the ATmega every time we wanted the Z80 to reset, and that the Z80 might well get going, asking for memory, before the sketch is ready for it. Instead, we’ll wire the /RESET signal on the Z80 (pin 26) to PD6 on the ATmega (pin 20) .

The Z80’s reset must be held down for at least three clock cycles to ensure a reset. The datasheet doesn’t specify why.

When you’re done with the control bus signals, go ahead and hank those wires up, too.

Memory Address Bus

One more bus to go. This, by now, is standard stuff for us. Divide the 16 bit memory address bus into an 8 bit LSB and MSB, and wire each of those into the corresponding pins of an ATmega port.

Starting with the Least Significant Bits, wire A0-A7 on the Z80 (pins 30-37 in order) to PA0-PA7 on the ATmega (pins 40-33, in that order). It’s nice when pins and their signals come in order like this. It doesn’t happen most of the time.

For the MSB, we’ll need pins A8-15 on the Z80. These are, in order, 38, 39, 40, 1, 2, 3, 4, and 5. Wire these to PB0-PB7, pins 1-8 in that order, on the ATmega.

That’s it. That’s all the wiring there is. I suggest hanking the memory address bus lines in two groups, one for each port on the ATmega. That’s how I did it.

The Sketch

If you drink coffee, now would be a good time. This sketch is a big one, with a lot going on. It’s got a timer with an output, two interrupts, three ISRs (although we only use one or two at a time), three classes, a handful of utility functions, and three, count ’em three global variables, two of which are pointers. It’s not really as bad as it sounds. Let’s dive in.

The Plan

This sketch has nine menu options divided into three groups.

We can reset the Z80, set the clock speed, and stop the clock in the Z80 Commands group.

We can hook the Free Run ISR up, or disconnect it in the Free Run ISR Commands Group.

The Memory Simulation and Z80 Programming group is the big one, where we can set up simulated memory, enter programs into it, run programs by attaching the mem_sim_ISRs, and dump the simulated memory to the Serial Monitor.

Clock Setting

Clock setting will configure timer/counter 1 to generate a specific, square-wave frequency and output it to OC1A without further intervention from the ATmega. Because the Z80 will begin running right then, and won’t stop as long as the clock is running, we’ll also provide a way to turn the clock off.

Free Running

Free running is an easy way to test a microprocessor to see if its most basic functionality is working: is it trying to read instructions. To free run, all you really need are the microprocessor, a clock, and some wire, although to see what’s going on requires a bit more.

From our discussions in Z80 internals, you know that the Z80, once it restarts, goes into an M1 instruction fetch cycle. It pulls /MREQ, /RD and /M1 down, puts an address on the address bus, and reads whatever is on the data bus. Free-running essentially puts a fixed value on the data bus, an instruction called NOP, whose opcode is, conveniently, 0x00.

A NOP causes the Z80 to do nothing at all. The Z80 fetches the instruction, increments the PC (Program Counter), and goes into its next fetch .

If we wire the data bus so that all the data lines are grounded, every time the Z80 does a read, it gets a NOP, and advances, advances the program counter, and tries again. It will carry on until it hits the end of memory, and when it overflows, it will go back to zero and start again.

It’s a great way to make sure your memory bus, data bus, and CPU are working.

You can do this without a sketch at all, except to generate the clock, but you’d have to wire a bunch of LEDs up to watch the data bus.

What we’ll do is generate the clock, connect an ISR to the /RD line (as instruction fetches are all memory reads). That ISR will always return 0x00 to the data lines, and it will output the memory address requested to the serial monitor.

We’ll also provide a way to disconnect the free_run_ISR without stopping the clock.

Memory Simulator

Memory simulation works pretty much the same way as free running, except that we’ll use an 8k array to simulate memory, and an ISR connected to the ATmega’s interrupts where the /RD and /WR signals come in.

Memory Reads

When the /RD signal goes active (low), INT1 will fire, and the mem_read_ISR will get called.

It will get the value on the memory bus by combining the inputs of port A and port B, then go to the cell of the memory simulator array corresponding to the address from the Z80, and return the contents of that cell to the Z80 on the data line.

The mem_read_ISR will also print what address was requested, what data was returned and, after reading the status of the /M1 signal, whether we’re in an instruction fetch or a data read cycle.

Memory Writes

When the /WR signal goes active (low) INT 0 will fire, and the mem_write_ISR will be called.

It will get the value on the Z80’s memory address bus by combining the input values of port a and port b, then go to that address in the memory simulator array. It will then copy data from the Z80’s data bus to the memory simulator array, then print the address requested and what data was written there on the serial monitor.

Editor

The big advantage of having simulated memory is that the ATmega can control what’s in memory. Since the Z80 will run happily with only 8k of RAM (or less) we’ll be writing a simple way to input bytes to the memory array and review those contents. Is it a slow way to write programs? Yes. Is it better than toggling them in with toggle switches on the address and data lines and reading them back on arrays of LEDs the way they did in the mid 1970s? Much.

Dumper

It’d be nice to be able to see what our programs look like in memory, and more importantly, what’s in memory after they’ve run. While this dumping is included in the editor, we’ll also connect the same function up so we can call it outside the editor.

The Code

So that’s the plan. Let’s get started. As usual, we begin with preprocessor defines.

Preprocessor Defines

#define MAX_CLOCK 20000000
#define MEM_SIZE 0x2000 // 8192 Bytes
#define RESET_MS 5000 // Z80 reset: hold /RESET low this long.

MAX_CLOCK is the maximum speed the a timer on our ATmega can be set to. This is always equal to the clock feeding the ATmega, which in this case is 20MHz. This means you’ll be able to generate a clock signal up to 20MHz. If you have an 8MHz or 4MHz Z80 on the end of that, bad things may happen to it.

MEM_SIZE is 0x2000, or 8192 Bytes. That’s 8k. That’s about 600 dollars worth (U.S.) in 1980. It’s half the ATmega’s available RAM. With careful tweaking, you can probably make this bigger, but none of the example programs use more than a tiny fraction of this space. Setting it to 0xFF would probably work, but I haven’t tried it.

RESET_MS is the number of milliseconds to hold /RESET low. I have this set for 5000, or five seconds. At the slowest clock speed available, 1Hz, this is more than enough. 3000 would probably work, but if your reset isn’t long enough your results can be very odd.

Function Prototypes

We talked about function prototypes already. Here are the prototypes for the utility functions, which we’ll discuss at length when we get done with all the classes.

String repeat(int number, char character);
String get_input_string();
uint32_t string2uint32_t(String input);
uint16_t hex_string2uint16_t(String input);
void menu();

Classes

The next part of the sketch is the declaration of the three classes we use in this sketch. As I’ll repeat several times, since we haven’t used objects much, remember that no object of a given class exists until one is instantiated, and that isn’t part of the class declaration. Okay? Okay.

cpu

The CPU class controls PORTD and PIND, the output and input registers of port d. It uses this to set and clear various control signals on the Z80, and to read the status of others. The only control signal it does not control is /CLK, the clock.

The CPU class also provides access to read the Z80’s address bus, and to read or set the Z80’s data bus. Remember that no objects of this type will exist until we declare one, which is much further down in the sketch. We’ll start by declaring the class.

class cpu {                                                      
Private Variables and Functions

The first thing in the class are the private variables and functions. They don’t have to be first. That’s just where I like to put them. The unsigned 8 bit integer saved_control_port is used to store the output setting of the control port (port D) on the ATmega. Could we just look at it directly, since it’s a global register? Yes, but if we’re going to use an object to control the CPU, then we should always use the object. So we need a place to store our current ATmega output status so we can restore it later.

  private:
    uint8_t saved_control_port = CTRL_DEFAULT;

Since we’re using this object and only this object to talk to the CPU, it didn’t make a lot of sense to use a lot of global defines, so instead I set these masks as constant uint8_ts. The first one’s a mask, to be ANDed with CONTROL_PINS (an alias for PIND we’ll set later) to return the /M1 signal’s status. The other three are values that can be set on CONTROL_PORT, the alias for PORTD. CTRL_DEFAULT holds /WAIT and /RESET high, and sets everything else low. Since most of the other signals are ATmega inputs (Z80 outputs), this matters not at all. It also ensures that all the ATmega inputs’ pullup resistors aren’t set. I’m kind of sloppy with them with the other states, but since we’re only in those states briefly and we don’t read any other signals in them, it doesn’t cause problems.

    const uint8_t M1_MASK = 0b00010000; //Read the /M1 signal
    const uint8_t RESET = 0b10111111; // Set the /RESET signal
    const uint8_t WAIT = 0b01111111; // Set the /WAIT signal
    const uint8_t CTRL_DEFAULT = 0b11000000; // default Z80 state

Next are variables that look like pointers and work kind of like pointers, but aren’t really pointers. These volatile uint8_t&s are reference variables. They are literally aliases, in this case for PORTD, PIND, PORTC, DDRC, and PINC. They could have been done with global #defines, but I chose not to. Note that these reference variables are private.

The prefix volatile, you’ll recall from chapter 10, is necessary any time a variable is accessed outside the current flow of code, like when an ISR access them.

    volatile uint8_t& CONTROL_PORT = PORTD;
    volatile uint8_t& CONTROL_PINS = PIND;
    volatile uint8_t& DATA_PORT = PORTC;
    volatile uint8_t& DATA_DDR = DDRC;
    volatile uint8_t& DATA_PINS = PINC;
Public Variables and Functions

Next come the public variables and member functions. Note the constructor, that gets fired when the CPU class is instantiated. It sets up all the ports this class uses. Note that I’ve used the real port registers. My reasoning for this is that for DDRA, DDRB, and DDRD, they are only set here, only accessed in this one function, and so have no aliases set. Using the alias in this particular function seemed confusing. Once the DDRs are set, we do use the aliases to set CONTROL_PORT and DATA_PORT to their default values. That’s all this constructor does.

  public:
    cpu() { // constructor
      Serial.println("CPU: Object created.");
      DDRA = 0b00000000; // Address LSB
      DDRB = 0b00000000; // Address MSB
      DDRC = 0b11111111; // Data Port;
      DDRD = 0b11100000; // control_port
      DATA_PORT = 0b00000000;
      CONTROL_PORT = CTRL_DEFAULT;
    }

The first member function of the cpu class is M1(). It returns the status of the /M1 flag as a boolean. Like the flag, what it returns is active LOW.

    boolean M1() {
      return (CONTROL_PINS & M1_MASK);
    }

The mode member functions, mode_default(), mode_wait(), save_mode(), restore_mode() either change the output settings of CONTROL_PORT, aka PORTD, to change the operating mode of the Z80, or save and/or restore the current status of CONTROL_PORT. Note that mode_save() and mode_restore() do no sanity checking whatsoever, so use with caution.

The mode_default() function sets the Z80 in normal mode, ready to run a program, and mode_wait() pulls the /WAIT line low, making it stay in the current instruction cycle. Should reset() (below) have been mode_reset()? Probably.

    void mode_default() {
      CONTROL_PORT = CTRL_DEFAULT;
    }
    void mode_wait() {
      CONTROL_PORT = CONTROL_PORT & WAIT;
    }
    void save_mode() {
      saved_control_port = CONTROL_PORT;
    }
    void restore_mode() {
      CONTROL_PORT = saved_control_port;
    }

Next, we set a public pair of reference variables to point to PINA and PINB, where our addresses will be visible. We use this functionality so often it didn’t seem prudent to abstract it in a function.

    volatile uint8_t& addr_msb = PINB;
    volatile uint8_t& addr_lsb = PINA;

The data_out() and data_in() functions set the DDR of the data port (DATA_DDR) to either input or output so the Z80 can send or receive data from the ATmega, and then either returns the value of the data bus (data_out()) or sets the data port to the value of the data passed to the function (data_in()).

These functions are a little confusingly named. They’re all about setting the ATmega’s ports, but they’re named (since this is the cpu class) from the Z80’s perspective. So when the Z80 is outputting (data_out()), the ATmega is inputting, and vice versa.

    uint8_t data_out() {
      DATA_DDR = 0b00000000;
      return DATA_PINS;
    }
    void data_in(uint8_t data) {
      DATA_DDR = 0b11111111;
      DATA_PORT = data;
    }

Here’s the reset function. You call it, and the Z80’s reset line gets held low for RESET_MS milliseconds. It also helpfully sends a message to the serial monitor.

    void reset(void) {
      Serial.println("CPU: Resetting...");
      CONTROL_PORT = CONTROL_PORT & RESET;
      delay(RESET_MS);
      Serial.println("CPU: Done Resetting.");
      CONTROL_PORT = CTRL_DEFAULT;
    }


Here's the end of the cpu class.
};
clock_gen

Next up is the clock_gen class. It’s a considerably simpler animal. It has only two member functions, a constructor and a destructor. The constructor takes a 32 bit unsigned integer parameter and uses it to compute the settings for the clock frequency the user requests. The destructor turns the clock off.

Once again, we’re controlling global registers by wrapping a class around them. Not strictly necessary, but it sure simplifies the rest of the sketch.

Private Variables

We start with a couple private arrays, prescale_values and prescale_bits, which are a lookup table. You look up the value you want (1024, 256, 64, 8, or 1) and use the index of the array you found it at to look up the bit setting to set timer/counter 1’s clock source bits to that prescaler.

class clock_gen {
  private:
    int prescale_values[5] = {1024, 256, 64, 8, 1};
    int prescale_bits[5] = {0b101, 0b100, 0b011, 0b010, 0b001};
Public Variables and Functions

The first public function of this class is the destructor, ∼clock_gen(). It’s fired when we delete an object of the clock_gen class. It sets timer/counter 1’s registers to zero, stopping the clock generator completely and disconnecting OCP1 from it. It also prints a helpful message to the serial monitor.

  public:
    // Destructor
    ∼clock_gen() {
      TCCR1A = 0;
      TCCR1B = 0;
      OCR1A = 0;
      TCNT1 = 0;
      Serial.println("Clock Generator: Object Deleted.");
    }

Next is the constructor, clock_gen(uint32_t desired_frequency). Given the desired frequency (in decimal Hz, always, so 2MHz is 2000000), it computes the best fit between prescaler and counter match value to get as close as possible to the frequency requested. Sometimes that’s not very close. The Cestino’s clock runs at 20MHz. That means if 20,000,000 doesn’t divide evenly by your desired frequency, you may be over or under a little bit. Fortunately, we don’t need absolute precision.

First, we Serial.println() a message to the serial monitor, then declare and initialize (in most cases) a fistful of variables. Notably, we set counter_value, a float, to the value of MAX_CLOCK/desired_frequency. If the desired frequency does not divide evenly into the Cestino’s clock (20MHz, normally) this will have a decimal component, and our resulting clock will not be exactly on the frequency.

    // Constructor
    clock_gen(uint32_t desired_frequency) {
      Serial.println("Clock: Object Created.");
      float counter_value = MAX_CLOCK / desired_frequency;
      float lowest_inaccuracy = 1.0;
      float current_steps = 0;
      int prescaler = 1;
      byte prescaler_config_bits;
      long int match;

Next, we step through the possible values of the prescaler (in prescale_values[]) and calculate which one produces the smallest decimal component when we divide counter_value by it. The smaller the decimal component, the closer the clock will be to the frequency we asked for.

Once we find the prescaler value, we set prescaler with that value, then set match to counter_value divided by the prescaler we chose. If match came out zero (usually because something rounded to there) we set match to counter_value.

      for (int c = 0; c <= 4; c++) {
        current_steps = counter_value / prescale_values[c];
        if ((current_steps - round(current_steps) < lowest_inaccuracy)
            && (current_steps <= 65535)) {
          lowest_inaccuracy = current_steps - ((int)current_steps);
          prescaler = prescale_values[c];
          match = round(current_steps);
          prescaler_config_bits = prescale_bits[c];
          if (match == 0) match = round(counter_value);
        }
      }

Next, we tell the user what values we generated.

      Serial.print("We want to count to ");
      Serial.println(counter_value, 2);
      Serial.print("For a clock speed of ");
      Serial.println(desired_frequency, DEC);
      Serial.print("I chose a prescaler of ");
      Serial.println(prescaler, DEC);
      Serial.print("And a match of ");
      Serial.println(match, DEC);
      Serial.print("We'll count to ");
      Serial.println((long int)prescaler * match, DEC);

Finally, we set the clock registers, activate the timer/counter, and tell the user that we’ve done so.

      TCCR1A = 0b01000000;
      TCCR1B = (0b00001000 | prescaler_config_bits);
      TCNT1 = 0;
      OCR1A = match;
      Serial.println("Clock Generator: Running");
    }
So ends the clock_gen class.
};
memory_simulator

The memory simulator class isn’t conceptually hard. Create an array of size MEM_SIZE, then, when given an index value for the array, either set or retrieve the value stored there. That’s really all it does, except that it also has the m_dump() member function that pretty-prints the contents of the memory array in 256 byte pages, and an editor for putting values into the memory array.

class memory_simulator {
Private Variables and Functions

We start by declaring two private variables, halt and mem_array[MEM_SIZE]. Halt stores the value of a halt instruction for this particular CPU (the Z80), and mem_array[] is the array we’ll use to simulate memory.

  private:
    volatile uint8_t halt = 0x76;
    volatile uint8_t mem_array[MEM_SIZE];

Next, we have some private functions for m_dump and m_edit. They Serial.println() messages when called.

    void dump_page_header() {
      Serial.println(" Address 0  1  2  3  4  5  6  7  8  9" +
                     String("  a  b  c  d  e  f   Data (text)"));
      Serial.println(repeat(75, '-'));
    }
    void m_edit_instructions() {
      Serial.println(" *** Mem-Sim Line Editor ***");
      Serial.print("Enter an address and data in HEX ");
      Serial.println("eg: 0x0000,0x76");
      Serial.println(""exit" to quit "dump" to view memory ");
    }
Public Variables and Functions

The public member variables are a volatile boolean called m_write_enable. If this is false, m_seek_write() will say it’s written to memory, but it won’t actually do it. This is a kludge to protect simulated memory from being written to during Z80 resets.

  public:
    volatile boolean m_write_enable = true;

The constructor of memory_simulator() zeros out the array. Some programming environments do that for you. The Arduino environment isn’t one of them. That memory can contain literally anything. If you’re curious, some time do a dump memory command without initializing the memory. Mine shows a lot of machine code and text strings from this sketch.

Once the array is wiped, tell the user how much memory is available in an old school “bytes free” message.

    memory_simulator() { // constructor
      Serial.println("Memory: Initializing...");
      for (int c = 0; c < MEM_SIZE; c++) {
        mem_array[c] = 0;
      }


      Serial.println("Memory: " + String(MEM_SIZE, DEC) +
                     " (0x" + String(MEM_SIZE, HEX) + ") bytes free.");
    }

Is there no destructor? Technically there is, but it’s the one C++ assigns for us. That’s fine, in this case. We’re not setting any ATmega registers that need to be cleared in this class.

The next member functions are m_seek_read() and m_seek_write(). These functions, as the name suggest, grind an array index value from the two address bytes we can read from the cpu class, look that value up in the array, and either read the value or write it. Once again, to keep track of what is reading and what is writing, remember that this is from the memory_simulator class’s perspective, and memory simulator is pretending to be RAM. A memory write is when the Z80 sends data to the simulator. A memory read is when the Z80 reads data from the simulator.

    volatile uint8_t m_seek_read(volatile uint8_t address_msb, volatile uint8_t address_lsb) {

You’ve seen me do this next bit half a dozen times now. Here, we’re doing it backward. Put two uint8_ts into the union, get one uint16_t out. If you’re changing code, make sure you put the bytes in in the correct order for a little endian CPU like the Z80. Does your ear tell you I might have gotten that wrong once during the development of this project? As usual, your ear is good.

      volatile union {
        volatile uint16_t sixteen_bit_address;
        volatile uint8_t byte_array[2];
      } addr_byte_union;
      addr_byte_union.byte_array[0] = address_lsb;
      addr_byte_union.byte_array[1] = address_msb;

Here, we sanity check the address that was requested. If it’s too big, we go ahead and return a value anyway, but we return the value in the private variable halt. Otherwise we return the value in mem_array at the 16 bit unit address we got from the union earlier.

      if (addr_byte_union.sixteen_bit_address >= MEM_SIZE) {
        return halt;
      } else {
        return mem_array[addr_byte_union.sixteen_bit_address];
      }
    }

The member function m_seek_write() works pretty much the same way, except that it takes three parameters, adding a volatile uint8_t for data, and returns nothing. It also checks to see if m_write_enable is set, and if it’s not, only pretends to do the write. Other than that it looks like the same code as m_seek_read() because it is the same code as m_seek_read(). If a write is requested to an invalid address, m_seek_write() silently fails.

    void m_seek_write(volatile uint8_t address_msb, volatile uint8_t address_lsb, volatile uint8_t data) {
      if (m_write_enable) {
        volatile union {
          volatile uint16_t sixteen_bit_address;
          volatile uint8_t byte_array[2];
        } addr_byte_union;
        addr_byte_union.byte_array[0] = address_lsb;
        addr_byte_union.byte_array[1] = address_msb;
        if (addr_byte_union.sixteen_bit_address >= MEM_SIZE) {
        } else {
          mem_array[addr_byte_union.sixteen_bit_address] = data;
        }
      }
    }

The next member function, and one of the two really enormous functions in this class, is m_dump. If it looks a lot like the pretty printer from chapter 9, ATA Explorer? That’s because it’s a modified version of that same code. You’ve seen it before, and you know how it works, so my comments will be sparse on this function.

    void m_dump() {
      Serial.println("Dumping Simulated Memory");
      int c = 0;
      String line_start_address = "0x";
      uint16_t row_address = 0;
      String hex_data = "";
      String human_readable_data = "";

The pretty printer in chapter 9 did not have a header. This one does. Here’s where we call the private function dump_page_header(). After that, we start the main loop, one for every row of 16 addresses. With 8192 addresses total, that would be 512 rows.

For each row we generate the start address of the row (something else the pretty-printer in chapter 9 didn’t do) then loop through 16 cells of the memory array (instead of 32 as we did in Chapter 9), all the while building a pair of output strings, hex_data and human_readable_data. When we finish a row, we combine the two strings, plus our third string with the start address of the row, and Serial print them.

512 rows is far too much to output in one gulp, so there’s a simple paging mechanism that’s also new. If c, which is incremented for every cell of the array % 256=0 (c mod 256 =0 for 256 bytes per page) we stop, ask the user if they want to continue, and if they do re-print the header and carry on. If not, we set row to MEM_SIZE/16, which is its exit value, and when the main loop comes around again, it exits.

      dump_page_header();
      for (int row = 0; row < (MEM_SIZE / 16); row++) {
        row_address = 16 * row;
        if (row_address < 0x1000)line_start_address += "0";
        if (row_address < 0x0100)line_start_address += "0";
        if (row_address < 0x0010)line_start_address += "0";
        line_start_address += String(row_address, HEX) + "|";


        for (int col = 0; col < 16; col++) {
          if (mem_array[c] < 0x10) hex_data += "0";


          hex_data += String(mem_array[c], HEX);
          hex_data += " ";


          if (isprint(mem_array[c])) {
            human_readable_data += (char)mem_array[c];
          } else {
            human_readable_data += ".";
          }


          c++;
        }
        Serial.println(line_start_address + " " + hex_data +
                       " | " + human_readable_data);
        hex_data = "";
        human_readable_data = "";
        line_start_address = "0x";


        if (!(c % 256)) {
          Serial.println("Continue (y/n)");
          if (get_input_string() == "n") {
            row = MEM_SIZE / 16;
          } else {
            dump_page_header();
          }
        }
      }
    }

The m_edit() member function, the last one in this class (remember, we’re in the memory_simulator class) is another fairly simple function. It takes a text string, processes into either a pair of hex values or y (to continue) or dump (to display memory). It does not keep track of what page of memory you’re on and intelligently dump that page, nor can it save data you input anywhere but in simulated memory. By the time I was done writing the sample programs for the Z80, I sorely wished it did both, but it gets the job done. This function makes extensive use of the String object in Arduino, which isn’t particularly well documented, and is certainly not part of standard C++.

The function starts out initializing input_string to “y” which is probably unnecessary. It has a 16 bit unsigned integer for an address and an eight bit unsigned integer for data. It also has an integer called comma_index to store the location of the comma in the pair of hexadecimal numbers. Finally, it declares a String object called temp.

    void m_edit() {
      String input_string = "y";
      uint16_t address;
      uint8_t  data;
      int comma_index = 0;
      String temp;

Next, m_edit calls the m_edit_instructions() private function to print its instructions. As with m_dump this happens more than once in m_edit, so it made sense to use a private function for it. Once that’s done we start a do loop. I haven’t used those much. Essentially they’re a while loop upside down, so the action is always done at least once. Then we call the external function get_input_string(). This is a function we’ll cover in the Utility Function section. It is a combination of two Serial object functions: it waits for serial data to become available (forever if need be) and reads that String in and returns it.

      m_edit_instructions();
      do {// repeat loop until the 'while' is satisfied.
        input_string = get_input_string();

If the input string is “dump” we call m_dump(). When we return from m_dump(), call m_edit_instructions() again.

        if (input_string == "dump") {
          m_dump();
          m_edit_instructions();
        }

If input_string starts with the character “0x”—a proper hex value for the address—then it’s okay to try and find the comma in the string, and set temp to the first half of the string, from the beginning to the comma. Once we call temp.trim(), another String object member function, to remove any excess spaces, we can call the external function “hex_string2uint16_t()” with the string. This, too, is in the Utility Functions section. Set address to the result.

        if (input_string.startsWith("0x", 0)) {
          comma_index = input_string.indexOf(",");


          temp = input_string.substring(0, comma_index);
          temp.trim();


          address = hex_string2uint16_t(temp);

Having found our address, we set temp to the substring function of the String object input_string, starting at comma_index+1 and going to the end. (We don’t have to tell it to go to the end. The fact that we haven’t included an end value makes it do that by default.) Call temp.trim() again, and feed the result to hex_string2uint16_t again, cast the result to a uint8_t, and store that in data. Data will always be an eight bit byte, but hex_string2uint16_t builds eight bit bytes on its way to building sixteen bit bytes, so the results will be correct.

Once that’s done, we tell the user what data we got and where we’re going to put it in simulated memory. Then we do it.

          temp = input_string.substring(comma_index + 1);
          temp.trim();
          data = (uint8_t)hex_string2uint16_t(temp);


          Serial.println(">  Addr: 0x" + String(address, HEX) +
                         " Data: 0x" + String(data, HEX) + " [OK]");


          mem_array[address] = data;
        }

Finally, we see if the user typed “exit”. If not, the do while loop test passes, and we go back to the beginning of the loop.

      } while (input_string != "exit");
    }
};

That’s the end of the memory_simulator class, and all the classes in this sketch. Remember that while we’ve declared three classes, no objects of these classes exist, and we can’t use their variables or their member functions until one does. I know. I’m repeating it. It bears repeating.

Fortunately, that happens next.

Global Variables

There are three global objects (variables are a class of object in C++) in this sketch. Two of them are ourclock, a pointer to a clock_gen object, and mem_sim, a pointer to a memory_simulator object. These objects still won’t be instantiated yet.

There is also Z80, which is declared as a cpu object. This object is instantiated right there, the constructor fires, and we can use the functions in it. It instantiates here because we never actually turn it off.

clock_gen* ourclock = NULL;
memory_simulator* mem_sim = NULL;
cpu Z80;

Utility Functions

The utility functions are part of no class, but they may be called from inside classes, other utility functions, setup() or loop(). They’re mostly tiny utilities, save one, menu(), which is quite large.

repeat()

We need to make a lot of lines of dashes and whatnot in the menu for this sketch. Repeat makes that much simpler. You pass it a single character in a char variable, give it a number, and it returns a String object with that character repeated that many times.

String repeat(int number, char character) {
  String temp = "";
  for (int c = 0; c < number; c++) {
    temp = temp + String(character);
  }
  return temp;()
}
get_input_string()

This sketch does a lot of input. Rather than implement the usual wait-forever-for-serial loop followed by a Serial-readString() over and over again, I put them in a function. This function takes no parameters and returns a String object with the string the user typed. If the user never sends any data through the serial monitor, this function will happily loop forever.

String get_input_string() {
  while (!Serial.available()) {
  }
  return (Serial.readString());
}
string2uint32_t()

This function may look very familiar. It’s copied from the sketch in Chapter 9. It takes a String object with a number typed in decimal in it and turns it into a uint32_t, a 32 bit unsigned integer. String objects actually have this functionality, it turns out, but it’s practically undocumented, and there’s no indication how large an integer their function will handle. Rather than poke through the Arduino core code, we’ll use the same function we used in Chapter 9.

This function processes the string from left to right. For each digit it encounters, it multiplies the uint32_t temp by ten, then computes the numeric value of the digit by subtracting the character number of zero from it, and adds the result to temp. When it reaches the end of the string, it returns temp.

uint32_t string2uint32_t(String input) {
  uint32_t temp = 0;
  input.trim();
  for (int c = 0; c < input.length(); c++) {
    temp = temp * 10 + input.charAt(c) - '0';
  }
  return temp;()
}
hex_string2uint16_t()

Not only does this sketch need to process strings into decimal values, it needs to process strings into hex values. Whereas strin2uint32_t is a classic algorithm, hex_string2uint16_t is a somewhat more complicated, modified version of that algorithm that I put together myself.

We start out with temp=0, just as in the last function, and at the beginning of the loop, we multiply temp by 16.

uint16_t hex_string2uint16_t(String input) {
  uint16_t temp = 0;
  char tempchar;
  input.trim();
  for (int c = 2; c < input.length(); c++) {
    temp = (temp * 0x10);

Here’s where things get complicated. Alphabetic characters are not as neatly arranged in numeric codes as numbers are, so we have to tinker more. We set tempchar to the character at position c of the String object input. If the character is lower on the ASCII chart than “:”, it’s a number from zero to nine. Add the character code minus the character code for zero to temp just as in the last function.

    tempchar = input.charAt(c);
    if (tempchar < ':') temp += (tempchar - '0');

If tempchar is between “@” and “G”, it’s a capital letter between A and F. Subtract the character code for “7” from the character code in tempchar and add the result to temp.

    if ((tempchar > '@') && (tempchar < 'G')) temp += (tempchar - '7');

if tempchar is between “’” and “g”, it’s a lower case letter between a and f. Subtract the character code of “W” from the character code value of tempchar and add the result to temp.

    if ((tempchar > '`') && (tempchar < 'g')) temp += (tempchar - 'W');
  }

Loop through the whole number like this. Once we’re done, return the value of temp.

  return temp;()                                                                                                                        

}
menu()

The menu function works exactly the same way the menu in loop() worked in Chapter 9,so I won’t cover the mechanics of the switch/case structure again. You already know. There’s quite a bit going on in this menu, though.

We start out clearing input_string and displaying the menu. Note the extensive use of repeat().

void menu() {
  String input_string = "";
  Serial.println(" " + repeat(15, ' ') + "*** Menu ***");
  Serial.println(repeat(42, '-'));
  Serial.println("Z80 Operations Commands");
  Serial.println(repeat(42, '-'));
  Serial.println("(1) Reset Z80");
  Serial.println("(2) Set Clock Speed");
  Serial.println("(3) Stop Clock");
  Serial.println(repeat(42, '-'));
  Serial.println("Free Run ISR Commands");
  Serial.println(repeat(42, '-'));
  Serial.println("(4) Attach Free Run ISR to INT1");
  Serial.println("(5) Detach Free Run ISR from INT1");
  Serial.println(repeat(42, '-'));
  Serial.println("Memory Simulation and Z80 Programming");
  Serial.println(repeat(42, '-'));
  Serial.println("(6) Initialize Simulated Memory");
  Serial.println("(7) Enter Program Into Simulated Memory");
  Serial.println("(8) Run Program (Attach Mem_Sim ISRs)");
  Serial.println("(9) Dump Simulated Memory");
  Serial.println(repeat(42, '-'));

Next, we print the status of the clock and whether simulated memory is available. Both of these checks are done the same way: see if the pointer is NULL. We initialized the pointers to NULL when we declared them in the global variables section, and when menu itself deletes these objects it resets the pointers to NULL, so it’s a safe assumption that when the pointers are not null, there really is something out there that they point to.

Thus, if ourclock is not NULL, the address in ourclock must point to a valid clock_gen object. Since clock_gen’s constructor demands a value to set the clock to, then sets the clock and starts it, the clock will be running if there’s a clock_gen object.

Likewise, if there’s something other than NULL in the mem_sim pointer, there will be a memory_simulator object at that address.

So we tell the user these things, and generate another dotted line.

  Serial.print("Clock is ");
  if (ourclock != NULL) {
    Serial.println("Running.");
  } else {
    Serial.println("Stopped.");
  }
  Serial.print("Memory is ");
  if (mem_sim != NULL) {
    Serial.println("Available.");
  } else {
    Serial.println("Not Available.");
  }
  Serial.println(repeat(42, '-'));

Next, we call get_input_string(). When it returns, we pass the result into string2uint32_t, cast that result into an int, and use that result of the cast to switch to the correct case. You already know all about switches and case statements.

Menu Option 1

Option 1 is reset. We call the reset() member function of the object Z80. Remember that Z80 was declared as an object of the cpu class, and if you recall, the cpu class has a function called reset(). This is how object/member function calling is done. There are other ways to do it, but this is the most clear as far as I’m concerned.

  switch ((int)string2uint32_t(get_input_string())) {
    case 1: Z80.reset();
      break;
Menu Option 2

Option 2 sets the clock speed. That’s what the menu item says, at least. And sure enough, we set input_string equal to the results of calling get_input_string() like you’d expect. What this option really does, however, is this:

Check to make sure there’s not already a clock_gen object that ourclock points to by deleting it and pointing it at NULL.

Call new and pass() the clock_gen class as an argument. The clock_gen constructor requires a numeric value of Hertz as a parameter. We have the String object the user typed in with that value as a text string, we call string2uint32_t on input_string, and pass the result as the parameter to the clock_gen constructor.

Congratulations, the clock_gen object ourclock finally exists, and the timer/counter is configured and the clock is running. But we need to reset the Z80.

    case 2:
      Serial.println("Menu: Enter Z80 Clock Speed (1 - 20000000Hz)");
      input_string = get_input_string();
      delete ourclock;
      ourclock = NULL; // clear the pointer. Otherwise things get confused.
      ourclock = new clock_gen(string2uint32_t(input_string));

Before we can reset the Z80, we should really disable writing to whatever memory_simulator object might happen to be available. We don’t know whether one is or not, and we don’t actually care. If mem_sim points to null, nothing will happen, and nothing needs to happen. So we just blindly call mem_sim.m_write_enable=false, right?

Well, no. Remember that mem_sim isn’t an object of the memory_simulator class, it’s a pointer to an object of the memory_simulator class. To access mem_sim’s member variable m_write_enable, we have to dereference it with a ->.

Once we’ve disabled writes to any memory_simulator mem_sim may or may not point to, we can go ahead and call Z80.reset(). Once the reset comes back, we set mem_sim->m_write_enable back to true.

      mem_sim->m_write_enable=false;
      Z80.reset();
      mem_sim->m_write_enable=true;
      break;
Menu Option 3

Option 3 stops the clock. The only way to do this is to delete it. That’s what option 3 does: call delete on the pointer ourclock, and then set the pointer’s address to NULL. Does this seem unnecessary? It’s not. I’ve seem some very strange behavior out of the clock without that step.

    case 3:
      delete ourclock;
      ourclock = NULL; // Clear the pointer, otherwise things get confused.
      break;()
Menu Option 4

Option 4 hooks the free_run_ISR (which we’ll talk about below) up to INT1, pretty much the way we did it in chapter 10, using noInterrupts, and attachInterrupt. The only difference is we make sure there’s not already an ISR connected to INT1 by calling detachInterrupt(1) (for INT1), and then call attachInterrupt and re-enable interrupts.

    case 4:
      Serial.println("Menu: Attaching free_run_ISR to INT1");
      noInterrupts();
      detachInterrupt(1);
      attachInterrupt(1, free_run_ISR, FALLING);
      interrupts();
      Z80.reset();
      break;
Menu Option 5

Option 5 detaches the free_run_ISR from INT1, and resets the Z80.

    case 5:
      Serial.println("Menu: Detatching free_run_ISR from INT1");
      noInterrupts();
      detachInterrupt(1);
      interrupts();
      Z80.reset();
      break;
Menu Option 6

Option 6 does two critical things for our memory_simulator object. First, it deletes any that already exist, then sets the mem_sim pointer to NULL for safety. It then calls new on the object type memory_simulator() and sets the mem_sim pointer to that address. Congratulations. Achievement unlocked. We have a memory_simulator class object instantiated, its constructor has fired, and the pointer mem_sim now points to it.

    case 6:
      delete mem_sim;
      mem_sim = NULL;
      mem_sim = new memory_simulator();
      break;()
Menu Option 7

Option 7 is to edit the contents of the memory_simulator object mem_sim points to. It calls mem_sim->m_edit(), which is the m_edit() member function of the memory_simulator object that mem_sim points to. There’s no safety net here. I really have no idea what happens if you try to edit a nonexistent memory simulator.

I can’t imagine it’s good.

    case 7:
      mem_sim->m_edit();
      break;
Menu Option 8

Option 8 hooks mem_read_ISR and mem_write_ISR up to INT1 and INT0, respectively. It works just exactly like attaching the free_run_ISR to INT1 did, except that there are two ISRs instead of one. We’ll talk about those ISRs later, I promise.

    case 8:
      Serial.println("Menu: Attaching mem_read_ISR to INT1.");
      Serial.println("Menu: Attaching mem_write_ISR to INT0.");
      Serial.println("Menu: Any program therein should run.");
      noInterrupts();
      detachInterrupt(1);
      detachInterrupt(0);
      mem_sim->m_write_enable=false;
      attachInterrupt(1, mem_read_ISR, FALLING);
      attachInterrupt(0, mem_write_ISR, FALLING);
      mem_sim->m_write_enable=true;
      interrupts();
      Z80.reset();
      break;
Menu Option 9

Option 9, the last option, calls the m_dump() member function of the memory_simulator object that mem_sim points to. This starts to sound like one of those chain folk stories that ends “Piggy won’t go over the style, and I sha’ant get home tonight.” Pointers always wind up sounding that way. If you’re curious, look up linked lists some time.

    case 9:
      mem_sim->m_dump();
      break;


  } // end of case.
} // end of menu function() .

Free Run ISR

The free_run_ISR is attached to INT1 on the falling edge (since /RD is active low). When triggered, it sets the Z80 into wait mode, thus setting /WAIT active (low), then it reports the address requested and sends a 0x00 (NOP) on the Z80’s data bus. Since NOP does nothing, the Z80 then goes on to the next address, and the next, and so on.

We begin by initializing a couple of variables and preserving the Z80’s current mode.

void free_run_ISR() {
  byte temp;
  String output = "";
  Z80.save_mode();

We then call mode_wait() from the cpu class object Z80. This makes the Z80 wait until the ISR can service its memory request. The memory request is serviced immediately afterwards, with a call to data_in() on the same object, with a parameter of 0x0, the opcode for NOP.

  Z80.mode_wait();
  Z80.data_in(0x0);

After that, we build the output string with both bytes of the Z80’s address bus, Z80.addr_msb and Z80.addr_lsb. Remember, those are alternate names for PINB and PINA, declared in the cpu class.

  output += String("free_run_ISR: Address: 0x");
  if (Z80.addr_msb < 0x10) output += String("0");
  output = output + String(Z80.addr_msb, HEX);
  if (Z80.addr_lsb < 0x10) output += String("0");
  output += String(Z80.addr_lsb, HEX);
  Serial.println(output);

Then we restore the Z80 to whatever mode it was in before, releasing /WAIT by setting it high.

  Z80.restore_mode();
}

That wasn’t too bad. The good news is the next two ISRs work pretty much the same way, except that they access the memory_simulator object.

Memory Read ISR

Like free_run_ISR , mem_read_ISR is attached to INT1 on the falling edge. (Obviously you can’t have both free_run_ISR and mem_read_ISR attached at the same time.) Like free_run_ISR, mem_read_ISR reads the address bus of the Z80 and returns a value to its data bus, but this ISR uses the m_seek_read() function of the memory_simulator class object pointed to by mem_sim to look up the value stored in the memory simulator at the address the Z80 asked for. It sends that value to the Z80’s data bus, and also prints what it’s doing for the user.

One other thing: this ISR also watches the /M1 signal, and tells the user whether the Z80 is in an M1 machine cycle, which is an instruction fetch. If /M1 isn’t low, what we’re doing is a data fetch. This turns out to be very useful for debugging assembly programs.

We begin by initializing a pair of variables, saving the Z80’s existing control bus state, and setting the Z80 in wait mode. This works exactly the same way it did in free_run_ISR().

void mem_read_ISR() {
  uint8_t tempdata = 0;
  String output = "";


  Z80.save_mode();

  Z80.mode_wait();

Next, we load tempdata with the data stored at the address the Z80 requested. We do this by calling mem_sim->m_seek_read with the MSB and LSB of the Z80’s address bus. Yes, the order looks wrong. It gets reversed again in m_seek_read so we’re in the right order for a little endian CPU like the Z80.

  tempdata = mem_sim->m_seek_read(Z80.addr_msb, Z80.addr_lsb);

Next, we start building the output string. We begin with the results of testing the /M1 line.

  if (!Z80.M1()) {
    output += String("mem_read_ISR: Z80 Fetched Address: 0x");
  } else {
    output += String("mem_read_ISR: Z80 Read Address: 0x");
  }

Now we send the Z80 tempdata, which, you will recall, contains the value stored in the memory simulator at the address requested by the Z80. After that, we build the string to tell the user what just happened, clear the /WAIT signal, and we’re done. Return control back to wherever it came from. ISRs should be as short as possible.

  Z80.data_in(tempdata);
  if (Z80.addr_msb < 0x10) output += String("0");
  output += String(Z80.addr_msb, HEX);
  if (Z80.addr_lsb < 0x10) output += String("0");
  output += String(Z80.addr_lsb, HEX);
  output += String(" Data: 0x");
  if (tempdata < 0x10) output += String("0");
  output += String(tempdata, HEX);
  Serial.println(output);
  Z80.restore_mode();
}

Memory Write ISR

The mem_write_ISR works almost exactly like the mem_read_ISR. If the code looks the same, it’s because started out as a copy of the mem_read_ISR. There are differences.

The mem_write_ISR is attached to the falling edge of INT0, which is connected to the /WR line. It calls m_seek_write() instead of m_seek_read() on the memory simulator object. And, it calls Z80.data_out() instead of Z80.data_in() mem_write_ISR does not pay attention to the /M1 signal. Writes do not occur in M1 cycles.

We begin, as usual, setting up the output String object, and the uint8_t tempdata. We save the Z80’s control bus mode, and activate the Z80’s /WAIT signal so the mem_write_ISR is guaranteed time to service the request.

void mem_write_ISR() {
  String output = "";
  uint8_t tempdata = 0;


  Z80.save_mode();
  Z80.mode_wait();

We then copy Z80.data_out, the data the Z80 has placed on its data bus, into tempdata, and immediately call mem_sim->m_seek_write with the address bytes and tempdata to write the data to the memory simulator. Then we begin generating the output string for the user.

  tempdata = Z80.data_out();
  mem_sim->m_seek_write(Z80.addr_msb, Z80.addr_lsb, tempdata);
  output += String("mem_write_ISR: Z80 Wrote Address: 0x");

The Arduino HEX output system leaves the leading zeros off. This isn’t wrong, but it’s harder to keep track that this is a byte value, or a two-byte word value. What we do next is, if a byte (addr_msb, addr_lsb or tempdata) is less than 0x10, add a leading zero to it in the output string.

  if (Z80.addr_msb < 0x10) output += String("0");
  output += String(Z80.addr_msb, HEX);
  if (Z80.addr_lsb < 0x10) output += String("0");
  output += String(Z80.addr_lsb, HEX);
  output += String(" Data: 0x");
  if (tempdata < 0x10) output += String("0");
  output += String(tempdata, HEX);

Finally, we Serial.println the output string, restore the Z80’s state, and we’re done.

  Serial.println(output);
 Z80.restore_mode();
}

Setup()

The setup() function does practically nothing in this sketch. It sets up the serial monitor at 115,200 baud. That’s it.

void setup() {
  Serial.begin(115200);
}

Loop()

The loop() function does even less. It calls menu(). That’s all. Sure, the menu could have been in loop() instead of its own function, as we’ve done in all the previous chapters, but there’s a certain amount of debate whether loop() or main() in more normal programming environments should do anything other than call functions. I chose to move the menu out to its own function this time to demonstrate that.

void loop() {
  menu();
}

And we’re done with the sketch. As always, the full code, replete with comments, is next.

The Full Code

// Hardware
//-----------------------------------------------------------
// Z80 Signals Connected to ATmega1284P Port D:
//--------------------------------------------
// Signal: /WAIT /RESET /CLK       /M1 /RD /WR (-) (-)
// class: cpu    cpu    clock_gen  cpu cpu cpu (serial)
//
// All Other Z80 Signals
//----------------------
// /MREQ /IORQ /RFSH /HALT /INT   /NMI /BUSRQ
// n/c   n/c   n/c   n/c   button n/c  n/c
//-----------------------------------------------------------
// Z80 Address + Busses
// Signal:      A0-A7   A8-A15 D0-D7
// ATmega Port: PORTA   PORTB  PORTC
// Class:       cpu     cpu    cpu
//-----------------------------------------------------------


//-----------------------------------------------------------
// Preprocessor #defines
//-----------------------------------------------------------
#define MAX_CLOCK 20000000 // Maximum clock speed
#define MEM_SIZE 0x2000 // 8192 Bytes
#define RESET_MS 5000 // Z80 reset: hold /RESET low this long.


//-----------------------------------------------------------
// Function Prototypes
// Since some member functions of our classes call utility
// functions, it behooves us to declare function prototypes.
// Arduino 1.6.5 does this for us, but 1.6.7 seems to break
// that behavior.
//-----------------------------------------------------------
String repeat(int number, char character);
String get_input_string();
uint32_t string2uint32_t(String input);
uint16_t hex_string2uint16_t(String input);
void menu();


//-----------------------------------------------------------
// Classes
//
// Since we're using software running on the ATmega1284P to
// simulate hardware, it makes sense to break the code up
// into objects along the same lines.
//-----------------------------------------------------------


//-----------------------------------------------------------
// cpu class:
//
// An object of this class reads and writes the various pins
// of PORT D to control the Z80, read its status signals,
// provide access to its address bus, and read from or write
// to its data bus.
//-----------------------------------------------------------
class cpu {
  private:
    // These are private variables, constants, and (potentially)
    // member functions. These can't be touched by code outside
    // this class.


    uint8_t saved_control_port = CTRL_DEFAULT;
    // We often need to preserve the state of the
    // control port before we change it, and we may not
    // know exactly what it is. It's stored in this private
    // variable.


    const uint8_t M1_MASK = 0b00010000; //Read the /M1 signal
    const uint8_t RESET = 0b10111111; // Set the /RESET signal
    const uint8_t WAIT = 0b01111111; // Set the /WAIT signal
    // These consts define various bit values we need to
    // set or read the named status signals of the Z80
    // with PORT D.


    const uint8_t CTRL_DEFAULT = 0b11000000; // default Z80 state
    // This is the default state for PORTD and by extension, the Z80.
    // Hold /WAIT and /RESET high. /CLK is under timer control.
    // /WR, /MREQ and /RD are all inputs from the ATmega's view.


    volatile uint8_t& CONTROL_PORT = PORTD;
    volatile uint8_t& CONTROL_PINS = PIND;
    // These are reference variables. They are run-time
    // aliases for PORTD and PIND. Note the & sign.
    // That denotes them as references. Note that they are private.


    volatile uint8_t& DATA_PORT = PORTC;
    volatile uint8_t& DATA_DDR = DDRC;
    volatile uint8_t& DATA_PINS = PINC;
    // Reference variables for our data port, PORTC.  These
    // are private and strictly for programming convenience
    // within this class.


  public:
    cpu() { // constructor
      Serial.println("CPU: Object created.");
      DDRA = 0b00000000; // Address LSB
      DDRB = 0b00000000; // Address MSB
      DDRC = 0b11111111; // Data Port;
      DDRD = 0b11100000; // control_port
      // Initialize the DDRs for all ports. Since only PORTC
      // has a DDR reference varable, I'm using the register
      // names for everything for consistency.


      DATA_PORT = 0b00000000;
      CONTROL_PORT = CTRL_DEFAULT;
      // Initialize the values of DATA_PORT and CONTROL_PORT.
    }
    // This is the constructor of this class. It is called when
    // an object of this class is instantiated. The constructor
    // sets ports A and B up to read the low and high memory
    // address bytes, and port C up (initially) to write data
    // to the Z80, but this port can also read data sent from
    // the Z80 to the system.


    boolean M1() {

      return (CONTROL_PINS & M1_MASK);
    }
    // This function returns the status of the Z80's /M1 signal.
    // It does a logical AND of CONTROL_PINS and the M1_MASK.
    // If any pins in that AND turn up true, the function will
    // return true. Because all Z80 signals are active low,
    // this will mean /M1 is inactive.


    void mode_default() {
      CONTROL_PORT = CTRL_DEFAULT;
    }
    // Set the CONTROL_PORT to its default. Put the Z80 in
    // non-halted, non-waited mode.  Run mode, really.


    void mode_wait() {
      CONTROL_PORT = CONTROL_PORT & WAIT;
    }
    // Lower the Z80's /WAIT signal so the Z80 will
    // not try and read until our ISRs are ready.
    // Even though the ATmega1284P is a universe
    // faster than we're running the Z80, our
    // memory is simulated in software. It's
    // really, really slow.


    void save_mode() {
      saved_control_port = CONTROL_PORT;
    }
    // Preserve the current value of CONTROL_PORT in the
    // private saved_control_port variable.


    void restore_mode() {
      CONTROL_PORT = saved_control_port;
    }
    // set CONTROL_PORT to the value in saved_control_port.
    // Note that we don't check that a save_mode was done
    // first, so use with caution.


    volatile uint8_t& addr_msb = PINB;
    volatile uint8_t& addr_lsb = PINA;
    // These two references give our sketch
    // a consistent name for the address bytes.
    // That way we can access them without
    // adding any more instructions.


    uint8_t data_out() {
      DATA_DDR = 0b00000000;
      return DATA_PINS;
    }
    // Set the DATA_PORT to read mode, and read the
    // Z80's data port for data coming FROM the Z80.
    // Return that data.
    // ATmega is READING. Z80 is WRITING.


    void data_in(uint8_t data) {
      DATA_DDR = 0b11111111;
      DATA_PORT = data;
    }
    // Make sure the data port is in WRITE mode, and
    // set it to the value of data.
    // ATmega is WRITING, Z80 is READING.


    void reset(void) {
      Serial.println("CPU: Resetting...");
      CONTROL_PORT = CONTROL_PORT & RESET;
      delay(RESET_MS);
      Serial.println("CPU: Done Resetting.");
      CONTROL_PORT = CTRL_DEFAULT;
    }
    // Lower the Z80's /RESET signal for RESET_MS milliseconds.
    // In order to reset properly the Z80 needs its /reset
    // signal held low for multiple clock cycles. Given that
    // our clock's minimum speed is 1Hz, 5 seconds seems like
    // a safe value. This could be dynamic based on the clock
    // speed, but we're not in that much of a hurry.
};


//-----------------------------------------------------------
// clock_gen class
//
// This class configures timer/counter 1 to output a square
// wave signal on OCP1, from 1Hz to MAX_CLOCK MHz, depending
// on how we configure it. There are only two member functions,
// a constructor and the destructor. When an object in this
// class is instantiated, it's done with a text string
// parameter containing the text value the user wants
// the clock set to.
// The constructor handles the rest: generates the prescaler
// value and the match value, and tells the user what it's
// done. Note that because the Cestino has a 20MHz system
// clock, its maximum clock resolution is 50ns. Some
// speeds will be approximations since they don't divide
// evenly by 50ns.
//-----------------------------------------------------------
class clock_gen {
  private:
    int prescale_values[5] = {1024, 256, 64, 8, 1};
    int prescale_bits[5] = {0b101, 0b100, 0b011, 0b010, 0b001};
    // lookup tables for prescale bit fields and their values.


  public:
    // Destructor
    ∼clock_gen() {
      TCCR1A = 0;
      TCCR1B = 0;
      OCR1A = 0;
      TCNT1 = 0;
      Serial.println("Clock Generator: Object Deleted.");
    }
    // The destructor is called when an object is deleted.
    // In this case, it clears all the timer registers, which
    // turns the timer off.


    // Constructor
    clock_gen(uint32_t desired_frequency) {
      Serial.println("Clock: Object Created.");
      float counter_value = MAX_CLOCK / desired_frequency;
      float lowest_inaccuracy = 1.0;
      float current_steps = 0;
      int prescaler = 1;
      byte prescaler_config_bits;
      long int match;


      for (int c = 0; c <= 4; c++) {
        current_steps = counter_value / prescale_values[c];
        if ((current_steps - round(current_steps) < lowest_inaccuracy)
            && (current_steps <= 65535)) {
          lowest_inaccuracy = current_steps - ((int)current_steps);
          prescaler = prescale_values[c];
          match = round(current_steps);
          prescaler_config_bits = prescale_bits[c];
          if (match == 0) match = round(counter_value);
        }
      }
      // Find the highest prescaler value that will both allow the
      // clock to generate the value the user wanted, with the
      // lowest inaccuracy of the resulting clock speed.
      // We do this by determining the number of steps the
      // maximum clock speed available (20MHz on the Cestino)
      // will occur for each step of our clock's output. Then we
      // iterrate from highest to lowest prescaler values to find the
      // one that divides most evenly into the number of steps.
      // We set our match value to the number of steps we want
      // divided by the prescaler.


      Serial.print("We want to count to ");
      Serial.println(counter_value, 2);
      Serial.print("For a clock speed of ");
      Serial.println(desired_frequency, DEC);
      Serial.print("I chose a prescaler of ");
      Serial.println(prescaler, DEC);
      Serial.print("And a match of ");
      Serial.println(match, DEC);
      Serial.print("We'll count to ");
      Serial.println((long int)prescaler * match, DEC);
      // tell the user what values we generated.


      TCCR1A = 0b01000000;
      TCCR1B = (0b00001000 | prescaler_config_bits);
      TCNT1 = 0;
      OCR1A = match;
      Serial.println("Clock Generator: Running");
      // set the timer/counter registers.
    }
};


//-----------------------------------------------------------
// memory_simulator class
// This class contains the array we're using as simulated ram
// for the Z80, and functions to access it.
//-----------------------------------------------------------
class memory_simulator {
  private:
    volatile uint8_t halt = 0x76;
    volatile uint8_t mem_array[MEM_SIZE];
    // declare a variable to hold the halt instruction
    // for the Z80, and declare the array we're using
    // for simulated memory. These are private variables and
    // cannot be touched directly by outside code.


    void dump_page_header() {
      Serial.println(" Address 0  1  2  3  4  5  6  7  8  9" +
                     String("  a  b  c  d  e  f   Data (text)"));
      Serial.println(repeat(75, '-'));
    }
    //Print the header for the dump pretty printer.


    void m_edit_instructions() {
      Serial.println(" *** Mem-Sim Line Editor ***");
      Serial.print("Enter an address and data in HEX ");
      Serial.println("eg: 0x0000,0x76");
      Serial.println(""exit" to quit "dump" to view memory ");
      // Print the handy instructions.
    }


  public:
    volatile boolean m_write_enable = true;
    // If m_write_enable is false, m_seek_write() will say it
    // wrote, but it won't actually do it. A kludge used to
    // protect simulated memory during Z80 resets.


    memory_simulator() { // constructor
      Serial.println("Memory: Initializing...");
      for (int c = 0; c < MEM_SIZE; c++) {
        mem_array[c] = 0;
      }// Wipe the entire array. There's no telling what was
      // in that memory before we declared the array.


      Serial.println("Memory: " + String(MEM_SIZE, DEC) +
                     " (0x" + String(MEM_SIZE, HEX) + ") bytes free.");
    }
    // tell the user how much memory we have.


    // The constructor of memory_simulator wipes the memory
    // array, which is declared on instantiation.after that,
    // it reports the available RAM to the user. We use the
    // default destructor, since we don't do anything special
    // there.


    volatile uint8_t m_seek_read(volatile uint8_t address_msb, volatile uint8_t address_lsb) {
      volatile union {
        volatile uint16_t sixteen_bit_address;
        volatile uint8_t byte_array[2];
      } addr_byte_union;
      addr_byte_union.byte_array[0] = address_lsb;
      addr_byte_union.byte_array[1] = address_msb;
      // We're using this union to plug in the address_msb and address_lsb
      // variables and get out a uint16_t.  Make sure to put the bytes in
      // in little endian order (lsb first) or your results will be very
      // odd once you go over address 0x0020.  Been there, did that.


      if (addr_byte_union.sixteen_bit_address >= MEM_SIZE) {
        return halt;


      } else {
        return mem_array[addr_byte_union.sixteen_bit_address];
      }
      // Check to see if we're trying to seek a legit address. If not,
      // return a z80 halt instruction and throw an error message.
    }
    // The m_seek_read member function decodes two separate bytes (address_msb and address_lsb)
    // into a single uint16_t address, then goes to that address and returns that cell of the
    // array. If the decoded address exceeds MEM_SIZE, we return halt, otherwise we return
    // the array at that address.


    void m_seek_write(volatile uint8_t address_msb, volatile uint8_t address_lsb, volatile uint8_t data) {
      if (m_write_enable) {
        // On Z80 resets we can get spurious triggering
        // of the write ISR resulting in random writes
        // to memory. Reset sets m_write_enable to false, then
        // back to true when the reset is done.


        volatile union {
          volatile uint16_t sixteen_bit_address;
          volatile uint8_t byte_array[2];
        } addr_byte_union;
        addr_byte_union.byte_array[0] = address_lsb;
        addr_byte_union.byte_array[1] = address_msb;
        // We're using this union to plug in the address_msb and address_lsb
        // variables and get out a uint16_t.  Make sure to put the bytes in
        // in little endian order (lsb first) or your results will be very
        // odd once you go over address 0x0020.  Been there, did that.


        if (addr_byte_union.sixteen_bit_address >= MEM_SIZE) {

        } else {
          mem_array[addr_byte_union.sixteen_bit_address] = data;
        }
        // Check to see if we're trying to seek a legit address. If not,
        // fail silently. Ugh.
      }
    }
    // The m_seek_write member function decodes the two bite field address
    // the same way m_seek_read does. If the decoded address exceeds
    // MEM_SIZE, we throw an error message, otherwise we set the
    // array[address] to data.


    void m_dump() {
      Serial.println("Dumping Simulated Memory");
      int c = 0;
      String line_start_address = "0x";
      uint16_t row_address = 0;
      String hex_data = "";
      String human_readable_data = "";
      // We have some variables. Initialize them.


      dump_page_header();
      //show the header


      for (int row = 0; row < (MEM_SIZE / 16); row++) {
        row_address = 16 * row;
        if (row_address < 0x1000)line_start_address += "0";
        if (row_address < 0x0100)line_start_address += "0";
        if (row_address < 0x0010)line_start_address += "0";
        line_start_address += String(row_address, HEX) + "|";


        for (int col = 0; col < 16; col++) {
          // We're printing blocks of 256 bytes.
          // each is 2 characters + two spaces
          // in hex, plus 1 character in text.
          if (mem_array[c] < 0x10) hex_data += "0";
          // Add leading zero for hex values below 0x10.


          hex_data += String(mem_array[c], HEX);
          hex_data += " ";
          // Add mem_array[c] as a hex string to hex_data.


          if (isprint(mem_array[c])) {
            human_readable_data += (char)mem_array[c];
          } else {
            human_readable_data += ".";
          }
          // If mem_array[c] is printable, add it to
          // human_readable_data. Otherwise add a . for
          // a placeholder.
          c++;
        }


        Serial.println(line_start_address + " " + hex_data +
                       " | " + human_readable_data);
        hex_data = "";
        human_readable_data = "";
        line_start_address = "0x";
        // Combine the three strings in one Serial.println.
        // Then clear them.
        if (!(c % 256)) {
          Serial.println("Continue (y/n)");
          if (get_input_string() == "n") {
            row = MEM_SIZE / 16;
          } else {
            dump_page_header();
            //show the header for each page, actually.
          }
        }
      }
    }
    // m_dump produces a human-readable dump of the memory array in // 256 byte pages, with each line 16 (0xF) items long.


    void m_edit() {
      String input_string = "y";
      uint16_t address;
      uint8_t  data;
      int comma_index = 0;
      String temp;


      m_edit_instructions();

      do {
        // repeat is loop until the 'while' is satisfied.
        input_string = get_input_string();
        // get the input string.
        if (input_string == "dump") {
          m_dump();
          m_edit_instructions();
          // If the user types "dump," dump the memory to
          // the serial console. Don't try and process it
          // into code. Show the instructions again.
        }
        if (input_string.startsWith("0x", 0)) {
          comma_index = input_string.indexOf(",");
          // find the comma in the input string


          temp = input_string.substring(0, comma_index);
          temp.trim();
          // copy the string from the beginning to the comma into temp.
          // also trim it - get rid of leading and trailing spaces.


          address = hex_string2uint16_t(temp);
          // call hex_string2uint16_t with temp. Set the results into
          // address.


          temp = input_string.substring(comma_index + 1);
          temp.trim();
          data = (uint8_t)hex_string2uint16_t(temp);
          // repeat the process with the back half of the input string.


          Serial.println(">  Addr: 0x" + String(address, HEX) +
                         " Data: 0x" + String(data, HEX) + " [OK]");
          // Print the line describing what is being entered where.


          mem_array[address] = data;
          // actually enter it.
        }
      } while (input_string != "exit");
      // if the user types "exit" stop repeating the loop.
    }
    //m_edit edits the memory array directly, allowing the user
    // to deposit a hex value in 0x notation at a hex address,
    // also in 0x notation. It allows the user to dump the
    // array to the screen at will, and exits on the exit
    //command.
};


//-----------------------------------------------------------
// Global variables
// The big advantage to using C++ objects in this project
// was the drastic reduction of global variables. There
// are still quite a few variables, but most of them are
// contained in functions or in classes and don't exist
// until an object of that class is instantiated.
//-----------------------------------------------------------
clock_gen* ourclock = NULL; // pointer to a clock_gen object.
memory_simulator* mem_sim = NULL; // pointer to memory object.
cpu Z80; // declare our CPU object. Not a pointer.


//-----------------------------------------------------------
// Functions which are not members of a class.
//-----------------------------------------------------------


//-----------------------------------------------------------
// Repeat(int number char character)
// This function creates a string of character
// exactly /number/ long.
//-----------------------------------------------------------
String repeat(int number, char character) {
  String temp = "";
  for (int c = 0; c < number; c++) {
    temp = temp + String(character);
  }
  return temp;
}
//-----------------------------------------------------------
// get_input_string()
// This function loops forever waiting for an input string
// and returns the string when it gets one.
//-----------------------------------------------------------
String get_input_string() {
  while (!Serial.available()) {
  }
  return (Serial.readString());
}


//-----------------------------------------------------------
// string2uint32_t()
//- The String class includes no nice way to turn strings of
// digits into a numeric value. (Actually, there's an
// undocumented one. Classy.)
//
// This function goes through the string from left to right.
// For each position it advances to the right, it multiplies
// the existing value by 10 and adds the value of the
// character at that position minus the value of the
// character '0'. When we reach the end of the string,
// return temp.
//-----------------------------------------------------------
uint32_t string2uint32_t(String input) {
  uint32_t temp = 0;
  input.trim();
  for (int c = 0; c < input.length(); c++) {
    temp = temp * 10 + input.charAt(c) - '0';
  }
  return temp;
}


//-----------------------------------------------------------
// hex_string2uint16_t()
// This function goes through the string from left to right.
// For each position it advances to the right, it multiplies
// the existing value by 16. if the new digit is 0-9 (less
// than the value of ':') add the character's numeric value
// minus the value of '0',  A-F and a-f are processed the
// same way, except that we check to make sure the new
// digit is actually in the range and subtract a different
// value.  Once we have the correct value for the character,
// we add the value of the character at that position minus
// the value of the character '0'. When we reach the end
// of the string, return temp.
//-----------------------------------------------------------
uint16_t hex_string2uint16_t(String input) {
  uint16_t temp = 0;
  char tempchar;
  input.trim();
  for (int c = 2; c < input.length(); c++) {
    temp = (temp * 0x10);
    tempchar = input.charAt(c);
    if (tempchar < ':') temp += (tempchar - '0');
    if ((tempchar > '@') && (tempchar < 'G')) temp += (tempchar - '7');
    if ((tempchar > '`') && (tempchar < 'g')) temp += (tempchar - 'W');
  }
  return temp;
}


//-----------------------------------------------------------
// menu
// This function generates the menu and selects which function
// to call, whether it's in an object or not.
//-----------------------------------------------------------
void menu() {
  String input_string = "";
  Serial.println(" " + repeat(15, ' ') + "*** Menu ***");
  Serial.println(repeat(42, '-'));
  Serial.println("Z80 Operations Commands");
  Serial.println(repeat(42, '-'));
  Serial.println("(1) Reset Z80");
  Serial.println("(2) Set Clock Speed");
  Serial.println("(3) Stop Clock");
  Serial.println(repeat(42, '-'));
  Serial.println("Free Run ISR Commands");
  Serial.println(repeat(42, '-'));
  Serial.println("(4) Attach Free Run ISR to INT1");
  Serial.println("(5) Detach Free Run ISR from INT1");
  Serial.println(repeat(42, '-'));
  Serial.println("Memory Simulation and Z80 Programming");
  Serial.println(repeat(42, '-'));
  Serial.println("(6) Initialize Simulated Memory");
  Serial.println("(7) Enter Program Into Simulated Memory");
  Serial.println("(8) Run Program (Attach Mem_Sim ISRs)");
  Serial.println("(9) Dump Simulated Memory");
  Serial.println(repeat(42, '-'));
  // print the text of the menu.


  Serial.print("Clock is ");
  if (ourclock != NULL) {
    Serial.println("Running.");
  } else {
    Serial.println("Stopped.");
  }
  Serial.print("Memory is ");
  if (mem_sim != NULL) {
    Serial.println("Available.");
  } else {
    Serial.println("Not Available.");
  }
  Serial.println(repeat(42, '-'));
  // check to see if there's an object attached to ourclock. If not,
  // then the clock is not enabled. Tell the user one way or the other.
  // Likewise, if there's an object attached to mem_sim, memory is
  // enabled. Otherwise it's not. Tell the user.


  switch ((int)string2uint32_t(get_input_string())) {
    case 1: Z80.reset();
      break;
    // Option 1 is reset. Call the reset member function of Z80.


    case 2:
      Serial.println("Menu: Enter Z80 Clock Speed (1 - 20000000Hz)");
      input_string = get_input_string();
      delete ourclock;
      ourclock = NULL; // clear the pointer. Otherwise things get confused.
      ourclock = new clock_gen(string2uint32_t(input_string));
      mem_sim->m_write_enable=false;
      Z80.reset();
      mem_sim->m_write_enable=true;
      break;
    // Option 2 is start the clock/set the clock. Call input_string()
    // and stash the results in a variable by the same name.
    // Delete anything that's already on the ourclock pointer
    // and set it to null.
    // Do the delete so as not waste system resources. Do the
    // set to null so as not to get weird clock behavior.
    // Then instantiate a clock_gen object. Take input_string and
    // feed it to string2uint32_t, and feed the output of /that/
    // to the clock_gen's constructor, and let it do all the work.


    case 3:
      delete ourclock;
      ourclock = NULL; // Clear the pointer, otherwise things get confused.
      break;
    // Option 3 stops the clock. To do this, delete the object pointed to
    // by ourclock, and set ourclock to NULL so the next clock doesn't have
    // weird behavior.
    case 4:
      Serial.println("Menu: Attaching free_run_ISR to INT1");
      noInterrupts();
      detachInterrupt(1);
      attachInterrupt(1, free_run_ISR, FALLING);
      interrupts();
      Z80.reset();
      break;
    // Option 4 hooks up the free running ISR to interrupt 1, which listens
    // for /mreq events. Every time the Z80 asks for memory and pulls
    // this signal low, our ISR will fire. Free-running means we always give
    // the Z80 NOP instructions (do nothing, go on to the next address),
    // so we can watch and see if the address signals change. Turn
    // interrupts off, attach the ISR to interrupt zero on the falling
    // edge (/MREQ is active low), then turn interrupts back on.
    // Finally, reset the Z80 so it starts from address 0x0000 in
    // the output.


    case 5:
      Serial.println("Menu: Detatching free_run_ISR from INT1");
      noInterrupts();
      detachInterrupt(1);
      interrupts();
      Z80.reset();
      break;
    // You know how the last option attached the free_run_ISR?
    // Option 5 detatches it. Turn interrupts off, detatch
    // interrupt 1, turn interrupts back on, then reset
    // the Z80 on general principles.


    case 6:
      delete mem_sim;
      mem_sim = NULL;
      mem_sim = new memory_simulator();
      break;
    // Option 6 turns on simulated RAM for the Z80. Delete anything
    // on the mem_sim pointer, and set the pointer to NULL to avoid
    // memory strangeness. Then instantiate a memory_simulator
    // object and attach it to the mem_sim pointer. Memory_simulator
    // objects' constructor takes no parameters.


    case 7:
      mem_sim->m_edit();
      break;
    // Option 7 is enter a program into simulated memory.
    // call the m_edit member function of the memory simulator.
    // This lets the user put simple, hand-assembled programs
    // into the simulated memory for the Z80 to run. It's
    // slightly less tedious than doing it with toggle switches
    // on a front panel.(but only slightly).


    case 8:
      Serial.println("Menu: Attaching mem_read_ISR to INT1.");
      Serial.println("Menu: Attaching mem_write_ISR to INT0.");
      Serial.println("Menu: Any program therein should run.");
      noInterrupts();
      detachInterrupt(1);
      detachInterrupt(0);
      mem_sim->m_write_enable=false;
      attachInterrupt(1, mem_read_ISR, FALLING);
      attachInterrupt(0, mem_write_ISR, FALLING);
      mem_sim->m_write_enable=true;
      interrupts();
      Z80.reset();
      break;
    // Just as option 4 attached the free running ISR to interrupt 1,
    // option 8 connects the memory simulator ISR to interrupt 1.
    // This means that when the Z80 lowers its /MREQ signal and
    // requests memory, our ISR will try to service it with calls
    // to the memory_simulator object connected to mem_sim. Why
    // isn't the ISR in the object?  Because the Arduino core
    // won't let you. Same as with option 4. Stop interrupts, detatch
    // anything already connected to interrupt 1, attach mem_sim_ISR
    // to interrupt 1 on the falling edge, then turn interrupts back on
    // and reset the Z80 so our output starts at 0x0000.
    // NOTE - CHNAGED FROM FALLING TO LOW


    case 9:
      mem_sim->m_dump();
      break;
      // Option 9 dumps the simulated memory array to the serial
      // console, one 256 byte page at a time. Which gets tedious
      // going through 8 kilobytes, but it gets there. We just call
      // the m_dump() function of the memory_simulator object
      // connected to the mem_sim pointer.
      // Such a tangled web we weave.
  } // end of case.
} // end of menu function.


//-----------------------------------------------------------
// free_run_ISR()
// This function is an interrupt service routine for
// INT1. When INT1 fires, we're in a read cycle.
// This ISR prints out the 16 bit address
// requested by the Z80, and returns a 0x00 (NOP) to the
// Z80, telling it to do nothing and go the next address,
// allowing us to observe the address lines (and make sure
// they all work and are connected correctly.
//-----------------------------------------------------------
void free_run_ISR() {
  byte temp;
  String output = "";
  Z80.save_mode();
  // Save the control signals we're sending to the Z80.


  Z80.mode_wait();
  // Set the Z80's mode to wait, so it stops asking for
  // new addresses while the ISR is trying to service
  // this request.


  Z80.data_in(0x0);
  // Always send the Z80 a NOP (0x0).


  output += String("free_run_ISR: Address: 0x");
  if (Z80.addr_msb < 0x10) output += String("0");
  output = output + String(Z80.addr_msb, HEX);
  if (Z80.addr_lsb < 0x10) output += String("0");
  output += String(Z80.addr_lsb, HEX);
  Serial.println(output);
  // Build the output string to show the user
  // what address was requested. This is what
  // free running is for.
  Z80.restore_mode();
  // un-wait the Z80.
}
//-----------------------------------------------------------
// mem_read_ISR()
// This function is an interrupt service routine for
// INT1. Like free_run_ISR, the first thing it does
// is save the Z80 control signal state, then set the Z80
// into wait mode, so we can survice this memory request
// before the Z80 asks for the next one. After that, it sends
// data FROM the memory simulator TO the Z80 (on read)
// After that, it builds up a string to tell the user what
// just happened and prints it.
//-----------------------------------------------------------
void mem_read_ISR() {
  uint8_t tempdata = 0;
  String output = "";


  Z80.save_mode();
  // Save the Z80 control signal state.


  Z80.mode_wait();
  // Put the Z80 into wait mode.


  tempdata = mem_sim->m_seek_read(Z80.addr_msb, Z80.addr_lsb);

  if (!Z80.M1()) {
    output += String("mem_read_ISR: Z80 Fetched Address: 0x");
  } else {
    output += String("mem_read_ISR: Z80 Read Address: 0x");
  }
  Z80.data_in(tempdata);
  // On a read cycle (from the Z80's perspective)
  // tell the object pointed to by mem_sim to seek the address
  // present on the Z80's address bus, and send the data
  // stored there to the Z80's data bus.
  // Tell the user that the Z80 read the address.


  if (Z80.addr_msb < 0x10) output += String("0");
  output += String(Z80.addr_msb, HEX);
  if (Z80.addr_lsb < 0x10) output += String("0");
  output += String(Z80.addr_lsb, HEX);
  output += String(" Data: 0x");
  if (tempdata < 0x10) output += String("0");
  output += String(tempdata, HEX);
  Serial.println(output);
  // Build the rest of the output string and display it. /M1
  // is the Z80's signal to indicate it's doing an instruction
  // fetch. If it is, tell the user so.


  Z80.restore_mode();
  //Restore the Z80 to non-halted mode.
}
//-----------------------------------------------------------
// mem_write_ISR()
// This function is an interrupt service routine for INT0.
// like mem_read_ISR(), the first thing it does is
// is save the Z80 control signal state, then set the Z80
// into wait mode, so we can survice this memory request
// before the Z80 asks for the next one. After that, it sends
// data FROM the Z80 TO the memory simulator.
// After that, it builds up a string to tell the user what
// just happened and prints it.
//-----------------------------------------------------------
void mem_write_ISR() {
  String output = "";
  uint8_t tempdata = 0;


  Z80.save_mode();
  // Save the Z80 control signal state.


  Z80.mode_wait();
  // Put the Z80 into wait mode.


  tempdata = Z80.data_out();
  mem_sim->m_seek_write(Z80.addr_msb, Z80.addr_lsb, tempdata);
  output += String("mem_write_ISR: Z80 Wrote Address: 0x");
  // On write cycle (from the Z80's perspective)
  // tell the memory simulator object to seek the address
  // present on the Z80's address bus, and set that address
  // of the memory simulator TO the value on the Z80's
  // data bus.


  if (Z80.addr_msb < 0x10) output += String("0");
  output += String(Z80.addr_msb, HEX);
  if (Z80.addr_lsb < 0x10) output += String("0");
  output += String(Z80.addr_lsb, HEX);
  output += String(" Data: 0x");
  if (tempdata < 0x10) output += String("0");
  output += String(tempdata, HEX);
  Serial.println(output);
  //Build the rest of the output string and display it.


  Z80.restore_mode();
  //Restore the Z80 to non-halted mode.
}
//-----------------------------------------------------------
// Setup
// Sets the serial console speed.
//-----------------------------------------------------------
void setup() {
  Serial.begin(115200);
}


//-----------------------------------------------------------
// Loop
// Calls the menu() function. Over and over again.
//-----------------------------------------------------------
void loop() {
  menu();
}

Output

So what happens when you run the sketch? Here’s a log file where I reset the Z80, set the clock, delete the clock, set the clock again, and run the free_run_ISR. This log tells me my Z80 explorer is working correctly. Here’s the log file.

               *** Menu ***
------------------------------------------
Z80 Operations Commands
------------------------------------------
(1) Reset Z80
(2) Set Clock Speed
(3) Stop Clock
------------------------------------------
Free Run ISR Commands
------------------------------------------
(4) Attach Free Run ISR to INT1
(5) Detach Free Run ISR from INT1
------------------------------------------
Memory Simulation and Z80 Programming
------------------------------------------
(6) Initialize Simulated Memory
(7) Enter Program Into Simulated Memory
(8) Run Program (Attach Mem_Sim ISRs)
(9) Dump Simulated Memory
------------------------------------------
Clock is Stopped.
Memory is Not Available.
------------------------------------------

I selected option 1.

CPU: Resetting...
CPU: Done Resetting.

The menu printed again, and I selected option 2. I chose a clock speed of 5Hz.

Menu: Enter Z80 Clock Speed (1 - 20000000Hz)
Clock: Object Created.
We want to count to 4000000.00
For a clock speed of 5
I chose a prescaler of 256
And a match of 15625
We'll count to 4000000
Clock Generator: Running
CPU: Resetting...
CPU: Done Resetting.

The menu printed again. Note the change where it says Clock is Running. I selected option 3, to stop the clock.

               *** Menu ***
[most of menu cut out.]
------------------------------------------
Clock is Running.
Memory is Not Available.
------------------------------------------


Clock Generator: Object Deleted.

The menu printed out again. I selected option 2 and asked for a 250Hz clock. This is about as fast as the terminal monitor can keep up with. It’s far too fast to read, but a full free run spans 65,535 addresses. It takes some time. Even the slowest Z80s normally ran 10,000 times as fast, but they weren’t waiting for a printout at 115,200 baud between every M1 cycle.

               *** Menu ***
[most of menu cut out]
------------------------------------------
Clock is Stopped.
Memory is Not Available.
------------------------------------------

I selected option 2.

Menu: Enter Z80 Clock Speed (1 - 20000000Hz)
Clock: Object Created.
We want to count to 80000.00
For a clock speed of 250
I chose a prescaler of 64
And a match of 1250
We'll count to 80000
Clock Generator: Running
CPU: Resetting...
free_run_ISR: Address: 0x0c0a
CPU: Done Resetting.

The menu printed again. I selected option 4, Attach Free Run ISR. The serial monitor got very, very busy.

               *** Menu ***                                                                              
[most of menu cut out]
------------------------------------------
Clock is Running.
Memory is Not Available.
------------------------------------------


Menu: Attaching free_run_ISR to INT1
CPU: Resetting...
CPU: Done Resetting.

The menu printed again. Why? We’re actually still in the loop() function, so menu gets called and will wait forever for input. All the Serial.printlns are coming from free_run_ISR.

               *** Menu ***
------------------------------------------
Z80 Operations Commands
------------------------------------------
(1) Reset Z80
(2) Set Clock Speed
(3) Stop Clock
------------------------------------------
Free Run ISR Commands
------------------------------------------
(4) Attach Free Run ISR to INT1
(5) Detach Free Run ISR from INT1
------------------------------------------
Memory Simulation and Z80 Programming
------------------------------------------
(6) Initialize Simulated Memory
(7) Enter Program Into Simulated Memory
(8) Run Program (Attach Mem_Sim ISRs)
(9) Dump Simulated Memory
------------------------------------------
Clock is Running.
Memory is Not Available.
------------------------------------------


free_run_ISR: Address: 0x0001
free_run_ISR: Address: 0x0002
free_run_ISR: Address: 0x0003
free_run_ISR: Address: 0x0004
free_run_ISR: Address: 0x0005
free_run_ISR: Address: 0x0006
free_run_ISR: Address: 0x0007
free_run_ISR: Address: 0x0008
free_run_ISR: Address: 0x0009
free_run_ISR: Address: 0x000a
free_run_ISR: Address: 0x000b
free_run_ISR: Address: 0x000c
free_run_ISR: Address: 0x000d
free_run_ISR: Address: 0x000e
free_run_ISR: Address: 0x000f

That’s the first 16 addresses. You’ll note as you get above 0x00ff that every address whose MSB is odd (so there’s a 1 in its lowest bit) will light up the pin 1 LED. This is a good sign that things are working. At 250Hz, it will pulse on and off a little over a second at a time as we run through the whole range of addresses with an odd-numbered MSB. Here are a couple of the addresses where that occurs.

free_run_ISR: Address: 0x0500
free_run_ISR: Address: 0x0501

On and on it goes. It gets a little dull to watch if everything’s working right. Finally, after many thousands of addresses I cut out for brevity:

free_run_ISR: Address: 0xfffd
free_run_ISR: Address: 0xfffe
free_run_ISR: Address: 0xffff

After that, I sent the Cestino a 3, for option 3. Even though the menu had long since scrolled off the screen, deleting the clock object stopped the torrent of addresses.

If you got this far, and your output from the free_run_ISR looks more or less like mine, your Z80 explorer is probably working correctly. We’ll learn to program the Z80 in the next section.

Assembly and Machine Language

There are essentially three types of operation the Z80 knows how to do. Copy data from one place to another, change the control flow of the program, and math/logic functions . When you expand all the permutations of all its instruction families (we’ve only seen the ALU instructions so far) there are hundreds and we’re not going to cover them all. It’s easier to try and understand the kind of instructions you have available.

This isn’t C++. What you have is empty RAM and tools for modifying what’s in it, mostly one byte at a time. From the discussion of the ALU and its instructions back in Z80 Microprocessor Anatomy, you know that the Z80 knows how to add and subtract, rotate left and right, AND, OR, XOR, and how to check individual bits in bytes. Any higher level mathematical instructions have to be built by the programmer.

The rest of the instruction set is the same way. You can LD a byte from a memory location pointed to by a register, a byte in a register, or a literal value passed as a parameter to a different memory location pointed to by a register, or a different register.

There are no complex data structures, either. Machine instructions know nothing about strings or characters or uint8_ts. They know registers, flags in the F register, literal values, bits, and pointers to memory. That’s it.

You also have to understand memory. Some would say that to understand assembly or machine instructions, you must understand memory first. Thankfully, the Z80’s memory map is very simple, at least the way we’re using it. Addresses start at zero and run to the end of memory (8192 bytes, in our case, but it can go to 65,535 bytes, or 64k). The first 256 bytes may or may not have special functions (interrupt handlers may go there, if you’re using them, and so on.) and somewhere you’ll have a stack, pointed to by the SP (stack pointer) register, and it will grow backwards from high values of memory to lower ones. Nothing protects one memory function from another. If you tell the Z80 to push so much stuff to the stack that it clobbers your code? It clobbers your code. If you give it an address to write to that is in the middle of the stack or your code, guess what. It clobbers the stack or your code. Working at this level gives you a lot of power, and as much speed as the CPU can give you, but the cost is knowing for sure what the program will do at every phase of its operation. There are no safety nets.

By contrast, resetting the Z80 takes seconds (five in our case, although most Z80 systems reset for a few milliseconds). So the price of a bug isn’t that high.

I’ve used the word instructions a lot. I should mention what it means, and what assembly language and machine language are.

An instruction is a command. You’ve seen that they can be one or more bytes. They can take parameters, in the form of literal values or pointers to other parts of memory. They cause the Z80 to do something specific.

Machine language is a series of instructions and their parameters (and data) in memory. Machine language is the raw bytes, in hex or binary (we’ll use hex.) Assembly language is code that you feed to an assembler to get machine language. Traditionally, every assembly mnemonic, like NOP, LD, or ADD has machine code associated with it, and you can translate literally from assembly to machine code and from machine code back into assembly.

The truth is that assemblers also have what are called macros. If you want to use an area of RAM as a variable, you can set a macro to do that, and every time you touch that area of ram in your assembly code, the assembler will generate the instructions needed to access it. It’s a fuzzy line between a sophisticated assembler and a compiler.

What we’ll be doing is called hand assembly. We’ll be looking up the instructions, putting the opcodes together so we get the right version of the instruction, doing the translation of instructions and addresses and everything into hex codes, which we will put in memory by hand.

You’ve seen videos of the old days when people toggled programs into early micro-computers and read the results out on arrays of LEDs. This is exactly what they were doing. Once you understand how to write machine language, assembly language is a lot more clear.

Program 1: Infinite Loop

The first machine language program we’ll write is an infinite loop . Anyone who’s ever messed with a computer with a BASIC interpreter has done this:

10 print "Hello world"
20 goto 10

We’re not even going to do the printing part. We don’t have any hardware to do the job, and we don’t need it. The mem_read_ISR will show us the loop as it happens by showing us the changes in address.

The infinite loop is one instruction with a two byte memory address for a parameter. Here’s the assembly version, with the mnemonics.

JP 0x0001

Traditionally, in an assembler, we’d put a semicolon after that to explain what we were doing and why. We’re hand assembling, so that winds up in your notebook instead.

The opcode for the version of JP that goes to a literal address is 0xC3. Because our Z80 is little endian, we have to put the least significant byte of the address we want to go to first, then the most significant byte. We also need to write down what address in memory each instruction will be on, both so we know where to put it, and so, when we’re writing the code, we know what addresses to jump to.

What we wind up with is this:

0x01 0xc3
0x02 0x01
0x03 0x00

Moving forward, I’ll put the two representations together. (That’s how they are in my notes, too.)

       JP 0x0001
0x01 0xC3
0x02 0x01
0x03 0x00

Go ahead and start up the Z80 Explorer, and get to the menu. Mac users, you may have to reset the Cestino manually when you start the serial monitor.

First, Initialize Simulated Memory, option 6. You’ll remember from the sketch that this instantiates the memory simulator object and points the mem_sim pointer at it. We’re at a point in the game where that no longer matters. We need to focus on the Z80.

Next, select option 7, Enter Program Into Simulated Memory.

Type the address and opcode pairs above.

Here’s what it looks like on my screen:

        *** Mem-Sim Line Editor ***
Enter an address and data in HEX eg: 0x0000,0x76
"exit" to quit
"dump" to view memory


>  Addr: 0x1 Data: 0xC3 [OK]
>  Addr: 0x2 Data: 0x1 [OK]
>  Addr: 0x3 Data: 0x0 [OK]

Typing “dump,” I get:

Dumping Simulated Memory

Address  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f   Data (text)
---------------------------------------------------------------------------
0x0000| 00 c3 01 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x0010| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x0020| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x0030| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x0040| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x0050| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x0060| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x0070| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x0080| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x0090| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x00a0| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x00b0| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x00c0| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x00d0| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x00e0| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x00f0| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
Continue (y/n)

It looks a little strange with nothing in the human-readable part of the field, but if you look at the hex dump, you can see that, in fact, everything is fine, and our three-byte program is there.

Go ahead and answer “n” to exit from the editor, and type “exit”.

Then set the clock to about 10Hz, and attach the Memory Simulator ISRs (options 2 and 8, respectively.)

You should get output from the mem_read_ISR that looks like this:

mem_read_ISR: Z80 Fetched Address: 0x0000       Data: 0x00
mem_read_ISR: Z80 Fetched Address: 0x0001       Data: 0xc3
mem_read_ISR: Z80 Read Address: 0x0002          Data: 0x01
mem_read_ISR: Z80 Read Address: 0x0003          Data: 0x00
mem_read_ISR: Z80 Fetched Address: 0x0001       Data: 0xc3
mem_read_ISR: Z80 Read Address: 0x0002          Data: 0x01
mem_read_ISR: Z80 Read Address: 0x0003          Data: 0x00
mem_read_ISR: Z80 Fetched Address: 0x0001       Data: 0xc3
mem_read_ISR: Z80 Read Address: 0x0002          Data: 0x01
mem_read_ISR: Z80 Read Address: 0x0003          Data: 0x00
Clock Generator: Object Deleted.

Note that because memory address 0x0000 has nothing in it, it’s a NOP. We’ll read it and go on to the next address. We fetch again from 0x0001, and get the instruction 0xC3. That’s our JP instruction.

Next we read two more addresses. Note that they’re not instruction fetches. We’re not in an M1 cycle, so the /M1 signal is not active (low). It’s reading those two bytes in 0x0002 and 0x0003 as parameters for the JP, which is exactly what it should do. Look what happens next.

All of a sudden, we’re back fetching at address 0x0001. The JP worked! I let it run for three more trips through the loop and hit option 3, Stop Clock.

The menu was scrolled off the serial monitor, but loop calls menu() every time we exit menu(), and we only exit menu when we’ve given it an option number. So menu is sitting there waiting for input the whole time while we watch our Z80 program loop endlessly from address 0x0001 to 0x003, over and over again.

Program 2: Index Controlled Loop

Infinite loops are fun for demonstrations, but they’re not the most useful of programs. For the next machine language program, we’ll write a loop controlled by an index. This is more or less the same as:.

for (PORTB = 5; PORTB == 0; PORTB--) {
    //do nothing.
  }

You could use an int, of course, and declare it right in the loop. We’ve done it lots of times, except that I sneakily decrement using PORTB--. In this case, however, using the ATmega’s PORTB register to do the job is much closer to how the loop works in machine language.

Here’s the code

        LD B,0x05 ; load register B with the value 0x05.
0x01 0x06
0x02 0x05
        DEC B; Decrement the B register
0x03 0x05
        JP Z 0x0A; Jump to 0x0A when B hits 0 and the Z flag is set.
0x04 0xCA
0x05 0x0A.
0x06 0x00
        JP 0x03 ; If we get here, we're not done. Jump to 0x03
0x07 0xC3
0x08 0x03
0x09 0x00
        HLT ; If we get here, halt the CPU.
0x0A 0x76

All that just for a loop? Yes. We’re indexing this loop on the B register, as it’s a general purpose register. We set it to 5 in the first instruction.

LD B, 0x05.

In the next instruction we decrement B. This works, but doing it here before the test means the loop will actually only run four times. Oops.

DEC B

The next instruction is the test. You’ll find a lot of loops run backwards in assembly and machine code. It’s much simpler and much faster to decrement and check a flag than to add and compare. The DEC instruction sets the Z flag automatically if the value it decremented reaches zero. So all we have to do is test to see if the Z flag is set, and if it is, jump out of the loop. From writing out all the bytes in my notes, I know that to escape the loop, I have to go to 0x0A, so that’s where the JP Z takes us. Assemblers figure this kind of thing out for you automatically.

JP Z 0x0A

If we haven’t jumped out of the loop, we go ahead and loop with a JP call, just like we did in the infinite loop.

JP 0x03

If we get to this point, then we’ve jumped out of the loop in the JP Z instruction earlier. So we go ahead and halt the Z80, which tells it to stop running the program.

HLT

Go ahead and stop the clock on the Z80 explorer (option 3) and initialize simulated memory again (option 6), then enter the ops above. When they’re in and they look right, you should get output that looks like this:

        *** Mem-Sim Line Editor ***
Enter an address and data in HEX eg: 0x0000,0x76
"exit" to quit
"dump" to view memory.


>  Addr: 0x1 Data: 0x6 [OK]
>  Addr: 0x2 Data: 0x5 [OK]
>  Addr: 0x3 Data: 0x5 [OK]
>  Addr: 0x4 Data: 0xca [OK]
>  Addr: 0x5 Data: 0xa [OK]
>  Addr: 0x6 Data: 0x0 [OK]
>  Addr: 0x7 Data: 0xc3 [OK]
>  Addr: 0x8 Data: 0x3 [OK]
>  Addr: 0x9 Data: 0x0 [OK]
>  Addr: 0xa Data: 0x76 [OK]

Typing “dump” I get:

Dumping Simulated Memory

Address  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f   Data (text)
---------------------------------------------------------------------------
0x0000| 00 06 05 05 ca 0a 00 c3 03 00 76 00 00 00 00 00  | ..........v.....
0x0010| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x0020| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x0030| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x0040| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x0050| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x0060| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x0070| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x0080| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x0090| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x00a0| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x00b0| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x00c0| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x00d0| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x00e0| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x00f0| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
Continue (y/n).

Looks good. Type n to quit, then go ahead and restart the clock. This will reset the Z80, but it won’t erase memory. Set the clock to about 20Hz. When the reset finishes, you’ll get output like this:

mem_read_ISR: Z80 Fetched Address: 0x0000       Data: 0x00
mem_read_ISR: Z80 Fetched Address: 0x0001       Data: 0x06
mem_read_ISR: Z80 Read Address: 0x0002          Data: 0x05
mem_read_ISR: Z80 Fetched Address: 0x0003       Data: 0x05
mem_read_ISR: Z80 Fetched Address: 0x0004       Data: 0xca
mem_read_ISR: Z80 Read Address: 0x0005          Data: 0x0a
mem_read_ISR: Z80 Read Address: 0x0006          Data: 0x00
mem_read_ISR: Z80 Fetched Address: 0x0007       Data: 0xc3
mem_read_ISR: Z80 Read Address: 0x0008          Data: 0x03
mem_read_ISR: Z80 Read Address: 0x0009          Data: 0x00

Well, B is not zero yet, so we’ve looped back to 0x0003. I’ve cut out three more iterations of this loop in the log.

mem_read_ISR: Z80 Fetched Address: 0x0003       Data: 0x05
mem_read_ISR: Z80 Fetched Address: 0x0004       Data: 0xca
mem_read_ISR: Z80 Read Address: 0x0005          Data: 0x0a
mem_read_ISR: Z80 Read Address: 0x0006          Data: 0x00
mem_read_ISR: Z80 Fetched Address: 0x000a       Data: 0x76
mem_read_ISR: Z80 Fetched Address: 0x000b       Data: 0x00
mem_read_ISR: Z80 Fetched Address: 0x000b       Data: 0x00
mem_read_ISR: Z80 Fetched Address: 0x000b       Data: 0x00
Clock Generator: Object Deleted.

Hey look. All of a sudden we don’t get past 0x0006. That’s where the second byte of the JP Z 0x000A instruction was. B must have hit 0, and the Zero flag got set. We jumped to 0x000A. Once at 0x000A, we picked up a HLT instruction. After that we stop advancing through memory addresses (at 0x000b) and sit there forever. I stopped the clock after that..

Program 3: Interrupts

Way back at the beginning of this chapter, I mentioned that I’d show you exactly what interrupts do with the Z80. This next program does that. We’ll create essentially two machine language programs. One is a loop, and the other is an interrupt handler, which is essentially an ISR.

The two programs aren’t connected together in any other way. If the /INT line on the Z80 never goes low, the interrupt handler is never executed.

There’s a little bit of setup. Remember when we built the Z80 Explorer, I mentioned that like the ATmega, the Z80’s interrupts are disabled by default. If you want to verify that, run the infinite loop again and press the button a few times. Nothing happens.

So the first instruction we’ll give the Z80 is the Enable Interrupts instruction.

EI ; Enable interrupts

The Z80, unlike the 8080 from which it was copied, has three different interrupt modes. Mode zero behaves exactly the way the 8080’s interrupts did. Complicated and hard to use. Mode two lets us pass a numeric value in on the data bus to tell the Z80 what the interrupt vector should be. We could do that, but the way the Z80 explorer is wired, the ATmega is completely unaware of the Z80’s interrupt signal. We’ll use mode 1.

When a mode 1 interrupt is triggered, two things happen. First, the PC (program counter) register is pushed onto the stack. Then the Program Counter is set to 0x0038, and we execute code from there on.

0x0038 is our interrupt vector. Keep this in mind.

Once our interrupt handler (ISR) executes a RETI instruction, the Z80 pops the old value of PC back off the stack into PC, and we resume executing code from where we left off when the interrupt was called .

Which in the case of this program is another infinite loop, a JP command that uses its own address as the target.

You’ll see every step of this happen.

Here’s the code:

        EI; Enable Interrupts
0x10 0xFB
        IM1; Set interrupts to mode 1
0x11 0xED
0x12 0x56

There’s no direct way to load the stack pointer with an address. Instead, we’ll load the 16 bit register pair HL with the address of the top of memory (0x2000), and set the stack pointer from that.

        LD HL 0x2000; Load the HL register with the highest memory address.
0x13 0x21
0x14 0x00
0x15 0x20
        LD SP HL; Load the stack pointer from HL.
0x16 0xF9

Here’s the infinite loop.

        JP 0x17; Loop infinitely
0x17 0xC3
0x18 0x17
0x19 0x00

That ends the main section of this program. Next, we have the interrupt handler. We start by disabling interrupts. If we don’t, another interrupt can interrupt our interrupt handler, which means that the value of PC stored on the stack may be wrong when we go to return.

Normally, you’d put the body of your interrupt handler between the DI and EI instructions. Since our handler doesn’t do anything, there’s nothing there. EI reenables interrupts.

        DI; Disable interrupts
0x38 0xF3
        EI; Enable Interrupts
0x39 0xFB

Finally, we return from the interrupt handler. This pops the old value of PC off the stack back into the PC, and sends the Z80 on its merry way executing our infinite loop. RETI is a two-byte instruction.

        RETI; Return from interrupt
0x3A 0xED
0x3B 0x4D

Go ahead and enter the opcodes for this program into memory, after reinitializing simulated memory and stopping the clock, of course.

You should get output like this:

        *** Mem-Sim Line Editor ***
Enter an address and data in HEX eg: 0x0000,0x76
"exit" to quit
"dump" to view memory


>  Addr: 0x38 Data: 0xf3 [OK]
Dumping Simulated Memory


Address  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f   Data (text)
---------------------------------------------------------------------------
0x0000| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x0010| fb ed 56 21 00 20 f9 c3 17 00 00 00 00 00 00 00  | ..V!. ..........
0x0020| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x0030| 00 00 00 00 00 00 00 00 f3 fb ed 4d 00 00 00 00  | ...........M....
0x0040| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x0050| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x0060| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x0070| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x0080| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x0090| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x00a0| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x00b0| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x00c0| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x00d0| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x00e0| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
0x00f0| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  | ................
Continue (y/n)

Looks good. Let’s go ahead and run it. Make sure your memory simulator ISRs are hooked up and start the clock. Note that while I trimmed the log so it starts when we fetch at address 0x0010, the Z80 will start at 0x0000, as always. It will fetch 16 NOPs before it does anything. When I wrote this one, I hadn’t yet put the m_write_enable mechanism in place, and memory below 0x10 was sometimes getting messed up.

mem_read_ISR: Z80 Fetched Address: 0x0010       Data: 0xfb
mem_read_ISR: Z80 Fetched Address: 0x0011       Data: 0xed
mem_read_ISR: Z80 Fetched Address: 0x0012       Data: 0x56
mem_read_ISR: Z80 Fetched Address: 0x0013       Data: 0x21
mem_read_ISR: Z80 Read Address: 0x0014          Data: 0x00
mem_read_ISR: Z80 Read Address: 0x0015          Data: 0x020
mem_read_ISR: Z80 Fetched Address: 0x0016       Data: 0xf9
mem_read_ISR: Z80 Fetched Address: 0x0017       Data: 0xc3
mem_read_ISR: Z80 Read Address: 0x0018          Data: 0x17
mem_read_ISR: Z80 Read Address: 0x0019          Data: 0x00

Okay, we’ve entered our infinite loop.

mem_read_ISR: Z80 Fetched Address: 0x0017     Data: 0xc3
mem_read_ISR: Z80 Read Address: 0x0018        Data: 0x17
mem_read_ISR: Z80 Read Address: 0x0019        Data: 0x00

Here’s another cycle of it. There were quite a few more, but they are all the same. We pick the log up when I started holding down the /INT button. Note that we finish the entire instruction cycle for our JP before anything happens.

mem_read_ISR: Z80 Fetched Address: 0x0017     Data: 0xc3
mem_read_ISR: Z80 Read Address: 0x0018        Data: 0x17
mem_read_ISR: Z80 Read Address: 0x0019        Data: 0x00

Bang. We’re writing to the stack. Twice. That’s our program counter (PC register) getting saved.

mem_write_ISR: Z80 Wrote Address: 0x1fff      Data: 0x00
mem_write_ISR: Z80 Wrote Address: 0x1ffe      Data: 0x17

Having saved the program counter, we jump to 0x0038. No JP command required. It just happens.

mem_read_ISR: Z80 Fetched Address: 0x0038       Data: 0xf3
mem_read_ISR: Z80 Fetched Address: 0x0039       Data: 0xfb
mem_read_ISR: Z80 Fetched Address: 0x003a       Data: 0xed
mem_read_ISR: Z80 Fetched Address: 0x003b       Data: 0x4d

We’re finished executing our tiny interrupt handler. Now we pop the old PC value off the stack back into PC.

mem_read_ISR: Z80 Read Address: 0x1ffe       Data: 0x17
mem_read_ISR: Z80 Read Address: 0x1fff       Data: 0x00

And here we are, back at our infinite loop at 0x0017

mem_read_ISR: Z80 Fetched Address: 0x0017     Data: 0xc3
mem_read_ISR: Z80 Read Address: 0x0018        Data: 0x17
mem_read_ISR: Z80 Read Address: 0x0019        Data: 0x00

Infinite loops are boring once the novelty wears off, so I stopped the clock.

Clock Generator: Object Deleted.

Program 4: Fun with the Stack

In the last program we touched the stack only briefly. The stack is one of those functions that is incredibly useful in the Z80. It’s a staple of machine language (and assembly) programming, so we should talk about it a little more.

For those who haven’t had formal computer programming classes, stacks are a data structure best visualized by plate dispensers (seriously, that’s what they’re called), those gadgets in cafeterias that hold stacks of plates. You can put a plate on the top of the dispenser. In programming we’d call this pushing a plate. When you put a second plate on the top, the spring in the dispenser compresses so the plates sink in and only the topmost plate is visible. This goes on for dozens of plates. When you want a plate out, you take one from the top, the springs expands, and the next plate becomes available. In software, this is called popping a plate. Push the plate on, pop the plate off. You only have access to the top of the stack, and it’s the last plate you put in. Stacks are last-in, first-out (LIFO) data structures .

In the Z80, the stack is defined by the Stack Pointer (SP) register.

When you PUSH an 8 bit value onto the stack, the SP decrements and your value is put in memory at that address. When you push a 16 bit value onto the stack, the bytes are pushed onto the stack, but they’re pushed there in reverse order, so the MSB is at the next highest address, and the LSB is at the next highest after that. If you were to read memory in the normal order, however, you’d find that little endian-ness has been respected. It’s fairly arbitrary, since you normally take things off the stack with a POP, and POP will put the bytes in the order they were in when they were PUSHed.

POPping the stack copies the byte or bytes on the stack (a 16 bit push had better be followed by a 16 bit pop, or the stack will probably stop making sense).

If you’ve ever had programming teachers mention that “recursive programming is bad because you run out of stack space,” consider that if you wind up using a CALL and RET to call your function, and it calls itself again and again, every call pushes more stuff onto the stack. Eventually, yes, you do run out of stack space (or in the Z80, your stack clobbers something important). If this hasn’t happened to you, don’t worry. It’s an argument out of academic circles, mostly.

So now that we know what the stack is, and we’ve seen the mnemonics go by that make it work, let’s do one more machine language program that demonstrates the stack. It’ll put the stack pointer at 0x00FF, which is the end of the first 256 byte page of memory, so we can see what’s in the stack in dumps, and it will show the last in/first out nature of the stack to give you a message

The program is called Fun with the Stack.

Here’s the code. We start with an initialization section.

        LD HL 0x00FF; Set the HL register to 0x00FF.
0x10 0x21
0x11 0xFF
0x12 0x00
        LD SP HL; Load the SP from HL
0x13 0xF9

We’ll have data starting at 0x0040. We’ll use HL as a pointer to that address.

        LD HL 0x0040; Load HL with the address of the start of data.
0x14 0x21
0x15 0x40
0x16 0x00

There are two loops in this program, and we start the first one here, at 0x17. The first loop will load B with a byte of data, then copy that data to A, compare A with 0xFF to see if we’re done. If not we’ll add 16 to A and push the result onto the stack. That’s why the data doesn’t look like anything in the memory dump. Not only is it out of order, it’s slightly encrypted. Then we’ll increment HL and go back to 0x17.

        LD B, (HL); Load B with the contents of memory at address HL.
0x17 0x46
        LD A, B; Load the accumulator A with the value in B.
0x18 0x78
        CP 0xFF; Compare 0xFF to register A.
0x19 0xFE
0x1A 0XFF
        JP Z 0x0026; If the zero flag is set (A=0xFF) jump to 0x0026
0x1B 0xCA
0x1C 0x26
0x1D 0x00
        LD A, B; Copy B into A again, in case the accumulator changed.
0x1E 0x78
        ADD A 10;
0x1F 0xC6
0x20 0x10
        PUSH AF; Do a 16 bit push of AF, even though all we want is A.
0x21 0xF5
        INC HL; Increment HL
0x22 0x23
        JP 0x17; Jump to 0x0017 to repeat our loop.
0x23 0xC3
0x24 0x17
0x25 0x00

The first loop pushed our message onto the stack after decoding it. The second loop will pop it back off the stack into a new area of memory starting at 0x0050. We initialize the second loop by setting HL to 0x0050.

        LD HL 0x50; Load HL with 0x50, the start of our output area.
0x26 0x21
0x27 0x50
0x28 0x00

We’re into loop two, and already doing something a little underhanded. We know that we have less than 16 items of data, so we know that the low order byte of HL is the only one that will ever change. So we index the loop on L. When L hits 0x58, the maximum address we’ve allowed for output, the loop will end. Elegant programming this is not.

        LD A, L; Load A with the low order byte of HL.
0x29 0x7D
        CP 0x57 ;Compare 0x58 and Register A.
0x2A 0xFE
0x2B 0x58
        JP Z 0x35; If the Z flag is set, jump to 0x35.
0x2C 0xCA
0x2D 0x35
0x2E 0x00

If we get here, we haven’t escaped from our loop. So we go ahead and pop AF (16 bit pop), and write A to location HL in memory. Then increment HL and repeat.

        POP AF; Pop AF from the stack.
0x2F 0xF1
        LD(HL),A; Load memory location HL with the value of A.
0x30 0x77
        INC HL; Increment HL to the next memory location.
0x31 0x23
        JP 0x29; Jump to 0x0029 to repeat the loop.
0x32 0xC3
0x33 0x29
0x34 0x00
        HLT; Halt the CPU when we're done.
0x35 0x76

That’s all the code, but we still need the data. It’s short.

0x40 0x11
0x41 0x62
0x42 0x55
0x43 0x58
0x44 0x64
0x45 0x62
0x46 0x65
0x47 0x36
0x48 0xFF

Normally I’d put the log here, but that would spoil the secret message. You already know what will happen. Loop one will read the data from 0x40 to 0x48, add 10 to it, and push it on the stack. Loop two will iterate from 0x50 to 0x57, pop the data back off the stack and write it to that address of memory. Make sure to do a dump so you can see the secret message this program will leave in memory for you.

And enjoy.

Credit Where Credit Is Due

I must credit Sergey Malinov, who made the boards of the first computer that I built from scratch. At the time, his boards were part of the N8VEM project, which has since disbanded. His website is here: http://www.malinov.com/Home/sergeys-projects .

The Retrobrew community website, which sprang from the ashes of the N8VEM project , is slowly rebuilding, and has much of the old information here: http://retrobrewcomputers.org/doku.php .

Quinn Dunki’s Blondihacks site and the long running Veronica computer build was a huge inspiration. Her site on that computer is here: http://quinndunki.com/blondihacks/?page_id=1761 . Above all, it was her website that first made it clear to me how the basic memory fetch/instruction cycle worked, and demonstrated free running. It was such an effective demonstration, I had to include it.

Dave Jones’s EEV blog provided a lot of basic technological knowhow, and some attitude: http://www.eevblog.com/ . While there was no soldering in the projects in this book, I have found no better soldering tutorials than his.

I must also credit Jeff Duntemann, http://duntemann.com/ , for advice, knowhow, experience, good stories on the subject, encouragement, and an excellent book on assembly language. He was there when the Z80 was king. He’s also my closest friend and fellow science fiction author.

Finally, I’ll sneak in a credit for Marcia Bednarcyk. Nerd, software engineer, wonderful human being, and for more than 20 years, my wife.

Further!

We’ve come a long way. Starting with Arduino skills, we built the Cestino. We learned ports and 8 bit binary. We climbed the tree from transistors all the way to microprocessors, and now we’re writing hand-assembled machine code for a Z80 and watching it run on something we built from scratch.

Neat, huh?

So where do we go from here?

For the Z80 explorer, there are lots of answers. The first and most obvious would be: some way for the memory editor to save files so we don’t have to retype everything every time we reboot. Believe me, I thought longingly of that functionality while I was developing the programs in this section. The Atmega actually can use part of its flash to store external data.

It might be nice to build a proper assembler into the editor, and perhaps wrap the whole sketch into it with a command parser, and use that instead of the menu. Apple II did it that way. You could escape to a machine language monitor (which is what we’re talking about) if you knew the right keystrokes. No external software required. Sketches can always be improved.

Obviously, there are lots more assembly programs you could write for the Z80. A Z80 with 8k of RAM can do quite a lot. The original Microsoft Basic ran on an 8080 or a Z80 with 4k, and had a little space left to write and run programs; 8k would leave a lot more. Where you get that code and how you get it into the Z80 is an exercise I’ll leave to the reader.

But this book is about hardware, mostly. What could you do with the hardware of the Z80 explorer to make it better? First and foremost, add real RAM. You can get 512k of static ram on a single IC from Mouser and the like (Alliance AS6c4008, 4mega-bit 512k8 through-hole SRAM) for less than five dollars U.S. By now you can probably read the datasheet and figure out how to connect the Z80 to it. Having freed up two full ports on the Cestino (at the expense of dramatically complicating how you get data from the Cestino into memory) you could add an SD card on the SPI interface of the ATmega1284P for storage. (Bear in mind that SD cards are 3.3v animals. Adafruit has good information on this kind of thing here: https://learn.adafruit.com/adafruit-micro-sd-breakout-board-card-tutorial/look-out .

If you took the memory ISRs out of the picture, you could turn the clock up to the maximum your Z80 is rated for, write a few assembly language routines (an interrupt handler and a driver, basically) to let the Z80 communicate with the ATmega and tell it what to do.

At some point in that project, you might want to get away from breadboards and into a method of construction that is more mechanically stable. I can’t tell you the number of times a project just stopped working (usually as a deadline loomed) during the writing of this book, and it turned out that a wire was loose. The breadboards in the photos are inexpensive, and pretty badly worn, at this point. Breadboards have their limits electronically too, and as I said in the beginning, we’re pushing our luck a little bit running 20MHz signals through it. We get away with it, but it might not always be so. We might want to add a proper power supply at that point too. Our hotwired ATX power supply from the ATA explorer would do that nicely. A little tinkering and the hotwiring could be switched. Off switches are always nice.

So let’s see. More software, speed, real RAM, storage, we can use the ATmega for RS232 communications, power supply ... I don’t think we can call this the Z80 explorer anymore. I think it’s become a computer. Don’t believe me? Consider.

Given a series of assembly programs for accessing the peripherals in a standardized fashion called a Basic Input Output System. (Ever wonder what BIOS stands for and what it does? Now you know.) you could run CP/M on it, using your desktop for a terminal (which CP/M does well) which would open a whole universe of software (much of it approaching 40 years old and free for the asking.) This is what made the Z80 famous. It was easy (by comparison) to connect it to a small set of peripherals and come up with a computer that could do useful work. Actually doing it is way beyond the scope of this chapter. We’d have to cover construction, programming and get into assembly/machine language in all its depth. It’d be a book unto itself. Hmm.

What could you do with a Z80 computer and 40 year old software today? Well, a few years ago, I mail-ordered the boards for a Zeta computer from Sergei Malinov. You can find the newer version of those same boards here: http://www.malinov.com/Home/sergeys-projects/zeta-sbc-v2 , although whether he’s got any left at this point is unclear. The Zeta is essentially what we’ve been talking about: a Z80, some SRAM, some peripheral chips (it talks to 3.5 inch floppy drives) and an EEPROM with CP/M, the BIOS, and a bunch of applications on it.

The boards arrived in an envelope. With the Internet as my shopping mall, I sourced the parts, and over a period of a couple months (plus a few more to debug some construction errors on my part) I had a working Z80 computer. Since it was part of a larger family of computers, someone else had already written the BIOS for it. All I had to do is download the firmware and burn it to my EEPROM.

I mention this, because it is this computer and Wordstar, rather than my monster Mac, that I’m typing this last section on.

Once you know some electronics, you can learn more. We’ve made some amazing things out of junk, just for fun. We live in the best of times to learn electronics. Never has more information been available on the subject, much of it coming out from under patents, and most of it available online at the click of a mouse. Never before has the hobbyist electronics scene been so large and so well embraced by the mainstream. Never, ever, have the parts been so cheap. In 1980, when the Z80 was a front-line computer, 16k of ram cost over $300 in mighty 1980 dollars. Multiply by about four to get their equivalent price in 2016. If you fried one of those (early CMOS dynamic RAM ICs were horrifically sensitive to static), it hurt. Today, the parts are free if they’re in the junk box. Even if they’re not, they’re shockingly cheap new. It’s a good time to learn electronics for other reasons, too.

Although the industry strives to sell us magical and wonderful gadgets, more and more of us know, and rejoice in the knowledge, that there is no magic, just people with skills. Are electronics complicated? Sure. But look how far we’ve come just in this book. From one LED to the rudiments of a personal computer.

I’m not an engineer. I’m a former system-admin, technical support guy who normally writes science fiction. You’re holding in your hands 99.9 percent of all the assembly code I’ve ever written, and all the hand-assembled machine code. These projects, from building the Cestino to the EPROM/Flash Explorer, to (and most especially) the Z80 explorer, have been triumphs for me. I hope they are for you, too. That sense of triumph is what sets off the next project, and gets the juices running for the one after that. What can I do with this? Can I add more to it? Can I make it do this other thing? Where can I go with this?

You know the answer already.

Go Further!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.184.102