Xử lý nhiều tiến trình cùng một lúc - Xử lý bất đồng bộ khởi đầu Arduino

Đăng lúc: Thứ năm - 20/10/2016 10:19 - Người đăng bài viết: SuperG
Xử lý nhiều tiến trình cùng một lúc - Xử lý bất đồng bộ khởi đầu Arduino

Xử lý nhiều tiến trình cùng một lúc - Xử lý bất đồng bộ khởi đầu Arduino

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?

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 Huynh (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:

  1. ...
  2. int setup() {
  3. //pinMode các kiểu
  4. }
  5.  
  6. int loop() {
  7. digitalWrite(led1, HIGH);
  8. digitalWrite(led2, HIGH);
  9. delay(500);
  10. digitalWrite(led2, LOW);
  11. delay(500);
  12. digitalWrite(led1, LOW);
  13. digitalWrite(led2, HIGH);
  14. delay(500);
  15. digitalWrite(led2, LOW);
  16. delay(500);
  17. }

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?

  1. byte led1 = 5;
  2. byte led2 = 6;
  3. unsigned long time1 = 0;
  4. unsigned long time2 = 0;
  5.  
  6. void setup()
  7. {
  8. pinMode(led1, OUTPUT);
  9. pinMode(led2, OUTPUT);
  10. }
  11.  
  12. void loop()
  13. {
  14. if ( (unsigned long) (millis() - time1) > 696 )
  15. {
  16. if ( digitalRead(led1) == LOW )
  17. {
  18. digitalWrite(led1, HIGH);
  19. } else {
  20. digitalWrite(led1, LOW );
  21. }
  22. time1 = millis();
  23. }
  24.  
  25. if ( (unsigned long) (millis() - time2) > 133 )
  26. {
  27. if ( digitalRead(led2) == LOW )
  28. {
  29. digitalWrite(led2, HIGH);
  30. } else {
  31. digitalWrite(led2, LOW );
  32. }
  33. time2 = millis();
  34. }
  35. }

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ư 

  1. if ( (unsigned long) (millis() - timeX) > <mc thi gian>  )
  2. {
  3. //...
  4. timeX = millis();
  5. }

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:

Cách viết chương trình không sử dụng hàm delay:

Nội dung chính, cần nắm

Thông thường trong chương trình Arduino, khi cần dừng lại để chờ qua 1 khoảng thời gian chúng ta thường sử dụng hàm delay để thực hiện việc chờ này. Tuy nhiên cách làm này gây hao phí thời gian của CPU một cách vô ích, chúng ta không thể vừa dừng lại để chờ, vừa chạy 1 đoạn chương trình khác được.

Kĩ thuật trong bài này sẽ giúp bạn chạy được nhiều đoạn chương trình song song với nhau.

Phần cứng

  • Arduino Uno
  • Breadboard
  • Dây cắm breadboard
  • 2 điện trở 560 Ohm (hoặc 220 Ohm hoặc 1kOhm)
  • 2 đèn LED siêu sáng

Lắp mạch

Chương trình

Mục tiêu của đoạn chương trình này là nhấp nháy cùng lúc 2 đèn led, mỗi đèn có chu kì nháy sáng khác nhau.

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) > 1000 )    {        if ( digitalRead(led1) == LOW )        {            digitalWrite(led1, HIGH);        } else {            digitalWrite(led1, LOW );        }        time1 = millis();    }        if ( (unsigned long) (millis() - time2) > 300  )    {        if ( digitalRead(led2) == LOW )        {            digitalWrite(led2, HIGH);        } else {            digitalWrite(led2, LOW );        }        time2 = millis();    }}

Phân tích

Đoạn chương trình trên có chức năng kiểm tra các biến thời gian time1, time2 so với giá trị của hàm millis(). Nếu vượt quá thời gian quy định 1000ms với biến time1 và 300ms với biến time2 thì sẽ thay đổi trạng thái đèn led. Nếu với cách làm thông thường chúng ta sẽ viết:

