Hệ điều hành thời gian thực (RTOS)

ntaquan

Gà con
Phần 1: Các khái niệm cơ bản

1. Hệ điều hành (OS)

OS là một phần mềm dùng để điều hành và quản lý tài nguyên và điều phối các hoạt động của hệ thống máy tính, nó có các chức năng sau:
  • Allocation of memory
  • Power management
  • Which process uses the CPU
  • When I/O takes place
  • Safety and Security Features
Các ứng dụng (application) sẽ chạy trên nền của hệ điều hành.
1629987413136.png

2. Hệ thống thời gian thực
1629987483838.png

A: non-realtime, B: soft-realtime, C: hard-realtime
Hard-realtime: được thiết kế để hoạt động trong các giới hạn thời gian rất nghiêm ngặt, trong đó việc bỏ lỡ thời hạn (deadline) có thể dẫn đến hậu quả thảm khốc. Ví dụ về các hệ thống thời gian thực cứng bao gồm hệ thống kiểm soát không lưu, hệ thống điều khiển cho các nhà máy điện hạt nhân. Các hệ thống này phải đáp ứng thời hạn một cách chắc chắn tuyệt đối và không thể chấp nhận bất kỳ sự chậm trễ hoặc lỗi hệ thống nào.

Soft-realtime: được thiết kế để hoạt động trong các ràng buộc thời gian linh hoạt hơn. Các hệ thống này có thể có thời hạn quan trọng, nhưng việc bỏ lỡ thời hạn không dẫn đến hậu quả thảm khốc. Ví dụ về các hệ thống thời gian thực mềm bao gồm các ứng dụng đa phương tiện như truyền phát video, gaming trực tiếp. Mặc dù các hệ thống này yêu cầu xử lý kịp thời, nhưng chúng có thể chịu được một số độ trễ mà không ảnh hưởng đến nhiều trải nghiệm chung của người dùng.

Non-realtime: hệ thống trong đó thời gian phản hồi không quan trọng và không có các ràng buộc nghiêm ngặt về thời gian xử lý. Thời gian phản hồi có thể khác nhau tùy thuộc vào tải trên hệ thống và tài nguyên hệ thống.

1629987553916.png

3. Hệ điều hành thời gian thực (RTOS)
Sử dụng trong các hệ thống cần thời gian đáp ứng nhanh và chính xác.
Sử dụng trong các hệ thống lớn, cần quản lý tài nguyên.
Ưu điểm:
  • Maintainability/Extensibility
  • Code reuse/Easier testing
  • Team development
  • Improved efficiency
  • Power Management

4. Kernel: là một lớp trừu tượng giúp quản lý hoạt động giữa hệ điều hành và phần cứng, là thành phần cốt lõi trong hệ điều hành. Các dịch vụ của kernel bao gồm:
1629987622923.png

5. Task: chương trình sử dụng RTOS được chia thành những đơn vị nhỏ gọi là task, mỗi task là một hàm. Mỗi task sẽ bao gồm một vòng lặp vô tận và không có câu lệnh trả về.
Kernel sẽ chịu trách nhiệm quản lý các task bằng cách dừng và phục hồi hoạt động của task.
Một chương trình có thể bao gồm nhiều task, vi điều khiển thường chỉ có 1 core nên trong một thời điểm chỉ có đúng 1 task được chạy. Vậy task sẽ có 2 trạng thái Runing và Not running. Trong đó trạng thái Not running còn bao gồm các trạng thái nhỏ hơn:
1629987679560.png

  • RUNNING: executing on the CPU
  • READY: ready to be executed
  • WAITING / BLOCKED: waiting for an event
  • INACTIVE / SUSPENDED: not active
Task chuyển từ trạng thái Not running sang Running gọi là “swapped in”, “switched in”. Task chuyển từ trạng thái Running sang Not running gọi là “swapped out”, “switched out”.

6. Cấu trúc chương trình nhúng
1629987724964.png


Simple loop: là phần mềm cơ bản trong đó một loạt các lệnh được thực hiện lặp đi lặp lại một cách tuần tự. Trong loại chương trình này, thời gian thực hiện chương trình là không xác định. Chương trình càng lớn, tính real-time càng bị ảnh hưởng.

Loop and ISRs: là một phần mở rộng của simple loop, trong đó các sự kiện ngắt nhất định có thể làm gián đoạn luồng chương trình chính và được thực thi ngay lập tức. Tuy nhiên, chương trình càng lớn, càng khó quản lý (thứ tự ưu tiên các ngắt, chương trình ngắt lớn, độ trễ ngắt)

RTOS: được thiết kế đặc biệt cho các hệ thống thời gian thực, trong đó thời gian thực hiện các task là xác định. Các chương trình RTOS đảm bảo rằng các task quan trọng được hoàn thành trong thời hạn của chúng.

