Skip to main content

waterui_cli/esp32/
chip.rs

1//! Chip-architecture-aware properties for the ESP32 (Dew) backend.
2//!
3//! The ESP32 family spans two instruction set architectures: the original
4//! Xtensa cores (`esp32`, `esp32s2`, `esp32s3`) and the newer RISC-V cores
5//! (`esp32c3`, `esp32c6`, ...). Almost everything the CLI does for an ESP32
6//! target — the Rust target triple, the QEMU system binary and machine model,
7//! whether QEMU needs the eFuse ADC-calibration workaround, the firmware
8//! console transport, the flash size, the main-task stack, and the codegen
9//! optimization level — follows directly from the chip's architecture.
10//!
11//! [`Esp32Chip`] parses a chip string once and answers all of those questions,
12//! so the rest of the backend never has to special-case a chip by name.
13
14use std::str::FromStr;
15
16use eyre::{Result, eyre};
17use target_lexicon::{Architecture, Riscv32Architecture, Triple};
18
19/// The instruction set architecture of an ESP32-class chip.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum Esp32Arch {
22    /// Tensilica Xtensa LX (`esp32`, `esp32s2`, `esp32s3`).
23    Xtensa,
24    /// RISC-V (`esp32c3`, `esp32c6`, `esp32h2`, ...).
25    RiscV,
26}
27
28/// A supported ESP32-class target chip.
29///
30/// The variant determines the chip's architecture and every architecture- and
31/// chip-specific build/emulation parameter. Parse one with
32/// [`Esp32Chip::from_str`]; the chip string is the single source of truth
33/// (`[backends.esp32] chip` in `Water.toml`, or the selected platform).
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum Esp32Chip {
36    /// ESP32-S3: dual-core Xtensa LX7.
37    Esp32S3,
38    /// ESP32-C3: single-core RISC-V (RV32IMC).
39    Esp32C3,
40    /// ESP32-P4: dual-core RISC-V (RV32IMAFC, hardware single-precision FPU).
41    Esp32P4,
42}
43
44impl Esp32Chip {
45    /// The canonical chip identifier (e.g. `"esp32s3"`), as used by `espflash`,
46    /// QEMU machine models, and `Water.toml`.
47    #[must_use]
48    pub const fn id(self) -> &'static str {
49        match self {
50            Self::Esp32S3 => "esp32s3",
51            Self::Esp32C3 => "esp32c3",
52            Self::Esp32P4 => "esp32p4",
53        }
54    }
55
56    /// The chip's instruction set architecture.
57    #[must_use]
58    pub const fn arch(self) -> Esp32Arch {
59        match self {
60            Self::Esp32S3 => Esp32Arch::Xtensa,
61            Self::Esp32C3 | Self::Esp32P4 => Esp32Arch::RiscV,
62        }
63    }
64
65    /// The Rust target triple to cross-compile the firmware for.
66    ///
67    /// Xtensa chips use a per-chip triple (`xtensa-<chip>-espidf`); RISC-V
68    /// chips use the architecture-level triple matching their ISA extensions
69    /// (`imc` on the C-series, `imafc` — hardware single-float — on the P4).
70    #[must_use]
71    pub const fn target_triple(self) -> &'static str {
72        match self {
73            Self::Esp32S3 => "xtensa-esp32s3-espidf",
74            Self::Esp32C3 => "riscv32imc-esp-espidf",
75            Self::Esp32P4 => "riscv32imafc-esp-espidf",
76        }
77    }
78
79    /// The target triple parsed into a [`Triple`].
80    ///
81    /// # Panics
82    ///
83    /// Panics only if the static triple stops being a valid `target_lexicon`
84    /// triple, which would be a build-time bug in this table.
85    #[must_use]
86    pub fn triple(self) -> Triple {
87        Triple::from_str(self.target_triple())
88            .unwrap_or_else(|error| panic!("{} target triple must be valid: {error}", self.id()))
89    }
90
91    /// The `target_lexicon` architecture for the chip.
92    #[must_use]
93    pub const fn lexicon_arch(self) -> Architecture {
94        match self {
95            Self::Esp32S3 => Architecture::XTensa,
96            Self::Esp32C3 => Architecture::Riscv32(Riscv32Architecture::Riscv32imc),
97            Self::Esp32P4 => Architecture::Riscv32(Riscv32Architecture::Riscv32imafc),
98        }
99    }
100
101    /// The QEMU system binary that emulates this chip's architecture.
102    #[must_use]
103    pub const fn qemu_binary(self) -> &'static str {
104        match self.arch() {
105            Esp32Arch::Xtensa => "qemu-system-xtensa",
106            Esp32Arch::RiscV => "qemu-system-riscv32",
107        }
108    }
109
110    /// The QEMU `-machine` model for this chip (the chip id doubles as the
111    /// machine name in Espressif's QEMU fork).
112    #[must_use]
113    pub const fn qemu_machine(self) -> &'static str {
114        self.id()
115    }
116
117    /// Whether QEMU needs the eFuse ADC-calibration workaround.
118    ///
119    /// Xtensa chips hang at startup in hardware ADC self-calibration, which
120    /// QEMU does not emulate; an eFuse image with calibration version 1 makes
121    /// startup read the (zeroed) calibration codes instead. RISC-V chips boot
122    /// cleanly under QEMU without it.
123    #[must_use]
124    pub const fn needs_qemu_efuse_workaround(self) -> bool {
125        matches!(self.arch(), Esp32Arch::Xtensa)
126    }
127
128    /// The espup toolchain component directory holding the chip's GCC, relative
129    /// to `~/.rustup/toolchains/esp/<component>`, together with the `bin`
130    /// subpath under the discovered version directory.
131    #[must_use]
132    pub const fn gcc_component(self) -> Esp32GccComponent {
133        match self.arch() {
134            Esp32Arch::Xtensa => Esp32GccComponent {
135                component: "xtensa-esp-elf",
136                bin_subpath: "xtensa-esp-elf/bin",
137                what: "Xtensa GCC toolchain",
138            },
139            Esp32Arch::RiscV => Esp32GccComponent {
140                component: "riscv32-esp-elf",
141                bin_subpath: "riscv32-esp-elf/bin",
142                what: "RISC-V GCC toolchain",
143            },
144        }
145    }
146
147    /// The harness firmware parameters that vary by chip: console transport,
148    /// flash size, main-task stack, app-partition size, and codegen profile.
149    #[must_use]
150    pub const fn firmware_params(self) -> Esp32FirmwareParams {
151        match self {
152            // Real S3 devkits expose the USB-Serial-JTAG console; 8 MB flash,
153            // generous stack for the Xtensa rasterization stack, and size-
154            // optimized codegen to dodge the Xtensa LLVM miscompile.
155            Self::Esp32S3 => Esp32FirmwareParams {
156                console_uart_default: false,
157                flash_size_mb: 8,
158                main_task_stack_bytes: 163_840,
159                app_partition_offset: "0x10000",
160                app_partition_size: "0x600000",
161                opt_level: "s",
162            },
163            // The C3 is RISC-V (mainline LLVM backend, no miscompile), so it
164            // builds at -O2. QEMU surfaces UART0, and the chip has ~400 KB
165            // SRAM, so the main task gets a ~48 KB stack against a small panel.
166            Self::Esp32C3 => Esp32FirmwareParams {
167                console_uart_default: true,
168                flash_size_mb: 4,
169                main_task_stack_bytes: 49_152,
170                app_partition_offset: "0x10000",
171                app_partition_size: "0x300000",
172                opt_level: "2",
173            },
174            // The P4 is the RISC-V flagship: dual 400 MHz cores with a
175            // hardware single-precision FPU, 768 KB of L2 memory, and
176            // MIPI-DSI/parallel-RGB LCD peripherals. Mainline LLVM, so full
177            // optimization; boards expose USB-Serial-JTAG and ship 16 MB
178            // flash, and the roomy SRAM affords a 96 KB main task stack.
179            Self::Esp32P4 => Esp32FirmwareParams {
180                console_uart_default: false,
181                flash_size_mb: 16,
182                main_task_stack_bytes: 98_304,
183                app_partition_offset: "0x10000",
184                app_partition_size: "0xC00000",
185                opt_level: "2",
186            },
187        }
188    }
189}
190
191impl FromStr for Esp32Chip {
192    type Err = eyre::Error;
193
194    fn from_str(value: &str) -> Result<Self> {
195        match value {
196            "esp32s3" => Ok(Self::Esp32S3),
197            "esp32c3" => Ok(Self::Esp32C3),
198            "esp32p4" => Ok(Self::Esp32P4),
199            other => Err(eyre!(
200                "unsupported ESP32 chip {other:?}. Supported chips: esp32s3, esp32c3, esp32p4."
201            )),
202        }
203    }
204}
205
206/// Location of a chip's GCC toolchain within the espup `esp` toolchain.
207#[derive(Debug, Clone, Copy)]
208pub struct Esp32GccComponent {
209    /// Component directory under `~/.rustup/toolchains/esp/`.
210    pub component: &'static str,
211    /// `bin` directory relative to the discovered version directory.
212    pub bin_subpath: &'static str,
213    /// Human-readable name for diagnostics.
214    pub what: &'static str,
215}
216
217/// Chip-specific firmware harness parameters.
218///
219/// These are baked into the generated `sdkconfig.defaults`, `partitions.csv`,
220/// and `Cargo.toml` codegen profile, and passed to `espflash` at image-merge
221/// time, so the same harness template renders correctly for any chip.
222#[derive(Debug, Clone, Copy, PartialEq, Eq)]
223pub struct Esp32FirmwareParams {
224    /// Route the firmware console to UART0 (`true`) or USB-Serial-JTAG
225    /// (`false`). QEMU surfaces UART0, so emulated chips must use it.
226    pub console_uart_default: bool,
227    /// Flash size in megabytes (drives `CONFIG_ESPTOOLPY_FLASHSIZE_*MB` and the
228    /// `espflash --flash-size` argument).
229    pub flash_size_mb: u32,
230    /// Main-task stack size in bytes (`CONFIG_ESP_MAIN_TASK_STACK_SIZE`).
231    pub main_task_stack_bytes: u32,
232    /// Offset of the app (`factory`) partition.
233    pub app_partition_offset: &'static str,
234    /// Size of the app (`factory`) partition.
235    pub app_partition_size: &'static str,
236    /// Cargo codegen `opt-level` for the firmware profiles.
237    pub opt_level: &'static str,
238}
239
240impl Esp32FirmwareParams {
241    /// The `espflash --flash-size` value (e.g. `"8mb"`).
242    #[must_use]
243    pub fn flash_size_arg(&self) -> String {
244        format!("{}mb", self.flash_size_mb)
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::{Esp32Arch, Esp32Chip};
251    use std::str::FromStr;
252
253    #[test]
254    fn parses_known_chips_and_rejects_unknown() {
255        assert_eq!(
256            Esp32Chip::from_str("esp32s3").expect("esp32s3 parses"),
257            Esp32Chip::Esp32S3
258        );
259        assert_eq!(
260            Esp32Chip::from_str("esp32c3").expect("esp32c3 parses"),
261            Esp32Chip::Esp32C3
262        );
263        assert_eq!(
264            Esp32Chip::from_str("esp32p4").expect("esp32p4 parses"),
265            Esp32Chip::Esp32P4
266        );
267        assert!(Esp32Chip::from_str("esp32c6").is_err());
268    }
269
270    #[test]
271    fn esp32p4_derives_riscv_fpu_build_parameters() {
272        let p4 = Esp32Chip::Esp32P4;
273        assert_eq!(p4.arch(), Esp32Arch::RiscV);
274        assert_eq!(p4.target_triple(), "riscv32imafc-esp-espidf");
275        assert_eq!(p4.qemu_binary(), "qemu-system-riscv32");
276        assert_eq!(p4.qemu_machine(), "esp32p4");
277        assert!(!p4.needs_qemu_efuse_workaround());
278        assert_eq!(p4.gcc_component().component, "riscv32-esp-elf");
279        let params = p4.firmware_params();
280        assert!(!params.console_uart_default);
281        assert_eq!(params.flash_size_mb, 16);
282        assert_eq!(params.opt_level, "2");
283    }
284
285    #[test]
286    fn xtensa_and_riscv_derive_distinct_build_parameters() {
287        let s3 = Esp32Chip::Esp32S3;
288        assert_eq!(s3.arch(), Esp32Arch::Xtensa);
289        assert_eq!(s3.target_triple(), "xtensa-esp32s3-espidf");
290        assert_eq!(s3.qemu_binary(), "qemu-system-xtensa");
291        assert_eq!(s3.qemu_machine(), "esp32s3");
292        assert!(s3.needs_qemu_efuse_workaround());
293        assert_eq!(s3.gcc_component().component, "xtensa-esp-elf");
294        assert!(!s3.firmware_params().console_uart_default);
295        assert_eq!(s3.firmware_params().flash_size_arg(), "8mb");
296
297        let c3 = Esp32Chip::Esp32C3;
298        assert_eq!(c3.arch(), Esp32Arch::RiscV);
299        assert_eq!(c3.target_triple(), "riscv32imc-esp-espidf");
300        assert_eq!(c3.qemu_binary(), "qemu-system-riscv32");
301        assert_eq!(c3.qemu_machine(), "esp32c3");
302        assert!(!c3.needs_qemu_efuse_workaround());
303        assert_eq!(c3.gcc_component().component, "riscv32-esp-elf");
304        assert!(c3.firmware_params().console_uart_default);
305        assert_eq!(c3.firmware_params().flash_size_arg(), "4mb");
306    }
307
308    #[test]
309    fn triple_parses_for_every_chip() {
310        for chip in [Esp32Chip::Esp32S3, Esp32Chip::Esp32C3] {
311            let triple = chip.triple();
312            assert_eq!(triple.architecture, chip.lexicon_arch());
313        }
314    }
315}