diff --git a/Cargo.toml b/Cargo.toml index fd5b004..15a0ab4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/board-tests/src/main.rs b/board-tests/src/main.rs index 9ae3b9e..8c47f9c 100644 --- a/board-tests/src/main.rs +++ b/board-tests/src/main.rs @@ -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(); diff --git a/bootloader/Cargo.toml b/bootloader/Cargo.toml index 9bef200..a2e3242 100644 --- a/bootloader/Cargo.toml +++ b/bootloader/Cargo.toml @@ -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" diff --git a/bootloader/README.md b/bootloader/README.md new file mode 100644 index 0000000..dd89c67 --- /dev/null +++ b/bootloader/README.md @@ -0,0 +1,48 @@ +VA416xx 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 0x1DFFC (~120K) 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 0x1DFFC (~120K) 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. diff --git a/bootloader/src/lib.rs b/bootloader/src/lib.rs index 2fc897a..24e41a6 100644 --- a/bootloader/src/lib.rs +++ b/bootloader/src/lib.rs @@ -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; + 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; } diff --git a/bootloader/src/main.rs b/bootloader/src/main.rs index 06ab2b2..2ac00f7 100644 --- a/bootloader/src/main.rs +++ b/bootloader/src/main.rs @@ -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 { + fn verify(&mut self, address: usize, data: &[u8]) -> Result { 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, +) { 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, +) -> ! { 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(); + } } diff --git a/examples/rtic/Cargo.toml b/examples/rtic/Cargo.toml index be520eb..5e1dc90 100644 --- a/examples/rtic/Cargo.toml +++ b/examples/rtic/Cargo.toml @@ -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,20 @@ 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" +git = "https://github.com/us-irs/ringbuf.git" +branch = "use-portable-atomic-crate" +default-features = false + +[dependencies.va108xx-hal] +path = "../../va108xx-hal" + +[dependencies.vorago-reb1] +path = "../../vorago-reb1" diff --git a/examples/rtic/src/bin/uart-echo-rtic.rs b/examples/rtic/src/bin/uart-echo-rtic.rs new file mode 100644 index 0000000..7392470 --- /dev/null +++ b/examples/rtic/src/bin/uart-echo-rtic.rs @@ -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> = + Lazy::new(StaticRb::::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>, + rx: RxWithIrq, + tx: Tx, + } + + #[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; + } + } +} diff --git a/examples/rtic/src/bin/uart-rtic.rs b/examples/rtic/src/bin/uart-rtic.rs deleted file mode 100644 index 50eafaf..0000000 --- a/examples/rtic/src/bin/uart-rtic.rs +++ /dev/null @@ -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, - 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); - } - } - } - } -} diff --git a/examples/simple/examples/blinky.rs b/examples/simple/examples/blinky.rs index 59615c0..bee4a96 100644 --- a/examples/simple/examples/blinky.rs +++ b/examples/simple/examples/blinky.rs @@ -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(); diff --git a/examples/simple/examples/cascade.rs b/examples/simple/examples/cascade.rs index f29b56a..6d96240 100644 --- a/examples/simple/examples/cascade.rs +++ b/examples/simple/examples/cascade.rs @@ -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>>> = +static CSD_TGT_1: Mutex>>> = Mutex::new(RefCell::new(None)); -static CSD_TGT_2: Mutex>>> = +static CSD_TGT_2: Mutex>>> = 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)) diff --git a/examples/simple/examples/spi.rs b/examples/simple/examples/spi.rs index 13d6b6f..11be074 100644 --- a/examples/simple/examples/spi.rs +++ b/examples/simple/examples/spi.rs @@ -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, }; diff --git a/examples/simple/examples/timer-ticks.rs b/examples/simple/examples/timer-ticks.rs index 241e4ab..1c0cb84 100644 --- a/examples/simple/examples/timer-ticks.rs +++ b/examples/simple/examples/timer-ticks.rs @@ -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), diff --git a/flashloader/.gitignore b/flashloader/.gitignore new file mode 100644 index 0000000..f9606a3 --- /dev/null +++ b/flashloader/.gitignore @@ -0,0 +1 @@ +/venv diff --git a/flashloader/Cargo.toml b/flashloader/Cargo.toml new file mode 100644 index 0000000..4f6ce9a --- /dev/null +++ b/flashloader/Cargo.toml @@ -0,0 +1,68 @@ +[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.3" +git = "https://github.com/us-irs/rtt-log-rs.git" +branch = "allow-usage-on-non-cas-systems" + +[dependencies.ringbuf] +version = "0.4" +git = "https://github.com/us-irs/ringbuf.git" +branch = "use-portable-atomic-crate" +default-features = false + +[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" diff --git a/flashloader/README.md b/flashloader/README.md new file mode 100644 index 0000000..885ead1 --- /dev/null +++ b/flashloader/README.md @@ -0,0 +1,66 @@ +VA416xx 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 interface of the VA416xx board 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/thumbv7em-none-eabihf/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. diff --git a/flashloader/image-loader.py b/flashloader/image-loader.py new file mode 100755 index 0000000..ee3178d --- /dev/null +++ b/flashloader/image-loader.py @@ -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() diff --git a/flashloader/loader.toml b/flashloader/loader.toml new file mode 100644 index 0000000..bfcf1ac --- /dev/null +++ b/flashloader/loader.toml @@ -0,0 +1 @@ +serial_port = "/dev/ttyUSB0" diff --git a/flashloader/requirements.txt b/flashloader/requirements.txt new file mode 100644 index 0000000..5a2f67b --- /dev/null +++ b/flashloader/requirements.txt @@ -0,0 +1,5 @@ +spacepackets == 0.24 +tmtccmd == 8.0.2 +toml == 0.10 +pyelftools == 0.31 +crcmod == 1.7 diff --git a/flashloader/slot-a-blinky/.gitignore b/flashloader/slot-a-blinky/.gitignore new file mode 100644 index 0000000..7f153fc --- /dev/null +++ b/flashloader/slot-a-blinky/.gitignore @@ -0,0 +1,2 @@ +/target +/app.map diff --git a/flashloader/slot-a-blinky/Cargo.toml b/flashloader/slot-a-blinky/Cargo.toml new file mode 100644 index 0000000..701674f --- /dev/null +++ b/flashloader/slot-a-blinky/Cargo.toml @@ -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. diff --git a/flashloader/slot-a-blinky/memory.x b/flashloader/slot-a-blinky/memory.x new file mode 100644 index 0000000..f64b2c6 --- /dev/null +++ b/flashloader/slot-a-blinky/memory.x @@ -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); diff --git a/flashloader/slot-a-blinky/src/main.rs b/flashloader/slot-a-blinky/src/main.rs new file mode 100644 index 0000000..cd302e4 --- /dev/null +++ b/flashloader/slot-a-blinky/src/main.rs @@ -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); + } +} diff --git a/flashloader/slot-b-blinky/.gitignore b/flashloader/slot-b-blinky/.gitignore new file mode 100644 index 0000000..7f153fc --- /dev/null +++ b/flashloader/slot-b-blinky/.gitignore @@ -0,0 +1,2 @@ +/target +/app.map diff --git a/flashloader/slot-b-blinky/Cargo.toml b/flashloader/slot-b-blinky/Cargo.toml new file mode 100644 index 0000000..d636e98 --- /dev/null +++ b/flashloader/slot-b-blinky/Cargo.toml @@ -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. diff --git a/flashloader/slot-b-blinky/memory.x b/flashloader/slot-b-blinky/memory.x new file mode 100644 index 0000000..4f7b2cf --- /dev/null +++ b/flashloader/slot-b-blinky/memory.x @@ -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); diff --git a/flashloader/slot-b-blinky/src/main.rs b/flashloader/slot-b-blinky/src/main.rs new file mode 100644 index 0000000..edb2f7a --- /dev/null +++ b/flashloader/slot-b-blinky/src/main.rs @@ -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); + } +} diff --git a/flashloader/src/lib.rs b/flashloader/src/lib.rs new file mode 100644 index 0000000..5c9605a --- /dev/null +++ b/flashloader/src/lib.rs @@ -0,0 +1,9 @@ +#![no_std] + +#[cfg(test)] +mod tests { + #[test] + fn simple() { + assert_eq!(1 + 1, 2); + } +} diff --git a/flashloader/src/main.rs b/flashloader/src/main.rs new file mode 100644 index 0000000..eb6e356 --- /dev/null +++ b/flashloader/src/main.rs @@ -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> = + Lazy::new(StaticRb::::default); +static mut SIZES_RB_TM: Lazy> = + Lazy::new(StaticRb::::default); + +// Ring buffers to handling variable sized telecommands +static mut BUF_RB_TC: Lazy> = + Lazy::new(StaticRb::::default); +static mut SIZES_RB_TC: Lazy> = + Lazy::new(StaticRb::::default); + +pub struct DataProducer { + pub buf_prod: StaticProd<'static, u8, BUF_SIZE>, + pub sizes_prod: StaticProd<'static, usize, SIZES_LEN>, +} + +pub struct DataConsumer { + pub buf_cons: CachingCons<&'static StaticRb>, + pub sizes_cons: CachingCons<&'static StaticRb>, +} + +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, + uart_tx: uart::Tx, + rx_context: IrqContextTimeoutOrMaxSize, + // We handle all TM in one task. + tm_cons: DataConsumer, + // We consume all TC in one task. + tc_cons: DataConsumer, + // We produce all TC in one task. + tc_prod: DataProducer, + verif_reporter: VerificationReportCreator, + nvm: M95M01, + } + + #[shared] + struct Shared { + // Having this shared allows multiple tasks to generate telemetry. + tm_prod: DataProducer, + } + + 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; + } + } +} diff --git a/scripts/memory_app_a.x b/scripts/memory_app_a.x new file mode 100644 index 0000000..4d8921b --- /dev/null +++ b/scripts/memory_app_a.x @@ -0,0 +1,10 @@ +MEMORY +{ + FLASH : ORIGIN = 0x00003000, LENGTH = 0x20000 /* 128K */ + 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); diff --git a/scripts/memory_app_b.x b/scripts/memory_app_b.x new file mode 100644 index 0000000..c30732f --- /dev/null +++ b/scripts/memory_app_b.x @@ -0,0 +1,10 @@ +MEMORY +{ + FLASH : ORIGIN = 0x00011800, LENGTH = 0x20000 /* 128K */ + 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); diff --git a/va108xx-hal/CHANGELOG.md b/va108xx-hal/CHANGELOG.md index 135e3e4..90ba77d 100644 --- a/va108xx-hal/CHANGELOG.md +++ b/va108xx-hal/CHANGELOG.md @@ -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 diff --git a/va108xx-hal/src/pwm.rs b/va108xx-hal/src/pwm.rs index 01f3ffc..a0933f8 100644 --- a/va108xx-hal/src/pwm.rs +++ b/va108xx-hal/src/pwm.rs @@ -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; diff --git a/va108xx-hal/src/timer.rs b/va108xx-hal/src/timer.rs index 9c5bed8..79d4cbe 100644 --- a/va108xx-hal/src/timer.rs +++ b/va108xx-hal/src/timer.rs @@ -371,7 +371,7 @@ unsafe impl TimRegInterface for TimDynRegister { //================================================================================================== /// Hardware timers -pub struct CountDownTimer { +pub struct CountdownTimer { tim: TimRegister, curr_freq: Hertz, irq_cfg: Option, @@ -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 TimRegInterface for CountDownTimer { +unsafe impl TimRegInterface for CountdownTimer { fn tim_id(&self) -> u8 { TIM::TIM_ID } } -impl CountDownTimer { +impl CountdownTimer { /// Configures a TIM peripheral as a periodic count down timer pub fn new(syscfg: &mut pac::Sysconfig, sys_clk: impl Into, 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 CountDownTimer { } /// CountDown implementation for TIMx -impl CountDownTimer { +impl CountdownTimer { #[inline] pub fn start(&mut self, timeout: T) where @@ -647,7 +647,7 @@ impl CountDownTimer { } } -impl embedded_hal::delay::DelayNs for CountDownTimer { +impl embedded_hal::delay::DelayNs for CountdownTimer { 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( irq_sel: Option<&mut pac::Irqsel>, sys_clk: impl Into, tim0: TIM, -) -> CountDownTimer { - let mut ms_timer = CountDownTimer::new(sys_cfg, sys_clk, tim0); +) -> CountdownTimer { + 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( sys_cfg: &mut pac::Sysconfig, sys_clk: impl Into, tim: TIM, -) -> CountDownTimer { - let mut provider = CountDownTimer::new(sys_cfg, sys_clk, tim); +) -> CountdownTimer { + 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); +pub struct DelayMs(CountdownTimer); impl DelayMs { - pub fn new(timer: CountDownTimer) -> Option { + pub fn new(timer: CountdownTimer) -> Option { if timer.curr_freq() != Hertz::from_raw(1000) || !timer.listening() { return None; } diff --git a/va108xx-hal/src/uart.rs b/va108xx-hal/src/uart.rs index daabd22..623347b 100644 --- a/va108xx-hal/src/uart.rs +++ b/va108xx-hal/src/uart.rs @@ -4,13 +4,13 @@ //! //! - [UART simple example](https://egit.irs.uni-stuttgart.de/rust/va108xx-rs/src/branch/main/examples/simple/examples/uart.rs) //! - [UART with IRQ and RTIC](https://egit.irs.uni-stuttgart.de/rust/va108xx-rs/src/branch/va108xx-update-package/examples/rtic/src/bin/uart-rtic.rs) -use core::{marker::PhantomData, ops::Deref}; -use embedded_hal_nb::serial::Read; +use core::{convert::Infallible, ops::Deref}; use fugit::RateExtU32; +use va108xx::Uarta; pub use crate::IrqCfg; use crate::{ - clock::{enable_peripheral_clock, PeripheralClocks}, + clock::enable_peripheral_clock, enable_interrupt, gpio::pin::{ AltFunc1, AltFunc2, AltFunc3, Pin, PA16, PA17, PA18, PA19, PA2, PA26, PA27, PA3, PA30, @@ -20,6 +20,7 @@ use crate::{ time::Hertz, PeripheralSelect, }; +use embedded_hal_nb::serial::Read; //================================================================================================== // Type-Level support @@ -46,17 +47,28 @@ impl Pins for (Pin, Pin) {} // Regular Definitions //================================================================================================== -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum Error { +pub struct TransferPendingError; + +#[derive(Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum RxError { Overrun, - FramingError, - ParityError, + Framing, + Parity, +} +#[derive(Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Error { + Rx(RxError), BreakCondition, - TransferPending, - BufferTooShort, - /// Can be a combination of overrun, framing or parity error - IrqError, +} + +impl From for Error { + fn from(value: RxError) -> Self { + Self::Rx(value) + } } impl embedded_io::Error for Error { @@ -65,13 +77,32 @@ impl embedded_io::Error for Error { } } +impl embedded_io::Error for RxError { + fn kind(&self) -> embedded_io::ErrorKind { + embedded_io::ErrorKind::Other + } +} +impl embedded_hal_nb::serial::Error for RxError { + fn kind(&self) -> embedded_hal_nb::serial::ErrorKind { + match self { + RxError::Overrun => embedded_hal_nb::serial::ErrorKind::Overrun, + RxError::Framing => embedded_hal_nb::serial::ErrorKind::FrameFormat, + RxError::Parity => embedded_hal_nb::serial::ErrorKind::Parity, + } + } +} + impl embedded_hal_nb::serial::Error for Error { fn kind(&self) -> embedded_hal_nb::serial::ErrorKind { - embedded_hal_nb::serial::ErrorKind::Other + match self { + Error::Rx(rx_error) => embedded_hal_nb::serial::Error::kind(rx_error), + Error::BreakCondition => embedded_hal_nb::serial::ErrorKind::Other, + } } } #[derive(Debug, PartialEq, Eq, Copy, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum Event { // Receiver FIFO interrupt enable. Generates interrupt // when FIFO is at least half full. Half full is defined as FIFO @@ -95,20 +126,23 @@ pub enum Event { TxCts, } -#[derive(Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum Parity { None, Odd, Even, } -#[derive(Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum StopBits { One = 0, Two = 1, } -#[derive(Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum WordSize { Five = 0, Six = 1, @@ -116,6 +150,8 @@ pub enum WordSize { Eight = 3, } +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct Config { pub baudrate: Hertz, pub parity: Parity, @@ -189,174 +225,193 @@ impl From for Config { // IRQ Definitions //================================================================================================== -struct IrqInfo { - rx_len: usize, +#[derive(Debug, Copy, Clone)] +pub struct IrqContextTimeoutOrMaxSize { rx_idx: usize, - irq_cfg: IrqCfg, mode: IrqReceptionMode, + pub max_len: usize, +} + +impl IrqContextTimeoutOrMaxSize { + pub fn new(max_len: usize) -> Self { + IrqContextTimeoutOrMaxSize { + rx_idx: 0, + max_len, + mode: IrqReceptionMode::Idle, + } + } } -pub enum IrqResultMask { - Complete = 0, - Overflow = 1, - FramingError = 2, - ParityError = 3, - Break = 4, - Timeout = 5, - Addr9 = 6, - /// Should not happen - Unknown = 7, +impl IrqContextTimeoutOrMaxSize { + pub fn reset(&mut self) { + self.rx_idx = 0; + self.mode = IrqReceptionMode::Idle; + } } /// This struct is used to return the default IRQ handler result to the user #[derive(Debug, Default)] pub struct IrqResult { - raw_res: u32, pub bytes_read: usize, + pub errors: Option, } -impl IrqResult { - pub const fn new() -> Self { - IrqResult { - raw_res: 0, - bytes_read: 0, - } - } +/// This struct is used to return the default IRQ handler result to the user +#[derive(Debug, Default)] +pub struct IrqResultMaxSizeOrTimeout { + complete: bool, + timeout: bool, + pub errors: Option, + pub bytes_read: usize, } -impl IrqResult { - #[inline] - pub fn raw_result(&self) -> u32 { - self.raw_res - } - - #[inline] - pub(crate) fn clear_result(&mut self) { - self.raw_res = 0; - } - #[inline] - pub(crate) fn set_result(&mut self, flag: IrqResultMask) { - self.raw_res |= 1 << flag as u32; - } - - #[inline] - pub fn complete(&self) -> bool { - if ((self.raw_res >> IrqResultMask::Complete as u32) & 0x01) == 0x01 { - return true; +impl IrqResultMaxSizeOrTimeout { + pub fn new() -> Self { + IrqResultMaxSizeOrTimeout { + complete: false, + timeout: false, + errors: None, + bytes_read: 0, } - false } - +} +impl IrqResultMaxSizeOrTimeout { #[inline] - pub fn error(&self) -> bool { - if self.overflow_error() || self.framing_error() || self.parity_error() { - return true; - } - false + pub fn has_errors(&self) -> bool { + self.errors.is_some() } #[inline] pub fn overflow_error(&self) -> bool { - if ((self.raw_res >> IrqResultMask::Overflow as u32) & 0x01) == 0x01 { - return true; - } - false + self.errors.map_or(false, |e| e.overflow) } #[inline] pub fn framing_error(&self) -> bool { - if ((self.raw_res >> IrqResultMask::FramingError as u32) & 0x01) == 0x01 { - return true; - } - false + self.errors.map_or(false, |e| e.framing) } #[inline] pub fn parity_error(&self) -> bool { - if ((self.raw_res >> IrqResultMask::ParityError as u32) & 0x01) == 0x01 { - return true; - } - false + self.errors.map_or(false, |e| e.parity) } #[inline] pub fn timeout(&self) -> bool { - if ((self.raw_res >> IrqResultMask::Timeout as u32) & 0x01) == 0x01 { - return true; - } - false + self.timeout + } + + #[inline] + pub fn complete(&self) -> bool { + self.complete } } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Copy, Clone)] enum IrqReceptionMode { Idle, Pending, } -//================================================================================================== -// UART implementation -//================================================================================================== - -/// Type erased variant of a UART. Can be created with the [`Uart::downgrade`] function. -pub struct UartBase { - uart: Uart, - tx: Tx, - rx: Rx, -} -/// Serial abstraction. Entry point to create a new UART -pub struct Uart { - inner: UartBase, - pins: Pins, +#[derive(Default, Debug, Copy, Clone)] +pub struct IrqUartError { + overflow: bool, + framing: bool, + parity: bool, + other: bool, } -/// UART using the IRQ capabilities of the peripheral. Can be created with the -/// [`Uart::into_uart_with_irq`] function. -pub struct UartWithIrq { - irq_base: UartWithIrqBase, - pins: Pins, +impl IrqUartError { + #[inline(always)] + pub fn overflow(&self) -> bool { + self.overflow + } + + #[inline(always)] + pub fn framing(&self) -> bool { + self.framing + } + + #[inline(always)] + pub fn parity(&self) -> bool { + self.parity + } + + #[inline(always)] + pub fn other(&self) -> bool { + self.other + } } -/// Type-erased UART using the IRQ capabilities of the peripheral. Can be created with the -/// [`UartWithIrq::downgrade`] function. -pub struct UartWithIrqBase { - pub uart: UartBase, - irq_info: IrqInfo, +impl IrqUartError { + #[inline(always)] + pub fn error(&self) -> bool { + self.overflow || self.framing || self.parity + } } -/// Serial receiver -pub struct Rx { - _usart: PhantomData, +#[derive(Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct BufferTooShortError { + found: usize, + expected: usize, } -/// Serial transmitter -pub struct Tx { - _usart: PhantomData, +//================================================================================================== +// UART peripheral wrapper +//================================================================================================== + +pub trait Instance: Deref { + const IDX: u8; + const PERIPH_SEL: PeripheralSelect; + + /// Retrieve the peripheral structure. + /// + /// # Safety + /// + /// This circumvents the safety guarantees of the HAL. + unsafe fn steal() -> Self; + fn ptr() -> *const uart_base::RegisterBlock; } -impl Rx { - fn new() -> Self { - Self { - _usart: PhantomData, - } +impl Instance for pac::Uarta { + const IDX: u8 = 0; + + const PERIPH_SEL: PeripheralSelect = PeripheralSelect::Uart0; + + unsafe fn steal() -> Self { + pac::Peripherals::steal().uarta + } + fn ptr() -> *const uart_base::RegisterBlock { + Uarta::ptr() as *const _ } } -impl Tx { - fn new() -> Self { - Self { - _usart: PhantomData, - } +impl Instance for pac::Uartb { + const IDX: u8 = 1; + + const PERIPH_SEL: PeripheralSelect = PeripheralSelect::Uart1; + + unsafe fn steal() -> Self { + pac::Peripherals::steal().uartb + } + fn ptr() -> *const uart_base::RegisterBlock { + Uarta::ptr() as *const _ } } -pub trait Instance: Deref { - fn ptr() -> *const uart_base::RegisterBlock; - const IDX: u8; - const PERIPH_SEL: PeripheralSelect; +//================================================================================================== +// UART implementation +//================================================================================================== + +/// Type erased variant of a UART. Can be created with the [`Uart::downgrade`] function. +pub struct UartBase { + uart: Uart, + tx: Tx, + rx: Rx, } -impl UartBase { +impl UartBase { /// This function assumes that the peripheral clock was alredy enabled /// in the SYSCONFIG register fn init(self, config: Config, sys_clk: Hertz) -> Self { @@ -473,7 +528,7 @@ impl UartBase { }); } - pub fn release(self) -> UART { + pub fn release(self) -> Uart { // Clear the FIFO self.uart.fifo_clr().write(|w| { w.rxfifo().set_bit(); @@ -486,11 +541,51 @@ impl UartBase { self.uart } - pub fn split(self) -> (Tx, Rx) { + pub fn split(self) -> (Tx, Rx) { (self.tx, self.rx) } } +impl embedded_io::ErrorType for UartBase { + type Error = Error; +} + +impl embedded_hal_nb::serial::ErrorType for UartBase { + type Error = Error; +} + +impl embedded_hal_nb::serial::Read for UartBase { + fn read(&mut self) -> nb::Result { + self.rx.read().map_err(|e| e.map(Error::Rx)) + } +} + +impl embedded_hal_nb::serial::Write for UartBase { + fn write(&mut self, word: u8) -> nb::Result<(), Self::Error> { + self.tx.write(word).map_err(|e| { + if let nb::Error::Other(_) = e { + unreachable!() + } + nb::Error::WouldBlock + }) + } + + fn flush(&mut self) -> nb::Result<(), Self::Error> { + self.tx.flush().map_err(|e| { + if let nb::Error::Other(_) = e { + unreachable!() + } + nb::Error::WouldBlock + }) + } +} + +/// Serial abstraction. Entry point to create a new UART +pub struct Uart { + inner: UartBase, + pins: Pins, +} + impl Uart where UartInstance: Instance, @@ -507,8 +602,8 @@ where Uart { inner: UartBase { uart, - tx: Tx::new(), - rx: Rx::new(), + tx: Tx::new(unsafe { UartInstance::steal() }), + rx: Rx::new(unsafe { UartInstance::steal() }), }, pins, } @@ -522,30 +617,6 @@ where self } - /// If the IRQ capabilities of the peripheral are used, the UART needs to be converted - /// with this function - pub fn into_uart_with_irq( - self, - irq_cfg: IrqCfg, - sys_cfg: Option<&mut pac::Sysconfig>, - irq_sel: Option<&mut pac::Irqsel>, - ) -> UartWithIrq { - let (uart, pins) = self.downgrade_internal(); - UartWithIrq { - pins, - irq_base: UartWithIrqBase { - uart, - irq_info: IrqInfo { - rx_len: 0, - rx_idx: 0, - irq_cfg, - mode: IrqReceptionMode::Idle, - }, - } - .init(sys_cfg, irq_sel), - } - } - #[inline] pub fn enable_rx(&mut self) { self.inner.enable_rx(); @@ -598,15 +669,6 @@ where (self.inner.release(), self.pins) } - fn downgrade_internal(self) -> (UartBase, PinsInstance) { - let base = UartBase { - uart: self.inner.uart, - tx: self.inner.tx, - rx: self.inner.rx, - }; - (base, self.pins) - } - pub fn downgrade(self) -> UartBase { UartBase { uart: self.inner.uart, @@ -620,106 +682,650 @@ where } } -impl Instance for pac::Uarta { - fn ptr() -> *const uart_base::RegisterBlock { - pac::Uarta::ptr() as *const _ - } - const IDX: u8 = 0; - - const PERIPH_SEL: PeripheralSelect = PeripheralSelect::Uart0; -} - -impl Instance for pac::Uartb { - fn ptr() -> *const uart_base::RegisterBlock { - pac::Uartb::ptr() as *const _ - } - const IDX: u8 = 1; +/// Serial receiver. +/// +/// Can be created by using the [Uart::split] or [UartBase::split] API. +pub struct Rx(Uart); - const PERIPH_SEL: PeripheralSelect = PeripheralSelect::Uart1; -} - -impl UartWithIrqBase { - fn init(self, sys_cfg: Option<&mut pac::Sysconfig>, irq_sel: Option<&mut pac::Irqsel>) -> Self { - if let Some(sys_cfg) = sys_cfg { - enable_peripheral_clock(sys_cfg, PeripheralClocks::Irqsel) - } - if let Some(irq_sel) = irq_sel { - if self.irq_info.irq_cfg.route { - irq_sel - .uart0(Uart::IDX as usize) - .write(|w| unsafe { w.bits(self.irq_info.irq_cfg.irq as u32) }); - } - } - self +impl Rx { + fn new(uart: Uart) -> Self { + Self(uart) } - /// This initializes a non-blocking read transfer using the IRQ capabilities of the UART - /// peripheral. + /// Direct access to the peripheral structure. /// - /// The only required information is the maximum length for variable sized reception - /// or the expected length for fixed length reception. If variable sized packets are expected, - /// the timeout functionality of the IRQ should be enabled as well. After calling this function, - /// the [`irq_handler`](Self::irq_handler) function should be called in the user interrupt - /// handler to read the received packets and reinitiate another transfer if desired. - pub fn read_fixed_len_using_irq( - &mut self, - max_len: usize, - enb_timeout_irq: bool, - ) -> Result<(), Error> { - if self.irq_info.mode != IrqReceptionMode::Idle { - return Err(Error::TransferPending); - } - self.irq_info.mode = IrqReceptionMode::Pending; - self.irq_info.rx_idx = 0; - self.irq_info.rx_len = max_len; - self.uart.enable_rx(); - self.uart.enable_tx(); - self.enable_rx_irq_sources(enb_timeout_irq); - if self.irq_info.irq_cfg.enable { - unsafe { - enable_interrupt(self.irq_info.irq_cfg.irq); - } - } - Ok(()) + /// # Safety + /// + /// You must ensure that only registers related to the operation of the RX side are used. + pub unsafe fn uart(&self) -> &Uart { + &self.0 } #[inline] - fn enable_rx_irq_sources(&mut self, timeout: bool) { - self.uart.uart.irq_enb().modify(|_, w| { - if timeout { - w.irq_rx_to().set_bit(); - } - w.irq_rx_status().set_bit(); - w.irq_rx().set_bit() - }); + pub fn clear_fifo(&self) { + self.0.fifo_clr().write(|w| w.rxfifo().set_bit()); } #[inline] - fn disable_rx_irq_sources(&mut self) { - self.uart.uart.irq_enb().modify(|_, w| { - w.irq_rx_to().clear_bit(); - w.irq_rx_status().clear_bit(); - w.irq_rx().clear_bit() - }); + pub fn enable(&mut self) { + self.0.enable().modify(|_, w| w.rxenable().set_bit()); } #[inline] - pub fn enable_tx(&mut self) { - self.uart.enable_tx() + pub fn disable(&mut self) { + self.0.enable().modify(|_, w| w.rxenable().clear_bit()); } - #[inline] - pub fn disable_tx(&mut self) { - self.uart.disable_tx() + /// Low level function to read a word from the UART FIFO. + /// + /// Uses the [nb] API to allow usage in blocking and non-blocking contexts. + /// + /// Please note that you might have to mask the returned value with 0xff to retrieve the actual + /// value if you use the manual parity mode. See chapter 4.6.2 for more information. + #[inline(always)] + pub fn read_fifo(&self) -> nb::Result { + if self.0.rxstatus().read().rdavl().bit_is_clear() { + return Err(nb::Error::WouldBlock); + } + Ok(self.read_fifo_unchecked()) } - pub fn cancel_transfer(&mut self) { - // Disable IRQ - cortex_m::peripheral::NVIC::mask(self.irq_info.irq_cfg.irq); - self.disable_rx_irq_sources(); - self.uart.clear_tx_fifo(); - self.irq_info.rx_idx = 0; - self.irq_info.rx_len = 0; + /// Low level function to read a word from from the UART FIFO. + /// + /// This does not necesarily mean there is a word in the FIFO available. + /// Use the [Self::read_fifo] function to read a word from the FIFO reliably using the [nb] + /// API. + /// + /// Please note that you might have to mask the returned value with 0xff to retrieve the actual + /// value if you use the manual parity mode. See chapter 4.6.2 for more information. + #[inline(always)] + pub fn read_fifo_unchecked(&self) -> u32 { + self.0.data().read().bits() + } + + pub fn into_rx_with_irq( + self, + sysconfig: &mut pac::Sysconfig, + irqsel: &mut pac::Irqsel, + interrupt: pac::Interrupt, + ) -> RxWithIrq { + RxWithIrq::new(self, sysconfig, irqsel, interrupt) + } + + pub fn release(self) -> Uart { + self.0 + } +} + +impl embedded_io::ErrorType for Rx { + type Error = RxError; +} + +impl embedded_hal_nb::serial::ErrorType for Rx { + type Error = RxError; +} + +impl embedded_hal_nb::serial::Read for Rx { + fn read(&mut self) -> nb::Result { + let uart = unsafe { &(*Uart::ptr()) }; + let status_reader = uart.rxstatus().read(); + let err = if status_reader.rxovr().bit_is_set() { + Some(RxError::Overrun) + } else if status_reader.rxfrm().bit_is_set() { + Some(RxError::Framing) + } else if status_reader.rxpar().bit_is_set() { + Some(RxError::Parity) + } else { + None + }; + if let Some(err) = err { + // The status code is always related to the next bit for the framing + // and parity status bits. We have to read the DATA register + // so that the next status reflects the next DATA word + // For overrun error, we read as well to clear the peripheral + self.read_fifo_unchecked(); + return Err(err.into()); + } + self.read_fifo().map(|val| (val & 0xff) as u8).map_err(|e| { + if let nb::Error::Other(_) = e { + unreachable!() + } + nb::Error::WouldBlock + }) + } +} + +impl embedded_io::Read for Rx { + fn read(&mut self, buf: &mut [u8]) -> Result { + if buf.is_empty() { + return Ok(0); + } + + for byte in buf.iter_mut() { + let w = nb::block!(>::read(self))?; + *byte = w; + } + + Ok(buf.len()) + } +} + +/// Serial transmitter +/// +/// Can be created by using the [Uart::split] or [UartBase::split] API. +pub struct Tx(Uart); + +impl Tx { + fn new(uart: Uart) -> Self { + Self(uart) + } + + /// Direct access to the peripheral structure. + /// + /// # Safety + /// + /// You must ensure that only registers related to the operation of the TX side are used. + pub unsafe fn uart(&self) -> &Uart { + &self.0 + } + + #[inline] + pub fn clear_fifo(&self) { + self.0.fifo_clr().write(|w| w.txfifo().set_bit()); + } + + #[inline] + pub fn enable(&mut self) { + self.0.enable().modify(|_, w| w.txenable().set_bit()); + } + + #[inline] + pub fn disable(&mut self) { + self.0.enable().modify(|_, w| w.txenable().clear_bit()); + } + + /// Low level function to write a word to the UART FIFO. + /// + /// Uses the [nb] API to allow usage in blocking and non-blocking contexts. + /// + /// Please note that you might have to mask the returned value with 0xff to retrieve the actual + /// value if you use the manual parity mode. See chapter 11.4.1 for more information. + #[inline(always)] + pub fn write_fifo(&self, data: u32) -> nb::Result<(), Infallible> { + if self.0.txstatus().read().wrrdy().bit_is_clear() { + return Err(nb::Error::WouldBlock); + } + self.write_fifo_unchecked(data); + Ok(()) + } + + /// Low level function to write a word to the UART FIFO. + /// + /// This does not necesarily mean that the FIFO can process another word because it might be + /// full. + /// Use the [Self::read_fifo] function to write a word to the FIFO reliably using the [nb] + /// API. + #[inline(always)] + pub fn write_fifo_unchecked(&self, data: u32) { + self.0.data().write(|w| unsafe { w.bits(data) }); + } +} + +impl embedded_io::ErrorType for Tx { + type Error = Infallible; +} + +impl embedded_hal_nb::serial::ErrorType for Tx { + type Error = Infallible; +} + +impl embedded_hal_nb::serial::Write for Tx { + fn write(&mut self, word: u8) -> nb::Result<(), Self::Error> { + self.write_fifo(word as u32) + } + + fn flush(&mut self) -> nb::Result<(), Self::Error> { + // SAFETY: Only TX related registers are used. + let reader = unsafe { &(*Uart::ptr()) }.txstatus().read(); + if reader.wrbusy().bit_is_set() { + return Err(nb::Error::WouldBlock); + } + Ok(()) + } +} + +impl embedded_io::Write for Tx { + fn write(&mut self, buf: &[u8]) -> Result { + if buf.is_empty() { + return Ok(0); + } + + for byte in buf.iter() { + nb::block!(>::write( + self, *byte + ))?; + } + + Ok(buf.len()) + } + + fn flush(&mut self) -> Result<(), Self::Error> { + nb::block!(>::flush(self)) + } +} + +/// Serial receiver, using interrupts to offload reading to the hardware. +/// +/// You can use [Rx::to_rx_with_irq] to convert a normal [Rx] structure into this structure. +/// This structure provides two distinct ways to read the UART RX using interrupts. It should +/// be noted that the interrupt service routine (ISR) still has to be provided by the user. However, +/// this structure provides API calls which can be used inside the ISRs to simplify the reading +/// of the UART. +/// +/// 1. The first way simply empties the FIFO on an interrupt into a user provided buffer. You +/// can simply use [Self::start] to prepare the peripheral and then call the +/// [Self::irq_handler] in the interrupt service routine. +/// 2. The second way reads packets bounded by a maximum size or a baudtick based timeout. You +/// can use [Self::read_fixed_len_or_timeout_based_using_irq] to prepare the peripheral and +/// then call the [Self::irq_handler_max_size_or_timeout_based] in the interrupt service +/// routine. You have to call [Self::read_fixed_len_or_timeout_based_using_irq] in the ISR to +/// start reading the next packet. +pub struct RxWithIrq { + pub rx: Rx, + pub interrupt: pac::Interrupt, +} + +impl RxWithIrq { + pub fn new( + rx: Rx, + syscfg: &mut pac::Sysconfig, + irqsel: &mut pac::Irqsel, + interrupt: pac::Interrupt, + ) -> Self { + enable_peripheral_clock(syscfg, PeripheralSelect::Irqsel); + irqsel + .uart0(Uart::IDX as usize) + .write(|w| unsafe { w.bits(interrupt as u32) }); + Self { rx, interrupt } + } + + /// This function should be called once at initialization time if the regular + /// [Self::irq_handler] is used to read the UART receiver to enable and start the receiver. + pub fn start(&mut self) { + self.rx.enable(); + self.enable_rx_irq_sources(true); + unsafe { enable_interrupt(self.interrupt) }; + } + + #[inline(always)] + pub fn uart(&self) -> &Uart { + &self.rx.0 + } + + /// This function is used together with the [Self::irq_handler_max_size_or_timeout_based] + /// function to read packets with a maximum size or variable sized packets by using the + /// receive timeout of the hardware. + /// + /// This function should be called once at initialization to initiate the context state + /// and to [Self::start] the receiver. After that, it should be called after each + /// completed [Self::irq_handler_max_size_or_timeout_based] call to restart the reception + /// of a packet. + pub fn read_fixed_len_or_timeout_based_using_irq( + &mut self, + context: &mut IrqContextTimeoutOrMaxSize, + ) -> Result<(), TransferPendingError> { + if context.mode != IrqReceptionMode::Idle { + return Err(TransferPendingError); + } + context.mode = IrqReceptionMode::Pending; + context.rx_idx = 0; + self.start(); + Ok(()) + } + + #[inline] + fn enable_rx_irq_sources(&mut self, timeout: bool) { + self.uart().irq_enb().modify(|_, w| { + if timeout { + w.irq_rx_to().set_bit(); + } + w.irq_rx_status().set_bit(); + w.irq_rx().set_bit() + }); + } + + #[inline] + fn disable_rx_irq_sources(&mut self) { + self.uart().irq_enb().modify(|_, w| { + w.irq_rx_to().clear_bit(); + w.irq_rx_status().clear_bit(); + w.irq_rx().clear_bit() + }); + } + + pub fn cancel_transfer(&mut self) { + self.disable_rx_irq_sources(); + self.rx.clear_fifo(); + } + + /// This function should be called in the user provided UART interrupt handler. + /// + /// It simply empties any bytes in the FIFO into the user provided buffer and returns the + /// result of the operation. + /// + /// This function will not disable the RX interrupts, so you don't need to call any other + /// API after calling this function to continue emptying the FIFO. RX errors are handled + /// as partial errors and are returned as part of the [IrqResult]. + pub fn irq_handler(&mut self, buf: &mut [u8; 16]) -> IrqResult { + let mut result = IrqResult::default(); + + let irq_end = self.uart().irq_end().read(); + let enb_status = self.uart().enable().read(); + let rx_enabled = enb_status.rxenable().bit_is_set(); + + // Half-Full interrupt. We have a guaranteed amount of data we can read. + if irq_end.irq_rx().bit_is_set() { + let available_bytes = self.uart().rxfifoirqtrg().read().bits() as usize; + + // If this interrupt bit is set, the trigger level is available at the very least. + // Read everything as fast as possible + for _ in 0..available_bytes { + buf[result.bytes_read] = (self.uart().data().read().bits() & 0xff) as u8; + result.bytes_read += 1; + } + } + + // Timeout, empty the FIFO completely. + if irq_end.irq_rx_to().bit_is_set() { + loop { + // While there is data in the FIFO, write it into the reception buffer + let read_result = self.rx.read(); + if let Some(byte) = self.read_handler(&mut result.errors, &read_result) { + buf[result.bytes_read] = byte; + result.bytes_read += 1; + } else { + break; + } + } + } + + // RX transfer not complete, check for RX errors + if rx_enabled { + self.check_for_errors(&mut result.errors); + } + + // Clear the interrupt status bits + self.uart() + .irq_clr() + .write(|w| unsafe { w.bits(irq_end.bits()) }); + result + } + + /// This function should be called in the user provided UART interrupt handler. + /// + /// This function is used to read packets which either have a maximum size or variable sized + /// packet which are bounded by sufficient delays between them, triggering a hardware timeout. + /// + /// If either the maximum number of packets have been read or a timeout occured, the transfer + /// will be deemed completed. The state information of the transfer is tracked in the + /// [IrqContextTimeoutOrMaxSize] structure. + /// + /// If passed buffer is equal to or larger than the specified maximum length, an + /// [BufferTooShortError] will be returned. Other RX errors are treated as partial errors + /// and returned inside the [IrqResultMaxSizeOrTimeout] structure. + pub fn irq_handler_max_size_or_timeout_based( + &mut self, + context: &mut IrqContextTimeoutOrMaxSize, + buf: &mut [u8], + ) -> Result { + if buf.len() < context.max_len { + return Err(BufferTooShortError { + found: buf.len(), + expected: context.max_len, + }); + } + let mut result = IrqResultMaxSizeOrTimeout::default(); + + let irq_end = self.uart().irq_end().read(); + let enb_status = self.uart().enable().read(); + let rx_enabled = enb_status.rxenable().bit_is_set(); + + // Half-Full interrupt. We have a guaranteed amount of data we can read. + if irq_end.irq_rx().bit_is_set() { + // Determine the number of bytes to read, ensuring we leave 1 byte in the FIFO. + // We use this trick/hack because the timeout feature of the peripheral relies on data + // being in the RX FIFO. If data continues arriving, another half-full IRQ will fire. + // If not, the last byte(s) is/are emptied by the timeout interrupt. + let available_bytes = self.uart().rxfifoirqtrg().read().bits() as usize; + + let bytes_to_read = core::cmp::min( + available_bytes.saturating_sub(1), + context.max_len - context.rx_idx, + ); + + // If this interrupt bit is set, the trigger level is available at the very least. + // Read everything as fast as possible + for _ in 0..bytes_to_read { + buf[context.rx_idx] = (self.uart().data().read().bits() & 0xff) as u8; + context.rx_idx += 1; + } + + // On high-baudrates, data might be available immediately, and we possible have to + // read continuosly? Then again, the CPU should always be faster than that. I'd rather + // rely on the hardware firing another IRQ. I have not tried baudrates higher than + // 115200 so far. + } + // Timeout, empty the FIFO completely. + if irq_end.irq_rx_to().bit_is_set() { + // While there is data in the FIFO, write it into the reception buffer + loop { + if context.rx_idx == context.max_len { + break; + } + let read_result = self.rx.read(); + if let Some(byte) = self.read_handler(&mut result.errors, &read_result) { + buf[context.rx_idx] = byte; + context.rx_idx += 1; + } else { + break; + } + } + self.irq_completion_handler_max_size_timeout(&mut result, context); + return Ok(result); + } + + // RX transfer not complete, check for RX errors + if (context.rx_idx < context.max_len) && rx_enabled { + self.check_for_errors(&mut result.errors); + } + + // Clear the interrupt status bits + self.uart() + .irq_clr() + .write(|w| unsafe { w.bits(irq_end.bits()) }); + Ok(result) + } + + fn read_handler( + &self, + errors: &mut Option, + read_res: &nb::Result, + ) -> Option { + match read_res { + Ok(byte) => Some(*byte), + Err(nb::Error::WouldBlock) => None, + Err(nb::Error::Other(e)) => { + // Ensure `errors` is Some(IrqUartError), initializing if it's None + let err = errors.get_or_insert(IrqUartError::default()); + + // Now we can safely modify fields inside `err` + match e { + RxError::Overrun => err.overflow = true, + RxError::Framing => err.framing = true, + RxError::Parity => err.parity = true, + } + None + } + } + } + + fn check_for_errors(&self, errors: &mut Option) { + let rx_status = self.uart().rxstatus().read(); + + if rx_status.rxovr().bit_is_set() + || rx_status.rxfrm().bit_is_set() + || rx_status.rxpar().bit_is_set() + { + let err = errors.get_or_insert(IrqUartError::default()); + + if rx_status.rxovr().bit_is_set() { + err.overflow = true; + } + if rx_status.rxfrm().bit_is_set() { + err.framing = true; + } + if rx_status.rxpar().bit_is_set() { + err.parity = true; + } + } + } + + fn irq_completion_handler_max_size_timeout( + &mut self, + res: &mut IrqResultMaxSizeOrTimeout, + context: &mut IrqContextTimeoutOrMaxSize, + ) { + self.disable_rx_irq_sources(); + self.rx.disable(); + res.bytes_read = context.rx_idx; + res.complete = true; + context.mode = IrqReceptionMode::Idle; + context.rx_idx = 0; + } + + /// # Safety + /// + /// This API allows creating multiple UART instances when releasing the TX structure as well. + /// The user must ensure that these instances are not used to create multiple overlapping + /// UART drivers. + pub unsafe fn release(self) -> Uart { + self.rx.release() + } +} + +/* + +impl UartWithIrq { + /// See [`UartWithIrqBase::read_fixed_len_using_irq`] doc + pub fn read_fixed_len_using_irq( + &mut self, + max_len: usize, + enb_timeout_irq: bool, + ) -> Result<(), Error> { + self.irq_base + .read_fixed_len_using_irq(max_len, enb_timeout_irq) + } + + pub fn cancel_transfer(&mut self) { + self.irq_base.cancel_transfer() + } + + /// See [`UartWithIrqBase::irq_handler`] doc + pub fn irq_handler(&mut self, res: &mut IrqResult, buf: &mut [u8]) -> Result<(), Error> { + self.irq_base.irq_handler(res, buf) + } + + pub fn release(self) -> (UART, PINS) { + (self.irq_base.release(), self.pins) + } + + pub fn downgrade(self) -> (UartWithIrqBase, PINS) { + (self.irq_base, self.pins) + } +} + +impl UartWithIrqBase { + fn init(self, sys_cfg: Option<&mut pac::Sysconfig>, irq_sel: Option<&mut pac::Irqsel>) -> Self { + if let Some(sys_cfg) = sys_cfg { + enable_peripheral_clock(sys_cfg, PeripheralClocks::Irqsel) + } + if let Some(irq_sel) = irq_sel { + if self.irq_info.irq_cfg.route { + irq_sel + .uart0(Uart::IDX as usize) + .write(|w| unsafe { w.bits(self.irq_info.irq_cfg.irq as u32) }); + } + } + self + } + + /// This initializes a non-blocking read transfer using the IRQ capabilities of the UART + /// peripheral. + /// + /// The only required information is the maximum length for variable sized reception + /// or the expected length for fixed length reception. If variable sized packets are expected, + /// the timeout functionality of the IRQ should be enabled as well. After calling this function, + /// the [`irq_handler`](Self::irq_handler) function should be called in the user interrupt + /// handler to read the received packets and reinitiate another transfer if desired. + pub fn read_fixed_len_using_irq( + &mut self, + max_len: usize, + enb_timeout_irq: bool, + ) -> Result<(), Error> { + if self.irq_info.mode != IrqReceptionMode::Idle { + return Err(Error::TransferPending); + } + self.irq_info.mode = IrqReceptionMode::Pending; + self.irq_info.rx_idx = 0; + self.irq_info.rx_len = max_len; + self.uart.enable_rx(); + self.uart.enable_tx(); + self.enable_rx_irq_sources(enb_timeout_irq); + if self.irq_info.irq_cfg.enable { + unsafe { + enable_interrupt(self.irq_info.irq_cfg.irq); + } + } + Ok(()) + } + + #[inline] + fn enable_rx_irq_sources(&mut self, timeout: bool) { + self.uart.uart.irq_enb().modify(|_, w| { + if timeout { + w.irq_rx_to().set_bit(); + } + w.irq_rx_status().set_bit(); + w.irq_rx().set_bit() + }); + } + + #[inline] + fn disable_rx_irq_sources(&mut self) { + self.uart.uart.irq_enb().modify(|_, w| { + w.irq_rx_to().clear_bit(); + w.irq_rx_status().clear_bit(); + w.irq_rx().clear_bit() + }); + } + + #[inline] + pub fn enable_tx(&mut self) { + self.uart.enable_tx() + } + + #[inline] + pub fn disable_tx(&mut self) { + self.uart.disable_tx() + } + + pub fn cancel_transfer(&mut self) { + // Disable IRQ + cortex_m::peripheral::NVIC::mask(self.irq_info.irq_cfg.irq); + self.disable_rx_irq_sources(); + self.uart.clear_tx_fifo(); + self.irq_info.rx_idx = 0; + self.irq_info.rx_len = 0; } /// Default IRQ handler which can be used to read the packets arriving on the UART peripheral. @@ -845,180 +1451,4 @@ impl UartWithIrqBase { self.uart.release() } } - -impl UartWithIrq { - /// See [`UartWithIrqBase::read_fixed_len_using_irq`] doc - pub fn read_fixed_len_using_irq( - &mut self, - max_len: usize, - enb_timeout_irq: bool, - ) -> Result<(), Error> { - self.irq_base - .read_fixed_len_using_irq(max_len, enb_timeout_irq) - } - - pub fn cancel_transfer(&mut self) { - self.irq_base.cancel_transfer() - } - - /// See [`UartWithIrqBase::irq_handler`] doc - pub fn irq_handler(&mut self, res: &mut IrqResult, buf: &mut [u8]) -> Result<(), Error> { - self.irq_base.irq_handler(res, buf) - } - - pub fn release(self) -> (UART, PINS) { - (self.irq_base.release(), self.pins) - } - - pub fn downgrade(self) -> (UartWithIrqBase, PINS) { - (self.irq_base, self.pins) - } -} - -/* -uart_impl! { - pac::Uarta: (uarta, clock::PeripheralClocks::Uart0), - pac::Uartb: (uartb, clock::PeripheralClocks::Uart1), -} */ - -impl Tx where Uart: Instance {} - -impl embedded_io::ErrorType for UartBase { - type Error = Error; -} - -impl embedded_hal_nb::serial::ErrorType for UartBase { - type Error = Error; -} - -impl embedded_hal_nb::serial::ErrorType for Rx { - type Error = Error; -} - -impl embedded_io::ErrorType for Rx { - type Error = Error; -} - -impl embedded_io::ErrorType for Tx { - type Error = Error; -} - -impl embedded_hal_nb::serial::ErrorType for Tx { - type Error = Error; -} - -impl embedded_hal_nb::serial::Write for Tx { - fn write(&mut self, word: u8) -> nb::Result<(), Self::Error> { - let reader = unsafe { &(*Uart::ptr()) }.txstatus().read(); - if reader.wrrdy().bit_is_clear() { - return Err(nb::Error::WouldBlock); - } else { - // DPARITY bit not supported yet - unsafe { - // NOTE(unsafe) atomic write to data register - (*Uart::ptr()).data().write(|w| w.bits(word as u32)); - } - } - Ok(()) - } - - fn flush(&mut self) -> nb::Result<(), Self::Error> { - // SAFETY: Only TX related registers are used. - let reader = unsafe { &(*Uart::ptr()) }.txstatus().read(); - if reader.wrbusy().bit_is_set() { - return Err(nb::Error::WouldBlock); - } - Ok(()) - } -} - -impl embedded_io::Write for Tx { - fn write(&mut self, buf: &[u8]) -> Result { - if buf.is_empty() { - return Ok(0); - } - - for byte in buf.iter() { - nb::block!(>::write( - self, *byte - ))?; - } - - Ok(buf.len()) - } - - fn flush(&mut self) -> Result<(), Self::Error> { - nb::block!(>::flush(self)) - } -} - -impl embedded_io::Write for UartBase { - fn write(&mut self, buf: &[u8]) -> Result { - self.tx.write(buf) - } - - fn flush(&mut self) -> Result<(), Self::Error> { - self.tx.flush() - } -} - -impl embedded_hal_nb::serial::Read for UartBase { - fn read(&mut self) -> nb::Result { - self.rx.read() - } -} - -impl embedded_hal_nb::serial::Write for UartBase { - fn write(&mut self, word: u8) -> nb::Result<(), Self::Error> { - self.tx.write(word) - } - - fn flush(&mut self) -> nb::Result<(), Self::Error> { - self.tx.flush() - } -} - -impl embedded_hal_nb::serial::Read for Rx { - fn read(&mut self) -> nb::Result { - let uart = unsafe { &(*Uart::ptr()) }; - let status_reader = uart.rxstatus().read(); - let err = if status_reader.rxovr().bit_is_set() { - Some(Error::Overrun) - } else if status_reader.rxfrm().bit_is_set() { - Some(Error::FramingError) - } else if status_reader.rxpar().bit_is_set() { - Some(Error::ParityError) - } else { - None - }; - if let Some(err) = err { - // The status code is always related to the next bit for the framing - // and parity status bits. We have to read the DATA register - // so that the next status reflects the next DATA word - // For overrun error, we read as well to clear the peripheral - uart.data().read().bits(); - Err(err.into()) - } else if status_reader.rdavl().bit_is_set() { - let data = uart.data().read().bits(); - Ok((data & 0xff) as u8) - } else { - Err(nb::Error::WouldBlock) - } - } -} - -impl embedded_io::Read for Rx { - fn read(&mut self, buf: &mut [u8]) -> Result { - if buf.is_empty() { - return Ok(0); - } - - for byte in buf.iter_mut() { - let w = nb::block!(>::read(self))?; - *byte = w; - } - - Ok(buf.len()) - } -} diff --git a/vorago-reb1/examples/max11619-adc.rs b/vorago-reb1/examples/max11619-adc.rs index 0271b8c..5936e68 100644 --- a/vorago-reb1/examples/max11619-adc.rs +++ b/vorago-reb1/examples/max11619-adc.rs @@ -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); } } diff --git a/vorago-reb1/examples/nvm.rs b/vorago-reb1/examples/nvm.rs index 07bdb28..ee7c3d7 100644 --- a/vorago-reb1/examples/nvm.rs +++ b/vorago-reb1/examples/nvm.rs @@ -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(); - - // 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.read(0, &mut orig_content).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); + nvm.write_page(0, 0, &[1, 2, 3, 4]).unwrap(); + nvm.read(0, &mut read_buf[0..4]).unwrap(); - // 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); } diff --git a/vorago-reb1/src/m95m01.rs b/vorago-reb1/src/m95m01.rs index 7dd794a..528b9b3 100644 --- a/vorago-reb1/src/m95m01.rs +++ b/vorago-reb1/src/m95m01.rs @@ -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; @@ -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, 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 { + pub fn verify(&mut self, address: usize, data: &[u8]) -> Result { 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); diff --git a/vscode/launch.json b/vscode/launch.json index eb3034e..d3ce0f8 100644 --- a/vscode/launch.json +++ b/vscode/launch.json @@ -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", @@ -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/debug/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/debug/flashloader", + "interface": "jtag", + "runToEntryPoint": "main", + "rttConfig": { + "enabled": true, + "address": "auto", + "decoders": [ + { + "port": 0, + "timestamp": true, + "type": "console" + } + ] + } + }, ] -} \ No newline at end of file +} diff --git a/vscode/tasks.json b/vscode/tasks.json index 314629e..7f02120 100644 --- a/vscode/tasks.json +++ b/vscode/tasks.json @@ -1,270 +1,200 @@ { - // See https://go.microsoft.com/fwlink/?LinkId=733558 - // for the documentation about the tasks.json format - "version": "2.0.0", - "tasks": [ - { - "label": "rust: cargo build", - "type": "shell", - "command": "~/.cargo/bin/cargo", // note: full path to the cargo - "args": [ - "build" - ], - "group": { - "kind": "build", - "isDefault": true - } - }, - { - "label": "rust: cargo build hal tests", - "type": "shell", - "command": "~/.cargo/bin/cargo", // note: full path to the cargo - "args": [ - "build", - "--bin", - "board-tests", - "--features", - "rt" - ], - "group": { - "kind": "build", - "isDefault": true - } - }, - { - "label": "rust: cargo build rtt", - "type": "shell", - "command": "~/.cargo/bin/cargo", // note: full path to the cargo - "args": [ - "build", - "--example", - "rtt-log", - ], - "group": { - "kind": "build", - "isDefault": true - } - }, - { - "label": "rust: cargo build systick", - "type": "shell", - "command": "~/.cargo/bin/cargo", // note: full path to the cargo - "args": [ - "build", - "--example", - "timer-ticks", - "--features", - "rt" - ], - "group": { - "kind": "build", - "isDefault": true - } - }, - { - "label": "rust: cargo build uart", - "type": "shell", - "command": "~/.cargo/bin/cargo", // note: full path to the cargo - "args": [ - "build", - "--example", - "uart", - ], - "group": { - "kind": "build", - "isDefault": true - } - }, - { - "label": "rust: cargo build spi", - "type": "shell", - "command": "~/.cargo/bin/cargo", // note: full path to the cargo - "args": [ - "build", - "--example", - "spi", - ], - "group": { - "kind": "build", - "isDefault": true - } - }, - { - "label": "rust: cargo build pwm", - "type": "shell", - "command": "~/.cargo/bin/cargo", // note: full path to the cargo - "args": [ - "build", - "--example", - "pwm", - "--features", - "rt" - ], - "group": { - "kind": "build", - "isDefault": true - } - }, - { - "label": "rust: cargo build cascade", - "type": "shell", - "command": "~/.cargo/bin/cargo", // note: full path to the cargo - "args": [ - "build", - "--example", - "cascade", - "--features", - "rt" - ], - "group": { - "kind": "build", - "isDefault": true - } - }, - { - "label": "rust: cargo build uart irq", - "type": "shell", - "command": "~/.cargo/bin/cargo", // note: full path to the cargo - "args": [ - "build", - "--bin", - "uart-rtic", - ], - "group": { - "kind": "build", - "isDefault": true - } - }, - { - "label": "blinky-hal", - "type": "shell", - "command": "~/.cargo/bin/cargo", // note: full path to the cargo - "args": [ - "build", - "--example", - "blinky", - ], - "group": { - "kind": "build", - } - }, - { - "label": "rust: cargo build led blinky", - "type": "shell", - "command": "~/.cargo/bin/cargo", // note: full path to the cargo - "args": [ - "build", - "--example", - "blinky-leds", - ], - "group": { - "kind": "build", - "isDefault": true - } - }, - { - "label": "rust: cargo build button blinky", - "type": "shell", - "command": "~/.cargo/bin/cargo", // note: full path to the cargo - "args": [ - "build", - "--example", - "blinky-button-irq", - ], - "group": { - "kind": "build", - "isDefault": true - } - }, - { - "label": "rust: cargo build temp sensor", - "type": "shell", - "command": "~/.cargo/bin/cargo", // note: full path to the cargo - "args": [ - "build", - "--example", - "adt75-temp-sensor", - ], - "group": { - "kind": "build", - "isDefault": true - } - }, - { - "label": "rust: cargo build button blinky rtic", - "type": "shell", - "command": "~/.cargo/bin/cargo", // note: full path to the cargo - "args": [ - "build", - "--example", - "blinky-button-rtic", - ], - "group": { - "kind": "build", - "isDefault": true - } - }, - { - "label": "rust: cargo build accelerometer", - "type": "shell", - "command": "~/.cargo/bin/cargo", // note: full path to the cargo - "args": [ - "build", - "--example", - "adxl343-accelerometer" - ], - "group": { - "kind": "build", - "isDefault": true - } - }, - { - "label": "rust: cargo build adc", - "type": "shell", - "command": "~/.cargo/bin/cargo", // note: full path to the cargo - "args": [ - "build", - "--example", - "max11619-adc", - ], - "group": { - "kind": "build", - "isDefault": true - } - }, - { - "label": "reb1-nvm", - "type": "shell", - "command": "~/.cargo/bin/cargo", // note: full path to the cargo - "args": [ - "build", - "--example", - "nvm", - ], - "group": { - "kind": "build", - "isDefault": true - } - }, - { - "label": "rtic-example", - "type": "shell", - "command": "~/.cargo/bin/cargo", // note: full path to the cargo - "args": [ - "build", - "--bin", - "rtic-example", - ], - }, - { - "label": "embassy-example", - "type": "shell", - "command": "~/.cargo/bin/cargo", // note: full path to the cargo - "args": [ - "build", - "--bin", - "embassy-example", - ], - }, - ] + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "rust: cargo build", + "type": "shell", + "command": "~/.cargo/bin/cargo", // note: full path to the cargo + "args": ["build"], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "rust: cargo build hal tests", + "type": "shell", + "command": "~/.cargo/bin/cargo", // note: full path to the cargo + "args": ["build", "--bin", "board-tests", "--features", "rt"], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "rust: cargo build rtt", + "type": "shell", + "command": "~/.cargo/bin/cargo", // note: full path to the cargo + "args": ["build", "--example", "rtt-log"], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "rust: cargo build systick", + "type": "shell", + "command": "~/.cargo/bin/cargo", // note: full path to the cargo + "args": ["build", "--example", "timer-ticks", "--features", "rt"], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "rust: cargo build uart", + "type": "shell", + "command": "~/.cargo/bin/cargo", // note: full path to the cargo + "args": ["build", "--example", "uart"], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "rust: cargo build spi", + "type": "shell", + "command": "~/.cargo/bin/cargo", // note: full path to the cargo + "args": ["build", "--example", "spi"], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "rust: cargo build pwm", + "type": "shell", + "command": "~/.cargo/bin/cargo", // note: full path to the cargo + "args": ["build", "--example", "pwm", "--features", "rt"], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "rust: cargo build cascade", + "type": "shell", + "command": "~/.cargo/bin/cargo", // note: full path to the cargo + "args": ["build", "--example", "cascade", "--features", "rt"], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "uart-echo-rtic-example", + "type": "shell", + "command": "~/.cargo/bin/cargo", // note: full path to the cargo + "args": ["build", "--bin", "uart-echo-rtic"], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "blinky-hal", + "type": "shell", + "command": "~/.cargo/bin/cargo", // note: full path to the cargo + "args": ["build", "--example", "blinky"], + "group": { + "kind": "build" + } + }, + { + "label": "rust: cargo build led blinky", + "type": "shell", + "command": "~/.cargo/bin/cargo", // note: full path to the cargo + "args": ["build", "--example", "blinky-leds"], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "rust: cargo build button blinky", + "type": "shell", + "command": "~/.cargo/bin/cargo", // note: full path to the cargo + "args": ["build", "--example", "blinky-button-irq"], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "rust: cargo build temp sensor", + "type": "shell", + "command": "~/.cargo/bin/cargo", // note: full path to the cargo + "args": ["build", "--example", "adt75-temp-sensor"], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "rust: cargo build button blinky rtic", + "type": "shell", + "command": "~/.cargo/bin/cargo", // note: full path to the cargo + "args": ["build", "--example", "blinky-button-rtic"], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "rust: cargo build accelerometer", + "type": "shell", + "command": "~/.cargo/bin/cargo", // note: full path to the cargo + "args": ["build", "--example", "adxl343-accelerometer"], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "rust: cargo build adc", + "type": "shell", + "command": "~/.cargo/bin/cargo", // note: full path to the cargo + "args": ["build", "--example", "max11619-adc"], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "reb1-nvm", + "type": "shell", + "command": "~/.cargo/bin/cargo", // note: full path to the cargo + "args": ["build", "--example", "nvm"], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "rtic-example", + "type": "shell", + "command": "~/.cargo/bin/cargo", // note: full path to the cargo + "args": ["build", "--bin", "rtic-example"] + }, + { + "label": "embassy-example", + "type": "shell", + "command": "~/.cargo/bin/cargo", // note: full path to the cargo + "args": ["build", "--bin", "embassy-example"] + }, + { + "label": "bootloader", + "type": "shell", + "command": "~/.cargo/bin/cargo", // note: full path to the cargo + "args": ["build", "--bin", "bootloader"] + }, + { + "label": "flashloader", + "type": "shell", + "command": "~/.cargo/bin/cargo", // note: full path to the cargo + "args": ["build", "--bin", "flashloader"] + } + ] }