7. Multitasking Vs Concurrency
Bằng cách chuyển qua lại giữa các task, nó cho ta cảm giác như các task được chạy một cách đồng thời mặc dù tại một thời điểm chỉ 1 task chạy.
1629987781742.png
Ngày nay thì đa số là sử dụng vi xử lý đa nhân nên sẽ là real parallelism chứ không còn là concurrency so với vi xử lý đơn nhân nữa.

8. Scheduler: Là một phần của kernel, xác định task nào được phép chạy.
Một số luật cho scheduling:
Cooperative scheduling: Giống với lập trình thông thường, mỗi task chỉ có thể thực thi khi task trước đó thực hiện xong, task có thể dùng hết tất cả tài nguyên của CPU. Ưu điểm là nó dễ thực hiện, lý tưởng cho các hệ thống nhỏ, nơi tài nguyên bị hạn chế. Tuy nhiên, nhược điểm chính là nó có thể dẫn đến hiệu quả và khả năng phản hồi của hệ thống thấp, vì các task có thể chiếm độc quyền của CPU và ngăn cản các task khác thực thi (vd max time out, delay).
1629987855370.png
Round-robin scheduling: Mỗi task được thực hiện trong thời gian định trước (time slice) và không có ưu tiên. Khi một task đã chạy hết thời gian của nó, nó sẽ bị dừng và di chuyển đến cuối queue, và task tiếp theo trong queue sẽ được trao quyền kiểm soát CPU.
1629987878287.png
Preemptive scheduling: Các task có mức ưu tiên thấp luôn nhường các task có mức ưu tiên cao hơn thực thi trước.
1629987938841.png

Tài liệu tham khảo
  • Bài giảng "Thiết kế hệ thống nhúng", bộ môn Điện Tử
  • Bài giảng "Lập trình hệ thống nhúng", bộ môn Điện Tử
  • Bài giảng "Mạng cảm biến không dây và ứng dụng", bộ môn Viễn Thông
  • "Mastering the FreeRTOS Real Time Kernel - A Hands-On Tutorial Guide"
  • "Mastering STM32", Carmine Noviello
 

ntaquan

Gà con
Phần 2: FreeRTOS trên STM32

1. Vài phút giới thiệu

Board sử dụng: STM32F407VG Discovery
Với STM32 thì chúng ta sẽ có thêm một phần implement mới là CMSIS-RTOS, phần này được thêm vào trong file cmsis-os.c (ở thư mục FreeRTOS/Source/CMSIS_RTOS), nếu dùng cái này thì nó sẽ có một số thay đổi so với bản FreeRTOS gốc, có đổi tên một số API, cần phải check bảng mapping bên dưới để biết API tương ứng giữa CMSIS-RTOS API và FreeRTOS API.
1630179425846.png
Dù gì thì CMSIS cũng được build trên FreeRTOS API, ví dụ như hình dưới, trong hàm osKernelStart() có gọi vTaskStartScheduler() =)), nên để cho tiện thì các code demo bài này sử dụng sẽ là của CMSIS-RTOS API (do CubeMX gen code ra) tuy nhiên các phần giải thích sẽ theo các hàm của FreeRTOS API.
1630333848767.png

2. Cấu trúc thư viện FreeRTOS
1630179523693.png
Muốn sử dụng được thư viện của FreeRTOS (compiler hiểu và build được) cần phải thêm những thứ sau vào include path
  1. Đường dẫn tới thư mục chưa các file header nằm ở FreeRTOS/Source/include.
  2. Đường dẫn tới thư mục chưa các file source nằm ở FreeRTOS/Source/portable/[compiler]/[architecture]. Với architecture là kiến trúc của bộ xử lý (ở đây sẽ là ARM_CM4F) và compiler là bộ biên dịch (GCC hoặc CCS hoặc Clang).
  3. Đường dẫn tới FreeRTOSConfig.h header file.
  4. Nếu dùng CMSIS_RTOS cũng phải có đường dẫn tới thư mục chưa các file của cmsis-os.
1630179618778.png
CubeIDE đã hỗ trợ add những thứ này cho người dùng sẵn rồi. Có thể kiểm tra lại bằng cách vào Project -> Properties -> C/C++ Build -> Settings -> Tool Settings -> MCU GCC Compiler -> Include Paths.
Tiếp theo chúng ta cần match những hàm ngắt của vi điều khiển với hàm ngắt của RTOS trong file FreeRTOSConfig.h (CubeIDE cũng làm cho luôn rồi :5cool_still_dreaming:)
1630204012080.png
Đối với cái dòng Cortex-M4F or Cortex-M7 có hỗ trợ Floating Point Unit (FPU), bởi vì các thanh ghi cũng cần được lưu trong quá trình switch context nên nếu không enable FPU sẽ xuất hiện lỗi như sau:
1632672376484.png
Để enable FPU, vào Project Settings->C/C++ Build->Settings->Target Processor chọn FP instructions (hard) trong mục Float ABI và fpv4-sp-d16 đối với dòng Cortex-M4F hoặc pv5sp-d16 đối với dòng Cortex- M7 trong mục FPU Type.