void loop(){   if ( digitalRead(led1) == LOW )   {        digitalWrite(led1, HIGH);   } else {        digitalWrite(led1, LOW );   }   delay (700); // giảm thời gian để tổng thời gian chờ của led 1 là 700 + 300 = 1000ms      if ( digitalRead(led2) == LOW )   {        digitalWrite(led2, HIGH);   } else {        digitalWrite(led2, LOW );   }   delay (300);}

Với cách viết thông thường, chương trình sẽ dừng lại ở hàm delay và chờ ở đó. Trong khi với cách viết dùng hàm millis() như ở trên, chúng ta có thể nhấp nháy nhiều led song song với nhau.

Tạo 1 đồng hồ theo thời gian thực và Lịch làm việc cho các Pin

Nếu ai đó đọc bài này mà đã học qua lập trình thì việc phải Sleep trên main thread là 1 điều không nên và ảnh hưởng rất nhiều đến hiệu suất của chương trình và hàm delay(int) trong Arduino cũng tương tự như thế. Vì tôi muốn tác vụ của tôi phải chạy thời gian thực, code ngắn gọn, hướng đối tượng nên tôi viết 1 lớp Timer cho riêng mình để hoạt động trong hàm loop() của tôi không phải nghỉ 1 khoảng rồi mới làm tiếp. Đồng thời tôi cũng viết thêm 1 lớp WorkScheduler để thuận tiện cho công việc lập lịch làm việc cho các pin mà tôi muốn làm việc.

  • Timer:
    • Sử dụng singleton để tất cả mọi hàm đều sử dụng 1 lần trong cả chương trình.
    • Yêu cầu initialize ( khởi tạo ) ban đầu ở hàm setup() của Arduino.
    • Yêu cầu gọi hàm update() đầu tiên trong hàm loop().
    • Yêu cầu gọi hàm resetTick() cuối cùng trong hàm loop().
    • Tôi xin giải thích về hàm Timer như sau:
      • Đầu tiên chúng ta sẽ khởi tao Timer bằng cách cho lastTick ( lần tick trước ) bằng thời gian trôi qua hiện tại trong Arduino và đó là lý do tại sao phải gọi hàm initialize() trong setup().
      • Ở hàm loop() mỗi lần bắt đầu phải gọi hàm update() để đánh dấu currentTick (lần tick hiện tại ) bằng thời gian trôi qua hiện tại trong Arduino để chúng ta có thể tính được delta giữa 2 lần loop() cũa Arduino. Thời gian delta phụ thuộc rất nhiều bằng rất nhiều vào logic trong hàm loop của bạn. Logic càng phức tạp thì thời gian delta giữa 2 lần loop() càng lớn. 
      • Ở cuối hàm loop() ta sẽ gọi resetTick() để cập nhật lastTick = currentTick để cho phép lần loop() sau tính delta()
class Timer {  private:    unsigned long _lastTick;    unsigned long _currentTick;        Timer() { }    ~Timer() { }  public:  static Timer* getInstance() {    static Timer* instance = new Timer();    return instance;   }    /*   * Gọi trong hàm setup 1 lần duy nhất   */  void initialize() {    _lastTick = millis();   }    /*   * Gọi đầu tiên nhất trong hàm loop()   */  void update() {    _currentTick = millis();  }    /*   * Trả về thời gian giữa 2 lần loop()   * giá trị delta là millisecond   */  unsigned long delta() {    return _currentTick - _lastTick;  }    /*   * Yêu cầu gọi cuối cùng trong hàm loop()   */  void resetTick() {    _lastTick = _currentTick;  }};
  • WorkScheduler:
    • Ý tưởng của tôi khi viết lớp này là tôi muốn viết 1 lớp để lên lịch làm việc cho 1 pin trong Arduino sau 1 khoảng thời gian nhất định.
    • Ở lớp này tôi dùng còn trỏ hàm với dạng như sau void (*func)(int) có thể hiểu là đây là 1 hàm không trả về giá trị void và nhận 1 tham số là int là số pin trên Arduino mà ta muốn nó sẽ làm việc. Bạn có thể thay đổi tùy ý dựa trên nhu cầu làm việc của mình.
    • Tham số đầu vào cho hàm dựng của WorkScheduler của tôi yêu cầu 3 tham số là: số pin sẽ phải làm việc, thời gian sẽ làm việc, con trỏ hàm định nghĩa việc sẽ làm.
    • Hàm update() được gọi để cập nhật thời gian trôi qua, nếu thời gian trôi qua lờn hơn thời gian làm việc thì Pin sẽ phải làm việc rồi chúng ta sẽ reset lại thời gian trôi qua. Tại sao tôi không set là 0 mà lại dùng -= cho thời gian sẽ làm việc? Đó là do thời gian delta giữa các loop() không đều nhau dẫn đến thời gian trôi qua nhiều khi sẽ lố qua thời gian làm việc nên chúng ta sẽ có 1 cơ số thời gian dư thừa cần phải bù đắp lại cho lần làm việc sau, vì vậy tôi sẽ giữ lại những cơ số thừa đó cho những lần cập nhật sau để lịch làm việc chính xác hơn
class WorkScheduler {  private:    unsigned long _ellapsedTime;    unsigned long _workTime;    int _workPin;    void (*func)(int);  public:    WorkScheduler(int workPin, unsigned long time,void (*func)(int)) {      _workPin = workPin;      _workTime = time;      _ellapsedTime = 0;      this->func = func;    }        ~WorkScheduler() {      _workPin = 0;      _workTime = 0;       _ellapsedTime = 0;      func = NULL;    }        void update() {      _ellapsedTime += Timer::getInstance()->delta();      if (_ellapsedTime >= _workTime) {        _ellapsedTime -= _workTime;       if (func != NULL) {         func(_workPin);       }       }    }};

 

Sau đây là chương trình mẫu của tôi là cho 1 pin làm việc bằng cách chớp tắt 1 LED và bạn hoàn toàn có thể thêm bao nhiêu cũng được. Tôi đã thử cho 3 LED hoạt động trong thời gian khác nhau và nó hoàn toàn chính xác.

 

const int pinRed = 8;WorkScheduler *redPinWorkScheduler;void OnRedPin_Work(int pin) {  static boolean wasLightedUp = false;  digitalWrite(pin, !wasLightedUp ? HIGH: LOW);  wasLightedUp = !wasLightedUp;}void setup() {  pinMode(pinRed, OUTPUT);    Timer::getInstance()->initialize();    redPinWorkScheduler = new WorkScheduler(pinRed, 1000UL, OnRedPin_Work);  }void loop() {  Timer::getInstance()->update();    redPinWorkScheduler->update();   Timer::getInstance()->resetTick(); 

}

 

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

(File)

 

 

 

 

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)

  1. ...
  2. void setup() {
  3. ...
  4. //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)
  5. Timer::getInstance()->initialize();
  6. ...
  7. //new WorkScheduler ...
  8. ...
  9. }
  10. ...

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).

  1. //khởi tạo các job là các biến toàn cục trong chương trình
  2. WorkScheduler *<tên job>; // nhớ là không có mấy dấu "<", ">" đâu nhé :3
  3. ...
  4.  
  5. void <tên hàm mà job s gi>() {
  6. //chạy một đoạn chương trình gì đó... xem thêm trong ví dụ ở trên
  7. }
  8.  
  9. void setup() {
  10. ...
  11. //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
  12. Timer::getInstance()->initialize();
  13.  
  14. //khởi tạo job
  15. <tên job> = new WorkScheduler(<khoàng thi gian gia 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 :)
  16. }
  17.  
  18. void loop()
  19. {
  20. //đầ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
  21. Timer::getInstance()->update();
  22.  
  23. ...
  24. <tên job>->update(); // kiểm tra job đã đến lúc chạy hay chưa?
  25.  
  26.  
  27. //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
  28. Timer::getInstance()->resetTick();
  29.  
  30. }

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

(file)

 

 

 

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é!  

KSP-Arduinovn.

Đánh giá bài viết
Tổng số điểm của bài viết là: 0 trong 0 đánh giá
Click để đánh giá bài viết
 

Hướng dẫn sử dụng Website

Hướng dẫn quý khách hàng cách tham khảo và sử dụng thông tin trên Website chúng tôi, với mục tiêu giúp quý khách hàng nhanh chóng hình dung về sản phẩm và dịch vụ mới của chúng tôi, kính mong được hợp tác cùng quý khách! ...

Thăm dò ý kiến

Bạn có muốn sở hữu một Robot trong nhà không?

Cần một Robot để dọn dẹp

Cần một Robot trông nhà

Cần một Robot để giải trí

Bạn cần một Robot theo cách khác

Bạn đã có rồi

Bạn không cần

Liên kết