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

AllyCat

Senior Member
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.
 

neiltechspec

Senior Member
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
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
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.
 

neiltechspec

Senior Member
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
...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
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
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
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
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
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
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
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
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
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!
 

stan74

Senior Member
Brill work with the math Al. Following with interest as I have a bmp280 and have longs but not floats.
 

Jeremy Harris

Senior Member
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
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
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.
 

Jeremy Harris

Senior Member
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
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
 

crowland

Member
I'm wondering if anyone knows of any example/test data for the humidity part of the BME280.

I've used the temperature calibration and test data from the BMP280 to implement the temperature and pressure and want to add humidity. I've managed to cram all the code into a 14M2 but it WBN to test in the simulator.
Thanks to Alan for the 32 bit math functions, they are a great help.

Chris
 

Skiwi

New Member
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 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.
Hi Alan
Did you manage to do the code for the Humidity section of the BME280?
I am thinking of using it as a one-chip solution, rather than the BMP208 and a DHT22. Not being able to find any subsequent posts on the subject, I am wondering if using the BME280 is not a good idea.
Skiwi
 

AllyCat

Senior Member
Hi,

Looking back into my files, it appears that I might have "finished" the Humidity code, but the problem was that I couldn't test it (for accuracy). My plan was to run it in parallel with the equivalent Arduino sketch to see if they produce the same results, ... but I hate Arduino (or to be more precise the C++ Language). :( Testing is rather important because of the two "horrible" features of the Bosch hardware/software offering: The "tangle" of nibbles, bytes and words which make up the Humidity data / constants, and that equation with 33 pairs of braces (when PICaxe Basic doesn't even support one level).

Another issue is that my complete program is 810 lines long (more than 24,000 characters), which produces 1900 bytes of code in a 20M2 (and exceeds the capacity of an 08M2), although that does include some SERTXD strings for testing. If you have the capability to accurately test the full code, I can look to posting it in this thread (as an "as is" file, not an in-line listing), but there's not much point unless its accuracy can be validated.

With the above issues and the added complications of potential "fakes" (or the higher cost of known-genuine breakout boards), I'm inclined to now use one of the Sensirion family of Humidity sensors. They appear to be the "market leader", used by manufacturers of semi-professional equipment such as Davis. They use the same I2C bus but give results with much less need for data-processing, and even Goretex-style filter/covers are available as an accessory. I believe the preferred version is now the SHT31, with some 5v breakout boards available from China at reasonable prices. They also provide accurate temperature measurements but not, of course, Atmospheric Presure.

Cheers, Alan.
 

Aries

New Member
This may be of some use to you - also "untested" in the sense that I have not compared it to the same program in another language, but "plausible" in that it gave similar answers to another humidity sensor.

The code is part of a larger 20M2 program, using two slots, which transmits the data on a Nordic NRF24L01. As it is, it won't compile because of the references to other things, but the core code should behave. The 32-bit arithmetic bits are not Alan's, so may differ in usage of registers (but I hope not in result). The code is too big to include inline, so it is attached. Irritatingly, the BME280.basinc file can't be attached, so it is inline ...
Code:
'    BME280 register names:
symbol BME280_DIG_T1_LSB_REG            = $88
symbol BME280_DIG_T1_MSB_REG            = $89
symbol BME280_DIG_T2_LSB_REG            = $8A
symbol BME280_DIG_T2_MSB_REG            = $8B
symbol BME280_DIG_T3_LSB_REG            = $8C
symbol BME280_DIG_T3_MSB_REG            = $8D
symbol BME280_DIG_P1_LSB_REG            = $8E
symbol BME280_DIG_P1_MSB_REG            = $8F
symbol BME280_DIG_P2_LSB_REG            = $90
symbol BME280_DIG_P2_MSB_REG            = $91
symbol BME280_DIG_P3_LSB_REG            = $92
symbol BME280_DIG_P3_MSB_REG            = $93
symbol BME280_DIG_P4_LSB_REG            = $94
symbol BME280_DIG_P4_MSB_REG            = $95
symbol BME280_DIG_P5_LSB_REG            = $96
symbol BME280_DIG_P5_MSB_REG            = $97
symbol BME280_DIG_P6_LSB_REG            = $98
symbol BME280_DIG_P6_MSB_REG            = $99
symbol BME280_DIG_P7_LSB_REG            = $9A
symbol BME280_DIG_P7_MSB_REG            = $9B
symbol BME280_DIG_P8_LSB_REG            = $9C
symbol BME280_DIG_P8_MSB_REG            = $9D
symbol BME280_DIG_P9_LSB_REG            = $9E
symbol BME280_DIG_P9_MSB_REG            = $9F
symbol BME280_DIG_H1_REG                    = $A1

symbol BME280_CHIP_ID_REG                    = $D0 '    Chip ID
symbol BME280_RST_REG                            = $E0 '    Softreset Reg

symbol BME280_DIG_H2_LSB_REG            = $E1
symbol BME280_DIG_H2_MSB_REG            = $E2
symbol BME280_DIG_H3_REG                    = $E3
symbol BME280_DIG_H4_MSB_REG            = $E4
symbol BME280_DIG_H4_LSB_REG            = $E5
symbol BME280_DIG_H5_MSB_REG            = $E6
symbol BME280_DIG_H6_REG                    = $E7

