Áp dụng quy trình hiện đại khi làm phần mềm cho hệ thống nhúng

Đi qua đi lại một vài đơn vị làm phần mềm nhúng, IoT Việt Nam, mình thấy không hài lòng lắm về cách làm việc hơi cũ. Mình thấy nhiều bạn làm điện tử có thể lập trình được, những vẫn còn một khoảng trống dài về phương pháp làm việc giữa những người này và những người thuần về phần mềm. Thế nên mình viết bài này, hi vọng kéo những người làm phần mềm nhúng tiến lên vài bước cho gần với chuẩn.

Lưu ý: Những cách làm sau đây, ban đầu sẽ gây thiệt thòi vì vẽ ra quá nhiều chuyện để làm, nhưng về lâu dài thì có lợi cho việc tiếp tục phát triển sản phẩm.

1. Chia nhỏ phần mềm thành những gói gần như độc lập

Lấy ví dụ về một hoạt động kiểm nghiệm ý tưởng thiết kế phần mềm tại AgriConnect.

Một số sản phẩm điện tử tại AgriConnect là kết quả của sự hợp tác, trong đó đối tác làm một module, AgriConnect làm một module và ghép với nhau. Trước khi bắt tay vào việc thì mình sẽ đề xuất API để 2 module trao đổi lệnh, dữ liệu với nhau, thường là phía đối tác sẽ lập trình ARM, bên mình thì lập trình ESP8266/ESP32.

Dù là lập trình cho thiết bị nhúng nhưng AgriConnect vẫn tổ chức một cách bài bản: tối đa module hóa các chức năng của phần mềm, cắt phần mềm ra những thư viện độc lập để có thể tái sử dụng. Ví dụ ở phần phân tích dữ liệu trao đổi với module ARM thực ra là xử lý chuỗi, không phụ thuộc vào phần cứng (vi điều khiển) cụ thể nên sẽ được tách ra thành thư viện. Việc tách ra thành thư viện trung tính này có một cái lợi nữa là, trong khi đối tác chưa làm xong thiết bị để có thể test thì phần code thư viện đó được viết và test thẳng trên PC.

A Rust code

Trong hình là lúc mình đang viết những dòng code đầu tiên để kiểm nghiệm ý tưởng thiết kế API có tốt không, và mình chọn Rust để viết! Viết bằng Rust có cái lợi là, vì là một ngôn ngữ hiện đại, công cụ đi kèm hiện đại nên có một số cấu trúc viết nhanh gọn hơn C, hỗ trợ unittest built-in luôn. Với C thì phải chọn lựa thư viện ngoài để viết unittest, rồi thêm việc setup rườm rà cho unittest (trong khi với Rust thì chỉ cần lệnh cargo test là đủ).

Tuy nhiên một điều đáng buồn là sau khi kiểm nghiệm ý tưởng xong thì mình vẫn phải chuyển qua C để viết code thật, vì lý do trình biên dịch của Rust chưa hỗ trợ biên dịch cho ESP8266, ESP32. Có điều vì tư tưởng "thư viện trung tính" nên code C đó vẫn dùng để chạy, test, debug trên PC được, trước khi ráp vào code dành cho ESP8266/ESP32.

Hình dưới đây là mình biên dịch thư viện trên PC (x86) và chạy unit test:

test-on-pc

(Về hệ thống build, thay vì dùng make, mình đang dùng MesonNinja, hiện đại hơn).

Còn hình dưới đây là code thư viện được ứng dụng vào code cho ESP32:

lib in C

2. Viết phần mềm giả lập / mô phỏng thiết bị

Trong quá trình làm sản phẩm kiểu 2 thiết bị giao tiếp với nhau thì luôn gặp trường hợp là không có sẵn thiết bị để test giao thức, vì nó vẫn còn đang được chế tạo, thế nên mình hay viết phần mềm mô phỏng để thay mặt thiết bị còn thiếu. Các thiết bị bên mình thường giao tiếp qua UART (serial), MQTT hoặc WebSocket. Phần mềm mô phỏng chạy trên PC và nắm một đầu serial, còn đầu kia của serial cắm vô board đang được lập trình. Việc viết ra phần mềm mô phỏng này thường là nhanh, vì mình dùng Python, với một kho thư viện khá khủng, hầu như mục đích gì cũng có.

Thỉnh thoảng, thói quen thứ nhất kể phía trên cũng có lợi luôn cho cả thói quen thứ hai. Đó là câu chuyện khi mình viết server để điều khiển thang máy trong một dự án nọ. Thang máy có một giao thức riêng để gửi yêu cầu gọi thang vào đó. Nhiệm vụ của mình trong dự án là viết ra phần mềm server, cung cấp nhiều API để cho phép các app, các thiết bị quẹt thẻ được gọi thang. Phần xử lý giao thức trò chuyện với thiết bị được mình tách ra một thư viện riêng. Do thang máy không có trong tay, nó đang phục vụ cho một tòa nhà tận ngoài Hà Nội, không đem về test được nên mình cũng phải viết ra một phần mềm mô phỏng luôn. Ngoài phần mềm mô phỏng, mình cũng còn viết một công cụ test, phân tích gói tin có giao diện người dùng. Do trong lúc làm server và viết ra thư viện kia, không có phần cứng thật, mà tài liệu mô tả thì không thể trông cậy hoàn toàn (tài liệu có thể viết sai, viết thiếu, hoặc thậm chí mình hiểu sai tài liệu), mình không thể biết chắc mình đang viết đúng hay sai, nên công cụ debug gói tin này sẽ dùng để đem ra tận nơi lắp đặt, test trước để kiểm chứng tài liệu. Nhờ việc tách ra thư viện con nên mình tái sử dụng được vào cả 3 phần mềm, tiết kiệm được khá nhiều công sức. Một điều thuận lợi nữa là thư viện của Python giúp kết quả debug rất dễ nhìn.

