A Network Extension for Applesoft BASIC
Michael J. Mahon - November 14, 2004
Revised - September 23, 2015
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 and buffers for the various NadaNet requests 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 both to write and to read.
Soon after its initial release, NadaNet added 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.
NadaNet 3.1 adds a new ampersand command (&PEEKPOKE), and a new version of the &BOOT command which only implements the "passive" boot protocol using the NadaNet 3.x message format.
Examples of usage of the ampersand interface, with commentary, are shown in the AppleCrate Parallel Work Simulator, the AppleCrate Polyphonic Synthesizer, 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 shielded cable. The practical data rate is over 10K bytes per second.
The services allow programs to transfer arbitrary amounts of 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 a 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), the locking of shared memory resources ($amp;PEEKPOKE), 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 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..31. |
|
`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 usually 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 the "census" code in NADADEFS is 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's 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 8196.
&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's 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' to variable 'oldval?', and atomically add 'increment' to the value in 'dest's memory.
&PEEKINC causes a timeout exception (PEEK(1)=1) if the `dest' machine is not serving.
&PEEKPOKE (dest, address, value, oldval?)
Return the 16-bit value at 'address' in machine 'dest' to variable 'oldval?', and atomically set the value in 'dest's memory to 'value'.
&PEEKPOKE 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. At the conclusion of the request, all serving machines are synchronized within a tolerance of three machine cycles.
&BPOKE has no exceptions.
&BRUN (dest, address, length, locaddr)
Move 'length' bytes to machine 'dest's memory at 'address' from the local machine at 'locaddr', then give control to it at 'address'.
The only exception is a timeout, indicating that the operation could not be carried out within the limit.
For example:
BUF = 8192 : REM General purpose buffer |
Transfers the PLNG bytes of "PROG" to machine 6's address 768 from this machine's address 8192, then gives control to "PROG" on machine 6.
&BRUN causes a timeout exception (PEEK(1)=1) if the `dest' machine is not serving.
&RUN (dest, address, length, locaddr)
Move 'length' bytes to machine 'dest's memory at 'address' from the local machine at 'locaddr', then RUN it as a BASIC program.
The value of the second (address) parameter must be greater than 2048 ($800), but this is not checked by &RUN.
When the program ends, or does any operation resulting in an input prompt (such as a syntax error) the action taken depends on whether the machine is running an OS or not. If no OS is running (as for a 'Crate machine), any request for keyboard input results in control being returned automatically to the SERVER loop. If an OS is running, then the machine waits for keyboard input as usual, and does not serve the network. Therefore, if a BASIC program is to be &RUN on both types of machine, and you wish the machine always to continue serving after the program ends, the program must execute a CALL 973 at its completion to re-enter the SERVER loop.
The only error detected is a timeout, indicating that the operation could not be carried out within the limit.
For example:
BUF = 8192 : REM General purpose buffer |
Transfers the PLNG bytes of "BASICPROG" to machine 6's address 2049 ($801) from this machine's address 8192, then RUNs "BASICPROG" on machine 6.
&RUN causes a timeout exception (PEEK(1)=1) if the `dest' machine is not serving.
&BCAST (dataclass, length, locaddr)
Send 'length' bytes at 'locaddr' in the local machine to all serving machines awaiting data of the 16-bit class specified by 'dataclass'.
The action of &BCAST on a serving machine depends entirely on the assembly language receiving routine running in that machine. For example, CR.BPRUNNER only requires that the high byte of 'dataclass' equals $E0 to signal that an Applesoft BASIC program to be loaded at $801 follows. On the other hand, SYNTH.LOADER interprets the high byte of 'dataclass' as a "type" field and, if it is equal to $F1, it interprets the low byte as the voice number.
The interpretation of this field and the reception of the &BCAST data is completely at the discretion of the receiving routine. (Note that NADAUSER.S contains the beginning of a table of assigned &BCAST types.) See A Native Network for the Apple II and the programs mentioned above for additional details on the use of &BCAST.
This is a "broadcast" request seen by all machines that are serving.
&BCAST has no exceptions.
&IDTBL(idaddr?)
Return the address of NadaNet's `idtable' (32 bytes in NadaNet 3.0) in the variable `idaddr?' for use by the application.
The `idtable' contains a 1-byte entry for each machine ID, from 1 to 31. The byte corresponding to machine X is at location `idtable'+X. These values are set by the "census" routine contained in NADADEFS and described below in the section Booting From The Network. 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) |
16 |
Unknown |
For advanced purposes, the address of 'idtable' is also useful for locating certain NadaNet counters and variables that are located just prior to 'idtable' (see a NadaNet listing for details).
&IDTBL has no exceptions.
&BOOT (bootaddr, bootleng, locaddr)
[This command is available only when running on a `master' version of NadaNet.]
Broadcast the code image to machines waiting for network booting using the passive boot protocol.
The `bootaddr' and `bootleng' parameters specify where the boot image will load in the booted machines and how long it is. The `locaddr' parameter specifies where in the master machine's memory the boot image is located.
&BOOT has no exceptions.
Command Exceptions
Most commands can fail. For example, a command 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.
Sometimes a command can be expected to fail, such as when attempting to &GETMSG when the target queue may be empty. To allow a program to deal gracefully with these cases, any command may be given in a "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 SERVER whenever it is not doing some other task. 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 request that takes control elsewhere.
The Message Server
As has been noted, two machines can only communicate over NadaNet synchronously—that is, when both machines rendezvous in time, with one receiving and the other sending. This normally happens because the machine receiving the request is serving, waiting for a message, and, while it is serving, a sender sends it a request. It can also result when, as a sender is busy retrying a request, the destination machine begins to serve and handles the request.
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 'msgclass' 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 fifteen 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 any machine on the network. The modification consists of replacing the self-test code in ROM with network boot code. The new "passive" boot code has been developed for the Enhanced Apple //e, the type of machine used in the AppleCrate II (see AppleCrate II: A New Apple II-Based Parallel Computer).
When a machine with a passive boot ROM is powered on, it enters a loop continually waiting for a BOOT request on the network.
To ensure that only unused IDs are allocated to booted machines, the booting master typically begins by taking a "census" of serving machines. This does not discover machines with assigned IDs that are not serving, but it is better than just assuming that there are no IDs already assigned. After taking a census, the master &BOOTs any slave machines awaiting boot, then &SERVEs for, say, 200ms. to allow the newly booted machines to acquire unique IDs using the GETID protocol. After serving, the master machine again takes a census to verify the new status of all machines on the network.
Standard Applesoft code for performing network boot is provided in the text file NADADEFS, which can be EXECed into any program which may need to boot slave machines. The code occupies statement numbers from 60000 up, and is called to initialize NadaNet and take a census. The variable MX sets the maximum ID that the census will scan, and is currently set to 20 to allow for a network with a 17-processor AppleCrate II and a few other machines, but you can change this value to a lower or higher value to suit your network. Examine any AppleCrate executive program, for example BPRUN, to see how this standard code is used.
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 (easily 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.