Sayfanın PDF versiyonu: Bootloader Geliştirme (.pdf)
Bootloader Geliştirme
Bir elektronik ürün tasarladığınızda, ürününüzü piyasaya, yazılım hatalarından arındırılmış ve tüm özellikleri çalışır şekilde sürmek istersiniz. Bir yazılımın son haline gelmesi uzun süren geliştirme ve test iterasyonları gerektirir. Bu süreyi beklemek, ürününüzü piyasaya geç sürmenize ve yenilikçi ürününüzün pazar payını rakiplerinize kaptırmanıza sebep olacaktır.

Buna ek olarak, ürününüzü piyasaya sürdükten sonra da yeni bir özellik eklemek isteyebilir, hatta uzun süreli kullanımda ortaya çıkan hataları da düzelterek, müşterilerinize daha iyi bir kullanıcı deneyimi yaşatmak isteyebilirsiniz. Tüm bunlar için ihtiyacınız olan eksik parça bootloader’ dır.

Bu dokümanda, teorik bilgi verildikten sonra STM32F4 Discovery geliştirme kiti kullanılarak örnek bir bootloader geliştirilecektir.
Bootloader Nedir?
Gömülü sistemlerde, sistemin tüm gereksinimlerinin karşılayan, kullanıcı ile etkileşimde olan kısım uygulama yazılımıdır (firmware). Uygulama yazılımını yükleyen, kimi durumlarda donanımı uygulama yazılımı için hazırlayan, uygulama yazılımını güncelleyen yazılım ise bootloader’ dır. Bir bilgisayardan örnek verirsek, BIOS bootloader’ a, işletim sistemi ve programlar da uygulama yazılımına karşılık gelir. Gömülü sistemde ise bootloader ve uygulama yazılımları daha basittirler.


Bootloader, her tasarımda olmamakla beraber, büyük çoğunlukla ilk çalışan yazılımdır.
Bootloader Bileşenleri
Bootloader, en basit haliyle uygulama yazılımını günceller ve çalıştırır. Bunun için ihtiyacı olanlar;
  1. Yeni uygulama yazılımının alınacağı bir veri arayüzü
  2. Eski uygulama yazılımını silerek, yeni uygulama yazılımını kopyalayacak bir yükleyici
  3. Uygulama yazılımını çalıştıracak olan bir yazılım modülü
  4. Opsiyonel olarak, veri güvenliği için bir şifreleyici, yazılımın doğrulunu kontrol edebilecek CRC ünitesi ve kullanıcı etkileşimi için bir komut arayüzü.
Veri arayüzü, yeni uygulama yazılımının alınabileceği herhangi bir donanım olabilir. Örneğin;
  • UART
  • Ethernet
  • Wi-Fi
  • SD Card
  • CAN
  • ModBUS
  • SPI vb.
Uygulama yazılımı UART üzerinden anlık olarak alınabileceği gibi, bir SPI Flash bellekten binary olarak veya SD Card üzerinden dosya olarak da okunabilir. Ethernet veya Wi-Fi kullanıldığında ise uzaktan yazılım güncellenebilir.
Yükleyici, donanımın yeteneklerine ve gereksinimlere bağlı olarak;
  1. Dahili Flash
  2. Harici SPI Flash
  3. Harici NAND Flash
  4. SD Card
  5. RAM
gibi belleklere erişerek, eski yazılımı silebilir ve yeni yazılımı yükler.
Bootloader Mimarileri
Veri arayüzü, yükleyici çeşitleri ve opsiyonel özellikler fazla olduğundan, bunların kombinasyonları ile tasarlanabilecek olan bootloader mimarileri hayal gücünüz ile sınırlıdır. Dolayısıyla burada bir kaç örnek verebiliriz sadece.
Mimari 1
SD Card üzerinde bulunan uygulama yazılımını dahili belleğe yükleyerek çalıştırabiliriz.


