>Working, with library!< Interfacing ADS1213 22-bit ADC

EDIT: The problems were fixed (it was some bad coding, confusion by direct port manipulation and ultimately an OR operation instead of an AND operation with direct port manipulation). A library has been written and is dowloadable here. Place it in the library folder of your Arduino install. It includes a readme with explanation of all functions, a sample sketch for channel selection, one for standard deviation and the datasheet. It can be easily adjusted for other, similar ADC chips. See the post here for more info.
Update 28/3/'12: Made compatible with Arduino 1.0, is still compatible with earlier versions. Nothing really changed except for the name of the Arduino library which was changed from Wiring.h to Arduino.h.

Hi, a few weeks ago I ordered an Arduino and since I've been really getting in to it. I've already used various sensors, used a LCD and written software for a new instrument I'm developing, including various menus and settings and data logging to EEPROM (actually I had already written a large part of it before the Arduino even arrived). The instrument I'm developing is for gliding, I want to measure the temperature of the air to detect thermals. So far we have always used variometers which detect the vertical speed, but the temperature is also a very valuable source of information as the increase in temperature is measurable outside the actual lift-sink boundary of the thermal.

My instrument will combine the variometer and the temperature sensor in a single audible signal, for easy processing by humans. A temperature measuring device has been made in the past but the problem was that it was a stand-alone device with its own audible signal, and it was really hard to concentrate on both the vario and temperature sound.

With the internal 10-bit ADC I'm able to get a .1*C sensitivity. It gives erratic data if the temperature is between 2 points, and I need more accuracy anyway. With more accuracy I could also implement some filters to filter noise and adjust sensitivity. So I ordered a Texas Instruments ADS1213 (was the only decent ADC available on ebay and had to buy it on ebay, because mouser, farnell and such charge insane shipping costs), which is accessible by SPI. It also has a very high sample rate of 1k SPS at an effective resolution of 16 bits and something like 3.5SPS at an amazing 22 bits. The fastest setting would allow me to get an accuracy of 65 times the Arduino's internal ADC, which equals .0015 *C. I spent yesterday reading the datasheet and trying to figure out how to make it work.

I did the following:

  • As the ADS1213 needs an external clock, I hacked together some code to configure Timer2 to output a 1MHz signal (the recommended nominal value).
  • I hacked together some other code to try to bit-bang SPI. In part because setting Timer2 to 1MHz makes pin 11 unusable too, and that's one of the hardware SPI ports. In the datasheet there were some minimum times and as I was worried that the Arduino would switch signals too fast, I built in some delays. Also, the code I took was written for a MCP3208 which changes data on the falling side of the SPI clock signal, and I modified it to change data on the rising side of the SPI clock signal.

Here's my pin connections and setup:
ADC pin
4 non-inverting input channel 1 Thermistor
6 Analog GND
8 Chip Select Pin 8
10 Crystal in Pin 3
12 Digital GND
13 Digital +5V
14 Xin, Serial Clock Pin 13
15 Serial in/out Pin 10 (MOSI)
16 Serial out Pin 12 (MISO)
17 Data Ready (high = ready) Pin 4
18 Mode (0 = slave) GND
19 Analog +5V
20 Refout > Refin
21 Refin < Refout

Here's my commented code, it polls every 1/2 second if data is available (data is available at >300Hz per second but I poll every 1/2 second to avoid too much data on the screen).

*** See post below for code, apparently I exceed the limit of 9500 characters ***

The problem is that I only get bogus data, sometimes it returns 14 or 15 bits of mainly 1's, sometimes it returns the complete long filled primarily with 1's. I don't really get why this is happening, since it shouldn't even be possible. I tried to vary the delay times and even delete them but it made no difference. I tried to read the command register which I wrote to in setup() but it just gave a 0.

Here's the ADS1213 datasheet: http://focus.ti.com/lit/ds/symlink/ads1213.pdf and product page: ADS1213 data sheet, product information and support | TI.com

Problems I could imagine myself:

  • The clock output on pin 3 is not working correctly. Though if I unplug it, I get no data at all.
  • The Refout > Refin should be connected to ground with a capacitor like in the datasheet?
  • The ADS1213 doesn't like the Clock input from Arduino's pin 3, because it is either a different frequency or it really does like real crystals that produce sine waves more. In the datasheet it says that it does like real crystals more because they produce sine waves, but it also says somewhere that Xin can be connected to a microcontroller to be able to vary the power consumption on the fly.
  • I fucked up somewhere in the code or connections but don't see it because I have too little experience with interfacing. (the most viable option...)

I'd really appreciate it if someone would take a look at my code and connections and see where it went wrong. When (or if?) it is working it can maybe also be converted into a library to aid other people in need of a very precise ADC.

Here's my commented code, it polls every 1/2 second if data is available (data is available at >300Hz per second but I poll every 1/2 second to avoid too much data on the screen).

The ADS1213 has a DRDY pin which goes LOW if data is available. That's the pin I poll every 1/2 second. Just now I spotted a mistake: I read it when it went HIGH, but I just corrected that to read if it is LOW. It makes no difference though, the data is still the same.

EDIT: I tried to connect the Xin to just a random port or touch it with my finger, and when I touch it or attach it to a random port the data is the same. When I don't touch it or attach it to a port it usually displays 0. So it seems that the data that is read isn't really data but simply noise. I'm starting to think that I really do need a crystal or the 1MHz output doesn't work at all. I don't have access to an oscilloscope, which would make everything a whole lot easier...

// This code tries to read the ADS1213 chip from Texas Instruments using SPI.

// The ADS1213 needs a clock input so we set pin 3 to oscillate at a 1MHz frequency. We lose pin 11 also though.
#define PWMout 3

#define DRDYPIN 4 // Data ready pin

#define SELPIN 8 //Selection Pin 
#define DATAOUT 10//MOSI 
#define DATAIN  12//MISO 
#define SPICLOCK  13//Clock 
int readvalue;

