Hỏi - đáp Nơi cung cấp thông tin nghề nghiệp và giải đáp những thắc mắc thường gặp của bạn

Sử dụng C để lập trình nhúng

Nội dung:

  • Kiến thức chung về môi trường phát triển
    • Compiler and linker
    • .prm và map file
    • Programming models.
  • Kiểu dữ liệu cho Embedded.
    • Lựa chọn đúng kiểu dữ liệu
    • Các loại biến.
    • Storage class modifiers
  • Kiến trúc phần mềm
    • Tổ chức file theo module
    • Một số ví dụ cài đặt.

Sử dụng C để lập trình nhúng

  • Ngôn ngữ C được dùng để viết hệ điều hành UNIX bởi Dennis Ritchie vào năm 1971.
  • Một trong những ưu điểm lớn nhất của C là nó không bị ràng buộc bởi một phần cứng hay một hệ thống cụ thể. Điều này khiến cho người phát triển có thể sử dụng để viết chương trình và chạy mà không cần thay đổi cho từng hê thống riêng biệt . Việc buld để chạy cho các hệ thống khác nhau phụ thuộc vào trình biên dịch. Chẳng hạn, chúng ta có thể viết chương trình C sau đó sử dụng các trình biên dịch build và chạy trên windows cũng như Linux.
  • C cũng được gọi là một ngôn ngữ middle-level bởi nó kết hợp giữa ngôn ngữ bậc cao và cũng có thể truy cập rất sâu vào hệ thống như ngôn ngữ bậc thấp (chẳng hạn thông qua con trỏ hay các hàm Assembly nhúng vào chương trình).
  • Để tạo ra mã máy hiệu quả (eficient high level) không những cần có thiết kế chương trình tốt mà còn cần chú ý đến các chi tiết cài đặt nhỏ, đặc biệt là đối với hệ thống nhúng.
  • Bên cạnh ưu điểm, C cũng có những nhược điểm sau:
    • Code lớn và không hiệu quả bằng assembly
    • Không hỗ trợ trực tiếp kiểu stack
    • Khó viết các hàm xử lý ngắt.

Một số lưu ý khi phát triển phần mềm nhúng


Đặc điểm đối với hệ thống nhúng:

  • ROM và RAM hạn chế.
  • Lập trình phụ thuộc phần cứng.
  • Cần đáp ứng chính xác về thời gian (hàm xử lý ngắt, tác vụ…)
  • Nhiều kiểu pointer (far/rom/ui/paged/…)
  • Một số keywords và token đặc biệt (@, interrupt, tiny,..)

Để phát triển tốt phần mềm nhúng bằng ngôn ngữ C cần nắm vững.

  • Thiết kế kiến trúc phần mềm hợp lý.
  • Thành thạo sử dụng các tool và debugging
  • Data types native support
  • Các thư viện chuẩn.
  • Phân biệt rõ về simple code với eficient code.

Một số điểm có thể tạo ra “sự khác biệt”

  • Inline assembly
  • Hàm xử lý ngắt.
  • Assembly language generation
  • Thư viện chuẩn
  • Startup code
  • Sử dụng các từ khóa near và far để tăng hiệu suất của biến khi biến nằm ở các vùng nhớ gần hoặc xa vùng đang sử dụng

Kiến thức chung về môi trường phát triển Compiler & Linker

Compiler

Compiler (trình biên dịch) là một chương trình máy tính làm công việc dịch các chuỗi câu lệnh viết bằng ngôn ngữ lập trình thành chương trình tương đương nhưng viết dưới dạng ngôn ngữ máy tính. Chương trình mới này được gọi là mã đối tượng (object code).

Quá trình biên dịch thông qua nhiều bước:



Front End

Trình biên dịch sẽ thực hiện phân tích từ vựng, chia các dòng mã nguồn thành từng phần nhỏ gọi là các thẻ khóa. Sau đó nó sẽ thực hiện việc phân tích cú pháp theo quy tắc để phát hiện lỗi. Tiếp theo là phân tích ý nghĩa nhằm biết được ý nghĩa của mã nguồn và chuẩn bị cho ra kết quả. Giai đoạn này sẽ thông báo tất cả các lỗi có thể gây ra trong mã nguồn.

