FreeRTOS, với vai trò là một hệ điều hành thời gian thực (RTOS), cung cấp nhiều cơ chế mạnh mẽ để các tác vụ có thể phối hợp và trao đổi thông tin với nhau. Việc lựa chọn phương pháp giao tiếp (IPC - Inter-Process Communication) phù hợp là rất quan trọng để xây dựng các ứng dụng đa nhiệm hiệu quả. Dưới đây là các kỹ thuật giao tiếp chính:
1. Hàng đợi (Queue)
Hàng đợi là một trong những phương tiện giao tiếp linh hoạt và được sử dụng phổ biến nhất trong FreeRTOS, cho phép truyền dữ liệu có cấu trúc giữa các tác vụ hoặc giữa tác vụ và ngắt. Dữ liệu được xử lý theo nguyên tắc "Vào trước - Ra trước" (FIFO).
Ưu điểm:
- Đa năng: Có khả năng gửi và nhận mọi loại dữ liệu với kích thước cố định, chẳng hạn như số nguyên, con trỏ hoặc các cấu trúc tùy chỉnh.
- Hỗ trợ nhiều-đối-nhiều: Cho phép nhiều tác vụ gửi dữ liệu tới một hàng đợi và nhiều tác vụ nhận dữ liệu từ cùng một hàng đợi.
- An toàn cho luồng: Các thao tác trên hàng đợi được FreeRTOS đảm bảo tính nguyên tử (atomic), loại bỏ nhu cầu sử dụng các cơ chế khóa bổ sung.
Nhược điểm:
- Tiêu tốn bộ nhớ: Cần cấp phát bộ nhớ cho cấu trúc điều khiển hàng đợi và bộ đệm lưu trữ dữ liệu. Lượng bộ nhớ sử dụng phụ thuộc vào chiều dài hàng đợi và kích thước từng thông điệp.
- Chi phí sao chép dữ liệu: Dữ liệu được sao chép vào bộ đệm của hàng đợi khi gửi và sao chép ra khỏi bộ đệm khi nhận. Đối với các khối dữ liệu lớn, quá trình này có thể ảnh hưởng đến hiệu suất.
Trường hợp sử dụng:
Lý tưởng để truyền các thông điệp có cấu trúc như kết quả cảm biến, lệnh điều khiển, hoặc trạng thái hệ thống giữa các thành phần khác nhau của ứng dụng.
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include <stdio.h> // For printf
// Định nghĩa cấu trúc dữ liệu cho một phép đo từ cảm biến
typedef struct {
uint8_t device_id;
float reading;
uint32_t timestamp_ms; // Thời điểm đo (miliseconds từ khi khởi động)
} SensorMeasurement_t;
// Khai báo handle cho hàng đợi cảm biến
static QueueHandle_t xSensorDataQueue;
// Tác vụ sản xuất dữ liệu (Producer)
void sensor_producer_task(void *pvParameters) {
SensorMeasurement_t data_point;
uint8_t current_dev_id = 10;
float current_val = 20.0f;
while (1) {
data_point.device_id = current_dev_id;
data_point.reading = current_val;
data_point.timestamp_ms = xTaskGetTickCount() * portTICK_PERIOD_MS; // Lấy thời gian hiện tại
// Gửi dữ liệu tới hàng đợi, chờ vô thời hạn nếu hàng đợi đầy
if (xQueueSend(xSensorDataQueue, &data_point, portMAX_DELAY) == pdPASS) {
printf("Producer: Sent [ID: %u, Val: %.2f, Time: %lu ms]\n",
data_point.device_id, data_point.reading, data_point.timestamp_ms);
} else {
printf("Producer: Failed to send data point.\n");
}
current_dev_id++;
if (current_dev_id > 12) current_dev_id = 10; // Giả lập 3 thiết bị
current_val += 0.5f;
if (current_val > 25.0f) current_val = 20.0f;
vTaskDelay(pdMS_TO_TICKS(1200)); // Gửi mỗi 1.2 giây
}
}
// Tác vụ tiêu thụ dữ liệu (Consumer)
void data_processing_task(void *pvParameters) {
SensorMeasurement_t received_data_point;
while (1) {
// Nhận dữ liệu từ hàng đợi, chờ vô thời hạn
if (xQueueReceive(xSensorDataQueue, &received_data_point, portMAX_DELAY) == pdPASS) {
printf("Consumer: Received [ID: %u, Val: %.2f, Time: %lu ms]. Processing...\n",
received_data_point.device_id, received_data_point.reading, received_data_point.timestamp_ms);
// Thực hiện xử lý dữ liệu ở đây
}
}
}
// Hàm khởi tạo ứng dụng chính
void application_main(void) {
// Tạo hàng đợi với 8 phần tử, mỗi phần tử có kích thước sizeof(SensorMeasurement_t)
xSensorDataQueue = xQueueCreate(8, sizeof(SensorMeasurement_t));
if (xSensorDataQueue != NULL) {
// Tạo các tác vụ
xTaskCreate(sensor_producer_task, "SensorProducer", configMINIMAL_STACK_SIZE * 4, NULL, 2, NULL);
xTaskCreate(data_processing_task, "DataProcessor", configMINIMAL_STACK_SIZE * 4, NULL, 1, NULL);
// Bắt đầu bộ lập lịch FreeRTOS
vTaskStartScheduler();
} else {
printf("Failed to create sensor data queue.\n");
}
}
2. Tín hiệu (Semaphore)
Tín hiệu là một công cụ đơn giản nhưng mạnh mẽ, đóng vai trò như một bộ đếm để quản lý truy cập tài nguyên hoặc đồng bộ hóa các sự kiện, thay vì truyền tải dữ liệu trực tiếp. FreeRTOS hỗ trợ ba loại tín hiệu chính:
a. Tín hiệu nhị phân (Binary Semaphore)
Hoạt động giống như một cờ hiệu, với giá trị chỉ có thể là 0 hoặc 1. Thường được dùng để thông báo sự kiện "có" hoặc "không".
- Ưu điểm: Đơn giản, hiệu quả cho việc đồng bộ hóa một-đối-một (ví dụ: ngắt thông báo tác vụ).
- Nhược điểm: Không có cơ chế giải quyết vấn đề đảo ngược ưu tiên.
- Trường hợp sử dụng: Thông báo từ Chương trình phục vụ ngắt (ISR) tới một tác vụ rằng một sự kiện đã xảy ra (ví dụ: nhấn nút, truyền DMA hoàn tất).
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h" // Thư viện cho Semaphore
#include <stdio.h>
// Khai báo handle cho tín hiệu nhị phân
static SemaphoreHandle_t xInterruptNotifierSemaphore;
// Giả lập hàm xử lý ngắt bên ngoài (ví dụ: ngắt từ nút nhấn)
// Lưu ý: IRAM_ATTR là một macro cụ thể cho ESP-IDF để đặt hàm trong RAM tốc độ cao
// có thể không cần thiết hoặc khác trên các nền tảng khác.
void IRAM_ATTR external_interrupt_handler(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// "Giải phóng" tín hiệu từ trong ngữ cảnh ngắt để thông báo sự kiện
xSemaphoreGiveFromISR(xInterruptNotifierSemaphore, &xHigherPriorityTaskWoken);
// Nếu việc giải phóng tín hiệu đã đánh thức một tác vụ ưu tiên cao hơn
// thì yêu cầu chuyển đổi ngữ cảnh ngay lập tức
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
// Tác vụ xử lý sự kiện ngắt
void interrupt_event_processor_task(void *pvParameters) {
printf("Event Processor Task: Waiting for interrupt events...\n");
while (1) {
// "Chiếm" tín hiệu, chờ vô thời hạn cho đến khi tín hiệu được giải phóng bởi ISR
if (xSemaphoreTake(xInterruptNotifierSemaphore, portMAX_DELAY) == pdTRUE) {
printf("Event Processor Task: Interrupt event received! Performing action...\n");
// Thực hiện các tác vụ cần thiết khi sự kiện ngắt xảy ra
}
}
}
// Hàm khởi tạo hệ thống tín hiệu và các tác vụ liên quan
void initialize_event_system(void) {
// Tạo tín hiệu nhị phân. Mặc định, tín hiệu nhị phân được tạo ở trạng thái "rỗng" (không có token),
// nghĩa là tác vụ sẽ chờ cho đến khi ISR giải phóng nó.
xInterruptNotifierSemaphore = xSemaphoreCreateBinary();
if (xInterruptNotifierSemaphore != NULL) {
// Tạo tác vụ xử lý sự kiện ngắt
xTaskCreate(interrupt_event_processor_task, "IntEventProcessor", configMINIMAL_STACK_SIZE * 3, NULL, 3, NULL);
// Ở đây, bạn sẽ cần cấu hình ngắt GPIO thực tế cho phần cứng của mình.
// Ví dụ: liên kết external_interrupt_handler với một chân GPIO cụ thể.
printf("Binary Semaphore system initialized. Ready to receive interrupt notifications.\n");
// --- Phần mô phỏng ngắt để kiểm tra ---
// Trong môi trường thực, hàm external_interrupt_handler sẽ được kích hoạt bởi phần cứng.
// Để kiểm tra nhanh, bạn có thể tạo một tác vụ mô phỏng việc kích hoạt ngắt.
// xTaskCreate(simulate_interrupt_trigger_task, "ISR_Sim", configMINIMAL_STACK_SIZE, NULL, 4, NULL);
} else {
printf("Failed to create binary semaphore for interrupt notification.\n");
}
}
// (Tùy chọn) Một tác vụ để mô phỏng việc kích hoạt ngắt cho mục đích kiểm thử
// void simulate_interrupt_trigger_task(void *pvParameters) {
// while (1) {
// vTaskDelay(pdMS_TO_TICKS(4000)); // Kích hoạt "ngắt" mỗi 4 giây
// printf("Simulating external interrupt trigger...\n");
// external_interrupt_handler(); // Gọi trực tiếp hàm xử lý ngắt mô phỏng
// }
// }
// Hàm main của FreeRTOS sẽ gọi initialize_event_system() và vTaskStartScheduler()
// void rtos_entry_point(void) {
// initialize_event_system();
// vTaskStartScheduler();
// }
b. Tín hiệu đếm (Counting Semaphore)
Có khả năng lưu trữ một giá trị đếm từ 0 đến một giới hạn tối đa được định cấu hình. Tín hiệu này thích hợp để quản lý một số lượng hữu hạn các tài nguyên giống nhau hoặc theo dõi số lần một sự kiện xảy ra.
- Ưu điểm: Theo dõi hiệu quả số lượng tài nguyên có sẵn hoặc các sự kiện đã được kích hoạt.
- Nhược điểm: Tương tự như tín hiệu nhị phân, không giải quyết được vấn đề đảo ngược ưu tiên.
- Trường hợp sử dụng: Quản lý một pool bộ đệm (ví dụ: 3 bộ đệm có sẵn), hoặc đếm số gói tin đã nhận và chờ xử lý.
c. Tín hiệu tương hỗ (Mutex Semaphore)
Đây là một dạng đặc biệt của tín hiệu nhị phân, được thiết kế chuyên biệt để bảo vệ các tài nguyên chia sẻ (critical section). Điểm nổi bật là cơ chế kế thừa ưu tiên (priority inheritance).
- Ưu điểm: Ngăn chặn hiệu quả vấn đề đảo ngược ưu tiên, đảm bảo an toàn cao nhất khi truy cập tài nguyên chung.
- Nhược điểm: Có chi phí xử lý cao hơn một chút so với các loại tín hiệu khác do cơ chế kế thừa ưu tiên.
- Trường hợp sử dụng: Bảo vệ các biến toàn cục, các thiết bị ngoại vi (ví dụ: SPI, I2C) hoặc bất kỳ đoạn mã nào truy cập tài nguyên chung mà chỉ một tác vụ được phép sử dụng tại một thời điểm.