标签归档:GOTO

Microchip公司16位单片机dsPIC33E/PIC24E系列bootloader的开发(5)

Bootloader必须百分百可靠。换句话说,bootloader永远是一个无名英雄,躲在芯片的flash里面,在必要时发挥其作用。它不能在任何时候被自己擦除或者覆盖。然而,如果你了解单片机bootloader的实现机制,你不难发现bootloader说到底也只是一段存储在flash空间里的二进制代码而已,与你其他的应用程序无二。因此,bootloader是可以被擦除或者覆盖的。一个设计优秀的bootloader可以避免绝大部分的自毁式错误,但却无法避免由于硬件异常而造成的此类错误。尽管硬件异常五花八门,有不可预测性。但我们任然可以通过一定的软件机制来保护bootloader免遭损坏。

举例来说,当单片机闪存第0页被擦除后,却由于某些原因(比如IO错误,跳电等)紧跟其后的自我重编程操作无法完成,那单片机的bootloader就丢失了位于闪存最前端最重要的跳转重置(GOTO-RESET)命令行。造成的结果就是在单片机冷启动以后无法正确找到bootloader,从而“挂死”。如果你不是很明白为什么单片机闪存0页0行对bootloader的重要意义,为什么这么重要的闪存内容必须每次都要擦除并重新编程,请阅读我本系列文章的第一篇

为了避免类似上述硬件异常所造成的bootloader损坏,我在这里设计了一种“回滚”机制,保存bootloader免遭丢失GOTO-RESET指令的危险。简而言之,一个用于“回滚”的覆盖标识变量用来监控单片机闪存中的GOTO-RESET指令是否被擦除。在每次执行bootloading之前,回滚机制首先将GOTO-RESET中的指令保存在另外一个安全的地方,然后执行擦除。一旦发生了擦除操作,覆盖标识变量就被设置为1,表明单片机原有的GOTO-RESET已被存在另外一个地方,该指令位置已被清空。而后续程序一旦发现正常的bootloader无法完成,而该变量值却任然为1,那就启动“回滚”,即将保存在他处的原GOTO-RESET指令重新写回到原来位置。那尽管这次bootloader失败了,但单片机任然可以正常启动,继续在必要的时候执行bootloader工作。显而易见,该标识变量只有在一次bootloader顺利完成之后才会被重置为0,表明无须对GOTO-RESET进行回滚。

        .bss
bt_Addr:.space 6

; 两个GOTO指令(6字节)用于指向bootloader的起始地址
; 该6字节指令位于闪存第0页第0行

下面是我在单片机中实现的回滚机制代码,注释在代码中间。你当然可以用不用的方式来实现

Roll_Bk:
; 读入写锁存
mov     #0xFA, W0
mov     W0, TBLPAG
mov     #0, W0

; 缓存变量bt_Addr已经保存了GOTO-RESET指令内容。
; 是“抢”在该指令被删除之前保存下来的
mov     #bt_Addr, W1
tblwth.b    [W1++], [W0]
tblwtl.b    [W1++], [W0++]
tblwtl.b    [W1++], [W0++]
tblwth.b    [W1++], [W0]
tblwtl.b    [W1++], [W0++]
tblwtl.b    [W1], [W0]

; 将bt_Addr中的内容写入地址0.
; 导入地址
mov     #0, W0
; W0此时是0,即GOTO-RESET的地址是0,位于闪存的最前端。
mov     W0, NVMADRU
mov     W0, NVMADR
; 赋值NVMCON使其开始编程。
mov     #0x4001, W0
rcall   Write
return

Microchip公司16位单片机dsPIC33E/PIC24E系列bootloader的开发(3)

如果你读过我bootloader开发系列文章的第二篇, 你应该了解了如何从MPLAB编译好的hex二进制文件中抽取所需要信息,包括数据的地址以及其内容。从另一个角度看,hex文件其实就是一个对照表,告 诉你某一个数据应该放在单片机闪存的哪个部位。这篇文章将着重讲一下dsPIC33E/PIC24E的用户闪存程序存储区(user memory space)的结构、一些解析hex文件的技巧以及GOTO/RESET指令的替换。

