ukasol

Blog Zephyr Serisi · 07/07

native_sim ile donanım olmadan firmware geliştirme

Kodu hedef için değil host için derliyor. native_sim nasıl çalışır, neleri emüle eder; GDB ve AddressSanitizer kart yokken hataları nasıl bulur.

Gömülü yazılım geliştirmenin en büyük handikabı şu: donanım elinizde yokken beklemek zorunda kalıyorsunuz.

native_sim tam olarak bu sorunu çözüyor. Birçok projede kullandım ve donanım hazır olmadan uygulama geliştirmeye başlayabilmek geliştirme sürecini ciddi ölçüde kısalttı.

Bu yazı onun gerçekte nasıl çalıştığını, neyi emüle edip neyi edemediğini ve kartlar geldikten sonra bile kullanmaya değer kılan iki aracı anlatıyor: gerçek bir debugger ve AddressSanitizer. Aşağıdaki her şey Zephyr 4.4.2 ile çalıştırıldı; örnek firmware-notes içindeki native-sim klasöründe.

Peki nasıl çalışıyor?#

native_sim, kodunuzu gömülü hedef için değil doğrudan host sisteminiz için derliyor. Yani ortaya çıkan çıktı bir .hex veya .bin değil, normal bir Linux executable’ı. Bunu çalıştırmak için ne flash cihazı ne de JTAG debugger gerekiyor.

$ west build -b native_sim
$ ./build/zephyr/zephyr.exe

Bu noktada Zephyr kernel tam anlamıyla çalışıyor. Thread scheduling, timer’lar, kernel primitive’leri, hepsi aktif.

Kaputun altında native_sim, kendi devicetree’si ve Kconfig’i olan, Zephyr’in POSIX mimarisi üzerine kurulu diğerleri gibi bir Zephyr kartı. Her Zephyr thread’inin arkasında bir host pthread’i var, ama aynı anda yalnızca biri çalışabiliyor, o da tam olarak Zephyr kernel’i öyle dediğinde. Gördüğünüz scheduling Linux’un değil Zephyr’in; davranışı tek çekirdekli bir mikrodenetleyiciyle örtüştüren de bu.

Zaman da simüle ediliyor ve insanları şaşırtan kısım bu. Bir uygulama varsayılan olarak gerçek zamana yavaşlatılıyor, böylece bir kartta nasıl davranacaksa öyle davranıyor. Bir test build’i ise yavaşlatılmıyor: CONFIG_TEST tanımlıyken Kconfig gerçek zamanı kapatıyor ve simüle edilen zaman host’un gidebildiği kadar hızlı ilerliyor. İkisini de çalışma anında seçebilirsiniz. İşte on kez birer saniye uyuyan küçük bir uygulama:

$ ./build/zephyr/zephyr.exe -stop_at=11
uptime: 10100 ms
$ ./build/zephyr/zephyr.exe -no-rt -stop_at=11
uptime: 10100 ms

İlk çalıştırma duvar saatiyle 11,0 saniye sürdü. İkincisi 0,004 saniye: dört milisaniyede on bir simüle saniye. Zaman aşımları ve uykularla dolu bir test suite’inin CI’da neredeyse anında bitmesinin sebebi bu. (Uptime 10.000 değil 10.100 ms, çünkü her bir saniyelik uyku bir tick fazla sürüyor: kernel, istenen süreden asla kısa olmaması için her göreli zaman aşımına bir tick ekliyor ve native_sim 100 Hz’de tick’liyor. Gerçek bir kart da aynı kuralı izliyor.)

Neleri emüle ediyor#

GPIO emülatörleri, I2C emülatörleri ve UART simülasyonu da mevcut, yani peripheral bağımlı kodları bile test edebiliyorsunuz. native_sim’in devicetree’sindeki liste çoğu kişinin beklediğinden uzun:

  • GPIO; bir testten girişleri sürmek ve çıkışları okumak için bir API’yla.
  • I2C ve SPI kontrolcüleri; cihaz emülatörleri bunlara bağlanıyor. Bir sensör emülatörü hat üzerinde gerçek parça nasıl cevap verecekse öyle cevap veriyor; böylece gerçek driver ona karşı çalışıyor.
  • ADC, DMA, RTC, bir sayaç ve bir rastgele sayı üreteci.
  • CAN; ya loopback olarak ya da gerçek bir Linux SocketCAN arayüzüne bağlı.
  • Bir flash simülatörü ve bir EEPROM simülatörü; depolama kodu ve ayarlar, bir “reboot” sonrasında olanlar dahil test edilebiliyor.
  • UART’lar; bir pseudo-terminal üzerinde ya da sürecin kendi stdin ve stdout’unda.
  • Bir ekran ve dokunmatik giriş; SDL üzerinden, masaüstünüzde bir pencerede.
  • Bluetooth; Linux’un HCI user channel’ı aracılığıyla host üzerindeki gerçek bir kontrolcü üzerinden.