Code Generator và Backend

Trong giai đoạn này, trình biên dịch sẽ thực hiện tạo ra các mã máy và thực hiện optimized code theo các tùy chọn người lập trình cài đặt cho trình biên dịch.

Linker

Linker là công cụ kết hợp các tập tin object và tập tin nén, sắp xếp lại dữ liệu của các tập tin đó và liên kết chúng lại với nhau thành tập tin thực thi.



Linker còn có tạo ra bản đồ liên kết (link map) vào tập tin output chuẩn. Bản đồ này sẽ cung cấp các thông tin về sự ánh xạ của tập tin object được ánh xạ vào bộ nhớ như thế nào, giá trị cấp pháp ra sao…
Nói một cách “nôm na”, Compiler sẽ tạo ra các file mã máy mà không cần biết nó sẽ được bố trí vào bộ nhớ như thế nào. Việc bố trí các file mã máy này sẽ là công việc của Linker.

Kiểu dữ liệu cho Embedded.



Có 3 nguyên tắc để chọn đúng kiểu dữ liệu:

  • Chọn kiểu dữ liệu nhỏ nhất đảm bảo hoàn thành công việc.
  • Sử dụng kiểu unsigned nếu có thể.
  • Sử dụng “cats” trong biểu thức để giảm kiểu dữ liệu.
  • Do một số trình biên dịch và một số hệ thống khác nhau sẽ có thể có kích thước dữ liệu khác nhau. Tránh việc sử dụng các loại cơ bản (char, int, short, long) trong các ứng dụng mà nên sử dụng fypedef để fixed size.



Cách thực hiện:

  • Tạo một file định nghĩa lại các kiểu dữ liệu, thường đặt tên là stdint.h
  • Một số phong cách định nghĩa kiểu dữ liệu được mô tả như hình dưới. Mọi người có thể lựa chọn cho mình một kiểu phù hợp.

Nên chú ý đến việc sắp xếp các biến để tối ưu truy cập. Chẳng hạn đối với hệ thống 32bit, nếu khai báo giống như biên trái thì biến var3 sẽ bị chia ra 2 ô khác nhau => hiệu suất kém hơn. Vì vậy có thể sắp xếp như bên phải.

Một số kiểu biến cần quan tâm gồm: stacic, volatile, const

Static

Đặc điểm của khai báo static

  • Do được khai báo static nên chỉ được khởi tạo 1 lần duy nhất và tồn tại suốt thời gian chạy của chương trình. Giá trị của biến count sẽ được tích luỹ mỗi khi hàm count được gọi.
  • Do khai báo trong nội bộ hàm count nên biến chỉ có thể được nhìn thấy bởi các câu lệnh trong hàm count. Nói cách khác, nó là 1 biến nội bộ (local variable).

Các trường hợp sử dụng

  • Khai báo bên trong 1 hàm, dùng để trao đổi dữ liệu trong bản thân hàm. Các phần khác của chương trình không thể truy cập được vào biến này. Điều này thể hiện tính đóng gói của chương trình
  • Khai báo như một biến toàn cục ở trong một module, dùng để trao đổi dữ liệu giữa các phần của module.

Volatile

Sử dụng để khai báo các biến mà giá trị có thể thay đổi bởi các tác nhân bên ngoài (ngoài code) ví dụ như khai báo các thanh ghi, các biến trong hàm ngắt, các nhân tố phần cứng tác động lên như thanh ghi, ram …

Một ví dụ kinh điển về khai báo thanh ghi:

Đối với các biến mà giá trị của nó có thể thay đổi không báo trước, khi trình biên dịch tiến hành tối ưu mã, nó sẽ thấy giá trị của biến không thay đổi trong hàm (mà chỉ thay đổi do các yếu tố bên ngoài như ngắt hay trao đổi đa luồng). Vì vậy, trình biên dịch sẽ loại bỏ phần thay đổi giá trị và coi nó như biến tĩnh. Khi đó giá trị của biến này sẽ không thay đổi ngay cả khi trong hàm ngắt có phần tác động vào nó.

