Chương trình C được viết để thực thi trên vi điều khiển thường mang tính đặc thù cao bởi tính hoạt động liên tục, tài nguyên hạn chế, cần các tương tác với phần cứng ngoại vi và yêu cầu thời gian thực. Để xây dựng một chương trình bạn cần hiểu rõ được cấu trúc các thành phần. Từ đó, bạn có thể nhận biết, phân biệt, đọc hiểu mã code, viết và tối ưu chương trình một cách hiệu quả. Trong bài viết này, chúng ta sẽ cùng tìm hiểu chi tiết về cấu trúc cơ bản của một chương trình C cho các ứng dụng nhúng, giúp bạn xây dựng nền tảng vững chắc cho mọi dự án của mình.
Chúng ta sẽ làm quen với một chương trình C đơn giản sau, các bạn cùng đọc qua và xác định xem các thành phần của chương trình này nhé.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
#include <stdint.h> #include "stm32f1xx_hal.h" #define GPIO_NUMBER 16u typedef struct { uint32_t CRL; uint32_t CRH; uint32_t IDR; uint32_t ODR; uint32_t BSRR; uint32_t BRR; uint32_t LCKR; } GPIO_TypeDef; uint32_t btn_counter = 0; void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin); int main(void) { while (1) { btn_counter++; HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13); //GPIOC and GPIO_PIN_13 are defined in the lib files of ST HAL_Delay(300); } } void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin) { uint32_t odr; odr = GPIOx->ODR; GPIOx->BSRR = ((odr & GPIO_Pin) << GPIO_NUMBER) | (~odr & GPIO_Pin); } |
Chương trình trên là một ví dụ đơn giản về cách điều khiển ngoại vi GPIO (General Purpose Input/Output) trên vi điều khiển STM32F1xx sử dụng thư viện HAL (Hardware Abstraction Layer) của STMicroelectronics. Chức năng chính của đoạn chương trình này là điều khiển cho chân GPIO thứ 13 trên vi điều khiển nhấp nháy (toggle) liên tục với một độ trễ 300ms. Ví dụ trên là một chương trình đơn giản nhưng có đầy đủ cấu trúc cơ bản của một chương trình Nhúng, đó là:
- Khai báo thư viện
- Khai báo toàn cục
- Hàm main
- Vòng lặp vô hạn
- Hàm chức năng
Bên cạnh đó, một thành phần rất quan trọng mà chúng ta thường sử dụng trong chương trình Nhúng giúp vi điều khiển đáp ứng nhanh chóng với các sự kiện bên ngoài hoặc bên trong hệ thống đó là:
- Trình xử lý ngắt
1. Khai báo thư viện
Thư viện là các file chứa khai báo và định nghĩa của các kiểu dữ liệu, các biến, các hàm chức năng liên quan đến mộtđối tượng cụ thể như các biểu thức toán học, các thao tác về kiểu dữ liệu, hoặc các điều khiển cho một module phần cứng hay cảm biến chuyên dụng… Thư viện có thể được xem như một bộ công cụ hoặc tập hợp các hướng dẫn tham khảo có sẵn, giúp người lập trình kế thừa lại các đoạn mã đã được viết và kiểm thử từ trước. Nhờ đó, việc phát triển trở nên nhanh chóng và hiệu quả hơn mà không cần phải xây dựng lại mọi thứ từ đầu.
Khai báo thư viện là việc liên kết các file thư viện vào chương trình của chúng ta, từ đó, chúng ta có thể gọi sử dụng các hàm chức năng, các biến tại file thư viện. Thư viện được khai báo bằng chỉ thị tiền xử lý chèn tệp (#include). Thư viện bao gồm 2 loại:
- Thư viện hệ thống
- Thư viện người dùng. (Đọc thêm tại mục 2.3 tại bài viết Chỉ thị tiền xử lý)
Ngoài ra, đối với các bạn lập trình nhúng và thiết bị IoTs, việc kế thừa là rất quan trọng. Khi làm việc với các cảm biến, module, ngoại vi, chúng ta thường kế thừa các hàm chức năng khai báo, cấu hình, điểu khiển, đọc ghi và truyền nhận dữ liệu thông qua các thư viện. Vì vậy, các bạn phải làm quen với việc đọc hiểu thư viện, biết cách sử dụng và từ đó có thể tự tay viết thư viện cho các module, cảm biến, ngoại vi mà mình sử dụng.
Một số thư viện C của hệ thống cần chú ý đối với các bạn mới bắt đầu với ngôn ngữ C:
<stdio.h>: Thư viện hỗ trợ việc nhập xuất dữ liệu (scanf, printf, fopen, fclose…)
<string.h>: Thư viện hỗ trợ người dùng làm việc với chuỗi dữ liệu (strlen, strcmp, strcpy, strstr, memset…)
<math.h>: Thư viện hỗ trợ các hàm công thức toán học cơ bản (pow, sqrt, sin, cos…)
<stdint.h>: Thư viện hỗ trợ định nghĩa các kiểu số nguyên với kích thước khác nhau (int8_t, uint8_t, INTN_MIN, INTN_MAX…)
Đây chỉ là một số thư viện mình đề cập trong ngân hàng thư viện C tiêu chuẩn khác để các bạn hình dung được các mảng nội dung mà thư viện đề cập đến. Các bạn có thể xem danh sách của các thư viện ngôn ngữ C tại đây.
2. Khai báo toàn cục
Trong một chương trình, chúng ta cần khai báo các biến, các hàm, macro, kiểu dữ liệu… Tại phần này, mình sẽ liệt kê các khai báo và định nghĩa theo một trình tự sau đây:
- #define: được sử dụng để định nghĩa macro. Bao gồm 2 loại: Object-like macro và Function-like macro. Tại quá trình tiền xử lý, trình biên dịch sẽ thay thế những định danh bạn đang sử dụng trên toàn bộ chương trình bằng chính giá trị/biểu thức của chúng. (Đọc thêm tại mục 2.1 tại bài viết Chỉ thị tiền xử lý)
- Ví dụ: Sau quá trình tiền xử lý, dòng lệnh thứ 33 trong chương trình trên sẽ trở thành:
1 |
GPIOx->BSRR = ((odr & GPIO_Pin) << 16u) | (~odr & GPIO_Pin); |
- typedef: đặt tên cho các kiểu dữ liệu để người dùng dễ sử dụng
- Ví dụ: Với struct GPIO_TypeDef, nếu chúng ta không sử dụng typedef, các bạn sẽ phải sử dụng tên kiểu dữ liệu là “struct GPIO_TypeDef”. Khi chương trình sử dụng rất nhiều kiểu dữ liệu struct, người lập trình có xu hướng muốn sử dụng tên kiểu dữ liệu ngắn gọn hơn, quen thuộc hơn (như với kiểu dữ liệu cơ bản) bằng cách khai báo struct với typedef. Với chương trình ví dụ, chúng ta sẽ sử dụng tên kiểu dữ liệu “GPIO_TypeDef” thay vì “struct GPIO_TypeDef”.
- Khai báo biến toàn cục, đó là những biến mà bạn có thể sử dụng xuyên suốt chương trình trong tất cả các hàm.
- Khai báo nguyên mẫu hàm. Việc này cung cấp cho trình biên dịch tên các hàm chức năng, kiểu dữ liệu mà hàm trả về, số lượng và kiểu dữ liệu của các tham số đầu vào. Đơn giản đó như một lời giới thiệu cho trình biên dịch rằng có hàm này tồn tại trước khi hàm đó được định nghĩa chi tiết trong file Source.
- Lưu ý: Khai báo nguyên mẫu hàm được đặt trước hàm main và các hàm chức năng khác (hoặc đặt trong các file Header .h và được include vào file Source .c). Nếu không khai báo nguyên mẫu hàm, nhớ rằng các bạn phải đặt định nghĩa của hàm trước khi hàm được gọi tới.
3. Hàm main()
Hay còn gọi là hàm chương trình chính. Xét về chương trình người lập trình, main() là nơi bắt đầu thực thi của chương trình C. Main() sẽ gọi các hàm chức năng khác để thực thi các tác vụ cụ thể từ cấu hình. thiết lập môi trường, đến điều khiển các khối ngoại vi trên Vi điều khiển.
- Đối với việc lập trình cho hệ thống nhúng và các thiết bị IoTs, tại hàm main, chúng ta sẽ thường gọi đến các hàm cấu hình:
- Cấu hình thực hiện khởi động lại (Reset)
- Cấu hình hệ thống xung nhịp (System Clock)
- Cấu hình hoạt động cho các khối ngoại vi (Peripheral)
- Cấu hình cho các thiết bị/module/cảm biến mà hệ thống sử dụng
- Một thành phần không thể thiếu tại hàm main, chính là vòng lặp vô hạn – nơi thực thi logic và chức năng của hệ thống.
4. Vòng lặp vô hạn
Trong một hệ thống Nhúng, việc duy trì hệ thống hoạt động liên tục và ổn định là rất quan trọng. Vòng lặp vô hạn thường được sử dụng, hệ thống sẽ chỉ dừng lại khi có sự cố nghiêm trọng xảy ra hoặc ngắt nguồn cung cấp. Tại vòng lặpvô hạn, các hàm chức năng (hay hàm con) sẽ được gọi để hệ thống thực thi. Nếu không có bất cứ sự cố nào (ví dụ: lỗi phần cứng, mất điện, hoặc lỗi phần mềm nghiêm trọng gây treo hệ thống), chương trình sẽ chạy các câu lệnh, các hàm trong vòng lặp này mãi mãi. Một số vòng lặp vô hạn mà chúng ta sẽ thường xuyên gặp như:
- while (1) { /* … */ }
- do { /* … */ } while (1);
- for (;;) { /* … */ }
=> Đây là các vòng lặp vô hạn vì điều kiện để nó hoạt động luôn đúng.
Mình có nhắc đến “Hàm main sẽ gọi các hàm chức năng”. Có nghĩa là, định nghĩa các hàm đã được xây dựng, bạn chỉ việc gọi tên các hàm tại hàm main. Các định nghĩa hàm có thể nằm tại chương trình chính mà bạn đang viết, hay nằm tại một thư viện đã được bạn liên kết/include ở đầu chương trình.
5. Hàm chức năng:
Một số hàm chức năng mà người lập trình nhúng, IoTs cần nắm rõ:
- Hàm khởi tạo và xoá khởi tạo: Init(), DeInit();
- Hàm nhập xuất, giao tiếp truyền nhận dữ liệu: Read(), Write(), Transmit(), Receive();
- Hàm điều khiển, giám sát: Set(), Get();
- Hàm nhận trạng thái và báo lỗi: GetState(), GetError().
—
Tùy tính năng và cách hoạt động của hệ thống mà người lập trình mong muốn, hệ thống vẫn có thể hoạt động khi chúng ta không sử dụng đến vòng lặp vô hạn. Khi đó, chúng ta sẽ sử dụng đến trình xử lý ngắt. Thông thường, tại hàm main trong chương trình chính, từng câu lệnh sẽ được thực thi một cách tuần tự từ trên xuống, khi gặp phải vòng lặp vô hạn, từng câu lệnh trong vòng lặp được thực thi cũng theo trình tự từ trên xuống, chu trình này được lặp đi lặp lại không xác định thời gian dừng.
=> Tuy nhiên với các chương trình trong hệ thống Nhúng và thiết bị IoTs, thiết bị chúng ta sử dụng mang đặc điểm tự động, chúng cần nhận biết và đáp ứng được các sự kiện tác động từ môi trường bên ngoài. Khi đó, chúng ta cần nhắc đến trình xử lý ngắt.
6. Trình xử lý ngắt:
Trình xử lý ngắt là một thành phần rất quan trọng. Vậy trước hết, ngắt là gì? Các bạn có thể dễ dàng hiểu được, ngắt là một sự kiện tác động lên thiết bị của chúng ta mà vi điều khiển có thể nhận biết được. Sau đó, VDK sẽ tạm ngưng luồng chương trình hiện tại và nhảy đến một hàm khác để thực thi nhằm phản hồi, đáp ứng cho sự kiện vừa diễn ra. Khi thực hiện xong, VDK sẽ trở lại luồng thực thi ban đầu để tiếp tục thực hiện chương trình một cách tuần tự. Việc thay đổi hướng thực thi để đáp ứng phản hồi một sự kiện đó, người ta gọi là trình xử lý ngắt.
Ví dụ, trong một thiết bị đồng hồ thông minh, bạn đang bật tính năng trình phát nhạc, một thiết bị khác thực hiện việc gọi điện đến SDT của bạn. Cuộc gọi đến chính là một sự kiện diễn ra, hay còn gọi là ngắt. Thiết bị dừng lại việc phát nhạc, và rung chuông cuộc gọi đến, đó chính là một trình xử lý ngắt. Nếu bạn nhấn chọn nghe máy, một sự kiện lại xảy ra, sau đó thiết bị hỗ trợ để bạn trò chuyện với họ, chính là một trình xử lý ngắt khác. Sau khi kết thúc cuộc gọi, nhạc tiếp tục phát lên, thiết bị đã trở về với luồng thực thi ban đầu của nó.
Các bạn có thể đọc thêm các bài viết khác giới thiệu về ngắt và hướng dẫn thực hành trên Vi điều khiển STM32 dưới đây.
Quá trình thực hiện ngắt của vi điều khiển – MCU Interrupt processing
Tìm hiểu System timer, ngắt SysTick và sử dụng HAL_Delay trong trình phục vụ ngắt VĐK STM32.
Cách phân biệt các chân sinh ra ngắt trên vi điều khiển STM32
Một số lỗi thường gặp khi sử dụng ngắt trong lập trình nhúng
Mình vừa giới thiệu đến mọi người cấu trúc chung cho một chương trình C cơ bản cho hệ thống Nhúng và thiết bị IoTs. Khi làm việc với đội nhóm, các bạn nên thống nhất một trình tự chung cho cấu trúc chương trình của mình. Khi đó thuận tiện hơn cho việc đọc hiểu chương trình, kế thừa các project hay cập nhật phiên bản tốt hơn cho chương trình. Việc nắm chắc cấu trúc của một project sẽ giúp bạn làm việc một cách chuyên nghiệp và hiệu quả hơn.
Chúc các bạn thành công!
N.T.Nhien