So … I want to implement enough of an operating system to keep BBC BASIC happy. But what elements of the Beeb OS does BASIC rely upon?
At this point I’d like to tip my hat towards Jonathan Harston, who is something of an oracle when it comes to the BBC Micro and BBC BASIC. His websites mdfs.net and BeebWiki host numerous useful documents, including this annotated disassembly of the entire OS which has been invaluable during my investigations into OS mechanisms.
Another vital resource has been “Guide to the BBC ROMs” by Don Thomasson – a book which my wife mocks me for reading in bed. It describes in great detail how the Beeb operating system works, and makes a perfect reference for those two or three people on the planet who are making their own BBC-Micro-compatible computers! I’m assuming there was more of an audience when it was published in 1984 …
Jonathan also provided me with a summary of what I’ll need to implement to make BASIC work:
- Leave RAM areas &0000-&005F and &0400-&07FF available for BASIC
- Implement OSBYTE &82, &83, &84 and &85
- Implement OSWORD 0 (“input line”)
- Implement OSWRCH
- Implement WRCHV at &020E
- For error reporting to work, BRK must set up &FD/E and jump to BRKV at &0202
- Everything else can just return doing nothing
Let’s look at each of these in more detail …
Leave RAM areas &0000-&005F and &0400-&07FF available for BASIC
This is fairly straightforward: my OS should not access those memory addresses, because they’re reserved for BASIC.
Actually, this highlights something quite important that I’ve not been too concerned with so far: the notion that bits of RAM (particularly in the first few pages) are reserved for certain purposes. These reservations were decided by Acorn. If I want to build a computer that is Beeb-compatible, then I must adhere to them.
The best practice when tracking RAM usage of my OS is to make a constants.asm file in my source code, and declare all memory usage in there. Then, in all places in code that requires accessing RAM, I make sure that the code uses those constants. Helps code readability, too.
mdfs.net has (of course) an invaluable document that summarises Beeb RAM allocation.
Implement OSBYTE &82, &83, &84 and &85
OSBYTE is a mechanism for getting or setting information between OS and running programs. It can perform lots of different functions – you specify the one you want by putting the right value into the accumulator, optionally set extra values in the X and Y registers, and then call OSBYTE:
LDA #&83 JSR &FFF4 ; entry point for OSBYTE
Thankfully I don’t need to implement all the OSBYTE functions at this stage – just &82, &83, &84 and &85. Any other OSBYTE call can be ignored for now.
What do those functions do?
- OSBYTE &82 – Read High Order Address
- OSBYTE &83 – Read OSHWM, bottom of user memory
- OSBYTE &84 – Read top of user memory
- OSBYTE &85 – Read start of display memory
BASIC calls these when it starts in order to find out how much RAM it has to work with. The start of RAM moves according to what expansion ROMs claim extra space on bootup, and the end of RAM depends on how much RAM needs to be allocated for display memory.
As my computer does not yet implement expansion ROMs, and I don’t need to reserve any space for display RAM, then just returning hard-coded addresses will be fine for now.
Implement OSWORD 0 (“input line”)
OSWORD is like OSBYTE, but less restricted by having to cram all data-passing into three registers.
Instead, OSWORD passes data via a control block – which is just an area of RAM. The address of that RAM should be passed in the X and Y registers, and the command number in A (as before). The OSWORD call then uses that control block (it might read data from it, or write data to it) as part of its operation.
OSWORD 0 is “input line” – it’s a standard way for a program to ask the operating system to accept a line of text from an input source. This line of text is stored in the control block. BASIC uses OSWORD 0 to receive typed commands for the interpeter. It’s also how the BASIC keyword INPUT works.
So it makes sense that I need to implement this routine so that BASIC will function – otherwise, it wouldn’t be able to get a program from the user!
This is just a case of moving my existing “send byte to serial port” routine so that it can be invoked from &FFEE.
BeebWiki’s article on OSWRCH is here.
Implement WRCHV at &020E
(“PRINT saves a couple of cycles by doing JMP (WRCHV) instead of JMP OSWRCH”)
This one requires a bit more work to do properly – but if I were to shortcut it now, I’d need to re-implement it properly later when I come to implement Sideways ROMs. So I might as well do it now.
The Beeb does it something like this …
The OS entry points are set in stone in the OS ROM, starting at &FFB9 (see here for disassembly). Only three bytes are permitted for each entry, which is just enough to get a JMP <address> instruction in to where each routine actually is.
But Acorn wanted their OS to be patchable by software – such as expansion ROMs – so that new functionality could be linked-in without other programs needing modification. I eluded to this in the last blog post with an example of a display expansion that could be utilised without any program requiring a modification.
But if the entry points are in ROM, how can we modify them? The answer is that the entry points don’t jump directly to the routine – instead, they use a table of addresses in RAM as a lookup, and jump indirectly. So, here is what exists at &FFEE, which is the entry point for OSWRCH:
&FFEE: JMP (&020E)
The brackets denote that this is an indirect jump – the processor will load the bytes from RAM at &020E and &020F, turn those bytes into a new address, and jump there instead!
To make this work, that location in RAM must have been setup with the address of the operating system’s OSWRCH implementation. The OS does this by copying a table of addresses into that area of RAM as part of its bootup sequence. If you’re interested, this table of addresses (one for each of the operating system routines) is in the OS ROM at &D940 (have a look here for the annotated disassembly) and is copied by a small routine at &DA5B (same link, but further down).
This copying is done quite early in the bootup, and then afterwards, any expansion ROMs are given opportunity to insert their own routines by changing the entries in that table. So when a program calls an OS function, an expansion ROM can choose to handle the call itself, before optionally passing it on to the OS to continue as normal.
Clever, isn’t it? Effectively, what you’re seeing is an early example of drivers!
Now … back to why this is relevant here.
To be honest – if BASIC was invoking OSWRCH the proper way (by calling &FFEE to print characters) then I probably won’t bother with this lookup table in my own OS. Not for now, anyway. I’d look at implementing it later, when I come to add sideways ROM support. But because BASIC is being a bit naughty and choosing to call it via the table in RAM instead (to save a few cycles, as Jonathan points out) then I need to implement it anyway.
To implement it here, I’ve copied the loop code from the OS ROM (at &DA5B) into my OS. And the table it copies contains entries for my OSWRCH, OSBYTE and OSWORD handler code, and a little routine called “stubbed” (which is just an RTS) for all the others.
For error reporting to work, BRK must set up &FD/E and jump to BRKV at &0202
The BASIC ROM implements its own error handler. The address of it is poked into memory addresses &0202 and &0203 (remember me talking about that lookup table earlier, for “patching” the computer behaviour?). So my OS needs to make sure that it implements a break/NMI handler, which eventually passes control onto the code pointed-to by those addresses.
Everything else can just return doing nothing
A subtle but important point – while I don’t need to implement the full range of entry points to make BASIC work, it is good practice to try and “stub-out” any functionality so that if any program tries to invoke it, it exits cleanly rather than leaving things undefined and causing random crashes.
All it means is that any OS subroutine I haven’t yet implemented should have an RTS in it, so that control is returned to the program that called it. Or possibly I should implement something that prints a message and then locks up, so it’s easy to see what I need to implement next.
That was a lot of code to update …
Ideally I’d be inserting code snippets all the way through this blog post, to better illustrate what I’m doing. But the code changes are beginning to get quite large, and it would make reading this article even more daunting. There’s a chance I might start making this project available as a kit one day (like rc2014) – if I do, then I’ll put the OS source up on github.
But does it work? Find out in our next episode …