본문 바로가기
IT/임베디드 시스템

임베디드 시스템 설계 기본이론1 - 일반 동작과 짧은 루틴

by 뽀짝뉴스 2020. 6. 11.

 

다양한 분야의 임베디드 시스템 설계에 적용되는 내용들에 대해서 얘기해보겠습니다.

 

 

[보통의 작동]

 

일반적으로 임베디드 시스템은 어떤 시간이 되거나 응답이 필요한 외부 이벤트가 발생할 때까지 아무 일도 하지 않고 기다리는 경우가 많습니다. 인쇄할 정보가 오지 않으면, 레이저 프린터는 매분 깨어나서 프린터 드럼을 조금 움직이는 동작 이외에는 아무것도 하지 않습니다. 사용자가 방아쇠를 당기지 않거나 키보드의 버튼을 누르지 않는다면, 무선 바코드 스캐너는 심지어 마이크로프로세서를 꺼버리기도 하지요.

 

외부 이벤트는 일반적으로 인터럽트를 발생시키고, 하드웨어 타이머를 설정해서 시간이 지날 때마다 인터럽트를 발생시키도록 할 수 있기 때문에, 인터럽트는 임베디드 소프트웨어에 있어서 구동력이라고 여겨집니다. 가장 일반적인 임베디듯 시스템 설계기술은 각각의 RTOS 태스크들이 대부분의 시간을 인터럽트 루틴을 기다리거나 다른 태스크에 메시지를 보내거나 이벤트를 발생시키거나 세마포어를 해제해서 기다리는 태스크에 알려주면서 대기하게 하는 것입니다. 인터럽트가 발생했을 때 인터럽트 루틴은 하나 또는 여러 개의 태스크의 시그널을 보내기 위해서 RTOS 서비스를 사용하고, 그 각각의 태스크들은 자신의 주어진 일을 수행하고 또한 다른 태스크에 시그널을 보내겠죠. 이런 방법으로 각각의 인터럽트는 시그널과 태스크 작동의 계층적인 구조를 만들 수 있게됩니다.

 

텔레그래프 시스템의 내부에서 어떤 일이 발생하는지에 대한 간단한 버전의 예도 있습니다. 곡선의 화살표가 RTOS를 통해 전달되는 메시지를 나타내죠.

 

시스템이 네트워크 패킷을 받으면 하드웨어는 인터럽트를 발생시킵니다. 인터럽트 루틴은 하드웨어를 리셋하고 받은 패킷 내용을 담은 메시지를 DDP 프로토콜 태스크에 보냅니다. DDP 프로토콜 태스크는 메시지를 기다리면서 대기하고 있습니다. 그러다가 메시지가 도착했을 경우에 깨어나고 패킷이 텔레그래프 용인지 아니면 다른 네트워크 장치용인데 실수로 텔레그래프가 받았느닞를 결정합니다. 패킷이 텔레그래프 용이면 DDP 프로토콜 태스크는 받은 패킷을 포함하는 메시지를 ADSP 프로토콜 태스크에 보냅니다. 이 메시지는 ADSP 프로토콜 태스크를 깨우고, 패킷의 내용을 결정합니다. 가령 패킷이 인쇄할 데이터를 담고 있다면, ADSP 프로토콜 태스크는 데이터를 담고 있는 메시지를 시리얼 포트 태크스에 보내서 시리얼 포트 하드웨어에 데이터를 전송해서 프린터에 보내 인쇄할 수 있도록 하게 됩니다. 패킷 데이터가 프린터 상태에 대한 요구를 담고 있을 때는 ADSP 프로토콜 태스크는 응답에 대한 패킷을 만들어서 DDP 프로토콜 태스크에 보내서 네트워크에 전송할 수 있도록 합니다.

 

같은 방법으로 시스템이 프린터로부터 시리얼 데이터를 받으면 인터럽트 루틴은 하드웨어를 리셋하고 메시지에 있는 데이터를 시리얼 포트 태스크에 보냅니다. 데이터가 프린터 상태 정보를 담고 있다면 시리얼 포트 태스크는 상태를 ADSP 프로토콜 태스크에 보냅니다. 그러면 상태를 저장하고 네트워크로부터 다음 상태 요구에 대한 응답을 할 때 이용하겠지요.

 

매번 시스템이 네트워크 패킷이나 시리얼 포트 데이터를 받을 때 인터럽트 루틴은 메시지를 태스크들에 보내고 궁극적으로 받은 데이터에 적합한 응답을 하도록 하는 이벤트 사슬을 가동시킬겁니다. 어떤 패킷이나 데이터도 도착하지 않을 때는 인터럽트는 발생하지 않고 시스템의 세 태스크들은 받게 될 메시지를 기다리면서 아이들 상태를 유지합니다.

 

 

[짧은 인터럽트 루틴의 필요성]

 

긴 것보다는 짧은 인터럽트 루틴을 작성하는 것이 훨씬 좋다는 것이 상식입니다. 두 가지 이유를 들 수 있는데, 하나는 낮은 우선 순위의 인터럽트 루틴이 가장 높은 순위의 태스크에 우선하여 실행되고 있을 때 긴 루틴을 작성하면 직접적으로 응답을 느리게 만들기 때문입니다. 나머지 하나는 인터럽트 루틴은 태스크 코드보다 더 버그가 발생하기 쉬운 편이고 디버깅하기 매우 어렵다는 것 입니다.

 

