From 6e75d13cbefa96938e701a3fae22b3b9fb0411fe Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Tue, 4 Aug 2026 22:21:50 +0200 Subject: [PATCH] Add special recovery mode for wiped calibration (no more reboot loop) --- App/CMakeLists.txt | 3 + App/helper/recovery.c | 129 ++++++++++++++++++++++++++++++++++++++++++ App/helper/recovery.h | 37 ++++++++++++ App/main.c | 12 ++++ App/settings.c | 6 ++ CMakePresets.json | 1 + 6 files changed, 188 insertions(+) create mode 100644 App/helper/recovery.c create mode 100644 App/helper/recovery.h diff --git a/App/CMakeLists.txt b/App/CMakeLists.txt index 9919cec1..3e02f209 100644 --- a/App/CMakeLists.txt +++ b/App/CMakeLists.txt @@ -197,6 +197,9 @@ enable_feature(ENABLE_FEAT_F4HWN_SCAN_FASTER) enable_feature(ENABLE_FEAT_F4HWN_SCAN_RSSI) enable_feature(ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE) enable_feature(ENABLE_FEAT_F4HWN_RESCUE_OPS) +enable_feature(ENABLE_FEAT_F4HWN_RECOVER + helper/recovery.c +) enable_feature(ENABLE_FEAT_F4HWN_VOL) enable_feature(ENABLE_FEAT_F4HWN_AUDIO) enable_feature(ENABLE_FEAT_F4HWN_RESET_VFO) diff --git a/App/helper/recovery.c b/App/helper/recovery.c new file mode 100644 index 00000000..c66288a7 --- /dev/null +++ b/App/helper/recovery.c @@ -0,0 +1,129 @@ +/* Copyright 2025 Armel FAUVEAU (F4HWN) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "py32f0xx.h" // NVIC_SystemReset + +#include "helper/recovery.h" +#include "driver/py25q16.h" +#include "driver/st7565.h" +#include "driver/system.h" // SYSTEM_DelayMs +#include "driver/backlight.h" +#include "app/uart.h" +#include "ui/helper.h" // UI_DisplayClear, UI_PrintString* + +// External-flash calibration zone (see the mapping in App/driver/eeprom_compat.c: +// EEPROM logical 0xB000..0xB200 <-> PY25Q16 physical 0x010000..0x010200). +#define CALIB_ADDR 0x010000 +#define CALIB_SIZE 512 + +bool RECOVERY_CalibrationIsWiped(void) +{ + uint8_t buf[64]; + bool all_ff = true; + bool all_00 = true; + + for (uint32_t off = 0; off < CALIB_SIZE; off += sizeof(buf)) + { + PY25Q16_ReadBuffer(CALIB_ADDR + off, buf, sizeof(buf)); + + for (uint32_t i = 0; i < sizeof(buf); i++) + { + if (buf[i] != 0xFF) all_ff = false; + if (buf[i] != 0x00) all_00 = false; + } + + // As soon as a byte breaks both patterns, a real calibration is + // present: nothing to recover. + if (!all_ff && !all_00) + return false; + } + + return all_ff || all_00; +} + +static void RECOVERY_DrawWaitScreen(void) +{ + UI_StatusClear(); + UI_DisplayClear(); + + UI_PrintString("RESTORE", 0, 127, 1, 10); + UI_PrintStringSmallNormal("CALIBRATION", 0, 127, 3); + UI_PrintStringSmallNormal("Connect UV Studio", 0, 127, 5); + UI_PrintStringSmallNormal("and restore calib", 0, 127, 6); + + ST7565_BlitStatusLine(); // blank status line + ST7565_BlitFullScreen(); +} + +void RECOVERY_Loop(void) +{ + BACKLIGHT_TurnOn(); + RECOVERY_DrawWaitScreen(); + + // Both counters are expressed in loop ticks (~10 ms each). + uint16_t idle = 0; // time since the last serviced command + uint16_t recheck = 0; // time since the last calibration re-read + + for (;;) + { + bool activity = false; + +#ifdef ENABLE_UART + if (UART_IsCommandAvailable(UART_PORT_UART)) + { + UART_HandleCommand(UART_PORT_UART); + activity = true; + } +#endif +#ifdef ENABLE_USB + if (UART_IsCommandAvailable(UART_PORT_VCP)) + { + UART_HandleCommand(UART_PORT_VCP); + activity = true; + } +#endif + + if (activity) + idle = 0; + else if (idle < 60000) + idle++; + + // Re-read the calibration about once per second, but only reboot once + // it is valid AND the link has been quiet for ~3 s. This guarantees we + // never reset in the middle of an UV Studio transfer (which briefly + // makes the zone look non-wiped after the first sector is written). + if (++recheck >= 100) + { + recheck = 0; + + if (idle >= 300 && !RECOVERY_CalibrationIsWiped()) + { + UI_DisplayClear(); + UI_PrintString("CALIB OK", 0, 127, 2, 10); + UI_PrintString("REBOOT", 0, 127, 4, 10); + ST7565_BlitStatusLine(); + ST7565_BlitFullScreen(); + + SYSTEM_DelayMs(1500); + NVIC_SystemReset(); + } + } + + SYSTEM_DelayMs(10); + } +} diff --git a/App/helper/recovery.h b/App/helper/recovery.h new file mode 100644 index 00000000..bedb86b0 --- /dev/null +++ b/App/helper/recovery.h @@ -0,0 +1,37 @@ +/* Copyright 2025 Armel FAUVEAU (F4HWN) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef HELPER_RECOVERY_H +#define HELPER_RECOVERY_H + +#include + +// True when the external-flash calibration zone (0x010000, 512 bytes) is +// entirely erased (all 0xFF) or entirely zeroed (all 0x00) -- i.e. a radio +// whose calibration has been wiped by a faulty firmware. The check is +// deliberately strict: a real calibration always contains mixed bytes, so it +// can never be mistaken for a wiped one. +bool RECOVERY_CalibrationIsWiped(void); + +// Recovery waiting screen. Displays "RESTORE CALIBRATION" and keeps the +// UART/USB link alive so an external tool (UV Studio) can rewrite the +// calibration zone. Never returns: once a valid calibration has been written +// and the link has gone quiet, the radio is reset to boot normally. +// +// This path deliberately bypasses the battery / reduced-service logic that +// would otherwise trap a wiped radio in a reboot loop. +void RECOVERY_Loop(void) __attribute__((noreturn)); + +#endif diff --git a/App/main.c b/App/main.c index a80d20af..9510ab44 100644 --- a/App/main.c +++ b/App/main.c @@ -60,6 +60,9 @@ #endif #include "helper/battery.h" #include "helper/boot.h" +#ifdef ENABLE_FEAT_F4HWN_RECOVER + #include "helper/recovery.h" +#endif #include "ui/lock.h" #include "ui/welcome.h" @@ -114,6 +117,15 @@ void Main(void) SETTINGS_WriteBuildOptions(); SETTINGS_LoadCalibration(); +#ifdef ENABLE_FEAT_F4HWN_RECOVER + // A radio whose calibration zone has been wiped by a faulty firmware would + // otherwise read a bogus battery voltage, be forced into reduced service + // and reset in a loop. Instead, park it on a safe recovery screen and wait + // for the calibration to be restored over USB (UV Studio). + if (RECOVERY_CalibrationIsWiped()) + RECOVERY_Loop(); // never returns until a valid calibration is restored +#endif + RADIO_ConfigureChannel(0, VFO_CONFIGURE_RELOAD); RADIO_ConfigureChannel(1, VFO_CONFIGURE_RELOAD); diff --git a/App/settings.c b/App/settings.c index 6cd43647..871e3226 100644 --- a/App/settings.c +++ b/App/settings.c @@ -534,6 +534,12 @@ void SETTINGS_LoadCalibration(void) gBatteryCalibration[0] = 1900; gBatteryCalibration[1] = 2000; } + // A wiped calibration zone (0x0000 / 0xFFFF) leaves gBatteryCalibration[3] + // invalid. As it is the divisor of the battery-voltage computation, that + // collapses the reading to "critical" and can trap the radio in reduced + // service -> reset (reboot loop). Fall back to a nominal value (RAM only). + if (gBatteryCalibration[3] < 1000 || gBatteryCalibration[3] > 3000) + gBatteryCalibration[3] = 2000; gBatteryCalibration[5] = 2300; #ifdef ENABLE_VOX diff --git a/CMakePresets.json b/CMakePresets.json index ec73c156..0af52264 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -200,6 +200,7 @@ "ENABLE_FEAT_F4HWN_PMR": true, "ENABLE_FEAT_F4HWN_GMRS_FRS_MURS": true, "ENABLE_FEAT_F4HWN_RESCUE_OPS": true, + "ENABLE_FEAT_F4HWN_RECOVER": true, "ENABLE_FEAT_F4HWN_VOL": true, "ENABLE_FEAT_F4HWN_AUDIO": true, "ENABLE_FEAT_F4HWN_AUDIO_SCOPE": true,