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

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

12. Hàm (function)

Một hàm (function) là một khối các câu lệnh có thể sử dụng nhiều lần trong cùng một chương trình. Tổng quát, hàm được dùng để chia chương trình thành các phần cơ bản, cho phép người lập trình phát triển, gỡ rối từng thành phần riêng biệt và sử dụng lại các thành phần này nhiều lần.

Một ứng dụng quan trọng của hàm là sau khi được phát triển, các hàm có thể lưu lại và được sử dụng bởi các lập trình viên khác như là “thư viện”. Điều đó giúp tiết kiệm thời gian và làm cho chương trình ổn định, vì các việc này đã được sử dụng và kiểm tra nhiều lần.

Một hàm có thể nhận hoặc không cần nhận các tham số vào, cũng như có thể trả về giá trị hoặc không. Một hàm chỉ có thể trả về tối đa một giá trị. Dạng cơ bản của một hàm như sau

type function_name(type param1, type param2, ...)
{
    statement1;
    statement2;
    statementx;
}

Hàm có thể có bất kỳ kiểu nào. Kiểu mặc định của hàm là int. Hàm có thể có kiểu void, tức là giá trị trả về có kích thước là 0.

Định nghĩa và mô tả hàm

Một hàm gồm có hai thành phần là định nghĩa và mô tả. Mô tả hàm cho ta biết được kiểu của hàm và các tham số của hàm. Ví dụ:

int sum(int a, int b);

Mô tả hàm trên cho ta biết hàm sum nhận hai tham số kiểu int, trả về một kết quả kiểu int. Định nghĩa hàm cho ta biết cụ thể hàm được xây dựng như thế nào. Ví dụ:

int sum(int a, int b)
{
    int temp;
    temp = a + b;
    return temp;
}

Tổ chức hàm

Để sử dụng được hàm, hàm phải được mô tả trước khi hàm được gọi. Có nhiều cách để sắp xếp các hàm.

Cách thứ nhất là đặt các định nghĩa hàm theo thứ tự. Một chương trình theo cách như vậy có cấu trúc như sau:

// declaration of global variables would be first 
int var1, var2, var3;
// declarations of functions would come next
int function1(int x, int y)
{
    
}
void function2(void)
{
    
}
// main would be built last
void main(void)
{
    var3 = function1(var1, var2);
    function2();
}

Cách sắp xếp này làm cho chương trình rõ ràng, tuy nhiên không phải lúc nào ta cũng có thể đặt các định nghĩa hàm theo thứ tự như vậy được. Cách thứ hai được sử dụng thông dụng hơn, đó là ta đặt các mô tả hàm trước, sau đó mới đặt các định nghĩa hàm.

int var1, var2, var3;  // declaration of global variables
int function1(int x, int y);
void function2(void);

void main(void) {
    var3 = function1(var1,  var2);
    function2();
}

int function1(int x, int y)
{
    
}

int function2(void)
{
    
}

Với cách sắp xếp như trên, khi trình biên dịch xử lý hàm main(), function1 và function2 đã được mô tả ở trên. Trình biên dịch đã biết mọi thông tin về kiểu và các tham số của hàm khi nó xử lý lệnh gọi hàm.

Trình biên dịch C cung cấp rất nhiều thư viện chuẩn. Để sử dụng các hàm trong thư viện này, C cung cấp các file có đuôi .h chứa các mô tả hàm gọi là header file. Chương trình cần sử dụng lệnh #include <filename.h>, với filename.h là tên header file chứa các mô tả hàm.

Ở ví dụ trước, ta sử dụng lệnh printf được dùng trong chương trình.

#include <stdio.h>

Hàm trả về giá trị

Một hàm thường được thiết kế để thực hiện một tác vụ và trả về kết quả hoặc trạng thái của tác vụ đã thực hiện. Từ khóa return được dùng để chỉ điểm thoát của hàm có kiểu void, hoặc để thoát và trả về một giá trị với hàm có kiểu khác void. Ở đoạn chương trình sau:

int z;  // global variable z
void sum(int x, int y)
{
    z = x + y; // z is modified by the function sum()
    return;
}

Từ khóa return làm cho chương trình thoát sau khi thưc hiện lệnh z = x + y. Lưu ý là ta có thể bỏ từ khóa return trong trường hợp này. Nếu từ khóa return được đặt như sau:

void sum(int x, int y)
{
    return;    // end function here
    z = x + y; // this statement cannot be reached 
}

Sau khi được gọi, hàm sum() sẽ thoát ngay khi gặp lệnh return, lệnh z = x + y không được thực hiện.

Nếu hàm có kiểu khác void, từ khóa return còn có tác dụng trả về giá trị của hàm:

int sum(int a, int b)
{
    int temp;
    temp = a + b;
    return temp;
}

Khi gặp từ khóa return, hàm được thoát và giá trị temp được trả về như là giá trị của hàm.

Truyền tham số vào hàm

Truyền giá trị (pass by value)

Giả sử ta có đoạn chương trình như sau

