25

Uzaktan Gömülü Yazılım Güncelleme

Remote Firmware Update
    Gömülü sistemler için önemli konulardan birisi de yazılımın uzaktan güncellenebilmesidir. İçinde gömülü yazılım barındıran ürünler geliştiriyoruz ve bazen bu ürünlerdeki gömülü yazılımı, kodlardaki hatalardan veya müşteri isteğine göre yazılımsal eklemeler, geliştirmeler yaparak güncelleme ihtiyacı duyuyoruz. Böyle durumlarda geliştirdiğimiz yeni yazılımı ürünlerin başına giderek güncellemek doğru bir seçim olmaz. Kaldı ki bu ürünlerden binlerce satılmış olabilir hatta sadece yurt içi değil yurt dışına dahi gönderilmiş olabilir. Böyle durumlar için geliştirdiğimiz ürünün uzaktan güncellenebilir olması önemli bir özelliktir.
    Bu yazıda gömülü yazılımın uzaktan(kablosuz) nasıl güncelleneceği ile ilgili bir örnek uygulama paylaşacağım. Mikrodenetleyici olarak STM32F103C8T6(blue pill) ve internete bağlanıp yeni yazılımı indirebilmek için ESP8266‘yı kullanacağım. Bu uygulama örneği için değineceğimiz başlıca konu başlıkları aşağıdaki gibidir.
  • IAP(In Application Programming) ve Bootloader
  • TFTP Protokolü
  • İndirilen Yeni Firmware’in Çalıştırılması.
  • CRC (checksum)

IAP(In Application Programming) ve Bootloader

    İnternette çoğu kaynakta IAP ve Bootloader için aynı kavramlarmış gibi bahsedilir. Geneline baktığımızda ikisinin de yaptığı işlemler aynıdır. Sistem resetlendiğinde veya kullanıcı tarafından bir etki ile çalışıp yeni bir firmware güncellemesi olup olmadığına bakar. Eğer yeni bir güncelleme var ise yeni firmware’i indirip çalıştırır. Bazı kaynaklar bu işlemlerin tamamına IAP, bazı kaynaklar ise bootloader demiş. Bu yüzden ben IAP yazayım siz bootloader anlayın.
    IAP, mikrodenetleyicinin kendi kendini programlaması demektir. Bu programlamayı flash hafızaya uygulama kodlarını yazarak yapar. IAP normal  bir gömülü yazılım gibi çalışan bir uygulamadır aslında. IAP kodları da normal uygulama kodları gibi mikrodenetleyici üzerinde çalışır ve her iki uygulamanın kodu da mikrodenetleyicinin flash hafızasında tutulur. Mikrodenetleyici resetlendiğinde veya uygulama kodunun herhangi bir yerinde IAP kodları çalıştırılabilir. IAP kodları eğer yeni bir güncelleme isteği var ise yeni uygulama kodlarını flash hafızadaki belirlenen alana yazar. Genelde IAP kodları flash hafızanın ilk sayfalarına yazılır. Flash hafızanın geri kalan sayfalarına ise uygulama kodları yazılır. Böylece mikrodenetleyici resetlendiğinde ilk olarak IAP programı çalışır.
    Bu uygulamada IAP için Flash hafızada 15KB’lık bir alan ayrılmıştır. Yani IAP kodları Flash hafızada ilk 15KB lık alanda saklanacak. IAP kodlarını yazdıktan sonra IAP uygulamamın yaklaşık 11KB olduğunu gördüm. Bu yüzden uygulama başlangıç adresi biraz daha aşağı çekilerek uygulama için daha fazla alan ayrılabilir. IAP kodlarının görevi internetten yeni yazılımı indirip uygulama için ayrılan hafızaya yazmak olacak. Daha sonra eğer indirilen dosya doğru bir şekilde flash hafızaya yazılmış ise IAP uygulamasından yeni indirilen uygulamaya geçiş yapılacak. IAP uygulaması uzaktan bir komutla veya mikrodenetleyici resetlendiğinde çalıştırılabilir. Böylece sistem firmware güncellemesi yapabilir.

IAP ve APP Uygulamaları İçin Keil Ayarları

    IAP ve APP uygulamalarını iki farklı proje olarak oluşturalım. Önce IAP kodlarını yazıp mikrodenetleyiciye yükledikten sonra APP kodlarını da IAP kodlarını silmeden keil ile yüklemek isteyebiliriz. IAP kodlarını yazarken keil de herhangi bir değişiklik yapmadan kodları yazıp,derleyip mikrodenetleyiciye yükleyebiliriz fakat APP kodlarını yazarken keil de bazı ayarlar yapmamız gerekir.
APP projesi için; options for target -> target pencerisinde aşağıdaki değişlikleri yapmamız gerekir.
veya linker sekmesinden aşağıdaki gibi ayarlamalar yapılabilir.
    Bu aşamadan sonra APP uygulamasını yani mikrodenetleyici üzerinde çalışacak olan asıl yazılımı IAP kodlarını silmeden mikrodenetleyiciye keil ile yükleyebilirsiniz. Unutmadan şunu da belirtelim. options for target -> utilities -> settings -> flash download pencerisinde  “Erase Full Chip” kutucuğunun işaretli olmadığından emin olun. Aksi takdirde APP kodunu yüklerken IAP kodları silinir.

TrueSTUDIO veya CubeIDE İçin Linker Dosyası Ayarları

    Eğer APP projesini Atollic veya CubeIDE üzerinde geliştiriyorsak yukarıdaki ayarları linker dosyasında ufak değişiklikler ile yapabiliriz. Linker dosyası “.ld” uzantılı bir dosyadır. Bu dosya içinde mikrodenetleyicinin bellek bölümlerini gösteren aşağıdaki gibi kod bloğu vardır.
    Linker script içinde başlangıçta flash başlangıç adresi 0x08000000 olarak tanımlıdır. Bu satırı yukarıdaki gibi düzenleyebiliriz. Ayrıca vektör tablosunun offset değerini de belirtmemiz gerekir. Bunu APP uygulamasının içinde main fonksiyonunun en üstünde aşağıdaki gibi yapabiliriz.
SCB->VTOR = 0x08003C00;
veya system_stm32f1xx.c dosyası içindeki VEC_TAB_OFFSET tanımlamasını aşağıdaki gibi değiştirebiliriz.
#define VECT_TAB_OFFSET  0x3C00

