C trong lập trình nhúng

Lập trình C trong embedded, đặc biệt với các vi điều khiển như ARM thì nó hơi khác một chút so với lập trình C thông thường, do nó có phải thao  tác nhiều tới các thanh ghi, hôm nay mình sẽ nói lại tổng quan một  chút về lập trình nhúng và các ví dụ cụ thể khi dùng C trong Embedded như thế nào

Mục tiêu

  • Các thành phần của C, syntax rule
  • Các kiểu dữ liệu cơ bản: char, short,long (unsigned and signed)
  • Tìm hiểu về hàm main trong chương trình được thực hiện như thế nào
  • Tìm hiểu về variable(biến) và expression(biểu thức)
  • Hiểu về cách sử dụng printf và scanf với I/O
  • Hiểu điều kiện if và while và cách sử dụng chúng trong các biểu thức điều kiện
  • Hiểu được hàm và cách sử dụng chúng với input và output

 

Giới thiệu về C

Về lịch sử của C mình tạm không bàn tới, nhưng có một lưu ý là lập trình cấu trúc thường có 3 dạng cơ bản

  • Sequential (tuần tự): chương trình thực hiện tuần tự, việc A trước, sau đó đến việc B
  • Decision (quyết định): chương trình dựa trên các điều kiện để thực hiện công việc, ví dụ với điều kiện 1 thì làm việc A, điều kiện 2 thì làm việc B
  • Iterative (lặp lại): chương trình thực hiện lặp lại 1 công việc cho đến khi hoàn thành, ví dụ như việc phải phải liên tục làm việc(đi bộ) cho tới khi đạt được kết quả (đến siêu thị)
  • Interrupts (ngắt) : chương trình thực hiện 1 nhiệm vụ khi có sự tác động tới hardware, ví dụ để nhận được dữ liệu từ GPS thì MCU phải thực hiện ngắt nhận dữ liệu

 

Flowchart

Nói về flowchart thì trong lúc học ở trường về môn lập trình C thì hầu như lúc nào cũng phải làm phần này, trong các bài kiểm tra cuối kỳ chắc chắn thì nó cũng nằm trong 1 câu nào đó của bài thi. Tại sao lại cần có phần này, để đơn giản các bạn có thể hình dung flowchart nó giống như bản thiết kế của một ngôi nhà vậy, để có thể xây nhà thì bạn cần phải có thiết kế mọi thứ ra, sau đó mới bắt đầu xây dựa trên thiết kế đúng không ? Trước khi lập trình thì cũng y như thế, chúng ta phải xác định được cần phải làm gì và các bước để thực hiện việc đó.

Trong khi vẽ flowchart thì có 7 khối thông dụng thường sử dụng như hình bên trên

  • Entry point: là phần bắt đầu của phần mềm, thông thường nó là phần begin của mỗi flowchart
  • Conditional: tương ứng với câu lệnh rẽ nhánh if
  • Input/Output: xác định đầu ra đầu vào hệ thống
  • Process: quá trình xử lý 1 việc gì đó liên tục
  • Connectors: mũi tên đi vào tương ứng với thuật toán, mũi tên đi ra tương ứng với lệnh jump hoặc goto
  • Function: thường dùng để mô tả một function(có trả về parameter), subroutine(thường dùng trong asembly) hoặc procedure(thường không trả về param)
  • Exit point: tương ứng với end

Khi thiết kế phần mềm cho vi điều khiển khoảng 1000 dòng code trở lên hoặc một hệ thống lớn với cả triệu, tỉ dòng code thì rất khó để duy trì một cấu trúc nhất quán(consisten structure), do đó cần phải có cấu trúc rõ ràng cho chương trình (structured program).

Với một structured programs trong C thường có 3 dạng cơ bản

  • Sequence
  • Conditional
  • While-loop

 

Thiết kế máy nướng bánh

Trong ví dụ này ta sẽ sử dụng flowchart để thiết kế thuật toán điều khiển máy nướng bánh.

