element14 Community
element14 Community
    Register Log In
  • Site
  • Search
  • Log In Register
  • Community Hub
    Community Hub
    • What's New on element14
    • Feedback and Support
    • Benefits of Membership
    • Personal Blogs
    • Members Area
    • Achievement Levels
  • Learn
    Learn
    • Ask an Expert
    • eBooks
    • element14 presents
    • Learning Center
    • Tech Spotlight
    • STEM Academy
    • Webinars, Training and Events
    • Learning Groups
  • Technologies
    Technologies
    • 3D Printing
    • FPGA
    • Industrial Automation
    • Internet of Things
    • Power & Energy
    • Sensors
    • Technology Groups
  • Challenges & Projects
    Challenges & Projects
    • Design Challenges
    • element14 presents Projects
    • Project14
    • Arduino Projects
    • Raspberry Pi Projects
    • Project Groups
  • Products
    Products
    • Arduino
    • Avnet & Tria Boards Community
    • Dev Tools
    • Manufacturers
    • Multicomp Pro
    • Product Groups
    • Raspberry Pi
    • RoadTests & Reviews
  • About Us
  • Store
    Store
    • Visit Your Store
    • Choose another store...
      • Europe
      •  Austria (German)
      •  Belgium (Dutch, French)
      •  Bulgaria (Bulgarian)
      •  Czech Republic (Czech)
      •  Denmark (Danish)
      •  Estonia (Estonian)
      •  Finland (Finnish)
      •  France (French)
      •  Germany (German)
      •  Hungary (Hungarian)
      •  Ireland
      •  Israel
      •  Italy (Italian)
      •  Latvia (Latvian)
      •  
      •  Lithuania (Lithuanian)
      •  Netherlands (Dutch)
      •  Norway (Norwegian)
      •  Poland (Polish)
      •  Portugal (Portuguese)
      •  Romania (Romanian)
      •  Russia (Russian)
      •  Slovakia (Slovak)
      •  Slovenia (Slovenian)
      •  Spain (Spanish)
      •  Sweden (Swedish)
      •  Switzerland(German, French)
      •  Turkey (Turkish)
      •  United Kingdom
      • Asia Pacific
      •  Australia
      •  China
      •  Hong Kong
      •  India
      • Japan
      •  Korea (Korean)
      •  Malaysia
      •  New Zealand
      •  Philippines
      •  Singapore
      •  Taiwan
      •  Thailand (Thai)
      • Vietnam
      • Americas
      •  Brazil (Portuguese)
      •  Canada
      •  Mexico (Spanish)
      •  United States
      Can't find the country/region you're looking for? Visit our export site or find a local distributor.
  • Translate
  • Profile
  • Settings
Embedded and Microcontrollers
  • Technologies
  • More
Embedded and Microcontrollers
Blog Making a portable, battery powered DS18B20 display unit
  • Blog
  • Forum
  • Documents
  • Quiz
  • Polls
  • Files
  • Members
  • Mentions
  • Sub-Groups
  • Tags
  • More
  • Cancel
  • New
Join Embedded and Microcontrollers to participate - click to join for free!
  • Share
  • More
  • Cancel
Group Actions
  • Group RSS
  • More
  • Cancel
Engagement
  • Author Author: vmate
  • Date Created: 4 Sep 2024 9:40 AM Date Created
  • Views 1532 views
  • Likes 9 likes
  • Comments 4 comments
  • avr
  • c
  • 3D Printing
  • max7219
  • ds18b20
  • pcb design
Related
Recommended

Making a portable, battery powered DS18B20 display unit

vmate
vmate
4 Sep 2024

Recently, I started making pour-over coffee at home, and it turns out that water temperature can change the taste a lot. I don’t have a kettle that can measure temperature, so time to make something.

I found a few DS18B20s with stainless steel housing and a long cable, which are perfect for this purpose. Initially, I just used an Arduino Uno with a TFT display shield, but it was quite janky, and also needed USB power to run.

