임베디드 시스템에서 인터럽트를 사용할 때 전역 변수 피하기


13

전역 변수를 피하는 내장 시스템의 ISR과 나머지 프로그램간에 통신을 구현하는 좋은 방법이 있습니까?

일반적인 패턴은 ISR과 나머지 프로그램 사이에서 공유되고 플래그로 사용되는 전역 변수를 갖는 것으로 보이지만, 전역 변수의 사용은 그레인에 맞지 않습니다. avr-libc 스타일 ISR을 사용하는 간단한 예제를 포함 시켰습니다.

volatile uint8_t flag;

int main() {
    ...

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

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

본질적으로 범위 지정 문제가 무엇인지는 알 수 없습니다. ISR과 나머지 프로그램 모두가 액세스 할 수있는 변수는 본질적으로 전역 적이어야합니까? 그럼에도 불구하고 나는 사람들이 "글로벌 변수는 ISR과 프로그램의 나머지 부분 사이의 의사 소통을 구현 하는 한 가지 방법 "(강조 광산) 이라는 행을 따라 말하는 것을 종종 보았습니다 . 다른 방법이 있다면 무엇입니까?



1
프로그램의 나머지 부분이 모두 액세스 할 수있는 것은 아닙니다. 변수를 정적으로 선언하면 변수가 선언 된 파일 만 볼 수 있습니다. 하나의 파일 전체에서 볼 수 있지만 나머지 프로그램에서는 볼 수 없으며 도움이 될 수있는 변수를 갖는 것은 결코 어렵지 않습니다.
DiBosco

1
또한 일반 프로그램 흐름 외부에서 플래그를 사용 / 변경하고 있으므로 플래그를 휘발성으로 선언해야합니다. 이로 인해 컴파일러는 읽기 / 쓰기 플래그를 최적화하지 않고 실제 읽기 / 쓰기 작업을 수행합니다.
next-hack

@ next-hack 그렇습니다. 정확하게 맞습니다. 죄송합니다. 예제를 빨리 찾으려고 노력했습니다.

답변:


18

이를 수행하는 사실상의 표준 방법이 있습니다 (C 프로그래밍 가정).

  • 인터럽트 / ISR은 하위 수준이므로 인터럽트를 생성하는 하드웨어와 관련된 드라이버 내에서만 구현해야합니다. 그것들은 다른 곳이 아니라 그 드라이버 안에 위치해야합니다.
  • ISR과의 모든 통신은 드라이버와 드라이버 만 수행합니다. 프로그램의 다른 부분이 해당 정보에 액세스해야하는 경우 setter / getter 기능 등을 통해 드라이버에 요청해야합니다.
  • "전역"변수를 선언해서는 안됩니다. 외부 연결이있는 전역 의미 파일 범위 변수. 즉, extern키워드로 또는 실수 로 호출 할 수있는 변수입니다 .
  • 대신, 드라이버 내부에서 개인 캡슐화를 강제하기 위해 드라이버와 ISR간에 공유되는 모든 변수가 선언되어야합니다 static. 이러한 변수는 전역 변수가 아니라 선언 된 파일로 제한됩니다.
  • 컴파일러 최적화 문제를 방지하려면 이러한 변수를로 선언해야합니다 volatile. 참고 : 이것은 원자 액세스를 제공하거나 재진입을 해결하지 않습니다!
  • ISR이 변수에 쓰는 경우 드라이버에 일부 재진입 메커니즘이 종종 필요합니다. 예 : 인터럽트 비활성화, 글로벌 인터럽트 마스크, 세마포어 / 뮤텍스 또는 보장 된 원자 읽기.

참고 : 다른 파일에있는 벡터 테이블에 배치하기 위해 헤더를 통해 ISR 함수 프로토 타입을 노출해야 할 수도 있습니다. 그러나 인터럽트이고 프로그램에 의해 호출되어서는 안된다고 문서화하는 한 문제가되지 않습니다.
Lundin

카운터 인수가 setter / getting 함수 사용으로 인한 오버 헤드 (및 추가 코드)가 증가했다면 어떻게할까요? 8 비트 임베디드 장치의 코드 표준에 대해 생각 하면서이 문제를 직접 해결했습니다.
Leroy105

2
@ Leroy105 C 언어는 지금까지 영원히 인라인 함수를 지원했습니다. 심지어 사용하지만의는 inline컴파일러가 똑똑하고 스마트 한 코드를 최적화에서 얻을 수 있기 때문에 쓸모가되고있다. 오버 헤드에 대한 걱정은 "미숙 한 최적화"라고 말하고 싶습니다. 대부분의 경우 오버 헤드가 머신 코드에 존재하더라도 오버 헤드는 중요하지 않습니다.
Lundin

2
즉, ISR 드라이버를 작성하는 경우 모든 프로그래머의 80-90 % (여기 과장하지 않음)는 항상 무언가 잘못되었다고합니다. 결과는 미묘한 버그입니다. 잘못 지워진 플래그, 휘발성 누락, 경쟁 조건, 실시간 성능 저하, 스택 오버플로 등으로 인한 잘못된 컴파일러 최적화. ISR이 드라이버 내부에 제대로 캡슐화되지 않은 경우 이러한 미묘한 버그의 가능성 더 증가했다. setter / getter가 약간의 오버 헤드를 유발하는 경우와 같이 주변 관심사에 대해 걱정하기 전에 버그가없는 드라이버 작성에 중점을 둡니다.
Lundin

10
이 전역 변수 사용은 곡물에 반합니다.

이것이 진짜 문제입니다. 극복 해

이제 무릎을 꿇는 사람들이 이것이 얼마나 부정한지 즉시 알기 전에, 그 점을 조금만 살펴 보겠습니다. 전역 변수를 사용하면 초과 할 위험이 있습니다. 그러나 효율성을 높일 수도 있는데, 이는 때때로 소규모 자원 제한 시스템에서 중요합니다.

열쇠는 당신이 그것들을 합리적으로 사용할 수있을 때를 생각하는 것입니다. 버그가 발생하기를 기다리는 대신에 문제를 일으키지 않을 것입니다. 항상 상충 관계가 있습니다. 하지만 일반적으로 인터럽트 및 전경 코드 사이의 통신을위한 전역 변수를 피하는 종교의 극단으로, 대부분의 다른 지침과 같이 복용하는 undertandable 지침 것은 비생산적이다.

때때로 전역 변수를 사용하여 인터럽트와 포 그라운드 코드 사이에 정보를 전달하는 몇 가지 예는 다음과 같습니다.

  1. 시스템 클럭 인터럽트에 의해 관리되는 클럭 틱 카운터. 나는 보통 1ms마다 실행되는주기적인 클록 인터럽트가 있습니다. 그것은 종종 시스템의 다양한 타이밍에 유용합니다. 이 정보를 인터럽트 루틴에서 가져 와서 나머지 시스템에서 사용할 수있는 한 가지 방법은 전역 클럭 틱 카운터를 유지하는 것입니다. 인터럽트 루틴은 매 클럭 틱마다 카운터를 증가시킵니다. 포 그라운드 코드는 언제든지 카운터를 읽을 수 있습니다. 종종 나는 10ms, 100ms, 심지어 1 초 틱 동안 이것을한다.

    1ms, 10ms 및 100ms 틱이 단일 원자 연산으로 읽을 수있는 단어 크기인지 확인하십시오. 고급 언어를 사용하는 경우 이러한 변수가 비동기 적으로 변경 될 수 있음을 컴파일러에 알리십시오. 예를 들어 C에서는 extern volatile 을 선언합니다 . 물론 이것은 통조림 포함 파일에 들어가는 것이므로 모든 프로젝트에서이를 기억할 필요는 없습니다.

    때로는 1 초 눈금 카운터를 총 경과 시간 카운터로 만들므로 32 비트 너비로 만듭니다. 그것은 내가 사용하는 많은 작은 마이크로에서 단일 원자 작업으로 읽을 수 없으므로 전역 적이지는 않습니다. 대신, 다중 단어 값을 읽고, 읽기간에 가능한 업데이트를 처리하고 결과를 리턴하는 루틴이 제공됩니다.

    물론 더 작은 1ms, 10ms 등의 틱 카운터를 얻는 루틴 이있을 있습니다. 그러나 그것은 실제로 당신을 위해 거의 아무것도하지 않으며, 한 단어를 읽는 대신 많은 지시를 추가하고 다른 호출 스택 위치를 사용합니다.

    단점은 무엇입니까? 누군가가 실수로 카운터 중 하나에 쓰는 오타를 만들고 시스템의 다른 타이밍을 망칠 수 있다고 생각합니다. 의도적으로 카운터에 쓰는 것은 의미가 없으므로 이런 종류의 버그는 오타와 같은 의도적이지 않은 것이어야합니다. 거의 없을 것 같습니다. 나는 그 기억하지 않습니다 지금까지 100 개 이상의 작은 마이크로 컨트롤러 프로젝트에서 일어나는.

  2. 최종 필터링 및 조정 된 A / D 값. 가장 일반적인 일은 인터럽트 루틴이 A / D로부터의 판독 값을 처리하도록하는 것입니다. 필자는 보통 아날로그 값을 필요한 것보다 빠르게 읽은 다음 약간의 저역 통과 필터링을 적용합니다. 적용되는 스케일링 및 오프셋도 종종 있습니다.

    예를 들어, A / D는 전압 분배기의 0 ~ 3V 출력을 판독하여 24V 공급을 측정 할 수 있습니다. 많은 측정 값은 일부 필터링을 통해 실행 된 다음 최종 값이 밀리 볼트가되도록 조정됩니다. 공급 장치가 24.015V 인 경우 최종 값은 24015입니다.

    시스템의 나머지 부분에는 공급 전압을 나타내는 실시간 업데이트 값이 표시됩니다. 저역 통과 필터 정착 시간보다 훨씬 자주 업데이트되기 때문에 정확히 업데이트되는 시점을 알거나 신경 쓸 필요가 없습니다.

    다시 말하지만 인터페이스 루틴을 사용할 수는 있지만 그 이점은 거의 없습니다. 전원 공급 장치 전압이 필요할 때마다 전역 변수를 사용하는 것이 훨씬 간단합니다. 단순성은 기계만을위한 것이 아니라 단순할수록 사람의 실수 가능성이 적다는 것을 기억하십시오.


나는 천천히 일주일 동안 치료를 받고 있었고, 실제로 내 코드를 nitpick하려고했습니다. 변수 액세스 제한에 대한 Lundin의 요점을 보았지만 실제 시스템을 살펴보면 시스템 담당자가 실제로 시스템 중요 글로벌 변수를 훼손 할 가능성이 있다고 생각합니다. Getter / Setter 함수는 전역을 사용하는 것보다 오버 헤드 비용이 많이
들며

3
@ Leroy105 문제는 "테러리스트"가 의도적으로 전역 변수를 남용하는 것이 아닙니다. 더 큰 프로젝트에서는 네임 스페이스 오염이 문제가 될 수 있지만 이름을 잘 지정하면 해결할 수 있습니다. 아니요, 진정한 문제는 프로그래머가 전역 변수를 의도 한대로 사용하려고하지만 올바르게 수행하지 못하는 것입니다. 모든 ISR에 존재하는 경쟁 조건 문제를 인식하지 못하거나 필수 보호 메커니즘의 구현을 엉망으로 만들거나 단순히 코드 전체에서 전역 변수 사용을 분출하여 긴밀한 결합을 만들고 읽을 수없는 코드.
Lundin

귀하의 포인트를 올린 유효하지만, 심지어 이들 예에서, 교체 extern int ticks10ms로하는 inline int getTicks10ms()반면에 실수로 프로그램의 다른 부분에서 값을 변경하기 어려운 것 동안, 어셈블리 컴파일에 전혀 차이가 없으며, 또한 당신을 수 이 호출에 "후크"하는 방법 (예 : 단위 테스트 중 시간을 조롱하거나이 변수에 대한 액세스를 기록하는 등) San 프로그래머가이 변수를 0으로 변경할 가능성이 있다고해도 인라인 게터의 비용은 없습니다.
Groo

@Groo : 인라인 함수를 지원하는 언어를 사용하는 경우에만 해당되며 게터 함수의 정의를 모두에게 표시해야합니다. 실제로 고급 언어를 사용할 때 getter 함수를 사용하고 전역 변수를 줄입니다. 어셈블리에서는 getter 함수를 사용하는 것보다 전역 변수의 값을 얻는 것이 훨씬 쉽습니다.
Olin Lathrop

물론 인라인 할 수 없다면 선택이 그렇게 간단하지 않습니다. 인라인 함수 (및 많은 사전 C99 컴파일러는 이미 인라인 확장을 지원)를 사용하면 getter에 대한 성능이 인수가 될 수 없다고 말하고 싶습니다. 합리적인 최적화 컴파일러를 사용하면 동일한 생산 어셈블리로 끝나야합니다.
Groo

2

특정 인터럽트는 전역 리소스가됩니다. 그러나 때때로 여러 인터럽트가 동일한 코드를 공유하도록하는 것이 유용 할 수 있습니다. 예를 들어, 시스템에는 여러 개의 UART가있을 수 있으며 모두 유사한 송신 / 수신 로직을 사용해야합니다.

처리하는 좋은 접근 방식은 인터럽트 핸들러가 사용하는 것을 포인터 또는 포인터로 구조 객체에 배치 한 다음 실제 하드웨어 인터럽트 핸들러를 다음과 같이 만드는 것입니다.

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

객체는 uart1_info, uart2_info, 등 글로벌 변수가 될 것입니다,하지만 그들은 것입니다 인터럽트 핸들러에 의해 사용되는 전역 변수. 핸들러가 다룰 다른 모든 것은 그 안에서 처리됩니다.

인터럽트 핸들러와 메인 라인 코드에 의해 액세스되는 것은 반드시 규정되어야합니다 volatile. volatile인터럽트 핸들러가 사용할 모든 것을 선언하는 것이 가장 간단 하지만 성능이 중요한 경우 임시 값으로 정보를 복사하고 작동 한 다음 다시 쓰는 코드를 작성하는 것이 좋습니다. 예를 들어, 쓰는 대신 :

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

쓰다:

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

전자의 접근 방식은 읽고 이해하기 쉽지만 후자의 접근 방식보다 효율성이 떨어집니다. 이것이 문제인지 여부는 응용 프로그램에 따라 다릅니다.


0

세 가지 아이디어가 있습니다.

범위를 단일 파일로 제한하려면 플래그 변수를 정적으로 선언하십시오.

플래그 변수를 private로 설정하고 getter 및 setter 함수를 사용하여 플래그 값에 액세스하십시오.

플래그 변수 대신 세마포어와 같은 신호 객체를 사용하십시오. ISR은 세마포어를 설정 / 게시합니다.


0

인터럽트 (즉, 핸들러를 가리키는 벡터)는 전역 리소스입니다. 따라서 스택이나 힙에서 일부 변수를 사용하더라도

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

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

또는 '가상'기능이있는 객체 지향 코드 :

HandlerObject *obj;

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

… 첫 번째 단계는 다른 데이터에 도달하기 위해 실제 전역 (또는 최소한 정적) 변수를 포함해야합니다.

이러한 모든 메커니즘은 간접적 인 기능을 추가하므로 일반적으로 인터럽트 처리기에서 마지막 사이클을 짜 내려는 경우에는 수행되지 않습니다.


volatile int *로 플래그를 선언해야합니다.
next-hack

0

나는 현재 Cortex M0 / M4를 코딩하고 있으며 C ++에서 사용하는 접근법 (C ++ 태그가 없으므로이 답변은 주제에 맞지 않을 수 있음)은 다음과 같습니다.

CInterruptVectorTable컨트롤러의 실제 인터럽트 벡터에 저장된 모든 인터럽트 서비스 루틴을 포함 하는 클래스 를 사용합니다 .

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

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

이 클래스 CInterruptVectorTable는 인터럽트 벡터의 추상화를 구현하므로 런타임 중에 다른 함수를 인터럽트 벡터에 바인딩 할 수 있습니다.

해당 클래스의 인터페이스는 다음과 같습니다.

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;
};