Olmayan şey ise kartınızın gerçek çevre birimleri. İçeride bir STM32 timer’ı ya da bir nRF radyosu yok ve bu sınır önemli; aşağıda ona geleceğiz.

Önemli bir detay#

native_sim bir ISS (Instruction Set Simulator) değil, QEMU da değil. Kodunuzu ARM gibi bir mimariyi emüle ederek çalıştırmıyor. Doğrudan x86 için derleyip Linux üzerinde çalıştırıyor. Bu sayede GDB, Valgrind, AddressSanitizer gibi tüm native Linux araçlarını kullanabiliyorsunuz.

Donanım geldikten sonra native_sim’i iş akışınızda tutmanın asıl sebebi bu araçlar; o yüzden onları somut bir hata üzerinde görelim. Örnek, son dört örneğin hareketli ortalaması:

int32_t avg_add(struct avg *a, int32_t sample)
{
    if (a->count == AVG_WINDOW) {
        /* The window is full: the oldest sample leaves the sum. */
        a->sum -= a->samples[a->next];
    } else {
        a->count++;
    }

    a->samples[a->next] = sample;
    a->sum += sample;

    if (++a->next == AVG_WINDOW) {
        a->next = 0;
    }

    return a->sum / a->count;
}

Şimdi bu tür kodun genelde bozulduğu şekilde bozun: başa sarma kontrolündeki ==‘yi > yapın; böylece indeksin AVG_WINDOW‘a, yani dizinin sonundan bir sonrasına ulaşmasına izin verilmiş olur.

AddressSanitizer olmadan: bir belirti#

Test suite’i başarısız oluyor, ama yalnızca yanlış bir sayıyla:

    Assertion failed at WEST_TOPDIR/firmware-notes/native-sim/tests/average/src/main.c:33: average_test_window_slides: (avg_add(&a, 0) not equal to 0)

Bu size ortalamanın yanlış olduğunu söylüyor. Nedenini söylemiyor; gerçek bir kartta da bunun gibi sınır dışı bir okuma pekâlâ makul bir değer döndürüp geçebilir.

AddressSanitizer ile: neden#

Build’e iki satır ekleyin, CONFIG_ASAN=y ve CONFIG_UBSAN=y, ve aynı testi çalıştırın:

==196404==ERROR: AddressSanitizer: stack-buffer-overflow on address 0xef2b0038 ...
READ of size 4 at 0xef2b0038 thread T5
    #0 0x0804badc in avg_add native-sim/lib/average/average.c:21
    #1 0x0804ac5f in average_test_window_slides native-sim/tests/average/src/main.c:31
    ...
  This frame has 1 object(s):
    [32, 56) 'a' (line 23) <== Memory access at offset 56 overflows this variable

Hatanın türünü, tam satırı (a->sum -= a->samples[a->next], dizinin sonunun ötesini okuyor), onu çağıran testi ve taşan değişkeni söylüyor. Bir mikrodenetleyici üzerindeki hiçbir donanım debugger’ı bu kadarını bu kadar hızlı söylemiyor. Örnek repoda sanitizer build’i yalnızca ikinci bir Twister senaryosu:

tests:
  native_sim.average: {}
  native_sim.average.asan:
    extra_configs:
      - CONFIG_ASAN=y
      - CONFIG_UBSAN=y

Pratik bir not: Twister sanitizer raporunu, normalde açacağınız handler.log‘un yanındaki handler_stderr.log dosyasına yazıyor.

Gerçek bir debugger#

zephyr.exe normal bir Linux süreci olduğu için gdb onun üzerinde herhangi bir host programında çalıştığı gibi çalışıyor; firmware’inizin kendi durumuna bağlı koşullu breakpoint’ler de dahil. Yalnızca indeksin sınır dışına çıktığı anda durun:

$ gdb -q build/zephyr/zephyr.exe
(gdb) break average.c:21 if a->next == 4
(gdb) run
Thread 6 "test_window_sli" hit Breakpoint 1, avg_add (a=0xf7b4c254, sample=0) at native-sim/lib/average/average.c:21
21          a->sum -= a->samples[a->next];
(gdb) print *a
$1 = {sum = 400, next = 4 '\004', count = 4 '\004', samples = {100, 100, 100, 100}}

Thread’in adı bile testin adı. Bağlanacak bir prob yok, denemeler arasında flash’lama yok ve yeniden derlemek birkaç saniye sürüyor.

Alternatifler#

Bu arada alternatifler de var. QEMU belirli bir CPU mimarisini emüle ederek çalışıyor. Renode ise gerçek donanım modellemesi yapıyor. Her ikisi de farklı senaryolarda işe yarıyor, ama native_sim‘in hızı ve tooling kolaylığı geliştirme aşamasında ciddi bir avantaj sağlıyor.

Kabaca her biri farklı bir soruyu cevaplıyor:

native_sim QEMU Renode Gerçek kart
Ne çalışıyor Host için derlenmiş kodunuz Emüle edilmiş bir CPU üzerinde hedef binary Modellenmiş SoC ve çevre birimleri üzerinde hedef binary Hedef binary
Hız Host hızı, isterseniz gerçek zamandan hızlı Native’den yavaş Native’den yavaş Gerçek zaman
Debug Host GDB, Valgrind, sanitizer’lar Bir stub üzerinden GDB Bir stub üzerinden GDB ve modelin donanım görünümü JTAG ya da SWD
En iyi olduğu iş Mantık, protokoller, depolama, CI’da testler Mimariye özel kod ve boot Çok düğümlü sistemler, çevre birimi seviyesinde testler Zamanlama, güç, RF, gerçek olan her şey

Rakip değiller. Sağlıklı bir proje testlerinin çoğu için native_sim’i kullanıyor, diğerlerini de yalnızca onların gösterebileceği şeyler için saklıyor.

Nerede duruyor#

native_sim’i hızlı yapan tasarım, sınırlarını da belirliyor ve Zephyr dokümantasyonu bunlar konusunda açık.

CPU sonsuz hızlı. Kod çalışırken hiç simüle zaman geçmiyor ve kesmeler yalnızca CPU uyuduğunda geliyor. Bir kesmenin ya da başka bir thread’in bir değişkeni değiştirmesini bekleyen bir busy-wait döngüsü hiç bitmiyor; bütün simülasyonu kilitliyor. Döngü sayarak bekleyen kod da çalışmıyor. İkisi de zaten kaldırılmaya değer, ama native_sim’de kaldırmak zorundasınız.

Sizin silikonunuz değil. Register erişimi, inline assembly ve vendor HAL çağrıları çalışmıyor; ölçülecek bir clock ağacı, DMA zamanlaması ya da kesme gecikmesi yok. Donanım ile yazılım arasındaki yarışlar burada yeniden üretilemiyor.

Sizin derleyiciniz değil. Kod x86 için host’un GCC’siyle derleniyor. Tanımsız davranış farklı şekilde ortaya çıkabiliyor ve bir x86 host, Cortex-M0’ın hataya çevirdiği hizasız erişimleri tolere ediyor.

Çizgi basit: native_sim kodunuzun ne karar verdiğini test ediyor, silikonun nasıl davrandığını değil. Donanım erişimini driver’ların ve API’ların arkasında tutun; o çizginin üstündeki kısım, ki genelde firmware’in çoğu o, kart olmadan geliştirilip test edilebilir.

Pro tip#

native_sim varsayılan olarak 32-bit ABI ile derleniyor (ILP32), bu da çoğu gömülü hedefin pointer boyutunu taklit ediyor. Pointer veya long boyutuna bağlı bug’ları tespit etmek için bu çok önemli. 64-bit sistem kullanıyorsanız ve 32-bit binary çalıştırmakta sorun yaşıyorsanız native_sim/native/64 hedefini kullanabilirsiniz. 32-bit build, host’un 32-bit geliştirme kütüphanelerine (multilib paketleri) ihtiyaç duyuyor; native_sim/native/64 ise ek bir şey istemiyor.

Detaylar native_sim dokümantasyonunda.

native_sim ile Twister’ı birleştirince CI pipeline’ında donanım olmadan otomatik test koşturabiliyorsunuz.