Stamp II math note #4

Calculation and display:

Negatives, Decimals, Display Time and Date, IF-THEN-LET
(c) 1998, 2001 EME Systems, Berkeley CA U.S.A.
Tracy Allen
<stamp index> <home>

Contents

Updated 1/5/2007 and 1/28/2007 in section on double precsion Julian time calculation
Again on 2/16/09, corrected typos in double precision Julian time calculation.
Again on 10/23/09, note on EXCEL time bug
Again 10/26/09, additional time calculation using DATA table


BS2 integer math and negative numbers

top

With the BS2 it is possible to work with negative numbers, to input them from a serial source (using SERIN) and to output them to a display (using SEROUT or DEBUG). However, it is necessary to understand which math operators do or do not work properly with negative numbers and to think about keeping the variables within proper limits. It is also necessary to understand how the math operations relate to numerical format options of the SERIN, SEROUT and DEBUG commands.

Most of the BS2 math operators, with only a couple of exceptions, are carried out using 16-bit words. When there is a negaitve sign, it is represented in the 15th, or most significant bit, thus:

  binary value         unsigned decimal        signed decimal 
16 bit unsigned twos complement
0000000000000000 0 0
1111111111111111 65535 -1
0000000000000001 1 1
1111111111111110 65534 -2

In the following, the input variable(s) and/or the result of the operation must lie within the specified range. Some operators can be thought of only in the context of positive integers, some only as twos complement, and some can be used as either. "nonsense" means that the operation does not make sense for the math operation in question. There will be some cases where something "tricky" could be done that makes sense out of nonsense. "bitwise logic operations" act bit by bit on the arguments.

                                     Range          
operator as unsigned as signed
abs (absolute value) nonsense -32768 to +32767
sqr (square root) 0 to 65535 nonsense
dcd (4 to 16 bit decode) 0 to 65535 nonsense
ncd (16 to 4+ bit encode)... 0 to 65535 nonsense
- (negation) nonsense -32768 to +32767
~ (ones complement) bitwise logic operation
SIN (sine) 0 to 255 input, output -127 to +128
COS (cosine) same as sine 0 to 255 input, output -127 to +128
note that the SIN And COS must be put into a word variable!.
+ (addition) 0 to 65535 -32768 to +32767
- (subrtraction) 0 to 65535 -32768 to +32767
/ (division) 0 to 65535 nonsense
// (remainder) 0 to 65535 nonsense
* (multiplication) 0 to 65535 -32768 to +32767
** (double multiply) 0 to 65535 nonsense
*/ (fractional multiply) 0 to 65535 nonsense
MIN (minimum) 0 to 65535 nonsense
MAX (maximum) 0 to 65535 nonsense
DIG (digit decode) 0 to 65535 nonsense
<< (shift left) 0 to 65535 nonsense
>> (shift right) 0 to 65535 nonsense
REV (reverse bit order) bitwise logic operation
& (bitwise AND) bitwise logic operation
| (bitwise OR) bitwise logic operation
^ (bitwise XOR) bitwise logic operation
IF-THEN (=,<,>) 0 to 65535 nonsense

The SERIN and SEROUT and DEBUG commands can accept negative numbers (-32768 to 32767), using the SDEC and SHEX and SBIN format modifiers, or positive integers (0-65525) using the DEC, HEX and BIN modifiers.

Never attempt to store a negative number in varible size other than word. The sign is always stored in the 15th bit.

If a division is to be performed, be sure all the arguments are positive numbers. If negative numbers are involved, first save the sign, convert the variables to positive numbers, do the division, then restore the sign. Example, to convert Celsius temperature (-30°C to +125°C) to Fahrenheit.

TF var word 
TC var word
TF = TC+30*9/5-22

The above adds 30 to the Celsius value to make it positive before the division. Then it multiplies times 9, divides by 5, and then subtracts 22, which you can verify makes it come out right. The usual way we think of this is TF=TC*9/5+32. But if we enter that formula as it is on a BS2, and the temperature is -30°C, then it will correctly muliply -30*9 to give -270. But it will not do the division correctly. Instead of -270/5=54, it will come up with -270/5=13053. The formula shown above first assures that the division will only be done on positive intgers. If there is any chance at all that the temperature will fall below 30°C, it would be better to play it safe and use, say, TF = TC+50*9/5-58 This will keep the answer correct for temperatures down to -50°C.

