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?

I. Giới thiệu

Một khi viết một chương trình lớn, bạn sẽ phải viết chương trình để thực hiện nhiều chức năng. Và khi viết chương trình với nhiều chức năng bạn sẽ gặp các vấn đề phức tạp như: làm thế nào để chức này hoạt động ổn định với chức năng kia, và khi thêm chức năng mới vào sản phẩm của mình nó sẽ đụng độ như thế nào với các chương trình khác? Qua bài viết này, mình muốn chia sẻ với các bạn một thư viện khá hay của anh Đại Huỳnh (trong đó mình có mod lại một tí laugh) để giải quyết các vấn đề nêu trên - xử lý nhiều tiến trình cùng một lúc trên Arduino.

II. Khởi nguồn

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

...
int setup() {
    //pinMode các kiểu
}

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);
}

Các bạn sẽ dễ dàng đưa ra một nhận xét rằng: "Sao mà nó lặp đi lặp lại hoài vậy, có cách nào rút gọn hơn không". Vâng, lúc đó, mình nghĩ thôi chắc dùng cái biến int state để lưu lại giá trị của led1 rồi từ đó tinh chỉnh cho led1 angry.

Giờ nghĩ lại sao thấy hồi đó "gà" quá blush.

Câu hỏi đặt ra trong đầu mình tiếp theo đó là, lỡ nếu chu kỳ nó "không đẹp" (ví dụ như một cái thì khu kỳ 696ms, còn một cái thì chu kình 133ms) thì sao? Lúc đó mà dùng delay thì chết. Thành ra mình dùng hàm millis để kiểm tra xem tại thời điểm kiểm tra đã đến lúc để thực hiện một đoạn chương trình nào đó 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 tinh ý một tí, bạn sẽ thấy, đoạn chương trình này có những thứ dòng lệnh giống nhau như 

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

Câu hỏi đặt ra, là có cách nào rút gọn hơn nữa không, dạng như kiểm tra đúng thời gian là nó chạy luôn mà không cần phải viết dài như thế. Vâng, xin thưa với bạn là có, và anh Đại Huỳnh đã có một bài viết về thư viện của ảnh về vấn đề này tại đây.

III. Cải tiến

Tuy là bài viết của anh Đại Huỳnh đã giải quyết gần hết những vấn đề mà mình đưa ra, nhưng có vẻ các bạn vẫn chưa dùng được nó một cách thuần thục, vì vậy, mình đã có cải tiến một chút và viết dưới dạng thư viện để các bạn có thể dễ dàng sử dụng sau này trong các dự án sau này.

Đến thời điểm này, mình khuyên các bạn nên mở thêm một tab nữa mở 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 để tiện theo dõi ý nghĩa của các hàm (nếu các bạn không hiểu).

Một lưu ý nhỏ trước khi đi tiếp dành cho các bạn đó 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ột mẫu thiết kế hướng đối tượng tên là singleton. Vì lý do khách quan là những kiến thức này không "thân thiện" với các bạn không có kiến thức lập trình C++ (hướng đối tượng vững), nên mình sẽ nói một cách "nôm na dễ hiểu" như cách mà mình đã dùng để nói đến vấn đề giao tiếp giữa các mạch Arduino.

À quên nữa, với thư viện của mình cải tiến từ thư viện WorkScheduler của anh Đại Huỳnh, mình đang hướng các bạn đến một phương pháp viết chương trình có tên gọi là "bất đồng bộ" (asynchronous) chứ không phải là một thư viện xây dựng một hệ điều hành thời gian thực - real time operator system (RTOS) nhé. Thành ra, nó có một số nhược điểm sẽ nói ở sau.

IV. Phần cứng

Trước khi đi sâu vào phần ví dụ và mô tả ví dụ, các 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

V. Các ví dụ

Qua các ví dụ này, mình sẽ trình bày quá 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

 

 

Như các bạn đã thấy trong đoạn code trên, để cho chương trình hoạt động được thì bạn phải khởi tạo đối tượng Timer (singleton)

