Skip to main content

m5stack_core/
lib.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Board support crate for the **M5Stack Fire27** (ESP32) and **CoreS3**
3//! (ESP32-S3).
4//!
5//! Provides chip-agnostic peripheral drivers ([`driver`]), a shared async I2C
6//! bus and reusable `embassy`-based IO task loops ([`io`]), and board bring-up
7//! helpers ([`board`]).
8//!
9//! The crate also owns the board/chip boilerplate a binary would otherwise
10//! hand-roll, so a consumer's `main` collapses to a thin entry shell:
11//!
12//! - [`mem::init_heap`] — the global heap (esp-alloc DRAM regions + HIL-proven
13//!   per-board sizes; `heap` feature, implied by `psram`).
14//! - [`io::console::install`] — one-call logging over the chip's native
15//!   transport (UART0 / USB-Serial-JTAG CDC) + an RTC panic breadcrumb
16//!   ([`io::console::take_panic_breadcrumb`]) + (`app-desc` feature) the
17//!   firmware identity, logged once as the very first BSP-emitted line.
18//! - the `panic-handler` feature exports the `#[panic_handler]`, and
19//!   [`app_desc!`] the esp-idf app descriptor (`app-desc` feature, implied by
20//!   `heap`) — plus [`app_elf_sha256`], and, under `identity`, an enforced
21//!   build-time git identity in `app_desc!()`'s version field (see the
22//!   README's "Firmware identity" section).
23//! - [`board::run_app_core`] — the second-core harness (`multicore` feature).
24//! - [`io::input_caps`] — the board's input model (keypad vs pointer), so a UI
25//!   installs the matching indev without hardcoding the board.
26//!
27//! Exactly one board feature must be enabled: `fire27` (xtensa-esp32) or
28//! `cores3` (xtensa-esp32s3). Radio (`ble`/`wifi`/`wifi-sta`/`coex`), `heap`/
29//! `psram`/`app-desc`/`identity`, `console-serial`, `panic-handler`, and
30//! `multicore` are orthogonal opt-ins. See the README for the full feature
31//! matrix and usage examples.
32#![no_std]
33#![feature(impl_trait_in_assoc_type)]
34#![feature(type_alias_impl_trait)]
35// `PsramSafe` (mem::, `psram` feature) is a Send/Sync-style auto trait with
36// negative impls for the atomic types. The item is `#[cfg(psram)]`, but cfg
37// stripping happens *after* parsing, so the `auto trait` syntax is parsed even
38// in a psram-free `heap` build and would warn unless the gate is active. The
39// crate is nightly-only regardless, so enable it unconditionally.
40#![feature(auto_traits, negative_impls)]
41// `board::run_app_core`'s APP-core idle loop uses the Xtensa `waiti` instruction
42// via inline asm, which is still unstable for this architecture.
43#![cfg_attr(feature = "multicore", feature(asm_experimental_arch))]
44
45// Link-only: pins esp-rom-sys to the version esp-hal's code actually needs
46// (esp-hal 1.1.x under-constrains it to ~0.1 but calls a 0.1.4 API). Referenced
47// here so the pin in Cargo.toml survives `cargo package` rather than being
48// dropped as an unused dependency.
49use esp_rom_sys as _;
50
51#[macro_use]
52mod fmt;
53
54/// Replaces embassy-executor's `Spawner::must_spawn`, dropped in 0.10: panics
55/// on pool exhaustion with call-site context. Works for `Spawner` and
56/// `SendSpawner`.
57#[macro_export]
58macro_rules! must_spawn {
59    ($spawner:expr, $task:expr) => {
60        $spawner.spawn($task.unwrap_or_else(|e| {
61            ::core::panic!(concat!("spawn ", stringify!($task), ": {:?}"), e)
62        }))
63    };
64}
65
66pub mod board;
67pub mod driver;
68pub mod io;
69#[cfg(feature = "heap")]
70pub mod mem;
71
72/// BSP-provided `#[panic_handler]` (opt in with the `panic-handler` feature).
73/// Body is [`io::console::on_panic`]: record the RTC breadcrumb, best-effort
74/// drain the ring over the raw transport, then halt and let the RWDT recover.
75/// A consumer that wants its own panic policy simply leaves the feature off.
76#[cfg(feature = "panic-handler")]
77#[panic_handler]
78fn panic(info: &core::panic::PanicInfo) -> ! {
79    crate::io::console::on_panic(info)
80}
81
82// `app_desc!` wraps esp-bootloader-esp-idf's `esp_app_desc!`. The bootloader
83// crate is pulled by the `app-desc` feature (implied by `heap`); re-exported
84// (hidden) so the macro can name it from the call site without the binary
85// depending on it directly.
86#[cfg(feature = "app-desc")]
87#[doc(hidden)]
88pub use esp_bootloader_esp_idf as __bootloader;
89
90/// Reads back the descriptor [`app_desc!`] emits.
91///
92/// [`app_desc!`] expands to a `static` named `ESP_APP_DESC` **at its call
93/// site** (i.e. in the binary, not this crate), so this reads it back by its
94/// linker symbol (`esp_app_desc`, `EspAppDesc` is `#[repr(C)]`) rather than by
95/// path — the one way a BSP function can reach a descriptor the *consumer*
96/// created. Requires [`app_desc!`] to have been invoked somewhere in the
97/// binary: otherwise this fails to **link**, not silently reads zeroes.
98///
99/// This indirection matters beyond just "how do we reach it": reading
100/// `ESP_APP_DESC` via an `extern` symbol, rather than by path from *inside*
101/// the same crate that defines it, is what keeps [`app_elf_sha256`] correct.
102/// `espflash` patches that field into the flashed image **after**
103/// compilation — the compiler only ever sees the macro's zero initializer.
104/// A same-crate path read of a `static` with a compiler-visible initializer
105/// is free to const-fold to that initializer (needing a `read_volatile` to
106/// stop it); an `extern` read of a symbol whose initializer lives in a
107/// *different* compilation unit has no initializer to see in the first
108/// place, so the hazard cannot arise — no volatile needed. `version` has no
109/// such concern (nothing patches it post-link), but `app_elf_sha256` does:
110/// don't "simplify" this back to a path reference.
111#[cfg(feature = "app-desc")]
112pub fn app_desc() -> &'static __bootloader::EspAppDesc {
113    unsafe extern "C" {
114        #[link_name = "esp_app_desc"]
115        static ESP_APP_DESC: __bootloader::EspAppDesc;
116    }
117    unsafe { &ESP_APP_DESC }
118}
119
120/// Bytes budget for [`__pkg_version_bytes`] — plenty for any real semver
121/// string (`"1.2.3"` is 5 bytes; `"1.0.0-beta.12"` is 13). Unlike the
122/// identity mark, a too-long crate version here is a cosmetic display
123/// nicety, not an identity-tracking risk, so it's a silent truncation, not
124/// enforced by a compile-time assertion the way `app_desc!`'s mark is.
125#[cfg(feature = "app-desc")]
126#[doc(hidden)]
127pub const PKG_VERSION_BYTES: usize = 16;
128
129/// Zero-pads/truncates `s` into a fixed-size, C-safe byte array — the same
130/// shape `EspAppDesc`'s own fields use, chosen deliberately: a plain `&str`
131/// (a fat pointer) has no stable ABI for an `extern` static to read back
132/// across the crate boundary the way [`app_desc()`] reads `EspAppDesc`
133/// (which is `#[repr(C)]`); a fixed byte array does. Used only by
134/// [`app_desc!`]'s expansion, to export `CARGO_PKG_VERSION` for
135/// [`pkg_version()`] to read back — needed because `identity` repurposes
136/// `EspAppDesc::version` for the git mark, so the crate version isn't
137/// available there anymore.
138#[cfg(feature = "app-desc")]
139#[doc(hidden)]
140pub const fn __str_to_fixed<const N: usize>(s: &str) -> [u8; N] {
141    let bytes = s.as_bytes();
142    let mut out = [0u8; N];
143    let mut i = 0;
144    while i < bytes.len() && i < N {
145        out[i] = bytes[i];
146        i += 1;
147    }
148    out
149}
150
151/// Reads back the `CARGO_PKG_VERSION` [`app_desc!`] exports alongside the
152/// descriptor — see [`__str_to_fixed`] for why this needs its own static
153/// rather than reusing [`app_desc()`]'s mechanism.
154///
155/// Gated on `identity`, not merely `app-desc`: without `identity` the
156/// descriptor's own `version` field still holds `CARGO_PKG_VERSION`, so
157/// [`log_boot_identity`] reads it straight from there and this reader has no
158/// caller. `app_desc!` exports the symbol in **both** cases, so a consumer that
159/// wants it can still link against it — only this crate's private reader is
160/// conditional.
161#[cfg(all(feature = "app-desc", feature = "identity"))]
162fn pkg_version() -> &'static str {
163    unsafe extern "C" {
164        #[link_name = "m5stack_core_pkg_version"]
165        static PKG_VERSION: [u8; PKG_VERSION_BYTES];
166    }
167    let bytes = unsafe { &PKG_VERSION };
168    let len = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
169    core::str::from_utf8(&bytes[..len]).unwrap_or("?")
170}
171
172/// The ELF's content hash, straight from the esp-idf application descriptor —
173/// the first bytes distinguish two images built from the same commit with
174/// different uncommitted edits. Unambiguous with no consumer input needed: it
175/// is a function of the linked image, computed and patched in by `espflash`.
176/// Requires [`app_desc!`] to have been invoked (see [`app_desc()`]).
177#[cfg(feature = "app-desc")]
178pub fn app_elf_sha256() -> &'static [u8; 32] {
179    app_desc().app_elf_sha256()
180}
181
182/// Logs the descriptor's version (plain `CARGO_PKG_VERSION`, or the enforced
183/// `<pkg>/<bin>/<features>/<hash><dirty>` git mark under `identity` — which
184/// already carries the binary name, so it isn't repeated separately there),
185/// the crate's `version=` (always the real `CARGO_PKG_VERSION`, even under
186/// `identity` where the descriptor's own version field no longer holds it —
187/// see [`pkg_version()`]), and a 6-byte `app_elf_sha256` prefix, once, as
188/// early as possible on boot — called by [`io::console::install`], not meant
189/// to be called directly. Like [`app_elf_sha256`], requires [`app_desc!`] to
190/// have been invoked somewhere in the binary (a link error otherwise, not a
191/// silent no-op): any binary enabling `app-desc` — directly, or via `heap` —
192/// is expected to call [`app_desc!`], matching this crate's existing "thin
193/// entry shell" framing.
194#[cfg(feature = "app-desc")]
195pub(crate) fn log_boot_identity() {
196    use core::fmt::Write as _;
197
198    let desc = app_desc();
199    let sha = desc.app_elf_sha256();
200    let mut hex = heapless::String::<12>::new();
201    for byte in &sha[..6] {
202        let _ = write!(hex, "{byte:02x}");
203    }
204    #[cfg(feature = "identity")]
205    log::info!(
206        "{} {} version={} app_elf_sha256={hex}",
207        io::console::markers::IDENTITY,
208        desc.version(),
209        pkg_version()
210    );
211    #[cfg(not(feature = "identity"))]
212    log::info!(
213        "{} {} version={} app_elf_sha256={hex}",
214        io::console::markers::IDENTITY,
215        desc.project_name(),
216        desc.version()
217    );
218}
219
220/// Emit the esp-idf application descriptor. Invoke once **in the binary** (not
221/// the BSP) so it captures the *application's* `CARGO_PKG_VERSION`. Thin wrapper
222/// over `esp_bootloader_esp_idf::esp_app_desc!` so the binary keeps a single
223/// `m5stack_core::app_desc!();` line instead of naming the bootloader crate.
224///
225/// With the `identity` feature off (default), the descriptor's version field
226/// is plain `CARGO_PKG_VERSION`, as always. With `identity` on, this same call
227/// site — unchanged — instead requires `M5STACK_CORE_BUILD_MARK`, a build-time
228/// git descriptor the consumer's own `build.rs` sets (see the
229/// `m5stack-core-build` crate). BSP owns the mechanism, never the content: if
230/// the `build.rs` wiring is missing, `env!()` fails to compile in the
231/// *consumer's* crate — a real compile error, not a silent fallback.
232///
233/// `project_name` is `CARGO_BIN_NAME`, not `CARGO_PKG_NAME`: a package can
234/// have more than one `[[bin]]`, and only the per-binary compilation (where
235/// this macro expands) knows which one is being built — `CARGO_PKG_NAME`
236/// would report the same value for every binary in a multi-bin package.
237///
238/// Also exports `CARGO_PKG_VERSION` under its own linker symbol (see
239/// [`__str_to_fixed`]) — even though the descriptor's own `version` field
240/// already holds it here, `identity`'s arm below doesn't have that field
241/// free, so both arms export it the same way for [`log_boot_identity`] /
242/// [`pkg_version()`] to read uniformly.
243#[cfg(all(feature = "app-desc", not(feature = "identity")))]
244#[macro_export]
245macro_rules! app_desc {
246    () => {
247        #[unsafe(export_name = "m5stack_core_pkg_version")]
248        #[used]
249        static __M5STACK_CORE_PKG_VERSION: [u8; $crate::PKG_VERSION_BYTES] =
250            $crate::__str_to_fixed(env!("CARGO_PKG_VERSION"));
251        $crate::__bootloader::esp_app_desc!(
252            env!("CARGO_PKG_VERSION"),
253            env!("CARGO_BIN_NAME"),
254            $crate::__bootloader::BUILD_TIME,
255            $crate::__bootloader::BUILD_DATE,
256            $crate::__bootloader::ESP_IDF_COMPATIBLE_VERSION,
257            $crate::__bootloader::MMU_PAGE_SIZE,
258            0,
259            u16::MAX,
260            $crate::__bootloader::SECURE_VERSION
261        );
262    };
263}
264
265/// See the non-`identity` [`app_desc!`] above — same default call site. The
266/// version field becomes `CARGO_PKG_NAME` + `CARGO_BIN_NAME` joined with the
267/// git mark (`<pkg>/<bin>/<features>/<hash><dirty>`, e.g.
268/// `demos/display/crypto-opt/0f63a4926303+`) — both names included for the
269/// same reason as `project_name` above (a package can have more than one
270/// `[[bin]]`, and disambiguating *which package* matters too once several
271/// crates in a workspace all use `identity`), joined here (rather than by
272/// `m5stack-core-build`) because only this per-binary compilation knows
273/// `CARGO_BIN_NAME`; a `build.rs` runs once per *package* and can't know
274/// which binary it's describing. `CARGO_PKG_VERSION` is exported separately
275/// (see [`__str_to_fixed`]) since the descriptor's `version` field is now
276/// the mark, not the crate version.
277///
278/// `EspAppDesc::version` is a fixed 32-byte C string with no reserved NUL
279/// terminator (see `m5stack-core-build`'s docs) — 31 bytes is the true safe
280/// ceiling, and real package/binary names routinely don't leave room for a
281/// features tag alongside a 12-hex commit (a package/binary pair over ~17
282/// bytes combined already eats the whole budget). Enforced here as a real
283/// compile error, not a silent truncation.
284///
285/// **`app_desc!("prefix")`** — an optional string-literal argument replaces
286/// the automatic `CARGO_PKG_NAME`/`CARGO_BIN_NAME` join with exactly what you
287/// pass, e.g. `app_desc!("oxichg/evcc-hl")`. This is the lever for a project
288/// whose real names don't fit: nothing here truncates or abbreviates on your
289/// behalf (an automatic scheme is a guess, and a wrong guess in an identity
290/// string is worse than no identity — see `#43`'s own reasoning for not
291/// letting the BSP derive a commit itself), so *you* decide what the prefix
292/// says and how long it is. `project_name` still reports the real
293/// `CARGO_BIN_NAME` regardless — only the mark's prefix is overridable.
294/// Default (no argument) is unaffected and unchanged for every existing
295/// caller.
296#[cfg(all(feature = "app-desc", feature = "identity"))]
297#[macro_export]
298macro_rules! app_desc {
299    () => {
300        const _: () = ::core::assert!(
301            ::core::concat!(
302                ::core::env!("CARGO_PKG_NAME"), "/", ::core::env!("CARGO_BIN_NAME"), "/",
303                ::core::env!("M5STACK_CORE_BUILD_MARK")
304            ).len() <= 31,
305            "m5stack_core::app_desc!(): the identity mark (CARGO_PKG_NAME + '/' + CARGO_BIN_NAME \
306             + '/' + M5STACK_CORE_BUILD_MARK) exceeds 31 bytes (EspAppDesc::version's safe \
307             ceiling — see m5stack-core-build's docs). Shorten the `features` tag passed to \
308             emit_identity_env(), or pass a shorter prefix explicitly: \
309             app_desc!(\"short-pkg/short-bin\") instead of the default \
310             CARGO_PKG_NAME/CARGO_BIN_NAME."
311        );
312        #[unsafe(export_name = "m5stack_core_pkg_version")]
313        #[used]
314        static __M5STACK_CORE_PKG_VERSION: [u8; $crate::PKG_VERSION_BYTES] =
315            $crate::__str_to_fixed(env!("CARGO_PKG_VERSION"));
316        $crate::__bootloader::esp_app_desc!(
317            ::core::concat!(
318                ::core::env!("CARGO_PKG_NAME"), "/", ::core::env!("CARGO_BIN_NAME"), "/",
319                ::core::env!("M5STACK_CORE_BUILD_MARK")
320            ),
321            env!("CARGO_BIN_NAME"),
322            $crate::__bootloader::BUILD_TIME,
323            $crate::__bootloader::BUILD_DATE,
324            $crate::__bootloader::ESP_IDF_COMPATIBLE_VERSION,
325            $crate::__bootloader::MMU_PAGE_SIZE,
326            0,
327            u16::MAX,
328            $crate::__bootloader::SECURE_VERSION
329        );
330    };
331    ($prefix:literal) => {
332        const _: () = ::core::assert!(
333            ::core::concat!($prefix, "/", ::core::env!("M5STACK_CORE_BUILD_MARK")).len() <= 31,
334            "m5stack_core::app_desc!(\"prefix\"): the identity mark (prefix + '/' + \
335             M5STACK_CORE_BUILD_MARK) exceeds 31 bytes (EspAppDesc::version's safe ceiling — see \
336             m5stack-core-build's docs). Shorten the `features` tag passed to \
337             emit_identity_env(), or the prefix passed here."
338        );
339        #[unsafe(export_name = "m5stack_core_pkg_version")]
340        #[used]
341        static __M5STACK_CORE_PKG_VERSION: [u8; $crate::PKG_VERSION_BYTES] =
342            $crate::__str_to_fixed(env!("CARGO_PKG_VERSION"));
343        $crate::__bootloader::esp_app_desc!(
344            ::core::concat!($prefix, "/", ::core::env!("M5STACK_CORE_BUILD_MARK")),
345            env!("CARGO_BIN_NAME"),
346            $crate::__bootloader::BUILD_TIME,
347            $crate::__bootloader::BUILD_DATE,
348            $crate::__bootloader::ESP_IDF_COMPATIBLE_VERSION,
349            $crate::__bootloader::MMU_PAGE_SIZE,
350            0,
351            u16::MAX,
352            $crate::__bootloader::SECURE_VERSION
353        );
354    };
355}