├── .gitignore ├── LICENSE ├── README.md ├── flasher ├── binary.py ├── bootloader_protocol.py ├── elf.py ├── flasher.py ├── program.py └── util.py ├── main.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | .idea 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Confed Solutions B.V. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pico-py-serial-flash 2 | 3 | ### Information 4 | Flashing application for Raspberry Pi Pico. 5 | 6 | Used to serially flash the application firmware of a Pico, using Python.
7 | The Pico is expected to run [UsedBytes](https://github.com/usedbytes/rp2040-serial-bootloader) bootloader C application. 8 | Instructions on how to flash the bootloader can be found on [UsedBytes Blogpost](https://blog.usedbytes.com/2021/12/pico-serial-bootloader/) 9 | 10 | Originally the bootloader was written to be used with UsedBytes [GoLang application](https://github.com/usedbytes/serial-flash), 11 | but since we would prefer to use Python on our deployment systems, the GoLang app has been re-written in Python. 12 | 13 | The UART cable used for testing is a [FTDI TTL-232R-3V3](https://docs.rs-online.com/588e/0900766b80d4cba6.pdf). 14 | 15 | ### Installation 16 | 1. Clone or download this repository. 17 | 2. Navigate to the downloaded directory `pico-py-serial-flash`. 18 | 3. Create a new Python Virtual Environment (tested with Python3.10): `python3 -m venv [name-of-venv]` and replace `[name-of-venv]` with a custom name. 19 | 4. Activate the newly created venv: `source [name-of-venv]/bin/activate`. 20 | 5. Install the modules in the requirements.txt file: `pip install -r requirements.txt`. 21 | 6. Done. 22 | 23 | ### Usage 24 | 1. Follow [UsedBytes instructions](https://blog.usedbytes.com/2021/12/pico-serial-bootloader/) on how to flash the Pico bootloader application to your Pico. 25 | 2. Create an application `.elf` binary file that works with the bootloader (as explained in chapter `Building programs to work with the bootloader` in [UsedBytes instructions](https://blog.usedbytes.com/2021/12/pico-serial-bootloader/)). 26 | 3. Activate your `pico-py-serial-flash` virtual environment (if not activated already) `source [name-of-venv]/bin/activate` 27 | 4. Wire a serial cable to the UART0 (the default UART used for flashing in the bootloader application). 28 | 5. The Pico stays in the bootloader, either by pulling down the `BOOTLOADER_ENTRY_PIN` (default is GPIO 15 of the Pico), or if the Pico has no application flashed on it. 29 | 6. Run the flasher application: `python3 main.py [port-to-uart-cable] [/path/to/elf/file.elf]` 30 | 7. For example: `python3 main.py /dev/ttyUSB0 /home/build/blink_noboot2.elf` 31 | 8. Wait for it to finish uploading, and voilà. 32 | 33 | ### Known issues 34 | None. Please create a `GitHub Issue` when you encounter any. 35 | 36 | ### Known shortcomings 37 | These are the shortcomings that the current Python implementation has. Please feel free to create a `pull request` if you have implemented any changes or new features. 38 | 1. There is no TCP implementation yet, to flash the Pico W over the air (unlike the original [GoLang application](https://github.com/usedbytes/serial-flash)). 39 | 2. The application does not support uploading `.bin` files (with according offsets). 40 | 3. Could use some kind of progress tracker, even though the flashing process can be quite quick for smaller application. -------------------------------------------------------------------------------- /flasher/binary.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ConfedSolutions/pico-py-serial-flash/ea946779a48c36796609e3ec2dc3dc83bc672d05/flasher/binary.py -------------------------------------------------------------------------------- /flasher/bootloader_protocol.py: -------------------------------------------------------------------------------- 1 | import time 2 | import serial 3 | import binascii 4 | from flasher.util import debug, puts, exit_prog, hex_bytes_to_int, bytes_to_little_end_uint32, little_end_uint32_to_bytes 5 | from dataclasses import dataclass 6 | 7 | 8 | @dataclass 9 | class PicoInfo: 10 | flash_addr: int 11 | flash_size: int 12 | erase_size: int 13 | write_size: int 14 | max_data_len: int 15 | 16 | 17 | @dataclass 18 | class Protocol_RP2040: 19 | MAX_SYNC_ATTEMPTS: int = 1 20 | has_sync: bool = False 21 | wait_time_before_read = 0.03 # seconds 22 | 23 | Opcodes = { 24 | 'Sync': bytes('SYNC', 'utf-8'), 25 | 'Read': bytes('READ', 'utf-8'), 26 | 'Csum': bytes('CSUM', 'utf-8'), 27 | 'CRC': bytes('CRCC', 'utf-8'), 28 | 'Erase': bytes('ERAS', 'utf-8'), 29 | 'Write': bytes('WRIT', 'utf-8'), 30 | 'Seal': bytes('SEAL', 'utf-8'), 31 | 'Go': bytes('GOGO', 'utf-8'), 32 | 'Info': bytes('INFO', 'utf-8'), 33 | 'ResponseSync': bytes('PICO', 'utf-8'), 34 | 'ResponseSyncWota': bytes('WOTA', 'utf-8'), 35 | 'ResponseOK': bytes('OKOK', 'utf-8'), 36 | 'ResponseErr': bytes('ERR!', 'utf-8') 37 | } 38 | 39 | def read_bootloader_resp(self, conn: serial.Serial, response_len: int, exit_before_flash=True) -> (bytes, bytes): 40 | # Do a small sleep because we need to wait for Pico to be able to respond. 41 | time.sleep(self.wait_time_before_read) 42 | debug("Start blocking code reponse length is hit. Resp_len: " + str(response_len)) 43 | all_bytes = conn.read(response_len) 44 | err_byte = all_bytes.removeprefix(self.Opcodes["ResponseErr"][:]) 45 | data_bytes = bytes() 46 | if len(err_byte) == response_len: 47 | data_bytes = all_bytes.removeprefix((self.Opcodes["ResponseOK"][:])) 48 | debug("No error encoutered") 49 | else: 50 | puts("Error encoutered in RPi Pico! Please POR your Pico and try again.") 51 | exit_prog(exit_before_flash) 52 | 53 | debug("Complete Buff: " + str(all_bytes)) 54 | debug("Data buff: " + str(data_bytes)) 55 | debug("Len Data buff: " + str(len(data_bytes))) 56 | return all_bytes, data_bytes 57 | 58 | def sync_cmd(self, conn: serial.Serial) -> bool: 59 | for i in range(1, self.MAX_SYNC_ATTEMPTS + 1): 60 | # print(i) 61 | response = bytes() 62 | # debug(response) 63 | try: 64 | debug("Serial conn port used: " + str(conn.port)) 65 | # conn.flushInput() 66 | # conn.flushOutput() 67 | debug("Starting sync command by sending: " + str(self.Opcodes["Sync"][:])) 68 | conn.write(self.Opcodes["Sync"][:]) 69 | 70 | # Small sleep because else Python is too fast, and serial buffer will still be empty. 71 | time.sleep(self.wait_time_before_read) 72 | debug("Have send Sync command, start reading response") 73 | while conn.inWaiting() > 0: 74 | data_byte = conn.read(conn.inWaiting()) 75 | response += data_byte 76 | 77 | debug("Whole response has arrived: " + str(response)) 78 | if response == self.Opcodes["ResponseSync"][:]: 79 | puts("Found a Pico device who responded to sync.") 80 | self.has_sync = True 81 | return self.has_sync 82 | else: 83 | puts("No Pico bootloader found that will respond to the sync command. Is your device connected " 84 | "and in bootloader?") 85 | exit_prog(True) 86 | # debug("Response: " + str(response)) 87 | except serial.SerialTimeoutException: 88 | puts("Serial timeout expired.") 89 | exit_prog(True) 90 | 91 | def info_cmd(self, conn: serial.Serial) -> PicoInfo: 92 | expected_len = len(self.Opcodes['ResponseOK']) + (4 * 5) 93 | conn.write(self.Opcodes["Info"][:]) 94 | debug("Written following bytes to Pico: " + str(self.Opcodes["Info"][:])) 95 | _, resp_ok_bytes = self.read_bootloader_resp(conn, expected_len, True) 96 | decoded_arr = [] 97 | if len(resp_ok_bytes) <= 0: 98 | puts("Something went horribly wrong. Please POR and retry.") 99 | exit_prog(True) 100 | else: 101 | decoded_arr = hex_bytes_to_int(resp_ok_bytes) 102 | debug("Decoded data array: " + str(decoded_arr)) 103 | 104 | flash_addr = bytes_to_little_end_uint32(resp_ok_bytes) 105 | flash_size = bytes_to_little_end_uint32(resp_ok_bytes[4:]) 106 | erase_size = bytes_to_little_end_uint32(resp_ok_bytes[8:]) 107 | write_size = bytes_to_little_end_uint32(resp_ok_bytes[12:]) 108 | max_data_len = bytes_to_little_end_uint32(resp_ok_bytes[16:]) 109 | this_pico_info = PicoInfo(flash_addr, flash_size, erase_size, write_size, max_data_len) 110 | 111 | debug("flash_addr: " + str(flash_addr)) 112 | debug("flash_size: " + str(flash_size)) 113 | debug("erase_size: " + str(erase_size)) 114 | debug("write_size: " + str(write_size)) 115 | debug("max_data_len: " + str(max_data_len)) 116 | 117 | return this_pico_info 118 | 119 | def erase_cmd(self, conn: serial.Serial, addr, length) -> bool: 120 | expected_bit_n = 3 * 4 121 | write_buff = bytes() 122 | write_buff += self.Opcodes['Erase'][:] 123 | write_buff += little_end_uint32_to_bytes(addr) 124 | write_buff += little_end_uint32_to_bytes(length) 125 | if len(write_buff) != expected_bit_n: 126 | missing_bits = expected_bit_n - len(write_buff) 127 | b = bytes(missing_bits) 128 | write_buff += b 129 | # write_readable = hex_bytes_to_int(write_buff) 130 | n = conn.write(write_buff) 131 | debug("Number of bytes written: " + str(n)) 132 | time.sleep(self.wait_time_before_read) 133 | all_bytes, resp_ok_bytes = self.read_bootloader_resp(conn, len(self.Opcodes['ResponseOK']), True) 134 | debug("Erased a length of bytes, response is: " + str(all_bytes)) 135 | if all_bytes != self.Opcodes['ResponseOK']: 136 | return False 137 | return True 138 | 139 | def write_cmd(self, conn: serial.Serial, addr, length, data): 140 | expected_bit_n_no_data = len(self.Opcodes['Write']) + 4 + 4 141 | # expected_bit_n = expected_bit_n_no_data + len(data) 142 | write_buff = bytes() 143 | write_buff += self.Opcodes['Write'][:] 144 | write_buff += little_end_uint32_to_bytes(addr) 145 | write_buff += little_end_uint32_to_bytes(length) 146 | len_before_data = len(write_buff) 147 | if len_before_data != expected_bit_n_no_data: 148 | missing_bits = expected_bit_n_no_data - len_before_data 149 | b = bytes(missing_bits) 150 | write_buff += b 151 | write_buff += data 152 | n = conn.write(write_buff) 153 | debug("Number of bytes written: " + str(n)) 154 | time.sleep(self.wait_time_before_read) 155 | all_bytes, data_bytes = self.read_bootloader_resp(conn, len(self.Opcodes['ResponseOK']) + 4, True) 156 | debug("All bytes return from read: " + str(all_bytes)) 157 | # all_bytes_readable = hex_bytes_to_int(all_bytes) 158 | resp_crc = bytes_to_little_end_uint32(data_bytes) 159 | calc_crc = binascii.crc32(data) 160 | 161 | if resp_crc != calc_crc: 162 | return False 163 | return True 164 | 165 | def seal_cmd(self, conn: serial.Serial, addr, data): 166 | expected_bits_before_crc = len(self.Opcodes['Seal']) + 4 + 4 167 | data_length = len(data) 168 | crc = binascii.crc32(data) 169 | write_buff = bytes() 170 | write_buff += self.Opcodes['Seal'][:] 171 | write_buff += little_end_uint32_to_bytes(addr) 172 | write_buff += little_end_uint32_to_bytes(data_length) 173 | len_before_data = len(write_buff) 174 | if len_before_data != expected_bits_before_crc: 175 | missing_bits = expected_bits_before_crc - len_before_data 176 | b = bytes(missing_bits) 177 | write_buff += b 178 | write_buff += little_end_uint32_to_bytes(crc) 179 | wr_buff_read = hex_bytes_to_int(write_buff) 180 | n = conn.write(write_buff) 181 | debug("Number of bytes written: " + str(n)) 182 | time.sleep(self.wait_time_before_read) 183 | all_bytes, data_bytes = self.read_bootloader_resp(conn, len(self.Opcodes['ResponseOK']), False) 184 | debug("All bytes seal: " + str(all_bytes)) 185 | if all_bytes[:4] != self.Opcodes['ResponseOK']: 186 | return False 187 | return True 188 | 189 | def go_to_application_cmd(self, conn: serial.Serial, addr): 190 | expected_bit_n = len(self.Opcodes['Go']) + 4 191 | write_buff = bytes() 192 | write_buff += self.Opcodes['Go'][:] 193 | write_buff += little_end_uint32_to_bytes(addr) 194 | if len(write_buff) != expected_bit_n: 195 | missing_bits = expected_bit_n - len(write_buff) 196 | b = bytes(missing_bits) 197 | write_buff += b 198 | write_readable = hex_bytes_to_int(write_buff) 199 | n = conn.write(write_buff) 200 | 201 | # Hopaatskeeeeee 202 | 203 | debug("Go.") 204 | -------------------------------------------------------------------------------- /flasher/elf.py: -------------------------------------------------------------------------------- 1 | from elftools.elf.elffile import ELFFile 2 | from flasher.util import debug, puts, exit_prog 3 | from dataclasses import dataclass 4 | from flasher.program import Image 5 | 6 | FLASH_BASE: int = 0x10000000 7 | FLASH_SIZE: int = 2 * 1024 * 1024 8 | 9 | 10 | def _is_in_flash(addr, size: int) -> bool: 11 | return (addr >= FLASH_BASE) and (addr + size <= FLASH_BASE + FLASH_SIZE) 12 | 13 | 14 | def _is_in_header(vaddr, size, header): 15 | return (vaddr >= header['p_vaddr']) and (vaddr + size <= (header['p_vaddr'] + header['p_memsz'])) 16 | 17 | 18 | @dataclass 19 | class Chunk: 20 | PAddr: int 21 | Data: bytes 22 | 23 | 24 | def chunk_sort_func(elem): 25 | return elem['PAddr'] 26 | 27 | 28 | def load_elf(file_name: str): 29 | debug("") 30 | chunks = [] 31 | try: 32 | # Open .elf file on system, and redirect file stream to ELFFile constructor. 33 | with open(file_name, 'rb') as f_stream: 34 | f = ELFFile(f_stream) 35 | debug(f.header) 36 | count = 0 37 | # For each program header entry, check program adress and memsize. Check if fits in flash. 38 | for head_count in range(f.header['e_phnum']): 39 | prog_head = f.get_segment(head_count).header 40 | debug("Prog_HEAD: " + str(prog_head)) 41 | p_paddr = prog_head['p_paddr'] 42 | p_memsz = prog_head['p_memsz'] 43 | if not _is_in_flash(p_paddr, p_memsz): 44 | debug("IDX: " + str(head_count) + " is not in flash.") 45 | debug("This is addr: " + str(p_paddr) + " and memsz: " + str(p_memsz)) 46 | continue 47 | # For each segment header entry, get size and address. 48 | for sec_count in range(f.header['e_shnum']): 49 | count += 1 50 | sec = f.get_section(sec_count) 51 | sec_size = sec.data_size 52 | sec_addr = sec.header['sh_addr'] 53 | is_in_header = _is_in_header(sec_addr, sec_size, prog_head) 54 | # debug("Count: " + str(count) + " section header: " + str(sec.header)) 55 | # debug("Count: " + str(count) + " section size: " + str(sec_size)) 56 | # debug("Count: " + str(count) + " section addr: " + str(sec_addr)) 57 | # debug("Count: " + str(count) + " section is_in_header: " + str(is_in_header)) 58 | if sec_size > 0 and is_in_header: 59 | prog_offset = sec_addr - prog_head['p_vaddr'] 60 | data = sec.data() 61 | # debug("Count: " + str(count) + " section prog_offset: " + str(prog_offset)) 62 | # debug(hex_bytes_to_int(data)) 63 | this_chunk = Chunk(PAddr=p_paddr + prog_offset, Data=data) 64 | chunks.append(this_chunk) 65 | # debug("") 66 | 67 | except IOError: 68 | puts("Failed to read .ELF file. Used filename was: " + file_name) 69 | exit_prog(True) 70 | 71 | debug("") 72 | 73 | # debug("Chunks list: " + str(chunks)) 74 | 75 | chunks.sort(key=lambda x: x.PAddr) 76 | 77 | # debug("Sorted chunks list: " + str(chunks)) 78 | min_p_addr = chunks[0].PAddr 79 | p_addr_of_last_elem = chunks[len(chunks) - 1].PAddr 80 | len_of_last_elements_data = len(chunks[len(chunks) - 1].Data) 81 | max_p_addr = p_addr_of_last_elem + len_of_last_elements_data 82 | 83 | debug("min_p_addr: " + str(min_p_addr)) 84 | debug("p_addr_of_last_elem: " + str(p_addr_of_last_elem)) 85 | debug("len_of_last_elements_data: " + str(len_of_last_elements_data)) 86 | debug("max_p_addr: " + str(max_p_addr)) 87 | 88 | img_data = [bytes()] * (max_p_addr - min_p_addr) 89 | # debug(img_data) 90 | 91 | for c in chunks: 92 | img_data[c.PAddr-min_p_addr:] = c.Data 93 | # debug(img_data) 94 | 95 | img_addr = min_p_addr 96 | img_byte = bytes(img_data) 97 | 98 | img: Image = Image(img_addr, img_byte) 99 | return img 100 | 101 | 102 | if __name__ == '__main__': 103 | debug("Const flash base: " + str(FLASH_BASE)) 104 | debug("Const flash size: " + str(FLASH_SIZE)) 105 | -------------------------------------------------------------------------------- /flasher/flasher.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ConfedSolutions/pico-py-serial-flash/ea946779a48c36796609e3ec2dc3dc83bc672d05/flasher/flasher.py -------------------------------------------------------------------------------- /flasher/program.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from flasher.util import debug, puts, exit_prog, hex_bytes_to_int 3 | from flasher.bootloader_protocol import Protocol_RP2040 4 | 5 | 6 | @dataclass 7 | class Image: 8 | Addr: int = -1 9 | Data: bytes = None 10 | 11 | 12 | @dataclass 13 | class ProgressReport: 14 | Stage: str 15 | Progress: int 16 | Max: int 17 | 18 | 19 | def align(val, to): 20 | return (val + (to - 1)) & ~(to - 1) 21 | 22 | 23 | def Program(conn, image: Image, progress_bar): 24 | # Normal RP2040 (not wireless) protocol 25 | protocol = Protocol_RP2040() 26 | 27 | # Check if there is a Pico device connected, ready to be flashed 28 | has_sync = protocol.sync_cmd(conn=conn) 29 | if not has_sync: 30 | puts("No Pico device to get in sync with.") 31 | exit_prog() 32 | 33 | # Receive information about flash size, and address offsets 34 | device_info = protocol.info_cmd(conn=conn) 35 | 36 | # Pad the image data message 37 | pad_len = align(int(len(image.Data)), device_info.write_size) - int(len(image.Data)) 38 | pad_zeros = bytes(pad_len) 39 | data = image.Data + pad_zeros 40 | 41 | debug("pad_len: " + str(pad_len)) 42 | debug("pad_zeros: " + str(hex_bytes_to_int(pad_zeros))) 43 | debug("data: " + str(data)) 44 | # debug("Data readable: " + str(data_ints)) 45 | 46 | if image.Addr < device_info.flash_addr: 47 | puts("Image load address is too low: " + str(hex(image.Addr)) + " < " + str(hex(device_info.flash_addr))) 48 | exit_prog(True) 49 | 50 | if image.Addr+int(len(data)) > device_info.flash_addr + device_info.flash_size: 51 | puts("Image of " + str(len(data)) + " bytes does not fit in target flash at: " + str(hex(image.Addr))) 52 | exit_prog(True) 53 | 54 | puts("Starting erase.") 55 | 56 | # Check how many bytes we need to erase, and start erasing. 57 | erase_len = int(align(len(data), device_info.erase_size)) 58 | for start in range(0, erase_len, device_info.erase_size): 59 | debug("Erase: " + str(start)) 60 | erase_addr = image.Addr + start 61 | has_succeeded = protocol.erase_cmd(conn, erase_addr, device_info.erase_size) 62 | if not has_succeeded: 63 | puts("Error when erasing flash, at addr: " + str(erase_addr)) 64 | exit_prog(True) 65 | 66 | puts("Erase completed.") 67 | 68 | puts("Starting flash.") 69 | # Start write 70 | for start in range(0, len(data), device_info.max_data_len): 71 | end = start + device_info.max_data_len 72 | if end > int(len(data)): 73 | end = int(len(data)) 74 | 75 | wr_addr = image.Addr + start 76 | wr_len = end - start 77 | wr_data = data[start:end] 78 | crc_valid = protocol.write_cmd(conn, wr_addr, wr_len, wr_data) 79 | if not crc_valid: 80 | puts("CRC mismatch! Exiting.") 81 | exit_prog(False) 82 | 83 | puts("Flashing completed.") 84 | 85 | puts("Adding seal to finalize.") 86 | has_sealed = protocol.seal_cmd(conn, image.Addr, data) 87 | debug("Has sealed: " + str(has_sealed)) 88 | if not has_sealed: 89 | puts("Sealing failed. Exiting.") 90 | exit_prog(False) 91 | 92 | protocol.go_to_application_cmd(conn, image.Addr) 93 | 94 | 95 | 96 | 97 | debug("Program is done.") 98 | -------------------------------------------------------------------------------- /flasher/util.py: -------------------------------------------------------------------------------- 1 | # Returns flasher usage message. 2 | import struct 3 | 4 | 5 | def usage_flasher(): 6 | return str("Usage: main.py port filepath [BASE_ADDR] \nFor example: main.py /dev/ttyUSB0 ~/pico/test.elf") 7 | 8 | 9 | # Wrapper function to be able to easily disable/alter all debugging string output. 10 | def debug(self, *args): 11 | # print(self, *args, sep=' ', end='\n', file=None) 12 | pass 13 | 14 | 15 | # Wrapper function to be able to easily disable/alter all user string output. 16 | def puts(self, *args): 17 | print(self, *args, sep=' ', end='\n', file=None) 18 | 19 | 20 | # Wrapper function to easily change behaviour before exiting program. 21 | def exit_prog(before_flash: bool = True): 22 | if before_flash: 23 | puts("Program was exited before flashing the target device.") 24 | else: 25 | puts("Program has exited after/during flash. Be careful, flash might be damaged.") 26 | exit() 27 | 28 | 29 | def hex_bytes_to_int(hex_bytes: bytes) -> []: 30 | tup = struct.unpack('<' + 'B' * len(hex_bytes), hex_bytes) 31 | test_list = list() 32 | for val in tup: 33 | test_list.append(val) 34 | return test_list 35 | 36 | 37 | def bytes_to_little_end_uint32(b: bytes): 38 | new_int = int(b[0]) | int(b[1]) << 8 | int(b[2]) << 16 | int(b[3]) << 24 39 | return new_int 40 | 41 | 42 | def little_end_uint32_to_bytes(v: int): 43 | return v.to_bytes((v.bit_length() + 7) // 8, 'little') 44 | 45 | 46 | # Print iterations progress 47 | def printProgressBar(iteration, total, prefix='', suffix='', decimals=1, length=100, fill='█', print_end="\r"): 48 | """ 49 | Call in a loop to create terminal progress bar 50 | @params: 51 | iteration - Required : current iteration (Int) 52 | total - Required : total iterations (Int) 53 | prefix - Optional : prefix string (Str) 54 | suffix - Optional : suffix string (Str) 55 | decimals - Optional : positive number of decimals in percent complete (Int) 56 | length - Optional : character length of bar (Int) 57 | fill - Optional : bar fill character (Str) 58 | printEnd - Optional : end character (e.g. "\r", "\r\n") (Str) 59 | """ 60 | percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total))) 61 | filledLength = int(length * iteration // total) 62 | bar = fill * filledLength + '-' * (length - filledLength) 63 | puts(f'\r{prefix} |{bar}| {percent}% {suffix}') 64 | # Print New Line on Complete 65 | if iteration == total: 66 | puts("") 67 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import traceback 3 | import os 4 | import serial.tools.list_ports 5 | import serial 6 | from flasher.elf import load_elf 7 | from flasher.util import debug, puts, usage_flasher, exit_prog 8 | from flasher.program import Image, Program 9 | 10 | 11 | # Called at start of main(), to catch program arguments and respond accordingly. 12 | def handle_args(): 13 | _sys_args = sys.argv[1:] 14 | debug("All args: " + str(_sys_args)) 15 | debug("Len args: " + str(len(_sys_args))) 16 | if len(_sys_args) <= 1 or len(_sys_args) > 3: 17 | return -1 18 | else: 19 | return _sys_args 20 | 21 | 22 | # Runs the flasher program 23 | def run(_sys_args): 24 | global bin_found, img 25 | if _sys_args == -1: 26 | puts(usage_flasher()) 27 | exit_prog(True) 28 | 29 | port = str(_sys_args[0]) 30 | flash_over_air = False 31 | 32 | if port.startswith("tcp:"): 33 | puts("Flashing over TCP is not yet implemented.") 34 | flash_over_air = True 35 | exit_prog(True) 36 | else: 37 | pc_port_paths = list() 38 | [pc_port_paths.append(p[0]) for p in list(serial.tools.list_ports.comports())] 39 | debug("All available serial communication ports on your machine: " + str(pc_port_paths)) 40 | if port not in pc_port_paths: 41 | puts("Given serial port was not available.") 42 | exit_prog(True) 43 | puts("Serial connection made.") 44 | file_path = str(_sys_args[1]) 45 | filename, file_extension = os.path.splitext(file_path) 46 | 47 | if file_extension == ".elf": 48 | debug("Elf found!: " + str(file_extension)) 49 | if len(_sys_args) >= 3: 50 | puts("Base address for ELF files can't be specified") 51 | puts(usage_flasher()) 52 | exit_prog(True) 53 | img = load_elf(file_path) 54 | debug("Returned .elf address: " + str(img.Addr) + " and data: " + str(img.Data)) 55 | debug("ELF Image Data List Length: " + str(len(img.Data))) 56 | debug("") 57 | 58 | elif file_extension == ".bin": 59 | debug("Bin found!: " + str(file_extension)) 60 | if len(_sys_args) != 3: 61 | puts("When flashing a binary file, make sure to pass a base address.") 62 | puts(usage_flasher()) 63 | exit_prog(True) 64 | else: 65 | bin_found = True 66 | else: 67 | puts("Incorrect file extension. Currently supported extensions are: '.elf' and '.bin'.") 68 | exit_prog(True) 69 | base_addr: int = -1 70 | if bin_found: 71 | base_addr = int(_sys_args[2]) 72 | puts("Binary file flashing not yet implemented.") 73 | exit_prog(True) 74 | 75 | debug("Base addr: " + str(base_addr)) 76 | debug("Img data: " + str(img.Data)) 77 | 78 | conn = None 79 | 80 | if img.Data is None or img.Addr <= -1: 81 | puts("Image file has not been read correctly.") 82 | exit_prog(True) 83 | 84 | if flash_over_air: 85 | puts("Flashing over TCP not yet implemented.") 86 | exit_prog(True) 87 | 88 | try: 89 | conn = serial.Serial(port=port, baudrate=921600, inter_byte_timeout=0.1, timeout=0) 90 | except ValueError as e: 91 | puts("Serial parameters out of range, with exception: " + str(e)) 92 | exit_prog(True) 93 | except serial.SerialException as s_e: 94 | puts("Serial Exception. Serial port probably not available: " + str(s_e)) 95 | exit_prog(True) 96 | 97 | puts("Image file has been read correctly.") 98 | # puts(conn.baudrate) 99 | # puts(Opcodes['OpcodeSync']) 100 | # puts(hex_bytes_to_int(Opcodes['OpcodeSync'])) 101 | program_err = Program(conn, img, None) 102 | 103 | 104 | # Module level global definitions 105 | bin_found: bool = False 106 | img: Image 107 | 108 | # Main of the program, handles args and captures the run function in try except clauses 109 | # to be able to easily catch errors 110 | if __name__ == '__main__': 111 | sys_args = handle_args() 112 | try: 113 | run(sys_args) 114 | puts("\nJobs done. Pico should have rebooted into the flashed application.") 115 | except TypeError as err: 116 | puts(usage_flasher()) 117 | except OSError as err: 118 | puts("OS error: {0}".format(err)) 119 | except ValueError as err: 120 | puts("Value error, with error: " + str(err)) 121 | except Exception: 122 | puts("Unexpected error: ", sys.exc_info()[0]) 123 | puts(traceback.print_exc()) 124 | raise 125 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyelftools==0.29 2 | pyserial==3.5 3 | --------------------------------------------------------------------------------