대부분의 이벤트들은 소프트웨어로부터 다양한 응답을 요구합니다. 시스템은 하드웨어 포트를 리셋해야 하고, 받은 데이터를 저장해야 하고, 인터럽트 컨트롤러를 리셋해야 하고, 받은 데이터를 분석해야 하고, 응답을 만드는 등의 작업을 해야합니다. 그러나 이런 응답들의 제한시간은 서로 다릅니다. 하드웨어 포트와 인터럽트 컨트롤러를 리셋하고 데이터를 저장하는 것은 즉시 해야 할 필요가 있긴한데 , 데이터를 분석하고 응답하는 것은 그렇게까지 급한일은 아닙니다. 인터럽트 루틴이 즉각적인 동작을 수행하고 나서 시그널을 태스크에 보내서 다른 일들을 처리하게 하는 것이 합리적인 방법이라는게 정설이죠.

 

아래와 같은 특성을 가진 시스템을 위한 소프트웨어를 작성한다고 생각해볼까요?

 

- 시스템은 시리얼 포트로부터 온 명령어에 응답을 해야만 함.

 

- 명령어는 항상 캐리지 리턴으로 끝나야 함.

 

- 명령어들은 한번에 하나씩 도착함. 시스템이 현재의 명령어에 응답하기 전까지 다음 명령어는 도착하지 않음.

 

- 시리얼 포트 하드웨어는 한번에 한 문자를 저장할 수 있고, 문자들은 매우 빠르게 도착될 것임.

 

- 시스템은 상대적으로 천천히 명령어에 응답할 수 있음. 분명히 '매우'나 '상대적으로'는 애매한 말이지만 실제 시스템 사양에서는 얼마나 빨리 문자가 도착하는지 시스템이 명령어에 응답하기 위해 얼만큼의 시간이 필요한지 명시 되어있다는 점 참고.

 

졸속으로 이런 시스템을 작성하는 방법은 모든 작업 처리를 문자로 받는 인터럽트 루틴에서 하게 만드는 것입니다. 그렇게만 되면 인터럽트 루틴은 길고 복잡해서 디버깅 하기가 어려워지고, 태스크 코드에 있는 다른 모든 동작에 대해서 응답이 늦어지게 됩니다.

 

극단적으로 반대 경우도 말해보자면, 모든 문자들을 받자마자 RTOS 메시지로 태스크 코드에 전달하기만 하는 단순한 인터럽트 루틴을 작성하는 일도 가능합니다. 인터럽트 루틴이 짧아지기 때문에 이론상으로 이런 방법은 매우매우 좋은 구조라고 볼 수 있습니다. 실제로 어떤 시스템에선 정말 괜찮은 구조가 될 수도 있고요. 그러나, 늘 정답은 아니죠. 실제적인 단점으로 인터럽트 루틴은 각각의 받은 문자들에 대해서 너무 나많은 메시지를 명령어 해석 태스크에 메시지 큐를 이요해서 보낸다는 것과 큐에 메시지를 보내는 것은 순간적인 작업이 아니라는 것입니다. 만약 문자들이 너무 빠르게 도착한 경우에는 인터럽트 루틴이 따라갈 수 없게 됩니다. 메시지를 RTOS 큐에 올려놨을 때, 알토스는 어떤 태스크가 그 메시지를 기다리는지를 점검하고 어떤 태스크가 기다리고 있다면 스케줄러를 호출할 것이라는 것을 기억해야합니다.

 

가능한 타협 중의 하나는 받은 각각의 문자를 버퍼에 저장하고 각각의 명령어를 끝내는 리턴 키를 감시하도록 인터럽트 루틴을 작성하는 겁니다. 리턴 값이 도착했을 때, 인터럽트 루틴은 단 하나의 메시지를 명령어 해석 태스크에 보내고, 그 태스크는 버퍼로부터 문자들을 읽습니다. 이런 타협에서 인터럽트 루틴은 여전히 상대적으로 간단하고, 시스템은 많은 메시지를 보낼 필요가 없어집니다.

 

마지막 방법을 구현한 코드를 떠올려볼게요. 인터럽트 루틴 vGetCommamdCharacter은 들어오는 문자들을 버퍼 a_chCommandBuffer에 넣고 각각의 문자들이 리턴 값이 아닌지를 점검합니다. 리턴 값을 발견하게 될 경우, mboxCommand 메일박스로 메시지를 보냅니다. 명령어 해석을 해석하는 태스크인 vInterpretCommandTask느 메일박스를 기다립니다. 메시지가 도착했을 때, a_chCommandBuffer 버퍼로부터 현재의 명령어 문자들을 읽고요.

 

sc_post와 sc_pend는 VRTX 시스템의 함수들인데 이 경우의 알토스에서는 메일박스가 한번에 하나의 메시지만 가질 수 있습니다.

 

댓글