int add(int a, int b)
{
    a = a + b;
    return a;
}
int main(void)
{
    int x, y, z;
    x = 0;
    y = 1;
    z = add(x, y);
}

Như trên, add là một hàm nhận hai tham số là hai biến kiểu int, trả về một giá trị kiểu int. Trong hàm main, ta gọi hàm add bằng cách truyền x và y vào như là hai tham số.

z = add(x, y);

Lưu ý rằng hàm add sẽ không xử lý trực tiếp trên hai biến x và y, mà sẽ cấp phát vùng nhớ cho hai biến nội bộ a và b, sau đó sao chép giá trị của x vào a và của y vào b. Chính vì vậy, mặc dù trong hàm add có lệnh a = a + b, tuy nhiên hai biến x và y không hề bị ảnh hưởng.

Với cơ chế này, khi chúng ta làm việc trực tiếp với biến có kích thước lớn, thường là struct, sẽ rất mất thời gian và tiêu tốn bộ nhớ cho việc cấp phát, sao chép dữ liệu vào biến nội bộ. Ví dụ như một hàm có tham số kiểu struct:

int PartSku(struct PART originalPart)
{
    int sku;
    struct PART myPart;
    sku = copyPart(myPart);
}

Được gọi như sau:

struct LOCATION {
    int x;  // this is the location 
    int y;  // coordinates x and y
};

struct PART {
    char part_name[20];  // a string for the part name
    long int sku;        // a SKU number for the part
    struct LOCATION bin; // its location in the warehouse
}

int PartSku(struct PART originalPart)
{
    return originalPart.sku;
}

int main(void)
{
    int sku;
    struct PART myPart = {“;
    sku = PartSku(myPart);
    return 0;
}

Khi xử lý lệnh sku = PartSku (originalPart), đầu tiên trình biên dịch sẽ cấp phát vùng nhớ cho một biến cục bộ cũng mang tên originalPart, rồi sao chép giá trị của biến originalPart vào biến này. Quá trình này sẽ gây mất thời gian xử lý một cách không cần thiết.

Vì vậy, khi cần làm việc với các đối tượng dữ liệu lớn, ta thường dùng phương pháp truyền tham chiếu hơn là truyền giá trị.

Truyền tham chiếu vào hàm (pass by reference)

Với phương pháp truyền tham chiếu, các hàm được thiết kế để các tham số là các con trỏ

int PartSku(struct PART *originalPart)
{
    return originalPart->sku;
}

Và được gọi:

int main(void)
{
    int sku;
    struct PART myPart;
    sku = PartSku(&myPart);
    return 0;
}

Khi hàm này được gọi, địa chỉ của biến myPart được chép vào biến con trỏ originalPart, và biến này có kích thước chỉ là 4 byte. Điều này làm giảm đi rất nhiều quá trình sao chép dữ liệu so với việc truyền giá trị vào hàm.

Ngoài ra, như đã nói, hàm chỉ có thể trả về 1 giá trị. Nếu ta muốn hàm trả về nhiều giá trị sau khi gọi, ta có thể sử dụng cách truyền tham chiếu như trên, vì khi đó hàm sẽ thao tác trực tiếp trên vùng nhớ mà con trỏ đang tham chiếu tới. Ví dụ:

#include <stdio.h>

void swapnum(int *i, int *j)
{
    int temp = i;
    i = j;
    j = temp;
}

int main(void)
{
    int a = 10, b = 20;
    swapnum(&a, &b);
    printf("A is %d and B is %d\n", a, b);
    return 0;
}

Sau khi thực hiện, biến a và b sẽ thay đổi giá trị: a = 20 và b = 10. Một cách tổng quát, nếu ta muốn một biến thay đổi giá trị sau khi truyền hàm, ta phải truyền tham chiếu của nó vào hàm.

Con trỏ hàm

Một trong những điểm mạnh của ngôn ngữ C là khả năng cho phép sử dụng con trỏ chỉ đến hàm. Điều này cho phép hàm được gọi kết quả của một bảng tra (look-up table). Con trỏ hàm còn cho phép một hàm được đưa vào hàm khác như là 1 tham số. Ví dụ sau mô tả cách sử dụng một con trỏ hàm:

#include <stdio.h>

// function prototype
int subtract(int x, int y);

int main() 
{
    int (*subtractPtr)(int, int);
    subtractPtr = subtract;
    
    int y = (*subtractPtr)(10, 2);
    printf("Subtract gives: %d\n", y);
    
    int z = subtract(10, 2);
    printf("Subtract gives: %d\n", z);
    
    return 0;
}

// function implementation
int subtract(int x, int y)
{
    return x - y;
}

Hàm substract có hai đối số vào là x và y kiểu int, trả về giá trị kiểu int. Ta khai báo một con trỏ int (*substract)(int, int). Con trỏ này là con trỏ hàm có hai tham số vào kiểu int, trả về giá trị kiểu int.

Phép gán subtractPtr = subtract làm cho con trỏ subtractPt trỏ đến hàm subtract. Chú ý rằng hai cách gọi hàm (*subtractPtr)(10, 2) và subtractPtr(10,2) là như nhau.

Ví dụ sau mô tả cách đưa con trỏ hàm vào một hàm khác như là một tham số:

#include <stdio.h>

// function prototypes
int add(int x, int y);
int subtract(int x, int y);
int domain(int (*mathop)(int, int), int x, int y);

// add x + y
int add(int x, int y)
{
    return x + y;
}

// subtract x + y
int subtract(int x, int y)
{
    return x - y;
}

// run the function pointer with inputs
int domath(int (*mathop)(int, int), int x, int y)
{
    return (*mathop)(x, y);
}

// calling from main
int main()
{
    // call math function with add
    int a = domath(add, 10, 2);
    printf("Add gives: %d\n", a);
    // call math function with add
    int b = domath(subtract, 10, 2);
    printf("Subtract gives: %d\n", b);

    return 0;
}

Ở đây, ta có hai hàm add và subtract có cùng cấu trúc int function(int , int). Ta khai báo hàm int domath(int (*mathop)(int, int)), int x, int y) có tham số đầu là một con trỏ hàm nhận hai tham số kiểu int và trả về một giá trị kiểu int.

