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
- Mbed Studio for developing and debugging your program in C++.
- The Bike computer - part I, Bike computer - part II and Multi-tasking codelabs are prerequisites to this codelab.
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
tomulti_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 yourBikeSystem
class. - creating and starting a dedicated thread for dispatching events on this
EventQueue
instance. - modifying the
BikeSystem::onReset()
method for deferring the call to theBikeSystem::resetTask()
method using theEventQueue
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 thisEvent
to theBikeSystem
event queue, or - share a
Queue
orMail
instance with theBikeSystem
. The gear and pedal tasks will put data to theQueue
orMail
instance, while theBikeSystem
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 parameter8
.
Once you made the documented changes, you must of course:
- Remove the periodic calls to
BikeSystem::gearTask()
andBikeSystem::speedDistanceTask()
. - You may also merge the
BikeSystem::displayTask1()
andBikeSystem::displayTask2()
methods into a singleBikeSystem::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 theSpeedometer
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 flagMBED_TEST_MODE
is defined:- Make the
BikeSystem::onReset()
methodpublic
- Add the public method
bike_computer::Speedometer &getSpeedometer()
to theBikeSystem
class when the preprocessor flagMBED_TEST_MODE
is defined. - Disable any logging in the
BikeSystem::resetTask()
method.
- Make the
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 theBikeSystem
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
// 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); }