To remain portable, units of time in this framework are measured in ticks represented by the ecu_tick_t type. The application is responsible for defining the conversion between ticks and explicit units of time.
Usually this is dictated by the resolution of available hardware timers and application requirements. In the example below, a hardware timer that ticks 10 times every 1 millisecond drives the framework:
/* Helper macro made by application. 10 ticks occur every 1ms. */#define MS_TO_TICKS(x_) ((ecu_tick_t)(10U * (x_)))/* Start a 10ms timer. Convert milliseconds to timer ticks to feed into this module. */ecu_tlist_timer_arm(&list,&timer,MS_TO_TICKS(10),ECU_TIMER_TYPE_ONE_SHOT);/* Service all software timers in list. Read hardware timer to get elapsed ticks. */ecu_tlist_service(&list,(ecu_tick_t)get_elapsed_ticks());
Note
There are no guarantees on ecu_tick_t’s size. The framework only mandates that it is unsigned. Currently it is an unsigned int since that best matches the target’s word-size but is subject to change. The maximum value the type can hold will always be ECU_TICK_MAX.
Note however that a timer cannot be a standalone object. It can only be ran by adding it to a timer list, which is an object that manages all timers attached to it.
/* Start timer by adding it to list. It is a periodic timer that expires every 100 ticks. */ecu_tlist_timer_arm(&list,&timer,100,ECU_TIMER_TYPE_PERIODIC);/* The list will service and manage all timers attached to it. */ecu_tlist_service(&list,20);
When a timer expires its callback assigned in ecu_timer_ctor() executes. This condition is checked and executed in the list’s service call, which must be periodically called by the application:
As stated in the previous section, all timers are managed through a list which is represented by the ecu_tlist object. Internally, it is a linked list of timers which are ordered by their expiration dates. When a timer is started, it is sorted and added to the list. For example:
Each ecu_tlist usually contains multiple software timers (ecu_timers) and is meant to be driven by a unique hardware timer. Usually separate lists are created for hardware timers possessing different resolutions. For example one list being driven by a hardware timer with 1 millisecond resolution and another list being driven by a hardware timer with 1 microsecond resolution:
Software timers requiring a specific resolution can then be added to the appropriate list. As expained earlier, this framework uses an arbitrary tick type to keep track of time. Full flexibility is given to the application by allowing the conversion between ticks and units of time to be freely defined for each list. For simplicity, a 1:1 conversion is used in the example below:
structecu_timerfive_ms_timer;structecu_timerten_us_timer;/* Assume timers constructed for conciseness. */ecu_tlist_timer_arm(&timer_list_ms,&five_ms_timer,5,ECU_TIMER_TYPE_ONE_SHOT);ecu_tlist_timer_arm(&timer_list_us,&ten_us_timer,10,ECU_TIMER_TYPE_ONE_SHOT);
As stated earlier, each list orders its timers by expiration dates, which are absolute. In other words, the actual timestamp when each timer expires is stored instead of elapsed time until expiration. The list keeps track of the current time with each service call, so a timer’s expiration date can be calculated using only its period:
Faster list sorting since elapsed times do not have to be calculated. The expiration dates can just be directly compared.
No hardware dependencies. A callback that gets the current time does not have to provided by the user. Elapsed ticks passed as a parameter into the list’s service call is all that is required.
Faster service call. On a high level, the algorithm loops over all timers in the list and checks if any are expired. This method allows this iteration to immediately exit as soon as an unexpired timer has been reached, as showcased in the example below:
Notice how only the timers highlighted in red have to be checked in the service call. The iteration immediately exits once t2 is reached. t2 has not yet expired and all timers after it will have later expiration dates since the list is ordered.
Clean handling of timer overflow. A timer’s expiration date can always be calculated using the same equation explained previously even if an overflow condition occurs. The result is stored in an ecu_tick_t type which is guaranteed to be unsigned and whose maximum value is ECU_TICK_MAX. Therefore the following condition can check if the operation overflows:
timer->expiration=list->current+timer->period;/* Unsigned overflow occurs if resulting value exceeds ECU_TICK_MAX. */if(timer->expiration<list->current){timerwillexpireafterlistcounteroverflows.}
Timers meeting this condition are handled by adding them to a second wraparounds list which is also contained within the ecu_tlist object. The example below showcases this, where ecu_tick_t is uint8_t and ECU_TICK_MAX is 255 for simplicity:
The framework detects overflow occured since the resulting expiration timestamp is less than the list’s current timestamp. Therefore t2 is added to the wraparounds list. The service call then knows an overflow condition occurred since the wraparounds list is no longer empty, and overflow can be handled accordingly.
In the following example a hardware timer is used to service two one-shot timers and a periodic timer through the help of this framework. This is a miniimal example to showcase basic functionality and can easily be expanded for more complex and realistic scenarios:
/*-------------- hardware_timer.h/.c -------------*/staticvolatileecu_tick_ttick_count=0;staticvoidtimer1_isr(void){tick_count++;}ecu_tick_ttimer1_get_ticks(void){ecu_tick_tticks;CRITICAL_SECTION_ENTER();/* I.e disable interrupts. */ticks=tick_count;/* tick_count updated in ISR context! */CRITICAL_SECTION_EXIT();/* I.e re-enable interrupts. */returnticks;}ecu_tick_ttimer1_ms_to_ticks(uint16_tmilliseconds){/* Convert milliseconds to timer1 ticks. In this case 1:1 ratio. */return(ecu_tick_t)milliseconds;}
Timer constructor. Initializes the ecu_timer data structure for use and defines its callback that executes when the timer expires:
Warning
Constructor must be called once on startup before the timer is used. User is responsible for allocating memory since ECU does not use dynamic memory allocation.
staticbooltimer_expired_callback(structecu_timer*me,void*obj){printf("Expired!");returntrue;}structecu_timerme;ecu_timer_disarm(&me);/* ILLEGAL. Must construct before using. */ecu_timer_ctor(&me,&timer_expired_callback,ECU_TIMER_OBJ_UNUSED);ecu_timer_disarm(&me);/* OK. */
The callback can return false if it must be re-executed when the timer is next serviced in ecu_tlist_service(). For example:
An example use case of this feature could be that a queue write operation failed due to it being full, so it must be immediately reattempted on the next service call:
staticbooltimer_expired_callback(structecu_timer*me,void*obj){/* Returns true if successful. False if queue write failed. */returnqueue_write(&timer_expired_event);}
This framework can also handle any edits made to the timer within its expiration callback. These include rearming a one-shot timer, disarming a periodic timer, changing a timer’s settings, etc. For example:
Timer list constructor. Initializes the ecu_tlist data structure for use, which is an object that manages all software timers attached to it. See Timer List Representation for more details.
Warning
Constructor must be called once on startup before the list is used. User is responsible for allocating memory since ECU does not use dynamic memory allocation.
structecu_tlisttlist;/* User must allocate memory before constructor. */ecu_tlist_service(&tlist,10);/* ILLEGAL. Must construct before using. */ecu_tlist_ctor(&tlist);ecu_tlist_service(&tlist,10);/* OK. */
Services all timers currently in the list. Servicing involves expiring appropriate timers, handling timer rearming, etc. This must be periodically called by the application at least once every ECU_TICK_MAX ticks. However the accuracy of the timers is proportional to how often this function is called. See Timer List Representation and Example Section for more details.
structecu_tlistlist;structecu_timert1;ecu_tlist_timer_arm(&list,&t1,10,....);/* Timer started that will expire in 10 ticks. */ecu_tlist_service(&list,5);/* 5 ticks has elapsed so t1 will now expire in 5 ticks. */ecu_tlist_timer_rearm(&list,&t1);/* t1 restarted so it now expires in 10 ticks. */