🌱 Critical Section trong Hệ thống Nhúng: Ý Nghĩa và Ứng Dụng

🌱 Critical Section trong Hệ thống Nhúng: Ý Nghĩa và Ứng Dụng

    Trong lập trình nhúng, đặc biệt khi làm việc với hệ thống thời gian thực (RTOS), việc đảm bảo tính toàn vẹn dữ liệu và đồng bộ hóa giữa các tác vụ hoặc ngắt là vô cùng quan trọng. Chúng ta đã tìm hiểu về Semaphore, Mutex, và bài viết này sẽ khám phá một cơ chế quan trọng khác – critical section. Vậy critical section là gì? Nó hoạt động ra sao trong hệ thống nhúng? Và khác biệt thế nào với mutex? Hãy cùng tìm hiểu!

Critical Section

Đặt vấn đề: Khi nào cần dùng Critical Section?

    Critical Section trở nên cần thiết trong các tình huống mà việc truy cập đồng thời vào tài nguyên dùng chung có thể dẫn đến lỗi dữ liệu hoặc hành vi không mong muốn. Đặc biệt, nó rất quan trọng khi:

  • Nhiều tác vụ hoặc ngắt cùng truy cập một biến toàn cục hoặc bộ nhớ mà không có cơ chế bảo vệ.
  • Thực hiện các thao tác không nguyên tử (non-atomic), chẳng hạn như cơ chế Read-Modify-Write trên thanh ghi ngoại vi.

    Ví dụ cụ thể: Khi lập trình nhúng trên STM32, bạn cần bật một bit trong thanh ghi GPIO để điều khiển chân xuất tín hiệu. Cơ chế Read-Modify-Write yêu cầu đọc giá trị hiện tại của thanh ghi, thay đổi bit mong muốn, rồi ghi lại. Nếu không có Critical Section, một ngắt (ISR) có thể thay đổi thanh ghi giữa bước đọc và ghi, dẫn đến lỗi.

void setGPIO_Pin() {
    uint32_t temp = GPIOA->ODR;       // Read: Current register's value
    temp |= (1 << 5);                 // Modify: bit 5
    GPIOA->ODR = temp;                // Write: New register's value
}

    Nếu không dùng Critical Section, một ISR khác có thể thay đổi GPIOA➔ODR sau bước đọc nhưng trước bước ghi, làm mất dữ liệu. Sử dụng Critical Section như sau sẽ giải quyết vấn đề:

void setGPIO_Pin_Safe() {
    disableInterrupts();              // Bắt đầu Critical Section
    uint32_t temp = GPIOA->ODR;       // Read
    temp |= (1 << 5);                 // Modify
    GPIOA->ODR = temp;                // Write
    enableInterrupts();               // Kết thúc Critical Section
}

Critical Section là gì?

    Critical section là một đoạn mã trong chương trình mà tại đó chỉ một luồng thực thi (thread, task, hoặc interrupt) được phép chạy tại một thời điểm. Mục tiêu chính là bảo vệ tài nguyên dùng chung – như biến toàn cục, bộ nhớ, hoặc thanh ghi phần cứng – khỏi sự truy cập đồng thời gây xung đột.

    Trong hệ thống nhúng:

  • Critical section đảm bảo tính atomicity, tức đoạn mã được thực thi hoàn toàn mà không bị gián đoạn.
  • Nó thường xuất hiện khi nhiều tác vụ (tasks) hoặc ngắt (interrupts) cùng truy cập một tài nguyên.

    Ví dụ thực tế: Một hệ thống đo nhiệt độ dùng biến toàn cục để lưu giá trị cảm biến. Nếu hai tác vụ cùng cập nhật biến này mà không có bảo vệ, dữ liệu có thể bị sai lệch.


Cách Critical Section hoạt động trong Hệ thống Nhúng

    Trong lập trình nhúng, critical section thường được triển khai bằng cách tạm thời vô hiệu hóa cơ chế ngắt hoặc lập lịch, hoặc sử dụng các lệnh đặc biệt của phần cứng để đảm bảo tính nguyên tử.

    Bản chất chung của Critical Section

  1. Tắt Ngắt (Interrupt Disable):
    • Phương pháp phổ biến nhất là vô hiệu hóa toàn bộ ngắt trong khoảng thời gian ngắn.
    • Điều này ngăn chặn ngắt hoặc chuyển đổi ngữ cảnh (context switch) làm gián đoạn đoạn mã.
  2. Cơ chế khóa phần cứng – Atomic Instructions:
    • Một số vi điều khiển hỗ trợ các lệnh nguyên tử như Test-and-Set, Compare-and-Swap, hoặc LDREX/STREX để bảo vệ mà không cần tắt ngắt.
  3. Quản lý lồng ghép:
    • Hệ thống thường dùng bộ đếm (nesting counter) để hỗ trợ lồng ghép critical section mà không gây lỗi.
Critical Section example

