2020년 12월 2일 수요일

ESP32로 알아보는 FreeRTOS

이번 시간에는 Wi-Fi & Bluetooth 기능을 저가로 공급하여 유명해진 Espressif 사의 ESP32 보드를 사용하여, RTOS OS 중에서 현재 가장 많은 사용자 층을 확보하고 있는 FreeRTOS를 소개해 보고자 한다.




목차
1. ESP32와 FreeRTOS
2. ESP-IDF SDK 소개
3. FreeRTOS Programming
4. 아직 못다한 이야기
5. References


IT에 입문한지 어느새 25년째다. AI, Big Data, Cloud, IoT, Robot, Drone, Autonomous Vehicle, Blockchain, 3D printing ... 예전에도 분명히 그랬을 텐데, 최근 몇년 동안의 IT 시장의 눈부신 변화가 유독 더 크게 다가오는 것은 것은 왜일까 ?  변화를 따라잡지 못하면 도태되고 말 것이라는 불안감이 엄습해 온다. 이럴 때는 어찌해야 할까 ? 답은 간단하다. 자신이 그동안 열심히 해 오던 분야를 더 깊이 파고 또 파는 것이다. 아무리 새로운 기술이 등장하고, 그 변화의 속도를 도져히 따라 잡을 수 없을 것 같아도, 이러한 기술들이 과연 어디에서 나왔겠는가 ? 내가 오늘 하는 작은 일을 결코 가볍게 여기지 말자~  😎

1. ESP32와 FreeRTOS
이번 장에서는 ESP32와 FreeRTOS에 관하여 간략히 살펴 보고자 한다.

a) ESP32 overview
ESP32는 ESP8266으로 유명한 Espressif 사에서 만든 Wi-Fi & Bluetooth용 저가의 chip(Wi-Fi/BLE SoC)이다. 그런데 저가의 chip이라고 해서 우습게(?) 볼 일이 아닌 것이, ESP32 자체는 Xtensa LX6(Dual-core 32bit) RISC CPU(160Mhz or 240Mhz)를 기반으로 하고 있는 엄연한 32bit microprocessor이다.

[그림 1.1] Espressif ESP32 SoC [출처 - https://www.espressif.com/]

[그림 1.2] ESP32 SoC 블럭도

[그림 1.3] ESP8266 vs ESP32 스펙 비교 [출처 - 참고문헌 7]

📌 이정도 spec이라면 RAM 크기만 좀 더 보강된다면 linux도 충분히 돌릴 수 있을 것 같다.

따라서 여기에서는 ESP32를 WiFi or Bluetooth용 device로 생각해서 Arduino의 주변 장치로 활용하기 보다는, ESP32를 CPU로 하여 단독으로 동작하는 방식을 활용하고자 한다. 아래 그림은 ESP32를 사용하여 만들어진 보드 중 하나인 ESP32 DeviKit이다. 실제로 (이와 유사한) ESP32를 기반으로 하는 다양한 업체의 다양한 종류의 보드가 저가로 판매되고 있다.

[그림 1.4] ESP32를 사용한 board(device kit) 예

ESP8266 및 ESP32는 Arduino or Raspberry Pi 만큼이나 워낙 유명하기 때문에 여기에서 부연 설명을 할 필요가 없어 보인다. 따라서 다음 장에서는 곧 바로 ESP-IDF 환경 설정으로 넘어가도록 하겠다.

b) FreeRTOS overview
FreeRTOS는 Richard Barry가 만들어 현재까지 가장 많은 사용자 층을 확보하고 있는 open source RTOS로, 아래 그림에서 보는 바와 같이 embedded linux에 이어 사용률 2위(2019년 기준)에 오를 정도로 그 기세가 대단하다(필자도 사실 이정도까지 FreeRTOS가 널리 사용되고 있는 줄은 몰랐다 😅).

[그림 1.5] 2019년 기준 향후 1년간 사용할 것으로 예상되는 Embedded OS 순위 [출처 - 참고문헌 4]

📌 이 자료는 미국(58%), EMEA(21%), APAC(21%)의 엔지니어들을 대상으로 조사한 결과이다. 조사 대상 중 EMEA(유럽 외)나 아시아권의 비중이 낮은 것이 좀 아쉽다.

사실 FreeRTOS 자체는 ZephyrmbedOS와는 달리 kernel core 자체만을 제공하고 있기 때문에, 나머지 부분(device driver, network stack, 주변 library 등)은 칩 제조사에서 자신들의 환경에 맞게 작업한 후, SDK 형태로 배포하는 방식을 따른다(예: Espressif ESP-IDF).

이런 상황에서 Amazon은 최근에 FreeRTOS를 인수하여 (kernel 이외의 기능을 보강하여) Amazon FreeRTOS 형태로 배포하고 있다. 왜 Cloud 업체인 Amazon에서 뜬금없이 FreeRTOS를 인수했을까 ? 답은 간단하다. 이는 IoT 시장의 폭발은 Cloud 시장의 그것과 직결되기 때문이다. 쉽게 말해 IoT 기기를 효과적으로 사용/관리하기 위해서는 Cloud(서버)와의 연결이 필수적인데, 현재 가장 많은 사용자 층을 확보한 FreeRTOS를 AWS와 잘 연동하도록 만들어(Amazon FreeRTOS를 사용할 수 밖에 없도록 만들어) 재 배포하는 것은 그만큼 AW$ cloud 시장을 확장하는 효과를 가져올 것이 자명하기 때문이다. 이는 M$에서 발빠르게 Azure cloud에 맞는 Azure RTOS를 밀고 있는 것과도 맥을 같이한다고 보아야 할 것이다. 근데, Google은 뭐하나 ? 😂

[그림 1.6] Amazon FreeRTOS [출처 - 참고문헌 5]

<앞으로 눈여겨 보아야 할 RTOS>
1. Amazon's AWS FreeRTOS  : 목적은 AWS Cloud 😓
2. Microsoft's Azure RTOS : 목적은 Azure Cloud  😓
3. ARM's mbedOS : 목적은 ARM Cortex-M series chips  😒
4. Linux foundation's Zephyr Project : Open source project 😃
5. Apache's NuttX : Open source project 😃

앞으로 누가 RTOS 시장의 패권을 거머쥘지는 예측하기 어려우나, 위의 조사 결과로 보아 당분간 FreeRTOS의 아성을 무너뜨리기는 쉽지 않아 보인다. 그래도 개인적으로는 Zephyr의 비상을 기대해 본다 ...



2. ESP-IDF SDK 소개
이번 장에서는 Espressif 사에서 만든 ESP-IDF 개발 환경(or SDK)을 소개해 보고자 한다. 

a) ESP-IDF 개요
ESP-IDF는 마치 Amazon FreeRTOS  처럼, FreeRTOS kernel과 주변 기능(device driver, network stack, security 기능, ...)을 하나로 통합하여 ESP series 보드에 맞게 최적화 시킨 RTOS(or SDK)이다.

[그림 2.1] Espressif ESP-IDF 아키텍쳐 [출처 - https://www.espressif.com/]

📌 Espressif에서는 FreeRTOS를 그대로 사용하지 않고 자신들의 CPU에 맞게 최적화(예: dual core CPU 상황을 고려하여 API 기능 확장)하여 배포하고 있다.

Espressif사는 ESP-IDF 외에도 아래와 같이 몇가지 흥미로운 framework & library 개발 환경을 제공하고 있다.

[그림 2.2] Espressif frameworks and libraries [출처 - https://www.espressif.com/en/products/sdks/esp-idf]

b) Target board
이번 posting에서 사용할 target board는 ESP32 기반 보드 중 주변에서 쉽게 구할 수 있는 NodeMCU-32S이다.

 
[그림 2.3] NodeMCU-32S
📌 (국내에서) 배송비 포함하여 11,800원에 구매하였다.

c) ESP-IDF 환경 설정하기
자, 그럼 이제 부터 본격적으로 ESP-IDF 환경 설정에 들어가 보도록 하자. 언제나 그렇듯 Ubuntu 환경(18.04 desktop)을 기준으로 하자. ESP-IDF는 당연히 Windows, macOS에서도 환경 설정이 가능하다. 하지만 필자는 언제나 그렇듯 Ubuntu에서 작업한다. 😎


