From c57c419f94c7f5a41b4f906c6dd52d1564f4ad4a Mon Sep 17 00:00:00 2001 From: Theo Hill Date: Mon, 20 May 2024 21:02:40 -0700 Subject: [PATCH 1/2] Toolchain improvements for GDB, VulCAN This replaces the outdated arm-none-eabi-gdb with gdb-multiarch, which modern distros package instead. This also adds a udev rule and systemd service which will automatically create a vulan0 network device when the adapter is plugged in. Place 99-vulcan.rule in /etc/udev/rules.d and slcand@.service in /etc/systemd/system. The rules for both will then have to be reloaded: `udevadm control --reload` and `systemctl daemon-reload`. --- README.md | 2 +- doc/toolchain.md | 4 ---- toolchain/99-vulcan.rules | 1 + toolchain/README.md | 2 +- toolchain/slcand@.service | 9 +++++++++ toolchain/toolchain.mk | 4 ++-- 6 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 toolchain/99-vulcan.rules create mode 100644 toolchain/slcand@.service diff --git a/README.md b/README.md index a70f90e4..c741ebcc 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ how to install these tools, as it varies between systems. Tools required: * make * arm-none-eabi-gcc -* arm-none-eabi-gdb (may require a symlink to gdb-multiarch on Debian systems) +* gdb-multiarch * openocd (Required, this is how we program and debug things) * stlink (Gets udev rules up and running, may be helpful) * srecord diff --git a/doc/toolchain.md b/doc/toolchain.md index 29a33240..700573bf 100644 --- a/doc/toolchain.md +++ b/doc/toolchain.md @@ -37,10 +37,6 @@ The packages needed for Debian are as follow: libusb-1.0-0 libusb-1.0-0-dev make pkg-config python3 srecord stlink-tools tcl xxd` -Make a symbollic link to `arm-none-eabi-gdb`: - -`$ sudo ln -s /usr/bin/gdb-multiarch /usr/bin/arm-none-eabi-gdb` - OpenOCD can be built as follow: - `$ git clone https://git.code.sf.net/p/openocd/code openocd` diff --git a/toolchain/99-vulcan.rules b/toolchain/99-vulcan.rules new file mode 100644 index 00000000..6e3cf809 --- /dev/null +++ b/toolchain/99-vulcan.rules @@ -0,0 +1 @@ +ACTION=="add", SUBSYSTEM=="tty", ATTRS{idVendor}=="ad50", ATTRS{idProduct}=="60c4", SYMLINK+="vulcan%n", TAG+="systemd", ENV{SYSTEMD_WANTS}="slcand@vulcan%n.service" diff --git a/toolchain/README.md b/toolchain/README.md index 333e9f02..8ffc3364 100644 --- a/toolchain/README.md +++ b/toolchain/README.md @@ -100,7 +100,7 @@ Terminal 2: ``` user@hostname:oresat-firmware/src/f0/app_blinky ➜ make gdb -arm-none-eabi-gdb -q /home/user/Projects/PSAS/oresat-firmware/src/f0/app_blinky/./build/app_blinky.elf -cd ../../../toolchain -x ./gdbstl.cmd +gdb-multiarch -q /home/user/Projects/PSAS/oresat-firmware/src/f0/app_blinky/./build/app_blinky.elf -cd ../../../toolchain -x ./gdbstl.cmd Reading symbols from /home/user/Projects/PSAS/oresat-firmware/src/f0/app_blinky/./build/app_blinky.elf... _port_thread_start () at ../../../ChibiOS/os/common/ports/ARMCMx/compilers/GCC/chcoreasm_v6m.S:110 110 bl chThdExit diff --git a/toolchain/slcand@.service b/toolchain/slcand@.service new file mode 100644 index 00000000..1ff3a2fa --- /dev/null +++ b/toolchain/slcand@.service @@ -0,0 +1,9 @@ +[Unit] +Description=Manages slcand for %I +BindsTo=dev-%i.device +After=dev-%i.device + + +[Service] +Type=simple +ExecStart=/usr/bin/slcand -F -o -c -s8 -t hw -S 3000000 /dev/%i %i diff --git a/toolchain/toolchain.mk b/toolchain/toolchain.mk index 90d94bc4..aca2f896 100644 --- a/toolchain/toolchain.mk +++ b/toolchain/toolchain.mk @@ -31,10 +31,10 @@ write_stl: gdb: $(GDB_ELF) gdb_ocd gdb_ocd: - $(TRGT)gdb -q $(shell pwd)/$(GDB_ELF) -cd $(BOARDDIR) -ex "target remote | openocd -f oocd.cfg -c '$(SERIAL_ARG) gdb_port pipe'" -x $(GDB_OOCD_CFG) + gdb-multiarch -q $(shell pwd)/$(GDB_ELF) -cd $(BOARDDIR) -ex "target extended-remote | openocd -f oocd.cfg -c '$(SERIAL_ARG) gdb_port pipe'" -x $(GDB_OOCD_CFG) gdb_stl: - $(TRGT)gdb -q $(shell pwd)/$(GDB_ELF) -cd $(TOOLCHAIN) -x ./$(GDB_STL_CFG) + gdb-multiarch -q $(shell pwd)/$(GDB_ELF) -cd $(TOOLCHAIN) -x ./$(GDB_STL_CFG) serial: picocom -b 115200 /dev/serial/by-id/usb-STMicroelectronics_STLINK-V3_$(SERIAL_RAW)-if02 From 2d0ecd278c6c46a0776319e52c2365c068d8382b Mon Sep 17 00:00:00 2001 From: Theo Hill Date: Mon, 20 May 2024 21:49:02 -0700 Subject: [PATCH 2/2] Overhaul F0 bootloader This overhauls the F0 bootloader and associated tooling. The original bootloader's command can IDs were incompatible with CANopen's address assignment and so the instigator for this patch was to reassign them to the reserved unused addresses in CANopen's protocol. While I was there I've fixed a number of other issues too: - Fixed linker scripts and memory layout. - Fixed memory region addressing, using linker defined symbols instead of ints. - Improved compatibility with the protocol that this bootloader is based on, AN3154 - Fixed a number of buffer overflows and off-by-one errors in the protocol - Rearranged the metadata section to not have 1kb of empty space in every image. This speeds up bootloader image write time. - Enabled CRC hardware acceleration. - Added a python based unit test suite - Overhauled the python write script to be used as a library as well - Fixed type issues - Reduced oresat common dependencies --- .gitignore | 4 + common/can_bootloader.c | 4 + common/include/oresat_f0.h | 43 +- common/oresat_f0.c | 15 +- ld/STM32F091xC-app.ld | 52 +- ld/STM32F091xC-bootloader.ld | 28 +- src/f0/app_bootloader/Makefile | 7 +- src/f0/app_bootloader/bootloader.py | 332 ++++++ src/f0/app_bootloader/cfg/halconf.h | 12 + src/f0/app_bootloader/main.c | 1019 +++++++++-------- src/f0/app_bootloader/requirements.txt | 1 + src/f0/app_bootloader/test_bootloader.py | 193 ++++ .../app_bootloader/test_stm32_bootloader.py | 271 ----- toolchain/f0_pack_image.py | 57 +- 14 files changed, 1167 insertions(+), 871 deletions(-) create mode 100755 src/f0/app_bootloader/bootloader.py create mode 100644 src/f0/app_bootloader/requirements.txt create mode 100755 src/f0/app_bootloader/test_bootloader.py delete mode 100644 src/f0/app_bootloader/test_stm32_bootloader.py diff --git a/.gitignore b/.gitignore index d3be4dff..0813a9f8 100644 --- a/.gitignore +++ b/.gitignore @@ -82,3 +82,7 @@ build*/ # Vagrant /.vagrant/ + +# Python +__pycache__ +.direnv diff --git a/common/can_bootloader.c b/common/can_bootloader.c index 0172f01a..9561a802 100644 --- a/common/can_bootloader.c +++ b/common/can_bootloader.c @@ -24,6 +24,10 @@ uint8_t m0_firmware_temp_buffer[M0_FIRMWARE_UPDATE_WRITE_CHUNK_SIZE]; #endif +#define ORESAT_F0_FLASH_START_ADDRESS 0x08000000 +#define ORESAT_F0_FIRMWARE_CRC_ADDRESS 0x0800A7F4 +#define ORESAT_F0_FIRMWARE_CODE_ADDRESS 0x0800A804 + /** * Used to fully initialize a can_bootloader_config_t structure. * diff --git a/common/include/oresat_f0.h b/common/include/oresat_f0.h index 58dd51f3..74cc046b 100644 --- a/common/include/oresat_f0.h +++ b/common/include/oresat_f0.h @@ -1,28 +1,35 @@ #ifndef COMMON_INCLUDE_ORESAT_F0_H_ #define COMMON_INCLUDE_ORESAT_F0_H_ -/* TODO: It'd be nice to derive these values from the linker script as the authoratative source */ -#define ORESAT_F0_FLASH_START_ADDRESS 0x08000000 - -#define ORESAT_F0_FIRMWARE_CRC_ADDRESS 0x0800A000 -#define ORESAT_F0_FIRMWARE_LENGTH_ADDRESS (ORESAT_F0_FIRMWARE_CRC_ADDRESS + 0x4) -#define ORESAT_F0_FIRMWARE_VERSION_ADDRESS (ORESAT_F0_FIRMWARE_CRC_ADDRESS + 0x8) -#define ORESAT_F0_FIRMWARE_CODE_ADDRESS (ORESAT_F0_FIRMWARE_CRC_ADDRESS + 0x400) -#define ORESAT_F0_FIRMWARE_CODE_END_ADDRESS 0x0803C000 - -#define ORESAT_F0_FIRMWARE_MAXIMUM_LENGTH (ORESAT_F0_FIRMWARE_CODE_END_ADDRESS - ORESAT_F0_FIRMWARE_CRC_ADDRESS) - - #ifdef __cplusplus extern "C" { #endif -extern int __flash0_base__; -extern size_t __flash0_size__; -extern int __flash0_end__; -extern int __flash1_base__; -extern size_t __flash1_size__; -extern int __flash1_end__; +// See STM32F091xC-bootloader.ld/STM32F4091xC-app.ld for definitions and +// rules_memory.ld for declarations. +extern uint8_t __flash0_base__[]; +extern uint8_t __flash0_end__[]; +extern uint8_t __flash1_base__[]; +extern uint8_t __flash1_end__[]; +extern uint8_t __flash2_base__[]; +extern uint8_t __flash2_end__[]; +extern uint8_t __ram0_base__[]; +extern uint8_t __ram0_end__[]; +extern uint8_t __ram1_base__[]; +extern uint8_t __ram1_end__[]; + +// The __*_size__ symbols are weird. The contents aren't relevent, it's the +// address, converted to a number, which is the represented region size. +extern const void __flash0_size__; +extern const void __flash1_size__; +extern const void __flash2_size__; +extern const void __ram0_size__; +extern const void __ram1_size__; +#define FLASH0_SIZE ((const uint32_t)(&__flash0_size__)) +#define FLASH1_SIZE ((const uint32_t)(&__flash1_size__)) +#define FLASH2_SIZE ((const uint32_t)(&__flash2_size__)) +#define RAM0_SIZE ((const uint32_t)(&__ram0_size__)) +#define RAM1_SIZE ((const uint32_t)(&__ram1_size__)) #ifdef __cplusplus } diff --git a/common/oresat_f0.c b/common/oresat_f0.c index 748ffc33..b4d69ab1 100644 --- a/common/oresat_f0.c +++ b/common/oresat_f0.c @@ -4,14 +4,19 @@ /** - * This function should be called on applications using the STM32F091xC-app.ld linker script and using the bootloader to run them. + * This function is called by applications using the STM32F091xC-app.ld + * linker script and using the bootloader to run them. It overwrites a weak + * symbol from ChibiOS's CRT0 and so will be called late in the init sequence. */ void __late_init(void) { - /* Relocate by software the vector table to the internal SRAM at 0x20000000 ***/ - /* Copy the vector table from the Flash (mapped at the base of the application - load address 0x08003000) to the base address of the SRAM at 0x20000000. */ + // Relocate by software the vector table to the internal SRAM at 0x20000000. + // The vector table comes from flash, mapped at the base of the application + // region, load address __flash2_base__ (0x0800A800). + // + // The SRAM then gets remapped to 0x00000000 to function as the real vector + // table - memcpy((void *)0x20000000, (void *)ORESAT_F0_FIRMWARE_CODE_ADDRESS, 0xC0); + memcpy(__ram0_base__, __flash2_base__, 0xC0); __DSB(); /* Remap SRAM at 0x00000000 */ SYSCFG->CFGR1 |= SYSCFG_CFGR1_MEM_MODE; diff --git a/ld/STM32F091xC-app.ld b/ld/STM32F091xC-app.ld index b7918b28..b5751e3f 100644 --- a/ld/STM32F091xC-app.ld +++ b/ld/STM32F091xC-app.ld @@ -1,34 +1,16 @@ -/* - ChibiOS - Copyright (C) 2006..2018 Giovanni Di Sirio - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -/* - * STM32F091xC memory setup. - */ +/* STM32F091xC memory setup. It has 256k flash (128 2k pages) and 32k SRAM */ MEMORY { flash0 (rx) : org = 0x08000000, len = 40k /* Bootloader */ - flash1 (rx) : org = 0x0800A400, len = 199k /* Application */ - flash2 (rx) : org = 0x00000000, len = 0 + flash1 (rx) : org = 0x0800A000, len = 2k /* Metadata */ + flash2 (rx) : org = 0x0800A800, len = 214k /* Application */ flash3 (rx) : org = 0x00000000, len = 0 flash4 (rx) : org = 0x00000000, len = 0 flash5 (rx) : org = 0x00000000, len = 0 flash6 (rx) : org = 0x00000000, len = 0 flash7 (rx) : org = 0x00000000, len = 0 - ram0 (wx) : org = 0x20000000, len = 32k - ram1 (wx) : org = 0x20000400, len = 31k + ram0 (wx) : org = 0x20000000, len = 32708 /* 32k - 4 (bootloader magic) */ + ram1 (wx) : org = 0x20000400, len = 31k /* 32k - 1k (fw vector table) */ ram2 (wx) : org = 0x00000000, len = 0 ram3 (wx) : org = 0x00000000, len = 0 ram4 (wx) : org = 0x00000000, len = 0 @@ -41,27 +23,27 @@ MEMORY and a load region (_LMA suffix).*/ /* Flash region to be used for exception vectors.*/ -REGION_ALIAS("VECTORS_FLASH", flash1); -REGION_ALIAS("VECTORS_FLASH_LMA", flash1); +REGION_ALIAS("VECTORS_FLASH", flash2); +REGION_ALIAS("VECTORS_FLASH_LMA", flash2); /* Flash region to be used for constructors and destructors.*/ -REGION_ALIAS("XTORS_FLASH", flash1); -REGION_ALIAS("XTORS_FLASH_LMA", flash1); +REGION_ALIAS("XTORS_FLASH", flash2); +REGION_ALIAS("XTORS_FLASH_LMA", flash2); /* Flash region to be used for code text.*/ -REGION_ALIAS("TEXT_FLASH", flash1); -REGION_ALIAS("TEXT_FLASH_LMA", flash1); +REGION_ALIAS("TEXT_FLASH", flash2); +REGION_ALIAS("TEXT_FLASH_LMA", flash2); /* Flash region to be used for read only data.*/ -REGION_ALIAS("RODATA_FLASH", flash1); -REGION_ALIAS("RODATA_FLASH_LMA", flash1); +REGION_ALIAS("RODATA_FLASH", flash2); +REGION_ALIAS("RODATA_FLASH_LMA", flash2); /* Flash region to be used for various.*/ -REGION_ALIAS("VARIOUS_FLASH", flash1); -REGION_ALIAS("VARIOUS_FLASH_LMA", flash1); +REGION_ALIAS("VARIOUS_FLASH", flash2); +REGION_ALIAS("VARIOUS_FLASH_LMA", flash2); /* Flash region to be used for RAM(n) initialization data.*/ -REGION_ALIAS("RAM_INIT_FLASH_LMA", flash1); +REGION_ALIAS("RAM_INIT_FLASH_LMA", flash2); /* RAM region to be used for Main stack. This stack accommodates the processing of all exceptions and interrupts.*/ @@ -73,7 +55,7 @@ REGION_ALIAS("PROCESS_STACK_RAM", ram1); /* RAM region to be used for data segment.*/ REGION_ALIAS("DATA_RAM", ram1); -REGION_ALIAS("DATA_RAM_LMA", flash1); +REGION_ALIAS("DATA_RAM_LMA", flash2); /* RAM region to be used for BSS segment.*/ REGION_ALIAS("BSS_RAM", ram1); diff --git a/ld/STM32F091xC-bootloader.ld b/ld/STM32F091xC-bootloader.ld index 748729ca..7100a3e2 100644 --- a/ld/STM32F091xC-bootloader.ld +++ b/ld/STM32F091xC-bootloader.ld @@ -1,34 +1,16 @@ -/* - ChibiOS - Copyright (C) 2006..2018 Giovanni Di Sirio - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -/* - * STM32F091xC memory setup. - */ +/* STM32F091xC memory setup. It has 256k flash (128 2k pages) and 32k SRAM */ MEMORY { flash0 (rx) : org = 0x08000000, len = 40k /* Bootloader */ - flash1 (rx) : org = 0x0800A400, len = 199k /* Application */ - flash2 (rx) : org = 0x00000000, len = 0 + flash1 (rx) : org = 0x0800A000, len = 2k /* Metadata */ + flash2 (rx) : org = 0x0800A800, len = 214k /* Application */ flash3 (rx) : org = 0x00000000, len = 0 flash4 (rx) : org = 0x00000000, len = 0 flash5 (rx) : org = 0x00000000, len = 0 flash6 (rx) : org = 0x00000000, len = 0 flash7 (rx) : org = 0x00000000, len = 0 - ram0 (wx) : org = 0x20000000, len = 32k - ram1 (wx) : org = 0x20000400, len = 31k + ram0 (wx) : org = 0x20000000, len = 32708 /* 32k - 4 for bootloader magic */ + ram1 (wx) : org = 0x20000400, len = 31k /* 32k - 1k for vector table */ ram2 (wx) : org = 0x00000000, len = 0 ram3 (wx) : org = 0x00000000, len = 0 ram4 (wx) : org = 0x00000000, len = 0 diff --git a/src/f0/app_bootloader/Makefile b/src/f0/app_bootloader/Makefile index cb735565..a21dbc74 100644 --- a/src/f0/app_bootloader/Makefile +++ b/src/f0/app_bootloader/Makefile @@ -5,7 +5,7 @@ # Compiler options here. ifeq ($(USE_OPT),) - USE_OPT = -Og -ggdb -fomit-frame-pointer -falign-functions=16 + USE_OPT = -Og -ggdb -falign-functions=16 endif # C specific options here (added to USE_OPT). @@ -106,8 +106,6 @@ DEPDIR := $(APP_ROOT)/.dep BOARDDIR = $(PROJ_ROOT)/boards/$(BOARD) # Project specific files. -#include $(PROJ_SRC)/oresat.mk -include $(PROJ_SRC)/bootloader.mk include $(PROJ_SRC)/util.mk # Licensing files. @@ -125,9 +123,6 @@ include $(CHIBIOS)/os/common/ports/ARMCMx/compilers/GCC/mk/port_v6m.mk # Auto-build files in ./source recursively. include $(CHIBIOS)/tools/mk/autobuild.mk # Other files (optional). -#include $(CHIBIOS)/test/lib/test.mk -#include $(CHIBIOS)/test/rt/rt_test.mk -#include $(CHIBIOS)/test/oslib/oslib_test.mk include $(CHIBIOS)/os/hal/lib/streams/streams.mk # Define linker script file here diff --git a/src/f0/app_bootloader/bootloader.py b/src/f0/app_bootloader/bootloader.py new file mode 100755 index 00000000..25f837f1 --- /dev/null +++ b/src/f0/app_bootloader/bootloader.py @@ -0,0 +1,332 @@ +#!/usr/bin/env python3 +'''This does testing of the CAN bootloader from python using a CANable attached to the host PC''' + +import argparse +import math +import struct +import time +from abc import ABC, abstractmethod +from copy import deepcopy +from dataclasses import dataclass +from itertools import islice + +import can + +# Implements some of ST-AN3154 + + +def batched(iterable, n): + '''batched('ABCDEFG', 3) → ABC DEF G''' + # FIXME: Replace with itertools.batched when 3.12 is standard. Copied from + # https://docs.python.org/3/library/itertools.html#itertools.batched + if n < 1: + raise ValueError('n must be at least one') + it = iter(iterable) + while batch := tuple(islice(it, n)): + yield batch + + +class ExpectMessage(can.Message): + '''Extends can.Message with a mask to compare only the unmasked bytes in .equals() + + Each bit in mask corresponds to a byte in data to compare or ignore + ''' + + def __init__(self, arbitration_id=0, dlc=None, data=None, is_rx=True, mask=0xFF): + assert 0x00 <= mask <= 0xFF + self.mask = [bool(int(b)) for b in reversed(f"{mask:08b}")] + super().__init__( + arbitration_id=arbitration_id, + dlc=dlc, + data=data, + is_rx=is_rx, + is_extended_id=False, + check=True, + ) + + def equals(self, other, *_): + if other is None: + return False + copy = deepcopy(other) + data = [] + for mask, theirs, ours in zip(self.mask, copy.data, self.data): + if mask: + data.append(theirs) + else: + data.append(ours) + copy.data = bytearray(data) + return super().equals(copy, timestamp_delta=None, check_channel=False) + + +class UnexpectedNack(ValueError): + pass + + +class Command(ABC): + ACK = 0x79 + NACK = 0x1F + offset = 0 + + def __init__(self, node_id): + self.canid = node_id + self.offset + + @abstractmethod + def run(self, reader): + pass + + def expect_ack(self, reader): + ack = ExpectMessage(arbitration_id=self.canid, data=[self.ACK]) + nak = ExpectMessage(arbitration_id=self.canid, data=[self.NACK]) + got = reader.get_message(timeout=10) + if not ack.equals(got): + if nak.equals(got): + raise UnexpectedNack("was expecting ACK") + raise ValueError(f"unexpected message in bagging area: {got}") + + def expect_nack(self, reader): + nak = ExpectMessage(arbitration_id=self.canid, data=[self.NACK]) + got = reader.get_message(timeout=10) + if not nak.equals(got): + raise ValueError(f"unexpected message in bagging area: {got}") + + +class Announce(Command): + '''The boootloader announces its presence on the CAN bus and then will wait 100ms for a response + + If the bootloader receives the response it will then enter and stay in command mode, where the + remaining commands become available. It will remain in that mode until a 'Go' is issued. + ''' + + offset = 0 + + def __init__(self, node_id): + super().__init__(node_id) + self.expect = ExpectMessage( + arbitration_id=self.canid, + data=[0x01, 0x02, 0x03, 0x04, 0, 0, 0, 0], + mask=0x0F, + ) + + def run(self, reader): + got = reader.get_message(timeout=100) + if not self.expect.equals(got): + raise ValueError(f"unexpected message in bagging area: {got}") + + yield can.Message( + arbitration_id=got.arbitration_id, + data=got.data[4:8] + b'\x01\x02\x03\x04', + is_extended_id=False, + ) + + +class Go(Command): + '''Commands bootloader to restart. + + The bootloader will revalidate the firmware and perform the standard bus announce message + ''' + + offset = 0x782 + + def __init__(self, node_id): + super().__init__(node_id) + self.command = can.Message(arbitration_id=self.canid, is_extended_id=False) + + def run(self, reader): + yield self.command + self.expect_ack(reader) + + +@dataclass +class GetResponse: + version: int + get: int + read: int + go: int + write: int + erase: int + + +class Get(Command): + '''Gets the version and list of command can IDs + + Can be used to verify which features the bootloader has avaliable. + ''' + + offset = 0x780 + + def __init__(self, node_id): + super().__init__(node_id) + self.msg = can.Message(arbitration_id=self.canid, is_extended_id=False) + + def run(self, reader): + yield self.msg + self.expect_ack(reader) + + count = reader.get_message(0.25) + frames = count.data[0] + 1 + + data = [] + for _ in range(frames): + r = reader.get_message(0.25) + data.append(int.from_bytes(r.data, byteorder='big', signed=False)) + + self.expect_ack(reader) + self.result = GetResponse(*data) + + +class ReadMemory(Command): + '''Reads up to a 256 byte region of memory. + + Valid regions are between addresses 0x0800000 and 0x0803FFFF (flash) and between addresses + 0x02000000 and 0x02007FFF for SRAM. System and Option regions are not currently implemented. + ''' + + offset = 0x781 + + def __init__(self, node_id, address, length): + super().__init__(node_id) + self.length = length + self.address = address + self.expected_frames = int(math.ceil((length) / 8.0)) + self.msg = can.Message( + arbitration_id=self.canid, + data=struct.pack('>IB', address, length - 1), + is_extended_id=False, + ) + + def run(self, reader): + yield self.msg + self.expect_ack(reader) + + data = bytearray() + for _ in range(self.expected_frames): + r = reader.get_message(0.25) + data.extend(r.data) + + self.expect_ack(reader) + self.result = data + + +class ErasePages(Command): + '''Erase a list of up to 7 2kb pages. + + Valid pages are 20 (metadata) and 21-127 (application firmware) + ''' + + offset = 0x02 + + def __init__(self, node_id, pages): + super().__init__(node_id) + self.count = len(pages) + assert 0 <= self.count <= 7 + if self.count == 0: + self.count = 256 # no pages given == erase all pages + data = [self.count - 1] + data.extend(pages) + self.msg = can.Message(arbitration_id=self.canid, data=data, is_extended_id=False) + + @classmethod + def all(cls, node_id): # FIXME: Not implemented in bootloader + return cls(node_id, []) + + def run(self, reader): + yield self.msg + # command ack + self.expect_ack(reader) + # effect ack + for _ in range(self.count): + self.expect_ack(reader) + + +class WriteMemory(Command): + '''Writes up to 256 bytes to flash. + + The page where writes take place must have been erased first. Valid addresses are between + 0x0800A000 and 0x0800A7FF (metadata) and between 0x0800A800 and 0x0803FFFF (application + firmware). It's recommended for alignment reasons to only start writes at even number addresses. + + To write a standard OreSat application image (e.g. foo.crc32.bin), start writing at 0x0800A7F4. + The image is prefixed with 12 bytes of metadata and then the actual application will start at + 0x0800A800. + ''' + + offset = 0x01 + + def __init__(self, node_id, base_address, byte_list): + super().__init__(node_id) + self.byte_list = byte_list + self.msg = can.Message( + arbitration_id=self.canid, + data=struct.pack(">IB", base_address, len(byte_list) - 1), + is_extended_id=False, + ) + + def run(self, reader): + yield self.msg + self.expect_ack(reader) + + for chunk in batched(self.byte_list, 8): + yield can.Message(arbitration_id=self.canid, data=chunk, is_extended_id=False) + + self.expect_ack(reader) + + +def firmware_update(node_id, address, firmware_image): + commands = [] + page_list = range(20, 127) + for chunk in batched(page_list, 7): + commands.append(ErasePages(node_id, chunk)) + + with open(firmware_image, 'rb') as f: + while chunk := f.read(256): + commands.append(WriteMemory(node_id, address, chunk)) + address += len(chunk) + return commands + + +def write_firmware(bus, node_id, filename): + reader = can.BufferedReader() + can.Notifier(bus, [can.Printer(), reader]) + + commands = [Announce(node_id), Get(node_id)] + commands.extend(firmware_update(node_id, 0x0800A7F4, filename)) + commands.append(Go(node_id)) + for command in commands: + print("~~~ Starting", command.__class__.__name__) + for message in command.run(reader): + print("TX msg:", message) + bus.send(message) + # FIXME: send() timeout option doesn't seem to work for socketcan. Get + # CanOperationError: Failed to transmit: No buffer space available [Error Code 105] + # if long writes are transmitted too quickly + time.sleep(0.01) + if isinstance(command, (Get, ReadMemory)): + print("Result:", command.result) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-b', '--bustype', default='socketcan', help="default: %(default)s") + parser.add_argument('-c', '--channel', default='can0', help="default: %(default)s") + parser.add_argument('-r', '--bitrate', default=1000000, help="default: %(default)s", type=int) + parser.add_argument('-n', '--node-id', default=0x7C, help="default: %(default)s", type=int) + parser.add_argument('-p', '--program', help="The image to write (usually foo.crc32.bin") + + args = parser.parse_args() + + filters = [ + {'can_id': Announce.offset + args.node_id, 'can_mask': 0x7FF, 'extende': False}, + {'can_id': Go.offset + args.node_id, 'can_mask': 0x7FF, 'extended': False}, + {'can_id': Get.offset + args.node_id, 'can_mask': 0x7FF, "extended": False}, + {'can_id': ReadMemory.offset + args.node_id, 'can_mask': 0x7FF, 'extended': False}, + {'can_id': WriteMemory.offset + args.node_id, 'can_mask': 0x7FF, 'extended': False}, + {'can_id': ErasePages.offset + args.node_id, 'can_mask': 0x7FF, 'extended': False}, + ] + + with can.Bus( + bustype=args.bustype, + channel=args.channel, + bitrate=args.bitrate, + can_filters=filters, + ) as b: + write_firmware(b, args.node_id, args.program) diff --git a/src/f0/app_bootloader/cfg/halconf.h b/src/f0/app_bootloader/cfg/halconf.h index 05e8e420..9ee6f822 100644 --- a/src/f0/app_bootloader/cfg/halconf.h +++ b/src/f0/app_bootloader/cfg/halconf.h @@ -526,6 +526,18 @@ #define WSPI_USE_MUTUAL_EXCLUSION TRUE #endif +/*===========================================================================*/ +/* Oresat custom CRC driver related settings. */ +/*===========================================================================*/ + +/** + * @brief Enables the CRC driver to use the hardware CRC peripheral. + * @note The driver falls back to a software implementation if FALSE + */ +#if !defined(USE_CRC_HW) || defined(__DOXYGEN__) +#define USE_CRC_HW TRUE +#endif + #endif /* HALCONF_H */ /** @} */ diff --git a/src/f0/app_bootloader/main.c b/src/f0/app_bootloader/main.c index 49da82bb..e76ad1af 100644 --- a/src/f0/app_bootloader/main.c +++ b/src/f0/app_bootloader/main.c @@ -1,20 +1,3 @@ -/* - ChibiOS - Copyright (C) 2006..2018 Giovanni Di Sirio - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -/* ChibiOS header files */ #include "ch.h" #include "hal.h" #include "chprintf.h" @@ -22,580 +5,648 @@ #include "can_hw.h" #include "crc.h" #include +// FIXME locking for flash commands #include "flash_f0.h" -#include "can_bootloader.h" #include "util.h" -#define BOOTLOADER_VERSION 0xAB +#define ARRAY_LEN(x) (sizeof(x)/sizeof(x[0])) +#define MIN(a,b) (((a)<(b))?(a):(b)) + +#define SERIAL ((BaseSequentialStream*)&SD2) +#ifdef DEBUG_PRINT +#define dbgprintf(...) chprintf(SERIAL, __VA_ARGS__) +#else +#define dbgprintf(...) +#endif + +#define BOOTLOADER_VERSION 0xAC #define CAN_DRIVER &CAND1 #define CAN_RECEIVE_TIMEOUT TIME_MS2I(150) #define CAN_RECEIVE_TIMEOUT_LONG TIME_MS2I(500) #define CAN_TRANSMIT_TIMEOUT TIME_MS2I(150) -#define TEMP_WRITE_BUFFER_SIZE 4096 -#define BOOTLOADER_VALIDATED_FIRMWARE_MAGIC_NUMBER 0x12345678 -//Memory address at the 31k mark, not allocated or cleared by CRT of hte OS due to linker script specifying RAM as 31k instead of the full 32k -volatile uint32_t *bootloader_magic_number_pointer = (uint32_t *) 0x20007C00; - -uint32_t *cpu_unique_id_low = (uint32_t *)UID_BASE; -uint8_t bootloader_temp_write_buffer[TEMP_WRITE_BUFFER_SIZE]; //Spec says maximum of 2048 bytes needed for buffer - -#define DEBUG_SD (BaseSequentialStream *) &SD2 - -void soft_reset_cortex_m0(void) { - canStop(CAN_DRIVER); - sdStop(&SD2); - - chSysDisable(); - - NVIC_SystemReset(); +#define CAN_ANNOUNCE_MAGIC_NUMBER 0x04030201 +#define UID_LOW (*((const uint32_t *) UID_BASE)) +#define BOOTLOADER_ACK 0x79 +#define BOOTLOADER_NACK 0x1F + +struct FirmwareMetadata { + uint32_t crc; + uint32_t length; + uint32_t version; +} __attribute__((packed)); + +struct FirmwareVectors { + uint32_t main_stack_addr; + __attribute__((noreturn)) void (*reset_handler)(void); +} __attribute__((packed)); + +// The firmware metadata block starts takes up flash page 20, between the +// bootloader and firmware blocks. The relevant metadata lives at the end of the block +#define METADATA (((struct FirmwareMetadata*)(__flash1_end__)) - 1) +#define FIRMWARE_MAX_LENGTH (FLASH1_SIZE + FLASH2_SIZE) +#define FIRMWARE_CRC_START ((void*)&(METADATA->length)) +#define VECTORS ((struct FirmwareVectors*)(__flash2_base__)) + +// The CAN bootloader protocol described in ST Application Note AN3154 +// designates CAN IDs for each of the bootloader commands, but there are two +// flaws with the layout for our purposes: +// - There's no way to distinguish messages from multiple bootloaders on the +// same CAN network - they all use the same IDs. (See also AN3154 p7 note) +// - The bootloader is not CANopen aware, so messages intrude into the CANopen +// address space. Particularly NMT messages which are top priority. +// The previous iteration attempted to solve the first flaw by extending the +// protocol by broadcasting the UID of the MCU as an initial step and then +// looking for the UID as part of the enter wait-for-command stage. The +// downside is that we have no way to associate the UID to CANopen Node ID +// a priori, and we also expect the hardware to change, meaning the UIDs would +// have to be re-associated every time. It also further impinged on the CANopen +// address space. +// +// This new scheme fits the bootloader commands entirely into the CANopen +// reserved address spaces, that are assigned no meaning in CANopen. It also +// remaps the commands based on Node ID, so that each card has a unique set of +// command CAN IDs. This scheme relies on Oresat's convention that Node IDs are +// spaced at least 3, but generally 4 apart. The only downside is that the CAN +// IDs need to be generated at runtime, they are not static. +// +// See CiA301 table 40 (also note typo in decimal representation for 780h) for +// reserved address ranges. See oresat-configs cards.csv for assigned node IDs +#define COMMAND_ANNOUNCE_OFFSET (0x000) +#define COMMAND_WRITE_MEMORY_OFFSET (0x001) +#define COMMAND_ERASE_OFFSET (0x002) +#define COMMAND_GET_OFFSET (0x780) +#define COMMAND_READ_MEMORY_OFFSET (0x781) +#define COMMAND_GO_OFFSET (0x782) + +#define COMMAND_ANNOUNCE(node_id) ((node_id) + COMMAND_ANNOUNCE_OFFSET) +#define COMMAND_WRITE_MEMORY(node_id) ((node_id) + COMMAND_WRITE_MEMORY_OFFSET) +#define COMMAND_ERASE(node_id) ((node_id) + COMMAND_ERASE_OFFSET) +#define COMMAND_GET(node_id) ((node_id) + COMMAND_GET_OFFSET) +#define COMMAND_READ_MEMORY(node_id) ((node_id) + COMMAND_READ_MEMORY_OFFSET) +#define COMMAND_GO(node_id) ((node_id) + COMMAND_GO_OFFSET) + +#define OPTION_DATA0 ((FLASH->OBR & FLASH_OBR_DATA0_Msk) >> FLASH_OBR_DATA0_Pos) + +static uint8_t node_id(void) { + const uint8_t id = OPTION_DATA0; + if(id == 0 || id == 0xFF || id % 4) { + return 0x7C; // prevents commands from intruding into CANopen ID space + } + return id; } -/** - * @return True if the key meta-data fields of the firmware image are reasonable and valid. False other wise - */ -bool validate_firmware_meta_data(const uint32_t length, const uint32_t expected_crc32) { - if (length == 0|| expected_crc32 == 0 || length == UINT32_MAX || expected_crc32 == UINT32_MAX) { - return false; +static const char* command_offset_to_str(const uint32_t v) { + switch (v) { + case COMMAND_GET_OFFSET: + return "COMMAND_GET"; + case COMMAND_READ_MEMORY_OFFSET: + return "COMMAND_READ_MEMORY"; + case COMMAND_GO_OFFSET: + return "COMMAND_GO"; + case COMMAND_WRITE_MEMORY_OFFSET: + return "COMMAND_WRITE_MEMORY"; + case COMMAND_ERASE_OFFSET: + return "COMMAND_ERASE"; } + return "Invalid command"; +} - if( length >= ORESAT_F0_FIRMWARE_MAXIMUM_LENGTH) { - return false; +// Duplicated because different TX and RX types +static void dbgprint_frame_tx(const CANTxFrame * const msg) { + dbgprintf("TX Frame - SID = 0x%03X DLC = %u RTR = %u IDE = %u [ ", + msg->SID, msg->DLC, msg->RTR, msg->IDE); + for(int i = 0; i < msg->DLC; i++) { + dbgprintf("%02X ", msg->data8[i]); } - - return true; + dbgprintf("] "); } -/** - * @param *base_address Memory address at which the firmware metadata is located - * @param length Number of bytes to CRC - * @param expected_crc32 Expected CRC value of memory - * - * @return True if the CRC of flash matche, false otherwise - */ -bool validate_firmware_image(void *base_address, const uint32_t length, const uint32_t expected_crc32) { - if( ! validate_firmware_meta_data(length, expected_crc32) ) { - return false; +static void dbgprint_frame_rx(const CANRxFrame * const msg) { + dbgprintf("RX Frame - SID = 0x%03X DLC = %u RTR = %u IDE = %u [ ", + msg->SID, msg->DLC, msg->RTR, msg->IDE); + for(int i = 0; i < msg->DLC; i++) { + dbgprintf("%02X ", msg->data8[i]); } + dbgprintf("] "); +} - const uint32_t crc_of_flash_data = crc32(base_address, length, 0); +static msg_t can_transmit(const CANTxFrame * const msg) { + dbgprint_frame_tx(msg); + const msg_t r = canTransmitTimeout(CAN_DRIVER, CAN_ANY_MAILBOX, msg, CAN_TRANSMIT_TIMEOUT); + if(r == MSG_OK) { + // FIXME: if we send messages too fast the can driver seems to silently drop them instead + // of returning an error??? Having these print statements here seems slow things down + // enough that it works. This is particularly noticeable in command_read_memory because + // it does a lot of sending + chprintf(SERIAL,"- SUCCESS\r\n"); + //dbgprintf("- SUCCESS\r\n"); + } else { + chprintf(SERIAL, " - FAIL\r\n"); + //dbgprintf(" - FAIL\r\n"); + } + return r; +} - if( crc_of_flash_data != expected_crc32 ) { - return false; +__attribute__((warn_unused_result)) static msg_t can_receive(CANRxFrame * const msg, const sysinterval_t timeout) { + const msg_t r = canReceiveTimeout(CAN_DRIVER, CAN_ANY_MAILBOX, msg, timeout); + switch(r) { + case MSG_OK: + dbgprint_frame_rx(msg); + dbgprintf("\r\n"); + break; + case MSG_TIMEOUT: + dbgprintf("."); + break; + default: + dbgprintf("Error receiving CAN frame: %s\r\n", msg_t_to_str(r)); } + return r; +} - return true; +static msg_t send_ack(const CANRxFrame * const cmd) { + dbgprintf("Transmitting CAN bootloader ACK\r\n"); + return can_transmit(&(CANTxFrame){ + .SID = cmd->SID, + .DLC = 1, + .data8 = { BOOTLOADER_ACK }, + }); } -void branch_to_firmware_image(void) { - chSysDisable(); +static msg_t send_nack(const CANRxFrame * const cmd) { + dbgprintf("Transmitting CAN bootloader NACK\r\n"); + return can_transmit(&(CANTxFrame){ + .SID = cmd->SID, + .DLC = 1, + .data8 = { BOOTLOADER_NACK }, + }); +} - __set_MSP(*(uint32_t*) ORESAT_F0_FIRMWARE_CODE_ADDRESS); - (*(void (**)()) (ORESAT_F0_FIRMWARE_CODE_ADDRESS + 4))(); +static msg_t announce_presence_on_bus(const uint16_t node_id) { + dbgprintf("\r\nBootloader announcing presence on bus...\r\n"); + return can_transmit(&(CANTxFrame){ + .SID = COMMAND_ANNOUNCE(node_id), + .DLC = 8, + .data32 = { 0x04030201, UID_LOW }, + }); } -/** - * @return true if the firmware image is valid, false otherwise - */ -bool check_firmware_crc_and_branch(const bool print_serial_output) { - const uint32_t firmware_expected_crc = *((uint32_t *) ORESAT_F0_FIRMWARE_CRC_ADDRESS); - const uint32_t firmware_expected_length = *((uint32_t *) ORESAT_F0_FIRMWARE_LENGTH_ADDRESS); - const uint32_t firmware_version = *((uint32_t *) ORESAT_F0_FIRMWARE_VERSION_ADDRESS); - const bool firmware_metadata_valid = validate_firmware_meta_data(firmware_expected_length, firmware_expected_crc); - - const bool is_firmware_valid = validate_firmware_image((void*) ORESAT_F0_FIRMWARE_LENGTH_ADDRESS, firmware_expected_length, firmware_expected_crc); - - if( print_serial_output ) { - chprintf(DEBUG_SD, "firmware_expected_crc = 0x%X\r\n", firmware_expected_crc); - chprintf(DEBUG_SD, "firmware_expected_length = %u\r\n", firmware_expected_length); - if( firmware_metadata_valid ) { - chprintf(DEBUG_SD, "firmware_actual CRC = 0x%X\r\n", crc32((void*) ORESAT_F0_FIRMWARE_LENGTH_ADDRESS, firmware_expected_length, 0)); - } - chprintf(DEBUG_SD, "firmware_version = %u\r\n", firmware_version); - chprintf(DEBUG_SD, "firmware_metadata_valid = %u\r\n", firmware_metadata_valid); - chprintf(DEBUG_SD, "is_firmware_valid = %u\r\n", is_firmware_valid); - } +__attribute__((noreturn)) static void soft_reset_cortex_m0(void) { + // FIXME: flush/wait for completion of CAN/Serial instead of sleep + chThdSleepMilliseconds(100); + sdStop(&SD2); + chSysDisable(); - return(is_firmware_valid); + NVIC_SystemReset(); } +static void command_get(const CANRxFrame * const cmd, const uint16_t id) { + send_ack(cmd); + + // We implement only a subset of AN3154, omitting all ReadOutProtect + // handling, write [un]protect, speed, and ID. + const uint16_t cmd_list[] = { + BOOTLOADER_VERSION, + COMMAND_GET(id), + COMMAND_READ_MEMORY(id), + COMMAND_GO(id), + COMMAND_WRITE_MEMORY(id), + COMMAND_ERASE(id), + }; -msg_t can_bootloader_transmit(CANTxFrame *msg) { - const msg_t r = canTransmit(CAN_DRIVER, CAN_ANY_MAILBOX, msg, CAN_TRANSMIT_TIMEOUT); - if( r == MSG_OK ) { - can_api_print_tx_frame(DEBUG_SD, msg, "", " - SUCCESS"); - } else { - can_api_print_tx_frame(DEBUG_SD, msg, "", " - FAIL"); + can_transmit(&(CANTxFrame) { + .SID = cmd->SID, + .DLC = 1, + .data8 = { ARRAY_LEN(cmd_list) - 1 }, + }); + + for(size_t i = 0; i < ARRAY_LEN(cmd_list); i++) { + can_transmit(&(CANTxFrame){ + .SID = cmd->SID, + .DLC = 2, + // We're little endian but data gets sent big endian + .data16 = { __builtin_bswap16(cmd_list[i]) }, + }); } - chThdSleepMilliseconds(20); - return(r); + send_ack(cmd); } -msg_t can_bootloader_receive(CANRxFrame *msg) { - msg_t r = canReceive(CAN_DRIVER, CAN_ANY_MAILBOX, msg, CAN_RECEIVE_TIMEOUT); - if( r == MSG_OK ) { - can_api_print_rx_frame(DEBUG_SD, msg, "", ""); - } - return(r); +static bool valid_read_region(const uint8_t * const start, const uint16_t length) { + const uint8_t * const end = start + length - 1; + if(start >= __flash0_base__ && end < __flash2_end__) + return true; + if(start >= __ram0_base__ && end < __ram1_end__) + return true; + // TODO: AN3154 specifies that the system and option memory regions are + // valid for reading, but they've been omitted here because of time + // constraints. + return false; } -msg_t can_bootloader_receive2(CANRxFrame *msg, sysinterval_t timeout) { - const msg_t r = canReceive(CAN_DRIVER, CAN_ANY_MAILBOX, msg, timeout); - if( r == MSG_OK ) { -#if 1 - can_api_print_rx_frame(DEBUG_SD, msg, "", ""); -#else - chprintf(DEBUG_SD, "CAN RX: SID = 0x%X DLC = %u [", msg->SID, msg->DLC); - for(int i = 0; i < msg->DLC && i < 8; i++ ) { - chprintf(DEBUG_SD, " 0x%X", msg->data8[i]); - } - chprintf(DEBUG_SD, " ]\r\n"); -#endif +static void command_read_memory(const CANRxFrame * const cmd) { + if(cmd->DLC != 5) { + send_nack(cmd); + return; } - return(r); -} + // Address is sent in big endian, we're little endian + uint8_t *read_address = (uint8_t *)__builtin_bswap32(cmd->data32[0]); + uint16_t bytes_to_read = cmd->data8[4] + 1; + dbgprintf("Address to read from: 0x%X\r\n", read_address); + dbgprintf("Number of bytes to read: %u\r\n", bytes_to_read); -void can_bootloader_init_frame(CANTxFrame *msg, const uint32_t sid, const uint32_t dlc) { - memset(msg, 0, sizeof(*msg)); - msg->SID = sid; - msg->IDE = CAN_IDE_STD; - msg->DLC = dlc; -} - -/** - * Sends a ACK or NACK to the host to indicate success or failure of a given CAN command - * - * @sid The command ID for which this ack/nack is with respect. - * @ack_flag ture if an ACK is to be sent, false if a NACK is to be sent - */ -bool can_bootloader_send_ack_nack(const uint32_t sid, const bool ack_flag) { - msg_t tx_r; - CANTxFrame tx_msg; - can_bootloader_init_frame(&tx_msg, sid, 1); - if( ack_flag ) { - tx_msg.data8[0] = STM32_BOOTLOADER_CAN_ACK; - } else { - tx_msg.data8[0] = STM32_BOOTLOADER_CAN_NACK; + if(bytes_to_read > 256) { + send_nack(cmd); + return; } - - if( (tx_r = can_bootloader_transmit(&tx_msg)) != MSG_OK ) { - return false; + if(!valid_read_region(read_address, bytes_to_read)) { + send_nack(cmd); + return; } - return true; + // TODO: AN3154 says nack if ReadOutProtection is enabled and I think this + // is something in the option bytes? But we don't use it and I'm too lazy to + // check right now. + send_ack(cmd); + + while(bytes_to_read > 0) { + CANTxFrame reply = { + .SID = cmd->SID, + .DLC = MIN(bytes_to_read, 8), + }; + memcpy(reply.data8, read_address, reply.DLC); + bytes_to_read -= reply.DLC; + read_address += reply.DLC; + + dbgprintf("Transmitting %u bytes in response to read request.\r\n", reply.DLC); + if(can_transmit(&reply)) { + break; + } + } + + send_ack(cmd); } -bool can_bootloader_send_ack(const uint32_t sid) { - chprintf(DEBUG_SD, "Transmitting CAN bootloader ACK\r\n"); - return(can_bootloader_send_ack_nack(sid, true)); +static void command_go(const CANRxFrame * const cmd) { + // TODO: AN3154 says nack if ReadOutProtection is enabled and I think this + // is something in the option bytes? But we don't use it and I'm too lazy to + // check right now. + // + // We also omit the the address argument, only allowing jumps to the linker + // specified start of firmware. This has the nice property of allowing GO + // to be unable to fail. + send_ack(cmd); } -bool can_bootloader_send_nack(const uint32_t sid) { - chprintf(DEBUG_SD, "Transmitting CAN bootloader NACK\r\n"); - return(can_bootloader_send_ack_nack(sid, false)); + +static bool valid_write_range(const uint8_t * const start, const uint32_t length) { + // TODO: While the general spec allows for writes to SRAM, option, and the + // whole flash range, we limit this to application flash for safety. SRAM + // and option might be worthwhile in the future? + const uint8_t * const end = start + length - 1; + return start >= __flash0_end__ && end < __flash2_end__; } -/** - * @return true if the page_number can be erased, false otherwise - */ -bool is_flash_page_eraseable(const uint8_t page_number) { - uint32_t *bootloader_end_flash = (uint32_t *) &__flash0_end__; - uint32_t last_bootloader_page_number = ((((uint32_t)bootloader_end_flash) - 0x08000000) / STM32F093_FLASH_PAGE_SIZE) - 1; - //chprintf(DEBUG_SD, "last_bootloader_page_number = %u\r\n", last_bootloader_page_number); +static void command_write_memory(const CANRxFrame * const cmd) { + if(cmd->DLC != 5) { + send_nack(cmd); + return; + } + // Address is sent in big endian, we're little endian + const void * const write_address = (void *)__builtin_bswap32(cmd->data32[0]); + const uint16_t bytes_to_write = cmd->data8[4] + 1; + + if(!valid_write_range(write_address, bytes_to_write)) { + dbgprintf("Address 0x%X outside of valid write range...\r\n", write_address); + send_nack(cmd); + return; + } - if( page_number > last_bootloader_page_number && page_number <= FLASH_PAGE_COUNT) { - return true; + send_ack(cmd); + + // Based on the protocol spec, the host will send a maximum of 256 bytes + uint8_t buf[256]; + uint32_t received_bytes = 0; + const uint16_t id = node_id(); + while(received_bytes < bytes_to_write) { + CANRxFrame frame; + if(can_receive(&frame, CAN_RECEIVE_TIMEOUT)) { + //Failed to receive an expected frame of data to be written + send_nack(cmd); + return; + } + // AN3154 recommends 0x04 but says any SID is valid. We choose the + // command SID to reduce unique SIDs. + if(frame.SID != COMMAND_WRITE_MEMORY(id)) { + // Wrong SID, protocol misalignment? + send_nack(&frame); + return; + } + if(frame.DLC + received_bytes > bytes_to_write) { + dbgprintf("Incorrect number of bytes: have %u out of %u, given %u\r\n", + received_bytes, bytes_to_write, frame.DLC); + send_nack(&frame); + return; + } + memcpy(buf + received_bytes, frame.data8, frame.DLC); + received_bytes += frame.DLC; } - return false; -} + dbgprintf("bytes_to_write = %u\r\n", bytes_to_write); + dbgprintf("received_bytes = %u\r\n", received_bytes); -/** - * @return true if the given flash address can be written to, false otherwise. - */ -bool is_flash_write_address_valid(const uint8_t *address) { - uint8_t *application_start_flash = (uint8_t *) &__flash1_base__; - uint8_t *application_end_flash = (uint8_t *) &__flash1_end__; + dbgprintf("Writing data to flash...\r\n"); - if( address >= application_start_flash && address < application_end_flash ) { - return true; + // FIXME: uintptr_t is almost certainly not the right type + int fw_r = flashWriteF091((uintptr_t) write_address, buf, bytes_to_write); + + if(fw_r == FLASH_RETURN_SUCCESS) { + dbgprintf("Successfully wrote data to flash...\r\n"); + send_ack(cmd); + } else { + dbgprintf("Failed to write data to flash...\r\n"); + send_nack(cmd); } +} - return false; +static bool is_page_valid(const uint8_t page) { + const uint32_t metadata_page = ((uint32_t)METADATA - 0x08000000) + / STM32F093_FLASH_PAGE_SIZE; + + return page >= metadata_page && page <= FLASH_PAGE_COUNT; } -bool is_flash_write_address_range_valid(const uint8_t *address, const uint32_t length) { - if( ! is_flash_write_address_valid(address) ) { - return false; +static void command_erase(const CANRxFrame * const cmd) { + if(cmd->DLC < 1) { + send_nack(cmd); + return; } - if( ! is_flash_write_address_valid(address + length)) { - return false; - } - return true; -} -/** - * Handles an incoming CAN frame with it's coresponding commands and actions. - */ -void can_bootloader_handle_frame(CANRxFrame *rx_msg) { - msg_t tx_r; - const uint32_t command_sid = rx_msg->SID; - chprintf(DEBUG_SD, "Handling frame SID 0x%X %s\r\n", command_sid, oresat_bootloader_can_command_t_to_str(command_sid)); - - CANTxFrame reply_msg; - memset(&reply_msg, 0, sizeof(reply_msg)); - - - switch ((oresat_bootloader_can_command_t) command_sid) { - case ORESAT_BOOTLOADER_CAN_COMMAND_GET: - { - can_bootloader_send_ack(command_sid); - - const uint8_t cmd_list[] = {BOOTLOADER_VERSION, - ORESAT_BOOTLOADER_CAN_COMMAND_GET, - ORESAT_BOOTLOADER_CAN_COMMAND_READ_MEMORY, - ORESAT_BOOTLOADER_CAN_COMMAND_GO, - ORESAT_BOOTLOADER_CAN_COMMAND_WRITE_MEMORY, - ORESAT_BOOTLOADER_CAN_COMMAND_ERASE }; - - can_bootloader_init_frame(&reply_msg, command_sid, 1); - reply_msg.data8[0] = sizeof(cmd_list) - 1; //number of tx messages sent below - if( (tx_r = can_bootloader_transmit(&reply_msg)) != MSG_OK ) { - chprintf(DEBUG_SD, "Failed to transmit response - 1.\r\n"); - } + const uint16_t page_count = cmd->data8[0] + 1; + dbgprintf("erase page_count = %u\r\n", page_count); - for(uint32_t i = 0; i < sizeof(cmd_list); i++ ) { - can_bootloader_init_frame(&reply_msg, command_sid, 1); - reply_msg.data8[0] = cmd_list[i]; - if( (tx_r = can_bootloader_transmit(&reply_msg)) != MSG_OK ) { - chprintf(DEBUG_SD, "Failed to transmit response - 2.\r\n"); - } - } + if(page_count <= 7 && cmd->DLC == page_count + 1) { + send_ack(cmd); + } else { + // Mass erase not implemented. Additionally the documentation is + // contradictory on how many pages can be erased in a single run so + // here we only accept a single can frame (the strictest interpretation) + // The other alternative is to accept up to 255/8 = 31 additional can + // frames which contain more page numbers. + send_nack(cmd); + return; + } - can_bootloader_send_ack(command_sid); + for(size_t i = 1; i <= page_count && i < 8; i++) { + const uint8_t page = cmd->data8[i]; + if(!is_page_valid(page) ) { + dbgprintf("Page %u cannot be erased.\r\n", page); + send_nack(cmd); + return; } - break; - case ORESAT_BOOTLOADER_CAN_COMMAND_READ_MEMORY: - { - const uint8_t *address_to_read_from = (uint8_t *) ((rx_msg->data8[0] << 24) | (rx_msg->data8[1] << 16) | (rx_msg->data8[2] << 8) | rx_msg->data8[3]); - const uint8_t number_of_bytes_to_read = rx_msg->data8[4] + 1; - - chprintf(DEBUG_SD, "Address to read from: 0x%X\r\n", address_to_read_from); - chprintf(DEBUG_SD, "Number of bytes to read: %u\r\n", number_of_bytes_to_read); - - can_bootloader_send_ack(command_sid); - - for(int i = 0; i < number_of_bytes_to_read;) { - can_bootloader_init_frame(&reply_msg, command_sid, 0); - for(int j = 0; j < 8 && i < number_of_bytes_to_read; j++, i++) { - reply_msg.data8[j] = address_to_read_from[i]; - reply_msg.DLC++; - } - - if( (tx_r = can_bootloader_transmit(&reply_msg)) != MSG_OK ) { - chprintf(DEBUG_SD, "Failed to transmit response data.\r\n"); - } else { - chprintf(DEBUG_SD, "Transmitted %u bytes in response to read request.\r\n", reply_msg.DLC); - } - } - can_bootloader_send_ack(command_sid); + dbgprintf("Erasing page number = %u\r\n", page); + if(flashPageEraseF091(page) == FLASH_RETURN_SUCCESS ) { + dbgprintf("Successfully erased page\r\n"); + send_ack(cmd); + } else { + dbgprintf("Failed to erase page\r\n"); + send_nack(cmd); + return; } - break; - case ORESAT_BOOTLOADER_CAN_COMMAND_GO: - { - can_bootloader_send_ack(command_sid); + } + dbgprintf("Done with erase command\r\n"); +} - chprintf(DEBUG_SD, "Reseting cortex M0...\r\n"); - chThdSleepMilliseconds(100); +static void handle_can_comms(bool * firmware_valid, const uint16_t id) { + systime_t last_announce = chVTGetSystemTime(); + announce_presence_on_bus(id); - soft_reset_cortex_m0(); - } - break; - case ORESAT_BOOTLOADER_CAN_COMMAND_WRITE_MEMORY: - { - const uint8_t *write_address = (uint8_t *) ((rx_msg->data8[0] << 24) | (rx_msg->data8[1] << 16) | (rx_msg->data8[2] << 8) | rx_msg->data8[3]); - const uint8_t number_of_bytes_to_write = rx_msg->data8[4]; - msg_t r = 0; - - if( is_flash_write_address_range_valid(write_address, number_of_bytes_to_write) ) { - can_bootloader_send_ack(command_sid); - - uint32_t temp_buffer_index = 0; - while( temp_buffer_index < number_of_bytes_to_write ) { - CANRxFrame write_rx_frame; - if( (r = can_bootloader_receive(&write_rx_frame)) != MSG_OK ) { - //Failed to receive an expected frame of data to be written - break; - } - if( write_rx_frame.SID != 0x04 ) {//Recommended ID based on AN3154 - //Wrong SID, protocol misalignment? - break; - } - - for(int frame_idx = 0; frame_idx < write_rx_frame.DLC && frame_idx < 8; frame_idx++ ) { - if( temp_buffer_index < TEMP_WRITE_BUFFER_SIZE ) { - //Based on the protocol spec, the host will send a maximum of 2048 bytes for writing (1 page worth) - bootloader_temp_write_buffer[temp_buffer_index] = write_rx_frame.data8[frame_idx]; - temp_buffer_index++; - } else { - break; - } - } - - can_bootloader_send_ack(command_sid); - } - - chprintf(DEBUG_SD, "number_of_bytes_to_write = %u\r\n", number_of_bytes_to_write); - chprintf(DEBUG_SD, "temp_buffer_index = %u\r\n", temp_buffer_index); - - - if( number_of_bytes_to_write > 0 && temp_buffer_index == number_of_bytes_to_write ) { - chprintf(DEBUG_SD, "Writing data to flash...\r\n"); - - int fw_r = flashWriteF091((uintptr_t) write_address, bootloader_temp_write_buffer, number_of_bytes_to_write); - - if( fw_r == FLASH_RETURN_SUCCESS ) { - chprintf(DEBUG_SD, "Successfully wrote data to flash...\r\n"); - can_bootloader_send_ack(command_sid); - } else { - chprintf(DEBUG_SD, "Failed to write data to flash...\r\n"); - can_bootloader_send_nack(command_sid); - } - } else { - chprintf(DEBUG_SD, "Incorrect number of bytes to write...\r\n"); - can_bootloader_send_nack(command_sid); - } - } else { - chprintf(DEBUG_SD, "Address 0x%X outside of valid write range...\r\n", write_address); - can_bootloader_send_nack(command_sid); + const uint32_t timeout_ms = 100; + const systime_t timeout = TIME_MS2I(timeout_ms) + chVTGetSystemTime(); + + while(true) { + const systime_t now = chVTGetSystemTime(); + if(*firmware_valid) { + if(now >= timeout) { + dbgprintf("No CAN frame after %u ms, resetting and running main firmware...\r\n", + timeout_ms); + return; } - } - break; - case ORESAT_BOOTLOADER_CAN_COMMAND_ERASE: - { - //const uint32_t number_of_pages_to_erase = rx_msg->DLC; - const uint32_t number_of_pages_to_erase = rx_msg->data8[0] + 1; - chprintf(DEBUG_SD, "number_of_pages_to_erase = %u\r\n", number_of_pages_to_erase); - - can_bootloader_send_ack(command_sid); - - for(uint32_t i = 1; i <= number_of_pages_to_erase && i < 8; i++ ) { - int error_count = 0; - - const uint32_t page_number_to_erase = rx_msg->data8[i]; - if( is_flash_page_eraseable(page_number_to_erase) ) { - chprintf(DEBUG_SD, "Erasing page number = %u\r\n", page_number_to_erase); - chThdSleepMilliseconds(10); - - int re = flashPageEraseF091(page_number_to_erase); - - if( re == FLASH_RETURN_SUCCESS ) { - chprintf(DEBUG_SD, "Successfully erased page\r\n"); - } else { - chprintf(DEBUG_SD, "Failed to erase page\r\n"); - error_count++; - } - } else { - chprintf(DEBUG_SD, "Page %u cannot be erased.\r\n", page_number_to_erase); - chThdSleepMilliseconds(10); - error_count++; - } - - chprintf(DEBUG_SD, "Done with erase command\r\n"); - - if( error_count > 0 ) { - can_bootloader_send_nack(command_sid); - } else { - can_bootloader_send_ack(command_sid); - } + } else { + // Re-announce presence on the bus every 5 seconds if the firmware + // image is specifically invalid + if((now - last_announce) > TIME_MS2I(5000)) { + last_announce = now; + announce_presence_on_bus(id); } } + + CANRxFrame msg = {}; + if(can_receive(&msg, CAN_RECEIVE_TIMEOUT_LONG)) { + continue; + } + + dbgprintf("0: %08X 1: %08X\r\n", msg.data32[0], msg.data32[1]); + dbgprintf("0: %08X 1: %08X\r\n", UID_LOW, CAN_ANNOUNCE_MAGIC_NUMBER); + if(msg.SID == COMMAND_ANNOUNCE(id) + && msg.DLC == 8 + && msg.data32[0] == UID_LOW + && msg.data32[1] == CAN_ANNOUNCE_MAGIC_NUMBER + ) { + // Host will TX a reply frame with the magic number and + // CPU unique ID values swapped to indicaet this node + // should stay in bootloader mode + dbgprintf("RXed frame to stay in bootloader mode...\r\n"); break; + } } -} -void can_bootloader_announce_presence_on_bus(void) { - CANTxFrame txmsg; - memset(&txmsg, 0, sizeof(txmsg)); - txmsg.SID = 0; - txmsg.DLC = 8; - txmsg.data32[0] = 0x04030201; - txmsg.data32[1] = *cpu_unique_id_low; + while(true) { + CANRxFrame msg = {}; + if(can_receive(&msg, CAN_RECEIVE_TIMEOUT_LONG)) { + continue; + } - chprintf(DEBUG_SD, "\r\nBootloader announcing presence on bus...\r\n"); - can_bootloader_transmit(&txmsg); + const uint16_t command_offset = msg.SID - id; + chprintf(SERIAL, "Handling frame SID 0x%X %s\r\n", + msg.SID, command_offset_to_str(command_offset)); + switch(command_offset) { + case COMMAND_GET_OFFSET: + command_get(&msg, id); + break; + case COMMAND_READ_MEMORY_OFFSET: + command_read_memory(&msg); + break; + case COMMAND_GO_OFFSET: + command_go(&msg); + // because GO can't fail, we can just return + return; + case COMMAND_WRITE_MEMORY_OFFSET: + *firmware_valid = false; + command_write_memory(&msg); + break; + case COMMAND_ERASE_OFFSET: + *firmware_valid = false; + command_erase(&msg); + break; + } + } } +#define filter4(index, c1, c2, c3, c4) ((CANFilter) { \ + .filter = index, /* Bank index */ \ + .mode = 1, /* List mode */ \ + .scale = 0, /* 16 bits mode */ \ + .assignment = 0, /* Must be set to zero - see docs */ \ + .register1 = (c1) << 21 | (c2) << 5, \ + .register2 = (c3) << 21 | (c4) << 5, \ +}) + +#define filter2(index, c1, c2) ((CANFilter) { \ + .filter = index, /* Bank index */ \ + .mode = 1, /* List mode */ \ + .scale = 1, /* 32 bits mode */ \ + .assignment = 0, /* Must be set to zero - see docs */ \ + .register1 = (c1) << 21, \ + .register2 = (c2) << 21, \ +}) + +#define filter1(index, c1) ((CANFilter) { \ + .filter = index, /* Bank index */ \ + .mode = 0, /* Mask mode */ \ + .scale = 1, /* 32 bits mode */ \ + .assignment = 0, /* Must be set to zero - see docs */ \ + .register1 = (c1) << 21, \ + .register2 = 0x7FF << 21, \ +}) + +static void can_bootloader_run(bool * firmware_valid) { + const CANConfig cancfg = { + // Tool for calculating STM32 bit timing: http://www.bittiming.can-wiki.info/ + .btr = CAN_BTR(1000), + .mcr = CAN_MCR_ABOM //Automatic Bus-Off Management + | CAN_MCR_AWUM //Automatic Wakeup Mode + | CAN_MCR_TXFP //Transmit FIFO Priority + | 0, //Automatic Retransmission (no -> CAN_MCR_NART) + }; + dbgprintf("Initializing CAN peripheral with MCR = 0x%X\r\n", cancfg.mcr); + dbgprintf("Initializing CAN peripheral with BTR = 0x%X\r\n", cancfg.btr); -/*0b100 - Data with EID or (0b110 - RemoteFrame with EID)*/ -//#define set_can_eid_data(x) ((x << 3)|0b100) - -/*0b110 - Mask enable for EID/SID and DATA/RTR*/ -//#define set_can_eid_mask(x) ((x << 3)|0b110) - - -#define set_can_sid_data(x) ((x << 21) | 0b000) -#define set_can_sid_mask(x) ((x << 21) | 0b000) - - -void can_bootloader_run(const bool is_firmware_a_valid) { - CANConfig cancfg; - memset(&cancfg, 0, sizeof(cancfg)); - // Tool for calculating STM32 bit timing: http://www.bittiming.can-wiki.info/ - cancfg.btr = CAN_BTR(1000); - cancfg.mcr = ( - /* MCR (Master Control Register) */ - CAN_MCR_ABOM | //Automatic Bus-Off Management - CAN_MCR_AWUM | //Automatic Wakeup Mode - CAN_MCR_TXFP | //Transmit FIFO Priority - 0 /*CAN_MCR_NART*/ ); // No Automatic Retransmission - - - chprintf(DEBUG_SD, "Initializing CAN peripheral with MCR = 0x%X\r\n", cancfg.mcr); - chprintf(DEBUG_SD, "Initializing CAN peripheral with BTR = 0x%X\r\n", cancfg.btr); - - - /* - *The same manner we can set up to 14 filters (STM32F103) - *For example to use 2 EID Data filter to 2 CAN RX FIFO: - * http://forum.chibios.org/viewtopic.php?t=4079 - * see hal_can_lld.h for definition of CANFilter struct. - */ - -#define NUM_CAN_FILTERS 8 + const uint16_t id = node_id(); + chprintf(SERIAL, "Node ID: %u\r\n", id); - CANFilter can_filter_12[NUM_CAN_FILTERS] = {\ - /*{, , , , , } */ \ - {0, 0, 1, 0, set_can_sid_data(ORESAT_BOOTLOADER_CAN_COMMAND_GET), set_can_sid_mask(0x7FF)},\ - {1, 0, 1, 0, set_can_sid_data(ORESAT_BOOTLOADER_CAN_COMMAND_READ_MEMORY), set_can_sid_mask(0x7FF)},\ - {2, 0, 1, 0, set_can_sid_data(ORESAT_BOOTLOADER_CAN_COMMAND_GO), set_can_sid_mask(0x7FF)},\ - {3, 0, 1, 0, set_can_sid_data(ORESAT_BOOTLOADER_CAN_COMMAND_WRITE_MEMORY), set_can_sid_mask(0x7FF)},\ - {4, 0, 1, 0, set_can_sid_data(ORESAT_BOOTLOADER_CAN_COMMAND_ERASE), set_can_sid_mask(0x7FF)},\ - {5, 0, 1, 0, set_can_sid_data(CAN_BOOTLOADER_WRITE_MEMORY_RESPONSE_SID), set_can_sid_mask(0x7FF)},\ - {6, 0, 1, 0, set_can_sid_data(STM32_BOOTLOADER_CAN_ACK), set_can_sid_mask(0x7FF)},\ - {7, 0, 1, 0, set_can_sid_data(STM32_BOOTLOADER_CAN_NACK), set_can_sid_mask(0x7FF)},\ + const CANFilter filters[] = { + filter4(0, + COMMAND_ANNOUNCE(id), + COMMAND_GET(id), + COMMAND_READ_MEMORY(id), + COMMAND_GO(id) + ), + filter2(1, COMMAND_WRITE_MEMORY(id), COMMAND_ERASE(id)), }; + canSTM32SetFilters(CAN_DRIVER, 0, ARRAY_LEN(filters), filters); -#if 1 - //Set 4 filters from total 14(0xE) - canSTM32SetFilters(CAN_DRIVER, 0, NUM_CAN_FILTERS, &can_filter_12[0]); -#else - /* Put CAN module in normal mode */ - canSTM32SetFilters(CAN_DRIVER, 0, 0, NULL); -#endif canStart(CAN_DRIVER, &cancfg); - chprintf(DEBUG_SD, "Done starting CAN peripheral\r\n"); - - systime_t last_announcement_time_ms = TIME_I2MS(chVTGetSystemTime()); - can_bootloader_announce_presence_on_bus(); - - CANRxFrame rxmsg; - const systime_t start_time = TIME_I2MS(chVTGetSystemTime()); - bool got_stay_bootloader_frame = false; + dbgprintf("Done starting CAN peripheral\r\n"); + handle_can_comms(firmware_valid, id); - const uint32_t bootloader_timeout_threshold_ms = 3000; - while ( true ) { - if( is_firmware_a_valid ) { - if( (! got_stay_bootloader_frame) && ((TIME_I2MS(chVTGetSystemTime())) - start_time) >= bootloader_timeout_threshold_ms ) { - chprintf(DEBUG_SD, "No CAN frame after %u ms, reseting and running main firmware...\r\n", bootloader_timeout_threshold_ms); - break; - } - } else { - if( ! got_stay_bootloader_frame ) { - //Re-announce presence on the bus every 5 seconds if the firmware image is specifically invalid - const systime_t now_time_ms = TIME_I2MS(chVTGetSystemTime()); - - if( now_time_ms < last_announcement_time_ms || (now_time_ms - last_announcement_time_ms) > 5000 ) { - last_announcement_time_ms = now_time_ms; - can_bootloader_announce_presence_on_bus(); - } - } - } + canStop(CAN_DRIVER); +} - const msg_t r = can_bootloader_receive2(&rxmsg, CAN_RECEIVE_TIMEOUT_LONG); - if( r == MSG_OK ) { - if( ! got_stay_bootloader_frame ) { - if( rxmsg.SID == BOOTLOADER_EXPECTED_FIRST_FRAME_ID && rxmsg.DLC == 8 ) { - if( rxmsg.data32[0] == *cpu_unique_id_low && rxmsg.data32[1] == CAN_ANNOUNCE_MAGIC_NUMBER ) { - //Host will TX a reply frame with the magic number and CPU unique ID values swapped to indicaet this node should stay in bootloader mode - chprintf(DEBUG_SD, "RXed frame to stay in bootloader mode...\r\n"); - got_stay_bootloader_frame = true; - } - } - } else { - can_bootloader_handle_frame(&rxmsg); - } - } else { - if( r == MSG_TIMEOUT ) { - chprintf(DEBUG_SD, "."); - } else { - chprintf(DEBUG_SD, "Error receiving CAN frame: %s\r\n", msg_t_to_str(r)); - } - } +static bool is_metadata_valid(void) { + if(METADATA->length == 0 || METADATA->length >= FIRMWARE_MAX_LENGTH) { + return false; } - - canStop(CAN_DRIVER); + return METADATA->crc != 0 && METADATA->crc != UINT32_MAX; } -int main(void) -{ - if( (*bootloader_magic_number_pointer) == BOOTLOADER_VALIDATED_FIRMWARE_MAGIC_NUMBER ) { - //Previous power cycle validated the firmware, just run the firmware without bootstraping the OS. - //We specifically want to branch to the firmware image with the CPU as close to out of reset mode as possible. - *bootloader_magic_number_pointer = 0; + +// The word at the very end of SRAM is not allocated or cleared by CRT of the +// bootloader OS due to linker script specifying RAM as 31k - 4 instead of +// the full 32k +#define VALIDATED_FIRMWARE (*((volatile uint32_t *) __ram0_end__)) +#define BOOTLOADER_VALIDATED_FIRMWARE_MAGIC_NUMBER 0x12345678 + +int main(void) { + if(VALIDATED_FIRMWARE == BOOTLOADER_VALIDATED_FIRMWARE_MAGIC_NUMBER) { + // Previous power cycle validated the firmware, just run the firmware + // without bootstraping the OS. We specifically want to branch to the + // firmware image with the CPU as close to out of reset mode as possible. + VALIDATED_FIRMWARE = 0; __DSB(); - if( check_firmware_crc_and_branch(false) ) { - branch_to_firmware_image(); - } + // The application firmware will eventually remap its vector table to + // SRAM in __late_init(). + __set_MSP(VECTORS->main_stack_addr); + VECTORS->reset_handler(); } - //Note: none of this actually gets run because the __reset_handler_hook branchs to either the A or B firmware images + // Note: none of this gets run when VALIDATED_FIRMWARE is set to the magic + // number because branch_to_firmware_image() branches to the firmware image + // and does not return. halInit(); chSysInit(); + palSetLine(LINE_LED); sdStart(&SD2, NULL); - chprintf(DEBUG_SD, "\r\n=======================================\r\n"); - chprintf(DEBUG_SD, "Bootloader started...\r\n"); - - chprintf(DEBUG_SD, "Bootloader __flash0_base__ = 0x%08X\r\n", (uint8_t *) &__flash0_base__); - chprintf(DEBUG_SD, "Bootloader __flash0_end__ = 0x%08X\r\n", (uint8_t *) &__flash0_end__); + chprintf(SERIAL, "\r\n=======================================\r\n"); + chprintf(SERIAL, "Bootloader started...\r\n"); + +#ifdef DEBUG_PRINT + dbgprintf("Bootloader __flash0_base__ = 0x%08X\r\n", __flash0_base__); + dbgprintf("Bootloader __flash0_end__ = 0x%08X\r\n", __flash0_end__); + dbgprintf("Bootloader __flash0_size__ = 0x%08X\r\n", FLASH0_SIZE); + dbgprintf("Bootloader metadata = 0x%08X\r\n", METADATA); + dbgprintf("Firmware __flash2_base__ = 0x%08X\r\n", __flash2_base__); + dbgprintf("Firmware __flash2_end__ = 0x%08X\r\n", __flash2_end__); + dbgprintf("Firmware __flash2_size__ = 0x%08X\r\n", FLASH2_SIZE); + dbgprintf("SRAM __ram0_base__ = 0x%08X\r\n", __ram0_base__); + dbgprintf("SRAM __ram0_end = 0x%08X\r\n", __ram0_end__); + dbgprintf("SRAM __ram1_base__ = 0x%08X\r\n", __ram1_base__); + dbgprintf("SRAM __ram1_end = 0x%08X\r\n", __ram1_end__); + dbgprintf("FLASH->OBR = 0x%08X\r\n", FLASH->OBR); + dbgprintf("Option bytes0 = 0x%08X\r\n", *((uint32_t *)0x1ffff800)); + dbgprintf("Option bytes1 = 0x%08X\r\n", *((uint32_t *)0x1ffff804)); extern stkalign_t __main_thread_stack_base__; extern stkalign_t __main_thread_stack_end__; - chprintf(DEBUG_SD, "Stack size: %u\r\n", ((uint8_t *) &__main_thread_stack_end__) - ((uint8_t *) &__main_thread_stack_base__)); + dbgprintf("Stack size: %u\r\n", ((uint8_t *) &__main_thread_stack_end__) + - ((uint8_t *) &__main_thread_stack_base__)); - chThdSleepMilliseconds(50); + dbgprintf("Checking firmware validity...\r\n"); +#endif -#if 0 - for(int i = 0; i < 140; i++ ) { - chprintf(DEBUG_SD, "is_page_eraseable(%u) = %u\r\n", i, is_flash_page_eraseable(i)); + const bool metadata_valid = is_metadata_valid(); + uint32_t crc = -1; + if(metadata_valid) { + // FIXME: verify that this is using the HW crc implementation + crc = crc32(FIRMWARE_CRC_START, METADATA->length, 0); } -#endif + bool firmware_valid = metadata_valid && crc == METADATA->crc; - chprintf(DEBUG_SD, "Checking firmware validity...\r\n"); chThdSleepMilliseconds(50); + dbgprintf("firmware expected crc = 0x%X\r\n", METADATA->crc); + dbgprintf("firmware expected length = %u\r\n", METADATA->length); + dbgprintf("firmware version = %u\r\n", METADATA->version); + chprintf(SERIAL, "firmware metadata_valid = %u\r\n", metadata_valid); + chprintf(SERIAL, "firmware actual CRC = 0x%X\r\n", crc); + chprintf(SERIAL, "Current firmware_valid = %u\r\n", firmware_valid); - bool is_firmware_a_valid = check_firmware_crc_and_branch(true); - //is_firmware_a_valid = false; - chprintf(DEBUG_SD, "Current is_firmware_a_valid = %u\r\n", is_firmware_a_valid);chThdSleepMilliseconds(20); - can_bootloader_run(is_firmware_a_valid); + // bootloader commands could invalidate the firmware + can_bootloader_run(&firmware_valid); - if( is_firmware_a_valid ) { - *bootloader_magic_number_pointer = BOOTLOADER_VALIDATED_FIRMWARE_MAGIC_NUMBER; + if(firmware_valid) { + VALIDATED_FIRMWARE = BOOTLOADER_VALIDATED_FIRMWARE_MAGIC_NUMBER; } else { - *bootloader_magic_number_pointer = 0; + VALIDATED_FIRMWARE = 0; } - chprintf(DEBUG_SD, "set *bootloader_magic_number_pointer = 0x%X\r\n", *bootloader_magic_number_pointer); - chprintf(DEBUG_SD, "reseting MCU\r\n"); - chThdSleepMilliseconds(50); + dbgprintf("set VALIDATED_FIRMWARE = 0x%X\r\n", VALIDATED_FIRMWARE); + dbgprintf("reseting MCU\r\n"); soft_reset_cortex_m0(); return 0; } - diff --git a/src/f0/app_bootloader/requirements.txt b/src/f0/app_bootloader/requirements.txt new file mode 100644 index 00000000..318274bf --- /dev/null +++ b/src/f0/app_bootloader/requirements.txt @@ -0,0 +1 @@ +python-can[serial] diff --git a/src/f0/app_bootloader/test_bootloader.py b/src/f0/app_bootloader/test_bootloader.py new file mode 100755 index 00000000..841c4cb2 --- /dev/null +++ b/src/f0/app_bootloader/test_bootloader.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +'''Tests primarly the firmware bootloader implementation, testing of bootloader.py is accessory''' +import argparse +import sys +import time +import unittest + +import can + +from bootloader import (Announce, ErasePages, Get, GetResponse, Go, ReadMemory, + UnexpectedNack, WriteMemory) + +if __name__ == '__main__': + # NOTE: this co-opts -b, -c, and -h from unittest. Don't need them now but deconflict in + # the future? + parser = argparse.ArgumentParser() + parser.add_argument('-b', '--bustype', default='socketcan', help="default: %(default)s") + parser.add_argument('-c', '--channel', default='can0', help="default: %(default)s") + parser.add_argument('-r', '--bitrate', default=1000000, help="default: %(default)s", type=int) + parser.add_argument('-n', '--node-id', default=0x7C, help="default: %(default)s", type=int) + + args, remaining = parser.parse_known_args() + + bus = can.Bus(bustype=args.bustype, channel=args.channel, bitrate=args.bitrate) + reader = can.BufferedReader() + notifier = can.Notifier(bus, [reader]) + + +class TestCommand(unittest.TestCase): + def setUp(self): + self.do_cmds([Announce(args.node_id)]) + + def tearDown(self): + self.do_cmds([Go(args.node_id)]) + + def do_cmds(self, commands): + for command in commands: + for message in command.run(reader): + bus.send(message) + # FIXME: send() timeout option doesn't seem to work for socketcan. Get + # CanOperationError: Failed to transmit: No buffer space available [Error Code 105] + # if long writes are transmitted too quickly + time.sleep(0.01) + + +class TestGet(TestCommand): + def test_get(self): + get = Get(args.node_id) + self.do_cmds([get]) + self.assertEqual( + get.result, + GetResponse( + version=0xAC, + get=Get.offset + args.node_id, + read=ReadMemory.offset + args.node_id, + go=Go.offset + args.node_id, + write=WriteMemory.offset + args.node_id, + erase=ErasePages.offset + args.node_id, + ), + ) + + +class TestReadMemory(TestCommand): + def test_good_address_short(self): + length = 1 + read = ReadMemory(args.node_id, 0x08000000, length) + self.do_cmds([read]) + self.assertEqual(len(read.result), length) + + def test_bad_address_short(self): + length = 1 + read = ReadMemory(args.node_id, 0x88000000, length) + with self.assertRaises(UnexpectedNack): + self.do_cmds([read]) + + def test_good_address_long(self): + length = 256 + read = ReadMemory(args.node_id, 0x08000000, length) + self.do_cmds([read]) + self.assertEqual(len(read.result), length) + + def test_bad_address_long(self): + length = 256 + read = ReadMemory(args.node_id, 0x88000000, length) + with self.assertRaises(UnexpectedNack): + self.do_cmds([read]) + + def test_last_address(self): + # Address is at the top of flash memory space + length = 1 + read = ReadMemory(args.node_id, 0x0803FFFF, length) + self.do_cmds([read]) + self.assertEqual(len(read.result), length) + + def test_last_address_too_long(self): + # Address is at the top of flash memory space + length = 2 + read = ReadMemory(args.node_id, 0x0803FFFF, length) + with self.assertRaises(UnexpectedNack): + self.do_cmds([read]) + + +class TestErasePages(TestCommand): + def test_good_page_short(self): + erase = ErasePages(args.node_id, [20]) + self.do_cmds([erase]) + + def test_bad_page_short(self): + erase = ErasePages(args.node_id, [19]) + with self.assertRaises(UnexpectedNack): + self.do_cmds([erase]) + + def test_good_page_long(self): + erase = ErasePages(args.node_id, range(20, 27)) + self.do_cmds([erase]) + + def test_bad_page_long(self): + erase = ErasePages(args.node_id, range(19, 26)) + with self.assertRaises(UnexpectedNack): + self.do_cmds([erase]) + + def test_last_good_page(self): + erase = ErasePages(args.node_id, [127]) + self.do_cmds([erase]) + + def test_last_bad_page_short(self): + erase = ErasePages(args.node_id, [128]) + with self.assertRaises(UnexpectedNack): + self.do_cmds([erase]) + + def test_last_bad_page_long(self): + erase = ErasePages(args.node_id, range(125, 132)) + with self.assertRaises(UnexpectedNack): + self.do_cmds([erase]) + + def test_mix_n_match(self): + erase = ErasePages(args.node_id, [20, 127, 33, 94, 33, 33]) + self.do_cmds([erase]) + + def test_too_long_range(self): + erase = ErasePages(args.node_id, [20]) + erase.msg.data[0] = 8 + with self.assertRaises(UnexpectedNack): + self.do_cmds([erase]) + + +class TestWriteMemory(TestCommand): + def test_good_address_short(self): + address = 0x0800A000 + data = bytearray([2]) + erase = ErasePages(args.node_id, [20]) + write = WriteMemory(args.node_id, address, data) + read = ReadMemory(args.node_id, address, len(data)) + self.do_cmds([erase, write, read]) + self.assertEqual(data, read.result) + + def test_bad_address_short(self): + address = 0x08000000 + data = bytearray([2]) + erase = ErasePages(args.node_id, [20]) + write = WriteMemory(args.node_id, address, data) + self.do_cmds([erase]) + with self.assertRaises(UnexpectedNack): + self.do_cmds([write]) + + def test_good_address_long(self): + address = 0x0800A000 + data = bytearray([2] * 256) + erase = ErasePages(args.node_id, [20]) + write = WriteMemory(args.node_id, address, data) + read = ReadMemory(args.node_id, address, len(data)) + self.do_cmds([erase, write, read]) + self.assertEqual(data, read.result) + + def test_bad_address_long(self): + address = 0x08000000 + data = bytearray([2] * 256) + erase = ErasePages(args.node_id, [20]) + write = WriteMemory(args.node_id, address, data) + self.do_cmds([erase]) + with self.assertRaises(UnexpectedNack): + self.do_cmds([write]) + + +# TODO: class TestMetadata which tests valid and invalid firmware metadata + + +def tearDownModule(): + bus.shutdown() + + +if __name__ == '__main__': + unittest.main(argv=[sys.argv[0]] + remaining) diff --git a/src/f0/app_bootloader/test_stm32_bootloader.py b/src/f0/app_bootloader/test_stm32_bootloader.py deleted file mode 100644 index 41d0952d..00000000 --- a/src/f0/app_bootloader/test_stm32_bootloader.py +++ /dev/null @@ -1,271 +0,0 @@ -import can -import time,math -#This does testing of the CAN bootloader from python using a CANable attached to the host PC - -BOOTLOADER_EXPECTED_FIRST_FRAME_ID = 0x79 - -CAN_COMMAND_GET = 0x00 -CAN_COMMAND_READ_MEMORY = 0x11 -CAN_COMMAND_GO = 0x21 -CAN_COMMAND_WRITE_MEMORY = 0x31 -CAN_COMMAND_ERASE = 0x43 - - -def rx_ack_nack(bus, sid, timeout=0.25): - msg = bus.recv(timeout) - print("RX msg: " + str(msg)) - - if( msg is not None and msg.arbitration_id == sid and msg.dlc == 1 ): - if( msg.data[0] == 0x79 ): - print("Got ACK from device") - return(True) - elif( msg.data[0] == 0x1F ): - print("Got NACK from device") - return(False) - - raise Exception("Got unexpected CAN frame type: " + str(msg)) - - -def purge_rx_buff(bus): - countdown = 3 - while( countdown > 0 ): - if( bus.recv(0.05) ): - countdown = 3 - else: - countdown -= 1 - - return - - -def read_memory(bus, read_address, num_bytes_to_read): - print("Reading " + str(num_bytes_to_read) + " bytes of data at address 0x" + hex(read_address)) - msg = can.Message(arbitration_id=CAN_COMMAND_READ_MEMORY, - data=[(read_address >> 24) & 0xFF, (read_address >> 16) & 0xFF, (read_address >> 8) & 0xFF, (read_address >> 0) & 0xFF, num_bytes_to_read - 1], - is_extended_id=False) - bus.send(msg) - rx_ack_nack(bus, CAN_COMMAND_READ_MEMORY) - - expected_frame_count = int(math.ceil((num_bytes_to_read) / 8.0)) - print("expected_frame_count = " + str(expected_frame_count)) - - #total_bytes_read = 0 - for idx in range(0, expected_frame_count): - # read data - msg = bus.recv(0.25) - print("Data readback is " + str(msg)) - #total_bytes_read += msg.dlc - - rx_ack_nack(bus, CAN_COMMAND_READ_MEMORY) - - purge_rx_buff(bus) - - return - - -def erase_all_pages(bus): - print("Eraseing all pages") - msg = can.Message(arbitration_id=CAN_COMMAND_ERASE, data=[0xFF], is_extended_id=False) - print("Sending " + str(msg)) - bus.send(msg) - rx_ack_nack(bus, CAN_COMMAND_ERASE) - - return - - -def erase_page(bus, list_of_page_numbers): - print("Erasing page numbers " + str(list_of_page_numbers)) - purge_rx_buff(bus) - - num_pages_to_erase = len(list_of_page_numbers) - data = [num_pages_to_erase - 1] - data.extend(list_of_page_numbers) - msg = can.Message(arbitration_id=CAN_COMMAND_ERASE, data=data, is_extended_id=False) - print("Sending " + str(msg)) - - bus.send(msg) - rx_ack_nack(bus, CAN_COMMAND_ERASE) - - for i in range(0, len(list_of_page_numbers)): - rx_ack_nack(bus, CAN_COMMAND_ERASE, timeout=5.0) - - purge_rx_buff(bus) - - return - - -def write_memory(bus, base_address, byte_list): - print("Writing " + str(len(byte_list)) + " bytes at address 0x" + hex(base_address)) - - msg = can.Message(arbitration_id=CAN_COMMAND_WRITE_MEMORY, - data=[(base_address >> 24) & 0xFF, (base_address >> 16) & 0xFF, (base_address >> 8) & 0xFF, (base_address >> 0) & 0xFF, len(byte_list)], - is_extended_id=False) - bus.send(msg) - rx_ack_nack(bus, msg.arbitration_id) - - idx = 0 - while(True): - temp_data = [] - while( idx < len(byte_list) and len(temp_data) < 8 ): - temp_data.append(byte_list[idx]) - idx += 1 - - if( len(temp_data) > 0 ): - msg = can.Message(arbitration_id=0x04, data=temp_data, is_extended_id=False) - print("Sending: " + str(msg)) - bus.send(msg) - rx_ack_nack(bus, CAN_COMMAND_WRITE_MEMORY) - else: - break - - rx_ack_nack(bus, CAN_COMMAND_WRITE_MEMORY) - - return - - -def firmware_update(bus, address, firmware_image): - page_list = range(20, 126) - - while( len(page_list) > 0 ): - page_list_2 = [] - while( len(page_list_2) < 7 and len(page_list) > 0 ): - page_list_2.append(page_list.pop(0)) - if( len(page_list) > 0 ): - erase_page(bus, page_list_2) - - # for i in range(20, 126): - # erase_page(bus, [i]) - - buff = [] - with open(firmware_image, 'rb') as f: - byte = f.read(1) - while byte != b"": - buff.append(byte) - byte = f.read(1) - - - while(len(buff) > 0): - data = [] - for i in range(0, 64): - if( len(buff) == 0 ): - break - - data.append(int.from_bytes(buff.pop(0), 'big')) - - write_memory(bus, address, data) - address += len(data) - - can_go(bus) - - return - - -def can_go(bus): - msg = can.Message(arbitration_id=CAN_COMMAND_GO, data=[], is_extended_id=False) - bus.send(msg) - - rx_ack_nack(bus, CAN_COMMAND_GO) - - return - - -def can_get(bus): - msg = can.Message(arbitration_id=CAN_COMMAND_GET, data=[], is_extended_id=False) - - bus.send(msg) - rx_ack_nack(bus, msg.arbitration_id) - - print("Reading number of bytes") - msg_number_of_bytes = bus.recv(0.25) - print(str(msg_number_of_bytes)) - - number_of_frames = msg_number_of_bytes.data[0] + 1 - print("number_of_frames = " + str(number_of_frames)) - - for i in range(0, number_of_frames): - msg_command_type = bus.recv(0.25) - print(str(msg_command_type)) - - rx_ack_nack(bus, msg.arbitration_id) - - print("Done with can_get()") - - purge_rx_buff(bus) - - return - - -def test_can_bootloader_client(bus, is_407_cpu=True): - #purge_rx_buff(bus) - can_get(bus) - - # read_memory(bus, 0x08000000, 31) - # read_memory(bus, 0x08000000, 32) - # read_memory(bus, 0x08000000, 33) - - # read_memory(bus, 0x0800A000, 32) - # write_memory(bus, 0x0800A000, [0x0A, 0x0B, 0x0C, 0x0D, 0x01, 0x02, 0x03, 0x04, 0x05]) - # read_memory(bus, 0x0800A000, 32) - - # erase_page_list = [20, 21, 22, 23] - # if( is_407_cpu ): - # erase_page_list = [0, 1, 2, 3] - # erase_page_list = [3] - - # erase_page(bus, erase_page_list) - # read_memory(bus, 0x0800A000, 32) - - # write_memory(bus, 0x0800A000, [0x0A, 0x0B, 0x0C, 0x0D, 0x01, 0x02, 0x03, 0x04, 0x05]) - # read_memory(bus, 0x0800A000, 32) - - if( not is_407_cpu ): - firmware_update(bus, 0x0800A000, '../app_protocard2/build/app_protocard2.crc32.bin') - - return - - - - -if( False ): - rx_msg = None - bitrate = 1000000 - - bus = can.interface.Bus(bustype='slcan', channel='/dev/ttyACM1', bitrate=bitrate) - while( rx_msg is None ): - rx_msg = bus.recv(1) - if( rx_msg is None ): - print("No RX message yet") - - if( rx_msg.arbitration_id == CAN_COMMAND_GET and rx_msg.dlc == 8 ): - if(rx_msg.data[0] == 0x01 and rx_msg.data[1] == 0x02 and rx_msg.data[2] == 0x03 and rx_msg.data[3] == 0x04 ): - msg = can.Message(arbitration_id=BOOTLOADER_EXPECTED_FIRST_FRAME_ID, - data=[rx_msg.data[4], rx_msg.data[5], rx_msg.data[6], rx_msg.data[7], 0x01, 0x02, 0x03, 0x04], - is_extended_id=False) - - print("Sending " + str(msg)) - bus.send(msg) - #rx_ack_nack(bus, msg.arbitration_id) - - test_can_bootloader_client(bus, False) -else: - bitrate = 125000 - bus = can.interface.Bus(bustype='slcan', channel='/dev/ttyACM1', bitrate=bitrate) - - for i in range(0, 99999): - msg = can.Message(arbitration_id=BOOTLOADER_EXPECTED_FIRST_FRAME_ID, data=[0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA], is_extended_id=False) - - bus.send(msg) - print(str(time.time()) + " Sent Initialization frame: " + str(msg)) - - rx_msg = bus.recv(0.05) - if( rx_msg is not None ): - print("RX: " + str(rx_msg)) - test_can_bootloader_client(bus, True) - break - - time.sleep(0.2) - - - - - - diff --git a/toolchain/f0_pack_image.py b/toolchain/f0_pack_image.py index 604558ee..6420a78b 100644 --- a/toolchain/f0_pack_image.py +++ b/toolchain/f0_pack_image.py @@ -1,33 +1,32 @@ -import sys,os -import struct +'''Prepends metadata (crc, length, version) to F0 firmware images''' import binascii +import os +import struct +import sys input_fw_bin_file = sys.argv[1] output_fw_bin_file = sys.argv[2] -version = sys.argv[3] # 20200101 - -total_length = 1020 + os.path.getsize(input_fw_bin_file) - - -f = open(output_fw_bin_file, 'wb') - -f.write(struct.pack("I", total_length)) -f.write(struct.pack("I", int(version))) -for i in range(0, int((1024 - 4 - 4 - 4) / 4)): - f.write(struct.pack("I", 0xFFFFFFFF)) - -f.write(open(input_fw_bin_file, 'rb').read()) -f.close() - - -concated_data = open(output_fw_bin_file, 'rb').read() -crc32 = binascii.crc32(concated_data) - -f = open(output_fw_bin_file, 'wb') -f.write(struct.pack("I", crc32)) -f.write(concated_data) -f.close() - -print("Final Length: " + str(total_length)) -print("Final CRC32: 0x" + str(hex(crc32))) - +version = sys.argv[3] # e.g. 20200101 + +# See app_bootloader struct FirmwareMetadata for definition. Metadata consists +# of 3 uint32_t at the end of the metadata block (which is just before the +# firmware block, see ld/STM32F091xC-bootloader.ld) +LENGTH_LEN = 4 +VERSION_LEN = 4 + +# The length that gets CRC'd. Doesn't include the CRC space itself, but +# does include everything else +total_length = LENGTH_LEN + VERSION_LEN + os.path.getsize(input_fw_bin_file) + +with open(input_fw_bin_file, 'rb') as f: + metadata = struct.pack("II", total_length, int(version)) + firmware = f.read() + crc32 = binascii.crc32(metadata + firmware) + +with open(output_fw_bin_file, 'wb') as f: + f.write(struct.pack("I", crc32)) + f.write(metadata) + f.write(firmware) + +print(f"Final Length: {total_length}") +print(f"Final CRC32: 0x{hex(crc32)}")