Ở flowchart bên trên ta có thể biết được cách hoạt động của máy như sau:

  • Có 1 nút để người dùng khởi động máy, nút này luôn ở trạng thái chờ, nghĩa là luôn kiểm tra xem người dùng có nhấn hay chưa(pressed/not pressed), nếu có nhấn thì thực hiện nướng bánh (cook)
  • Trong khi máy nướng bánh thì hoạt động bên trong là luôn kiểm tra nhiệt độ hiện tại của máy, nếu nhỏ hơn nhiệt độ làm chín bánh(desired) thì tự động tăng nhiệt lên, khi quá nhiệt thì tắt không tăng nhiệt nữa

Vậy trước khi viết một phần mềm (ta thường gọi là viết một chương trình C) thì cần ít nhất là 4 bước

  1. Xác định input/output là gì, hệ thống sẽ hoạt động như thế nào, các ngưỡng giá trị và tầm quan trọng của chúng
  2. Đưa ra danh sách các dữ liệu cần thiết, xác định được cấu trúc dữ liệu là như thế nào, ý nghĩa của nó, làm sao để thu thập và thay đổi nó
  3. Phát triển các thuật toán, chuỗi hoạt động mà chúng ta cần thực hiện để hệ thống hoạt động bằng flowchart hoặc pseudo code
  4. Debug để tăng chất lượng và hiệu quả của code

Thuật ngữ thường gặp

Trước khi nói tới cấu trúc của C thì mình nói qua một chút về mối quan hệ giữa C lại liên quan tới vi điều khiển và bằng cách nào để người ta đưa code C vào trong vi điều khiển

Compiler(trình biên dịch): là thứ dùng để chuyển ngôn ngữ bậc cao (C) về object code(định dạng mã máy có thể đọc được), để hiểu đơn giản thì compiler này nó cũng giống như phần mềm lacviet dịch tiếng anh(C) về tiếng việt(mã máy). Bạn có thể tìm hiểu thêm có thể xem bài compiler
Ví dụ: C code (z = x+y;) → Assembly code (ADD R2,R1,R0)  → Machine code (0xEB010200)

Assembler: là thứ dùng để chuyển ngôn ngữ assembly về object code, các bạn có thể xem qua bài lập trình hợp ngữ với ARM để hiểu thêm về assembly, assembler

Ví dụ: Assembly code (ADD R2,R1,R0)  → Machine code (0xEB010200)

Interpreter(trình thông dịch): thực hiện trực tiếp trên ngôn ngữ bậc cao, tuy nhiên nó sẽ chậm hơn là compile code

Ví dụ: khi gười dùng gõ một lệnh nào đó trên máy tính chẳng hạn thì Interpreter sẽ thực thi ngay lập tức lệnh mà họ vừa gõ vào, đơn giản dễ thấy nhất là dùng debugger với các command của nó

Linker: là phần mềm để build hệ thống bằng cách kết nối(linking) các thành phần của software lại với nhau

Ví dụ: Ta thường thấy trong một chương trình uart thì cần có 3 file phải compile và link với nhau là startup.s, uart.c, main.c
Loader: thực hiện đưa object code vào bộ nhớ (memory), trong hệ thống nhúng thì thường loader sẽ đưa object code vào trong flash ROM.

Debugger: là thiết bị gồm phần cứng và phần mềm dùng để đảm bảo được hệ thống hoạt động một cách chính xác, yêu cầu quan trọng đối với debugger là phải gỡ lỗi tốt và khả năng quan sát được thông tin.

Cấu trúc và cách tổ chức

Punctuation

Khác với lập trình hợp ngữ assembly thì C có thể được xem là ngôn ngữ khá là tự do và thoải mái, chúng ta có thể viết một chương trình theo nhiều style khác nhau ví dụ dưới đây là 3 phong cách code khác nhau cho một chương trình trả về một số bất kỳ

Style 1

unsigned long M=1;
unsigned long Random(void){M=1664525*M+1013904223;return(M);}

Style 2

unsigned long M=1;
unsigned long

Random(void){
 M = 1664525*M
 +1013904223;
 return(M);
}

Style 3

unsigned long M=1;
unsigned long Random(void){
 M = 1664525*M+1013904223;
 return(M);}

Trong chương trình trên thì M là biến(variable), Random là hàm(function) và toán tử(operator) là phép nhân *, ngoài ra còn có sử dụng dấu cách, dấu chấm phẩy, ngoặc nhọn ngoặc vuông ..

Các dấu chấm câu thường được dùng trong lập trình C các bạn có thể tham khảo

