
Hướng dẫn lựa chọn hệ điều hành thời gian thực (RTOS) phù hợp cho ứng dụng của bạn
Chắc hẳn khi nhắc đến hệ điều hành, bạn sẽ nghĩ ngay đến những cái tên quen thuộc như Windows và macOS. Nhưng bạn đã bao giờ nghe đến cái tên RTOS chưa?
Khi nhắc đến hệ điều hành chắc hẳn là trong đầu các bạn sẽ nghĩ ngay đến những cái tên quen thuộc như Windows, macOS cho laptop hay Android, iOS cho smartphone. Vậy đã bao giờ các bạn có nghe đến cái tên RTOS hay chưa ?
Khái niệm
RTOS (real-time operating system) hay hệ điều hành thời gian thực là một hệ điều hành (OS) nhằm phục vụ các ứng dụng thời gian thực, với khả năng xử lý dữ liệu đầu vào nhanh chóng do không có sự chậm trễ của bộ đệm (buffer).
Hệ điều hành thời gian thực (RTOS) được sử dụng trong môi trường có số lượng lớn các dữ kiện, tác vụ, cần xử lý trong thời gian ngắn hoặc trong thời hạn nhất định. Với RTOS, thời gian xử lý được tính bằng phần mười giây hoặc ít hơn. Quá trình xử lý trong RTOS phải diễn ra trong các ràng buộc, giới hạn thời gian được chỉ định, nếu không sẽ dẫn đến lỗi hệ thống.
Khác với các hệ điều hành thông thường như Windows, Android, iOS,… chứa rất nhiều ứng dụng và tính năng nên cần có thời gian khởi chạy khi mở ứng dụng lên, RTOS được thiết kế cho các nhiệm vụ đặc biệt, với thời gian thực thi các tác vụ trong thời gian chính xác, các lỗi được cô lập và xử lý nhanh chóng.
Khi nào cần sử dụng RTOS?
Nếu ứng dụng của bạn có kích thước chương trình lớn dần và độ phức tạp tăng lên thì RTOS sẽ rất hữu dụng trong trường hợp này, RTOS sẽ chia các ứng dụng phức tạp thành các phần nhỏ hơn và dễ quản lý hơn.
Tại sao lại phải dùng RTOS ?
• Chia sẻ tài nguyên một cách đơn giản: cung cấp cơ chế để phân chia các yêu cầu về bộ nhớ và ngoại vi của MCU
• Dễ debug và phát triển: Mọi người trong nhóm có thể làm việc một cách độc lập, các lập trình viên thì có thể tránh được các tương tác với ngắt, timer, với phần cứng ( không khuyến khích vì hiểu được phần cứng là điều cần thiết)
• Tăng tính linh động và dễ dàng bảo trì: thông qua API của RTOS,…
Các ứng dụng không cần dùng RTOS:RTOS
• Ứng dụng đơn (ứng dụng chỉ có 1 chức năng)
• Ứng dụng có vòng lặp đơn giản
• Ứng dụng <32kB
Các thuật ngữ cơ bản:
Kernel
Kernel sẽ có nhiệm vụ quản lý nhiều task cùng chạy 1 lúc, mỗi task thường chạy mất vài ms. Tại lúc kết thúc task thường:
• Lưu trạng thái task
• Thanh ghi CPU sẽ load trạng thái của task tiếp theo
• Task tiếp theo cần khoảng vài ms để thực hiện
• Vì CPU thực hiện tác vụ rất nhanh nên dưới góc nhìn người dùng thì hầu như các task là được thực hiện 1 cách liên tục.
Task state
Một task trong RTOS thường có các trạng thái như sau:
• RUNNING: đang thực thi
• READY: sẵn sàng để thực hiện
• WAITING: chờ sự kiện
• INACTIVE: không được kích hoạt
Scheduler
Đây là 1 thành phần của kernel quyết định task nào được thực thi. Có một số luật cho scheduling như:
• Cooperative: giống với lập trình thông thường, mỗi task chỉ có thể thực thi khi task đang chạy dừng lại, nhược điểm của nó là task này có thể dùng hết tất cả tài nguyên của CPU
• Round-robin: mỗi task được thực hiện trong thời gian định trước (time slice) và không có ưu tiên.
• Priority base: Task được phân quyền cao nhất sẽ được thực hiện trước, nếu các task có cùng quyền như nhau thì sẽ giống với round-robin, các task có mức ưu tiên thấp hơn sẽ được thực hiện cho đến cuối time slice
Task A chờ event
Task B chờ event
Task B event sẵn sàng
Task A event sẵn sàng
• Priority-based pre-emptive: Các task có mức ưu tiên cao nhất luôn nhường các task có mức ưu tiên thấp hơn thực thi trước.
Task A chờ event
Task B chờ event
Task B event sẵn sàng
Task A event sẵn sàng
Task
Một task là một chương trình, chương trình này chạy liên tục trong vòng lặp vô tận và không bao giờ dừng lại. Với mỗi task thì có niềm tin duy nhất là chỉ mình nó đang chạy và có thể sử dụng hết nguồn tài nguyên sẵn có của bộ xử lý (mặc dù là thực tế thì nó vẫn phải chia sẻ nguồn tài nguyên này với các task khác).
Một chương trình thường sẽ có nhiều task khác nhau. Ví dụ như máy bán đồ uống tự động sẽ có các thành task sau:
• Task quản lý việc lựa chọn của người dùng
• Task để kiểm tra đúng số tiền người dùng đã trả
• Task để điều khiển động cơ/ cơ cấu cung cấp nước uống.
Kernel sẽ quản lý việc chuyển đổi giữa các task, nó sẽ lưu lại ngữ cảnh của task sắp bị hủy và khôi phục lại ngữ cảnh của task tiếp theo bằng cách:
• Kiểm tra thời gian thực thi đã được định nghĩa trước (time slice được tạo ra bởi ngắt systick)
• Khi có các sự kiện unblocking một task có quyền cao hơn xảy ra (signal, queue, semaphore,…)
• Khi task gọi hàm Yield() để ép Kernel chuyển sang các task khác mà không phải chờ cho hết time slice
• Khi khởi động thì kernel sẽ tạo ra một task mặc định gọi là Idle Task.
Để tạo một task thì cần phải khai báo hàm định nghĩa task, sau đó tạo task và cấp phát bộ nhớ, sẽ được đề cập phía sau.
Kết nối Inter-task & Chia sẻ tài nguyên
Các task cần phải kết nối và trao đổi dữ liệu với nhau để có thể chia sẻ tài nguyên, có một số khái niệm cần lưu ý
Với Inter-task Communication:
• Signal Events – Đồng bộ các task
• Message queue – Trao đổi tin nhắn giữa các task trong hoạt động giống như FIFO
• Mail queue – Trao đổi dữ liệu giữa các task sử dụng hằng đợi của khối bộ nhớ
Với Resource Sharing
Semaphores – Truy xuất tài nguyên liên tục từ các task khác nhau
Mutex – Đồng bộ hóa truy cập tài nguyên sử dụng Mutual Exclusion
Signal event
Signal event được dùng để đồng bộ các task, ví dụ như bắt task phải thực thi tại một sự kiện nào đó được định sẵn
Ví dụ: Một cái máy giặt có 2 task là Task A điều khiển động cơ, Task B đọc mức nước từ cảm biến nước đầu vào.Task A cần phải chờ nước đầy trước khi khởi động động cơ. Việc này có thể thực hiện được bằng cách sử dụng signal event.Task A phải chờ signal event từ Task B trước khi khởi động động cơ.Khi phát hiện nước đã đạt tới mức yêu cầu thì Task B sẽ gửi tín hiệu tới Task A.Với trường hợp này thì task sẽ đợi tín hiệu trước khi thực thi, nó sẽ nằm trong trạng thái là WAITING cho đến khi signal được set. Ngoài ra ta có thể set 1 hoặc nhiều signal trong bất kỳ các task nào khác.
Mỗi task có thể được gán tối đa là 32 signal event
Ưu điểm của nó là thực hiện nhanh, sử dụng ít RAM hơn so với semaphore và message queue nhưng có nhược điểm lại chỉ được dùng khi một task nhận được signal.
Semaphore
Được sử dụng để đồng bộ task với các sự kiện khác trong hệ thống. Có 2 loại là Binary semaphore và Counting semaphore :
• Binary semaphore: Là trường hợp đặc biệt của counting semaphore, có duy nhất 1 token, chỉ có 1 hoạt động đồng bộ
• Counting semaphore: Có nhiều token, có nhiều hoạt động đồng
Counting semaphore được dùng để counting event hoặc resource management:
Counting event
• Một event handler sẽ ‘give’ semaphore khi có event xảy ra (tăng giá trị đếm semaphore)
• Một task handler sẽ ‘take’ semaphore khi nó thực thi sự kiện (giảm giá trị đếm semaphore)
• Count value là khác nhau giữa số sự kiện xảy ra và số sự kiện được thực thi
• Trong trường hợp counting event thì semaphore được khởi tạo giá trị đếm bằng 0
Resource management
• Count value sẽ chỉ ra số resource sẵn có
• Để điều khiển và kiểm soát được resource của task dựa trên count value của semaphore(giá trị giảm), nếu count value giảm xuống bằng 0 nghĩa là không có source nào free.
• Khi một task finish với resource thì nó sẽ give semaphore trở lại để tăng count value của semaphore.
• Trong trường hợp resource management thì count value sẽ bằng với giá trị max của count value khi semaphore được tạo.
Mutex
Sử dụng cho việc loại trừ (mutual exclusion), hoạt động như là một token để bảo vệ tài nguyên được chia sẻ. Một task nếu muốn truy cập vào tài nguyên chia sẻ:
• Cần yêu cầu (đợi) mutex trước khi truy cập vào tài nguyên chia sẻ
• Đưa ra token khi kết thúc với tài nguyên.
Tại mỗi một thời điểm thì chỉ có 1 task có được mutex. Những task khác muốn cùng mutex thì phải block cho đến khi task cũ thả mutex ra.
Về cơ bản thì Mutex giống như binary semaphore nhưng được sử dụng cho việc loại trừ chứ không phải đồng bộ. Ngoài ra thì nó bao gồm cơ chế thừa kế mức độ ưu tiên(Priority inheritance mechanism) để giảm thiểu vấn đề đảo ngược ưu tiên, cơ chế này có thể hiểu đơn giản qua ví dụ sau:
• Task A (low priority) yêu cầu mutex
• Task B (high priority) muốn yêu cầu cùng mutex trên.
• Mức độ ưu tiên của Task A sẽ được đưa tạm về Task B để cho phép Task A được thực thi
• Task A sẽ thả mutex ra, mức độ ưu tiên sẽ được khôi phục lại và cho phép Task B tiếp tục thực thi.