Category (Click)
개발보드 덕질하기

[Zephyr] WorkQueue 정리

 차완기 - @2/7/2024, 12:37:00 AM
“My First nRF52840 Project”
nRF Connect SDK에 입문하며 공부 겸 진행해보는 간단한 프로젝트입니다. AM2320 온습도센서를 붙여 Home Assistant에 Z2M으로 연결하는것이 목표입니다.
잘못된 내용이 있을 확률이 매우 높으니 주의해주시고, 부디 둥근 지적 부탁드립니다
#FW #nRF Connect SDK #HomeAssistant #Z2M #ZigBee

센서를 사용하다 보면 비동기적으로 데이터를 처리할 일이 많습니다.
대부분 DMA와 인터럽트를 이용해 CPU라는 귀중한 자원을 아끼기 위해서이죠. 많은 경우에서 아래와 같은 과정으로 CPU를 아끼곤 합니다.
1.
DMA 컨트롤러가 Peripheral에서 데이터를 뜯어와 RAM에 꽂아줌
2.
CPU 반환 후 데이터 읽기 완료 대기
3.
데이터 읽기 완료 인터럽트 작동 시 CPU 재점유
4.
어딘가에서 데이터 처리
여태껏 이런 상황에서 데이터 처리를 위한 “어딘가”로 전용 Task를 만들어 무한 대기에 빠뜨려 놓고, 보낼 데이터의 량에 따라 Queue, TaskEvent를 사용하고는 했습니다.
그럴싸한 방법이지만, 센서마다 Task를 만들자니 메모리가 아깝고, 처리용 Task를 공유하자니 자료구조의 정리가 매번 골치거리였는데요,
Zephyr에서는 WorkQueue를 활용해 이를 쉽게 해결할 수 있었습니다.

WorkQueue

DevAcademy - Zephyr RTOS basics 중 “Workflow of a workqueue“ 이미지 [링크]
“너를 노동시키겠다!” 라는 느낌을 풍기는 WorkQueue는 높은 우선순위(priority)를 가지는 ISR 또는 Thread에서 낮은 우선순위를 가지는 Thread로 후속 작업을 넘겨주기 위해 사용합니다.
여기서 WorkQueue는 “System WorkQueue”라는 이름으로 커널에서 기본 제공하며, 만약 필요하다면 직접 만들수도 있습니다.
예를 들어 어떤 센서에서 DMA와 인터럽트로 데이터를 가져오고, 이걸 가공해 UART로 던져줘야 한다면, 1) ISR에서 WorkQueue로 후속 작업이 있음을 꽂아주고 CPU 반환한 후 2)이에 대응하는 낮은 우선순위 함수가 낮은 우선순위로 작동하며 후속 작업과 UART 전송을 수행하는 것이죠.
여기서 후속 작업을 실행하는 함수인 WorkQueue Handler 함수는 WorkQueue가 생성될 때 정해지는 우선순위와 Stack 크기를 가지며, 여러 WorkQueue Handler 함수가 하나의 WorkQueue에 연결되어 사용할 수 있습니다.
즉, 하나의 WorkQueue를 통해 아래와 같이 재활용할 수 있는 거죠.
foo라는 센서의 ISR에서 foo_work라는 WorkQueue Handler 함수에 WorkQueue를 이용해 후속 작업 전달
boo라는 센서의 ISR에서 boo_work라는 WorkQueue Handler 함수에 WorkQueue를 이용해 후속 작업 전달
...
WorkQueue에는 데이터를 집어넣을 수는 없지만, WorkQueue와 WorkQueue Handler를 연결해주는 work 구조체 포인터(struct k_work *)는 넘길 수 있어, 이를 구조체로 감싸주면 CONTAINER_OF을 활용해 데이터를 간접적으로 전달할 수 있습니다.

예시

이해를 위해 만들어보았던 코드를 정리해 보았습니다.

test Thread 생성

예시에서는 ISR으로 설명했지만, 여기서는 WorkQueue보다 우선선위가 높은 Thread를 사용하기로 했습니다.
test라는 이름의 Thread에서는 k_work_queue_start() 함수를 통해 s_work_queue WorkQueue를 시작한 후 k_work_submit_to_queue() 함수를 통해 1초 간격으로 foo, boo 두 후속 작업을 WorkQueue에 집어넣는 동작을 합니다.
신기하게도 Zephyr에서는 K_THREAD_DEFINE() 매크로를 중괄호 밖에서 사용해 Thread를 생성할 수 있었습니다. 이후에 시간이 날 때 조금 더 알아보도록 하고..일단 넘어갔습니다.

WorkQueue 생성

앞서 WorkQueue Handler 함수는 고유한 우선순위와 Stack을 가지고 있다고 언급하였는데요, WorkQueue를 생성하는 과정에서 이를 설정해주어야 했습니다.
WorkQueue는 s_work_queue라는 이름으로 만들어주었습니다.
만약 구조체의 내용이 0으로 초기화되지 않으면 k_work_queue_start()를 실행하기 전, k_work_queue_init()을 호출해 내용을 비워줘야 합니다.
마지막으로 WorkQueue에 사용할 Stack을 K_THREAD_STACK_DEFINE()매크로를 이용해 만들어줍니다.

WorkQueue Work Handler 구조체 생성

테스트를 위해 foo, boo 두 이름의 후속 작업을 만들어보았습니다.
K_WORK_DEFINE()매크로를 통해 각각의 work 구조체를 생성하였습니다.
이후 ISR에서 후속 작업이 필요할 때, 앞에서 생성한 WorkQueue와 여기서 만든 구조체를 이용해 WorkQueue에 특정 Handler 함수를 실행하도록 Push하는 거죠.

실행

우선순위가 높은 test Thread가 모두 끝난 후 WorkQueue에 push된 순서대로 handler 함수가 작동하는것을 확인할 수 있습니다.
전체 소스코드는 아래에서 확인할 수 있습니다.

마무리

Zephyr Thread 튜토리얼을 보며 처음 보는 신기한 개념이 있어 한번 정리해 보았습니다.
다음 포스팅에서는 WorkQueue를 센서 드라이버 모듈에 추가해, 센서로부터 데이터를 보다 효율적으로 얻을 수 있도록 해보겠습니다.