DấuÝ nghĩa
;Kết thúc câu lệnh
:Định nghĩa nhãn label
,Phân chia các thành phần của 1 list
( )Bắt đầu và kết thúc của một chuỗi parameter
{ }Bắt đầu và kết thúc của compund statement
[ ]Bắt đầu và kết thúc của array index
" "Bắt đầu và kết thúc của một chuỗi
' 'Bắt đầu và kết thúc của character constant

Những dấu chấm câu (Punctuation marks) rất quan trọng trong C, nó là khởi nguồn của những lỗi thường gặp nhất khi lập trình, kể cả đối với người có hay không có kinh nghiệm.

Cấu trúc chương trình C

Tiếp tục một ví dụ minh họa cụ thể về cấu trúc của chương trình C cho vi điều khiển để ta có thể hình dung rõ hơn

Nhìn vào ví dụ trên ta có thể hình dung ra một chương trình C gồm 4 phần

  • Phần 0 được gọi là documentation section sẽ mô tả thông tin nội dung của file này, thường thì là file này viết ra cho mục đích gì, ai là người code, thời gian code và các thông tin cập nhật
  • Phần 1 được gọi là preprocessor directives(tiền chỉ thị), thường thì ta thấy trong chương trình C là các phần như #include để có thể kết nối với các modul khác. Ở đây khi dùng thư viện trong <> nghĩa là dùng các thư viện của hệ thống, còn dùng “” nghĩa là dùng các thư viện của user(cụ thể là uart.h)
  • Phần 2 được gọi là global declarations đây là phần khai báo các biến toàn cục (global variables) và các hàm sẽ được sử dụng trong chương trình(function prototypes), phần này mình sẽ nói cụ thể hơn ở các bài sau
  • Phần 3 được gọi là functions đây là chương trình chính, mỗi chương trình C sẽ có duy nhất một hàm main, hàm này sẽ xác định đâu là chỗ bắt đầu thực hiện

