當你熟悉了程序的仿真和下載,你就應該了解一下IAP了。本質上IAP和單片機內部固化的ISP程序一樣,都是負責幫你把新程序下進單片機的FLASH。那為什麼還需要IAP呢?
舉個例子,ISP的啟動一般需要硬體控制BOOT0,因此對於加USB轉TTL和三極體電容電阻等組成自動下載電路這種燒錢做法一般做產品肯定是不提倡的。而即便是不使用自動下載電路,也需要你手動去設置這個管腳,而產品批量的版本BOOT0一般都是直接通過電阻接地甚至電阻也省略了。所以很難做到像開發板一樣輕易使用ISP。這還是在硬體方面,你還需要抱著電腦連上位機去發送hex或者bin文件。即便是用下載器,不還是需要接線連上SWD接口才能下載嘛。
那如果是產品已經裝殼了呢?如果還是那種膠封的防水外殼,那麼這時候拆殼下載很明顯外殼就報廢了。而基本上產品也沒有把下載接口引出來的,比如你家的路由器、WIFI燈泡等等。再例如如果是太陽能路燈控制器呢?如果是池塘水質檢測器呢?總不能下一次程序抱電腦爬一次路燈杆、劃一次水吧。雖然也有很多的脫機下載器了,但是終究還是需要連接產品去更新。如果是傳感器節點的話可能同時還會有成百上千個產品需要下載程序。
因此出現bootloader是必然的過程。通過前面的內容,我們知道下載新程序的辦法主要是兩個,ISP和仿真器。操作都比較麻煩。一個是系統存儲區程序,一個是RAM存儲區程序。那麼IAP就是主存儲區程序了。
IAP就是In Application Programming,也還是編程,也就是寫FLASH程序。對於主存儲區(也就是0x0800 0000那塊存儲)裡面存儲的就是用戶程序(User Application)了。那麼In Application Programming也就是在這塊區域寫FLASH。
前面我們分析過,程序就運行在這裡,那麼再寫新程序的話,無疑會把自己寫掛掉。而IAP又無疑是在主存儲區運行的。那怎麼辦呢?
我們首先翻到復位中斷入口處的程序。
復位程序入口
然後跳轉到系統初始化程序SystemInit,在這裡我們可以看到中斷向量表偏移的設置。
中斷向量表偏移量設置
由於工程中未啟用宏:VECT_TAB_SRAM
MDK工程宏定義
所以執行下方的語句:SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;翻譯過來就是:內部FLASH中的向量表重定位。
FLASH基地址定義
說人話就是中斷向量表設置的0x0800 0000,也就是我們的程序運行的位置。
我們知道中斷函數也是個函數,並且中斷函數在發生中斷時可以及時響應運行。那麼CPU調用中斷函數的時候就得知道中斷函數的入口地址,而MDK完全不知道你用的什麼單片機、有幾塊存儲、多大存儲、存儲在哪(FLASH地址)等等。所以需要設置這些內容,當然這些東西你當初裝的pack包都幫你做好了。
其實如果程序是從0地址(0x0000 0000)運行的話那就不需要偏移,但是STM32的FLASH是0x0800 0000,所以這個入口地址要加上0x0800 0000(關於為什麼stm32的FLASH地址為什麼是0x0800 0000可以查看安富萊的帖子:https://www.bilibili.com/read/cv13767372),而如果是F7的XIP模式,那麼外置FLASH的地址還需要設置,比如F730、F750就是0x0900 0000。
DM00514974.pdf 13頁
欸,不是說IAP嘛,怎麼又扯到了中斷向量表?因為存在中斷像量表,並且是可以設置的,所以才具有了靈活性。比如我們給它往後挪一下呢?
比如我們把中斷向量表設置成0x0800 0000 | 0x10000,也就是FLASH_BASE | VECT_TAB_OFFSET,定義VECT_TAB_OFFSET為0x10000,這時候程序就在FLASH基地址也就是0x0800 0000往後64K的地址跑起來了。
是這樣嗎?並不是。從主存儲區啟動的時候程序依舊是從主存儲區地址:0x0800 0000開始跑的。對勁嗎?不對勁,不對勁嗎?好像又對勁。如果電腦這麼設置可以的話,那系統崩了是不是就沒法重裝系統了。所以從主存儲區啟動是對的,但是目前還沒法跑挪過去後面的那段程序。
再回想一下中斷怎麼執行來著?是要知道中斷函數的入口地址對吧。那麼我們也這麼做。首先定義一個函數指針,然後把函數指針指嚮往後挪的那段程序。是0x0800 0000 + 0x10000嗎?不是的。
回顧一下你的開發板上電第一個跑的復位中斷程序,也就是Reset_Handler。它的地址在哪呢?
中斷函數偏移地址
是的,它在4位元組的位置,也就是一個32位數之後。所以你的新程序的地址還要+4。
typedef void (* iapfun)(void);iapfun go_app;go_app = (iapfun)*(__IO uint32_t *)((0x0800 0000 | 0x10000) + 4);__set_MSP(*(__IO uint32_t *)(0x0800 0000 | 0x10000));go_app();新的程序還需要新的堆棧,因此再設置一下堆棧指針。之後直接調用函數就可以了。
棧頂指針也就是程序最開始的位置,即0x0000 0000,比如像這樣。
棧頂地址
同樣的,往後4個字節就是復位中斷函數入口地址。
復位中斷入口地址
這些說明在Cortex權威說明手冊裡有寫。
Cortex M3/4權威指南114頁
我們的程序不是存儲的0x0800 0000嗎?為什麼在0這裡也可以查到呢?這就是存儲區重映射的。其實在0x0800 0000也是一樣的。
系統存儲區數據
好了,到現在為止,我們已經可以通過主存儲區先啟動的程序來創建函數指針然後讓往後挪的程序跑起來了。還有一個問題,怎麼把程序寫進往後挪的那個位置呢?這其實就比較簡單了。之前我們說的FLASH編程就行。把新程序編譯出來bin文件。然後再用首先啟動的這段程序把新程序寫到往後挪的那個位置。
FLASH構成
最簡單的寫法,把新程序存成數組,這樣第一段程序就包含新程序了,再往後面寫一遍屬實沒必要,所以一般不這麼淦。
反正還是把新程序變成數組寫進FLASH,那方法可就多了去了。跟ISP一樣,用串口把新程序數組發給第一段程序讓它寫到後面。這樣就行了。又或者你用SPI、CAN、USB等等,或者直接用文件系統,讓第一段程序從SD卡或者SPI FLASH裡面直接讀取bin文件寫到後面。當然如果你用了文件系統,或者屏幕顯示,那麼可能第一段程序就比較大了,這時候就得把新程序再往後挪一下了,比如0x0801 2000。如果沒有串口、USB、卡槽呢?比如你的WIFI燈泡,對的,你還有WIFI和藍牙,說白了它倆不也是串口嘛,所以你還可以無線更新,還是串口。
這樣算一個IAP了嗎?還不行,為什麼呢?總不能每次插電都寫一次FLASH吧,FLASH寫壽命相對來說還是比較短的。這時候你可以做個標誌位,寫好之後就標記一下,下次上電開機就不再寫了。但是,你如果還想再接著更新新的程序呢?顯然這時候又不行了。
比較常用的做法是,正常情況下不更新,比如100次開機可能99次都是正常使用。那什麼時候不正常呢?正常開機怎麼開呢?懟一下開關或者長按開機對吧,如果還有其他按鍵,我們可以讓第一段程序檢測是不是開機和其他的按鍵一起按下去了,是的話就更新,不是的話那就是正常開機,不去更新程序就是了。比如你的手機開機的時候和電源鍵一起長按音量-就可以刷機。
Bootloader流程
對了,我們的標題不是Bootloader嗎,怎麼說了一堆的IAP呢?其實實現IAP功能方便我們更新程序的這個程序就是Bootloader,由它來引導新程序的運行。它總是最先運行的,因此才能引導和更新別的程序。就和linux的uboot、windiws的BOOT一樣,它在一開機的時候就跑起來了,如果你快速點del按鍵就能讓它不引導windows,這時候你就可以裝新系統了。
再來歸納一下Bootloader,首先它是系統運行時跑的第一個程序。在stm32單片機中就是0x0800 0000這裡的程序。其次如果沒有更新需求(比如沒有特殊組合按鍵按下),它就引導新的程序,就是跳轉過去執行。如果有呢,它就從別處更新新的程序,然後再跳轉過去引導剛更新過的程序。Bootloader還需要知道自己的體積,把新程序寫在自己的後面。或者再往後寫一點留一點空間,比如後期需要換成SD卡更新呢,可能之前的空間就不夠了。
對於新的程序,需要知道自己的位置,然後把中斷向量表偏移過去,也就是自己存儲的首地址。就像bootloader和普通的程序同樣都是偏移到0x0800 0000一樣。而新程序還要在此基礎上加上自己存儲的偏移量。比如0x0801 0000。
至此我們才設計完成了一個完整的最簡單的IAP。
在此基礎上,你還可以設計多個新程序,存儲在bootloader之後的不同的位置,選擇性跳轉,想跑哪個跑哪個,想怎麼跑怎麼跑。或者你還可以在新程序中設置變量,讓bootloader重啟的時候根據這個變量來更新,又或者讓bootloader自己搜一下SD卡或者USB接口什麼的甚至聯網查一下有沒有新的程序,有的話就讓它自己更新。
最後值得注意的是中斷,因為bootloader是函數指針式跳轉,不是重啟,所以bootloader中有的中斷服務函數在新程序中找不到的話,可能中斷時找不到入口程序就崩了。因此在運行新程序的時候關閉所有的中斷,之後再跳轉。或者Bootloader程序直接觸發一下復位。即調用__NVIC_SystemReset。