Cơ bản về Lập Trình C cho Hệ thống Nhúng (Phần 1)

(Phần 1) , (Phần 2) , (Phần 3)

1. Giới thiệu

C là ngôn ngữ lập trình có lịch sử lâu đời, được phát triển bởi Dennis Ritchie vào năm 1971 cho các hệ thống sử dụng hệ điều hành UNIX. Ngôn ngữ C là ngôn ngữ độc lập với phần cứng, cho phép cùng một chương trình có thể chạy được trên nhiều nền tảng phần cứng khác nhau. C kết hợp giữa tính chất của một ngôn ngữ lập trình cấp cao và các tính năng của hợp ngữ (assembly), do đó nó có tính gần với phần cứng và hay được gọi là ngôn ngữ lập trình cấp trung (middle-level computer language). Khi sử dụng ngôn ngữ C, người lập trình không chỉ phải chú ý đến việc thiết kế chương trình ở cấp cao mà còn phải chú ý đến việc thực thi chi tiết như thế nào.

So với các ngôn ngữ lập trình cấp cao khác, ví dụ như Pascal. C có được mức độ linh hoạt cao hơn rất nhiều. C cung cấp cấu trúc kiểu dữ liệu linh hoạt, cho phép người lập trình chuyển đổi kiểu dữ liệu dễ dàng. C cũng là ngôn ngữ lập trình hướng cấu trúc. Vì vậy, ngôn ngữ lập trình C được sử dụng rộng rãi so với các ngôn ngữ lập trình khác.

So với ngôn ngữ lập trình hợp ngữ, C cũng có rất nhiều ưu điểm. Mã nguồn C có khả năng mở rộng tốt hơn hợp ngữ, có thể chạy trên nhiều nền tảng phần cứng khác nhau, dễ dàng sửa đổi và sử dụng lại.

Với các ưu điểm nói trên, C là ngôn ngữ phổ biến nhất cho việc lập trình cho hệ thống nhúng, vì nó cho phép tương tác với phần cứng ở cấp thấp đồng thời vẫn giữ được cấu trúc chương trình.

Tuy nhiên, có nhiều sự khác nhau giữa việc lập trình C cho máy tính và cho hệ thống nhúng. Một hệ thống nhúng thường có dung lượng bộ nhớ chương trình (ROM), bộ nhớ dữ liệu (RAM) giới hạn. Lập trình hệ thống nhúng hướng đến đối tượng là phần cứng, tương tác với các sự kiện xảy ra trong thời gian thực. đòi hỏi chương trình phải có đáp ứng nhanh. Vì vậy, một lập trình viên cho hệ thống nhúng phải có kiến thức về phần cứng hệ thống, các công cụ để lập trình/ gỡ rối, các thư viện chuẩn, kiểu dữ liệu chuẩn cũng như cách viết một chương trình có hiệu suất cao, thực thi nhanh nhất với dung lượng nhỏ nhất.

2. Các vấn đề cơ bản về C cho hệ thống nhúng

Khi lập trình với máy tinh, chương trình đầu tiên thường là chương trình “hello world”

#include <stdio.h>
main()
{
    printf(“Hello world\n”);
}

Hình 2-1: Chương trình Hello World trên máy tính

Chương trình này khi chạy sẽ in dòng chữ “Hello world” ra màn hình, sau đó thoát khỏi chương trình và trả lại quyền điều khiển cho hệ điều hành.

Tất cả chương trình C đều có một hàm tên main(). Hàm main() là hàm đầu tiên được gọi khi chương trình bắt đầu khởi chạy. Thông thường, trong hàm main() thường chỉ chứa các đoạn mã khởi động, sau đó gọi các hàm hay chương trình con khác.

Khác với chương trình cho máy tính, chương trình cho hệ thống nhúng chạy liên tục từ khi bắt đầu, không bao giờ kết thúc trừ khi bị tắt nguồn. Vì vậy trong hàm main() của chương trình nhúng bao giờ cũng có một vòng lặp vô tận, sau khi khởi động chương trình sẽ chạy trong vòng lặp này.

Một chương trình nhúng đơn giản nhất sẽ có dạng như sau:

void main(void)
{
    while(1)
    { }
}

Tuy nhiên, chương trình này không có bất kỳ quá trình xử lý dữ liệu và tương tác nào, vì vậy nó cũng không phải là một chương trình nhúng đúng nghĩa. Chúng ta thử xem xét một chương trình khác

