10. Embedded-Software Architecture
Trong những ngày đầu, lập trình firmware sử dụng các bộ vi điều khiển cực kỳ hạn chế về tài nguyên. Mỗi từng bit phải được tách ra khỏi không gian bộ nhớ code và data. Khả năng tái sử dụng phần mềm là một mối quan tâm nhỏ và các chương trình được phát triển nguyên khối. Các chương trình sẽ là một file với 50.000 dòng lập trình khổng lồ, tất cả chỉ trong một module duy nhất, với ít hoặc không có suy nghĩ nào về việc thiết kế kiến trúc hoặc tái sử dụng. Mục tiêu duy nhất là làm cho phần mềm hoạt động. Rất may, thời thế đã thay đổi, trong khi nhiều ứng dụng vi điều khiển vẫn bị “hạn chế về tài nguyên”, thì khả năng biên dịch và chi phí bộ nhớ giảm hiện nay đã cho phép kiến trúc phần mềm được tái sử dụng.
Phát triển phần mềm phức tạp, có thể mở rộng (scalable), có thể di động (portable) và có thể tái sử dụng (reusable) đòi hỏi phải có một kiến trúc phần mềm. Kiến trúc phần mềm (software architecture) là tổ chức cơ bản mà một hệ thống thể hiện trong các thành phần của nó, mối quan hệ của chúng với nhau và với môi trường. Các nguyên tắc hướng dẫn thiết kế và phát triển của nó.4 Nói cách khác, kiến trúc phần mềm là bản thiết kế (blueprint) mà từ đó nhà phát triển triển khai phần mềm. Kiến trúc phần mềm thực ra tương tự như bản thiết kế mà một kiến trúc sư sẽ sử dụng để thiết kế một tòa nhà hoặc một cây cầu.
Kiến trúc phần mềm cung cấp cho nhà phát triển từng thành phần và cấu trúc phần mềm chính, các ràng buộc về hoạt động của chúng, xác định các yếu tố phụ thuộc và tương tác của chúng (input và output). Đối với mục đích của chúng ta chỉ là xem xét kiến trúc phần mềm từ góc độ tổ chức firmware thành các software layer riêng biệt có giao diện (interface) được cụ thể hóa theo hợp đồng để cải thiện khả năng port và tái sử dụng mã. Mỗi phần mềm có một chức năng cụ thể, chẳng hạn như điều khiển trực tiếp phần cứng (hardware) của bộ vi điều khiển, chạy phần mềm trung gian (middleware), hoặc chứa mã ứng dụng của hệ thống (system application code). Phần mềm có cấu trúc phù hợp có thể đem lại nhiều lợi thế cho các nhà phát triển
Đầu tiên, một kiến trúc phân lớp (layered architecture) có thể cho ranh giới chức năng giữa các thành phần khác nhau trong phần mềm. Lấy ví dụ, low-level driver code hoạt động của vi điều khiển. Gồm driver code trực tiếp trong application code dính liền bộ vi điều khiển với application code. Vì application code thường chứa các thuật toán có thể được sử dụng trên nhiều sản phẩm, việc trộn lẫn code vi điều khiển cấp thấp sẽ gây trở ngại và tốn thời gian sử dụng lại code. Thay vào đó, một nhà phát triển với kiến trúc phần mềm phân lớp có thể tách application code và low-level code, cho phép cả hai layer được dùng lại trong các ứng dụng khác hoặc trên phần cứng khác.
Thứ hai, kiến trúc phân lớp gợi ý những vị trí cần tạo giao diện trong phần mềm. Với một nhóm phát triển để tạo firmware có thể sử dụng lại, thì cần một ranh giới xác định nơi một giao diện có thể được tạo ra mà vẫn nhất quán và không đổi theo thời gian. Giao diện chứa các khai báo (declaration) và function prototype để điều khiển phần mềm ở các layer thấp hơn.
Thứ ba, kiến trúc phân lớp cho phép ẩn thông tin trong ứng dụng khỏi các khu vực khác có thể không cần truy cập vào. Hãy xem xét ví dụ với low-level driver. Application code có thực sự cần biết chi tiết triển khai làm thế nào driver hoạt động không? Chắc chắn là ai đó làm ở application level sẽ muốn có một function đơn giản để gọi, với kết quả mong muốn xảy ra đằng sau. Đây là ý tưởng đằng sau sự trừu tượng, ẩn hành vi thực thi khỏi người lập trình và chỉ đơn giản là cung cấp cho họ một hộp đen (black box). Phát triển một kiến trúc phần mềm đơn giản có thể giúp các nhà phát triển tận dụng những lợi ích này.
Các nhà phát triển đang tìm cách tạo ra portable firmware tuân theo mô hình kiến trúc phần mềm phân lớp (layered software architecture) mà có nhiều mô hình khả thi khác nhau có thể chọn và nhiều mô hình hỗn hợp tùy chỉnh (custom hybird model) mà họ có thể tin tưởng phát triển. Kiến trúc phân lớp đơn giản nhất có thể được nhìn thấy trong Figure 1-9 vẽ driver layer và application layer vận hành trên phần cứng (hardware). Driver layer bao gồm tất cả code cần thiết để bộ vi điều khiển và bất kỳ board hardware kết nối nào khác, chẳng hạn như cảm biến, nút nhấn, v.v. Application code không chứa driver code nhưng có thể truy cập vào low-level hardware thông qua một giao diện driver-layer đã ẩn các chi tiết hardware với nhà phát triển ứng dụng nhưng vẫn cho phép họ thực hiện các function hữu ích.
Mô hình tiếp theo mà một nhà phát triển có thể chọn để triển khai tách phần mềm thành 3-layer, tương tự như Figure 1-10. Trong mô hình 3-layer, driver layer và application layer vẫn tồn tại, nhưng một layer “giữa” (middle) thứ ba được thêm vào. Middle layer có thể chứa phần mềm như real-time operating system (RTOS), USB và/hoặc Ethernet stacks, cùng với file systems. Middele layer chứa phần mềm chưa phải là toàn bộ application code nhưng cũng không điều khiển trực tiếp low-level hardware. Vì lý do này, các thành phần trong layer này thường được gọi là phần mềm trung gian (middleware).
Từ mô hình 3-layer, các nhà phát triển bắt đầu thấy lợi ích khi tách phần mềm thành các layer vận hành tinh vi hơn và thậm chí có thể cung cấp đường dẫn cho các high-level layer vượt qua các layer để truy cập trực tiếp vào các software layer thấp hơn. Các kiến trúc có thể trở nên khá phức tạp và nằm ngoài phạm vi của bài viết này. Hiện tại, một mô hình 4-layer sẽ là một ví dụ phức tạp mà chúng ta sẽ xem xét. Ví dụ: một nhà phát triển có thể quyết định rằng board-support package (BSP) — các mạch tích hợp bên ngoài vi điều khiển — nên được tách biệt khỏi microcontroller driver layer. Dù sao thì BSP cũng phụ thuộc vào microcontroller driver, và để cải thiện khả năng port, nó nên được tách riêng. Điều này dẫn đến một mô hình 4-layer khả dĩ như mô hình ở Figure 1-11.
Nhiều mô hình chính thức xuất hiện để phát triển kiến trúc phần mềm phân lớp, bao gồm mô hình OSI nổi tiếng, chứa trên bảy layer. Một nhà phát triển nên kiểm tra các yêu cầu của họ, khả năng port, nhu cầu tái sử dụng và chọn kiến trúc đơn giản nhất có thể đáp ứng các yêu cầu của họ. Đừng bị cám dỗ xây dựng một kiến trúc phần mềm 30-layer nếu chỉ cần ba layer là đáp ứng đủ các yêu cầu! Mục đích là để tránh code bị phức tạp, đan xen và chồng chéo và phát triển code nhiều layer thay thế!
11. Hardware Abstraction Layers (HAL)
Mỗi layer phần mềm (software layer) có ít nhất một giao diện (interface) cho một layer phần mềm liền kề. Kiểu phần mềm được chứa trong layer tiếp theo xác định tên được đặt cho giao diện. Mỗi layer, nếu được phát triển hợp lý, có thể coi như một hộp đen đối với nhà phát triển. Nó chỉ có đặc tả giao diện mô tả làm thế nào để được hành vi và kết quả cần thiết. Giao diện đem lại nhiều lợi ích, chẳng hạn như:
- Cung cấp một phương pháp nhất quán để truy cập các tính năng
- Trừu tượng hóa chi tiết về code bên dưới hoạt động như thế nào
- Chỉ định vỏ bọc làm giao diện để hợp nhất code không nhất quán thành software layer
Firmware layer thú vị nhất mà các nhà phát triển có khả năng tận dụng là lớp trừu tượng phần cứng – Hardware Abstrction Layer (HAL)5. HAL là một giao diện cung cấp cho nhà phát triển ứng dụng một bộ function chuẩn có thể được dùng để truy cập các chức năng phần cứng mà không cần hiểu chi tiết về cách phần cứng hoạt động.
HAL về cơ bản là các API được thiết kế để tương tác với phần cứng. Một HAL được thiết kế phù hợp cung cấp cho các nhà phát triển nhiều lợi ích, chẳng hạn như một phần mềm:
- có thể port
- có thể tái sử dụng
- có chi phí thấp (kết quả của việc tái sử dụng)
- được trừ trượng hóa (ta không cần biết cách vi điều khiển làm những gì)
- có ít lỗi hơn do sử dụng nhiều lần
- có thể điều chỉnh – scalable (di chuyển sang MCU khác trong cùng một họ)
THUẬT NGỮ CHUYÊN NGÀNH
Driver Layer đề cập đến software layer chứa phần mềm cấp thấp, dành riêng cho vi điều khiển. Driver layer tạo cơ sở mà phần mềm cấp cao hơn tương tác và điều khiển microcontroller
Board-Support Package đề cập đến driver code phụ thuộc vào driver code cấp thấp hơn của microcontroller. Những driver này thường hỗ trợ các IC ngoài như chip EEPROM hoặc chip flash
Middleware đề cập đến software layer có chứa phần mềm phụ thuộc vào các hardware driver nằm bên dưới nó nhưng không chứa trực tiếp application code. Application code thường phụ thuộc vào phần mềm nằm ở layer giữa là middleware.
Application Layer đề cập đến một software layer được dùng cho các mục đích dành riêng cho hệ thống và ứng dụng được tách biệt khỏi phần cứng bên dưới. Application code đáp ứng các tính năng và yêu cầu của sản phẩm cụ thể.
Một HAL được thiết kế yếu có thể gây hậu quả làm tăng chi phí và lỗi phần mềm. Nhà phát triển có thể phải ước rằng họ đã xử lý HAL trước đó. Một kiến trúc phần mềm mẫu sử dụng HAL có thể tương tự Figure 1-12. Chúng ta sẽ thảo luận chi tiết về thiết kế HAL ở các phần sau.
THUẬT NGỮ CHUYÊN NGÀNH
Hardware abstraction layer (HAL) đề cập đến firmware layer thay thế các truy cập hardware-level bằng các lệnh gọi hàm cấp cao hơn.
Application programming interface (API) đề cập đến những function, routine và library được dùng để tăng tốc độ phát triển application software.
12. Application Programming Interfaces (APIs)
Giao diện lập trình ứng dụng – Application Programming Interface, thường được gọi là API6, là một tập những function, routine và library được dùng để tăng tốc độ phát triển application software. Các API thường được phát triển ở software layer cao nhất. Có nhiều trường hợp những nhà phát triển sẽ sử dụng thuật ngữ API để bao gồm cả HAL, vì HAL thật sự là một API chuyên biệt được thiết kế để tương tác phần cứng. Một ví dụ API có thể xuất hiện trong các layer phần mềm như Figure 1-13.
Một ứng dụng cụ thể có thể gồm nhiều thành phần middleware, như là RTOS, TCP/IP stack, file system, v.v. Mỗi thành phần có thể gồm API riêng được liên kết với software package của chúng. Thậm chí có thể có các thành phần cấp ứng dụng (application-level) có API riêng của chúng để tạo điều kiện phát triển nhanh. Quy tắc chung là bất kì chỗ nào bạn thấy hai software layer kế nhau thì ở đó sẽ có một giao diện xác định API hoặc HAL.
13. Tổ chức dự án
Tổ chức một dự án (project organization) có thể giúp cải thiện khả năng port và bảo trì. Có nhiều cách để các nhà phát triển tổ chức phần mềm của họ, nhưng cách dễ nhất là cố gắng tuân theo chồng software layer. Tạo hệ thống tệp (file system) và cấu trúc thư mục dự án (project folder structure) khớp với các layer giúp nó dễ thay thế một folder (một layer) bằng phần mềm mới, phần mềm này cũng sẽ bao gồm các thành phần (component) trong layer đó.
Dự án cũng nên được tổ chức theo cách trong mỗi layer để các module, task và code liên quan khác có thể dễ dàng định vị. Một số nhà phát triển thích tạo các folder cho các module hoặc các thành phần và chứa tất cả cấu hình (configuration), header và source của module trong cùng folder. Tổ chức phần mềm theo cách này giúp bạn thêm và bớt các software module rất dễ dàng. Các nhà phát triển khác lại thích chia nhỏ và giữ các header và source file riêng biệt. Phương pháp được sử dụng không quan trọng bằng sự nhất quán và tuân theo một cùng phương pháp.
Sau đây là một ví dụ về cách tổ chức mà nhà phát triển có thể phải quyết định (chọn dùng) để tổ chức dự án của mình:
- Drivers
- Application
- Task Schedulers
- Protocol Stacks
- Confguration
- Supporting Files and Docs
14. Getting Started Writing Portable Firmware
Những developer nào muốn tái sử dụng phần mềm thành công sẽ có vài thách thức phải vượt qua. Bao gồm:
- Endianess
- Kiến trúc vi xử lý
- Độ rộng bus
- Những tiêu chuẩn mơ hồ
- Ngân sách và thời gian phát triển
- Module hóa
- Code coupling
Trên đây chỉ là một vài tiêu chí. Lúc mới tìm hiểu có thể có quá nhiều thứ choáng ngợp khiến bạn bị stress và bối rối nhiều hơn là chỉ đơn giản viết code chức năng cụ thể, vấn đề này sẽ được nói sau. Chìa khóa thành công để develop portable code là thẩm định rõ firmware hiện tại của bạn hội đủ những tính chất (charateristic) của phần mềm portable tốt như thế nào. Một khi bạn hiểu rõ chúng ta đang ở đâu, chúng ta có thể quyết định nơi muốn tới và tiến hành những bước cần thiết để đến đó.
Để thẩm định hiện nay ta đang ở đâu trong việc develop portable firmware, hãy bắt đầu bằng cách vẽ một diagram như được trình bày ở Figure 1-14. Trên diagram, dán nhãn mỗi tiêu chí đã nêu với một tính chất của portable firmware và chọn tám tính chất quan trọng nhất đối với bạn.
Trong mỗi danh mục đã xác định, một developer có thể đánh giá code của họ thể hiện những thuộc tính (properties) của nó tốt như thế nào. Lấy ví dụ, một developer đang cố gắng chuyển sang viết nhiều portable code hơn có thể tự đánh giá bằng kết quả của một diagram như Figure 1-5.
Lướt qua Figure 1-15 có thể nói cho developer biết rất nhiều thông tin. Đầu tiên, chúng ta có thế mạnh về tài liệu (documentation) và module hóa (modularity). Đó là một bước tiến tuyệt vời để develop portable firmware và chúng ta chỉ mới bắt đầu. Figure này cũng cho thấy điểm yếu của chúng ta nằm ở đâu, chẳng hạn như code coupling và cohesion.
Từ cái nhìn này, bây giờ chúng ta có thể thẩm định ra chỗ nào cần tập trung sự chú ý. Tính chất nào, nếu được cải thiện chỉ một vài điểm, sẽ cải thiện ngoạn mục code của chúng ta? Hãy lấy code coupling làm ví dụ. Nếu một developer định cải thiện tính chất code coupling, họ cần thẩm định xem họ sẽ làm như thế nào. Họ có thể quyết định cách tốt nhất để làm điều này là thực hiện một hoặc nhiều bước sau:
- Lên lịch đánh giá code (Schedule code reviews)
- Tìm một công cụ có thể cung cấp biểu đồ phụ thuộc module (module-dependency graph)
- Sử dụng công cụ biểu đồ phụ thuộc (dependency-graph tool) (có một công cụ không nghĩa là chúng ta có quy định sử dụng nó)
- Develop một kiến trúc cấp cao đã có cân nhắc module coupling
Một developer có thể quyết định rằng việc cải tiến chỉ cần làm một vùng nào đó là đủ tốt rồi. Điểm mấu chốt là chúng ta sẽ không thể bắt đầu viết code hoàn hảo, có khả năng tái sử dụng chỉ ngay trong một đêm. Quy trình này sẽ lặp đi lặp lại và có thể mất một vài năm trước khi các khiếm khuyết được hoàn thiện, nhưng vấn đề này ổn.
Dưới đây là một quy trình mà những developer có thể sử dụng để cải tiến khả năng port của firmware:
- Phân tích những tính chất của code.
- Xác định điểm mạnh và điểm yếu.
- Thẩm định tính chất nào sẽ cải tiến trong ba tháng kế tiếp.
- Xác định cái gì có thể làm để gia tăng việc cản tiến.
- Thực hiện việc cải tiến.
- Sau một khoảng thời gian quy định, lặp lại.
15. Going Further
Đọc về portable và reusable code là một chuyện; thật sự làm lại là một chuyện hoàn toàn khác. Dưới đây là vài lời khuyên theo từng bước mà bạn có thể xài để bắt đầu develop firmware có khả năng port hiệu quả hơn:
- Chọn tiêu chuẩn ngôn ngữ (language standard) sẽ được dùng cho nỗ lực develop của bạn và dành 30 phút mỗi ngày để đọc qua những tiêu chuẩn ngôn ngữ này. Ghi chú những khu vực không được định nghĩa (define) đầy đủ hoặc có thể trở thành yếu điểm.
- Chọn hai hoặc ba trình biên dịch (compiler), như là GCC, Keil và IAR. Download hướng dẫn sử dụng (user manual) của chúng và tài liệu (documentation) về cách họ làm (implement) những phần còn mơ hồ trong tiêu chuẩn được chọn.
- Mua một bản copy của MISRA C/C++ và tập làm quen với những bài tập thực tế tốt nhất được đề xuất.
Phát triển tiêu chuẩn viết code của riêng bạn với các cấu trúc (construct) được cho phép trong một ứng dụng và cách xử lý (handle) các compiler intrinsic và extenstion của trình biên dịch.
Xem lại kiến trúc phần mềm điển hình của bạn. Nó có các layer được định nghĩa tốt không? Mỗi layer có một giao diện (interface) được xác định rõ ràng không? Nếu chưa, thì bây giờ chính là thời điểm hoàn hảo để dành vài phút tái cấu trúc firmware của bạn. (Đừng lo lắng về cách làm giao diện lúc này. Chúng ta sẽ bàn cách thực hiện trong các chương tới.)
Xem lại phần vừa rồi “Getting Started Writing Portable Firmware”. Lấy một tờ giấy và vẽ diagram hình mạng nhện của riêng bạn và xếp hạng mức độ code của bạn thể hiện các tính chất của portable firmware tốt như thế nào. Chọn một hoặc hai tính chất mà bạn cảm thấy sẽ có tác động lớn nhất đến code của mình và tập trung vào việc cải tiến chúng. Định kỳ xem xét và đánh giá lại.
4ISO/IEC/IEEE 42010:2011, Systems and software engineering — Architecture
5http://whatis.techtarget.com/definition/layering