Aller au contenu

Bike Computer Part II

Introduction

Event-driven model

In part 1 of this codelab, we have implemented a version of the bike computer program that uses cyclic static scheduling. This implementation used polling for checking the button and joystick status. The limitations of this approach have been demonstrated and the need for an event-driven approach has been made clear.

In this part of the codelab, we will study how to implement an event-driven model for non periodic tasks. We will compare the event response time to the ones obtained in part 1 of the codelab.

What you’ll build

In this codelab, you’re going to program a simple bike computer program that is handling non periodic events using an event-driven approach, with static cyclic scheduling of the periodic tasks.

What you’ll learn

  • How to use and print statistics about CPU usage.
  • How to use the Mbed OS EventQueue API for programming periodic tasks.
  • How to develop a simple embedded program using an event-driven approach.

What you’ll need

Use the CPULogger class for printing CPU statistics

A CPULogger class is made available in the AdvEmbSof library. Use this class in your BikeSystem implementation for understanding how the CPU is used in your application. You may use it as illustrated below:

_cpuLogger.printStats();

Question 1

If you print CPU statistics at the end of every major cycle (in the super-loop), what CPU usage do you observe? How can you explain the observed CPU uptime?

Solution

You should observe an uptime time of about 99-100%. The reason is simple: with the current implementation we poll constantly for checking for button and joystick status and we perform busy waiting loops for matching the periods of the different tasks

Enabling statistics on the Mbed OS platform

