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:
- 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.
- Device Tree: donanım ne.
- Kconfig: hangi kod, hangi seçeneklerle derleniyor.
- 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. - 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.