Bunun çalışabilmesi için yeni uygulamanın SD Card üzerine yüklü olması gerekli ama yeni uygulama SD Card üzerine nasıl yüklenecek? Bunun için de farklı alternatifler olabilir;
  1. Kullanıcı bilgisayar kullanarak, SD Card üzerine yeni yazılımı bir dosya olarak kaydedebilir. Daha sonra SD Card’ ı bizim sistemimizin SD Card yuvasına takarak sisteme enerji verdiğinde bootloader devreye girebilir.
  2. Eski uygulama yazılımı ile Wi-Fi veya Ethernet üzerinden yeni yazılım SD Card’ a kopyalanır ve donanım resetlendiğinde bootloader devreye girebilir. Tabi ki burada donanımın Ethernet veya Wi-Fi arayüzü olduğunu varsayıyorum.
Bootloader devreye girdiğinde ilk iş olarak, SD Card üzerinde yeni bir uygulama yazılımı olup olmadığını kontrol eder ve yeni uygulamayı bulması durumunda dahili bellekteki eski uygulamayı siler, yeni uygulamayı kopyalar ve uygulamayı çalıştırır.
Mimari 2
SPI Flash üzerinde bulunan uygulama yazılımını RAM belleğe yükleyerek çalıştırabiliriz. Bu durumda uygulama yazılımı güç kesildiğinde RAM bellekten silinecektir. Dolayısıyla, bootloader her açılışta uygulama yazılımını RAM belleğe yeniden yüklemek durumundadır.


Mimari 3
UART ile uygulama yazılımını dahili Flash belleğe yükleyebilir ve çalıştırabiliriz. Buradaki sorun, UART üzerinden uygulamanın nasıl aktarılacağıdır. Çözüm olarak, bir PC yazılımı seri port üzerinden yeni uygulama yazılımını gönderebilir. Peki, haberleşme protokolü ne olacak? Bunun için kendi protokolünüzü geliştirebilir veya Kermit, XModem gibi hazır protokoller kullanabilirsiniz. Kendi protokolünüzü geliştirdiğinizde, bir de bu protokolü kullanarak yeni uygulamayı gönderecek bir PC yazılımına ihtiyacınız olacaktır. Mevcut protokoller kullandığınızda PC yazılımı yazmak zorunda kalmazsınız.


Bunlara benzer şekilde çok farklı alternatifler üretilebilir. Örnek uygulamada Mimari 3’ ü kullanacağız fakat yaptığımız seçim “olması gereken bu” demek değildir. Her tasarımın gereksinimleri farklı olacağından çözümler de farklı olmalıdır. Burada birçok olasılıktan sadece 3 tanesi verilmiştir.
UART Bootloader
Örneğe geçmeden önce ne yapmak istediğimizi ortaya koyalım;
  1. Bootloader sistem açıldığında çalışmaya başlamalı,
  2. UART üzerinden XModem protokolü ile haberleşmeli,
  3. Çalışmaya başladıktan sonra 3 saniye içerisinde yeni uygulama alınmaya başlanmazsa mevcut uygulamayı çalıştırmalı,
  4. Yeni uygulama gönderilemeye başlandıysa, eski uygulamayı silerek yeni uygulamayı yüklemeli, yükleme sonunda da yeni uygulamayı çalıştırmalı.
Örnek uygulamamızı STM32F4 Discovery geliştirme kartı ile yapacağımızı söylemiştik. Bu geliştirme kartı üzerinde STM32F407VG mikrodenetleyici bulunmaktadır. Önce, bu mikrodenetleyici hakkında kritik bir kaç noktayı belirtelim.

Mikrodenetleyicinin 1 MByte’ lık dahili Flash belleği, aşağıdaki tabloda görüleceği gibi 12 sektöre bölünmüştür. Flash bellek üzerinde silme işlemi sektör üzerinden yapılabilir. Yani, bir byte yazmak mümkün iken, bir byte silmek istediğinizde tüm sektörü silmeniz gerekmektedir. Bu sektör adresleri, uygulama yazılımı için ayıracağımız alanı belirlemekte bize yardımcı olacak.


