damay27 / tftp_loader

TFTP bootloader for the Pi Pico W.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

What is this?
This is a bootloader for the Pi Pico W which downloads an application binary
over Wifi/TFTP and writes it to flash. This is a convenient method of doing
overthe air updates for your projects (but see the limitations section below).

This project has been heavily inspired by these bootloader projects:
https://github.com/usedbytes/rp2040-serial-bootloader
https://github.com/rhulme/pico-flashloader
https://github.com/dwelch67/raspberrypi-pico/tree/main/bootloader10

Bootloader Build Instructions:
Checkout this repo alongside the pico-sdk repo.
From within the repo directory create a subdirectory named "build".
Move into the build directory and run this command: 
    cmake -DPICO_BOARD=pico_w -DWIFI_SSID=<SSID> -DWIFI_PASSWORD=<password> ..
Followed by:
    make
Assuming the build finished without errors you can then load it onto the board
like normal using USB, SWD, etc.

The specific file name that the bootloader will request from the TFTP server
and the IP address of the server itself are defined at compile time in tftp.h.

Application Build Instructions:
Building applications that can be properly loaded/run by the bootloader
requires a custom memmap_default.ld script which is included in this repo.
This is needed so that applications know to run from the 0x80000 offset within
flash instead of from the start of flash.

As such the first step is to copy the memmap_default.ld script from the repo
base directory to this path in the pico-sdk:
    <pico-sdk dir>/src/rp2_common/pico_standard_link
Now you should run a clean build of your application. The bootloader
specifically needs the .bin file output.

How to use it:
TFTP Server.
You will need a TFTP server running to server application binary file. The
bootloader looks for a file named PROG.BIN in the TFTP server directory. After
the server is up and running you should power cycle the board to re-run the
bootloader. If you don't want to set up a dedicated TFTP server you can also
use the tftp_loader_test.py script (See the Test section).

How it works:
The bootloader itself is located at the start of flash memory while the
application data is stored at a 0x80000 offset from the start of flash memory.

+-------------------------+0x00000000
|                         |
|                         |
|       Bootloader        |
|                         |
|                         |
|                         |
+-------------------------+
|                         |
|                         |
|                         |
+-------------------------+0x00080000
|                         |
|                         |
|                         |
|                         |
|                         |
|       Application       |
|          Data           |
|                         |
|                         |
|                         |
|                         |
+-------------------------+

A detailed description of TFTP can be found in the RFC here:
https://datatracker.ietf.org/doc/html/rfc1350

At a high level though the bootloader will startup and do configuration and
then send a read request packet to the TFTP server. It is looking for a file
named PROG.BIN

The bootloader will then go back and forth with the server (server sends data
packets and the bootloader ACKs them) until a data packet of less than 512
bytes is received. A data packet of less than 512 bytes indicates the end of
the file being sent.

The one slight complication added into this process has to do with writing
to flash. To write to flash you need to first erase the addresses prior to
writing to them. Flash data must be erased as entire sectors. As a result we
need additional logic to determine if we are at the start of a new sector. If
we are then we should delete all the data from the sector prior to writing the
data.

Lastly, once the new application binary has been downloaded and written to
flash, the bootloader will need to set the Vector Table Offset Register (VTOR)
to point to the application, set the stack pointer using the value from the
vector table, and then finally jump to the applications reset vector. The vector
table contains various pieces of information about the application including
the stack pointer, start address, and addresses of exception handlers. More
info about the vector table can be found on the ARM website.
https://developer.arm.com/documentation/dui0662/b/The-Cortex-M0--Processor/Exception-model/Vector-table
In binaries built for the Pi Pico the vector table is located at on offset of
0x100 from the start of the binary. You can easily double check this if you
want by opening the disassembly file that is generated by the SDK build
process and looking and search for "__VECTOR_TABLE" in the file. Setting the
stack pointer and jumping is accomplished by a small amount of assembly code
located at the end of the bootloader's main function.

The bootloader makes an attempt to not boot into an application that is known
to be corrupted. It does this by keeping a flash_modified flag. If an error
occurs during download then we will have partially overwritten the previous
application's data so booting into that application will make the processor
hang. To avoid this the bootloader checks for errors and if one occurred it
then checks the flash_modified flag. If there was an error but flash wasn't
modified it's safe to just go ahead and boot into whatever is currently in
flash. If there was an error and the flash contents were modified then the
bootloader will retry the download.

Below is a high-level pseudo code representation of what was discussed.

    SETUP_WIFI()
    SEND_TFTP_RRQ()

    ADDR = FLASH_ADDR
    DO
        DATA_BLOCK = WAIT_FOR_TFTP_DATA()

        IF SECTOR_START(ADDR)
            EREASE_SECTOR()
        WRITE_DATA_BLOCK(DATA_BLOCK)
        
        ADDR += SIZE(DATA_BLOCK)

        SEND_TFTP_ACK()
    WHILE SIZE(DATA_BLOCK) == 512

    IF ERR() AND FLASH_MODIFIED
        RETRY_DOWNlOAD()

    SET_VTOR()
    SET_STACK_PTR()
    JUMP_TO_APP()

Test:
Under the test/ directory, there is a Python script that is useful for
simulating various error states, most of the TFTP related. To use the script I
recommend making a python virtual environment and installing scapy (you can use
the requirements.txt for this but it's a little overkill). From there the test
procedure is to run the script with a binary file and selected test case and
then power cycle the board. You will then be able to see the error state get
reported in the serial console. Regardless of the error being injected the
board should always reach a nominal state where it safely boots into the
application.

Another potential use for the test script is to load application data without
running a dedicated TFTP server. To do this simply run the nominal test case
(index 0) with the script pointed to the application binary that you want to
load.

Limitations:
The primary limitation of this bootloader is the lack of integrity checking for
the downloaded file. This means that if the application binary is corrupted
during download the bootloader will be completely unaware of this and will
write a corrupted binary to flash and boot into it. The bootloader itself is
left in a good state but the processor will lock up when it attempts to boot
into the corrupted binary. In some situations this isn't so bad since just
power cycling the board and allowing the bootloader to download a new
application binary should resolve the issue.

However, there are cases where having the board boot into a corrupted
application and hang is unacceptable. For example, if you had a sensor platform
deployed in a location where you don't have physical access to it to power
cycle you will have a big problem since you will need to power cycle the board
to clear the fault and pull a new binary. There are multiple ways around this
issue but here are a few ideas:

    1. Enable the watchdog time before booting into the application. If the
       bootloader boots into a corrupted application the watchdog will
       eventually kick in and reboot into the bootloader.
       The reason I didn't do this by default is that not all application
       binaries are set up to feed the watchdog.

    2. Download application images as hex files instead of binary files.
       Hex files include a checksum value for each record that the bootloader
       could check. If the bootloader detects a corrupted record,
       it would simply need to re-request the current TFTP data block from the
       server and then keep going.

    3. Download a second file from the TFTP server that contains a hash of the
       entire application binary file. Then as you download the binary file
       compute its hash. After the full application binary has been
       downloaded/written the bootloader can compare its computed hash to the
       downloaded hash. If they don't match start the entire download process
       over again.

I didn't do any of these things :D
At the end of the day, I don't have an immediate need for a bootloader like
this and the project was one of those "let's see if I can do it" things. As a
result, I just decided to barf what I have up onto the internet and possibly
loop back to adding features later on as needed.

About

TFTP bootloader for the Pi Pico W.

License:GNU General Public License v2.0


Languages

Language:C 59.2%Language:Python 27.1%Language:CMake 12.9%Language:GDB 0.8%