Your PC's Printer Port: a Window on the Real World Tom Dickinson tsd@bnl.gov The printer port or parallel port on your IBM computer is well suited for hardware control, data input, or other interfacing. If you just send (write or output) a number to the correct processor I/O port the corresponding bits will appear as ones and zeros (5 volts and 0 volts) on the connector pins on the printer adapter. It's as simple as that. It's nearly as simple to get signals back, but there are only five bits of input -- the status signals that the PC expects from the printer. The printer port works well as a hardware interface because it is a "real-time" device. When you output a number to the port, voltages appear on the connector pins, and when you input you get the real time status of the bits (voltages) in the input pins. This is in contrast to the serial port which handles things by the RS232 protocol and sends and receives strings of pulses which have to be 're-assembled' into meaningful numbers. RS232 also uses 'handshaking' signals to figure out when to start and stop strings of pulses and when to expect another batch. When the printer adapter is feeding a printer it also uses handshaking, hardware interrupts and so forth, but for our simple interfacing we can ignore these. IBM compatible computers keep in touch with the outside world mostly through processor I/O ports. These are addressed though the same address lines as memory, but use different read/write lines, so they don't occupy memory address space. The Intel processors can address 64 k I/O ports, but the IBM machines implement only about 1000 of these, which is plenty. They are used for keyboard, com and parallel ports, disk I/O, video, and other things. The processor I/O ports consist of logic built into the processor and address assignments which permit the processor to pass data back and forth in 8 bit chunks to different destinations and sources. They should be distinguished from the connectors on the back of your computer for the serial port, parallel port, etc. Each of these uses several processor I/O ports to carry out its functions. More on this below. Reading and writing to the I/O ports is slower than to memory, and as computers have gotten faster and more sophisticated, some of these functions have been changed to use Direct Memory Access (DMA), in particular the newer video and disk interfaces. IBM compatible computers accommodate three printer adapters, that is, three sets of processor I/O ports are assigned to this function. When the computer boots up, the POST program detects any printer adapters and assigns 'logical' ports to them, LPT1, LPT2, and LPT3. Which port addresses are assigned to LPT1, 2, or 3 depends on whether the printer adapter is part of the video card, but for hardware interfacing we don't care, since the logical designations are mostly used by applications programs. Each printer adapter uses three processor I/O ports. The first is for data output, the second for input of status signals from the printer, and the third is for commands that go to the printer. The three are known as the data, status, and control ports. You can execute a 'read' to the first and third ports, but all that you'll see is the state of the latches set by the last 'write'. Doing a read on the second port returns the state of the five status signals on the printer connector, while writing to that port doesn't do anything. The physical address of a printer adapter card is determined by how the computer address lines are connected up to the logic on the card, and most adapters have a jumper plug so you can select 378h or 278h. The first (data output) port is the base address, and the status and control ports mentioned above are base+1 and base+2. The three base addresses available for printer adapters are 378h, 278h, and 3BCh. These are hexadecimal numbers, indicated by the 'h' after each one. You will need to know the physical addresses of your printer adapter in order to send and get signals, so here's how to figure it out. If your printer connector is on your mono or CGA video card, then the base address is almost certainly 3BCh. That means that the data output address is 3BCh, the status input address is 3BDh and the control port is 3BEh. (Base address, base+1, and base+2. Get the picture?) If the printer connector is not on the video adapter, then the address is probably 378h, and if not that then it will be 278h. If you can't figure it out, run the program on the next page and check for the pattern of voltages on the printer connector. If it doesn't work, try again with a different base address. You can also use the same technique for finding the printer adapter address that the computer does. During the Power On Self Test, the computer looks for adapter cards, keyboard etc, and stores address and other configuration information in the BIOS Data Area of memory. The printer adapter base addresses are stored at 40:0008h, and can be accessed by using DEBUG. The DEBUG program is probably on your DOS disk, so change to that disk or directory and type debug and hit enter. You will get a minus sign (not blinking) at the left edge of the screen which is the debug prompt. Type the following (with no spaces): - d40:0008 and hit return. You will be rewarded with: 0040:0008 78 03 78 02 00 00 00 00 x.x..... and several more lines of similar numbers and letters. This is a "hex dump" of the contents of the eight memory locations starting with 40:0008h. The first six number pairs are the base addresses of the installed printer adapters. In the example above, the first adapter is at 0378h, the second is at 0278h, and there is no LPT3 installed. The number pairs are reversed because Intel stores such numbers in a 'low byte-high byte' format. If you only have one adapter installed, this tells you all you need to know. If you have more than one, you still have to figure out which 25 pin printer connector is which, either by opening up your computer and checking the address selection jumpers on the adapter cards, or by doing the experiment on the next page. After you write down the addresses displayed, you can exit DEBUG by typing q. You can buy a printer adapter card for $10 to $20 and set it to a different address than the one already in your computer so you can play around with hardware I/O and still have your printer plugged in and ready to go. In fact, if your printer port is on a multi I/O card or on your motherboard you should certainly do this, so if you slip up and burn the port out, the damage is limited. Let's make something happen: If you write a number to the base address I/O port, pin 2 on the 25 pin printer connector on the back of your pc corresponds to bit 0 and pin 9 corresponds to bit 7 and the ones in between are in order. So if you output binary 10101010 (=hex AA =dec 170) on port 378h, and then take a meter to the pins on the printer connector, you will find 5 volts on pins 9, 7, 5, and 3, and zero on pins 8, 6,4, and 2. When I say 5 and 0 volts, I mean whatever is an LS-TTL high and low, which may be between 2.4 and 5.0 volts for a high and 0.8 and 0.0 volts for a low. Most printer adapters now days are made from CMOS LSI chips, and the unloaded signals will be close to 5 and 0 volts. These outputs are latched, so you don't need a software loop or anything, they will stay there until a new number is output to the printer port. In BASIC the relevant statement is OUT p,n where p is port and n is data, ie the number you want to output. Try this program. 10 INPUT "Enter a number 0 to 255 ", N% 20 OUT &H378, N% (the &H tells BASIC that the number is hexadecimal, otherwise it expects decimal numbers) If you run this program, you should then find the pattern of voltages on the printer connector corresponding to the bits of the number you entered. If your printer adapter is set to 378h, that is. If it doesn't work, try &H278 or &H3BC in line 20. The assembly language equivalent to the BASIC OUT statement is: MOV DX,p MOV AL,n OUT DX,AL Inputing data to the PC is a little more complicated. Bits 3,4,5,6,and 7 correspond to connector pins 15, 13,12, 10, and 11 (note that they are not in order). When you input a byte from the status port at 379h, bits 3 through 7 will be set according to what voltage is on the connector pins listed above. I don't know what is on bits 0, 1, and 2, but they are not determined by connector voltages. Another complication is that the input from pin 11 is inverted, so a ground on the pin gives a one bit and 5 volts gives a zero bit. The other inputs are the right way round. By the way, these are printer signals Error, Select, Paper out, Acknowledge, and Busy. In BASIC to read a port you use the INP function. To be useful, you need to assign the number that you read to a variable. For example: 10 K% = INP(&H379) 20 N% = K% AND &HF8 rem F8h=binary 11111000. Strip last 3 bits 30 PRINT N% If you run this with nothing connected to the printer connector, you should get 120 (= 78h = 01111000 bin). This is because an open TTL input usually reads high. Remember that the top bit is inverted, and the last three bits are not connected and are zero'ed out in line 20 of the program. Now ground pins 15, 13, 12, 10, and 11 one at a time to see the effect on the number returned. Use a 2000 ohm resistor to do the grounding, and you won't burn anything out if you stick it in the wrong hole. The assembly equivalent of INP is: MOV DX,p IN AL,DX Where p is the input port (base+1). The control port at base+2 (37Ah to follow the examples above), can be used for four more output bits if you need them. Pins 1, 14, 16, and 17 correspond to bits 0 through 3 of a byte output to the control port and bits 0, 1, and 3 of this are inverted. In addition, bit 4 of this control byte is not connected to the printer connector, but if it is set, then the next time pin 10 (the Acknowledge signal) goes from high to low, the processor is interrupted. This will invoke the interrupt service routine for the Centronics protocol, and who knows what will happen if you are trying to control a robot or something at the time. You can also use this port for input. This is complicated, but may be worth it if you need a full 8 bit byte of input. The penalty comes in lines of programming needed to do it since you first need to set the bottom three control outputs high, read from the control and status ports, mask off the unused bits, assemble the two sets of bits into a byte, then flip the inverted bits. The control port can be used for input because it has open collector output circuits, and if you set the outputs high then an external signal can determine the voltage on pins 1, 14, 16, and 17 on the parallel connector. Bits 0, 1, and 3 are inverted on input as well as output. To summarize: on the 25 pin female 'D' connector on the printer adapter, pins 2 through 9 correspond to bits 0 through 7 of a byte output to the adapter base address. The voltages on pins 15,13,12,10, and 11 determine bits 3 through 7 of the byte input from base+1, and bit 7 will be inverted. Pins 1, 14, 16, and 17 are driven by bits 0 through 3 output to the control port at base+2, and bits 0, 1, and 3 are inverted. If the outputs on these pins are set high by writing binary 00000100 to base+2 (remember the inverted bits) then external signals can be input on these pins. Pull-up resistors of 4.7k are built into the printer adapter. Pins 18 to 25 of the parallel connector are ground. Unfortunately, 5 volts does not appear on the connector, so if you need power for interface circuits you will need an external supply or to get it from somewhere inside the computer. Be careful since you can cause expensive damage by putting 5 volts in the wrong place or shorting it to ground. So what can we do with this? The data bits can source a couple of milliamps, so you could light (dimly) up to eight LED's in patterns of your choice. (This is one way to burn out your printer adapter, so be sure you use the right size resistor to limit the current to 2.6 mA if you are going to attach LED's). Or, with an external power supply, you could drive transistors to operate relays to control up to eight devices. The inputs could be used to keep track of whether five devices were on or off. Not too exciting so far. More possibilities are opened up with an A to D converter, so you can measure things and perhaps make decisions based on the measurements. The ADC0831 is a nifty eight bit A-D converter which can be operated with two output and one input lines, leaving lots of I/O lines still available for doing stuff. Description of this is left for another time. How to Input 8 bits Through a Unidirectional Parallel Port Tom Dickinson tsd@bnl.gov The scheme I suggest for 8 bit input uses the three lowest bits of the control port and the five high bits from the status port. This is also described in Zhahai Stewart's "Parallel Port FAQ" in section 16, mode 3B. We evidently thought of this independently, but it is pretty obvious. This leaves one control bit (bit 3) unused, and it could be used as a bidirectional control or strobe bit as mentioned in the faq. Here are some code examples: 8 bit output through the data port: OUT &H378, N 'where N is the number you want to output In assembly this is: MOV DX,p MOV AL,n OUT DX,AL ' p is port and n is data Here is an example of an 8 bit input using 3 control bits and 5 status bits. This is in basic to show the operations involved. (I haven't tried this code, so it may have bugs) The bits to be input are connected to the following pins: BIT 7 6 5 4 3 2 1 0 PIN 11 10 12 13 15 16 14 1 OUT &H37A, &H04 'set output of bits 0-3 of cont. port high LO = INP(&H37A) AND &H07 'input and mask bottom 3 bits HI = INP(&H379) AND &HF8 'input and mask top 5 bits BYTE = LO OR HI 'combine LO and HI into 8 bits RITEBYTE = BYTE XOR &H83 'flip bits 0, 1, and 7 Ritebyte is the number (0 to 255) represented by the 8 bits input on the connector. The first line above may be a bit confusing, but remember that bits 0, 1, and 3 are inverted, so to get 1111 on the pins, you need to send out 0100. 04h = 00000100, so we also are zeroing the top 4 bits. This insures that the interrupt enable on bit four is not set, which might cause trouble. In basic this routine is apt to be pretty slow, and if you need speed, you should code in assembly. These are byte wide logical operations which are very fast in machine language. You would then pass the byte to a high level language to use as an integer variable. I understand that C can do this kind of stuff very fast, if optimized. I have no experience with this. ------------------------------------------------------------------ Thomas S. Dickinson Internet: tsd@bnl.gov National Synchrotron Light Source (516) 282-7196 (work) Brookhaven National Laboratory (516) 282-3238 (fax) Upton, NY 11973 USA also dickinso@ls7501.nsls.bnl.gov and dickinso@bnlls1.nsls.bnl.gov -----------------------------------------------------------------