You are viewing an older revision! See the latest version

Event Driven framework

An introduction to the EventFramework

Import libraryEventFramework

EventFramework library allows the creation of an event-driven infrastructure in which small "threads" can handle events in a multithreaded execution context. The EventFramework can be configured to act as a cooperative or a fully-preemptive kernel with fixed-priority scheduling. Furthermore, this kernel matches run-to-completion semantics, and hence a single-stack configuration is enough to keep running this multithreaded execution environment. As running threads shares global stack, a huge quantity of RAM is saved in contrast with traditional RTOSes.

The EventFramework library has been designed having in mind those developers who don’t really need a full traditional RTOS but still need a bit of underlying management to implement a lightweight multithreaded application.

The most important feature against other traditional RTOSes is the way in which memory is optimized. While traditional RTOSes need to keep a stack region per task, in this case, the EventFramework uses processor’s global stack to save threads’ context when a higher priority thread is ready for execution.

/media/uploads/raulMrello/rtos_vs_rtc_kernel.png

In a common application, suppose a GPS-enabled device with a file system in an external SD memory and a communication infrastructure like TCP/IP or similar; can count with around 8-12 tasks.

In a traditional RTOS, this is translated in a memory allocation around 10KB (1KB per task) for RTOS context saving. In a 20Kbytes RAM microcontroller this implies more than 50% of RAM memory, limiting the user’s application to other 50% (around 10Kbytes) for variables, buffers, memory pools, etc… Nevertheless, using a single-stack-based EventFramework like this, we don’t need individual stacks for each thread. You can adjust processor’s global stack to meet its worst case and use the rest of memory for user’s variables, saving a huge quantity of RAM.

Using a single stack for all threads, implies that high priority threads will block the execution of low priority ones if they do not finish their execution. For this reason, threads must be executed in an event-driven manner.

By default, threads are in a “Sleep” state waiting for some kind of event. Once the event is raised, the EventFramework scheduler activates that thread to process the event. Once processed, the thread is deactivated to its default “Sleep” state again, allowing the execution of other pending low priority threads.

/media/uploads/raulMrello/rtc_thread_states.png

EventFramework Priority scheme

In order to carry out multithreading, this EventFramework has a priority scheme inherent. In contrast with traditional RTOSes in which tasks use to have fixed priorities, in this case a dual priority scheme has been implemented:

  • Event priority: each event has a priority in the range 0 - 65535 where 0 is the highest and 65535 the lowest.
  • Thread priority: each thread (also known as EventHandler) also has a priority in the same range. On the other hand, several threads can be registered as listeners of the same event, and hence a scheduling policy must be defined. In a first stage, during the scheduling mechanism, the EventFramework scheduler will evaluate events priorities. Highest priority event will be dispatched first. Once selected the highest priority event, its internal listener list is evaluated and the highest priority thread is invoked to handle the event.

/media/uploads/raulMrello/dispatching_order.png

EventFramework Scheduling mechanism

The EventFramework will schedule pending events in a similar way than traditional RTOSes:

  • Cooperative scheduling: An active low priority event will be handled by all its listeners and then a high priority pending event will be handled. There is no preemption.

/media/uploads/raulMrello/cooperative.png

  • Fully-preemptive scheduling: An active low priority event handling process will be blocked by a high priority event arrival. Once finished the handling process of the high priority event (by all its installed listeners), the low priority event will be resumed to finish its handling.

/media/uploads/raulMrello/preemptive.png

Events

Events are the inter-process communication mechanism of the EventFramework. They signal some software/hardware situation and also can attach data to be processed accordingly with their event type.

Its internal structure is formed by these properties:

  • Priority
  • Attached data, to be processed by its listeners (handlers)
  • An EventHandler list (list of listeners that must be invoked to handle the event)
  • Base reference (registered event reference matching with its priority).

/media/uploads/raulMrello/event.png

