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

Một số lỗi hay gặp khi phát triển phần mềm nhúng

Là một lập trình viên đôi lúc bạn sẽ phải thốt lên:

  • Ơ hay, vừa mới chạy ngon mà giờ bây giờ lại lỗi được.
  • Sao ban nãy vừa chạy xong, thêm có đúng một lệnh xxx vào mà nó lại không chạy nhỉ?
  • M**, lại treo à, reset lại giúp tôi với.

Trong quá trình phát triển có lẽ chúng ta thường xuyên phải đối mặt với những tình huống không ai muốn đó. Hãy cùng nhau chia sẻ những kinh nghiệm về những lỗi hay gặp, những trải nghiệm mà chúng ta phải đối mặt trong quá trình phát triển để công việc lập trình đỡ phải thốt lên như trên nhé.

Sau đây là một số lỗi hay gặp nhất, hoặc đã chứng kiến nhiều người gặp trong quá trình dev/review code…

1. Vấn đề bộ nhớ

Biểu hiện dễ thấy nhất của các bệnh về bộ nhớ nhiều lúc là nó không có biểu hiện gì, bỗng nhiên chỉ thấy chương trình treo cứng, không có bất cứ một thông tin nào, không có bất cứ một assert, một cảnh báo gì, vì đơn giản bộ nhớ sai các địa chỉ nhảy, các địa chỉ trả về, các vùng dữ liệu bị corrupt thì chương trình hầu hết là bị treo.

Các vấn đề hay gặp trong bộ nhớ:

Thiếu RAM (chú ý lúc build, stack overflow)

Đọc/Ghi tràn mảng: vấn đề tưởng như đơn giản, nhưng lại khá hay gặp (ví dụ: mảng có 10 phần tử mà ông lại ghi ra phần tử thứ 11 là đi rồi). Vấn đề này chỉ cần để ý cần thận khi code thì sẽ không phải là khó tránh.

Memory leak: Một vấn đề tưởng như đơn giản khác nhưng dễ gặp đó là cấp phát động mà vì nhiều nguyên nhân dữ liệu không được giải phóng (alloc, calloc, malloc mà ko free). Tư tưởng để phòng tránh các vấn đề này là phải có check-point, tức là từ lúc hệ thống hoạt động, có những thời điểm đảm bảo tính chất tĩnh nhất định của bộ nhớ cần cấp phát. Tại các điểm đó chúng ta sẽ kiểm tra kích thước dữ liệu đã cấp phát, dữ liệu này về cơ bản phải là tĩnh, không có sự tăng dần, như vậy nghĩa là chúng ta không bị rò bộ nhớ. Nếu phát hiện dò thì chỉ có cách “mò, lần” từng biến cấp phát.
Do tài nguyên của hệ thống nhúng thường rất hạn chế, vì vậy hạn chế sử dụng các khối dữ liệu có kích thước lớn (tôi thấy thì cỡ vài chục byte là bắt đầu thấy là nhiều rồi :D). Nên khai báo các biến tĩnh và hạn chế sử dụng cấp phát động.

Chia cho số 0, phép khai căn của những số nhỏ hơn 0… (đương nhiên, biến là số được truyền vào trong các phép tính toán số học này).

Biến bị tràn (out of range).

Vấn đề làm việc với con trỏ khi ghi dữ liệu hay gọi con trỏ hàm:
+ Cần valid địa chỉ con trỏ trỏ đến để đảm bảo dữ liệu không ghi ra ngoài vùng mong muốn.
+ Valid địa chỉ con trỏ hàm cần gọi.

Tràn stack: Task stack, System stack. Các hệ điều hành RTOS các service có sẵn để monitor được các vùng stack này là khá hạn chế, đôi khi có nghi ngờ chúng ta phải tự tìm ra cách để monitor chúng. Nghi ngờ tràn stack có thể đến từ việc chương trình gọi đến các thư viện lớn (khi đó sẽ cần đến Task stack lớn), lúc đó cách monitor có thể là đặt các hàm check đỉnh của Stack tại các hàm gọi sâu nhất để chắc chắn rằng vùng stack không bị ghi đè (phân vùng stack cho một task hoàn toàn có thể xem được qua các memory-map file).

2. Vấn đề quản lý ngắt

  • Không có quá trình bảo vệ biến giữa ngắt và chương trình chính, cái này khá phổ biến.
  • Không lường trước được vấn đề interrupt nesting (ngắt chồng ngắt – đang ngắt thì có một ngắt khác), dẫn đến không có các biện pháp bảo vệ cần thiết cho biến, hay cho luồng xử lý của các ISR có quyền ưu tiên thấp.
  • Dead-lock: dead-lock đơn giản được hiểu là quá trình chờ đợi các state khác của hệ thống, các thay đổi giá trị trong hệ thống phù hợp cho xử lý, tuy nhiên trong ngắt mà chờ thì thường là chờ đến… già.
  • Đặc biệt lưu ý với việc sử dụng các thư viện, các service có sẵn ở trong ngắt vì không phải thư viện nào cũng dùng được trong ngắt, service nào cũng dùng được trong ngắt, có các thư viện, có các service sử dụng các hàm lock, wait, delay phải hiểu để không sử dụng chúng, nếu sử dụng thì chương trình sẽ treo vĩnh viễn trong ngắt.
  • Các xử lý trong ngắt cần lưu ý về thời gian chiếm dụng, thời gian chiếm dụng càng lâu thì nguy cơ bị các vấn đề về interrupt nesting, các vấn đề về overload CPU càng lớn, vì vậy nếu có thể hãy để các xử lý ra hết ngoài chương trình chính, ngắt chỉ là nơi để thông báo cần phải xử lý một sự kiện (signal ra chương trình chính).