...
void setup() {
    ...
    //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 đó, là khai báo cái job như trong ví dụ hướng dẫn. Hoặc cụ thể hơn, bạn hãy 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>; // nhớ là không có mấy dấu "<", ">" đâu nhé :3
...

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ọ>); // Chữ UL phía sau là để trình biên dịch hiểu là con số bạn nhập vào là một hằng số dương kiểu usigned long :)
}

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, các bạn hoàn toà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 được 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 người thấy được cool). Nhưng chạy mỗi đổi thì thấy tự nhiên nó tắt liệm đi, vì sao? Bởi vì tại job analogReadScheduler (sẽ đọc liên tục) giá trị tại chân analog A0. Như vậy, vì chân A0 không được nối gì hết thành ra nó nhảy giá trị tùm lum và nếu nó nhảy xuống 0 thì đèn LED sẽ tắt mãi cho đến khi nó nhảy đến một giá trị khác 0 cheeky. Để đèn nhấp nháy liên tục, bạn hãy lấy dây breadboard nối A0 với 5V hoặc 3.3V, và nếu muốn đèn tắt xin hãy nối tới GND.

Lưu ý: các bạn có thể dễ dàng suy nghĩ rằng, có lẻ như vậy nó đã thay thế được interrupt! Nhưng không, nó không thể nào thay thế đươc interrupt cả. Vì sao, tôi rất mong bạn trả lời được và comment phía dưới bài viết cho các bạn khác thấy nhé.

VI. Kết luận

Với câu hỏi đầu bài thì câu trả lời chắc chắn là có và bạn còn có thể đi sâu hơn nữa đó! Nó sẽ dễ dàng giúp bạn viết các đoạn chương trình bằng cách lên lịch cho Arduino. Từ đó cho chương trình chạy một cách thoải mái mà không sợ đụng nhau, 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ế được interrupt và nếu có quá nhiều job có chu kỳ quá nhỏ sẽ làm cho chương trình hoạt động sai. Ví dụ, bạn cho lệnh Serial print 100ms cho chu kỳ xuống 1ms thử là biết ngay.

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

lên
24 thành viên đã đánh giá bài viết này hữu ích.
Các dự án được truyền cảm hứng

Select any filter and click on Apply to see results

Các bài viết cùng tác giả

Vấn đề của số chấm động và số nguyên trong ngôn ngữ lập trình C++ trên board mạch Arduino

Có bao giờ bạn tự hỏi: "Dự án của mình làm tốt thế này, chạy ngon lành rành rành thế này, chắc không có bugs đâu?". Thực sự, nếu dự án của bạn không có phần xử lý số thực chấm động trong đó thì mình nghĩ phần code của bạn sẽ hoạt động ngon lành theo thời gian. Nhưng mà có số thực thì từ từ, chúng ta cần xét lại code. Trước đây, có một số bạn nhắn tin riêng hỏi mình về code với điểm chung là "code mình chạy ngon lành lúc đầu, sau đó bị lỗi, không rõ nguyên nhân". Loại trừ các phần code logic sai ra, thì hầu hết đều là do lỗi khi xử lý số chấm động mà không quan tâm đến nền tảng lập trình bên dưới! Mà cũng đúng, chúng ta rất dễ bị đánh lừa bởi chính đoạn code chúng ta viết. Vì nó có báo lỗi biên dịch đâu mà, kaka. Qua bài viết này, mình muốn phân tích và cùng các bạn rút kinh nghiệm về số chấm động float, cách hạn chế lỗi sai với số chấm động.

lên
22 thành viên đã đánh giá bài viết này hữu ích.

Bài 1: Một chương trình trên Arduino cần tối thiểu những gì?

Trong bài viết này, tôi muốn chỉ cho các bạn biết để viết một chương trình Arduino, bạn cần chuẩn bị TỐI THIỂU những điều gì!

lên
121 thành viên đã đánh giá bài viết này hữu ích.