ukasol

Blog Zephyr Serisi · 01/07

Zephyr başta çok karıştı

Hello World ilk gün çalıştı, sonrasında kayboldum. Zephyr neden yeni bir kütüphane değil, build main.c'nin arkasında neler saklıyor ve hangi sıra işe yarıyor.

8-9 yıldır gömülü yazılım geliştiriyorum. Zephyr’e ilk merak saldığımda çok karışık gelmişti ve anlamam epey zaman almıştı. (LLM’lerden önce 😅)

Getting Started sayfasını takip edip Hello World’ü çalıştırmak kolaydı ama ondan sonrası için hiçbir fikrim yoktu. Device Tree, Kconfig, driver yapısı, hepsi yeni ve karmaşık gelmişti.

Linux tarafından gelenler için bunlar tanıdık konseptler. Ama bare-metal ya da FreeRTOS ile gelen biri için tamamen yeni bir dünya.

Bu yazı, o zaman elimde olmasını isteyeceğim harita: Hello World neden yanıltıcı, build arka planda sessizce neler yapıyor ve Zephyr’in geri kalanı anlam kazanmadan önce düşünme biçiminde neyin değişmesi gerekiyor.

Hello World neredeyse her şeyi saklıyor#

Uygulamanın tamamı bu:

#include <stdio.h>

int main(void)
{
    printf("Hello World! %s\n", CONFIG_BOARD_TARGET);

    return 0;
}

Herhangi bir C ortamındaki Hello World’e benziyor, tuzak da tam burada. Bu tek satır bile henüz tanışmadığınız iki sisteme dayanıyor. CONFIG_BOARD_TARGET bir Kconfig sembolü; printf ise kartın devicetree’sinin konsol olarak gösterdiği UART’a gidiyor.

Bunu bir STM32 Nucleo kartı için derleyin ve west’in daha hiçbir şeyi derlemeden önce ne yazdığını okuyun. Kısaltılmış, yollar sadeleştirilmiş hâliyle:

$ west build -b nucleo_f401re samples/hello_world
-- Board: nucleo_f401re, qualifiers: stm32f401xe
-- Found BOARD.dts: zephyr/boards/st/nucleo_f401re/nucleo_f401re.dts
-- Generated zephyr.dts: build/zephyr/zephyr.dts
-- Generated devicetree_generated.h: build/zephyr/include/generated/zephyr/devicetree_generated.h
Parsing zephyr/Kconfig
Loaded configuration 'zephyr/boards/st/nucleo_f401re/nucleo_f401re_defconfig'
Merged configuration 'zephyr/samples/hello_world/prj.conf'
Configuration saved to 'build/zephyr/.config'
Kconfig header saved to 'build/zephyr/include/generated/zephyr/autoconf.h'

Daha tek bir C dosyası derlenmeden build kartın tarifini bulmuş, onu bir header’a çevirmiş, iki yapılandırma dosyasını birleştirmiş ve sonucu başka bir header’a dönüştürmüş. Bu dört satırlık program için üretilen devicetree header’ında 14.283 tane DT_ makrosu var; nihai yapılandırma 1.249 satır.

Sonra derleme başlıyor ve üzerinde durmaya değer satır şu:

[13/157] Building C object CMakeFiles/app.dir/src/main.c.obj

Sizin main.c‘niz 157 adımın 13.’sü. Build bu örnek için 115 C dosyası derliyor ve bunlardan yalnızca biri sizin. Geri kalan 114’ü kernel, driver’lar, C kütüphanesi ve başlangıç kodu; hepsi yukarıdaki sistemler tarafından bu kart için seçilmiş ve yapılandırılmış. Getting Started sayfası size yalan söylemiyor; sadece bunların hiçbirinden bahsetmiyor, çünkü Hello World bunları bilmenize gerek kalmadan çalışıyor.

Beş şey aynı anda geliyor#

O build çıktısının arkasında beş parça var ve her biri, bare-metal geliştiricinin zaten elle yaptığı bir şeyin yerini alıyor:

Zephyr’de Ne yapıyor Bare-metal’de neyin yerini alıyor
west Zephyr’i ve modüllerini sabit sürümlerde indiriyor, build’i yönetiyor Vendor SDK’sını repoya kopyalamayı
CMake build sistemi Kernel’i, driver’ları ve sizin kodunuzu tek bir imajda derliyor Makefile’ınızı ya da IDE projenizi
Device Tree Kartı tarif ediyor: hangi çevre birimleri var, hangi pinleri kullanıyorlar, onlara ne bağlı #define LED_PIN 5 dolu bir board.h‘yi
Kconfig Hangi kodun derleneceğine ve nasıl ayarlanacağına karar veriyor config.h‘yi ya da FreeRTOSConfig.h‘yi
Driver modeli Her cihaz sınıfı için tek bir API; driver’ı devicetree seçiyor Doğrudan vendor HAL’ini çağırmayı

