Temperatur sensor function fails with timer, works fine in loop

I’m having a rather odd issue with the temperature sensor bing used in a timer callback. If the temperature callback function is executed in the loop everything works perfectly. However, if the the callback is executed by the timer, the sensor always reads -127. I have no idea why this could happen?

You can see the results on the OLED screen. The full code is quite large, so i’ll start basic and add anything that may be required, but i believe all the relevant bits of code are below the images.

In particle loop everything works

As timer callback (all i did was comment out function in loop and enable checkTempTimer.start() in setup)

It makes no sense to me that a sensor would behave differently as a timer call back vs in the loop function. Any ideas appreciated…

CONFIG

// CONFIG OLED
#define OLED_CLK     D0
#define OLED_MOSI    D1
#define OLED_RESET   D2
#define OLED_DC      D3
#define OLED_CS      D4
Adafruit_SSD1306 display(OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS);

// CONFIG TEMPERATURE SENSOR
int tempSensorPin = A0;
OneWire oneWire(tempSensorPin);
DallasTemperature temp(&oneWire);

// APP GLOBALS
double tempC = 0.0;
double tempF = 0.0;
String lastTempTime = "";
String lastTempCheck = "";

// TIMERS
Timer publishTempTimer(5000, publishTemp);
Timer checkTempTimer(2000, checkTemp); // Timer fires callback, but temp data is -127

FUNCTIONS (other functions omitted for brevity)

void checkTemp () {
    temp.requestTemperatures();
    double tempCheck = temp.getTempCByIndex(0);
    lastTempCheck = String(tempCheck);
    lastTempTime = getTime();
    if ( tempCheck > -50 && tempCheck < 50) 
    {
        tempC = tempCheck;
        tempF = tempC * 9.0 / 5.0 + 32.0;
    }
}

PARTICLE SETUP

void setup() {
    
    Serial.begin(9600);
    
    // OLED
    display.begin(SSD1306_SWITCHCAPVCC);
    showSplash("loading...", 0); // custom splash screen

    // Temp
    temp.begin();
    checkTempTimer.start(); //disable for loop checkTemp
    publishTempTimer.start();
}

PARTICLE LOOP

void loop() {

   # Check for button class HIGH state and show splash screen
    if (btnSplash.isOn()){
        showSplash("button pressed", 0);
    }else{
        displayTemp(lastTempTime+" = "+lastTempCheck); 
    }

    // Comment out checkTempTimer.start() and uncomment line below end everything works
    //checkTemp();
}

Timers behave like interrupts and should thus be kept as short as possible. Use them to set flags to act upon in the loop, but don’t do any time consuming tasks in them (calling other functions).

2 Likes

Ok, thanks. I used a mills timer in my check temp function (i dont need extream accuracy). are there any tech details on how timers and interupts work exactly? Im trying to create a non-blocking loop so that buttons have a snappy response.

My other timer sends two Particle.publish commands and that seems to work fine. I dont see how a sensor read can be affected.

If you want snappy button response, you can use interrupts for them.
And the reason for the bad readings might be that the OneWire communication doesn’t play well with the seperate thread for timers.

Assuming they work like interrupts then any global variables you are setting inside the interrupt and reading outside it in the loop, or in your publishTemp function might need to be declared volatile, see:

EMBED WITH ELLIOT: THE VOLATILE KEYWORD

2 Likes

Thanks @ ScruffR. I am using an interrupt for the button but there still seems to be a delay at times which i assumed was during a temp check cycle. So i tried the timer implementation, i assumed it was threaded. Moving the temp check to the timer did solve the random delay issue, but as noted the sensor failed.

So the larger question about interrupts is, they seem to interrupt the loop, but do not interrupt code running external to the loop ie functions? Do you know it that is a correct assessment?

@chrisb2 I will try setting the temp globals to volatile and see if that helps.

1 Like