Vektör tablosuna aşağıda tekrar değineceğiz.

Örnek Devre Şeması

    Bu IAP uygulaması için kurduğum örnek devre şeması yukarıdaki gibidir. Bu örnekte firmware dosyası UART1 üzerinden DMA ile alınıp flash hafızaya yazılacaktır. UART2 ise uzaktan güncelleme adımlarını gözlemleyebilmek için bir nevi komut satırı gibi kullanılmıştır. İndireceğimiz yeni firmware in çalışıp çalışmadığını görebilmek için Bluepill üzerindeki C13 ledi kullanılmıştır. Yani indireceğimiz APP uygulaması aslında bir led blink yapacaktır.
    Peki yeni yazılımı internetten nasıl indireceğiz? Bunun için dosya transfer protokollerinden olan TFTP‘ yi kullanacağım.

TFTP Protokolü

    Trivial File Transfer Protocol (TFTP) , UDP üzerinde çalışan basit bir dosya transfer protokolüdür. TFTP de dosya aktarımı, bir client TFTP server’a okuma veya yazma isteği gönderdiğinde başlar. Server dosya transferini onayladığında transfer başlar. Veriler sabit bloklar halinde gönderilir(örneğin 512 byte’lık bloklar). Transfer edilen her data bloğu alıcı tarafından onaylanmalıdır(acknowledge). Eğer sabit blok uzunluğundan daha az uzunlukta bir blok gelirse transferin son bloğu olduğu anlaşılır ve dosyanın tamamının alındığı anlamına gelir.
     Bu uygulamada bilgisayara bir TFTP server kurup STM32’den bir dosya okuma isteği(read request) göndereceğiz. Eğer dosya mevcut ise server bize dosyayı paketler halinde göndermeye başlayacak. Bu paketler ESP8266 üzerinden dolayısı ile UART1 üzerinden STM32’ye gelecek. IAP uygulamamız UART üzerinden okuduğu yeni firmware dosyasını flash belleğe yazacak.
    Server’a bağlanmak için IAP.c dosyası içinde aşağıdaki fonksiyonu oluşturuyorum.
Connection_Status_Typedef TFTPServerConnect(char *ipAddress,char *port)
{
    char txBuffer[40],rxBuffer[15];
    
    HAL_UART_Transmit(&huart1,(uint8_t *)"AT+CIPCLOSE\r\n",strlen("AT+CIPCLOSE\r\n"),1000);
    HAL_Delay(100);
    HAL_UART_Transmit(&huart1,(uint8_t *)"AT+CIPMUX=0\r\n",strlen("AT+CIPMUX=0\r\n"),1000);
    HAL_Delay(100);
//    HAL_UART_Transmit(&huart1,(uint8_t *)"AT+CIFSR\r\n",strlen("AT+CIFSR\r\n"),1000);
//    HAL_Delay(100);
    HAL_UART_Transmit(&huart1,(uint8_t *)txBuffer,sprintf(txBuffer,"AT+CIPSTART=\"UDP\",\"%s\",%s\r\n",ipAddress,port),5000);
    HAL_UART_Receive_DMA(&huart1,(uint8_t *)rxBuffer,15);
    volatile uint32_t TimeOut = 5000;
    volatile uint32_t Time = HAL_GetTick();
    while(HAL_GetTick() - Time < TimeOut)
    {
    for(int i=0;i<15-strlen("CONNECT");i++)
    {
        if(  rxBuffer[i]   == 'C' &&
             rxBuffer[i+1] == 'O' &&
             rxBuffer[i+2] == 'N' &&
             rxBuffer[i+3] == 'N' &&
             rxBuffer[i+4] == 'E' &&
             rxBuffer[i+5] == 'C' &&
             rxBuffer[i+6] == 'T')
        {
                HAL_UART_DMAStop(&huart1);
                return Connection_OK;
        }
    }
}
    HAL_UART_DMAStop(&huart1);
    return Connection_ERROR;
}
    Server’a dosya okuma isteği göndermek için ise aşağıdaki fonksiyonu oluşturuyorum. TFTP server’a dosya okuma isteği gönderdiğimizde eğer dosya mevcut ise server bize dosyayı data paketleri halinde göndermeye başlar. Eğer dosya mevcut değil ise “file not found” şeklinde bir error paketi yollar.
void ReadRequest(char *filename)
{
    char txBuffer[50],rxBuffer[10];
    TFTP_Opcode opcode = TFTP_RRQ;
    char *TransferMode = "octet";
    uint8_t TxBufferLength,ESPRespond=0;
    
    TxBufferLength = sprintf(txBuffer,"%c%s%c%s%c",(char)opcode,filename,(char)0,TransferMode,(char)0);
    HAL_UART_Receive_DMA(&huart1,(uint8_t *)rxBuffer,10);
    HAL_UART_Transmit(&huart1,(uint8_t *)txBuffer,sprintf(txBuffer,"AT+CIPSEND=%d\r\n",TxBufferLength),1000);
    uint32_t TimeOut = 5000;              // 5 seconds timeout
    uint32_t Time = HAL_GetTick();
    while(HAL_GetTick() - Time < TimeOut) // wait until the ESP sends the ">" character.(max 5 seconds)
    {
        for(int i=0;i<10;i++)
        {
            if(rxBuffer[i] == '>')
            {
                ESPRespond = 1;
                break;
            }
        }
        if(ESPRespond)
            break;
    }
    HAL_UART_DMAStop(&huart1);
    HAL_UART_Transmit(&huart1,(uint8_t *)txBuffer,sprintf(txBuffer,"%c%c%s%c%s%c",(char)0,(char)opcode,filename,(char)0,TransferMode,(char)0),1000);
    HAL_UART_Receive_DMA(&huart1,(uint8_t *)RxBuffer,550);
    HAL_Delay(200);
    
}

TFTP Server

    TFTP server kurmak için Tftpd64  programını kullanacağım.  Öncelikle bahsetmek isterim ki kuracağımız server ile ESP8266 aynı ağa bağlı olacak. Farklı ağlar üzerinden uzaktan güncelleme yapmak için statik ip ve port yönlendirme ile ilgili ayarlar yapmak gerekmektedir.
    Program ilk açıldığında settings sekmesinden yukarıdaki ayarları yapıyoruz. Burada server için base directory den bir dosya yolu belirtmemiz gerek. Bunun için masaüstüne “upgrade folder” isminde bir yeni klasör oluşturuyorum. Güncellenecek olan yeni firmware bu dosya içerisinde bulunacak. Bir başka önemli ayrıntı ise IP kutucuğudur. Bu kutucuktan server’ın kurulu olduğu bilgisayarın IP’sini seçiyoruz.

