[PATCH v2 10/11] hw/timer: Add QTimer device

Brian Cain posted 11 patches 3 weeks, 5 days ago
[PATCH v2 10/11] hw/timer: Add QTimer device
Posted by Brian Cain 3 weeks, 5 days ago
From: Sid Manning <sidneym@quicinc.com>

Note: QTimer was implemented before ARM SSE Timer was upstreamed, there may
be opportunity to use that device instead.

Co-authored-by: Damien Hedde <damien.hedde@dahe.fr>
Co-authored-by: Tobias Röhmel <quic_trohmel@quicinc.com>
Signed-off-by: Brian Cain <brian.cain@oss.qualcomm.com>
---
 MAINTAINERS                    |   2 +
 include/hw/timer/qct-qtimer.h  |  85 ++++++
 hw/hexagon/hexagon_dsp.c       |   7 +-
 hw/hexagon/hexagon_globalreg.c |   9 +-
 hw/hexagon/virt.c              |  22 ++
 hw/timer/qct-qtimer.c          | 520 +++++++++++++++++++++++++++++++++
 hw/timer/meson.build           |   2 +
 7 files changed, 642 insertions(+), 5 deletions(-)
 create mode 100644 include/hw/timer/qct-qtimer.h
 create mode 100644 hw/timer/qct-qtimer.c

diff --git a/MAINTAINERS b/MAINTAINERS
index e19fcf9e69..4f7748679b 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -230,7 +230,9 @@ M: Brian Cain <brian.cain@oss.qualcomm.com>
 S: Supported
 F: target/hexagon/
 F: hw/hexagon/
+F: hw/timer/qct-qtimer.c
 F: include/hw/hexagon/
+F: include/hw/timer/qct-qtimer.h
 X: target/hexagon/idef-parser/
 X: target/hexagon/gen_idef_parser_funcs.py
 F: linux-user/hexagon/