<install전 사전 준비 작업>
$ sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 10
  => ESP-IDF에 포함된 python은 version 3를 기준으로 동작한다. 따라서 python3을 default로 변경하도록 하자.
update-alternatives: using /usr/bin/python3 to provide /usr/bin/python (python) in auto mode
$ python
Python 3.6.9 (default, Oct  8 2020, 12:12:24) 
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> exit
Use exit() or Ctrl-D (i.e. EOF) to exit
>>> exit()

$ sudo apt-get install git wget flex bison gperf python python-pip python-setuptools cmake ninja-build ccache libffi-dev libssl-dev
  => 추가로 필요한 ubuntu package를 설치하자.
$ sudo usermod -a -G dialout $YOURID
  => super user 권한 없이 flash(firmware writing) 명령 실행을 위해 반드시 필요하다. 즉, /dev/ttyUSB* 파일에 접근할 수 있도록 설정해 주도록 한다.

<ESP-IDF 설치>
$ mkdir esp32; cd esp32
$ git clone -b v4.1 --recursive https://github.com/espressif/esp-idf.git
  => git으로 부터 source code를 내려 받는다. 양이 좀 많아 오래 걸린다.
$ ls -la
합계 12
drwxr-xr-x 3 chyi chyi 4096 11월 20 11:03 .
drwxr-xr-x 3 chyi chyi 4096 11월 20 11:03 ..
drwxr-xr-x 9 chyi chyi 4096 11월 20 11:05 esp-idf
$ cd esp-idf/
$ ls -la
합계 176
drwxr-xr-x  9 chyi chyi  4096 11월 20 11:05 .
drwxr-xr-x  3 chyi chyi  4096 11월 20 11:03 ..
-rw-r--r--  1 chyi chyi   682 11월 20 11:05 .editorconfig
-rw-r--r--  1 chyi chyi  8916 11월 20 11:05 .flake8
drwxr-xr-x  9 chyi chyi  4096 11월 20 11:05 .git
drwxr-xr-x  4 chyi chyi  4096 11월 20 11:05 .github
-rw-r--r--  1 chyi chyi  1197 11월 20 11:05 .gitignore
-rw-r--r--  1 chyi chyi  4870 11월 20 11:05 .gitlab-ci.yml
-rw-r--r--  1 chyi chyi  2389 11월 20 11:05 .gitmodules
-rw-r--r--  1 chyi chyi   538 11월 20 11:05 .readthedocs.yml
-rw-r--r--  1 chyi chyi  4537 11월 20 11:05 CMakeLists.txt
-rw-r--r--  1 chyi chyi  2406 11월 20 11:05 CONTRIBUTING.rst
-rw-r--r--  1 chyi chyi 15087 11월 20 11:05 Kconfig
-rw-r--r--  1 chyi chyi 11358 11월 20 11:05 LICENSE
-rw-r--r--  1 chyi chyi  5780 11월 20 11:05 README.md
-rw-r--r--  1 chyi chyi  5669 11월 20 11:05 README_CN.md
-rw-r--r--  1 chyi chyi  2821 11월 20 11:05 SUPPORT_POLICY.md
-rw-r--r--  1 chyi chyi  2516 11월 20 11:05 SUPPORT_POLICY_CN.md
-rw-r--r--  1 chyi chyi   723 11월 20 11:05 add_path.sh
drwxr-xr-x 71 chyi chyi  4096 11월 20 11:05 components
drwxr-xr-x  5 chyi chyi  4096 11월 20 11:05 docs
drwxr-xr-x 15 chyi chyi  4096 11월 20 11:05 examples
-rw-r--r--  1 chyi chyi  2059 11월 20 11:05 export.bat
-rw-r--r--  1 chyi chyi  2099 11월 20 11:05 export.ps1
-rw-r--r--  1 chyi chyi  4816 11월 20 11:05 export.sh
-rw-r--r--  1 chyi chyi   529 11월 20 11:05 install.bat
-rw-r--r--  1 chyi chyi   657 11월 20 11:05 install.ps1
-rwxr-xr-x  1 chyi chyi   357 11월 20 11:05 install.sh
drwxr-xr-x  2 chyi chyi  4096 11월 20 11:05 make
-rw-r--r--  1 chyi chyi   971 11월 20 11:05 requirements.txt
-rw-r--r--  1 chyi chyi  1958 11월 20 11:05 sdkconfig.rename
drwxr-xr-x 21 chyi chyi  4096 11월 20 11:05 tools

<toolchain 설치>
$ ./install.sh
  => Xtensa LX6 RISC CPU에 맞는 toolchain을 자동으로 설치한다.
...
Successfully built future
Installing collected packages: click, pyserial, future, ipaddress, six, pycparser, cffi, enum34, cryptography, pyparsing, pyelftools
Successfully installed cffi-1.14.3 click-7.1.2 cryptography-3.2.1 enum34-1.1.10 future-0.18.2 ipaddress-1.0.23 pycparser-2.20 pyelftools-0.27 pyparsing-2.3.1 pyserial-3.4 six-1.15.0
All done! You can now run:

  . ./export.sh

$ .   ./export.sh
  => 환경 설정(file path, toolchain path 지정 등)을 역시 자동으로 해 준다.
📌 시스템 재부팅 시마다 해 주어야 하므로 ~/.bashrc에 넣어주면 편리할 수 있다.

d) hello world 예제 돌려 보기
환경 설정이 마무리 되었으니 hello world 예제를 돌려 보도록 하자. 그전에 아래 그림과 같이 USB cable을 이용하여 target board와 PC를 연결해 주도록 한다.

[그림 2.4] Target board에 firmware image 올리기 [출처 - 참고문헌 1]
📌 위 그림은 예제 program을 하나 build하여 target board에 설치하는 과정을 보여준다.

ESP-IDF의 example은 일반적으로 아래와 같은 형태로 구성되어 있다.

[그림 2.5] example project 구성 [출처 - 참고문헌 1]

📌 ESP-IDF의 build system은 이전 posting에서 소개한 zephyr project의 그것과 유사하게 CMake와 Ninja/Make로 구성되어 있다. 또한 ESP-IDF는 idf.py로 build하고, zephyr는 west(python script)로 build하는 것도 유사하다.
📌 CMake, Ninja/Make로 build system을 구성하는 것이 최근 trend인 듯하다 - Zephyr, AWS FreeRTOS, Azure RTOS 등이 모두 그렇게 되어있다.

$ cd examples/get-started/hello_world
  => hello_world 디렉토리로 이동한다.

[그림 2.6] hello_world_main.c 파일 내용

📌 ESP-IDF application의 main 함수는 app_main( )이다.

$ idf.py menuconfig
  => compile(build)을 위한 환경 설정을 한다.


[그림 2.7] idf.py menuconfig 명령 실행 모습

📌 ESP-IDF는 linux kernel 처럼 Kconfig를 이용하여 환경 설정을 한다.

menuconfig로 변경된 내용은 현재 directory의 sdskconfig 파일에 저장된다. 이는 linux kernel의 .config와 동일한 방식이라고 볼 수 있다.

[그림 2.8] sdkconfig 파일 내용 확인

$ idf.py build
  => compile을 시도한다. 결과는 build 디렉토리에 생긴다.

[그림 2.9] build 결과물 확인

$ idf.py -p /dev/ttyUSB1 flash
  => build 디렉토리에 생성된 hello-world.bin 파일을 target board에 writing하도록 한다.
  => -p 다음에는 실제 Ubuntu에서 인식된 USB2serial device file 명을 입력해준다.

[그림 2.10] target board에 firmware image write하기

📌 위 명령 실행 시 아래와 같은 메시지를 뿌리며 flash fail이 날 수 있는데, 이는 'sudo usermode -a -G dialout $YOURID' 내용이 반영되지 않았기 때문으로 이를 해결하려면 logout or reboot 후 다시 시도해 주면 된다.
...
serial.serialutil.SerialException: [Errno 13] could not open port /dev/ttyUSB1: [Errno 13] Permission denied: '/dev/ttyUSB1'
...

idf.py -p /dev/ttyUSB1 monitor
  => Serial console을 통해 target board에 올라간 firmware의 동작을 확인한다.
📌 이 명령 대신 minicom(115200, 8N1)등을 이용해도 된다.

[그림 2.11] target board에 올라간 hello world program 동작 확인하기