#include <stdio.h>
void main()
{
    /* the classic C test program… */
    printf(“HELLO WORLD”);
    while(1) {}
    // do forever
}

Hình 2-2: Chương trình HelloWorld.c cho hệ thống nhúng

Chương trình này sẽ xuất dòng chữ “HELLO WORLD” ra thiết bị xuất (stdout). Với hệ thống nhúng, stdout thường là port nối tiếp (serialport). Sau đó, chương trình sẽ đi vào vòng lặp vô tận cho đến khi bị reset.

Chương trình trên sử dụng hàm printf(), là hàm được chứa trong thư viện xuất nhập stdio. Hàm này được khai báo trong file stdio.h. Chỉ dẫn tiền xử lý #include khai báo file stdio.h như là một phần của chương trình. Khi biên dịch, file HelloWorld.c sẽ được biên dịch thành file HelloWorld.o sau đó liên kết với thư viện stdio để tạo thành file thực thi hoàn chỉnh.

Trong chương trình trên, các đoạn mã được thêm vào các dòng chú thích (comment). Các chú thích sẽ không được biên dịch, mà giúp cho chương trình trở nên dễ hiểu. Điều này là cực kỳ quan trọng vì chương trình sẽ được đọc bởi người khác nếu người lập trình làm việc theo nhóm, hoặc là chính người lập trình sau đó một thời gian. Các chú thích phải giải thích cụ thể chức năng đoạn mã được chú thích, giúp cho người đọc hiểu được cách thức chương trình làm việc.

3. Định danh và từ khóa trong ngôn ngữ C

Một định danh (identifier) là tên của một biến hay hàm, bắt đầu bằng 1 ký tự hoặc gạch nối (_), theo sau bởi ký tự, gạch nối hoặc các ký số.

Định danh trong C thì được phân biệt giữa chữ hoa và chữ thường.

Định danh có thể có độ dài tùy ý, tuy nhiên một vài trình biên dịch chỉ có thể nhận biết một số lượng giới hạn các ký tự trong định danh, ví dụ 32 ký tự đầu tiên. Vì vậy, người lập trình cần kiểm tra trình biên dịch mà mình đang sử dụng.

Ngôn ngữ C có các từ khóa có ý nghĩa đặc biệt. Các từ khóa này luôn luôn viết bằng chữ thường và không được dùng để đặt tên cho định danh. Bảng 2-1 liệt kê các từ khóa được dùng trong ngôn ngữ C.

Bảng 2-1: Các từ khóa trong C

autodefinefloatlongstaticwhile
breakdoforregisterstruct 
bitdoublefuncusedreturnswitch 
caseeepromgotoshorttypedef 
charelseifsignedunion 
constenuminlinesizeofunsigned 
continueexternintsfrbvoid 
defaultflashinterruptsfrwvolatile 

4. Biến và hằng số

Kiểu biến

Một biến được định nghĩa bằng một từ khóa xác định kiểu của nó, theo sau bởi tên biến, ví dụ:

unsigned char character;
int index;
long total;

Biến là các giá trị được lưu trong bộ nhớ dữ liệu của vi điều khiển, tùy theo kiểu của nó mà mỗi biến sẽ có kích cỡ và tầm biểu diễn khác nhau. Kích thước và tầm biểu diễn của các kiểu được cho bởi Bảng 2-2.

Tùy theo kích thước, mỗi biến sẽ chiếm một hoặc một số ô nhớ trong bộ nhớ. Địa chỉ của các ô nhớ này được chỉ định khi biên dịch chương trình bởi trình biên dịch (đối với biến toàn cục) hoặc khi chương trình đang chạy (với biến nội bộ). Quá trình này gọi là quá trình cấp phát bộ nhớ cho biến.

Bảng 2-2: Kích thước và tầm biểu diễn của các kiểu biến

KiểuKích thước (bits)Tầm
bit10, 1
char8[-128:127]
unsigned char8[0:255]
signed char8[-128:127]
int16[-32768:32767]
short int16[-32768:32767]
unsigned int16[0:65535]
signed int16[-32768:32767]
long int32[-2147483648:2147483647]
unsigned long int32[0:4294967295]
signed long int32[-2147483648:2147483647]
float32[±1.175e-38:±3.402e38]
double32[±1.175e-38:±3.402e38]

Biến nội bộ và biến toàn cục

