Tránh các biến toàn cục khi sử dụng ngắt trong các hệ thống nhúng


13

Có cách nào tốt để thực hiện giao tiếp giữa ISR và phần còn lại của chương trình cho một hệ thống nhúng để tránh các biến toàn cục không?

Dường như mô hình chung là có một biến toàn cục được chia sẻ giữa ISR và phần còn lại của chương trình và được sử dụng làm cờ, nhưng việc sử dụng các biến toàn cục này đi ngược lại với tôi. Tôi đã bao gồm một ví dụ đơn giản sử dụng ISR kiểu avr-libc:

volatile uint8_t flag;

int main() {
    ...

    if (flag == 1) {
        ...
    }
    ...
}

ISR(...) {
    ...
    flag = 1;
    ...
}

Tôi không thể nhìn thấy xung quanh những gì thực chất là một vấn đề phạm vi; bất kỳ biến nào có thể truy cập được bởi cả ISR và phần còn lại của chương trình phải là toàn cầu, chắc chắn? Mặc dù vậy, tôi thường thấy mọi người nói những điều dọc theo "các biến toàn cầu là một cách để thực hiện giao tiếp giữa các ISR và phần còn lại của chương trình" (nhấn mạnh của tôi), dường như ngụ ý rằng có các phương pháp khác; Nếu có các phương pháp khác, chúng là gì?



1
Không nhất thiết là TẤT CẢ phần còn lại của chương trình sẽ có quyền truy cập; nếu bạn khai báo biến là tĩnh, chỉ có tệp trong đó biến được khai báo sẽ thấy nó. Không khó để có các biến có thể nhìn thấy trong toàn bộ một tệp, nhưng không phải là phần còn lại của chương trình và điều đó có thể giúp ích.
DiBosco

1
bên cạnh, cờ phải được khai báo là không ổn định, vì bạn đang sử dụng / thay đổi nó bên ngoài luồng chương trình bình thường. Điều này buộc trình biên dịch không tối ưu hóa bất kỳ đọc / ghi thành cờ và thực hiện thao tác đọc / ghi thực tế.
hack tiếp theo

@ next-hack Có, điều đó hoàn toàn chính xác, xin lỗi tôi chỉ đang cố gắng đưa ra một ví dụ nhanh chóng.

Câu trả lời:


18

Có một cách tiêu chuẩn thực tế để làm điều này (giả sử lập trình C):

  • Ngắt / ISR ở mức độ thấp và do đó chỉ nên được thực hiện bên trong trình điều khiển liên quan đến phần cứng tạo ra ngắt. Họ không nên được đặt ở bất cứ nơi nào khác ngoài tài xế đó.
  • Tất cả các giao tiếp với ISR ​​chỉ được thực hiện bởi người lái xe và người lái xe. Nếu các phần khác của chương trình cần truy cập vào thông tin đó, nó phải yêu cầu nó từ trình điều khiển thông qua các hàm setter / getter hoặc tương tự.
  • Bạn không nên khai báo các biến "toàn cầu". Biến phạm vi tập tin ý nghĩa toàn cầu với liên kết bên ngoài. Đó là: các biến có thể được gọi bằng externtừ khóa hoặc đơn giản là do nhầm lẫn.
  • Thay vào đó, để buộc đóng gói riêng bên trong trình điều khiển, tất cả các biến như vậy được chia sẻ giữa trình điều khiển và ISR sẽ được khai báo static. Một biến như vậy không phải là toàn cục mà chỉ giới hạn ở tệp được khai báo.
  • Để ngăn chặn các vấn đề tối ưu hóa trình biên dịch, các biến đó cũng nên được khai báo là volatile. Lưu ý: điều này không cung cấp quyền truy cập nguyên tử hoặc giải quyết quyền truy cập lại!
  • Một số cách của cơ chế nhập lại thường là cần thiết trong trình điều khiển, trong trường hợp ISR ghi vào biến. Ví dụ: vô hiệu hóa ngắt, mặt nạ ngắt toàn cầu, semaphore / mutex hoặc đọc nguyên tử được bảo đảm.

