32-bit maths routines for BMP180, BMP280 and BME280 Atmospheric sensors.

AllyCat

Senior Member
#1
Introduction:

The BMP180 is a tiny, high resolution, Atmospheric Pressure (and Temperature) sensor which uses the I2C two-wire data bus, so it can be easily interfaced with most PICaxe chips, including the 08M2. Many ebay listings suggest the BMP280 "replaces" the 180, but this is rather misleading because it is not a "drop in" replacement. It uses different data formats and most of the "breakout" modules don't include the (required) 3.3 volt supply regulator which is included on most BMP180 modules. However, it can give even higher resolution results, around +/- 0.002 hPa (aka millibars) and 0.01 degree C, and adds other hardware options such as a SPI bus interface. Then, the BME280 (E for Environmental) adds Humidity sensing, again at modest cost such that these devices are worthy of consideration even if only one or two of the sensor functions are required.

However, an issue for PICaxe is that these sensors supply "Raw" (uncalibrated) measurements, together with up to 18 "Calibration Constants" (3 for Temperature, 9 for Pressure and 6 for Humidity), which need quite complex mathematical processing to deliver a useful result. There are several related threads on the forum, and one "Finished Project" for the BMP180, but it requires an X2 chip and my trial run using the PE5 simulator took half an hour to compute the test result (and returned an incorrect pressure value). Therefore, I have been looking to see if it is practical to use these sensors with an M2 PICaxe (Hint: Yes it is, or I wouldn't be bothering with this thread :) ).

For my "Library" subroutines and programs I try to use less than half of the "available resources" (Program and Variables Memory, etc.) of an 08M2. That won't be possible for the full gamut of Pressure, Temperature and Humidity sensing (however they should all fit within a single slot of any larger M2), but could give, for example, useful high resolution temperature sensing with an 08M2.

This is still a "work in progress"; I don't yet have test data or a validated program for the Humidity sensor, and perhaps my mathematical subroutines can be improved? This thread will (necessarily) be quite long; it will definitely spread into multiple posts, if only because of the 10,000 character forum limit, so I am including some subheadings to help readers find relevant (or skip irrelevant) details.

The Mathematics:

The Bosch data sheets for these devices are quite "readable", but the mathematical computations become progressively more obscure (IMHO). It is worthwhile looking at all three data sheets because they contain rather different details, for example only the BMP180 data describes how to convert a pressure change into altitude above sea level, whilst only the BME280 data includes the Humidity computation. Unfortunately the .PDF listings are in the form of "Images", or are "Protected", so it may be necessary to jump through a few hoops to transfer the computational details into a text editor (such as the PICaxe PE). Also, the data sheets give no indication of the range or function of the Calibration Constants, so the conversion of the equations or programs into PICaxe Basic must be mainly via a rather "dumb" replication process.

The BMP180 data includes a list of equations and a "worked example" with typical 32-bit data values. However, the BMP280 data sheet shows only sample "C" program code, primarily in 64-bit "Long-Long-Signed-Integer" form, but with options of 32-bit integer ("for 8-bit microcontrollers") and "Double-Precision Floating-Point" (for systems where integers are "not well supported"). Again there is a sample calculation, but using Floating-Point values, whilst the full listings use around ten different numerical formats (from nibbles up to 64-bit words, Signed and Unsigned integers, FP and a few "fractional binary" formats such Q22.10). The BME280 data sheet continues with the C program format, where a single line in the Humidity calculation contains no less than 33 pairs of partially-nested brackets () and there is no example data or calculation. :(

Although the "recommended" calculations use 64-bit integer maths, this is probably "overkill", at least for most PICaxe applications. The calibration data uses 16-bit integer words (mostly Signed, a few Unsigned) and the "Raw" measurements use 16 - 20 bits depending on the degree of "oversampling" (adding together repeated measurements). To maintain resolution in integer multiplication and division processes, twice this number of bits is required, but in practice 32 bits should be sufficient. The advantage of this is that for most processes we can simply "double-up" the PICaxe's normal Word variables, rather than working with groups of 4+ bytes or even strings of individual bits.

Basic Implementation:

32-bit (4 byte) variables can be stored in the same way as if we run out of (named) variables in PICaxe Basic, i.e. by using a "pointer" variable. This is similar to the use of read/write or peek/poke, or @bptr into the "extended" RAM area (beyond the named byte/word variables). In the same way that we need to step the pointer by 2 (bytes) for each complete Word variable, we must step it by 4 for each Double-Word. However, the Calibration Constants are only 16-bit words and never change for any individual sensor, so they can be read just once and stored in EEPROM. Alternatively, for simplicity or convenience (e.g. whilst debugging) they may be stored entirely in RAM and/or as 32-bit double-words, but generally the EEPROM (or Table) memory is most appropriate.

For high precision numerical calculations, of the type A = B * C , we need to create a software "Arithmetic Logic Unit" (ALU) using "normal" Word variables. But usually, the operator (e.g. the * multiplier, or + , / , etc.) will be part of a continuous sequence of operations, so it is more efficient and compact to use the form A = A * B . As with many ALUs, the primary variable has been named the "Accumulator" (ACC), with the second variable named BCC (to assist text editing). However, some operations act directly on the Accumulator alone, so a separate Auxiliary Word register (WA) may be used to "control" the operation, rather than disturb (overwrite) a running value in BCC. Thus, the basic ALU requires 5 words (or 10 bytes) of variable space, plus two "Pointer bytes" to load/save the registers. For most "internal" ALU operations the ACC and BCC are split into High and Low Words, namely AH, AL, BH, and BL.

Since the word variables "overlay" the normal RAM space, only one subroutine is required to move data between any of the ALU Registers or Memory (Double-) Words. For this subroutine I've used the keyword "COPY" to indicate that the source value remains unchanged. However, the current routines do NOT reinstate the Pointers on exit (Return) because this appears to be (mainly) unneeded. Conversely, the "WORD" qualifier in PICaxe data transfers does maintain the pointer value, but these appear to be "Pseudo" commands (generated within the PE), so separate byte commands can be more efficient. Nearly all the "ALU" functions must be within Subroutines (not Macros) because most will be used many times throughout the program.

If the calibration data are stored as 16-bit words and/or in EEPROM, then a second subroutine is needed to load them into the 32-bit Long-Word format. The keyword "GET" is used here (no reverse path is relevant), which might be used only when loading the data values from the sensor chip via I2C, or more probably "on the fly" during the normal calculations. Most of these constants are Signed (a few are Unsigned), so when expanding a Word to fill a Double-Word variable, it's necessary to "Sign Extend" the most significant bit (the sign bit) all through the upper two bytes.

Of course PICaxe Basic doesn't support Signed arithmetic, but ADD (ition) works "automatically" if the operands are in "two's complement" format (i.e. where the MS bit = 1 indicates a negative number). But for other operations such as multiplication and division it's necessary to convert negative numbers to positive, perform the operation on the positive numbers and then convert back to negative if one (but not both) of the operands was negative.

A detailed description of the subroutines will take me past the 10,000 character forum post limit, so I will continue in another post (in due course). The impatient can take a look at (and try in the simulator) the Finished Project linked above. ;)

Cheers, Alan.
 
#2
I found this (sorry, can't remember the author) on this forum.
Has additions for my application, but may be of use.

Code:
hi2csetup i2cmaster, $EE, i2cslow, i2cbyte  ;BMP180 base address

pressure:
	hi2cin $D0,(b10)		'read sensor id - read only value, never changes
	if b10 <> $55 then	'should be $55
	 serout ip_out,baud,("BMP180 Not Found",cr,lf)
	 return
	endif
	
	pause 100
	hi2cin $B4,(b3,b2)
	pause 10
	hi2cout $F4,($2E)
	pause 50
	hi2cin $F6,(b5,b4)
	pause 10
	hi2cin $B2,(b7,b6)
	pause 10
	w4=w2-w1*2**w3
	hi2cin $BC,(b3,b2)
	pause 10
	if w1>32768 then: w1=0-w1: endif
	hi2cin $BE,(b5,b4)
	pause 10
	w2=w4+w2
	w3=w1/w2

	for b0=1 to 11
	 w3=w3*2
	 w1=w1//w2*2
	 w3=w1/w2+w3
	next b0

	hi2cin $BC,(b3,b2)
	pause 10
	if w1>32768 then: w3=0-w3: endif
	w2=w3+w4
	w1=w2+8/16
	b20 = w1 /10
	b21 = w1 // 10
	serout ip_out,baud,("Loft Temp: ",#b20,".",#b21,$b0,"C",cr,lf)
	w3=w2-4000
	w5=w3
	if w5>32768 then: w5=0-w5: endif
	w5=w5*4
	w5=w5**w5
	w5=w5*4
	hi2cin $B8,(b13,b12)
	pause 10
	w5=8*w6**w5
	hi2cin $AC,(b9,b8)
	pause 10
	w7=w4
	if w4>32768 then: w7=0-w7: endif
	w6=w3
	if w6>32768 then: w6=0-w6: endif
	w7=w7*8
	w6=4*w6**w7
	if w4<32768 or w3<32768 then: w6=0-w6: endif	'if either is positive then negate
	w7=w5+w6
	hi2cin $AA,(b17,b16)
	pause 10
	w8=w8*4+w5+w6+2/4
	hi2cin $AE,(b19,b18)
	pause 10
	w9=0-w9
	w10=w3
	if w10>32768 then: w10=0-w10: endif
	w5=w10*8**w9
	w9=w10*4
	hi2cin $B6,(b21,b20)
	pause 10
	w6=w9**w9**w10
	w7=w5+w6+2/4
	hi2cin $B0,(b21,b20)
	pause 10
	w9=32768+w7
	w11=w9**w10
	w10=w9*w10
	w9=w11*2
	w10=w10/32768
	w9=w9+w10
	hi2cout $F4,($F4)
	pause 100
	hi2cin $F6,(b21,b20)
	pause 10
	w10=w10-w8
	w8=w10**50000	'high word of B7
	w7=w10*50000	'low word of B7
	if w9>32768 then
	 w9=w9/2
	 b1=1
	else
	 if w8<32768 then
	  b1=1
	  w8=w8*2
	if w7<32768 then
	 w7=w7*2
	else
	 w7=w7*2
	 w8=w8+1
	endif
	else
	 b1=0
	 endif
	endif

	w3=0
	w2=w8/w9

	for b0=1 to 16
	 w3=w3*2
	 if w2>32768 then: w3=w3+1: endif
	 w2=w2*2
	 w8=w8//w9*2
	 if w7>32767 then: w8=w8+1: endif
	 w7=w7*2
	 w2=w8/w9+w2
	next b0

	if b1=0 then
	 w3=w3*2
	 if w2<32768 then
	  w2=w2*2
	else
	 w2=w2*2
	 w3=w3+1
	 endif
	endif

	b9=b6
	b8=b5
	w5=w4**w4
	w4=w4*w4
	w5=w5*3038
	w5=w4**3038+w5
	w6=w3*7357
	w6=w2**7357+w6
	w5=w5-w6+3791
	w7=w3

 	if w5>32768 then
	 w5=0-w5/16
	 w6=w2-w5
	if w6>w2 then: w7=w7-1: endif
	else
	 w5=w5/16
	 w6=w2+w5
	if w6<w2 then: w7=w7+1: endif
	endif

	b7=b14
	b6=b13
	b4=b12
	bptr=28
	b5=w3//10
	@bptrinc=w2//10
	w2=w2/10
	w3=w3/10
	b5=b6

	do until w2=0
  	@bptrinc=w2//10
  	w2=w2/10
	loop

	bptr=bptr-1
	serout ip_out,baud,("Barometric Pressure: ")
	do until bptr <28
  	if bptr = 29 then serout ip_out,baud,(".") endif
  	serout ip_out,baud,(#@bptrdec)
	loop
	serout ip_out,baud,("mB",cr,lf)
Seems reasonably accurate - against my wall mounted weather station & the Met Office.
edit: Should have added, it's running on a 14m2

Neil.
 
Last edited:

SteveDee

Senior Member
#3
I'm a mathematical light-weight, so I struggled a bit when I wrote software for the BMP180 and BME280 sensors using Gambas. What helped me was being able to run one of the C program examples and using it to check the accuracy of my Gambas code. This approach revealed a few unexpected bugs (mostly due to data type selection).

Checking measurements against another temp/humidity device is another useful check, but I think comparing your results with a C program example should be the primary check.

I suspect many of the devices on sale are not quite what they appear (e.g. maybe some are fake or they have not been assembled to the circuit board with enough care). My first BME280 started giving high readings for RH after just a few months and had to be replaced.
 

AllyCat

Senior Member
#4
Hi,

@Steve: Yes, some of the BME280s do seem suspiciously cheap (about the same price as a DS18B20) so I don't have high expectations for mine, still on their way from China. But my modestly-priced test BMP280 (using code transcribed for PICaxe) has been completely consistent with the pressure from the NPL on-line barograph at Teddington, not too many miles from me.

This approach revealed a few unexpected bugs (mostly due to data type selection).
Were those bugs in the Data Sheet or your transcription? I'm rather concerned that Bosch are no longer giving typical values/calculations in the Data Sheet, and their "It is strongly advised to use the API from Bosch Sensortec ........ the code below can be applied at the user's risk" doesn't entirely inspire confidence. They certainly don't appear to have made their code as User-Friendly as it might have been. :(

@Neil: That program code is quite compact and does appear to give fairly good results with the standard test values (the last digit of the pressure looks a little high), but a few program Comments and Symbols would have been nice ! It reads the Calibration Constants directly from the I2C bus "on the fly" (i.e. when required) which minimises memory usage, but makes it impossible to Simulate or Debug the program without alterations. Having already written code for the BMP180, it wasn't too difficult to "reverse engineer" it with SERTXDs and fixed Calibration Constants, etc., but I was left with a "lump" of instructions with no apparent structure.

It's remarkable that the program includes no subroutines at all, and appears to just use (all) the named PICaxe 16-bit integer variables, with some "hand-crafted" sections to handle negative values, scaling (to maintain accuracy) and doubled-words (High:Low) "when required". I don't know whether such a process can still be applied to the BMx280s which potentially offer around ten times higher resolution (20 bits) than the 180. But my main aim here is to provide some generic subroutines for the whole family of sensors and I may not get to any device-specific coding for a few posts yet. ;)

Thanks, Alan.
 
#5
Alan,

I agree about the comments & symbols, but I don't recall them being in the orginal info I found (otherwise I would have thought I'd have left them in).

Pity I can't remember who the original author was !.

edit: Think I found it - here, post 6 seems to be the one. I must have removed all the comments for my application.

Neil.
 

SteveDee

Senior Member
#6
...some of the BME280s do seem suspiciously cheap...Were those bugs in the Data Sheet or your transcription? ...
I worry that when it is known that bad devices are available on the market, the asking price may not be a guide to quality (i.e. you could end up spending 4x as much for a piece of junk that you could have got much cheaper). At least if you buy cheap, you are sometimes pleasantly surprised!


Although I didn't find the data sheet for the BME280 as easy to follow as the BMP180, the errors were down to me; either misunderstanding the Memory Map, selecting the wrong data type, mis-counting brackets, or putting the matching bracket in the wrong place.

Getting the right result for humidity seemed more of a chore than for temperature. As an example, this is my RH routine which takes the raw humid reading (lngRawHumid) and applies the necessary corrections;
Code:
Public Function HumidityCalculation() As Float
Dim intVar As Integer 
Dim fHumid As Float
  
  intVar = intTempFineResolution - 76800
  intVar = Shr((Shl(lngRawHumid, 14) - Shl(CDigH4, 20) - (CDigH5 * intVar)) + 16384, 15)
  intVar = intVar * Shr(((Shr(Shr(intVar * CDigH6, 10) * (Shr(intVar * CDigH3, 11) + 32768), 10) + 2097152) * CDigH2 + 8192), 14)
  intVar = intVar - Shr((Shr(Shr(intVar, 15) * Shr(intVar, 15), 7)) * CDigH1, 4)
  If intVar < 0 Then
    intVar = 0
  Endif
  If intVar > 419430400 Then
    intVar = 419430400
  Endif
  fHumid = Shr(intVar, 12) / 1024
  Return fHumid
Catch
  lblStatus.Text = "HumidCalc: " & Error.Text
End
There is more left and right shifting going on there, than at a typical Barn Dance!

The device address will depend upon the circuit board that the device is mounted on. There might be some generic notes of interest on my Pi post: https://captainbodgit.blogspot.co.uk/2017/05/replacing-dht22-with-bosch-bme280.html
 

AllyCat

Senior Member
#7
Hi,

@Neil: The program seems to date back to Bill Wann in post #10 here in 2012. I just put "if either is positive then negate" into the forum search and it found only 3 hits (including this current thread). I chose that search string because the whole line appears to be not strictly correct (what if both are positive), so it seemed fairly "unique". ;)

IMHO Bill writes some clever maths programming, but the problem here is that after the BMP085 and 180, Bosch seem to have made significant changes to their coding methodology (including reversing the sequence of High and Low bytes), so "adapting" the code for BMx280 may not be too easy. Also the "it appears to work" might not be adequate for somebody with a "seat" on a High Altitude Balloon flight, as there was recently, which was the original inspiration for this thread.

@Steve: You can say that again about the brackets. And the problem with PICaxe Basic is that it doesn't support them. :( The number of times that I've been through that one line in the Humidity calculation, trying to match up all the "closes" with the "opens"............... I believe I've done it now, and with only two additional (stored) variables (i.e. two levels of nesting), but I won't know for sure until my BME280s (or will they just be junk) turn up on the doormat.

Cheers, Alan.
 

stan74

Senior Member
#9
this has caused so much concern on another forum. with no floats or signed long the math is difficult. proton basic is source for solution, gcb people trying with long vars and converting c. have fun, I bought one for £1.80 but not sorted yet...back burner project
 

AllyCat

Senior Member
#10
Hi,

It could be considered an "advantage" of PICaxe Basic that high accuracy maths calculations aren't supported, so it's necessary to devise some custom subroutines which then can be optimised for the particular application. Thus one of the purposes of this thread is to document some 32-bit "ALU" routines, with the BMx 280 sensors only used as a convenient demonstration vehicle. I did much the same with the 16-bit trig routines used in my 08M2 "Worldwide" Sunrise/Sunset calculation program.

As far as the hardware is concerned, I've had no issues purchasing BMP180/280 modules (mainly from China) but it appears that some ebay sellers are confusing the BME280 sensors (which add Humidity) with the BMP280. That may be due not just to the similar part numbers, but because the packages have the same pinout, so many of the "breakout" PCB modules are marked with both prefixes. This seems to be a particular problem with the "6-pin" (sometimes described as 3.3v) modules, so if you need a BME, ensure that "Humidity" appears in the listing title. Also check that the picture shows a square metal package (not rectangular), but you might still receive a BMP. :(

The "4-pin" BME modules (sometimes referred to as "5 volt") appear to be a better choice because they include a regulator and 5 volt bus interface FETs (on the underside of the PCB so not always obvious in photographs). Or the "10 pin" modules (6 pins on one side, 4 the other) if you want a direct SPI bus interface capability (at 3v3). Also, the price may be some guide; BMPs are available for $1 upwards from China and £2 if UK-sourced, whilst a "genuine" BME 280 appears to cost more than US$3, even direct from China.

But back to the main topic of this thread, the 32-bit maths/software description will continue in my next post....

Cheers, Alan.
 

AllyCat

Senior Member
#11
Hi,

The ALU Subroutines:

Here are the provisional ALU subroutines with a few extra "preambles" to give some additional functions, quite efficiently by "falling through" into the main routines. They also share some exit path / Returns to save a few more bytes / delays, but purists might prefer a more formal structure. The final mix may depend on the overall calculations required; some operations may occur frequently enough to justify a dedicated subroutine, or others prove unnecessary. The Bosch computations are rather unusual in mixing both signed and unsigned maths, so more efficient algorithms might be possible.

The listings include a few Conditional Compilation options (#IFDEF compiler directives) to suit the application. The "diagnostics" information needs to be included only whilst debugging, and the "(auto-)scaling" before Division only if divisors greater than 16 bits might be encountered in the calculations. With neither, the code compiles to around 250 bytes.

Code:
; 32-Bit Subroutines, for BMP280, etc,   AllyCat, December 2017.
 
#picaxe 08m2           ; And most others
; #no_data
; #define showDIAGS	; For debugging
#define scaleDIV 	; Scale divisor down to 16 bits

symbol AL = w8             ; Low word of Accumulator
symbol AH = w9             ; High word of Acc
symbol BL = w12            ; Low word of Bcc register
symbol BH = w13            ; High word of Bcc register
symbol WA = w4             ; Auxiliary Word register
symbol sign = bit7  		; Flags within b0
symbol carry = bit6 
symbol dloop = bit4  		; For 16 passes
symbol from = b0  		; Source pointer

symbol Acc = 16			; Pointer to register A
symbol Bcc = 24			; Pointer to register B

; 32 bit ALU SUBROUTINES:

doubleA:                           ; Double the value in Acc (uses Bcc register)
   BH = AH : BL = AL                     ; Copy A into B and fall into the Addition routine
addAB:					; LWA = LWA + LWB , Bcc unchanged
	AL = AL + BL			; Add low words
	AH = AH + BH			; Add High words
	if AL < BL then			; Add Carry
		inc AH
	endif
return

negateA:				; Twos complement of AH:AL
	AL = not AL + 1
	AH = not AH
	if AL = 0 then
		inc AH
	endif
return

negateB:				; Twos complement
	BL = not BL + 1
	BH = not BH
	if BL = 0 then
		inc BH
	endif
return	

makepos:				; Convert Acc and Bcc to positive and set result sign flag
	b0 = 0				; Clear flags and loop counter
	if AH > 32767 then
		sign = 1
		call negateA
	endif
	if BH > 32767 then
		sign = sign xor 1	; Toggle the sign flag 
		call negateB
	endif
return

squareA:				; LWA = LWA * LWA
	BH = AH : BL = AL		; Copy Acc then fall into multiply routine (result always positive)
umult:					; Unsigned multiplication
	WA = AL * BL			; Temp store Low word
	AH = AH * BL			; Partial high word
	AH = AL ** BL + AH
	AH = AL * BH + AH		; Complete the High word
	AL = WA
return

smult:					; Signed Multiplication  LWA = LWA * LWB	
	call makepos
	call umult
resign:					; Correct the sign if required
	if sign = 1 then goto negateA	; Then use its Return
	return

sdiv:		; Signed Division	; LWA = LWA / LWB (if BH > 0 then scale down both)
	call makepos
	call udiv
	goto resign
udiv:					; Unsigned Division
#ifdef scaleDIV 			; Scale divisor down to 16 bits
	WA = 1				; Required scaling binary divisor
	do while BH > 0			; Scale down until BH = 0
		WA = WA + WA		; Double the value
		BL = BL / 2		; Shift right
		BL = BH and 1 * 32768 + BL	; Add carry
		BH = BH / 2		; Shift right completed
	loop	 
#IFDEF showDIAGS
	if WA = 1 then goto div3216H		; Faster but adds ~9 bytes to code
	sertxd(" Divisor Reduced ")   	; Scaled down to a single word
#ENDIF
	call ScaleA		; Scale the numerator correspondingly
#endif

div3216H:		; Calculate High Result word for 32 by 16 bits division
	BH = AH / BL * BL		; Divisible part of High byte 
	AH = AH - BH			; Subtract to give numerator for Low calc
	BH = BH / BL			; High Result
div3216L:	; Calculate Low Result and (Low) Remainder of 32 by 16 bits division
	b0 = b0 andnot 31		; Clear counter flags
	do while dloop = 0		; Flag in b0 is set after 16 iterations
		inc b0 
		carry = AH ** 2 	; Set or clear carry flag
		AH = AL ** 2 + AH + AH	; Carry from low word and shift left
		AL = AL + AL
		if AH >= BL OR carry = 1 then	; Can subtract
			AH = AH - BL	; Subtract divisor from the numerator
			inc AL		; Set LS bit in result
		endif
	loop
	AH = AH xor BH 	   		;)
	BH = BH xor AH 	   		;} swap AH , BH  (AH was remainder)
	AH = AH xor BH       		;)	
