DS18B20 displaying on 128 x 32 SSD1306 OLED with 08m2

WhiteSpace

Well-known member
I've been distracted from a couple of larger projects over the past week, trying to create a small thermometer based on the DS18B20. I originally tried the LM335, but the readings fluctuated wildly, I think because the sensor produces an analogue ADC reading that varies by 10mV/degree C and I found it difficult to get a steady reading.

The DS18B20 was a bit tricky to set up in ReadTemp12 mode - I still don't really understand how the 12 bit data that it produces is converted to a temperature. However, a short piece of code in the manual (the 5th example here: https://picaxe.com/basic-commands/analogue-inputoutput/readtemp12/ ) provided a good base. For the display, I relied on @Hemi345's deciphering of the 128 x 32 code, and I also got some inspiration from @AllyCat's very impressive snippet here: https://picaxeforum.co.uk/threads/full-ascii-character-font-for-ssd1306-with-08m2-and-above.32003/ . I didn't understand large parts of it, but the use of "on x, do y" pointed me in the right direction and I eventually created a "Select Case" for displaying the digits that relied on program space rather than storage
Rich (BB code):
main:
gosub ClearDisplay

readtemp12 TempSensor, w10

;this part of the code based on the text on the Picaxe website: https://picaxe.com/basic-commands/analogue-inputoutput/readtemp12/