Lưu ý: bạn có thể phải hiển thị nguyên mẫu hàm ISR thông qua một tiêu đề, để đặt nó trong một bảng vectơ nằm trong một tệp khác. Nhưng đó không phải là vấn đề miễn là bạn chứng minh rằng đó là một sự gián đoạn và không nên được chương trình gọi.
Lundin

Bạn sẽ nói gì, nếu đối số là giá trị gia tăng (và mã bổ sung) của việc sử dụng các hàm setter / get? Tôi đã tự mình trải qua điều này, suy nghĩ về các tiêu chuẩn mã cho các thiết bị nhúng 8 bit của chúng tôi.
Leroy105

2
@ Leroy105 Ngôn ngữ C đã hỗ trợ các chức năng nội tuyến vĩnh viễn. Mặc dù ngay cả việc sử dụng inlineđang trở nên lỗi thời, vì trình biên dịch ngày càng thông minh hơn và thông minh hơn trong việc tối ưu hóa mã. Tôi muốn nói rằng lo lắng về chi phí là "tối ưu hóa trước khi trưởng thành" - trong hầu hết các trường hợp, chi phí không thành vấn đề, nếu nó hoàn toàn có mặt trong mã máy.
Lundin

2
Điều đó đang được nói, trong trường hợp viết trình điều khiển ISR, khoảng 80-90% tất cả các lập trình viên (không phóng đại ở đây) luôn nhận được điều gì đó ở họ. Kết quả là các lỗi tinh vi: cờ bị xóa không chính xác, tối ưu hóa trình biên dịch không chính xác từ thiếu biến động, điều kiện cuộc đua, hiệu suất thời gian thực tệ hại, tràn ngăn xếp, v.v. tăng thêm. Tập trung vào việc viết một trình điều khiển miễn phí lỗi trước khi lo lắng về những điều quan tâm ngoại vi, chẳng hạn như nếu setter / getters giới thiệu một chút chi phí nhỏ.
Lundin

10
việc sử dụng các biến toàn cầu này đi ngược lại với tôi

Đây thực sự là vấn đề. Hãy vượt qua nó.

Bây giờ trước khi những kẻ đầu gối lập tức phát cuồng về việc điều này là ô uế, hãy để tôi đủ điều kiện một chút. Chắc chắn có nguy hiểm trong việc sử dụng các biến toàn cầu để vượt quá. Nhưng, chúng cũng có thể tăng hiệu quả, đôi khi là vấn đề trong các hệ thống hạn chế tài nguyên nhỏ.

Điều quan trọng là suy nghĩ về thời điểm bạn có thể sử dụng chúng một cách hợp lý và không có khả năng khiến bản thân gặp rắc rối, thay vì một lỗi chỉ chờ xảy ra. Luôn có sự đánh đổi. Mặc dù nói chung tránh các biến toàn cục để giao tiếp giữa mã ngắt và tiền cảnh là một hướng dẫn có thể thực hiện được, việc đưa nó, giống như hầu hết các hướng dẫn khác, đến cực đoan của tôn giáo là phản tác dụng.

