/* * Nuvoton NPCM7xx MFT Module * * Copyright 2021 Google LLC * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. */ #include "qemu/osdep.h" #include "hw/irq.h" #include "hw/qdev-clock.h" #include "hw/qdev-properties.h" #include "hw/misc/npcm7xx_mft.h" #include "hw/misc/npcm7xx_pwm.h" #include "hw/registerfields.h" #include "migration/vmstate.h" #include "qapi/error.h" #include "qapi/visitor.h" #include "qemu/bitops.h" #include "qemu/error-report.h" #include "qemu/log.h" #include "qemu/module.h" #include "qemu/timer.h" #include "qemu/units.h" #include "trace.h" /* * Some of the registers can only accessed via 16-bit ops and some can only * be accessed via 8-bit ops. However we mark all of them using REG16 to * simplify implementation. npcm7xx_mft_check_mem_op checks the access length * of memory operations. */ REG16(NPCM7XX_MFT_CNT1, 0x00); REG16(NPCM7XX_MFT_CRA, 0x02); REG16(NPCM7XX_MFT_CRB, 0x04); REG16(NPCM7XX_MFT_CNT2, 0x06); REG16(NPCM7XX_MFT_PRSC, 0x08); REG16(NPCM7XX_MFT_CKC, 0x0a); REG16(NPCM7XX_MFT_MCTRL, 0x0c); REG16(NPCM7XX_MFT_ICTRL, 0x0e); REG16(NPCM7XX_MFT_ICLR, 0x10); REG16(NPCM7XX_MFT_IEN, 0x12); REG16(NPCM7XX_MFT_CPA, 0x14); REG16(NPCM7XX_MFT_CPB, 0x16); REG16(NPCM7XX_MFT_CPCFG, 0x18); REG16(NPCM7XX_MFT_INASEL, 0x1a); REG16(NPCM7XX_MFT_INBSEL, 0x1c); /* Register Fields */ #define NPCM7XX_MFT_CKC_C2CSEL BIT(3) #define NPCM7XX_MFT_CKC_C1CSEL BIT(0) #define NPCM7XX_MFT_MCTRL_TBEN BIT(6) #define NPCM7XX_MFT_MCTRL_TAEN BIT(5) #define NPCM7XX_MFT_MCTRL_TBEDG BIT(4) #define NPCM7XX_MFT_MCTRL_TAEDG BIT(3) #define NPCM7XX_MFT_MCTRL_MODE5 BIT(2) #define NPCM7XX_MFT_ICTRL_TFPND BIT(5) #define NPCM7XX_MFT_ICTRL_TEPND BIT(4) #define NPCM7XX_MFT_ICTRL_TDPND BIT(3) #define NPCM7XX_MFT_ICTRL_TCPND BIT(2) #define NPCM7XX_MFT_ICTRL_TBPND BIT(1) #define NPCM7XX_MFT_ICTRL_TAPND BIT(0) #define NPCM7XX_MFT_ICLR_TFCLR BIT(5) #define NPCM7XX_MFT_ICLR_TECLR BIT(4) #define NPCM7XX_MFT_ICLR_TDCLR BIT(3) #define NPCM7XX_MFT_ICLR_TCCLR BIT(2) #define NPCM7XX_MFT_ICLR_TBCLR BIT(1) #define NPCM7XX_MFT_ICLR_TACLR BIT(0) #define NPCM7XX_MFT_IEN_TFIEN BIT(5) #define NPCM7XX_MFT_IEN_TEIEN BIT(4) #define NPCM7XX_MFT_IEN_TDIEN BIT(3) #define NPCM7XX_MFT_IEN_TCIEN BIT(2) #define NPCM7XX_MFT_IEN_TBIEN BIT(1) #define NPCM7XX_MFT_IEN_TAIEN BIT(0) #define NPCM7XX_MFT_CPCFG_GET_B(rv) extract8((rv), 4, 4) #define NPCM7XX_MFT_CPCFG_GET_A(rv) extract8((rv), 0, 4) #define NPCM7XX_MFT_CPCFG_HIEN BIT(3) #define NPCM7XX_MFT_CPCFG_EQEN BIT(2) #define NPCM7XX_MFT_CPCFG_LOEN BIT(1) #define NPCM7XX_MFT_CPCFG_CPSEL BIT(0) #define NPCM7XX_MFT_INASEL_SELA BIT(0) #define NPCM7XX_MFT_INBSEL_SELB BIT(0) /* Max CNT values of the module. The CNT value is a countdown from it. */ #define NPCM7XX_MFT_MAX_CNT 0xFFFF /* Each fan revolution should generated 2 pulses */ #define NPCM7XX_MFT_PULSE_PER_REVOLUTION 2 typedef enum NPCM7xxMFTCaptureState { /* capture succeeded with a valid CNT value. */ NPCM7XX_CAPTURE_SUCCEED, /* capture stopped prematurely due to reaching CPCFG condition. */ NPCM7XX_CAPTURE_COMPARE_HIT, /* capture fails since it reaches underflow condition for CNT. */ NPCM7XX_CAPTURE_UNDERFLOW, } NPCM7xxMFTCaptureState; static void npcm7xx_mft_reset(NPCM7xxMFTState *s) { int i; /* Only registers PRSC ~ INBSEL need to be reset. */ for (i = R_NPCM7XX_MFT_PRSC; i <= R_NPCM7XX_MFT_INBSEL; ++i) { s->regs[i] = 0; } } static void npcm7xx_mft_clear_interrupt(NPCM7xxMFTState *s, uint8_t iclr) { /* * Clear bits in ICTRL where corresponding bits in iclr is 1. * Both iclr and ictrl are 8-bit regs. (See npcm7xx_mft_check_mem_op) */ s->regs[R_NPCM7XX_MFT_ICTRL] &= ~iclr; } /* * If the CPCFG's condition should be triggered during count down from * NPCM7XX_MFT_MAX_CNT to src if compared to tgt, return the count when * the condition is triggered. * Otherwise return -1. * Since tgt is uint16_t it must always <= NPCM7XX_MFT_MAX_CNT. */ static int npcm7xx_mft_compare(int32_t src, uint16_t tgt, uint8_t cpcfg) { if (cpcfg & NPCM7XX_MFT_CPCFG_HIEN) { return NPCM7XX_MFT_MAX_CNT; } if ((cpcfg & NPCM7XX_MFT_CPCFG_EQEN) && (src <= tgt)) { return tgt; } if ((cpcfg & NPCM7XX_MFT_CPCFG_LOEN) && (tgt > 0) && (src < tgt)) { return tgt - 1; } return -1; } /* Compute CNT according to corresponding fan's RPM. */ static NPCM7xxMFTCaptureState npcm7xx_mft_compute_cnt( Clock *clock, uint32_t max_rpm, uint32_t duty, uint16_t tgt, uint8_t cpcfg, uint16_t *cnt) { uint32_t rpm = (uint64_t)max_rpm * (uint64_t)duty / NPCM7XX_PWM_MAX_DUTY; int32_t count; int stopped; NPCM7xxMFTCaptureState state; if (rpm == 0) { /* * If RPM = 0, capture won't happen. CNT will continue count down. * So it's effective equivalent to have a cnt > NPCM7XX_MFT_MAX_CNT */ count = NPCM7XX_MFT_MAX_CNT + 1; } else { /* * RPM = revolution/min. The time for one revlution (in ns) is * MINUTE_TO_NANOSECOND / RPM. */ count = clock_ns_to_ticks(clock, (60 * NANOSECONDS_PER_SECOND) / (rpm * NPCM7XX_MFT_PULSE_PER_REVOLUTION)); } if (count > NPCM7XX_MFT_MAX_CNT) { count = -1; } else { /* The CNT is a countdown value from NPCM7XX_MFT_MAX_CNT. */ count = NPCM7XX_MFT_MAX_CNT - count; } stopped = npcm7xx_mft_compare(count, tgt, cpcfg); if (stopped == -1) { if (count == -1) { /* Underflow */ state = NPCM7XX_CAPTURE_UNDERFLOW; } else { state = NPCM7XX_CAPTURE_SUCCEED; } } else { count = stopped; state = NPCM7XX_CAPTURE_COMPARE_HIT; } if (count != -1) { *cnt = count; } trace_npcm7xx_mft_rpm(clock->canonical_path, clock_get_hz(clock), state, count, rpm, duty); return state; } /* * Capture Fan RPM and update CNT and CR registers accordingly. * Raise IRQ if certain contidions are met in IEN. */ static void npcm7xx_mft_capture(NPCM7xxMFTState *s) { int irq_level = 0; NPCM7xxMFTCaptureState state; int sel; uint8_t cpcfg; /* * If not mode 5, the behavior is undefined. We just do nothing in this * case. */ if (!(s->regs[R_NPCM7XX_MFT_MCTRL] & NPCM7XX_MFT_MCTRL_MODE5)) { return; } /* Capture input A. */ if (s->regs[R_NPCM7XX_MFT_MCTRL] & NPCM7XX_MFT_MCTRL_TAEN && s->regs[R_NPCM7XX_MFT_CKC] & NPCM7XX_MFT_CKC_C1CSEL) { sel = s->regs[R_NPCM7XX_MFT_INASEL] & NPCM7XX_MFT_INASEL_SELA; cpcfg = NPCM7XX_MFT_CPCFG_GET_A(s->regs[R_NPCM7XX_MFT_CPCFG]); state = npcm7xx_mft_compute_cnt(s->clock_1, sel ? s->max_rpm[2] : s->max_rpm[0], sel ? s->duty[2] : s->duty[0], s->regs[R_NPCM7XX_MFT_CPA], cpcfg, &s->regs[R_NPCM7XX_MFT_CNT1]); switch (state) { case NPCM7XX_CAPTURE_SUCCEED: /* Interrupt on input capture on TAn transition - TAPND */ s->regs[R_NPCM7XX_MFT_CRA] = s->regs[R_NPCM7XX_MFT_CNT1]; s->regs[R_NPCM7XX_MFT_ICTRL] |= NPCM7XX_MFT_ICTRL_TAPND; if (s->regs[R_NPCM7XX_MFT_IEN] & NPCM7XX_MFT_IEN_TAIEN) { irq_level = 1; } break; case NPCM7XX_CAPTURE_COMPARE_HIT: /* Compare Hit - TEPND */ s->regs[R_NPCM7XX_MFT_ICTRL] |= NPCM7XX_MFT_ICTRL_TEPND; if (s->regs[R_NPCM7XX_MFT_IEN] & NPCM7XX_MFT_IEN_TEIEN) { irq_level = 1; } break; case NPCM7XX_CAPTURE_UNDERFLOW: /* Underflow - TCPND */ s->regs[R_NPCM7XX_MFT_ICTRL] |= NPCM7XX_MFT_ICTRL_TCPND; if (s->regs[R_NPCM7XX_MFT_IEN] & NPCM7XX_MFT_IEN_TCIEN) { irq_level = 1; } break; default: g_assert_not_reached(); } } /* Capture input B. */ if (s->regs[R_NPCM7XX_MFT_MCTRL] & NPCM7XX_MFT_MCTRL_TBEN && s->regs[R_NPCM7XX_MFT_CKC] & NPCM7XX_MFT_CKC_C2CSEL) { sel = s->regs[R_NPCM7XX_MFT_INBSEL] & NPCM7XX_MFT_INBSEL_SELB; cpcfg = NPCM7XX_MFT_CPCFG_GET_B(s->regs[R_NPCM7XX_MFT_CPCFG]); state = npcm7xx_mft_compute_cnt(s->clock_2, sel ? s->max_rpm[3] : s->max_rpm[1], sel ? s->duty[3] : s->duty[1], s->regs[R_NPCM7XX_MFT_CPB], cpcfg, &s->regs[R_NPCM7XX_MFT_CNT2]); switch (state) { case NPCM7XX_CAPTURE_SUCCEED: /* Interrupt on input capture on TBn transition - TBPND */ s->regs[R_NPCM7XX_MFT_CRB] = s->regs[R_NPCM7XX_MFT_CNT2]; s->regs[R_NPCM7XX_MFT_ICTRL] |= NPCM7XX_MFT_ICTRL_TBPND; if (s->regs[R_NPCM7XX_MFT_IEN] & NPCM7XX_MFT_IEN_TBIEN) { irq_level = 1; } break; case NPCM7XX_CAPTURE_COMPARE_HIT: /* Compare Hit - TFPND */ s->regs[R_NPCM7XX_MFT_ICTRL] |= NPCM7XX_MFT_ICTRL_TFPND; if (s->regs[R_NPCM7XX_MFT_IEN] & NPCM7XX_MFT_IEN_TFIEN) { irq_level = 1; } break; case NPCM7XX_CAPTURE_UNDERFLOW: /* Underflow - TDPND */ s->regs[R_NPCM7XX_MFT_ICTRL] |= NPCM7XX_MFT_ICTRL_TDPND; if (s->regs[R_NPCM7XX_MFT_IEN] & NPCM7XX_MFT_IEN_TDIEN) { irq_level = 1; } break; default: g_assert_not_reached(); } } trace_npcm7xx_mft_capture(DEVICE(s)->canonical_path, irq_level); qemu_set_irq(s->irq, irq_level); } /* Update clock for counters. */ static void npcm7xx_mft_update_clock(void *opaque, ClockEvent event) { NPCM7xxMFTState *s = NPCM7XX_MFT(opaque); uint64_t prescaled_clock_period; prescaled_clock_period = clock_get(s->clock_in) * (s->regs[R_NPCM7XX_MFT_PRSC] + 1ULL); trace_npcm7xx_mft_update_clock(s->clock_in->canonical_path, s->regs[R_NPCM7XX_MFT_CKC], clock_get(s->clock_in), prescaled_clock_period); /* Update clock 1 */ if (s->regs[R_NPCM7XX_MFT_CKC] & NPCM7XX_MFT_CKC_C1CSEL) { /* Clock is prescaled. */ clock_update(s->clock_1, prescaled_clock_period); } else { /* Clock stopped. */ clock_update(s->clock_1, 0); } /* Update clock 2 */ if (s->regs[R_NPCM7XX_MFT_CKC] & NPCM7XX_MFT_CKC_C2CSEL) { /* Clock is prescaled. */ clock_update(s->clock_2, prescaled_clock_period); } else { /* Clock stopped. */ clock_update(s->clock_2, 0); } npcm7xx_mft_capture(s); } static uint64_t npcm7xx_mft_read(void *opaque, hwaddr offset, unsigned size) { NPCM7xxMFTState *s = NPCM7XX_MFT(opaque); uint16_t value = 0; switch (offset) { case A_NPCM7XX_MFT_ICLR: qemu_log_mask(LOG_GUEST_ERROR, "%s: register @ 0x%04" HWADDR_PRIx " is write-only\n", __func__, offset); break; default: value = s->regs[offset / 2]; } trace_npcm7xx_mft_read(DEVICE(s)->canonical_path, offset, value); return value; } static void npcm7xx_mft_write(void *opaque, hwaddr offset, uint64_t v, unsigned size) { NPCM7xxMFTState *s = NPCM7XX_MFT(opaque); trace_npcm7xx_mft_write(DEVICE(s)->canonical_path, offset, v); switch (offset) { case A_NPCM7XX_MFT_ICLR: npcm7xx_mft_clear_interrupt(s, v); break; case A_NPCM7XX_MFT_CKC: case A_NPCM7XX_MFT_PRSC: s->regs[offset / 2] = v; npcm7xx_mft_update_clock(s, ClockUpdate); break; default: s->regs[offset / 2] = v; npcm7xx_mft_capture(s); break; } } static bool npcm7xx_mft_check_mem_op(void *opaque, hwaddr offset, unsigned size, bool is_write, MemTxAttrs attrs) { switch (offset) { /* 16-bit registers. Must be accessed with 16-bit read/write.*/ case A_NPCM7XX_MFT_CNT1: case A_NPCM7XX_MFT_CRA: case A_NPCM7XX_MFT_CRB: case A_NPCM7XX_MFT_CNT2: case A_NPCM7XX_MFT_CPA: case A_NPCM7XX_MFT_CPB: return size == 2; /* 8-bit registers. Must be accessed with 8-bit read/write.*/ case A_NPCM7XX_MFT_PRSC: case A_NPCM7XX_MFT_CKC: case A_NPCM7XX_MFT_MCTRL: case A_NPCM7XX_MFT_ICTRL: case A_NPCM7XX_MFT_ICLR: case A_NPCM7XX_MFT_IEN: case A_NPCM7XX_MFT_CPCFG: case A_NPCM7XX_MFT_INASEL: case A_NPCM7XX_MFT_INBSEL: return size == 1; default: /* Invalid registers. */ return false; } } static void npcm7xx_mft_get_max_rpm(Object *obj, Visitor *v, const char *name, void *opaque, Error **errp) { visit_type_uint32(v, name, (uint32_t *)opaque, errp); } static void npcm7xx_mft_set_max_rpm(Object *obj, Visitor *v, const char *name, void *opaque, Error **errp) { NPCM7xxMFTState *s = NPCM7XX_MFT(obj); uint32_t *max_rpm = opaque; uint32_t value; if (!visit_type_uint32(v, name, &value, errp)) { return; } *max_rpm = value; npcm7xx_mft_capture(s); } static void npcm7xx_mft_duty_handler(void *opaque, int n, int value) { NPCM7xxMFTState *s = NPCM7XX_MFT(opaque); trace_npcm7xx_mft_set_duty(DEVICE(s)->canonical_path, n, value); s->duty[n] = value; npcm7xx_mft_capture(s); } static const struct MemoryRegionOps npcm7xx_mft_ops = { .read = npcm7xx_mft_read, .write = npcm7xx_mft_write, .endianness = DEVICE_LITTLE_ENDIAN, .valid = { .min_access_size = 1, .max_access_size = 2, .unaligned = false, .accepts = npcm7xx_mft_check_mem_op, }, }; static void npcm7xx_mft_enter_reset(Object *obj, ResetType type) { NPCM7xxMFTState *s = NPCM7XX_MFT(obj); npcm7xx_mft_reset(s); } static void npcm7xx_mft_hold_reset(Object *obj) { NPCM7xxMFTState *s = NPCM7XX_MFT(obj); qemu_irq_lower(s->irq); } static void npcm7xx_mft_init(Object *obj) { NPCM7xxMFTState *s = NPCM7XX_MFT(obj); SysBusDevice *sbd = SYS_BUS_DEVICE(obj); DeviceState *dev = DEVICE(obj); memory_region_init_io(&s->iomem, obj, &npcm7xx_mft_ops, s, TYPE_NPCM7XX_MFT, 4 * KiB); sysbus_init_mmio(sbd, &s->iomem); sysbus_init_irq(sbd, &s->irq); s->clock_in = qdev_init_clock_in(dev, "clock-in", npcm7xx_mft_update_clock, s, ClockUpdate); s->clock_1 = qdev_init_clock_out(dev, "clock1"); s->clock_2 = qdev_init_clock_out(dev, "clock2"); for (int i = 0; i < NPCM7XX_PWM_PER_MODULE; ++i) { object_property_add(obj, "max_rpm[*]", "uint32", npcm7xx_mft_get_max_rpm, npcm7xx_mft_set_max_rpm, NULL, &s->max_rpm[i]); } qdev_init_gpio_in_named(dev, npcm7xx_mft_duty_handler, "duty", NPCM7XX_MFT_FANIN_COUNT); } static const VMStateDescription vmstate_npcm7xx_mft = { .name = "npcm7xx-mft-module", .version_id = 0, .minimum_version_id = 0, .fields = (const VMStateField[]) { VMSTATE_CLOCK(clock_in, NPCM7xxMFTState), VMSTATE_CLOCK(clock_1, NPCM7xxMFTState), VMSTATE_CLOCK(clock_2, NPCM7xxMFTState), VMSTATE_UINT16_ARRAY(regs, NPCM7xxMFTState, NPCM7XX_MFT_NR_REGS), VMSTATE_UINT32_ARRAY(max_rpm, NPCM7xxMFTState, NPCM7XX_MFT_FANIN_COUNT), VMSTATE_UINT32_ARRAY(duty, NPCM7xxMFTState, NPCM7XX_MFT_FANIN_COUNT), VMSTATE_END_OF_LIST(), }, }; static void npcm7xx_mft_class_init(ObjectClass *klass, void *data) { ResettableClass *rc = RESETTABLE_CLASS(klass); DeviceClass *dc = DEVICE_CLASS(klass); dc->desc = "NPCM7xx MFT Controller"; dc->vmsd = &vmstate_npcm7xx_mft; rc->phases.enter = npcm7xx_mft_enter_reset; rc->phases.hold = npcm7xx_mft_hold_reset; } static const TypeInfo npcm7xx_mft_info = { .name = TYPE_NPCM7XX_MFT, .parent = TYPE_SYS_BUS_DEVICE, .instance_size = sizeof(NPCM7xxMFTState), .class_init = npcm7xx_mft_class_init, .instance_init = npcm7xx_mft_init, }; static void npcm7xx_mft_register_type(void) { type_register_static(&npcm7xx_mft_info); } type_init(npcm7xx_mft_register_type);