Parallel Port Games part II

So after you've read and played with the previous page, which shows you how to use the parallel port for output, now you're ready to play with INPUT.
This is necessarily going to be far more complex.

There are two main ways in which the parallel port can read information: via status and control lines, or via the data lines we've used previously.

Because I already have wires from the data lines, I'm going to concentrate on using the data lines in what's called bidirectional mode, where the outputs from the computer are put in a state where they are neither high nor low, neither 1 or 0. Instead they are essentially floating, so they can be pushed to a value by an external source, and then read by the computer.

In order to do this, the first thing you have to do is instruct the computer to put the port in bidirectional mode, by writing to the control register.

"What," you might think, "is a control register?"

Well, you remember that we spent a lot of time writing to the address h378. That is the parallel port data register. One byte of memory above that is the status register, where the five lines from the parallel port deposit data from the other end of the parallel cable, from eg. the printer. One byte of memory above THAT is the control register, which has four lines of output and a couple other bits that tell the parallel port how to act. One of these is the control register for bidirectional mode.

If you put the port in bidirectional mode and then write data to it, the port should ignore the data. So, if you put the port in bid mode, write to it, read from it, and get a different answer than the data you wrote to it, you have successfully put the port in bidirectional mode. (In other words, it has ignored what you wrote to it.) If the data you receive is the same as the data written, probably you're not in bidirectional mode, and it's time to start reading more about your particular motherboard. Older motherboards do not support bidirectional mode, and some newer ones have odd control registers so you have to write to multiple places. So here's some qbasic code that tries to set your port in bid mode:

    dataline% = &H378                            ' address of parallel port data lines
    statline% = dataline% + 1                    ' address of status (in) lines
    ctrlline% = dataline% + 2                    ' address of control register
    bidir% = 32                                  ' bit 5 of control register

PRINT "Attempting to enter bidirectional mode"
    y% = INP(ctrlline%)                          ' get contents of control reg
    PRINT "Start control register: ", y%         ' display
    OUT ctrlline%, y% AND bidir%                 ' set bidir bit on control reg
    y% = INP(ctrlline%)                          ' get new contents of control reg 
    PRINT "Bidir control register: ", y%         ' display

       ' at this point the control register has been initialized to run
       '  in bidirectional mode; the data lines should be tristated.
       '  If they are, data written to the lines will be ignored.
       '  We can test for this by writing some random data ('111') out to
       '  the port, then trying to detect data on the port.  If it detects
       '  the same data we wrote, it is not in bidirectional mode.  (this
       '  assumes the data value is not '0' or '255' because in tristate
       '  mode it is probable that the trying to read the port will yield 
       '  one of these two values.)

    OUT dataline%, 111                            ' dump arbitrary number to data lines
    y% = INP(dataline%)                           ' read data lines
    IF y% = 111 THEN                              ' compare
       PRINT "This port does not seem to support bidirectional mode."
       END
    ELSE 
       PRINT "Cool.  The port is now in bidirectional mode."
    END IF

       ' now let's put it back where it was and exit gracefully.
  
    indata% = INP(ctrlline%)
    OUT ctrlline%, indata% XOR bidir%             ' This unsets only bit 5.
    PRINT "The port is restored to normal operation."
  END
If the program indicates your port was (briefly) in bidirectional mode, you're ready to go onwards. If not, however, don't despair. There are ways to work around this, many of which are surveyed in the manifold and fabulous pages of beyondlogic's parallel port pages. This is always a good resource to look at when you're stumped.

But let's say you have in fact gotten your system in bidirectional mode and now you want to do something with it.

Here is an extremely simple data logging program:

    dataline% = &H378            ' address of parallel port data lines
    statline% = dataline% + 1    ' address of status (in) lines
    ctrlline% = dataline% + 2    ' address of control register
    bidir% = 32                  ' mask for input
  
  OUT ctrlline%, bidir%          ' set control register bit 5
  INPUT "Enter a file name for data storage:" file$
  OPEN file$ FOR OUTPUT AS #1

  PRINT "hit ESC to exit: exit will not be immediate."
  LOOP
    indata% = INP(dataline%)     ' get data
    PRINT "data: ";indata%       ' display 
    PRINT #1, indata%            ' write to file
    GOSUB pause:                 ' cheesy pause routine
  WHILE INKEY$ <> "ESC"          ' go forever until someone escapes out.
    CLOSE                        ' close file$
    END

pause:                           ' another arbitrary pause subroutine
  FOR x% = 1 TO 1000             ' change these to change pause.
  FOR y% = 1 TO 1000
  NEXT y%
  NEXT x%
  RETURN

Now, there is no subtlety in this. However, it will read data from the port and write it to a file. You can test this by shorting one of the data lines to ground with a resistor, and seeing the change in what has been read.