return

ScaleAs:				; Rotate Right with sign extension
	if AH < 32768 then ScaleA	; Positive so no sign extension required
	call negateA			; Make the value positive
	call ScaleA
	goto negateA			; Restore the negative sign and Return

ScaleA:					; Rotate Right by dividing by WA (1 - 32768)
	AL = AL / WA
	AL = 32768 / WA * 2 * AH + AL
	AH = AH / WA
return					; WA unchanged
The most-used routine is NEGATE which converts a positive number to its two's complement negative value and vice-versa. It's potentially required for both operands in signed multiplication or division and also avoids the need for a separate Subtraction routine. As such, it's the only subroutine which appears worthwhile to apply directly to both Acc and Bcc. Alternatively (if other operations are also directly applicable to Bcc) a SWAP (exchange Acc and Bcc) routine might be used, but an 8-byte exchange in PICaxe Basic is not very efficient.

To ADD (A + B) , the Low and High words are individually added, with a one bit carry from Low to High when appropriate. This also handles subtraction by negating either A or B before the addition.

Signed MULTIPLY first needs to Negate any negative values, then calculate the result by adding all the products involving a Low word. This assumes that the operands are small enough to avoid an overflow of the 32 bit result (or strictly 31 bits for signed values) because there is no error-handling capability. Before Return the result is negated when appropriate.