So here’s the plan:
- 18650 cell for power
- 4 digit 7 segment displays
- USB-C charging
- 3D printed housing

Schematics and PCB

I didn’t want to wait a week for PCBs, so I decided to use my 3D printer, which I modified to be able to hold a 48V spindle motor. To make this more interesting, I decided to limit myself to using a single layer PCB design, and an ATTINY AVR microcontroller.

I chose the ATTINY24A, and a MAX7219 for driving the display. The ATTINY needs 1.8V to 5.5V, while the MAX7219 requires 4V to 5.5V, but I found that it works down to about 3V without any issues. This means I do not need a voltage regulator and can run everything straight off the 18650(~3.5V to 4.2V).

image

Originally I had the MAX7219 share the SCK/MISO pins with the ICSP header, and also forgot the RST pullup, but didn’t make a new PCB with these changes, just cut the traces and hooked up some bodge wires, visible in later photos. The original version would’ve worked too, but having separate pins helped with debugging, and had plenty of unused GPIOs on the AVR anyways.

Here’s the original layout that I ended up milling:

image

In the end, I needed 3 through-hole jumpers, shown as the back layer with blue in the layout.

image

This was one of the first PCBs I milled, didn’t have everything figured out yet, so it’s not the best looking, with a lot of the traces ending up thinner than they should be. They all made proper electrical contact though, so good enough for this purpose.

image

image

AVR code

Controlling the MAX7219 was quite easy, just need to send out two bytes on MOSI, the first being register address, and the second is the value to write.

#define CS 3
#define MOSI 2
#define SCK 1

void set(uint8_t pin, uint8_t val) {
	if (val == 0) {
		PORTA &= ~(1 << pin);
	}
	else {
		PORTA |= (1 << pin);
	}
}

void shiftOut(uint8_t data) {
	for(uint8_t i = 0; i < 8; i++) {
		int bit = data & (1 << (7-i));
		set(MOSI, bit);

		_delay_us(1);
		set(SCK, 1);
		_delay_us(1);
		set(SCK, 0);
	}
}

void setMAXRegister(uint8_t reg, uint8_t value) {
	set(CS, 0);
	_delay_us(1);
	shiftOut(reg);
	shiftOut(value);
	_delay_us(1);
	set(CS, 1);
	_delay_us(1);
}

To set up the MAX7219, I had to disable shutdown mode, disable “Display Test mode”, enable “Code B decode for digits 0-3”, set the brightness, and enable the first 4 displays only:

setMAXRegister(0x0C, 1);	// Shutdown -> Normal mode
setMAXRegister(0x0F, 0);	// Display Test -> Normal mode
setMAXRegister(0x09, 0x0F);	// Decode Mode -> Code B decode for digits 0-3
setMAXRegister(0x0A, 0);	// Intensity -> 0
setMAXRegister(0x0B, 3);	// Scan Limit -> 0,1,2,3

Displaying digits was simple as well, the MAX7219 can figure out on its own what segments to power on, all I needed to do was put the number I wanted to display in a register.

void setMAXDigit(uint8_t digit, uint8_t val, bool dot) {
	uint8_t lookup[] = {0, 2, 3, 1};
	uint8_t dig = lookup[digit];
	if (dot == true) {
		val |= (1 << 7);
	}
	setMAXRegister(dig+1, val);
}

The lookup array is needed, because I didn’t connect the 7 segment displays in order, due to layout constraints on the PCB.

