Compare commits

...
43 changed files with 3790 additions and 85 deletions

View file

@ -1,85 +0,0 @@
# mUART Failsafe Interposer
<a href="https://opensource.org"><img height="150px" align="left" src="https://opensource.org/files/OSIApprovedCropped.png" alt="Open Source Initiative Approved License logo"></a>
This repository contains firmware and hardware designs for PCBs that
can be used with the [mUART](https://muart-group.github.io/)
components in [ESPHome](https://esphome.io/). The PCBs provide
interfaces for the UART links to the heat pump/air handler and
thermostat (using CN105-style connectors), and implement a 'failsafe'
mode ensuring that the HP/AH and the thermostat can communicate with
each other even if the ESPHome 'host' is not operating properly (or
not connected at all). They are designed to be interposed between the
heat pump/air handler and the thermostat.
## &nbsp;
## Design
The 'failsafe' feature is implemented by using four analog multiplexer
chips (each chip can route either of two signals to its
input/output). These are used to either connect the UART TX/RX signals
between the HP/AH and thermostat (the 'bypass' mode), or to connect
them to the GPIOs on the ESPHome device (the 'host' mode, so that the
mUART software can communicate with the HP/AH and thermostat). At
power-up, two of the multiplexers connect the UART signals in 'bypass'
mode and the other two multiplexers are disabled.
In addition to the multiplexers there is an
[MSP430](https://www.ti.com/microcontrollers-mcus-processors/msp430-microcontrollers/overview.html)
microcontroller running a very small program (see Firmware below). The
MSP430 can switch the multiplexers into 'host' mode or allow them to
fall back to 'bypass' mode, depending on whether it receives periodic
'alive' signals (pulses on a GPIO) from the ESPHome device. By default
it requires the ESPHome device to send at least one pulse every four
seconds; if no pulse is received during a four-second period, the
multiplexers will switch back to 'bypass' mode. As long as the ESPHome
device's software is operating correctly and sending periodic pulses,
it will be able to communicate with the HP/AH and the thermostat. This
also means that the ESPHome device must send an initial pulse when it
starts up in order to switch the multiplexers into 'host' mode; the
example configuration below shows one way to do that.
## Firmware
The [firmware](../firmware) is a C program which runs on the
MSP430. It can be used with any variant of the MSP430 low-cost
mixed-signal MCUs (FR2000, FR2100, FR2110, or FR2111) with no code
changes.
## Hardware
### Olimex UEXT Adapter
<img height="250px" width="410px" src="../olimex-uext/olimex-uext-front-2024.5.png" alt="Ray-traced 3D rendering of the front side of the adapter board"><img height="250px" width="410px" src="../olimex-uext/olimex-uext-back-2024.5.png" alt="Ray-traced 3D rendering of the back side of the adapter board">
The [Olimex UEXT Adapter board](../olimex-uext) is designed to
be used with an [Olimex](https://www.olimex.com/Products/IoT/)
ESP32-based host board that has a
[UEXT](https://www.olimex.com/Products/Modules/) connector. However,
since the host connections are standard header pins, it can be used
with any ESP32-based board by using jumper wires.
The board contains the above-mentioned CN105-style connectors, the
analog multiplexers and MSP430, 3.3V-to-5V level shifters, and various
status LEDs.
## ESPHome Example Configuration
(coming soon)
## Credits
["Standing on the shoulders of
giants"](https://en.wikipedia.org/wiki/Standing_on_the_shoulders_of_giants)
could not be more true than it is in the open source hardware and
software community; this project relies on many wonderful tools and
libraries produced by the global open source hardware and software
community. I've listed many of them below, but if I've overlooked any
please do not be offended :-)
* [Fabrication Toolkit](https://github.com/bennymeg/Fabrication-Toolkit)
* [KiCAD](https://www.kicad.org/)
* [MSP430 GCC](https://www.ti.com/tool/MSP430-GCC-OPENSOURCE)
* [mspdebug](https://github.com/dlbeer/mspdebug)
* [opensource-toolchain-msp430](https://github.com/cjacker/opensource-toolchain-msp430)

View file

@ -0,0 +1,2 @@
[env]
CARGO_WORKSPACE_DIR = { value = "", relative = true }

1
firmware/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

1291
firmware/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

51
firmware/Cargo.toml Normal file
View file

@ -0,0 +1,51 @@
[workspace]
resolver = "3"
members = ["app", "boot"]
[workspace.dependencies]
embassy-boot = { version = "0.6.0" }
embassy-boot-stm32 = { version = "0.4.0" }
embassy-executor = { version = "0.8.0", features = ["arch-cortex-m", "executor-thread"] }
embassy-futures = { version = "0.1.1" }
embassy-stm32 = { version = "0.2.0", features = ["time-driver-tim1", "stm32g070kb", "unstable-pac" ] }
embassy-sync = { version = "0.7.0" }
embassy-time = { version = "0.4.0", features = ["tick-hz-32_768", "defmt-timestamp-uptime"] }
cortex-m = { version = "0.7.6", features = ["inline-asm", "critical-section-single-core"] }
cortex-m-rt = { version = "0.7.0" }
portable-atomic = { version = "1.5", features = ["unsafe-assume-single-core"] }
embedded-hal = { version = "1.0.0" }
embedded-io = { version = "0.6.1" }
embedded-io-async = { version = "0.6.1" }
embedded-resources = { version = "0.2.1", features = ["stm32"] }
embedded-storage = { version = "0.3.1" }
embedded-storage-async = { version = "0.4.1" }
byteorder = { version = "1.5.0", default-features = false }
static_cell = { version = "2.1.0" }
never = { version = "0.1.0", default-features = false }
thiserror = { version = "2.0.12", default-features = false }
defmt = { version = "1.0.0" }
defmt-rtt = { version = "1.0.0" }
panic-probe = { version = "1.0.0" }
[patch.crates-io]
embassy-boot = { git = "https://github.com/embassy-rs/embassy" }
embassy-boot-stm32 = { git = "https://github.com/embassy-rs/embassy" }
embassy-executor = { git = "https://github.com/embassy-rs/embassy" }
embassy-futures = { git = "https://github.com/embassy-rs/embassy" }
embassy-stm32 = { git = "https://github.com/embassy-rs/embassy" }
embassy-sync = { git = "https://github.com/embassy-rs/embassy" }
embassy-time = { git = "https://github.com/embassy-rs/embassy" }
[profile.dev]
opt-level = "s"
debug = 2
[profile.release]
codegen-units = 1
lto = "fat"
opt-level = "z"
debug = 2

View file

@ -0,0 +1,6 @@
[build]
target = "thumbv6m-none-eabi"
rustdocflags = ["--document-private-items"]
[env]
DEFMT_LOG = "trace"

50
firmware/app/Cargo.toml Normal file
View file

@ -0,0 +1,50 @@
[package]
name = "app"
version = "0.1.0"
edition = "2024"
[dependencies]
embassy-boot = { workspace = true, features = ["defmt"] }
embassy-boot-stm32 = { workspace = true, features = ["defmt"] }
embassy-executor = { workspace = true, features = ["defmt"] }
embassy-futures = { workspace = true, features = ["defmt"] }
embassy-stm32 = { workspace = true, features = ["defmt"] }
embassy-sync = { workspace = true }
embassy-time = { workspace = true, features = ["defmt"] }
cortex-m = { workspace = true }
cortex-m-rt = { workspace = true }
portable-atomic = { workspace = true }
embedded-hal = { workspace = true }
embedded-io = { workspace = true }
embedded-io-async = { workspace = true }
embedded-resources = { workspace = true }
byteorder = { workspace = true }
static_cell = { workspace = true }
never = { workspace = true }
thiserror = { workspace = true }
defmt = { workspace = true }
defmt-rtt = { workspace = true }
panic-probe = { workspace = true, features = ["print-defmt"] }
micropb = { version = "0.1.1", default-features = false, features = ["encode", "decode", "container-heapless"] }
[build-dependencies]
micropb-gen = "0.1.0"
[features]
default = ["board-nucleo64", "status-led"]
board-nucleo64 = []
status-led = []
copy-within = []
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
cargo = { level = "warn", priority = -1 }
wildcard_imports = "allow"
used_underscore_binding = "allow" # necessary because Embassy macros 'use' task arguments
cargo_common_metadata = "allow"
multiple_crate_versions = "allow" # necessary because the Embassy crates... yeah

62
firmware/app/build.rs Normal file
View file

@ -0,0 +1,62 @@
use std::{env, fs, path::Path};
use micropb_gen::Generator;
fn proto_generate(package_dir: &Path, project_dir: &Path) {
let proto_dir = Path::new(project_dir).join("proto");
let target_dir = package_dir.join("src");
let proto_files: Vec<_> = fs::read_dir(&proto_dir)
.unwrap()
.filter_map(Result::ok)
.filter(|de| {
if let Some(ext) = de.path().extension() {
ext == "proto"
} else {
false
}
})
.map(|de| de.file_name())
.collect();
let mut g = Generator::new();
g.use_container_heapless()
.add_protoc_arg(format!("-I{}", proto_dir.display()))
.compile_protos(&proto_files, target_dir.join("proto.rs"))
.unwrap();
println!("cargo:rerun-if-changed={}", proto_dir.display());
}
fn main() {
let workspace_dir = Path::new(env!("CARGO_WORKSPACE_DIR"));
let package_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let project_dir = workspace_dir.parent().unwrap();
// Make `all-memory.x` available to the linker.
// Tell Cargo where to find the file.
println!(
"cargo:rustc-link-search={}",
workspace_dir.join("common").display()
);
// Tell Cargo to rebuild if the file is updated.
println!(
"cargo:rerun-if-changed={}",
workspace_dir.join("common").join("all-memory.x").display()
);
// Make `memory.x` available to the linker.
// Tell Cargo where to find the file.
println!("cargo:rustc-link-search={}", package_dir.display());
// Tell Cargo to rebuild if the file is updated.
println!(
"cargo:rerun-if-changed={}",
package_dir.join("memory.x").display()
);
println!("cargo:rustc-link-arg-bins=--nmagic");
println!("cargo:rustc-link-arg-bins=-Tlink.x");
println!("cargo:rustc-link-arg-bins=-Tdefmt.x");
proto_generate(package_dir, project_dir);
}

3
firmware/app/memory.x Normal file
View file

@ -0,0 +1,3 @@
INCLUDE all-memory.x
REGION_ALIAS("FLASH", APP)

70
firmware/app/src/crc.rs Normal file
View file

@ -0,0 +1,70 @@
use defmt::*;
use embassy_stm32::crc;
use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex;
use embassy_sync::mutex;
use static_cell::StaticCell;
type CrcEngine = mutex::Mutex<ThreadModeRawMutex, crc::Crc<'static>>;
static ENGINE: StaticCell<CrcEngine> = StaticCell::new();
#[derive(Clone, Copy)]
pub struct Handle {
engine: &'static CrcEngine,
}
pub struct Stream {
guard: mutex::MutexGuard<'static, ThreadModeRawMutex, crc::Crc<'static>>,
}
impl Handle {
pub fn new(r: crate::CrcEngineResources) -> Self {
let cfg = crc::Config::new(
crc::InputReverseConfig::None,
false,
crc::PolySize::Width16,
0x1d0f,
0x1021,
)
.unwrap();
let engine_ref = ENGINE.init(mutex::Mutex::new(crc::Crc::new(r.crc_engine, cfg)));
Handle { engine: engine_ref }
}
#[allow(clippy::cast_possible_truncation)]
pub async fn compute(&self, data: &[u8]) -> u16 {
let mut guard = self.engine.lock().await;
let engine = &mut *guard;
engine.reset();
trace!("CRC computed");
engine.feed_bytes(data) as u16
}
pub async fn stream(&self) -> Stream {
let mut guard = self.engine.lock().await;
let engine = &mut *guard;
trace!("CRC stream started");
engine.reset();
Stream { guard }
}
}
impl Stream {
pub fn feed_bytes(&mut self, data: &[u8]) {
let engine = &mut *self.guard;
engine.feed_bytes(data);
}
#[allow(clippy::cast_possible_truncation)]
pub fn result(&self) -> u16 {
let engine = &*self.guard;
trace!("CRC stream completed");
engine.read() as u16
}
}

View file

@ -0,0 +1,214 @@
use crate::{io, utils};
use defmt::*;
use embassy_time::Duration;
use embedded_io::{ReadReady, Write};
use embedded_io_async::Read;
use micropb::heapless::Vec;
use thiserror::Error;
pub trait Sender {
type Error;
fn send_sync(&mut self) -> Result<(), Self::Error>;
fn send_frame_fragment(&mut self, buf: &[u8]) -> Result<(), Self::Error>;
}
pub trait Receiver {
type Error;
fn buf(&self) -> &[u8];
fn clear_buf(&mut self);
fn remove_frame(&mut self, bytes: usize);
async fn receive_sync(&mut self) -> Result<(), Self::Error>;
async fn receive_frame_fragment(
&mut self,
fragment_pos: usize,
fragment_len: usize,
) -> Result<(), Self::Error>;
}
#[derive(Error)]
pub enum AsyncReadReceiverError<T>
where
T: Read + ReadReady,
{
#[error("insufficent buffer capacity")]
BufferCapacity,
#[error("timeout expired waiting for data")]
Timeout,
#[error("source error")]
Other(T::Error),
}
pub struct AsyncReadReceiver<const N: usize, T>
where
T: Read + ReadReady,
{
buf: &'static mut Vec<u8, N>,
rx: T,
sync: u8,
first_byte_timeout: Duration,
between_bytes_timeout: Duration,
}
impl<const N: usize, T> AsyncReadReceiver<N, T>
where
T: Read + ReadReady,
{
pub fn new(
buf: &'static mut Vec<u8, N>,
rx: T,
sync: u8,
first_byte_timeout: Duration,
between_bytes_timeout: Duration,
) -> Self {
Self {
buf,
rx,
sync,
first_byte_timeout,
between_bytes_timeout,
}
}
}
impl<const N: usize, T> Receiver for AsyncReadReceiver<N, T>
where
T: Read + ReadReady,
{
type Error = AsyncReadReceiverError<T>;
fn buf(&self) -> &[u8] {
self.buf.as_slice()
}
fn clear_buf(&mut self) {
self.buf.clear();
}
fn remove_frame(&mut self, bytes: usize) {
utils::remove_leading_bytes(self.buf, bytes);
}
async fn receive_sync(&mut self) -> Result<(), Self::Error> {
// look for a sync byte in the buffer; the buffer may have
// leftover bytes from a previous cycle (due to an incomplete
// message, or a message which failed validation)
if let Some(sync_pos) = self.buf.iter().position(|&x| x == self.sync) {
// if a sync byte was found, reset the buffer to the data
// following it
//
utils::remove_leading_bytes(self.buf, sync_pos + 1);
} else {
// remove any leftover data in the buffer, since a sync
// byte will begin a new frame
self.buf.clear();
// wait for a sync byte to arrive
let mut buf = [0_u8; 1];
loop {
debug!("waiting for sync byte");
match self.rx.read(&mut buf[..]).await {
Ok(_) => {
trace!("rx byte {=u8:#x}", buf[0]);
if buf[0] == self.sync {
break;
}
}
Err(e) => {
return Err(AsyncReadReceiverError::Other(e));
}
}
}
}
Ok(())
}
async fn receive_frame_fragment(
&mut self,
fragment_pos: usize,
fragment_len: usize,
) -> Result<(), Self::Error> {
// compute the number of bytes available in the buffer, and the
// number of bytes to read
let bytes_available = self.buf[fragment_pos..].len();
let bytes_to_read = fragment_len.saturating_sub(bytes_available);
// if there are enough bytes in the buffer, return
if bytes_to_read == 0 {
return Ok(());
}
// if the fragment will not fit in the buffer's available space,
// return an error
if bytes_to_read > (self.buf.capacity() - self.buf.len()) {
return Err(AsyncReadReceiverError::BufferCapacity);
}
// wait for the needed bytes to arrive, with timeouts
let read_start = fragment_pos + bytes_available;
let read_end = read_start + bytes_to_read;
// make temporary space in the buffer for the bytes to be read
unsafe {
self.buf.set_len(read_end);
}
match io::read_exact_with_timeouts(
&mut self.rx,
&mut self.buf[read_start..read_end],
self.first_byte_timeout,
self.between_bytes_timeout,
)
.await
{
Ok(()) => Ok(()),
Err(io::ReadError::Timeout { bytes_read }) => {
// set the buffer length to include the number of bytes
// that were actually read
unsafe {
self.buf.set_len(read_start + bytes_read);
}
Err(AsyncReadReceiverError::Timeout)
}
Err(io::ReadError::Other { bytes_read, source }) => {
// set the buffer length to include the number of bytes
// that were actually read
unsafe {
self.buf.set_len(read_start + bytes_read);
}
Err(AsyncReadReceiverError::Other(source))
}
}
}
}
pub struct WriteSender<T>
where
T: Write,
{
tx: T,
sync: u8,
}
impl<T> WriteSender<T>
where
T: Write,
{
pub fn new(tx: T, sync: u8) -> Self {
Self { tx, sync }
}
}
impl<T> Sender for WriteSender<T>
where
T: Write,
{
type Error = T::Error;
fn send_sync(&mut self) -> Result<(), Self::Error> {
let buf = [self.sync; 1];
self.tx.write_all(&buf)
}
fn send_frame_fragment(&mut self, buf: &[u8]) -> Result<(), Self::Error> {
self.tx.write_all(buf)
}
}

105
firmware/app/src/io/mod.rs Normal file
View file

@ -0,0 +1,105 @@
use embassy_futures::select::*;
use embassy_time::{Duration, Timer};
use embedded_io::ReadReady;
use embedded_io_async::Read;
use thiserror::Error;
pub mod framed;
pub mod proto;
#[derive(Error)]
pub enum ReadError<T>
where
T: Read + ReadReady,
{
#[error("timeout expired waiting for data")]
Timeout { bytes_read: usize },
#[error("source error")]
Other { bytes_read: usize, source: T::Error },
}
/// Reads data from an object which implements both
/// [`embedded_io_async::Read`] and [`embedded_io::ReadReady`], and
/// supports two timeouts:
///
/// `first_byte`: if no data is available immediately, will wait this
/// long for the first byte to arrive
///
/// `between_bytes`: after the first byte has been read, will wait
/// this long between bytes
///
/// Returns [`ReadError::Timeout`] if a timeout expires; the
/// `bytes_read` field of the error will contain the number of bytes
/// which had been read before the timeout occurred, if any
///
/// Returns [`ReadError::Other`] if an error occurs in the source
/// object; the `bytes_read` field of the error will contain the
/// number of bytes which had been read before the error occurred, if
/// any
///
/// Returns `Ok` if no error or timeout occurs
async fn read_exact_with_timeouts<T>(
rx: &mut T,
mut buf: &mut [u8],
first_byte: Duration,
between_bytes: Duration,
) -> Result<(), ReadError<T>>
where
T: Read + ReadReady,
{
let mut bytes_read = 0;
while !buf.is_empty() {
// check for immediately-available bytes, and read them
match rx.read_ready() {
Ok(false) => {}
Ok(true) => match rx.read(buf).await {
Ok(r) => {
buf = &mut buf[r..];
bytes_read += r;
continue;
}
Err(e) => {
return Err(ReadError::Other {
source: e,
bytes_read,
});
}
},
Err(e) => {
return Err(ReadError::Other {
source: e,
bytes_read,
});
}
}
// set a timeout based on whether the first byte has been read
let timeout = Timer::after(if bytes_read == 0 {
first_byte
} else {
between_bytes
});
// setup a single-byte read for the next byte; if more than
// one byte becomes available, the remainder will be read at
// the top of the next loop iteration
let src = rx.read(&mut buf[0..1]);
// wait for either the read or the timeout to complete
match select(src, timeout).await {
Either::First(Ok(r)) => {
buf = &mut buf[1..];
bytes_read += r;
}
Either::First(Err(e)) => {
return Err(ReadError::Other {
source: e,
bytes_read,
});
}
Either::Second(()) => {
return Err(ReadError::Timeout { bytes_read });
}
}
}
Ok(())
}

View file

@ -0,0 +1,143 @@
use byteorder::{ByteOrder, LittleEndian};
use defmt::*;
use micropb::{DecodeError, MessageDecode, MessageEncode, PbDecoder, PbEncoder, PbWrite};
use never;
use thiserror::Error;
use super::framed;
use crate::{crc, proto};
pub const SYNC_BYTE: u8 = 0xfc;
pub const MESSAGE_LENGTH_SIZE: usize = 2;
pub const MESSAGE_CRC_SIZE: usize = 2;
struct Writer<'a, T>
where
T: framed::Sender,
{
ftx: &'a mut T,
crc: &'a mut crc::Stream,
}
impl<T> PbWrite for Writer<'_, T>
where
T: framed::Sender,
{
type Error = T::Error;
fn pb_write(&mut self, data: &[u8]) -> Result<(), T::Error> {
self.ftx.send_frame_fragment(data)?;
self.crc.feed_bytes(data);
Ok(())
}
}
pub async fn send_target_message<T>(
ftx: &mut T,
msg: proto::api_::TargetMessage,
crc: crc::Handle,
) -> Result<(), T::Error>
where
T: framed::Sender,
{
debug!("sending sync");
ftx.send_sync()?;
debug!("message is {=u16} bytes", msg.compute_size() as u16);
#[allow(clippy::cast_possible_truncation)]
let encoded_size = (msg.compute_size() as u16).to_le_bytes();
ftx.send_frame_fragment(&encoded_size)?;
let mut crc_stream = crc.stream().await;
let mut writer = Writer {
ftx,
crc: &mut crc_stream,
};
let mut encoder = PbEncoder::new(&mut writer);
debug!("sending message");
msg.encode(&mut encoder)?;
let crc_result = crc_stream.result().to_le_bytes();
debug!("sending CRC");
ftx.send_frame_fragment(&crc_result)
}
#[derive(Error)]
pub enum ReceiveError<T>
where
T: framed::Receiver,
{
#[error("sync error")]
Sync,
#[error("CRC-16 mismatch")]
Crc,
#[error("framing error")]
Framing(T::Error),
#[error("decode error")]
Decode(DecodeError<never::Never>),
}
pub async fn receive_host_message<T>(
frx: &mut T,
crc: crc::Handle,
) -> Result<proto::api_::HostMessage, ReceiveError<T>>
where
T: framed::Receiver,
{
frx.receive_sync().await.or(Err(ReceiveError::Sync))?;
debug!("got sync");
// sync byte was seen, read the length of the message
frx.receive_frame_fragment(0, MESSAGE_LENGTH_SIZE)
.await
.map_err(ReceiveError::Framing)?;
debug!("got length");
// get the message length from the buffer
let message_len = LittleEndian::read_u16(&frx.buf()[..MESSAGE_LENGTH_SIZE]) as usize;
// read the message
let message_pos = MESSAGE_LENGTH_SIZE;
frx.receive_frame_fragment(message_pos, message_len)
.await
.map_err(ReceiveError::Framing)?;
debug!("got message");
// read the CRC-16 of the message
let crc_pos = message_pos + message_len;
frx.receive_frame_fragment(crc_pos, MESSAGE_CRC_SIZE)
.await
.map_err(ReceiveError::Framing)?;
debug!("got CRC");
// get the expected CRC-16 from the buffer
let expected_crc = LittleEndian::read_u16(&frx.buf()[crc_pos..crc_pos + MESSAGE_CRC_SIZE]);
// compute the actual CRC-16
let computed_crc = crc
.compute(&frx.buf()[message_pos..message_pos + message_len])
.await;
// check the CRC-16
if computed_crc != expected_crc {
return Err(ReceiveError::Crc);
}
debug!("CRC valid");
// at this point the buffer has been confirmed to contain a
// valid message frame, so any failures beyond this point can
// only drop the message, it cannot be re-parsed
let mut decoder = PbDecoder::new(&frx.buf()[message_pos..message_pos + message_len]);
let mut msg = proto::api_::HostMessage::default();
let result = msg
.decode(&mut decoder, message_len)
.map_err(ReceiveError::Decode)
.map(|_x| msg);
frx.remove_frame(crc_pos + MESSAGE_CRC_SIZE);
result
}

185
firmware/app/src/lib.rs Normal file
View file

@ -0,0 +1,185 @@
#![no_std]
extern crate core;
mod crc;
mod io;
mod tasks;
mod utils;
mod proto {
#![allow(clippy::all, clippy::pedantic)]
#![allow(nonstandard_style, unused, irrefutable_let_patterns)]
include!("proto.rs");
}
use core::mem;
use embassy_executor::Spawner;
use embassy_stm32::peripherals::*;
use embassy_stm32::{Peri, bind_interrupts, gpio, rcc, usart, wdg};
use embedded_resources::resource_group;
use tasks::*;
use defmt::*;
use defmt_rtt as _;
#[resource_group(no_aliases)]
struct SupervisorResources {
wdg: IWDG,
#[cfg(all(feature = "status-led", feature = "board-nucleo64"))]
status_led: PA5,
}
#[resource_group(no_aliases)]
struct CrcEngineResources {
crc_engine: CRC,
}
#[resource_group(no_aliases)]
struct HostInterfaceResources {
usart: USART1,
tx_pin: PA9,
rx_pin: PA10,
clr_host_transmit: PC14,
set_host_transmit: PC15,
host_active_led: PB7,
}
#[resource_group(no_aliases)]
struct HeatPumpInterfaceResources {
usart: USART4,
tx_pin: PA0,
rx_pin: PA1,
}
#[resource_group(no_aliases)]
struct ThermostatInterfaceResources {
usart: USART3,
tx_pin: PB2,
rx_pin: PB0,
}
#[resource_group(no_aliases)]
struct UnusedPinsResources {
pa2: PA2,
pa3: PA3,
pa4: PA4,
#[cfg(not(all(feature = "status-led", feature = "board-nucleo64")))]
pa5: PA5,
pa6: PA6,
pa7: PA7,
pa8: PA8,
pa11: PA11,
pa12: PA12,
pa15: PA15,
pb1: PB1,
pb3: PB3,
pb4: PB4,
pb5: PB5,
pb6: PB6,
pb8: PB8,
pb9: PB9,
pc6: PC6,
}
bind_interrupts!(struct Irqs {
USART1 => usart::BufferedInterruptHandler<USART1>;
USART3_4 => usart::BufferedInterruptHandler<USART3>, usart::BufferedInterruptHandler<USART4>;
});
pub fn spawn_tasks(spawner: Spawner) {
let p = embassy_stm32::init(build_rcc_config());
// enable Prefetch and Instruction Cache in FLASH peripheral
embassy_stm32::pac::FLASH.acr().modify(|w| {
w.set_prften(true);
w.set_icen(true);
});
disable_unused_peripherals();
configure_unused_pins(unused_pins_resources!(p));
let r = supervisor_resources!(p);
#[cfg(feature = "status-led")]
{
let led = gpio::Output::new(r.status_led, gpio::Level::Low, gpio::Speed::Low);
spawner.must_spawn(supervisor::status_task(led));
}
let wdg = wdg::IndependentWatchdog::new(r.wdg, 4_000_000);
spawner.must_spawn(supervisor::watchdog_task(wdg));
let crc_handle = crc::Handle::new(crc_engine_resources!(p));
host_interface::start(host_interface_resources!(p), spawner, crc_handle);
heat_pump_interface::start(heat_pump_interface_resources!(p), spawner);
thermostat_interface::start(thermostat_interface_resources!(p), spawner);
info!("startup complete");
}
fn configure_unused_pin(p: Peri<'_, impl gpio::Pin>) {
mem::forget(gpio::Input::new(p, gpio::Pull::Up));
}
fn configure_unused_pins(r: UnusedPinsResources) {
configure_unused_pin(r.pa2);
configure_unused_pin(r.pa3);
configure_unused_pin(r.pa4);
#[cfg(not(all(feature = "status-led", feature = "board-nucleo64")))]
configure_unused_pin(r.pa5);
configure_unused_pin(r.pa6);
configure_unused_pin(r.pa7);
configure_unused_pin(r.pa8);
configure_unused_pin(r.pa11);
configure_unused_pin(r.pa12);
configure_unused_pin(r.pa15);
configure_unused_pin(r.pb1);
configure_unused_pin(r.pb3);
configure_unused_pin(r.pb4);
configure_unused_pin(r.pb5);
configure_unused_pin(r.pb6);
configure_unused_pin(r.pb8);
configure_unused_pin(r.pb9);
configure_unused_pin(r.pc6);
}
fn build_rcc_config() -> embassy_stm32::Config {
let mut config = embassy_stm32::Config::default();
config.rcc.hsi = Some(rcc::Hsi {
sys_div: rcc::HsiSysDiv::DIV1,
});
config.rcc.pll = Some(rcc::Pll {
source: rcc::PllSource::HSI,
prediv: rcc::PllPreDiv::DIV1,
mul: rcc::PllMul::MUL16,
divp: None,
divq: Some(rcc::PllQDiv::DIV2), // 16 / 1 * 16 / 2 = 128 MHz
divr: Some(rcc::PllRDiv::DIV4), // 16 / 1 * 16 / 4 = 64 MHz
});
config.rcc.sys = rcc::Sysclk::PLL1_R;
config
}
fn disable_unused_peripherals() {
rcc::disable::<ADC1>();
rcc::disable::<I2C1>();
rcc::disable::<I2C2>();
rcc::disable::<I2C1>();
rcc::disable::<I2C2>();
rcc::disable::<SPI1>();
rcc::disable::<SPI2>();
rcc::disable::<TIM3>();
rcc::disable::<TIM6>();
rcc::disable::<TIM7>();
rcc::disable::<TIM14>();
rcc::disable::<TIM15>();
rcc::disable::<TIM16>();
rcc::disable::<TIM17>();
rcc::disable::<USART2>();
}

11
firmware/app/src/main.rs Normal file
View file

@ -0,0 +1,11 @@
#![no_std]
#![no_main]
use embassy_executor::Spawner;
use panic_probe as _;
#[embassy_executor::main]
async fn main(spawner: Spawner) {
app::spawn_tasks(spawner);
}

390
firmware/app/src/proto.rs Normal file
View file

@ -0,0 +1,390 @@
pub mod echo_ {
#[derive(Debug, Clone)]
pub struct EchoRequest {}
impl ::core::default::Default for EchoRequest {
fn default() -> Self {
Self {}
}
}
impl ::core::cmp::PartialEq for EchoRequest {
fn eq(&self, other: &Self) -> bool {
let mut ret = true;
ret
}
}
impl EchoRequest {}
impl ::micropb::MessageDecode for EchoRequest {
fn decode<IMPL_MICROPB_READ: ::micropb::PbRead>(
&mut self,
decoder: &mut ::micropb::PbDecoder<IMPL_MICROPB_READ>,
len: usize,
) -> Result<(), ::micropb::DecodeError<IMPL_MICROPB_READ::Error>> {
use ::micropb::{PbVec, PbMap, PbString, FieldDecode};
let before = decoder.bytes_read();
while decoder.bytes_read() - before < len {
let tag = decoder.decode_tag()?;
match tag.field_num() {
0 => return Err(::micropb::DecodeError::ZeroField),
_ => {
decoder.skip_wire_value(tag.wire_type())?;
}
}
}
Ok(())
}
}
impl ::micropb::MessageEncode for EchoRequest {
fn encode<IMPL_MICROPB_WRITE: ::micropb::PbWrite>(
&self,
encoder: &mut ::micropb::PbEncoder<IMPL_MICROPB_WRITE>,
) -> Result<(), IMPL_MICROPB_WRITE::Error> {
use ::micropb::{PbVec, PbMap, PbString, FieldEncode};
Ok(())
}
fn compute_size(&self) -> usize {
use ::micropb::{PbVec, PbMap, PbString, FieldEncode};
let mut size = 0;
size
}
}
#[derive(Debug, Clone)]
pub struct EchoResponse {}
impl ::core::default::Default for EchoResponse {
fn default() -> Self {
Self {}
}
}
impl ::core::cmp::PartialEq for EchoResponse {
fn eq(&self, other: &Self) -> bool {
let mut ret = true;
ret
}
}
impl EchoResponse {}
impl ::micropb::MessageDecode for EchoResponse {
fn decode<IMPL_MICROPB_READ: ::micropb::PbRead>(
&mut self,
decoder: &mut ::micropb::PbDecoder<IMPL_MICROPB_READ>,
len: usize,
) -> Result<(), ::micropb::DecodeError<IMPL_MICROPB_READ::Error>> {
use ::micropb::{PbVec, PbMap, PbString, FieldDecode};
let before = decoder.bytes_read();
while decoder.bytes_read() - before < len {
let tag = decoder.decode_tag()?;
match tag.field_num() {
0 => return Err(::micropb::DecodeError::ZeroField),
_ => {
decoder.skip_wire_value(tag.wire_type())?;
}
}
}
Ok(())
}
}
impl ::micropb::MessageEncode for EchoResponse {
fn encode<IMPL_MICROPB_WRITE: ::micropb::PbWrite>(
&self,
encoder: &mut ::micropb::PbEncoder<IMPL_MICROPB_WRITE>,
) -> Result<(), IMPL_MICROPB_WRITE::Error> {
use ::micropb::{PbVec, PbMap, PbString, FieldEncode};
Ok(())
}
fn compute_size(&self) -> usize {
use ::micropb::{PbVec, PbMap, PbString, FieldEncode};
let mut size = 0;
size
}
}
}
pub mod api_ {
pub mod HostMessage_ {
#[derive(Debug, PartialEq, Clone)]
pub enum Msg {
Echo(super::super::echo_::EchoRequest),
}
}
#[derive(Debug, Clone)]
pub struct HostMessage {
pub r#id: u32,
pub r#msg: ::core::option::Option<HostMessage_::Msg>,
}
impl ::core::default::Default for HostMessage {
fn default() -> Self {
Self {
r#id: ::core::default::Default::default(),
r#msg: ::core::default::Default::default(),
}
}
}
impl ::core::cmp::PartialEq for HostMessage {
fn eq(&self, other: &Self) -> bool {
let mut ret = true;
ret &= (self.r#id == other.r#id);
ret &= (self.r#msg == other.r#msg);
ret
}
}
impl HostMessage {
///Return a reference to `id`
#[inline]
pub fn r#id(&self) -> &u32 {
&self.r#id
}
///Return a mutable reference to `id`
#[inline]
pub fn mut_id(&mut self) -> &mut u32 {
&mut self.r#id
}
///Set the value of `id`
#[inline]
pub fn set_id(&mut self, value: u32) -> &mut Self {
self.r#id = value.into();
self
}
///Builder method that sets the value of `id`. Useful for initializing the message.
#[inline]
pub fn init_id(mut self, value: u32) -> Self {
self.r#id = value.into();
self
}
}
impl ::micropb::MessageDecode for HostMessage {
fn decode<IMPL_MICROPB_READ: ::micropb::PbRead>(
&mut self,
decoder: &mut ::micropb::PbDecoder<IMPL_MICROPB_READ>,
len: usize,
) -> Result<(), ::micropb::DecodeError<IMPL_MICROPB_READ::Error>> {
use ::micropb::{PbVec, PbMap, PbString, FieldDecode};
let before = decoder.bytes_read();
while decoder.bytes_read() - before < len {
let tag = decoder.decode_tag()?;
match tag.field_num() {
0 => return Err(::micropb::DecodeError::ZeroField),
1u32 => {
let mut_ref = &mut self.r#id;
{
let val = decoder.decode_varint32()?;
let val_ref = &val;
if *val_ref != 0 {
*mut_ref = val as _;
}
};
}
2u32 => {
let mut_ref = loop {
if let ::core::option::Option::Some(variant) = &mut self
.r#msg
{
if let HostMessage_::Msg::Echo(variant) = &mut *variant {
break &mut *variant;
}
}
self.r#msg = ::core::option::Option::Some(
HostMessage_::Msg::Echo(::core::default::Default::default()),
);
};
mut_ref.decode_len_delimited(decoder)?;
}
_ => {
decoder.skip_wire_value(tag.wire_type())?;
}
}
}
Ok(())
}
}
impl ::micropb::MessageEncode for HostMessage {
fn encode<IMPL_MICROPB_WRITE: ::micropb::PbWrite>(
&self,
encoder: &mut ::micropb::PbEncoder<IMPL_MICROPB_WRITE>,
) -> Result<(), IMPL_MICROPB_WRITE::Error> {
use ::micropb::{PbVec, PbMap, PbString, FieldEncode};
{
let val_ref = &self.r#id;
if *val_ref != 0 {
encoder.encode_varint32(8u32)?;
encoder.encode_varint32(*val_ref as _)?;
}
}
if let Some(oneof) = &self.r#msg {
match &*oneof {
HostMessage_::Msg::Echo(val_ref) => {
let val_ref = &*val_ref;
encoder.encode_varint32(18u32)?;
val_ref.encode_len_delimited(encoder)?;
}
}
}
Ok(())
}
fn compute_size(&self) -> usize {
use ::micropb::{PbVec, PbMap, PbString, FieldEncode};
let mut size = 0;
{
let val_ref = &self.r#id;
if *val_ref != 0 {
size += 1usize + ::micropb::size::sizeof_varint32(*val_ref as _);
}
}
if let Some(oneof) = &self.r#msg {
match &*oneof {
HostMessage_::Msg::Echo(val_ref) => {
let val_ref = &*val_ref;
size
+= 1usize
+ ::micropb::size::sizeof_len_record(
val_ref.compute_size(),
);
}
}
}
size
}
}
pub mod TargetMessage_ {
#[derive(Debug, PartialEq, Clone)]
pub enum Msg {
Echo(super::super::echo_::EchoResponse),
}
}
#[derive(Debug, Clone)]
pub struct TargetMessage {
pub r#id: u32,
pub r#msg: ::core::option::Option<TargetMessage_::Msg>,
}
impl ::core::default::Default for TargetMessage {
fn default() -> Self {
Self {
r#id: ::core::default::Default::default(),
r#msg: ::core::default::Default::default(),
}
}
}
impl ::core::cmp::PartialEq for TargetMessage {
fn eq(&self, other: &Self) -> bool {
let mut ret = true;
ret &= (self.r#id == other.r#id);
ret &= (self.r#msg == other.r#msg);
ret
}
}
impl TargetMessage {
///Return a reference to `id`
#[inline]
pub fn r#id(&self) -> &u32 {
&self.r#id
}
///Return a mutable reference to `id`
#[inline]
pub fn mut_id(&mut self) -> &mut u32 {
&mut self.r#id
}
///Set the value of `id`
#[inline]
pub fn set_id(&mut self, value: u32) -> &mut Self {
self.r#id = value.into();
self
}
///Builder method that sets the value of `id`. Useful for initializing the message.
#[inline]
pub fn init_id(mut self, value: u32) -> Self {
self.r#id = value.into();
self
}
}
impl ::micropb::MessageDecode for TargetMessage {
fn decode<IMPL_MICROPB_READ: ::micropb::PbRead>(
&mut self,
decoder: &mut ::micropb::PbDecoder<IMPL_MICROPB_READ>,
len: usize,
) -> Result<(), ::micropb::DecodeError<IMPL_MICROPB_READ::Error>> {
use ::micropb::{PbVec, PbMap, PbString, FieldDecode};
let before = decoder.bytes_read();
while decoder.bytes_read() - before < len {
let tag = decoder.decode_tag()?;
match tag.field_num() {
0 => return Err(::micropb::DecodeError::ZeroField),
1u32 => {
let mut_ref = &mut self.r#id;
{
let val = decoder.decode_varint32()?;
let val_ref = &val;
if *val_ref != 0 {
*mut_ref = val as _;
}
};
}
2u32 => {
let mut_ref = loop {
if let ::core::option::Option::Some(variant) = &mut self
.r#msg
{
if let TargetMessage_::Msg::Echo(variant) = &mut *variant {
break &mut *variant;
}
}
self.r#msg = ::core::option::Option::Some(
TargetMessage_::Msg::Echo(
::core::default::Default::default(),
),
);
};
mut_ref.decode_len_delimited(decoder)?;
}
_ => {
decoder.skip_wire_value(tag.wire_type())?;
}
}
}
Ok(())
}
}
impl ::micropb::MessageEncode for TargetMessage {
fn encode<IMPL_MICROPB_WRITE: ::micropb::PbWrite>(
&self,
encoder: &mut ::micropb::PbEncoder<IMPL_MICROPB_WRITE>,
) -> Result<(), IMPL_MICROPB_WRITE::Error> {
use ::micropb::{PbVec, PbMap, PbString, FieldEncode};
{
let val_ref = &self.r#id;
if *val_ref != 0 {
encoder.encode_varint32(8u32)?;
encoder.encode_varint32(*val_ref as _)?;
}
}
if let Some(oneof) = &self.r#msg {
match &*oneof {
TargetMessage_::Msg::Echo(val_ref) => {
let val_ref = &*val_ref;
encoder.encode_varint32(18u32)?;
val_ref.encode_len_delimited(encoder)?;
}
}
}
Ok(())
}
fn compute_size(&self) -> usize {
use ::micropb::{PbVec, PbMap, PbString, FieldEncode};
let mut size = 0;
{
let val_ref = &self.r#id;
if *val_ref != 0 {
size += 1usize + ::micropb::size::sizeof_varint32(*val_ref as _);
}
}
if let Some(oneof) = &self.r#msg {
match &*oneof {
TargetMessage_::Msg::Echo(val_ref) => {
let val_ref = &*val_ref;
size
+= 1usize
+ ::micropb::size::sizeof_len_record(
val_ref.compute_size(),
);
}
}
}
size
}
}
}

View file

@ -0,0 +1,4 @@
pub mod heat_pump_interface;
pub mod host_interface;
pub mod supervisor;
pub mod thermostat_interface;

View file

@ -0,0 +1,57 @@
use embassy_executor::Spawner;
use embassy_stm32::usart;
use static_cell::StaticCell;
struct UartBuf {
buffer: [u8; 32],
}
impl Default for UartBuf {
fn default() -> UartBuf {
unsafe { core::mem::zeroed() }
}
}
static UART_TX_BUF: StaticCell<UartBuf> = StaticCell::new();
static UART_RX_BUF: StaticCell<UartBuf> = StaticCell::new();
#[allow(dead_code)]
struct Tx {
uart: usart::BufferedUartTx<'static>,
}
#[allow(dead_code)]
struct Rx {
uart: usart::BufferedUartRx<'static>,
}
pub fn start(r: crate::HeatPumpInterfaceResources, spawner: Spawner) {
let tx_buf = UART_TX_BUF.init(UartBuf::default());
let rx_buf = UART_RX_BUF.init(UartBuf::default());
let mut config = usart::Config::default();
config.baudrate = 2400;
config.parity = usart::Parity::ParityEven;
config.tx_config = usart::OutputConfig::OpenDrain;
let (uart_tx, uart_rx) = usart::BufferedUart::new(
r.usart,
r.rx_pin,
r.tx_pin,
&mut tx_buf.buffer,
&mut rx_buf.buffer,
crate::Irqs,
config,
)
.unwrap()
.split();
spawner.must_spawn(heat_pump_tx_task(Tx { uart: uart_tx }));
spawner.must_spawn(heat_pump_rx_task(Rx { uart: uart_rx }));
}
#[embassy_executor::task]
async fn heat_pump_tx_task(_tx: Tx) {}
#[embassy_executor::task]
async fn heat_pump_rx_task(_rx: Rx) {}

View file

@ -0,0 +1,197 @@
use crate::io::framed::Receiver;
use crate::{crc, io, proto};
use defmt::*;
use embassy_executor::Spawner;
use embassy_stm32::{gpio, usart};
use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex;
use embassy_sync::channel;
use embassy_time::Duration;
use static_cell::StaticCell;
use micropb::{DecodeError, heapless::Vec};
use crate::proto::api_::*;
use crate::proto::echo_::*;
const MAX_HOST_MESSAGE_SIZE: usize =
io::proto::MESSAGE_LENGTH_SIZE + io::proto::MESSAGE_CRC_SIZE + 2048;
struct UartBuf {
buffer: [u8; 128],
}
impl Default for UartBuf {
fn default() -> UartBuf {
unsafe { core::mem::zeroed() }
}
}
static UART_TX_BUF: StaticCell<UartBuf> = StaticCell::new();
static UART_RX_BUF: StaticCell<UartBuf> = StaticCell::new();
type MessageBuf = Vec<u8, MAX_HOST_MESSAGE_SIZE>;
static MESSAGE_BUF: StaticCell<MessageBuf> = StaticCell::new();
type TargetMessageChannel = channel::Channel<ThreadModeRawMutex, proto::api_::TargetMessage, 3>;
static TARGET_MESSAGE_CHANNEL: TargetMessageChannel = channel::Channel::new();
struct Tx {
uart: usart::BufferedUartTx<'static>,
crc: crc::Handle,
}
#[allow(dead_code)]
struct Rx {
message_buf: &'static mut MessageBuf,
uart: usart::BufferedUartRx<'static>,
crc: crc::Handle,
clr_host_transmit: gpio::OutputOpenDrain<'static>,
set_host_transmit: gpio::OutputOpenDrain<'static>,
host_active_led: gpio::OutputOpenDrain<'static>,
}
pub fn start(r: crate::HostInterfaceResources, spawner: Spawner, crc: crc::Handle) {
let tx_buf = UART_TX_BUF.init(UartBuf::default());
let rx_buf = UART_RX_BUF.init(UartBuf::default());
let message_buf = MESSAGE_BUF.init(MessageBuf::new());
let mut config = usart::Config::default();
config.baudrate = 38400;
let (uart_tx, uart_rx) = usart::BufferedUart::new(
r.usart,
r.rx_pin,
r.tx_pin,
&mut tx_buf.buffer,
&mut rx_buf.buffer,
crate::Irqs,
config,
)
.unwrap()
.split();
let clr_host_transmit =
gpio::OutputOpenDrain::new(r.clr_host_transmit, gpio::Level::High, gpio::Speed::Low);
let set_host_transmit =
gpio::OutputOpenDrain::new(r.set_host_transmit, gpio::Level::High, gpio::Speed::Low);
let host_active_led =
gpio::OutputOpenDrain::new(r.host_active_led, gpio::Level::High, gpio::Speed::Low);
spawner.must_spawn(host_tx_task(Tx { uart: uart_tx, crc }));
spawner.must_spawn(host_rx_task(Rx {
message_buf,
uart: uart_rx,
crc,
clr_host_transmit,
set_host_transmit,
host_active_led,
}));
}
#[embassy_executor::task]
async fn host_tx_task(tx: Tx) {
let mut ftx = io::framed::WriteSender::new(tx.uart, io::proto::SYNC_BYTE);
let receiver = TARGET_MESSAGE_CHANNEL.receiver();
loop {
let msg = receiver.receive().await;
debug!("sending message");
let _ = io::proto::send_target_message(&mut ftx, msg, tx.crc).await;
}
}
#[embassy_executor::task]
async fn host_rx_task(rx: Rx) {
let mut frx = io::framed::AsyncReadReceiver::new(
rx.message_buf,
rx.uart,
io::proto::SYNC_BYTE,
Duration::from_millis(250),
Duration::from_millis(50),
);
loop {
match io::proto::receive_host_message(&mut frx, rx.crc).await {
Ok(msg) => {
debug!("message received");
handle_host_message(msg).await;
}
Err(io::proto::ReceiveError::Sync) => {
error!("sync error");
}
Err(io::proto::ReceiveError::Crc) => {
error!("CRC error");
}
Err(io::proto::ReceiveError::Framing(e)) => {
match e {
io::framed::AsyncReadReceiverError::BufferCapacity => {
// the host message will not fit in the
// buffer, so clear the buffer and restart the
// loop
error!("message too large");
frx.clear_buf();
}
io::framed::AsyncReadReceiverError::Timeout => {
// restart loop, since the sync byte seen wasn't
// the beginning of a message, or the sender
// stopped sending
error!("timeout");
}
io::framed::AsyncReadReceiverError::Other(_) => {
// restart loop when a USART error occurs
error!("USART error");
}
}
}
Err(io::proto::ReceiveError::Decode(e)) => match e {
DecodeError::VarIntLimit => {
error!("decoder: VarInt too long");
}
DecodeError::UnexpectedEof => {
error!("decoder: unexpected EOF");
}
DecodeError::Deprecation => {
error!("decoder: deprecated wire type found");
}
DecodeError::UnknownWireType => {
error!("decoder: unknown wire type found");
}
DecodeError::ZeroField => {
error!("decoder: invalid field number 0");
}
DecodeError::Utf8 => {
error!("decoder: invalid UTF-8 string");
}
DecodeError::Capacity => {
error!("decoder: capacity of fixed-length field exceeed");
}
DecodeError::WrongLen => {
error!("decoder: length-delimited-record too long");
}
_ => {
error!("decoder: unknown");
}
},
}
}
}
async fn handle_host_message(host: HostMessage) {
match host.msg {
None => {
error!("empty message");
}
Some(HostMessage_::Msg::Echo(_)) => {
info!("echo request");
TARGET_MESSAGE_CHANNEL
.sender()
.send(TargetMessage {
id: host.id,
msg: Some(TargetMessage_::Msg::Echo(EchoResponse {})),
})
.await;
}
}
}

View file

@ -0,0 +1,30 @@
use defmt::*;
#[cfg(feature = "status-led")]
use embassy_stm32::gpio;
use embassy_stm32::{peripherals, wdg};
#[cfg(feature = "status-led")]
use embassy_time::Timer;
use embassy_time::{Duration, Ticker};
#[cfg(feature = "status-led")]
#[embassy_executor::task]
pub async fn status_task(mut led: gpio::Output<'static>) {
let mut ticker = Ticker::every(Duration::from_millis(1500));
loop {
ticker.next().await;
led.toggle();
Timer::after_millis(100).await;
led.toggle();
}
}
#[embassy_executor::task]
pub async fn watchdog_task(mut wdg: wdg::IndependentWatchdog<'static, peripherals::IWDG>) {
let mut ticker = Ticker::every(Duration::from_secs(1));
wdg.unleash();
loop {
ticker.next().await;
wdg.pet();
trace!("watchdog");
}
}

Some files were not shown because too many files have changed in this diff Show more