3. Convention của FreeRTOS
3.1. Kiểu dữ liệu:
1630179732873.png
3.2. Tên biến:
Tên biến có tiền tố là kiểu dữ liệu: ‘c’ cho char, ‘s’ cho int16_t (short), ‘l’ cho int32_t (long), và ‘x’ cho BaseType_t và các kiểu dữ liệu không thuộc kiễu dữ liệu chuẩn (người dùng tự đặt) như: structures, task handles, queue handles,...
Nếu biến là số nguyên không dấu, sẽ có thêm tiền tố ‘u’.
Nếu biến là con trỏ, sẽ có thêm tiền tố ‘p’.
3.3. Tên hàm:
Cũng có cách đặt tiền tố giống tên biến ứng với kiểu dữ liệu trả về cùng với file mà chúng được định nghĩa. Ví dụ:
vTaskPrioritySet() trả về kiểu void và được định nghĩa trong task.c.
xQueueReceive() trả về kiểu BaseType_t và được định nghĩa trong queue.c.
pvTimerGetTimerID() trả về con trỏ kiểu void và được định nghĩa trong timers.c.
Để dùng các hàm của FreeRTOS API cần phải include ‘FreeRTOS.h’, kèm theo header file chứa nguyên mẫu của API function: ‘task.h’, ‘queue.h’, ‘semphr.h’, ‘timers.h’ or ‘event_groups.h’.

4. Convention của CMSIS RTOS
Mọi thứ đều có tiền tố ‘os’ :6cool_boss: chấm hết.

5. Hello world
Config trong cubeMX:
1630227197711.png
1630227209576.png
Dùng time source là TIM14 cho HAL thay vì Systick. Vì FreeRTOS sử dụng time source Systick, ta muốn tránh nó xung đột với thư viện HAL.

1630202716080.png
1630202772302.png
Code generate ra sẽ có dạng giống như vậy:
C:
osThreadId defaultTaskHandle;
int main(void)
{
  /* MCU Configuration--------------------------------------------------------*/
  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();
  /* Configure the system clock */
  SystemClock_Config();
  /* Initialize all configured peripherals */
  MX_GPIO_Init();

  /* Create the thread(s) */
  /* definition and creation of defaultTask */
  osThreadDef(defaultTask, StartDefaultTask, osPriorityNormal, 0, 128);
  defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);

  /* Start scheduler */
  osKernelStart();

  /* We should never get here as control is now taken by the scheduler */
  /* Infinite loop */
  while (1)
  {

  }
}
Để tạo 1 task (thread) mới, dùng hàm:
C:
osThreadId osThreadCreate(const osThreadDef_t *thread_def, void *argument);
Thông số của thread_def được đưa vào thông qua macro osThreadDef(name, thread, priority, instances, stacksz)
Trong đó:
  • name: là tên của task
  • thread: là con trỏ chỉ tới hàm gọi khi task chạy
  • priority: mức ưu tiên của task
  • instances: số lần có thể gọi osThreadCreate() trên cùng 1 osThreadDef
  • stacksz: dung lượng stack để lưu ngữ cảnh của task
  • argument: con trỏ chỉ tới tham số truyền vào hàm
osThreadCreate() sẽ tạo một task mới vào báo cho kernel biết để scheduling, hàm trả về Thread ID, người dùng có thể từ đó giám sát được trạng thái của task osThreadCreate() đưa tham số vào hàm.
Khai báo xong các task thì hàm osKernelStart() được gọi để bắt đầu scheduler.

C:
void StartDefaultTask(void const * argument)
{
    /* Infinite loop */
    for(;;)
    {
        HAL_GPIO_TogglePin(LD3_GPIO_Port, LD3_Pin);
        osDelay(500);
    }
}
Chương trình trên chỉ là một chương trình nháy led bình thường :gach. Chỉ khác ở chỗ thay vì dùng HAL_Delay() thì đổi thành osDelay() để tạo một delay 500ms. Hàm này có tác dụng đưa task vào trạng thái block để không ngốn tài nguyên của CPU. Sau 500ms thì task sẽ chạy trở lại và nháy LED. Tạm hiểu vậy đi, hàm delay sau này sẽ gặp lại :whaaat:.

Hàm xTaskCreate(), vTaskStartScheduler()vTaskDelay() interface hơi khác một xíu nhưng ý tưởng thì tương tự. Người viết bài này khuyến khích xem qua để tới phần sau không bị bỡ ngỡ :D.

