07 Oct 2021 - tsp
Last update 07 Oct 2021
17 mins
The AD7705 is a programmable gain dual channel 16 bit sigma-delta analog digital converter that’s controlled via an 3 wire SPI bus (in addition to a chip select, single reset, input and data ready output that one can but doesn’t have to use). So in worst case one requires 6 pins connecting the microcontroller / host with the ADC. It’s well suited for low speed applications (up to around 200 Hz or 500 Hz depending on the master clock running at 1 MHz or 2.4576 MHz - or double the frequencies using the divide by two logic which is what’s done on most breakout boards).
Since I’m using this ADC again in a physics experiment (to be specific in an electron beam control system) I thought it would be a good idea to write a short summary on how to typically use this part.
The maximum SPI frequency reachable by the AD7705 is 5 MHz - which is just a little bit above the capabilities of an AVR running with 16 MHz and not using SPI2X (the AVR covers stable SPI operation from it’s master clock /2 down to /128 - so at 16 MHz the range supported would be 8 MHz down to 125 kHz, without SPI2X it covers 4 MHz and lower).
The ADC contains 8 different registers (i.e. 3 register selection bits).
Index | RS 2:0 | Size (Bits) | Register | Comments |
---|---|---|---|---|
0 | 000 | 8 | Communication register | Selects the register that is accessed as well as the channel (and controls standby mode) |
1 | 001 | 8 | Setup register | Configured operational mode, gain, buffers, etc. |
2 | 010 | 8 | Clock register | First register written after reset. Configures clock source and frequency |
3 | 011 | 16 | Data register | Obviously used to transfer ADC data |
4 | 100 | 8 | Test register | Should not be used except by the manufacturer |
5 | 101 | reserved | No operation | Reserved |
6 | 110 | 24 | Offset register | Allows one to configure the offset of the ADC |
7 | 111 | 24 | Gain register | Configures gain of the amplifier |
All access is done via a serial interface consisting of a serial clock (SCK) as well as typical master out / slave in (MOSI, DIN) and master in / slave out (MISO, DOUT) lines. During access the ADC has to be selected using a chip select signal. One could of course tie ties permanently to ground to permanently select the chip - but it’s a good idea to not do so since de-asserting and reasserting the chip select also resynchronizes the serial link in case there had been some spurious signals on the clock line. It’s of course also possible to resynchronize only in case communication errors arise but the simpler approach is just to do for every transaction. Access is done through an internal shift register on the device as usual for SPI. If synchronization is lost at any time one can also simply write 32 clock cycles keeping MOSI high (i.e. write a sequence of ones for 4 bytes). This resets the part.
It’s possible to use hardware SPI on the AVR as well as using software bit banging to access the ADC. The latter one can be used in situation in which the hardware SPI implementation has already been used for something different.
The SPI port is capable of pretty fast access - it usually requires a high and low period of SCK for at least $100 ns$ and a maximum of $100 ns$ delay until the data shifted out is guaranteed to be stable after SCK felt to low:
Read timing:
Operation | Timing |
---|---|
CS falling edge to SCK rising edge | $120 ns$ |
SCK falling edge to data valid | $ \leq 80 ns$ at 5V operation, $\leq 100 ns$ at 3.3V operation |
high and low pulse width | $ \geq 100 ns$ |
bus release after rising SCK | $ \leq 100 ns$ |
Write timing:
Operation | Timing |
---|---|
CS falling edge to SCK rising edge | $120 ns$ |
Data valid to SCK rising edge | $ \geq 20 ns $ |
high and low pulse width | $ \geq 100 ns$ |
bus release after rising SCK | $ \leq 100 ns$ |
All access starts with an write access to the communication register.
Bit | Mnemonic | Description |
---|---|---|
7 | DRDY | Writes always have to be 0, on read it provides the status of the data ready pin |
6:4 | RS2:0 | Register selection (values see above) |
3 | R/W | Read / write access selection (0: write, 1: read) |
2 | STBY | Standby. Writing 1 puts the part into standby mode (~ 10 uA consumption), 0: normal operation mode |
1:0 | CH1:0 | Channel selection bits |
The channel selection bits select a channel but also a given operating mode.
CH1:0 | AIN(+) | AIN(-) | Calibration register pair |
---|---|---|---|
00 | AIN1(+) | AIN1(-) | Calibration register pair 0 |
01 | AIN2(+) | AIN2(-) | Calibration register pair 1 |
10 | AIN1(-) | AIN1(-) | Calibration register pair 0 |
11 | AIN2(-) | AIN2(-) | Calibration register pair 1 |
In case CH1 is set the AIN1(-)
or AIN2(-)
pin is internally shorted
to itself. This can be used to evaluate noise performance without external parts
being connected - which is rather interesting in case one wants to determine
noise floor of the ADC before a measurement.
Reset value: 0x01
Bit | Mnemonic | Description |
---|---|---|
7:6 | MD1:0 | Operational mode selection (see below) |
5:3 | GD2:0 | Gain selection bits for the on-chip PGA (see below) |
2 | B/U | Bipolar (0) or unipolar (1) operation |
1 | BUF | Buffer control. If this is set to 1 the internal buffer is enabled (increases input impedance) |
0 | FSYNC | Filter synchronization. If 1 components of the digital filter are held in reset, when set to 0 the analog filter runs |
The AD7705 supports 4 normal operating modes that are selected by MD1:0
:
Mode | MD1:0 | Description |
---|---|---|
0 | 00 | Normal mode. The device performs normal conversions. |
1 | 01 | Self calibration mode. Calibrates the channel that is selected via CH bits in the communication register. For zero scale the inputs are shorted, for full scale an internal reference is used. After calibration has finished MD bits return to 0 again (and DRDY gets asserted) |
2 | 10 | Zero scale calibration. Performs calibration on the externally applied voltage (!). After calibration is finished MD bits return to 0 |
3 | 11 | Full scale calibration. Performs calibration on the externally applied voltage (!). After calibration is finished MD bits return to 0 |
The gain selection is specified as $2^{setting}$:
GD2:0 | Gain |
---|---|
000 | 1 |
001 | 2 |
010 | 4 |
011 | 8 |
100 | 16 |
101 | 32 |
110 | 64 |
111 | 128 |
Reset value: 0x05
Bit | Mnemonic | Description |
---|---|---|
7:5 | Reserved | Write 0 |
4 | CLKDIS | Master clock disable. 1 disables the master clock output (MCLK OUT). In case a ceramic oscillator is used 1 disabled all conversions. |
3 | CLKDIV | Clock division. If this is set to 1 the input clock signal at MCLK IN is divided by two before being used internally |
2 | CLK | Clock selection (Master clock $2.4576 MHz$: set to 1, $1 MHz$ set to 0) |
1:0 | FS1:0 | Filter selection. Selects the output update rate and the -3dB filter cutoff frequency |
CLK | FS1:0 | Output update rate | -3 dB filter cutoff |
---|---|---|---|
0 | 00 | 20 Hz | 5.24 Hz |
0 | 01 | 25 Hz | 6.55 Hz |
0 | 10 | 100 Hz | 26.2 Hz |
0 | 11 | 200 Hz | 52.4 Hz |
1 | 00 | 50 Hz | 13.1 Hz |
1 | 01 | 60 Hz | 15.7 Hz |
1 | 10 | 250 Hz | 65.5 Hz |
1 | 11 | 500 Hz | 131 Hz |
This 16 bit register contains the most recently sampled data value of the ADC.
In case a write is executed the written data is silently dropped. One should only
try to access the data register while DRDY
is active (low).
This register is used internally by the manufacturer to test the device. The features it exposes are undocumented.
The 24 bit zero scale calibration is used in conjunction with the full scale
calibration register. Note that the device is not able to access the register
content while the microprocessor does access it - the data values sampled
during this period will not be correctly scaled. Reading uncalibrated
data can be avoided by setting FSYNC
before accessing these registers.
The 24 bit full scale calibration is used in conjunction with the zero scale
calibration register. Note that the device is not able to access the register
content while the microprocessor does access it - the data values sampled
during this period will not be correctly scaled. Reading uncalibrated
data can be avoided by setting FSYNC
before accessing these registers.
TL;DR:
CPOL
(clock polarity): 1CPHA
(clock phase): 1Using the hardware interface is of course always the desired way of using SPI.
In this case the hardware takes over control of the clock source and shifts
data in and out of the data register. Anytime a transfer has finished the SPI_STC_vect
interrupt is triggered (if interrupt based processing has been enabled) and
one can either transfer the next data element or turn off SPI. One might also perform
polling on the SPIF
bit in the SPI status register to check when operations
have finished which is what I usually do during initialization if this always stays
the same during the whole device operation.
This way one can implement a simple state machine for processing of ADC values after one used a synchronous approach to initialize the ADC.
As one can see from the timing diagram in the AD7705 datasheet the leading edge
is falling, the trailing edge rising - thus one has to configure clock
polarity CPOL = 1
in the SPCR
. Since the data is also setup on
leading and samples on trailing edge the clock phase CPHA = 1
.
The synchronous read and write routines are interesting for initialization during boot up or when one wants to do a synchronous implementation out of other reasons.
First one has to configure the SPI bus of the AVR. In the following code snipped I’m configuring:
I assume that chip select is attached to PB0
- the corresponding code is
moved into static line
routines for better readability.
/*
Asserting and deasserting chip select including the minimum delay
*/
static inline void ad7705CSAssert() {
PORTB = PORTB & 0xFE;
/* Currently hardcoding two NOP's for 16 MHz AVR */
__builtin_avr_nop();
__builtin_avr_nop();
}
static inline void ad7705CSDeassert() {
/* Currently hardcoding two NOP's for 16 MHz AVR */
__builtin_avr_nop();
__builtin_avr_nop();
PORTB = PORTB | 0x01;
}
/*
Initialize SPI for AD7705
*/
void ad7705InitSPI() {
uint8_t sregOld = SREG;
cli();
DDRB = 0x17;
PORTB = 0x13; /* Deassert nSS, assert nRESET, keep clock high */
SPSR = 0x00; /* Clear SPI2X */
uint8_t dummy = SPDR; /* Dummy read */
SPCR = 0x5C;
/* Re-enable interrupts if they've been enabled before */
SREG = sregOld;
}
/*
Transfer functions to read or write 8 and 16 bit
values; one might also need 24 bit transfer in case
one wants to read or write calibration registers
*/
static uint8_t ad7705SPITransfer8Sync(uint8_t bDataOut) {
SPDR = bDataOut;
while(((spStatus = SPSR) & 0x80) == 0) {
__builtin_avr_nop();
}
return SPDR;
}
static uint16_t ad7705SPITransfer16Sync(uint16_t bDataOut) {
uint8_t high;
uint8_t low;
high = (uint8_t)((bDataOut >> 8) & 0xFF);
low = (uint8_t)((bDataOut ) & 0xFF);
high = ad7705SPITransfer8Sync(high);
low = ad7705SPITransfer8Sync(low);
return ((((uint16_t)high) << 8) & 0xFF00) | (((uint16_t)low) & 0x00FF);
}
static uint32_t ad7705SPITransfer24Sync(uint32_t bDataOut) {
uint8_t b1;
uint8_t b2;
uint8_t b3;
b1 = (uint8_t)((bDataOut >> 16) & 0xFF);
b2 = (uint8_t)((bDataOut >> 8) & 0xFF);
b3 = (uint8_t)((bDataOut ) & 0xFF);
b1 = ad7705SPITransfer8Sync(b1);
b2 = ad7705SPITransfer8Sync(b2);
b3 = ad7705SPITransfer8Sync(b3);
return ((((uint32_t)b1) << 16) & 0x00FF0000)
| ((((uint32_t)b2) << 8) & 0x00FF0000)
| ((((uint32_t)b3) ) & 0x00FF0000);
}
A missing part is of course asserting and deasserting the chip select (SS
)
line of the chip
Using these simple I/O sequences one can simply initialize the ADC to the desired operation,
During initialization the device will:
#define AD7705_UPDATE_HZ20 0x00
#define AD7705_UPDATE_HZ25 0x01
#define AD7705_UPDATE_HZ100 0x02
#define AD7705_UPDATE_HZ200 0x03
#define AD7705_UPDATE_HZ50 0x04
#define AD7705_UPDATE_HZ60 0x05
#define AD7705_UPDATE_HZ250 0x06
#define AD7705_UPDATE_HZ500 0x07
#define AD7705_MASTERCLK_MHZ1 0x00
#define AD7705_MASTERCLK_MHZ2 0x80
#define AD7705_MASTERCLK_MHZ24576 0x40
#define AD7705_MASTERCLK_MHZ49152 0xC0
#define AD7705_CH1 0x00
#define AD7705_CH2 0x01
/*
Note: clock setup requires CS to be asserted ...
* Writes into communication register to prime write into clock register
* Writes into clock register
*/
void ad7705ClockSetupSync(
uint8_t adMasterClock,
uint8_t adUpdateFrq,
uint8_t channel
) {
ad7705CSAssert();
ad7705SPITransfer8Sync(0x20 | channel);
ad7705SPITransfer8Sync(adMasterClock | adUpdateFrq);
ad7705CSDeassert();
}
#define AD7705_SETUP_BUFFER_ENABLE 0x02
#define AD7705_SETUP_BUFFER_DISABLE 0x00
#define AD7705_SETUP_UNIPOLAR 0x04
#define AD7705_SETUP_BIPOLAR 0x00
#define AD7705_SYNCHRONIZE 0x01
void ad7705SetupAndCalibrateSync(
uint8_t gain,
uint8_t flags,
uint8_t channel
) {
ad7705CSAssert();
ad7705SPITransfer8Sync(0x10 | channel);
ad7705SPITransfer8Sync(flags | ((gain & 0x07) << 3) | 0x40);
ad7705CSDeassert();
}
void ad7705SetupNormalSync(
uint8_t gain,
uint8_t flags,
uint8_t channel
) {
ad7705CSAssert();
ad7705SPITransfer8Sync(0x10 | channel);
ad7705SPITransfer8Sync(flags | ((gain & 0x07) << 3));
ad7705CSDeassert();
}
To read data one just has to wait till DRDY
is asserted. Reading data
without a valid DRDY
signal is undefined behavior. I’m assuming in this
sample that DRDY
is attached to PB5
. After one has made sure the
device has asserted DRDY
one writes into the communication register to
prime for the read of the data register (note to select the same channel
and not writing FSYNC or anything similar)
uint16_t ad7705GetADCSync() {
uint16_t result;
while((PORTB & 0x20) != 0) {
__builtin_avr_nop();
}
ad7705CSAssert();
ad7705SPITransfer8Sync(0x38); /* Or 0x39 if channel 2 should be selected */
result = ad7705SPITransfer16Sync(0xFFFF);
ad7705CSDeassert();
return result;
}
There are multiple ways to reset the device:
RESET
line. This performs a full hardware reset of the ADC. I’m
assuming thevoid ad7705ResetSoftSync() {
ad7705CSAssert();
ad7705SPITransfer8Sync(0xFF);
ad7705SPITransfer8Sync(0xFF);
ad7705SPITransfer8Sync(0xFF);
ad7705SPITransfer8Sync(0xFF);
ad7705CSDeassert();
}
The following example program will just initialize the first ADC channel, initialize the USART at a BAUD rate of 115200 and then dump measurements results whenever they get available as fast as possible. Everything will be done synchronous - including the serial line transmission (this of course limits the capabilities of this code). The code is available as a GitHub GIST
Usually the better way of handling the ADCs data transfer in many cases - when the
microcontroller has to perform other tasks too - is interrupt / even driven. Since
the ADC signals new available data via it’s DRDY
data ready line it’s simple
as registering an pin change interrupt. Another way is to synchronously check
for the pin change interrupt and then perform the SPI transaction asynchronously.
To be completed: This article will be extended hopefully in near future.
This article is tagged: Programming, AVR, ANSI C, Tutorial, DAC, Electronics, Basics, DIY, Measurements, Microcontroller
Dipl.-Ing. Thomas Spielauer, Wien (webcomplains389t48957@tspi.at)
This webpage is also available via TOR at http://rh6v563nt2dnxd5h2vhhqkudmyvjaevgiv77c62xflas52d5omtkxuid.onion/