showsignedtemperature:
      if w10 < $8000 then ;apparently this is the same as 32768, FWIW.  Is that how the sensor displays 0 degrees in 12 bits?
            w11 = w10
      else
            w11 = -w10
      end if
      
      w12 = w11 & 15 * 625 ; what does this do?
            
      bintoascii w12, b15,b14,b13,b12,b11 ; here we are only interested in b14, which equates to the first decimal place.  It would be good to show the decimal place properly, ie. rounding up or down.  To be done: if b13 >= 5, then b14 = b14 + 1 and then I need to be able to roll over the unit and the 10 too.
      w12 = w11 / 16
      
      bintoascii w12, b20,b19,b18,b17,b16 ; we're only interested in the 10s and units b17 and b16
      
      sertxd ( "degrees = ", "(tens) ", b17, " (units) ", b16, CR, LF) ; just to test that we have got them right.  These come from splitting w12 into its component characters
      sertxd( "or put another way, #w12 = ", #w12, ".", b14, CR, LF ) ; I have no idea why w12 has a hash and b14 doesn't.  strangely b14 comes from the first bintoascii line
What took some time to understand was the difference in value, when looking at Sertxd, between eg. #w12 and w12, and also when to enclose characters in quotation marks.

It seems to work OK: here's a photo:

23968

(The battery holder is for an N battery, with three LR44 batteries and a dummy battery made from a sawn-off copper nail glued inside a cardboard tube). The intention is to squeeze it into a small footprint.

It shows a negative signed number if put in the freezer. It seems fairly accurate - within 1/10 of a degree of our central heating thermostat. I've attached the code in case it's of any use to anyone. Next steps - to provide for rounding up of the digits where required.
 

Attachments

AllyCat

Senior Member
Hi,
... the difference in value, when looking at Sertxd, between eg. #w12 and w12, and also when to enclose characters in quotation marks.
The difference is that one represents a "number" and the other a "character". If all the bits in a byte are clear (i.e. %0000 0000) then it represents the number zero. But the (ASCII) character which represents "0" has bits 4 and 5 set (i.e. %0011 0000 = 48 decimal). The "printing" commands (e.g. SERTXD(#0) ) can use the "#" symbol to convert the number 0 to the character "0" (which has the value of 48). Furthermore, if you use the # with a byte value of 48, it prints the characters "4" and then "8" (which have ASCII values 52 and 56).

However, if you are not printing the number, but want to "manipulate" the individual digits then the BINTOASCII command is available, which does what it says, i.e. it converts a (BINARY) number to ASCII character(s). That's a two-stage process, first it converts the byte or word into separate digits (each in its own byte) and then converts each digit to ASCII (which in practice just adds 48 (or "0") to each byte. The ASCII character numbering may seem unnecessarily confusing, but there are good (historical) reasons why. However, I think a full explanation had better wait for another day.

Code:
main: 
      w12 = w11 & 15 * 625 ; what does this do?
       bintoascii w12, b15,b14,b13,b12,b11 ; here we are only interested in b14, which equates to the first decimal place.  It would be good to show the decimal place properly, ie. rounding up or down.  To be done: if b13 >= 5, then b14 = b14 + 1 and then I need to be able to roll over the unit and the 10 too.
The w11 & 15 is selecting out the low 4 bits of the number (i.e. 0 ... 15), which represents the "fractional" (decimal) part of the value. A clue to what's happening is to consider the next value, i.e. 16 * 625 which equals 10,000 , that represents "1" (one degree C). So b14 in the BINTOASCII represents the first decimal place. and the "trick" to round up (by a "half" in the net decimal place) is to add 500 to w12 before the bintoascii command.

Then the w11 / 16 removes the four "fractional" (decimal) bits to leave the integer part of the number, which can be split into individual digits as before:

Code:
#picaxe 08M2
    w12 = w11 / 16
      bintoascii w12, b20,b19,b18,b17,b16 ; we're only interested in the 10s and units b17 and b16
        lookup b21, (b16, ".", b14, "?", "C"), b22       ;this takes the digits of the temperature plus degree symbol and C and feeds them in succession to b22, which points to the right subroutine        
        gosub DisplayDigits
......
DisplayDigits:
Select Case b22; tried this as "on b22 gosub..." but it wouldn't recognise the values of b22.  So Select Case instead.
    Case "0" gosub Num0; need to do it this way because not much space for storage
    Case "1" gosub Num1 
    Case "2" gosub Num2 
    Case "3" gosub Num3 
    Case "4" gosub Num4 
    Case "5" gosub Num5 
    Case "6" gosub Num6 
    Case "7" gosub Num7 
    Case "8" gosub Num8 
    Case "9" gosub Num9
EndSelect 
Return
I eventually created a "Select Case" for displaying the digits that relied on program space rather than storage
You probably just needed to subtract 48 from the character number to give the ON ... GOTO ... index value. Personally I "dislike" the SELECT ... CASE structure because it's quite inefficient. If you copy the above code and do a Syntax check, the PE will report over 200 bytes of program space, of which just over 150 are the SELECT ... CASE (and the BINTOASCII takes almost 50). Interestingly, changing to a larger M2 (e.g. 14M2) saves about 20 bytes, as does changing the GOSUBs to GOTOs with the 08M2. And the GOTO should work just as well, because after the ENDSELECT the program has a RETURN, so the RETURN in the subroutine(s) would have the same effect (but faster).

The version of code below uses only about 25 bytes of space and should be much faster: My notes suggest that the tenth entry (i.e. "9") in a SELECT .. CASE .. takes about 16 ms to execute (at 4 MHz), whilst the equivalent ON .. GOTO .. takes about 4 ms.
Code:
b22 = b22 - "0"      ; Convert ASCII character back to a numerical value by subtracting 48
on b22 goto Num0,Num1,Num2,Num3,Num4,Num5,Num6,Num7,Num8,Num9
Num0:
Num1:
Num2:
Num3:
Num4:
Num5:
Num6:
Num7:
Num8:
Num9:
I still have to add the "QUAD Height with Character Rounding" routine to my Code Snippet thread, which is able to use an 08M2 to construct all 96 characters of the ASCII set, from the basic 5 x 8 cells, up to a 12 x 32 pixels font, but here's a teaser. ;)

IMG_3684 (2).JPG

Cheers, Alan.
 

Aries

New Member
Code:
showsignedtemperature: 
if w10 < $8000 then ;apparently this is the same as 32768, FWIW. Is that how the sensor displays 0 degrees in 12 bits? 
    w11 = w10
else
    w11 = -w10
end if
Zero is zero - i.e. $0000. The Picaxe does not actually have negative numbers - a word value goes from 0 to 65535. However, if you think about what happens when you add 1 to 65535, you get zero (because the carry bit is lost). So, 65535 can represent -1. $8000, or 32768 is half of 65536, and the convention is that numbers smaller than this are positive, and larger than this are negative. Addition, subtraction and multiplication all work regardless of sign, but division and bit manipulation need to work with positive numbers and then adjust for the sign at the end - hence the need to change the sign of w10 if it is negative before converting it.

The value that comes back is really a two hex digit integer with a one hex digit fraction. So a value of $123 would be a decimal value of 18 + 3/16.
There is a quick conversion of this into a decimal value with one decimal place:
Code:
w10 = w10 * 10 / 16 '(which truncates)
or
w10 = W10 * 10 + 8 / 16 '(which rounds)
 

WhiteSpace

Well-known member
Thanks @Aries.

I've adopted @AllyCat's suggestion of using On... Goto... for picking up the code to load the digits, although I used GoSub instead of Goto simply because the relevant digits were already written as GoSubs and it was easier to keep that structure rather than change them all to Goto. It's certainly rather neater - thanks again.
 

WhiteSpace

Well-known member
Well I have got the decimals to round up, although not in the approved manner. I read @AllyCat's explanation that "the "trick" to round up (by a "half" in the net decimal place) is to add 500 to w12 before the bintoascii command" and tried it, but couldn't get it to work. I'm obviously doing something wrong. I think I understand that adding 500 is the equivalent of adding 0.05, if 10,000 is equivalent to a temperature of 1.0, but I don't understand why that doesn't just increase all decimals rather than only those needing rounding. After a few hours' head-scratching, I finally worked out the following far less elegant solution :

Rich (BB code):
w12 = w11 & 15 * 625 ; @AllyCat explained that this is selecting out the low 4 bits of the number (i.e. 0 ... 15), which represents the "fractional" (decimal) part of the value. A clue to what's happening is to consider the next value, i.e. 16 * 625 which equals 10,000 , that represents "1" (one degree C). So b14 in the BINTOASCII represents the first decimal place. and the "trick" to round up (by a "half" in the net decimal place) is to add 500 to w12 before the bintoascii command.
            
      bintoascii w12, b15,b14,b13,b12,b11 ; here we are only interested in b14 and b13, which equate to the first and second decimal places (tenths and hundredths. 
      
      w12 = w11 / 16 ; @AllyCat explained that the w11 / 16 removes the four "fractional" (decimal) bits to leave the integer part of the number, which can be split into individual digits as before:
      
      sertxd ("before rounding, temp is #w12 = ", #w12, ", tenths (b14) = ", b14, " and hundredths (b13) = ",b13, CR, LF )
      sertxd ("which means temp = ", #w12,".", b14, b13, CR, LF)
      
      if b13 >= "5" then ; if hundredths is greater than 5, we need to round up, so:
            b14 = b14 + 1 ;add 1 to the tenths
            sertxd("b14 = ", b14, CR, LF ) ; and show the result
            if b14 > "9" then ; if this means that the tenths are now greater than 9, ie. 10/10, because we rounded up a 9, eg. n.98, then
                  let b14 = "0" ; we make the tenths a zero and
                  let w12 = w12 + 1; add 1 to the integer
            endif
      endif

                  
      bintoascii w12, b20,b19,b18,b17,b16 ; we're only interested in the 10s and units b17 and b16
      
      ;sertxd ( "degrees = ", "(tens) ", b17, " (units) ", b16, CR, LF) ; just to test that we have got them right.  These come from splitting w12 into its component characters
      sertxd( "post rounding (b17,b16.b14) = ", b17,b16,".", b14, CR, LF ) ; 
It seems to work OK - here are a couple of lines from the serial window, showing that the rounding has worked, although I haven't yet had it round up the integer as well as the decimal, and I have a horrible feeling that adding 1 to w12 is not the right answer because 1 degree centigrade is not represented by the number 1.

"before rounding, temp is #w12 = 24, tenths (b14) = 2 and hundredths (b13) = 5
which means temp = 24.25
b14 = 3
post rounding (b17,b16.b14) = 24.3"

Any help in getting me back on the right track would be very welcome!
 

WhiteSpace

Well-known member
On reflection rather than let w12 = w12 + 1 which I think is doomed to failure, I probably need to do for b16 and b17 what I have already done for b14 - it's an even more messy solution, but at least I think I know why it works. At least until I understand how to work directly on w12.
 

AllyCat

Senior Member
Hi,

Ah Yes, rounding up the "fractional" number (i.e. that after the decimal point) won't (automatically) carry across into the integer part. So it's necessary to check if b15 has been incremented (to "1") and if so, increment w12 again (when it contains the integer part). Either with an IF .. THEN , or simply add (b15 - "0") to w12 .

Of course, you are mapping 16 ("fractional binary") steps (from the 18B20) into 10 decimal steps (to be displayed), so the incrementing can't be entirely uniform and "rounding up" has a limited meaning. Simply incrementing the binary value directly from the 18B20 (i.e. adding 1/16 = 0.0625) will have a very similar effect (to adding 0.05).

Cheers, Alan.
 

Aries

New Member
Rounding from one hex place to one decimal place is what the second version in post#4 does - it adds 8 and then divides by 16. If you want it in ASCII, then something like
Code:
w10 = w10 * 10 + 8 / 16
bintoascii w10, b15,b14,b13,b12,b11
will give you the decimal place in b11.
IMO, It's easier to work with the original temperature rather than trying to handle bits of it individually.
 

WhiteSpace

Well-known member
So here's the finished project: thanks again to those who provided help with the code. In spite of the explanations, I still couldn't work out some of the rounding in binary o_O, so I took the easier but less elegant route and did the rounding in the ASCII characters. I changed the 3 button cells for a 3.7V rechargeable, which lasts rather longer. I had an embarrassing moment when it didn't work after assembly. I had changed the pin for the temp sensor to pin 4 in the interests of easier routing, but hadn't properly read the instructions for READTEMP12 in the manual and had overlooked the fact that C.3/pin 4 on the 08M2 doesn't work with that command, so I had to break the strip and solder a small link across in order to connect to pin 3. Fortunately fairly easy to resolve! The pull-up resistors for the I2C connections to the OLED sit beneath the display. Just in case it's of any use to anyone else, the serial connection uses a 3-pin JST XH socket with the middle pin bent and extended forward so that I can fit it perpendicular to the stripboard strips. I like the JST connectors because of their size, and because they cost less than the jack connector for the serial cable. I have made a small interface board with the jack socket on it, and two separate leads off it: one to a JST plug for finished soldered projects and and one to a double row of 3 header pins for projects that are still on a breadboard. That part of it was a useful tip somewhere on this forum.

I'm quite pleased with the set-up. If anyone would like the stripboard layout, please let me know and I'll post it.

Temperature sensor.jpg
 

AllyCat

Senior Member
Hi,
.... I still couldn't work out some of the rounding in binary ....
Yes, with 16 "input" levels and only 10 (decimal) digits to display, it's never going to look quite "right". Strictly the .0 and .5 should appear only once each (in the 16) because 0.0625, 0.4375, 0.5625 and 0.9375 are nearer to the adjacent digits (.1 , .4 , .6 and .9). Then, 0.25 and 0.75 could either round down to .2 and .7 or up to .3 and .8 (and mathematically should be the opposite for negative temperatures) !

So personally, I'd just use a LOOKUP with digits that I think "look" the best (and the same sequence after converting the negatives to positive). My preference might be LOOKUP binarynibble , ("0012234555678890") , decimalcharacter although strictly there should be another 1 , 4 , 6 and 9 (in place of the extra 0s and 5s) and perhaps a 2 changed to 3 and/or an 8 to 7. But an advantage of the lookup method is that you can tailor the sequence exactly as you like (or believe to be correct). :)

With such a small board it's possible to use the PICaxe's internal Weak Pullup resistors for the I2C bus (the bus speed could be lowered in the unlikely event that the capacitance proved to be too high) and surprisingly the WPU also seems to work perfectly well for the DS18B20, even in its "probe" cabled form.

I'm still planning my (perhaps over-ambitious) version, with the "PCB" (stripboard) designed to fit in the space of a single AA cell, so that the whole assembly can be contained within a standard (lidded and switched) AA battery box (2x , 3x or 4x depending on the type of cells used). Current plans are to dual-use the 3.5mm programming socket to also accept the 18B20 probe (not trivial), and I believe that I can fit in a RTC and a serial EEPROM (hardware and software) for data logging (and large/fancy character fonts), together with a photodiode to auto-dim the display ! And a few other ideas, but time will tell ;)

Cheers, Alan.
 
Last edited:

WhiteSpace

Well-known member
Thanks @AllyCat. I concluded that the sensor error is unlikely to be very much less than any rounding error, so I opted for the simple approach. I'm very impressed by your "AA size" projects - you posted photos of a couple of them in my thread on "stuck at home projects" in March, and they were partly the inspiration for trying to squeeze my temperature display into the smallest possible space. I haven't quite reached your levels of sophistication, though, as you can see from the photo! I'll bear in mind the WPUs for other projects - I was vaguely aware that they existed but hadn't really thought about them. I'll take a look at the manual. In this case they aren't necessary for the I2C - the external pull-ups lie very neatly about half way along the OLED, fitting in the 2mm or so space behind the OLED where the plastic of the header pins lifts the display off the board.
 
Top