Biến nội bộ là biến được khai báo và sử dụng trong nội bộ một hàm. Biến này sẽ được cấp phát vùng nhớ khi hàm được gọi. Biến nội bộ chỉ có thể sử dụng nội bộ bên trong hàm, các hàm bên ngoài không thể truy cập vào các biến này. Ngoài ra, các biên nội bộ bên trong các hàm khác nhau có thể sử dụng chung một tên.

int add(int a, int b)
{
    int tmp;
    tmp = a + b;
    return tmp;
}

int sub(int a, int b)
{
    int tmp;
    tmp = a - b;
    return tmp;
}

Như ta thấy ở ví dụ trên, tên tmp được sử dụng để khai báo cho hai biến nội bộ ở hai hàm khác nhau.

Khác với biến nội bộ, biến toàn cục được sử dụng để khai báo cho hai biến nội bộ ở hai hàm khác nhau.

Khác với biến nội bộ. biến toàn cục được cấp phát bộ nhớ bởi trình biên dịch, có thể được truy cập bởi bất kỳ hàm nào trong chương trình. Khi chương trình bắt đầu chạy, thông thường biến toàn cục sẽ bị xóa về 0 bởi đoạn mã khởi động (startup code).

Nếu trong một hàm có khai báo một biến nội bộ trùng tên với biến toàn cục. biến nội bộ sẽ được dùng bởi hàm này. Giá trị của biến toàn cục không thay đổi khi hàm này được thực hiện.

Ví dụ dưới đây mô tả cách khai báo và sử dụng biến toàn cục index trong hai hàm increase()main()

int index;
void increase(void)
{
    index++;
}

int main(void)
{
    index = 0;
    increase();
    while(1);
}

Các kiểu lưu trữ của biến

Một biến có thể có ba định nghĩa cho phương thức nó được lưu trong bộ nhớ: auto, static và register.

Automatic:

Biến automatic không được khởi tạo giá trị đầu khi nó được cấp phát địa chỉ. Nếu nó là biến nội bộ, khi chương trình thoát, địa chỉ dành cho nó sẽ được giải phóng, có nghĩa là giá trị của nó bị mất đi.

Automatic là kiểu mặc định, nghĩa là hai khai báo:

int a;
auto int a;

có ý nghĩa như nhau.

Static:

Biến tĩnh (static) được dành cho một địa chỉ cố định trong bộ nhớ. Nếu là biến nội bộ trong hàm, nó không thể truy cập được từ các hàm khác nhưng cũng không mất đi khi thoát khỏi hàm. Ở lần đầu truy cập, biến static mang giá trị 0.

void increase(void)
{
    static sum;
    sum++;
    return sum;
}

int main(void)
{
    int ret;
    increase();
    increase();
    ret = increase();
    ...
}

Sau khi thực hiện đoạn chương trình trong main(), ret mang giá trị 3.

Register:

Với kiểu register, trình biên dịch sẽ sử dụng riêng một thanh ghi trong CPU để chứa biến này. Điều này làm tăng tốc độ truy cập biến vì CPU sử dụng các lệnh truy cập thanh ghi thay vì truy cập bộ nhớ.

Tuy nhiên, số lượng thanh ghi trong CPU là rất ít, do đó cần thận trọng khi sử dụng biến này.

Ép kiểu biến

Các biến được lưu trữ trong bộ nhớ tùy theo cách nó được khai báo. Ví dụ kiểu char được lưu bằng 1 byte, int lưu bằng 4 byte. Có nhiều lúc ta phải chuyển kiểu đã khai báo của một biến thành một kiểu khác cho phù hợp. Ví dụ

int  x;  // a signed, 16-bit integer (-32768 to  +32767)
char y;  // a signed, 8-bit character (-128 to +127)
x = 12;  // x is an integer, but its value will fit into a character

xy có kiểu khác nhau, khi thực hiện phép tính giữa xy ta ép kiểu của x từ int thành char:


y = (char)x + 3;  // x is converted to a character and then 3 is added, and the value is then placed into y

Trong khi thực hiện các phép tính giữa các biến có kiểu khác nhau, kết quả có thể không chính xác vì giới hạn biểu diễn của các biến. Ví dụ đoạn chương trình sau:

int z;             // declare z
int x = 150;       // declare and initialize x
char y = 63;       // declare and initialize y
z = (y * 10) + x;

