Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

Measuring Temperature using the dsPIC Charge Time Measurement Unit (CTMU)

0.00/5 (No votes)
6 Apr 2023CPOL5 min read 4.3K  
How to measure temperature using the dsPIC CTMU

In an attempt to save an I/O pin originally used for the DHT11 to measure ambient temperature in one of my projects, I attempted to do the same using the dsPIC Charge Time Measurement Unit (CTMU) instead. After studying the dsPIC33EP512MC502 datasheet as well as Microchip’s DS61167B/TB3016 documents on how the CTMU can be used to measure the PIC’s die temperature, I tried to search for sample codes but could not find any useful code snippets. Attempts to post question on the Microchip forum would only result in replies from unfriendly folks telling me to read the manual and referring me back to the datasheet. Well, it took me a day to come up with an implementation that works, which I will share here for those who are interested.

The theory behind the measurement of temperature is described in Microchip’s TB3016 as well as this link. Granted, this is the PIC’s core temperature and not the ambient temperature which is measured by the DHT11, but unless the PIC is running hot, both measurements should approximate each other. Basically, the process involves connecting a diode to one of the PIC’s A/D pins, applying a constant current source using the CTMU, and finally calculating the temperature from the voltage across the diode. For the dsPIC, the diode is built-in so all you need to do is to write codes to activate the CTMU as well as the ADC in order to perform the measurements. The following code shows how to initialize the ADC (modify it to suit your needs):

C++
void initADC()
{
    AD1CON1bits.ADON = 0;          // A/D converter is OFF

    // disable input scan
    AD1CSSH = 0;
    AD1CSSL = 0;

    // 1 = 12-bit, 1-channel ADC operation
    // 0 = 10-bit, 4-channel ADC operation
    AD1CON1bits.AD12B = 1;         

    AD1CON1bits.SSRCG = 0;         // Sample Trigger Source Group bit (see SSRC)
    AD1CON1bits.SSRC =  0b111;     // Internal counter ends sampling and starts 
                                   // conversion (auto-convert), when SSRCG = 0

    AD1CON1bits.ADSIDL = 1;        // Discontinues module operation 
                                   // when device enters Idle mode

    /*
    AD1CON1bits.FORM = 0;          // Data output in unsigned integer format by default
    */

    /*
    Simultaneous SAMPLING is not simultaneous CONVERSION !
    Only the sampling is simultaneous. 
    You still have to multiplex the ADC inputs to convert the stored samples -
    one at a time. Whether you do that polling (switch the MUX, 
    start the conversion, wait for the conversion to complete, switch the MUX etc.)
    or via interrupt (eventually using the conversion time to good purpose) is up to you.
    */
    // AD1CON1bits.SIMSAM = 0;     // No need to samples CH0, CH1, CH2, 
                                   // CH3 simultaneously (10-bit mode only)

    AD1CON1bits.ASAM = 0;          // Sampling begins when the SAMP bit is set
    AD1CON1bits.SAMP = 0;          // Write 0 to end sampling and start conversion, 
                                   // 1 to begin sampling

    AD1CON2bits.VCFG = 0;          // Voltage Reference, VREFL = AVSS, VREFH = AVDD
    AD1CON2bits.CSCNA = 0;         // Disable input scan        

    /*
    if (!is12Bit)
    {
        AD1CON2bits.CHPS = 0b11;   // Converts CH0, CH1, CH2, CH3 (10-bit mode only)
    }
    */

    AD1CON2bits.ALTS = 0;          // Uses MUX A input multiplexer settings
    AD1CON2bits.BUFM = 0;          // Buffer configured as one 16-word buffer 
                                   // ADCBUF(15...0)

    AD1CON3bits.SAMC = 0b11111;    // 31 TAD for auto-sample time
    AD1CON3bits.ADCS = 255;        // +1 = TAD for conversion clock
    AD1CON3bits.ADRC = 1;          // Selecting Conversion clock source 
                                   // derived from system clock
    AD1CON1bits.ADON = 1;          // Enable AD converter
}

