|  | /* | 
|  | * SiFive PWM | 
|  | * | 
|  | * Copyright (c) 2020 Western Digital | 
|  | * | 
|  | * Author:  Alistair Francis <alistair.francis@wdc.com> | 
|  | * | 
|  | * Permission is hereby granted, free of charge, to any person obtaining a copy | 
|  | * of this software and associated documentation files (the "Software"), to deal | 
|  | * in the Software without restriction, including without limitation the rights | 
|  | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | 
|  | * copies of the Software, and to permit persons to whom the Software is | 
|  | * furnished to do so, subject to the following conditions: | 
|  | * | 
|  | * The above copyright notice and this permission notice shall be included in | 
|  | * all copies or substantial portions of the Software. | 
|  | * | 
|  | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | 
|  | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | 
|  | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | 
|  | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | 
|  | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | 
|  | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | 
|  | * THE SOFTWARE. | 
|  | */ | 
|  |  | 
|  | #include "qemu/osdep.h" | 
|  | #include "trace.h" | 
|  | #include "hw/irq.h" | 
|  | #include "hw/timer/sifive_pwm.h" | 
|  | #include "hw/qdev-properties.h" | 
|  | #include "hw/registerfields.h" | 
|  | #include "migration/vmstate.h" | 
|  | #include "qemu/log.h" | 
|  | #include "qemu/module.h" | 
|  |  | 
|  | #define HAS_PWM_EN_BITS(cfg) ((cfg & R_CONFIG_ENONESHOT_MASK) || \ | 
|  | (cfg & R_CONFIG_ENALWAYS_MASK)) | 
|  |  | 
|  | #define PWMCMP_MASK 0xFFFF | 
|  | #define PWMCOUNT_MASK 0x7FFFFFFF | 
|  |  | 
|  | REG32(CONFIG,                   0x00) | 
|  | FIELD(CONFIG, SCALE,            0, 4) | 
|  | FIELD(CONFIG, STICKY,           8, 1) | 
|  | FIELD(CONFIG, ZEROCMP,          9, 1) | 
|  | FIELD(CONFIG, DEGLITCH,         10, 1) | 
|  | FIELD(CONFIG, ENALWAYS,         12, 1) | 
|  | FIELD(CONFIG, ENONESHOT,        13, 1) | 
|  | FIELD(CONFIG, CMP0CENTER,       16, 1) | 
|  | FIELD(CONFIG, CMP1CENTER,       17, 1) | 
|  | FIELD(CONFIG, CMP2CENTER,       18, 1) | 
|  | FIELD(CONFIG, CMP3CENTER,       19, 1) | 
|  | FIELD(CONFIG, CMP0GANG,         24, 1) | 
|  | FIELD(CONFIG, CMP1GANG,         25, 1) | 
|  | FIELD(CONFIG, CMP2GANG,         26, 1) | 
|  | FIELD(CONFIG, CMP3GANG,         27, 1) | 
|  | FIELD(CONFIG, CMP0IP,           28, 1) | 
|  | FIELD(CONFIG, CMP1IP,           29, 1) | 
|  | FIELD(CONFIG, CMP2IP,           30, 1) | 
|  | FIELD(CONFIG, CMP3IP,           31, 1) | 
|  | REG32(COUNT,                    0x08) | 
|  | REG32(PWMS,                     0x10) | 
|  | REG32(PWMCMP0,                  0x20) | 
|  | REG32(PWMCMP1,                  0x24) | 
|  | REG32(PWMCMP2,                  0x28) | 
|  | REG32(PWMCMP3,                  0x2C) | 
|  |  | 
|  | static inline uint64_t sifive_pwm_ns_to_ticks(SiFivePwmState *s, | 
|  | uint64_t time) | 
|  | { | 
|  | return muldiv64(time, s->freq_hz, NANOSECONDS_PER_SECOND); | 
|  | } | 
|  |  | 
|  | static inline uint64_t sifive_pwm_ticks_to_ns(SiFivePwmState *s, | 
|  | uint64_t ticks) | 
|  | { | 
|  | return muldiv64(ticks, NANOSECONDS_PER_SECOND, s->freq_hz); | 
|  | } | 
|  |  | 
|  | static inline uint64_t sifive_pwm_compute_scale(SiFivePwmState *s) | 
|  | { | 
|  | return s->pwmcfg & R_CONFIG_SCALE_MASK; | 
|  | } | 
|  |  | 
|  | static void sifive_pwm_set_alarms(SiFivePwmState *s) | 
|  | { | 
|  | uint64_t now_ns = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL); | 
|  |  | 
|  | if (HAS_PWM_EN_BITS(s->pwmcfg)) { | 
|  | /* | 
|  | * Subtract ticks from number of ticks when the timer was zero | 
|  | * and mask to the register width. | 
|  | */ | 
|  | uint64_t pwmcount = (sifive_pwm_ns_to_ticks(s, now_ns) - | 
|  | s->tick_offset) & PWMCOUNT_MASK; | 
|  | uint64_t scale = sifive_pwm_compute_scale(s); | 
|  | /* PWMs only contains PWMCMP_MASK bits starting at scale */ | 
|  | uint64_t pwms = (pwmcount & (PWMCMP_MASK << scale)) >> scale; | 
|  |  | 
|  | for (int i = 0; i < SIFIVE_PWM_CHANS; i++) { | 
|  | uint64_t pwmcmp = s->pwmcmp[i] & PWMCMP_MASK; | 
|  | uint64_t pwmcmp_ticks = pwmcmp << scale; | 
|  |  | 
|  | /* | 
|  | * Per circuit diagram and spec, both cases raises corresponding | 
|  | * IP bit one clock cycle after time expires. | 
|  | */ | 
|  | if (pwmcmp > pwms) { | 
|  | uint64_t offset = pwmcmp_ticks - pwmcount + 1; | 
|  | uint64_t when_to_fire = now_ns + | 
|  | sifive_pwm_ticks_to_ns(s, offset); | 
|  |  | 
|  | trace_sifive_pwm_set_alarm(when_to_fire, now_ns); | 
|  | timer_mod(&s->timer[i], when_to_fire); | 
|  | } else { | 
|  | /* Schedule interrupt for next cycle */ | 
|  | trace_sifive_pwm_set_alarm(now_ns + 1, now_ns); | 
|  | timer_mod(&s->timer[i], now_ns + 1); | 
|  | } | 
|  |  | 
|  | } | 
|  | } else { | 
|  | /* | 
|  | * If timer incrementing disabled, just do pwms > pwmcmp check since | 
|  | * a write may have happened to PWMs. | 
|  | */ | 
|  | uint64_t pwmcount = (s->tick_offset) & PWMCOUNT_MASK; | 
|  | uint64_t scale = sifive_pwm_compute_scale(s); | 
|  | uint64_t pwms = (pwmcount & (PWMCMP_MASK << scale)) >> scale; | 
|  |  | 
|  | for (int i = 0; i < SIFIVE_PWM_CHANS; i++) { | 
|  | uint64_t pwmcmp = s->pwmcmp[i] & PWMCMP_MASK; | 
|  |  | 
|  | if (pwms >= pwmcmp) { | 
|  | trace_sifive_pwm_set_alarm(now_ns + 1, now_ns); | 
|  | timer_mod(&s->timer[i], now_ns + 1); | 
|  | } else { | 
|  | /* Effectively disable timer by scheduling far in future. */ | 
|  | trace_sifive_pwm_set_alarm(0xFFFFFFFFFFFFFF, now_ns); | 
|  | timer_mod(&s->timer[i], 0xFFFFFFFFFFFFFF); | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | static void sifive_pwm_interrupt(SiFivePwmState *s, int num) | 
|  | { | 
|  | uint64_t now = sifive_pwm_ns_to_ticks(s, | 
|  | qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL)); | 
|  | bool was_incrementing = HAS_PWM_EN_BITS(s->pwmcfg); | 
|  |  | 
|  | trace_sifive_pwm_interrupt(num); | 
|  |  | 
|  | s->pwmcfg |= R_CONFIG_CMP0IP_MASK << num; | 
|  | qemu_irq_raise(s->irqs[num]); | 
|  |  | 
|  | /* | 
|  | * If the zerocmp is set and pwmcmp0 raised the interrupt | 
|  | * reset the zero ticks. | 
|  | */ | 
|  | if ((s->pwmcfg & R_CONFIG_ZEROCMP_MASK) && (num == 0)) { | 
|  | /* If reset signal conditions, disable ENONESHOT. */ | 
|  | s->pwmcfg &= ~R_CONFIG_ENONESHOT_MASK; | 
|  |  | 
|  | if (was_incrementing) { | 
|  | /* If incrementing, time in ticks is when pwmcount is zero */ | 
|  | s->tick_offset = now; | 
|  | } else { | 
|  | /* If not incrementing, pwmcount = 0 */ | 
|  | s->tick_offset = 0; | 
|  | } | 
|  | } | 
|  |  | 
|  | /* | 
|  | * If carryout bit set, which we discern via looking for overflow, | 
|  | * also reset ENONESHOT. | 
|  | */ | 
|  | if (was_incrementing && | 
|  | ((now & PWMCOUNT_MASK) < (s->tick_offset & PWMCOUNT_MASK))) { | 
|  | s->pwmcfg &= ~R_CONFIG_ENONESHOT_MASK; | 
|  | } | 
|  |  | 
|  | /* Schedule or disable interrupts */ | 
|  | sifive_pwm_set_alarms(s); | 
|  |  | 
|  | /* If was enabled, and now not enabled, switch tick rep */ | 
|  | if (was_incrementing && !HAS_PWM_EN_BITS(s->pwmcfg)) { | 
|  | s->tick_offset = (now - s->tick_offset) & PWMCOUNT_MASK; | 
|  | } | 
|  | } | 
|  |  | 
|  | static void sifive_pwm_interrupt_0(void *opaque) | 
|  | { | 
|  | SiFivePwmState *s = opaque; | 
|  |  | 
|  | sifive_pwm_interrupt(s, 0); | 
|  | } | 
|  |  | 
|  | static void sifive_pwm_interrupt_1(void *opaque) | 
|  | { | 
|  | SiFivePwmState *s = opaque; | 
|  |  | 
|  | sifive_pwm_interrupt(s, 1); | 
|  | } | 
|  |  | 
|  | static void sifive_pwm_interrupt_2(void *opaque) | 
|  | { | 
|  | SiFivePwmState *s = opaque; | 
|  |  | 
|  | sifive_pwm_interrupt(s, 2); | 
|  | } | 
|  |  | 
|  | static void sifive_pwm_interrupt_3(void *opaque) | 
|  | { | 
|  | SiFivePwmState *s = opaque; | 
|  |  | 
|  | sifive_pwm_interrupt(s, 3); | 
|  | } | 
|  |  | 
|  | static uint64_t sifive_pwm_read(void *opaque, hwaddr addr, | 
|  | unsigned int size) | 
|  | { | 
|  | SiFivePwmState *s = opaque; | 
|  | uint64_t cur_time, scale; | 
|  | uint64_t now = sifive_pwm_ns_to_ticks(s, | 
|  | qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL)); | 
|  |  | 
|  | trace_sifive_pwm_read(addr); | 
|  |  | 
|  | switch (addr) { | 
|  | case A_CONFIG: | 
|  | return s->pwmcfg; | 
|  | case A_COUNT: | 
|  | cur_time = s->tick_offset; | 
|  |  | 
|  | if (HAS_PWM_EN_BITS(s->pwmcfg)) { | 
|  | cur_time = now - cur_time; | 
|  | } | 
|  |  | 
|  | /* | 
|  | * Return the value in the counter with bit 31 always 0 | 
|  | * This is allowed to wrap around so we don't need to check that. | 
|  | */ | 
|  | return cur_time & PWMCOUNT_MASK; | 
|  | case A_PWMS: | 
|  | cur_time = s->tick_offset; | 
|  | scale = sifive_pwm_compute_scale(s); | 
|  |  | 
|  | if (HAS_PWM_EN_BITS(s->pwmcfg)) { | 
|  | cur_time = now - cur_time; | 
|  | } | 
|  |  | 
|  | return ((cur_time & PWMCOUNT_MASK) >> scale) & PWMCMP_MASK; | 
|  | case A_PWMCMP0: | 
|  | return s->pwmcmp[0] & PWMCMP_MASK; | 
|  | case A_PWMCMP1: | 
|  | return s->pwmcmp[1] & PWMCMP_MASK; | 
|  | case A_PWMCMP2: | 
|  | return s->pwmcmp[2] & PWMCMP_MASK; | 
|  | case A_PWMCMP3: | 
|  | return s->pwmcmp[3] & PWMCMP_MASK; | 
|  | default: | 
|  | qemu_log_mask(LOG_GUEST_ERROR, | 
|  | "%s: Bad offset 0x%"HWADDR_PRIx"\n", __func__, addr); | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static void sifive_pwm_write(void *opaque, hwaddr addr, | 
|  | uint64_t val64, unsigned int size) | 
|  | { | 
|  | SiFivePwmState *s = opaque; | 
|  | uint32_t value = val64; | 
|  | uint64_t new_offset, scale; | 
|  | uint64_t now = sifive_pwm_ns_to_ticks(s, | 
|  | qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL)); | 
|  |  | 
|  | trace_sifive_pwm_write(value, addr); | 
|  |  | 
|  | switch (addr) { | 
|  | case A_CONFIG: | 
|  | if (value & (R_CONFIG_CMP0CENTER_MASK | R_CONFIG_CMP1CENTER_MASK | | 
|  | R_CONFIG_CMP2CENTER_MASK | R_CONFIG_CMP3CENTER_MASK)) { | 
|  | qemu_log_mask(LOG_UNIMP, "%s: CMPxCENTER is not supported\n", | 
|  | __func__); | 
|  | } | 
|  |  | 
|  | if (value & (R_CONFIG_CMP0GANG_MASK | R_CONFIG_CMP1GANG_MASK | | 
|  | R_CONFIG_CMP2GANG_MASK | R_CONFIG_CMP3GANG_MASK)) { | 
|  | qemu_log_mask(LOG_UNIMP, "%s: CMPxGANG is not supported\n", | 
|  | __func__); | 
|  | } | 
|  |  | 
|  | if (value & (R_CONFIG_CMP0IP_MASK | R_CONFIG_CMP1IP_MASK | | 
|  | R_CONFIG_CMP2IP_MASK | R_CONFIG_CMP3IP_MASK)) { | 
|  | qemu_log_mask(LOG_UNIMP, "%s: CMPxIP is not supported\n", | 
|  | __func__); | 
|  | } | 
|  |  | 
|  | if (!(value & R_CONFIG_CMP0IP_MASK)) { | 
|  | qemu_irq_lower(s->irqs[0]); | 
|  | } | 
|  |  | 
|  | if (!(value & R_CONFIG_CMP1IP_MASK)) { | 
|  | qemu_irq_lower(s->irqs[1]); | 
|  | } | 
|  |  | 
|  | if (!(value & R_CONFIG_CMP2IP_MASK)) { | 
|  | qemu_irq_lower(s->irqs[2]); | 
|  | } | 
|  |  | 
|  | if (!(value & R_CONFIG_CMP3IP_MASK)) { | 
|  | qemu_irq_lower(s->irqs[3]); | 
|  | } | 
|  |  | 
|  | /* | 
|  | * If this write enables the timer increment | 
|  | * set the time when pwmcount was zero to be cur_time - pwmcount. | 
|  | * If this write disables the timer increment | 
|  | * convert back from pwmcount to the time in ticks | 
|  | * when pwmcount was zero. | 
|  | */ | 
|  | if ((!HAS_PWM_EN_BITS(s->pwmcfg) && HAS_PWM_EN_BITS(value)) || | 
|  | (HAS_PWM_EN_BITS(s->pwmcfg) && !HAS_PWM_EN_BITS(value))) { | 
|  | s->tick_offset = (now - s->tick_offset) & PWMCOUNT_MASK; | 
|  | } | 
|  |  | 
|  | s->pwmcfg = value; | 
|  | break; | 
|  | case A_COUNT: | 
|  | /* The guest changed the counter, updated the offset value. */ | 
|  | new_offset = value; | 
|  |  | 
|  | if (HAS_PWM_EN_BITS(s->pwmcfg)) { | 
|  | new_offset = now - new_offset; | 
|  | } | 
|  |  | 
|  | s->tick_offset = new_offset; | 
|  | break; | 
|  | case A_PWMS: | 
|  | scale = sifive_pwm_compute_scale(s); | 
|  | new_offset = (((value & PWMCMP_MASK) << scale) & PWMCOUNT_MASK); | 
|  |  | 
|  | if (HAS_PWM_EN_BITS(s->pwmcfg)) { | 
|  | new_offset = now - new_offset; | 
|  | } | 
|  |  | 
|  | s->tick_offset = new_offset; | 
|  | break; | 
|  | case A_PWMCMP0: | 
|  | s->pwmcmp[0] = value & PWMCMP_MASK; | 
|  | break; | 
|  | case A_PWMCMP1: | 
|  | s->pwmcmp[1] = value & PWMCMP_MASK; | 
|  | break; | 
|  | case A_PWMCMP2: | 
|  | s->pwmcmp[2] = value & PWMCMP_MASK; | 
|  | break; | 
|  | case A_PWMCMP3: | 
|  | s->pwmcmp[3] = value & PWMCMP_MASK; | 
|  | break; | 
|  | default: | 
|  | qemu_log_mask(LOG_GUEST_ERROR, | 
|  | "%s: Bad offset 0x%"HWADDR_PRIx"\n", __func__, addr); | 
|  | } | 
|  |  | 
|  | /* Update the alarms to reflect possible updated values */ | 
|  | sifive_pwm_set_alarms(s); | 
|  | } | 
|  |  | 
|  | static void sifive_pwm_reset(DeviceState *dev) | 
|  | { | 
|  | SiFivePwmState *s = SIFIVE_PWM(dev); | 
|  | uint64_t now = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL); | 
|  |  | 
|  | s->pwmcfg = 0x00000000; | 
|  | s->pwmcmp[0] = 0x00000000; | 
|  | s->pwmcmp[1] = 0x00000000; | 
|  | s->pwmcmp[2] = 0x00000000; | 
|  | s->pwmcmp[3] = 0x00000000; | 
|  |  | 
|  | s->tick_offset = sifive_pwm_ns_to_ticks(s, now); | 
|  | } | 
|  |  | 
|  | static const MemoryRegionOps sifive_pwm_ops = { | 
|  | .read = sifive_pwm_read, | 
|  | .write = sifive_pwm_write, | 
|  | .endianness = DEVICE_NATIVE_ENDIAN, | 
|  | }; | 
|  |  | 
|  | static const VMStateDescription vmstate_sifive_pwm = { | 
|  | .name = TYPE_SIFIVE_PWM, | 
|  | .version_id = 1, | 
|  | .minimum_version_id = 1, | 
|  | .fields = (VMStateField[]) { | 
|  | VMSTATE_TIMER_ARRAY(timer, SiFivePwmState, 4), | 
|  | VMSTATE_UINT64(tick_offset, SiFivePwmState), | 
|  | VMSTATE_UINT32(pwmcfg, SiFivePwmState), | 
|  | VMSTATE_UINT32_ARRAY(pwmcmp, SiFivePwmState, 4), | 
|  | VMSTATE_END_OF_LIST() | 
|  | } | 
|  | }; | 
|  |  | 
|  | static Property sifive_pwm_properties[] = { | 
|  | /* 0.5Ghz per spec after FSBL */ | 
|  | DEFINE_PROP_UINT64("clock-frequency", struct SiFivePwmState, | 
|  | freq_hz, 500000000ULL), | 
|  | DEFINE_PROP_END_OF_LIST(), | 
|  | }; | 
|  |  | 
|  | static void sifive_pwm_init(Object *obj) | 
|  | { | 
|  | SiFivePwmState *s = SIFIVE_PWM(obj); | 
|  | int i; | 
|  |  | 
|  | for (i = 0; i < SIFIVE_PWM_IRQS; i++) { | 
|  | sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irqs[i]); | 
|  | } | 
|  |  | 
|  | memory_region_init_io(&s->mmio, obj, &sifive_pwm_ops, s, | 
|  | TYPE_SIFIVE_PWM, 0x100); | 
|  | sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->mmio); | 
|  | } | 
|  |  | 
|  | static void sifive_pwm_realize(DeviceState *dev, Error **errp) | 
|  | { | 
|  | SiFivePwmState *s = SIFIVE_PWM(dev); | 
|  |  | 
|  | timer_init_ns(&s->timer[0], QEMU_CLOCK_VIRTUAL, | 
|  | sifive_pwm_interrupt_0, s); | 
|  |  | 
|  | timer_init_ns(&s->timer[1], QEMU_CLOCK_VIRTUAL, | 
|  | sifive_pwm_interrupt_1, s); | 
|  |  | 
|  | timer_init_ns(&s->timer[2], QEMU_CLOCK_VIRTUAL, | 
|  | sifive_pwm_interrupt_2, s); | 
|  |  | 
|  | timer_init_ns(&s->timer[3], QEMU_CLOCK_VIRTUAL, | 
|  | sifive_pwm_interrupt_3, s); | 
|  | } | 
|  |  | 
|  | static void sifive_pwm_class_init(ObjectClass *klass, void *data) | 
|  | { | 
|  | DeviceClass *dc = DEVICE_CLASS(klass); | 
|  |  | 
|  | dc->reset = sifive_pwm_reset; | 
|  | device_class_set_props(dc, sifive_pwm_properties); | 
|  | dc->vmsd = &vmstate_sifive_pwm; | 
|  | dc->realize = sifive_pwm_realize; | 
|  | } | 
|  |  | 
|  | static const TypeInfo sifive_pwm_info = { | 
|  | .name          = TYPE_SIFIVE_PWM, | 
|  | .parent        = TYPE_SYS_BUS_DEVICE, | 
|  | .instance_size = sizeof(SiFivePwmState), | 
|  | .instance_init = sifive_pwm_init, | 
|  | .class_init    = sifive_pwm_class_init, | 
|  | }; | 
|  |  | 
|  | static void sifive_pwm_register_types(void) | 
|  | { | 
|  | type_register_static(&sifive_pwm_info); | 
|  | } | 
|  |  | 
|  | type_init(sifive_pwm_register_types) |