Particle Reading Analog & Digital Inputs

Analog Inputs

ADC Calibration


/*
 * Project      analog_input_calibration
 * Description: Calibrate analog input channels A0..A1 using precision voltage reference
 *              (Adafruit product #2200) and then calculates the ADC calibration offset
 *              to apply to the measured analog input ADC values. 
 * Author:      Mark Kiehl / Mechatronic Solutions LLC
 * Date:        May 2020
 */


/////////////////////////////////////////////////////////////////////////
unsigned long pubCount = 0;
unsigned long samples = 0;
unsigned long A0ADC = 0;
unsigned long A1ADC = 0;

struct analog_input_calib_ADC_t {
  byte pin;
  byte ADCoffset;
};
analog_input_calib_ADC_t arr_ai_ADC_calib[2];

// Fastest Particle publish inteval is 1 event/sec.
// Publish one analog input's value every 15 sec or longer in order to stay 
// within the free limits of most services.
unsigned long timerPublishInterval = 15000;  
unsigned long timerPublishLast = 0;  // timer
/////////////////////////////////////////////////////////////////////////
unsigned int A1ADCdeltaAvg = 0; 
unsigned int A0ADCdeltaAvg = 0; 
//////////////////////////////////////////////////////////////////////////////


void setup() {
  Mesh.off(); // Turn the Mesh Radio off
  
  pinMode(D7, OUTPUT);
  pinMode(A0, INPUT);
  pinMode(A1, INPUT);

  Serial.begin();
  waitFor(Serial.isConnected, 30000);
  delay(1000);
  Serial.printlnf("System version: %s", (const char*)System.version());

  // Initialize arr_ai_ADC_calib
  arr_ai_ADC_calib[0].pin = A0;
  arr_ai_ADC_calib[0].ADCoffset = 0;  // set = 0 to perform calibration.  Set = avg delta after calibration.
  arr_ai_ADC_calib[1].pin = A1;
  arr_ai_ADC_calib[1].ADCoffset = 0;  // set = 0 to perform calibration.  Set = avg delta after calibration.

} // setup()


void loop() {
  
  ReadAnalogInputs();

  // Calculate analog input avg mV values and write them to serial monitor
  if (timerPublishLast > millis())  timerPublishLast = millis();
  if ((millis() - timerPublishLast) > timerPublishInterval) {
    CalcAvgADCdelta();
    timerPublishLast = millis();
  } // timer

} // loop()


void ReadAnalogInputs() {
    // 12 bit ADC (values between 0 and 4095 or 2^12) or a resolution of 0.8 mV
    // Hardware minimum sample time to read one analog value is 10 microseconds. 
    // Raw ADC values are adjusted by a calibration factor arr_ai_ADC_calib[n].ADCoffset
    // That is determined by bench testing against precision voltage reference. 
    A0ADC += analogRead(A0) + arr_ai_ADC_calib[0].ADCoffset;
    A1ADC += analogRead(A1) + arr_ai_ADC_calib[1].ADCoffset;
    samples++;
} // ReadAnalogInputs()


void CalcAvgADCdelta() {
  // Calculate average difference from the average analog input A0..A1
  // ADC value from the precision voltage reference value. 

  if (samples %2)
    digitalWrite(D7, HIGH);
  else
    digitalWrite(D7, LOW);

  pubCount++;
  if (pubCount > 1) {
    // Calculate and report the delta from 2048
    A0ADCdeltaAvg += int(2048.0 * 4096.0 / 3300.0) - int(double(A0ADC)/double(samples));
    A1ADCdeltaAvg += int(2048.0 * 4096.0 / 3300.0) - int(double(A1ADC)/double(samples));
    // int(2048.0 * 4096.0 / 3300.0) = ADC value for 2048 mV from precision voltage reference
    Serial.printlnf("%u pubCount, %u samples, %d ADC0 avg delta, %d ADC1 avg delta", pubCount, samples, int(double(A0ADCdeltaAvg)/double(pubCount-1)), int(double(A0ADCdeltaAvg)/double(pubCount-1)));
  }
  A0ADC = 0;
  A1ADC = 0;
  samples = 0;
} // PublishAnalogValue()

Install CLI, run, then execute the command particle serial monitor to see the serial output.  

Analog to digital conversion using DMA for Particle Gen 3 devices (Argon, Boron, Xenon)

 

Variable Analog Input Sampling With ADC Calibration Applied


/*
 * Project      calibrated_analog_input
 * Description: Read analog input channels A0..A1 as quickly as posssible
 *              and offset the raw ADC values based on the measured
 *              calibration offset determined previously using
 *              'analog_input_calibration.ino' using a precision voltage
 *              reference (Adafruit product #2200). 
 *              The ADC calibration offset is hard coded in arr_ai_ADC_calib[#].ADCoffset
 * Author:      Mark Kiehl / Mechatronic Solutions LLC
 * Date:        May 2020
 */

