Sử dụng các biến toàn cục trong Hệ thống nhúng


17

Tôi bắt đầu viết firmware cho sản phẩm của mình và tôi là một tân binh ở đây. Tôi đã đi qua nhiều bài viết về việc không sử dụng các biến hoặc hàm toàn cầu. Có giới hạn nào cho việc sử dụng các biến toàn cục trong hệ thống 8 bit hay không hoặc nó hoàn toàn là 'Không-Không'. Tôi nên sử dụng các biến toàn cục trong hệ thống của mình như thế nào hoặc tôi nên tránh hoàn toàn chúng?

Tôi muốn nhận lời khuyên có giá trị từ các bạn về chủ đề này để làm cho phần sụn của tôi nhỏ gọn hơn.


Câu hỏi này không phải là duy nhất cho các hệ thống nhúng. Một bản sao có thể được tìm thấy ở đây .
Lundin

@Lundin Từ liên kết của bạn: "Những ngày này chỉ quan trọng trong môi trường nhúng mà bộ nhớ khá hạn chế. Một số điều cần biết trước khi bạn cho rằng nhúng giống như các môi trường khác và cho rằng các quy tắc lập trình giống nhau trên bảng."
endolith

@endolith staticphạm vi tập tin không giống như "toàn cầu", xem câu trả lời của tôi dưới đây.
Lundin

Câu trả lời:


31

Bạn có thể sử dụng các biến toàn cục thành công, miễn là bạn ghi nhớ các nguyên tắc của @ Phil. Tuy nhiên, đây là một số cách hay để tránh các vấn đề của họ mà không làm cho mã được biên dịch trở nên nhỏ gọn hơn.

  1. Sử dụng các biến tĩnh cục bộ cho trạng thái liên tục mà bạn chỉ muốn truy cập bên trong một hàm.

    #include <stdint.h>
    void skipper()
    {
        static uint8_t skip_initial_cycles = 5;
        if (skip_initial_cycles > 0) {
            skip_initial_cycles -= 1;
            return;
        }
        /* ... */
    }
  2. Sử dụng một cấu trúc để giữ các biến liên quan với nhau, để làm cho nó rõ ràng hơn ở nơi chúng nên được sử dụng và nơi không.

    struct machine_state {
         uint8_t level;
         uint8_t error_code;
    } machine_state;
    
    struct led_state {
        uint8_t red;
        uint8_t green;
        uint8_t blue;
    } led_state;
    
    void machine_change_state()
    {
        machine_state.level += 1;
        /* ... */
        /* We can easily remember not to use led_state in this function. */
    }
    
    void machine_set_io()
    {
        switch (machine_state.level) {
        case 1:
            PIN_MACHINE_IO_A = 1;
            /* ... */
        }
    }
  3. Sử dụng các biến tĩnh toàn cục để làm cho các biến chỉ hiển thị trong tệp C hiện tại. Điều này ngăn chặn truy cập ngẫu nhiên bằng mã trong các tệp khác do xung đột đặt tên.

    /* time_machine.c */
    static uint8_t current_time;
    /* ... */
    
    /* delay.c */
    static uint8_t current_time; /* A completely separate variable for this C file only. */
    /* ... */

Như một lưu ý cuối cùng, nếu bạn đang sửa đổi một biến toàn cục trong một thói quen ngắt và đọc nó ở nơi khác:

  • Đánh dấu biến volatile.
  • Hãy chắc chắn rằng nó là nguyên tử cho CPU (tức là 8 bit cho CPU 8 bit).

HOẶC LÀ

  • Sử dụng một cơ chế khóa để bảo vệ quyền truy cập vào biến.

các bình dễ bay hơi và / hoặc nguyên tử sẽ không giúp bạn tránh được lỗi, bạn cần một số loại khóa / semaphore hoặc để che dấu một cách ngắn gọn các ngắt khi ghi vào biến.
John U

3
Đó là một định nghĩa khá hẹp về "làm việc tốt". Quan điểm của tôi là tuyên bố một cái gì đó không ổn định không ngăn ngừa xung đột. Ngoài ra, ví dụ thứ 3 của bạn không phải là một ý tưởng tuyệt vời - có hai quả cầu riêng biệtcùng tên ít nhất là làm cho mã khó hiểu / duy trì hơn.
John U

1
@ John Bạn không nên sử dụng dễ bay hơi để ngăn chặn các điều kiện cuộc đua, thực sự điều đó sẽ không giúp ích. Bạn nên sử dụng dễ bay hơi để ngăn chặn các lỗi tối ưu hóa trình biên dịch nguy hiểm phổ biến trong trình biên dịch hệ thống nhúng.
Lundin

