I think i must be getting old and grumpy, it seems like every year there is a new addition to languages that you need to learn. Im not sure these are all good things myself. Im mainly moaning about the new (and not so new) features to C++ like foreach and auto.
Lets clear one thing up, i dont find it hard to understand these, ive used similar in many languages but im not sure they belong in C++. I feel that all it really does is make for lazy programming. Why do you need a foreach instruction, it can be done just as easily in a standard for instruction. What is the advantage beyond having to type a few less characters. I can tell you one thing.. It damn well slows down the compiler.
I remember getting the first Intel P90 in the office to compile on, it was amazing, code built in no time. Now i find myself waiting well over an hour sometimes for something to compile! Im on a i7 Six Core with 32Gb of Ram running SSD, this should never NEVER happen!
So yes, im sure lots of people will tell me how wonderful it is to use foreach/auto etc and all these things, im sure they will give good reasons for them... to me you are wrong.. its lazy, just learn to type faster, and then maybe we can all compile code quicker again....
Coders Bucket
Various bits, ideas, tutorials and ramblings from an old and cranky programmer
Monday, 13 April 2015
Updates to interrupt details
Following some questions about the interrupt details posted, i updated the end of the document to include the following paragraphs.
RET / RETI
EI but not DI?
RET / RETI
You may have noticed that the interrupt routine ends with
RETI rather than the standard RET, this is for completeness and not
specifically required, you can end with RET if you prefer. The main
reason RETI was added to the instruction set is to allow hardware to detect
when the end of the interrupt routine and to pass control onto the next
interrupt in a dasiy chained system. The hardware could specifically
check the opcode for RETI. If it checked RET it could get confused when
coming to the end of a function rather than the entire interrupt routine.
To my knowledge no devices for the spectrum use this feature.
EI but not DI?
There is another oddity in the interrupt routine, we have an
EI at the end but we never do a DI. This
is due to the fact the Z80 will do an internal DI when it starts processing the
interrupts. The reason for this is to
stop multiple interrupts triggering on top of each other and overrunning the
system stack. You can do an EI before
the end of your interrupt routine if you wish but general practice is to wait
until you have finished then enable them, that way there is no chance of you
starting a new interrupt routine before the old one has finished.
Interrupts on the ZX Spectrum
What are interrupts?
Interrupts, as the name suggests, are when the normal
running of a computer program are interrupted so something else can
happen. On modern computers there can be
various interrupts, but on the ZX Spectrum there were very few, most times you
will hear of only 3 different interrupt modes 0, 1 and 2. While the main focus of this document is
interrupt mode 2, we will quickly cover modes 0 and 1.
These three interrupts are all maskable, that is to say we can stop the effect of them by using
the DI (Disable interrupts) instruction.
If we want them to start happening again we use the EI (Enable
interrupts) instruction. For
completeness I will point out that when we use the DI instruction, the interrupts
still trigger, that is the Z80 is still aware of them, it just ignores them and
carries on doing what it was doing anyway.
Interrupt Mode 0
This interrupt mode is not really used. The reason for this is that it is triggered
by external hardware. The devices must place instructions (such as RST or CALL)
onto the data bus when it sets the interrupt request on the Z80. It is this interrupt mode however that is
running when the spectrum first powered up, luckily the Spectrum ROM soon
setups up Interrupt Mode 1.
Interrupt Mode 1
This interrupt is very similar to IM2 (Interrupt Mode 2) in
that it is triggered by the vertical blanking of the screen refresh that
happens roughly 50 times a second. The
vertical blank is the time when the beam that draws the screen has reached the
bottom and is turned off while it moves back to the top, ready to draw the next
frame. Mode 1 is the standard interrupt
used by the Spectrum while running basic and, unless you alter it, while your
programs are running.
When the vertical blank occurs the current contents of the
program counter (PC) are pushed onto the stack (SP). The address $0038 is then loaded into PC and
the Spectrum will start running the program stored at this location. As you may notice by the address, this is in
the ROM and as such we have no way of altering what this does. The ROM routine mainly scans the keyboard and
stores details of what is being pressed.
Interrupt Mode 2
As mentioned IM2 is very similar to IM1 above except it is
known as a vectored interrupt. Like IM1
it is triggered roughly 50 times a second on the vertical blank, it pushes the
current PC onto the stack but then rather than jumping to a specific address it
uses a vector table to find out what value to load into PC. There is a lot of confusion over the exact
operation of this vector table, and as different emulators deal with it in
different ways, it is best to go with the method that works for all.
The vector table is simple a list of memory addresses, 128
of them in total. The table sits on a
256 byte boundary and the high byte of its address is loaded into the special I
register on the Z80. The confusion seems
to occur with what fills in the bottom 8 bits.
The value comes from the BUS and general feeling is (and as such the
safest rule to follow) that any value can occur. Some documentation such as ZAKS (Programming
the Z80 book) will say that only the higher 7 bits will be used from the BUS,
the least significant bit will always be 0.
While to me this seems the most logical given the vector table, as
mentioned not all emulators follow this rule and as such I would strongly advise
against assuming this bottom bit is 0.
Why does it make a difference you may ask? To answer that lets look at
what happens when the interrupt is triggered.
When the interrupt is triggered, and they are enabled, the
contents of the PC are pushed onto the stack. The contents of the I register
are loaded into the upper 8 bits of the address bus, the BUS supplies the lower
8 bits, as we are not using any devices these bits can be any value. . Given
this address ((I * 256) + BUS bits) a
two byte address is read and loaded into the PC. The Z80 will then run the code
stored at this address as per normal.
First lets assume that the I register is set to $40 and that
the bottom bit is always 0. When the
interrupt is triggered the memory address will we calculated as $40xx where xx
is any even 8 but value (0, 2, 4… 254).
We can simply store the address of our interrupt routine at all 128
locations $4000, $4002, $4004 …… $40fe.
This way when the interrupt occurs the same routine address will be
loaded into the program counter no matter what the BUS value is. The code to set this up would be something
like
ORG $8000
; Setup the 128 entry vector
table
di
ld hl,
VectorTable
ld de,
IM2Routine
ld b,
128
; Setup the I register (the high
byte of the table)
ld a, h
ld i,
a
; Loop to set all 128 entries in
the table
_Setup:
ld (hl), e
inc hl
ld (hl),
d
inc hl
djnz _Setup
; Setup IM2 mode
im 2
ei
ret
; Basically nothing
IM2Routine:
ei
reti
ei
reti
; Make sure this is on a 256 byte boundary
ORG $F000
ORG $F000
VectorTable:
defs 256
defs 256
Hopefully the code shows that the vector table stores the
128 entries pointing to the routine IM2Routine.
When the interrupt is triggered the byte address at (I * 256) + Bus will
be read along with the following byte to form the address. The routine at this address will be called,
in our example no matter what the Bus is it will always call IM2Routine. But what happens if we don’t assume bit 0
will always be 0?
In our example above IM2Routine will have the address $8016,
so the first eight bytes of our vector table will be
defb $16,
$80, $16, $80, $16, $80, $16, $80
The rest of the table will follow the same pattern. When bit 0 was always 0 the low byte would
always be pulled out as $16 and the following high byte of the address would be
$80 (assuming Bus was 0 the low byte would be pulled out from the start of the
table and the high byte would be the next byte), the next option would be Bus
was 2 which would give the same result just reading from 2 bytes into the
table. If however bit 0 was not always 0
we could end up reading starting from byte 1 in the table giving our low byte
of $80 and our high byte of $16. This
gives us the address $1680 which certainly isn’t where we want the Z80 to jump
to.
Given the reasons behind how the vectored interrupt is
designed I would speculate that the hardware does indeed always reset bit 0 to
0 however as mentioned not all emulators follow this rule (The hardware ive
tested this on does mind you). To be
safe its best to work on the theory it can do a read from any memory
address. So, how do you deal with
that? Simply make sure all the bytes in
the table are the same value.
One other thing to note with the issue of reading from any
byte is what happens if the Bus value is 255 when the interrupt triggers. This will be used as the low end byte of the
address on the vector table, but as we need to read two bytes it will read the
last entry in the 256 byte table as well as the byte following it. To allow for this we must use a 257 byte
table when allowing for any Bus value.
So with that in mind lets take a look at the code to setup the table.
ORG $8000
; Setup the 128 entry vector
table
di
ld hl,
IM2Table
ld de,
IM2Table+1
ld bc,
256
; Setup the I register (the high
byte of the table)
ld a, h
ld i,
a
; Set the first entries in the
table to $FC
ld a, $FC
ld (hl), a
; Copy to all the remaining 256
bytes
ldir
; Setup IM2 mode
im 2
ei
ret
; This routine now needs to be at a specific address
(remember we only have from $FCFC to $FE00 else we will overwrite our table)
ORG $FCFC
; Basically nothing
IM2Routine:
ei
reti
ei
reti
; Make sure this is on a 256 byte boundary
ORG $FE00
ORG $FE00
IM2Table:
defs 257
defs 257
Why do we need an interrupt routine?
Having looked at how we setup an interrupt routine, you may
find yourself asking why you need one.
Well there are a few good reasons to set one up, firstly why let the ROM
waste good cpu cycles doing something that you may not need? The main reason however often comes down to
timing, especially with things like music.
A music player will usually require that you call an update
function every frame, which is 50 times a second. If you don’t do this then the music will
start to sound very strange indeed as frequencies etc will all go wrong. While it might seem easy to do this if you
know your program will always run in a frame, when you start to write more
complex programs and games this might not always be the case. In a game you may have an unknown number of
enemies on the screen firing an unknown number of bullets. While your code might happily run in a frame
if there are 5 enemies, what happens when the 6th one appears?
To solve this we can put the music update routine into the
interrupt, then no matter how long the game takes to update the music will continue
to be updated at a constant 50 frames a second.
To do this will simply add a call to the music player update in the
IM2Routine.
; Update a music player
IM2Routine:
call Music_Update
ei
reti
call Music_Update
ei
reti
Seems easy enough right? Well yes and no, the chances are
this would crash your computer horribly.
The reason is that the interrupt can occur at absolutely any time and it
doesn’t care what the state of your registers are. When we just had EI/RETI it was fine, unfortunately
the music update function will most likely alter registers, and even if it
handles storing and restoring them all itself, if it very good practice to
store and restore them yourself to be sure.
; Update a music player with register
storing / restore
IM2Routine:
push af
push hl
push bc
push de
push ix
push iy
exx
ex af, af’
push af
push hl
push bc
push de
push af
push hl
push bc
push de
push ix
push iy
exx
ex af, af’
push af
push hl
push bc
push de
call Music_Update
pop de
pop bc
pop hl
pop af
ex af, af’
exx
pop iy
pop ix
pop de
pop bc
pop hl
pop af
ei
reti
Just to clear up a few things…
The Z80 provides 2 other interrupts that are not maskable,
that is you cannot disable them with the DI instruction, they are the NMI and
BUSRQ. To my knowledge the BUSRQ was
never used as this was designed to allow DMA systems to work along side the
Z80, something the spectrum never had.
The NMI (Non-Maskable Interrupt) was used on the Spectrum but was only
available via additional hardware.
Certain hardware devices had a button that would trigger a
NMI which would cause the current program to stop and the routine at address
$0066 to be executed. As this address is
fixed and in the ROM it is of very little use for software programmers.
RET / RETI
You may have noticed that the interrupt routine ends with RETI rather than the standard RET, this is for completeness and not specifically required, you can end with RET if you prefer. The main reason RETI was added to the instruction set is to allow hardware to detect when the end of the interrupt routine and to pass control onto the next interrupt in a dasiy chained system. The hardware could specifically check the opcode for RETI. If it checked RET it could get confused when coming to the end of a function rather than the entire interrupt routine. To my knowledge no devices for the spectrum use this feature.
EI but not DI?
EI but not DI?
There is another oddity in the interrupt routine, we have an
EI at the end but we never do a DI. This
is due to the fact the Z80 will do an internal DI when it starts processing the
interrupts. The reason for this is to
stop multiple interrupts triggering on top of each other and overrunning the
system stack. You can do an EI before
the end of your interrupt routine if you wish but general practice is to wait
until you have finished then enable them, that way there is no chance of you
starting a new interrupt routine before the old one has finished.
Subscribe to:
Posts (Atom)