symbol BME280_CTRL_HUMIDITY_REG        = $F2 ' Ctrl Humidity Reg
symbol BME280_STAT_REG                        = $F3 ' Status Reg
symbol BME280_CTRL_MEAS_REG                = $F4 ' Ctrl Measure Reg
symbol BME280_CONFIG_REG                    = $F5 ' Configuration Reg
symbol BME280_PRESSURE_MSB_REG        = $F7 ' Pressure MSB
symbol BME280_PRESSURE_LSB_REG        = $F8 ' Pressure LSB
symbol BME280_PRESSURE_XLSB_REG        = $F9 ' Pressure XLSB
symbol BME280_TEMPERATURE_MSB_REG    = $FA ' Temperature MSB
symbol BME280_TEMPERATURE_LSB_REG    = $FB ' Temperature LSB
symbol BME280_TEMPERATURE_XLSB_REG= $FC ' Temperature XLSB
symbol BME280_HUMIDITY_MSB_REG        = $FD ' Humidity MSB
symbol BME280_HUMIDITY_LSB_REG        = $FE ' Humidity LSB
 

Attachments

Skiwi

New Member
Thanks Alan for replying with excellent advice, and Aries for replying with your code
Keeping up with the constantly changing devices is a challenge and your advice is invaluable.
It will take me a while to digest and experiment but I will get onto it. SHT31 looks like the way to go

regards
Skiwi
 

AllyCat

Senior Member
Hi,

I did try the code from post #26, but it reads data "on the fly" from the I2C bus, so can't be used directly within the Simulator. It was easy to replace the HI2CIN commands with reads from EEPROM (sample DATA) but I still got a "zero" result (from a negative calculated value), so I guess something still wasn't initialised correctly. But the main purpose of this thread has been to show a structured calculation method, rather than just to produce "numbers", so I didn't proceed further. There is a "suspicious" double-assignment of w1 around line 800, but I doubt if that's the problem:
Code:
    hi2cin BME280_DIG_H1_REG,(b4)
    b5 = 0
    w3 = 0
    gosub Multiply32
    gosub ShiftRight4
    w2 = w0
    w1 = w3     ; ???
    w0 = w10
    w1 = w11
    gosub Subtract32
There is now a "new" (2018) version of the Bosch Data Sheet: "Document number: BST-BME280-DS002-15 Revision_1.6_092018" ; I haven't noticed any significant changes, but it might give more confidence that any errors may have been corrected. Also it (now) seems possible to directly cut-and-paste the calculation code (at least from my browser) directly into a Program Editor, to avoid any transcription errors. The "Floating Point" version (in Appendix A .8.1) is not too complicated and gives the opportunity to calculate the results from any test data using a reasonable Pocket Calculator or an "App". I still use the calculator in my ten-year-old Galaxy S, with its multiple-nesting brackets and enormous FP resolution. :) After cutting out most of the "C-code verbosity" the calculation becomes (with test sample values after the == markers) :
Code:
// FLOATING POINT VERSION:  // Returns humidity in %RH, an output value of “46.332” represents 46.332 %
var_H = t_fine – 76800 ;      ==  23526
var_H = (adc_H – (dH4 * 64 + dH5 / 16384 * var_H) ) * (dH2 / 65536 * (1 + dH6 / 67108864 * var_H   _
         * (1 + dH3 / 67108864 * var_H)  )  ) ;        ==  39.8105
