“PIC24與dsPIC33都是Microchip 16-bit MCU,閃存程序存儲器架構一致,因此將PIC24與dsPIC33系列MCU的Bootloader開發放到一起來講解。通過本文,您將學習16-bit MCU閃存程序存儲器的架構與操作,進而具備基本的Bootloader開發能力。”
1. 示例工程
為了方便大家學習,我這里挑選常見的MCU型號做了些Bootloader參考工程,如PIC24FJ64GU205,dsPIC33CK256MP508和dsPIC33CH512MP508等,大家可以登錄如下Gitee鏈接下載,下載后每個工程下面有一個readme.hml,內有詳細的工程建立及驗證說明,因此本文中出現的像MCC設置細節,大家都可以參考相關例程的readme.hml,以了解具體操作,本文不會重復說明。
- https://gitee.com/chaoa51933/pic24-series-mcu-bootloader-development
- https://gitee.com/chaoa51933/ds-pic33-series-mcu-bootloader-development
本文基于上述Gitee網頁的PIC24FJ64GU205的示例工程講解,兩個文件夾分別對應下文不同的“中斷向量“處理方式。
2. 閃存程序存儲器構成
如圖1所示,程序存儲器空間由可字尋址的區塊構成,指令字寬度為24位,用戶可用23位程序計數器(PC),可尋址4M×24位的程序存儲器地址空間。程序存儲器空間雖然被視為24位寬(16-bit MCU指令字寬度),但將程序存儲器的每個地址視作一個低位字和一個高位字的組合更加合理,其中高位字的高字節部分未實現。低位字的地址始終為偶數,而高位字的地址為奇數。所以在代碼執行過程中,PC地址按2遞增,即PC<0>固定為0。
圖1 - 程序存儲器構成
接下來看下程序空間存儲器映射示意,圖2左側為PIC24和dsPIC33 MCU的默認程序存儲空間映射,起始地址0x000000處為復位向量,會存儲一條goto語句。地址0x000004開始為中斷向量表(IVT),對于PIC24 MCU向量表延伸至0x0000FE;對于dsPIC33 MCU向量表延伸至0x0001FE。接下來是用戶程序存儲空間和閃存配置字;在往后是0x800000至0xFFFFFF測試存儲空間,這部分用戶不可用。
圖2 - 程序空間存儲器映射
圖2右側為加入Bootloader功能后用戶程序空間需要分為2塊,1塊為Booloader代碼,另一塊為應用程序代碼。同時需要注意最后一個包含配置字的頁進行保留,僅供配置字使用。因為程序存儲器按頁擦除,為了修改配置字可能需要讀取最后一個頁的程序存儲器數據,將其存入RAM,然后修改指定配置字內容,在將這一頁按行逐步回寫,但擦除回寫過程異常掉電可能導致配置字未正確寫入使MCU變磚塊,因此雖有空間浪費但仍建議將最后一頁保留給配置字使用,并且保證bootloader和應用程序配置字一致,這樣燒錄應用程序過程中也不需要更改配置字,只燒寫其它程序內容即可。
注:存儲器頁的大小不同器件可能不同,這里PIC24FJ64JU205和dsPIC33CK系列MCU 8行構成一個存儲器的頁,每個行128條指令,所以一個頁共1024條指令,換算為字的話一個存儲器頁含有0x800個字。
3. Bootloader與應用程序的中斷向量關聯圖3 - 中斷向量映射
3.1 通用方法 - 中斷向量表重映射圖4 - 中斷向量表映射
3.1.1 中斷向量表重映射圖5 - 中斷向量映射
將T2Interrupt及所有其它的中斷分配給應用程序工程(T2Interrupt硬件中斷向量地址0x0022),之后在Bootloader工程的程序空間可以看到0x0022處存放的為0x1A2C地址,在應用程序工程的程序空間中可以看到0x1A2C處存放的為重映射goto語句,進一步goto到0x1CBA的地址,而該地址即是應用程序中斷函數__T2Interrupt的地址。圖6 - 中斷向量映射
下圖為Booloader工程實現上述中斷重映射功能MCC生成的代碼,主要基于偽指令實現。圖7 - 中斷向量映射代碼實現
同樣的在應用程序工程中也會有相應的映射表存在,就是前面說的該種方法在開發之前Bootloader工程和應用程序工程都要知道硬件向量的重映射地址,先溝通好。除了與Bootloader工程中hardware_interrupt_table.S和interrupt.S類似的中斷向量表映射部分,應用程序工程特殊的還在application_header_not_blank.S中為應用程序空間的起始地址處放置了一條goto語句,保證和0x0地址處復位向量處goto語句一致,這就使得應用程序無論是單獨燒錄,還是與Bootloader工程結合(通過Bootloader工程跳轉到應用程序工程起始地址)都可以正常工作。#include "boot_config.h"
.section .application_header_not_blank, code, address(BOOT_CONFIG_APPLICATION_IMAGE_APPLICATION_HEADER_ADDRESS), keep
/* Firmware Image Reset Remap */
goto __resetPRI
3.1.2 Flash空間分配
3.1.2.1 Bootloader工程
Bootloader工程的Flash空間分配通過MCC實現,配置如下,具體空間大小可根據實際應用分配。圖8 - Bootloader工程Flash空間分配
該配置下生成的核心代碼在memory_partition.S中,對于未分配給Bootloader的空間利用noload和keep屬性進行保護,保證Bootloader代碼不會被分配到該空間。#include "boot_config.h"
.section *, code, address(BOOT_CONFIG_PROGRAMMABLE_ADDRESS_LOW), noload, keep
reserved_application_memory:
.space 0xAEFE - BOOT_CONFIG_PROGRAMMABLE_ADDRESS_LOW, 0x00
3.1.2.2 應用程序工程
相應的應用程序工程的Flash空間分配在MCC界面顯示如下。
圖9 - Application工程Flash空間分配
該配置下生成的核心代碼仍在memory_partition.S中,將Bootloader程序空間和包含配置字的flash空間最后一個頁(除了配置字部分)進行保留。Bootloader程序保留空間不包括復位向量和硬件中斷向量表,因為應用程序工程為了可以單獨工作這部分同樣需要中斷向量重映射。而flash最后一個頁空間保留是因為若配置字寫保護使能,則flash最后一個頁不能擦除,所以一般最后一個頁不分配代碼,防止配置字寫保護。注意:Bootloader和應用程序工程的配置字必須嚴格一致。#include "boot_config.h"
.equ ERASE_PAGE_MASK,(~((2048) - 1))
.equ LAST_PAGE_START_ADDRESS, (0xAEFE & ERASE_PAGE_MASK)
.equ RESERVED_MEMORY_START, (0xA7FE+2)
.equ PROGRAM_MEMORY_ORIGIN, (0x100)
.equ LAST_ADDRESS_OF_MEMORY, (0xAEFE)
.section *, code, address(PROGRAM_MEMORY_ORIGIN), noload, keep
boot_loader_memory:
.space (BOOT_CONFIG_PROGRAMMABLE_ADDRESS_LOW - PROGRAM_MEMORY_ORIGIN), 0x00
.section *, code, address(RESERVED_MEMORY_START), noload, keep
config_page_memory:
.space (LAST_ADDRESS_OF_MEMORY-RESERVED_MEMORY_START), 0x00
3.2 特殊方法 - 輔助中斷向量表AIVT使能
對于具有CodeGuard安全性的芯片,可以將0x000000到0x0XXX00的用戶程序空間分為三部分:1)BootSegment (BS) 引導段
2)GeneralSegment (GS) 通用段
3)ConfigurationSegment (CS) 配置段。
圖10 - CodeGuard使能 (PIC24FJ64GU205)
可以通過配置寄存器FSEC的AIVTDIS位使能CodeGuardTM MCU的AVIT。同時將BSEN引導段控制位使能,這樣就可以通過配置寄存器FBSLIM來決定引導段的大小。那么AVIT的偏移量基址BOA,位于引導段最后一頁的起始地址。那么既然使能了這幾個段,肯定是希望代碼保護的,代碼保護可以通過配置寄存器FSEC的BWRP引導段寫保護位和CWRP配置段寫保護位設置。但引導段的寫保護有點特殊,他只是將圖中IVT和BS這部分進行寫保護,而對于AIVT+512 IW(IW是指令字)沒有寫保護。這也合理,因為AIVT是用戶應用程序來使用的,所以AIVT+512個指令字和GS段都沒有被寫保護,使得這些內容可以被自編程進行升級操作。
圖10中BSLIM為實際分配給引導段頁數的補碼形式,這里示意0x1FFA是0x0005的補碼,代表有5個頁用于引導段。前4個頁用于實際的Bootloader代碼空間,并進行寫保護,而最后一個頁用于應用程序工程的AIVT,可以被Bootloader升級改寫。而最后配置字所在的這一頁(共0x800個字),當配置字(CWRP配置段寫保護位)寫保護后,會一起保護起來,因此最后一個頁同樣在配置字寫保護下同樣不可以分配給應用程序。
3.2.1 輔助中斷向量表使能圖11所示,開啟了輔助中斷向量表后,Bootloader和應用程序工程的有自己的中斷向量表,那么此時可以實現Bootloader和應用程序工程開啟同一個中斷。當然同一時刻僅Bootloader或應用程序工程之一使用。
圖11 - 中斷向量映射
這里我們可以看下Gitee中PIC24FJ64GU205工程中斷向量使用情況,在MCC配置界面勾選"Code Protect Bootloader"即是該種中斷方法,這里將T1Interrupt分配給Bootloader工程(硬件中斷向量地址0x001A),之后在Bootloader工程的程序空間可以看到0x001A處存放的為Bootloader工程中中斷函數__T1Interrupt的地址0x000B8E。
圖11 - 中斷向量映射
// FSEC
#pragma config BWRP = ON //Boot Segment Write-Protect bit->Boot Segment is write protected
#pragma config BSS = DISABLED //Boot Segment Code-Protect Level bits->No Protection (other than BWRP)
#pragma config BSEN = ON //Boot Segment Control bit->Boot Segment size determined by FBSLIM
#pragma config GWRP = OFF //General Segment Write-Protect bit->General Segment may be written
#pragma config GSS = DISABLED //General Segment Code-Protect Level bits->No Protection (other than GWRP)
#pragma config CWRP = ON //Configuration Segment Write-Protect bit->Configuration Segment is write protected
#pragma config CSS = DISABLED //Configuration Segment Code-Protect Level bits->No Protection (other than CWRP)
#pragma config AIVTDIS = ON //Alternate Interrupt Vector Table bit->Enabled AIVT
// FBSLIM
#pragma config BSLIM = 8187 //Boot Segment Flash Page Address Limit bits->8187
boot_demo.c中使能輔助中斷向量表函數如下:
static void AIVTEnable(bool enable)
{
#if defined(_ALTIVT)
_ALTIVT = enable;
#elif defined(_AIVTEN)
_AIVTEN = enable;
#else
#error "Unknown/unsupported device type. Implement support for switching to alternate interrupt table mode."
#endif
}
編譯MCC生成的工程會報若干錯誤,需要手動解決。部分錯誤提示需對Bootloader和應用程序工程屬性的xc16-ld下“Addtional options”加上相應鏈接屬性,按編譯錯誤提示操作即可解決。那么借助這些屬性定義鏈接文件會自動將Bootloader工程分配到引導段,應用程序工程分配到通用段。而引導段的大小則為前述的相關配置字指定。
圖12 - Flash空間分配
4. 閃存編程
Bootloader開發目的為閃存運行時自編程,主要靠如下寄存器進行控制。NVMCON和NVMKEY寄存器用于使能和選擇所有操作。其余4個寄存器NVMADRL、NVMADRH、NVMSRCADRL和NVMSRCADRH用于定義數據和地址指針,另有TBLPAG用于表讀表寫操作。所有的閃存編程API函數可以通過MCC生成,這里對生成代碼flash.s簡單解讀方便大家更好理解編程操作。首先看一下解鎖函數FLASH_Unlock。這里解鎖并不是真正的解鎖,只是將解鎖的key值保存在_FlashKey指定的地址。void FLASH_Unlock(uint32_t key);
.global _FLASH_Unlock
.type _FLASH_Unlock, @function
reset
_FLASH_Unlock:
mov #_FlashKey, W2
mov W0, [W2++]
mov W1, [W2]
return;
真正的解鎖過程在具體flash要操作前通過_FLASH_SendNvmKey部分的“wtite the key sequence”實現。因為_FlashKey已經存儲著前述FLASH_Unlock過程的key,所以此處用這個key去解。而key的值0x00AA0055是通過通信協議傳遞過來的。并且解鎖后會伴隨著NVMCON的WR位置1啟動相應的閃存操作。
reset
.global _FLASH_SendNvmKey
.type _FLASH_SendNvmKey, @function
.extern NVMKEY
.extern TBLPAG
reset
_FLASH_SendNvmKey:
push W0
push W1
push W2
mov #_FlashKey, w1
; Disable interrupts
mov INTCON2, W2 ; Save Global Interrupt Enable bit.
bclr INTCON2, #15 ; Disable interrupts
; Write the KEY sequence
mov [W1++], W0
mov W0, NVMKEY
mov [W1], W0
mov W0, NVMKEY
bset NVMCON, #15
; Insert two NOPs after programming
nop
nop
; Wait for operation to complete
prog_wait:
btsc NVMCON, #15
bra prog_wait
; Re-enable interrupts,
btsc W2,#15
BSET INTCON2, #15 ; Restore Global Interrupt Enable bit.
pop W2
pop W1
pop W0
return
而上鎖FLASH_Unlock就是給_FlashKey寫0,這樣即使調用_FLASH_SendNvmKey也不能實現解鎖。
void FLASH_Unlock(uint32_t key);
.global _FLASH_Lock
.type _FLASH_Lock, @function
.extern NVMKEY
reset
_FLASH_Lock:
clr W0
clr W1
rcall _FLASH_Unlock
clr NVMKEY
return;
表讀FLASH_ReadWord24用于讀取Flash內容,一次讀出1個指令字。首先保存TBLPAG的當前值導W2,用于函數執行完恢復。W0和W1構成24位地址address,address高8位地址就是W1的低8位,所以將W1賦值給TBLPAG。而address低16位地址在W0中,因為TBLPAG已經有值,所以從低16位地址調用表讀高位字和表讀低位字指令,將結果讀到W1和W0中,返回32位數,即一個24位指令。
uint32_t FLASH_ReadWord24(uint32_t address);
reset
.global _FLASH_ReadWord24
.type _FLASH_ReadWord24, @function
.extern TBLPAG
_FLASH_ReadWord24:
mov TBLPAG, W2
mov W1, TBLPAG ; Little endian, w1 has MSW, w0 has LSX
tblrdh [W0], W1 ; read MSW of data to high latch
tblrdl [W0], W0 ; read LSW of data
mov W2, TBLPAG ; Restore register,
return
表寫FLASH_WriteDoubleWord24用于寫Flash內容,一次寫入2個指令字。首先進來判斷下NVMCON的WR位清零了沒有,沒有是1則繼續等待WR變為0。寫的起始地址由w1和w0構成,因雙字編程操作需要兩個在4字邊界處對齊的相鄰指令字(各24位),所以要判斷W0的bit0和bit1是否為1,為1則跳到后面的標號3異常處理,如果起始地址正確則繼續往下運行。接著將起始地址賦給目標地址寄存器NVMADRU和NVMADR。緊接著賦值NVMCON為WRITE_DWORD_CODE,這里代表NVMCON的WREN使能,允許寫,NVMOP值為1,即雙字編程。接下來就是當前表寄存器存儲,為了后續恢復。最后表寫要通過表寫保持鎖存器實現,因為表寫指令不會直接寫入閃存程序陣列,而是要將編程的數據先裝入保持鎖存器。而保持鎖存器的起始地址為FA0000h。所以TBLPAG寄存器要賦值為立即數#0xFA,緊接著表寫2條指令字到保持鎖存器。W2、W3和W4、W5就是要寫入的2個32位數。緊接著調用_FLASH_SendNvmKey,完成解鎖和WR置位開始2個指令字寫操作。寫操作完成后如果NVMCON的WRERR置位,代表發生了錯誤的編程/擦除終止,需返回W0的值為1,否則返回0。
bool FLASH_WriteDoubleWord24(uint32_t address, uint32_t Data0, uint32_t Data1);
.global _FLASH_WriteDoubleWord24
.type _FLASH_WriteDoubleWord24, @function
.extern TBLPAG
.extern NVMCON
.extern NVMADRU
.extern NVMADR
reset
_FLASH_WriteDoubleWord24:
btsc NVMCON, #15 ; Loop, blocking until last NVM operation is complete (WR is clear)
bra _FLASH_WriteDoubleWord24
btsc w0, #0 ; Check for a valid address Bit 0 and Bit 1 clear
bra 3f
btsc w0, #1
bra 3f
good24:
mov W1,NVMADRU
mov W0,NVMADR
mov #WRITE_DWORD_CODE, W0
mov W0, NVMCON
mov TBLPAG, W0 ; save it
mov #0xFA,W1
mov W1,TBLPAG
mov #0,W1
; Perform the TBLWT instructions to write the latches
tblwtl W2,[W1]
tblwth W3,[W1++]
tblwtl W4,[W1]
tblwth w5,[W1++]
call _FLASH_SendNvmKey
mov W0, TBLPAG
mov #1, w0 ; default return true
btsc NVMCON, #13 ; if error bit set,
3: mov #0, w0 ; return false
return;
FLASH_WriteRow24行寫操作,基于RAM。同樣首先進來判斷下NVMCON的WR位清零了沒有,沒有是1則繼續等待WR變為0。緊接著是地址有效判斷,行編程要128指令字地址對齊,所以地址W0低7位必須是0,不是0則需跳到后面的標號3異常處理。將起始地址賦給目標地址寄存器NVMADR,高位地址NCMADRU不涉及。緊接著賦值NVMCON為WRITE_ROW_CODE,這里NVMCON的WREN使能,允許寫,NVMOP值為2,代表行編程。接著將RAM數據緩沖區data的地址w2賦值給NVMSRCADRL,因為并沒有用到擴展數據空間EDS,所以NVMSRCADRH不用賦值。緊接著調用_FLASH_SendNvmKey,完成解鎖和WR置位開始行寫操作。寫操作完成后如果NVMCON的WRERR置位,代表發生了錯誤的編程/擦除終止,需返回W0的值為1,否則返回0。
bool FLASH_WriteRow24(uint32_t flashAddress, uint32_t *data);
.global _FLASH_WriteRow24
.type _FLASH_WriteRow24, @function
.extern TBLPAG
.extern NVMCON
.extern NVMADRU
.extern NVMADR
.extern NVMSRCADRL
reset
_FLASH_WriteRow24:
btsc NVMCON, #15 ; Loop, blocking until last NVM operation is complete (WR is clear)
bra _FLASH_WriteRow24
mov #((FLASH_WRITE_ROW_SIZE_IN_INSTRUCTIONS*2)-1), w3 ; get mask and validate all lower bits = 0
and w3, w0, w3
bra NZ,3f
mov W0,NVMADR
mov W1,NVMADRU
mov #FLASH_WRITE_ROW_CODE, W0 ; RPDF = 0 so not compressed
mov W0, NVMCON
mov W2, NVMSRCADRL
call _FLASH_SendNvmKey
mov #1, w0 ; default return true
btsc NVMCON, #13 ; if error bit set,
3: mov #0, w0 ; return false
return;
頁擦除操作FLASH_ErasePage,PIC24FJ64GU205和dsPIC33CK 8行構成一個存儲器頁,每個行128條指令,這樣一個頁共1024條指令,所以頁擦除要判斷1024指令字地址對齊,所以地址W0低10位必須是0,不是0則需跳到后面的標號3異常處理。接著賦值NVMCON為ERASE_PAGE_CODE,這里NVMCON的WREN使能,允許寫,NVMOP值為3,代表頁擦除。然后將起始地址賦給目標地址寄存器NVMADRU和NVMADR。緊接著調用_FLASH_SendNvmKey,完成解鎖和WR置位開始行寫操作。寫操作完成后如果NVMCON的WRERR置位,代表發生了錯誤的編程/擦除終止,需返回W0的值為1,否則返回0。
bool FLASH_ErasePage(uint32_t address);
.global _FLASH_ErasePage
.type _FLASH_ErasePage, @function
.extern TBLPAG
.extern NVMCON
.extern NVMADRU
.extern NVMADR
reset
_FLASH_ErasePage:
mov #FLASH_ERASE_PAGE_SIZE_IN_PC_UNITS-1, w2 ; get mask and validate all lower bits = 0
and w2, w0, w2
bra NZ,3f
mov #ERASE_PAGE_CODE, w2
mov w2, NVMCON
mov w0, NVMADR
mov w1, NVMADRU ; MSB
call _FLASH_SendNvmKey
mov #1, w0 ; default return true
btsc NVMCON, #13 ; if error bit set,
3: mov #0, w0 ; return false
return;
5. 通信協議
串口通信協議可以詳見16-bit Bootloader的幫助文檔,可以在MCC下點擊?號獲得,在大家開發過程中可以參考。
圖13 - 通信協議
其基本協議格式如下:
下面是部分基礎命令格式示例,供大家參考。
1) 0 - Get Version & More