Hello! Welcome to the next lesson in our module on Multi-Peripheral System Integration.
In the last lesson, we successfully built the temp_sensor_task, which uses a software timer and task notifications to perform periodic measurements. This created a self-contained, event-driven module for sensing the environment.
Today, we'll build the other major component of our smart fan project: the motor control logic. The learning outcome for this lesson is to implement motor control logic in a separate FreeRTOS task that responds to commands.
This task will act as the "actuator" part of our system. Instead of being driven by a timer like the sensor task, it will be an "actor" task that waits idly for instructions and then executes them. We'll explore two ways to send commands, starting with a very simple method and then moving to a more robust and scalable architecture that will form the foundation for our fully integrated system.
1. The "Actor" Task Pattern
In RTOS design, tasks often fall into different patterns. The sensor task we built previously follows a periodic pattern—it wakes up at fixed intervals. The task we will build today follows an actor pattern. An actor task typically has these characteristics:
- It encapsulates a specific capability or resource (e.g., controlling a motor, writing to a display).
- It spends most of its time blocked, consuming no CPU resources.
- It wakes up only when it receives a message or "command" from another part of the system.
- Once awake, it performs an action based on the command and then goes back to waiting.
This is a powerful pattern for managing actuators. A motor shouldn't be running constantly; it should run when, and only when, it's told to.
2. A Simple Command Mechanism: Global Flags
The simplest way to send a command to a task is by using a shared global variable, often called a flag. One task sets the flag, and the actor task polls this flag in its main loop to see if it needs to do something.
This approach is demonstrated in the following article, which implements stepper motor control using a dedicated FreeRTOS task.
Smooth Stepper Motor Control with ESP32
The article 'Smooth Stepper Motor Control with ESP32' by Protonest IoT provides a clear example of using a dedicated task for motor control. We will focus on how it uses a simple flag to trigger the motor.
Please read the sections 'Setup Function' and 'Step 5: Stepper Control Task'. Pay close attention to how the stepperMoving boolean variable is used to communicate between the loop() function and the stepperControl task.
Let's break down the pattern you just read about:
- A Global Flag: A boolean variable
stepperMovingis declared globally.bool stepperMoving = false; - The Actor Task (
stepperControl): This task runs in an infinite loop. It continuously checks the flag. If the flag isfalse, it yields withvTaskDelay(1)to allow other tasks to run. Iftrue, it executes the motor movement and then resets the flag.void stepperControl(void * parameter) { for (;;) { // Infinite loop if (stepperMoving) { moveStepper(steps); // Perform the action stepperMoving = false; // Reset the flag } else { vTaskDelay(1); // Wait and let other tasks run } } } - The "Commander" (
loop): Another part of the code, in this case the main Arduinoloop(), is responsible for setting the flag totruewhen it wants the motor to move.void loop() { if (millis() - lastStepperMoveTime > 10000) { stepperMoving = true; // Issue the command steps = 12800; lastStepperMoveTime = millis(); } }
This flag-based method is simple and effective for binary commands ("go" / "don't go"). However, it has limitations:
- Limited Information: It's difficult to pass more complex information. What if we wanted to tell the motor how many steps to move or at what speed? We would need more global variables, which quickly becomes messy and error-prone.
- Polling: The motor task is constantly waking up every tick to check the flag. While
vTaskDelay(1)is far better than a busy-wait, it's not as efficient as a task that remains fully blocked until there's real work to do.
To overcome these limitations, we need a more powerful inter-task communication mechanism.
3. A Robust Command Mechanism: The Command Queue
The standard and most robust way for FreeRTOS tasks to communicate is through a queue. A queue is a thread-safe data structure that allows one task to send structured data to another. The receiving task can wait on the queue, and FreeRTOS will automatically block it (consuming zero CPU) until a message arrives.
This perfectly fits our "actor" model. The motor task will block on an empty queue, and other tasks can send "command" objects to it to be processed.
The following article demonstrates a command processing system using a queue to control LEDs. We will adapt this powerful pattern for our motor control task.
Implementing FreeRTOS Solutions on ESP 32 Devices ...
The article 'Implementing FreeRTOS Solutions on ESP 32 Devices' explains several RTOS concepts. We will focus on its excellent implementation of a command processing task using a queue.
Please read the section 'Queues' and pay special attention to the TaskCmdExec function. Notice how it defines a struct for command data (cmdData), creates a queue (xQueueCreate), and then enters a while(true) loop where it waits for commands using xQueueReceive.
Implementing the Command Queue Pattern for Motor Control
Let's design the structure for our motor control task based on the queue pattern.
Step 1: Define the Command Structure
First, we define a struct that represents all the possible commands we might want to send to our motor. An enum is perfect for defining the command types.
// Defines the types of commands our motor can accept
typedef enum {
MOTOR_CMD_STOP,
MOTOR_CMD_SET_SPEED,
MOTOR_CMD_SET_DIRECTION
} motor_cmd_t;
// The structure for a single motor command message
typedef struct {
motor_cmd_t cmd; // The command type
int value; // An optional value (e.g., speed percentage, direction 0/1)
} motor_command_t;
Step 2: Create the Task and Queue
In our app_main, we'll create the queue and the motor task. The handle to the queue is passed to the task as its parameter.
#include "freertos/queue.h"
// Declare a handle for the queue
static QueueHandle_t s_motor_command_queue = NULL;
void motor_control_task(void *pvParameters);
void app_main(void) {
// Create a queue that can hold up to 5 motor_command_t objects
s_motor_command_queue = xQueueCreate(5, sizeof(motor_command_t));
// Create the motor control task
xTaskCreate(motor_control_task,
"motor_task",
2048,
(void*)s_motor_command_queue, // Pass the queue handle as the parameter
5,
NULL);
// ... other initializations ...
}
Step 3: Implement the Actor Task's Main Loop
This is the core of our lesson. The motor task's loop is simple and highly efficient. It does nothing but wait for a command to arrive in the queue.
void motor_control_task(void *pvParameters) {
// The parameter is the queue handle. Cast it to the correct type.
QueueHandle_t command_queue = (QueueHandle_t)pvParameters;
motor_command_t received_cmd;
// Initialize motor peripherals (GPIO, MCPWM/LEDC) here...
// For example, using the LEDC peripheral we saw in Module 3 for PWM.
while (1) {
// Wait indefinitely for a command to arrive on the queue.
// The task will be in the 'Blocked' state, consuming no CPU, until a message is available.
if (xQueueReceive(command_queue, &received_cmd, portMAX_DELAY) == pdPASS) {
// A command was successfully received. Process it.
printf("Motor task received command: %d with value: %d\n", received_cmd.cmd, received_cmd.value);
switch (received_cmd.cmd) {
case MOTOR_CMD_SET_SPEED:
// Here you would call your function to set motor speed
// e.g., ledc_set_duty(..., speed_percentage_to_duty(received_cmd.value));
// ledc_update_duty(...);
printf("Setting motor speed to %d%%\n", received_cmd.value);
break;
case MOTOR_CMD_SET_DIRECTION:
// Here you would set the GPIO for direction
// e.g., gpio_set_level(DIRECTION_PIN, received_cmd.value);
printf("Setting motor direction to %d\n", received_cmd.value);
break;
case MOTOR_CMD_STOP:
// Stop the motor
// e.g., ledc_set_duty(..., 0);
// ledc_update_duty(...);
printf("Stopping motor.\n");
break;
default:
printf("Unknown motor command.\n");
break;
}
}
}
}
This structure is clean, efficient, and infinitely scalable. Adding new motor commands is as simple as adding a new entry to the motor_cmd_t enum and a new case to the switch statement.
Test your understanding!
Suppose you want to add a new feature: running the motor at a specific speed for a fixed duration, then stopping.
How would you modify the motor_command_t struct and the switch statement in motor_control_task to accommodate a new command MOTOR_CMD_RUN_FOR_MS? The command should take two values: speed (0-100) and duration (in milliseconds).
Show answer
-
Modify the
motor_command_tstruct: You need to add another field to hold the second value.typedef struct { motor_cmd_t cmd; int value1; // Renamed for clarity (e.g., speed) int value2; // New field for duration } motor_command_t;You would also add
MOTOR_CMD_RUN_FOR_MSto themotor_cmd_tenum. -
Add a new case to the
switchstatement:case MOTOR_CMD_RUN_FOR_MS: printf("Running motor at speed %d for %d ms\n", received_cmd.value1, received_cmd.value2); // 1. Set motor speed using received_cmd.value1 // e.g., ledc_set_duty(..., speed_percentage_to_duty(received_cmd.value1)); // ledc_update_duty(...); // 2. Wait for the specified duration. vTaskDelay is perfect for this. vTaskDelay(pdMS_TO_TICKS(received_cmd.value2)); // 3. Stop the motor // e.g., ledc_set_duty(..., 0); // ledc_update_duty(...); printf("Timed run complete. Stopping motor.\n"); break;
This demonstrates how easily the pattern can be extended to handle more complex, multi-parameter commands.
Conclusion
In this lesson, we have successfully designed and structured a dedicated FreeRTOS task to control a motor. This task follows the "actor" pattern, waiting efficiently for commands before taking action.
Key Takeaways:
- Actor Task Pattern: A common and efficient design where a task remains blocked until it receives a command, performs an action, and returns to a blocked state.
- Simple vs. Robust Commands: While global flags are simple for basic triggers, they don't scale. FreeRTOS queues are the standard, thread-safe solution for sending structured commands between tasks.
- Command Queue Loop: The core logic of an actor task is a
while(1)loop containingxQueueReceive(). This function blocks the task until a message arrives, ensuring zero CPU usage while idle. - Scalability: The command struct and
switchstatement pattern is highly scalable, allowing you to easily add new functionality to your motor controller without changing its fundamental structure.
We have now built the two main pillars of our smart fan application: a periodic sensor task and a command-driven motor task. Both are currently operating in isolation. In our next lesson, we will finally connect them. We'll modify the sensor task to send commands to the motor task's queue, bringing our system to life and closing the control loop.