Analog to digital converters (ADCs) provide an embedded controller's window into the analog world. A properly chosen ADC can eliminate a board's worth of op amps and adjustment potentiometers, resulting in a clean, robust design.
The design process involves making sure that the analog requirements of the circuit are met, and that the processor can do something useful with the ADC's output data; the glue between the two is the low-level driver code.
Analog to digital converters require special consideration when writing firmware, when compared with purely digital peripherals such as memory, UARTs and I/O expanders. If an ac signal is to be analyzed, the timing of the start of the conversion must be absolutely deterministic and free from software-induced jitter, placing strict requirements on how the controller initiates a conversion.
High-resolution ADCs for dc measurements may have a conversion time that is significantly longer than any other process that the controller has to perform. And lastly, the digital interface may not be a perfect match to the controller's built in peripheral controllers.
This article describes a step-by-step approach to writing a driver for a 24-bit, delta-sigma ADC with an I2 C interface.(1) It illustrates many of the subtleties of driver design for a number of reasons:
• The conversion time is very long compared with most things that happen in a microcontroller.
• It uses an I2 C interface, which is built into many controllers, but each controller has its own specific implementation.
• The I2 C interface itself is used to indicate the status of a conversion, rather than a status bit.
• It has multiple input channels, requiring proper sequencing in firmware.
An ADC has to do two things–take an analog measurement and send the data to a controller. These can occur simultaneously, depending on the ADC architecture. For example, the LTC2366 (a 12-bit ADC) uses the serial data clock to advance the conversion.(2) This makes sense for a high-speed part; the more operations you can do at the same time, the faster you can sample. But the ADC described here uses a delta-sigma architecture to achieve precision DC specifications, and a long conversion time (145 milliseconds) is a consequence.
The delta-sigma ADC uses the I2 C interface for configuration and data transfer. This interface operates at standard speeds of 100 kHz and 400 kHz, which equates to about 112.5 microseconds to 450 microseconds to read a conversion result (send 7-bit Address and read bit; receive 4 bytes of data, with an ACK/NACK bit at the end of each byte).
This means that the ADC is not capable of producing a result nearly as fast as the controller can communicate with it. The good news is that with a little planning, the controller can use the conversion time to do other useful operations, such as number crunching or displaying data.
The ADC also communicates to the controller that a conversion is in progress by not acknowledging its I2 C address. This scheme is also used by some EEPROMS during write cycles to indicate that a write is in progress, which may take tens of milliseconds.
Another task that will save lots of time later in development is to start defining some useful constants. The two bytes that configure the ADC are conveniently split between the A0 and EN2 bits.
You can start by defining the address selection byte for an input to use for testing (a differential measurement between CH0 and CH1 in this case), and the configuration to measure an input in 50/60-Hz rejection mode (rather than the internal temperature sensor), as shown in Table 1 . See Listing 1 for code.
Many microcontrollers have built-in peripherals for I2 C communications. The hardware will usually automate tasks such as generation of start and stop conditions, sending and receiving bytes of data, and alerting if a transfer is not acknowledged.
Often these tasks are initiated by loading a register with data, then asserting a control bit when the data should be sent; an interrupt is generated when the task is completed or an error has occurred.
This bridges the speed difference between the instruction rate (many instructions per microsecond) to the speed at which the I2 C bus operates; 2.5 to 10microseconds per I2 C clock cycle, or 22.5µs to 90µs for a 9-bit data transfer for the 100-kHz and 400-kHz standard clock rates, respectively.
It's tempting to avoid wasting a single instruction cycle and start right into a fully interrupt-driven scheme. But a lot can go wrong, so a safe approach is to begin by initiating a task, then waiting for it to finish before continuing.
When deciding whether to transition to an interrupt-driven I2 C port, one should weigh the tradeoffs between added complexity and efficiency. In a slow processor running the I2 C bus at 400kHz, the overhead associated with entering and exiting the interrupt service routine may make it less efficient than simply polling for the completion of each operation. In addition, the main program may not have anything useful to do at all if it cannot continue without the data being read through the I2 C port.
An interrupt-driven driver does, however, provide some advantages. The time spent waiting for a conversion to be completed, can be used for number crunching or updating a display. The important thing is for the firmware to properly pipeline the data in such a way that data is not used before it's valid. However, this is beyond the scope of this article, so we'll focus on firmware that doesn't rely on interrupts.
Putting it all together
The steps in communicating with I2 C peripherals vary from device to device, and this is where many of the pitfalls lie. The data sheet shows the sequence of events required to confirm that the conversion is finished, write the configuration for the next conversion, read the data, and initiate the next conversion. Use figures such Figure 1 , as your “pseudocode,” and translate directly into C.
In executing this code, you expect to read back four data bytes that constitute the ADC conversion result. Note that all zeros does not constitute a valid output from the delta-sigma ADC, so a zero return code can be used to indicate that a conversion is in progress. A great place to do number crunching is immediately after reading a valid result. This is because reading the last data byte triggers the next conversion, and the ADC will be tied up performing the next conversion for 145 ms.
Before attempting to interpret the bytes that have been read back, it's a good idea to look at the digital SDA and SCL signals with an analog oscilloscope–not with a logic analyzer. While these are “digital” signals, a significant fraction of bugs are analog in nature.
Confirm that both signals are not pulled low all the time, even if you double checked that the processor pins were properly configured. Check to see that you have the proper pull-up resistors installed–5 k or 10 k are common, but the optimum value depends on bus capacitance and other factors.
The upper trace of Figure 2 shows the result of a 50-k pull-up resistor (excessive rise time, fall time is okay) and the bottom trace shows the result of a 100-ohm resistor (fast rise and fall times, but low level is too high; may even damage processor or peripheral pins or both.)
SDA and SCL are two, separate, single-ended signals–not a differential pair. Figure 3 shows what happens if these lines are very long and in close proximity–120 centimeters of CAT-5 twisted pair with 5-k pull-up resistors in this case. The upper trace should be at a constant high level, yet shows excessive coupling to the lower trace on both rising and falling edges. And Figure 4 shows acceptable waveforms that result from short traces and 5-k pull-up resistors.
With the physical interface operating properly, turn your attention to the actual conversion data. A good first step with an ADC is to apply a known input and look for the expected output. Start by setting the ADC to convert on Channel 0-1 differential by sending the previously defined configuration bytes, and reading back the conversion result.
In the case of the delta-sigma ADC used here, connecting the CH1 pin to ground and the CH0 pin to 5 V should result in the positive overrange code of 0xC0000000. Reversing the connections should result in an output code of 0x3FFFFFFF. And grounding both inputs (zero differential input) should produce a code close to 0x80000000.
A great sanity-saving approach at this point is to write a function that translates the ADC code to a human-readable voltage. The data format of this device is offset binary; that is, when the input is zero, the output is 231 . (31 because of the sub-LSBs [least significant bits] that the ADC outputs along with the 24 bits of data and the 1 sign bit.)
Subtracting this offset from the ADC result using signed integer arithmetic results in a 2's complement word that has a range of +/-230 as the input voltage goes from negative full-scale to positive full-scale. So:
Input Voltage = Reference Voltage * (ADC code – 231 ) / 231 (1)
Displaying the ADC output in volts allows you to compare the conversion result to a digital voltmeter out in the “real analog world.” Note that the input range of the device is –Vref / 2 to +Vref / 2, or +/-2.5 V when the reference voltage is 5V.
A common task with multichannel ADCs is to repeatedly scan a predetermined sequence of channels. An easy way to do this is to set up an array of channel addresses and an array to store the received data, offset by 1 (remember–you're reading back data from the last conversion and writing the channel configuration for the next conversion). The circuit in Figure 5 applies a “staircase” of voltages to the various differential channels. This lets you easily verify that you're selecting the right channels and storing the values in the correct location.
Table 2 shows the contents of the Channel Array constant, the hexadecimal values from the result table, and these values converted to voltages using the input voltage equation (Equation1). The code is shown in Listing 2 .
Two things are important to notice about the table and the code. The first is the “dummy conversion” that takes place before the main for-loop. The purpose of this conversion is to load the ADC with the correct setup values for the first valid conversion; the data read back is not stored into Result_table .
Remember, every time the ADC is given a read command, it outputs the results from the last conversion that took place, not the one that is being set up. The second thing to notice is how the Channel_Array data is actually offset by one. This is also a direct result of the ADC read behavior that necessitated the “dummy conversion.”
Know before you code
Communicating with ADCs presents a new set of challenges to an embedded systems programmer. One should take a systematic approach that allows hardware and software to be debugged separately. A thorough understanding of the ADC's operation is necessary to produce an efficient driver that is easy to integrate into the application code.
1. This technique was devised using the LTC2499 from Linear Technology.
2. The device referred to is Linear Technology's LTC2366, a 12-bit, 3-megasample per second successive approximation (SAR) ADC.
Leo Chen is an applications engineer for high resolution mixed signal products at Linear Technology. He has a BS in electrical engineering and computer sciences from the University of California, Berkeley.
Mark Thoren is applications engineering manager for mixed signal products at Linear Technology. He has a BS in agricultural/mechanical engineering and an MS in electrical engineering, both from the University of Maine.