PIC24 & dsPIC33 MCU Bootloader开发

“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,以了解具体操作,本文不会重复说明。

      本文基于上述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与应用程序的中断向量关联
      对于本文的Bootloader开发方法,将复位向量,中断向量表和Bootloader应用程序代码三部分作为整个Bootloader工程,也就是bootloader工程放在了程序存储器空间地址0x000000起始处。如此处理便要面临一个问题,中断向量表在bootloader工程中,并且可能提前编译烧录到MCU中,那么发生特定中断后,bootloader中的硬件中断向量表如何才能正确跳转到后续烧录的应用程序中断代码?这里有2种方法,一种是非CodeGuardTM的MCU采用中断重映射的通用方法;另一种是带CodeGuardTM的MCU,因其有辅助中断向量表(AVIT),可将IVT分配给Bootloader,而AVIT分配给应用程序工程。当然带有CodeGuardTM的MCU也可以采用中断重映射的通用方法。接下来我们分别讲解这两种方法Bootloader开发的关键思路。

图3 - 中断向量映射

3.1 通用方法 - 中断向量表重映射
      如图4所示,中断向量映射便是事先Bootloader工程和应用程序工程沟通好,明确应用程序工程中各个中断向量重映射的地址,这样当硬件中断向量来了之后,会自动跳转到应用程序的重映射中断向量入口,接着借由重映射中断向量处存放的goto语句进一步跳转到最终的用户中断向量程序代码。通过图4也可以进一步理解,一个中断向量分配给Bootloader工程还是分配给应用工程要提前规划好,中断向量不能共用。

图4 - 中断向量表映射

3.1.1 中断向量表重映射
      为了更好的理解我们可以看下Gitee中PIC24FJ64GU205工程中断向量重映射的情况,在MCC配置界面不勾选"Code Protect Bootloader"即是该种映射方法,这里将T1Interrupt分配给Bootloader工程(硬件中断向量地址0x001A),之后在Bootloader工程的程序空间可以看到0x001A处存放的为Bootloader工程中断函数__T1Interrupt的地址0x000208。

图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 - 中断向量映射

      而T2Interrupt分配给应用程序工程(硬件中断向量地址0x1822),这里使用了辅助中断向量表,之后在应用程序工程的程序空间可以看到0x1822处存放的为应用程序中断函数__T2Interrupt的地址0x00232c。Booloader工程MCC会自动生成使能辅助中断向量表的相应代码,system.c中配置字设置如下:
// 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

      2) 3 - Erase Flash Memory

      3) 2 - Write Flash Memory

      4) A - Self Verify Program Memory

      5) 9 - Reset Device

6.参考文档
1)器件Datasheet
声明:本内容为作者独立观点,不代表电子星球立场。未经允许不得转载。授权事宜与稿件投诉,请联系:editor@netbroad.com
觉得内容不错的朋友,别忘了一键三连哦!
赞 5
收藏 3
关注 40
成为作者 赚取收益
全部留言
0/200
成为第一个和作者交流的人吧