Aller au contenu

Multi-tasking under Mbed OS

Introduction

Multi-tasking Programs

In this codelab, we will learn basic principle for creating multi-tasking application using Mbed OS, namely:

  • How to define threads for specific tasks of a program.
  • How to synchronize multiple threads and control access to shared resources.
  • Study the influence of shared resources on task scheduling.

Ultimately, we will transform the BikeComputer program into a multi-tasking concurrent application.

What you’ll build

In this codelab, you’re going to program simple programs using various multi-tasking primitives. Programs will be presented as exercices that run independently from each other.

What you’ll learn

  • How to use threads for performing different tasks in a program.
  • How to share resources among different threads.
  • How to synchronize tasks based on resources or events, including interrupt-based events.
  • What the influence of shared resources can have on task scheduling.

What you’ll need

Threads in Mbed OS

Mbed OS runs on single-microcontroller embedded systems. In this context, a thread is an independent segment of the program that executes within the single process running on the microcontroller. Threading allows multiple tasks to run concurrently using a scheduler. It is important to point out that threads provide a lot of flexibility to the developer but come at the cost of resources for the thread itself and for the scheduler.

Your application (main function) starts execution in the main thread, but it’s not the only thread running in Mbed OS. When starting an Mbed OS application, there are three threads running system services:

  • Main: The default thread that executes the application’s main function. The main thread has 4KiB of stack space by default. The application can configure it in “mbed_app.json” by defining the `MAIN_STACK_SIZE`` parameter, as follows (note that “config” is a node at the top level of the configuration parameters)
multi_tasking/mbed_app.json
...
"config": {
  "main-stack-size": {
   "value": 8192
  }
}
...
  • Idle: The thread that’s run by the scheduler when there’s no other activity in the system (for example, all other threads are waiting for some event). It’s used to make sure the board is not burning empty processor cycles, but is put to sleep for as long as possible.

  • Timer: The thread that handles system and user timer objects.

Using the MemoryLogger helper class for logging thread info

A MemoryLogger class is made available in the AdvEmbSof library. You may use this class in your BikeSystem implementation for logging statistics related to each thread running in your application.

If you add a MemoryLogger instance as data member of your BikeComputer class and add a call to the getAndPrintStatistics() method at the start of the BikeComputer::start() method, you may observe these three different threads, as shown below:

Thread statistics logging
[DBG ][MemoryLogger]: Thread Info:
[DBG ][MemoryLogger]:   Thread: 0
[DBG ][MemoryLogger]:           Thread Id: 0x240035b0 with name main, state Running, priority 24
[DBG ][MemoryLogger]:           Stack size 8192 (free bytes remaining 5816)
[DBG ][MemoryLogger]:   Thread: 1
[DBG ][MemoryLogger]:           Thread Id: 0x24003528 with name rtx_idle, state Ready, priority 1
[DBG ][MemoryLogger]:           Stack size 896 (free bytes remaining 832)
[DBG ][MemoryLogger]:   Thread: 2
[DBG ][MemoryLogger]:           Thread Id: 0x2400356c with name rtx_timer, state Waiting, priority 40
[DBG ][MemoryLogger]:           Stack size 768 (free bytes remaining 672)

Using EventFlags For Waiting For an Event

It is very common in a multi-tasking environment to wait for a specific event to happen before performing a given task. An obvious example is to wait for a user to press for a button. For this purpose, Mbed OS provides the EventFlags API.

For demonstrating the use of this API, we create a WaitOnButton class defined as follows:

wait_on_button.hpp
wait_on_button.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 wait_on_button.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief WaitOnButton class used for demonstrating EventFlags
 * @date 2022-09-01
 * @version 0.1.0
 ***************************************************************************/

#pragma once

#include "mbed.h"

#include "mbed_trace.h"

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

namespace multi_tasking {

#if defined(TARGET_DISCO_H747I)
#define PUSH_BUTTON BUTTON1
#define POLARITY_PRESSED 0
#endif

class WaitOnButton {
public:
    explicit WaitOnButton(const char* threadName) :
        _thread(osPriorityNormal, OS_STACK_SIZE, nullptr, threadName),
        _pushButton(PUSH_BUTTON) 
    {
        _pushButton.fall(callback(this, &WaitOnButton::buttonPressed));
        _timer.start();
        _pressedTime = std::chrono::microseconds::zero();
    }

    void start() {
        osStatus status = _thread.start(callback(this, &WaitOnButton::waitForButtonEvent));
        tr_debug("Thread %s started with status %d", _thread.get_name(), status);
    }

    void wait_started() {
        _eventFlags.wait_any(kStartedEventFlag);
    }

    void wait_exit() {
        _thread.join();
    }

private:
    void waitForButtonEvent() {
        tr_debug("Waiting for button press");
        _eventFlags.set(kStartedEventFlag);
        while (true) {
            _eventFlags.wait_all(kPressedEventFlag);
            std::chrono::microseconds time = _timer.elapsed_time();
            std::chrono::microseconds latency = time - _pressedTime;
            tr_debug("Button pressed with response time: %lld usecs", latency.count());
            tr_debug("Waiting for button press");
        }
    }

    void buttonPressed() {    
        _pressedTime = _timer.elapsed_time();
        _eventFlags.set(kPressedEventFlag);
    }

    static constexpr uint8_t kPressedEventFlag = (1UL << 0);
    static constexpr uint8_t kStartedEventFlag = (1UL << 1);
    Thread _thread;
    Timer _timer;
    std::chrono::microseconds _pressedTime;
    EventFlags _eventFlags;
    InterruptIn _pushButton;
};

} // namespace multi_tasking

In the WaitOnButton class, we create an additional thread and start it in the WaitOnButton::start() method. The thread executes the WaitOnButton::waitForButtonEvent() method that waits in an infinite loop for a specific event to happen. The event is set upon button press in the WaitOnButton::buttonPressed() method.

The code of the main function is shown below:

main_button.cpp
main_button.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 main function for demonstrating WaitOnButton
 * @date 2022-09-01
 * @version 0.1.0
 ***************************************************************************/

#include "mbed.h"
#include "mbed_trace.h"
#include "wait_on_button.hpp"
#include "memory_logger.hpp"
#if MBED_CONF_MBED_TRACE_ENABLE
#undef TRACE_GROUP
#define TRACE_GROUP "main"
#endif  // MBED_CONF_MBED_TRACE_ENABLE

int main() {
    // use trace library for console output
    mbed_trace_init();

    tr_debug("EventFlags program started\n");

    // log thread statistics
    advembsof::MemoryLogger memoryLogger;
    memoryLogger.getAndPrintThreadStatistics();

    // create the WaitOnButton instance and start it
    multi_tasking::WaitOnButton waitOnButton("ButtonThread");
    waitOnButton.start();

    // wait that the WaitOnButton thread started 
    waitOnButton.wait_started();

    // log thread statistics
    memoryLogger.getAndPrintThreadStatistics();

    // wait for the thread to exit (will not because of infinite loop)
    waitOnButton.wait_exit();
    // or do busy waiting
    // while (true) {
    //}

    return 0;
}

In this example, we may note the following:

  • The use of the WaitOnButton instance in the main function is (self-)documented. Make sure that you understand it in details.
  • After starting the WaitOnButton instance, the main thread then waits that the thread exited. It will not happen and the main thread will wait forever in the waitOnButton.wait_exit() method. Note that it is required that the main thread never exits from the main() function for a proper behavior of an Mbed OS program.
  • Each time the button is pressed, the flag kPressedEventFlag is set on the _eventFlags instance in the WaitOnButton::buttonPressed() method. When this event is set, the “ButtonThread” thread will move from the WaitingEventFlag to the Running state. The kPressedEventFlag flag will be reset on the _eventFlags instance and the “ButtonThread” thread will start waiting again. You may experience this behavior by pressing multiple times on the button.

For a better understanding of the program, you may compile and run it. Upon program startup, you should observe the following in the console:

  • Before the WaitOnButton instance is created and started, the application runs the three “main”, “rtx_idle” and “rtx_timer” threads, as illustrated above.
  • After the WaitOnButton instance is started, an additional thread named “ButtonThread” is created and run by the application.
  • When the program is running, each time the button is pressed, the “ButtonThread” thread will move from the WaitingEventFlag state to the Running state and enter the WaitingEventFlag state immediately after printing its message on the console. This behavior is depicted in the diagram below (yellow oval):

Thread state change (waiting/running)

Thread state change (waiting/running)

Note that it is possible to wait for multiple events at once, as documented in the EventFlags API. This is a very useful mechanism in some situations where a task must be executed when multiple events arise. It is also possible to wait for a specific event as demonstrated in the WaitOnButton class.

Exercice Multi-tasking under Mbed OS/1

It is also useful to measure the interrupt latency time in this example. The WaitOnButton class implements such a mechanism by registering the press time in the ISR method, and by computing the time interval until the waitForButtonEvent thread is signaled. With this mechanism, we may then measure the interrupt latency.

You may observe that the interrupt latency varies depending on some multi-tasking behaviors. Test the following scenarios:

  • The main thread is in a Waiting state, waiting for the WaitOnButton thread to terminate using the Thread::join method. This is the default behavior as documented above.
  • Replace the call to Thread::join in the main function with a busy infinite wait while (true) {}.
  • Using the busy wait, modify the priority of the WaitOnButton thread to osPriorityAboveNormal (in the constructor)
  • Using the busy wait, modify the priority of the WaitOnButton thread to osPriorityBelowNormal (in the constructor).

Report the interrupt latency times for each scenario and explain what are possible reasons for the observed values.

Solution

For the different scenarios:

  • Thread::join: observed latency times should be around \(10\,\mathsf{us}\), with a very low jitter. The reason is that the main thread is in waiting state and it will never be running. The ISR is thus served very fast and with very little jitter.
  • Busy wait (button thread with normal priority): the observed latency time should be around \(1\,\mathsf{ms}\) with a larger jitter. The reason is that the main thread is always running and context switching to the routine serving the deferred ISR happens through round-robin.
  • Busy wait (button thread with higher priority): the routine serving the deferred ISR prempts the main thread and the observed latency time should be around \(30\,\mathsf{us}\), with very small jitter.
  • Busy wait (button thread with lower priority): the routine serving the deferred _ISR is never executed, because the main thread is running with higher priority, without any interruption (premption).

Shared Resources and Mutual Exclusion

Multiple threads can run concurrently on a uniprocessor with interleaved or time shared execution. On multiple processor systems, multiple threads can also run simultaneously. In both cases, resource management like access to shared memory/variables by multiple threads is an issue. A multitasking or multithreaded program must make sure that resources are accessed properly, meaning in the correct sequence as if the program was executed by a single thread. In other words, in some cases, the program must enforce a mutually exclusive access to the shared resource to only one thread at a time. For this purpose, most _RTOS_es provide a so-called mutex mechanism that can be implemented both at the hardware or software level.

We first illustrate this problem with an example that simulates a clock keeping track of the current time. For updating the clock, a ticker with 1s interrupt interval is used. The structure for defining the time and the ISR for updating it are given below:

clock.hpp
clock.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 clock.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief Clock class used for demonstrating race condition/critical section
 * @date 2022-09-01
 * @version 0.1.0
 ***************************************************************************/

#pragma once

#include "mbed.h"

namespace multi_tasking {

class Clock {
public:
    struct DateTimeType {
        uint32_t day;
        uint32_t hour;
        uint32_t minute;
        uint32_t second;
    };

    Clock() = default;

    void start() {
        // start a ticker thread for dispatching events that are queued in the tickerUpdate() method
        _tickerThread.start(callback(&_tickerQueue, &EventQueue::dispatch_forever));
        // call the tickerUpdate() method every second, for queueing an event to be dispatched by the ticker thread
        _ticker.attach(callback(this, &Clock::tickerUpdate), clockUpdateTimeout);

        // schedule an event every second for displaying the time on the console
        _clockDisplayQueue.call_every(clockDisplayTimeout, 
                                      callback(this, &Clock::getAndPrintDateTime));
        // dispatch events from the thread calling the start() method (main thread)
        _clockDisplayQueue.dispatch_forever();
    }

private:
    void getAndPrintDateTime() {
        DateTimeType dt = {0};

        dt.day = _currentTime.day;
        dt.hour = _currentTime.hour;

        dt.minute = _currentTime.minute;
        dt.second = _currentTime.second;

        printf("Day %u Hour %u min %u sec %u\n", dt.day, dt.hour, dt.minute, dt.second);
    }

    void tickerUpdate() {
        _tickerQueue.call(callback(this, &Clock::updateCurrentTime));
    }

    void updateCurrentTime() {
        _currentTime.second += std::chrono::duration_cast<std::chrono::seconds>(
          clockUpdateTimeout).count();

        if (_currentTime.second > 59) {
            _currentTime.second = 0;
            _currentTime.minute++;
            if (_currentTime.minute > 59) {
                _currentTime.minute = 0;
                _currentTime.hour++;
                if (_currentTime.hour > 23) {
                    _currentTime.hour = 0;
                    _currentTime.day++;
                }
            }
        }
    }

    EventQueue _clockDisplayQueue;
    Ticker _ticker;
    EventQueue _tickerQueue;
    Thread _tickerThread;
    DateTimeType _currentTime{ .day = 0, .hour = 10, .minute = 59, .second = 59};
    static constexpr std::chrono::milliseconds clockUpdateTimeout = 1000ms;
    static constexpr std::chrono::milliseconds clockDisplayTimeout = 1000ms;
};

} // namespace multi_tasking

It is important to point out that the update of the current time (method updateCurrentTime()) is not executed within the ISR but it is rather deferred to the _tickerQueue event queue - so it is executed by the _tickerThread thread. The main reason is that blocking behaviors are forbidden in ISRs - so acquiring a mutex in an ISR is not possible. Acquiring the mutex in the updateCurrentTime() method is not currently done, but we will show below that it is required.

The main program used for demonstrating the use of the Clock class is shown below:

main_clock.cpp
main_clock.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 main function for demonstrating Clock
 * @date 2022-09-01
 * @version 0.1.0
 ***************************************************************************/

#include "clock.hpp"
#include "mbed.h"
#include "mbed_trace.h"
#if MBED_CONF_MBED_TRACE_ENABLE
#undef TRACE_GROUP
#define TRACE_GROUP "main"
#endif  // MBED_CONF_MBED_TRACE_ENABLE

int main() {
    // use trace library for console output
    mbed_trace_init();

    tr_debug("Clock update program started");

    // create and start a clock
    multi_tasking::Clock clock;
    clock.start();

    return 0;
}

If you execute the program, you should get the following output:

Clock times

Clock update program started
Day 0 Hour 10 min 59 sec 59
Day 0 Hour 11 min 0 sec 0
Day 0 Hour 11 min 0 sec 1
Day 0 Hour 11 min 0 sec 2
Day 0 Hour 11 min 0 sec 3
Day 0 Hour 11 min 0 sec 5
Day 0 Hour 11 min 0 sec 6
...

So everything looks fine and it seems that the above program will always execute correctly.

There is however a race condition issue in this program: the problem is that an interrupt at the wrong time may lead to half-updated values in the getAndPrintDateTime() method. The problem can be detailed as follows:

  • Say the current time is day: 0, hour: 10, minute: 59, second: 59 or {0, 10, 59, 59}.
  • The first call to getAndPrintDateTime() is executed from the main() function (as an event in the queue). The first two instructions of the function are executed and thus { 0, 10 } are copied to dt.day/hour.
  • A timer interrupt occurs, which updates the current time to {0, 11, 0, 0}.
  • Later, the getAndPrintDateTime() function resumes executing and copies the remaining current_time fields to dt, thus dt.minute/second = {0, 0}.
  • dt contains the values {0, 10, 0, 0} and the program believes that the time just jumped backwards one hour !

Although the failure case described above can happen, it is unlikely to happen. For making it easily happen, it is enough to have our getAndPrintDateTime() method to go to sleep for \(1\,\mathsf{s}\) after the execution of the instruction updating the hour. In this case, the ISR will for sure execute before the execution of getAndPrintDateTime() resumes and the failure will happen. If you modify the getAndPrintDateTime() function as follows:

clock_with_wait.hpp
clock_with_wait.hpp
...
void getAndPrintDateTime() 
{
    DateTimeType dt = {0};

    dt.day = _currentTime.day;
    dt.hour = _currentTime.hour;

    static constexpr std::chrono::microseconds waitTime = 1s;
    wait_us(waitTime.count());

    dt.minute = _currentTime.minute;  
    dt.second = _currentTime.second;

    printf("Day %d Hour %d min %d sec %d\n", dt.day, dt.hour, dt.minute, dt.second);
  }
...

you should observe an output on the console similar to the one below. This shows a jump in the clock time:

Clock times

Clock update program started
Day 0 Hour 10 min 0 sec 0
Day 0 Hour 11 min 0 sec 2
Day 0 Hour 11 min 0 sec 3
Day 0 Hour 11 min 0 sec 4
Day 0 Hour 11 min 0 sec 5
Day 0 Hour 11 min 0 sec 6
Day 0 Hour 11 min 0 sec 7

The output above exhibits a typical race condition problem. Preemption enables the ISR to interrupt the execution of another code and to possibly overwrite data. To resolve this problem, one must ensure atomic or indivisible access to the object, in our case the _currentTime object. In this case, and for uniprocessor systems, one solution would be to disable or turn off interrupts at the beginning of the getAndPrintDateTime() function and turn it on later. This solution solves the problem but it may lead to missing interrupt events - in this particular case, the current displayed time becomes incorrect.

The solution that disables interrupts during the atomic access is :

clock_with_irq_disabled.hpp
clock_with_irq_disabled.hpp
void getAndPrintDateTime() {
    ...

    uint32_t m = __get_PRIMASK();
    __disable_irq();

    dt.day = _currentTime.day;
    dt.hour = _currentTime.hour;
    dt.minute = _currentTime.minute;  
    dt.second = _currentTime.second;

    __set_PRIMASK(m);

    ...
  }

In the code above, since we know that an ISR can write to our shared data object, we disable the interrupt, after saving the current interrupt masking state. We then restore the masking state at the end of the function. The same code needs to be inserted in the updateCurrentTime() method for protecting access to _currentTime. Again, although this solves the problem, disabling interrupts should be avoided since it may affect other processing requests.

Another way of protecting the critical section is with the use of mutual exclusion objects or mutex. The goal of a mutex is to ensure that only one thread is executing in a critical section at the same time. Mutex can thus synchronize the execution of threads and protect access to a shared resource as illustrated below:

Mutex and threads

Mutex and threads

Exercice Multi-tasking under Mbed OS/2

Remove the code for disabling interrupts and modify the Clock class for implementing a thread safe class using a Mutex instance.

Note that using a Mutex in our example is possible only because the execution of updateCurrentTime() is deferred to the _tickerThread thread.

Indeed, Mutex objects cannot be used from ISR context, since acquiring a mutex is a blocking operation.

Solution

The following steps are required:

  • Remove the code for disabling interrupts in all methods.
  • Add a Mutex as a data member of the Clock class.
  • Add the code for protecting access to _currentTime in the getAndPrintDateTime() and updateCurrentTime() methods.

The solution is given here:

clock_with_mutex.hpp
clock_with_mutex.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 clock_with_mutex.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief Clock class used for demonstrating race condition/critical section
 * @date 2022-09-01
 * @version 0.1.0
 ***************************************************************************/

#pragma once

#include <chrono>

namespace multi_tasking {

class Clock {
public:
    struct DateTimeType {
        uint32_t day;
        uint32_t hour;
        uint32_t minute;
        uint32_t second;
    };

    Clock() = default;

    void start() {
        // start a ticker thread for dispatching events that are queued in the tickerUpdate() method
        _tickerThread.start(callback(&_tickerQueue, &EventQueue::dispatch_forever));
        // call the tickerUpdate() method every second, for queueing an event to be dispatched by the ticker thread
        _ticker.attach(callback(this, &Clock::tickerUpdate), clockUpdateTimeout);

        // schedule an event every second for displaying the time on the console
        _clockDisplayQueue.call_every(clockDisplayTimeout, callback(this, &Clock::getAndPrintDateTime));
        // dispatch events from the thread calling the start() method (main thread)
        _clockDisplayQueue.dispatch_forever();
    }

private:
    void getAndPrintDateTime() {
        DateTimeType dt = {0};

        _currentTimeMutex.lock();

        dt.day = _currentTime.day;
        dt.hour = _currentTime.hour;
        dt.minute = _currentTime.minute;
        dt.second = _currentTime.second;

        _currentTimeMutex.unlock();

        printf("Day %d Hour %d min %d sec %d\n", dt.day, dt.hour, dt.minute, dt.second);
    }

    void tickerUpdate() {
        _tickerQueue.call(callback(this, &Clock::updateCurrentTime));
    }

    void updateCurrentTime() {
        _currentTimeMutex.lock();

        _currentTime.second += 
          std::chrono::duration_cast<std::chrono::seconds>(clockUpdateTimeout).count();
        if (_currentTime.second > 59) {
            _currentTime.second = 0;
            _currentTime.minute++;
            if (_currentTime.minute > 59) {
                _currentTime.minute = 0;
                _currentTime.hour++;
                if (_currentTime.hour > 23) {
                    _currentTime.hour = 0;
                    _currentTime.day++;
                }
            }
        }

        _currentTimeMutex.unlock();
    }

    EventQueue _clockDisplayQueue;
    Ticker _ticker;
    EventQueue _tickerQueue;
    Thread _tickerThread;
    DateTimeType _currentTime{ .day = 0, .hour = 10, .minute = 59, .second = 59};
    Mutex _currentTimeMutex;
    static constexpr std::chrono::milliseconds clockUpdateTimeout = 1000ms;
    static constexpr std::chrono::milliseconds clockDisplayTimeout = 1000ms;
};

} // namespace multi_tasking

Mutex not allowed from ISR context

For experiencing the unallowed use of the Mutex mechanism in an ISR, you may modify the callback used by the ticker as follows:

clock_unallowed_ISR.hpp
...
_ticker.attach(callback(this, &Clock::updateCurrentTime), clockUpdateTimeout);
...

With this change, the updateCurrentTime() method becomes the ISR of the ticker and is thus executed from ISR context. In this case, your program should halt with a message stating that the use of a Mutex is not allowed from ISR context.

Using the ConditionVariable API for waiting or signaling state changes

It is worth noting that Mbed OS provides a mechanism called ConditionVariable for safely waiting for or signaling state changes. When protecting shared resources with a mutex and then releasing that mutex to wait for a change of that data, a condition variable provides a safe solution by handling the wait for a state change, along with releasing and acquiring the mutex automatically during this waiting period. However, unlike EventFlags, ConditionVariable does not let you wait on multiple events at the same time.

Deadlock

Deadlocks can arise in situations where two or more threads wait for each other to complete the task. A deadlock is thus an infinite wait and it occurs when a thread enters a wait state because a shared resource that was requested is being held by another waiting thread, which in turn is waiting for another resource held by another waiting thread.

A number of solutions have been developed for analyzing and preventing deadlock situations. For analyzing situations where deadlocks may arise, Coffman proved that deadlock situation on a resource can arise if and only if all of the following conditions illustrated in the figure below hold simultaneously:

  1. Mutex: exclusive resource, non-shareable (Resource A and B in the figure below)
  2. Resource holding: request additional resources while holding one (Requesting resource B while holding Resource A, Resource B held by another process)
  3. No preemption: resource can not be de-allocated or forcibly removed (Resource B cannot be deallocated)
  4. Circular wait: circular dependency or a closed chain of dependency (A vs B, Process 1 vs Process 2).

Deadlock conditions

Deadlock conditions

Deadlock prevention works by preventing one of the four Coffman conditions from occurring:

  1. Removing the mutual exclusion condition means that no process will have exclusive access to a resource (called non-blocking synchronization algorithms).
  2. The hold and wait or resource holding conditions are changed by making processes ask for all the resources they will need before starting up (difficult to implement).
  3. Allowing the preemption condition will cause difficulty and the processing outcome may be inconsistent or thrashing may occur.
  4. The final condition is the circular wait condition. Approaches that avoid circular waits include disabling interrupts during critical sections.

A very simple example for demonstrating a deadlock condition is shown below:

deadlock.hpp
deadlock.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 deadlock.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief Deadlock class used for demonstrating deadlock
 * @date 2022-09-01
 * @version 0.1.0
 ***************************************************************************/

#pragma once

#include "mbed.h"

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

namespace multi_tasking {

class Deadlock {
public:
    Deadlock(int index, const char *threadName) :
        _index(index),
        _thread(osPriorityNormal, OS_STACK_SIZE, nullptr, threadName) {
    }

    void start() {
        osStatus status = _thread.start(callback(this, &Deadlock::execute));
        tr_debug("Thread %s started with status %d", _thread.get_name(), status);
    }

    void wait() {
        _thread.join();
    }

private:
    void execute() {
        // enter the first critical section
        _mutex[_index].lock();
        tr_debug("Thread %d entered critical section %d", _index, _index);

        // perform some operations
        wait_us(kProcessingWaitTime.count());
        tr_debug("Thread %d processing in mutex %d done", _index, _index);

        // enter the second critical section
        int secondIndex = (_index + 1) % kNbrOfMutexes;
        tr_debug("Thread %d trying to enter critical section %d", _index, secondIndex);
        _mutex[secondIndex].lock();
        tr_debug("Thread %d entered critical section %d", _index, secondIndex);

        // perform some operations
        wait_us(kProcessingWaitTime.count());
        tr_debug("Thread %d processing in mutex %d and %d done", 
                 _index, _index, secondIndex);

        // exit the second critical section
        _mutex[secondIndex].unlock();

        // perform some operations
        wait_us(kProcessingWaitTime.count());
        tr_debug("Thread %d processing in mutex %d done", 
                 _index, _index);

        // exit the first critical section
        _mutex[_index].unlock();
    }

    // time that the threads should spend processing (e.g. wait in our case)
    static constexpr std::chrono::microseconds kProcessingWaitTime = 1000000us;
    static constexpr int kNbrOfMutexes = 2;
    int _index;
    Thread _thread;
    // the mutex must be declared as static for being a class instance
    static Mutex _mutex[kNbrOfMutexes];
};

} // namespace multi_tasking
main_deadlock.cpp
multi-tasking/main_deadlock.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 main function for demonstrating Deadlock
 * @date 2022-09-01
 * @version 0.1.0
 ***************************************************************************/

#include "deadlock.hpp"
#include "mbed.h"
#include "mbed_trace.h"
#if MBED_CONF_MBED_TRACE_ENABLE
#undef TRACE_GROUP
#define TRACE_GROUP "main"
#endif  // MBED_CONF_MBED_TRACE_ENABLE

// declare static variables
Mutex multi_tasking::Deadlock::_mutex[kNbrOfMutexes];

int main() {
    // use trace library for console output
    mbed_trace_init();

    tr_debug("Deadlock program started");

    // create a first deadlock instance
    multi_tasking::Deadlock deadlock0(0, "Thread0");
    deadlock0.start();

    // create a second deadlock instance
    multi_tasking::Deadlock deadlock1(1, "Thread1");
    deadlock1.start();

    // wait for both threads to terminate (will not because of deadlock)
    deadlock0.wait();
    deadlock1.wait();

    return 0;
}

In this example, we use two Mutex instances for synchronizing the execution of two threads. The program implements the circular wait as described in the conditions and in the figure above. The output of the program is shown below. As you can observe Thread 0 is blocked waiting for mutex 1 (owned by Thread 1) and Thread 1 is blocked waiting for mutex 0 (owned by Thread 0), since no thread can enter the second critical section.

Deadlock program

[DBG ][main]: Deadlock program started
[DBG ][Deadlock]: Thread Thread0 started with status 0
[DBG ][Deadlock]: Thread Thread1 started with status 0
[DBG ][Deadlock]: Thread 0 entered critical section 0
[DBG ][Deadlock]: Thread 1 entered critical section 1
[DBG ][Deadlock]: Thread 0 processing in mutex 0 done
[DBG ][Deadlock]: Thread 0 trying to enter critical section 1
[DBG ][Deadlock]: Thread 1 processing in mutex 1 done
[DBG ][Deadlock]: Thread 1 trying to enter critical section 0

Shared Data and Semaphore

Is a Mutex a Binary Semaphore?

Like a mutex, a semaphore can manage and control access to shared resources. Unlike a mutex, it does not have the concept of an owner (a mutex is owned by the thread that entered the critical section). On the contrary, a semaphore defines the maximum number of threads that are allowed to use a particular resource at the same time. A mutex is a locking mechanism while a semaphore is a signaling mechanism.

It is important to mention that mutex and semaphore are different concepts and that it is incorrect to think of a mutex as a binary semaphore. As explained in Mutexes and Semaphores Demystified, a mutex is meant to be taken and released, always in that order, by each task that uses the shared resource. By contrast, tasks that use semaphores either signal or wait - not both. In such, they are often related to producer-consumer problems. Also, mutex objects cannot be used within an ISR, while semaphores can be used to signal from ISR context to another thread - signaling is not a blocking mechanism. This makes semaphores a powerful mechanism for signaling from ISR context.

Semaphore for Access to Shared Resources

For illustrating the use of semaphores, we demonstrate a typical producer-consumer problem. The problem can be solved using different Mbed OS concurrency primitives and we will also evaluate their advantages and disadvantages of each approach.

  • The problem is stated as follows (with references to methods implemented in the Buffer, Consumer, and Producer classes (given below):
  • The scenario is executed by two separate tasks/threads: the producer thread implemented in the Producer class (_producerThread) and the consumer thread implemented in the Consumer class (_consumerThread).
  • Both threads share a FILO buffer implemented in the Buffer class (_buffer), with a single index for accessing the buffer (_index).
  • One producer will repeatedly produce an integer ranging from 0 to 19 at a random rate (Producer::produce() method). It will then append the produced value to the FILO buffer (Buffer::append() method).
  • One consumer will repeatedly consume an integer from the FILO buffer (Buffer::extract() method) and consume it (Consumer::consume() method).
  • The FILO buffer has no mutual exclusion, under- or overflow protection. This means that the index for accessing it can be negative or exceed the buffer size. It can also be accessed concurrently by the consumer and producer threads. In this example, the size of the buffer was arbitrarily chosen and does not reflect any required value.
  • For simulating computing time required for consuming or producing data, wait_us() calls are added in the Consumer::consume() and Producer::produce() methods. The same applies for simulating the time required for appending and extracting data to/from the buffer (in the Buffer::append() and Buffer::extract() methods).
  • The green led is used for making append to the buffer visible, while the yellow led is used for making extract from the buffer visible. Both leds are on when one or the other event happens.

The basic implementation of the Buffer class is given below:

buffer.hpp
buffer.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 buffer.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief Buffer class used for demonstrating consumer/producer
 * @date 2022-09-01
 * @version 0.1.0
 ***************************************************************************/

#pragma once

#include "mbed.h"

namespace multi_tasking {

#if defined(TARGET_DISCO_H747I)
#define GREEN_LED LED1
#define BLUE_LED LED4
static constexpr uint8_t kLedOn = 0;
static constexpr uint8_t kLedOff = 1;
#endif

class Buffer {
public:
    Buffer() :
        _producerLed(GREEN_LED),
        _consumerLed(BLUE_LED) {
        // initialize random seed
        srand(time(NULL));

        _producerLed = kLedOff;
        _consumerLed = kLedOff;
    }

    void append(uint32_t datum) {
        _producerLed = kLedOn;
        _buffer[_index] = datum;
        _index++;
        wait_us(computeRandomWaitTime(kApppendWaitTime));
        _producerLed = kLedOff;
    }

    uint32_t extract(void) {
        _consumerLed = kLedOn;
        _index--;
        wait_us(computeRandomWaitTime(kExtractWaitTime));
        int datum = _buffer[_index];
        _consumerLed = kLedOff;
        return datum;
    }

    int computeRandomWaitTime(const std::chrono::microseconds &waitTime) {
        return rand() % waitTime.count() + waitTime.count();
    }

    int count() {
        return _index;
    }

private:
    static constexpr uint8_t kBufferSize = 10;
    static const std::chrono::microseconds kApppendWaitTime;
    static const std::chrono::microseconds kExtractWaitTime;
    DigitalOut _producerLed;
    DigitalOut _consumerLed;
    uint32_t _buffer[kBufferSize] = {0};
    int _index = 0;
};

} // namespace multi_tasking

The code of the producer that uses the Buffer class is:

producer.hpp
producer.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 producer.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief Producer class used for demonstrating producer/consumer
 * @date 2022-09-01
 * @version 0.1.0
 ***************************************************************************/

#pragma once

#include "mbed_trace.h"

#include "buffer.hpp"

#if MBED_CONF_MBED_TRACE_ENABLE
#undef TRACE_GROUP
#define TRACE_GROUP "Producer"
#endif // MBED_CONF_MBED_TRACE_ENABLE

namespace multi_tasking {

class Producer {
public:
    explicit Producer(Buffer &buffer) :
        _buffer(buffer),
        _producerThread(osPriorityNormal, OS_STACK_SIZE, nullptr, "ProducerThread") {
        // initialize random seed
        srand(time(NULL));
    }

    void start() {
        _producerThread.start(callback(this, &Producer::producerMethod));
    }

    void wait() {
        _producerThread.join();
    }

private:
    int produce(void) {
        wait_us(_buffer.computeRandomWaitTime(kProduceWaitTime));
        // Produce a random number ranging from 0 to kMaxRandomValue - 1
        return rand() % kMaxRandomValue;
    }

    void producerMethod() {
        while (true) {
            int producerDatum = produce();
            _buffer.append(producerDatum);
            tr_debug("Producer datum is %d (index %d)", producerDatum, _buffer.count());
        }
    }

private:
    static const std::chrono::microseconds kProduceWaitTime;
    static constexpr uint8_t kMaxRandomValue = 20;
    Buffer &_buffer;
    Thread _producerThread;
};

} // namespace multi_tasking

The code of the consumer that uses the Buffer class is:

consumer.hpp
consumer.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 consumer.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief Consumer class used for demonstrating producer/consumer
 * @date 2022-09-01
 * @version 0.1.0
 ***************************************************************************/

#pragma once

#include "mbed_trace.h"

#include "buffer.hpp"

#if MBED_CONF_MBED_TRACE_ENABLE
#undef TRACE_GROUP
#define TRACE_GROUP "Consumer"
#endif // MBED_CONF_MBED_TRACE_ENABLE

namespace multi_tasking {

class Consumer {
public:
    explicit Consumer(Buffer &buffer) :
        _buffer(buffer),
        _consumerThread(osPriorityNormal, OS_STACK_SIZE, nullptr, "ConsumerThread") {
    }

    void start() {
        _consumerThread.start(callback(this, &Consumer::consumerMethod));
    }

    void wait() {
        _consumerThread.join();
    }

private:

    void consume(int datum) {
        // does nothing
        wait_us(_buffer.computeRandomWaitTime(kConsumeWaitTime));
    }

    void consumerMethod(void) {
        while (true) {
            int consumerDatum = _buffer.extract();
            consume(consumerDatum);
            tr_debug("Consumer datum is %d (index %d)", consumerDatum, _buffer.count());
        }
    }

    int computeRandomWaitTime(const std::chrono::microseconds &waitTime) {
        return rand() % waitTime.count() + waitTime.count();
    }

private:
    static const std::chrono::microseconds kConsumeWaitTime;
    Buffer &_buffer;
    Thread _consumerThread;
};

} // namespace multi_tasking

The main program that uses both the Producer and Consumer classes is:

main_producer_consumer.cpp
main_producer_consumer.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 main function for demonstrating Producer/Consumer
 * @date 2022-09-01
 * @version 0.1.0
 ***************************************************************************/

#include "buffer.hpp"
#include "consumer.hpp"
#include "mbed.h"
#include "mbed_trace.h"
#include "producer.hpp"
#if MBED_CONF_MBED_TRACE_ENABLE
#undef TRACE_GROUP
#define TRACE_GROUP "main"
#endif  // MBED_CONF_MBED_TRACE_ENABLE

// declare static variables
const std::chrono::microseconds multi_tasking::Buffer::kApppendWaitTime   = 500000us;
const std::chrono::microseconds multi_tasking::Buffer::kExtractWaitTime   = 500000us;
const std::chrono::microseconds multi_tasking::Producer::kProduceWaitTime = 500000us;
const std::chrono::microseconds multi_tasking::Consumer::kConsumeWaitTime = 500000us;

int main() {
    // use trace library for console output
    mbed_trace_init();

    tr_debug("Consumer producer program started");
    multi_tasking::Buffer buffer;
    multi_tasking::Producer producer(buffer);
    multi_tasking::Consumer consumer(buffer);

    producer.start();
    consumer.start();

    // wait for threads to terminate (will not)
    producer.wait();
    consumer.wait();

    return 0;
}

The steps for a full understanding of this program are the following:

  • Start the program with the producer running only (comment the call to consumer.start() in the main function). Make sure to run the program, describe what you observe and give an explanation of your observations. In particular, answer the following questions:

    1. Which led is blinking?
    2. How is the _index value of the Buffer class changing?
    3. What happens if you run the program long enough? (Note: make the time constant small enough for producing/consuming fast enough).
    4. Can any buffer underflow or overflow issue happen and what are the possible consequences?
  • Do the opposite and start the program with the consumer running only. Answer the same questions.

  • Then, start the program with both the producer and consumer running. Answer the same questions again. Experiment with different time values, simulating both cases where production is faster than consumption and where consumption is faster than production, by making kApppendWaitTime or kExtractWaitTime much smaller than the other waiting times.

Exercice Multi-tasking under Mbed OS/3

Answer all questions for each different scenario above.

Solution

Answers for each scenario

  • Producer only:

    • The producer led only.
    • _index is increasing only (by 1 at each append). Since the memory gets corrupted, _index may get corrupted values once the buffer is full and data continues to be appended.
    • The program will eventually crash (it will produce data in the buffer array beyond its range).
    • Buffer overflow, crash when memory gets corrupted.
  • Consumer only:

    • The consumer led only.
    • _index is decreasing only (by 1 at each append). The _index value is not corrupted.
    • The program will eventually crash, but it will take much longer since no memory gets corrupted (the consumer only reads data).
    • Buffer underflow, the program may crash when it starts reading from an invalid address.
  • Producer and consumer, producer faster:

    • Both leds.
    • _index is both decreasing and increasing, but it is increasing faster. At some point, it may get corrupted.
    • The program will eventually crash (it will produce data in the buffer array beyond its range).
    • Buffer overflow, crash when memory gets corrupted.
  • Producer and consumer, consumer faster:

    • Both leds.
    • _index is both decreasing and increasing, but it is decreasing faster.
    • The program will eventually crash, but it will take much longer since no memory gets corrupted (the consumer only reads data).
    • Buffer underflow, the program may crash when it starts reading from an invalid address.

From the experiments and observations made above, one should be able to make the following conclusions:

  • The access to the buffer should be protected and one should ensure that only one thread can execute append() or extract() at the same time. This mechanism can be implemented with a mutex as seen in a previous section of this codelab.
  • There should be a mechanism for handling buffer under- and overflow. In other words, the consumer should not be able to consume an element when the buffer is empty and the producer should not be able to produce an element when the buffer is full. This control mechanism should move the threads in a waiting state until the requested operation becomes possible. This is where semaphores enter the game!

The Semaphore`` mechanism allows the producer thread to signal when an element is added to the buffer and the consumer thread to wait for at least one element to be present in the buffer. TheSemaphore` also implements a counting mechanism.

