PC ISA Bus Analog-to-Digital, Digital-to-Analog Card

Keywords: analog-to-digital, ADC, digital-to-analog, DAC, MAX158, DAC0832, temperature measurement, interrupt driven

Using your PC to measure physical parameters like temperature, pressure and sound require an analog-to-digital converter (ADC). Furthermore applications like PC-driven voltage-based motor speed control and PC-based wave generators demand a digital-to-analog converter (DAC). Commerical ADC/DAC cards can cost hundreds of dollars. This tutorial shows how you can construct your own ADC/DAC card (left photo) in an afternoon or two with widely available parts costing less than $50 while having comparable performance and features.

Motivation and Audience

This tutorial's audience is characterized by the above questions and its focus is an ADC/DAC card made from affordable off-the-self parts with good performance.

Many books and webpages canvas the ADC and/or DAC topic but often fall short of expectation. First, not everyone wants ADC and DAC, hence this tutorial doesn't use a ADC/DAC combo chip. Such chips are often pricey and difficult to get and hence this tutorial features the affordable and widely available Maxim MAX158 ADC and National Semiconductor's DAC0832 chips. You can wire up one or both chips on the same ISA prototyping card depending if you want ADC, DAC or both. Second, much of the literature features parallel port based ADC designs, but one finds data acquisition speed quite limiting. Further aggravating is that older PC's are equipped with standard parallel ports (SPP) while newer PC's have extended (EPP/ECP) ports. This difference makes porting the circuit design dependent on whether 4 or 8 bits are to be handled. The literature often fails to mention this and the builder learns this frustration after a time and money investment. Third, is that the literature often ignores hardware interrupts and use polling techniques. Again, this neglect isn't mentioned and the builder learns another lesson in frustration; data acquisition is slow, and can even yield false data or bizarre aliasing. The design featured in this tutorial was motivated by the frustrations with the existing literature. The net result is an ADC/DAC card that has:

To be fair, its perhaps best to let you know before you start collecting parts and constructing, what the tutorial does not feature: The above limitations are elaborated in the Final Words section. The current design's 1 KHz sampling frequency was determined experimentally. The time to continously collect 1000 samples was measured to be 833 usec. A hardware interrupt (IRQ3) was used to monitor the MAX158's end-of-conversion pin. The tutorial gives more details.

This tutorial doesn't claim to be the end-all to all ADC designs, nor pretend to be a your one-stop solution for your data acquisition needs. Rather it hopes to be stepping stone for more ambitious designs you envision. Given its simplicity, details and caveats you could replace the MAX158 with a higher bit resolution ADC, combine a hardware timer or add more DACs. The tutorial breakdown is follows:

Parts List and Sources

US-based vendors include Jameco, Digikey, JDR and Radio Shack. Note: Boondog has no association with these vendors.

0.1 UF CAPJAMECO1511160.101
47 UF CAPJAMECO111050.151



Headers, housings and crimps were used for quick and professional looking connections. Example Digikey part numbers are WM4002-ND (headers), WM2002-ND (housings), WM2200-ND (crimps) and WM2312-ND for crimp tool. Of course, you can choose to wirewrap/solder your circuit without these parts.

An effort was made to find a single source supplier of all parts. Digikey has every part cited in the tables. Jameco has lower prices but it does not carry the MAX158 chip.


A combination of wirewrapping and soldering was used to construct the ADC/DAC Card. Jameco's ISA prototyping card provides more than enough room for (non-critical) part placement. The photo below illustrate possible part placement for your card. As mentioned in the Motivation and Audience section, this circuit performs ADC and/or DAC. If you just want ADC, then you can omit the DAC0832 and LF353 op-amp related circuity (all parts in Table 2). Or if you prefer to only have DAC, then the MAX158 connections can be omitted. Lastly, HC TTL chips, like the 74HC138 3-to-8 decoder were used versus the more current-hungry LS style chips. I rarely use LS chips anymore, but they should pose no problem if you choose them for your circuit.


max158Schematic020501.pdf is the Acrobat file of the same schematic. You will need Adobe's free Acrobat reader to view it.

