Tag Archives: sensor

How to make ‘High-Resolution’ Sensor Readings with DIGITAL I/O pins

AN685 figure 8: The RC rise-time response of the circuit allows microcontroller timers to be used to determine the relative resistance of the NTC element.

For more than a year we’ve been oversampling the Arduino’s humble ADC to get >16bit ambient temperature readings from a 20¢ thermistor. That pin-toggling method is simple and delivers solid results, but it requires the main CPU to stay awake long enough to capture multiple readings for the decimation step. (~200 miliseconds @ 250 kHz ADC clock) While that only burns 100 mAs per day with a typical 15 minute sample interval, a read through Thermistors in Single Supply Temperature Sensing Circuits hinted that I could ditch the ADC and read those sensors with a pin-interrupt method that would let me sleep the cpu during the process. An additional benefit is that the sensors would draw no power unless they were actively being read.

Given how easy it is drop a trend-line on your data, I’ve never understood why engineers dislike non-linear sensors so much. If you leave out the linearizing resistor you end up with this simple relationship.

The resolution of time based methods depends on the speed of the clock, and Timer1 can be set with a prescalar = 1; ticking in step with the 8 mHz oscillator on our Pro Mini based loggers. The input capture unit can save Timer1’s counter value as soon as the voltage on pin D8 passes a high/low threshold. This under appreciated feature of the 328p is faster than typical interrupt handling, and people often use it to measure the time between rising edges of two inputs to determine the pulse width/frequency of incoming signals.

You are not limited to one sensor here – you can line them up like ducks on as many driver pins as you have available.  As long as they share that common connection you just read each one sequentially with the other pins in input mode. Since they are all being compared to the same reference, you’ll see better cohort consistency than you would by using multiple series resistors.

