A Network Extension for Applesoft BASIC
Michael J. Mahon – November 14, 2004
Introduction
The low-level implementation details of NadaNet have been separately described in A Native Network for the Apple II. The purpose of this document is to present an "Applesoft programmer level" interface to NadaNet services, as implemented in a suite of ampersand commands.
The NadaNet package natively uses a small internal memory area to hold parameters to the various operations and to receive results. While this memory interface is convenient for machine language programmers, it is relatively burdensome to Applesoft BASIC programmers.
In the initial release of the software, Applesoft programmers had to execute multiple POKE statements to modify the NadaNet parameter area, CALL the desired operation, and then use PEEK statements to retrieve any returned values. The fact that many parameters are 16-bit quantities and PEEK and POKE are 8-bit operations made it even more cumbersome. In practice, this meant several lines of BASIC for each NadaNet service request, resulting in larger, slower programs that were harder to read and write.
Since version 1.1, NadaNet has incorporated an extension to Applesoft, using the "&" interface to pass parameters and invoke the services. In most cases, NadaNet service requests can now be done in a single BASIC statement, greatly improving program size, speed, and clarity. Another very useful advantage of the ampersand interface is that NadaNet commands can be entered directly at the keyboard, allowing easier interaction with remote machines.
Examples of usage of the ampersand interface, with commentary, are shown in The AppleCrate Parallel Work Simulator and in the File Server.
NadaNet Capabilities
Nadanet is a compact (about 2KB) package of library routines providing reliable peer-to-peer network services for 8-bit Apple II computers. The underlying communication medium is a twisted pair or a shielded cable. The practical data rate is over 10K bytes per second.
The services allow programs to transfer data from local memory to remote memory (&POKE), and from remote memory to local memory (&PEEK). It is also possible to cause a remote machine to execute specific code within its memory (&CALL).
Because a machine can only receive a message when it is actively polling the network, there is real benefit in dedicating at least one machine as a Message Server—a machine that is always awaiting requests to enqueue (&PUTMSG) or to dequeue (&GETMSG) messages in specified FIFO message queues. A message server eliminates the need for machines to rendezvous in order to exchange short messages, effectively making message passing asynchronous.
To more efficiently support the needs of parallel programmers, network atomic operations are provided to support the allocation of resources or work (&PEEKINC), and the broadcast notification of multiple machines that an event has occurred (&BPOKE).
These services have proven sufficient to allow several distributed programs to be written, including ATTACH, a facility for remotely operating networked machines (even if they lack keyboards or displays), and a File Server that allows NadaNet-connected clients to remotely use the server's ProDOS file system.
Network Commands
All commands accept parameters which specify the desired action. Most parameters are inputs to the command, and may be arbitrary numeric expressions. Some commands may contain an output parameter, which is a variable set upon successful completion of the command. The parameter list is enclosed in parentheses, and individual parameters are separated by commas. A parameter list may be empty "()", but null parameters (consecutive commas) are not permitted.
In all commands, trailing parameters may be omitted if they are irrelevant or if their values, as established by a prior command, need not be changed.
For example:
|
&CALL (3,ENTRY) |
(Normally has three parameters, AX parameter omitted) |
|
|
&TIMEOUT() |
(Has an optional parameter, here omitted) |
are acceptable, but not a null parameter:
|
&CALL (3,,44) |
(Null second parameter not permitted) |
Following is a description of each of the implemented network commands. In these descriptions:
|
‘dest’ is an expression evaluating to a machine ID in the range of 1..15. |
|
|
‘address’ is an expression evaluating to a 16-bit memory address. |
|
|
‘length’ is an expression evaluating to a 16-bit length in bytes |
|
|
‘locaddr’ is an expression evaluating to a 16-bit memory address. |
&SERVE# (iter)
Call the SERVER, waiting for up to ‘iter’ * 20 milliseconds. The command completes when a network request for this machine is processed, when any key is pressed, or when the iteration count is exhausted.
The parameter ‘iter’ is an arithmetic expression with a value between 0 and 255.
If the network is idle, each iteration requires about 20 milliseconds. If there is network activity, then the time per iteration is dependent on the type and frequency of requests. The default number of iterations is 256, which corresponds to an ‘iter’ value of 0.
A machine which processes other machines’ requests must spend a major fraction of its time in SERVER. This command provides an approximately timed way of serving. (As opposed to calling the SERVER loop at location 973 ($3CD), which re-calls SERVER forever—or until a request transfers control to some other activity.)
&SERVE can produce exceptions if a service routine or the routine &CALLed by a requesting machine returns with the Carry flag set. Since responsibility for request recovery lies with the requesting machine, the serving machine can take no meaningful action in these cases.
The "fail quiet" form of this command (&SERVE#) should be generally used. It is unnecessary for the programmer to examine exception status after a &SERVE# command.
&INIT ()
Initialize NadaNet with the machine ID stored in location 972 ($3CC). This call also initializes a JMP to the SERVER loop at 973 ($3CD) and prints the NadaNet version and current ID (in hex).
NadaNet is typically initialized prior to running an application, so &INIT is needed only to change a machine’s ID.
&INIT has no exceptions.
&TIMEOUT (time)
Set the request timeout interval to ‘time’, expressed in units of 60 milliseconds.
This can be used prior to a request which has a high probability of timing out, to reduce the time required to do so. For example, when probing the network to determine which machine IDs are serving, setting the timeout interval to a fraction of a second allows a relatively fast enumeration.
The default value of 50 (corresponding to 3 seconds) can be re-established by calling with a null parameter:
|
&TIMEOUT () |
&TIMEOUT has no exceptions.
&PEEK (dest, address, length, locaddr)
Move <length> bytes from machine <dest> memory at <address> to the local machine at <locaddr>.
The only exception is a timeout, indicating that the operation could not be carried out within the limit.
For example:
|
BUF = 8192 |
|
|
&PEEK (3,768,4,BUF+4) |
Transfers 4 bytes from machine 3’s address 768 to this machine’s address 8192.
&PEEK causes a timeout exception (PEEK(1)=1) if the ‘dest’ machine is not serving.
&POKE (dest, address, length, locaddr)
Move <length> bytes to machine <dest> memory at <address> from the local machine at <locaddr>.
The only exception is a timeout, indicating that the operation could not be carried out within the limit.
For example:
|
BUF = 8192 |
|
|
&POKE (6,768,4,BUF) |
Transfers 4 bytes to machine 6’s address 768 from this machine’s address 8192.
&POKE causes a timeout exception (PEEK(1)=1) if the ‘dest’ machine is not serving.
&CALL (dest, address, ax)
Load the A and X registers of machine <dest> and cause it to JSR to <address>.
The low byte of the 16-bit value <ax> is loaded into A and the high byte into X.
&CALL causes a timeout exception (PEEK(1)=1) if the ‘dest’ machine is not serving.
&PUTMSG (mserve, msgclass, msgleng, locaddr)
Enqueue the <msgleng>-byte message at <locaddr> into the <msgclass> queue on message server <mserve>.
The parameter ‘mserve’ is an expression evaluating to the machine ID of a Message Server, ‘msgclass’ is an expression evaluating to a 16-bit integer naming the desired message queue, and ‘msgleng’ is an expression evaluating to a message byte length in the range of 1 to 255.
&PUTMSG causes a timeout exception (PEEK(1)=1 and PEEK(0)=0) if the message server is not serving.
&PUTMSG causes a prompt exception (PEEK(1)=1 and PEEK(0)=1) if the message server is serving but cannot accept the message.
&GETMSG (mserve, msgclass, msgleng?, locaddr)
Dequeue the oldest message of class <msgclass> on message server <mserve> to our <locaddr>. If no exception occurs, the actual length of the message is returned in the variable <msgleng?>.
&GETMSG causes a timeout exception (PEEK(1)=1 and PEEK(0)=0) if the message server is not serving.
&GETMSG causes a prompt exception (PEEK(1)=1 and PEEK(0)=1) if the message server is serving but there is no waiting message.
&PEEKINC (dest, address, increment, oldval?)
Return the 16-bit value at <address> in machine <dest>, and increment it in <dest>’s memory by <increment>.
&PEEKINC causes a timeout exception (PEEK(1)=1) if the ‘dest’ machine is not serving.
&BPOKE (address, value)
Set the 2-bytes at ‘address’ in all serving machines to ‘value’.
This is a "broadcast" request acted upon by all machines that are serving.
&BPOKE has no exceptions.
&IDTBL(idaddr?)
[This command is available only when running on a ‘master’ version of NadaNet.]
Return the address of NadaNet’s 16-byte ‘idtable’ in the variable ‘idaddr?’ for use by the application.
The ‘idtable’ contains a 1-byte entry for each machine ID, from 1 to 15. The byte corresponding to machine X is at location ‘idtable’+X. The interpretation of the value is as follows:
|
Value |
Interpretation |
|
0 |
Not serving |
|
1 |
Booting |
|
2 |
AppleCrate machine |
|
3 |
Message Server |
|
4 |
ProDOS (master capable) |
|
5 |
DOS (master capable) |
&IDTBL has no exceptions.
&BOOTCODE (bootaddr, bootleng, locaddr)
[This command is available only when running on a ‘master’ version of NadaNet.]
Set up the parameters for network booting prior to a &SERVE call to accomplish the boot.
The ‘bootaddr’ and ‘bootleng’ parameters specify where the boot image will go in the booted machines and how long it is. The ‘locaddr’ parameter specifies the address in the master machine’s memory where the boot image is located.
If the master machine needs to stop offering boot service in order to reclaim the memory space occupied by the boot image, it may do so by issuing a &BOOTCODE (0,0) command, which sets the boot image length to zero, effectively cancelling boot service. In this mode, the master will still respond to GETID requests by allocating the requesting machines unique IDs, but no boot code will be sent, and unbooted machines will continue to try to boot.
&BOOTCODE has no exceptions.
Command Exceptions
Most commands can fail. For example, it may time out because the addressed machine is not serving requests. The default action in case of such an exception is a "DATA" error (number 49), which halts execution of the program unless it is "caught" by an active ONERR statement.
A command may also fail because the system state prevents it from completing. For example, a &PUTMSG command can fail if the message server is full, and cannot accept another message. Conversely, &GETMSG can fail because the specified message queue is empty.
If a command fails, any return variable specified in the command is not set.
If a command can be expected to fail in normal cases, such as attempting to &GETMSG when a queue may be empty, any command may be given in an "fail quiet" form, allowing the program to continue and explicitly inspect the status after execution to determine the appropriate action. The "fail quiet" form is specified by appending a "#" to the command name, for example:
|
&PEEK#(machine,address,length,locaddr) |
would typically be followed by:
|
IF PEEK(1) THEN … |
which examines the exception status of the command and takes some corrective action.
The completion status of commands is always stored in locations 1 (Carry) and 0 (A register). If PEEK(1) = 0, then the command completed without exception. If PEEK(1) = 1, then, for some commands, PEEK(0) may provide additional status information.
Serving Network Requests
The normal state of a "slave" machine is to be endlessly re-calling the SERVER whenever it is not doing some other task. The SERVER exits to the caller whenever a request is processed, a key is pressed, or the iteration count expires, so that the caller can perform other work. A 1-byte counter controls the internal iteration of the SERVER. Normally, SERVER is entered (and exits) with this counter equal to zero, so that SERVER iterates 256 times before returning to its caller. If desired, it can be preset to a different value to cause fewer iterations before returning. The approximate time for one iteration is 20ms., though this is dependent on network traffic.
For a machine with no other work to do except process network requests, NadaNet provides a SERVER Loop that re-calls SERVER each time it returns. This loop is entered by performing a CALL 973 (or JMP $3CD). Once a machine is in the SERVER Loop, it will exit only if directed to do so by serving a &CALL request that takes control elsewhere.
The Message Server
As has been noted, two machines can only communicate over NadaNet synchronously—when both machines rendezvous in time, with one receiving and the other sending. This normally happens because the machine receiving the request has been waiting for a message, and, while it is waiting, a sender sends.
This synchronous interaction is characteristic of any communication channel which cannot buffer incoming messages until the receiver is ready to receive them.
Synchronous interactions would be less troublesome if the receiver could be interrupted by an incoming message, and then receive it immediately. However, interrupts introduce a level of concurrency that complicates the interactions between the network software and the application. The current version of NadaNet does not use interrupts.
Another method of providing message buffering was devised, at the cost of dedicating one machine on the network as a "message server". The message server listens constantly to the network, serving all requests directed to it. In addition, it is provided with service routines for two commands: &PUTMSG and &GETMSG.
&PUTMSG sends a short (1- to 255-byte) message to a specified FIFO queue maintained in the memory of the message server. The queue is specified by a 16-bit "class" number. &GETMSG removes messages from the specified queue, delivering it to the receiver. These two operations provide message buffering and so permit machines to communicate indirectly without synchronization.
Messages sent between machines through the message server must traverse the network twice—once when enqueued and once when received.
The message server supports short messages. If a large block of data needs to be transferred, a message can be sent notifying the other party, and a rendezvous can then occur for the primary data transfer.
Experiments involving random message traffic among seven machines (and a message server) indicates that the network can support more than 60 queued and delivered messages per second, with correspondingly short response time.
Booting From The Network
Machines with little or no attached I/O can be modified to boot from the master machine (ID=1) on the network. The modification consists of replacing the self-test code in ROM with network boot code. This code has been developed for the unenhanced Apple //e, the type of machine used in the AppleCrate (see AppleCrate: An Apple II-Based Parallel Computer).
When a modified machine is powered on, it enters a loop continually announcing itself to the master machine. When the master machine enters its SERVER, it assigns the slave an ID and then, after a short delay, loads NadaNet into the slave and starts its SERVER Loop. The purpose of the short delay is to allow all slave machines needing boot code to announce themselves and get IDs assigned before sending the boot code. This allows the boot image to be broadcast to all booting machines at once.
For network boot to occur, the master machine must pre-load the boot image and set up its load address, length, and local address using &BOOTCODE, and then calling &SERVE for at least two seconds (an iteration count of 100 or more).
Compatibility
This version of NadaNet, with ampersand extensions for Applesoft BASIC, has been tested on enhanced and unenhanced Apple //e computers. It should be compatible with any 64K Apple II running Applesoft that has a 16-pin game port connector, including the IIgs running in 1MHz mode.
The only Apple II computers which will not run NadaNet are the Apple //c and Apple //c+, which do not have 16-pin game ports, and certain late-model enhanced Apple //e machines, which have (removable) capacitors bypassing their pushbutton inputs. More detail about the capacitor to remove can be found in A Native Network for the Apple II.
Since my Apple //e development machine has a Zip Chip accelerator installed, the NadaNet code contains instructions which will cause the Zip Chip (and most other accelerators) to slow down during the network code. The slowdown code does not attempt to control the chip directly, but simply makes a reference to a slot 6 Disk Controller I/O address. By default, most accelerators slow down for many milliseconds when such an address is referenced, since it is commonly used for a 5.25" disk controller, which requires a 1MHz speed. No actual disk controller needs to be installed in slot 6 for this to work, as long as the acceleration mechanism slows down on slot 6 accesses.
A version of NadaNet specifically for the Apple IIgs is under consideration, that will slow down to 1MHz speed while running timing-critical sections of NadaNet, and restore the prior speed when exiting. This will allow the computer to run at its control panel-selected speed outside of timing-critical code.