The EventFramework manages two event queues:

  • Registered event queue: is a queue formed by all the events that the EventFramework can manage. Each event is added to this queue according with its priority, through the interface EventFramework::AddEvent(). The highest priority event will be the first event of the queue and the lowest priority event will be the last one.
  • Published event queue: when a software entity raises an event, it must “Publish” it to the framework (through EventFramework::PublishEvent), and after scheduling, its listeners will be invoked to handle it. All published events are queued according with their priority. Highest first and lowest last. Each published event must match with one of the registered events. This matching mechanism is managed by a private event property named “base”. So a published event will always have a registered event base reference accordingly with its priority. Next example shows a new event published based on Event2 type.

/media/uploads/raulMrello/pending_event_list.png

Interrupt Service Issues

When fully-preemptive scheduling is selected for multithreading, there is an important issue to have in mind: interrupt nesting. Most traditional RTOSes don’t allow post messages from ISR context or inherits some mechanism to allow it without user’s intervention.

In this case, when an event is published from ISR context it is necessary to control the interrupt nesting level. For such purpose, a couple of interfaces have been added:

  • EventFramework::SaveContext, this must be the first instruction of the ISR routine. It increments the interrupt nesting level of the framework. Scheduling is forbidden if there is a nesting level higher than 0.
  • EventFramework::RestoreContext, this must be the last instruction of the ISR routine. It decrements the interrupt nesting level of the framework. In this case, exiting from ISR context could imply the execution of the scheduler if a new event has been published while ISR context has been executed.

/media/uploads/raulMrello/isr.png

Next example shows how preemption is applied by the scheduler. A low priority event (ev2) is being handled by one of its installed listeners. In the middle of the process, ISR1 is raised. While ISR1 is serviced, a higher priority interrupt ISR2 is raised and serviced. Again a higher priority interrupt ISR3 is raised and serviced. Along ISR3 service, (ev1) event is published but thread preemption cannot be executed with a nesting level higher than 0. So ISR3 is exited, then ISR2 is completed and then ISR1.

Once ISR1 finishes, nesting level comes back to 0 and the scheduler is invoked, causing thread preemption. Now event (ev1) is handled by all of its installed listeners. Once handled, (ev2) handler operation is resumed.

/media/uploads/raulMrello/preemption.png

Tips and tricks

Using this framework is quite simple. You only have in mind next tips:

  • Assign priorities as you prefer. But be careful you will obtain different results depending on the selection you do.
  • Keep event handling routines as short as possible to keep the system highly reactive. Have in mind that only one event can be processed at a time.
  • Fully-preemptive scheduling consumes much more stack than cooperative. It depends on the number of events and eventhandlers. Have in mind that low level handlers can be stacked to service high priority ones. Nevertheless, preemptive scheduling ensures a high reactive response in contrast with cooperative.
  • You can use your preferred middleware above the EventFramework. For example a State Machine processor to develop your application as UML compliant state charts, or other inter-process mechanism as mutexes, semaphores, timers and more.

Example 1

Next example shows a sketch of a requirement list for a new project, in particular a GPS-enabled device. It must meet these requirements:

  • It must receive RMB nmea sentences from a GPS modem (uart interface).
  • NMEA sentences must be decoded and printed in a LCD display (i2c interface).
  • Users can setup several configuration issues through a keyboard or remotely with a mobile APP via Bluetooth. Configuration changes must be displayed accordingly.
  • GPS data and user configuration must be stored in a FAT filesystem mounted on a SD flash (spi interface).
  • It can be connected to a host PC as a USB mass storage device and filesystem could be accessed via USB (usb device interface).
  • The device is cost critical, it must be a high volume mass production device. Selected processor is Cortex-M3 core with 24Kbytes RAM and 128Kbytes Flash.
  • Underlying RTOS is not an option here: few RAM resources, task stacks limits communication i/o buffers for USB and Bluetooth.

