AK Embedded Base Kit - STM32L151 - Event Driven: Task & Signal

AK Embedded Base Kit - STM32L151 banner

Tại sao Event Driven cần thiết cho hệ thống nhúng ?

Phần mềm nhúng ngày càng phức tạp. Trước kia để viết phần mềm nhúng trên MCU, bạn chỉ cần biết điều khiển ngoại vi: IO, TIMER, UART, SPI, I2C là có thể xây dựng được các ứng dụng.
Với xu hướng phát triển của IoT và Automotive,... Các SDK của hãng ngày càng phức tạp với nhiều module, một số module chứa phần hiện thực hiện code stack như zigbee, RTLS,... không đơn thuần là gọi hàm thực hiện, hoặc đăng kí callback fucntion, mà thường được tổ chức theo nhiều loại mô hình Event Driven khác nhau tùy thuộc vào từng hãng, rất ít khi chúng ta được viết ở lớp ngoại vi.
 
Event Driven là một lớp software nằm ngay bên dưới, tiếp xúc với lớp Application.

Mô hình Event Driven với Bare Metal Project & RTOS

 

Các khái niệm của hệ thống Event Driven

  • Event Driven nôn na là một hệ thống gửi thư (gửi message) để thực thi các công việc. Trong đó, Task đóng vai trò là người nhận thư, Signal đại diện cho nội dung công việc. Task & Signal nền tảng của một hệ Event Driven.
  • Thông thường mỗi Task sẽ nhận một nhóm công công việc nào nào đó, ví dụ: quản lý state-machine, quản lý hiển thị của màn hình, quản lý việc cập nhật phần mềm, quản lý hệ thống watchdog ... 
  • Message được chia làm 2 loại chính, Message chỉ chứa Signal, hoặc vừa chứa Signal và Data. Message tương đương với Signal.
  • Chỗ thực thi một công việc nào đó thì mình gọi là Handler.

Các services của hệ thống Event Driven

Timer là một service của Event Driven, dùng để gửi định kì các Signal đến Task, các signal sẽ được gửi liên tục theo chu kì (PERIODIC) hoặc gửi một lần (ONE_SHOT) sau khi gọi hàm.
 
State Machine để phân chia việc hứng Signal trong Task. Ví dụ: với State A thì hứng và xử lý một nhóm Signal liên quan đến State A, các Signal khác sẽ bỏ qua không xử lý. Trong hệ thống Event Driven các Signal gửi rất nhiều và bất đồng bộ, để đảm bảo đồng bộ một Sequence thì cần sử dụng State Machine. State Machine có nhiều loại, độ phức tạp từ thấp lên cao như sau: 
  • Sử dụng 1 biến để lưu State, mỗi State tương ứng với 1 giá trị của biến.
  • Sử dụng 1 con trỏ hàm để lưu State, mỗi hàm là mỗi State.
  • Sử dụng 1 table để làm State, mỗi table là mỗi State, việc thay đổi các State theo Signal sẽ được biểu diễn ở dạng bảng (Table), Developer rất dễ trace sự chuyển đổi State với dạng Table. Với hệ thống nhiều State và Signal, mình ưu tiên sử dụng loại này.
Timer và State machine là 2 món lớn mình sẽ trình bày ở các bài viết kế tiếp.
 

