echoplayer: implement bootloader power on/off logic

On the Echo R1, the main regulator is enabled primarily by
the power button and USB input, and secondarily by the CPU's
own output pins (cpu_power_on signal or RTC alarm output).

From a user perspective, the player should appear to power
up and down only if the power button is long pressed, which
must be implemented in software. These logical power states
are called "active" and "inactive" in the bootloader.

Going from inactive to active will attempt to boot Rockbox
unless a button (d-pad down) is held to enter bootloader
USB mode instead. Going from active to inactive will shut
down the player. The bootloader will also automatically
shut down after a short timeout, if USB is not plugged in.

In the inactive state, the player is supposed to enumerate
over USB so it can negotiate a charging current, but should
otherwise appear "off". In particular it shouldn't expose
mass storage or even power up the SD card, nor power up the
LCD/backight. This isn't implemented yet, because there's
no way to dynamically change USB configurations (eg. going
from active to inactive should trigger re-enumeration to
switch to charge only mode). To avoid surprising behavior,
the bootloader will just boot Rockbox immediately if USB is
plugged in at boot.

Change-Id: Icd1d48ef49a31eb32b54d440e9211aaf40c6b974
This commit is contained in:
Aidan MacDonald 2026-01-13 14:12:41 +00:00 committed by Solomon Peachy
parent 58ace97a4e
commit 0f5c42122c
2 changed files with 254 additions and 96 deletions

View file

@ -96,5 +96,4 @@ x1000/recovery.c
x1000/utils.c x1000/utils.c
#elif defined(ECHO_R1) #elif defined(ECHO_R1)
echoplayer.c echoplayer.c
show_logo.c
#endif #endif

View file

