Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce016b1e3f |
No files matched your search
+1
-2
@@ -13,6 +13,5 @@ members = [
|
||||
]
|
||||
|
||||
exclude = [
|
||||
"examples/stm32h7-nucleo-rtic",
|
||||
"examples/stm32h7-nucleo-embassy",
|
||||
"examples/embedded",
|
||||
]
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -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"
|
||||
@@ -0,0 +1,3 @@
|
||||
/.cargo/config.toml
|
||||
.vscode/
|
||||
app.map
|
||||
+112
-7
@@ -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)
|
||||
File renamed without changes.
@@ -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
|
||||
File renamed without changes.
+2
-1
@@ -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");
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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
|
||||
}
|
||||
File renamed without changes.
@@ -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
|
||||
File renamed without changes.
+2
-1
@@ -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");
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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;
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -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
|
||||
@@ -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"
|
||||
@@ -1,4 +0,0 @@
|
||||
/target
|
||||
/.cargo/config.toml
|
||||
/.vscode
|
||||
/app.map
|
||||
Generated
-1400
File diff suppressed because it is too large.
Load diff
@@ -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 # <-
|
||||
@@ -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"]
|
||||
@@ -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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ pub mod request {
|
||||
pub enum Request {
|
||||
Ping,
|
||||
TestEvent,
|
||||
SimConnect(Option<core::net::Ipv4Addr>),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user