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}