According with these requirements, a preliminary diagram block is sketched:

/media/uploads/raulMrello/gps_device.png

In order to manage each peripheral individually, a control subsystem is planned for each device. A middleware subsystem will manage communication with remote devices, abstracting the type of peripheral used. Also a user's application manager and a system integrity manager are planned. In total, there are 8 EventHandler threads which will process events in a multithreaded environment.

/media/uploads/raulMrello/gps_system.png

Example 2

This example shows a functional example tested on mbed NXP LCP1768 board.

There are several actors, which are:

Simulated Hardware timer

This actor will emulates the execution of a hardware timer triggering interrupts at a periodical pace. Those interrupts will be serviced by ISR_Timer routine, which will publish two kinds of events:

  • tick1Evt: is published each time the ISR is invoked.
  • tick16Evt: is published each 16 times the ISR is invoked.

Handler2

Is an EventHandler listening tick16Evt events. It'll print a log message and pusblish (periodically) a new event tick32Evt after 2 consecutive executions.

Handler1

Is an EventHandler listening both tick1Evt and tick32Evt events. It will print a log message to debug its execution.

Handler0

Is an EventHandler listening tick32Evt . It will print a log to debug its execution.

Events

As listed above, three different events will be registered into the framework with this priority order (from highest to lowest): tick32Evt > tick16Evt > tick1Evt.

So, executing the example with COOPERATIVE or PREEMPTIVE scheduling will give different results. Graphically, we have this situation:

/media/uploads/raulMrello/example2_-_main.png

example2-main.cpp

/** EventFramework Example Program
 *
 */
#include "mbed.h"
#include "EventFramework/EventFramework.h"

// Enable this key to debug with mbed board.
#define EVF_DEBUG
#ifdef EVF_DEBUG
Serial pc(USBTX, USBRX); // for logging debug messages
#endif


/** Step 1: EventFramework creation. 
 *
 * Scheduling mechanism can be selected to see different results. It accepts:
 * EventFramework::SCHED_VALUE_COOPERATIVE
 * EventFramework::SCHED_VALUE_PREEMPTIVE
 *
 */
EventFramework kernel(EventFramework::SCHED_VALUE_PREEMPTIVE);


/** Step 2: Event creation
 *
 *  In this case 3 events are created:
 *  tick1Evt with priority 2, will be raised each time ISR_Timer is executed.
 *  tick16Evt with priority 1, will be raised each 16 times ISR_Timer is executed.
 *  tick32Evt with priority 0, will be raised after 2 consecutive Handler2 invocations.
 *
 *  Highest event priority is 0, lowest event priority is 2.
 */
Event tick1Evt(2);
Event tick16Evt(1);
Event tick32Evt(0);


/** Step 3: EventHandler creation.
 *
 *  EvenHandlers can be created in two different ways:
 *  a) Declared as EventHandler instances: In this case they can be declared with up to three
 *     parameters: priority, event dispatching routine, object reference notified about the event
 *     processing. If no dispatching routine is specified, it must be attached later through the 
 *     [EventHandler::Attach] interface. In this example, [Handler0] and [Handler1] has been 
 *     declared in this way.
 *  b) Derived class from EventHandler: A new derived class can be implemented. In this case
 *     a private method should carry out the event dispatching process, and the [EventHandler::Attach]
 *     interface is invoked inside its constructor. In this example [Handler2] has been implemented in
 *     this way.
 *
 *  On the other hand, EventHandler priorities are useful when several handlers are subscribed to the 
 *  same event. In that case the handler with highest priority will be invoked first. In this example
 *  we decided next situation:
 *  a) Handler0, priority 1 (medium)
 *  b) Handler1, priority 0 (high)
 *  c) Handler2, priority 0 (high)
 *
 *  So, when [tick32Evt] event will be published, [Handler1] will be invoked prior to [Handler0].
 */
