Data Logging Thermometer with History
2016-12-09 | By Joshua Bishop
License: General Public License
Overview
This idea originated from the fact that I do not like to be freezing when I’m camping or backpacking. I had recently purchased a sleeping bag and while I had its rating, I had no idea how well it would actually work. I wanted something that would tell me how cold it got at night. I searched for small thermometers that had some history as well and was unable to find anything. Most camping thermometers were simple mercury based thermometers or fishing thermometers that were digital, and none of them had memory. So I set out to create a simple thermometer that recorded the previous twenty-four hours of readings and displayed current temperature as well as highest and lowest temperatures over those last twenty-four hours.
Approach
Using a simple 1-wire thermometer, an OLED display and a single button the thermometer will take samples every minute and store that sample to memory in a ring counter style storage. When the button is depressed, the OLED display lights up for five seconds, displaying the current temperature, high and low over the last twenty-four hours and a small graph tracking the changes.
Challenges and Design Considerations
The biggest challenges we expect are keeping a small form factor and minimizing power consumption.
Small form factor is relatively simple on the electronics side of a project this size. We selected a microcontroller in a very small QFN package. The temperature sensor is also a very small SOIC package, with only very small passives to support these. The two items likely to define the overall size are the battery (and its holder) and the screen. The screen needs to be large enough to display information but small enough to be highly portable. Finding this balance will be more of a user interface challenge with engineering ramifications.
To keep power consumption low, it was originally anticipated to use an STN display that constantly showed current and historical values. STN displays have incredibly low power consumption and great daytime visibility. To further minimize power consumption, the sampling rate needs to be reduced to a reasonable rate that gives acceptable time resolution without using too much power. These two items are anticipated to be the largest benefits to power savings, though other small yet crucial component choices, such as voltage dividers, will affect this as well.
End Result
While the plan was originally to use a small STN display, it was difficult to find an off-the-shelf display for prototyping purposes. Plus, the display wouldn’t be visible in the dark. A backlight would have increased cost and complexity and lacking that feature was a serious drawback. While we discussed this, the idea was put forth to have an OLED display that would only display for a couple seconds when a button was pushed. This is idea was first resisted because we thought there was a greater benefit to having the information continuously displayed.
However, it was pointed out that most people are accustomed to pushing a button to see the time or other information on their phone. At this point we asked a group of five or six colleagues for their opinion. This solution was unanimously preferred by those polled and the final interface was chosen to be the OLED.
The OLED selected was a .96” single-color graphical OLED display. This size was perfect for portability while also allowing us to display the current temperature, the high and low over the last 24 hours, and a simple graph showing the trends over that same time period. While the OLED is on, it consumes approximately 7-8 mA, which is still relatively minimal and even with repeated use, should have passable longevity.
For this version, we included two buttons, a button that will turn on the screen and another button on the back in the battery compartment to toggle between imperial and metric units. While this simple interface worked flawlessly, we discovered we could very easily limit it to one button for the entire interface by using a timing scheme. This is a minimal reduction in cost but opens up a decent percentage of real estate on the circuit board and within the enclosure.
When the OLED is off, average current draw is on the order of tens of microamps as most of the time the controller and sensors are asleep, with the occasional sensor sampling. With the 2032 battery used for this first prototype, rough calculations would give a theoretical life of around 6 months on a single battery with moderate usage.
Moving Forward
This is a simple project that has a lot of potential and is definitely something that will be actively pursued. As it is not an original concept and there are already similar products on the market, the main differentiators we will pursue will be the historical data, as well as a robust and completely waterproof enclosure, with the goal of IP68 standards.
To help make waterproofing simpler, we will want to reduce the amount of openings required. We will integrate a LiPo battery with a waterproof microUSB port to make it so the device never needs to be opened. In doing this, we have the benefit of reducing to a single button interface, because the unit changing button would no longer be accessible in the device itself and we’re hoping to minimize case openings.
Code – main.c
- #define MOTORPIN A4
- #define MOTORPIN A4
- #include "msp430.h"
- #include "DisplayLib\Adafruit_SSD1306.h"
- #include "DS18B20Lib\DS18B20.h"
- #define WAKEUP_PIN BIT1
- #define C_F_PIN BIT0
- Adafruit_SSD1306 display(128, 64);
- DS18B20 sensor(1,2);
- int8_t temperature, min=100, max=-100;
- char strtemp[6]={0,0,0,'∞','C',0};
- char strminmax[4]={0,0,0,'∞'};
- int8_t local_temp[12], day_min[120], day_max[120];
- int8_t t_counter = 0, d_counter = 0;
- int8_t overflow = 0;
- int8_t global_min, global_max;
- uint8_t c_f;
- uint8_t *Flash_ptrC
- void read_temperature (void) //Read the current temperature and calculate min and max values
- {
- do
- temperature = sensor.GetData();//Read current temperature
- while (temperature == 85);
- if (temperature > max) //If current temperature is more than maximum value
- max = temperature; //then set current temperature as maximum
- if (temperature < min)="" if="" current="" temperature="" is="" less="" than="" minimum="">
- min = temperature; //then set current temperature as minimum
- local_temp[t_counter] = temperature;//Save temperature value to the local 12-values array
- if (t_counter == 0) //If counter is 0
- {
- day_min[d_counter] = min; //Store min value during the last 12 minutes
- day_max[d_counter] = max; //Store max value during the last 12 minutes
- min = 100; //Set the unreacheable value for min
- max = -100; //Set the unreacheable value for max
- d_counter ; //Increment the day array counter
- if (d_counter == 120) //If day counter is 120 (24 hours)
- {
- d_counter = 0; //then reset the day counter
- overflow = 1; //And set the overflow flag (to show that there is not the first day)
- }
- }
- t_counter ; //Increment the local temperature array counter
- if (t_counter == 12) //If counter is 12
- t_counter = 0; //Reset local temperature counter
- }
- void init (void)
- {
- //Basic clock module initialization
- DCOCTL = CALDCO_8MHZ; //DCO setting = 8MHz
- BCSCTL1 = CALBC1_8MHZ; //DCO setting = 8MHz
- BCSCTL1 |= DIVA_3; //ACLK = 32768 / 8 = 4096 Hz
- BCSCTL3 |= XCAP_1; //12.5 pF internal capacitors
- while (IFG1 & OFIFG) //While oscillator fault flag is set
- {
- IFG1 &= ~OFIFG; //Reset oscillation fault flag
- __delay_cycles(8000); //1 ms delay
- }
- //I/O initialization
- P1REN |= WAKEUP_PIN C_F_PIN; //Switch on internal resistors
- P1OUT |= WAKEUP_PIN C_F_PIN; //Setup resistors as pull-up
- P1IES = WAKEUP_PIN C_F_PIN; //Allow high-to-low transition interrupts
- P1IE = WAKEUP_PIN C_F_PIN; //Enable external interrupts
- //Timer 0 initialization
- TA0CTL = TASSEL_1 MC_1 ID_3 TAIE;//Set timer 0 frequency f_timer0 = 4096/8 = 512 Hz and enable timer overflow interrupt;
- TA0CCR0 = 30720; //Set timer 0 period T_timer = 60 sec = 1 min (512 * 60 = 30720)
- //Timer 1 initialization
- TA1CTL = TASSEL_1; //Set timer 1 frequency f_timer1 = 4096 Hz
- TA1CCR0 = 20480; //Set timer 1 period T_timer = 5 sec (4096 * 5 = 20480)
- //Display initialization
- display.begin(SSD1306_SWITCHCAPVCC, SSD1306_I2C_ADDRESS); //Initialize OLED display with the I2C addr 0x3C (for the 128x64)
- display.setTextColor(WHITE); //Set text color visible
- display.sleep(1); //Put display into sleep mode
- //Read temperature
- read_temperature();
- //Read the units (0 - Fahrenheits, 1 - Celsius)
- Flash_ptrC = (uint8_t *) 0x1040;//Initialize Flash segment C ptr
- FCTL3 = FWKEY; //Clear Lock bit
- c_f = (*Flash_ptrC & 0x01); //Read the c_f value from the flash
- FCTL3 = FWKEY LOCK; //Set LOCK bit
- }
- void split_min_max (int8_t value)
- {
- if (value > 100)
- {
- strminmax[0] = (value/100) 0x30;
- strminmax[1] = ((value/10)) 0x30;
- strminmax[2] = (value) 0x30;
- }
- else if (value >= 0)
- {
- strminmax[0] = ' ';
- strminmax[1] = (value/10) 0x30;
- strminmax[2] = (value) 0x30;
- }
- else
- {
- strminmax[0] = '-';
- strminmax[1] = (-value/10) 0x30;
- strminmax[2] = (-value) 0x30;
- }
- }
- void show_screen (void) //Switch on the display for 5 seconds
- {
- uint8_t limit;
- int16_t temp;
- if (c_f) //If units are Celsius
- {
- temp = temperature; //Save temperature as is
- strtemp[4] = 'C'; //Write 'C'
- }
- else //If units are Fahrenheits
- {
- temp = temperature*9/5 32; //Convert Celsius to Fahrenheit
- strtemp[4] = 'F'; //Write 'F'
- }
- //Split temperature value into digits and save it into strtemp array
- if (temp > 100)
- {
- strtemp[0] = (temp/100) 0x30;
- strtemp[1] = ((temp/10)) 0x30;
- strtemp[2] = (temp) 0x30;
- }
- else if (temperature >= 0)
- {
- strtemp[0] = ' ';
- strtemp[1] = (temp/10) 0x30;
- strtemp[2] = (temp) 0x30;
- }
- else
- {
- strtemp[0] = '-';
- strtemp[1] = (-temp/10) 0x30;
- strtemp[2] = (-temp) 0x30;
- }
- display.sleep(0); //Wake up the display
- display.clearDisplay(); //Clear the display buffer
- display.setTextSize(3); //Set text size for current temperature
- display.setCursor(15,0); //Set cursor to the first line
- display.write(strtemp); //Draw the current temperature
- display.drawLine(0, 43, 120, 43, WHITE); //Draw the horisontal axle
- display.drawLine(0, 43, 0, 23, WHITE); //Draw the vertical axle
- for (int8_t i = 0; i < 24;="" i )="" draw="" 24="" dashes="" horisontal="">
- display.drawLine(5*i 1, 43, 5*i 1, 42, WHITE);
- //Search global min and max
- if (!overflow) //If there is the first day
- limit = d_counter;//then serch in the first d_counter values
- else //Otherwise
- limit = 120; //search in the whole array
- global_min = 100; //Set unreacheable value
- global_max = -100; //Set unreacheable value
- //Find min and max value in the archive array
- for (int8_t i = 0; i < limit;="">
- {
- if (day_min[i] <>
- global_min = day_min[i];
- if (day_max[i] > global_max)
- global_max = day_max[i];
- }
- //Check if global max and min are among the latest values
- if (min <>
- global_min = min;
- if (max > global_max)
- global_max = max;
- //If there is not the first day than display trend for the second part of the array
- if (overflow)
- {
- for (int8_t i = d_counter-1; i < 119;="">
- {
- int8_t y1,y2;
- if (global_max != global_min)
- {
- y1 = (int8_t)(43-((day_max[i] day_min[i])/2-global_min)*20/(global_max-global_min));
- y2 = (int8_t)(43-((day_max[i 1] day_min[i 1])/2-global_min)*20/(global_max-global_min));
- }
- else
- {
- y1 = y2 = 33;
- }
- display.drawLine(i-d_counter, y1, i-d_counter 1, y2, WHITE);
- }
- }
- //For any day display the first part of the array at the right part of the diagramm
- for (int8_t i = 0; i < d_counter-1;="">
- {
- int8_t y1,y2;
- if (global_max != global_min)
- {
- y1 = (int8_t)(43-((day_max[i] day_min[i])/2-global_min)*20/(global_max-global_min));
- y2 = (int8_t)(43-((day_max[i 1] day_min[i 1])/2-global_min)*20/(global_max-global_min));
- }
- else
- {
- y1 = y2 = 33;
- }
- display.drawLine(120-d_counter-1 i, y1, 120-d_counter i, y2, WHITE);
- }
- display.setTextSize(2); //Set text size for min/max temperatures
- display.drawRect(0, 46, 60, 18, WHITE); //Draw rectangle for min temperature
- display.drawRect(68, 46, 60, 18, WHITE); //Draw rectangle for max temperature
- display.setCursor(0,48); //Set cursor to the left bottom
- if (c_f)
- split_min_max (global_min); //Split min temperature into digits (Celsius)
- else
- split_min_max (global_min*9/5 32); //Split min temperature into digits (Fahrenheit)
- display.write(strminmax); //Draw the min temperature
- display.setCursor(68,48); //Set cursor to center bottom
- if (c_f)
- split_min_max (global_max); //Split max temperature into digits (Celsius)
- else
- split_min_max (global_max*9/5 32); //Split max temperature into digits (Fahrenheit)
- display.write(strminmax); //Draw the max temperature
- display.display(); //Write the display buffer into the display
- TA1CTL |= TACLR; //Clear timer 1
- TA1CTL |= TAIE MC_1; //Enable interrupts to turn off display in 5 seconds
- }
- #pragma vector=TIMER0_A1_VECTOR//Timer 0 overflow interrupt vector
- __interrupt void TIMER0_ISR (void)//Interrupt subroutine
- {
- read_temperature();
- TA0CTL &= ~TAIFG; //Clear interrupt flag
- }
- #pragma vector=TIMER1_A1_VECTOR//Timer 1 overflow interrupt vector
- __interrupt void TIMER1_ISR (void)//Interrupt subroutine
- {
- display.sleep(1); //Put display into sleep mode
- TA1CTL &= ~(TAIFG TAIE MC_1); //Clear interrupt flag and disable interrupt
- }
- #pragma vector=PORT1_VECTOR//Port1 change interrupt vector
- __interrupt void PORT1_ISR (void)//Interrupt subroutine
- {
- if (~P1IN & C_F_PIN) //If C/F button is pressed
- {
- __delay_cycles(160000); //Debounce delay
- if (~P1IN & C_F_PIN)
- {
- while (~P1IN & C_F_PIN); //Wait while C/F button is pressed
- __delay_cycles(160000); //Debounce delay
- if (P1IN & C_F_PIN) //If C/F button is released
- {
- c_f ^= 1; //Switch between Celsius and Fahrenheit
- Flash_ptrC = (uint8_t *) 0x1040;//Initialize Flash pointer
- FCTL1 = FWKEY ERASE; //Set Erase bit
- FCTL3 = FWKEY; //Clear Lock bit
- *Flash_ptrC = 0; //Dummy write to erase Flash segment
- FCTL1 = FWKEY WRT; //Set WRT bit for write operation
- *Flash_ptrC = c_f; //Read value from flash 0x1040
- FCTL1 = FWKEY; //Clear WRT bit
- FCTL3 = FWKEY LOCK; //Set LOCK bit
- show_screen();
- }
- }
- }
- if (~P1IN & WAKEUP_PIN) //If wakeup button is pressed
- {
- show_screen();
- }
- P1IFG &= ~(WAKEUP_PIN C_F_PIN); //Clear interrupt flags
- }
- int main( void )
- {
- WDTCTL = WDTPW WDTHOLD; // Stop watchdog timer to prevent time out reset
- init(); //Init all required modules
- __bis_SR_register (GIE LPM3_bits); //Sleep mode 3 with interrupts
- }
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.
Visit TechForum