Binary Dosyası ve Hex Dosyası

    İndireceğimiz dosya binary mi yoksa hex dosyası mı olmalı? Her ikisi de olabilir fakat hex uzantılı firmware dosyaları sadece program kodunu içermezler. Binary dosya ise sadece mikrodenetleyici üzerinde çalıştırılabilir binary dataları tutar. Yani hex dosyasını indirirsek doğrudan çalıştıramayız. Bu yüzden bu örnekte indireceğimiz dosya .bin uzantılı binary dosyası olacak. Binary dosyasının dezavantajı ise içerisinde checksum değeri barındırmamasıdır. Bu yazının asıl amacı basitçe bir güncelleme yapmak olduğu için binary dosyasını indirip doğrudan flash hafızaya yazacağım. Bu yüzden binary dosyasının sonuna checksum değerini python ile basit bir kod yazarak ekleyeceğim. Eğer .hex uzantılı dosya ile ilgilenseydik python ile checksum hesaplama ve dosya sonuna ekleme gibi ekstra işlemler yapmayacaktık çünkü hex dosyasının içinde checksum değeri vardır. Bu yüzden yazıyı daha da uzatmamak için hex yerine binary dosyayı kullanacağım. Checksum’ın ne olduğuna aşağıda tekrar değineceğiz.

Keil ile Binary Dosya Çıktısı Alma

    Keil de binary dosya çıktısı alabilmek için options for target -> user -> After Build/Rebuild -> Run #1 kutucuğunu işaretleyip, aşağıdaki komut yazılabilir.
fromelf.exe  --bin -o "$L@L.bin" "#L"

TrueStudio ile Binary Dosya Çıktısı Alma

    TrueStudio ile binary dosya çıktısı almak için aşağıdaki adımlar takip edilmelidir.
Project -> Properties -> C/C++ Build -> Settings -> Tool Settings -> Output Format
    Atollic’te sadece bu ayarlar yeterli değildir. Atollic projeyi derledğimizde bize .binary uzantılı bir dosya çıkarır. Bu uzantıyı .bin olarak düzenleyip mikrodenetleyiciye yükleyebilirsiniz.

CRC(Checksum)

    Dosyayı indirip flasha yazdık peki dosyanın bozulmadan tam olarak alındığından nasıl emin olabiliriz? Bu gibi durumlarda veri doğrulama için checksum kullanılır. Cheksum ile bütün byte’lar belli matematiksel veya lojik işlemlerden geçirilerek bir checksum değeri üretilir. Eğer flash’a yazdığımız byte’ların checksum’ı ile binary dosyasının checksum’ı uyuşuyor ise binary dosya bozulmadan hafızaya yazılmış demektir.
    Binary dosyasının içinde sadece mikrodenetleyici tarafından çalıştırılacak byte’lar bulunur. Peki checksum’ı binary dosyaya nasıl ekleyeceğiz? Araştırmalarıma göre bu işlemi yapacak program yok denecek kadar az. Bu yüzden binary dosyasına checksum eklemek için kendimce yöntemler kullandım. Bunun için python da basit bir kod yazıp binary dosyasının cheksum’ını hesaplayıp tekrar aynı dosyanın sonuna eklemeyi düşündüm. Belki IDE lerin bunun için bir eklentisi vardır. Bununla ilgili farklı bir fikri olan veya bunun için eklenti bilen varsa yorumlarda belirtebilirseniz müteşekkir olurum.
    Checksum hesaplamak için farklı algoritmalar bulunmaktadır. Peki hangi algoritmayı seçeceğiz? Bunu STM32’ye göre belirleyeceğiz. STM32’ler de cheksum hesaplamak için CRC birimi bulunmaktadır. STM32F1 için reference manual’de CRC biriminin CRC-32 Ethernet standardına göre hesaplama yaptığını görebiliriz. Bu yüzden python tarafında binary dosyasının checksum’ını hesaplarken bu standarda göre hesaplattıracağız. Aksi takdirde python tarafında hesaplanan checksum ile stm32’nin hesapladığı checksum farklı çıkar.
    Python’da cheksum hesaplamak için yazmış olduğum kod aşağıdaki gibidir. Checksum hesaplamak için “libscrc” modülünü kullandım. Bu python kodu binary dosyasının sonuna “CRC” karakterlerini ve hemen ardından da dosyanın 32bitlik CRC’sini ekler.
import libscrc
import os


FileName = str(input("Enter file name:"))
try:
    file = open(FileName,"rb")
    if "CRC".encode() in file.read():
        print("Checksum is already exist!")
        NumOfBytes = os.path.getsize(FileName) #get file size
        file.close()
        file = open(FileName,"rb")
        DataBytes = file.read()
        print("CRC: 0x",end="")
        for i in range(4,0,-1):
            print(hex(DataBytes[NumOfBytes-5+i]).lstrip('0x'),end="")
        print()
        print("Number of bytes(with CRC + 4 bytes): " + str(NumOfBytes))
        file.close()
    else:
        file = open(FileName, "rb")
        BinData = file.read()
        NumOfBytes = os.path.getsize(FileName)
        CRC32 = libscrc.fsc(BinData) #Ethernet Frame Sequence
        print("CRC: " + hex(CRC32))
        print("Number of bytes: " + str(NumOfBytes))
        NewFile = open(FileName,"ab")
        NewFile.write("CRC".encode())
        NewFile.write(CRC32.to_bytes(4,byteorder= 'little')) # little endian
        print("CRC added successfully.")
        NewFile.close()
        NewFile = open(FileName,"rb")
        NewBinData = NewFile.read()
        print("End of the file -> ....",end="")
        for i in range(0,7):
            if i < 3:
                print(chr(NewBinData[NumOfBytes+i]),end=" ")
            else:
                print(hex(NewBinData[NumOfBytes + i]), end=" ")


except OSError as error:
    print("OS error: {0}".format(error))
