Some very good posts recently about DOS/65, CP/M (65) and
Flex, so I thought I'd post something about another OS that rarely gets a mention - and that's the
Acorn MOS (Machine Operating System) and how my version of it works in my Ruby boards.
MOS came with the BBC Micro (c1981) and is a slightly different thing and more of a "Modular OS" or at least extendable, than an integrated Disk, screen, IO, etc. handler, however while there is a CLI as such, it's not normally visible in the Beeb which like a lot of the "classic" 6502 systems, boots directly to BASIC (OS and BASIC in ROM). To run MOS commands in languages/applications that have their own command line, you'd typically prefixed them with a star (
*) and commands can be shortened with a dot - hence the name of the website dedicated to the Beeb and other Acorn systems; Star Dot - after the shortest command
*. which is short for
*cat which is the 'catalog' or 'ls', 'dir', etc. command.
https://stardot.org.uk/forums/The OS defines an interface to the hardware, filing system, network, etc. via a set of
JSR entry points at fixed locations. And Even though the OS itself doesn't directly support some hardware, e.g. a disk system, the vectors for filing system operations are defined and those calls passed onto ROMs to handle them when required.
Some OS calls take a single parameter in
A - e.g.
OSWRCH (write a character), or return a result in
A - e.g.
OSRDCH (read a character), some take a function code in
A, then byte wide parameters in
X and
Y (
OSBYTE), sometimes returning results in X and Y, and some take a function code in
A with
XY pointing to a block of RAM containing more word-wide parameters (
OSWORD)
Trivial example: Print A to the screen:
Code:
OSWRCH := $FFEE
...
LDA #'A'
JSR OSWRCH
Another example - part of my RubyOS is the 'getline' call which is a wrapper round the OS call
OSWORD 0Code:
; getline:
; Handy short-cut to osWord 0 with fixed parameters for command
; line entry in Ruby OS.
;********************************************************************************
_getline:
ldx #<_getlineData
ldy #>_getlineData
lda #0
jmp osWord
_getlineData:
.word keyboardIn ; Address of input buffer
.byte 250 ; Max length
.byte 32 ; Smallest value to accept
.byte 126 ; largest...
So a pointer to the parameter block is passed in XY, the OSWORD code in A. The parameter block has the address of the input buffer, length, and the range of keys to accept.
When I ported EhBASIC, I used this, slightly modified for EhBASIC:
Code:
; call for BASIC input (main entry point)
LAB_1357
ldx #<_getlineData
ldy #>_getlineData
lda #0
jsr osWord
lda Ibuffs
cmp #'*'
bne :+
ldx #<Ibuffs
ldy #>Ibuffs
jsr osCli
bra LAB_1357
: dey ; -1,
tya ; Length
tax
stz Ibuffs,x ; null terminate input
ldx #<Ibuffs ; set X to buffer start-1 low byte
ldy #>Ibuffs ; set Y to buffer start-1 high byte
rts
_getlineData:
.word Ibuffs ; Address of input buffer
.byte Ibuffe-Ibuffs ; Max length
.byte 32 ; Smallest value to accept
.byte 126 ; largest...
So almost trivially, I got nice command-line editing and history handling right inside EhBASIC and note the check for a line starting with a
* so it can "shell-out" back to the RubyOS CLI, so typing e.g.
*. to get a disk directory "just works", as well as OS debug commands to dump RAM and so on.
There are additional entry points for file operations - from "whole file at a time" commands (e.g. load an image from tape or ROM) to partial file commands (open, read bytes, write bytes, close sort of thing).
Almost all OS entry points jump via a vector in page 2, so it's possible for user programs to intercept just about anything to implement their own thing - like graphics extensions and so on. (All graphics and text screen manipulation is done via the VDU (print character) driver rather than dedicated "draw a line", etc. type of system calls). Interrupts are also vectored too, so user code, utilities and so on can hook into the interrupt systems if needed.
The BBC Micro has the facility for up to 16 "sideways" ROMs - these are 16KB images which all start at $8000 and go up to $BFFF. The OS starts at $C000 and the OS entry points/vectors are up in $FF8x upwards, just below the hardware vectors.
The OS manages the ROMs in a way normally transparent to user programs, even those currently running from a ROM in the same region - to do this you must use the OS calls, but that's no bad thing. ROMs are activated sequentially at power on time, given a chance to initialise, then a default "language" ROM (e.g. BASIC) is selected and executed. The OS remembers the ROM number of the "current filing system" and will activate that ROM when a filing system call is made. So typically, the OS keeps track of 2 ROMs - the current language (e.g. BASIC, BCPL, EhBASIC, COMAL, Forth, etc.) and the current filing system ROM which may be one of several different disk systems or network.
So out of the box, you get a tape filing system, a ROM filing system and BASIC to go with the OS. (Tape and ROM filing systems are part of the OS ROM).
Disk? That's another ROM in the $8000 through $BFFF range - same for Network (The Beeb has an optional network interface and network file servers were available). Because everything was open (or rather not closed) there were several disk filing systems that quickly evolved some with more facilities, faster/better controllers, double density, etc. than the default one and so on.
One thing to note about the filing systems - the OS doesn't specify the on-media format in any way - all you see in your user program (BASIC, ASM, Forth, whatever) is the file level interface.
Also worth noting is that the boffins in Acorn HQ defined a somewhat interesting file naming convention - it was 1981, Unix, DOS, etc. were well defined but they did something a little different - full filenames start with a colon (:) then the drive number then a dot, then it's directory dot filename (or directory dot directory dot and so on...) The top-level directory is dollar. Dots separate directories rather than say slashes. This means you can't have filename.b or filename.c and so on.
So...
:0.$.hello maybe you're doing C development? Well, you create a directory called
c then,
:0.$.c.hello might be the file you want.
Utility ROMs quickly sprang up too - and the OS allowed for this - when you executed a star command, if it wasn't recognised it would be passed to the other ROMs. Same for un-recognised OSBYTE and OSWORD calls, so extending the OS was relatively trivial.
There are other facilities like managing interrupts, the parallel printer port, serial port, sound and so on. The keyboard scan is also handled by the OS rather than a dedicated encoder chip (as in e.g. the Apple II)
Another feature I've not seen elsewhere is BRK handling. BRK is used to signal an error code the message, so the code to handle OSBYTE #0 which is also accessed via the *FX 0 command:
Code:
; 000/00:
; Identify OS version
;********************************************************************************
osByte_000:
cpx #00
bne osByte0a
brk
.byte 0,"Ruby 6502 2.0",0
osByte0a:
ldx #$2
rts
Languages can redirect the BRK vector to their own handler and BBC Basic does this so it can handle the error, print the message if required and continue inside BASIC.
Fast forward a few decades...
So after I'd written my own "WozMon" for my Ruby 6502 board, I decided I wanted to run a better BASIC than EhBASIC so I went down the "how hard can it be" route and wrote just enough to make BBC Basic work... Then I decided to go the whole hog. Ruby is quite different from a BBC Micro though in that it has a support co-processor which is an ATmega - initially intended to drive a composite video monitor, but I changed that to be a general purpose host processor responsible for booting the 6502, acting as a serial port and filing system/SD card driver. Also, there are no "sideways" ROMs to manage, so it's more monolithic than the original Acorn MOS.
Ruby is more like the original BBC Micro with a 2nd processor (it was designed to have a 2nd processor talking to the 'host' via a high speed 2Mhz interface) with the Ruby 65C02 being the 2nd processor and the ATmega being the host processor.
It was relatively easy to write little stubs of code to handle the 6502 side of the MOS interface and pass the data over to the ATmega. That way existing code for the BBC Micro "just works" - provided it uses the OS. I was able to run BBC Basic (all 4 versions), COMAL, BCPL and a few others. Some of the applications like spreadsheets and word processors evaded me because they do weird stuff outside the OS and expect a real BBC Micro. Assembling and patching the 65C02 version of EhBASIC was trivial - partly thanks to the work already done by floobydust. I also managed to get other MS Basics going - e.g. CBM BASIC 2.0 - Applesoft still eludes me, although I can run it, but I've not fully sorted out its zero page usage.
(Note - I'm running the Acorn ROMs un-changed, no patches, etc. in the Ruby6502 system)
So for the most part I have a 16Mhz 65C02 which I can run 6502 code and BBC Basic programs on.
On the VDU/Graphics side - Programs on the Beeb do screen stuff via the VDU driver, so to clear the screen;
PRINT CHR$(12); but BASIC provides a 'CLS' commands for this. Similarly to draw a line - it's output the code for "plot", then the plot type (line, dot, etc.) then X and Y locations as signed 16-bit integers (so 6 bytes) and a line is drawn - and again BASIC has commands for this - an advantage of having 16KB for BASIC to fit into compared to other 8 or 12K BASICs in the 6502 world at the time. (And because there is no real serial comms involved on the Beeb it's all very fast - slightly slower over the 1152Kbaud line in Ruby)
My RubyOS doesn't do anything with these codes - it just passes them up the line to the ATmega - at that point, the ATmega "knows" if it's taking to a VT100/ANSI style terminal and if-so, it translates the codes into ANSI codes (mostly e.g. text cursor movement, etc.) or if it's talking to "RubyTerm" which is an terminal application I wrote that runs under Linux, it passes then up "as is" and RubyTerm deals with them, so can draw lines, circles, re-define font characters and so-on.
Back to the OS: RubyOS doesn't have a "language ROM" loaded at boot time, so it drops into the CLI. The CLI is a trivial application that I wrote into the OS - it formulates a call to "OSWORD 0" which is an OS call to read a line of text, then passes that line of text to OSCLI which is a call that executes a command line. the RubyOS version of OSWORD 0 does nice things like cursor movement for editing, character deletion, swapping, finding and history retention and recall.
There is no automatic "search the disk" for a command if you type a command not recognised by the OS ROM, however there is a
*RUN command which is shortened to
*/ so
*/basic4 lets me load BBC BASIC4 into RAM at it's specified start address and then it's executed at it's specified run address - similarly, I start BCPL on my '816 board with
*/bcpl.
On the file storage side - the host processor maintains the actual filing system and it's a Posix like hierarchical filesystem with the usual open/read/write/close/seek interface. There is a shim-layer in the 6502 side that translates Acorn style filing system calls to this Posix like call. The one extra that the Acorn MOS (and some languages) expect is a few more words of file attributes - namely load address and execute address. These are 32-bit values. I've kept these in RubyOS. There are some other attributes for a whole disk - e.g. an action to do if you type Shift+Break on the keyboard - autorun a file called
!Boot for example. I don't have this facility in RubyOS, but the ATmega does treat its on-board 4KB of NVRAM as a disk, so it could be implemented to run a script.
On the "run a script" front - there isn't a shell command language but you can switch input from the keyboard to a text file with the
*EXEC command and capture output to the screen in a text file with the
*SPOOL command.
So what do you get from the point of view of writing and running your own code?
Well, BASIC, COMAL, BCPL will work as they did on a Beeb - more or less. That gives you local development, but for anything else?
BBC Basic has a built-in assembler, but for my own thing I use cc65 (both assembler and C) on my Linux desktop and copy the code over. (The RubyTerm application supports a file-transfer mechanism to get data to/from the local SD card and filing system.
User programs would normally load in the 16K space starting at $8000, but they can load lower if required - all the way down to the value of "PAGE" which is controlled by the OS and is normally $0E00, so you effectively have RAM from $0E00 through $BFFF.
Zero page usage - $00 through $8F is free with the OS claiming $90 through $FF for its own use.
The stack is all available for user code, but calling the OS will use some space in it for return addresses and interrupts, but not data. (Generally language applications like BASIC, etc. are not expected to return to the OS, but utilities run from the OS are expected to behave and not trample over ZP $00 through $8F
- Page 2 is used for the redirection vectors.
- Page 3 is the keyboard input and editing buffer.
- Pages 4,5,6 and 7 are reserved for your user application use. e.g. BBC Basic uses this area for high-speed 32-bit integer variables; @% through Z% and other data it wants at a fixed location.
- Pages $8 through $D are used by the OS for various buffers leaving $0E00 upwards free.
User programs should use the OS call "PAGE" to find out where this actually starts though - in Ruby it's always $0E00 but in a BBC Micro it's variable as the disk and network filing systems need more buffer space.
The opposite is "HIMEM" which is usually $8000 in Ruby, but it's variable in a BBC Micro as the top of RAM is used for video memory.
I put together a little framework for cc65 to let me run C programs on it. It works well and I am able to run my nano-like editor on it - which compiles to just under the 16KB limit, leaving a lot of RAM for text to edit. the C startup code reads PAGE and HIMEM and uses these for the start of data and stack.
When I developed the ruby816 board I ported my RubyOS over to it - which ran very well, but I deliberately decided to restrict the executable '816 code to bank 0 to keep things easy - the only thing I run there is my BCPL OS which lives in the 16KB region from $8000 and executes the compiled bytecode which can live anywhere in RAM.
I ported it over to the 6502 side of the Cerberus2080 board too and it worked well there, so it ought to be easy to move to other systems - providing the memory map is suitable - I've looked at the Commander X16 system, but things are just in the wrong place - or just wrong enough that I don't want to spend the time and energy on it.
I've thought of writing a better "shell" in C for it - very possible, but again it's effort vs. what it would give me (on the 6502 side). The BCPL OS has it's own CLI/Shell which I'm slowly turning into something more Unix shell like, even if it's not really what I want to do - but it's hard to get away from 40+ years of Unix...
Anyway, there you are - thought I'd mention it as it seems vaguely topical right now!
Cheers,
-Gordon