2
@ John: Việc sử dụng volatilebiến thông thường là cho phép mã chạy trong một bối cảnh thực thi để cho mã trong bối cảnh thực thi khác biết điều gì đó đã xảy ra. Trên hệ thống 8 bit, bộ đệm sẽ chứa một số byte có sức mạnh không quá 128 có thể được quản lý bằng một byte dễ bay hơi cho biết tổng số byte được đặt vào bộ đệm (mod 256) và một cái khác chỉ ra số byte được lấy ra, với điều kiện là chỉ có một bối cảnh thực thi sẽ đưa dữ liệu vào bộ đệm và chỉ một cái lấy dữ liệu ra khỏi nó.
supercat

2
@ John: Mặc dù có thể sử dụng một số hình thức khóa hoặc tạm thời vô hiệu hóa các ngắt để quản lý bộ đệm, nhưng nó thực sự không cần thiết hoặc hữu ích. Nếu bộ đệm phải chứa 128-255 byte, mã hóa sẽ phải thay đổi một chút và nếu nó phải giữ nhiều hơn thế, việc vô hiệu hóa các ngắt có thể là cần thiết, nhưng trên bộ đệm hệ thống 8 bit có thể nhỏ; các hệ thống có bộ đệm lớn hơn thường có thể ghi nguyên tử lớn hơn 8 bit.
supercat

24

Những lý do bạn không muốn sử dụng các biến toàn cục trong hệ thống 8 bit giống như bạn không muốn sử dụng chúng trong bất kỳ hệ thống nào khác: chúng khiến việc lập luận về hành vi của chương trình trở nên khó khăn.

Chỉ những lập trình viên tồi mới bị treo lên theo các quy tắc như "không sử dụng biến toàn cục". Lập trình viên giỏi hiểu lý do đằng sau các quy tắc, sau đó đối xử với các quy tắc giống như hướng dẫn hơn.

Chương trình của bạn có dễ hiểu không? Là hành vi của nó có thể dự đoán được? Có dễ dàng để sửa đổi các phần của nó mà không phá vỡ các phần khác? Nếu câu trả lời cho mỗi câu hỏi này là , thì bạn đang trên đường đến một chương trình tốt.


1
Những gì @MichaelKara nói - hiểu ý nghĩa của những điều này và cách chúng ảnh hưởng đến mọi thứ (và cách chúng có thể cắn bạn) là điều quan trọng.
John U

5

Bạn hoàn toàn không nên tránh sử dụng các biến toàn cục (gọi tắt là "toàn cầu"). Nhưng, bạn nên sử dụng chúng một cách thận trọng. Các vấn đề thực tế với việc sử dụng quá mức toàn cầu:

  • Globals có thể nhìn thấy trong suốt đơn vị biên dịch. Bất kỳ mã nào trong đơn vị biên dịch có thể sửa đổi toàn cục. Hậu quả của việc sửa đổi có thể xuất hiện ở bất cứ nơi nào mà toàn cầu này được đánh giá.
  • Kết quả là toàn cầu làm cho mã khó đọc và hiểu hơn. Lập trình viên luôn phải ghi nhớ tất cả những nơi mà toàn cầu được đánh giá và phân công.
  • Việc sử dụng quá mức toàn cầu làm cho mã dễ bị lỗi hơn.

Đó là một cách thực hành tốt để thêm tiền tố g_vào tên của các biến toàn cục. Ví dụ , g_iFlags. Khi bạn thấy biến có tiền tố trong mã, bạn ngay lập tức nhận ra rằng đó là toàn cầu.


2
Cờ không phải là một toàn cầu. ISR có thể gọi một hàm có biến tĩnh chẳng hạn.
Phil Frost

+1 Tôi chưa từng nghe về thủ thuật như vậy trước đây. Tôi đã xóa đoạn đó khỏi câu trả lời. Làm thế nào để staticcờ trở nên hiển thị với main()? Bạn có ngụ ý rằng cùng một chức năng có staticthể trả lại cho main()sau này?
Nick Alexeev

Đó là một cách để làm điều đó. Có lẽ hàm lấy trạng thái mới để đặt và trả về trạng thái cũ. Có rất nhiều cách khác. Có thể bạn có một tệp nguồn với một hàm để đặt cờ và một tệp khác để lấy nó, với một biến toàn cục tĩnh giữ trạng thái cờ. Mặc dù về mặt kỹ thuật, đây là "toàn cầu" theo thuật ngữ C, phạm vi của nó chỉ giới hạn ở tệp đó, chỉ chứa các chức năng cần biết. Đó là, phạm vi của nó không phải là "toàn cầu", đó thực sự là vấn đề. C ++ cung cấp các cơ chế đóng gói bổ sung.
Phil Frost

