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 bike_computer program implements the following functionalities:
- Bike gear: the bike’s gear system provides a value representing the current
gear that the
bike_computerprogram can read. On our system, gear may be changed using the buttons. - Pedal rotation: the
bike_computerprogram 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 environmental temperature.
- Display: the user can read the information provided by the
bike_computerprogram on an LCD screen.
In this and the following codelabs, we will explore different approaches to implementing
the bike_computer program. We will analyze the advantages and disadvantages of
these implementations. We will also study some important performance indicators, such
as the reset response time, which is the delay between the user pressing the reset
button and the LCD displaying the reset values.
What you’ll build
In this codelab, you will program a simple bike_computer application using a
a Timeline Cyclic Scheduling algorithm with a Super Loop mechanism.
In the next codelab, we will implement Timeline Cyclic Scheduling scheduling using
the Time-Triggered Cyclic Executive (TTCE)_ mechanism.
What you’ll learn
- How to develop a embedded program using a Super Loop mechanism.
- How to schedule the different tasks of an embedded program using {{ ttce_scheduling }}.
- The limitations of the proposed Super Loop and Time-Triggered Cyclic Executive mechanisms.
- 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.
- All preceding codelabs are prerequisites for this codelab.
The different implementations
The bike_computer 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 a Time-Triggered Cyclic Executive program with Timeline Cyclic Scheduling of tasks. In these implementations, the system never generates any event and all tasks are executed periodically.
- We will analyze the limitations of this approach, and, based on these, implement
several event-driven programs (parts 2 and 3 of the
bike_computercodelab).
To start the different implementations, you must create a new Zephyr RTOS
bike_computer application under the bike_computer folder. This can be done
with
just create-app bike_computer
We will implement the different versions of the bike_computer computer app program using the following
approach. We implement the different versions into a single program and keep them in separate subfolders,
distinguishing the entry point in the main() function. This ensures that all versions are implemented within the
same project. To clearly differentiate between the versions within the program, we use different namespaces for each one.
Conceive and Implement the bike_computer 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 bike_computer program.
We start by implementing the classes that will be used by all
bike_computer program implementations: the SensorDevice and Speedometer
classes. As these classes will be used by all bike_computer 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 std::literals::chrono_literals::operator""ms;
// 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 BME280 sensor and hide some
hardware details, our bike_computer 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 {
public:
// constructor
SensorDevice() = default;
// method for initializing the device
[[nodiscard]] zpp_lib::ZephyrResult initialize();
// methods used for reading sensor measurements
[[nodiscard]] zpp_lib::ZephyrResult read_temperature(float& temperature);
[[nodiscard]] zpp_lib::ZephyrResult read_humidity(float& humidity);
private:
// data members
const struct device* _sensor_device = nullptr;
};
} // namespace bike_computer
It uses 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
// bike_computer
#include "common/sensor_device.hpp"
// zpp_lib
#include "zpp_include/zpp_assert.hpp"
#include "zpp_include/zpp_log.hpp"
#include "zpp_include/zpp_test.hpp"
ZPP_LOG_MODULE_REGISTER(bike_computer, CONFIG_APP_LOG_LEVEL);
ZPP_ZTEST(sensor_device, test_sensor_device) {
// create the SensorDevice instance
bike_computer::SensorDevice sensor_device;
auto res = sensor_device.initialize();
if (!res) {
zpp_zassert_true(res, "Cannot initialize sensor device: %d", static_cast<int>(res.error()));
}
float temperature = 0.0F;
res = sensor_device.read_temperature(temperature);
if (!res) {
zpp_zassert_true(res, "Cannot initialize sensor device: %d", static_cast<int>(res.error()));
}
static constexpr float kTemperatureRange = 20.0F;
static constexpr float kMeanTemperature = 15.0F;
zpp_zassert_within(temperature, kMeanTemperature, kTemperatureRange, "Temperature outside range: %f", static_cast<double>(temperature));
float humidity = 0.0F;
res = sensor_device.read_humidity(humidity);
if (!res) {
zpp_zassert_true(res, "Cannot initialize sensor device: %d", static_cast<int>(res.error()));
}
static constexpr float kHumidityRange = 45.0F;
static constexpr float kMeanHumidity = 50.0F;
zpp_zassert_within(humidity, kMeanHumidity, kHumidityRange, "Humidity outside range: %f", static_cast<double>(humidity));
}
ZPP_ZTEST_SUITE(sensor_device, nullptr, nullptr, nullptr, nullptr, nullptr);
If you integrate this program into the tests folder of your bike_computer
program in the appropriate subfolder, you may validate its correct
implementation by using the command
just test bike_computer/tests "your_map_file.yaml"
Emulation on QEMU_X86
To run the SensorDevice test on qemu_x86, the BME280 sensor must be fully emulated
on this platform. Since the sensor functionality is encapsulated in the SensorDevice class,
we can emulate the sensor more easily. In the class implementation, use the #if !CONFIG_EMUL pragma to
differentiate the implementation for the qemu_x86 device and implement a simple emulation that
delivers sensor values without interacting with a physical sensor. Although 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 application in the precommit phase by adding
the following hook:
.pre-commit-config.yamlOnce the precommit phase succeeds, you may commit and push your changes.
- id: clang-tidy-bike-computer-sensor-device name: clang-tidy-bike-computer-sensor-device entry: python deps/zpp_lib/scripts/run_clang_tidy.py --app bike_computer/tests/sensor_device --configs "log+debug+gpio+display+sensor+test+phase_a" --wd "." language: system pass_filenames: true require_serial: true files: ^(bike_computer/tests/sensor_device)/.*\.(c|cc|cpp|cxx)$
The Speedometer Class
Another class that will be used by all bike_computer 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
#include <functional>
#endif // CONFIG_TEST
namespace bike_computer {
using std::literals::chrono_literals::operator""ms;
using std::literals::chrono_literals::operator""us;
class Speedometer : private zpp_lib::NonCopyable {
public:
Speedometer();
// method used for setting the current pedal rotation time
void set_current_pedal_rotation_time(const std::chrono::milliseconds& current_rotation_time);
// method used for setting/getting the current gear
void set_gear_size(uint8_t gear_size);
// method called for getting the current speed (expressed in km / h)
[[nodiscard]] float get_current_speed() const;
// method called for getting the current traveled distance (expressed in km)
[[nodiscard]] float get_traveled_distance();
// method called for resetting the traveled distance
void reset();
// methods used for tests only
#if CONFIG_TEST == 1
[[nodiscard]] uint8_t get_gear_size() const {
return _gear_size;
}
static float s_get_wheel_circumference() {
return kWheelCircumference;
}
static uint8_t s_get_tray_size() {
return kTraySize;
}
[[nodiscard]] std::chrono::milliseconds get_current_pedal_rotation_time() const {
return _pedal_rotation_time;
}
using CallbackFunction = std::function<void()>;
void set_on_reset_callback(CallbackFunction cb) {
_cb = std::move(cb);
}
#endif // CONFIG_TEST == 1
private:
// private methods
void compute_speed();
float compute_traveled_distance();
// 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 _last_time = std::chrono::microseconds::zero();
std::chrono::milliseconds _pedal_rotation_time = kInitialPedalRotationTime;
// data members
// LowPowerTicker _ticker;
float _current_speed = 0.0F;
zpp_lib::Mutex _total_distance_mutex;
float _total_distance = 0.0F;
uint8_t _gear_size = 1;
#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
// std
#include <chrono>
#include <cstdio>
// zpp_lib
#include "zpp_include/this_thread.hpp"
#include "zpp_include/zpp_assert.hpp"
#include "zpp_include/zpp_log.hpp"
#include "zpp_include/zpp_test.hpp"
// bike_computer
#include "common/speedometer.hpp"
ZPP_LOG_MODULE_REGISTER(bike_computer, 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;
// function called by test handler functions for verifying the current speed
// Internal function
void check_current_speed(const std::chrono::milliseconds& pedal_rotation_time,
uint8_t tray_size,
uint8_t gear_size, // NOLINT(bugprone-easily-swappable-parameters)
float wheel_circumference, // NOLINT(bugprone-easily-swappable-parameters)
float current_speed) {
// compute the number of pedal rotation per hour
static constexpr auto kMillisecondsPerSecond = 1000;
static constexpr auto kSecondsPerHour = 3600;
static constexpr auto kMillisecondsPerHour = kMillisecondsPerSecond * kSecondsPerHour;
float pedal_rotations_per_hour = static_cast<float>(kMillisecondsPerHour) / static_cast<float>(pedal_rotation_time.count());
// compute the expected speed in km / h
// first compute the distance in meter for each pedal turn
float tray_gear_ratio = static_cast<float>(tray_size) / static_cast<float>(gear_size);
float distance_per_pedal_turn = tray_gear_ratio * wheel_circumference;
float expected_speed = (distance_per_pedal_turn / kMillisecondsPerSecond) * pedal_rotations_per_hour;
ZPP_LOG_INF(" Expected speed is %f, current speed is %f\n", static_cast<double>(expected_speed), static_cast<double>(current_speed));
zpp_zassert_within(current_speed, expected_speed, kAllowedSpeedDelta, "Current speed is not within bounds");
}
// compute the traveled distance for a time interval
// Internal function
float compute_distance(const std::chrono::milliseconds& pedal_rotation_time,
uint8_t tray_size,
uint8_t gear_size, // NOLINT(bugprone-easily-swappable-parameters)
float wheel_circumference,
const std::chrono::milliseconds& travel_time) {
// compute the number of pedal rotation during travel time
// both times are expressed in ms
float pedal_rotations = static_cast<float>(travel_time.count()) / static_cast<float>(pedal_rotation_time.count());
// compute the distance in meter for each pedal turn
float tray_gear_ratio = static_cast<float>(tray_size) / static_cast<float>(gear_size);
float distance_per_pedal_turn = tray_gear_ratio * wheel_circumference;
// distancePerPedalTurn is expressed in m, divide per kMetersPerKilometer for a distance in km
static constexpr float kMetersPerKilometer = 1000.0F;
return (distance_per_pedal_turn * pedal_rotations) / kMetersPerKilometer;
}
// function called by test handler functions for verifying the distance traveled
void check_distance(const std::chrono::milliseconds& pedal_rotation_time,
uint8_t tray_size,
uint8_t gear_size,
float wheel_circumference,
const std::chrono::milliseconds& travel_time,
float distance) {
// distancePerPedalTurn is expressed in m, divide per 1000 for a distance in km
float expected_distance = compute_distance(pedal_rotation_time, tray_size, gear_size, wheel_circumference, travel_time);
ZPP_LOG_INF(" Expected distance is %f, current distance is %f\n", static_cast<double>(expected_distance), static_cast<double>(distance));
zpp_zassert_within(distance, expected_distance, kAllowedDistanceDelta, "Current distance is not within bounds");
}
// test the speedometer by modifying the gear
ZPP_ZTEST(speedometer, test_gear_size) {
// create a speedometer instance
bike_computer::Speedometer speedometer;
// get speedometer constant values (for this test)
auto tray_size = bike_computer::Speedometer::s_get_tray_size();
auto wheel_circumference = bike_computer::Speedometer::s_get_wheel_circumference();
auto pedal_rotation_time = speedometer.get_current_pedal_rotation_time();
for (uint8_t gear_size = bike_computer::kMinGearSize; gear_size <= bike_computer::kMaxGearSize; gear_size++) {
// set the gear
ZPP_LOG_INF("Testing gear size %d\n", gear_size);
speedometer.set_gear_size(gear_size);
// get the current speed
auto current_speed = speedometer.get_current_speed();
// check the speed against the expected one
check_current_speed(pedal_rotation_time, tray_size, gear_size, wheel_circumference, current_speed);
}
}
// test the speedometer by modifying the pedal rotation speed
ZPP_ZTEST(speedometer, test_rotation_speed) {
// create a speedometer instance
bike_computer::Speedometer speedometer;
// set the gear size
speedometer.set_gear_size(bike_computer::kMaxGearSize);
// get speedometer constant values
auto tray_size = bike_computer::Speedometer::s_get_tray_size();
auto wheel_circumference = bike_computer::Speedometer::s_get_wheel_circumference();
auto gear_size = speedometer.get_gear_size();
// first test increasing rotation speed (decreasing rotation time)
auto pedal_rotation_time = speedometer.get_current_pedal_rotation_time();
while (pedal_rotation_time > bike_computer::kMinPedalRotationTime) {
// decrease the pedal rotation time
pedal_rotation_time -= bike_computer::kDeltaPedalRotationTime;
speedometer.set_current_pedal_rotation_time(pedal_rotation_time);
// get the current speed
auto current_speed = speedometer.get_current_speed();
// check the speed against the expected one
check_current_speed(pedal_rotation_time, tray_size, gear_size, wheel_circumference, current_speed);
}
// second test decreasing rotation speed (increasing rotation time)
pedal_rotation_time = speedometer.get_current_pedal_rotation_time();
while (pedal_rotation_time < bike_computer::kMaxPedalRotationTime) {
// increase the pedal rotation time
pedal_rotation_time += bike_computer::kDeltaPedalRotationTime;
speedometer.set_current_pedal_rotation_time(pedal_rotation_time);
// get the current speed
auto current_speed = speedometer.get_current_speed();
// check the speed against the expected one
check_current_speed(pedal_rotation_time, tray_size, gear_size, wheel_circumference, current_speed);
}
}
// test the speedometer by modifying the pedal rotation speed
ZPP_ZTEST(speedometer, test_distance) {
// create a speedometer instance
bike_computer::Speedometer speedometer;
// set the gear size
speedometer.set_gear_size(bike_computer::kMaxGearSize);
// get speedometer constant values
auto tray_size = bike_computer::Speedometer::s_get_tray_size();
auto wheel_circumference = bike_computer::Speedometer::s_get_wheel_circumference();
auto gear_size = speedometer.get_gear_size();
auto pedal_rotation_time = speedometer.get_current_pedal_rotation_time();
// test different travel times
using std::literals::chrono_literals::operator""s;
using std::literals::chrono_literals::operator""ms;
static constexpr std::array<std::chrono::milliseconds, 4> kTravelTimes = {500ms, 1000ms, 5s, 10s};
// first check travel distance without changing gear and rotation speed
auto total_travel_time = std::chrono::milliseconds::zero();
for (auto travel_time : kTravelTimes) {
// run for the travel time and get the distance
zpp_lib::ThisThread::sleep_for(travel_time);
// get the traveled distance
auto traveled_distance = speedometer.get_traveled_distance();
// accumulate travel time
total_travel_time += travel_time;
// check the distance vs the expected one
check_distance(pedal_rotation_time, tray_size, gear_size, wheel_circumference, total_travel_time, traveled_distance);
}
// now change gear at each time interval
auto expected_distance = speedometer.get_traveled_distance();
for (auto travel_time : kTravelTimes) {
// update the gear size
gear_size++;
speedometer.set_gear_size(gear_size);
// run for the travel time and get the distance
zpp_lib::ThisThread::sleep_for(travel_time);
// compute the expected distance for this time segment
float distance = compute_distance(pedal_rotation_time, tray_size, gear_size, wheel_circumference, travel_time);
expected_distance += distance;
// get the distance traveled
auto traveled_distance = speedometer.get_traveled_distance();
printk(" Expected distance is %f, current distance is %f\n",
static_cast<double>(expected_distance),
static_cast<double>(traveled_distance));
zpp_zassert_within(traveled_distance, expected_distance, kAllowedDistanceDelta);
}
// now change rotation speed at each time interval
expected_distance = speedometer.get_traveled_distance();
for (auto travel_time : kTravelTimes) {
// update the rotation speed
pedal_rotation_time += bike_computer::kDeltaPedalRotationTime;
speedometer.set_current_pedal_rotation_time(pedal_rotation_time);
// run for the travel time and get the distance
zpp_lib::ThisThread::sleep_for(travel_time);
// compute the expected distance for this time segment
float distance = compute_distance(pedal_rotation_time, tray_size, gear_size, wheel_circumference, travel_time);
expected_distance += distance;
// get the distance traveled
auto traveled_distance = speedometer.get_traveled_distance();
printk(" Expected distance is %f, current distance is %f\n",
static_cast<double>(expected_distance),
static_cast<double>(traveled_distance));
zpp_zassert_within(traveled_distance, expected_distance, kAllowedDistanceDelta);
}
}
// test the speedometer by modifying the pedal rotation speed
ZPP_ZTEST(speedometer, test_reset) {
// create a speedometer instance
bike_computer::Speedometer speedometer;
// set the gear size
speedometer.set_gear_size(bike_computer::kMinGearSize);
// get speedometer constant values
auto tray_size = bike_computer::Speedometer::s_get_tray_size();
auto wheel_circumference = bike_computer::Speedometer::s_get_wheel_circumference();
auto gear_size = speedometer.get_gear_size();
auto pedal_rotation_time = speedometer.get_current_pedal_rotation_time();
// travel for 5 seconds
using std::literals::chrono_literals::operator""ms;
static constexpr auto kTravelTime = 5000ms;
zpp_lib::ThisThread::sleep_for(kTravelTime);
// check the expected distaance traveled
auto expected_distance = compute_distance(pedal_rotation_time, tray_size, gear_size, wheel_circumference, kTravelTime);
// get the distance traveled
auto traveled_distance = speedometer.get_traveled_distance();
ZPP_LOG_INF(" Expected distance is %f, current distance is %f\n",
static_cast<double>(expected_distance),
static_cast<double>(traveled_distance));
zpp_zassert_within(traveled_distance, expected_distance, kAllowedDistanceDelta);
// reset the speedometer
speedometer.reset();
// traveled distance should now be zero
traveled_distance = speedometer.get_traveled_distance();
ZPP_LOG_INF(" Expected distance is %f, current distance is %f\n", 0.0, static_cast<double>(traveled_distance));
zpp_zassert_within(0.0F, traveled_distance, kAllowedDistanceDelta);
// travel again for 5 seconds
zpp_lib::ThisThread::sleep_for(kTravelTime);
// reset the speedometer without getting the distance
speedometer.reset();
// travel again for 5 seconds
zpp_lib::ThisThread::sleep_for(kTravelTime);
// get the distance traveled
traveled_distance = speedometer.get_traveled_distance();
ZPP_LOG_INF(" Expected distance is %f, current distance is %f\n",
static_cast<double>(expected_distance),
static_cast<double>(traveled_distance));
zpp_zassert_within(traveled_distance, expected_distance, kAllowedDistanceDelta);
}
ZPP_ZTEST_SUITE(speedometer, nullptr, nullptr, nullptr, nullptr, nullptr);
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
// std
#include <chrono>
// zpp_lib
#include "zpp_include/time.hpp"
#include "zpp_include/zpp_assert.hpp"
#include "zpp_include/zpp_log.hpp"
ZPP_LOG_MODULE_DECLARE(bike_computer, CONFIG_APP_LOG_LEVEL);
namespace bike_computer {
Speedometer::Speedometer() : _last_time(zpp_lib::Time::get_uptime()) {}
void Speedometer::set_current_pedal_rotation_time(const std::chrono::milliseconds& current_rotation_time) {
if (_pedal_rotation_time != current_rotation_time) {
// compute distance before changing the rotation time
compute_traveled_distance();
// change pedal rotation time
_pedal_rotation_time = current_rotation_time;
// compute speed with the new pedal rotation time
compute_speed();
}
}
void Speedometer::set_gear_size(uint8_t gear_size) {
if (_gear_size != gear_size) {
// compute distance before changing the gear size
compute_traveled_distance();
// change gear size
_gear_size = gear_size;
// compute speed with the new gear size
compute_speed();
}
}
float Speedometer::get_current_speed() const {
return _current_speed;
}
float Speedometer::get_traveled_distance() {
// make sure to update the distance traveled
return compute_traveled_distance();
}
void Speedometer::reset() {
#if CONFIG_TEST
if (_cb != nullptr) {
_cb();
}
#endif // CONFIG_TEST
// TODO(Student)
}
#if CONFIG_TEST
uint8_t Speedometer::get_gear_size() const {
return _gear_size;
}
float Speedometer::get_wheel_circumference() const {
return kWheelCircumference;
}
uint8_t Speedometer::get_tray_size() const {
return kTraySize;
}
std::chrono::milliseconds Speedometer::get_current_pedal_rotation_time() const {
return _pedal_rotation_time;
}
void Speedometer::set_on_reset_callback(CallbackFunction cb) {
_cb = cb;
}
#endif // CONFIG_TEST
void Speedometer::compute_speed() {
// 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(Student)
float Speedometer::compute_traveled_distance() {
// 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(Student)
} // namespace bike_computer
If you implement these methods correctly and run the following command:
just test bike_computer/tests/speedometer "your_map_file.yaml"
twister-out/twister.log file
to understand and fix the problem with your implementation.
Once your implementation has been successfully tested, you may commit and push
the changes as documented above. Remember to add the appropriate hook in the pre-commit configuration
file.
Integrate the Other Required Software Components
In addition to the classes described above and below, you need to integrate further classes
to implement the bike_computer application:
- The
BikeDisplayclass provides an API to display information on the LCD screen. It is used in theBikeSystemclass. The.cpp/.hppfiles can be downloaded here and here. - Resources used in the
BikeDisplaytask can be downloaded here and copied to thebike_computer/src/common/resourcesfolder. - 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/.hppfiles here and here.
All files downloaded here must be copied to the appropriate folders.
The BikeDisplay class encapsulates the zpp_lib::Display class. To compile any application that uses this class
on your nrf5340dk/nrf5340/cpuapp device, the provided scripts automatically add the corresponding shield by adding the
--shield adafruit_2_8_tft_touch_v2 argument to the build commands.
In the context of Zephyr RTOS, a shield is an add-on board that attaches to the main board to extend its features,
as explained here. In our case,
the shield is the Adafruit 2.8" TFT Touch v2 screen. Files describing the shield can be found under
deps/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 to connect an SD card.
The bike_computer Program Tasks
It is usually a good, simple approach to analyze a program’s requirements by describing the tasks it must perform.
These tasks can then be implemented independently of each other in separate classes, methods, or functions.
Of course, tasks often depend on each other, so the implementation must account for these dependencies.
For our bike_computer program, and for the sake of simplicity, we will minimize these dependencies.
The tasks of our bike_computer program are defined as follows:
- Gear task: the
bike_computerprogram 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
bike_computerprogram 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
bike_computerprogram 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
bike_computerprogram 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. However, since we are interested in analyzing the different application behaviors without introducing idle times, using these times makes the analysis simpler.
The figure below shows the tasks and their run times. This figure simply shows the relative run times of all the tasks and does not show how they may be scheduled.
We use specific classes to implement different tasks and simulate certain behaviors:
- The gear task is implemented using the
GearDeviceclass, which allows to read the current gear and check for gear changes. Gear changes are implemented by pressing the buttons. - The speed and distance task is implemented using the
PedalDeviceclass, which allows to read the current speed and traveled distance, as well as to check speed changes. Speed changes are implemented by pressing 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 the
ResetDeviceclass, which allows to check for reset. Reset is implemented by pressing 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::static_scheduling {
class GearDevice : private zpp_lib::NonCopyable {
public:
GearDevice();
// method called for updating the bike system
[[nodiscard]] uint8_t get_current_gear();
[[nodiscard]] uint8_t get_current_gear_size() const;
private:
// data members
uint8_t _current_gear = bike_computer::kMinGear;
// buttons
zpp_lib::InterruptIn _button2;
zpp_lib::InterruptIn _button3;
zpp_lib::InterruptIn _button4;
};
} // namespace bike_computer::static_scheduling
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::static_scheduling {
GearDevice::GearDevice()
: _button2(zpp_lib::InterruptIn::PinName::BUTTON2), _button3(zpp_lib::InterruptIn::PinName::BUTTON3),
_button4(zpp_lib::InterruptIn::PinName::BUTTON4) {}
uint8_t GearDevice::get_current_gear() {
std::chrono::microseconds initial_time = zpp_lib::Time::get_uptime();
std::chrono::microseconds elapsed_time = 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 has_changed = false;
while (elapsed_time < TaskManager::get_task_computation_time(TaskManager::TaskType::GearTaskType)) {
if (!has_changed) {
if (_button2.read() == zpp_lib::kPolarityPressed) {
if (_button3.read() == zpp_lib::kPolarityPressed) {
if (_current_gear > bike_computer::kMinGear) {
_current_gear--;
}
has_changed = true;
}
if (_button4.read() == zpp_lib::kPolarityPressed) {
if (_current_gear < bike_computer::kMaxGear) {
_current_gear++;
}
has_changed = true;
}
}
}
elapsed_time = zpp_lib::Time::get_uptime() - initial_time;
}
return _current_gear;
}
uint8_t GearDevice::get_current_gear_size() const {
// simulate task computation by waiting for the required task run time
// wait_us(kTaskRunTime.count());
return bike_computer::kMaxGearSize - _current_gear;
}
} // namespace bike_computer::static_scheduling
This code uses the static_scheduling namespace because the first
implementation of the bike_computer program implements static scheduling. In
the get_current_gear() 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 method loops until the task run time elapses.
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::static_scheduling {
class PedalDevice : private zpp_lib::NonCopyable {
public:
PedalDevice();
// method called for updating the bike system
std::chrono::milliseconds get_current_rotation_time();
private:
// private methods
void increase_rotation_speed();
void decrease_rotation_speed();
// data members
std::chrono::milliseconds _pedal_rotation_time = bike_computer::kInitialPedalRotationTime;
// buttons
zpp_lib::InterruptIn _button2;
zpp_lib::InterruptIn _button3;
zpp_lib::InterruptIn _button4;
};
} // namespace bike_computer::static_scheduling
The PedalDevice implementation must modify the speed when the user presses
the Button 3 or 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"
#include "zpp_include/registration_token.hpp"
namespace bike_computer::static_scheduling {
class ResetDevice : private zpp_lib::NonCopyable {
public:
// constructor and destructor
ResetDevice();
// method called for checking the reset status
bool check_reset();
// for computing the response time
std::chrono::microseconds get_press_time();
private:
// called when one of the buttons is pressed
void on_fall_button1();
// data members
zpp_lib::InterruptIn _button1;
zpp_lib::RegistrationToken _button1_token;
std::chrono::microseconds _press_time{std::chrono::microseconds::zero()};
};
} // namespace bike_computer::static_scheduling
The ResetDevice must implement a reset when the user presses Button 1.
In this implementation, the program must check the polarity of the input pin.
In the ResetDevice::on_fall_button1() method, the press time is registered using
_pressTime = zpp_lib::Time::get_uptime();
BikeComputer class.
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 bike_computer 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 the 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 establishing 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 each of the 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
// stl
#include <atomic>
// 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::static_scheduling {
class BikeSystem {
public:
// constructor
BikeSystem() = default;
// destructor
~BikeSystem();
/** Explicity prevent (move) copy and assignment
rather than inheriting from NonCopyable. This avoids
cppcoreguidelines-special-member-functions warning by clang-tidy.
*/
BikeSystem(const BikeSystem&) = delete;
BikeSystem(BikeSystem&&) = delete;
BikeSystem& operator=(const BikeSystem&) = delete;
BikeSystem& operator=(BikeSystem&&) = delete;
// 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 gear_task();
void speed_distance_task();
void temperature_task();
void reset_task();
void display_task1();
void display_task2();
// flag stating whether sleep is allowed when simulating computation times
static constexpr bool kAllowSleep = false;
// stop flag, used for stopping the super-loop (set in stop())
volatile std::atomic<bool> _stop_flag = false;
// data member that represents the device for manipulating the gear
GearDevice _gear_device;
uint8_t _current_gear = bike_computer::kMinGear;
uint8_t _current_gear_size = bike_computer::kMinGearSize;
// data member that represents the device for manipulating the pedal rotation
// speed/time
PedalDevice _pedal_device;
float _current_speed = 0.0F;
float _traveled_distance = 0.0F;
// data member that represents the device used for resetting
ResetDevice _reset_device;
// data member that represents the display
BikeDisplay _bike_display;
// data member that represents the device for counting wheel rotations
Speedometer _speedometer;
// data member that represents the sensor device
SensorDevice _sensor_device;
float _current_temperature = 0.0F;
// used for managing tasks info
TaskManager _task_manager;
};
} // namespace bike_computer::static_scheduling
The implementation of most 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
// zpp_lib
#include "zpp_include/this_thread.hpp"
#include "zpp_include/time.hpp"
#include "zpp_include/utils.hpp"
#include "zpp_include/work_queue.hpp"
#include "zpp_include/zpp_assert.hpp"
#include "zpp_include/zpp_log.hpp"
// common
#include "common/ttce.hpp"
ZPP_LOG_MODULE_DECLARE(bike_computer, CONFIG_APP_LOG_LEVEL);
namespace bike_computer::static_scheduling {
// The complexity is increased by zephyr macros
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
zpp_lib::ZephyrResult BikeSystem::start() {
ZPP_LOG_INF("Starting Super-Loop without event handling");
zpp_lib::Utils::log_threads_summary();
auto res = initialize();
if (!res) {
ZPP_LOG_ERR("Init failed: %d", (int)res.error());
return res;
}
ZPP_LOG_DBG("Starting super-loop");
// initialize the task manager phase
_task_manager.initialize_phase();
while (true) {
#if CONFIG_APP_LOG_LEVEL_DEBUG
auto start_time = zpp_lib::Time::get_uptime();
#endif // CONFIG_APP_LOG_LEVEL_DEBUG
// TODO(Student): 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
#if CONFIG_APP_LOG_LEVEL_DEBUG
std::chrono::microseconds end_time = zpp_lib::Time::get_uptime();
auto cycle = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
ZPP_LOG_DBG("Repeating cycle time is %" PRIu64 " milliseconds", cycle.count());
#endif // CONFIG_APP_LOG_LEVEL_DEBUG
if (_stop_flag.load()) {
break;
}
#ifdef CONFIG_CPU_LOAD
zpp_lib::Utils::log_cpu_load();
#endif
}
return res;
}
void BikeSystem::stop() {
_stop_flag.store(true);
}
zpp_lib::ZephyrResult BikeSystem::initialize() {
// initialize the display
auto res = _bike_display.initialize();
if (!res) {
ZPP_LOG_ERR("Cannot initialize display: %d", (int)res.error());
return res;
}
// initialize the sensor device
res = _sensor_device.initialize();
if (!res) {
ZPP_LOG_ERR("Sensor not present or initialization failed: %d", (int)res.error());
}
return {};
}
void BikeSystem::gear_task() {
// gear task
_task_manager.register_task_start(TaskManager::TaskType::GearTaskType);
// no need to protect access to data members (single threaded)
_current_gear = _gear_device.get_current_gear();
_current_gear_size = _gear_device.get_current_gear_size();
_task_manager.simulate_computation_time(TaskManager::TaskType::GearTaskType, kAllowSleep);
}
void BikeSystem::speed_distance_task() {
// speed and distance task
_task_manager.register_task_start(TaskManager::TaskType::SpeedTaskType);
auto pedal_rotation_time = _pedal_device.get_current_rotation_time();
_speedometer.set_current_pedal_rotation_time(pedal_rotation_time);
_speedometer.set_gear_size(_current_gear_size);
// no need to protect access to data members (single threaded)
_current_speed = _speedometer.get_current_speed();
_traveled_distance = _speedometer.get_traveled_distance();
_task_manager.simulate_computation_time(TaskManager::TaskType::SpeedTaskType, kAllowSleep);
}
void BikeSystem::temperature_task() {
_task_manager.register_task_start(TaskManager::TaskType::TemperatureTaskType);
// no need to protect access to data members (single threaded)
zpp_lib::ZephyrResult res = _sensor_device.read_temperature(_current_temperature);
if (!res) {
ZPP_LOG_ERR("Cannot read temperature: %d", (int)res.error());
}
// simulate task computation by waiting for the required task computation time
_task_manager.simulate_computation_time(TaskManager::TaskType::TemperatureTaskType, kAllowSleep);
}
void BikeSystem::reset_task() {
_task_manager.register_task_start(TaskManager::TaskType::ResetTaskType);
if (_reset_device.check_reset()) {
#if CONFIG_APP_LOG_LEVEL >= CONFIG_LOG_LEVEL_INFO
std::chrono::microseconds response_time = // TODO(Student): compute response time
ZPP_LOG_INF("Reset task: response time is %" PRIu64 " usecs", response_time.count());
#endif // CONFIG_APP_LOG_LEVEL >= CONFIG_LOG_LEVEL_INFO
_speedometer.reset();
}
_task_manager.simulate_computation_time(TaskManager::TaskType::ResetTaskType, kAllowSleep);
}
void BikeSystem::display_task1() {
_task_manager.register_task_start(TaskManager::TaskType::DisplayTask1Type);
// TODO(Student): update gear, speed and distance displayed on screen
_task_manager.simulate_computation_time(TaskManager::TaskType::DisplayTask1Type, kAllowSleep);
}
void BikeSystem::display_task2() {
_task_manager.register_task_start(TaskManager::TaskType::DisplayTask2Type);
// TODO(Student): update temperature on screen
_task_manager.simulate_computation_time(TaskManager::TaskType::DisplayTask2Type, kAllowSleep);
}
} // namespace bike_computer::static_scheduling
First understand how the class is written, what the data members are and what
the methods do. 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:
#if CONFIG_BIKE_COMPUTER_STATIC_SCHEDULING
bike_computer::static_scheduling::BikeSystem bike_system;
#endif
bikeSystem.start();
start() method call and that
the bike_computer program will thus be executed by the main thread.
The CONFIG_BIKE_COMPUTER_STATIC_SCHEDULING configuration parameter must
be defined in the application Kconfig file as follows:
menu "Zephyr"
source "Kconfig.zephyr"
endmenu
module = APP
module-str = APP
source "subsys/logging/Kconfig.template.log_config"
config BIKE_COMPUTER_STATIC_SCHEDULING
bool "Build system with static scheduling"
default n
help
This option must be enabled to use the static scheduling version.
Code Instrumentation to Measure Effective Task Periods and Computation Times
To verify that period and computation times are correct, the tasks must be instrumented.
At the start of each method representing a task, such as the BikeSystem::gear_task()
method, the current time is registered by calling _taskManager.register_task_start().
Then, the task runs, after which the method calls the _taskManager.simulate_computation_time() method.
This registers task time information in the TaskManager instance.
By default, the TaskManager class does not perform any logging, because logging has a significant impact on the system.
If you wish to add logging to better understand the program behavior at the time of conception, you may do the following:
-
Add the following lines in the
Kconfigfile:bike_computer/Kconfigconfig LOG_TASK_TIMES bool "Log task times for debugging purposes" default n help This option must be enabled to log task times with the TaskManager class This option must be disabled for test and production -
Create a
prj_log_task_times.conffile with the following contentbike_computer/prj_log_task_times.conf# enable logging of task times in TaskManager class CONFIG_LOG_TASK_TIMES=y -
Build your application with the following command:
This command build the application with logging of task times enabled.just build bike_computer log_app+gpio+display+sensor+phase_a prj_log_yes prj_log_task_times.conf -
Flash your board.
You should see an output similar to the one below on the console:
Log with the various task times
```txt title=”Serial Terminal”
[00:00:01.025,360] <dbg> bike_computer: start: Starting super-loop
[00:00:01.131,896] <dbg> bike_computer: simulate_computation_time: Task Gear: start time 0 (bounds 0 - 700000), computation time 100006
[00:00:01.344,421] <dbg> bike_computer: simulate_computation_time: Task Speed: start time 112488 (bounds 0 - 200000), computation time 200042
[00:00:01.457,427] <dbg> bike_computer: simulate_computation_time: Task Reset: start time 325531 (bounds 0 - 700000), computation time 100006
[00:00:01.670,440] <dbg> bike_computer: simulate_computation_time: Task Display(1): start time 438537 (bounds 0 - 1400000), computation time 200013
[00:00:01.884,002] <dbg> bike_computer: simulate_computation_time: Task Speed: start time 652069 (bounds 0 - 200000), computation time 200012
[00:00:01.997,009] <dbg> bike_computer: simulate_computation_time: Task Gear: start time 865112 (bounds 0 - 700000), computation time 100006
[00:00:02.209,960] <dbg> bike_computer: simulate_computation_time: Task Speed: start time 978027 (bounds 0 - 200000), computation time 200012
[00:00:02.322,937] <dbg> bike_computer: simulate_computation_time: Task Temperature: start time 1191040 (bounds 0 - 1500000), computation time 100006
[00:00:02.436,645] <dbg> bike_computer: simulate_computation_time: Task Display(2): start time 1304748 (bounds 0 - 1500000), computation time 100006
[00:00:02.650,299] <dbg> bike_computer: simulate_computation_time: Task Speed: start time 1418365 (bounds 0 - 200000), computation time 200013
[00:00:02.763,366] <dbg> bike_computer: simulate_computation_time: Task Reset: start time 1631470 (bounds 0 - 700000), computation time 100006
[00:00:02.776,458] <dbg> bike_computer: start: Repeating cycle time is 1744 milliseconds
```
From this log, one can confirm that:
- The Major Cycle or Repeating Cycle Time is close to the expected \(1600\,\mathsf{ms}\). The overshoot is caused by logging.
- The periods and computation times of the Gear, Speed, Temperature and Reset tasks are close to expected, with overshoot also caused by logging.
- The Display task is splitted into 2 subtasks, with correct periods for a total computation time of \(300\,\mathsf{ms}\), as expected.
From this experiment, it is clear that logging information affects computation times, so logging should be disabled to test that computation and period times are respected. This is confirmed as follows:
-
Build your application without task logging with:
just build bike_computer log_app+gpio+display+sensor+phase_a -
Flash your board.
With this configuration, you should see an output similar to the one below on the console:
Log with the various task times
[00:00:01.024,261] <dbg> bike_computer: start: Starting super-loop
[00:00:02.630,920] <dbg> bike_computer: start: Repeating cycle time is 1600 milliseconds
[00:00:04.239,440] <dbg> bike_computer: start: Repeating cycle time is 1600 milliseconds
[00:00:05.847,961] <dbg> bike_computer: start: Repeating cycle time is 1600 milliseconds
[00:00:07.456,481] <dbg> bike_computer: start: Repeating cycle time is 1600 milliseconds
As you can see, the Major Cycle is now \(1600\,\mathsf{ms}\). The overshoot caused by logging has disappeared. Note that in test mode, logging is entirely disabled, as explained below.
Run the Test Program
To check the implementation of the BikeSystem::start() method, you must
successfully run the test program available here.
This program validates that the tasks run at the correct periods with the correct computation times.
It is important to note the following points regarding Zephyr RTOS test programs and their configurations:
-
Zephyr RTOS test programs can be built and flashed as standard Zephyr RTOS programs, for instance with the command
If you flash your board, the test program will run. The main difference as compared with a twister run is that test results are not collected and that the application simply hangs at the end of the tests.just build bike_computer/tests/bike_system_part1/ gpio+sensor+display+test+phase_a -
When running test programs with twister, configuration parameters cannot be passed through the command line. Instead configuration parameters are specified in the
testcase.yamlfile. For this test program, the testcase.yaml specifies a number of configuration files to be used withextra_conf_filesand a number of arguments specific to each platform on which tests are run withextra_args. -
With twister, a
prj.conffile must be present at the root directory of the test program. Since we use default application parameters by adding theprj.conffile fromdeps/zpp_lib/configs/prj.conf, theprj.conffile of the test program is left empty. -
The support for display cannot be specified in the twister command and must also be specified in the
testcase.yamlfile.
To run the test, you must execute the following command:
just test bike_computer/tests/bike_computer_part1 "my_map_file.yaml"
Expected Test Results
When the CONFIG_TEST configuration is enabled, the TaskManager class verifies that the computation times and
periods of tasks are correct. If any of these values are outside the expected range, an assertion error is generated.
To account for variations due to imprecision and extra statements, the TaskManager class allows for a variation
of kAllowedDelta. This constant is set to 100 ticks.
Given that the CPU utilization factor is 1, the test will fail after a certain amount of
time. This is expected, and the goal is to determine how long it takes for the test to fail.
Modify test duration accordingly so that the test succeed on the different platforms.
The test duration can be adjusted in the testcase.yaml file by modifying the value for each platform.
It is expected that the test will fail earlier on a physical board than on an emulated
device. If the test fails before reaching a duration of 20s, you must identify and resolve
any scheduling errors. To run this test successfully, logging must be disabled entirely.
The expected results demonstrate that static scheduling is sensitive to CPU load.
Code Instrumentation to Measure the Reset Response Time
In theBikeSystem::reset_task() method, the speedometer resets when the user
presses the button. You must also compute the task response time in this
method (marked as TODO(Student)). The response time is the time elapsed between
the button being pressed (or, more precisely, the button press being detected) and the
reset request being handled.
After implementing the response time calculation, you can build the application with
Flash your device. Then 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:02.634,307] <inf> bike_computer: Reset task: response time is 348968 usecs
...
[00:00:05.859,222] <inf> bike_computer: Reset task: response time is 1005402 usecs
...
[00:00:09.084,228] <inf> bike_computer: Reset task: response time is 360504 usecs
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:
bike_computer 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 bike_computer 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 the required additional hooks. All software quality tools succeed, changes are committed and pushed to your repository.
Deliverables
Deliverables/Requirements (Project Phase B)
The deliverables/requirements for project Phase B include the following:
- The implementation of the
bike_computerusing a a Timeline Cyclic Scheduling algorithm with a Super Loop mechanism is functional. All related files are available in thestatic_schedulingfolder and all related code is developed under thestatic_schedulingnamespace. -
A diagram showing how tasks are scheduled during one major cycle is added to your repository and included in the README file of the project.
-
The developed code is compliant to the
clang-tidyrules andjust run-clang-tidy bike_computer log+debug+gpio+display+sensor+phase_aruns successfully. -
The following hooks are added to the
pre-commitconfiguration:id: clang-tidy-bike-computerid: clang-tidy-bike-computer-speedometerid: clang-tidy-bike-computer-sensor-deviceid: clang-tidy-bike-computer-bike-system-part1
pre-commitruns successfully. -
The
bike_computer/tests/bike_system_part1test runs successfully on your board and on qemu_x86. TheCONFIG_TEST_DURATION_IN_MSparameter have been estimated and updated in thebike_computer/tests/bike_system_part1/testcase.yamlfile. Theci/applications_for_build.yamlandci/applications_for_test.yamlfiles are updated. The following commands run successfully:just test bike_computer/tests/bike_system_part1 "you_map_file.yaml"just test-qemu bike_computer/tests/bike_system_part1just test-all "your_map_file.yaml", including thebike_computer/tests/bike_system_part1test.just test-all-qemu, including thebike_computer/tests/bike_system_part1test.
All build and tests pass on the GitHub repository.
-
The answer to question 1 is documented in the README.md file.