İkinci olarak, mikrodenetleyiciye güç verildiğinde veya mikrodenetleyici resetlendiğinde, donanımsal olarak 0x08000000 adresindeki değeri CPU’ nun Stack pointer (SP) register’ ına yazar, 0x08000004 adresindeki değeri reset vektörü – çalıştırılacak ilk komut adresi/program başlangıç adresi – olarak kullanır. Bu bilgi ile uygulama yazılımının başlangıç adresini bulup uygulamaya sıçramak için kullanacağız.

Üçüncü kritik nokta ise, mikrodenetleyicinin interrupt vektör tablosunun başlangıçta 0x08000000 adresininden başlaması ve bu tablonun başlangıç adreslerinin uygulamaya geçilmeden önce uygulama başlangıç adresi olarak değiştirilmesi gerektiğidir. Bunun sebebini ve nasıl yapılacağını daha sonra göstereceğim.
Uygulama Başlangıç Adresi
Uygulama başlangıç adresi şeçmeden önce bootloader boyutunun ne olacağını kabaca belirlemeliyiz. Gömülü yazılım konusunda biraz çalıştıysanız bunun için sezgisel bir tahmin yapabilirsiniz. Bootloader boyutu önemli çünkü uygulama bootloader dan sonraki sektörden başlayacak.

Örneğin bootloader için 48KB alan ayırırsak, uygulamanın başlangıç adresi 0x08000000+48KB = 0x0800C000 olacaktır. Bu da Sector 3’ ün başlangıç adresine karşılık gelir. Yani Sector 0~2 bootloader için, Sector 3~11 uygulama için kullanılacaktır.


Jump-To-Application
Bootloader’ ın yapması gereken işlerden ilki mevcut(eski) uygulamayı veya yeni uygulamayı çalıştırmak olacaktır. Bunun için 0x080C0000 adresinde yüklü olan uygulamayı, mikrodenetleyici resetlendiğinde çalışıyormuş gibi çalıştırabilmeliyiz.

Yapılması gereken özünde basit bir JUMP/CALL işlemi olsa da öncesinde birkaç satır işimiz daha var.
#define APPLICATION_START_ADDRESS  0x0800C000 //(FLASH_BASE+ 48*1024)

typedef  void (*pFunction)(void);

int main(void)
{

    SystemInit();

    pFunction Jump_To_Application = *( (pFunction*) (APPLICATION_START_ADDRESS + 4) );

    __set_CONTROL(0);

    SysTick->CTRL = 0; //disable SysTick

    SCB->VTOR = APPLICATION_START_ADDRESS;

    /* Initialize user application's Stack Pointer */
    __set_MSP( *( (uint32_t*) APPLICATION_START_ADDRESS ) );

    Jump_To_Application();

}
                    
Yukarıdaki kod ile 0x0800C004 adresinde bulunan uygulama başlangıç adresindeki uygulama yazılımı çalıştırılmaktadır. Fakat öncesinde 0x0800C000 adresindeki değer Stack pointer içerisine yazılmaktadır.
__set_MSP( *( (uint32_t*) APPLICATION_START_ADDRESS ) );
                    
Bootlaoder’ ın mevcut haline buradan erişebilirsiniz. Bunu test etmek istersek, öncesinde 0x0800C000 adresine bir uygulama kodunu yazmış olmalıyız. O zaman bootloader geliştirmede kullancağımız basit bir uygulama yazılımı hazırlayalım. En azından çalıştığını göreceğimiz bir yazılım olmalı. Bunun için yazılımımız D15 pinine bağlı olan LED’ i yakıp söndürsün.
void LEDTask( void* param )
{
    while( 1 )
    {
        //Do smth
        GPIOD->ODR |= (0x00000001<<15);
        vTaskDelay( 100 );
        GPIOD->ODR &= ~(0x00000001<<15);
        vTaskDelay( 900 );
    }
}

int main(void)
{

    //Initialization code
    SystemInit();

    RCC->AHB1ENR |= 0x00000008;
    GPIOD->MODER |= 0x55000000;

    xTaskCreate( LEDTask, "LEDTask", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY, NULL );

    vTaskStartScheduler();

    while( 1 )
    {
    }
}
                    