Trong định nghĩa của hàm domath, con trỏ hàm lấy hai tham số x và y như là hai tham số ngõ vào, giá trị trả về của hàm domath chính là kết quả của con trỏ hàm mathop.

Trong hàm main, ta gọi hàm domath(add, 10, 2). Trong lệnh này, ta đưa vào tên hàm add, có nghĩa là ta đưa vào địa chỉ của hàm add trong bộ nhớ chương trình. Điều này sẽ làm cho con trỏ hàm mathop trỏ đến hàm add, do đó hàm domath(add, 10, 2) sẽ thực hiện phép tính add(10, 2)

Ví dụ sau mô tả cách sử dụng con trỏ hàm như là kết quả của một bảng tra

#include <stdio.h>

void do_start_task(void)
{
    printf("start selected\n");
}

void do_stop_task(void)
{
    printf("stop selected\n");
}

void do_up_task(void)
{
    printf("up selected\n");
}
void do_down_task(void)
{
    printf("down selected\n");
}

void do_left_task(void)
{
    printf("left selected\n");
}

void do_right_task(void)
{
    printf("right selected\n");
}

void (*task_list[6])(void) = {
    do_start_task,
    do_stop_task,
    do_up_task,
    do_down_task,
    do_left_task,
    do_right_task,
};

void main(void)
{
    int func_number;
    void (*fp)(void); // fp is a pointer to a function
    while(1)
    {
        printf("\nSelect a function 1-6:");
        scanf("%d", &func_number);
        if( (func_number > 0) && (func_number < 7) )
        {
            fp = task_list[func_number-1];
            (*fp)();
        }
    }
}

Ở đây, địa chỉ của 6 hàm được đưa vào một mảng có kiểu là con trỏ hàm

void (*task_list[6])(void) = {
    do_start_task,
    do_stop_task,
    do_up_task,
    do_down_task,
    do_left_task,
    do_right_task,
};

Tùy theo giá trị nhận được từ hàm scanf, ta sẽ thực thi hàm tương ứng:

fp = task_list[func_number-1];
(*fp)();

Ta có thể sử dụng các lệnh như switch-case hoặc if-else để xem xét giá trị vào và thực hiện hàm tương ứng, tuy nhiên với việc sử dụng con trỏ hàm chương trình trở nên ngắn hơn và thực thi nhanh hơn.

13. Phong cách lập trình

Ở các phần trên ta đã tìm hiểu về các vấn đề cơ bản khi lập trình bằng ngôn ngữ C cho hệ thống nhúng. Tuy nhiên, viết mã nguồn C cho hệ thống chỉ là một phần trong quá trình phát triển phần mềm cho hệ thống. Ngoài việc viết chương trình cho hệ thống chạy đúng, còn có các khía cạnh khác cần phải quan tâm là sự rõ ràng, dễ hiểu và khả năng sửa chữa, nâng cấp của phần mềm.

  • Các tài liệu đi kèm
  • Quản lý dự án
  • Quản lý chất lượng (ví dụ các chuẩn ISO9001 và ISO9003)
  • Các quy luật thiết kế (design-rule) và phong cách lập trình. Các luật này là riêng cho từng công ty.
  • Các quy trình kiểm tra chất lượng, đặc biệt là cho các hệ thống y tế và công nghiệp.

Mỗi công ty thường có một quy chuẩn riêng về cách tổ chức và viết mã nguồn, cách đặt tên biến, cách viết các chú thích, đặt dấu ngoặc … Viết mã theo đúng quy chuẩn sẽ làm cho chương trình dễ đọc, dễ sửa lỗi và sử dụng lại, đặc biết là khi người lập trình làm việc trong một nhóm.

Nhóm Netrino đã đưa ra một chuẩn cho việc viết mã C cho hệ thống nhúng trong đó đưa ra các quy luật cơ bản trong quá trình phát triển chương trình. Người đọc có thể tìm hiểu ở tài liệu Embedded C Coding Standard, trên trang web www.netrino.com.

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