EventHandler Handler0 (1, NULL, NULL);
EventHandler Handler1 (0); // by default 2� and 3� params are set to NULL.

class Handler2Class : public EventHandler{
    public:
    Handler2Class(uint16_t prio, void* data=NULL) : EventHandler(prio, NULL, data){
        Attach(&DispatchFromHandler2);
    }

    private:
    /** This routine will process [tick16Evt] events. It will keep an internal variable
     *  named [twice] which will be incremented on each invocation. On each invocation,
     *  it will print a logging message. After two invocations it will publish a [tick32Evt]
     *  event, with [twice] value attached to the event.
     *  <static> must be qualified to avoid ISO C++ error.
     */
    static uint32_t DispatchFromHandler2(void* me, void* args){
        static uint32_t twice = 0;
        twice++;
        #ifdef EVF_DEBUG
        pc.printf("                                            processing\r\n");
        pc.printf("                                             tick16Evt\r\n");
        pc.printf("                                                ...\r\n");
        #endif
        if((twice & 1) == 0){
            #ifdef EVF_DEBUG
            pc.printf("                                              Publish\r\n");
            pc.printf("                                         <---tick32Evt\r\n");
            pc.printf("                            <----------------\r\n");
            #endif
            // Event is published here!!!
            kernel.PublishEvent(&tick32Evt, (void*)twice);
        }
        #ifdef EVF_DEBUG
        pc.printf("                                               ...\r\n");
        pc.printf("                                               end\r\n");
        #endif
        return 0;
    }
};
Handler2Class Handler2 (0);


/** Step 4: User's App dependent declarations
 *
 *  At this point, user's declarations prior [main] routine are required.
 *  In this case two event dispatching routines are declared: one for [Handler0] and other
 *  for [Handler1]. Remember that [Handler2] provides its own dispatching routine. All Event
 *  dispatching routines must follow this typedef:
 *  typedef uint32_t EventDispatchingRoutine(void* me, void* args);
 *
 *  Also, a routine named ISR_Timer is declared. This routine will emulate the behaviour
 *  of a hardware timer interrupt service routine that raises Events.
 *
 */
EventDispatchingRoutine DispatchFromHandler0;
EventDispatchingRoutine DispatchFromHandler1;
void ISR_Timer(void);


/** Step 5: Application startup
 *
 *  In a first stage, the EventFramework is initialized registering events and EventHandlers 
 *  previously created. 
 *  In a second stage an infinite loop, keep executing the EventFramework scheduler, who will be
 *  constantly evaluating which is the highest prioritized event to be dispatched.
 *
 */
int main() {

    /// a) register all the events previously created into the framework
    kernel.AddEvent(&tick1Evt);
    kernel.AddEvent(&tick16Evt);
    kernel.AddEvent(&tick32Evt);

    /// b) attach dispatching routines to EventHandlers and register these handlers to listen specific events.
    Handler0.Attach(&DispatchFromHandler0);
    Handler1.Attach(&DispatchFromHandler1);
    kernel.AddEventListener(&tick1Evt,  &Handler1);
    kernel.AddEventListener(&tick16Evt, &Handler2);
    kernel.AddEventListener(&tick32Evt, &Handler0);
    kernel.AddEventListener(&tick32Evt, &Handler1);

    /// c) start user's application, in this case a counter (as basetime for simulated timer) is started
    uint8_t counter = 0;
    #ifdef EVF_DEBUG
    pc.printf("Starts execution!!\r\n");
    #endif

    /// d) keep EventFramework scheduler running forever
    while(1) {
        kernel.Schedule();

        /// Optional: in this case a hardware timer simulation is included to generate events at a periodic pace
        if(++counter == 0){
            ISR_Timer();
        }
    }
    #ifdef EVF_DEBUG
    pc.printf("If something goes wrong: Ends execution!!\r\n");
    #endif
}