Bài tập: Tạo 3 task cùng độ ưu tiên: Task 1 chớp tắt led chu kỳ 1s, Task 2 gửi 'Task 2\r\n' qua UART mỗi 1s, Task 3 gửi 'Task 3\r\n' qua UART mỗi 2s.
 

ntaquan

Gà con
Phần 3: Task
1. Create task

Tên của task thường được sử dụng cho mục đích debug. Macro configMAX_TASK_NAME_LEN quy định số ký tự tối đa có thể đặt cho task bao gồm cả ký tự NULL, nếu số ký tự vượt quá giá trị này thì chuỗi tên sẽ bị cắt bớt.
Mỗi task sẽ chiếm một phần bộ nhớ dùng để lưu thông tin về trạng thái làm việc và ngữ cảnh (context).
1632665493395.png
Khi các task được tạo ra, kernel sẽ cấp cho nó một vùng nhớ trong Heap gồm TCB và Task Stack. Heap có độ lớn định nghĩa bởi macro configTOTAL_HEAP_SIZE. Đối số usStackDepth của hàm xTaskCreate() đại diện cho độ lớn của Stack được cấp phát tính theo longword. usStackDepth = 100 thì (100 * 4) byte sẽ được cấp phát cho stack.
1632665507413.png
Tương tự khi create thêm Task, Semaphore, Queue kernel sẽ cấp các vùng nhớ tương ứng (TCB, SCB, QCB,...) trong heap. Cách thức cấp phát bộ nhớ tùy vào các file 'heap_x.c' trong thư viện RTOS.
1632665521867.png

1632665528956.png
Tìm hiểu thêm về cách thức hoạt động của cấp phát bộ nhớ động: Basics of Dynamic Memory Allocation - YouTube

2. Task priority
Khi có 2 task có cùng mức ưu tiên, scheduler sẽ cho chúng chạy luân phiên nhau như hình:
1632665590321.png
C:
void StartTask01(void const * argument)
{
    osPriority priority = osThreadGetPriority(myTask01Handle);
    char str[25];
    /* Infinite loop */
    for(;;)
    {
        sprintf(str, "Task 1,priority:%d\r\n", priority);
        HAL_UART_Transmit(&huart2, str, strlen(str), 100);
        for (int i = 0; i < 100000; i++);
    }
}

void StartTask02(void const * argument)
{
    osPriority priority = osThreadGetPriority(myTask02Handle);
    char str[25];
    /* Infinite loop */
    for(;;)
    {
        sprintf(str, "Task 2,priority:%d\r\n", priority);
        HAL_UART_Transmit(&huart2, str, strlen(str), 100);
        for (int i = 0; i < 100000; i++);
    }
}
Đối số uxPriority của hàm xTaskCreate() đặt cho task một mức ưu tiên nhất định. Mức ưu tiên này có thể được thay đổi sau khi bắt đầu scheduler bằng hàm vTaskPrioritySet().
Số mức ưu tiên được define bằng macro configMAX_PRIORITIES. Vậy ta có mức ưu tiên thấp nhất là 0 và mức ưu tiên cao nhất là (configMAX_PRIORITIES-1).
Scheduler sẽ cho các task có mức ưu tiên cao (và có thể chạy) vào Running State. Khi có nhiều task có cùng mức ưu tiên thì scheduler sẽ cho chúng chạy luân phiên nhau. Mỗi task sẽ chạy trong một khoảng ‘time slice’.
Vào cuối mỗi time slice, sẽ có một tick interrupt nhằm để cho scheduler chọn task nào được chạy trong time slice tiếp theo. Tần số tick interrupt quyết định thời gian của time slice, có thể config bằng macro configTICK_RATE_HZ. configTICK_RATE_HZ=100 tương ứng với time slice là 10ms.
vTaskPriorityGet()vTaskPrioritySet() dùng để lấy và thay đổi mức ưu tiên của task. Chỉ có thể dùng 2 hàm này khi macro INCLUDE_vTaskPrioritySet=1.
Khi 2 task có mức ưu tiên khác nhau. Task có mức ưu tiên cao hơn sẽ luôn chạy, task có mức ưu tiên thấp hơn không được chạy cho đến khi các task có mức ưu tiên cao hơn bị block hoặc suspend.
1632673422127.png
C:
void StartTask02(void const * argument)
{
    osPriority priority = osThreadGetPriority(myTask02Handle);
    char str[25];
    priority = priority + 1;
    /* Infinite loop */
    for(;;)
    {
        osThreadSetPriority(myTask02Handle, priority);
        sprintf(str, "Task 2,priority:%d\r\n", priority);
        HAL_UART_Transmit(&huart2, str, strlen(str), 100);
        for (int i = 0; i < 100000; i++);
    }
}
3. Delete Task
Để xóa một task, dùng hàm vTaskDelete(). Tuy nhiên vTaskDelete() sẽ không xóa đi vùng nhớ trong heap của task mà chỉ đánh dấu là task đó đã được xóa.

