Xử lý nhiều tiến trình cùng một lúc trên Arduino – Xử lý bất đồng bộ – Có thể hay không?

Giới thiệu

Bạn đã từng gặp phải những vấn đề phức tạp khi viết chương trình Arduino với nhiều chức năng? Cảm giác rằng chúng không hoạt động ổn định khi kết hợp với nhau? Bài viết này sẽ giới thiệu với bạn một thư viện tuyệt vời của anh Đại Huỳnh để giải quyết những vấn đề này – xử lý nhiều tiến trình cùng một lúc trên Arduino.

Khởi nguồn

Từ khi mới học Arduino, bạn đã tự hỏi có cách nào để viết chương trình mà không sử dụng hàm delay? Từ những chia sẻ của cộng đồng Arduino, bạn đã hiểu được vấn đề và tìm thấy một bài viết chia sẻ về nó. Vậy bạn đã suy nghĩ từ đâu? Đó là vì khi sử dụng hàm delay, chương trình sẽ tạm ngừng cho đến khi hàm delay chạy xong. Nếu bạn muốn bật tắt nhiều LED với các chu kỳ khác nhau, bạn sẽ phải viết một đoạn chương trình dài như thế này:

int setup() {
  //Khởi tạo các LED
}

int loop() {
  digitalWrite(led1, HIGH);
  digitalWrite(led2, HIGH);
  delay(500);
  digitalWrite(led2, LOW);
  delay(500);
  digitalWrite(led1, LOW);
  digitalWrite(led2, HIGH);
  delay(500);
  digitalWrite(led2, LOW);
  delay(500);
}

Bạn có thể thấy rằng đoạn chương trình này lặp lại liên tục. Có cách nào rút gọn hơn không? Đó là tại sao tôi đã nghĩ đến việc sử dụng một biến state để lưu trạng thái của led1 và điều chỉnh nó.

Giờ nghĩ lại, tôi cảm thấy hồi đó rất “gà”.

Tuy nhiên, câu hỏi tiếp theo trong đầu tôi là, nếu các chu kỳ không đều (ví dụ một cái là 696ms, một cái là 133ms) thì sao? Nếu sử dụng delay, chương trình sẽ bị gián đoạn. Vì vậy, tôi sử dụng hàm millis để kiểm tra xem đã đến thời điểm thực hiện một đoạn chương trình hay chưa?

byte led1 = 5;
byte led2 = 6;
unsigned long time1 = 0;
unsigned long time2 = 0;

void setup() {
  pinMode(led1, OUTPUT);
  pinMode(led2, OUTPUT);
}

void loop() {
  if ( (unsigned long) (millis() - time1) > 696 ) {
    if ( digitalRead(led1) == LOW ) {
      digitalWrite(led1, HIGH);
    } else {
      digitalWrite(led1, LOW );
    }
    time1 = millis();
  }

  if ( (unsigned long) (millis() - time2) > 133 ) {
    if ( digitalRead(led2) == LOW ) {
      digitalWrite(led2, HIGH);
    } else {
      digitalWrite(led2, LOW );
    }
    time2 = millis();
  }
}

Nếu bạn để ý, bạn sẽ thấy rằng đoạn chương trình này có một số dòng lệnh giống nhau như:

if ( (unsigned long) (millis() - timeX) > <mốc thời gian> ) {
  //...
  timeX = millis();
}

Có cách nào rút gọn hơn không? Có! Anh Đại Huỳnh đã viết một bài viết về thư viện của mình để giải quyết vấn đề này.

Cải tiến

Bài viết của anh Đại Huỳnh đã giải quyết hầu hết các vấn đề mà tôi đã đề cập, nhưng bạn vẫn chưa sử dụng thư viện của anh một cách thành thạo. Vì vậy, tôi đã cải tiến một chút và viết thư viện để bạn có thể dễ dàng sử dụng trong các dự án sau này.

Trước khi tiếp tục, tôi khuyên bạn nên mở một tab khác để xem bài viết “millis() – Tạo 1 đồng hồ theo thời gian thực” và “Lịch làm việc cho các Pin” của anh Đại Huỳnh để hiểu ý nghĩa của các hàm (nếu bạn không hiểu).

Một lưu ý nhỏ trước khi đi tiếp là anh Đại Huỳnh đã viết thư viện bằng phương pháp lập trình hướng đối tượng và sử dụng mẫu thiết kế singleton. Song vì lý do khách quan là kiến thức này không phù hợp với những người không có kiến thức vững về lập trình C++ (hướng đối tượng), vì vậy tôi sẽ giải thích một cách đơn giản như cách tôi đã giải thích vấn đề giao tiếp giữa các mạch Arduino.