The schematic is relatively straight-forward. One possible (and common) confusing point with ISA bus circuits is the use of the prefix "A". An ISA card has a component and solder side, often called "A" and "B" respectively. There are 62 edge tabs: 31 on the component side (A1-to-A31) and 31 on the solder side (B1-to-B31). However, address lines often use the prefix "A" too. On the PC there are 20 address lines (A0-to-A19).

To avoid this prefix confusion, this tutorial uses the lower case "a" to refer to the component side's 31 edge tabs. For example a11 is the AEN pin. "b" refers to the solder side's 31 edge tabs. For example b13 identifies the /WR pin. The schematic above includes the ISA 62 pinout for referencing connections to the other components.


The circuit's 8-position double row header and shorting block are used for address decoding. Like modem or sound cards for example, your ADC/DAC card will plug into an ISA slot on your PC's motherboard. The shorting block row position is used to assign your card with a unique address. This address serves to distinguish your card from other ISA cards that may already be installed on your motherboard. The shorting block allows you to choose one of a possible eight addresses, as shown on Table 4:


For example, placing the shorting block on row 4, assigns your card an address of 260h (or 608d). The suffixes h and d refer to hex and decimal representations respectively. The Turbo C example, given in the section below, illustrates card reading and/or writing using this address.

The 74138 is a 3-to-8 decoder. The ISA pins a24, a25 and a26 serve as its three inputs and depending on their values, will yield one 8 possible outputs. This output will be used to enable the MAX158 to acquire data or the DAC0832 to transmit a voltage.

For example, the card address, 608d is 1001100000 binary:

which results in a24, a25 and a26 to be 0, 1 and 1 respectively. These 3 values feed into the 74138's A, B and C inputs thus selecting the 74138's Y3 output:

In addition, for Y3 to go active low, the 74138's /G2A, /G2B and G1 must 0, 0 and 1 respectively. 608d already sets /G2B and G1 to 0 and 1 respectively, as seen by its binary representation figure above. The 74138's G1 pin is connected to ISA bus pin a11 (AEN). AEN = 0 whenever the expansion bus is called. This happens whenever an outportb(608, someValue) or inportb(608) is invoked in Turbo C. In QuickBasic, AEN = 0 when out(608, someValue) or inp(608) is executed.

The MAX158 ADC's pin 13 (/INT) goes low once data has been successfully acquired. This end-of-conversion (EOC) state is used to trigger a hardware interrupt (IRQ3 off the ISA bus, pin b25) and serviced by an interrupt service routine (ISR). You can refer to the Hardware Interrupt tutorial if you need an IRQ primer. IRQ3 was used but you can use any interrupt you wish to. The schematic above gives the ISA bus pin numbers for IRQ's 2 through 7.

You'll recall that a rising edge signal (+5V) on IRQ3 will generate a hardware interrupt. As such, the 74125 (a 3-state, non-inverting quad buffer, see above photo) is used with /INT to generate this rising edge signal. The following figure may be helpful:

At EOC, /INT goes low which allows the +5V input (74125 pin 2) to pass through and exit (74125 pin 3). This results in sending a +5V signal to IRQ3 and thus trigger a hardware interrupt. If the ADC is still busy acquiring data, /INT is high and the 74125 pin 3 remains at a high impedance state and IRQ3 is not triggered.

Using a hardware interrupt is your option. As stated in the Motivation and Audience section, most build-it-yourself tutorials avoid interrupts, favoring simplicity. However you must resort to polling which limits performance (slower sampling time). Experienced hobbyists will know that programming delays in time slices less than 55 msec is rather involved resulting in a 18 Hz sampling frequency at best. Furthermore, not monitoring EOC can result in bad data. The above however shows that implementing a hardware interrupt does not require much overhead and the associated ISR (explained in the Programming Section) is not complicated at all. The end result is a much faster sampling rate (apx. 1 KHz) and correctly sampled data.

The DAC0832 is a single channel, 8-bit digital-to-analog converter. The schematic uses VREF=+5V as the reference voltage (VREF on DAC0832 pin 8) and the LF353 is wired as a current-to-voltage converter (the 20K resistor was formed from two 10K resistors in series). The net effect is the DAC0832 provides a bi-polar output between -VREF to +VREF.

