boot: add NuttX bootloader with update and recovery support

This commit adds NuttX based bootloader with the support for image
update and recovery if not confirmed. The algorithm utilizes three
flash partitions: primary (image runs from this area), secondary
and tertiary. Secondary and tertiary areas are used for update upload
and recovery.

The update is performed by simple copy from update area to primary
area with recovery being created in recovery area if not already present.
Once image is confirmed by the user, the image in update area is
confirmed as well, update area becomes recovery area and vice versa.
This means the recovery is always present (except for the first update)
and subsequent updates just copy image from update to primary. This
makes the update significantly faster and more considerable to flash
wear while keeping the recovery/revert possibility.

A header (aligned to flash's erase size) must be added to the beginning
of the image. Python script nximage.py can be used to prepend this
header to built binary. The algorithm also uses one erase page at the
end of a partition (partition, not image!) to store flags used to
indicate image confirm status and to detect update/recovery partitions.
Any program uploading update image to the update partition has to erase
this page for the boot to work correctly!

The algorithm implementation is based on a patch initially developed
for MCUboot project but rejected by the project's maintainers

https://github.com/mcu-tools/mcuboot/pull/1902

Signed-off-by: Michal Lenc <michallenc@seznam.cz>
This commit is contained in:
Michal Lenc
2024-10-20 20:48:58 +02:00
committed by Alan C. Assis
parent 242b947342
commit 61fec07c62
11 changed files with 1800 additions and 0 deletions

View File

@@ -0,0 +1,115 @@
"""Python script that prepares the NuttX image to be used with NX bootloader"""
import argparse
import io
import os
import struct
import zlib
import semantic_version
class NxImage:
def __init__(
self, path: str, result: str, version: str, header_size: int, primary: bool
) -> None:
self.path = path
self.result = result
self.size = os.stat(path).st_size
self.version = semantic_version.Version(version)
self.header_size = header_size
self.primary = primary
self.crc = 0
with open(path, "rb") as f:
while data := f.read(io.DEFAULT_BUFFER_SIZE):
self.crc = zlib.crc32(data, self.crc)
def __repr__(self):
repr = (
"<NxImage\n"
f" path: {self.path}\n"
f" result: {self.result}\n"
f" fsize: {self.size}\n"
f" version: {self.version}\n"
f" header_size: {self.header_size}\n"
f" primary: {self.primary}\n"
f" crc: {self.crc}\n"
f">"
)
return repr
def add_header(self):
with open(self.path, "r+b") as src, open(self.result, "w+b") as dest:
if self.primary:
dest.write(b"\xb1\xab\xa0\xac")
else:
dest.write(b"\x4e\x58\x4f\x53")
dest.write(struct.pack("<I", self.size))
if self.primary:
dest.write(struct.pack("<I", 0xFFFFFFFF))
else:
dest.write(struct.pack("<I", self.crc))
dest.write(struct.pack("<H", self.version.major))
dest.write(struct.pack("<H", self.version.minor))
dest.write(struct.pack("<H", self.version.patch))
if not self.version.prerelease:
dest.write(struct.pack("@110s", b"\x00"))
else:
dest.write(
struct.pack("@110s", bytes(self.version.prerelease[0], "utf-8"))
)
dest.write(bytearray(b"\xff") * (self.header_size - 128))
while data := src.read(io.DEFAULT_BUFFER_SIZE):
dest.write(data)
def parse_args() -> argparse.Namespace:
"""Parse passed arguments and return result."""
parser = argparse.ArgumentParser(description="Tool for Nuttx Bootloader")
parser.add_argument(
"--version",
default="0.0.0",
help="Image version according to Semantic Versioning 2.0.0.",
)
parser.add_argument(
"--header_size",
type=lambda x: int(x, 0),
default=0x200,
help="Size of the image header.",
)
parser.add_argument(
"--primary",
action="store_true",
help="Primary image intended to be uploaded directly to primary memory.",
)
parser.add_argument(
"-v",
action="store_true",
help="Verbose output. This prints information abourt the created image.",
)
parser.add_argument(
"PATH",
default="nuttx.bin",
help="Path to the NuttX image.",
)
parser.add_argument(
"RESULT",
default="nuttx.img",
help="Path where the resulting NuttX image is stored.",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
image = NxImage(
args.PATH, args.RESULT, args.version, args.header_size, args.primary
)
image.add_header()
if args.v:
print(image)
if __name__ == "__main__":
main()