Khi trình biên dịch thực hiện phép tính, vì y là biến có kiểu char nên trình biên dịch mặc định kết quả phép nhân y*10 là kiểu char với kích thước 8 bit. Kết quả sẽ bị cắt thành 118 thay vì 630. Ở lệnh cộng thứ 2, trình biên dịch sẽ xem đây là phép tính 32 bit, kết quả cuối cùng z bằng 268 là kết quả sai!

Để tránh các sai số như trên, ta cần ép y sang kiểu int:

z = ((int)y * 10) + x;

Hằng số

Hằng số có giá trị cố định không thay đổi trong suốt quá trình chương trình chạy. Hằng số thường được đặt trong bộ nhớ chương trình (ROM), được tạo ra trong quá trình biên dịch chương trình. Ví dụ như dòng lệnh sau:

x = 3 + y;

Số 3 là một hằng số và sẽ được đưa ra trực tiếp vào lệnh cộng bởi trình biên dịch. Với ARM, lệnh sau có thể chuyển thành:

ADDS R1, R2, 3

Hằng số cũng có thể là một chuỗi hay ký tự:

printf("hello world"); /* The text "hello world" is placed in program memory and never changes */
x = 'B'; /* The letter 'B' is permanently set in program memory */

Một hằng số cũng có thể được định nghĩa bằng từ khóa const, theo sau bởi kiểu và tên định danh:

Khi khai báo như trên, biến c sẽ được đặt vào bộ nhớ chương trình (ROM) thay vì RAM. Điều này hết sức quan trọng vì bộ nhớ RAM trong hệ thống nhúng thường là nhỏ.

Giá trị hằng số thường được biểu diễn bằng cơ số và giá trị của nó:

  • Cơ số 10: không biểu diễn cơ số (ví dụ 1234)
  • Nhị phân (binary): với biểu diễn cơ số 0b (ví dụ 0b1010)
  • Cơ số 16 (hexa): với biểu diễn cơ số 0x (ví dụ 0x0A)
  • Cơ số 8 (octa): với biểu diễn cơ số là ký tự o (ví dụ o76)

Hằng số ký tự được chia làm hai loại: ký tự có thể in được (ví dụ 0-9, A-Z) và không in được (ví dụ xuống hàng, tab, v.v.).

Ký tự tin được có thể được biểu diễn bằng dấu nháy đơn hoặc dấu / theo sau bởi mã ASCII nằm trong dấu nháy đơn (‘).

Ví dụ: hằng số ký tự t có thể được biểu diễn bới ‘t’ hoặc ‘/164’

Hằng số chuỗi được biểu diễn trong dấu nháy kép (“)

Ví dụ: “embedded”

Bảng 2-3: mô tả một số hằng số ký tự đặc biệt dùng trong C

CharacterRepresentationEquivalent Hex value
BEL‘\a’‘\x07’
Backspace‘\b’‘\x08’
TAB‘\t’‘\x09’
LF (new line)‘\n’‘\x0a’
VT‘\v’‘\x0b’
FF‘\f’‘\x0c’
CR‘\r’‘\x0d’

Kiểu enum

Kiểu liệt kê enum định nghĩa một danh sách các hằng số. Ví dụ:

const char c = 57;
int day;
enum {Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday};
day = Monday;

Sau những lệnh trên, biến day mang giá trị 0, Monday sẽ bằng 0, Tuesday bằng 1, v.v.

Ta có thể đưa giá trị đầu vào như sau:

enum {Monday = 2, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday};

Khi đó, Moday sẽ bằng 2, Tuesday bằng 3, v.v. Với cách khai báo trên, chương trình sẽ trở nên rõ ràng và dễ hiểu hơn.

5. Các phép toán trong C

Phép gán

Một giá trị hay biểu thức có thể được đưa vào một biến bằng phép gán:

acharacter = 'A';
val = 5;
val = acharacter + 5;

Các phép toán số học

Một biểu thức thường được tạo thành từ các biến và các phép toán số học nhân, chia, lấy dư, cộng, trừ và các dấu ngoặc. Các phép toán bên trong dấu ngoặc được thực hiện trước các phép toán khác. Ngoài ra, các phép toán nhân và chia sẽ được thực hiện trước phép toán cộng, trừ.

Bảng 2-4: Các toán tử số học

Nhân*
Chia/
Lấy dư%
Cộng+
Trừ

Các phép toán trên bit

