Pages: [1] 2 3   Go Down
Author Topic: Arduino timer scheduler library  (Read 9368 times)
0 Members and 1 Guest are viewing this topic.
London
Offline Offline
Tesla Member
***
Karma: 10
Posts: 6255
Have fun!
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Here is the first cut of the timerAlarm library. I will post it here for people to play with and comments while I finish testing and writing up.

The library is the suggested subset of scheduling functionality mentioned in this thread http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1217833080


The library provides for co-operative scheduling of time based tasks with periods from one second to many years. Alarms can be created that repeat at a given interval or that trigger only once. Alarms once created can be enabled, disabled or rescheduled as required.

The number of Alarms concurrent alarms is fixed at library compile time, the default is 6 alarms. The number can be changed by altering a define in the header file, each alarm takes 12 bytes of RAM.

Alarms are created by calling one of the following two methods:
   AlarmId1 = dtAlarms.createRepeating( PeriodInSeconds );  // fires periodically
   AlarmId2 = dtAlarms.createOneshot( PeriodInSeconds );    // fires once

There are convenience macros to set periods of minutes, hours, days and weeks

The scheduling algorithm is a simple round robin, priority is given to alarms based on the order the create methods are called,  first created alarms have the highest priority. Any timers that mature while a callback is executing while wait for the callback to finish.

The library requires the timing mechanism from the DateTime library which is available here:
http://www.arduino.cc/playground/Code/DateTime


I intend to post the finished code for the Alarm library in the playground along with a write-up but welcome a discussion on the functionality here.

example sketch
Code:
#include <DateTime.h>
#include <TimerAlarms.h>

AlarmID_t AlarmA, AlarmB, AlarmC, AlarmD;

void onAlarm(AlarmID_t Sender){
// alarm callback function
 Serial.print("cume time ");  
 Serial.print(DateTime.now());  // print elapsed seconds since sketch started runnin
 
 Serial.print(": ");
  if( Sender == AlarmA) {
      Serial.println("15 seconds Alarm");        
  }
  else  if( Sender == AlarmB){
      Serial.println("90 second Alarm");    
  }
  else  if( Sender == AlarmC){
      Serial.print("One Shot alarm, elapsed period was ");    
      time_t alarmValue  = dtAlarms.getValue(Sender);
      Serial.println(alarmValue,DEC);
  }
  else  if( Sender == AlarmD){
      Serial.print("re-trigged One Shot alarm, elapsed period was ");    
      time_t alarmValue  = dtAlarms.getValue(Sender);
      Serial.println(alarmValue,DEC);
      dtAlarms.setValue(Sender, alarmValue + 2); //re-enable with a new value two seconds longer
    }
}

void setup(){
   pinMode(13,OUTPUT);
   Serial.begin(19200);

   AlarmA = dtAlarms.createRepeating( 15 );  // alarm every 15 seconds
   AlarmB = dtAlarms.createRepeating( AlarmHMS(0,1,30) );  // alarm every 1 minute 30 seconds
   AlarmC = dtAlarms.createOneshot( 10 );  // one shot alarm in 10 seconds
   AlarmD = dtAlarms.createOneshot( 12 );  // one shot alarm that will be manually retriggered

   Serial.println("started");
 }

void loop(){
  dtAlarms.waitUntilThisSecond(0); //  this code blocks waiting for start of the next minute, background alarms are still serviced
  Serial.println("turning LED on");
  digitalWrite(13, HIGH);
  dtAlarms.delay(2000);  // note we call the alarm delay to service the background alarms
  Serial.println("turning LED off");
  digitalWrite(13,LOW);  

}
Logged

London
Offline Offline
Tesla Member
***
Karma: 10
Posts: 6255
Have fun!
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Library Header file, save as

TimerAlarms.h  

Code:
/*
  TimerAlarms.h - Arduino Timer Alarm library

  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  

*/

#ifndef TimerAlarms_h
#define TimerAlarms_h

#include <inttypes.h>
#include <wiring.h> // for boolean

#include "DateTime.h"

typedef enum {
     dtMillisecond, dtSecond, dtMinute, dtHour, dtDay //,clkMonth,clkYear
 } dtUnits_t;

