.. _clockworks_keeping_track: Keeping Track of Time ===================== :Date: 2017-08-08 .. contents:: :local: :depth: 1 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 <https://en.wikipedia.org/wiki/Gregorian_calendar>`_ 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 ``12`` --- **offset 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 <https://en.wikipedia.org/wiki/Julian_day>`_ 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 <https://en.wikipedia.org/wiki/List_of_calendars>`_ . Design Decisions ---------------- * We shall implement the usual counters used in the `Gregorian Calender <https://en.wikipedia.org/wiki/Gregorian_calendar>`_ : 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* <https://en.wikipedia.org/wiki/Leap_year>`_ * We shall ignore the concepts of `*time zone* <https://en.wikipedia.org/wiki/Time_zone>`_ and `*daylight savings <https://en.wikipedia.org/wiki/Daylight_saving_time>`_ time*. The clock will be running in some *local time zone* * We shall ignore the concept of the `*leap second* <https://en.wikipedia.org/wiki/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. .. code-block:: forth 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. .. code-block:: forth 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 .. code-block:: forth 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. .. code-block:: forth 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 .. code-block:: forth 1 bv tu.flags fset? if ... then or by giving the bits explicit names .. code-block:: forth 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``. .. code-block:: forth variable tu.limits #4 allot These values need to be initialized upon startup. .. code-block:: forth : 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? .. code-block:: forth \ 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. .. code-block:: forth 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. .. code-block:: forth : 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: .. code-block:: forth : 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. .. code-block:: forth : 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``. .. code-block:: forth : 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 #. in ``init`` we need to call ``timeup.init`` #. in ``run-loop`` we call ``timeup`` after determining that a second has passed .. code-block:: forth \ 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 :ref:`clockworks_periodic_jobs`. The Code -------- .. code-block:: forth :linenos: \ 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! ;