Có 3 loại biến mà giá trị của nó có thể thay đổi không báo trước là :

  • Memory-mapped peripheral registers (thanh ghi ngoại vi có ánh xạ đến ô nhớ)
  • Biến toàn cục được truy xuất từ các tiến trình con xử lý ngắt (interrupt service routine)
  • Biến toàn cục được truy xuất từ nhiều tác vụ trong một ứng dụng đa luồng.

Chú ý, biến kiểu volatile sẽ không được trình biên dịch tối ưu hóa.

Cấu trúc souce code

Các chương trình sẽ có cấu trúc cơ bản sau:

  • Ngay sau khi reset, chương trình sẽ thực hiện khởi tạo thiết lập phần cứng (xung nhịp clock, cấu hình các chân GPIO, khai báo các ngoại vi sẽ sử dụng trong chương trình …).
  • Bước tiếp theo là là cấu hình phần mềm, cài đặt chế độ cho các ngoại vi, đăng ký các hàm xử lý ngắt…
  • Sau khi đã xong các bước thiết lập và khởi tạo, chương trình sẽ đi vào vòng lặp vô hạn. Trong vòng lặp này sẽ có các hàm xử lý các sự kiện xảy ra đối với hệ thống. Trong vòng lặp vô hạn thường có thêm Watchdog timer để phát hiện và reset chương trình khi bị treo (chương trình quá thời gian quy định ở watchdog).
  • Kiến trúc phần mềm
    Đối với các dự án lớn, việc thiết kế kiến trúc phần mềm là rất quan trọng.
    Thứ nhất, nó phân chia chương trình thành các tầng có chức năng riêng biệt để dễ dàng quản lý bảo trì và kiểm soát lỗi.
    Ưu điểm thứ 2 là nó tối ưu tính sửa dụng lại của chương trình khi thực hiện thay đổi nền tảng phàn cứng.
    Ta lần lượt phân tích từng ý nghĩa.
    Đối với việc phân chia thành các tầng, thông thường sử dụng 3 tầng bao gồm:
    • Foundatation: Chứa các phần source code riêng biệt cho từng nền tảng phần cứng và không thể thay đổi theo ý người phát triển. Các source code này thường được cung cấp bởi hãng cung cấp giải pháp dưới dạng các driver cho chức năng cơ bản GPIO, UART, Timer, I2C…, tóm lại nó dùng để thiết lập các chế độ hoạt động của từng nền tảng phần cứng.
    • Translation: Chứa các phần source code được trừu tượng hóa, cài đặt các thuật toán nâng cao và không phụ thộc phần cứng. Đây là phần chuyển tiếp giữa tầng dưới (Foundatation) và tầng ứng dụng. Nó sử dụng các hàm từ tầng Foundatation và đưa ra các API cho tầng ứng dụng.
    • Application: Chứa phần cài đặt cho từng ứng dụng cụ thể, tầng này sử dụng các API từ tầng Translation và hạn chế truy cập trực tiếp phần cứng.
  • Nếu cài đặt đúng theo kiến trúc như trên, khi chuyển đổi nền tảng phần cứng ta sẽ tối ưu sử dụng lại source code mà không phải viết lại từ đầu.
    • Tầng Foundation do phụ thuộc hoàn toàn đối với từng nền tảng nên sẽ bị thay thế bằng nền tảng mới.
    • Tầng Translation sẽ được chỉnh sửa lại cho phù hợp với nền tảng mới (thêm bớt modue so với nền tảng cũ).
    • Tâng Application có thể giữ nguyên.



  • Có thể tham khảo cách bố trí các tầng như hình dưới

Cài đặt đối với các ứng dụng sử dụng SCI như sau:

Tầng Foundataion cung cấp các hàm cài đặt module SCI, phần này phụ thuộc vào từng nền tảng cụ thể.

Tầng Terminal cung cấp các hàm tổng quát, không phụ thuộc phần cứng. Cài đặt dựa vào các hàm cung cấp từ tầng dưới.

Tầng App sẽ sử dụng các hàm cung cấp từ terminal để dùng cho các ứng dụng cụ thể.

Via Vimach.net