Serial Programming/DOS Programming
Serial Programming: Introduction and OSI Network Model -- RS-232 Wiring and Connections -- Typical RS232 Hardware Configuration -- 8250 UART -- DOS -- MAX232 Driver/Receiver Family -- TAPI Communications In Windows -- Linux and Unix -- Java -- Hayes-compatible Modems and AT Commands -- Universal Serial Bus (USB) -- Forming Data Packets -- Error Correction Methods -- Two Way Communication -- Packet Recovery Methods -- Serial Data Networks -- Practical Application Development -- IP Over Serial Connections
Introduction
[edit | edit source]It is now time to build on everything that has been established so far. While it is unlikely that you are going to be using MS-DOS for a major application, it is a good operating system to demonstrate a number of ideas related to software access of the 8250 UART and driver development. Compared to modern operating systems like Linux, OS-X, or Windows, MS-DOS can hardly be called an operating system at all. All it really offers is basic access to the hard drive and a few minor utilities. That really doesn't matter so much for what we are dealing with here, and it is a good chance to see how we can directly manipulate the UART to get the full functionality of all aspects of the computer. The tools I'm using are all available for free (as in beer) and can be used in emulator software (like VMware or Bochs) to try these ideas out as well. Emulation of serial devices is generally a weak point for these programs, so it may work easier if you work from a floppy boot of DOS, or on an older computer that is otherwise destined for the trash can because it is obsolete.
For Pascal, you can look here:
- Turbo Pascal [1] version 5.5 - This is the software I'm actually using for these examples, and the compiler that most older documentation on the web will also support (generally).
- Free Pascal [2] - *note* this is a 32-bit version, although there is a port for DOS development. Unlike Turbo Pascal, it also has ongoing development and is more valuable for serious projects running in DOS.
For MS-DOS substitution (if you don't happen to have MS-DOS 6.22 somewhere):
- FreeDOS [3] Project - Now that Microsoft has abandoned development of DOS, this is pretty much the only OS left that is pure command line driven and following the DOS architecture.
Hello World, Serial Data Version
[edit | edit source]In the introduction, I mentioned that it was very difficult to write computer software that implements RS-232 serial communications. A very short program shows that at least a basic program really isn't that hard at all. In fact, just three more lines than a typical "Hello World" program.
program HelloSerial;
var
DataFile: Text;
begin
Assign(DataFile,'COM1');
Rewrite(DataFile);
Writeln(DataFile,'Hello World');
Close(DataFile);
end.
All of this works because in DOS (and all version of Windows as well... on this particular point) has a "reserved" file name called COM1 that is the operating system hooks into the serial communications ports. While this seems simple, it is deceptively simple. You still don't have access to being able to control the baud rate or any of the other settings for the modem. That is a fairly simple thing to add, however, using the knowledge of the UART discussed in the previous chapter Programming the 8250 UART.
To try something even easier, you don't even need a compiler at all. This takes advantage of the reserved "device names" in DOS and can be done from the command prompt.
C:\>COPY CON COM1
What you are doing here is taking input from CON (the console or the standard keyboard you use on your computer) and it "copies" the data to COM1. You can also use variations of this to do some interesting file transfers, but it has some important limitations. Most importantly, you don't have access to the UART settings, and this simply uses whatever the default settings of the UART might be, or what you used last time you changed the settings to become with a serial terminal program.
Finding the Port I/O Address for the UART
[edit | edit source]The next big task that we have to work with is trying to find the base "address" of the Port I/O so that we can communicate with the UART chip directly (see the part about interface logic in the Typical RS232-Hardware Configuration module for information what this is about). For a "typical" PC system, the following are usually the addresses that you need to work with:
Serial Port Name | Base I/O Port Address | IRQ (interrupt) Number |
---|---|---|
COM1 | 3F8 | 4 |
COM2 | 2F8 | 3 |
COM3 | 3E8 | 4 |
COM4 | 2E8 | 3 |
Looking up UART Base Address in RAM
[edit | edit source]We will get back to the issue of the IRQ Number in a little bit, but for now we need to know where to start accessing information about each UART. As demonstrated previously, DOS also keeps track of where the UART IO ports are located at for its own purpose, so you can try to "look up" within the memory tables that DOS uses to try and find the correct address as well. This doesn't always work, because we are going outside of the normal DOS API structure. Alternative operating systems ( FreeDOS works fine here ) that are otherwise compatible with MS-DOS may not work in this manner, so take note that this may simply give you a wrong result altogether.
The addresses for the serial I/O Ports can be found at the following locations in RAM:
Port | Segment | Offset |
---|---|---|
COM1 | $0040 | $0000 |
COM2 | $0040 | $0002 |
COM3 | $0040 | $0004 |
COM4 | $0040 | $0006 |
Those addresses are written to memory by the BIOS when it boots. If one of the ports doesn't exist, the BIOS writes zero to the respective address. Note that the addresses are given in segment:offset format and that you have to multiply the address of the segment with 16 and add the offset to get to the physical address in memory. This is where DOS "finds" the port addresses so you can run the first sample program in this chapter.
In assembler you can get the addresses like this:
; Data Segment
.data
Port dw 0
...
; Code Segment
.code
mov ax,40h
mov es,ax
mov si,0
mov bx,Port ; 0 - COM1 , 1 - COM2 ...
shl bx,1
mov Port, es:[si+bx]
In Turbo Pascal, you can get at these addresses almost the same way and in some ways even easier because it is a "high level language". All you have to do is add the following line to access the COM Port location as a simple array:
var
ComPort: array [1..4] of Word absolute $0040:$0000;
The reserved, non standard, word absolute is a flag to the compiler that instead of "allocating" memory, that you already have a place in mind to have the computer look instead. This is something that should seldom be done by a programmer unless you are accessing things like these I/O port addresses that are always stored in this memory location.
For a complete program that simply prints out a table of the I/O port addresses for all four standard COM ports, you can use this simple program:
program UARTLook;
const
HexDigits: array [$0..$F] of Char = '0123456789ABCDEF';
var
ComPort: array [1..4] of Word absolute $0040:$0000;
Index: Integer;
function HexWord(Number:Word):String;
begin
HexWord := '$' + HexDigits[Hi(Number) shr 4] +
HexDigits[Hi(Number) and $F] +
HexDigits[Lo(Number) shr 4] +
HexDigits[Lo(Number) and $F];
end;
begin
writeln('Serial COMport I/O Port addresses:');
for Index := 1 to 4 do begin
writeln('COM',Index,' is located at ',HexWord(ComPort[Index]));
end;
end.
Searching BIOS Setup
[edit | edit source]Assuming that the standard I/O addresses don't seem to be working for your computer and you haven't been able to find the correct I/O Port offset addresses through searching RAM either, all hope is still not lost. Assuming that you have not accidentally changed these settings earlier, you can also try to look up these numbers in the BIOS setup page for your computer. It may take some pushing around to find this information, but if you have a conventional serial data port on your computer, it will be there.
If you are using a serial data port that is connected via USB (common on more recent computers), you are simply not going to be (easily) able to do direct serial data communications in DOS. Instead, you need to use more advanced operating systems like Windows or Linux that is beyond the scope of this chapter. We will cover how to access the serial communications routines in those operating systems in subsequent chapters. The basic principles we are discussing here would still be useful to review because it goes into the basic UART structure.
While it may be useful to try and make IRQs selectable and not presume that the information listed above is correct in all situations, it is important to note that most PC-compatible computer equipment usually has these IRQs and I/O port addresses used in this way because of legacy support. And surprisingly as computers get more sophisticated with even more advanced equipment like USB devices, these legacy connections still work for most equipment.
Making modifications to UART Registers
[edit | edit source]Now that we know where to look in memory to modify the UART registers, let's put that knowledge to work. We are also now going to do some practical application of the tables listed earlier in the chapter 8250 UART Programming.
To start with, let's redo the previous "Hello World" application, but this time we are going to set the RS-232 transmission parameters to 1200 baud, 7 databits, even parity, and 2 stop bits. I'm choosing this setting parameter because it is not standard for most modem applications, as a demonstration. If you can change these settings, then other transmission settings are going to be trivial.
First, we need to set up some software constants to keep track of locations in memory. This is mainly to keep things clear to somebody trying to make changes to our software in the future, not because the compiler needs it.
const
LCR = 3;
Latch_Low = $00;
Latch_High = $01;
Next, we need to set the DLAB to a logical "1" so we can set the baud rate:
Port[ComPort[1] + LCR] := $80;
In this case, we are ignoring the rest of the settings for the Line Control Register (LCR) because we will be setting them up in a little bit. Remember this is just a "quick and dirty" way to get this done for now. A more "formal" way to set up things like baud rate will be demonstrated later on with this module.
Following this, we need to put in the baud rate for the modem. Looking up 1200 baud on the Divisor Latch Bytes table gives us the following values:
Port[ComPort[1] + Latch_High] := $00;
Port[ComPort[1] + Latch_Low] := $60;
Now we need to set the values for the LCR based on our desired setting of 7-2-E for the communication settings. We also need to "clear" the DLAB which we can also do at the same time.
Clearing DLAB = 0 * 128
Clearing "Set Break" flag = 0 * 64
Even Parity = 2 * 8
Two Stop bits = 1 * 4
7 Data bits = 2 * 1
Port[ComPort[1] + LCR] := $16 {8*2 + 4 + 2 = 22 or $16 in hex}
Are things clear so far? What we have just done is some bit-wise arithmetic, and I'm trying to keep things very simple here and to try and explain each step in detail. Let's just put the whole thing together as the quick and dirty "Hello World", but with adjustment of the transmission settings as well:
program HelloSerial;
const
LCR = 3;
Latch_Low = $00;
Latch_High = $01;
var
ComPort: array [1..4] of Word absolute $0040:$0000;
DataFile: Text;
begin
Assign(DataFile,'COM1');
Rewrite(DataFile);
{Change UART Settings}
Port[ComPort[1] + LCR] := $80;
Port[ComPort[1] + Latch_High] := $00;
Port[ComPort[1] + Latch_Low] := $60;
Port[ComPort[1] + LCR] := $16
Writeln(DataFile,'Hello World');
Close(DataFile);
end.
This is getting a little more complicated, but not too much. Still, all we have done so far is just write data out to the serial port. Reading data from the serial data port is going to be a little bit trickier.
Basic Serial Input
[edit | edit source]In theory, you could use a standard I/O library and simply read data from the COM port like you would be reading from a file on your hard drive. Something like this:
Readln(DataFile,SomeSerialData);
There are some problems with doing that with most software, however. One thing to keep in mind is that using a standard input routine will stop your software until the input is finished ending with a "Enter" character (ASCII code 13 or in hex $0D).
Usually what you want to do with a program that receives serial data is to allow the user to do other things while the software is waiting for the data input. In a multitasking operating system, this would simply be put on another "thread", but with this being DOS, we don't (usually) have threading capabilities, nor is it necessary. There are some other alternatives that we do in order to get the serial data brought into your software.
Polling the UART
[edit | edit source]Perhaps the easiest to go, besides simply letting the standard I/O routines grab the input) is to do software polling of the UART. One of the reasons why this works is because serial communications is generally so slow compared to the CPU speed that you can perform many tasks in between each character being transmitted to your computer. Also, we are trying to do practical applications using the UART chip, so this is a good way to demonstrate some of the capabilities of the chip beyond simple output of data.
Serial Echo Program
[edit | edit source]Looking at the Line Status Register (LSR), there is a bit called Data Ready that indicates there is some data available to your software in the UART. We are going to take advantage of that bit, and start to do data access directly from the UART instead of relying on the standard I/O library. This program we are going to demonstrate here is going to be called Echo because all it does is take whatever data is sent to the computer through the serial data port and display it on your screen. We are also going to be configuring the RS-232 settings to a more normal 9600 baud, 8 data bits, 1 stop bit, and no parity. To quit the program, all you have to do is press any key on your keyboard.
program SerialEcho;
uses
Crt;
const
RBR = 0;
LCR = 3;
LSR = 5;
Latch_Low = $00;
Latch_High = $01;
var
ComPort: array [1..4] of Word absolute $0040:$0000;
InputLetter: Char;
begin
Writeln('Serial Data Terminal Character Echo Program. Press any key on the keyboard to quit.');
{Change UART Settings}
Port[ComPort[1] + LCR] := $80;
Port[ComPort[1] + Latch_High] := $00;
Port[ComPort[1] + Latch_Low] := $0C;
Port[ComPort[1] + LCR] := $03;
{Scan for serial data}
while not KeyPressed do begin
if (Port[ComPort[1] + LSR] and $01) > 0 then begin
InputLetter := Chr(Port[ComPort[1] + RBR]);
Write(InputLetter);
end; {if}
end; {while}
end.
Simple Terminal
[edit | edit source]This program really isn't that complicated. In fact, a very simple "terminal" program can be adapted from this to allow both sending and receiving characters. In this case, the Escape key will be used to quit the program, which will in fact be where most of the changes to the program will happen. We are also introducing for the first time direct output into the UART instead of going through the standard I/O libraries with this line:
Port[ComPort[1] + THR] := Ord(OutputLetter);
The Transmit Holding Register (THR) is how data you want to transmit gets into the UART in the first place. DOS just took care of the details earlier, so now we don't need to open a "file" in order to send data. We are going to assume, to keep things very simple, that you can't type at 9600 baud, or roughly 11,000 words per minute. Only if you are dealing with very slow baud rates like 110 baud is that going to be an issue anyway (still at over 130 words per minute of typing... a very fast typist indeed).
program SimpleTerminal;
uses
Crt;
const
THR = 0;
RBR = 0;
LCR = 3;
LSR = 5;
Latch_Low = $00;
Latch_High = $01;
{Character Constants}
NullLetter = #0;
EscapeKey = #27;
var
ComPort: array [1..4] of Word absolute $0040:$0000;
InputLetter: Char;
OutputLetter: Char;
begin
Writeln('Simple Serial Data Terminal Program. Press "Esc" to quit.');
{Change UART Settings}
Port[ComPort[1] + LCR] := $80;
Port[ComPort[1] + Latch_High] := $00;
Port[ComPort[1] + Latch_Low] := $0C;
Port[ComPort[1] + LCR] := $03;
{Scan for serial data}
OutputLetter := NullLetter;
repeat
if (Port[ComPort[1] + LSR] and $01) > 0 then begin
InputLetter := Chr(Port[ComPort[1] + RBR]);
Write(InputLetter);
end; {if}
if KeyPressed then begin
OutputLetter := ReadKey;
Port[ComPort[1] + THR] := Ord(OutputLetter);
end; {if}
until OutputLetter = EscapeKey;
end.
Interrupt Drivers in DOS
[edit | edit source]The software polling method may be adequate for most simple tasks, and if you want to test some serial data concepts without writing a lot of software, it may be sufficient. Quite a bit can be done with just that method of data input.
When you are writing a more complete piece of software, however, it becomes important to worry about the efficiency of your software. While the computer is "polling" the UART to see if a character has been sent through the serial communications port, it spends quite a few CPU cycles doing absolutely nothing at all. It also get very difficult to expand a program like the one demonstrated above to become a small section of a very large program. If you want to get that last little bit of CPU performance out of your software, we need to turn to interrupt drivers and how you can write them.
I'll openly admit that this is a tough leap in complexity from a simple polling application listed above, but it is an important programming topic in general. We are also going to expose a little bit about the low-level behavior of the 8086 chip family, which is knowledge you can use in newer operating systems as well, at least for background information.
Going back to earlier discussions about the 8259 Programmable Interrupt Controller (PIC) chip, external devices like the UART can "signal" the 8086 that an important task needs to occur that interrupts the flow of the software currently running on the computer. Not all computers do this, however, and sometimes the software polling of devices is the only way to get data input from other devices. The real advantage of interrupt events is that you can process data acquisition from devices like the UART very quickly, and CPU time spent trying to test if there is data available can instead be used for other tasks. It is also useful when designing operating systems that are event driven.
Interrupt Requests (IRQs) are labeled with the names IRQ0 to IRQ15. UART chips typically use either IRQ 3 or IRQ 4. When the PIC signals to the CPU that an interrupt has occurred, the CPU automatically start to run a very small subroutine that has been previously setup in the Interrupt Table in RAM. The exact routine that is started depends on which IRQ has been triggered. What we are going to demonstrate here is the ability to write our own software that "takes over" from the operating system what should occur when the interrupt occurs. In effect, writing our own "operating system" instead, at least for those parts we are rewriting.
Indeed, this is exactly what operating system authors do when they try to make a new OS... deal with the interrupts and write the subroutines necessary to control the devices connected to the computer.
The following is a very simple program that captures the keyboard interrupt and produces a "clicking" sound in the speaker as you type each key. One interesting thing about this whole section, while it is moving slightly off topic, this is communicating with a serial device. The keyboard on a typical PC transmits the information about each key that you press through a RS-232 serial protocol that operates usually between 300 and 1200 baud and has its own custom UART chip. Normally this isn't something you are going to address, and seldom are you going to have another kind of device connected to the keyboard port, but it is interesting that you can "hack" into the functions of your keyboard by understanding serial data programming.
program KeyboardDemo;
uses
Dos, Crt;
const
EscapeKey = #27;
var
OldKeybrdVector: Procedure;
OutputLetter: Char;
{$F+}
procedure Keyclick; interrupt;
begin
if Port[$60] < $80 then begin
Sound(5000);
Delay(1);
Nosound;
end;
inline($9C) { PUSHF - Push the flags onto the stack }
OldKeybrdVector;
end;
{$F-}
begin
GetIntVec($9,@OldKeybrdVector);
SetIntVec($9,Addr(Keyclick));
repeat
if KeyPressed then begin
OutputLetter := ReadKey;
Write(OutputLetter);
end; {if}
until OutputLetter = EscapeKey;
SetIntVec($9,@OldKeybrdVector);
end.
There are a number of things that this program does, and we need to explore the realm of 16-bit DOS software as well. The 8086 chip designers had to make quite a few compromises in order to work with the computer technology that was available at the time it was designed. Computer memory was quite expensive compared to the overall cost of the computer. Most of the early microcomputers that the IBM-PC was competing against only had 64K or 128K of main CPU RAM anyway, so huge programs were not considered important. In fact, the original IBM-PC was designed to operate on only 128K of RAM although it did become standard with generally up to 640K of main RAM, especially by the time the IBM PC-XT was released and the market for PC "clones" turned out what is generally considered the "standard PC" computer today.
The design came up with what is called segmented memory, where the CPU address is made up of a memory "segment" pointer and a 64K block of memory. That is why some early software on these computers could only run in 64K of memory, and created nightmares for compiler authors on the 8086. Pentium computers don't generally have this issue, as the memory model in "protected mode" doesn't use this segmented design methodology.
Far Procedure Calls
[edit | edit source]
{$F+}
{$F-}
This program has two "compiler switches" that inform the compiler of the need to use what are called far procedure calls. Normally for small programs and simple subroutines, you are able to use what is called relative indexing with the software so the CPU "jumps" to the portion of RAM with the procedure by doing a bit of simple math and "adding" a number to the current CPU address in order to find the correct instructions. This is done especially because it uses quite a bit less memory to store all of these instructions.
Sometimes, however, a procedure must be accessed from somewhere in RAM that is quite different from the current CPU memory address "instruction pointer". Interrupt procedures are one of these, because it doesn't even have to be the same program that is stored in the interrupt vector table. That brings up the next part to discuss:
Interrupt Procedures
[edit | edit source]procedure Keyclick; interrupt;
The word "interrupt" after this procedure name is a key item here. This tells the compiler that it must do something a little bit different when organizing this function than how a normal function call behaves. Typically for most software on the computer, you have a bunch of simple instructions that are then followed by (in assembler) an instruction called:
RET
This is the mnemonic assembly instruction for return from procedure call. Interrupts are handled a little bit differently and should normally end with a different CPU instruction that in assembly is called:
IRET
or Interrupt return for short. One of the things that should also happen with any interrupt service routine is to "preserve" the CPU information before doing anything else. Each "command" that you write in your software will modify the internal registers of the CPU. Keep in mind that an interrupt can occur right in the middle of doing some calculations for another program, like rendering a graphic image or making payroll calculations. We need to hand onto that information and "restore" those values on all of the CPU registers at the end of our subroutine. This is usually done by "pushing" all of the register values onto the CPU stack, performing the ISR, and then restoring the CPU registers afterward.
In this case, Turbo Pascal (and other well-written compilers having a compiler flag like this) takes care of these low-level details for you with this simple flag. If the compiler you are using doesn't have this feature, you will have to add these features "by hand" and explicitly put them into your software. That doesn't mean the compiler will do everything for you to make an interrupt procedure. There are more steps to getting this to work still.
Procedure Variables
[edit | edit source]var
OldKeybrdVector: Procedure;
These instructions are using what is called a procedure variable. Keep in mind that all software is located in the same memory as variables and other information your software is using. Essentially, a variable procedure where you don't need to worry about what it does until the software is running, and you can change this variable while your program is running. This is a powerful concept that is not often used, but it can be used for a number of different things. In this case we are keeping track of the previous interrupt service routine and "chaining" these routines together.
There are programs called Terminate and Stay Resident (TSRs) that are loaded into your computer. Some of these are called drivers, and the operating system itself also puts in subroutines to do basic functions. If you want to "play nice" with all of this other software, the established protocol for making sure everybody gets a chance to review the data in an interrupt is to link each new interrupt subroutine to the previously stored interrupt vector. When we are done with whatever we want to do with the interrupt, we then let all of the other programs get a chance to use the interrupt as well. It is also possible that the Interrupt Service Routine (ISR) that we just wrote is not the first one in the chain, but instead one that is being called by another ISR.
Getting/Setting Interrupt Vectors
[edit | edit source] GetIntVec($9,@OldKeybrdVector);
SetIntVec($9,Addr(Keyclick));
SetIntVec($9,@OldKeybrdVector);
Again, this is Turbo Pascal "hiding" the details in a convenient way. There is a "vector table" that you can directly access, but this vector table is not always in the same location in RAM. If instead you go through the BIOS with a software interrupt, you are "guaranteed" that the interrupt vector will be correctly replaced.
Hardware Interrupt Table
[edit | edit source]Interrupt | Hardware IRQ | Purpose |
---|---|---|
$00 | CPU | Divide by Zero |
$01 | CPU | Single Step Instruction Processing |
$02 | CPU | Non-maskable Interrupts |
$03 | CPU | Breakpoint Instruction |
$04 | CPU | Overflow Instruction |
$05 | CPU | Bounds Exception |
$06 | CPU | Invalid Op Code |
$07 | CPU | Math Co-processor not found |
$08 | IRQ0 | System Timer |
$09 | IRQ1 | Keyboard |
$0A | IRQ2 | Cascade from IRQ8 - IRQ15 |
$0B | IRQ3 | Serial Port (COM2) |
$0C | IRQ4 | Serial Port (COM1) |
$0D | IRQ5 | Sound Card |
$0E | IRQ6 | Floppy Disk Controller |
$0F | IRQ7 | Parallel Port (LPT1) |
$10 - $6F | Software Interrupts | |
$70 | IRQ8 | Real-time Clock |
$71 | IRQ9 | Legacy IRQ2 Devices |
$72 | IRQ10 | Reserved (often PCI devices) |
$73 | IRQ11 | Reserved (often PCI devices) |
$74 | IRQ12 | PS/2 Mouse |
$75 | IRQ13 | Math Co-Processor Results |
$76 | IRQ14 | Hard Disk Drive |
$77 | IRQ15 | Reserved |
$78 - $FF | Software Interrupts |
This table gives you a quick glance at some of the things that interrupts are used for, and the interrupt numbers associated with them. Keep in mind that the IRQ numbers are mainly reference numbers, and that the CPU uses a different set of numbers. The keyboard IRQ, for example, is IRQ1, but it is numbered as interrupt $09 inside the CPU.
There are also several interrupts that are "generated" by the CPU itself. While technically hardware interrupts, these are generated by conditions within the CPU, sometimes based on conditions setup by your software or the operating system. When we start writing the interrupt service routine for the serial communication ports, we will be using interrupts 11 and 12 ($0B and $0C in hex). As can be seen, most interrupts are assigned for specific tasks. I've omitted the software interrupts mainly to keep this on topic regarding serial programming and hardware interrupts.
Other features
[edit | edit source]There are several other parts to this program that don't need too much more explanation. Remember, we are talking about serial programming, not interrupt drivers. I/O Port $60 is interesting as this is the Receiver Buffer (RBR) for the keyboard UART. This returns the keyboard "scan code", not the actual character pressed. In fact, when you use a keyboard on a PC, the keyboard actually transmits two characters for each key that you use. One character is transmitted when you press the key down, and another character when the key is "released" to go back up. In this case, the interrupt service routine in DOS normally converts the scan codes into ASCII codes that your software can use. In fact, simple keys like the shift key are treated as just another scan code.
The sound routines access the internal PC speaker, not something on a sound card. About the only thing that uses this speaker any more is the BIOS "beep codes" that you hear only when there is a hardware failure to your computer, or the quick "beep" when you start or reboot the computer. It was never designed for doing things like speech synthesis or music playback, and driver attempts to use it for those purposes sound awful. Still, it is something neat to experiment with and a legacy computer part that is surprisingly still used on many current computers..
Terminal Program Revisited
[edit | edit source]I'm going to go back to the serial terminal program for a bit and this time redo the application by using an interrupt service routine. There are a few other concepts I'd like to introduce as well so I'll try to put them in with this example program. From the user perspective, I would like to add the ability to change the terminal characteristics from the command line and allow an "end-user" the ability to change things like the baud rate, stop bits, and parity checking, and allow these to be variables instead of hard-coded constants. I'll explain each section and then put it all together when we are through.
Serial ISR
[edit | edit source]This is an example of a serial ISR we can use:
{$F+}
procedure SerialDataIn; interrupt;
var
InputLetter: Char;
begin
if (Port[ComPort[1] + LSR] and $01) > 0 then begin
InputLetter := Chr(Port[ComPort[1] + RBR]);
end; {if}
end;
{$F-}
This isn't that much different from the polling method that we used earlier, but keep in mind that by placing the checking inside an ISR that the CPU is only doing the check when there is a piece of data available. Why even check the LSR to see if there is a data byte available? Reading data sent to the UART is not the only reason why the UART will invoke an interrupt. We will go over that in detail in a later section, but for now this is good programming practice as well, to confirm that the data is in there.
By moving this checking to the ISR, more CPU time is available for performing other tasks. We could even put the keyboard polling into an ISR as well, but we are going to keep things very simple for now.
FIFO disabling
[edit | edit source]There is one minor problem with the way we have written this ISR. We are assuming that there is no FIFO in the UART. The "bug" that could happen with this ISR as it is currently written is that multiple characters can be in the FIFO buffer. Normally when this happens, the UART only sends a single interrupt, and it is up to the ISR to "empty" the FIFO buffer completely.
Instead, all we are going to do is simply disable the FIFO completely. This can be done using the FCR (FIFO Control Register) and explicitly disabling the FIFO. As an added precaution, we are also going to "clear" the FIFO buffers in the UART as a part of the initialization portion of the program. Clearing the FIFOs look like this:
Port[ComPort[1] + FCR] := $07; {clearing the FIFOs}
Disabling the FIFOs look like this:
Port[ComPort[1] + FCR] := $00; {disabling FIFOs}
We will be using the FIFOs in the next section, so this is more a brief introduction to this register so far.
Working with the PIC
[edit | edit source]Up until this point, we didn't have to worry about working with the Programmable Interrupt Controller (the PIC). Now we need to. There isn't the need to do all of the potential instructions for the PIC, but we do need to enable and disable the interrupts that are used by the UART. There are two PICs typically on each PC, but due to the typical UART IRQ vector, we really only have to deal with the master PIC.
Pic Function | I/O Port Address |
---|---|
PIC Commands | 0x20 |
Interrupt Flags | 0x21 |
This adds the following two constants into the software:
{PIC Constants}
MasterPIC = $20;
MasterOCW1 = $21;
After consulting the PIC IRQ table we need to add the following line to the software in order to enable IRQ4 (used for COM1 typically):
Port[MasterOCW1] := Port[MasterOCW1] and $EF;
When we do the "cleanup" when the program finishes, we also need to disable this IRQ as well with this line of software:
Port[MasterOCW1] := Port[MasterOCW1] or $10;
Remember that COM2 is on another IRQ vector, so you will have to use different constants for that IRQ. That will be demonstrated a little bit later. We are using a logical and/or with the existing value in this PIC register because we don't want to change the values for the other interrupt vectors that other software and drivers may be using on your PC.
We also need to modify the Interrupt Service Routine (ISR) a little bit to work with the PIC. There is a command you can send to the PIC that is simply called End of Interrupt (EOI). This signals to the PIC that it can clear this interrupt signal and process lower-priority interrupts. If you fail to clear the PIC, the interrupt signal will remain and none of the other interrupts that are "lower priority" can be processed by the CPU. This is how the CPU communicates back to the PIC to end the interrupt cycle.
The following line is added to the ISR to make this happen:
Port[MasterPIC] := EOI;
Modem Control Register
[edit | edit source]This is perhaps the most non-obvious little mistake you can make when trying to get the UART interrupt. The Modem Control register is really the way for the UART to communicate to the rest of the PC. Because of the way the circuitry on the motherboards of most computers is designed, you usually have to turn on the Auxiliary Output 2 signal in order for interrupts to "connect" to the CPU. In addition, here we are going to turn on the RTS and DTS signals on the serial data cable to make sure the equipment is going to transmit. We will cover software and hardware flow control in a later section.
To turn on these values in the MCR, we need to add the following line in the software:
Port[ComPort[1] + MCR] := $0B;
Interrupt Enable Register
[edit | edit source]We are still not home free yet. We still need to enable interrupts on the UART itself. This is very simple, and for now all we want to trigger an interrupt from the UART is just when data is received by the UART. This is a very simple line to add here:
Port[ComPort[1] + IER] := $01;
Putting this together so far
[edit | edit source]Here is the complete program using ISR input:
program ISRTerminal;
uses
Crt, Dos;
const
{UART Constants}
THR = 0;
RBR = 0;
IER = 1;
FCR = 2;
LCR = 3;
MCR = 4;
LSR = 5;
Latch_Low = $00;
Latch_High = $01;
{PIC Constants}
MasterPIC = $20;
MasterOCW1 = $21;
{Character Constants}
NullLetter = #0;
EscapeKey = #27;
var
ComPort: array [1..4] of Word absolute $0040:$0000;
OldSerialVector: procedure;
OutputLetter: Char;
{$F+}
procedure SerialDataIn; interrupt;
var
InputLetter: Char;
begin
if (Port[ComPort[1] + LSR] and $01) > 0 then begin
InputLetter := Chr(Port[ComPort[1] + RBR]);
Write(InputLetter);
end; {if}
Port[MasterPIC] := EOI;
end;
{$F-}
begin
Writeln('Simple Serial ISR Data Terminal Program. Press "Esc" to quit.');
{Change UART Settings}
Port[ComPort[1] + LCR] := $80;
Port[ComPort[1] + Latch_High] := $00;
Port[ComPort[1] + Latch_Low] := $0C;
Port[ComPort[1] + LCR] := $03;
Port[ComPort[1] + FCR] := $07; {clearing the FIFOs}
Port[ComPort[1] + FCR] := $00; {disabling FIFOs}
Port[ComPort[1] + MCR] := $0B;
{Setup ISR vectors}
GetIntVec($0C,@OldSerialVector);
SetIntVec($0C,Addr(SerialDataIn));
Port[MasterOCW1] := Port[MasterOCW1] and $EF;
Port[ComPort[1] + IER] := $01;
{Scan for keyboard data}
OutputLetter := NullLetter;
repeat
if KeyPressed then begin
OutputLetter := ReadKey;
Port[ComPort[1] + THR] := Ord(OutputLetter);
end; {if}
until OutputLetter = EscapeKey;
{Put the old ISR vector back in}
SetIntVec($0C,@OldSerialVector);
Port[MasterOCW1] := Port[MasterOCW1] or $10;
end.
At this point you start to grasp how complex serial data programming can get. We are not finished yet, but if you have made it this far you hopefully understand each part of the program listed above. We are going to try and stay with this one step at a time, but at this point you should be able to write some simple custom software that uses serial I/O.
Command Line Input
[edit | edit source]There are a number of different ways that you can "scan" the parameters that start the program. For example, if you start a simple terminal program in DOS, you can use this command to begin:
C:> terminal COM1 9600 8 1 None
or perhaps
C:> terminal COM4 1200 7 2 Even
Obviously there should not be a need to have the end-user recompile the software if they want to change something simple like the baud rate. What we are trying to accomplish here is to grab those other items that were used to start the program. In Turbo Pascal, there is function that returns a string
ParamStr(index)
which contains each item of the command line. These are passed to the program through strings. A quick sample program on how to extract these parameters can be found here:
program ParamTst;
var
Index: Integer;
begin
writeln('Parameter Test -- displays all command line parameters of this program');
writeln('Parameter Count = ',ParamCount);
for Index := 0 to ParamCount do begin
writeln('Param # ',Index,' - ',ParamStr(Index));
end;
end.
One interesting "parameter" is parameter number 0, which is the name of the program that is processing the commands. We will not be using this parameter, but it is something useful in many other programming situations.
Grabbing Terminal Parameters
[edit | edit source]For the sake of simplicity, we are going to require that either all of the parameters are going to be in that format of baud rate, bit size, stop bits, parity; or there will be no parameters at all. This example is going to be mainly to demonstrate how to use variables to change the settings of the UART by the software user rather than the programmer. Since the added sections are self-explanatory, I'm just going to give you the complete program. There will be some string manipulation going on here that is beyond the scope of this book, but that is going to be used only for parsing the commands. To keep the user interface simple, we are using the command line arguments alone for changing the UART parameters. We could build a fancy interface to allow these settings to be changed while the program is running, but that is an exercise that is left to the reader.
Serial Programming: Introduction and OSI Network Model -- RS-232 Wiring and Connections -- Typical RS232 Hardware Configuration -- 8250 UART -- DOS -- MAX232 Driver/Receiver Family -- TAPI Communications In Windows -- Linux and Unix -- Java -- Hayes-compatible Modems and AT Commands -- Universal Serial Bus (USB) -- Forming Data Packets -- Error Correction Methods -- Two Way Communication -- Packet Recovery Methods -- Serial Data Networks -- Practical Application Development -- IP Over Serial Connections