Enhance your Logger with an OLED & TTP223 Capacitive Touch Switch

A capacitive touch switch works through the lid of the housing. This lets you do things like check the battery status without disturbing your experiment on long runs.

I highlighted these cheap OLED screens as a useful addition in the 2020 build tutorial, but given that a typical deployment leaves the logger for long periods of time where nobody will see it, some have been asking if it’s worth sand-bagging the unit with a 20mA drain on every reading cycle. (ie: larger than the rest of the logger combined) So the today I want to explore another addition to the EDU build that makes screens viable without hurting the power budget. In sleep mode these screens only draw about ~20 μA. (even if you leave it’s redundant regulator in place) So the key is only triggering the pixels when there’s someone around to actually see it.

For text output l like the SSD1306Ascii library which is available through the library manager.  Grieman’s libraries are some of the best on offer whenever you need low power operation and a small memory footprint. With that installed you can drive the SSD1306 with a basic set of commands:

//  Compiler instructions at the start of your program: 
#include <SSD1306Ascii.h>                   // includes the main library itself
#include <SSD1306AsciiWire.h>          // use I2C peripheral inside the Arduino (optional)
SSD1306AsciiWire oled;                         // create a library object calledoled
#define   oled_I2C_Address   0x3C        // 0x3C or 0x3D depending on manufacturer

//  basic screen initialization in Setup{}  -this MUST be after wire.begin starts the I2C bus
oled.begin(&Adafruit128x64, oled_I2C_Address);
oled.setFont(System5x7);     // fonts specified in setup are included with the compile

// sending information the screen at end of the main Loop after the SD save
// you can also package these up into a stand-alone function (see below)

oled.clear();                          // erases anything displayed on the screen
oled.setCursor(0,2);           // ( 0-127 pixel columns , 0-7 text rows)
oled.set1X();                        // set single-row font height for labels
oled.print(F(“B280 T”));      // standard .print syntax supported
oled.setCursor(46,2);         // move cursor to column 46, but remain in same row
oled.set2X();                         // set double-row font height for readability
oled.print(bmp280_temp,2);    // a float variable, limited to two decimal places
oled.print(F(“o”));                    // a lower case ‘o’ for the degree symbol
 // … etc ... add more here until the display is ‘full’

// display pixels can be enabled at ANY time later . . .
oled.ssd1306WriteCmd(SSD1306_DISPLAYON);   // turn on the screen pixels
    delay(10000); // enough time to read the information 
oled.ssd1306WriteCmd(SSD1306_DISPLAYOFF);   // turn OFF the screen pixels

I’m only including the print statements for one line of the the display shown here ->  (click to enlarge)  but hopefully you see the pattern well enough to lather-rinse-repeat. The pixels do not need to be ‘turned on’ while you load the screen memory, and that data is persistent as long as the screen has power.  So if you wanted to tackle more advanced graphic output, you could build plots ‘one line at a time’ in the eeprom without needing a 127 field array to buffer that data.

The TTP223 Touch Switch:

Here I disabled the LED by removing the limit resistor, set the mode to momentary low, and trimmed the header pins so the upper surface is flat. The unmarked solder pads on the upper right are were you would add trimming caps to reduce sensitivity. These switches self calibrate for 0.5 sec at startup, so they need a bit of ‘settling time’ at power-on.

These small capacitive switches can be had for less than ~20¢ each on eBay. The power LED wastes about 8mA, so that needs to be disabled for logger applications. Then you set the operating modes by bridging the A / B solder pads. These switches are so sensitive that moving a finger within 2cm of the surface will trigger them. In applications where the sensor is exposed you probably need to add a 10-20 pF ‘trimming cap’ to prevent self triggering. Noise on your rail can also set them off, and you don’t want loose wires inside the housing near that sensing pad. TTP223 chips do not go rail to rail, but they still work ok driving MOSFETS.

A small square of double sided tape holds the T233 inside the student build just above the batteries. (The outer most rows of the breadboards are power rails)

In our case the sensor will be under 1.5mm of HDPE and another millimeter of double sided foam tape. So the default sensitivity level is  almost perfect for sensing through the lid of the  housing. With the LED disabled the TTP223 module pulls about 5μA when NOT being triggered, and 100μA when actively signaling the logger. Here I’ve connected the switch using 10cm pre-made jumpers but you want to be careful that you don’t put the switch in a position on the lid where it might cast a shadow over any light sensors as the sun moves through the sky. With our most recent student logger, you also need to shift the indicator led over to R4-GND5-Gr6-Bl7 so that the switch output (orange wire) can be fed into the hardware interrupt on D3. This chip produces ‘clean’ transitions so you don’t need to worry about de-bouncing. These switches seem to work fine without pullup, but I’ve been enabling the internal pull on D3 anyway.

Sensors such as rain gauges generate on/off interrupt signals at any time due to environmental conditions. But no matter how many times that happens, only the RTC’s sampling interval alarm should control the ‘regular’ read cycles in the main loop. To handle multiple interrupt sources we ‘trap’ the processor in a Do-While loop that checks flag variables (set in the associated ISR functions) to see where the wake-up signal originated.