Một số ưu điểm khi apply Event Driven

  • Thiết kế phần mềm tổng thể bằng UML, Sequence Diagram.
  • Dễ dàng phân chia nhỏ khối lượng công việc phát triển phần mềm nhúng.
  • Khả năng cách ly cao, chúng ta có thể viết thêm tính năng mới trên một source cũ với khối lượng code lớn rất dễ dàng, ít ảnh hưởng đến đến source cũ.
  • Dễ dàng phát triển trên ứng dụng kết hợp nhiều CPU với các nền tảng khác nhau (ví dụ: Kết hợp giữa MCU với MPU (Linux).
  • Dễ dàng Debug cả khi đang phát triển sản phẩm lẫn khi sản phẩm chạy thực tế.
  • Dễ dàng lên các hệ thống auto test phần mềm.
  • Tiết kiệm bộ nhớ của chương trình.
Các khía cạnh về ứng dụng cũng như các ưu điểm của Event Driven mình sẽ phân tích ở các bài viết khác, trong khuôn khổ bài viết này mình mong muốn các bạn có thể bắt tay ngay vào việc sử dụng Task, Signal, 2 món cơ bản nhất của một hệ Event Driven.
 
OK vào việc thôi !
 
Trước khi thực hiện các bước bên dưới các bạn cần đọc và hoàn thành các bước trong bài viết này nhé:
 
Trong bài viết, Message và Signal ý nghĩa khá tương đương nhau. Có khi mình dùng từ bắn message đến Task thay cho cụm từ gửi thư cho Task, hoặc hứng message tại Task, thay cho cụm từ Task nhận thư và Bắn message và Hứng message là " Event Driven foundation concept".
 

Hướng dẫn tạo Task và bắn message đến Task

 
Bài tập 1 kịch bản yêu cầu như sau:
Tạo một "task_hello", khi nhất nút [Up] trên Kit thì bắn một message đến "task_hello" để in ra màn hình console minicom "Hello World", đồng thời có thêm biến Counter, mỗi lần in "Hello World" biến đếm sẽ tăng lên 1 đơn vị.
 
Bước 1: Trong forder “app” tạo file source "task_hello.cpp" và "task_hello.h"

1. New File

(1. New File)

 

2. task_hello.h

(2. task_hello.h)

 

3. task_hello.cpp

(3. task_hello.cpp)

 
Bước 2: Vào file app.h tạo Signal cho task_hello

4. Khai báo signal AC_TASK_HELLO_PRINT

(4. Khai báo signal AC_TASK_HELLO_PRINT)

 

Bước 3: Khai báo task_hello vào trong task_list

5. Khai báo task_id trong file task_list.h, đặt tên là: AC_TASK_HELLO_ID

(5. Khai báo task_id trong file task_list.h, đặt tên là: AC_TASK_HELLO_ID)

 

6. Khai báo handler task_hello trong task_list.h

(6. Khai báo handler task_hello trong task_list.h)

 

7. Khai báo “task_hello” vào task_list.cpp

(7. Khai báo “task_hello” vào task_list.cpp)

 

Bước 4: Thêm task_hello.cpp vào app/Makefile.mk để build source

8. Add task_hello.cpp vào Makefile.mk để build source

(8. Thêm task_hello.cpp vào Makefile.mk để build source)

 

Bước 5: Bắn message đến task_hello

Nhấn nút Button [Up] trên kit để bắn message đến task_hello, code trong file app_bsp.cpp

9. Tạo trigger bắn message đến task_hello

(9. Tạo trigger bắn message đến task_hello)

 

AK hỗ trợ 3 loại hàm để bắn message đến Task, vai trò của từng hàm như sau:

  • task_post_pure_msg: Dùng để gửi Pure Message, Pure Message là loại messge chỉ mang theo Signal, không chứa data.
  • task_post_common_msg: Dùng để gửi Common Message, Common Message là loại messge có chứa thêm data, và kích thước data nó mang theo tối đa là 64 Bytes. Đây là loại message dùng nhiều nhất trong hệ thống.
  • task_post_dynamic_msg: Dùng để gửi Dynamic Message, Dynamic Message là loại message có chứa thêm data, kích thước của nó mang theo giới hạn bởi bộ nhớ heap.
Mỗi loại message: Pure, Common, Dynamic sẽ có một pool message riêng, vấn đề pool message tốn khá nhiều giấy mực nên mình sẽ có bài viết khác để trình bày nhé.
 

AK hỗ trợ 3 loại hàm để bắn message đến Task

(10. Các loại message được sử dụng trong AK)

 
Bước 6: Build và nạp code vào AK Embedded Base Kit để test thôi.
Các bạn mở minicom như sau: sudo minicom -D /dev/ttyUSB0 -b 115200
Sau đó nhấp nút [Up] trên Kit, minicom sẽ hiện thị như sau, hiển thị ra như vậy là thành công rồi nhé.

Kết quả hiển thị test tạo task trên minicom

(11. kết quả hiển thị trên minicom)

 

Bài tập 2: kịch bản yêu cầu như sau:

  • Tạo thêm một Task mới đặt tên là “task_hello_x”, mỗi lần nhấn nút [Up] sẽ bắn một message đến “task_hello” như bài tập 1.
  • Sau đó từ “task_hello” sẽ bắn một Common Message mang theo giá trị của biến x đến “task_hello_x”, biến x sẽ tăng dần. 
  • Từ “task_hello_x” đọc nội dung message và xuất ra data đã nhận được.

 

Lưu ý: Chúng ta sẽ tạo thêm "task_hello_x" như hướng dẫn ở Bài tập 1 nhé !

Bước 1: Gửi và nhận giá trị biến x từ "task_hello" đến "task_hello_x".

Gửi message từ “task_hello” sang “task_hello_x”

(12. Gửi message từ “task_hello” sang “task_hello_x”)

 

Xử lý nhận Common Message tại “task_hello_x”

(13. Xử lý nhận Common Message tại “task_hello_x”)

 

Bước 2: Test thành quả qua minicom thôi !

Kết quả màn hình minicom đúng ở bài tập 2

(14. Kết quả hiện thị đúng như kịch bản yêu cầu)

 

Như vậy qua 2 bài tập trên các bạn đã làm quen được với tạo Task và Signal, đồng thời biết được cách gửi nhận message giữa các Task với nhau.

Nếu trong quá trình thao tác có phát sinh các lỗi hoặc các thắc mắc, các bạn thông tin về EPCB theo các kênh sau nhé !