/** User's application code: Hardware timer emulation
 *
 *  This routine emulates the service interrupt routine of a hardware timer. Each time the ISR is serviced
 *  an internal variable named [time] is incremented. An event [tick1Evt] is published on each increment.
 *  No attached data is added to this event.
 *  Also, after 16 increments an extra event [tick16Evt] is also published. In this case, the value of variable
 *  [time] is attached to the event, so that EventHandlers listening could keep track of it.
 */
void ISR_Timer(void){
    static volatile uint32_t time = 0;
    kernel.SaveContext();
    time++;
    // each increment... publish a tick1Evt event without arguments
    #ifdef EVF_DEBUG
    pc.printf("--------------------------------------------------------\r\n");
    pc.printf("  ISR_Timer       Handler0     Handler1     Handler2    \r\n");
    pc.printf("--------------------------------------------------------\r\n");
    pc.printf("   Publish\r\n");
    pc.printf("   tick1Evt!!----------------->\r\n");
    #endif
    kernel.PublishEvent(&tick1Evt, NULL);

    // each 16 increments... publish a tick16Evt event passing time value as argument
    if((time&0xF)==0){
        #ifdef EVF_DEBUG
        pc.printf("   Publish\r\n");
        pc.printf("   tick16Evt!!----------------------------->\r\n");
        #endif
        kernel.PublishEvent(&tick16Evt, (void*)time);
   }
   kernel.RestoreContext();
}


/** User's application code: Event Dispatching routine attached to Handler0
 *
 *  This routine is attached to [Handler0] and as show above it will process [tick32Evt]
 *  events. It will print a message to a logger terminal to keep track of it.
 */
uint32_t DispatchFromHandler0(void* me, void* args){
    // catches tick32Evt event's attached data
    uint32_t twice = (uint32_t)args;
    #ifdef EVF_DEBUG
    pc.printf("                  processing\r\n");
    pc.printf("                  tick32Evt\r\n");
    pc.printf("                     ...\r\n");
    pc.printf("                     end\r\n");
    #endif

    return 0;
}


/** User's application code: Event Dispatching routine attached to Handler1
 *
 *  This routine is attached to [Handler1] and as show above it will process [tick1Evt]
 *  and [tick32Evt ]events. It will check the attached data to know which event has been
 *  processed on each invocation. If no attached data then tick1Evt, else tick32Evt.
 */
uint32_t DispatchFromHandler1(void* me, void* args){
    if(!args){
        #ifdef EVF_DEBUG
        pc.printf("                               processing\r\n");
        pc.printf("                                tick1Evt\r\n");
        pc.printf("                                  ...\r\n");
        pc.printf("                                  end\r\n");
        #endif
        return 0;
    }
   // catches tick32Evt event's attached data
   uint32_t twice = (uint32_t)args;
    #ifdef EVF_DEBUG
    pc.printf("                               processing\r\n");
    pc.printf("                                tick32Evt\r\n");
    pc.printf("                                  ...\r\n");
    pc.printf("                                  end\r\n");
    #endif
    return 0;
}

Further readings

Event-Driven systems is a wonderful world. If you like it you can start from here. I’m sure you won’t get bored.

http://www.embedded.com/design/prototyping-and-development/4207786/Low-Cost-Cooperative-Multitasking-Part-1-Building-a-simple-FM-player-

http://www.embedded.com/design/prototyping-and-development/4026990/Using-finite-state-machines-to-design-software-item-1

http://www.embedded.com/design/prototyping-and-development/4008247/A-crash-course-in-UML-state-machines-Part-1

http://www.state-machine.com/

http://www.eetimes.com/design/automotive-design/4006764/Embedded-multitasking-with-small-MCUs-Part-1--State-Machine-Constructs?cid=NL_Embedded&Ecosystem=embedded

http://www.embedded.com/design/prototyping-and-development/4210574/Tracing-of-the-event-flow-in-state-based-designs


All wikipages