BikeComputer Part I
Introduction
BikeComputer program
In this codelab, we consider the implementation of a bike computer for spinning bikes. Spinning bike only have a flywheel and one needs to continuously pedal to turn the wheel. Our spinning bike is original, because it also has a gear system (not sure whether this is mechanically feasible…).
Our BikeComputer program implements the following functionalities:
- Bike gear: the bike’s gear system provides a value representing the current gear that the BikeComputer program can read. On our system, gear may be changed using the buttons.
- Pedal rotation: the BikeComputer program simulates pedal rotation at a given speed. On our system, rotation speed can be modified using the buttons.
- Reset: with a button, the user can reset the counters.
- Speedometer: based on the current gear and pedal rotation, it computes the current speed and the traveled distance.
- Temperature: our bike has a temperature sensor that is used for displaying the current room temperature.
- Display: the user can read the information provided by the BikeComputer program on an LCD screen.
In this and the following codelabs, we will study different ways of implementing the BikeComputer program. We will analyse the advantages and disadvantages of these implementations. We will study some important performance indicators, such as the reset response time — the delay between the user pressing the reset button and the LCD showing the reset values on screen.
What you’ll build
In this codelab, you will program a simple BikeComputer program using a
timeline cyclic scheduling algorithm and using a Super-Loop program. In the
next codelab, we will implement timeline cyclic scheduling as a
Time-Triggered Cyclic Executive (TTCE) program.
What you’ll learn
- How to schedule the different tasks of an embedded program using timeline cyclic scheduling.
- How to develop a embedded program using a Super-Loop mechanism.
- The limitations of the proposed timeline cyclic scheduling program.
- Together with the following codelabs, the advantages and disadvantages of the different programming models and scheduling algorithms.
What you’ll need
- The Zephyr Development Environment for developing and debugging C++ code snippets.
- The C++ Basics and all codelabs required for phase1 of the project Phase 1 are prerequisites to this codelab.
The different implementations
The BikeComputer codelab is divided into three different parts, in which we
will realize different implementations of the same program:
- The first implementations (codelab parts 1 and 2) are the simplest: a Super-Loop and one TTCE program with timeline cyclic scheduling of tasks. In these implementations, the system never generates an event and all tasks are executed periodically.
- We will analyze the limitations of this approach, based on these, implement
several event-driven programs using different scheduling algorithms (parts 2
and 3 of the
BikeComputercodelab).
To start the different implementations, you must use the Zephyr RTOS
BikeComputer application, which was created under the “bike_computer” folder
for the phase 1 of the project. To implement the different versions of the
BikeComputer program, you must apply the following approach:
- You must combine the different versions into a single program, but keep them
in separate subfolders and make slight changes to the
main()function. This will ensure that all versions are implemented within the same project. To clearly differentiate the different versions within the same program, you must use different namespaces for each version.
Conceive and implement the BikeComputer modules
It is a good practice to modularize the implementation of a program. In C++,
this naturally involves the creation of C++ classes, each of which is responsible
for implementing a specific feature. First, we implement these C++ classes, and
then we integrate them into our BikeComputer program.
We will start by implementing the classes that will be used by all
BikeComputer program implementations: the SensorDevice and Speedometer
classes. As these classes will be used by all BikeComputer program
implementations, they are added to a subfolder named “common” and are
defined within the bike_computer namespace.
The constants used in the program
The constants are defined as follows:
Definition of constants
// Copyright 2025 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 constants.hpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Constants definition used for implementing the bike system
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
#pragma once
// std
#include <chrono>
namespace bike_computer {
// gear related constants
static constexpr uint8_t kMinGear = 1;
static constexpr uint8_t kMaxGear = 9;
// smallest gear (= 1) corresponds to a gear size of 20
// when the gear increases, the gear size descreases
static constexpr uint8_t kMaxGearSize = 20;
static constexpr uint8_t kMinGearSize = kMaxGearSize - kMaxGear;
// pedal related constants
// When compiling and linking with gcc, we get a link error when using static
// constexpr. The error is related to template instantiation.
using namespace std::literals;
// definition of pedal rotation initial time (corresponds to 80 turn / min)
static constexpr std::chrono::milliseconds kInitialPedalRotationTime = 750ms;
// definition of pedal minimal rotation time (corresponds to 160 turn / min)
static constexpr std::chrono::milliseconds kMinPedalRotationTime = 375ms;
// definition of pedal maximal rotation time (corresponds to 10 turn / min)
static constexpr std::chrono::milliseconds kMaxPedalRotationTime = 1500ms;
// definition of pedal rotation time change upon acceleration/deceleration
static constexpr std::chrono::milliseconds kDeltaPedalRotationTime = 25ms;
} // namespace bike_computer
The SensorDevice class
To simplify the integration of the BME-280 sensor and hide some
hardware details, our BikeComputer program includes a SensorDevice class.
The class declaration is given below:
SensorDevice declaration
// Copyright 2025 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 sensor_device.hpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief SensorDevice header file
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
#pragma once
// zephyr
#include <zephyr/kernel.h>
// zpp_lib
#include "zpp_include/non_copyable.hpp"
#include "zpp_include/zephyr_result.hpp"
namespace bike_computer {
class SensorDevice : private zpp_lib::NonCopyable<SensorDevice> {
public:
// constructor
SensorDevice() = default;
// method for initializing the device
[[nodiscard]] zpp_lib::ZephyrResult initialize();
// methods used for reading sensor measurements
[[nodiscard]] zpp_lib::ZephyrResult readTemperature(float& temperature);
[[nodiscard]] zpp_lib::ZephyrResult readHumidity(float& humidity);
private:
// data members
const struct device* _sensorDevice = nullptr;
};
} // namespace bike_computer
It is also using the namespace bike_computer. Based on the class declaration, you must
implement it in the “bike_computer/src/common/sensor_device.cpp” file. Once you have implemented
it, you may test your implementation by using the following test program:
SensorDevice test program
// Copyright 2025 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 test_sensor_device.cpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Test program for the SensorDevice class
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
// zephyr
#include <zephyr/logging/log.h>
#include <zephyr/ztest.h>
// bike_computer
#include "common/sensor_device.hpp"
LOG_MODULE_REGISTER(test_sensor_device, CONFIG_APP_LOG_LEVEL);
ZTEST(sensor_device, test_sensor_device) {
// create the SensorDevice instance
bike_computer::SensorDevice sensorDevice;
auto res = sensorDevice.initialize();
if (!res) {
zassert_true(res, "Cannot initialize sensor device: %d", res.error());
}
float temperature = 0.0f;
res = sensorDevice.readTemperature(temperature);
if (!res) {
zassert_true(res, "Cannot initialize sensor device: %d", res.error());
}
static constexpr float kTemperatureRange = 20.0f;
static constexpr float kMeanTemperature = 15.0f;
zassert_within(temperature,
kMeanTemperature,
kTemperatureRange,
"Temperature outside range: %f",
static_cast<double>(temperature));
float humidity = 0.0f;
res = sensorDevice.readTemperature(humidity);
if (!res) {
zassert_true(res, "Cannot initialize sensor device: %d", res.error());
}
static constexpr float kHumidityRange = 45.0f;
static constexpr float kMeanHumidity = 50.0f;
zassert_within(humidity,
kMeanHumidity,
kHumidityRange,
"Humidity outside range: %f",
static_cast<double>(humidity));
}
ZTEST_SUITE(sensor_device, NULL, NULL, NULL, NULL, NULL);
If you integrate this program into the “tests” folder of your BikeComputer
program in the appropriate subfolder, you may validate its correct
implementation by using the command
west twister -T bike_computer/tests/bike_computer/sensor_device -p nrf5340dk/nrf5340/cpuapp --device-testing --hardware-map "your-hardware-file.yaml" --short-build-path
Emulation on QEMU_X86
In order to run the SensorDevice test on qemu_x86, it is necessary to fully emulate the BME280 sensor on this platform. Since we encapsulate the sensor functionality in the SensorDevice class,
we can emulate the sensor more simply. In the class implementation, use the #if CONFIG_QEMU_TARGET != 1 pragma to differentiate the implementation for the qemu_x86 device and to implement a very simple emulation that delivers sensor values without interacting with a physical sensor. While a full emulation would be preferable, emulating the sensor in this manner is sufficient for our purposes.
If the test program succeeds (assuming that you have connected your BME280 sensor and screen properly), then it is time to commit and push your changes.
Small commits
Remember:
- Small commits are better than large commits that include many unrelated changes.
- Before committing the changes, you must also integrate the related folder in the precommit phase by modifying the “.pre-commit-config.yaml” file accordingly: you must modify the “files” property by adding the folder in the regular expression. Once the precommit phase succeeds, you may commit and push your changes.
The Speedometer class
Another class that will be used by all BikeComputer implementations is the
Speedometer class. This class is responsible to compute the current speed and
traveled distance, given the gear size, the wheel circumference and the pedal
rotation time. The class declaration is given below:
Speedometer declaration
// Copyright 2025 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 speedometer.hpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Speedometer header file
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
#pragma once
// std
#include <chrono>
// local
#include "constants.hpp"
// zpp_lib
#include "zpp_include/mutex.hpp"
#include "zpp_include/non_copyable.hpp"
#include "zpp_include/thread.hpp"
// stl
#if CONFIG_TEST == 1
#include <functional>
#endif // CONFIG_TEST == 1
namespace bike_computer {
class Speedometer : private zpp_lib::NonCopyable<Speedometer> {
public:
Speedometer();
// method used for setting the current pedal rotation time
void setCurrentRotationTime(const std::chrono::milliseconds& currentRotationTime);
// method used for setting/getting the current gear
void setGearSize(uint8_t gearSize);
// method called for getting the current speed (expressed in km / h)
float getCurrentSpeed() const;
// method called for getting the current traveled distance (expressed in km)
float getDistance();
// method called for resetting the traveled distance
void reset();
// methods used for tests only
#if CONFIG_TEST == 1
uint8_t getGearSize() const;
float getWheelCircumference() const;
float getTraySize() const;
std::chrono::milliseconds getCurrentPedalRotationTime() const;
void setOnResetCallback(std::function<void()> cb);
#endif // CONFIG_TEST == 1
private:
// private methods
void computeSpeed();
void computeDistance();
// definition of task period time
static constexpr std::chrono::milliseconds kTaskPeriod = 400ms;
// definition of task execution time
static constexpr std::chrono::microseconds kTaskRunTime = 200000us;
// constants related to speed computation
static constexpr float kWheelCircumference = 2.1f;
static constexpr uint8_t kTraySize = 50;
std::chrono::microseconds _lastTime = std::chrono::microseconds::zero();
std::chrono::milliseconds _pedalRotationTime = kInitialPedalRotationTime;
// data members
// LowPowerTicker _ticker;
float _currentSpeed = 0.0f;
zpp_lib::Mutex _totalDistanceMutex;
float _totalDistance = 0.0f;
uint8_t _gearSize = 1;
zpp_lib::Thread _thread;
#if CONFIG_TEST == 1
std::function<void()> _cb;
#endif // CONFIG_TEST == 1
};
} // namespace bike_computer
The test program for this class is given below:
Speedometer test program
// Copyright 2025 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 test_speedometer.cpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Test program for the Speedometer class
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
// zephyr
#include <zephyr/logging/log.h>
#include <zephyr/ztest.h>
// std
#include <chrono>
#include <cstdio>
// zpp_lib
#include "zpp_include/this_thread.hpp"
// bike_computer
#include "common/speedometer.hpp"
LOG_MODULE_REGISTER(test_speedometer, CONFIG_APP_LOG_LEVEL);
// allow for 0.1 km/h difference
static constexpr float kAllowedSpeedDelta = 0.1f;
// allow for 1m difference
static constexpr float kAllowedDistanceDelta = 1.0f / 1000.0f;
// for ms or s literals
using namespace std::literals;
// function called by test handler functions for verifying the current speed
void check_current_speed(const std::chrono::milliseconds& pedalRotationTime,
uint8_t traySize,
uint8_t gearSize,
float wheelCircumference,
float currentSpeed) {
// compute the number of pedal rotation per hour
uint32_t milliSecondsPerHour = 1000 * 3600;
float pedalRotationsPerHour = static_cast<float>(milliSecondsPerHour) /
static_cast<float>(pedalRotationTime.count());
// compute the expected speed in km / h
// first compute the distance in meter for each pedal turn
float trayGearRatio = static_cast<float>(traySize) / static_cast<float>(gearSize);
float distancePerPedalTurn = trayGearRatio * wheelCircumference;
float expectedSpeed = (distancePerPedalTurn / 1000.0f) * pedalRotationsPerHour;
printk(" Expected speed is %f, current speed is %f\n",
static_cast<double>(expectedSpeed),
static_cast<double>(currentSpeed));
zassert_within(currentSpeed,
expectedSpeed,
kAllowedSpeedDelta,
"Current speed is not within bounds");
}
// compute the traveled distance for a time interval
float compute_distance(const std::chrono::milliseconds& pedalRotationTime,
uint8_t traySize,
uint8_t gearSize,
float wheelCircumference,
const std::chrono::milliseconds& travelTime) {
// compute the number of pedal rotation during travel time
// both times are expressed in ms
float pedalRotations = static_cast<float>(travelTime.count()) /
static_cast<float>(pedalRotationTime.count());
// compute the distance in meter for each pedal turn
float trayGearRatio = static_cast<float>(traySize) / static_cast<float>(gearSize);
float distancePerPedalTurn = trayGearRatio * wheelCircumference;
// distancePerPedalTurn is expressed in m, divide per 1000 for a distance in km
return (distancePerPedalTurn * pedalRotations) / 1000.0f;
}
// function called by test handler functions for verifying the distance traveled
void check_distance(const std::chrono::milliseconds& pedalRotationTime,
uint8_t traySize,
uint8_t gearSize,
float wheelCircumference,
const std::chrono::milliseconds& travelTime,
float distance) {
// distancePerPedalTurn is expressed in m, divide per 1000 for a distance in km
float expectedDistance = compute_distance(
pedalRotationTime, traySize, gearSize, wheelCircumference, travelTime);
printf(" Expected distance is %f, current distance is %f\n",
static_cast<double>(expectedDistance),
static_cast<double>(distance));
zassert_within(distance, expectedDistance, kAllowedDistanceDelta);
}
// test the speedometer by modifying the gear
ZTEST(speedometer, test_gear_size) {
// create a speedometer instance
bike_computer::Speedometer speedometer;
// get speedometer constant values (for this test)
const auto traySize = speedometer.getTraySize();
const auto wheelCircumference = speedometer.getWheelCircumference();
const auto pedalRotationTime = speedometer.getCurrentPedalRotationTime();
for (uint8_t gearSize = bike_computer::kMinGearSize;
gearSize <= bike_computer::kMaxGearSize;
gearSize++) {
// set the gear
printf("Testing gear size %d\n", gearSize);
speedometer.setGearSize(gearSize);
// get the current speed
auto currentSpeed = speedometer.getCurrentSpeed();
// check the speed against the expected one
check_current_speed(
pedalRotationTime, traySize, gearSize, wheelCircumference, currentSpeed);
}
}
// test the speedometer by modifying the pedal rotation speed
ZTEST(speedometer, test_rotation_speed) {
// create a speedometer instance
bike_computer::Speedometer speedometer;
// set the gear size
speedometer.setGearSize(bike_computer::kMaxGearSize);
// get speedometer constant values
const auto traySize = speedometer.getTraySize();
const auto wheelCircumference = speedometer.getWheelCircumference();
const auto gearSize = speedometer.getGearSize();
// first test increasing rotation speed (decreasing rotation time)
auto pedalRotationTime = speedometer.getCurrentPedalRotationTime();
while (pedalRotationTime > bike_computer::kMinPedalRotationTime) {
// decrease the pedal rotation time
pedalRotationTime -= bike_computer::kDeltaPedalRotationTime;
speedometer.setCurrentRotationTime(pedalRotationTime);
// get the current speed
const auto currentSpeed = speedometer.getCurrentSpeed();
// check the speed against the expected one
check_current_speed(
pedalRotationTime, traySize, gearSize, wheelCircumference, currentSpeed);
}
// second test decreasing rotation speed (increasing rotation time)
pedalRotationTime = speedometer.getCurrentPedalRotationTime();
while (pedalRotationTime < bike_computer::kMaxPedalRotationTime) {
// increase the pedal rotation time
pedalRotationTime += bike_computer::kDeltaPedalRotationTime;
speedometer.setCurrentRotationTime(pedalRotationTime);
// get the current speed
const auto currentSpeed = speedometer.getCurrentSpeed();
// check the speed against the expected one
check_current_speed(
pedalRotationTime, traySize, gearSize, wheelCircumference, currentSpeed);
}
}
// test the speedometer by modifying the pedal rotation speed
ZTEST(speedometer, test_distance) {
// create a speedometer instance
bike_computer::Speedometer speedometer;
// set the gear size
speedometer.setGearSize(bike_computer::kMaxGearSize);
// get speedometer constant values
const auto traySize = speedometer.getTraySize();
const auto wheelCircumference = speedometer.getWheelCircumference();
auto gearSize = speedometer.getGearSize();
auto pedalRotationTime = speedometer.getCurrentPedalRotationTime();
// test different travel times
const std::chrono::milliseconds travelTimes[] = {500ms, 1000ms, 5s, 10s};
const uint8_t nbrOfTravelTimes = sizeof(travelTimes) / sizeof(travelTimes[0]);
// first check travel distance without changing gear and rotation speed
std::chrono::milliseconds totalTravelTime = std::chrono::milliseconds::zero();
for (uint8_t index = 0; index < nbrOfTravelTimes; index++) {
// run for the travel time and get the distance
zpp_lib::ThisThread::sleep_for(travelTimes[index]);
// get the distance traveled
const auto distance = speedometer.getDistance();
// accumulate travel time
totalTravelTime += travelTimes[index];
// check the distance vs the expected one
check_distance(pedalRotationTime,
traySize,
gearSize,
wheelCircumference,
totalTravelTime,
distance);
}
// now change gear at each time interval
auto expectedDistance = speedometer.getDistance();
for (uint8_t index = 0; index < nbrOfTravelTimes; index++) {
// update the gear size
gearSize++;
speedometer.setGearSize(gearSize);
// run for the travel time and get the distance
zpp_lib::ThisThread::sleep_for(travelTimes[index]);
// compute the expected distance for this time segment
float distance = compute_distance(
pedalRotationTime, traySize, gearSize, wheelCircumference, travelTimes[index]);
expectedDistance += distance;
// get the distance traveled
const auto traveledDistance = speedometer.getDistance();
printf(" Expected distance is %f, current distance is %f\n",
static_cast<double>(expectedDistance),
static_cast<double>(traveledDistance));
zassert_within(traveledDistance, expectedDistance, kAllowedDistanceDelta);
}
// now change rotation speed at each time interval
expectedDistance = speedometer.getDistance();
for (uint8_t index = 0; index < nbrOfTravelTimes; index++) {
// update the rotation speed
pedalRotationTime += bike_computer::kDeltaPedalRotationTime;
speedometer.setCurrentRotationTime(pedalRotationTime);
// run for the travel time and get the distance
zpp_lib::ThisThread::sleep_for(travelTimes[index]);
// compute the expected distance for this time segment
float distance = compute_distance(
pedalRotationTime, traySize, gearSize, wheelCircumference, travelTimes[index]);
expectedDistance += distance;
// get the distance traveled
const auto traveledDistance = speedometer.getDistance();
printf(" Expected distance is %f, current distance is %f\n",
static_cast<double>(expectedDistance),
static_cast<double>(traveledDistance));
zassert_within(traveledDistance, expectedDistance, kAllowedDistanceDelta);
}
}
// test the speedometer by modifying the pedal rotation speed
ZTEST(speedometer, test_reset) {
// create a speedometer instance
bike_computer::Speedometer speedometer;
// set the gear size
speedometer.setGearSize(bike_computer::kMinGearSize);
// get speedometer constant values
const auto traySize = speedometer.getTraySize();
const auto wheelCircumference = speedometer.getWheelCircumference();
const auto gearSize = speedometer.getGearSize();
const auto pedalRotationTime = speedometer.getCurrentPedalRotationTime();
// travel for 1 second
const auto travelTime = 1000ms;
zpp_lib::ThisThread::sleep_for(travelTime);
// check the expected distaance traveled
const auto expectedDistance = compute_distance(
pedalRotationTime, traySize, gearSize, wheelCircumference, travelTime);
// get the distance traveled
auto traveledDistance = speedometer.getDistance();
printk(" Expected distance is %f, current distance is %f\n",
static_cast<double>(expectedDistance),
static_cast<double>(traveledDistance));
zassert_within(traveledDistance, expectedDistance, kAllowedDistanceDelta);
// reset the speedometer
speedometer.reset();
// traveled distance should now be zero
traveledDistance = speedometer.getDistance();
printk(" Expected distance is %f, current distance is %f\n",
0.0,
static_cast<double>(traveledDistance));
zassert_within(0.0f, traveledDistance, kAllowedDistanceDelta);
}
ZTEST_SUITE(speedometer, NULL, NULL, NULL, NULL, NULL);
The implementation of the Speedometer class is given below, at the exception
of the reset(), computeSpeed() and computeDistance() methods that you must
implement yourselves.
Speedometer implementation (partial)
// Copyright 2025 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 speedometer_device.cpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Speedometer implementation
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
#include "speedometer.hpp"
// zephyr
#include <zephyr/logging/log.h>
// std
#include <chrono>
#include <ratio>
// zpp_lib
#include "zpp_include/time.hpp"
LOG_MODULE_DECLARE(bike_computer, CONFIG_APP_LOG_LEVEL);
namespace bike_computer {
Speedometer::Speedometer() : _lastTime(zpp_lib::Time::getUpTime()) {}
void Speedometer::setCurrentRotationTime(
const std::chrono::milliseconds& currentRotationTime) {
if (_pedalRotationTime != currentRotationTime) {
// compute distance before changing the rotation time
computeDistance();
// change pedal rotation time
_pedalRotationTime = currentRotationTime;
// compute speed with the new pedal rotation time
computeSpeed();
}
}
void Speedometer::setGearSize(uint8_t gearSize) {
if (_gearSize != gearSize) {
// compute distance before chaning the gear size
computeDistance();
// change gear size
_gearSize = gearSize;
// compute speed with the new gear size
computeSpeed();
}
}
float Speedometer::getCurrentSpeed() const { return _currentSpeed; }
float Speedometer::getDistance() {
// make sure to update the distance traveled
computeDistance();
return _totalDistance;
}
void Speedometer::reset() {
#if CONFIG_TEST == 1
if (_cb != nullptr) {
_cb();
}
#endif // CONFIG_TEST == 1
// TODO
}
#if CONFIG_TEST == 1
uint8_t Speedometer::getGearSize() const { return _gearSize; }
float Speedometer::getWheelCircumference() const { return kWheelCircumference; }
float Speedometer::getTraySize() const { return kTraySize; }
std::chrono::milliseconds Speedometer::getCurrentPedalRotationTime() const {
return _pedalRotationTime;
}
void Speedometer::setOnResetCallback(std::function<void()> cb) { _cb = cb; }
#endif // CONFIG_TEST == 1
void Speedometer::computeSpeed() {
// For computing the speed given a rear gear (braquet), one must divide the size of
// the tray (plateau) by the size of the rear gear (pignon arrière), and then multiply
// the result by the circumference of the wheel. Example: tray = 50, rear gear = 15.
// Distance run with one pedal turn (wheel circumference = 2.10 m) = 50/15 * 2.1 m
// = 6.99m If you ride at 80 pedal turns / min, you run a distance of 6.99 * 80 / min
// ~= 560 m / min = 33.6 km/h
// TODO
}
void Speedometer::computeDistance() {
// For computing the speed given a rear gear (braquet), one must divide the size of
// the tray (plateau) by the size of the rear gear (pignon arrière), and then multiply
// the result by the circumference of the wheel. Example: tray = 50, rear gear = 15.
// Distance run with one pedal turn (wheel circumference = 2.10 m) = 50/15 * 2.1 m
// = 6.99m If you ride at 80 pedal turns / min, you run a distance of 6.99 * 80 / min
// ~= 560 m / min = 33.6 km/h. We then multiply the speed by the time for getting the
// distance traveled.
// TODO
}
} // namespace bike_computer
If you implement these methods correctly and run the following command:
west twister -T bike_computer/tests/bike_computer/speedometer --device-testing --hardware-map "your-hardware-file.yaml" --short-build-path
twister-out/twister.log file for understanding the problem and for
fixing your implementation.
Once your implementation has been successfully tested, you may commit and push the changes as documented above.
Integrate Other Required Software Components
In addition to the classes described above and below, you need to integrate further classes
to implement the BikeComputer:
- The
BikeDisplayclass provides an API to display information on the LCD screen. It is used in theBikeSystemclass. The .cpp/.hpp files can be downloaded here and here. - Resources used in the
BikeDisplaytask can be downloaded here and copied to the “bike_computer/src/common/resources” folder. - The
TaskManagerclass allows to monitor and test that tasks are respecting their timing constraints. When used in test programs (withCONFIG_TEST=y), the class will assert when timing constraints are not respected. You can download the .cpp/.hpp files here and here.
All files downloaded here must be copied to the “bike_computer/src/common” folder.
The BikeDisplay class encapsulates the zpp_lib::Display class. For compiling any application that uses this class on your nrf5340dk/nrf5340/cpuapp device, you need to add the corresponding shield with the command:
west build --shield adafruit_2_8_tft_touch_v2 bike_computer
adafruit_2_8_tft_touch_v2 screen. Files describing the shield can be found under “zephyr/boards/shields/adafruit_2_8_tft_touch_v2/”. The “shield.yml” file describes the shield’s name and main features, which can be used for display and input (touch screen) purposes, as well as for an SDHC (Secure Digital Host Controller) interface for connecting a SD card.
The BikeComputer program tasks
It is usually a good and simple approach to analyse the requirements of a
program by describing the tasks that the program must perform. These tasks can
then be implemented independently of each other, in separate classes, methods or
functions. Of course, tasks often have dependencies among them and the task
implementation must account for those dependencies. For our BikeComputer
program, and for the sake of simplicity, we will make these dependencies as
small as possible.
The tasks of our BikeComputer program are defined as follows:
- Gear task: the
BikeComputerprogram reads the current gear and gear size from the gear system. The task run time is \(100\,\mathsf{ms}\) and its period is \(800\,\mathsf{ms}\). - Speed and distance task: the
BikeComputerprogram reads the pedal rotation time and updates the speed and traveled distance. The task run time is $200\,\mathsf{ms} and it period is \(400\,\mathsf{ms}\) - Temperature task: the
BikeComputerprogram reads the temperature from the sensor device. The task run time is \(100\,\mathsf{ms}\) and its period is \(1600\,\mathsf{ms}\) - Reset task: the user may press a reset button for resetting the traveled distance. The task run time is \(100\,\mathsf{ms}\) and its period is \(800\,\mathsf{ms}\).
- Display task: The
BikeComputerprogram updates the information displayed on the LCD screen. The task run time is \(300\,\mathsf{ms}\) and its period is \(1600\,\mathsf{ms}\). The task may be splitted in two different subtasks.
The task run times used in this example are of course not very realistic - they should be much shorter - but since we are interested in analysing the different scheduling algorithms without introducing idle times, this will make the analysis simpler.
The tasks and their run times are displayed in the figure below. This figure simply shows the relative run times of all tasks and does not show how the tasks may be scheduled.
For implementing the different tasks, we use specific classes that helps in simulating some behaviors:
- The gear task is implemented using a
GearDeviceclass that allows for reading the current gear and checking for gear changes. Gear changes are implemented by using the buttons. - The speed and distance is implemented using a
PedalDeviceclass that allows for reading the current speed and traveled distance and for checking for speed changes. Speed changes are implemented by using the buttons. - The display task uses the
BikeDisplayclass that provides an API to display the information on the LCD screen (encapsulating thezpp_lib::Displayclass). - The reset task is implemented using a
ResetDeviceclass that allows for checking for reset. Reset is implemented using a button.
The GearDevice class
The GearDevice class code is given below:
GearDevice declaration
// Copyright 2025 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 gear_device.hpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief GearDevice header file (static scheduling)
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
#pragma once
// local
#include "common/constants.hpp"
// zpp_lib
#include "zpp_include/interrupt_in.hpp"
#include "zpp_include/non_copyable.hpp"
namespace bike_computer {
namespace static_scheduling {
class GearDevice : private zpp_lib::NonCopyable<GearDevice> {
public:
GearDevice() = default;
// method called for updating the bike system
uint8_t getCurrentGear();
uint8_t getCurrentGearSize() const;
private:
// data members
uint8_t _currentGear = bike_computer::kMinGear;
// buttons
zpp_lib::InterruptIn<zpp_lib::PinName::BUTTON2> _button2;
zpp_lib::InterruptIn<zpp_lib::PinName::BUTTON3> _button3;
zpp_lib::InterruptIn<zpp_lib::PinName::BUTTON4> _button4;
};
} // namespace static_scheduling
} // namespace bike_computer
GearDevice implementation
// Copyright 2025 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 gear_device.cpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief GearDevice implementation (static scheduling)
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
#include "gear_device.hpp"
// from common
#include "common/task_manager.hpp"
// zpp_lib
#include "zpp_include/time.hpp"
namespace bike_computer {
namespace static_scheduling {
uint8_t GearDevice::getCurrentGear() {
std::chrono::microseconds initialTime = zpp_lib::Time::getUpTime();
std::chrono::microseconds elapsedTime = std::chrono::microseconds::zero();
// we bound the change to one decrement/increment per call
// we increment/decrement rotation speed when button3/button4 is pressed
// while button2 is pressed
bool hasChanged = false;
while (elapsedTime <
TaskManager::getTaskComputationTime(TaskManager::TaskType::GearTaskType)) {
if (!hasChanged) {
if (_button2.read() == zpp_lib::kPolarityPressed) {
if (_button3.read() == zpp_lib::kPolarityPressed) {
_currentGear--;
hasChanged = true;
}
if (_button4.read() == zpp_lib::kPolarityPressed) {
_currentGear++;
hasChanged = true;
}
}
}
elapsedTime = zpp_lib::Time::getUpTime() - initialTime;
}
return _currentGear;
}
uint8_t GearDevice::getCurrentGearSize() const {
// simulate task computation by waiting for the required task run time
// wait_us(kTaskRunTime.count());
return bike_computer::kMaxGearSize - _currentGear;
}
} // namespace static_scheduling
} // namespace bike_computer
This code uses the static_scheduling namespace because the first
implementation of the BikeComputer program implements static scheduling. In
the getCurrentGear() method, the program checks the state of the buttons to
see if Button 3 (“Gear Down”) or Button 4 (“Gear Up”) has been pressed while
Button 2 is pressed. If so, the current gear is modified and further changes are
not allowed in the current call. The duration of the call is kTaskRunTime,
meaning that the program loops until the task run time has elapsed. Finally, the
method returns the current gear.
The PedalDevice and ResetDevice classes
Based on the GearDevice class, you must implement the PedalDevice and
ResetDevice classes that implement the classes as declared below:
PedalDevice declaration
// Copyright 2025 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 pedal_device.hpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief PedalDevice header file (static scheduling)
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
#pragma once
// local
#include "common/constants.hpp"
// zpp_lib
#include "zpp_include/interrupt_in.hpp"
#include "zpp_include/non_copyable.hpp"
namespace bike_computer {
namespace static_scheduling {
class PedalDevice : private zpp_lib::NonCopyable<PedalDevice> {
public:
PedalDevice() = default;
// method called for updating the bike system
std::chrono::milliseconds getCurrentRotationTime();
private:
// private methods
void increaseRotationSpeed();
void decreaseRotationSpeed();
// data members
std::chrono::milliseconds _pedalRotationTime = bike_computer::kInitialPedalRotationTime;
// buttons
zpp_lib::InterruptIn<zpp_lib::PinName::BUTTON2> _button2;
zpp_lib::InterruptIn<zpp_lib::PinName::BUTTON3> _button3;
zpp_lib::InterruptIn<zpp_lib::PinName::BUTTON4> _button4;
};
} // namespace static_scheduling
} // namespace bike_computer
The PedalDevice must be implemented to modify the speed when the user presses
the button 3 and button 4 without pressing button 2.
ResetDevice declaration
// Copyright 2025 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 reset_device.hpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief ResetDevice header file (static scheduling)
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
#pragma once
// std
#include <chrono>
// zpp_lib
#include "zpp_include/interrupt_in.hpp"
#include "zpp_include/non_copyable.hpp"
namespace bike_computer {
namespace static_scheduling {
class ResetDevice : private zpp_lib::NonCopyable<ResetDevice> {
public:
ResetDevice();
// method called for checking the reset status
bool checkReset();
// for computing the response time
std::chrono::microseconds getPressTime();
private:
// called when one of the buttons is pressed
void onFallButton1();
// data members
zpp_lib::InterruptIn<zpp_lib::PinName::BUTTON1> _button1;
std::chrono::microseconds _pressTime;
};
} // namespace static_scheduling
} // namespace bike_computer
The ResetDevice must implement a reset when the user presses the button 1.
In this implementation, the program must check the polarity of the input pin.
In the ResetDevice::onFallButton1() method, the press time is registered using
_pressTime = zpp_lib::Time::getUpTime();
ResetDevice::checkReset() method, the time at which the application detects
the event is registered and the time interval between the press and application
time is computed. This time interval is a good approximation of the reset
response time.
For more details about the Zephyr RTOS timer, you may read the related documentation.
Integration of Components into a BikeSystem class
Super-Loop implementation with static cyclic scheduling
The very first implementation of the BikeComputer program is the simplest one.
It implements a timeline cyclic scheduling of tasks, without event handling.
In this implementation, no event is ever generated by the system and all tasks
are executed within the infinite super-loop executed in the
BikeSystem::start() method (see description below).
Before completing the BikeSystem class and running your program, you must
first understand how tasks will be precisely scheduled in your super-loop. You
can do this by accomplishing the related
exercise.
The task periods and computation times are defined
above.
Based on this value, the shortest repeating cycle defined as the least common
multiple of task periods is easily computed to be \(1600\,\mathsf{ms}\) in this
case.
Implementation of the BikeSystem class
After implementing all device related classes and having established a cyclic
scheduling of the tasks, the next step is to declare and define a BikeSystem
class that stores all bike variables and update methods. The BikeSystem class
owns an instance of all classes implemented above and its interface is defined
as follows:
BikeSystem declaration
// Copyright 2025 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 bike_system.hpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Bike System header file (static scheduling)
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
#pragma once
// local
#include "gear_device.hpp"
#include "pedal_device.hpp"
#include "reset_device.hpp"
// zpp_lib
#include "zpp_include/display.hpp"
// from common
#include "common/bike_display.hpp"
#include "common/sensor_device.hpp"
#include "common/speedometer.hpp"
#include "common/task_manager.hpp"
namespace bike_computer {
namespace static_scheduling {
class BikeSystem : private zpp_lib::NonCopyable<BikeSystem> {
public:
// constructor
BikeSystem() = default;
// method called in main() for starting the system
[[nodiscard]] zpp_lib::ZephyrResult start();
// method called for stopping the system
void stop();
private:
// private methods
[[nodiscard]] zpp_lib::ZephyrResult initialize();
void gearTask();
void speedDistanceTask();
void temperatureTask();
void resetTask();
void displayTask1();
void displayTask2();
// stop flag, used for stopping the super-loop (set in stop())
atomic_t _stopFlag = ATOMIC_INIT(0x00);
// data member that represents the device for manipulating the gear
GearDevice _gearDevice;
uint8_t _currentGear = bike_computer::kMinGear;
uint8_t _currentGearSize = bike_computer::kMinGearSize;
// data member that represents the device for manipulating the pedal rotation
// speed/time
PedalDevice _pedalDevice;
float _currentSpeed = 0.0f;
float _traveledDistance = 0.0f;
// data member that represents the device used for resetting
ResetDevice _resetDevice;
// data member that represents the display
BikeDisplay _bikeDisplay;
// data member that represents the device for counting wheel rotations
Speedometer _speedometer;
// data member that represents the sensor device
SensorDevice _sensorDevice;
float _currentTemperature = 0.0f;
// used for managing tasks info
TaskManager _taskManager;
};
} // namespace static_scheduling
} // namespace bike_computer
Most of the implementation of the BikeSystem class is given below:
BikeSystem implementation (partial)
// Copyright 2025 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 bike_system.cpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Bike System implementation (static scheduling)
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
#include "bike_system.hpp"
// std
#include <chrono>
// zephyr
// false positive cpplint warning
// NOLINTNEXTLINE(build/include_order)
#include <zephyr/logging/log.h>
// zpp_lib
#include "zpp_include/this_thread.hpp"
#include "zpp_include/time.hpp"
#include "zpp_include/work_queue.hpp"
LOG_MODULE_DECLARE(bike_computer, CONFIG_APP_LOG_LEVEL);
namespace bike_computer {
namespace static_scheduling {
zpp_lib::ZephyrResult BikeSystem::start() {
LOG_INF("Starting Super-Loop without event handling");
auto res = initialize();
if (!res) {
LOG_ERR("Init failed: %d", (int)res.error());
return res;
}
LOG_DBG("Starting super-loop");
// initialize the task manager phase
_taskManager.initializePhase();
uint32_t iteration = 0;
static constexpr uint32_t iterationsForFixingDrift = 10;
while (true) {
auto startTime = zpp_lib::Time::getUpTime();
// TODO: implement calls to different tasks based on computed schedule
// register the time at the end of the cyclic schedule period and print the
// elapsed time for the period
std::chrono::microseconds endTime = zpp_lib::Time::getUpTime();
const auto cycle =
std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime);
LOG_DBG("Repeating cycle time is %" PRIu64 " milliseconds", cycle.count());
if (atomic_test_bit(&_stopFlag, 1)) {
break;
}
// fix the schedule drift to pass the tests
// this demonstrates that static scheduling is very sensitive to overload
if (iteration % iterationsForFixingDrift == 0) {
_taskManager.initializePhase();
}
}
return res;
}
void BikeSystem::stop() {
atomic_set_bit(&_stopFlag, 1);
if (gTTCE.isStarted()) {
gTTCE.stop();
}
}
zpp_lib::ZephyrResult BikeSystem::initialize() {
// initialize the display
auto res = _bikeDisplay.initialize();
if (!res) {
LOG_ERR("Cannot initialize display: %d", (int)res.error());
return res;
}
// initialize the sensor device
res = _sensorDevice.initialize();
if (!res) {
LOG_ERR("Sensor not present or initialization failed: %d", (int)res.error());
}
return zpp_lib::ZephyrResult();
}
void BikeSystem::gearTask() {
// gear task
_taskManager.registerTaskStart(TaskManager::TaskType::GearTaskType);
// no need to protect access to data members (single threaded)
_currentGear = _gearDevice.getCurrentGear();
_currentGearSize = _gearDevice.getCurrentGearSize();
_taskManager.simulateComputationTime(TaskManager::TaskType::GearTaskType);
}
void BikeSystem::speedDistanceTask() {
// speed and distance task
_taskManager.registerTaskStart(TaskManager::TaskType::SpeedTaskType);
const auto pedalRotationTime = _pedalDevice.getCurrentRotationTime();
_speedometer.setCurrentRotationTime(pedalRotationTime);
_speedometer.setGearSize(_currentGearSize);
// no need to protect access to data members (single threaded)
_currentSpeed = _speedometer.getCurrentSpeed();
_traveledDistance = _speedometer.getDistance();
_taskManager.simulateComputationTime(TaskManager::TaskType::SpeedTaskType);
}
void BikeSystem::temperatureTask() {
_taskManager.registerTaskStart(TaskManager::TaskType::TemperatureTaskType);
// TO DO: read temperature from _sensorDevice
// simulate task computation by waiting for the required task computation time
_taskManager.simulateComputationTime(TaskManager::TaskType::TemperatureTaskType);
}
void BikeSystem::resetTask() {
_taskManager.registerTaskStart(TaskManager::TaskType::ResetTaskType);
if (_resetDevice.checkReset()) {
std::chrono::microseconds responseTime =
zpp_lib::Time::getUpTime() - _resetDevice.getPressTime();
LOG_INF("Reset task: response time is %" PRIu64 " usecs", responseTime.count());
_speedometer.reset();
}
_taskManager.simulateComputationTime(TaskManager::TaskType::ResetTaskType);
}
void BikeSystem::displayTask1() {
_taskManager.registerTaskStart(TaskManager::TaskType::DisplayTask1Type);
// TODO: update gear, speed and distance displayed on screen
_taskManager.simulateComputationTime(TaskManager::TaskType::DisplayTask1Type);
}
void BikeSystem::displayTask2() {
_taskManager.registerTaskStart(TaskManager::TaskType::DisplayTask2Type);
// TODO: update temperature on screen
_taskManager.simulateComputationTime(TaskManager::TaskType::DisplayTask2Type);
}
} // namespace static_scheduling
} // namespace bike_computer
First understand how the class is written, what the data members are and what
the methods do. You must then complete the class implementation (marked as
TODO in the code).
Integration of your BikeSystem class in your main() function
Your main() function must create a BikeSystem instance on the stack and it
must start it as shown below:
static_scheduling::BikeSystem bikeSystem;
bikeSystem.start();
start() method call and that
the BikeComputer program will thus be executed by the main thread.
Code instrumentation for measuring task periods
For checking that period and computation times are correct, the tasks need to be
instrumented. As you can see for instance in the BikeSystem::gearTask()
method, at the start of each method representing a task, the current time is
registered by calling _taskManager.registerTaskStart(). The task is then run and the method calls the
_taskManager.simulateComputationTime(). This allows for registering task time
information in the TaskManager instance.
If you run your BikeComputer program with trace level set to
CONFIG_APP_LOG_LEVEL_DBG, you should see an output similar to the one below on
the console:
Log with the various task times
[00:00:00.462,188]
[00:00:00.469,116]
[00:00:00.477,111]
[00:00:00.483,764]
[00:00:00.491,485]
[00:00:00.497,924]
[00:00:00.503,509]
[00:00:01.229,370]
[00:00:01.322,998]
[00:00:01.521,392]
[00:00:01.620,300]
[00:00:01.819,122]
[00:00:02.018,646]
[00:00:02.118,041]
[00:00:02.317,321]
[00:00:02.416,778]
[00:00:02.516,296]
[00:00:02.715,789]
[00:00:02.815,429]
[00:00:02.827,880]
From this log, one can confirm that:
- The Major Cycle or Repeating Cycle Time are as expected at \(1600\,\mathsf{ms}\).
- The periods and computation times of the Gear, Speed, Temperature and Reset tasks are as expected.
- The Display task is splitted into 2 subtasks, with correct periods for a total computation time of \(300\,\mathsf{ms}\), as expected.
- Some tasks need to be dropped to respect the task periods. This demonstrates the sensitivity of the super-loop implementation to computation times. In our case, logging information affects computation times, so tracing should be used instead.
Run the test program
To check your implementation of the BikeSystem::start() method, you must
successfully run the test program below. This program validates that the tasks
run at the correct periods for the correct computation times. If the test fails,
you must identify and resolve any scheduling errors.
BikeSystem Scheduling Test Program (part 1)
// Copyright 2025 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 test_bike_system_part1.cpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Test program for the BikeSystem class (codelab part 1)
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
// zephyr
#include <zephyr/logging/log.h>
#include <zephyr/ztest.h>
// std
#include <chrono>
#include <cstdio>
// zpp_lib
#include "zpp_include/this_thread.hpp"
#include "zpp_include/thread.hpp"
// bike computer
#include "static_scheduling/bike_system.hpp"
LOG_MODULE_REGISTER(bike_system, CONFIG_APP_LOG_LEVEL);
// for ms or s literals
using namespace std::literals;
static constexpr std::chrono::milliseconds testDuration = 10s;
// test_bike_system_static handler function
ZTEST(bike_system_part1, test_bike_system_static) {
// create the BikeSystem instance
static bike_computer::static_scheduling::BikeSystem bikeSystem;
// run the bike system in a separate thread
zpp_lib::Thread thread(zpp_lib::PreemptableThreadPriority::PriorityNormal,
"Test BS static");
LOG_DBG("Starting thread");
auto res = thread.start(
std::bind(&bike_computer::static_scheduling::BikeSystem::start, &bikeSystem));
zassert_true(res, "Could not start thread");
// let the bike system run for the test duration
zpp_lib::ThisThread::sleep_for(testDuration);
// stop the bike system
bikeSystem.stop();
// wait for thread to terminate
res = thread.join();
zassert_true(res, "Could not join thread");
}
ZTEST_SUITE(bike_system_part1, NULL, NULL, NULL, NULL, NULL);
Integrate the Display with Twister
To integrate the display shield when testing on device, you need to add the following lines in the “testcase.yml” file:
tests:
bike_computer.bike_system_part1:
...
extra_args: SHIELD=adafruit_2_8_tft_touch_v2
The CONFIG_TEST=y configuration is added when running the test program. With
this option enabled, the TaskManager class will verify the computation times and
periods of tasks. If any of these is not within the expected range, it will
assert. For accounting for some variations due to imprecisions and extra
statements, such as logging statements, the TaskManager class allows for a
variation of kAllowedDelta. This constant is set to \(1\,\mathsf{ms}\). You may
need to adjust the allowed delta. If it is the case, you then need to document
this exception.
The Impact of Logging
The TaskManager::logTaskTime() method is useful for understanding the
program behavior and for debugging purposes. However, logging has an impact
on system load and behavior, and thus on the ability of tasks to fulfil
their timing constraints. To reduce this impact, you may:
- Remove the calls to logging functions, once your program is functional.
- Log only when an unexpected behavior is detected, as it is implemented in test mode.
- Replace logging calls with tracing calls.
Code instrumentation for measuring the response time
In theBikeSystem::resetTask() method, the speedometer is reset when the user
presses the button. The program also computes the task response time in this
method, which is the time elapsed between the button being pressed (or, more
precisely, the button press being detected) and the reset request being handled.
Observe the implementation of BikeSystem::resetTask() method to understand to
understand how the reset response time is calculated.
Press the button and observe the different response times obtained from this computation. If you do so, you should observe traces on the console similar to:
Log with multiple response times for the reset task
[00:00:12.429,992]
…
[00:00:41.636,016]
…
[00:01:10.442,596]
…
When running the program, you should also ensure that the push button is pressed for long enough to allow the event to be detected in the main program.
Question 1: Response time of the reset event
When running multiple tests to compute the response time of the reset event, you should observe the following:
- There is a large variation in the response time values, from a few milliseconds to hundreds of milliseconds.
- If you do not press long enough on the push button, the event may be missed and no reset happens.
Based on the program itself and on the task scheduling, explain these two behaviors. Explain also why such behaviors may be problematic.
Software Architecture
The class documented in this codelab follow the follow class architecture:
BikeComputer programMake sure that you have not deviated from this architecture. Please note that this class diagram does not document the constructors and method arguments.
Limitations of this implementation
Varying and large response times are a problem that needs to be solved. In the next part of the codelab, we will implement an asynchronous button press mechanism to allow instantaneous handling of the event. We will then start making the next version of the program event-driven!
Wrap-Up
Before moving to the next implementation of the BikeComputer program in the
next codelab, make sure that you have accomplished the following:
- All required classes are implemented and functional.
- The
mainfunction is implemented and functional. - There is no significant deviations from the class architecture as documented above.
- The three test programs (
test_sensor_device,test_speedometerandtest_bike_system_part1) run successfully, as documented in the codelab. - The
pre-commitconfiguration file has been modified to include all files added in this codelab. All software quality tools succeed, changes are committed and pushed to your repository.