Các phép toán trên bit gồm có phép NOT, AND, OR, XOR và dịch bit. Các phép toán này thực hiện trên các biến có kiểu nguyên (char, short, int, long)

Bảng 2-5: Các toán tử trên bit

Lấy bù (NOT)~
Dịch trái<< 
Dịch phải>> 
AND&
XOR^
OR|

Ví dụ ta có biến x kiểu char có giá trị y = 0xF0

Phép toánKết quả
x = ~yx = 0x0F
x = y << 2x = 0XC0
x = y >> 2x = 0x3C
x = y & 0xAAx = 0xA0
x = y ^ 0xFFx = 0x0F
x = y | 0x0Ax = 0xFA

Khi lập trình cho hệ thống nhúng, ta thường phải làm việc với chỉ một số bit trên một thanh ghi. Ví dụ như ta có 3 LED kết nối vào 3 bit 0, 1, 2 của port song song PORTA của vi điều khiển 8 bit 8051. Ta muốn bật 3 LED này bằng cách đưa 3 chân này lên 1 mà không làm ảnh hưởng các bit khác.

Điều này có thể thực hiện bằng cách OR giá trị hiện hành của PORTA với 0x07

PORTA = PORTA | 0x07;  // 0x07 = 0b00000111

Tương tự, ta có 3 switch nối vào 3 bit cao 7, 6, 5 của PORTA và muốn đọc giá trị của chúng. Khi đó, ta chỉ quan tâm đến giá trị của 3 bit này trên thanh ghi PORTA. Giá trị này có thể đọc về như sau:

switchVal = (PORTA & 0xE0) >> 6; // 0xE0 = 0b11100000

Biểu thức (PORTA & 0xE0) trả về giá trị của 3 bit cao của thanh ghi 8 bit PORTA và xóa các bit khác. Kỹ thuật này gọi là mặt nạ (masking).

Toán tử logic và so sánh

Toán tử logic

Toán tử logic gồm có hai toán tử AND và OR. Điểm khác nhau cơ bản giữa phép toán logic và phép toán trên bit là phép toán logic làm việc trên các giá trị logic: TRUE và FALSE

Bảng 2-6: Các toán tử logic

AND&&
OR||

Một biến có giá trị khác không được xem là TRUE trong phép toán logic, ngược lại nếu biến bằng 0 xem như nó có giá trị FALSE. Ví dụ: ta có x = 0x0Fy = 0xF0

(x && y) có giá trị TRUE vì x và y đều khác 0 (TRUE)

(x & y) có giá trị FALSE vì x & y = 0x00

Toán tử so sánh

Toán tử so sánh được sử dụng để so sánh hai giá trị. Kết quả mang giá trị TRUE hoặc FALSE

Bảng 2.7: Các toán tử so sánh

Bằng nhau==
Không bằng nhau!=
Nhỏ hơn
Nhỏ hơn hoặc bằng<=
Lớn hơn
Lớn hơn hoặc bằng>=

Giả sử ta có x = 3 và y = 5 thì:

x == yFALSE
x != yTRUE
x < yTRUE
x <= yTRUE
x > yFALSE
x >= yFALSE

Toán tử tăng, giảm và phép toán phức

Toán tử tăng

Toán tử tăng cho phép một biến tăng lên 1. Ví dụ:

x = x + 1;

Có cùng ý nghĩa với hai biểu thức:

x++;  // tăng sau, post-increment
++x;  // tăng trước, pre-increment

Trong các biểu thức trên, giá trị của x được tăng lên 1. ++x là lệnh tăng trước, còn x++ là lệnh tăng sau. Điều này có nghĩa là nếu toán tử ++ được sử dụng trong một biểu thức, giá trị của biến được tăng trước khi thực hiện biểu thức hoặc là tăng sau. Ví dụ:

i = 1;
k = 2 * i++;  // sau lệnh này k = 2 và i = 2
i = 1;
k = 2 * ++i;  // sau lệnh này k = 4 và i = 2

Toán tử giảm

Toán tử giảm được dùng tương tự như toán tử tăng, có tác dụng giảm biến đi 1

i--;  // i = i - 1
--i;  // i = i - 1

Toán tử phức

Toán tử phức là cách viết để giảm độ dài lệnh bằng cách kết hợp lệnh gán (=) với toán tử số học hoặc toán tử xử lý bit. Ví dụ:

a += 3;  // a = a + 3
a -= 2;  // a = a - 2
a *= 5;  // a = a - 5
a /= 4;  // a = a / 4
a |= 3;  // a = a OR 3
a &= 2;  // a = a AND 2
a ^= 5;  // a = a XOR 5

Toán tử điều kiện

Toán tử điều kiện là dạng viết tắt của cấu trúc rẽ nhánh IF-THEN-ELSE. Câu lệnh như sau

if(expression_A)
    expression_B;
else
    expression_C;

Có thể viết thành:

expression_A ? expression_B : expression_C;

Ví dụ:

(x < 5) ? y = 1 : y = 0;  /* If x is less than 5, then y = 1, else y = 0 */

6. Các câu lệnh điều khiển luồng chương trình

Vòng lặp WHILE

Vòng lặp while có cấu trúc như sau:

while(expression)
{
    Statement1;
    Statement2;
}

Khi chương trình chạy đến vòng lặp while, biểu thức expression sẽ được tính toán. Nếu nó mang giá trị TRUE, các câu lệnh trong vòng while sẽ được lần lượt thực thi. Sau khi thực thi các câu lệnh xong, quá trình này lặp lại cho đến khi expression mang giá trị FALSE.

Trong lập trình cho hệ thống nhúng, chương trình chính sẽ thực thi trong một vòng lặp vô tận như sau:

while(1)
{
    Task1();
    Task2();
}

Vòng lặp while còn có thể sử dụng để chờ một sự kiện, ví dụ một nút nhấn gắn vào chân PA.0 được nhấn:

while(PORTA & 0x01);

Đoạn mã trên sẽ chờ cho đến khi PA.0 mang giá trị 0 (nút nhấn được nhấn).

Vòng lặp DO-WHILE

Vòng lặp do-while có cấu trúc như sau:

do{
    Statement1;
    Statement2;
} while(expression)

Điểm khác nhau giữa vòng lặp WHILE và DO-WHILE là với vòng lặp do-while, biểu thức expression được tính sau khi thực hiện các lệnh trong vòng lặp.

Vòng lặp FOR

Vòng lặp FOR được dùng để thực thi một khối lệnh trong một số lần biết trước. Cấu trúc vòng lặp FOR như sau:

for(expr1; expr2; expr3)
{
    Statement1;
    Statement2;
}

Ví dụ: đoạn mã

for(i = 0; i < 100; i++)
{
    a += 1;
}

Sẽ lặp lại 100 lần, kết quả là a = a + 100

Với vòng for, biểu thức expr1 chỉ thực hiện một lần khi vào vòng for, thường được sử dụng để khởi động giá trị đếm trong vòng for. Biểu thức expr2 sẽ được kiểm tra, nếu mang giá trị TRUE thì các lệnh trong vòng for được thực thi. Sau khi thực thi xong các lệnh trong vòng for, biểu thức expr3 được thực thi, chương trình quay lại đầu vòng for và kiểm tra giá trị expr2. Quá trình lặp lại cho đến khi expr2 mang giá trị FALSE.

Vòng lặp FOR còn có thể được sử dụng để tạo những khoản trễ (delay) trong chương trình. Ví dụ đoạn chương trình sau chớp tắt 1 LED gắn vào chân PA.0

while(1)
{
    PORTA ^= 0x01;  // đảo chân PA.0
    for(i = 0; i < 200000; i++);  // tạo delay 200000 chu kỳ máy
}

Câu lệnh rẽ nhánh IF và IF-ELSE

Câu lệnh IF có cấu trúc sau:

if(expression)
{
    Statement1;
    Statement2;
}

Nếu biểu thức expression mang giá trị TRUE, các lệnh trong dấu ngoặc được thực thi.

Cấu trúc IF-ELSE được mô tả như sau:

if(expression)
{
    Statement1;
    Statement2;
}
else
{
    Statement3;
    Statement4;
}

Nếu biểu thức expression mang giá trị TRUE, các câu lệnh trong khối IF sẽ được thực thi, ngược lại các câu lệnh trong khối ELSE sẽ được thực thi

Ngoài ra, câu lệnh IF-ELSE còn có thể được ghép nối tiếp như sau:

if(expr1)
    statement1;
else if(expr2)
    statement2;
else if(expr3)
    statement3;
else
    statement4;

