“PIC16與PIC18都是Microchip的8-bit MCU,雖在閃存程序存儲器架構(gòu)方面略有差異,但開發(fā)過程基本一致,因此將PIC16與PIC18系列MCU的Bootloader開發(fā)放到一起來講解。通過本文,您將學(xué)習(xí)8-bit MCU閃存程序存儲器的架構(gòu)與操作,進(jìn)而具備基本的Bootloader開發(fā)能力。”
1. 示例工程
為了方便大家學(xué)習(xí),這里挑選常見的MCU系列做了些Bootloader參考工程,如PIC16F15xxx,PIC16F17xx, PIC16F18xxx和PIC18FxxQxx等。大家可以登錄如下Gitee鏈接下載,下載后每個工程下面有一個readme.hml,內(nèi)有詳細(xì)的工程建立及驗證說明,因此本文中出現(xiàn)的像MCC設(shè)置細(xì)節(jié),大家都可以參考相關(guān)例程的readme.hml,以了解具體操作,本文不會重復(fù)說明。
- https://gitee.com/chaoa51933/pic16-series-mcu-bootloader-development
- https://gitee.com/chaoa51933/pic18-series-mcu-bootloader-development
2. 閃存程序存儲器構(gòu)成
2.1 PIC16閃存程序存儲器構(gòu)成
如圖1所示,程序存儲器空間由可字尋址的區(qū)塊構(gòu)成,指令字寬度為14位,高位字節(jié)的高2位未實現(xiàn)。PIC16增強型中檔內(nèi)核具有一個15位程序計數(shù)器,可尋址32K×14位的程序存儲空間。
圖1 - PIC16程序存儲器構(gòu)成
接下來看下程序空間存儲器映射示意。圖2左側(cè)為PIC16 MCU的默認(rèn)程序存儲空間映射,起始地址0x0000處為復(fù)位向量,會存儲一條goto語句。地址0x0004為中斷向量,若相應(yīng)中斷使能,則中斷發(fā)生后都將跳轉(zhuǎn)到該中斷入口地址。接下來是用戶程序存儲空間,存放用戶工程代碼。而PIC16的配置字遠(yuǎn)離用戶程序空間,這里沒有畫出,如PIC16F15223的5個配置字存放在0x8007~0x800B。因配置字遠(yuǎn)離用戶程序存儲空間,這里就帶來一個問題,若將配置字包含在內(nèi),則應(yīng)用程序工程的hex文件轉(zhuǎn)bin文件時得到的bin文件將特別大。因此一般hex轉(zhuǎn)bin不包含配置字,而這就有一個前提條件,即應(yīng)用程序工程和Bootloader工程的配置字一致,這樣應(yīng)用程序燒錄時就不需要更新配置字。當(dāng)然我這里強烈建議應(yīng)用程序工程和Bootloader工程的配置字一致,以免帶來一些不必要的麻煩。
圖2 - PIC16程序空間存儲器映射
圖2右側(cè)為加入Bootloader功能后的用戶程序空間,將用戶程序空間分為2塊,1塊為Booloader代碼,另一塊為應(yīng)用程序代碼。
2.2 PIC18閃存程序存儲器構(gòu)成
如圖3所示,程序存儲器空間由可字節(jié)尋址的區(qū)塊構(gòu)成,指令字寬度為16位。PIC18 MCU實現(xiàn)了一個21位程序計數(shù)器,能夠?qū)ぶ?MB的程序存儲空間。
圖3 - PIC18程序存儲器構(gòu)成
圖4左側(cè)為PIC18 MCU的默認(rèn)程序存儲空間映射,起始地址0x000000處為復(fù)位向量,會存儲一條goto語句。與PIC16器件不同,PIC18器件有兩個中斷向量,地址分別為0x000008和0x000018,代表2個優(yōu)先級,即高優(yōu)先級中斷向量入口和低優(yōu)先級中斷向量入口。特殊的有些PIC18 MCU還具備中斷向量控制模塊(VIC),那么對于此類器件,當(dāng)中斷向量表使能時每個中斷都會有唯一的中斷向量入口地址,而各個中斷的優(yōu)先級仍只能選高優(yōu)先級或低優(yōu)先級。同樣PIC18的配置字也遠(yuǎn)離用戶程序空間,這里沒有畫出,如PIC18F57Q43的10個配置字存放在0x300000~0x300009。圖4右側(cè)為加入Bootloader功能后的用戶程序空間,將用戶程序空間分為2塊,1塊為Booloader代碼,另一塊為應(yīng)用程序代碼。
圖4 - PIC18程序空間存儲器映射
3. Bootloader與應(yīng)用程序的中斷向量關(guān)聯(lián)
對于本文的Bootloader開發(fā)方法,將復(fù)位向量,中斷向量和Bootloader應(yīng)用程序代碼三部分作為整個Bootloader工程,也就是bootloader工程放在了程序存儲器空間地址0x0000起始處。如此處理便要面臨一個問題,中斷向量表在bootloader工程中,并且可能提前編譯燒錄到MCU中,那么發(fā)生特定中斷后,bootloader中的硬件中斷向量表如何才能正確跳轉(zhuǎn)到后續(xù)在線升級的應(yīng)用程序中斷代碼?因此需要在開發(fā)Bootloader工程前提前規(guī)劃好中斷向量映射。
圖5 - 中斷向量映射
3.1 PIC16中斷向量映射
如下圖所示,中斷向量映射便是Bootloader工程和應(yīng)用程序工程事先溝通好,明確應(yīng)用程序工程中各個中斷向量重映射的地址,這樣當(dāng)硬件中斷向量來了之后,會自動跳轉(zhuǎn)到應(yīng)用程序的重映射中斷向量入口,接著借由重映射中斷向量處存放的goto語句進(jìn)一步跳轉(zhuǎn)到最終的用戶中斷向量程序代碼。通過圖5也可以進(jìn)一步理解,注意采用該方法Bootloader工程不應(yīng)該開啟中斷,因為中斷僅供應(yīng)用程序工程使用。
圖6 - PIC16 中斷向量關(guān)聯(lián)
為了更好的理解我們可以看下Gitee中PIC16F15223工程中斷向量的實際重映射情況(圖7),在應(yīng)用程序中開啟了Timer0中斷,發(fā)生中斷后硬件會自動跳轉(zhuǎn)到0x0004的中斷向量入口,在該入口中存在一條goto指令,自動goto到應(yīng)用程序的中斷管理函數(shù)_INTERRUPT_InterruptManager,地址為0x0404,然后在該中斷管理函數(shù)中會進(jìn)一步處理以尋找到真正需要執(zhí)行的中斷代碼_TMR0_ISR。
圖7 - PIC16 中斷向量映射實例
圖8為Booloader工程實現(xiàn)上訴中斷重映射功能MCC生成的代碼,主要基于偽指令實現(xiàn)。
圖8 - PIC16 中斷向量映射代碼實現(xiàn)
3.2 PIC18中斷向量映射
3.2.1 通用方法-中斷向量表重映射
同樣Bootloader工程和應(yīng)用程序工程事先溝通好,明確應(yīng)用程序工程中各個中斷向量重映射的地址。這樣當(dāng)硬件中斷向量來了之后,會自動跳轉(zhuǎn)到應(yīng)用程序的重映射中斷向量入口,接著借由重映射中斷向量處存放的goto語句進(jìn)一步跳轉(zhuǎn)到最終的用戶中斷向量程序代碼。對于所有的PIC18器件都可以采用這種方法,如圖9所示。
圖9 - PIC18 中斷向量映射
為了更好的理解我們可以看下Gitee中PIC18F47Q10工程中斷向量的實際重映射情況(圖10),在應(yīng)用程序中開啟了Timer0中斷,發(fā)生中斷后硬件會自動跳轉(zhuǎn)到0x000008的中斷向量入口,在該入口中存在一條goto指令,自動goto到應(yīng)用程序的中斷管理函數(shù)_INTERRUPT_InterruptManager,地址為0x000A08,然后在該中斷管理函數(shù)中會進(jìn)一步處理以尋找到真正需要執(zhí)行的中斷代碼_Timer0_OverflowISR。
圖10 - PIC18 中斷向量映射實例
圖10中應(yīng)用程序工程在MCC中并沒有使能中斷高低優(yōu)先級,所以此時相當(dāng)于PIC16的處理方式,僅有一個中斷優(yōu)先級。若需要使能2個優(yōu)先級可以按圖11處理,在應(yīng)用程序工程的MCC中斷頁面使能高低優(yōu)先級,然后對應(yīng)的MCC會自動生成2個中斷管理函數(shù),分別為_INTERRUPT_InterruptManagerHigh和_INTERRUPT_InterruptManagerLow。
圖11 - PIC18 中斷向量映射實例
無論應(yīng)用程序工程是否使能中斷優(yōu)先級,Booloader工程都會實現(xiàn)高低優(yōu)先級中斷的重映射功能,MCC生成代碼如下,主要基于偽指令實現(xiàn)。
圖12 - PIC18 中斷向量映射代碼實現(xiàn)
3.2.2 特殊方法-改變中斷向量表IVTBASE
特殊的有些PIC18 MCU還具備中斷向量控制模塊(VIC),那么對于此類器件可以使能中斷向量表,若MVECEN使能則每個中斷都會有唯一的中斷向量入口地址,但各個中斷的優(yōu)先級仍只能選高優(yōu)先級或低優(yōu)先級。
表1 - PIC18 中斷向量表使能
那么對于此類器件中斷向量的映射比較簡單,在中斷向量表使能的情況下可以改變IVTBASE的值,默認(rèn)情況在Bootloader工程中IVTBASE的值為0x000008,而在應(yīng)用程序工程中IVTBASE的值可改為Application Start + 0x000008。因此Bootloader和應(yīng)用程序工程都有自己的中斷向量表,也就是說同一中斷Bootlaoder和應(yīng)用程序工程都可以使用,中斷發(fā)生后會各自調(diào)用各自的中斷函數(shù),但注意同一時間中斷僅能Bootloader用或應(yīng)用程序工程用。
圖13 - PIC18 中斷向量映射(VIC)
MCC中IVTBASE的更改如下,如下顯示設(shè)置IVTBASE為0xD08,并且使能了中斷向量表,為了使得應(yīng)用程序可以重新修改中斷向量表IVTBASE,則配置字的IVTWAY位需要按如下設(shè)置。
圖14 - PIC18 中斷向量表使能(VIC)
注意:Bootloader和應(yīng)用程序工程需要都開啟中斷向量表功能,也就是要保證所有的配置字一致。
4. Flash空間分配
Flash空間分配如下,對于Bootloader工程,在項目工程屬性的ROM ranges中指定代碼空間范圍;對于應(yīng)用程序工程,在項目工程屬性的Code offset指定應(yīng)用程序首地址的偏移情況。
圖15 - Flash空間分配
5. 閃存編程
在研究閃存編程之前,有必要了解閃存程序存儲器的結(jié)構(gòu),第2章節(jié)對PIC16和PIC18的閃存存儲器結(jié)構(gòu)已經(jīng)有所說明。還要了解的是閃存程序存儲器是由行單元組成,因為擦除操作是基于行的。每個行包含的程序空間不定,如PIC16F15223每個行含有32個字,一次只能擦除1行。而寫入的話基于寫緩存,一次可以寫入1個字或多個字,但一次最多寫入的字?jǐn)?shù)受寫緩存大小控制,這里寫緩存是32個字,同行大小一致。
表2 - PIC16F152xx 器件配置信息
而對于PIC18F57Q43,每個行含有128個字,同樣擦除操作針對行,一次擦除1行。
表3 - PIC18-Q84器件配置信息
但是寫入的話,PIC18F57Q43同PIC16F15223不同,不是基于寫緩存而是基于RAM中的Buffer RAM,因此一次也可以寫1行128個字。對于該種器件一定要注意在Bootloader的程序中不要將其它變量分配到Buffer RAM空間,因程序空間的寫操作需要基于該Buffer RAM,這樣寫操作過程會覆蓋分配到Buffer RAM中的變量值,可能導(dǎo)致程序異常。
圖16 - PIC18-Q84器件RAM空間
Bootloader開發(fā)主要為閃存的運行時自編程,主要靠相關(guān)寄存器控制。下面介紹均基于PIC16F15223,寄存器NVMCON1和NVMCON2用于使能和選擇所有操作,NVMADR和NVMDAT為地址和數(shù)據(jù)寄存器。相關(guān)的閃存操作代碼均通過MCC生成(在pic16f1_bootload.c文件中),這里簡單介紹如下。首先看一下解鎖過程,解鎖在操作寫控制位(NVMCON1的bit1 WR位置1)之前,解鎖序列EE_Key_1和EE_Key_2的值來源于接收到的通信
void StartWrite()
{
CLRWDT();
asm ("movf " str(_EE_Key_1) ",w");
asm ("movwf " str(BANKMASK(NVMCON2)));
asm ("movf " str(_EE_Key_2) ",w");
asm ("movwf " str(BANKMASK(NVMCON2)));
asm ("bsf " str(BANKMASK(NVMCON1)) ",1"); // Start the write
NOP();
NOP();
return;
}
擦除操作,基于通信協(xié)議傳遞過來的擦除起始地址,一次擦除1行,然后遞增擦除地址,直至擦除指定的行數(shù)。NVMCON1=0x94代表下一次操作是擦除操作,并已經(jīng)使能了擦除操作,之后NVMCON1的bit1 WR位置1即可啟動擦除操作。
uint8_t Erase_Flash ()
{
NVMADRL = frame.address_L;
NVMADRH = frame.address_H;
for (uint16_t i=0; i < frame.data_length; i++)
{
if ((NVMADRH & 0x7F) >= ((END_FLASH & 0xFF00) >> 8))
{
frame.data[0] = ERROR_ADDRESS_OUT_OF_RANGE;
return (10);
}
NVMCON1 = 0x94; // Setup writes
StartWrite();
if ((NVMADRL += ERASE_FLASH_BLOCKSIZE) == 0x00)
{
++ NVMADRH;
}
}
frame.data[0] = COMMAND_SUCCESS;
frame.EE_key_1 = 0x00; // erase EE Keys
frame.EE_key_2 = 0x00;
return (10);
}
寫操作,基于通信協(xié)議傳遞過來的寫入起始地址和1行待寫入內(nèi)容進(jìn)行寫操作。NVMCON1=0xA4代表下一次操作是寫入操作,并已經(jīng)使能了編程寫入操作。之后在NVMCON1的bit5 LWLO為1的狀態(tài)下,依次將1行待寫入內(nèi)容寫入到寫緩存中,當(dāng)1行數(shù)據(jù)寫完后需要將LWLO寫0,才真正觸發(fā)了閃存寫操作,即寫緩存中的內(nèi)容進(jìn)一步寫入閃存空間。
uint8_t Write_Flash()
{
NVMADRL = frame.address_L;
NVMADRH = frame.address_H;
NVMCON1 = 0xA4; // Setup writes
for (uint16_t i= 0; i < frame.data_length; i += 2)
{
if (((NVMADRL & LAST_WORD_MASK) == LAST_WORD_MASK)
|| (i == frame.data_length - 2))
NVMCON1bits.LWLO = 0;
NVMDATL = frame.data[i];
NVMDATH = frame.data[i+1];
StartWrite();
if ((++ NVMADRL) == 0x00)
{
++ NVMADRH;
}
}
frame.data[0] = COMMAND_SUCCESS;
EE_Key_1 = 0x00; // erase EE Keys
EE_Key_2 = 0x00;
return (10);
}
最后介紹的就是閃存讀操作,這一部分體現(xiàn)在和校驗過程中。通信協(xié)議傳遞過來需要校驗程序空間的首地址,和校驗數(shù)據(jù)的長度。NVMCON1=0x80代表下一次操作是讀操作,每次將NVMCON1的bit0 RD置位1來讀取一個指令字,接著遞增讀取地址來讀取下一個,直到所有內(nèi)容讀取完畢完成和校驗。
uint8_t Calc_Checksum()
{
NVMADRL = frame.address_L;
NVMADRH = frame.address_H;
NVMCON1 = 0x80;
check_sum = 0;
for (uint16_t i = 0;i < frame.data_length; i += 2)
{
NVMCON1bits.RD = 1;
NOP();
NOP();
check_sum += (uint16_t)NVMDATL;
check_sum += ((uint16_t)NVMDATH) << 8;
if ((++ NVMADRL) == 0x00)
{
++ NVMADRH;
}
}
frame.data[0] = (uint8_t) (check_sum & 0x00FF);
frame.data[1] = (uint8_t)((check_sum & 0xFF00) >> 8);
return (11);
}
6. 通信協(xié)議
串口通信協(xié)議可以詳見《Bootloader Generator User’s Guide》的第7章,在大家開發(fā)過程中可以參考。
圖17 - 通信協(xié)議
其基本協(xié)議格式如下:
下面是部分基礎(chǔ)命令格式示例,供大家參考。
1) 0 - Get Version & More
2) 3 - Erase Flash Memory
3) 2 - Write Flash Memory
4) 8 - Calculate Checksum
5) 9 - Reset Device
7. 參考文檔
1)具體器件Datasheet如下章節(jié)
- Memory Organization
- NVM - Nonvolatile Memory Control