Now should be the time when you are introduced to a flood of instructions that you might never fully commit to memory. I am hoping instead, that you will be patient and learn fully about all the concepts involved; the instructions introduced below are not necessarily the most useful, nor do they build any foundations for the more important instructions, nevertheless, learning about stacks now will ease greater learning further on.
A stack has three operations; you can push,
pop
and peek a stack. The idea is that you can push an object
onto a stack and recover it later with a pop. Stacks ergonomically
stack objects in such a way that whatever was put on last is the first
to come off. A long time ago, a team of computer “geeks” observed
the quaint behavior of a lunch room tray stack wherein a removed tray seemed
to “pop” the stack up some and a replaced tray seemed to “push” the stack
back down. This same idea was applicable to computer stacks; the
vocabulary was written accordingly. If an object is the first to
go in, it is the last to go out; an object last to go in is the first to
come out.
In assembly language, all pushing / popping is done
with register pairs. You can push a register pair, say HL, and then
pop it later as another register pair, say IX. Pushing a register
pair has no affect on the contents of that pair, the instruction <push
hl> (with that syntax) puts the value of HL on the stack (once on the stack
it becomes a nonspecific word, no registers are associated with it’s value)
meanwhile preserving the contents of HL. <push de / pop bc>.
Now, the value that came from HL is still on the stack, again in the foremost
position; DE has been copied to BC and whatever HL was will be loaded into
the next register pair to pop. Let’s say that instead of popping
the stack value we choose <ld hl,$5a92> and then <pop de>.
Our result is the previous HL in DE, $5a92 in HL and the stack pointing
to whatever word was first pushed but hasn’t yet seen a corresponding pop.
Note: it’s very important that you remember to pop everything you have
pushed. If you leave a different stack than the one you began with,
the calculator will crash. Also, you can push / pop the accumulator
by pushing / popping it with the F register (flag register ... to be explained
in the next section) -- <push af / pop af>.
A peek must be simulated by adjoining pop / push
instructions since the Z80 doesn’t have a built in peek command.
<pop hl / push hl> will, in effect, use HL to “look at” the top of the
stack and leave the stack unmodified.
The stack must have an area of allotted memory somewhere
in our 64k. The calculator uses the area starting at $fbff and working
its way down in memory. The register SP (Stack Pointer) is
what tells the Z80 where the top of the stack is located. SP is merely
an address. Whenever a word is pushed, SP decrements by two; whenever
a word is popped, SP increments by two (remember that a word is two consecutive
bytes).
A strange instruction dealing with stacks is <ex
(sp),hl>, which exchanges the top stack word and HL. After this execution,
the top of stack ((SP) is a way of referring to the address addressed indirectly
by SP) will have the previous contents of HL and HL will contain what was
previously stacked. Another instruction you might use if you need
to change the location of the stack, is <ld sp, operand>, which
loads SP with either a register pair or an explicit value (must be an address).
There are no instructions to load a register pair with SP, however, making
it difficult to know where to return SP to if you move the stack.
Here is another register to burden you with: PC,
the program counter. This register keeps track of where the Z80 is currently
executing in memory. PC holds a pointer to the current instruction
and is incremented on that instruction’s completion. If you want
to change the location of the program counter in order to, say, jump over
code, then you can load PC with an address by using the jump instructions.
<jp $e1c0> will move execution to $e1c0. If you have a label called
shift_left_carried_bits at $e1c0 then you are able to type <jp
shift_left_carried_bits> as well. You should always have a well named
label at every address you jump to and use that label in your instruction
syntax.
A relative jump (127 bytes forward or 128 bytes
back) can be accomplished with jr. <jr shift_left_carried_bits>
can be used when $e1c0 is within the relative jumping range. Even
though jp can always be used in the place of jr, the opcode of jr is only
two bytes in length compared to the three bytes of jp, the absolute jump.
Using jr wherever possible is generally considered good style ... don’t
worry about counting bytes to see if jr can be used; the compiler will
tell you when its range is exceeded.
<jp (hl)>, <jp (ix)>, <jp (iy)> are three
more instructions that do exactly what you’d imagine them to. Each
one loads PC with the address in the operand register, jumping to
that location.
You may end up having a portion of code in your program that needs to be run several times. Instead of repeating that code, you have an ability to partition the repeated area into a call. A call is like a jump in that it loads the program counter with a given address, but it differs in that a call is powerful enough to return to the calling instruction once a <ret> instruction is encountered. A coded call might look like this:
ld hl,stored_number
;hl points to a RAM location we named “stored_number”
ld a,(hl)
;ld a with the value at that location
call _next_integer
;call a routine that
ld a,(hl)
;get the new value of hl
... ;more code
next_integer:
inc a
;increment a by one
ld (hl),a
;put incremented number back into (hl) or (stored_number)
ret
;go back
This is a ridiculous scrap of code that doesn’t really
serve any practical function. You can see, however, what an implemented
call will look like. There is no limit to the length of a called
sub-routine, nor are any registers destroyed by the call or the return.
There is one thing of which you must be cautious when writing calls, and
that is: pushing and popping without returning the stack to what it was
before the call was made is enough to crash the calculator. This
is a result of the Z80’s pushing the current program counter address when
a call is found and then popping that address back into PC when a return
is found. This doesn’t mean you can’t use the stack inside a call,
but don’t push before a call and pop within a call expecting to get out
what you pushed.
An assembly program is itself a call. When
the ROM decides to execute an asm program, it makes a call to _asm_exec_ram
($d748); an assembly program is terminated when it reaches the outer most
return.