From e0c56f1998b8971f4f41e9a21ced62980c770801 Mon Sep 17 00:00:00 2001 From: Andy Ross Date: Sat, 5 Aug 2023 06:11:43 -0700 Subject: [PATCH] drivers/timer: MediaTek audio DSP timer device These devices have a somewhat odd hybrid design, with a free-running 64 bit up counter but no comparator. Instead interrupts are triggered by (one of an array of) 32 bit down counters with reset (a-la SysTick, but without the 24 bit precision issues). The combination actually results in a fairly simple driver as we can skip the comparator rounding math. Signed-off-by: Andy Ross --- drivers/timer/CMakeLists.txt | 1 + drivers/timer/Kconfig | 1 + drivers/timer/Kconfig.mtk_adsp | 11 ++ drivers/timer/mtk_adsp_timer.c | 180 +++++++++++++++++++++++++++++++++ 4 files changed, 193 insertions(+) create mode 100644 drivers/timer/Kconfig.mtk_adsp create mode 100644 drivers/timer/mtk_adsp_timer.c diff --git a/drivers/timer/CMakeLists.txt b/drivers/timer/CMakeLists.txt index 80f8431c4d0..659d86df801 100644 --- a/drivers/timer/CMakeLists.txt +++ b/drivers/timer/CMakeLists.txt @@ -35,3 +35,4 @@ zephyr_library_sources_ifdef(CONFIG_STM32_LPTIM_TIMER stm32_lptim_timer.c) zephyr_library_sources_ifdef(CONFIG_XLNX_PSTTC_TIMER xlnx_psttc_timer.c) zephyr_library_sources_ifdef(CONFIG_XTENSA_TIMER xtensa_sys_timer.c) zephyr_library_sources_ifdef(CONFIG_SMARTBOND_TIMER smartbond_timer.c) +zephyr_library_sources_ifdef(CONFIG_MTK_ADSP_TIMER mtk_adsp_timer.c) diff --git a/drivers/timer/Kconfig b/drivers/timer/Kconfig index 2ddfe321e77..69eb00cbc5e 100644 --- a/drivers/timer/Kconfig +++ b/drivers/timer/Kconfig @@ -94,6 +94,7 @@ source "drivers/timer/Kconfig.smartbond" source "drivers/timer/Kconfig.stm32_lptim" source "drivers/timer/Kconfig.xlnx_psttc" source "drivers/timer/Kconfig.xtensa" +source "drivers/timer/Kconfig.mtk_adsp" endmenu diff --git a/drivers/timer/Kconfig.mtk_adsp b/drivers/timer/Kconfig.mtk_adsp new file mode 100644 index 00000000000..d3b7501e307 --- /dev/null +++ b/drivers/timer/Kconfig.mtk_adsp @@ -0,0 +1,11 @@ +# Copyright 2023 The ChromiumOS Authors +# SPDX-License-Identifier: Apache-2.0 + +config MTK_ADSP_TIMER + bool "MediaTek Audio DSP timer" + select TICKLESS_CAPABLE + select TIMER_HAS_64BIT_CYCLE_COUNTER + select SYSTEM_CLOCK_LOCK_FREE_COUNT + help + MediaTek MT81xx Audio DSPs have a 13 Mhz wall clock timer + for system time that is independent of CPU speed. diff --git a/drivers/timer/mtk_adsp_timer.c b/drivers/timer/mtk_adsp_timer.c new file mode 100644 index 00000000000..7a6c06d9e61 --- /dev/null +++ b/drivers/timer/mtk_adsp_timer.c @@ -0,0 +1,180 @@ +/* Copyright 2023 The ChromiumOS Authors + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include + +#define OSTIMER64_BASE DT_REG_ADDR(DT_NODELABEL(ostimer64)) +#define OSTIMER_BASE DT_REG_ADDR(DT_NODELABEL(ostimer0)) + +/* + * This device has a LOT of timer hardware. There are SIX + * instantiated devices, with THREE different interfaces! Not + * including the three Xtensa CCOUNT timers! + * + * In practice only "ostimer0" is used as an interrupt source by the + * original SOF code, and the "ostimer64" and "platform" timers + * reflect the same underlying clock (though they're different + * counters with different values). There is also a "ptimer" device, + * which is unused by SOF and not exercised by this driver. + * + * The driver architecture itself is sort of a hybrid of what other + * Zephyr drivers use: there is no (or at least no documented) + * comparator facility. The "ostimer64" is used as the system clock, + * which is a 13 MHz 64 bit up-counter. But timeout interrupts are + * delivered by ostimers[0], which is a 32 bit (!) down-counter (!!) + * running at twice (!!!) the rate: 26MHz. Testing shows they're + * slaved the same underlying clock -- they don't skew relative to + * each other. + */ +struct mtk_ostimer { + unsigned int con; + unsigned int rst; + unsigned int cur; + unsigned int irq_ack; +}; + +struct mtk_ostimer64 { + unsigned int con; + unsigned int init_l; + unsigned int init_h; + unsigned int cur_l; + unsigned int cur_h; + unsigned int tval_h; + unsigned int irq_ack; +}; + +#define OSTIMER64 (*(volatile struct mtk_ostimer64 *)OSTIMER64_BASE) + +#define OSTIMERS ((volatile struct mtk_ostimer *)OSTIMER_BASE) + +#define OSTIMER_CON_ENABLE BIT(0) +#define OSTIMER_CON_CLKSRC_MASK 0x30 +#define OSTIMER_CON_CLKSRC_32K 0x00 /* 32768 Hz */ +#define OSTIMER_CON_CLKSRC_26M 0x10 /* 26 MHz */ +#define OSTIMER_CON_CLKSRC_BCLK 0x20 /* CPU speed, 720 MHz */ +#define OSTIMER_CON_CLKSRC_PCLK 0x30 /* ~312 MHz experimentally */ + +#define OSTIMER_IRQ_ACK_ENABLE BIT(4) /* read = status, write = enable */ +#define OSTIMER_IRQ_ACK_CLEAR BIT(5) + +#define OST64_HZ 13000000U +#define OST_HZ 26000000U +#define OST64_PER_TICK (OST64_HZ / CONFIG_SYS_CLOCK_TICKS_PER_SEC) +#define OST_PER_TICK (OST_HZ / CONFIG_SYS_CLOCK_TICKS_PER_SEC) + +#define MAX_TICKS ((0xffffffffU - OST_PER_TICK) / OST_PER_TICK) +#define CYC64_MAX (0xffffffff - OST64_PER_TICK) + +static struct k_spinlock lock; + +static uint64_t last_announce; + +uint32_t sys_clock_cycle_get_32(void) +{ + return OSTIMER64.cur_l; +} + +uint64_t sys_clock_cycle_get_64(void) +{ + uint32_t l, h0, h1; + + do { + h0 = OSTIMER64.cur_h; + l = OSTIMER64.cur_l; + h1 = OSTIMER64.cur_h; + } while (h0 != h1); + + return (((uint64_t)h0) << 32) | l; +} + +void sys_clock_set_timeout(int32_t ticks, bool idle) +{ + /* Compute desired expiration time */ + uint64_t now = sys_clock_cycle_get_64(); + uint64_t end = now + CLAMP(ticks - 1, 0, MAX_TICKS) * OST64_PER_TICK; + uint32_t dt = (uint32_t)MIN(end - last_announce, CYC64_MAX); + + /* Round up to tick boundary */ + dt = ((dt + OST64_PER_TICK - 1) / OST64_PER_TICK) * OST64_PER_TICK; + + /* Convert to "fast" OSTIMER[0] cycles! */ + uint32_t cyc = 2 * (dt - (uint32_t)(now - last_announce)); + + /* Writes to RST need to be done when the device is disabled, + * and automatically reset CUR (which reads zero while disabled) + */ + OSTIMERS[0].con &= ~OSTIMER_CON_ENABLE; + OSTIMERS[0].rst = cyc; + OSTIMERS[0].irq_ack |= OSTIMER_IRQ_ACK_CLEAR; + OSTIMERS[0].irq_ack |= OSTIMER_IRQ_ACK_ENABLE; + OSTIMERS[0].con |= OSTIMER_CON_ENABLE; +} + +uint32_t sys_clock_elapsed(void) +{ + k_spinlock_key_t key = k_spin_lock(&lock); + uint32_t ret; + + ret = (uint32_t)((sys_clock_cycle_get_64() - last_announce) + / OST64_PER_TICK); + k_spin_unlock(&lock, key); + return ret; +} + +static void timer_isr(__maybe_unused void *arg) +{ + /* Note: no locking. As it happens, on MT8195/8186/8188 all + * Zephyr-usable interrupts are delivered at the same level. + * So we can't be preempted and there's actually no need to + * take a spinlock here. But ideally we should verify/detect + * this instead of trusting blindly; this is fragile if future + * devices add nested interrupts. + */ + uint64_t dcyc = sys_clock_cycle_get_64() - last_announce; + uint64_t ticks = dcyc / OST64_PER_TICK; + + /* Leave the device disabled after clearing the interrupt, + * sys_clock_set_timeout() is responsible for turning it back + * on. + */ + OSTIMERS[0].irq_ack |= OSTIMER_IRQ_ACK_CLEAR; + OSTIMERS[0].con &= ~OSTIMER_CON_ENABLE; + OSTIMERS[0].irq_ack &= ~OSTIMER_IRQ_ACK_ENABLE; + + last_announce += ticks * OST64_PER_TICK; + sys_clock_announce(ticks); + + if (!IS_ENABLED(CONFIG_TICKLESS_KERNEL)) { + sys_clock_set_timeout(1, false); + } +} + +static int mtk_adsp_timer_init(void) +{ + IRQ_CONNECT(DT_IRQN(DT_NODELABEL(ostimer0)), 0, timer_isr, 0, 0); + irq_enable(DT_IRQN(DT_NODELABEL(ostimer0))); + + /* Disable all timers */ + for (int i = 0; i < 4; i++) { + OSTIMERS[i].con &= ~OSTIMER_CON_ENABLE; + OSTIMERS[i].irq_ack |= OSTIMER_IRQ_ACK_CLEAR; + OSTIMERS[i].irq_ack &= ~OSTIMER_IRQ_ACK_ENABLE; + } + + /* Set them up to use the same clock. Note that OSTIMER64 has + * a built-in divide by two (or it's configurable and I don't + * know the register) and exposes a 13 MHz counter! + */ + OSTIMERS[0].con = ((OSTIMERS[0].con & ~OSTIMER_CON_CLKSRC_MASK) + | OSTIMER_CON_CLKSRC_26M); + OSTIMERS[0].con |= OSTIMER_CON_ENABLE; + + /* Clock is free running and survives reset, doesn't start at zero */ + last_announce = sys_clock_cycle_get_64(); + + return 0; +} + +SYS_INIT(mtk_adsp_timer_init, PRE_KERNEL_2, CONFIG_SYSTEM_CLOCK_INIT_PRIORITY);