4. Task Delay


5. Task States

Nhắc lại: Task sẽ có 2 trạng thái chính là Running và Not Running. Trong đó trạng thái Not Running bao gồm:
  • Block state: ta đã biết task khi gọi hàm delay sẽ bị block, đây gọi là Time event. Ngoài ra task còn có thể bị block khi chờ một sự kiện từ queue, semaphore, mutex, đây gọi là Synchronization events.
  • Suspended State: task có thể vào ra trạng thái này thông qua 2 hàm vTaskSuspend()vTaskResume(). Khi ở trạng thái suspend, scheduler sẽ bỏ qua task. Ít dùng nên chả có gì để nói thêm :v
  • Ready State: task không bị blocked và suspend thì sẽ ở trạng thái này, chờ scheduler xếp lịch cho chạy.
Ta còn có thể chia task ra 2 loại:
  • Non-blocking: là task không bao giờ gọi các hàm để đưa nó vào trạng thái blocked. Các task này luôn ở trạng thái ready hoặc running
  • Blocking: là task dùng các hàm để đưa nó vào trạng thái blocked.

6. Idle Task
Idle task được tạo ra bởi scheduler trong hàm vTaskStartScheduler() để đảm bảo trong 1 thời điểm luôn có 1 task đang chạy. Có nghĩa là khi các task khác vào trạng thái blocked hoặc suspended, idle task sẽ chạy.

Đặc điểm:
  • Idle task sẽ có mức ưu tiên thấp nhất (0).
  • Idle task có nhiệm vụ giải phóng bộ nhớ khi có 1 task bị xóa.
1633400814370.png
Trong prvIdleTask() sẽ gọi prvCheckTasksWaitingTermination() -> prvDeleteTCB( pxTCB ) để xóa TCB.
1633401184071.png

Idle task hook là 1 hàm được idle task gọi trong mỗi vòng lặp của nó.
Idle task hook thường dùng để:
  • Thực hiện các xử lý có ưu tiên thấp
  • Đo thời gian rảnh của hệ thống
  • Đưa CPU vào trạng thái công suất thấp
1633401514259.png
Để sử dụng vApplicationIdleHook() thì phải để configUSE_IDLE_HOOK = 1.
Nếu hàm vTaskDelete() được gọi, idle task hook phải quay về idle task sau 1 khoảng thời gian (có nghĩa là ta không dùng lặp while (1) trong idle task hook.
Idle task hook không bao giờ gọi hàm làm cho nó đi vào trạng thái blocked và suspended.

VD sử dụng idle task hook để đưa MCU vào sleep mode:
C:
void vApplicationIdleHook( void )
{
    //Assume __HAL_RCC_PWR_CLK_ENABLE() is called elsewhere
    HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFE);
}
 
Last edited:

ntaquan

Gà con
Phần 4: Scheduler
1. Luật scheduling

Người dùng có thể thay đổi luật scheduling thông qua 2 macro configUSE_PREEMPTION và configUSE_TIME_SLICING
1633440858848.png
Câu hỏi: Giải thích 3 luật scheduling trên?

2. Kiến trúc của Scheduler
Code của scheduler bao gồm 2 phần: Generic (task.c) và Architecture (port.c)
Phần Generic bắt đầu từ hàm xTaskStartScheduler(), hàm này sau khi tạo idle task và timer service task sẽ gọi xPortStartScheduler().
Phần Architecture bắt đầu từ hàm xPortStartScheduler(), hàm này sẽ:
  • Thiết lập systick timer để tạo tần số theo macro configTICK_RATE_HZ
  • Thiếp lập mức ưu tiên của PendSV và ngắt systick
  • Chạy task đầu tiên, bằng lệnh SVC
1633440929457.png
Vậy sẽ có 3 hàm interrupt handler chịu trách nhiệm scheduling.
  • vPortSVCHandler() gọi bởi lệnh SVC dùng để chạy task đầu tiên
  • xPortPendSVHandler() thực hiện context switching nhằm chuyển qua lại giữa các task
  • xPortSystickHandler() quản lý systick và tạo ngắt systick

3. RTOS Tick


4. TCB và Stack

TCB (Task control block) chứa thông tin của 1 task (tên task, mức ưu tiên, số thứ tự, con trỏ chỉ tới stack, ...).
1633711617973.png
Mỗi task sẽ có 1 stack tương ứng, cần phân biệt với kernel stack (bắt đầu từ vị trí SRAM_END = 0x20008000). Vậy sẽ có 2 loại stack trong 1 ứng dụng RTOS là task stack (điều khiển bởi PSP) và kernel stack (điều khiển bởi MSP).
1633710795157.png

