Keeping Track of Time

Date:2017-08-08

Intro

While just counting clock cycles or seconds would still make a clock, its usability would be seriously lacking. To be able to catch the next train, we would like our clock to use the well known set of counters used in the Gregorian Calender of the western world.

seconds: ranging from 0 to 59 (and very occasionally to 60)

minutes: ranging from 0 to 59

hours: ranging from 0 to 23, allthough a 12 mod operation is applied to common clock faces and commonly used spoken terms.

days: ranging from 1 to 31 during a month — offset by 1

months: ranging from 1 to 12offset by 1

years: being 2017 currently in the gregorian calender

However, if you think about this for a few moments, there is no shortage of alternatives.

seconds of a day: ranging from 0 to 86400-1.

minutes of a day: ranging from 0 to 1440-1.

epoch seconds used in the Unix world: ranging from 0 on 1970-01-01 0h UTC to some fairly large number in the forseeable future (like 1502223440 at the time of this writing)

day of year: ranging from 1 to 365 or 366 during the course of a year, and possibly much further (Julian Days anyone?)

Other calendar systems use different values for the lengths of months and years. I will not be concerned about them, but implementing different time/calendar systems is not difficult.

There is no shortage of calendar systems, see this wikipedia article for a List of Calendars .

Design Decisions

  • We shall implement the usual counters used in the Gregorian Calender : seconds, minutes, hours, days, months, years
  • Hours range from 0 to 23, we shall ignore the concept of am/pm flags
  • Days and months shall start at 0, not 1
  • We shall certainly implement the concept of the *leap year*
  • We shall ignore the concepts of *time zone* and *daylight savings time*. The clock will be running in some local time zone
  • We shall ignore the concept of the *leap second*
  • A function called timeup shall increment the lowest counter used (seconds) and then correctly keep track of all overflows (e.g. one hour or one day has passed). I have first found this concept in a book on PIC Microcontroller Programming (German: A.König/M.König — Das PICmicro Profi-Buch, 1999, Franzis Verlag, ISBN 3-7723-4284-1).
  • overflows will be recorded to enable a system of periodic jobs, allthough this is not strictly needed
  • the counters are defined as variables to keep it simple for now

Code Details

This is going to be lengthy but not difficult, I hope.

Counters

We need a set of the desired counters. I decided a very long time ago to use an array, but also to add words to access certain fields directly. Forth structures offer a cleaner way that I will show later on.

We have 7 counters: tick, sec, min, hour, day, month, year. variable allots 1 cell already, so we need another 6.

variable tu.counts #6 cells allot

The values reside in an array starting at address tu.counts. The address of the seconds counter is at offset 1 cells, so I define a constant to point there, and similar for the other counters.

tu.counts            constant tick
tu.counts #1 cells + constant sec
tu.counts #2 cells + constant min
tu.counts #3 cells + constant hour
tu.counts #4 cells + constant day
tu.counts #5 cells + constant month
tu.counts #6 cells + constant year

With these definitions I can write

sec @ .

without remembering, that sec is actually a field in an array. Also when calling sec, there is no index calculation done any more, because I stored the result in a constant.

Flags

I want to record any overflows that have occured. This information fits into one bit, so I decided to use flags for them.

include common/lib/flags.frt
variable tu.flags

tu.flags offers space for 16 bit flags. They can be defined and used directly like this

1 bv tu.flags fset? if ... then

or by giving the bits explicit names

tu.flags  1 flag: f.tu.sec.over

f.tu.sec.over fset? if
  \ ... do something
  f.tu.sec.over fclr
then

Limits

We need a place to store the limits at which each of the counters is overflowing. The values are smaller than 255, so I decided to use an array of Bytes. year does not have such a limit, so 6 Bytes are sufficient. Again, 2 Bytes get reserved by variable.

variable tu.limits  #4 allot

These values need to be initialized upon startup.

: timeup.init
  0      tu.flags !                  \ clear flags
  tu.counts #8 erase                 \ clear counters
  #60    tu.limits 1 + c!            \ init limits
  #60    tu.limits 2 + c!
  #24    tu.limits 3 + c!
  #31    tu.limits 4 + c!            \ months: may be wrong!
  #12    tu.limits 5 + c!
;

Why don’t I keep these limits in flash? Well, that would work for all except the limit of month. That limit varies between 28 and 31 and needs to be adjusted accordingly.

Leapyear?

Leap years are an integral part of the gregorian calender, so we better have a function to determine, whether a given year is one or not. This function is so simple that everyone rolls its own, maybe it should be included in AmForth?

\ ewlib/leap_year_q.fs
\ is yyyy a leap year? answer yes (-1) or no (0)!
: leap_year? ( yyyy -- t/f )
  dup    #4 mod 0=
  over #100 mod 0<> and
  swap #400 mod 0=  or
;

Last Day of Month

Unfortunately, the length of our months is not constant. And they do not follow a simple scheme — for political reasons very long ago. So we have to make due with that somehow.

Firstly I create a table in flash. The index is month-1, the value is its length in days, good for a common year.

create tu.lastday_of_month
   #31 , #28 , #31 , #30 , #31 , #30 ,
   #31 , #31 , #30 , #31 , #30 , #31 ,

Then I create a function to determine the last day of a given month in a given year. This function consults the table just defined, but checks whether February’s result must be adjusted.

: lastday_of_month ( year month -- last_day )
  dup 1-                             \ array starts at 0
  tu.lastday_of_month + @i
  swap #2 = if                       \ if month == 2
    swap leap_year? if               \   if leap_year
      1+                             \     month += 1
    then
  else                               \ else
    swap drop                        \   remove year
  then
;

