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.