Test uygulama yazılımına buradan ulaşabilirsiniz. Bootloader ile yüklenecek uygulama yazılımının normal uygulama yazılımlarından bir farkı var. Başlangıç adresi 0x08000000 yerine 0x0800C000 olmalıdır. Bunun için linker ayarlarından uygulama başlangıç adresini değiştirmeliyiz.


Ek olarak, system_stm32f4xx.c dosyasındaki VECT_TAB_OFFSET değerini aşağıdaki gibi değiştirmeliyiz.
#define VECT_TAB_OFFSET  0x0000c000
                    
Örnek uygulama için tüm bunlar yapılmıştır fakat kendi uygulama yazılımınızı hazırlarken buna dikkat etmelisiniz.

Bootloader ve test uygulamasını denemek için, bootloader.bin ve testApp.bin dosyalarını sıarsıyla 0x08000000 ve 0x0800C0000 adreslerine yazmalısınız. Bunun için STM32 ST-LINK Utility uygulamasını kullanabilirsiniz. İki binary dosyayı da flash belleğe yazdığınızda mavi LED’ in yanıp söndüğünü göreceksiniz. Demek ki bootloader daha önceden yüklenmiş olan uygulamayı çalıştırabiliyor ama yeni yazılım yükleyemiyoruz. Yeni yazılım da yüklemek istersek UART portunu aktif etmeli ve XModem protokolünü de eklemeliyiz.
XModem Protokolü
XModem protokolü ile detaylı bilgiye buradan ulaşabilirsiniz.

Neden XModem protokolünü seçtiğimize gelirsek; öncelikle hazır bir protokol olduğundan, bir de PC yazılımı hazırlamak zorunda kalmayacağız. Ayrıca XModem protokolünü gerçeklemesi çok kolaydır. Alternatif olarak farklı bir protokol şeçebilir veya kendi protokolümüzü geliştirebilirdik ama bu sefer amacımıza ulaşmak için daha fazla yorulacaktık.

XModem protokolü alıcı tarafından(bizim için bootloader) kontrol edilen bir protokoldür. Haberleşme parametreleri olarak 115200, 8n1 kullanacağız ama farklı hızlarda da çalışabilirsiniz.

XModem data paketleri 132 byte uzunluğundadır ve içeriği aşağıdaki gibidir;


Checksum değeri 128 byte Data içeriğindeki değerlerin toplamının 256 modülo’ sudur (checksum = toplam%256). Haberleşme Bootloader tarafından gönderilen NAK paketi ile başlar ve aşağıdaki gibi devam eder.


PC tarafından gönderilen her paket 128 byte veri içerir fakat son paket içeriği 128 den küçük olduğunda da kalan alanlar SUB (0x1A) ile doldurularak 128 byte tamamlanır. Bootloader içerisine XModem protokolünü de ekledikten sonra PC den istediğimiz dosyayı bootloader’a gönderebiliyor olduk. Bootloader projesine buradan ulaşabilirsiniz. Projede UART4 ve PA0(TX) PA1(RX) pinleri kullanılmaktadır. Test etmek için PC tarafında kullanabileceğiniz bir çok yazılım mevcut fakat biz TeraTerm’ ü kullanacağız. Bootloader uygulamasını mikrodenetleyicinize yazdıktan ve gerekli bağlantıları yaptıktan sonra TeraTerm uygulamasını açıp seri port ayarlarını yapmalısınız.


Artık ilk dosyamızı mikrodenetleyiciye gönderebiliriz. TeraTerm üzerinden File->Transfer->XModem->Send dedikten sonra açılan pencereden göndermek istediğimiz dosyayı seçiyoruz. TeraTerm dosyayı göndermek için bekliyor. Söylediğimiz gibi XModem alıcı tarafından yönetilen bir protokoldür.


Bootloader’ in çalışması için mikrodenetleyiciyi resetlediğimizde dosya gönderme başlayacak, progressBar %100’e ulaşacak ve dosya gönderme tamamlandığında yukarıdaki ekran kapanacaktır. Proje içerisinde alınan dosyayı flash belleğe henüz yazmadık. Dolayısıyla gönderdiğimiz dosya şimdilik bir işe yaramayacak.
if( checksum == data )
{
    //frame received successfully

    //ToDo: write to flash

    commChannel->write( commChannel->channel, XMODEM_ACK );

    bootloaderDelay = 1000;
}
                    
