“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