Aller au contenu

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 joystick.
  • Pedal rotation: the BikeComputer program simulates pedal rotation at a given speed. On our system, rotation speed can be modified using the joystick.
  • 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 codelab and the following ones, we will study different ways of implementing this BikeComputer program. We will analyze the advantages and disadvantages of the different implementations. Some important performance indicators of the different implementations will be studied such as the reset response time, e.g. the delay between the user pressing the reset button and the LCD to show reset values on screen.

What you’ll build

In this codelab, you’re going to program a simple BikeComputer program using a timeline cyclic scheduling algorithm.

What you’ll learn

  • How to develop a embedded program using a super-loop.
  • How to schedule the different tasks of an embedded program using timeline cyclic scheduling.
  • 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 different implementations

The BikeComputer codelab is divided into three different parts, in which we will realize different implementations of the same program:

  • The first implementation (part 1) is the most straightforward one: a simple Super-Loop program with timeline cyclic scheduling of tasks. In this implementation, no event is ever generated by the system and all tasks are executed within the infinite super-loop executed in the main() function.
  • Limitations of this approach will be analyzed and based on these limitations, we will then implement several event-driven programs using different scheduling algorithms (parts 2 and 3 of the BikeComputer codelab). The implemented scheduling algorithms will range from timeline cyclic to dynamic scheduling with task preemption.

For starting the different implementations, you must use the Mbed OS project that was created in the getting started codelab and that you named “bike-computer”.

For implementing the different versions of the BikeComputer program, you must apply the following approach:

  • You must create the different versions into a single program but in separate subfolders and with slight changes in the main() function. In this way, all versions will be implemented within the same project. For differentiating clearly the different versions within the same program, you must use different namespaces for each version.

Integrate the required libraries

For implementing some of the BikeComputer program mechanisms, you must import two different libraries:

  • the DISCO_H747I library that provides functionalities for driving some of the DISCO_H747I peripherals such as the LCD screen or the joystick.
  • the AdvEmbSof library that provides helper classes for implementing the BikeComputer program. This library has dependencies with the DISCO_H747I library and cannot be used without importing it.

Instructions for importing external libraries into your Mbed OS project are given here

Testing the integration of the libraries

The “AdvEmbSof library” implements a test program named “advdembsof_library-tests-sensors-hdc1000”. For making sure that the library has been properly added to your BikeComputer program, compile and run this test program on the DISCO_H747I board. The test should be successful if you connected the HDC-1000 click board properly, and a failure should be detected otherwise. Instructions for connecting the click board will be given during the lecture.

Recall that the command to run for running the test is

mbed test -m DISCO_H747I -t GCC_ARM -n advdembsof_library-tests-sensors-hdc1000 --compile --run
with the proper toolchain selected.

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 that are each responsible of implementing a specific feature. We first implement these C++ classes and we will then integrate them into our BikeComputer program.

We start implementing classes that will be used by all BikeComputer program implementation: the SensorDevice and Speedometer classes.

The constants used in the program

The constants are all defined as follows:

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

/****************************************************************************
 * @file constants.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief Constants definition used for implementing the bike system
 *
 * @date 2023-08-20
 * @version 1.0.0
 ***************************************************************************/

#pragma once

#include <stdint.h>

#include "mbed.h"

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.

// 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

For making the integration of the HDC-1000 click board easier and hiding some hardware details, our BikeComputer program integrates a SensorDevice class. The class declaration is given below:

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

/****************************************************************************
 * @file sensor_device.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief SensorDevice header file (static scheduling)
 *
 * @date 2023-08-20
 * @version 1.0.0
 ***************************************************************************/

#pragma once

#include "hdc1000.hpp"
#include "mbed.h"

namespace bike_computer {

class SensorDevice {
   public:
    // constructor
    SensorDevice();

    // method for initializing the device
    bool init();

    // methods used for
    float readTemperature();
    float readHumidity();

   private:
    // data members
    advembsof::HDC1000 _hdc1000;
};

}  // namespace bike_computer

