Skip to content

Commit

Permalink
Finished flashloader and bootloader implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
robamu committed Sep 30, 2024
1 parent e2a55e7 commit d6f69d4
Show file tree
Hide file tree
Showing 41 changed files with 2,847 additions and 980 deletions.
8 changes: 5 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,21 @@ members = [
"examples/embassy",
"board-tests",
"bootloader",
"flashloader",
]

exclude = [
"defmt-testapp",
"flashloader/slot-a-blinky",
"flashloader/slot-b-blinky",
]

[profile.dev]
codegen-units = 1
debug = 2
debug-assertions = true # <-
incremental = false
# This is problematic for stepping..
# opt-level = 'z' # <-
# 1 instead of 0, the flashloader is too larger otherwise..
# opt-level = 1 # <-
overflow-checks = true # <-

# cargo build/run --release
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ This workspace contains the following released crates:

It also contains the following helper crates:

- The [`bootloader`](https://egit.irs.uni-stuttgart.de/rust/va108xx-rs/src/branch/main/bootloader)
crate contains a sample bootloader strongly based on the one provided by Vorago.
- The [`flashloader`](https://egit.irs.uni-stuttgart.de/rust/va108xx-rs/src/branch/main/flashloader)
crate contains a sample flashloader which is able to update the redundant images in the NVM which
is compatible to the provided bootloader as well.
- The [`board-tests`](https://egit.irs.uni-stuttgart.de/rust/va108xx-rs/src/branch/main/board-tests)
contains an application which can be used to test the libraries on the board.
- The [`examples`](https://egit.irs.uni-stuttgart.de/rust/va108xx-rs/src/branch/main/examples)
Expand Down
4 changes: 2 additions & 2 deletions board-tests/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use va108xx_hal::{
pac::{self, interrupt},
prelude::*,
time::Hertz,
timer::{default_ms_irq_handler, set_up_ms_tick, CountDownTimer, IrqCfg},
timer::{default_ms_irq_handler, set_up_ms_tick, CountdownTimer, IrqCfg},
};

#[allow(dead_code)]
Expand Down Expand Up @@ -168,7 +168,7 @@ fn main() -> ! {
ms_timer.delay_ms(500);
}

let mut delay_timer = CountDownTimer::new(&mut dp.sysconfig, 50.MHz(), dp.tim1);
let mut delay_timer = CountdownTimer::new(&mut dp.sysconfig, 50.MHz(), dp.tim1);
let mut pa0 = pinsa.pa0.into_readable_push_pull_output();
for _ in 0..5 {
led1.toggle().ok();
Expand Down
3 changes: 1 addition & 2 deletions bootloader/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ edition = "2021"
cortex-m = "0.7"
cortex-m-rt = "0.7"
embedded-hal = "1"
embedded-hal-bus = "0.2"
dummy-pin = "1"
panic-rtt-target = { version = "0.1.3" }
panic-halt = { version = "0.2" }
rtt-target = { version = "0.5" }
crc = "3"
static_assertions = "1"

[dependencies.va108xx-hal]
path = "../va108xx-hal"
Expand Down
48 changes: 48 additions & 0 deletions bootloader/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
VA108xx Bootloader Application
=======

This is the Rust version of the bootloader supplied by Vorago.

## Memory Map

The bootloader uses the following memory map:

| Address | Notes | Size |
| ------ | ---- | ---- |
| 0x0 | Bootloader start | code up to 0x3FFC bytes |
| 0x2FFE | Bootloader CRC | word |
| 0x3000 | App image A start | code up to 0xE7F8 (~58K) bytes |
| 0x117F8 | App image A CRC check length | word |
| 0x117FC | App image A CRC check value | word |
| 0x11800 | App image B start | code up to 0xE7F8 (~58K) bytes |
| 0x1FFF8 | App image B CRC check length | word |
| 0x1FFFC | App image B CRC check value | word |
| 0x20000 | End of NVM | end |

## Additional Information

This bootloader was specifically written for the REB1 board, so it assumes a M95M01 ST EEPROM
is used to load the application code. The bootloader will also delay for a configurable amount
of time before booting. This allows to catch the RTT printout, but should probably be disabled
for production firmware.

This bootloader does not provide tools to flash the NVM memory by itself. Instead, you can use
the [flashloader](https://egit.irs.uni-stuttgart.de/rust/va108xx-rs/src/branch/main/flashloader)
application to perform this task using a CCSDS interface via a UART.

The bootloader performs the following steps:

1. The application will calculate the checksum of itself if the bootloader CRC is blank (all zeroes
or all ones). If the CRC is not blank and the checksum check fails, it will immediately boot
application image A. Otherwise, it proceeds to the next step.
2. Check the checksum of App A. If that checksum is valid, it will boot App A. If not, it will
proceed to the next step.
3. Check the checksum of App B. If that checksum is valid, it will boot App B. If not, it will
boot App A as the fallback image.

You could adapt and combine this bootloader with a non-volatile memory to select a prefered app
image, which would be a first step towards an updatable flight software.

Please note that you *MUST* compile the application at slot A and slot B with an appropriate
`memory.x` file where the base address of the `FLASH` was adapted according to the base address
shown in the memory map above. The memory files to do this were provided in the `scripts` folder.
6 changes: 3 additions & 3 deletions bootloader/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use core::convert::Infallible;

/// Simple trait which makes swapping the NVM easier. NVMs only need to implement this interface.
pub trait NvmInterface {
fn write(&mut self, address: u32, data: &[u8]) -> Result<(), Infallible>;
fn read(&mut self, address: u32, buf: &mut [u8]) -> Result<(), Infallible>;
fn verify(&mut self, address: u32, data: &[u8]) -> Result<bool, Infallible>;
fn write(&mut self, address: usize, data: &[u8]) -> Result<(), Infallible>;
fn read(&mut self, address: usize, buf: &mut [u8]) -> Result<(), Infallible>;
fn verify(&mut self, address: usize, data: &[u8]) -> Result<bool, Infallible>;
}
94 changes: 62 additions & 32 deletions bootloader/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@
use bootloader::NvmInterface;
use cortex_m_rt::entry;
use crc::{Crc, CRC_16_IBM_3740};
use embedded_hal::delay::DelayNs;
#[cfg(not(feature = "rtt-panic"))]
use panic_halt as _;
#[cfg(feature = "rtt-panic")]
use panic_rtt_target as _;
use rtt_target::{rprintln, rtt_init_print};
use va108xx_hal::{pac, time::Hertz};
use va108xx_hal::{pac, time::Hertz, timer::CountdownTimer};
use vorago_reb1::m95m01::M95M01;

// Useful for debugging and see what the bootloader is doing. Enabled currently, because
// the binary stays small enough.
const RTT_PRINTOUT: bool = true;
const DEBUG_PRINTOUTS: bool = false;
const DEBUG_PRINTOUTS: bool = true;
// Small delay, allows RTT printout to catch up.
const BOOT_DELAY_MS: u32 = 2000;

// Dangerous option! An image with this option set to true will flash itself from RAM directly
// into the NVM. This can be used as a recovery option from a direct RAM flash to fix the NVM
Expand All @@ -35,23 +38,32 @@ const CLOCK_FREQ: Hertz = Hertz::from_raw(50_000_000);

// Important bootloader addresses and offsets, vector table information.

const NVM_SIZE: u32 = 0x20000;
const BOOTLOADER_START_ADDR: u32 = 0x0;
const BOOTLOADER_CRC_ADDR: u32 = BOOTLOADER_END_ADDR - 2;
// This is also the maximum size of the bootloader.
const BOOTLOADER_END_ADDR: u32 = 0x3000;
const APP_A_START_ADDR: u32 = 0x3000;
const APP_A_START_ADDR: u32 = BOOTLOADER_END_ADDR;
// 0x117F8
const APP_A_SIZE_ADDR: u32 = APP_A_END_ADDR - 8;
// Four bytes reserved, even when only 2 byte CRC is used. Leaves flexibility to switch to CRC32.
// 0x117FC
const APP_A_CRC_ADDR: u32 = APP_A_END_ADDR - 4;
pub const APP_A_END_ADDR: u32 = 0x11000;
// 0x11800
pub const APP_A_END_ADDR: u32 = APP_A_START_ADDR + APP_IMG_SZ;
// The actual size of the image which is relevant for CRC calculation.
const APP_B_START_ADDR: u32 = 0x11000;
const APP_B_START_ADDR: u32 = APP_A_END_ADDR;
// The actual size of the image which is relevant for CRC calculation.
// 0x1FFF8
const APP_B_SIZE_ADDR: u32 = APP_B_END_ADDR - 8;
// Four bytes reserved, even when only 2 byte CRC is used. Leaves flexibility to switch to CRC32.
// 0x1FFFC
const APP_B_CRC_ADDR: u32 = APP_B_END_ADDR - 4;
pub const APP_B_END_ADDR: u32 = 0x20000;
pub const APP_IMG_SZ: u32 = 0xE800;
// 0x20000
pub const APP_B_END_ADDR: u32 = NVM_SIZE;
pub const APP_IMG_SZ: u32 = (APP_B_END_ADDR - APP_A_START_ADDR) / 2;

static_assertions::const_assert!((APP_B_END_ADDR - BOOTLOADER_END_ADDR) % 2 == 0);

pub const VECTOR_TABLE_OFFSET: u32 = 0x0;
pub const VECTOR_TABLE_LEN: u32 = 0xC0;
Expand All @@ -69,15 +81,15 @@ pub struct NvmWrapper(pub M95M01);

// Newtype pattern. We could now more easily swap the used NVM type.
impl NvmInterface for NvmWrapper {
fn write(&mut self, address: u32, data: &[u8]) -> Result<(), core::convert::Infallible> {
fn write(&mut self, address: usize, data: &[u8]) -> Result<(), core::convert::Infallible> {
self.0.write(address, data)
}

fn read(&mut self, address: u32, buf: &mut [u8]) -> Result<(), core::convert::Infallible> {
fn read(&mut self, address: usize, buf: &mut [u8]) -> Result<(), core::convert::Infallible> {
self.0.read(address, buf)
}

fn verify(&mut self, address: u32, data: &[u8]) -> Result<bool, core::convert::Infallible> {
fn verify(&mut self, address: usize, data: &[u8]) -> Result<bool, core::convert::Infallible> {
self.0.verify(address, data)
}
}
Expand All @@ -90,6 +102,7 @@ fn main() -> ! {
}
let mut dp = pac::Peripherals::take().unwrap();
let cp = cortex_m::Peripherals::take().unwrap();
let mut timer = CountdownTimer::new(&mut dp.sysconfig, CLOCK_FREQ, dp.tim0);

let mut nvm = M95M01::new(&mut dp.sysconfig, CLOCK_FREQ, dp.spic);

Expand Down Expand Up @@ -124,9 +137,9 @@ fn main() -> ! {
}
}

nvm.write(BOOTLOADER_CRC_ADDR, &bootloader_crc.to_be_bytes())
nvm.write(BOOTLOADER_CRC_ADDR as usize, &bootloader_crc.to_be_bytes())
.expect("writing CRC failed");
if let Err(e) = nvm.verify(BOOTLOADER_CRC_ADDR, &bootloader_crc.to_be_bytes()) {
if let Err(e) = nvm.verify(BOOTLOADER_CRC_ADDR as usize, &bootloader_crc.to_be_bytes()) {
if RTT_PRINTOUT {
rprintln!(
"error: CRC verification for bootloader self-flash failed: {:?}",
Expand All @@ -139,23 +152,28 @@ fn main() -> ! {
let mut nvm = NvmWrapper(nvm);

// Check bootloader's CRC (and write it if blank)
check_own_crc(&dp.sysconfig, &cp, &mut nvm);
check_own_crc(&dp.sysconfig, &cp, &mut nvm, &mut timer);

if check_app_crc(AppSel::A) {
boot_app(&dp.sysconfig, &cp, AppSel::A)
boot_app(&dp.sysconfig, &cp, AppSel::A, &mut timer)
} else if check_app_crc(AppSel::B) {
boot_app(&dp.sysconfig, &cp, AppSel::B)
boot_app(&dp.sysconfig, &cp, AppSel::B, &mut timer)
} else {
if DEBUG_PRINTOUTS && RTT_PRINTOUT {
rprintln!("both images corrupt! booting image A");
}
// TODO: Shift a CCSDS packet out to inform host/OBC about image corruption.
// Both images seem to be corrupt. Boot default image A.
boot_app(&dp.sysconfig, &cp, AppSel::A)
boot_app(&dp.sysconfig, &cp, AppSel::A, &mut timer)
}
}

fn check_own_crc(sysconfig: &pac::Sysconfig, cp: &cortex_m::Peripherals, nvm: &mut NvmWrapper) {
fn check_own_crc(
sysconfig: &pac::Sysconfig,
cp: &cortex_m::Peripherals,
nvm: &mut NvmWrapper,
timer: &mut CountdownTimer<pac::Tim0>,
) {
let crc_exp = unsafe { (BOOTLOADER_CRC_ADDR as *const u16).read_unaligned().to_be() };
// I'd prefer to use [core::slice::from_raw_parts], but that is problematic
// because the address of the bootloader is 0x0, so the NULL check fails and the functions
Expand All @@ -176,7 +194,7 @@ fn check_own_crc(sysconfig: &pac::Sysconfig, cp: &cortex_m::Peripherals, nvm: &m
rprintln!("BL CRC blank - prog new CRC");
}
// Blank CRC, write it to NVM.
nvm.write(BOOTLOADER_CRC_ADDR, &crc_calc.to_be_bytes())
nvm.write(BOOTLOADER_CRC_ADDR as usize, &crc_calc.to_be_bytes())
.expect("writing CRC failed");
// The Vorago bootloader resets here. I am not sure why this is done but I think it is
// necessary because somehow the boot will not work if we just continue as usual.
Expand All @@ -191,7 +209,7 @@ fn check_own_crc(sysconfig: &pac::Sysconfig, cp: &cortex_m::Peripherals, nvm: &m
);
}
// TODO: Shift out minimal CCSDS frame to notify about bootloader corruption.
boot_app(sysconfig, cp, AppSel::A);
boot_app(sysconfig, cp, AppSel::A, timer);
}
}

Expand Down Expand Up @@ -240,43 +258,52 @@ fn check_app_given_addr(crc_addr: u32, start_addr: u32, image_size_addr: u32) ->

// The boot works by copying the interrupt vector table (IVT) of the respective app to the
// base address in code RAM (0x0) and then performing a soft reset.
fn boot_app(syscfg: &pac::Sysconfig, cp: &cortex_m::Peripherals, app_sel: AppSel) -> ! {
fn boot_app(
syscfg: &pac::Sysconfig,
cp: &cortex_m::Peripherals,
app_sel: AppSel,
timer: &mut CountdownTimer<pac::Tim0>,
) -> ! {
if DEBUG_PRINTOUTS && RTT_PRINTOUT {
rprintln!("booting app {:?}", app_sel);
}
timer.delay_ms(BOOT_DELAY_MS);

// Clear all interrupts set.
unsafe {
cp.NVIC.icer[0].write(0xFFFFFFFF);
cp.NVIC.icpr[0].write(0xFFFFFFFF);
}
// Disable ROM protection.
syscfg.rom_prot().write(|w| unsafe { w.bits(1) });
syscfg.rom_prot().write(|w| w.wren().set_bit());
let base_addr = if app_sel == AppSel::A {
APP_A_START_ADDR
} else {
APP_B_START_ADDR
};
// Clear all interrupts set.
unsafe {
cp.NVIC.icer[0].write(0xFFFFFFFF);
cp.NVIC.icpr[0].write(0xFFFFFFFF);

// First 4 bytes done with inline assembly, writing to the physical address 0x0 can not
// be done without it. See https://users.rust-lang.org/t/reading-from-physical-address-0x0/117408/2.
core::ptr::read(base_addr as *const u32);
let first_four_bytes = core::ptr::read(base_addr as *const u32);
core::arch::asm!(
"str {0}, [{1}]", // Load 4 bytes from src into r0 register
in(reg) base_addr, // Input: App vector table.
"str {0}, [{1}]",
in(reg) first_four_bytes, // Input: App vector table.
in(reg) BOOTLOADER_START_ADDR as *mut u32, // Input: destination pointer
);
core::slice::from_raw_parts_mut(
(BOOTLOADER_START_ADDR + 4) as *mut u32,
(BOOTLOADER_START_ADDR + 4) as *mut u8,
(VECTOR_TABLE_LEN - 4) as usize,
)
.copy_from_slice(core::slice::from_raw_parts(
(base_addr + 4) as *const u32,
(base_addr + 4) as *const u8,
(VECTOR_TABLE_LEN - 4) as usize,
));
}
/* Disable re-loading from FRAM/code ROM on soft reset */
// Disable re-loading from FRAM/code ROM on soft reset
syscfg
.rst_cntl_rom()
.modify(|_, w| w.sysrstreq().clear_bit());

soft_reset(cp);
}

Expand All @@ -292,5 +319,8 @@ fn soft_reset(cp: &cortex_m::Peripherals) -> ! {
// Ensure completion of memory access.
cortex_m::asm::dsb();

unreachable!();
// Loop until the reset occurs.
loop {
cortex_m::asm::nop();
}
}
25 changes: 18 additions & 7 deletions examples/rtic/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,11 @@ cortex-m-rt = "0.7"
embedded-hal = "1"
embedded-io = "0.6"
rtt-target = { version = "0.5" }
panic-rtt-target = { version = "0.1" }

# Even though we do not use this directly, we need to activate this feature explicitely
# so that RTIC compiles because thumv6 does not have CAS operations natively.
portable-atomic = { version = "1", features = ["unsafe-assume-single-core"]}
panic-rtt-target = { version = "0.1" }

[dependencies.va108xx-hal]
path = "../../va108xx-hal"

[dependencies.vorago-reb1]
path = "../../vorago-reb1"

[dependencies.rtic]
version = "2"
Expand All @@ -31,3 +26,19 @@ features = ["cortex-m-systick"]
[dependencies.rtic-sync]
version = "1.3"
features = ["defmt-03"]

[dependencies.once_cell]
version = "1"
default-features = false
features = ["critical-section"]

[dependencies.ringbuf]
version = "0.4.7"
default-features = false
features = ["portable-atomic"]

[dependencies.va108xx-hal]
path = "../../va108xx-hal"

[dependencies.vorago-reb1]
path = "../../vorago-reb1"
Loading

0 comments on commit d6f69d4

Please sign in to comment.