/* * SPDX-License-Identifier: MIT */ #include "ZephyrBoard.h" #include "battery_curve.h" #include "led_gate.h" #include #include #include #include #include #include #if defined(CONFIG_SOC_SERIES_NRF52) #include /* Adafruit bootloader GPREGRET magic values */ #define BOOTLOADER_DFU_SERIAL_MAGIC 0x4e /* Enter serial DFU mode (CDC only) */ #define BOOTLOADER_DFU_UF2_MAGIC 0x57 /* Enter UF2 mass storage mode (CDC + MSC) */ #define BOOTLOADER_DFU_OTA_MAGIC 0xA8 /* Enter BLE OTA DFU mode */ #define NRF52_GPREGRET 1 #endif /* ESP32 boards whose console is the native USB-Serial-JTAG: sys_reboot() does * a digital soft reset but does not detach the USB PHY, so the D+ pull-up stays * asserted and the host never sees a disconnect. On macOS the serial port then * wedges after a settings reboot (GH #43). Drop the pad before rebooting so the * host gets a clean disconnect/re-enumerate. Gated to exactly the boards that * both exhibit the defect and expose the PHY knob to fix it. */ #if defined(CONFIG_SOC_FAMILY_ESPRESSIF_ESP32) && \ DT_NODE_HAS_COMPAT(DT_CHOSEN(zephyr_console), espressif_esp32_usb_serial) #include #define ESP32_USB_SERIAL_DETACH 1 #endif /* ESP32-S3: reboot into the ROM download mode rather than back into the app. * * The USB CDC companion transport (boards/common/esp32s3_usb.conf) hands the * shared D+/D- pads to USB OTG and disables USB-Serial-JTAG. USJ is exactly * what esptool drives to put the chip into download mode, so once our CDC-ACM * device owns the port esptool's auto-reset is inert: DTR/RTS land on a Zephyr * CDC endpoint that is not wired to EN/BOOT, and the only way back into the * bootloader is the physical BOOT button. * * FORCE_DOWNLOAD_BOOT lives in the RTC domain, which a software system reset * does not clear (only a power-on reset does), so setting it and rebooting * brings the ROM up in download mode with USB-Serial-JTAG back on the pads. * That gives both `start dfu` and the Arduino-style 1200-baud touch a way to * hand the port back to esptool / esptool-js / the web configurator. * * Same register and sequence Arduino-ESP32 uses in usb_persist_restart() * (RESTART_BOOTLOADER). OPTION1 carries no other bits on this SoC, but set the * bit rather than writing the word so it stays correct if that changes. Scoped * to the S3 deliberately: it is the only part we ship whose USB port can be * taken away from the ROM this way (C3/C6 use a different LP_AON register, and * classic ESP32 has no native USB — it always flashes through its UART bridge). */ #if defined(CONFIG_SOC_SERIES_ESP32S3) #include #include #define ESP32_FORCE_DOWNLOAD_BOOT 1 #endif /* Detach the USB device stack before a reset so the host sees a real unplug — * see zephcore_usbd_detach(). Matters most on the ESP32-S3 USB companion, * whose OTG PHY survives a soft reset with D+ still pulled up. * * The condition below must be EXACTLY the one CMakeLists.txt uses to compile * adapters/usb/ZephyrUSBCDC.cpp, or this call site compiles against an * implementation that is never linked (findings #22 — that failure mode has * already cost this repo four separate undefined-reference bugs). The * repeater/observer/room-server branches use the inner half alone; the * companion branch wraps it in (LOG || COMPANION_USB || COMPANION_SERIAL), and * ZEPHCORE_COMPANION is the compile definition that identifies that branch. */ #if !defined(CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT) && \ (defined(CONFIG_USB_CDC_ACM) || defined(CONFIG_USBD_CDC_ACM_CLASS)) #if !defined(ZEPHCORE_COMPANION) || defined(CONFIG_LOG) || \ defined(CONFIG_ZEPHCORE_COMPANION_USB) || defined(CONFIG_ZEPHCORE_COMPANION_SERIAL) #include #define ZEPHCORE_USBD_DETACH 1 #endif #endif /* LoRa TX activity LED (optional — defined per-board via DT alias) */ #if DT_NODE_EXISTS(DT_ALIAS(lora_tx_led)) static const struct gpio_dt_spec tx_led = GPIO_DT_SPEC_GET(DT_ALIAS(lora_tx_led), gpios); #define HAS_TX_LED 1 #else #define HAS_TX_LED 0 #endif #include LOG_MODULE_REGISTER(zephcore_board, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL); #if DT_NODE_EXISTS(DT_PATH(zephyr_user)) && \ DT_NODE_HAS_PROP(DT_PATH(zephyr_user), io_channels) #include #include /* Battery ADC channel from devicetree zephyr,user { io-channels } */ static const struct adc_dt_spec vbat_adc = ADC_DT_SPEC_GET(DT_PATH(zephyr_user)); static bool vbat_adc_configured; /* Battery ADC enable regulator (optional - saves power when not reading) */ #if DT_NODE_EXISTS(DT_NODELABEL(vbat_enable)) static const struct device *vbat_enable_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(vbat_enable)); #else static const struct device *vbat_enable_dev = NULL; #endif /* * Battery voltage multiplier - prefer devicetree, fallback to Kconfig * Formula: Battery_mV = (raw * VBAT_MV_MULTIPLIER) / 4096 * * To define in devicetree, add to board's DTS/overlay: * zephyr,user { * vbat-mv-multiplier = <7200>; * }; */ #define ZEPHYR_USER_NODE DT_PATH(zephyr_user) #if DT_NODE_HAS_PROP(ZEPHYR_USER_NODE, vbat_mv_multiplier) #define VBAT_MV_MULTIPLIER DT_PROP(ZEPHYR_USER_NODE, vbat_mv_multiplier) #else #define VBAT_MV_MULTIPLIER CONFIG_ZEPHCORE_VBAT_MV_MULTIPLIER #endif #define VBAT_ADC_SAMPLES 8 #endif /* Battery fuel gauge (AXP2101 PMU etc.) — preferred over the ADC divider when * present. Reports battery voltage and state-of-charge over I2C, so boards with * a PMU and no battery ADC (e.g. LilyGo T-Beam) still get battery telemetry. */ #if DT_HAS_COMPAT_STATUS_OKAY(x_powers_axp2101_fuel_gauge) #include #define HAS_FUEL_GAUGE 1 static const struct device *const fuel_gauge_dev = DEVICE_DT_GET(DT_COMPAT_GET_ANY_STATUS_OKAY(x_powers_axp2101_fuel_gauge)); #else #define HAS_FUEL_GAUGE 0 #endif /* Initialize TX LED GPIO at boot */ #if HAS_TX_LED static int tx_led_init(void) { if (gpio_is_ready_dt(&tx_led)) { gpio_pin_configure_dt(&tx_led, GPIO_OUTPUT_INACTIVE); } return 0; } SYS_INIT(tx_led_init, APPLICATION, 90); #endif namespace mesh { uint16_t ZephyrBoard::getBattMilliVolts() { #if HAS_FUEL_GAUGE if (device_is_ready(fuel_gauge_dev)) { union fuel_gauge_prop_val val; int ret = fuel_gauge_get_prop(fuel_gauge_dev, FUEL_GAUGE_VOLTAGE, &val); if (ret == 0) { return (uint16_t)(val.voltage / 1000); /* µV -> mV */ } LOG_WRN("Fuel gauge voltage read failed: %d", ret); } return 0; #elif DT_NODE_EXISTS(DT_PATH(zephyr_user)) && \ DT_NODE_HAS_PROP(DT_PATH(zephyr_user), io_channels) if (!adc_is_ready_dt(&vbat_adc)) { LOG_ERR("ADC not ready"); return 0; } if (!vbat_adc_configured) { int ret = adc_channel_setup_dt(&vbat_adc); if (ret < 0) { LOG_ERR("ADC channel setup failed: %d", ret); return 0; } vbat_adc_configured = true; } /* Enable battery ADC voltage divider (saves power when not reading) */ if (vbat_enable_dev && device_is_ready(vbat_enable_dev)) { regulator_enable(vbat_enable_dev); k_msleep(10); /* 10ms settling time for voltage divider + capacitor (matches Arduino) */ } int32_t raw = 0; int valid_samples = 0; for (int i = 0; i < VBAT_ADC_SAMPLES; i++) { int16_t val = 0; /* Use int16_t for 12-bit ADC */ struct adc_sequence seq = { .buffer = &val, .buffer_size = sizeof(val), }; int ret = adc_sequence_init_dt(&vbat_adc, &seq); if (ret < 0) { LOG_WRN("ADC sequence init failed: %d", ret); continue; } ret = adc_read_dt(&vbat_adc, &seq); if (ret == 0) { raw += val; valid_samples++; } else { LOG_WRN("ADC read failed: %d", ret); } } /* Disable battery ADC voltage divider to save power */ if (vbat_enable_dev && device_is_ready(vbat_enable_dev)) { regulator_disable(vbat_enable_dev); } if (valid_samples == 0) { LOG_ERR("No valid ADC samples"); return 0; } raw /= valid_samples; int64_t mult = (_adc_multiplier_override != 0.0f) ? (int64_t)_adc_multiplier_override : (int64_t)VBAT_MV_MULTIPLIER; uint16_t mv = (uint16_t)((mult * (int64_t)raw) / 4096); LOG_DBG("Battery: raw=%d multiplier=%lld mv=%u", (int)raw, (long long)mult, mv); return mv; #else return 0; #endif } uint8_t ZephyrBoard::getBattPercent() { #if HAS_FUEL_GAUGE if (device_is_ready(fuel_gauge_dev)) { union fuel_gauge_prop_val val; int ret = fuel_gauge_get_prop(fuel_gauge_dev, FUEL_GAUGE_RELATIVE_STATE_OF_CHARGE, &val); if (ret == 0) { return val.relative_state_of_charge; } LOG_WRN("Fuel gauge SoC read failed: %d", ret); } /* Fall through to the voltage-curve estimate if the gauge read fails. */ #endif return battery_curve_lookup(&battery_curve_default, getBattMilliVolts()); } bool ZephyrBoard::setAdcMultiplier(float multiplier) { #if DT_NODE_EXISTS(DT_PATH(zephyr_user)) && \ DT_NODE_HAS_PROP(DT_PATH(zephyr_user), io_channels) _adc_multiplier_override = multiplier; return true; #else (void)multiplier; return false; #endif } float ZephyrBoard::getAdcMultiplier() const { #if DT_NODE_EXISTS(DT_PATH(zephyr_user)) && \ DT_NODE_HAS_PROP(DT_PATH(zephyr_user), io_channels) return (_adc_multiplier_override != 0.0f) ? _adc_multiplier_override : (float)VBAT_MV_MULTIPLIER; #else return 0.0f; #endif } float ZephyrBoard::getMCUTemperature() { /* nRF52840 die temperature sensor - "nordic,nrf-temp" at 0x4000c000 * Nodelabel "temp" is defined in nrf52840.dtsi, status="okay" by default */ const struct device *dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(temp)); if (!dev || !device_is_ready(dev)) { return NAN; } struct sensor_value val; if (sensor_sample_fetch(dev) == 0 && sensor_channel_get(dev, SENSOR_CHAN_DIE_TEMP, &val) == 0) { return sensor_value_to_float(&val); } return NAN; } const char *ZephyrBoard::getManufacturerName() const { return CONFIG_ZEPHCORE_BOARD_NAME; } void ZephyrBoard::onBeforeTransmit() { #if HAS_TX_LED /* Honour the LED master gate ("set leds off"). On a headless repeater this * is the only LED that ever lights, so the gate has to be checked here and * not just in the UI layer. onAfterTransmit() still clears the pin * unconditionally, so a gate flipped mid-transmit can't strand it lit. */ if (!zephcore_leds_disabled()) { gpio_pin_set_dt(&tx_led, 1); } #endif } void ZephyrBoard::onAfterTransmit() { #if HAS_TX_LED gpio_pin_set_dt(&tx_led, 0); #endif } void ZephyrBoard::reboot() { k_msleep(50); /* Let UART/USB flush */ #ifdef ZEPHCORE_USBD_DETACH /* USB device stack (CDC ACM): detach so the host sees an unplug. */ zephcore_usbd_detach(); k_msleep(100); /* Hold SE0 long enough for host disconnect debounce */ #endif #ifdef ESP32_USB_SERIAL_DETACH /* Drop the D+ pull-up so the host sees a clean USB disconnect before the * soft reset (GH #43 — wedged serial port on macOS). */ usb_serial_jtag_ll_phy_enable_pad(false); k_msleep(100); /* Hold SE0 long enough for host disconnect debounce */ #endif sys_reboot(SYS_REBOOT_COLD); } void ZephyrBoard::rebootToBootloader() { #ifdef NRF52_GPREGRET /* Write magic value to GPREGRET0 - enter UF2 bootloader mode. * UF2 supports both drag-and-drop (.uf2) and serial DFU (nrfutil). */ nrf_power_gpregret_set(NRF_POWER, 0, BOOTLOADER_DFU_UF2_MAGIC); #endif #ifdef ESP32_FORCE_DOWNLOAD_BOOT /* Come back up in the ROM download mode instead of the app, handing the * USB port back to USB-Serial-JTAG so esptool can reach it. */ REG_SET_BIT(RTC_CNTL_OPTION1_REG, RTC_CNTL_FORCE_DOWNLOAD_BOOT); #endif k_msleep(50); /* Let UART/USB flush */ #ifdef ZEPHCORE_USBD_DETACH /* Detach the CDC ACM device — same reason as reboot(), and doubly so here: * the ESP32-S3 comes back in ROM download mode, where the port belongs to * USB-Serial-JTAG. The host must see the old device leave the bus first or * it will not enumerate the new one. */ zephcore_usbd_detach(); k_msleep(100); #endif #ifdef ESP32_USB_SERIAL_DETACH /* Clean USB disconnect before the soft reset — same reason as reboot(). */ usb_serial_jtag_ll_phy_enable_pad(false); k_msleep(100); #endif sys_reboot(SYS_REBOOT_COLD); } bool ZephyrBoard::startOTAUpdate(const char *id, char reply[]) { #ifdef NRF52_GPREGRET /* Write magic value to GPREGRET0 - enter BLE OTA DFU mode */ nrf_power_gpregret_set(NRF_POWER, 0, BOOTLOADER_DFU_OTA_MAGIC); sprintf(reply, "OK - rebooting to BLE DFU (name: %s)", id ? id : "DfuTarg"); k_msleep(50); /* Let UART/USB flush */ sys_reboot(SYS_REBOOT_COLD); return true; /* Never reached */ #else (void)id; strcpy(reply, "Error: BLE OTA not supported on this platform"); return false; #endif } bool ZephyrBoard::getBootloaderVersion(char *out, size_t max_len) { #if defined(CONFIG_SOC_SERIES_NRF52) /* Scan flash for UF2 bootloader version string. * info.txt lives somewhere in the 0xFB000-0xFE000 range depending * on SoftDevice version and bootloader build. */ static const char MARKER[] = "UF2 Bootloader "; const uint8_t *flash = (const uint8_t *)0x000FB000; for (uint32_t i = 0; i < 0x3000 - (sizeof(MARKER) - 1); i++) { if (memcmp(&flash[i], MARKER, sizeof(MARKER) - 1) == 0) { const char *ver = (const char *)&flash[i + sizeof(MARKER) - 1]; size_t len = 0; while (len < max_len - 1 && ver[len] != '\0' && ver[len] != ' ' && ver[len] != '\n' && ver[len] != '\r') { out[len] = ver[len]; len++; } out[len] = '\0'; return len > 0; } } #else (void)out; (void)max_len; #endif return false; } void ZephyrBoard::clearBootloaderMagic() { #ifdef NRF52_GPREGRET /* Clear any stale GPREGRET values from previous sessions. * GPREGRET0: bootloader DFU mode select (0x57=UF2, 0xA8=OTA) * GPREGRET1: wake gate / deep sleep flag */ nrf_power_gpregret_set(NRF_POWER, 0, 0x00); nrf_power_gpregret_set(NRF_POWER, 1, 0x00); #endif } uint8_t ZephyrBoard::getStartupReason() const { return BD_STARTUP_NORMAL; } bool ZephyrBoard::isExternalPowered() { #if defined(CONFIG_SOC_SERIES_NRF52) /* VBUS detect from the POWER peripheral — true when USB/charger is * attached. Read-only status register; safe alongside the BLE controller * (we already poke NRF_POWER->GPREGRET directly elsewhere). */ return nrf_power_usbregstatus_vbusdet_get(NRF_POWER); #else /* Other platforms have no portable VBUS query — report battery (false) * so low-battery auto-shutdown is never inhibited. */ return false; #endif } } /* namespace mesh */