typedef struct  {
      uint8_t isEnabled              :1 ; // alarm will only fire if true
      uint8_t isOneShot              :1 ; // alarm will fire once only if true
      uint8_t isAlarm                :1 ; // alarm based on absolute time, not used this version
   }
     AlarmMode_t   ;

typedef uint8_t AlarmID_t;
#define INVALID_ALARM_ID 255
#define NBR_ALARMS 6 // this sets the number of alarms, each alarm uses 12 bytes of RAM

class AlarmClass;  // forward reference
typedef void (*OnTick_t)(AlarmID_t);  // alarm callback function typedef
extern void onAlarm(AlarmID_t);  // User supplied callback for all alarms

// class defining an alarm instance, only used by dtAlarmsClass
class AlarmClass
{      
private:
      friend class dtAlarmsClass;
      void updateNextTrigger();
      time_t value;
      time_t nextTrigger;
      AlarmID_t ID;           // unique instance id
      AlarmMode_t Mode;
      AlarmClass();
public:
};

// class containing the collection of alarms
class dtAlarmsClass
{
private:
   AlarmClass Alarm[NBR_ALARMS];
   boolean isServicing;
   AlarmID_t nextID;
   AlarmID_t create( time_t value, boolean isOneShot, boolean isEnabled );
   void serviceAlarms();

public:
      dtAlarmsClass();
      void delay(unsigned long ms);
      uint8_t getDigitsNow( dtUnits_t Units);  // returns the current digit value for the given time unit
      void waitForDigits( uint8_t Digits, dtUnits_t Units);
      void waitForRollover(dtUnits_t Units);
    AlarmID_t createOneshot( time_t value, boolean isEnabled = true);
    AlarmID_t createRepeating( time_t value, boolean isEnabled = true);
      void setValue(AlarmID_t ID, time_t value);
      time_t getValue(AlarmID_t ID);
      void enable(AlarmID_t ID);
      void disable(AlarmID_t ID);

};

extern dtAlarmsClass dtAlarms;  // make an instance for the user

/*==============================================================================
 * MACROS
 *============================================================================*/

/* public */
#define waitUntilThisSecond(_val_) waitForDigits( _val_, dtSecond)
#define waitUntilThisMinute(_val_) waitForDigits( _val_, dtMinute)
#define waitUntilThisHour(_val_)   waitForDigits( _val_, dtHour)
#define waitUntilThisDay(_val_)    waitForDigits( _val_, dtDay)
#define waitMinuteRollover() waitForRollover(dtSecond)
#define waitHourRollover()   waitForRollover(dtMinute)
#define waitDayRollover()    waitForRollover(dtHour)

#define AlarmHMS(_hr_, _min_, _sec_) (_hr_ * SECS_PER_HOUR + _min_ * SECS_PER_MIN + _sec_)

#endif

Logged

London
Offline Offline
Tesla Member
***
Karma: 10
Posts: 6255
Have fun!
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Library source file, save as

TimerAlarms.cpp

Code:

/*
  TimerAlarms.cpp - Arduino Timer Alarm library
  Copyright (c) Michael Margolis.  All right reserved.

  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*/

extern "C" {
#include <string.h> // for memset
}

#include "DateTime.h"
#include "TimerAlarms.h"

//**************************************************************
//* Alarm Class Constructor

AlarmClass::AlarmClass(){
  Mode.isAlarm =  Mode.isEnabled = Mode.isOneShot = 0;
  value = nextTrigger = 0;  
}

//**************************************************************
//* Private Methods

void AlarmClass::updateNextTrigger(){
  if( (value != 0) && (Mode.isEnabled != 0) ){
    time_t time = DateTime.now();
    nextTrigger = time + value;  // add the value to previous time (this ensures delay always at least Value seconds)    
  }
  else {
    Mode.isEnabled = 0;  // Disable if the value is 0
  }
}

//**************************************************************
//* Date Time Alarms Public Methods

dtAlarmsClass::dtAlarmsClass(){
  nextID = 0;
  isServicing = false;
}

AlarmID_t dtAlarmsClass::createOneshot( time_t value, boolean isEnabled){
  return create( value, true, isEnabled );
}

AlarmID_t dtAlarmsClass::createRepeating( time_t value, boolean isEnabled){
   return create( value, false, isEnabled );
}