For reading temperature from the DS18B20, I ended up slightly modifying a library I found on GitHub(https://github.com/Jacajack/avr-ds18b20), mostly just removing parts I didn’t need.

First, I set the DS18B20 to 11bit resolution (0.125C per count), which I found to be a good compromise between readout rate and precision:

image

ds18b20wsp(&PORTB, &DDRB, &PINB, ( 1 << 2 ), 0, 0, 125, DS18B20_RES11);

Then all I had to do was tell the DS18B20 to start a conversion, and then read the result.

int16_t temp = 0;
ds18b20convert(&PORTB, &DDRB, &PINB, ( 1 << 2 ), 0);
_delay_ms(10);
ds18b20read(&PORTB, &DDRB, &PINB, ( 1 << 2 ), 0, &temp);

The library returns the temperature multiplied by 16, to avoid having to use floats/doubles(the highest resolution 12-bit mode has a resolution of 0.0625C, which is 1/16th C).

To display this number properly, I wrote the following function:

void setMAXfromDS(int16_t dstemp) {
	if(dstemp >= 0) {
		uint16_t temp_integer = dstemp / 16;
		if(dstemp < 100*16) { // Under 100C
			uint16_t temp_fraction = ((dstemp % 16) * 100) / 16;

			setMAXDigit(0, temp_integer / 10, false);
			setMAXDigit(1, temp_integer % 10, true);
			setMAXDigit(2, temp_fraction / 10, false);
			setMAXDigit(3, temp_fraction % 10, false);
		}
		else { // Over 100C
			uint16_t temp_fraction = ((dstemp % 16) * 10) / 16;
				
			setMAXDigit(0, temp_integer / 100, false);
			temp_integer = temp_integer % 100;
			setMAXDigit(1, temp_integer / 10, false);
			setMAXDigit(2, temp_integer % 10, true);
			setMAXDigit(3, temp_fraction, false);
		}
	}
	else {
		//TODO - negative temp handling
	}
}

Temperatures at or over 100C are displayed with one decimal point of accuracy, between 0-100 two decimal points are used. I haven’t implemented negative temperature handling yet, as I have no need for it currently.

I also wanted some way to measure battery voltage and display it at startup. The straightforward solution, which would be to connect the battery to an ADC pin, wouldn’t work without an external reference, which would also need to be at least 4.2V to work properly.

The solution is to not connect the battery to an ADC pin at all, but use the ADC to measure an internal 1.1V reference instead, with the ADC reference set to Vcc. Here’s an example how this would work:

The ADC is 10-bit, meaning 0-1023. Let’s assume Vcc=2.2V, and we know that the internal reference is 1.1V. This means the ADC will read 512, since the internal reference(1.1V) is exactly half the voltage of the ADC reference(Vcc). (Vcc/1.1V = 1024/ADC_counts)

The internal reference isn’t exactly 1.1V though, so first step is to figure out the exact value. By measuring Vcc externally with a multimeter, and taking an ADC measurement, we can calculate the accurate value using this formula: measured_vcc * ADC_counts / 1024. In my case, I got 1.0906875V.

Knowing this, Vcc can be calculated like so: 1.0906875V * 1024 / ADC_counts. To avoid having to deal with floats here too, I further multiplied this value by 1000, to end up with 3 decimal points of accuracy in the end. Since Vcc will always be a number between 1.8V(hopefully more like 3.5V) and 4.2V, I can always assume the decimal point is after the first digit(that’s what the “true” in the first “setMAXDigit” function call is). The formula now simplifies to 1116860 / ADC_counts.

Accuracy is within 0.05V compared to what my multimeter measures, so I’m pleasantly surprised with how well this works, although I haven’t tested how much the reference drifts with ambient temperature change.

void showVccVoltage(uint16_t adcval) {
	// Bandgap voltage: (actualVcc * adcReading) / 1024
	// Vcc: (bandgapVoltage * 1024) / adcReading
	
	uint16_t num = 1116860 / adcval; // Multiplied by 1000 to not have to deal with floats
	
	setMAXDigit(0, num / 1000, true);
	num = num % 1000;
	setMAXDigit(1, num / 100, false);
	num = num % 100;
	setMAXDigit(2, num / 10, false);
	setMAXDigit(3, num % 10, false);
}

ADMUX = 0b00100001; // Vcc as reference, measuring 1.1V internal bandgap
ADCSRA = 0b10000100; // Enable ADC, prescaler 16

ADCSRA |= (1 << ADSC); // Start conversion
while(!(ADCSRA & (1 << ADIF))); // Wait for conversion to complete
ADCSRA |= (1 << ADIF); // Clear the ADC interrupt flag
volatile uint16_t adcl = ADCL; // Read lower byte
uint16_t value = (((uint16_t)ADCH) << 8) | adcl; // Read upper byte and merge into uint16_t
showVccVoltage(value);

Here's the entire code:

#include <avr/io.h>
#include <util/delay.h>
#include <stdbool.h>
#include "ds18b20.h"

#define CS 3
#define MOSI 2
#define SCK 1

void set(uint8_t pin, uint8_t val) {
	if (val == 0) {
		PORTA &= ~(1 << pin);
	}
	else {
		PORTA |= (1 << pin);
	}
}

void shiftOut(uint8_t data) {
	for(uint8_t i = 0; i < 8; i++) {
		int bit = data & (1 << (7-i));
		set(MOSI, bit);

		_delay_us(1);
		set(SCK, 1);
		_delay_us(1);
		set(SCK, 0);
	}
}

void setMAXRegister(uint8_t reg, uint8_t value) {
	set(CS, 0);
	_delay_us(1);
	shiftOut(reg);
	shiftOut(value);
	_delay_us(1);
	set(CS, 1);
	_delay_us(1);
}

void setMAXDigit(uint8_t digit, uint8_t val, bool dot) {
	uint8_t lookup[] = {0, 2, 3, 1};
	uint8_t dig = lookup[digit];
	if (dot == true) {
		val |= (1 << 7);
	}
	setMAXRegister(dig+1, val);
}

void showVccVoltage(uint16_t adcval) {
	// Bandgap voltage: (actualVcc * adcReading) / 1024
	// Vcc: (bandgapVoltage * 1024) / adcReading
	
	uint16_t num = 1116860 / adcval; // Multiplied by 1000 to not have to deal with floats
	
	setMAXDigit(0, num / 1000, true);
	num = num % 1000;
	setMAXDigit(1, num / 100, false);
	num = num % 100;
	setMAXDigit(2, num / 10, false);
	setMAXDigit(3, num % 10, false);
}

void setMAXfromDS(int16_t dstemp) {
	if(dstemp >= 0) {
		uint16_t temp_integer = dstemp / 16;
		if(dstemp < 100*16) { // Under 100C
			uint16_t temp_fraction = ((dstemp % 16) * 100) / 16;

			setMAXDigit(0, temp_integer / 10, false);
			setMAXDigit(1, temp_integer % 10, true);
			setMAXDigit(2, temp_fraction / 10, false);
			setMAXDigit(3, temp_fraction % 10, false);
		}
		else { // Over 100C
			uint16_t temp_fraction = ((dstemp % 16) * 10) / 16;
				
			setMAXDigit(0, temp_integer / 100, false);
			temp_integer = temp_integer % 100;
			setMAXDigit(1, temp_integer / 10, false);
			setMAXDigit(2, temp_integer % 10, true);
			setMAXDigit(3, temp_fraction, false);
		}
	}
	else {
		//TODO - negative temp handling
	}
}

int main(void) {
	_delay_ms(500);
	
	// Set pins as output
	DDRA |= (1 << CS);
	DDRA |= (1 << SCK);
	DDRA |= (1 << MOSI);
	
	set(CS, 1);
	set(SCK, 0);
	set(MOSI, 0);
	
	ADMUX = 0b00100001; // Vcc as reference, measuring 1.1V internal bandgap
	ADCSRA = 0b10000100; // Enable ADC, prescaler 16
	_delay_ms(1);

	setMAXRegister(0x0C, 1);	// Shutdown -> Normal mode
	setMAXRegister(0x0F, 0);	// Display Test -> Normal mode
	setMAXRegister(0x09, 0x0F);	// Decode Mode -> Code B decode for digits 0-3
	setMAXRegister(0x0A, 0);	// Intensity -> 0
	setMAXRegister(0x0B, 3);	// Scan Limit -> 0,1,2,3
	_delay_ms(200);

	ds18b20wsp(&PORTB, &DDRB, &PINB, ( 1 << 2 ), 0, 0, 125, DS18B20_RES11); // Set DS18B20 to 11bit resolution mode
	
	// Display battery voltage for about 3 seconds (displays 30 values, each taking 100ms to calculate)
	for(uint8_t j = 0; j < 30; j++) {
		// Take an average of 100 ADC readings
		uint16_t adcsum = 0;
		for(uint8_t i = 0; i < 100; i++) {
			ADCSRA |= (1 << ADSC); // Start conversion
			while(!(ADCSRA & (1 << ADIF))); // Wait for conversion to complete
			ADCSRA |= (1 << ADIF); // Clear the ADC interrupt flag
			volatile uint16_t adcl = ADCL; // Read lower byte
			adcsum += (((uint16_t)ADCH) << 8) | adcl; // Read upper byte and merge into uint16_t, add to sum
			_delay_ms(1);
		}
		// Show the averaged voltage
		showVccVoltage(adcsum / 100);
	}
	
	int16_t temp = 0;
	while (1) {
		// Start DS18B20 conversion
		ds18b20convert(&PORTB, &DDRB, &PINB, ( 1 << 2 ), 0);
		_delay_ms(10);
		// Fetch result
		ds18b20read(&PORTB, &DDRB, &PINB, ( 1 << 2 ), 0, &temp);
		// Display result
		setMAXfromDS(temp);
	}
	
	return 0;
}

Housing

I found a TP4056 and DW01 based USB-C charger and protection board to use, but it didn’t have any mounting holes:

image

To simplify the housing design, I made a small plastic holder that the module can slide into, and has two holes on the side for screws:

image

image

I will use a simple slide switch for turning the power on and off, and a 3.5mm audio jack to connect the DS18B20. Here’s the top part of the case:

image

image

image

image

image

image

The other half of the case only has to fit the round 18650 cell, so I made the top and bottom angled, and the back curved:

image

image

Finished device

image

image

Showing battery voltage on startup, for about 3 seconds:

image

And measuring temperature:

image

image

  • Sign in to reply

Top Comments

  • dougw
    dougw over 1 year ago +1
    Nicely done!
  • DAB
    DAB over 1 year ago +1
    Very nice looking unit. The PC board look good for just using a router.
  • James123
    James123 over 1 year ago

    Fantastic work. 

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • DAB
    DAB over 1 year ago

    Very nice looking unit.

    The PC board look good for just using a router.

    • Cancel
    • Vote Up +1 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • vmate
    vmate over 1 year ago in reply to dougw

    Thanks!

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • dougw
    dougw over 1 year ago

    Nicely done!

    • Cancel
    • Vote Up +1 Vote Down
    • Sign in to reply
    • More
    • Cancel
element14 Community

element14 is the first online community specifically for engineers. Connect with your peers and get expert answers to your questions.

  • Members
  • Learn
  • Technologies
  • Challenges & Projects
  • Products
  • Store
  • About Us
  • Feedback & Support
  • FAQs
  • Terms of Use
  • Privacy Policy
  • Legal and Copyright Notices
  • Sitemap
  • Cookies

An Avnet Company © 2025 Premier Farnell Limited. All Rights Reserved.

Premier Farnell Ltd, registered in England and Wales (no 00876412), registered office: Farnell House, Forge Lane, Leeds LS12 2NE.

ICP 备案号 10220084.

Follow element14

  • X
  • Facebook
  • linkedin
  • YouTube