@arctelix, this is a tough one. Software timers on the Photon are implemented using FreeRTOS software timers. All timers run in a shared thread, separate from the user app thread. Since the timers share the same thread, within that thread the run “round robin” style meaning when a timer fires, it will run the user defined callback and only when the callback returns will the next timer be evaluated. This means that if a callback hogs a lot of time or takes longer than the timer’s interval, timing events fail. The best case timer resolution is 1ms.

I suspect that the dallas/onewire code is no happy being run this way since it is entirely bit-banged and timing is critical. Frankly, this is an area of software timers I have not investigated myself. It may be possible to run the sensor code in its own unique thread though this would need to be tested. I will be looking into this over the next few days.

BTW, in your code, you call getTime(). Can you share the code for that?

1 Like

@peekay123 thanks for the reply. getTime() is just a string time stamp for the OLED output, its not used as a time based function at all.

String getTime() {
    String h = String(Time.hour());
    String m = String(Time.minute());
    String s = String(Time.second());
    return h+":"+m+":"+s;
}

You might want to use Time.format("%H:%M:%S") or just Time.format("%T")

See
http://www.cplusplus.com/reference/ctime/strftime/

Ah, yes. That is better.

1 Like

@arctelix, @ScruffR, I ran some tests yesterday using the timer callback for reading the ds18b20 temperature. I streamlined the code to remove String operations in the callback and added some counters to keep track of total and bad readings from the sensor. All tests were compiled on Build with a 0.6.0-rc.2 target version. The test app prints to Serial since I don’t have an OLED display. I never enabled the Particle.publish() timer during the tests.

Running the code from loop() I found that there were 1400 error out of 22000 total readings (6.3% error rate) which seems ok for this onewire sensor. Enabling the software timer to trigger the sampling, the error rate jumps to about 50%.

The most interesting thing came when I enabled system threading. With it enabled, the timer-based sampling failed 100% of the time. I have no idea why that is the case. I believe the timer and user app threads run at the same priority and I wonder if this causes problems. Perhaps @mdma can lend his expertise here.

Here is the code I used:

// This #include statement was automatically added by the Particle IDE.
#include "OneWire/OneWire.h"

// 18B20 Temperature Sensor demo using One Wire// This #include statement was automatically added by the Particle IDE.
#include "spark-dallas-temperature/spark-dallas-temperature.h"

//SYSTEM_THREAD(ENABLED);

// CONFIG TEMPERATURE SENSOR
int tempSensorPin = A0;
OneWire ds18b20(tempSensorPin);
DallasTemperature temp(&ds18b20);

// APP GLOBALS
volatile float tempC = 0.0;
volatile float tempF = 0.0;
volatile int lastTempTime = 0;
volatile float lastTempCheck = 0;
volatile bool newTempAvail = false;

unsigned long tTemp = 0;
volatile unsigned long tconv = 0;
unsigned long totConvCount = 0;
unsigned long totErrCount = 0;


// TIMERS
Timer publishTempTimer(5000, publishTemp);
Timer checkTempTimer(2000, checkTemp); // Timer fires callback, but temp data is -127


void checkTemp() {

    totConvCount++;
    tconv = micros();
    temp.requestTemperaturesByIndex(0);

    float tempCheck = temp.getTempCByIndex(0);
    lastTempCheck = tempCheck;
    lastTempTime = Time.now();
    if ( tempCheck != -127) 
    {
        tempC = tempCheck;
        tempF = (tempC * 0.0140625) + 32;
    }
    else {
        totErrCount++;
    }
    newTempAvail = true;
    tconv = micros() - tconv;

}


void publishTemp() {
    char buf[100];
    sprintf(buf, "tempC:%4.2fF tempF:%4.2fC  lastTemp:%4.2f lastTime:%s", tempC, tempF, lastTempCheck, Time.format(lastTempTime, "%T").c_str());
    Particle.publish("PMK_TempTest", buf, 60, PRIVATE);
}


void setup() {
    
    Serial.begin(9600);
    
    // Temp
    temp.begin();
    //checkTempTimer.start(); //disable for loop checkTemp
    //publishTempTimer.start();
    
}


