move UF2 converter to storage, allow specifying start address

the former is to avoid an upcoming circular dependency, the latter is to
allow for creating an e.g. user-config-only .uf2 by specifying the
proper offset to start the UF2 addressing at.

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
This commit is contained in:
Brian S. Stephan 2024-03-25 09:05:42 -05:00
parent 610e1a2801
commit a3f9f12e74
Signed by: bss
GPG Key ID: 3DE06D3180895FCB
4 changed files with 62 additions and 53 deletions

View File

@ -6,7 +6,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
import argparse import argparse
import copy import copy
import logging import logging
import struct
from typing import Optional from typing import Optional
from google.protobuf.message import Message from google.protobuf.message import Message
@ -14,19 +13,14 @@ from google.protobuf.message import Message
from gp2040ce_bintools import core_parser from gp2040ce_bintools import core_parser
from gp2040ce_bintools.rp2040 import get_bootsel_endpoints, read, write from gp2040ce_bintools.rp2040 import get_bootsel_endpoints, read, write
from gp2040ce_bintools.storage import (BOARD_CONFIG_BINARY_LOCATION, STORAGE_SIZE, USER_CONFIG_BINARY_LOCATION, from gp2040ce_bintools.storage import (BOARD_CONFIG_BINARY_LOCATION, STORAGE_SIZE, USER_CONFIG_BINARY_LOCATION,
USER_CONFIG_BOOTSEL_ADDRESS, get_config_from_json, pad_config_to_storage_size, USER_CONFIG_BOOTSEL_ADDRESS, convert_binary_to_uf2, get_config_from_json,
serialize_config_with_footer) pad_config_to_storage_size, serialize_config_with_footer)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
GP2040CE_START_ADDRESS = 0x10000000 GP2040CE_START_ADDRESS = 0x10000000
GP2040CE_SIZE = 2 * 1024 * 1024 GP2040CE_SIZE = 2 * 1024 * 1024
UF2_FAMILY_ID = 0xE48BFF56
UF2_MAGIC_FIRST = 0x0A324655
UF2_MAGIC_SECOND = 0x9E5D5157
UF2_MAGIC_FINAL = 0x0AB16F30
################# #################
# LIBRARY ITEMS # # LIBRARY ITEMS #
@ -115,38 +109,6 @@ def concatenate_firmware_and_storage_files(firmware_filename: str,
write(endpoint_out, endpoint_in, GP2040CE_START_ADDRESS, bytes(new_binary)) write(endpoint_out, endpoint_in, GP2040CE_START_ADDRESS, bytes(new_binary))
def convert_binary_to_uf2(binary: bytearray) -> bytearray:
"""Convert a GP2040-CE binary payload to Microsoft's UF2 format.
https://github.com/microsoft/uf2/tree/master#overview
Args:
binary: bytearray content to convert to a UF2 payload
Returns:
the content in UF2 format
"""
size = len(binary)
blocks = (len(binary) // 256) + 1 if len(binary) % 256 else len(binary) // 256
uf2 = bytearray()
index = 0
while index < size:
pad_count = 476 - len(binary[index:index+256])
uf2 += struct.pack('<LLLLLLLL',
UF2_MAGIC_FIRST, # first magic number
UF2_MAGIC_SECOND, # second magic number
0x00002000, # familyID present
0x10000000 + index, # address to write to
256, # bytes to write in this block
index // 256, # sequential block number
blocks, # total number of blocks
UF2_FAMILY_ID) # family ID
uf2 += binary[index:index+256] + bytearray(b'\x00' * pad_count) # content
uf2 += struct.pack('<L', UF2_MAGIC_FINAL) # final magic number
index += 256
return uf2
def get_gp2040ce_from_usb() -> tuple[bytes, object, object]: def get_gp2040ce_from_usb() -> tuple[bytes, object, object]:
"""Read the firmware + config sections from a USB device. """Read the firmware + config sections from a USB device.

View File

@ -6,6 +6,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
import argparse import argparse
import binascii import binascii
import logging import logging
import struct
from google.protobuf.json_format import MessageToJson from google.protobuf.json_format import MessageToJson
from google.protobuf.json_format import Parse as JsonParse from google.protobuf.json_format import Parse as JsonParse
@ -25,6 +26,11 @@ USER_CONFIG_BOOTSEL_ADDRESS = 0x10000000 + USER_CONFIG_BINARY_LOCATION
FOOTER_SIZE = 12 FOOTER_SIZE = 12
FOOTER_MAGIC = b'\x65\xe3\xf1\xd2' FOOTER_MAGIC = b'\x65\xe3\xf1\xd2'
UF2_FAMILY_ID = 0xE48BFF56
UF2_MAGIC_FIRST = 0x0A324655
UF2_MAGIC_SECOND = 0x9E5D5157
UF2_MAGIC_FINAL = 0x0AB16F30
################# #################
# LIBRARY ITEMS # # LIBRARY ITEMS #
@ -47,6 +53,39 @@ class ConfigMagicError(ConfigReadError):
"""Exception raised when the config section does not have the magic value in its footer.""" """Exception raised when the config section does not have the magic value in its footer."""
def convert_binary_to_uf2(binary: bytearray, start: int = 0) -> bytearray:
"""Convert a GP2040-CE binary payload to Microsoft's UF2 format.
https://github.com/microsoft/uf2/tree/master#overview
Args:
binary: bytearray content to convert to a UF2 payload
start: position offset to start at rather than flash start (for creating e.g. user config UF2s)
Returns:
the content in UF2 format
"""
size = len(binary)
blocks = (len(binary) // 256) + 1 if len(binary) % 256 else len(binary) // 256
uf2 = bytearray()
index = 0
while index < size:
pad_count = 476 - len(binary[index:index+256])
uf2 += struct.pack('<LLLLLLLL',
UF2_MAGIC_FIRST, # first magic number
UF2_MAGIC_SECOND, # second magic number
0x00002000, # familyID present
0x10000000 + start + index, # address to write to
256, # bytes to write in this block
index // 256, # sequential block number
blocks, # total number of blocks
UF2_FAMILY_ID) # family ID
uf2 += binary[index:index+256] + bytearray(b'\x00' * pad_count) # content
uf2 += struct.pack('<L', UF2_MAGIC_FINAL) # final magic number
index += 256
return uf2
def get_config(content: bytes) -> Message: def get_config(content: bytes) -> Message:
"""Read the config from a GP2040-CE storage section. """Read the config from a GP2040-CE storage section.

View File

@ -12,10 +12,9 @@ from decorator import decorator
from gp2040ce_bintools import get_config_pb2 from gp2040ce_bintools import get_config_pb2
from gp2040ce_bintools.builder import (FirmwareLengthError, combine_firmware_and_config, from gp2040ce_bintools.builder import (FirmwareLengthError, combine_firmware_and_config,
concatenate_firmware_and_storage_files, convert_binary_to_uf2, concatenate_firmware_and_storage_files, get_gp2040ce_from_usb,
get_gp2040ce_from_usb, pad_binary_up_to_board_config, pad_binary_up_to_board_config, pad_binary_up_to_user_config,
pad_binary_up_to_user_config, replace_config_in_binary, replace_config_in_binary, write_new_config_to_filename, write_new_config_to_usb)
write_new_config_to_filename, write_new_config_to_usb)
from gp2040ce_bintools.storage import (get_board_storage_section, get_config, get_config_footer, from gp2040ce_bintools.storage import (get_board_storage_section, get_config, get_config_footer,
get_user_storage_section, serialize_config_with_footer) get_user_storage_section, serialize_config_with_footer)
@ -76,15 +75,6 @@ def test_concatenate_both_configs_to_file(tmp_path):
assert footer_size == 3309 assert footer_size == 3309
def test_convert_binary_to_uf2(whole_board_with_board_config_dump):
"""Do some sanity checks in the attempt to convert a binary to a UF2."""
uf2 = convert_binary_to_uf2(whole_board_with_board_config_dump)
assert len(uf2) == 4194304 # binary is 8192 256 byte chunks, UF2 is 512 b per chunk
assert uf2[0:4] == b'\x55\x46\x32\x0a' == b'UF2\n' # proper magic
assert uf2[8:12] == bytearray(b'\x00\x20\x00\x00') # family ID set
assert uf2[524:528] == bytearray(b'\x00\x01\x00\x10') # address to write the second chunk
@with_pb2s @with_pb2s
def test_concatenate_user_json_to_file(tmp_path): def test_concatenate_user_json_to_file(tmp_path):
"""Test that we write a file with firmware + JSON user config as expected.""" """Test that we write a file with firmware + JSON user config as expected."""

View File

@ -130,6 +130,24 @@ def test_config_from_whole_board_parses(whole_board_dump):
assert config.hotkeyOptions.hotkey02.dpadMask == 1 assert config.hotkeyOptions.hotkey02.dpadMask == 1
def test_convert_binary_to_uf2(whole_board_with_board_config_dump):
"""Do some sanity checks in the attempt to convert a binary to a UF2."""
uf2 = storage.convert_binary_to_uf2(whole_board_with_board_config_dump)
assert len(uf2) == 4194304 # binary is 8192 256 byte chunks, UF2 is 512 b per chunk
assert uf2[0:4] == b'\x55\x46\x32\x0a' == b'UF2\n' # proper magic
assert uf2[8:12] == bytearray(b'\x00\x20\x00\x00') # family ID set
assert uf2[524:528] == bytearray(b'\x00\x01\x00\x10') # address to write the second chunk
def test_convert_binary_to_uf2_with_offsets(whole_board_with_board_config_dump):
"""Do some sanity checks in the attempt to convert a binary to a UF2."""
uf2 = storage.convert_binary_to_uf2(whole_board_with_board_config_dump, start=storage.USER_CONFIG_BINARY_LOCATION)
assert len(uf2) == 4194304 # binary is 8192 256 byte chunks, UF2 is 512 b per chunk
assert uf2[0:4] == b'\x55\x46\x32\x0a' == b'UF2\n' # proper magic
assert uf2[8:12] == bytearray(b'\x00\x20\x00\x00') # family ID set
assert uf2[524:528] == bytearray(b'\x00\xc1\x1f\x10') # address to write the second chunk
@with_pb2s @with_pb2s
def test_serialize_config_with_footer(storage_dump, config_binary): def test_serialize_config_with_footer(storage_dump, config_binary):
"""Test that reserializing a read in config matches the original. """Test that reserializing a read in config matches the original.