5. Context switching
Ngữ cảnh (context) tất cả dữ liệu cần để định nghĩa trạng thái hiện tại của task. Gồm các thanh ghi của processor ngoài ra còn có thể có thông tin của các ngoại vi.
1633441039940.png
Nếu luật scheduling là Preemptive, cứ sau mỗi một RTOS tick, scheduler sẽ so sánh mức ưu tiên của task đang chạy với từng task trong 1 list các task ready. Nếu task trong list đó có mức ưu tiên lớn hơn task hiện tại thì context switch sẽ diễn ra.
1633441383846.png
Nếu luật scheduling là Cooperative, có thể gọi context switching thông qua hàm taskYEILD()

1634829212149.png
Thủ tục switch out gồm có:
  1. Các thanh ghi R0-R3, R12, LR, PC, xPSR lưu vào stack trước khi ngắt Systick xảy ra.​
  2. Nếu cần context switching, sẽ gọi PendSV Handler.​
  3. Các thanh ghi R4-R11 lưu vào stack.​
  4. Lưu top of stack (PSP) vào TCB​
  5. Chọn task tiếp theo để chạy​
1634830930834.png
Thủ tục switch in gồm có:
  1. Lấy top of stack từ TCB lưu vào PSP​
  2. Pop các thanh ghi R4-R11 khỏi stack​
  3. Thoát khỏi Interrupt, pop các thanh ghi R0-R3, R12, LR, PC, xPSR khỏi stack​

Context switching cho first task?
1634829106293.png
Có thể nhìn vào hình và giải thích tương tự, lười viết qué :5cool_sweat:

1634829285254.png
Thật ra quy trình trên khá giống với lúc gọi và thoát khỏi một hàm của một chương trình bình thường (khi gọi hàm, context của chương trình chính được lưu vào stack và PC trỏ đến địa chỉ của hàm và khi thoát khỏi hàm thì các thanh ghi lưu trong stack được lấy ra lại), điểm khác nhau của RTOS ở đây là mỗi task sẽ có một stack riêng và scheduler sẽ quyết định PSP trỏ đến stack của task nào trong chương trình, từ đó switch in mới lấy PC để chạy task đó.

Câu hỏi: MSP dùng ở đâu?
Lưu context của hàm main() và các hàm xử lý ngắt.

6. Task priority list
1633712505849.png
Làm thế nào để scheduler có thể chọn được task tiếp theo để chạy?? Câu trả lời là các task ở trạng thái ready sẽ được lập thành một array với index là mức ưu tiên, mỗi phần tử sẽ là một linked list, mỗi node trong linked list sẽ chỉ tới TCB của task. Scheduler sẽ đi từ pxReadyTasksLists[uxTopReadyPriority] xuống để tìm task có mức ưu tiên cao nhất (!empty list), nếu tìm thấy pxCurrentTCB sẽ chỉ tới task đó và gọi hàm vTaskSwitchContext().
Phần code trên được gọi trong PendSV_Handler().
 
Last edited:

ntaquan

Gà con
Phần 5: Quản lý bộ nhớ
1. Phân vùng nhớ

1635363200987.png
Câu hỏi: Stack overflow là gì? Nguyên nhân dẫn đến stack overflow? Cách khắc phục?
Nguyên nhân:
  • Hàm gọi hàm, đệ quy
  • Đối số của hàm quá nhiều, quá lớn
  • Khai báo quá nhiều biến cục bộ hàm
Cách khắc phục:
  • Khử đệ quy​
  • Truyền tham trị​
  • Sử dụng inline function​
  • Sử dụng chế độ optimization​

2. Cấp phát bộ nhớ trong RTOS
Khi tạo task, TCB và Stack của task sẽ được lưu ở đâu? Có 2 phương án:
  • Nếu sử dụng cấp phát động (calloc, malloc), lưu trong Heap​
  • Nếu sử dụng cấp phát tĩnh, lưu trong .data section​
Câu hỏi: Tại sao người ta thường dùng cấp phát động cho RTOS?
Bởi vì các ứng dụng RTOS thường rất lớn, mà dung lượng của vi điều khiển có hạn. Với cấp phát động, ta có thể giải phỏng bộ nhớ khi không sử dụng bằng cách gọi hàm free().
Câu hỏi: Tại sao những ứng dụng thường lại không có cấp phát động?
Bởi vì malloc và free tốn nhiều chu kỳ xử lý và nếu không được quản lý sẽ dễ gây ra phân mảnh bộ nhớ.

3. Các phương thức cấp phát bộ nhớ sử dụng trong FreeRTOS
Code của cấp phát bộ nhớ thuộc phần Port, ở trong file heap_1.c tới heap_5.c, thư mục portable/MemMang.