在能够将二进制数据放入单片机的闪存程序存储区相对应的位置之前,我们必须充分了解单片机闪存的结构。dsPIC33E/PIC24E单片机的用户 闪存存储区是一块连续的存储空间,地址范围从0×000000到0x02ABFE(对大容量型号单片机而言是0x0557FE)。为了便于批量编程/擦 除,整一块空间可以以不同单位来分割:擦除页(erase block,page)大小为1024个指令;编程块(program block,row)大小为128个指令。换句话说,一个擦除页含有8个编程块,或者1页有8行。所以从擦除页或者编程块的角度来看,地址上限是 0x02ABFE的单片机有86个擦除页或者684个编程块;而对0x0557FE大小的单片机来说则是171个擦除页或者1368个编程块。另外,小容 量单片机地址上限是0x02ABFE,换成十进制那就意味着175,104个地址。如果你将175,104除以1024或者128,你应该获得相应的 171和1368,你可能会纳闷为什么我前面说小容量的单片是86个擦除页和684个编程块,才一半?如果你不明白其中的道理,你可以查看我上篇文章,我 提到了每个指令需要两个地址空间。你也许还会注意到,175,104除以2再除以1024是应该得到85.5而不是86,你算的没错,Microchip 给最后一个擦除页只做了一半大小,512个指令,不过仍旧称它为“一个”页而已。擦除页是删除程序的最小单位,意味着你一旦执行一个删除命令,则至少要删 除一个页,即1024个指令,当然单片机还有一个整体擦除器件命令,顾名思义,一擦整个芯片都擦掉。同样的,编程块是编程的最小单位,跑一个块编程指令则 会一下子写入128个指令,当然这也意味着你在开始执行块编程之前,这个128个指令必须准备就位。锁存器(latch media)就是在写入闪存之前用来临时存放这些指令的地方,我会在后面的文章里详细解释这个。

如果你读了我上一篇文章,二进制代码提取应该是相当简单明了的。下面是一段我写的Python代码用来抽出相关数据以及按照地址重新编排。

def _Parse_Hex32(self):
    extended_Lineaer_Address = 0    # Left-shift by 16 bits
    extended_Segment_Address = 0    # Left-shift by 4 bits
    for i in range(0, len(self._hex32_Lines)):
        #print str(self._hex32_Lines[i])

        byte_Count, starting_Address, record_Type, data = self._Parse_Line(self._hex32_Lines[i])
        if record_Type == 1:        # End record
            if i != (len(self._hex32_Lines) - 1):
                raise Hex32_Invalid("Data type "End(0)" appears (line "" + str(i+1) + "") before the end of file.")
        elif record_Type == 2:      # Extended segment address
            if len(data) == 2:
                print ("!!! Warning !!!: Data type "Extended Segment Address(2)" appears on line"" + str(i+1) + "". ")
                extended_Segment_Address = (data[0] * 256 + data[1]) * 16
            else:
                raise Hex32_Invalid("Data type "Extended Segment Address(2)" (line"" + str(i+1) + "") contains more than two bytes.")
        elif record_Type == 4:      # Extended linear address
            if len(data) == 2:
                extended_Linear_Address = (data[0] * 256 + data[1]) * 256
            else:
                raise Hex32_Invalid("Data type "Extended Linear Address(4)" (line"" + str(i+1) + "") contains more than two bytes.")
        elif record_Type == 0:      # Data record
            for i in range(0, len(data) / 4):
                device_Address = (extended_Linear_Address + extended_Segment_Address + starting_Address) / 2 + i
                # Flag the LUT, divide the address by because the real device addres inrements by 2
                self._Flag_LUT(device_Address)

                # Fill in the hex into the array
                self._flash_Memory[device_Address * self._instruction_Size_In_Hex + 0] = data[self._instruction_Size_In_Hex * i + 0]
                self._flash_Memory[device_Address * self._instruction_Size_In_Hex + 1] = data[self._instruction_Size_In_Hex * i + 1]
                self._flash_Memory[device_Address * self._instruction_Size_In_Hex + 2] = data[self._instruction_Size_In_Hex * i + 2]
                self._flash_Memory[device_Address * self._instruction_Size_In_Hex + 3] = data[self._instruction_Size_In_Hex * i + 3]

        else:
            raise Hex32_Invalid("Unsupport data type: " + str(reccord_Type))

在本系列的第一篇文章里, 我提到了GOTO-RESET的指令必须用你指向bootloader开头地址的自定义数据替换。这样,一个被修改过GOTO-RESET指令的单片机在 上电开机以后就直奔bootloader,而不是单片机默认的用户程序起始地址。如果bootloader发现有新的固件程序达到,则将其烧入自己的闪存 并执行;如果没有,则执行闪存里已有的程序。

所以这里有两个步骤来处理GOTO-RESET问题:1、将编译器生成的GOTO-RESET数据提取出来并暂时保存好,然后用你自定义的 bootloader起始地址数据填入;2、拿出前面暂存好的原始GOTO-RESET数据放在bootloader的末尾,这样bootloader结 束之后,单片机将会开始执行用户程序。说白了,就是在正常的启动顺序中间插入一个bootloader过程,所以bootloader结束之后,还得将单 片机引回到正常执行用户程序的状态。