void setup ()
{
// Original code from: http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1295040231
// Sets timer 2 (port 3 & 11) to a 1MHz square wave
  TCCR2A = 0xB3 ; // fast PWM with programmed TOP val
  TCCR2B = 0x09 ; // divide by 1 prescale
  TCNT2  = 0x00 ;
  OCR2A  = 0x0F ; // TOP = 15, cycles every 16 clocks
  OCR2B  = 0x07 ; // COMP for pin3
  pinMode (PWMout, OUTPUT) ;

/* Or maybe this would work (using the Timer library from http://www.arduino.cc/playground/Code/Timer1)?
  pinMode(10, OUTPUT);
  Timer1.initialize(1);         // initialize timer1, and set a 1 microsecond period
  Timer1.pwm(9, 1); */

// Bitbanging the SPI interface, from: http://www.arduino.cc/playground/Code/MCP3208
 //set pin modes 
  pinMode(SELPIN, OUTPUT); 
  pinMode(DATAOUT, OUTPUT); 
  pinMode(DATAIN, INPUT); 
  pinMode(SPICLOCK, OUTPUT); 
  //disable device to start with 
  digitalWrite(SELPIN,HIGH); 
  digitalWrite(DATAOUT,LOW); 
  digitalWrite(SPICLOCK,LOW); 

  Serial.begin(115200);
  
  // Set the desired mode
  byte commandWriteCMR = B00000100;
  // 0 for write: 00 for 1 byte: 0 because it has to be there: 0100 for command register byte 3
  byte CMRbyte1 = B01100010; // Only bit that has to be written (other settings can be left standard)
  //byte CMR_2 = B00000000 // Defaults
  //byte CMR_3 = B00000000 // Defaults
  //byte CMR_4 = B00010111 // Defaults

// Now for the actual write sequence:
  digitalWrite(SELPIN,LOW); //Select ADC
  delayMicroseconds(11); // t24 in datasheet
  // Write the instruction byte
  for (int i=7; i>=0; i--){
    // Clock high (transmits data on RISING EDGE of clock)
    digitalWrite(SPICLOCK,HIGH);
    digitalWrite(DATAOUT,commandWriteCMR&1<<i);
    delayMicroseconds(5); // t10 in datasheet. NOTE: Is this neccesary?
    digitalWrite(SPICLOCK,LOW);
    delayMicroseconds(5); // t11 in datasheet
  }
  
  delayMicroseconds(12); // t19 in datasheet, min time between sending INSR and writing CMR
  // Write to the command register to set the right settings
    for (int i=7; i>=0; i--){
    // Clock high (transmits data on RISING EDGE of clock)
    digitalWrite(SPICLOCK,HIGH);
    digitalWrite(DATAOUT,CMRbyte1&1<<i);
    delayMicroseconds(5); // t10 in datasheet
    digitalWrite(SPICLOCK,LOW);
    delayMicroseconds(5); // t11 in datasheet
  }

  digitalWrite(SELPIN,HIGH); // Disable device again
  delay(100); // Stabilize everything. Is this neccesary?
  Serial.println("Finished with setup");
}


int read_adc(){
  unsigned long startTime = micros();
  
  long adcvalue = 0;
  byte commandbits = B11000000;
  // 1 for read: 10 for three bytes: 0 don't care: 0000 for DOR byte 2 (and proceed from there)

  digitalWrite(SELPIN,LOW); //Select adc
  
  delayMicroseconds(11); // t24 in datasheet
  
  // Write the instruction byte
  for (int i=7; i>=0; i--){
    // Cycle clock (transmits data on RISING EDGE of clock)
    digitalWrite(SPICLOCK,HIGH);
    digitalWrite(DATAOUT,commandbits&1<<i);
    delayMicroseconds(5); // t10 in datasheet. NOTE: Is this neccesary?
    digitalWrite(SPICLOCK,LOW);
    delayMicroseconds(5); // t11 in datasheet
  }
  
  delayMicroseconds(12); // t19 in datasheet, min time between sending INSR and reading DOR
  
  // Read bits from adc
  for (int i=23; i>=0; i--){ // Assumes data is provided in Most Significant Bit first
    digitalWrite(SPICLOCK,HIGH);
    adcvalue+=digitalRead(DATAIN)<<i;
    delayMicroseconds(5); // t10 in datasheet. NOTE: Is this neccesary?
    digitalWrite(SPICLOCK,LOW);
    // ADC value can also be read here right?
    delayMicroseconds(5); // t11 in datasheet
  }
  digitalWrite(SELPIN, HIGH); //turn off device
  unsigned long TotalTime = micros() - startTime;
  Serial.print("Finished with reading ADC, operation took in micros: "); Serial.println(TotalTime,DEC);
  return adcvalue;
}


void loop() {
 if (digitalRead(DRDYPIN) == LOW) {
   //Serial.println("DRDY line high, now reading");
   readvalue = read_adc();
 Serial.print("ADC value is: "); Serial.println(readvalue,BIN);
 //readvalue = read_adc(2); 
 //Serial.println(readvalue,DEC); 
 Serial.println(" ");
 }
 delay(500);
}

The ADS1213 has a DRDY pin which goes LOW if data is available. That's the pin I poll every 1/2 second. Just now I spotted a mistake: I read it when it went HIGH, but I just corrected that to read if it is LOW. It makes no difference though, the data is still the same.

You may be onto something here.

I've used ADS1211 which I think share the same architecture and DRDY needs to be low not only when reading, but also when writing. You may want to try this first, before getting exotic in terms of fault finding.

It's a big datasheet with lots of options and parameters to relate to, but I'm impressed with performance. The device has a dynamic range (24-bits for the ADS1210/11) which is just incredible. For many designs this can eliminate the need for an analog front end.

I modified the code using interrupts to make sure that I start reading right when DRDY goes LOW, but still only noise. I shuffled some wires and it seems that all that's coming in is pure noise, no data whatsoever. Disconnecting some random wires doesn't do anything except for disconnecting the Xin wire, which leads me to believe that the Xin creates a little bit of noise which leaks through to the interrupt pin and creates the interrupts which then reads just noise.

Ben, that sounds interesting. I think they're very similar, large parts of the datasheet are the same and the serial interface is probably exactly the same. Did you interface it with the Arduino, and if so, do you still have the code? And how did you check that DRDY was LOW when writing? Did you use a crystal for the clock?

Murdock,
Got me stumped.
I have some of these ordered as samples from TI.
I'm going to use with a crystal and the SPI.h library tho.
(note to self - order a 1 MHz crystal somewhere ...)

It's been a while since I wrote the interface, but when reading your post it gave me flashbacks on using the DRDY pin.

I use timer2 as clock source (4MHz in my case) and bit bang SPI. First step is doing a reset in accordance with the datasheet. DRDY should then output a valid signal (you may want to check DRDY with a scope or otherwise to see if the low/high frequency reflects the default configuration). SPI will then be configured for MSB (most significant bit first) and you should be able to get some consistant readings as an initial test (write adr/cmd byte followed by read).

Also note that when in differential mode, the returned value will be signed (manual sign extension to 32-bits is needed however to conform to a long). My implementation is polled and I use direct port I/O to check DRDY (wait for high then low before proceeding). Also for the SPI part I use direct port io as it is "impossible" to get timing right with digitalRead and digitalWrite.

@CrossRoads: I might go that way too, I'm getting pretty desperate.

Today I rewrote the code to use direct port access and corrected some mistakes that were in the code. I also finally realized what 'DRDY needs to be low also when writing' meant so I made a while loop to hold when DRDY is still high. I also made a reset function which should reset the chip.

Unfortunately, it's still not working. I reconnected all the wires, took a look at my code multiple times but I really can't figure out what is wrong. I measured Vref and that outputs a nice and steady 2.45v, just to test that it's at least doing something. I tried it in a lot of different ways, used the code with and without command register writing etc. but it's still not working. I get only 1's as data, and it doesn't even matter if I connect power to the chip or not.

