diff mbox

[v3,06/14] ARM: exynos4210: PWM support.

Message ID 1323672206-11891-7-git-send-email-e.voevodin@samsung.com
State New
Headers show

Commit Message

Evgeny Voevodin Dec. 12, 2011, 6:43 a.m. UTC
Signed-off-by: Evgeny Voevodin <e.voevodin@samsung.com>
---
 Makefile.target     |    2 +-
 hw/exynos4210.c     |   12 ++
 hw/exynos4210_pwm.c |  433 +++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 446 insertions(+), 1 deletions(-)
 create mode 100644 hw/exynos4210_pwm.c

Comments

Peter Maydell Dec. 13, 2011, 10:51 a.m. UTC | #1
On 12 December 2011 06:43, Evgeny Voevodin <e.voevodin@samsung.com> wrote:
>
> Signed-off-by: Evgeny Voevodin <e.voevodin@samsung.com>
> ---
>  Makefile.target     |    2 +-
>  hw/exynos4210.c     |   12 ++
>  hw/exynos4210_pwm.c |  433 +++++++++++++++++++++++++++++++++++++++++++++++++++
>  3 files changed, 446 insertions(+), 1 deletions(-)
>  create mode 100644 hw/exynos4210_pwm.c
>
> diff --git a/Makefile.target b/Makefile.target
> index 779c9d4..709e9e2 100644
> --- a/Makefile.target
> +++ b/Makefile.target
> @@ -345,7 +345,7 @@ obj-arm-y += arm_boot.o pl011.o pl031.o pl050.o pl080.o pl110.o pl181.o pl190.o
>  obj-arm-y += versatile_pci.o
>  obj-arm-y += realview_gic.o realview.o arm_sysctl.o arm11mpcore.o a9mpcore.o
>  obj-arm-y += exynos4210.o exynos4210_cmu.o exynos4210_uart.o exynos4210_gic.o \
> -             exynos4210_combiner.o
> +             exynos4210_combiner.o exynos4210_pwm.o
>  obj-arm-y += armv7m.o armv7m_nvic.o stellaris.o pl022.o stellaris_enet.o
>  obj-arm-y += pl061.o
>  obj-arm-y += arm-semi.o
> diff --git a/hw/exynos4210.c b/hw/exynos4210.c
> index 45d427e..7a7760d 100644
> --- a/hw/exynos4210.c
> +++ b/hw/exynos4210.c
> @@ -65,6 +65,9 @@
>  /* SFR Base Address for CMUs */
>  #define EXYNOS4210_CMU_BASE_ADDR            0x10030000
>
> +/* PWM */
> +#define EXYNOS4210_PWM_BASE_ADDR            0x139D0000
> +
>  /* UART's definitions */
>  #define EXYNOS4210_UART_BASE_ADDR           0x13800000
>  #define EXYNOS4210_UART_SHIFT               0x00010000
> @@ -364,6 +367,15 @@ static void exynos4210_init(ram_addr_t ram_size,
>     /* CMU */
>     sysbus_create_simple("exynos4210.cmu", EXYNOS4210_CMU_BASE_ADDR, NULL);
>
> +    /* PWM */
> +    sysbus_create_varargs("exynos4210.pwm", EXYNOS4210_PWM_BASE_ADDR,
> +            irq_table[exynos4210_get_irq(22, 0)],
> +            irq_table[exynos4210_get_irq(22, 1)],
> +            irq_table[exynos4210_get_irq(22, 2)],
> +            irq_table[exynos4210_get_irq(22, 3)],
> +            irq_table[exynos4210_get_irq(22, 4)],
> +            NULL);
> +
>     /*** UARTs ***/
>     for (n = 0; n < EXYNOS4210_UARTS_NUMBER; n++) {
>
> diff --git a/hw/exynos4210_pwm.c b/hw/exynos4210_pwm.c
> new file mode 100644
> index 0000000..1e80f10
> --- /dev/null
> +++ b/hw/exynos4210_pwm.c
> @@ -0,0 +1,433 @@
> +/*
> + * Samsung exynos4210 Pulse Width Modulation Timer
> + *
> + * Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd.
> + * All rights reserved.
> + *
> + * Evgeny Voevodin <e.voevodin@samsung.com>
> + *
> + * 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.
> + *
> + * You should have received a copy of the GNU General Public License along
> + * with this program; if not, write to the Free Software Foundation, Inc., 51
> + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
> + */
> +
> +#include "sysbus.h"
> +#include "qemu-timer.h"
> +#include "qemu-common.h"
> +#include "hw.h"
> +
> +#include "exynos4210.h"
> +
> +//#define DEBUG_PWM
> +
> +#ifdef DEBUG_PWM
> +#define DPRINTF(fmt, ...) \
> +        do { fprintf(stdout, "PWM: [%24s:%5d] " fmt, __func__, __LINE__, \
> +                ## __VA_ARGS__); } while (0)
> +#else
> +#define DPRINTF(fmt, ...) do {} while (0)
> +#endif
> +
> +#define     EXYNOS4210_PWM_TIMERS_NUM      5
> +#define     EXYNOS4210_PWM_REG_MEM_SIZE    0x50
> +
> +#define     TCFG0        0x0000
> +#define     TCFG1        0x0004
> +#define     TCON         0x0008
> +#define     TCNTB0       0x000C
> +#define     TCMPB0       0x0010
> +#define     TCNTO0       0x0014
> +#define     TCNTB1       0x0018
> +#define     TCMPB1       0x001C
> +#define     TCNTO1       0x0020
> +#define     TCNTB2       0x0024
> +#define     TCMPB2       0x0028
> +#define     TCNTO2       0x002C
> +#define     TCNTB3       0x0030
> +#define     TCMPB3       0x0034
> +#define     TCNTO3       0x0038
> +#define     TCNTB4       0x003C
> +#define     TCNTO4       0x0040
> +#define     TINT_CSTAT   0x0044
> +
> +#define     TCNTB(x)    (0xC*x)
> +#define     TCMPB(x)    (0xC*x+1)
> +#define     TCNTO(x)    (0xC*x+2)
> +
> +#define     GET_PRESCALER(reg, x)  ((reg&(0xFF<<(8*x)))>>8*x)
> +#define     GET_DIVIDER(reg, x)    (1<<((0xF<<(4*x))>>(4*x)))
> +
> +/*
> + * Attention! Timer4 doesn't have OUTPUT_INVERTER,
> + * so Auto Reload bit is not accessible by macros!
> + */
> +#define     TCON_TIMER_BASE(x)          ((x ? 1 : 0)*4 + 4*x)
> +#define     TCON_TIMER_START(x)         (1<<(TCON_TIMER_BASE(x) + 0))
> +#define     TCON_TIMER_MANUAL_UPD(x)    (1<<(TCON_TIMER_BASE(x) + 1))
> +#define     TCON_TIMER_OUTPUT_INV(x)    (1<<(TCON_TIMER_BASE(x) + 2))
> +#define     TCON_TIMER_AUTO_RELOAD(x)   (1<<(TCON_TIMER_BASE(x) + 3))
> +#define     TCON_TIMER4_AUTO_RELOAD     (1<<22)
> +
> +#define     TINT_CSTAT_STATUS(x)        (1<<(5+x))
> +#define     TINT_CSTAT_ENABLE(x)        (1<<x)
> +
> +/* timer struct */
> +typedef struct {
> +    uint32_t    id;             /* timer id */
> +    qemu_irq    irq;            /* local timer irq */
> +    uint32_t    freq;           /* timer frequency */
> +
> +    /* use ptimer.c to represent count down timer */
> +    ptimer_state *ptimer;       /* timer  */
> +
> +    /* registers */
> +    uint32_t    reg_tcntb;       /* counter register buffer */
> +    uint32_t    reg_tcmpb;       /* compare register buffer */
> +
> +} Exynos4210_pwm;
> +
> +
> +typedef struct Exynos4210PWMState {
> +    SysBusDevice busdev;
> +    MemoryRegion iomem;
> +
> +    uint32_t    reg_tcfg[2];
> +    uint32_t    reg_tcon;
> +    uint32_t    reg_tint_cstat;
> +
> +    Exynos4210CmuClock clk;         /* clock source for timer */
> +    Exynos4210_pwm timer[EXYNOS4210_PWM_TIMERS_NUM];
> +
> +} Exynos4210PWMState;
> +
> +/*** VMState ***/
> +static const VMStateDescription VMState_Exynos4210_pwm = {
> +        .name = "exynos4210.pwm.pwm",
> +        .version_id = 1,
> +        .minimum_version_id = 1,
> +        .minimum_version_id_old = 1,
> +        .fields = (VMStateField[]) {
> +            VMSTATE_UINT32(id, Exynos4210_pwm),
> +            VMSTATE_UINT32(freq, Exynos4210_pwm),
> +            VMSTATE_UINT32(reg_tcntb, Exynos4210_pwm),
> +            VMSTATE_UINT32(reg_tcmpb, Exynos4210_pwm),
> +            VMSTATE_END_OF_LIST()
> +        }
> +};
> +
> +static const VMStateDescription VMState_Exynos4210PWMState = {
> +        .name = "exynos4210.pwm",
> +        .version_id = 1,
> +        .minimum_version_id = 1,
> +        .minimum_version_id_old = 1,
> +        .fields = (VMStateField[]) {
> +            VMSTATE_UINT32_ARRAY(reg_tcfg, Exynos4210PWMState, 2),
> +            VMSTATE_UINT32(reg_tcon, Exynos4210PWMState),
> +            VMSTATE_UINT32(reg_tint_cstat, Exynos4210PWMState),
> +            VMSTATE_STRUCT_ARRAY(timer, Exynos4210PWMState,
> +                    EXYNOS4210_PWM_TIMERS_NUM, 0,
> +            VMState_Exynos4210_pwm, Exynos4210_pwm),
> +            VMSTATE_END_OF_LIST()
> +        }
> +};
> +
> +/*
> + * PWM update frequency
> + */
> +static void exynos4210_pwm_update_freq(Exynos4210PWMState *s, uint32_t id)
> +{
> +    uint32_t freq;
> +    freq = s->timer[id].freq;
> +    if (id > 1) {
> +        s->timer[id].freq = exynos4210_cmu_get_rate(s->clk) /
> +        ((GET_PRESCALER(s->reg_tcfg[0], 1) + 1) *
> +                (1<<GET_DIVIDER(s->reg_tcfg[1], id)));
> +    } else {
> +        s->timer[id].freq = exynos4210_cmu_get_rate(s->clk) /
> +        ((GET_PRESCALER(s->reg_tcfg[0], 0) + 1) *
> +                (1<<GET_DIVIDER(s->preg_tcfg[1], id)));
> +    }
> +
> +    if (freq != s->timer[id].freq) {
> +        ptimer_set_freq(s->timer[id].ptimer, s->timer[id].freq);
> +        DPRINTF("freq=%dHz\n", s->timer[id].freq);
> +    }
> +}
> +
> +/*
> + * Counter tick handler
> + */
> +static void exynos4210_pwm_tick(void *opaque, uint32_t id)
> +{
> +    Exynos4210PWMState *s = (Exynos4210PWMState *)opaque;
> +    bool cmp;
> +
> +    DPRINTF("timer %d tick\n", id);
> +
> +    /* set irq status */
> +    s->reg_tint_cstat |= TINT_CSTAT_STATUS(id);
> +
> +    /* raise IRQ */
> +    if (s->reg_tint_cstat & TINT_CSTAT_ENABLE(id)) {
> +        DPRINTF("timer %d IRQ\n", id);
> +        qemu_irq_raise(s->timer[id].irq);
> +    }
> +
> +    /* reload timer */
> +    if (id != 4) {
> +        cmp = s->reg_tcon & TCON_TIMER_AUTO_RELOAD(id);
> +    } else {
> +        cmp = s->reg_tcon & TCON_TIMER4_AUTO_RELOAD;
> +    }
> +
> +    if (cmp) {
> +        DPRINTF("auto reload timer %d count to %x\n", id,
> +                s->timer[id].reg_tcntb);
> +        ptimer_set_count(s->timer[id].ptimer, s->timer[id].reg_tcntb);
> +        ptimer_run(s->timer[id].ptimer, 1);
> +    } else {
> +        /* stop timer, set status to STOP, see Basic Timer Operation */
> +        s->reg_tcon = ~TCON_TIMER_START(id);
> +        ptimer_stop(s->timer[id].ptimer);
> +    }
> +}
> +
> +static void exynos4210_pwm_tick0(void *opaque)
> +{
> +    exynos4210_pwm_tick(opaque, 0);
> +}
> +static void exynos4210_pwm_tick1(void *opaque)
> +{
> +    exynos4210_pwm_tick(opaque, 1);
> +}
> +static void exynos4210_pwm_tick2(void *opaque)
> +{
> +    exynos4210_pwm_tick(opaque, 2);
> +}
> +static void exynos4210_pwm_tick3(void *opaque)
> +{
> +    exynos4210_pwm_tick(opaque, 3);
> +}
> +static void exynos4210_pwm_tick4(void *opaque)
> +{
> +    exynos4210_pwm_tick(opaque, 4);
> +}

If you make the Exynos4210_pwm struct include a pointer back to the
Exynos4210PWMState struct, then you can make exynos4210_pwm_tick
take a pointer to the Exynos4210_pwm struct, and then you can
drop these wrapper functions and just say
   bh[i] = qemu_bh_new(exynos4210_pwm_tick, &s->timer[i]);

> +
> +/*
> + * PWM Read
> + */
> +static uint64_t exynos4210_pwm_read(void *opaque, target_phys_addr_t offset,
> +        unsigned size)
> +{
> +    Exynos4210PWMState *s = (Exynos4210PWMState *)opaque;
> +    uint32_t value = 0;
> +    int index;
> +
> +    switch (offset) {
> +    case TCFG0: case TCFG1:
> +        index = (offset - TCFG0)>>2;
> +        value = s->reg_tcfg[index];
> +        break;
> +
> +    case TCON:
> +        value = s->reg_tcon;
> +        break;
> +
> +    case TCNTB0: case TCNTB1:
> +    case TCNTB2: case TCNTB3: case TCNTB4:
> +        index = (offset - TCNTB0)/0xC;
> +        value = s->timer[index].reg_tcntb;
> +        break;
> +
> +    case TCMPB0: case TCMPB1:
> +    case TCMPB2: case TCMPB3:
> +        index = (offset - TCMPB0)/0xC;
> +        value = s->timer[index].reg_tcmpb;
> +        break;
> +
> +    case TCNTO0: case TCNTO1:
> +    case TCNTO2: case TCNTO3: case TCNTO4:
> +        index = (offset == TCNTO4) ? 4 : (offset - TCNTO0)/0xC;
> +        value = ptimer_get_count(s->timer[index].ptimer);
> +        break;
> +
> +    case TINT_CSTAT:
> +        value = s->reg_tint_cstat;
> +        break;
> +
> +    default:
> +        fprintf(stderr,
> +                "[exynos4210.pwm: bad read offset " TARGET_FMT_plx "]\n",
> +                offset);
> +        break;
> +    }
> +    return value;
> +}
> +
> +/*
> + * PWM Write
> + */
> +static void exynos4210_pwm_write(void *opaque, target_phys_addr_t offset,
> +        uint64_t value, unsigned size)
> +{
> +    Exynos4210PWMState *s = (Exynos4210PWMState *)opaque;
> +    int index;
> +    uint32_t new_val;
> +    int i;
> +
> +    switch (offset) {
> +    case TCFG0: case TCFG1:
> +        index = (offset - TCFG0)>>2;
> +        s->reg_tcfg[index] = value;
> +
> +        /* update timers frequencies */
> +        for (i = 0; i < EXYNOS4210_PWM_TIMERS_NUM; i++) {
> +            exynos4210_pwm_update_freq(s, s->timer[i].id);
> +        }
> +        break;
> +
> +    case TCON:
> +        for (i = 0; i < EXYNOS4210_PWM_TIMERS_NUM; i++) {
> +            if ((value & TCON_TIMER_MANUAL_UPD(i)) >
> +            (s->reg_tcon & TCON_TIMER_MANUAL_UPD(i))) {
> +                /*
> +                 * TCNTB and TCMPB are loaded into TCNT and TCMP.
> +                 * Update timers.
> +                 */
> +
> +                /* this will start timer to run, this ok, because
> +                 * during processing start bit timer will be stopped
> +                 * if needed */
> +                ptimer_set_count(s->timer[i].ptimer, s->timer[i].reg_tcntb);
> +                DPRINTF("set timer %d count to %x\n", i,
> +                        s->timer[i].reg_tcntb);
> +            }
> +
> +            if ((value & TCON_TIMER_START(i)) >
> +            (s->reg_tcon & TCON_TIMER_START(i))) {
> +                /* changed to start */
> +                ptimer_run(s->timer[i].ptimer, 1);
> +                DPRINTF("run timer %d\n", i);
> +            }
> +
> +            if ((value & TCON_TIMER_START(i)) <
> +                    (s->reg_tcon & TCON_TIMER_START(i))) {
> +                /* changed to stop */
> +                ptimer_stop(s->timer[i].ptimer);
> +                DPRINTF("stop timer %d\n", i);
> +            }
> +        }
> +        s->reg_tcon = value;
> +        break;
> +
> +    case TCNTB0: case TCNTB1:
> +    case TCNTB2: case TCNTB3: case TCNTB4:
> +        index = (offset - TCNTB0)/0xC;
> +        s->timer[index].reg_tcntb = value;
> +        break;
> +
> +    case TCMPB0: case TCMPB1:
> +    case TCMPB2: case TCMPB3:
> +        index = (offset - TCMPB0)/0xC;
> +        s->timer[index].reg_tcmpb = value;
> +        break;
> +
> +    case TINT_CSTAT:
> +        new_val = (s->reg_tint_cstat&0x3E0) + (0x1F & value);
> +        new_val &= ~(0x3E0 & value);
> +
> +        for (i = 0; i < EXYNOS4210_PWM_TIMERS_NUM; i++) {
> +            if ((new_val & TINT_CSTAT_STATUS(i)) <
> +                    (s->reg_tint_cstat & TINT_CSTAT_STATUS(i))) {
> +                qemu_irq_lower(s->timer[i].irq);
> +            }
> +        }
> +
> +        s->reg_tint_cstat = new_val;
> +        break;
> +
> +    default:
> +        fprintf(stderr,
> +                "[exynos4210.pwm: bad write offset " TARGET_FMT_plx "]\n",
> +                offset);
> +        break;
> +
> +    }
> +}
> +
> +/*
> + * Set default values to timer fields and registers
> + */
> +static void exynos4210_pwm_reset(Exynos4210PWMState *s)
> +{
> +    int i;
> +    s->reg_tcfg[0] = 0x0101;
> +    s->reg_tcfg[1] = 0x0;
> +    s->reg_tcon = 0;
> +    s->reg_tint_cstat = 0;
> +    for (i = 0; i < EXYNOS4210_PWM_TIMERS_NUM; i++) {
> +        s->timer[i].reg_tcmpb = 0;
> +        s->timer[i].reg_tcntb = 0;
> +
> +        exynos4210_pwm_update_freq(s, s->timer[i].id);
> +        ptimer_stop(s->timer[i].ptimer);
> +    }
> +}
> +
> +static const MemoryRegionOps exynos4210_pwm_ops = {
> +        .read = exynos4210_pwm_read,
> +        .write = exynos4210_pwm_write,
> +        .endianness = DEVICE_NATIVE_ENDIAN,
> +};
> +
> +/*
> + * PWM timer initialization
> + */
> +static int exynos4210_pwm_init(SysBusDevice *dev)
> +{
> +    Exynos4210PWMState *s = FROM_SYSBUS(Exynos4210PWMState, dev);
> +    int i;
> +    QEMUBH * bh[EXYNOS4210_PWM_TIMERS_NUM];
> +
> +    s->clk = ACLK_100;
> +
> +    bh[0] = qemu_bh_new(exynos4210_pwm_tick0, s);
> +    bh[1] = qemu_bh_new(exynos4210_pwm_tick1, s);
> +    bh[2] = qemu_bh_new(exynos4210_pwm_tick2, s);
> +    bh[3] = qemu_bh_new(exynos4210_pwm_tick3, s);
> +    bh[4] = qemu_bh_new(exynos4210_pwm_tick4, s);
> +
> +    for (i = 0; i < EXYNOS4210_PWM_TIMERS_NUM; i++) {
> +        sysbus_init_irq(dev, &s->timer[i].irq);
> +        s->timer[i].ptimer = ptimer_init(bh[i]);
> +        s->timer[i].id = i;
> +    }
> +
> +    memory_region_init_io(&s->iomem, &exynos4210_pwm_ops, s, "exynos4210-pwm",
> +            EXYNOS4210_PWM_REG_MEM_SIZE);
> +    sysbus_init_mmio(dev, &s->iomem);
> +
> +    exynos4210_pwm_reset(s);
> +
> +    qemu_register_reset((QEMUResetHandler *)exynos4210_pwm_reset, s);
> +    vmstate_register(NULL, -1, &VMState_Exynos4210PWMState, s);

Same remarks about reset, vmstate as in other patch review mail.

> +    return 0;
> +}
> +
> +static void exynos4210_pwm_register_devices(void)
> +{
> +    sysbus_register_dev("exynos4210.pwm", sizeof(Exynos4210PWMState),
> +            exynos4210_pwm_init);
> +}
> +
> +device_init(exynos4210_pwm_register_devices)
> --
> 1.7.4.1
>
>
diff mbox

Patch

diff --git a/Makefile.target b/Makefile.target
index 779c9d4..709e9e2 100644
--- a/Makefile.target
+++ b/Makefile.target
@@ -345,7 +345,7 @@  obj-arm-y += arm_boot.o pl011.o pl031.o pl050.o pl080.o pl110.o pl181.o pl190.o
 obj-arm-y += versatile_pci.o
 obj-arm-y += realview_gic.o realview.o arm_sysctl.o arm11mpcore.o a9mpcore.o
 obj-arm-y += exynos4210.o exynos4210_cmu.o exynos4210_uart.o exynos4210_gic.o \
-             exynos4210_combiner.o
+             exynos4210_combiner.o exynos4210_pwm.o
 obj-arm-y += armv7m.o armv7m_nvic.o stellaris.o pl022.o stellaris_enet.o
 obj-arm-y += pl061.o
 obj-arm-y += arm-semi.o
diff --git a/hw/exynos4210.c b/hw/exynos4210.c
index 45d427e..7a7760d 100644
--- a/hw/exynos4210.c
+++ b/hw/exynos4210.c
@@ -65,6 +65,9 @@ 
 /* SFR Base Address for CMUs */
 #define EXYNOS4210_CMU_BASE_ADDR            0x10030000
 
+/* PWM */
+#define EXYNOS4210_PWM_BASE_ADDR            0x139D0000
+
 /* UART's definitions */
 #define EXYNOS4210_UART_BASE_ADDR           0x13800000
 #define EXYNOS4210_UART_SHIFT               0x00010000
@@ -364,6 +367,15 @@  static void exynos4210_init(ram_addr_t ram_size,
     /* CMU */
     sysbus_create_simple("exynos4210.cmu", EXYNOS4210_CMU_BASE_ADDR, NULL);
 
+    /* PWM */
+    sysbus_create_varargs("exynos4210.pwm", EXYNOS4210_PWM_BASE_ADDR,
+            irq_table[exynos4210_get_irq(22, 0)],
+            irq_table[exynos4210_get_irq(22, 1)],
+            irq_table[exynos4210_get_irq(22, 2)],
+            irq_table[exynos4210_get_irq(22, 3)],
+            irq_table[exynos4210_get_irq(22, 4)],
+            NULL);
+
     /*** UARTs ***/
     for (n = 0; n < EXYNOS4210_UARTS_NUMBER; n++) {
 
diff --git a/hw/exynos4210_pwm.c b/hw/exynos4210_pwm.c
new file mode 100644
index 0000000..1e80f10
--- /dev/null
+++ b/hw/exynos4210_pwm.c
@@ -0,0 +1,433 @@ 
+/*
+ * Samsung exynos4210 Pulse Width Modulation Timer
+ *
+ * Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd.
+ * All rights reserved.
+ *
+ * Evgeny Voevodin <e.voevodin@samsung.com>
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 51
+ * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "sysbus.h"
+#include "qemu-timer.h"
+#include "qemu-common.h"
+#include "hw.h"
+
+#include "exynos4210.h"
+
+//#define DEBUG_PWM
+
+#ifdef DEBUG_PWM
+#define DPRINTF(fmt, ...) \
+        do { fprintf(stdout, "PWM: [%24s:%5d] " fmt, __func__, __LINE__, \
+                ## __VA_ARGS__); } while (0)
+#else
+#define DPRINTF(fmt, ...) do {} while (0)
+#endif
+
+#define     EXYNOS4210_PWM_TIMERS_NUM      5
+#define     EXYNOS4210_PWM_REG_MEM_SIZE    0x50
+
+#define     TCFG0        0x0000
+#define     TCFG1        0x0004
+#define     TCON         0x0008
+#define     TCNTB0       0x000C
+#define     TCMPB0       0x0010
+#define     TCNTO0       0x0014
+#define     TCNTB1       0x0018
+#define     TCMPB1       0x001C
+#define     TCNTO1       0x0020
+#define     TCNTB2       0x0024
+#define     TCMPB2       0x0028
+#define     TCNTO2       0x002C
+#define     TCNTB3       0x0030
+#define     TCMPB3       0x0034
+#define     TCNTO3       0x0038
+#define     TCNTB4       0x003C
+#define     TCNTO4       0x0040
+#define     TINT_CSTAT   0x0044
+
+#define     TCNTB(x)    (0xC*x)
+#define     TCMPB(x)    (0xC*x+1)
+#define     TCNTO(x)    (0xC*x+2)
+
+#define     GET_PRESCALER(reg, x)  ((reg&(0xFF<<(8*x)))>>8*x)
+#define     GET_DIVIDER(reg, x)    (1<<((0xF<<(4*x))>>(4*x)))
+
+/*
+ * Attention! Timer4 doesn't have OUTPUT_INVERTER,
+ * so Auto Reload bit is not accessible by macros!
+ */
+#define     TCON_TIMER_BASE(x)          ((x ? 1 : 0)*4 + 4*x)
+#define     TCON_TIMER_START(x)         (1<<(TCON_TIMER_BASE(x) + 0))
+#define     TCON_TIMER_MANUAL_UPD(x)    (1<<(TCON_TIMER_BASE(x) + 1))
+#define     TCON_TIMER_OUTPUT_INV(x)    (1<<(TCON_TIMER_BASE(x) + 2))
+#define     TCON_TIMER_AUTO_RELOAD(x)   (1<<(TCON_TIMER_BASE(x) + 3))
+#define     TCON_TIMER4_AUTO_RELOAD     (1<<22)
+
+#define     TINT_CSTAT_STATUS(x)        (1<<(5+x))
+#define     TINT_CSTAT_ENABLE(x)        (1<<x)
+
+/* timer struct */
+typedef struct {
+    uint32_t    id;             /* timer id */
+    qemu_irq    irq;            /* local timer irq */
+    uint32_t    freq;           /* timer frequency */
+
+    /* use ptimer.c to represent count down timer */
+    ptimer_state *ptimer;       /* timer  */
+
+    /* registers */
+    uint32_t    reg_tcntb;       /* counter register buffer */
+    uint32_t    reg_tcmpb;       /* compare register buffer */
+
+} Exynos4210_pwm;
+
+
+typedef struct Exynos4210PWMState {
+    SysBusDevice busdev;
+    MemoryRegion iomem;
+
+    uint32_t    reg_tcfg[2];
+    uint32_t    reg_tcon;
+    uint32_t    reg_tint_cstat;
+
+    Exynos4210CmuClock clk;         /* clock source for timer */
+    Exynos4210_pwm timer[EXYNOS4210_PWM_TIMERS_NUM];
+
+} Exynos4210PWMState;
+
+/*** VMState ***/
+static const VMStateDescription VMState_Exynos4210_pwm = {
+        .name = "exynos4210.pwm.pwm",
+        .version_id = 1,
+        .minimum_version_id = 1,
+        .minimum_version_id_old = 1,
+        .fields = (VMStateField[]) {
+            VMSTATE_UINT32(id, Exynos4210_pwm),
+            VMSTATE_UINT32(freq, Exynos4210_pwm),
+            VMSTATE_UINT32(reg_tcntb, Exynos4210_pwm),
+            VMSTATE_UINT32(reg_tcmpb, Exynos4210_pwm),
+            VMSTATE_END_OF_LIST()
+        }
+};
+
+static const VMStateDescription VMState_Exynos4210PWMState = {
+        .name = "exynos4210.pwm",
+        .version_id = 1,
+        .minimum_version_id = 1,
+        .minimum_version_id_old = 1,
+        .fields = (VMStateField[]) {
+            VMSTATE_UINT32_ARRAY(reg_tcfg, Exynos4210PWMState, 2),
+            VMSTATE_UINT32(reg_tcon, Exynos4210PWMState),
+            VMSTATE_UINT32(reg_tint_cstat, Exynos4210PWMState),
+            VMSTATE_STRUCT_ARRAY(timer, Exynos4210PWMState,
+                    EXYNOS4210_PWM_TIMERS_NUM, 0,
+            VMState_Exynos4210_pwm, Exynos4210_pwm),
+            VMSTATE_END_OF_LIST()
+        }
+};
+
+/*
+ * PWM update frequency
+ */
+static void exynos4210_pwm_update_freq(Exynos4210PWMState *s, uint32_t id)
+{
+    uint32_t freq;
+    freq = s->timer[id].freq;
+    if (id > 1) {
+        s->timer[id].freq = exynos4210_cmu_get_rate(s->clk) /
+        ((GET_PRESCALER(s->reg_tcfg[0], 1) + 1) *
+                (1<<GET_DIVIDER(s->reg_tcfg[1], id)));
+    } else {
+        s->timer[id].freq = exynos4210_cmu_get_rate(s->clk) /
+        ((GET_PRESCALER(s->reg_tcfg[0], 0) + 1) *
+                (1<<GET_DIVIDER(s->preg_tcfg[1], id)));
+    }
+
+    if (freq != s->timer[id].freq) {
+        ptimer_set_freq(s->timer[id].ptimer, s->timer[id].freq);
+        DPRINTF("freq=%dHz\n", s->timer[id].freq);
+    }
+}
+
+/*
+ * Counter tick handler
+ */
+static void exynos4210_pwm_tick(void *opaque, uint32_t id)
+{
+    Exynos4210PWMState *s = (Exynos4210PWMState *)opaque;
+    bool cmp;
+
+    DPRINTF("timer %d tick\n", id);
+
+    /* set irq status */
+    s->reg_tint_cstat |= TINT_CSTAT_STATUS(id);
+
+    /* raise IRQ */
+    if (s->reg_tint_cstat & TINT_CSTAT_ENABLE(id)) {
+        DPRINTF("timer %d IRQ\n", id);
+        qemu_irq_raise(s->timer[id].irq);
+    }
+
+    /* reload timer */
+    if (id != 4) {
+        cmp = s->reg_tcon & TCON_TIMER_AUTO_RELOAD(id);
+    } else {
+        cmp = s->reg_tcon & TCON_TIMER4_AUTO_RELOAD;
+    }
+
+    if (cmp) {
+        DPRINTF("auto reload timer %d count to %x\n", id,
+                s->timer[id].reg_tcntb);
+        ptimer_set_count(s->timer[id].ptimer, s->timer[id].reg_tcntb);
+        ptimer_run(s->timer[id].ptimer, 1);
+    } else {
+        /* stop timer, set status to STOP, see Basic Timer Operation */
+        s->reg_tcon = ~TCON_TIMER_START(id);
+        ptimer_stop(s->timer[id].ptimer);
+    }
+}
+
+static void exynos4210_pwm_tick0(void *opaque)
+{
+    exynos4210_pwm_tick(opaque, 0);
+}
+static void exynos4210_pwm_tick1(void *opaque)
+{
+    exynos4210_pwm_tick(opaque, 1);
+}
+static void exynos4210_pwm_tick2(void *opaque)
+{
+    exynos4210_pwm_tick(opaque, 2);
+}
+static void exynos4210_pwm_tick3(void *opaque)
+{
+    exynos4210_pwm_tick(opaque, 3);
+}
+static void exynos4210_pwm_tick4(void *opaque)
+{
+    exynos4210_pwm_tick(opaque, 4);
+}
+
+/*
+ * PWM Read
+ */
+static uint64_t exynos4210_pwm_read(void *opaque, target_phys_addr_t offset,
+        unsigned size)
+{
+    Exynos4210PWMState *s = (Exynos4210PWMState *)opaque;
+    uint32_t value = 0;
+    int index;
+
+    switch (offset) {
+    case TCFG0: case TCFG1:
+        index = (offset - TCFG0)>>2;
+        value = s->reg_tcfg[index];
+        break;
+
+    case TCON:
+        value = s->reg_tcon;
+        break;
+
+    case TCNTB0: case TCNTB1:
+    case TCNTB2: case TCNTB3: case TCNTB4:
+        index = (offset - TCNTB0)/0xC;
+        value = s->timer[index].reg_tcntb;
+        break;
+
+    case TCMPB0: case TCMPB1:
+    case TCMPB2: case TCMPB3:
+        index = (offset - TCMPB0)/0xC;
+        value = s->timer[index].reg_tcmpb;
+        break;
+
+    case TCNTO0: case TCNTO1:
+    case TCNTO2: case TCNTO3: case TCNTO4:
+        index = (offset == TCNTO4) ? 4 : (offset - TCNTO0)/0xC;
+        value = ptimer_get_count(s->timer[index].ptimer);
+        break;
+
+    case TINT_CSTAT:
+        value = s->reg_tint_cstat;
+        break;
+
+    default:
+        fprintf(stderr,
+                "[exynos4210.pwm: bad read offset " TARGET_FMT_plx "]\n",
+                offset);
+        break;
+    }
+    return value;
+}
+
+/*
+ * PWM Write
+ */
+static void exynos4210_pwm_write(void *opaque, target_phys_addr_t offset,
+        uint64_t value, unsigned size)
+{
+    Exynos4210PWMState *s = (Exynos4210PWMState *)opaque;
+    int index;
+    uint32_t new_val;
+    int i;
+
+    switch (offset) {
+    case TCFG0: case TCFG1:
+        index = (offset - TCFG0)>>2;
+        s->reg_tcfg[index] = value;
+
+        /* update timers frequencies */
+        for (i = 0; i < EXYNOS4210_PWM_TIMERS_NUM; i++) {
+            exynos4210_pwm_update_freq(s, s->timer[i].id);
+        }
+        break;
+
+    case TCON:
+        for (i = 0; i < EXYNOS4210_PWM_TIMERS_NUM; i++) {
+            if ((value & TCON_TIMER_MANUAL_UPD(i)) >
+            (s->reg_tcon & TCON_TIMER_MANUAL_UPD(i))) {
+                /*
+                 * TCNTB and TCMPB are loaded into TCNT and TCMP.
+                 * Update timers.
+                 */
+
+                /* this will start timer to run, this ok, because
+                 * during processing start bit timer will be stopped
+                 * if needed */
+                ptimer_set_count(s->timer[i].ptimer, s->timer[i].reg_tcntb);
+                DPRINTF("set timer %d count to %x\n", i,
+                        s->timer[i].reg_tcntb);
+            }
+
+            if ((value & TCON_TIMER_START(i)) >
+            (s->reg_tcon & TCON_TIMER_START(i))) {
+                /* changed to start */
+                ptimer_run(s->timer[i].ptimer, 1);
+                DPRINTF("run timer %d\n", i);
+            }
+
+            if ((value & TCON_TIMER_START(i)) <
+                    (s->reg_tcon & TCON_TIMER_START(i))) {
+                /* changed to stop */
+                ptimer_stop(s->timer[i].ptimer);
+                DPRINTF("stop timer %d\n", i);
+            }
+        }
+        s->reg_tcon = value;
+        break;
+
+    case TCNTB0: case TCNTB1:
+    case TCNTB2: case TCNTB3: case TCNTB4:
+        index = (offset - TCNTB0)/0xC;
+        s->timer[index].reg_tcntb = value;
+        break;
+
+    case TCMPB0: case TCMPB1:
+    case TCMPB2: case TCMPB3:
+        index = (offset - TCMPB0)/0xC;
+        s->timer[index].reg_tcmpb = value;
+        break;
+
+    case TINT_CSTAT:
+        new_val = (s->reg_tint_cstat&0x3E0) + (0x1F & value);
+        new_val &= ~(0x3E0 & value);
+
+        for (i = 0; i < EXYNOS4210_PWM_TIMERS_NUM; i++) {
+            if ((new_val & TINT_CSTAT_STATUS(i)) <
+                    (s->reg_tint_cstat & TINT_CSTAT_STATUS(i))) {
+                qemu_irq_lower(s->timer[i].irq);
+            }
+        }
+
+        s->reg_tint_cstat = new_val;
+        break;
+
+    default:
+        fprintf(stderr,
+                "[exynos4210.pwm: bad write offset " TARGET_FMT_plx "]\n",
+                offset);
+        break;
+
+    }
+}
+
+/*
+ * Set default values to timer fields and registers
+ */
+static void exynos4210_pwm_reset(Exynos4210PWMState *s)
+{
+    int i;
+    s->reg_tcfg[0] = 0x0101;
+    s->reg_tcfg[1] = 0x0;
+    s->reg_tcon = 0;
+    s->reg_tint_cstat = 0;
+    for (i = 0; i < EXYNOS4210_PWM_TIMERS_NUM; i++) {
+        s->timer[i].reg_tcmpb = 0;
+        s->timer[i].reg_tcntb = 0;
+
+        exynos4210_pwm_update_freq(s, s->timer[i].id);
+        ptimer_stop(s->timer[i].ptimer);
+    }
+}
+
+static const MemoryRegionOps exynos4210_pwm_ops = {
+        .read = exynos4210_pwm_read,
+        .write = exynos4210_pwm_write,
+        .endianness = DEVICE_NATIVE_ENDIAN,
+};
+
+/*
+ * PWM timer initialization
+ */
+static int exynos4210_pwm_init(SysBusDevice *dev)
+{
+    Exynos4210PWMState *s = FROM_SYSBUS(Exynos4210PWMState, dev);
+    int i;
+    QEMUBH * bh[EXYNOS4210_PWM_TIMERS_NUM];
+
+    s->clk = ACLK_100;
+
+    bh[0] = qemu_bh_new(exynos4210_pwm_tick0, s);
+    bh[1] = qemu_bh_new(exynos4210_pwm_tick1, s);
+    bh[2] = qemu_bh_new(exynos4210_pwm_tick2, s);
+    bh[3] = qemu_bh_new(exynos4210_pwm_tick3, s);
+    bh[4] = qemu_bh_new(exynos4210_pwm_tick4, s);
+
+    for (i = 0; i < EXYNOS4210_PWM_TIMERS_NUM; i++) {
+        sysbus_init_irq(dev, &s->timer[i].irq);
+        s->timer[i].ptimer = ptimer_init(bh[i]);
+        s->timer[i].id = i;
+    }
+
+    memory_region_init_io(&s->iomem, &exynos4210_pwm_ops, s, "exynos4210-pwm",
+            EXYNOS4210_PWM_REG_MEM_SIZE);
+    sysbus_init_mmio(dev, &s->iomem);
+
+    exynos4210_pwm_reset(s);
+
+    qemu_register_reset((QEMUResetHandler *)exynos4210_pwm_reset, s);
+    vmstate_register(NULL, -1, &VMState_Exynos4210PWMState, s);
+    return 0;
+}
+
+static void exynos4210_pwm_register_devices(void)
+{
+    sysbus_register_dev("exynos4210.pwm", sizeof(Exynos4210PWMState),
+            exynos4210_pwm_init);
+}
+
+device_init(exynos4210_pwm_register_devices)