Sau khi đã nắm vững các khái niệm cơ bản về hệ điều hành thời gian thực ở bài trước, hôm nay chúng ta sẽ cùng nhau “mổ xẻ” cơ chế vận hành của FreeRTOS trên dòng vi điều khiển STM32. Chúng ta sẽ tìm hiểu cách các Task giao tiếp với nhau qua Queue, cách bảo vệ tài nguyên bằng Mutex và quy trình cấu hình dự án đa tác vụ đầu tiên trên STM32CubeIDE.
🎯 Mục tiêu bài học
- Hiểu cơ chế RTOS Tick và Critical Section.
- Phân biệt ngắt thông thường và ngắt trong môi trường RTOS.
- Sử dụng thành thạo Queue và Mail Queue để truyền dữ liệu giữa các Task.
- Làm chủ Semaphore và Mutex trong quản lý tài nguyên.
- Phân biệt các hàm Delay:
HAL_Delay, vTaskDelay, vTaskDelayUntil.
- Cấu hình dự án FreeRTOS hoàn chỉnh trên STM32CubeIDE.
1. Cơ chế vận hành của Nhân (Kernel) RTOS
a) RTOS Tick và Critical Section
Nhân của RTOS hoạt động dựa trên một ngắt Timer của chip, thường gọi là RTOS Tick (mặc định là 1ms). Mỗi khi có nhịp Tick, bộ lập lịch (Scheduler) sẽ được đánh thức để quyết định Task nào tiếp theo sẽ được quyền chiếm dụng CPU.
Trong quá trình xử lý các dữ liệu nhạy cảm của hệ thống, Kernel sẽ sử dụng hàm enter_critical_section() để tạm thời vô hiệu hóa tất cả các ngắt khác. Điều này đảm bảo tính toàn vẹn dữ liệu, tránh việc một ngắt ngoại vi xen ngang khi Kernel đang thay đổi danh sách các Task. Sau khi hoàn tất, hàm exit_critical_section() sẽ được gọi để khôi phục lại trạng thái bình thường.
b) Ngắt thông thường vs Ngắt trong RTOS
Trong lập trình nhúng thông thường, khi có ngắt, mọi xử lý đều nằm trong hàm phục vụ ngắt (ISR). Tuy nhiên, trong RTOS, ISR thường rất ngắn. Nó chỉ phát đi một “tín hiệu” (Signal/Semaphore) để giải phóng (Unblock) một Task có độ ưu tiên cao. Task này sau đó sẽ thực hiện công việc chính, giúp ISR kết thúc nhanh chóng và không làm nghẽn hệ thống.
⚠️ Lưu ý về API trong ISR: Không phải hàm RTOS nào cũng dùng được trong ngắt. Bạn chỉ được phép sử dụng các hàm có hậu tố FromISR (ví dụ: xQueueSendFromISR()) vì chúng được thiết kế an toàn để không làm treo bộ lập lịch khi đang ở trong chế độ ngắt.
2. Trao đổi dữ liệu với Queue và Mail Queue
a) Hàng đợi (Queue)
Queue là một bộ đệm theo kiểu FIFO (First In First Out). Đây là cách an toàn nhất để truyền dữ liệu giữa hai Task mà không sợ tranh chấp vùng nhớ.
- Dữ liệu được sao chép trực tiếp vào Queue.
- Task nhận sẽ bị Block (chờ) nếu Queue trống.
- Task gửi sẽ bị Block nếu Queue đã đầy (tùy cấu hình timeout).
b) Mail Queue
Khác với Queue thông thường truyền các giá trị đơn lẻ, Mail Queue truyền các khối bộ nhớ (Memory Blocks). Đây là giải pháp tối ưu khi bạn cần truyền các cấu trúc dữ liệu lớn (Struct) giữa các tác vụ xử lý ảnh hoặc âm thanh.
3. Quản lý tài nguyên: Semaphore và Mutex
a) Semaphore (Đèn hiệu)
Dùng để đồng bộ hóa tác vụ với các sự kiện. Binary Semaphore giống như một mã thông báo (Token): Một Task phải “Take” được token mới được chạy, và khi xong phải “Give” lại để Task khác sử dụng.
b) Mutex (Loại trừ tương hỗ)
Hoạt động giống như một chiếc khóa bảo vệ tài nguyên dùng chung (ví dụ: cổng UART). Điểm khác biệt lớn nhất của Mutex là cơ chế Priority Inheritance (Thừa kế ưu tiên) giúp ngăn chặn tình trạng “Đảo ngược ưu tiên” – một lỗi kinh điển khiến Task cao cấp bị kẹt bởi Task thấp cấp.
4. Phân biệt các loại Delay
| Hàm Delay |
Đặc điểm |
Ảnh hưởng đến RTOS |
HAL_Delay() |
Vòng lặp tiêu tốn chu kỳ CPU. |
Làm treo Task hiện tại nhưng CPU vẫn chạy, lãng phí tài nguyên. |
vTaskDelay() |
Đưa Task vào trạng thái Blocked. |
Giải phóng CPU để các Task khác chạy trong lúc chờ. |
vTaskDelayUntil() |
Delay dựa trên mốc thời gian tuyệt đối. |
Đảm bảo tính chu kỳ chính xác tuyệt đối cho các Task điều khiển. |
5. Thực hành: Kích hoạt FreeRTOS trên STM32CubeIDE
Để triển khai dự án đa tác vụ trên STM32, hãy thực hiện theo các bước chuẩn công nghiệp sau:
Bước 1: Cấu hình Middleware
- Trong CubeMX, chọn Middleware -> FREERTOS.
- Thay đổi Interface thành CMSIS_V2 (Phiên bản mới nhất, hỗ trợ nhiều tính năng nâng cao).
Bước 2: Quản lý Task và Queue
- Tại ngăn Configuration -> Tasks and Queues:
- Tạo Task 1:
Blink01 (Priority: osPriorityNormal).
- Tạo Task 2:
Blink02 (Priority: osPriorityBelowNormal).
- Lưu ý: Task có độ ưu tiên cao hơn sẽ luôn được Scheduler ưu tiên thực thi trước.
Bước 3: Giải quyết xung đột Timebase (Quan trọng)
Mặc định, cả HAL Library và FreeRTOS đều muốn chiếm dụng SysTick làm nguồn đếm thời gian. Để tránh xung đột:
- Đi tới System Core -> SYS.
- Thay đổi Timebase Source từ
SysTick sang một Timer khác (thường là TIM6 hoặc TIM7).
- Việc này giúp HAL hoạt động ổn định trên Timer rời, nhường SysTick cho bộ lập lịch của FreeRTOS.
📝 Tóm tắt: Việc chuyển đổi sang RTOS yêu cầu bạn phải thay đổi cách tư duy về Delay và Ngắt. Hãy luôn ưu tiên dùng vTaskDelay, sử dụng đúng API FromISR trong ngắt và luôn cấu hình Timebase Source sang Timer rời để đảm bảo hệ thống vận hành trơn tru nhất.
🚀 Bài tập thực hành
- Task & Priority: Tạo 2 Task nháy LED với tần số khác nhau. Thử thay đổi độ ưu tiên và quan sát sự thay đổi nếu bạn sử dụng hàm
HAL_Delay thay vì vTaskDelay.
- Communication: Tạo một Task đọc cảm biến (giả lập bằng biến tăng dần) và gửi giá trị này qua Queue cho Task 2 để in lên màn hình qua UART.
- Software Timer: Sử dụng Software Timer để tạo một sự kiện chớp tắt LED mỗi 5 giây mà không cần tạo thêm Task mới.
“Làm chủ FreeRTOS là bước ngoặt quan trọng để bạn bước vào thế giới của các dự án nhúng phức tạp và chuyên nghiệp.”
Gợi ý bài tiếp theo: Làm Chủ Giao Tiếp Bộ Nhớ Flash Trên Vi Điều Khiển STM32F407.