벡터 테이블이 객체가 아니기 static때문에 컨트롤러가- this포인터를 제공 할 수 없기 때문에 벡터 테이블에 저장된 기능을 만들어야 합니다. 따라서이 문제를 해결하기 위해 pThis내부에 정적 포인터가 있습니다 CInterruptVectorTable. 정적 인터럽트 함수 중 하나를 입력하면 pThis-pointer에 액세스하여의 한 객체의 멤버에 액세스 할 수 있습니다 CInterruptVectorTable.


이제 프로그램에서를 사용하여 인터럽트가 발생할 때 호출 SetIsrCallbackfunction되는 static함수에 대한 함수 포인터를 제공 할 수 있습니다 . 포인터는에 저장됩니다 InterruptVectorTable_t virtualVectorTable.

그리고 인터럽트 함수의 구현은 다음과 같습니다 :

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

따라서 static다른 클래스 의 메소드를 호출 할 수 있습니다.이 클래스 private는 다른 static this포인터를 포함 하여 해당 객체의 멤버 변수에 액세스 할 수 있습니다 (하나만).

IInterruptHandler객체에 대한 포인터를 만들고 인터페이스 하고 저장할 수 있다고 생각 하므로 static this모든 클래스에서 포인터가 필요하지 않습니다 . (아마도 아키텍처의 다음 반복에서 시도해 볼 수 있습니다)