Ví dụ với Atomic Instructions

    Trên ARM Cortex-M (như STM32), lệnh LDREX/STREX được dùng để tăng biến nguyên tử mà không tắt ngắt:

  1. volatile uint32_t counter = 0;
  2. void incrementAtomic() {
  3. uint32_t temp, status;
  4. do {
  5. __asm volatile (
  6. "ldrex %0, [%1] \n" // Read Counter
  7. "add %0, #1 \n" // Increase
  8. "strex %2, %0, [%1] \n" // Re-Write Value
  9. : "=&r" (temp), "=r" (&counter), "=&r" (status)
  10. : "1" (&counter)
  11. : "memory"
  12. );
  13. } while (status != 0);
  14. }

    Phương pháp này nhanh và không làm gián đoạn các ngắt khác trong hệ thống.

Ví dụ với FreeRTOS trên STM32

    Trong FreeRTOS, hàm taskENTER_CRITICAL() tắt ngắt bằng lệnh CPSID I:

  1. int sharedData = 0;
  2. void taskExample(void *params) {
  3. taskENTER_CRITICAL(); // Disable Interrupt
  4. sharedData++; // Critical Section
  5. taskEXIT_CRITICAL(); // Re-Enable Interrupt
  6. }

    FreeRTOS sử dụng biến uxCriticalNesting để quản lý lồng ghép.


Critical Section khác Mutex như thế nào?

    Cả critical sectionmutex đều bảo vệ tài nguyên dùng chung, nhưng chúng khác nhau về cách triển khai và ứng dụng.

So sánh Critical Section và Mutex

Tiêu chí Critical Section Mutex
Cách hoạt động Tắt ngắt hoặc khóa cấp thấp. Khóa tài nguyên qua hệ điều hành.
Phạm vi Task và ngắt (ISR). Chủ yếu giữa các task.
Hiệu suất Nhanh, nhẹ (vài chu kỳ CPU). Chậm hơn (phụ thuộc scheduler).
Thời gian sử dụng Ngắn (vài lệnh). Dài hoặc phức tạp.
Tính năng chờ Không chờ (non-blocking). Có thể chờ (blocking).

Minh họa sự khác biệt

  • Critical Section: Cập nhật nhanh một biến:
volatile int counter = 0;
void updateCounter() {
    disableInterrupts();  // Disable Interrupt
    counter++;
    enableInterrupts();   // Re-Enable Interrupt
}
  • Mutex: Xử lý lâu hơn trong FreeRTOS:
SemaphoreHandle_t mutex = xSemaphoreCreateMutex();
void processData() {
    xSemaphoreTake(mutex, portMAX_DELAY);  // Lock
    // Complex data processing
    xSemaphoreGive(mutex);                 // Un-Lock
}

    Khi nào nên dùng gì?

  • Critical Section: Thích hợp cho thao tác ngắn, như cập nhật biến hoặc truy cập thanh ghi.
  • Mutex: Dùng cho tác vụ dài, cần đồng bộ giữa các task mà không muốn tắt ngắt.

Ứng dụng của Critical Section trong Lập Trình Nhúng

Critical section có vai trò quan trọng trong nhiều kịch bản lập trình nhúng, từ hệ thống không RTOS đến RTOS phức tạp.

1. Bảo vệ tài nguyên phần cứng

    Khi cấu hình ngoại vi như UART hoặc I2C, bạn cần đảm bảo không có ngắt nào can thiệp. Ví dụ trên STM32:

void configClock() {
    disableInterrupts();
    RCC->CR |= RCC_CR_HSEON;  // Enable HSE
    enableInterrupts();
}

2. Đồng bộ hóa dữ liệu

    Trong hệ thống đa luồng, critical section ngăn xung đột khi ghi/đọc bộ đệm dữ liệu.

3. Tối ưu hiệu suất

    So với mutex, critical section nhanh hơn vì không cần cơ chế lập lịch, rất hữu ích trong hệ thống thời gian thực.


Lưu ý khi sử dụng Critical Section

  1. Thời gian ngắn: Giữ critical section ngắn để tránh ảnh hưởng tính real-time, đặc biệt khi tắt ngắt.
  2. Ngắt ưu tiên cao: Một số ngắt như NMI không bị chặn, cần xử lý riêng.
  3. Hiệu suất hệ thống: Clock CPU cao giảm thời gian thực thi, nhưng cần cân nhắc tác động của việc tắt ngắt.

Kết luận

    Critical section là một công cụ mạnh mẽ trong lập trình nhúng, giúp bảo vệ tài nguyên dùng chung nhanh chóng và hiệu quả. Dù khác biệt với mutex, cả hai đều có vai trò riêng tùy ngữ cảnh. Hiểu rõ cách hoạt động của critical section – từ tắt ngắt đến atomic instructions – sẽ giúp bạn xây dựng hệ thống nhúng đáng tin cậy hơn.

>>>>>> Follow ngay <<<<<<<

Để nhận được những bài học miễn phí mới nhất nhé 😊
Chúc các bạn học tập tốt 😊

Nguyễn Văn Nghĩa

Mình là một người thích học hỏi và chia sẻ các kiến thức về Nhúng IOT.

Đăng nhận xét

Mới hơn Cũ hơn
//