Một số ví dụ đôi khi tôi sử dụng các biến toàn cục để truyền thông tin giữa mã ngắt và tiền cảnh là:

  1. Đồng hồ đánh dấu quầy được quản lý bởi hệ thống ngắt đồng hồ. Tôi thường có một ngắt đồng hồ định kỳ chạy cứ sau 1 ms. Điều đó thường hữu ích cho thời gian khác nhau trong hệ thống. Một cách để đưa thông tin này ra khỏi thói quen gián đoạn đến nơi mà phần còn lại của hệ thống có thể sử dụng nó là giữ một bộ đếm đồng hồ toàn cầu. Thói quen ngắt làm tăng bộ đếm mỗi tích tắc. Mã tiền cảnh có thể đọc quầy bất cứ lúc nào. Thường thì tôi làm điều này trong 10 ms, 100 ms và thậm chí là 1 giây.

    Tôi chắc chắn rằng các dấu 1 ms, 10 ms và 100 ms có kích thước từ có thể được đọc trong một hoạt động nguyên tử duy nhất. Nếu sử dụng ngôn ngữ cấp cao, hãy đảm bảo thông báo cho trình biên dịch rằng các biến này có thể thay đổi không đồng bộ. Trong C, bạn khai báo chúng extern volility , ví dụ. Tất nhiên đây là thứ đi vào một tệp bao gồm đóng hộp, vì vậy bạn không cần phải nhớ nó cho mọi dự án.

    Đôi khi, tôi làm cho bộ đếm 1 giây của bộ đếm thời gian đã trôi qua, vì vậy hãy làm cho nó rộng 32 bit. Điều đó không thể được đọc trong một hoạt động nguyên tử duy nhất trên nhiều vi mô nhỏ mà tôi sử dụng, do đó không được thực hiện trên toàn cầu. Thay vào đó, một thói quen được cung cấp để đọc giá trị nhiều từ, xử lý các cập nhật có thể có giữa các lần đọc và trả về kết quả.

    Tất nhiên là có thể có các thói quen để có được các bộ đếm nhỏ hơn 1 ms, 10 ms, v.v. Tuy nhiên, điều đó thực sự rất ít đối với bạn, thêm rất nhiều hướng dẫn thay cho việc đọc một từ duy nhất và sử dụng hết một vị trí ngăn xếp cuộc gọi khác.

    Nhược điểm là gì? Tôi cho rằng ai đó có thể tạo ra một lỗi đánh máy vô tình ghi vào một trong các quầy, sau đó có thể làm rối loạn thời gian khác trong hệ thống. Viết thư cho một bộ đếm có chủ ý sẽ không có ý nghĩa, vì vậy loại lỗi này sẽ cần phải là một thứ gì đó không chủ ý như một lỗi đánh máy. Có vẻ rất khó xảy ra. Tôi không nhớ rằng đã từng xảy ra trong hơn 100 dự án vi điều khiển nhỏ.

  2. Các giá trị A / D được lọc và điều chỉnh cuối cùng. Một điều phổ biến cần làm là có một bài đọc xử lý thường xuyên ngắt từ A / D. Tôi thường đọc các giá trị tương tự nhanh hơn mức cần thiết, sau đó áp dụng một bộ lọc thông thấp. Cũng thường có tỷ lệ và bù được áp dụng.

    Ví dụ, A / D có thể đang đọc đầu ra 0 đến 3 V của bộ chia điện áp để đo nguồn cung cấp 24 V. Nhiều bài đọc được chạy qua một số bộ lọc, sau đó thu nhỏ lại để giá trị cuối cùng tính bằng millivolts. Nếu nguồn cung ở mức 24,015 V, thì giá trị cuối cùng là 24015.

    Phần còn lại của hệ thống chỉ nhìn thấy một giá trị được cập nhật trực tiếp cho biết điện áp cung cấp. Nó không biết và cũng không cần quan tâm khi chính xác nó được cập nhật, đặc biệt là vì nó được cập nhật thường xuyên hơn nhiều so với thời gian giải quyết bộ lọc thông thấp.

    Một lần nữa, một thói quen giao diện có thể được sử dụng, nhưng bạn nhận được rất ít lợi ích từ đó. Chỉ cần sử dụng biến toàn cục bất cứ khi nào bạn cần điện áp nguồn sẽ đơn giản hơn nhiều. Hãy nhớ rằng sự đơn giản không chỉ dành cho máy, nhưng đơn giản hơn cũng có nghĩa là ít có khả năng xảy ra lỗi của con người.


Tôi đã đi trị liệu, trong một tuần chậm chạp, thực sự cố gắng để mã hóa mã của tôi. Tôi thấy quan điểm của Lundin về việc hạn chế quyền truy cập biến, nhưng tôi nhìn vào các hệ thống thực tế của mình và nghĩ rằng đó là một khả năng từ xa BẤT K P CÁ NHÂN NÀO thực sự sẽ đưa ra một biến toàn cầu quan trọng của hệ thống. Các chức năng Getter / Setter cuối cùng khiến bạn phải trả phí thay vì chỉ sử dụng toàn cầu và chấp nhận đây là những chương trình khá đơn giản ...
Leroy105

