RTOS hẳn mọi người đã có nghe qua, tuy nhiên làm sao để có thể hiểu được RTOS một cách đơn giản và cơ bản nhất thì hơi khó khăn với người mới bắt đầu. May thay có lần tham gia workshop của STM32 có bài hướng cơ bản dễ hiểu mình xin trình bày lại cho các bạn dễ nắm bắt.

RTOS là gì ?

Trước tiên chúng ta sẽ tìm hiểu xem khi nào là non-real time, khi nào là soft real time và hard real time ?

Với mô phỏng máy tính sẽ là non-real time ở cấp độ cao nhất, tiếp đến là giao tiếp người dùng, truyền video thông qua internet, tới mức soft real time là hệ thống cruise control trên ô tô (hệ thống tự kiểm soát tốc độ), các ứng dụng về viễn thông, điều khiển trên máy bay,động cơ điện thì yêu cầu mức hard real time, bạn thử nghĩ xem nếu như đi máy bay mà máy bay hạ xuống đường băng rồi mà 1,2s sau mới có thông báo thì sẽ như thế nào ?

Một hệ điều hành có thể

  • Cho phép nhiều chương trình chạy cùng 1 lúc (multi-tasking)
  • Có quản lý tài nguyên về phần cứng và cung cấp các dịch vụ cho các chương trình khác

Hình trên là cấu tạo của một hệ điều hành thời gian thực (RTOS) các bạn có thể xem qua để biết được bên trong nó sẽ có những gì.

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

Nếu ứng dụng của bạn mà 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, lúc đó 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 (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ác khái niệm 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
  1. Task A chờ event
  2. Task B chờ event
  3. Task B event sẵn sàng
  4. 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.
  1. Task A chờ event
  2. Task B chờ event
  3. Task B event sẵn sàng
  4. 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ớ, phần này mình sẽ nói 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.

Message queue

Message queue là cơ chế cho phép các task có thể kết nối với nhau, nó là một FIFO buffer đượ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 các thành phần 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 bộ nhớ trong message queue trống
  • Trường hợp nhiều task mà bị block thì task với 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 nếu có dữ liệu trong message queue.
  • Tương tự ghi thì task được unblock dựa trên mức độ ưu tiên

Ví dụ:

Mail queue

Giống như message queue nhưng dũ liệu sẽ được truyền dưới dạng khối(memory block) thay vì dạng đơn. Mỗi memory block thì cần phải cấp phát trước khi đưa dữ liệu vào và giải phóng sau khi đưa dữ liệu ra.

Gửi dữ liệu với mail queue

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

Nhận dữ liệu trong mail queue bởi task khác

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

Ví dụ:

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

  • 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 bộ

Couting 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ó 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ó resource 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 resouce 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ừ (mutial exclution), 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.

Tạm kết

Phù, thế cũng xong được một số khái niệm cơ bản thường hay dùng trong RTOS, bài này khá nhiều lý thuyết, bài sau mình sẽ tiếp tục với phần ứng dụng và lập trình một bài toán với cách lập trình thông thường và cách sử dụng RTOS với kit F4 chẳng hạn.

Update: Phần 2 các bạn có thể theo dõi tại đây thực hành RTOS.