12-bit analogue-to-digital converter

This is a 12 bit A/D converter based on the MAX186, and the hardware side is very straightforward. For medium resolution applications, it's hard to find fault with this device. A particularly good feature is the SPI interface, which is not difficult to implement with a parallel port.

Serial 12 bit, 8 channel A/D converter

MAX186 serial, 12-bit, 8 channel A/D converter
[PDF schematic: max186-serial-adc.pdf]

Code: MAX186 serial A/D converter

The code should be fairly understandable by inspection. It runs an interval timer in the main loop, which fires every so many seconds (from a value set at compile time). This calls a function which starts a conversion and returns a value. Multiple functions (convert, read_conversion, read_byte) handle different subtasks that make up the whole conversion process.

Various levels of information reporting can be set by changing the booleans named verbose, debug and inform. They allow you to get a glimpse of some of the actions that are happening as the program runs.

Any number of improvements could be made, for instance:

/* Filename: MAX_ADC.CPP Purpose: Controls and displays output from an analogue-to-digital converter circuit, based on the MAX186DCPP i.c. connected to the parallel port. Date: Saturday 22 October 2005 Author: Chris Carter, chris.carter@iee.org Usage: ./io ./max_adc */ #include <iostream> #include <unistd.h> #include <sys/io.h> #include <signal.h> #include <sys/time.h> // Declare various function prototypes // 'read_adc' - calls all other functions necessary to initiate a conversion and read the value back void read_adc(int); // 'convert' - sends a configuration byte to the ADC, at the end of which conversion begins int convert(unsigned int channel, bool uni_bip, bool sgl_dif, bool pd1, bool pd0); // 'read_conversion' - reads two bytes from the ADC, once conversion is complete, calculates coverted value double read_conversion(void); // 'read_byte' - reads a single byte back from the ADC (called by 'read_conversion') unsigned int read_byte(void); using namespace std; // Declare global variables int time_interval_sec; // The time between samples, in seconds int j; // General-purpose loop variable unsigned int port = 0x378; // First parallel port is 0x378 unsigned int data_register = port; // Address of the data register unsigned int status_register = port + 1; // Address of the status register // Sets levels of verbosity, debug info and informativeness bool verbose = 0; bool debug = 0; bool inform = 0; unsigned int result_mask; // Bit command masks - used to set/clear individual bits unsigned int cs_mask = 0x01; // D0 line (Data register) unsigned int sclk_mask = 0x02; // D1 line (Data register) unsigned int din_mask = 0x04; // D2 line (Data register) unsigned int shdn_mask = 0x08; // D3 line (Data register) // Bit status mask - used to check the state of individual bits unsigned int dout_mask = 0x20; // Paper Out will read the DOUT line (Status register) // Start of the main function int main() { // Set an interval timer that calls the conversion routine every so many seconds time_interval_sec = 1; // Calls a conversion from the ADC every second struct itimerval itimer; signal(SIGALRM, read_adc); // 'read_adc' is the function that gets called when SIGALRM happens itimer.it_value.tv_sec = time_interval_sec; itimer.it_value.tv_usec = 0; // If both are 0, nothing ever happens itimer.it_interval.tv_sec = time_interval_sec; itimer.it_interval.tv_usec = 0; setitimer(ITIMER_REAL, &itimer, NULL); // Start the timer! while(1); return(0); } void read_adc(int) { if (verbose) cout << hex << "Parallel port address is: 0x" << port << "." << endl; // Device setup starts here... start by zeroing the data port if (verbose) cout << "Setting data port bits (D0 - D7) to zero..."; outb(0, data_register); if (verbose) cout << "done." << endl; // First, set Shutdown (SHDN/) high (takes MAX186 out of shutdown mode) if (verbose) cout << "Setting SHDN/ (D3) high..."; outb((inb(data_register) | shdn_mask), data_register); if (verbose) cout << "done." << endl; // Call convert() function: // // First parameter: integer, 0 - 7, specifies the channel to read // Second parameter: boolean, 0 - 1, selects unipolar (1) or bipolar (0) measurement mode // Third parameter: boolean, 0 - 1, selects single-ended (1) or differential (0) conversion mode // Fourth parameter: boolean, 0 - 1, value of control bit PD1 (see datasheet) // Fifth parameter: boolean, 0 - 1, value of control bit PD0 (see datasheet) convert(0, 1, 1, 1, 0); // Read back the conversion from the ADC cout << "Conversion value: " << read_conversion() << " V" << endl; } // Function definitions // Function: convert int convert(unsigned int channel, bool uni_bip, bool sgl_dif, bool pd1, bool pd0) { // Force Chip Select (CS/) high until we are ready to talk to the device if (verbose) cout << "Setting CS/ (D0) high..."; outb((inb(data_register) | cs_mask), data_register); if (verbose) cout << "done." << endl; // Error check for invalid configurations if (channel > 7 || channel < 0) { cout << "Channel to convert must be between 0 and 7." << endl; exit(-1); } // Convert the various individual configuration variables into a 'control byte' bool cbyte[8] = {0}; cbyte[7] = 1; // Start bit (always 1) switch (channel) { case 0 : cbyte[6] = 0; // SEL2 cbyte[5] = 0; // SEL1 cbyte[4] = 0; // SEL0 break; case 1 : cbyte[6] = 1; // SEL2 cbyte[5] = 0; // SEL1 cbyte[4] = 0; // SEL0 break; case 2 : cbyte[6] = 0; // SEL2 cbyte[5] = 0; // SEL1 cbyte[4] = 1; // SEL0 break; case 3 : cbyte[6] = 1; // SEL2 cbyte[5] = 0; // SEL1 cbyte[4] = 1; // SEL0 break; case 4 : cbyte[6] = 0; // SEL2 cbyte[5] = 1; // SEL1 cbyte[4] = 0; // SEL0 break; case 5 : cbyte[6] = 1; // SEL2 cbyte[5] = 1; // SEL1 cbyte[4] = 0; // SEL0 break; case 6 : cbyte[6] = 0; // SEL2 cbyte[5] = 1; // SEL1 cbyte[4] = 1; // SEL0 break; case 7 : cbyte[6] = 1; // SEL2 cbyte[5] = 1; // SEL1 cbyte[4] = 1; // SEL0 break; default : cout << "Unable to properly set channel bits." << endl; exit(-1); } cbyte[3] = uni_bip; // UNI/BIP/ cbyte[2] = sgl_dif; // SGL/DIF/ cbyte[1] = pd1; // PD 1 cbyte[0] = pd0; // PD 0 if (inform) { // Print configuration request summary cout << endl; cout << "Convert channel: " << channel << endl; cout << "Conversion type: " << (uni_bip ? "Unipolar" : "Bipolar") << endl; cout << "Input mode: " << (sgl_dif ? "Single-ended" : "Differential") << endl; cout << "Clock & power-down modes: "; if (pd1 && pd0) cout << "External clock"; else if (!pd1 && pd0) cout << "Fast power-down (Iq = 30 uA)"; else if (pd1 && !pd0) cout << "Internal clock mode"; else if (!pd1 && !pd0) cout << "Full power-down (Iq = 2 uA)"; else { cout << "Unknown clock & power-down mode!" << endl; exit(-1); } cout << endl << endl; } // Clock the control byte into the device to set things up if (verbose) cout << "Setting CS/ (D0) low..."; outb((inb(data_register) & ~cs_mask), data_register); if (verbose) cout << "done." << endl; // Clock control byte into DIN (D2) j = 7; while (j != -1) { if (verbose) cout << "Testing cbyte[" << j << "]" << endl; if (cbyte[j]) { outb((inb(data_register) | din_mask), data_register); // Send '1' } else { outb((inb(data_register) & ~din_mask), data_register); // Send '0' } outb((inb(data_register) | sclk_mask), data_register); if (verbose) cout << "SCLK (D1) high" << endl; outb((inb(data_register) & ~sclk_mask), data_register); if (verbose) cout << "SCLK (D1) low" << endl; j--; }; // Conversion starts now if (inform) cout << "Control byte sent OK. Starting conversion..." << endl; return(0); } // Function: read_conversion double read_conversion(void) { unsigned int high_byte = read_byte(); unsigned int low_byte = read_byte(); double conversion = (double)((high_byte * 16) + (low_byte / 16)); conversion = (conversion / 4096) * 4.096; return(conversion); } // Function: read_byte unsigned int read_byte(void) { unsigned int byte = 0; // OK, let's clock out the bits j = 7; result_mask = 0x80; while(j != -1) { outb((inb(data_register) | sclk_mask), data_register); if (verbose) cout << "SCLK (D1) high" << endl; outb((inb(data_register) & ~sclk_mask), data_register); if (verbose) cout << "SCLK (D1) low" << endl; if (verbose) cout << "Reading bit " << j << " of the high byte" << endl; if ((inb(status_register) & dout_mask) >> 2) { byte = byte | result_mask; result_mask = result_mask >> 1; } else { result_mask = result_mask >> 1; }; if (verbose) cout << "Bit " << j << " is " << ((inb(status_register) & dout_mask) >> 2) << endl; j--; } if (verbose) cout << "Byte is " << byte << endl << endl; return(byte); } // End of file.