One problem that often comes up in Stamp math problems is appling the sign of one number to another number. Something like, multiply X times the sign of Y. There may be a calculation performed on the absolute value of a number, and then the sign restored to the result. The sign of Y in twos complement is

sign = Y.bit15

method #1 (my favorite, based on the definition of negation, fast):

sign = Y.bit15
X = -sign ^ X + sign

method #2 (slower because of muliplication):

sign = Y.bit15
X = -sign * 2 + 1 * X

method #1 (I like to avoid if-then)

if sign=0 then skipneg
X=-X
skipneg:


Display of data with a decimal point or justification

top

Sometimes you need to display data on a screen or to transfer it to a database in standard scientific or engineering units. This often requires that the data be given with a decimal point. Another requirement may be to have columns of numbers line up with the right edge aligned. The formatting options in the debug and serout commands make it relatively easy to get the result you want for data upload or display. But it takes a few tricks. The commands have no built in method to display a decimal point, or to right justify a number in a field.

To display positive result in tenths, 0<=x<=65535, with one digit to the right of the decimal point, to display as 0.0 to 6553.5

' one digit right of the point, tenths, positive numbers
x var word
debug DEC x/10,".",DEC1 x

5555.5
555.5
55.5
5.5
0.5

Same, in hundredths, to display the value as 0.00 to 655.35

' two digits right of the point, hundredths, positive numbers
x var word
debug DEC x/100,".",DEC2 x

555.55
55.55
5.55
0.55
0.05

 

What if the result can be negative in twos complement interpretation?
-32768<=result<=32767, to display as -3276.8 to 3276.7 e.g. a temperature.

' one digit right of the point, tenths, + or - numbers
x var word
debug 13*x.bit15+32,DEC abs x/10,".",DEC1 abs x

5555.5
-5555.5
555.5
-555.5
55.5
-55.5
5.5
-5.5
0.5
-0.5

This business with 13*sign+32 prints either a space or a minus sign as appropriate. The sign and absolute value are calculated as part of the display routine. Here is another way to write the debug command in the above, that does not print a leading space:

debug REP "-"\x.bit15,DEC abs x/10,".",DEC1 abs x

5555.5
-5555.5
555.5
-555.5
55.5
-55.5
5.5
-5.5
0.5
-0.5

The REP function prints either "-" or nothing at all, depending on the value of the sign bit.


If you want the display always to have a fixed number of digits, use DEC4 or DEC3 in the above to display the integer part, instead of just DEC.

' positive hundredths, right justified in 6 space field, leading zeros
x var word
debug DEC3 x/100,".",DEC2 x

555.55
055.55
005.55
000.55
000.05 <-leading zeros fill fields

It will come out with leading zeros. It is possible to get leading spaces, instead of zeros, but that takes more code. The following prints pads the data with enough leading spaces to keep the numbers right-aligned on a display. This is for integers, 65536>x>=0

' positive integers right justified in field of width fldw, leading spaces
x var word
n var nib
fldw con 5 ' field width desired, could be a variable
lookdown x,<=[9,99,999,9999,65535],n ' how many digits in the number?
n = fldw - 1 - n ' how many spaces to print in front
debug REP 32\n,DEC x


55555
5555
555
55
5

The operator, REP 32\n, prints a string of n spaces. (and zero spaces if n=0!) The lookdown command determines how many leading spaces to print based on the number of digits in x. This looks nice on a display. You can use other values or a variable for the field width. Just be sure that the field width is greatter than or equal to the maximum number of digits in the number, x. The following formula protects against errors in field width:
n=fldw min 1 - 1 min n- n

Here is the same thing, but with one digit to the right of the decimal point.

' positive tenths, right justified in 6 space field, leading spaces
x var word
n var nib
n=3 ' covers case of x<9
lookdown x,>[9999,999,99,9],n
debug REP 32\n,DEC x/10,".",DEC1 x

5555.5
555.5
55.5
5.5
0.5 <-note leading zero before decimal point.
as is considered good scientific notation

Just for variety, the above formula uses a different scheme for the lookdown function. It comes up directly with the number of spaces to add, for a fixed field width of 6.

And as a final example, the following is the same thing again, but the numbers can be either positive or negative
-32768<x<32768

