- BS2 integer math and negative numbers
- Display of data with decimal point
- Bar Graph on LCD display, high resolution
- Julian Date and time
- IF-THEN LET constructs
- Comparisons of magnitude

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

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.

Rangeoperatoras unsignedas signedabs (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:

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.

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.

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:

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

- (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.

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.

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)

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.

' {$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

' {$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.

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 withHH 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.

IF JS // 1800 = 0 THEN ... ' every hour

JS // 10 =0 THEN ... ' every 20 seconds

JS // 90 = 0 THEN ... ' every 3 minutes

' 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

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.

- (temperature min 87) is clamped at 87 for all values of temperature <=87
- so (temperature min 87 -87) equals zero for all values of temperature <=87
- for values of temperature >87, the expression (temperature min 87) is some value greater than 87
- so (temperature min 87 -87 max 1) equals 1

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 (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 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.

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 1y 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.