Understanding Semaphores in Zephyr OS on nRF7002DK

Table of Contents


Introduction to Semaphores in Zephyr OS

In Zephyr OS, a semaphore is a synchronization primitive used to manage shared resources or coordinate tasks in concurrent embedded systems. It maintains a counter that represents the availability of a resource or the occurrence of an event. Semaphores are particularly useful in real-time applications where tasks must operate predictably and efficiently.

Key operations include:

  • Give: Increments the semaphore count, signaling resource availability.
  • Take: Decrements the count, allowing a task to proceed or block until the semaphore is given.
  • Reset: Sets the count to zero.

Semaphores in Zephyr are lightweight and versatile, supporting both binary (count ≤ 1) and counting (count > 1) use cases, making them ideal for task synchronization and resource management.


Why Use Semaphores on nRF7002DK?

The nRF7002DK, powered by the nRF5340 SoC and nRF7002 Wi-Fi chip, is a robust platform for developing IoT and wireless applications. Its dual-core architecture (application and network cores) and real-time capabilities make it a perfect fit for Zephyr OS, which is designed for resource-constrained devices.

Semaphores are essential on the nRF7002DK for:

  • Thread Synchronization: Coordinating tasks on the application core, such as handling sensor data or Wi-Fi events.
  • Resource Sharing: Managing access to peripherals like UART, I2C, or SPI.
  • Producer-Consumer Patterns: Enabling efficient data exchange between tasks, such as sending sensor readings over Wi-Fi.

This post demonstrates a practical semaphore-based application on the nRF7002DK’s application core, showcasing Zephyr’s capabilities in a real-world scenario.


Example: Producer-Consumer with Semaphores

Below is a Zephyr OS example that implements a producer-consumer pattern using a semaphore on the nRF7002DK. The producer thread generates data, and the consumer thread processes it, with a semaphore ensuring proper synchronization.

Code: main.c

#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>

LOG_MODULE_REGISTER(nrf7002dk_sem_example, LOG_LEVEL_INF);

K_SEM_DEFINE(data_sem, 0, 5);

#define BUFFER_SIZE 10
static int data_buffer[BUFFER_SIZE];
static int buffer_index = 0;

void producer_thread(void *arg1, void *arg2, void *arg3)
{
    while (1) {
        data_buffer[buffer_index] = buffer_index + 1;
        LOG_INF("Producer: Generated data %d at index %d", 
                data_buffer[buffer_index], buffer_index);
        buffer_index = (buffer_index + 1) % BUFFER_SIZE;
        k_sem_give(&data_sem);
        k_msleep(1000);
    }
}

void consumer_thread(void *arg1, void *arg2, void *arg3)
{
    while (1) {
        if (k_sem_take(&data_sem, K_MSEC(2000)) == 0) {
            int read_index = (buffer_index - 1 + BUFFER_SIZE) % BUFFER_SIZE;
            LOG_INF("Consumer: Processed data %d from index %d", 
                    data_buffer[read_index], read_index);
        } else {
            LOG_ERR("Consumer: Timeout waiting for data");
        }
    }
}

#define PRODUCER_STACK_SIZE 1024
#define CONSUMER_STACK_SIZE 1024
#define PRODUCER_PRIORITY 5
#define CONSUMER_PRIORITY 5

K_THREAD_DEFINE(producer_id, PRODUCER_STACK_SIZE, producer_thread, 
                NULL, NULL, NULL, PRODUCER_PRIORITY, 0, 0);
K_THREAD_DEFINE(consumer_id, CONSUMER_STACK_SIZE, consumer_thread, 
                NULL, NULL, NULL, CONSUMER_PRIORITY, 0, 0);

void main(void)
{
    LOG_INF("nRF7002DK Semaphore Example Started");
}

Configuration: prj.conf

CONFIG_LOG=y
CONFIG_LOG_DEFAULT_LEVEL=3
CONFIG_LOG_BACKEND_UART=y

How It Works

  1. Semaphore Setup:

    • A semaphore (data_sem) is defined with an initial count of 0 and a maximum count of 5, limiting the number of unprocessed data items.
    • The semaphore signals when the producer has added data to the buffer.
  2. Producer Thread:

    • Writes data to a circular buffer (data_buffer) and increments buffer_index.
    • Calls k_sem_give to signal data availability.
    • Sleeps for 1 second to simulate work (e.g., polling a sensor).
  3. Consumer Thread:

    • Waits for the semaphore using k_sem_take with a 2-second timeout.
    • On success, reads the latest data from the buffer and logs it.
    • If the timeout expires, logs an error.
  4. Circular Buffer:

    • A simple array (data_buffer) stores data, with buffer_index managing writes and reads.
    • This example assumes one producer and one consumer for simplicity.
  5. Logging:

    • Zephyr’s logging module outputs messages to the nRF7002DK’s UART, viewable via a terminal emulator (baud rate: 115200).

Setting Up the nRF7002DK

To run the example on the nRF7002DK:

  1. Install Prerequisites:

    • Set up nRF Connect SDK and Toolchain using VS Code
  2. Build the Application:

    • Place main.c and prj.conf in a project folder.
    • Create and build a configuration using VS Code extension
  3. Flash the Firmware:

    • Connect the nRF7002DK via USB.
    • Flash using VS Code extension
  4. View Output:

    • Use a terminal emulator (e.g., minicom, baud rate 115200) to see logs:
[00:00:00.123] <inf> nrf7002dk_sem_example: nRF7002DK Semaphore Example Started
[00:00:01.234] <inf> nrf7002dk_sem_example: Producer: Generated data 1 at index 0
[00:00:01.235] <inf> nrf7002dk_sem_example: Consumer: Processed data 1 from index 0

Best Practices for Semaphores

  • Initialize Correctly: Set appropriate initial and maximum counts for your use case.
  • Use Timeouts: Prevent indefinite blocking with timeouts in k_sem_take.
  • Avoid Priority Inversion: Be cautious with high-priority tasks, as semaphores lack priority inheritance (use mutexes for critical sections).
  • Monitor Semaphore State: Use k_sem_count_get for debugging.
  • Optimize for Embedded Systems: Minimize contention and keep semaphore operations lightweight.

Conclusion

Semaphores in Zephyr OS provide a robust mechanism for task synchronization and resource management, making them invaluable for real-time embedded applications. The nRF7002DK example demonstrates how to use semaphores to implement a producer-consumer pattern, showcasing Zephyr’s capabilities on a modern IoT platform. By following best practices and leveraging Zephyr’s APIs, developers can build efficient, reliable systems for the nRF7002DK and beyond.

Try extending this example by integrating the nRF7002’s Wi-Fi capabilities or connecting to real sensors. For more details, explore the Zephyr OS documentation and Nordic Developer Zone.