Menu

The millis() Function - A GCBASIC Implementation

2019-01-02
2020-02-24
  • Chris Roper

    Chris Roper - 2019-01-02

    Introduction

    One of the more useful, and misunderstood, functions on the Arduino platform is the millis() function.

    millis() returns the elapsed time in milliseconds since the device was last reset. A 32 bit unsigned Long variable contains the count and will only rollover in slightly over 49 days, making it useful for both very small and very long timing situations. Many people assume that the millis() function is part of the C compiler, especially as a similar function is available to Compilers under Linux and Windows. As a result they ask why other versions of C, especially on the PIC platform, do not have such a function.

    What they miss is that millis(), whilst implemented in software, is not a software function it is a doorway to a hardware timer. Arduino is a platform, not just a bare chip, and certain hardware is devoted to Arduino Functions in the same way that windows and linux use features of the motherboard chipset. In fact the equivalent of the millis() function predates microcontrollers and PC’s as it was a function of early discreet logic PLC’s and was used to record system up time for maintenance.

    So what is Millis() good for?

    millis() is a simple way of using a Hardware Timer without having to know anything about Hardware Timers.

    In its simplest form millis() is a record of how long the system has been running since the last reset, think UNIX Time on a smaller scale, but it can also be used to time events, control the duration of actions, create a Clock / Calendar or replace the Wait or Delay Function amongst many other uses. Trying to port applications from Arduino to GCBASIC can be a daunting task if the application relies on functions such as millis(). In addition to that anyone who has ever used the millis() function will soon long for an equivalent function in their current platform.

    More importantly, however, It allows user applications to implement rudementery multitasking and, for 8 Bit devices with limited resources, that alone makes it worth more than the effort required to implement it, so I set out to find a simple way to implement a millis() function as part of the GCBASIC language.

    Implementation

    Setting up a millis() function is not a particularly difficult task but, as it relies on a hardware timer, it could easily result in portability issues unless proper consideration is given in the design process. GCBASIC Supports PIC devices in the 12F, 16F, Enhanced 16F and 18F families along with AVR devices used by the Arduino family of boards. They are all different architectures but all of them have at least one hardware Timer and, as a hardware timer is the basis of the Millis function, all we need to do is set it up to generate an Interrupt every Millisecond and then count them.

    Each family or architecture has its own set of registers to control the onboard timers whilst the devices themselves are are all capable of being clocked at a range of clock speeds, but with careful programming GCBASIC can hide a lot of that complexity through the use of Functions and Scripts, completely hidden from the users in the GCBASIC Preprocessor. As a result, once the hardware differences are factored out in the preprocessor, simple code written to use a Timer on the ATmega328p, the device in the Arduino UNO, or a PIC device on a Breadboard, should then run on a similar Timer on any of the supported device families, regardless of architecture.

    I chose to use Timer0 for the millis() function as that timer is the most common amongst the supported devices, has a similar architecture regardless of family, is rarely used for other hardware functions such as PWM and being 8 bit (in most architectures) is the least useful timer in most applications.

    Future Versions will hopefully be able to use alternative timers but, unless there is a major conflict with other core libraries that I am unaware of (Highly likely) that is not a short term goal and more of a convenience for advanced users.

    All that remains is to implement it in such a way that it is easy to use for experienced Arduino Users and simple enough for PIC and other GCBASIC users to begin to utilise the benefits of a millis() Function on their chosen hardware platform too.

    millis() version 0.1

    This first release of the code is a Proof of concept / Beta Test release. Hopefully it will - once tested, massaged, expanded and polished by the GCBASIC community - become part of the GCBASIC Language.

    The proof of concept was written and tested on Arduino UNO hardware for easy correlation and testing with the Arduino version of the function. It was then ported to a Microchip PIC16F690 microcontroller on a Low Pin Count Demo Board for comparison, and then tested on several PIC devices of varying Vintage and Clock Speed on the LPCDB. Once acceptable results were obtained Development and testing progressed satisfactorily to the 28 Pin demo board and a selection of Enhanced Core PIC16 and PIC18 devices.

    The following points apply as a result:

    1. As the Arduino UNO has a fixed clock rate of 16 Mhz that is the only supported clock rate for AVR devices as of this post.
    2. The PIC has an internal clock which it divides by 4 before passing to the Prescaler, so a larger range of frequencies are available to PIC devices.
    3. The Frequencies implemented so far are Binary divisions i.e 1Mhz, 2MHz, 4Mhz, 8Mhz, 16Mhz, 32Mhz and 64Mhz (The PIC18F26K22 was used to test 64Mhz)
    4. Tested devices work at their default frequency or any frequency specified. i.e. #Chip 16F690, 2
    5. If a frequency higher than the device is capable of is selected it should revert to its default frequency, however, unexpected results could be observed as this is not explicitly tested for.
    6. Future versions may use scripts to setup the Timers to cope with additional and arbitrary Clock Speeds.
    7. Despite not being integrated into the core Libraries Millis is still easy to use if you wish to test and give feedback to aid development.

    Testing and Feedback

    If you would like to try the millis Function download the attached “millis.h” and place it in a working folder along with any *.gcb files that use it, then in the source for your GCBASIC program add the line:

    #include “millis.h”
    

    The quotes are important, do not remove them.

    The two examples in below are a new take on the old BLINK function that most of us recall as our first ever program and serves to shows that portability has been achieved.

    '
    #chip mega328p, 16        ' Declare the Target Processor and Speed
    #option explicit          ' Require Explicit declaration of Variables
    #include "millis.h"       ' Include the Library
    
    #define LED PORTB.5       ' Define the LED Pin - Digital Pin 13
    #define LEDRate 1000      ' Flash rate in mS
    ' Setup
    Dir LED Out               ' Make the LED Pin an Output
    Dim CurMs, LstMs as Word  ' declare working variables
    ' Main                    ' This loop runs over and over forever.
    Do
      CurMs = millis()
      if CurMs - LstMs >= LEDRate then  ' required Time has Elapsed
        LED = !LED                      ' So Toggle state of LED
        LstMs = CurMs                   ' And Record Toggle Time
      end if
    Loop
    

    By changing only the #Chip field and, of course the LED Pin, this same code will run on a PIC16F690 on a Microchip Low Pin Count Demo Board:

    '
    #chip 16F690,8            ' Declare the Target Processor and Speed
    #option explicit          ' Require Explicit declaration of Variables
    #include "millis.h"       ' Include the Library
    '
    #define LED PORTC.0       ' Define the LED Pin
    #define LEDRate 1000      ' Flash rate in mS
    ' Setup
    Dir LED Out               ' Make the LED Pin an Output
    Dim CurMs, LstMs as Word  ' declare working variables
    ' Main                    ' This loop runs over and over forever.
    Do
      CurMs = millis()
      if CurMs - LstMs >= LEDRate then  ' required Time has Elapsed
        LED = !LED                      ' So Toggle state of LED
        LstMs = CurMs                   ' And Record Toggle Time
      end if
    Loop
    

    With Arduino millis() compatibility, Ease of use and Device Portability all in the bag we can now start to look at improvements and enhancements. But first I will run through a set of experiments to show why millis() is important and a couple of features that go beyond the Arduino version of millis, once again showing that GCBASIC is the better tool, even on the Arduino platform.

    Subsequent posts in this thread will hold examples and descriptions in tutorial format to help you get started with millis() and will hopefully scratch the surface of the abilities of this useful function so stay tuned

    Cheers
    Chris

    EDIT - Attachment updated to detect unsupported Devices

     

    Last edit: Chris Roper 2019-01-03
  • Chris Roper

    Chris Roper - 2019-01-02

    Using millis()

    Description
    Returns the number of milliseconds since the Device began running the current program. This number will overflow (go back to zero), after approximately 50 days.

    Syntax
    time = millis()

    Parameters
    Nothing

    Returns
    Number of milliseconds since the program started as a 32bit value (unsigned long)

     

    Last edit: Chris Roper 2019-01-02
  • Chris Roper

    Chris Roper - 2019-01-02

    Why use millis rather than wait ?

    In the first post I showed an example that used millis() as an alternative to the wait x mS format.

    Which raises the obvious question of why use a function that requires a hardware timer 177 words of Flash and 21 bytes of RAM when the wait requires no hardware timers, 55 words of flash and only 1 byte of RAM to do the same thing.

    Well if you were creating a device with the sole purpose of toggling an LED every second then either way would work as they hardly impact the chips resources anyway. But if the device was intended to do other things in addition to flashing an LED then millis becomes the better choice, here's why.

    The wait function is a tight loop that throws away instruction cycles, decrementing a number and looping back until the required time has elapsed. So whilst wait is executing the device can do nothing else.

    Millis on the other hand is a simple comparison test.
    The Start time is stored in LstMs and periodically compared to the current time, if the desired period LEDRate, has been reached or exceeded then the action is taken else the device is free to do other things whilst the milliseconds tick away in the background.

    As an example let's see what happens if we need to Flash 2 LEDs at different rates.

    '
    #define LED0 PORTC.0       ' Define the LED0 Pin
    #define LEDRate0 1000      ' Flash 0 rate in mS
    #define LED1 PORTC.1       ' Define the LED1 Pin
    #define LEDRate1 250       ' Flash 1 rate in mS
    ' Setup
    Dir LED0 Out               ' Make the LED0 Pin an Output
    Dir LED1 Out               ' Make the LED1 Pin an Output
    '
    Dim CurMs as Word          ' declare working variables
    Dim LstMs0, LstMs1 as Word ' declare working variables
    ' Main                     ' This loop runs over and over forever.
    Do
      CurMs = millis()         ' Read current time
    
      if CurMs - LstMs1 >= LEDRate1 then  ' required Time has Elapsed
        LED1 = !LED1                      ' So Toggle state of LED
        LstMs1 = CurMs                    ' And Record Toggle Time
      end if
    
      if CurMs - LstMs0 >= LEDRate0 then  ' required Time has Elapsed
        LED0 = !LED0                      ' So Toggle state of LED
        LstMs0 = CurMs                    ' And Record Toggle Time
      end if
    
    Loop
    

    Run the above code and you will have two LEDs flashing simultaneously and independently of each other.
    The real challenge is to do that with the wait statement, it is nigh on impossible.

     

    Last edit: Chris Roper 2019-01-02
  • Chris Roper

    Chris Roper - 2019-01-02

    Ram use can be an issue on smaller PIC’s and although the last example only used 24 bytes of RAM - 3 more than the single flash - it can still add up. One feature of the GCBASIC implementation of millis() that can’t be reproduced easily in the Arduino Compiler is bit testing. As most of the time flashing LEDs are just an indication of a state or that something is running, accurate timing is rarely that critical. If that is the case we can recreate the Dual flash example with flash rates of 1023 mS and 255 mS and save both Flash and RAM like this:

    '
    #chip 16F690,8            ' Declare the Target Processor and Speed
    #option explicit          ' Require Explicit declaration of Variables
    #include "millis.h"       ' Include the Library
    '
    #define LED0 PORTC.0       ' Define the LED0 Pin
    #define LED1 PORTC.1       ' Define the LED1 Pin
    ' Setup
    Dir LED0 Out               ' Make the LED0 Pin an Output
    Dir LED1 Out               ' Make the LED1 Pin an Output
    '
    ' Main                     ' This loop runs over and over forever.
    Do
      LED0 = millis.10
      LED1 = millis.8
    Loop
    

    RAM use is now down to only 15 Bytes and the difference in the Blink rate is hardly noticeable to the human eye.

    The source code is a lot less readable but if RAM is getting tight it is worth doing, provided you comment the code well.

     

    Last edit: Chris Roper 2019-01-02
  • mmotte

    mmotte - 2019-01-02

    Thanks Chris! Very useful tool.

     
  • stan cartwright

    stan cartwright - 2019-01-02

    Nice to know. Thanks for explaining. Hope it gets documented.

     
  • mkstevo

    mkstevo - 2019-01-03

    Many thanks for this. I really appreciate your hard work.

     
  • Chris Roper

    Chris Roper - 2019-01-03

    Testing has shown that millis fails silently on newer PIC16 devices with the 8/16 Bit Timer0.

    We are working on a solution to support these devices but, in the interim , the attached Version of "millis.h" will issue an error and abort the compile if an unsupported device is detected.

    The First Post attachment has also been updated.

    Cheers
    Chris

     

    Last edit: Chris Roper 2019-01-03
  • Moto Geek

    Moto Geek - 2019-01-04

    Hello Chris, this is excellent. I had used something like this in the past borrowed from some code from this forum a while back. You may have been the one that posted it I believe. It has been working great. I will try this one now as an include.

    I saw this statement in another thread about millis...

    It returns a Long variable value
    'that will return to zero after 49 days of running. If checking
    'for values being greater than millis() in programs intended to
    'run for long periods, remember to watch for it resetting to zero.

    I am trying to wrap my head around what would happen if it resets to zero and if that case would wreak havic of somekind. Can you describe what would happen and can it be handled someway?

    Thanks for your efforts, this is good stuff!

     
  • mkstevo

    mkstevo - 2019-01-04

    Showing my lack of understanding here I'm sure...

    But millis() does not seem to return a zero value on initialising the device.
    On my test for the 16F1825 the initial value of millis() gave values of:
    419,373,823
    419,103,487
    419,365,631

    To give a timing of mS since the device started I'm having to store the initial value of millis() and then deduct this from the current value.

    Partial code follows:

    #Chip 16F1825, 32
    #Option explicit
    
    include "millis.h"
    Dim Initial_Millis As Long
    Dim Current_Millis As Long
    
    Dim CurMs          As Word
    Dim LstMs          As Word
    
      ' turn on the RS232 and terminal port.
      ' Define the USART port
    #define USART_BAUD_RATE 9600
    #define USART_BLOCKING
    #define SerOutPort portC.4 'Pin  6
    
    #Define OutPin     PortC.5 'Pin  5
    
    'Set pin directions
    Dir SerOutPort  Out         'Pin  6
    Dir OutPin      Out         'Pin  5
    
    #Define LEDRate    1000 'mS
    Let Initial_Millis = Millis()
    
    Do
        If Initial_Millis > Millis() Then
            Let Initial_Millis = Millis()
        End If
        Let Current_Millis = Millis() - Initial_Millis
        HSerPrint Current_Millis
    
        CurMs = millis()
        If CurMs - LstMs >= LEDRate Then    'Required Time has Elapsed
            Let OutPin = !OutPin          'So Toggle state of LED
            Let LstMs = CurMs               'And Record Toggle Time
        End If
    Loop
    

    Is this the expected behavior or have I done something daft again?

     
    • Chris Roper

      Chris Roper - 2019-01-04

      This code is redundant

          If Initial_Millis > Millis() Then
              Let Initial_Millis = Millis()
          End If
          Let Current_Millis = Millis() - Initial_Millis
          HSerPrint Current_Millis
      

      I am not even sure what it is attempting to achieve.
      This is the correct code (also copied from your code) for measuring an elapsed period:
      ~~~
      CurMs = millis()
      If CurMs - LstMs >= LEDRate Then ' Required Time has Elapsed
      doTimedEvent() ' Do something
      Let LstMs = CurMs ' And Record Toggle Time
      End If
      ~~~
      The longest period timed in my examples is 1000 mS so I dimensioned CurMs and LstMs as Word to save memory. They should really be dimensioned as Long and if you were counting a period that would not fit in a word then Long is essential.

      Hope that helps clarify.

      Cheers
      Chris

       

      Last edit: Chris Roper 2019-01-04
      • mkstevo

        mkstevo - 2019-01-04

        Ahhh... That code is printing out the value of millis(), since the processor was started. I was thinking I could use millis() as a runtime counter, but as it appears to start at some nominal value (rather than zero), when my program starts I initially store the value of millis() in a long variable and calculate the 'run time' by deducting the initial value, from the value returned by the millis() function.

            If Initial_Millis > Millis() Then 'If the Initial_Millis value is greater than the current value, assume millis() has rolled over.
                Let Initial_Millis = Millis() 'Store the new value of millis() in the Initial_Millis variable
            End If
            Let Current_Millis = Millis() - Initial_Millis 'Calculate the time since the processor started by deducting the initial value from the current value
            HSerPrint Current_Millis 'Print the time since the processor started
        

        For shorter timings (such as your example) the start value of millis() is, as you rightly point out, irrelevant.

        Sorry if I have misunderstood.

         
  • Chris Roper

    Chris Roper - 2019-01-04

    Rollover should not be a problem as the count is stored as an unsigned long value.

    If you need a long period you would also have to store the StartTime as a long and then subtract the StartTime from the current millis and compare it to the required duration in mS.

    As all variables are unsigned the math's will work out as the difference will still be the same.
    That is in theory, one could set up an experiment and let it run for 50 days to see what happens, but mathematically it should work.

    More detailed discussion can be found in the Arduino forums.

    Cheers
    Chris

     
  • mkstevo

    mkstevo - 2019-01-04

    If you need a long period you would also have to store the StartTime as a long and then subtract the StartTime from the current millis and compare it to the required duration in mS.
    As all variables are unsigned the math's will work out as the difference will still be the same.
    That is in theory, one could set up an experiment and let it run for 50 days to see what happens, but mathematically it should work.

    I see what you are saying. So my:

        If Initial_Millis > Millis() Then 'If the Initial_Millis value is greater than the current value, assume millis() has rolled over.
            Let Initial_Millis = Millis() 'Store the new value of millis() in the Initial_Millis variable
        End If
    

    Is irrelevant as both Initial_Millis and millis() are unsigned Long variables.
    That makes sense.

    Thanks for the clarification. And sorry again for not grasping this earlier.

     
  • Chris Roper

    Chris Roper - 2019-01-05

    Nothing wrong with not instantly grasping it, that is part of learning and we all have to do it.

    Infact the debate still rages on the Arduino forums. That is why I opened the thread with:
    "One of the more useful, and misunderstood, functions on the Arduino platform "

    I even had to run through the maths myself before I trusted it.

    Cheers
    Chris

     
  • mkstevo

    mkstevo - 2019-03-12

    I have left my 'test' running for a total of 4,294,967,295 mS. All seemed happy once the rollover ocurred. My program comprised of a button test that uses the millis() function to time how long the button was pressed for, dispaying this, along with the current value of millis() on an LCD display.

    Out of interest to probably no one, I have uploaded a video of the final ten seconds or so of the test as proof of functionality. Nothing went bang, and the program continued to work as expected.

    Many, many thanks to Chris for this function, I will find it extremely useful as I'm sure will many others.

    15 second demonstration video

     

    Last edit: mkstevo 2019-03-12
    • Anobium

      Anobium - 2019-03-12

      Great stuff. Really good to read this!

      We will include millis() in the corr compiler within the next release. Currently (March 19) we are resolving an issue with 18fxK42 chips - when resolved I will update this thread.

       
    • Chris Roper

      Chris Roper - 2019-03-12

      As much as I trust math's it is always comforting to know that the theory worked in practice - especially as I never would have had the patience to run a test for that long :)

      We held off publication because of concerns around compatibility issues on certain devices and, indeed, it proved to be a good stress test in general for both the dev's and the devices.

      We flagged a few issues for Microchip to solve and a few for ourselves, but nothing was broken beyond repair.

      I see Evan replied about the release dates so I have nothing new to add but my thanks.

      Cheers
      Chris

       
  • Moto Geek

    Moto Geek - 2019-03-12

    Nice work on this!!! @mkstevo, thanks for posting your rollover results and nice to see it works perfectly. BTW, how do you like your 4 piece tweezer set??? Kidding...

    millis is a great and extremely useful function to have, thanks for the effort! Glad to see it will be included with the compiler.

     
  • Chris Roper

    Chris Roper - 2019-04-04

    Great News - Change log entry: 615 New Millis - Initial formal release of millis.h

    It is ready for real time (Pun Intended) and will be included in the upcoming release of Great Cow BASIC as a core Library.

    It has been extensively tested on all PIC (10,12,16,18) and AVR families supported by GCBASIC and example files will be included in the release.

    The holdup was caused by uncovering a couple of low level compiler errors which have now been fixed so, whilst Evan and I have been silent for the past few weeks, millis was never dead it was still being used as a debug tool to stress test the compiler.

    It has already proven useful in a couple of projects too so I expect to see more when it is formally released with the New Compiler and the community get to use it too.

    Cheers
    Chris

     

    Last edit: Chris Roper 2019-04-04
  • Alex

    Alex - 2020-02-24

    hello where can i donwload millis library? it is no included in GCB donwload

     

Log in to post a comment.