void loop() {

    if (millis() - tTemp > 2000) {
        tTemp = millis();
        
        // Comment out checkTempTimer.start() and uncomment line below end everything works
        checkTemp();
        Serial.println(tconv);

        if (newTempAvail) {
            newTempAvail = false;
            Serial.printlnf("conversion time:%u ms  total conversions:%u  total errors:%u", tconv, totConvCount, totErrCount);
            Serial.printlnf("tempC:%4.2fF tempF:%4.2fC  lastTemp:%4.2f lastTime:%s", tempC, tempF, lastTempCheck, Time.format(lastTempTime, "%T").c_str());
        }
    }
}
3 Likes

@peekay123 Thanks for taking the time to verify the issue so precisely!

2 Likes

What conversion times were you getting (approx)?

I tried another experiment, where i used a timer to output the data to the OLED and checked the temperature inside the loop. The tempC and tempF variables always showed “0” and the photon errors. IT appears that the temp variables are not read from inside the timer thread fro some reason. Perhapse the timer thread is isolated from the global scope?

I guess @peekay123 has set all used variables (excluding the ones used in the library itself) to volatile for his test. Have you too?

1 Like

The problem is probably that DS18B20 use time critical parts of the code.
I always about timers who call another complex function or library use following template below.
Direct call I use only for simple functions.
I customize template for your application and you can try :smile:


// TIMERS
Timer publishTempTimer(5000, publishTemp_ISR);
Timer checkTempTimer(2000, checkTemp_ISR); 

bool state_publishTempTimer = false;
bool state_checkTempTimer = false;

void checkTemp_ISR() 
{
  state_checkTempTimer = true;
}

void publishTemp_ISR()
{
   state_publishTempTimer = true;
   
}

void checkTemp() {
   
   state_checkTempTimer = false; 
    
    /*
    code 
    
    */
}

void publishTemp()
{
    state_publishTempTimer = false;
    
    /*
       code 
    */
}

void TIMER_RUN()
{
  	if(state_checkTempTimer == true)
    {
        checkTemp();
    }  
    
  if(state_publishTempTimer == true)
    {
        publishTemp();
    }
    
}

void setup() 
{
    
    checkTempTimer.start(); 
    publishTempTimer.start();
}

void loop() 
{

TIMER_RUN();

/*
code 

*/
}


I did.

@developer_bt, What you suggested is the recommended thing to do. However, I’m searching for a way to create a totally non-blocking loop. The temperature read takes enough time that there is a noticeable delay in the loop which interferes with button interrupts when they are triggered during a temperature read.

For the buttons you need external interruptions? Or already have in the code?
https://docs.particle.io/reference/firmware/photon/#interrupts
It also has the option of reducing the time of reading DS18b20. But it also reduces the resolution of the sensor. It is made with the following code in setup:

//Temp
temp.begin();
temp.setResolution(9);

    Information from DS18B20 datasheet
Mode	Resol.        Conversion time
9 bits	0.5°C	        93.75 ms
10 bits	0.25°C	        187.5 ms
11 bits	0.125°C	        375 ms
12 bits	0.0625°C	750 ms

Also you have the opportunity to check buttons in a separate thread.
This I do not know for sure whether it will work because this is still in beta version.

    SYSTEM_THREAD(ENABLED);
    
    ApplicationWatchdog wd(60000, System.reset);
    
    Thread hardwareThread_1;
    
    
    void check_Button() 
    {
      while(true)
      {
         //Your code for buttons
      }
    
    }
    
    void setup() {
    
      hardwareThread_1 = Thread("hardware1", check_Button, OS_THREAD_PRIORITY_DEFAULT); 
    }
    
    void loop()
    {
      
    }

Just a suggestion, as it’s not something that I’ve tried, but I wonder whether if you were to remove the dallas library and get the temperature using OneWire yourself (it’s about 150 lines of code) then you could perform the one-second conversion delay as a soft delay (like: while millis() not greater than …, rather than delay(1000)) during which you’d be able to do other things like check whether a button has been pressed. You ought to be able to run it all within loop() if you write it as a multi-state program with a switch statement.