Aller au contenu

Bike Computer Part III

Introduction

Dynamic Scheduling

In part 1 of the BikeComputer codelab, we have implemented a version of the bike computer program that uses cyclic static scheduling. In part 2 of the BikeComputer codelab, an event-driven model was implemented for an improved handling of events. This improves the performance of the application, because it guarantees that no event is ever missed. But, task scheduling is still implemented in a static way and tasks are always executed in the same order, independently of their importance.

In this part of the codelab, we will study how to implement preemptive dynamic scheduling using the capabilities of Mbed OS. With dynamic scheduling, tasks are scheduled on the fly based on their importance. Dynamic scheduling also simplifies the creation of tasks with different rates and preemption allows better prioritization and shorter response times.

Multi-tasking Application with periodic, event-driven and data-driven tasks

Another important capability of multi-tasking applications is to adapt the behavior of each task to its intrisic nature, namely:

  • periodic tasks: tasks such as reading sensor data are essentially periodic. In general, these tasks cannot be driven by specific events and they will run at a pre-defined interval.
  • event-driven tasks: tasks such as reacting to a user action cannot be predicted and are by nature not periodic. They are thus driven by external events and implemented using interrupt mechanisms.
  • data-driven tasks: some tasks need to receive data for accomplishing their job. They must thus wait for data and react when new data becomes available.

In this codelab, we will implement the different BikeComputer tasks using the appropriate type of task, namely:

  • reset task as an event-driven task, served by a dedicated thread.
  • gear and pedal tasks as data-driven tasks, where data is produced upon user action (using the joystick)
  • display and temperature tasks as periodic tasks that run at fixed periodic time intervals.

What you’ll build

In this codelab, you’re going to program our bike computer program in a multi-threaded approach using the Mbed OS scheduler capabilities. Tasks will be implemented either as periodic, event-driven or data-driven tasks.

What you’ll learn

  • How to develop a simple embedded program using multiple threads.
  • How to use the different multitasking mechanisms for implementing the BikeComputer program.

What you’ll need

Prepare for integrating a multi-tasking program into your BikeComputer program

For implementing a version of the BikeComputer program as a multi-tasking program, you must apply some changes to the program developed in part II of the codelab. The changes are documented below.

As a starting point you must duplicate the static_scheduling_with_event version and apply some changes. The steps for applying the changes are:

  • Copy the code from the “static_scheduling_with_event” folder in a new subfolder named “multi_tasking”. The version under “static_scheduling_with_event” contains the version that you delivered for phase 2 of the project.

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

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

Reduce Event Response Times

In the Bike computer - part II codelab, we implemented an event-driven mechanism for registering events such as button or joystick actions. This improved the program behavior by making sure that no event was ever missed, but this change does not yet improve the event response time. This is due to the fact that event-driven tasks are still executed at periodic time intervals with a static schedule.

You must modify this behavior by:

  • creating a EventQueue instance dedicated to serving ISRs in your BikeSystem class.
  • creating and starting a dedicated thread for dispatching events on this EventQueue instance.
  • modifying the BikeSystem::onReset() method for deferring the call to the BikeSystem::resetTask() method using the EventQueue instance - rather than setting a flag.
  • cleaning up the code - remove periodic call to BikeSystem::resetTask(), remove useless attributes, etc.

Once the changes are correctly implemented, test that the reset mechanism works properly.

Question 1

Try to modify the priority of the thread used for serving ISRs. This can be done at thread creation time: if the thread is declared as an attibute of the BikeSystem class, this can be done in the BikeSystem constructor initializer list using _deferredISRThread(osPriorityNormal, OS_STACK_SIZE, nullptr, "deferredISRThread").

Test with different priorites set to osPriorityBelowNormal, osPriorityNormal and osPriorityAboveNormal and observe the behavior of the response time in each case. Make sure that you understand what threads are running in your application and what the priority of each thread is.

How can you explain the observed response times? If you observe a difference depending on the priority of the _deferredISRThread thread, explain why. If you cannot observe any difference, also give an explanation and explain a scenario where different priorities would produce different results.

Solution

Our application runs the following threads:

  • Thread with name main, priority 24
  • Thread with name rtx_idle, priority 1
  • Thread with name deferredISRThread, priority …
  • Thread with name rtx_timer, priority 40

Given that the CPU usage is low (about 1%), the running thread is rtx_idle most of the time. This thread has the lowest possible priority (below any other thread) and any thread will immediately preempt it. This is why a low response time (about \(15-17 us\)) with a very small jitter can be observed for any priority used by the _deferredISRThread thread.

A scenario where different priorities will produce different results is the one where the system is busy (cpu usage around 100%). In this case, the _deferredISRThread thread would need to preempt the running thread when an ISR is deferred. Premption will happen if the _deferredISRThread has a priority higher than the one of the running thread or the same priority as the running thread with round-robin scheduling enabled. In the latter case, response time will probably be higher (depending on quantum used for round-robin) and jitter will also increase.