except:
    print("Unexpected error!")

    Serverdan binary dosyayı indirirken sonundaki CRC ile birlikle flash hafızaya yazıyorum. Flash hafızadan aşağıdaki fonksiyon ile binary dosyanın CRC’sini çekiyorum. Yukarıda farkettiyseniz 4 byte’lık CRC değerini little-endian formatta dosyanın sonuna ekledik. Çünkü çoğu ARM cortex M serisi işlemciler flash hafızaya little-endian formatta yazar. Örneğin; 0xA2FC değerini flash hafızaya 0xFC 0xA2 şeklinde kaydeder.
    Flash hafızadan CRC değerini okumak için aşağıdaki fonksiyonu oluşturuyorum.
uint32_t getCRCfromFile(void)
{
    if(AppSize == 0)
        return 0xFFFFFFFF;
    
    uint32_t CRCval;
    
    CRCval = Read_Flash(APP_START_ADDRESS + AppSize+3);
    CRCval = (Read_Flash(APP_START_ADDRESS + AppSize+5) << 16) | CRCval;
    
    return CRCval;
}
    Binary dosyanın sonuna eklediğimiz CRC değerini bu fonksiyon ile okuduk. Şimdi STM32 ile CRC birimini kullanarak APP başlangıç adresinden bitiş adresine kadar her 32 bit lik değerleri CRC hesaplama işlemine sokacağız. Bunun için aşağıdaki fonksiyonu oluşturuyorum.
uint32_t CalculateCRC(void)
{
    if(AppSize == 0)
        return 0xFFFFFFFF;
    
    uint32_t WordData;
    uint16_t WordDataH,WordDataL, MSB, LSB;
    
    __HAL_CRC_DR_RESET(&hcrc);
    for(uint32_t i=0; i<AppSize;i+=4)
    {
        MSB = Read_Flash(APP_START_ADDRESS+i) & 0xFF;  // convert little endian to big endian (0x45E3 -> 0xE345)
        LSB = Read_Flash(APP_START_ADDRESS+i) >> 8;
        WordDataH = (MSB << 8) | LSB;
        MSB = Read_Flash(APP_START_ADDRESS+i+2) & 0xFF;
        LSB = Read_Flash(APP_START_ADDRESS+i+2) >> 8;
        WordDataL = (MSB << 8) | LSB;
        WordData = (WordDataH << 16) | WordDataL;            // combine two 16 bit variables.
        
        hcrc.Instance->DR = WordData;                    // Load 32 bit flash data into CRC data register.
        
    }
    return hcrc.Instance->DR;    
}
    Bu iki fonksiyonun döndürdüğü değerler eşit ise indirdiğimiz .bin uzantılı dosya bozulmadan hafızaya yazılmış demektir. Bu aşamadan sonra indirdiğimiz APP uygulamasını çalıştırabiliriz.

İndirilen Yazılıma Atlama(Jump Function)

    Öncelikle APP uygulamasına atlamak için kullandığımız kodları bir inceleyelim.
if (((*(__IO uint32_t*)APP_START_ADDRESS) & 0x2FFE0000 ) == 0x20000000) // if there is an application at the APP_START_ADDRESS
{
                
  typedef void(*pFunction)(void);
  pFunction Jump_To_Application;
  uint32_t JumpAddress;    
            
  JumpAddress = *(__IO uint32_t*)(APP_START_ADDRESS + 4); // reset handler address
  Jump_To_Application = (pFunction)JumpAddress;
            
  __set_PRIMASK(1); // disable interrupts


  SysTick->CTRL = 0;    // disable systick
            
            
   __set_MSP(*(__IO uint32_t*)APP_START_ADDRESS);    // set Main Stack Pointer
        
        
   Jump_To_Application();
}
    İlk satırda if koşulu ile APP uygulamasının stack değeri kontrol edilir. Binary dosyasının en başında  vektör tablosu(vector table) bulunur ve aşağıda da görüldüğü gibi bu tablonun ilk 32 bitlik değeri başlangıç stack değerini gösterir(initial stack pointer).
    Burada if koşulu ile APP_START_ADDRESS adresinde bir uygulama var mı yok mu kontrol edilir. Peki bu if şartının sağlamasını nasıl yapabiliriz? initial sp değerini nasıl öğrenebiliriz?  Bunun için .map uzantılı dosyayı inceleyebilirsiniz.
    Yukarıda görüldüğü gibi APP uygulamamızın başlangıç stack pointer değerinin 0x20000410 olduğunu görebiliriz. if kouşulunun içindeki bitsel ve(bitwise and) işlemi yapıldığında sonucun 0x20000000 olduğu görülebilir. Bu değerin alabileceği belli bir aralık vardır o yüzden if koşulunun içinde 0x2FFE0000 ile bitwise and işlemine tutulur. Daha sonra aşağıdaki fonksiyon işaretiçisi(function pointer) tip tanımlaması yapılmıştır.
typedef void(*pFunction)(void);
pFunction Jump_To_Application;
    Bu fonksiyon işaretçisi yeni indirdiğimiz uygulamayı sanki bir fonksiyonmuş gibi çalıştıracak.
JumpAddress = *(__IO uint32_t*)(APP_START_ADDRESS + 4);
Jump_To_Application = (pFunction)JumpAddress;
    Yukarıdaki işlem ile fonksiyon pointerın adresini belirliyoruz. Burada dikkat ettiyseniz JumpAddress  APP başlangıç adresinin 4 fazlası olarak seçilmiştir. Tekrar vektör tablosuna dönüp baktığımızda bu adresin reset handler fonksiyonuna ait olduğunu görürüz. Yani Jump_To_Application() fonksiyonu çağrıldığında aslında program APP uygulamasının vektör tablosuna ait olan reset handler’ını çalıştıracaktır. Peki reset handler’ın içinde hangi işlemler yapılıyor bir bakalım. Bunun için startup_stm32f103xb.s assembly dosyasını açalım. Bu dosyanın içinde reset handler’ın hangi fonksiyonları çağırdığını görebiliriz.
    Yukarıda görüldüğü gibi stm32 ilk çalıştığında reset handler’a girer ve burada ilk olarak SystemInit fonksiyonu çağrılır. SystemInit fonskiyonu ise system_stm32f1xx.c dosyası içinde bulunur. Daha sonra APP uygulamasının main fonksiyonu çağrılır. Jump_To_Application() fonksiyonu çağrılmadan önce kesmelerin kapatılması önerilir. Bunun için aşağıdaki macro kullanılabilir.