/CS (DAC0832 pin 1) is wired to the 8-pin double row header and hence the DAC's address is the base address (see Table 4). For example, if the shorting block is across row position 4, a Turbo C statement:

        outportb(608, voltageOutInDecimal);

will yield an analog output (VOLTAGE OUT LF353 pin 7) of +5 Volts if voltageOutInDecimal is 255. If voltageOutInDecimal is 0, then the analog output is -5 Volts. This is according to the following formula:
        VOLTAGE OUT = VREF*(voltageOutInDecimal - 128)/128
The MAX158 can acquire data from up to eight sensors (MAX158 pin names A0 through A7). As such, you can construct a terminal expansion board (TEB) using a small breadboard populated with terminal connectors (Jameco Part 152346). Your sensors are screwed into these terminal connectors and your TEB is tethered by ribbon cable (up to 25 feet long) to your ADC/DAC prototyping card sitting in your motherboard's ISA slot.

A terminal connector alternative is to use a 40-pin wirewrapping socket (above photo). I frequently attach and remove sensors and find it convenient to just plug their wire ends into a DIP socket. Of course, for more permanent and secure connections, terminal connectors are preferred.


DOS Turbo C code samples illustrate that programming your card is quite simple. Three different examples are provided to exercise your card's features: ADC.C: Analog-to-Digital conversion only; DAC.C: Digital-to-Analog conversion only; ADDA.C: Both analog and digital conversion. Note: do not cut and paste these code samples from your web browser. Download them by clicking on their hyperlinks. The MAX158 allows you to acquire data from eight channels. This example illustrates first channel (A0) reading. 5000 samples are collected and stored in an array named data and written to an ASCII file named data.txt. Almost all spreadsheets, like Microsoft Excel, and graphing programs, like GNU plot, can read and plot such ASCII files.

ADC.C uses an interrupt service routine (ISR), called eocTrue() to service IRQ3. This was introduced in section Hardware Interrupt and 74HC125. The ISR monitors the MAX158's /INT pin for end-of-conversion (EOC) and ensures that data is correctly read. ADC.C also uses a 608 decimal base address as per section Shorting Block, Address Decoding and 74HC138 and Table 4. Code and explanations follow:

DOS Turbo C code
Note: download ADC.C rather than cutting and pasting from below.

#include < stdio.h >      /* Replace quotes with <> if necessary */
#include < stdlib.h >
#include < dos.h > 
#include < conio.h >  
#define IRQ3         0x0b      /* IRQ3 */
#define BASEADDRESS  608       /* Shorting block on row 4 of card */
#define TRUE         1         /* Boolean */
#define FALSE        0         /* Boolean */
#define MAXSIZE      5000      /* Total number of samples */

/* globals */
int   EOC;                     /* End of conversion boolean */
int   readData;                /* Byte acquired from ADC (in decimal) */
float i;                       /* Sample iteration number */
float data[MAXSIZE][2];        /* Array of acquired samples */
FILE* fp;                      /* File pointer to ASCII data file */
int   j;                       /* Dummy counter variable */

/* prototypes */
void interrupt (*oldIrq3)(void);  /* IRQ 3 original setting */
void interrupt eocTrue(void);     /* IRQ 3 new ISR */

int main(void)
  fp = fopen("data.txt", "wt");   /* ASCII data file */

  /* initialize data array to 0.0 */
  for(j=0; j < MAXSIZE; j++) {
    data[j][0] = 0.0;
    data[j][1] = 0.0;
  printf("data array initialized\n");

  i = 0.0;
  oldIrq3 = getvect(IRQ3);  /* Set up the ISR we call eocTrue */
  setvect(IRQ3, eocTrue);
  outportb(0x21, ( inportb(0x21) & 0xF7 ) );

  do {
    readData = inportb(BASEADDRESS);  /* Do first read of Channel 0 */
    while(EOC == FALSE) { /* do nothing until EOC true */ };
    readData = inportb(BASEADDRESS);  /* Read Channel 0 again */
    EOC = FALSE; /* Reset to FALSE */
    data[i][0] = i;
    data[i][1] = (float)(readData * 5.0/255.0); /* Volts calculation */
  } while(i < MAXSIZE);

  printf("Writing to file...\n");
  for(j=0; j < MAXSIZE; j++) {
    fprintf(fp, "%f\t%f\n", data[j][0], data[j][1]);

  setvect(IRQ3, oldIrq3);
  outportb(0x21, (inportb(0x21) | 0x08) );

  return 0;

} /* end of main */

/* ISR executes every time IRQ3 is triggered HIGH */
void interrupt eocTrue(void)
  #pragma asm pushf;
  #pragma asm cli;
  outportb(0x20, 0x20);
  #pragma asm popf;
} /* end of eofTrue */