I also tried to write to the command register to set Vbias (because with default settings it's off) and then measured it, but that gave 0v. Obviously the command register never gets written properly.

I hope I haven't fried the chip with all my experiments. Is it easy to fry such a chip by applying wrong signals or voltages to wrong pins?

@Ben: Unfortunately I don't have a scope. Did I do the reset part right in my code? The datasheet is kinda vague, it only specifies SCLK. I don't know if my direct port parts of the code are right either, I use if statements and use two steps to make dataout low and serialclock high. I couldn't figure out a faster way. Also, I use digitalRead() to check DRDY, is this fast enough or should I use your method of checking it with direct port I/O? And do you still have the code you wrote and if so, would you care to donate some of it? The serial part should be exactly the same. Because I'm getting kinda desperate with this chip.

// This code tries to read the ADC1213 chip from Texas Instruments using SPI.

// The ADC1213 needs a clock input so we set pin 3 to oscillate at a 1MHz frequency. We lose pin 11 also though.
#define PWMout 3 // Clock input (Xin)

#define DRDYPIN 6 // Data ready pin

#define SELPIN 8 // Selection Pin (CS)
#define DATAOUT 10// MOSI
#define DATAIN  12// MISO
#define SPICLOCK  13// Serial clock (SCLK)
int readvalue;

void ResetADC() { // Resets the ADS1213, needed after power-up
 // PORTB for ports 8-13 is in order (13 is LSB), 2 MSB are not usable
 // PORTD for ports 0-7 is reverse (7 is MSB)
  // NOTE: Does SELPIN have to be low (selected) while resetting, and if so, what are the minimum delay times? Datasheet is quite vague here, specifying nothing else than SLKC times.
  delay(300); // Delay to stabilize power

  PORTB &= B11011111; //digitalWrite(SELPIN,LOW); //Select ADC
  delayMicroseconds(11); // t24 in datasheet
  
//  NOTE: A reset doesn't need a DRDY LOW right?

  PORTB |= B00000001; // Set port 13 (SCLK) HIGH
  delayMicroseconds(1050); // t3 on datasheet p.30, @1MHz clock
  PORTB &= B11111110; // Set port 13 LOW
  delayMicroseconds(12); // t2
  PORTB |= B00000001; // Port 13 HIGH
  delayMicroseconds(520); // t1
  PORTB &= B11111110; // Port 13 LOW
  delayMicroseconds(12); // t2
  PORTB |= B00000001; // Port 13 HIGH
  delayMicroseconds(1050); // t3
  PORTB &= B11111110; // Port 13 LOW
  delayMicroseconds(12); // t2
  PORTB |= B00000001; // Port 13 HIGH
  delayMicroseconds(2060); // t4
  PORTB &= B11111110; // Port 13 LOW

  delayMicroseconds(11); // Random quirk (necessary?)

  digitalWrite(SELPIN,HIGH); // Deselect device again
  delay(100); // Stabilize everything. Is this necessary?
}

void setup ()
{
// Original code from: http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1295040231
// Sets timer 2 (port 3 & 11) to a 1MHz square wave
  TCCR2A = 0xB3 ; // fast PWM with programmed TOP val
  TCCR2B = 0x09 ; // divide by 1 prescale
  TCNT2  = 0x00 ;
  OCR2A  = 0x0F ; // TOP = 15, cycles every 16 clocks
  OCR2B  = 0x07 ; // COMP for pin3
  pinMode (PWMout, OUTPUT) ;

/* Or maybe this would work (using the Timer library from http://www.arduino.cc/playground/Code/Timer1)?
  pinMode(10, OUTPUT);
  Timer1.initialize(1);         // initialize timer1, and set a 1 microsecond period
  Timer1.pwm(9, 1); */

// Bitbanging the SPI interface, from: http://www.arduino.cc/playground/Code/MCP3208
 //set pin modes 
  pinMode(SELPIN, OUTPUT); 
  pinMode(DATAOUT, OUTPUT); 
  pinMode(DATAIN, INPUT); 
  pinMode(SPICLOCK, OUTPUT); 
  pinMode(DRDYPIN, INPUT);
  //Deselect device to start with 
  digitalWrite(SELPIN,HIGH); 
  digitalWrite(DATAOUT,LOW); 
  digitalWrite(SPICLOCK,LOW); 

  Serial.begin(115200);
  
  ResetADC();
  
  // Set the desired mode
  byte commandWriteCMR = B00000100;
  // 0 for write: 00 for 1 byte: 0 because it has to be there: 0100 for command register byte 3
  byte CMRbyte1 = B01100010; // Only bit that has to be written (other settings can be left standard)

//********************************************************** NOTE: DRDY should be low when writing! Therefore I'll use this bullocks while loop. It's not bullocks though.
  while (digitalRead(DRDYPIN) == HIGH) { // Actually it says DRDYPIN != 0
  Serial.println("while (DRDYPIN)");
  }

// Now for the actual write sequence:

  PORTB |= B11011111; //digitalWrite(SELPIN,LOW); //Select ADC
  delayMicroseconds(11); // t24 in datasheet
  
  //*** Write the instruction byte
  for (int i=7; i>=0; i--){
    if (commandWriteCMR&1<<i) {
      PORTB |= B00001001; // Set SPICLOCK & DATAOUT HIGH
    }
    else {
      PORTB &= B11110111; // Set DATAOUT LOW
      PORTB |= B00000001; // Set SPICLOCK HIGH
    }
    delayMicroseconds(5); // t10 in datasheet. NOTE: Is this necessary?
    PORTB &= B11111110; //digitalWrite(SPICLOCK,LOW); note: Also possible using ^=
    delayMicroseconds(5); // t11 in datasheet
  }
  PORTB &= B11110111; // Set DATAOUT LOW, for when the last bit was a 1 (we don't want it to stay high though it might work?)
  
  delayMicroseconds(12); // t19 in datasheet, min time between sending INSR and writing CMR
  
  //*** Write to the command register to set the right settings  
  for (int i=7; i>=0; i--){
    if (CMRbyte1&1<<i) {
      PORTB |= B00001001; // Set SPICLOCK & DATAOUT HIGH
    }
    else {
      PORTB &= B11110111; // Set DATAOUT LOW
      PORTB |= B00000001; // Set SPICLOCK HIGH
    }
    delayMicroseconds(5); // t10 in datasheet. NOTE: Is this necessary?
    PORTB &= B11111110; //digitalWrite(SPICLOCK,LOW); note: Also possible using ^=
    delayMicroseconds(5); // t11 in datasheet
  }
  PORTB &= B11110111; // Set DATAOUT LOW, for when the last bit was a 1 (we don't want it to stay high though it might work?)

  digitalWrite(SELPIN,HIGH); // Deselect device again
  delay(100); // Stabilize everything. Is this necessary?
  Serial.println("Finished with setup");
}

// Declare adcvalue here so read_adc can be made void which hopefully saves some clock cycles.
  long adcvalue;
//******************************************** ADC reading function
int read_adc(){
  adcvalue = 0;
  //unsigned long startTime = micros();
  byte commandbits = B11000000;
  // 1 for read: 10 for three bytes: 0 gapfiller: 0000 for DOR byte 2 (and proceed from there)
  
  PORTB |= B11011111; //digitalWrite(SELPIN,LOW); //Select ADC
  delayMicroseconds(11); // t24 in datasheet
  
  // Write the instruction byte
  for (int i=7; i>=0; i--){
    if (commandbits&1<<i) {
      PORTB |= B00001001; // Set SPICLOCK & DATAOUT HIGH
    }
    else {
      PORTB &= B11110111; // Set DATAOUT LOW
      PORTB |= B00000001; // Set SPICLOCK HIGH
    }
    delayMicroseconds(5); // t10 in datasheet. NOTE: Is this necessary?
    PORTB &= B11111110; //digitalWrite(SPICLOCK,LOW); note: Also possible using ^=
    delayMicroseconds(5); // t11 in datasheet
  }
  PORTB &= B11110111; // Set DATAOUT LOW, for when the last bit was a 1 (we don't want it to stay high though it might work?)
  
  delayMicroseconds(12); // t19 in datasheet, min time between sending INSR and reading DOR
  
  // Read bits from adc
  for (int i=23; i>=0; i--){ // Assumes data is provided in Most Significant Bit first
    PORTB |= B00000001; // Set SPICLOCK HIGH
    delayMicroseconds(5); // t10 in datasheet. NOTE: Is this necessary?
    adcvalue+=digitalRead(DATAIN)<<i; // Should it be placed here or one line higher/lower?
    PORTB &= B11111110; // Set SPICLOCK LOW
    // ADC value can also be read here right?
    delayMicroseconds(5); // t11 in datasheet
  }
  
  while (digitalRead(DRDYPIN) == LOW) { // Hold when DRYDY is still low, then delay to adhere to t23 in the datasheet
  }
  // Following line probably not even neccesary due to slowness of digitalWrite().
  delayMicroseconds(2); // t23 in the datasheet (min time from rising edge of DRDY to rising edge of CS)
  digitalWrite(SELPIN, HIGH); //turn off device
}

 byte HasBeenHigh = 0;
void loop() {
 if (digitalRead(DRDYPIN) == HIGH) {
   HasBeenHigh = 1;
 }
 if (digitalRead(DRDYPIN)==LOW && HasBeenHigh>0) {
   read_adc();
   Serial.print("ADC value is: "); Serial.println(adcvalue,BIN);
   Serial.println(" ");
   HasBeenHigh = 0;
 }
 // No delay to ensure that we don't arrive at the end of a DRDYPIN LOW!
}

Today I bought a 2MHz crystal (1MHz was way expensive), and 3 capacitors to comply with the datasheet examples. But, however, still no luck. I tried with and without resetting, initializing etc. but it just doesn't work. I get 10000000 as an output (I only tried to read a single byte), sometimes 0, sometimes all 1's. So it's still pretty much noise that's read.

To see if writing the settings was successful, I modified the code to set Vbias on, because default it's off. Then the Vbias pin gets connected to analog pin 2. Every second it returns the number of times that data was ready (the DRDY pin went low), and the internal ADC's read from the Vbias pin. The Vbias stays 0 though, indicating that nothing is ever written to the command register.

I used the SPI library to communicate, but the problem is that the SPI.transfer() function writes a byte, and then reads the byte that's returned. So the command register can never even be written to... Is there another way to solve this than bitbanging SPI? As that obviously didn't work for me.

@CrossRoads: Did you already receive your ADS1213?

@Ben: Do you still have the code?

Here's my code (obviously with the faulty command register write at init_ADS1213()

// Reads ADS1213.

#include <SPI.h>

const byte SCKpin = 13;
/*
pin 13	SCK	SPI clock	
pin 12	MISO	SPI master in, slave out
pin 11	MOSI	SPI master out, slave in
pin 10	SS	SPI slave select
*/

const byte DRDYPIN = 9;
const byte chipSelectPin = 10;
byte ADCvalue = 0;

void init_ADS1213() {
  while (digitalRead(DRDYPIN)) {// don't do anything while DRDYPIN is still high
  Serial.println("DRDY HIGH");
  }
  digitalWrite(chipSelectPin, LOW);
  SPI.transfer(B00000100);  // Set the desired instruction
  // 0 for write: 00 for 1 byte: 0 because it has to be there: 0100 for command register byte 3
  SPI.transfer(B11100010); // Only bit that has to be written (other settings can be left standard)
  digitalWrite(chipSelectPin, HIGH);
}

void ResetADC() { // Resets the ADS1213, needed after power-up?
 // PORTB for ports 8-13 is in order (13 is LSB), 2 MSB are not usable
 // PORTD for ports 0-7 is reverse (7 is MSB)
  // NOTE: Does SELPIN have to be low (selected) while resetting, and if so, what are the minimum delay times? Datasheet is quite vague here, specifying nothing else than SLKC times.

  digitalWrite(chipSelectPin,LOW); //Select ADC
  delayMicroseconds(11); // t24 in datasheet
  
//  NOTE: A reset doesn't need a DRDY LOW right?

  digitalWrite(SCKpin,HIGH); // Set port 13 (SCLK) HIGH
  delayMicroseconds(1050); // t3 on datasheet p.30, @1MHz clock
  digitalWrite(SCKpin,LOW); // Set port 13 LOW
  delayMicroseconds(12); // t2
  digitalWrite(SCKpin,HIGH); // Port 13 HIGH
  delayMicroseconds(520); // t1
  digitalWrite(SCKpin,LOW); // Port 13 LOW
  delayMicroseconds(12); // t2
  digitalWrite(SCKpin,HIGH); // Port 13 HIGH
  delayMicroseconds(1050); // t3
  digitalWrite(SCKpin,LOW); // Port 13 LOW
  delayMicroseconds(12); // t2
  digitalWrite(SCKpin,HIGH); // Port 13 HIGH
  delayMicroseconds(2060); // t4
  digitalWrite(SCKpin,LOW); // Port 13 LOW

  delayMicroseconds(11); // Random quirk (necessary?)

  digitalWrite(chipSelectPin,HIGH); // Deselect device again
  delay(50); // Stabilize everything. Is this necessary?
}

void setup() {
  Serial.begin(115200);
  pinMode (chipSelectPin, OUTPUT);  // set the chipSelectPin as an output
  pinMode (DRDYPIN, INPUT);
  SPI.begin();  // initialize SPI:
  SPI.setDataMode(1); // CPOL 0, CPHA 1
  SPI.setClockDivider(128);
  ResetADC();
  init_ADS1213();
}

unsigned long LowCount = 0;
unsigned long StartTime = 0;
void loop() {
  static byte HasBeenHigh = 0;
  if (digitalRead(DRDYPIN) == HIGH) {
    HasBeenHigh = 1;
  }
  if (digitalRead(DRDYPIN)==LOW && HasBeenHigh>0) {
    //ReadADC();
    //Serial.print("ADC value is: "); Serial.println(ADCvalue,BIN);
    HasBeenHigh = 0;
    LowCount = LowCount + 1;
    if (millis() - 1000 > StartTime) {
      Serial.println(LowCount, DEC);
      Serial.println(analogRead(2));
      StartTime = millis();
      Serial.println(" ");
      LowCount = 0;
    }
  }
}

void ReadADC() {
  digitalWrite(chipSelectPin, LOW);
  ADCvalue = SPI.transfer(B10000010);
  // 1 for read: 00 for one byte: 0 gapfiller: 0010 for DOR byte 0 (and proceed from there)
  digitalWrite(chipSelectPin, HIGH);
}

Yes I did, haven't got a crystal yet tho.

Today I glanced over my code again, and I discovered a major fault: I wrote the PORTB bit the wrong way round! So now port 7 was being set instead of 13... But I fixed it, as well as adjusting the timing for the reset for a 2 MHz crystal, as the reset function could not have worked before because the reset function has maximum timings which were not met.

I looked over my code a lot of times, tested a lot of times but it just doesn't work. The main loop simply reads the Vbias, the easiest way to check if the write to the CMR has been successful.

So I really don't know what to do any more because I tried everything but it still doesn't seem to work.

@CrossRoads: Have you done any testing with the ADS1213 yet, and if not, would you like to check and try my code? (it might work in the very unlikely event that both my ADS1213 chips are smoked)

Here's the latest code:

// This code tries to read the ADC1213 chip from Texas Instruments using SPI.

// The ADC1213 is assumed to have a 2MHz external crystal connected here.

#define DRDYPIN 6 // Data ready pin

#define SELPIN 8 // Selection Pin (CS)
#define DATAOUT 10// MOSI
#define DATAIN  12// MISO
#define SPICLOCK  13// Serial clock (SCLK)

void ResetADC() { //********************************************************* Resets the ADS1213, needed after power-up
  // NOTE: Does SELPIN have to be low (selected) while resetting, and if so, what are the minimum delay times? Datasheet is quite vague here, specifying nothing else than SLKC times.
  delay(300); // Delay to stabilize power

  PORTB &= B11111110; //digitalWrite(SELPIN,LOW); //Select ADC
  delayMicroseconds(6); // t24 in datasheet

//  NOTE: A reset doesn't need a DRDY LOW right?

  PORTB |= B00100000; // Set port 13 (SCLK) HIGH
  delayMicroseconds(550); // t3 on datasheet p.30, @2MHz clock
  PORTB &= B11011111; // Set port 13 LOW
  delayMicroseconds(6); // t2
  PORTB |= B00100000; // Port 13 HIGH
  delayMicroseconds(300); // t1
  PORTB &= B11011111; // Port 13 LOW
  delayMicroseconds(6); // t2
  PORTB |= B00100000; // Port 13 HIGH
  delayMicroseconds(550); // t3
  PORTB &= B11011111; // Port 13 LOW
  delayMicroseconds(6); // t2
  PORTB |= B00100000; // Port 13 HIGH
  delayMicroseconds(1030); // t4
  PORTB &= B11011111; // Port 13 LOW

  delayMicroseconds(5); // Random quirk (necessary?)
  
  PORTB |= B00000001;// Deselect device again (= //digitalWrite(SELPIN,HIGH);)
  delay(100); // Stabilize everything. Is this necessary?
}



void setup ()//********************************************************* Setup
{
 // Bitbanging the SPI interface, from: http://www.arduino.cc/playground/Code/MCP3208
 //set pin modes 
  pinMode(SELPIN, OUTPUT); 
    digitalWrite(SELPIN,HIGH);   //Deselect device to start with 
  pinMode(DATAOUT, OUTPUT); 
    digitalWrite(DATAOUT,LOW); 
  pinMode(DATAIN, INPUT); 
  pinMode(SPICLOCK, OUTPUT); 
    digitalWrite(SPICLOCK,LOW); 
  pinMode(DRDYPIN, INPUT);

  Serial.begin(115200);
  ResetADC();
  
//*** NOTE: DRDY should be low when writing! Therefore I'll first check if it's low and if it has been high (to avoid arriving at the end of a LOW).
  byte GoOn = 0;
  byte HasBeenHigh = 0;
  while (GoOn == 0) {
    if (PIND & B01000000) HasBeenHigh = 1;
    else if (HasBeenHigh) GoOn = 1;
    //Serial.println('W');
  }
  
  // Set the desired mode
  byte commandWriteCMR = B00000100;
  // 0 for write: 00 for 1 byte: 0 because it has to be there: 0100 for command register byte 3
  byte CMRbyte1 = B11100010; // Sets Vbias ON
  
  //delayMicroseconds(2); // t21 in datasheet (probably not neccesary...)
  
//*** Now for the actual write sequence:
  PORTB |= B11111110; //digitalWrite(SELPIN,LOW); //Select ADC
  delayMicroseconds(6); // t24 in datasheet
  
  //*** Write the instruction byte
  for (int i=7; i>=0; i--){
    if (commandWriteCMR&1<<i) {
      PORTB |= B00100100; // Set SPICLOCK & DATAOUT HIGH
    }
    else {
      PORTB &= B11111011; // Set DATAOUT LOW
      PORTB |= B00100000; // Set SPICLOCK HIGH
    }
    delayMicroseconds(3); // t10 in datasheet. NOTE: Is this necessary?
    PORTB &= B11011111; //digitalWrite(SPICLOCK,LOW); note: Also possible using ^=
    delayMicroseconds(3); // t11 in datasheet
  }
  PORTB &= B11111011; // Set DATAOUT LOW, for when the last bit was a 1 (we don't want it to stay high though it might work?)
  
  delayMicroseconds(7); // t19 in datasheet, min time between sending INSR and writing CMR
  
  //*** Write to the command register to set the right settings  
  for (int i=7; i>=0; i--){
    if (CMRbyte1&1<<i) {
      PORTB |= B00100100; // Set SPICLOCK & DATAOUT HIGH
    }
    else {
      PORTB &= B11111011; // Set DATAOUT LOW
      PORTB |= B00100000; // Set SPICLOCK HIGH
    }
    delayMicroseconds(3); // t10 in datasheet. NOTE: Is this necessary?
    PORTB &= B11011111; //digitalWrite(SPICLOCK,LOW); note: Also possible using ^=
    delayMicroseconds(3); // t11 in datasheet
  }
  PORTB &= B11111011; // Set DATAOUT LOW, for when the last bit was a 1 (we don't want it to stay high though it might work?)

  while (digitalRead(DRDYPIN) == LOW) { // Hold when DRYDY is still low, then delay to adhere to t23 in the datasheet
    //Serial.println('D');
  }
  delayMicroseconds(2); // t23 in the datasheet (min time from rising edge of DRDY to rising edge of CS)
  digitalWrite(SELPIN,HIGH); // Deselect device again
  delay(100); // Stabilize everything. Is this necessary?
  Serial.println("Finished with setup");
}


void loop() {//********************************* Main loop
  int VbiasPin = 1;
  Serial.println(analogRead(VbiasPin));
  delay(1000);
}

Maybe get some ideas from this recent thread?

http://arduino.cc/forum/index.php/topic,57873.0.html

Lefty

Lefty, thanks for that link. That seems to be a great ADC, but it is only single channel. There's a couple fundamental differences between that ADS1252 and this ADS1213: he doesn't have to write instructions to the command register as it doesn't have one, and therefore he can use the SPI library to read stuff.

Such an approach would also be possible with the ADS1213 (using continuous read mode), but for my application I really need to be able to switch between channels. Maybe I'll write some code to be able to get data from it while only receiving though, just for testing.

I'll try to take a look at your code, after going through the datasheet.

In the meantime, if you absolutely need multiple channels and are not having much luck with getting the code to work, how about going with this 18-bit ADC (simple, well-documented, tested and tried):
http://forums.adafruit.com/viewtopic.php?f=31&t=12269&p=58824

But then again, programming your new ADC and going through all the perfect-timing business might be one of the hardest and most rewarding things hehe

Giantsfan, I'd really appreciate that.
I've seen that one, but I need at least 18 bits of resolution and I want to measure temperature, an analog pressure sensor (height) and another one for airspeed. So I need at least 3 channels with 18 bits, and then I also need to be able to read them really fast, in the order of at least 100SPS per channel as I want to do some data filtering to get extremely accurate results. The microchip one only has 3.75SPS for 18 bits which is way too slow.

Murdock:
You seemed to be on a good roll to get his up and running, but I understand now it hasn’t progressed much. When having issues it is always a good idea to take a step back, clean up the code and verify basic functionality. In terms of code there really isn’t much required as the main challenge is with understanding the datasheet and getting timing right. Once the basic functions are in place you can explore options further.

Supplying a clock:
When using the ADC with a microcontroller, the obvious option is to supply the clock from a timer. Low level code to output a clock on PD3 is as follows (set OCR2A to 1 for a 4MHz clock or 2 for a 2MHz clock):

  // configure timer2 for 8MHz output on PD3
  TCCR2B = _BV(CS20);  // prescaler = F_CPU/1
  OCR2A = 0;  // clear timer at 0 count, f = F_CPU/2
  TCCR2A = _BV(COM2B0) | _BV(WGM21);  // toggle PD3 on compare match
  DDRD |= _BV(PORTD3);  // start clock output on XIN/PD3

I would create a separate init function with the above code and call this from setup. Next step is reset.

Resetting the ADC:
The ADC reset procedure is well explained in the datasheet, but you need to observe that timing is relative to clock. Basic steps (appropriate for 8MHz) are as follows:

  PORTB |= _BV(PORTB5);
  delayMicroseconds(32+1);  // 256 x Xin
  PORTB &= ~_BV(PORTB5);
  delayMicroseconds(1);
  PORTB |= _BV(PORTB5);
  delayMicroseconds(64+1);  // 512 x Xin
  PORTB &= ~_BV(PORTB5);
  delayMicroseconds(1);
  PORTB |= _BV(PORTB5);
  delayMicroseconds(128+1); // 1024 x Xin
  PORTB &= ~_BV(PORTB5);

Again I would put this in a separate function to be called from within setup.

Then we need two more functions, one for writing to the chip and another for reading. For this we can use bit bang SPI.

Writing to the ADC:
The function is called with the ADC command register address (adr), number of bytes to write (count) and the actual data to send (val). Choice of IO pins will have to match how you wired the ADC to your Arduino. Using direct port io is extremely fast so depending on the clock you supply to the ADC you may have to verify/adjust delays in accordance with the datasheet.

#define WRITE 0
#define READ 1

typedef struct _insr_type {
  uint8_t adr :5;        // high-bit unused
  uint8_t count :2;
  uint8_t rw :1;         // 0=write, 1=read
} insr_type;

typedef union {
    insr_type t;
    byte b;
} insr_union;

byte spi_write(byte adr, byte count, byte val[])
{
  insr_union insr;
 
  insr.t.adr = adr;
  insr.t.count = count - 1;
  insr.t.rw = WRITE;

  while (!(PIND & _BV(PIND2)));  // wait for ready to go high
  while (PIND & _BV(PIND2));     // wait for ready to go low
  
  // clock out instruction byte
  DDRB |= _BV(PORTB3); 
  delayMicroseconds(1); 
  for (byte mask = 0x80; mask; mask >>= 1) {
    if (mask & insr.b) PORTB |= _BV(PORTB3); else PORTB &= ~_BV(PORTB3);
    PORTB |= _BV(PORTB5);
    delayMicroseconds(1);
    PORTB &= ~_BV(PORTB5);
    delayMicroseconds(1);
  }
  
  // clock out bits, MSB first
  for (byte i = 0; i < count; i++) { 
    for (byte mask = 0x80; mask; mask >>= 1) {
      if (mask & val[count - i - 1]) PORTB |= _BV(PORTB3); else PORTB &= ~_BV(PORTB3);
      PORTB |= _BV(PORTB5);
      delayMicroseconds(1);
      PORTB &= ~_BV(PORTB5);
      delayMicroseconds(1);
    }
  }
  DDRB &= ~_BV(PORTB3);
}

Reading from the ADC:
The final piece of the puzzle is getting data back from the ADC. Again we can use bit bang SPI as follows:

void spi_read(byte adr, byte count, byte val[], boolean sync)
{
  insr_union insr;
 
  insr.t.adr = adr;
  insr.t.count = count - 1;
  insr.t.rw = READ;
 
  if (sync) {
    while (!(PIND & _BV(PIND2)));  // wait for ready to go high
    while (PIND & _BV(PIND2));     // wait for ready to go low
  }
  
  // clock out instruction byte
  DDRB |= _BV(PORTB3); 
  delayMicroseconds(1); 
  for (byte mask = 0x80; mask; mask >>= 1) {
    if (mask & insr.b) PORTB |= _BV(PORTB3); else PORTB &= ~_BV(PORTB3);
    PORTB |= _BV(PORTB5);
    delayMicroseconds(1);
    PORTB &= ~_BV(PORTB5);
    delayMicroseconds(1);
  }
  
  DDRB &= ~_BV(PORTB3);
  PORTB &= ~_BV(PORTB3);
  
  // clock in bits, MSB first
  for (byte i = 0; i < count ; i++) {
    val[count-i-1] = 0;
    for (byte j = 0; j < 8; j++) {
      PORTB |= _BV(PORTB5);
      delayMicroseconds(1);
      val[count-i-1] <<= 1;
      if (PINB & _BV(PINB3)) val[count-i-1] |= 1;
      PORTB &= ~_BV(PORTB5);
      delayMicroseconds(1);
    }
  }
}

This function uses the same command byte structure as for the write function. Parameters include the ADC command register starting address (adr), number of bytes to read (count), a reference to an array where data is returned (val) and a boolean sync parameter. The sync parameter can be used to avoid waiting for a DRDY sync if you already account for this outside of the read function (such as in a polling routine).

Next step would be to test the core functions as well as your hardware wiring (and get a much needed confidence boost) it is always a good idea to first read a command register and check if what you get back is in accordance with the manual. Next step is to write to a command register (change some non critical value) and then read back the same command register to verify it is matching the value you wrote. Once this is complete you will have the basic tools required to use the ADC in your project.

I’m using the ADC1211 (4 channel multiplexer) running at 8Mhz. Getting the full 24-bit differential resolution with acceptable absolute accuracy requires a careful board layout with appropriate filters as per datasheet recommendations. The end result was very rewarding however and provides a tremendous dynamic range that would be very hard to match with an op-amp ADC pre stage design.

BenF, a tremendous thanks for your write-up. It makes everything really clear, the code is really well structured. Now it WORKS! Writing settings and reading settings or data goes perfectly now. Thanks again. The accuracy is not much better than 12 bits yet, but that's because of my extremely noisy desktop power supply and the breadboard with long leads doesn't make things better either.

You seem to use a lot of C specific code, I could understand and look up most (PORTB |= _BV(PORTB5); is a really handy way of port manipulation!) but there's a few parts I don't understand yet and couldn't find anywhere:

typedef struct _insr_type {
  uint8_t adr :5;        // high-bit unused
  uint8_t count :2;
  uint8_t rw :1;         // 0=write, 1=read
} insr_type;

typedef union {
    insr_type t;
    byte b;
} insr_union;

I don't get this part. It provides the framework for the insruction bit later on, but how does 'adr :5;' for example write the address bits from bit 5-8 in the top part of the write function?
And what does 'insr_type t;' specify in the 'typedef union' definition?
Defining it this way is purely a personal preference and could also be done with bitshifts right?

Here's the working code, adjusted for an ADS1213 with 2MHz external clock (timings are indicated for easy adjustment and function to supply the clock with an internal timer is included, output for channel 3 and samples per second are displayed).

// ADS1213, 2MHz external clock source: Xin = .5 microseconds

const byte biasPin = 1; // Analog pin where ADS Vbias pin is connected, just for testing

/* Pin info:
Serial clock = PORTB5 = pin 13
Digital In/Out = PORTB3 = pin 11
DRDY = PIND2 = pin 2
*/

long Value;
byte CommandBytes[] = {B01100000,B00100010,B01000000};
// First byte: default except for Data Format (offset binary)
// Second byte: default except for mode (self-calibrate) and channel (3)
// Third byte: default except for turbo mode (4)

void setup() {
  // OPTIONAL: Supply the ADS1213 with a 2MHz clock from PIND3 (pin 3) by uncommenting the next line
  //set_clock();
  Serial.begin(115200);
  DDRB |= _BV(PORTB5); // Set Serial clock as output
  reset_ADC;
  spi_write(B0100,sizeof(CommandBytes),CommandBytes);
}

void loop() {
  // Uncomment the following line and make sure the first bit of the first (MSB) command byte is a 1
  // to test if writing to the command register is possible (the value should read about 700)
  //Serial.println(analogRead(biasPin),DEC);
  static int numberOfSamples = 400; // WARNING: 'Value' might overflow if set higher than 255
  unsigned long ValueAccum = 0;
  unsigned long SPSbegin = micros();
  for (int i=1; i<=numberOfSamples; i++) {
    spi_read(B0000,3,true);
    ValueAccum += Value;
  }
  int SPS = 1000000*numberOfSamples/(micros() - SPSbegin); // Calculates samples per second
  ValueAccum = ValueAccum/numberOfSamples; // Takes the average of the 100 values
  
  Serial.println(ValueAccum,DEC);
  Serial.print("SPS: "); Serial.println(SPS,DEC);
}

// ********************ADC declare data structures********************
#define WRITE 0
#define READ 1

typedef struct _insr_type {
  uint8_t adr :5;        // high-bit unused
  uint8_t count :2;
  uint8_t rw :1;         // 0=write, 1=read
} insr_type;

typedef union {
    insr_type t;
    byte b;
} insr_union;

// ********************ADC write section********************
void spi_write(byte adr, byte count, byte val[])
{
  insr_union insr;
 
  insr.t.adr = adr;
  insr.t.count = count - 1;
  insr.t.rw = WRITE;

  while (!(PIND & _BV(PIND2)));  // wait for ready to go high
  while (PIND & _BV(PIND2));     // wait for ready to go low
  
  // clock out instruction byte
  DDRB |= _BV(PORTB3); 
  delayMicroseconds(6); //t38// 11 x Xin
  for (byte mask = 0x80; mask; mask >>= 1) {
    if (mask & insr.b) PORTB |= _BV(PORTB3); else PORTB &= ~_BV(PORTB3);
    PORTB |= _BV(PORTB5);
    delayMicroseconds(3); //t10// 5 x Xin
    PORTB &= ~_BV(PORTB5);
    delayMicroseconds(3); //t11// 5 x Xin
  }
  
  delayMicroseconds(4); //t19// 13 x Xin (t11 adds up)
  // clock out bits, MSB first
  for (byte i = 0; i < count; i++) { 
    for (byte mask = 0x80; mask; mask >>= 1) {
      if (mask & val[i]) PORTB |= _BV(PORTB3); else PORTB &= ~_BV(PORTB3);
      PORTB |= _BV(PORTB5);
      delayMicroseconds(3); //t10// 5 x Xin
      PORTB &= ~_BV(PORTB5);
      delayMicroseconds(3); //t11// 5 x Xin
    }
  }
  DDRB &= ~_BV(PORTB3);
}

// ********************ADC read section********************
void spi_read(byte adr, byte count, boolean sync)
{
  insr_union insr;
 
  insr.t.adr = adr;
  insr.t.count = count - 1;
  insr.t.rw = READ;
 
  if (sync) {
    while (!(PIND & _BV(PIND2)));  // wait for ready to go high
    while (PIND & _BV(PIND2));     // wait for ready to go low
  }
  
  // clock out instruction byte
  DDRB |= _BV(PORTB3); 
  delayMicroseconds(6); //t38// 11 x Xin 
  for (byte mask = 0x80; mask; mask >>= 1) {
    if (mask & insr.b) PORTB |= _BV(PORTB3); else PORTB &= ~_BV(PORTB3);
    PORTB |= _BV(PORTB5);
    delayMicroseconds(3); //t10// 5 x Xin
    PORTB &= ~_BV(PORTB5);
    delayMicroseconds(3); //t11// 5 x Xin
  }
  
  DDRB &= ~_BV(PORTB3);
  PORTB &= ~_BV(PORTB3);
  
  delayMicroseconds(4); //t19// 13 x Xin (t11 adds up)
  Value = 0;
    for (byte j = 0; j < (8*count); j++) {
      PORTB |= _BV(PORTB5);
      delayMicroseconds(3); //t10// 5 x Xin
      Value <<= 1;
      if (PINB & _BV(PINB3)) Value |= 1;
      PORTB &= ~_BV(PORTB5);
      delayMicroseconds(3); //t11// 5 x Xin
    }
}

// ********************Reset ADC function********************
void reset_ADC() {
  PORTB |= _BV(PORTB5);
  delayMicroseconds(512+1);  //t3// 1024 x Xin
  PORTB &= ~_BV(PORTB5);
  delayMicroseconds(5+1);//t2// 10 x Xin
  PORTB |= _BV(PORTB5);
  delayMicroseconds(256+1);  //t1// 512 x Xin
  PORTB &= ~_BV(PORTB5);
  delayMicroseconds(5+1);//t2//
  PORTB |= _BV(PORTB5);
  delayMicroseconds(512+1); //t3// 1024 x Xin
  PORTB &= ~_BV(PORTB5);
  delayMicroseconds(5+1);//t2//
  PORTB |= _BV(PORTB5);
  delayMicroseconds(1024+1); //t4// 2048 x Xin
  PORTB &= ~_BV(PORTB5);
}

// ********************Set clock function********************
void set_clock() {  // Configures timer2 for 8MHz output on PD3
  TCCR2B = _BV(CS20);  // prescaler = F_CPU/1
  OCR2A = 2;  // clear timer at 2 count, f = F_CPU/8
  TCCR2A = _BV(COM2B0) | _BV(WGM21);  // toggle PD3 on compare match
  DDRD |= _BV(PORTD3);  // start clock output on XIN/PD3
}

So I have been checking my original code to see what the problem was... and guess what: it works fine, the problem was ONE CHARACTER. One tiny, lousy little character.

Because when setting the Chip Select pin low before writing, I had this:

PORTB |= ~B00000001;

Instead of this:

PORTB &= ~B00000001;

The former obviously writes all PORTB pins high EXCEPT for the right one! Correcting it made the code run perfectly and show a beautiful 700 on the serial output showing that the Vbias was set.

So lesson learnt: as BenF already stated, a step back and cleaning up of the code is often in order after having problems. I already took a step back by limiting the code to only write a command register bit and then determining if it had worked by very simply polling the Vbias pin. What I should have done after that, was cleaning up the code as much as possible, rationalizing what should work and what not and then going over the small details of the code once again. Or starting a new sketch and copy-pasting everything in while thoroughly re-writing and cleaning it. Also, I could've just pulled the CS pin low which would also have showed that it worked (that's how I discovered it in the first place).