...
...
W (297) spi_flash: Detected size(4096k) larger than the size in the binary image header(2048k). Using the size in the binary image header.
I (307) cpu_start: Starting scheduler on PRO CPU.
I (0) cpu_start: Starting scheduler on APP CPU.
Hello world!
This is ESP32 chip with 2 CPU cores, WiFi/BT/BLE, silicon revision 1, 2MB external flash
Restarting in 10 seconds...
Restarting in 9 seconds...
Restarting in 8 seconds...
Restarting in 7 seconds...
Restarting in 6 seconds...
Restarting in 5 seconds...
Restarting in 4 seconds...
Restarting in 3 seconds...
Restarting in 2 seconds...
Restarting in 1 seconds...
Restarting in 0 seconds...
Restarting now.
ets Jun  8 2016 00:22:57

아래 내용은 debugging(w/ GDB)시, OpenOCD와 JTAG을 이용하고, flash writing & monitoring을 위해서는 esptool.py(idf.py에서 호출)와 USB2UART 장치를 이용할 수 있음을 하나의 그림으로 표현한 것이다.

[그림 2.12] JTAG debugging 및 flash writing & monitoring [출처 - 참고문헌 1]

e) application startup flow
ESP32 SoC는 일반적인 32bit microprocessor와 마찬가지로 아래와 같은 (전형적인) 부팅 방식을 따른다.

bootloader(ROM) -> application(SPI flash)

📌 참고로 ESP32는 secure boot(flash encryption)도 지원한다(편의상 여기에서는 소개하지 않음).

Application image는 flash에 배치되는데, 아래 두개의 그림은 4MB SPI flash의 파티션 table의 구성 예를 보여준다.

먼저 아래 그림은 가장 기본적인 예(table layout)로 1개의 application(= factory app)이 0x10000(64KB) offset에 배치된 모습을 나타낸다.

[그림 2.13] ESP32 flash 파티션 테이블 예 - factory app 영역 할당 [출처 - 참고문헌 1]

한편, 다음 그림은 factory app 이외에도 2개의 OTA app 영역이 할당된 모습을 보여준다. 이때 어떤 녀석으로 부팅할지는 otadata 파티션 정보를 참조하여 bootloader가 결정하게 된다.

[그림 2.14] ESP32 flash 파티션 테이블 예 -  1개의 factory app 및 2개의 OTA 영역 할당 [출처 - 참고문헌 1]

📌 OTA(Over The Air)가 의미하는 것은 network(예: wifi)으로 firmware image를 download 받아 flash에 설치한다는 뜻이다. 

다음으로 알아볼 내용은 system reset 부터 application main 함수 - app_main( ) - 까지의 코드 흐름에 관한 것이다. 특별한 설명 보다는 하나의 그림으로 이를 대신하고자 한다.

[그림 2.15] ESP32의 booting flow - app_main( ) 함수 호출 과정
___________________________________________________________________

이상으로 ESP32용 SDK인 ESP-IDF를 설치하고, 간단한 예제 program 하나를 target board에서 돌려 보았다. 하나의 시스템을 제대로 이해하기 위해서는 파악해야 할 내용이 참으로 많다. (여기서는 수박 겉핥기 식으로 간략히 살펴 보았으니) 보다 자세한 사항은 아래 page 내용을 참조하기 바란다.



3. FreeRTOS Programming
이번 장에서는 ESP32 target board 상에서 몇가지 FreeRTOS 예제 program을 돌려 보고, 이를 분석해 보도록 하겠다. 

당초 계획은 아래 문서에서 설명하는 예제를 중심으로 FreeRTOS의 주요 개념(task, queue, timer, interrupt, event group 등)을 하나씩 상세히 소개하는 것이었으나, 시간 & 지면 관계상 ESP-IDF 예제 중 freertos API를 다양하게 사용하는 것을 골라 분석해 보는 것으로 이를 대신하고자 한다. 😂

FreeRTOS를 제대로 이해하기 위해서는 우선적으로 아래 문서를 정독할 필요가 있다.


1장에서 언급한 바와 같이 FreeRTOS는 kernel core만으로 구성되어 있다. Code도 아주 간결하여 아래에 보이는 C(header file 포함) 파일이 전부이다.

[그림 3.1] FreeRTOS source codes

📌 FreeRTOS는 지난 15년간 (마치 street fighter 처럼) field에서 직접 부딪치며 튼튼하게 발전한 OS이다.

a) FreeRTOS 주요 개념
RTOS에서 task의 개념은 매우 중요하다. RTOS application은 task의 묶음이라고 해도 과언이 아닌데, 각각의 task는 자신만의 context 내에서 실행된다. 특정 시점에 동작하는 task는 오직 한개 뿐이며, task scheduler가 각각의 task가 언제 실행될 지를 결정하게 된다. Task는 또한 자신만의 stack을 할당 받게 되는데, 만일 task swap(교체)이 발생하여 다른 task가 실행된다면, 현재 task의 실행 context 정보는 자신의 stack에 저장되게 된다. 그래야 이 정보를 토대로 나중에 다시 실행될 때 이전 상태를 유지할 수 있는 것이다. 

<Task and Scheduling>
FreeRTOS task는 두가지 상태 즉, Running state와 Not Running state(= Blocked state)를 갖게 되며, 상황에 따라 아래 [그림 3.2]와 같이 상태 전환(천이) 과정을 겪게 된다.

[그림 3.2] freertos task states [출처 - 참고문헌2]


[그림 3.3] freertos task state machine [출처 - 참고문헌2]

📌 task state & transition 부분은 Linux kernel의 그것과 크게 다르지 않다.

FreeRTOS는 동일한 우선순위를 갖는 task가 일정한 시간 간격(time slice)를 두고 CPU를 사용할 수 있도록 하기 위해 tick interrupt(h/w timer를 이용하여 구현)라는 개념을 사용한다. Task scheduler는 tick interrupt가 발생할 때마다 next task가 누가될 지를 결정하게 된다. 아래 그림은 우선 순위가 동일한 2개의 task(Task1, Task2) 중 Task1이 실행되던 상태에서 tick interrupt가 발생했고, Kernel(task scheduler)이 다음번 task로 Task2를 선택한 모습을 보여준다.

[그림 3.4] freertos task management 1 - scheduler의 역할 [출처 - 참고문헌2]

한편 아래 그림은 idle task의 개념을 설명하기 위한 것으로, 2개의 Task2, Task1이 차례로 vTaskDelay( ) 함수(일정시간 지연시 호출함)를 호출하여 각각 Blocked state로 전환될 경우, CPU를 사용하는 task가 하나도 남지 않아 program이 종료될 수 있으므로, 이때 idle task를 사용하여 이 문제를 해결할 수 있게 된다. Idle task는 CPU에 부담을 주지 않는 task를 말한다. 

[그림 3.5] freertos task management 2 - idle task의 역할 [출처 - 참고문헌2]

(FreeRTOSConfig.h 파일 설정에 따라 달라질 수 있지만) FreeRTOS는 기본적으로 preemptive scheduling을 사용한다. 즉, 우선 순위가 높은 task가 그렇지 못한 task로 부터 CPU를 가로챌 수 있다는 뜻이다. FreeRTOS는 real-time behavior를 위해 각각의 task에 부여한 우선순위를 엄격하게 다룬다.

[그림 3.6] freertos task management 3 - task 우선 순위 및 preemption [출처 - 참고문헌2]

📌 역시 정도의 차이는 있으나 task scheduling 부분도 (개념으로 볼 때) Linux kernel의 그것과 별반 다르지 않다. 물론 Real-time이라는 부분에서는 엄격한 차이를 보이긴 하지만 말이다. 예를 들어 linux의 경우 work queue로 던져진 task의 경우 언제 실행될 지 예측할 수가 없다. FreeRTOS에서는 이러한 방식을 절대로 용납하지 않는다.

<Queue>
FreeRTOS는 둘 이상의 task 간 or interrupt와 task 간의 message 전달 및 동기화 등의 목적으로 Queue(List) 개념을 사용한다. 
Queue API에는 block time을 지정할 수 있는 parameter가 있는데, task가 비어 있는 queue에서 message를 읽으려 할 때, task는 읽을 수 있는 message가 새로 추가되거나, block time이 경과하게 될 때까지 Blocked state로 들어가게 된다. 이와 유사하게 queue가 꽉 차 있는 상태에서 task가 queue에 message가 쓰려고 할 경우에도 queue에 쓸 공간이 생기거나, block time이 경과할 때까지 해당 task는 Blocked state로 들어가게 된다.