void dtAlarmsClass::enable(AlarmID_t ID){
  if(ID < NBR_ALARMS){
    Alarm[ID].Mode.isEnabled = (Alarm[ID].value != 0) ;  // only enable if value is non zero
    Alarm[ID].updateNextTrigger(); // trigger is updated whenever  this is called, even if already enabled      
  }
}

void dtAlarmsClass::disable(AlarmID_t ID)
{
  if(ID < NBR_ALARMS)
    Alarm[ID].Mode.isEnabled = false;
}

void dtAlarmsClass::setValue(AlarmID_t ID, time_t value){
  if(ID < NBR_ALARMS){
    Alarm[ID].value = value;
    enable(ID);
  }
}

time_t dtAlarmsClass::getValue(AlarmID_t ID){
  if(ID < NBR_ALARMS)
    return Alarm[ID].value;
  else
    return 0;
}

// following functions are not Alarm ID specific.
void dtAlarmsClass::delay(unsigned long ms)
{
  unsigned long endtime = millis() + ms;
  boolean Overflow = endtime < millis();
  if( Overflow){
    while( millis() > endtime)
      serviceAlarms();
  }
  while( millis() < endtime)
    serviceAlarms();
}

void dtAlarmsClass::waitForDigits( uint8_t Digits, dtUnits_t Units){
  while(Digits != getDigitsNow(Units) )  {
    serviceAlarms();
  }
}

void dtAlarmsClass::waitForRollover( dtUnits_t Units){
  while(getDigitsNow(Units) == 0  ) // if its just rolled over than wait for another rollover                                  
    serviceAlarms();
  waitForDigits(0, Units);
}

uint8_t dtAlarmsClass::getDigitsNow( dtUnits_t Units){
  time_t time = DateTime.now();
  if(Units == dtSecond) return numberOfSeconds(time);
  if(Units == dtMinute) return numberOfMinutes(time);
  if(Units == dtHour) return numberOfHours(time);
  if(Units == dtDay) return dayOfWeek(time);
  return 255;  // This should never happen
}

//***********************************************************
//* Private Methods

void dtAlarmsClass::serviceAlarms(){
  if(! isServicing)
  {
    isServicing = true;
    for(uint8_t i = 0; i < NBR_ALARMS; i++)
    {
      if( Alarm[i].Mode.isEnabled && (DateTime.now() >= Alarm[i].nextTrigger)  )
      {
              if( Alarm[i].Mode.isOneShot == 1 ){
                 Alarm[i].Mode.isEnabled = 0;  // this prevents the oneshot from re-triggering unless its explicitly re-armed
              }
        Alarm[i].updateNextTrigger();
        if( onAlarm != NULL) {                        
                onAlarm(i);
        }
      }
    }
    isServicing = false;
  }
}

AlarmID_t dtAlarmsClass::create( time_t value, boolean isOneShot, boolean isEnabled ){  // returns true if has been registerd ok
  AlarmID_t id = INVALID_ALARM_ID;
  if( nextID < NBR_ALARMS){
      id = nextID;
    Alarm[id].Mode.isOneShot = isOneShot;
      Alarm[id].value = value;
      isEnabled ?  enable(id) : disable(id);  
    nextID++;  
  }
  return id;
}

// make one instance for the user to use
dtAlarmsClass dtAlarms = dtAlarmsClass() ;
Logged

Boulder, CO
Offline Offline
Newbie
*
Karma: 0
Posts: 43
I want to be a really useful engine!
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Hmm, why does the Alarm class store its own ID?  I do not see it being used anywhere, perhaps it should be eliminated to save space?  It seems like this class is obfuscated from the users so they would not need to look it up either, right?



A Naming question, why "alarms"?  Just curious, is this standard terminology for this type of thing?  I think of Alarms as something that gets triggered when something bad happens.  Would "Events" or 'DtEvents" not be better? smiley



It appears that you have gone through great pains to save space, very nice. smiley  I assume that is one reason why you use indexes instead of a more "traditional" object oriented design?  I.E. you pass an index to your onAlarm() function instead of a pointer to the Alarm object, this saves a byte.  As a user, I would much prefer to have a more object oriented approach, so that instead of doing something like:

  dtAlarms.getValue(Sender);

I could do something like:

  alarm->getValue();