The SensorDevice class will be used by all BikeComputer program implementations and it is thus added to a subfolder named “common”. It is also using the namespace bike_computer. Based on the class declaration, you must implement it in the “common/sensor_device.cpp” file. Once you have implemented it, you may test your implementation by using the following test program:

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

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

#include "greentea-client/test_env.h"
#include "hdc1000.hpp"
#include "mbed.h"
#include "sensor_device.hpp"
#include "unity/unity.h"
#include "utest/utest.h"

using namespace utest::v1;

// test_hdc1000 test handler function
static control_t test_sensor_device(const size_t call_count) {
    // create the SensorDevice instance
    bike_computer::SensorDevice sensorDevice;

    bool rc = sensorDevice.init();
    TEST_ASSERT_TRUE(rc);

    float temperature                        = sensorDevice.readTemperature();
    static constexpr float kTemperatureRange = 20.0f;
    static constexpr float kMeanTemperature  = 15.0f;
    TEST_ASSERT_FLOAT_WITHIN(kTemperatureRange, kMeanTemperature, temperature);

    float humidity                        = sensorDevice.readHumidity();
    static constexpr float kHumidityRange = 40.0f;
    static constexpr float kMeanHumidity  = 50.0f;
    TEST_ASSERT_FLOAT_WITHIN(kHumidityRange, kMeanHumidity, humidity);

    // execute the test only once and move to the next one, without waiting
    return CaseNext;
}

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

    return greentea_test_setup_handler(number_of_cases);
}

// List of test cases in this file
static Case cases[] = {Case("test sensor device", test_sensor_device)};

static Specification specification(greentea_setup, cases);

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

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

mbed test -m DISCO_H747I -t GCC_ARM -n tests-bike-computer-sensor-device --compile --run
If the test program succeeds (assuming that you have connected your HDC-1000 click board properly), then it is time to commit and push your changes. Remember: small commits are better than large commits that include many unrelated changes. Before committing the changes, you must also integrate the “common” folder in the precommit phase by modifying the “.pre-commit-config.yaml” file accordingly: you must modify the “files” property by adding the “common” 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
common/speedometer.hpp
// Copyright 2022 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file speedometer_device.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief WheelCounterDevice header file (static scheduling)
 *
 * @date 2023-08-20
 * @version 1.0.0
 ***************************************************************************/

#pragma once

#include "constants.hpp"
#include "mbed.h"

namespace bike_computer {

class Speedometer {
   public:
    explicit Speedometer(Timer& timer);  // NOLINT(runtime/references)

    // 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 defined(MBED_TEST_MODE)
    uint8_t getGearSize() const;
    float getWheelCircumference() const;
    float getTraySize() const;
    std::chrono::milliseconds getCurrentPedalRotationTime() const;
    void setOnResetCallback(mbed::Callback<void()> cb);
#endif  // defined(MBED_TEST_MODE)

   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
    Timer& _timer;
    LowPowerTicker _ticker;
    float _currentSpeed = 0.0f;
    Mutex _totalDistanceMutex;
    float _totalDistance = 0.0f;
    uint8_t _gearSize    = 1;

    Thread _thread;

#if defined(MBED_TEST_MODE)
    mbed::Callback<void()> _cb;
#endif
};

}  // namespace bike_computer

The test program for this class is given below:

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

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

#include <chrono>

#include "common/constants.hpp"
#include "common/speedometer.hpp"
#include "greentea-client/test_env.h"
#include "mbed.h"
#include "static_scheduling/gear_device.hpp"
#include "unity/unity.h"
#include "utest/utest.h"

using namespace utest::v1;

// allow for 0.1 km/h difference
static constexpr float kAllowedSpeedDelta = 0.1f;
// allow for 1m difference
static constexpr float kAllowedDistanceDelta = 1.0f / 1000.0;

// 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;

    printf("  Expected speed is %f, current speed is %f\n", expectedSpeed, currentSpeed);
    TEST_ASSERT_FLOAT_WITHIN(kAllowedSpeedDelta, expectedSpeed, currentSpeed);
}

// 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.0;
}

// 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",
           expectedDistance,
           distance);
    TEST_ASSERT_FLOAT_WITHIN(kAllowedDistanceDelta, expectedDistance, distance);
}