[그림 3.7] freertos queue management [출처 - 참고문헌2]

아래 그림은 Queue 1개를 만든 상태에서 이를 사용하는 sender task가 여러 개(예: 3개)이고, receiver task가 1개인 예를 보여준다. 또한 Queue를 통해 전달하는 내용이 구조체 형태임도 알 수  있다.

[그림 3.8] freertos queue management  - multiple sender, 1 receiver [출처 - 참고문헌2]

<Timer>
Timer는 정해진 시간(period)이 경과하면 원하는 action(function - callback function)을 수행하도록 하는 s/w 기능이다. FreeRTOS는 한차례 정해진 action만 실행하는 one-shot timer와 일정한 주기마다 자동으로 action을 반복 수행하는 auto-reload timer 두가지를 제공한다.

[그림 3.9] freertos s/w timer management  1 - one-shot, auto-reload timer [출처 - 참고문헌2]

[그림 3.10] freertos s/w timer management  2 - auto-reload timer의 상태도 [출처 - 참고문헌2]

그렇다면 Timer의 action(function)은 누가 실행해 주는 것일까 ? 아래 그림에서 보는 것 처럼 RTOS Daemon task(scheduler 시작 시 자동으로 생성되는 task)라는 녀석이 있어서 timer function을 실행해 주게 된다. 아래 그림과 같이 Daemon task는 timer function을 실행하거나 timer를 중지하거나 등의 요청을 Timer Command Queue를 통해 다른 task(예: main routine)로 부터 전달받는 형태를 취한다.

[그림 3.11] freertos s/w timer management  3 - daemon task의  역할 [출처 - 참고문헌2]

📌 timer도 역시 linux의 그것과 별반 다르지 않다.

<Interrupt and synchronization>
마지막으로 아래 그림은 ISR(Interrupt Service Routine)과 지연처리 task(deferring interrupt processing task)의 개념을 보여준다. ISR 내에서는 수행시간이 오래 걸리거나 Blocked state로 전환되는 API(함수)를 호출해서는 안된다. 따라서 이 때에는 시간이 많이 걸리는 부분을 별도의 task로 분리하여 실행(deferring task)해 줌으로써 ISR을 간소화시켜 주게 된다. 또한 별도로 분리된 task의 우선 순위를 여타 task에 비해 높게 설정해 줌으로써, interrupt service routine이 연속적으로 실행되는 효과를 가져오게 만들어 줄 수 있다. 

[그림 3.12] freertos s/w interrupt service routine & deferring interrupt processing [출처 - 참고문헌2]

📌 위의 개념은 Linux의 interrupt(top half)와 bottom half(tasklet, workqueue, thread) 개념과 동일한 내용이라고 볼 수 있다.
📌 ISR내에서는 기존의 API 대신 "FromISR"로 끝나는 API만을 사용할 수 있다.

FreeRTOS는 mutual exclusion(상호배제) 및 synchronization(동기화)을 위해 binary semaphore, couting semaphore 및 mutex 기능을 제공한다. Binary semaphore는 2가지 값만을 가지고 있어서 task와 task 간 or task와 interrupt 간의  동기화를 맞추고자 할 때 쓰면 편리하다. Counting semaphore는 2개 이상의 값을 가지게 되므로, 2개 이상의 task간의 동기화를 맞추고자 할 때 사용할 수  있다. 마지막으로 mutex는 우선 순위 상속 메카니즘이 있는 일종의 binary semaphore라고 말할 수 있다. 참고로 위의 그림(3.12)에는 Semaphore를 사용하여 ISR(semaphore give)과 지연처리 task인 Task2(semaphore take)를 동기화 시켜 주는 내용이 포함되어 있다.

이상으로 FreeRTOS의 개념과 관련하여 아주 기본적은 사항만을 요약해 보았다. 거의 370여 page에 달하는 FreeRTOS 관련 내용을 11개의 그림으로 응축해 놓았기 때문에, 당연히 추가로 확인해 보아야 할 내용(Heap management, Event group, Task notification 등)이 많이 있을 것으로 보인다. 부족한 설명은 독자 여러분의 몫으로 남겨두도록 한다. 😋

b) FreeRTOS 예제 소개
앞서 FreeRTOS의 기본 동작 원리를 살펴 보았으니, 이제 부터는 간단한 예제를 하나 분석해 보도록 하자.

esp-idf/examples/system/freertos/real_time_stats

$ cd esp-idf/examples/system/freertos/real_time_stats
$ idf.py menuconfig
$ idf.py build
$ idf.py -p /dev/ttyUSB1 flash
$ idf.py -p /dev/ttyUSB1 monitor

[그림 3.13] real_time_stats application 실행 모습

--------------------------------------------------- ............................ (A)
Getting real time stats over 100 ticks
| Task | Run Time | Percentage
| stats | 945 | 0%                                     # stats task
| spin1 | 225416 | 11%                          # spin task 1
| IDLE1 | 390994 | 19%                         # idle task 1(for cpu1)
| IDLE0 | 288281 | 14%                         # idle task 1(for cpu0)
| spin3 | 225379 | 11%                          # spin task 3
| spin0 | 225412 | 11%                          # spin task 0
| spin2 | 217254 | 10%                          # spin task 2
| spin5 | 200956 | 10%                          # spin task 5
| spin4 | 225362 | 11%                          # spin task 4
| Tmr Svc | 0 | 0%                                    # daemon task(for timer)
| ipc1 | 0 | 0%                                          # queue(for cpu1) ???
| ipc0 | 0 | 0%                                          # queue(for cpu0) ???
| esp_timer | 0 | 0%                                # esp timer
Real time stats obtained
---------------------------------------------------

예상했겠지만, 코드 자체의 분량은 많지가 않다.

chyi@mars:~/workspace/Boards/ESP/esp32/esp-idf/examples/system/freertos/real_time_stats/main$ ls -la
합계 24
drwxr-xr-x 2 chyi chyi 4096 12월  1 20:57 .
drwxr-xr-x 4 chyi chyi 4096 11월 25 11:44 ..
-rw-r--r-- 1 chyi chyi   98 11월 20 11:05 CMakeLists.txt
-rw-r--r-- 1 chyi chyi  146 11월 20 11:05 component.mk
-rw-r--r-- 1 chyi chyi 6514 11월 20 11:05 real_time_stats_example_main.c

real_time_stats_example_main.c 파일은 아래와 같이 4개의 함수로 이루어진 간단한 program에 불과하다. 이 program이 하는 일은 주기적으로 위의 결과 (A) console에 출력(마치 Linux의 ps 명령 처럼)하는 것이 전부이다.
___________________________________________________________________________________

아래 함수는 stats_task( ) 함수에서 pdMS_TO_TICKS(1000)(= 1 초) 시간마다 한번씩 호출되는 함수로, 이곳에서 위의 결과 (A)를 출력한다.


아래 두개의 함수는 app_main( )에서 생성한 두개의 task 함수이다.
먼저 spin_task는 sync_spin_task semaphore가 해제되기를 기다린 후, 해제될 경우 while loop을 돌며 pdMS_TO_TICKS(100) 간격으로 CPU를 점유(점유 후, 단순히 No operation 수행)하도록 하는 단순한 코드이다.

반면에 stats_task는 역시 sync_stats_task semphore가 해제되기를 기다린 후, 해제 시 앞서 spin_tasks가 기다리는 sync_spin_task semaphore를 해제시켜 준다. 이후, while loop을 돌면서 pdMS_TO_TICKS(1000) 간격으로 print_real_time_stats( ) 함수를 호출해 준다.


아래 코드가 main routine이다. main routine이 하는 일은 간단 명료하다.

1) 1개의 count semaphore sync_spin_task를 생성한다. 얘는 6개의 spin_task에서 사용하게 된다.
2) 1개의 binary semaphore sync_stats_task를 생성한다.
3) 6개의 spin_task를 생성한다. 여기에서는 xTaskCreate( ) 함수 대신 espressif에서 만든 xTaskCreatePinnedToCore( ) 함수를 사용한다.
4) 1개의 stats_task를 생성한다. 이때도 역시 xTaskCreate( ) 함수 대신 espressif에서 만든 xTaskCreatePinnedToCore( ) 함수를 사용한다.
5) 마지막으로, xSemaphoreGive(sync_stats_task)를 호출하여 sync_stats_task semaphore를 해제(give)한다.


