Add special recovery mode for wiped calibration (no more reboot loop)

This commit is contained in:
Armel FAUVEAU committed 2026-08-04 22:21:50 +02:00
1 parent fe9c4e9432
commit 6e75d13cbe
6 files changed
+188

No files matched your search

+3
View File
@@ -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)
+129
View File
@@ -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 <stdint.h>
#include <stdbool.h>
#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);
}
}
+37
View File
@@ -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 <stdbool.h>
// 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
+12
View File
@@ -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);
+6
View File
@@ -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
+1
View File
@@ -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,