But this just leads me to the next "user interface" request, which would be to simply have a separate onAlarm() function for each Alarm.  This now trades 1 byte of temporary (stack) RAM,  your index, for 2 bytes of permanent RAM per alarm for a function pointer (6*2 =12 bytes).  I can see why you probably did not take this approach.  I wonder however if there aren't some clever tricks with constants that could be played to still get the separate function per Alarm while not using an extra 12 bytes of RAM.  Is this something that would be desirable?

What about allowing the user to create a lookup table of function pointers that could be declared constant?  Would these perhaps get optimized out of RAM by the compiler?  If so, would this be cleaner from a user interface standpoint?
Logged

London
Offline Offline
Tesla Member
***
Karma: 10
Posts: 6255
Have fun!
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Martin,

Great feedback, here is a quick response with a more detailed post to follow tomorrow when I have some more time.

This alarm code is a very much simplified version of a more sophisticated library I developed some time ago, and some and the names (and it seems a redundant variable or two ) reflect the more complex version.

The original library allowed the user to instantiate alarm (or event) instances in the sketch as required, the ID was used to register the creation of an instance with the alarm collection class. This allowed access to the public methods and properties of the individual class but did require the use of the indirection syntax which did not seem a common notation in popular arduino code.

The current functionality has been pared down to the minimum in order to keep it the usage as simple as possible.  

In the earlier library I actually used the terms 'timers' for event triggers that are based on a expiration of a given time period (this is the functionality supported in the posted version) and 'alarms' for events triggered at a specific time of day (like an alarm clock). I am certainly open to suggestions on the most intuitive term for the duration based triggers in the current code.

The library did allow each alarm to have its own callback or share callbacks, you can see examples of both in the code I posted in another thread a few days ago. But mellis's comment reinforced my view that simple is best so I removed that functionality.

Perhaps I should post both versions.  

In the mean time I would certainly want to hear suggestions for naming of any or all of the variables and methods in the code and suggestions an finding the right balance between simplicity and functionality in the version above.
« Last Edit: August 05, 2008, 03:25:59 pm by mem » Logged

Forum Administrator
Cambridge, MA
Offline Offline
Faraday Member
*****
Karma: 11
Posts: 3538
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

I could imagine an even simpler API.  Something along the lines of:

void setup()
{
  Alarm.once(500, foo); // call foo() in half a second
  Alarm.repeat(1000, blink); // call blink() every second
}

void loop()
{
  Alarm.delay(1000);
}

void foo() {}
void blink() {
}

That seems like it would cover most needs.  Or am I missing some common use cases?
Logged

Boulder, CO
Offline Offline
Newbie
*
Karma: 0
Posts: 43
I want to be a really useful engine!
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

1) Well, how do you change alarms?  For repeating alarms it is likely that it will need to be turned on/off at various times or the rate may need to be changed.

2) Do you envision that users might want to call each of those functions more than once?  If so, then either some default amount of memory needs to be assigned (evil), or malloc() needs to be used, or the user needs to pass in some Alarm object that they have allocated themselves. Do you agree?  Passing in an Alarm object solves issue number 1 also.
Logged

0
Offline Offline
Faraday Member
**
Karma: 8
Posts: 2526
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Quote
how do you change alarms?
Well, since I'm not writing the code:

Code:
Alarm.disable(foo);
Alarm.enable(foo);
Alarm.delete(foo);
 Of course, the first two could be accomplished by just using the delete and once/repeat calls, but adding enable and disable seems neater to me.

As for the table (we're setting up a "timer" signal vector, analogous to the hardware's interrupt vector), I don't have a glib response for that one, but it shouldn't be too difficult.  Worst case, use some default number and allow the user to override it at compile time.

-j

Logged

Boulder, CO
Offline Offline
Newbie
*
Karma: 0
Posts: 43
I want to be a really useful engine!
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Quote
Alarm.disable(foo);
Alarm.enable(foo);
Alarm.delete(foo);

Ahh, but what happens when the user tries this:

  Alarm.once(500, foo); // call foo() in half a second
  Alarm.repeat(1000, foo); // call the same foo() every second

If you let me register a function, is it not reasonable to assume that I might want to register that function with more than one alarm?  Now which one am I enabling/disabling?

