Skip to main content

oxstack_cardputer/
lib.rs

1//! Board support for the **M5Stack Cardputer ADV** (ESP32-S3FN8, Stamp-S3A).
2//!
3//! ```ignore
4//! #[esp_rtos::main]
5//! async fn main(_spawner: Spawner) -> ! {
6//!     let mut board = oxstack_cardputer::init(oxstack_cardputer::default_config());
7//!     let _backlight = board.backlight.init();
8//!     let i2c = board.sys_i2c.into_bus();
9//!     let mut display = board.display.init().await.unwrap();
10//!     let mut keyboard = board.keyboard.init(i2c).await.unwrap();
11//!     let fb = oxstack_cardputer::display::framebuffer();
12//!     loop {
13//!         let ev = keyboard.next_event().await.unwrap();
14//!         // draw into `fb`, then:
15//!         display.flush(fb).await.unwrap();
16//!     }
17//! }
18//! ```
19//!
20//! # Model
21//!
22//! [`Board`] splits the chip's peripherals into one *resource group* per
23//! board function: `display`, `keyboard`, `audio`, `spi_bus` and so on. Each
24//! group owns exactly the pins and peripherals that function is wired to, so
25//! the compiler rules out two drivers fighting over a pin. Groups you never
26//! `init` cost nothing.
27//!
28//! Every group has an `init` that **borrows** the group (`&mut self`) and
29//! returns a driver tied to that borrow. Drop the driver and the group is
30//! free again, so one firmware can start the same driver twice, or the
31//! microphone after the speaker, without rebooting:
32//!
33//! ```ignore
34//! let mut mic = board.audio.recorder(i2c, rate, gain).await?;
35//! // ... record ...
36//! drop(mic);
37//! let mut speaker = board.audio.player(i2c, rate, volume).await?; // same hardware
38//! ```
39//!
40//! Groups are plain structs of `esp-hal` peripherals with a `'d` lifetime.
41//! Destructure one to drive the pins yourself, or `reborrow()` it to lend a
42//! shorter-lived copy to a function, exactly as with `esp-hal` peripherals.
43//!
44//! Three things are one-per-boot by nature and keep a consuming API: the
45//! shared I²C bus ([`i2c::SysI2cResources::into_bus`]), the framebuffer
46//! ([`display::framebuffer`]) and the Wi-Fi station ([`net::start_station`]).
47//!
48//! # Project setup
49//!
50//! The quickest start is the template in the repository:
51//!
52//! ```sh
53//! cargo install cargo-generate espup espflash --locked
54//! espup install && source ~/export-esp.sh
55//! cargo generate --git https://github.com/theashraf/oxstack templates/cardputer-app
56//! ```
57//!
58//! By hand, a firmware crate needs:
59//!
60//! * **Dependencies** at the versions this crate pins. The ones you use in
61//!   types are re-exported here ([`esp_hal`], [`esp_rtos`],
62//!   [`embassy_executor`], [`embassy_time`], [`embassy_sync`],
63//!   [`embassy_futures`], [`esp_bootloader_esp_idf`]). Add `esp-backtrace`
64//!   (panic handler) and `esp-println` with `defmt-espflash` + `jtag-serial`
65//!   (log transport) yourself; a library must not pull in a panic handler.
66//!   `#[esp_rtos::main]` expands to paths in `esp_rtos` and
67//!   `embassy_executor`, so list those two as direct dependencies too.
68//! * **`rust-toolchain.toml`** selecting the `esp` channel (`espup install`).
69//! * **`.cargo/config.toml`**: target `xtensa-esp32s3-none-elf`,
70//!   `build-std = ["core", "alloc"]`, `rustflags = ["-C", "link-arg=-nostartfiles"]`
71//!   and an `espflash flash --monitor --log-format defmt` runner.
72//! * **`build.rs`** emitting `-Tdefmt.x` and, last, `-Tlinkall.x`.
73//! * `esp_bootloader_esp_idf::esp_app_desc!();` once in `main.rs`.
74//!
75//! API docs are built for the ESP32-S3 and hosted at
76//! <https://theashraf.github.io/oxstack/oxstack_cardputer/>; docs.rs cannot
77//! build for Xtensa.
78#![no_std]
79#![warn(missing_docs)]
80
81#[cfg(feature = "radio")]
82extern crate alloc;
83
84/// Implements `reborrow` for a resource group: a copy of the group whose
85/// lifetime is that of the borrow, exactly like `esp-hal` peripherals.
86macro_rules! reborrow_group {
87    ($ty:ident { $($field:ident),* $(,)? }) => {
88        impl<'d> $ty<'d> {
89            /// A copy of this group that lives only as long as the borrow,
90            /// to lend the hardware to a function without giving it up.
91            pub fn reborrow(&mut self) -> $ty<'_> {
92                $ty { $($field: self.$field.reborrow()),* }
93            }
94        }
95    };
96}
97
98/// `(buffer, descriptors)` for a DMA buffer of `$size` bytes split into
99/// `$chunk`-byte descriptors, backed by statics in internal RAM.
100///
101/// Each expansion owns one pair of statics and hands out `'static`
102/// references to them, so the caller must guarantee that references from
103/// the previous expansion are dead. Resource groups ensure this by borrowing
104/// themselves for the lifetime of the driver that holds the buffer.
105macro_rules! static_dma_memory {
106    ($size:expr, $chunk:expr) => {{
107        use esp_hal::dma::DmaDescriptor;
108        use esp_hal::dma::aligned::InternalMemory;
109        const COUNT: usize = esp_hal::dma::descriptor_count($size, $chunk);
110        static mut BUFFER: InternalMemory<[u8; $size]> = InternalMemory::new([0; $size]);
111        static mut DESCRIPTORS: InternalMemory<[DmaDescriptor; COUNT]> =
112            InternalMemory::new([DmaDescriptor::EMPTY; COUNT]);
113        // SAFETY: see above. `&raw mut` creates no intermediate reference.
114        unsafe {
115            (
116                (*&raw mut BUFFER).get_mut().unsize(),
117                (*&raw mut DESCRIPTORS).get_mut().unsize(),
118            )
119        }
120    }};
121}
122
123pub mod audio;
124pub mod backlight;
125pub mod battery;
126#[cfg(feature = "ble")]
127pub mod ble;
128mod board;
129pub mod display;
130pub mod i2c;
131pub mod imu;
132pub mod ir;
133pub mod keyboard;
134pub mod led;
135#[cfg(feature = "wifi")]
136pub mod net;
137pub mod pins;
138pub mod radio;
139pub mod rmt;
140pub mod sd;
141
142pub use board::*;
143pub use oxstack_audio as dsp;
144pub use oxstack_drivers as drivers;
145pub use oxstack_gfx as gfx;
146pub use oxstack_input as input;
147// The crates whose types appear in this API, at the versions it pins.
148pub use {
149    embassy_executor, embassy_futures, embassy_sync, embassy_time, esp_bootloader_esp_idf, esp_hal,
150    esp_rtos,
151};
152
153/// Initialises the chip, starts the `esp-rtos` scheduler (required for
154/// Embassy timers), and returns the board. With the `radio` feature it also
155/// adds the reclaimed-RAM heap that `esp-radio` needs.
156///
157/// Use [`Board::new`] instead to keep control of the timer and software
158/// interrupt the scheduler would otherwise claim.
159pub fn init(config: esp_hal::Config) -> Board {
160    let (board, rtos) = Board::new(esp_hal::init(config));
161    #[cfg(feature = "radio")]
162    radio::init_heap();
163    rtos.start();
164    board
165}
166
167/// The configuration [`init`] should normally be given: CPU at 240 MHz.
168pub fn default_config() -> esp_hal::Config {
169    esp_hal::Config::default().with_cpu_clock(esp_hal::clock::CpuClock::max())
170}