It's good to know that my original code worked though, and that I was thinking in the right direction and adhered to the datasheet pretty well. I was beginning to think that external peripherals were just a big pain in the behind and SPI was some sort of black magic involving quantum computing and big spell books in the form of datasheets. Luckily it's all simpler than I thought, SPI is no black magic but just bits and bytes and common sense, and external peripherals are a great way to expand an Arduino to become even more functional.

I decided to write a library for the ADS1213. I think it could benefit many people who are looking for a multiple-channel, very accurate external ADC as there are a lot of projects out there that need fast and accurate analog to digital conversions and the ADS1213 is a really powerful ADC with a lot of functions.
It includes some functions to be able to easily set the various settings without having to define the bytes by hand. It also includes a function to be able to change channel really quickly, and a function to read the signed output in either 1, 2 or 3 bytes.

Any bugs found or suggestions for improvement are of course welcome. [EDIT 4 may '11: bugfixed version uploaded, offset binary/two's complement option was written to the wrong bit]

The library is attached and includes a readme file which documents all functions in more detail and an example which reads signed values of all ports sequentially, as well as the datasheet itself. Place it in the library folder of your Arduino installation.

The library can be easily adjusted to be used with other TI ADC chips.

It's possible to use multiple ADS1213 chips by using separate Chip Select pins.