인터럽트 처리기를 구현할 수있는 유일한 객체는 하드웨어 추상화 계층 내부의 객체이기 때문에 다른 접근법은 우리에게 잘 작동합니다. 우리는 일반적으로 각 하드웨어 블록에 대해 하나의 객체 만 가지고 있으므로- static this포인터로 작업하는 것이 좋습니다. 하드웨어 추상화 계층은 인터럽트에 대한 또 다른 추상화를 제공하며,이 추상화 ICallback는 하드웨어 위의 장치 계층에서 구현됩니다.


글로벌 데이터에 액세스하십니까? 물론, 필요한 전역 데이터의 대부분을- this포인터 및 인터럽트 기능과 같은 개인용으로 만들 수 있습니다 .

방탄이 아니며 오버 헤드가 추가됩니다. 이 접근법을 사용하여 IO-Link 스택을 구현하는 데 어려움을 겪을 것입니다. 그러나 타이밍이 너무 빡빡하지 않으면 어디에서나 액세스 할 수있는 전역 변수를 사용하지 않고 모듈에서 유연한 인터럽트 및 통신 추상화를 얻을 수 있습니다.


1
"따라서 런타임 중에 다른 함수를 인터럽트 벡터에 바인딩 할 수 있습니다"이것은 나쁜 생각처럼 들립니다. 프로그램의 "순환 복잡성"은 단지 지붕을 통과 할 것입니다. 타이밍과 스택 사용 충돌이 발생하지 않도록 모든 사용 사례 조합을 테스트해야합니다. 유용성이 매우 제한적인 기능에 대한 많은 수고가 있습니다. (부트 로더 케이스가 없다면 다른 이야기입니다) 전반적으로 메타 프로그래밍 냄새가납니다.
Lundin

@Lundin 나는 당신의 요점을 실제로 보지 못합니다. 예를 들어 DMA가 SPI에 사용중인 경우 DMA 인터럽트를 SPI 인터럽트 핸들러에 바인딩하고 UART에 사용중인 경우 UART 인터럽트 핸들러에 바인딩합니다. 두 핸들러 모두 테스트해야하지만 문제는 아닙니다. 그리고 메타 프로그래밍과는 아무런 관련이 없습니다.
아스날

DMA는 한 가지이며, 인터럽트 벡터의 런타임 할당은 완전히 다른 것입니다. DMA 드라이버 설정을 런타임에 가변적으로 설정하는 것이 좋습니다. 벡터 테이블은 그리 많지 않습니다.
Lundin

@Lundin 나는 우리가 그것에 대해 다른 견해를 가지고 있다고 생각합니다. 우리는 그것에 대해 여전히 당신의 문제를 보지 못하기 때문에 그것에 대해 대화를 시작할 수 있습니다.
아스날
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.