Make the Gear and Pedal tasks data-driven

In the Bike computer - part II codelab, both gear and pedal tasks are implemented as periodic tasks. In a way similar to the reset mechanism, gear and rotation speed are modified whenever the user presses on the joystick, in an event-based manner using interrupts. But this does not make these tasks event-driven or data-driven, since they are still implemented as periodic tasks in the BikeSystem class.

This implementation suffers from two important shortcomings:

  • Both tasks are executed periodically independently of any change in gear or rotation speed. This uses CPU even in the case where no change was made.
  • As for the reset task, the response time is not optimal and has a large jitter.

For improving this implementation, you must make both the gear and pedal tasks data-driven. This means that data will be generated whenever the user presses the joystick and that this data will be consumed almost immediately by the main thread. For implementing this change, you must choose of the following options:

  • create an Event upon user action and post this Event to the BikeSystem event queue, or
  • share a Queue or Mail instance with the BikeSystem. The gear and pedal tasks will put data to the Queue or Mail instance, while the BikeSystem will get data from it.

If you choose to implement your solution using Event, note that the Event class is a template class where the template parameter specified the callback function to execute when the event is dispatched. This illustrated below for a callback function that receives a uint8_t parameter:

  • Callback definition: mbed::Callback<void(uint8_t, uint8_t)> _cb;
  • Event creation: Event<void(uint8_t)> event(&_eventQueue, _cb);
  • Event posting: event.post(8);
  • Event handling: when the event is dispatched on the queue, the callback method _cb will be executed with the parameter 8.

Once you made the documented changes, you must of course:

  • Remove the periodic calls to BikeSystem::gearTask() and BikeSystem::speedDistanceTask().
  • You may also merge the BikeSystem::displayTask1() and BikeSystem::displayTask2() methods into a single BikeSystem::displayTask() method.

At this point, the BikeSystem::start() method schedule only two periodic tasks, using the BikeSystem::temperatureTask() and BikeSystem::displayTask() methods.

Prepare for Testing

Your final version of the BikeComputer program must of course be tested. The test cases test_multi_tasking_bike_system and test_reset_multi_tasking_bike_system in the test program given below validate the following behaviors:

  • test_multi_tasking_bike_system: The display and temperature task periods are correct.
  • test_reset_multi_tasking_bike_system: Resetting the BikeSystem works properly, with the expected response time (below \(20 us\) with a jitter smaller than \(3 us\))

Some changes are required in different classes for the test program to compile and work properly:

  • Add the method void setOnResetCallback(mbed::Callback<void()> cb) to the Speedometer class. This method must allow to register a callback with the proper signature and this callback function must be called (without additional delay) upon reset.
  • Make the following changes to the BikeSystem class when the preprocessor flag MBED_TEST_MODE is defined:

    • Make the BikeSystem::onReset() method public
    • Add the public method bike_computer::Speedometer &getSpeedometer() to the BikeSystem class when the preprocessor flag MBED_TEST_MODE is defined.
    • Disable any logging in the BikeSystem::resetTask() method.

Write your own Gear Device Test

For validating the correct implementation of the Gear task, you must also validate that changing the gear (up and down) works properly. Your test program must simulate a joystick up or down press by directly invoking the GearDevice callback methods (e.g. GearDevice::onUp()/onDown()).

For implementing the test program, you probably need to modify the accessibility of GearDevice methods and add some methods in the BikeSystem class. Those changes must apply only when the MBED_TEST_MODE compilation flag is defined.

Expected Deliverables

  • Deliverable: The BikeSystem program works the the different tasks implemented as documented in this codelab.

  • Deliverable: You have added a test case in the “TESTS/bike-computer/bike-system” program for validating the correct implementation of the GearDevice class and of the Gear task in the BikeSystem class.

  • Deliverable: the code passes all test cases of the “TESTS/bike-computer/bike-system” test program successfully. The entire “bike-system” test program is given below (without your own test case).

BikeComputer test program
TESTS/bike-computer/bike-system/main.cpp
// Copyright 2022 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file main.cpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief Bike computer test suite: scheduling
 *
 * @date 2023-08-26
 * @version 0.1.0
 ***************************************************************************/

#include <chrono>

#include "greentea-client/test_env.h"
#include "mbed.h"
#include "multi_tasking/bike_system.hpp"
#include "static_scheduling/bike_system.hpp"
#include "static_scheduling_with_event/bike_system.hpp"
#include "task_logger.hpp"
#include "unity/unity.h"
#include "utest/utest.h"

using namespace utest::v1;