The library allows for different clock speeds and different pins (while still using direct port manipulation of course). The restrictions are:
SCLK pin can be from pin 8 to pin 13
IO pin can be from pin 8 to pin 13
DRDY pin can be from pin 2 to pin 7
CS pin can be any pin and is OPTIONAL (if not used (you pull CS LOW) use 0)

The constructor:
ADS1213(clockspeed in MHz (e.g. 2 or 1.6), Offset Binary true/false, SCLK pin, IO pin, DRDY pin, CS pin (0 if not in use))
Clock speed automatically adjusts the timings. It's conservative and can be cranked up for faster transfer. Reading will be the first thing failing when cranked up too high. Reset is probably even more sensitive to it and should be adjusted by hand in ADS1213.cpp for the real clock frequency when over-cranking.

Writing:
write(address, number of bytes, (name of) array with bytes)

Easy writing to the Command Register:
CMRwrite(channel, mode, gain, Turbo Mode Rate, Decimation Ratio)
See datasheet p.19-21 for explanation about mode, gain, TMR and DR. All are set using decimal numbers, except for mode which is set by bytes (e.g. B001 = self calibration, 0 is normal mode). Gain and TMR can be 1,2,4,8 and 16. Decimation Ratio can be from 19 to 8000 (= the number of samples taken for each conversion result).

