Tổng quan về hệ điều hành thời gian thực RTOS

tong quan ve he dieu hanh thoi gian thuc rtos

Nếu đã là dân công nghệ, chắc hẳn bạn chẳng lạ lẫm gì với hệ điều hành hay OS, thế nhưng bạn đã nghe RTOS bao giờ chưa? RTOS là cái gì và cách nó hoạt động như thế nào? Tất cả sẽ có trong bài viết ngày hôm nay.

Rtos là gì?

RTOS là viết tắt của cụm từ Real-Time Operating System hay hệ điều hành thời gian thực, thường được nhúng trong các dòng vi điều khiển để điều khiển thiết bị một cách nhanh chóng và đa nhiệm (multitasking). Để hiểu rõ nó là gì, trước hết hãy làm rõ khái niệm về hệ điều hành đã.

Hệ điều hành (tiếng Anh: Operating System – viết tắt: OS) là một phần mềm dùng để điều hành, quản lý toàn bộ các thành phần (bao gồm cả phần cứng và phần mềm) của thiết bị điện tử.

Nói đơn giản, hệ điều hành giống như hội đồng quản trị vậy. Họ có quyền quyết định ai làm gì và làm trong bao lâu. Các nhân viên cũng như các ứng dụng, nhận lệnh của cấp trên và thực thi các công việc theo đúng chức năng của mình.

Vậy hệ điều hành thời gian thực với hệ điều hành bình thường khác gì nhau?

  • Hệ điều hành thông thường (non-realtime): như Windows, Linux, Android, iOS… chính là thứ mà chúng ta sử dụng hằng ngày. Khi mở một phần mềm trên đó, có thể bạn phải chờ nó tải rất lâu, việc chờ đợi này cũng không ảnh hưởng gì cả, bởi vì đa số phần mềm đó tương tác với con người chứ ít tương tác với các thiết bị khác.
  • Hệ điều hành thời gian thực (realtime): sinh ra cho các tác vụ cần hệ thống phản hồi trong một khoảng thời gian xác định, thường được nhúng trong các loại vi điều khiển và không có giao diện (GUI) tương tác với người dùng. Chúng cần phản hồi đúng hạn bởi vì đa số các tác vụ tương tác với thiết bị, máy móc khác chứ không phải con người. Tài nguyên bên trong rất hữu hạn, nên chỉ một sự chậm trễ cũng có thể làm hệ thống làm việc hoàn toàn sai lệch.

Bạn cứ thử tưởng tượng một hệ điều hành đang chạy các tác vụ điều khiển tên lửa mà độ trễ chỉ 2s. Với tốc độ của tên lửa thì cũng có thể bắn lệch từ Hà Nội thành TP Hồ Chí Minh rồi.

Thực tế hệ điều hành thời gian thực còn chia thành 2 loại:

  • Soft-realtime: thỉnh thoảng trễ hạn thì chất lượng giảm nhưng hệ thống vẫn chạy, ví dụ phát video/âm thanh, các ứng dụng viễn thông
  • Hard-realtime: trễ hạn dù chỉ 1 lần cũng coi như hỏng, ví dụ túi khí, phanh ABS trên ô tô, điều khiển máy bay, điều khiển động cơ điện

realtimesystem e1497500445332

Khi nào bạn cần sử dụng RTOS ?

Các ứng dụng không cần dùng 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 ứng dụng cần RTOS:

  • Ứng dụng nhiều trạng thái máy (State Machine)
  • Ứng dụng lớn
  • Ứng dụng liên quan tới các tác vụ xử lý nhanh, xử lý ảnh, âm thanh.

Nếu kích thước chương trình của bạn lớn dần và độ phức tạp tăng lên thì RTOS sẽ rất hữu dụng, lúc đó RTOS giúp chia ứng dụng phức tạp thành các phần nhỏ hơn và dễ quản lý hơn.

RTOS được sử dụng rất nhiều khi lập trình ESP32, ESP8266, STM32 và các dòng chip khác.

Tại sao lại phải dùng RTOS ?

Chia sẻ tài nguyên một cách đơn giản: RTOS cung cấp cơ chế để phân chia bộ nhớ và ngoại vi của MCU cho các tác vụ.

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, lập trình viên có thể ít phải đụng tới ngắt, timer, phần cứng (cái này mình không khuyến khích lắm vì hiểu được phần cứng vẫn sẽ tốt hơn nhiều).

Tăng tính linh động và dễ dàng bảo trì: thông qua API của RTOS,…

Cách hoạt động của Rtos

RTOS là một phần của chương trình, giải quyết việc điều phối các task, lập lịch và phân mức ưu tiên cho task, chuyển các thông điệp gửi đi giữa các task.