// test_bike_system handler function
static void test_bike_system() {
    // 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::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
    constexpr std::chrono::microseconds taskComputationTimes[] = {
        100000us, 200000us, 100000us, 100000us, 200000us, 100000us};
    constexpr std::chrono::microseconds taskPeriods[] = {
        800000us, 400000us, 1600000us, 800000us, 1600000us, 1600000us};

    // allow for 2 msecs offset
    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());
        TEST_ASSERT_UINT64_WITHIN(
            deltaUs,
            taskComputationTimes[taskIndex].count(),
            bikeSystem.getTaskLogger().getComputationTime(taskIndex).count());
    }
}

// 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)
    constexpr uint64_t kDeltaUs = 2000;
    for (uint8_t taskIndex = 0; taskIndex < advembsof::TaskLogger::kNbrOfTasks;
         taskIndex++) {
        TEST_ASSERT_UINT64_WITHIN(
            kDeltaUs,
            taskPeriods[taskIndex].count(),
            bikeSystem.getTaskLogger().getPeriod(taskIndex).count());
    }
}

// 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)
    constexpr uint64_t kDeltaUs = 2000;
    for (uint8_t taskIndex = 0; taskIndex < advembsof::TaskLogger::kNbrOfTasks;
         taskIndex++) {
        TEST_ASSERT_UINT64_WITHIN(
            kDeltaUs,
            taskPeriods[taskIndex].count(),
            bikeSystem.getTaskLogger().getPeriod(taskIndex).count());
    }
}

// test_multi_tasking_bike_system handler function
static void test_multi_tasking_bike_system() {
    // create the BikeSystem instance
    multi_tasking::BikeSystem bikeSystem;

    // run the bike system in a separate thread
    Thread thread;
    thread.start(callback(&bikeSystem, &multi_tasking::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)
    constexpr uint64_t kDeltaUs = 2000;
    TEST_ASSERT_UINT64_WITHIN(
        kDeltaUs,
        taskPeriods[advembsof::TaskLogger::kTemperatureTaskIndex].count(),
        bikeSystem.getTaskLogger()
            .getPeriod(advembsof::TaskLogger::kTemperatureTaskIndex)
            .count());
    TEST_ASSERT_UINT64_WITHIN(
        kDeltaUs,
        taskPeriods[advembsof::TaskLogger::kDisplayTask1Index].count(),
        bikeSystem.getTaskLogger()
            .getPeriod(advembsof::TaskLogger::kDisplayTask1Index)
            .count());
}

// test_reset_multi_tasking_bike_system handler function
Timer timer;
static std::chrono::microseconds resetTime = std::chrono::microseconds::zero();
static EventFlags eventFlags;
static constexpr uint32_t kResetEventFlag = (1UL << 0);
static void resetCallback() {
    resetTime = timer.elapsed_time();
    eventFlags.set(kResetEventFlag);
}

static void test_reset_multi_tasking_bike_system() {
    // create the BikeSystem instance
    multi_tasking::BikeSystem bikeSystem;

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

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

    // test reset on BikeSystem
    bikeSystem.getSpeedometer().setOnResetCallback(resetCallback);

    // start the timer instance
    timer.start();

    // check for reset response time
    constexpr uint8_t kNbrOfResets             = 10;
    std::chrono::microseconds lastResponseTime = std::chrono::microseconds::zero();
    for (uint8_t i = 0; i < kNbrOfResets; i++) {
        // take time before reset
        auto startTime = timer.elapsed_time();

        // reset the BikeSystem
        bikeSystem.onReset();

        // wait for resetCallback to be called
        eventFlags.wait_all(kResetEventFlag);

        // get the response time and check it
        auto responseTime = resetTime - startTime;

        printf("Reset task: response time is %lld usecs\n", responseTime.count());

        // cppcheck generates an internal error with 20us
        constexpr std::chrono::microseconds kMaxExpectedResponseTime(20);
        TEST_ASSERT_TRUE(responseTime.count() <= kMaxExpectedResponseTime.count());

        constexpr uint64_t kDeltaUs = 4;
        constexpr std::chrono::microseconds kMaxExpectedJitter(3);
        if (i > 0) {
            auto jitter = responseTime - lastResponseTime;
            TEST_ASSERT_UINT64_WITHIN(
                kDeltaUs, kMaxExpectedJitter.count(), std::abs(jitter.count()));
        }
        lastResponseTime = responseTime;

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

    // stop the bike system
    bikeSystem.stop();
}
static utest::v1::status_t greentea_setup(const size_t number_of_cases) {
    // Here, we specify the timeout (60s) and the host test (a built-in host test or the
    // name of our Python file)
    GREENTEA_SETUP(180, "default_auto");

    return greentea_test_setup_handler(number_of_cases);
}

// List of test cases in this file
static Case cases[] = {
    Case("test bike system", test_bike_system),
    Case("test bike system with event queue", test_bike_system_event_queue),
    Case("test bike system with event", test_bike_system_with_event),
    Case("test multi-tasking bike system", test_multi_tasking_bike_system),
    Case("test reset multi-tasking bike system", test_reset_multi_tasking_bike_system)};

static Specification specification(greentea_setup, cases);

int main() { return !Harness::run(specification); }