' + or - tenths, right justified in 7 place field, leading spaces
x var word
n var nib
fldw con 7 ' field width desired, could be a variable
lookdown abs x,<=[99,999,9999,65535],n
n=fldw - 4 - n
debug REP 32\n,13*X.bit15+32,DEC abs X/10,".",DEC1 abs X


5555.5
-5555.5
555.5
-555.5
55.5
-55.5
5.5
-5.5
0.5
-0.5

Note that a field width of 7 will accomodate any word value for X. You can use a value of 7 or up, to set the width of the field. The number is right justified in that field. If the value of X will be less than or equal to 999.9, then the minimum is a 6 space field. And so on.

Any of the above can be written with the SEROUT command instead of the DEBUG.


Bar Graph on LCD display

top

The following implements a bar graph on an LCD. The CG ram characters give the bar graph a resolution of 1% across a line of a 4x20 LCD module, with a Scott Edwards' backpack. This is a much higher resolution than some other routines I have seen published, which just use the 20 big blocks across the screen for 5% resolution.

' program LCD_BAR for BS2 10/1997 
' Thomas Tracy Allen, tracy@emesystems.com
' Demo for bar-graph display on 4x20 lcd with Scott Edward's LCB backpack.
' Displays bar-graph of |sin t| on line 2 of the display, and noise on line 4
' Uses CG ram special characters for narrow bars to resolve 100 levels across each line.
' LCD connected to BS2 I/O 12.
n var byte
t var byte
z var byte
' Fill CG ram with narrow vertical bars of width zero, 1, 2, 3 and 4. left justified. 5 chars, 40 bytes.
' *Ascii 255 gives block 5 lines wide.
serout 12,$4054,[254,$40] ' select CG ram
for n=0 to 39 ' five chars, 40 bytes
serout 12,$4054,[DCD (n/8) - 1 REV 5] ' math trick to get patterns
next
serout 12,$4054,[254,1] ' clear screen
pause 100
' Now display graphs on LCD:
loop:
t=t+1 ' argument for sin t
n=abs sin t */ 200 ' normalize to 100 steps
serout 12,$4054,[254,128,"|sin ",dec3 t */ 360,"|=0.",DEC2 n]
' line 1, numbers |sin t|=...
serout 12,$4054,[254,192,rep 255\(n/5),n//5,rep 32\(19-(n/5))]
' shows bar graph on line 2
' noise: amplitude & time noisy
if t//(z.nib0 min 1) then loop ' random dwell time
random z
n=z*/100 ' random length, 0-99
serout 12,$4054,[254,148,"noise=0.",dec2 n]
serout 12,$4054,[254,212,rep 255\(n/5),n//5,rep 32\(19-(n/5))]
' show noise on lines 3,4
goto loop:
end

' Explaining serout for bar graph:
' 254,192 selects 2nd line of LCD
' rep 255\(n/5) prints 5x7 block (ascii 255) repeated (n/5) times
' n//5 prints the remainder 0,1,2,3,4 as a narrow bar of width 0->4 taken from CG rom
' rep 32\(19-(n/5)) clears to end of line by printing (19-(n/5)) spaces
'
' Explaining bars in CG ram:
' DCD (n/8) -1 generates 0,1,3,7,15
' .. REV 5 generates %0000, %10000 %11000, %11100 and %11110
' each pattern repeated 8 times for each char in CG ram.


Julian Date

top

Given day, month and year, the following Julian date routine produce one number, JD, that count the number of days from January 1st of a given year, up to 365 or 366 on December 31st. The second number, JDN, increases from 1 on January 1, 2001 through 36159 on Dec 31, 2099.

These numbers are useful for date calculations (finding the number of days between two dates) and for tagging data files by date, or for a simple way to export dates to spreadsheets such as Microsoft EXCEL. Excel stores dates internally as Julian day numbers offset from January 1, 1900 (= day 1on the PC) The Mac is different, of course, and for some mysterious reason counts from Jan 1, 1904 as day zero.  [Note:  A correspondent pointed out to me that there is actually a bug in the PC version, because a programmer way back when forgot that the year 1900 was not a leap year (divisible by 100 but not 400).   So EXCEL on the PC returns January 1st 1900 as day 1, but it also includes February 29th, 1900.   Oops.  So March 1st, 1900 ends up as day 61 instead of day 60.   In consequence, all days from March 1st 1900 on to the present are counted as if December 31st, 1899 was day 1.    Okay.   The Mac version did not have that problem because 1904 was in fact a leap year.]

Astronomers count Julian day numbers from January 1, 4713 BC as day 0, starting at noon.   January 1st, 2001 is julian day number 2451910.5 to 2451911.5 in that system.  This URL, {http://quasar.as.utexas.edu/BillInfo/JulianDatesG.html} explains that astronomers like to make the change of day at noon, so that one night of observation will all fall into one day.   Aha!

The main code here is a single long line of fancy BS2 arithmetic; there are no loops or lookup tables. You put in the year, the month and the day-of-month, as binary numbers (not BCD).   The code below assumes that every year divisible by 4 is a leap year.   Note that year 2000 was a leap year in the Gregorian Calendar, but that 2100, 2200 and 2300 will not be leap years.  Years that are divisible by 4 are leap years, unless also divisible by 100, but not 400.  The following algorithm will work from 2001 to 2099.   It does not know that 2100 will not be a leap year!

' JULIAN.BS2 code tested tracy@emesystems.com ------ 
' given mm/dd/yy
' calcs julian date 1->{365,366} in current year
' and julian date number 1->36159 based at 1/1/2001 as day 1,
' valid from 1/1/2001 to 12/31/2099
' YY var byte ' year, integer 1 to 99, not BCD
MM var byte ' month 1 to 12, ditto
DD var byte ' day 1 to 31, ditto
JD var word ' julian date in year, 0->365 or 366
JDN var word ' julian date 1->36158
year var word ' for a demo of the subroutine
' use Monday Jan. 1, 2001 as base date
' offset=36890 for PC EXCEL ' base date (Jan 1, 1900=1)
' offset=2451910 from 1 Jan 4713 BC, astronomers' base
' demo to show JD and JDN on 12/31 for each year
For year=2001 to 2099 ' demo covers 98 years
YY=year//100 ' last two digits 50 wrap around to 48
gosub julian ' find Julian data numbers and show...
debug CR,dec2 MM,"/",dec2 DD,"/",dec4 year," ",dec JD," ",dec JDN
pause 1000
next
end


'subroutine calculates & displays JD & JDN from mm/dd/yy (2001 to 2099)
' mm, dd, yy are straight binary values, not BCD
julian:
JD=MM-1*30+(MM/9+MM/2)-(MM max 3/3*(YY//4 max 1 +1))+ DD
JDN=JD+(YY-1*365)+(YY-1/4)+offset
return

' ---- end of program ----

Here is a rundown of what is going on in the formula:

(MM-1*30)
is a first approximation to the running date, assuming 30 days/month
+(MM/9+MM/2)
adds an extra day for every other month. That would be simply MM/2, however, both July and August are long, with 31 days. This must first be accounted for in September, the ninth month. Note the order of operations, where the 2 divides (MM/9+MM). The value added to the 30 day per month approximation by this term is, by month, 0,1,1,2,2,3,3,4,5,5,6,6.
-(MM max 3/3*(YY//4 max 1 +1))
accounts for the weirdness of February and leap years. This consists of two separate terms that are multiplied together.
(MM max 3/3)
equals 1 for months March and later, but it is zero in January and February. The overall term is always zero in January and February.
(YY//4 max 1 +1)
equals 2 in non-leap years, and 1 in leap years. The overall result is that starting in March, subtract either 2 or 1 to account for leap years and the shortness of the month of February.
+DD
is simply the number of days into the current month. All the calculation that has gone before is to calculate the number of days in the previous months. So on February 17th, the first term gives 30, the second term contributes 1, and the third term contribuites nothing, so the day of the year is 30+1+17=day 48. On October 25th in a leap year, the first term contributes 270, the second contributes 5, and the third term -1, so the day of year is: 270+5-1+25=299.
+(YY-1*365)
equals the number of days in previous years, assuming non-leap years. Eg. this term is zero in 2001, 365 in 2002, 2*365 in 2003 and so on up to 98*365 in 2099.
+(YY-1/4)
corrects for leap years. The first leap year after 2001 is 2004, but we don't have to account for that in the JDN formula until 2005.   This is integer division, so it becomes 1 in 2005, 2 in 2009, 3 in 2013 etc., to count the prior leap years.
+offset
offset added if needed for compatability with EXCEL or other base datum.  A big offset if you are an astronomer and want to start at 4713 B.C.

If you prefer to start at a different year, use (YY-startYear*365)  instead of (YY-1*365), and use (YY-startYear+(startYear-1//4) / 4) in the JDN calculation.   For example, to start at 2006, use
JDN = (YY-6 *365) + (YY-5/4) +  offset

Here is a formula that gives day of the week, given DDMMYY. Note that DD, MM and YY have to be in flat binary, not BCD notation.

'Sunday=0,Monday=1, Tuesday=2,..,Saturday=6
' for 21st century through 2099
dayofweek:
GOSUB julian
DOW = JDN//7

The Julian Day Number modulo 7 reduces to day of the week, and it happens that January 1st, 2001 was a Monday.


• Conversion of BCD time to and from flat binary values

Most real time clock chips return their data in BCD format, which is great for display, but not so great for calculations. But it is easy to convert BCD to binary, for use in the above formulae. For example, here the BCD on the right side is converted to binary on the left.

DD = DD.nib1 * 10 + DD.nib0   ' BCD to binary conversion of day
MM = MM.nib1 * 10 + MM.nib0 ' BCD to binary conversion of month
YY = YY.nib1 * 10 + YY.nib0 ' BCD to binary conversion of year

and the inverse

DD = (DD / 10 * 16) + (DD // 16)    ' binary to BCD
MM = (MM / 10 * 16) + (MM // 16)
YY = (YY / 10 * 16) + (YY // 16)

• Convert Julian day to standard yy/mm/dd format for one year

Here is a way to find the yy/mm/dd date given the julian date, JD. This come up because some clocks, the WWVB clock from Ultralink for example, give the day of year from 0 to 365 (or 366) along with a flag (LY) to show if the current year is a leap year or not. The stamp has to calculate the month and the day based on the number and the flag. Another use for this inverse formula is in this kind of question, "The cheese has to age for 100 days; what date should we take it out?" The calculation is easiest using JDN, but at the end it has to be converted back to a YYMMDD.

Given the ordinal number of the day in the year, JD, calulate the current month and day, mm/dd. I assume there is a flag LY that is 1 if the current year is a leap year, and 0 if it is not.

MM   var  byte  ' month, (not BCD)
DD var byte ' day, (not BCD)
JD var word ' julian date in year, 0->365 or 366
X var bit
X=JD max 60 / 60 * LY
lookdown JD-X,<=[31,59,90,120,151,181,212,243,273,304,334,365],MM
MM=MM+1
DD=JD-(MM-1*30+(MM/9+MM/2)-(MM max 3/3*(YY//4 max 1 +1)))

The lookdown table entries are ordinal values of the last day of each month, when the year is not a leap year. In a non-leap year, the last day of February is the 59th day of the year (as shown in the lookdown list), but in a leap year, it is the 60th day of the year. The formula uses a trick to make the same table apply for leap years, which after all, vary by only one day. The trick uses the auxiliary varible X. The value of X is one in leap years, starting on the last day of February. The lookdown formula, come the 60th day of a leap year, subtracts one from JD, and thus gets the correct month for leap years too. Then, having the correct month, the formula for DD calculates the difference between the current ordinal day of year and the last day of the previous month.


• Another way to calculate the day of the year 1-365 (366) from month, day and year, and inversely, month year and day from day of year.
' {$STAMP BS2pe}
' {$PBASIC 2.5}
' calculate mm/dd from day of year
' using a DATA table lookup for months
jd VAR WORD ' day of year, 0-365 (366 in leap year)
mm VAR BYTE ' month
yy VAR BYTE ' year
dd VAR BYTE ' day of month
ly VAR BIT ' iff leap year ly=1
xb VAR BIT ' helper for calc
yb VAR BIT ' helper for calc

' the DATA table values are ordinal last day of month in non-leap year.
' note months Sep-Dec are Bytes in the table mod 256
' The term (yb * 256) belos is a trick to allow the DATA table to be Bytes only
DATA 0,31,59,90,120,151,181,212,243,273,304,334,365

top:
DO
DEBUG CR, "enter mm/dd/yy:"
DEBUGIN DEC mm,DEC dd, DEC yy
ly = 1 - (yy // 4 MAX 1) ' leap year -> ly=1
yb = mm/9 ' = 1 for months Sept-Dec
xb = mm MAX 2 / 2 ' = 1 for months Feb on
READ mm-1, jd
jd = jd + dd + (yb * 256) + (xb * ly)
DEBUG CR," DayOfYear= ", DEC jd," mm/dd= ",DEC2 mm,"/",DEC2 dd,CR,CR
PAUSE 1024
LOOP
• And the inverse, dd/mm from the ordinal day of year, uses the same DATA table
' {$STAMP BS2pe}
' {$PBASIC 2.5}
mm VAR BYTE ' month, (not BCD)
dd VAR BYTE ' day, (not BCD)
jd VAR WORD ' julian date in year, 0->365 or 366
ly VAR BIT ' 1 if leap year, 0 if not
xb VAR BIT ' helper bits for calculation
yb VAR BIT
DATA 0,31,59,90,120,151,181,212,243,273,304,334,365
top:
DEBUG "enter year, 2001 to 2099:"
DEBUGIN DEC jd
ly = 1 - (jd // 4 MAX 1) ' leap year -> ly=1, else ly=0
DO
DEBUG CR, "enter day of year, 1-",DEC 365+ly,": "
DEBUGIN DEC jd
IF jd=0 THEN top ' user wants a different year
mm=0
DO ' scan data to find proper month
mm = mm+1
READ mm,dd
yb = mm/9 ' flag teble entries 9,10,11,12 are >256
xb = mm MAX 2 / 2 ' flag February and later.
LOOP UNTIL jd <= dd + (yb * 256) + (xb * ly)
READ mm-1,dd
dd = jd - dd - (yb * 256) - (xb * ly)
DEBUG CR," DayOfYear= ", DEC jd," mm/dd= ",DEC2 mm,"/",DEC2 dd,CR,CR
LOOP



The hour of the year is necessary to change from UT to local time. This kind of adjustment is necessary if you are getting your time from a time server.

JD   var  word  ' julian date in year, 0->365 or 366 
HH var byte ' hour in binary (not BCD)
JH var word ' julian hour, 0->8760 or 0->8784 (hours in one year)
LT con -8 ' local time offset from UT (-8 hours is for California)
JH = JD*24+HH+LT ' compute local hour of year.
HH=JH//24 ' hour local time
JD=JH/24 ' day of year
' use above formula to calculate month and day
' a correction is needed to go smoothly over the transition to a new year.

 


Julian minutes or Julian seconds  in a day acquired from a real time clock
Time calculations often use a sequential time.   Here it is calculated directly from BCD values that would come directly from an RTC:

HH   var  byte  ' month, (BCD)
MM var byte ' day, (BCD)
SS var word ' julian date in year, 0->365 or 366
JH VAR Word ' julian hour of day
JM VAR Word ' julian time of day, minutes. 0 to 1439
JS VAR Word ' julian time of day, seconds/2, 0 to 43199
JDH VAR Word ' cumulative hours from start of a previous year, 8760 hours in a year, 8784 in leap year

JM = HH.nib1*10+HH.nib0*6+MM.nib1*10+MM.nib0 ' minute of day, 0-1339. This formula is very useful!!

JS = JM *6 + SS.nib1 * 5 + (SS.nib0 / 2) ' second of day in units of 2 seconds
' 86400 seconds in a day, to many for one Stamp word, but the Stamp can handle 43200 units of 2 seconds.
Sequential time is useful for locking events onto clock time.   For example, if you have some events that must occur every hour, and others every 3 minutes and others every 20 seconds, then it is easy with
IF  JS // 1800 = 0  THEN ...     ' every hour
JS // 10 =0 THEN ... ' every 20 seconds
JS // 90 = 0 THEN ... ' every 3 minutes



Julian minutes extended to cover many years in double precision
Sometimes it is convenient to extend the Julian time variable to double precision, in order to have an ordinal time number in units of minutes or other fine time unit that covers a period of years.   Sometimes events have to be synchronized to the clock, for example the start and stop times for data logging.    It is true that this can be done in steps by comparing year, month, day, hour and minute for a match.   But when you get into it, the code becomes kind of messy and even more so when it is necessary to compare two different times or to calculate the difference between two times.   It is most convenient not to have to worry about crossing boundaries of days or months or years.    The following routines rely on double precision tricks in the math6 document. There are 52067520 minutes in the 21st century, or 3124051200 seconds.   Either number fits with no problem into a 32 bit double precision word.   A single word can hold the number of minutes in 45+ days, but not even a full day's worth of seconds (86400 seconds per day).

   
' Calculate minutes elapsed from midnight starting 1/1/2001 through the yr/mo/dy, hr:mn passed in at entry
' The values passed in are flat binary, not BCD.
julianMinute:
jd=mo-1*30+(mo/9+mo/2)-(mo max 3/3*(yr//4 max 1 +1))+ dy
jdn=jd+(yr-1*365)+(yr-1/4) - 1 ' day since 1/1/2001, minus 1 (through "yesterday")
jm = hr * 60 + mn ' minute of day for today, 0 to 1339
x1 = jdn ** 1440 ' minutes elapsed from 1/1/2001 through "yesterday", double precision
x0 = jdn * 1440 + jm ' low word of product plus minutes from "today"
IF x0<jm THEN x1=x1+1 ' add possible carry from the addition of jm
RETURN ' x1:x0 contains the number of sequential minutes from 1/1/2001 up to specified datetime.


' Compare two julian minute values x1:x0 and y1:y0 to see if the first is greater than the second.
' Also returns the difference in minutes
sub1616: ' Z=X-Y in s16:16 notation
z0 = x0 - y0
z1 = x1 - y1
IF z0 > x0 THEN z1 = z1 - 1 ' borrow
sign = z1.bit15
IF sign THEN ' want to return absolute value of z1:z0
z0 = ~z0 + 1 ' note tilda (ones complement) not subtract
z1= ~z1
IF z0=0 THEN z1 = z1 + 1 ' a carry is generated if z0=0
ENDIF
' sign=1 if y1:y0 > x1:x0
' sign=0 if y1:y0 <= x1:x0
' z1:z0 holds the absolute value of the difference in minutes
RETURN



IF-THEN LET constructs and control variables

top

The obvious way to make a thermostat in PBASIC code is to use IF and THEN statements:

loop:
gosub read_temperature ' not shown
if temperature > 87 then fan_on
if temperature < 82 then fan_off
goto loop

fan_on
high fan_pwr ' turn it on
goto loop

fan_off
low fan_pwr ' turn it off
goto loop

This is easy to read when there are just a few things that need to be done in the program, but as the program gets more complicated it reaches a point where all the gotos become what is often called "spagetti" and it is hard to follow one strand through the bowl. The stamp does not allow subroutine or function calls directly as part of an IF-THEN construct, and that adds to the confusion.

Here is a way to make a thermostat, using the BS2 MAX and MIN operators, without using IF-THEN:

' control on at >=87, off at <=86
loop:
gosub read_temperature ' not shown
fan_pin = temperature min 87 - 87 max 1 ' fan on above 87
goto loop

The arithmetic assignment holds the fan_pin low and for temperatures up to and including 87, and turns the fan_pin high when the temperature exceeds 87. Note now that the control of the fan is all contained in one single line of code.

No IF-THEN or spagetti required. The MIN and MAX operators here are used to construct a desired mathematical switching function.

You have to understand how the MIN and MAX operators work on the BASIC Stamp. The numbers are treated as positive numbers. No twos complement ordering!

z = x MIN y
equals y when x<=y "minimum value is y"
equals x when x>=y "except when x exceeds y"

z = x MAX y
equals x when x<=y "can be less than y"
equals y wnen x>=y "but maximum value is y"

(This is opposite of the way MIN and MAX work in some other computer languages. In some languages, x MIN y would equal whichever is smallest, x or y, and x MAX y would equal which ever of x or y is largest.)

The relationship is commutative. Look at reversing x and y in the first equation above and write out the result:

x = y MIN x
equals x when y<=x
equals y when y>=x

Compare this with the first equation above, and you will see that they are identical, that is, commutative in x and y.

Note that the if-then example that started off this section has a 5 degree hysteresis band, so that the fan will not turn on and off too frequently. The program just above does not have any hysteresis. Here is a variation that adds it. The state of the fan control output (fan_pwr) is a bit variable, and the state of that variable is rolled back into the equation on the right hand side to add the hysteresis:

' fan on when temperature>=87, off when temp.<=82
' 5 degree hysteresis band
loop:
gosub read_temperature ' not shown
fan_pwr = temperature + (fan_pwr*5) min 87 - 87 max 1
goto loop

When the fan is off, that means fan_pwr=0, so the term (fan_pwr*5) also equals zero. The control expression is the same as it was above, and the fan will run on as soon as the temperature rises above 87. But that makes fan_pwr=1. So now (temperature+5) has to fall to 87, that is, temperature has to fall to 82, and only then will the expression on the right hand again equal zero, and the fan will turn off. And so on.

Note that the MIN and MAX functions do not work with twos complement negative numbers, so you cannot, say, have threshold of -10 degrees. For that kind of thing, you need to make adjustments. Remember that MIN and MAX always treat the numbers as positive values in the range of 0 to 65535.

The point is to show a way of thinking that tries to change IF-THEN conditionals into arithmetic.

Here are a couple more tricks:

heater = temperature - 99 max 1 

gives heater=0 only when temperature=99. (note that in BS2 math, 98 - 99 equals 65535, which greater than 1)

heater = temperature/threshold

This makes heatr = 1 when temperature>threshold, so long as temperature is never larger than 2*threshold. Or use,

heater = temperature max threshold /threshold

This limits the division to 0 or 1. Be aware that division on the stamp is slower than addition and the logic operators.

There are many tricks of this sort. These tricks can get quite complicated in themselves when several variables are involved. The conditions can be combined logically, for example, to turn on the fan when the temperature is greater than 87 but only when the lights are on and a timer has not run down to zero.

  fan_pwr = temperature+(fan_pwr*5) min 87-87 max 1 & lights_on & (timer max 1)

When you use these tricks, it is very important to document what they are supposed to do in the code. It is awfully easy to lose track when the expressions get more complicated.


Comparisons of magnitude

top

The max and min operators are useful for constructing comparisons, as shown in the above section. Here is a collection of comparison operators. Remember that these are all treated as positive integers 0 to 65535, unless otherwise noted. All variables are converted internally in the Stamp to 16 bit before computation.

y - x max 1
x - y max 1
0 if and only if y = x
This applies even if the numbers are twos complement.
 
y min x - x max 1
y max x - y max 1
0 if y <= x
1 if y > x

 

y max x - x max 1
y min x - y max 1
0 if y >= x
1 if y < x

Sometimes it is necessary to have similar tests for double precision numbers. We use two 16 bit words to contain a 32 bit double precision number, for example y is y1:y0 and x is x1:x0. The tests can be done with IF-THEN logic:

'equals
if y1<>x1 then notequal
if y0<>x0 then notequal
equal:
'do equal thing
return
notequal:
'do notequal thing
return

' greater than, less than, or equals
if y1>x1 the greaterthan
if y1<x1 then lessthan
' here if high words are equal
if y0>x0 then greaterthan
if y0<x0 then lessthan
' here if equal, do the equals thing
return
greaterthan:
' do it
return
lessthan:
'do it
return

Or it can be done with formulae:

equals = (y1 - x1 max 1) & (y0 - x0 max 1)
' is 1 iff y1:y0 = x1:x0

equals = (y1 ^ x1) & (y0 ^ x0)
' is 1 iff y1:y0 = x1:x0

greater = (y1 min x1 -x1 max 1) | ((y0 max x0 - x0 max 1) & (y1-x1 max 1 -1))
' is 1 if y1:y0 is greater than x1:x0
' note that the low words are important only if the high words are equal
' which leads to the compound expression on the right side of the OR operator
' (y1-x1 max 1 -1) is -1 (all bits set) only if y1 and x1 are equal

The above are not much more than curiousities.   The most useful comparison is simple a double precision subtraction, which yields both the sign bit and the value of the difference.   From that follows equal, greater than or less than, and by how much.

y1 = y1 - x1 - (y0 max x0 - x0 max 1)   ' high subtract minus borrow
y0 = y0 - x0
if y1.bit15=0 then ygreaterthanorequaltox

Also see above the Julian time comparison in double precision for another way of doing the math, and the BS2math6 essay on double precision.


<top> <index> <home> logo< mailto:info@emesystems.com >