diff --git a/include/hw/timer/qct-qtimer.h b/include/hw/timer/qct-qtimer.h
new file mode 100644
index 0000000000..90f7981ccf
--- /dev/null
+++ b/include/hw/timer/qct-qtimer.h
@@ -0,0 +1,85 @@
+/*
+ * Qualcomm QCT QTimer
+ *
+ * Copyright(c) 2019-2025 Qualcomm Innovation Center, Inc. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+#ifndef TIMER_QCT_QTIMER_H
+#define TIMER_QCT_QTIMER_H
+
+#include "hw/ptimer.h"
+#include "hw/sysbus.h"
+
+#define TYPE_QCT_QTIMER "qct-qtimer"
+#define TYPE_QCT_HEXTIMER "qct-hextimer"
+OBJECT_DECLARE_SIMPLE_TYPE(QCTQtimerState, QCT_QTIMER)
+OBJECT_DECLARE_SIMPLE_TYPE(QCTHextimerState, QCT_HEXTIMER)
+
+struct QCTHextimerState {
+    QCTQtimerState *qtimer;
+    ptimer_state *timer;
+    uint64_t cntval; /*
+                      * Physical timer compare value interrupt when cntpct >
+                      * cntval
+                      */
+    uint64_t cntpct; /* Physical counter */
+    uint32_t control;
+    uint32_t cnt_ctrl;
+    uint32_t cntpl0acr;
+    uint64_t limit;
+    uint32_t freq;
+    uint32_t int_level;
+    qemu_irq irq;
+};
+
+#define QCT_QTIMER_TIMER_FRAME_ELTS (8)
+#define QCT_QTIMER_TIMER_VIEW_ELTS (2)
+struct QCTQtimerState {
+    SysBusDevice parent_obj;
+
+    MemoryRegion iomem;
+    MemoryRegion view_iomem;
+    uint32_t secure;
+    struct QCTHextimerState timer[QCT_QTIMER_TIMER_FRAME_ELTS];
+    uint32_t frame_id;
+    uint32_t freq;
+    uint32_t nr_frames;
+    uint32_t nr_views;
+    uint32_t cnttid;
+};
+
+#define QCT_QTIMER_AC_CNTFRQ (0x000)
+#define QCT_QTIMER_AC_CNTSR (0x004)
+#define QCT_QTIMER_AC_CNTSR_NSN_1 (1 << 0)
+#define QCT_QTIMER_AC_CNTSR_NSN_2 (1 << 1)
+#define QCT_QTIMER_AC_CNTSR_NSN_3 (1 << 2)
+#define QCT_QTIMER_AC_CNTTID (0x08)
+#define QCT_QTIMER_AC_CNTACR_0 (0x40)
+#define QCT_QTIMER_AC_CNTACR_1 (0x44)
+#define QCT_QTIMER_AC_CNTACR_2 (0x48)
+#define QCT_QTIMER_AC_CNTACR_RWPT (1 << 5) /* R/W of CNTP_* regs */
+#define QCT_QTIMER_AC_CNTACR_RWVT (1 << 4) /* R/W of CNTV_* regs */
+#define QCT_QTIMER_AC_CNTACR_RVOFF (1 << 3) /* R/W of CNTVOFF register */
+#define QCT_QTIMER_AC_CNTACR_RFRQ (1 << 2) /* R/W of CNTFRQ register */
+#define QCT_QTIMER_AC_CNTACR_RPVCT (1 << 1) /* R/W of CNTVCT register */
+#define QCT_QTIMER_AC_CNTACR_RPCT (1 << 0) /* R/W of CNTPCT register */
+#define QCT_QTIMER_VERSION (0x0fd0)
+#define QCT_QTIMER_CNTPCT_LO (0x000)
+#define QCT_QTIMER_CNTPCT_HI (0x004)
+#define QCT_QTIMER_CNT_FREQ (0x010)
+#define QCT_QTIMER_CNTPL0ACR (0x014)
+#define QCT_QTIMER_CNTPL0ACR_PL0CTEN (1 << 9)
+#define QCT_QTIMER_CNTPL0ACR_PL0TVEN (1 << 8)
+#define QCT_QTIMER_CNTPL0ACR_PL0VCTEN (1 << 1)
+#define QCT_QTIMER_CNTPL0ACR_PL0PCTEN (1 << 0)
+#define QCT_QTIMER_CNTP_CVAL_LO (0x020)
+#define QCT_QTIMER_CNTP_CVAL_HI (0x024)
+#define QCT_QTIMER_CNTP_TVAL (0x028)
+#define QCT_QTIMER_CNTP_CTL (0x02c)
+#define QCT_QTIMER_CNTP_CTL_ISTAT (1 << 2)
+#define QCT_QTIMER_CNTP_CTL_INTEN (1 << 1)
+#define QCT_QTIMER_CNTP_CTL_ENABLE (1 << 0)
+#define QCT_QTIMER_AC_CNTACR_START 0x40
+#define QCT_QTIMER_AC_CNTACR_END 0x5C
+
+#endif /* TIMER_QCT_QTIMER_H */
diff --git a/hw/hexagon/hexagon_dsp.c b/hw/hexagon/hexagon_dsp.c
index 510378280e..e0e39fca90 100644
--- a/hw/hexagon/hexagon_dsp.c
+++ b/hw/hexagon/hexagon_dsp.c
@@ -3,7 +3,6 @@
  * subsystem with few peripherals, like the Compute DSP.
  *
  * Copyright (c) 2020-2024 Qualcomm Innovation Center, Inc. All Rights Reserved.
- *
  * SPDX-License-Identifier: GPL-2.0-or-later
  */
 