4
Một số phương pháp tránh toàn cầu (chẳng hạn như chỉ truy cập phần cứng thông qua trình điều khiển thiết bị) có thể quá kém hiệu quả đối với môi trường 8 bit bị thiếu tài nguyên nghiêm trọng.
Spehro Pefhany

1
@SpehroPefhany Lập trình viên giỏi hiểu lý do đằng sau các quy tắc, sau đó coi các quy tắc giống như hướng dẫn hơn. Khi các hướng dẫn bị xung đột, lập trình viên giỏi cân nhắc cân bằng một cách cẩn thận.
Phil Frost

4

Ưu điểm của cấu trúc dữ liệu toàn cầu trong công việc nhúng là chúng là tĩnh. Nếu mọi biến bạn cần là toàn cục, thì bạn sẽ không bao giờ vô tình hết bộ nhớ khi các chức năng được nhập và không gian được tạo cho chúng trên ngăn xếp. Nhưng sau đó, tại thời điểm đó tại sao có chức năng? Tại sao không phải là một chức năng lớn xử lý tất cả logic và quy trình - như chương trình BASIC không cho phép GOSUB. Nếu bạn đưa ý tưởng này đủ xa, bạn sẽ có một chương trình hợp ngữ điển hình từ những năm 1970. Hiệu quả, và không thể duy trì và rắc rối bắn.

Vì vậy, hãy sử dụng toàn cầu một cách thận trọng, như các biến trạng thái (ví dụ: nếu mọi hàm cần biết hệ thống đang ở trạng thái diễn giải hay chạy trạng thái) và các cấu trúc dữ liệu khác cần được xem bởi nhiều hàm và như @PhilFrost nói, là hành vi của chức năng của bạn có thể dự đoán? Có khả năng lấp đầy ngăn xếp với một chuỗi đầu vào không bao giờ kết thúc không? Đây là những vấn đề cho thiết kế thuật toán.

Lưu ý rằng tĩnh có ý nghĩa khác nhau bên trong và bên ngoài một chức năng. /programming/5868947/difference-b between-static-variable-inside-and-outide-of-a-hàm

/programming/5033627/static-variable-inside-of-a-f ghép-in-c


1
Nhiều trình biên dịch hệ thống nhúng phân bổ các biến tự động một cách tĩnh, nhưng các biến lớp phủ được sử dụng bởi các hàm không thể có trong phạm vi đồng thời; điều này thường mang lại mức sử dụng bộ nhớ tương đương với mức sử dụng trong trường hợp xấu nhất có thể xảy ra đối với một hệ thống dựa trên ngăn xếp trong đó tất cả các chuỗi cuộc gọi có thể có trong thực tế có thể xảy ra.
supercat

4

Biến toàn cầu chỉ nên được sử dụng cho nhà nước toàn cầu thực sự. Sử dụng một biến toàn cục để biểu thị một cái gì đó như ví dụ vĩ độ của ranh giới phía bắc của bản đồ sẽ chỉ hoạt động nếu chỉ có thể có một "ranh giới phía bắc của bản đồ". Nếu trong tương lai, mã có thể phải hoạt động với nhiều bản đồ có ranh giới phía bắc khác nhau, mã sử dụng biến toàn cục cho ranh giới phía bắc có thể sẽ cần phải được làm lại.

Trong các ứng dụng máy tính thông thường, thường không có lý do cụ thể nào để cho rằng sẽ không bao giờ có nhiều hơn một thứ gì đó. Tuy nhiên, trong các hệ thống nhúng, các giả định như vậy thường hợp lý hơn nhiều. Mặc dù có thể một chương trình máy tính thông thường có thể được yêu cầu hỗ trợ nhiều người dùng đồng thời, giao diện người dùng của một hệ thống nhúng thông thường sẽ được thiết kế để hoạt động bởi một người dùng tương tác với các nút và màn hình. Như vậy, bất cứ lúc nào nó cũng có một trạng thái giao diện người dùng. Thiết kế hệ thống sao cho nhiều người dùng có thể tương tác với nhiều bàn phím và màn hình sẽ đòi hỏi sự phức tạp hơn rất nhiều và mất nhiều thời gian hơn để thực hiện, hơn là thiết kế nó cho một người dùng. Nếu hệ thống không bao giờ được yêu cầu hỗ trợ nhiều người dùng, bất kỳ nỗ lực bổ sung nào được đầu tư để tạo điều kiện cho việc sử dụng như vậy sẽ bị lãng phí. Trừ khi có khả năng sẽ cần hỗ trợ nhiều người dùng, sẽ có nguy cơ khôn ngoan hơn khi phải loại bỏ mã được sử dụng cho giao diện một người dùng trong trường hợp cần hỗ trợ nhiều người dùng, hơn là dành thêm thời gian để thêm nhiều người hỗ trợ người dùng có thể sẽ không bao giờ cần thiết.