📌 xTaskCreatePinnedToCore( ) 함수와 관련해서는 아래 page의 내용을 살펴 보기 바란다.
___________________________________________________________________________________

지금까지 FreeRTOS의 동작 원리를 살펴 보고, FreeRTOS 예제를 하나 선택하여 그 코드를 분석해 보았다(Queue API 사용 부분이 빠져 있어서 좀 아쉽다).


4. 아직 못다한 이야기
ESP32는 Wi-Fi와 BLE를 위한 SoC이다. 따라서 ESP-IDF에는 이와 관련한 많은 예제가 포함되어 있다. 앞으로 시간을 두고 좀 더 살펴 보아야 하겠다. 😜

a) Wi-Fi programming

b) BLE programming

e) Secure Boot

f) Flash encryption


To be continued...


5. References
[1] https://docs.espressif.com/projects/esp-idf/en/stable/get-started/index.html
[2] Mastering the FreeRTOS Real Time Kernel - A Hands On Tutorial Guide, Richard Barry
[3] The FreeRTOS Reference Manual, Richard Barry
[4] 2019 Embedded Markets Study - Integrating IoT and Advanced Technology Designs,
Application Development & Processing Environments, March 2019, AspenCore
[5] https://aws.amazon.com/ko/blogs/korea/announcing-amazon-freertos/
[6] 사물인터넷을 품은 라즈베리파이 개정판, 김성우, Jpub
[7] https://www.cnx-software.com/2016/03/25/esp8266-and-esp32-differences-in-one-single-table/
[8] And, Google~


Slowboot

2020년 11월 6일 금요일

Zephyr RTOS Project

이번 시간에는 3년 전에 한차례 소개한 바 있는 Zephyr RTOS Project를 다시 조명해 보고자 한다. 그 동안 많은 변화(v1.7 -> v2.4.99)가 있었는데, 왜 Zephyr가 앞으로 성공할 수 밖에 없는지 하나 하나 파헤쳐 보도록 하자. 😎



RTOS/IoT OS 계의 새로운 바람~


목차
1. Zephyr reloaded
2. Zephyr 개발 환경 설정
3. Build system 소개
4. Device tree 기반의 device driver model 
5. 아직 못다한 이야기
6. References


아래 내용을 소개하기에 앞서 3년 전에 작성해 둔 아래 posting을 먼저 확인해 보기 바란다.

<2024년 11월 새로운 글> 🚀


1. Zephyr reloaded
3년 전 v1.7에 비해 이 글을 쓰고 있는 현재(2020. 11) v2.4.99 버젼이 나오기까지 눈에 띄는 변화는 CMake, Ninja/Make & Python3를 중심으로 하는 build system과 swiss army knife에 해당하는 west(python으로 작성)라는 tool이 추가된 점을 들 수 있다. 또한 200여개 이상의 board를 지원하면서 다양한 soc 및 board 관련 코드가 추가되었고, 많은 보드의 다양한 device를 효과적으로 처리하기 위해 (linux 처럼) device tree를 기반으로 하는 device driver model이 제대로 정착되었다는 점 등을 꼽을 수 있을 것 같다.

주요 구성 요소:
− Python 3: Script interpreter and packages
− CMake/Ninja/Make: Build system
− Device Tree Compiler: Compiles device tree hardware descriptions
− Toolchain: gcc for Arm, RISC-V, x86, etc.
− Debug/Flash Tools: J-Link, pyOCD, OpenOCD, etc.
− West: Custom tool for repository management, build/flash/debug assistance, and image signing
− Zephyr Git repositories: The source code!

주요 특징:
1) Zephyr는 Linux를 사용하기에는 부담이 되는 작은 시스템(resource constrained system)을 target으로 하고 있다.

Linux Foundation's Strategy
Zephyr(resource constrained system) + Linux(resource not constrained system)
=
IoT Devices based on Zephyr + IoT Gateway based on Linux

[그림 1.1] Zephyr RTOS Overview [출처 - 참고 문헌 2]

2) Zephyr는 여타 RTOS(예: FreeRTOS, ARM mbedOS, NuttX 등)와 마찬가지로 RTOS 및 IoT OS를 위해 필요한 왠만한 기능은 이미 다 갖추고 있다.

 
[그림 1.2] Zephyr RTOS Architecture [출처 - 참고 문헌 2]

3) Zephyr는 이제 200개 이상의 board(ARM Cortex-M 계열, x86, ARC, XTENSA 등)를 지원하며, 다양한 상용 제품에 널리 활용되고 있다.

[그림 1.3] Zephyr 지원 보드 - 200개 이상(2020년 현재) [출처 - 참고 문헌 2]

[그림 1.4] Zephyr를 활용한 상용 제품 [출처 - 참고 문헌 2]

4) 문서 정리가 아주 잘 되어 있다.

[그림 1.5] Zephyr Documentation


5) 특정 회사, 개인이 중심이 되어 개발된 project가 아니라 community가 그 중심인 project라는 면에서 Zephyr만이 진정한 의미에서의 open source RTOS 이다.

____________________
아래 내용은 Zephyr 진영에서 작성한 것이긴 하지만, zephyr가 여타 RTOS에 비해 지난 1년간 가장 활발히 개발되고 있는 open source RTOS project임을 보여준다.

[그림 1.6] 지난 해 RTOS 간 commit 횟수 비교 [출처 - 참고 문헌 2]

📌 Code commit이 활발하다는 것은 말 그대로 가장 관심이 높다는 의미이기도 하겠지만, 아직 개발할 게 많이 남아있다는 의미일 수도 있겠다. 참고로 우측의 FreeRTOS는 sourceforge => Github으로 전환한 것이 얼마되지 않아 활동량이 많지 않다는 얘기도 있다.
📌 현재까지 RTOS 1위는 FreeRTOS(얼마 전에 AWS에서 인수하여 AWS FreeRTOS로 판매 중)이지만, 그 뒤를 ARM mbedOS가 바짝 뒤 쫒고 있는 듯하다. 하지만 머지 않아 Zephyr가 이들을 제치고 그 위용을 들어낼 날이 곧 올 것으로 믿어 의심치 않는다.
📌 참고로 위 그림에는 없지만 최근 (개인적으로 별로 좋아하지 않는)M$에서도 Azure Sphere라는 RTOS를 밀고 있는 듯하다.

그야말로 RTOS 춘추 전국 시대 ~ 특정 RTOS가 천하를 통일할 것으로는 보지 않는다. 하지만 여러가지 면에서 볼 때 Zephyr는 장래가 매우 촉망되는 RTOS임에 틀림없어 보인다. 😍


2. Zephyr 개발 환경 설정
Zephyr 개발 환경 구축과 관련해서는 아래 site에 아주 정리가 잘 되어 있다(그대로 따라하기만 하면 된다 😋). 기존(make 중심)에 비해 설치 과정이 다소 복잡해 진 느낌이나, west라는 tool이 추가되어 있어 전체적으로 편리해 진 것도 사실이다.


이 절에서는 Ubuntu 18.04를 기준으로 환경 설정을 해 보도록 하겠다. 상세한 내용은 위의 page를 참조하기 바란다.
📌 당연히 Windows, macOS 등에서도 개발 가능하다.

<package 설치>
$ sudo apt update
$ sudo apt upgrade

$ sudo apt install --no-install-recommends git cmake ninja-build gperf \
  ccache dfu-util device-tree-compiler wget \
  python3-dev python3-pip python3-setuptools python3-tk python3-wheel xz-utils file \
  make gcc gcc-multilib g++-multilib libsdl2-dev

<cmake upgrade>
$ cmake --version
cmake version 3.10.2

📌 cmake version 3.13.1 이상이어야 한다. 따라서 아래 내용을 수행해 준다.
$ wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | sudo apt-key add -
$ sudo apt-add-repository 'deb https://apt.kitware.com/ubuntu/ bionic main'
$ sudo apt update
$ sudo apt install cmake

$ cmake --version
cmake version 3.18.4

<zephyr 설치 및 python 관련 package 설치>
$ pip3 install --user -U west
$ echo 'export PATH=~/.local/bin:"$PATH"' >> ~/.bashrc
$ source ~/.bashrc
📌 python으로 작성된 west는 source code download(마치 repo와 같은 역할)는 물론이고 build 시에도 사용된다.