Một cái cần lưu ý khi lập trình C nữa là comment, có 2 loại comment:

  • Một là loại giải thích cách sử dụng phần mềm (thường kí hiệu // hoặc /**/) dùng để mô tả, chú thích code, loại này có thể nằm ở trên cùng (phần 0) để mô tả về code.
  • Hai là loại được dùng để hỗ trợ developer có thể thay đổi, debug và mở rộng khi cần phải nâng cấp, thường thấy nhất là các comment nằm bên phải của mỗi dòng code

Preprocessor directives (chỉ thị tiền xử lý) thường bắt đầu với #, như tên gọi của nó là tiền xử lý nghĩa là khi biên dịch nó sẽ thực hiện xử lý cái này đầu tiên. Chúng ta sẽ tạo ra một macro sử dụng #define để xác định hằng số

Ví dụ: #define SIZE 10 nghĩa là ở bất kỳ chỗ nào có SIZE thì được coi như sự thay thế cho giá trị 10
Một directive khác nữa là #include cho phép bạn có thể thêm toàn bộ một file tại một ví trị trong chường trình
Ví dụ: #include “tm4c123gh6pm.h” nghĩa là chỉ thị #include sẽ thêm file tm4c123gh6pm.h vào trong chương trình, file này sẽ định nghĩa tất cả các tên port I/O cho TM4C123

Variable và Expression

Biến và kiểu dữ liệu

Kiểu dữ liệuKích thướcNgưỡng
unsigned char8-bit unsigned0 đến +255
signed char8-bit signed-128 đến +127
unsigned intphụ thuộc vào compiler
intphụ thuộc vào compiler
unsigned short16-bit unsigned0 đến +65535
short16-bit signed-32768 đến +32767
unsigned longunsigned 32-bit0 đến 4294967295L
longsigned 32-bit-2147483648L đến 2147483647L
float32-bit float±10^-38 đến ±10^+38 
double64-bit float±10^-308 đến ±10^+308

Ở bảng trên ta sẽ thấy được các kiểu dữ liệu trong C và ngưỡng cũng như giá trị của nó. Có một số kiểu dữ liệu phụ thuộc vào compiler ví dụ như trong compiler của keil C thì chỉ có duy nhất kiểu char mà không quan tâm tới signed hoặc unsigned, hay như kiểu int được Keil C coi là 32 bit.

Biến mà được khai báo bên ngoài hàm thì được gọi là external variable, external variable thường gặp nhất là global variable, có 2 lý do để sử dụng global variable, thứ nhất là data permanence, nghĩa là dữ liệu lúc nào cũng sẵn có, thứ hai là việc chia sẻ thông tin. Ví dụ khi modul đó có đưa dữ liệu vào global variable thì modul khác có thể xem được dữ liệu đó
Ngược lại thì ta có local variable, biến này thường được khai báo ngay bên trong hàm
Ví dụ

Ở ví dụ trên thì side và area là biến local, nó được định nghĩa ngay sau dấu {, khác với biến global là tĩnh(static) thì biến local thường là động (dynamic), ngoài ra quy định về tên của biến global là không được trùng tên, còn biến local thì có thể dùng thoải mái, ví dụ như trong function a bạn có khai báo biến int i thì trong function b bạn hoàn toàn có thể khai báo biến int i.

Tiếp tục tìm hiểu tiếp xem đoạn code ở bên trên đang làm cái gì? Bước đầu khai báo 2 biến local là side và area xong thì bước tiếp theo sẽ gọi hàm UART_Init() để cấu hình UART in dữ liệu ra màn hình (nó giống như lệnh Serial.begin của Arduino), sau đó là gán side = 3 và tính diện tích của căn phòng hình vuông theo công thức S = a *a, cuối cùng là in thông tin ra màn hình để chúng ta có thể quan sát được

Nếu như bạn để ý sẽ có một loại C khác là C99 thì kiểu dữ liệu của nó sẽ hơi khác một chút.

Kiểu dữ liệuKích thướcNgưỡng
uint_88-bit unsigned 0 đến +255
int_88-bit signed -128 đến +127
uint_1616-bit unsigned 0 đến +65535
int_1616-bit signed -32768 đến +32767
uint_32unsigned 32-bit 0 đến 4294967295L
int_32signed 32-bit -2147483648L đến 2147483647L

Expression

Ví dụ: Chương trình dưới đây là một số phép toán cơ bản được dùng trong C

Toán tử ở trong C các bạn có thể xem ở bảng sau

Bảng toán tử
Toán tửÝ nghĩa Toán tửÝ nghĩa
=Assignment statement==Equal to comparison
?Selection<=Less than or equal to
<Less than>=Greater than or equal to
>Greater than!=Not equal to
!Logical not (true to false, false to true)<<Shift left
~1’s complement>>Shift right
+Addition++Increment
-Subtraction--Decrement
*Multiply or pointer reference&&Boolean and
/Divide||Boolean or
%Modulo, division remainder+=Add value to
|Logical or-=Subtract value to
&Logical and, or address of*=Multiply value to
^Logical exclusive or/=Divide value to
.Used to access parts of a structure|=Or value to
&=And value to
^=Exclusive or value to
<<=Shift value left
>>=Shift value right
%=Modulo divide value to
->Pointer to a structure

Thường có một số vấn đề về thứ tự(precedence) khi thực hiện các phép toán phức tạp. Ví dụ: z = x+4y thì 4y sẽ được thực hiện trước vì phép tính nhân sẽ được ưu tiên hơn phép + và -, nếu bạn cảm thấy bối rối về điều này thì có thể xem bảng dưới

Bảng thứ tự toán tử
PrecedenceOperatorsAssociativity
Highest() [].Left to right
++(prefix) Right to left
*Left to right
+ Left to right
<< Left to right
< Left to right
==Left to right
&Left to right
^Left to right
|Left to right
&&Left to right
||Left to right
? :Right to left
= Right to left
Lowest,Left to right

Function

Function cho phép chúng ta tổ chức lại cấu trúc của phần mềm, thường khi hoàn thành một chức năng của một nhiệm vụ thì ta cho nó vào một function. Vậy cú pháp của một function sẽ như thế nào ?

Function Syntax

Một function là một chuỗi các hoạt động trong phần mềm, nó có thể có một hoặc nhiều tham số(parameter). Có 2 thứ quan trọng khi lập trình C cần phải phân biệt đó là declaration và definition

Function Declaration (prototype) sẽ xác định tên, tham số đầu vào, đầu ra, cấu trúc dữ liệu của nó sẽ là kiểu và định dạng, trong khi đó Function definition sẽ xác định chính xác các hoạt động được thực thi khi nó được gọi. Function definition sẽ tạo ra các object code ddeerr CPU có thể đưa nó vào trong memory để thực hiện các hoạt động dự định sẵn, cấu trúc dữ liệu của nó sẽ là một vùng nhớ trong memory.
Phần gây khó hiểu sẽ là definition sẽ lặp lại declaration. Với C compiler sẽ thực hiện biên dịch 1 lần qua toàn bộ code, và bắt buộc chúng ta phải khai báo data/function trước khi chúng ta truy cập và gọi chúng.

Thôi vào ví dụ luôn cho mọi người dễ hình dung được những cái mình nói trên nó là thế nào trong code

Với cách tiếp cận là top-down thì trước tiên declare function, sử dụng function, và cuối cùng là define function như đoạn code dưới đây

Với cách tiếp cận bottom-down thì đầu tiên là define function, sau đó là dùng function. Ở cách này thì definiton sẽ làm chức năng là declare cấu trúc và define những gì nó làm

Function Parameters

Với hàm sum trong chương trình dưới đây có 2 input và 1 output, điều thú vị ở đây là trong assembly và C thì sau khi các hoạt động trong chương trình con được thực hiện thì nó sẽ return lại vị trí ban đầu nơi chương trình con được gọi.

Các bạn có thể xem hình dưới đây để hình dung được cách mà chương trình chạy tương ứng với mã Assembly với code C và tác động lên các thanh ghi

Address    Machine Code     Label  Instruction       Comments
0x00000660 EB010200         sum    ADD  R2,R1,R0     ;z=x+y
0x00000664 4610                    MOV  R0,R2        ;return value 
0x00000666 4770                    BX   LR
0x00000668 F44F60FA         main   MOV  R0,#2000     ;first parameter  
0x0000066C F44F61FA                MOV  R1,#2000     ;second parameter  
0x00000670 F7FFFFF6                BL   sum          ;call function  
0x00000674 4603                    MOV  R3,R0        ;a=sum(2000,2000)  
0x00000676 F04F0400                MOV  R4,#0x00     ;b=0  
0x0000067A 4620             loop   MOV  R0,R4        ;first parameter  
0x0000067C F04F0101                MOV  R1,#0x01     ;second parameter  
0x00000680 F7FFFFEE                BL   sum          ;call function  
0x00000684 4604                    MOV  R4,R0        ;b=sum(b,1)  
0x00000686 E7F8                    B    loop

 

Address là vị trí của ROM sẽ lưu trữ các instruction
Machine code là các instruction thực tế dưới dạng mã hex
Label là kí hiệu vị trí trong chương trình, ta thường dùng label để gọi các function và nhảy tới các vị trí khác trong cùng một chương trình

Mỗi Instruction sẽ có một opcode và một hoặc nhiều toán hạng’
Comment được thêm vào để giải thich những gì chương trình đang thực hiện
Với hàm không có tham số thì sẽ thường sẽ dùng void. Ví dụ hàm unsigned long Calc_Area(unsigned long s) không có tham số s thì sẽ được khai báo là unsigned long Calc_Area(void)

Vòng lặp và điều kiện

Chúng ta sẽ đi qua một ví dụ đơn giản đầu tiên về vòng lặp và câu lệnh điều kiện

Chương trình khai báo biến global là error và thiết lập giá trị khởi tạo của nó bằng 0. Mục đích là để đảm bảo function được sử dụng đúng. Mục đích của việc thiết kế phần mềm ban đầu là kiểm tra giá trị đầu vào của function và đảm bảo rằng giá trị đó có ý nghĩa. Kiểu unsigned long có thể hiển thị số lên tới 4 tỷ, điều này có nghĩa là hệ thống sẽ không hoạt động được nếu như chúng ta cố gắng tính toán kích thước của một căn phòng với chiều dài là 4 tỷ. Mà làm gì có phòng nào có chiều dài lớn như vậy, ở trong chương trình thì kích thước lớn nhất là 25m, nếu nhỏ hơn giá trị này thì sẽ return true và ngược lại sẽ return false. Để làm được việc này thì cần phải sử dụng lệnh rẽ nhánh là if, else.

Ở chương trình tiếp theo giống như câu lệnh if ở trên thì câu lệnh while cũng là câu lệnh điều kiện (return về true hoặc false), có điểm khác là câu lệnh sẽ được thực hiện liên tục cho đến khi điều kiện kiểm tra trở thành false, trong ví dụ trên thì sẽ liên tục tăng giá trị của side lên 1 và in ra giá trị đó cho tới khi side = 50 thì không in nữa

Với cấu trúc của for sẽ có dạng như sau for(part1;part2;part3){body;}.

Trong chương trình trên part1 side = 1 sẽ được thực hiện 1 lần ngay từ đầu, sau đó sẽ là part2 được thực thi. Nếu điều kiện đúng với size < 50 thì chương trình tính toán bên trong sẽ được thực thi. Sau đó sẽ là thực thi part3, side = side + 1. Part2 luôn luôn được thực hiện để kiểm tra và trả về kết quả là true hoặc false. Chương trình bên trong vòng for (body) và part3 sẽ được thực hiện cho tới khi kết quả là false
Có một lưu ý là khi sử dụng for với 2 trường hợp dưới đây là khác nhau
if(n1>100) n2=100; n3=0;
if(n1>100) {n2=100; n3=0;}
Với n3 = 0 sẽ luôn được thực thi trong trường hợp 1, còn ở trường hợp 2 thì n3=0 chỉ khi n1>100

Thao tác nhập từ bàn phím

Ở trong hầu hết các chương trình C dành cho vi điều khiển thì có sử dụng khá nhiều thao tác xuất nhập sử dụng printf và scanf, với 2 lệnh này chúng ta có thể nhập thông tin vào và xuất thông tin ra máy tính để debug

Ví dụ trong chương trình trên ta dùng printf để in ra thông tin và scanf để nhập kích thước side từ bàn phím

Từ khóa trong C

Dưới đây là một số từ khóa thường dùng trong C, các bạn có thể xem bảng tham khảo, mình để nguyên tiếng anh để các bạn có thể dễ dàng tìm kiếm với các từ khóa có liên quan

C keywords
KeywordMeaning
__asmSpecify a function is written in assembly code (specific to ARM Keil™ uVision® )
autoSpecifies a variable as automatic (created on the stack)
breakCauses the program control structure to finish
caseOne possibility within a  switch  statement
charDefines a number with a precision of 8 bits
constDefines parameter as constant in ROM, and defines a local parameter as fixed value
continueCauses the program to go to beginning of loop
defaultUsed in  switch  statement for all other cases
doUsed for creating program loops
doubleSpecifies variable as double precision floating point
elseAlternative part of a conditional
externDefined in another module
floatSpecifies variable as single precision floating point
forUsed for creating program loops
gotoCauses program to jump to specified location
ifConditional control structure
intDefines a number with a precision that will vary from compiler to compiler
longDefines a number with a precision of 32 bits
registerSpecifies how to implement a local
returnLeave function
shortDefines a number with a precision of 16 bits
signedSpecifies variable as signed (default)
sizeofBuilt-in function returns the size of an object
staticStored permanently in memory, accessed locally
structUsed for creating data structures
switchComplex conditional control structure
typedefUsed to create new data types
unsignedAlways greater than or equal to zero
voidUsed in parameter list to mean no parameter
volatileCan change implicitly outside the direct action of the software.
whileUsed for creating program loops

Thực hành

Ở trên mình có nói cả lý thuyết và các ví dụ cụ thể, tuy nhiên để mắt thấy tai nghe, nhìn được dòng code của mình chạy thế nào thì vẫn cần có project mẫu để tiết kiệm bớt thời gian, mình đã up sẵn 1 project bằng keil C tại link Embedded C, chương trình có init sẵn uart và in ra dòng lệnh Result tại góc dưới bên phải sau khi ấn debug(ctrl + F5) và Run (F5)

Với các code bên trên bạn có thể bỏ vào project này sau đó build lại và kiểm tra kết quả

Tạm kết

Vậy là mình đã đi được các khái niệm cơ bản về lập trình C nhúng trong vi điều khiển, về các khái niệm về flowchart, cấu trúc và cách tổ chức của một chương trình C và các ví dụ minh họa với biến, hàm, các cấu trúc rẽ nhánh và lặp,.., hi vọng các bạn có cái nhìn tổng quan về việc dùng C trong lập trình vi điều khiển

Add Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.