I used to program the Spectrum (48K and +3) in assembly language back in the day. I always used my own interrupt handler (and im 2). I never needed to set up a 257 byte table as the lower byte of the address containing the location of the interrupt routine was always FF. Until a few days ago, when looking at the Z80 data sheet (and how to write a Z80 emulator), I was unaware of the 257 byte table requirement and it was never mentioned in the assembly programming books for the machine. There was never a problem with getting the interrupts to work as desired.
Such a strange thing to come across on youtube. I started programming ZX81 when I was 17 just by placing the hex in the memory. I had never heard of assemblers. I ended up making and selling some of the first games for it. Good times.
There's a full page of FF in the ROM. I usually load I with that page causing a jump to FFFF. At FFFF I put a JR instruction. The address will roll over to 0000 where there value is negative so the interrupt will execute a relative jump backwards some bytes. There I make a DI and jump to the real interrupt service routine. Try it out and see if you like it.
This does come with the caveat that it will only work on the 48k model. I don't think it works for the 128 or other models (unless possibly when running in 48k compatibility mode).
20:52 line 92 add hl, de is 11 cycles, better do add a, reg (4 cycles) then ld l, a (4 cycles). Also use DJNZ instead dec b, jp nz to economy one more cycle. 22:30 very likely if you reverse shift direction and use add hl, hl to shift instead lines 107 and 108, you save more cycles, and also djnz will help a bit.
I had fun building the Sinclair ZX81, but I rapidly outgrew it. At least it intrigued me to move forward with my learning. I'm WAAAY beyond the z80 now, but I did some cool stuff with it (did more with the newer more powerful 50MHz eZ80F91 though)
The Z80 interrupt modes and requirements looked a bit tricky but this example was fairly straightforward. Too bad you need to dedicate 257 bytes for the interrupt vector table though!
For some Spectrums and Spectrum derivative machines, there are better solutions. There are also machines that ensure the table address is always even and/or normally high ($FE or $FF), but for portable code you can't count on any of this. Even worse with the bits being pulled high, peripherals can interfere with this and make it so you code normally works on a machine, but a new peripheral can cause it to stop working.
@@slithymatt Many Z80 machines had RAM in the low address space, and it's a requirement of the CP/M OS. But the original Speccy had ROM in the low address space.
If you feel like running your code at the "fast" memory and think you can save some of that by putting your interrupt jump table in the contended memory, you will hit a hardware bug (in at least some spectrum models): snow will appear on screen. This isn't emulated by most spectrum emulators.
Thanks. This is a nice clear example of how to create an interrupt routine. However, I'd love to hear a bit more about what examples you (or another viewer) could give where it would be essential to use such methods for something to work. I am thinking things like the Nirvana engine for multi-color graphics, but there must be other things too?? Maybe certain audio playback routines??
for any display update, if you want to provide finished result to user in single frame, you need to sync against the display beam (see how CRT displays work, and even modern LCD screens which keep pixels on permanently still refresh the pixel values simulating the EM beam of CRT, including the vertical/horizontal blank periods in the signal). The only way on ZX Spectrum to sync against the display beam is to use the interrupt and count execution time since last interrupt. Some games/demos are precise enough to even change video RAM values around the display beam displaying particular data, for example changing the attribute byte to display more than 2 colors in 8x8 char, like the Nirvana engine. But even for simple single "clear sprite + draw sprite", if you don't sync to do the redraw in period when display beam is elsewhere, you will see the sprite randomly blinking as sometimes the beam will display the unfinished result of sprite being cleared, but not drawn yet. So any ZX code wanting to provide smooth/stable image needs to sync against the display and that means to use maskable interrupt.
@@ped7g Just doing a halt and then quickly moving the data to the screen will work if you only have a small amount of data to move to the screen. Drawing to the lower 2/3 of the screen gives you some more time to push data before the beam catches up.
So, the routine basically processes the sprite into the buffer and checks key presses, then when the handler is triggered at the VB it writes the data to the screen? I'm very new to this and am just using a halt at the top of my main loop in IM1, but I still get visible tearing and flicker, so this may be a great solution.
Yes you want to do all the screen writes during VBLANK to avoid tearing. Doing your own interrupt handler will make sure you get that done before the blanking period is over.
@@slithymatt Thanks for that. I'm finding the documentation really hard to get a grip on because it's so vague, and there's rarely any examples, or even context. Do you have any tutorials which deal with loading assets (like graphics/sprite sheets etc) from storage devices like tape/disk files into DS blocks of RAM? Different game levels will require different graphics/audio to be loaded, so I assumed it'd be a common, well documented process. Sadly I've scoured the internet and found nothing on this topic, and no other tutorials cover it. I looked at some ROM routines for loading files, but again there's just a very vague description of what it does and no guide of actually how to do it.
@Sakura_Shadows unfortunately, I have not done much with tape or disk I/O in the series so far. I plan on restarting it, but the focus may be more on SD card access on the Spectrum Next.
The main reaction was to the statement that there is no z80 instruction that copy a byte from one address to another. LDI does these steps: LD (DE),(HL) INC DE INC HL DEC BC ... and sets the P/V flag in the case of overflowing. You do a DEC C and use B as loop counter so would LDI really not had worked?
@@slithymatt `ldi/ldd/ldir/lddr` set P/V flag to BC!=0. So you can loop until BC is zero after `ldi` by `jp pe,loop` (but for low amount of repetitions like 8x you can instead write 8x ldi, in sjasmplus you can use dot-repeat operator like `draw: .8 ldi` to get 8x `ldi`. (and you can often fight the unwanted BC decrement by adjusting the value in BC according to the expected memory transfer, for example if you have in B some counter and you know you will transfer only 16-50 bytes, put into C 50+ value and then you can use unrolled `ldi` and still use B as some djnz counter, knowing the `ldi` will keep decrementing only the large-enough value in C, and not interfere with B, etc... there are several tricks how neutralise the BC-- of ldi/ldd, depending on the specific code and requirements. size-coding fun trick: checking whether keys Z and X are both pressed or released can be also resolved by parity flag, when you initially do `and $06`, it sets also parity flag, and because both pressed/released keys produce 00 or 11, those are parity-even. Single key pressed is either 01 or 10 resulting in odd parity. Thus single `jp pe,main_loop` is enough to resolve both-keys situation, and you can drop the `cp $06 : jr z,main_loop` from the code.
Not really related, but how to disable the "scroll?" message thing that keeps popping up whenever I attempt placing tiles on the two rows of space on the bottom of the screen?
You made a mistake regarding backing up registers at the start of the interrupt routine. You stated to save a bit of time to only back up registers you use in the main routine, this is the wrong way to select which registers to backup! You should be backing up registers you use in the interrupt routine. While your way does work, it is not "future proof". Consider you have your interrupt routine working using your backup method (I.E. only backing up registers used in the main routine) and you now go on to add more to the main routine. Later down the line you start using a register you were not already using that just happens to be used in the interrupt routine. Because you think the interrupt routine is working without issues you do not think to go double check it for registers being backed up. Now the two routines are going to clash using the same register and cause runtime errors that you did not foresee and are difficult to find in the code due to the assumption the interrupt routine works fine! But if the interrupt routine is backing up the registers it's using it wont matter if you start using one of those in the main routine, it's protected by the interrupt routine and so no runtime errors! Of course it's all moot if you are backing up all registers like your actual code does in the video, but in a large program, like a game, both RAM as well as speed are important factors so usually you will be only backing up used registers.