/////////////////////////////////////////////////////////////////////////
unsigned long pubCount = 0;
unsigned long samples = 0;
unsigned long A0ADC = 0;
double A0mV = 0.0;
unsigned long A1ADC = 0;
double A1mV = 0.0;

struct analog_input_calib_ADC_t {
  byte pin;
  byte ADCoffset;
};
analog_input_calib_ADC_t arr_ai_ADC_calib[2];

// Fastest Particle publish inteval is 1 event/sec.
// Publish one analog input's value every 15 sec or longer in order to stay 
// within the free limits of most services.
unsigned long timerPublishInterval = 15000;  
unsigned long timerPublishLast = 0;  // timer
/////////////////////////////////////////////////////////////////////////


void setup() {
  Mesh.off(); // Turn the Mesh Radio off
  
  pinMode(D7, OUTPUT);
  pinMode(A0, INPUT);
  pinMode(A1, INPUT);

  Serial.begin();
  waitFor(Serial.isConnected, 30000);
  delay(1000);
  Serial.printlnf("System version: %s", (const char*)System.version());

  // Initialize arr_ai_ADC_calib
  arr_ai_ADC_calib[0].pin = A0;
  arr_ai_ADC_calib[0].ADCoffset = 0;  // set = 0 to perform calibration.  Set = avg delta after calibration.
  arr_ai_ADC_calib[1].pin = A1;
  arr_ai_ADC_calib[1].ADCoffset = 0;  // set = 0 to perform calibration.  Set = avg delta after calibration.

} // setup()


void loop() {
  
  ReadAnalogInputs();

  // Calculate analog input avg mV values for A0..A1 and write them to the serial monitor
  if (timerPublishLast > millis())  timerPublishLast = millis();
  if ((millis() - timerPublishLast) > timerPublishInterval) {
    PublishAnalogInputValues();
    timerPublishLast = millis();
  } // timer

} // loop()


void ReadAnalogInputs() {
    // 12 bit ADC (values between 0 and 4095 or 2^12) or a resolution of 0.8 mV
    // Hardware minimum sample time to read one analog value is 10 microseconds. 
    // Raw ADC values are adjusted by a calibration factor arr_ai_ADC_calib[n].ADCoffset
    // that is determined by bench testing against a precision voltage reference. 
    A0ADC += analogRead(A0) + arr_ai_ADC_calib[0].ADCoffset;
    A1ADC += analogRead(A1) + arr_ai_ADC_calib[1].ADCoffset;
    samples++;
} // ReadAnalogInputs()


void PublishAnalogInputValues() {
  // Publish the analog values A0 .. A1 to the Particle Cloud.
  // Sequence from one input to the next with each publish to
  // prevent simultaneous publishing within a short period. 

  if (samples %2)
    digitalWrite(D7, HIGH);
  else
    digitalWrite(D7, LOW);

  pubCount++;
  if (pubCount > 1) {
    A0mV = double(A0ADC)/double(samples)*3300.0/4096.0;
    A1mV = double(A1ADC)/double(samples)*3300.0/4096.0;
    Serial.printlnf("%u pubCount, %u samples, %.1f mV avg A0, %.1f mV avg A1", pubCount, samples, A0mV, A1mV);
  }
  A0ADC = 0;
  A1ADC = 0;
  samples = 0;
} // PublishAnalogValue()

Install CLI, run, then execute the command particle serial monitor to see the serial output.  

Digital Inputs

Monitor a set of digital inputs and publish each LOW/HIGH or HIGH/LOW change to the Particle Cloud.  


#if (PLATFORM_ID == PLATFORM_XENON)
SYSTEM_MODE(MANUAL);
SYSTEM_THREAD(ENABLED);
#endif

// Timer for publishing to the Particle Cloud
const unsigned long TIMER_PUBLISH_INTERVAL_MS = 2000; 
unsigned long timerPublishLast = 0;  
const unsigned long iPubErrCount = 0;

// Bundle all of the digital input data into a structure.
const byte DI_COUNT = 2;
// initialize DI_DEFAULT_STATE LOW if pulldown resistor, HIGH if pullup resistor.
// Must use the same LOW / HIGH (pullup / pulldown) for all digital inputs monitored.
const byte DI_DEFAULT_STATE = LOW;
struct digital_inputs_t {
  byte pin;
  byte state;
  byte last_state;
  unsigned long timer_interval;
  unsigned long timer_last;
  unsigned long hi_lo_ms; // Duration between HIGH / LOW
  boolean alarm;
  unsigned long alarm_last_ms;
};
digital_inputs_t arr_di[DI_COUNT];


