Using the Parallel Adapter as a Host Interface Port
New uses for old tools
Dhananjay V. Gadre
Dhananjay is a scientific officer for the instrumentation laboratory at the Inter-University Centre for Astronomy & Astrophysics (IUCAA) in Pune, India. He can be contacted at [email protected].
There's a world of difference between cameras used to collect astronomical image data and those we use for home videos. Personal video cameras typically provide 500x500-pixel resolution, but Astronomical Image Cameras usually offer 2048x2048 resolution; while emerging state-of-the-art cameras called "mosaic CCDs" provide 8096x8096 resolution. With data-acquisition cameras, the pixel voltage is almost always encoded to 16 bits. Consequently, each frame of data can range in size from 8 MB to 12 MB.
In a recent "star-gazing" project involving data acquisition for astronomy-related applications, I needed to connect a high-performance, charge coupled devices (CCD) camera controller to a PC. Eventually the controller would be connected to the PC via a high-speed fiber-optic link. For prototyping and diagnostic purposes, however, I decided to use the PC's parallel-printer port as a link to the camera controller. (The RS-232 interface was immediately ruled out because, at more than 20 KB/sec, the data-transfer rate was too high. Nor, for that matter, did I want to invest in a dedicated async transmission circuit on the controller.) In this article, I'll describe the link between a PC and the controller using the parallel-printer adapter. While this discussion is based on a digital-signal processor (DSP), the design is flexible enough to accommodate virtually any microprocessor or controller, even an 8051.
The camera controller is built around a fixed-point DSP from Analog Devices--the ADSP-2101. Several features make this processor an ideal camera controller: It is fast, executes any instruction in one clock cycle, has zero-overhead looping, has on-chip program and data memory, and has two buffered synchronous serial ports capable of transmission rates up to 5 Mbits/sec.
As a camera controller, the DSP helps acquire CCD images. The image parameters are set by the user through a host computer. These parameters define the exposure time, size of the CCD image, pixel, row binning, and so on. To do this, the ADSP-2101 performs the following:
- Receives commands from the host.
- Waits for the signal-integration time.
- Generates CCD clock waveforms to shift out each pixel signal.
- Reads CCD pixel signal voltage through an analog/digital converter (ADC).
- Transfers the ADC data over a suitable link to the host.
The components of the controller are:
- A backplane bus that carries interconnections between the various cards of the CCD controller.
- An ADSP-2101 processor card to implement a programmable-waveform generator, optional host-communication link (using the synchronous serial interface of the DSP), serial ADC interface, and backplane bus interface to connect the other components. The waveform generator is a crucial component of the CCD controller. Having a programmable-waveform generator allows users to operate the CCD camera in a variety of modes by downloading a new waveform-description table from the host in real time.
- A high-speed (50 Mbits/sec) serial link using a TAXI chipset that interfaces to an 850-nm fiber-optic physical link. This card connects to the backplane bus. The TAXI chipset receives 8-bit-wide characters from the DSP card to be transmitted on the fiber link to the host. Characters received from the host are read by the DSP, eight bits at a time.
- A temperature controller, shutter driver, and telemetry card connect to the backplane bus. This card has a proportional-control temperature controller to maintain the CCD chip temperature, which is set by a preset potentiometer. A voltage multiplier using a high-frequency transformer charges a reservoir capacitor that discharges into the shutter through a FET switch when the shutter is to be operated. The DSP controls the voltage multiplier and shutter operation. A multichannel, 12-bit serial ADC reads chip temperature, dewar temperature, shutter status, and so on. This card also has stepper-motor drivers for controlling two stepper motors.
- An analog-signal processor (with double-correlated sampling) and serial ADC card. This card also connects to the backplane. The ASP circuitry receives the CCD pixel voltage from the backplane bus. This voltage is encoded by the 16-bit serial ADC. The serial-ADC-control signals on the backplane bus are derived from the serial port of the DSP.
- A CCD clock bias and driver card that uses the majority of waveform signals (generated by the DSP card) present on the backplane bus. These signals pass through appropriate line drivers before being filtered on the backplane card. The CCD clock levels are referenced by the bias voltage generator on this card. The bias voltages are generated by 24 DACs initialized and controlled by the DSP.
Testing the Camera Controller
The most important components of the controller are the DSP processor card, bias/clock-driver card, and signal-conditioning/ADC card. Prototyping a CCD controller begins with these functions. However, testing the functions and the performance of the CCD controller require that the host computer send parameters and receive encoded data.
A host interface port (HIP) to connect the controller hardware to the PC via the parallel-printer adapter simplifies evaluation of the ADC performance and allows testing the waveform-generator algorithm and noise characteristics of the signal-conditioning circuit. The bidirectional data-transfer of the PC parallel-printer port provides high enough data-transfer rates to support prototyping. Also, the parallel port can be controlled by manipulating common latches, buffers, and decoders. At the software level, only device-level routines need to be modified, as the final optical link is a parallel port at the host end.
The Parallel-Printer Adapter
The printer adapter has three independent TTL-compatible ports:
- The data port, an 8-bit output port.
- The control port, a 4-bit output port with open collector drivers.
- The status port, a 5-bit input port.
The parallel-printer adapter can be converted into a simple HIP using the circuitry in Figure 2. (Figure 3, on the other hand, shows how to connect the HIP to an 8051-based circuit.) The port can be used to connect virtually any processor to the PC for bidirectional data transfer. A conservative estimate of the data-transfer rates between a 486/66-MHz PC and the CCD controller is in the range of 20-50 KB/sec. The test program uses routines in Listing One for communication. Suitable programs are written for the DSP on the CCD controller. Coding parts of the routines in Listing One in assembler can significantly increase data-transfer rates.
To convert the parallel-printer adapter into a host interface, the host uses the data port to transmit eight bits of data to the application. A flip-flop U6-A is reset to indicate to the application hardware that a data byte is available. This flag could also be used to interrupt the application hardware. In my case, the DSP application hardware reads the data bits through its input port buffer U5 and sets up the flag U6-A. The host program monitors U6-A before transmitting a new byte.
To receive a byte from the application hardware, the host monitors flag U6-B. If it is reset, a byte is ready for it. Reading this byte is tricky, as the parallel-printer adapter can read only five bits at a time, so a set of tristate buffers U1 and U2 are used. U1 allows eight bits at its input and transmits only four of these inputs to the output. Nibble control pins 1G and 2G on U1 and U2 are controlled by the decoder outputs of U4 to determine which four of the 16 possible inputs are connected to the output pins. The four output pins of U1 and U2 are connected to the status-port bits.
The host program manipulates the decoder U4 to enable the lower four bits of the incoming byte to reach the status port. The status port is then read and its contents stored temporarily away. The decoder is then manipulated to read the upper four bits of the byte into the status port. The actual byte is reconstructed by shifting the first status-port read four bits to the right and bitwise ORing with the second status-port read result. The seventh and third bits are complemented to reconstruct the actual byte.
Thereafter, flag U6-B is set to indicate to the application hardware that the current byte has been read by the host.
A note of caution here. The process of reading the eight bits of incoming data and the setting up of the flag by the host is not atomic. The flag is set up by executing a few instructions by the host program. On my 486/66 PC, this translates to about 5s, so for error-free data transmission, the application hardware waits for, say, 10s after the byte has been read by the host, before transmitting a new byte to the host. Such a precaution is not required for data transmission from the host to the application hardware, as reading the byte and setting up the flag U6-A is atomic.
HIP Software
Listing One is a sample program that communicates bidirectionally through the HIP. The program contains four routines that help regulate data flow. (Additional support software is available electronically; see "Availability," page 3.)
The function rxstat() reads the status of U6-B. U6-B is reset by the controller to indicate that a byte has been latched into U3. rxstat() is used by the main program to detect the presence of a new byte before reading the byte.
Function txstat() returns the status of U6-A. When the host program transmits a byte to the controller, it resets U6-A. After the controller reads the byte, U6-A is set by the controller. The host program uses txstat() to ensure that the previously transmitted byte has been read by the controller.
The functions rx() and tx() receive and transmit a byte, respectively. Function rx() reads the byte sent by the controller (as indicated by a logic 0 of flag U6-B). After the byte is read, rx() sets U6-B to logic 1. Function tx() resets flag U6-A after a byte is latched into the data-port register of the printer adapter.
The main program reads the status of the printer-adapter ports and transmits a sequence of 256 bytes to the DSP application circuit. The application circuit echoes back the received bytes. This is a good test for missing bytes.
It is possible to add interrupt capability to the HIP. The interrupt signal is derived from pin 10 of the 25-pin D as well as the 36-pin Centronics connector (connected to bit 6 of the status port). A logic 1 (the control-port bit 4) enables pin 10 to interrupt the PC. The interrupt signal can be derived from either U6-A or U6-B. Apart from incorporating an interrupt routine, the current HIP design would need modifications.
With increasing PC speeds, it is possible to induce glitches on the control-port bits using standard printer cables. I'd advise using a cable with a twisted pair of wires for each signal.
References
Gadre, Dhananjay V. "Parallel-Printer Adapters Find New Use as Instrument Interfaces." EDN (June 22, 1995).
Gadre, Dhananjay V., Pramod K. Upadhyay, and Vijaya S. Varma. "Choosing the Right Bus, Part II: Using the Parallel Printer Adapter as an Inexpensive Interface." Computers in Physics (January/February 1994).
Hook, Brian and Dennis Shuman. "Digital I/O with the PC." Dr. Dobb's Journal (April 1994).
Smith, Roger. "Programmable Digital Waveform Generator." Microprocessors and Microsystems (April, 1989).
Webb, Steve. "Two Chip Video Digitizer." Electronics World + Wireless World (June 1995).
Figure 1: CCD camera-controller block diagram.
Figure 2: ADSP-2101-based HIP design.
Figure 3: 8051-based HIP design.
Table 1: Pin number for the bits of the three printer-adapter ports. * Indicates that the pin-to-bit relation is inverted: A logic 0 at the pin corresponds to a logic 1 in the corresponding register. The $ entry at bit 4 in the control-port register is the interrupt-enable bit.
Listing One
/* Connect PC through the printer adapter to the DSP-based CCD controller.*/ /* Dhananjay V. Gadre */ #include<stdio.h> #include<conio.h> #include<dos.h> #include<process.h> #include<time.h> /* external variables */ extern unsigned dport, sport, cport; /* external routines. gets addresses of 3 ports from the DOS data RAM */ extern void set_lpt_base_address(int); /* status port */ #define pin_11 0x80 #define pin_10 0x40 #define pin_12 0x20 #define pin_13 0x10 #define pin_32 0x08 /* control port */ #define pin_17 0x08 #define pin_16 0x04 #define pin_14 0x02 #define pin_1 0x01 /* op & ip semaphores */ #define ip_buffer_flag 0x04 #define ip_buffer_Flag 0xfb /* this flag is on bit 2 (pin 16 ) of the control port and can set by a logic low on the pin 16*/ #define op_latch_flag 0x08 #define op_latch_Flag 0xf7 /* this flag is set by pulsing a low on pin 17 (bit 3) of the control port. SET condition of this flag indicates that the oplatch contains a new byte */ /* local routines */ unsigned char txstat(void); /* check to see if the o/p latch is empty; empty=0 */ unsigned char rxstat(void); /* check to see if the i/p buffer has any char; empty=0 */ void tx(unsigned char); /* transmit the char to the latch */ unsigned char rx(void); /* receive a char from the buffer */ void enable_nibble(unsigned char); /* this function controls which nibble gets connected to status port pins */ /* txstat: This routines checks pin_13 of printer adapter status port if the PIN is SET, the o/p latch is full & should not be written to again. When the DSP reads the latch, the PIN is RESET. Now the latch can be written to again */ /* return value: 1 is latch full, 0 if latch empty */ unsigned char txstat(void) { char latch_status; enable_nibble(1); /* this function connects the sport to nibble 1*/ latch_status=inportb(sport) & pin_13; return latch_status; } /* rxstat: this function checks pin_12 of the status port. If the PIN is set, the buffer is full & should be read. if RESET, it is empty. */ /* return value: 0 if the buffer is empty, 1 if the buffer is full */ unsigned char rxstat(void) { char buffer_status; enable_nibble(1); /* this function connects the sport to nibble 1*/ buffer_status=inportb(sport) & pin_12; return buffer_status; } /* tx: This routine latches a byte into the o/p latch */ /* return value: none */ void tx(unsigned char op_byte) { unsigned char temp; outportb(dport, op_byte); /* latch the byte*/ /* now set up the op_latch_flag to indicate that a new byte is available */ temp=inportb(cport) & (0xff ^ op_latch_flag); temp=temp ^ op_latch_flag; outportb(cport, temp); temp=temp ^ op_latch_flag; temp=temp | op_latch_flag; temp=temp ^ op_latch_flag; outportb(cport, temp); return; } /* rx: This routine reads the i/p 8 bit buffer */ /* return value: the byte read from the buffer */ unsigned char rx(void) { unsigned char ip_byte, temp; enable_nibble(3); /* set the buffer to read the lower nibble */ temp=inportb(sport); temp=temp >> 4; enable_nibble(2); /* set up the buffer to read upper nibble */ ip_byte=inportb(sport); ip_byte = ip_byte & 0xf0; /* reset lower 4 bits */ ip_byte=0x88 ^ (ip_byte | temp); /* concatenate 2 nibbles & flip bit 7 & 3 */ /* now reset the flag to indicate that the byte has been read */ temp=inportb(cport) & (0xff ^ ip_buffer_flag); outportb(cport, temp); temp = temp | ip_buffer_flag; outportb(cport, temp); return ip_byte; /* return the converted byte */ } void enable_nibble(unsigned char nibble_number) { unsigned char cport_status; cport_status=( inportb(cport) & 0xfc) ; /* isolate bit 0 & 1*/ nibble_number = nibble_number & 0x03; nibble_number = 0x03 ^ nibble_number; /* invert bit 0 & 1 */ cport_status=cport_status | nibble_number; outportb(cport, cport_status); return; } main() { unsigned long count; unsigned char portval, tempp, tempq; time_t t1,t2; FILE *fp1; int temp=1; clrscr(); printf("\n\nFinding Printer adapter lpt%d...", temp); set_lpt_base_address(temp); if(dport == 0) {printf("\nPrinter adapter lpt%d not installed...", temp); exit(0); } else { printf("found. Base address: %xhex", dport); portval=inportb(sport); printf("\n\n D7 D6 D5 D4 D3 D2 D1 D0"); printf("\nStatus port value = %x %x %x %x %x X X X ", \ (portval & pin_11)>>7, (portval & pin_10)>>6, (portval & pin_12)>>5, \ (portval & pin_13)>>4, (portval & pin_32)>>3 ); portval=inportb(cport); printf("\nControl port value = X X X X %X %X %X %X ", \ (portval & pin_17)>>3, (portval & pin_16)>>2, (portval & pin_14)>>1, \ (portval & pin_1) ); portval=inportb(dport); printf("\nData port value = %X %X %X %X %X %X %X %X ", \ (portval & 0x80)>>7, (portval & 0x40)>>6, (portval & 0x20)>>5, \ (portval & 0x10)>>4, (portval & 0x08)>>3, (portval & 0x04)>>2, \ (portval & 0x02)>>1, portval & 0x01 ); printf("\n\n\n"); } /* set up reset states on the control port, all pins to logic 1 */ outportb(cport,0x04); fp1=fopen("tx_rx", "w"); t1=time(NULL); /* just to log time */ for (count=0;count<256;count++) { while (!txstat()); /* wait till the DSP application reads the previous byte*/ tx(tempp); /* transmit a byte*/ while(rxstat()); /* wait till a byte is transmitted by the DSP */ tempq=rx(); /* byte is available, read it */ fprintf(fp1, "TX=%x, RX=%x\n ", tempp, tempq); /* store it in a file */ tempp=tempp++; } fclose(fp1); t2=time(NULL); printf("time taken = %ld secs", t2-t1); }