Hidden Response Accuracy in the ATMEGA328P

UPDATED – See Below

Some people may have known already about the temperature sensor built into the ATMEGA8, 168, 328 series microcontrollers, but the most serious enthusiast would write off the 1°C resolution as just a coarse sensor of no practical use.   I would have to say I was part of that group, until I compared the response between the two sensors.

The TI LM35DZ is a simple temperature sensor that has an accuracy of +/-0.5°C at 25°C and with 0.08°C of self heating, this should be sufficient to demonstrate the rough nature of the ATMEGA328’s internal temperature when compared to dedicated external sensor.  The TO-92’s have the best Thermal Reponses on the datasheets and are the cheapest.   Connection is straight-forward, +5VDC, Vout and Gnd with Vout to the ATMEGA328’s PC0 (Arduino pin A0).

For the ATMEGA328 internal sensor, adjustment of the voltage offset can be used to calibrate to another sensor.

The Arduino IDE sketch is as follows:

//Internal and External Temperature Sensor Sketch

int sample_count;
float val;
float sample_avg;
float temp;

void setup()
{
Serial.begin(9600);

Serial.println(F(“Internal Temperature Sensor”));
analogReference(INTERNAL);
}

void loop()
{
// Show the temperature in degrees Celcius.
Serial.print(GetTemp(),1);
Serial.print(F(” “));
Serial.println(GetLM35(),1);
delay(1000);
}

double GetTemp(void)
{
unsigned int wADC;
double t;

// The internal temperature has to be used
// with the internal reference of 1.1V.
// Channel 8 can not be selected with
// the analogRead function yet.

// Set the internal reference and mux.
ADMUX = (_BV(REFS1) | _BV(REFS0) | _BV(MUX3));
ADCSRA |= _BV(ADEN); // enable the ADC

delay(20); // wait for voltages to become stable.

ADCSRA |= _BV(ADSC); // Start the ADC

// Detect end-of-conversion
while (bit_is_set(ADCSRA,ADSC));

// Reading register “ADCW” takes care of how to read ADCL and ADCH.
wADC = ADCW;

// The offset of 324.31 could be wrong. It is just an indication.
t = (wADC – 326.31 ) / 1.22;

// The returned temperature is in degrees Celcius.
return (t);
}

double GetLM35(void)
{
sample_count++;
val = analogRead(0);
sample_avg = sample_avg + (val – sample_avg) / sample_count;

temp=(1.1 * sample_avg * 100.0) / 1024.0;
return (temp);
}

Both devices were within an inch or two and subjected to a 40 watt clip lamp for heating and a small space fan for cooling.  The response curve is as follows (one sample per second):

Atmega-LM35

While the precision of the ATMEGA328 internal sensor is to be desired, the temperature response is quite dramatic.  The LM35DZ’s cooling response of 1°C per 10 minutes could be an issue with some projects.

Update 21 Jan 14:

Scott Teal brought up an issue concerning the smoothing algorithm.  I retested including a no smoothing sample and the results for the LM35DZ are much better.  With a slope correction factor of the ATmega328P I believe the LM35DZ will correlate very well.

Atmega-LM35-2

Update 2 21Jan14:

The ATmega328 correlation test is as follows:

Atmega-LM35-3

Update 3 – 22Jan14:

Little more tuning and the Ring Buffer Included…

Atmega-LM35-4

Pretty good match!

Advertisements