$ west init ~/zephyrproject
$ cd ~/zephyrproject
chyi@mars:~/zephyrproject$ west update

chyi@mars:~/zephyrproject$ ls -la
합계 28
drwxr-xr-x  7 chyi chyi 4096 10월 30 10:38 .
drwxr-xr-x 21 chyi chyi 4096 10월 30 10:40 ..
drwxr-xr-x  2 chyi chyi 4096 10월 30 10:35 .west
drwxr-xr-x  3 chyi chyi 4096 10월 30 10:38 bootloader
drwxr-xr-x  9 chyi chyi 4096 10월 30 10:39 modules
drwxr-xr-x  5 chyi chyi 4096 10월 30 10:39 tools
drwxr-xr-x 23 chyi chyi 4096 10월 30 10:35 zephyr

chyi@mars:~/zephyrproject$ west zephyr-export
Zephyr (/home/chyi/zephyrproject/zephyr/share/zephyr-package/cmake)
has been added to the user package registry in:
~/.cmake/packages/Zephyr

ZephyrUnittest (/home/chyi/zephyrproject/zephyr/share/zephyrunittest-package/cmake)
has been added to the user package registry in:
~/.cmake/packages/ZephyrUnittest


chyi@mars:~/zephyrproject/zephyr$ pip3 install --user -r ~/zephyrproject/zephyr/scripts/requirements.txt

<toolchain 설치>
chyi@mars:~/workspace/Zephyr$ wget https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v0.11.4/zephyr-sdk-0.11.4-setup.run
chyi@mars:~/workspace/Zephyr$ chmod 755 ./zephyr-sdk-0.11.4-setup.run
chyi@mars:~/workspace/Zephyr$ ./zephyr-sdk-0.11.4-setup.run -- -d ~/zephyr-sdk-0.11.4
chyi@mars:~/zephyr-sdk-0.11.4$ ls -la
합계 56
drwxr-xr-x 13 chyi chyi 4096 10월 30 11:05 .
drwxr-xr-x 23 chyi chyi 4096 10월 30 11:04 ..
drwxr-xr-x  8 chyi chyi 4096  6월 25 20:49 aarch64-zephyr-elf
drwxr-xr-x  8 chyi chyi 4096  6월 25 21:50 arc-zephyr-elf
drwxr-xr-x  8 chyi chyi 4096  6월 25 21:56 arm-zephyr-eabi
drwxr-xr-x  3 chyi chyi 4096  6월 25 20:42 cmake
drwxr-xr-x  2 chyi chyi 4096 10월 30 11:05 info-zephyr-sdk-0.11.4
drwxr-xr-x  8 chyi chyi 4096  6월 25 21:00 nios2-zephyr-elf
drwxr-xr-x  8 chyi chyi 4096  6월 25 21:05 riscv64-zephyr-elf
-rw-r--r--  1 chyi chyi    7 10월 30 11:05 sdk_version
drwxr-xr-x  8 chyi chyi 4096  6월 25 21:01 sparc-zephyr-elf
drwxr-xr-x  3 chyi chyi 4096  6월 25 21:15 sysroots
drwxr-xr-x  8 chyi chyi 4096  6월 25 20:50 x86_64-zephyr-elf
drwxrwxr-x  9 chyi chyi 4096 10월 30 11:05 xtensa

chyi@mars:~/workspace/Zephyr$ sudo cp ~/zephyr-sdk-0.11.4/sysroots/x86_64-pokysdk-linux/usr/share/openocd/contrib/60-openocd.rules /etc/udev/rules.d

$ sudo udevadm control --reload

필자는 Linux 환경에 익숙해서인지 terminal 환경에서 개발하는 것을 좋아한다. 하지만 IDE 개발 환경을 선호하는 개발자들도 많은데, 이를 위해 zephyr는 아래의 두가지 방식을 지원한다.

1) Eclipse 환경


2) (Third party에서 개발한) Platform IO(Visual Studio plugin) 환경



자, zephyr 개발 환경이 모두 준비되었으니, 이제부터는 실제 예제를 하나 돌려 보면서 zephyr build system이 어떻게 동작하는지를 살펴 보도록 하자.


3. Build system 소개
이 장에서는 zephyr sample code를 하나 돌려본 후, 실제 build 시스템이 어떻게 구성되어 있는지를 그림을 통해 상세히 파악해 볼 것이다.

a) Target board
예제 코드를 돌려 보기 위해서는 target board가 하나 필요한데, 지금부터는 아래 보드를 사용하기로 한다.
ARM Cortex-M3, 72Mhz, 128KB Flash, 20KB SRAM, ST-Link/V2 debugger 장착
[그림 3.1]  Target Board - STM32 Nucleo F103RB [출처 - 참고문헌 3]

📌 Nucleo F103RB 보드는 $10 ~ 15(한화로 13,000 ~ 14,000원 정도) 정도로 가격이 아주 저렴하다. 또한 ST-Link/V2 debugger(선이 그어져 있는 그림 우측 부분)가 장착되어 있어, 곧 바로 flash writing이 가능하다. 

b) 예제 build해 보기 - synchronization
chyi@mars:~$ cd ~/zephyrproject/
chyi@mars:~/zephyrproject$ cd zephyr/
chyi@mars:~/zephyrproject/zephyr$ west build -b nucleo_f103rb -s samples/synchronization

[그림 3.2] Zephyr 예제 코드 build 모습(1)

[그림 3.3] Zephyr 예제 코드 build 모습(2) - output directory

zephyr.elf 파일이 생성되었으니, west flash 명령을 이용하여 target board에 writing해 보도록 하자.

chyi@mars:~/zephyrproject/zephyr$ west flash

[그림 3.4] west를 이용한 flash writing 모습(1)

[그림 3.5] west를 이용한 flash writing 모습(2) - minicom console(/dev/ttyACM0, 115200, 8N1)

📌 west를 이용해  clean을 하려면 ...
$ west build -t clean          # built 파일을 모두 삭제
$ west build -t pristine      # build 디렉토리를 통째로 날림.
_________________________________________________

앞서 build해 본 바와 같이 zephyr build 과정은 크게 2단계로 나뉜다.

   1) CMake에 의한 configuration 단계 
        => dts/binding/Kconfig 등을 이용하여 주요 header 파일 및 Makefile(or Ninja 파일) 생성
        ex) cmake -B build -GNinja -DBOARD=nucleo_f103rb samples/hello_world
   2) Make or Ninja를 이용한 실제 build 단계
        => 실제 build 절차를 진행하여 elf 파일 등 생성
        ex) ninja -C build or make -C build

📌 Zephyr build system은 CMake, Ninja/Make를 주축으로 하며, 중간 중간에 python script가 적절히 활용되고 있다.

c) CMake에 의한 configuration 단계
이 단계에서는 dts 파일, binding 정보, prj.conf 및 Kconfig 파일 정보 등을 토대로 아래와 같이 autoconf.h, .config, devicetree.h, Makefile or Ninja file 등을 생성한다.

[그림 3.6] CMake에의 configuration 단계(header 파일 생성[출처 - 참고문헌 1]


d) Make or Ninja를 이용한 실제 build 단계
실제 build 단계는 다시 4개의 과정 즉, pre-build, first-pass binary, final binary, post-processing 으로 세분화 될 수 있다.

