動手寫一個STM8的輕量級bootloader

small野狼發表於2017-06-30

STM8憑藉其低廉的成本、超高的價效比獲得了許多公司的青睞。而在產品中由於方便、安全等需求,往往要使用到IAP下載的方式對已經拿到產品客戶進行軟體升級(如果產品在批量生產後發現你的程式有問題,而不能IAP更新,那售後維護成本就高了)。
而STM8S003等只有8K Flash的型號是不自帶Bootloader的,且ST官方的Bootloader足足會佔領4KB Flash空間,且還要考慮韌體的保密性等因素,所以編寫一個自己的輕量級的Bootloader尤為重要。

下面向大家分享一下本人在一個專案中寫的一個Bootloader,最少只需佔用0.5KB Flash空間!

【串列埠收發】

對於微控制器而言,Bootloader最重要的功能就是把從串列埠傳送過來的程式資料儲存到MCU 的Flash上(即IAP下載),並跳轉到所下載程式的起始地址並執行。所以串列埠功能必不可少。下面是我們用到的關於串列埠收發的函式(標頭檔案新增stm8s.h,直接操作暫存器以節省Flash空間),MCU型號為STM8S003。

void UART1_SendByte(u8 data) //串列埠發一個位元組
{ 
    UART1->DR=data;
    while (!(UART1->SR & 0x80));//等待傳送完成
}
void UART_Init(void) //串列埠初始化函式
{     
    UART1->CR2 = 0;                       
    UART1->CR3 = 0;// b5,b4 = 00,1個停止位
    UART1->BRR2=0x00;
    UART1->BRR1=0x1a;
    //BRR2和BRR1設定串列埠波特率,這裡設定的在2MHz默頻下是4800的波特率
    UART1->CR2 = 0x2C; // b3 = 1,允許傳送
    UART1->CR1 |= (0<<5);
}
void UART1_SendStr(u8* data)//傳送字串函式
{
    while (*data)
        UART1_SendByte(*data++);
}
u8 UART1_RcvB(void)//串列埠接收函式,以掃描(等待)方式
{
     while(!(UART1->SR & (u8)UART1_FLAG_RXNE));//等待接收到資料
         return ((uint8_t)UART1->DR);
}

有小夥伴就會問了,串列埠接收為何不用中斷?因為STM8沒有STM32那樣的向量地址的偏移,因此你的APP(正式的程式)和Bootloader中只能有一個使用中斷。當然也有同時都可以用中斷的方法,但是本人愚見,覺得實現基本的下載升級功能用不著中斷,新增中斷還會增加Bootloader的程式碼量不是?不過如果真的需要,CSDN論壇有一篇《集合帖:STM8之支援中斷方式的IAP技術實現》的帖子大家可以搜來看看。

【Flash的寫入】

查閱Datasheet可以知道STM8S003 FLASH的起始地址是0x8000,到0x9fff結束共8KB,每64個位元組在一個Block(塊)。而我這裡的Bootloader放在開頭,即從0x8000開始,這樣上電預設就進入bootloader。那麼APP程式放在哪裡呢,有空的地方就可以放~,比如你想把bootloader的存放空間預留1KB,那麼你的APP就從0x8400開始存放,0x8400-0x9fff共7KB空間可供你程式設計。如果你IAP功能比較精簡,沒有資料加密、校驗什麼的,0.5KB就夠,那麼甚至可以把APP起始地址提到0x8200。
下面是個寫一個塊的Flash的函式,因為寫Flash時程式不能在Flash中執行,故函式在RAM中執行。

//寫Flash函式,addr地址必須是每個Block的開頭(0x40的倍數)
IN_RAM(void FLASH_ProgBlock(uint8_t *addr, uint8_t *Buffer))
{
    u8 i;    

    FLASH->NCR2 &= (uint8_t)(~FLASH_NCR2_NPRG);
    FLASH->CR2 |= FLASH_CR2_PRG;
    for (i = 0; i < 64; i++)//一個塊共寫64個位元組
    {
        *((PointerAttr uint8_t*) (uint16_t)addr + i) = ((uint8_t)(Buffer[i]));
    }
}

在寫Flash之前別忘了對Flash解鎖

#define FLASH_RASS_KEY1 ((uint8_t)0x56)
#define FLASH_RASS_KEY2 ((uint8_t)0xAE)//巨集定義新增

    FLASH->PUKR = FLASH_RASS_KEY1;//程式碼新增,解鎖Flash程式區
    FLASH->PUKR = FLASH_RASS_KEY2;

最常見的IAP方式是在剛上電時等待一定時間檢測有沒有串列埠命令,有就進入下載模式,沒有就跳到APP,以下是最基本的IAP實現:

int main(void)
{           
    u16 j = 0;
    u8 ch, high, low;
    u8 buf[64]; //接收緩衝區

    asm("sim"); //關閉總中斷使能
    UART_Init();
    UART1_SendStr("START\r\n");//傳送開機提示

    while (j < 50000)//等待迴圈這麼長時間
    {
        if(UART1->SR & (u8)0x20)    //如果串列埠收到資料
        {
            ch = (uint8_t)UART1->DR;
            if(ch == 0xa5) break; //收到了0xa5,則進入下載模式
        }
        j++;
    }

    if (j == 50000)//迴圈是超時退出的
    {
        asm("JPF $8400");//跳到0x8400這個地址去執行APP。
    }      
    //unlock flash,解鎖flash
    FLASH->PUKR = FLASH_RASS_KEY1;
    FLASH->PUKR = FLASH_RASS_KEY2;
    UART1_SendStr("pro\r\n");
//傳送開始程式設計提示
    while(1)
    {
        high = UART1_RcvB();
        low = UART1_RcvB();
        addr = (u8 *)((high << 8) | low);//先接收到本次資料16位的起始地址
        if (addr == 0)//如果收到的是0,就是下載結束了
        {
            UART1_SendStr("OK!\r\n");//傳送下載完成提示給上位機
            FLASH->IAPSR &= FLASH_MEMTYPE_PROG; //鎖住flash
            asm("JPF $8400");//跳轉到0x8400執行剛下載完的APP
        }
        for (i = 0; i < 64; i++)
            buf[i] = UART1_RcvB();//接收64位元組資料
        FLASH_ProgBlock(addr, buf);//將收到的資料寫到相應地址的Flash
    }
}

上述程式使用IAR編譯,僅用不到0.5KBFlash就實現了最基本的IAP功能,當然,下載程式需要使用協議與之匹配的上位機。

【中斷向量重對映】

使用上述bootloader程式下載app程式後你會發現中斷無法使用,因為STM8的預設中斷向量地址是在0x8000-0x8080,stm8編譯出的二進位制檔案會把向量表放在前0x80個位元組。即如果在app中使用了中斷,那麼程式就會跳回bootloader程式中去了,解決這個問題的方是進行中斷向量重對映。

在bootloader程式main.c檔案的函式之外新增以下程式碼:

__root const long reintvec[]@".intvec"= 
{
0x82008080,0x82008404,0x82008408,0x8200840c, 
0x82008410,0x82008414,0x82008418,0x8200841c, 
0x82008420,0x82008424,0x82008428,0x8200842c, 
0x82008430,0x82008434,0x82008438,0x8200843c, 
0x82008440,0x82008444,0x82008448,0x8200844c, 
0x82008450,0x82008454,0x82008458,0x8200845c, 
0x82008460,0x82008464,0x82008468,0x8200846c, 
0x82008470,0x82008474,0x82008478,0x8200847c, 
};//當應用程式地址不是0x8400時則要相應改掉除第一個0x82008080以外的數值 

便完成了中斷向量的重對映,這裡對映到了0x8400-0x8480,即APP程式的中斷向量表存放區。
但是這時候你的IAR編譯器會報錯:
這裡寫圖片描述
中斷向量空間不夠?當然,因為加入了重定向陣列。

接下來進入IAR設定下,Linker的Config下,可以看到使用了inkstm8s003f3.icf 這個配置檔案:
這裡寫圖片描述

我們用文字編輯器開啟:
這裡寫圖片描述
這不就是報錯的INTVEC size嗎,把它從0x80改成0x100,並儲存,再回IAR編譯就完美通過了!

【APP程式的設定】

現在有bootloader了,APP存放的地址改變了,那麼APP中一些關於地址的量也需要跟著改變,換句話說就是編譯器編譯出的地址也要變。所以你需要將上面改INTVEC size大小的那張圖改成下面這樣再對APP進行編譯:
這裡寫圖片描述
嗯,所有的0x8000都改成了0x8400(APP程式的起始地址),中斷向量表佔用空間為0x80。在這樣設定、儲存編譯後,APP通過bootloader下載進去就可以用了。

【資料的校驗】

前面我們的bootloader程式是收到位元組便馬上寫入Flash,寫完後直接執行,可是萬一出現串列埠多收/漏收/錯收或者是Flash寫入失敗的情況怎麼辦?那就…

void get_flash_verify(u8 *add)//傳送校驗碼的函式
{
  u16 i;
  u8 ch;
  u8 verify = 0;

  for (i = 0; i < 64; i++)
  {     
      ch = *((PointerAttr uint8_t*) (uint16_t)add + i);//讀取flash並累加
      verify += ch;
  }
  UART1_SendByte(verify);
}

此函式讀取了add地址開始的64位元組Flash的值,並將他們累加,相加完的和取低8位得到一個校驗碼,並將該碼通過串列埠傳送出去。此函式在微控制器每次寫完一個塊的Flash後呼叫,上位機將這個碼與自己的資料算出的值進行比對便實現了校驗,如果校驗不正確,上位機便可重新傳送命令寫該快。


未完待續

相關文章