Although the datasheet recommends using 10-bit ADC mode as not all PICs have the diode connected in 12-bit, on my dsPIC33EP512MC502, the CTMU works well (and is more accurate) in 12-bit ADC mode. I would still recommend you to use 10-bit ADC first and only switch to 12-bit once you are sure everything is working.

After initializing the ADC, the next step is to do the same for the CTMU and the integrated diode:

C++
void initCTMU()
{
    CTMUCON1bits.CTMUEN   = 0;            // Disable CTMU, will be enabled later

    CTMUCON1bits.CTMUSIDL = 1;            // Discontinues module operation 
                                          // when device enters Idle mode
    CTMUCON1bits.TGEN = 0;                // Disabled edge delay generation
    CTMUCON1bits.EDGEN = 0;               // Software is used to trigger edges 
                                          // (manual set of EDGxSTAT)
    CTMUCON1bits.EDGSEQEN = 0;            // No edge sequence is needed
    CTMUCON1bits.CTTRIG = 0;              // CTMU triggers ADC start of conversion

    CTMUCON2bits.EDG1STAT = 1;            // EDGESTAT1 = EDGESTAT2 to enable 
                                          // current through diode
    CTMUCON2bits.EDG2STAT = 1;            // EDGESTAT1 = EDGESTAT2 to enable 
                                          // current through diode

    CTMUICONbits.IRNG = 0b11;             // 100xBase current level
}

Next, we need to retrieve the CTMU temperature readings. The following function returns the temperature between -128°C to 127°C, accurate to 1°C:

C++
char getSingleCTMUTemperature()
{
    AD1CHS0bits.CH0SA = 0b11110;          // Channel 0 positive input is connected 
                                          // to the CTMU temperature measurement diode 
                                          // (CTMU TEMP)
    AD1CHS0bits.CH0NA = 0;                // VREF- as negative input to channel 0
    CTMUCON1bits.CTMUEN = 1;              // Enable CTMU to start conversion
    AD1CON1bits.FORM = 0;                 // unsigned

    CTMUCON1bits.IDISSEN = 1;             // Discharge ADC Sample-and-Hold capacitor
    delay_us(50);
    CTMUCON1bits.IDISSEN = 0;             // Finish discharging

    AD1CON1bits.SAMP = 1;                 // start sampling, automatic conversion 
                                          // will follow

    // wait for a while to complete the conversion
    unsigned char c = 0;
    while (AD1CON1bits.DONE == 0)
    {
        if (c > 200)
        {
            debugPrint("CTADErr");
            return 0;
        }
        c++;
    } 

    /*
    VDD - supply voltage
    ADCVal - ADC value of the forward voltage
    ADC_STEPS - 1023 (for 10-bit ADC),
    DIODE_25C: Internal diode forward voltage at 25°C
    SLOPE: Rate of change (refer to Electricial Specifications in datasheet)

    VF = ADCVal * ((float)VDD / ADC_STEPS);
    T = 25.0 + ((VF - DIODE_25C)/SLOPE)*1000;
    */
    unsigned int adcVal = ADC1BUF0;
    double VF = adcVal * (3.3 / 4095);    // Diode forward voltage reading
    double ctmuTemp  = 25.0 + ( (VF - 0.721) / (-1.56) ) * 1000.0;
    debugPrint("CTM:%dC(%04X) OK:%d", (int)ctmuTemp, adcVal, isCTMUOK);

    CTMUCON1bits.CTMUEN = 0;              // Disable CTMU when done

    // good enough (-128 to 127) within PIC operating temperature range
    return (char)(ceilf(ctmuTemp));
}

The while loop at the beginning is needed to ensure the code waits for the ADC conversion to complete. The formula is taken from here. Use 1023 (10-bit ADC) or 4095 (12-bit ADC) for ADC_STEPS. Section “Electrical Characteristics” of the datasheet provides the diode rate of change and forward voltage. The values we need are 0.721V and -1.56mV/°C at 25°C:

