You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
467 lines
12 KiB
467 lines
12 KiB
/* |
|
* Copyright (c) 2018-2021 Intel Corporation |
|
* |
|
* SPDX-License-Identifier: Apache-2.0 |
|
*/ |
|
|
|
#define DT_DRV_COMPAT intel_hpet |
|
#include <zephyr/init.h> |
|
#include <zephyr/drivers/timer/system_timer.h> |
|
#include <zephyr/sys_clock.h> |
|
#include <zephyr/spinlock.h> |
|
#include <zephyr/irq.h> |
|
#include <zephyr/linker/sections.h> |
|
|
|
#include <zephyr/dt-bindings/interrupt-controller/intel-ioapic.h> |
|
|
|
#include <soc.h> |
|
|
|
/** |
|
* @file |
|
* @brief HPET (High Precision Event Timers) driver |
|
* |
|
* HPET hardware contains a number of timers which can be used by |
|
* the operating system, where the number of timers is implementation |
|
* specific. The timers are implemented as a single up-counter with |
|
* a set of comparators where the counter increases monotonically. |
|
* Each timer has a match register and a comparator, and can generate |
|
* an interrupt when the value in the match register equals the value of |
|
* the free running counter. Some of these timers can be enabled to |
|
* generate periodic interrupt. |
|
* |
|
* The HPET registers are usually mapped to memory space on x86 |
|
* hardware. If this is not the case, custom register access functions |
|
* can be used by defining macro HPET_USE_CUSTOM_REG_ACCESS_FUNCS in |
|
* soc.h, and implementing necessary initialization and access |
|
* functions as described below. |
|
* |
|
* HPET_COUNTER_CLK_PERIOD can be overridden in soc.h if |
|
* COUNTER_CLK_PERIOD is not in femtoseconds (1e-15 sec). |
|
*/ |
|
|
|
/* General Configuration register */ |
|
#define GCONF_ENABLE BIT(0) |
|
#define GCONF_LR BIT(1) /* legacy interrupt routing, */ |
|
/* disables PIT */ |
|
|
|
/* General Interrupt Status register */ |
|
#define TIMER0_INT_STS BIT(0) |
|
|
|
/* Timer Configuration and Capabilities register */ |
|
#define TIMER_CONF_INT_LEVEL BIT(1) |
|
#define TIMER_CONF_INT_ENABLE BIT(2) |
|
#define TIMER_CONF_PERIODIC BIT(3) |
|
#define TIMER_CONF_VAL_SET BIT(6) |
|
#define TIMER_CONF_MODE32 BIT(8) |
|
#define TIMER_CONF_FSB_EN BIT(14) /* FSB interrupt delivery */ |
|
/* enable */ |
|
|
|
DEVICE_MMIO_TOPLEVEL_STATIC(hpet_regs, DT_DRV_INST(0)); |
|
|
|
#define HPET_REG_ADDR(off) \ |
|
((mm_reg_t)(DEVICE_MMIO_TOPLEVEL_GET(hpet_regs) + (off))) |
|
|
|
/* High dword of General Capabilities and ID register */ |
|
#define CLK_PERIOD_REG HPET_REG_ADDR(0x04) |
|
|
|
/* General Configuration register */ |
|
#define GCONF_REG HPET_REG_ADDR(0x10) |
|
|
|
/* General Interrupt Status register */ |
|
#define INTR_STATUS_REG HPET_REG_ADDR(0x20) |
|
|
|
/* Main Counter Register */ |
|
#define MAIN_COUNTER_LOW_REG HPET_REG_ADDR(0xf0) |
|
#define MAIN_COUNTER_HIGH_REG HPET_REG_ADDR(0xf4) |
|
|
|
/* Timer 0 Configuration and Capabilities register */ |
|
#define TIMER0_CONF_REG HPET_REG_ADDR(0x100) |
|
|
|
/* Timer 0 Comparator Register */ |
|
#define TIMER0_COMPARATOR_LOW_REG HPET_REG_ADDR(0x108) |
|
#define TIMER0_COMPARATOR_HIGH_REG HPET_REG_ADDR(0x10c) |
|
|
|
#if defined(CONFIG_TEST) |
|
const int32_t z_sys_timer_irq_for_test = DT_IRQN(DT_INST(0, intel_hpet)); |
|
#endif |
|
|
|
/** |
|
* @brief Return the value of the main counter. |
|
* |
|
* @return Value of Main Counter |
|
*/ |
|
static inline uint64_t hpet_counter_get(void) |
|
{ |
|
#ifdef CONFIG_64BIT |
|
uint64_t val = sys_read64(MAIN_COUNTER_LOW_REG); |
|
|
|
return val; |
|
#else |
|
uint32_t high; |
|
uint32_t low; |
|
|
|
do { |
|
high = sys_read32(MAIN_COUNTER_HIGH_REG); |
|
low = sys_read32(MAIN_COUNTER_LOW_REG); |
|
} while (high != sys_read32(MAIN_COUNTER_HIGH_REG)); |
|
|
|
return ((uint64_t)high << 32) | low; |
|
#endif |
|
} |
|
|
|
/** |
|
* @brief Get COUNTER_CLK_PERIOD |
|
* |
|
* Read and return the COUNTER_CLK_PERIOD, which is the high |
|
* 32-bit of the General Capabilities and ID Register. This can |
|
* be used to calculate the frequency of the main counter. |
|
* |
|
* Usually the period is in femtoseconds. If this is not |
|
* the case, define HPET_COUNTER_CLK_PERIOD in soc.h so |
|
* it can be used to calculate frequency. |
|
* |
|
* @return COUNTER_CLK_PERIOD |
|
*/ |
|
static inline uint32_t hpet_counter_clk_period_get(void) |
|
{ |
|
return sys_read32(CLK_PERIOD_REG); |
|
} |
|
|
|
/** |
|
* @brief Return the value of the General Configuration Register |
|
* |
|
* @return Value of the General Configuration Register |
|
*/ |
|
static inline uint32_t hpet_gconf_get(void) |
|
{ |
|
return sys_read32(GCONF_REG); |
|
} |
|
|
|
/** |
|
* @brief Write to General Configuration Register |
|
* |
|
* @param val Value to be written to the register |
|
*/ |
|
static inline void hpet_gconf_set(uint32_t val) |
|
{ |
|
sys_write32(val, GCONF_REG); |
|
} |
|
|
|
/** |
|
* @brief Return the value of the Timer Configuration Register |
|
* |
|
* This reads and returns the value of the Timer Configuration |
|
* Register of Timer #0. |
|
* |
|
* @return Value of the Timer Configuration Register |
|
*/ |
|
static inline uint32_t hpet_timer_conf_get(void) |
|
{ |
|
return sys_read32(TIMER0_CONF_REG); |
|
} |
|
|
|
/** |
|
* @brief Write to the Timer Configuration Register |
|
* |
|
* This writes the specified value to the Timer Configuration |
|
* Register of Timer #0. |
|
* |
|
* @param val Value to be written to the register |
|
*/ |
|
static inline void hpet_timer_conf_set(uint32_t val) |
|
{ |
|
sys_write32(val, TIMER0_CONF_REG); |
|
} |
|
|
|
/* |
|
* The following register access functions should work on generic x86 |
|
* hardware. If the targeted SoC requires special handling of HPET |
|
* registers, these functions will need to be implemented in the SoC |
|
* layer by first defining the macro HPET_USE_CUSTOM_REG_ACCESS_FUNCS |
|
* in soc.h to signal such intent. |
|
* |
|
* This is a list of functions which must be implemented in the SoC |
|
* layer: |
|
* void hpet_timer_comparator_set(uint32_t val) |
|
*/ |
|
#ifndef HPET_USE_CUSTOM_REG_ACCESS_FUNCS |
|
|
|
/** |
|
* @brief Write to the Timer Comparator Value Register |
|
* |
|
* This writes the specified value to the Timer Comparator |
|
* Value Register of Timer #0. |
|
* |
|
* @param val Value to be written to the register |
|
*/ |
|
static inline void hpet_timer_comparator_set(uint64_t val) |
|
{ |
|
#if CONFIG_X86_64 |
|
sys_write64(val, TIMER0_COMPARATOR_LOW_REG); |
|
#else |
|
sys_write32((uint32_t)val, TIMER0_COMPARATOR_LOW_REG); |
|
sys_write32((uint32_t)(val >> 32), TIMER0_COMPARATOR_HIGH_REG); |
|
#endif |
|
} |
|
#endif /* HPET_USE_CUSTOM_REG_ACCESS_FUNCS */ |
|
|
|
#ifndef HPET_COUNTER_CLK_PERIOD |
|
/* COUNTER_CLK_PERIOD (CLK_PERIOD_REG) is in femtoseconds (1e-15 sec) */ |
|
#define HPET_COUNTER_CLK_PERIOD (1000000000000000ULL) |
|
#endif |
|
|
|
/* |
|
* HPET_INT_LEVEL_TRIGGER is used to set HPET interrupt as level trigger |
|
* for ARM CPU with NVIC like EHL PSE, whose DTS interrupt setting |
|
* has no "sense" cell. |
|
*/ |
|
#if (DT_INST_IRQ_HAS_CELL(0, sense)) |
|
#ifdef HPET_INT_LEVEL_TRIGGER |
|
__WARN("HPET_INT_LEVEL_TRIGGER has no effect, DTS setting is used instead") |
|
#undef HPET_INT_LEVEL_TRIGGER |
|
#endif |
|
#if ((DT_INST_IRQ(0, sense) & IRQ_TYPE_LEVEL) == IRQ_TYPE_LEVEL) |
|
#define HPET_INT_LEVEL_TRIGGER |
|
#endif |
|
#endif /* (DT_INST_IRQ_HAS_CELL(0, sense)) */ |
|
|
|
static __pinned_bss struct k_spinlock lock; |
|
static __pinned_bss uint64_t last_count; |
|
static __pinned_bss uint64_t last_tick; |
|
static __pinned_bss uint32_t last_elapsed; |
|
|
|
#ifdef CONFIG_TIMER_READS_ITS_FREQUENCY_AT_RUNTIME |
|
static __pinned_bss unsigned int cyc_per_tick; |
|
#else |
|
#define cyc_per_tick \ |
|
(CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC / CONFIG_SYS_CLOCK_TICKS_PER_SEC) |
|
#endif /* CONFIG_TIMER_READS_ITS_FREQUENCY_AT_RUNTIME */ |
|
|
|
#define HPET_MAX_TICKS ((int32_t)0x7fffffff) |
|
|
|
#ifdef HPET_INT_LEVEL_TRIGGER |
|
/** |
|
* @brief Write to General Interrupt Status Register |
|
* |
|
* This is used to acknowledge and clear interrupt bits. |
|
* |
|
* @param val Value to be written to the register |
|
*/ |
|
static inline void hpet_int_sts_set(uint32_t val) |
|
{ |
|
sys_write32(val, INTR_STATUS_REG); |
|
} |
|
#endif |
|
|
|
/* ensure the comparator is always set ahead of the current counter value */ |
|
static inline void hpet_timer_comparator_set_safe(uint64_t next) |
|
{ |
|
hpet_timer_comparator_set(next); |
|
|
|
uint64_t now = hpet_counter_get(); |
|
|
|
if (unlikely((int64_t)(next - now) <= 0)) { |
|
uint32_t bump = 1; |
|
|
|
do { |
|
next = now + bump; |
|
bump *= 2; |
|
hpet_timer_comparator_set(next); |
|
now = hpet_counter_get(); |
|
} while ((int64_t)(next - now) <= 0); |
|
} |
|
} |
|
|
|
__isr |
|
static void hpet_isr(const void *arg) |
|
{ |
|
ARG_UNUSED(arg); |
|
|
|
k_spinlock_key_t key = k_spin_lock(&lock); |
|
|
|
uint64_t now = hpet_counter_get(); |
|
|
|
#ifdef HPET_INT_LEVEL_TRIGGER |
|
/* |
|
* Clear interrupt only if level trigger is selected. |
|
* When edge trigger is selected, spec says only 0 can |
|
* be written. |
|
*/ |
|
hpet_int_sts_set(TIMER0_INT_STS); |
|
#endif |
|
|
|
if (IS_ENABLED(CONFIG_SMP) && |
|
IS_ENABLED(CONFIG_QEMU_TARGET)) { |
|
/* Qemu in SMP mode has observed the clock going |
|
* "backwards" relative to interrupts already received |
|
* on the other CPU, despite the HPET being |
|
* theoretically a global device. |
|
*/ |
|
int64_t diff = (int64_t)(now - last_count); |
|
|
|
if (last_count && diff < 0) { |
|
now = last_count; |
|
} |
|
} |
|
uint32_t dticks = (uint32_t)((now - last_count) / cyc_per_tick); |
|
|
|
last_count += (uint64_t)dticks * cyc_per_tick; |
|
last_tick += dticks; |
|
last_elapsed = 0; |
|
|
|
if (!IS_ENABLED(CONFIG_TICKLESS_KERNEL)) { |
|
uint64_t next = last_count + cyc_per_tick; |
|
|
|
hpet_timer_comparator_set_safe(next); |
|
} |
|
|
|
k_spin_unlock(&lock, key); |
|
sys_clock_announce(dticks); |
|
} |
|
|
|
__pinned_func |
|
static void config_timer0(unsigned int irq) |
|
{ |
|
uint32_t val = hpet_timer_conf_get(); |
|
|
|
/* 5-bit IRQ field starting at bit 9 */ |
|
val = (val & ~(0x1f << 9)) | ((irq & 0x1f) << 9); |
|
|
|
#ifdef HPET_INT_LEVEL_TRIGGER |
|
/* Set level trigger if selected */ |
|
val |= TIMER_CONF_INT_LEVEL; |
|
#endif |
|
|
|
val &= ~((uint32_t)(TIMER_CONF_MODE32 | TIMER_CONF_PERIODIC | |
|
TIMER_CONF_FSB_EN)); |
|
val |= TIMER_CONF_INT_ENABLE; |
|
|
|
hpet_timer_conf_set(val); |
|
} |
|
|
|
__boot_func |
|
void smp_timer_init(void) |
|
{ |
|
/* Noop, the HPET is a single system-wide device and it's |
|
* configured to deliver interrupts to every CPU, so there's |
|
* nothing to do at initialization on auxiliary CPUs. |
|
*/ |
|
} |
|
|
|
__pinned_func |
|
void sys_clock_set_timeout(int32_t ticks, bool idle) |
|
{ |
|
ARG_UNUSED(idle); |
|
|
|
#if defined(CONFIG_TICKLESS_KERNEL) |
|
uint32_t reg; |
|
|
|
if (ticks == K_TICKS_FOREVER && idle) { |
|
reg = hpet_gconf_get(); |
|
reg &= ~GCONF_ENABLE; |
|
hpet_gconf_set(reg); |
|
return; |
|
} |
|
|
|
ticks = ticks == K_TICKS_FOREVER ? HPET_MAX_TICKS : ticks; |
|
ticks = CLAMP(ticks, 0, HPET_MAX_TICKS/2); |
|
|
|
k_spinlock_key_t key = k_spin_lock(&lock); |
|
uint64_t cyc = (last_tick + last_elapsed + ticks) * cyc_per_tick; |
|
|
|
hpet_timer_comparator_set_safe(cyc); |
|
k_spin_unlock(&lock, key); |
|
#endif |
|
} |
|
|
|
__pinned_func |
|
uint32_t sys_clock_elapsed(void) |
|
{ |
|
if (!IS_ENABLED(CONFIG_TICKLESS_KERNEL)) { |
|
return 0; |
|
} |
|
|
|
k_spinlock_key_t key = k_spin_lock(&lock); |
|
uint64_t now = hpet_counter_get(); |
|
uint32_t ret = (uint32_t)((now - last_count) / cyc_per_tick); |
|
|
|
last_elapsed = ret; |
|
k_spin_unlock(&lock, key); |
|
return ret; |
|
} |
|
|
|
__pinned_func |
|
uint32_t sys_clock_cycle_get_32(void) |
|
{ |
|
return (uint32_t)hpet_counter_get(); |
|
} |
|
|
|
__pinned_func |
|
uint64_t sys_clock_cycle_get_64(void) |
|
{ |
|
return hpet_counter_get(); |
|
} |
|
|
|
__pinned_func |
|
void sys_clock_idle_exit(void) |
|
{ |
|
uint32_t reg; |
|
|
|
reg = hpet_gconf_get(); |
|
reg |= GCONF_ENABLE; |
|
hpet_gconf_set(reg); |
|
} |
|
|
|
__boot_func |
|
static int sys_clock_driver_init(void) |
|
{ |
|
extern unsigned int z_clock_hw_cycles_per_sec; |
|
uint32_t hz, reg; |
|
|
|
ARG_UNUSED(hz); |
|
ARG_UNUSED(z_clock_hw_cycles_per_sec); |
|
|
|
DEVICE_MMIO_TOPLEVEL_MAP(hpet_regs, K_MEM_CACHE_NONE); |
|
|
|
#if DT_INST_IRQ_HAS_CELL(0, sense) |
|
IRQ_CONNECT(DT_INST_IRQN(0), |
|
DT_INST_IRQ(0, priority), |
|
hpet_isr, 0, DT_INST_IRQ(0, sense)); |
|
#else |
|
IRQ_CONNECT(DT_INST_IRQN(0), |
|
DT_INST_IRQ(0, priority), |
|
hpet_isr, 0, 0); |
|
#endif |
|
config_timer0(DT_INST_IRQN(0)); |
|
irq_enable(DT_INST_IRQN(0)); |
|
|
|
#ifdef CONFIG_TIMER_READS_ITS_FREQUENCY_AT_RUNTIME |
|
hz = (uint32_t)(HPET_COUNTER_CLK_PERIOD / hpet_counter_clk_period_get()); |
|
z_clock_hw_cycles_per_sec = hz; |
|
cyc_per_tick = hz / CONFIG_SYS_CLOCK_TICKS_PER_SEC; |
|
#endif |
|
|
|
reg = hpet_gconf_get(); |
|
reg |= GCONF_ENABLE; |
|
|
|
#if (DT_INST_PROP(0, no_legacy_irq) == 0) |
|
/* Note: we set the legacy routing bit, because otherwise |
|
* nothing in Zephyr disables the PIT which then fires |
|
* interrupts into the same IRQ. But that means we're then |
|
* forced to use IRQ2 contra the way the kconfig IRQ selection |
|
* is supposed to work. Should fix this. |
|
*/ |
|
reg |= GCONF_LR; |
|
#endif |
|
|
|
hpet_gconf_set(reg); |
|
|
|
last_tick = hpet_counter_get() / cyc_per_tick; |
|
last_count = last_tick * cyc_per_tick; |
|
hpet_timer_comparator_set_safe(last_count + cyc_per_tick); |
|
|
|
return 0; |
|
} |
|
|
|
SYS_INIT(sys_clock_driver_init, PRE_KERNEL_2, |
|
CONFIG_SYSTEM_CLOCK_INIT_PRIORITY);
|
|
|