Measuring temperature using the dsPIC Charge Time Measurement Unit (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 RTFM 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):
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:
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 trough diode CTMUCON2bits.EDG2STAT = 1; // EDGESTAT1 = EDGESTAT2 to enable current trough diode CTMUICONbits.IRNG = 0b11; // 100xBase current level }
Next, we need to retrieve the CTMU temperature readings. The following function return the temperature between -128°C to 127°C, accurate to 1°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:
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:
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.
See also:
Emulating EEPROM using flash memory on dsPIC33EP512MC502