diff --git a/justfile b/justfile index 5cae177..5bdf021 100644 --- a/justfile +++ b/justfile @@ -22,13 +22,12 @@ check-va108xx: [working-directory: 'va416xx'] check-va416xx: cargo check --target thumbv7em-none-eabihf - cargo check --target thumbv7em-none-eabihf --examples cargo check -p va416xx --target thumbv7em-none-eabihf --all-features cargo check -p va416xx-hal --target thumbv7em-none-eabihf --features "defmt va41630" [working-directory: 'va108xx'] build-va108xx: - cargo build --target thumbv6m-none-eabi + cargo build --target thumbv6m-none-eabi --release [working-directory: 'va416xx'] build-va416xx: diff --git a/tools/va108xx-flashloader-client/.gitignore b/tools/va108xx-flashloader-client/.gitignore new file mode 100644 index 0000000..9aa6a64 --- /dev/null +++ b/tools/va108xx-flashloader-client/.gitignore @@ -0,0 +1 @@ +output.log diff --git a/tools/va108xx-flashloader-client/Cargo.toml b/tools/va108xx-flashloader-client/Cargo.toml new file mode 100644 index 0000000..055e84a --- /dev/null +++ b/tools/va108xx-flashloader-client/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "client" +version = "0.1.0" +edition = "2024" + +[dependencies] +clap = { version = "4", features = ["derive"] } +fern = "0.7" +object = "0.39" +humantime = "2" +crc = "3" +log = "0.4" +toml = "1.1.2+spec-1.1.0" +anyhow = "1" +serialport = "4" +cobs = "0.5" +serde = { version = "1", features = ["derive"] } +models = { path = "../../va108xx/flashloader/models" } +postcard = { version = "1", features = ["alloc"] } +spacepackets = { version = "0.17" } +# tmtc-utils = { git = "https://egit.irs.uni-stuttgart.de/rust/tmtc-utils.git", version = "0.1" } +tmtc-utils = { path = "../../../../ROMEO/tmtc-utils", version = "0.1" } diff --git a/tools/va108xx-flashloader-client/config.toml b/tools/va108xx-flashloader-client/config.toml new file mode 100644 index 0000000..5b018b6 --- /dev/null +++ b/tools/va108xx-flashloader-client/config.toml @@ -0,0 +1,2 @@ +[interface] +serial_port = "/dev/ttyUSB0" diff --git a/tools/va108xx-flashloader-client/src/elf.rs b/tools/va108xx-flashloader-client/src/elf.rs new file mode 100644 index 0000000..2d7e291 --- /dev/null +++ b/tools/va108xx-flashloader-client/src/elf.rs @@ -0,0 +1,92 @@ +use std::path::Path; + +use models::{ + APP_A_START_ADDR, APP_A_MAX_SIZE, APP_B_START_ADDR, APP_B_MAX_SIZE, + BOOTLOADER_START_ADDR, BOOTLOADER_MAX_SIZE, +}; + +use crate::Target; + +pub struct LoadableSegment { + pub name: String, + pub offset: u32, + pub data: Vec, +} + +pub fn parse_elf(target: &Target, path: &Path) -> anyhow::Result> { + use object::{Object, ObjectSection, ObjectSegment}; + + let raw = std::fs::read(path)?; + let obj = object::File::parse(raw.as_slice())?; + + let expected_base: u32 = match target { + Target::Bl => BOOTLOADER_START_ADDR, + Target::A => APP_A_START_ADDR, + Target::B => APP_B_START_ADDR, + }; + + let mut segments = Vec::new(); + + for (idx, segment) in obj.segments().enumerate() { + let data = segment.data()?; + if data.is_empty() { + continue; + } + + let addr = segment.address() as u32; + + if idx == 0 && addr != expected_base { + anyhow::bail!( + "unexpected base address {addr:#010x} for {target:?}, expected {expected_base:#010x}" + ); + } + + let name = obj + .sections() + .find(|s| { + let s_addr = s.address() as u32; + s_addr >= addr + && s_addr < addr + data.len() as u32 + && !s.name().unwrap_or("").is_empty() + }) + .and_then(|s| s.name().ok().map(str::to_owned)) + .unwrap_or_else(|| format!("")); + + segments.push(LoadableSegment { + name, + offset: addr, + data: data.to_vec(), + }); + } + + Ok(segments) +} + +pub fn check_total_size(target: &Target, total_size: usize) -> anyhow::Result<()> { + let max = match target { + Target::Bl => BOOTLOADER_MAX_SIZE, + Target::A => APP_A_MAX_SIZE, + Target::B => APP_B_MAX_SIZE, + } as usize; + + if total_size > max { + anyhow::bail!( + "{target:?} image is {total_size} bytes, exceeds maximum of {max} bytes" + ); + } + Ok(()) +} + +pub fn log_segments_info(target: &Target, segments: &[LoadableSegment], total_size: usize, path: &Path) { + log::info!( + "Flashing {target:?} with image '{}' — {} segment(s), {total_size} bytes total", + path.display(), + segments.len() + ); + for (i, seg) in segments.iter().enumerate() { + log::info!( + " segment {i}: '{}' @ {:#010x}, {} bytes", + seg.name, seg.offset, seg.data.len() + ); + } +} diff --git a/tools/va108xx-flashloader-client/src/flash.rs b/tools/va108xx-flashloader-client/src/flash.rs new file mode 100644 index 0000000..45c017e --- /dev/null +++ b/tools/va108xx-flashloader-client/src/flash.rs @@ -0,0 +1,142 @@ +use std::path::Path; + +use anyhow::Context; +use crc::{CRC_16_IBM_3740, Crc}; +use spacepackets::CcsdsPacketReader; +use tmtc_utils::transport::serial::PacketTransportSerialCobs; + +use crate::{ + Target, + elf::{check_total_size, log_segments_info, parse_elf}, + tc, +}; +use models::{ + APP_A_CRC_ADDR, APP_A_SIZE_ADDR, APP_B_CRC_ADDR, APP_B_SIZE_ADDR, BOOTLOADER_CRC_ADDR, +}; +use models::{Request, Response}; + +pub const CHUNK_SIZE: usize = 256; + +fn send_and_await_ok( + transport: &mut PacketTransportSerialCobs, + request: &Request, + payload: &[u8], + timeout: std::time::Duration, +) -> anyhow::Result<()> { + let tc = tc::create_tc_with_additional_payload(request, payload); + log::debug!( + "TX TC {:#010x}: {request:?} + {} payload bytes", + tc.ccsds_packet_id_and_psc().raw(), + payload.len() + ); + transport.send(&tc.to_vec())?; + + let deadline = std::time::Instant::now() + timeout; + let mut got_ok = false; + + while !got_ok { + if std::time::Instant::now() > deadline { + anyhow::bail!("timeout waiting for Ok response to {request:?}"); + } + transport.receive(|packet| { + let Ok(reader) = CcsdsPacketReader::new_with_checksum(packet) else { + return; + }; + match postcard::take_from_bytes::(reader.packet_data()) { + Ok((Response::Ok, _)) => got_ok = true, + #[allow(unreachable_patterns)] + Ok((other, _)) => log::warn!("unexpected response: {other:?}"), + Err(e) => log::error!("failed to deserialise response: {e}"), + } + })?; + } + + Ok(()) +} + +pub fn flash_image( + transport: &mut PacketTransportSerialCobs, + target: &Target, + path: &Path, +) -> anyhow::Result<()> { + let segments = parse_elf(target, path) + .with_context(|| format!("failed to parse ELF: {}", path.display()))?; + + let total_size: usize = segments.iter().map(|s| s.data.len()).sum(); + check_total_size(target, total_size)?; + log_segments_info(target, &segments, total_size, path); + + let ack_timeout = std::time::Duration::from_secs(2); + + // 1. Write all segments in chunks, waiting for Ok after each. + for seg in &segments { + let mut pos = 0usize; + while pos < seg.data.len() { + let chunk_len = CHUNK_SIZE.min(seg.data.len() - pos); + let offset = seg.offset + pos as u32; + let chunk = &seg.data[pos..pos + chunk_len]; + + log::info!( + "Writing {chunk_len} bytes @ {offset:#010x} ({}/{} of '{}')", + pos + chunk_len, + seg.data.len(), + seg.name, + ); + + send_and_await_ok(transport, &Request::WriteNvm { offset }, chunk, ack_timeout)?; + pos += chunk_len; + } + } + + // 2. CRC + size postprocessing. + match target { + Target::Bl => { + log::info!("Blanking bootloader CRC @ {BOOTLOADER_CRC_ADDR:#010x}"); + send_and_await_ok( + transport, + &Request::WriteNvm { + offset: BOOTLOADER_CRC_ADDR, + }, + &[0x00, 0x00], + ack_timeout, + )?; + } + Target::A | Target::B => { + let (size_addr, crc_addr) = match target { + Target::A => (APP_A_SIZE_ADDR, APP_A_CRC_ADDR), + Target::B => (APP_B_SIZE_ADDR, APP_B_CRC_ADDR), + Target::Bl => unreachable!(), + }; + + log::info!("Writing app size {total_size} @ {size_addr:#010x}"); + send_and_await_ok( + transport, + &Request::WriteNvm { offset: size_addr }, + &(total_size as u32).to_be_bytes(), + ack_timeout, + )?; + + let crc = Crc::::new(&CRC_16_IBM_3740); + let mut digest = crc.digest(); + for seg in &segments { + digest.update(&seg.data); + } + let checksum = digest.finalize().to_be_bytes(); + + log::info!( + "Writing CRC [{:#04x}, {:#04x}] @ {crc_addr:#010x}", + checksum[0], + checksum[1] + ); + send_and_await_ok( + transport, + &Request::WriteNvm { offset: crc_addr }, + &checksum, + ack_timeout, + )?; + } + } + + log::info!("Flash complete."); + Ok(()) +} diff --git a/tools/va108xx-flashloader-client/src/main.rs b/tools/va108xx-flashloader-client/src/main.rs new file mode 100644 index 0000000..4a8726f --- /dev/null +++ b/tools/va108xx-flashloader-client/src/main.rs @@ -0,0 +1,194 @@ +use std::{ + fs::File, + io::Read as _, + net::SocketAddr, + path::{Path, PathBuf}, + time::SystemTime, +}; + +use anyhow::bail; +use clap::Parser as _; +use cobs::CobsDecoderOwned; +use models::Response; +use spacepackets::CcsdsPacketReader; +use tmtc_utils::transport::serial::PacketTransportSerialCobs; + +pub mod elf; +pub mod flash; +pub mod tc; + +#[derive(clap::Parser)] +#[command(name = "image-loader", about = "VA416XX Image Loader Application")] +pub struct Cli { + /// Serial port to use (overrides loader.toml) + #[arg(short, long)] + pub port: Option, + + #[command(subcommand)] + pub command: Command, +} + +#[derive(clap::Subcommand)] +pub enum Command { + /// Send a ping command + Ping, + + /// Set the active boot slot + SetBootSlot { + /// Boot slot to activate + app: AppTarget, + }, + + /// Corrupt an app slot (for testing) + Corrupt { + /// Target slot to corrupt + app: AppTarget, + }, + + /// Flash an ELF image to a target slot + Flash { + /// Target to flash + target: Target, + + /// Path to the ELF image + path: PathBuf, + }, +} + +#[derive(clap::ValueEnum, Clone, Debug)] +pub enum Target { + /// Bootloader slot + Bl, + /// Application slot A + A, + /// Application slot B + B, +} + +/// Only app slots can be corrupted (not the bootloader) +#[derive(clap::ValueEnum, Clone, Debug)] +pub enum AppTarget { + A, + B, +} + +#[derive(Debug, serde::Deserialize)] +pub struct Config { + pub interface: Interface, +} + +#[derive(Debug, serde::Deserialize)] +pub struct Interface { + pub serial_port: Option, + pub udp_addr: Option, +} + +impl Config { + pub fn new_from_file() -> Self { + let mut config_file = + File::open(Path::new("config.toml")).expect("opening config.toml file failed"); + let mut toml_str = String::new(); + config_file + .read_to_string(&mut toml_str) + .expect("reading config.toml file failed"); + let config: Config = toml::from_str(&toml_str).expect("parsing config.toml file failed"); + config + } +} + +pub fn setup_logger() -> Result<(), fern::InitError> { + fern::Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "[{} {} {}] {}", + humantime::format_rfc3339_seconds(SystemTime::now()), + record.level(), + record.target(), + message + )) + }) + .level(log::LevelFilter::Info) + .chain(std::io::stdout()) + .chain(fern::log_file("output.log")?) + .apply()?; + Ok(()) +} + +fn main() -> anyhow::Result<()> { + setup_logger().expect("failed to initialize logger"); + println!("-- VA108xx Flashloader Client --"); + let cli = Cli::parse(); + let config = Config::new_from_file(); + + if config.interface.serial_port.is_none() { + bail!("Serial port not specified in configuration file."); + } + let serial_port = config.interface.serial_port.as_ref().unwrap(); + let serial = serialport::new(serial_port, 115200) + .open() + .expect("opening serial port failed"); + let mut transport = PacketTransportSerialCobs::new(serial, CobsDecoderOwned::new(4096)); + + match cli.command { + Command::Ping => { + let tc = tc::create_tc(&models::Request::Ping); + log::info!( + "Sending ping request with TC ID: {:#010x}", + tc.ccsds_packet_id_and_psc().raw() + ); + let tc_raw = tc.to_vec(); + transport.send(&tc_raw).unwrap(); + } + Command::SetBootSlot { app } => { + let app_sel = match app { + AppTarget::A => models::AppSel::A, + AppTarget::B => models::AppSel::B, + }; + let tc = tc::create_tc(&models::Request::SetBootSlot(app_sel)); + log::info!( + "Sending app select {:?} command with TC ID: {:#010x}", + app_sel, + tc.ccsds_packet_id_and_psc().raw() + ); + transport.send(&tc.to_vec()).unwrap(); + } + Command::Corrupt { app } => { + let app_sel = match app { + AppTarget::A => models::AppSel::A, + AppTarget::B => models::AppSel::B, + }; + let tc = tc::create_tc(&models::Request::SetBootSlot(app_sel)); + log::info!( + "Sending corrupt slot {:?} command with TC ID: {:#010x}", + app_sel, + tc.ccsds_packet_id_and_psc().raw() + ); + transport.send(&tc.to_vec()).unwrap(); + } + Command::Flash { target, path } => { + flash::flash_image(&mut transport, &target, &path)?; + } + } + + log::info!("Waiting for response..."); + loop { + transport + .receive(|packet: &[u8]| { + let reader = CcsdsPacketReader::new_with_checksum(packet); + log::debug!("Received packet: {:?}", reader); + if let Ok(reader) = reader { + let packet_data = reader.packet_data(); + let response_result = postcard::take_from_bytes::(packet_data); + if let Ok((response, _remainder)) = response_result { + log::info!("Received TM with response: {:?}", response); + } else { + log::error!( + "Failed to parse response from packet data: {:?}", + response_result + ); + } + } + }) + .unwrap(); + } +} diff --git a/tools/va108xx-flashloader-client/src/tc.rs b/tools/va108xx-flashloader-client/src/tc.rs new file mode 100644 index 0000000..2d1896e --- /dev/null +++ b/tools/va108xx-flashloader-client/src/tc.rs @@ -0,0 +1,14 @@ +use spacepackets::{CcsdsPacketCreatorOwned, SpHeader}; + +pub fn create_tc(request: &models::Request) -> CcsdsPacketCreatorOwned { + let req_raw = postcard::to_allocvec(&request).unwrap(); + let sp_header = SpHeader::new_from_apid(models::APID); + CcsdsPacketCreatorOwned::new_tc_with_checksum(sp_header, &req_raw).unwrap() +} + +pub fn create_tc_with_additional_payload(request: &models::Request, payload: &[u8]) -> CcsdsPacketCreatorOwned { + let mut req_raw = postcard::to_allocvec(&request).unwrap(); + req_raw.extend_from_slice(payload); + let sp_header = SpHeader::new_from_apid(models::APID); + CcsdsPacketCreatorOwned::new_tc_with_checksum(sp_header, &req_raw).unwrap() +} diff --git a/va108xx/Cargo.toml b/va108xx/Cargo.toml index b0e02e3..73523ac 100644 --- a/va108xx/Cargo.toml +++ b/va108xx/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -resolver = "2" +resolver = "3" members = [ "vorago-reb1", "va108xx", @@ -10,6 +10,7 @@ members = [ "board-tests", "bootloader", "flashloader", + "flashloader/models", ] exclude = [ "flashloader/slot-a-blinky", @@ -43,3 +44,6 @@ lto = true opt-level = 'z' # <- overflow-checks = false # <- strip = true # Automatically strip symbols from the binary. + +[patch.crates-io] +spacepackets = { git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git", rev = "ec952b771cc62a371786cb1a04fd1baa9c33523c" } diff --git a/va108xx/examples/rtic/src/bin/uart-echo-rtic.rs b/va108xx/examples/rtic/src/bin/uart-echo-rtic.rs index 855ab1b..5dc6786 100644 --- a/va108xx/examples/rtic/src/bin/uart-echo-rtic.rs +++ b/va108xx/examples/rtic/src/bin/uart-echo-rtic.rs @@ -67,7 +67,7 @@ mod app { InterruptConfig::new(pac::Interrupt::OC3, true, true), ); let (tx, rx) = irq_uart.split(); - let mut rx = rx.into_rx_with_irq(); + let mut rx = rx.into_rx_with_interrupt(); rx.start(); diff --git a/va108xx/flashloader/Cargo.toml b/va108xx/flashloader/Cargo.toml index 008e361..c27c773 100644 --- a/va108xx/flashloader/Cargo.toml +++ b/va108xx/flashloader/Cargo.toml @@ -11,23 +11,27 @@ defmt = "1" defmt-rtt = { version = "1" } panic-probe = { version = "1", features = ["print-defmt"] } num_enum = { version = "0.7", default-features = false } -cobs = { version = "0.5", default-features = false } -satrs = { version = "0.3.0-alpha.3", default-features = false, features = ["defmt"] } +cobs = { version = "0.5", default-features = false, features = ["defmt"] } fugit = "0.4" arbitrary-int = "2" -ringbuf = { version = "0.4.7", default-features = false, features = ["portable-atomic"] } -# spacepackets = { version = "0.17", path = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git", default-features = false, features = ["defmt"] } +embassy-sync = "0.8" +embedded-io-async = "0.7" +embassy-time = { version = "0.5", features = ["defmt-timestamp-uptime-ms"] } +static_cell = "2" +postcard = { version = "1", features = ["use-defmt"] } +spacepackets = { version = "0.17", default-features = false, features = ["defmt"] } +serde = { version = "1", 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. portable-atomic = {version = "1", features = ["unsafe-assume-single-core"]} +models = { path = "./models", features = ["defmt"] } rtic = { version = "2", features = ["thumbv6-backend"] } -rtic-monotonics = { version = "2", features = ["cortex-m-systick"] } [dependencies.va108xx-hal] version = "0.13" path = "../va108xx-hal" -features = ["defmt"] +features = ["defmt", "embassy-oc30-oc31"] [dependencies.vorago-reb1] version = "0.10" diff --git a/va108xx/flashloader/README.md b/va108xx/flashloader/README.md index 7bd7aeb..cda5456 100644 --- a/va108xx/flashloader/README.md +++ b/va108xx/flashloader/README.md @@ -2,9 +2,9 @@ VA108xx Flashloader Application ======== This flashloader shows a minimal example for a self-updatable Rust software which exposes -a simple PUS (CCSDS) interface to update the software. It also provides a Python application -called the `image-loader.py` which can be used to upload compiled images to the flashloader -application to write them to the NVM. +a simple PUS (CCSDS) interface to update the software. It also provides a Rust application +which can be used to upload compiled images to the flashloader application to write them to the NVM. +You can find it inside the `tools/va108xx-image-loader` directory. 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/vorago-rs/src/branch/main/va108xx/bootloader). @@ -13,7 +13,7 @@ The software can quickly be adapted to interface with a real primary on-board so the Python script provided here to upload images because it uses a low-level CCSDS based packet interface. -## Using the Python image loader +## Using the image loader The Python image loader communicates with the Rust flashload application using a dedicated serial port with a baudrate of 115200. diff --git a/va108xx/flashloader/models/Cargo.toml b/va108xx/flashloader/models/Cargo.toml new file mode 100644 index 0000000..e068cd7 --- /dev/null +++ b/va108xx/flashloader/models/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "models" +version = "0.1.0" +edition = "2024" + +[dependencies] +postcard = { version = "1" } +spacepackets = { version = "0.17", default-features = false, features = ["defmt"] } +num_enum = { version = "0.7", default-features = false } +defmt = { version = "1", optional = true } +serde = { version = "1", default-features = false, features = ["derive"] } +cobs = { version = "0.5", default-features = false } +thiserror = { version = "2", default-features = false } +arbitrary-int = "2" diff --git a/va108xx/flashloader/models/src/lib.rs b/va108xx/flashloader/models/src/lib.rs new file mode 100644 index 0000000..c139ec2 --- /dev/null +++ b/va108xx/flashloader/models/src/lib.rs @@ -0,0 +1,98 @@ +#![no_std] + +use arbitrary_int::u11; +use cobs::DestBufTooSmallError; +use spacepackets::{ + CcsdsPacketCreationError, CcsdsPacketCreatorWithReservedData, SpacePacketHeader, +}; + +pub const APID: u11 = u11::new(0x01); + +pub const BOOTLOADER_START_ADDR: u32 = 0x0000_0000; +pub const BOOTLOADER_END_ADDR: u32 = 0x0000_3000; +pub const BOOTLOADER_CRC_ADDR: u32 = BOOTLOADER_END_ADDR - 2; +pub const BOOTLOADER_MAX_SIZE: u32 = BOOTLOADER_END_ADDR - BOOTLOADER_START_ADDR - 2; + +pub const APP_A_START_ADDR: u32 = 0x0000_3000; +pub const APP_B_END_ADDR: u32 = 0x0002_0000 - 8; +pub const IMG_SLOT_SIZE: u32 = (APP_B_END_ADDR - APP_A_START_ADDR) / 2; + +pub const APP_A_END_ADDR: u32 = APP_A_START_ADDR + IMG_SLOT_SIZE; +pub const APP_A_SIZE_ADDR: u32 = APP_A_END_ADDR - 8; +pub const APP_A_CRC_ADDR: u32 = APP_A_END_ADDR - 4; +pub const APP_A_MAX_SIZE: u32 = IMG_SLOT_SIZE - 8; + +pub const APP_B_START_ADDR: u32 = APP_A_END_ADDR; +pub const APP_B_SIZE_ADDR: u32 = APP_B_END_ADDR - 8; +pub const APP_B_CRC_ADDR: u32 = APP_B_END_ADDR - 4; +pub const APP_B_MAX_SIZE: u32 = IMG_SLOT_SIZE - 8; + +#[derive( + Debug, + Copy, + Clone, + PartialEq, + Eq, + num_enum::TryFromPrimitive, + serde::Serialize, + serde::Deserialize, +)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u8)] +pub enum AppSel { + A = 0, + B = 1, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Request { + Ping, + Corrupt(AppSel), + WriteNvm { offset: u32 }, + SetBootSlot(AppSel), +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Response { + Ok, +} + +pub fn create_tm_packet( + buf: &mut [u8], + sp_header: SpacePacketHeader, + response: Response, +) -> Result { + let packet_data_size = postcard::experimental::serialized_size(&response).unwrap(); + let mut creator = + CcsdsPacketCreatorWithReservedData::new_tm_with_checksum(sp_header, packet_data_size, buf)?; + + let current_index = 0; + postcard::to_slice(&response, &mut creator.packet_data_mut()[current_index..]).unwrap(); + Ok(creator.finish()) +} + +#[derive(Debug, thiserror::Error)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum PacketCreationAndEncodingError { + #[error("packet creation failed: {0}")] + Creation(#[from] CcsdsPacketCreationError), + #[error("destination buffer too small: {0}")] + Encoding(#[from] DestBufTooSmallError), +} + +pub fn create_encoded_tm_packet( + buf: &mut [u8], + encoded_buf: &mut [u8], + sp_header: SpacePacketHeader, + response: Response, +) -> Result { + let packet_len = create_tm_packet(buf, sp_header, response)?; + let encoded_len = + cobs::try_encode_including_sentinels(&buf[0..packet_len], &mut encoded_buf[..])?; + Ok(encoded_len) +} + +#[cfg(test)] +mod tests {} diff --git a/va108xx/flashloader/src/main.rs b/va108xx/flashloader/src/main.rs index 5776257..1ccf03d 100644 --- a/va108xx/flashloader/src/main.rs +++ b/va108xx/flashloader/src/main.rs @@ -4,51 +4,22 @@ #![no_std] use defmt_rtt as _; // global logger -use num_enum::TryFromPrimitive; use panic_probe as _; -use ringbuf::{ - traits::{Consumer, Observer, Producer}, - StaticRb, -}; -use rtic_monotonics::fugit::ExtU32; use va108xx_hal::time::Hertz; const SYSCLK_FREQ: Hertz = Hertz::from_raw(50_000_000); +const UART_BANK: va108xx_hal::uart::Bank = va108xx_hal::uart::Bank::Uart0; -const MAX_TC_SIZE: usize = 524; -const MAX_TC_FRAME_SIZE: usize = cobs::max_encoding_length(MAX_TC_SIZE); +pub const MAX_TC_SIZE: usize = 524; +pub 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, - SetBootSlot = 130, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, defmt::Format)] -#[repr(u8)] -enum AppSel { - A = 0, - B = 1, -} - -// 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; - -pub struct RingBufWrapper { - pub buf: StaticRb, - pub sizes: StaticRb, -} +const TC_PIPE_SIZE: usize = 1024; +const TM_PIPE_SIZE: usize = 128; pub const APP_A_START_ADDR: u32 = 0x3000; pub const APP_A_END_ADDR: u32 = 0x117FC; @@ -60,21 +31,16 @@ pub const PREFERRED_SLOT_OFFSET: u32 = 0x20000 - 1; #[rtic::app(device = pac, dispatchers = [OC20, OC21, OC22])] mod app { use super::*; - use arbitrary_int::traits::Integer as _; - use arbitrary_int::{u11, u14}; + use cobs::CobsDecoderHeapless; use cortex_m::asm; - use embedded_io::Write; - use rtic::Mutex; - use rtic_monotonics::Monotonic; - use satrs::pus::verification::{FailParams, VerificationReportCreator}; - use satrs::spacepackets::ecss::PusServiceId; - use satrs::spacepackets::ecss::{ - tc::PusTcReader, tm::PusTmCreator, EcssEnumU8, PusPacket, WritablePusPacket, - }; + use embassy_sync::blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex}; + use embedded_io_async::Write as _; + use models::{create_encoded_tm_packet, Response}; + use spacepackets::{CcsdsPacketReader, SpacePacketHeader}; use va108xx_hal::pins::PinsA; use va108xx_hal::spi::SpiClockConfig; - use va108xx_hal::uart::InterruptContextTimeoutOrMaxSize; - use va108xx_hal::{pac, uart, InterruptConfig}; + use va108xx_hal::uart::{self, TxAsync}; + use va108xx_hal::{pac, InterruptConfig}; use vorago_reb1::m95m01::M95M01; #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] @@ -88,32 +54,29 @@ mod app { #[local] struct Local { uart_rx: uart::RxWithInterrupt, - uart_tx: uart::Tx, - rx_context: InterruptContextTimeoutOrMaxSize, - verif_reporter: VerificationReportCreator, + uart_tx: uart::TxAsync, nvm: M95M01, + tc_tx: embassy_sync::pipe::Writer<'static, CriticalSectionRawMutex, TC_PIPE_SIZE>, + tc_rx: embassy_sync::pipe::Reader<'static, CriticalSectionRawMutex, TC_PIPE_SIZE>, + // We are only using this in threads, and RTIC ensures there are no conflicts. + tm_tx: embassy_sync::pipe::Writer<'static, NoopRawMutex, TM_PIPE_SIZE>, + tm_rx: embassy_sync::pipe::Reader<'static, NoopRawMutex, TM_PIPE_SIZE>, } #[shared] - struct Shared { - // Having this shared allows multiple tasks to generate telemetry. - tm_rb: RingBufWrapper, - tc_rb: RingBufWrapper, - } - - rtic_monotonics::systick_monotonic!(Mono, 1000); + struct Shared {} #[init] fn init(cx: init::Context) -> (Shared, Local) { defmt::println!("-- Vorago flashloader --"); - Mono::start(cx.core.SYST, SYSCLK_FREQ.to_raw()); + let periphs = cx.device; + va108xx_hal::embassy_time::init(periphs.tim14, periphs.tim15, SYSCLK_FREQ); - let dp = cx.device; let spi_clock_config = SpiClockConfig::new(2, 4); - let nvm = M95M01::new(dp.spic, spi_clock_config); + let nvm = M95M01::new(periphs.spic, spi_clock_config); - let gpioa = PinsA::new(dp.porta); + let gpioa = PinsA::new(periphs.porta); let tx = gpioa.pa9; let rx = gpioa.pa8; @@ -124,7 +87,7 @@ mod app { ); let uart_config = uart::Config::new_with_clock_config(clock_config); let irq_uart = uart::Uart::new_with_interrupt_uart0( - dp.uarta, + periphs.uarta, tx, rx, uart_config, @@ -132,32 +95,32 @@ mod app { ); let (tx, rx) = irq_uart.split(); // Unwrap is okay, we explicitely set the interrupt ID. - let mut rx = rx.into_rx_with_irq(); + let mut rx = rx.into_rx_with_interrupt(); - let verif_reporter = VerificationReportCreator::new(u11::new(0)); + rx.start(); + tc_handler::spawn().unwrap(); + tm_tx_handler::spawn().unwrap(); - let mut rx_context = InterruptContextTimeoutOrMaxSize::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(); + let tx_async = TxAsync::new(tx); + + static TC_PIPE: static_cell::ConstStaticCell< + embassy_sync::pipe::Pipe, + > = static_cell::ConstStaticCell::new(embassy_sync::pipe::Pipe::new()); + static TM_PIPE: static_cell::ConstStaticCell< + embassy_sync::pipe::Pipe, + > = static_cell::ConstStaticCell::new(embassy_sync::pipe::Pipe::new()); + let (tc_rx, tc_tx) = TC_PIPE.take().split(); + let (tm_rx, tm_tx) = TM_PIPE.take().split(); ( - Shared { - tc_rb: RingBufWrapper { - buf: StaticRb::default(), - sizes: StaticRb::default(), - }, - tm_rb: RingBufWrapper { - buf: StaticRb::default(), - sizes: StaticRb::default(), - }, - }, + Shared {}, Local { uart_rx: rx, - uart_tx: tx, - rx_context, - verif_reporter, + uart_tx: tx_async, nvm, + tc_tx, + tc_rx, + tm_tx, + tm_rx, }, ) } @@ -174,285 +137,156 @@ mod app { #[task( binds = OC0, local = [ - cnt: u32 = 0, - rx_buf: [u8; MAX_TC_FRAME_SIZE] = [0; MAX_TC_FRAME_SIZE], - rx_context, uart_rx, + tc_tx ], - shared = [tc_rb] )] - fn uart_rx_irq(mut cx: uart_rx_irq::Context) { - match cx - .local - .uart_rx - .on_interrupt_max_size_or_timeout_based(cx.local.rx_context, cx.local.rx_buf) - { - Ok(result) => { - if RX_DEBUGGING { - defmt::debug!("RX Info: {:?}", cx.local.rx_context); - defmt::debug!("RX Result: {:?}", result); + fn uart_irq(cx: uart_irq::Context) { + let mut buf: [u8; 16] = [0; 16]; + let result = cx.local.uart_rx.on_interrupt(&mut buf); + if result.bytes_read > 0 { + let mut written_so_far = 0; + while written_so_far < result.bytes_read { + let write_result = cx + .local + .tc_tx + .try_write(&buf[written_so_far..result.bytes_read]); + if write_result.is_err() { + defmt::warn!("TC pipe full, dropping bytes"); + break; } - 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() { - defmt::warn!("COBS decoding failed"); - } else { - let decoded_size = decoded_size.unwrap(); - let mut tc_rb_full = false; - cx.shared.tc_rb.lock(|rb| { - if rb.sizes.vacant_len() >= 1 && rb.buf.vacant_len() >= decoded_size - { - rb.sizes.try_push(decoded_size).unwrap(); - rb.buf.push_slice(&cx.local.rx_buf[1..1 + decoded_size]); - } else { - tc_rb_full = true; - } - }); - if tc_rb_full { - defmt::warn!("COBS TC queue full"); - } - } - } else { - defmt::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() { - defmt::warn!("UART error: {:?}", result.errors.unwrap()); - } - } - Err(e) => { - defmt::warn!("UART error: {:?}", e); + written_so_far += write_result.unwrap(); } } + va108xx_hal::uart::tx_async::on_interrupt_tx(UART_BANK); } #[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], nvm, - verif_reporter + tc_rx, + tm_tx ], - shared=[tm_rb, tc_rb] )] - async fn pus_tc_handler(mut cx: pus_tc_handler::Context) { + async fn tc_handler(cx: tc_handler::Context) { + let mut read_buf: [u8; 64] = [0; 64]; + let mut tm_buf: [u8; MAX_TM_FRAME_SIZE] = [0; MAX_TM_FRAME_SIZE]; + let mut encoded_tm_buf: [u8; MAX_TM_FRAME_SIZE] = [0; MAX_TM_FRAME_SIZE]; + let mut cobs_decoder = CobsDecoderHeapless::::new(); loop { - // Try to read a TC from the ring buffer. - let packet_len = cx.shared.tc_rb.lock(|rb| rb.sizes.try_pop()); - if packet_len.is_none() { - // Small delay, TCs might arrive very quickly. - Mono::delay(20_u32.millis()).await; - continue; + let read_bytes = cx.local.tc_rx.read(&mut read_buf).await; + for &byte in read_buf[0..read_bytes].iter() { + match cobs_decoder.feed(byte) { + Ok(result) => { + if result.is_none() { + continue; + } + let frame = result.unwrap(); + match CcsdsPacketReader::new_with_checksum(&cobs_decoder.dest()[0..frame]) { + Ok(packet) => { + let request = postcard::take_from_bytes::( + packet.user_data(), + ); + if request.is_err() { + defmt::warn!( + "Failed to parse command: {}", + request.err().unwrap() + ); + continue; + } + let (request, remainder) = request.unwrap(); + let response = match request { + models::Request::Corrupt(slot) => { + match slot { + models::AppSel::A => { + defmt::info!("corrupting App Image A"); + corrupt_image(APP_A_START_ADDR, cx.local.nvm); + } + models::AppSel::B => { + defmt::info!("corrupting App Image B"); + corrupt_image(APP_B_START_ADDR, cx.local.nvm); + } + } + Response::Ok + } + models::Request::WriteNvm { offset } => { + defmt::info!( + "writing {} bytes to NVM at offset 0x{:08x}", + remainder.len(), + offset + ); + cx.local.nvm.write(offset as usize, remainder); + defmt::info!("write complete"); + Response::Ok + } + models::Request::SetBootSlot(app_sel) => { + defmt::info!( + "received boot selection command with app select: {:?}", + app_sel + ); + cx.local.nvm.write( + PREFERRED_SLOT_OFFSET as usize, + &[app_sel as u8], + ); + Response::Ok + } + models::Request::Ping => { + defmt::info!("received ping TC"); + Response::Ok + } + }; + match create_encoded_tm_packet( + &mut tm_buf, + &mut encoded_tm_buf, + SpacePacketHeader::new_from_apid(models::APID), + response, + ) { + Ok(encoded_len) => { + cx.local + .tm_tx + .write_all(&encoded_tm_buf[0..encoded_len]) + .await; + } + Err(e) => { + defmt::warn!("Failed to create or encode TM packet: {}", e); + } + } + } + Err(e) => { + defmt::warn!("CCSDS packet error: {}", e); + } + } + } + Err(e) => { + defmt::warn!("COBS decoding error: {}", e); + } + } } - let packet_len = packet_len.unwrap(); - defmt::info!("received packet with length {}", packet_len); - let popped_packet_len = cx - .shared - .tc_rb - .lock(|rb| rb.buf.pop_slice(&mut cx.local.tc_buf[0..packet_len])); - assert_eq!(popped_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 let Err(e) = pus_tc { - defmt::warn!("PUS TC error: {}", e); - 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_rb.lock(|prod| { - prod.sizes.try_push(tm.len_written()).unwrap(); - prod.buf.push_slice(&cx.local.verif_buf[0..written_size]); - }); - }; - let request_id = cx.local.verif_reporter.read_request_id(&pus_tc); - let tm = cx - .local - .verif_reporter - .acceptance_success(cx.local.src_data_buf, &request_id, u14::ZERO, 0, &[]) - .expect("acceptance success failed"); - write_and_send(&tm); - - let tm = cx - .local - .verif_reporter - .start_success(cx.local.src_data_buf, &request_id, u14::ZERO, 0, &[]) - .expect("acceptance success failed"); - write_and_send(&tm); - - if pus_tc.service_type_id() == 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); - buf[0] += 1; - cx.local.nvm.write(base_addr as usize + 32, &buf); - let tm = cx - .local - .verif_reporter - .completion_success(cx.local.src_data_buf, &request_id, u14::ZERO, 0, &[]) - .expect("completion success failed"); - write_and_send(&tm); - }; - if pus_tc.message_subtype_id() == ActionId::CorruptImageA as u8 { - defmt::info!("corrupting App Image A"); - corrupt_image(APP_A_START_ADDR); - } - if pus_tc.message_subtype_id() == ActionId::CorruptImageB as u8 { - defmt::info!("corrupting App Image B"); - corrupt_image(APP_B_START_ADDR); - } - if pus_tc.message_subtype_id() == ActionId::SetBootSlot as u8 { - if pus_tc.app_data().is_empty() { - defmt::warn!("App data for preferred image command too short"); - } - let app_sel_result = AppSel::try_from(pus_tc.app_data()[0]); - if app_sel_result.is_err() { - defmt::warn!("Invalid app selection value: {}", pus_tc.app_data()[0]); - } - defmt::info!( - "received boot selection command with app select: {:?}", - app_sel_result.unwrap() - ); - cx.local - .nvm - .write(PREFERRED_SLOT_OFFSET as usize, &[pus_tc.app_data()[0]]); - let tm = cx - .local - .verif_reporter - .completion_success(cx.local.src_data_buf, &request_id, u14::ZERO, 0, &[]) - .expect("completion success failed"); - write_and_send(&tm); - } - } - if pus_tc.service_type_id() == PusServiceId::Test as u8 && pus_tc.message_subtype_id() == 1 - { - defmt::info!("received ping TC"); - let tm = cx - .local - .verif_reporter - .completion_success(cx.local.src_data_buf, &request_id, u14::ZERO, 0, &[]) - .expect("completion success failed"); - write_and_send(&tm); - } else if pus_tc.service_type_id() == PusServiceId::MemoryManagement as u8 { - let tm = cx - .local - .verif_reporter - .step_success( - cx.local.src_data_buf, - &request_id, - u14::ZERO, - 0, - &[], - EcssEnumU8::new(0), - ) - .expect("step success failed"); - write_and_send(&tm); - // Raw memory write TC - if pus_tc.message_subtype_id() == 2 { - let app_data = pus_tc.app_data(); - if app_data.len() < 10 { - defmt::warn!( - "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 { - defmt::warn!("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() { - defmt::warn!( - "invalid data length {} for raw mem write detected", - data_len - ); - // TODO: Error reporting - return; - } - let data = &app_data[10..10 + data_len as usize]; - defmt::info!("writing {} bytes at offset {} to NVM", data_len, offset); - cx.local.nvm.write(offset as usize, data); - let tm = if !cx.local.nvm.verify(offset as usize, data) { - defmt::warn!("verification of data written to NVM failed"); - cx.local - .verif_reporter - .completion_failure( - cx.local.src_data_buf, - &request_id, - u14::ZERO, - 0, - FailParams::new(&[], &EcssEnumU8::new(0), &[]), - ) - .expect("completion success failed") - } else { - cx.local - .verif_reporter - .completion_success(cx.local.src_data_buf, &request_id, u14::ZERO, 0, &[]) - .expect("completion success failed") - }; - write_and_send(&tm); - defmt::info!("NVM operation done"); - } - } + pub fn corrupt_image(base_addr: u32, nvm: &mut M95M01) { + let mut buf = [0u8; 4]; + nvm.read(base_addr as usize + 32, &mut buf); + buf[0] += 1; + nvm.write(base_addr as usize + 32, &buf); } #[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_rx ], - shared=[tm_rb] )] - async fn pus_tm_tx_handler(mut cx: pus_tm_tx_handler::Context) { + async fn tm_tx_handler(cx: tm_tx_handler::Context) { + let mut buf: [u8; 256] = [0; 256]; loop { - let mut occupied_len = cx.shared.tm_rb.lock(|rb| rb.sizes.occupied_len()); - while occupied_len > 0 { - let next_size = cx.shared.tm_rb.lock(|rb| { - let next_size = rb.sizes.try_pop().unwrap(); - rb.buf.pop_slice(&mut cx.local.read_buf[0..next_size]); - 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_all(&cx.local.encoded_buf[0..send_size + 2]) - .unwrap(); - occupied_len -= 1; - Mono::delay(2.millis()).await; + let read_len = cx.local.tm_rx.read(&mut buf).await; + if let Err(e) = cx.local.uart_tx.write_all(&buf[0..read_len]).await { + defmt::warn!("UART TX overrun error: {}", e); } - Mono::delay(50.millis()).await; } } } diff --git a/va108xx/rust-toolchain.toml b/va108xx/rust-toolchain.toml new file mode 100644 index 0000000..c3d3073 --- /dev/null +++ b/va108xx/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "stable" +components = ["clippy", "llvm-tools"] +target = "thumbv6m-none-eabi" diff --git a/vorago-shared-hal/src/uart/mod.rs b/vorago-shared-hal/src/uart/mod.rs index e6f1bd0..56cda9e 100644 --- a/vorago-shared-hal/src/uart/mod.rs +++ b/vorago-shared-hal/src/uart/mod.rs @@ -47,6 +47,9 @@ pub use tx_async::*; pub mod rx_async; pub use rx_async::*; +/// FIFO depth of the UART for both the RX and TX FIFO. +pub const FIFO_DEPTH: usize = 16; + //================================================================================================== // Type-Level support //================================================================================================== @@ -731,7 +734,7 @@ impl Uart { Parity::Odd => (true, false), Parity::Even => (true, true), }; - reg_block.write_ctrl( + reg_block.write_control( Control::builder() .with_baud8(config.clock_config.baud_mode == BaudMode::_8) .with_auto_rts(false) @@ -813,14 +816,14 @@ impl Uart { } pub fn listen(&mut self, event: Event) { - self.tx.regs.modify_irq_enabled(|mut value| { + self.tx.regs.modify_interrupt_enable(|mut value| { match event { Event::RxError => value.set_rx_status(true), Event::RxFifoHalfFull => value.set_rx(true), Event::RxTimeout => value.set_rx_timeout(true), Event::TxEmpty => value.set_tx_empty(true), Event::TxError => value.set_tx_status(true), - Event::TxFifoHalfFull => value.set_tx(true), + Event::TxFifoHalfFull => value.set_tx_below_trigger(true), Event::TxCts => value.set_tx_cts(true), } value @@ -828,14 +831,14 @@ impl Uart { } pub fn unlisten(&mut self, event: Event) { - self.tx.regs.modify_irq_enabled(|mut value| { + self.tx.regs.modify_interrupt_enable(|mut value| { match event { Event::RxError => value.set_rx_status(false), Event::RxFifoHalfFull => value.set_rx(false), Event::RxTimeout => value.set_rx_timeout(false), Event::TxEmpty => value.set_tx_empty(false), Event::TxError => value.set_tx_status(false), - Event::TxFifoHalfFull => value.set_tx(false), + Event::TxFifoHalfFull => value.set_tx_below_trigger(false), Event::TxCts => value.set_tx_cts(false), } value @@ -904,7 +907,7 @@ pub fn disable_rx(uart: &mut MmioUart<'static>) { #[inline(always)] pub fn enable_rx_interrupts(uart: &mut MmioUart<'static>, timeout: bool) { - uart.modify_irq_enabled(|mut value| { + uart.modify_interrupt_enable(|mut value| { value.set_rx_status(true); value.set_rx(true); if timeout { @@ -916,7 +919,7 @@ pub fn enable_rx_interrupts(uart: &mut MmioUart<'static>, timeout: bool) { #[inline(always)] pub fn disable_rx_interrupts(uart: &mut MmioUart<'static>) { - uart.modify_irq_enabled(|mut value| { + uart.modify_interrupt_enable(|mut value| { value.set_rx_status(false); value.set_rx(false); value.set_rx_timeout(false); @@ -1035,6 +1038,13 @@ impl Rx { self.regs.read_data().raw_value() } + #[inline] + pub fn into_rx_with_interrupt(self) -> RxWithInterrupt { + RxWithInterrupt::new(self) + } + + #[deprecated(since = "0.3.0", note = "Use into_rx_with_interrupt instead")] + #[inline] pub fn into_rx_with_irq(self) -> RxWithInterrupt { RxWithInterrupt::new(self) } @@ -1101,9 +1111,9 @@ pub fn disable_tx(uart: &mut MmioUart<'static>) { } #[inline(always)] -pub fn enable_tx_interrupts(uart: &mut MmioUart<'static>) { - uart.modify_irq_enabled(|mut value| { - value.set_tx(true); +pub fn enable_tx_interrupts(tx_below_trigger: bool, uart: &mut MmioUart<'static>) { + uart.modify_interrupt_enable(|mut value| { + value.set_tx_below_trigger(tx_below_trigger); value.set_tx_empty(true); value.set_tx_status(true); value @@ -1112,8 +1122,8 @@ pub fn enable_tx_interrupts(uart: &mut MmioUart<'static>) { #[inline(always)] pub fn disable_tx_interrupts(uart: &mut MmioUart<'static>) { - uart.modify_irq_enabled(|mut value| { - value.set_tx(false); + uart.modify_interrupt_enable(|mut value| { + value.set_tx_below_trigger(false); value.set_tx_empty(false); value.set_tx_status(false); value @@ -1176,18 +1186,24 @@ impl Tx { /// Enables the IRQ_TX, IRQ_TX_STATUS and IRQ_TX_EMPTY interrupts. /// - /// - The IRQ_TX interrupt is generated when the TX FIFO is at least half empty. + /// - The IRQ_TX interrupt is generated when the TX FIFO is at least half empty and the + /// `tx_below_trigger` parameter is set to `true`. This should be set to true if the total + /// amount of data to be transmitted is larger than the FIFO size. /// - The IRQ_TX_STATUS interrupt is generated when write data is lost due to a FIFO overflow /// - The IRQ_TX_EMPTY interrupt is generated when the TX FIFO is empty and the TXBUSY signal /// is 0 #[inline] - pub fn enable_interrupts(&mut self, #[cfg(feature = "vor4x")] enable_in_nvic: bool) { + pub fn enable_interrupts( + &mut self, + tx_below_trigger: bool, + #[cfg(feature = "vor4x")] enable_in_nvic: bool, + ) { #[cfg(feature = "vor4x")] if enable_in_nvic { unsafe { enable_nvic_interrupt(self.id.interrupt_id_tx()) }; } // Safety: We own the UART structure - enable_tx_interrupts(&mut self.regs); + enable_tx_interrupts(tx_below_trigger, &mut self.regs); } /// Disables the IRQ_TX, IRQ_TX_STATUS and IRQ_TX_EMPTY interrupts. @@ -1297,6 +1313,7 @@ impl embedded_io::Write for Tx { pub struct RxWithInterrupt(Rx); impl RxWithInterrupt { + #[inline] pub fn new(rx: Rx) -> Self { Self(rx) } @@ -1378,8 +1395,8 @@ impl RxWithInterrupt { pub fn on_interrupt(&mut self, buf: &mut [u8; 16]) -> InterruptResult { let mut result = InterruptResult::default(); - let irq_status = self.0.regs.read_irq_status(); - let irq_enabled = self.0.regs.read_irq_enabled(); + let irq_status = self.0.regs.read_interrupt_status(); + let irq_enabled = self.0.regs.read_interrupt_enable(); let rx_enabled = irq_enabled.rx(); // Half-Full interrupt. We have a guaranteed amount of data we can read. @@ -1443,7 +1460,7 @@ impl RxWithInterrupt { } let mut result = InterruptResultMaxSizeOrTimeout::default(); - let irq_status = self.0.regs.read_irq_status(); + let irq_status = self.0.regs.read_interrupt_status(); let rx_enabled = self.0.regs.read_enable().rx(); // Half-Full interrupt. We have a guaranteed amount of data we can read. diff --git a/vorago-shared-hal/src/uart/regs.rs b/vorago-shared-hal/src/uart/regs.rs index 9cc353a..24802ed 100644 --- a/vorago-shared-hal/src/uart/regs.rs +++ b/vorago-shared-hal/src/uart/regs.rs @@ -188,7 +188,7 @@ pub struct InterruptControl { /// Generates an interrupt when the TX FIFO is at least half-empty (FIFO count < trigger level) #[bit(4, rw)] - tx: bool, + tx_below_trigger: bool, /// Generates an interrupt on TX FIFO overflow. #[bit(5, rw)] tx_status: bool, @@ -213,7 +213,7 @@ pub struct InterruptStatus { /// Generates an interrupt when the TX FIFO is at least half-empty (FIFO count < trigger level) #[bit(4, r)] - tx: bool, + tx_below_trigger: bool, /// Generates an interrupt on TX FIFO overflow. #[bit(5, r)] tx_status: bool, @@ -262,7 +262,7 @@ pub struct State { pub struct Uart { data: Data, enable: Enable, - ctrl: Control, + control: Control, clkscale: ClockScale, #[mmio(PureRead)] rx_status: RxStatus, @@ -274,11 +274,11 @@ pub struct Uart { txbreak: u32, addr9: u32, addr9mask: u32, - irq_enabled: InterruptControl, + interrupt_enable: InterruptControl, #[mmio(PureRead)] - irq_raw: InterruptStatus, + interrupt_raw: InterruptStatus, #[mmio(PureRead)] - irq_status: InterruptStatus, + interrupt_status: InterruptStatus, #[mmio(Write)] irq_clr: InterruptClear, rx_fifo_trigger: FifoTrigger, diff --git a/vorago-shared-hal/src/uart/rx_async.rs b/vorago-shared-hal/src/uart/rx_async.rs index a93ea8f..9f89b6f 100644 --- a/vorago-shared-hal/src/uart/rx_async.rs +++ b/vorago-shared-hal/src/uart/rx_async.rs @@ -134,8 +134,8 @@ pub fn on_interrupt_rx_async_heapless_queue_overwriting( shared_consumer: &Mutex>>>, ) -> Result<(), AsyncUartErrors> { let uart_regs = unsafe { bank.steal_regs() }; - let irq_status = uart_regs.read_irq_status(); - let irq_enabled = uart_regs.read_irq_enabled(); + let irq_status = uart_regs.read_interrupt_status(); + let irq_enabled = uart_regs.read_interrupt_enable(); let rx_enabled = irq_enabled.rx(); let mut read_some_data = false; let mut queue_overflow = false; @@ -202,8 +202,8 @@ pub fn on_interrupt_rx_async_heapless_queue( prod: &mut heapless::spsc::Producer<'_, u8>, ) -> Result<(), AsyncUartErrors> { let uart_regs = unsafe { bank.steal_regs() }; - let irq_status = uart_regs.read_irq_status(); - let irq_enabled = uart_regs.read_irq_enabled(); + let irq_status = uart_regs.read_interrupt_status(); + let irq_enabled = uart_regs.read_interrupt_enable(); let rx_enabled = irq_enabled.rx(); let mut read_some_data = false; let mut queue_overflow = false; diff --git a/vorago-shared-hal/src/uart/tx_async.rs b/vorago-shared-hal/src/uart/tx_async.rs index 21b553f..e8cfbff 100644 --- a/vorago-shared-hal/src/uart/tx_async.rs +++ b/vorago-shared-hal/src/uart/tx_async.rs @@ -38,13 +38,14 @@ fn tx_is_drained(tx: &Tx) -> bool { pub fn on_interrupt_tx(bank: Bank) { let mut uart = unsafe { bank.steal_regs() }; let idx = bank as usize; - let irq_enabled = uart.read_irq_enabled(); + let irq_enabled = uart.read_interrupt_enable(); // IRQ is not related to TX. - if !irq_enabled.tx() && !irq_enabled.tx_empty() { + if !irq_enabled.tx_below_trigger() && !irq_enabled.tx_empty() { return; } let tx_status = uart.read_tx_status(); + let interrupt_status = uart.read_interrupt_status(); let unexpected_overrun = tx_status.wr_lost(); let mut context = critical_section::with(|cs| { let context_ref = TX_CONTEXTS[idx].borrow(cs); @@ -54,17 +55,14 @@ pub fn on_interrupt_tx(bank: Bank) { // Safety: We documented that the user provided slice must outlive the future, so we convert // the raw pointer back to the slice here. let slice = unsafe { context.slice.get().unwrap() }; - if context.progress >= slice.len() && !tx_status.tx_busy() { - uart.modify_irq_enabled(|mut value| { - value.set_tx(false); - value.set_tx_empty(false); - value.set_tx_status(false); - value - }); - uart.modify_enable(|mut value| { - value.set_tx(false); + if context.progress >= slice.len() && interrupt_status.tx_empty() { + uart.modify_interrupt_enable(|value| { value + .with_tx_below_trigger(false) + .with_tx_empty(false) + .with_tx_status(false) }); + uart.modify_enable(|value| value.with_tx(false)); // Write back updated context structure. critical_section::with(|cs| { let context_ref = TX_CONTEXTS[idx].borrow(cs); @@ -84,6 +82,10 @@ pub fn on_interrupt_tx(bank: Bank) { uart.write_data(Data::new_with_raw_value(slice[context.progress] as u32)); context.progress += 1; } + // Now we only require the TX empty interrupt. + if context.progress == slice.len() { + uart.modify_interrupt_enable(|value| value.with_tx_below_trigger(false)); + } // Write back updated context structure. critical_section::with(|cs| { @@ -125,7 +127,7 @@ impl TxFuture { tx.disable(); tx.clear_fifo(); - let init_fill_count = core::cmp::min(data.len(), 16); + let init_fill_count = core::cmp::min(data.len(), FIFO_DEPTH); // We fill the FIFO. for data in data.iter().take(init_fill_count) { tx.regs.write_data(Data::new_with_raw_value(*data as u32)); @@ -139,6 +141,7 @@ impl TxFuture { // Ensure those are enabled inside a critical section at the same time. Can lead to // weird glitches otherwise. tx.enable_interrupts( + data.len() > init_fill_count, #[cfg(feature = "vor4x")] true, );