STM32 IAP應用開發(fā)——通過串口/RS485實現(xiàn)固件升級(方式1)
什么是IAP?
IAP(In-Application Programming) 指MCU可以在系統(tǒng)中獲取新代碼并對自己重新編程,即可用程序來改變程序。在應用編程(IAP)是用戶的應用代碼對片內(nèi)Flash存儲器進行擦除/編程的方法。這種方式的典型應用就是用一小段代碼來實現(xiàn)程序的下載,實際上單片機的ISP功能就是通過IAP技術來實現(xiàn)的,即片子在出廠前就已經(jīng)有一段小的boot程序在里面,片子上電后,開始運行這段程序,當檢測到上位機有下載要求時,便和上位機通信,然后下載數(shù)據(jù)到數(shù)據(jù)存儲區(qū),從而實現(xiàn)固件升級。
什么是BootLoader?
百度百科:在嵌入式操作系統(tǒng)中,BootLoader是在操作系統(tǒng)內(nèi)核運行之前運行??梢猿跏蓟?a class="article-link" target="_blank" href="/tag/%E7%A1%AC%E4%BB%B6/">硬件設備、建立內(nèi)存空間映射圖,從而將系統(tǒng)的軟硬件環(huán)境帶到一個合適狀態(tài),以便為最終調(diào)用操作系統(tǒng)內(nèi)核準備好正確的環(huán)境。在嵌入式系統(tǒng)中,通常并沒有像BIOS那樣的固件程序(注,有的嵌入式CPU也會內(nèi)嵌一段短小的啟動程序),因此整個系統(tǒng)的加載啟動任務就完全由BootLoader來完成。
實際上,BootLoader不僅僅在操作系統(tǒng)上使用,在一些內(nèi)存小,功能應用較為簡單的單片機設備上面也可以通過BootLoader來完成固件升級。
我之前也有發(fā)過一些關于STM32遠程升級的文章,實現(xiàn)的方式有很多種,感興趣的同學可以去看一下。
那么這一期我來介紹一下如何自己制作一個BootLoader程序,并且通過串口或者RS485實現(xiàn)固件升級。
1 環(huán)境搭建
關于STM32以及Keil的環(huán)境這里就不具體介紹了,網(wǎng)上教程也很多,不懂的同學自行查閱資料。
2 功能描述
在做bootloader之前一定要先想好升級的途徑和方式,這樣才好規(guī)劃分區(qū)以及制作bootloader。
關于bootloader詳細的講解,可以看下我之前發(fā)的博客:STM32 IAP應用開發(fā)——自制BootLoader
分區(qū)介紹:
我用的是STM32F407,內(nèi)存是512K的(想用內(nèi)存更小的MCU也是可以的,改下各個分區(qū)的內(nèi)存分配就行了)。
注:F4系列的MCU不像F1那樣,內(nèi)存扇區(qū)都很大(最少也是16K),而且同一塊扇區(qū)只能一起擦除,所以就沒辦法分的那么細了。詳細的內(nèi)存分布可以參考下面的兩個圖。
STM32F4x扇區(qū)分布圖如下:
STM32F1x扇區(qū)分布圖如下:
那么我這里呢,就用一個512k的內(nèi)存,分成3個區(qū)域,來實現(xiàn)一個升級的功能。
分區(qū)表如下:
name | offset | size | function |
---|---|---|---|
boot | 0x08000000 | 0x00004000 | 存放boot程序 |
setting | 0x08004000 | 0x00004000 | 存放設備需要保存的一些參數(shù) |
app | 0x08008000 | 0x00078000 | 存放應用程序 |
方案介紹:
1)bootloader部分:
開始運行后先等待5s,在這個時間內(nèi)如果收到串口2或者RS485的升級命令就進入升級模式,如果超時則跳轉(zhuǎn)到用戶程序(APP)。
在升級模式,可以通過串口2或者RS485傳輸要升級的固件,傳輸?shù)臄?shù)據(jù)協(xié)議我這里圖方便就直接用Ymodem了,不知道Ymodem協(xié)議的可以先自行查閱一下資料。
2)APP部分:
APP部分修改一下中斷向量表地址即可,其他的隨便你做什么應用。
另外,我在分區(qū)的時候留了一塊settimg
區(qū),在實際的應該中如果有需要記錄一些掉電后還能保存的數(shù)據(jù),那么這塊區(qū)域就可以用得上了。
3 程序編寫
3.1 BootLoader部分
不管用的是什么MCU,要實現(xiàn)固件升級都離不開BootLoader,BootLoader是一個統(tǒng)稱,它其實只是一段引導程序,在MCU啟動的時候會先運行這段代碼,判斷是否需要升級,如果不需要升級就跳轉(zhuǎn)到APP分區(qū)運行用戶代碼,如果需要升級則先通過一些硬件接口接收和搬運要升級的新固件,然后再跳轉(zhuǎn)到APP分區(qū)運行新固件,從而實現(xiàn)固件升級。
BootLoader的制作需要根據(jù)實際的需求來做,不同的運行方式或者升級方式在做法上都是有區(qū)別的,包括BootLoader所需要的內(nèi)存空間也不盡相同。
不過不管是用什么方式,Bootloader都應該盡可能做的更小更簡潔,這樣的話內(nèi)存的開銷就更小,對于內(nèi)存較小的MCU來說壓力就沒那么大了。
注:我這里是基于正點原子的工程模板改的,增加了自己的功能。
示例代碼如下:
Bootloader分區(qū)定義:
#define FLASH_SECTOR_SIZE 1024
#define FLASH_SECTOR_NUM 512 // 512K
#define FLASH_START_ADDR ((uint32_t)0x8000000)
#define FLASH_END_ADDR ((uint32_t)(0x8000000 + FLASH_SECTOR_NUM * FLASH_SECTOR_SIZE))
//flash sector addr
#define ADDR_FLASH_SECTOR_0 ((uint32_t)0x08000000) //sector0 addr, 16 Kbytes
#define ADDR_FLASH_SECTOR_1 ((uint32_t)0x08004000) //sector1 addr, 16 Kbytes
#define ADDR_FLASH_SECTOR_2 ((uint32_t)0x08008000) //sector2 addr, 16 Kbytes
#define ADDR_FLASH_SECTOR_3 ((uint32_t)0x0800C000) //sector3 addr, 16 Kbytes
#define ADDR_FLASH_SECTOR_4 ((uint32_t)0x08010000) //sector4 addr, 64 Kbytes
#define ADDR_FLASH_SECTOR_5 ((uint32_t)0x08020000) //sector5 addr, 128 Kbytes
#define ADDR_FLASH_SECTOR_6 ((uint32_t)0x08040000) //sector6 addr, 128 Kbytes
#define ADDR_FLASH_SECTOR_7 ((uint32_t)0x08060000) //sector7 addr, 128 Kbytes
#define ADDR_FLASH_SECTOR_8 ((uint32_t)0x08080000) //sector8 addr, 128 Kbytes
#define ADDR_FLASH_SECTOR_9 ((uint32_t)0x080A0000) //sector9 addr, 128 Kbytes
#define ADDR_FLASH_SECTOR_10 ((uint32_t)0x080C0000) //sector10 addr,128 Kbytes
#define ADDR_FLASH_SECTOR_11 ((uint32_t)0x080E0000) //sector11 addr,128 Kbytes
#define BOOT_SECTOR_ADDR 0x08000000 // BOOT sector start addres
#define BOOT_SECTOR_SIZE 0x4000 // BOOT sector size
#define SETTING_SECTOR_ADDR 0x08004000 // SETTING sector start addres
#define SETTING_SECTOR_SIZE 0x4000 // SETTING sector size
#define APP_SECTOR_ADDR 0x08008000 // APP sector start address
#define APP_SECTOR_SIZE 0x78000 // APP sector size
#define BOOT_ERASE_SECTORS_NUM 1 // 16k
#define SETTING_ERASE_SECTORS_NUM 1 // 16k
#define APP_ERASE_SECTORS_NUM 6 // 16k + 16k + 64k + 128k + 128k + 128k
main函數(shù):
#include "bootloader.h"
#include "usart.h"
#include "rs485.h"
#include "delay.h"
#include "ymodem.h"
#define WAIT_TIMEOUT 5
void print_boot_message(void)
{
uart_log("---------- Enter BootLoader ----------rn");
uart_log("rn");
uart_log("======== flash pration table =========rn");
uart_log("| name | offset | size |rn");
uart_log("--------------------------------------rn");
uart_log("| boot | 0x%08X | 0x%08X |rn", BOOT_SECTOR_ADDR, BOOT_SECTOR_SIZE);
uart_log("| setting | 0x%08X | 0x%08X |rn", SETTING_SECTOR_ADDR, SETTING_SECTOR_SIZE);
uart_log("| app | 0x%08X | 0x%08X |rn", APP_SECTOR_ADDR, APP_SECTOR_SIZE);
uart_log("======================================rn");
}
void print_wait_message(void)
{
uart_log("------- Please enter parameter -------rn");
uart_log("[1].Start programrn");
uart_log("[2].Update programrn");
uart_log("--------------------------------------rn");
}
int main()
{
process_status process;
uint16_t timerout = 0;
delay_init(168);
uart_init(115200);
ymodem_init();
print_boot_message();
print_wait_message();
while (1)
{
process = get_ymodem_status();
switch (process)
{
case WAIT_START_PROGRAM:
uart_log("wait start app...(%ds)rn", WAIT_TIMEOUT - timerout);
delay_ms(1000);
timerout ++;
if(timerout >= WAIT_TIMEOUT)
{
set_ymodem_status(START_PROGRAM);
}
break;
case START_PROGRAM:
uart_log("start app...rn");
delay_ms(50);
if (!jump_app(APP_SECTOR_ADDR))
{
uart_log("start app failed: app no programrn");
delay_ms(1000);
}
break;
case UPDATE_PROGRAM:
ymodem_c();
uart_log("update app program...rn");
delay_ms(1000);
break;
case UPDATE_SUCCESS:
uart_log("update successrn");
uart_log("system reboot...rn");
delay_ms(1000);
system_reboot();
break;
default:
break;
}
}
}
Ymodem協(xié)議處理:
#define YMODEM_SOH 0x01
#define YMODEM_STX 0x02
#define YMODEM_EOT 0x04
#define YMODEM_ACK 0x06
#define YMODEM_NAK 0x15
#define YMODEM_CA 0x18
#define YMODEM_C 0x43
#define MAX_QUEUE_SIZE 1200
typedef void (*ymodem_callback)(process_status);
typedef struct
{
process_status process;
uint8_t status;
uint8_t id;
uint32_t addr;
uint8_t sectors_size;
ymodem_callback cb;
} ymodem_t;
//順序循環(huán)隊列的結(jié)構體定義如下:
typedef struct
{
uint8_t queue[MAX_QUEUE_SIZE];
int rear; //隊尾指針
int front; //隊頭指針
int count; //計數(shù)器
} seq_queue_t;
typedef struct
{
uint8_t data[1200];
uint16_t len;
} download_buf_t;
void ymodem_ack(void)
{
uint8_t buf;
buf = YMODEM_ACK;
RS485_Send_Data(&buf, 1);
}
void ymodem_nack(void)
{
uint8_t buf;
buf = YMODEM_NAK;
RS485_Send_Data(&buf, 1);
}
void ymodem_c(void)
{
uint8_t buf;
buf = YMODEM_C;
RS485_Send_Data(&buf, 1);
}
void set_ymodem_status(process_status process)
{
ymodem.process = process;
}
process_status get_ymodem_status(void)
{
process_status process = ymodem.process;
return process;
}
void ymodem_start(ymodem_callback cb)
{
if (ymodem.status == 0)
{
ymodem.cb = cb;
}
}
void ymodem_recv(download_buf_t *p)
{
uint8_t type = p->data[0];
switch (ymodem.status)
{
case 0:
if (type == YMODEM_SOH)
{
ymodem.process = BUSY;
ymodem.addr = APP_SECTOR_ADDR;
uart_log("erase flash: 0x%08Xrn", APP_SECTOR_ADDR);
mcu_flash_erase(ymodem.addr, APP_ERASE_SECTORS_NUM);
uart_log("erase flash successrn");
ymodem_ack();
ymodem_c();
ymodem.status++;
}
else if (type == '1')
{
uart_log("start program nowrn");
ymodem.process = START_PROGRAM;
}
else if (type == '2')
{
uart_log("enter update modern");
ymodem.process = UPDATE_PROGRAM;
}
break;
case 1:
if (type == YMODEM_SOH || type == YMODEM_STX)
{
if (type == YMODEM_SOH)
{
mcu_flash_write(ymodem.addr, &p->data[3], 128);
ymodem.addr += 128;
}
else
{
mcu_flash_write(ymodem.addr, &p->data[3], 1024);
ymodem.addr += 1024;
}
ymodem_ack();
}
else if (type == YMODEM_EOT)
{
ymodem_nack();
ymodem.status++;
}
else
{
ymodem.status = 0;
}
break;
case 2:
if (type == YMODEM_EOT)
{
ymodem_ack();
ymodem_c();
ymodem.status++;
}
break;
case 3:
if (type == YMODEM_SOH)
{
ymodem_ack();
ymodem.status = 0;
ymodem.process = UPDATE_SUCCESS;
}
}
p->len = 0;
}
void ymodem_init(void)
{
RS485_Init(115200);
timer_init();
queue_initiate(&rx_queue);
}
關于bootloader詳細的講解,可以看下我之前發(fā)的博客:
STM32 IAP應用開發(fā)——自制BootLoader
完整代碼下載地址:https://download.csdn.net/download/ShenZhen_zixian/87553496
3.2 APP的制作
APP部分根據(jù)自己實際的功能來做,只要記得修改中斷向量表地址即可。地址的值等于你APP區(qū)的起始地址。
示例代碼如下:
main函數(shù):
#include "main.h"
#include "usart.h"
#include "delay.h"
#define APP_VERSION "V100"
#define NVIC_VTOR_MASK 0x3FFFFF80
#define APP_PART_ADDR 0x08008000
void ota_app_vtor_reconfig(void)
{
/* Set the Vector Table base location by user application firmware definition */
SCB->VTOR = APP_PART_ADDR & NVIC_VTOR_MASK;
}
void led_init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOF, &GPIO_InitStructure);
GPIO_SetBits(GPIOF, GPIO_Pin_9);
}
void print_boot_message(void)
{
uart_log("======================================rn");
uart_log("-------------- Enter APP -------------rn");
uart_log ("app version is: %srn", APP_VERSION);
uart_log("======================================rn");
}
int main(void)
{
ota_app_vtor_reconfig();
delay_init(168);
uart_init(115200);
print_boot_message();
led_init();
uart_log ("app init successrn");
while (1)
{
GPIO_SetBits(GPIOF, GPIO_Pin_9);
delay_ms(1000);
GPIO_ResetBits(GPIOF, GPIO_Pin_9);
delay_ms(1000);
}
}
完整代碼下載地址:https://download.csdn.net/download/ShenZhen_zixian/87553496
4 修改工程中的內(nèi)存配置
因為我們對stm32的內(nèi)存進行了分區(qū),不同的代碼要存放在不同的區(qū)域,因此,我們在編譯工程之前需要先定義好各自的區(qū)域,以免出現(xiàn)內(nèi)存越界。
4.1 Bootloader工程內(nèi)存配置
Bootloader的起始地址不需要改,按flash默認地址即可,size需要改成實際分區(qū)大小。
4.2 APP工程內(nèi)存配置
APP的起始地址和size都需要根據(jù)實際的分區(qū)來改。
5 燒錄相關配置
我們的Bootloader做好以后需要燒錄到MCU里面,可以直接用Keil uVison來下載,也可以用J-Flash或者其他,這個都沒關系,但是要注意內(nèi)存的分配,要把固件燒到對應的內(nèi)存地址上。
5.1 BootLoader部分
1)使用Keil uVision下載
如果是用keil下載的話,需要注意flash的配置,具體如下:
2)使用其他下載工具
如果是用J-Flash或者STlink的工具燒錄的話注意燒錄的起始地址是0x08000000就好了。
5.2 APP部分
1)使用Keil uVision下載
跟BootLoader一樣,我們按照前面分配好的空間配置APP的參數(shù)即可。
2)使用其他下載工具
如果是用J-Flash或者STlink的工具燒錄的話注意燒錄的起始地址是0x08008000就好了。
6 運行測試
用串口助手查看運行l(wèi)og(我這里用的是XShell,用其他的也是可以的)。
1)開始運行代碼
等待5s,如果不需要升級就跳轉(zhuǎn)到App區(qū),如下圖:
2)發(fā)送命令1
在等待的5s內(nèi)通過串口2或者RS485發(fā)送一個’1’,直接跳轉(zhuǎn)到APP。
注:我這里為了方便調(diào)試才用的這種方式,實際上可以根據(jù)自己的需求來做。
3)發(fā)送命令2,進入升級模式
在等待的5s內(nèi)通過串口2或者RS485發(fā)送一個’2’,進入升級模式。
注:我這里為了方便調(diào)試才用的這種方式,實際上可以根據(jù)自己的需求來做。比如用按鍵進入,或者用其他串口,USB之類的,也可以在APP部分做這個功能。
串口調(diào)試窗口log如下圖:
4)通過Ymodem傳輸新固件
調(diào)試工具我用的是XShell,實際上用其他工具也行,只要支持Ymodem方式傳輸文件即可。
5)升級固件
固件升級完成后自動重啟,重新運行Bootloader和APP。
至此,整個升級流程就走完了。
結(jié)束語
好了,關于自制BootLoader并實現(xiàn)串口以及RS485升級固件的介紹就講到這里,本文列舉的例子其實只是升級的其中一種方式,只是提供一個思路,不是唯一的方法,實際上最好還是根據(jù)自己實際的需求來做。我之前也發(fā)給幾篇升級相關的文章,用的都是不同的方式,各有各的優(yōu)點和缺點,感興趣的同學可以去看一下。
需要源碼的同學可以在下面的鏈接下載,我把BootLoader和APP都上傳了。
如果你有什么問題或者有更好的方法,歡迎在評論區(qū)留言。
完整代碼下載地址:https://download.csdn.net/download/ShenZhen_zixian/87553496