Trong mục này, không có hình ảnh minh họa vì thiết bị đã đem đi lắp cho khách hàng, không còn cái nào để cho tương tác với phần mềm mô phỏng, còn dự án thang máy kia thì giao thức kia là nội dung bí mật.

Cập nhật: Có một hình ảnh minh họa về một trường hợp khác có sử dụng phần mềm giả lập thiết bị, trong bài Một số mẹo cho việc phát triển ứng dụng hệ thống nhúng.

3. Sử dụng Git

Sử dụng Git (hoặc các phần mềm tương tự) là kĩ năng cực kỳ quan trọng nhưng mình kể ra sau vì dù sao thì nó cũng đã được chấp nhận nhiều nơi rồi. Trước đây mình từng phỏng vấn một anh trạc tuổi, có thâm niên làm hệ thống nhúng. Với chừng ấy năm làm sản phẩm, lại gánh team cho một startup thì hẳn kinh nghiệm về điện tử là dày dạn rồi. Thế nhưng mình không tuyển được vì anh này viết code và lưu trữ trên Google Drive, chưa từng đụng đến Git. Dù đã có nhiều công ty áp dụng Git, nhưng mình vẫn nhắc ở đây một lời: Tập dùng Git bằng dòng lệnh đi! Chắc 99% những bạn mà mình đã phỏng vấn, kể cả mảng làm app di động, app backend, đều chỉ dùng Git thông qua phần mềm có giao diện đồ họa (GUI). Ưu thế của Git dòng lệnh là tất cả tính năng nâng cao của Git đều chỉ nằm ở đó.

Một câu hỏi phỏng vấn về Git mà mình đưa ra khiến tất cả ứng viên thất bại là:

  • Giả sử bạn vừa tạo xong một commit thì phát hiện ra mình nhầm. Giả sử đội của bạn quy ước một commit chỉ gồm một file, nhưng bạn lỡ gom ba file trong một lần commit. Vậy bạn sửa lại làm sao?

Hoặc một phiên bản khác:

  • Giả sử bạn vừa commit một file. Trong file đó có năm chỗ thay đổi nhưng thật ra chỉ có ba chỗ thay đổi là liên quan đến tính năng bạn đang làm, còn hai chỗ thay đổi kia đáng lẽ nằm ở commit khác. Nhưng bạn lỡ commit xong rồi mới phát hiện ra mình nhầm, vậy bạn sửa lại làm sao?

Trong những năm đi làm phần mềm của mình thì tình huống sắp xếp, chia lại commit như vầy xảy ra cũng nhiều lần, thậm chí mình còn gặp những tính huống oái oăm hơn (nhưng ít gặp hơn). Tất cả những tình huống từ cấp độ này trở lên đều chỉ giải quyết được bằng Git dòng lệnh.

Một câu chuyện khác về lợi ích của Git là khi bạn cần sửa lại thư viện của người khác, chẳng hạn các thư viện được quản lý trên kho của PlatformIO. Những thư viện này thường đã được quản lý bằng Git. Cách làm của mình là fork thư viện này ra một repo khác, tạo một nhánh mới và sửa đổi code trên nhánh đó. Khi thư viện được tác giả nâng cấp thì chỉ cần "merge" code từ repo gốc vào là sẽ nhận được cập nhật. Nếu không được quản lý bằng Git thì khi bộ code gốc của tác giả được cập nhật, muốn copy phần cập nhật qua bản sửa đổi là không biết copy từ đoạn nào, vào đoạn nào.

Trên đây là mình kể sơ một vài bước thực hành tốt để nâng cao độ chuyên nghiệp và chất lượng sản phẩm cho các anh em lập trình nhúng. Để kết bài, mình còn một lời khuyên nhỏ khác: Tập sử dụng Linux và các phần mềm mã nguồn mở, cố gắng đưa việc lập trình lên làm trên Linux. Một lý do thực tế là Git và những quy trình xoay quanh nó được ra đời từ Linux. Một dự án quy mô cỡ Linux, bộ source code được viết bởi 15.600 người, đến từ 1.400 công ty rải rác khắp nơi trên thế giới, thì bài toán quản lý, đảm bảo chất lượng là bài toán lốn khủng khiếp. Ngoài ra, việc tham gia dự án mã nguồn mở khó tính như OpenSC đã tạo cơ hội cho mình rèn luyện, va chạm với những tình huống oái oăm và khai thác tính năng nâng cao của Git. Cộng đồng Python thì giúp mình chú trọng đến việc viết tài liệu một cách chỉn chu. Chắc ít người để ý, rất nhiều dự án, cho dù không phải Python, đều đăng tài liệu trên https://readthedocs.org/. Website này ra đời từ cộng đồng Python, sử dụng những công cụ nền được sáng tạo từ hệ sinh thái của Python.