Bunların hiçbiri tek başına zor değil. Sorun şu: Hello World beşini birden kullanıyor ve her birinin dokümantasyonu diğer dördünü zaten bildiğinizi varsayıyor. Hello World’den sonraki adımın duvar gibi gelmesinin sebebi bu.

Önce zihniyeti değiştirmek gerekiyor#

Sonunda şunu fark ettim: Zephyr öğrenmek yeni bir kütüphane öğrenmekten farklı. Önce zihniyeti değiştirmek gerekiyor.

Bare-metal’de donanımın sahibi sizin kodunuz. Aynı Nucleo kartında, STM32 register’larıyla yazılmış yanıp sönen bir LED şöyle görünüyor:

#include "stm32f401xe.h"

int main(void)
{
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;   /* clock GPIO port A */
    GPIOA->MODER |= GPIO_MODER_MODE5_0;    /* PA5 as an output */

    for (;;) {
        GPIOA->ODR ^= GPIO_ODR_OD5;    /* toggle the LED on PA5 */
        for (volatile int i = 0; i < 400000; i++) {
        }
    }
}

Her satır donanım hakkında bir şey biliyor: portu, pini, clock kapısını, bu çip ailesinin register düzenini. Başka bir mikrodenetleyiciye geçmek hepsini yeniden yazmak demek.

Zephyr bu bilgiyi üçe bölüyor. Devicetree donanımı tarif ediyor, Kconfig hangi kodun var olacağını yapılandırıyor, sizin kodunuz ise yalnızca bir API’yı kullanıyor. Zephyr’in blinky örneğinin uygulama tarafı özünde şuna iniyor:

static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios);

/* in main(): */
gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
gpio_pin_toggle_dt(&led);

Pin kodun içinde değil. Kartın devicetree’sinden geliyor; Nucleo’da şöyle yazıyor:

green_led_2: led_2 {
    gpios = <&gpioa 5 GPIO_ACTIVE_HIGH>;
    label = "User LD2";
};

Nordic’in nRF52840 DK’sında ise şöyle:

led0: led_0 {
    gpios = <&gpio0 13 GPIO_ACTIVE_LOW>;
    label = "Green LED 0";
};

İki kart da led0 takma adını kendi LED’ine bağlıyor ve aynı main.c ikisi için de derleniyor, ikisinde de LED’i yakıp söndürüyor. Dikkat edin, ikinci LED active low: nRF52840 DK LED’i yakmak için pini sıfıra çekiyor. C kodu bununla ilgilenmiyor, çünkü polarite devicetree’de pinle birlikte geliyor. O tek detay, zihniyetin küçük bir modeli. Donanımı bilen kod yazmayı bırakıp donanımı devicetree’ye soran kod yazmaya başlıyorsunuz.

Başta gereksiz bir dolaylılık gibi geliyor. Bu his, bir kart revizyonunda LED başka bir pine taşındığında ya da tasarıma koyduğunuz parça bulunamaz hale geldiğinde ve çözüm uygulamanın içinde arama yapmak yerine birkaç satır devicetree olduğunda geçiyor.

İşe yarayan bir sıra#

Parçaları tek tek ele almak çok daha kolay, hem de her birinin yalnızca öncekilere dayandığı bir sırayla:

  1. Build çıktısını okuyun. Henüz anlamanız gerekmiyor; bu parçaların var olduğunu ve hangi sırayla çalıştıklarını bilmeniz yeterli.
  2. Device Tree: donanım ne.
  3. Kconfig: hangi kod, hangi seçeneklerle derleniyor.
  4. Driver modeli: GPIO gibi tek bir cihaz sınıfı seçin ve gpio_dt_spec‘in bir devicetree node’unu API’ya nasıl bağladığını takip edin.
  5. native_sim: her zaman elinizde olan bir kart; hiçbir şey flash’lamadan deneme yapabilirsiniz.

Serinin geri kalanı da aynı sırayı izliyor.

Herhangi bir tutorial’dan daha çok işe yarayan bir alışkanlık var: her build’den sonra, üretilen iki dosya çoğu sorunun cevabını veriyor. Bunlar build/zephyr/zephyr.dts, yani bütün overlay’ler uygulanmış nihai devicetree, ve build/zephyr/.config, yani nihai yapılandırma. Build beklemediğiniz bir şey yaptığında cevap neredeyse her zaman bu ikisinden birinde.

Aynı şeyleri yaşıyorsan normal, sadece devam et.

Sıradaki · Zephyr fundamentals Bireysel olarak neden Zephyr'e yatırım yapmalı