Digital Racing

Wednesday, October 26, 2011

Arduino Nitrox Analyzer phase 2: ADC/I2C

I was able to get reliable samples on the Arduino in the original post.  I did it by amplifying the input voltage source by 43x which put it into a reliable range for the built-in ADC (analog to digital converter) of the Arduino. This is probably good enough.  But things could be better- I could reduce the sampling voltage of the Arduino so that it compares to a smaller value instead of 5v.  The problem with that is that pretty much limits anything else I'd want to sample to the same voltage range.  I just got a bunch of sensors that are up to 5V- so I need those to be read over the 5V range.

John H helped me a lot in figuring this out- as he went through the same exercise I did before he came to this conclusion

The solution is to use an external ADC.   Two big benefits to this:
  1. I can use different voltages for each one
  2. The resolution is much higher.
The second part is where it's really going to matter for me, because I have a long-term plan to use the Arduino to take care of several functions, and being able to read multiple sources- and accurately is the issue.  The Arduino is 10-bit, which comes out to 1024 discrete values.  At 5V, that's 5mv per step, which is far too large for something like a pressure transducer which goes to 10k psi.

Enter the ADS1115.  It's a TI ADC with a ton of features including a comparator.  This is an additional feature that let's me program a threshold where I could set an alarm- such as an overpressure switch.  I should be monitoring the pressure via the Arduino itself, but this could be a hardwired backup to that.  It also has 4 single-ended inputs, or two differential inputs.

I found a guy on Ebay selling this *tiny* (about half the size of a grain of rice) chip mounted on a nice PCB for $15.  It's a well-done chip, but light on documentation.



The hard part about the ADS115 is how to wire it up!  I was able to get this diagram off of the listing on Ebay.  But, then I have to get it talking to the Arduino.

The Arduino uses I2C which is a serial protocol.  There's only one set of pins on the Arduino that you can hook up I2C.  But.. I2C is an addressable protocol, so you can have 127 devices on that set of pins.  I'm hoping the 1115 can serve all my ADC needs, but I could get a second one easily.

This page was a good start- it talks about I2C and helped me get the next set of pins figured out.  SDA and SCL are Serial Data and Serial Clock, respectively and map to pins 4 and 5 on the Arduino.  GND and VCC were easy enough to get as well.



RetroLefty published a very nice test sketch which, by reading through the comments, allowed me to get it talking using differential inputs on Analog 0/1.   And it worked!  Even with the 'worst' resolution, each count is 0.18mV.  Cranking up the resolution let me get accuracy to 0.02mV.  And that's not even as high as it goes!

This solution is sounding a lot better than my amplified one.  But, there may be a reason to use the op-amp yet.  If I have enough inputs, I may need to use the single-ended counters (4 of them) instead of the pair of differential.  And, this circuit lets me turn a differential into a SE if it doesn't work on its own.  My initial attempt and single-ended without the circuit failed, but I'll revisit it.

It took a while, but slogging through the datasheet (btw, datasheets are your friend when dealing with ICs), I found that you can change the address of the ADS1115.  It is normally 0x48, but by connecting one pin to either VCC, SDA or SCL, you can bump to 3 higher addresses (in that order).

(18 November 2011)

Last night was a success.  I was able to run my compressor with the analyzer in the intake with some degree of confidence in the results.  But the road getting here was complicated.

The test sketch I used above was solid- but subsequently a library was created to do I2C communication in a standardized way.  I wanted to go down this route because there are other I2C devices I may want to add and I didn't want to write customer communication code for each of them.  I already knew I might need more than one ADS1115, and unless I wanted all my pins on the Arduino taken up, I probably need an I2C LCD display.  Fortunately, I2CDevLib was created.   It builds on the Wire library for Arduino and simplifies some of the communication protocols by having a bunch of read and writebit functions because that's how you communicate in I2C.  For the ADS1115, for example, you set a bit that you're going to do a read, and you can set the gain, the inputs, etc. 

Except it wouldn't work for me.  Only the simplest examples would work, and I was trying to use the ADS1115 as designed- I wanted several inputs and read between them.  I would set the mux to read from input 0/1, and then I want to read something else (pressure) from 2/3.  I would get bizarre values.  I also tried to set the gain and it wouldn't take effect- and would then read a different input!  It took a full weekend of debugging, but I finally found the problem.  When you need to write to a 16-bit register, you have to write the entire register- you can't simply 'write this bit' because a byte (8 bits) is the smallest addressable space.  So, what you have to do is read the existing 16-bits and update just those bits.  This is done by using a 'bitmask'.  You create a bunch of zeros/ones to 'mask' the values you want to reset.  If you have 16x1s, and then do an 'and' function on the original, you'll end up with the original again because a '1' and a '1' gives a 1, but a 1 & 0 gives a zero.  So what you do is build a mask with zeros in the spaces you want to wipe out.  You then put in your values where they should like up and OR them together so that only new '1's you added get added to the register before you write it back.   Here's the way it is supposed to work (setting a '010').
     //    xxx   args: bitStart=4, length=3
    // 11100011 mask byte
     // 10101111 original value (sample)
    // 10100011 original & mask
// 10101011 masked | value
 What happened instead was that the mask was set too large and was wiping out other values it shouldn't have.  I contacted the author and he made changes to the base code.

What was missing in the base ADS1115 code was functions to read the differential inputs- it only had code to read the single-sided ones.  I went ahead and wrote code for those and submitted them to the open-source project.  And when I found that what I really wanted were voltage values for the different inputs, I wrote code for that was well.

What I ended up with was very simple code to get one input, change the gain, then get the other input.  This way, I could read the oxygen sensor to a very high accuracy, and also get the pressure which had a much wider range of values, meaning I had to turn down the gain on it.
 
I did a successful run last evening reading my oxygen sensor while I pumped Nitrox.  Oddly, though I was reading to a high accuracy, I also got pretty substantial variances in my reading. This had be worried because one of the concerns of building a nitrox stick to blend oxygen and air is to make sure that it is well mixed, otherwise, you could not know what you're really pumping.  So I was thinking that my stick was defective.  While I was pumping, the oxygen ran out- and yet the varied reading still continued.  I think this might be because I'm reading the ADS1115 at a very high rate (800 samples per second), as well as 'single shot' mode.  I should probably switch to continuous mode, and perhaps even do averaging to get consistent numbers if needed.