Heap 1: Không bao gồm hàm vPortFree() được sử dụng trong các ứng dụng không cần giải phóng bộ nhớ. Vậy nên không cần lo tới vấn đề phân mảnh.
Heap 2: Cho phép giải phóng bộ nhớ, kèm với một thuật toán best-fit để tái sử dụng lại vùng nhớ đã được giải phóng.
1637256735267.png
Heap 3: Sử dụng hàm malloc()free() của thư viện chuẩn. Trong lúc đó scheduler sẽ bị tạm ngưng.
1637256892090.png
Heap 4: Cũng có thuật toán best fit, nhưng nó tiếp tục chia nhỏ vùng nhớ đó ra nếu còn chỗ trống, nhằm tránh phân mảnh bộ nhớ.
1637257068070.png
Heap 5: khó hiểu quá, chưa viết được.
 
  • Like
Reactions: Ref

ntaquan

Gà con
Phần 6: Quản lý tài nguyên
1. Câu chuyện bug khi chia sẻ tài nguyên

Race condition là một vấn đề phổ biến xảy ra khi nhiều task truy cập đồng thời cùng một tài nguyên được chia sẻ mà không có được đồng bộ thích hợp.

Giả sử rằng có hai task T1 và T2, mỗi task muốn tăng giá trị của một số nguyên toàn cục lên một. Lý tưởng, trình tự các hoạt động sau đây sẽ diễn ra:
  • Đặt i = 0;
  • T1 đọc giá trị của i từ bộ nhớ và lưu vào thanh ghi : 0
  • T1 tăng giá trị của i trong thanh ghi: (register) + 1 = 1
  • T1 lưu giá trị của thanh ghi vào lại bộ nhớ : 1
  • T2 đọc giá trị của i từ bộ nhớ và lưu vào thanh ghi : 1
  • T2 tăng giá trị của i trong thanh ghi: (register) + 1 = 2
  • T2 lưu giá trị của thanh ghi vào lại bộ nhớ : 2
  • Vậy i = 2
Trong trường hợp của race condition:
  • Đặt i = 0;
  • T1 đọc giá trị của i từ bộ nhớ và lưu vào thanh ghi : 0
  • T2 đọc giá trị của i từ bộ nhớ và lưu vào thanh ghi : 0
  • T1 tăng giá trị của i trong thanh ghi: (register) + 1 = 1
  • T2 tăng giá trị của i trong thanh ghi: (register) + 1 = 1
  • T1 lưu giá trị của thanh ghi vào lại bộ nhớ : 1
  • T2 lưu giá trị của thanh ghi vào lại bộ nhớ : 1
  • Vậy i = 1 thay vì 2
Để tránh race condition xảy ra trong FreeRTOS, ta có thể sử dụng các cơ chế đồng bộ hóa như mutex, semaphore hoặc critical section. Các cơ chế này đảm bảo rằng chỉ một task tại một thời điểm có thể truy cập vào tài nguyên được chia sẻ, ngăn race condition xảy ra.

2. Critical section
Critical section là một đoạn code sẽ được thực thi mà không bị gián đoạn bởi các task hoặc ngắt nào khác. Nó thường được sử dụng để bảo vệ các tài nguyên được chia sẻ khỏi sự truy cập đồng thời của nhiều task.
Critical section có thể được thực hiện theo nhiều cách, nhưng phương pháp phổ biến nhất là sử dụng cơ chế vô hiệu hóa tạm thời các ngắt hoặc task switching để đảm bảo quyền truy cập độc quyền vào tài nguyên được chia sẻ.
C:
void critical_section(void) {
    portENTER_CRITICAL(); // disable interrupts
    // code for critical section goes here
    portEXIT_CRITICAL(); // enable interrupts
}
Trong ví dụ này, portENTER_CRITICAL() vô hiệu hóa các ngắt và portEXIT_CRITICAL() kích hoạt lại các ngắt, tạo một critical section nơi không có ngắt nào có thể xảy ra. Tuy nhiên, điều quan trọng cần lưu ý là phương pháp này có nhược điểm:
  • Vô hiệu hóa ngắt có thể có tác dụng phụ ngoài ý muốn đối với hoạt động của hệ thống, vì một số thiết bị ngoại vi phụ thuộc vào ngắt.
  • Vô hiệu hóa ngắt trong một khoảng thời gian dài có thể khiến hệ thống bỏ lỡ các sự kiện quan trọng.
Nói chung cách này không khuyến khích mà thay vào đó nên sử dụng các cơ chế đồng bộ hóa khác do FreeRTOS cung cấp, chẳng hạn như mutexes, semaphores hoặc queues, cho hiệu quả.

3. Queue là gì
Là một cấu trúc dữ liệu được sử dụng để lưu trữ một chuỗi các thành phần dữ liệu, nó hoạt động dựa trên cơ chế FIFO (first in fisrt out), nghĩa là phần tử nào và trước thì phần tử đó sẽ ra trước. Khi đưa một phần tử vào queue thì gọi là enqueue, khi lấy một phần tử khỏi queue gọi là dequeue. Kích thước mỗi phần tử được quy định khi queue được khởi tạo.
1637258071372.png