For enabling CPU statistics on the Mbed OS library, you also need to modify your “mbed_app.json” file as follows:

    "target_overrides": {
        "*": {
            ...
            "platform.all-stats-enabled": true,
            ...

Reduce CPU usage with sleep calls rather than busy wait

For matching the expected task periods, the temperatureTask(), displayTask1() and displayTask2() methods of the BikeSystem class have implemented a busy wait mechanism. With this mechanism, the CPU is busy while waiting for a given time to elapse. This causes a CPU usage that is higher than it should be. This can be improved by replacing the busy wait implementation by calls to the ThisThread::sleep_for() method. The parameter passed to the method should be the remaining time to be spend in the method specified as std::chrono::milliseconds.

Question 2

If you run the program after the change from busy wait to sleep calls, what CPU usage do you observe? How can you explain the observed CPU uptime?

Solution

You should observe an uptime time of about 75%. The reason is simple: the temperature and display tasks have a cumulated execution time of \(400\,\mathsf{ms}\). Given that the remaining execution time is each method is slightly smaller, this represents approximately 24% of the major cycle time of \(1600\,\mathsf{ms}\). This thus reduces the CPU usage from approx. 99% to approx. 75%.

Use the Mbed OS EventQueue API for implementing periodic tasks

Rather than establishing the precise schedule of all tasks in the super-loop, another way of programming periodic tasks with Mbed OS is through the use of the EventQueue API. This API provides an easy way for scheduling events, including periodic events. Thus, rather than building a cyclic static scheduling of tasks in the super-loop, one can simply post Events configured with a given period to an EventQueue and then dispatch the events on the queue. This can be done as shown below:

EventQueue eventQueue;

Event<void()> gearEvent(&eventQueue, callback(this, &BikeSystem::gearTask));
gearEvent.delay(kGearTaskDelay);
gearEvent.period(kGearTaskPeriod);
gearEvent.post();

...

eventQueue.dispatch_forever();

You need to add a method called startWithEventQueue() to your BikeSystem class. In this method, you must post an event properly configured for each task and then dispatch the events on the event queue. Note that the dispatch_forever() method will never return (unless it is stopped) and you will thus never return from the startWithEventQueue() method.

Setting a delay for each event is not required. However, if you want to reproduce the exact same schedule as in your super-loop implementation, you need to schedule each task to start with a delay that corresponds to its first execution in the super-loop. If you do not do so, then the EventQueue dispatcher will serve events using its own strategy (e.g. FIFO) and it is very likely that tasks will not be executed in the same order.

Run the test program

Once you have implemented the startWithEventQueue(), you should add the test case given below to your “TESTS/bike-computer/bike-system” test program and run the test again. The test program now consists of two test cases and the two test cases should run successfully. Note that for the startWithEventQueue() test to be successful, you need to add the correct delay for each task/event. Otherwise, the period may be correct but the task logger will deliver periods that do not match the expected values.

test EventQueue case
TESTS/bike-computer/bike-system/main.cpp
// test_bike_system_event_queue handler function
static void test_bike_system_event_queue() {
    // create the BikeSystem instance
    static_scheduling::BikeSystem bikeSystem;

    // run the bike system in a separate thread
    Thread thread;
    thread.start(callback(&bikeSystem, &static_scheduling::BikeSystem::startWithEventQueue));

    // let the bike system run for 20 secs
    ThisThread::sleep_for(20s);

    // stop the bike system
    bikeSystem.stop();

    // check whether scheduling was correct
    // Order is kGearTaskIndex, kSpeedTaskIndex, kTemperatureTaskIndex,
    //          kResetTaskIndex, kDisplayTask1Index, kDisplayTask2Index
    // When we use the event queue, we do not check the computation time
    constexpr std::chrono::microseconds taskPeriods[] = {
        800000us, 400000us, 1600000us, 800000us, 1600000us, 1600000us};

    // allow for 2 msecs offset (with EventQueue)
    uint64_t deltaUs = 2000;
    for (uint8_t taskIndex = 0; taskIndex < advembsof::TaskLogger::kNbrOfTasks;
         taskIndex++) {
        TEST_ASSERT_UINT64_WITHIN(
            deltaUs,
            taskPeriods[taskIndex].count(),
            bikeSystem.getTaskLogger().getPeriod(taskIndex).count());        
    }
}

Static Scheduling Event-Driven Implementation

One of the major drawback of the implementation of our BikeComputer program so far, is that events can be missed. Moreover, polling continuously for detecting the button or joystick state consumes useless CPU time.

On embedded systems, this can be improved by handling events using interrupts. Interrupts are implemented using special dedicated hardware in the MCU that detects the event and runs an Interrupt Service Routine (ISR) in response. For handling asynchronous events that are by nature non-predictable, using interrupts is the most appropriate approach:

  • An interrupt-based routine only runs the code when it is called/initiated by the interrupt-service routine. This routine directly executes the addressed code, sometimes without any CPU interaction.
  • Therefore, it is fully scalable, because it is independent from the number of monitored events. In other words, the interrupt supervision doesn’t consume CPU time.
  • Using interrupts, the controller is prevented from doing unnecessary tasks by operating in a passive mode for as long as possible, responding mostly just to events and triggers.

When the interrupt trigger occurs, the processor does some hard-wired processing and then executes the ISR, including return-from-interrupt processing at the end, as depicted in the figure below.

Interrupt processing

Interrupt processing

Using the Mbed OS API for detecting an event on an input pin is very easy. Rather than continuously and periodically polling for input status, it is enough to create an instance of the InterruptIn class and to trigger an event when a digital input pin changes. You can trigger interrupts on the rising edge (change from 0 to 1) or falling edge (change from 1 to 0) of signals. The Joystick class delivered with the DISCO_H747I library offers the same functionality and a callback can be set for any joystick event (like “Up” or “Down” button pressed).

Implementing the BikeComputer program in an event-driven approach

For implementing a version of your program with an event-driven detection of the button and joystick press, you must apply some changes to the program developed in the part I of the codelab. The major change is that you must register a function that is called upon button or joystick press.

The steps for applying the changes are detailed below:

  • Copy the code from the “static_scheduling” folder in a new subfolder named “static_scheduling_with_event”. This code has to contain the changes described in the previous sections of this codelab.

  • Modify the namespace in all classes from static_scheduling to static_scheduling_with_event.

  • Modify your main program for using this new implementation (e.g. include path or namespace use).

  • Replace the constructor of the ResetDevice class with a constructor that takes a callback argument, as illustrated below. Please note the use of the template class mbed::Callback in the definition of the constructor. This template acts as an object-oriented version of a pointer to a function. Callback is a generic class that allows defining callback methods on different classes and with different signatures. In the example below, the use of void() in mbed::Callback<void()> means that the callback is a method that is not returning any value (void) and that takes no parameter (()).

static_scheduling_with_event/reset_device.hpp
class ResetDevice {
  // constructor used for event-driven behavior
  ResetDevice(mbed::Callback<void()> cb);
  ...
}
  • Remove all features (data members/methods) which are not used anymore from the ResetDevice class.

  • In the implementation of the new constructor of the ResetDevice class, modify the call to the rise() method of the InterruptIn pin instance. The callback passed as parameter must be used here.

  • For constructing an instance of ResetDevice, one must then pass as parameter an instance of callback as shown below. In our code, this is done in the initializer list of the BikeSystem class constructor. Here we construct the ResetDevice instance with the BikeSystem::onReset() callback method. For this purpose, the onReset() method must be added to the BikeSystem class.

static_scheduling_with_event/bike_system.cpp
BikeSystem::BikeSystem()
    : _resetDevice(callback(this, &BikeSystem::onReset))
...
  • In the BikeSystem class, implement the onReset() method called upon button press. In this implementation, set a volatile data member that registers the occurrence of the reset. The reason why it is necessary to use a volatile data member is explained below.

  • Modify the “BikeSystem::resetTask()method for checking the volatile data member and not calling theResetDevice::checkReset()` method any more.

Note that you must also modify the GearDevice and PedalDevice classes in a similar way for an event-driven implementation. Rather than polling for checking the joystick state at regular intervals, you must register an event handler using the different Joystick::setXXXCallback() methods. These classes register a private method as callbacks to the Joystick classes, for either changing the gear (GearDevice) or the pedal rotation speed (PedalDevice).

Once all classes have been changed accordingly, you may compile and test your application. Note that your implementation of the static_scheduling_with_event::BikeSystem::start() method should be the same as the one of the static_scheduling::BikeSystem::startWithEventQueue(). In other words, it must use the EventQueue mechanism for scheduling periodic tasks.

Linker warning

When you compile your program with the two versions of the BikeSystem classes (static_scheduling and static_scheduling_with_event), you may a compiler warning stating that multiple versions of the same objects exist. In this case, you may modify the “.mbedignore” file of your project for ignoring a specific version of the BikeSystem classes.

Run the test program

Once you have implemented all changes documented above, you should add the test case given below to your “TESTS/bike-computer/bike-system” test program and run the test again. The test program now consists of three test cases and the three test cases should run successfully. Your that for the startWithEventQueue() test to be successful, you need to add the correct delay for each task/event. Otherwise, the period may be correct but the task logger will deliver periods that do not match the expected values.

test with event case
TESTS/bike-computer/bike-system/main.cpp
...
#include "static_scheduling_with_event/bike_system.hpp"
...
// test_bike_system_with_event handler function
static void test_bike_system_with_event() {
    // create the BikeSystem instance
    static_scheduling_with_event::BikeSystem bikeSystem;

    // run the bike system in a separate thread
    Thread thread;
    thread.start(callback(&bikeSystem, &static_scheduling_with_event::BikeSystem::start));

    // let the bike system run for 20 secs
    ThisThread::sleep_for(20s);

    // stop the bike system
    bikeSystem.stop();

    // check whether scheduling was correct
    // Order is kGearTaskIndex, kSpeedTaskIndex, kTemperatureTaskIndex,
    //          kResetTaskIndex, kDisplayTask1Index, kDisplayTask2Index
    // When we use event handling, we do not check the computation time
    constexpr std::chrono::microseconds taskPeriods[] = {
        800000us, 400000us, 1600000us, 800000us, 1600000us, 1600000us};

    // allow for 2 msecs offset (with EventQueue)
    uint64_t deltaUs = 2000;
    for (uint8_t taskIndex = 0; taskIndex < advembsof::TaskLogger::kNbrOfTasks;
         taskIndex++) {
        TEST_ASSERT_UINT64_WITHIN(
            deltaUs,
            taskPeriods[taskIndex].count(),
            bikeSystem.getTaskLogger().getPeriod(taskIndex).count());        
    }
}

Question 3

If you run the static_scheduling_with_event program, what CPU usage do you observe? How can you explain the observed CPU uptime?

Solution

The uptime should go down from approx 75% to 1%. The explanation is simple: the GearDevice, PedalDevice and ResetDevice do NOT poll for button and joystick status anymore. The total polling time per major cycle was \(100\,\mathsf{ms} * 2\) + \(200\,\mathsf{ms} * 4\) + \(200\,\mathsf{ms} * 2\) for a total of \(1200\,\mathsf{ms}\). We thus reduce CPU usage by approx 75% !

Volatile data

At this stage, it is important to introduce the concept of volatile data. Volatile data is created in your program by adding the volatile keyword to a variable declaration. This is sometimes required because compilers assume that variables in memory don’t change spontaneously, and optimize the code based on that belief. The reasons for optimizing the code are:

  • Don’t reload a variable from memory if the current function hasn’t changed it.
  • Read a variable from memory into a register for faster access.
  • Write back to memory at the end of the procedure, or before a procedure call, or when the compiler runs out of free registers, but not before, for saving memory write.

This optimization can fail in some circumstances. For example, while reading from input port or polling for key press using while (status);. The compiler may generate code that reads from status only once and reuse that value. This will generate an infinite loop triggered by status being true.

The variables for which it fails in all circumstances are:

  • Memory-mapped peripheral register: register changes on its own.
  • Global variables modified by an ISR: the ISR changes the variable.
  • Global variables in a multithreaded application: another thread or ISR changes the variable.

This is the reason why you must declare the reset variable modified in the ISR as volatile.

Static scheduling with interrupts

This implementation of the bike computer program can be described as cyclic static scheduling with interrupts. It essentially has two operating modes:

  • The main application code runs in the foreground and schedule all period tasks as expected. Some period tasks check for device status stored in global volatile memory for executing portions of the main application code.
  • The interrupt service routines run in the background with high priority. These routines are executed when an event is triggered, they handle the most urgent work and modify global volatile variables that will be queried for processing in the main application code.

This implementation is an improvement over the implementation that does not use events, since it guarantees that no event is ever missed.

Question 4: Response time of the reset event

When you run multiple tests for computing the response time of the reset event, what do you observe? Is there an improvement as compared to the static_scheduling::BikeSystem implementation?

Based on the program itself and on the task scheduling, explain the observed behavior.

Solution

The improvement is that you can no longer miss a keystroke, as the event is registered in interrupts in any case. However, there is no improvement in response time, and there is still a wide range of response time values, from a few milliseconds to about \(800\,\mathsf{ms}\). The explanation is that the reset task is still run as a periodic task with a period of \(800\,\mathsf{ms}\).

There is thus a clear need for more dynamic scheduling, which can account for task importance, improve response times and allow for more dynamic behaviors.

Deliverables

At the end of this codelab, make sure that you have accomplished the following:

  • The BikeSystem class has replace busy waits with sleep calls.
  • The ResetDevice, GearDevice and PedalDevice classes has been modified as specified, for supporting event-driven handling of the button and joystick.
  • The BikeSystem class has been modified for registering ISRs and handling events with volatile data members.
  • You have run all tests successfully.
  • You have answered all questions.

Also make sure that you read carefully the deliverable sections of the project Phase 2 before starting the work on this codelab.