Một ví dụ cho việc không bảo vệ biến giữa ngắt và chương trình chính:

int a = 0;
void main() {
    //Do somethings
    while(1) {
        if (a == 3) {
            Process(a);
        }
    //Do somethings
    }
}
 
void ISR_Process() {
    a++;
}


Trong đó ISR_Process là hàm xử lý ngắt cho một ngắt bất kỳ nào đó trong hệ thống của bạn. Trong trường hợp sau khi check giá trị biến a bằng 3 xong, bạn expect xử lý một trạng thái của hệ thống qua hàm Process(), giữa 2 sự kiện này xảy ra ngắt, và hàm xử lý ngắt làm tăng giá trị biến lên 1 (a++). Sau khi quay trở lại từ ngắt, hệ thống gọi hàm Process(a), nhưng giá trị a lúc này thực tế đã là 4, điều này làm các thao tác xử lý trong hàm Process mà ta expect khi đó giá trị đầu vào là 3 (thực tế đã là 4) bị sai. Lỗi này có thể khắc phục bằng việc thêm vào các câu lệnh bảo vệ vô hiệu hóa ngắt ISR_Process trước câu lệnh check (a == 3), sau khi xử lý xong hàm Process mới enable ngắt trở lại.

3. Vấn đề khi làm việc với các hệ điều hành

  • Khi mới làm việc trên RTOS, bản thân tôi cũng không hiểu hết được ý nghĩa của việc sử dụng hệ điều hành, và đến bây giờ với tôi khi sử dụng hệ điều hành, thì nghĩa là hệ thống “Event Based System” – hệ thống được trigger bởi các event, không giống như các hệ thống StandAlone (không hệ điều hành) – là các hệ thống dựa trên việc polling trạng thái hệ thống (kết hợp với các ISR). Vì nó là Event Based nên theo tôi hệ thống cần tránh những câu lệnh kiểu như while(condition); hay for (…) ; nghĩa là không để cho hệ thống có những điểm hoạt động vô nghĩa, mà thay vào đó là dạng while (condition) yield(); hoặc while (condition) sleep(N);. Sử dụng hệ điều hành phải là sử dụng hệ thống không có tải vô ích
  • Lỗi phổ biến nữa trong hệ điều hành cũng là lỗi không lock biến trong quá trình truy cập thay đổi giữa task-task, task – interrupt, interrupt – interrupt.
  • Lỗi này tương tự như trong ví dụ phía trên, bạn cũng phải bảo vệ biến khi truy cập giữa các luồng, bằng cách disable việc task switch (thường là scheduler timer của OS), hoặc sử dụng các dịch vụ mà OS hỗ trợ (như semaphore, lock, task_enter_critical) để lock tài nguyên trước khi truy cập, xử lý xong lại unlock để cho task khác truy cập.
  • Wait-Event trong Wait-Event (ví dụ như trong sem_pend lại có một sem_pend khác): đây không thực sự là một lỗi mà đơn giản chỉ là một nguy cơ dẫn đến khó kiểm soát hệ thống, vì khi sem_pend ở vòng trong, nhưng thao tác sem_post lại nằm ở vòng ngoài, dẫn đến dễ bị deadlock về Event.
  • Một kiểu phổ biến nữa hay gặp đó là đôi khi chúng ta không hiểu rõ về các service của OS dẫn đến sử dụng sai, như có những service cho phép hoạt động trong ngắt, có những service không cho phép hoạt động trong ngắt, cần phải hiểu rõ để sử dụng cho đúng. Hay có những service truyền dữ liệu chỉ là truyền dạng con trỏ, thì không được phép giải phóng bộ nhớ cho đến khi yêu cầu xử lý dữ liệu được thực thi thì mới giải phóng.
  • Dead-Lock: deadlock trong hệ điều hành tương tự như deadlock trong các ngắt.

4. Biên dịch và Debug

  • Một số trường hợp chương trình khi build ở chế độ debug thì OK nhưng chuyển sang build release thì chương trình chạy không đúng hoặc bị treo một cách khó hiểu?
    Build ở chế độ Release theo tôi biết IDE sẽ có quá trình tối ưu lại so với việc build ở chế độ Debug (tối ưu các Debug Symbol, bỏ các lệnh, các macro cho assert …), vì vậy khi build ở một chế độ khác với chế độ hay chạy thì Timing (đáp ứng) của hệ thống sẽ khác.