Change the channel:
channel(number of channel)
Channel can be from 1 to 4.

Reading if Offset binary = true: ( - full scale = 0, mid range = maximum value / 2, + full scale = maximum value)
read(start address, number of bytes, (optional) synchronize)
Returns an unsigned long. If synchronize is left out, the function automatically waits for DRDY to go high and low again. If 'false', it does not wait (if you've already accounted for it in your own code).

Reading if Offset binary = false (using two's complement, - full scale = - min. value, mid range = 0, + full scale = + max. value):
readSigned(number of bytes)
Returns a signed long. When only reading 1 or 2 bytes instead of 3, the maximum and minimum values will be closer to 0 than when reading 3 bytes because the bytes get placed at the end of the long. When reading 1 byte the resolution is very low (8 bits), when reading 2 bytes the resolution is already very good for most projects (16 bits).

reset(): Resets the chip. Happens automatically at initialization.

Here's a sample sketch which cycles through all channels:

// This sketch demonstrates the use of the ADS1213 library by first initializing
// it with the neccesary parameters, then reading it every half second while cycling through the channels.

#include <ADS1213.h>

ADS1213 ADC_ext(2,false,13,11,2,0);
// clock speed MHz, true=offset binary (false=two's complement (can be negative)), SCLK pin, IO pin, DRDY pin, CS pin (0 if not in use)

void setup() {
  Serial.begin(115200);
  ADC_ext.CMRwrite(3,B001,1,1,255);
  // channel 3, mode 001 (self-calibration), gain 1, TMR 1, DR 255
  Serial.println("Done with setup");
}

void loop() {
  static byte Channel=1;
  Serial.print(Channel,DEC); Serial.print(" ch ");
  Serial.println(ADC_ext.readSigned(3),DEC); // Read and print the signed value
  Channel += 1;
  if (Channel>4) Channel = 1;
  ADC_ext.channel(Channel); // Set the current (incremented) channel by using the channel() function
  delay(500);
}

ADS1213.rar (911 KB)

Very cool. Now if some vendor would design a ADS1213 sheild board we would be good to go. I assume 24 bit resolution requires careful board layout and not have to rely on some noisy protoboard or breadboarded lashup.

Lefty

@Murdock,
This library is a nice contribution; and glad you got your initial problem solved.
I look forward to checking out one of these chips when, one of these days, I get finished with my other projects!

@Lefty:
Now, after trying bunches of clean voltage sources (regulation, precision reference, etc.) I can't help but think that this basic fact (i.e., my current breadboard layout versus refined PCB) might be the culprit in my own 24-bit endeavors resulting in consistent stability of only about 19 bit-precision.
I'm always a little hesitant about soldering a veroboard circuit (just the complaint of "oh man, things aren't rearrangeable so I've to be sure about my planned layout and if there's major mistakes, I've to desolder a bunch of things and re-solder") but this time, it seems absolutely necessary that I try veroboarding this and shortening wires, etc. to check for any improvement.