Tuesday, 28 February 2012

In Which We Consider Initialisation


The first thing the new ROM has to do when the machine is powered-on is get the hardware ready for action. There are a few basic things that we have to do which the original Commodore code already does, since initialisation of the system is largely predicated on a hardware design which we're not changing. However there are a few improvements to be made, since I'm going to be running that hardware somewhat differently. Here's a summary of the steps the Commodore code takes at startup:
  1. Do a little bit of CPU housework
  2. Check for an autostart cartridge in the expansion slot
  3. Initialise and test RAM
  4. Initialise I/O vector jump addresses
  5. Initialise VIA I/O chips
  6. Initialise VIC chip and screen
  7. Enable interrupts and start BASIC
Now there's nothing inherently wrong with any of this, but I'm going to point out a few drawbacks which my ROM will attempt to address. Let's look at the original code (annotations by Lee Davison):

$FD22:
LDX #$FF ; set X for stack
SEI ; disable interrupts
TXS ; clear stack
CLD ; clear decimal mode
JSR $FD3F ; scan for autostart ROM at $A000
BNE $FD2F ; if not there continue Vic startup
JMP ($A000) ; call ROM start code
$FD2F:
JSR $FD8D ; initialise and test RAM
JSR $FD52 ; restore default I/O vectors
JSR $FDF9 ; initialize I/O registers
JSR $E518 ; initialise hardware
CLI ; enable interrupts
JMP ($C000) ; execute BASIC

The first thing to note is that the code looks for an autostart cartridge at $A000 (by calling the routine at $FD3F) and will immediately hand-over the rest of the initialisation process to it if it exists. This seems like a bad idea to me, because arguably if there's some fatal issue with memory (like a RAM chip has died) then we want to stop very early in the initialisation sequence and not do anything else, especially as we don't know what actual initialisation the autostart cartridge will subsequently do or what assumptions it might have about whether memory is working. In fact, the memory test routine at $FD8D (known as RAMTAS) does have an 'emergency stop' clause for halting the use of the machine if some of the on-board memory is found to be faulty. More on that later.

Can we assume that the cartridge, if present, will do its' own memory test? I don't think so, and if we're going to prevent the use of the machine in the event of a memory failure, we should do it consistently - regardless of whether an expansion cartridge has been plugged-in or not. That means doing the memory test before we look for a cartridge, and if a cartridge then does in fact go on to do some memory testing of its' own, there's no harm done.

Also, observe that the code for checking whether there is an autostart cartridge is called with a JSR instruction. This instruction works by placing the address* of the next instruction (in this case  BNE $FD2F, which is at $FD2A) on the Stack and then jumping to the routine at the address specified. Eventually that subroutine executes an RTS instruction, which tells the CPU to pop the return address off the Stack and carry-on. Well, so what?

Here's the thing - the Stack is a 256-byte area of memory starting at $0100 that the CPU requires all to itself just for things like stashing subroutine return addresses. But what happens if that bit of  memory has failed, or in fact if the entire 1K chip providing addresses $0000-$03FF has failed? We haven't done the RAM test yet, but we're invoking a subroutine with a JSR instruction that implicitly demands that those 256 bytes of Stack space are working. If they're not, the CPU may not be able to write the correct return address into the Stack, or more likely will fail to get the correct address back when executing the RTS. So ironically, if there is a memory fault across the Stack area, there's a good chance that the CPU would return to a completely garbled address and chaos would ensue. This is another reason to do the memory test early, and not make use of any CPU instruction that depends on the Stack until we're sure the memory is OK.

The code to initialise the I/O vector addresses is tied very closely to the way the original Commodore ROM works, and since we're re-writing it, this bit of initialisation is largely superfluous at the moment. We might include something like this towards the end of the project, but my intention is to make the new OS as coherent as possible, which means stuff like vector initialisation should happen close to the code that needs it. Also, the VIA initialisation  code is pretty standard and is going to look much the same in the new ROM - but the VIC configuration will differ substantially.

In the Commodore design, the VIC is set up to generate a 22 x 23 character screen, but this can also be manipulated as a 'hi-res' 176 x 184 pixel grid. Furthermore, the dimensions of the screen area can be altered, subject to memory constraints, so that additional rows and columns can be displayed. With some clever programming and a custom-designed character set, it is possible for the VIC to display a legible 40-column text screen - there were several software and cartridge-based 40-column solutions for the VIC-20 available. Since we're designing the ROM from the ground up, we can build this 40-column mode right into the heart of the new machine so that it's a native display mode - we'll need some considered adjustment of the 4K Character Generator ROM at $8000 (essentially just a series of bit patterns representing individual character display glyphs) to give the new OS something sensible to draw, of course.

Oh, one other thing - take a look at the last line:  JMP ($C000). This is the finale of the initialisation routine, jumping through the hard-coded vector at $C000 to the start of BASIC. This is a perfectly acceptable approach for the ROM to take, because the KERNAL ROM (the bit written by Commodore that does all the bare-metal stuff with the machines' innards) is separate from the BASIC ROM (the bit written by Microsoft to provide the BASIC interpreter and runtime platform) and they live in physically separate 8K ROM chips - KERNAL is in the $E000-$FFFF chip, and BASIC is in the $C000-$DFFF chip. So having the KERNAL finish the startup process by jumping through a vector at the start of the BASIC ROM is a good idea, because then if BASIC were ever to be upgraded (which might mean things moved around) Microsoft could just alter the vector at $C000 to point to the upgraded entry-point, and you could just drop the new ROM chip in as a replacement. KERNAL doesn't have to change in the slightest, because it still jumps through the vector and neither knows nor cares where that vector points.

Er, except that this can't actually work in practice, because the BASIC ROM overlaps into the KERNAL ROM a bit - BASIC starts at $C000 and actually ends at $E1BA, with the KERNAL starting at $E1BB and running up to $FFF9 (the six bytes from $FFFA-$FFFF are hardwired CPU vectors for RESET, NMI and IRQ). So this means that if the BASIC ROM were to be altered in an upgrade, you'd almost certainly have to replace both BASIC and KERNAL because otherwise Microsoft and Commodore would have to work very hard to make sure all the code from $E000-$E1BB remained in exactly the same place, and didn't change at all.

My new ROM isn't even going to try to separate the OS and language elements into two discrete blocks of 8K just so they'd neatly fit into a chip each - they'll be separated logically, but not to the extent that I'll compromise the design just to make them both fit into 8K. If the OS needs 10K and the language needs 6K, so be it. As such, I shall dispense with a vectored entry-point into the language runtime and just have the OS drop into it as a natural end to initialisation - the new ROM starts at $C000 and will grow up towards $FFFA as it needs to.

But before we get to any of that cool and groovy stuff, we have to rework the way the memory map is managed by the OS because we're going to do things quite a bit differently in VIC++, and the starting-point is knowing how much memory is installed in the system. And that means our new ROM is going to start with a brand-new, faster and more sophisticated version of RAMTAS.


* afficionados of the inner workings of the 6502 will note that I have simplified this somewhat, not wanting to get into the subtleties of the actual adjustments that the CPU makes to the JSR/RTS return address.

No comments:

Post a Comment