Gatekeeper Tasks

In embedded systems, one of the most important design considerations is how tasks interact with hardware resources. This becomes especially critical in larger projects where multiple developers are working on different parts of the application. When several tasks may need access to the same hardware resource, it’s essential to establish a consistent and reliable strategy for resource sharing. Doing so ensures system stability, prevents conflicts, and helps maintain correct system behavior. 

One of the key challenges in sharing hardware resources among tasks is providing a reliable mechanism for claiming ownership of those resources. Developers often use a combination of semaphores and critical sections to enforce mutual exclusion, but this approach requires careful structuring to prevent simultaneous access. The complexity increases when tasks have different priorities, introducing the risk of deadlocks. Ensuring safe and efficient access to shared hardware demands experience, disciplined coding practices, and a thorough understanding of real-time system behavior. 

A common example of a shared hardware resource is the I2C bus, which supports multiple external devices that each contribute specific functionality to the system. It’s typical for each device to have a dedicated task responsible for sending commands and interpreting responses. The challenge arises when multiple tasks need to communicate with their respective devices over the shared I2C bus. 

If one task is in the middle of a multi-byte transmission, it’s critical to prevent other tasks from attempting to access the bus simultaneously. One common solution is to use a semaphore to enforce mutual exclusion. Each task would attempt to take the semaphore before accessing the I2C bus and release it afterward. While this is a valid and effective approach, it relies on all developers consistently following the correct pattern of acquiring and releasing the semaphore—any oversight can lead to race conditions or bus contention. 

An alternative approach to managing access to shared hardware resources is the use of a gatekeeper task. While not a special feature of FreeRTOS, a gatekeeper task is a design pattern that promotes structured and centralized access to a resource. In this model, all direct interactions with the hardware are handled exclusively by the gatekeeper task. It remains blocked until it receives a request from another task, typically through a queue. 

Each request is placed into the queue, ensuring that operations are processed in the order they are received. This ordered handling simplifies debugging, improves predictability, and helps maintain fairness among tasks. Once a request is dequeued, the gatekeeper performs the necessary hardware operation. Because it is the only task allowed to directly interface with the hardware, this approach guarantees that each operation completes without interference from other tasks. 

Defining Gatekeeper Operations 

The first step in implementing a gatekeeper task is to define the set of operations it will support. These operations will vary depending on the type of hardware the task manages, and developers have flexibility in determining what functionality to expose.  

Consider a gatekeeper task designed to control an LCD display. A good design practice is to use an enumerated type to define the supported operations—such as clearing the screen, drawing a rectangle, or drawing an image. This approach improves code readability, simplifies request handling, and makes the system easier to extend and maintain 

Gatekeeper Message Structure 

In addition to the command itself, many gatekeeper tasks require supplementary information to carry out the requested operation. Continuing with the LCD example, the gatekeeper may need details such as the coordinates of an image and the foreground color. To support this, developers can define a custom data structure that encapsulates both the command type and any associated parameters. This structure is then sent to the gatekeeper task via a queue. 

Creating a Gatekeeper Queue

Each gatekeeper task receives requests through a message queue. This queue is initialized to accept data that matches the custom structure defined for the task. The structure typically includes both the command type and any additional parameters needed to perform the operation.

Providing Data to Gatekeeper 

To send a request to the gatekeeper, a task populates the fields of the custom data structure with the appropriate command and any required parameters. Once the structure is fully populated, the task sends it to the gatekeeper’s mailbox queue. This queue-based communication ensures that requests are handled in a thread-safe and orderly manner, allowing the gatekeeper to process each operation without interference from other tasks. 

Returning Data 

The current example only supports one-way communication—tasks can send requests to the gatekeeper, but they cannot receive responses. To enable two-way communication, the data structure used for requests can be extended to include a queue handle. For instance, in cases where a task expects a response (such as reading sensor data or querying the state of a device), it can include a valid queue handle in the request. The gatekeeper can then use this handle to send the result back to the requesting task. This approach maintains the benefits of centralized hardware access while enabling flexible, bidirectional communication. 

In most real-world applications, reading from an LCD screen is uncommon. However, for the sake of learning and exploring gatekeeper task design, we’ll expand our example to include scenarios where the gatekeeper returns data—such as acknowledging a command or reporting an error. 

To support two-way communication, the application will create a dedicated response queue for each task that expects data from the gatekeeper. This ensures that responses are routed to the correct task without ambiguity.

When a task sends a request to the gatekeeper, it includes a handle to its response queue in the message structure. The gatekeeper can then use this handle to send the result back to the requesting task. 

With the response queue in place, a task can now send a message to the gatekeeper and then block while waiting for a response. Once the gatekeeper processes the request, it sends the result back through the provided queue, allowing the requesting task to resume execution with the returned data.