4. Truyền nhận dữ liệu giữa các task
Dữ liệu có thể được đưa vào queue bởi một task và loại bỏ khỏi queue bởi một task khác, cho phép các task giao tiếp với nhau mà không có rủi ro về race condition hoặc làm hỏng dữ liệu.

Lợi ích của việc sử dụng queue:
  • Blocking và Non-Blocking Modes cho phép các task đợi dữ liệu có sẵn hoặc tiếp tục thực thi nếu queue trống.
  • Thừa hưởng mức ưu tiên (Priority Inheritance) có nghĩa là task có mức ưu tiên thấp đang queue sẽ kế thừa độ ưu tiên của task có mức ưu tiên cao bị block trên cùng một queue. Điều này có thể cải thiện hiệu suất hệ thống.

Vấn đề: Chuyện gì sẽ xảy ra trong những trường hợp sau? Giả sử task A gửi dữ liệu liên tục cho task B:
  • A gửi khi Queue đầy
  • B nhận khi Queue rỗng
  • B đọc Queue trong lúc A đang ghi

5. Thao tác với Queue
Queue được khởi tạo sử dụng hàm xQueueCreate(). Để các task khác nhau có thể truy cập được queue, biến handle của queue phải được khai báo như là một biến toàn cục:

Sau khi được khởi tạo, queue có thể được xóa bằng hàm xQueueDelete() nếu nó không còn cần thiết. Sau khi dùng lệnh này, vùng nhớ của queue (trong heap) sẽ được giải phóng.
Để ghi dữ liệu vào queue, ta dùng hàm xQueueSend()

Ngoài hàm xQueueSend(), xQueueSendToBack(). Hai hàm này hoàn toàn giống nhau.
Khi muốn dữ liệu ghi vào được đọc ra trước (LIFO), thì dùng hàm xQueueSendToBack().

Để đọc dữ liệu ra khỏi queue, dùng hàm xQueueReceive() --> dữ liệu sau khi đọc được lấy ra khỏi queue, hoặc xQueuePeak() --> dữ liệu vẫn nằm trong queue.
 
Last edited:
  • Like
Reactions: Ref

ntaquan

Gà con
Phần 7: Quản lý tài nguyên (phần 2)
1. Binary semaphore

Binary semaphore có thể xem là 1 cờ, chỉ có 2 giá trị là 0 và 1, được truy cập bởi nhiều task.
Task signal sẽ bật semaphore lên 1, còn task wait sẽ chờ cho đến khi semaphore lên 1, nó sẽ xóa semaphore và thực hiện công việc nào đó.

2. Counting semaphore

3. Mutex

4. Đảo ngược mức ưu tiên (Priority inversion)

Đây là tình huống mà trong đó task có độ ưu tiên thấp chặn việc thực thi của task có mức độ ưu tiên cao. Điều này có thể xảy ra khi hai hoặc nhiều task chia sẻ tài nguyên và một task có mức độ ưu tiên cao hơn các task khác, nhưng nó không thể truy cập tài nguyên vì task có độ ưu tiên thấp hơn đang giữ nó.
1676666864331.png



5. Thừa hưởng mức ưu tiên (Priority Inheritance)

6. Deadlock

7. Gate keeper
 
  • Like
Reactions: Ref

ntaquan

Gà con
Phần 8: Quản lý ngắt
1. Ngắt sử dụng bởi RTOS

Ngắt là một chức năng của phần cứng vi điều khiển, dùng để báo tín hiệu khi có một sự kiện xảy ra.
nterrupt Service Routine (ISR) là một hàm của vi điều khiển gọi để xử lý khi ngắt xảy ra.
Interrupt Vector Table (IVT) là một phần của bộ nhớ lưu địa chỉ của các hàm ngắt.
1676567242734.png
Các vector có exception number từ 1-15 (với mức ưu tiên âm) được định nghĩa bới ARM, các vector có exception number trên 15 được triển khai bởi các nhà cung cấp (TI, ST, ...). Đối với mức ưu tiên: mức ưu tiên càng thấp thì độ ưu tiên càng cao, NGƯỢC với mức ưu tiên của task.

Phần Architecture của RTOS (port.c) được viết bằng C và assembly, hỗ trợ tương tác với ngắt của phần cứng. Khi sử dụng các ứng dụng chạy FreeRTOS, 3 loại ngắt SysTick, PendSV (Pendable SerVice), và SVCall (SuperVisor Call) phải được kết nối với port của FreeRTOS. Theo thư viện CMSIS, 3 hàm ngắt này có tên lần lượt là SysTick_Handler, PendSV_Handler, SVC_Handler.

2. Gọi hàm của RTOS trong ngắt
 
  • Like
Reactions: Ref
Top