“PIC24與dsPIC33都是Microchip 16-bit MCU,閃存程序存儲(chǔ)器架構(gòu)一致,因此將PIC24與dsPIC33系列MCU的Bootloader開發(fā)放到一起來講解。通過本文,您將學(xué)習(xí)16-bit MCU閃存程序存儲(chǔ)器的架構(gòu)與操作,進(jìn)而具備基本的Bootloader開發(fā)能力。”
1. 示例工程
為了方便大家學(xué)習(xí),我這里挑選常見的MCU型號(hào)做了些Bootloader參考工程,如PIC24FJ64GU205,dsPIC33CK256MP508和dsPIC33CH512MP508等,大家可以登錄如下Gitee鏈接下載,下載后每個(gè)工程下面有一個(gè)readme.hml,內(nèi)有詳細(xì)的工程建立及驗(yàn)證說明,因此本文中出現(xiàn)的像MCC設(shè)置細(xì)節(jié),大家都可以參考相關(guān)例程的readme.hml,以了解具體操作,本文不會(huì)重復(fù)說明。
- https://gitee.com/chaoa51933/pic24-series-mcu-bootloader-development
- https://gitee.com/chaoa51933/ds-pic33-series-mcu-bootloader-development
本文基于上述Gitee網(wǎng)頁的PIC24FJ64GU205的示例工程講解,兩個(gè)文件夾分別對(duì)應(yīng)下文不同的“中斷向量“處理方式。
2. 閃存程序存儲(chǔ)器構(gòu)成
如圖1所示,程序存儲(chǔ)器空間由可字尋址的區(qū)塊構(gòu)成,指令字寬度為24位,用戶可用23位程序計(jì)數(shù)器(PC),可尋址4M×24位的程序存儲(chǔ)器地址空間。程序存儲(chǔ)器空間雖然被視為24位寬(16-bit MCU指令字寬度),但將程序存儲(chǔ)器的每個(gè)地址視作一個(gè)低位字和一個(gè)高位字的組合更加合理,其中高位字的高字節(jié)部分未實(shí)現(xiàn)。低位字的地址始終為偶數(shù),而高位字的地址為奇數(shù)。所以在代碼執(zhí)行過程中,PC地址按2遞增,即PC<0>固定為0。
圖1 - 程序存儲(chǔ)器構(gòu)成
接下來看下程序空間存儲(chǔ)器映射示意,圖2左側(cè)為PIC24和dsPIC33 MCU的默認(rèn)程序存儲(chǔ)空間映射,起始地址0x000000處為復(fù)位向量,會(huì)存儲(chǔ)一條goto語句。地址0x000004開始為中斷向量表(IVT),對(duì)于PIC24 MCU向量表延伸至0x0000FE;對(duì)于dsPIC33 MCU向量表延伸至0x0001FE。接下來是用戶程序存儲(chǔ)空間和閃存配置字;在往后是0x800000至0xFFFFFF測(cè)試存儲(chǔ)空間,這部分用戶不可用。
圖2 - 程序空間存儲(chǔ)器映射
圖2右側(cè)為加入Bootloader功能后用戶程序空間需要分為2塊,1塊為Booloader代碼,另一塊為應(yīng)用程序代碼。同時(shí)需要注意最后一個(gè)包含配置字的頁進(jìn)行保留,僅供配置字使用。因?yàn)槌绦虼鎯?chǔ)器按頁擦除,為了修改配置字可能需要讀取最后一個(gè)頁的程序存儲(chǔ)器數(shù)據(jù),將其存入RAM,然后修改指定配置字內(nèi)容,在將這一頁按行逐步回寫,但擦除回寫過程異常掉電可能導(dǎo)致配置字未正確寫入使MCU變磚塊,因此雖有空間浪費(fèi)但仍建議將最后一頁保留給配置字使用,并且保證bootloader和應(yīng)用程序配置字一致,這樣燒錄應(yīng)用程序過程中也不需要更改配置字,只燒寫其它程序內(nèi)容即可。
注:存儲(chǔ)器頁的大小不同器件可能不同,這里PIC24FJ64JU205和dsPIC33CK系列MCU 8行構(gòu)成一個(gè)存儲(chǔ)器的頁,每個(gè)行128條指令,所以一個(gè)頁共1024條指令,換算為字的話一個(gè)存儲(chǔ)器頁含有0x800個(gè)字。
3. Bootloader與應(yīng)用程序的中斷向量關(guān)聯(lián)圖3 - 中斷向量映射
3.1 通用方法 - 中斷向量表重映射圖4 - 中斷向量表映射
3.1.1 中斷向量表重映射圖5 - 中斷向量映射
將T2Interrupt及所有其它的中斷分配給應(yīng)用程序工程(T2Interrupt硬件中斷向量地址0x0022),之后在Bootloader工程的程序空間可以看到0x0022處存放的為0x1A2C地址,在應(yīng)用程序工程的程序空間中可以看到0x1A2C處存放的為重映射goto語句,進(jìn)一步goto到0x1CBA的地址,而該地址即是應(yīng)用程序中斷函數(shù)__T2Interrupt的地址。圖6 - 中斷向量映射
下圖為Booloader工程實(shí)現(xiàn)上述中斷重映射功能MCC生成的代碼,主要基于偽指令實(shí)現(xiàn)。圖7 - 中斷向量映射代碼實(shí)現(xiàn)
同樣的在應(yīng)用程序工程中也會(huì)有相應(yīng)的映射表存在,就是前面說的該種方法在開發(fā)之前Bootloader工程和應(yīng)用程序工程都要知道硬件向量的重映射地址,先溝通好。除了與Bootloader工程中hardware_interrupt_table.S和interrupt.S類似的中斷向量表映射部分,應(yīng)用程序工程特殊的還在application_header_not_blank.S中為應(yīng)用程序空間的起始地址處放置了一條goto語句,保證和0x0地址處復(fù)位向量處goto語句一致,這就使得應(yīng)用程序無論是單獨(dú)燒錄,還是與Bootloader工程結(jié)合(通過Bootloader工程跳轉(zhuǎn)到應(yīng)用程序工程起始地址)都可以正常工作。#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實(shí)現(xiàn),配置如下,具體空間大小可根據(jù)實(shí)際應(yīng)用分配。圖8 - Bootloader工程Flash空間分配
該配置下生成的核心代碼在memory_partition.S中,對(duì)于未分配給Bootloader的空間利用noload和keep屬性進(jìn)行保護(hù),保證Bootloader代碼不會(huì)被分配到該空間。#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 應(yīng)用程序工程
相應(yīng)的應(yīng)用程序工程的Flash空間分配在MCC界面顯示如下。
圖9 - Application工程Flash空間分配
該配置下生成的核心代碼仍在memory_partition.S中,將Bootloader程序空間和包含配置字的flash空間最后一個(gè)頁(除了配置字部分)進(jìn)行保留。Bootloader程序保留空間不包括復(fù)位向量和硬件中斷向量表,因?yàn)閼?yīng)用程序工程為了可以單獨(dú)工作這部分同樣需要中斷向量重映射。而flash最后一個(gè)頁空間保留是因?yàn)槿襞渲米謱懕Wo(hù)使能,則flash最后一個(gè)頁不能擦除,所以一般最后一個(gè)頁不分配代碼,防止配置字寫保護(hù)。注意:Bootloader和應(yīng)用程序工程的配置字必須嚴(yán)格一致。#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使能
對(duì)于具有CodeGuard安全性的芯片,可以將0x000000到0x0XXX00的用戶程序空間分為三部分:1)BootSegment (BS) 引導(dǎo)段
2)GeneralSegment (GS) 通用段
3)ConfigurationSegment (CS) 配置段。
圖10 - CodeGuard使能 (PIC24FJ64GU205)
可以通過配置寄存器FSEC的AIVTDIS位使能CodeGuardTM MCU的AVIT。同時(shí)將BSEN引導(dǎo)段控制位使能,這樣就可以通過配置寄存器FBSLIM來決定引導(dǎo)段的大小。那么AVIT的偏移量基址BOA,位于引導(dǎo)段最后一頁的起始地址。那么既然使能了這幾個(gè)段,肯定是希望代碼保護(hù)的,代碼保護(hù)可以通過配置寄存器FSEC的BWRP引導(dǎo)段寫保護(hù)位和CWRP配置段寫保護(hù)位設(shè)置。但引導(dǎo)段的寫保護(hù)有點(diǎn)特殊,他只是將圖中IVT和BS這部分進(jìn)行寫保護(hù),而對(duì)于AIVT+512 IW(IW是指令字)沒有寫保護(hù)。這也合理,因?yàn)锳IVT是用戶應(yīng)用程序來使用的,所以AIVT+512個(gè)指令字和GS段都沒有被寫保護(hù),使得這些內(nèi)容可以被自編程進(jìn)行升級(jí)操作。
圖10中BSLIM為實(shí)際分配給引導(dǎo)段頁數(shù)的補(bǔ)碼形式,這里示意0x1FFA是0x0005的補(bǔ)碼,代表有5個(gè)頁用于引導(dǎo)段。前4個(gè)頁用于實(shí)際的Bootloader代碼空間,并進(jìn)行寫保護(hù),而最后一個(gè)頁用于應(yīng)用程序工程的AIVT,可以被Bootloader升級(jí)改寫。而最后配置字所在的這一頁(共0x800個(gè)字),當(dāng)配置字(CWRP配置段寫保護(hù)位)寫保護(hù)后,會(huì)一起保護(hù)起來,因此最后一個(gè)頁同樣在配置字寫保護(hù)下同樣不可以分配給應(yīng)用程序。
3.2.1 輔助中斷向量表使能圖11所示,開啟了輔助中斷向量表后,Bootloader和應(yīng)用程序工程的有自己的中斷向量表,那么此時(shí)可以實(shí)現(xiàn)Bootloader和應(yīng)用程序工程開啟同一個(gè)中斷。當(dāng)然同一時(shí)刻僅Bootloader或應(yīng)用程序工程之一使用。
圖11 - 中斷向量映射
這里我們可以看下Gitee中PIC24FJ64GU205工程中斷向量使用情況,在MCC配置界面勾選"Code Protect Bootloader"即是該種中斷方法,這里將T1Interrupt分配給Bootloader工程(硬件中斷向量地址0x001A),之后在Bootloader工程的程序空間可以看到0x001A處存放的為Bootloader工程中中斷函數(shù)__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中使能輔助中斷向量表函數(shù)如下:
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生成的工程會(huì)報(bào)若干錯(cuò)誤,需要手動(dòng)解決。部分錯(cuò)誤提示需對(duì)Bootloader和應(yīng)用程序工程屬性的xc16-ld下“Addtional options”加上相應(yīng)鏈接屬性,按編譯錯(cuò)誤提示操作即可解決。那么借助這些屬性定義鏈接文件會(huì)自動(dòng)將Bootloader工程分配到引導(dǎo)段,應(yīng)用程序工程分配到通用段。而引導(dǎo)段的大小則為前述的相關(guān)配置字指定。
圖12 - Flash空間分配
4. 閃存編程
Bootloader開發(fā)目的為閃存運(yùn)行時(shí)自編程,主要靠如下寄存器進(jìn)行控制。NVMCON和NVMKEY寄存器用于使能和選擇所有操作。其余4個(gè)寄存器NVMADRL、NVMADRH、NVMSRCADRL和NVMSRCADRH用于定義數(shù)據(jù)和地址指針,另有TBLPAG用于表讀表寫操作。所有的閃存編程API函數(shù)可以通過MCC生成,這里對(duì)生成代碼flash.s簡(jiǎn)單解讀方便大家更好理解編程操作。首先看一下解鎖函數(shù)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”實(shí)現(xiàn)。因?yàn)開FlashKey已經(jīng)存儲(chǔ)著前述FLASH_Unlock過程的key,所以此處用這個(gè)key去解。而key的值0x00AA0055是通過通信協(xié)議傳遞過來的。并且解鎖后會(huì)伴隨著NVMCON的WR位置1啟動(dòng)相應(yīng)的閃存操作。
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,這樣即使調(diào)用_FLASH_SendNvmKey也不能實(shí)現(xiàn)解鎖。
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內(nèi)容,一次讀出1個(gè)指令字。首先保存TBLPAG的當(dāng)前值導(dǎo)W2,用于函數(shù)執(zhí)行完恢復(fù)。W0和W1構(gòu)成24位地址address,address高8位地址就是W1的低8位,所以將W1賦值給TBLPAG。而address低16位地址在W0中,因?yàn)門BLPAG已經(jīng)有值,所以從低16位地址調(diào)用表讀高位字和表讀低位字指令,將結(jié)果讀到W1和W0中,返回32位數(shù),即一個(gè)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內(nèi)容,一次寫入2個(gè)指令字。首先進(jìn)來判斷下NVMCON的WR位清零了沒有,沒有是1則繼續(xù)等待WR變?yōu)?。寫的起始地址由w1和w0構(gòu)成,因雙字編程操作需要兩個(gè)在4字邊界處對(duì)齊的相鄰指令字(各24位),所以要判斷W0的bit0和bit1是否為1,為1則跳到后面的標(biāo)號(hào)3異常處理,如果起始地址正確則繼續(xù)往下運(yùn)行。接著將起始地址賦給目標(biāo)地址寄存器NVMADRU和NVMADR。緊接著賦值NVMCON為WRITE_DWORD_CODE,這里代表NVMCON的WREN使能,允許寫,NVMOP值為1,即雙字編程。接下來就是當(dāng)前表寄存器存儲(chǔ),為了后續(xù)恢復(fù)。最后表寫要通過表寫保持鎖存器實(shí)現(xiàn),因?yàn)楸韺懼噶畈粫?huì)直接寫入閃存程序陣列,而是要將編程的數(shù)據(jù)先裝入保持鎖存器。而保持鎖存器的起始地址為FA0000h。所以TBLPAG寄存器要賦值為立即數(shù)#0xFA,緊接著表寫2條指令字到保持鎖存器。W2、W3和W4、W5就是要寫入的2個(gè)32位數(shù)。緊接著調(diào)用_FLASH_SendNvmKey,完成解鎖和WR置位開始2個(gè)指令字寫操作。寫操作完成后如果NVMCON的WRERR置位,代表發(fā)生了錯(cuò)誤的編程/擦除終止,需返回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。同樣首先進(jìn)來判斷下NVMCON的WR位清零了沒有,沒有是1則繼續(xù)等待WR變?yōu)?。緊接著是地址有效判斷,行編程要128指令字地址對(duì)齊,所以地址W0低7位必須是0,不是0則需跳到后面的標(biāo)號(hào)3異常處理。將起始地址賦給目標(biāo)地址寄存器NVMADR,高位地址NCMADRU不涉及。緊接著賦值NVMCON為WRITE_ROW_CODE,這里NVMCON的WREN使能,允許寫,NVMOP值為2,代表行編程。接著將RAM數(shù)據(jù)緩沖區(qū)data的地址w2賦值給NVMSRCADRL,因?yàn)椴]有用到擴(kuò)展數(shù)據(jù)空間EDS,所以NVMSRCADRH不用賦值。緊接著調(diào)用_FLASH_SendNvmKey,完成解鎖和WR置位開始行寫操作。寫操作完成后如果NVMCON的WRERR置位,代表發(fā)生了錯(cuò)誤的編程/擦除終止,需返回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行構(gòu)成一個(gè)存儲(chǔ)器頁,每個(gè)行128條指令,這樣一個(gè)頁共1024條指令,所以頁擦除要判斷1024指令字地址對(duì)齊,所以地址W0低10位必須是0,不是0則需跳到后面的標(biāo)號(hào)3異常處理。接著賦值NVMCON為ERASE_PAGE_CODE,這里NVMCON的WREN使能,允許寫,NVMOP值為3,代表頁擦除。然后將起始地址賦給目標(biāo)地址寄存器NVMADRU和NVMADR。緊接著調(diào)用_FLASH_SendNvmKey,完成解鎖和WR置位開始行寫操作。寫操作完成后如果NVMCON的WRERR置位,代表發(fā)生了錯(cuò)誤的編程/擦除終止,需返回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. 通信協(xié)議
串口通信協(xié)議可以詳見16-bit Bootloader的幫助文檔,可以在MCC下點(diǎn)擊?號(hào)獲得,在大家開發(fā)過程中可以參考。
圖13 - 通信協(xié)議
其基本協(xié)議格式如下:
下面是部分基礎(chǔ)命令格式示例,供大家參考。
1) 0 - Get Version & More