3
@ Leroy105 Vấn đề không phải là "những kẻ khủng bố" cố tình lạm dụng biến toàn cầu. Ô nhiễm không gian tên có thể là một vấn đề trong các dự án lớn hơn, nhưng điều đó có thể được giải quyết với việc đặt tên tốt. Không, vấn đề thực sự là lập trình viên cố gắng sử dụng biến toàn cục như dự định, nhưng không thực hiện đúng. Hoặc là vì họ không nhận ra vấn đề về điều kiện chủng tộc tồn tại với tất cả các ISR, hoặc vì họ làm rối tung việc thực hiện cơ chế bảo vệ bắt buộc, hoặc đơn giản là vì họ đã sử dụng biến toàn cục trên toàn bộ mã, tạo ra sự liên kết chặt chẽ và mã không thể đọc được.
Lundin

Điểm của bạn là Olin hợp lệ, nhưng ngay cả trong các ví dụ này, việc thay thế extern int ticks10msbằng inline int getTicks10ms()sẽ hoàn toàn không có sự khác biệt trong tập hợp đã biên dịch, trong khi mặt khác, nó sẽ khó vô tình thay đổi giá trị của nó trong các phần khác của chương trình, và cũng cho phép bạn một cách để "nối" vào cuộc gọi này (ví dụ để giả định thời gian trong quá trình kiểm tra đơn vị, để ghi nhật ký quyền truy cập vào biến này hoặc bất cứ điều gì). Ngay cả khi bạn lập luận rằng cơ hội của một lập trình viên san thay đổi biến này thành 0, thì cũng không có chi phí cho một getter nội tuyến.
Groo

@Groo: Điều đó chỉ đúng nếu bạn đang sử dụng ngôn ngữ hỗ trợ các hàm nội tuyến và điều đó có nghĩa là định nghĩa của hàm getter cần phải hiển thị cho tất cả mọi người. Trên thực tế khi sử dụng một ngôn ngữ cấp cao, tôi sử dụng các hàm getter nhiều hơn và các biến toàn cục ít hơn. Trong lắp ráp, việc lấy giá trị của biến toàn cục sẽ dễ dàng hơn rất nhiều so với việc bận tâm với hàm getter.
Olin Lathrop

Tất nhiên, nếu bạn không thể nội tuyến, thì sự lựa chọn không đơn giản như vậy. Tôi muốn nói rằng với các hàm được nội tuyến (và nhiều trình biên dịch C99 trước đã được hỗ trợ các phần mở rộng nội tuyến), hiệu suất không thể là một đối số chống lại getters. Với một trình biên dịch tối ưu hóa hợp lý, bạn sẽ kết thúc với cùng một hội đồng được sản xuất.
Groo

2

Bất kỳ gián đoạn cụ thể sẽ là một nguồn tài nguyên toàn cầu. Tuy nhiên, đôi khi, có thể hữu ích khi có một vài ngắt chia sẻ cùng một mã. Ví dụ, một hệ thống có thể có một số UART, tất cả đều nên sử dụng logic gửi / nhận tương tự.

Một cách tiếp cận tốt để xử lý đó là đặt những thứ được sử dụng bởi trình xử lý ngắt, hoặc con trỏ tới chúng, trong một đối tượng cấu trúc, và sau đó có các trình xử lý ngắt phần cứng thực tế giống như:

void UART1_handler(void) { uart_handler(&uart1_info); }
void UART2_handler(void) { uart_handler(&uart2_info); }
void UART3_handler(void) { uart_handler(&uart3_info); }

Các đối tượng uart1_info, uart2_infov.v. sẽ là các biến toàn cục, nhưng chúng sẽ là duy nhất biến toàn cục được sử dụng bởi các trình xử lý ngắt. Mọi thứ khác mà những người xử lý sẽ chạm vào sẽ được xử lý trong những người đó.

Lưu ý rằng bất cứ điều gì được truy cập bởi cả trình xử lý ngắt và mã dòng chính đều phải đủ điều kiện volatile. Có thể đơn giản nhất là chỉ cần khai báo là volatiletất cả mọi thứ sẽ được sử dụng bởi trình xử lý ngắt, nhưng nếu hiệu suất là quan trọng, người ta có thể muốn viết mã sao chép thông tin vào các giá trị tạm thời, vận hành theo chúng, sau đó viết lại chúng. Ví dụ: thay vì viết:

if (foo->timer)
  foo->timer--;

viết:

uint32_t was_timer;
was_timer = foo->timer;
if (was_timer)
{
  was_timer--;
  foo->timer = was_timer;
}