void setup() {
  Mesh.off(); // Turn the Mesh Radio off

  pinMode(D7, OUTPUT);

  Serial.begin();
  waitFor(Serial.isConnected, 30000);
  Serial.printlnf("System version: %s", (const char*)System.version());

  if (PLATFORM_ID != PLATFORM_XENON)
    // System.freeMemory() shows available RAM (not FLASH).
    Particle.publish("Free RAM", String::format("%d",System.freeMemory()), PRIVATE);

  // Initialize arr_di
  arr_di[0].pin = D5;
  arr_di[1].pin = D6;
  for (int i=0; i<DI_COUNT; i++) {
    if (DI_DEFAULT_STATE == LOW) {
      pinMode(arr_di[i].pin, INPUT_PULLDOWN);
      arr_di[i].state = LOW;
      arr_di[i].last_state = LOW;
    } else {
      pinMode(arr_di[i].pin, INPUT_PULLUP);
      arr_di[i].state = HIGH;
      arr_di[i].last_state = HIGH;
    }
    arr_di[i].timer_interval = 100; // debounce timer ms
    arr_di[i].timer_last = millis();
    arr_di[i].hi_lo_ms = 0;
    arr_di[i].alarm = false;
    arr_di[i].alarm_last_ms = millis();
  }

} // setup()

void loop() {

  ProcessDigitalInputs();
  PublishDiEvents();

} // loop()


void ProcessDigitalInputs() {
  // Look for a change in state (HIGH/LOW) for the digital inputs referenced by arr_di
  // WARNING:   Multiple DI events that occur in a duration less than TIMER_PUBLISH_INTERVAL_MS
  //            will be missed and only reported as a single event.
  for (int i=0; i<DI_COUNT; i++) {
    if (arr_di[i].timer_last > millis())  arr_di[i].timer_last = millis();
    arr_di[i].state = digitalRead(arr_di[i].pin);
    if (arr_di[i].state != arr_di[i].last_state && millis() - arr_di[i].timer_last > arr_di[i].timer_interval) {
      // do something with the change in state for arr_di[i].pin
      if (arr_di[i].state == !DI_DEFAULT_STATE) {
        arr_di[i].alarm = true; // Causes the alarm to be published by PublishDiEvents()
        digitalWrite(D7, !DI_DEFAULT_STATE);
        arr_di[i].hi_lo_ms = millis();
      } else {
        digitalWrite(D7, DI_DEFAULT_STATE);
        arr_di[i].hi_lo_ms = millis() - arr_di[i].hi_lo_ms;
      }
      // Update the state and timer for arr_di[i]
      arr_di[i].last_state = arr_di[i].state; 
      arr_di[i].timer_last = millis();
    } 
  }
}  // ProcessDigitalInputs()


void PublishDiEvents() {
  // Publish the oldest alarm, indicated by arr_di[#].alarm to the Particle Cloud.
  if (timerPublishLast > millis())  timerPublishLast = millis();
  if ((millis() - timerPublishLast) > TIMER_PUBLISH_INTERVAL_MS) {
    // Find the oldest alarm
    unsigned long oldest_ms = 0;
    byte oldest_alarm = 255;
    for (int i=0; i<DI_COUNT; i++) {
      if (arr_di[i].alarm && millis() - arr_di[i].alarm_last_ms > oldest_ms) {
        oldest_ms = millis() - arr_di[i].alarm_last_ms;
        oldest_alarm = i;
      }
    }  // for
    if (oldest_alarm == 255) {
      Serial.printlnf("%u No alarms to publish", millis());
    } else {
      // Publish the oldest alarm
      Serial.printlnf("Pin D%d alarm %u ms",arr_di[oldest_alarm].pin, arr_di[oldest_alarm].hi_lo_ms);
      arr_di[oldest_alarm].alarm_last_ms = millis();
      if (PLATFORM_ID == PLATFORM_XENON) {
          arr_di[oldest_alarm].alarm = false;
      } else {
        byte iPubResult = Particle.publish(String::format("D%d",arr_di[oldest_alarm].pin), String::format("%u",arr_di[oldest_alarm].hi_lo_ms), PRIVATE);
        if (iPubResult == 1) {
          arr_di[oldest_alarm].alarm = false;
        }
      }
    }
    timerPublishLast = millis();
  } // timer
} // PublishDiEvents()

 

 


Do you need help developing or customizing a IoT product for your needs?   Send me an email requesting a free one hour phone / web share consultation.  

 

The information presented on this website is for the author's use only.   Use of this information by anyone other than the author is offered as guidelines and non-professional advice only.   No liability is assumed by the author or this web site.