RTOS khá phức tạp, nói một cách dễ hiểu hơn là nó thực hiện việc xử lý các trạng thái máy (State Machine). Các bạn có thể tìm hiểu tại bài viết State Machine và lập trình nhúng.

Để giải quyết một bài toán nhiều trạng thái máy, thông thường chúng ta sử dụng code sau:

while(1)
{
    switch(state)
    {
        case 1: //Code for Task 1;
            state = 2;
            break;
        case 2: //Code for Task 2;
            state = 3;
            break;
        case 3: //Code for Task 3;
            state = 4;
            break;
        case 4: //Code for Task 4;
            state = 1;
            break;
    }
}

 

Bạn có thể thấy, chương trình sẽ thực thi từ state 1 tới state 4 sau đó quay vòng lại. Bất kì khi nào state thay đổi, ở vòng lặp tiếp theo chương trình sẽ nhảy qua phục vụ task tương ứng.

Ví dụ: nếu trong Task 1 có lệnh state = 4, thì ngay sau khi Task 1 thực thi xong, chương trình sẽ nhảy qua Task 4 mà bỏ qua Task 2 và 3.

Lưu ý: Mỗi case bắt buộc phải kết thúc bằng break;. Nếu quên, chương trình sẽ “rơi” xuống chạy tiếp code của các case bên dưới (fall-through) trong cùng 1 vòng lặp, và việc gán state để nhảy Task sẽ không còn đúng nữa.

Nhược điểm của phương pháp này là các task dùng chung tài nguyên, chuyển trạng thái chậm vì phải chạy xong mỗi Task trước khi chuyển sang Task khác, và khó kiểm soát khi có nhiều tác vụ (Task).

Vậy nên RTOS ra đời để giải quyết các nhược điểm trên.

RTOS hoạt động như thế nào
Cách hoạt động của RTOS

Nhân Kernel sẽ điều phối sự hoạt động của các tác vụ (Task), mỗi task có một mức ưu tiên (priority). Nếu có sự tác động như ngắt, tín hiệu hoặc tin nhắn giữa các Task, Kernel sẽ điều phối chuyển tới Task tương ứng.

Việc chuyển đổi giữa các Task rất linh động, độ trễ thấp và có thể tính trước được, mang lại độ tin cậy cao cho chương trình.

Các khái niệm trong hệ điều hành thời gian thực RTOS

Kernel – Nhân

Kernel hay còn gọi là Nhân có nhiệm vụ quản lý và điều phối các Task. Mọi sự kiện (Event) như ngắt, Timer, dữ liệu truyền tới… đều qua Kernel xử lý để quyết định xem nên làm gì tiếp theo.
Thời gian xử lý của Kernel thường rất nhanh nên độ trễ rất thấp.

kernel Rtos

Task – Tác vụ

Bạn cứ tưởng tượng một chương trình là một công ty, với ông to nhất là Giám đốc – Kernel. Ông này chỉ điều hành và chả biết nghiệp vụ gì cả, để thực hiện các nghiệp vụ khác nhau, công ty đó cần các nhân viên. Và các nhân viên đó gọi là các Task.

Nói đơn giản, Task là một đoạn chương trình thực thi một hoặc nhiều công việc nào đó, được Kernel quản lý.

Kernel sẽ quản lý việc chuyển đổi giữa các task: nó lưu lại ngữ cảnh (context) của task sắp bị tạm dừng và khôi phục lại ngữ cảnh của task tiếp theo. Việc chuyển task xảy ra khi:

  • Hết thời gian thực thi đã định trước (time slice, được tạo ra bởi ngắt SysTick)
  • Có sự kiện làm một task có mức ưu tiên cao hơn thoát khỏi trạng thái chờ (signal, queue, semaphore,…)
  • Task gọi hàm Yield (ví dụ taskYIELD() trong FreeRTOS) để chủ động nhường CPU sang task khác mà không phải chờ hết time slice

Ngoài ra, khi khởi động thì Kernel sẽ tạo ra một task mặc định gọi là Idle Task, chạy khi không có task nào khác cần chạy.

Task States – Trạng thái Task

Một ông nhân viên thì cũng có lúc này lúc kia, lúc rảnh rỗi, lúc đang làm việc, hay cũng có lúc nghỉ vì… vợ đẻ. Những lúc như vậy Kernel phải điều phối làm sao cho chương trình chạy hợp lý.

Không thể bắt ông đang trông vợ đẻ đi làm được, đúng không?

Một task trong RTOS thường có các trạng thái như sau:

Trạng thái Task trong Rtos
Trạng thái Task trong Rtos
  • RUNNING: đang thực thi
  • READY: sẵn sàng để thực hiện, chờ tới lượt
  • WAITING: chờ sự kiện (trong FreeRTOS gọi là Blocked)
  • INACTIVE: không được kích hoạt (trong FreeRTOS gần giống Suspended)