__set_PRIMASK(1); // disable interrupts
    Bu makro NMI(Non-maskable interrupt) ve hard fault kesmesi hariç bütün kesmeleri kapatır. Burada dikkat etmemiz gereken önemli bir nokta var. Bu satırda kesmeler kapatıldığı için APP uygulamasına atlandığında da kesmeleriniz çalışmaz. Bu yüzden APP uygulamasının main fonksiyon içinde ilk olarak kesmeleri tekrar aktif etmelisiniz. Bunu aynı makroya parametre olarak sıfır değeri göndererek yapabiliriz. Aynı şekilde systick timerı da kapatıyoruz. Bunun için control registerına sıfır değerini yüklemeliyiz.
SysTick->CTRL = 0;   // disable systick
    Daha sonra main stack pointer register’ına APP uygulamasının başlangıç adresini(initial stack pointer) yüklüyoruz. Bu aşamadan sonra yeni indirilen uygulamaya atlayabiliriz.
__set_MSP(*(__IO uint32_t*)APP_START_ADDRESS);    // set Main Stack Pointer

Jump_To_Application();

IAP Akış Diyagramı ve Kodları

    Bu örnekte IAP kodlarını mikrodenetleyici resetlendiğinde çalıştırıcağız. Daha önce de dediğim gibi IAP kodları kablosuz olarak bir komut gönderilerekte çalıştırılabilir. APP uygulaması içinden IAP uygulamasına atlamak için yine aynı şekilde jump fonksiyonu kullanılabilir. Mikrodenetleyici resetlendiğinde bazı sebeplerden dolayı internete ve dolayısı ile server’a bağlanamayabilir. Böyle durumlarda hali hazırda yüklü olan APP uygulamasını çalıştırmak isteyebiliriz. Bu yüzden yukarıdaki diyagramda eğer servera veya internete bağlanma ile ilgili bir sorun oluşursa program indirme yapmadan doğrudan flash hafızadan CRC okuma ve flash hafızanın(APP) CRC’sini hesaplama işlemlerini yapacak. Bunun için kullanılan fonksiyonlar eğer indirme yapılmadı ise default olarak 0xFFFFFFFF değerini dönerler. Böylece eğer yeni firmware indirilemedi ise hali hazırda yüklü olan uygulama çalıştırılır.
IAP_Init();            // initialize IAP peripherals
    
printf("/****************************/\n");
printf("/** STM32 IAP APPLICATION  **/\n");
printf("/**    mehmettopuz.net     **/\n");
printf("/****************************/\n");
    
printf("\nESP8266 is connecting to the internet...\n");
    
if(WifiConnect(SSID,Password) == Connection_OK)
{
  printf("Connected.\n");
  printf("Connecting to TFTP server...\n");
  if(TFTPServerConnect(ServerIP,Port) == Connection_OK)
  {
    printf("Server connection is successful.\n");
    printf("The binary file is downloading...\n");
    ReadRequest(FileName);
    ReceiveHandler();
   }
   else
   {
     printf("Server connection error!\n");
   }
        
}
else
{
  printf("Wifi connection error!!!\nPlease check the SSID and password.\n");
}

uint32_t CRCofFile = getCRCfromFile();
printf("CRC of the bin file: 0x%x\n",CRCofFile);
    
uint32_t CRCofFlash = CalculateCRC();
printf("CRC of the flash memory: 0x%x\n",CRCofFlash);
    
if(CRCofFile == CRCofFlash)
{
  printf("Checksum is correct.\nJumping to application...\n");
        
  if (((*(__IO uint32_t*)APP_START_ADDRESS) & 0x2FFE0000 ) == 0x20000000) // if there is an application at the APP_START_ADDRESS
  {
                
     typedef void(*pFunction)(void);
     pFunction Jump_To_Application;
     uint32_t JumpAddress;    
            
     JumpAddress = *(__IO uint32_t*)(APP_START_ADDRESS + 4); // reset handler address
     Jump_To_Application = (pFunction)JumpAddress;
            
     __set_PRIMASK(1); // disable interrupts


     SysTick->CTRL = 0;    // disable systick
            
            
      __set_MSP(*(__IO uint32_t*)APP_START_ADDRESS);    // set Main Stack Pointer
        
        
      Jump_To_Application();
    }
    else
    {
      printf("Execution failed!\nTry again\n");
      UserLedOn();
    }
}
else
{
  UserLedOn();
  printf("Checksum is not correct!Please try again.\n");
}

Github Linki

Tüm kodlara github üzerinden erişmek için buraya tıklayabilirsiniz.

Eklenebilecek Ekstra Özellikler

Bu yazıda bir gömülü yazılımın uzaktan basitçe nasıl güncellenebileceğini anlatmaya çalıştım. Bu yazı bunun için basit bir örnek olması amacı ile yazılmıştır. Bu örneğe ekstra özellikler eklenebilir. Örneğin;

  • Firmware güvenliği açısından binary dosyanın şifrelenmesi.
  • Server ile olan dosya transferinde bir hata okuma(error packets) mekanizması kurulması.
  • Firmware dosyası için versiyon sorgulama mekanizması kurulması.
  • Bu örnekte indirilen firmware bir öncekinin üzerine yazılmaktadır. Flash hafızası daha büyük olan mikrodenetleyicilerde iki farklı uygulama alanı belirlenip veya harici bir hafıza entegresi kullanılarak indirilecek olan firmware ikinci alana yazılabilir. Böylece eski firmware silinmemiş olur ve güncellemeyi geri alma gibi bir özellik eklenebilir.
  • Bu örnekte bir hafıza sınırı belirlenmemiştir. Yani indirilen firmware boyutuna bakılmaksızın hafızaya yazılmaktadır. Eğer 49 kb’tan büyük bir dosya indirmek istersek sistemde hata oluşacaktır. Bu yüzden bunun için bir hafıza sınırı belirlenebilir.
  • Bu örnekte ESP8266 AT komutları ile kullanılmıştır. STM32 üzerindeki kod yükünü azaltmak için ESP Arduino IDE si ile programlanarak kullanılabilir. Böylece güncelleme olup olmadığını ESP kendi kontrol edebilir ve eğer güncelleme var ise GPIO çıkışları ile STM32 yi uyararak güncelleme moduna sokabilir. Bunun için STM32-ESP arasında iyi bir UART haberleşme stratejisi kurularak firmware dosyası stm32’nin hafızasına yazılmalıdır. Böylece IAP kodlarının yükü ESP ve STM32 üzerinde paylaştırılmış olur ve IAP kodları STM32’nin flash hafızasında daha az yer kaplar.