Since we need to update the entry for month in the tu.limits table regularly, I defined a function to do just that, too:

: tu.upd.limits ( Y m -- )  lastday_of_month  tu.limits #4 + c! ;

Timeup: advance counters

Now we should have all data structures and tools to increment one counter and correctly infer, whether a higher counter needs to be incremented as well.

timeup is called, when a second has passed. So it sets the corresponding flag and increments the sec counter.

: timeup ( -- )
  $02 tu.flags fset                     \ secflag++
  1 sec +!                              \ sec++

  \ for ( sec ) min hour day month year
  #6 1 do
    i cells tu.counts + @   1+          \ Counts[i]+1
    i       tu.limits + c@              \ Limits[i]
    > if                                \ if Counts[i]+1 > Limits[i]
      0  i cells tu.counts +  !         \ . Counts[i]=0
      i 1+ bv tu.flags fset             \ . Flags[i+1]=1
      1 i 1+ cells tu.counts + +!       \ . Counts[i+1]++
    then                                \ fi
  loop
;

After that, it loops over these counters to see, whether the corresponding limit has been reached. If this is the case, the inspected counter (e.g. sec) is reset to 0, the flag of the next higher counter (min in this case) is set (i 1+ bv tu.flags) and the next higher counter is incremented. To make this task possible as a loop, I decided to keep the counters of day and month with an offset of 1.

Get / Set / Show

To set and inspect the counters, three more words are useful. Please note that the counters day and month need offset-by-1 treatment, and that the setter also needs to update the entry of month in table tu.limits.

: tu.set ( Y m d H M S -- )
  sec ! min ! hour !
  1- day !
  over over
  1- month !
  year !
  ( Y m ) tu.upd.limits
;
: tu.get ( -- S M H d m Y )
  sec @ min @ hour @
  day @ 1+ month @ 1+ year @
;
: tu.show ( -- )
  tu.get
  #4 u0.r  #2 u0.r  #2 u0.r  [char] -  emit
  #2 u0.r  #2 u0.r  #2 u0.r
;

Putting it all together

These tools do show up in the main program in two places
  1. in init we need to call timeup.init
  2. in run-loop we call timeup after determining that a second has passed
\ main-04.fs
include ewlib/clockticks_clock_crystal.fs
include ewlib/timeup_v1.fs
include ewlib/leap_year_q.fs

variable ticks
: init
  ...
  0 ticks !
  timeup.init
  +ticks
;

: run-loop
  init
  begin
    tick.over? if
      tick.over!                     \ acknowledge
      \ ...                          \ one tick over, do someting
      1 ticks +!                     \ count ticks
    then

    ticks @ 1+  ticks/sec > if
      ticks @ ticks/sec - ticks !    \ reduce ticks
      timeup                         \ advance clock counters
      \ ...                          \ one second over, do something!
    then

  again
;

The recorded tu.flags are not yet used by the main program. This is detailed in section Periodic Jobs.

The Code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
\ 2015-10-11 ewlib/timeup_v0.0.fs
\
\ Written in 2015--2017 by Erich Wälde <erich.waelde@forth-ev.de>
\
\ To the extent possible under law, the author(s) have dedicated
\ all copyright and related and neighboring rights to this software
\ to the public domain worldwide. This software is distributed
\ without any warranty.
\
\ You should have received a copy of the CC0 Public Domain
\ Dedication along with this software. If not, see
\ <http://creativecommons.org/publicdomain/zero/1.0/>.
\
\
\ variables
\     tu.counts -- fields available as:
\     tick sec min hour day month year
\ words:
\     timeup.init
\     timeup
\     lastday_of_month ( year month -- last_day )
\     tu.get  ( -- S M H d m Y )
\     tu.set  ( Y m d H M S -- )
\     tu.show ( -- )

#include leap_year_q.fs

variable tu.flags

variable tu.counts     #7 cells allot
tu.counts            constant tick
tu.counts #1 cells + constant sec
tu.counts #2 cells + constant min
tu.counts #3 cells + constant hour
tu.counts #4 cells + constant day
tu.counts #5 cells + constant month
tu.counts #6 cells + constant year

variable tu.limits     #6       allot

create tu.lastday_of_month
   #31 , #28 , #31 , #30 , #31 , #30 ,
   #31 , #31 , #30 , #31 , #30 , #31 ,

: lastday_of_month ( year month -- last_day )
  dup 1-                \ array starts at 0
  tu.lastday_of_month + @i
  swap #2 = if          \ if month == 2
    swap leap_year? if  \   if leap_year
      1+                \     month += 1
    then
  else                  \ else
    swap drop           \   remove year
  then
;

: timeup.init
  0      tu.flags !
  tu.counts #8 erase
  #60    tu.limits 1 + c!
  #60    tu.limits 2 + c!
  #24    tu.limits 3 + c!
  #31    tu.limits 4 + c! \ fixme: may be wrong later!
  #12    tu.limits 5 + c!
;

: timeup ( -- )
  $02 tu.flags fset                \ secflag++
  1 sec +!                         \ sec++

  \ for ( sec ) min hour day month year
  #6 1 do
    i cells tu.counts + @   1+     \ Counts[i]+1
    i       tu.limits + c@         \ Limits[i]
    > if                           \ if C[i]+1 > L[i]
      0  i cells tu.counts +  !    \ . C[i]=0
      i 1+ bv tu.flags fset        \ . F[i+1]++
      1 i 1+ cells tu.counts + +!  \ . C[i+1]++
    then                           \ fi
  loop
;

\ update lastday_of_month in tu.limits
\ once current date is known
: tu.upd.limits ( Y m -- )
  ( Y m ) lastday_of_month  tu.limits #4 + c!
;