Quote
As for the table (we're setting up a "timer" signal vector, analogous to the hardware's interrupt vector), I don't have a glib response for that one, but it shouldn't be too difficult.  Worst case, use some default number and allow the user to override it at compile time.

Unfortunately, you have to do a lot more than setup a table which would only be two bytes per Alarm.  You also have to store information about the date/time of when the alarm should be triggered.  If you look at the implementation provided at the beginning of this thread, it works out to about ~12 bytes per Alarm.  It is probably not reasonable with only 1024 bytes of RAM to preallocate enough space for several of these in advance.  Making this a compile time option eliminates it from ever being becoming a core library right?  The remaining alternatives I believe are:

1) making the user allocate them (more complicated for users, but more efficient)
 or
2) using malloc (easier for users, but uses more RAM).
Logged

0
Offline Offline
Faraday Member
**
Karma: 8
Posts: 2526
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Quote
If you let me register a function, is it not reasonable to assume that I might want to register that function with more than one alarm?
Yes.

But, if the library doesn't allow for it, I could easily write code that looks like
Code:
setup()
{
   Alarm.once(500,foo);
   Alarm.repeat(1000, bar);
}

void bar()
{
   foo();
}
...

Quote
Now which one am I enabling/disabling?
If you allow the function to be used with multiple alarms, this introduces the need for an alarm identifier beyond using the function's address.

Quote
Unfortunately, you have to do a lot more than setup a table which would only be two bytes per Alarm.  You also have to store information about the date/time of when the alarm should be triggered.
That's just a bigger table.

Quote
Making this a compile time option eliminates it from ever being becoming a core library right?
Why?
Code:
#define MAX_ALARMS 17
#include <Alarm.h>
...

I'm not necessarily against making the user allocate them, but the target Arduino audience does not consist exclusively of your typical microcontroller hacker, and that is a huge part of the success of the project, IMO.

-j

Logged

0
Offline Offline
Faraday Member
**
Karma: 8
Posts: 2526
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

One thing that does worry me is the apparent reentrant nature of the monolithic signal handler (onAlarm())in the first post in this thread.  If it is reentrant, you've got to be careful (e.g. static variables) in the way you write the code.  I'm not sure that reentrant code is appropriate for the Arduino, just be cause of the complexity of the concept and the target audience of the Arduino.

-j

Logged

Boulder, CO
Offline Offline
Newbie
*
Karma: 0
Posts: 43
I want to be a really useful engine!
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

This handler is never called in a reentrant fashion since it is only triggered by the main program, not interrupt routines.  It requires a voluntary call to dtAlarm.delay() or other functions which check it for Alarm triggers.  Care for static variables should be no different than with any other Arduino function used.
Logged

0
Offline Offline
Faraday Member
**
Karma: 8
Posts: 2526
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Thanks.  I wasn't clear if the core trigger was a timer ISR or something else, and am currently too busy/lazy to read the code and find out.

-j

Logged

Boulder, CO
Offline Offline
Newbie
*
Karma: 0
Posts: 43
I want to be a really useful engine!
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Quote
setup()
{
   Alarm.once(500,foo);
   Alarm.repeat(1000, bar);
}

void bar()
{
   foo();
}

Uhh, don't be so sure that the compiler would not optimize away bar() making the pointer to &bar() == &foo().  Now you still can't distinguish between the two. smiley-sad

Quote
That's just a bigger table.
In this case, a much bigger table! smiley  It is worth spending some time analyzing mem's code.  While I may not have made the same design decisions as him, it did allow me to form the opinion that while on the surface this task/problem seems simple/easy to solve, to do it in a way that will actually be useful probably means making things a little more complicated than first desired and making certain tradeoffs.

Quote
#define MAX_ALARMS 17
#include <Alarm.h>
...
I would assume that the Alarm library code would need to be compiled with this MAX_ALARMS macro set to the user's custom value (this is not just setting a variable somewhere).  If so, how could that be delivered as a precompiled library in the core then?
Logged

0
Offline Offline
Faraday Member
**
Karma: 8
Posts: 2526
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Quote
how could that be delivered as a precompiled library in the core then?
It's my understanding that the libraries are not pre-compiled, but that the source gets copied to the applet subdirectory of your sketch directory, and gets compiled as part of your sketch.  I could be wrong about that, too, though...

-j

Logged

Pages: [1] 2 3   Go Up
Jump to: