Flashloader and Bootloader #16

Merged
muellerr merged 1 commits from flashloader into main 2024-09-30 11:43:59 +02:00
41 changed files with 2783 additions and 916 deletions
Showing only changes of commit d6f69d4a54 - Show all commits

View File

@ -9,10 +9,12 @@ members = [
"examples/embassy",
"board-tests",
"bootloader",
"flashloader",
]
exclude = [
"defmt-testapp",
"flashloader/slot-a-blinky",
"flashloader/slot-b-blinky",
]
[profile.dev]
@ -20,8 +22,8 @@ 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

View File

@ -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)

View File

@ -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)]
@ -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();

View File

@ -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"

48
bootloader/README.md Normal file
View File

@ -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.

View File

@ -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>;
}

View File

@ -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
@ -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;
@ -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)
}
}
@ -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);
@ -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: {:?}",
@ -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
@ -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.
@ -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);
}
}
@ -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);
}
@ -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();
}
}

View File

@ -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"
@ -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"

View File

@ -0,0 +1,143 @@
//! More complex UART application
//!
//! Uses the IRQ capabilities of the VA10820 peripheral and the RTIC framework to poll the UART in
//! a non-blocking way. All received data will be sent back to the sender.
#![no_main]
#![no_std]
use once_cell::sync::Lazy;
use ringbuf::StaticRb;
// Larger buffer for TC to be able to hold the possibly large memory write packets.
const RX_RING_BUF_SIZE: usize = 1024;
// Ring buffers to handling variable sized telemetry
static mut RINGBUF: Lazy<StaticRb<u8, RX_RING_BUF_SIZE>> =
Lazy::new(StaticRb::<u8, RX_RING_BUF_SIZE>::default);
#[rtic::app(device = pac, dispatchers = [OC4])]
mod app {
use super::*;
use embedded_io::Write;
use panic_rtt_target as _;
use ringbuf::{
traits::{Consumer, Observer, Producer, SplitRef},
CachingCons, StaticProd,
};
use rtic_example::SYSCLK_FREQ;
use rtic_monotonics::Monotonic;
use rtt_target::{rprintln, rtt_init_print};
use va108xx_hal::{
gpio::PinsA,
pac,
prelude::*,
uart::{self, RxWithIrq, Tx},
};
#[local]
struct Local {
data_producer: StaticProd<'static, u8, RX_RING_BUF_SIZE>,
data_consumer: CachingCons<&'static StaticRb<u8, RX_RING_BUF_SIZE>>,
rx: RxWithIrq<pac::Uarta>,
tx: Tx<pac::Uarta>,
}
#[shared]
struct Shared {}
rtic_monotonics::systick_monotonic!(Mono, 1_000);
#[init]
fn init(cx: init::Context) -> (Shared, Local) {
rtt_init_print!();
rprintln!("-- VA108xx UART Echo with IRQ example application--");
Mono::start(cx.core.SYST, SYSCLK_FREQ.raw());
let mut dp = cx.device;
let gpioa = PinsA::new(&mut dp.sysconfig, Some(dp.ioconfig), dp.porta);
let tx = gpioa.pa9.into_funsel_2();
let rx = gpioa.pa8.into_funsel_2();
let irq_uart = uart::Uart::new(
&mut dp.sysconfig,
SYSCLK_FREQ,
dp.uarta,
(tx, rx),
115200.Hz(),
);
let (tx, rx) = irq_uart.split();
let mut rx = rx.into_rx_with_irq(&mut dp.sysconfig, &mut dp.irqsel, pac::interrupt::OC3);
rx.start();
let (data_producer, data_consumer) = unsafe { RINGBUF.split_ref() };
echo_handler::spawn().unwrap();
(
Shared {},
Local {
data_producer,
data_consumer,
rx,
tx,
},
)
}
// `shared` cannot be accessed from this context
#[idle]
fn idle(_cx: idle::Context) -> ! {
loop {
cortex_m::asm::nop();
}
}
#[task(
binds = OC3,
shared = [],
local = [
rx,
data_producer
],
)]
fn reception_task(cx: reception_task::Context) {
let mut buf: [u8; 16] = [0; 16];
let mut ringbuf_full = false;
let result = cx.local.rx.irq_handler(&mut buf);
if result.bytes_read > 0 && result.errors.is_none() {
if cx.local.data_producer.vacant_len() < result.bytes_read {
ringbuf_full = true;
} else {
cx.local
.data_producer
.push_slice(&buf[0..result.bytes_read]);
}
}
if ringbuf_full {
// Could also drop oldest data, but that would require the consumer to be shared.
rprintln!("buffer full, data was dropped");
}
}
#[task(shared = [], local = [
buf: [u8; RX_RING_BUF_SIZE] = [0; RX_RING_BUF_SIZE],
data_consumer,
tx
], priority=1)]
async fn echo_handler(cx: echo_handler::Context) {
loop {
let bytes_to_read = cx.local.data_consumer.occupied_len();
if bytes_to_read > 0 {
let actual_read_bytes = cx
.local
.data_consumer
.pop_slice(&mut cx.local.buf[0..bytes_to_read]);
cx.local
.tx
.write_all(&cx.local.buf[0..actual_read_bytes])
.expect("Failed to write to TX");
}
Mono::delay(50.millis()).await;
}
}
}

View File

@ -1,165 +0,0 @@
//! More complex UART application
//!
//! Uses the IRQ capabilities of the VA10820 peripheral and the RTIC framework to poll the UART in
//! a non-blocking way. You can send variably sized strings to the VA10820 which will be echoed
//! back to the sender.
//!
//! This script was tested with an Arduino Due. You can find the test script in the
//! [`/test/DueSerialTest`](https://egit.irs.uni-stuttgart.de/rust/va108xx-hal/src/branch/main/test/DueSerialTest)
//! folder.
#![no_main]
#![no_std]
#[rtic::app(device = pac, dispatchers = [OC4])]
mod app {
use embedded_io::Write;
use panic_rtt_target as _;
use rtic_example::SYSCLK_FREQ;
use rtic_sync::make_channel;
use rtt_target::{rprintln, rtt_init_print};
use va108xx_hal::{
gpio::PinsB,
pac,
prelude::*,
uart::{self, IrqCfg, IrqResult, UartWithIrqBase},
};
#[local]
struct Local {
rx_info_tx: rtic_sync::channel::Sender<'static, RxInfo, 3>,
rx_info_rx: rtic_sync::channel::Receiver<'static, RxInfo, 3>,
}
#[shared]
struct Shared {
irq_uart: UartWithIrqBase<pac::Uartb>,
rx_buf: [u8; 64],
}
#[derive(Debug, Copy, Clone)]
struct RxInfo {
pub bytes_read: usize,
pub end_idx: usize,
pub timeout: bool,
}
rtic_monotonics::systick_monotonic!(Mono, 1_000);
#[init]
fn init(cx: init::Context) -> (Shared, Local) {
rtt_init_print!();
rprintln!("-- VA108xx UART IRQ example application--");
Mono::start(cx.core.SYST, SYSCLK_FREQ.raw());
let mut dp = cx.device;
let gpiob = PinsB::new(&mut dp.sysconfig, Some(dp.ioconfig), dp.portb);
let tx = gpiob.pb21.into_funsel_1();
let rx = gpiob.pb20.into_funsel_1();
let irq_cfg = IrqCfg::new(pac::interrupt::OC3, true, true);
let (mut irq_uart, _) =
uart::Uart::new(&mut dp.sysconfig, 50.MHz(), dp.uartb, (tx, rx), 115200.Hz())
.into_uart_with_irq(irq_cfg, Some(&mut dp.sysconfig), Some(&mut dp.irqsel))
.downgrade();
irq_uart
.read_fixed_len_using_irq(64, true)
.expect("Read initialization failed");
let (rx_info_tx, rx_info_rx) = make_channel!(RxInfo, 3);
let rx_buf: [u8; 64] = [0; 64];
(
Shared { irq_uart, rx_buf },
Local {
rx_info_tx,
rx_info_rx,
},
)
}
// `shared` cannot be accessed from this context
#[idle]
fn idle(_cx: idle::Context) -> ! {
loop {
cortex_m::asm::nop();
}
}
#[task(
binds = OC3,
shared = [irq_uart, rx_buf],
local = [cnt: u32 = 0, result: IrqResult = IrqResult::new(), rx_info_tx],
)]
fn reception_task(cx: reception_task::Context) {
let result = cx.local.result;
let cnt: &mut u32 = cx.local.cnt;
let irq_uart = cx.shared.irq_uart;
let rx_buf = cx.shared.rx_buf;
let (completed, end_idx) = (irq_uart, rx_buf).lock(|irq_uart, rx_buf| {
match irq_uart.irq_handler(result, rx_buf) {
Ok(_) => {
if result.complete() {
// Initiate next transfer immediately
irq_uart
.read_fixed_len_using_irq(64, true)
.expect("Read operation init failed");
let mut end_idx = 0;
for (idx, val) in rx_buf.iter().enumerate() {
if (*val as char) == '\n' {
end_idx = idx;
break;
}
}
(true, end_idx)
} else {
(false, 0)
}
}
Err(e) => {
rprintln!("reception error {:?}", e);
(false, 0)
}
}
});
if completed {
rprintln!("counter: {}", cnt);
cx.local
.rx_info_tx
.try_send(RxInfo {
bytes_read: result.bytes_read,
end_idx,
timeout: result.timeout(),
})
.expect("RX queue full");
}
*cnt += 1;
}
#[task(shared = [irq_uart, rx_buf], local = [rx_info_rx], priority=1)]
async fn reply_handler(cx: reply_handler::Context) {
let mut irq_uart = cx.shared.irq_uart;
let mut rx_buf = cx.shared.rx_buf;
loop {
match cx.local.rx_info_rx.recv().await {
Ok(rx_info) => {
rprintln!("reception success, {} bytes read", rx_info.bytes_read);
if rx_info.timeout {
rprintln!("timeout occurred");
}
rx_buf.lock(|rx_buf| {
let string = core::str::from_utf8(&rx_buf[0..rx_info.end_idx])
.expect("Invalid string format");
rprintln!("read string: {}", string);
irq_uart.lock(|uart| {
writeln!(uart.uart, "{}", string).expect("Sending reply failed");
});
});
}
Err(e) => {
rprintln!("error receiving RX info: {:?}", e);
}
}
}
}
}

View File

@ -16,8 +16,8 @@ use va108xx_hal::{
gpio::PinsA,
pac::{self, interrupt},
prelude::*,
pwm::{default_ms_irq_handler, set_up_ms_tick, CountDownTimer},
timer::DelayMs,
timer::{default_ms_irq_handler, set_up_ms_tick, CountdownTimer},
IrqCfg,
};
@ -32,7 +32,7 @@ fn main() -> ! {
dp.tim0,
))
.unwrap();
let mut delay_tim1 = CountDownTimer::new(&mut dp.sysconfig, 50.MHz(), dp.tim1);
let mut delay_tim1 = CountdownTimer::new(&mut dp.sysconfig, 50.MHz(), dp.tim1);
let porta = PinsA::new(&mut dp.sysconfig, Some(dp.ioconfig), dp.porta);
let mut led1 = porta.pa10.into_readable_push_pull_output();
let mut led2 = porta.pa7.into_readable_push_pull_output();

View File

@ -17,13 +17,13 @@ use va108xx_hal::{
prelude::*,
timer::{
default_ms_irq_handler, set_up_ms_delay_provider, CascadeCtrl, CascadeSource,
CountDownTimer, Event, IrqCfg,
CountdownTimer, Event, IrqCfg,
},
};
static CSD_TGT_1: Mutex<RefCell<Option<CountDownTimer<pac::Tim4>>>> =
static CSD_TGT_1: Mutex<RefCell<Option<CountdownTimer<pac::Tim4>>>> =
Mutex::new(RefCell::new(None));
static CSD_TGT_2: Mutex<RefCell<Option<CountDownTimer<pac::Tim5>>>> =
static CSD_TGT_2: Mutex<RefCell<Option<CountdownTimer<pac::Tim5>>>> =
Mutex::new(RefCell::new(None));
#[entry]
@ -36,7 +36,7 @@ fn main() -> ! {
// Will be started periodically to trigger a cascade
let mut cascade_triggerer =
CountDownTimer::new(&mut dp.sysconfig, 50.MHz(), dp.tim3).auto_disable(true);
CountdownTimer::new(&mut dp.sysconfig, 50.MHz(), dp.tim3).auto_disable(true);
cascade_triggerer.listen(
Event::TimeOut,
IrqCfg::new(pac::Interrupt::OC1, true, false),
@ -46,7 +46,7 @@ fn main() -> ! {
// First target for cascade
let mut cascade_target_1 =
CountDownTimer::new(&mut dp.sysconfig, 50.MHz(), dp.tim4).auto_deactivate(true);
CountdownTimer::new(&mut dp.sysconfig, 50.MHz(), dp.tim4).auto_deactivate(true);
cascade_target_1
.cascade_0_source(CascadeSource::Tim(3))
.expect("Configuring cascade source for TIM4 failed");
@ -72,7 +72,7 @@ fn main() -> ! {
// Activated by first cascade target
let mut cascade_target_2 =
CountDownTimer::new(&mut dp.sysconfig, 50.MHz(), dp.tim5).auto_deactivate(true);
CountdownTimer::new(&mut dp.sysconfig, 50.MHz(), dp.tim5).auto_deactivate(true);
// Set TIM4 as cascade source
cascade_target_2
.cascade_1_source(CascadeSource::Tim(4))

View File

@ -15,8 +15,8 @@ use va108xx_hal::{
gpio::{PinsA, PinsB},
pac::{self, interrupt},
prelude::*,
pwm::{default_ms_irq_handler, set_up_ms_tick},
spi::{self, Spi, SpiBase, SpiClkConfig, TransferConfigWithHwcs},
timer::{default_ms_irq_handler, set_up_ms_tick},
IrqCfg,
};

View File

@ -12,7 +12,7 @@ use va108xx_hal::{
pac::{self, interrupt},
prelude::*,
time::Hertz,
timer::{default_ms_irq_handler, set_up_ms_tick, CountDownTimer, Event, IrqCfg, MS_COUNTER},
timer::{default_ms_irq_handler, set_up_ms_tick, CountdownTimer, Event, IrqCfg, MS_COUNTER},
};
#[allow(dead_code)]
@ -72,7 +72,7 @@ fn main() -> ! {
dp.tim0,
);
let mut second_timer =
CountDownTimer::new(&mut dp.sysconfig, get_sys_clock().unwrap(), dp.tim1);
CountdownTimer::new(&mut dp.sysconfig, get_sys_clock().unwrap(), dp.tim1);
second_timer.listen(
Event::TimeOut,
IrqCfg::new(interrupt::OC1, true, true),

1
flashloader/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/venv

65
flashloader/Cargo.toml Normal file
View File

@ -0,0 +1,65 @@
[package]
name = "flashloader"
version = "0.1.0"
edition = "2021"
[dependencies]
cortex-m = "0.7"
cortex-m-rt = "0.7"
embedded-hal = "1"
embedded-hal-nb = "1"
embedded-io = "0.6"
panic-rtt-target = { version = "0.1.3" }
rtt-target = { version = "0.5" }
log = "0.4"
crc = "3"
[dependencies.satrs]
version = "0.2"
default-features = false
[dependencies.rtt-log]
version = "0.4"
[dependencies.ringbuf]
version = "0.4.7"
default-features = false
features = ["portable-atomic"]
[dependencies.once_cell]
version = "1"
default-features = false
features = ["critical-section"]
[dependencies.spacepackets]
version = "0.11"
default-features = false
[dependencies.cobs]
git = "https://github.com/robamu/cobs.rs.git"
branch = "all_features"
default-features = false
# 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.
[dependencies.portable-atomic]
version = "1"
features = ["unsafe-assume-single-core"]
[dependencies.rtic]
version = "2"
features = ["thumbv6-backend"]
[dependencies.rtic-monotonics]
version = "2"
features = ["cortex-m-systick"]
[dependencies.rtic-sync]
version = "1"
features = ["defmt-03"]
[dependencies.va108xx-hal]
path = "../va108xx-hal"
[dependencies.vorago-reb1]
path = "../vorago-reb1"

66
flashloader/README.md Normal file
View File

@ -0,0 +1,66 @@
VA108xx Flashloader Application
========
This flashloader shows a minimal example for a self-updatable Rust software which exposes
a simple PUS (CCSDS) interface to update the software. It also provides a Python application
called the `image-loader.py` which can be used to upload compiled images to the flashloader
application to write them to the NVM.
Please note that the both the application and the image loader are tailored towards usage
with the [bootloader provided by this repository](https://egit.irs.uni-stuttgart.de/rust/va416xx-rs/src/branch/main/bootloader).
The software can quickly be adapted to interface with a real primary on-board software instead of
the Python script provided here to upload images because it uses a low-level CCSDS based packet
interface.
## Using the Python image loader
The Python image loader communicates with the Rust flashload application using a dedicated serial
port with a baudrate of 115200.
It is recommended to run the script in a dedicated virtual environment. For example, on UNIX
systems you can use `python3 -m venv venv` and then `source venv/bin/activate` to create
and activate a virtual environment.
After that, you can use
```sh
pip install -r requirements.txt
```
to install all required dependencies.
After that, it is recommended to use `./image-load.py -h` to get an overview of some options.
The flash loader uses the UART0 with the Pins PA8 (RX) and PA9 (TX) interface of the VA108xx to perform CCSDS based
communication. The Python image loader application will search for a file named `loader.toml` and
use the `serial_port` key to determine the serial port to use for serial communication.
### Examples
You can use
```sh
./image-loader.py -p
```
to send a ping an verify the connection.
You can use
```sh
cd flashloader/slot-a-blinky
cargo build --release
cd ../..
./image-loader.py -t a ./slot-a-blinky/target/thumbv6m-none-eabi/release/slot-a-blinky
```
to build the slot A sample application and upload it to a running flash loader application
to write it to slot A.
You can use
```sh
./image-loader.py -c -t a
```
to corrupt the image A and test that it switches to image B after a failed CRC check instead.

430
flashloader/image-loader.py Executable file
View File

@ -0,0 +1,430 @@
#!/usr/bin/env python3
from typing import List, Tuple
from spacepackets.ecss.defs import PusService
from spacepackets.ecss.tm import PusTm
from tmtccmd.com import ComInterface
import toml
import struct
import logging
import argparse
import time
import enum
from tmtccmd.com.serial_base import SerialCfg
from tmtccmd.com.serial_cobs import SerialCobsComIF
from tmtccmd.com.ser_utils import prompt_com_port
from crcmod.predefined import PredefinedCrc
from spacepackets.ecss.tc import PusTc
from spacepackets.ecss.pus_verificator import PusVerificator, StatusField
from spacepackets.ecss.pus_1_verification import Service1Tm, UnpackParams
from spacepackets.seqcount import SeqCountProvider
from pathlib import Path
import dataclasses
from elftools.elf.elffile import ELFFile
BAUD_RATE = 115200
BOOTLOADER_START_ADDR = 0x0
BOOTLOADER_END_ADDR = 0x3000
BOOTLOADER_CRC_ADDR = BOOTLOADER_END_ADDR - 2
BOOTLOADER_MAX_SIZE = BOOTLOADER_END_ADDR - BOOTLOADER_START_ADDR - 2
APP_A_START_ADDR = 0x3000
APP_A_END_ADDR = 0x11800
# The actual size of the image which is relevant for CRC calculation.
APP_A_SIZE_ADDR = APP_A_END_ADDR - 8
APP_A_CRC_ADDR = APP_A_END_ADDR - 4
APP_A_MAX_SIZE = APP_A_END_ADDR - APP_A_START_ADDR - 8
APP_B_START_ADDR = APP_A_END_ADDR
APP_B_END_ADDR = 0x20000
# The actual size of the image which is relevant for CRC calculation.
APP_B_SIZE_ADDR = APP_B_END_ADDR - 8
APP_B_CRC_ADDR = APP_B_END_ADDR - 4
APP_B_MAX_SIZE = APP_A_END_ADDR - APP_A_START_ADDR - 8
APP_IMG_SZ = (APP_B_END_ADDR - APP_A_START_ADDR) // 2
CHUNK_SIZE = 400
MEMORY_SERVICE = 6
ACTION_SERVICE = 8
RAW_MEMORY_WRITE_SUBSERVICE = 2
BOOT_NVM_MEMORY_ID = 1
PING_PAYLOAD_SIZE = 0
class ActionId(enum.IntEnum):
CORRUPT_APP_A = 128
CORRUPT_APP_B = 129
_LOGGER = logging.getLogger(__name__)
SEQ_PROVIDER = SeqCountProvider(bit_width=14)
@dataclasses.dataclass
class LoadableSegment:
name: str
offset: int
size: int
data: bytes
class Target(enum.Enum):
BOOTLOADER = 0
APP_A = 1
APP_B = 2
class ImageLoader:
def __init__(self, com_if: ComInterface, verificator: PusVerificator) -> None:
self.com_if = com_if
self.verificator = verificator
def handle_ping_cmd(self):
_LOGGER.info("Sending ping command")
ping_tc = PusTc(
apid=0x00,
service=PusService.S17_TEST,
subservice=1,
seq_count=SEQ_PROVIDER.get_and_increment(),
app_data=bytes(PING_PAYLOAD_SIZE),
)
self.verificator.add_tc(ping_tc)
self.com_if.send(bytes(ping_tc.pack()))
data_available = self.com_if.data_available(0.4)
if not data_available:
_LOGGER.warning("no ping reply received")
for reply in self.com_if.receive():
result = self.verificator.add_tm(
Service1Tm.from_tm(PusTm.unpack(reply, 0), UnpackParams(0))
)
if result is not None and result.completed:
_LOGGER.info("received ping completion reply")
def handle_corruption_cmd(self, target: Target):
if target == Target.BOOTLOADER:
_LOGGER.error("can not corrupt bootloader")
if target == Target.APP_A:
self.send_tc(
PusTc(
apid=0,
service=ACTION_SERVICE,
subservice=ActionId.CORRUPT_APP_A,
),
)
if target == Target.APP_B:
self.send_tc(
PusTc(
apid=0,
service=ACTION_SERVICE,
subservice=ActionId.CORRUPT_APP_B,
),
)
def handle_flash_cmd(self, target: Target, file_path: Path) -> int:
loadable_segments = []
_LOGGER.info("Parsing ELF file for loadable sections")
total_size = 0
loadable_segments, total_size = create_loadable_segments(target, file_path)
segments_info_str(target, loadable_segments, total_size, file_path)
result = self._perform_flashing_algorithm(loadable_segments)
if result != 0:
return result
self._crc_and_app_size_postprocessing(target, total_size, loadable_segments)
return 0
def _perform_flashing_algorithm(
self,
loadable_segments: List[LoadableSegment],
) -> int:
# Perform the flashing algorithm.
for segment in loadable_segments:
segment_end = segment.offset + segment.size
current_addr = segment.offset
pos_in_segment = 0
while pos_in_segment < segment.size:
next_chunk_size = min(segment_end - current_addr, CHUNK_SIZE)
data = segment.data[pos_in_segment : pos_in_segment + next_chunk_size]
next_packet = pack_memory_write_command(current_addr, data)
_LOGGER.info(
f"Sending memory write command for address {current_addr:#08x} and data with "
f"length {len(data)}"
)
self.verificator.add_tc(next_packet)
self.com_if.send(bytes(next_packet.pack()))
current_addr += next_chunk_size
pos_in_segment += next_chunk_size
start_time = time.time()
while True:
if time.time() - start_time > 1.0:
_LOGGER.error("Timeout while waiting for reply")
return -1
data_available = self.com_if.data_available(0.1)
done = False
if not data_available:
continue
replies = self.com_if.receive()
for reply in replies:
tm = PusTm.unpack(reply, 0)
if tm.service != 1:
continue
service_1_tm = Service1Tm.from_tm(tm, UnpackParams(0))
check_result = self.verificator.add_tm(service_1_tm)
# We could send after we have received the step reply, but that can
# somehow lead to overrun errors. I think it's okay to do it like
# this as long as the flash loader only uses polling..
if (
check_result is not None
and check_result.status.completed == StatusField.SUCCESS
):
done = True
# This is an optimized variant, but I think the small delay is not an issue.
"""
if (
check_result is not None
and check_result.status.step == StatusField.SUCCESS
and len(check_result.status.step_list) == 1
):
done = True
"""
self.verificator.remove_completed_entries()
if done:
break
return 0
def _crc_and_app_size_postprocessing(
self,
target: Target,
total_size: int,
loadable_segments: List[LoadableSegment],
):
if target == Target.BOOTLOADER:
_LOGGER.info("Blanking the bootloader checksum")
# Blank the checksum. For the bootloader, the bootloader will calculate the
# checksum itself on the initial run.
checksum_write_packet = pack_memory_write_command(
BOOTLOADER_CRC_ADDR, bytes([0x00, 0x00])
)
self.send_tc(checksum_write_packet)
else:
crc_addr = None
size_addr = None
if target == Target.APP_A:
crc_addr = APP_A_CRC_ADDR
size_addr = APP_A_SIZE_ADDR
elif target == Target.APP_B:
crc_addr = APP_B_CRC_ADDR
size_addr = APP_B_SIZE_ADDR
assert crc_addr is not None
assert size_addr is not None
_LOGGER.info(f"Writing app size {total_size} at address {size_addr:#08x}")
size_write_packet = pack_memory_write_command(
size_addr, struct.pack("!I", total_size)
)
self.com_if.send(bytes(size_write_packet.pack()))
time.sleep(0.2)
crc_calc = PredefinedCrc("crc-ccitt-false")
for segment in loadable_segments:
crc_calc.update(segment.data)
checksum = crc_calc.digest()
_LOGGER.info(
f"Writing checksum 0x[{checksum.hex(sep=',')}] at address {crc_addr:#08x}"
)
self.send_tc(pack_memory_write_command(crc_addr, checksum))
def send_tc(self, tc: PusTc):
self.com_if.send(bytes(tc.pack()))
def main() -> int:
print("Python VA108XX Image Loader Application")
logging.basicConfig(
format="[%(asctime)s] [%(levelname)s] %(message)s", level=logging.DEBUG
)
parser = argparse.ArgumentParser(
prog="image-loader", description="Python VA416XX Image Loader Application"
)
parser.add_argument("-p", "--ping", action="store_true", help="Send ping command")
parser.add_argument("-c", "--corrupt", action="store_true", help="Corrupt a target")
parser.add_argument(
"-t",
"--target",
choices=["bl", "a", "b"],
help="Target (Bootloader or slot A or B)",
)
parser.add_argument(
"path", nargs="?", default=None, help="Path to the App to flash"
)
args = parser.parse_args()
serial_port = None
if Path("loader.toml").exists():
with open("loader.toml", "r") as toml_file:
parsed_toml = toml.loads(toml_file.read())
if "serial_port" in parsed_toml:
serial_port = parsed_toml["serial_port"]
if serial_port is None:
serial_port = prompt_com_port()
serial_cfg = SerialCfg(
com_if_id="ser_cobs",
serial_port=serial_port,
baud_rate=BAUD_RATE,
serial_timeout=0.1,
)
verificator = PusVerificator()
com_if = SerialCobsComIF(serial_cfg)
com_if.open()
target = None
if args.target == "bl":
target = Target.BOOTLOADER
elif args.target == "a":
target = Target.APP_A
elif args.target == "b":
target = Target.APP_B
image_loader = ImageLoader(com_if, verificator)
file_path = None
result = -1
if args.ping:
image_loader.handle_ping_cmd()
com_if.close()
return 0
if target:
if not args.corrupt:
if not args.path:
_LOGGER.error("App Path needs to be specified for the flash process")
file_path = Path(args.path)
if not file_path.exists():
_LOGGER.error("File does not exist")
if args.corrupt:
if not target:
_LOGGER.error("target for corruption command required")
com_if.close()
return -1
image_loader.handle_corruption_cmd(target)
else:
assert file_path is not None
assert target is not None
result = image_loader.handle_flash_cmd(target, file_path)
com_if.close()
return result
def create_loadable_segments(
target: Target, file_path: Path
) -> Tuple[List[LoadableSegment], int]:
loadable_segments = []
total_size = 0
with open(file_path, "rb") as app_file:
elf_file = ELFFile(app_file)
for idx, segment in enumerate(elf_file.iter_segments("PT_LOAD")):
if segment.header.p_filesz == 0:
continue
# Basic validity checks of the base addresses.
if idx == 0:
if (
target == Target.BOOTLOADER
and segment.header.p_paddr != BOOTLOADER_START_ADDR
):
raise ValueError(
f"detected possibly invalid start address {segment.header.p_paddr:#08x} for "
f"bootloader, expected {BOOTLOADER_START_ADDR}"
)
if (
target == Target.APP_A
and segment.header.p_paddr != APP_A_START_ADDR
):
raise ValueError(
f"detected possibly invalid start address {segment.header.p_paddr:#08x} for "
f"App A, expected {APP_A_START_ADDR}"
)
if (
target == Target.APP_B
and segment.header.p_paddr != APP_B_START_ADDR
):
raise ValueError(
f"detected possibly invalid start address {segment.header.p_paddr:#08x} for "
f"App B, expected {APP_B_START_ADDR}"
)
name = None
for section in elf_file.iter_sections():
if (
section.header.sh_offset == segment.header.p_offset
and section.header.sh_size > 0
):
name = section.name
if name is None:
_LOGGER.warning("no fitting section found for segment")
continue
# print(f"Segment Addr: {segment.header.p_paddr}")
# print(f"Segment Offset: {segment.header.p_offset}")
# print(f"Segment Filesize: {segment.header.p_filesz}")
loadable_segments.append(
LoadableSegment(
name=name,
offset=segment.header.p_paddr,
size=segment.header.p_filesz,
data=segment.data(),
)
)
total_size += segment.header.p_filesz
return loadable_segments, total_size
def segments_info_str(
target: Target,
loadable_segments: List[LoadableSegment],
total_size: int,
file_path: Path,
):
# Set context string and perform basic sanity checks.
if target == Target.BOOTLOADER:
if total_size > BOOTLOADER_MAX_SIZE:
_LOGGER.error(
f"provided bootloader app larger than allowed {total_size} bytes"
)
return -1
context_str = "Bootloader"
elif target == Target.APP_A:
if total_size > APP_A_MAX_SIZE:
_LOGGER.error(f"provided App A larger than allowed {total_size} bytes")
return -1
context_str = "App Slot A"
elif target == Target.APP_B:
if total_size > APP_B_MAX_SIZE:
_LOGGER.error(f"provided App B larger than allowed {total_size} bytes")
return -1
context_str = "App Slot B"
_LOGGER.info(f"Flashing {context_str} with image {file_path} (size {total_size})")
for idx, segment in enumerate(loadable_segments):
_LOGGER.info(
f"Loadable section {idx} {segment.name} with offset {segment.offset:#08x} and "
f"size {segment.size}"
)
def pack_memory_write_command(addr: int, data: bytes) -> PusTc:
app_data = bytearray()
app_data.append(BOOT_NVM_MEMORY_ID)
# N parameter is always 1 here.
app_data.append(1)
app_data.extend(struct.pack("!I", addr))
app_data.extend(struct.pack("!I", len(data)))
app_data.extend(data)
return PusTc(
apid=0,
service=MEMORY_SERVICE,
subservice=RAW_MEMORY_WRITE_SUBSERVICE,
seq_count=SEQ_PROVIDER.get_and_increment(),
app_data=bytes(app_data),
)
if __name__ == "__main__":
main()

1
flashloader/loader.toml Normal file
View File

@ -0,0 +1 @@
serial_port = "/dev/ttyUSB0"

View File

@ -0,0 +1,5 @@
spacepackets == 0.24
tmtccmd == 8.0.2
toml == 0.10
pyelftools == 0.31
crcmod == 1.7

2
flashloader/slot-a-blinky/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
/app.map

View File

@ -0,0 +1,42 @@
[package]
name = "slot-a-blinky"
version = "0.1.0"
edition = "2021"
[workspace]
[dependencies]
cortex-m-rt = "0.7"
panic-rtt-target = { version = "0.1.3" }
rtt-target = { version = "0.5" }
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
embedded-hal = "1"
va108xx-hal = { path = "../../va108xx-hal" }
[profile.dev]
codegen-units = 1
debug = 2
debug-assertions = true # <-
incremental = false
# This is problematic for stepping..
# opt-level = 'z' # <-
overflow-checks = true # <-
# cargo build/run --release
[profile.release]
codegen-units = 1
debug = 2
debug-assertions = false # <-
incremental = false
lto = 'fat'
opt-level = 3 # <-
overflow-checks = false # <-
[profile.small]
inherits = "release"
codegen-units = 1
debug-assertions = false # <-
lto = true
opt-level = 'z' # <-
overflow-checks = false # <-
# strip = true # Automatically strip symbols from the binary.

View File

@ -0,0 +1,11 @@
/* Special linker script for application slot A with an offset at address 0x3000 */
MEMORY
{
FLASH : ORIGIN = 0x00003000, LENGTH = 0xE800
RAM : ORIGIN = 0x10000000, LENGTH = 0x08000 /* 32K */
}
/* This is where the call stack will be allocated. */
/* The stack is of the full descending type. */
/* NOTE Do NOT modify `_stack_start` unless you know what you are doing */
_stack_start = ORIGIN(RAM) + LENGTH(RAM);

View File

@ -0,0 +1,25 @@
//! Simple blinky example using the HAL
#![no_main]
#![no_std]
use cortex_m_rt::entry;
use embedded_hal::{delay::DelayNs, digital::StatefulOutputPin};
use panic_rtt_target as _;
use rtt_target::{rprintln, rtt_init_print};
use va108xx_hal::{gpio::PinsA, pac, prelude::*, timer::CountdownTimer};
#[entry]
fn main() -> ! {
rtt_init_print!();
rprintln!("VA108xx HAL blinky example for App Slot A");
let mut dp = pac::Peripherals::take().unwrap();
let mut timer = CountdownTimer::new(&mut dp.sysconfig, 50.MHz(), dp.tim0);
let porta = PinsA::new(&mut dp.sysconfig, Some(dp.ioconfig), dp.porta);
let mut led1 = porta.pa10.into_readable_push_pull_output();
loop {
led1.toggle().ok();
timer.delay_ms(500);
}
}

2
flashloader/slot-b-blinky/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
/app.map

View File

@ -0,0 +1,42 @@
[package]
name = "slot-b-blinky"
version = "0.1.0"
edition = "2021"
[workspace]
[dependencies]
cortex-m-rt = "0.7"
panic-rtt-target = { version = "0.1.3" }
rtt-target = { version = "0.5" }
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
embedded-hal = "1"
va108xx-hal = { path = "../../va108xx-hal" }
[profile.dev]
codegen-units = 1
debug = 2
debug-assertions = true # <-
incremental = false
# This is problematic for stepping..
# opt-level = 'z' # <-
overflow-checks = true # <-
# cargo build/run --release
[profile.release]
codegen-units = 1
debug = 2
debug-assertions = false # <-
incremental = false
lto = 'fat'
opt-level = 3 # <-
overflow-checks = false # <-
[profile.small]
inherits = "release"
codegen-units = 1
debug-assertions = false # <-
lto = true
opt-level = 'z' # <-
overflow-checks = false # <-
# strip = true # Automatically strip symbols from the binary.

View File

@ -0,0 +1,11 @@
/* Special linker script for application slot B with an offset at address 0x11800 */
MEMORY
{
FLASH : ORIGIN = 0x00011800, LENGTH = 0xE800
RAM : ORIGIN = 0x10000000, LENGTH = 0x08000 /* 32K */
}
/* This is where the call stack will be allocated. */
/* The stack is of the full descending type. */
/* NOTE Do NOT modify `_stack_start` unless you know what you are doing */
_stack_start = ORIGIN(RAM) + LENGTH(RAM);

View File

@ -0,0 +1,25 @@
//! Simple blinky example using the HAL
#![no_main]
#![no_std]
use cortex_m_rt::entry;
use embedded_hal::{delay::DelayNs, digital::StatefulOutputPin};
use panic_rtt_target as _;
use rtt_target::{rprintln, rtt_init_print};
use va108xx_hal::{gpio::PinsA, pac, prelude::*, timer::CountdownTimer};
#[entry]
fn main() -> ! {
rtt_init_print!();
rprintln!("VA108xx HAL blinky example for App Slot B");
let mut dp = pac::Peripherals::take().unwrap();
let mut timer = CountdownTimer::new(&mut dp.sysconfig, 50.MHz(), dp.tim0);
let porta = PinsA::new(&mut dp.sysconfig, Some(dp.ioconfig), dp.porta);
let mut led2 = porta.pa7.into_readable_push_pull_output();
loop {
led2.toggle().ok();
timer.delay_ms(1000);
}
}

9
flashloader/src/lib.rs Normal file
View File

@ -0,0 +1,9 @@
#![no_std]
#[cfg(test)]
mod tests {
#[test]
fn simple() {
assert_eq!(1 + 1, 2);
}
}

474
flashloader/src/main.rs Normal file
View File

@ -0,0 +1,474 @@
//! Vorago flashloader which can be used to flash image A and image B via a simple
//! low-level CCSDS memory interface via a UART interface.
#![no_main]
#![no_std]
use once_cell::sync::Lazy;
use panic_rtt_target as _;
use ringbuf::{
traits::{Consumer, Observer, Producer, SplitRef},
CachingCons, StaticProd, StaticRb,
};
use va108xx_hal::prelude::*;
const SYSCLK_FREQ: Hertz = Hertz::from_raw(50_000_000);
const MAX_TC_SIZE: usize = 524;
const MAX_TC_FRAME_SIZE: usize = cobs::max_encoding_length(MAX_TC_SIZE);
const MAX_TM_SIZE: usize = 128;
const MAX_TM_FRAME_SIZE: usize = cobs::max_encoding_length(MAX_TM_SIZE);
const UART_BAUDRATE: u32 = 115200;
const BOOT_NVM_MEMORY_ID: u8 = 1;
const RX_DEBUGGING: bool = false;
pub enum ActionId {
CorruptImageA = 128,
CorruptImageB = 129,
}
// Larger buffer for TC to be able to hold the possibly large memory write packets.
const BUF_RB_SIZE_TC: usize = 1024;
const SIZES_RB_SIZE_TC: usize = 16;
const BUF_RB_SIZE_TM: usize = 256;
const SIZES_RB_SIZE_TM: usize = 16;
// Ring buffers to handling variable sized telemetry
static mut BUF_RB_TM: Lazy<StaticRb<u8, BUF_RB_SIZE_TM>> =
Lazy::new(StaticRb::<u8, BUF_RB_SIZE_TM>::default);
static mut SIZES_RB_TM: Lazy<StaticRb<usize, SIZES_RB_SIZE_TM>> =
Lazy::new(StaticRb::<usize, SIZES_RB_SIZE_TM>::default);
// Ring buffers to handling variable sized telecommands
static mut BUF_RB_TC: Lazy<StaticRb<u8, BUF_RB_SIZE_TC>> =
Lazy::new(StaticRb::<u8, BUF_RB_SIZE_TC>::default);
static mut SIZES_RB_TC: Lazy<StaticRb<usize, SIZES_RB_SIZE_TC>> =
Lazy::new(StaticRb::<usize, SIZES_RB_SIZE_TC>::default);
pub struct DataProducer<const BUF_SIZE: usize, const SIZES_LEN: usize> {
pub buf_prod: StaticProd<'static, u8, BUF_SIZE>,
pub sizes_prod: StaticProd<'static, usize, SIZES_LEN>,
}
pub struct DataConsumer<const BUF_SIZE: usize, const SIZES_LEN: usize> {
pub buf_cons: CachingCons<&'static StaticRb<u8, BUF_SIZE>>,
pub sizes_cons: CachingCons<&'static StaticRb<usize, SIZES_LEN>>,
}
pub const APP_A_START_ADDR: u32 = 0x3000;
pub const APP_A_END_ADDR: u32 = 0x11800;
pub const APP_B_START_ADDR: u32 = APP_A_END_ADDR;
pub const APP_B_END_ADDR: u32 = 0x20000;
#[rtic::app(device = pac, dispatchers = [OC20, OC21, OC22])]
mod app {
use super::*;
use cortex_m::asm;
use embedded_io::Write;
use panic_rtt_target as _;
use rtic::Mutex;
use rtic_monotonics::systick::prelude::*;
use rtt_target::rprintln;
use satrs::pus::verification::{FailParams, VerificationReportCreator};
use spacepackets::ecss::PusServiceId;
use spacepackets::ecss::{
tc::PusTcReader, tm::PusTmCreator, EcssEnumU8, PusPacket, WritablePusPacket,
};
use va108xx_hal::gpio::PinsA;
use va108xx_hal::uart::IrqContextTimeoutOrMaxSize;
use va108xx_hal::{pac, uart};
use vorago_reb1::m95m01::M95M01;
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
pub enum CobsReaderStates {
#[default]
WaitingForStart,
WatingForEnd,
FrameOverflow,
}
#[local]
struct Local {
uart_rx: uart::RxWithIrq<pac::Uarta>,
uart_tx: uart::Tx<pac::Uarta>,
rx_context: IrqContextTimeoutOrMaxSize,
// We handle all TM in one task.
tm_cons: DataConsumer<BUF_RB_SIZE_TM, SIZES_RB_SIZE_TM>,
// We consume all TC in one task.
tc_cons: DataConsumer<BUF_RB_SIZE_TC, SIZES_RB_SIZE_TC>,
// We produce all TC in one task.
tc_prod: DataProducer<BUF_RB_SIZE_TC, SIZES_RB_SIZE_TC>,
verif_reporter: VerificationReportCreator,
nvm: M95M01,
}
#[shared]
struct Shared {
// Having this shared allows multiple tasks to generate telemetry.
tm_prod: DataProducer<BUF_RB_SIZE_TM, SIZES_RB_SIZE_TM>,
}
rtic_monotonics::systick_monotonic!(Mono, 1000);
#[init]
fn init(cx: init::Context) -> (Shared, Local) {
rtt_log::init();
rprintln!("-- Vorago flashloader --");
Mono::start(cx.core.SYST, SYSCLK_FREQ.raw());
let mut dp = cx.device;
let nvm = M95M01::new(&mut dp.sysconfig, SYSCLK_FREQ, dp.spic);
let gpioa = PinsA::new(&mut dp.sysconfig, Some(dp.ioconfig), dp.porta);
let tx = gpioa.pa9.into_funsel_2();
let rx = gpioa.pa8.into_funsel_2();
let irq_uart = uart::Uart::new(
&mut dp.sysconfig,
SYSCLK_FREQ,
dp.uarta,
(tx, rx),
UART_BAUDRATE.Hz(),
);
let (tx, rx) = irq_uart.split();
let mut rx = rx.into_rx_with_irq(&mut dp.sysconfig, &mut dp.irqsel, pac::interrupt::OC0);
let verif_reporter = VerificationReportCreator::new(0).unwrap();
let (buf_prod_tm, buf_cons_tm) = unsafe { BUF_RB_TM.split_ref() };
let (sizes_prod_tm, sizes_cons_tm) = unsafe { SIZES_RB_TM.split_ref() };
let (buf_prod_tc, buf_cons_tc) = unsafe { BUF_RB_TC.split_ref() };
let (sizes_prod_tc, sizes_cons_tc) = unsafe { SIZES_RB_TC.split_ref() };
let mut rx_context = IrqContextTimeoutOrMaxSize::new(MAX_TC_FRAME_SIZE);
rx.read_fixed_len_or_timeout_based_using_irq(&mut rx_context)
.expect("initiating UART RX failed");
pus_tc_handler::spawn().unwrap();
pus_tm_tx_handler::spawn().unwrap();
(
Shared {
tm_prod: DataProducer {
buf_prod: buf_prod_tm,
sizes_prod: sizes_prod_tm,
},
},
Local {
uart_rx: rx,
uart_tx: tx,
rx_context,
tm_cons: DataConsumer {
buf_cons: buf_cons_tm,
sizes_cons: sizes_cons_tm,
},
tc_cons: DataConsumer {
buf_cons: buf_cons_tc,
sizes_cons: sizes_cons_tc,
},
tc_prod: DataProducer {
buf_prod: buf_prod_tc,
sizes_prod: sizes_prod_tc,
},
verif_reporter,
nvm,
},
)
}
// `shared` cannot be accessed from this context
#[idle]
fn idle(_cx: idle::Context) -> ! {
loop {
asm::nop();
}
}
// This is the interrupt handler to read all bytes received on the UART0.
#[task(
binds = OC0,
local = [
cnt: u32 = 0,
rx_buf: [u8; MAX_TC_FRAME_SIZE] = [0; MAX_TC_FRAME_SIZE],
rx_context,
uart_rx,
tc_prod
],
)]
fn uart_rx_irq(cx: uart_rx_irq::Context) {
match cx
.local
.uart_rx
.irq_handler_max_size_or_timeout_based(cx.local.rx_context, cx.local.rx_buf)
{
Ok(result) => {
if RX_DEBUGGING {
log::debug!("RX Info: {:?}", cx.local.rx_context);
log::debug!("RX Result: {:?}", result);
}
if result.complete() {
// Check frame validity (must have COBS format) and decode the frame.
// Currently, we expect a full frame or a frame received through a timeout
// to be one COBS frame. We could parse for multiple COBS packets in one
// frame, but the additional complexity is not necessary here..
if cx.local.rx_buf[0] == 0 && cx.local.rx_buf[result.bytes_read - 1] == 0 {
let decoded_size =
cobs::decode_in_place(&mut cx.local.rx_buf[1..result.bytes_read]);
if decoded_size.is_err() {
log::warn!("COBS decoding failed");
} else {
let decoded_size = decoded_size.unwrap();
if cx.local.tc_prod.sizes_prod.vacant_len() >= 1
&& cx.local.tc_prod.buf_prod.vacant_len() >= decoded_size
{
// Should never fail, we checked there is enough space.
cx.local.tc_prod.sizes_prod.try_push(decoded_size).unwrap();
cx.local
.tc_prod
.buf_prod
.push_slice(&cx.local.rx_buf[1..1 + decoded_size]);
} else {
log::warn!("COBS TC queue full");
}
}
} else {
log::warn!("COBS frame with invalid format, start and end bytes are not 0");
}
// Initiate next transfer.
cx.local
.uart_rx
.read_fixed_len_or_timeout_based_using_irq(cx.local.rx_context)
.expect("read operation failed");
}
if result.has_errors() {
log::warn!("UART error: {:?}", result.errors.unwrap());
}
}
Err(e) => {
log::warn!("UART error: {:?}", e);
}
}
}
#[task(
priority = 2,
local=[
tc_buf: [u8; MAX_TC_SIZE] = [0; MAX_TC_SIZE],
readback_buf: [u8; MAX_TC_SIZE] = [0; MAX_TC_SIZE],
src_data_buf: [u8; 16] = [0; 16],
verif_buf: [u8; 32] = [0; 32],
tc_cons,
nvm,
verif_reporter
],
shared=[tm_prod]
)]
async fn pus_tc_handler(mut cx: pus_tc_handler::Context) {
loop {
// Try to read a TC from the ring buffer.
let packet_len = cx.local.tc_cons.sizes_cons.try_pop();
if packet_len.is_none() {
// Small delay, TCs might arrive very quickly.
Mono::delay(20.millis()).await;
continue;
}
let packet_len = packet_len.unwrap();
log::info!(target: "TC Handler", "received packet with length {}", packet_len);
assert_eq!(
cx.local
.tc_cons
.buf_cons
.pop_slice(&mut cx.local.tc_buf[0..packet_len]),
packet_len
);
// Read a telecommand, now handle it.
handle_valid_pus_tc(&mut cx);
}
}
fn handle_valid_pus_tc(cx: &mut pus_tc_handler::Context) {
let pus_tc = PusTcReader::new(cx.local.tc_buf);
if pus_tc.is_err() {
log::warn!(target: "TC Handler", "PUS TC error: {}", pus_tc.unwrap_err());
return;
}
let (pus_tc, _) = pus_tc.unwrap();
let mut write_and_send = |tm: &PusTmCreator| {
let written_size = tm.write_to_bytes(cx.local.verif_buf).unwrap();
cx.shared.tm_prod.lock(|prod| {
prod.sizes_prod.try_push(tm.len_written()).unwrap();
prod.buf_prod
.push_slice(&cx.local.verif_buf[0..written_size]);
});
};
let token = cx.local.verif_reporter.add_tc(&pus_tc);
let (tm, accepted_token) = cx
.local
.verif_reporter
.acceptance_success(cx.local.src_data_buf, token, 0, 0, &[])
.expect("acceptance success failed");
write_and_send(&tm);
let (tm, started_token) = cx
.local
.verif_reporter
.start_success(cx.local.src_data_buf, accepted_token, 0, 0, &[])
.expect("acceptance success failed");
write_and_send(&tm);
if pus_tc.service() == PusServiceId::Action as u8 {
let mut corrupt_image = |base_addr: u32| {
let mut buf = [0u8; 4];
cx.local
.nvm
.read(base_addr as usize + 32, &mut buf)
.expect("reading from NVM failed");
buf[0] += 1;
cx.local
.nvm
.write(base_addr as usize + 32, &buf)
.expect("writing to NVM failed");
let tm = cx
.local
.verif_reporter
.completion_success(cx.local.src_data_buf, started_token, 0, 0, &[])
.expect("completion success failed");
write_and_send(&tm);
};
if pus_tc.subservice() == ActionId::CorruptImageA as u8 {
rprintln!("corrupting App Image A");
corrupt_image(APP_A_START_ADDR);
}
if pus_tc.subservice() == ActionId::CorruptImageB as u8 {
rprintln!("corrupting App Image B");
corrupt_image(APP_B_START_ADDR);
}
}
if pus_tc.service() == PusServiceId::Test as u8 && pus_tc.subservice() == 1 {
log::info!(target: "TC Handler", "received ping TC");
let tm = cx
.local
.verif_reporter
.completion_success(cx.local.src_data_buf, started_token, 0, 0, &[])
.expect("completion success failed");
write_and_send(&tm);
} else if pus_tc.service() == PusServiceId::MemoryManagement as u8 {
let tm = cx
.local
.verif_reporter
.step_success(
cx.local.src_data_buf,
&started_token,
0,
0,
&[],
EcssEnumU8::new(0),
)
.expect("step success failed");
write_and_send(&tm);
// Raw memory write TC
if pus_tc.subservice() == 2 {
let app_data = pus_tc.app_data();
if app_data.len() < 10 {
log::warn!(
target: "TC Handler",
"app data for raw memory write is too short: {}",
app_data.len()
);
}
let memory_id = app_data[0];
if memory_id != BOOT_NVM_MEMORY_ID {
log::warn!(target: "TC Handler", "memory ID {} not supported", memory_id);
// TODO: Error reporting
return;
}
let offset = u32::from_be_bytes(app_data[2..6].try_into().unwrap());
let data_len = u32::from_be_bytes(app_data[6..10].try_into().unwrap());
if 10 + data_len as usize > app_data.len() {
log::warn!(
target: "TC Handler",
"invalid data length {} for raw mem write detected",
data_len
);
// TODO: Error reporting
return;
}
let data = &app_data[10..10 + data_len as usize];
log::info!(
target: "TC Handler",
"writing {} bytes at offset {} to NVM",
data_len,
offset
);
cx.local
.nvm
.write(offset as usize, data)
.expect("writing to NVM failed");
let tm = if !cx
.local
.nvm
.verify(offset as usize, data)
.expect("NVM verification failed")
{
log::warn!("verification of data written to NVM failed");
cx.local
.verif_reporter
.completion_failure(
cx.local.src_data_buf,
started_token,
0,
0,
FailParams::new(&[], &EcssEnumU8::new(0), &[]),
)
.expect("completion success failed")
} else {
cx.local
.verif_reporter
.completion_success(cx.local.src_data_buf, started_token, 0, 0, &[])
.expect("completion success failed")
};
write_and_send(&tm);
log::info!(
target: "TC Handler",
"NVM operation done");
}
}
}
#[task(
priority = 1,
local=[
read_buf: [u8;MAX_TM_SIZE] = [0; MAX_TM_SIZE],
encoded_buf: [u8;MAX_TM_FRAME_SIZE] = [0; MAX_TM_FRAME_SIZE],
uart_tx,
tm_cons
],
shared=[]
)]
async fn pus_tm_tx_handler(cx: pus_tm_tx_handler::Context) {
loop {
while cx.local.tm_cons.sizes_cons.occupied_len() > 0 {
let next_size = cx.local.tm_cons.sizes_cons.try_pop().unwrap();
cx.local
.tm_cons
.buf_cons
.pop_slice(&mut cx.local.read_buf[0..next_size]);
cx.local.encoded_buf[0] = 0;
let send_size = cobs::encode(
&cx.local.read_buf[0..next_size],
&mut cx.local.encoded_buf[1..],
);
cx.local.encoded_buf[send_size + 1] = 0;
cx.local
.uart_tx
.write(&cx.local.encoded_buf[0..send_size + 2])
.unwrap();
Mono::delay(2.millis()).await;
}
Mono::delay(50.millis()).await;
}
}
}

10
scripts/memory_app_a.x Normal file
View File

@ -0,0 +1,10 @@
MEMORY
{
FLASH : ORIGIN = 0x00003000, LENGTH = 0xE7F8 /* (128k - 12k) / 2 - 8 */
RAM : ORIGIN = 0x10000000, LENGTH = 0x08000 /* 32K */
}
/* This is where the call stack will be allocated. */
/* The stack is of the full descending type. */
/* NOTE Do NOT modify `_stack_start` unless you know what you are doing */
_stack_start = ORIGIN(RAM) + LENGTH(RAM);

10
scripts/memory_app_b.x Normal file
View File

@ -0,0 +1,10 @@
MEMORY
{
FLASH : ORIGIN = 0x00011800, LENGTH = 0xE7F8 /* (128k - 12k) / 2 - 8 */
RAM : ORIGIN = 0x10000000, LENGTH = 0x08000 /* 32K */
}
/* This is where the call stack will be allocated. */
/* The stack is of the full descending type. */
/* NOTE Do NOT modify `_stack_start` unless you know what you are doing */
_stack_start = ORIGIN(RAM) + LENGTH(RAM);

View File

@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Improve and fix SPI abstractions. Add new low level interface. The primary SPI constructor now
only expects a configuration structure and the transfer configuration needs to be applied in a
separate step.
- Removed complete `timer` module re-export in `pwm` module
- `CountDownTimer` renamed to `CountdownTimer`
## Fixes

View File

@ -9,8 +9,11 @@ use core::convert::Infallible;
use core::marker::PhantomData;
use crate::pac;
use crate::timer::{
TimAndPinRegister, TimDynRegister, TimPin, TimRegInterface, ValidTim, ValidTimAndPin,
};
use crate::{clock::enable_peripheral_clock, gpio::DynPinId};
pub use crate::{gpio::PinId, time::Hertz, timer::*};
pub use crate::{gpio::PinId, time::Hertz};
const DUTY_MAX: u16 = u16::MAX;

View File

@ -371,7 +371,7 @@ unsafe impl TimRegInterface for TimDynRegister {
//==================================================================================================
/// Hardware timers
pub struct CountDownTimer<TIM: ValidTim> {
pub struct CountdownTimer<TIM: ValidTim> {
tim: TimRegister<TIM>,
curr_freq: Hertz,
irq_cfg: Option<IrqCfg>,
@ -395,17 +395,17 @@ pub fn disable_tim_clk(syscfg: &mut pac::Sysconfig, idx: u8) {
.modify(|r, w| unsafe { w.bits(r.bits() & !(1 << idx)) });
}
unsafe impl<TIM: ValidTim> TimRegInterface for CountDownTimer<TIM> {
unsafe impl<TIM: ValidTim> TimRegInterface for CountdownTimer<TIM> {
fn tim_id(&self) -> u8 {
TIM::TIM_ID
}
}
impl<TIM: ValidTim> CountDownTimer<TIM> {
impl<TIM: ValidTim> CountdownTimer<TIM> {
/// Configures a TIM peripheral as a periodic count down timer
pub fn new(syscfg: &mut pac::Sysconfig, sys_clk: impl Into<Hertz>, tim: TIM) -> Self {
enable_tim_clk(syscfg, TIM::TIM_ID);
let cd_timer = CountDownTimer {
let cd_timer = CountdownTimer {
tim: unsafe { TimRegister::new(tim) },
sys_clk: sys_clk.into(),
irq_cfg: None,
@ -614,7 +614,7 @@ impl<TIM: ValidTim> CountDownTimer<TIM> {
}
/// CountDown implementation for TIMx
impl<TIM: ValidTim> CountDownTimer<TIM> {
impl<TIM: ValidTim> CountdownTimer<TIM> {
#[inline]
pub fn start<T>(&mut self, timeout: T)
where
@ -647,7 +647,7 @@ impl<TIM: ValidTim> CountDownTimer<TIM> {
}
}
impl<TIM: ValidTim> embedded_hal::delay::DelayNs for CountDownTimer<TIM> {
impl<TIM: ValidTim> embedded_hal::delay::DelayNs for CountdownTimer<TIM> {
fn delay_ns(&mut self, ns: u32) {
let ticks = (u64::from(ns)) * (u64::from(self.sys_clk.raw())) / 1_000_000_000;
@ -709,8 +709,8 @@ pub fn set_up_ms_tick<TIM: ValidTim>(
irq_sel: Option<&mut pac::Irqsel>,
sys_clk: impl Into<Hertz>,
tim0: TIM,
) -> CountDownTimer<TIM> {
let mut ms_timer = CountDownTimer::new(sys_cfg, sys_clk, tim0);
) -> CountdownTimer<TIM> {
let mut ms_timer = CountdownTimer::new(sys_cfg, sys_clk, tim0);
ms_timer.listen(timer::Event::TimeOut, irq_cfg, irq_sel, Some(sys_cfg));
ms_timer.start(1000.Hz());
ms_timer
@ -720,8 +720,8 @@ pub fn set_up_ms_delay_provider<TIM: ValidTim>(
sys_cfg: &mut pac::Sysconfig,
sys_clk: impl Into<Hertz>,
tim: TIM,
) -> CountDownTimer<TIM> {
let mut provider = CountDownTimer::new(sys_cfg, sys_clk, tim);
) -> CountdownTimer<TIM> {
let mut provider = CountdownTimer::new(sys_cfg, sys_clk, tim);
provider.start(1000.Hz());
provider
}
@ -745,10 +745,10 @@ pub fn get_ms_ticks() -> u32 {
// Delay implementations
//==================================================================================================
pub struct DelayMs(CountDownTimer<pac::Tim0>);
pub struct DelayMs(CountdownTimer<pac::Tim0>);
impl DelayMs {
pub fn new(timer: CountDownTimer<pac::Tim0>) -> Option<Self> {
pub fn new(timer: CountdownTimer<pac::Tim0>) -> Option<Self> {
if timer.curr_freq() != Hertz::from_raw(1000) || !timer.listening() {
return None;
}

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,7 @@ use max116xx_10bit::{AveragingConversions, AveragingResults};
use panic_rtt_target as _;
use rtt_target::{rprintln, rtt_init_print};
use va108xx_hal::spi::{OptionalHwCs, SpiClkConfig};
use va108xx_hal::timer::CountDownTimer;
use va108xx_hal::timer::CountdownTimer;
use va108xx_hal::{
gpio::PinsA,
pac::{self, interrupt},
@ -154,7 +154,7 @@ fn main() -> ! {
spi_cfg,
)
.downgrade();
let delay_provider = CountDownTimer::new(&mut dp.sysconfig, 50.MHz(), dp.tim1);
let delay_provider = CountdownTimer::new(&mut dp.sysconfig, 50.MHz(), dp.tim1);
let spi_with_hwcs = SpiWithHwCs::new(spi, pinsa.pa17.into_funsel_2(), delay_provider);
match EXAMPLE_MODE {
ExampleMode::NotUsingEoc => spi_example_externally_clocked(spi_with_hwcs, delay),
@ -162,7 +162,7 @@ fn main() -> ! {
spi_example_internally_clocked(spi_with_hwcs, delay, pinsa.pa14.into_floating_input());
}
ExampleMode::NotUsingEocWithDelay => {
let delay_us = CountDownTimer::new(&mut dp.sysconfig, 50.MHz(), dp.tim2);
let delay_us = CountdownTimer::new(&mut dp.sysconfig, 50.MHz(), dp.tim2);
spi_example_externally_clocked_with_delay(spi_with_hwcs, delay, delay_us);
}
}

View File

@ -1,13 +1,12 @@
//! Example application which interfaces with the boot EEPROM.
#![no_main]
#![no_std]
use cortex_m_rt::entry;
use embedded_hal::delay::DelayNs;
use panic_rtt_target as _;
use rtt_target::{rprintln, rtt_init_print};
use va108xx_hal::{pac, pwm::CountDownTimer, time::Hertz};
use vorago_reb1::m95m01::M95M01;
use va108xx_hal::{pac, time::Hertz, timer::CountdownTimer};
use vorago_reb1::m95m01::{M95M01, PAGE_SIZE};
const CLOCK_FREQ: Hertz = Hertz::from_raw(50_000_000);
@ -18,46 +17,39 @@ fn main() -> ! {
let mut dp = pac::Peripherals::take().unwrap();
let mut timer = CountDownTimer::new(&mut dp.sysconfig, CLOCK_FREQ, dp.tim0);
let mut timer = CountdownTimer::new(&mut dp.sysconfig, CLOCK_FREQ, dp.tim0);
let mut nvm = M95M01::new(&mut dp.sysconfig, CLOCK_FREQ, dp.spic);
let status_reg = nvm.read_status_reg().expect("reading status reg failed");
if status_reg.zero_segment() == 0b111 {
panic!("status register unexpected values");
}
let mut orig_content: [u8; 16] = [0; 16];
let mut read_buf: [u8; 16] = [0; 16];
let write_buf: [u8; 16] = [0; 16];
for (idx, val) in read_buf.iter_mut().enumerate() {
*val = idx as u8;
let mut orig_content: [u8; 512] = [0; 512];
let mut read_buf: [u8; 512] = [0; 512];
let mut write_buf: [u8; 512] = [0; 512];
for (idx, val) in write_buf.iter_mut().enumerate() {
*val = ((idx as u16) % (u8::MAX as u16 + 1)) as u8;
}
nvm.read(0x4000, &mut orig_content).unwrap();
nvm.read(0, &mut orig_content).unwrap();
// One byte write and read.
nvm.write(0x4000, &write_buf[0..1]).unwrap();
nvm.read(0x4000, &mut read_buf[0..1]).unwrap();
assert_eq!(write_buf[0], read_buf[0]);
read_buf.fill(0);
nvm.write_page(0, 0, &[1, 2, 3, 4]).unwrap();
nvm.read(0, &mut read_buf[0..4]).unwrap();
// Four bytes write and read.
nvm.write(0x4000, &write_buf[0..4]).unwrap();
nvm.read(0x4000, &mut read_buf[0..4]).unwrap();
assert_eq!(&read_buf[0..4], &write_buf[0..4]);
read_buf.fill(0);
// Full sixteen bytes
nvm.write(0x4000, &write_buf).unwrap();
nvm.read(0x4000, &mut read_buf).unwrap();
// Read the whole content. Write will internally be split across two page bounaries.
nvm.write(0, &write_buf).unwrap();
// Memory can be read in one go.
nvm.read(0, &mut read_buf).unwrap();
assert_eq!(&read_buf, &write_buf);
assert!(nvm.verify(0, &write_buf).unwrap());
read_buf.fill(0);
// 3 bytes
nvm.write(0x4000, &write_buf[0..3]).unwrap();
nvm.read(0x4000, &mut read_buf[0..3]).unwrap();
assert_eq!(&read_buf[0..3], &write_buf[0..3]);
// Write along page boundary
nvm.write(PAGE_SIZE - 2, &write_buf[0..8]).unwrap();
nvm.read(PAGE_SIZE - 2, &mut read_buf[0..8]).unwrap();
assert_eq!(&read_buf[0..8], &write_buf[0..8]);
assert!(nvm.verify(PAGE_SIZE - 2, &write_buf[0..8]).unwrap());
// Write back original content.
nvm.write(0x4000, &orig_content).unwrap();
nvm.write(0, &orig_content).unwrap();
loop {
timer.delay_ms(500);
}

View File

@ -10,6 +10,8 @@
use core::convert::Infallible;
use embedded_hal::spi::SpiBus;
pub const PAGE_SIZE: usize = 256;
bitfield::bitfield! {
pub struct StatusReg(u8);
impl Debug;
@ -41,7 +43,7 @@ use regs::*;
use va108xx_hal::{
pac,
prelude::*,
spi::{RomMiso, RomMosi, RomSck, Spi, SpiConfig, BMSTART_BMSTOP_MASK},
spi::{RomMiso, RomMosi, RomSck, Spi, SpiClkConfig, SpiConfig, BMSTART_BMSTOP_MASK},
};
pub type RomSpi = Spi<pac::Spic, (RomSck, RomMiso, RomMosi), u8>;
@ -53,6 +55,9 @@ pub struct M95M01 {
pub spi: RomSpi,
}
#[derive(Debug, PartialEq, Eq)]
pub struct PageBoundaryExceededError;
impl M95M01 {
pub fn new(syscfg: &mut pac::Sysconfig, sys_clk: impl Into<Hertz>, spi: pac::Spic) -> Self {
let spi = RomSpi::new(
@ -60,7 +65,7 @@ impl M95M01 {
sys_clk,
spi,
(RomSck, RomMiso, RomMosi),
SpiConfig::default(),
SpiConfig::default().clk_cfg(SpiClkConfig::new(2, 4)),
);
let mut spi_dev = Self { spi };
spi_dev.clear_block_protection().unwrap();
@ -105,7 +110,7 @@ impl M95M01 {
self.spi.write(&[WRSR, reg.0])
}
fn common_init_write_and_read(&mut self, address: u32, reg: u8) -> Result<(), Infallible> {
fn common_init_write_and_read(&mut self, address: usize, reg: u8) -> Result<(), Infallible> {
nb::block!(self.writes_are_done())?;
self.spi.flush()?;
if reg == WRITE {
@ -114,41 +119,78 @@ impl M95M01 {
} else {
self.spi.write_fifo_unchecked(READ as u32);
}
self.spi.write_fifo_unchecked((address >> 16) & 0xff);
self.spi.write_fifo_unchecked((address >> 8) & 0xff);
self.spi.write_fifo_unchecked(address & 0xff);
self.spi.write_fifo_unchecked((address as u32 >> 16) & 0xff);
self.spi
.write_fifo_unchecked((address as u32 & 0x00ff00) >> 8);
self.spi.write_fifo_unchecked(address as u32 & 0xff);
Ok(())
}
fn common_read(&mut self, address: u32) -> Result<(), Infallible> {
fn common_read(&mut self, address: usize) -> Result<(), Infallible> {
self.common_init_write_and_read(address, READ)?;
for _ in 0..4 {
// Pump the FIFO.
self.spi.write_fifo_unchecked(0);
// Ignore the first 4 bytes.
self.spi.read_fifo_unchecked();
nb::block!(self.spi.read_fifo())?;
}
Ok(())
}
pub fn write(&mut self, address: u32, data: &[u8]) -> Result<(), Infallible> {
self.common_init_write_and_read(address, WRITE)?;
pub fn write(&mut self, mut address: usize, mut data: &[u8]) -> Result<(), Infallible> {
// Loop until all data is written
while !data.is_empty() {
// Calculate the page and the offset within the page from the address
let page = address / PAGE_SIZE;
let offset = address % PAGE_SIZE;
// Calculate how much space is left in the current page
let space_left = PAGE_SIZE - offset;
// Determine how much data to write in the current page
let to_write = data.len().min(space_left);
// Write the current portion of the data
self.write_page(page, offset, &data[..to_write]).unwrap();
// Update the address and data for the next iteration
address += to_write;
data = &data[to_write..];
}
Ok(())
}
pub fn write_page(
&mut self,
page: usize,
offset: usize,
data: &[u8],
) -> Result<(), PageBoundaryExceededError> {
// Check that the total data to be written does not exceed the page boundary
if offset + data.len() > PAGE_SIZE {
return Err(PageBoundaryExceededError);
}
self.common_init_write_and_read(page * PAGE_SIZE + offset, WRITE)
.unwrap();
for val in data.iter().take(data.len() - 1) {
nb::block!(self.spi.write_fifo(*val as u32))?;
self.spi.read_fifo_unchecked();
nb::block!(self.spi.write_fifo(*val as u32)).unwrap();
nb::block!(self.spi.read_fifo()).unwrap();
}
nb::block!(self
.spi
.write_fifo(*data.last().unwrap() as u32 | BMSTART_BMSTOP_MASK))?;
self.spi.flush()?;
nb::block!(self.writes_are_done())?;
.write_fifo(*data.last().unwrap() as u32 | BMSTART_BMSTOP_MASK))
.unwrap();
self.spi.flush().unwrap();
nb::block!(self.writes_are_done()).unwrap();
Ok(())
}
pub fn read(&mut self, address: u32, buf: &mut [u8]) -> Result<(), Infallible> {
pub fn read(&mut self, address: usize, buf: &mut [u8]) -> Result<(), Infallible> {
self.common_read(address)?;
for val in buf.iter_mut() {
nb::block!(self.spi.write_fifo(0))?;
self.spi.write_fifo_unchecked(0);
*val = (nb::block!(self.spi.read_fifo()).unwrap() & 0xff) as u8;
}
nb::block!(self.spi.write_fifo(BMSTART_BMSTOP_MASK))?;
@ -156,10 +198,10 @@ impl M95M01 {
Ok(())
}
pub fn verify(&mut self, address: u32, data: &[u8]) -> Result<bool, Infallible> {
pub fn verify(&mut self, address: usize, data: &[u8]) -> Result<bool, Infallible> {
self.common_read(address)?;
for val in data.iter() {
nb::block!(self.spi.write_fifo(0))?;
self.spi.write_fifo_unchecked(0);
let read_val = (nb::block!(self.spi.read_fifo()).unwrap() & 0xff) as u8;
if read_val != *val {
return Ok(false);

View File

@ -222,6 +222,30 @@
]
}
},
{
"type": "cortex-debug",
"request": "launch",
"name": "UART Echo with RTIC",
"servertype": "jlink",
"cwd": "${workspaceRoot}",
"device": "Cortex-M0",
"svdFile": "./va108xx/svd/va108xx-base.svd.patched",
"preLaunchTask": "uart-echo-rtic-example",
"executable": "${workspaceFolder}/target/thumbv6m-none-eabi/debug/uart-echo-rtic",
"interface": "jtag",
"runToEntryPoint": "main",
"rttConfig": {
"enabled": true,
"address": "auto",
"decoders": [
{
"port": 0,
"timestamp": true,
"type": "console"
}
]
}
},
{
"type": "cortex-debug",
"request": "launch",
@ -303,7 +327,7 @@
"device": "Cortex-M0",
"svdFile": "./va108xx/svd/va108xx.svd.patched",
"preLaunchTask": "blinky-hal",
"executable": "${workspaceFolder}/target/thumbv6m-none-eabi/debug/examples/adxl343-accelerometer",
"executable": "${workspaceFolder}/target/thumbv6m-none-eabi/debug/examples/blinky",
"interface": "jtag",
"runToEntryPoint": "main",
},
@ -427,5 +451,53 @@
]
}
},
{
"type": "cortex-debug",
"request": "launch",
"name": "Bootloader",
"servertype": "jlink",
"cwd": "${workspaceRoot}",
"device": "Cortex-M0",
"svdFile": "./va108xx/svd/va108xx.svd.patched",
"preLaunchTask": "bootloader",
"executable": "${workspaceFolder}/target/thumbv6m-none-eabi/release/bootloader",
"interface": "jtag",
"runToEntryPoint": "main",
"rttConfig": {
"enabled": true,
"address": "auto",
"decoders": [
{
"port": 0,
"timestamp": true,
"type": "console"
}
]
}
},
{
"type": "cortex-debug",
"request": "launch",
"name": "Flashloader",
"servertype": "jlink",
"cwd": "${workspaceRoot}",
"device": "Cortex-M0",
"svdFile": "./va108xx/svd/va108xx.svd.patched",
"preLaunchTask": "flashloader",
"executable": "${workspaceFolder}/target/thumbv6m-none-eabi/release/flashloader",
"interface": "jtag",
"runToEntryPoint": "main",
"rttConfig": {
"enabled": true,
"address": "auto",
"decoders": [
{
"port": 0,
"timestamp": true,
"type": "console"
}
]
}
},
]
}

View File

@ -38,7 +38,7 @@
"args": [
"build",
"--example",
"rtt-log",
"rtt-log"
],
"group": {
"kind": "build",
@ -68,7 +68,7 @@
"args": [
"build",
"--example",
"uart",
"uart"
],
"group": {
"kind": "build",
@ -82,7 +82,7 @@
"args": [
"build",
"--example",
"spi",
"spi"
],
"group": {
"kind": "build",
@ -122,13 +122,13 @@
}
},
{
"label": "rust: cargo build uart irq",
"label": "uart-echo-rtic-example",
"type": "shell",
"command": "~/.cargo/bin/cargo", // note: full path to the cargo
"args": [
"build",
"--bin",
"uart-rtic",
"uart-echo-rtic"
],
"group": {
"kind": "build",
@ -142,10 +142,10 @@
"args": [
"build",
"--example",
"blinky",
"blinky"
],
"group": {
"kind": "build",
"kind": "build"
}
},
{
@ -155,7 +155,7 @@
"args": [
"build",
"--example",
"blinky-leds",
"blinky-leds"
],
"group": {
"kind": "build",
@ -169,7 +169,7 @@
"args": [
"build",
"--example",
"blinky-button-irq",
"blinky-button-irq"
],
"group": {
"kind": "build",
@ -183,7 +183,7 @@
"args": [
"build",
"--example",
"adt75-temp-sensor",
"adt75-temp-sensor"
],
"group": {
"kind": "build",
@ -197,7 +197,7 @@
"args": [
"build",
"--example",
"blinky-button-rtic",
"blinky-button-rtic"
],
"group": {
"kind": "build",
@ -225,7 +225,7 @@
"args": [
"build",
"--example",
"max11619-adc",
"max11619-adc"
],
"group": {
"kind": "build",
@ -239,7 +239,7 @@
"args": [
"build",
"--example",
"nvm",
"nvm"
],
"group": {
"kind": "build",
@ -253,8 +253,8 @@
"args": [
"build",
"--bin",
"rtic-example",
],
"rtic-example"
]
},
{
"label": "embassy-example",
@ -263,8 +263,30 @@
"args": [
"build",
"--bin",
"embassy-example",
],
"embassy-example"
]
},
{
"label": "bootloader",
"type": "shell",
"command": "~/.cargo/bin/cargo", // note: full path to the cargo
"args": [
"build",
"--bin",
"bootloader",
"--release",
]
},
{
"label": "flashloader",
"type": "shell",
"command": "~/.cargo/bin/cargo", // note: full path to the cargo
"args": [
"build",
"--bin",
"flashloader",
"--release"
]
}
]
}