Posted on January 19, 2014, in ATmega328. Bookmark the permalink. 13 Comments.

  1. Perhaps I’m missing something, but the GetLM35 function looks flawed. It looks like you tried to take an average value over many samples, but that’s not what happened. You sample_count variable increases indefinitely, and sample_avg thus changes less and less regardless of what val is set to.

    Try using a ring buffer perhaps:

    const BUF_SIZE = 8;
    int lm35_values[BUF_SIZE]; // Ring buffer
    int lm35_index = 0; // Buffer location
    int sample_total = 0;

    double GetLM35(void)
    {

    val = analogRead(0);

    // Moving total of samples in buffer
    sample_total = sample_total + val;
    sample_total = sample_total – lm35_values[lm35_index];
    lm35_values[lm35_index] = val;

    // Increment index
    lm35_index++;
    if (lm35_index >= BUF_SIZE)
    {
    lm35_index = 0;
    }

    temp=(1.1 * sample_avg * 100.0) / (1024.0 * BUF_SIZE);
    return (temp);
    }

    This should allow you to do a proper moving average. Hope that made sense, and that I’m not completely misunderstanding your code.

    • Whoops, that should be

      temp=(1.1*sample_total*100)/(1024*BUF_SIZE);

      • Hmmm…I tried to compile your suggestion and I finally got goose-eggs. Their were some unseen WordPress Unicodes that I fixed and the only other change was to “const BUF_SIZE = 8;”, the compiler wanted a type, so I put “int”.

        //Internal and External Temperature Sensor Sketch

        int sample_count;
        float val;
        float sample_avg;
        const int BUF_SIZE = 8;
        int lm35_values[BUF_SIZE]; // Ring buffer
        int lm35_index = 0; // Buffer location
        int sample_total = 0;
        float temp;
        float temp2;

        void setup()
        {
        Serial.begin(9600);

        Serial.println(F("Internal Temperature Sensor"));
        analogReference(INTERNAL);
        }

        void loop()
        {
        // Show the temperature in degrees Celcius.
        Serial.print(GetTemp(),1);
        Serial.print(F(" "));
        Serial.print(GetLM35(),1);
        Serial.print(F(" "));
        Serial.println(GetLM35raw(),1);
        delay(1000);
        }

        double GetTemp(void)
        {
        unsigned int wADC;
        double t;

        // The internal temperature has to be used
        // with the internal reference of 1.1V.
        // Channel 8 can not be selected with
        // the analogRead function yet.

        // Set the internal reference and mux.
        ADMUX = (_BV(REFS1) | _BV(REFS0) | _BV(MUX3));
        ADCSRA |= _BV(ADEN); // enable the ADC

        delay(20); // wait for voltages to become stable.

        ADCSRA |= _BV(ADSC); // Start the ADC

        // Detect end-of-conversion
        while (bit_is_set(ADCSRA,ADSC));

        // Reading register "ADCW" takes care of how to read ADCL and ADCH.
        wADC = ADCW;

        // The offset of 324.31 could be wrong. It is just an indication.
        t = (wADC - 313.5 ) / 1.9;

        // The returned temperature is in degrees Celcius.
        return (t);
        }

        double GetLM35(void)
        {

        val = analogRead(0);

        // Moving total of samples in buffer
        sample_total = sample_total + val;
        sample_total = sample_total - lm35_values[lm35_index];
        lm35_values[lm35_index] = val;

        // Increment index
        lm35_index++;
        if (lm35_index >= BUF_SIZE)
        {
        lm35_index = 0;
        }

        temp=(1.1 * sample_avg * 100.0) / (1024.0 * BUF_SIZE);
        return (temp);
        }

        double GetLM35raw(void)
        {
        val = analogRead(0);
        temp2=(val * 1.1 * 100) / 1024;
        return (temp2);
        }

        Anything obvious?

      • I almost missed it in your code, but you’re still using sample_avg by mistake in the line:

        temp=(1.1 * sample_avg * 100.0) / (1024.0 * BUF_SIZE);

        It should be sample_total now. You can get rid of “float sample_avg” too. I *think* that should give something reasonable after the first BUF_SIZE samples, but I can’t run it here to verify.

      • That was it. Some null data sag, but the Ring Buffer moves out of it quickly. The Ring Buffer has very good response; I will have to test it on something really noisy. Thanks again.

    • Good point. After your comment, I thought to verify the reason of using a smoothing operation at all. So I added a non-smoothed measurement and the LM35 performed +/-0.5% with a response that will correlate very well with slope adjustment on the ATMEGA328P. At least the post title is still good 😉

      I will compare your smoothing sketch later tonight. Thank for the input!

      • Cool, your new graphs make a lot more sense to me. It is interesting to see how the two sensors respond differently to ambient temperature changes and direct heating from a lamp. What’s especially cool about your graphs is the way the onboard sensor transitions between values…looks kind of like sigma-delta modulation. A digital low-pass filter could probably give you a really close estimate, provided the temperature changes little over the course of a minute or so.

  2. I am trying this with an Arduino Micro, which has an ATmega32u4. According to the spec sheet, it also has an on-board temperature sensor. However, when I try to read register ADCW, I get an “‘ADCW’ was not declared in this scope” error.

    Being very new to Arduino programming and C (I’m more of a MATLAB person), what should I be looking for in the data sheet to find the appropriate register to read the temperature for my chip?

  1. Pingback: Hidden Response Accuracy in the ATmega328P « adafruit industries blog

  2. Pingback: ATMEGA32U2 and LM35DZ Temperature Responses | Synthetic Physical Computing

  3. Pingback: ATMEGA328P Internal Temperature Sensor Bias | Synthetic Physical Computing

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: