Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,17 +174,17 @@ Both auto-detect the emulator backend from the board, or accept `--emulator <bac
|---------|-----------|------|--------------|
| **avr8js** | AtmelAVR | ATmega328P | Node.js (bundled headless script) |
| **simavr** | AtmelAVR, MegaAVR | ATmega2560, ATmega32U4, and others | `simavr` binary on PATH |
| **qemu** | Espressif32 | ESP32, ESP32-S3 | Native QEMU (auto-downloaded) |
| **qemu** | Espressif32 | ESP32, ESP32-S3 (Xtensa); ESP32-C3, ESP32-C6, ESP32-H2 (RISC-V) | Native QEMU (auto-downloaded) |

Auto-detection rules when `--emulator` is omitted:

- ATmega328P defaults to **avr8js** (no external binary needed)
- Other AVR MCUs with `simavr` in `debug_tools` default to **simavr**
- ESP32 / ESP32-S3 default to **qemu**
- ESP32, ESP32-S3 (Xtensa) and ESP32-C3, ESP32-C6, ESP32-H2 (RISC-V) default to **qemu**

### QEMU notes

ESP32-S3 QEMU runs from a normal ESP32-S3 Arduino environment. fbuild adds the required QEMU build flags automatically when deploying to `--to emu`.
ESP32-family QEMU runs from a normal Arduino environment. fbuild launches the correct Espressif QEMU binary (`qemu-system-xtensa` for ESP32/ESP32-S3; `qemu-system-riscv32` for ESP32-C3/C6/H2) based on the selected `board`. The required QEMU build flags are injected automatically when deploying to `--to emu`.

```ini
[env:esp32s3]
Expand All @@ -193,6 +193,13 @@ board = esp32-s3-devkitc-1
framework = arduino
board_build.flash_mode = dio
board_upload.flash_mode = dio

[env:esp32c3]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.34/platform-espressif32.zip
board = esp32-c3-devkitm-1
framework = arduino
board_build.flash_mode = dio
board_upload.flash_mode = dio
```

QEMU requires DIO flash mode. Boards configured with `qio` or `qout` will fail fast before building.
Expand All @@ -201,8 +208,8 @@ QEMU runtime is native-only. Supported hosts: Linux x86_64/arm64, macOS x86_64/a

**Known limitations**:

1. **ESP32 QEMU** currently supports ESP32 and ESP32-S3 only. ESP32-C3/C6/S2 are not supported by upstream QEMU.
2. **QEMU-specific firmware patching**: fbuild patches the generated ESP32-S3 app image for QEMU to bypass an ADC calibration constructor that hangs under emulation, then repairs the image checksum and hash.
1. **ESP32 QEMU** supports ESP32, ESP32-S3 (Xtensa) and ESP32-C3, ESP32-C6, ESP32-H2 (RISC-V). ESP32-S2 and ESP32-P4 are not yet supported by upstream Espressif QEMU.
2. **QEMU-specific firmware patching**: fbuild patches the generated ESP32-S3 app image for QEMU to bypass an ADC calibration constructor that hangs under emulation, then repairs the image checksum and hash. RISC-V variants (C3/C6/H2) do not require this patch.
3. **Performance**: QEMU emulation is slower than real hardware. Use it for functional validation, not timing-sensitive behavior.
4. **Peripheral coverage**: Not all peripherals are fully emulated. Real hardware is still required for production validation.

Expand Down
106 changes: 92 additions & 14 deletions crates/fbuild-daemon/src/handlers/emulator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1082,7 +1082,8 @@ pub async fn deploy_qemu(
Json(OperationResponse::fail(
request_id,
format!(
"native QEMU deploy currently supports ESP32 and ESP32-S3 boards, got '{}'",
"native QEMU deploy currently supports ESP32, ESP32-S3 (Xtensa) and \
ESP32-C3, ESP32-C6, ESP32-H2 (RISC-V) boards, got '{}'",
board.mcu
),
)),
Expand Down Expand Up @@ -1132,9 +1133,7 @@ pub async fn deploy_qemu(
}
};

let qemu = match fbuild_packages::toolchain::EspQemuXtensa::new(&project_dir)
.and_then(|pkg| pkg.resolve_executable())
{
let qemu = match resolve_esp_qemu_for_mcu(&project_dir, &board.mcu) {
Ok(path) => path,
Err(e) => {
return (
Expand Down Expand Up @@ -1443,8 +1442,7 @@ impl EmulatorRunner for QemuRunner {
mcu_config.default_flash_size(),
)?;

let qemu = fbuild_packages::toolchain::EspQemuXtensa::new(&self.project_dir)
.and_then(|pkg| pkg.resolve_executable())?;
let qemu = resolve_esp_qemu_for_mcu(&self.project_dir, &self.board.mcu)?;

let session_dir = qemu_session_dir(&self.project_dir, &self.env_name);
std::fs::create_dir_all(&session_dir)?;
Expand Down Expand Up @@ -1716,8 +1714,28 @@ impl EmulatorRunner for SimavrRunner {
}

/// Check whether a given MCU is supported by the QEMU runner.
///
/// Supported MCUs:
/// - Xtensa (`qemu-system-xtensa`): `esp32`, `esp32s3`
/// - RISC-V (`qemu-system-riscv32`): `esp32c3`, `esp32c6`, `esp32h2`
fn is_qemu_supported_esp32_mcu(mcu: &str) -> bool {
mcu.eq_ignore_ascii_case("esp32") || mcu.eq_ignore_ascii_case("esp32s3")
fbuild_packages::toolchain::EspQemuArch::for_mcu(mcu).is_some()
}

/// Resolve the Espressif QEMU executable appropriate for the given MCU.
///
/// Picks `qemu-system-xtensa` for ESP32/ESP32-S3 and `qemu-system-riscv32`
/// for ESP32-C3/C6/H2. Returns the resolved binary path (downloading into
/// the managed fbuild cache if required).
fn resolve_esp_qemu_for_mcu(project_dir: &Path, mcu: &str) -> fbuild_core::Result<PathBuf> {
let arch = fbuild_packages::toolchain::EspQemuArch::for_mcu(mcu).ok_or_else(|| {
fbuild_core::FbuildError::DeployFailed(format!(
"no QEMU backend available for MCU '{}'",
mcu
))
})?;
let pkg = fbuild_packages::toolchain::EspQemu::new(project_dir, arch)?;
pkg.resolve_executable()
}

/// Fail fast if the board's flash mode is incompatible with QEMU (DIO only).
Expand Down Expand Up @@ -1761,7 +1779,8 @@ pub fn select_runner(
}
if !is_qemu_supported_esp32_mcu(&board.mcu) {
return Err(fbuild_core::FbuildError::DeployFailed(format!(
"QEMU runner currently supports ESP32 and ESP32-S3, got '{}'",
"QEMU runner currently supports ESP32, ESP32-S3 (Xtensa) and \
ESP32-C3, ESP32-C6, ESP32-H2 (RISC-V), got '{}'",
board.mcu
)));
}
Expand Down Expand Up @@ -1841,7 +1860,8 @@ pub fn select_runner(
} else {
Err(fbuild_core::FbuildError::DeployFailed(format!(
"no emulator runner available for ESP32 MCU '{}'; \
ESP32 and ESP32-S3 are supported via QEMU",
ESP32, ESP32-S3 (Xtensa) and ESP32-C3, ESP32-C6, ESP32-H2 (RISC-V) \
are supported via QEMU",
board.mcu
)))
}
Expand Down Expand Up @@ -2722,7 +2742,7 @@ mod tests {
}

#[test]
fn select_runner_explicit_qemu_rejects_esp32c3() {
fn select_runner_explicit_qemu_accepts_esp32c3() {
let result = select_runner(
Path::new("/tmp/test"),
"esp32-c3-devkitm-1",
Expand All @@ -2731,9 +2751,60 @@ mod tests {
&HashMap::new(),
Some("qemu"),
);
assert!(
result.is_ok(),
"QEMU should accept ESP32-C3 via qemu-system-riscv32: {:?}",
result.err()
);
}

#[test]
fn select_runner_explicit_qemu_accepts_esp32c6() {
let result = select_runner(
Path::new("/tmp/test"),
"esp32-c6-devkitc-1",
fbuild_core::Platform::Espressif32,
"esp32-c6-devkitc-1",
&HashMap::new(),
Some("qemu"),
);
assert!(
result.is_ok(),
"QEMU should accept ESP32-C6 via qemu-system-riscv32: {:?}",
result.err()
);
}

#[test]
fn select_runner_explicit_qemu_accepts_esp32h2() {
let result = select_runner(
Path::new("/tmp/test"),
"esp32-h2-devkitm-1",
fbuild_core::Platform::Espressif32,
"esp32-h2-devkitm-1",
&HashMap::new(),
Some("qemu"),
);
assert!(
result.is_ok(),
"QEMU should accept ESP32-H2 via qemu-system-riscv32: {:?}",
result.err()
);
}

#[test]
fn select_runner_explicit_qemu_rejects_esp32s2() {
let result = select_runner(
Path::new("/tmp/test"),
"esp32-s2-saola-1",
fbuild_core::Platform::Espressif32,
"esp32-s2-saola-1",
&HashMap::new(),
Some("qemu"),
);
assert!(
result.is_err(),
"QEMU should reject ESP32-C3 (RISC-V, not yet supported)"
"QEMU should reject ESP32-S2 (not emulated upstream)"
);
}

Expand Down Expand Up @@ -2774,18 +2845,25 @@ mod tests {
}

#[test]
fn is_qemu_supported_esp32_mcu_accepts_esp32() {
fn is_qemu_supported_esp32_mcu_accepts_xtensa() {
assert!(is_qemu_supported_esp32_mcu("esp32"));
assert!(is_qemu_supported_esp32_mcu("ESP32"));
assert!(is_qemu_supported_esp32_mcu("esp32s3"));
assert!(is_qemu_supported_esp32_mcu("ESP32S3"));
}

#[test]
fn is_qemu_supported_esp32_mcu_accepts_riscv() {
assert!(is_qemu_supported_esp32_mcu("esp32c3"));
assert!(is_qemu_supported_esp32_mcu("ESP32C3"));
assert!(is_qemu_supported_esp32_mcu("esp32c6"));
assert!(is_qemu_supported_esp32_mcu("esp32h2"));
}

#[test]
fn is_qemu_supported_esp32_mcu_rejects_unsupported() {
assert!(!is_qemu_supported_esp32_mcu("esp32c3"));
assert!(!is_qemu_supported_esp32_mcu("esp32s2"));
assert!(!is_qemu_supported_esp32_mcu("esp32h2"));
assert!(!is_qemu_supported_esp32_mcu("atmega328p"));
assert!(!is_qemu_supported_esp32_mcu("esp32p4"));
}
}
5 changes: 4 additions & 1 deletion crates/fbuild-deploy/src/esp32.rs
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,10 @@ pub fn create_qemu_flash_image(
/// Build the QEMU argv for ESP32-family emulation.
///
/// The `mcu` parameter selects the QEMU machine type and watchdog timer
/// driver name. Supported values: `esp32`, `esp32s2`, `esp32s3`.
/// driver name. Supported values: `esp32`, `esp32s3` (via
/// `qemu-system-xtensa`) and `esp32c3`, `esp32c6`, `esp32h2` (via
/// `qemu-system-riscv32`). Callers are responsible for launching the
/// matching QEMU binary; this function only emits the argv.
pub fn build_qemu_args(
mcu: &str,
flash_image: &Path,
Expand Down
Loading
Loading