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