앞서 살펴본 임베디드 시스템 설계 예제에 이어서 추가적으로 알아야 할 내용들을 두 번에 걸쳐서 결론까지 이어가 보도록 하겠습니다.
[RTOS 사용을 결정하려면]
먼저 RTOS 구조가 적합한지를 결정해야 합니다. 이 시스템에서 가장 덩치가 큰 부분은 탱크에서 가솔린의 양을 계산하는데 4~5초가 걸리는 부분인데 분명, 먼저 말했던 다른 제한 시간을 엄수하기 위해서 다른 작업에 필요할 때는 이 계산을 잠시 멈춰야 합니다.
RTOS를 쓰지 않는다면, 제시한 시간보다 금방 걸리는 것들은 모두 인터럽트 루틴에서 실행되어야 합니다. 사용자가 버튼을 눌렀을 때, 시스템은 인터럽트 루틴에서 응답을 해야만 합니다. 사용자가 보고서를 인쇄하고 싶어 하면, 사용자는 태스크 코드에서 5초를 기다리거나 또는 인터럽트 루틴에서 보고서를 만들어야 합니다. 시스템은 넘치는 것을 감지하는 작업을 인터럽트 루틴에서 처리해야만 하고요. 인터럽트 루틴에서 이런 모든 것을 처리하는 시스템을 만들 수 있을까요? 아마도 그럴 수 있겠죠. 그러면 인터럽트 루틴에서 이런 모든 것을 처리하는 시스템을 만드는 것이 쉬울까요? 답은 아니다에 가깝습니다. 이럴 때는 RTOS를 사용하는 것이 더 나은 해결책이라고 판단됩니다.
선택한 마이크로컨트롤러가 RTOS를 지원하지 못하는 경우(8비트 마이크로컨트롤러의 일부는 지원하지 못합니다.) 비용조건을 만족시키면서도 RTOS를 지원하는 다른 마이크로컨트롤러를 찾아야 합니다.
[작업을 태스크로 분배하기]
이번 절에서는 시스템의 작업을 개개의 태스크에 분배할 겁니다.
먼저 부표의 레벨과 온도를 입력으로 받고, 각 탱크에 얼마나 많은 가솔린이 있는 지를 계산하고, 이전의 가솔린 양과 비교해서 탱크가 새는지를 감지할 수 있는 레벨 계산 태스크가 필요합니다.
이 작업은 각 탱크마다 4 또는 5초가 필요하고, 다른 작업들은 이것보다 훨씬 바르게 수행되기 때문에, 별도로 분리되고, 낮은 우선순위를 갖는 전통적인 RTOS 태스크가 되어야 합니다. 그다음, 각 탱크를 위해 별도의 분리된 태스크들을 가질 수도 있고 또는 한 태스크에서 차례로 각 탱크를 계산할 수도 있습니다. 그러나 탱크마다 하나의 태스크를 사용하는 것은 문제를 발생시키기 쉽습니다. 한 번에 한 태스크가 부표를 읽을 수 있고, 태스크들 사이에서 마이크로프로세서를 공유해야 하고, 각 태스크의 스택을 위해 메모리를 가져야 하기 때문입니다. 한 태스크로 모든 탱크를 계산하는 것은 단지 한 가지 단점이 있긴 한데, 어떤 탱크가 다음에 다루어져야 하는지를 결정하는 코드를 태스크 내에 가져야 한다는 겁니다. 얼마나 많은 태스크가 필요한지를 이미 다뤘었는데 그 이유를 따져보면, 하나의 태스크로 처리하는 것이 더 좋다는 것을 기억해낼 수 있겠죠.
또한, 레벨 계산 태스크와 별도의 넘침 감지 태스크가 필요합니다. 넘침 감지는 레벨 계산과 가솔린 유출 감지 태스크보다 높은 순위를 가져야 하기 때문에 반드시 별도의 태스크들로 되어있어야 합니다.
레벨 계산 태스크와 넘침 감지 태스크는 둘 다 부표 하드웨어를 읽어야 합니다. 그러므로 두 태스크가 하드웨어 때문에 싸우는 일이 없도록 해야 하죠. 가령 한 태스크가 하드웨어에게 세 번째 탱크의 부표를 읽고 싶다고 하고 다른 태스크가 5번째 탱크로부터 값을 읽겠다고 하면 적어도 한 태스크는 잘못된 데이터를 얻게 됩니다. 세마포어를 사용해서 한 번에 하나의 태스크만 부표 하드웨어에 접근할 수 있도록 하는 방법을 쓰면 됩니다. 아니면, 별도의 부표 하드웨어 태스크를 설정해서 다른 태스크의 요구는 큐에 메시지로 저장되게끔 할 수도 있고요. 세마포어는 상대적으로 더 효율적이고 코드를 작성하기가 더 쉽습니다. 하지만 태스크들은 다른 태스크가 부표의 레벨을 읽고 세마포어를 풀어주기까지 소요되는 수 밀리세컨드 동안 세마포어를 기다려야만 합니다. 세마포어와 별도의 태스크의 차이점은 그리 크지 않습니다. 태스크 구조를 추천한 적이 있었는데 오랜 시간 동안 세마포어를 기다리는 것은 태스크가 큐에 있는 다른 이벤트 응답을 못하기 때문에 좋은 생각이 아니죠. 그러나 태스크들이 부표 하드웨어를 위한 세마포어를 기다린다면 그동안 다른 할 수 있는 일이 아무것도 없기 때문에, 이번 경우는 앞의 규칙의 예외를 적용할 수 있습니다. 즉 기다려도 좋습니다. 여기에 대해서는 다시 다뤄볼게요.
버튼 처리 태스크도 필요합니다. 어떤 명령들은 몇 개의 버튼이 눌러져야 할 필요가 있는 경우도 있어서 사용자가 먼저 누른 버튼을 기억하는 스테이트 머신을 사용할 필요가 생깁니다. 이런 것을 인터럽트 루틴에서 처리할 수 도 있긴 한데, 인터럽트 루틴이 길고 복잡해질 가능성이 있다는 것을 항상 염두에 둬야 합니다.
우리는 이미 디스플레이에 표시할 메시지들을 가지고 있는 다양한 태스크를 만들었습니다. 유출을 감지했을 때의 레벨 계산 태스크, 넘침 감지, 버튼 처리 태스크 등이 그것이죠. 그렇기 때문에 한 태스크가 다른 태스크의 디스플레이 표시를 방해하지 않도록 하는 장치를 가져야 합니다. 부표 하드웨어를 공유하는 문제와는 다르게, 표시장치를 공유하는 문제는 세마포어로 쉽게 해결되지 않습니다. 만약 사용자가 유출이 감지된 직후에 버튼을 누르면 읽을 새도 없이 유출!이라는 메시지를 평범한 대기화면으로 바꿀 겁니다. 이건 바람직한 방향이 아니죠? 누가 봐도 그렇습니다. 유출 메시지는 대기 화면보다는 우선시되어야만 합니다. 공유 하드웨어를 통제하는 별도의 태스크는 이런 상황에서 유용합니다. 따라서 디스플레이 태스크 역시 필요합니다.
경보 벨은 또 다른 종류의 공유 하드웨어입니다. 레벨 계산과 넘침 감지 태스크는 경보 벨을 켤 수 있고, 버튼을 눌러 끌 수도 있죠. 이런 것을 위한 별도의 태스크가 필요할까요? 부표 하드웨어와는 다르게, 벨 하드웨어는 무언가 하는 도중에 라는 개념이 없습니다. 벨을 켜고 끄는 것은 아토믹하다고 표현하는데 이와 관련된 질문을 했던 걸 기억해봅시다. 만약 유출을 감지한 직후에 사용자가 버튼을 눌렀다면 시스템은 사용자가 벨을 끄고 싶어 한다고 생각할 수밖에 없습니다. 만약 사용자가 벨을 끈 직후에 또 다른 유출이나 넘침이 발생하면 시스템은 사용자에게 새로운 문제가 발생했음을 알리기 위해서 다시 벨을 켜야 합니다. 다양한 태스크가 벨을 놓고 싸우도록 만드는 것이 바로 시스템이 원하는 대로 동작하는 겁니다. 따라서 각 태스크에서 직접 벨을 켜고 끌 수 있도록 만드는 것이 합리적입니다. 별도의 경보 벨 태스크는 필요가 없습니다. 경보 벨 태스크를 만들지 않는 것이 누구나 어느 모듈에서든 벨을 직접 다룰 수 있는 코드를 작성할 수 있다는 것을 의미하지는 않습니다. 벨 하드웨어를 캡슐화시키는 vBellOn과 vBellOff 함수를 작성해서 별도로 모듈화 시켜야 합니다. 일반적으로 좋은 설계 기법은 이런 캡슐화를 이루는 것입니다. 물론 어떤 태스크든 두 함수를 호출할 수는 있어요.
논의할만한 마지막 기능은 보고서를 인쇄하는 겁니다. 각 줄을 인쇄한 후에 프린터는 인터럽트를 발생시키기 때문에 보고서의 계속되는 줄을 프린터에 보내는 인터럽트 루틴을 쓸 수도 있습니다. 하지만, 어딘가에서는 보고서를 형식에 맞춰서 만들어내는 코드가 있어야 하고 사용자가 여러 보고서를 보려고 요청할 수 있기 때문에, 그런 요청을 큐에 유지하면서 관리하는 코드가 필요합니다. 따라서 별도의 인쇄 양식 태스크를 만드는 것이 합리적인 것입니다. 먼저 보고서를 만드는데 0.1초 이상이 걸린다고 가정하면 이 태스크는 버튼 응답을 간섭하지 않기 위해서 버튼 처리 태스크보다 낮은 우선순위를 가져야 합니다. 두 번째로 출력 큐를 유지하는 것이 복잡하기 때문에 별도의 태스크로 처리하는 것이 더 쉽습니다.
'IT > 임베디드 시스템' 카테고리의 다른 글
세마포어와 큐의 캡슐화 & 하드리얼타임스케줄링 짚고 넘어가기 (0) | 2020.06.19 |
---|---|
임베디드 시스템 설계 예제3 - 시스템/공유데이터 처리 그리고 결론 (0) | 2020.06.18 |
임베디드 시스템 설계 예제1 - 설계의 절차와 타이밍 문제 해결 (0) | 2020.06.13 |
임베디드 시스템 설계 기본이론3 - 태스크 구조 추천 / 시분할 끌기 (0) | 2020.06.11 |
임베디드 시스템 설계 기본이론2 - 태스크의 수, 우선순위 그리고 캡슐화 (0) | 2020.06.11 |
댓글