Because I'm trying to develop this, rather than trying to teach programming (a job for which I am manifestly unqualified) the next program is going to involve a whole lot more material. This program logs data based on the qbasic timer, and includes a subroutine which ensures that the parallel port is indeed in bidirectional mode so you won't mash something if your system is incompatible. In other words, it is a close-to-real-time combination of the two above programs. It also reads one hundred values from the port and averages them before writing them to a text file, and writes data with the intent of the data being imported into a program like Excel for statistical analysis.

    dataline% = &H378                    ' address of parallel port data lines
    statline% = dataline% + 1            ' address of status (in) lines
    ctrlline% = dataline% + 2            ' address of control register
    bidir% = 32                          ' mask for bidirectional mode
    iter% = 1                            ' counter for loop
    DIM ave AS LONG                      ' ave is the sum of 100 sequential measurements.

    CLS
    INPUT "Total run time (in seconds):"; cycletime%
    INPUT "Time between records (in seconds):"; pausetime%
  
    ON TIMER(pausetime%) GOSUB average:
    TIMER ON
    CLS

    OUT ctrlline%, bidir%                ' set bidir bit
    OUT dataline%, 111                   ' write random number out
    y% = INP(dataline%)                  ' read data lines 
    IF y% = 111 THEN                     ' compare.  They should NOT be the same.
       PRINT "This port does not seem to support bidirectional mode."
       END
    END IF

    INPUT "Data file to open?", file$
    OPEN file$ FOR OUTPUT AS #1
    PRINT "starting datacq."
 
    ' loop.  Timer triggers gosub line up above this, which goes into the recording subroutine.

  PRINT "Start time: "; TIME$
    started = TIMER
    WHILE elapsed% < cycletime%
        elapsed% = TIMER - started
    WEND
    PRINT "Exiting.  Port in bidirectional mode; reset this if it is used for other projects."

   '  unremark the following lines to have the parallel port restored to normal after program exit;
   '   I left it in input mode because putting it in output mode with powered inputs still attached has
   '   the potential of injuring the parallel port driver chip.
  
   '  OUT ctrlline%, 0                 ' reset to output mode
   '  OUT dataline%, 0                 ' reset pins to ground
    CLOSE
    END

average:                               ' take 100 readings
    counter% = 1
    ave = 0
    WHILE counter% < 100
      FOR x% = 1 TO 100                ' arbitrary delay so the 100 measurements aren't instantaneous.
      NEXT x%
      ave = ave + INP(dataline%)       ' sum
      counter% = counter% + 1          ' increment counter
    WEND
    GOSUB record                       ' have 100 measurements: go to the recording sub
  RETURN

record:                                ' print results of average to screen and file
    indata% = ave / 100
    PRINT "record"; iter%; " value "; indata%; " at ", TIME$   
    PRINT #1, iter%; ","; indata%; ","; TIME$                  ' print comma-delimited data
    iter% = iter% + 1
  RETURN
  
Now, this sort of begs for something to record, or put another way, let's do some hardware.

This is going to get ugly. I'm going to present this in functional modules, each of which I built on a protoboard, and each of which I got working by built-in test circuitry.

First, we're going to work with the analog-to-digital converter. For my project, as it stands, I used the National Semiconductor ADC0804 analog to digital eight bit converter, which is available at jameco (part number 10153) among other places. You can download the data sheets which might help you quite a bit.
I chose this particular converter because it's cheap, (jameco lists it for $3) fairly accurate, (~1%) takes very little external hardware, (one regulator, one cap and, as I have it set up, one resistor and three trimmer resistors) and has an output of eight bits, exactly the same as the width of the parallel port data line. By golly.


The important pins in this are pins 6, 7, and 9. 6 is the input, while 7 and 9 act to bias the upper and lower bounds of the input -- in other words, they can be used to make the ADC register its full range on an input that has less than zero to five volt range. I built this schematic based on the chip datasheet with a couple modifications for my own purposes, and it works fairly well. The switch shown at the bottom, that shorts pins 3 and 5 to ground, is a momentary contact that signals the ADC that we are ready to start running. The ADC will run without this, but its output is unreliable. (Of course, you can also write to a control line to do this but I didn't. I'm lazy. I just bridge it with a pair of pliers.)

Here's a test rig that adds eight led's and a current-limiting resistor so you can test the ADC operation and get a nice visual confirmation. (Make sure you get the LED polarity right: if you have one in backwards, they will light up in either forward or reverse direction, as they conduct via two LED's in series, so you get very weird results that make you doubt the ADC's operation, sez I with frustrated hindsight.) As you turn the potentiometer connected to pin 6, the LED's should strobe a binary output proportional to the input voltage.