[그림 3.7] Ninja or Make를 이용한 build 단계 1(pre-build[출처 - 참고문헌 1]


[그림 3.8] Ninja or Make를 이용한 build 단계 2(first-pass binary[출처 - 참고문헌 1]


[그림 3.9] Ninja or Make를 이용한 build 단계 3(final binary[출처 - 참고문헌 1]


[그림 3.10] Ninja or Make를 이용한 build 단계 4(post-processing[출처 - 참고문헌 1]

각각의 단계를 상세히 소개하는 것은 간단한 일이 아니다. 따라서 보다 자세한 사항은 아래 site 내용을 살펴 보기 바란다.



4. Device tree 기반의 device driver model
이번 장에서는 device tree를 기반으로 하는 zephyr RTOS의 device driver model을 분석해 볼 것이다. 사실 이 부분은 조금 난해할 수 있다. 하지만 반드시 이해해야 하는 부분이기도 하다.

a)  blinky 예제 분석하기
Zephyr의 device driver 동작 원리를 파악하기 전에, 관련 예제 코드( blinky)를 먼저 살펴 보도록 하자.

chyi@mars:~/zephyrproject/zephyr/samples/basic/blinky$ ls -la
합계 28
drwxr-xr-x  3 chyi chyi 4096 11월  2 10:39 .
drwxr-xr-x 10 chyi chyi 4096 10월 30 11:14 ..
-rw-r--r--  1 chyi chyi  188 10월 30 10:35 CMakeLists.txt          # CMake 파일용
-rw-r--r--  1 chyi chyi 1196 10월 30 10:35 README.rst
-rw-r--r--  1 chyi chyi   14 10월 30 10:35 prj.conf                        # kernel config 기술
-rw-r--r--  1 chyi chyi  224 10월 30 10:35 sample.yaml              # binding 파일
drwxr-xr-x  2 chyi chyi 4096 11월  4 11:09 src

[그림 4.1] CMakeLists.txt 파일

[그림 4.2] prj.conf 파일

chyi@mars:~/zephyrproject/zephyr/samples/basic/blinky/src$ ls -la
합계 16
drwxr-xr-x 2 chyi chyi 4096 11월  4 11:09 .
drwxr-xr-x 3 chyi chyi 4096 11월  2 10:39 ..
-rw-r--r-- 1 chyi chyi 1084 10월 30 11:31 main.c

📌 Zephyr application(or device driver)은 모두 위와 같이 구성되어 있다. src 디렉토리 아래에 C & header 파일을 추가해 주면 된다.

main.c 파일을 열어 보니, 예상대로 몇줄 되지 않는다. 하지만 몇 줄 안되는 아래 코드를 제대로 이해하기 위해서는 파악할 내용이 만만치 않다.

[그림 4.3] blinky 예제 코드

우선 제일 먼저 눈에 들어오는 코드 부분은 아래 한 줄이다. 다른 device driver 예제를 살펴 보아도 모두 제일 먼저 이 함수를 호출한다.

dev = device_get_binding(LED0)

____________________________ 위의 코드 분석 시도 ____________________________
위 코드에서 LED0는 아래와 같이 define되어 있다. 

#define LED0    DT_GPIO_LABEL(LED0_NODE, gpios)
#define LED0_NODE DT_ALIAS(led0)
-> 줄여서 이렇게도 볼 수 있다.
#define LED0    DT_GPIO_LABEL(DT_ALIAS(led0), gpios)

📌그렇다면  led0, gpios는 어디에 있는 정보일까 ?

좀더 따라 들어가 보도록 하자. DT_GPIO_LABEL은 include/devicetree/gpio.h 파일에 아래와 같이 정의되어 있다.

#define DT_GPIO_LABEL(node_id, gpio_pha) \
    DT_GPIO_LABEL_BY_IDX(node_id, gpio_pha, 0)
=>
#define DT_GPIO_LABEL_BY_IDX(node_id, gpio_pha, idx) \
    DT_PROP_BY_PHANDLE_IDX(node_id, gpio_pha, idx, label)
=>
#define DT_PROP_BY_PHANDLE_IDX(node_id, phs, idx, prop) \
    DT_PROP(DT_PHANDLE_BY_IDX(node_id, phs, idx), prop)
=>
#define DT_PROP(node_id, prop) DT_CAT(node_id, _P_##prop)    ... include/devicetree.h
=>
#define DT_CAT(node_id, prop_suffix) node_id##prop_suffix

node_id##prop_suffix 가 의미하는 것은 또 무엇일까 ?
________________________________________________________________________

눈치 빠른 분은 예상했겠지만, 위 내용은 device tree와 연관이 있다. 따라서 이 대목에서 우리는 Nucleo f103rb의 device tree 파일 및 binding file(yaml)을 확인해 볼 필요가 있다.

[그림 4.4] zephyr/boards/arm/nucleo_f103rb/nucleo_f103rb.dts 파일

[그림 4.5] zephyr/samples/basic/blinky/sample.yaml

(설명이 길어지는 관계로) 답을 미리 말하자면, 앞에서 분석한 내용 중 led0gpios는 각각 위의 device tree 내용 중 아래 부분에 해당한다. 
______________________________________________
led0 = &green_led_2;

leds {
        compatible = "gpio-leds";    # samples/basic/blinky/sample.yaml 내용과 연관
        green_led_2: led_2 {
               gpios = <&gpioa 5 GPIO_ACTIVE_HIGH>;
               label = "User LD2";
        };
 };
______________________________________________

📌 Linux에서 device driver와 device tree를 연결하는 것이 compatible 속성이었던 것을 기억하는가 ? Zephyr에서는 이 연결 작업을 device driver에서 run time에 하는 것이 아니라 compile time에 device tree와 위의 binding file을 이용하여 진행한다.

위 내용 중에서 device driver에서 실제로 필요로 하는 것은 녹색으로 표시한 GPIO 관련  정보일 것이다. 따라서 (다시 처음 코드로 돌아와) 우리는 아래 코드가 led0(alias 명) -> gpios(속성 명) 를 이용해서 leds 노드 정보를 찾는 과정임을 유추해 볼 수가 있다.

dev = device_get_binding(LED0)

📌그런데 kernel source code 어디에도 device_get_binding( ) 함수가 보이질 않는다 ? 어디에 있을까 ?

아직 필자는 앞에서 살펴본 매우 복잡한 #define 문의 내용이 무엇을 의미하는지를 제대로 설명하지 못했다. 이 내용을 파악하기 위해서는 (아래 page를 참조하여) 좀 더 깊게 들어가 볼 필요가 있다.


b) Zephyr의 device tree
Zephyr는 Linux의 brother OS이다. 따라서 Linux의 많은 장점을 그대로 채용하여 사용하고 있는데, 그 중 하나가 바로 device tree이다. Device tree에 대해서는 본 blog를 통해 여러 차례 소개한 바 있다.


하지만 zephyr는 resource constrained 장치용 OS이다 보니, device tree를 binary 형태(dtb 파일)로 유지(flash memory에 탑재)하는 것도 부담이 된다. 따라서 Linux와는 달리 dtb 파일 형태를 유지하지 않고, pre-compile 단계에서 device tree를 적절히 파싱하여 header 파일로 만든 후, binding 과정(yaml 파일 내용)을 거쳐 elf에 적절히 녹이는 방식을 사용한다(?).

<Linux의 device tree 처리 방식>
dtb 파일 생성 -> memory에 load -> kernel 에서 runtime에 dtb 내용을 읽어 들여 필요한 device 속성 정보 추출 -> device driver에서 이를 사용

[그림 4.6] Linux에서 device tree를 처리하는 방식 [출처 - 참고문헌 4]

<Zephyr의 device tree 처리 방식>
pre-compile 단계에서 device tree  파일로 부터 header 파일 생성 -> build  단계에서 device tree 정보를 이용하여 elf 파일에 포함 -> device driver에서 device_get_binding( ) 함수를 호출하여 사용

아래 그림은 dts 파일로 부터 header 파일이 생성되는 과정을 보여준다.


[그림 4.7] Zephyr에서 device tree를 처리하는 방식 - device tree로 부터 header 전환 과정 [출처 - 참고문헌 1]

📌 중간 결과물인  zephyr.dts는 debugging 용일 뿐 실제로 사용되지는 않는다. 즉 dtb로 변환되어 memory에 load되지 않는다.

이렇게 해서 생성한 header 파일 중 하나가 바로 devicetree_unfixed.h 파일(전부 #define 문으로 구성됨)이다.

[그림 4.8] build/zephyr/include/generated/devicetree_unfixed.h

이후, (너무 모호하게 설명하는 듯 하지만) build 과정에서 device tree 정보가 elf 파일에 적절히 녹아 들어가게 된다(linker.cmd 이용).

c) Device driver model
다음으로 알아볼 내용은 zephyr의 device driver model에 관한 것이다. Zephyr는 device를 위해 아래와 같이 struct devicedevice_get_binding( ) 함수를 정의해 두고 있다(Linux kernel 처럼 복잡한 방식이 아니다). Application code(device driver)에서는 device_get_binding( ) 함수를 이용하여 device(struct device)를 정보를 획득한 후, 이를 이용해 실제 device driver 관련 나머지 작업을 진행한다.

[그림 4.9] struct device - zephyr/include/device.h

앞서 찾을 수 없었던, device_get_binding( ) 함수의 실체는 다음과 같다.
______________________________________________________________________
extern const struct device * z_impl_device_get_binding(const char * name);
static inline const struct device * device_get_binding(const char * name)
{
#ifdef CONFIG_USERSPACE
    if (z_syscall_trap()) {
        return (const struct device *) arch_syscall_invoke1(*(uintptr_t *)&name, K_SYSCALL_DEVICE_GET_BINDING);
    }
#endif
    compiler_barrier();
    return z_impl_device_get_binding(name);
}
______________________________________________________________________
 [코드 4.1] device_get_binding( ) 함수 - 
zephyr/build/zephyr/include/generated/syscalls/device.h

z_impl_device_get_binding( ) 함수에서 device 정보를 찾는 과정을 좀 더 깊게 따라가 보면 다음과 같다.

[그림 4.10] z_impl_device_get_binding( ) 함수 - kernel/device.c

위 함수는 __device_start 부터 __device_end까지의 device를 대상으로 dev->name이 일치하는 무언가(= struct device)를 찾아내는 일을 하고 있는 듯 보인다.

그렇다면, (kernel/device.c에 아래와 같이 선언되어 있는) __device_start ~ __device_end의 실제 내용은 어디에 있을까 ?
extern const struct device __device_start[];
extern const struct device __device_end[];

이를 source code에서는 찾을 수 없어, build 결과물 중에서 뒤져 보기로 한다.

chyi@mars:~/zephyrproject/zephyr/build/zephyr$ grep -rl __device_start *
kernel/libkernel.a
kernel/CMakeFiles/kernel.dir/device.c.obj
linker.cmd
linker_pass_final.cmd
zephyr.elf
zephyr.lst
zephyr.map
zephyr_prebuilt.elf
zephyr_prebuilt.map

그 결과, linker.cmd 파일에서 __device_start 부분을 확인할 수 있었다. 아래 내용은 linker script로 보이는데, 이게 어떤 것을 의미하는 것일까 ?

[그림 4.11] zephyr/build/zephyr/linker.cmd 내용 중에서 발췌

아직 내용이 정확하지는 않지만, 앞서 언급한 대로 "build 단계에서 binding 정보와 일치하는 device tree 정보가 linker.cmd 등을 참조하여 elf에 포함되었고, application(device driver)에서는 device_get_binding( ) 함수를 통해 name이 일치하는 device 정보를 바로 추출하여 사용한다" 정도로 정리가 될 것 같다.
📌 이 부분은 시간을 두고 좀 더 정확하게 분석해 볼 필요가 있다. 😂

d) C/C++ code에서 device tree 접근하기 
앞서 복잡하게 분석해 들어간 #define 문의 정확한 의미를 파악하기 위해서는 아래 글이 도움이 될 것이다.


내용이 길어지는 관계로 아래 내용을 copy & paste하는 것으로 설명을 대신하기로 하겠다.

[그림 4.12] device tree node ID 값을 얻는 방법 [출처 - 참고문헌 1]

📌 위의 내용이 뜻하는 바는 윗 부분의 device tree 내용 중 node 부분을 아래 부분의 DT_XXX( ) macro로 찾을 수 있다는 뜻이다.

앞서도 이미 언급한 바와 같이 zephyr는 device tree blob 형태로 device tree를 사용하지 않고, 이를 header 파일로 적절히 변환(#define 정보 형태로 기술)하고, C code에서는 #define 정보를 이용하여 device tree의 node 정보를 획득하게 된다.

Device tree node를 인식하는 방법으로는 크게 아래와 같이 6가지가 있다(매우 중요한 부분이지만 자세한 설명은 생략한다).
1) Path에 의한 방법
2) Node label을 이용한 방법
3) Alias 정보를 이용한 방법
4) Instance number를 이용한 방법
5) Chosen node를 이용한 방법
6) Parent or child node를 이용한 방법