Scheduler – Lập lịch

Nói đơn giản thì Scheduler là một bé thư kí hoặc trợ lý của ông giám đốc Kernel. Bé này sẽ có nhiệm vụ nhắc nhở ông sếp mình cần làm gì tại các thời điểm khác nhau.

Đây là 1 thành phần của Kernel quyết định task nào được thực thi. Có một số kiểu lập lịch như:

Cooperative: giống với lập trình thông thường, task tiếp theo chỉ được chạy khi task đang chạy tự dừng lại. Nhược điểm là 1 task có thể chiếm hết CPU nếu nó không chịu nhường.

Round-robin: mỗi task được thực hiện trong một khoảng thời gian định trước (time slice) rồi tới lượt task khác, không có ưu tiên.

Round robin

Priority base: task có mức ưu tiên cao nhất sẽ được chọn chạy trước ở lần lập lịch tiếp theo. Nếu các task có cùng mức ưu tiên thì sẽ chạy giống round-robin. Task đang chạy không bị ngắt giữa chừng mà được chạy cho đến hết time slice.

Priority based e1497687849850

Priority-based pre-emptive: ngay khi một task có mức ưu tiên cao hơn sẵn sàng (READY), nó sẽ chiếm quyền CPU (preempt) của task có mức ưu tiên thấp hơn đang chạy, không cần chờ hết time slice. Đây là kiểu lập lịch mặc định của FreeRTOS.

Priority based pre emptive

Kết nối Inter-task & Chia sẻ tài nguyên

Để công ty vận hành được trơn tru thì các nhân viên cũng phải liên lạc và trao đổi với nhau. Làm coder thì cũng phải nói chuyện với ông làm Sales để xem mình code vậy có bán được không, làm nhân viên cũng phải hiểu các nghiệp vụ của nhau, chứ không xung đột tài nguyên là vỡ đầu chảy máu.

Vậy nên các task cần 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, hoạt động giống như FIFO
  • Mail queue – trao đổi dữ liệu giữa các task bằng hàng đợi các khối bộ nhớ

Với Resource Sharing:

  • Semaphore – đồng bộ và giới hạn số task truy cập tài nguyên cùng lúc
  • Mutex – bảo vệ tài nguyên dùng chung theo cơ chế loại trừ lẫn nhau (Mutual Exclusion)

Signal event

Signal event được dùng để đồng bộ các task, ví dụ như bắt task phải chờ tới một sự kiện nào đó được định sẵn mới thực thi.

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 WAITING cho đến khi signal được set. Ngoài ra ta có thể set 1 hoặc nhiều signal từ bất kỳ task nào khác.

Mỗi task có một tập cờ signal riêng, số cờ tối đa tùy vào từng RTOS (thường từ 16 tới 32 cờ).

Message queue – Hàng đợi tin nhắn

Nói nôm na là một group chat trong công ty để các thành viên trao đổi với nhau.

Message queue là cơ chế cho phép các task có thể kết nối với nhau, nó là một bộ đệm FIFO (First In First Out) được định nghĩa bởi độ dài (số phần tử mà buffer có thể lưu trữ) và kích thước dữ liệu (kích thước của mỗi phần tử trong buffer).

Một ứng dụng tiêu biểu là buffer cho Serial I/O, buffer cho lệnh được gửi tới task.

Task có thể ghi vào hàng đợi (queue):

  • Task sẽ bị khóa (block) khi gửi dữ liệu tới một message queue đã đầy
  • Task sẽ hết bị khóa (unblock) khi message queue có chỗ trống
  • Trường hợp nhiều task cùng bị block thì task có mức ưu tiên cao nhất sẽ được unblock trước

Task có thể đọc từ hàng đợi (queue):

  • Task sẽ bị block nếu message queue trống
  • Task sẽ được unblock khi có dữ liệu trong message queue.
  • Tương tự như khi ghi, task được unblock dựa trên mức độ ưu tiên

Ví dụ:

Message queue ex

 

Mail queue

Nói nôm na giống như các nhân viên trao đổi email với nhau. Các email này có thể bao gồm nhiều thứ như văn bản và các file đính kèm.

Mail queue truyền dữ liệu dưới dạng khối (memory block) thay vì từng giá trị đơn lẻ. Mỗi memory block cần được cấp phát trước khi đưa dữ liệu vào và giải phóng sau khi lấy dữ liệu ra.

Thao tác gửi dữ liệu với mail queue:

  1. Cấp phát bộ nhớ từ mail queue cho dữ liệu cần gửi
  2. Lưu dữ liệu cần gửi vào bộ nhớ đã được cấp phát
  3. Đưa dữ liệu vào mail queue

