coreboot-libre-fam15h-rdimm/3rdparty/chromeec/chip/ish/power_mgt.c

740 lines
18 KiB
C
Raw Permalink Normal View History

2024-03-04 11:14:53 +01:00
/* Copyright 2019 The Chromium OS Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "aontaskfw/ish_aon_share.h"
#include "console.h"
#include "hwtimer.h"
#include "interrupts.h"
#include "ish_dma.h"
#include "ish_persistent_data.h"
#include "power_mgt.h"
#include "system.h"
#include "task.h"
#include "util.h"
#include "watchdog.h"
#define CPUTS(outstr) cputs(CC_SYSTEM, outstr)
#define CPRINTS(format, args...) cprints(CC_SYSTEM, format, ##args)
#define CPRINTF(format, args...) cprintf(CC_SYSTEM, format, ##args)
/* defined in link script: core/minute-ia/ec.lds.S */
extern uint32_t __aon_ro_start;
extern uint32_t __aon_ro_end;
extern uint32_t __aon_rw_start;
extern uint32_t __aon_rw_end;
/**
* on ISH, uart interrupt can only wakeup ISH from low power state via
* CTS pin, but most ISH platforms only have Rx and Tx pins, no CTS pin
* exposed, so, we need block ISH enter low power state for a while when
* console is in use.
* fixed amount of time to keep the console in use flag true after boot in
* order to give a permanent window in which the low speed clock is not used.
*/
#define CONSOLE_IN_USE_ON_BOOT_TIME (15*SECOND)
/* power management internal context data structure */
struct pm_context {
/* aontask image valid flag */
int aon_valid;
/* point to the aon shared data in aontask */
struct ish_aon_share *aon_share;
/* TSS segment selector for task switching */
int aon_tss_selector[2];
/* console expire time */
timestamp_t console_expire_time;
/* console in use timeout */
int console_in_use_timeout_sec;
} __packed;
static struct pm_context pm_ctx = {
.aon_valid = 0,
/* aon shared data located in the start of aon memory */
.aon_share = (struct ish_aon_share *)CONFIG_AON_RAM_BASE,
.console_in_use_timeout_sec = 60
};
/* D0ix statistics data, including each state's count and total stay time */
struct pm_stat {
uint64_t count;
uint64_t total_time_us;
};
struct pm_statistics {
struct pm_stat d0i0;
struct pm_stat d0i1;
struct pm_stat d0i2;
struct pm_stat d0i3;
};
static struct pm_statistics pm_stats;
/*
* Log a new statistic
*
* t0: start time, in us
* t1: end time, in us
*/
static void log_pm_stat(struct pm_stat *stat, uint32_t t0, uint32_t t1)
{
stat->total_time_us += t1 - t0;
stat->count++;
}
#ifdef CONFIG_ISH_PM_AONTASK
/* The GDT which initialized in init.S */
extern struct gdt_entry __gdt[];
extern struct gdt_header __gdt_ptr[];
/* TSS desccriptor for saving main FW's cpu context during aontask switching */
static struct tss_entry main_tss;
/**
* add new entry in GDT
* if defined 'CONFIG_ISH_PM_AONTASK', the GDT which defined in init.S will
* have 3 more empty placeholder entries, this function is help to update
* these entries which needed by x86's HW task switching method
*
* @param desc_lo lower DWORD of the entry descriptor
* @param desc_up upper DWORD of the entry descriptor
*
* @return the descriptor selector index of the added entry
*/
static uint32_t add_gdt_entry(uint32_t desc_lo, uint32_t desc_up)
{
int index;
/**
* get the first empty entry of GDT which defined in init.S
* each entry has a fixed size of 8 bytes
*/
index = __gdt_ptr[0].limit >> 3;
/* add the new entry descriptor to the GDT */
__gdt[index].dword_lo = desc_lo;
__gdt[index].dword_up = desc_up;
/* update GDT's limit size */
__gdt_ptr[0].limit += sizeof(struct gdt_entry);
return __gdt_ptr[0].limit - sizeof(struct gdt_entry);
}
static void init_aon_task(void)
{
uint32_t desc_lo, desc_up;
struct ish_aon_share *aon_share = pm_ctx.aon_share;
struct tss_entry *aon_tss = aon_share->aon_tss;
if (aon_share->magic_id != AON_MAGIC_ID) {
pm_ctx.aon_valid = 0;
return;
}
pm_ctx.aon_valid = 1;
pm_ctx.aon_tss_selector[0] = 0;
/* fill in the 3 placeholder GDT entries */
/* TSS's limit specified as 0x67, to allow the task has permission to
* access I/O port using IN/OUT instructions,'iomap_base_addr' field
* must be greater than or equal to TSS' limit
* see 'I/O port permissions' on
* https://en.wikipedia.org/wiki/Task_state_segment
*/
main_tss.iomap_base_addr = GDT_DESC_TSS_LIMIT;
/* set GDT entry 3 for TSS descriptor of main FW
* limit: 0x67
* Present = 1, DPL = 0
*/
desc_lo = GEN_GDT_DESC_LO((uint32_t)&main_tss,
GDT_DESC_TSS_LIMIT, GDT_DESC_TSS_FLAGS);
desc_up = GEN_GDT_DESC_UP((uint32_t)&main_tss,
GDT_DESC_TSS_LIMIT, GDT_DESC_TSS_FLAGS);
add_gdt_entry(desc_lo, desc_up);
/* set GDT entry 4 for TSS descriptor of aontask
* limit: 0x67
* Present = 1, DPL = 0, Accessed = 1
*/
desc_lo = GEN_GDT_DESC_LO((uint32_t)aon_tss,
GDT_DESC_TSS_LIMIT, GDT_DESC_TSS_FLAGS);
desc_up = GEN_GDT_DESC_UP((uint32_t)aon_tss,
GDT_DESC_TSS_LIMIT, GDT_DESC_TSS_FLAGS);
pm_ctx.aon_tss_selector[1] = add_gdt_entry(desc_lo, desc_up);
/* set GDT entry 5 for LDT descriptor of aontask
* Present = 1, DPL = 0, Readable = 1
*/
desc_lo = GEN_GDT_DESC_LO((uint32_t)aon_share->aon_ldt,
aon_share->aon_ldt_size, GDT_DESC_LDT_FLAGS);
desc_up = GEN_GDT_DESC_UP((uint32_t)aon_share->aon_ldt,
aon_share->aon_ldt_size, GDT_DESC_LDT_FLAGS);
aon_tss->ldt_seg_selector = add_gdt_entry(desc_lo, desc_up);
/* update GDT register and set current TSS as main_tss (GDT entry 3) */
__asm__ volatile("lgdt __gdt_ptr;\n"
"push %eax;\n"
"movw $0x18, %ax;\n"
"ltr %ax;\n"
"pop %eax;");
aon_share->main_fw_ro_addr = (uint32_t)&__aon_ro_start;
aon_share->main_fw_ro_size = (uint32_t)&__aon_ro_end -
(uint32_t)&__aon_ro_start;
aon_share->main_fw_rw_addr = (uint32_t)&__aon_rw_start;
aon_share->main_fw_rw_size = (uint32_t)&__aon_rw_end -
(uint32_t)&__aon_rw_start;
ish_dma_init();
}
static inline void check_aon_task_status(void)
{
struct ish_aon_share *aon_share = pm_ctx.aon_share;
if (aon_share->last_error != AON_SUCCESS) {
CPRINTF("aontask has errors:\n");
CPRINTF(" last error: %d\n", aon_share->last_error);
CPRINTF(" error counts: %d\n", aon_share->error_count);
}
}
static void switch_to_aontask(void)
{
interrupt_disable();
__sync_synchronize();
/* disable cache and flush cache */
__asm__ volatile("movl %%cr0, %%eax;\n"
"orl $0x60000000, %%eax;\n"
"movl %%eax, %%cr0;\n"
"wbinvd;"
:
:
: "eax");
/* switch to aontask through a far call with aontask's TSS selector */
__asm__ volatile("lcall *%0;" ::"m"(*pm_ctx.aon_tss_selector) :);
/* clear TS (Task Switched) flag and enable cache */
__asm__ volatile("clts;\n"
"movl %%cr0, %%eax;\n"
"andl $0x9FFFFFFF, %%eax;\n"
"movl %%eax, %%cr0;"
:
:
: "eax");
interrupt_enable();
}
__attribute__ ((noreturn))
static void handle_reset_in_aontask(enum ish_pm_state pm_state)
{
pm_ctx.aon_share->pm_state = pm_state;
/* only enable PMU wakeup interrupt */
disable_all_interrupts();
task_enable_irq(ISH_PMU_WAKEUP_IRQ);
if (IS_ENABLED(CONFIG_ISH_PM_RESET_PREP))
task_enable_irq(ISH_RESET_PREP_IRQ);
/* enable Trunk Clock Gating (TCG) of ISH */
CCU_TCG_EN = 1;
/* enable power gating of RF(Cache) and ROMs */
PMU_RF_ROM_PWR_CTRL = 1;
switch_to_aontask();
__builtin_unreachable();
}
#endif
static void enter_d0i0(void)
{
uint32_t t0, t1;
t0 = __hw_clock_source_read();
pm_ctx.aon_share->pm_state = ISH_PM_STATE_D0I0;
/* halt ISH cpu, will wakeup from any interrupt */
ish_mia_halt();
t1 = __hw_clock_source_read();
pm_ctx.aon_share->pm_state = ISH_PM_STATE_D0;
log_pm_stat(&pm_stats.d0i0, t0, t1);
}
/**
* ISH PMU does not support both-edge interrupt triggered gpio configuration.
* If both edges are configured, then the ISH can't stay in low poer mode
* because it will exit immediately.
*
* As a workaround, we scan all gpio pins which have been configured as
* both-edge triggered, and then temporarily set each gpio pin to the single
* edge trigger that is opposite of its value, then restore the both-edge
* trigger configuration immediately after exiting low power mode.
*/
static uint32_t convert_both_edge_gpio_to_single_edge(void)
{
uint32_t both_edge_pins = 0;
int i = 0;
/**
* scan GPIO GFER, GRER and GIMR registers to find the both edge
* interrupt trigger mode enabled pins.
*/
for (i = 0; i < 32; i++) {
if (ISH_GPIO_GIMR & BIT(i) &&
ISH_GPIO_GRER & BIT(i) &&
ISH_GPIO_GFER & BIT(i)) {
/* Record the pin so we can restore it later */
both_edge_pins |= BIT(i);
if (ISH_GPIO_GPLR & BIT(i)) {
/* pin is high, just keep falling edge mode */
ISH_GPIO_GRER &= ~BIT(i);
} else {
/* pin is low, just keep rising edge mode */
ISH_GPIO_GFER &= ~BIT(i);
}
}
}
return both_edge_pins;
}
static void restore_both_edge_gpio_config(uint32_t both_edge_pin_map)
{
ISH_GPIO_GRER |= both_edge_pin_map;
ISH_GPIO_GFER |= both_edge_pin_map;
}
static void enter_d0i1(void)
{
uint64_t current_irq_map;
uint32_t both_edge_gpio_pins;
uint32_t t0, t1;
/* only enable PMU wakeup interrupt */
current_irq_map = disable_all_interrupts();
task_enable_irq(ISH_PMU_WAKEUP_IRQ);
if (IS_ENABLED(CONFIG_ISH_PM_RESET_PREP))
task_enable_irq(ISH_RESET_PREP_IRQ);
t0 = __hw_clock_source_read();
pm_ctx.aon_share->pm_state = ISH_PM_STATE_D0I1;
both_edge_gpio_pins = convert_both_edge_gpio_to_single_edge();
/* enable Trunk Clock Gating (TCG) of ISH */
CCU_TCG_EN = 1;
/* halt ISH cpu, will wakeup from PMU wakeup interrupt */
ish_mia_halt();
/* disable Trunk Clock Gating (TCG) of ISH */
CCU_TCG_EN = 0;
restore_both_edge_gpio_config(both_edge_gpio_pins);
pm_ctx.aon_share->pm_state = ISH_PM_STATE_D0;
t1 = __hw_clock_source_read();
log_pm_stat(&pm_stats.d0i1, t0, t1);
/* Reload watchdog before enabling interrupts again */
watchdog_reload();
/* restore interrupts */
task_disable_irq(ISH_PMU_WAKEUP_IRQ);
restore_interrupts(current_irq_map);
}
static void enter_d0i2(void)
{
uint64_t current_irq_map;
uint32_t both_edge_gpio_pins;
uint32_t t0, t1;
/* only enable PMU wakeup interrupt */
current_irq_map = disable_all_interrupts();
task_enable_irq(ISH_PMU_WAKEUP_IRQ);
if (IS_ENABLED(CONFIG_ISH_PM_RESET_PREP))
task_enable_irq(ISH_RESET_PREP_IRQ);
t0 = __hw_clock_source_read();
pm_ctx.aon_share->pm_state = ISH_PM_STATE_D0I2;
both_edge_gpio_pins = convert_both_edge_gpio_to_single_edge();
/* enable Trunk Clock Gating (TCG) of ISH */
CCU_TCG_EN = 1;
/* enable power gating of RF(Cache) and ROMs */
PMU_RF_ROM_PWR_CTRL = 1;
switch_to_aontask();
/* returned from aontask */
/* disable power gating of RF(Cache) and ROMs */
PMU_RF_ROM_PWR_CTRL = 0;
/* disable Trunk Clock Gating (TCG) of ISH */
CCU_TCG_EN = 0;
restore_both_edge_gpio_config(both_edge_gpio_pins);
t1 = __hw_clock_source_read();
pm_ctx.aon_share->pm_state = ISH_PM_STATE_D0;
log_pm_stat(&pm_stats.d0i2, t0, t1);
/* Reload watchdog before enabling interrupts again */
watchdog_reload();
/* restore interrupts */
task_disable_irq(ISH_PMU_WAKEUP_IRQ);
restore_interrupts(current_irq_map);
}
static void enter_d0i3(void)
{
uint64_t current_irq_map;
uint32_t both_edge_gpio_pins;
uint32_t t0, t1;
/* only enable PMU wakeup interrupt */
current_irq_map = disable_all_interrupts();
task_enable_irq(ISH_PMU_WAKEUP_IRQ);
if (IS_ENABLED(CONFIG_ISH_PM_RESET_PREP))
task_enable_irq(ISH_RESET_PREP_IRQ);
t0 = __hw_clock_source_read();
pm_ctx.aon_share->pm_state = ISH_PM_STATE_D0I3;
both_edge_gpio_pins = convert_both_edge_gpio_to_single_edge();
/* enable Trunk Clock Gating (TCG) of ISH */
CCU_TCG_EN = 1;
/* enable power gating of RF(Cache) and ROMs */
PMU_RF_ROM_PWR_CTRL = 1;
switch_to_aontask();
/* returned from aontask */
/* disable power gating of RF(Cache) and ROMs */
PMU_RF_ROM_PWR_CTRL = 0;
/* disable Trunk Clock Gating (TCG) of ISH */
CCU_TCG_EN = 0;
restore_both_edge_gpio_config(both_edge_gpio_pins);
t1 = __hw_clock_source_read();
pm_ctx.aon_share->pm_state = ISH_PM_STATE_D0;
log_pm_stat(&pm_stats.d0i3, t0, t1);
/* Reload watchdog before enabling interrupts again */
watchdog_reload();
/* restore interrupts */
task_disable_irq(ISH_PMU_WAKEUP_IRQ);
restore_interrupts(current_irq_map);
}
static int d0ix_decide(timestamp_t cur_time, uint32_t idle_us)
{
int pm_state = ISH_PM_STATE_D0I0;
if (DEEP_SLEEP_ALLOWED) {
/* check if the console use has expired. */
if (sleep_mask & SLEEP_MASK_CONSOLE) {
if (cur_time.val > pm_ctx.console_expire_time.val) {
enable_sleep(SLEEP_MASK_CONSOLE);
ccprints("Disabling console in deep sleep");
} else {
return pm_state;
}
}
if (IS_ENABLED(CONFIG_ISH_PM_D0I3) &&
idle_us >= CONFIG_ISH_D0I3_MIN_USEC &&
pm_ctx.aon_valid)
pm_state = ISH_PM_STATE_D0I3;
else if (IS_ENABLED(CONFIG_ISH_PM_D0I2) &&
idle_us >= CONFIG_ISH_D0I2_MIN_USEC &&
pm_ctx.aon_valid)
pm_state = ISH_PM_STATE_D0I2;
else if (IS_ENABLED(CONFIG_ISH_PM_D0I1))
pm_state = ISH_PM_STATE_D0I1;
}
return pm_state;
}
static void pm_process(timestamp_t cur_time, uint32_t idle_us)
{
int decide;
decide = d0ix_decide(cur_time, idle_us);
switch (decide) {
case ISH_PM_STATE_D0I1:
enter_d0i1();
break;
case ISH_PM_STATE_D0I2:
enter_d0i2();
check_aon_task_status();
break;
case ISH_PM_STATE_D0I3:
enter_d0i3();
check_aon_task_status();
break;
default:
enter_d0i0();
break;
}
}
void ish_pm_init(void)
{
/* clear reset bit */
ISH_RST_REG = 0;
/* clear reset history register in CCU */
CCU_RST_HST = CCU_RST_HST;
/* disable TCG and disable BCG */
CCU_TCG_EN = 0;
CCU_BCG_EN = 0;
if (IS_ENABLED(CONFIG_ISH_PM_AONTASK))
init_aon_task();
/* unmask all wake up events */
PMU_MASK_EVENT = ~PMU_MASK_EVENT_BIT_ALL;
if (IS_ENABLED(CONFIG_ISH_PM_RESET_PREP)) {
/* unmask reset prep avail interrupt */
PMU_RST_PREP = 0;
task_enable_irq(ISH_RESET_PREP_IRQ);
}
if (IS_ENABLED(CONFIG_ISH_PM_D3)) {
/* unmask D3 and BME interrupts */
PMU_D3_STATUS &= (PMU_D3_BIT_SET | PMU_BME_BIT_SET);
if ((!(PMU_D3_STATUS & PMU_D3_BIT_SET)) &&
(PMU_D3_STATUS & PMU_BME_BIT_SET))
PMU_D3_STATUS = PMU_D3_STATUS;
task_enable_irq(ISH_D3_RISE_IRQ);
task_enable_irq(ISH_D3_FALL_IRQ);
task_enable_irq(ISH_BME_RISE_IRQ);
task_enable_irq(ISH_BME_FALL_IRQ);
}
}
__attribute__ ((noreturn))
void ish_pm_reset(enum ish_pm_state pm_state)
{
if (IS_ENABLED(CONFIG_ISH_PM_AONTASK) &&
pm_ctx.aon_valid) {
handle_reset_in_aontask(pm_state);
} else {
ish_mia_reset();
}
__builtin_unreachable();
}
void __idle(void)
{
timestamp_t t0;
int next_delay = 0;
/**
* initialize console in use to true and specify the console expire
* time in order to give a fixed window on boot
*/
disable_sleep(SLEEP_MASK_CONSOLE);
pm_ctx.console_expire_time.val = get_time().val +
CONSOLE_IN_USE_ON_BOOT_TIME;
while (1) {
t0 = get_time();
next_delay = __hw_clock_event_get() - t0.le.lo;
/*
* Most of the time, 'next_delay' will be positive. But, due to
* interrupt latency, it's possible that get_time() returns
* the value bigger than the one from __hw_clock_event_get()
* which is supposed to be updated by ISR before control reaches
* to the get_time().
*
* Here, we handle the delayed update by changing negative to 0.
*/
pm_process(t0, MAX(0, next_delay));
}
}
/*
* helper for command_idle_stats
*/
static void print_stats(const char *name, const struct pm_stat *stat)
{
if (stat->count)
ccprintf(" %s:\n"
" counts: %lu\n"
" time: %.6lus\n",
name, stat->count, stat->total_time_us);
}
/**
* Print low power idle statistics
*/
static int command_idle_stats(int argc, char **argv)
{
struct ish_aon_share *aon_share = pm_ctx.aon_share;
ccprintf("Aontask exists: %s\n", pm_ctx.aon_valid ? "Yes" : "No");
ccprintf("Total time on: %.6lus\n", get_time().val);
ccprintf("Idle sleep:\n");
print_stats("D0i0", &pm_stats.d0i0);
ccprintf("Deep sleep:\n");
print_stats("D0i1", &pm_stats.d0i1);
print_stats("D0i2", &pm_stats.d0i2);
print_stats("D0i3", &pm_stats.d0i3);
if (pm_ctx.aon_valid) {
ccprintf(" Aontask status:\n");
ccprintf(" last error: %lu\n", aon_share->last_error);
ccprintf(" error counts: %lu\n", aon_share->error_count);
}
return EC_SUCCESS;
}
DECLARE_CONSOLE_COMMAND(idlestats, command_idle_stats, "",
"Print power management statistics");
/**
* main FW only need handle PMU wakeup interrupt for D0i1 state, aontask will
* handle PMU wakeup interrupt for other low power states
*/
__maybe_unused
static void pmu_wakeup_isr(void)
{
/* at current nothing need to do */
}
#ifdef CONFIG_ISH_PM_D0I1
DECLARE_IRQ(ISH_PMU_WAKEUP_IRQ, pmu_wakeup_isr);
#endif
/**
* from ISH5.0, when system doing S0->Sx transition, will receive reset prep
* interrupt, will switch to aontask for handling
*
*/
__maybe_unused __attribute__ ((noreturn))
static void reset_prep_isr(void)
{
/* mask reset prep avail interrupt */
PMU_RST_PREP = PMU_RST_PREP_INT_MASK;
/*
* Indicate completion of servicing the interrupt to IOAPIC first
* then indicate completion of servicing the interrupt to LAPIC
*/
IOAPIC_EOI_REG = ISH_RESET_PREP_VEC;
LAPIC_EOI_REG = 0x0;
system_reset(0);
__builtin_unreachable();
}
#ifdef CONFIG_ISH_PM_RESET_PREP
DECLARE_IRQ(ISH_RESET_PREP_IRQ, reset_prep_isr);
#endif
__maybe_unused
static void handle_d3(uint32_t irq_vec)
{
PMU_D3_STATUS = PMU_D3_STATUS;
if (PMU_D3_STATUS & (PMU_D3_BIT_RISING_EDGE_STATUS | PMU_D3_BIT_SET)) {
/*
* Indicate completion of servicing the interrupt to IOAPIC
* first then indicate completion of servicing the interrupt
* to LAPIC
*/
IOAPIC_EOI_REG = irq_vec;
LAPIC_EOI_REG = 0x0;
ish_persistent_data_commit();
ish_pm_reset(ISH_PM_STATE_D3);
}
}
static void d3_rise_isr(void)
{
handle_d3(ISH_D3_RISE_VEC);
}
static void d3_fall_isr(void)
{
handle_d3(ISH_D3_FALL_VEC);
}
static void bme_rise_isr(void)
{
handle_d3(ISH_BME_RISE_VEC);
}
static void bme_fall_isr(void)
{
handle_d3(ISH_BME_FALL_VEC);
}
#ifdef CONFIG_ISH_PM_D3
DECLARE_IRQ(ISH_D3_RISE_IRQ, d3_rise_isr);
DECLARE_IRQ(ISH_D3_FALL_IRQ, d3_fall_isr);
DECLARE_IRQ(ISH_BME_RISE_IRQ, bme_rise_isr);
DECLARE_IRQ(ISH_BME_FALL_IRQ, bme_fall_isr);
#endif
void ish_pm_refresh_console_in_use(void)
{
disable_sleep(SLEEP_MASK_CONSOLE);
/* Set console in use expire time. */
pm_ctx.console_expire_time = get_time();
pm_ctx.console_expire_time.val +=
pm_ctx.console_in_use_timeout_sec * SECOND;
}