A bootloader
is a program to load initialization code.
In our case, it is the initial segment of the program located at the start of
the flash of the microcontroller (TI TMS570LC4357), and its primary purpose
is to update the foxBMS 2 application via interfaces like CAN on the fly
without the use of a debugger.
To transfer the application binary to BMS-Master, the binary file first needs
to be parsed into small sections, and a CRC signature will be calculated for
each section.
(‘section’ in this context refers to a data block that can be stored in a
single flash sector of the physical flash memory.)
During transferring, the data sections will be sent one by one.
Once a section transfer is completed, the program section will be written into
its relevant flash space, and an onboard CRC signature will be calculated.
Next, the CRC signature for this program section will be sent and compared with
the onboard calculated CRC signature to ensure the flashed section data is
correct.
If the result is true, the next section will be transferred.
Otherwise, the onboard bootloader will wait for the same section data again.
Once all section data has been sent successfully, the CRC signature of the last
section, which is also the CRC signature of the entire application binary, will
be saved into the flash memory where the program information is stored.
After that, the vector table will be sent, and a CRC signature for this vector
table will also be sent to verify its validity.
The availability flag of the program will be set only if the CRC signatures of
the vector table from both the PC side and the BMS-Master side match.
Once the bootloader receives the run command via a communication interface like
CAN, it will load the program information from the last loading session into a
global structure variable and check its availability flag to see if a program
exists.
If a program exists, an onboard CRC calculation will be performed based on the
start address and length of the program contained in the variable that carries
the program information.
The obtained CRC signature will then be compared with the one saved during the
previous loading session to verify the program’s validity.
If the onboard program is valid, the bootloader will jump to the application’s
init function.
Otherwise, it will inform the PC that there is no valid
program available onboard.
If a reset boot process command is received by the bootloader via CAN, the
global structure variable that carries the program information will first be
reset and updated to flash.
After that, the CAN communication-related variables will be reset, and the RAM
will be cleaned up.
Additionally, the flash banks where the application data is supposed to be
written will be erased, and a software reset will be performed to reset the
MCU.
11.1.4. Checking the Status of the Onboard Bootloader
The command to check the status of the bootloader will trigger the bootloader’s
callback functions to retrieve its status, including the boot FSM status and
the CAN FSM status, which represent the status of the boot process and the
CAN module, respectively.
This can be used to verify if the connection (e.g., CAN) between the PC
and the bootloader is error-free and to check if the bootloader program is
successfully running on the BMS-Master.
This bootloader project contains two parts: the bootloader itself and the PC
program, which communicates with the bootloader (e.g., send the newest binary
file of foxBMS 2 to BMS-Master).
Contains low level driver modules to
control the on-board resources.
engine
Contains mid-level engine modules to
control the overall program flow.
hal
Contains the build script and the hash
code for HALCoGen.
main
Contains the files where the ‘main’
function and ‘_c_int00’ function are
located.
In addition, it also contains the linker
script for configuring the memory
distribution and the files supported at
the system level.
Contains the Bootloader class, which
serves as the main entry point for
sending application data or requests to
the bootloader.
bootloader_can.py
Contains the BootloaderInterfaceCan class,
which enables high-level communication
with the bootloader via CAN.
bootloader_can_basics.py
Contains the BootloaderCanBasics class,
where the basic CAN communication
functions, including sending and receiving
specified messages, are implemented.
bootloader_binary_file.py
Contains the BootloaderBinaryFile class,
responsible for managing the application
binary file and providing functions to
perform operations on it, such as
calculating CRC, extracting data, and
more.
bootloader_can_messages.py
Contains all enums of CAN messages and
functions to get specified CAN messages in
a dictionary.
To build the bootloader binary, use the command variant
build_bootloader_embedded.
After the binary is successfully built, you can flash it onto the BMS-Master
board using a debugger.
Once the binary is flashed, you can control it using commands available in the
fox tool’s CLI.
There are four commands can be used to communicate the bootloader:
Check the status of the bootloader:
.\fox.ps1bootloadercheck-vv
fox.bat bootloader check -vv
./fox.ps1bootloadercheck-vv
./fox.ps1bootloadercheck-vv
Load the foxBMS 2 binary into the flash memory of BMS-Master:
.\fox.ps1bootloaderload-app-vv
fox.bat bootloader load-app -vv
./fox.ps1bootloaderload-app-vv
./fox.ps1bootloaderload-app-vv
Run the foxBMS 2 application on BMS-Master:
.\fox.ps1bootloaderrun-app-vv
fox.bat bootloader run-app -vv
./fox.ps1bootloaderrun-app-vv
./fox.ps1bootloaderrun-app-vv
Reset the boot process:
.\fox.ps1bootloaderreset-vv
fox.bat bootloader reset -vv
./fox.ps1bootloaderreset-vv
./fox.ps1bootloaderreset-vv
In the case of an error status, a reset command or a power-on restart
should resolve the problem.
The microcontroller (TI TMS570LC4357) has two independent flash banks.
Each of them consists of 16 sectors and has a storage area of 2MB.
While the sizes of the sectors in the second flash bank are uniform, the sizes
of the sectors in the first flash bank are not identical.
More details can be found in
Technical Reference Manual of TMS570LC43
.
In this project, the flash memory (from 0x0x00000000 to 0x00400000)
has been divided into 5 regions, as shown in Table 11.3.
The initial vector table is saved in the memory labeled
VECTORS_DIRECT, and the second vector table, where the actual exception
entries are implemented, is saved in the memory labeled
VECTORS_INDIRECT.
More details about the vector tables can be found in About Vector Table.
The program of bootloader (except for its vector table) has been saved in the
memory labeled BOOTLOADER.
The binary of foxBMS 2 is supposed to be put in the memory labeled APP.
The information of the program will be saved to memory labeled
PROGRAM_INFO_AREA.
In this project, the RAM of the microcontroller is configured as shown in
Table 11.3.
As the names indicate, the memory labeled STACK refers to the space
allocated for stack usage, while the memory labeled RAM serves as the
general RAM space.
The memory labeled RAM_FLASH serves as the section buffer to temporarily
store transferred data before it is written to flash sector.
The flash-related functions and libraries will run from the memory labeled
RAM_FLASH_API.
More details can be found in How to load the “Flash API” into the SRAM?.
The memory labeled with the ‘ECC’ prefix is where the calculated error
correction codes (ECC) are saved.
More details about the ECC can be found in
Technical Reference Manual of TMS570LC43
.
The workflow of the bootloader is achieved through the implementation of a boot
state machine and a CAN state machine.
While the CAN state machine controls the CAN communication and the correct
sequence of the data transfer process, the boot state machine controls the main
workflow of the bootloader.
As shown in the following figure, at the start of the program, the boot FSM
state will be initialized to BOOT_FSM_STATE_WAIT.
From this state, the boot FSM state can change to BOOT_FSM_STATE_LOAD,
BOOT_FSM_STATE_RUN, or BOOT_FSM_STATE_RESET, in response to changes in
the CAN FSM state.
The following state diagram represents the change of states in the CAN module.
At the beginning, the CAN FSM state will be loaded with
CAN_FSM_STATE_NO_COMMUNICATION.
To run the program, a corresponding request will be sent to the bootloader via
the CAN bus.
Once the CAN module has received this CAN message, it will change its state to
CAN_FSM_STATE_RUN_PROGRAM.
If the boot state machine is in the expected state (BOOT_FSM_STATE_WAIT) at
this moment, a validation process will be started to check if the onboard
program is available and validated.
The program will start if everything is correct.
Fig. 11.3 Interaction between boot and CAN FSM in the case of a run command
If a reset request has been sent via CAN bus, the CAN FSM state will change to
CAN_FSM_STATE_RESET_BOOT from any state.
Fig. 11.4 Interaction between boot and CAN FSM in the case of a reset command
If the incoming CAN message is to start a data transfer process, the CAN FSM
state will then change to CAN_FSM_STATE_WAIT_FOR_INFO, and the boot FSM
state will change to BOOT_FSM_STATE_LOAD.
Fig. 11.5 Interaction between boot and CAN FSM during program loading
Once the information of the program has been successfully transferred, the
state will change to CAN_FSM_STATE_WAIT_FOR_DATA_LOOPS.
From this state, the data will be transferred.
To ensure the transferring data (8 bytes every time) is correct, a
corresponding loop number needs to be sent before the 8 data bytes in each
loop.
Only if the loop number is the one the CAN module expects, the data bytes will
be considered correct and will be written into the sector buffer.
To transmit the data efficiently and precisely, the whole program will be
divided into sectors.
Each sector contains the data that can be filled into the sector buffer.
One sector contains 16 sub-sectors.
Each sub-sector contains 1024 * 8 bytes and can be transferred by 1024 data
loops.
Every time a sub-sector has been transferred, an acknowledgment (ACK) message
will be sent to inform the data sender that the whole sub-sector has been
successfully received.
If a whole sector has been transferred and written into the sector buffer, the
corresponding flash sector will be written using the data that is temporarily
saved in the sector buffer.
Immediately, the written flash sector will be validated by comparing the
received CRC signature (8 bytes) with the onboard calculated CRC signature
(8 bytes).
If the result is invalid, the variables involved with data transfer and the
sector buffer will be reset to the original state at the start of the data
transfer for this sector.
The CAN FSM state will reset to either CAN_FSM_STATE_WAIT_FOR_DATA_LOOPS or
CAN_FSM_STATE_RECEIVED_8_BYTES_CRC.
Once all data has been received, written, and validated, the CAN FSM state will
be set to CAN_FSM_STATE_FINISHED_FINAL_VALIDATION.
After the whole program has been received and validated, the vector table for
this program will be transferred and validated as well, as shown in the
following diagram:
The functionality of this state machine has been achieved through the calling
of functions inside the boot module, which basically checks the state of the
CAN module.
The
vector table
is usually placed at the start address (0x00) of the flash and has a length
of 0x20.
It contains 8 8 32-bit ARM instructions in our case.
In the bootloader, there are two vector tables (VECTORS_DIRECT and
VECTORS_INDIRECT)
located at 0x00 and 0x0001FFE0 of the flash memory.
While the first vector table only reroutes to the undefined entry, the SVC
(supervisor call) entry, the prefetch entry, the data abort entry, and the
phantom interrupt entry, to these inside the second vector table
(VECTORS_INDIRECT), the real function entries to handle these entries are
implemented in the second vector table using
b xxx.
Different from these entries, the reset entry points always to the _c_int00
function which will also be called first before any other functions.
The IRQ and FIQ interrupt table will be loaded by
ldr pc,[pc,#-0x1b0]
inside the first vector table.
The configuration and initialization of the vectored interrupt manager is done
in _c_int00 by vimInit().
During booting, the _c_int00 function is first called, but the actual
working exception entries (except for IRQ and FIQ) will be these inside the
second vector table.
After the application is loaded, the vector table of the application will
replace the second vector table.
This means that by jumping into the second vector table, the _c_int00
function of the application will be called, where the configuration for, e.g.,
VIM will be reset for the application.
Meanwhile, the real entries for the exceptions will be replaced by the entries
shipped with the application.
Since some functions inside flash and CRC modules change values in the
protected flash area, such as register values, certain privileges need to be
claimed before calling these functions.
11.7.4. How to load the “Flash API” into the SRAM?
To erase and write the flash bank where the bootloader is located, the relevant
flash API and the functions that use the flash API need to be run from SRAM
rather than flash.
More details can be found in
here,
which is also called
run-time relocation.
To run program from SRAM, the following steps must be done:
Change the MPU configuration for the region from 0x08000000 to
0x0807FFFF to PRIV_RW_USER_RW_EXEC to enable calling the functions
in this area without triggering any error.
More details can be found in
here
Configure loading the flash API and flash relevant functions to flash memory
and run them from SRAM by using build-in link operators in linker script.
More details can be found in
here.
11.7.5. How to use the onboard CRC module in Semi-AUTO mode?
The detailed information regarding CRC onboard module and the CRC algorithm
used in the CRC onboard module can be found in
here.
assigns the value of APP_START_ADDRESS (0x00020020) to the
boot_jumpAddress variable after casting it to an unsigned 32-bit
integer.
((void(*)(void))boot_jumpAddress)();
is a function pointer cast and call operation. Here’s what it does :
(void(*)(void))boot_jumpAddress casts the
boot_jumpAddress variable to a function pointer. This cast
assumes that the address stored in boot_jumpAddress points to
a function with no arguments and no return value (i.e., a function
that takes void as both its argument and return types).
() immediately invokes (calls) the function pointed to by the
casted function pointer.
In summary, this code converts an address (boot_jumpAddress) into
a function pointer and then calls the function at that address.