끝으로, 아래 내용은 그림 4.8에서 한 차례 언급했던  build/zephyr/include/generated/devicetree_unfixed.h 파일을 보여준다.

[그림 4.13] device tree가 header로 변경된 모습

e) blinky 예제의 나머지 부분 
이 부분은 크게 어려운 부분이 아니다. 먼저 (A)에서는 GPIO 5번 pin(GPIO_ACTIVE_HIGH)을 output mode로 설정하고, (B)에서는 무한 loop을 돌면서 GPIO 5번 pin의 값을 5초 간격으로 1 -> 0 -> 1 ... 로 설정 변경해 준다. 그 결과 당연히 LED가 On -> Off -> On ... 을 반복하게 된다.

_________________________________________________
    ret = gpio_pin_configure(dev, PIN, GPIO_OUTPUT_ACTIVE | FLAGS);   ...... (A)
    if (ret < 0) {
        return;
    }

    while (1) {
        gpio_pin_set(dev, PIN, (int)led_is_on);    ...... (B)
        led_is_on = !led_is_on;
        k_msleep(SLEEP_TIME_MS);
    }
_________________________________________________

Zephyr는 Linux의 장점 중 하나인 device tree 개념을 그대로 채용하여, 많은 device를 효과적으로 기술하고 있다. 하지만 resource constrained라는 환경 탓에 Linux와는 달리 다소 복잡한 내부 구조를 가져갈 수 밖에 없게 된 듯 보인다.

어찌 되었든, 복잡한 내부 구조하고는 무관하게 개발자 입장에서는 몇가지 사실만 제대로 알고 있다면, zephyr 환경에서의 device driver 개발은 그리 어렵지 않을 것으로 보인다.


5. 아직 못다한 이야기
필자가 blog에 글을 올리는 이유는 모든 것을 다 알고 있는 상태에서 이를 남들에게 과시하기 위해서가 절대 아니다. 필자가 열심히 study한 내용을 글로 정리해 보는 것은 필자 자신에게 가장 큰 도움(정리를 하다 보면 아는 부분과 모르는 부분을 정확히 알 수 있게됨)이 되기 때문이다. 더불어 이렇게 어렵사리 정리한 글이 (도움의 손길이 필요한) 다른 이들에게 조금이나마 도움이 된다면 그 또한 나쁘지 않겠다는 생각에서 그렇게 하는 것이다. 갑자기 넉두리를 ㅎㅎ 😀

아직도 zephyr 관련하여 설명이 충분히 이루어지지 못한 부분(필자가 아직 제대로 파악하지 못한 부분)이 많이 남아 있다. 하지만 여기가 끝이 아니다. 오히려 이제 부터가 시작이다. Zephyr가 완벽히 이해될 때까지 계속 고민하고 또 고민할 것이다.

a) Kernel Services
RTOS kernel programming은 application 단의 thread programming(예: pthread)에 익숙한 분들에게는 그다지 어려운 내용이 아니다. 아래 page 내용을 참조하여 sample code를 분석해 보기 바란다.



b) 부팅 flow와 application main( ) 함수 호출 과정
Zephyr의 version이 올라가면서 많은 부분이 수정되었다. Kernel code도 예외는 아니었는데, zephyr의 부팅 코드 흐름을 다시 정리해 보면 다음과 같다. 끝 부분에 main( ) 함수 호출 부분이 보이는가 ? 이 곳에서 application에 있는 main( ) 함수가 호출되는 것이다. 참고로 application 입장에서는 zephyr kernel(zephyr/kernel 디렉토리 코드)은 라이브러리(libkernel.a)에 불과하다.


[그림 5.1] zephyr booting flow - arm32 기준

📌 예제 코드를 보다 보면, main() 함수가 없는 경우가 있다(그 대신에 thread를 생성하는 코드만 있음). 이 경에는 main thread(kernel code)는 실행을 종료하게 되며, application에서 생성한 thread만이 동작하게 된다.

c) 새로운 보드에 application 추가하기

...

d) CMake & Ninja

...

e) Device tree bindings
...

To be continued ...


6. References
[1] https://docs.zephyrproject.org/
[2] Zephyr Project: Unlocking Innovation with an Open Source RTOS, Kate Stewart, Linux Foundation
[3] https://www.st.com/en/evaluation-tools/nucleo-f103rb.html
[4] https://events.static.linuxfound.org/sites/events/files/slides/petazzoni-device-tree-dummies.pdf


Slowboot