In the code below; if the RTC flag variable is false when the processor reaches the while(flag status check) at the end of the loop, then the program gets sent back to the initial do{  statement. This code assumes you have already programmed the screens memory with data to display:

//  when you are about to put the logger to sleep  (after setting the RTC alarm)

pinMode( 0, INPUT_PULLUP );        // I always use pin D2 for the RTC alarm signal
rtc_d2_INT0_Flag = false;                 // D2 interrupt flag – this can only be set true in the ISR

pinMode ( 1 , INPUT_PULLUP );            // Cap. switch output is connected to D3
d3_INT1_Flag =
false;      // setting false here prevents any display until TTP223 is pressed

EIFR=EIFR;           // clears old ‘EMI noise triggers’ on BOTH hardware interrupt lines

do {                       // The ‘nesting order’ here is critical:

attachInterrupt (1,switchPressed_ISR, FALLING);
            attachInterrupt(0, rtc_d2_ISR_function, LOW);
                         LowPower.powerDown(SLEEP_FOREVER, ADC_OFF, BOD_ON);
            detachInterrupt(0);                    // MUST detach the high priority D2 first
detachInterrupt (1);                               // then detach the lower priority D3 interrupt

// Here we are simply powering the OLED display pixels but any other code put here
// is isolated from the main sequence, so you could trigger ‘special’ sensor readings, etc. 

  if (d3_INT1_Flag == true){
        oled.ssd1306WriteCmd(SSD1306_DISPLAYON);   // turn on the screen pixels
             attachInterrupt(0, rtc_d2_ISR_function, LOW);
               LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_ON);  // long enough to read
        oled.ssd1306WriteCmd(SSD1306_DISPLAYOFF);   // turn OFF the screen pixels
        d3_INT1_Flag = false;   // reset D3 interrupt flag

} while (rtc_d2_INT0_Flag == false);    // if RTC flag has not changed repeat the ‘trap’ loop 

if (rtc.checkIfAlarm(1)) { rtc.turnOffAlarm(1); }   // processor awake so disable the RTC alarm
EIFR=EIFR;    // clears leftover trigger-flags from BOTH D2 & D3

// now return to the start of the main loop & capture the next round of sensor readings

// and each attached interrupt requires an ISR function   (outside the main loop!)
switchPressed_ISR() {
if (d3_INT1_Flag==false)
{ d3_INT1_Flag = true; }     // Flag variables set in an ISR must be global & ‘volatile’

rtc_d2_ISR_function() {
if (rtc_d2_INT0_Flag==false)
{ rtc_d2_INT0_Flag= true; }

BOTH the TTP on D3 and the RTC alarm on D2 can wake the logger from sleep – but switchPressed_ISR() does not change the critical flag variable so a wakeup caused by the D3 will not break out of the Do-While loop. Only the rtc_d2_ISR_function() can set the tested flag to true and thus escape from the trap.  This general method that works equally well with inputs from reed switch sensors for wind, rain, etc. With a few tweaks the same basic idea can also be used to capture ‘opportunistic’ sensor readings  – provided you save those to a different logfile, or tag the ‘extra’ records with something that’s easily sorted from the regular records later in Excel.

Note that the two interrupt sources use different triggers: FALLING for the TTP223, and LOW for the RTC. It doesn’t really affect anything if the switch transition is missed (you can just tap it again), but the whole logger operation is affected if the RTC alarm fails. The RTC must also be able to interrupt the ‘display time’. Fortunately the RTC’s alarm output is ‘latched’ so it will cascade the wake-ups until we exit the do-while loop. The most important thing to know when using HIGH or LOW as your interrupt state is that you MUST detach the interrupt IMMEDIATELY upon waking. If you forget to detach a HIGH or LOW triggered ISR it will just keep on firing until it fills all of the variable memory with stack pointers and crashes your logger.

In addition to preventing the display from using power when nobody is around, having the ability to trigger the display ‘at any time’ is a great way to make sure the logger is OK without opening it – especially if all you want to see is the most recent timestamp & battery level.  This is also helpful when students are running labs that can’t be ‘physically disturbed’ ( for example, soil sensors tend to produce significant discontinuities if they get bumped in the middle of a run)  Nothing is worse than letting an experiment run for a week, only to find that the system froze up a few hours after it was started.

Handling multiple screens of information

Once you’ve got the basic screen operation working you might want to check our page on using two displays. Keeping each display in different ‘memory access modes’ lets you do some interesting graphical output tricks.  But what if you only have one screen and there just isn’t enough room to display everything at one time?

You can use flag variables &  switch-case to toggle between different screens of information:

//  I use defines in setup{} so the code is more readable, but these are just numbers
#define displaySOILdataNext 0

#define displayTEMPdataNext 1

//  set the ‘first screen’ you want to display before entering the do-while trapping loop
uint8_t next_OLED_info = displaySOILdataNext;

do {       // our processor ‘trapping loop’ described above

// load the screen’s eeprom at the start of the do-while loop
// each successive press of the touch-switch changes which data gets loaded

switch (next_OLED_info{

displaySOILdataNext:      // case where next_OLED_info = 0
         sendSOILdata2OLED();    // a separate function to program the display memory
         next_OLED_info=displayTEMPdataNext;  //toggles to send different info on next pass

case displayTEMPdataNext:    // case where next_OLED_info = 1
         sendTEMPdata2OLED();   // a separate function to load the eeprom
         next_OLED_info=displaySOILdataNext;  //toggle to opposite screen on next pass
}  // terminator for switch (next_OLED_info)

      {insert here} all the loop content controlling wake/sleep described earlier

} while (rtc_d2_INT0_Flag == false);    // end of our processor ‘trapping loop’

// and you will need stand-alone functions for each screen of information:

void sendTEMPdata2OLED() {
oled.print(TimeStamp+5);   // +5 skips first 5 characters of the string because it’s too long

oled.print(F(“Temp1: “));
               // … etc ... add more here until the display is ‘full’

void sendSOILdata2OLED() {
                // … etc ... add more here until the display is ‘full’ 

The nice thing about this method is that you can toggle your way through as many different screens of information as you need simply by adding more ‘cases’ and screen ‘loading’ functions.

Adding two OLED displays to your Arduino logger (without a library)

Two I2C 0.96″ OLED screens used simultaneously. Left is split Yellow: 128×16 pixels & Blue: 128×48 pixels while the right screen is mono-white. The CODE ON GITHUB drives each screen in different memory modes to simplify the functions and uses internal eeprom memory to store fonts/bitmaps.

Oceanographic instruments rarely have displays, because they don’t contribute much to profilers being lowered off the side of a boat on long cables. And most of our instruments spend months-to-years in underground darkness, far from any observer.  But a few years ago we built a hand-held flow sensor that needed to provide operator feedback while the diver hunted for the peak discharge point of an underwater spring. That prototype was our first compelling use-case for a display screen, and we used cheap Nokia 5110 LCDs because they were large enough to see in murky water.  While driver libraries were plentiful, I realized that Julian Ilett’s shift-out method could pull font maps from the Arduino’s internal eeprom rather than progmem. This let us add those SPI screens to a codebase that was already near the memory limits of a 328p.

Modifications done to the second display: The only thing you must do for dual screens is change the bus address. Here I’ve also removed the redundant I2C  pullups (R5&R6, 4k7) and bridged out the 662k regulator which isn’t needed on a 3.3v system. These changes usually bring sleep-mode current on 0.96″ OLEDs to about 2μA per screen. (or ~5μA ea. with the 662k regulator left in place)

Now it’s time to work on the next gen, and the 0.96″ OLEDs that we initially ignored have dropped to ~$2.50 each.  Popular graphic libraries for these displays (Adafruit, U8G2, etc)  provide more options than a swiss army knife but require similarly prodigious system resources. Hackaday highlighted the work of David Johnson-Davies who’s been using these screens with ATtiny processors where such extravagant memory allocations aren’t possible. His Tiny Function Plotter leverages the ssd1306‘s vertical access mode to plot a graph with a remarkably simple function.  These OLED screens support two bus addresses, and that set me to work combining that elegant  grapher with my eeprom/fonts method for a two screen combo. Updating Iletts shiftout cascade to I2C would loose some of the memory benefit, but I was already embedding wire.h in the codebuild for other sensors. It’s worth noting that David also posted a full featured plotter for the 1106, but these tiny displays are already at the limits of legibility and I didn’t want to lose any of those precious pixels. Moving the axis labels to the other screen forced me to add some leading lines so the eye could ‘jump the gap’:

My eyes had trouble with single-pixel plots so I made the line 2-pixels thick.

Histogram seems the best underwater visibility mode. Dashed lead-lines toggle off

A little repetition produces a bar graph variant.

The battery indicator at upper left uses the same function as the buffer memory progress bar in the center. Axis label dashes along the right hand side correspond to the lead lines on the next screen. My goal with this layout was ‘at-a-glance” readability. Horizontal addressing makes it easy to update these elements without refreshing the rest of the screen; so there is no frame buffer.

To drive these screens without a library it helps  to understand the controllers different memory addressing modes. There are plenty of good tutorials out there, but the gist is that you first specify a target area on screen by sending (8-pixel high) row and (single pixel wide) column ranges.  These define upper left  & lower right corners of a modifiable region and the ssd1306 plugs any subsequent data that gets sent into the pixels between those corner points using a horizontal or vertical flow pattern. If you send more data than the square can hold, it jumps back to the upper left starting point and continues to fill over top the previous data. In ALL memory modes: each received byte represents a vertical 8-pixel stripe of pixels , which is perfect for displaying small fonts defined in 6×8-bit blocks. For the taller characters I use a two-pass process that prints the top of the large numbers first, changes eeprom memory offset, and then does a second pass in the next row to create the bottoms of the characters. This is different from typical scaling approaches, but gives the option of creating a three-row (24 pixel high) font with basically the same method, and since the fonts are stored in eeprom there’s no real penalty for those extra bits.

In addition to text & graphs, I need a battery status indicator and some way to tell the operator when they have waited long enough to fill sampling buffers. In the first gen units I used LED pips, but similar to the way vertical addressing made the Tiny Function Plotter so clean, horizontal mode makes it very easy to generate single-row progress bars:

void ssd1306_HorizontalProgressBar
(int8_t rowint percentCompleteint barColumnMin,  int barColumnMax)    {

// Set boundary area for the progress bar
Wire.write(row); Wire.write(row);    // page  start / end  are the same in this case 
Wire.write(barColumnMin); Wire.write(barColumnMax); // column start / end

//determine column for the progress bar transition
int changePoint = ((percentComplete*(barColumnMax-barColumnMin))/100)

//loop through the column range, sending a ‘full’ or ’empty’ byte pattern
for (int col = barColumnMin ; col < barColumnMax+1; col++) {
   Wire.write(ssd1306_oneData);   // send 1 data byte at a time to avoid wire.h limits

        // full indicator = all but top pixel on,  also using the ‘filled’ byte as end caps
(col<changePoint || col<=barColumnMin || col>=barColumnMax)
        { Wire.write(0b11111110);  }
        else   Wire.write(0b10000010);   }    // empty indicator  = edge pixels only


I mount screens under epoxy & acrylic for field  units but hot glue holds alignment just fine at the prototyping stage, and it can be undone later.

And just for the fun of it, I added a splash screen bitmap that’s only displayed at startup. Since the processors internal eeprom is already being used to store the font definitions, this 1024 byte graphic gets shuttled from progmem into the 4k EEprom on the RTC modules we use on our loggers. It’s important to note that I’ve wrapped the font & bitmap arrays, and the eeprom transfer steps, within a define at the start of the program:

#define runONCE_addData2EEproms
—progmem functions that only need to execute once —

Since data stored in eeprom is persistent, you can eliminate those functions (and the memory they require) from the compile by commenting-out that define after it’s been run. One thing to note is that the older AT24c32 eeprom on our RTC module is only rated to 100kHz, while bus coms to the OLED work well at 400kHz.

You can update the memory in these screens even if the display is sleeping. So a typical logger could send single line updates to the graph display over the course of a day. There’s a scrolling feature buried in the 1306 that would make this perpetual without any buffer variables on the main system.  Color might make it possible to do this with three separate sensor streams, and the 1106 lets you read back from the screens memory, providing an interesting possibility for ancillary storage. However with Arduino migrating to newer mcu’s I think creative code tricks like that are more likely to emerge from ATtiny users in future. As a logger jockey, the first questions that comes to mind are: can I use these screens as the light source in some interesting sensor arrangement that leverages the way I could vary the output?
What’s the spectra of the output?  Could they be calibrated?   What’s the maximum switch rate?

Screens are quite readable through the clear 3440 Plano Stowaway boxes used for our classroom logger. (here I’ve hot glued them into place on the lid) Avoid taking new sensor readings while information is being displayed on the OLED screens because they ‘pulse’ power to the pixels (100ms default) when information is being displayed. Each screen shown here peaks at about 20 mA (this varies with the number of pixels lit) so you could be hitting the rail with a high frequency load of about 40mA with dual screens – which could cause noise in your readings unless you buffer with some beefy caps. Also note that the screens tiny solder connections are somewhat fragile, so avoid putting hot glue on the ribbon cable.

As a final comment, the code on github is just an exploration of concepts so it’s written with readability in mind rather than efficiency.  It’s just a repository of functions that will be patched into other projects when needed, and as such it will change/grow over time as I explore other dual screen ideasKris Kasprzak has a slick looking oscilloscope example (+more here) , and Larry Banks BitBang library lets you drive as many I2C screens as you have the pins for – or you could try a multiplexer on the I2C bus. These OLEDs will only get larger and cheaper over time. (& it will be a while before we see that with e-paper) An important question for our project is: which screens hold up best when subjected to pressure at depth? This was a real problem with the Nokia LCDs.

Addendum 2021-04-05:

I finally took a closer look at the noise from those cheap SSD1306 OLED screens, and was surprised to find the usual [104] & [106] decoupling combo simply weren’t up to the task. So I had to go to a 1000uF Tantalum [108] to really make a dent in it. Contrast settings reduced the current spikes somewhat, but did not change the overall picture very much.

The following are before & after graph of a start sequence from one loggers that end with 8 seconds of the logger sleeping while the latest readings get displayed on screen:

Logger current with a SSD1306 0.96″ OLED display: before & after the addition of a 1000uF Tantalum [108]

This unit was running without a regulator on 2x Lithium AA’s , and I had noticed intermittently flakey coms when the screen pixels were on. A forum search reveal people complaining about ‘audible’ noise problems in music applications, and others finding that their screens charge pump was driving the pixels near the default I2C bus frequency of 100kHz. I’ve dozens of these things lying around, and will add more to this post if testing reveals anything else interesting. The cap. pulls about 1A at startup so I’m not even sure I could put one that large on a regulated ProMini without pushing the MIC5205 into over-current shutdown.

Hacking a Capacitive Soil Moisture Sensor (v1.2) for Frequency Output

A $1.50 soil moisture sensor: ready to deploy. My first experiments with the pulsed output hack didn’t quite work due to probe polarization at low frequencies…but I’m still noodling with it.

There’s something of a cultural divide between the OpAmp/555 crowd and Arduino users. I think that goes some way to explaining the number of crummy sensor modules in the hobbyist market: engineers probably assume that anyone playing in the Arduino sandbox can’t handle anything beyond analogRead().  So it’s not unusual to find cheap IC sensor modules with cool features simply grounded out, because how many ‘duino users could config those registers anyway – right?

Legit suppliers like Adafruit do a much better job in that regard, but our project rarely uses their sensors because they are often festooned with regulators, level shifters, and other ‘user-friendly’ elements that push the sleep current out of our power budget. Sparkfun boards usually have leaner trim, but with all the cheap PCB services available now I’m tempted to just roll my own. Thing is, I keep running into the problem that in ‘prototype quantities’ the individual sub-components often cost more than complete modules with the reflow already done – and that’s before everything gets sand-bagged with shipping charges larger than the rest of the project combined.

So we still use a lot of eBay modules – after removing the usual load of redundant pullups and those ubiquitous 662k regulators.  Once you go down that rabbit hole you discover a surprising number of those cheap boards can be improved with ‘other’ alterations. The reward can be substantial, with our low power RTC mod bringing $1 DS3231 modules from ~0.1 mA down to less than 5µA sleep current. Another easily modified sensor is the soil moisture probe I flagged in the electrical conductivity post:

Schematic before conversion:  (regulator not shown). The output frequency is controlled by the time constant when  charging/discharging C3 through R2/R3. Gadget Reboot gives a good overview of how the basic configuration works, feeding the RC filtered 555 output into a simple peak detector. The V1.2 probes with the TLC555 run at a higher 1.5Mhz frequency with a 34% duty cycle.

These sensors use coplanar traces to filter high frequency output from a 555 oscillator, but you’re lucky to see a range of more than 400 raw counts reading that filtered output with Arduino’s 10-bit ADC.  On 3.3v systems, you can remove the regulator and combine the sensor with an ADS1115 for 15-bit resolution. I found you can’t push that combination past the ±4.096V gain setting or the 1115’s low input impedance starts draining the output capacitor. But that still gives you a working range of several thousand counts as long as you remember that the ADS1115 uses an internal vRef – so you also need to monitor the voltage supplied to the sensor to correct for battery variation.  Trying to read the sensors output with my EX330 when the sensor was running brought the output voltage down by about 0.33v and it’s got 10MΩ internal impedance similar to the ADS so a cheaper voltmeter will knock the output from this sensor around even more. Differential readings can reduce this problem because that doubles the ADC’s impedance.

Analog mode works well with long cable runs and piping that through an ADS1115 lets you communicate with three soil sensors over I2C if you are low on ports.  And up to four of those $1 ADC could be hung off the same bus – though I’ve learned the hard way not to put too many sensors on one logger because it risks more data loss if you have a point failure due to battery leaks, critters, or vandalism. It only takes a couple of hours to build another logger for each set anyway.

3.3v ProMini ADC readings from an analog soil moisture sensor at ~8cm depth (vertical insertion) with  DS18b20 temp from the same depth. We ran several of these sensors in our back yard this summer. This segment shows daily drawdown by vegetation during the hottest/driest month of the year – followed by several rainfall events starting on 9/4 . This logger was running unregulated from 2x lithium AA cells and the regulator was also removed from the soil sensor. Aref moves in step with the supply voltage so we didn’t have interference from battery variation. C5&6 were removed from the board and the sensor was powered from a digital pin with 8 seconds to stabilize ( probably more time than needed). This humble sensor was less affected by daily temperature cycles than I was expecting but if you use ‘field capacity’ as your starting point the delta was only ~200 raw ADC counts. I manually watered the garden on 8/23 & 8/24 to keep the flowers from dying; but it was only a brief sprinkle. We have 6 months of continuous operation on these probes so far but with only that thin ink-mask protecting the copper traces I’d be surprised if they go more than a year. Even with epoxy encapsulating the 555 circuit, small rocks could easily scratch the probe surface on insertion leading to accelerated corrosion.

These things are cheap enough that I’ve started noodling around with them for other tasks like measuring water level.  With a water/air dielectric difference of about 80:1 even unmodified soil probes do that job well if you put in a little calibration time. When powered at 3.3v, the default config outputs a range of ~3.0v dry to 1.5v fully submerged in water. So Arduino’s 10-bit ADC gives you decent resolution over the probes 10cm length. And since the TL555 doesn’t draw more than 5mA (after settling), you can supply it from a digital I/O pin to save power – provided give the sensor a couple of seconds to charge the output capacitor after power on. Adding 120-150Ω in series to throttle capacitor inrush still leaves you with ~1.2v air/water delta. The sensors I tested rise quickly but take more than 35 seconds to ‘discharge’ down if you suddenly move the probe from air to water – but that might be a faulty resistor connection. (That won’t have much effect on soil readings given how slowly the natural system changes…)

A quick search reveals plenty of approaches to making the sensor ready for the real world, but we’ve had success hardening different types of sensor modules with a epoxy + heat-shrink method that lets you inspect the circuits over time:

Cable & Epoxy Mounting:                               (click images to enlarge)

Clean the sensor with 90% isopropyl alcohol. An old USB or phone cable works for light – duty sensor applications. Here I’ve combined two of the four wires to carry the output.

3/4″(18mm) to 1″(24mm), 2:1 heat-shrink forms a container around the circuits, with smaller adhesive lined 3:1 at the cable to provide a seal to hold the liquid epoxy.

Only fill to about 15% of the volume with epoxy. Here I’m using Loctite E30-CL. This epoxy takes 24 hours to fully cure.

GENTLE heating compresses the tubing from the bottom up. This forces the epoxy over the components

Before finishing, use a cotton swab to seal the edges of the probe with the epoxy. Cut PCBs can absorb several % water if edges are left exposed.

Complete the heating over a rubbish bin to catch the overflow & wipe the front surface of the probe so that you don’t have any epoxy on the sensing surfaces.

Soil moisture sensors measure the volumetric water content of a soil indirectly by using some other property such as electrical resistance or dielectric permittivity. The relation between the measurement and soil moisture varies depending on environmental factors such as soil type, temperature, or the amount of salt dissolved in the pore water.  This is kind of obvious if you think about how differently sand behaves with respect to water infiltration & retention compared to a soil high in clay content – but the low level details get quite complex.

I generally use our Classroom logger for backyard experiments. You want at least 2-3 monitoring locations due to the spatial variability of infiltration pathways, and then 2-3 sampling depths at each spot for a complete profile. Agricultural applications usually install two sensors, with one shallow sensor at 25-30% of the root zone depth and one deep at 65-80% of the root zone depth. But research projects install up to one sensor every 10cm through the soil profile. If you are doing that then you might want to install the sensor boards parallel to the ground surface and only power them one at a time so they don’t interfere with each other.

Vernier outlines a basic volumetric calibration procedure for soil sensors which starts by measuring soil from your target area that’s been dried in an oven for 24 hours. To get another calibration point you add an amount of water equivalent to some percentage (say 5%) of that soils dry volume and take another sensor reading after that’s had a chance to equilibrate.  To get a complete response curve you would use 5-10 jars of dry soil, adding different relative %-volumes of water to each pot from 5 to 50%. (Even in clay the %volume of water at field capacity is usually less than 50%.) Then you can interpolate any intermediate sensor readings with something like Multimap.

If the distance between interdigital coplanar electrodes is comparable to the smallest dimension of each electrode, then ‘fringe fields’ are significant.  As a rule of thumb, the field usually has a penetration distance of between 0.5-1x the distance between the centers of the electrodes.  I found several papers modeling that as one third of the width of the electrode plus the gap between electrodes: EPD ≈ (W + G)/3. In this case I estimate the useful sensing distance is less, say 3-6 millimeters from the probes surface, so you have to take care there are no air gaps near the sensor surface during the calibration, and when you deploy. Agricultural sensors are often placed in an augured hole filled with a screen filtered soil slurry to avoid air bubbles. One of the biggest challenges is that even after a good deployment, the soil drys out and ‘breaks away’ from contact with the surface on your sensor. This will give you grief with almost all of the soil measuring sensors on the market. (with the possible exception of neutron probes)

The story gets more complicated with respect to growing things because the pores of a sandy soil might provide more plant available water than absorbent clay soils (see: matric potential) plus you have the root depth of your crop to consider. (typically 6-24 inches)  With all the confounding factors, soil sensors are often used in the context of ‘relative’ boundaries by setting an arbitrary upper threshold for the instrument’s raw output at field capacity (~ two days after a rain event) and the lower threshold when plant wilting is observed. Soil moisture sensors also need to be placed in a location that receives a similar amount of sunlight to account for evapotranspiration. Then there’s all the factors related to your sampling technique.

Using ‘field capacity’ as our upper limit might make it possible to do a basic three -point calibration of your sensors: Dig your trench and take ‘in situ’ measurements with your probe before you disturb the rest of the soil. Extract two samples  with a tubular ‘cutter’  who’s volume is large enough to cover your sensor and weigh them (you can back-calculate the gravimetric % moisture for those in-situ readings later from the sample you dry out ).  Dry one completely in an oven and the raise the other one to field capacity by adding water and then letting it drain & stabilize for a day or so in a 100% humidity chamber. (a big zip-lock bag?)  The wet sample must be allowed to drain under gravity until water is no longer actively dripping from it. Then weigh both samples again & take readings with your sensor embedded in the wet & dry soil samples packed back into their original sampling volume. The tricky part is that you probably need to use a metal tube for cutting your sample (and for the oven drying) but you don’t want metal near a capacitance probe later on – so you would want to transfer your soil plugs into a PVC pipe with the same internal volume using some kind of plunger before the final measurements. The idea is you want to keep the relative ‘density’ of the sample similar to the original soil insitu.

The Hack:                                                               (click images to enlarge)

After testing some ‘naked’ boards ( where I had removed all the components)  I realized that the bare traces fell within a workable capacitance range for the same astable configuration the timer was already configured for:

Get the regulated boards with the CMOS TLC555 chip rather than the NE555. 3.3v operation is out-of-spec for the NE and 1/2 the NE’s I’ve tried didn’t even work in their default analog config at 5v. (& the 3.3v 662k reg is below spec if your supply falls below ~3.4v)

I remove the regulator & bridge the Vin to Vout pads. If you are going to pin-power the sensor you also need to remove the reg’s caps C5&C6 as their inrush current could damage your MCU. Or add  ~150 limit resistor in series, but that will drop output voltage.

Note that some variants of these sensors come with the regulator already removed DO NOT BUY THESE BOARDS  – they use older NE555 chips which won’t operate at 3.3v.

Remove the T4 diode, C3 & C4 caps and R2& R3 resistors as shown.

Move the10k R1 to the R3 pads, and the 1Meg ohm R4 to the R2 pads. When the new R2 exceeds is more than 10x the new R3 resistor you approach a 50% duty cycle on the FM output.

Bridge the R1/T4 pads closest to the 555. This patches the frequency output to AOUT. Linking the other T4 pad to C3 replaces put’s the probes capacitance in its place.

There isn’t much left after the mod. It’s important to leave the C1 bypass in place but I haven’t had problems pin-powering other sensor boards with similar caps. It’s the bare minimum you can get away with. (& add a series limiter if pin powering)

It’s worth mentioning that the 555’s notorious supply-current spikes during output transitions might give you bad reads or weird voltage spikes. I tried pin powering anyway because the TLC555 is so much better than the the old NE555s which would surely destroy any IO pin used to supply it.  So I’m sailing pretty close to the wind here: it would be much wiser to just leave the original C5/6 caps in place and skip pin powering all together. Using a mosfet is the safer choice, but hey – this is a hack…right? Officially, as little as 0.1uF needs a limit resistor to protect I/O pins, but ‘unofficially’ pin-powering dodgey circuits with AVR pins is surprisingly robust.

With the probe capacitance varying from <30 pF to ~400 pF (air/water), using 1M/10K resistors on the R2&R3 pads will give you about 50 kHz output in air, and ~1.5 kHz in water. Or you could drop in your own resistor combination to tune the output.  Our ProMini based loggers have a relatively slow 8MHz clock so the upper limit for measuring period intervals is about 120kHz before things get flakey. On a 16MHz board you can reach 200 kHz easily and with a faster MCU you could tweak that to even higher speeds. But at first bounce, with parts I already had, the 1M&10K combination seemed  workable.

the TLC555 draws far less current and works at 3.3v.  It can also be pushed to 2 MHz in astable mode, though you’d need a more advanced counter to keep up. LMC555 & TLC551 work at even lower voltages than TLC555

You have variety of options to read pulsed signals with an Arduino. We reviewed those in the ‘Adding Sensors to an Arduino Datalogger‘ but the short version of that is:  Tillart’s TSL235R example works, or you could try PJRC’s FreqCount Library. Gate interval methods count many output cycles. This reduces error and lets you approach frequencies to about 1/2 your uC clock speed (depending on the interrupt handling). However at lower frequencies the precision suffers, so measuring the elapsed time during a single cycle is better.  Since I’ve already switched over to using the Input Capture Unit to read thermistors, I started with  Reply #12, on Nick’s Timers & Counters page.  This works with these hacked soil sensors, though it will occasionally throw a spurious High/Low outlier with those rounded trapezoidal pulses. Putting a few repeats through Paul Badgers digital smooth filter crops away those glitch reads and median filters are also good for single spike suppression.  Denes Sene’s Simple Kalman filter is also fun to play with, though in this case that’s  killing flies with a sledge hammer.

As usual, I pretty much ignored all the calibration homework and simply stuck the thing in the ground for a couple of weeks to see what I’d get. Which was fortunate in a way, because the sensor developed a problem which I probably would have missed during short calibration runs:

Probe Frequency [Hz] (left axis) at 5cm soil depth. Rain event on 9/5 reset the probe to normal for a few days.

The  daily thermal wobble was expected, but the probe repeatedly displayed an upward bend which did not match the daily ‘stair-step’ drying cycles seen with other nearby soil sensors. Were we slowly pulling the ions out of the soil matrix? Rain events would reset back to normal  behavior, but eventually the rising curve would happen again.  Perhaps that was because the water was leaving, but the ions were being held on the probe surface?  When I looked into the frequency of commercial sensors, they were running at least 100 kHz, and most were in the 10’s of MHz range. Some of them automatically increased frequency with increasing conductance specifically to avoid this type of problem.

While the heat shrink/epoxy method is our gold standard for sensor encapsulation, adhesive lined 3:1 heat-shrink can do a reasonable job on these sensors if you make sure the surfaces are super-clean with isopropyl alcohol & take time to carefully push out any air bubbles. (use gloves so you don’t burn your fingers!) A third alternative is to use hot glue inside regular heat shrink tubing – squashing it into full contact with the circuits while the glue is still warm & pliable. You still have to treat the edges of the PCB, but that can also be done with nail polish. Both the heat-shrink & hot glue methods can ‘pull away’ from the smooth sensor surface over time – epoxy encapsulation is more robust.

I also tried to measure the electrical conductivity of solutions with this hacked probe. But without polarity reversals, polarization again became a limiting problem – and this only gets worse if you increase resistor values to resolve more detail with slower 555 pulses.  So at low freshwater conductivities, where polarization is negligible, the probe works ‘ok’ as a low-resolution EC sensor if you take your readings quickly & then de-power the probe for a long rest period. (while stirring the heck out of it..? ) However the readings ‘plateau’ as you approach 10 mS/cm – so it’s not much use in the kind of brackish coastal environments we play in.

At it’s heart, this is just a variation of standard RC rise-time methods with the 555 converting that into pulsed output.  While my attempts with the 1M&10K pair didn’t deliver much, the parrot ain’t dead yet.  Changing that R2/R3 resistor combination to boost frequencies to much higher frequencies when the probe is in dry soil might reduce that polarization problem.  Or, with a fixed capacitor instead of the probe I could try replacing a resistor with some plaster of Paris & carbon rods for a matric potential sensor with pulsed output. Essentially using the circuit as a cheap resistance to frequency converter.

And then there’s all the other capacitive sensors that I could jumper onto the C3 pads.  At that point I might as well get out the hack-saw, because I’m really just using the top section of the board as a TLC555 breakout that I can mount under epoxy. When you consider how many of us are working with 3.3v MCUs, a low voltage 555 module should have been on the market already.  And while I’m thinking about that – where are all the 3v op-amp boards, with each stage jumper-able into several ‘standard recipes’, and linkable to the next by solder pads or holes along the outer edge?  I’m envisioning an SMD quad in the middle, and a few layers of elegantly designed traces & well labeled through-holes for population with resistors & caps.  Even at eBay prices, you’d think the mark-up would have made those common as dirt a long time ago . . .

Selected Sources:

Addendum: 2020-12-18

The analog soil sensor from the start of the post is still chugging away, but (with the exception of a few rainy days) after leaf-fall the soil sensor has basically leveled out at ‘field capacity’

Two heavy rain events in this record. With no more evapotranspiration, the soil stays saturated between them.

So there won’t be much going on over winter but, I will be leaving most of the loggers outside anyway. We have some northern projects waiting in the wings and I want to see how well the unregulated student builds stand up to the cold. Be interesting to see if the soil sensor can withstand being frozen right into the soil.  I expect those events will register as ‘extremely dry’ due to the reduced dielectric of ice.

Pro Mini Classroom Datalogger [2020 update]

2020 update to the Cave Pearl Classroom logger. This is a combination of inexpensive pre-made modules from the open-source Arduino ecosystem, and can usually be assembled by beginners in 1-2 hours.

(Latest Update: Jan 12, 2021)

Covid has thrown a spanner into the works for hands-on learning because even if you have the space to run a ‘socially distanced’ course, your students could still be sent home at any time.  With that in mind, we’ve divided the build tutorial from 2019 into separate stages that make it easier to restructure the labs:

1) Component prep:  requires the equipment you  normally have access to in the lab like soldering irons, heat guns, drills, etc.

2) Logger assembly: can be done remotely with scissors, wire strippers & a screwdriver. All connections are made by Dupont connectors or by clamping wires under screw terminals.

The complete tutorial can be run in person or if students are ‘distance learning’, the instructor can do the soldering (~15-20 minutes per set) and send out kits for the overall assembly. Even that will be challenging through a zoom window, so you might want to add USB isolators to protect tethered student laptops from accidental shorts. One big challenge of running a course remotely is the extra time to test all the parts before sending them out. A ‘breadboard logger’ like the one shown later in this post lets you do that testing quickly. Another challenge is that other USB devices can push the bus too fast for the slow serial UARTS, causing them to drop off the system due to timing conflicts.

During ‘normal’ runs if a student gets a bad component, or accidentally zaps something in one of the labs, then it simply triggers a brief process-of-elimination lesson while they swap in replacements from the storage cabinet until things are working. But remote students won’t have that option unless you send two of every part – which might be viable approach considering how cheap these components are:

This is a variation of the logger described in our 2018 paper but we’ve removed the regulator/ voltage divider and added screw terminals + breadboards for faster sensor connections during labs.  Bridging the I2C bus over the A2 & A3 pins leaves only two analog inputs on the screw-terminals. However for ~$1 you can add a 15-bit ADS1115 which provides differential analog readings using a fixed internal reference. So it’s unaffected by changes in battery voltage.

The main components:

 (NOTE: complete parts list with supplier links can be found at the end of this post) You don’t need the cable glands if you are using sensors that will work inside the housing (light, temp, acceleration, magnetometers, GPS, etc.) Don’t put holes in your housing unless you are sure you need them.

You will need a UART adapter module to program your logger This must support 3.3v output & is easier to use if the pin order matches the ProMini connections. UART modules available with the FT232, CP2102, & CH340 chips will all work if the chip maker supplies drivers for your operating system.
The Arduino IDE will NOT be able to communicate with your logger unless the driver for your UART module is installed on your Windows or MAC operating system. We use FTDI basic UART modules & you can download the driver at the FTDI website.   There’s a basic installation guide at Adafruit , a more detailed one at Sparkfun, and PDF guides from FTDI.UART chips can only supply ~50mA so if your sensors need more current you might run out of power causing a restart of the ProMini. (A somewhat common problem when testing high-drain GPS modules or wireless transmitters)

Two FT232 adapters (red) & a CP2102 UART module (blk) with a pin order that matches the ProMini. [ DTR-RX-TX-3v3-CTS-GND ]  I have connected ProMini’s to these UART modules the wrong way round many, many, times, and none have been harmed by the temporary reversal. Also note that FAKE FTDI chips are a common problem with cheap eBay vendors, so it might be safer to buy a  SiLabs CP2102, or CH340 UART from those sources. If you are using the ‘cheap ones’ it’s a good idea to include UART modules from two different manufacturers in the student kits to deal with the inevitable driver compatibility issues.

Component Prep.  Part 1:   Pro Mini   ( 3.3v 8Mhz )                       (click any image to enlarge)

Install the UART driver & IDE. Solder the UART pins & test ProMini board with the blink sketch:  Set the IDE to (1) TOOLS> Board: Arduino Pro or Pro Mini (2)TOOLS> ATmega328(3.3v, 8mhz) in addition to the (3)TOOLS> COM port to match the # that appears when you plug in the serial adapter.

Remove Limit Resistor to disable power indicator

With a known good Promini: Remove the power LED [in red square].                               SPI bus clock pulses will flash the pin 13 LED – so  leaving the pin13 LED connected will show you when data is being saved to the SD card.

Carefully remove the voltage regulator from the two leg side with snips. System voltage will vary over time, but the logging code we’ve provided on gitHub records the rail  without a voltage divider by comparing it to the 328p’s internal bandgap. Lithium AA’s also provide a very flat discharge curve.

Add header pins to the sides but DO NOT SOLDER THE RESET pin HEADERS – we will be using those screw terminals as power rails.

Bridge the two I2C bus connections with the wire leg of a resistor. Connect A4->A2 & A5->A3.

A4 & A5 I2C bus bridged to side rails

Adding:   DIDR0 = 0x0F;  in Setup disables digital I/O on pins A0-A3 so they can’t interfere with I2C bus.

Lithium AA batteries are preferred when running a 2-cell unregulated system because the slope of an alkaline discharge curve will reach the ProMini’s 2.7v brown-out with >50% of the battery capacity unused. (note that SD cards are safe down to ~1.8v) While the voltage of a ‘brand new’ Lithium AA is usually 1.8v/cell, that upper plateau usually settles at ~1.7 v/cell within an hour or two of starting the logger. That briefly dips below 1.6v/cell during >100mA SD card save events. At temps near 5°C (in my refrigerator) the SD write battery-droop reached about 1.5 v/cell while on the upper plateau. Lithium cells only deliver ~50% of their rated capacity at temps below freezing, but that’s still an improvement. And alkaline batteries leak quite ofteneven when they are not fully discharged. To date, leaking batteries have been the most common reason for data loss on our project.

The MIC5205 regulator is not efficient at low currents, so removing it reduces your sleep current by ~50%. However that modification also forces you to deal with a rail voltage that changes over time. Thermal rise from 15 to 45°C will raise your rail voltage by about 100 millivolts on lithium cells that have been in service for a few months. (~ 5mv /°C)  If your sensor circuit is a voltage divider that is being powered by the same  voltage the ADC is using as a reference then the ADC readings are unaffected by this.  However you will need to compensate for this in your calculations if your analog sensor circuits are not ratio-metric.  Battery thermal mass will cause hysteresis unless you read your reference resistors under the same conditions. Regulated ProMini’s usually see the rail vary by ~10-20 millivolts over a similar range of temperatures however it’s worth noting the reg/cap combinations on cheap eBay modules can be subject to other problems such as noise; which can be even more problematic wrt the quality of your data.  Most chip-based I2C sensor modules carry their own regulators (usually a 662k LDO) and use internal bandgap reference voltages so they are unaffected by the changing rail.

So making students deal with deal with power supply variation right from the start will save them from making more serious mistakes later because every component in your logger is a temperature sensor.  

Component Prep. Part 2:     Screw-Terminal Board & SD adapter

Reset terminals repurposed with jumpers under the board

At the UART end of the board: Use a tinned resistor leg to repurpose the RESET terminals: Join RST & GND pins on the digital side and link the pins labeled RST, 5v and Vin for the positive rail.  (include Vin only if the reg. has been removed! )

Label the screw terminal blocks that were connected under the shield with red & black markers to indicate that these are now the power rails connections.

Gently rock the Pro Mini back to front (holding the two short sides) until the pins are fully inserted. Some ST shields have slightly misaligned headers so this insertion can be tricky.

Remove the last three ‘unused’ female headers to make room for the SD adapter

…which fits perfectly into that pocket

Remove the bottom 3 resistors from the SD adapter – leave the top resistor near the C1 label in place!

Connection map for analog side of Nano S-Terminal board

NOTE: The Screw Terminal board we use in this build was designed for the 5v Arduino NANO, so the shield labels don’t match the Pro Mini pins on the ‘analog’ side. (the digital side does match) To avoid confusion may want to tape over those incorrect labels and hand write new labels to match the pattern above. Wire connections in this tutorial will be specified by ProMini pins:  D10-13 are used for the SD card, A4/A2 is the I2C Data line, and A5/A3 is the I2C clock line.

Technically speaking, bridging the I2C bus (A4=data & A5=clock) over top of A2 & A3 subjects those lines to more capacitance and pin leakage. (regardless of whether that channel is selected as input for the ADC p257).  However in practice, the 4K7 pull-up resistors on the RTC module handle that OK at the 100 kHz default bus speed.

Adding DIDR0 = 0x0F;

in setup disables digital I/O on pins A0-A3 to prevent interference with A0/1 ADC readings and the I2C bus on A2/3. If you want to disable only the digital IO on only A2 & A3 add

bitSet (DIDR0, ADC2D);
bitSet (DIDR0, ADC3D);

to setup{}.

Component Prep.  Part 3:     RTC Module,  Indicator  LED  &  Plano 3440 Housing

Remove two SMD resistors from the RTC board with the tip of your soldering iron. Note that this module includes 4k7 pullup resistors on SQW, SCL & SDA – leave those in place!

This step is optional!  Cutting the Vcc leg on the DS3231 lowers sleep current by 0.1mA

Add 90 degree header pins to the I2C cascade port. Note: Cutting the VCC leg also requires you to ‘enable alarms from the backup battery’ with a registry setting.

Clean flux residue from both the main & cascade header pins with 90% isopropyl alcohol

Use a coin cell to determine the GND leg of a diffused common cathode RGB LED

Solder a 1-2k ohm limit resistor on the common GND. The precise value is not critical.

Add heat shrink, bend & trim the pins for connection to the screw terminal board. Pre-made 5050 modules also work but are not as good in light sensing mode.

Add holes in the rear struts of the Plano 3440 Stowaway housing to provide logger tie-down points later.

Stepped drill bits make clean holes in plastic  housings for different thread diameters. We use glands with waterproof DS18b20 sensors on 1m cables.

Centered holes are ‘slightly’ closer the bottom of the box to allow the gland to rotate without hitting the rim. Inner nuts must also be able to rotate for tightening.

Rubber washers are added to both the inside AND the outside surfaces. PG7 sized washers also fit the M12 cable glands we use.

A 3440 Plano Box configured for two external sensors with nylon M12 cable glands for 3-6mm cables. After threading  sensor cables I often seal the outside of the glands with a layer of silicone goop or nail polish.

As with the ProMini, it’s worth testing the RTC modules  & SD adapters before logger assembly. I keep a breadboard version of the logger handy so I can test several at a time. If the RTC temp register reads too high I throw them out because the clock time is corrected based on those internal readings. You could also set the clock at this point if the coin-cell is already in place. Note the 5050 LED module on the breadboard shown above could replace the 5mm LED in this tutorial.

Forcing the RTC to run from the backup coin cell reduces sleep current by ~0.1mA, bringing a typical “no-reg & noRTCvcc” build to ~0.15 mA  between readings. (with most of that for the sleeping SD card)  As a rough estimate, Lithium AA’s provide ~7 million milliamp-seconds of power, and your logger will burn ~12,960 mAs/day at 0.15mA. So ‘in theory’ you could approach a year before you fall off the ‘upper plateau’.  The clock will reset to Jan 1st, 2000 if you disconnect the RTC’s coin cell with a hard bump, but a couple of drops of hot glue should prevent this. If a reset does occur the time stamps will be wrong, but the logger will continue running the next day once the clock rolls around to the previous hour/minute alarm that was set.  A CR2032 can power the RTC for about four years but if you cut the vcc leg you must set bit six of the DS3231_CONTROL_REG to 1 to enable alarms or the logger will not be able to wake up. (NOTE: our logger code does this by at startup with the enableRTCAlarmsonBackupBattery function, which only has to run once – the RTC remembers the setting after that)

The soldered components ready for assembly.

Cutting the Vcc leg on the RTC is optional: if you leave the RTC power leg attached you’ll see typical logger sleep sleep currents in the 0.25 mA range, which should still give at least 4 months of operation before you trigger a low voltage shutdown. I’m being conservative here because runtime also depends on sensors and other additions you make to the base configuration.

See our RTC page for more detailed information on this DS3231 module.

Assembly Part 1:  The Screw-Terminal Stack

Part 1: This stack is the ‘core’ of your data logger.

It is very easy to get a couple of wires switched around at this stage so work through these instructions slowly & carefully.  Connect the Dupont jumpers to the SD module so that the metal retainer clips are facing upwards after the logger is assembled. That way you can diagnose connection issues with the tip of a meter probe on the exposed metal and, if necessary, pull out & replace a single bad wire without taking everything apart. The extra wires you trim from the SD module are re-used.

Add 2-3 layers of double sided foam tape to the sides & center of the screw terminal shield. The tape needs to extend farther the height of the solder points.

Add single wires from the  20cm Dupont cable to the SD adapter.

The ‘metal tabs’ of the Dupont ends are facing the large metal shield over the SD card.



A layer of double sided foam tape secures the connectors & extends over the solder points.

Grey=CableSelect [d10]
Orange=MOSI [d11]
Brown=CLocK [d13]
Purple=MISO [d12]
Black =GND

Tape the SD adapter into place on the area that’s been cleared at the back of  the screw terminal board.

Bend the jumper wires into place, mark the length & trim the original 20cm wires so that the final insulation length is 9cm or less.

Score the wire insulation about 1.5cm back from the wire end but do not remove the insulation.

Gently ‘pull & twist’ the  scored insulation away from the wire to wind the thin strands together.

‘Fold-back’ about half of the stripped wire to provide more copper surface for the screw terminals to bite on to.



SD Connection pattern:    Grey (CS) to ProMini D10Orange (MOSI) -> pm D11Purple (MISO) ->pm D12,      Brown (CLK) -> pm D13  (NOTE the shield labels say A0-A3 which do not match the D10-13 pattern of the Pro Mini pins)

Bring the red SD wire over to the re-purposed RST power connection

and the black wire to GND on the digital side. At this point you could test the  connections by inserting an SD card & running the CardInfo utility.



Connect the legs of the indicator LED at: D3=red, D4=GND, D5=green, D6=blue

Use the male ends of the wires you trimmed from the SD module to break out pin connections: Grey to A0, Brown to A1, Orange to D7, Purple to D8.

Add a layer of heavy duty (30Lb) double sided mounting tape to the back of the 2xAA battery holder. The battery holder wires need to be approx. 6inches/15cm long.

Attach the battery holder wires to RED>Vin & black>GND.  The breakout wires from A0/1 & D7/8  should be about 12cm long to comfortably reach the breadboard area.

Checking continuity at the ProMini solder point also confirms your pin header & screw terminal connections are good.

Take a moment to check the continuity of the SD module wires. With one probe on the Dupont metal & the other on top of the corresponding ProMini pin – you should read ~1 ohm or less for each connection path. Occasionally you get a bad crimp-end on those multi-wire Dupont ribbons, and it’s easier to replace a bad wire at this stage than it is after the parts are in the housing.

Note: We’ve used the hardware interrupt port at D3 for the red LED channel, but if you have sensors that need that simply shift the LED over by one. Any digital I/O pins can be used for the LED, but 3,5 & 6 have PWM outputs which lets you do multi-color fades with analogWrite()

Assembly Part 2: Add RTC Module Jumper Wires

Attach Dupont jumper wires to the RTC module using White=I2C Data, Yellow = I2C Clock. Blue is the SQW alarm output line. Nothing is attached to the 32k output pin as cutting the chips Vcc leg disables it.

Use 20cm M-F jumpers on the input side and shorter 10cm M-F wires on the smaller cascade port.


Add a layer of double sided tape to secure the jumper shrouds, and provide housing attachment points for the module.

Add as small piece of 1/16″ heat shrink tubing to reinforce the contact spring. This reduces the chance that the connection will be bumped loose if the logger is dropped.

Write the installation date on the coin cell with a marker. A new coin cell should power the RTC for about 4 years.          To avoid accidentally disconnecting the coin cell battery after the logger is assembled ->

Always check the backup voltage by placing the positive probe tip between the spring plates rather than on the surface of the cell.

Assembly Part 3:    Connect modules inside the housing:

Part 3: This is what you are aiming for. (click any images in the table below to enlarge them)

The final assembly stage can be a bit tricky – sometimes the metal contact flaps under the green screw terminals are ‘sticky’ so take some time to loosen  the screws and poke with the sharp end of your tweezers to make sure you can insert the bus wires from the RTC. It’s helpful to mark the wire lengths with a pen before cutting or stripping the RTC connections. When in doubt leave them a bit long. With screw terminals you always have the option of shortening those wire later on.

Attach the screw terminal stack onto the upper left corner of the housing, and the 2xAA battery holder in the lower right corner. These parts sb as far from each other as possible.

Add the first mini breadboard against the back so that it’s rear right edge aligns with the second rear support strut on the housing. Connect the long stack jumpers to that bboard to keep them out of the way.

Tape the RTC module into the lower left side of the housing, The blue board should be up against the front edge of the housing  so that you can easily access the nearby screw terminals.

Measure, mark and trim:  Red=3.3v, Yellow=>A3/A5 I2C clock, & White=>A2/A4 I2C data with enough wire to twist & fold the stripped ends under the terminals (~10cm of insulation length)

RTC module power & I2C bus connections:  All ports on the analog side of the Pro Mini / Shield combination are occupied with a wire connection.

Connect the black wire to the re-purposed RST=GND, and the blue alarm wire to  D2, leaving some extra wire length in a loop for strain relief. (5-6cm of insulation length)

Use the other side of the trimmed blue jumper wire  to extend the D9 connection over to the breadboard. You want enough wire length that the pins reach back-most row on the breadboard.

Attach a cable mount to the back of the housing, as close to the bottom of the box as possible so that it does not interfere with closing the housing lid. You can trim those plastic mounts with scissors to make them thinner.

‘Loosely’ tie the long wires to the rear mount. Add another cable mount near the center and attach the 2nd b-board leaving equal side of the 2nd board.

Every year at least one student gets confused about the orientation of the connections inside the breadboard and connects all the jumper wires together in the same row – including the red and black power wires. The resulting short circuit usually kills either the Pro Mini, the UART module, and/or possibly even the USB port on the computer it’s connected to:

Each 5-hole row on the top of the breadboard is connected by a metal rail of spring contacts.

Also note that the internal connectors do not cross the ‘gutter’ depression in the middle, so each side of the breadboard board has its own separate set of connections.

Your Logger is now ready for testing!

A typical I2C sensor configuration with: BMP280 pressure, BH1750 lux & 0.96″ I2C OLED display – connected by short jumper wires made with a crimping tool. The combination shown above averages ~10mA with screen & cpu running, and a sleep current of 0.147 mA with a 1Gb Sandisk SD card. Without the SD, the sleep current on this unit was 37µA; with the sensor modules needing 2-3µA each & the sleeping 0.96″ OLED drawing ~7µA.  A 25µA sleep current from the ProMini clone hints that the MCU might be fake but with a AA power supply it doesn’t really matter. Anything up to 250uA sleep current for a student build with an SD card connected should be considered good.  Watch out for SD cards that don’t go to sleep properly as they can draw up to 30-50mA all the time.

(Note: Most of the time the tests listed below go well, however if you run into trouble at any point read through the steps suggested for Diagnosing Connection Problems at the end of this page.)

1. If you have not already done so, Install the UART driver. The IDE will NOT be able to communicate with your logger unless the driver for your UART module is installed on your operating system.

2. Install the Arduino IDE into whatever default directory it wants – we’ve had several issues where students tried to install the IDE into some other custom sub-directory, and then code wouldn’t verify without errors because the IDE could not find the libraries. The programming environment is written in Java, and the IDE installer comes with its own bundled Java runtime so there should be no need for an extra Java installation. However we have seen machines in the past which would not compile known-good code until Java was updated on those machines; but this problem is rare.

If you have not already done so, there are three things you need to set under the IDE>TOOLS menu to enable communication with the logger:

Note: that the “COM’ setting will be different for each computer, so you will have to look for the one that appears on your system AFTER you plug in the UART module.

Using 22AWG solid core jumpers to bridge a set of ‘power rails’ along each side keeps the wiring tidy. If you are using several I2C sensors you could also do this with those bus lines.  After testing your prototypes, you can make them permanent by  transferring your circuit to solder-able mini breadboards.

The one that’s easy to forget is choosing the 328P 3.3v 8Mhz clock speed. If you leave the 328p 5v 16mhz (default), the programs will upload OK, but any text displayed on the serial monitor will be random garbled characters because of the clock speed mismatch.  Also be sure to disconnect battery power (by removing one of the AA batteries) whenever you connect your logger to a computer.  There is no power switch on the loggers, which are turned on or off via the battery insertion. Use a screwdriver when removing the batteries so that you don’t accidentally cause a series of disconnect-reconnect voltage spikes which might hurt the SD card.

3. Test the LED – the default blink sketch uses the pin13 LED, but because that pin is shared with the SD card’s clock line it’s recommended that you test the RGB indicator instead by adding commands in setup which set the digital pin 4 act as the ground line for the LED:
     pinMode(4, OUTPUT);   digitalWrite(4, LOW); 
Then change LED_BUILTIN in the blink code example to the number of a pin connected to your led module. (ie: for Red set it to 3, for Green use 5, or Blue use 6)

4. Scan the I2C bus with the scanner from the Arduino playgound. The RTC module has a 4K eeprom at address 0x56 (or 57) and the DS3231 RTC chip should show up at address 0x68.

The address of the eeprom can be changed via solder pads on the board, so it may have a different address. If you don’t see at least these two devices listed in the serial monitor when you run the scan, there is something wrong with your RTC module or the way it’s connected: It’s very common for a beginner to get some of the wire connections switched around during assembly but with the screw terminals this takes only a few moments to fix.

5. Set the RTC time, and check that the time was set – The easiest method would be to use the SetTime / Gettime scripts from our Github repository, but you first you need to download & install this RTC control library  The SetTime script automatically updates the RTC to the moment the code was compiled (just before uploading) so only run SetTime once, and then upload the GetTime sketch to remove SetTime from memory. Otherwise SetTime will keep setting the RTC to the old ‘code compile time’ every time it runs – and one of the quirks of the Arduino environment is that it restarts the processor EVERY TIME you open a serial window. The SetTime script also has a function which enables the alarm(s) while running the RTC from the backup coin cell battery.

Note:  the RTClib by Mr. Alvin that we use has the same name as the Adafruit library for this RTC and this can give you compiler errors if you let the IDE ‘auto-update’ all of your libraries because it will over-write the Alvin RTClib with Adafruit’s library of the same name. You then have to uninstall the Adafruit library ‘manually’ before re-installing Alvin’s RTClib again. This problem of ‘two different libraries with the same name’ was common back when this project started many years ago, but back then the IDE didn’t try to update them automatically.

Typical Cardinfo output on a windows computer when the connections are correct. If you format your SD card on an Apple computer there will also be a long list of ‘invisible’ .trash and .Spotlight files/folders at the root of the SDcard that show up with a CardInfo scan.

6. Check the SD card with Cardinfo
Note that the SDfat library we use to communicate with SD cards works well with smaller cards formatted as fat16, but ‘some’ Apple users find they can not write to cards in that format, requiring the SD cards to be reformatted as fat32 (note that most Apple systems have no problem with the fat16 SDcards). With either OS you should format the micoSD cards with this SDFormatter utility.  With a 15 minute sampling interval, most loggers generate ~ 5Mb of CSV format text files per year. Older, smaller SD cards in the 256-512Mb range often use less power. Note that we apply internal pullup resistors on some of the SD card lines in setup to help the SD cards go into low power sleep modes more reliably.

7. Calibrate your internal voltage reference with CalVref from OpenEnergyMonitor.

This logger uses an advanced code trick to read the positive rail voltage to ~11mv resolution by comparing it to an internal 1.1v bandgap reference inside the processor. That internal ref. can vary by ±10% from one chip to another, and CalVref gives you a numerical constant which usually brings the starter script’s rail=battery readings within ±20mv of actual. An accurate rail reading is more important when you are using ANALOG sensors where the positive rail directly affects the ADC output, but you can skip this procedure if you are only using digital sensors because they use their own internal reference voltages.

Typical CalVref output          (click to enlarge)

Load CalVref while the logger is running from USB power and then measure the voltage between GND and the positive rail with a  voltmeter. (this voltage will vary depending on your computer’s USB output, and the UART adapter you are using) Then type that voltage into the entry line at the top of the serial monitor window & press the enter key. Write down the reference voltage & constant which is then output to the serial monitor window. I write these ‘chip-specific’ numbers inside the logger with a black marker as they are related only to the 328p processor on the ProMini board used to make that particular logger. You then need to change the line #define InternalReferenceConstant 1126400L in our starter code to match the long number returned by CalVref. Alternatively you could just tweak the value of the reference constant ‘by hand’, increasing or decreasing the value till the reported rail readings match what you measure with a voltmeter. After you’ve done this once or twice you can usually reach the correct value with about 10 successive guesses.

8. Find a script to run your on logger. For test runs on a USB tether, the simplest bare-bones logger code is probably Tom Igoe’s 1-pager at the Arduino playground. It’s not really deploy-able because it never sleeps the processor, but it is still a useful ‘1-pager’ for teaching exercises and testing sensor libraries.  In 2016 we posted an extended version of Tom’s code for UNO based loggers that included sleeping the logger with RTC wakeup alarms. Our current logging “Starter Script” has grown since then to ~750 lines, but it should still be understandable once you have a few basic Arduino programming concepts under your belt.

Using the logger for experiments:

It’s important to understand that this logger was designed a teaching tool rather than a off the shelf, plug-&-play solution. Learning how to solder and getting some experience physically ‘putting things together’ are key outcomes.  Wrangling code into shape driving some new sensor combination is another vital part of that process.  So perhaps the best piece of advice I can give to new builders is:

Test,  Test,  and when you think your logger working: Test it Again

It’s nearly impossible to write code without little bugs and the only way to root them out is with multiple test runs. And even if the script you wrote is ‘perfect’ the processors on the sensor modules are also running code that you don’t have access to. For example, the mcu inside your SD card memory is more powerful, and may have more code on it than the ProMini at the heart of this logger. The only way to catch timing errors that might not get triggered until the 10th or 200th(?) pass is to run your code with a short 1 minute sampling interval until it’s crossed those roll-over thresholds many times. Use “Starting sensor X read” & “Finished sensor X read” print statements liberally during early USB tethered tests so you can observe the timing of events.

If you see water condensing like this on the lid of your logger then it’s time to examine the o-ring and add some extra sealant (nail polish or silicone) around the exterior of the cable glands. This logger quit after one week and it only lasted that long because of the desiccant pack.

Same thing applies to the sensor hardware in terms of durability, only now you have moisture to deal with. Everything that can be sealed in adhesive lined heat shrink, or potted in epoxy should be, once that hardware has passed your ‘dry’ tests. As a general rule no $10 sensor is going to be rated past IP68 which at best gives you 2-3 weeks of operation in the real world before water works it’s way in because of pressure imbalances caused by daily thermal cycling. You’d be surprised how easily moisture can wick along the air space ‘between the copper strands’ inside wires.

A doubling schedule works well for testing:  Check the logfile at 1 hour, 2 hours, 4 hours, overnight, 1 day, 2 days, 4 days, 8, 16, 32… etc. Move to the next longer test only when the data from the previous run is confirmed. Keep a close eye on that battery burn down rate: Until you get the hang of putting your sensors into low current ‘sleep’ states – getting your first logger builds to run for a couple of months on new batteries should be considered a spectacular success. At every startup watch and wait for the pattern of LED flashes to confirm that the launch went smoothly – it is very easy to insert a battery or SD card crooked by ‘just enough’ that the unit does not start, and it’s very frustrating to discover you have no data a week later.

You never really know how long a sensor is going to last until you’ve deployed it – no matter what the manufacturer says in the data sheet. Even then we usually deploy three of every ‘new’  combination, and if we are lucky we get one complete data set for the year.  Batteries leak, critters love to chew on things, and whenever humans come across something they’ve not seen before they will pick it up – especially if you had to invest a good deal of time securing your logger in exactly the correct position in the stream, on the tree, etc. We never deploy anything for real research until it has passed a several week-long rapid sample ‘burn-in’ test.

One positive aspect of the relatively loose fit of the Plano box lid is that it lets you run sensor tests quickly if you jumper your sensor module with thin 28-30 gauge wires:

A BMP280 pressure sensing module on long wires with crimped male dupont ends in the breadboard.

~1″ square of foam mounting tape with wires spaced evenly


Leave the red backing facing up as you fold the tape & wires over the corner edge.

The front corners of the box exert less pressure than the back corner shown here.

The sharp inner edge of the lid would cut the wire insulation if the tape was not there to protect, and even then you can only use this trick a few times.

The tape over the wires has to be replaced every time.

This gives you a chance to do some test runs before you commit to modifying the housing with holes or cable glands. For some indoor experiments this might be all you actually need, though I would still coat the ‘non-sensing’ parts of that dangling breakout with either conformal coating or clear nail polish. My general advice is: Do not put holes in the housing unless you are sure you need them.  The most common failure mode for student loggers used in outdoor environments is from moisture seeping into the through the cable gland. Natural heating and cooling cycles creates pressure differences between the inside and outside of the logger that drive this vapor exchange.  Moisture then condenses when temperatures fall at night, collecting on leftover flux residue to corrode contacts. An outer layer of self-fusing rubber mastic tape is often used on cable glands by electricians on out-door installations – even when using the expensive ones with soft rubber ‘cages‘.

After 1-2 minutes of kneading to mix the epoxy you have ~ 1 minute to work the putty into place. (it will become rock-hard within ~10 minutes). Be sure to leave yourself enough extra wire/space inside the housing so that you can open and close the lid easily without disconnecting anything after the putty hardens. This seal is not strong enough for underwater deployments, but it should easily withstand exposure to rain-storm events. HOT GLUE also works to seal pass-through ports with smaller wires & cables. Both pass-through methods can be helped by a layer of silicon caulking, nail polish, or conformal coating applied to the outside edges.

For a classroom project you could simply drill small a hole through the lid and stick the sensor/module on top of the housing, sealing the hole with double-sided tape. Thicker pass-throughs can be also be sealed reasonably well with plumbers epoxy putty which is non-conductive, and adheres quite well to  metal, glass & plastic surfaces->  This putty is also a quick way to make custom mounting brackets, or even threaded fittings if you wrap it around a bolt (which you carefully remove before the putty hardens completely)

No matter which pass-through method you are using: Silica gel desiccant packs are important for any outdoor deployments and 5-10 gram packets are a good size for this logger.

Don’t subject these loggers to a lot of bashing around by deploying them in a rough surf-wash zone, or swaying freely in the wind off the end of a tree branch. Solid core wires are pretty good if you cut them to exactly the right length , but longer beadboard jumpers are very easy to bump loose, so once you have your prototype working, it’s usually best to re-connect the sensors directly to the screw terminals before deploying a logger where it could get knocked around. In a pinch you can secure breadboard pins with a small drop of hot glue to keep them from wiggling.  Also remember that there are six ‘unused’ screw terminals on the base shield and these can be use to join wires together without soldering. 

2019 Logger mounted on a south-facing window. The top surface was covered with  white label-maker tape to act as a diffuser. 

[Click HERE] to read about the many types of sensors can be added to this logger The transparent enclosure makes it easy to do light-based experiments. Grounding the indicator LED through a digital pin allows it to be used as both a status indicator, and as a light sensorThe code we use is a polarity reversal technique that relies on the tiny capacitance inside the LED. (~5 to 20pF & the processor port adds 10pF) This technique requires the LED to connected directly to ProMini inputs because breadboards can add random amounts of changing capacitance. At these sub pF ranges, any humidity that condenses between the pins will also upset the readings, so desiccants are required. And finally the reverse bias decay is affected by the starting voltage, so if you want to use the technique in a rigorous ‘analytical’ setting you should leave the regulator on your logger.

We have integrated this LED sensor technique into the starter script on GitHub. I’ve tweaked the playground version with port commands so the loop execution takes about 100 clock cycles instead of the default of about 400 clock cycles.  The faster version was used to generate the following light exposure graph with a generic 5mm RGB LED, with a 4k7Ω limiter on the common ground.

Red, Green & Blue channel readings from the indicator LED  (from a regulated logger) over the course of one day (logger photo above)  The yellow line is from an LDR sensor the same unit, that was over-sampled to 16-bit resolution. The LED sensor has a logarithmic response and the left axis on the graph is a time- based measurement where more light hitting the LED sensor results in a lower number. Note how the RED signal changes before/after Blue & Green at sunrise & sunset.  LED’s work well with natural full-spectrum light, but their limited frequency bands can give you trouble with the odd spectral distribution of indoor light sources. The peak response of LED’s is usually 30–50nm lower than their peak emission wavelength If we assume the Red was Aluminum gallium arsenide (AlGaAs) then that channel probably had an absorption band @ ~680 nm (~15 nm FWHM?)  while the blue Indium gallium nitride (InGaN) channel is responding in the UV-A range, the Green channel (probably also InGaN ?) is most likely peaking around 420nm which is blue to our eyes. But without a spectrometer to test, these are just guesses. No temp. or cosine corrections were determined/applied, although blue/green channels tend to have low temperature coefficients because their bandgap is so far from the thermal spectrum. LED absorption bands have very little drift over time.

You can read more about LED based sensing techniques in the post about our leaf testing experiments which used two LEDs for a transmission-based variant of the NDVI ratio.

While the LED sensor idea is fun to work with, it’s a relatively slow method that can keep the logger running for many seconds when light levels are low. Figuring out how to take those light readings only during the day is a good coding exercise for students.

Note: VERY FEW light sensors can withstand exposure to direct sunlight. PTFE is an excellent light diffusing material which available in different sheet thickness.  The ‘divot’ on the lid of the Plano box is just a bit larger than 55mm x 130mm x 3mm (depth). The “teflon” tape that plumbers use to seal threaded joints can also be used. PTFE introduces fewer absorbance artifacts than other DIY diffusers like ping-pong balls, thermoplastic, or hot melt glue. Most light sensors like the TSL2561 need 3-5mm of that PTFE sheeting to prevent the sensors from saturating in full sun. LED’s have logarithmic response so you lose quite a bit of detail above 40,000 lux unless you add a diffusion layer to attenuate the signal.

Full sun exposure can also cook your logger. Internal temps above 80°C may cause batteries to leak or damage the SD card.  So if you are leaving the logger in full sun, add a bit of reflective film or some aluminum foil around the outside to protect the electronics. Of course if you have a light sensor you’ll need to leave some ‘window area’ for it to take a reading. 

The RTC has a built-in temperature register which automatically gets saved with our starter script however that record only resolves 0.25°C, so we’ve also added support for the DS18b20 temperature sensor to the base code. A genuine DS18b20 (yes, fake sensors are a thing) draws very little power between readings and you can add many DS18b’s to the same logger.

Addendum: Diagnosing Connection Problems

If you successfully loaded the blink sketch to test the ProMini during your initial assembly, then issues during the testing stage are often due to incomplete connections to the I/O pins.

If you see only “Scanning I2C….. ” but nothing else appears when running the bus scanner, then it means that the ProMini can not establish communication with the RTC module. One common cause of this problem is that the white & yellow wires have been switched around at one end or the other. It’s also easy to not quite remove enough insulation from the wires to provide a good electrical connection under the screw terminals, so undo those connection and check that the wires were stripped, cleaned & wrapped together before being put under the terminals.

Scanner lockup can also happen if one of the I2C devices on the bus is simply not working: usually about 1 in 6 logger builds ends up with some bad component that you have to identify by process of elimination. (These are 99¢ parts from eBay…right?) It only takes a moment to swap in a new RTC board via the black Dupont connector and re-run the scan. If the replacement RTC also does not show up with the I2C scanner then it’s likely that one of the four bus lines does not provide a complete connection between the ProMini & the RTC module.

On this unit I measured 1 ohm of resistance on the I2C clock line between the ProMini A5 pin (on top of the board) and the SCL header pin on the RTC module. So this electrical connection path is good. It’s not unusual for each ‘dry’ connection to add 0.5-1 ohm of resistance to a signal path.

To diagnose: Unplug any power sources to the logger. Set a multi-meter to measure resistance and put one probe lead on the topmost point of the promini header pins, and the other probe on the corresponding header pin of the RTC module. If there is a continuous electrical connection between the two points then the meter should read one ohm or less. Higher resistances mean that you don’t have a good electrical path between those points even if they look connected:

1) the ground (black) wire should provide a continuous path from the ground pin on the digital side of the Promini board to the GND pin on the RTC module
2) the positive power (red) wire should provide a continuous path from the Promini positive rail pin (the one with the bundle of 4 red wires) to the VCC pin on the RTC
3) A4 (I2C data) near the 328P chip on the Promini must connect all the way through the screw terminal board and through the white Dupont wires to the SDA post on the RTC
4) A5 (I2C clock) nearest the UART end on the Promini must connect through through the yellow Dupont wire to the SCL header on the RTC .

You occasionally get a bad Dupont wire where the silver metal end is not in contact with the  copper wire inside because the crimp ‘wings’ did not fold properly. With a pair of tweezers, you can ‘gently’ lift the little plastic tab on the black shrouds holding the female Dupont ends in place, and then replace any single bad wire. Be careful not to break the little black tab or you will have to replace the entire shroud.

Everyone uses short male-to-male Dupont jumper wires when they are creating test circuits because they are so convenient. But pre-made jumper wires are usually too long and so they get knocked around when you close the lid of the logger: so before you deploy take the time to convert your flexible-wire test circuit into one with solid core jumpers:

Flexible wires get used during the initial testing stages (when the lid of the logger is open).

Then the circuit gets re-done with solid core wires

Running wires ‘under’ the modules  makes it easier to close the lid  without disturbing the connections.

Also look at the little jumpers used to bridge the A4>A2 and A5>A3. If you have a ‘cold’ solder join, or an accidental bridge connection to something else, it could stop the bus from working. Re-melt each connection point one at a time, holding the iron long enough to make sure the solder melts into a nice ‘liquid flow’ shape for each solder point.

The connection diagnosis procedures described above also apply to the connections for the SD adapter board. Sometimes you end up with an adapter that has a defective spring contact inside the SD module, but the only way to figure that out is to swap it with another one.

Here a jumper wire from the ProMini pin is by-passing a bad connection on the screw terminal board.  This is also how you would break out A6 & A7 if you need them.

Sometimes those screw terminal boards have a poor connection inside the black female headers below the ProMini. It’s also possible to accidentally over-tighten a terminal and ‘crack’ the solder connection below the board – or there may simply be a cold solder joint on one of the terminal posts. If you have only one bad connection, you can jumper from the ProMini header pins on top, down to the other wires under the corresponding screw terminal. If you accidentally strip the threads on a screw terminal, you can use this same approach but move that set of wires over to one of the three ‘unused’ screw terminals at the far end of the board. (beside the SD card adapter) If you’ve gotten through all of the above steps and still have not fixed the problem, then it might be time to simply rebuild the logger with a different screw terminal adapter board.


If you do accidentally kill the ProMini by shorting a pin, etc, you can carefully lever it up away from the screw terminal shield and replace it without having to rebuild the whole logger.

Build two loggers at a time, because that lets you determine whether problems are code related (which will affect both machines the same way) or hardware related. (which will only affect one of your two units) At any given time I usually have 2-3 units running overnight tests so that I can compare the effect of two different code/hardware changes the next morning.  As a general rule you want to run a new build for at least a week before deploying to get beyond any ‘infant mortality’, and reach the good part of the bathtub curve.



An I2C OLED is quite readable through the lid of the housing. I often use Griemans text-only SSD1306Ascii library because it has a low memory footprint and sleeps well. While few loggers need live output when they are deployed, it’s often helpful to view diagnostic messages on battery power during testing. Adding two OLED displays let’s you view text & graphic output at the same time.  Adding a capacitive touch switch lets you check your logger’s status at any time.

Addendum:  A note about I2C sensors
The I²C bus is slow, so topology (star, daisy-chain, etc.) doesn’t matter much, but capacitance does. Both length & number of sensors increase capacitance. If you find that the devices work when you switch to a slower speed (e.g. 50 kHz), then this is probably your issue, and you need to minimize bus length and/or maybe decrease the combined resistance of the pull-ups to 2 kΩ or less. The DS3231 RTC module has 4k7 ohm pull-up resistors on the SDA & SCL lines & the Pro Mini adds internal 50k pull ups when the wire library is enabled. Typical I2C sensor modules usually add another set of 10k pullups so your ‘net pullup resistance’ on the I2C bus wires is usually:  50k // 4k7 // 10k = ~3k. With a 3.3v rail that means the devices draw 3.3v / 3k = 1 mA during communication which is fairly normal ( 3mA is max allowed) for total wire lengths below 1m. It’s common for pre-packaged sensors to arrive with housings at the end of about 1m of wire. If each sensor also adds another set of 10k pullups, the resistance generally compensates for the extra wire length, so the combination still works OK. But that depends on the cable too. A very bad cable might not even get to 0.5 meters and a very good cable (little capacitance to ground, no crosstalk between the wires) can go up to 6 meters.

For most sensor types there will be some options that draw much less power than others, and it’s always worth a look at the data sheet to make sure you are using one that will run longer.  The best chip based sensors automatically go into low current modes whenever the bus has been inactive, but more often you need to ‘manually’ put the sensors to sleep via specific commands. So it’s also important to check if your sensor library supports those ‘go to sleep’ & ‘wake up’ commands –  many common Arduino libraries do not.

Addendum:  The importance of moisture protection

I was noodling around in the garden recently and installed a few loggers without desiccants because it was only a short experiment. It rained immediately afterward and I noticed a small amount of moisture condensed inside the plano-box housing. While this didn’t prevent the logger from functioning, it completely disrupted the LED light sensors because the increased humidity provided an alternate discharge path for the reverse bias charge  on the LED’s:

Green channel data from a 5mm diffused RGB LED used as light level sensor. This logger was under some leaf cover, so there was considerable variability from the dappled light crossing over the sensor. An arbitrary cutoff of 200,000 was set in the code at low light levels.

After examining the O-ring I decided to add a little silicone to the channel holding the o-ring to improve the seal:

Gently pry the O-ring loose and apply sealant in the groove before replacing.

Bead only needs to be 3-4mm in diameter.

Close the housing & let the sealant set for a few days. The improved seal is especially visible at the corners

If you already have your logger assembled, try to find a silicone sealant that does not off-gas acetic acid (smells like vinegar) which could harm your circuits. If you are simply preparing empty boxes before assembly, then any regular bathroom sealant will do provided you give it about a week to finish curing.

Attach a mounting base to the lid so that a dessicant pack can be secured above the battery holder without interfering with any breadboard jumpers. Use a desiccant pack with color indicator beads, so you can check whether they are still working simply by looking through the transparent lid.


Addendum:  If you want to leave the original regulator in place

It’s worth mentioning that an unregulated build will run for many months – even on 2x regular alkaline batteries which reach the system cutoff (at 2750mv) more quickly. The key deciding factor is whether your sensors require tight voltage regulation. The DS18b20 has a nominal low voltage limit of 3v.  So if your project is making heavy use of those then there are only a few of modifications to the tutorial shown above to leave the ProMini’s default MIC5205 regulator in place:

Use straight header pins on the RTC modules cascade port to leave more space for the battery holder.

Only bridge the unused RST terminals to the rail connections Leave the Vin terminal separate for the raw battery input.


Add a 10/3.3 Meg voltage divider to read the raw battery voltage on A0

You will need more space for the extra batteries. You could go with a 3xAA holder but that leaves about 50% or your alkaline battery capacity unused. Or you could keep the standard layout and use 4xAAA batteries.

An alternative would be to add a better regulator to some kind of intermediate battery connector. The the photo on the right shows two ceramic 105’s stabilizing an MCP1702-3302E/TO, while the 10/3.3M ohm divider provides a third output  line so the ADC channel can monitor the raw battery voltage. This is the simplest way to retro-fit a unit that was built without a reg, with the added benefit that the new regulator is far more efficient than the original MIC5205 on the ProMini. It’s worth noting that even on a regulated logger you can monitor the rail voltage to determine when the main batteries are depleted because the regulators output will fall if the batteries reach a point below the minimum dropout voltage. If the rail falls under load by more than ~40mv, then it’s probably time to shut down the logger. With the regulator in place you probably don’t need the USB isolator, as the reg. itself cant pass more current than a USB port.

Addendum:  Things to keep in mind when ordering parts

When a finished module arrives at your doorstep for less than you’d pay for any of its sub-components – it’s because you are doing the quality control.

My advice is to order at least 5-6 of each of the core components (Promini, RTC, SD module, screw terminal board, etc) with the expectation that about 10% of any cheap eBay modules will be DOA or have some other problem. I build in batches of six, and one logger typically ends up with a bad part somewhere. Having replacement bits on hand is your #1 way to diagnose and fix these issues. Bad parts tend to come “in bunches”, so if you scale up to ordering in quantities of 10’s & 20’s then spread those orders to a few different suppliers so you don’t end up with all your parts from the same flakey grey market production run. Order from different vendors in different odd-number quantities (11, 21, 9, etc.) because that will be the only way you can distinguish which supplier, sent which parts, because nothing on the package will be written in English.

The other thing I can’t stress enough is CLEAN ALL THE PARTS as soon as they arrive. Leftover flux is very hygroscopic, and solder points will start to corrode the moment your logger gets exposed to atmospheric moisture. I usually give everything about 10 minutes in a cheap sonic bath with 90% isopropyl alcohol, rinse with water, and then dry the parts out in front of a strong fan for an hour. Clean parts that can’t take the sonic vibration (RTC modules, humidity sensors, accelerometers, etc) by hand with a cotton swab. Then store parts in a sealed container with desiccant packs till you need them.  I also coat the non-sensing/non-contact surfaces with a layer of MG Chemicals 422B Silicone Conformal Coating and let that dry for a day before assembling the loggers.  One hint that you may have moisture issues is that the sensors seem to run fine during indoor tests  but start to act strangely when you deploy the unit outside.

Used nut containers make excellent “dry storage” once the parts have been cleaned – but any air-tight container will do.

Another insight I can offer is that the quality of a sensor component is often related to the current it draws – if your ‘cheap module’ is pulling significantly more power than the data sheet indicates, then theres a good chance it’s a junk part. Usually if the sleep current is near spec, then the sensor is probably going to work. It is much easier to check low currents with a µCurrent or a Current Ranger. (I prefer the CR for it’s auto-ranging features) Sensors which automatically go into low current sleep modes take time – so you might need to watch the  current for several seconds before they enter their quiescent states. A common reason for a short operating lifespan on a logger is an SD card that refuses to go into sleep mode. If there is an SD card connected to your logger you must initialize it (with sd.begin in setup) or it may ‘stay awake’ causing a constant 30-40mA drain and/or may even cause the logger to freeze up. Also with SD cards, if the freshly formatted throughput drops below its rated write speeds when tested with H2testw, then find another card to use. I avoid cards bigger than 2Gb because they usually draw too much current, and it’s rare to need that much space for a logger.

With cheap part variation & beginner soldering skills, student builds range from 0.15mA to .5mA sleep current. But even at that high end you should still get a couple of months of operation of from the logger on fresh batteries.

Bill of Materials: ~$22.00
Plano 3440-10 Waterproof Stowaway Box
Sometimes cheaper at Amazon. $4.96 at Walmart and there are a selection of larger size boxes in the series. 6″ Husky storage bins are an alternate option.
Pro Mini Style clone 3.3v 8mHz
Get the ones with A6 & A7 broken out at the back edge of the board. Just make sure its the 3.3v version because you can’t direct-connect the SD cards to a 5v board.
‘Pre-assembled’ Nano V1.O Screw Terminal Expansion Board
by Deek Robot, Keyes, & Gravitech (CHECK: some of them have the GND terminals interconnected)  You will also need to have a few small flat head screw drivers to tighten those terminals down.  Since this shield is was originally designed for an Arduino Nano many of the labels on ST board will not agree with the pins on the ‘analog side’ of the ProMini.
DS3231 IIC RTC with 4K AT24C32 EEprom (zs-042)
Some ship with CR2032 batteries already installed.  These will pop if you don’t disable the charging circuit!  
CR2032 lithium battery  $0.40
SPI Mini SD card Module for Arduino AVR
Buy the ones with four ‘separate’ pull-up resistors so that you can remove three of them.
SD card 256mb -to-1Gb 
 Test used cards from eBay before putting them in service. Older Nokia 256 & 512mb cards have lower write currents in the 50-75mA range. This is less than half the current draw on most cards 1gb or larger. I tend to avoid older cards labeled as ‘TransFlash’ because they seem to have more controller artifacts during saves. Small 128mb & 256mb cards under the name Cloudisk have appeared on eBay, and so far they seem to be working ok.
Small White 170 Tie-Points Prototype Breadboard
These mini breadboards for inside the logger are also available in other colors.
20cm Dupont 2.54mm M2F 40wire ribbon cable
Dupont connector hook-up wires might be expected to add an ohm or two of resistance and carry at most 100mA reliably with their thin 28-30 gauge wires.  Each 40-wire cable will let you make at least 2 loggers.
10cm Dupont 2.54mm M2F ribbon cable
Sometimes these 10cm cables are harder to find, so you can just use the longer 20cm wires in a pinch.  It’s usually also helpful to have a few Male-to-Male 10cm cables for interconnections on the breadboard.
2×1.5V AA Battery Batteries Holder w Wire Leads
If you are running an unregulated system on 2 lithium batteries, then you can use a 2x AA battery holder. If you need to keep the regulator in place to stabilize the rail voltage for particularly picky sensors, use alkaline batteries and a 4xAA battery holder. Watch out for ‘cheap’ battery holders with weak plastic at the connection ends which will slowly bend out of shape until the batteries pop out in warmer climate.
5mm Common cathode RGB LED
Although you might want to use 10mm LEDs to increase surface area when using the LED as a light sensor.
M12 Nylon Cable Glands (pack of 20 pcs) 
You will also need some extra rubber washers.
$0.70 /2pcs
3.3V FT232 UART Module (get at least 2-3 modules – they are easy kill with a brief short)
 *jumper the pads on your UART module to 3.3v output before using it!* You will also need a few USB 2.0 A Male to Mini B cables. You may need to install drivers from the FTDI website depending on your OS. These boards can only supply ~50mA which can be tricky if your sensors need more for sustained periods. If you are running the class via distance learning it’s probably a good idea to also get some CP2102 (c231932) UART boards and send your students one of each type. If they are unable to get the drivers working for the FT232, they have a second option. You may have to hunt around for non-FTDI chip boards with the same pin order as the ProMini [ DTR-RX-TX-3v3-CTS/gnd-GND ]  The DTR pin is critical for uploading code, while the CTS (clear to send) is an input pin for the FTDI chip only and CTS is not used by the ProMini (so it’s usually just tied to ground).  So many UART adapters only have 5 connections and you have to cross the wires over each other to get the connections sorted out.  Watch out for 6-pin UART modules that put a (+)ive power connection in the same physical alignment as the GND connection on the ProMini  –  those boards can create a short circuit unless you re-route the wires. It’s also worth knowing that UARTs can communicate directly to serial sensors like GPS modules for testing.
3M Double-side Foam Tape, LEDs, header pins, 3/4 inch zip Tie Mounts, etc…
I use 30lb ‘outdoor’ or VHB (high bond) foam tape, each logger takes ~25cm length
Some extra tools you may need to get started:                (not included in the total above)
2in1 862D+ Soldering Iron & Hot Air station Combination
a combination unit which you can sometimes find as low as $40 on eBay.
Or you can get the Yihua 936 soldering iron alone for about $25. While the Yihua is a so-so iron, replacement handles and soldering tips cost very little, and that’s very important in a classroom situation where you can count on replacing at least 1-2 tips per student, per course, because they let them run dry till they oxidize and won’t hold solder any more.  Smaller hand-held heat-shrink guns are available for ~$15, $10 80Watt-AC &  $5 USB soldering irons are quite useable.
$15.00 – $50.00
SYB-46 270 breadboards (used ONLY for soldering header pins )
Soldering the header pins on the pro-mini is MUCH easier if you use a scrap breadboard to hold everything in place while you work. I use white plastic breadboards that only have one power rail on the side since I won’t mistake them for my regular breadboards. I also write ‘for soldering only’ on them with a black marker.
SN-01BM Crimp Plier Tool 2.0mm 2.54mm 28-20 AWG Crimper Dupont JST
I use my crimping pliers almost as often as my soldering iron –  usually to add male pins to component lead wires for connection on a breadboard. But making good crimp ends takes some practice.  But once you get the hang of it,  Jumper wires that you make yourself are always better quality than the cheap premade ones.
Micro SD TF Flash Memory Card Reader
Get several, as these things are lost easily. My preferred model at the moment is the SanDisk MobileMate SD+ SDDR-103 or 104 which can usually be found on the ‘bay for ~$6.
Side Shear Flush Wire Cutters & Precision Wire Stripper AWG 30-20
HAKKO is the brand name I use most often for these, but there are much cheaper versions.
Dt380 Multimeter
Dirt Cheap & good enough for most classroom uses.
Syba SY-ACC65018 Precision Screwdriver Set
A good precision screwdriver set makes it so much easier to work with the screw terminal boards. But there are many cheaper options.
Donation to Arduino.cc
If you don’t use a ‘real’ Pro Mini from Sparkfun to build your logger, you should at least consider sending a buck or two back to the mother-ship to keep the open source hardware movement going…

.. and the required lithium AA batteries are also somewhat expensive, so a realistic estimate is about $25-30 for each logger when you add a couple of sensors. Expect parts from low-end suppliers to take 4-6 weeks to arrive and always order at least 50% more than you actually need so you have replacement spares. If you’re pressed for time everything on this list is also available from trusted first-tier suppliers like Sparkfun, Adafruit, Pololu, etc – but you will pay 5-10x as much, with an additional $10-15 shipping charge unless you pass the minimum order level. Amazon is now in a kind of weird grey zone between the two as many vendors that sell on eBay, are also selling on Amazon for 2-3x the price. 

Addendum:  Using a more advanced processor

Moteino MEGA based Cave Pearl Logger

After you’ve built a few ProMini based loggers, you might want to try a processor upgrade. The 1284p CPU has twice the speed & 4x the memory, but delivers comparable sleep current & operating life.




Addendum:  Low Temp. effects on 2x Lithium powered logger

2x LithiumAA millivolts (blue-left) vs RTC Temp °C (orange-right) on cells that have been in service for 5 months. We will leave this unit running over winter to see how DS18b20 on that logger handles it if/when the cells fall below the DS18’s 3V minimum, and then rise back up again. (click image to enlarge)

A crop of these loggers have been running in our back-yard garden since mid-summer with various sensor combinations. Winter is finally reaching us so we can now observe how the cold affects an unregulated 2x Lithium AA supply.  This ‘student build’ sleeps at ~170uA and has been running for five months. The battery curve was virtually flat above 15°C but it is now being quite strongly affected. Peak loads from the SD card are in the range of about 150mA and the unit is running with a 5 minute sampling interval.

Addendum:  Adding a TTP233 Capacitive Switch lets you check your logger ‘any time’

With a capacitive touch switch that works through the housing, you can check the status of your logger at any time.

Our next tutorial post in the student logger series: Enhance your Logger with an OLED & T233 Capacitive Touch Switch  is an excellent ‘next step’ for people using this logger in a classroom setting. The method is easily adapted to trigger ‘opportunistic’ readings in environments that require manual control, but it’s also handy when you need to check the battery level on a complex installation that you don’t want to disturb before the end of the experiment.

Burst Sampling with the ADS1115 in Continuous Mode

Testing configuration w differential reads from a piezo triggering burst-samples with a $1 ADS1115 module.

The 16-bit ADS1115 has a programmable amplifier at the front end, with the highest gain setting providing a range of +/- 0.256 v and a resolution of about 8 micro volts.  But readers of this blog know you can already approach 14-16 bit sensitivity levels with Arduino’s ADC by oversampling with lower Arefs & scaled ranges. PA1EJO demonstrated a ADS1115 / thermistor combination which resolved 5 milli Kelvin, but we reach that resolution on our NTC’s using the ICU peripheral with no ADC at all. The beauty of time-based methods is that they scale gracefully with sensors that change over a huge range of values.  So why am I noodling around with this ADC module?

The primary attraction is this ADC has differential inputs. This is especially useful with Wheatstone bridge arrangements. A typical combination would be a two element varying bridge, and an inexpensive voltage reference like the LM4040, or the TL431. Adding the second sensor doubles the voltage swing, and the ADC’s  -32768 to +32767 raw output fits perfectly into Arduino’s 16-bit integer variables. It’s also worth noting that unlike most ADC’s – the ‘volts per bit’ via the gain settings are independent of the rail voltage supplying the chip. This means that the ADS1115 can measure it’s own supply using that internal reference without a divider. The drawback of that is I have to set full-scale as +/-4.096V on my 3.3v loggers, so the ADC only uses ~80% of the bit-range.   

Read the difference between A0 and A1 as a differential input, and read A2 as a single-ended input. That will give you an ‘almost‘ ratio-metric measurement because you record every voltage affecting the output. (although since the supply is not Aref like it would be in a regular ADC – the uncorrelated noise in the LM4040 excitation voltage ‘between’ those two readings will not get corrected)  I treat the ref. resistors as ‘perfect’ in my calculations, which forces their tempco errors into the thermistor constants during calibration.

The diodes shunt any voltages that exceed their vF, protecting the ADC from spikes beyond the +/-0.256v range at high gain. AND the leakage that Shottky’s are known for bleeds away residual charge on the piezo, preventing drift from the 1.65v bias point. All the data presented in this post used this circuit. Other diodes, or even garden variety LED’s, could also be used to clip the signal at different voltages. Most have far less leakage than Shottkys, so you might need to add a large value bleed resistor. If you do end up with an offset, an old trick is to run a very aggressive low pass filter on your readings to obtain the offset and then remove it by  subtraction.

Piezos can also be read with bridge arrangements if they are physically connected with alternating polarities, but that’s not usually the case for active sensors. I have a new project in the works where the sensor will only generate +/- 5mv, and I’d like to see if capturing signals that small is even possible with the ADS1115. To reveal where the weak points are I’ll test it with a single piezo disk reading at the highest gain and fastest sample rates. At this sensitivity a 5mv swing will only produce ~640 counts.  With my signal only covering 2% of the bit range, I’m hoping that the differential readings will compensate (?) for  noise on the rails. The data sheet warns about low input impedance (710kΩ) but I don’t think that will affect this test. Another significant limitation of the ADS1115 is that, like the Arduino driving it, no voltages below GND, or above Vcc are allowed on any input. Bridge arrangements automatically bias to the mid-point, so while the tie-in points might go ‘negative’ relative to each other, they are still positive relative to GND on the ADC.  For single sensor applications with +/- output, you need to provide that biasing with a couple of resistors.

An often overlooked feature of the ADS is the programmable comparator which can set threshold alarms on the ALRT/RDY output. Most loggers operate with fixed interval sampling, but this makes it difficult to measure things like airborne exposure peaks for chemical vapors; even with short intervals. Sensor-triggered sampling can also save battery power by letting you sleep the CPU – especially when you are monitoring environments that are only ‘active’ during certain seasons, or with rainfall events. The different comparator modes on the ADS1115 also offer some interesting possibilities for system control.

Driving the ADS1115:

This chip’s been around for a long time, so there are several libraries to choose from. And, as usual, many of them don’t support the features that a project building loggers would be most interested in. I suspect this is because wireless coms use such a prodigious amount of power that few in the IOT crowd would bother writing efficient code for chips that already auto-sleep. The Adafruit library even inserts 8ms delay statements – wasting cpu power and throttling the sample rate to 125sps. Rowberg’s I2Cdevlib does a better job with setConversionReadyPinMode() functions. But his code example only polls the I/O status, rather than using the hardware interrupts available on the same pin.

Perhaps the easiest starting point for beginners is the ADS1115 lite library This is a stripped down version of Adafruit’s lib. but Myers has removed the explicit delays and replaced them with a do-while loop which polls the Operational Status bit to see when the ADC has a new reading in the output registers. This minimalist approach uses only two main functions:

triggerConversion() – Sets config register bits  & then writes that register back to the sensor (which automatically starts a single-shot reading) The ADS1115 auto-sleeps after the reading.

getConversion() – A do-while loop forces the CPU to continuously check the Operational Status bit. That bit change breaks the loop, and getConversion then reads the 16-bit output register.

With this single-shot approach, a short for-loop takes burst of readings:

#include <ADS1115_lite.h>    // https://github.com/terryjmyers/ADS1115-Lite
#define numberOfSamples 500
int16_t ADS1115raw[numberOfSamples];

setMux(ADS1115_REG_CONFIG_MUX_DIFF_0_1);          // uses #define statements
setSampleRate(ADS1115_REG_CONFIG_DR_860SPS);  // for the config bitmasks from
setGain(ADS1115_REG_CONFIG_PGA_0_256V);             // the original Adafruit library

for (int i = 0; i < numberOfSamples ; i++) {     //  I usually read 500 samples
triggerConversion();                                           //  during testing, which fills the
ADS1115raw[i] = getConversion();                   //  the serial plotter window nicely

In single shot mode, you have to re-write the configuration register every time you want a reading:

Myers triggerConversion() function sets the config register with a common Bitwise-OR method. I’m going use this as a starting point, tweaking a few things for better readability and including my standard a 16-bit register functions so this page doesn’t scroll on forever.
(also note that in addition to my typos, wordpress inserts a ton of invisible cruft characters that will mess with the IDE – so don’t copy/paste directly from this post…)

uint16_t config = 0;   //  All bits set to 0
config |= _rate;   config |= _gain;   config |= _mux;    // sets matching bits to 1
bitSet(config, 8);      // MODE set to 1 = single-shot & power-down  (the default)
bitSet(config, 15);    // setting oneshot bit starts a conversion, bit goes low when done
i2c_write16bitRegister(ADS1115address, ADS1115_REG_POINTER_CONFIG, config);

Sensor Triggered Sampling:

Let’s use the comparator to start each burst of readings in response to me tapping the surface of the desk that the piezo sensor is resting on. Although Myers polling method doesn’t use the ADC’s ALERT/RDY output, we are already set up for triggered bursts because all the comparator control bits were zeroed with config = 0 at the start. 

COMP_MODE: 0    =>   Traditional hysteresis mode (On above Hi_thresh, Off below Lo_thresh)
COMP_POL:  0       =>   ALERT active brings the pin LOW
COMP_LAT: 0         =>   NON latching
COMP_QUE: 00     =>   ALERT after one reading above Hi_thresh  (01=2reads, 10=4reads)

With this as the starting point all you have to do to initiate comparator threshold alerts is load some non-default trigger values into the Hi_thresh & Lo_thresh registers.  Hi_thresh must be greater than Lo_thresh, and you have to use ‘2s complement’ values

// set Lo_threshold register (0x02) to ‘2’s complement’ equivalent of decimal 250:  
i2c_write16bitRegister(ADS1115address,  0x02,  0x00FA);
// set Hi_threshold register (0x03) to equivalent of decimal 300:
i2c_write16bitRegister(ADS1115address,  0x03,  0x012C);

Now we need a way to make the processor respond the ADC’s alert. If I wanted to use the same power wasting methods you find in most sensor libraries, I’d connect ALRT/RDY  from the ADC to any digital input pin, and poll the pin until it goes low:  

void pollAlertReadyPin() {   // this code will time out eventually
for (uint32_t i = 0; i<100000; i++) {
if (!digitalRead(AlertReadyPin)) return; }
Serial.println(“Timeout waiting for AlertReadyPin, it’s stuck high!”);

This might be OK for an IOT sensor hanging off of a wall-wart. But for logging applications a hardware interrupt based approach lets you save power by sleeping the processor until the trigger event happens:

void INT1pin_triggered() {
INT1_Flag = true;

// – – – – – – later on – – – – – – – – in the main loop – – – – – – – – – – –
config = 0;   //  All bits set to 0
config |=  ADS1115_REG_CONFIG_MUX_DIFF_0_1 ; // using #defines from Adafruit lib
config |=  ADS1115_REG_CONFIG_DR_475SPS ;
config |=  ADS1115_REG_CONFIG_PGA_0_256V ;

bitClear(config, 8);   // MODE set to zero = continuous sampling – redundant here
i2c_write16bitRegister(ADS1115address, ADS1115_REG_POINTER_CONFIG, config);

i2c_write16bitRegister(ADS1115address,  0x02,  0x00FA); // set Lo_thresh = 250
i2c_write16bitRegister(ADS1115address,  0x03,  0x012C);  // set Hi_thresh = 300

// ALRT/RDY output from ADC is connected to the hardware INT1 pin
bitSet(EIFR,INTF1);    //  clear pre-existing system flags on INT1 pin
noInterrupts ();  
INT1_Flag = false;     //  reset the flag before the do loop

    do {       // loop keeps the processor asleep until INT1pin_Flag = true
          interrupts ();   
          sleep_cpu ();   
          noInterrupts ();    
    } while ( !INT1_Flag );   
sleep_disable ();
interrupts ();

// after waking, reset the threshold registers to their defaults to disable the ALERTs
i2c_write16bitRegister(ADS1115address,0x02,0x8000);// Lo_thresh default = 8000h
i2c_write16bitRegister(ADS1115address,0x03,0x7FFF);// Hi_thresh default = 7FFFh

// now gather single-shot readings as before
for (int i = 0; i < numberOfSamples ; i++) {
triggerConversion();    //  resets the config register to single shot mode every cycle
ADS1115raw[i] = getConversion();    

With 200 / 300 set as thresholds, tapping the desk beside the piezo produced:

RAW ADC output vs Sample Number: Threshold triggered: A0-A1 differential, 475SPS, 16x PGA,500 samples

With readings above 2000, I was hitting the desk a bit too hard for that 5mv target range. And 475 samples-per-second is not quite fast enough to show piezo sensor behavior. Zooming in also shows that the ADC was aliasing through some background signal:

RAW ADC output vs Sample Number: ADS1115 & piezo sensor, A0-A1 differential, 475SPS, 16x PGA, 500 samples

That’s a classic ‘mains hum’ problem.  Annoying, but from a research perspective the loss of information from the start of the event was is a more of an issue: What happens if we only get one chance to record our event?

Pretriggered acquisition:

To capture infrequent events, I need to start acquiring data before the reference trigger. And since the waiting period is unknown, those readings need to go into a circular buffer that wraps around and stores each new sample over the oldest one in memory. With this approach the trigger event actually serves to stop the acquisition rather than to start it. And you want to do this gradually, so the samples in the array represent a “slice-in-time” covering the entire event.

The real trick is to sleep the main processor as much as possible during the pre-fetch period. In continuous conversion mode the ADS1115 can alert completion with an 8 msec pulse, but with only one alarm output, the ‘threshold detection’ will have to be done in software:

void INT1pin_triggered() {
INT1_Flag = true;

//  – – – – – – – – in the main loop – – – – – – – – – – –
config = 0;   //  All bits set to 0
config |=  ADS1115_REG_CONFIG_MUX_DIFF_0_1 ; // #defines from Adafruit lib
config |=  ADS1115_REG_CONFIG_DR_860SPS ;       // the max speed
config |=  ADS1115_REG_CONFIG_PGA_0_256V ;     // maximum gain

bitClear(config, 8);   // MODE set to zero = continuous sampling – redundant
i2c_write16bitRegister(ADS1115address, ADS1115_REG_POINTER_CONFIG, config);

// continuous mode 8ms ‘pulses’ require these specific values in the threshold registers:
i2c_write16bitRegister(ADS1115address,  0x02,  0x0000); //Lo_thresh MS bit must be 0
i2c_write16bitRegister(ADS1115address,  0x03,  0x8000);  //Hi_thresh  MS bit must be 1

// ALRT/RDY output from ADC is connected to the hardware INT1 pin
// housekeeping variables for sampling loop control:

triggerHasHappened = false
int countdown = numberOfSamples/2; // sets # of samples taken AFTER trigger event
int countup = 0; // If triggered before the array is 1/2 full, countup used to fill remaining
int arrayPointer =0; // tracks where we are in the circular buffer

// now loop forever till trigger event starts the countdown
// then collect  numberOfSamples/2  more readings
while ( countdown>0 ){  
  bitSet(EIFR,INTF1);   //  clear any pre-existing system flags on INT1 pin
  INT1_Flag = false;     //  reset the flag before the do loop

      do      // short sleeps while waiting for each ADC reading
          {  interrupts ();  sleep_cpu ();   noInterrupts ();  }
      while ( !INT1_Flag );   
      detachInterrupt(1);  sleep_disable ();   interrupts ();

// load one reading into the ADS1115raw array
ADS1115raw[arrayPointer] =
i2c_read16bitRegister (ADS1115address , ADS1115_REG_POINTER_CONVERT);

// here I’m using 200 as the threshold reading to start the countdown
(( ADS1115raw [ arrayPointer ] > 200) && ( !triggerHasHappened ) ){
triggerHasHappened = true //only needs to occur once
if (countup < (numberOfSamples/2)){  // trigger happened b4 array was 1/2 full
 // increases countdown by the difference so you always capture numberOfSamples

if ( triggerHasHappened ){  //then only fill the last half of the array
countdown = countdown-1; // limits the number of new readings

// advance arrayPointer with ring-buffer MODULUS formula:
// automatically goes back to zero when the pointer reaches the end of the array

arrayPointer = (arrayPointer + 1) % numberOfSamples;

 // =====end of while ( countdown>0 ) loop======
sleep_disable ();

// reset the registers to startup defaults to stop ADC continuous running
//sets: ±2.048V,128SPS,NoCOMParator,AIN0&AIN1,Trad,NoLAT,NoALERTs,ActiveLOW
i2c_write16bitRegister(ADS1115address,0x02,0x8000);  // Lo_thresh default
i2c_write16bitRegister(ADS1115address,0x03,0x7FFF);   // Hi_thresh default
//read the ADC output registers to remove any residual ALERT/RDY latches

Setting countdown = numberOfSamples/2 centers the event in the array. (although 1/3 to 2/3 split might be better?) A tap on the desk with a pencil produced a +/- 5mv swing (~640 raw counts), and my breadboard proto circuit is picking up almost 100 counts of mains hum. 

RAW ADC output vs Sample #: 860sps, 16xPGA, diff. A0-A1, USB powered from laptop

Losing 20% of my available signal range to background cruft is a problem. Adding a $10 USB isolator, reduced that by about 1/3. But the 60 Hz signal was still distorting the shape of the waveform significantly or we’d be seeing a smoother damped harmonic..

RAW ADC output vs Sample #: 860sps, 16xPGA, diff. A0-A1, with USB isolator. Hit the desk a bit to hard on this one.

My first thought was ‘This is a well behaved, repeating signal – I’ll just subtract it out’. Unfortunately 860 SPS is not a multiple of 60Hz, so simple corrections tend to pass in & out of phase with the hum – eventually making the situation worse.  The misalignment means we’d need to do some serious math for the correction at 860SPS, so I’m probably not going to be implementing that filtering on an 8-bit processor. Alternatively I could go back to single shot sampling and use the processors internal timers to only request each new sample at some  whole number multiple of the 60 Hz mains cycles, like for example at 600 readings a second. The maximum possible would be 840, and you’d might want to add some jitter to that.

Next I tried a run from batteries only, with the nearby electrical devices all turned off. This reduced the mains hum by ~10x relative to the USB tethered operation:

RAW ADC output vs Sample #: 860sps, 16xpga, differential A0-A1, Battery powered logger

A dramatic improvement, with the ‘pre-event’ noise falling below +/- 10 counts. Most of our field deployments are in caves and this ADC looks like it has an acceptable noise floor for work in that kind of isolated environment . But the project also has a teaching component so I’d also like to use this ADC module in classroom settings.  Zooming in on that last graphs shows that working with tiny sensor signals will be challenging if we are anywhere inside a building – even if the resting state of the system looks OK:

Once the sensor is set in motion, even tiny interferences from the mains will reinforce each other before the system settles again. Even if I use internal timing control to synchronize the readings with a whole number multiple of the mains, it looks like I still won’t be able to use the ‘before’ data to fully correct the ‘after’ effects. This might be specific to way piezo sensor’s resonate, but I’ve got some homework to characterize the effect before we start building a student lab with this module.

Looking in the bright side, even with a power hungry 1284p based logger, the current draw while capturing the pre-event readings averaged less than ~450 μA for the whole system.

The path forward:

The successor to the ADS1115 is the 24-bit ADS1219 which reads up to 1000 SPS (20 Effective Bits, PGA x4). It has integrated input buffers to allow measurement of high impedance inputs & separate inputs for analog vref (true ratiometric!) and digital power/ground. This gives you more options to mitigate power supply noise, which as we’ve seen can be important for small signals. It also offers some built in 50-Hz and 60-Hz rejection, but only at slow sample rates. The ADS1115 is a delta-sigma converter so it continuously samples its inputs (oversampling @250kHz internally) which causes a transfer of charge. From the outside, this appears as a resistance which depends on the sampling rate and depends on the gain of the input stage. Higher gain for more sensitivity yields lower effective input resistance which adds in parallel to the resistance of your sensor circuit. So if you were reading the output of a voltage divider (equivalent) the ADS1115 itself would change the result. So input buffers on the ADS1219 are a welcome addition.

The low input impedance of the ADS1115 can prevent you from using the higher gain settings in differential mode  unless you add an opamp/buffer to prevent the ADC from putting to much drain on the sensors output. This is really what separates the ADS from ‘Instrumentation quality ‘ components which generally have much higher input impedances.

There are other 24-bit options in the hobbyist market like the HX711 (24-bit, PGA x32,64,128 – but only x32 on the 2nd channel? ) that is commonly sold with load cells, and I’ve seen it mentioned that the SPI HX711 works with libraries written for the ADS123x series. The ADS1232  (24 bit, fixed x128 gain) might be a easier option for dedicated bridge sensors, and they can be found on eBay for ~$7. One advantage of the ADS123x over the HX711 is that they have a switch that can shut off current to the sensor bridge when the ADC is in Standby or PowerDown mode. Of course then you have the problem that load cells take some time to warm up when power is applied, often taking several minutes to stabilize.   You occasionally see seismometer projects using the 32-bit ADS1262, which has a sensitivity >1000x better than the 1115, but with a fairly slow sample rate.

This circuit from Gadget Reboot shows one method of obtaining programmable gain control using an X9C digital potentiometer in the opamp feedback loop. See: Part1 & Part2    The DS3502 gives you an I2C bus version with the same 10k range, though I have no idea what the long term stability of these digital pots is. And 5% tolerance is a bit grim.

But this little experiment has me wondering if for signals in the 1mV range it might be better to spend more effort amplifying rather than moving to higher resolution ADCs. If the real issues are going to be noise and drift, then those might be easier to deal with if the level is boosted first. Microphone preamps can be made from a single 2N3904 transistor and placed in front of a (200x) LM386 modules for less than 50¢ though I suspect there might be lots of distortion. A general purpose (100x) LM358 might do the job on its own, or a (1000x) INA333 or AD623 modules (with the trimpot) which can usually be had for less than $6, as can the AD8221AR. The INA129-HT gets you to 10,000x for ~$9.  What I’d really like is an amplifier with the same simplicity of the ADS1115’s PGA. If anyone knows of a cheap I2C/register controlled opamp module in the hobby market price range, I’d love to hear about it.

Addendum 2020-05-24:   Interrupt latency with wake from sleep

I just watched an interesting video about the sleeping the ESP32 processors and was quite surprised to find out how long (150 µS) and how variable the hardware interrupt latency is on these expressive processors. This set me down the rabbit hole to find out what the latency is on the AVR processors. On a normally running processor you enter the ISR in 23 clock cycles, which is about 1.5µS @16MHz. However if you loop through POWER_DOWN there are extra things to consider like the fact that disabling the BOD in software (just before sleep) is going to add 60 µS to your wake-up time. You also have an ‘oscillator stabilization period’ of 16k CPU cycles with a  standard external oscillator. The net result is that the Wake/Start-up time for a 8MHz Arduino is ~1.95ms.  AVR’s with 16MHz clocks like the one I used for this test should have a wake-up time of less than 1ms.  So I was actually cutting it close to combine full POWER_DOWN sleep & the ADS1115’s highest sampling rate. A 3.3v Pro Mini based build @8MHz would not have kept up unless I used SLEEP_MODE_IDLE to keep the main oscillator running which avoids that long stabilization delay.

Other projects using this ADC:

While I’m giving it a B rating for my current use case, this $1 ADC module is probably one of the best options once your signals get above above 10 mv.  The UNO/ADS1115 combo is a ready replacement for benchtop DAQs. Especially since you can add up to four of the modules on the same bus for a multi channel capability.  This build of InstESRE’s Pyranometer solders a PDB-C139 directly onto the ADS1115 module, and adds an analog TMP36 for temperature correction.

If you actually want mains signals in your data, then Open energy monitor has a project reading AC with the YHDC SCT-013-000 . Current sensors like that often make readings that are not referenced to ground so you have to use an ADC capable of differential readings. Although this project focuses getting the most out of cheap eBay modules, the ADS1115 repeatedly makes appearances alongside more pricey sensors like this DIY nitrox tester, and this rather impressive Air Quality Index (AQI) monitor from Ryan Kinnett; Those low power modules from Spec-Sensor look very interesting…

Building an ATmega 1284p based Data Logger

In this tutorial, a logger is built using a 3.3v Moteino MEGA with a 1284p CPU @ 16Mhz, w 4K eeprom,16K SRAM for variables & 128K program space. Considerably more than the 328’s 1K eeprom, 2K ram & 32K progmem. Also has a spare serial port for GPS/NEMA sensors.

In the 2018 paper we tried to convey that it doesn’t matter which processor you use with our system as long as it’s supported by the Arduino IDE. The ATmega family includes several CPUs more capable than the humble 328p in the Pro Mini / UNO. In fact, some have suggested that the 1284 would have been a better choice right from the start, with full code compatibility once you account for the different port / pin mapping.  We built several loggers around that chip early in the project, but at the time I was mostly just leaning on the extra memory to compensate for some fairly crude programming. As my skills improved, and I stopped using bloated sensor libraries, that problem went away.  So it’s been a while since I needed anything more than the 328p. But we have a new project spinning up this year that calls for burst sampling of high-bit ADC channels, and we’ll need more SRAM to juggle that data with some fairly large buffer-arrays.

Dave’s protoboard build

For those who build from scratch, Dave Cheney shows one approach to starting with the raw chip.  At the opposite end of the spectrum, Stroud Research Center’s Mayfly is a good combination if you’d prefer something ready-made.  But I still have a a few Moteino MEGAs lying around, and this project has always followed the ‘middle path’ to enlightenment.  So we’ll use one of those boards to demonstrate how easy it is to give our classroom logger a processor upgrade:

Add Screw Terminals to the Moteino:   (click images to enlarge)

Raspberry Pi expansion terminal boards are about $1 each, use 3mm pitch blocks from Nano kits. SPLIT Nano shields also work in a pinch, but you loose A4-A7 & D19-23

20 screw terminal ports per side, OR you could solder 2.54mm blocks directly to the MEGA. But they are not as robust for multiple reconnects during prototype experiments.

The Moteno MEGA from ‘just fits’ between the rails on the Raspberry Pi adapter. The ProMini XL fits on a Nano shield – but I like having all those extra I/O pins on the MEGA to play with.

Pins on the MEGA board need to be bent ‘slightly’ to align with the header holes on the Rpi shield.

10 / 3 Meg divider to track battery voltage

This is the basic version of the MEGA, but it’s also available with flash & radio transceiver options. LowPowerLabs also sells the Current Rangeran extremely useful tool for checking the sleep current on my loggers.   The shiny surface is due to the conformal coating we put on all our components. Clear nail polish also works once you’ve cleaned all the flux.

At the time of this writing there are a few 1284  boards on the market: the Moteino MEGA , the Megabrick, the Dwee, and the ultra compact Pro Mini XL. Other projects sporting the chip seem to pop up regularly, though few  seem to last very long.  This is a shame because 1284 has enough juice to do single-chip emulation of early 8-bit computers.  But perhaps the chip never really caught on in Arduino-land because few beginners need that much capability. Sustained interest from dedicated makers means that some version, or at least DIY instructions, will will be available as long as the chip is in production. If I had to, the terminal expansion board I’ve used here would let me build one of those from the raw chip & a few passives. The trick would be making templates for the boards.txt and pins_arduino.h, and finding a bootloader that matches the system clock.

Component Prep & Logger Assembly:

Black =GND,

Foam tape holds the  Dupont jumpers together & provides accidental contact protection on the header pins. 3.3v system lets you connect the SD card directly

Place the SD module to one side, leaving room for wire pass-through under MEGA Board. Note:a 10k pullup on the CLocK line was removed because mode0 sleeps low

Route all but the GND wire  under the main board

Score the insulation at ~15mm, and ROLL THE INSULATION between your fingers to TWIST the thin 30AWG strands together.

Add a 5mm ‘hook’ to provide more wire under the screw terminal contacts

SD card connections:
Grey= (CS) -> D4,
Orange= (MOSI) ->  D5,
Purple= (MISO) -> D6,
Brown= (CLK) ->  D7,
Vcc 3.3v

SD power is controlled by switching the GND line with a TN0702 logic level mosfet on D0 -> After <- SPI peripheral is disabled & all SPI bus lines are pulled up.




Diffused common cathode RGB w GND pin bent to identify. No limit resistor is needed if you use INPUT_PULLUP to light each color channel.

Any 4 unused pins would work for the  indicator.

We add NTC thermistors to all of our builds & read them with the Input Capture unit on D14.



Attach the GND wires before putting the MEGA into the Plano 3440 stowaway box being use as a housing.

Here using 3xAA to supply the MCP1700 reg. on the MEGA. For unregulated builds, I use 2x Lithium AAs with flat discharge curves.

Blk(GND), Red(3.3v), White(SDA), Yellow(SCL), Blue(SQW)  with Dupont pin headers added to the cascade port. 32K is not used as our power-mod disables that output.

The DS3231 Vcc pin is clipped to force battery power & 1N1418 isolates the coin-cell (see end of the RTC post for details on this power-saving modification)

SCL=>D16 (yellow), SDA=>D17 (white), &
SQW =>D10 = Int0 (blue)

170 tie point breadboard completes assembly. I2C bus is tapped from the RTC modules 4-pin cascade port.

For comparison, here’s a regulated Pro-Mini based build from 2019 . In 2020 we switched the 328p builds to 2xAA with no regulator.

The Moteino MEGA also makes Aref available (purple). Setting that to the internal1.1v bandgap gives you a reference voltage with thermal stability similar to a T431 provided you leave the ADC running long enough to stabilize the cap on aref & account for the internal resistance.

Now all I have is a bit of code tweaking to change the I/O in the codebase so those commands match the Moteino’s pinmap.

To complete the trim on this prototype I’ve added a 16-Bit ADS1115 (I’ll be giving that ADC a serious workout soon & will post any interesting results). Pinning the add-on module vertically preserves space on that tiny breadboard. With the ADS I won’t be using the internal ADC for much other than battery tracking. However analog pins will safely tolerate mid-range voltages which cause serious leakage problems on normal digital I/O pins. This lets you use a resistor bridge for scaled ranging techniques.

With all that extra memory to play with I’ll finally get to try some graphing libraries with that OLED – a luxury I can’t usually afford with ProMini based builds (though tiny plotter works in a pinch). And rumor has it that you can bit-bang I2C on ‘any’ GPIO pins, driving SSD1306 displays up to 10 frames per second. We have plenty of I/O lines to spare now for those experiments.

All of Atmels ‘-P’ variants have low power modes, and the logger shown here sleeps below 20μA (without the OLED, which draws ~25uA while sleeping) – that’s the about the same as I usually get with the 328p based units.  The 1284 draws more runtime current (~17mA), but you can reduce that considerably by throttling the system with a prescaler.  If you then accelerate the I2C bus by the same factor with TWBR, your sensors are  none the wiser.  In fact I’ve had no problem polling the 1115  with the system clock brought all the way down to 62.5 Khz with clock_prescale_set(clock_div_256); 

I guess the last thing on my list is a name for this beast. I had been thinking of calling it the ‘MEGA pearl’, but a quick google search convinced me otherwise. So I’m open to suggestions.

Addendum 2020-05-12:

There are also 2650 (not-P) based options between $10-$20. The board shown above has double rows, so you’re stuck using straight pin headers unless you put 0.1” terminal blocks both above AND below. Plenty of ports there for hexapods, gigantic LED displays, musical instruments, or perhaps a game of chess. It is somewhat hard to find these boards at 3.3v 8mhz.

Well that didn’t take long: a few have already pointed out that I missed the Microduino Core + in the ‘currently available’ list. I’m sure there’s more so I’ll add them if I hear about others with a physical footprint that’s small enough to work as a drop-in replacement with the student build. I skipped the 644p in the preamble, but it’s another good low-power option if you are looking for more program memory.  The 1284 is pin compatible with the 644, ATmega16, ATmega32 and probably a lot more.

Now that I’m playing with the 1284 again, I’ll also post any interesting projects I come across using these more powerful processors, such as this acoustic impulse marker or this body fat analyzer.


Addendum 2020-05-21:   Using the ADS1115 in Continuous Mode for Burst Sampling

As promised, here’s a first look at that ADS1115 ADC module.  For day to day tasks, it’s already something of a work-horse for Makers. But I wanted to push it out to the max settings to see if it’s ready for some typical research level applications. The answer is a tentative yes, but only at the fastest 860 sample per second speed. And with small signals EMI is more of a problem than the limitations of the ADC itself. I’m sure that’s not news to the analog hardware hackers out there, but I’m kind of a digital kid who’s still learning as I go along.

DIY Data Logger Housing from PVC parts (2020 update)

Basic concept: two table leg caps held together with 3.5″ 1/4-20 bolts & 332 EPDM o-ring. Internal length is 2x3cm for the caps + about 5mm for each o-ring. SS bolts work fine dry, but we use nylon in salt water due to corrosion; tightening the bolts enough that the o-rings will expand to compensate for nylons 2-3% length expansion when hydrated. PVC is another good bolt material option if you deploy in harsh environments.

We’ve been building our underwater housings from 2″ Formufit Table Screw Caps since 2015. Those housings have proven robust on multi year deployments to 50m. While that’s a respectable record for DIY kit, we probably over-shot the mark for the kind of surface & shallow water environments that typical logger builders are aiming for.

The additional RTC & SD power control steps that we’ve added to the basic ‘logger stack’ since 2017 are now bringing typical sleep currents below 25μA.  So the extra batteries our original ‘long-tube’ design can accommodate are rarely needed. (described in Fig. A1 ‘Exploded view’ at the end of the Sensors paperIn fact, pressure sensors often expire before power runs out on even a single set of 2xAA lithium cells.

This raises the possibility of reducing the overall size of the housing, while addressing the problem that some were having drilling out the slip ring in that design. Any time I can reduce the amount of solvent welding is an improvement, as those chemicals are nasty:
(click to enlarge)

Basic components of the  smaller 2020 housing cost about $10. O-rings shown  are 332 3/16″width 2 3/8″ID x 2 3/4″OD EPDM (or other compound )

Double sided tape attaches a 2xAA battery pack to the logger stack from 2017 ( w MIC5205 reg. removed, unit runs on 2x Lithium AA batteries)


The o-ring backer tube does not need to be solvent welded. Cut ~5cm for 1-ring build, & 5.5cm for a 2-ring. Leaving ~1.5cm head-space for wires in the top cap.

The logger stack fits snugly into the 5.5cm backer tube with room for a 2 gram desiccant pack down the side.

The screw-terminal board is only 5.5cm long, but the 2x AA battery stack is just under 6cm long.  With shorter AAA cells you can use only one o-ring.

With several 4-pin Deans micro-plug breakouts & AA batteries things get a bit tight with one o-ring. So I add a second o-ring for more interior space.

Sand away any logos or casting sprues on the plugs & clamp the pass-through fitting to the upper cap for at least 4 hours to make sure the solvent weld is really solid. (I usually leave them overnight) Then wet-sand the large O-ring seat to about 800 grit.  Sensor connections are threaded 1/2″ NPT, but I use a slip fit for the indicator LED, which gets potted in clear Loctite E30-CL epoxy w silica desiccant beads as filler. Most clear epoxies will yellow over time with salt water exposure, so for optical sensors or display screens I usually add an acrylic disk at the upper surface.

The only real challenge in this build is solvent welding the pass through ports. In the 2017 build video we describe connectors with pigtails epoxied to the housing.  But you don’t necessarily need that level of hardening for shallow / surface deployments. The potted sensor connections shown in the video (& our connectors tutorial) can be threaded directly to the logger body via 1/2 threaded NTP male plugs. 

Note: Position the NPT risers on the caps directly opposite the bolt struts, and as near to the edges of the cap as you can so that there is enough separation distance to spin the lock down nuts on your sensor dongles. In the photos below I had the pass-through in line with the struts, but with long bolts this may limit your finger room when tightening the sensor cable swivel nuts. These direct-to-housing connections do make the unit somewhat more vulnerable to failures at the cone washer, or cuts in the PUR insulating jacket of the sensor dongle.


Threaded bulkhead pass-throughs get drilled out with a 1/2″ bit. Alignment with bolt struts shown here is suboptimal.

This closeup shows a slight gap near the center – I could have done a better job sanding the base of the NPT to make it completely flat before gluing & clamping!


the pass through style sensor cap mates to the the lower half of the housing. We’ve always used our o-rings “dry” on these pvc housings.

I describe the creation of the sensor dongles with pex swivel connectors in the 2017 build video series.

Dongle wires need to be at least 6cm long to pass completely through the cap.

“2-Cap” housing: Aim for 5 to 15% o-ring compression but stop if there is too much bending in the PVC struts.

It’s also worth noting that there are situations where it’s a good idea to have another connector to break the line between the sensor and the logger. (shown in 2017)  We often mount rain gauges on top of buildings with 10-20m of cable – so we aren’t going to haul the whole thing in just to service the logger. But on-hull connections like the ones shown with this new housing necessarily open the body cavity to moisture when you disconnect a sensor, and nothing makes a tropical rainstorm more likely to occur during fieldwork than disconnecting the loggers that were supposed to be measuring rainfall.

With a double o-ring and additional seal(s) in the cap, we probably won’t be deploying this new design past ~10m. Given how quickly they can be made, this short body will be a standard for the next few years; perhaps by then those fancy resin printers will be cheap enough for regular DIY builders to start using them – at least for shallow water work.  For now we’ll continue with the long body style for deeper deployments or remote locations that we might not get to again for a long time. The second o-ring is not really necessary if you make a nice tight stack when you assemble the logger.

In general I’d say these ‘plumbing part’ housings reach their long term deployment limit at about ~60m because the the flat end caps starts blowing noticeably at that depth. That overlaps nicely with the limit of standard sport diving, but if your research needs more depth it’s worth looking into the aluminum body tubes/endcaps becoming available in the ROV market. As an example:  Blue robotics makes some interesting enclosures if you need clear acrylic endcaps for camera based work.

Single Diode Temperature Sensor with Arduino ICU (& reverse-bias leakage)

Our LED sensor experiments lead to an interesting observation: When these ‘light-sensing’ loggers are left running overnight they still produce readings because reverse-bias ‘leakage-current’ eventually triggers the Interrupt Capture Unit (ICU) – in the absence of any light. The speed of this self-discharge depends on the ambient temperature. If you cover an rgb LED with black heat shrink, the different color channels have different rates of thermal decay:

Temp (Celsius) vs ‘Covered’ LED reverse-bias discharge time (Seconds) , Red, Blue & Green channels, generic rgb LED.  The LED was encapsulated in black heat shrink tubing and was connected directly to the IO pins with no limit resistor. LED’s take a very long time to discharge compared to other diodes, so at those time scales you can capture the data with the non-ICU based timing method of simply reading the high/low pin state in a loop. We use that simpler method in the 2019 classroom logger starter code on Github.

Both voltage and temperature affect reverse current, so these measurements must start from from a stable, regulated voltage.  Increasing the temperature by almost 20°C reduced the time to 20% of the low-temp value. The green channel appears to be more resistant to leakage which is surprising given that reverse bias currents are usually rated at ~1 µA for RY and ~10 µA for BGW colors. So perhaps this result says more about the volume & surface area of this particular unit than it does about LED color chemistry.

Even if I sleep the processor to save power, multi-minute readings would interfere with the other things we are recording on the Cave Pearl loggers.  However, this LED-based approach has interesting applications where space is limited and temps fall within a warmer range. The idea also has a lot of potential in situations that require high levels of sensitivity, although there aren’t many of those that can wait such a long time for readings.

Checking if I could use the technique with other types of diodes led to this jeelabs post where he compares the reverse bias leakage in three common diodes at 5V:

1N4004 – a high power diode: = 1.3 nA
1N4148 – a low power diode: = 3.4 nA
BAT34 – a Schottky diode:  = 50 nA

He also had the realization that “the reverse current could even be used as a temperature sensor.”  Small diodes have internal capacitance of a few pico-farads, so 5-50 nA will discharge them considerably faster than the LED channels I was using. In fact, reverse leakage increases so much with Schottky diodes it can cause a thermal instability issues which limit their useful reverse voltage to well below their max rating.  Germanium Diodes are even more susceptible.

Add black heat-shrink around  diodes with clear encapsulation (like the one shown here) or they will also be affected by light levels in the local environment.

It’s worth noting that most diode based temperature sensors use the change in forward voltage  because that relationship is linear, with about 2mV less voltage drop for every degree increase of temperature.  But chasing a few milli-volts with Arduino’s 10-bit ADC only allows a precision of ±1°C unless you add amplification, or some other trick.
By comparison, leakage current can be expected to double with every 10°C increase in temperature, making higher resolutions possible with the same hardware. The trade off is using a non-linear relationship which produces variable resolution over the sensing range. And since leakage is also a byproduct of manufacturing variations you need to calibrate each diode individually. That’s a show-stopper in production environments where that time costs more than the whole device, but not so much for DIY projects which need to run-test their build for a few days anyway. We don’t usually send a logger into the field until it’s had several weeks of stable operation.

Testing a 1n5819 Schottky Diode:

Here I’m timing the leakage-discharge with Timer1 clock ticks from an 8mHz 3.3v ProMini:

Temperature (°C) vs 1n5819 Shottky Reverse-bias Leakage Discharge Time (8 mHz clock cycles)  Diode connected between D7 & ICU on D8. Blue dots are Excel’s trend-line. That fit was better than I was expecting.

The Schottky discharges very quickly at room temperatures, with raw Timer1 counts of about 1300 at room temperature ( ~0.16 milliseconds) and about 100 counts of variation /°C.  Counts increase as temperature falls to ~5800 ( ~0.7 ms) at 6°C, with a delta of 580 counts per degree. The curve flattens out at the lower limit of this test with raw counts about 62,000 ( ~7.7 ms) at -15°C, and a delta of 7000 counts/degree.  

The timing jitter on these ICU readings ranges between 10-20 counts depending on the board  (even with 4x noise reduction enabled) and this is a significant source of error when you only have a per-degree delta of 100 counts.  You can over-sample the Schottky to compensate, and testing showed that 256x OS readings produced results that looked very comparable to 1n1418 diodes. (although some authorities say that timing-jitter may be resistant to this smoothing technique.)  Even with oversampling, these short discharge times could become too brief to even count with an 8mhz Promini at temps above 50°C.  However measuring cold temperatures can sometimes be more challenging than warm ones, and for those applications a fast discharging diode like a Schottky might be preferred. With communications overhead, it’s not unusual for an I2C sensor reading to take 1-2ms, so a Schottky might also be better for low power systems trying to minimize CPU runtime.

Testing a 1n4148 Signal Diode:

Temperature (°C) vs 1n4148 Reverse-bias Leakage Discharge Time (8 mHz clock cycles) Diode connected between D7 & ICU on D8. Diode wrapped in black heat shrink tubing. Blue dots are Excel trend-line fit.

The 1n1418 discharges more slowly, with raw Timer1 counts of about 36,000 at room temperatures (~5 msec.), and about 2000 counts of variation/degree at 25°C.  Raw counts increase to ~158000 (~20 ms) at 6°C, with a delta of ~17000 counts per degree and the lower limit of this test saw raw counts 1.3 million (166 ms) at -15°C, and a delta of 140,000 counts/degree.

The 1n1418 is better sensor overall because it won’t drop below the Arduino’s timing capability at natural environment temperatures, and it’s discharge takes long enough that jitter becomes an insignificant source of error. Even in colder environments, 166ms of SLEEP_MODE_IDLE (which leaves Timer 1 running for the clock cycle count) only burns about 0.16 milliamp-seconds per reading on a Promini. That’s not going to break our power budget.


Its worth noting again that you must use a regulated system. Ideally, shifting supply voltage causes a corresponding change to the Schmitt trigger points on the I/O pins. That compensates to some extent, however batteries have significant thermal mass and this causes serious hysteresis problems when sensing temperature.

To calibrate my diodes, I covered them with black heat shrink tubing and taped them in physical contact with an si7051 sensor. Then I placed the logger into a rice filled double ceramic pot ( to add thermal mass) and moved the pot around the kitchen, from the radiators to the refrigerator & freezer. You want stable periods that let the ref & diode sensors equalize, using an average of 20 readings to smooth compressor wobble at the lower end, and those crest/peaks at higher temperatures.

Typical SI7051 (±0.1°C) reference temperature run for calibrating the 1n1418 diode. Boxes indicate plateaus chosen for the calibration data points & coverage areas of closeup graphs shown in the ‘sets’ comparison below.

Excel trend-lines got reasonably close to the response from the Shottky & 1n1418; perhaps needing only one more term for a better fit.  Since thermistors are also semiconductor devices I wondered if those diode decays would generate workable S&H constants if I treat the raw Timer1 counts AS IF they were resistance values from an NTC:

Here I used 20 reading averages to compensate for the fact that the diode is higher resolution than the Si7051 reference, and the long-integration 1n1418  readings have considerably less jitter than the IC sensor.

Then you can convert the discharge time to temperature with the Steinhart-Hart equation:

#define COEFF_A   2.0007E-03    // Coefficient from SRS online calculator
#define COEFF_B   1.3166E-04
#define COEFF_C   3.5441E-09

float Temp = log(RawDiodeDischargeTime);  // note that Log(x) on Arduino is actually LN(x)!
Temp = COEFF_A + (COEFF_B * Temp) + (COEFF_C*(Temp*Temp*Temp));
Temp = (1 /Temp) -273.15;       // -273 converts  Kelvin to Celsius
(Note: I usually save raw readings on the loggers & convert them later in Excel. I’ve been burned several times by loss of significant figures during calculation on the 8-bit 328P processor)

That equation has a quoted accuracy of about ±0.1°C over a 100 degree range when used for a thermistor, but does this hold with a diode sensor?  Yes  – but over a smaller 40 degree range:  (Click Image to Enlarge)

Comparison of si7051 reference temps (blue) (°C) vs 1n1418 based S&H calculations (red) Two examples shown with different ‘center’ points (21°C on the left & 5°C on right) used to generate the three equation constants.

I choose these sets to show calculation errors creeping in as you move farther from the points used to generate the constants. The calculated temperatures in this example drift ~0.07°C from the reference at a distance of ~15°C from the center point.  A tighter set with calibration points at 5, 21, & 36°C produces a near-perfect fit inside that range, with the trade-off  that temps down at -14°C then show an increased deviation >0.1°C.  Overall, it’s about 30% more error than I’d expect to see when calibrating a cheap 10K thermistors with the same points. Given that our Si7051 reference thermometer has a rated accuracy of  ±0.13 °C (datasheet pg 7), I think the best we can achieve for this diode based method is ~±0.2 °C at typical cave temperatures.

So max-middle-min gives you about 40 degrees of usable range and you want at least one of your cal. points at the area of interest.  That’s pretty good considering we are applying the thermistor equation to a different physical system. I will experiment with solver to see if models with more parameters provide a better fit, but this is already is good enough for most of our logger deployments. 

Figure 14-1 I/O Pin Equivalent Schematic from the 328p datasheet. Those protection diodes can also cause problems when de-powering voltage dividers.

My gut feeling is that the re-purposed equation  would work over a wider range if this was a single diode system. However AVR inputs are also connected to two protection diodes and a pull-up MOSFET. Each of these is subject to its own reverse bias leakage to some extent, with the upper protection diode acting in direct opposition to the discharge of the ‘sensor’ diode.  In fact, you can simply run the ICU timing code with nothing at all connected to the D8 pin, and it will still give you a temperature based reading. That makes this the second ‘no parts temperature sensor’  method I’ve discovered for Arduino but, like LED’s, these diodes are low leakage; taking five seconds for a read at 20°C, and five minutes for a reading down at -14°C.  Unless you change the prescaler, the raw numbers could exceed the range of easy calculation on a 328, and show significant hysteresis due to the mass of the chip & Promini board it’s attached to.

The implication here is that temperature sensing via this reverse bias decay method has a sweet spot somewhere between the too-rapid response of a Shottky diode (which approaches the counting limits of an 8MHz clock) and interference from the other stuff connected to Arduino I/O pins. 1n1418’s work well, but I’m sure there are other diodes out there that could do a better job. I have yet to find any good data on the long term stability of reverse bias leakage but we are not stressing the part by exceeding it’s reverse voltage rating, or running enough current to cause much self-heating.  So I suspect that diode leakage is at least as stable as thermistor response over time. There’s a lot of further experimentation to do here, and given the tighter manufacturing spec, I’m curious to see if the method works with diode connected transistors which could make interchangeable temperature sensors possible.

I should also mention that some ‘better quality’ Arduino boards have temperature compensation embedded in their system oscillator. This is a bad thing for this ICU timing method because it introduces a sharp discontinuity in the clock speed when the comp. circuitry kicks in. The S&H constants can’t absorb that like the normal ‘thermal response’ of a cheap oscillator, so the method works better on some boards than others. Another potential problem is moisture accumulating on surfaces -which could provide an alternate current path to discharge the diode. So as with our LED light sensing, desiccants are required inside the logger housing.

the CODE:

D7 is simply acting as a convenient GND connection.

I’ve left this till last, because it’s essentially just a tweaked version of the ICU timing method I posted for reading thermistors. With the diode discharge you triggering on fall instead of rise, and you don’t have to read a reference resistor because we are treating the decay time as a resistance.   The diode’s tiny internal capacitance charges through the INPUT_PULLUP resistor in a few nanoseconds, and there’s no need to discharge afterward.

(Note: This code is modified from the ICU capacitor reading example by Nick Gammon)

#include <avr/power.h>                        //  for peripherals shutdown
#include <avr/sleep.h>                          //  to sleep the processor
volatile boolean triggered;
volatile uint16_t timer1CounterValue;
volatile uint16_t 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
if (triggered){ return; }  // multiple trigger error catch
timer1CounterValue = ICR1;    // Input Capture register (datasheet p117)
triggered = true;
if ((TIFR1 & bit (TOV1)) && timer1CounterValue < 256){    // 256 is an arbitrary low value
overflowCount++;    // if “just missed” an overflow
bitClear(TIMSK1,TOIE1);   // disable interrupts on Timer 1 overflow
bitClear(TIMSK1,ICIE1);     // disable input capture


void prepareForInterrupts() {
noInterrupts ();
triggered = false;                   // reset for the  do{ … }while(!triggered);  loop
TCCR1A = 0;     // set entire TCCR1A register to 0
TCCR1B = 0;     // same for TCCR1B
TIFR1 = bit (ICF1) | bit (TOV1);   // clear flags so we don’t get a bogus interrupt
TCNT1 = 0;       // initialize counter value to 0
overflowCount = 0;        // reset overflow counter
bitSet(TCCR1B,CS10);     // set prescaler to 1x system clock (F_CPU)
bitSet(TIMSK1,TOIE1);    // interrupt on Timer 1 overflow
bitSet(TCCR1B,ICNC1);       // Input Capture Noise Canceler = 4x repeat b4 trigger
bitClear(TCCR1B,ICES1);       // Input Capture Edge Select ICES1: =0 for falling edge
// or use bitSet(TCCR1B,ICES1); to record rising edge
bitSet(TIMSK1,ICIE1);             // Enable input capture unit

TIFR1 = bit (ICF1) | bit (TOV1);   // clear flags again ( this may be unnecessary?)
interrupts ();


//========== READ DIODE connected between D7 —>|— D8 ICU ===========

digitalWrite(7,LOW); pinMode(7, OUTPUT);  // simply acting as GND
digitalWrite(8,LOW); pinMode(8,OUTPUT);
power_timer0_disable();    // otherwise Timer0 generates interrupts every 1us
power_timer1_enable();    // this whole method depends on timer1
bitSet(ACSR,ACD);    // Disable the analog comparator
//could disable other peripherals to save power during idle

digitalWrite(8,INPUT_PULLUP);    // charging the diode-capacitor (occurs VERY quickly)
prepareForInterrupts ();
noInterrupts ();
set_sleep_mode (SLEEP_MODE_IDLE);  // leaves Timer1 running
PORTB ^= B00000001;     // toggles OFF pull-up resistor on D8 (leaving pin in INPUT)
TCNT1 = 0;                          // re-initialize Timer1 counter

interrupts ();
sleep_cpu ();  //sleep until D8 falls to the 33% threshold voltage
noInterrupts ();
}while(!triggered);  //trapped here till TIMER1_CAPT_vect sets triggered=true

uint32_t diodeDischargeTime= ((uint32_t)overflowCount*65535) + timer1CounterValue;
// change to uint64_t calculations when timing diodes that decay slowly

interrupts ();
power_timer1_disable();    // cleanup
power_timer0_enable();    //  needed for delay, micros, etc.

(Note: Integer arithmetic on the Arduino defaults to 16 bit & never promotes to higher bit calculations, unless you cast one of the numbers to a high-bit integer first. After casting the Arduino supports 64-bit “long long” int64_t & uint64_t integers for large number calculations but they do gobble up lots of program memory space – typically adding 1 to 3k to the compiled 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

Addendum 2021-01-24        Don’t sleep with regular I/O pins.

I’ve been noodling around with other discharge timing methods and come across something  that’s relevant to using these methods on other digital pins. Here’s the schematic from the actual 328 datasheet, with a bit of highlighting added. The green path is PINx.  It’s always available to the databus through the synchronizer (except for in SLEEP mode?) The purple path is PORTx.   Whether or not it is connected to PINx depends on the state of DDRx (which is the yellow path.)

As shown in the figure of General Digital I/O, the digital input signal can be clamped to ground at the input of the Schmitt Trigger. The signal denoted SLEEP in the figure, is set by the MCU Sleep Controller in Power-down mode and Standby mode to avoid high power consumption if some input signals are left floating, or have an analog signal level close to VCC/2.

When sleeping, any GPIO that is not used an an interrupt input has its input buffer disconnected from the pin and in clamped LOW by the MOSFET.

Clearly D8 on the ICU must one of those ‘interrupt exceptions’ or the thermal discharge of the diode would have been grounded out by entering the sleep state.  If you use a similar method on regular IO pins you can’t sleep the processor in that central do-while loop.

Creating a Normalized Vegetation Index Sensor with two LEDs

Here I’m using a 2019 (v. regulated) classroom logger to create a custom ‘Leaf Transmittance Index’ based on readings from an IR LED and the red channel of the RGB indicator already on the logger. Although using generic LED’s introduces non-optimal aspects wrt frequency & bandwidth, the trial successfully distinguished ‘healthy’ vs ‘unhealthy’ plant leaves where a simple visual inspection could not.

When we released the 2019 version of the classroom logger  we updated the starter script to include a technique that uses the indicator LED as a light sensor. This under-appreciated technique leverages the timing capability of microprocessor inputs, rather than the more common approach of using an op-amp to amplify sensor output. Reversing the potential across a diode charges it’s internal capacitance, which can then be discharged by light photons hitting the surface.  In ‘reverse bias’ mode, the photon flux is linearly related to the discharge current, however this depletion method changes the voltage across the capacitor at the same time (+ other factors), so we see a response with exponential decay instead of linear.

Typical discharge curve for an LED charged up to 5 V and then discharged to a threshold of 1.7 V under artificial lighting (fluorescent tube).    Figure 8 from: Absorbance Based Light Emitting Diode Optical Sensors and Sensing Devices pg 2462

Electrically speaking, there is little difference between an LED and a typical photo-diode sensor however an LED’s capacitance is considerably smaller. ( 25 – 60 pF ) The tiny light sensing surface area of an LED (~0.1mm squared) only generates about 50 pA of discharge current in normal ambient conditions, and the reverse leakage through LED’s is exceptionally low (~0.002 pA). The net result is that LED’s are rather slow light detectors, and this phenomenon would be nothing more than a curiosity except for one important aspect: most LEDs detect a relatively narrow band of light wavelengths, making it possible to build a frequency-selective detector without the filters (or monochromators) you’d need to do the same job with photo-diodes or LDRs. That sensitivity band often has less drift over time than many types of filters and the discharge/photocurrent method has less temperature dependence than using the same LED in photovoltaic mode.

Illustration from that same Sensors review paper, but originally from: Novel fused-LEDs devices as optical sensors for colorimetric analysis.  Talanta 2004, 63, 167–173. Sometimes these these emitter-receiver pairs are dip coated with a chemo-reactive membrane.  Their next paper: Quantitative colorimetric analysis of dye mixtures using an optical photometer based on LED array Sensors and Actuators (2006), used a series of different emitter LEDs and a low band gap IR LED as a universal light detector. Xiao et al in 2009 used blue excitation LED’s that match the excitation wavelengths of common fluorescent dyes like fluorescein.

This makes a host of new LED-based instruments possible at the DIY level, and Forest Mims demonstrated this with some  elegant experiments using near-IR LEDs detecting atmospheric water vapor, aerosols with twilight photometers, and he even proved that a single red LED reading provides a reasonable proxy for total PAR. (using a red gallium phosphide (GaP) LED with a wide (115 nm FWHM) absorption band @ 600-655 nm)  Since Mim’s pioneering work in 1977, the number of applications for LED sensors has grown so fast that now it’s hard to keep up with the ‘review papers’,  let alone the individual publications.  Bench-top chemistry is seeing a host of fluorescence & reaction cell experiments based on frequency matched LED emitter-detector pairs.  By rapidly toggling the same LED between emitting and detecting light, several projects have created other types of sensors like ocean PH. We can only imagine what will happen when up-converting nanoparticles get thrown into that mix.

What can we do with our logger
using this LED measurement technique

Here a day of raw readings from all three LED channels are compared to an LDR in same classroom logger. The unit was deployed in a south facing window with diffusing tape over the housing surface.

Light detectors are often used to make measurements of energy balance, usually by tracking solar insulation. Using the RGB indicator LED already on the logger means we only have a limited number of light frequencies to work with, so we can’t create a ‘full spectrum’ pyranometer unless we use a more advanced solution like SparkFun’s Triad Spectroscopy Sensor . Combining that with good mounting bracket , would provide enough frequency coverage to match some commercial instruments

Figure 2.1. LED-based Sun photometer for the GLOBE project. The rectangles with the green (505nm) and red (625nm) spheres represent the LEDs.  From The Contribution of Dutch GLOBE Schools to Validation of Aerosol Measurements from Space

Despite this limitation, a few dedicated groups have proven that LED photometers can still be quite capable. Most notably the 2-LED Globe program photometers by Brooks et al. at the Institute for Earth Science Research and Education It is quite inspiring to see students using hand-made instruments to produce research good enough to publish in peer-reviewed journals. 

The Globe device uses a more traditional op-amp approach to reading the LEDs, but several aspects of those instruments are directly transferable to other light-sensing projects:

  1. LED sensors are exceptionally stable over time.
  2. Students can manually aim the detectors at the sun, enabling a basic instrument to do the work of more complicated “sun tracking” machines that use directional control, collimators or shadow bands to measure diffuse irradiance.  From Mims PAR paper:  “Measurements are made each day at or near local solar noon when the solar disk is not obscured by clouds. Measurements are made by placing the radiometer on a level platform 175 cm above an open grass field. Two measurements are made of each channel; first the full sky and then the diffuse sky. The latter measurement is made when the diffuser of the sensor is shaded by a 19 mm diameter disk mounted on a rod.”
  3. Two or more measurements are needed because it’s the difference between those readings that allows you to derive the property you were trying to measure. For example, if you had two diodes, with one responding to UV-a and the other responding to UV-b, the difference between those readings could be attributed to atmospheric ozone. Comparing readings from two IR sensors with one at the 940nm H20 absorption peak and another sensor at 890nm, would let you derive water content.
  4. This also requires correcting for scattering/absorbance by the atmosphere ( Path Length = 1/cos(θ) ) based on the suns angle in the sky (also note: many diy PAR projects hack the white plastic domes out of old photometers as cheap cosine correctors.) Better instruments also correct the ~1% / °C temperature coefficient of red spectrum LEDs, which is higher than that of silicone photodiodes.
  5. The biggest challenge is determining unique the absorption band of the LED you are working with since that information is not supplied by manufacturers. The process usually requires testing the LED(s) with a wavelength scanning monochromatic light source.

All of Forest Mims LED-based experiments could be replicated with the Cave Pearl classroom logger using the capacitance discharge timing method instead of using an op-amp for current to voltage translation. But there’s a more information to be had from LED-sensors by anyone willing to do a little tinkering, especially in the area of vegetation monitoring.

Spectral Characteristics of Vegetation:

Note photosynthesis has a relatively absorption-free range: 550-630nm. to protect the plants from oversaturation damage.

Photosynthetic pigments absorb significant amounts of the red and blue light falling on healthy vegetation. Many papers focus on chlorophyll, but there are several other ‘antenna pigments’ which also absorb light to enhance photosynthetic efficiency. Approximately half of the un-absorbed visible radiation is reflected, leading to the green appearance of leaves. Reflectance is much higher at near infrared (NIR) frequencies than in the visible region due to the cellular structures inside the leaves.


Normalized Vegetation Indexes:

A friend recently sent me a link to Rick Shory’s extensive work on the greenlogger which hit the in-box around the same time as SciAm’s article: Earth Stopped Getting Greener 20 Years Ago. Reading about the global decline in vegetation set me on a deep dive into how indexes are used in bio-physical monitoring:

NDVI was developed to estimate vegetation cover from remote sensing data. It is calculated from red and NIR spectral reflectance measurements, and the first key understanding is that spectral reflectances are normalized ratios of the reflected over the incoming radiation in each spectral band. Feeding those ratios into the NDVI calculation means that it can only produce values between -1 to +1.

The second key understanding is that using the ratio of the difference of the red & infrared over their sum corrects for the effect of the solar zenith angle. This eliminates irradiance from the equation, and largely corrects for differences due to topography and transmittance loss in the atmosphere. This allows the comparison of remote sensing data from different times of day, and different latitudes.

NASA uses NDVI as a an indicator of drought, When water limits vegetation growth, it has a different relative NDVI than when the same plant is hydrated because the spongy mesophyll layer deteriorates, and the plant absorbs more of that near-infrared light rather than reflecting it. This is a significant factor for agricultural yield prediction..

Moderate NDVI values represent low density of vegetation (0.1 to 0.3), while high values indicate dense vegetation (0.6 to 0.8)  Zero indicates the water cover, and lower values of NDVI (-0.1 and below) correspond to barren areas of rock, sand, or urban/built-up.  In addition to land cover classification, people use NDVI to infer parameters like Leaf Area Index (LAI), fractional light interception (fPAR), wildfire  burn-area, and other aspects of the biological environment.   But the NDVI index also has some limitations: Any time there’s very low vegetation cover (majority of the scene is bare earth), NDVI will be sensitive to that soil. On the other extreme, where there’s a large amount of vegetation, NDVI tends to saturate.

Over time NDVI has been tweaked in various ways and today there are a large number of different Broadband ‘Greenness’ Indexes that accent different aspects of plant physiology.  And the booming agricultural drone business seems to be inventing more by the day, with claims that somehow their camera tweak produces a new index that’s superior to those of it’s competitors, while their competitors make equally strident claims that company #1 doesn’t know what they are talking about. Public lab has an active community of people hacking cameras for this kind of imagery.

Can we use the RGB LED on the Classroom logger to measure a Vegetation index?

Fig.4.24, pg49, Approximated Emission and Sensitivity Spectra (of an OSRAM LH-W5AM RGB led)  from: Using an LED as a Sensor and Visible Light Communication Device in a Smart Illumination System, Dissertation by André Filipe Rato Bispo

The first challenge is figuring out what we can actually detect, since each index works with a given set of frequencies. An LED will only detect light with higher energy photons than the light it emits, so a blue LED will be unable to detect red frequencies. LEDs generally have peak detection capability 20-60nm shorter than the wavelength they emit with the range widening,  but this information is rarely available from manufacturers because it’s a use case they were never designed for.  So there are not many sources that compare the emission and detection frequencies for the different LED chemistries. LEDs: Sources and Intrinsically Bandwidth-Limited Detectors (Figure 5) has a reasonable list of specific LEDs characterized as both sources and detectors, but even they didn’t bother to test a garden variety RGB.  Fortunately a few researchers working on visible light communication projects have tested them, generally finding that the blue emitter shifts into UV-A 320-400 nm detection range (possibly near one of the UV-A peaks of the phototropic action spectrum?) , the green emitter shifts down to about 440-460 nm (detecting in chlorophyll a/b blue absorption bands?), and the red LED channel shifts down to ~680nm, with a spectral spread 2 to 3 times wider than its emission band. (overlaps the chlorophyll a red absorption peak?)

Testing to see how a 488nm dichroic mirror  (blue cut-off filter) affected readings on the Green LED detection channel. Note that in this case the round lens was also removed from the top of the LED with sandpaper to both collumnate and diffuse the incoming light. But with the PTFE tape layer added later, the sanding was unnecessary for the index measurements.

But was this true for my LED?  Since we didn’t have a “wavelength scanning monochromator” just lying around I tested the green channel with a blue-cutoff Dichroic Mirror Unlike regular filters, dichroic mirrors are intended to work only for incoming light that is normal to their surface – but given the tight 20 degree dispersion angle of typical 5mm leds, that’s probably OK.  If I was looking for a range of different filters on the cheap, I’d probably look at safety glasses designed for laser work – they usually come with a well specified transmission curves & very sharp cut-offs at certain frequencies.

Sure enough, the discharge on the ‘blue-shifted’ green channel took more than 10x longer with the filter in place, indicating that the green channel was sensitive to frequencies below the filters 488nm cut-offIf the red LED channel also follows the pattern from Bipso’s paper, then red’s detection will include some green & some red, with a peak at yellow.  We might be able to use these frequencies for a BLUE vs GREEN variant of NDVI, but several sources indicating that blue indexes were sub-optimal because that chlorophyll absorption is strongly overlapped by carotene. So blue based indexes usually show less contrast between stressed versus non-stressed plants.  The loss of both the blue channel (now a UV detector) and the green channel (now a blue detector) meant that we need to add an IR led to have enough information for a viable index.

IR obstacle avoidance modules are one inexpensive source of IR LEDs.  These sensors are somewhat limited when used for their intended purpose, but a little cut & paste lets you merge the emitter LED with a daylight filter ‘cap’ cut from the photo-transistor on the same board:

IR detectors can be sensitive to visible light unless you add a daylight filter, and this little hack is much cheaper than buying a Wratten 87, 87B, or 87C. These distance modules usually have LEDs which emit at 940nm, but the same shorter wavelength-shift also applies to the IR LED, and this pulls the detection peak into the 920nm range – safely out of the absorbance well created by atmospheric water vapor(?). (something to keep in mind if you are actually trying to build a water vapor sensor – you probably need to select an emitter at least 25 nm above 940)

Note that the negative terminal of the IR LED is under the same screw terminal (D7) as the common cathode leg of the RGB indicator led. Columnating the LEDs with heat shrink tubing should also make them less sensitive to light reflections inside the logger. Note: our GitHub code uses a port command that assumes that shared ‘negative’ pin to be one of D3 to D7 on the pro mini. Here I’ve set #define LED_GROUND_PIN 7 as the common GND connection, RED_PIN 6, GREEN_PIN 5, and the positive side of the IR led is on pin D8. Set  #define BLUE_PIN 8 to force the former blue pin code to take the IR LED reading. Also note: that our code initially ‘lights’ the indicator LED via INPUT_PULLUP mode, instead of setting the pins to OUTPUT & HIGH, so limiting resistors are not necessary for that step.

Even with a filter and heat shrink tubing the IR LED was easily saturated, and it took 28 layers of plumbers tape to bring readings from the IR LED into approximately the same range as the RGB readings (which had only 1 layer of diffusing PTFE tape on it)  This was done with the logger tethered a laptop, displaying readings in the serial monitor window. I simply added one layer at a time until the readings under direct full sunshine for both the IR & Red channel were in the 300 to 500 range (using our logger code from Github). Then I add some heat shrink to hold those layers of teflon tape in place, leaving only the round dome of the LED(s) exposed.

Even with IR on board, the lack of a sharp red detector means we can’t produce the traditional NDVI. (unless we add a Wratten25) With the ‘red’ LED channel actually detecting from 525 to 650 (FWHM 560-610) and peaking around 575nm we have to invent our own pseudo index that might be described as  ‘lime-green’ NDVI.  This actually isn’t so bad, as several recent papers demonstrate that green-based NDVIs are more sensitive to chlorophyll than the red based indexes, and they have stronger correlation with total leaf area. (It’s also worth noting how close we are to the bands typically used for pulse oximetry, where the reading from a red LED at 600nm is ratioed with a second measurement from an IR-LED responding at 950nm)

Other Challenges:

With a tinfoil wrap, light can only enter through the 5 x 8cm exposure window above the LEDs. One layer of blank label-maker tape on the lid diffuses the light and prevents the plastic struts inside the housing from creating hot-spots. A final outer wrap of clear packing tape protects the tinfoil.

Bandgap voltages vary with temperature, changing the LED emission wavelength by ~ 0.1nm/°C.  Detection wavelengths should follow suit, so it’s probably best to make sure the temperature varies as little as possible between our scale points and the target readings.  Since the LED detectors are inside a Plano box, there is potential for some frequencies to be lost, but materials like high density polyethylene (HDPE)  have remarkably smooth absorption curves that don’t become extreme until you reach UV. The fact that Vi’s are ‘a ratio of ratios’ means that we have to compare the raw sensor readings to direct insulation values before the index can be calculated. Housing losses should affect the high reference and the target readings in the same way, so it should not throw off the final index – essentially we treat it like transmittance loss in the atmosphere. And I need to use a desiccant because this is a capacitance – discharge method, so things like ambient humidity could affect the capacitance.

Since we can’t know the responsivity of our garden variety LEDs without lab testing, the best we can do is standardize the data by scaling it against a “maximum” reading (when the sun is shining directly on the top of the logger) and a second “minimum” point for each channel (obtained by covering the entire logger with tinfoil).  The dark point reading also address thermal leakage through the LEDs.  One drawback of this mcu-based method is that the raw discharge-time readings follow an exponential decay curve, so we need to take the log of those readings to linearize the data before scaling, and since our decay-time readings are inversely related to the photon flux, we need to apply  1/LN(reading)  to all data before the max/min scaling. Technically speaking, photon flux is not the same as irradiance, but the the index’s normalization sweeps that little issue under the rug too.

Using the sun as our high scale point necessitates that the readings are done under a clear blue sky. Clouds passing overhead between readings (or other haze / humidity variations) could change the ratio of IR to visible light more than the plants you are trying to monitor:

Rain is another potential complication, as water strongly absorbs IR.  So the readings have to be taken long enough after a rainfall that no water droplets are present on the surfaces. So with the basic 2x LED configuration I’m using here, you have to wait for good weather, and take the readings under a clear blue sky, between mid morning & mid afternoon.

Cloud induced variations could be compensated by putting two sets of sensors on the logger (one pointing up  &  one pointing down) for simultaneous correction of direct insolation vs surface reflectance, but for now this is just a prototype trial to see if a decent ‘lab exercise’ can be developed for the Cave Pearl Loggers with minimal additions to the basic build.

Does it work?

Backyard trial testing the reflectivity of my lawn in an area deliberately chosen as “unhealthy” grass .

With all the rough assumptions up to this point I was surprised to find the reflectance readings falling within the broad range of ‘textbook’ values. A reading 1m above a relatively healthy section of my lawn produced an index of 0.39 while a reading above a mangy half-dead section (photo: right) produced a much lower index value around 0.215   A patch of bare dirt read at 0.044, and my gravel driveway produced an index of -0.13   The front flower garden produced a reading of 0.292  It’s hard to know how representative this is given the wide range of values listed for different plant species in the various spectral libraries.

With the challenges of species variation & water condition, the use of verified bright & dark targets is pretty common in biophysical sensing. White panels coated with barium sulfate paint, Komatex, or Teflon are sometimes used because they have reflectance near 100% with very few  specular artifacts. NASA’s Aster Spectral Library suggests that most flat black paints have a similar response through visible and NIR, so some Rustoleum on a big sheet of cardboard might work as a 4% calibration point.

A better idea for the classroom:

Covering large areas with a grid of these single-shot readings would take a substantial amount of time, and even relatively short trials run into issues with trees and other large shadows creeping into the test patches throughout the day. So logging reflectance is more suited to long-term measurements of vegetation cover at single location. (or forest canopy transmittance)

After a bit more reading,  I began to notice a pattern:

(click to enlarge) Leaves cover the input window, secured to prevent wind shifting.

So index calculations ought to work with light that is transmitted through the leaves because the two curves contain very similar information. With the standard reflectance-based NDVI, ratings between 0 and 0.33 indicate unhealthy or stressed plants, 0.33 to 0.66 is moderately healthy, and 0.66 to 1 is very healthy. Flipping the calculation to a transmission-based version will shift those values significantly, but we can still use that trend as a rough guide to whether the method is working.

As luck would have it, this insight occurred just before another fieldwork trip, so it was more than a week before I could test the idea. The first trials used leaves from tropical almond trees which are dry-season deciduous. Re-capturing some of their ‘energetically expensive’ chlorophyll turns the leaves pinkish-red or yellow-brown due to the leftover pigments such as violaxanthin, lutein, and zeaxanthin. These Xanthophylls are yellow pigments from one of the two major divisions of the carotenoid group (the other division is formed by the carotenes). They act to modulate light energy and may also serve as a non-photochemical quenching agents to deal with excited states of chlorophyll that are overproduced during photosynthesis under the intense light conditions.

Leaf Number Red RGB channel: scaled
% of full sun reading
IR LED reading: scaled
% of full-sun reading
(gYr) “Transmission
based” NDVI
#1 (green) 37.24 82.81 0.380
#2 (orange) 59.47 83.13 0.166
#3 (red) 42.22 80.95 0.314
#4 (yellow) 67.62 82.91 0.102

The overlap of the sub-LEDs at 550nm is leveraged for transmission (green) and photodetection (red) in short range visible light communications.

All leaves were ‘fresh-picked’ from the tree, and the percent transmission numbers were averaged from five readings taken one minute apart. Natural light is notoriously variable, so most index sensors use considerably more sample averaging than that.

It’s not surprising that the yellow leaf was well discriminated, but the fact that the green & red leaves produced similar values highlights an issue with our rough prototype: the widened spectral spread of the red LED channel makes it difficult to distinguish between light frequencies on either side of the response curve.

Leaf (click to enlarge)  Red RGB channel: scaled
% of full sun reading
IR LED reading: scaled
% of full-sun read
(gYr) T-NDVI
#10 (Dead on ground) 26.96 65.38 0.416
#11 (yellow patch) 44.90 79.45 0.278
#12 (green & healthy) 34.27 76.17 0.379
#13 (>50% yellow) 55.56 77.79 0.167

Despite the limited red/green selectivity, this set shows that, at least for ‘fresh’ leaves, our custom index can still discriminate the loss of the chlorophyll. The dead leaf suggests that will only work up to a point. Dead vegetation reflects a greater amount of energy than healthy vegetation throughout the visible spectrum but reflects less IR, so any actual ‘dead-spots’ will raise a transmission-based index value. This is a limitation for our method – which does not produce a natural separation between healthy and dead vegetation like a reflectance based index would. This would be serious problem for remote sensing applications, but when a leaf that you can hold in your hand is that badly off, you probably don’t need a sensor to tell you the plant is not thriving.

Does this transfer to other plant species?

Leaf (click to enlarge)  Red RGB channel:
scaled % of full sun
IR LED reading: scaled
% of full-sun read
(gYr) T-NDVI
#14 (Green leaf – from
stressed plant1)
39.29 77.51 0.327
#15 (Brown spots – from stressed plant1) 37.71 76.23 0.338
#16 (Green leaf from
healthy plant2)
21.83 68.05 0.514
#18 (>50% yellow – other stressed plant3) 32.65 65.47 0.334

As expected, the overall light transmission numbers were different for palm than they were for the tropical almond leaves. So a calibration set would need to be created for each plant species to put these numbers into context. I’m assuming the green/yellow discrimination is due to chlorophyll levels in the almond leaves, but there could be other confounding factors like Anthocyanin in the hardier palm leaves. (Antho. also absorbs in our sensor band, and is abundant in senescing leaves) 

While #14 and #15 look different they were taken from same plant, and the yellow-brown spots on #15 make it clear the plant was under some kind of stress.  Visually, I would not have been able to distinguish leaf #14 from #16 but the index identified #14 as being from an “unhealthy” plant.  Given the relatively wide spectrum we are working with here, this is a remarkable result – suggesting that with a bit of homework to find LED’s with tighter detection bands, we could produce an inexpensive chlorophyll meter (like the SPAD?) or we could tune the idea for other pigments/applications. It should be possible to at least match the performance of the leaf color charts currently being used to assess when fertilizer is needed. Using a white LED emitter above the leaf could enable a variant of our simple approach that was not dependent on the sun, and adding couple of reads using only green & red emitters above the leaf would enable us to distinguish which side of that wide detection curve we were on. Commercial chlorophyll meters sometimes use emitters at 660 nm & 940 nm , measuring both reflectance AND transmission so an accurate absorbance value can be calculated. Even then plants cease to create chlorophyll once a certain threshold has been reached, so these meters are often used to detect deficiencies (by comparison to a well fertilized control group) rather than concentrations.

And finally, a classroom lab could combine this kind of index-based characterization with paper chromatography to verify the pigments in the leaves via their Rf factors. A more advanced approach would do this quantitatively, turning this index into a true diagnostic tool. This could also be a good accompaniment to the many other Arduino based plant monitoring projects, as a growth or health verification stage. In northern climates this could even be done with house plants by taking readings ‘before’ and ‘after’ some experimental intervention. With those large temporal separations, you would want to take new max/min readings each time for the scaling & avoid using artificial light sources as these  introduce frequency artifacts that could interfere with the index.

Addendum 2020-10-28

“The same procedure is used for oxygen saturation measurement. Here the principle is to measure the absorption of the hemoglobin in the blood. Oxygenated hemoglobin (HbO2) has a significantly different absorption of light than non-oxygenated hemoglobin (Hb). To detect this difference, the skin is illuminated with one red and one IR LED light and a photodetector measures the absorption.”

How to make Resistive 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 data 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 more precise 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 resistor, 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 at 0.66*Vcc rather than a level set through the comparator. That’s almost the same as one RC time constant (63%) 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:

#include <avr/sleep.h>              //  to sleep the processor
#include <avr/power.h>            //  for peripherals shutdown
#include <LowPower.h>            //  https://github.com/rocketscream/Low-Power

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


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 discharge and then repeat the process a second time with D7 & D8 in INPUT mode, and D9 HIGH to charge the capacitor through the 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 with a thermistor. To figure out your 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 14-bit resolution, 10.8 msec/read ] 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 ±0.1 °C accuracy 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 longer ints Or you could make you life easy and save the raw elapsedTimeReff & elapsedTimeSensor values and do the calculations later in Excel. Whenever you see a sudden discontinuity where the result of a calculation suddenly takes a big jump to larger or smaller values – then you should suspect a variable type/cast error.

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: Integer arithmetic on the Arduino defaults to 16 bit & never promotes to higher bit calculations, unless you cast one of the numbers to a high-bit integer first. After casting the Arduino supports 64-bit “long long” int64_t & uint64_t integers for large number calculations but they do gobble up lots of program memory space – typically adding 1 to 3k to the compiled 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 across 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  temperature, aging, and other coefficients, but for the accuracy level I’m after many of those factors are absorbed into the S&H coefficients. Even if you pay top dollar for reference resistors it doesn’t necessarily mean they are “Low TC”. That’s why expensive resistors have a temperature compensation curve in the datasheet. What you’re talking about in quality references is usually low long-term drift @ a certain fixed temperature (normally around 20 ~ 25°C) so ‘real world’ temps up at 40°C are going to cause accelerated drift.

The ratio-metric nature of the method means it’s almost independent of value of the capacitor, so you can get away with a cheap ceramic cap even though it’s value changes dramatically with temperature. (& also with DC bias ) In my tests thermal variation of a Y5V causes a delta in the reference resistor count that’s about 1/3 the size of the delta in the NTC thermistor.  Last year I also found that the main system clock was variable to the tune of about 0.05% over a 40°C range, but that shouldn’t be a problem if the reference and the sensor readings are taken immediately after one another. Ditto for variations in your supply. None of it matters unless it affects the system ‘between’ the two counts you are using to make the ratio.

The significant digits from your final calculation depend on the RC rise time, so switching to 100k thermistors increases the resolution, as would processors with higher clock speeds.  You can shut down more peripherals with the PRR to use less power during the readings as long as you leave Timer1 running with SLEEP_MODE_IDLE.  I’ve also found that cycling the capacitor through an initial charge/discharge cycle (through the 300Ω on D8) improved the consistency of the readings. That capacitor shakedown might be an artifact of the ceramics I was using but you should take every step you can to eliminating errors that might arise from the pre-existing conditions. I’ve also noticed that the read order matters, though the effect is small.

Code consistency is always important with time based calibrations no matter how stable your reference is. Using a smaller integrating capacitor makes it more likely that your calibration constants will be affected by variations in code execution time. Any software scheme is going to show some timing jitter because both interrupts and the loop are subject to being delayed by other interrupts. So using a larger 1uF (105) capacitor is a safer option. This method bakes a heap of small systemic errors into those NTC calibration constants, and this approach works because most of those errors are thermal variations too. However code-dependant variations mess with the fit of the thermistor equation as they tend to be independent of temperature, so make sure the deployment uses exactly the same code that you calibrated with.

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 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 drift is for the Pro Mini’s oscillator, and the jitter seen in 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.

In reality, the pull-up “resistor” likely isn’t a real resistor at all, but an active device made out of transistor(s) which looks like a resistor when operated in its active region. 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.

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(3,OUTPUT);digitalWrite(3,LOW);  // ground & drain the cap through 300Ω
LowPower.powerDown(SLEEP_30MS, ADC_OFF, BOD_ON);  // 5T is only 1.5ms w 10k

pinMode(3,INPUT);digitalWrite(3, LOW);    // Now pin D3 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.

Addendum 20191020:

After running more tests of this technique, I’m starting to appreciate that even on regulated systems, you always have about 10-30 counts of jitter in the Timer1 counts, even with the 4x input capture filter enabled.  I suspect this is due to the Shimitt triggers on the digital pins also being subject to noise/temp/etc and because other system interrupts find a way to sneak in. A smaller 104 integrating capacitor you make your readings 10x faster, but the fixed jitter error is a correspondingly larger percentage of that total reading (100nF typically sees raw counts in the 3000 range for the 10k reference). By the time you’ve over-sampled 104 capacitor readings up to a bit-depth equivalent of the single-pass readings with the 105 ceramic capacitor ( raw counts in the 60,000 range for the same 10k ref), you’ve spent about the same amount of run-time getting to that result. (Keep in mind that even with a pair of single-pass readings using the 10k/104 capacitor combination; raw counts of ~3500 yield a jitter-limited thermistor resolution of about 0.01C)

So, as a general rule of thumb, if your raw Timer1 counts are in the 20,000-60,000 range, you get beautiful smooth results no matter what you did to get there. This translates into about 2.5 – 7.5 milliseconds per read, and this seems to be a kind of ‘sweet-spot’ for ICU based timing methods because the system’s timing jitter error is insignificant at that point. With 5 significant figures in the raw data, the graphs are so smooth they make data from the si7051 reference I’m using look like a scratchy mess.

Another thing to watch out for is boards using temperature compensated oscillators for their system clock. ICU methods work better with the crappy ceramic oscillators on clone boards because their poor thermal behavior just gets rolled into the thermistors S&H constants during calibration. However better quality boards like the 8mhz Ultra from Rocket Scream have compensation circuits that kick in around 20C, which puts a weird discontinuity into the system behavior which can not be gracefully absorbed by the thermistor constants.  So the net result is that you get worse results from your calibration with boards using temperature compensation on their oscillators.

The thermistor constants also neatly absorb the tempco and even offset errors in the reference resistor. So if you are calibrating a thermistor for a given logger, and it will never be removed from that machine, you can set your reference resistor in the code to some arbitrary perfect value like 10,000 ohms, and just let the calibration push any offset between the real reference and your arbitrary value into the S&H constants. This lets you standardize the code across multiple builds if you are not worried about  ‘interchangeability’.

And finally, this method is working well on unregulated systems with significant battery supply variations as I test the loggers down to -15C in my freezer.  In addition to battery droop, those cheap ceramic caps have wicked tempcos, so the raw readings from the reference resistor are varying dramatically during these tests, but the ‘Ratio/Relationship’ of the NTC to this reference is remaining stable over a 30-40C range, with errors in the ±0.1°C range, relative to the reference. (Note: si7051 itself has ±0.13°C, so the net is probably around ±0.25°C)

Addendum 20201011:

Re: the temperature coefficient of resistance (TCR) of your reference resistor:

“Using a thin film resistor at ±10 ppm/°C would result in a 100 ppm (0.01%) error if the ambient changes by only 10°C. If the temperature of operation is not close to the midpoint of the temperature range used to quantify the TCR at ±10 ppm/°C, it would result in a much larger error over higher temperature ranges. A foil resistor would only change 0.0002% to 0.002% over that same 10°C span, depending upon which model is used (0.2 ppm/°C to 2 ppm/°C.) And for larger temperature spans, it would be even more important to use a resistor with an inherently low TCR.”

During calibration, I usually bake the reference resistor’s tempco into the thermistor constants by simply assuming the resistor is a ‘perfect 10k ohm’ during all calculations. (this also makes the code more portable between units)  However this does nothing to correct long-term drift in your reference.  If you want to tackle that problem with a something like a $10 Vishay Z-foil resistor (with life stability of ± 0.005 %) then it’s probably also worth adding Plastic film capacitors which have much better thermal coefficients: Polyphenylene sulfide (PPS ±1.5%) or Polypropylene (CBB or PP ±2.5%)A quick browse around the Bay shows those are often available for less than $1 each, and the aging rate (% change/decade hour) for both of those dielectrics is listed as negligible. The trade off is that they are huge in comparison to ceramics, so you are not going to just sneak one in between the pins on your pro-mini. Be sure to check the rated voltage – and don’t order them if they are rated >100v as the common 650v film caps are too big to be practical on small logger builds.  For the coup de grâce you could correct away the system clock variation by comparing it to the RTC

Addendum 2020-03-31:  Small capacitors make this method sensitive to a noisy rail

After a reasonable number of builds I have finally identified one primary cause of timing jitter with this technique: a noisy regulator. To get sleep current down I replace the stock MIC5205’s on clone ProMini boards with MCP1700’s and I noticed a few from a recent batch of loggers were producing noisy curves on my calibration runs. One of them was extreme, creating >0.5°C of variation in the record:

ICU based Thermistor readings VS si7051 reference. Sensors in physical contact with each other. Y axis = Celsius

But in the same batch, I had others thermistors with less noise than the si7051’s I was using as a reference. All were using small 104 ceramic capacitors for the integration, producing relatively low counts (~3500 clock ticks) on the 10k reference resistor.

For the unit shown above I removed the regulator, and re-ran the calibration using 2x Lithium AA batteries in series to supply the rail voltage. No other hardware was changed:

Same unit, after regulator was removed. Samples taken at 1 minute interval on both runs.

In hindsight, I should have guessed a bad regulator was going to be a problem, as few other issues can cause that much variation in the brief interval between the reference resistor & thermistor readings. Reg. noise/ripple translates instantaneously into a variation in the Schmitt trigger point on the input pin – which affects the ICU’s timer count. It’s possible that this issue could be eliminated with more smoothing so I will try a few caps across the rails on the less problematic units. (~1000µF/108 Tantalums can be found for 50¢ on eBay but I will start with a 10µF/106 & work up from there)

Addendum 2020-04-05:  106 ceramic across rails increased ICU reading noise (w bad reg)

Adding a cheap 106 (Y5V ceramic) across the rails more than doubled the noise in the readings of the NTC with this ICU technique.  This is interesting, as it goes completely against what I was expecting to happen.  Possibly that 10µF cap was just badly sized for this job or had some other interaction via inductance effects that actually accentuated the noise? I probably need a smaller, faster cap for the job.

Changing the sampling capacitor from a 104 (0.1µF) , to a 105 (1µF) dramatically reduced the problem. Not surprising as the rail noise from the regulator is relatively consistent, while the reference timer counts change from ~3500 with a 104 capacitor to ~60,000 with the larger 105. So the jitter is still there, but it is proportionally much smaller. I’m currently re-running the thermistor calibration with that larger capacitor. If the gods are kind, the S&H thermistor constants will be the same no matter what sampling capacitor is used.

It’s worth noting that this issue only appeared with the most recent crop of crappy eBay regulators. But if you are sourcing parts from dodgy vendors, you’d best go with a 105 sampling capacitor right from the start to smooth out that kind of noise

Addendum 2020-04-06:  Re-use old S&H constants after changing the sample capacitor?

After discovering the reg. issue, I re-ran a few thermistor calibrations once the sampling capacitor had been changed from a 104 to a 105:  This reveals that the thermistor constants obtained with a 104 sampling capacitor, still work, but it depends on the tolerance you are aiming for: with the older 104 cap constants drifting over a 40°C range by about ±0.3 Celsius. The extra resolution provided by the larger 105 cap is only useful if you have the accuracy to utilize it (ie: It doesn’t matter if the third decimal point is distinguishable of the whole number is wrong) I generally aim for a maximum of ±0.1°C over that range, so for our research loggers that’s a complete do-over on the calibration. From now on I will only use 105 caps (or larger) with this ICU technique on regulated systems. The battery-only units were smooth as silk with smaller 104 caps because the rail had zero noise.

Addendum 2020-05-21: Using the ADS1115 in Continuous Mode for Burst Sampling

For single resistive sensors, it’s hard to beat this ICU method for elegance & efficiency. However there’s still one sensor situation that forces me to go to an external ADC module: Differential readings on bridge sensors.  In those cases I use an ADS1115 module, which can also generate interrupt alerts for other tricks like ‘event’ triggered sampling.

Addendum 2021-01-24        Don’t sleep during the do-while loop with regular I/O pins.

I’ve been noodling around with other discharge timing methods and come across something  that’s relevant to using these methods on other digital pins. Here’s the schematic from the actual 328 datasheet, with a bit of highlighting added. The green path is PINx.  It’s always available to the databus through the synchronizer (except for in SLEEP mode?) The purple path is PORTx.   Whether or not it is connected to PINx depends on the state of DDRx (which is the yellow path.)

As shown in the figure of General Digital I/O, the digital input signal can be clamped to ground at the input of the Schmitt Trigger. The signal denoted SLEEP in the figure, is set by the MCU Sleep Controller in Power-down mode and Standby mode to avoid high power consumption if some input signals are left floating, or have an analog signal level close to VCC/2.

When sleeping, any GPIO that is not used an an interrupt input has its input buffer disconnected from the pin and in clamped LOW by the MOSFET.

Clearly D8 on the ICU must one of those ‘interrupt exceptions’ or the cap discharge would have been grounded out when entering the sleep state.  If you use a similar timing method on normal digital IO pins you can’t sleep the processor in the central do-while loop.

The datasheet also warns that on pins where this is overridden, such as external interrupts, you must ensure these inputs don’t float (page 53). The implication being that when you put anything near 1/2 of the rail voltage on the input’s Schmitt trigger this _will_ cause current to flow into or out of the pin. Yet another source of error that gets rolled into those catch-all S&H constants.