Đầu tiên, expr1 sẽ được kiểm tra. Nếu expr1 mang kết quả TRUE, satement1 sẽ được thực thi và phần còn lại của câu lệnh sẽ được bỏ qua. Nếu không expr2 sẽ được kiểm tra. Quá trình diễn ra tương tự cho các biểu thức còn lại. Nếu không có biểu thức nào mang giá trị TRUE, statement4 được thực thi.

Câu lệnh SWITCH-CASE

switch(expression)
{
    case const1:
        statement1;
        statement2;
        break;
    case const2;
        statement3;
        statement4;
        break;
    default:
        statement5;
        statement6;
        break;
}

Đầu tiên, biểu thức expression sẽ được tính toán và lần lượt so sánh với các giá trị const1, const2,… Nếu kết quả so sánh là bằng nhau, các câu lệnh tại trường hợp (case) đó sẽ được thực thi.

Thông thường, nếu cuối mỗi case ta đặt câu lệnh break thì chương trình gặp lệnh break sẽ thoát khỏi vòng case mà không kiểm tra các trường hợp tiếp theo.

BREAK, CONTINUE, GOTO

Các chỉ dẫn break, continue, goto được sử dụng để thay đổi quá trình làm việc của các vòng lặp for, while, do-whileswitch

Break

Chỉ dẫn break làm chương trình thoát khỏi vòng lặp for, while, do-while, switch. Nếu có nhiều vòng lặp lồng nhau, lệnh switch chỉ thoát khỏi vòng lặp chứa nó. Ví dụ:

#include <stdio.h>
void main(void)
{
    int c = 0;
    while(1)
    {
        while(1)
        {
            if(c > 100)
                break;  // thoát khỏi vòng while(1) phía trên
            ++c;  // tăng biến đếm
            printf("c = %d\n", c);
        }
        c = 0; // xóa về 0
    }// In các giá trị 0-100, 0-100, ...
}

Đoạn chương trình trên xuất ra màn hình các giá trị từ 0-100 và lặp lại. Đầu tiên, c mang giá trị 0 và chương trình đi vào vòng lặp while(1) phía trong. Cứ sau mỗi lần lặp c tăng lên 1 đến khi c bằng 101 thì chương trình thực hiện lệnh break, thoát khỏi vòng while(1) phía trong. Sau đó c được xóa về 0 và chương trình lặp lại từ đầu.

Continue

Chỉ dẫn continue làm cho chương trình bỏ qua các lệnh còn lại trong vòng lặp while, do-while, for và bắt đầu một vòng lặp mới. Ví dụ:

#include <stdio.h>
void main(void)
{
    int c;
    while(1)
    {
        c = 0;
        while(1)
        {
            if(c > 100) continue;
            ++c;
            printf("c = %d\n", c);
        }
    }
}

Ở đoạn chương trình trên, khi vào vòng while(1) bên trong chương trình in ra màn hình các giá trị từ 0-100. Khi c = 101, lệnh continue được thực thi và hai lệnh phía dưới bị bỏ qua. Lúc này chương trình vẫn chạy nhưng không có ký tự nào được in ra màn hình.

Goto

goto identifier;
...
identifier:
    statement;

Lệnh goto làm cho chương trình nhảy đến một nhãn identifier. Nhãn là một tên hợp lệ trong C, theo sau bởi dấu “:”

#include <stdio.h>
void main(void)
{
    int c;
    while(1)
    {
        ClearIndex:
        c = 0;
        while(1)
        {
            if(c > 100) goto ClearIndex;
            ++c;
            printf("c = %d\n", c);
        }
    }
}

Đoạn chương trình trên xuất ra màng hình các giá trị từ 0-100 và lặp lại. Đầu tiên, c mang giá trị 0 và chương trình đi vào vòng lặp while(1) phía trong. Cứ sau mỗi lần lặp c tăng lên 1 đến khi c bằng 101 thì chươn trình thực hiện lệnh goto ClearIndex;, thoát khỏi vòng lặp while(1) phía trong. Sau đó c được xóa về 0 và chương trình lặp lại từ đầu.

Lệnh goto làm cho cấu trúc chương trình trở nên khó theo dõi, tuy nhiên trong nhiều trường hợp nó làm cho chương trình thực hiện nhanh hơn và giảm kích thước chương trình. Điều này rất quan trọng trong lập trình nhúng.

(Phần 2)

Chương 1: Cơ bản về lập trình C cho hệ thống nhúng

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

Icons made by Freepik from www.flaticon.com