To maintain resolution after integer multiplication and division (or to avoid overflows) it's often necessary to "scale" the operands by Right-Shifting or Left-Shifting them by a specific number of data bits. Left-Shifting is equivalent to multiplying by a power of two, but is not required very often and the multiplication subroutine is quite fast, so there is no need for a dedicated Left-Shift routine. The power of 2 could be obtained from a LOOKUP shift,(1,2,4,8,16,.....,32768) command, but the program size is minimised by simply embedding the actual multiplier within the program.

Similarly, Right-Shifting could use the main Division subroutine, but division is a much slower procedure and there are other benefits in having a dedicated Right-Shifting subroutine. With an M2 this must use Basic division, but by limiting it to a maximum of 15 steps (i.e. dividing by 32768) we can use a single Word register (WA) and leave Bcc unchanged, as a temporary store if required. Shifting by more than 15 steps is rarely required and can be achieved by simply calling the subroutine twice. Again, a LOOKUP could be used for the division factor but the program space is reduced by pre-calculating the divisor.

The operand in the Acc might be negative so it may be necessary to "Sign Extend" the result, but the simplest method appears to be to convert negative numbers to positive and then vice-versa after the Right-shifting. In naming these subroutines I've avoided the term "Shift" or >> characters because the required parameter is a divisor (not the number of shifts). but also avoided a "Division-based" name because this routine is specific to powers of two. Therefore, I've used "Scale" to indicate Right-Shifting.

