home *** CD-ROM | disk | FTP | other *** search
- From: steve@thelake.mn.org (Steve Yelvington)
- Newsgroups: comp.sys.atari.st.tech
- Subject: DOC: Writing communications software
- Date: 13 Oct 91 03:15:54 GMT
- Organization: The little computer in the little town that time forgot
- Lines: 760
-
- It's been a few weeks since I posted the original version of this, and
- there have been some additions and changes. As usual, corrections/
- suggestions/complaints/questions are appreciated.
-
- 0/ cut here
- ==0\================================================================
-
- The Atari ST modem programming guide
- October 12, 1991
-
- Copyright 1991 Steve Yelvington
- Internet: <steve@thelake.mn.org>
- GEnie: S.YELVINGTO2
-
-
- Introduction
- ------------
-
- Modem i/o on the ST is isn't difficult. In fact, compared with other
- personal systems, the ST is unusually easy to program for serial
- communications.
-
- The hard work is done for you. TOS includes interrupt-driven serial
- port routines. This means that when a character arrives at the serial
- port, TOS breaks away from whatever it's doing, grabs the character,
- stores it in a buffer, and returns to the task that was interrupted.
- You don't need to write code to do that.
-
- The serial port buffers -- one for input, one for output -- are FIFO
- (First In, First Out) queues. TOS provides functions for reading from
- the input buffer, writing to the output buffer, and even changing the
- size of the buffers.
-
- All you have to do is read from or write to the serial port as if it
- were a console.
-
- This document contains examples in C. If you're programming in another
- language, don't despair, because C is used here only to string together
- TOS functions. The strategies don't change when you shift to another
- system of notation.
-
- The modem i/o routines
- ----------------------
-
- Both BIOS and GEMDOS provide functions for single-character serial
- i/o that work exactly like their console equivalents.
-
- >From GEMDOS:
-
- Console Serial Description
- ------- ------ -----------
- Cconin Read a character with echo
- Cnecin Cauxin Read a character without echo
- Cconout Cauxout Write a character
- Cconis Cauxis Return TRUE if a character is available
- Cconos Cauxos Return TRUE if output is possible, i.e.,
- the output buffer has room for more data
-
- There is not a serial port equivalent for Cconws, but you should be
- able to write one easily enough:
-
- #include <osbind.h>
- void Cauxws(s) /* write a nul-terminated string to the aux port */
- register char *s;
- {
- register char c;
- while (c=*s++)
- Cauxout(c);
- }
-
-
- In C, GEMDOS functions usually are implemented as preprocessor
- macros that translate into gemdos() function calls. Other
- languages may require that you write GEMDOS calls explicitly. If
- you're coding in assembler, you should be able to translate the
- following by keeping in mind that you push the arguments onto
- the stack from right to left and then call trap #1 for gemdos.
-
- Here are common C preprocessor definitions for the
- above-mentioned operations (plus a few more).
-
- #define Cconin() (long) gemdos(0x1)
- #define Cconout(a) (void) gemdos(0x2,(short)(a))
- #define Cauxin() (short) gemdos(0x3)
- #define Cauxout(a) (void) gemdos(0x4,(short)(a))
- #define Cprnout(a) (void) gemdos(0x5,(short)(a))
- #define Cnecin() (long) gemdos(0x8)
- #define Crawio(a) (long) gemdos(0x6,(short)(a))
- #define Crawcin() (long) gemdos(0x7)
- #define Cconws(a) (void) gemdos(0x9,(char*)(a))
- #define Cconrs(a) (void) gemdos(0x0a,(char*)(a))
- #define Cconis() (short) gemdos(0x0b)
- #define Cconos() (short) gemdos(0x10)
- #define Cprnos() (short) gemdos(0x11)
- #define Cauxis() (short) gemdos(0x12)
- #define Cauxos() (short) gemdos(0x13)
-
-
- What about BIOS?
-
- >From BIOS, the approach is slightly different. BIOS provides one
- function for each operation, with the device indicated by an
- argument. The first argument to the function is the device
- number:
-
- Console Serial Description
- ------- ------ -----------
- Bconin(2) Bconin(1) Read a character
- Bconout(2,c) Bconout(1,c) Write a character c
- Bconstat(2) Bconstat(1) Return TRUE if a character is available
- Bcostat(2) Bcostat(1) Return TRUE if output is possible
-
- Note that the device numbers used by the BIOS are different from the
- file handles used at the GEMDOS level. The BIOS device numbers may be
- defined in this fashion:
-
- #define BCON_PRT 0 /* printer */
- #define BCON_AUX 1 /* aux (modem) port */
- #define BCON_CON 2 /* console */
- #define BCON_MIDI 3 /* MIDI port */
- #define BCON_KBD 4 /* intelligent keyboard device output */
- #define BCON_RAW 5 /* raw console output, no VT52 */
-
- The BIOS macros are defined as follows:
-
- #define Bconstat(dev) bios(1,(short)(dev))
- #define Bconin(dev) bios(2,(short)(dev))
- #define Bconout(dev,ch) bios(3,(short)(dev),(short)(ch))
- #define Bcostat(dev) bios(8,(short)(dev))
-
- If you're using assembly language, the bios trap is #13.
-
- There is a no function to read a string from the serial port at
- either the GEMDOS level or the BIOS level. For reasons that will
- become clear shortly, you should write your own.
-
- All of the GEMDOS and BIOS input functions return 32-bit LONG values.
-
- In the case of the serial port, only the low 8 bits are interesting.
-
- In the case of the console, the low 8 bits will contain the
- ASCII code corresponding to the key -- if there is one. If the
- keystroke is a nonstandard key, such as a function key, HELP or
- UNDO, the low eight bits will be an ASCII nul value. In all
- cases, the upper 24 bits contain a unique keyscan code and
- information regarding the state of the shift keys.
-
- A simple terminal program
- -------------------------
-
- A ``dumb'' terminal program using these functions is simple to write.
- All you need is a loop in which you will ``poll'' the console and the
- serial port. It might look like this:
-
- #include <osbind.h>
- #define BANNER "\033EIncredibly dumb full-duplex terminal version 0.1\r\n"
-
- main()
- {
- int c;
- Cconws(BANNER);
- for(;;)
- {
- if (Cauxstat()) /* data waiting at the aux port */
- {
- c = (int) (Cauxin() & 0xFF); /* get it and clean it */
- Cconout(c);
- }
- if (Cconstat()) /* data waiting at the console */
- {
- c = (int) (Cnecin() & 0xFF); /* get it and clean it */
- if (c) /* if it's not a NUL from a function key */
- Cauxout(c);
- }
- }
- }
-
- You'll notice that the above example runs forever. Well, not quite.
- GEMDOS console i/o processes control-characters, and a control-C will
- kill the program.
-
- Of course, that means you can't send control-C out the modem port.
- That's one of many reasons it's called an ``incredibly dumb''
- terminal.
-
- You'll probably want to rewrite the program using BIOS functions and
- test for use of the UNDO or F10 keys.
-
-
- Whatever happened to GEM?
- -------------------------
-
- None of this discussion has dealt with GEM. Why? Because so far as
- GEM is concerned, the serial port doesn't exist.
-
- The AES Event Manager doesn't poll the aux port to determine whether
- an event has occurred.
-
- If you want to write a GEM event-driven communications program that
- will cooperate with desk accessories for multitasking, you'll need to
- use the timer with a time value of 0. This will allow a task switch
- to take place, and when you regain control you can check the serial
- port's status with GEMDOS or BIOS.
-
- If you're worried that the serial input buffer will overflow while
- some system-hog task is running, resize the buffer as described
- below.
-
- The real world intrudes
- -----------------------
-
- None of the above functions cares whether the modem is live or dead.
- They only deal with data. If your caller falls asleep at the keyboard
- or abruptly hangs up, the above functions won't worry about a thing.
- If you're writing a communications program or a BBS, you'll have to
- deal with these real-world conditions. Modems and phone lines are not
- perfect.
-
- There are many ways to handle such errors. In C, you might want to
- use the setjmp and longjmp functions to ``bail out'' and restart your
- program. Assuming you have set up a jump buffer called ``panic,'' you
- might write a low-level aux port input function that looks like this:
-
- #include <time.h>
- #include <osbind.h>
- #define TIMEOUT 60 /* 60-second timeout */
-
- int auxin()
- {
- clock_t time1,elapsed;
- if (!rs232cd())
- {
- hangup();
- longjmp(panic);
- }
- time1 = clock(); /* get tick time */
-
- /* loop for up to TIMEOUT seconds, waiting for aux input */
- for(;;)
- {
- if (Cauxis()) /* these could just as well be BIOS calls */
- return Cauxin();
-
- /* no activity yet, so ... */
- /* get new tick time and convert to seconds */
-
- elapsed = (clock() - time1) / CLK_TCK;
-
- /* if too much time has elapsed, restart */
-
- if (elapsed > TIMEOUT)
- {
- hangup();
- longjmp(panic);
- }
- } /* continue the loop */
- }
-
-
- The above code has used a couple of functions that aren't in the
- library and bear some explaining. Here is the function that checks
- the carrier-detect status of the aux port by examining a hardware
- register:
-
- #include <osbind.h>
- int rs232cd() /* state of rs232 carrier detect line */
- {
- register long ssp;
- register int *mfp, status;
- mfp = ((int *) 0xFFFFFA00L); /* base address of MFP */
- ssp = Super(0L); /* enter supervisor mode */
- status = *mfp; /* get MFP status */
- Super(ssp); /* return to user mode */
- return(!(status & 0x0002)); /* check for carrier */
- }
-
-
- Here is a function that hangs up the phone line by dropping the DTR
- (Data Terminal Ready) signal. Note that most modems are shipped from
- the factory with DTR sensitivity disabled. You should be able to turn
- it back on by toggling a DIP switch or with a command from the
- keyboard.
-
-
- #include <osbind.h>
- void hangup()
- {
- Ongibit(0x10); /* dtr off */
- sleep(1); /* wait one second */
- Offgibit(0xEF); /* dtr on */
- }
-
- Now, what about getting a string from the modem port? You'll need to
- call your custom auxin() function, as above. Here is an auxgets
- function modified from the dLibs getln. Note that it uses a function
- ``auxput'' for output; you can rewrite or #define it to mirror the
- BIOS or GEMDOS-level routine you used for input:
-
- #define KEY_CR 0x0D /* carriage return */
- #define KEY_LF 0x0A /* linefeed */
- #define KEY_BS 0x08 /* backspace */
- #define KEY_DEL 0x7F /* delete */
-
- char *auxgets(buffer, limit)
- char *buffer; /* where keystrokes will be stored */
- register int limit; /* size of the buffer */
- {
- register char *bp = buffer;
- register int c, i = 0;
- for(;;)
- {
- c = auxin() & 0xFF; /* mask off high bits, just in case */
-
- /* end of line */
- if((c == KEY_CR) || (c == KEY_LF))
- {
- *bp = '\0';
- break;
- }
-
- /* backspace or delete */
- else if(((c == KEY_BS) || (c == KEY_DEL)) && (bp != buffer))
- {
- --bp;
- auxput('\b');
- auxput(' ');
- auxput('\b');
- --i;
- }
- else if((c >= ' ') && (i < limit))
- {
- auxput(*bp++ = c);
- ++i;
- }
- }
- return(buffer);
- }
-
-
-
- Configuring the serial port
- ---------------------------
-
- In order to communicate with another machine over the serial port,
- speed and flow-control settings may need to be altered. Atari's XBIOS
- (Extended BIOS) provides a function that can configure the serial
- port. The function Rsconf sets the speed, flow-control and four
- bitmapped hardware registers.
-
- Speed (bits per second, often incorrectly called baud) can be set to
- any of 16 values:
-
- #define BPS_19200 0 /* standard */
- #define BPS_9600 1 /* standard */
- #define BPS_4800 2 /* standard */
- #define BPS_3600 3
- #define BPS_2400 4 /* standard */
- #define BPS_2000 5
- #define BPS_1800 6
- #define BPS_1200 7 /* standard */
- #define BPS_600 8
- #define BPS_300 9 /* standard */
- #define BPS_200 10
- #define BPS_150 11
- #define BPS_134 12
- #define BPS_110 13
- #define BPS_75 14
- #define BPS_50 15
-
- Only the values marked /* standard */ are commonly used in modem
- communications.
-
- If you are using a slow- or medium-speed modem (under 9600bps) you
- probably will need to match the ST's serial port speed to that of the
- remote system.
-
- If you are using a high-speed modem, you may be able to set the speed
- at 19,200bps and let the modem handle speed-matching chores. In this
- case, you probably will need to use RTS/CTS (hardware) flow-control
- to avoid overloading the ST's input buffer and the modem's output
- buffer. RTS/CTS is broken in most versions of TOS, and an /auto/
- folder program must be installed to fix it.
-
- Flow control can be configured to any of four settings:
-
- #define FLOW_NONE 0 /* default */
- #define FLOW_XONXOFF 1 /* control-s, control-q software */
- #define FLOW_RTSCTS 2 /* hardware */
- #define FLOW_BOTH 3 /* both hardware and software */
-
- For binary file transfers, software flow control must not be used.
-
- The hardware registers are not useful in ordinary modem-related
- programming, and altering their values incorrectly will have
- unpleasant side effects. Passing -1 to Rsconf will leave the
- corresponding setting unchanged.
-
- Thus, to set bps rate to 2400 and flow control to none, the Rsconf
- function call would look like this:
-
- Rsconf(BPS_1200,FLOW_NONE,-1,-1,-1,-1)
-
-
- Parity and duplex
- -----------------
-
- Parity is not commonly used in communicating with personal computers,
- but you may need to deal with it when communicating with a mainframe.
-
- The idea behind parity is to provide at least a small degree of
- assurance that the transmitted character is the same as the received
- character. The way this is done is by fiddling with the high bit of
- each byte, which ordinarily would be zero for an ASCII character.
-
- ``Even parity'' means that the high bit is either set or unset, as
- necessary, to ensure that the result is an even number. ``Odd
- parity'' means the opposite. You can test this with the modulus
- operator in C, then strip the high bit by ANDing the byte with 0x7F
- or set the high bit by ANDing with 0xFF.
-
- TOS also can generate parity values automatically if the proper
- hardware register is set. However, setting the register requires that
- you also worry about start/stop bits, word length and the serial
- hardware frequency, which is a can of worms.
-
- Duplex is a potentially confusing term that describes whether you or
- a remote system is responsible for putting a character on your
- screen.
-
- Most bulletin board systems run ``full duplex,'' which means that a
- keystroke from a caller is sent to the BBS, which then echoes that
- character back to the caller. This relationship is not symmetrical.
- The caller doesn't echo anything back to the BBS, because the echoes
- would be interpreted as commands.
-
- ``Half duplex'' means that each party is responsible for managing his
- or her own display. GEnie normally runs half duplex. Neither caller
- nor BBS echoes anything.
-
- If you're writing a BBS or a terminal program with a ``chat mode,''
- you may want to include a ``both local and remote echo'' option. This
- would allow the remote to run full duplex while you manage both your
- screen and the caller's screen.
-
-
- Resizing the serial buffers
- ---------------------------
-
- When a character arrives at the serial port, TOS interrupts what
- it's doing, grabs the character before it disappears, and
- records it in an input buffer. When your program wishes to write
- to the serial port, TOS buffers the output so that your program
- can proceed with other chores without having to wait for a slow
- modem to transmit the data.
-
- TOS provides those buffers, but TOS also provides a facility for
- changing them. Why would you want to do so? Here's a scenario:
-
- You're using Xmodem and you have a slow floppy drive. Xmodem
- ACKS a block, then proceeds to write it to disk while the sender
- sends the next block. So far, so good. But if you switch to
- Ymodem's 1K blocks and a very fast modem, the TOS default buffer
- might overflow while you're writing the previous block to that
- slow floppy drive. Solution: Use a bigger input buffer.
-
- Another scenario: You're running a BBS. The caller is reading a
- long message and decides it's boring. The caller pushes the
- cancel key (control-C, or S, or whatever you've decided makes
- sense.) The BBS stops sending, but that big output buffer is
- full of data -- and it just keeps coming, and coming ....
- Solution: Use small i/o buffers for interacting with humans.
-
- TOS provides an XBIOS function that returns a pointer to an
- input buffer record. It requires one argument: The BIOS number
- of a device (see above).
-
- #define Iorec(dev) (void*) xbios(14, (short)(dev))
-
- Assembly programmers: The xbios trap is #14, so push the device
- number followed by two 14's, then do the trap. The pointer will
- be found in d0.
-
- The iorec structure is defined as follows:
-
- struct iorec
- {
- char *buf; /* pointer to an array: char buffer[bufsiz] */
- short bufsiz; /* size of the array */
- short head; /* index for writing */
- short tail; /* index for reading */
- short low; /* low water mark, for XON */
- short high; /* high water mark, for XOFF */
- }
-
- For the AUX port, the output buffer record immediately follows
- the input buffer record.
-
- Before you go poking around inside these records, keep in mind
- that they are internal to TOS, and they are shared by all
- processes that run on your system. You can change the buffers
- and even bypass GEMDOS and BIOS for reading and writing, if you
- so desire, but you must be prepared to live with the
- consequences.
-
- In particular, be very careful to restore the original record
- when your program terminates (unless you're writing a TSR with
- the express purpose of enlarging a buffer).
-
- Immediately after the serial port's input and output records are
- some internal TOS variables that may be useful. The following
- code has worked on all versions of TOS that I've encountered. I
- don't know whether Atari guarantees that the variables won't be
- changed on future versions of TOS.
-
- With those cautions in mind, here is some code, originally
- lifted from an early ST communications program written by Dale
- Schumacher. I've been using the open_aux and close_aux functions
- for years, but I haven't had a need to deal with the rsr, tsr,
- and flow control variables.
-
- typedef struct
- {
- struct iorec in; /* 0 Input buffer record */
- struct iorec out; /* 14 Output buffer record */
- char rsr_status; /* 28 MFP(0x1C) Receiver status */
- char tsr_status; /* 29 MFP(0x1D) Transmit status */
- char xoff_sent; /* 30 TRUE if we sent XOFF */
- char xoff_received; /* 31 TRUE if we got XOFF */
- char mode; /* 32 Bit 0 XON, Bit 1 RTS mode */
- char filler; /* 33 Unused */
- } IOREC;
-
- #define IBUFSIZ 1200
- #define OBUFSIZ 2
-
- char st_ibuf[IBUFSIZ]; /* our own input buffer */
- char st_obuf[OBUFSIZ]; /* and our own output buffer */
-
- IOREC *st_sysr; /* ptr to system rs232 record */
- IOREC st_savr; /* to save system rs232 record */
-
- IOREC st_myiorec =
- {
- /* first, an input record */
- st_ibuf, IBUFSIZ, 0, 0, (IBUFSIZ/4), (3*(IBUFSIZ/4)),
-
- /* then an output record */
- st_obuf, OBUFSIZ, 0, 0, 0, 1,
-
- /* and the rsr, tsr, flow control stuff */
- 0, 0, 0, 0, 0, 0
- };
-
- void openaux() /* set up our own rs232 input and output buffers */
- {
- while(Bconstat(AUX)) /* flush existing buffer */
- Bconin(AUX);
- st_sysr = (IOREC *)Iorec(0);
- st_savr = *st_sysr; /* Save system buffer */
- *st_sysr = st_myiorec; /* Set my io buffer */
- }
-
- void closeaux() /* restore system i/o buffer */
- {
- *st_sysr = st_savr;
- }
-
-
- Take a break
- ------------
-
- Some remote systems, notably Unix sites, need a BREAK signal to
- wake them up and help them synchronize with a caller's bps rate.
- To generate a BREAK on the ST, you briefly toggle the transmit
- status register. Here is code that does so by directly accessing
- the hardware on the ST. I think Dale Schumacher is the author of
- this code.
-
- #include <time.h>
- #include <osbind.h>
-
- void nap(n) /* do nothing for n-100ths of a second */
- register int n;
- {
- register clock_t t, dt;
- dt = (CLK_TCK * ((clock_t) n)) / ((clock_t) 100);
- t = clock() + dt;
- while(clock() < t)
- ;
- }
-
- void snd_brk() /* send a BREAK to the rs232 port */
- {
- register long ssp;
- register char *tsr;
- tsr = ((char *) 0xFFFFFA2DL); /* address of tsr */
- ssp = (long) Super(0L); /* enter supervisor mode */
- *tsr = 0x09; /* turn break on */
- Super(ssp); /* return to user mode */
- nap(30); /* 0.3 second BREAK */
- ssp = (long) Super(0L); /* enter supervisor mode */
- *tsr = 0x01; /* turn break off */
- Super(ssp); /* return to user mode */
- }
-
- The clock() function should be available with most C compilers.
- If you're writing in a language that doesn't have an equivalent,
- the trick is simply to peek at memory location 4BA (hex), which
- is the 200Hz system timer. You must be in supervisor mode to do
- so. The value is a 32-bit long.
-
-
- File transfers
- --------------
-
- When transferring a binary file, the sender and the receiver
- have to agree on a method for signalling the end of the
- transmission. Ideally, they should have some method of
- determining whether the file was mangled by line noise. And if
- it was, there should be a way to retransmit the bad data.
-
- The methods for solving these problems are called file-transfer
- protocols, and their name should be legion, for they are many.
- Xmodem, Ymodem, Zmodem, Quick B, WXmodem, Kermit, Punter, FAST,
- Telink, UUCP 'G' ... the list goes on.
-
- Most of them follow the same basic strategy. A file is broken up
- into multiple packets. Each packet starts with a header that
- includes a packet number and a mathematical value, called a
- checksum, that can be used to determine whether the data in the
- packet has been corrupted.
-
- XMODEM is a very simple implementation of this strategy.
-
- It uses 128-byte data packets. It uses packet headers consisting
- of a a byte for the packet number, and (in the original version)
- a byte for a checksum. The checksum is calculated by adding the
- numeric value of each byte and throwing away the overflow. It's
- not foolproof, but it detects most errors.
-
- Later versions of Xmodem use a 16-bit ``cyclical redundancy
- check'' calculated by using a fairly complex algorithm. It's
- much harder to fool.
-
- If the Xmodem receiver adds up all the values and finds they
- don't match the total that was encoded in the packet header, it
- sends a NAK (Negative AcKnowledgement) signal to the sender,
- which then retransmits the corrupted packet. If they match, it
- the receiver sends an ACK.
-
- Because the packages are small, and because the sender always waits
- for an ACK before proceeding, Xmodem is not particularly fast. When
- XMODEM runs over a network that does its own error-checking and
- packet operations internally (such as CompuServe or GEnie's data
- systems), Xmodem can get painfully slow. That lonely ACK signal
- can take a long time to wander through the network and get back
- to you.
-
- There are two ways of dealing with Xmodem's speed problems. One is to
- use bigger packets, which is the approach taken by most alternatives,
- including Ymodem. The other is to use ``windowing,'' which is used by
- WXmodem, UUCP 'G' and Zmodem.
-
- A windowing protocol doesn't wait for an ACK before sending.
- Instead, it transmits the next block. If a NAK arrives, it can
- back up and resend the bad block. The number of blocks that can
- be ``remembered'' is the size of the ``window.'' A
- sliding-window implementation is one in which the sender's
- window moves along as you move through the file and get ACKs
- from the receiver.
-
- And now, the code.
-
- This document isn't going to attempt to present a C
- implementation of a file-transfer protocol. But the question of
- ``how do I calculate a checksum'' comes up frequently, here is C
- code to create the values used by Xmodem, Ymodem and some
- related programs.
-
- Some CRC functions index the CRC values out of an array of
- precalculated numbers. That approach probably is faster than the
- one below, but the ST has plenty of horsepower for this sort of
- thing.
-
- Don't ask me to explain the math. I can't. I didn't write this;
- I just use it.
-
- /*
- * This function calculates the CRC used by the XMODEM/CRC
- * Protocol.
- * The first argument is a pointer to the message block.
- * The second argument is the number of bytes in the message block.
- * The function returns a short integer that contains the CRC value.
- */
- short crc16(ptr, count)
- register char *ptr;
- register short count;
- {
- register short crc = 0, i;
- while(--count >= 0)
- {
- crc = crc ^ (short)*ptr++ << 8;
- for(i=0; i<8; ++i)
- {
- if(crc & 0x8000)
- crc = crc << 1 ^ 0x1021;
- else
- crc = crc << 1;
- }
- }
- return(crc & 0xFFFF);
- }
-
-
- If you want to port this to another language, keep in mind that
-
- a << b evaluates to the bit pattern of a shifted left b bits
-
- * dereferences a pointer; *ptr++ evaluates to the pointed-to value
- and then increments ptr by the size of the pointed-to value
-
- ^ is a bitwise exclusive OR operator; 11110000 (binary) ^ 00001111
- evaluates to 11111111
-
- & in the context above is a bitwise AND operator; crc & 0xFFFF
- basically strips anything above 16 bits (important if your
- environment supports 32-bit ints).
-
-
- Acknowledgements
- ----------------
-
- This document wouldn't have been possible without the assistance
- of the following people. Some provided instructive code samples;
- others caught errors in earlier versions of this document or
- raised important questions. Thanks to: Dale Schumacher, John
- Stanley, Robert Royar, Adam David, Rob McMullen, Michael
- Fischer.
-
- Corrections
- -----------
-
- Send corrections to steve@thelake.mn.org (Internet),
- S.YELVINGTO2 (GEnie), plains!umn-cs!thelake!steve (UUCP), or
- snail mail to P.O. Box 38, Marine on St. Croix, MN 55047 USA.
-
- Permissions
- -----------
-
- This file may be distributed only in electronic format, and only
- in its original, unaltered state. All other reproduction is
- prohibited without written permission of the author. This
- information is intended to be accurate, but the author makes no
- representation that is is free of errors. This information is
- presented without warranty of any sort, and the author is not
- responsible for any side effects. None of this information is
- extracted from any proprietary documents or trade secrets.
- PS: Shakespeare was right about the lawyers.
-
- =================================================================
-
- ə