// test the speedometer by modifying the gear
static control_t test_gear_size(const size_t call_count) {
    // create a timer
    Timer timer;
    // start the timer
    timer.start();

    // create a speedometer instance
    bike_computer::Speedometer speedometer(timer);

    // 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);
    }

    // execute the test only once and move to the next one, without waiting
    return CaseNext;
}

// test the speedometer by modifying the pedal rotation speed
static control_t test_rotation_speed(const size_t call_count) {
    // create a timer
    Timer timer;
    // start the timer
    timer.start();

    // create a speedometer instance
    bike_computer::Speedometer speedometer(timer);

    // 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);
    }

    // execute the test only once and move to the next one, without waiting
    return CaseNext;
}

// test the speedometer by modifying the pedal rotation speed
static control_t test_distance(const size_t call_count) {
    // create a timer
    Timer timer;

    // create a speedometer instance
    bike_computer::Speedometer speedometer(timer);

    // 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]);

    // start the timer (for simulating bike start)
    timer.start();

    // 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
        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
        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",
               expectedDistance,
               traveledDistance);
        TEST_ASSERT_FLOAT_WITHIN(
            kAllowedDistanceDelta, expectedDistance, traveledDistance);
    }
    // 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
        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",
               expectedDistance,
               traveledDistance);
        TEST_ASSERT_FLOAT_WITHIN(
            kAllowedDistanceDelta, expectedDistance, traveledDistance);
    }

    // execute the test only once and move to the next one, without waiting
    return CaseNext;
}

// test the speedometer by modifying the pedal rotation speed
static control_t test_reset(const size_t call_count) {
    // create a timer instance
    Timer timer;

    // create a speedometer instance
    bike_computer::Speedometer speedometer(timer);

    // 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();

    // start the timer (for simulating bike start)
    timer.start();

    // travel for 1 second
    const auto travelTime = 1000ms;
    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();

    printf("  Expected distance is %f, current distance is %f\n",
           expectedDistance,
           traveledDistance);
    TEST_ASSERT_FLOAT_WITHIN(kAllowedDistanceDelta, expectedDistance, traveledDistance);

    // reset the speedometer
    speedometer.reset();

    // traveled distance should now be zero
    traveledDistance = speedometer.getDistance();

    printf("  Expected distance is %f, current distance is %f\n", 0.0f, traveledDistance);
    TEST_ASSERT_FLOAT_WITHIN(kAllowedDistanceDelta, 0.0f, traveledDistance);

    // execute the test only once and move to the next one, without waiting
    return CaseNext;
}

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

    return greentea_test_setup_handler(number_of_cases);
}

// List of test cases in this file
static Case cases[] = {
    Case("test speedometer gear size change", test_gear_size),
    Case("test speedometer rotation speed change", test_rotation_speed),
    Case("test speedometer distance", test_distance),
    Case("test speedometer reset", test_reset)};

static Specification specification(greentea_setup, cases);

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

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

/****************************************************************************
 * @file speedometer_device.cpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief WheelCounterDevice implementation (static scheduling)
 *
 * @date 2023-08-20
 * @version 1.0.0
 ***************************************************************************/

#include "speedometer.hpp"

#include <chrono>
#include <ratio>

// from disco_h747i/wrappers
#include "joystick.hpp"
#include "mbed_trace.h"

#if MBED_CONF_MBED_TRACE_ENABLE
#define TRACE_GROUP "Speedometer"
#endif  // MBED_CONF_MBED_TRACE_ENABLE

namespace bike_computer {

Speedometer::Speedometer(Timer& timer) : _timer(timer) {
    // update _lastTime
    _lastTime = _timer.elapsed_time();
}

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() {
    // TODO 
}

#if defined(MBED_TEST_MODE)
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;
}

#endif  // defined(MBED_TEST_MODE)

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 thefollowing command:

mbed test -m DISCO_H747I -t GCC_ARM -n tests-bike-computer-speedometer --compile --run
then the 4 test cases should succeed. If some test cases fail, you may run the test command in verbose mode (with the -v flag) 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. Make sure that your build script (e.g. “build.yml”) define the target to be exclusively DISCO_H747I, since the project won’t compile for other targets.

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 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 joystick.
  • 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 joystick.
  • The display task is implemented using a DisplayDevice class that displays the information on LCD screen with the help of the dedicated “DISCO_H747I” and “advembsof” libraries.
  • The reset task is implemented using a ResetDevice class that allows for checking for reset. Reset is implemented using the push button device.

The GearDevice class

The GearDevice class code is given below:

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

/****************************************************************************
 * @file gear_device.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief Gear Device header file (static scheduling)
 *
 * @date 2023-08-20
 * @version 1.0.0
 ***************************************************************************/

#pragma once

#include "constants.hpp"
#include "mbed.h"

namespace static_scheduling {

class GearDevice {
   public:
    explicit GearDevice(Timer& timer);  // NOLINT(runtime/references)

    // make the class non copyable
    GearDevice(GearDevice&)            = delete;
    GearDevice& operator=(GearDevice&) = delete;

    // method called for updating the bike system
    uint8_t getCurrentGear();
    uint8_t getCurrentGearSize() const;