ADC.C Fuller Code Description

The ISR eocTrue() simply sets EOC true when IRQ3 is triggered. A primer on ISR programming can be referenced if you are unfamiliar with ISR's. #pragma is in-line assembler and the associated calls similar roles to Turbo C's enable() and disable() functions.

main() begins with some initializations, namely opening an ASCII file, initializing the data array to zero and setting the ISR. The do-while loop acquires 5000 samples from channel 0:

  do {
    readData = inportb(BASEADDRESS);
    while(EOC == FALSE) { /* do nothing */ };
    readData = inportb(BASEADDRESS);
    EOC = FALSE;
    data[i][0] = i;
    data[i][1] = (float)(readData * 5.0/255.0);
  } while(i < MAXSIZE);

Reading a channel requires two inportb() calls. The first notifies the MAX158 which channel to read. Once EOC is true, data can be safely read and hence the second call. The 8-bit data, readData is then calculated as an analog voltage (0 to +5V) and written to the array. Once 5000 samples are acquired, the array is written to file and the program exits. Acquiring more or different channels is achieved by appropriate inportb() address reads as per Table 5:

0AIN1 PIN 6608
1AIN2 PIN 5609
2AIN3 PIN 4610
3AIN4 PIN 3611
4AIN5 PIN 2612
5AIN6 PIN 1613
6AIN7 PIN 28614
7AIN8 PIN 27615

The above photo shows a function generator (set to a 5 kHz sine wave) wired to the TEB (channel 0) and ADC.C was executed. data.txt was read and plotted using Excel to produce the following figure (only first 20 samples were plotted). The average sample time (do-while loop time divided by 5000 samples) was measured to be 833 usec or 1.2 kHz.

Your card can generate a voltage ranging from -5 to +5 Volts. The figure (left) below shows the PC installed DAC0832 hooked up to a voltmeter. DAC.C allows the user to input a desired voltage output. The figure closeup (right) of the voltmeter shows the 1.37 Volt readout while running DAC.C

DOS Turbo C code
Note: download DAC.C rather than cutting and pasting from below.

#include < stdio.h >
#include < stdlib.h >
#include < dos.h >
#include < conio.h >  

void main(void)
  int   baseAddress = 608;      /* Row 4 of 8-pin header */
  int   dataWord;               /* Byte representation in decimal */
  float voltageOut;             /* Voltage out in Volts */
  float voltageRef = 5.0;	/* Reference voltage set to +5V */


    cprintf("Enter desired output voltage: e.g. -4.5 to 4.5 or 999.0 to Quit:");
    scanf("%f", &voltageOut);
    if(voltageOut == 999.0) exit(0);
    dataWord = (int)(((voltageOut * 128.0) / voltageRef) + 128);
    cprintf("Data word is %d", dataWord);
    outportb(baseAddress, dataWord);

DAC.C Fuller Code Description

Hooking a voltmeter or oscilloscope on LF353 pin 7 should read the user's desired voltage as seen in the previous figure. DAC.C first prompts the user for the voltage between -5 and +5 Volts. The corresponding decimal value, dataWord is calculated:

    dataWord = (int)(((voltageOut * 128.0) / voltageRef) + 128);

and then written out to the DAC0832 with a outportb(baseAddress, dataWord); statement.

If your card has both the MAX158 and DAC0832 chips you can perform both ADC and DAC using the sample code. ADDA.C reads the analog input voltage on channel 0 thus generating its 8-bit digital equivalent. This digital equivalent is then written to the DAC0832 and produces the analog output voltage. This trivial example is a sanity check; the input and output voltages should be the same. The two figures below illustrate. The first is an oscilloscope capture of a function generator 50 Hz sine wave and the second is the MAX158 ADC and corresponding DAC0832 output. It contrasts the differences in the actual input (top) and digitized output (bottom). The classic staircase form of the output illustrates the results of zero-order-hold.

DOS Turbo C code
Note: download ADDA.C rather than cutting and pasting from below.

#include < stdio.h >
#include < stdlib.h >
#include < dos.h >
#include < conio.h >  

#define IRQ3       0x0b   /* IRQ 3 address */
#define TRUE       1      /* Boolean for ISR */
#define FALSE      0      /* Boolean for ISR */
#define VREF       5      /* Reference voltage [V] for DAC and ADC */

/* globals */
int     EOC;

/* prototypes */
void interrupt (*oldIrq3)(void);     /* IRQ3 hardware interrupt */
void interrupt newIrq3(void);        /* IRQ3 ISR */

void main(void)
  int   baseAddress = 608;
  int   adcData, dacData;         /* Acquired data, output data */
  int   i;                        /* Dummy counter variable */
  int   totalIterations = 1000;   /* 1000 samples */

  /* setup IRQ3 hardware interrupt */
  oldIrq3 = getvect(IRQ3);
  setvect(IRQ3, newIrq3);
  outportb(0x21, (inportb(0x21) & 0xF7));



  i = 0;

     adcData = inportb(baseAddress);
     while(EOC == FALSE) { /* do nothing */ };
     adcData = inportb(baseAddress);
     dacData = 128*adcData/255 + 128;
     outportb(baseAddress, dacData);
  } while (!kbhit());

  /* Return things back to original */
  setvect(IRQ3, oldIrq3);
  outportb(0x21, (inportb(0x21) | 0x08));

} /* end of main */

void interrupt newIrq3(void)
  #pragma asm pushf;    /* preserve interrupt flag */
  #pragma asm cli;
  EOC = TRUE;           /* signal end-of-interrupt (EOI) */
  outportb(0x20, 0x20); /* signal end-of-interrupt (EOI) */
  #pragma asm popf;     /* restore interrupt flag */
} /* end of newIrq3 */

ADDA.C Fuller Code Description

Closer inspection of ADDA.C reveals its combination of ADC.C and DAC.C code. Like ADC.C, an ISR services IRQ3 to safely read the 8-bit data from channel 0. Its voltage equivalent is calculated and then written to the DAC0832 at 608 decimal.

Final Words

This tutorial featured a ISA-bus based ADC and DAC card that you can construct from affordable and widely available off-the-shelf parts. By using the MAX158 and DAC0832 chips you can choose to implement ADC, DAC or both according to your needs and budget using the complete parts lists, schematic and code samples. The tutorial was motivated by frustrations in existing literature that often neglect hardware interrupt implementation. Monitoring the MAX158 /INT pin through an ISR ensures correct data acquisition and moreover, it increases sampling rates above polling techniques without much hardware or software overhead.

You can specify sampling rates in a number of ways. Turbo C provides the delay(numberOfMilliSeconds) function call to specify millisecond delays. However this should be used with caution since the BIOS tick rate are tied to 55 msec timeslices (18 ticks/sec) and hence numberOfMilliSeconds should be an integer greater than 55. Rates faster than this require software BIOS interrupt programming and is beyond the scope of this tutorial. Alternatively, the do-while loop in ADC.C and samples at the fastest possible rate (833 usec/sample on a 40 MHz 386SX), can be embedded with dummy delays tuned for micro or sub-55 msec rates. Of course this approach is ad hoc and CPU dependent. Further software techniques like re-programming the PC's 8254 tick rate is possible but again is beyond the scope of this tutorial.

A better approach for specifying sub-milli or microsecond sampling rates is to introduce a hardware timer into the circuit. This was alluded to in the Motivation and Audience section. The tutorial on building your own 8254-based ISA card provides some details on how this would be achieved. Such an approach would be the next logical improvement of the ADC/DAC card.

Click here to email me