[Source text file: max_adc.cpp]

The program is run in conjunction with the I/O enabler, in the same way as the earlier examples.

./io ./max_adc

Add-on: LM35 temperature sensor

As an example of doing something truly useful with this, a temperature sensor can be added with some simple electronics. The National Semiconductor LM35 is particularly straightforward to interface to, and it puts out +10 mV per ° Celsius between -55 °C and +150 °C with accuracies of between 0.25 and 0.75 °C, depending on the temperature range over which it is used.

The following schematic gives a circuit that can be connected to one of the inputs of the A/D above, and used to read temperatures between approx. 0 °C and +40 °C. Although the temperature measurement range of the LM35 is much greater than this, I have eliminated the ability to read out temperatures below 0°C by leaving out a resistor at the output of the device (if you want to add this, check the datasheet for details of the value needed.) Bear in mind that you will also need to switch the ADC to read in bipolar mode, so that it will read voltages below zero.

The upper limit of the range is set by the fact that the maximum input voltage to the A/D converter (in unipolar mode) is +4.096 V and I have multiplied the +10 mV/°C output of the LM35 by a factor of 10 (using op-amp U2 in a non-inverting configuration) to give a sensor voltage at the output of +100 mV/°C.

LM35 temperature sensor schematic
[PDF schematic: lm35-temperature-sensor.pdf]

Note that U3, the ICL7660, is used to generate a -5 V rail for the op-amp from the existing single +5 V supply.