Kod içerisinde yukarıdaki //ToDo: satırında bu işlemi yapacağız ama önce bir işimiz daha var.
Eski Uygulamayı Silme
Uygulamanın bulunacağı alanda daha önceden eski bir uygulama bulunduğundan önce bunun silinmesi gerekli. Uygulamanın sektor 3~11 arasında olduğunu biliyoruz. Buna göre uygulama alanını silmek için;
void EraseApplicationArea( void )
{
    //unlock flash
    FLASH_Unlock();

    FLASH_EraseSector( FLASH_Sector_3, VoltageRange_3 );
    FLASH_EraseSector( FLASH_Sector_4, VoltageRange_3 );
    FLASH_EraseSector( FLASH_Sector_5, VoltageRange_3 );
    FLASH_EraseSector( FLASH_Sector_6, VoltageRange_3 );
    FLASH_EraseSector( FLASH_Sector_7, VoltageRange_3 );
    FLASH_EraseSector( FLASH_Sector_8, VoltageRange_3 );
    FLASH_EraseSector( FLASH_Sector_9, VoltageRange_3 );
    FLASH_EraseSector( FLASH_Sector_10, VoltageRange_3 );
    FLASH_EraseSector( FLASH_Sector_11, VoltageRange_3 );

    //lock flash
    FLASH_Lock();
}
                    
Peki bu fonksiyon ne zaman çağırılmalı? İlk 128 byte’ lık frame’i aldığımızda bu fonksiyonu bir kez çağırırsak uygulama yazılmadan önce flash bellekteki uygulama alanını temizlemiş olacağız.
if( firstFrame )
{
    EraseApplicationArea();
    firstFrame = 0;
}
                    
Yeni Uygulamayı Kaydetme
Yeni uygulamayı kaydetmek de, sildiğimiz alana aldığımız frame’ leri geldikleri sıra ile kaydetmekten ibaret. Bunun için;
void ProgramAplicationAreaBlock(uint32_t address, uint32_t * data, uint32_t count ){
    uint32_t i;

    if( address < APPLICATION_START_ADDRESS ) return;

    //unlock flash
    FLASH_Unlock();

    for( i=0; i < count; i++ ){
        FLASH_ProgramWord( address, *data++ );
        address+=sizeof(uint32_t);
    }
    //lock flash
    FLASH_Lock();
}
                    
Bu fonksiyonu da her yeni frame için çağırmalıyız.
//ToDo: write to flash
ProgramAplicationAreaBlock( currentProgramAddress, (uint32_t *)dataBuffer, 32 );

currentProgramAddress += sizeof(dataBuffer);
                    
Sonunda yeni uygulamayı da kaydetmeyi başardık. Şimdi bir test yapalım. Projenin son haline buradan ulaşabilirsiniz. Test uygulamasını derledikten sonra TeraTerm ile \testApp\Debug\bin\testApp.bin dosyasını göndermeyi deneyebilirsiniz. Bu uygulama 0x0800C000 adresinden itibaren yazılacak ve çalıştırılacaktır.

Siz de denediyseniz dosya gönderme işlemi biraz yavaşlamış olduğunu göreceksiniz. Özellikle ilk paketin gitmesi biraz fazla zaman alıyor. Bunun sebebi ilk paket transferi ile uygulama alanının silinmesi ve bunun biraz zaman almasıdır.
Sıra Sizde
Bundan sonra bootloader için yeni özellikler ekleyebilirsiniz. Örneğin;
  1. XModem yerine farklı bir protokol deneyebilirsiniz.
  2. Veri arayüzü olarak Ethernet/WiFi gibi farklı bir arayüz veya SDCard gibi farklı depolama alanları kullanabilirsiniz.
  3. Gönderdiğiniz dosyayı şifreleyebilir ve bootloader içerisinde şifreyi çözerek veri güvenliğini sağlayabilirsiniz.