@ -21,154 +21,313 @@
#include "kernel/kernel-internal.h" #include "kernel/kernel-internal.h"
#include "system.h" #include "system.h"
#include "power.h" #include "power.h"
#include "rtc.h"
#include "lcd.h" #include "lcd.h"
#include "backlight.h" #include "backlight.h"
#include "button.h" #include "button.h"
#include "timefuncs.h"
#include "storage.h" #include "storage.h"
#include "disk.h" #include "disk.h"
#include "file_internal.h" #include "file_internal.h"
#include "usb.h" #include "usb.h"
#include "common.h" /* For show_logo() */ #include "rbversion.h"
#include "system-echoplayer.h"
#include "gpio-stm32h7.h"
static bool is_usb_connected = false; /* Events for the monitor callback to signal the main thread */
#define EV_POWER_PRESSED MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0)
#define EV_POWER_RELEASED MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 1)
#define EV_USB_UNPLUGGED MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 2)
static void demo_rtc(void) /* Long press duration for power button */
#define POWERBUTTON_LONG_PRESS_TIME (HZ)
/* Time to remain powered with no USB cable inserted */
#define USB_UNPLUGGED_ACTIVE_TIME (30 * HZ)
#define USB_UNPLUGGED_INACTIVE_TIME (3 * HZ)
/* Power button monitor state */
static bool pwr_curr_state;
static bool pwr_prev_state;
struct timeout pwr_stable_tmo;
/* USB monitor state */
static bool usb_curr_state;
static bool usb_prev_state;
struct timeout usb_unplugged_tmo;
static volatile bool restart_pwr_stable_tmo;
static volatile bool restart_usb_unplugged_tmo;
/*
* Because power is always enabled while USB is plugged in the
* bootloader decides whether to appear "active" or "inactive"
* to the user.
*/
static bool is_active;
/*
* This flag is set if the bootloader is entered after software
* poweroff. The user may still be holding the power button and
* we don't want to boot Rockbox because of this, so we have to
* wait for the power button to be released first.
*/
static bool wait_for_power_released;
/* Optional error message displayed on LCD */
static const char *status_msg = NULL;
/* Helper functions */
static bool is_power_button_pressed(void)
{ {
int y = 0; return button_status() & BUTTON_POWER;
struct tm *time = get_time();
lcd_clear_display();
lcd_putsf(0, y++, "time: %02d:%02d:%02d",
time->tm_hour, time->tm_min, time->tm_sec);
lcd_putsf(0, y++, "year: %d", time->tm_year + 1900);
lcd_putsf(0, y++, "month: %d", time->tm_mon);
lcd_putsf(0, y++, "day: %d", time->tm_mday);
lcd_update();
} }
static void demo_storage(void) static bool is_usbmode_button_pressed(void)
{ {
int y = 0; return button_status() & BUTTON_DOWN;
}
lcd_clear_display(); static int send_event_on_tmo(struct timeout *tmo)
lcd_putsf(0, y++, "tick %ld", current_tick); {
button_queue_post(tmo->data, 0);
return 0;
}
if (is_usb_connected) /*
* Monitors the state of the power button and USB cable
* insertion status. It will post an event to the main
* thread when (a) the power button is continously held
* or released for long enough, or (b) the USB cable is
* unplugged for long enough.
*/
static void monitor_tick(void)
{
/* Power button state */
pwr_prev_state = pwr_curr_state;
pwr_curr_state = is_power_button_pressed();
if (pwr_curr_state != pwr_prev_state || restart_pwr_stable_tmo)
{ {
lcd_puts(0, y++, "storage disabled by USB"); long event = pwr_curr_state ? EV_POWER_PRESSED : EV_POWER_RELEASED;
lcd_update(); int ticks = POWERBUTTON_LONG_PRESS_TIME;
restart_pwr_stable_tmo = false;
timeout_register(&pwr_stable_tmo, send_event_on_tmo, ticks, event);
}
/* USB cable state */
usb_prev_state = usb_curr_state;
usb_curr_state = usb_inserted();
/* Ignore cable state change in inactive state */
if (usb_curr_state != usb_prev_state || restart_usb_unplugged_tmo)
{
long event = EV_USB_UNPLUGGED;
int ticks = is_active ? USB_UNPLUGGED_ACTIVE_TIME : USB_UNPLUGGED_INACTIVE_TIME;
restart_usb_unplugged_tmo = false;
if (usb_curr_state)
timeout_cancel(&usb_unplugged_tmo);
else
timeout_register(&usb_unplugged_tmo, send_event_on_tmo, ticks, event);
}
}
static void monitor_init(void)
{
pwr_curr_state = is_power_button_pressed();
usb_curr_state = usb_inserted();
/* Make sure events fire even if inputs don't change after boot */
restart_pwr_stable_tmo = true;
restart_usb_unplugged_tmo = true;
tick_add_task(monitor_tick);
}
static void go_active(void)
{
is_active = true;
restart_usb_unplugged_tmo = true;
gpio_set_level(GPIO_CPU_POWER_ON, 1);
storage_enable(true);
}
static void go_inactive(void)
{
is_active = false;
restart_usb_unplugged_tmo = true;
storage_enable(false);
gpio_set_level(GPIO_CPU_POWER_ON, 0);
}
static void refresh_display(void)
{
if (!is_active)
{
lcd_shutdown();
return; return;
} }
struct partinfo pinfo;
if (storage_present(IF_MD(0,)) && disk_partinfo(0, &pinfo))
{
lcd_putsf(0, y++, "start %d", (int)pinfo.start);
lcd_putsf(0, y++, "count %d", (int)pinfo.size);
lcd_putsf(0, y++, "type %d", (int)pinfo.type);
DIR *d = opendir("/");
struct dirent *ent;
while ((ent = readdir(d)))
{
lcd_putsf(0, y++, "/%s", ent->d_name);
}
closedir(d);
}
lcd_update();
}
static void demo_usb(void)
{
static const char *phyname[] = {
[STM32H743_USBOTG_PHY_ULPI_HS] = "ULPI HS",
[STM32H743_USBOTG_PHY_ULPI_FS] = "ULPI FS",
[STM32H743_USBOTG_PHY_INT_FS] = "internal FS",
};
int y = 0; int y = 0;
lcd_clear_display(); lcd_clear_display();
lcd_putsf(0, y++, "tick %ld", current_tick);
lcd_putsf(0, y++, "usb connected %d", (int)is_usb_connected);
lcd_putsf(0, y++, "instance = USB%d", STM32H743_USBOTG_INSTANCE + 1); lcd_putsf(0, y++, "Rockbox on %s", MODEL_NAME);
lcd_putsf(0, y++, "phy = %s", phyname[STM32H743_USBOTG_PHY]); y++;
if (status_msg)
{
lcd_putsf(0, y++, "Error: %s", status_msg);
y++;
}
if (!usb_inserted())
{
lcd_putsf(0, y++, "Hold POWER to power off");
lcd_putsf(0, y++, "Connect USB cable for USB mode");
y++;
}
else
{
lcd_putsf(0, y++, "Bootloader USB mode");
if (charging_state())
{
lcd_putsf(0, y++, "Battery charging (%d mA)",
usb_charging_maxcurrent());
}
else
{
lcd_putsf(0, y++, "Battery charged");
}
y++;
}
lcd_putsf(0, y++, "Version: %s", RBVERSION);
lcd_update(); lcd_update();
lcd_enable(true);
} }
static void (*demo_funcs[]) (void) = { static void launch(void)
demo_rtc, {
demo_storage, /* No-op if USB mode was requested */
demo_usb, if (is_usbmode_button_pressed())
}; return;
/* TODO: load rockbox */
status_msg = "Can't boot RB yet!";
}
void main(void) void main(void)
{ {
system_init(); system_init();
kernel_init(); kernel_init();
power_init(); power_init();
rtc_init();
lcd_init();
button_init(); button_init();
/* Start monitoring power button / usb state */
monitor_init();
/* Prepare LCD in case we need to display something */
lcd_init();
backlight_init(); backlight_init();
backlight_on();
show_logo();
/*
* Prepare storage subsystem, but keep SD card unpowered
* until we actually need to access it.
*/
storage_init(); storage_init();
storage_enable(false);
/*
* Initialize fs/disk internal state, the disk will not
* be mountable due to being disabled but this will not
* cause any fatal errors.
*/
filesystem_init(); filesystem_init();
disk_mount_all(); disk_mount_all();
if (echoplayer_boot_reason == ECHOPLAYER_BOOT_REASON_SW_REBOOT ||
usb_detect() == USB_INSERTED)
{
/*
* For software reboot we want to immediately launch Rockbox.
*
* We also do so if USB is plugged in at boot, because there's
* no way to dynamically switch between charge only and mass
* storage mode depending on the active/inactive state; to avoid
* confusion, it is simpler to just boot Rockbox.
*/
go_active();
launch();
}
else if (echoplayer_boot_reason == ECHOPLAYER_BOOT_REASON_SW_POWEROFF)
{
/* Ignore power button pressed event until button is first released */
wait_for_power_released = true;
}
/*
* Initialize USB so we can enumerate with the host and
* negotiate charge current (needed even in inactive mode)
*/
usb_init(); usb_init();
usb_start_monitoring(); usb_start_monitoring();
usb_charging_enable(USB_CHARGING_FORCE);
int demo_page = 0; for (;;)
const int num_pages = ARRAYLEN(demo_funcs);
while (1)
{ {
int btn = button_get_w_tmo(HZ); refresh_display();
switch (btn)
long refresh_tmo = is_active ? HZ : TIMEOUT_BLOCK;
switch (button_get_w_tmo(refresh_tmo))
{ {
case BUTTON_START: case EV_POWER_PRESSED:
demo_page += 1; if (wait_for_power_released)
if (demo_page >= num_pages) break;
demo_page = 0;
if (!is_active)
{
/* Launch Rockbox due to user pressing power button */
go_active();
launch();
}
else
{
/*
* NOTE: The check for USB insertion here is only
* because we can't change to charging only mode.
*/
if (!usb_inserted())
go_inactive();
}
break; break;
case BUTTON_SELECT: case EV_POWER_RELEASED:
if (demo_page == 0) /*
demo_page = num_pages - 1; * This cuts power if the power button is not
else * held and we're not in the active state due
demo_page -= 1; * to USB mode, etc.
*/
wait_for_power_released = false;
gpio_set_level(GPIO_CPU_POWER_ON, is_active);
break;
case EV_USB_UNPLUGGED:
go_inactive();
break; break;
case SYS_USB_CONNECTED: case SYS_USB_CONNECTED:
go_active();
usb_acknowledge(SYS_USB_CONNECTED_ACK); usb_acknowledge(SYS_USB_CONNECTED_ACK);
is_usb_connected = true;
break;
case SYS_USB_DISCONNECTED:
is_usb_connected = false;
case BUTTON_X:
power_off();
break;
default:
break; break;
} }
demo_funcs[demo_page]();
} }
} }