Cách tiếp cận trước đây có thể dễ đọc và dễ hiểu hơn, nhưng sẽ kém hiệu quả hơn phương pháp sau. Cho dù đó là một mối quan tâm sẽ phụ thuộc vào ứng dụng.


0

Đây là ba ý tưởng:

Khai báo biến cờ là tĩnh để giới hạn phạm vi trong một tệp duy nhất.

Đặt biến cờ riêng tư và sử dụng các hàm getter và setter để truy cập giá trị cờ.

Sử dụng một đối tượng báo hiệu như semaphore thay vì biến cờ. ISR sẽ thiết lập / đăng semaphore.


0

Một ngắt (tức là vectơ chỉ đến trình xử lý của bạn) là một tài nguyên toàn cầu. Vì vậy, ngay cả khi bạn sử dụng một số biến trên ngăn xếp hoặc trên heap:

volatile bool *flag;  // must be initialized before the interrupt is enabled

ISR(...) {
    *flag = true;
}

hoặc mã hướng đối tượng có chức năng 'ảo':

HandlerObject *obj;

ISR(...) {
    obj->handler_function(obj);
}

Bước đầu tiên phải liên quan đến một biến toàn cầu thực tế (hoặc ít nhất là tĩnh) để tiếp cận dữ liệu khác đó.

Tất cả các cơ chế này thêm một sự gián tiếp, vì vậy điều này thường không được thực hiện nếu bạn muốn nén chu trình cuối cùng ra khỏi trình xử lý ngắt.


bạn nên khai báo cờ là int *.
hack tiếp theo

0

Hiện tại tôi đang mã hóa cho Cortex M0 / M4 và cách tiếp cận chúng tôi đang sử dụng trong C ++ (không có thẻ C ++, vì vậy câu trả lời này có thể lạc đề) như sau:

Chúng tôi sử dụng một lớp CInterruptVectorTablecó chứa tất cả các thói quen dịch vụ ngắt được lưu trữ trong vectơ ngắt thực tế của bộ điều khiển:

#pragma location = ".intvec"
extern "C" const intvec_elem __vector_table[] =
{
  { .__ptr = __sfe( "CSTACK" ) },           // 0x00
  __iar_program_start,                      // 0x04

  CInterruptVectorTable::IsrNMI,            // 0x08
  CInterruptVectorTable::IsrHardFault,      // 0x0C
  //[...]
}

Lớp CInterruptVectorTablethực hiện một sự trừu tượng hóa các vectơ ngắt, vì vậy bạn có thể liên kết các hàm khác nhau với các vectơ ngắt trong thời gian chạy.

Giao diện của lớp đó trông như thế này:

class CInterruptVectorTable  {
public :
    typedef void (*IsrCallbackfunction_t)(void);                      

    enum InterruptId_t {
        INTERRUPT_ID_NMI,
        INTERRUPT_ID_HARDFAULT,
        //[...]
    };

    typedef struct InterruptVectorTable_t {
        IsrCallbackfunction_t IsrNMI;
        IsrCallbackfunction_t IsrHardFault;
        //[...]
    } InterruptVectorTable_t;

    typedef InterruptVectorTable_t* PinterruptVectorTable_t;


public :
    CInterruptVectorTable(void);
    void SetIsrCallbackfunction(const InterruptId_t& interruptID, const IsrCallbackfunction_t& isrCallbackFunction);

private :

    static void IsrStandard(void);

public :
    static void IsrNMI(void);
    static void IsrHardFault(void);
    //[...]

private :

    volatile InterruptVectorTable_t virtualVectorTable;
    static volatile CInterruptVectorTable* pThis;
};

Bạn cần tạo các hàm được lưu trữ trong bảng vectơ staticvì bộ điều khiển không thể cung cấp một con thistrỏ vì bảng vectơ không phải là một đối tượng. Vì vậy, để giải quyết vấn đề đó, chúng ta có con pThistrỏ tĩnh bên trong CInterruptVectorTable. Khi nhập một trong các hàm ngắt tĩnh, nó có thể truy cập vào pThis-pulum để có quyền truy cập vào các thành viên của một đối tượng CInterruptVectorTable.