var_H = var_H * (1 – ( dH1 * var_H / 524288) ;         ==  39.584
Belatedly, in response to post #23, here is a set of sample data from a (believed) genuine Bosch BME280 which produced the results shown above. Note that "dH3" has a value zero, so the "continuation" line (following the line terminated "_") shown above, resolves to multiplication by unity and can be skipped.
Code:
; adcH   = 33600   ; Unsigned Word
; tfine  = 100326  ; 32 bits
; dH1    = 75      ; Unsigned byte
; dH2    = 331     ; Signed Word
; dH3    = 0       ; Unsigned byte
; dH4    = 402     ; Signed Word (12 bits)
; dH5    = 50      ; Signed Word (12 bits)
; dH6    = 30      ; Signed byte
; H280   = 39.58%  ; Returned in centi%  (14 bits)
; Slave Address  = $EC or $EE  ; Chip ID @ $D0 = $60
Applying a similar editing procedure to the 32-bit integer version of the code gives the following. In this case, the continuation marker divides the formula into the product of two parts which can be calculated separately; initially the second part can be resolved into a temporary variable "var2" to be then included in the first part. The "ugly" C range-testing and scaling has been replaced with something more familiar to PICaxe users (but assuming a signed 2's complement internal value). ;)
Code:
  var1 =  t_fine – 76800 ;
  var1 =  (( (( adc_H << 14 ) – ( dH4 << 20 ) – ( dH5 * var1 )) + 16384 ) >> 15 )  *   _                  ; _ [var2]
   (( (( var1 * dH6 >> 10 * ( var1 * dH3 >> 11 + 32768 ) >> 10 ) + 2097152 ) * dH2 + 8192 ) >> 14 )      ; [=var2]
  var1 =  var1 – (( (( ( var1 >> 15 ) * ( var1 >> 15 )) >> 7 ) * dH1 ) >> 4 ) >> 10 * 100
  H280 =  var1 + 2048 >> 12 min 0 max 10000                          ;   Returned result in hundredths of a Percent RH.
That's probably enough for this post. Next, I'll further rearrange the above formulae to be used more efficiently in a PICaxe, and include program code to organise the Humidity data (bytes) from the I2C bus, and finally to calculate a Humidity value.

Cheers, Alan.
 
Last edited:

Aries

New Member
There is a "suspicious" double-assignment of w1 around line 800, but I doubt if that's the problem:
Code:
    hi2cin BME280_DIG_H1_REG,(b4)
  ...
    w2 = w0
    w1 = w3     ; ???
    w0 = w10
    w1 = w11
    gosub Subtract32
Thanks for spotting and reporting the error. It should, of course, be w3 = w1 (Subtract32 subtracts (w2,w3) from (w0,w1)).

There is now a "new" (2018) version of the Bosch Data Sheet: "Document number: BST-BME280-DS002-15 Revision_1.6_092018" ; I haven't noticed any significant changes, but it might give more confidence that any errors may have been corrected.
I did a comparison between the "old" and "new" versions, and there are no changes in the section 8.2 (32-bit integer) routines between version 1.1 (07/05/2015) and version 1.5 (17/09/2018)
 

AllyCat

Senior Member
Hi,

An update, but with a "spoiler" that this particular post won't include any completed program code (which should follow soon) :

However, I have revised the Humidity code, so that it now gives exactly the same results (within 0.01%) of a "manual" calculation using a Pocket Calculator with the alternative set of (Floating Point) equations. So I'm confident that the code itself is correct, but further tests are planned. The full 32-bit computation for BME280 Temperature, Pressure and Humidity fits within a single M2 PICaxe slot, with sufficient space for additional code to compare two Sensirion SHT30 T/H sensors and an 18B20 temperature sensor. The SHT3x data management is much easier, delivering just two 16-bit calibrated words directly, giving linear ranges of 0 to 99.99 % RH and -45 to +129.99 degrees C (so a "Code Snippet" for the Finished Projects section should also follow soon).

All the temperature values agree within a few tenths of a degree C, the merit of the 18B20 being that it is in a waterproof probe so can be used as a "Wet Bulb" sensor for a calculation of Relative Humidity. The two SHT30 sensors give RH values within 1% of each other and two BME280s (plugged in individually) similarly agree with each other. But the two manufacturers' products differ by around 8% RH, which is greater than the sum of their claimed absolute accuracy (about +/- 3% each). The Wet Bulb method requires a suitable "wick" and is dependent on the speed of air flow across the bulb/wick that creates evaporation and thus the resulting temperature drop. I've used a miniature 12 volt computer-style fan which gives a moderate air flow and the typical temperature drop suggests a RH much closer to the Sensirion than the Bosch values. Here is a typical block of output:
Code:
BMx280 ID= 96
BMx280 UT = 126:55:0 UP = 81:168:0 UH = 36303
BME280= 20.82 C : 50.21 % : Pressure= 999.16 mb
SHT30 = 20.66 C : 58.52 %
SHT31 = 20.68 C : 58.81 %
18B20 = 16.18 C     (Fan-aspirated "Wet-Bulb" temperature)
So there is still some doubt around the BME280 results; one uncertainty being the transfer of data from the I2C bus into the calculation algorithm. Normally, this wouldn't be an issue, with just a minor concern about Murphy's Law (that "Anything that can go wrong will go wrong"). But Bosch seem to have actively invited Murphy to the party : the six Humidity "Calibration Constants" are supplied in 5 different numerical formats (permutations of Unsigned and Signed, 8 , 12 and 16-bit number fields) with 5 having "missing" bits (in the form of nibbles or bytes) from the nominal 16-bit words, which must be supplied by the program-writer. I must admit that in my first version of the coding, I "overlooked" some of the "Sign Extension" bits (which add significantly to the code size) and I am again "suspicious" of a small part of the code attached in post #26 above :
Code:
' H5 MSB is BME280_DIG_H5_MSB_REG    ;  LSB is bits 7-4 of BME280_DIG_H4_LSB_REG
    hi2cin BME280_DIG_H4_LSB_REG,(b4,b5)   
    w2 = w2 / 16                            ' shift out unwanted low bits
    b5 = b5 / 16 * $F0 | b5        ' extend sign bit through w2
The Sign Extension above is determined by dividing by 16, but I believe that should be 8 or 128 depending on whether the lower or higher nibble is to be extended through the byte or word. However, I must emphasise that although most of the constants are defined as "Signed" in the data sheet, I have not (yet) seen any of the Humidity constants actually having a negative sign, so again the final result is unlikely to be affected much, if at all. But it would have been so much easier if Bosch had added just one more byte, and avoided those separated nibbles, particularly as the "extra" bits would not have been additional "data", but just copies of the adjacent sign bits.

The Humidity Calibration Constants : (continuing with the Section headings that I started back in post #1).

My present program has too many Conditional Compilation options (about 10 #IFDEFs) so I propose to remove the option to recover the calibration constants from EEPROM instead of RAM (which of course still can be easily and quickly loaded from either EEPROM or the I2C bus, by a small additional program module). The RAM may seem a strange choice, but the 08M2's EEPROM space is "borrowed" from the program memory, which is probably more critical than RAM space in this complex program. Also, I wish to add support for the use of the two I2C Slave addresses (at the same time), which would be much more difficult to code for EEPROM storage.

Of course, reading the constants directly from the sensor "On The Fly" via the I2C bus is a very efficient method for a "finished" project, but it cannot be used in the Simulator or for test / comparison data. For this project I want the ability to supply "artificial" data, for example to check that negative (signed) values are being handled correctly and perhaps for a "Sensitivity Analysis" (changing individual values by a small amount to see how they affect the final result).

Even more radically, I plan to reserve some of the direct RAM space (w9 - w13) for the Humidity constants, because of their mix of Words, Bytes and Nibbles. These can be handled more efficiently and with more clarity when directly accessed, compared with using PEEK / POKE and @bptr data manipulation in the indirect RAM space. The "ALU" code uses only 10 - 12 bytes of direct RAM (and 50+ indirect), so the register space is generally available. The Humidity calibration bytes are recovered from a separate block (address range) to the other calibration bytes in the Slave, so the former can be loaded into RAM address $12 upwards, followed by the others from $1C up to $35.

Program code is required to "map" the 7 "Humidity" bytes (and the 1 byte included with the "Pressure" set) to create the 6 (x 16-bit) Words of the Calibration Constants. IMHO the table in the data sheet is "confusing", with the bits in each byte ordered (conventionally) in descending sequence (from left to right), but the bytes are ordered (mainly) Low byte first (whilst the "ADC" data bytes are sent High-byte first, with a half-filled byte at the end). Therefore, I have added a column which shows the individual nibbles in a consistent (descending) sequence, with an H suffix indicating the High nibble [bits 7:4] and L the Low nibble {bits 3:0] of the byte. So here is a copy of the table from the data sheet, with columns added for the data nibbles and typical code instructions to convert the "E" bytes (from the I2C bus) into the "H" words to be used by the Humidity calculation algorithm. The first two columns are exactly as in the data sheet but, for clarity, I would have exchanged the first two columns and also the bytes within each of them.
Code:
Register                                                             Typical Program Code assignment
Address      Register content      Data nibbles     Data type            High byte    :  Low byte
A1           dig_H1 [7:0]          0   0 :A1H A1L   Unsigned char          Clear      :     A1
E1/E2        dig_H2 [7:0]/[15:8]  E2H E2L:E1H E1L   Signed short            E2        :     E1
E3           dig_H3 [7:0]          0   0 :E3H E3L   Unsigned char          Clear      :     E3
E4/E5[3:0]   dig_H4 [11:4]/[3:0]  E4x E4H:E4L E5L   Signed short     E4 / 128 * $F + E4 * 16 + (E5 AND 15) **
E5[7:4]/E6   dig_H5 [3:0]/[11:4]  E6x E6H:E6L E5H   Signed short     E6 / 128 * $F00 + E6 * 16 + (E5 / 16)
E7           dig_H6 [7:0]         E7x E7x:E7H E7L   Signed char      E7 / 128 * $FF   :     E7

** Adjusted dig_H4 << 4 :
E4/E5[3:0]  dig_H4x [15:8]/[7:4]  E4H E4L:E5L  0    Signed short * 16       E4        :   E5 * 16
Word dig_H4 is a particular "exception" (of all the 18 calibration words) because it is listed with its High byte first. Also, in the subsequent algorithm it is Left-Shifted 20 times (i.e. multiplied by just over one million) which adds to the complexity of the computation. Therefore, I have pre-multiplied its value by 16 (i.e. 4 Left-Shifts) which removes the need for a Sign-Extension and reduces the assignment to simply reading E4 into the High byte and multiplying E5 by 16 for the Low byte. The remaining 16 shifts (and subsequent addition) can then be implemented by simply adding the "Low" data Word directly into the High data Word of the BCC register, which also removes the need to store and later recover the "running total", were a normal "Sum of Products"(Shift/multiplication and add) being performed.

Now, here are the rearranged Humidity equations derived from post #28, which can be processed from left to right in PICaxe Basic style. A few brackets are retained for clarity and to show the "delayed" subtraction which is implemented by a "negate". Only two "temporary storage" registers are required, "var1" contains the result from the first line (used multiple times) for most of the calculation and the original line 3 is calculated before line 2, with its result stored in "var2" until line 2 has been computed. The "Rounded" Right-Shifts (which were achieved by adding half of the subsequent "divisor" before shifting) are shown as R>> , for which a dedicated routine is available (but probably unnecessary).
Code:
var1 =  t_fine – 76800
var2 = ( ( ( var1 * dH3 >> 11 + 32768 ) * dH6 >> 10 * var1 >> 10 ) + [32 : 0] ) * dH2 R>> 14
var1 = dH5 * var1 + [dH4x : 0]
var1 = (adc_H * 16384) - var1  R>> 15 * var2
var1 =  – ( ( ( var1 >> 15 ) Squared >> 7 ) * dH1 ) + var1  R>> 4
H280 = var1 R>> 11 * 100 R>11 min 0 max 10000    ;   Returned result in hundredths of a Percent RH
I think the next stage will be to edit all my "Humidity-relevant" code into a single file to post here; maybe not fully compatible with the code around post #11 above, due to the memory changes and limitations.

Cheers, Alan.
 

Aries

New Member
The Sign Extension above is determined by dividing by 16, but I believe that should be 8 or 128 depending on whether the lower or higher nibble is to be extended through the byte or word. However, I must emphasise that although most of the constants are defined as "Signed" in the data sheet, I have not (yet) seen any of the Humidity constants actually having a negative sign, so again the final result is unlikely to be affected much, if at all.
You're right - it should be divide-by-8 (to pick up bit 3). However, as long as the 12 bits are positioned in bits 3-0 of the high byte and bits 7-0 of the low byte, it is always divide-by-8.

As you imply, the 12-bit numbers don't appear to be negative.

Edit: did a bit of research. The Bosch API does this to get dig_H5:
Code:
dig_h5_msb = (int16_t)(int8_t)reg_data[5] * 16;
dig_h5_lsb = (int16_t)(reg_data[4] >> 4);
calib_data->dig_h5 = dig_h5_msb | dig_h5_lsb;
which compares to my corrected (thanks again Alan) code:
Code:
' H5 MSB is BME280_DIG_H5_MSB_REG;  LSB is bits 7-4 of BME280_DIG_H4_LSB_REG
    hi2cin BME280_DIG_H4_LSB_REG,(b4,b5)  
    w2 = w2 / 16                            ' shift out unwanted low bits
    b5 = b5 / 8 * $F0 | b5        ' extend sign bit through w2
The right shift in C keeps the sign bit - but it seems to do it with LSB, so the result is that if we start with the 12-bit number 0x12A, we end up with the 16-bit number 0x12FA - or am I misinterpreting something?

Second edit:
The Bosch code does sign-extend as it should - for anyone who wants to know the detail
dig_h5_msb = (int16_t)(int8_t)reg_data[5] * 16;
does a sign-extend, because it interprets the raw data as a signed 8-bit number, so multiplying by 16 (or anything else positive) automatically extends the sign to (in this case) a full 16 bits. The right-shift
dig_h5_lsb = (int16_t)(reg_data[4] >> 4);
does not do a sign-extend, because the raw data (reg_data) is unsigned. Had it been a signed number, the sign extension would have taken place.
With Alan's correction to my code, at least that calculation now agrees.
 
Last edited:

AllyCat

Senior Member
Hi,

Not "finished" yet, but I do now have "fully functioning" code for Temperature, Pressure and Humidity from a BME280 sensor. However, I have not resolved the issue that it reports Relative Humidity around 8% lower than several other "reputable" sources of Humidity data. Maybe it's time for somebody else to look for the "flaw", if there is one. :)

The full program is too large for a single slot in an M2, but it can be "Conditionally Compiled" to suit various applications. In particular, all the T/P/H measurements can be run together in a single slot, either directly from the I2C bus, or from embedded Test Data, even in an 08M2. The 08M2 is a challenge because its code compiles to around 200 bytes greater than for the larger M2s, because the latter contain a separate "invisible" subroutine Return stack (and there are a lot of "Returns"). Also, any EEPROM space used for the Test Data bytes is removed from the 08M2's available Program space (but the EEPROM is still the most efficient place to store this data).

Therefore, it is necessary to select the active #DEFINEs carefully; if all three Environmental parameters are enabled, then most of the "diagnostics" must be omitted, but it is only necessary to enable the HUMIDity and COEFFicients for all of the (31) Calibration Coefficient bytes to be reported in an EEPROM/DATA-compatible output format. For full diagnostics testing, enable either the HUMIDITY or the PRESSURE as appropriate. Temperature is always compiled and computed as it is required for both the Humidity and Pressure calculations (via the tfine parameter).

Below, I am posting the "new" (i.e. previously unlisted) program code in-line, so in principle it can be combined with the code around posts #11 and #15 above. However, there have been a number of other changes, so a complete program file is also attached. For example, the RAM/Registers have been reorganised and Negative Temperatures should now be supported in a new "output" routine. The DIVision routine is required only once (in the Pressure computation) so it is conditionally compiled (with that subroutine). Since it uses more than 100 bytes of code space (at least with the optional Error/Exception checking included) it might be worth looking for an "alternative" method (e.g. a "custom" routine for up to 17 - 20 bits).

Code:
; BME280 Atmospheric sensor,  AllyCat,  November 2020
; Additional code for Humidity
; Requires Symbols, Macros, Subroutines, Temperature code, etc. from forum posts around #11 & #15
symbol OFFSET = $88 - 28         ; BMP280 T+P Calibration Constant Words now start at RAM $1C 
symbol dP9     = $9E - OFFSET     ; Final byte, $A1 now mapped to 28 + 26 = 54
symbol dH1u = $A0 - OFFSET      ; Unsigned word for Humidity
symbol dH1H = $A1 - OFFSET     ; Receives Humidity byte (then convert to word at $A0)
#ifdef HUMID                ; Additional BME280 Calibration Constants    
symbol OSH     = $E1 - 18     ; Load dH2 upwards into variables space from b18/w9 
; 10 Bytes of Humidity constants in direct variables region (to reduce program code size)
symbol dH2     = $E1 - OSH      ; Signed Word (map to dH1 + 2)
symbol dH3u = $E3 - OSH        ; Unsigned byte (clear dH3h)
symbol dH3H = $E4 - OSH       ; High byte = 0
Rymbol dH4     = $E5 - OSH      ; Signed word (dH3h*16 + dH5&15)
symbol dH4x = $E5 - OSH        ; dH4 multiplied by 16 to reduce overall codesize    
symbol dH5     = $E7 - OSH      ; Signed word (dH5/16)
symbol dH6     = $E9 - OSH      ; Signed byte (last rx byte)
symbol dH6h = $EA - OSH        ; High byte = sign extension
symbol E1 = b18                        ; Humidity calibration bytes from I2C
symbol E2 = b19
symbol E3 = b20
symbol E4 = b21                        ; Also d_H3 High byte 
symbol E5 = b22
symbol E6 = b23
symbol E7 = b24
symbol d_H3 = w10               ; Humidity calibration words for formulae
symbol d_H4    = w11            ; Replaced by d_H4x
symbol d_H4x = w11             ; d_H4 multiplied by 16 to reduce overall code space (= E5:E4H)
symbol d_H5 = w12
symbol d_H6 = w13        
#endif 
main:
do    
    call    init280
    call    getcoeff
    call    getADCs    
    call    T_280    ; Temperature calculation
    call    H_280   ; Humidity calculation
    call    P_280    ; Pressure calculation
    reportCC = 1    ; Skip output on subsequent passes
    pause 10000
loop
; SUBROUTINES:
getcoeff:
#IFDEF TESTDATA
data(106,110, 213,104, 50,0, 116,146, 176,214, 208,11)    ; dT1u - dP3
data (97,29, 216,255, 249,255, 12,48, 32,209, 136,19)       ; dP4 - dP9
data (0,75 ,75,1,0,25,34,3,30)         ; dH1 - dH7
; BMx280 UT = 124:154:0 UP = 76:188:0 UH = 35910
   bptr = dT1u
   for from = 0 to 25 step 2       ; Copy EE data into RAM
      read from,@bptrinc,@bptrinc
   next from
#IFDEF HUMID
data (75,1,0,25,34,3,30)         ; E1 - E7    
   bptr = dH2    
   for from = 26 to 32               ; Copy EE data into RAM
      read from,@bptrinc
   next from
#ENDIF    ; HUMID
#ELSE    ; /TESTDATA 
   bptr = dT1u
   for from = $88 to $A1 step 2               ; Copy I2C data into RAM
     hi2cin from,(@bptrinc,@bptrinc)
   next from
#IFDEF HUMID
   bptr = dH2    
   for from = $E1 to $E7           ; Copy I2C data into RAM
      hi2cin from,(@bptrinc)
   next from
#ENDIF    ; HUMID
#ENDIF  ; /TESTDATA

#IFDEF showCOEFF
   if reportCC = 0 then
   sertxd(cr,lf,"CC:88-A1,E1-E7= (")    
   bptr = dT1u      ; RAM28        ; CC data in RAM
   do
      sertxd(#@bptrinc,",",#@bptrinc,", ")
   loop until bptr > dP9
      sertxd(#@bptrinc,",",#@bptrinc," ")        ; Allocated to Humidity
#IFDEF HUMID
   bptr = dH2        ; RAM 18
   do
      sertxd(",",#@bptrinc)
    loop until bptr > dH5
#ENDIF     ; HUMID    
    sertxd(")")                ; End of data list
    endif  ; reportCC
#ENDIF    ; showCOEFF    
#IFDEF HUMID
    peek dH1H,b1                            ; Move from High to low byte (only indirectly addressed)
    poke dH1u,b1,0                         ; Zero High byte for char variable
    d_H6 = E7 AND 128 * $1FE + E7       ; w13    ; Sign extend (written to w13)
    d_H5 = E5 / 16                ; w12    ; High nibble (dH5 is w12,overwrites E7+)
    d_H5 = E6 AND 128 * $1E + E6 * 16 + d_H5     ; w11    ; Sign extend on High byte
    E6 = E4                        ; b23    ; d_H4x High byte (Avoids sign extension)
    E5 = E5 * 16                ; b22    ; d_H4x Low byte (deletes upper nibble)
    E4 = 0                        ; b21    ; d_H3 High byte is zero (E3-E1 ok)    
#IFDEF showCOEFF 
if reportCC = 0 then
    peek dH1u,WAL,WAH
   sertxd(cr,lf,"H1-H6= ",#WA)
   for tempb = dH2 to dH6 step 2
      peek tempb,WAL,WAH
      sertxd(" : ",#WA)
      pause 5
   next tempb
   reportCC = 1            ; Cancel future reports
endif ; reportCC    
#ENDIF    ; /showCOEFF
#ENDIF    ; /HUMID
return    ; getcoeff

H_280: 
#ifdef HUMID
;== H1-6 = 75,331,0,402,50,30  ==
;== UH = 33600, tfine =      ==
;== X = tfine - 76800  ==
   copy(tfine,Acc)
   BH = -2 : BL = 54272    ; -76800
   call addAB
showDEB(18396)
   copy(Acc,var1)
;==  var2 = (( ( var1 * dH3 >> 11 + 32768 ) * dH6 >> 10 * var1 >> 10 ) + 32:0 ) * dH2 R>> 14 ==
   get(dH3u,Bcc)
   call umult
   scaleu(2048)
   BH = 0 : BL = 32768
   call addAB
   get(dH6,Bcc)
   call smult
   scales(1024)
   copy(var1,Bcc)
   call smult
   scales(1024)
   BH = 32 : BL = 0
   call addAB
   get(dH2,Bcc)
   call smult
   scaleR(8192)
   copy(Acc,var2)
showDEB(42716)
;== var1 =  (( ([ adc_H << 14 ] - [( dH4x << 16 ) + ( dH5 * var1 )] + 16384 )) >> 15 ) * [var2]  ==
;  var1 = dH5 * var1  ==
   get(dH5,Acc)
   copy(var1,Bcc)                    ; Last use of var1 
   call smult                        ; Keep result in Acc
showDEB(14)
   get(dH4,Bcc)
   AH = AH + BL                    ; Add Bcc << 16  ; Saves 7 bytes  over shift and addAB **
   copy(Acc,Bcc)                     ; Can avoid copying to var1
;==  var1 = (adc_H * 16384) - var1  R>> 15  ==
   getu(adcH,Acc)                    ; adcH is an Unsigned Word variable (in RAM)
   AH = AL : AL = 0                ; Multiply by 2^16 (Avoids corrupting Bcc)
   scaleU(4)                        ; Scale back to the required 14 left-shifts, MUST be Unsigned    
   call negateB
   call addAB                        ; Subtract Bcc from Acc
   scaleR(16384)
showDEB(5063)
   copy(var2,Bcc)
   call smult
   copy(Acc,var1)
showDEB(3300)
;==  var1 = (var1 - ( ( ( var1 >> 15 ) Squared >> 7 ) * dH1 ) R>> 4)  ==
;==  H280 = var1 R>> 11 * 100 R>11 min 0 max 10000 ;    = 39.58%  ==
   scaleS(32768)
   call squareA
   scaleU(128)
   get(dH1u,Bcc)     
   call smult
   scales(16)
   call negateA
   copy(var1,Bcc)
   call addAB
showDEB(3275) 
;== Humid = X>>12 / 1024   % = 39.24  ==
   scaleR(4096)                                ; / 8192 to fit into a word
   AL = AL ** 12800                        ; = * 100 / 512
   poke h280,ALO,ALH                    ; Store the Word result
   sertxd(": ")
   call show2dp                                ; Show word to 2 decimal places
   sertxd(" %")                                ; 51.18 %
#endif ; HUMID
return   ; H_280

show2dps:                                        ; For Temperature
   if AL > 32767 then
      sertxd("-") : AL = -AL
   endif
show2dp:                                            ; For Humidity
   BH = AL // 100 : AL = AL / 100
show2dpa:    
   sertxd(#AL,".")
   if BH < 10 then
      sertxd("0")
   endif
   sertxd(#BH)
return    ; show2dps
Well, that's probably close to the forum's 10,000 character limit again. ;)

Cheers, Alan.
 

Attachments

AllyCat

Senior Member
Hi,

Time for another "update": I purchased a "new" BME280 from a UK source which gives results close to my previous devices, and also confirmed the results using the Adafruit Library in a "Nano". So these PICaxe calculation routines do seem to be fully validated, but still the BME280 indicates almost 10% lower Relative Humidity, compared with my other "reference" sensors. In particular, the "Wet Bulb" tests cluster much closer to the Sensirions (SHT30), just occasionally "splitting the difference" with the BME280, using my "best" (forced air) cooling / evaporation conditions. Also, the Bosch sensor indicated a value of only 82% under the recent "foggy autumn morning" conditions, which seems rather low.

However, the BME280 does have some useful advantages: It is somewhat cheaper, additionally delivers atmospheric pressure and responds at least twice as quickly as my other sensors. Also, because of its "calibration architecture" it can report values above 100%, which might seem a strange "advantage", but this means that it can always be calibrated to a "correct" value. A "problem" with sensors like the Sensirion SHT31 (which represents 100% as $FFFF) is that IF they report a maximum value when the Humidity is clearly not (quite) that high, then there is no way to "recalibrate" the device, it must be replaced.

The DIVISION Subroutines :

As is common with regular PICaxe Basic, division is a "bottleneck" in the Pressure calculation, so this section may have some relevance to other, less complex PICaxe programs. Some detail about the 32-bit Division subroutine was given in the last two paragraphs of post #11 above, and also in a related code snippet. However, as this is the largest subroutine, but is required only once, it is worthy of further consideration. Actually, it is used more than once because it is "available" and division by 100 gives the Integer and Remainder ("decimal") parts of the result, directly from the 32-bit ALU number format. But this subroutine is slow and the Temperature and Humidity results need less than 16-bit resolution, so a separate "display" calculation directly in PICaxe Basic is more efficient. This can also report a negative value (needed only) for the Temperature value, which was omitted from my original program. The Pressure does need to show a 17-bit (unsigned) value (i.e. ~100,000 Pascals), but the additional bit could be patched as "special case" in the PICaxe code if needed. Therefore, our attention turns to the one "essential" use of the Division subroutine, which converted from the Bosch (32-bit) code to PICaxe Basic is as follows:
Code:
; == if p < 0x80000000 : p = p<<1 / var1 ==
	if AH < 32768 then						; MSb is clear (so can be doubled)
		call doubleA
		copy(var1,Bcc)
 		call udiv                              ; Unsigned DIVision
; == else : p = (p / var1) * 2 ==			; Alternative path (adds ~30 bytes)
	else
		copy(var1,Bcc)
		call udiv
		call doubleA
	endif
    copy(Acc,P280)		; Store the (preliminary) Pressure
That code simply multiplies by 2 either before or after the Division, to increase the effective resolution from 16 bits to 17 bits (the "Copy" routine must be listed twice because the present "doubleA" routine overwrites the Bcc register). That's quite an "overhead" to add just one bit of resolution, so an alternative approach, adopted here, is to increase the resolution of the Division routine itself : Immediately after the normal 16-bit division, the Remainder is still stored in the numerator register, so the Division can be continued. In principle, since the division is bitwise we could continue the division for any number of bits, but this routine uses a "trick" of rotating the Numerator and Result through a single 32-bit register, which can start to overwrite the Result after 16 bits.

The solution here, is to revise the Division subroutine to execute only 8 iterations (loops), with the 16-bit result calculated by calling the routine and then immediately "falling into it" again. Then, the High byte of the Result is stored, before calling the routine once more, to add an 8-bit "fractional binary" Result (i.e. multiplying the normal Integer result by 256). Finally, the "Rounded Scaling" Macro routine can be used to multiply the integer part by 2, also rounding up when appropriate (which was not implemented in the original code). The calculation of another full 8-bits is rather inefficient in this case, but in PICaxe Basic it's convenient to move whole bytes directly within the 32-bit Double Word Result, and it does give a more "generic" solution. Furthermore, the High Word Result probably will be zero (and its calculation optionally skipped), so adding just one byte (at the High end) should be more than sufficient, giving an intermediate Result of 24 integer bits and 8 fractional bits (i.e. Q24.8 format).

So here is the revised section of program code and Division Routine, which employs fewer program bytes and maybe higher accuracy due to the rounding. The #DEFINEs can be unspecified (or commented out) to save a few more program bytes, when the limited "range" of the data values is known to be satisfactory (as appears to be the case here).
Code:
     copy(var1,Bcc)
     call div3216L	
    BHL = ALH			; Store High byte of bitwise result
    call div328L			; Calculate the fractional result 
    AH = BH			; Copy High word back to ACC (Result in ACC is 24 bits + 8 fractional bits)
    scaleR(64)			; Effectively multiplies by 2 with rounding to create 17-bit rounded result in Acc
    copy(Acc,P280)		; Store the (preliminary) Pressure
; -------------
#IFDEF PRESSURE           ; Not required for Humidity or Temperature calculations
sdiv:		                ; Signed Division; LWA = LWA / LWB ; (if BH > 0 then can scale down both)
	call makepos
	call udiv
	goto resign
udiv:				        ; Unsigned Division (No tests for divisor BH > 0 or BL = 0)
#IFDEF FULLDIV                      ; Only required if High Word Result > 0
div3216H: 				; Calculate High Result word for 32 by 16 bits division (Not required for 16-bit result)
	BH = AH / BL * BL		; Divisible part of High byte 
	AH = AH - BH			; Subtract to give numerator for Low Word result
	BHH = BH / BL		; High Result ; ** Store Low 8 bits temporarily in BHH **
	call div3216L
	AH = AH xor BH 	   		;)  Swap AH with BH (AH was remainder,BH was High result)
	BH = BH xor AH 	   		;}  Restore AH:AL format  (~10+ bytes)
	AH = AH xor BH       		;)  Exit with divisor in BL, Remainder in BH, Result in AH:AL
return
#ENDIF	; FULLDIV
div3216M:
	BH = 0						; If result <= 16 bits is expected (saves about 12 program bytes)
div3216L: 						; Calculate Low Result & Remainder of 32 by 16 bits division
	call div328L					; Calculate 8 bits then fall through for another 8
div328L:
	b0 = b0 AND 15				; Clear loop counter (bits7-5) and Carry flag (4),(bits0-3 unchanged)
	do 							; b0 will be < 32 after 8 iterations
		b0 = b0 - 32 				; First decrement wraps back to 224+  (use -16 for 16 bits)
		carry = AH ** 2 			; Set or clear carry flag (more efficient than / 32768)
		AH = AL ** 2 + AH + AH	; Carry from low word and shift left
		AL = AL + AL				; Shift left, clearing LSBit
		if AH >= BL OR carry = 1 then		; Can subtract
			AH = AH - BL			; Subtract divisor from the numerator
			AL = AL OR 1			; Set LS bit in result
		endif
	loop until b0 < 32			; Result in AL, Remainder in AH  (use 16 for 16 loops)
return
#ENDIF 	; PRESSURE
Probably the next tasks will be to code a "Wet Bulb" calibration program and also a compensation for pressure against altitude (or vice versa). However, both of these calculations involve an Exponent or Logarithm, so I plan to first add these conversions to my Trigonometric and extended maths routines thread.

Cheers, Alan.
 
Top