Skip to content

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 BikeComputer codelab).

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
bike_computer/src/common/constants.hpp
// 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
bike_computer/src/common/sensor_device.hpp
// 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
bike_computer/tests/bike_computer/sensor_device/src/test_sensor_device.cpp
// 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
bike_computer/src/common/speedometer.hpp
// 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
bike_computer/tests/bike_computer/speedometer/src/test_speedometer.cpp
// 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)
bike_computer/src/common/speedometer.cpp
// 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
then the 4 test cases should succeed. If some test cases fail, you can have a look in the 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 BikeDisplay class provides an API to display information on the LCD screen. It is used in the BikeSystem class. 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 TaskManager class allows to monitor and test that tasks are respecting their timing constraints. When used in test programs (with CONFIG_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
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 “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 BikeComputer program 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 BikeComputer program 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 BikeComputer program 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 BikeComputer program 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.

BikeComputer tasks

BikeComputer Tasks

For implementing the different tasks, we use specific classes that helps in simulating some behaviors:

  • The gear task is implemented using a GearDevice class 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 PedalDevice class 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 BikeDisplay class that provides an API to display the information on the LCD screen (encapsulating the zpp_lib::Display class).
  • The reset task is implemented using a ResetDevice class that allows for checking for reset. Reset is implemented using a button.

The GearDevice class

The GearDevice class code is given below:

GearDevice declaration
bike_computer/src/static_scheduling/gear_device.hpp
// 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
bike_computer/src/static_scheduling/gear_device.cpp
// 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
bike_computer/src/static_scheduling/pedal_device.hpp
// 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
bike_computer/src/static_scheduling/reset_device.hpp
// 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();
Given that the press time is registered using an ISR method, it is a good approximation of the time at which the button was really pressed. In the 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
static_scheduling/bike_system.hpp
// 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)
static_scheduling/bike_system.cpp
// 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();
Note that the program will never return from the 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] bike_computer: main: Starting the bike system
[00:00:00.469,116] bike_computer: Starting Super-Loop without event handling
[00:00:00.477,111] zpp_drivers: Display sample for ili9340@0
[00:00:00.483,764] zpp_drivers: Display capabilities: x_res 320 y_res 240
[00:00:00.491,485] zpp_drivers: Display size: x 320, y 240
[00:00:00.497,924] zpp_drivers: Buffer size: 960
[00:00:00.503,509] bike_computer: initialize: Display initialized
[00:00:01.229,370] bike_computer: start: Starting super-loop
[00:00:01.322,998] bike_computer: logTaskTime: Task Gear: start time 0 (bounds 0 - 700000), computation time 87006
[00:00:01.521,392] bike_computer: logTaskTime: Task Speed: start time 98328 (bounds 0 - 200000), computation time 187072
[00:00:01.620,300] bike_computer: logTaskTime: Task Reset: start time 297302 (bounds 0 - 700000), computation time 87006
[00:00:01.819,122] bike_computer: logTaskTime: Task Display(1): start time 396149 (bounds 0 - 1400000), computation time 187011
[00:00:02.018,646] bike_computer: logTaskTime: Task Speed: start time 595642 (bounds 400000 - 600000), computation time 187042
[00:00:02.118,041] bike_computer: logTaskTime: Task Gear: start time 795044 (bounds 800000 - 1500000), computation time 87005
[00:00:02.317,321] bike_computer: logTaskTime: Task Speed: start time 894318 (bounds 800000 - 1000000), computation time 187042
[00:00:02.416,778] bike_computer: logTaskTime: Task Temperature: start time 1093811 (bounds 0 - 1500000), computation time 87006
[00:00:02.516,296] bike_computer: logTaskTime: Task Display(2): start time 1193329 (bounds 0 - 1500000), computation time 87005
[00:00:02.715,789] bike_computer: logTaskTime: Task Speed: start time 1292786 (bounds 1200000 - 1400000), computation time 187011
[00:00:02.815,429] bike_computer: logTaskTime: Task Reset: start time 1492462 (bounds 800000 - 1500000), computation time 87006
[00:00:02.827,880] bike_computer: start: Repeating cycle time is 1591 milliseconds

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)
bike_computer/tests/bike_system_part1/src/test_bike_system_part1.cpp
 // 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:

bike_computer/tests/bike_computer/bike_system_part1/testcase.yaml
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] bike_computer: Reset task: response time is 603394 usecs

[00:00:41.636,016] bike_computer: Reset task: response time is 213958 usecs

[00:01:10.442,596] bike_computer: Reset task: response time is 1285370 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:

Class Diagram Class Diagram for the BikeComputer program

Make 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 main function is implemented and functional.
  • There is no significant deviations from the class architecture as documented above.
  • The three test programs (test_sensor_device, test_speedometer and test_bike_system_part1) run successfully, as documented in the codelab.
  • The pre-commit configuration 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.