Open hickery opened 4 years ago
This is the TL;DNR stuff about the internals of the SmartBox OS.
It only documents the 6502 boxes (and probably misses out some bits of the very later OS revisions), but the Mitsubishi boxes followed pretty much the same layout with minor mods.
Documentation for SmartBox OS 2.066
The term JobCall is the concept of the routine itself, as opposed to
JobName (which is the actual name associated with it (eg. NameCode)) and
JobCode (which is the actual number associated with it (eg. 3)).
The term controller means the SmartBox, or any other incarnation
which may appear in future time !
The term OS means the controllers' Operating System.
Preview
There are a number of reasons why using Machine Code on the
controller is more desirable than using the various JobCalls via the serial
link. One reason is speed, if the task which needs to be performed, has to
be performed very quickly (more quickly than can be achieved via the serial
link at least), then some code must be written to work in the controller
itself to achieve this sort of speed. If the task required is to be a
"background" task (ie. a task which may happen while other "things" carry
on as normal, like a change of state of a particular port) then it is far
easier to let the controller do all the work, maybe under interupts, than
continuously poll the controller checking the state of various things.
Another reason may be that you would like to add another JobCall to the OS,
extending the calls available to external applications (and the user at the
same time), without the user having to write the code himself to achieve the
aim. For advanced users, there is also the ablity to alter the way the OS
does various things and to have the controller in their "power", for
dedicated tasks etc.
One advantage of writing IN the controller is that that "function"
is available to any computer using the serial link, and is thus not fixed to
one computer type or even a computer at all, where the controller is used
free running, doing a particular task which doesn't need the intervention of
the user's own computer.
Call types
There are three different types of machine code routines which can
be added to the controller. Each one is suited to a more advanced level
than the other, which also relates to the amount of work needed to actually
get the routine working ! The three levels are such:
a) Simple stand alone routine. Here the routine is just the routine
itself with no supporting code (ie. code to integrate with the rest of the
OS). It has to be called directly, by use of the ExecuteCode JobCall, thus
it usually resides at a fixed address and obviously does not offer the user
friendly way of accessing itself like there is with normal OS calls.
b) Extended JobCall. There is one JobCall in the OS (ExtendJob)
which can call various routines residing in memory, each one is identified
by a 8 bit number. The disadvantages of this way is that the call hasn't
got it's own JobName/JobCode and needs the extra byte to identify itself.
This is ideal for the quick routine which the user would like to add for
himself without having to resort to the extra code needed for the full
JobCode implementation.
c) Full JobCall. This is where the routine can define itself one
(or a number) of JobNames/JobCodes, it can integrate itself into an
extension of the OS in a complete way. The disadvantage is that extra
coding is needed to deal with the various OS calls it has to service and
that extra work is needed to produce a suitable file to place in the
controller.
Normally with most processors, including the 65c02, the machine code
produced is produced for a fixed memory address, and it cannot be made to
run in a different place in memory unless extra work is carried out by the
user, or at least a combination of the user and the OS. For the controller
this is a very severe disadvantage as most extensions are downloaded as
"modules" and to optimise memory a system has to be devised to enable
routines to be placed anywhere in memory, thus allowing no memory to be
wasted. In the OS there is a routine to do just this, though it does need
some help. A special version of the users code has to be developed, or
rather it has to be "processed" to add extra information on the end. Extra
code is needed to set up a few pointers and call the OS routine to relocate.
See later.
Memory
The first 256 bytes of memory are somewhat special, most zero page
locations are reserved for use by the OS:
0 General workspace, used to pass parameters to some OS
routines, can also be used by other routines DURING
14 their execution.
15 Reserved
6f
70 Available to the user
9f
a0 Accumulator store for irqs (irq_A)
a1 Two byte counter, decremented at 100hz, useful for temporary
a2 timing purposes (fcount)
a3 RAM size, high byte of RAM size of machine, ie. for 32k
this will be &80 (RAM_size)
a4 Reserved
af
b0 Used by the OS
ff
The rest of memory space is used for the processors stack, OS'
buffers, tables, vectors and, of course, the user available memory.
A breakdown of the rest of memory:
&100 to &1FF is the 65c02's stack.
&200-&2FF is used by the OS for various system functions and also
contains the vector table, more on that later.
&300-&3FF is the serial input buffer.
&400-&47F is the internal job code call output buffer. (jobout_buf)
&480-&4FF is the internal job code call input buffer. (jobin_buf)
&500-&5FF is the job status table.
&600 to the end of RAM memory is for the user. The user should not
assume what the top of AVAILABLE memory is and use the
system variable HIMEM.
In the controller the maximum limit of RAM is &8000 (which is 32k),
from &8000-&DFFF lies the memory mapped hardware, from &E000 to &FFF9 lies
the OS and from &FFFA to &FFFF lies the 65c02 vectors.
So the user has his own limits of free memory. The lowest limit is
called LOMEM and the highest limit is called HIMEM, everything between these
boundaries is termed "available" for use by anyone, the user or the OS.
Anyone can also claim memory, thus making private to them and not allowing
anyone else to use it. You do this by altering the value of LOMEM, HIMEM
should not be moved. Of course anyone claiming memory should first check
that there is enough memory available for the amount they want to claim for.
To take a common example, a downloadable routine which installs itself as a
extension routine, The download routine will first of all read LOMEM, read
HIMEM, check there is enough memory for the routine, download it to LOMEM
and call it, the downloaded routine will then relocate itself and alter
LOMEM so it is protected.
There is one other memory limit, this is called TOPMEM, this is the
absolute top limit of RAM, HIMEM will normally equal TOPMEM, but a future OS
may alter HIMEM to below TOPMEM. TOPMEM should be ignored ideally, and
HIMEM used.
OS Calls
The OS has calls (ie. a jump instruction which you call, which
then calls, via a RAM vector, a routine in the OS) setup near the top of
memory, through these calls you do everything associated with the
controller. The calls are are such:
OS_PRINTER at address &FFB9
OS_CALLOS at address &FFBC
OS_SENDBYTE at address &FFBF
OS_READBYTE at address &FFC2
OS_SENDJOB at address &FFC5
OS_READJOB at address &FFC8
OS_DECODEJOB at address &FFCB
The address is the one to call for the particular routine.
OS_PRINTER is reserved for future use.
OS_CALLOS calls upon the OS to do various things internally.
OS_SENDBYTE sends a byte out of the serial port.
OS_READBYTE reads a byte from the serial port.
OS_SENDJOB 'sends' a value to the JobCall caller.
OS_READJOB 'reads' a value from the JobCalls caller.
OS_DECODEJOB decodes the Job number held in A and acts on it.
Note none of these things should be used in interupt routines,
except OS_CALLOS, of any sort, except if YOU know what state the machine is
in and WHY.
Serial Port
The serial port on the controller is currently based on the 6850
UART, this need not bother you as the OS deals with the thing entirely by
itself. The chip is setup by the OS to always have a word format of 8n1 and
a baud rate of 9600, this translates to 960cps. The 6850 has two divide
rates for the baud rate, the OS only uses one, using the other one is beyond
the scope of this document and shouldn't be neccessary to use.
Only receive interupts are used, transmission of characters is done
on a polled basis.
As said, there are two calls OS_READBYTE and OS_SENDBYTE.
OS_READBYTE tests the serial input buffer and returns the character (if any
in the A register), the c flag states whether there was a character
returned.
Call : OS_READBYTE
Entry : A, X, Y, c, z = undefined
Exit : X, Y preserved
z = undefined
if c = 1 (no character received)
A is preserved
if c = 0 (character received)
A = character received
OS_SENDBYTE sends the character in the A register out of the serial
port, taking into consideration flow control etc.
Call : OS_SENDBYTE
Entry : A = character to send
X, Y, c, z = undefined
Exit : A, X, Y, c is preserved
z = undefined
OS_READJOB and OS_SENDJOB
To send/receive data from the user (either internally or via the
serial port) you call OS_SENDJOB/OS_READJOB for the next byte. If your
JobCall requires dynamic use of data, ie. interaction, then a check of the
enviroment should be done to check the call came from the serial port. One
example of this is the OS JobCall MultipleServer, which requires dynamic use
of sending a stream of data until a reaction from the user to tell it to
stop.
Call : OS_READJOB
Purpose: To read job value from the user
Entry : A, X, Y, c, z = undefined
Exit : A = byte read
X, Y is preserved
c, z = undefined
Note : When coming from the serial port it waits for a character to be
received, if you are doing a serial only call and wish to have a loop
checking for a character from the user while doing "your own thing", then
OS_READBYTE should be used. For internal calls this reads data out of
jobin_buf.
Call : OS_SENDJOB
Purpose: To give a job value back to the user
Entry : A = byte to give
X, Y, c, z = undefined
Exit : A, X, Y is preserved
c, z = undefined
Note : This will send either out to the serial port or place a byte in
jobout_buf. For internal calls this stores the value in jobout_buf, which
is allocated 128 bytes, no bound checking is done so sending more than 128
bytes to OS_SENDJOB will cause corruption of some memory.
OS_DECODEJOB
JobCalls in practise are really only used by the user, JobCalls
tend not to call other calls at all, infact you shouldn't really call
another call unless you were called from the serial port. If you do need to
call another JobCall then you do it like this; with each call you have
associated with it a number, and the call might require some data or give
some data back or both. With the serial port this is simple, you just send
data down and receive data as it is produced. You can't do this with
internal calls, and some calls will not let you call them from inside the
controller, you HAVE to call them from outside. But for the ones which do
let you, you do it with two data blocks, one for input TO the call and one
for output FROM the call. These are setup in a position in memory
(jobin_buf and jobout_buf), each one has a maximum of 128 bytes and this
should not be overflowed (OS_READJOB and OS_SENDJOB do NOT do any bound
checking). So to start with you place in the input block the data you want
to give the call (if any), call it, and then act on the data in the output
block (if any). The OS call OS_DECODEJOB is the one which actually calls
the JobCall handler and eventually the actual routine, you tell the JobCall
handler which call you want by placing the JobCode in the A register.
So for OS_DECODEJOB:
Call : OS_DECODEJOB
Entry : A = JobCode
X, Y, c, z = undefined
jobin_buf should contain any input data
Exit : A = flag
X = number of bytes given back by routine
Y = undefined
c = status, if set JobCall does not exist
z = status, if set JobCall cannot be called internally
jobout_buf contains any data sent from routine
A typical example would be:
LDA #readhimem ; JobCode for ReadHimem
JSR OS_DECODEJOB ; Call the JobCall handler
LDX jobout_buf ; Read HIMEM from buffer LSB
LDY jobout_buf+1 ; Read HIMEM from buffer MSB
STX jobin_buf ; Place LSB in input buffer
STY jobin_buf+1 ; Place MSB in input buffer
LDA #writelomem ; JobCode for WriteLomem
JSR OS_DECODEJOB ; Call the JobCall handler
which would read HIMEM, and then write LOMEM to be the same value, leaving
no spare memory left.
The reason why you should not call an internal call from another
internal call is that the same buffers (jobin_buf and jobout_buf) would be
used for both calls, causing conflict between the two calls, or rather to
the originator of the primary call ! The only time it would be safe if is
if you have already got all the data you want from jobin_buf and haven't
placed any in the jobout_buf (it would be lost if you had done).
CallOS
This OS call does various calls which are completely linked to the
OS, everything from relocating code to reading and writing the hardware
ports.
As normal, a "function" request value is placed in the A register,
any extra data is put in the X and Y registers and a call to OS_CALLOS is
made, and data is passed back in X, Y, c and z. The A register will return
0 for all routines except for calls 0 (deemed always to exist anyway), 12 (A
is reserved for future use), 14 (returns 1 instead) and 15 (12, 14 and 15 do
not return 12, 14, 15 in A though), if the routine passes back the same
value in A (the exception being call 0) as you gave it then that OS_CALLOS
function is not supported.
CALLOS : 0
Purpose: Returns the OS version number
Entry : A = 0
X, Y, c, z = undefined
Exit : A = Hardware release version
XY = OS version number
c, z is preserved
Note : Hardware version 0 is obsolete
Hardware version 1 is SmartBox
VIA at &8030
UART at &8010
ADC at &8000
AUX_PORT (inputs) at &8020
CALLOS : 1
Purpose: To claim a block of JobCodes
Entry : A = 1
X = number of JobCodes required
Y, c, z = undefined
Exit : A = 0
X = base JobCode, if 0, no big enough block available
Y = Job ID number, 0 if no big enough block
c = set if no big enough block
z is preserved
Note : This call claims JobCodes codes from the JobCode list. JobCodes
are in the range 0 to 255, 0 is special and is the JobCall "Blank". All the
OS ones are spaced out but an "application" can claim a block of JobCodes,
usually just the 1 call. If there is not a contigous block big enough then
no JobCodes are allocated and the appropiate result is returned. The Job ID
is from 0 to 255, 0 is unallocated, 1 is the OS calls, the rest are external
applications, each successful call increments the Job ID returned, meaning a
maximum of 255 successful calls (not important as their is not that many
free calls available !), the Job ID can be used to check whether a JobCall
request is yours.
CALLOS : 2
Purpose: To handle NameCode request for the application.
Entry : A = 2
XY = block pointing to block of JobNames
c, z = undefined
jobin_buf contains JobName (terminated by CR)
zero_gp3 = JobId (as returned by CALLOS 1)
Exit : A = 0
XY = undefined
z is preserved
if c = 1 (Name not found)
if c = 0 (Name found)
jobin_buf contains the JobCode
Note : Used via the internal_vec to convert from JobName to job code, the
table format is just a list of the JobNames (each one CR terminated)
terminated by a &ff
CALLOS : 3
Purpose: To handle CodeName request for the application.
Entry : A = 3
XY = block pointing to block of JobNames
c, z = undefined
jobin_buf contains JobCode
zero_gp3 = job id (as returned by CALLOS 1)
Exit : A = 0
XY = undefined
z is preserved
if c = 1 (JobCode not found)
if c = 0 (JobCode found)
jobin contains JobName (terminated by CR)
Note : See above
CALLOS : 4
Purpose: To relocate code
Entry : A = 4
XY = execution address (offset from start of code)
c, z = undefined
zero_gp1 = offset address of execution after relocation
zero_gp2 = offset address of relocation BitMap
zero_gp3 = length of code to relocate
Exit : No exit, calls begining of code straight away
Note : See later section on Relocation
CALLOS : 5
Purpose: To "describe" the enviroment
Entry : A = 5
X, Y, c, z = undefined
Exit : A = 0
c, z is preserved
XY = address of table, format as such:
1st byte: number of bytes (ie. 14 currently)
byte 2+:
address of VIA
address of ACIA
address of ADC
address of AUX_PORT
address of jobs_status
address of jobin_buf
address of jobout_buf
Note : It is preferred that this call is consulted first if direct use of
the hardware is going to be used to get the correct addresses, or use CALLOS
0 and decide what addresses/configuration to use from the Hardware version
number.
CALLOS : 6
Purpose: To read a register in the VIA
Entry : A = 6
X = register (0 to 15)
Y, c, z = undefined
Exit : A = 0
Y = value read
X, c, z is preserved
Note : The register number is not checked for range. This call allows
access to the hardware without actually directly accessing the hardware
yourself.
CALLOS : 7
Purpose: To write to a register in the VIA
Entry : A = 7
X = register (0 to 15)
Y = value to write
c, z = undefined
Exit : A = 0
X, Y, c, z is preserved
Note : See above
CALLOS : 8
Purpose: To read a register in the ACIA
Entry : A = 8
X = register (0 to 15)
Y, c, z = undefined
Exit : A = 0
Y = value read
X, c, z is preserved
Note : See above
CALLOS : 9
Purpose: To write to a register in the ACIA
Entry : A = 9
X = register (0 to 15)
Y = value to write
c, z = undefined
Exit : A = 0
X, Y, c, z is preserved
Note : See above
CALLOS : 10
Purpose: To read a register in the ADC
Entry : A = 10
X = register (0 to 15)
Y, c, z = undefined
Exit : A = 0
Y = value read
X, c, z is preserved
Note : See above
CALLOS : 11
Purpose: To write to a register in the ADC
Entry : A = 11
X = register (0 to 15)
Y = value to write
c, z = undefined
Exit : A = 0
X, Y, c, z is preserved
Note : See above
CALLOS : 12
Purpose: To read a ADC channel
Entry : A = 12
X = channel number to read (0 to 3)
Y, c, z = undefined
Exit : A = reserved for future use
if c = 0 (8 bit reading)
Y = reading
X = undefined
if c = 1 (16 bit reading)
X = reading (LSB)
Y = reading (MSB)
z = undefined
Note : The A register MAY not return 0, it is guaranteed though that it
will not return 12.
CALLOS : 13
Purpose: To read a register in the AUX_PORT
Entry : A = 13
X = register (0 to 15)
Y, c, z = undefined
Exit : A = 0
Y = value read
X, c, z is preserved
Note : See above
CALLOS : 14 (OS 2.066+)
Purpose: To start sensor type checking
Entry : A = 14
X, Y, c, z = undefined
Exit : A = 1
Y = value read
X, c, z is preserved
Note : A returns 1. This flags the ADC routines to start checking the
sensors. After calling this routine, you should poll CALLOS 15 to check
when all the sensors have been checked.
CALLOS : 15 (OS 2.066+)
Purpose: To check the status of sensor checking
Entry : A = 15
X, Y, c, z = undefined
Exit : A = status
XY = pointer to sensor types block
c, z is preserved
Note : A does not return 0, though it is guaranteed to not return 15.
Status of 0 = checking finished and no futher check pending
Status of 1 = Waiting for current ADC channel to finish converting
before switching sensor checking on
Status of >127 = Checking sensor types
The block pointed to by XY consists of 4 bytes, each byte
contains the sensor type for the appropiate sensor, a type
of 0 means no sensor present
CALLOS : 16 (OS 2.069+)
Purpose: To set the OS' irq mask for the VIA
Entry : A = 16
X = EOR mask
Y = AND mask
c, z = undefined
Exit : A = 0
X = old mask value
Y = new mask value
c, z is preserved
Note :
CALLOS : 17 (OS 2.069+)
Purpose: To set the OS' irq mask for the ACIA
Entry : A = 17
X = EOR mask
Y = AND mask
c, z = undefined
Exit : A = 0
X = old mask value
Y = new mask value
c, z is preserved
Note :
CALLOS : 18 (OS 2.069+)
Purpose: To set the OS' irq mask for the ADC
Entry : A = 18
X = EOR mask
Y = AND mask
c, z = undefined
Exit : A = 0
X = old mask value
Y = new mask value
c, z is preserved
Note :
CALLOS : 19 (OS 2.069+)
Purpose: To set the ACIA's ctrl register and OS' soft copy
Entry : A = 19
X = EOR mask
Y = AND mask
c, z = undefined
Exit : A = 0
X = old mask value
Y = new mask value
c, z is preserved
Note :
CALLOS : 20 (OS 2.069+)
Purpose: To set the reset vector for battery back RAM support
Entry : A = 20
XY = address to set reset vector or 0 for read only
c, z = undefined
Exit : A = 0
XY = old reset vector address
c, z is preserved
Note : Sets the reset vector to address supplied and sets check bytes in
workspace.
Vectors
Some parts of the system are controlled by vectors, vectors also
exist to patch the normal operation of some parts of the system. Vectors
are RAM pointers, which are called to point to the routine to use. Being in
RAM this means that they can be altered by the user to allow the user to
modify the operation of part, or all, of a system call. All the call
routines (eg. OS_DECODEJOB, OS_READBYTE etc.) are called via a RAM vector.
There are vectors which the user patches to pick up unknown jobcodes, and to
service requests like NameCode/CodeName etc.
The vectors are placed at address &200 in memory, each are 2 bytes
long.
brk_vec at address &200
nmi_vec at address &202
irq_vec at address &204
irq2_vec at address &206
sendserial_vec at address &208
readserial_vec at address &20A
sendjob_vec at address &20C
readjob_vec at address &20E
decode_job_vec at address &210
unknownjob_vec at address &212
extjob_vec at address &214
centisec_vec at address &216
internal_vec at address &218
callos_vec at address &21A
printer_vec at address &21C
reset_vec at address &21E
The brk_vec is called whenever there is a BRK error, which should
not occur under normal use, as the controller OS does not make use of BRK
errors.
The nmi_vec is called whenever there is a NMI request (not really
applicable, as the NMI line is not connected to anything).
The irq_vec is called whenever there is an interupt.
The irq2_vec is called whenever an unknown interupt is encountered,
ie. didnXt come from the controllerXs VIA, ACIA or ADC, or the interupt
from the VIA was not used by the OS.
The sendserial_vec is called when OS_SENDBYTE is called.
The readserial_vec is called when OS_READBYTE is called.
The sendjob_vec is called when OS_SENDJOB is called.
The readjob_vec is called when OS_READJOB is called.
The decodejob_vec is called when OS_DECODEJOB is called. Note that
OS_DECODEJOB first calls a OS routine which sets a flag to say it's been
called internally, JobCode requests the OS gets from the serial port are
called via this vector, with a flag saying "from the serial port".
The unknownjob_vec is called whenever an unknown JobCode is
encountered.
The extendedjob_vec is called whenever the ExtendedJob is called,
the A register holds the extension value.
The centisecond_vec is called 100 times a second from the IRQ
routine from interupts off timer 1 of the VIA. The OS has its pulsing
routines on the end of this. Note that you should return with a RTS, not
RTI, you may also corrupt any of the registers.
The internal_vec is called when some information is needed from
various parts of the system, which includes the OS whch lies on the end of
this vector. An example is NameCode, which calls this vector to ask
everybody if they recognise the JobName in question.
The callos_vec is called when OS_CALLOS is called.
The printer_vec is called when OS_PRINTER is called.
The reset_vec is called when the OS gets a reset and the internal
check bytes flag the integrity of the RAM. It is first called with C
cleared for everything to setup vectors and then called with C set for a
foreground "language" application to start up. Use CALLOS 20 to set this
vector.
All vectors should be 'daisy chained', ie. any routine which uses
any of them should store the current value in its own workspace and alter
the vector and at the end of the routine which services the vector should
jump to the original saved value, if this isnXt done the system may 'fall'
down.
unknown, extendjob and irq2 all point to (in the present OS) to a
routine which just returns. brk in the current OS just resets the stack and
jumps back to the OS' main idle loop.
Irqs are reenabled before brk_vec is called
Irq_vec
This is called when the 65c02 receives an irq from a device. As the
65c02 signals brk and irq through the same cpu vector the OS first finds out
which one it really was and calls either brk_vec or irq_vec. It also stores
the A reg at address &a0, as the A reg is corrupted while it finds out which
vector to really call, any irq routine which has claimed the irq should
restore the A reg from &a0 before returning via a RTI, else you should pass
on the call to the old vector owner.
irq2_vec is called by the OS irq routine if it does not find a irq
it can service (or as been masked out) in as-good-as the same conditions as
irq_vec is called. The OS has a default irq2_vec handler of restoring A
from &a0 and doing a RTI.
Internal_vec and Unknownjob_vec
The main operation of extended JobCalls rely on patching some
vectors, two infact. These two are unknownjob_vec and internal_vec.
unknownjob_vec is the one which the OS calls to actually tell everybody that
a JobCall has been requested which it does not know about (the JobCall MUST
have been claimed before hand with CALLOS, function 1, or else it is not
passed on). internal_vec is the one which the OS calls in request to a
NameCode or CodeName.
The vectors are defined as thus:
internal_vec
Function: 0
Purpose: Nothing
Entry : A = 0
X, Y, c, z = undefined
Exit : Leave everything as it is and exit straight away
Note : Used for claimed calls
Function: 1
Purpose: NameCode request
Entry : A = 1
X, Y, c, z = undefined
jobin_buf contains JobName (CR terminated)
Exit : if JobName recognised then
A = 0
first byte of jobin_buf contains matched JobCode
if JobName not recognised then
A = 1
X, Y, c, z = undefined
Note :
Function: 2
Purpose: CodeName request
Entry : A = 2
X, Y, c, z = undefined
jobin_buf contains JobCode
Exit : if JobCode recognised then
A = 0
jobin_buf contains matched JobName (CR terminated)
if Code not recognised then
A = 2
X, Y, c, z = undefined
Note :
All other calls (ie. not 0, 1 or 2 should be passed on straight
away for future expansion)
unknownjob_vec
Purpose: To pass on unknown JobCalls to the correct owner
Entry : A = enviroment (0 = internal call, 1 = serial call)
Y = JobCode
X = JOB ID of owner
c, z = undefined
Exit : if jobcode yours then
if wrong enviroment then
A = 1
Y = 0
X, c, z = undefined
if right enviroment then
perform function
A = 0
Y = 0
X, c, z = undefined
if jobcode not yours then
pass on with registers unaltered
Note :
Starting Up
The setup procedure for setting up extension job calls is to:
a) Claim the number of JobCodes you want
if success:
b) patch the internal_vec and unknownjob_vec vectors
c) move LOMEM up to protect your program
d) do anything else for initial startup you may want to do
e) return back, ready and waiting
This in code looks like:
execute
LDX #no_of_calls ; Number of JobCodes wanted
LDA #1 ; CALLOS, function 1, Claim JobCodes
JSR OS_CALLOS ; Call CALLOS
CPX #0 ; Check we were given some codes
BEQ noinstall ; No, dont bother installing ourself
STX call ; Store base address of JobCodes allocated
STY job_id ; Store JOB ID given
LDA internal_vec ; Get old internal_vec (LSB)
STA ovec ; Store it
LDA internal_vec+1 ; Get old internal_vec (MSB)
STA ovec+1 ; Store it
LDA #>internal_handle ; Our internal handler (LSB)
STA internal_vec ; Place it in the vector (LSB)
LDA #<internal_handle ; Our internal handler (MSB)
STA internal_vec+1 ; Place it in the vector (MSB)
LDA unknownjob_vec ; Get old unknownjob_vec (LSB)
STA ovec2 ; Store it
LDA unknownjob_vec+1 ; Get old unknownjob_vec (MSB)
STA ovec2+1 ; Store it
LDA #>job_handle ; Our unknown job handler (LSB)
STA unknownjob_vec ; Place it in the vector (LSB)
LDA #<job_handle ; Our unknown job handler (MSB)
STA unknownjob_vec+1 ; Place it in the vector (MSB)
LDX #>WriteLomem ; Point to JobName
LDY #<WriteLomem
JSR findjob ; Find WriteLomem code
BEQ noinstall ; Doesn't exist !
LDX #>end_of_code ; End of code (LSB)
LDY #<end_of_code ; End of code (MSB)
STX jobin_buf ; Place it in jobin_buf (LSB)
STY jobin_buf+1 ; Place it in jobin_buf+1 (MSB)
JSR OS_DECODEJOB ; Call JobCall handler
noinstall
RTS ; End - return to OS
findjob
STX zero.gp1 ; Zero page
STY zero.gp1+
LDY #0
findthisjoblo
LDA (zero.gp1),Y
STA jobin.buf,Y ; Store name in jobin_buf
INY
CMP #13
BNE findthisjoblo
LDA #3 ; NameCode
JSR OS.DECODEJOB ; Call NameCode
LDA jobout.buf ; Get returned value
RTS
WriteLomem STR "WriteLomem"
This will setup the various vectors and memory pointers for the
applicaion, the actual routines (internal_handle and job_handle) are fairly
straight forward and simple:
internal_handle
CMP #1 : Is it function call 1 (NameCode) ?
BNE internal_handle_2 ; No, check for function 2
LDA job_id ; Get our JOB ID number
STA zero_gp3 ; Place it in zero_gp3 for CALLOS
LDX #>job_names ; Our Job name table (LSB)
LDY #<job_names ; Our Job name table (MSB)
LDA #2 ; CALLOS function 2
JSR OS_CALLOS ; Call CALLOS
BCC internal_yes : Found, go off and claim call
LDA #1 ; Not found, restore A
JMP (ovec) ; Call back to old vector
internal_handle_2
CMP #2 ; Is it function call 2 (CodeName) ?
BNE internal_handle_3 ; No, unknown, pass on
LDA job_id ; Get our JOB ID
STA zero_gp3 ; Place it in zero_gp3 for CALLOS
LDX #>job_names ; Our Job name table (LSB)
LDY #<job_names ; Our Job name table (MSB)
LDA #3 ; CALLOS function 3
JSR OS_CALLOS ; Call CALLOS
BCC internal_yes ; Found, go off and claim call
LDA #2 ; Not found, restore A
JMP (ovec) ; Call back to old vector
internal_yes
LDA #0 ; We want to claim call for some reason
internal_handle_3
JMP (ovec) ; Call back to old vector
job_handle
PHA ; Mark sure to preserve A
CPY call ; Check call wanted against base of ours
BCC job_handle_no ; Less than our base, so definately not ours
TYA
SBC #no_of_calls ; Subtract number of calls we have
CMP call ; Now compare with base
BCC job_handle2 ; Yes, one of ours
job_handle.no
PLA ; Restore A
JMP (ovec2) ; Call back to old vector
job_handle2
TYA
SEC
SBC call ; Subtract our base from call
ASL A ; Times by 2 for offset into table
TAY ; Place in Y
LDA job_run_table,Y ; Get LSB of routine
STA zero_gp1 ; Store it for indirect jump (LSB)
LDA job_run_table+1,Y ; Get MSB of routine
STA zero_gp1+1 ; Store it for indirect jump (MSB)
PLA ; Restore A
JMP (zero_gp1) ; Call our relevant routine
job_names ; List of our JobNames
STR "TestJob" ; Test job name
DFB &FF ; &FF - marks end of table
job_run_table ; List of routine addresses to match Jobs
DFW job_TestJob ; Test job routine
Relocation
The relocation method used will not do a complete relocation, ie.
from ANY address to ANY address. The only constraint on the addresses is
that they must be the same offset in the page, which means, &2602 and &3402
is alright but &2602 and &3455 is not, ie. the difference between the two
addresses must be a multiple of 256 bytes, as only the MSB is altered, this
should not cause any real problems. What is meant by the two addresses is
the address the code was originally assembled at and the address it wants
relocating to. Because of this programs must be multiples of 256 bytes, ie.
the low byte of LOMEM must ALWAYS be 0, your program should be padded out.
The relocation mechanism works by having a table, and to each byte
of code there is one BIT, if unset it means the byte of code should not be
relocated, else it should be, thus the BitMap table is 8 times smaller than
the code. To generate the BitMap you have to assemble the code at two
different places, and compare both sets of assembled code against each
other, the differences are where the code is to be relocated. The code
which is sent up to the controller is assumed to be assembled at address
&100, so the first code should be assembled at &100, the other one should be
assembled at another address, say &600, and the first one be sent up with
the BitMap. A program is availble for the Elk/Beeb/Arc with 65Tube/Mac with
BBC BASIC to compare the two files and create the BitMap.
All the program has to do to relocate itself is execute this bit of
code first of all: (address is the base address of the program, ie. &100
for the downloaded version)
execcall
LDA #>(execute-address) ; Final execution address offset (LSB)
STA zero_gp1
LDA #<(execute-address) ; Final execution address offset (MSB)
STA zero_gp1+1
LDA #>(BitMap-address) ; BitMap address offset (LSB)
STA zero_gp2
LDA #<(BitMap-address) ; BitMap address offset (MSB)
STA zero_gp2+1
LDA #>(execcall-address); Length of data to relocate (LSB)
STA zero_gp3
LDA #<(execcall-address); Length of data to relocate (MSB)
STA zero_gp3+1
TXA ; Execcall REAL address
SEC
SBC #>(execcall-address); Subrtract length
TAX ; Put it back in X
TYA ; Ditto for MSB
SBC #<(execcall-address)
TAY
LDA #4 ; CALLOS function for Relocate
JMP OS_CALLOS ; Call CALLOS
BitMap ; BitMap start
This will relocate the code and automatically call the execution
address, which should be the initialisation code (ie. setup internal_vec
and unknownjob_vec etc.). "address" is the address the code is started to
assemble at, &100 for the version to be uploaded. The upload routine should
pass in X and Y (via ExecuteCode) the address of the start of execcall, this
means that the routine can tell where it is in memory, subtracting the
length gives the start of the code to relocate.
Layout Summary
The layout of a full application should be like this by now:
start of code
program job codes, data etc.
internal handler
job handler
end of code
initialisation (setup internal etc.)
end of code to be relocated
execution (relocation)
relocation BitMap
Because of memory space the initialisation code should be placed
outside the reserved memory limit of the program, as it is only called once
and not needed again.
Other miscellaneous calls:
Call : OS_PRINTER
Purpose : To place a character in the printer buffer
Entry : A = character to send
X, Y, c, z = undefined
Exit : A, X, Y is preserved
if c = 0 (printed)
The printer is on and the character was inserted
if c = 1 (not printed)
The printer is off and the character was forgotten
z = undefined
Note : The character goes into the printer buffer and will be
actually sent to the printer when the printer asks for it. If the printer
was not awake to begin with, an attempt is made to wake it up, thus
meaning that in some circumstances when the printer wasn't ready a
character may be lost, this cannot be helped. If the printer is off (via
the BCD switch) then the character will be forgotten as the Printer Buffer
is inactive.
The module is located at &8000 in memory, and should be a EPROM placed
in the second ROM socket in the controller (ie. the top RAM socket). This
module is primary designed to turn the Controller into a dedicated task,
making it perform some function from switch on. It could also be used to
add more permant JobCalls to the Controller, in the process taking up much
less RAM), but because of there being only one Module present this feature
is limited, and is much more suited to a collection of the user's own
personal routines.
For the OS to recognise a module as being in place the text "Module" is
looked for, the layout of a module should be like this (starting at
&8000):
Offset value comment
0 "Module" The OS checks for this string
6 0 End of check string
7 Language entry Address of 'Language' entry
9 Service entry Address of 'Service' entry
11 "<Module Title>" Module title
0 End of Module title
"<Module part>" Part Module Title
0 End of Part Module Title
"x.xx" Part version number
&FF End of parts list
The language entry is a 2 byte value holding the address of the language
entry routine, this is called on reset, if the BCD is set correctly. A
value of less than &8000 means there is no valid language entry and any
language calls are ignored. If a RTS is made the OS carries on as it would
have if there wasn't a language call.
The service entry is a 2 byte value holding the address of the service
entry routine, this is called at various appropiate moments with service
numbers in A. A list of current calls is thus:
0 - Not used
1 - Unknown Job Code
2 - Centisecond call
3 - Irq2 (unknown IRQ)
4 - internal_vec call (exit with A=0 to claim)
254 - RESET
255 - BRK
On exit all registers and flags should be preserved. For call 1 the
unknown job value is held in the Y register, the enviroment is held in the
X register, on exit, X should contain the flag, as A does from the exit of
unknownjob_vec. For the rest of calls, X and Y are undefined.
For both Service and Language entry points, a value of 0 means "there is
no valid routine" and the module isn't called for that type of entry.
Finally a full implementation of a JobCall, from start to finish.
DIM data% &1000
:
no_of_calls = 1
:
VIA = &E030
ACIA = &E010
ADC = &E000
AUX_PORT = &E020
brk_vec = &200
nmi_vec = &202
irq_vec = &204
irq2_vec = &206
sendserial_vec = &208
readserial_vec = &20A
sendjob_vec = &20C
readjob_vec = &20E
decodejob_vec = &210
unknownjob_vec = &212
extjob_vec = &214
centisec_vec = &216
internal_vec = &218
callos_vec = &21A
printer_vec = &21C
zero_gp1 = 0
zero_gp2 = 2
zero_gp3 = 4
zero_gp4 = 6
zero_gp5 = 8
zero_gp6 = 10
zero_gp7 = 12
zero_gp8 = 14
zero_gp9 = 16
zero_gp10 = 18
user_reserved = &70
irq_A = &A0
fcount = &A1
RAM_size = &A3
jobout_buf = &400
jobin_buf = &480
OS_PRINTER = &FFB9
OS_CALLOS = &FFBC
OS_SENDBYTE = &FFBF
OS_READBYTE = &FFC2
OS_SENDJOB = &FFC5
OS_READJOB = &FFC8
OS_DECODEJOB = &FFCB
:
FOR create=1 TO 2
:
FOR pass%=4 TO 7 STEP 3
P%=&100*create : O%=data%
[OPT pass%
:
.job_DemoJob
CMP #1
BEQ DemoJob_go
LDA #1
LDY #0
RTS
/
.DemoJob_go
:
.internal_handle
CMP #1 ; Is it function call 1 (NameCode) ?
BNE internal_handle_2 ; No, check for function 2
/
LDA job_id ; Get our JOB ID number
STA zero_gp3 ; Place it in zero_gp3 for CALLOS
LDX #job_names MOD 256 ; Our Job name table (LSB)
LDY #job_names DIV 256 ; Our Job name table (MSB)
LDA #2 ; CALLOS function 2
JSR OS_CALLOS ; Call CALLOS
BCC internal_yes ; Found, go off and claim call
LDA #1 ; Not found, restore A
JMP (ovec) ; Call back to old vector
/
.internal_handle_2
CMP #2 ; Is it function call 2 (CodeName) ?
BNE internal_handle_3 ; No, unknown, pass on
/
LDA job_id ; Get our JOB ID
STA zero_gp3 ; Place it in zero_gp3 for CALLOS
LDX #job_names MOD 256 ; Our Job name table (LSB)
LDY #job_names DIV 256 ; Our Job name table (MSB)
LDA #3 ; CALLOS function 3
JSR OS_CALLOS ; Call CALLOS
BCC internal_yes ; Found, go off and claim call
LDA #2 ; Not found, restore A
JMP (ovec) ; Call back to old vector
/
.internal_yes
LDA #0 ; We want to claim call for some reason
/
.internal_handle_3
JMP (ovec) ; Call back to old vector
:
.job_handle
PHA ; Make sure to preserve A
CPY call ; Check call wanted against base of ours
BCC job_handle_no ; Less than our base, so definately not ours
TYA
SBC #no_of_calls ; Subtract number of calls we have
CMP call ; Now compare with base
BCC job_handle2 ; Yes, one of ours
/
.job_handle.no
PLA ; Restore A
JMP (ovec2) ; Call back to old vector
/
.job_handle2
TYA
SEC
SBC call ; Subtract our base from call
ASL A ; Times by 2 for offset into table
TAY ; Place in Y
LDA job_run_table,Y ; Get LSB of routine
STA zero_gp1 ; Store it for indirect jump (LSB)
LDA job_run_table+1,Y ; Get MSB of routine
STA zero_gp1+1 ; Store it for indirect jump (MSB)
PLA ; Restore A
JMP (zero_gp1) ; Call our relevant routine
:
.job_names ; List of our JobNames
EQUS "DemoJob":EQUB 13 ; Test job name
/
EQUB &FF ; &FF - marks end of table
:
.job_run_table ; List of routine addresses to match Jobs
EQUW job_DemoJob ; Test job routine
:
.call : EQUB 0 ; Store for our JobCode base
.job_id : EQUB 0 ; Store for our JobId
.ovec : EQUW 0 ; Store for old internal_vec value
.ovec2 : EQUW 0 ; Store for old unknownjob_vec value
:
EQUS STRING$(&100-(P% MOD 256),CHR$0)
.end_of_code ; end_of_code should be on page boundry
:
.execute
LDX #no_of_calls ; Number of JobCodes wanted
LDA #1 ; CALLOS, function 1, Claim JobCodes
JSR OS_CALLOS ; Call CALLOS
STX call ; Store base address of JobCodes allocated
STY job_id ; Store JOB ID given
CPX #0 ; Check we were given some codes
BEQ noinstall ; No, don't bother installing ourself
/
LDA internal_vec ; Get old internal_vec (LSB)
STA ovec ; Store it
LDA internal_vec+1 ; Get old internal_vec (MSB)
STA ovec+1 ; Store it
LDA #internal_handle MOD 256 ; Our internal handler (LSB)
STA internal_vec ; Place it in the vector (LSB)
LDA #internal_handle DIV 256 ; Our internal handler (MSB)
STA internal_vec+1 ; Place it in the vector (MSB)
/
LDA unknownjob_vec ; Get old unknownjob_vec (LSB)
STA ovec2 ; Store it
LDA unknownjob_vec+1 ; Get old unknownjob_vec (MSB)
STA ovec2+1 ; Store it
LDA #job_handle MOD 256 ; Our unknown job handler (LSB)
STA unknownjob_vec ; Place it in the vector (LSB)
LDA #job_handle DIV 256 ; Our unknown job handler (MSB)
STA unknownjob_vec+1 ; Place it in the vector (MSB)
/
LDX #end_of_code MOD 256 ; End of code (LSB)
LDY #end_of_code DIV 256 ; End of code (MSB)
STX jobin_buf ; Place it in jobin_buf (LSB)
STY jobin_buf+1 ; Place it in jobin_buf+1 (MSB)
LDA #65 ; JobCode for WriteLomem
JSR OS_DECODEJOB ; Call JobCall handler
/
.noinstall
RTS ; End - return to OS
:
.execcall
LDA #>(execute-address) ; Final execution address offset (LSB)
STA zero_gp1
LDA #<(execute-address) ; Final execution address offset (MSB)
STA zero_gp1+1
/
LDA #>(BitMap-address) ; BitMap address offset (LSB)
STA zero_gp2
LDA #<(BitMap-address) ; BitMap address offset (MSB)
STA zero_gp2+1
/
LDA #>(execcall-address) ; Length of data to relocate (LSB)
STA zero_gp3
LDA #<(execcall-address) ; Length of data to relocate (MSB)
STA zero_gp3+1
/
TXA ; Execcall REAL address
SEC
SBC #>(execcall-address) ; Subrtract length
TAX ; Put it back in X
TYA ; Ditto for MSB
SBC #<(execcall-address)
TAY
LDA #4 ; CALLOS function for Relocate
JMP OS_CALLOS ; Call CALLOS
/
.BitMap ; BitMap start
:
]
NEXT
:
OSCLI "SAVE code"+STR$create+" "+STR$~data%+" "+STR$~O%
:
NEXT
Changelog.
The origins of the SmartBoxes was the RESOURCE Controller - this was codenamed Albert.
After RESOURCE died, a colleague and I from RESOURCE went on to design further control boxes via Economatics.
"bob" was the first (SB-01) using the 65c02 "jim" was the SmartBox Discovery using a 6502-like Mitsubishi 740, which had LCD, Printer and Battery. "bill" was a heavily cut down box with very little RAM using a Mitsubishi 375 (SB-04).
There were also other unreleased variants ("dk8", "etui").