diff --git a/.github/workflows/firmware-ci.yml b/.github/workflows/firmware-ci.yml index 522ac845a..ffc3e55ae 100644 --- a/.github/workflows/firmware-ci.yml +++ b/.github/workflows/firmware-ci.yml @@ -98,6 +98,8 @@ jobs: run: | make test_adr110 ./test_adr110 + make test_c6_antenna + ./test_c6_antenna - name: Verify binary size budget working-directory: firmware/esp32-csi-node @@ -184,3 +186,22 @@ jobs: name: esp32-csi-node-firmware-${{ matrix.variant }} path: firmware/esp32-csi-node/release-staging/ retention-days: 90 + + build-c6-idf6: + name: Build XIAO C6 external antenna (ESP-IDF 6.0.2) + runs-on: ubuntu-latest + container: + image: espressif/idf:v6.0.2 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + submodules: recursive + + - name: Build the IDF 6 canary image + working-directory: firmware/esp32-csi-node + run: | + . $IDF_PATH/export.sh + idf.py -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32c6;sdkconfig.defaults.xiao-external" set-target esp32c6 + idf.py build + test "$(stat -c%s build/esp32-csi-node.bin)" -le $((1152 * 1024)) + strings build/esp32-csi-node.bin | grep -F "external U.FL" diff --git a/firmware/esp32-csi-node/README.md b/firmware/esp32-csi-node/README.md index 2469814f7..382b841ad 100644 --- a/firmware/esp32-csi-node/README.md +++ b/firmware/esp32-csi-node/README.md @@ -115,6 +115,30 @@ python -m esptool --chip esp32s3 --port COM7 --baud 460800 \ 0x20000 firmware/esp32-csi-node/build/esp32-csi-node.bin ``` +#### Seeed XIAO ESP32-C6 external antenna + +The XIAO ESP32-C6 has a hardware RF switch between its internal ceramic +antenna and U.FL connector. Generic C6 boards do not share this wiring, so the +firmware leaves GPIO3 and GPIO14 untouched by default. For an XIAO with an +external antenna already attached while powered off, build with one of the +opt-in overlays: + +```bash +# Controlled A condition: internal antenna, RF path stated in the boot log. +idf.py -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32c6;sdkconfig.defaults.xiao-internal" \ + set-target esp32c6 +idf.py build + +# Controlled B condition: external U.FL antenna. +idf.py -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32c6;sdkconfig.defaults.xiao-external" \ + set-target esp32c6 +idf.py build +``` + +The boot log must report the intended `internal ceramic` or `external U.FL` +path before that measurement is labelled. The standard C6 build keeps the +board default and emits no antenna-path claim. + ### 3. Provision WiFi credentials (no reflash needed) ```bash diff --git a/firmware/esp32-csi-node/main/CMakeLists.txt b/firmware/esp32-csi-node/main/CMakeLists.txt index dc527813a..e70112167 100644 --- a/firmware/esp32-csi-node/main/CMakeLists.txt +++ b/firmware/esp32-csi-node/main/CMakeLists.txt @@ -14,6 +14,7 @@ set(SRCS "c6_twt.c" "c6_timesync.c" "c6_lp_core.c" + "c6_antenna_select.c" # ADR-110 D1 workaround — ESP-NOW cross-node sync (works on S3+C6) "c6_sync_espnow.c" # ADR-110 B1/B2 unblock — soft-AP HE/TWT (C6-only when enabled) @@ -48,6 +49,12 @@ if(IDF_TARGET STREQUAL "esp32c6") list(APPEND REQUIRES ieee802154 ulp esp_hw_support) endif() +# ESP-IDF 6 split the temperature sensor driver out of the legacy `driver` +# component. Keep the 5.4 lane working while declaring the v6 dependency. +if(IDF_VERSION_MAJOR GREATER_EQUAL 6) + list(APPEND REQUIRES esp_driver_tsens) +endif() + # ADR-061: Mock CSI generator for QEMU testing + ADR-081 mock radio binding if(CONFIG_CSI_MOCK_ENABLED) list(APPEND SRCS "mock_csi.c" "rv_radio_ops_mock.c") diff --git a/firmware/esp32-csi-node/main/Kconfig.projbuild b/firmware/esp32-csi-node/main/Kconfig.projbuild index 9a51c4fd2..404745c5e 100644 --- a/firmware/esp32-csi-node/main/Kconfig.projbuild +++ b/firmware/esp32-csi-node/main/Kconfig.projbuild @@ -1,5 +1,27 @@ menu "CSI Node Configuration" + config C6_XIAO_ANTENNA_SELECT + bool "Control the Seeed XIAO ESP32-C6 RF switch" + depends on IDF_TARGET_ESP32C6 + default n + help + Enable only for Seeed XIAO ESP32-C6 boards. The XIAO uses GPIO3 + to enable RF-switch control and GPIO14 to choose its internal or + external antenna. Leave this disabled for generic ESP32-C6 boards + because GPIO3 and GPIO14 may have unrelated functions there. + + choice C6_XIAO_ANTENNA_PATH + prompt "Seeed XIAO ESP32-C6 antenna path" + depends on C6_XIAO_ANTENNA_SELECT + default C6_XIAO_ANTENNA_INTERNAL + + config C6_XIAO_ANTENNA_INTERNAL + bool "Internal ceramic antenna" + + config C6_XIAO_ANTENNA_EXTERNAL + bool "External U.FL antenna" + endchoice + config CSI_NODE_ID int "Node ID (0-255)" default 1 diff --git a/firmware/esp32-csi-node/main/c6_antenna_select.c b/firmware/esp32-csi-node/main/c6_antenna_select.c new file mode 100644 index 000000000..5b0341039 --- /dev/null +++ b/firmware/esp32-csi-node/main/c6_antenna_select.c @@ -0,0 +1,56 @@ +#include "c6_antenna_select.h" + +#include "sdkconfig.h" + +#if defined(CONFIG_IDF_TARGET_ESP32C6) && defined(CONFIG_C6_XIAO_ANTENNA_SELECT) +#include "driver/gpio.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#define XIAO_RF_SWITCH_POWER_GPIO GPIO_NUM_3 +#define XIAO_RF_SWITCH_SELECT_GPIO GPIO_NUM_14 + +static const char *TAG = "c6_antenna"; +#endif + +esp_err_t c6_xiao_antenna_apply(void) +{ +#if defined(CONFIG_IDF_TARGET_ESP32C6) && defined(CONFIG_C6_XIAO_ANTENNA_SELECT) + gpio_config_t config = { + .pin_bit_mask = (1ULL << XIAO_RF_SWITCH_POWER_GPIO) | + (1ULL << XIAO_RF_SWITCH_SELECT_GPIO), + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + esp_err_t err = gpio_config(&config); + if (err != ESP_OK) { + return err; + } + + err = gpio_set_level(XIAO_RF_SWITCH_POWER_GPIO, 0); + if (err != ESP_OK) { + return err; + } + vTaskDelay(pdMS_TO_TICKS(100)); + +#if defined(CONFIG_C6_XIAO_ANTENNA_EXTERNAL) + const c6_xiao_antenna_t antenna = C6_XIAO_ANTENNA_EXTERNAL; + const char *name = "external U.FL"; +#else + const c6_xiao_antenna_t antenna = C6_XIAO_ANTENNA_INTERNAL; + const char *name = "internal ceramic"; +#endif + + err = gpio_set_level(XIAO_RF_SWITCH_SELECT_GPIO, + c6_xiao_antenna_gpio14_level(antenna)); + if (err == ESP_OK) { + ESP_LOGI(TAG, "XIAO ESP32-C6 antenna path: %s", name); + } + return err; +#else + return ESP_OK; +#endif +} diff --git a/firmware/esp32-csi-node/main/c6_antenna_select.h b/firmware/esp32-csi-node/main/c6_antenna_select.h new file mode 100644 index 000000000..905f9c8ad --- /dev/null +++ b/firmware/esp32-csi-node/main/c6_antenna_select.h @@ -0,0 +1,17 @@ +#pragma once + +typedef enum { + C6_XIAO_ANTENNA_INTERNAL = 0, + C6_XIAO_ANTENNA_EXTERNAL = 1, +} c6_xiao_antenna_t; + +static inline int c6_xiao_antenna_gpio14_level(c6_xiao_antenna_t antenna) +{ + return antenna == C6_XIAO_ANTENNA_EXTERNAL ? 1 : 0; +} + +#ifdef ESP_PLATFORM +#include "esp_err.h" + +esp_err_t c6_xiao_antenna_apply(void); +#endif diff --git a/firmware/esp32-csi-node/main/c6_lp_core.c b/firmware/esp32-csi-node/main/c6_lp_core.c index d6e096ad5..0e4259428 100644 --- a/firmware/esp32-csi-node/main/c6_lp_core.c +++ b/firmware/esp32-csi-node/main/c6_lp_core.c @@ -13,7 +13,7 @@ * `ulp_embed_binary(...)` in main/CMakeLists.txt from lp_core/main.c. * * 2. DISABLED — falls back to plain deep-sleep + GPIO wake-up - * (`esp_deep_sleep_enable_gpio_wakeup`). No debounce, no + * (ESP-IDF deep-sleep GPIO wake API). No debounce, no * sub-10 µA floor, but no LP toolchain dependency either. * This is the path the v0.6.6 firmware shipped with. * @@ -26,6 +26,7 @@ #if defined(CONFIG_IDF_TARGET_ESP32C6) && defined(CONFIG_ULP_COPROC_TYPE_LP_CORE) #include "c6_lp_core.h" +#include "esp_idf_version.h" #include "esp_log.h" #include "esp_sleep.h" #include "driver/rtc_io.h" @@ -134,10 +135,17 @@ esp_err_t c6_lp_core_arm(int wake_gpio, bool active_high) #else /* --- Fallback path: plain deep-sleep GPIO wakeup (~10 µA floor) --- */ uint64_t mask = 1ULL << wake_gpio; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + esp_sleep_gpio_wake_up_mode_t mode = active_high + ? ESP_GPIO_WAKEUP_GPIO_HIGH + : ESP_GPIO_WAKEUP_GPIO_LOW; + esp_err_t err = esp_sleep_enable_gpio_wakeup_on_hp_periph_powerdown(mask, mode); +#else esp_deepsleep_gpio_wake_up_mode_t mode = active_high ? ESP_GPIO_WAKEUP_GPIO_HIGH : ESP_GPIO_WAKEUP_GPIO_LOW; esp_err_t err = esp_deep_sleep_enable_gpio_wakeup(mask, mode); +#endif if (err != ESP_OK) { ESP_LOGE(TAG, "enable_gpio_wakeup failed: %s", esp_err_to_name(err)); return err; diff --git a/firmware/esp32-csi-node/main/main.c b/firmware/esp32-csi-node/main/main.c index 660899708..9431e86f7 100644 --- a/firmware/esp32-csi-node/main/main.c +++ b/firmware/esp32-csi-node/main/main.c @@ -37,6 +37,7 @@ #include "c6_twt.h" /* ADR-110: TWT (no-op stub on S3) */ #include "c6_timesync.h" /* ADR-110: 802.15.4 mesh time-sync (no-op on S3) */ #include "c6_lp_core.h" /* ADR-110: LP-core hibernation (no-op on S3) */ +#include "c6_antenna_select.h" /* XIAO C6 RF switch (opt-in; generic C6 no-op) */ #include "c6_sync_espnow.h" /* ADR-110 D1 workaround: ESP-NOW sync */ #include "c6_softap_he.h" /* ADR-110 B1/B2: HE/TWT soft-AP (no-op when disabled) */ #ifdef CONFIG_CSI_MOCK_ENABLED @@ -331,6 +332,11 @@ void app_main(void) ESP_LOGI(TAG, "%s CSI Node (ADR-018 / ADR-110) — v%s — Node ID: %d", target_name, app_desc->version, g_nvs_config.node_id); + /* Apply the opt-in XIAO RF path before WiFi starts. Generic C6 boards keep + * ownership of GPIO3 and GPIO14 because the default implementation is a + * no-op unless CONFIG_C6_XIAO_ANTENNA_SELECT is enabled. */ + ESP_ERROR_CHECK(c6_xiao_antenna_apply()); + /* Onboard WS2812. C6 wires the LED to GPIO 8; S3 to GPIO 38 (DevKitC-1 v1.0) * or GPIO 48 (DevKitC-1 v1.1 / N16R8 — see #962). On S3 we drive 48 (the * common module). On C6, GPIO 38/48 don't exist (only 0-30) — gate by target. diff --git a/firmware/esp32-csi-node/sdkconfig.defaults.xiao-external b/firmware/esp32-csi-node/sdkconfig.defaults.xiao-external new file mode 100644 index 000000000..2fe679692 --- /dev/null +++ b/firmware/esp32-csi-node/sdkconfig.defaults.xiao-external @@ -0,0 +1,4 @@ +# Seeed XIAO ESP32-C6 external U.FL antenna test overlay. +# Use only with the XIAO board; generic C6 boards may use GPIO3/GPIO14. +CONFIG_C6_XIAO_ANTENNA_SELECT=y +CONFIG_C6_XIAO_ANTENNA_EXTERNAL=y diff --git a/firmware/esp32-csi-node/sdkconfig.defaults.xiao-internal b/firmware/esp32-csi-node/sdkconfig.defaults.xiao-internal new file mode 100644 index 000000000..d60c4a748 --- /dev/null +++ b/firmware/esp32-csi-node/sdkconfig.defaults.xiao-internal @@ -0,0 +1,4 @@ +# Seeed XIAO ESP32-C6 internal ceramic antenna test overlay. +# Keeps RF-switch control enabled so the boot log proves the selected path. +CONFIG_C6_XIAO_ANTENNA_SELECT=y +CONFIG_C6_XIAO_ANTENNA_INTERNAL=y diff --git a/firmware/esp32-csi-node/test/Makefile b/firmware/esp32-csi-node/test/Makefile index f07145574..950eb539a 100644 --- a/firmware/esp32-csi-node/test/Makefile +++ b/firmware/esp32-csi-node/test/Makefile @@ -54,10 +54,20 @@ FUZZ_JOBS ?= 1 .PHONY: all clean run_serialize run_edge run_nvs run_all test_adr110 run_adr110 \ test_vitals run_vitals test_mmwave_detect run_mmwave_detect host_tests \ - test_thermal run_thermal test_csi_sanitize run_csi_sanitize + test_thermal run_thermal test_csi_sanitize run_csi_sanitize \ + test_c6_antenna run_c6_antenna all: fuzz_serialize fuzz_edge fuzz_nvs test_adr110 test_vitals test_mmwave_detect \ - test_thermal test_csi_sanitize + test_thermal test_csi_sanitize test_c6_antenna + +# --- XIAO ESP32-C6 RF switch selection --- +# Host-side truth table for the GPIO14 antenna selector. GPIO3 is always driven +# low by the device implementation before this level is applied. +test_c6_antenna: test_c6_antenna_select.c $(MAIN_DIR)/c6_antenna_select.h + cc -std=c99 -Wall -Wextra -I$(MAIN_DIR) -o $@ $< + +run_c6_antenna: test_c6_antenna + ./test_c6_antenna # --- ESP-IDF invalid first-word sanitation --- # Links the production serializer. Function sections and dead stripping keep @@ -112,8 +122,9 @@ test_thermal: test_thermal_hysteresis.c $(MAIN_DIR)/thermal.h run_thermal: test_thermal ./test_thermal -host_tests: run_adr110 run_vitals run_mmwave_detect run_thermal run_csi_sanitize - @echo "Host tests passed (ADR-110 + CSI sanitation + vitals + mmwave + thermal)" +host_tests: run_adr110 run_vitals run_mmwave_detect run_thermal run_csi_sanitize \ + run_c6_antenna + @echo "Host tests passed (ADR-110 + CSI sanitation + vitals + mmwave + thermal + C6 antenna)" # --- Serialize fuzzer --- # Tests csi_serialize_frame() with random wifi_csi_info_t inputs. @@ -150,5 +161,5 @@ run_all: run_serialize run_edge run_nvs clean: rm -f fuzz_serialize fuzz_edge fuzz_nvs test_adr110 test_vitals test_mmwave_detect \ - test_thermal test_csi_sanitize + test_thermal test_csi_sanitize test_c6_antenna rm -rf corpus_serialize/ corpus_edge/ corpus_nvs/ diff --git a/firmware/esp32-csi-node/test/test_c6_antenna_select.c b/firmware/esp32-csi-node/test/test_c6_antenna_select.c new file mode 100644 index 000000000..4429108ca --- /dev/null +++ b/firmware/esp32-csi-node/test/test_c6_antenna_select.c @@ -0,0 +1,13 @@ +#include +#include + +#include "c6_antenna_select.h" + +int main(void) +{ + assert(c6_xiao_antenna_gpio14_level(C6_XIAO_ANTENNA_INTERNAL) == 0); + assert(c6_xiao_antenna_gpio14_level(C6_XIAO_ANTENNA_EXTERNAL) == 1); + + puts("PASS: XIAO ESP32-C6 antenna selection truth table"); + return 0; +} diff --git a/firmware/esp32-csi-node/version.txt b/firmware/esp32-csi-node/version.txt index 6201b5f77..ef5056161 100644 --- a/firmware/esp32-csi-node/version.txt +++ b/firmware/esp32-csi-node/version.txt @@ -1 +1 @@ -0.8.8 +0.8.10