UF2 is a file format, developed by Microsoft for PXT (also known as Microsoft MakeCode), that is particularly suitable for flashing microcontrollers over MSC (Mass Storage Class; aka removable flash drive).
The list of family IDs is stored in a separate JSON file:
https://github.com/microsoft/uf2/blob/master/utils/uf2families.json
This JSON file is regularly updated. The family_id enum should be kept in
sync with it. For simplicity and consistency, it's strongly recommended to
auto-generate this enum from uf2families.json directly. This was last done
using the following commands:
$ curl -fsSLO https://github.com/microsoft/uf2/raw/90e9741f217f5a40c98ba74d663e408041037578/utils/uf2families.json
$ jq -r '
.[]
| " \(.id | ascii_downcase):\n"
+ " id: \(.short_name | ascii_downcase | gsub("-"; "_"))\n"
+ " doc: \(.description | tojson)"
' uf2families.json
Test files, picked to cover as many different shapes as possible:
num_blocks.family_id::rp2xxx_absolute block that picotool prepends (see the
is_rp2350_e10_block value instance in the block type).file_size rather
than family_id.family_id::rp2040) is moved to the end of the second file, which was
intended for family_id::rp2xxx_absolute (see
https://github.com/raspberrypi/pico-examples/blob/c81c855ffdedc825975a40ba357723a71358ddf0/universal/CMakeLists.txt#L159-L169).flags.is_file_container flag set (for
example
https://github.com/microsoft/pxt-microsoft-boot-sequence/releases/download/v0.0.4/arcade-p0.uf2
is a pure file container for
file_name == "Projects/microsoft-boot-sequence.elf"). Generated by
MakeCode, the only known producer of file container UF2s.This page hosts a formal specification of UF2 (USB Flashing Format) using Kaitai Struct. This specification can be automatically translated into a variety of programming languages to get a parsing library.
All Ruby code generated by Kaitai Struct depends on the Kaitai Struct runtime library for Ruby. You must add this dependency to your project before you can parse or serialize any data.
The Ruby runtime library can be installed from RubyGems:
gem install kaitai-struct
Parse a local file and get structure in memory:
data = Uf2.from_file("path/to/local/file.uf2")
Or parse structure from a string of bytes:
bytes = "\x00\x01\x02..."
data = Uf2.new(Kaitai::Struct::Stream.new(bytes))
After that, one can get various attributes from the structure by invoking getter methods like:
data.first_block # => get first block
# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild
require 'kaitai/struct/struct'
unless Gem::Version.new(Kaitai::Struct::VERSION) >= Gem::Version.new('0.11')
raise "Incompatible Kaitai Struct Ruby API: 0.11 or later is required, but you have #{Kaitai::Struct::VERSION}"
end
##
# UF2 is a file format, developed by Microsoft for PXT (also known as
# Microsoft MakeCode), that is particularly suitable for flashing
# microcontrollers over MSC (Mass Storage Class; aka removable flash drive).
#
# The list of family IDs is stored in a separate JSON file:
#
# <https://github.com/microsoft/uf2/blob/master/utils/uf2families.json>
#
# This JSON file is regularly updated. The `family_id` enum should be kept in
# sync with it. For simplicity and consistency, it's strongly recommended to
# auto-generate this enum from `uf2families.json` directly. This was last done
# using the following commands:
#
# ```bash
# $ curl -fsSLO https://github.com/microsoft/uf2/raw/90e9741f217f5a40c98ba74d663e408041037578/utils/uf2families.json
# $ jq -r '
# .[]
# | " \(.id | ascii_downcase):\n"
# + " id: \(.short_name | ascii_downcase | gsub("-"; "_"))\n"
# + " doc: \(.description | tojson)"
# ' uf2families.json
# ```
#
# Test files, picked to cover as many different shapes as possible:
#
# * <https://micropython.org/download/RPI_PICO/> - a typical case: all blocks
# have the same family ID and `num_blocks`.
# * <https://micropython.org/download/RPI_PICO2/> - these .uf2 files are
# actually two UF2 files concatenated. The first UF2 file is the standalone
# `family_id::rp2xxx_absolute` block that `picotool` prepends (see the
# `is_rp2350_e10_block` value instance in the `block` type).
# * <https://circuitpython.org/downloads> - the builds for SAMD boards (for
# example
# [Feather M0 Express](https://circuitpython.org/board/feather_m0_express/))
# set no flags at all, so the field at offset 28 is read as `file_size` rather
# than `family_id`.
# * <https://github.com/raspberrypi/pico-sdk-prebuilts/releases> -
# [Universal UF2](https://github.com/raspberrypi/pico-examples/blob/c81c855ffdedc825975a40ba357723a71358ddf0/universal/README.md#universal-binary-vs-universal-uf2)
# files, which are again two UF2 files concatenated. What's interesting about
# these is that the last block of the first file (with the family ID
# `family_id::rp2040`) is moved to the end of the second file, which was
# intended for `family_id::rp2xxx_absolute` (see
# <https://github.com/raspberrypi/pico-examples/blob/c81c855ffdedc825975a40ba357723a71358ddf0/universal/CMakeLists.txt#L159-L169>).
# * <https://github.com/microsoft/pxt-microsoft-boot-sequence/releases> - some
# files contain blocks with the `flags.is_file_container` flag set (for
# example
# <https://github.com/microsoft/pxt-microsoft-boot-sequence/releases/download/v0.0.4/arcade-p0.uf2>
# is a pure file container for
# `file_name == "Projects/microsoft-boot-sequence.elf"`). Generated by
# MakeCode, the only known producer of file container UF2s.
# * <https://github.com/umi-eng/uftwo/tree/35bccf75b4f81c43f088696a8c4a9912f1f4104e/uftwo/tests> -
# synthetic test files for features that real firmware doesn't seem to use
# (e.g. MD5 checksums).
# @see https://github.com/microsoft/uf2/blob/90e9741f217f5a40c98ba74d663e408041037578/README.md Source
class Uf2 < Kaitai::Struct::Struct
EXTENSION_TAG_TYPE = {
0 => :extension_tag_type_end,
780791 => :extension_tag_type_page_size,
6622621 => :extension_tag_type_description,
10049507 => :extension_tag_type_rp2_ignore_block,
10471356 => :extension_tag_type_version,
11824560 => :extension_tag_type_sha2_checksum,
13149993 => :extension_tag_type_device_type_id,
}
I__EXTENSION_TAG_TYPE = EXTENSION_TAG_TYPE.invert
FAMILY_ID = {
16738585 => :family_id_stm32l4,
69471199 => :family_id_stm32l5,
114362747 => :family_id_stm32f411xc,
299792458 => :family_id_m0sense,
374814231 => :family_id_atmega32,
407992330 => :family_id_saml21,
458716255 => :family_id_nrf52,
475996592 => :family_id_esp32,
505365293 => :family_id_stm32l1,
539900561 => :family_id_stm32l0,
558239728 => :family_id_stm32wl,
585160444 => :family_id_rtl8710b,
716994540 => :family_id_lpc55,
730387100 => :family_id_esp32c2,
767756741 => :family_id_stm32f411xe,
806311475 => :family_id_stm32g0,
822212545 => :family_id_esp32s31,
835856582 => :family_id_gd32f350,
858203894 => :family_id_esp32h2,
863621090 => :family_id_rtl8720d,
1026592404 => :family_id_esp32p4,
1265126769 => :family_id_maixplay_u4,
1282483210 => :family_id_stm32g4,
1318001757 => :family_id_stm32h5,
1332399698 => :family_id_csk4,
1337120189 => :family_id_mimxrt10xx,
1374225320 => :family_id_xr809,
1404571392 => :family_id_stm32f7,
1410195298 => :family_id_esp32c6,
1427194976 => :family_id_samd51,
1467308631 => :family_id_stm32f4,
1511523995 => :family_id_fx2,
1561987630 => :family_id_stm32f2,
1591873650 => :family_id_stm32f1,
1646171002 => :family_id_nrf52833,
1685595318 => :family_id_stm32f0,
1733968048 => :family_id_bk7231u,
1760373640 => :family_id_samd21,
1771791084 => :family_id_ch32v,
1786956866 => :family_id_bk7251,
1803837832 => :family_id_stm32f3,
1829315322 => :family_id_stm32f407,
1840668802 => :family_id_stm32h7,
1853049000 => :family_id_csk6,
1869948536 => :family_id_nrf52832xxab,
1892771411 => :family_id_stm32wb,
1920081230 => :family_id_nrf52832xxaa,
1947226634 => :family_id_max32690,
2010665156 => :family_id_esp32c61,
2067722800 => :family_id_bk7231n,
2078840685 => :family_id_ra4m1,
2105173743 => :family_id_py32f071_uvk5_v3,
2125160941 => :family_id_esp8266,
2139350931 => :family_id_kl32l2,
2181929567 => :family_id_nrf52820,
2410701054 => :family_id_stm32f407vg,
2446589208 => :family_id_max78002,
2501329455 => :family_id_rza1lu,
2599435827 => :family_id_gd32vf103,
2651564682 => :family_id_esp32h4,
2684343619 => :family_id_rtl8710a,
2697558926 => :family_id_at32f415,
2913282112 => :family_id_nrf52840,
3067936943 => :family_id_esp32h21,
3218951918 => :family_id_esp32s2,
3296614247 => :family_id_esp32s3,
3559628908 => :family_id_esp32c3,
3594487346 => :family_id_max32650,
3725750455 => :family_id_bl602,
3767498084 => :family_id_rtl8720c,
3834380118 => :family_id_rp2040,
3834380119 => :family_id_rp2xxx_absolute,
3834380120 => :family_id_rp2xxx_data,
3834380121 => :family_id_rp2350_arm_s,
3834380122 => :family_id_rp2350_riscv,
3834380123 => :family_id_rp2350_arm_ns,
4039314801 => :family_id_max32666,
4145808195 => :family_id_esp32c5,
}
I__FAMILY_ID = FAMILY_ID.invert
def initialize(_io, _parent = nil, _root = nil)
super(_io, _parent, _root || self)
_read
end
def _read
@first_block = Block.new(@_io, self, @_root)
@blocks = []
(first_block.num_blocks - 1).times { |i|
@blocks << Block.new(@_io, self, @_root)
}
self
end
class Block < Kaitai::Struct::Struct
def initialize(_io, _parent = nil, _root = nil)
super(_io, _parent, _root)
_read
end
def _read
@magic = @_io.read_bytes(4)
raise Kaitai::Struct::ValidationNotEqualError.new([85, 70, 50, 10].pack('C*'), @magic, @_io, "/types/block/seq/0") if not @magic == [85, 70, 50, 10].pack('C*')
@second_magic = @_io.read_bytes(4)
raise Kaitai::Struct::ValidationNotEqualError.new([87, 81, 93, 158].pack('C*'), @second_magic, @_io, "/types/block/seq/1") if not @second_magic == [87, 81, 93, 158].pack('C*')
@flags = Flags.new(@_io, self, @_root)
@target_address = @_io.read_u4le
_ = @target_address
raise Kaitai::Struct::ValidationExprError.new(@target_address, @_io, "/types/block/seq/3") if not _ % 4 == 0
@len_payload = @_io.read_u4le
_ = @len_payload
raise Kaitai::Struct::ValidationExprError.new(@len_payload, @_io, "/types/block/seq/4") if not _ % 4 == 0
@block_number = @_io.read_u4le
@num_blocks_raw = @_io.read_u4le
raise Kaitai::Struct::ValidationLessThanError.new(block_number + 1, @num_blocks_raw, @_io, "/types/block/seq/6") if not @num_blocks_raw >= block_number + 1
if !(flags.has_family_id)
@file_size = @_io.read_u4le
end
if flags.has_family_id
@family_id = Kaitai::Struct::Stream::resolve_enum(Uf2::FAMILY_ID, @_io.read_u4le)
end
_io_data = @_io.substream(476)
@data = BlockData.new(_io_data, self, @_root)
@final_magic = @_io.read_bytes(4)
raise Kaitai::Struct::ValidationNotEqualError.new([48, 111, 177, 10].pack('C*'), @final_magic, @_io, "/types/block/seq/10") if not @final_magic == [48, 111, 177, 10].pack('C*')
self
end
##
# Determines whether this is a block that `picotool` prepends to RP2350
# flash images as a workaround for erratum RP2350-E10 (i.e. a hardware
# bug in the A2 version of the RP2350 boot ROM).
#
# Such a block is always written on its own, but its `num_blocks_raw` is
# set to 2. If we trusted this value, we would attempt to read one block
# too many (which would most likely fail). Therefore, we must correct it
# to 1 before using it.
#
# The conditions for detecting this block come from the
# [`check_abs_block()`](https://github.com/raspberrypi/picotool/blob/6f6458d792b93685a11423b244a585eaa99eafcf/elf2uf2/elf2uf2.cpp#L147)
# function in `picotool`. However, there are some differences:
#
# 1. In Kaitai Struct, we cannot easily check whether all 256 payload
# bytes are set to `0xef`, so we only check the first and last bytes.
# 2. There are .uf2 files in the wild where `flags.has_extension_tags`
# is true, but there are actually no extension tags (the first and
# only tag has a size of 0, which is just a terminator), so our
# condition allows for this case. You can download an example of such
# a .uf2 file here:
# <https://github.com/neednotapply/DC32-cfw/releases/tag/1.69.13.37>
#
# It's worth noting that we cannot require the presence of the
# `extension_tag_type::rp2_ignore_block`
# (`UF2_EXTENSION_RP2_IGNORE_BLOCK`) tag because the UF2 files generated
# by `picotool` prior to
# <https://github.com/raspberrypi/picotool/commit/78c9bd121b09399823b67ee7ea89003ca0d3315f>
# don't have it. Therefore, we check whether this tag is present only if
# `flags.has_extension_tags` is set.
#
# Test .uf2 files with this special block can be downloaded from
# <https://micropython.org/download/RPI_PICO2/>. Note that all the .uf2
# files there are actually two UF2 (sub)files concatenated, and this
# Kaitai Struct implementation parses only one at a time (so in order to
# parse both, you need something like the helper spec `uf2_files.ksy`
# from
# <https://github.com/kaitai-io/kaitai_struct_formats/pull/542#discussion_r3906386820>).
# v1.24.x releases predate the `extension_tag_type::rp2_ignore_block`
# tag, while releases v1.25.0 and later include it.
# @see https://github.com/raspberrypi/picotool/blob/6f6458d792b93685a11423b244a585eaa99eafcf/elf2uf2/elf2uf2.cpp#L147 Git tag "2.3.0"
# @see https://github.com/raspberrypi/picotool/commit/78c9bd121b09399823b67ee7ea89003ca0d3315f Source
def is_rp2350_e10_block
return @is_rp2350_e10_block unless @is_rp2350_e10_block.nil?
@is_rp2350_e10_block = (( ((flags.value == 8192) || (flags.value == 40960)) ) && (family_id == :family_id_rp2xxx_absolute) && (num_blocks_raw == 2) && (block_number == 0) && (len_payload == 256) && (data.payload[0].ord == 239) && (data.payload[-1].ord == 239) && ( ((!(flags.has_extension_tags)) || (data.extension_tags[0].len_tag == 0) || ( ((data.extension_tags[0].len_tag == 4) && (data.extension_tags[0].tag_type == :extension_tag_type_rp2_ignore_block)) )) ))
@is_rp2350_e10_block
end
def num_blocks
return @num_blocks unless @num_blocks.nil?
@num_blocks = (is_rp2350_e10_block ? 1 : num_blocks_raw)
@num_blocks
end
attr_reader :magic
attr_reader :second_magic
attr_reader :flags
##
# Address in flash where `data.payload` should be written, or an offset
# in the file specified by `data.file_name` if `flags.is_file_container`
# is set.
#
# The [official
# spec](https://github.com/microsoft/uf2/blob/90e9741f217f5a40c98ba74d663e408041037578/README.md#payload-sizes)
# says:
#
# > In any event, payload size and target address should always be
# > 4-byte aligned.
attr_reader :target_address
##
# The [official
# spec](https://github.com/microsoft/uf2/blob/90e9741f217f5a40c98ba74d663e408041037578/README.md#payload-sizes)
# says:
#
# > In any event, payload size and target address should always be
# > 4-byte aligned.
attr_reader :len_payload
attr_reader :block_number
##
# Number of blocks that make up the UF2 file to which this block
# belongs. Every block of a file has the same value. It is at least 1
# and every `block_number` in the file must be less than this value
# (which is validated by this Kaitai Struct implementation).
attr_reader :num_blocks_raw
##
# Size of the file this block belongs to, but only if
# `flags.is_file_container` is true. Otherwise, the official spec allows
# this field to be set to anything - though in practice, it's always
# zero.
attr_reader :file_size
attr_reader :family_id
attr_reader :data
attr_reader :final_magic
attr_reader :_raw_data
end
class BlockData < Kaitai::Struct::Struct
def initialize(_io, _parent = nil, _root = nil)
super(_io, _parent, _root)
_read
end
def _read
@payload = @_io.read_bytes(_parent.len_payload)
if _parent.flags.is_file_container
@file_name = (@_io.read_bytes_term(0, false, true, true)).force_encoding("UTF-8")
end
if _parent.flags.has_extension_tags
@extension_tags = []
i = 0
begin
_ = ExtensionTag.new(@_io, self, @_root)
@extension_tags << _
i += 1
end until _.len_tag == 0
end
self
end
##
# Describes a region that doesn't need to be flashed again if the
# checksum matches.
#
# The [official
# spec](https://github.com/microsoft/uf2/blob/90e9741f217f5a40c98ba74d663e408041037578/README.md#md5-checksum)
# says that "This is currently only used on ESP32", but no real-world
# firmware sample has been found (at least none of the .uf2 files from
# <https://github.com/adafruit/tinyuf2/releases>,
# [MicroPython](https://micropython.org/download/) or
# [CircuitPython](https://circuitpython.org/downloads) contain it). It
# was found only in some synthetic test files, e.g.
# <https://github.com/umi-eng/uftwo/blob/35bccf75b4f81c43f088696a8c4a9912f1f4104e/uftwo/tests/checksum_256.uf2>.
def md5_checksum
return @md5_checksum unless @md5_checksum.nil?
if _parent.flags.has_md5_checksum
_pos = @_io.pos
@_io.seek(_io.size - 24)
@md5_checksum = Md5Checksum.new(@_io, self, @_root)
@_io.seek(_pos)
end
@md5_checksum
end
##
# The bytes to be written to `_parent.target_address`, which is either
# an address in flash, or an offset in the file specified by `file_name`
# if `_parent.flags.is_file_container` is set.
attr_reader :payload
attr_reader :file_name
attr_reader :extension_tags
end
##
# @see https://github.com/microsoft/uf2/blob/90e9741f217f5a40c98ba74d663e408041037578/README.md#extension-tags Source
class ExtensionTag < Kaitai::Struct::Struct
def initialize(_io, _parent = nil, _root = nil)
super(_io, _parent, _root)
_read
end
def _read
@len_tag = @_io.read_u1
_ = @len_tag
raise Kaitai::Struct::ValidationExprError.new(@len_tag, @_io, "/types/extension_tag/seq/0") if not ((_ == 0) || (_ >= min_len_tag))
@tag_type = Kaitai::Struct::Stream::resolve_enum(Uf2::EXTENSION_TAG_TYPE, @_io.read_bits_int_le(24))
@_io.align_to_byte
if len_tag != 0
@value = @_io.read_bytes(len_value)
end
@padding = @_io.read_bytes(-(len_tag) % 4)
self
end
def len_value
return @len_value unless @len_value.nil?
@len_value = (len_tag >= min_len_tag ? len_tag - min_len_tag : 0)
@len_value
end
def min_len_tag
return @min_len_tag unless @min_len_tag.nil?
@min_len_tag = 1 + 3
@min_len_tag
end
##
# Total size of the tag in bytes, including this byte and `tag_type`, so
# at least 4. The exception is the last tag which terminates the list -
# it specifies a total size of 0.
attr_reader :len_tag
attr_reader :tag_type
attr_reader :value
##
# Tags are 4-byte aligned, so a tag whose size is not a multiple
# of 4 is followed by padding.
attr_reader :padding
end
##
# @see https://github.com/microsoft/uf2/blob/90e9741f217f5a40c98ba74d663e408041037578/uf2.h#L43-L47 Source
# @see https://github.com/raspberrypi/pico-sdk/blob/98a542c1a62fb549ffb5d66a3e5892b06276b670/src/common/boot_uf2_headers/include/boot/uf2.h#L23-L27 Git tag "2.3.0"
class Flags < Kaitai::Struct::Struct
def initialize(_io, _parent = nil, _root = nil)
super(_io, _parent, _root)
_read
end
def _read
@value = @_io.read_u4le
_ = @value
raise Kaitai::Struct::ValidationExprError.new(@value, @_io, "/types/flags/seq/0") if not ((_ & ~61441 == 0) && (!( ((is_file_container) && (has_extension_tags)) )))
self
end
##
# Indicates whether extension tags are present after the payload.
def has_extension_tags
return @has_extension_tags unless @has_extension_tags.nil?
@has_extension_tags = value & 32768 != 0
@has_extension_tags
end
##
# The field at offset 28 in the block is `family_id` instead of
# `file_size`.
def has_family_id
return @has_family_id unless @has_family_id.nil?
@has_family_id = value & 8192 != 0
@has_family_id
end
##
# Indicates whether `md5_checksum` is present at the end of `data`.
def has_md5_checksum
return @has_md5_checksum unless @has_md5_checksum.nil?
@has_md5_checksum = value & 16384 != 0
@has_md5_checksum
end
##
# When set, the UF2 format is used as a container for regular files
# (akin to a TAR file, or ZIP archive without compression).
#
# `target_address` is the offset in the file where the payload is to be
# written, and `file_size` is the size of that file. The name of the
# destination file is stored in `data.file_name`.
def is_file_container
return @is_file_container unless @is_file_container.nil?
@is_file_container = value & 4096 != 0
@is_file_container
end
##
# Indicates that this block should be skipped when writing the device
# flash. It can be used to store data that does not fit on the device,
# typically embedded source code or debug info.
def not_main_flash
return @not_main_flash unless @not_main_flash.nil?
@not_main_flash = value & 1 != 0
@not_main_flash
end
##
# Only the five bits that we cover in value instances below are defined,
# and no other bit may be set. The [official
# spec](https://github.com/microsoft/uf2/blob/90e9741f217f5a40c98ba74d663e408041037578/README.md#flags)
# says "Currently, there are five flags defined". If any other flags are
# added in the future, this .ksy spec will need to be updated.
#
# The `is_file_container` and `has_extension_tags` flags disagree about
# what follows the payload in `data` (a file name or a list of extension
# tags), so this Kaitai Struct implementation treats them as mutually
# exclusive and will reject a block that sets both. The official spec
# does not specify how this should be handled, but logically there's a
# conflict, so we've decided to strictly treat it as an error.
attr_reader :value
end
##
# @see https://github.com/microsoft/uf2/blob/90e9741f217f5a40c98ba74d663e408041037578/README.md#md5-checksum Source
class Md5Checksum < Kaitai::Struct::Struct
def initialize(_io, _parent = nil, _root = nil)
super(_io, _parent, _root)
_read
end
def _read
@start_address = @_io.read_u4le
@len_region = @_io.read_u4le
@md5 = @_io.read_bytes(16)
self
end
attr_reader :start_address
attr_reader :len_region
attr_reader :md5
end
attr_reader :first_block
attr_reader :blocks
end