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}