mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-08-29 14:58:16 +00:00
437 lines
14 KiB
C++
437 lines
14 KiB
C++
/*
|
|
* SPDX-License-Identifier: MIT
|
|
*/
|
|
|
|
#include "ZephyrBoard.h"
|
|
#include "battery_curve.h"
|
|
#include "led_gate.h"
|
|
#include <zephyr/kernel.h>
|
|
#include <zephyr/sys/reboot.h>
|
|
#include <zephyr/drivers/sensor.h>
|
|
#include <zephyr/drivers/gpio.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#if defined(CONFIG_SOC_SERIES_NRF52)
|
|
#include <hal/nrf_power.h>
|
|
/* 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 <hal/usb_serial_jtag_ll.h>
|
|
#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 <soc/rtc_cntl_reg.h>
|
|
#include <soc/soc.h>
|
|
#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 <ZephyrUSBCDC.h>
|
|
#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 <zephyr/logging/log.h>
|
|
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 <zephyr/drivers/adc.h>
|
|
#include <zephyr/drivers/regulator.h>
|
|
|
|
/* 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 <zephyr/drivers/fuel_gauge.h>
|
|
#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 */
|