MODEM PROGRAMMING GUIDE The Atari ST modem programming guide October 12, 1991 Copyright 1991 Steve Yelvington Internet: 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 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 #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 #include #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 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 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 = 0) { crc = crc ^ (short)*ptr++ __________________________________________________________________________ Back to the main menu Last modified Nov 23, '94 by Christer Gustavsson