Quên một điều nữa, với thư viện của tôi đã cải tiến từ thư viện WorkScheduler của anh Đại Huỳnh, tôi đang hướng dẫn bạn cách viết chương trình bất đồng bộ (asynchronous) chứ không phải xây dựng một hệ điều hành thời gian thực (RTOS). Vì vậy, nó có một số nhược điểm sẽ được nói ở sau.

Phần cứng

Trước khi tiếp tục ví dụ và mô tả, bạn cần chuẩn bị:

  1. 01 mạch Arduino (dùng UNO cho dễ)
  2. 01 breadboard
  3. Chùm dây cắm breadboard

Các ví dụ

Qua các ví dụ này, tôi sẽ trình bày từng điểm mạnh, điểm yếu của thư viện này (vì cách tiếp cận là xử lý không đồng bộ chứ không phải là xây dựng hệ điều hành thời gian thực).

1. Viết chương trình không đồng bộ một cách dễ dàng

//...
//Khởi gạo class timer (design pattern singleton) - bắt buộc phải có trong hàm setup (trước khi khởi tạo các job)
Timer::getInstance()->initialize();
//new WorkScheduler
//...

Sau đó, bạn khai báo công việc như trong ví dụ hướng dẫn. Bạn có thể tuân thủ theo quy trình sau để khai báo một công việc (job).

//khởi tạo các job là các biến toàn cục trong chương trình
WorkScheduler *<tên job>;
//...
void <tên hàm mà job sẽ gọi>() {
  //chạy một đoạn chương trình gì đó... xem thêm trong ví dụ ở trên
}

void setup() {
  //Khởi tạo Timer một lần duy nhất - bạn có lỡ chạy thêm lần nữa thì cũng không có gì khác xảy ra đâu - do đây là mẫu singleton với bản chất xây dựng một toolkit dạng hướng đối tượng mà :D
  Timer::getInstance()->initialize();
  //khởi tạo job
  <tên job> = new WorkScheduler(<khoảng thời gian giữa các chu kỳ>UL, <tên hàm mà job sẽ gọi>);
}
void loop() {
  //đầu hàm loop phải có để cập nhập thời điểm diễn ra việc kiểm tra lại các tiến trình
  Timer::getInstance()->update();
  //<tên job>->update(); // kiểm tra job đã đến lúc chạy hay chưa?
  //cuối hàm loop phải có để cập nhập lại THỜI ĐIỂM (thời điểm chứ ko phải thời gian nha, tuy tiếng Anh chúng đều là time) để cho lần xử lý sau
  Timer::getInstance()->resetTick();
}

Ngoài cách khởi tạo một job như trên, bạn có thể lên lịch cho một pin như trong bài viết của anh Đại Huỳnh.

Ưu điểm của thư viện này và qua ví dụ này bạn có thể thấy rằng, ta lên lịch làm việc một cách rất khoa học và cách tổ chức hàm loop cực kì đơn giản không hề phức tạp!

2. Chạy nhiều job hơn và chu kỳ ngắn hơn

Bạn thử chạy đoạn chương trình xem sao. Đèn LED 13 sẽ nhấp nháy rất nhanh với chu kỳ 100ms (đủ để mắt thấy được). Tuy nhiên, mỗi khi đổi, bạn sẽ thấy đèn tắt ngay lập tức. Tại sao lại như vậy? Vì job analogReadScheduler (sẽ đọc liên tục) giá trị từ chân analog A0. Do đó, khi chân A0 không được kết nối với bất kỳ thứ gì, giá trị của nó sẽ thay đổi ngẫu nhiên và nếu giá trị xuống 0, đèn LED sẽ tắt cho đến khi giá trị khác 0. Để đèn nhấp nháy liên tục, bạn hãy kết nối chân A0 với 5V hoặc 3.3V, và nếu muốn đèn tắt, hãy kết nối với GND.

Lưu ý: Bạn có thể nghĩ rằng, có lẽ như vậy thì thay thế được interrupt! Nhưng không, nó không thể thay thế interrupt được. Tại sao? Tôi rất mong bạn trả lời và comment dưới bài viết cho những người khác thấy.

Kết luận

Với câu hỏi ở đầu bài, câu trả lời chắc chắn là có và bạn có thể khám phá thêm nữa! Nó sẽ giúp bạn viết chương trình Arduino một cách thoải mái mà không cần lo lắng về xung đột. Bạn có thể viết các đoạn chương trình độc lập hoặc phụ thuộc vào nhau. Tuy nhiên, nó không thể thay thế interrupt và nếu có quá nhiều job có chu kỳ quá nhỏ, chương trình có thể hoạt động không đúng. Ví dụ, nếu bạn in Serial mỗi 100ms trong một chu kỳ 1ms, bạn sẽ thấy kết quả không chính xác.

Nấu code vui vẻ cuối tuần nhé!