Careful adjustment of the potentiometers attached to pins 7 and 9 adjusts the full-scale range of the input, and the potentiometer on pin 6 simulates an input between 0 and 5 volts. Note that the pot attached to pin 7 adjusts the lower bound and should be delivering a voltage of something like 50 millivolts, while the pot attached to pin 9 determines half the full scale voltage, so it should read less than 2.5 volts. (which would result in a 5 volt full scale reading.)

Now that we've got THAT done, let's provide it with an input. I used a type k thermocouple (which I had sitting around; I don't know quite where you could get one, although Don Lancaster's website often has this sort of stuff for sale in the bargain department. Companies like Newark Electronics and Jameco also carry thermocouple parts.)

The output of a thermocouple ranges from -6 to 53 millivolts. Normal temperatures encountered in houses and people will have voltages between roughly 0 and 4 millivolts. Although it is (theoretically) possible to adjust the lower and upper potentiometers on the ADC to span only this range, it's tricky and ugly. Instead, I used an op amp.

The resistors R1 and R2 are chosen to match the required gain of your application, where gain = R1/R2. (technically it's r1/r2 + 1; we can ignore that for large numbers.)
For my application, I have an output that ranges from roughly 0 to 5 millivolts, and I would like an output that ranges from 0 to 5 volts, so I need an amplification of 1000:1. As a rough solution, therefore, I can use a 22 ohm resistor for R2 and a 22,000 ohm resistor (22K) for R1 and get a good result. Even better, as shown in the schematic, I can use a variable resistor to adjust the gain. I can test the result by firing up the op amp with the required bipolar power supply (TWO nine volt batteries in series, so V+ is nine volts, and V- is negative nine volts) and hooking the thermocouple positive lead to the signal in, and the negative lead to the (virtual) ground. (I refer to it as a virtual ground because it is at or near zero volts, since it is floating between the two batteries.)

Op amps will sometimes have some offset between their inputs and ground, meaning the output of the amp will be offset by the gain times the input offset; I used a dual-stage amplifier with a second potentiometer (r4) to zero the output. By careful manipulation of R2 and R4, you can determine both the gain (the slope) and the bias (the y-intercept) of your amp. What this means is that you can set the amp to output zero volts at the lowest temperature you expect to measure, and 5 volts at the highest temperature. Yes, this duplicates the potentiometers on the ADC module, but I'm using both these modules for other projects as well as this one, and it's much easier to put in pots than fool around with resistors, trying to get exactly the right values. Of course, the downside of this is that once you've calibrated the system, you have to leave those pots alone; any fiddling with them will distort your readings. So once you've got everything working, pull them, measure their resistances, build resistor networks that match them as closely as possible, put everything back, and solder it all in place, then recalibrate. Hassle, huh.

Now, because this is a sort of hastily thrown together project, with several different power supplies, I end up chaining the modules together, rather than integrating them fully. That's a fancy way of saying I end up powering them with three nine volt batteries, one running the ADC and two running the op amp. In a finer, brighter world I would power them all three with a dedicated power supply with +12v/-12v and some regulators; in a perfect world I would have a power supply that would derive these voltages from a disc power cable so the whole thing would be powered by your computer. But let's all say it together in unison: It's Not A Perfect World. I'm in a hurry and this works. If you want to build this power supply I'll link to it, but seeing as I can buy one for $6.95 from Jameco, I refuse to bother with it.

So, ignoring LOTS of details, here's a block diagram of the setup.

In this, blue represents data flowing from the thermocouple through the amp and the ADC to the parallel data lines, black is attached to the negative terminal of a nine volt battery, red is attached to the positive terminal, and green represents system ground, continuous from the data port to the negative lead of the ADC power supply and the node between the two power supplies for the op amp. This looks ugly (particularly the op amp node, where, yes, a red, a green, and a black all come together) but the idea behind this is that if each module works correctly, which you have tested extensively before hooking the whole mess up, then each connection between modules will have at least 0 volts and at most 5 volts on it.

To sum up, it works, for me, and is happily recording my house temperature with the op amp gain set to about 1500, or my oven, with the gain set to about 200.

Next, we can run a relay from the parallel port outputs based on the temperature readings from the thermocouple, and have an intelligent oven thermostat, and then we can fuss with the software so it'll follow a heating/cooling schedule and provide a full oven controller.

You might wonder how much this costs. Here's a rough breakdown:

Total cost about $40. Total time to get the system working: probably twenty hours, but to rewire it now that I've worked the bugs out, maybe two hours. Not too bad, given that the modules can be used for other projects.

This page created 8/16/02, last modified 8/16/02.

Questions, comments, suggestions: email me!

Back to the main page.