- Semaphore là gì?
- Làm thế nào để sử dụng Semaphore trong FreeRTOS?
- Giải thích mã Semaphore
- Sơ đồ mạch
- Mutex là gì?
- Làm thế nào để sử dụng Mutex trong FreeRTOS?
- Giải thích mã Mutex
Trong các hướng dẫn trước, chúng ta đã trình bày những kiến thức cơ bản về FreeRTOS với Arduino và đối tượng hạt nhân Queue trong FreeRTOS Arduino. Bây giờ, trong hướng dẫn FreeRTOS thứ ba này, chúng ta sẽ tìm hiểu thêm về FreeRTOS và các API nâng cao của nó, có thể giúp bạn hiểu sâu hơn về nền tảng đa tác vụ.
Semaphore và Mutex (Loại trừ lẫn nhau) là các đối tượng hạt nhân được sử dụng để đồng bộ hóa, quản lý tài nguyên và bảo vệ tài nguyên khỏi tham nhũng. Trong nửa đầu của hướng dẫn này, chúng ta sẽ thấy ý tưởng đằng sau Semaphore, cách sử dụng và ở đâu. Trong nửa sau, chúng tôi sẽ tiếp tục với Mutex.
Semaphore là gì?
Trong các hướng dẫn trước đây, chúng ta đã thảo luận về các mức độ ưu tiên của nhiệm vụ và cũng biết rằng nhiệm vụ có mức độ ưu tiên cao hơn ưu tiên nhiệm vụ có mức độ ưu tiên thấp hơn, do đó, trong khi thực hiện tác vụ có mức độ ưu tiên cao, có thể xảy ra lỗi dữ liệu trong tác vụ có mức độ ưu tiên thấp hơn vẫn chưa được thực thi và dữ liệu liên tục đến với tác vụ này từ một bộ cảm biến gây mất dữ liệu và hoạt động sai của toàn bộ ứng dụng.
Vì vậy, cần phải bảo vệ tài nguyên khỏi mất mát dữ liệu và ở đây Semaphore đóng một vai trò quan trọng.
Semaphore là một cơ chế báo hiệu trong đó một tác vụ ở trạng thái chờ được báo hiệu bởi một tác vụ khác để thực hiện. Nói cách khác, khi một task1 hoàn thành công việc của nó, nó sẽ hiển thị một lá cờ hoặc tăng một lá cờ lên 1 và sau đó lá cờ này được nhận bởi một tác vụ khác (task2) cho thấy rằng nó có thể thực hiện công việc của mình ngay bây giờ. Khi task2 hoàn thành công việc của nó thì cờ sẽ giảm đi 1.
Vì vậy, về cơ bản, nó là cơ chế “Cho” và “Nhận” và semaphore là một biến số nguyên được sử dụng để đồng bộ hóa quyền truy cập vào tài nguyên.
Các loại Semaphore trong FreeRTOS:
Semaphore có hai loại.
- Binary Semaphore
- Đếm Semaphore
1. Binary Semaphore: Nó có hai giá trị nguyên 0 và 1. Nó hơi giống với Queue có độ dài 1. Ví dụ, chúng ta có hai task, task1 và task2. Task1 gửi dữ liệu đến task2 để task2 liên tục kiểm tra mục hàng đợi nếu có 1, sau đó nó có thể đọc dữ liệu khác mà nó phải đợi cho đến khi nó trở thành 1. Sau khi lấy dữ liệu, task2 giảm hàng đợi và biến nó thành 0. Điều đó có nghĩa là task1 lại có thể gửi dữ liệu đến task2.
Từ ví dụ trên, có thể nói rằng semaphore nhị phân được sử dụng để đồng bộ hóa giữa các tác vụ hoặc giữa các tác vụ và ngắt.
2. Semaphore đếm: Nó có các giá trị lớn hơn 0 và có thể được coi là hàng đợi có độ dài hơn 1. Semaphore này được sử dụng để đếm các sự kiện. Trong trường hợp sử dụng này, một trình xử lý sự kiện sẽ 'cung cấp' một semaphore mỗi khi một sự kiện xảy ra (tăng giá trị đếm semaphore) và một tác vụ xử lý sẽ 'lấy' một semaphore mỗi khi nó xử lý một sự kiện (giảm giá trị đếm semaphore).
Do đó, giá trị đếm là sự khác biệt giữa số lượng sự kiện đã xảy ra và số lượng đã được xử lý.
Bây giờ, hãy xem cách sử dụng Semaphore trong mã FreeRTOS của chúng tôi.
Làm thế nào để sử dụng Semaphore trong FreeRTOS?
FreeRTOS hỗ trợ các API khác nhau để tạo semaphore, lấy semaphore và đưa ra semaphore.
Bây giờ, có thể có hai loại API cho cùng một đối tượng nhân. Nếu chúng ta phải cung cấp semaphore từ ISR, thì API semaphore bình thường không thể được sử dụng. Bạn nên sử dụng các API được bảo vệ ngắt.
Trong hướng dẫn này, chúng tôi sẽ sử dụng semaphore nhị phân vì nó dễ hiểu và dễ thực hiện. Vì chức năng ngắt được sử dụng ở đây, bạn cần sử dụng các API được bảo vệ ngắt trong hàm ISR. Khi chúng ta đang nói đồng bộ hóa một tác vụ với một ngắt, điều đó có nghĩa là đặt tác vụ vào trạng thái Đang chạy ngay sau ISR.
Tạo Semaphore:
Để sử dụng bất kỳ đối tượng kernel nào, trước tiên chúng ta phải tạo nó. Để tạo semaphore nhị phân, hãy sử dụng vSemaphoreCreateBinary ().
API này không nhận bất kỳ tham số nào và trả về một biến kiểu SemaphoreHandle_t. Tên biến toàn cục sema_v được tạo để lưu trữ semaphore.
SemaphoreHandle_t sema_v; sema_v = xSemaphoreCreateBinary ();
Đưa ra một semaphore:
Để cung cấp một semaphore, có hai phiên bản - một phiên bản dành cho ngắt và một phiên bản khác dành cho tác vụ bình thường.
- xSemaphoreGive (): API này chỉ nhận một đối số là tên biến của semaphore như sema_v như đã cho ở trên trong khi tạo một semaphore. Nó có thể được gọi từ bất kỳ tác vụ bình thường nào mà bạn muốn đồng bộ hóa.
- xSemaphoreGiveFromISR (): Đây là phiên bản API được bảo vệ ngắt của xSemaphoreGive (). Khi chúng ta cần đồng bộ hóa ISR và tác vụ bình thường, thì xSemaphoreGiveFromISR () nên được sử dụng từ hàm ISR.
Tham gia một semaphore:
Để lấy một semaphore, hãy sử dụng hàm API xSemaphoreTake (). API này có hai tham số.
xSemaphoreTake (SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);
xSemaphore: Tên của semaphore được sử dụng trong trường hợp sema_v của chúng ta.
xTicksToWait: Đây là khoảng thời gian tối đa mà tác vụ sẽ đợi ở trạng thái Bị chặn để semaphore khả dụng. Trong dự án của chúng tôi, chúng tôi sẽ đặt xTicksToWait thành portMAX_DELAY để thực hiện tác vụ_1 chờ vô thời hạn ở trạng thái Bị chặn cho đến khi sema_v khả dụng.
Bây giờ, hãy sử dụng các API này và viết mã để thực hiện một số tác vụ.
Ở đây có một nút nhấn và hai đèn LED được giao diện. Nút nhấn sẽ hoạt động như một nút ngắt được gắn vào chân 2 của Arduino Uno. Khi nhấn nút này, một ngắt sẽ được tạo ra và một đèn LED được kết nối với chân 8 sẽ được BẬT và khi bạn nhấn lại, nó sẽ TẮT.
Vì vậy, khi nhấn nút xSemaphoreGiveFromISR () sẽ được gọi từ hàm ISR và hàm xSemaphoreTake () sẽ được gọi từ hàm TaskLED.
Để làm cho hệ thống trông đa nhiệm, hãy kết nối các đèn LED khác với chân 7 sẽ ở trạng thái luôn nhấp nháy.
Giải thích mã Semaphore
Hãy bắt đầu viết mã bằng cách mở Arduino IDE
1. Đầu tiên, hãy bao gồm tệp tiêu đề Arduino_FreeRTOS.h . Bây giờ, nếu bất kỳ đối tượng hạt nhân nào được sử dụng như semaphore hàng đợi thì một tệp tiêu đề cũng phải được bao gồm cho nó.
#include #include
2. Khai báo một biến kiểu SemaphoreHandle_t để lưu các giá trị của semaphore.
SemaphoreHandle_truptSemaphore;
3. Trong void setup (), hãy tạo hai tác vụ (TaskLED và TaskBlink) bằng cách sử dụng API xTaskCreate () và sau đó tạo một semaphore bằng cách sử dụng xSemaphoreCreateBinary (). Hãy tạo một tác vụ có mức độ ưu tiên bằng nhau và sau đó hãy thử chơi với số này. Ngoài ra, Định cấu hình chân 2 làm đầu vào và kích hoạt điện trở kéo lên bên trong và gắn chân ngắt. Cuối cùng, khởi động trình lên lịch như hình bên dưới.
void setup () { pinMode (2, INPUT_PULLUP); xTaskCreate (TaskLed, "Led", 128, NULL, 0, NULL); xTaskCreate (TaskBlink, "LedBlink", 128, NULL, 0, NULL); ruptSemaphore = xSemaphoreCreateBinary (); if ( ruptSemaphore ! = NULL) { attachmentInterrupt (digitalPinToInterrupt (2), debounceInterrupt, LOW); } }
4. Bây giờ, thực hiện chức năng ISR. Tạo một hàm và đặt tên nó giống như đối số thứ hai của hàm đính kèm () . Để làm cho ngắt hoạt động bình thường, bạn cần loại bỏ vấn đề gỡ lỗi của nút nhấn bằng cách sử dụng chức năng mili hoặc micro và bằng cách điều chỉnh thời gian gỡ lỗi. Từ hàm này, hãy gọi hàm ngắtHandler () như hình bên dưới.
long debouncing_time = 150; dễ bay hơi không dấu dài last_micros; void debounceInterrupt () { if ((long) (micros () - last_micros)> = debouncing_time * 1000) {ruptHandler (); last_micros = micros (); } }
Trong interruptHandler () chức năng, gọi xSemaphoreGiveFromISR () API.
void ngắtHandler () { xSemaphoreGiveFromISR (ngắtSemaphore, NULL); }
Hàm này sẽ cung cấp một semaphore cho TaskLed để BẬT đèn LED.
5. Tạo một hàm TaskLed và bên trong vòng lặp while, gọi API xSemaphoreTake () và kiểm tra xem semaphore có được thực hiện thành công hay không. Nếu nó bằng pdPASS (tức là 1) thì làm cho đèn LED chuyển đổi như hình dưới đây.
void TaskLed (void * pvParameters) { (void) pvParameters; pinMode (8, OUTPUT); while (1) { if (xSemaphoreTake (ruptSemaphore, portMAX_DELAY) == pdPASS) { digitalWrite (8,! digitalRead (8)); } } }
6. Đồng thời, tạo chức năng nháy đèn LED khác được kết nối với chân số 7.
void TaskLed1 (void * pvParameters) { (void) pvParameters; pinMode (7, OUTPUT); while (1) { digitalWrite (7, HIGH); vTaskDelay (200 / portTICK_PERIOD_MS); digitalWrite (7, LOW); vTaskDelay (200 / portTICK_PERIOD_MS); } }
7. Hàm void loop sẽ vẫn trống. Đừng quên nó.
void loop () {}
Vậy là xong, bạn có thể tìm thấy mã hoàn chỉnh ở cuối hướng dẫn này. Bây giờ, tải lên mã này và kết nối các đèn LED và nút nhấn với Arduino UNO theo sơ đồ mạch.
Sơ đồ mạch
Sau khi tải mã lên, bạn sẽ thấy một đèn LED nhấp nháy sau 200ms và khi nhấn nút, ngay lập tức đèn LED thứ hai sẽ phát sáng như trong video ở cuối.
Bằng cách này, các semaphores có thể được sử dụng trong FreeRTOS với Arduino nơi nó cần truyền dữ liệu từ tác vụ này sang tác vụ khác mà không bị mất mát.
Bây giờ, chúng ta hãy xem Mutex là gì và cách sử dụng FreeRTOS.
Mutex là gì?
Như đã giải thích ở trên semaphore là một cơ chế báo hiệu, tương tự, Mutex là một cơ chế khóa không giống như semaphore có các chức năng riêng biệt để tăng và giảm nhưng trong Mutex, hàm tự nhận và cho. Đó là một kỹ thuật để tránh tham nhũng của các tài nguyên được chia sẻ.
Để bảo vệ tài nguyên được chia sẻ, người ta gán thẻ mã thông báo (mutex) cho tài nguyên. Ai có thẻ này có thể truy cập tài nguyên kia. Những người khác nên đợi cho đến khi thẻ trả lại. Bằng cách này, chỉ một tài nguyên có thể truy cập nhiệm vụ và những tài nguyên khác chờ cơ hội của họ.
Hãy hiểu Mutex trong FreeRTOS với sự trợ giúp của một ví dụ.
Ở đây chúng ta có ba nhiệm vụ, một để in dữ liệu trên LCD, thứ hai để gửi dữ liệu LDR đến tác vụ LCD và tác vụ cuối cùng để gửi dữ liệu Nhiệt độ trên LCD. Vì vậy, ở đây hai tác vụ đang chia sẻ cùng một tài nguyên tức là LCD. Nếu tác vụ LDR và tác vụ nhiệt độ gửi dữ liệu đồng thời thì một trong các dữ liệu có thể bị hỏng hoặc mất.
Vì vậy, để bảo vệ việc mất dữ liệu, chúng ta cần khóa tài nguyên LCD cho task1 cho đến khi nó hoàn thành nhiệm vụ hiển thị. Sau đó tác vụ LCD sẽ mở khóa và khi đó task2 có thể thực hiện công việc của mình.
Bạn có thể quan sát hoạt động của Mutex và semaphores trong sơ đồ dưới đây.
Làm thế nào để sử dụng Mutex trong FreeRTOS?
Mutex cũng được sử dụng theo cách tương tự như semaphores. Đầu tiên, tạo nó, sau đó cung cấp và sử dụng các API tương ứng.
Tạo Mutex:
Để tạo Mutex, hãy sử dụng API xSemaphoreCreateMutex () . Như tên gọi của nó cho thấy Mutex là một loại semaphore nhị phân. Chúng được sử dụng trong các bối cảnh và mục đích khác nhau. Semaphore nhị phân là để đồng bộ hóa các tác vụ trong khi Mutex được sử dụng để bảo vệ tài nguyên được chia sẻ.
API này không nhận bất kỳ đối số nào và trả về một biến kiểu SemaphoreHandle_t . Nếu mutex không thể được tạo, xSemaphoreCreateMutex () trả về NULL.
SemaphoreHandle_t mutex_v; mutex_v = xSemaphoreCreateMutex ();
Chụp Mutex:
Khi một nhiệm vụ muốn truy cập một tài nguyên, nó sẽ sử dụng Mutex bằng cách sử dụng API xSemaphoreTake () . Nó giống như một semaphore nhị phân. Nó cũng có hai tham số.
xSemaphore: Tên của Mutex được sử dụng trong trường hợp của chúng tôi là mutex_v .
xTicksToWait: Đây là khoảng thời gian tối đa mà tác vụ sẽ đợi ở trạng thái Bị chặn để Mutex khả dụng. Trong dự án của chúng tôi, chúng tôi sẽ đặt xTicksToWait thành portMAX_DELAY để thực hiện tác vụ_1 chờ vô thời hạn ở trạng thái Bị chặn cho đến khi có mutex_v .
Đưa ra một Mutex:
Sau khi truy cập tài nguyên được chia sẻ, tác vụ sẽ trả về Mutex để các tác vụ khác có thể truy cập nó. API xSemaphoreGive () được sử dụng để trả lại Mutex.
Hàm xSemaphoreGive () chỉ nhận một đối số là Mutex được cung cấp trong trường hợp của chúng ta là mutex_v.
Sử dụng các API ở trên, Hãy triển khai Mutex trong mã FreeRTOS bằng Arduino IDE.
Giải thích mã Mutex
Ở đây mục tiêu của phần này là sử dụng Serial monitor làm tài nguyên được chia sẻ và hai tác vụ khác nhau để truy cập serial monitor để in một số thông báo.
1. Các tệp tiêu đề sẽ vẫn giống như một semaphore.
#include #include
2. Khai báo một biến kiểu SemaphoreHandle_t để lưu các giá trị của Mutex.
SemaphoreHandle_t mutex_v;
3. Trong void setup (), khởi tạo serial monitor với tốc độ truyền 9600 và tạo hai tác vụ (Task1 và Task2) bằng cách sử dụng API xTaskCreate () . Sau đó, tạo một Mutex bằng xSemaphoreCreateMutex (). Tạo một nhiệm vụ với mức độ ưu tiên như nhau và sau đó hãy thử chơi với số này.
void setup () { Serial.begin (9600); mutex_v = xSemaphoreCreateMutex (); if (mutex_v == NULL) { Serial.println ("Không thể tạo Mutex"); } xTaskCreate (Task1, "Task 1", 128, NULL, 1, NULL); xTaskCreate (Task2, "Task 2", 128, NULL, 1, NULL); }
4. Bây giờ, tạo các hàm tác vụ cho Task1 và Task2. Trong một thời gian vòng lặp về chức năng nhiệm vụ, trước khi in một tin nhắn trên màn hình nối tiếp, chúng ta phải mất một Mutex sử dụng xSemaphoreTake () sau đó in thông điệp và sau đó trở Mutex sử dụng xSemaphoreGive (). Sau đó, đưa ra một số chậm trễ.
void Task1 (void * pvParameters) { while (1) { xSemaphoreTake (mutex_v, portMAX_DELAY); Serial.println ("Xin chào từ Task1"); xSemaphoreGive (mutex_v); vTaskDelay (pdMS_TO_TICKS (1000)); } }
Tương tự, thực hiện chức năng Task2 với độ trễ 500ms.
5. Vòng lặp Void () sẽ vẫn trống.
Bây giờ, tải mã này lên Arduino UNO và mở màn hình nối tiếp.
Bạn sẽ thấy các thông báo đang in từ task1 và task2.
Để kiểm tra hoạt động của Mutex, chỉ cần nhận xét xSemaphoreGive (mutex_v); từ bất kỳ nhiệm vụ nào. Bạn có thể thấy rằng chương trình bị treo ở thông báo in cuối cùng .
Đây là cách Semaphore và Mutex có thể được triển khai trong FreeRTOS với Arduino. Để biết thêm thông tin về Semaphore và Mutex, bạn có thể truy cập tài liệu chính thức của FreeRTOS.
Mã và video hoàn chỉnh cho Semaphore và Mutes được cung cấp bên dưới.