Cấu trúc mã nguồn FreeRTOS trên STM32
Khi sử dụng STM32CubeMX để cấu hình FreeRTOS cho các dòng vi điều khiển như STM32F103, công cụ sẽ tự động sinh ra tệp freertos.c. Dưới đây là cấu trúc template chuẩn sử dụng CMSIS-RTOS V2 API:
/* Định nghĩa cho tác vụ xử lý cảm biến */
osThreadId_t sensorTaskHandle;
const osThreadAttr_t sensorTask_attributes = {
.name = "SensorReader",
.stack_size = 256 * 4,
.priority = (osPriority_t) osPriorityAboveNormal,
};
/* Khai báo prototype */
void SensorReaderTask(void *argument);
void MX_FREERTOS_Init(void);
/* Hàm khởi tạo các tác vụ */
void MX_FREERTOS_Init(void) {
sensorTaskHandle = osThreadNew(SensorReaderTask, NULL, &sensorTask_attributes);
}
/* Hàm thực thi của tác vụ */
void SensorReaderTask(void *argument) {
ADC_Init();
for(;;) {
ReadSensorData();
osDelay(100);
}
}
Trong quá trình phát triển dự án, lập trình viên sẽ mở rộng và tùy biến các tác vụ dựa trên khung sườn này.
Các khái niệm cốt lõi trong RTOS
Một ứng dụng nhúng (application) có thể được chia nhỏ thành nhiều luồng thực thi độc lập, gọi là tác vụ (task) hoặc luồng (thread). Để hiểu rõ cơ chế hoạt động, chúng ta cần nắm vững các khái niệm sau:
1. Trạng thái tác vụ (Task State)
- Running: Tác vụ đang chiếm CPU và thực thi lệnh.
- Not Running: Tác vụ không chạy, được chia nhỏ thành:
- Ready: Đã sẵn sàng, chỉ chờ CPU rảnh để được cấp quyền chạy.
- Blocked: Đang chờ một sự kiện (ví dụ: chờ tín hiệu từ ngoại vi, chờ hết thời gian delay).
- Suspended: Bị tạm dừng có chủ đích bởi lập trình viên, không được bộ lập lịch xem xét.
2. Độ ưu tiên (Priority)
Mỗi tác vụ được gán một mức ưu tiên. Bộ lập lịch (scheduler) sẽ luôn chọn tác vụ có độ ưu tiên cao nhất đang ở trạng thái Ready để chạy. Các tác vụ cùng mức ưu tiên sẽ được chạy luân phiên (time-slicing). Hệ thống luôn duy trì một Idle Task (tác vụ rảnh) có độ ưu tiên thấp nhất để xử lý khi không có tác vụ nào cần chạy.
3. Ngăn xếp (Stack)
Mỗi tác vụ sở hữu một vùng nhớ Stack riêng biệt. Stack dùng để lưu trữ biến cục bộ, địa chỉ trả về và ngữ cảnh (context) khi chuyển đổi tác vụ. Việc cô lập Stack giúp các tác vụ không ghi đè dữ liệu lên nhau.
4. Điều hướng theo sự kiện (Event-Driven)
Thay vì dùng vòng lặp while(1) để thăm dò (polling) liên tục gây lãng phí CPU, RTOS cho phép tác vụ ngủ (block) và chỉ thức dậy khi có sự kiện xảy ra (như nhận được message, cờ flag, hoặc ngắt).
5. Lập lịch hợp tác (Co-operative Scheduling)
Trong mô hình hợp tác, tác vụ đang chạy sẽ không bị ép nhường CPU. Nó chỉ chuyển quyền điều khiển khi tự gọi các hàm yield, block hoặc kết thúc. Ngược lại, FreeRTOS chủ yếu sử dụng Lập lịch ưu tiên (Preemptive Scheduling), nơi hệ thống có thể cưỡng bức ngắt tác vụ hiện tại để chuyển sang tác vụ có độ ưu tiên cao hơn khi có ngắt SysTick.
Cơ chế Quản lý Bộ nhớ
1. Ngăn xếp (Stack)
Stack là vùng nhớ được quản lý bởi thanh ghi SP (Stack Pointer). Khi gọi hàm, CPU sử dụng lệnh nhảy (như BL trong ARM) để lưu địa chỉ trả về vào thanh ghi LR (Link Register) và cập nhật PC (Program Counter). Đoạn mã dưới đây minh họa luồng thực thi:
int system_error_code = 0;
int compute_temperature_offset(int raw_val) {
raw_val += 5;
return raw_val;
}
int apply_calibration_factor(int val) {
val *= 2;
return val;
}
void process_sensor_reading(int input_data) {
system_error_code = compute_temperature_offset(input_data);
system_error_code = apply_calibration_factor(system_error_code);
}
int main(void) {
int raw_adc = 1024;
process_sensor_reading(raw_adc);
return 0;
}
Các vấn đề cần lưu ý:
- Bảo vệ thanh ghi LR: Trình biên dịch tự động chèn lệnh
PUSHở đầu hàm để lưu LR và các thanh ghi cần thiết xuống Stack, sau đóPOPkhi hàm kết thúc. - Phân phối biến cục bộ: Biến cục bộ được ưu tiên lưu trong thanh ghi. Nếu hết thanh ghi, chúng được đẩy xuống Stack.
- Chuyển đổi ngữ cảnh (Context Switch): Khi RTOS đổi task, nó sẽ
PUSHtoàn bộ thanh ghi CPU vào Stack của task hiện tại, lưu lại SP. Khi quay lại task, nó dùng SP đểPOPcác thanh ghi, khôi phục chính xác trạng thái trước đó.
2. Vùng nhớ Heap
Heap là vùng nhớ trống dùng để cấp phát động trong quá trình chạy. Trong C tiêu chuẩn, ta dùng malloc để cấp phát và free để giải phóng.
3. Các chiến lược quản lý Heap trong FreeRTOS
FreeRTOS cung cấp các hàm pvPortMalloc và vPortFree thay thế cho chuẩn C. Thư viện nguồn bao gồm 5 tệp triển khai (heap_1.c đến heap_5.c) tương ứng với 5 thuật toán quản lý bộ nhớ:
| Tệp triển khai | Ưu điểm | Nhược điểm |
|---|---|---|
| heap_1.c | Thuật toán đơn giản, thời gian thực thi xác định (deterministic). | Chỉ hỗ trợ cấp phát, không thể giải phóng bộ nhớ. |
| heap_2.c | Hỗ trợ giải phóng, tối ưu hóa việc chọn block vừa vặn nhất. | Dễ sinh ra phân mảnh bộ nhớ, thời gian thực thi không xác định. |
| heap_3.c | Bọc (wrap) lại hàm malloc/free của thư viện C chuẩn. |
Tốc độ chậm, thời gian thực thi phụ thuộc vào thư viện C, không an toàn cho thread. |
| heap_4.c | Gộp các block nhớ trống liền kề, giảm thiểu phân mảnh. | Thời gian thực thi không xác định. |
| heap_5.c | Kế thừa heap_4, hỗ trợ quản lý nhiều vùng RAM rời rạc (ví dụ: RAM trong và RAM ngoài). | Thời gian thực thi không xác định. |
Trong thực tế, heap_4 và heap_5 được sử dụng phổ biến nhất nhờ khả năng chống phân mảnh. heap_3 thường bị hạn chế do vấn đề thread-safety của thư viện C chuẩn.
Một số API quản lý bộ nhớ thường dùng trong FreeRTOS:
// Cấp phát bộ nhớ, trả về con trỏ NULL nếu không đủ dung lượng
void * pvPortMalloc(size_t xWantedSize);
// Giải phóng vùng nhớ đã cấp phát
void vPortFree(void * pv);
// Lấy dung lượng Heap còn trống hiện tại (không khả dụng với heap_3)
size_t xPortGetFreeHeapSize(void);
// Lấy dung lượng Heap trống nhỏ nhất kể từ khi hệ thống khởi động (chỉ dùng cho heap_4, heap_5)
size_t xPortGetMinimumEverFreeHeapSize(void);