Daha önceden Raspberry PI üzerinde Python programlama dilini kullanarak tekerlek döndürmüş birkaç Led yakmıştım ancak Python ve kütüphanelerinin sunduğu soyutlamalar ve RasPi'nin yetenekleri işi epeyce kolaylaştırmıştı (Üzerinde Linux tabanlı işletim sistemi koşabiliyordu, elimdeki Raspi'de 4Gb Ram vardı, kitap vs kaynak da oldukça fazlaydı) Diğer yandan çok uzun zamandır Rust ile kodlama yapıyorum ve onu asıl sahasında deneyimlemeye çalışıyorum. Şu anda eksik kaldığımı hissettiğimi önemli bir kısım var; Bare Metal Programming. Kabaca, işletim sistemi olmayan ortamlarda sadece core kütüphaneyi kullanarak geliştirme yapmak olarak özetleyebilirim. Bu felsefe ile bir işletim sistemi yazabilir ya da buna uygun çeşitli mikrodenetleyiciler (Microcontroller) üzerinde geliştirmeler yapılabilir. Bu cihazları hedefleyen strateji daha çok Embedded Programming olarak da geçiyor. Bu özet yazıda kısa süre önce tedarik ettiğim BBC Micro:bit kartı üzerindeki tecrübelerimi paylaşmaya çalışacağım.
Internet dünyasında gömülü sistemlerde Rust ile kodlama için sınırsız kaynak var ve hatta birincil kaynak olarak The Embedded Rustacean sitesini tavsiye etmek isterim. Haftalık bir bültenleri var ve oldukça sıkı makalelere yer veriyorlar. Lakin derli toplu ve kısa yoldan bir giriş yapmak isteyenler için bana göre birincil kaynak Rust Embedded organizasyonun şu adresteki ücretsiz keşif kitabı. Bende bu kitabı baz alarak ilermeye çalıştım. Saf zihnim ilk etapta bir emulator üzerinden hareket edebilirim yönündeydi. Hatta bu konuda oldukça güzel bir çevrimiçi simülator siteside bulunuyor. En azından neler yapılabiliyoru görmek açısından faydalı olabileceğini düşünüyorum. Takip etmekde olduğum Discovery kitabı konuyu BBC micro:bit üzerinden ele almakta. Bende yakın zamanda heyecanla bu karttan bir tane aldım. ARM tabanlı bu mikrodenetleyici için iki fotoğrafı da şöyle bırakayım.
ve
Bu ufacık kart öğrendiğim kadarıyla STEM müfredatında 7-14 yaş arası çocukların eğitimlerinde de kullanılıyor. Kart üzerinde Pyhton, Scratch ve Microsoft MakeCode ile geliştirme yapılabiliyor. Ben hiçbir ekstra sensör veya genişletme yuvası almadım. Buna rağmen kartın üstünde programlanabilir led'ler (ki ilk açıldğında kırmızı bir kalp şeklini aldılar), yerleşik bir hoparlör (ki açıldığında sevimli bir ikaz sesi çıkarıp selam veriyor), iki düğme, bluetooth, hareket sensörü vs yer alıyor. Benim tedarik ettim v2.2 modelinde 512 Kb Flash ve 128 Kb RAM var. nRF52833 kodlu işlemci ise Nordic Semiconductor üretimi. Bilgisayara kolayca USB kablosu ile bağlanabiliyor ve hatta harici güç üniteleri ile seyyar hale de gelebiliyor. Normalde üzerinde bir işletim sistemi bulunmuyor dolayısıyla yazılan kodu Flash belleğe aldığımız andan itibaren cihaz üzerinde çalışmaya başlıyor (Karşılaştığınızda niye sonsuz bir loop var şaşırmayın)
En çok zorlandığım kısım cihaza uygun kod geliştirmek, yazılımı taşımak ve hata ayıklamak için gerekli ortam araçlarını kurmak oldu. Öncelikle rust ile yazılan çıktının ARM tabanlı bu işlemci modeli için build edilmesi, cihaz üzerine bir şekilde aktarılması gerekiyor. Ayrıca yer yer debug etmek de gerekebilir ki bunun için de bazı araçlara ve kurulumlara ihtiyaç var. Burada tavsiyem kitaptaki ilgili bölümü harfiyen takip edip kendi ortamınız için gerekli kurulumları yapmanız olacak. Neler neler çektim bi bilseniz :D Ancak buraya kısa birkaç not bırakabiliriz de.
# Sistemde rust'ın yüklü olduğunu düşünüyorum tabii
# Tooling
rustup component add llvm-tools
cargo install cargo-binutils
cargo install cargo-embed
# Micro:bit v2.2 sürümü için gerekli target enstrümanlarını ekleyelim
rustup target add thumbv7em-none-eabihfİlk uygulamada amacım kartın arkasında yer alan led ışıklardan herhangi birisinin alarm ikazı gibi yanım sönmesini sağlamaktı. Her şeyden önce normal bir rust projesi oluşturarak işe başlanabilir.
cargo new micro-lightsHer ne kadar standart kütüphane olmadan geliştirme yapsak da bize yardımcı olacak bazı crate' ler de mevcut. Bu amaçla Cargo.toml dosyasını aşağıdaki gibi düzenleyebiliriz. Genelde yararlandığım kaynaklardaki örnekler rust 2018 sürümü ve eski crate'lere bağlı olduklarından 2024 sürümü için biraz araştırma yapmam gerekti diyebilirim.
[package]
name = "micro-lights"
description = "A simple hello world app for micro:bit v2.2."
authors = ["Burak Selim Şenyurt"]
version = "0.1.0"
edition = "2024"
[dependencies.microbit-v2]
version = "0.15.1"
optional = true
[dependencies.microbit]
version = "0.15.1"
optional = true
[dependencies]
cortex-m = "0.7.7"
cortex-m-rt = "0.7.5"
embedded-hal = "1.0.0"
panic-halt = "1.0.0"
[features]
v2 = ["microbit-v2"]
v1 = ["microbit"]cortex-m küfesini mikrodenetleyicinin işlemcisi ile low-level iletişim kurmak için, cortex-m-rt'yi runtime ortamı için, embedded-hal'ı donanım unsurlarına (led'ler, GPIO'lar gibi) ulaşmayı sağlamak için, panic-halt'ı ise panic implementasyonunu kolaylaştırmak için kullanıyoruz. Bu arada HAL (Hardware Abstraction Layer) olarak geçen genel bir kavram var ve gömülü sistemlerde donanımla haberleşmeyi kolaylaştıran soyutlamaları ifade ediyor diyebiliriz. Burada kullanılan embedded-hal işimizi önemli ölçüde kolaylaştırmakta. Normalde donanım ile doğrudan konuşmamız da mümkün. Microcontroller Unit ile ya da peripherals'e ulaşıp iletişim kurmamızı sağlayan paketler de bulunuyor. Bu benim için biraz daha zorlayıcı zira cihaz üzerindeki örneğin GPIO'ların register adreslerini bilmeyi bazen bit kaydırma işlemi yapmayı ve hatta unsafe kod blokları ile çalışmayı gerektiriyor. İlerleyen kısımlarda bununla ilgili bir örnekte bulabilirsiniz.
İşletim sistemi olmayan ortamlarda kodlama yaparken NoStd ve NoMain direktiflerine sıkça rastlıyoruz. Özellikle NoStd ile standart kütüphaneyi terk etmiş oluyoruz. Burada karşımıza çıkan sorunlardan birisi panic macro implementasyonu. Normalde bunu aşağıdaki gibi bir fonksiyon ile koda eklemek gerekiyor.
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}Kaynaklara göre bunun yerine panic-halt kütüphanesi de kullanılabilir. No Standard kullanımı az önce de belirttiğim gibi rust std kütüphanesini devre dışı bırakmak demektir. Buna göre I/O, heap, panic, thread, fs gibi işletim sistemi bazlı çalışan birçok modül ve fonksiyonellik devre dışı kalır. Bunlar yerine elimizde core ve alloc gibi daha sınırlı kütüphaneler kalır. panic enstrümanı konsola hata mesajı basılması, stack trace gösterilmesi ve programın anında durdurulması gibi işleri üstlenen bir enstrümandır. Artık elimizde böyle bir imkan olmayacağından derleyici bu gibi hata durumlarında ele alacağı bir [panic_handler] tanımlaması arar. Yukarıda bunu ele alan kod parçası esasında hata oluştuğunda sadece sonsuz bir döngü işletmekte ki bu en basit çözüm (Şşşştt kimseye söyleme der gibi) Hataları ele alma şekli değişkenlik gösterebilir. Bir mikrodenetleyici var sonuçta ve demem o ki hata oluştuğunda kırmızı led bile yaktırılabilir. Ancak her farklı durum için böyle handler kodları tariflemek zahmetli olur. Bunu daha sade şekilde ele almanın yolu da panic-halt küfesini kullanmaktır (Tam bir reklam oldu ama bazı soyutlamalar olmadan olmuyor işte)
Toml dosyasında dikkat çekici noktalardan bir diğeri de iki versiyon için microbit tanımlanması içermesidir. Kullanmakta olduğum BBC Micro:bit' in yazıyı hazırladığım tarih itibariyle iki ana versiyonu bulunuyor. Yazdığımız kodu hangisine göre derlemek istiyorsak seçim yapmamızı kolaylaştırması için feature bildirimleri kullanmaktayız. Şimdi biraz da kod tarafına geçelim. İlk örnek kodlar için çok uzağa gitmedim ve github kaynaklarında genel olarak verilen örneği ele almaya çalıştım.
#![deny(unsafe_code)]
#![no_main] // klasik rust giriş fonksiyonuna bağlı çalışmayacak
#![no_std] // rust standart kütüphanesi kullanılmayacak
use cortex_m_rt::entry;
use embedded_hal::delay::DelayNs;
use embedded_hal::digital::OutputPin;
use microbit::{board::Board, hal::timer::Timer};
use panic_halt as _;
#[entry] // entry point olduğunu belirtir
fn start() -> ! { // ! dönüyor. Bu fonksiyonun hiç sonlanmayacağını belirtmekte
let mut board = Board::take().unwrap(); // Board'un sahipliğini alıyoruz
let _ = board.display_pins.col3.set_low();
let mut row3 = board.display_pins.row3;
let mut timer = Timer::new(board.TIMER0);
loop {
let _ = row3.set_low(); // Işığı kapatıyor
timer.delay_ms(1_500); // 1.5 saniye gecikme
let _ = row3.set_high(); // Işığı açıyor
timer.delay_ms(1_500); // 1.5 saniye gecikme
}
}Genel rust programlama pratiklerine göre biraz farklı bir kurgu olduğunu ifade edebilirim. start fonksiyonu programın çalışmaya başladığı yer. Mikrodenetleyici üzerinde bir işletim sistemi bulunmadığından ve doğal olarak rust'ın klasik main fonksiyonunu yürütecek çalışma zamanı olmayacağından farklı bir yöntemle gidiliyor diye düşünebiliriz (Bunu biraz daha derinlemesine araştırmam lazım) Bana göre no_main ve no_std direktifleriyle rust derleyicisinin beklediği main fonksiyonunun olmadığını ve standart kütüphaneye bağlamadan (linkleme) derlemenin yapılması gerektiğini belirtiyoruz. Tabii programın bir yerden çalışmaya başlayacağının da belirtilmesi lazım. Bu, [entry] direktifinin görevi. start fonksiyonu geriye ! dönüyor gibi duruyor ancak bunun anlamı fonksiyonun hiç sonlanmayacak olması.
Diğer yandan kodda gördüğünüz üzere koşulsuz çalışan sonsuz bir loop var. Bu döngüde 5x5 matris şeklindeki dizilime göre tam ortadaki LED ışığı ile ilgili bir işlem yapıyoruz. Bir LED ışığı kapatıp açmak demek o bölgedeki GPIO pin'leri iki noktadan ayarlamakla mümkün oluyor. Dikkat ederseniz döngüden önce col3 isimli pin'i low konumuna çeiyoruz. Aynı konuma gelen row pin değerini ise döngü sırasında önce low pozisyonuna sonra da high pozisyonuna çekiyoruz ve aralarda 1.5 saniyelik bekletmeler yapıyoruz. Bunun sonucu ışığın yanıp sönmesi oluyor. (Şu anda tam ortadaki LED yanıp sönmeye devam ediyor. Aradan dakikalar geçti. Kastettiğim bu) Sonuçta elimizde bir devre kartı var ve üzerinde sunulan led'lere erişmek istiyoruz hatta tam ortadakini yakıp söndüreceğiz ve bunu devamlı yapacağız. Bununla ilgili soyutlamaları kullanıyoruz. Board'un sahipliğini alıp onun üzerinden satır ve sütun yönlendirmeleri ile led'lere ulaşıp set_low ve set_hihg çağrıları ile gerekli yakıp söndürme işlemlerini icra ediyoruz.
Micro:bit kartının şu adreste oldukça detaylı bir devre şeması yer alıyor. Örnek kodda 3ncü satır ve sütundaki LED yakılıyor. Nasıl bir LED matrisi ile karşı karşıya olduğumuzu aşağıdaki grafikten de görebilirsiniz.
Dolayısıyla microbit küfesi üzerinden erişebildiğimiz birçok soyutlama bu şemada belirtilen adreslere erişmemizi kolaylaştırmakta.
Kodları bu şekilde yazmak onu çalıştırmak için yeterli değil ne yazık ki. Her şeyden önce normal bir cargo build operasyonu işimizi görmeyecek. Bu koddan ARM tabanlı mikro denetliyici için bir çıktı üretilmesi gerekiyor. Araştırdığım örnekler bunun için genel olrak şöyle bir yol izlemekte.
- src klasörü altında .cargo/config.toml dosyası
- root klasörde Embed.toml dosyası
- yine root klasörde memory.x dosyası (Bazen de link.x dosyası gerekiyor)
.cargo klasöründe yer alan bu konfigurasyon dosyası aslında cargo aracının build, run ve check komutları çalıştırıldığında sürece özel ayarlar eklemel için kullanılmakta. Örneğin aşağıdaki içeriğe göre eğer derleme işlemi ARM tabanlı ve işletim sistemi olmayan (bare metal) bir hedef için yapılıyorsa derleyiciye -C link-arg=-TLink.x şeklinde bir parametre daha ekleniyor. TLink.x genelde içinde memory.x'i tarifleyen bir dosya olarak belirtilmekte ancak bizim projemizde yer almıyor zira kullandığımız cortex-m-rt crate'i bunu kendi içerisinde belirtmekte.
İlerleyen kısımlarda belirteceğiz ama şimdiden bahsetmekte yarar var. cargo embed işleminde özel bir target kullanacağız (thumbv7em-none-eabihf) ve bu aşağıdaki dosyada belirtilen parametrelerin devreye girmesine ve memory.x içerisindeki bellek talimatlarına göre hareket edilmesine neden olacak. (Aslında sadece bizimki gibi tek bir mikrodenetleyici ile çalışacaksak bu bilgiyi Embed.toml dosyasında belirtip terminaldeki komut parametrelerini azaltabiliriz. Terminal Hi! yazdırdığımız örnek projeyi bu açıdan inceleyebilirsiniz)
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
rustflags = [
"-C", "link-arg=-Tlink.x",
]Gömülü sistemlerde geliştirme yapılırken özellikle debug işlemleri için probe-rs isimli bir toolkit kullanılıyor. Bu araç setindeki unsurlar genellikle Embed.toml içeriğini baz alıyor. Söz konusu dosya içeriği genelde aşağıdaki gibi ve debug, reset, flash işlemleri için ortama bilgi sağlıyor.
[default.general]
chip = "nrf52833_xxAA" # uncomment this line for micro:bit V2
# chip = "nrf51822_xxAA" # uncomment this line for micro:bit V1
[default.reset]
halt_afterwards = true
[default.rtt]
enabled = false
[default.gdb]
enabled = falseBen v2 sürümünü kullandığım için ilk satırı etkinleştirdim. Buna göre probe-rs aracı belirtilen çipi hedefleyecek ki memory haritasının doğru ayarlanması için bu önemli. Reset, Rtt ve Gdb ise birer flag. Reset flag'i kod yüklendikten sonra cihazın sıfırlanıp sıfırlanmayacağını (reset) belirtir. true verdiğimiz için Flash işlemi bitince cihaz resetlenir ve doğal olarak program hemen çalışmaya başlamaz. Program start metodumuzda durur. Bu genellikle debug işlemi yapmak istiyorsak o anda bir breakpoint eklememiz için idealdir. False olarak belirlendiğindeyse program hemen çalışmaya başlar ki bu durumda debug yapamayabiliriz. RTT (Real-Time Transfer) flag'i true olduğunda seri port yerine debug kanalı üzerinden loglama gibi işlemler yapılabilir. rtt-target gibi bazı küfeler bu kanalı kullanarak loglama yapabilir örneğin. Kodun ilerleyen kısımlarında bu ayarı deneriz. GDB flag true olduğunda program bir debug sunucusu ile birlikte başlar. Bu durumda başka bir terminalden bir istemci açıp debug sunucusuna bağlanılabilir. Dahası çalışan uygulama buraya attach edilerek kod debug edilebilir. Bu, VS Code gibi araçlarda debug yapacaksak kıymetli bir özellik olabilir.
Bu dosyasının içeriği yine internetteki örneklerde aşağıdakine benzer şekilde oluşturulmuş. En yalın halini kullandığımızı belirteyim. Bendeki versiyon 512K FLASH ve 128K RAM kullandığından değerleri buna göre ayarladım. Bu dosya linker'a mikrodenetleyicinin bellek yapısı ile ilgili bilgi vermek için kullanımakta. FLASH başlangıç adresi neresi ne kadar yer kaplıyor hangi adresten başlıyor vb. Mikrodenetleyici üzerinde bir işletim sistemi olmadığından bellek haritası linker için önemlidir. cargo embed ile bir derleme başlatıldığında programın hangi bölümünün nereye yerleştirileceğine karar verilmesinde rol oynayan önemli bir dosyadır.
MEMORY
{
/* NOTE K = KiBi = 1024 bytes */
FLASH : ORIGIN = 0x00000000, LENGTH = 512K
RAM : ORIGIN = 0x20000000, LENGTH = 128K
}
Not: Farklı kaynaklarda buradaki birtakım ayarların direkt toml dosyaları içerisinden yönetimi de söz konusu. Örneğin kimi kaynak cargo embed arkasından gelen feature gibi özellikleri toml dosyası içerisine alıyor.
Yukarıdaki ayarlamalar yeterli değil. Ayrıca rust derleyicisinin bu mikrodenetleyici için çıktı üretmesini sağlayacak enstrümanın yüklenmesi de gerekiyor. Sonuçta farklı bir mikrodenetleyici mimarisi söz konusu. Kullandığım BBC micro:bit V2.2 denetleyicisi üzerinde Cortex-M4F işlemcisi yer alıyor ki bu, rust dünyasında thumbv7em-none-eabihf olarak belirtilmiş. Bunu target parametresi ile kullanılabilecek şekilde yüklemek için aşağıdaki terminal komutunu çalıştırmak yeterli.
rustup target add thumbv7em-none-eabihf
# Bu arada sistemde yüklü target enstrümanlarını görmek için aşağıdaki komut kullanılabilir
# Sistemde yüklü olanlar, yanında installed yazılı olanlardır
rustup target listVeya şu anda sistemde nelerin kurulu olduğunu görmek için show komutu da kullanılabilir.
rustup showPeki onca target içerisinden hangisini kullanacağımızı nasıl bileceğiz? Bu örnek özelinde ilerleyelim. Öncelikle Micro:bit resmi kaynaklarından hangi model ile çalıştığımıza bakmamız gerekiyor. Örneğin benim çalıştığım model Nordic nRF52833 (ki bu isimle hem Hardware Abstraction Layer hem Peripheral Access Crate türünden küfeler var) ve çekirdek bilgisi de Arm Cortex-M4 32 bit processor with FPU şeklinde. Buradan hareketle ARM Developer sitesine bakıp Cortex-M4'ün mimari ve ISA bilgilerini not ediyoruz. Örneklerdeki model için Armv7E-M mimarisi kullanılıyor ve ISA desteği de Thumb / Thumb-2 şeklinde belirtilmiş. Şimdi buraya kadar elde ettiğimiz anahtar kelimeler Arm Cortex-M4 FPU, Armv7E-M, Thumb/Thumb-2 şeklinde. Bu isimlendirmeler hedef platform için önemli. Şimdi şu adresten rust'ın desteklediği platformlara baktığımız şu iki isim karşımıza çıkıyor.
- thumbv7em-none-eabi Bare Armv7E-M
- thumbv7em-none-eabihf Bare Armv7E-M, hardfloat
Dikkat edileceği üzer thumb ve v7em ifadeleri var. İşletim sistemi olmayan bir cihaza çıktı alacağımızdan none bilgisi de yer alıyor. Kaynaklardan öğrendiğim kadarı ile burada aşağıdaki gibi bir söz dizimi ele alınmakta.
<arc><sub>-<vendor>-<sys>-<env>
Örneği çalıştırmak için aşağıdaki komutu vermemiz yeterli. Dikkat edileceği üzere cargo run yerine cargo embed şeklinde bir kullanım söz konusu zira cargo run gömülü sistemlerde çalışmaz. Unutmayalım ki bilgisayarımızda yazdığımız kodu, işletim sistemi olmayan bir mikro denetleyicinin sabit belleğine gönderen ve başlatan bir düzenek gerekiyor.
cargo embed --features v2 --target thumbv7em-none-eabihfŞimdi bu komut üzerine birşeyler söylemek lazım. Öncelikle bir gömülü sistem söz konusu ise bir ELF (Executable and Linkable Format) dosyasının üretilmesi ve mikrodenetleyiciye yüklenmesi (Flashing olarak geçiyor) gerekiyor. Hatta bu işlem, programı cihazın kalıcı belleğine yüklemek olarak da düşünülebilir. Tabii bazı durumlarda bu ELF dosyasının resetlenmesi veya debug için duraklatılarak kullanılması da gerekiyor. Bunlar tabii bir dizi işlem demek. cargo embed komutu bunun için probe-rs altyapısını kullanmakta ki yazımızın başında referans ettiğimiz doküman tüm bu araçları yüklemeyi anlatıyor. Kısacası cargo embed, gerekli derleme işlemini yapar ve oluşan elf dosyasını bulup Embed.toml daki talimatlara göre yükler (flash) diyebiliriz. Komutta --features v2 kullandığımız için cargo.toml' da belirtilen microbit-v2 sürümü baz alınır. --target 'a göre de az önce yüklediğimiz enstrüman için bir çıktı üretilir. thumbv7em ifadesi ARM Cortex-M4 işlemcisini, none kelimesi işletim sistemi olmadığını, eabihf ise Hard Float özelliği olduğunu (Floating point'ler ile çalışıyorsak daha hızlı çalışma anlamına geliyor) belirtir.
Örneği çalıştırdığımda beklediğim gibi (ve de sürpriz bir şekilde) tam ortadaki LED ışığı yanıp sönmeye başladı. Zira akıllarda harika bir soru var; Nasıl duracak? :D Üstelik cihazın gücünü kessem bile tekrar bağladığımda ışık yanıp sönmeye devam etti. Aslında bu son derece doğal zira cargo embed operasyonu bu programı cihazın Flash belleğine yazar. Klasik bir işletim sisteminde bunun yerine program hard disk gibi bir fiziki depolama alanında durur ve işletim sistemi tarafından kontrol edilir. Program başlatıldığında işletim sistemi tarafında RAM'e alınır. Kapandığındaysa...Kapanır ha ha :D Şu anki senaryomuz ise farklı. Flash belleğe yazdığımız program kalıcı olarak orada duruyor. Öğrendiğim kadarı ile mikrodenetleyicilerde kod genellikle Flash bellekte saklanıyor (Bazen Read Only Memory-ROM diye bahsedildiğini de gördüm) Cihazı reset etsek bile cihazın reset vektörü [entry] bildirimi ile işaret edilen giriş fonksiyonu (Bizim örnekt start) bulur ve hemen çalıştırır. Programı resetlemek için uygulanabilecek birkaç taktik var. Örneğin bunlardan birisi program kodunu aşağıdaki gibi değişitirip yüklemek. Tam bir hile :D
#[entry]
fn start() -> ! {
loop {
}
}Bunun dışında cihaz üzerinde gelen düğme kontrolleri ile farklı bir yola da gidilebilir. LED ışığın yanıp söndürülmesi düğmeye basılmasına bağlanabilir. Mikdodenetleyicilerde genelde tek bir program çalıştırıldığını ifade etmekte yarar var. O nedenle Flash edilen program kısıtlı bellek bölgesinde her daim çalışmak üzere tasarlanan bir iş modeline sahip olmakta. Söz gelimi sensörlerinden gelen veriyi değerlendirip uyarı ikazı vermek veya anlık durumu panellerde göstermek gibi. Bunlar sürekli çalışır halde olmayı gerektirir.
Şimdilik ışık yakmaya devam edelim derim :D
Bu seferki yaklaşımda doğrudan donanım seviyesine inip GPIO (General Purpose Input Output) register adreslerini kullanarak ortadaki LED ışığını yanıp söndürmeye çalışacağız. Çalışmakta olduğumuz BBC Micro:bit kartındaki LED matrisi 5x5 boyutlarında. Elektronik ile çok aram yoktur ancak öğrendiğim kadarı ile bir LED'i yakmak için bu LED'e giden ROW pinini LOW, yine bu LED'e giden COLUMN pinini HIGH yapmak gerekiyor (Ya da tam tersi de geçerli olabilir) Tabii burada ilgili pinlerin register adreslerine nasıl gideceğimiz de bir soru işareti. Kaynaklarda dolanıp durdum ama anladığım kadarı ile örneğin 21nci pinin adresi aşağıdaki formmülle ifade edilebiliyor (Aslında kartların üretici kitapçıklarında bu adres bilgilerine detaylıca yer verilmekte)
0x5000_0700 + 21 * 4 = 0x5000_0700 + 0x54 = 0x5000_0754
PIN register adreslerini bulmakta yeterli değil. Işıkların yanması için PIN yönünün output yapılması ve OUTCLR/OUTSET gibi registerlarından yararlanarak çıkış seviyelerinin ayarlanması da gerekiyor. Neyse ki The Rusty Bits kanalında yayınlanan şu video bu konularda önemli ayrıntılar veriyor. En nihayetinde aşağıdaki gibi bir kodlama ile ışık yanıp söndürülebilir.
#![no_std]
#![no_main]
use core::ptr::write_volatile;
use cortex_m::asm::nop;
use cortex_m_rt::entry;
use panic_halt as _;
#[entry]
fn start() -> ! {
const GPIO0_BASE: u32 = 0x5000_0000;
const PIN_CNF_OFFSET: u32 = 0x700;
const P0_21: usize = 21;
const P0_28: usize = 28;
const GPIO0_PIN_CNF_21_ROW_1_ADDR: *mut u32 =
(GPIO0_BASE + PIN_CNF_OFFSET + (P0_21 * 4) as u32) as *mut u32;
const GPIO0_PIN_CNF_28_COL_1_ADDR: *mut u32 =
(GPIO0_BASE + PIN_CNF_OFFSET + (P0_28 * 4) as u32) as *mut u32;
const DIRECTION_OUTPUT_POS: u32 = 0;
const PIN_CNF_DRIVE_LED: u32 = 1 << DIRECTION_OUTPUT_POS;
unsafe {
write_volatile(GPIO0_PIN_CNF_21_ROW_1_ADDR, PIN_CNF_DRIVE_LED);
write_volatile(GPIO0_PIN_CNF_28_COL_1_ADDR, PIN_CNF_DRIVE_LED);
}
const GPIO0_OUTPUT_ADDRESS: *mut u32 = (GPIO0_BASE + 4) as *mut u32;
const GPIO0_OUTPUT_ROW_1_POS: u32 = 21;
let mut light_is_on: bool = false;
loop {
unsafe {
write_volatile(
GPIO0_OUTPUT_ADDRESS,
(light_is_on as u32) << GPIO0_OUTPUT_ROW_1_POS,
);
for _ in 0..400_000 {
nop();
}
light_is_on = !light_is_on;
}
}
}İlk önce row ve column PIN adresleri hesaplanıp birer Constant olarak tutulmakta. GPIO başlangıç adresine offset değerini ekledikten sonra kaçıncı PIN'e ulaşmak istiyorsak 4 ile çarpıyoruz. Ancak bu formül cihazdan cihaza değişiklik gösterebilir. OUTPUP pin için bir yön belirlenmesi de söz konusu. Registerlar üzerindeki değerlerde değişiklik yapmak içinse write_volatile fonksiyonundan yararlanılıyor. Her ne zaman bir register değerini değiştirmek istersek bu fonksiyonu kullanabiliyoruz. Elimizde bir timer bulunmadığından duraksatma işlemi için bir döngü kullanılıyor ve içerisinde nop (No Operations anlamında) fonksiyonu çağırılıyor. Ancak dikkat edileceği üzere bu örnek kodlarda unsafe kod blokları içerisinde de işlemler yapılmakta. Rust'ın borrow cheker mekanizması devre dışı kalacağından bazı riskleri de almış oluyoruz. Ayrıca bu küçük örnek için olmasa bile farklı örneklerde kodlaması epey zahmetli olabilir. Örneğin pusulayı kullanmak istesek sırf pusulanın önden kalibrasyonu için nasıl bir kod yazmamız gerekir pek düşünemiyorum. Zaten bu sebeple Hardware Abstraction Layer (HAL) kütüphanelerini kullanmak daha anlamlı duruyor gibi.
Sıradaki örnekte amacım LED panelde RUST kelimesini yazdırmak. Ancak öncelikle LED paneldeki ışıkları kullanarak yanıp sönen bir harf görsek de yeterli olur. Mesela aşağıdaki gibi :)
Bunun için aşağıdaki kod parçasını kullanabiliriz.
#![deny(unsafe_code)]
#![no_main]
#![no_std]
use cortex_m_rt::entry;
use microbit::display::blocking::Display;
use microbit::{board::Board, hal::timer::Timer};
use panic_halt as _;
#[entry]
fn start() -> ! {
let mut board = Board::take().unwrap();
// Tüm Led'ler kapalı
let clear = [
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
];
// Belli ledler açık ve H harfi şeklinde yanıyor.
let H = [
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 1, 1, 1, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
];
// Matrisleri kullanabilecek bir Display nesnesi
let mut display = Display::new(board.display_pins);
let mut timer = Timer::new(board.TIMER0);
loop {
// 1 saniye boyunca clear matrisine göre led'ler konumlanacak (Hepsi Kapalı)
display.show(&mut timer, clear, 1000);
// 1 saniye boyunca H matrisindeki Led'ler yanacak
display.show(&mut timer, H, 1000);
// ve bu böyle sürüp gidecek (Blinky effect)
}
}Önceden de belirttiğimiz üzere Micro:bit üzerinde 5x5 lik bir LED matrisi var. microbit crate ile sunulan Display yapısını kullanarak ve iki boyutlu bir dizinin elemanlarını 0 veya 1 olarak değiştirerek istenilen ışıkların yanmasını sağlayabiliriz. İşte kısa aralıklarla ve R, U, S ve T harflerini nasıl gösterebileceğimizin kodu.
#![deny(unsafe_code)]
#![no_main]
#![no_std]
use cortex_m_rt::entry;
use microbit::display::blocking::Display;
use microbit::{board::Board, hal::timer::Timer};
use panic_halt as _;
#[entry]
fn start() -> ! {
let board = Board::take().unwrap();
// Tüm Led'ler kapalı
let clear = [
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
];
// Belli ledler açık ve R harfi şeklinde yanıyor.
let r = [
[1, 1, 1, 0, 0],
[1, 0, 0, 1, 0],
[1, 1, 1, 0, 0],
[1, 0, 1, 0, 0],
[1, 0, 0, 1, 0],
];
let u = [
[1, 0, 0, 1, 0],
[1, 0, 0, 1, 0],
[1, 0, 0, 1, 0],
[1, 0, 0, 1, 0],
[1, 1, 1, 1, 0],
];
let s = [
[1, 1, 1, 1, 0],
[1, 0, 0, 0, 0],
[1, 1, 1, 1, 0],
[0, 0, 0, 1, 0],
[1, 1, 1, 1, 0],
];
let t = [
[1, 1, 1, 1, 1],
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
];
// Matrisleri kullanabilecek bir Display nesnesi
let mut display = Display::new(board.display_pins);
let mut timer = Timer::new(board.TIMER0);
const WAIT:u32 = 500;
loop {
// 250 milisaniye boyunca clear matrisine göre led'ler konumlanacak (Hepsi Kapalı)
display.show(&mut timer, clear, WAIT);
// 250 saniye boyunca R matrisindeki Led'ler yanacak
display.show(&mut timer, r, WAIT);
display.show(&mut timer, u, WAIT);
display.show(&mut timer, s, WAIT);
display.show(&mut timer, t, WAIT);
// ve bu böyle sürüp gidecek (Blinky effect)
}
}Örneğin güncel hali biraz daha efektif esasında. Harfler constant olarak bir enum türü ile ilişkilendirilerek kullanılabilir. Bunun için letter_pipe isimli bir modül oluşturup içeriğini aşağıdaki gibi güncelleyebiliriz.
#[derive(Debug, Clone, Copy)]
pub enum Letter {
Clear,
R,
U,
S,
T,
}
pub const fn get_letter(letter: Letter) -> [[u8; 5]; 5] {
match letter {
Letter::Clear => [[0; 5]; 5],
Letter::R => [
[1, 1, 1, 0, 0],
[1, 0, 0, 1, 0],
[1, 1, 1, 0, 0],
[1, 0, 1, 0, 0],
[1, 0, 0, 1, 0],
],
Letter::U => [
[1, 0, 0, 1, 0],
[1, 0, 0, 1, 0],
[1, 0, 0, 1, 0],
[1, 0, 0, 1, 0],
[1, 1, 1, 1, 0],
],
Letter::S => [
[1, 1, 1, 1, 0],
[1, 0, 0, 0, 0],
[1, 1, 1, 1, 0],
[0, 0, 0, 1, 0],
[1, 1, 1, 1, 0],
],
Letter::T => [
[1, 1, 1, 1, 1],
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
],
}
}İlgili fonksiyon kod tarafında da aşağıdaki gibi kullanabiliriz.
display.show(&mut timer, get_letter(Letter::R), WAIT);// MicroRust reposunda ele alınıyor
// MicroRust reposunda ele alınıyor
Aslında doğrudan LED'lere ulaşmaya çalışmadan önce basit bir örnekle cihazla iletişim kurup kuramadığımızı da anlayabiliriz. Bu sefer bilgisayarımızdaki terminale Real-Time Transfer protokolünden de faydalanarak mesaj bastıracağız. Öncelikle program için gerekli crate'leri ekleyerek başlayalım.
[package]
name = "micro-hello-2"
version = "0.1.0"
edition = "2024"
[dependencies]
cortex-m = { version = "0.7.7", features = ["critical-section-single-core"] }
cortex-m-rt = "0.7.5"
panic-halt = "1.0.0"
rtt-target = "0.6.1"Burada dikkat edilmesi gereken hususlardan birisi rtt-target ile gelen ve terminale mesaj yazdırmak için kullanacağımız fonksiyonların sorunsuz çalışması için cortex-m küfesindeki critical-section-single-core özelliğinin etkinleştirilmiş olması gerekir. Özellikle kodun işleyişi ile ilgili bilgi almak için debug maliyetine girmek istemediğimiz durumlarda bu tip terminal logları çok işe yaramaktadır.
Embed parametresi ile çalışacak komutlar için kullandığımız Embed.toml içeriğini de aşağıdaki gibi geliştirebiliriz.
[default.general]
chip = "nrf52833_xxAA"
[default.rtt]
enabled = truePek tabii yine bir memory.x dosyası gerekmekte. Diğer örneklerde kullandığımız içeriğin aynısını bu örnekte de kullanabiliriz. Buna bağlı olarak .cargo/config.toml içeriğini de aşağıdaki gibi ayarlayabiliriz.
[build]
target = "thumbv7em-none-eabihf"
[target.thumbv7em-none-eabihf]
rustflags = [
"-C", "link-arg=-Tlink.x",
]main.rs dosyasındaki kodlarımız ise şöyle.
#![no_std]
#![no_main]
use cortex_m::asm::nop;
use cortex_m_rt::entry;
use panic_halt as _;
use rtt_target::{rprintln, rtt_init_print};
#[entry]
fn main() -> ! {
rtt_init_print!();
rprintln!("Starting up...");
loop {
rprintln!("Hi!");
for _ in 0..400_000 { // Kastılı bir gecikme yaptırıyoruz. Yaklaşık bir saniye gibi.
nop();
}
}
}Kod düzenli aralıklarla terminal ekranına Hi! mesajını bırakır. Bu ayarlara göre programı çalıştırmak için cargo aracına sadece embed parametresini göndermek yeterlidir. Bu komutla mikrodenetleyici program kodu gönderilir, bir terminal açılır ve mesajlar bu pencereye doğru akmaya başlar.
cargo embedRtt özelliğini etkinleştirdiğimizden terminal açılacak ve aşağıdaki gibi düzenli olarak mesaj basılacaktır.
Bu örnekle birlikte terminal üzerinden deneyebileceğimiz birkaç komutu daha paylaşalım.
# Yapılanların kontrolü için
cargo check
# Standart build işlemi
cargo build
# Build sonrası program için kullanılacak segment bilgilerini görmek için
cargo size -- -AxÇok doğal olarak yazılan proje kodunun bazı hallerde debug edilmesi de gerekebilir. Esasında iyi bir loglama alt yapısı ve mantıklı log çıktıları işi kolaylaştırabilir ancak bir mikrodenetleyici ile çalışıyoruz. Log çıkarmak bile zor. Nereye çıkaracağız, nasıl çıkaracağız, kaç KB atabileceğiz, ne kadar sıklıkla gönderebileceğiz vs. Hiç terminal arabirimi olmayan bir denetleyici söz konusu ise bunu bir iletişim protokolü ile biryere çıkmak düşünülebilir. Aklıma MQTT gibi protokoller geliyor ama bunu başka bir araştırmaya bırakalım derim. Bu nedenle debug yapabilme yetisine de sahip olmamız lazım. Öncelikle GDB modunu etkinleştirmek gerekiyor. Yani Embed.toml dosyasındaki default.gdb değeri aşağıdaki gibi değiştirilmeli.
[default.gdb]
enabled = trueUygulama kodunu yine aşağıdaki komut satırı ile derleyebiliriz.
cargo embed --features v2 --target thumbv7em-none-eabihfgdb modunu etkinleştirdiğimiz için localhost 1337 portundan erişilebilen bir GDB server ayağa kalkacaktır. Bu debug server'a mikrodenetleyici üzerinde çalışmakta olan programı attach ederek debug işlemleri yapılabilir. Kodda breakpoint konulabilir, step-in gibi aksiyonlar icra edilebilir vb Sunucu tarafı başarılı şekilde çalıştırıldıktan sonra ise ayrı bir terminal penceresi açıp debugger aracını başlatmamız ve debug sunucusuna bağlanmamız gerekiyor.
# İstemci programı debug modda açmak için aşağıdaki komut kullanılır
arm-none-eabi-gdb .\target\thumbv7em-none-eabihf\debug\micro-lights
# Sonrasında debug sunucusuna bağlanmak için de şu komut işletilmelidir
target remote :1337
# register'ları görmek için
info registers
# Assembly kodlarını görmek için
set print asm-demangle on
disassemble
# Eğer işlem başarılı olursa örneğin aşağıdaki komut ile main dosyası başına breakpoint koyup durabiliriz
# Hatta ilk örnekte bunu denersek bulunduğumuz yere göre ışık yanmayabilir veya sürekli yanar pozisyonda da kalabilir
# Zira kod breakpoint noktasında durmaktadır.
break main
# Aşağıdaki komutu vererek kod akışını devam ettirebiliriz
continue
# Belli bir satıra breakpoint eklemek için (Örneğin 14ncü satıra)
break 14
# Step into için
stepi
# local değişkenlerin değerlerini görmek için
info locals
# Değerini görmek için (örneğin age isimli bir değişkenimiz varsa)
print age
# adresini görmek için
print &age
# hatta local değişken değerini değiştirmek için
set var age=49
# breakpoint'i silmek için
delete 1
# mikrodenetleyiciyi resetlemek için
monitor reset
# Debugger'dan çıkmak içinse
quitKabaca aşağıdaki gibi bir durum deneyimledim diyebilirim. Debugger'ı esasında VS Code gibi bir arabirime bağlamak daha iyi bir çözüm olabilir. Zira kalabalık kod kümelerinde bu adımlar işi zorlaştırabilir (Bu arada Linux tabanlı sistemlerde User Interface Terminal açılabiliyor ve debug işlemleri çok daha kolay icra ediliyor) Esasında bu tip mikrodenetleyici çözümlerinde çok karmaşık kodlama yapmak da ne kadar doğru bilemiyorum. Sanki tam anlamıyla Single Responsibility ilkesinin Software ve Hardware ekseninde vücut bulmuş haliyle geliştirme yapmak çok daha mantıklı.
Benim gibi gömülü sistem programcılığına uzak olanlar için gerekli belli başlı terimleri aşağıdaki gibi sıralayabiliriz.
- GPIO (General Purpose Input/Output) : Genel amaçlı giriş/çıkış pinleridir. LED yakmak, buton okumak, sensörlerden veri almak vb işlemlerde kullanılır. Hem giriş (Input) hem de çıkış (output) olarak yapılandırılabilir.
- UART (Universal Asynchronous Receiver-Transmitter): Mikrodenetleyicilerde sensör verilerinin aktarım işlemlerini tanımlayan bir seri iletişim protokoldür. Sadece mikrodenetleyiciler değil bilgisayarlar içinde geçerlidir.
- SPI (Serial Peripheral Interface): Ağırlıklı olarak yine mikrodenetleyicilerde ele alınan bir senkron ve seri haberleşme standardıdır.
- I2C (Inter-Integrated Circuit):
- ADC (Analog-to-Digital Converter): Analog sinyali dijitale çeviren dönüştürücüdür. Örneğin mikrofon sensörüne gelen veriyi dijital hale çevirmekte kullanılır.
- BSP (Board Support Package) : Donanım kartına özel olarak geliştirilmiş başlangıç için gerekli tüm unsurları içeren paketlerin genel adıdır. Karta özel pin tanımlarını, saat ayarlarını, buton buzzer pin ayarlarını vb içerir. Örneğin Micro:bit için kullandığımız microbit-v2 BSP örneklerindendir. Bu tip paketler kullanılarak HAL katmanları da geliştirilebilir.
- HAL (Hardware Abstraction Layer) : Donanım seviyesindeki enstrümanlarla konuşmayı kolaylaştıran bir arayüz olarak düşünülebilir. Örneğin GPIO pinlerine doğrudan erişmek yerine detaylardan uzak ve kolay kullanılabilir bir soyutlama sağlar. Örneğin pin registerlarına doğrudan yazmak yerine pin.set_high gibi anlamlı fonksiyonlar sağlar. Bazen BSP ile karıştırılabilir.nrf52833-hal örnek olarak verilebilir. Bu HAL örneğin belli mikrodenetleyicileri hedefler. Birde daha genel soyutlama sağlayan embedded-hal gibi küfeler vardır. Bunu şöyle de düşünebiliriz; embedded-hal genel arayüz tanımlamalarını içerir (traits), nrf52833-hal ise nRF52833'e özel olarak ilgil trait'leri gerçekten implemente eder. Dolayısıyla cihaza özgü komutlar da içerebilir.
- Peripheral : Mikrodenetleyicinin içinde bulunan GPIO, UART, SPI, I2C, Timer, ADC gibi birimlerdir. Her biri ayrı bir periferik modül olarak kabul edilir.
- PAC (Peripheral Access Crate) : Mikrodenetleyici üreticisinin sağladığı register haritalarını, API'leri otomatik olarak Rust koduna çeviren paketlerdir. HAL kütüphaneleri genelde PAC modülleri üzerine kurulur.
- MCU (Microcontroller Unit) : İşlemci çekirdeği, flash bellek, RAM ve .eşitli çevresel birimleri tekbir çipte barındıran elektronik birim.
- Flashing : Yazılan programın mikrodenetleyici üzerinde çalıştırılması genellikle Flask bellek bölgesine taşınması ile gerçekleştirilir. Bu işlem flashing olarak adlandırılır. probe-rs veya openocd gibi araçlarla yapılır.
- ELF (Executable and Linkable Format) : Derlenen programın hedef sistemde çalıştırılabilir hale getirildiği dosya formatıdır.
- GDB (GNU Debugger) : GNU ekosisteminde yaygın olarak kullanılan debugger.
- Bare Metal Programming: İşletim sistemi olmadan doğrudan donanım üzerinde yazılım geliştirme yaklaşımının adıdır. Yazıda ele aldığımız BBC micro:bit gibi cihazlarda no_std ile yazılan kodlar bare-metal seviyede olur.
- SVD (System View Description): Mikrodenetleyici üzerindeki register ve ilişkili bitleri tarifleyen bir harita dosyası olarak düşünülebilir. svd2rust gibi crate'ler bu dosyaları parse edebilir ve buda Peripherals Access Create'lerin oluşturulmasını kolaylaştırır. Genellikle XML tabanlı bir dosyadır.
- Reset Vector: Mikrodenetleyici yeniden başlatıldığında (reset) çalışmaya başladığı ilk bellek adresidir. Başlangıç kodu da buradan çalıştırılır. Örneklerde attığımız kodlar bu adresten başlatılır.
- Debug Probe: Bilgisayar ile mikrodenetleyici arasındaki fiziksel debug bağlantısını sağlayan araçtır.








