Compare commits

..
1 Commits
Author SHA1 Message Date
Robin Mueller ce016b1e3f embedded apps now connect to minisim 2026-09-30 12:39:54 +02:00
44 changed files with 1109 additions and 2551 deletions

No files matched your search

+1 -2
View File
@@ -13,6 +13,5 @@ members = [
]
exclude = [
"examples/stm32h7-nucleo-rtic",
"examples/stm32h7-nucleo-embassy",
"examples/embedded",
]
+5 -7
View File
@@ -51,13 +51,11 @@ All examples and their helper crates are located inside the
* [`client`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/client): Ground client to command the `example-std` application and the STM32H7 examples.
* [`types`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/types): Telecommand and telemetry definitions shared by the
`example-std` application, the STM32H7 examples and the `client`.
* [`stm32h7-nucleo-rtic`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/stm32h7-nucleo-rtic):
Simple example using sat-rs components on a bare-metal system with constrained resources.
This example uses the [RTIC](https://github.com/rtic-rs/rtic) framework on the NUCLEO-H753ZI
board.
* [`stm32h7-nucleo-embassy`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/stm32h7-nucleo-embassy):
Same as `stm32h7-nucleo-rtic`, but using the [embassy](https://embassy.dev/) executor instead
of RTIC.
* [`embedded`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/embedded):
Simple examples using sat-rs components on a bare-metal system with constrained resources,
running on the NUCLEO-H753ZI board. `stm32h7-nucleo-rtic` uses the
[RTIC](https://github.com/rtic-rs/rtic) framework and `stm32h7-nucleo-embassy` uses the
[embassy](https://embassy.dev/) executor.
The library crates and the `example-std` application have their own `CHANGELOG.md`.
+36
View File
@@ -67,6 +67,7 @@ enum Commands {
EventManager(EventManagerArgs),
/// Blinking LEDs of the embedded examples.
Led(LedArgs),
Sim(SimArgs),
}
#[derive(clap::Parser)]
@@ -75,6 +76,22 @@ struct EventManagerArgs {
action: EventFilterAction,
}
#[derive(clap::Parser)]
struct SimArgs {
#[command(subcommand)]
action: SimAction,
}
#[derive(clap::Subcommand)]
enum SimAction {
/// Let the OBSW connect to the minisim.
Connect {
/// IP address of the minisim host. Defaults to the address of this client if nothing
/// is specified.
ip: Option<Ipv4Addr>,
},
}
#[derive(clap::Subcommand)]
enum EventFilterAction {
/// Enable event TM generation.
@@ -341,6 +358,24 @@ fn send_led_request(client: &UdpSocket, addr: SocketAddr, request: types::led::r
client.send_to(&packet.to_vec(), addr).unwrap();
}
fn handle_sim_command(client: &UdpSocket, addr: SocketAddr, args: SimArgs) {
let request = match args.action {
SimAction::Connect { ip } => types::control::request::Request::SimConnect(ip),
};
let packet = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
TcHeader::new(types::ComponentId::Controller, MessageType::Action),
request,
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
log::info!(
"sending SIM request {:?} with TC ID {:#010x}",
request,
sent_tc_id.raw()
);
client.send_to(&packet.to_vec(), addr).unwrap();
}
fn handle_led_command(client: &UdpSocket, addr: SocketAddr, args: LedArgs) {
use types::led::request::Request;
@@ -676,6 +711,7 @@ fn main() -> anyhow::Result<()> {
}
Commands::EventManager(args) => handle_event_manager_command(&client, addr, args),
Commands::Led(args) => handle_led_command(&client, addr, args),
Commands::Sim(args) => handle_sim_command(&client, addr, args),
}
}
@@ -27,3 +27,6 @@ rrb = "run --release --bin"
[env]
DEFMT_LOG = "info"
# IP address of the host running the minisim. Without it, the firmware does not try to connect
# to a simulator.
# SIM_IP_ADDR = "192.168.1.10"
+3
View File
@@ -0,0 +1,3 @@
/.cargo/config.toml
.vscode/
app.map
@@ -557,6 +557,7 @@ dependencies = [
"defmt 1.1.0",
"document-features",
"embassy-time-driver",
"embassy-time-queue-utils",
"embedded-hal 0.2.7",
"embedded-hal 1.0.0",
"embedded-hal-async",
@@ -737,6 +738,12 @@ dependencies = [
"embedded-storage",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "find-msvc-tools"
version = "0.1.14"
@@ -811,6 +818,12 @@ dependencies = [
"byteorder",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "heapless"
version = "0.7.17"
@@ -849,6 +862,16 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "indexmap"
version = "2.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
@@ -922,6 +945,17 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "minisim-types"
version = "0.1.0"
dependencies = [
"num_enum",
"serde",
"tai-time",
"thiserror",
"types",
]
[[package]]
name = "nb"
version = "0.1.3"
@@ -1117,6 +1151,37 @@ dependencies = [
"svgbobdoc",
]
[[package]]
name = "rtic"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16dff5e56cf22c25d0223e8abc0f60b428c577d4dd4a2459e55eeb289165106c"
dependencies = [
"cortex-m",
"critical-section",
"portable-atomic",
"rtic-core",
"rtic-macros",
]
[[package]]
name = "rtic-core"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9369355b04d06a3780ec0f51ea2d225624db777acbc60abd8ca4832da5c1a42"
[[package]]
name = "rtic-macros"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14db06b1d38d3591975d0971b2a5cde129767a93f5815fb01c99efa8d1898c16"
dependencies = [
"indexmap",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "rustc_version"
version = "0.2.3"
@@ -1251,6 +1316,24 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "shared-embedded"
version = "0.1.0"
dependencies = [
"arbitrary-int",
"defmt 1.1.0",
"embassy-futures",
"embassy-net",
"embassy-stm32",
"embassy-sync",
"embassy-time",
"minisim-types",
"postcard",
"serde",
"spacepackets",
"types",
]
[[package]]
name = "shlex"
version = "2.0.1"
@@ -1351,25 +1434,38 @@ dependencies = [
name = "stm32h7-nucleo-embassy"
version = "0.1.0"
dependencies = [
"arbitrary-int",
"cortex-m",
"cortex-m-rt",
"defmt 1.1.0",
"defmt-rtt",
"defmt-test",
"embassy-executor",
"embassy-futures",
"embassy-net",
"embassy-stm32",
"embassy-sync",
"embassy-time",
"embedded-alloc",
"panic-probe",
"postcard",
"serde",
"spacepackets",
"shared-embedded",
"static_cell",
]
[[package]]
name = "stm32h7-nucleo-rtic"
version = "0.1.0"
dependencies = [
"cortex-m",
"cortex-m-rt",
"defmt 1.1.0",
"defmt-rtt",
"defmt-test",
"embassy-net",
"embassy-stm32",
"embassy-time",
"embedded-alloc",
"panic-probe",
"rtic",
"shared-embedded",
"static_cell",
"types",
]
[[package]]
@@ -1434,6 +1530,15 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "tai-time"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad64ffafdaee29ede4339e0cafd5f949c9a4e30be5ba6e25a41f711076b4e6c3"
dependencies = [
"serde",
]
[[package]]
name = "thiserror"
version = "2.0.18"
@@ -1,42 +1,37 @@
[package]
name = "stm32h7-nucleo-embassy"
edition = "2024"
version = "0.1.0"
default-run = "stm32h7-nucleo-embassy"
[workspace]
resolver = "3"
members = [
"shared-embedded",
"stm32h7-nucleo-embassy",
"stm32h7-nucleo-rtic",
]
[lib]
harness = false
# needed for each integration test
[[test]]
name = "integration"
harness = false
[dependencies]
[workspace.dependencies]
types = { path = "../types" }
minisim-types = { path = "../minisim-types" }
shared-embedded = { path = "shared-embedded" }
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
arbitrary-int = "2"
cortex-m-rt = "0.7"
arbitrary-int = "2"
defmt = "1"
defmt-rtt = "1"
defmt-test = "0.5"
panic-probe = { version = "1", features = ["print-defmt"] }
embedded-alloc = "0.7"
static_cell = "2"
spacepackets = { version = "0.18", default-features = false, features = ["defmt"] }
postcard = "1"
serde = { version = "1", default-features = false }
rtic = { version = "2", features = ["thumbv7-backend"] }
embassy-stm32 = { version = "0.6", features = ["stm32h753zi", "memory-x", "defmt", "time-driver-any"] }
embassy-stm32 = { version = "0.6", features = ["stm32h753zi", "defmt"] }
embassy-executor = { version = "0.10", features = ["platform-cortex-m", "executor-thread", "defmt"] }
embassy-time = { version = "0.5", features = ["defmt-timestamp-uptime-ms"] }
embassy-time = "0.5"
embassy-net = { version = "0.9", features = ["medium-ethernet", "proto-ipv4", "tcp", "udp", "auto-icmp-echo-reply", "dhcpv4", "defmt"] }
embassy-sync = "0.8"
embassy-futures = "0.1"
[dev-dependencies]
defmt-test = "0.5"
# cargo build/run
[profile.dev]
codegen-units = 1
@@ -1,18 +1,22 @@
sat-rs embassy example for the STM32H753ZI-Nucleo board
sat-rs examples for the STM32H753ZI-Nucleo board
=======
This example application shows how the [sat-rs library](https://egit.irs.uni-stuttgart.de/rust/sat-rs)
These example applications show how the [sat-rs library](https://egit.irs.uni-stuttgart.de/rust/sat-rs)
can be used on an embedded target.
It also shows how a relatively simple OBSW could be built when no standard runtime is available.
It uses the [embassy](https://embassy.dev/) executor as the concurrency framework and the
[defmt](https://defmt.ferrous-systems.com/) framework for logging.
They also show how a relatively simple OBSW could be built when no standard runtime is available.
Both use the [defmt](https://defmt.ferrous-systems.com/) framework for logging and provide the
same functionality with a different concurrency framework:
This is the embassy variant of the
[`stm32h7-nucleo-rtic`](../stm32h7-nucleo-rtic) example. Both provide the same functionality.
- [`stm32h7-nucleo-embassy`](./stm32h7-nucleo-embassy) uses the [embassy](https://embassy.dev/)
executor.
- [`stm32h7-nucleo-rtic`](./stm32h7-nucleo-rtic) uses [RTIC](https://rtic.rs/2/book/en/).
The application code is shared inside the [`shared-embedded`](./shared-embedded) crate.
Each application only initializes the hardware and wraps the shared code inside its own tasks.
The STM32H753ZIT device was picked because it is one of the more powerful Cortex-M based STM32
devices. It has more RAM available and allows commanding via Ethernet. The example is written for
the NUCLEO-H753ZI board, which uses the MB1364 Nucleo-144 board layout.
devices. It has more RAM available and allows commanding via Ethernet. The examples are written
for the NUCLEO-H753ZI board, which uses the MB1364 Nucleo-144 board layout.
## Pre-Requisites
@@ -32,9 +36,11 @@ If you have not installed it yet, you can do so with
rustup target add thumbv7em-none-eabihf
```
A default `.cargo` config file is provided as `.cargo/config.toml.template`. The build script
copies it to `.cargo/config.toml` if that file does not exist yet. The copy is not tracked by git,
so you can change settings like the runner for your setup.
All crates in this directory form a separate workspace, because they are built for a different
target than the rest of the repository. They share one `.cargo` config file. A default is
provided as `.cargo/config.toml.template`. The build scripts copy it to `.cargo/config.toml`
if that file does not exist yet. The copy is not tracked by git, so you can change settings like
the runner for your setup.
Cargo reads the configuration before the build script runs, so the very first build on a fresh
checkout does not use it yet and might fail. Simply run the build again, or copy the file
@@ -50,18 +56,21 @@ the `--target` argument.
## Building
After that, assuming that you have a `.cargo/config.toml` setting the correct build target,
you can simply build the application with
you can build all applications from this directory with
```sh
cargo build
```
or a single application with `cargo build -p stm32h7-nucleo-embassy`, for example.
## Flashing from the command line
You can flash the application from the command line using `probe-rs`:
The configuration file sets `probe-rs` as the runner, so you can flash and run an application
with
```sh
probe-rs run --chip STM32H753ZITx
cargo run -p stm32h7-nucleo-embassy
```
## Debugging with VS Code
@@ -110,6 +119,34 @@ Use `cargo run -p client -- led --help` to list all modes.
You can also pass the board address with `--udp-addr` instead of setting it inside the
configuration file.
## Connecting to the mini simulator
The firmware can connect to the [`minisim`](../minisim), which simulates the devices of the OBSW.
The simulator address can be passed with a sim connect request:
```sh
cargo run -p client -- sim connect
```
Without an IP address, the firmware uses the sender address of the request, which fits the
common setup where the client and the simulator run on the same host. Otherwise, pass the IP
address of the simulator host, for example `sim connect 192.168.1.10`.
The simulator address can also be set at build time inside `.cargo/config.toml`, so the firmware
connects on its own after startup:
```toml
[env]
SIM_IP_ADDR = "192.168.1.10"
```
The firmware pings the simulator on UDP port 7303 and logs the result. It reconnects after a
network link loss. A new sim connect request, for example after restarting the simulator,
triggers a new connection attempt.
Like the `example-std` application, the device handlers use dummy interfaces if no simulator
address is known or the simulator does not reply. The simulator sends its replies to the
last client which contacted it, so only one application can use it at a time.
## Resources
- [STM32H743ZI Ethernet link checker example](https://github.com/stm32-rs/stm32h7xx-hal/blob/master/examples/ethernet-nucleo-h743zi2.rs)
@@ -0,0 +1,20 @@
[package]
name = "shared-embedded"
version = "0.1.0"
edition = "2024"
[dependencies]
types.workspace = true
minisim-types.workspace = true
arbitrary-int.workspace = true
defmt.workspace = true
spacepackets.workspace = true
postcard.workspace = true
serde.workspace = true
embassy-stm32.workspace = true
embassy-time.workspace = true
embassy-net.workspace = true
embassy-sync.workspace = true
embassy-futures.workspace = true
@@ -0,0 +1,68 @@
use embassy_futures::select::{Either, select};
use embassy_stm32::gpio;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::signal::Signal;
use embassy_time::{Duration, Timer};
use types::led;
pub static LED_MODE: Signal<CriticalSectionRawMutex, led::Mode> = Signal::new();
const HEARTBEAT_PERIOD: Duration = Duration::from_millis(500);
const DEFAULT_LED_MODE: led::Mode =
led::Mode::AlternatingToggle(core::time::Duration::from_millis(1000));
pub struct Leds {
pub red: gpio::Output<'static>,
pub orange: gpio::Output<'static>,
}
pub async fn heartbeat(led: &mut gpio::Output<'static>) {
loop {
led.toggle();
Timer::after(HEARTBEAT_PERIOD).await;
}
}
/// Applies the current mode to the red and orange LED. A new mode is applied immediately.
pub async fn led_task(leds: &mut Leds) {
let mut mode = DEFAULT_LED_MODE;
loop {
let toggle_period = match mode {
led::Mode::AllOff => {
leds.red.set_low();
leds.orange.set_low();
None
}
led::Mode::RedOn => {
leds.red.set_high();
leds.orange.set_low();
None
}
led::Mode::OrangeOn => {
leds.red.set_low();
leds.orange.set_high();
None
}
led::Mode::AlternatingToggle(period) => {
leds.red.toggle();
leds.orange.set_level((!leds.red.is_set_high()).into());
Some(period)
}
led::Mode::UnifiedToggle(period) => {
leds.red.toggle();
leds.orange.set_level(leds.red.is_set_high().into());
Some(period)
}
};
mode = match toggle_period {
Some(period) => {
let period = Duration::try_from(period).unwrap_or(Duration::MAX);
match select(Timer::after(period), LED_MODE.wait()).await {
Either::First(()) => mode,
Either::Second(new_mode) => new_mode,
}
}
None => LED_MODE.wait().await,
};
}
}
@@ -0,0 +1,9 @@
//! Application code shared by the STM32H7 Nucleo applications. Each application wraps the
//! async functions inside tasks of its own executor.
#![no_std]
extern crate alloc;
pub mod leds;
pub mod net;
pub mod sim_client;
pub mod tmtc;
@@ -0,0 +1,118 @@
use core::cell::Cell;
use embassy_futures::select::{Either3, select3};
use embassy_net::{
IpEndpoint,
udp::{PacketMetadata, UdpSocket},
};
use embassy_stm32::{eth, peripherals};
use embassy_sync::blocking_mutex::Mutex;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_time::Timer;
use crate::tmtc::{TcSender, TmReceiver};
const PORT: u16 = 7301;
const MTU: usize = 1500;
/// Locally administered MAC address
pub const MAC_ADDRESS: [u8; 6] = [0x02, 0x00, 0x11, 0x22, 0x33, 0x44];
pub static LAST_SENDER: Mutex<CriticalSectionRawMutex, Cell<Option<core::net::Ipv4Addr>>> =
Mutex::new(Cell::new(None));
pub type Device = eth::Ethernet<
'static,
peripherals::ETH,
eth::GenericPhy<eth::Sma<'static, peripherals::ETH_SMA>>,
>;
pub async fn net_stack_task(runner: &mut embassy_net::Runner<'static, Device>) -> ! {
runner.run().await
}
pub async fn udp_task(stack: embassy_net::Stack<'static>, tc_tx: TcSender, tm_rx: TmReceiver) {
// Task futures are allocated statically, so these buffers do not live on the stack.
let mut rx_udp_meta = [PacketMetadata::EMPTY; 8];
let mut tx_udp_meta = [PacketMetadata::EMPTY; 8];
let mut rx_udp_buf = [0; MTU];
let mut tx_udp_buf = [0; MTU];
let mut rx_buffer = [0; MTU];
loop {
stack.wait_link_up().await;
defmt::info!("Network link is up");
// Ensure DHCP configuration is up before trying connect
stack.wait_config_up().await;
defmt::info!("Network task initialized, config: {}", stack.config_v4());
let mut udp = UdpSocket::new(
stack,
&mut rx_udp_meta,
&mut rx_udp_buf,
&mut tx_udp_meta,
&mut tx_udp_buf,
);
if let Err(e) = udp.bind(PORT) {
defmt::error!("Failed to bind UDP socket: {}", e);
Timer::after_secs(1).await;
continue;
}
defmt::info!("UDP socket bound to port {}", PORT);
let mut remote_endpoint = None;
loop {
match select3(
udp.recv_from(&mut rx_buffer),
tm_rx.receive(),
stack.wait_link_down(),
)
.await
{
Either3::First(Ok((len, meta))) => {
remote_endpoint = Some(meta.endpoint);
let embassy_net::IpAddress::Ipv4(sender_ip) = meta.endpoint.addr;
LAST_SENDER.lock(|val| {
val.set(Some(sender_ip));
});
defmt::debug!("UDP RX {}, Meta: {}", len, meta);
tc_tx.send(rx_buffer[0..len].to_vec()).await;
// Incoming TCs take priority in the select. Draining TM here prevents a burst
// of TCs from overflowing the TM queue. This could lead to a task deadlock
// where each task is waiting on each other.
while let Ok(packet) = tm_rx.try_receive() {
handle_tm(&packet, &mut udp, &remote_endpoint).await;
}
}
Either3::First(Err(e)) => {
defmt::warn!("udp receive error: {}", e);
Timer::after_millis(100).await;
}
// TM is only generated as a response to a TC, so the endpoint is usually known.
Either3::Second(packet) => handle_tm(&packet, &mut udp, &remote_endpoint).await,
Either3::Third(()) => {
defmt::warn!("Network link is down");
break;
}
}
}
}
}
async fn handle_tm(
packet: &[u8],
udp_socket: &mut UdpSocket<'_>,
remote_endpoint: &Option<IpEndpoint>,
) {
match remote_endpoint {
Some(endpoint) => match udp_socket.send_to(packet, *endpoint).await {
Ok(_) => {
defmt::debug!("UDP TX: {} bytes to: {}", packet.len(), endpoint)
}
Err(e) => defmt::warn!("udp send error: {}", e),
},
None => defmt::warn!("dropping TM, no remote endpoint known"),
};
}
@@ -0,0 +1,116 @@
//! UDP client for the minisim, which simulates the devices of the OBSW.
use core::net::{Ipv4Addr, SocketAddrV4};
use defmt::Debug2Format;
use embassy_futures::select::{Either, select};
use embassy_net::udp::{PacketMetadata, UdpSocket};
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::signal::Signal;
use embassy_sync::watch::Watch;
use embassy_time::{Duration, WithTimeout as _};
use minisim_types::udp::SIM_CTRL_PORT;
use minisim_types::{SimCtrlReply, SimCtrlRequest, SimReply, SimRequestWithTime};
/// Set by a sim connect request. Each new address triggers a connection attempt.
pub static SIM_HOST: Signal<CriticalSectionRawMutex, Ipv4Addr> = Signal::new();
/// IP address of the host running the minisim, set at build time. Without it, no connection
/// to a simulator is attempted until a sim connect request is received.
const SIM_FROM_ENV: Option<&str> = option_env!("SIM_IP_ADDR");
const PING_ATTEMPTS: usize = 3;
const PING_TIMEOUT: Duration = Duration::from_millis(500);
const MAX_PACKET_LEN: usize = 1024;
/// Result of the connection check. Device handlers use it to pick either the simulator or a
/// dummy interface.
pub static SIM_AVAILABLE: Watch<CriticalSectionRawMutex, bool, 4> = Watch::new();
pub async fn sim_client_task(stack: embassy_net::Stack<'static>) {
let mut rx_meta = [PacketMetadata::EMPTY; 4];
let mut tx_meta = [PacketMetadata::EMPTY; 4];
let mut rx_buf = [0; MAX_PACKET_LEN];
let mut tx_buf = [0; MAX_PACKET_LEN];
let sim_available = SIM_AVAILABLE.sender();
sim_available.send(false);
// Bound to an ephemeral port without a fixed local address, so the socket survives a new
// DHCP lease and does not need to be recreated after a link loss.
let mut udp = UdpSocket::new(stack, &mut rx_meta, &mut rx_buf, &mut tx_meta, &mut tx_buf);
if let Err(e) = udp.bind(0) {
defmt::error!("Failed to bind simulator UDP socket: {}", e);
return;
}
let mut sim_addr = sim_addr_from_env();
loop {
stack.wait_link_up().await;
stack.wait_config_up().await;
loop {
if let Some(addr) = sim_addr {
let connected = check_connection(&udp, addr).await;
if connected {
defmt::info!("Connected to simulator at {}", Debug2Format(&addr));
} else {
defmt::warn!(
"Simulator at {} not reachable, using dummy interfaces",
Debug2Format(&addr)
);
}
sim_available.send(connected);
}
match select(SIM_HOST.wait(), stack.wait_link_down()).await {
Either::First(ip_addr) => {
sim_addr = Some(SocketAddrV4::new(ip_addr, SIM_CTRL_PORT));
}
Either::Second(()) => {
sim_available.send(false);
break;
}
}
}
}
}
fn sim_addr_from_env() -> Option<SocketAddrV4> {
let ip_addr = SIM_FROM_ENV?;
match ip_addr.parse::<Ipv4Addr>() {
Ok(ip_addr) => Some(SocketAddrV4::new(ip_addr, SIM_CTRL_PORT)),
Err(_) => {
defmt::error!("Invalid simulator IP address {}", ip_addr);
None
}
}
}
/// Uses several attempts, because the first packet can get lost while the MAC address of the
/// simulator host is resolved.
async fn check_connection(udp: &UdpSocket<'_>, sim_addr: SocketAddrV4) -> bool {
let mut tx_buf = [0; MAX_PACKET_LEN];
let mut rx_buf = [0; MAX_PACKET_LEN];
let request = SimRequestWithTime::new_with_epoch_time(SimCtrlRequest::Ping);
let ping = match postcard::to_slice(&request, &mut tx_buf) {
Ok(ping) => ping,
Err(e) => {
defmt::error!("Failed to serialize simulator ping: {}", Debug2Format(&e));
return false;
}
};
for _ in 0..PING_ATTEMPTS {
if let Err(e) = udp.send_to(ping, sim_addr).await {
defmt::warn!("Failed to send simulator ping: {}", e);
continue;
}
if let Ok(Ok((len, _))) = udp.recv_from(&mut rx_buf).with_timeout(PING_TIMEOUT).await
&& matches!(
postcard::from_bytes::<SimReply>(&rx_buf[..len]),
Ok(SimReply::SimCtrl(SimCtrlReply::Pong))
)
{
return true;
}
}
false
}
@@ -0,0 +1,157 @@
use alloc::vec::Vec;
use arbitrary_int::u14;
use defmt::Debug2Format;
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
use embassy_sync::channel::{Channel, Receiver, Sender};
use spacepackets::{CcsdsPacketIdAndPsc, CcsdsPacketReader, SpHeader};
use types::ccsds::{CcsdsCreationError, CcsdsTmPacketOwned};
use types::{Apid, ComponentId, Message, TcHeader, TmHeader, control, led, tmtc};
use crate::leds::LED_MODE;
use crate::net::LAST_SENDER;
use crate::sim_client;
pub const TC_QUEUE_DEPTH: usize = 32;
pub const TM_QUEUE_DEPTH: usize = 32;
pub type TcChannel = Channel<NoopRawMutex, Vec<u8>, TC_QUEUE_DEPTH>;
pub type TmChannel = Channel<NoopRawMutex, Vec<u8>, TM_QUEUE_DEPTH>;
pub type TcSender = Sender<'static, NoopRawMutex, Vec<u8>, TC_QUEUE_DEPTH>;
pub type TcReceiver = Receiver<'static, NoopRawMutex, Vec<u8>, TC_QUEUE_DEPTH>;
pub type TmSender = Sender<'static, NoopRawMutex, Vec<u8>, TM_QUEUE_DEPTH>;
pub type TmReceiver = Receiver<'static, NoopRawMutex, Vec<u8>, TM_QUEUE_DEPTH>;
pub async fn tc_handler(tc_rx: TcReceiver, telemetry: &mut Telemetry) {
loop {
let tc = tc_rx.receive().await;
let packet = match CcsdsPacketReader::new_with_checksum(&tc) {
Ok(packet) => packet,
Err(e) => {
defmt::warn!("Failed to parse received TC packet: {}", e);
send_tmtc_event(telemetry, tmtc::Event::InvalidTcPacket).await;
continue;
}
};
let tc_id = CcsdsPacketIdAndPsc {
packet_id: packet.packet_id(),
psc: packet.psc(),
};
let Ok((tc_header, payload)) = postcard::take_from_bytes::<TcHeader>(packet.user_data())
else {
defmt::warn!("Failed to deserialize TC header");
send_tmtc_event(telemetry, tmtc::Event::InvalidTcHeader).await;
continue;
};
match tc_header.target_id {
ComponentId::Controller => handle_controller_tc(payload, tc_id, telemetry).await,
ComponentId::Led => handle_led_tc(payload, tc_id, telemetry).await,
target_id => {
defmt::warn!("No TC handler for target ID {}", Debug2Format(&target_id));
send_tmtc_event(telemetry, tmtc::Event::UnknownTargetId(target_id)).await;
}
}
}
}
/// All TCs are received via UDP, so the UDP server is the sender of TMTC events.
async fn send_tmtc_event(telemetry: &mut Telemetry, event: tmtc::Event) {
telemetry.send(ComponentId::UdpServer, None, &event).await;
}
/// The controller does not control anything yet, but handles generic requests like pings.
async fn handle_controller_tc(
payload: &[u8],
tc_id: CcsdsPacketIdAndPsc,
telemetry: &mut Telemetry,
) {
let Ok(request) = postcard::from_bytes::<control::request::Request>(payload) else {
defmt::warn!("Failed to deserialize controller request");
return;
};
match request {
control::request::Request::Ping => defmt::info!("Received controller ping request"),
control::request::Request::TestEvent => {
defmt::info!("Received test event request");
let event = types::Event::ControllerEvent(control::Event::TestEvent);
telemetry.send(ComponentId::Controller, None, &event).await;
}
control::request::Request::SimConnect(opt_ipv4_addr) => match opt_ipv4_addr {
Some(ipv4_addr) => sim_client::SIM_HOST.signal(ipv4_addr),
None => {
// If the UDP socket has received anything, the last sender should be stored and
// we use that IP address.
let opt_last_sender = LAST_SENDER.lock(|val| val.clone());
match opt_last_sender.get() {
Some(last_sender) => sim_client::SIM_HOST.signal(last_sender),
None => defmt::warn!("SimConnect without IP and no known sender"),
}
}
},
}
telemetry
.send(
ComponentId::Controller,
Some(tc_id),
&control::response::Response::Ok,
)
.await;
}
async fn handle_led_tc(payload: &[u8], tc_id: CcsdsPacketIdAndPsc, telemetry: &mut Telemetry) {
let Ok(request) = postcard::from_bytes::<led::request::Request>(payload) else {
defmt::warn!("Failed to deserialize LED request");
return;
};
match request {
led::request::Request::Ping => defmt::info!("Received LED ping request"),
led::request::Request::SetMode(mode) => {
defmt::info!("Received LED mode request: {}", Debug2Format(&mode));
LED_MODE.signal(mode);
}
}
telemetry
.send(ComponentId::Led, Some(tc_id), &led::response::Response::Ok)
.await;
}
/// Packs TM and passes it to the UDP task.
pub struct Telemetry {
tx: TmSender,
sequence_count: u14,
}
impl Telemetry {
pub fn new(tx: TmSender) -> Self {
Self {
tx,
sequence_count: u14::new(0),
}
}
/// TM without a TC ID is sent unsolicited, for example events.
async fn send(
&mut self,
sender_id: ComponentId,
tc_id: Option<CcsdsPacketIdAndPsc>,
payload: &(impl serde::Serialize + Message),
) {
let sp_header = SpHeader::new_for_unseg_tm(Apid::Tmtc.raw_value(), self.sequence_count, 0);
let tm_header = TmHeader::new_without_timestamp(
sender_id,
ComponentId::Ground,
payload.message_type(),
tc_id,
);
match CcsdsTmPacketOwned::new_with_serde_payload(sp_header, &tm_header, payload)
.map_err(CcsdsCreationError::from)
.and_then(|packet| packet.try_to_vec())
{
Ok(raw_packet) => {
self.tx.send(raw_packet).await;
self.sequence_count = self.sequence_count.wrapping_add(u14::new(1));
}
Err(e) => defmt::warn!("Failed to create TM packet: {}", Debug2Format(&e)),
}
}
}
@@ -0,0 +1,32 @@
[package]
name = "stm32h7-nucleo-embassy"
edition = "2024"
version = "0.1.0"
default-run = "stm32h7-nucleo-embassy"
[lib]
harness = false
# needed for each integration test
[[test]]
name = "integration"
harness = false
[dependencies]
shared-embedded.workspace = true
cortex-m.workspace = true
cortex-m-rt.workspace = true
defmt.workspace = true
defmt-rtt.workspace = true
panic-probe.workspace = true
embedded-alloc.workspace = true
static_cell.workspace = true
embassy-stm32 = { workspace = true, features = ["memory-x", "time-driver-any"] }
embassy-executor.workspace = true
embassy-time = { workspace = true, features = ["defmt-timestamp-uptime-ms"] }
embassy-net.workspace = true
[dev-dependencies]
defmt-test.workspace = true
@@ -3,7 +3,8 @@ use std::{env, fs};
fn main() {
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
let cargo_dir = manifest_dir.join(".cargo");
// The configuration is shared by all applications of the embedded workspace.
let cargo_dir = manifest_dir.parent().unwrap().join(".cargo");
let config = cargo_dir.join("config.toml");
let config_template = cargo_dir.join("config.toml.template");
@@ -0,0 +1,150 @@
#![no_main]
#![no_std]
extern crate alloc;
use embassy_executor::Spawner;
use embassy_net::StackResources;
use embassy_stm32::{bind_interrupts, eth, gpio, peripherals, rng};
use static_cell::{ConstStaticCell, StaticCell};
bind_interrupts!(struct Irqs {
ETH => eth::InterruptHandler;
HASH_RNG => rng::InterruptHandler<peripherals::RNG>;
});
#[embassy_executor::main]
async fn main(spawner: Spawner) {
defmt::println!("Starting sat-rs demo application for the STM32H753ZIT");
// Safety: Called once, before the first allocation.
unsafe { stm32h7_nucleo_embassy::init_heap() };
let mut config = embassy_stm32::Config::default();
{
use embassy_stm32::rcc::*;
config.rcc.hsi = Some(HSIPrescaler::DIV1);
config.rcc.csi = true;
config.rcc.hsi48 = Some(Default::default()); // needed for RNG
config.rcc.pll1 = Some(Pll {
source: PllSource::HSI,
prediv: PllPreDiv::DIV4,
mul: PllMul::MUL50,
divp: Some(PllDiv::DIV2),
divq: None,
divr: None,
});
config.rcc.sys = Sysclk::PLL1_P; // 400 Mhz
config.rcc.ahb_pre = AHBPrescaler::DIV2; // 200 Mhz
config.rcc.apb1_pre = APBPrescaler::DIV2; // 100 Mhz
config.rcc.apb2_pre = APBPrescaler::DIV2; // 100 Mhz
config.rcc.apb3_pre = APBPrescaler::DIV2; // 100 Mhz
config.rcc.apb4_pre = APBPrescaler::DIV2; // 100 Mhz
config.rcc.voltage_scale = VoltageScale::Scale1;
}
let periphs = embassy_stm32::init(config);
let green_led = gpio::Output::new(periphs.PB0, gpio::Level::Low, gpio::Speed::Medium);
let leds = shared_embedded::leds::Leds {
red: gpio::Output::new(periphs.PB14, gpio::Level::Low, gpio::Speed::Medium),
orange: gpio::Output::new(periphs.PE1, gpio::Level::Low, gpio::Speed::Medium),
};
static PACKETS: StaticCell<eth::PacketQueue<4, 4>> = StaticCell::new();
// warning: Not all STM32H7 devices have the exact same pins here
// for STM32H747XIH, replace p.PB13 for PG12
let device = eth::Ethernet::new(
PACKETS.init(eth::PacketQueue::<4, 4>::new()),
periphs.ETH,
Irqs,
periphs.PA1, // ref_clk
periphs.PA7, // CRS_DV: Carrier Sense
periphs.PC4, // RX_D0: Received Bit 0
periphs.PC5, // RX_D1: Received Bit 1
periphs.PG13, // TX_D0: Transmit Bit 0
periphs.PB13, // TX_D1: Transmit Bit 1
periphs.PG11, // TX_EN: Transmit Enable
shared_embedded::net::MAC_ADDRESS,
periphs.ETH_SMA,
periphs.PA2, // mdio
periphs.PC1, // mdc
);
let net_config = embassy_net::Config::dhcpv4(embassy_net::DhcpConfig::default());
// Generate random seed.
let mut rng = rng::Rng::new(periphs.RNG, Irqs);
let mut seed = [0; 8];
rng.fill_bytes(&mut seed);
let seed = u64::from_le_bytes(seed);
// DHCP, TMTC and simulator socket.
static RESOURCES: StaticCell<StackResources<3>> = StaticCell::new();
let (stack, runner) = embassy_net::new(
device,
net_config,
RESOURCES.init(StackResources::new()),
seed,
);
static TC_CHANNEL: ConstStaticCell<shared_embedded::tmtc::TcChannel> =
ConstStaticCell::new(shared_embedded::tmtc::TcChannel::new());
let tc_channel = TC_CHANNEL.take();
static TM_CHANNEL: ConstStaticCell<shared_embedded::tmtc::TmChannel> =
ConstStaticCell::new(shared_embedded::tmtc::TmChannel::new());
let tm_channel = TM_CHANNEL.take();
spawner.spawn(net_stack_task(runner).expect("spawning net stack task failed"));
spawner.spawn(sim_client_task(stack).expect("spawning sim client task failed"));
spawner.spawn(
udp_task(stack, tc_channel.sender(), tm_channel.receiver())
.expect("spawning UDP task failed"),
);
spawner.spawn(heartbeat(green_led).expect("spawning heartbeat task failed"));
spawner.spawn(led_task(leds).expect("spawning LED task failed"));
spawner.spawn(
tc_handler(
tc_channel.receiver(),
shared_embedded::tmtc::Telemetry::new(tm_channel.sender()),
)
.expect("spawning TC handler task failed"),
);
}
#[embassy_executor::task]
async fn net_stack_task(
mut runner: embassy_net::Runner<'static, shared_embedded::net::Device>,
) -> ! {
shared_embedded::net::net_stack_task(&mut runner).await
}
#[embassy_executor::task]
async fn udp_task(
stack: embassy_net::Stack<'static>,
tc_tx: shared_embedded::tmtc::TcSender,
tm_rx: shared_embedded::tmtc::TmReceiver,
) {
shared_embedded::net::udp_task(stack, tc_tx, tm_rx).await
}
#[embassy_executor::task]
async fn sim_client_task(stack: embassy_net::Stack<'static>) {
shared_embedded::sim_client::sim_client_task(stack).await
}
#[embassy_executor::task]
async fn heartbeat(mut led: gpio::Output<'static>) {
shared_embedded::leds::heartbeat(&mut led).await
}
#[embassy_executor::task]
async fn led_task(mut leds: shared_embedded::leds::Leds) {
shared_embedded::leds::led_task(&mut leds).await
}
#[embassy_executor::task]
async fn tc_handler(
tc_rx: shared_embedded::tmtc::TcReceiver,
mut telemetry: shared_embedded::tmtc::Telemetry,
) {
shared_embedded::tmtc::tc_handler(tc_rx, &mut telemetry).await
}
@@ -0,0 +1,32 @@
[package]
name = "stm32h7-nucleo-rtic"
edition = "2024"
version = "0.1.0"
default-run = "stm32h7-nucleo-rtic"
[lib]
harness = false
# needed for each integration test
[[test]]
name = "integration"
harness = false
[dependencies]
shared-embedded.workspace = true
cortex-m.workspace = true
cortex-m-rt.workspace = true
defmt.workspace = true
defmt-rtt.workspace = true
panic-probe.workspace = true
embedded-alloc.workspace = true
static_cell.workspace = true
rtic.workspace = true
embassy-stm32 = { workspace = true, features = ["memory-x", "time-driver-any"] }
embassy-time = { workspace = true, features = ["defmt-timestamp-uptime-ms", "generic-queue-16"] }
embassy-net.workspace = true
[dev-dependencies]
defmt-test.workspace = true
@@ -3,7 +3,8 @@ use std::{env, fs};
fn main() {
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
let cargo_dir = manifest_dir.join(".cargo");
// The configuration is shared by all applications of the embedded workspace.
let cargo_dir = manifest_dir.parent().unwrap().join(".cargo");
let config = cargo_dir.join("config.toml");
let config_template = cargo_dir.join("config.toml.template");
@@ -0,0 +1,163 @@
#![no_main]
#![no_std]
extern crate alloc;
use rtic::app;
#[app(device = embassy_stm32, peripherals = false)]
mod app {
use embassy_net::StackResources;
use embassy_stm32::{bind_interrupts, eth, gpio, peripherals, rng};
use static_cell::{ConstStaticCell, StaticCell};
bind_interrupts!(struct Irqs {
ETH => eth::InterruptHandler;
HASH_RNG => rng::InterruptHandler<peripherals::RNG>;
});
#[local]
struct Local {
net_runner: embassy_net::Runner<'static, shared_embedded::net::Device>,
net_stack: embassy_net::Stack<'static>,
sim_net_stack: embassy_net::Stack<'static>,
leds: shared_embedded::leds::Leds,
green_led: gpio::Output<'static>,
tc_rx: shared_embedded::tmtc::TcReceiver,
tc_tx: shared_embedded::tmtc::TcSender,
tm_rx: shared_embedded::tmtc::TmReceiver,
telemetry: shared_embedded::tmtc::Telemetry,
}
#[shared]
struct Shared {}
#[init]
fn init(_cx: init::Context) -> (Shared, Local) {
defmt::println!("Starting sat-rs demo application for the STM32H753ZIT");
// Safety: Called once, before the first allocation.
unsafe { stm32h7_nucleo_rtic::init_heap() };
let mut config = embassy_stm32::Config::default();
{
use embassy_stm32::rcc::*;
config.rcc.hsi = Some(HSIPrescaler::DIV1);
config.rcc.csi = true;
config.rcc.hsi48 = Some(Default::default()); // needed for RNG
config.rcc.pll1 = Some(Pll {
source: PllSource::HSI,
prediv: PllPreDiv::DIV4,
mul: PllMul::MUL50,
divp: Some(PllDiv::DIV2),
divq: None,
divr: None,
});
config.rcc.sys = Sysclk::PLL1_P; // 400 Mhz
config.rcc.ahb_pre = AHBPrescaler::DIV2; // 200 Mhz
config.rcc.apb1_pre = APBPrescaler::DIV2; // 100 Mhz
config.rcc.apb2_pre = APBPrescaler::DIV2; // 100 Mhz
config.rcc.apb3_pre = APBPrescaler::DIV2; // 100 Mhz
config.rcc.apb4_pre = APBPrescaler::DIV2; // 100 Mhz
config.rcc.voltage_scale = VoltageScale::Scale1;
}
let periphs = embassy_stm32::init(config);
let green_led = gpio::Output::new(periphs.PB0, gpio::Level::Low, gpio::Speed::Medium);
let leds = shared_embedded::leds::Leds {
red: gpio::Output::new(periphs.PB14, gpio::Level::Low, gpio::Speed::Medium),
orange: gpio::Output::new(periphs.PE1, gpio::Level::Low, gpio::Speed::Medium),
};
static PACKETS: StaticCell<eth::PacketQueue<4, 4>> = StaticCell::new();
// warning: Not all STM32H7 devices have the exact same pins here
// for STM32H747XIH, replace p.PB13 for PG12
let device = eth::Ethernet::new(
PACKETS.init(eth::PacketQueue::<4, 4>::new()),
periphs.ETH,
Irqs,
periphs.PA1, // ref_clk
periphs.PA7, // CRS_DV: Carrier Sense
periphs.PC4, // RX_D0: Received Bit 0
periphs.PC5, // RX_D1: Received Bit 1
periphs.PG13, // TX_D0: Transmit Bit 0
periphs.PB13, // TX_D1: Transmit Bit 1
periphs.PG11, // TX_EN: Transmit Enable
shared_embedded::net::MAC_ADDRESS,
periphs.ETH_SMA,
periphs.PA2, // mdio
periphs.PC1, // mdc
);
let config = embassy_net::Config::dhcpv4(embassy_net::DhcpConfig::default());
// Generate random seed.
let mut rng = rng::Rng::new(periphs.RNG, Irqs);
let mut seed = [0; 8];
rng.fill_bytes(&mut seed);
let seed = u64::from_le_bytes(seed);
// DHCP, TMTC and simulator socket.
static RESOURCES: StaticCell<StackResources<3>> = StaticCell::new();
let (stack, runner) =
embassy_net::new(device, config, RESOURCES.init(StackResources::new()), seed);
static TC_CHANNEL: ConstStaticCell<shared_embedded::tmtc::TcChannel> =
ConstStaticCell::new(shared_embedded::tmtc::TcChannel::new());
let tc_channel = TC_CHANNEL.take();
static TM_CHANNEL: ConstStaticCell<shared_embedded::tmtc::TmChannel> =
ConstStaticCell::new(shared_embedded::tmtc::TmChannel::new());
let tm_channel = TM_CHANNEL.take();
net_lib_task::spawn().expect("spawning net library task failed");
udp_task::spawn().expect("spawning UDP task failed");
sim_client_task::spawn().expect("spawning sim client task failed");
heartbeat::spawn().expect("spawning heartbeat task failed");
led_task::spawn().expect("spawning LED task failed");
tc_handler::spawn().expect("spawning TC handler task failed");
(
Shared {},
Local {
green_led,
leds,
net_runner: runner,
net_stack: stack,
sim_net_stack: stack,
tc_tx: tc_channel.sender(),
tc_rx: tc_channel.receiver(),
telemetry: shared_embedded::tmtc::Telemetry::new(tm_channel.sender()),
tm_rx: tm_channel.receiver(),
},
)
}
#[task(local = [green_led])]
async fn heartbeat(cx: heartbeat::Context) {
shared_embedded::leds::heartbeat(cx.local.green_led).await;
}
#[task(local = [leds])]
async fn led_task(cx: led_task::Context) {
shared_embedded::leds::led_task(cx.local.leds).await;
}
#[task(local = [net_runner])]
async fn net_lib_task(cx: net_lib_task::Context) {
shared_embedded::net::net_stack_task(cx.local.net_runner).await;
}
#[task(local = [net_stack, tc_tx, tm_rx])]
async fn udp_task(cx: udp_task::Context) {
shared_embedded::net::udp_task(*cx.local.net_stack, *cx.local.tc_tx, *cx.local.tm_rx).await;
}
#[task(local = [sim_net_stack])]
async fn sim_client_task(cx: sim_client_task::Context) {
shared_embedded::sim_client::sim_client_task(*cx.local.sim_net_stack).await;
}
#[task(local = [tc_rx, telemetry])]
async fn tc_handler(cx: tc_handler::Context) {
shared_embedded::tmtc::tc_handler(*cx.local.tc_rx, cx.local.telemetry).await;
}
}
+7 -6
View File
@@ -43,16 +43,17 @@ impl Controller {
tc_id.raw()
);
match request {
control::request::Request::Ping => self
.send_telemetry(Some(tc_id), control::response::Response::Ok),
control::request::Request::Ping => (),
control::request::Request::TestEvent => {
self.event_ctrl_tx.send(control::Event::TestEvent).unwrap();
self.send_telemetry(
Some(tc_id),
control::response::Response::Ok,
);
}
control::request::Request::SimConnect(_ipv4_addr) => {
// TODO: Does this make sense here? I guess it does to try
// a connect when the minisim is started after the OBSW?
log::warn!("sim connect request not supported yet");
}
}
self.send_telemetry(Some(tc_id), control::response::Response::Ok);
}
Err(e) => {
log::warn!("failed to deserialize request: {}", e);
@@ -1,4 +0,0 @@
/target
/.cargo/config.toml
/.vscode
/app.map
-393
View File
@@ -1,393 +0,0 @@
#![no_main]
#![no_std]
extern crate alloc;
use alloc::vec::Vec;
use arbitrary_int::u14;
use defmt::Debug2Format;
use embassy_executor::Spawner;
use embassy_futures::select::{Either, Either3, select, select3};
use embassy_net::StackResources;
use embassy_net::udp::{PacketMetadata, UdpSocket};
use embassy_stm32::{bind_interrupts, eth, gpio, peripherals, rng};
use embassy_sync::blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex};
use embassy_sync::channel::{Channel, Receiver, Sender};
use embassy_sync::signal::Signal;
use embassy_time::{Duration, Timer};
use spacepackets::{CcsdsPacketIdAndPsc, CcsdsPacketReader, SpHeader};
use static_cell::{ConstStaticCell, StaticCell};
use types::ccsds::{CcsdsCreationError, CcsdsTmPacketOwned};
use types::{Apid, ComponentId, Message, TcHeader, TmHeader, control, led, tmtc};
const HEARTBEAT_PERIOD: Duration = Duration::from_millis(500);
const DEFAULT_LED_MODE: led::Mode =
led::Mode::AlternatingToggle(core::time::Duration::from_millis(1000));
const PORT: u16 = 7301;
const MTU: usize = 1500;
/// Locally administered MAC address
const MAC_ADDRESS: [u8; 6] = [0x02, 0x00, 0x11, 0x22, 0x33, 0x44];
const TC_QUEUE_DEPTH: usize = 32;
const TM_QUEUE_DEPTH: usize = 32;
static LED_MODE: Signal<CriticalSectionRawMutex, led::Mode> = Signal::new();
bind_interrupts!(struct Irqs {
ETH => eth::InterruptHandler;
HASH_RNG => rng::InterruptHandler<peripherals::RNG>;
});
type Device = eth::Ethernet<
'static,
peripherals::ETH,
eth::GenericPhy<eth::Sma<'static, peripherals::ETH_SMA>>,
>;
type TcSender = Sender<'static, NoopRawMutex, Vec<u8>, TC_QUEUE_DEPTH>;
type TcReceiver = Receiver<'static, NoopRawMutex, Vec<u8>, TC_QUEUE_DEPTH>;
type TmSender = Sender<'static, NoopRawMutex, Vec<u8>, TM_QUEUE_DEPTH>;
type TmReceiver = Receiver<'static, NoopRawMutex, Vec<u8>, TM_QUEUE_DEPTH>;
struct Leds {
red: gpio::Output<'static>,
orange: gpio::Output<'static>,
}
#[embassy_executor::main]
async fn main(spawner: Spawner) {
defmt::println!("Starting sat-rs demo application for the STM32H753ZIT");
// Safety: Called once, before the first allocation.
unsafe { stm32h7_nucleo_embassy::init_heap() };
let mut config = embassy_stm32::Config::default();
{
use embassy_stm32::rcc::*;
config.rcc.hsi = Some(HSIPrescaler::DIV1);
config.rcc.csi = true;
config.rcc.hsi48 = Some(Default::default()); // needed for RNG
config.rcc.pll1 = Some(Pll {
source: PllSource::HSI,
prediv: PllPreDiv::DIV4,
mul: PllMul::MUL50,
divp: Some(PllDiv::DIV2),
divq: None,
divr: None,
});
config.rcc.sys = Sysclk::PLL1_P; // 400 Mhz
config.rcc.ahb_pre = AHBPrescaler::DIV2; // 200 Mhz
config.rcc.apb1_pre = APBPrescaler::DIV2; // 100 Mhz
config.rcc.apb2_pre = APBPrescaler::DIV2; // 100 Mhz
config.rcc.apb3_pre = APBPrescaler::DIV2; // 100 Mhz
config.rcc.apb4_pre = APBPrescaler::DIV2; // 100 Mhz
config.rcc.voltage_scale = VoltageScale::Scale1;
}
let periphs = embassy_stm32::init(config);
let green_led = gpio::Output::new(periphs.PB0, gpio::Level::Low, gpio::Speed::Medium);
let leds = Leds {
red: gpio::Output::new(periphs.PB14, gpio::Level::Low, gpio::Speed::Medium),
orange: gpio::Output::new(periphs.PE1, gpio::Level::Low, gpio::Speed::Medium),
};
static PACKETS: StaticCell<eth::PacketQueue<4, 4>> = StaticCell::new();
// warning: Not all STM32H7 devices have the exact same pins here
// for STM32H747XIH, replace p.PB13 for PG12
let device = eth::Ethernet::new(
PACKETS.init(eth::PacketQueue::<4, 4>::new()),
periphs.ETH,
Irqs,
periphs.PA1, // ref_clk
periphs.PA7, // CRS_DV: Carrier Sense
periphs.PC4, // RX_D0: Received Bit 0
periphs.PC5, // RX_D1: Received Bit 1
periphs.PG13, // TX_D0: Transmit Bit 0
periphs.PB13, // TX_D1: Transmit Bit 1
periphs.PG11, // TX_EN: Transmit Enable
MAC_ADDRESS,
periphs.ETH_SMA,
periphs.PA2, // mdio
periphs.PC1, // mdc
);
let net_config = embassy_net::Config::dhcpv4(embassy_net::DhcpConfig::default());
// Generate random seed.
let mut rng = rng::Rng::new(periphs.RNG, Irqs);
let mut seed = [0; 8];
rng.fill_bytes(&mut seed);
let seed = u64::from_le_bytes(seed);
static RESOURCES: StaticCell<StackResources<3>> = StaticCell::new();
let (stack, runner) = embassy_net::new(
device,
net_config,
RESOURCES.init(StackResources::new()),
seed,
);
static TC_CHANNEL: ConstStaticCell<Channel<NoopRawMutex, Vec<u8>, TC_QUEUE_DEPTH>> =
ConstStaticCell::new(Channel::new());
let tc_channel = TC_CHANNEL.take();
static TM_CHANNEL: ConstStaticCell<Channel<NoopRawMutex, Vec<u8>, TM_QUEUE_DEPTH>> =
ConstStaticCell::new(Channel::new());
let tm_channel = TM_CHANNEL.take();
spawner.spawn(net_stack_task(runner).expect("spawning net stack task failed"));
spawner.spawn(
udp_task(stack, tc_channel.sender(), tm_channel.receiver())
.expect("spawning UDP task failed"),
);
spawner.spawn(heartbeat(green_led).expect("spawning heartbeat task failed"));
spawner.spawn(led_task(leds).expect("spawning LED task failed"));
spawner.spawn(
tc_handler(
tc_channel.receiver(),
Telemetry {
tx: tm_channel.sender(),
sequence_count: u14::new(0),
},
)
.expect("spawning TC handler task failed"),
);
}
#[embassy_executor::task]
async fn heartbeat(mut led: gpio::Output<'static>) {
loop {
led.toggle();
Timer::after(HEARTBEAT_PERIOD).await;
}
}
/// Applies the current mode to the red and orange LED. A new mode is applied immediately.
#[embassy_executor::task]
async fn led_task(mut leds: Leds) {
let mut mode = DEFAULT_LED_MODE;
loop {
let toggle_period = match mode {
led::Mode::AllOff => {
leds.red.set_low();
leds.orange.set_low();
None
}
led::Mode::RedOn => {
leds.red.set_high();
leds.orange.set_low();
None
}
led::Mode::OrangeOn => {
leds.red.set_low();
leds.orange.set_high();
None
}
led::Mode::AlternatingToggle(period) => {
leds.red.toggle();
leds.orange.set_level((!leds.red.is_set_high()).into());
Some(period)
}
led::Mode::UnifiedToggle(period) => {
leds.red.toggle();
leds.orange.set_level(leds.red.is_set_high().into());
Some(period)
}
};
mode = match toggle_period {
Some(period) => {
let period = Duration::try_from(period).unwrap_or(Duration::MAX);
match select(Timer::after(period), LED_MODE.wait()).await {
Either::First(()) => mode,
Either::Second(new_mode) => new_mode,
}
}
None => LED_MODE.wait().await,
};
}
}
#[embassy_executor::task]
async fn net_stack_task(mut runner: embassy_net::Runner<'static, Device>) -> ! {
runner.run().await
}
#[embassy_executor::task]
async fn udp_task(stack: embassy_net::Stack<'static>, tc_tx: TcSender, tm_rx: TmReceiver) {
// Task futures are allocated statically, so these buffers do not live on the stack.
let mut rx_udp_meta = [PacketMetadata::EMPTY; 8];
let mut tx_udp_meta = [PacketMetadata::EMPTY; 8];
let mut rx_udp_buf = [0; MTU];
let mut tx_udp_buf = [0; MTU];
let mut rx_buffer = [0; MTU];
loop {
stack.wait_link_up().await;
defmt::info!("Network link is up");
// Ensure DHCP configuration is up before trying connect
stack.wait_config_up().await;
defmt::info!("Network task initialized, config: {}", stack.config_v4());
let mut udp = UdpSocket::new(
stack,
&mut rx_udp_meta,
&mut rx_udp_buf,
&mut tx_udp_meta,
&mut tx_udp_buf,
);
if let Err(e) = udp.bind(PORT) {
defmt::error!("Failed to bind UDP socket: {}", e);
Timer::after_secs(1).await;
continue;
}
defmt::info!("UDP socket bound to port {}", PORT);
let mut remote_endpoint = None;
loop {
match select3(
udp.recv_from(&mut rx_buffer),
tm_rx.receive(),
stack.wait_link_down(),
)
.await
{
Either3::First(Ok((len, meta))) => {
remote_endpoint = Some(meta.endpoint);
defmt::debug!("UDP RX {}, Meta: {}", len, meta);
tc_tx.send(rx_buffer[0..len].to_vec()).await;
}
Either3::First(Err(e)) => {
defmt::warn!("udp receive error: {}", e);
Timer::after_millis(100).await;
}
// TM is only generated as a response to a TC, so the endpoint is usually known.
Either3::Second(packet) => match remote_endpoint {
Some(endpoint) => match udp.send_to(&packet, endpoint).await {
Ok(_) => defmt::debug!("UDP TX: {} bytes to: {}", packet.len(), endpoint),
Err(e) => defmt::warn!("udp send error: {}", e),
},
None => defmt::warn!("dropping TM, no remote endpoint known"),
},
Either3::Third(()) => {
defmt::warn!("Network link is down");
break;
}
}
}
}
}
#[embassy_executor::task]
async fn tc_handler(tc_rx: TcReceiver, mut telemetry: Telemetry) {
loop {
let tc = tc_rx.receive().await;
let packet = match CcsdsPacketReader::new_with_checksum(&tc) {
Ok(packet) => packet,
Err(e) => {
defmt::warn!("Failed to parse received TC packet: {}", e);
send_tmtc_event(&mut telemetry, tmtc::Event::InvalidTcPacket).await;
continue;
}
};
let tc_id = CcsdsPacketIdAndPsc {
packet_id: packet.packet_id(),
psc: packet.psc(),
};
let Ok((tc_header, payload)) = postcard::take_from_bytes::<TcHeader>(packet.user_data())
else {
defmt::warn!("Failed to deserialize TC header");
send_tmtc_event(&mut telemetry, tmtc::Event::InvalidTcHeader).await;
continue;
};
match tc_header.target_id {
ComponentId::Controller => handle_controller_tc(payload, tc_id, &mut telemetry).await,
ComponentId::Led => handle_led_tc(payload, tc_id, &mut telemetry).await,
target_id => {
defmt::warn!("No TC handler for target ID {}", Debug2Format(&target_id));
send_tmtc_event(&mut telemetry, tmtc::Event::UnknownTargetId(target_id)).await;
}
}
}
}
/// All TCs are received via UDP, so the UDP server is the sender of TMTC events.
async fn send_tmtc_event(telemetry: &mut Telemetry, event: tmtc::Event) {
telemetry.send(ComponentId::UdpServer, None, &event).await;
}
/// The controller does not control anything yet, but handles generic requests like pings.
async fn handle_controller_tc(
payload: &[u8],
tc_id: CcsdsPacketIdAndPsc,
telemetry: &mut Telemetry,
) {
let Ok(request) = postcard::from_bytes::<control::request::Request>(payload) else {
defmt::warn!("Failed to deserialize controller request");
return;
};
match request {
control::request::Request::Ping => defmt::info!("Received controller ping request"),
control::request::Request::TestEvent => {
defmt::info!("Received test event request");
let event = types::Event::ControllerEvent(control::Event::TestEvent);
telemetry.send(ComponentId::Controller, None, &event).await;
}
}
telemetry
.send(
ComponentId::Controller,
Some(tc_id),
&control::response::Response::Ok,
)
.await;
}
async fn handle_led_tc(payload: &[u8], tc_id: CcsdsPacketIdAndPsc, telemetry: &mut Telemetry) {
let Ok(request) = postcard::from_bytes::<led::request::Request>(payload) else {
defmt::warn!("Failed to deserialize LED request");
return;
};
match request {
led::request::Request::Ping => defmt::info!("Received LED ping request"),
led::request::Request::SetMode(mode) => {
defmt::info!("Received LED mode request: {}", Debug2Format(&mode));
LED_MODE.signal(mode);
}
}
telemetry
.send(ComponentId::Led, Some(tc_id), &led::response::Response::Ok)
.await;
}
/// Packs TM and passes it to the UDP task.
struct Telemetry {
tx: TmSender,
sequence_count: u14,
}
impl Telemetry {
/// TM without a TC ID is sent unsolicited, for example events.
async fn send(
&mut self,
sender_id: ComponentId,
tc_id: Option<CcsdsPacketIdAndPsc>,
payload: &(impl serde::Serialize + Message),
) {
let sp_header = SpHeader::new_for_unseg_tm(Apid::Tmtc.raw_value(), self.sequence_count, 0);
let tm_header = TmHeader::new_without_timestamp(
sender_id,
ComponentId::Ground,
payload.message_type(),
tc_id,
);
match CcsdsTmPacketOwned::new_with_serde_payload(sp_header, &tm_header, payload)
.map_err(CcsdsCreationError::from)
.and_then(|packet| packet.try_to_vec())
{
Ok(raw_packet) => {
self.tx.send(raw_packet).await;
self.sequence_count = self.sequence_count.wrapping_add(u14::new(1));
}
Err(e) => defmt::warn!("Failed to create TM packet: {}", Debug2Format(&e)),
}
}
}
@@ -1,29 +0,0 @@
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "probe-rs run --chip STM32H753ZITx"
# runner = ["probe-rs", "run", "--chip", "$CHIP", "--log-format", "{L} {s}"]
rustflags = [
"-C", "linker=flip-link",
"-C", "link-arg=-Tlink.x",
"-C", "link-arg=-Tdefmt.x",
# This is needed if your flash or ram addresses are not aligned to 0x10000 in memory.x
# See https://github.com/rust-embedded/cortex-m-quickstart/pull/95
"-C", "link-arg=--nmagic",
# Can be useful for debugging.
# "-Clink-args=-Map=app.map"
]
[build]
# (`thumbv6m-*` is compatible with all ARM Cortex-M chips but using the right
# target improves performance)
# target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+
# target = "thumbv7m-none-eabi" # Cortex-M3
# target = "thumbv7em-none-eabi" # Cortex-M4 and Cortex-M7 (no FPU)
target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
[alias]
rb = "run --bin"
rrb = "run --release --bin"
[env]
DEFMT_LOG = "info"
-4
View File
@@ -1,4 +0,0 @@
/target
/.cargo/config.toml
/.vscode
/app.map
File diff suppressed because it is too large. Load diff
-76
View File
@@ -1,76 +0,0 @@
[package]
name = "stm32h7-nucleo-rtic"
edition = "2024"
version = "0.1.0"
default-run = "stm32h7-nucleo-rtic"
[lib]
harness = false
# needed for each integration test
[[test]]
name = "integration"
harness = false
[dependencies]
types = { path = "../types" }
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
arbitrary-int = "2"
cortex-m-rt = "0.7"
defmt = "1"
defmt-rtt = "1"
panic-probe = { version = "1", features = ["print-defmt"] }
embedded-alloc = "0.7"
static_cell = "2"
rtic = { version = "2", features = ["thumbv7-backend"] }
spacepackets = { version = "0.18", default-features = false, features = ["defmt"] }
postcard = "1"
serde = { version = "1", default-features = false }
embassy-stm32 = { version = "0.6", features = ["stm32h753zi", "memory-x", "defmt", "time-driver-any"] }
embassy-time = { version = "0.5", features = ["defmt-timestamp-uptime-ms", "generic-queue-16"] }
embassy-net = { version = "0.9", features = ["medium-ethernet", "proto-ipv4", "tcp", "udp", "auto-icmp-echo-reply", "dhcpv4", "defmt"] }
embassy-sync = "0.8"
embassy-futures = "0.1"
[dev-dependencies]
defmt-test = "0.5"
# cargo build/run
[profile.dev]
codegen-units = 1
debug = 2
debug-assertions = true # <-
incremental = false
opt-level = 's' # <-
overflow-checks = true # <-
# cargo test
[profile.test]
codegen-units = 1
debug = 2
debug-assertions = true # <-
incremental = false
opt-level = 3 # <-
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 # <-
# cargo test --release
[profile.bench]
codegen-units = 1
debug = 2
debug-assertions = false # <-
incremental = false
lto = 'fat'
opt-level = 3 # <-
overflow-checks = false # <-
-113
View File
@@ -1,113 +0,0 @@
sat-rs example for the STM32H753ZI-Nucleo board
=======
This example application shows how the [sat-rs library](https://egit.irs.uni-stuttgart.de/rust/sat-rs)
can be used on an embedded target.
It also shows how a relatively simple OBSW could be built when no standard runtime is available.
It uses [RTIC](https://rtic.rs/2/book/en/) as the concurrency framework and the
[defmt](https://defmt.ferrous-systems.com/) framework for logging.
The STM32H753ZIT device was picked because it is one of the more powerful Cortex-M based STM32
devices. It has more RAM available and allows commanding via Ethernet. The example is written for
the NUCLEO-H753ZI board, which uses the MB1364 Nucleo-144 board layout.
## Pre-Requisites
Make sure the following tools are installed:
1. [`probe-rs`](https://probe.rs/): Application used to flash and debug the MCU.
2. Optional and recommended: [VS Code](https://code.visualstudio.com/) with
[probe-rs plugin](https://marketplace.visualstudio.com/items?itemName=probe-rs.probe-rs-debugger)
for debugging.
## Preparing Rust and the repository
Building an application requires the `thumbv7em-none-eabihf` cross-compiler toolchain.
If you have not installed it yet, you can do so with
```sh
rustup target add thumbv7em-none-eabihf
```
A default `.cargo` config file is provided as `.cargo/config.toml.template`. The build script
copies it to `.cargo/config.toml` if that file does not exist yet. The copy is not tracked by git,
so you can change settings like the runner for your setup.
Cargo reads the configuration before the build script runs, so the very first build on a fresh
checkout does not use it yet and might fail. Simply run the build again, or copy the file
manually beforehand:
```sh
cp .cargo/config.toml.template .cargo/config.toml
```
The configuration file also sets the target so it does not always have to be specified with
the `--target` argument.
## Building
After that, assuming that you have a `.cargo/config.toml` setting the correct build target,
you can simply build the application with
```sh
cargo build
```
## Flashing from the command line
You can flash the application from the command line using `probe-rs`:
```sh
probe-rs run --chip STM32H753ZITx
```
## Debugging with VS Code
The Nucleo board comes with an on-board ST-Link so all that is required to flash and debug
the board is a USB cable. The code in this repository was debugged using [`probe-rs`](https://probe.rs/docs/tools/debuggerA)
and the VS Code [`probe-rs` plugin](https://marketplace.visualstudio.com/items?itemName=probe-rs.probe-rs-debugger).
Make sure to install this plugin first.
## Commanding the board
The board is commanded via UDP on port 7301. It gets its IP address via DHCP, so it needs to be
connected to a network with a DHCP server. The network configuration including the IP address is
logged after startup. According to the board user manual UM2407, jumper JP6 and solder bridge SB72
must be ON when using Ethernet. The board uses the same TMTC protocol as the
[`example-std`](../example-std) application, which is defined inside the [`types`](../types)
crate.
The [`client`](../client) application is used to command the board. Set the address of the board
inside `client/config.toml`, which is created from `client/config.toml.template` on the first
build:
```toml
[interface]
udp_addr = "192.168.1.50:7301"
```
For example, you can then send a ping to the MCU using
```sh
cargo run -p client -- --ping
```
Like the `example-std` application, the board has a controller component which handles pings and
test events. A test event can be triggered with `--test-event`.
The green LED blinks every 0.5 seconds as a heartbeat. The red and the orange LED are controlled
with a mode. For example, you can let both toggle together every 200 ms using
```sh
cargo run -p client -- led --mode unified-toggle --toggle-period-ms 200
```
Use `cargo run -p client -- led --help` to list all modes.
You can also pass the board address with `--udp-addr` instead of setting it inside the
configuration file.
## Resources
- [STM32H743ZI Ethernet link checker example](https://github.com/stm32-rs/stm32h7xx-hal/blob/master/examples/ethernet-nucleo-h743zi2.rs)
- [smoltcp DHCP client](https://github.com/smoltcp-rs/smoltcp/blob/main/examples/dhcp_client.rs)
@@ -1,2 +0,0 @@
[toolchain]
targets = ["thumbv7em-none-eabihf"]
-466
View File
@@ -1,466 +0,0 @@
#![no_main]
#![no_std]
extern crate alloc;
use embassy_stm32::bind_interrupts;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::signal::Signal;
use rtic::app;
use types::led;
const HEARTBEAT_PERIOD: embassy_time::Duration = embassy_time::Duration::from_millis(500);
const DEFAULT_LED_MODE: led::Mode =
led::Mode::AlternatingToggle(core::time::Duration::from_millis(1000));
const PORT: u16 = 7301;
/// Locally administered MAC address
const MAC_ADDRESS: [u8; 6] = [0x02, 0x00, 0x11, 0x22, 0x33, 0x44];
const TC_QUEUE_DEPTH: usize = 32;
const TM_QUEUE_DEPTH: usize = 32;
static LED_MODE: Signal<CriticalSectionRawMutex, led::Mode> = Signal::new();
#[app(device = embassy_stm32, peripherals = false)]
mod app {
use super::*;
use arbitrary_int::u14;
use defmt::Debug2Format;
use embassy_futures::select::{Either, select};
use embassy_net::StackResources;
use embassy_net::udp::UdpSocket;
use embassy_stm32::eth;
use embassy_stm32::gpio;
use embassy_stm32::peripherals;
use embassy_stm32::rng;
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
use embassy_time::Duration;
use embassy_time::Timer;
use embassy_time::WithTimeout as _;
use spacepackets::CcsdsPacketIdAndPsc;
use spacepackets::CcsdsPacketReader;
use spacepackets::SpHeader;
use static_cell::StaticCell;
use types::ccsds::{CcsdsCreationError, CcsdsTmPacketOwned};
use types::{Apid, ComponentId, Message, TcHeader, TmHeader, control, tmtc};
bind_interrupts!(struct Irqs {
ETH => eth::InterruptHandler;
HASH_RNG => rng::InterruptHandler<peripherals::RNG>;
});
type Device = eth::Ethernet<
'static,
peripherals::ETH,
eth::GenericPhy<eth::Sma<'static, peripherals::ETH_SMA>>,
>;
struct Leds {
red: gpio::Output<'static>,
orange: gpio::Output<'static>,
}
#[local]
struct Local {
net_runner: embassy_net::Runner<'static, Device>,
net_stack: embassy_net::Stack<'static>,
leds: Leds,
green_led: gpio::Output<'static>,
tc_rx: embassy_sync::channel::Receiver<
'static,
NoopRawMutex,
alloc::vec::Vec<u8>,
TC_QUEUE_DEPTH,
>,
tc_tx: embassy_sync::channel::Sender<
'static,
NoopRawMutex,
alloc::vec::Vec<u8>,
TC_QUEUE_DEPTH,
>,
tm_rx: embassy_sync::channel::Receiver<
'static,
NoopRawMutex,
alloc::vec::Vec<u8>,
TM_QUEUE_DEPTH,
>,
telemetry: Telemetry,
}
#[shared]
struct Shared {}
#[init]
fn init(_cx: init::Context) -> (Shared, Local) {
defmt::println!("Starting sat-rs demo application for the STM32H753ZIT");
// Safety: Called once, before the first allocation.
unsafe { stm32h7_nucleo_rtic::init_heap() };
let mut config = embassy_stm32::Config::default();
{
use embassy_stm32::rcc::*;
config.rcc.hsi = Some(HSIPrescaler::DIV1);
config.rcc.csi = true;
config.rcc.hsi48 = Some(Default::default()); // needed for RNG
config.rcc.pll1 = Some(Pll {
source: PllSource::HSI,
prediv: PllPreDiv::DIV4,
mul: PllMul::MUL50,
divp: Some(PllDiv::DIV2),
divq: None,
divr: None,
});
config.rcc.sys = Sysclk::PLL1_P; // 400 Mhz
config.rcc.ahb_pre = AHBPrescaler::DIV2; // 200 Mhz
config.rcc.apb1_pre = APBPrescaler::DIV2; // 100 Mhz
config.rcc.apb2_pre = APBPrescaler::DIV2; // 100 Mhz
config.rcc.apb3_pre = APBPrescaler::DIV2; // 100 Mhz
config.rcc.apb4_pre = APBPrescaler::DIV2; // 100 Mhz
config.rcc.voltage_scale = VoltageScale::Scale1;
}
let periphs = embassy_stm32::init(config);
let green_led = gpio::Output::new(periphs.PB0, gpio::Level::Low, gpio::Speed::Medium);
let leds = Leds {
red: gpio::Output::new(periphs.PB14, gpio::Level::Low, gpio::Speed::Medium),
orange: gpio::Output::new(periphs.PE1, gpio::Level::Low, gpio::Speed::Medium),
};
static PACKETS: StaticCell<eth::PacketQueue<4, 4>> = StaticCell::new();
// warning: Not all STM32H7 devices have the exact same pins here
// for STM32H747XIH, replace p.PB13 for PG12
let device = eth::Ethernet::new(
PACKETS.init(eth::PacketQueue::<4, 4>::new()),
periphs.ETH,
Irqs,
periphs.PA1, // ref_clk
periphs.PA7, // CRS_DV: Carrier Sense
periphs.PC4, // RX_D0: Received Bit 0
periphs.PC5, // RX_D1: Received Bit 1
periphs.PG13, // TX_D0: Transmit Bit 0
periphs.PB13, // TX_D1: Transmit Bit 1
periphs.PG11, // TX_EN: Transmit Enable
MAC_ADDRESS,
periphs.ETH_SMA,
periphs.PA2, // mdio
periphs.PC1, // mdc
);
let config = embassy_net::Config::dhcpv4(embassy_net::DhcpConfig::default());
// Generate random seed.
let mut rng = rng::Rng::new(periphs.RNG, Irqs);
let mut seed = [0; 8];
rng.fill_bytes(&mut seed);
let seed = u64::from_le_bytes(seed);
// Init network stack
static RESOURCES: StaticCell<StackResources<3>> = StaticCell::new();
let (stack, runner) =
embassy_net::new(device, config, RESOURCES.init(StackResources::new()), seed);
static TC_CHANNEL: static_cell::ConstStaticCell<
embassy_sync::channel::Channel<NoopRawMutex, alloc::vec::Vec<u8>, TC_QUEUE_DEPTH>,
> = static_cell::ConstStaticCell::new(embassy_sync::channel::Channel::new());
let tc_channel = TC_CHANNEL.take();
let tc_sender = tc_channel.sender();
let tc_receiver = tc_channel.receiver();
static TM_CHANNEL: static_cell::ConstStaticCell<
embassy_sync::channel::Channel<NoopRawMutex, alloc::vec::Vec<u8>, TM_QUEUE_DEPTH>,
> = static_cell::ConstStaticCell::new(embassy_sync::channel::Channel::new());
let tm_channel = TM_CHANNEL.take();
let tm_sender = tm_channel.sender();
let tm_receiver = tm_channel.receiver();
net_lib_task::spawn().expect("spawning net library task failed");
net_app_task::spawn().expect("spawning net application task failed");
heartbeat::spawn().expect("spawning heartbeat task failed");
led_task::spawn().expect("spawning LED task failed");
tc_handler::spawn().expect("spawning TC handler task failed");
(
Shared {},
Local {
green_led,
leds,
net_runner: runner,
net_stack: stack,
tc_tx: tc_sender,
tc_rx: tc_receiver,
telemetry: Telemetry {
tx: tm_sender,
sequence_count: u14::new(0),
},
tm_rx: tm_receiver,
},
)
}
#[task(local = [green_led])]
async fn heartbeat(cx: heartbeat::Context) {
loop {
cx.local.green_led.toggle();
Timer::after(HEARTBEAT_PERIOD).await;
}
}
/// Applies the current mode to the red and orange LED. A new mode is applied immediately.
#[task(local = [leds])]
async fn led_task(cx: led_task::Context) {
let leds = cx.local.leds;
let mut mode = DEFAULT_LED_MODE;
loop {
let toggle_period = match mode {
led::Mode::AllOff => {
leds.red.set_low();
leds.orange.set_low();
None
}
led::Mode::RedOn => {
leds.red.set_high();
leds.orange.set_low();
None
}
led::Mode::OrangeOn => {
leds.red.set_low();
leds.orange.set_high();
None
}
led::Mode::AlternatingToggle(period) => {
leds.red.toggle();
leds.orange.set_level((!leds.red.is_set_high()).into());
Some(period)
}
led::Mode::UnifiedToggle(period) => {
leds.red.toggle();
leds.orange.set_level(leds.red.is_set_high().into());
Some(period)
}
};
mode = match toggle_period {
Some(period) => {
let period = Duration::try_from(period).unwrap_or(Duration::MAX);
match select(Timer::after(period), LED_MODE.wait()).await {
Either::First(()) => mode,
Either::Second(new_mode) => new_mode,
}
}
None => LED_MODE.wait().await,
};
}
}
#[task(local=[net_runner])]
async fn net_lib_task(cx: net_lib_task::Context) {
cx.local.net_runner.run().await;
}
#[task(local = [net_stack, tc_tx, tm_rx])]
async fn net_app_task(cx: net_app_task::Context) {
pub const MTU: usize = 1500;
// Ensure those are in the data section by making them static.
static RX_UDP_META: static_cell::ConstStaticCell<[embassy_net::udp::PacketMetadata; 8]> =
static_cell::ConstStaticCell::new([embassy_net::udp::PacketMetadata::EMPTY; 8]);
static TX_UDP_META: static_cell::ConstStaticCell<[embassy_net::udp::PacketMetadata; 8]> =
static_cell::ConstStaticCell::new([embassy_net::udp::PacketMetadata::EMPTY; 8]);
static TX_UDP_BUFS: static_cell::ConstStaticCell<[u8; MTU]> =
static_cell::ConstStaticCell::new([0; MTU]);
static RX_UDP_BUFS: static_cell::ConstStaticCell<[u8; MTU]> =
static_cell::ConstStaticCell::new([0; MTU]);
let rx_udp_meta = RX_UDP_META.take();
let rx_udp_bufs = RX_UDP_BUFS.take();
let tx_udp_meta = TX_UDP_META.take();
let tx_udp_bufs = TX_UDP_BUFS.take();
let mut rx_buffer = [0; MTU];
loop {
cx.local.net_stack.wait_link_up().await;
defmt::info!("Network link is up");
// Ensure DHCP configuration is up before trying connect
cx.local.net_stack.wait_config_up().await;
let config = cx.local.net_stack.config_v4();
defmt::info!("Network task initialized, config: {}", config);
let mut udp = UdpSocket::new(
cx.local.net_stack.clone(),
rx_udp_meta,
rx_udp_bufs,
tx_udp_meta,
tx_udp_bufs,
);
if let Err(e) = udp.bind(PORT) {
defmt::error!("Failed to bind UDP socket: {}", e);
Timer::after_secs(1).await;
continue;
}
defmt::info!("UDP socket bound to port {}", PORT);
let mut remote_endpoint = None;
loop {
if !cx.local.net_stack.is_link_up() {
defmt::warn!("Network link is down");
break;
}
match udp
.recv_from(&mut rx_buffer)
.with_timeout(Duration::from_millis(200))
.await
{
Ok(result) => match result {
Ok((data, meta)) => {
remote_endpoint = Some(meta.endpoint);
defmt::debug!("UDP RX {}, Meta: {}", data, meta);
cx.local.tc_tx.send(rx_buffer[0..data].to_vec()).await;
}
Err(e) => {
defmt::warn!("udp receive error: {}", e);
Timer::after_millis(100).await;
}
},
Err(_e) => (),
}
if let Some(endpoint) = remote_endpoint {
while let Ok(packet) = cx.local.tm_rx.try_receive() {
match udp.send_to(&packet, endpoint).await {
Ok(_) => {
defmt::debug!("UDP TX: {} bytes to: {}", packet.len(), endpoint)
}
Err(e) => defmt::warn!("udp send error: {}", e),
}
}
}
}
}
}
#[task(local = [tc_rx, telemetry])]
async fn tc_handler(cx: tc_handler::Context) {
let telemetry = cx.local.telemetry;
loop {
let tc = cx.local.tc_rx.receive().await;
let packet = match CcsdsPacketReader::new_with_checksum(&tc) {
Ok(packet) => packet,
Err(e) => {
defmt::warn!("Failed to parse received TC packet: {}", e);
send_tmtc_event(telemetry, tmtc::Event::InvalidTcPacket).await;
continue;
}
};
let tc_id = CcsdsPacketIdAndPsc {
packet_id: packet.packet_id(),
psc: packet.psc(),
};
let Ok((tc_header, payload)) =
postcard::take_from_bytes::<TcHeader>(packet.user_data())
else {
defmt::warn!("Failed to deserialize TC header");
send_tmtc_event(telemetry, tmtc::Event::InvalidTcHeader).await;
continue;
};
match tc_header.target_id {
ComponentId::Controller => handle_controller_tc(payload, tc_id, telemetry).await,
ComponentId::Led => handle_led_tc(payload, tc_id, telemetry).await,
target_id => {
defmt::warn!("No TC handler for target ID {}", Debug2Format(&target_id));
send_tmtc_event(telemetry, tmtc::Event::UnknownTargetId(target_id)).await;
}
}
}
}
/// All TCs are received via UDP, so the UDP server is the sender of TMTC events.
async fn send_tmtc_event(telemetry: &mut Telemetry, event: tmtc::Event) {
telemetry.send(ComponentId::UdpServer, None, &event).await;
}
/// The controller does not control anything yet, but handles generic requests like pings.
async fn handle_controller_tc(
payload: &[u8],
tc_id: CcsdsPacketIdAndPsc,
telemetry: &mut Telemetry,
) {
let Ok(request) = postcard::from_bytes::<control::request::Request>(payload) else {
defmt::warn!("Failed to deserialize controller request");
return;
};
match request {
control::request::Request::Ping => defmt::info!("Received controller ping request"),
control::request::Request::TestEvent => {
defmt::info!("Received test event request");
let event = types::Event::ControllerEvent(control::Event::TestEvent);
telemetry.send(ComponentId::Controller, None, &event).await;
}
}
telemetry
.send(
ComponentId::Controller,
Some(tc_id),
&control::response::Response::Ok,
)
.await;
}
async fn handle_led_tc(payload: &[u8], tc_id: CcsdsPacketIdAndPsc, telemetry: &mut Telemetry) {
let Ok(request) = postcard::from_bytes::<led::request::Request>(payload) else {
defmt::warn!("Failed to deserialize LED request");
return;
};
match request {
led::request::Request::Ping => defmt::info!("Received LED ping request"),
led::request::Request::SetMode(mode) => {
defmt::info!("Received LED mode request: {}", Debug2Format(&mode));
LED_MODE.signal(mode);
}
}
telemetry
.send(ComponentId::Led, Some(tc_id), &led::response::Response::Ok)
.await;
}
/// Packs TM and passes it to the network task.
struct Telemetry {
tx: embassy_sync::channel::Sender<
'static,
NoopRawMutex,
alloc::vec::Vec<u8>,
TM_QUEUE_DEPTH,
>,
sequence_count: u14,
}
impl Telemetry {
/// TM without a TC ID is sent unsolicited, for example events.
async fn send(
&mut self,
sender_id: ComponentId,
tc_id: Option<CcsdsPacketIdAndPsc>,
payload: &(impl serde::Serialize + Message),
) {
let sp_header =
SpHeader::new_for_unseg_tm(Apid::Tmtc.raw_value(), self.sequence_count, 0);
let tm_header = TmHeader::new_without_timestamp(
sender_id,
ComponentId::Ground,
payload.message_type(),
tc_id,
);
match CcsdsTmPacketOwned::new_with_serde_payload(sp_header, &tm_header, payload)
.map_err(CcsdsCreationError::from)
.and_then(|packet| packet.try_to_vec())
{
Ok(raw_packet) => {
self.tx.send(raw_packet).await;
self.sequence_count = self.sequence_count.wrapping_add(u14::new(1));
}
Err(e) => defmt::warn!("Failed to create TM packet: {}", Debug2Format(&e)),
}
}
}
}
+1
View File
@@ -24,6 +24,7 @@ pub mod request {
pub enum Request {
Ping,
TestEvent,
SimConnect(Option<core::net::Ipv4Addr>),
}
}
+5 -5
View File
@@ -11,16 +11,16 @@ test:
cargo nextest run --all-features
cargo test --doc --all-features
embedded: embedded-stm32h7 embedded-stm32h7-embassy
embedded: embedded-stm32h7
cargo check -p satrs --target=thumbv7em-none-eabihf --no-default-features
[working-directory:"examples/stm32h7-nucleo-rtic"]
[working-directory:"examples/embedded"]
embedded-stm32h7:
cargo build --target=thumbv7em-none-eabihf --release
[working-directory:"examples/stm32h7-nucleo-embassy"]
embedded-stm32h7-embassy:
cargo build --target=thumbv7em-none-eabihf --release
clean:
cargo clean
cargo clean --manifest-path examples/embedded/Cargo.toml
check-fmt:
cargo fmt --all -- --check