Designing bootloader for Microchip dsPIC33E/PIC24E micro-controller(3)
If you have read my “Designing bootloader for Microchip dsPIC33E/PIC24E micro-controller(2)” of this series, you should be able to extract all the necessary information from the hex file compiled by Microchip’s MPLAB. The necessary information includes nothing but address and data. The whole hex file is actually a map telling you what data is supposed to be placed at which part of the MCU’s flash. Today, I am going to focus on dsPIC33E/PIC24E’s user memory space structure, tips for hex file extraction and GOTO/RESET replacement.
Before you can put any data to its target location in a MCU’s flash, the first thing you must know is how the flash is structured. Although the user memory space is implemented continuously on the MCU’s flash from 0x000000 to 0x02ABFE (or 0x0557FE for bigger memory version), it is actually sectioned into different pieces for manage purpose: Erase block or “page” contains 1024 instructions, and each program block or “row” contains 128 instructions, or you can say each page contains 8 rows. From the view of “page/erase block” or “row/program block”, there are 86 pages or 684 rows in a device with address limit 0x02ABFE, or 171 pages or 1368 rows for 0x0557FE. For smaller memory device, you might wonder, since 0x02ABFE means there are 175104 available address, if you divide 175104 by 1024 or 128, you should get 171 and 1368 respectively, why you end up only having half of the result? If you couldn’t understand, you should check out my previous post of this series where I explained why each instruction takes two address. You might also have notice that 175104/2/1024 should give you 85.5 rather than 86. This time you are right, it is 85.5, and the last page is only half as big as all the others — 512 instructions wide. But microchip still counts the half page as a page. That’s how you get 86 on its datasheet. “Page” is the smallest unit to erase, which means if you run a page erase operation on a MCU, you will at least have to erase the whole page of data, or you can also do a whole chip erase….. Accordingly, “row” is the smallest unit to program. When you run a program operation, you will program 128 instructions in a row. This also requires that you should have your 128 instructions ready to go before the execution. Latch media is the temporary warehouse to store those 128 instructions before you burn them into the flash, I will cover it in the following post.
Hex extraction is quite straightforward if you have read my previous post of this series. Here is some python codes I wrote to extract hex data and line them up according to their address:
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))
As I explained in my first post of this series, the GOTO-RESET data in the hex data must be replaced by your own instructions that point to the starting address of your bootloader. Rather than executing your customer application, a device with modified GOTO-RESET will first jump to the bootloader to check if there is new firmware coming in. If yes, then burn the firmware into the flash, otherwise, jump to the starting address of your customer application and run it.
So attention should be paid in two places: 1) Store the GOTO-RESET data from your compiler, and replace it with your starting address of bootloader; 2) Save the stored GOTO-RESET data at the end of your bootloader, so that the device knows where to find your application once the booloading is finished.
######################################################################################################## # Replace the very first two instrutions (which is the GOTO-RESET) with the ones that points to the # starting address of bootloader. # Write the original GOTO-RESET instructions to the ending of bootloader. # So that when a hardware reset occurs, the program will hardwarely points to address zero. Since we've # already changed the first two instrutions, the program then will jump to the bootloader first instead # of user's application. After the booloader is done, the program will jump back to user's application # by reading in the ending GOTO-RESET. ######################################################################################################## def _Modify_Goto_Reset(self): # Check if there is GOTO-RESET instruction in the beginning if self._flash_Memory[2] == 4 and self._flash_Memory[6] == 0: # Copy user's original GOTO-RESET instruction user_Lower_Address_Goto = self._flash_Memory[0:2] # (04)jump to lower 16-bit address, format LSB, MSB user_Higher_Address_Goto = self._flash_Memory[4:6] # (00)jump to higher 16-bit address, format LSB, MSB # Replace with the ones that points to where the bootloader is bootloader_Lower_Address_Goto = [ self._bootloader_Starting_Address & 0xff, (self._bootloader_Starting_Address >> 8) & 0xff ] bootloader_Higher_Address_Goto = [(self._bootloader_Starting_Address >> 16) & 0xff, (self._bootloader_Starting_Address >> 24) & 0xff] self._flash_Memory[0] = bootloader_Lower_Address_Goto[0] self._flash_Memory[1] = bootloader_Lower_Address_Goto[1] self._flash_Memory[4] = bootloader_Higher_Address_Goto[0] self._flash_Memory[5] = bootloader_Higher_Address_Goto[1] # Fill back in the user's original GOTO-RESET towards to the ending of bootloader # The GOTO-RESET after bootloader is set to be physically placed at two instructions # ahead of bootloader. Program will jump onto that address once bootloader is finished new_Goto_Reset_Address = self._bootloader_Starting_Address / 2 - 2 self._Flag_LUT(new_Goto_Reset_Address) self._flash_Memory[new_Goto_Reset_Address * self._instruction_Size_In_Hex + 0] = user_Lower_Address_Goto[0] self._flash_Memory[new_Goto_Reset_Address * self._instruction_Size_In_Hex + 1] = user_Lower_Address_Goto[1] self._flash_Memory[new_Goto_Reset_Address * self._instruction_Size_In_Hex + 2] = 4 self._flash_Memory[new_Goto_Reset_Address * self._instruction_Size_In_Hex + 3] = 0 # Phantom byte new_Goto_Reset_Address = new_Goto_Reset_Address + 1 self._flash_Memory[new_Goto_Reset_Address * self._instruction_Size_In_Hex + 0] = user_Higher_Address_Goto[0] self._flash_Memory[new_Goto_Reset_Address * self._instruction_Size_In_Hex + 1] = user_Higher_Address_Goto[1] self._flash_Memory[new_Goto_Reset_Address * self._instruction_Size_In_Hex + 2] = 0 self._flash_Memory[new_Goto_Reset_Address * self._instruction_Size_In_Hex + 3] = 0 # Phantom byte else: raise Hex32_Invalid("No user's GOTO-RESET found at the beginning of hex file.")
Hello. Isn’t PIC24 applications always starting from 0x200 (after IVT tables)? So why to bower to save user firmware vector? Just jump to bootloader code, and from bootloader to 0x200?
This is good question. For a particular PIC, the user program starting address is always the same. e.g. 0x200 in this case. However, saving the default value will 1) help make this bootloader more general-purposed. 2) avoid problems in a case when the compiler could place some data starting from 0x200 and your code might have to be starting from somewhere else.
Compiler and the programmer burns the program from 0x200.
How do I make the pickit and the compiler to burn the bootloader code from 0x10000(in general any other location).
This is a good questions. I’ve never used pickt to burn the firmware, but I assume it’s pretty much the same as ICD. First of all, you don’t ask pickit to burn from certain location, as neither of them provide this option. Rather, programmer just takes in a piece of compiled firmware, and “dumps” it into the chip’s flash as is.
If you open the firmware with a binary editor, you will find out that each line of the compiled instruction starts with an address, this address is instructing the programmer where to sit itself inside the flash. So as long as we can change that address correctly, the programmer should be able to burn it accordingly.
In order to do this, you have two options: 1) manually change the address in the binary firmware file, and make sure you also update its CRC bit. To learn more about this, you can check my blog regarding:
INTEL HEX32 format
or 2) set a flag inside your code to instruct the compiler to generate the custom address automatically. Obviously, the 2nd option is easier. Here is the how flag looks like:
.section *, code, address(your_start_address)