   private:
    // data members
    uint8_t _currentGear = bike_computer::kMinGear;
    Timer& _timer;
};

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

/****************************************************************************
 * @file gear_device.cpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief Gear Device implementation (static scheduling)
 *
 * @date 2023-08-20
 * @version 1.0.0
 ***************************************************************************/

#include "gear_device.hpp"

// from disco_h747i/wrappers
#include <chrono>

#include "joystick.hpp"
#include "mbed_trace.h"

#if MBED_CONF_MBED_TRACE_ENABLE
#define TRACE_GROUP "GearDevice"
#endif  // MBED_CONF_MBED_TRACE_ENABLE

namespace static_scheduling {

// definition of task execution time
static constexpr std::chrono::microseconds kTaskRunTime = 100000us;

GearDevice::GearDevice(Timer& timer) : _timer(timer) {}

uint8_t GearDevice::getCurrentGear() {
    std::chrono::microseconds initialTime = _timer.elapsed_time();
    std::chrono::microseconds elapsedTime = std::chrono::microseconds::zero();
    // we bound the change to one increment/decrement per call
    bool hasChanged = false;
    while (elapsedTime < kTaskRunTime) {
        if (!hasChanged) {
            disco::Joystick::State joystickState =
                disco::Joystick::getInstance().getState();
            switch (joystickState) {
                case disco::Joystick::State::UpPressed:
                    if (_currentGear < bike_computer::kMaxGear) {
                        _currentGear++;
                    }
                    hasChanged = true;
                    break;

                case disco::Joystick::State::DownPressed:
                    if (_currentGear > bike_computer::kMinGear) {
                        _currentGear--;
                    }
                    hasChanged = true;
                    break;

                default:
                    break;
            }
        }
        elapsedTime = _timer.elapsed_time() - 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

This code uses the static_scheduling namespace since the first implementation of the BikeComputer program implements static scheduling. In the getCurrentGear() method, the program checks whether the Joystick “Up” or “Down” button is pressed, by checking the joystick state. If it is so, it modifies the current gear and does not allow for further changes in the current call. The duration of the call is kTaskRunTime, which means 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
static_scheduling/pedal_device.hpp
// Copyright 2022 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file pedal_device.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief Pedal System header file (static scheduling)
 *
 * @date 2023-08-20
 * @version 1.0.0
 ***************************************************************************/

#pragma once

#include "constants.hpp"
#include "mbed.h"

namespace static_scheduling {

class PedalDevice {
   public:
    explicit PedalDevice(Timer& timer);  // NOLINT(runtime/references)

    // make the class non copyable
    PedalDevice(PedalDevice&)            = delete;
    PedalDevice& operator=(PedalDevice&) = delete;

    // 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;
    Timer& _timer;
};

}  // namespace static_scheduling

The PedalDevice must be implemented for modifying the speed when the user presses the joystick “Left” and “Right” button.

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

/****************************************************************************
 * @file reset_device.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief ResetDevice header file (static scheduling)
 *
 * @date 2023-08-20
 * @version 1.0.0
 ***************************************************************************/

#pragma once

#include "mbed.h"

namespace static_scheduling {

class ResetDevice {
   public:
    explicit ResetDevice(Timer& timer);  // NOLINT(runtime/references)

    // make the class non copyable
    ResetDevice(ResetDevice&)            = delete;
    ResetDevice& operator=(ResetDevice&) = delete;

    // method called for checking the reset status
    bool checkReset();

    // for computing the response time
    std::chrono::microseconds getPressTime();

   private:
    // called when the button is pressed
    void onRise();

    // data members
    // instance representing the reset button
    InterruptIn _resetButton;
    Timer& _timer;
    std::chrono::microseconds _pressTime;
};

}  // namespace static_scheduling

The ResetDevice must implement a reset when the user presses the push button. The push button is defined in the Mbed OS library with the symbol BUTTON1. In this implementation, the program must check the polarity of the input pin. For a portable implementation of your program, you should define the pin names such as:

static_scheduling/reset_device.cpp
// includes come here

#if defined(TARGET_DISCO_H747I)
#define PUSH_BUTTON BUTTON1
static constexpr uint8_t kPolarityPressed = 1;
#endif


// code comes here

In the ResetDevice::onRise() method, the press time is registered using

_pressTime = _timer.elapsed_time();
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 Mbed OS 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 period 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 2022 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file bike_system.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief Bike System header file (static scheduling)
 *
 * @date 2023-08-20
 * @version 1.0.0
 ***************************************************************************/

#pragma once

// from advembsof
#include "display_device.hpp"
#include "task_logger.hpp"

// from common
#include "sensor_device.hpp"
#include "speedometer.hpp"

// local
#include "gear_device.hpp"
#include "pedal_device.hpp"
#include "reset_device.hpp"

namespace static_scheduling {

class BikeSystem {
   public:
    // constructor
    BikeSystem();

    // make the class non copyable
    BikeSystem(BikeSystem&)            = delete;
    BikeSystem& operator=(BikeSystem&) = delete;

    // method called in main() for starting the system
    void start();

    // method called for stopping the system
    void stop();

#if defined(MBED_TEST_MODE)
    const advembsof::TaskLogger& getTaskLogger();
#endif  // defined(MBED_TEST_MODE)

   private:
    // private methods
    void init();
    void gearTask();  
    void speedDistanceTask(); 
    void temperatureTask();
    void resetTask();
    void displayTask1();
    void displayTask2();

    // stop flag, used for stopping the super-loop (set in stop())
    bool _stopFlag = false;
    // timer instance used for loggint task time and used by ResetDevice
    Timer _timer;
    // 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 device display
    advembsof::DisplayDevice _displayDevice;
    // data member that represents the device for counting wheel rotations
    bike_computer::Speedometer _speedometer;
    // data member that represents the sensor device
    bike_computer::SensorDevice _sensorDevice;
    float _currentTemperature = 0.0f;

    // used for logging task info
    advembsof::TaskLogger _taskLogger;
};

}  // namespace static_scheduling

Most of the implementation of the BikeSystem class is given below:

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

/****************************************************************************
 * @file bike_system.cpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief Bike System implementation (static scheduling)
 *
 * @date 2023-08-20
 * @version 1.0.0
 ***************************************************************************/

#include "bike_system.hpp"

#include <chrono>

#include "mbed_trace.h"
#if MBED_CONF_MBED_TRACE_ENABLE
#define TRACE_GROUP "BikeSystem"
#endif  // MBED_CONF_MBED_TRACE_ENABLE

namespace static_scheduling {

static constexpr std::chrono::milliseconds kGearTaskPeriod = 800ms;
static constexpr std::chrono::milliseconds kGearTaskDelay = 0ms;
static constexpr std::chrono::milliseconds kGearTaskComputationTime = 100ms;
static constexpr std::chrono::milliseconds kSpeedDistanceTaskPeriod = 400ms;
static constexpr std::chrono::milliseconds kSpeedDistanceTaskDelay = 0ms;
static constexpr std::chrono::milliseconds kSpeedDistanceTaskComputationTime = 200ms;
static constexpr std::chrono::milliseconds kDisplayTask1Period = 1600ms;
static constexpr std::chrono::milliseconds kDisplayTask1Delay = 300ms;
static constexpr std::chrono::milliseconds kDisplayTask1ComputationTime    = 200ms;
static constexpr std::chrono::milliseconds kResetTaskPeriod = 800ms;
static constexpr std::chrono::milliseconds kResetTaskDelay = 700ms;
static constexpr std::chrono::milliseconds kResetTaskComputationTime = 100ms;
static constexpr std::chrono::milliseconds kTemperatureTaskPeriod = 1600ms;
static constexpr std::chrono::milliseconds kTemperatureTaskDelay = 1100ms;
static constexpr std::chrono::milliseconds kTemperatureTaskComputationTime = 100ms;
static constexpr std::chrono::milliseconds kDisplayTask2Period = 1600ms;
static constexpr std::chrono::milliseconds kDisplayTask2Delay = 1200ms;
static constexpr std::chrono::milliseconds kDisplayTask2ComputationTime    = 100ms;

// TODO: implement the constructor

void BikeSystem::start() {
    tr_info("Starting Super-Loop without event handling");

    init();

    // TODO: implement the super-loop based for implementing the appropriate schedule
    while (true) {
        auto startTime = _timer.elapsed_time();

        // 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 = _timer.elapsed_time();
        const auto cycle =
            std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime);
        tr_debug("Repeating cycle time is %" PRIu64 " milliseconds", cycle.count());

        // TODO: implement loop exit when applicable

    }
}

void BikeSystem::stop() { core_util_atomic_store_bool(&_stopFlag, true); }

#if defined(MBED_TEST_MODE)
const advembsof::TaskLogger& BikeSystem::getTaskLogger() { return _taskLogger; }
#endif  // defined(MBED_TEST_MODE)

void BikeSystem::init() {
    // start the timer
    _timer.start();

    // initialize the lcd display
    disco::ReturnCode rc = _displayDevice.init();
    if (rc != disco::ReturnCode::Ok) {
        tr_error("Failed to initialized the lcd display: %d", static_cast<int>(rc));
    }

    // initialize the sensor device
    bool present = _sensorDevice.init();
    if (!present) {
        tr_error("Sensor not present or initialization failed");
    }

    // enable/disable task logging
    _taskLogger.enable(true);
}

void BikeSystem::gearTask() {
    // gear task
    auto taskStartTime = _timer.elapsed_time();

    // no need to protect access to data members (single threaded)
    _currentGear     = _gearDevice.getCurrentGear();
    _currentGearSize = _gearDevice.getCurrentGearSize();

    _taskLogger.logPeriodAndExecutionTime(
        _timer, advembsof::TaskLogger::kGearTaskIndex, taskStartTime);
}

void BikeSystem::speedDistanceTask() {
    // speed and distance task
    auto taskStartTime = _timer.elapsed_time();

    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();

    _taskLogger.logPeriodAndExecutionTime(
        _timer, advembsof::TaskLogger::kSpeedTaskIndex, taskStartTime);
}

void BikeSystem::temperatureTask() {
    auto taskStartTime = _timer.elapsed_time();

    // no need to protect access to data members (single threaded)
    _currentTemperature = _sensorDevice.readTemperature();

    // simulate task computation by waiting for the required task computation time

    std::chrono::microseconds elapsedTime = std::chrono::microseconds::zero();
    while (elapsedTime < kTemperatureTaskComputationTime) {
        elapsedTime = _timer.elapsed_time() - taskStartTime;
    }

    _taskLogger.logPeriodAndExecutionTime(
        _timer, advembsof::TaskLogger::kTemperatureTaskIndex, taskStartTime);
}

void BikeSystem::resetTask() {
    auto taskStartTime = _timer.elapsed_time();

    if (_resetDevice.checkReset()) {
        std::chrono::microseconds responseTime =
            _timer.elapsed_time() - _resetDevice.getPressTime();
        tr_info("Reset task: response time is %" PRIu64 " usecs", responseTime.count());
        _speedometer.reset();
    }

    _taskLogger.logPeriodAndExecutionTime(
        _timer, advembsof::TaskLogger::kResetTaskIndex, taskStartTime);
}

void BikeSystem::displayTask1() {
    auto taskStartTime = _timer.elapsed_time();

    _displayDevice.displayGear(_currentGear);
    _displayDevice.displaySpeed(_currentSpeed);
    _displayDevice.displayDistance(_traveledDistance);

    // simulate task computation by waiting for the required task computation time

    std::chrono::microseconds elapsedTime = std::chrono::microseconds::zero();
    while (elapsedTime < kDisplayTask1ComputationTime) {
        elapsedTime = _timer.elapsed_time() - taskStartTime;
    }

    _taskLogger.logPeriodAndExecutionTime(
        _timer, advembsof::TaskLogger::kDisplayTask1Index, taskStartTime);
}

void BikeSystem::displayTask2() {
    auto taskStartTime = _timer.elapsed_time();

    _displayDevice.displayTemperature(_currentTemperature);

    // simulate task computation by waiting for the required task computation time

    std::chrono::microseconds elapsedTime = std::chrono::microseconds::zero();
    while (elapsedTime < kDisplayTask2ComputationTime) {
        elapsedTime = _timer.elapsed_time() - taskStartTime;
    }
    _taskLogger.logPeriodAndExecutionTime(
        _timer, advembsof::TaskLogger::kDisplayTask2Index, taskStartTime);
}

}  // namespace static_scheduling

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) as follows:

  • The constructor is missing: you must implement it.
  • The start() method is empty: you must implement it. This method implements the cyclic scheduling as computed.

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. The task is then run and the method calls the TaskLogger::logPeriodAndExecutionTime(). This allows for registering task time information in the TaskLogger instance.

If you run your BikeComputer program with trace level set to TRACE_LEVEL_DEBUG, you should also observe an output similar to the one below on the console:

Log

[DBG ][TaskLogger]: Gear task: period 800550 usecs execution time 100002 usecs start time 1797299 usecs
[DBG ][TaskLogger]: Speed task: period 400384 usecs execution time 200079 usecs start time 1897382 usecs
[DBG ][TaskLogger]: Display(1) task: period 1600954 usecs execution time 200001 usecs start time 2097541 usecs
[DBG ][TaskLogger]: Speed task: period 400245 usecs execution time 200080 usecs start time 2297627 usecs
[DBG ][TaskLogger]: Reset task: period 800628 usecs execution time 100002 usecs start time 2497786 usecs
[DBG ][TaskLogger]: Gear task: period 800575 usecs execution time 100003 usecs start time 2597874 usecs
[DBG ][TaskLogger]: Speed task: period 400331 usecs execution time 200079 usecs start time 2697958 usecs
[DBG ][TaskLogger]: Temperature task: period 1601123 usecs execution time 100001 usecs start time 2898117 usecs
[DBG ][TaskLogger]: Display(2) task: period 1601207 usecs execution time 100001 usecs start time 2998203 usecs
[DBG ][TaskLogger]: Speed task: period 400331 usecs execution time 200079 usecs start time 3098289 usecs
[DBG ][TaskLogger]: Reset task: period 800662 usecs execution time 100002 usecs start time 3298448 usecs
[DBG ][BikeSystem]: Repeating cycle time is 1601 milliseconds

From this log, one can observe that:

  • The Major Cycle or Repeating Cycle Time is as expected \(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.

Run the test program

For checking your implementation of the start() method, you must run the test program given below successfully. The test program validates that the tasks run at the correct period times for the correct computation times. If the test fails, run the test program in verbose mode, detect the error and fix the scheduling of the tasks.

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

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

#include <chrono>

#include "static_scheduling/bike_system.hpp"

#include "greentea-client/test_env.h"
#include "mbed.h"
#include "task_logger.hpp"
#include "unity/unity.h"
#include "utest/utest.h"

using namespace utest::v1;

// test_bike_system handler function
static void test_bike_system() {
    // create the BikeSystem instance
    static_scheduling::BikeSystem bikeSystem;

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

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

    // stop the bike system
    bikeSystem.stop();

    // check whether scheduling was correct
    // Order is kGearTaskIndex, kSpeedTaskIndex, kTemperatureTaskIndex,
    //          kResetTaskIndex, kDisplayTask1Index, kDisplayTask2Index
    constexpr std::chrono::microseconds taskComputationTimes[] = {
        100000us, 200000us, 100000us, 100000us, 200000us, 100000us};
    constexpr std::chrono::microseconds taskPeriods[] = {
        800000us, 400000us, 1600000us, 800000us, 1600000us, 1600000us};

    // allow for 2 msecs offset
    uint64_t deltaUs = 2000;
    for (uint8_t taskIndex = 0; taskIndex < advembsof::TaskLogger::kNbrOfTasks;
         taskIndex++) {
        TEST_ASSERT_UINT64_WITHIN(
            deltaUs,
            taskPeriods[taskIndex].count(),
            bikeSystem.getTaskLogger().getPeriod(taskIndex).count());
        TEST_ASSERT_UINT64_WITHIN(
            deltaUs,
            taskComputationTimes[taskIndex].count(),
            bikeSystem.getTaskLogger().getComputationTime(taskIndex).count());
    }
}

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

    return greentea_test_setup_handler(number_of_cases);
}

// List of test cases in this file
static Case cases[] = {Case("test bike system", test_bike_system)};

static Specification specification(greentea_setup, cases);

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

In the test program, the time information is extracted from the TaskLogger instance for each task and the period and computation times are checked to be within the expected range. For accounting for some variations due to imprecisions and extra statements like printf statement, the test programs allows for a variation of \(2\,\mathsf{ms}\). Also, for mitigating the relative effect of calls to printf, we have defined task run times in ms units. Note that there exist other means for logging task time information but those usually requires tools that are not available in the scope of this lecture.

Code instrumentation for measuring the response time

In the resetTask() method, if the user presses the button, then the speedometer is reset. In this method, the program also computes the task response time, which is the time elapsed between the button press (or more precisely the button press detection time) and the time at which the reset request was handled. Observe the implementation of the BikeSystem::resetTask() for understanding how the reset response time is computed.

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

[INFO][BikeSystem]: Reset task: response time is 205678 usecs

[INFO][BikeSystem]: Reset task: response time is 189442 usecs

[INFO][BikeSystem]: Reset task: response time is 470790 usecs

When running the program, you should also observe that the push button must be pressed long enough for the event to be detected in your main program.

Question 1: Response time of the reset event

When you run multiple tests for computing the response time of the reset event, you may observe the following:

  • There is a large variation in the response time values, from a few milliseconds to about \(800\,\mathsf{ms}\).
  • 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.

Solution

The period of the reset task is \(800\,\mathsf{ms}\). This means that if the user presses the button just after the task completed, it has to wait until the next reset task starts for the press to be detected. For the same reason, the response time is distributed between \(0\,\mathsf{ms}\) and \(800\,\mathsf{ms}\).

Response time to specific events need to be short and the jitter in response times also need to be short. This makes an embedded program behave in a more predictable way.

Wrap-up: software architecture

The class documented in this codelab follow the follow class architecture:

Class Diagram Class Diagram for the BikeComputer program

You should make sure that you have not deviated from this architecture. Note that the constructors and method arguments are not documented in this class diagram. Classes delivered in the libraries are not documented either.

Wrap-up: limitations of this implementation

The behaviors described above are not the only limitations of this implementation, but a varying and large response time is a problem that needs to be solved. As a solution, we will in the next part of the codelab implement the button press with an asynchronous mechanism, allowing for instantaneous handling of the event. We will start making the next version of the program event-driven!

Deliverables

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 run successfully, as documented in the codelab. The github workflow also includes the build of these three tests.
  • The precommit configuration file has been modified for including all files added in this codelab - the imported libraries should not be added. All software quality tools succeeds, changes are committed and push to your repository.