dsPIC CTMU

With the above code, you should be able to get a sensible temperature reading such as 31°C from the CTMU. But all is not said and done yet. Ignoring the fact that the dsPIC might run hot resulting in measured temperature being higher than ambient temperature, which is not expected to happen in my product, a few tests revealed that the readings were much more unstable than the DHT11. One reading could be 31°C, the next could be 28°C and the next could be 32°C. Occasionally, there might be a value as low as 25°C or as high as 35°C! This was most likely due to noises on the VCC line which is also used as VREF for the ADC. The noises cannot be suppressed alone with decoupling capacitors (I tried very hard). Because of the 1000 multiplication factor in the formula, a single ADC count could result in a temperature shift of 2 or 3 degrees! Although I could use an external VREF for more stable ADC measurements, this defeated the purposes as I might as well use the DHT11 instead. After some considerations, I decided to fix the problem by taking multiple readings, sorting them in ascending order and averaging out only the middle readings. This effectively removes outliers and returns the average value of measurements within a certain confidence interval. The following sample codes show how to do this by taking 50 measurements and returning the average of the middle 30 values:

C++
char getAvgCTMUTemperature()
{
#define MAX_CTMU_COUNT      50
#define MAX_CTMU_COUNT_5TH  (MAX_CTMU_COUNT / 5)

    char allVals[MAX_CTMU_COUNT];
    char temp;
    unsigned char c1, c2;
    double avgVal;

    // get multiple measurements
    for (c1 = 0; c1 < MAX_CTMU_COUNT; c1++)
    {
        allVals[c1] = getSingleCTMUTemperature();
    }

    // bubble sort the list in ascending order
    for (c1 = 0; c1 < MAX_CTMU_COUNT - 1; c1++)
        for (c2 = c1 + 1; c2 < MAX_CTMU_COUNT; c2++)
        {
            if (allVals[c1] > allVals[c2])
            {
                temp = allVals[c1];
                allVals[c1] = allVals[c2];
                allVals[c2] = temp;
            }
        }

    // take average of the middle values, ignore bottom and top 20%
    avgVal = 0;
    for (c1 = MAX_CTMU_COUNT_5TH; c1 < 4 * MAX_CTMU_COUNT_5TH; c1++)
    {
        avgVal += allVals[c1];
    }
    avgVal = ceilf(avgVal / (3 * MAX_CTMU_COUNT_5TH));
    temp = (char)avgVal;
    debugPrint("CTMU: %dC", temp);
    return temp;
}

With this implementation, the returned temperature appeared to be much more stable. During my tests within 15 minutes, the returned values were consistently 28°C or 29°C, with no excessively high or low values. Still, the reading-to-reading shift, even if single degree e.g. from 28°C to 29°C, might not be good enough for some users. I worked around this by not updating the display if the temperature only changes by one degree, unless a significant amount of time has lapsed (e.g. more than 5 minutes). For my product, this implementation is good enough.

Another issue that I observed is that, if the PIC has been kept in storage for a long time, then the first CTMU reading immediately after startup might read rather high or low. For example, if the ambient temperature is 29°C, the reading might be 21°C or 35°C. This issue does not happen very often but once it happens, the measurement will stay consistent for 2-3 minutes before becoming normal again, presumably after the PIC has warmed up. I am not sure what the issue is, but I suspect that characteristics of the diode might change slightly after not being used for a long time causing the hard-coded parameters (rate of change and forward voltage) to be no longer valid. It could also be due to my LT1763 voltage regulator momentarily not being able to provide exactly 3.3V on the VCC line which is used as VREF. In any case, the inaccuracies do not seem to appear if the PIC is constantly in use and is therefore not a major issue for me. Regardless, I would recommend using DHT11 or some other dedicated temperature sensor chip if you have enough I/O pins in your design. The CTMU should only be used to measure temperature at a last resort.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)