For implementing the changes documented above, proceed with the following steps:

  • Add a Mutex in the Buffer class and make sure that each access to _buffer and _index is protected.

    1. Run the program after implementing this change and observe whether the buffer under- or overflow issues that you encountered in the previous version still arise.
    2. Observe the led for checking that the access to the critical sections is exclusive - this can be observed more easily only when using large constant time values.
  • Implement a mechanism for making sure that there will be at least one element in the buffer before the consumer can consume it. This can be done by using a Semaphore with the initial value corresponding to no element produced. It is important to place the code for signaling and waiting for the semaphore in the correct places:

    1. The producer and consumer must either signal or wait but not both.
    2. The semaphore calls must be placed correctly with the respect to the mutex lock/unlock calls.
  • Modify the waiting times again and verify whether the buffer underflow and overflow issues have been solved.

  • You should have observed that the underflow issue is solved - no consumption possible when the buffer is empty. For solving the overflow issue, we should make sure that no element can be produced when the buffer is full. This can also be implemented with another Semaphore`` initialized at the buffer capacity and with reversed roles - the producer waits for the semaphore while the consumer signals it. Add another semaphore in theBuffer` class and make sure that both under- and overflow problems are resolved.

Exercice Multi-tasking under Mbed OS/4

Implement a fully functional Buffer class, based on the instructions above.

Solution
buffer_solution.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 buffer.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief Buffer class used for demonstrating consumer/producer
 * @date 2022-09-01
 * @version 0.1.0
 ***************************************************************************/

#pragma once

#include "mbed.h"

namespace multi_tasking {

#if defined(TARGET_DISCO_H747I)
#define GREEN_LED LED1
#define BLUE_LED LED4
static constexpr uint8_t kLedOn = 0;
static constexpr uint8_t kLedOff = 1;
#endif

class Buffer {
public:
    Buffer() :
        _producerLed(GREEN_LED),
        _consumerLed(BLUE_LED) {
        // initialize random seed
        srand(time(NULL));

        _producerLed = kLedOff;
        _consumerLed = kLedOff;
    }

    void append(uint32_t datum) {
        // make sure that we can produce without overflow
        _inSemaphore.acquire();

        // lock buffer
        _producerConsumerMutex.lock();

        _producerLed = kLedOn;
        _buffer[_index] = datum;
        _index++;

        // unlock buffer 
        _producerConsumerMutex.unlock();

        // tell that one element is available for consumer
        _outSemaphore.release();

        wait_us(computeRandomWaitTime(kApppendWaitTime));        
        _producerLed = kLedOff;
    }

    uint32_t extract(void) {      
        // make sure that we can consume without underflow
        _outSemaphore.acquire();

        // lock buffer
        _producerConsumerMutex.lock();

        _consumerLed = kLedOn;
        _index--;
        wait_us(computeRandomWaitTime(kExtractWaitTime));
        int datum = _buffer[_index];

        // unlock buffer 
        _producerConsumerMutex.unlock();

        // tell that one element is available for producer
        _inSemaphore.release();

        _consumerLed = kLedOff;

        return datum;
    }

    int computeRandomWaitTime(const std::chrono::microseconds &waitTime) {
        return rand() % waitTime.count() + waitTime.count();
    }

    int count() {
        return _index;
    }

private:
    static constexpr uint8_t kBufferSize = 10;
    static const std::chrono::microseconds kApppendWaitTime;
    static const std::chrono::microseconds kExtractWaitTime;
    DigitalOut _producerLed;
    DigitalOut _consumerLed;
    Mutex _producerConsumerMutex;
    Semaphore _outSemaphore {0};
    Semaphore _inSemaphore {kBufferSize - 1};
    uint32_t _buffer[kBufferSize] = {0};
    int _index = 0;
};

} // namespace multi_tasking

Exercice Multi-tasking under Mbed OS/5

Once you have implemented a fully functional Buffer class, with the correct use of semaphores, consider removing the call to Semaphore::release() or Semaphore::acquire(). Explain what would happen in each case.

Solution

There are two calls to Semaphore::release() or Semaphore::acquire():

  • remove call to Semaphore::acquire() in Buffer::append(): this call make sure that there is no overflow at production. Removing the call allows overflow.
  • remove call to Semaphore::release() in Buffer::append(): this call make sure that the production of an element is signaled. Removing the call will prevent consumption of elements once the buffer is empty.
  • remove call to Semaphore::acquire() in Buffer::extract(): this call make sure that there is no underflow at consumption. Removing the call allows underflow.
  • remove call to Semaphore::release() in Buffer::extract(): this call make sure that the consumption of an element is signaled. Removing the call will prevent production of elements once the buffer is full.

Built-in mechanism: Queue and Mail

At this point, it is worth mentioning that RTOS usually provide other built-in mechanisms for consumer-producer related problems. With Mbed OS, these mechanisms are the Queue and the Mail mechanisms. In the next step, we will implement the solution to the consumer-producer problem with the Queue mechanism.

As a side note, it is interesting to mention that all changes required for making the implementation correct were made solely in the Buffer class. This shows that encapsulation of a functionality with a well designed API facilitates the development, debugging and maintenance of a well designed program.

Shared Data and Queue

As stated before, RTOS usually provide higher level abstractions for implementing buffers and producer/consumer scenarios. Here, we implement the same Buffer class as in the previous step - it provides the same API. The class must use a Queue data field rather than all buffer, mutex and semaphore related fields. The size of the Queue must be the same as in the previous step.

The declaration of the Queue data field must thus be

...
  Queue<int, kBufferSize> _producerConsumerQueue;
...

Exercice Multi-tasking under Mbed OS/6

Once you replace the mentioned fields with the definition shown above, you need to modify the append(), extract() and count() methods accordingly (e.g. using try_put_for(), try_get_for() and count() methods from the Queue class). Make the changes, compile and run the program. By modifying the waiting times appropriately, you should notice that the consumer never consumes an element when the queue is empty and that no element is produced beyond the queue capacity.

Solution

The Buffer class rewritten for using Queue is given below:

buffer_queue.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 buffer_queue.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief Buffer class used for demonstrating consumer/producer
 * @date 2022-09-01
 * @version 0.1.0
 ***************************************************************************/

#pragma once

#include "mbed.h"

namespace multi_tasking {

#if defined(TARGET_DISCO_H747I)
#define GREEN_LED LED2
#define YELLOW_LED LED1
#define LED_ON 1
#define LED_OFF 0
#endif

class Buffer {
public:
    Buffer() :
        _producerLed(GREEN_LED),
        _consumerLed(YELLOW_LED) {
        // initialize random seed
        srand(time(NULL));

        _producerLed = LED_OFF;
        _consumerLed = LED_OFF;
    }

    void append(uint32_t datum) {
        _producerLed = LED_ON;
        uint32_t datum_uint32t = datum;
        _producerConsumerQueue.try_put_for(Kernel::wait_for_u32_forever, (uint32_t*) datum); 
        wait_us(computeRandomWaitTime(kApppendWaitTime));
        _producerLed = LED_OFF;
    }

    uint32_t extract(void) {
        _consumerLed = LED_ON;
        wait_us(computeRandomWaitTime(kExtractWaitTime));
        uint32_t datum = 0;
        _producerConsumerQueue.try_get_for(Kernel::wait_for_u32_forever, (uint32_t**) &datum);  
        _consumerLed = LED_OFF;
        return datum;
    }

    int computeRandomWaitTime(const std::chrono::microseconds &waitTime) {
        return rand() % waitTime.count() + waitTime.count();
    }

    int count() {
        return _producerConsumerQueue.count() - 1;
    }

private:
    static constexpr uint8_t kBufferSize = 10;
    static const std::chrono::microseconds kApppendWaitTime;
    static const std::chrono::microseconds kExtractWaitTime;
    DigitalOut _producerLed;
    DigitalOut _consumerLed;
    Queue<uint32_t, kBufferSize> _producerConsumerQueue;
};

} // namespace multi_tasking

The Mail mechanism is very similar to the Queue mechanism, with the added functionality of being able to allocate blocks of memory for exchanging data between a consumer and a producer. The Queue mechanism provided in Mbed OS allows to exchange only integer or pointer values, which is sufficient in our example. The Mail mechanism is not demonstrated in this codelab.