Kaynaklar

  1. STM Application Note (AN3226)
  2. STM Application Note(AN3965)
  3. Python libscrc module

Mehmet Topuz

25 Comments

  1. Mehmet bey merhabalar, elinize sağlık güzel bir çalışma olmuş.Ben bu işlemleri yine stm32f103c8 kullanarak fakat internet arayüzü için w5500 kullandım. Göndereceğim data paketleri hakkında tam emin olamadım.
    Örneğin sizin esp’ye;
    TxBufferLength = sprintf(txBuffer,”%c%s%c%s%c”,(char)opcode,filename,(char)0,TransferMode,(char)0);
    HAL_UART_Receive_DMA(&huart1,(uint8_t *)rxBuffer,10);
    HAL_UART_Transmit(&huart1(uint8_t*)txBuffer,
    sprintf(txBuffer,”AT+CIPSEND=%d\r\n”,
    TxBufferLength),1000);

    şeklinde kodlar yüklemişsiniz. Benim anladığım buradan esp’ye ‘UDP’,’192.168.1.100′,69\r\n\0 bu şekilde bir kod gidiyor. Bu kodlar direkt tftp request kod içeriyor mu yoksa esp ile ilgili bir kod mu?

    • Merhabalar,
      Burada ESP ‘ye önce AT+CIPSTART=”UDP”,”192.168.1.100″,69 şeklinde bir string göndererek servera bağlanıyorum. Daha sonra paketleri oluşturmak için yine sprintf fonksiyonunu kullanıyorum. Örneğin; sprintf(txBuffer,”%c%s%c%s%c”,(char)opcode,filename,(char)0,TransferMode,(char)0) ifadesi ile read request paketini txBuffer dizisine kaydediyorum. Aynı zamanda bu fonksiyon bana bu diziye kaydettiğim verilerin uzunluğunu veriyor. Esp ile servera bağlandıktan sonra atıyorum AT+CIPSEND=20 şeklinde bir komut gönderiyorum. Burada 20 sayısı CIPSEND komutundan sonra kaç byte daha gönderileceğini belirler. Yani esp benden 20 byte lık bir veri bekler. Ben burada 20 yerine sprintf fonksiyonun döndürdüğü değeri kullanıyorum. Çünkü paket içindeki filename uzunluğu değişkenlik gösterebilir. Eğer esp bana > karakterini gönderip bu verileri almaya hazır olduğunu söylerse hemen arkasından yukarıda bahsettiğim gibi sprintf fonsiyonu ile paketin hepsini tekrar txBuffer a kaydedip bu bufferı uart ile esp ye gönderiyorum.

  2. merhaba , tftp server dan illegal TpFP operation cevabı alıyorum
    gönderdiğim komut
    0x00
    0x01
    semih.bin
    0x00
    octet
    0x00

    sizde benzer birşey göndermişsiniz ama hata alıyroum
    ve buradaki octet nedir
    teşekkürler

    • Merhaba, gönderdiğiniz verilerin sırası doğru. Verileri dizi içerisine yukarıdaki gibi mi kaydediyorsunuz yoksa farklı bir şekilde mi? Tftp64 programından dosya yolunu veya ip belirtmeyi unutmuş olabirsiniz bir kontrol edin. Almak istediğiniz semih.bin bu dosyanın içinde olsun. Göndermek istediğiniz diziye debug ekranından bakarak karşılaştırın.
      Buradaki octet octo(latince 8 den) gelir.8 bitlik diziler için kullanılan bir terim.

      • diziyi yukarıdaki gibi gönderiyorum
        Aynı komutu esp ttl adaptör ile gönderince
        AT+CIPSEND=18

        OK
        > semih.binßøÐð….
        …..FF€FF&ø[-±ðNøh@]
        +IPD,26:Undefined error code
        şeklinde cevap geliyor, sonrasında ack göndermediğim için timeout a düşüyüor server.
        stm le debug da bakınca illegal operation alıyorum.

        • Bloğu daha almadan ack gönderiyorsun galiba. Bloğu okuduktan sonra ack göndermeden önce 100 veya 200 ms kadar bir gecikme ekleyip dener misin? Bu dosya indirme hızını biraz yavaşlatacaktır.

        • Tftp64 programında eğer ack gelmediyse veriyi atıyorum 3 kez tekrar gönder diye bir seçenek olması lazım onu da kontrol et.

          • sorunu anladım hocam teşekkürler usart ile gönderirken pointer ı arttırmadığıum için hep 0x00 göndermişim.
            ack kısmına şimdi bakacağım
            ,her 512 lik blok tan sonra mı ACK göndermeliyiz

        • Rica ederim. Evet her bloğu aldıktan sonra ack göndermen gerek yoksa server sonraki bloğu göndermez.

  3. Merhabalar Mehmet bey, bu konuyu yeni yeni öğreniyorum. Paylaşımınız çok faydalı olmuş elinize emeğinize sağlık. bi konuda kafama takılan bir soru var, aslında sormaya da çekiniyorum. Iap kodları için şöyle bir yöntem düşünülebilir mi ? ya da böyle yapmak mantıklı mı? usart kullanarak seri olarak hex datalarını veya .bin uzantılı dataları sırasıyla 08003000 adresinden itibaren byte byte flash hafızaya yazdısak ve reset attıktan sonra jump işlemini yapsak doğru bir algoritma olur mu ? konuda yeni oldugum ıcın cok bılgım yok.

    • Merhabalar,
      Öncelikle yorumunuz için teşekkür ederim. Sorunuz gayet güzel ve bunda çekinecek bir şey yok.
      Bu örnekte jump işlemi bootloader kodları içerisinde yapılıyor. Flash hafızanın ilk başında bootloader kodları bulunuyor ve bu yüzden mikrodenetleyici her resetlendiğinde önce bootloader çalışıyor. Şimdi sizin söylediğinize gelirsek; bootloader çalıştı ve dosyayı hafızaya yazdı bu aşamadan sonra yeni programa atlamak yerine reset atılmasını bekleyecek. Daha sonra reset attığımızı farz edelim. Bu sefer yine bootloader çalışacak ve tekrar aynı dosyayı indirip hafızaya yazacak(eğer versiyon sorgulama vb yapmadıysak) ve tekrar reset atılmasını bekleyecek. Böyle bir durumda sistem çıkmaza girecektir ve yeni program nasıl çalıştırılacaktır? Yani bir yerlerde jump işlemini yapmamız gerekiyor. Dediğiniz yöntem yapılamaz demiyorum ama burada tekrar reset atmak çok mantıklı değil gibi sanki. Bir kere reset atıp güncelleme yapmak varken iki kere reset atmak (hem güncelleme öncesi hem de sonrası) çok mantıklı değil yani. Bootloader kodunda ufak değişiklikler yaparak sizin söylediğiniz yöntem de kullanılabilir. Örneğin; bootloader çalışır versiyon kontrolü yapar eğer yazılım güncel ise indirir kaydeder ve indirme işleminin bittiğine dair kullanıcıya bir bilgi verir(seri arayüz, ekran üzerinden vb.). Kullanıcı indirmenin bittiğini anlayıp reset atar ve bootloader tekrar çalışır ve yeni yazılım güncel olduğu için hiç beklemeden yeni yazılıma atlar. Böyle bir senaryo da düşünülebilir ama indirdikten sonra otomatik olarak yazılıma atlamak daha mantıklıdır.

      • Hocam cevabınız için teşekkürler, bir soru daha sormak istiyorum, siz bin dosyasını kullanarak yukleme yapmışsınız, internette baktıgım birkaç projede de aynı şekilde bin dosyası aktarılmış. ben bu işlemi hex dosyası ile yapmak ıstıyorum bır yere kadar geldim. hex dosyasında ilk data satırında şu yazıyor :103C000010040020053D00080
        B46000895450008FB
        burada ilk 2 byte data boyutunu ifade ediyor, sonraki 4 byte yazılacak olan adresi… sonraki 2 byte yazılacak datanın başladığını ifade ediyor, sondaki 2 byte ise check sum… benim anladıgım kadarı ile 7.byttan checksuma kadar olan kısmı ben byte byte fash adrese yazdırmam gerekiyor… bunu daha önce pic ile yapan tanıdıklarım var, detaylı bilgiyi onlardan aldım ancak bunun stm32 için olup olmayacağını bilmiyorum. bilginiz varsa bu konuda yardımcı olabılır mısınız. yoksa sadece bin dosyasını mı yukllemem gerekli…

        • Rica ederim. Evet hex dosyası ile de güncelleme yapılabilirsiniz. Hex uzantılı dosyayı satır satır işleyip sadece data bytler’ını hafızaya yazmanız yeterlidir. Burada hafızaya yazarken satırlardaki data byte’larını ardı ardına yazmanız gerekmektedir. Binary dosyayı yazmak hex’e göre daha kolaydır. Sadece data byte’ları yer alır. Hex dosyasını kullanacaksanız data byte’larını çok dikkatli çekmeniz gerekmekte. Burada checksum kontrolünü her satır için yapacaksınız. Şöyle bir yöntem de uygulayabilirsiniz. Dosya paketleri kullandığınız haberleşme çevrebiriminden(uart,spi vb) hızlı bir şekilde gelecektir. Burada her satır geldiğinde işleyip data byte’larını çekip hafızaya yazmak gecikmelere sebep olur ve bir sonraki satırın kaçırılmasına sebep olabilir. Bu yüzden hafızanın başka bir adresine(tabi yeterli hafıza var ise) hex dosyası olduğu gibi yazılıp, indirme bittikten sonra içerisinden data byte’ları çekilip asıl istenen adrese data byte’ları yazılabilir. Hex dosyası ile güncelleme yapmak her mikrodenetleyicide yapılabilir. Yani sadece PIC’e veya STM32’ye özgü değildir.

  4. Mehmet Bey merhabalar,
    Öncelikle paylaşımınız için çok teşekkürler. Size CRC ile ilgili bir şey sormak istiyorum. Yazınızda,
    ” Serverdan binary dosyayı indirirken sonundaki CRC ile birlikle flash hafızaya yazıyorum. Flash hafızadan aşağıdaki fonksiyon ile binary dosyanın CRC’sini çekiyorum. Yukarıda farkettiyseniz 4 byte’lık CRC değerini little-endian formatta dosyanın sonuna ekledik.” demişsiniz. Python ile dosya sonuna tüm datalar için tek bir CRC kodu mu ekliyorsunuz? STM32 için ise ilgili kod da her 32 bit lik data için bir CRC hesaplanıyor. Python daha önce kullanmadığım için inceleme fırsatım olmadı. Pythonda da her 32 bit için bir CRC oluşturmadan nasıl tek CRC kodu ile STM32 tarafındaki her 32 bitlik data için oluşturulan CRC kıyaslanıyor tam anlayamadım. Bu konuda yardımcı olursanız çok sevinirim. Şimdiden teşekkür ederim…

    • Merhabalar, rica ederim.
      Evet python tarafında tüm datalar için tek bir CRC ekleniyor.
      CRC32 = libscrc.fsc(BinData) satırında fsc() metodu CRC32 Ethernet standardına göre bir çıktı veriyor. Yani aslında bu metod içerisinde yine 32 bitlik değerler CRC işlemine tutuluyor ve bu metodun döndürdüğü değer bütün binary dataların CRC’si olmuş oluyor.

  5. Mehmet hocam merhaba, uzun süredir yazı paylaşmıyorsunuz bir sıkıntınız yoktur inşallah. Lütfen bizi güzel yazılarınızdan mahrum bırakmayın:(

    • 🙂 Araya askerlik girdi sonrasında işten güçten çok fırsat bulamadım. Şuan Unit test ile ilgili bir yazı hazırlıyorum. Daha sonrasında TDD( Test Driven Development) ve Kriptografi ile ilgili güzel bir yazı paylaşmayı düşünüyorum. Bir iki ay içerisinde güzel yazılar gelecek. Takipte kalın.

  6. Merhabalar hocam size bir sorum olacak? Diyelim ki ; müşterinin elinde bir cihaz var içinde bootloader yüklü durumda. Kod koruması da aktif edilmiş. Bir problem çıktı ve bootloader i uzaktan güncellemek gerekli, bunu nasıl yaparsınız?

    • Böyle bir durumla karşılaşmadım daha önce. Zaten bootloader programı çok küçük ve basit bir işlemi yapan program. Bir kere yazılıp yüklenecek ve hep orada kalacak. Çok bir güncelleme isteği duyulmayacak bir program. Sizin bahsettiğiniz gibi bir durumda ben olsam şöyle bir yöntem uygulardım. Tamamen aklıma gelen ilk fikir bu belki başka bir yöntemi vardır bilemiyorum. Böyle bir durumda flash hafızada bootloader programını iki farklı adreste tutardım. Yani aynı programdan iki tane olurdu. Birini sadece bootloader’ı güncellemek için kullanırdım. Eğer güncelleme bootloader için gelmişse normal olarak çalışan bootloader diğerini güncellerdi. Hemen ardından güncellenen bootloader bu sefer diğerini güncellerdi. Böylece iki bootloader da güncel olurdu . Tabi böyle bir kullanım var mıdır bilemiyorum. Kod korumasına gelince kod koruması kod ile açılıp kapatılabiliyor. Bootloader çalışmadan önce kod koruma kapatılıp, bootloader işini bitirince kod koruması tekrar aktif edilebilir diye düşünüyorum.

  7. Mehmet Bey merhaba, elinize sağlık çok güzel bir yazı olmuş. Ben bir stm32f429zi kartı ile ethernet üzerinden tftp IAP yaptım. Bu projeye sizin yazınızdan faydalanarak checksum ekledim, Python üzerinden .bin dosyasına CRC ekledim ve gönderdiğim uygulama çalışıyor. uart ile aşşağıdaki printf çıktılarını alırken

    uint32_t CRCofFile = getCRCfromFile();
    printf(“CRC of the bin file: 0x%x\n”,CRCofFile);
         
    uint32_t CRCofFlash = CalculateCRC();
    printf(“CRC of the flash memory: 0x%x\n”,CRCofFlash);

    kısmında CRCofFile ve CRCofFlash “0xFFFFFFFF” değerini gösteriyor ve anladığım kadarı ile bu şekilde if döngüsü içine giriyor. Sanırım burada bir sakıntı var. Bunun sebebi ne olabilir?

    • Merhaba rica ederim.
      Öncelikle flash hafızadan okunan değerin 0xff olması o hafızanın boş olması anlamına gelir. Yani flash’a yazma yaparken sorun yaşıyor olabilirsiniz. Bu yüzden CRC işlemine .bin uzantılı dosyanın boyutu kadar 0xff değerini girip bunun sonucunda çıktı olarak 0xff alıyor olabilirsiniz. Tavsiyem program debug altında iken binary dosyayı yazdığınız hafızayı memory ekranından izleyin. Gerçekten istenilen adrese yazılmış mı? Bir başka sorun benim yazdığım CRC hesaplama ve dosyanın sonundaki CRC’yi okuma fonksiyonu eğer AppSize değişkeni sıfır ise 0xFFFFFFFF değerini döner. AppSize global değişkeni IAP.c içerisinde tanımlı ve yine IAP.c içindeki ReceiveHandler() fonksiyonu tarafından arttırılıyor. Yani eğer flash hafızaya yazmada bir hata var ise ve AppSize değişkeni artmıyor ise getCRC fonksiyonları 0xFFFFFFFF dönüyor olabilir. Diğer yandan python tarafında CRC hesaplama sonucunun 0xffffffff den farklı olması gerekir. Eğer farklı ise getCRCfromFile() fonksiyonu 0xffffffff değerini dönüyorsa burada yüksek ihtimalle flash write veya read kısımlarında sorun var gibi geliyor. O kısımları kontrol edebilirsin.
      Ayrıca stm32’nin her serisi için flash key değerleri farklıdır. O kısımları ilgili stm32’nin flash programming manual dökümanından faydalanarak ilerlemelisin. Yani birebir aynı uygulama F4 serisinde çalışmaz. Örneğin F1 serisinde flash hafızanın bölümleri page iken başka serilerde sector olabiliyor. Bu yüzden benim yaptığım uygululamanın birebir aynısını kopyalama, bu uygulamayı kendi denetleyicine göre uyarla. Burada amaç balık tutmayı öğretmek:) Bu kodu benim gibi yazma benden daha iyi yaz. Bu koda şimdi bakınca keşke şuraları şöyle yazsaymışım diyorum. Belki birgün geliştiririm bu kodu yeni özellikler ekleyerek.
      Yorumu fazlada uzatmayayım. Umarım sorununu bulmanda yardımcı olur bu yorum. Kolay gelsin…

  8. Hocam merhaba bu çok değerli bir konu yazınız için teşekkür ederim ama bu konuyu daha detaylı bir şekilde videolu olarak anlatma imkanınız var mı ya da udemy gibi bir platformda ders olarak çekseniz sadece ST kartları için değil ayrıca diğer işlemciler için nasıl yapabiliriz gibisinden.

    • Merhaba İlker,
      Şuan için udemy gibi platformlar için eğitim çekmeyi düşünmüyorum. Bu yazıları da yoğunluktan dolayı öyle bir iki günde yazamıyorum. Haftada birkaç gün birer saatlik çalışmaların toplamında belki bir ay, belki de iki ay sonunda çıkıyor. Bu yüzden youtube için bile olsa bir günde video çekebilecek zaman ayıramayabilirim diye düşünüyorum. Belki ileride video için ekipmanlar alıp video da çekebilirim ama dediğim gibi şuan için öyle bir düşüncem yok malesef:(

  9. Merhaba, yazılım güncelleme ile yüklenen kod başkası tarafından okunabilir mi(code protection)? Teşekkürler.

    • Merhaba, read-out protection ayarları yine aynı şekilde geçerli olur diye düşünüyorum. Yani okumaya karşı koruma ayarlarını yaptığınızda flash’ı okuyamacağı için hem bootloader hem de application kodunu okuyamaması lazım. Tam bir korumanın mümkün olmadığını söylüyorlar ama o konuları çok bilmiyorum. Üreticinin tavsiye ettiği koruma ayarlarını yapıyorum sadece. Başka nasıl bir yöntem izlenebilir bilmiyorum malesef

Mehmet Topuz için bir cevap yazın Cevabı iptal et

E-posta hesabınız yayımlanmayacak.