@@ -126,7 +125,11 @@ static void hexagon_common_init(MachineState *machine, Rev_t rev,
          */
         qdev_prop_set_bit(DEVICE(cpu), "start-powered-off", (i != 0));
         qdev_prop_set_uint32(DEVICE(cpu), "l2vic-base-addr", m_cfg->l2vic_base);
-        qdev_prop_set_uint32(DEVICE(cpu), "config-table-addr", m_cfg->cfgbase);
+        if (!object_property_set_link(OBJECT(cpu), "global-regs",
+                                      OBJECT(glob_regs_dev), errp)) {
+            error_report("Failed to link global system registers to CPU %d", i);
+            return;
+        }
         qdev_prop_set_uint32(DEVICE(cpu), "hvx-contexts",
                              m_cfg->cfgtable.ext_contexts);
         qdev_prop_set_uint32(DEVICE(cpu), "jtlb-entries",
diff --git a/hw/hexagon/hexagon_globalreg.c b/hw/hexagon/hexagon_globalreg.c
index fcbf2ae4b2..c94d2c4c7f 100644
--- a/hw/hexagon/hexagon_globalreg.c
+++ b/hw/hexagon/hexagon_globalreg.c
@@ -14,6 +14,7 @@
 #include "migration/vmstate.h"
 #include "qom/object.h"
 #include "target/hexagon/cpu.h"
+#include "hw/timer/qct-qtimer.h"
 #include "target/hexagon/hex_regs.h"
 #include "qemu/log.h"
 #include "trace/trace-hw_hexagon.h"
@@ -137,9 +138,11 @@ static inline uint32_t apply_write_mask(uint32_t new_val, uint32_t cur_val,
 
 static void read_timer(HexagonGlobalRegState *s, uint32_t *low, uint32_t *high)
 {
-    /* Not yet implemented */
-    *low = 0;
-    *high = 0;
+    const hwaddr low_addr  = s->qtimer_base_addr + QCT_QTIMER_CNTPCT_LO;
+    const hwaddr high_addr = s->qtimer_base_addr + QCT_QTIMER_CNTPCT_HI;
+
+    cpu_physical_memory_read(low_addr, low, sizeof(*low));
+    cpu_physical_memory_read(high_addr, high, sizeof(*high));
 }
 
 uint32_t hexagon_globalreg_read(HexagonCPU *cpu, uint32_t reg)
diff --git a/hw/hexagon/virt.c b/hw/hexagon/virt.c
index 615fde773d..c93d19ff89 100644
--- a/hw/hexagon/virt.c
+++ b/hw/hexagon/virt.c
@@ -17,6 +17,8 @@
 #include "hw/qdev-properties.h"
 #include "hw/qdev-clock.h"
 #include "hw/register.h"
+#include "hw/timer/qct-qtimer.h"
+#include "qapi/error.h"
 #include "qemu/error-report.h"
 #include "qemu/guest-random.h"
 #include "qemu/units.h"
@@ -256,6 +258,25 @@ static void fdt_add_virtio_devices(const HexagonVirtMachineState *vms)
     }
 }
 
+static void create_qtimer(HexagonVirtMachineState *vms,
+                          const hexagon_machine_config *m_cfg)
+{
+    Error **errp = NULL;
+    QCTQtimerState *qtimer = QCT_QTIMER(qdev_new(TYPE_QCT_QTIMER));
+
+    object_property_set_uint(OBJECT(qtimer), "nr_frames", 2, errp);
+    object_property_set_uint(OBJECT(qtimer), "nr_views", 1, errp);
+    object_property_set_uint(OBJECT(qtimer), "cnttid", 0x111, errp);
+    sysbus_realize_and_unref(SYS_BUS_DEVICE(qtimer), errp);
+
+
+    sysbus_mmio_map(SYS_BUS_DEVICE(qtimer), 1, m_cfg->qtmr_region);
+    sysbus_connect_irq(SYS_BUS_DEVICE(qtimer), 0,
+                       qdev_get_gpio_in(vms->l2vic, irqmap[VIRT_QTMR0]));
+    sysbus_connect_irq(SYS_BUS_DEVICE(qtimer), 1,
+                       qdev_get_gpio_in(vms->l2vic, irqmap[VIRT_QTMR1]));
+}
+
 static void virt_instance_init(Object *obj)
 {
     HexagonVirtMachineState *vms = HEXAGON_VIRT_MACHINE(obj);
@@ -395,6 +416,7 @@ static void virt_init(MachineState *ms)
     fdt_add_clocks(vms);
     fdt_add_uart(vms, VIRT_UART0);
     fdt_add_gpt_node(vms);
+    create_qtimer(vms, m_cfg);
 
     rom_add_blob_fixed_as("config_table.rom", &m_cfg->cfgtable,
                           sizeof(m_cfg->cfgtable), m_cfg->cfgbase,
diff --git a/hw/timer/qct-qtimer.c b/hw/timer/qct-qtimer.c
new file mode 100644
index 0000000000..bd7123264c
--- /dev/null
+++ b/hw/timer/qct-qtimer.c
@@ -0,0 +1,520 @@
+/*
+ * Qualcomm QCT QTimer
+ *
+ * Copyright(c) 2019-2025 Qualcomm Innovation Center, Inc. All Rights Reserved.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+
+#include "qemu/osdep.h"
+#include "hw/irq.h"
+#include "hw/qdev-properties.h"
+#include "hw/timer/qct-qtimer.h"
+#include "migration/vmstate.h"
+#include "qapi/error.h"
+#include "qemu/log.h"
+#include "qemu/module.h"
+#include "qemu/timer.h"
+
+/* Common timer implementation.  */
+
+#define QTIMER_MEM_SIZE_BYTES 0x1000
+#define QTIMER_MEM_REGION_SIZE_BYTES 0x1000
+#define QTIMER_DEFAULT_FREQ_HZ 19200000ULL
+#define QTMR_TIMER_INDEX_MASK (0xf000)
+#define HIGH_32(val) (0x0ffffffffULL & (val >> 32))
+#define LOW_32(val) (0x0ffffffffULL & val)
+
+/*
+ * QTimer version reg:
+ *
+ *    3                   2                   1
+ *  1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Major |         Minor         |           Step                |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+static unsigned int TIMER_VERSION = 0x20020000;
+
+/*
+ * qct_qtimer_read/write:
+ * if offset < 0x1000 read restricted registers:
+ * QCT_QTIMER_AC_CNTFREQ/CNTSR/CNTTID/CNTACR/CNTOFF_(LO/HI)/QCT_QTIMER_VERSION
+ */
+static uint64_t qct_qtimer_read(void *opaque, hwaddr offset, unsigned size)
+{
+    QCTQtimerState *s = (QCTQtimerState *)opaque;
+    uint32_t frame = 0;
+
+    switch (offset) {
+    case QCT_QTIMER_AC_CNTFRQ:
+        return s->freq;
+    case QCT_QTIMER_AC_CNTSR:
+        return s->secure;
+    case QCT_QTIMER_AC_CNTTID:
+        return s->cnttid;
+    case QCT_QTIMER_AC_CNTACR_START ... QCT_QTIMER_AC_CNTACR_END:
+        frame = (offset - 0x40) / 0x4;
+        if (frame >= s->nr_frames) {
+            qemu_log_mask(LOG_GUEST_ERROR,
+                          "%s: QCT_QTIMER_AC_CNT: Bad offset %x\n", __func__,
+                          (int)offset);
+            return 0x0;
+        }
+        return s->timer[frame].cnt_ctrl;
+    case QCT_QTIMER_VERSION:
+        return TIMER_VERSION;
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: QCT_QTIMER_AC_CNT: Bad offset %" PRIx32 "\n",
+                      __func__, (int)offset);
+        return 0x0;
+    }
+
+    qemu_log_mask(LOG_GUEST_ERROR, "%s: Bad offset 0x%" PRIx32 "\n", __func__,
+                  (int)offset);
+    return 0;
+}
+
+static void qct_qtimer_write(void *opaque, hwaddr offset, uint64_t value,
+                             unsigned size)
+{
+    QCTQtimerState *s = (QCTQtimerState *)opaque;
+    uint32_t frame = 0;
+
+    if (offset < 0x1000) {
+        switch (offset) {
+        case QCT_QTIMER_AC_CNTFRQ:
+            s->freq = value;
+            return;
+        case QCT_QTIMER_AC_CNTSR:
+            if (value > 0xFF)
+                qemu_log_mask(LOG_GUEST_ERROR,
+                              "%s: QCT_QTIMER_AC_CNTSR: Bad value %x\n",
+                              __func__, (int)value);
+            else
+                s->secure = value;
+            return;
+        case QCT_QTIMER_AC_CNTACR_START ... QCT_QTIMER_AC_CNTACR_END:
+            frame = (offset - QCT_QTIMER_AC_CNTACR_START) / 0x4;
+            if (frame >= s->nr_frames) {
+                qemu_log_mask(LOG_GUEST_ERROR,
+                              "%s: QCT_QTIMER_AC_CNT: Bad offset %x\n",
+                              __func__, (int)offset);
+                return;
+            }
+            s->timer[frame].cnt_ctrl = value;
+            return;
+        default:
+            qemu_log_mask(LOG_GUEST_ERROR,
+                          "%s: QCT_QTIMER_AC_CNT: Bad offset %x\n", __func__,
+                          (int)offset);
+            return;
+        }
+    } else
+        qemu_log_mask(LOG_GUEST_ERROR, "%s: Bad offset %" PRIx32 "\n", __func__,
+                      (int)offset);
+}
+
+static const MemoryRegionOps qct_qtimer_ops = {
+    .read = qct_qtimer_read,
+    .write = qct_qtimer_write,
+    .endianness = DEVICE_NATIVE_ENDIAN,
+};
+
+static const VMStateDescription vmstate_qct_qtimer = {
+    .name = "qct-qtimer",
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .fields = (VMStateField[]){ VMSTATE_END_OF_LIST() }
+};
+
+static void qct_qtimer_init(Object *obj)
+{
+    QCTQtimerState *s = QCT_QTIMER(obj);
+
+    object_property_add_uint32_ptr(obj, "secure", &s->secure,
+                                   OBJ_PROP_FLAG_READ);
+    object_property_add_uint32_ptr(obj, "frame_id", &s->frame_id,
+                                   OBJ_PROP_FLAG_READ);
+}
+
+static void hex_timer_update(QCTHextimerState *s)
+{
+    /* Update interrupts.  */
+    int level = s->int_level && (s->control & QCT_QTIMER_CNTP_CTL_ENABLE);
+    qemu_set_irq(s->irq, level);
+}
+
+static MemTxResult hex_timer_read(void *opaque, hwaddr offset, uint64_t *data,
+                                  unsigned size, MemTxAttrs attrs)
+{
+    QCTQtimerState *qct_s = (QCTQtimerState *)opaque;
+    uint32_t slot_nr = (offset & 0xF000) >> 12;
+    uint32_t reg_offset = offset & 0xFFF;
+    uint32_t view = slot_nr % qct_s->nr_views;
+    uint32_t frame = slot_nr / qct_s->nr_views;
+
+    if (frame >= qct_s->nr_frames) {
+        *data = 0;
+        return MEMTX_ACCESS_ERROR;
+    }
+    QCTHextimerState *s = &qct_s->timer[frame];
+
+
+    /*
+     * This is the case where we have 2 views, but the second one is not
+     * implemented.
+     */
+    if (view && !(qct_s->cnttid & (0x4 << (frame * 4)))) {
+        *data = 0;
+        return MEMTX_OK;
+    }
+
+    switch (reg_offset) {
+    case (QCT_QTIMER_CNT_FREQ): /* Ticks/Second */
+        if (!(s->cnt_ctrl & QCT_QTIMER_AC_CNTACR_RFRQ)) {
+            return MEMTX_ACCESS_ERROR;
+        }
+
+        if (view && !((s->cntpl0acr & QCT_QTIMER_CNTPL0ACR_PL0PCTEN) ||
+                      (s->cntpl0acr & QCT_QTIMER_CNTPL0ACR_PL0VCTEN))) {
+            return MEMTX_ACCESS_ERROR;
+        }
+
+        *data = s->freq;
+        return MEMTX_OK;
+    case (QCT_QTIMER_CNTP_CVAL_LO): /* TimerLoad */
+        if (!(s->cnt_ctrl & QCT_QTIMER_AC_CNTACR_RWPT)) {
+            return MEMTX_ACCESS_ERROR;
+        }
+
+        if (view && !(s->cntpl0acr & QCT_QTIMER_CNTPL0ACR_PL0CTEN)) {
+            return MEMTX_ACCESS_ERROR;
+        }
+
+        *data = LOW_32((s->cntval));
+        return MEMTX_OK;
+    case (QCT_QTIMER_CNTP_CVAL_HI): /* TimerLoad */
+        if (!(s->cnt_ctrl & QCT_QTIMER_AC_CNTACR_RWPT)) {
+            return MEMTX_ACCESS_ERROR;
+        }
+
+        if (view && !(s->cntpl0acr & QCT_QTIMER_CNTPL0ACR_PL0CTEN)) {
+            return MEMTX_ACCESS_ERROR;
+        }
+
+        *data = HIGH_32((s->cntval));
+        return MEMTX_OK;
+    case QCT_QTIMER_CNTPCT_LO:
+        if (!(s->cnt_ctrl & QCT_QTIMER_AC_CNTACR_RPCT)) {
+            return MEMTX_ACCESS_ERROR;
+        }
+
+        if (view && !(s->cntpl0acr & QCT_QTIMER_CNTPL0ACR_PL0PCTEN)) {
+            return MEMTX_ACCESS_ERROR;
+        }
+
+        *data = LOW_32((s->cntpct + (ptimer_get_count(s->timer))));
+        return MEMTX_OK;
+    case QCT_QTIMER_CNTPCT_HI:
+        if (!(s->cnt_ctrl & QCT_QTIMER_AC_CNTACR_RPCT)) {
+            return MEMTX_ACCESS_ERROR;
+        }
+
+        if (view && !(s->cntpl0acr & QCT_QTIMER_CNTPL0ACR_PL0PCTEN)) {
+            return MEMTX_ACCESS_ERROR;
+        }
+
+        *data = HIGH_32((s->cntpct + (ptimer_get_count(s->timer))));
+        return MEMTX_OK;
+    case (QCT_QTIMER_CNTP_TVAL): /* CVAL - CNTP */
+        if (!(s->cnt_ctrl & QCT_QTIMER_AC_CNTACR_RWPT)) {
+            return MEMTX_ACCESS_ERROR;
+        }
+
+        if (view && !(s->cntpl0acr & QCT_QTIMER_CNTPL0ACR_PL0CTEN)) {
+            return MEMTX_ACCESS_ERROR;
+        }
+
+        *data =
+            (s->cntval - (HIGH_32((s->cntpct + (ptimer_get_count(s->timer)))) +
+                          LOW_32((s->cntpct + (ptimer_get_count(s->timer))))));
+        return MEMTX_OK;
+    case (QCT_QTIMER_CNTP_CTL): /* TimerMIS */
+        if (!(s->cnt_ctrl & QCT_QTIMER_AC_CNTACR_RWPT)) {
+            return MEMTX_ACCESS_ERROR;
+        }
+
+        if (view && !(s->cntpl0acr & QCT_QTIMER_CNTPL0ACR_PL0CTEN)) {
+            return MEMTX_ACCESS_ERROR;
+        }
+
+        *data = s->int_level;
+        return MEMTX_OK;
+    case QCT_QTIMER_CNTPL0ACR:
+        if (view) {
+            *data = 0;
+        } else {
+            *data = s->cntpl0acr;
+        }
+        return MEMTX_OK;
+
+    case QCT_QTIMER_VERSION:
+        *data = TIMER_VERSION;
+        return MEMTX_OK;
+
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR, "%s: Bad offset %" PRIx32 "\n", __func__,
+                      (int)offset);
+        *data = 0;
+        return MEMTX_ACCESS_ERROR;
+    }
+}
+
+/*
+ * Reset the timer limit after settings have changed.
+ * May only be called from inside a ptimer transaction block.
+ */
+static void hex_timer_recalibrate(QCTHextimerState *s, int reload)
+{
+    uint64_t limit;
+    /* Periodic.  */
+    limit = s->limit;
+    ptimer_set_limit(s->timer, limit, reload);
+}
+
+static MemTxResult hex_timer_write(void *opaque, hwaddr offset, uint64_t value,
+                                   unsigned size, MemTxAttrs attrs)
+{
+    QCTQtimerState *qct_s = (QCTQtimerState *)opaque;
+    uint32_t slot_nr = (offset & 0xF000) >> 12;
+    uint32_t reg_offset = offset & 0xFFF;
+    uint32_t view = slot_nr % qct_s->nr_views;
+    uint32_t frame = slot_nr / qct_s->nr_views;
+
+    if (frame >= qct_s->nr_frames) {
+        return MEMTX_ACCESS_ERROR;
+    }
+    QCTHextimerState *s = &qct_s->timer[frame];
+
+    /*
+     * This is the case where we have 2 views, but the second one is not
+     * implemented.
+     */
+    if (view && !(qct_s->cnttid & (0x4 << (frame * 4)))) {
+        return MEMTX_OK;
+    }
+
+    switch (reg_offset) {
+    case (QCT_QTIMER_CNTP_CVAL_LO): /* TimerLoad */
+        if (!(s->cnt_ctrl & QCT_QTIMER_AC_CNTACR_RWPT)) {
+            return MEMTX_ACCESS_ERROR;
+        }
+
+        if (view && !(s->cntpl0acr & QCT_QTIMER_CNTPL0ACR_PL0CTEN)) {
+            return MEMTX_ACCESS_ERROR;
+        }
+
+
+        s->int_level = 0;
+        s->cntval = value;
+        ptimer_transaction_begin(s->timer);
+        if (s->control & QCT_QTIMER_CNTP_CTL_ENABLE) {
+            /*
+             * Pause the timer if it is running.  This may cause some
+             * inaccuracy due to rounding, but avoids other issues.
+             */
+            ptimer_stop(s->timer);
+        }
+        hex_timer_recalibrate(s, 1);
+        if (s->control & QCT_QTIMER_CNTP_CTL_ENABLE) {
+            ptimer_run(s->timer, 0);
+        }
+        ptimer_transaction_commit(s->timer);
+        break;
+    case (QCT_QTIMER_CNTP_CVAL_HI):
+        if (!(s->cnt_ctrl & QCT_QTIMER_AC_CNTACR_RWPT)) {
+            return MEMTX_ACCESS_ERROR;
+        }
+
+        if (view && !(s->cntpl0acr & QCT_QTIMER_CNTPL0ACR_PL0CTEN)) {
+            return MEMTX_ACCESS_ERROR;
+        }
+
+        break;
+    case (QCT_QTIMER_CNTP_CTL): /* Timer control register */
+        if (!(s->cnt_ctrl & QCT_QTIMER_AC_CNTACR_RWPT)) {
+            return MEMTX_ACCESS_ERROR;
+        }
+
+        if (view && !(s->cntpl0acr & QCT_QTIMER_CNTPL0ACR_PL0CTEN)) {
+            return MEMTX_ACCESS_ERROR;
+        }
+
+        ptimer_transaction_begin(s->timer);
+        if (s->control & QCT_QTIMER_CNTP_CTL_ENABLE) {
+            /*
+             * Pause the timer if it is running.  This may cause some
+             * inaccuracy due to rounding, but avoids other issues.
+             */
+            ptimer_stop(s->timer);
+        }
+        s->control = value;
+        hex_timer_recalibrate(s, s->control & QCT_QTIMER_CNTP_CTL_ENABLE);
+        ptimer_set_freq(s->timer, s->freq);
+        ptimer_set_period(s->timer, 1);
+        if (s->control & QCT_QTIMER_CNTP_CTL_ENABLE) {
+            ptimer_run(s->timer, 0);
+        }
+        ptimer_transaction_commit(s->timer);
+        break;
+    case (QCT_QTIMER_CNTP_TVAL): /* CVAL - CNTP */
+        if (!(s->cnt_ctrl & QCT_QTIMER_AC_CNTACR_RWPT)) {
+            return MEMTX_ACCESS_ERROR;
+        }
+
+        if (view && !(s->cntpl0acr & QCT_QTIMER_CNTPL0ACR_PL0CTEN)) {
+            return MEMTX_ACCESS_ERROR;
+        }
+
+        ptimer_transaction_begin(s->timer);
+        if (s->control & QCT_QTIMER_CNTP_CTL_ENABLE) {
+            /*
+             * Pause the timer if it is running.  This may cause some
+             * inaccuracy due to rounding, but avoids other issues.
+             */
+            ptimer_stop(s->timer);
+        }
+        s->cntval = s->cntpct + value;
+        ptimer_set_freq(s->timer, s->freq);
+        ptimer_set_period(s->timer, 1);
+        if (s->control & QCT_QTIMER_CNTP_CTL_ENABLE) {
+            ptimer_run(s->timer, 0);
+        }
+        ptimer_transaction_commit(s->timer);
+        break;
+    case QCT_QTIMER_CNTPL0ACR:
+        if (view) {
+            break;
+        }
+
+        s->cntpl0acr = value;
+        break;
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR, "%s: Bad offset %" PRIx32 "\n", __func__,
+                      (int)offset);
+        return MEMTX_ACCESS_ERROR;
+    }
+    hex_timer_update(s);
+    return MEMTX_OK;
+}
+
+static void hex_timer_tick(void *opaque)
+{
+    QCTHextimerState *s = (QCTHextimerState *)opaque;
+    if ((s->cntpct >= s->cntval) && (s->int_level != 1)) {
+        s->int_level = 1;
+        hex_timer_update(s);
+        return;
+    }
+    s->cntpct += s->limit;
+}
+
+static const MemoryRegionOps hex_timer_ops = {
+    .read_with_attrs = hex_timer_read,
+    .write_with_attrs = hex_timer_write,
+    .endianness = DEVICE_NATIVE_ENDIAN,
+};
+
+static const VMStateDescription vmstate_hex_timer = {
+    .name = "hex_timer",
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .fields = (VMStateField[]){ VMSTATE_UINT32(control, QCTHextimerState),
+                                VMSTATE_UINT32(cnt_ctrl, QCTHextimerState),
+                                VMSTATE_UINT64(cntpct, QCTHextimerState),
+                                VMSTATE_UINT64(cntval, QCTHextimerState),
+                                VMSTATE_UINT64(limit, QCTHextimerState),
+                                VMSTATE_UINT32(int_level, QCTHextimerState),
+                                VMSTATE_PTIMER(timer, QCTHextimerState),
+                                VMSTATE_END_OF_LIST() }
+};
+
+static void qct_qtimer_realize(DeviceState *dev, Error **errp)
+{
+    SysBusDevice *sbd = SYS_BUS_DEVICE(dev);
+    QCTQtimerState *s = QCT_QTIMER(dev);
+    unsigned int i;
+
+    if (s->nr_frames > QCT_QTIMER_TIMER_FRAME_ELTS) {
+        error_setg(errp, "nr_frames too high");
+        return;
+    }
+
+    if (s->nr_views > QCT_QTIMER_TIMER_VIEW_ELTS) {
+        error_setg(errp, "nr_views too high");
+        return;
+    }
+
+    memory_region_init_io(&s->iomem, OBJECT(sbd), &qct_qtimer_ops, s, "qutimer",
+                          QTIMER_MEM_SIZE_BYTES);
+    sysbus_init_mmio(sbd, &s->iomem);
+
+    memory_region_init_io(&s->view_iomem, OBJECT(sbd), &hex_timer_ops, s,
+                          "qutimer_views",
+                          QTIMER_MEM_SIZE_BYTES * s->nr_frames * s->nr_views);
+    sysbus_init_mmio(sbd, &s->view_iomem);
+
+    for (i = 0; i < s->nr_frames; i++) {
+        s->timer[i].limit = 1;
+        s->timer[i].control = QCT_QTIMER_CNTP_CTL_ENABLE;
+        s->timer[i].cnt_ctrl =
+            (QCT_QTIMER_AC_CNTACR_RWPT | QCT_QTIMER_AC_CNTACR_RWVT |
+             QCT_QTIMER_AC_CNTACR_RVOFF | QCT_QTIMER_AC_CNTACR_RFRQ |
+             QCT_QTIMER_AC_CNTACR_RPVCT | QCT_QTIMER_AC_CNTACR_RPCT);
+        s->timer[i].qtimer = s;
+        s->timer[i].freq = QTIMER_DEFAULT_FREQ_HZ;
+
+        s->secure |= (1 << i);
+
+        sysbus_init_irq(sbd, &(s->timer[i].irq));
+
+        (s->timer[i]).timer =
+            ptimer_init(hex_timer_tick, &s->timer[i], PTIMER_POLICY_LEGACY);
+        vmstate_register(NULL, VMSTATE_INSTANCE_ID_ANY, &vmstate_hex_timer,
+                         &s->timer[i]);
+    }
+}
+
+static const Property qct_qtimer_properties[] = {
+    DEFINE_PROP_UINT32("freq", QCTQtimerState, freq, QTIMER_DEFAULT_FREQ_HZ),
+    DEFINE_PROP_UINT32("nr_frames", QCTQtimerState, nr_frames, 2),
+    DEFINE_PROP_UINT32("nr_views", QCTQtimerState, nr_views, 1),
+    DEFINE_PROP_UINT32("cnttid", QCTQtimerState, cnttid, 0x11),
+};
+
+static void qct_qtimer_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *k = DEVICE_CLASS(klass);
+
+    device_class_set_props(k, qct_qtimer_properties);
+    k->realize = qct_qtimer_realize;
+    k->vmsd = &vmstate_qct_qtimer;
+}
+
+static const TypeInfo qct_qtimer_info = {
+    .name = TYPE_QCT_QTIMER,
+    .parent = TYPE_SYS_BUS_DEVICE,
+    .instance_size = sizeof(QCTQtimerState),
+    .instance_init = qct_qtimer_init,
+    .class_init = qct_qtimer_class_init,
+};
+
+static void qct_qtimer_register_types(void)
+{
+    type_register_static(&qct_qtimer_info);
+}
+
+type_init(qct_qtimer_register_types)
diff --git a/hw/timer/meson.build b/hw/timer/meson.build
index 178321c029..69468672bc 100644
--- a/hw/timer/meson.build
+++ b/hw/timer/meson.build
@@ -34,3 +34,5 @@ specific_ss.add(when: 'CONFIG_IBEX', if_true: files('ibex_timer.c'))
 system_ss.add(when: 'CONFIG_SIFIVE_PWM', if_true: files('sifive_pwm.c'))
 
 specific_ss.add(when: 'CONFIG_AVR_TIMER16', if_true: files('avr_timer16.c'))
+
+specific_ss.add(when: 'CONFIG_HEX_DSP', if_true: files('qct-qtimer.c'))
-- 
2.34.1