Theoretically, a full 32 bit by 32 bit Division routine might be needed (but hasn't been encountered in my simulations so far) and hippy has documented one in post #38 here . However, to make the program more compact, I've chosen to (optionally) pre-scale the Divisor (and Numerator correspondingly) down to 16-bits when / if required, using the Scaling (right-shifting) routine above. If the divisor were more than 16 bits, then the result must have less than 16 bits, so some resolution would be lost anyway.

The 32/16 bits DIVISION routine is adapted from a Code Snippet that I documented previously. Again, Signed and Unsigned versions might be required so the main SDIV routine first tests for (and negates) any negative value(s). The High word of the result is easily calculated by PICaxe Basic (and stored in BH). Then the Lower Result word is constructed by repeated left-shifts of the Numerator and the Divisor conditionally subtracted. Therefore the division process is rather slow, but much faster than 32/32 bits. On Return, the Divisor (in Bcc) contains a maximum of 16 bits and remains in BL, whilst BH and AH are swapped, so that the Remainder is in BH with the full 32-bit Result in Acc (AH:AL).

Well, that's nearly another 10,00 characters. To be continued....... ;)
 
Last edited:

AllyCat

Senior Member
#12
Hi,

Further Subroutines:

To perform useful computations it's necessary to move data into and out of the ALU registers, but only a single COPY subroutine is needed to move the 32-bit Long-Words around the entire RAM area. Using the PICaxe's BPTR to define the destination is most efficient, particularly with its "post increment" option, but for debugging and demonstration an alternative compiler option (#IFDEF) uses a normal byte variable pointer. Then. all the variables used in the computations can be shown at the top left-hand-side of the Simulator RAM window

However, a separate routine is needed to GET the 16-bit Calibration Constants into the ALU registers, because the high word needs to be cleared, or negative values Sign-Extended up to 32 bits. A further compilation option allows the constants to be stored in either EEPROM or RAM: Normally, EEPROM is more appropriate, but RAM can be useful for debugging, or to save EE/Program memory space in an 08M2.

Optional subroutines:

For testing and debugging, an additional subroutine can send the "expected" and actual (Low Word) values to the Terminal Emulator at appropriate stages of the computation. After testing, the subroutine contents can be conditionally excluded, but this doesn't remove the subroutine CALLs, which need to be individually commented out (or conditionally compiled). If the required value is not known (or the Low word not meaningful) then the "expected" value can be set as (-)32768 which is not displayed.

The subroutines described above should be sufficient for all the required calculations, but a few operations may occur frequently enough that it's worthwhile to include some additional entry points. The SQUARE (of the operand in Acc) can be very easily implemented by copying Acc into Bcc and then "falling into" the Unsigned Multiplication routine (the square of a number is always positive). Similarly Acc can be DOUBLED (or shifted left by 1 bit) by copying Acc into Bcc and falling into the ADD subroutine, as was shown in the listings in the previous post.

Another operation that appears several times in the Bosch calculations is of the form "A = A + 128 >> 8". which simply "Rounds" the LS bit of the result when scaling down (Right-shifting). In that basic form, a zero always needs to be written to BH (the High Word of Bcc) and WA/2 created in BL, so any existing value in Bcc must be overwritten. Therefore, a custom "Rounded Scaling" routine can be more efficient: It starts with HALF of the normal scaling factor in WA, which it adds to Acc, then doubles WA before jumping to (or falling into) the normal Scaling subroutine. Theoretically, both Signed and Unsigned versions might be needed, but Rounding is mainly appropriate for relatively "small" numbers (compared with 32 bits) so it's reasonable to implement only a Signed version (i.e. with positive values limited to 31 bits).

So here are the remaining Subroutines, which bring their total code size to around 350 bytes.

Code:
#define useBPTR     ; More efficient but a byte variable pointer may be easier when debugging
#define useEECAL    ; RAM may be more useful when debugging and with 08M2
#define showDIAGS    ; Report debugging data to the Terminal Emulator

;symbol from = b0    ; Data source pointer
symbol tempb =  b8  ; Temporary data byte
symbol dest = b9    ; Destination pointer (when BPTR is not used)

copyRAM: 		; Copy Long-Words between RAM pointers
#ifdef useBPTR  		
	peek from,@bptrinc : inc from
	peek from,@bptrinc : inc from
	peek from,@bptrinc : inc from
	peek from,@bptrinc			
#else
	peek from,tempb : inc from   ; Could use a loop if program space is more precious than variables 
	poke dest,tempb : inc dest
	peek from,tempb : inc from
	poke dest,tempb : inc dest	
	peek from,tempb : inc from
	poke dest,tempb : inc dest
	peek from,tempb
	poke dest,tempb
#endif	
return

getCAL:					; Copy Calibration word to Long-word
#IFDEF useBPTR
#ifdef useEECAL					; Calibration constants in EEPROM
	read from,@bptrinc : inc from
	read from,@bptr
#else						; Calibration Constants in RAM
	peek from,@bptrinc : inc from
	peek from,@bptr	
#endif
	WA = 0					; Clear temp store for sign extension
	if @bptrinc > 127 then	   		; Test sign and advance pointer 
		WA = 255		  	  	; Negative sign extension
	endif
	@bptrinc = WA				; Sign extend from 16 to 32 bits
	@bptr = WA
return

getcalU:					; Unsigned, so clear the sign extension
	call getcal
	@bptrdec = 0
	@bptr = 0
return

#ELSE	   ; Don't use bptr
#ifdef useEECAL					; Calibration Constants in EEPROM
	read from,tempb : inc from
	poke dest,tempb : inc dest
	read from,tempb : poke dest,tempb
#else						; Calibration Constants in RAM
	peek from,tempb : inc from
	poke dest,tempb : inc dest
	peek from,tempb : poke dest,tempb	
#endif
	if tempb > 127 then
		tempb = 255			; Negative sign extension
	else
		tempb = 0			; Clear temp store for sign extension		
	endif
	inc dest : poke dest,tempb
	inc dest : poke dest,tempb		; Sign extend from 16 to 32 bits
return

getcalU:					; Unsigned, so overwrite the sign extension
	call getcal
	poke dest,0 : dec dest
	poke dest,0	
return
#ENDIF	

scaleAR:      ; Shift Right, rounding the LS bit of the Result
	AL = AL + WA
	if AL < WA then		; Carry bit from Low to High Word
		inc AH
	endif
	WA = WA + WA		; Double the (halved) value in WA 
        goto scaleAS

show:				; Report expected and actual Low Word values for testing
#ifdef showDIAGS
	if WA = 32768 then				; "Unknown" value
		sertxd(" #")
   else
	   sertxd(" ",#WA)
   endif
	sertxd("=",#AL)
#endif
return
Macros:

The program code above is written in the form of Subroutines because it needs to be executed many times during the calculations, so is compatible with PE5. But calling the subroutines is itself very repetitive, particularly when moving data into the Acc and Bcc registers. Macros (which are only available in PE6) make this programming simpler and also can help to avoid errors (particularly in forgetting to load one of the pointers, or to include the CALL keyword). However, the PE6 > File > Options > Diagnostics > "Display Pre-processor output" allows the output from the compiler to be copied back into PE5 (which may have a faster Simulator). The number of macros doesn't affect the final (compiled) program size, but the following should be sufficient for most purposes.

Code:
#macro showDEB(expected)  ; Show sample test values for debugging (Unsigned)
   wA = expected           ; Can't conditional compile (because of nested #s) so ...
   call show               ; Comment out these two lines for final version
#endmacro

#macro copy(source,destination)	; Copy Long Words between RAM locations
	from = source : dest = destination : call copyram
#endmacro

#macro get(source,destination)	; Fetch Calibration Constant (Signed)
	from = source : dest = destination : call getcal
#endmacro

#macro getU(source,destination)	 ; Fetch Calibration Constant (Unsigned)
	from = source : dest = destination : call getcalu
#endmacro

#macro scale(divis)	; Right-Shift (Unsigned)
   WA = divis : call scaleA
#endmacro

#macro scaleS(divis)	; Right-Shift (With Sign Extension)
   WA = divis : call scaleAS
#endmacro

#macro scaleR(divis) 	; Right-Shift (With Rounding)
   WA = divis : call scaleAR
#endmacro

#macro copyAB		; Copy Acc to Bcc
   BH = AH : BL = AL 
#endmacro
Unfortunately it's not possible to nest a #macro within a conditional compilation, so it's necessary to "comment out" the contents of the showDEBUG macro for the final application. The COPY and GET macros require the source and destination pointers to be specified every time, which is one reason why the subroutines do not bother to restore them before each exit/Return. But if WA is unchanged then the corresponding SCALE subroutine could be called directly. The COPYAB macro doesn't use a subroutine because its code size is comparable to a basic subroutine CALL (which is slower).


My next post will show a full calculation program for the BMP180 using these Subroutines and Macros.
 
Last edited:

AllyCat

Senior Member
#13
BMP 180 Temperature and Pressure calculations

Normally the Calibration Constants (and Raw data values) will be received via the I2C bus (either at initialisation or "on the fly"), but for simulation / debugging a Compilation option uses DATA from the example in the device Data Sheet (or previously downloaded from a real chip). AFAIK the DATA statement supports only byte values, so for convenience, the whole 16-bit value is written into the Low byte position (the High byte content is ignored) and the true High byte value written into the next byte.

For simplicity, the Calibration Constants are stored at the same byte memory address as when mapped in the source chip. But alternatively, the addresses may be OFFSET to better fit into the available RAM/EE memory space, particularly for he 08M2.

As far as possible, the program uses the same constant/variable names as the BMP180 data sheet, but the (double) Words "B1" to "B7" create a conflict with PICaxe nomenclature. However, B1 - B7 actually represent a range of Signed/Unsigned and Constant/Variable numbers, so a suffix is added to indicate the type. Normally the Raw Temperature and Pressure values will be obtained on the fly from the I2C bus, so these are always written directly in 32-bit form (via Acc and Bcc) to the RAM, and reported on the Terminal output.

The calculations for the BMP180 sensor are quite well documented in the Data Sheet. Each stage uses only a few variables so can be included in the program as a single comment line, together with the expected result (from the example data). The program then follows each equation with the appropriate sequence of PICaxe commands and subroutines. Sometimes it's necessary to pull a starting value from the memory area and/or to store the result back into RAM, but as far as possible the data is chained on to the next stage via the Acc register.

The following program, with the routines from the two previous posts pasted in, should calculate the Temperature and Pressure in the mode determined by the Compiler Configuration options. However, a sample compiler output file (for an 08M2 with all data in the RAM map) is also attached, which should run in the PE5 or PE6 simulator.

Code:
; BMP180 Temperature and Pressure calculation.  AllyCat,  December 2017.

; * Paste the compiler options, symbols and MACRO definitions here *

#define useTESTDATA
symbol WAL = b8			; Low byte of WA, etc. 
symbol WAH = b9
symbol ALO = b16			; Lowest byte of Acc, etc (ALL is a reserved word)
symbol ALH = b17
symbol BLL = b24
symbol BLH = b25

symbol OSSval = 8		; 1,  2,  4,  8 		; Over-sampling divider (not shifts)
symbol ossflg = $F4		; $34,$74,$B4,$F4 	; OSS configuration flags 
symbol ossdel = 26		; 5, 8, 14, 26		; Conversion delay (ms)

#ifdef useBPTR
symbol dest = bptr
#else
symbol tempb = b8
symbol dest = b9
#endif

; RAM Variables pointers (to LSB of 32 bits)	; u = Unsigned, s or v = Signed variable
symbol UT = 32			; Raw Temperature (27898 , 108)
symbol UP = 36			; Raw Pressure (23853 , 93)
symbol X1 = 40
symbol X2 = 44
symbol X3 = 48
symbol B3s = 52
symbol B4u = 56			; Unsigned variable
symbol B5s = 60
symbol B6s = 64
symbol B7u = 68			; Unsigned variable
symbol T180 = 72
symbol P180 = 76

; CALIBRATION values (16 bits) in RAM or EEPROM ; p=positive (unsigned), c=calibration
symbol OFFSET = $AA - 80         ; Move to Valid RAM area for 08M2
symbol AC1 = $AA - OFFSET			; 408
symbol AC2 = $AC - OFFSET			; -72  = 65464 , 255
symbol AC3 = $AE - OFFSET			; -14383 = 51153 , 199
symbol AC4p = $B0 - OFFSET			; 32741 , 127	; Positive (Unsigned)
symbol AC5p = $B2 - OFFSET			; 32757 , 127	; Positive (Unsigned)
symbol AC6p = $B4 - OFFSET			; 23153 , 90	; Positive (Unsigned)
symbol B1c = $B6 - OFFSET			; 6190 . 24
symbol B2c = $B8 - OFFSET			; 4
symbol MBc = $BA - OFFSET			; -32768 = 32768	; Not used
symbol MCc = $BC - OFFSET			; -8711 = 56825 , 221
symbol MDc = $BE - OFFSET			; 2868 , 11

#IFDEF useTESTDATA
data $AA,(408,1,65464,255,51153,199,32741,127,32757,127)	; From BMP180 data sheet
data $B4,(23153,90,6190,24,4,0,32768,128,56825,221,2868,11)

#ifNdef useEECAL
	bptr = $AA - OFFSET
	for from = $AA to $BF
		read from,@bptrinc		; Copy data bytes to RAM
	next  
#endif
do
	WA = 27898	 		; Raw temperature from BMP180 datasheet 
	poke Bcc,word WA,0,0 		; 27898 , 108   (Hi byte included in low word)
	WA = 23843			; Raw pressure
	poke Acc,0,word wA,0 		; 23843 , 93
	symbol OSS = 1	
#ELSE    ; Read data from the I2C bus
	symbol OSS = OSSval			; 1,  2,  4,  8 	; Over-sampling Divider (not shifts)

	hi2csetup i2cmaster,$EE, i2cslow, i2cbyte 		; BMP180 address
	hi2cin $D0,(b0)							; read sensor id 
	sertxd(cr,lf,"BMP180 ID= ",#b0,cr,lf)

	for from = $AA to $BF step 2
		dest = from - OFFSET      
		hi2cin from,(ALH,ALO)			; High  byte first
#ifdef useEECAL
		write dest,ALO,ALH
#else
		poke dest,ALO,ALH
#endif
	sertxd(" ",#wA)
	next from
do							; Loop to repeat Temp and Pressure measurements
	hi2cout $F4,($2E) 			; Initiate temperature conversion
	pause 10					; Allow time for conversion
	hi2cin $F6,(BLH,BLL)			; Read raw Temperature data into Bcc
	hi2cout $F4,(ossflg) 			; Initiate pressure conversion
	pause ossdel				; Allow time for conversion
	hi2cin $F6,(AH,ALH,ALO)		; Read raw Pressure data into Acc
	BH = 0 					; Fill 32-bit data field
#ENDIF      ; useTESTDATA

	copy(Bcc,UT) 				; Store Raw Temperature
	scale(256 / oss)				; Scale Acc
	copy(Acc,UP)  				; Store Raw Pressure
	sertxd(cr,lf,"BMP180 UT= ",#BL," UP= ",#AH,":",#AL)	
	
; BMP180 TEMPERATURE CALCULATION:

;* X1 = (UT - AC6) * AC5 / 2^15 == 4743
	getu(AC6p,Acc)  				; Unsigned calibration AC6
	copy(UT,Bcc)				; Raw temperature
	call negateA :	call addAB 		; Subtract
	getu(AC5p,Bcc)				; Unsigned calibration AC5
	call smult
	scale(32768)				; Shift Right 15 bits
	copy(Acc,X1)				; Store in X1
ShowDeb(4743)
;* X2 = MCc * 2^11 / (X1 + MDc) == -2344
	get(MDc,Bcc)				; Signed calibration MDc
	call addAB
	copy(Acc,X2)				; Temporary store
	get(MCc,Acc)				; Signed calibration MCc
	BH = 0 : BL = 2048 : call smult 	; Directly enter the multiplier
	copy(X2,Bcc)
	call sdiv  					; Don't need to store X2
ShowDeb(-2344)
;* B5 = X1 + X2 == 2399
	copy(X1,Bcc)   
	call addAB
	copy(Acc,B5s)					; Store B5
ShowDeb(2399)
;* T = (B5 + 8) / 2^4 ==  150      			; Rounding  (=degrees *10)	
	scaleR(8)				; Round up before division
	copy(Acc,T180)					; Store Temperature
ShowDeb(150)
	AH = AL // 10 : AL = AL / 10
	sertxd(" T= ",#AL,".",#AH," C ")

; BMP180 PRESSURE CALCULATION :

;* B6 = B5 - 4000 == -1601 (Data sheet example values)
	copy(B5s,Acc)
	BH = 65535 : BL = 61536 : call addAB	; Add -4000
	copy(Acc,B6s)					; Store B6
ShowDeb(-1601)
;* X1 = (B2 * (B6 * B6 / 2^12)) / 2^11	== 1
	call squareA					; Squared must be poitive
	scale(4096)
	get(B2c,Bcc)
	call smult
	scales(2048)
	copy(Acc,X1)					; Store X1
ShowDeb(1)
;* X2 AC2 * B6 / 2^11 == 56
	get(AC2,Acc)
	copy(B6s,Bcc)
	call smult 
	scaleS(2048)				; X2 might be negative,not saved
ShowDeb(56)
;* X3 = X1 + X2 == 57
	copy(X1,Bcc)
	call addAB
	copy(Acc,X3)				; Store X3
ShowDeb(57)
;* B3 = (((AC1 * 4 + X3) << oss) + 2 / 4 == 422
	get(AC1,Acc)
	BH = 0 : BL = 4 : call smult			; Product in A
   copy(X3,Bcc)
   call addAB
	BH = 0 : BL = OSS : call smult
	scaleR(2)						; Add rounding
	copy(Acc,B3s)					; Store B3
ShowDeb(422)
;* X1 = AC3 * B6 / 2^13  == 2810
	get(AC3,Acc)
	copy(B6s,Bcc)
	call smult
	scales(8192)
	copy(Acc,X1)					; Store X1
ShowDeb(2810)
;* X2 = (B1 * (B6 * B6 / 2*12)) / 2^16	== 59
	copy(B6s,Acc)
	call squareA
	scale(4096)
	get(B1c,Bcc)
	call smult : AL = AH : AH = 0				; shift 16 bits right, X2 not stored
ShowDeb(59)
;* X3 = ((X1 + X2) + 2) / 2^2 == 717
	copy(X1,Bcc)
	call addAB 
	scaleR(2)				 ; Round but don't need to store X3
ShowDeb(717)
;* B4 = AC4 * ((unsigned long)(X3 + 32768) / 2^15 == 33457
	BH = 0 : BL = 32768 : call addAB
	getu(AC4p,Bcc)
	call umult	
	scale(32768)					; Positive so use scale divide
	copy(Acc,B4u)					; Store B4
ShowDeb(33457) 
update:
;* B7 = ((unsigned long)UP - B3) * (50000 >> oss) == 1,171,050,000  (~2^30)
	copy(UP,Acc)
	copy(B3s,Bcc)
	call negateB : call addAB
	BH = 0 : BL = 50000 / oss : call umult		   ; Don't need to store B7
ShowDeb(32768)     					; WA is LS Word only (~52572)
;*  if (B7 < $80000000) then P = (B7 * 2) / B4 == 70003
	if AH < $8000 then 			 ; Optimise division accuracy
#ifdef showDIAGS
	sertxd(" <2^15 ")				; Mark IF path is used
#endif
	call doubleA 				; Double before division
	copy(B4u,Bcc)
	call udiv				; Unsigned ?   P stored in A
;* else P = (B7 / B4) * 2 ==		; Double after division
	else
#ifdef showDIAGS
      sertxd(" >2^15 ")			; Mark ELSE path
#endif
      copy(B4u,Bcc)   
		call udiv
      call doubleA
	endif
   copy(Acc,P180)				; Store P
ShowDeb(4467)
;* X1 = (P / 2^8) squared ==	 74529
	scale(256) 
	call squareA
ShowDeb(8993)
;* X1 = X1 * 3038) / 2^16 == 3454
	BH = 0 : BL = 3038 : call smult
	AL = AH : AH = 0					; shift right 16 bits
   copy(Acc,X1)
ShowDeb(3454)
;* X2 = (-7357 * P) / 2^16	== -7859
	copy(P180,Acc)
	BH = 0 : BL = 7357 : call negateB
	call smult : AL = AH : AH = 0 					
if AL > 32767 then : dec AH : endif				; Sign extend X2
ShowDeb(-7859)
;* P = P + (X1 + X2 + 3791) / 2^4 == 69964 (Pascals)
	copy(X1,Bcc)
	call addAB 
	BH = 0 : BL = 3791 : call addAB
	scales(16)
	copy(P180,Bcc)
	call addAB 
	copy(Acc,P180)
ShowDeb(4429)   
	BH = 0 : BL = 100 : call udiv			; Reduce to <=16 bit value
	sertxd(" P= ",#AL,".")
	if BH < 10 then
		sertxd("0")
	endif
	sertxd(#BH," mb")			; BH is remainder (BL is divisor)
	pause 5000
loop
; * Paste the Subroutines here *
View attachment BMP180compiled for08M2.bas

Next, a corresponding program for the BMP280 sensor.
 
Last edited:

AllyCat

Senior Member
#14
BMP280 Temperature Measurement:

The BMP280 and BME280 have some architectural differences compared with the BMP180 and earlier sensors. They offer more advanced features, but initially I will show primarily the necessary changes to the BMP180 program. The program still makes "one shot" (or "on demand") measurements, now called the "Forced" mode, but two features have been added.

Firstly, the normal Temperature resolution is now 0.01 degree C, which seems to be easily achieved in practice (the absolute accuracy is around +/- 0.5 to 1.0 degree C). This resolution is about 6 times better than the DS18B20, whilst the BMP modules have a similar price. Also, in its tiny metal package, the BMP280 responds more quickly to temperature variations, so it may be useful purely as a temperature sensor. Therefore the first listing below includes only the Temperature calculation, which is needed anyway in the calibration process for the Pressure (and Humidity), and keeps the program to a more manageable size.

Secondly, the majority of BMP280 "breakout" modules appear NOT to include a voltage regulator, so they are rated at only 3.3 volts. Therefore, the program adds some support for this, with one PICaxe pin allocated (optionally) to connect to the BMP280 supply rail. At power-up, the PICaxe measures this pin voltage and also its own supply rail and reports both values to the serial terminal. Then, IF the pin voltage is "Low" AND the PICaxe supply is less than 4 volts, the program assumes that the pin is required to supply current to the BMP280 and pulls it high. The PICaxe itself can work well over a supply range of at least 2.2 - 5.5 volts, so this is a useful safety precaution, but beware that the current drain of the BMP280 can be very low. Therefore, in the case of a "Warm Reset" the BMP may appear to be externally powered (by the charge on its decoupling capacitors) so the program might not activate the supply in this situation.

Below, is as much of the complete program as will fit into the forum's 10,000 character posting limit. But it should be necessary only to add at its end, the missing subroutines (from posts #10 and #11 above) and remove any duplicated symbol definitions (indicated by the Syntax check). The "show" (DEBug) subroutine has been revised to give a better indication where there may be a significant discrepancy between the "actual" and "expected" values (which show only the Low Word).

The Conditional Compilation (#) definitions can be selected as required, but are configured to Simulate an 08M2, with all the variables displayed in the RAM window. The code size should be just under 900 bytes, depending on the options selected, with the test data giving a Temperature result of 25.08 degrees C.

Code:
; BMP280 Atmospheric sensor calibration;  AllyCat ,  December 2017
#picaxe 08m2		  		  ; And most others
#no_data
#terminal 4800
;  #define useBPTR		  ; Pointer Variable may be better when debugging
#define useTESTDATA
;  #define useEECAL		  ; RAM maybe more useful when debugging or with 08M2
#define showDIAGS		  ; Report debugging data to the Terminal
#define scaleDIV 	      ; Auto-Scale Divisor down to 16 bits
#define showRAW		  ; Report Raw data

; MACRO Definitions:

; Show sample test values for debugging (Unsigned)
; Comment out the command line below for final version
#macro showDEB(expected)
	WA = expected : call show
#endmacro

; Copy Long Words between RAM locations
#macro copy(source,destination)
	from = source : dest = destination : call copyram
#endmacro

; Fetch Calibration Constant (Signed)
#macro get(source,destination)
	from = source : dest = destination : call getcal
#endmacro

; Fetch Calibration Constant (Unsigned)
#macro getU(source,destination)
	from = source : dest = destination : call getcalu
#endmacro

; Right-Shift (Unsigned)
#macro scaleU(divis)
   WA = divis : call scaleA
#endmacro

; Right-Shift (With Sign Extension)
#macro scaleS(divis)
   WA = divis : call scaleAS
#endmacro

; Right-Shift (With Rounding)
#macro scaleR(divis)
   WA = divis : call scaleAR
#endmacro

; SYMBOL DEFINITIONS
symbol AL = w8		  ; Low word of Accumulator
symbol AH = w9		; High word of Acc
symbol BL = w12		; Low word of Bcc register
symbol BH = w13		  ; High word of Bcc register
symbol WA = w4		  ; Auxiliary Word register
symbol WAL = b8		; Low byte of WA, etc. 
symbol WAH = b9
symbol ALO = b16	 	; Lowest byte of Acc,(ALL is a reserved word)
symbol ALH = b17		   ; High byte of Low word of Acc
symbol BLL = b24			  ; Lowest byte of Bcc
symbol BLH = b25
symbol sign = bit7 		  ; Flags within b0
symbol carry = bit6 
symbol dloop = bit4		  ; For 16 passes
symbol from = b0		  ; Source pointer for data transfers

symbol vcc280 = b.4

#ifdef useBPTR
symbol dest = bptr
#else
symbol tempb = b8  ; Temporary data byte    ; Overlays WA
symbol dest = b9    ; Destination pointer (when BPTR not used)
#endif

; RAM Variables pointers (to LSB of 32 bits)	; u = Unsigned, s or v = Signed variable
symbol Acc = 16			   ; Pointer to register A
symbol Bcc = 24			   ; Pointer to register B
symbol adcT = 32		; BMP280 Variables from Data sheet
symbol adcP = 36
symbol var1 = 40
symbol var2 = 44
symbol tfine = 48
symbol T280 = 52
symbol P280 = 56

symbol OFFSET = $88 - 60      ; BMP280 Calibration Constant Words
symbol dT1u = $88 - OFFSET	; 27504     ; Unsigned
symbol dT2 = $8A - OFFSET	; 26435
symbol dT3 = $8C - OFFSET     ; 64536
symbol dP1u = $8E - OFFSET	; 36477     ; Unsigned
symbol dP2 = $90 - OFFSET	; 54851
symbol dP3 = $92 - OFFSET	; 3024
symbol dP4 = $94 - OFFSET	; 2855
symbol dP5 = $96 - OFFSET	; 140
symbol dP6 = $98 - OFFSET	; 65529
symbol dP7 = $9A - OFFSET	; 15500
symbol dP8 = $9C - OFFSET	; 50936
symbol dP9 = $9E - OFFSET	; 6000
symbol lastEE = $9F - OFFSET	; Increase to $A1 for BME280

#IFDEF useTESTDATA
; BMP280 Data sheet test values:
data dT1u,(27504,107,26435,103,64536,252,36477,142,54851,214,3024,11)
data dP4,(2855,11,140,0,65529,255,15500,60,50936,198,6000,23) 

#ifNdef useEECAL			; NOT EECAL
	for bptr = dT1u to lastEE			; Copy EE data into RAM
		read bptr,@bptr
	next
#endif 
DO       ; Main Loop to measure Temp and Pressure (Testdata)  
	poke bcc,0,237,126,0 ;     208,238,7,0		; 519888 x 16
	poke acc,192,90,101,0 ;    172,85,6,0 		; 415148 x 16

#ELSE		; not testdata
#no_data
   pause 2000
	calibadc10 AL
	AL = AL / 2 + 52430 / AL      ; Round up and calc Vdd/20 mV
	calibadc10 AH
	AH = 52430 / AH + AL * 10  ; Measure Vdd in tens of mV
	readadc10 vcc280,WA				; BMP280 supply rail voltage
   WA = WA * 64 ** AH            ; ADC converted to fractional binary
  	sertxd(cr,lf,"BMx/axe Vcc= ",#WA," / ",#AH," mV") 
   if AH < 4000 and WA < 1000 then		; Apply power if Vcc < 4v
	high vcc280				; Power BMP280 directly
   else
      sertxd(cr,lf,"Vdd too High ")
	endif

	hi2csetup i2cmaster,$EC,i2cslow,i2cbyte 	; BMP280 address
	hi2cin $D0,(b0)					; read sensor id
	sertxd(cr,lf,"BMx280 ID= ",#b0,cr,lf)     ; BMP = 88, BME = 96
	
	 for b0 = $88 to $9F step 2      ; Use A1 for BME humidity
		hi2cin b0,(WAL,WAH)				; LSB first
      b0 = b0 - OFFSET      
#ifdef useEECAL
		write b0,WAL,WAH
#else
		poke b0,WAL,WAH
#endif
		sertxd(" ",#WA)
      b0 = b0 + OFFSET
	next b0

DO  ; Main Loop to measure Temperature and Pressure (from I2C)
	hi2cout $F4,(%00100101) 	; Initiate standard conversion
	pause 10				; Allow time for conversion
	hi2cin $F7,(AH,ALH,ALO,BH,BLH,BLL,WAH,WAL) ; Block read of raw data
#ENDIF
#ifdef showRAW    ; Report the received data
   sertxd(cr,lf,"BMx280 UT= ",#BH,":",#BLH,":",#BLL)  ; Pressure
   sertxd(" UP= ",#AH,":",#ALH,":",#ALO," UH= ",#WA)  ; Temp:Humid
#endif
	scaleU(16)              ; Acc = Pressure x 16
	copy(Acc,adcP)
	AH = BH : AL = BL       ; Copy Bcc to Acc
	scaleU(16)              ; Acc = Temperature x 16
   copy(Acc,adcT)
	call T_280
;	call P_280        ; Un-comment this line when subroutine is available **
	pause 10000
LOOP

T_280:  ; BMP280 TEMPERATURE
;// Returns temperature in DegC, resolution is 0.01 DegC.
;//  Output value of 5123 represents 51.23 DegC.
;//  t_fine carries fine temperature as global value   

;== var1  = ( ( adc_T>>3 - dig_T1<<1 ) * dig_T2 ) >> 11 ==
   getu(dT1u,Acc)	
	BH = -1 : BL = -16	: call smult            ; -16 * dT1u
   copy(adcT,Bcc)                   ; Subtract before scaling adcT
	call addAB
   scaleS(8)
   get(dT2,Bcc)
   call smult
   scaleS(2048)	
   copy(Acc,var1) 
showDEB(63257)

;== var2 = ( ( (adc_T>>4  - dig_T1) squared ) >> 12) * dig_T3) >> 14
	copy(adcT,Acc) 
	scaleS(16) 
   getu(dT1u,Bcc)
   call negateB : call addAB
   call squareA
	scaleS(4096)
	get(dT3,Bcc)
	call smult 
   scaleS(16384)
;	copy(Acc,var2)             ; Not required
showDEB(65165)	

;== t_fine = var1 + var2	==
	copy(var1,Bcc) 
	call addAB
	copy(Acc,tfine)
showDEB(62886)	
;== T = (t_fine * 5 + 128) >> 8 ==
   BH = 0 : BL = 5 : call smult
	scaleR(128)
;	copy(Acc,T280)		; Not required
	sertxd(cr,lf,"BMP280 Temp= ")
	call showA2dp	; Report temperature to 2 decimal places
	sertxd(" C ")
return

; SUBROUTINES (Paste all others needed from posts #10 and #11  OR  #15)

showA2dp:		; Show Acc with two decimal places
	BL = 100 : BH = 0 : call udiv			; Remainder returned in BH	
	sertxd(#AL,".")
	if BH < 10 then
		sertxd("0")		; Restore leading zero
	endif
	sertxd(#BH)
return

show: ; Report expected and actual Low Word values for testing
#ifdef showDIAGS
   sertxd(" ",#WA)
   WA = AH + 2		; Make smaller negative numbers positive
   if WA < 4 then	; |Acc| is not very large (<121k)
   	sertxd("=",#AL)
   else
   	sertxd("#",#AL)
   endif 		; Mark that the equality might not be exact (Data also in High Word) 
#endif
return

The next post will add a routine to measure the Atmospheric Pressure.
 
Last edited:

AllyCat

Senior Member
#15
BMP 280 Pressure Measurement:

Here is the complete calibration routine for Atmospheric Pressure, followed by all the subroutines omitted in the previous post. So it should be possible to create a complete program to measure Temperature and Pressure by merging the code in just this and the previous post (#14).

The equations commented at the head of each section are similar to the C code in the BMP280 Data Sheet. A few brackets are retained (some changed from curved to square to improve clarity) to indicate the calculation sequence, which may employ neither the normal maths priority nor PICaxe's left-to-right rule. The data sheet example values should give a final Pressure result of 1006.54 mb (aka hPa)

The Pressure calculation almost doubles the program size to around 1700 bytes (or 1500 bytes in the larger M2s with PE6) but it's not yet clear whether this could be reduced. Some of the Calibration Constants and intermediate values hardly seem to need full 32-bit maths.

Code:
;  *** UN-COMMENT THE CALL TO HERE (near the end of the main program loop) ***

P_280: ; BMP280 Pressure: 
;// Returns pressure in Pa as unsigned 32 bit integer  ==
;// e.g. Output value of 96386 represents 96386 Pa = 963.86 hPa  ==

;== var1 = t_fine >> 1 -  64000 ==
	copy(tfine,Acc)
	scaleS(2)
   BH = -1 : BL = -64000 		; Bcc = -64000 
   call addAB
	copy(Acc,var1)			; Store var1, remains in Acc
showDEB(211)

;== var2 = ( [ (var1>>2) squared ] >> 11 ) * dig_P6 ==
;   scaleS(4)
	call squareA			; Divisor = 32768 in Floating Point version
;   scaleS(2048)
   scaleU(32768)
   get(dP6,Bcc)
	call smult
	copy(Acc,var2)			; Store var2
showDEB(-9)

;== var2 = var2 + ( (var1*dig_P5) << 1) ==
	copy(var1,Acc)
	get(dP5,Bcc)
	call smult : call doubleA
	copy(var2,Bcc)
	call addAB
;	copy(Acc,var2)			; Store var2 (not needed)
showDEB(59110)

;== var2 = (var2>>2) + ( dig_P4<<16) == 187120058 = 2855:14778
	scaleS(4)
	get(dP4,Bcc)
	BH = BL : BL = 0 : call addAB			; shift 16 bits and add
	copy(Acc,var2)				; Store var2
showDEB(14778)

;== VAR1A = [ dig_P3 * ( [ (var1>>2) squared ] >> 13 ) ] >> 3 
	copy(var1,Acc)
	scaleS(4) 
   call squareA
	scaleS(8192)
	get(dP3,Bcc)
	call smult
   scaleS(8)
	copy(var1,Bcc)				; Store var1
	copy(Acc,var1)				; Store partial value in var1

;== var1 = VAR1A  + ( dig_P2 * var1 >> 1 ) >> 18 ==  -4.3026
	get(dP2,Acc)
	call smult
   scaleS(2)
	copy(var1,Bcc)
	call addAB  
	scaleS(512)
   call scaleAS						; Two shifts of 9 bits
;	copy(Acc,var1)					 ; Store var1  (not required)
showDEB(-4)

;== var1 = ( (32768 + var1)*dig_P1) >> 15 == 
	BH = 0 : BL = 32768 : call addAB			; var1 in Acc
	getu(dP1u,Bcc)
	call umult
   scaleU(32768) 
	copy(Acc,var1)
showDEB(36472)

;== if var1 == 0
;== return 0; // avoid exception caused by division by zero ==
#ifdef showDIAGS
   if AL = 0 then
      sertxd("#DIV by 0#")
   endif
#endif

;== p = ( [ ( 1048576 - adc_P ) - (var2>>12) ] ) * 3125  [Floating Point uses 6250] ==
	copy(adcP,Acc)
	call negateA : BH = 16 : BL = 0
	call addAB 
	BH = AH : BL = AL
	copy(var2,Acc)
	scaleS(4096)         ; wasn't signed  
	call negateA : call addAB   
	BH = 0 : BL = 3125 : call umult			; Must be positive from now on
;	copy(Acc,P280)    					; Store P (not required)

;== if p < 0x80000000 : p = p<<1 / var1 ==
   if AH < 32768 then
      call doubleA
      copy(var1,Bcc)
      call udiv
   else   
;== else : p = (p / var1) * 2 ==	; [not used with FP]
	   copy(var1,Bcc)
	   call udiv
      call doubleA
   endif   
	copy(Acc,P280)		; Store P
showDEB(35181)

;== var1 = (dig_P9 * [ ( [ (p>>3) squared ] >>13) ] ) >>12 ==	; [FP uses >>31]
	scaleS(8)
   call squareA
	scaleS(8192)
	get(dP9,Bcc)
	call umult
   scaleS(4096)
	copy(Acc,var1)
showDEB(28342)

;== var2 = ( (p>>2) * dig_P8) >>13 ==  -44875
	copy(p280,Acc)
	scaleS(4) 
	get(dP8,Bcc)
	call umult
   scaleS(8192)
showDEB(20661) 		; negative value  

;== p = p + ( (var1 + var2 + dig_P7) >>4 ) ==  100653
	copy(var1,Bcc) 
   call addAB
	get(dP7,Bcc)
	call addAB
   scaleS(16)
	copy(P280,Bcc)  
   call addAB
	copy(Acc,P280)    ; Store final P in Pa
showDEB(35117)	
	sertxd(" Pressure= ")
	call showA2dp
	sertxd(" mb")
return 

; 32-BIT MATHS SUBROUTINES:
;======================

doubleA:		 ; Double the value in Acc (uses Bcc register)
   BH = AH : BL = AL		 	; Copy A into B and fall into the Addition routine
addAB:					; LWA = LWA + LWB , Bcc unchanged
	AL = AL + BL			; Add low words
	AH = AH + BH			; Add High words
	if AL < BL then			; Add Carry
		inc AH
	endif
return

negateA:				; Twos complement of AH:AL
	AL = not AL + 1
	AH = not AH
	if AL = 0 then
		inc AH
	endif
return

negateB:				; Twos complement
	BL = not BL + 1
	BH = not BH
	if BL = 0 then
		inc BH
	endif
return	

makepos:		; Convert Acc and Bcc to positive and set result sign flag
	b0 = 0				; Clear flags and loop counter
	if AH > 32767 then
		sign = 1
		call negateA
	endif
	if BH > 32767 then
		sign = sign xor 1	; Toggle the sign flag 
		call negateB
	endif
return

squareA:	; LWA = LWA * LWA
	BH = AH : BL = AL		; Copy Acc then fall into multiply routine
                        ; (result always positive)
umult: ; Unsigned multiplication
	WA = AL * BL			; Temp store Low word
	AH = AH * BL			; Partial high word
	AH = AL ** BL + AH
	AH = AL * BH + AH		; Complete the High word
	AL = WA
return

smult: ; Signed Multiplication  LWA = LWA * LWB	
	call makepos
	call umult
resign:					; Correct the sign if required
	if sign = 1 then goto negateA	; Then use its Return
	return

sdiv:	; Signed Division	; LWA = LWA / LWB
	call makepos
	call udiv
	goto resign
udiv:	; Unsigned Division
#ifdef scaleDIV 					; If BH > 0 then scale down both
	WA = 1					; Required scaling binary divisor
	do while BH > 0				; Scale down until BH = 0
		WA = WA + WA			; Double the value
		BL = BL / 2				; Shift right
		BL = BH and 1 * 32768 + BL	; Add carry
		BH = BH / 2		  		; Shift right completed
	loop	 
#IFDEF showDIAGS
	if WA = 1 then goto div3216H		; Faster but adds ~9 bytes to code
	sertxd(" Divisor Reduced ")   		; Scaled down to a single word
#ENDIF
	call ScaleA					; Scale the numerator correspondingly
#endif

div3216H: ; Calculate High Result word for 32 by 16 bits division
	BH = AH / BL * BL				; Divisible part of High byte 
	AH = AH - BH				; Subtract to give numerator for Low calc
	BH = BH / BL				; High Result
div3216L: ; Calculate Low Result & Remainder of 32 by 16 bits division
	b0 = b0 andnot 31			; Clear counter flags
	do while dloop = 0			; Flag in b0 is set after 16 iterations
		inc b0 
		carry = AH ** 2 			; Set or clear carry flag
		AH = AL ** 2 + AH + AH	; Carry from low word and shift left
		AL = AL + AL
		if AH >= BL OR carry = 1 then	; Can subtract
			AH = AH - BL		; Subtract divisor from the numerator
			inc AL			; Set LS bit in result
		endif
	loop
	AH = AH xor BH 	   		;)
	BH = BH xor AH 	   		;} swap AH , BH  (AH was remainder)
	AH = AH xor BH       		;)	
return

scaleAR: ; Shift Right, rounding the LS bit of the Result
	AL = AL + WA
	if AL < WA then				; Carry bit from Low to High Word
		inc AH
	endif
	WA = WA + WA				; Double the (halved) value in WA 
;  goto scaleAS      ; Fall through

ScaleAS:	; Rotate Right with sign extension
	if AH < 32768 then ScaleA		; Positive so no sign extension
	call negateA				; Make the value positive
	call ScaleA
	goto negateA				; Restore the negative sign and Return

ScaleA:  ; Rotate Right by dividing by WA (1 - 32768)
	AL = AL / WA
	AL = 32768 / WA * 2 * AH + AL
	AH = AH / WA
return		; With WA unchanged

copyRAM: ; Copy Long-Words between RAM pointers
#ifdef useBPTR  		
	peek from,@bptrinc : inc from
	peek from,@bptrinc : inc from
	peek from,@bptrinc : inc from
	peek from,@bptrinc			
#else
	peek from,tempb : inc from    ; Could use a loop if 
	poke dest,tempb : inc dest    ; program space is more
	peek from,tempb : inc from    ; precious than variables 
	poke dest,tempb : inc dest	
	peek from,tempb : inc from
	poke dest,tempb : inc dest
	peek from,tempb
	poke dest,tempb
#endif	
return

getCAL:	; Copy Calibration word to Long-word
#IFDEF useBPTR
#ifdef useEECAL	; Calibration constants in EEPROM
	read from,@bptrinc : inc from
	read from,@bptr
#else		; Calibration Constants in RAM
	peek from,@bptrinc : inc from
	peek from,@bptr	
#endif
	WA = 0					; Clear temp store for sign extension
	if @bptrinc > 127 then	  		; Test sign and advance pointer 
		WA = 255		  	  	; Negative sign extension
	endif
	@bptrinc = WA				; Sign extend from 16 to 32 bits
	@bptr = WA
return

getcalU:	; Unsigned, so clear the sign extension
	call getcal
	@bptrdec = 0
	@bptr = 0
return

#ELSE	; Don't use bptr
#ifdef useEECAL	; Calibration Constants in EEPROM
	read from,tempb : inc from
	poke dest,tempb : inc dest
	read from,tempb : poke dest,tempb
#else	; Calibration Constants in RAM
	peek from,tempb : inc from
	poke dest,tempb : inc dest
	peek from,tempb : poke dest,tempb	
#endif
	if tempb > 127 then
		tempb = 255			; Negative sign extension
	else
		tempb = 0				; Clear temp store for sign extension		
	endif
	inc dest : poke dest,tempb
	inc dest : poke dest,tempb		; Sign extend from 16 to 32 bits
return

getcalU:	; Unsigned, so overwrite the sign extension
	call getcal
	poke dest,0 : dec dest
	poke dest,0	
return
#ENDIF

The next post may discuss the Humidity calculation for the BME280 and/or the more advanced Oversampling and Low-Pass Filtering features of the BMP/E 280 family.
 

techElder

Well-known member
#16
AllyCat, although I haven't developed a need for the routines themselves, your programming is an educational mix of macros and subroutines that everyone here should be studying. Good show!
 
#18
Just as a word of caution, DO NOT buy the 6 pin packages either from a UK or overseas seller. As mentioned above, these are fakes, beyond any doubt. I bought some modules advertised as being BME280 breakout boards from a UK seller, that specified that they measured temperature, pressure and humidity. They are faked copies of the BMP280, return a non-valid product ID, and when looked at under a microscope they have markings that don't correspond with any Bosch coding. They don't return valid calibration data, and so are effectively junk. For info, this is what the junk boards look like (photo from UK seller):

Fake BME280.jpg

I've wasted a few hours trying to get to the bottom of why these don't behave as expected, before realising they were fakes........................
 

AllyCat

Senior Member
#19
Hi,

I don't believe I've received any "fakes", but I have received several (like the one pictured) that I would call "mis-described". The BME has a square tin/package whilst the BMP280 is rectangular (2 x 2.5mm), so the photo is of a BMP regardless of the description. Therefore, I've received several "free" (refunded) BMP280s from China, one seller eventually accepted that it wasn't as described and the other's took so long to arrive that they refunded for that reason. By the time they did arrive, the seller had edited his listing to BMP, but not removed the reference to humidity, so I didn't bother to update the reason for the refund. ;)

The Bosch data sheet itself seems rather misleading because the picture on the front page shows a neat "Bosch" logo, but it appears that even the "real" sensors have only a few digits embossed, but I've never bothered to decypher them (or perhaps all my devices have been fakes).

Certainly all the BMx... modules I've received have delivered "correct" (or at least plausible) calibration data, but there are several "gotchas". Sometimes the data is low-byte first and sometimes high byte first (or a mixture of both) and the BME humidity data is scattered and merged in assorted nibbles. I believe I've got the Humidity calculation working correctly now, but not fully validated yet.

As I mentioned above (unless you want some "free" BMPs ;) ), I recommend the "4-pin" modules which have a regulator and 5-volt tolerant bus interface, about £2+ from China or £6+ locally in UK (or the "10-pin" module if you want to use SPI).

Cheers, Alan.
 

stan74

Senior Member
#20
Seems I got the bmp280 that does temp and pressure, not humidity. Someone posted a basic include that uses long words instead of floats on another basic forum eventually..went on like this. The picaxe alu is brill but not to be converted being picaxe and peek poke memory...well, could but hard.
Nice devices but no 16bit math support.
ps sorting <0C temp was a challenge for the guy.
 
#21
The characters laser etched on the cases of the fakes doesn't tally with either the BMP280 codes or the BME280 codes, plus the device ID returned doesn't match any ID code from Bosch, not even the engineering sample codes, plus the calibration data stored in the devices are just the defaults from the Bosch data. The net result is that these are pretty useless, as although you can get temperature and pressure data out it doesn't bear any relationship to reality.

I can't be bothered to chase the seller for the sake of a few pounds, but I'm in no doubt whatsoever that these are fake junk.
 

cpedw

Senior Member
#22
I have found that analogue pressure sensors (MPX6115) are good enough for 1hPa resolution, at least comparing with an Oregon weather station (which is probably using the same sensor!). Admittedly, temperature and humidity would need to be acquired elsewhere and they are a bit pricey, about £15.

But the output is linear with pressure so the processing is very simple using READADC10.

Derek
 
Top