Bây giờ trong chương trình, bạn có thể sử dụng SetIsrCallbackfunctionđể cung cấp một con trỏ hàm cho một statichàm được gọi khi xảy ra gián đoạn. Các con trỏ được lưu trữ trongInterruptVectorTable_t virtualVectorTable .

Và việc thực hiện một chức năng ngắt trông như thế này:

void CInterruptVectorTable::IsrNMI(void) {
    pThis->virtualVectorTable.IsrNMI(); 
}

Vì vậy, nó sẽ gọi một staticphương thức của một lớp khác (có thể là private), sau đó có thể chứa một con static thistrỏ khác để có quyền truy cập vào các biến thành viên của đối tượng đó (chỉ một).

Tôi đoán bạn có thể xây dựng và giao diện thích IInterruptHandlervà lưu trữ các con trỏ tới các đối tượng, vì vậy bạn không cần con trỏ static thistrong tất cả các lớp đó. (có lẽ chúng tôi thử điều đó trong lần lặp lại tiếp theo của kiến ​​trúc của chúng tôi)

Cách tiếp cận khác hoạt động tốt đối với chúng tôi, vì các đối tượng duy nhất được phép thực hiện trình xử lý ngắt là những đối tượng bên trong lớp trừu tượng phần cứng và chúng tôi thường chỉ có một đối tượng cho mỗi khối phần cứng, do đó, nó hoạt động tốt với các con static thistrỏ. Và lớp trừu tượng phần cứng cung cấp một sự trừu tượng hóa khác cho các ngắt, được gọi ICallbacklà lớp sau đó được triển khai trong lớp thiết bị phía trên phần cứng.


Bạn có truy cập dữ liệu toàn cầu? Chắc chắn bạn làm như vậy, nhưng bạn có thể làm cho hầu hết các dữ liệu toàn cầu cần thiết ở chế độ riêng tư như các con thistrỏ và các hàm ngắt.

Nó không chống đạn, và nó có thêm chi phí. Bạn sẽ đấu tranh để thực hiện ngăn xếp IO-Link bằng cách sử dụng phương pháp này. Nhưng nếu bạn không quá chặt chẽ với thời gian, điều này hoạt động khá tốt để có được sự trừu tượng hóa linh hoạt của các ngắt và giao tiếp trong các mô-đun mà không sử dụng các biến toàn cục có thể truy cập từ mọi nơi.


1
"Vì vậy, bạn có thể liên kết các chức năng khác nhau với các vectơ ngắt trong thời gian chạy" Điều này nghe có vẻ là một ý tưởng tồi. "Độ phức tạp chu kỳ" của chương trình sẽ chỉ đi qua mái nhà. Tất cả các kết hợp ca sử dụng sẽ phải được kiểm tra sao cho không có xung đột sử dụng thời gian cũng như ngăn xếp. Rất nhiều đau đầu cho một tính năng với IMO hữu ích rất hạn chế. (Trừ khi bạn có trường hợp bộ nạp khởi động, đó là một câu chuyện khác) Nhìn chung, điều này có mùi lập trình meta.
Lundin

@Lundin Tôi không thực sự thấy quan điểm của bạn. Chúng tôi sử dụng nó để liên kết ví dụ ngắt DMA với trình xử lý ngắt SPI nếu DMA được sử dụng cho SPI và cho trình xử lý ngắt UART nếu nó được sử dụng cho UART. Cả hai xử lý phải được kiểm tra, chắc chắn, nhưng không phải là một vấn đề. Và nó chắc chắn không có gì để làm với lập trình meta.
Arsenal

DMA là một điều, việc gán thời gian chạy của vectơ ngắt là một thứ hoàn toàn khác. Thật hợp lý khi để thiết lập trình điều khiển DMA thay đổi, trong thời gian chạy. Một bảng vector, không quá nhiều.
Lundin

@Lundin Tôi đoán chúng tôi có quan điểm khác nhau về điều đó, chúng tôi có thể bắt đầu một cuộc trò chuyện về nó, bởi vì tôi vẫn không thấy vấn đề của bạn với nó - vì vậy có thể câu trả lời của tôi được viết rất tệ, rằng toàn bộ khái niệm bị hiểu sai.
Arsenal
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.