Một yếu tố liên quan với các hệ thống nhúng là trong nhiều trường hợp (đặc biệt là liên quan đến giao diện người dùng), cách thực tế duy nhất để hỗ trợ có nhiều hơn một thứ sẽ là sử dụng nhiều luồng. Trong trường hợp không có một số nhu cầu khác về đa luồng, có lẽ nên sử dụng một thiết kế đơn luồng đơn giản hơn là tăng độ phức tạp của hệ thống với đa luồng có thể không bao giờ thực sự cần thiết. Nếu thêm nhiều hơn một thứ gì đó sẽ yêu cầu thiết kế lại hệ thống khổng lồ, thì nó cũng không thành vấn đề nếu nó cũng yêu cầu làm lại việc sử dụng một số biến toàn cục.


Vì vậy, việc giữ các biến toàn cầu sẽ không đụng độ với nhau sẽ không phải là vấn đề đúng. ví dụ: Day_cntr, week_cntr, v.v ... để đếm ngày và tuần tương ứng. Và tôi tin rằng người ta không nên cố tình sử dụng nhiều biến toàn cục giống với mục đích tương tự và phải xác định rõ ràng. Cảm ơn rất nhiều cho phản ứng áp đảo. :)
Rookie91

-1

Rất nhiều người nhầm lẫn về chủ đề này. Định nghĩa của một biến toàn cục là:

Một cái gì đó có thể truy cập từ mọi nơi trong chương trình của bạn.

Đây không giống như các biến phạm vi tệp , được khai báo bởi từ khóa static. Chúng không phải là biến toàn cục, chúng là biến riêng cục bộ.

 int x; // global variable
 static int y; // file scope variable

 void some_func (void) {...} // added to demonstrate that the variables above are at file scope.

Bạn có nên sử dụng các biến toàn cầu? Có một vài trường hợp là ổn:

Trong mọi trường hợp khác, bạn sẽ không sử dụng các biến toàn cục. Không bao giờ có một lý do để làm như vậy. Thay vào đó sử dụng các biến phạm vi tập tin , đó là hoàn toàn tốt.

Bạn nên cố gắng viết các mô-đun mã độc lập, tự trị được thiết kế để thực hiện một nhiệm vụ cụ thể. Trong các mô-đun đó, các biến phạm vi tệp nội bộ sẽ nằm trong các thành viên dữ liệu riêng tư. Phương pháp thiết kế này được gọi là hướng đối tượng và được công nhận rộng rãi là thiết kế tốt.


Tại sao các downvote?
m.Alin

2
Tôi nghĩ rằng "biến toàn cục" cũng có thể được sử dụng để mô tả phân bổ cho một phân khúc toàn cầu (không phải văn bản, ngăn xếp hoặc đống). Theo nghĩa đó, các biến tĩnh và hàm tĩnh là / có thể là "toàn cục". Trong bối cảnh của câu hỏi này, có phần rõ ràng rằng toàn cầu đang đề cập đến phạm vi phân khúc không phân bổ (mặc dù có thể OP không biết điều này).
Paul A. Clayton

1
@ PaulA.Clayton Tôi chưa bao giờ nghe về một thuật ngữ chính thức gọi là "phân khúc bộ nhớ toàn cầu". Các biến của bạn sẽ kết thúc một trong các vị trí sau: thanh ghi hoặc ngăn xếp (phân bổ thời gian chạy), heap (phân bổ thời gian chạy), phân đoạn .data (biến lưu trữ tĩnh được khởi tạo rõ ràng), phân đoạn .bss (biến lưu trữ tĩnh không), .rodata (đọc hằng số -only) hoặc .text (một phần của mã). Nếu họ kết thúc ở bất cứ nơi nào khác, đó là một số thiết lập dành riêng cho dự án.
Lundin

1
@ PaulA.Clayton Tôi nghi ngờ rằng những gì bạn gọi là "phân khúc toàn cầu" là .dataphân khúc.
Lundin
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.