Thao tác nhận dữ liệu trong mail queue bởi task khác:

  1. Lấy dữ liệu từ mail queue, sẽ có một hàm trả về cấu trúc/đối tượng
  2. Lấy con trỏ chứa dữ liệu
  3. Giải phóng bộ nhớ sau khi sử dụng dữ liệu

Ví dụ:

Mail queue 2

 

Semaphore

Nói đơn giản Semaphore là thủ quỹ của công ty bạn vậy. Mỗi công ty có 1 lượng tiền cố định, nếu ông nhân viên nào cũng đòi tiền để làm cái này cái kia mà không hoàn lại cho công ty, thì chắc chắn phá sản.

Trong vi điều khiển chúng ta dùng Semaphore để quản lý những tài nguyên (Resource) như RAM, ngoại vi…

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:

Binary semaphore

Binary semaphore giống như một tấm thẻ (token) duy nhất: task nào lấy được thẻ thì mới được chạy tiếp. Nếu thẻ đang không có, task sẽ nằm ở trạng thái chờ (block) và không thể thực thi cho tới khi có task hoặc ngắt khác trả thẻ lại.

  • Có duy nhất 1 token
  • Chỉ có 1 hoạt động đồng bộ

Binary Semaphore defintion

Counting semaphore

Counting semaphore đưa ra các Token cho các Task sử dụng tài nguyên, nếu tài nguyên được sử dụng hết thì các Task còn lại sẽ phải chờ đến khi tài nguyên được giải phóng trở lại.

cc5541a7 c87f 4c4a 8ae5 709801e8ab3a

Counting semaphore được dùng để:

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ó xử lý sự kiện (giảm giá trị đếm semaphore)
  • Count value là hiệu số giữa số sự kiện đã xảy ra và số sự kiện đã được xử lý
  • 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ó
  • Task muốn dùng resource phải take semaphore (giá trị đếm giảm), nếu count value giảm xuống bằng 0 nghĩa là không còn resource nào trống.
  • Khi một task dùng xong resource thì nó sẽ give semaphore trở lại để tăng count value.
  • Trong trường hợp resource management thì count value ban đầu sẽ bằng giá trị max của count value khi semaphore được tạo.

Mutex

Mutex giống như ông thủ kho của công ty bạn vậy. Vì tài nguyên của công ty hữu hạn, giả sử khi nhân viên A đang mượn cái đồng hồ bấm giờ, nếu nhân viên B tới mượn chiếc đồng hồ đó thì Mutex sẽ bắt nhân viên B chờ cho tới khi nhân viên A trả lại.

Mutex được sử dụng cho việc loại trừ lẫn nhau (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ẻ thì:

  • Cần yêu cầu (đợi) mutex trước khi truy cập vào tài nguyên chia sẻ (Shared Resource)
  • Trả lại token khi dùng xong tài nguyên.

Tại mỗi thời điểm chỉ có 1 task giữ được mutex. Những task khác muốn lấy 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 nó có cơ chế kế thừa mức độ ưu tiên (Priority Inheritance) để giảm thiểu vấn đề đảo ngược ưu tiên (Priority Inversion), cơ chế này có thể hiểu đơn giản qua ví dụ sau:

  • Task A (low priority) đang giữ mutex
  • Task B (high priority) muốn lấy cùng mutex trên nên phải chờ.
  • Task A được tạm nâng mức ưu tiên lên bằng Task B, để nó chạy xong nhanh mà không bị các task ưu tiên trung bình chen ngang
  • Task A thả mutex ra, mức độ ưu tiên của nó được khôi phục lại như cũ và Task B tiếp tục thực thi.

Lưu ý: Khi cần bảo vệ tài nguyên dùng chung (UART, biến toàn cục…), hãy dùng Mutex chứ đừng dùng Binary semaphore, vì Binary semaphore không có cơ chế kế thừa ưu tiên nên dễ dính lỗi đảo ngược ưu tiên. Ngược lại, Mutex không được dùng trong hàm ngắt (ISR), muốn báo từ ngắt sang task thì dùng semaphore.

Kết

Và đó là tất cả những lý thuyết về hệ điều hành thời gian thực RTOS, có thể nghe thì rất mông lung, huyền ảo và có phần hại não. Nhưng thực ra nó cũng không khó chút nào.

Vậy nên hãy tiếp tục đến với những bài tiếp theo để biết RTOS hoạt động trên STM32 như thế nào nhé!

Bài viết này được tham khảo tại nguồn: học arm, thanhnt, deviot… và một số nguồn nước ngoài khác

Nếu thấy hay, hãy like và tham gia vào nhóm những người anh em Nghiện Lập Trình để giao lưu và học hỏi nhé!

 

4.8/5 - (13 bình chọn)

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *