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.
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:
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:
| PART DESCRIPTION | VENDOR | PART | PRICE (1999) | QTY | |
| PC BUS PROTOTYPING CARD | JAMECO | 21531 | 17.95 | 1 | |
| MAXIM MAX158 ADC CHIP | DIGIKEY | MAX158BCPI-ND | 22.63 | 1 | |
| 28 PIN WIREWRAP SOCKET | JAMECO | 40336 | 1.35 | 1 | |
| 74HC138 3-8 DECODER | DIGIKEY | MM74HC138N-ND | 0.66 | 1 | |
| 16-PIN WIREWRAP SOCKET | JAMECO | 37436 | 0.89 | 1 | |
| 74HC125 3-STATE QUAD DRIVER | DIGIKEY | MM74HC125N-ND | 0.66 | 1 | |
| 14-PIN WIREWRAP SOCKET | JAMECO | 37217 | 0.75 | 1 | |
| 8 PIN DOUBLE ROW 0.1" HEADER | JAMECO | 109516 | 0.15 | 1 | |
| JUMPER SHORTING BLOCK | JAMECO | 22023 | 0.19 | 1 | |
| 0.1 UF CAP | JAMECO | 151116 | 0.10 | 1 | |
| 47 UF CAP | JAMECO | 11105 | 0.15 | 1 | |
| PART DESCRIPTION | VENDOR | PART | PRICE (1999) | QTY | |
| NATIONAL DAC0832 DAC CHIP | DIGIKEY | DAC0832LCN-ND | 4.17 | 1 | |
| 20-PIN WIREWRAP SOCKET | JAMECO | 169148 | 1.09 | 1 | |
| LF353 OP-AMP | DIGIKEY | LF353-ND | 0.53 | 1 | |
| 8-PIN WIREWRAP SOCKET | JAMECO | 112660 | 0.65 | 1 | |
| 10 KOHM RESISTORS | JAMECO | 29911 | 0.99/100 | 5 | |
| PART DESCRIPTION | VENDOR | PART | PRICE (1999) | QTY | |
| SMALL (2 SQ.IN) BREADBOARD | JAMECO | 105099 | 2.75 | 1 | |
| 40-PIN WIREWRAP SOCKET | JAMECO | 94502 | 2.25 | 1 | |
| Alternatively: | |||||
| 2-PIN TERMINAL CONNECTOR 0.2 INCH | JAMECO | 152346 | 0.49 | 8 | |
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.
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:
| ROW POSITION | HEX VALUE | DECIMAL VALUE |
| 1 | 200 | 512 |
| 2 | 220 | 544 |
| 3 | 240 | 576 |
| 4 | 260 | 608 |
| 5 | 280 | 640 |
| 6 | 2A0 | 672 |
| 7 | 2C0 | 704 |
| 8 | 2E0 | 736 |
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.
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
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.
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:
#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)
{
clrscr();
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 ) );
EOC = FALSE;
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 */
i++;
} 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]);
};
printf("done\n");
fclose(fp);
setvect(IRQ3, oldIrq3);
outportb(0x21, (inportb(0x21) | 0x08) );
printf("Bye!\n");
return 0;
} /* end of main */
/* ISR executes every time IRQ3 is triggered HIGH */
void interrupt eocTrue(void)
{
#pragma asm pushf;
#pragma asm cli;
EOC = TRUE;
outportb(0x20, 0x20);
#pragma asm popf;
return;
} /* end of eofTrue */
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);
i++;
} 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:
| CHANNEL NUMBER | MAX158 PIN NAME | DECIMAL ADDRESS |
| 0 | AIN1 PIN 6 | 608 |
| 1 | AIN2 PIN 5 | 609 |
| 2 | AIN3 PIN 4 | 610 |
| 3 | AIN4 PIN 3 | 611 |
| 4 | AIN5 PIN 2 | 612 |
| 5 | AIN6 PIN 1 | 613 |
| 6 | AIN7 PIN 28 | 614 |
| 7 | AIN8 PIN 27 | 615 |
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.
#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 */
clrscr();
window(5,5,50,75);
for(;;)
{
gotoxy(2,2);
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);
gotoxy(2,3);
cprintf("Data word is %d", dataWord);
outportb(baseAddress, dataWord);
}
}
dataWord = (int)(((voltageOut * 128.0) / voltageRef) + 128);
and then written out to the DAC0832 with a outportb(baseAddress, dataWord); statement.
#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));
EOC = FALSE;
clrscr();
window(5,5,50,75);
i = 0;
do
{
i++;
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));
printf("Bye!\n");
} /* 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 */
return;
} /* end of newIrq3 */
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