Using 328p timers is described in detail at Nick Gammons Timers & Counters page, and I realized that I could tweak the method from ‘Timing an interval using the input capture unit’ (Reply #12) so that it recorded only the initial rise of an RC circuit.  This is essentially the same idea as his capacitance measuring method except that I’m using D8’s external pin change threshold rather than a level set through the comparator. That should be somewhere around 0.6*Vcc, but the actual level doesn’t matter so long as it’s consistent between the two consecutive reference & sensor readings. It’s also worth noting here that the method  doesn’t require an ICU peripheral – any pin that supports a rising/ falling interrupt can be used. (see addendum for details) It’s just that the ICU makes the method more precise which is important with small capacitor values. (Note that AVRs have a Schmitt triggers on  the digital GPIO pins. This is not necessarily true for other digital chips. For pins without a Schmitt trigger, this method may not give consistent results)

Using Nicks code as my guide, here is how I setup the Timer1 with ICU:

float referencePullupResistance=10351.6;   // a 1% metfilm measured with a DVM
volatile boolean triggered;
volatile unsigned long overflowCount;

ISR (TIMER1_OVF_vect) {  // triggers when T1 overflows: every 65536 system clock ticks


ISR (TIMER1_CAPT_vect) {     // transfers Timer1 when D8 reaches the threshold
unsigned int timer1CounterValue = ICR1;       // Input Capture register (datasheet p117)
unsigned long overflowCopy = overflowCount;

if ((TIFR1 & bit (TOV1)) && timer1CounterValue < 256){    // 256 is an arbitrary low value
overflowCopy++;    // if “just missed” an overflow

if (triggered){ return; }  // multiple trigger error catch

finishTime = (overflowCopy << 16) + timer1CounterValue;
triggered = true;
TIMSK1 = 0;  // all 4 interrupts controlled by this register are disabled by setting zero


void prepareForInterrupts() {
noInterrupts ();
triggered = false;                   // reset for the  do{ … }while(!triggered);  loop
overflowCount = 0;               // reset overflow counter
TCCR1A = 0; TCCR1B = 0;     // reset the two (16-bit) Timer 1 registers
TCNT1 = 0;                             // we are not preloading the timer for match/compare
bitSet(TCCR1B,CS10);           // set prescaler to 1x system clock (F_CPU)
bitSet(TCCR1B,ICES1);          // Input Capture Edge Select ICES1: =1 for rising edge
// or use bitClear(TCCR1B,ICES1); to record falling edge

// Clearing Timer/Counter Interrupt Flag Register  bits  by writing 1
bitSet(TIFR1,ICF1);         // Input Capture Flag 1
bitSet(TIFR1,TOV1);       // Timer/Counter Overflow Flag

bitSet(TIMSK1,TOIE1);   // interrupt on Timer 1 overflow
bitSet(TIMSK1,ICIE1);    // Enable input capture unit
interrupts ();

With the interrupt vectors ready, take the first reading with pins D8 & D9 in INPUT mode and D7 HIGH. This charges the capacitor through the reference resistor:

//========== read 10k reference resistor on D7 ===========

power_timer0_disable();    // otherwise Timer0 generates interrupts every 1us

pinMode(7, INPUT);digitalWrite(7,LOW);     // our reference
pinMode(9, INPUT);digitalWrite(9,LOW);     // the thermistor
pinMode(8,OUTPUT);digitalWrite(8,LOW);  // ground & drain the cap through 300Ω
LowPower.powerDown(SLEEP_30MS, ADC_OFF, BOD_ON);  //overkill: 5T is only 0.15ms

pinMode(8,INPUT);digitalWrite(8, LOW);    // Now pin D8 is listening

set_sleep_mode (SLEEP_MODE_IDLE);  // leaves Timer1 running
prepareForInterrupts ();
noInterrupts ();
DDRD |= (1 << DDD7);          // Pin D7 to OUTPUT
PORTD |= (1 << PORTD7);    // Pin D7 HIGH -> charging the cap through 10k ref

interrupts ();
sleep_cpu ();                //sleep until D8 reaches the threshold voltage
noInterrupts ();
}while(!triggered);      //trapped here till TIMER1_CAPT_vect changes value of triggered

sleep_disable();           // redundant here but belt&suspenders right?
interrupts ();
unsigned long elapsedTimeReff=finishTime;   // this is the reference reading

Now repeat the process a second time with D7 & D8 in INPUT mode, and D9 HIGH to charge the capacitor through the NTC thermistor:

//==========read the NTC thermistor on D9 ===========

pinMode(7, INPUT);digitalWrite(7,LOW);     // our reference
pinMode(9, INPUT);digitalWrite(9,LOW);     // the thermistor
pinMode(8,OUTPUT);digitalWrite(8,LOW);  // ground & drain the cap through 300Ω
LowPower.powerDown(SLEEP_30MS, ADC_OFF, BOD_ON);

pinMode(8,INPUT);digitalWrite(8, LOW);    // Now pin D8 is listening

set_sleep_mode (SLEEP_MODE_IDLE);
prepareForInterrupts ();
noInterrupts ();
DDRB |= (1 << DDB1);          // Pin D9 to OUTPUT
PORTB |= (1 << PORTB1);    // set D9 HIGH -> charging through 10k NTC thermistor

interrupts ();
sleep_cpu ();
noInterrupts ();

interrupts ();
unsigned long elapsedTimeSensor=finishTime;   //this is your sensor reading

Now you can determine the resistance of the NTC thermistor via the ratio:

unsigned long resistanceof10kNTC=
(elapsedTimeSensor * (unsigned long) referencePullupResistance) / elapsedTimeReff;

pinMode(9, INPUT);digitalWrite(9,LOW);pinMode(7, INPUT);digitalWrite(7,LOW);
pinMode(8,OUTPUT);digitalWrite(8,LOW);  //discharge the capacitor when you are done

The integrating capacitor does a fantastic job of smoothing the readings and getting the resistance directly eliminates 1/2 of the calculations you’d normally do. To figure out your thermistor constants, you need to know the resistance at three different temperatures. These should be evenly spaced and at least 10 degrees apart with the idea that your calibration covers the range you expect to use the sensor for. I usually put loggers in the refrigerator & freezer to get points with enough separation from normal room temp. – with the thermistor taped to the surface of a si7051.  Then plug those values into the thermistor calculator provided by Stanford Research Systems.

Just for comparison I ran a few head-to-head trials against my older dithering/oversampling method:

°Celcius vs time [1 minute interval]  si7051 reference [±0.01°C resolution] vs. Pin-toggle Oversampled 5k NTC vs. ICUtiming 10k NTC.   I’ve artificially separated these curves for visual comparison, and the 5K was not in direct contact with the si7051 reference, while the 10k NTC was taped to the surface of chip – so some of the 5ks offset is an artifact. The Timer1 ratios delver better resolution than 16-bit (equivalent) oversampling in 1/10 the time.

There are a couple of things to keep in mind with this method:

si7051 reference temp (C) vs 10k NTC temp with with a ceramic 106 capacitor. If your Ulong calculations overflow at low temperatures like the graph above, switch to doing the division before the multiplication or use larger ‘long-long’ variables. Also keep in mind that the Arduino will default to 16bit calculations unless you set/cast everything to unsigned long.  Or you could make you life easy and save the raw elapsedTimeReff & elapsedTimeSensor values to your data file and do the calculations later in Excel.

1) Because my Timer1 numbers were small with a 104 cap I did the multiplication before the division. But keep in mind that this method can easily generate values that over-run your variables during calculationUlong MAX is 4,294,967,295  so the elapsedTimeSensor reading must be below 429,496 or the multiplication overflows with a 10,000 ohm reference. Dividing that by our 8mHz clock gives about 50 milliseconds. The pin interrupt threshold is reached after about one rise-time constant so you can use an RC rise time calculator to figure out your capacitors upper size limit. But keep in mind that’s one RC at the maximum resistance you expect from your NTC – that’s the resistance at the coldest temperature you expect to measure as opposed to its nominal rating.  (But it’s kind of a chicken&egg thing with an unknown thermistor right?  See if you can find a manufacturers table of values on the web, or simply try a 0.1uF and see if it works).  Once you have some constants in hand Ametherm’s Steinhart & Hart page lets you check the actual temperature at which your particular therm will reach a given resistance.  Variable over-runs are easy to spot because the problems appear & then disappear whenever some temperature threshold is crossed. I tried to get around this on a few large-capacitor test runs by casting everything to float variables, but that lead to other calculation errors.

(Note: Arduino supports 64-bit “long long” int64_t & uint64_t integers for large number calculations but be careful –  they will gobble up lots of program memory space. typically adding 1-3k to a given compile size. Also Arduino’s printing function can not handle 64 bit numbers, so you have to slice them into smaller pieces before using any .print functions

2) This method works with any kind of resistive sensor, but if you have one that goes below ~200 ohms (like a photoresistor in full sunlight) then the capacitor charging could draw more power than you can safely supply from the D9 pin.  In those cases add a ~300Ω resistor in series with your sensor to limit the current, and subtract that value from the output of the final calculation. At higher currents you’ll also have voltage drop accross the mosfets controlling the I/O pins (~40Ω on a 3.3v Pro Mini), so make sure the calibration includes the ends of your range.

There are a host of things that might affect the readings because every component has a temperature, aging, and other coefficients, but for the accuracy level I’m after many of those factors are handled by the ratio-metric nature of the method. It’s almost independent of value of the capacitor, so you can get away with a cheap ceramic even though it’s value changes dramatically with temperature and age. (I’d still use at least an X7R or, a plastic film cap for measuring colder temps) Last year I found that the main system clock was variable to the tune of about 0.05% over a 40°C range, but that shouldn’t matter if the reference and the sensor readings are taken close to each other. Ditto for variations in your supply. None of it matters unless it affects the system ‘between’ the two measurements.

The significant digits from your final calculation depend on the RC rise time, so switching to 100k thermistors increases the precision. Processors with higher clock speeds should also boost your resolution.  You can shut down more peripherals with the PRR to use less power during the readings as long as you leave Timer1 running.  I’ve also found that cycling the capacitor through an initial charge/discharge cycle (via the 300Ω on D8) after long sleep intervals improved the consistency of the readings. That capacitor shakedown might be an artifact of the ceramics I was using but reading your reference twice at the start of each run might be a good way of eliminating errors that arise from the pre-existing conditions, though you’d also be heating up that resistor so it’s a bit of a trade-off.

Will this method replace our pin-toggled oversampling?  Perhaps not for something as simple as a thermistor since that method has already proven itself in the real world, and I don’t really have anything better to do with A6 & A7. And oversampling still has the advantage of being simultaneously available on all the analog inputs, while the ICU is a more limited resource.  Given the high resolution that’s potentially available with the Timer1/ICU combination, I might save this method for sensors with less dynamic range.  I already have some ideas there and, of course, lots more testing to do before I figure out if there are other problems related to this new method.  I still haven’t determined what the long-term precision is with our Pro Mini clones, and the WDT experiment taught me to be cautious about counting those chickens.

Addendum:   Using the processors built in pull-up resistors

After a few successful runs, I realized that I could use the internal pull-up resistor on D8 as my reference; bringing it down to only three components. Measuring the resistance of the internal pull-up is simply a matter of enabling it and then measuring the current that flows between that pin and ground. I ran several tests, and the Pro Mini’s the internal pullups were all close to 36,000 ohms, so my reference would become 36k + the 300Ω resistor needed on D8 to safely discharge of the capacitor between cycles. I just have to set the port manipulation differently before the central do-while loop:

PORTB |= (1 << PORTB0);     //  enable pullup on D8 to start charging the capacitor

A comparison of the two approaches:

°Celcius vs time: (Post-calibration, 0.1uF X7R ceramic cap)  si7051 reference [±0.01°C] vs. thermistor ICU ratio w 10k reference resistor charging the capacitor vs. the same thermistor reading calibrated using the rise time through pin D8’s internal pull-up resistor. The reported ‘resistance’ value of the NTC themistor was more than 1k different between the two methods, with the 10k met-film reference providing values closer to the rated spec. However the Steinhart-Hart equation constants from the calibration were also quite different, so the net result was indistinguishable between the two references in the room-temperatures range.

The internal pull-up probably isn’t a real resistor. More likely its a very thin semiconductor channel that simply acts like one, and I found the base-line temperature variance to be about 200 ohms over my 40°C calibration range. And because you are charging the capacitor through the reference and through the thermistor, the heat that generates necessarily changes those values during the process.  However when you run a calibration, those factors get absorbed into the S&H coefficients provided you let the system equilibrate during the run.

As might be expected, all chip operation time affects the resistance of the internal pull-up, so the execution pattern of the code used for your calibration must exactly match your deployment code or the calibration constants will give you an offset error proportional to the variance of the internal pull-up caused by the processors run-time. Discharging the capacitor through D8, also generates some internal heating so those (~30-60ms) sleep intervals also have to be consistent.  In data logging applications you can read that reference immediately after a long cool-down period of processor sleep and use the PRR to reduce self-heating while the sample is taken.  Code consistency is always important with time based calibrations no matter how stable your reference is.

Another issue was lag because that pull-up is embedded with the rest of the chip in a lump of epoxy. This was a pretty small, with a maximum effect less than ±0.02°C/minute and I didn’t see that until temperatures fell below -5 Celsius.  Still, for situations where temperature is changing quickly I’d stick with the external reference, and physically attach it to the thermistor so they stay in sync.

Addendum:   What if your processor doesn’t have an Input Capture Unit?

With a 10k / 0.1uF combination, I was seeing Timer1 counts of about 5600 which is pretty close to one 63.2% R * C time constant for the pair.  That combination limits you to 4 significant figures and takes about 2x 0.7msec per reading on average.  Bumping the integrating capacitor up to 1uF (ceramic 105) multiplies your time by a factor of 10 – for another significant figure and an average of ~15msec per paired set readings.  Alternatively, a 1uF or greater capacitor allows you to record the rise times with micros()  (which counts 8x slower than timer1) and still get acceptable results. (even with the extra interrupts that leaving Timer0 running causes…) So the rise-time method could be implemented on processors that lack an input capture unit – provided that they have Schmitt triggers on the digital inputs like the AVR which registers a cmos high transition at ~0.6 * Vcc.

void d3isr() {
triggered = true;

pinMode(7, INPUT);digitalWrite(7,LOW);     // reference resistor
pinMode(9, INPUT);digitalWrite(9,LOW);     // the thermistor
pinMode(8,OUTPUT);digitalWrite(8,LOW);  // ground & drain the cap through 300Ω
LowPower.powerDown(SLEEP_30MS, ADC_OFF, BOD_ON);  // 5T is now 1.5ms w 10k

pinMode(8,INPUT);digitalWrite(8, LOW);    // Now pin D8 is listening

triggered = false;
set_sleep_mode (SLEEP_MODE_IDLE);  // leave micros timer0 running for micros()
unsigned long startTime = micros();
noInterrupts ();
attachInterrupt(1, d3isr, RISING); // using pin D3 here instead of D8
DDRD |= (1 << DDD7);          // Pin D7 to OUTPUT
PORTD |= (1 << PORTD7);    // Pin D7 HIGH -> charging the cap through 10k ref

interrupts ();
sleep_cpu ();
noInterrupts ();

interrupts ();
unsigned long D7riseTime = micros()-startTime;

Then repeat the pattern shown earlier for the thermistor reading & calculation. I’d probably bump it up to a ceramic 106 for the micros method just for some extra wiggle room. The method doesn’t really care what value of capacitor you use, but you have to leave more time for the discharge step as the size of your capacitor increases. Note that I’m switching between relatively slow digital writes (~5 µs each) outside the timing loop, and direct port manipulation (~200 ns each) inside the timed sequences to reduce that source of error.

‘No-Parts’ Temperature Measurement with Arduino Pro Mini

328p processor System Clocks & their Distribution pg26

Most micro-controllers use a quartz crystal oscillator to drive the system clock, and their resonant frequency is reasonably stable with temperature variations. In high accuracy applications like real time clocks even that temperature variation can be compensated, and last year I devised a way to measure temperature by comparing a 1-second pulse from a DS3231 to the uncompensated 8Mhz oscillator on a Pro Mini. This good clock / bad clock method worked to about 0.01°C, but the coding was complicated, and it relied on the ‘quality’ of the cheap RTC modules I was getting from fleaBay – which is never a good idea.

But what if you could read temperature better than 0.01°C using the Pro Mini by itself?

Figure 27-34:  Watchdog Oscillator Frequency vs. Temperature. Pg 346 (Odd that the frequency is listed as 128kHz on pg55?)  Variation is ~100 Hz/°C

The 328P watchdog timer is driven by a separate internal oscillator circuit running at about 110 kHz. This RC oscillator is notoriously bad at keeping time, because that on-chip circuit is affected by external factors like temperature. But in this particular case, that’s exactly what I’m looking for.  The temperature coefficient of crystal resonators is usually quoted at 10–6/°C and for RC oscillation circuits the coefficient is usually somewhere between 10–3/°C to 10–4/°C.  There’s plenty of standard sensors don’t give you a delta that large to play with!

To compare the crystal-driven system clock to the Watchdogs unstable RC oscillator I needed a way to prevent the WDT from re-starting the system.  Fortunately you can pat the dog and/or disable it completely inside its interrupt vector:

volatile boolean WDTalarm=false;
wdt_disable();  // disable watchdog so the system does not restart
WDTalarm=true;    // flag the event

leaves the timers running, and they link back to the system clock.  So you can use micros() to track how long the WDT actually takes for a given interval.  Arduinos Micros() resolution cannot be better than 4 microseconds (not 1 µs as you’d expect) because of the way the timer is configured, but that boosts our detectable delta/° by a factor of four, and the crystal is far more thermally stable than the watch-dog. It’s worth noting that timer0 (upon which micros() depends) generates interrupts all the time during the WDT interval, in fact at the playground they suggest that you have to disable timer0 during IDLE mode sleeps. But for each time interval, the extra loops caused by those non-WDT interrupts create a consistant positive offset, and this does not affect the temperature related delta.

// Set the Watchdog timer                    from: https://www.gammon.com.au/power
byte interval =0b000110;  // 1s=0b000110,  2s=0b000111, 4s=0b100000, 8s=0b10000
//64ms= 0b000010, 128ms = 0b000011, 256ms= 0b000100, 512ms= 0b000101
MCUSR = 0;
WDTCSR |= 0b00011000;    // set WDCE, WDE
WDTCSR = 0b01000000 | interval;    // set WDIE & delay interval
wdt_reset();  // pat the dog
interrupts ();
unsigned long startTime = micros();
while (!WDTalarm)  {    //sleep while waiting for the WDT
set_sleep_mode (SLEEP_MODE_IDLE);
noInterrupts ();  sleep_enable();  interrupts ();  sleep_cpu ();
sleep_disable();  //processor starts here when any interrupt occurs
unsigned long WDTmicrosTime = micros()-startTime;  // this is your measurement!

The while-loop check is required to deal with the system interrupts that result from leaving the micros timer running, otherwise you never make it all the way through the WDT interval. I haven’t yet figured out how many interrupts you’d have to disable to get the method working without that loop.

To calibrate, I use my standard refrigerator->freezer->room sequence for a repeatable range >30°.  Since the mcu has some serious thermal lag, the key is doing everything VERY SLOWLY with the logger inside a home made “calibration box” made from two heavy ceramic pots, with a bag of rice between them to add thermal mass:

1sec WDT micros() (left axis) vs si7051 °C Temp (right axis) : Calibration data selected from areas with the smallest change/time so that the reference and the 328p have equilibrated.

If you use reference data from those quiescent periods, the fit is remarkably good:

si7051 reference temperature vs 1 sec WDT micros() : A fit this good makes me wonder if the capacitor on the xtal oscillator is affected the same way as the capacitor in the watchdogs RC oscillator, with the net result being improved linearity. In this example, there was a constant over-count of 100,000 microseconds / 1-second WDT interval.

I’m still experimenting with this method, but my cheap clone boards are delivering a micros() delta > 400 counts /°C with a one second interval – for a nominal resolution of ~0.0025°C.  Of course that’s just the raw delta.  When you take that beautiful calibration equation and apply it to the raw readings you discover an inter-reading jitter of about 0.1°C – and that lack of precision becomes the ‘effective’ limit of the resolution.  It’s going to take some serious smoothing to get that under control, and I’ll be attacking the problem with my favorite median filters over the next few days. I will also see if I can reduce it at the source by shutting down more peripherals and keeping an eye on stray pin currents.

Noise from si7051 reference (red)  vs Cal. equation applied to raw WDT micros readings (blue). 

Doubling the interval cuts the the noise and the apparent resolution in half, and if you are willing to wait around for the watchdogs 8-second maximum you can add an order of magnitude. Of course you could also go in the other direction: a quarter second WDT interval would deliver ~100 counts/°C, which still gets you a nominal 0.01°C though the jitter gets worse. Note that you can’t use the ‘b’ coefficients from one interval to the next, because of the overhead caused by the non-WDT interrupts. That “awake time” must also be contributing some internal chip heating.

The si7051 reference sensor needs to be held in direct physical contact with the surface of the mcu during the room->fridge->freezer calibration; which takes 24 hours. Since my ref is only ± 0.1ºC accuracy, calibrations based on it are probably only good to about ± 0.2ºC.

There are a few limitations to keep in mind, the biggest being that messing with WDT_vect means that you can’t use the watchdog timer for it’s intended purpose any more. The other big limitation is that you can only do this trick on a voltage regulated system, because RC oscillators are affected by the applied voltage, though in this case both oscillators are exposed to whatever is on the rail, so a bit more calibration effort might let you get away with a battery driven system.

Self-heating during normal operation means that this method will not be accurate unless you take your temperature readings after waking the processor from about 5-10 minutes of power-down sleep. The mass of the circuit board means that the mcu will always have significant thermal lag. So there is no way to make this method work quickly and any non-periodic system interrupts will throw off your micros() reading.

Every board has a different crystal/capacitor/oscillator combination, so you have to re-calibrate for each one. Although the slopes are similar, I’ve also found that the raw readings vary by more than ±10k between different Pro Minis for the same 1sec WDT interval, at the same temperature. The silver lining there is that the boards I’m using probably have the cheapest parts available, so better quality components could boost the accuracy , though I should insert the usual blurb here that resolution and accuracy are not the same thing at all.  I haven’t had enough time yet to assess things like drift, or hysteresis beyond the thermal lag issue, but those are usually less of a problem with quality kit. If your board is using Y5V caps it probably won’t go much below -15°C before capacitor failure disrupts the method.

It’s also worth noting that many sleep libraries, like Rocketscreem’s Lowpower lib, do their own modifications to the watchdog timer, so this method won’t work with them unless you add the flag variable to their modified version of the WDT_vect.  To add this technique to the base code for our 1-hour classroom logger, I’ll will have to get rid of that library dependency.

Where to go from here:

  1. Turning off peripherals with PRR can save power and reduce heating during the interval.
  2. Switching from micros(), to timer based overflows could increase the time resolution to less than 100 ns;  raising nominal thermal resolution.
  3. Clocking the system from the DS3231’s temperature compensated 32khz output could give another 100 counts/°C and improve the thermal accuracy. My gut feeling is the noise would also be reduced, but that depends on where it’s originating.

Despite the limitations, this might be the best “no-extra-parts” method for measuring temperature that’s possible with a Pro Mini, and the method generalizes to every other micro-controller board on the market provided they have an independent internal oscillator for the watchdog timer.


As I run more units through the full calibration, I’m seeing about 1 in 3 where a polynomial fits the data better for the -15 to +25°C range:

si7051 reference temperature vs 1 sec WDT micros() : a different unit, but both clone boards from the same supplier

This is actually what I was expecting in the first place and I suspect all the fits would be 2nd order with a wider range of calibration temperatures. Also, this is the raw micros output – so you could make those coefficients more manageable by subtracting the lowest temperature reading from all those above. This would leave you with a numerical range of about 16000 ticks over 40°C, which takes less memory and is easier for calculations.

And just for fun I ran a trial on an unregulated system powered by 2xAA lithium batteries.  Two things happened: 1) the jitter/noise in the final Temperature readings more than doubled – to about 0.25°C  and 2) calibration was lost whenever the thermal mass of the batteries meant that the supply was actively changing – regardless of whether the mcu & reference had settled or not:

Red is the Si reference [left axis], Green is the calibration fit equation applied to the WDT micros() [left], and blue is the rail voltage supplied by 2xAA lithium batteries [right axis] (Note: low voltage spikes are caused by internal housekeeping events in the Nokia 256mb SD cards)

Addendum 2019-02-26

This morning I did a trial run which switched from micros() to timer1 overflows, using code from Nick Gammon’s Improved sketch using Timer 1. This increased the raw delta to almost 5000/°C, but unfortunately the width of the jitter also increased to about 500 counts. So I’m seeing somewhere near ±0.05°C equivalent of precision error – although my impression is that it’s been reduced somewhat because Timer1 only overflows 122 times per second, while the Timer0/micros had to overflow 100k times. So changing timers means less variability from the while-loop code execution. Next step will be to try driving the timer with the 32khz from the RTC…

Addendum 2019-02-27

So I re-jigged another one of Nicks counting routines which increments timer1 based on input from pin D5, using the WDT interrupt to set the interval. Then I enabled the 32.768 kHz output from a DS3231N and connected it to that pin. This pulse is dead-dog slow compared to the WDT oscillator, so I extended the interval out to 4 seconds.  This long-ish sample time only produced a delta of about 40 counts/°C.

Si7051 reference temp vs Timer1 counts of 32kHz output from DS3231N  (based on data selected from quiescent periods)

There wasn’t enough data to produce high resolution, but my thought was that since the DS3231N has temperature compensated frequency output, it eliminates the xtal as a variable in the question of where the jitter was coming from.  This approach also causes only 2-3 overflows on timer1, so the impact of code execution is further reduced.  Unfortunately, this experiment did not improve the noise situation:

DS3231 32khz clock tics vs 4sec WDT interval Raw reading jitter during a relatively quiescent period.

That’s about 8 counts of jitter in the raw, which produces readings about ±0.1C away from the central line.  That’s actually worse than what I saw with the Xtal vs WDT trials, but the increase might be an artifact of the pokey time-base.  The smoking gun now points squarely at variations in the WDT oscilator output as the source of the noise.

That’s kind of  annoying, suggesting it will take filtering/overhead to deliver better than about 0.1°C from this technique, even though higher resolution is obviously there in the deltas. The real trick will matching the right filter with all the other time lag / constraints in this system. Still, extra data that you can get from a code trick is handy, even if it sometimes it only serves to verify that one of your other sensors hasn’t gone squirrely.

—> just had a thought: oversampling & decimation eats noise like that for breakfast!
Just fired up a run taking 256 x 16ms samples (the shortest WDT interval allowed) with Timer1 back on the xtal clock. Now I just have to wait another 24 hours to see if it works…

Addendum 2019-02-28

OK: Data’s in from oversampling the WDT vs timer1 method.  I sum the the timer1 counts from 256 readings (on a 16msec WDT interval) and then >>4 to decimate. These repeated intervals took about 4 seconds of sampling time.

si7051 reference temperature vs 256x Oversampled Timer 1 reading on 16 msec WDT interval: Fit Equation

This produced 750 counts/°C for a potential resolution of 0.0013°, but as with the previous trials, the method falls down because the jitter is so much larger:

Variability on 256 Timer1 readings of 16msec WDT interval : During quiescent period

100 points of raw precision error brings the method back down to a modest ‘real’ resolution of only ±0.066°C at best.  The fact that this variability is so similar to the previous trials, and that oversampling did not improve it, tells me that the the problem is not noise – but rather the WDT oscillator is wandering around like a drunken sailor because of factors other than just temperature.  If that’s the case, there’s probably nothing I can throw at the problem to make it go away.

Several people pointed out that there is another way to measure temperature with some of the Atmel chips, so I decided to fire that up for a head-to-head trial against the WDT method.  Most people never use it because the default spec is ±10°C and it only generates 1 LSB/°C correlation to temperature for a default resolution of only 1°C.  Some previous efforts with this internal sensor produced output so bad it was used as a random seed generator.

But heck, if I’m going through the effort of calibration anyway, I might be able to level the playing field somewhat by oversampling those readings too:

si7051 reference temperature  °C  vs  4096 reading oversample of the internal diode: Fit equation

Even with 4096 samples from the ADC, this method only delivered ~75 counts /°C. But the internal diode is super linear, and the data is less variable than the WDT:

Variability from 4096 ADC readings of the internal reference diode : During quiescent period

Five counts of raw variability means the precision error is only  ±0.033°C  (again, this becomes our real resolution, regardless of the raw count delta) .  So even after bringing out the big guns to prop up the WDT, the internal reference diode blew the two-oscillator method out of the water on the very first try.

volatile uint16_t adc_irq_count;

ISR (ADC_vect)
adc_irq_count++;   //flag to track how many samples have been taken


adc_irq_count = 0;
unsigned long sum = 0;
unsigned int wADC;
ADMUX = (_BV(REFS1) | _BV(REFS0) | _BV(MUX3)); // Set the 1.1v aref and mux for diode
ADCSRA |= _BV(ADSC);  // 1st conversion to engage settings
delay(10);    // wait for ADC reference cap. to stabilize
noInterrupts ();
set_sleep_mode( SLEEP_MODE_ADC ); // Sleep Mode just to save power here
ADCSRA |= _BV(ADSC);  // Start the ADC
sleep_cpu();      // Sleep (MUST be called immediately after interrupts()
noInterrupts(); // Disable interrupts so while(bit_is_set) check isn’t interrupted
} while (bit_is_set(ADCSRA,ADSC));  // back to sleep if conversion not done 
sleep_disable();  interrupts(); // Enable interrupts again
wADC = ADCW;  // Reading “ADCW” combines both ADCL & ADCH
sum += wADC;   // Add new reading to the total
} while (adc_irq_count<4096);  //sets how many times the ADC is read
ADCSRA &= ~ _BV( ADIE ); // No more ADC interrupts after this
internalDiodeReading=(sum >> 6); //decimation turns sum into an over-sampled reading


For now, I think I’ve kind of run out of ideas on how to make the WDT method more precise..? Oh well – It’s not the first time I’ve tried to re-invent the wheel and failed (and it probably won’t be my last….) At least it was a fun experiment, and who knows, perhaps I will find a better use for the technique in some other context.

I’ll spend some more time noodling around with the internal diode sensor and see if I can wrestle better performance out of it since it’s probably less vulnerable to aging drift than the WDT oscillator.  I’m still a bit cautious about oversampling the internal diode readings because the math depends on there being at least 1-2LSB’s of noise to work, and I already know the internal 1.1v ref is quite stable..?  I threw the sleep mode in there just to save power & reduce internal heating, but now that I think about it the oversampling might work better I let the chip heat a little over the sampling interval – substituting that as synthetic replacement for noise if there is not enough. The other benefit of the diode is that it will be resilient to a varying supply voltage, and we are currently  experimenting with more loggers running directly from batteries to see if it’s a viable way to extend the operating lifespan.

Addendum 2019-04-05

So I’ve kept a few units perking along with experiments oversampling the internal diode. And on most of those runs I’ve seen an error that’s quite different from the smoothly wandering WDT method. The internal diode readings have random & temporary jump-offsets of about 0.25°C:

si7051 (red) reference vs internal Diode (orange) over sampled 16384 reads then >>10 for heavy decimation.

These occur all all temperatures from +20 to -15C,  and I have yet to identify any pattern. The calculation usually returns to a very close fitting line after some period with the offset with about 50% of the overall time with a closely fit calibration. This is persistent through all the different oversampling intervals and stronger decimation does not remove the artifact – it’s in the raw data. No idea why…yet.

Tutorial: Using an MS5803 pressure sensor with Arduino

With the DS18B20 temperature sensors in place, it was time to add the ‘depth’ part of the standard CDT suite.  After reading an introduction to the Fundamentals of Pressure Sensor Technology, I understood that most of the pressure sensors out there would not be suitable for depth sensing because they are gauge pressure sensors, which need to have a “low side” port vented to atmosphere (even if the port for this is hidden from view).

This is one of our earliest pressure loggers with a 3″ pvc housing. We now use 2″ PVC pipes (shown at the end of this post) which are much easier to to construct. For an exploded view of the new housing see the appendix at the end of the article in Sensors.

I needed an “absolute” pressure transducer, that has had it’s low side port sealed to a vacuum. I found a plethora of great altimiter projects in the rocketry & octocopter world, (with Kalman filtering!) but far fewer people doing underwater implementations in caves.  But there are a few DIY dive computer  projects out there, at various stages of completion, that use versions of the MS5541C & MS5803 pressure sensors from Measurement Specialties, or the MPX5700 series from Freescale. Victor Konshin had published some code support for the MS5803 sensors on Github, but his .cpp was accessing them in SPI mode, and I really wanted to stick with an I2C implementation as part of my quest for a system with truly interchangeable sensors. That lead me to the Open ROV project were they had integrated the 14 bar version of the MS5803 into their IMU sensor package. And they were using an I2C implementation. Excellent! I ordered a couple of 2 bar, and 5 bar sensors from Servoflo ($25 each +shipping..ouch!) , and a set of SMT breakout boards from Adafruit. A little bit of kitchen skillet reflow, and things were progressing well. (note: I mount these sensors now by hand, which is faster after you get the hang of it it)

Breaking out the I2C interface traces to drive my pressure sensor. My first "real" hack on the project so far.

My first “real” hack on the project so far. (Note: This material was posted in early 2014 , and the only reason I did this hack was that at the time I was running the Tinyduino stack directly from an unregulated 4.5 battery. On my more recent loggers, built with regulated 3.3v promini style boards, I can just use the Vcc line to power the MS5803 sensors, without all this bother…)

But as I dug further into the MS5803 spec sheets I discovered a slight complication. These sensors required a supply voltage between 1.8 – 3.6 V, and my unregulated Tinyduino stack, running on 3 AA’s, was swinging from 4.7 down to 2.8v.  I was going to need some sort of voltage regulator to bring the logic levels into a range that the senor’s could tolerate, with all the attendant power losses that implied… And then it dawned on me that this same problem must exist for the other I2c sensors already available on the Tinyduino platform. So perhaps I might be able to hack into those board connections and drive my pressure sensor? (instead of burning away months worth of power regulating the entire system) The Tiny Ambient Light Sensor shield carried the  TAOS TSL2572 which had nearly identical voltage and power requirements to my MS5803.

I used JB weld to provide support for those delicate solder connections.

I used JB weld to provide support for those delicate solder connections.

So their voltage regulator, and level shifter, could do all the work for me if I could lift those traces.  But that was going to be the most delicate soldering work I have ever attempted. And I won’t pull your leg, it was grim, very grim indeed. Excess heat from the iron  conducted across the board and melted the previous joints with each additional wire I added.  So while the sensors themselves came off easily with an drywall knife, it took two hours (of colorful language…) to lift the traces out to separate jumper wires. I immediately slapped on a generous amount  of JB weld, because the connections were so incredibly fragile. I produced a couple of these breakouts, because I have other sensors to test, and I face this same logic level/voltage problem on the I2C lines every time I power the unregulated Tiny duino’s from a computer USB port.

With a connection to the mcu sorted, it was time to look at the pressure sensor itself. Because I wanted the sensor potted as cleanly as possible, I put the resistor, capacitor, and connections below the breakout board when I translated the suggested connection pattern from the datasheets to this diagram:

The viewed from above, with only one jumper above the plane of the breakout board.

This is viewed from above, with only one jumper above the plane of the SOIC-8 breakout. I used a 100nF (104) decoupling cap. The PS pin (protocol select) jumps to VDD setting I2C mode, and a 10K pulls CSB high, to set the address to 0x76. Connecting CSB to GND would set the I2C address to 0x77 so you can potentially connect two MS5803 pressure sensors to the same bus.

And fortunately the solder connections are the same for the 5 bar, and the 2 bar versions:

I've learned not waste time making the solder joints "look pretty". If they work, I just leave them.

I’ve learned not waste time making the solder joints “look pretty”. If they work, I just leave them.

After testing that the sensors were actually working, I potted them into the housings using JB plastic weld putty, and Loctite E30CL:

The Loctite applicator gun is damned expensive, but it does give you ultra-fine control.

The Loctite applicator gun is expensive, but it gives you the ability to bring the epoxy right to the edge of the metal ring on the pressure sensor.

So that left only the script. The clearly written code by by Walt Holm  (on the Open ROV github) was designed around the 14 bar sensor; great for a DIY submersible, but not quite sensitive enough to detecting how a rainfall event affects an aquifer.  So I spent some time modifying their calculations to match those on the 2 Bar MS5803-02 datasheet :

// Calculate the actual Temperature (first-order computation)
TempDifference = (float)(AdcTemperature – ((long)CalConstant[5] * pow(2, 8)));
Temperature = (TempDifference * (float)CalConstant[6])/ pow(2, 23);
Temperature = Temperature + 2000; // temp in hundredths of a degree C

// Calculate the second-order offsets
if (Temperature < 2000.0) // Is temperature below or above 20.00 deg C?

{T2 = 3 * pow(TempDifference, 2) / pow(2, 31);
Off2 = 61 * pow((Temperature – 2000.0), 2);
Off2 = Off2 / pow(2, 4);
Sens2 = 2 * pow((Temperature – 2000.0), 2);}


{T2 = 0;
Off2 = 0;
Sens2 = 0;}

// Calculate the pressure parameters for 2 bar sensor
Offset = (float)CalConstant[2] * pow(2,17);
Offset = Offset + ((float)CalConstant[4] * TempDifference / pow(2, 6));
Sensitivity = (float)CalConstant[1] * pow(2, 16);
Sensitivity = Sensitivity + ((float)CalConstant[3] * TempDifference / pow(2, 7));

// Add second-order corrections
Offset = Offset – Off2;
Sensitivity = Sensitivity – Sens2;

// Calculate absolute pressure in bars
Pressure = (float)AdcPressure * Sensitivity / pow(2, 21);
Pressure = Pressure – Offset;
Pressure = Pressure / pow(2, 15);
Pressure = Pressure / 100; // Set output to millibars

The nice thing about this sensor is that it also delivers a high resolution temperature signal, so my stationary pressure logger does not need a second sensor for that.

A Reef census is co-deployed with the pressure sensor to ground truth this initial test.

A Reefnet Census Ultra is co-deployed with my pressure sensor to ground truth this initial run.

So that’s it, the unit went under water on March 22, 2014, and the current plan is to leave it there for about 4 months. This kind of long duration submersion is probably way out of spec for the epoxy, and for the pressure sensors flexible gel cap. But at least we potted the sensor board with a clear epoxy,  so it should be relatively easy to see how well everything stands up to the constant exposure. (I do wonder if I should have put a layer of silicone over top of the sensor like some of the dive computer manufacturers)


Addendum 2014-03-30

I keep finding rumors of a really cheap “uncompensated” pressure sensor out there on the net for about 5 bucks: the HopeRF HSF700-TQ.  But I have yet to find any for sale in quantities less than 1000 units.  If anyone finds a source for a small number of these guys, please post a link in the comments, and I will test them out.  The ten dollar MS5805-02BA might also be pressed into service for shallow deployments using its extended range, if one can seal the open port well enough with silicone. And if all of these fail due to the long duration of exposure, I will go up market to industrial sensors isolated in silicon oil , like the 86bsd, but I am sure they will cost an arm and a leg. 

Addendum 2014-04-15

Looks like Luke Miller has found the the float values used in the calculations from the ROV code generates significant errors. He has corrected them to integers and posted code on his github. Unfortunately one of the glitches he found was at 22.5°C, right around the temperature of the water my units are deployed in. I won’t know for some months how this affects my prototypes. With my so many sensors hanging off of my units, I don’t actually have enough free ram left for his “long int” calculations, so I am just logging the raw data for processing later.

Addendum 2014-09-10

The unit in the picture above survived till we replaced that sensor with a 5-Bar unit on Aug 25th. That’s five months under water for a sensor that is only rated in the spec sheets for a couple of hours of submersion. I still have to pull the barometric signal out of the combined” readings, but on first bounce, the data looks good (mirroring the record from the Reefnet Sensus Ultra)  Since the 2-Bar sensor was still running, it was redeployed in Rio Secreto Cave (above the water table) on 2014-09-03. It will be interesting to see just how long one of these little sensors will last.

Addendum 2014-12-18

The 2Bar unit (in the photo above) delivered several months of beautiful barometric data from it’s “dry” cave deployment, and was redeployed for a second underwater stint in Dec 2014. The 5Bar unit survived 4 months of salt water exposure, but we only got a month of data from it because an epoxy failure on the temperature sensor drained the batteries instantly.  After a makeshift repair in the field, it has now been re-deployed as a surface pressure unit. The good news is that we had the 5Bar sensor under a layer of QSil 216 Clear Liquid Silicone, and the pressure readings look absolutely normal compared to the naked 2bar sensor it replaced. So this will become part of my standard treatment for underwater pressure sensors to give them an extra layer of protection.

Addendum 2015-01-16

I know the MCP9808 is a bit redundant here, but at only $5, it's nice to get to ±0.25C accuracy. The MS5803's are only rated to ±2.5ºC

I know the MCP9808 is a little redundant here, but at $5 each, it’s nice to reach ±0.25ºC accuracy. The MS5803’s are only rated to ±2.5ºC, and you can really see that in the data when you compare the two. The low profile 5050 LED still has good visibility with a 50K Ω limiter on the common ground line. Test your sensors & led well before you pour the epoxy! (Note: the 9808 temp sensor & LED pictured here failed after about 8 months. I suspect this was due to the epoxy flexing under pressure at depth because of the large exposed surface area. The MS5803 was still working ok.)

Just thought I would post an update on how I am mounting the current crop of pressure sensors.  My new underwater housing design had less surface area so I combined the pressure sensor, the temperature sensor, and the indicator LED into a single well which gives me the flexibility to use larger breakout boards. That’s allot of surface area to expose at depth, so I expect there will some flexing forces. At this point I have enough confidence  in the Loctite ECL30 to pot everything together, even though my open ocean tests have seen significant yellowing. The bio-fouling is pretty intense out there, so it could just be critters chewing on the epoxy compound. Hopefully a surface layer of Qsil will protect this new batch from that fate.

Addendum 2015-03-02

Just put a 4-5mm coating of Qsil over a few MS5803’s in this new single-ring mount, and on the bench the coating seems to reduce the pressure reading by between 10-50 mbar, as compared to the readings I get from the sensors that are uncoated. Given that these sensors are ±2.5% to begin with, the worst ones have about doubled their error.  I don’t know if this will be constant through the depth range, or if the offset will change with temperature, but if it means that I can rely on the sensor operating for one full year under water, I will live with it.

Addendum 2015-04-06

Just returned from a bit of fieldwork where we had re-purposed a pressure sensor from underwater work to the surface. That sensor had Qsil silicone on it, and while it delivered a beautiful record in the the flooded caves, where temperatures vary by less than a degree, it went completely bananas out in the tropical sun where temps varied by 20°C or more per day. I suspect that the silicone was expanding and contracting with temperature, and this caused physical pressure on the sensor that completely swamped the barometric pressure signal. 

Addendum 2016-02-01

Holding MS5803 sensor in place for soldering

Use the smallest with zip tie you can find.


I also use a wood block to steady my hand during the smd scale soldering.

Since these are SMD sensors, mounting them can be a bit of a pain so I though would add a few comments about getting them ready. I find that holding the sensor in place with a zip tie around the SOIC-8 breakout makes a huge difference.  Also, I find it easier to use the standard sharp tip on my Hakko, rather than a fine point  which never seem to transfer the heat as well.  I plant the point of the iron into the small vertical grooves on the side of the sensor.  I then apply a tiny bead of solder to the tip of the iron, which usually ends up sitting on top, then I roll the iron between my fingers to bring this the bead around to make contact with the pads on the board. So far this technique has been working fairly well, and though the sensors do get pretty warm they have all survived so far.  If you get bridging, you can usually flick away the excess solder if you hold the sensor so that the bridged pads are pointing downwards when you re-heat them.


Stages of MS5803 mounting procedure

After mounting the sensor to the breakout board, I think of the rest of the job in two stages: step one is the innermost pair (which are flipped horizontally relative to each other) , and step two by the outermost pair where I attach the incoming I2C wires.  Here SCL is yellow, and SDA is white.  In this configuration CSB is pulled up by that resistor, giving you an I2C address of 0x76.  If you wanted a 0x77 buss address, you would leave out the resistor and attach the now empty hole immediately beside the black wire to that GND line.

Sometimes you need to heat all of the contacts on the side of the sensor at the same time with the flat of the iron to re-flow any bridges that have occurred underneath the sensor itself. If your sensor does not work, or gives you the wrong I2C address, its probably because of this hidden bridging problem.

back side connection ms5803

Adafruit still makes the nicest boards to work with, but the cheap eBay SOIC8 breakouts (like the one pictured above)  have also worked fine and they let me mount the board in smaller epoxy wells.  Leave the shared leg of the pullup resistor/capacitor long enough to jump over to Vcc on the top side of the board .

Addendum 2016-03-08

Have the next generation of pressure sensors running burn tests to see what offsets have been induced by contraction of the epoxy.  I’ve been experimenting with different mounting styles, to see if that plays a part too:

These housings are still open, as it takes a good two weeks for the pvc solvent to clear out...

The housings are left open during the bookshelf runs as it takes a couple of weeks for the pvc solvent to completely clear out, and who knows what effect that would have on the circuits. (Note: for more details on how I built these loggers and housings, you can download the paper from Sensors )

The MS5803’s auto-sleep brilliantly, so several of these loggers make it down to ~0.085 mA between readings, and most of that is due to the SD card.  I’m still using E-30Cl, but wondering if other potting compounds might be better? There just isn’t much selection out there in if you can only buy small quantities at one time. The E30 flexed enough on deeper deployments that it eventually killed off the 5050 LEDs. (using standard 5mm body LEDs  now) And I saw some suspicious trends in the temp readings from the MCP9808 under that epoxy too…

Addendum 2016-03-09

Just a quick snapshot from the run test pictured above:


These are just quick first-order calculations (in a room above 20°C). Apparently no effect from the different mounting configurations, but by comparison to the local weather.gov records, the whole set is reading low by about 20 mbar. This agrees with the offsets I’ve seen in other MS5803 builds, but I am kicking myself now for not testing this set of sensors more thoroughly before I mounted them. Will do that properly on the next batch.

Addendum 2016-09-22

You can change the I2C address on these sensors to either 0x76 or 0x77 by connecting the CSB pin to either VDD or GND. This lets you connect two pressure sensors to the same logger, and I’ve been having great success putting that second sensor on cables as long as 25 m. This opens up a host of possibilities for environmental monitoring especially for things like tide-gauge applications, where the logger can be kept above water for easier servicing.

Addendum 2017-04-30

Given what a pain these little sensors are to mount, it’s been interesting to see the price of these pressure sensor breakout modules falling over time. This spring the 1 & 14 bar versions fell below $24 on eBay.  Of course they could be fake or defective in some way, but I’m probably going to order a few GY-MS5803’s to see how they compare to the real thing.