diff mbox series

[v3,3/5] hw/adc: Add an ADC module for NPCM7XX

Message ID 20201215001312.3120777-4-wuhaotsh@google.com (mailing list archive)
State New, archived
Headers show
Series Additional NPCM7xx devices | expand

Commit Message

Hao Wu Dec. 15, 2020, 12:13 a.m. UTC
The ADC is part of NPCM7XX Module. Its behavior is controled by the
ADC_CON register. It converts one of the eight analog inputs into a
digital input and stores it in the ADC_DATA register when enabled.

Users can alter input value by using qom-set QMP command.

Reviewed-by: Havard Skinnemoen <hskinnemoen@google.com>
Reviewed-by: Tyrone Ting <kfting@nuvoton.com>
Signed-off-by: Hao Wu <wuhaotsh@google.com>
---
 docs/system/arm/nuvoton.rst    |   2 +-
 hw/adc/meson.build             |   1 +
 hw/adc/npcm7xx_adc.c           | 321 ++++++++++++++++++++++++++
 hw/adc/trace-events            |   5 +
 hw/arm/npcm7xx.c               |  24 +-
 include/hw/adc/npcm7xx_adc.h   |  72 ++++++
 include/hw/arm/npcm7xx.h       |   2 +
 meson.build                    |   1 +
 tests/qtest/meson.build        |   3 +-
 tests/qtest/npcm7xx_adc-test.c | 400 +++++++++++++++++++++++++++++++++
 10 files changed, 828 insertions(+), 3 deletions(-)
 create mode 100644 hw/adc/npcm7xx_adc.c
 create mode 100644 hw/adc/trace-events
 create mode 100644 include/hw/adc/npcm7xx_adc.h
 create mode 100644 tests/qtest/npcm7xx_adc-test.c

Comments

Hao Wu Dec. 15, 2020, 12:16 a.m. UTC | #1
On Mon, Dec 14, 2020 at 4:13 PM Hao Wu <wuhaotsh@google.com> wrote:

> The ADC is part of NPCM7XX Module. Its behavior is controled by the
> ADC_CON register. It converts one of the eight analog inputs into a
> digital input and stores it in the ADC_DATA register when enabled.
>
> Users can alter input value by using qom-set QMP command.
>
> Reviewed-by: Havard Skinnemoen <hskinnemoen@google.com>
> Reviewed-by: Tyrone Ting <kfting@nuvoton.com>
> Signed-off-by: Hao Wu <wuhaotsh@google.com>
> ---
>  docs/system/arm/nuvoton.rst    |   2 +-
>  hw/adc/meson.build             |   1 +
>  hw/adc/npcm7xx_adc.c           | 321 ++++++++++++++++++++++++++
>  hw/adc/trace-events            |   5 +
>  hw/arm/npcm7xx.c               |  24 +-
>  include/hw/adc/npcm7xx_adc.h   |  72 ++++++
>  include/hw/arm/npcm7xx.h       |   2 +
>  meson.build                    |   1 +
>  tests/qtest/meson.build        |   3 +-
>  tests/qtest/npcm7xx_adc-test.c | 400 +++++++++++++++++++++++++++++++++
>  10 files changed, 828 insertions(+), 3 deletions(-)
>  create mode 100644 hw/adc/npcm7xx_adc.c
>  create mode 100644 hw/adc/trace-events
>  create mode 100644 include/hw/adc/npcm7xx_adc.h
>  create mode 100644 tests/qtest/npcm7xx_adc-test.c
>
> diff --git a/docs/system/arm/nuvoton.rst b/docs/system/arm/nuvoton.rst
> index b00d405d52..35829f8d0b 100644
> --- a/docs/system/arm/nuvoton.rst
> +++ b/docs/system/arm/nuvoton.rst
> @@ -41,6 +41,7 @@ Supported devices
>   * Random Number Generator (RNG)
>   * USB host (USBH)
>   * GPIO controller
> + * Analog to Digital Converter (ADC)
>
>  Missing devices
>  ---------------
> @@ -58,7 +59,6 @@ Missing devices
>   * USB device (USBD)
>   * SMBus controller (SMBF)
>   * Peripheral SPI controller (PSPI)
> - * Analog to Digital Converter (ADC)
>   * SD/MMC host
>   * PECI interface
>   * Pulse Width Modulation (PWM)
> diff --git a/hw/adc/meson.build b/hw/adc/meson.build
> index 0d62ae96ae..6ddee23813 100644
> --- a/hw/adc/meson.build
> +++ b/hw/adc/meson.build
> @@ -1 +1,2 @@
>  softmmu_ss.add(when: 'CONFIG_STM32F2XX_ADC', if_true:
> files('stm32f2xx_adc.c'))
> +softmmu_ss.add(when: 'CONFIG_NPCM7XX', if_true: files('npcm7xx_adc.c'))
> diff --git a/hw/adc/npcm7xx_adc.c b/hw/adc/npcm7xx_adc.c
> new file mode 100644
> index 0000000000..c2c4819d3f
> --- /dev/null
> +++ b/hw/adc/npcm7xx_adc.c
> @@ -0,0 +1,321 @@
> +/*
> + * Nuvoton NPCM7xx ADC Module
> + *
> + * Copyright 2020 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 "hw/adc/npcm7xx_adc.h"
> +#include "hw/qdev-clock.h"
> +#include "hw/qdev-properties.h"
> +#include "migration/vmstate.h"
> +#include "qemu/log.h"
> +#include "qemu/module.h"
> +#include "qemu/timer.h"
> +#include "qemu/units.h"
> +#include "trace.h"
> +
> +/* 32-bit register indices. */
> +enum NPCM7xxADCRegisters {
> +    NPCM7XX_ADC_CON,
> +    NPCM7XX_ADC_DATA,
> +    NPCM7XX_ADC_REGS_END,
> +};
> +
> +/* Register field definitions. */
> +#define NPCM7XX_ADC_CON_MUX(rv) extract32(rv, 24, 4)
> +#define NPCM7XX_ADC_CON_INT_EN  BIT(21)
> +#define NPCM7XX_ADC_CON_REFSEL  BIT(19)
> +#define NPCM7XX_ADC_CON_INT     BIT(18)
> +#define NPCM7XX_ADC_CON_EN      BIT(17)
> +#define NPCM7XX_ADC_CON_RST     BIT(16)
> +#define NPCM7XX_ADC_CON_CONV    BIT(14)
> +#define NPCM7XX_ADC_CON_DIV(rv) extract32(rv, 1, 8)
> +
> +#define NPCM7XX_ADC_MAX_RESULT      1023
> +#define NPCM7XX_ADC_DEFAULT_IREF    2000000
> +#define NPCM7XX_ADC_CONV_CYCLES     20
> +#define NPCM7XX_ADC_RESET_CYCLES    10
> +#define NPCM7XX_ADC_R0_INPUT        500000
> +#define NPCM7XX_ADC_R1_INPUT        1500000
> +
> +static void npcm7xx_adc_reset(NPCM7xxADCState *s)
> +{
> +    timer_del(&s->conv_timer);
> +    timer_del(&s->reset_timer);
> +    s->con = 0x000c0001;
> +    s->data = 0x00000000;
> +}
> +
> +static uint32_t npcm7xx_adc_convert(uint32_t input, uint32_t ref)
> +{
> +    uint32_t result;
> +
> +    result = input * (NPCM7XX_ADC_MAX_RESULT + 1) / ref;
> +    if (result > NPCM7XX_ADC_MAX_RESULT) {
> +        result = NPCM7XX_ADC_MAX_RESULT;
> +    }
> +
> +    return result;
> +}
> +
> +static uint32_t npcm7xx_adc_prescaler(NPCM7xxADCState *s)
> +{
> +    return 2 * (NPCM7XX_ADC_CON_DIV(s->con) + 1);
> +}
> +
> +static void npcm7xx_adc_start_timer(Clock *clk, QEMUTimer *timer,
> +        uint32_t cycles, uint32_t prescaler)
> +{
> +    int64_t now = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL);
> +    int64_t freq = clock_get_hz(clk);
> +    int64_t ns;
> +
> +    ns = (NANOSECONDS_PER_SECOND * cycles * prescaler / freq);
> +    ns += now;
> +    timer_mod(timer, ns);
> +}
> +
> +static void npcm7xx_adc_start_reset(NPCM7xxADCState *s)
> +{
> +    uint32_t prescaler = npcm7xx_adc_prescaler(s);
> +
> +    npcm7xx_adc_start_timer(s->clock, &s->reset_timer,
> NPCM7XX_ADC_RESET_CYCLES,
> +            prescaler);
> +}
> +
> +static void npcm7xx_adc_start_convert(NPCM7xxADCState *s)
> +{
> +    uint32_t prescaler = npcm7xx_adc_prescaler(s);
> +
> +    npcm7xx_adc_start_timer(s->clock, &s->conv_timer,
> NPCM7XX_ADC_CONV_CYCLES,
> +            prescaler);
> +}
> +
> +static void npcm7xx_adc_reset_done(void *opaque)
> +{
> +    NPCM7xxADCState *s = opaque;
> +
> +    npcm7xx_adc_reset(s);
> +}
> +
> +static void npcm7xx_adc_convert_done(void *opaque)
> +{
> +    NPCM7xxADCState *s = opaque;
> +    uint32_t input = NPCM7XX_ADC_CON_MUX(s->con);
> +    uint32_t ref = (s->con & NPCM7XX_ADC_CON_REFSEL)
> +        ? s->iref : s->vref;
> +
> +    g_assert(input < NPCM7XX_ADC_NUM_INPUTS);
> +    s->data = npcm7xx_adc_convert(s->adci[input], ref);
> +    if (s->con & NPCM7XX_ADC_CON_INT_EN) {
> +        s->con |= NPCM7XX_ADC_CON_INT;
> +        qemu_irq_raise(s->irq);
> +    }
> +    s->con &= ~NPCM7XX_ADC_CON_CONV;
> +}
> +
> +static void npcm7xx_adc_calibrate(NPCM7xxADCState *adc)
> +{
> +    adc->calibration_r_values[0] =
> npcm7xx_adc_convert(NPCM7XX_ADC_R0_INPUT,
> +            adc->iref);
> +    adc->calibration_r_values[1] =
> npcm7xx_adc_convert(NPCM7XX_ADC_R1_INPUT,
> +            adc->iref);
> +}
> +
> +static void npcm7xx_adc_write_con(NPCM7xxADCState *s, uint32_t new_con)
> +{
> +    uint32_t old_con = s->con;
> +
> +    /* Write ADC_INT to 1 to clear it */
> +    if (new_con & NPCM7XX_ADC_CON_INT) {
> +        new_con &= ~NPCM7XX_ADC_CON_INT;
> +    } else if (old_con & NPCM7XX_ADC_CON_INT) {
> +        new_con |= NPCM7XX_ADC_CON_INT;
> +    }
> +
> +    s->con = new_con;
> +
> +    if (s->con & NPCM7XX_ADC_CON_RST) {
> +        if (!(old_con & NPCM7XX_ADC_CON_RST)) {
> +            npcm7xx_adc_start_reset(s);
> +        }
> +    } else {
> +        timer_del(&s->reset_timer);
> +    }
> +
> +    if ((s->con & NPCM7XX_ADC_CON_EN)) {
> +        if (s->con & NPCM7XX_ADC_CON_CONV) {
> +            if (!(old_con & NPCM7XX_ADC_CON_CONV)) {
> +                npcm7xx_adc_start_convert(s);
> +            }
> +        } else {
> +            timer_del(&s->conv_timer);
> +        }
> +    }
> +}
> +
> +static uint64_t npcm7xx_adc_read(void *opaque, hwaddr offset, unsigned
> size)
> +{
> +    uint64_t value = 0;
> +    NPCM7xxADCState *s = opaque;
> +    hwaddr reg = offset / sizeof(uint32_t);
> +
> +    switch (reg) {
> +    case NPCM7XX_ADC_CON:
> +        value = s->con;
> +        break;
> +
> +    case NPCM7XX_ADC_DATA:
> +        value = s->data;
> +        break;
> +
> +    default:
> +        qemu_log_mask(LOG_GUEST_ERROR,
> +                      "%s: invalid offset 0x%04" HWADDR_PRIx "\n",
> +                      __func__, offset);
> +        break;
> +    }
> +
> +    trace_npcm7xx_adc_read(DEVICE(s)->canonical_path, offset, value);
> +    return value;
> +}
> +
> +static void npcm7xx_adc_write(void *opaque, hwaddr offset, uint64_t v,
> +        unsigned size)
> +{
> +    NPCM7xxADCState *s = opaque;
> +    hwaddr reg = offset / sizeof(uint32_t);
> +
> +    trace_npcm7xx_adc_write(DEVICE(s)->canonical_path, offset, v);
> +    switch (reg) {
> +    case NPCM7XX_ADC_CON:
> +        npcm7xx_adc_write_con(s, v);
> +        break;
> +
> +    case NPCM7XX_ADC_DATA:
> +        qemu_log_mask(LOG_GUEST_ERROR,
> +                      "%s: register @ 0x%04" HWADDR_PRIx " is
> read-only\n",
> +                      __func__, offset);
> +        break;
> +
> +    default:
> +        qemu_log_mask(LOG_GUEST_ERROR,
> +                      "%s: invalid offset 0x%04" HWADDR_PRIx "\n",
> +                      __func__, offset);
> +        break;
> +    }
> +
> +}
> +
> +static const struct MemoryRegionOps npcm7xx_adc_ops = {
> +    .read       = npcm7xx_adc_read,
> +    .write      = npcm7xx_adc_write,
> +    .endianness = DEVICE_LITTLE_ENDIAN,
> +    .valid      = {
> +        .min_access_size        = 4,
> +        .max_access_size        = 4,
> +        .unaligned              = false,
> +    },
> +};
> +
> +static void npcm7xx_adc_enter_reset(Object *obj, ResetType type)
> +{
> +    NPCM7xxADCState *s = NPCM7XX_ADC(obj);
> +
> +    npcm7xx_adc_reset(s);
> +}
> +
> +static void npcm7xx_adc_hold_reset(Object *obj)
> +{
> +    NPCM7xxADCState *s = NPCM7XX_ADC(obj);
> +
> +    qemu_irq_lower(s->irq);
> +}
> +
> +static void npcm7xx_adc_init(Object *obj)
> +{
> +    NPCM7xxADCState *s = NPCM7XX_ADC(obj);
> +    SysBusDevice *sbd = &s->parent;
> +    int i;
> +
> +    sysbus_init_irq(sbd, &s->irq);
> +
> +    timer_init_ns(&s->conv_timer, QEMU_CLOCK_VIRTUAL,
> +            npcm7xx_adc_convert_done, s);
> +    timer_init_ns(&s->reset_timer, QEMU_CLOCK_VIRTUAL,
> +            npcm7xx_adc_reset_done, s);
> +    memory_region_init_io(&s->iomem, obj, &npcm7xx_adc_ops, s,
> +                          TYPE_NPCM7XX_ADC, 4 * KiB);
> +    sysbus_init_mmio(sbd, &s->iomem);
> +    s->clock = qdev_init_clock_in(DEVICE(s), "clock", NULL, NULL);
> +
> +    for (i = 0; i < NPCM7XX_ADC_NUM_INPUTS; ++i) {
> +        object_property_add_uint32_ptr(obj, "adci[*]",
> +                &s->adci[i], OBJ_PROP_FLAG_WRITE);
> +    }
> +    object_property_add_uint32_ptr(obj, "vref",
> +            &s->vref, OBJ_PROP_FLAG_WRITE);
> +    npcm7xx_adc_calibrate(s);
> +}
> +
> +static const VMStateDescription vmstate_npcm7xx_adc = {
> +    .name = "npcm7xx-adc",
> +    .version_id = 0,
> +    .minimum_version_id = 0,
> +    .fields = (VMStateField[]) {
> +        VMSTATE_TIMER(conv_timer, NPCM7xxADCState),
> +        VMSTATE_TIMER(reset_timer, NPCM7xxADCState),
> +        VMSTATE_UINT32(con, NPCM7xxADCState),
> +        VMSTATE_UINT32(data, NPCM7xxADCState),
> +        VMSTATE_CLOCK(clock, NPCM7xxADCState),
> +        VMSTATE_UINT32_ARRAY(adci, NPCM7xxADCState,
> NPCM7XX_ADC_NUM_INPUTS),
> +        VMSTATE_UINT32(vref, NPCM7xxADCState),
> +        VMSTATE_UINT32(iref, NPCM7xxADCState),
> +        VMSTATE_UINT16_ARRAY(calibration_r_values, NPCM7xxADCState,
> +                NPCM7XX_ADC_NUM_CALIB),
> +        VMSTATE_END_OF_LIST(),
> +    },
> +};
> +
> +static Property npcm7xx_timer_properties[] = {
> +    DEFINE_PROP_UINT32("iref", NPCM7xxADCState, iref,
> NPCM7XX_ADC_DEFAULT_IREF),
> +    DEFINE_PROP_END_OF_LIST(),
> +};
> +
> +static void npcm7xx_adc_class_init(ObjectClass *klass, void *data)
> +{
> +    ResettableClass *rc = RESETTABLE_CLASS(klass);
> +    DeviceClass *dc = DEVICE_CLASS(klass);
> +
> +    dc->desc = "NPCM7xx ADC Module";
> +    dc->vmsd = &vmstate_npcm7xx_adc;
> +    rc->phases.enter = npcm7xx_adc_enter_reset;
> +    rc->phases.hold = npcm7xx_adc_hold_reset;
> +
> +    device_class_set_props(dc, npcm7xx_timer_properties);
> +}
> +
> +static const TypeInfo npcm7xx_adc_info = {
> +    .name               = TYPE_NPCM7XX_ADC,
> +    .parent             = TYPE_SYS_BUS_DEVICE,
> +    .instance_size      = sizeof(NPCM7xxADCState),
> +    .class_init         = npcm7xx_adc_class_init,
> +    .instance_init      = npcm7xx_adc_init,
> +};
> +
> +static void npcm7xx_adc_register_types(void)
> +{
> +    type_register_static(&npcm7xx_adc_info);
> +}
> +
> +type_init(npcm7xx_adc_register_types);
> diff --git a/hw/adc/trace-events b/hw/adc/trace-events
> new file mode 100644
> index 0000000000..4c3279ece2
> --- /dev/null
> +++ b/hw/adc/trace-events
> @@ -0,0 +1,5 @@
> +# See docs/devel/tracing.txt for syntax documentation.
> +
> +# npcm7xx_adc.c
> +npcm7xx_adc_read(const char *id, uint64_t offset, uint32_t value) " %s
> offset: 0x%04" PRIx64 " value 0x%04" PRIx32
> +npcm7xx_adc_write(const char *id, uint64_t offset, uint32_t value) "%s
> offset: 0x%04" PRIx64 " value 0x%04" PRIx32
> diff --git a/hw/arm/npcm7xx.c b/hw/arm/npcm7xx.c
> index fabfb1697b..b22a8c966d 100644
> --- a/hw/arm/npcm7xx.c
> +++ b/hw/arm/npcm7xx.c
> @@ -51,6 +51,9 @@
>  #define NPCM7XX_EHCI_BA         (0xf0806000)
>  #define NPCM7XX_OHCI_BA         (0xf0807000)
>
> +/* ADC Module */
> +#define NPCM7XX_ADC_BA          (0xf000c000)
> +
>  /* Internal AHB SRAM */
>  #define NPCM7XX_RAM3_BA         (0xc0008000)
>  #define NPCM7XX_RAM3_SZ         (4 * KiB)
> @@ -61,6 +64,7 @@
>  #define NPCM7XX_ROM_BA          (0xffff0000)
>  #define NPCM7XX_ROM_SZ          (64 * KiB)
>
> +
>  /* Clock configuration values to be fixed up when bypassing bootloader */
>
>  /* Run PLL1 at 1600 MHz */
> @@ -73,6 +77,7 @@
>   * interrupts.
>   */
>  enum NPCM7xxInterrupt {
> +    NPCM7XX_ADC_IRQ             = 0,
>      NPCM7XX_UART0_IRQ           = 2,
>      NPCM7XX_UART1_IRQ,
>      NPCM7XX_UART2_IRQ,
> @@ -296,6 +301,14 @@ static void npcm7xx_init_fuses(NPCM7xxState *s)
>                              sizeof(value));
>  }
>
> +static void npcm7xx_write_adc_calibration(NPCM7xxState *s)
> +{
> +    /* Both ADC and the fuse array must have realized. */
> +    QEMU_BUILD_BUG_ON(sizeof(s->adc.calibration_r_values) != 4);
> +    npcm7xx_otp_array_write(&s->fuse_array, s->adc.calibration_r_values,
> +            NPCM7XX_FUSE_ADC_CALIB, sizeof(s->adc.calibration_r_values));
> +}
> +
>  static qemu_irq npcm7xx_irq(NPCM7xxState *s, int n)
>  {
>      return qdev_get_gpio_in(DEVICE(&s->a9mpcore), n);
> @@ -322,6 +335,7 @@ static void npcm7xx_init(Object *obj)
>                              TYPE_NPCM7XX_FUSE_ARRAY);
>      object_initialize_child(obj, "mc", &s->mc, TYPE_NPCM7XX_MC);
>      object_initialize_child(obj, "rng", &s->rng, TYPE_NPCM7XX_RNG);
> +    object_initialize_child(obj, "adc", &s->adc, TYPE_NPCM7XX_ADC);
>
>      for (i = 0; i < ARRAY_SIZE(s->tim); i++) {
>          object_initialize_child(obj, "tim[*]", &s->tim[i],
> TYPE_NPCM7XX_TIMER);
> @@ -414,6 +428,15 @@ static void npcm7xx_realize(DeviceState *dev, Error
> **errp)
>      sysbus_realize(SYS_BUS_DEVICE(&s->mc), &error_abort);
>      sysbus_mmio_map(SYS_BUS_DEVICE(&s->mc), 0, NPCM7XX_MC_BA);
>
> +    /* ADC Modules. Cannot fail. */
> +    qdev_connect_clock_in(DEVICE(&s->adc), "clock", qdev_get_clock_out(
> +                          DEVICE(&s->clk), "adc-clock"));
> +    sysbus_realize(SYS_BUS_DEVICE(&s->adc), &error_abort);
> +    sysbus_mmio_map(SYS_BUS_DEVICE(&s->adc), 0, NPCM7XX_ADC_BA);
> +    sysbus_connect_irq(SYS_BUS_DEVICE(&s->adc), 0,
> +            npcm7xx_irq(s, NPCM7XX_ADC_IRQ));
> +    npcm7xx_write_adc_calibration(s);
> +
>      /* Timer Modules (TIM). Cannot fail. */
>      QEMU_BUILD_BUG_ON(ARRAY_SIZE(npcm7xx_tim_addr) != ARRAY_SIZE(s->tim));
>      for (i = 0; i < ARRAY_SIZE(s->tim); i++) {
> @@ -528,7 +551,6 @@ static void npcm7xx_realize(DeviceState *dev, Error
> **errp)
>      create_unimplemented_device("npcm7xx.vdmx",         0xe0800000,   4 *
> KiB);
>      create_unimplemented_device("npcm7xx.pcierc",       0xe1000000,  64 *
> KiB);
>      create_unimplemented_device("npcm7xx.kcs",          0xf0007000,   4 *
> KiB);
> -    create_unimplemented_device("npcm7xx.adc",          0xf000c000,   4 *
> KiB);
>      create_unimplemented_device("npcm7xx.gfxi",         0xf000e000,   4 *
> KiB);
>      create_unimplemented_device("npcm7xx.gpio[0]",      0xf0010000,   4 *
> KiB);
>      create_unimplemented_device("npcm7xx.gpio[1]",      0xf0011000,   4 *
> KiB);
> diff --git a/include/hw/adc/npcm7xx_adc.h b/include/hw/adc/npcm7xx_adc.h
> new file mode 100644
> index 0000000000..7f9acbeaa1
> --- /dev/null
> +++ b/include/hw/adc/npcm7xx_adc.h
> @@ -0,0 +1,72 @@
> +/*
> + * Nuvoton NPCM7xx ADC Module
> + *
> + * Copyright 2020 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.
> + */
> +#ifndef NPCM7XX_ADC_H
> +#define NPCM7XX_ADC_H
> +
> +#include "qemu/osdep.h"
> +#include "hw/clock.h"
> +#include "hw/irq.h"
> +#include "hw/sysbus.h"
> +#include "qemu/timer.h"
> +
> +#define NPCM7XX_ADC_NUM_INPUTS      8
> +/**
> + * This value should not be changed unless write_adc_calibration function
> in
> + * hw/arm/npcm7xx.c is also changed.
> + */
> +#define NPCM7XX_ADC_NUM_CALIB       2
> +
> +/**
> + * struct NPCM7xxADCState - Analog to Digital Converter Module device
> state.
> + * @parent: System bus device.
> + * @iomem: Memory region through which registers are accessed.
> + * @conv_timer: The timer counts down remaining cycles for the conversion.
> + * @reset_timer: The timer counts down remaining cycles for reset.
> + * @irq: GIC interrupt line to fire on expiration (if enabled).
> + * @con: The Control Register.
> + * @data: The Data Buffer.
> + * @clock: The ADC Clock.
> + * @adci: The input voltage in units of uV. 1uv = 1e-6V.
> + * @vref: The external reference voltage.
> + * @iref: The internal reference voltage, initialized at launch time.
> + * @rv: The calibrated output values of 0.5V and 1.5V for the ADC.
> + */
> +typedef struct {
> +    SysBusDevice parent;
> +
> +    MemoryRegion iomem;
> +
> +    QEMUTimer    conv_timer;
> +    QEMUTimer    reset_timer;
> +
> +    qemu_irq     irq;
> +    uint32_t     con;
> +    uint32_t     data;
> +    Clock       *clock;
> +
> +    /* Voltages are in unit of uV. 1V = 1000000uV. */
> +    uint32_t     adci[NPCM7XX_ADC_NUM_INPUTS];
> +    uint32_t     vref;
> +    uint32_t     iref;
> +
> +    uint16_t     calibration_r_values[NPCM7XX_ADC_NUM_CALIB];
> +} NPCM7xxADCState;
> +
> +#define TYPE_NPCM7XX_ADC "npcm7xx-adc"
> +#define NPCM7XX_ADC(obj) \
> +    OBJECT_CHECK(NPCM7xxADCState, (obj), TYPE_NPCM7XX_ADC)
> +
> +#endif /* NPCM7XX_ADC_H */
> diff --git a/include/hw/arm/npcm7xx.h b/include/hw/arm/npcm7xx.h
> index 5469247e38..51e1c7620d 100644
> --- a/include/hw/arm/npcm7xx.h
> +++ b/include/hw/arm/npcm7xx.h
> @@ -17,6 +17,7 @@
>  #define NPCM7XX_H
>
>  #include "hw/boards.h"
> +#include "hw/adc/npcm7xx_adc.h"
>  #include "hw/cpu/a9mpcore.h"
>  #include "hw/gpio/npcm7xx_gpio.h"
>  #include "hw/mem/npcm7xx_mc.h"
> @@ -76,6 +77,7 @@ typedef struct NPCM7xxState {
>      NPCM7xxGCRState     gcr;
>      NPCM7xxCLKState     clk;
>      NPCM7xxTimerCtrlState tim[3];
> +    NPCM7xxADCState     adc;
>      NPCM7xxOTPState     key_storage;
>      NPCM7xxOTPState     fuse_array;
>      NPCM7xxMCState      mc;
> diff --git a/meson.build b/meson.build
> index f344b25955..fb03cdbdcc 100644
> --- a/meson.build
> +++ b/meson.build
> @@ -1435,6 +1435,7 @@ if have_system
>      'chardev',
>      'hw/9pfs',
>      'hw/acpi',
> +    'hw/adc',
>      'hw/alpha',
>      'hw/arm',
>      'hw/audio',
> diff --git a/tests/qtest/meson.build b/tests/qtest/meson.build
> index 6a67c538be..955710d1c5 100644
> --- a/tests/qtest/meson.build
> +++ b/tests/qtest/meson.build
> @@ -134,7 +134,8 @@ qtests_sparc64 = \
>    ['prom-env-test', 'boot-serial-test']
>
>  qtests_npcm7xx = \
> -  ['npcm7xx_gpio-test',
> +  ['npcm7xx_adc-test',
> +   'npcm7xx_gpio-test',
>     'npcm7xx_rng-test',
>     'npcm7xx_timer-test',
>     'npcm7xx_watchdog_timer-test']
> diff --git a/tests/qtest/npcm7xx_adc-test.c
> b/tests/qtest/npcm7xx_adc-test.c
> new file mode 100644
> index 0000000000..e63c544e51
> --- /dev/null
> +++ b/tests/qtest/npcm7xx_adc-test.c
> @@ -0,0 +1,400 @@
> +/*
> + * QTests for Nuvoton NPCM7xx ADCModules.
> + *
> + * Copyright 2020 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 "qemu/bitops.h"
> +#include "qemu/timer.h"
> +#include "libqos/libqtest.h"
> +#include "qapi/qmp/qdict.h"
> +
> +#define REF_HZ          (25000000)
> +
> +#define CON_OFFSET      0x0
> +#define DATA_OFFSET     0x4
> +
> +#define NUM_INPUTS      8
> +#define DEFAULT_IREF    2000000
> +#define CONV_CYCLES     20
> +#define RESET_CYCLES    10
> +#define R0_INPUT        500000
> +#define R1_INPUT        1500000
> +#define MAX_RESULT      1023
> +
> +#define DEFAULT_CLKDIV  5
> +
> +#define FUSE_ARRAY_BA   0xf018a000
> +#define FCTL_OFFSET     0x14
> +#define FST_OFFSET      0x0
> +#define FADDR_OFFSET    0x4
> +#define FDATA_OFFSET    0x8
> +#define ADC_CALIB_ADDR  24
> +#define FUSE_READ       0x2
> +
> +/* Register field definitions. */
> +#define CON_MUX(rv) ((rv) << 24)
> +#define CON_INT_EN  BIT(21)
> +#define CON_REFSEL  BIT(19)
> +#define CON_INT     BIT(18)
> +#define CON_EN      BIT(17)
> +#define CON_RST     BIT(16)
> +#define CON_CONV    BIT(14)
> +#define CON_DIV(rv) extract32(rv, 1, 8)
> +
> +#define FST_RDST    BIT(1)
> +#define FDATA_MASK  0xff
> +
> +#define MAX_ERROR   10000
> +#define MIN_CALIB_INPUT 100000
> +#define MAX_CALIB_INPUT 1800000
> +
> +static const uint32_t input_list[] = {
> +    100000,
> +    500000,
> +    1000000,
> +    1500000,
> +    1800000,
> +    2000000,
> +};
> +
> +static const uint32_t vref_list[] = {
> +    2000000,
> +    2200000,
> +    2500000,
> +};
> +
> +static const uint32_t iref_list[] = {
> +    1800000,
> +    1900000,
> +    2000000,
> +    2100000,
> +    2200000,
> +};
> +
> +static const uint32_t div_list[] = {0, 1, 3, 7, 15};
> +
> +typedef struct ADC {
> +    int irq;
> +    uint64_t base_addr;
> +} ADC;
> +
> +ADC adc = {
> +    .irq        = 0,
> +    .base_addr  = 0xf000c000
> +};
> +
> +static uint32_t adc_read_con(QTestState *qts, const ADC *adc)
> +{
> +    return qtest_readl(qts, adc->base_addr + CON_OFFSET);
> +}
> +
> +static void adc_write_con(QTestState *qts, const ADC *adc, uint32_t value)
> +{
> +    qtest_writel(qts, adc->base_addr + CON_OFFSET, value);
> +}
> +
> +static uint32_t adc_read_data(QTestState *qts, const ADC *adc)
> +{
> +    return qtest_readl(qts, adc->base_addr + DATA_OFFSET);
> +}
> +
> +static uint32_t adc_calibrate(uint32_t measured, uint32_t *rv)
> +{
> +    return R0_INPUT + (R1_INPUT - R0_INPUT) * (int32_t)(measured - rv[0])
> +        / (int32_t)(rv[1] - rv[0]);
> +}
> +
> +static void adc_qom_set(QTestState *qts, const ADC *adc,
> +        const char *name, uint32_t value)
> +{
> +    QDict *response;
> +    const char *path = "/machine/soc/adc";
> +
> +    g_test_message("Setting properties %s of %s with value %u",
> +            name, path, value);
> +    response = qtest_qmp(qts, "{ 'execute': 'qom-set',"
> +            " 'arguments': { 'path': %s, 'property': %s, 'value': %u}}",
> +            path, name, value);
> +    /* The qom set message returns successfully. */
> +    g_assert_true(qdict_haskey(response, "return"));
> +}
> +
> +static void adc_write_input(QTestState *qts, const ADC *adc,
> +        uint32_t index, uint32_t value)
> +{
> +    char name[100];
> +
> +    sprintf(name, "adci[%u]", index);
> +    adc_qom_set(qts, adc, name, value);
> +}
> +
> +static void adc_write_vref(QTestState *qts, const ADC *adc, uint32_t
> value)
> +{
> +    adc_qom_set(qts, adc, "vref", value);
> +}
> +
> +static uint32_t adc_calculate_output(uint32_t input, uint32_t ref)
> +{
> +    uint32_t output;
> +
> +    g_assert_cmpuint(input, <=, ref);
> +    output = (input * (MAX_RESULT + 1)) / ref;
> +    if (output > MAX_RESULT) {
> +        output = MAX_RESULT;
> +    }
> +
> +    return output;
> +}
> +
> +static uint32_t adc_prescaler(QTestState *qts, const ADC *adc)
> +{
> +    uint32_t div = extract32(adc_read_con(qts, adc), 1, 8);
> +
> +    return 2 * (div + 1);
> +}
> +
> +static int64_t adc_calculate_steps(uint32_t cycles, uint32_t prescale,
> +        uint32_t clkdiv)
> +{
> +    return (NANOSECONDS_PER_SECOND / (REF_HZ >> clkdiv)) * cycles *
> prescale;
> +}
> +
> +static void adc_wait_conv_finished(QTestState *qts, const ADC *adc,
> +        uint32_t clkdiv)
> +{
> +    uint32_t prescaler = adc_prescaler(qts, adc);
> +
> +    /*
> +     * ADC should takes roughly 20 cycles to convert one sample. So we
> assert it
> +     * should take 10~30 cycles here.
> +     */
> +    qtest_clock_step(qts, adc_calculate_steps(CONV_CYCLES / 2, prescaler,
> +                clkdiv));
> +    /* ADC is still converting. */
> +    g_assert_true(adc_read_con(qts, adc) & CON_CONV);
> +    qtest_clock_step(qts, adc_calculate_steps(CONV_CYCLES, prescaler,
> clkdiv));
> +    /* ADC has finished conversion. */
> +    g_assert_false(adc_read_con(qts, adc) & CON_CONV);
> +}
> +
> +/* Check ADC can be reset to default value. */
> +static void test_init(gconstpointer adc_p)
> +{
> +    const ADC *adc = adc_p;
> +
> +    QTestState *qts = qtest_init("-machine quanta-gsj");
> +    adc_write_con(qts, adc, CON_REFSEL | CON_INT);
> +    g_assert_cmphex(adc_read_con(qts, adc), ==, CON_REFSEL);
> +    qtest_quit(qts);
> +}
> +
> +/* Check ADC can convert from an internal reference. */
> +static void test_convert_internal(gconstpointer adc_p)
> +{
> +    const ADC *adc = adc_p;
> +    uint32_t index, input, output, expected_output;
> +    QTestState *qts = qtest_init("-machine quanta-gsj");
> +    qtest_irq_intercept_in(qts, "/machine/soc/a9mpcore/gic");
> +
> +    for (index = 0; index < NUM_INPUTS; ++index) {
> +        for (size_t i = 0; i < ARRAY_SIZE(input_list); ++i) {
> +            input = input_list[i];
> +            expected_output = adc_calculate_output(input, DEFAULT_IREF);
> +
> +            adc_write_input(qts, adc, index, input);
> +            adc_write_con(qts, adc, CON_MUX(index) | CON_REFSEL | CON_INT
> |
> +                    CON_EN | CON_CONV);
> +            adc_wait_conv_finished(qts, adc, DEFAULT_CLKDIV);
> +            g_assert_cmphex(adc_read_con(qts, adc), ==, CON_MUX(index) |
> +                    CON_REFSEL | CON_EN);
> +            g_assert_false(qtest_get_irq(qts, adc->irq));
> +            output = adc_read_data(qts, adc);
> +            g_assert_cmpuint(output, ==, expected_output);
> +        }
> +    }
> +
> +    qtest_quit(qts);
> +}
> +
> +/* Check ADC can convert from an external reference. */
> +static void test_convert_external(gconstpointer adc_p)
> +{
> +    const ADC *adc = adc_p;
> +    uint32_t index, input, vref, output, expected_output;
> +    QTestState *qts = qtest_init("-machine quanta-gsj");
> +    qtest_irq_intercept_in(qts, "/machine/soc/a9mpcore/gic");
> +
> +    for (index = 0; index < NUM_INPUTS; ++index) {
> +        for (size_t i = 0; i < ARRAY_SIZE(input_list); ++i) {
> +            for (size_t j = 0; j < ARRAY_SIZE(vref_list); ++j) {
> +                input = input_list[i];
> +                vref = vref_list[j];
> +                expected_output = adc_calculate_output(input, vref);
> +
> +                adc_write_input(qts, adc, index, input);
> +                adc_write_vref(qts, adc, vref);
> +                adc_write_con(qts, adc, CON_MUX(index) | CON_INT | CON_EN
> |
> +                        CON_CONV);
> +                adc_wait_conv_finished(qts, adc, DEFAULT_CLKDIV);
> +                g_assert_cmphex(adc_read_con(qts, adc), ==,
> +                        CON_MUX(index) | CON_EN);
> +                g_assert_false(qtest_get_irq(qts, adc->irq));
> +                output = adc_read_data(qts, adc);
> +                g_assert_cmpuint(output, ==, expected_output);
> +            }
> +        }
> +    }
> +
> +    qtest_quit(qts);
> +}
> +
> +/* Check ADC interrupt files if and only if CON_INT_EN is set. */
> +static void test_interrupt(gconstpointer adc_p)
> +{
> +    const ADC *adc = adc_p;
> +    uint32_t index, input, output, expected_output;
> +    QTestState *qts = qtest_init("-machine quanta-gsj");
> +
> +    index = 1;
> +    input = input_list[1];
> +    expected_output = adc_calculate_output(input, DEFAULT_IREF);
> +
> +    qtest_irq_intercept_in(qts, "/machine/soc/a9mpcore/gic");
> +    adc_write_input(qts, adc, index, input);
> +    g_assert_false(qtest_get_irq(qts, adc->irq));
> +    adc_write_con(qts, adc, CON_MUX(index) | CON_INT_EN | CON_REFSEL |
> CON_INT
> +            | CON_EN | CON_CONV);
> +    adc_wait_conv_finished(qts, adc, DEFAULT_CLKDIV);
> +    g_assert_cmphex(adc_read_con(qts, adc), ==, CON_MUX(index) |
> CON_INT_EN
> +            | CON_REFSEL | CON_INT | CON_EN);
> +    g_assert_true(qtest_get_irq(qts, adc->irq));
> +    output = adc_read_data(qts, adc);
> +    g_assert_cmpuint(output, ==, expected_output);
> +
> +    qtest_quit(qts);
> +}
> +
> +/* Check ADC is reset after setting ADC_RST for 10 ADC cycles. */
> +static void test_reset(gconstpointer adc_p)
> +{
> +    const ADC *adc = adc_p;
> +    QTestState *qts = qtest_init("-machine quanta-gsj");
> +
> +    for (size_t i = 0; i < ARRAY_SIZE(div_list); ++i) {
> +        uint32_t div = div_list[i];
> +
> +        adc_write_con(qts, adc, CON_INT | CON_EN | CON_RST |
> CON_DIV(div));
> +        qtest_clock_step(qts, adc_calculate_steps(RESET_CYCLES,
> +                    adc_prescaler(qts, adc), DEFAULT_CLKDIV) - 1);
> +        g_assert_true(adc_read_con(qts, adc) & CON_EN);
> +        qtest_clock_step(qts, 1);
> +        g_assert_false(adc_read_con(qts, adc) & CON_EN);
> +    }
> +    qtest_quit(qts);
> +}
> +
> +/* Check ADC is not reset if we set ADC_RST for <10 ADC cycles. */
> +static void test_premature_reset(gconstpointer adc_p)
> +{
> +    const ADC *adc = adc_p;
> +    QTestState *qts = qtest_init("-machine quanta-gsj");
> +
> +    for (size_t i = 0; i < ARRAY_SIZE(div_list); ++i) {
> +        uint32_t div = div_list[i];
> +
> +        adc_write_con(qts, adc, CON_INT | CON_EN | CON_RST |
> CON_DIV(div));
> +        qtest_clock_step(qts, adc_calculate_steps(RESET_CYCLES,
> +                    adc_prescaler(qts, adc), DEFAULT_CLKDIV) - 1);
> +        g_assert_true(adc_read_con(qts, adc) & CON_EN);
> +        adc_write_con(qts, adc, CON_INT | CON_EN | CON_DIV(div));
> +        qtest_clock_step(qts, 1000);
> +        g_assert_true(adc_read_con(qts, adc) & CON_EN);
> +    }
> +    qtest_quit(qts);
> +}
> +
> +/* Check ADC Calibration works as desired. */
> +static void test_calibrate(gconstpointer adc_p)
> +{
> +    int i, j;
> +    const ADC *adc = adc_p;
> +
> +    for (j = 0; j < ARRAY_SIZE(iref_list); ++j) {
> +        uint32_t iref = iref_list[j];
> +        uint32_t expected_rv[] = {
> +            adc_calculate_output(R0_INPUT, iref),
> +            adc_calculate_output(R1_INPUT, iref),
> +        };
> +        char buf[100];
> +        QTestState *qts;
> +
> +        sprintf(buf, "-machine quanta-gsj -global npcm7xx-adc.iref=%u",
> iref);
> +        qts = qtest_init(buf);
> +
> +        /* Check the converted value is correct using the calibration
> value. */
> +        for (i = 0; i < ARRAY_SIZE(input_list); ++i) {
> +            uint32_t input;
> +            uint32_t output;
> +            uint32_t expected_output;
> +            uint32_t calibrated_voltage;
> +            uint32_t index = 0;
> +
> +            input = input_list[i];
> +            /* Calibration only works for input range 0.1V ~ 1.8V. */
> +            if (input < MIN_CALIB_INPUT || input > MAX_CALIB_INPUT) {
> +                continue;
> +            }
> +            expected_output = adc_calculate_output(input, iref);
> +
> +            adc_write_input(qts, adc, index, input);
> +            adc_write_con(qts, adc, CON_MUX(index) | CON_REFSEL | CON_INT
> |
> +                    CON_EN | CON_CONV);
> +            adc_wait_conv_finished(qts, adc, DEFAULT_CLKDIV);
> +            g_assert_cmphex(adc_read_con(qts, adc), ==,
> +                    CON_REFSEL | CON_MUX(index) | CON_EN);
> +            output = adc_read_data(qts, adc);
> +            g_assert_cmpuint(output, ==, expected_output);
> +
> +            calibrated_voltage = adc_calibrate(output, expected_rv);
> +            g_assert_cmpuint(calibrated_voltage, >, input - MAX_ERROR);
> +            g_assert_cmpuint(calibrated_voltage, <, input + MAX_ERROR);
> +        }
> +
> +        qtest_quit(qts);
> +    }
> +}
> +
> +static void adc_add_test(const char *name, const ADC* wd,
> +        GTestDataFunc fn)
> +{
> +    g_autofree char *full_name = g_strdup_printf("npcm7xx_adc/%s",  name);
> +    qtest_add_data_func(full_name, wd, fn);
> +}
> +#define add_test(name, td) adc_add_test(#name, td, test_##name)
> +
> +int main(int argc, char **argv)
> +{
> +    g_test_init(&argc, &argv, NULL);
> +
> +    add_test(init, &adc);
> +    add_test(convert_internal, &adc);
> +    add_test(convert_external, &adc);
> +    add_test(interrupt, &adc);
> +    add_test(reset, &adc);
> +    add_test(premature_reset, &adc);
> +    add_test(calibrate, &adc);
> +
> +    return g_test_run();
> +}
> --
> 2.29.2.684.gfbc64c5ab5-goog
>
>
diff mbox series

Patch

diff --git a/docs/system/arm/nuvoton.rst b/docs/system/arm/nuvoton.rst
index b00d405d52..35829f8d0b 100644
--- a/docs/system/arm/nuvoton.rst
+++ b/docs/system/arm/nuvoton.rst
@@ -41,6 +41,7 @@  Supported devices
  * Random Number Generator (RNG)
  * USB host (USBH)
  * GPIO controller
+ * Analog to Digital Converter (ADC)
 
 Missing devices
 ---------------
@@ -58,7 +59,6 @@  Missing devices
  * USB device (USBD)
  * SMBus controller (SMBF)
  * Peripheral SPI controller (PSPI)
- * Analog to Digital Converter (ADC)
  * SD/MMC host
  * PECI interface
  * Pulse Width Modulation (PWM)
diff --git a/hw/adc/meson.build b/hw/adc/meson.build
index 0d62ae96ae..6ddee23813 100644
--- a/hw/adc/meson.build
+++ b/hw/adc/meson.build
@@ -1 +1,2 @@ 
 softmmu_ss.add(when: 'CONFIG_STM32F2XX_ADC', if_true: files('stm32f2xx_adc.c'))
+softmmu_ss.add(when: 'CONFIG_NPCM7XX', if_true: files('npcm7xx_adc.c'))
diff --git a/hw/adc/npcm7xx_adc.c b/hw/adc/npcm7xx_adc.c
new file mode 100644
index 0000000000..c2c4819d3f
--- /dev/null
+++ b/hw/adc/npcm7xx_adc.c
@@ -0,0 +1,321 @@ 
+/*
+ * Nuvoton NPCM7xx ADC Module
+ *
+ * Copyright 2020 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 "hw/adc/npcm7xx_adc.h"
+#include "hw/qdev-clock.h"
+#include "hw/qdev-properties.h"
+#include "migration/vmstate.h"
+#include "qemu/log.h"
+#include "qemu/module.h"
+#include "qemu/timer.h"
+#include "qemu/units.h"
+#include "trace.h"
+
+/* 32-bit register indices. */
+enum NPCM7xxADCRegisters {
+    NPCM7XX_ADC_CON,
+    NPCM7XX_ADC_DATA,
+    NPCM7XX_ADC_REGS_END,
+};
+
+/* Register field definitions. */
+#define NPCM7XX_ADC_CON_MUX(rv) extract32(rv, 24, 4)
+#define NPCM7XX_ADC_CON_INT_EN  BIT(21)
+#define NPCM7XX_ADC_CON_REFSEL  BIT(19)
+#define NPCM7XX_ADC_CON_INT     BIT(18)
+#define NPCM7XX_ADC_CON_EN      BIT(17)
+#define NPCM7XX_ADC_CON_RST     BIT(16)
+#define NPCM7XX_ADC_CON_CONV    BIT(14)
+#define NPCM7XX_ADC_CON_DIV(rv) extract32(rv, 1, 8)
+
+#define NPCM7XX_ADC_MAX_RESULT      1023
+#define NPCM7XX_ADC_DEFAULT_IREF    2000000
+#define NPCM7XX_ADC_CONV_CYCLES     20
+#define NPCM7XX_ADC_RESET_CYCLES    10
+#define NPCM7XX_ADC_R0_INPUT        500000
+#define NPCM7XX_ADC_R1_INPUT        1500000
+
+static void npcm7xx_adc_reset(NPCM7xxADCState *s)
+{
+    timer_del(&s->conv_timer);
+    timer_del(&s->reset_timer);
+    s->con = 0x000c0001;
+    s->data = 0x00000000;
+}
+
+static uint32_t npcm7xx_adc_convert(uint32_t input, uint32_t ref)
+{
+    uint32_t result;
+
+    result = input * (NPCM7XX_ADC_MAX_RESULT + 1) / ref;
+    if (result > NPCM7XX_ADC_MAX_RESULT) {
+        result = NPCM7XX_ADC_MAX_RESULT;
+    }
+
+    return result;
+}
+
+static uint32_t npcm7xx_adc_prescaler(NPCM7xxADCState *s)
+{
+    return 2 * (NPCM7XX_ADC_CON_DIV(s->con) + 1);
+}
+
+static void npcm7xx_adc_start_timer(Clock *clk, QEMUTimer *timer,
+        uint32_t cycles, uint32_t prescaler)
+{
+    int64_t now = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL);
+    int64_t freq = clock_get_hz(clk);
+    int64_t ns;
+
+    ns = (NANOSECONDS_PER_SECOND * cycles * prescaler / freq);
+    ns += now;
+    timer_mod(timer, ns);
+}
+
+static void npcm7xx_adc_start_reset(NPCM7xxADCState *s)
+{
+    uint32_t prescaler = npcm7xx_adc_prescaler(s);
+
+    npcm7xx_adc_start_timer(s->clock, &s->reset_timer, NPCM7XX_ADC_RESET_CYCLES,
+            prescaler);
+}
+
+static void npcm7xx_adc_start_convert(NPCM7xxADCState *s)
+{
+    uint32_t prescaler = npcm7xx_adc_prescaler(s);
+
+    npcm7xx_adc_start_timer(s->clock, &s->conv_timer, NPCM7XX_ADC_CONV_CYCLES,
+            prescaler);
+}
+
+static void npcm7xx_adc_reset_done(void *opaque)
+{
+    NPCM7xxADCState *s = opaque;
+
+    npcm7xx_adc_reset(s);
+}
+
+static void npcm7xx_adc_convert_done(void *opaque)
+{
+    NPCM7xxADCState *s = opaque;
+    uint32_t input = NPCM7XX_ADC_CON_MUX(s->con);
+    uint32_t ref = (s->con & NPCM7XX_ADC_CON_REFSEL)
+        ? s->iref : s->vref;
+
+    g_assert(input < NPCM7XX_ADC_NUM_INPUTS);
+    s->data = npcm7xx_adc_convert(s->adci[input], ref);
+    if (s->con & NPCM7XX_ADC_CON_INT_EN) {
+        s->con |= NPCM7XX_ADC_CON_INT;
+        qemu_irq_raise(s->irq);
+    }
+    s->con &= ~NPCM7XX_ADC_CON_CONV;
+}
+
+static void npcm7xx_adc_calibrate(NPCM7xxADCState *adc)
+{
+    adc->calibration_r_values[0] = npcm7xx_adc_convert(NPCM7XX_ADC_R0_INPUT,
+            adc->iref);
+    adc->calibration_r_values[1] = npcm7xx_adc_convert(NPCM7XX_ADC_R1_INPUT,
+            adc->iref);
+}
+
+static void npcm7xx_adc_write_con(NPCM7xxADCState *s, uint32_t new_con)
+{
+    uint32_t old_con = s->con;
+
+    /* Write ADC_INT to 1 to clear it */
+    if (new_con & NPCM7XX_ADC_CON_INT) {
+        new_con &= ~NPCM7XX_ADC_CON_INT;
+    } else if (old_con & NPCM7XX_ADC_CON_INT) {
+        new_con |= NPCM7XX_ADC_CON_INT;
+    }
+
+    s->con = new_con;
+
+    if (s->con & NPCM7XX_ADC_CON_RST) {
+        if (!(old_con & NPCM7XX_ADC_CON_RST)) {
+            npcm7xx_adc_start_reset(s);
+        }
+    } else {
+        timer_del(&s->reset_timer);
+    }
+
+    if ((s->con & NPCM7XX_ADC_CON_EN)) {
+        if (s->con & NPCM7XX_ADC_CON_CONV) {
+            if (!(old_con & NPCM7XX_ADC_CON_CONV)) {
+                npcm7xx_adc_start_convert(s);
+            }
+        } else {
+            timer_del(&s->conv_timer);
+        }
+    }
+}
+
+static uint64_t npcm7xx_adc_read(void *opaque, hwaddr offset, unsigned size)
+{
+    uint64_t value = 0;
+    NPCM7xxADCState *s = opaque;
+    hwaddr reg = offset / sizeof(uint32_t);
+
+    switch (reg) {
+    case NPCM7XX_ADC_CON:
+        value = s->con;
+        break;
+
+    case NPCM7XX_ADC_DATA:
+        value = s->data;
+        break;
+
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: invalid offset 0x%04" HWADDR_PRIx "\n",
+                      __func__, offset);
+        break;
+    }
+
+    trace_npcm7xx_adc_read(DEVICE(s)->canonical_path, offset, value);
+    return value;
+}
+
+static void npcm7xx_adc_write(void *opaque, hwaddr offset, uint64_t v,
+        unsigned size)
+{
+    NPCM7xxADCState *s = opaque;
+    hwaddr reg = offset / sizeof(uint32_t);
+
+    trace_npcm7xx_adc_write(DEVICE(s)->canonical_path, offset, v);
+    switch (reg) {
+    case NPCM7XX_ADC_CON:
+        npcm7xx_adc_write_con(s, v);
+        break;
+
+    case NPCM7XX_ADC_DATA:
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: register @ 0x%04" HWADDR_PRIx " is read-only\n",
+                      __func__, offset);
+        break;
+
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: invalid offset 0x%04" HWADDR_PRIx "\n",
+                      __func__, offset);
+        break;
+    }
+
+}
+
+static const struct MemoryRegionOps npcm7xx_adc_ops = {
+    .read       = npcm7xx_adc_read,
+    .write      = npcm7xx_adc_write,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+    .valid      = {
+        .min_access_size        = 4,
+        .max_access_size        = 4,
+        .unaligned              = false,
+    },
+};
+
+static void npcm7xx_adc_enter_reset(Object *obj, ResetType type)
+{
+    NPCM7xxADCState *s = NPCM7XX_ADC(obj);
+
+    npcm7xx_adc_reset(s);
+}
+
+static void npcm7xx_adc_hold_reset(Object *obj)
+{
+    NPCM7xxADCState *s = NPCM7XX_ADC(obj);
+
+    qemu_irq_lower(s->irq);
+}
+
+static void npcm7xx_adc_init(Object *obj)
+{
+    NPCM7xxADCState *s = NPCM7XX_ADC(obj);
+    SysBusDevice *sbd = &s->parent;
+    int i;
+
+    sysbus_init_irq(sbd, &s->irq);
+
+    timer_init_ns(&s->conv_timer, QEMU_CLOCK_VIRTUAL,
+            npcm7xx_adc_convert_done, s);
+    timer_init_ns(&s->reset_timer, QEMU_CLOCK_VIRTUAL,
+            npcm7xx_adc_reset_done, s);
+    memory_region_init_io(&s->iomem, obj, &npcm7xx_adc_ops, s,
+                          TYPE_NPCM7XX_ADC, 4 * KiB);
+    sysbus_init_mmio(sbd, &s->iomem);
+    s->clock = qdev_init_clock_in(DEVICE(s), "clock", NULL, NULL);
+
+    for (i = 0; i < NPCM7XX_ADC_NUM_INPUTS; ++i) {
+        object_property_add_uint32_ptr(obj, "adci[*]",
+                &s->adci[i], OBJ_PROP_FLAG_WRITE);
+    }
+    object_property_add_uint32_ptr(obj, "vref",
+            &s->vref, OBJ_PROP_FLAG_WRITE);
+    npcm7xx_adc_calibrate(s);
+}
+
+static const VMStateDescription vmstate_npcm7xx_adc = {
+    .name = "npcm7xx-adc",
+    .version_id = 0,
+    .minimum_version_id = 0,
+    .fields = (VMStateField[]) {
+        VMSTATE_TIMER(conv_timer, NPCM7xxADCState),
+        VMSTATE_TIMER(reset_timer, NPCM7xxADCState),
+        VMSTATE_UINT32(con, NPCM7xxADCState),
+        VMSTATE_UINT32(data, NPCM7xxADCState),
+        VMSTATE_CLOCK(clock, NPCM7xxADCState),
+        VMSTATE_UINT32_ARRAY(adci, NPCM7xxADCState, NPCM7XX_ADC_NUM_INPUTS),
+        VMSTATE_UINT32(vref, NPCM7xxADCState),
+        VMSTATE_UINT32(iref, NPCM7xxADCState),
+        VMSTATE_UINT16_ARRAY(calibration_r_values, NPCM7xxADCState,
+                NPCM7XX_ADC_NUM_CALIB),
+        VMSTATE_END_OF_LIST(),
+    },
+};
+
+static Property npcm7xx_timer_properties[] = {
+    DEFINE_PROP_UINT32("iref", NPCM7xxADCState, iref, NPCM7XX_ADC_DEFAULT_IREF),
+    DEFINE_PROP_END_OF_LIST(),
+};
+
+static void npcm7xx_adc_class_init(ObjectClass *klass, void *data)
+{
+    ResettableClass *rc = RESETTABLE_CLASS(klass);
+    DeviceClass *dc = DEVICE_CLASS(klass);
+
+    dc->desc = "NPCM7xx ADC Module";
+    dc->vmsd = &vmstate_npcm7xx_adc;
+    rc->phases.enter = npcm7xx_adc_enter_reset;
+    rc->phases.hold = npcm7xx_adc_hold_reset;
+
+    device_class_set_props(dc, npcm7xx_timer_properties);
+}
+
+static const TypeInfo npcm7xx_adc_info = {
+    .name               = TYPE_NPCM7XX_ADC,
+    .parent             = TYPE_SYS_BUS_DEVICE,
+    .instance_size      = sizeof(NPCM7xxADCState),
+    .class_init         = npcm7xx_adc_class_init,
+    .instance_init      = npcm7xx_adc_init,
+};
+
+static void npcm7xx_adc_register_types(void)
+{
+    type_register_static(&npcm7xx_adc_info);
+}
+
+type_init(npcm7xx_adc_register_types);
diff --git a/hw/adc/trace-events b/hw/adc/trace-events
new file mode 100644
index 0000000000..4c3279ece2
--- /dev/null
+++ b/hw/adc/trace-events
@@ -0,0 +1,5 @@ 
+# See docs/devel/tracing.txt for syntax documentation.
+
+# npcm7xx_adc.c
+npcm7xx_adc_read(const char *id, uint64_t offset, uint32_t value) " %s offset: 0x%04" PRIx64 " value 0x%04" PRIx32
+npcm7xx_adc_write(const char *id, uint64_t offset, uint32_t value) "%s offset: 0x%04" PRIx64 " value 0x%04" PRIx32
diff --git a/hw/arm/npcm7xx.c b/hw/arm/npcm7xx.c
index fabfb1697b..b22a8c966d 100644
--- a/hw/arm/npcm7xx.c
+++ b/hw/arm/npcm7xx.c
@@ -51,6 +51,9 @@ 
 #define NPCM7XX_EHCI_BA         (0xf0806000)
 #define NPCM7XX_OHCI_BA         (0xf0807000)
 
+/* ADC Module */
+#define NPCM7XX_ADC_BA          (0xf000c000)
+
 /* Internal AHB SRAM */
 #define NPCM7XX_RAM3_BA         (0xc0008000)
 #define NPCM7XX_RAM3_SZ         (4 * KiB)
@@ -61,6 +64,7 @@ 
 #define NPCM7XX_ROM_BA          (0xffff0000)
 #define NPCM7XX_ROM_SZ          (64 * KiB)
 
+
 /* Clock configuration values to be fixed up when bypassing bootloader */
 
 /* Run PLL1 at 1600 MHz */
@@ -73,6 +77,7 @@ 
  * interrupts.
  */
 enum NPCM7xxInterrupt {
+    NPCM7XX_ADC_IRQ             = 0,
     NPCM7XX_UART0_IRQ           = 2,
     NPCM7XX_UART1_IRQ,
     NPCM7XX_UART2_IRQ,
@@ -296,6 +301,14 @@  static void npcm7xx_init_fuses(NPCM7xxState *s)
                             sizeof(value));
 }
 
+static void npcm7xx_write_adc_calibration(NPCM7xxState *s)
+{
+    /* Both ADC and the fuse array must have realized. */
+    QEMU_BUILD_BUG_ON(sizeof(s->adc.calibration_r_values) != 4);
+    npcm7xx_otp_array_write(&s->fuse_array, s->adc.calibration_r_values,
+            NPCM7XX_FUSE_ADC_CALIB, sizeof(s->adc.calibration_r_values));
+}
+
 static qemu_irq npcm7xx_irq(NPCM7xxState *s, int n)
 {
     return qdev_get_gpio_in(DEVICE(&s->a9mpcore), n);
@@ -322,6 +335,7 @@  static void npcm7xx_init(Object *obj)
                             TYPE_NPCM7XX_FUSE_ARRAY);
     object_initialize_child(obj, "mc", &s->mc, TYPE_NPCM7XX_MC);
     object_initialize_child(obj, "rng", &s->rng, TYPE_NPCM7XX_RNG);
+    object_initialize_child(obj, "adc", &s->adc, TYPE_NPCM7XX_ADC);
 
     for (i = 0; i < ARRAY_SIZE(s->tim); i++) {
         object_initialize_child(obj, "tim[*]", &s->tim[i], TYPE_NPCM7XX_TIMER);
@@ -414,6 +428,15 @@  static void npcm7xx_realize(DeviceState *dev, Error **errp)
     sysbus_realize(SYS_BUS_DEVICE(&s->mc), &error_abort);
     sysbus_mmio_map(SYS_BUS_DEVICE(&s->mc), 0, NPCM7XX_MC_BA);
 
+    /* ADC Modules. Cannot fail. */
+    qdev_connect_clock_in(DEVICE(&s->adc), "clock", qdev_get_clock_out(
+                          DEVICE(&s->clk), "adc-clock"));
+    sysbus_realize(SYS_BUS_DEVICE(&s->adc), &error_abort);
+    sysbus_mmio_map(SYS_BUS_DEVICE(&s->adc), 0, NPCM7XX_ADC_BA);
+    sysbus_connect_irq(SYS_BUS_DEVICE(&s->adc), 0,
+            npcm7xx_irq(s, NPCM7XX_ADC_IRQ));
+    npcm7xx_write_adc_calibration(s);
+
     /* Timer Modules (TIM). Cannot fail. */
     QEMU_BUILD_BUG_ON(ARRAY_SIZE(npcm7xx_tim_addr) != ARRAY_SIZE(s->tim));
     for (i = 0; i < ARRAY_SIZE(s->tim); i++) {
@@ -528,7 +551,6 @@  static void npcm7xx_realize(DeviceState *dev, Error **errp)
     create_unimplemented_device("npcm7xx.vdmx",         0xe0800000,   4 * KiB);
     create_unimplemented_device("npcm7xx.pcierc",       0xe1000000,  64 * KiB);
     create_unimplemented_device("npcm7xx.kcs",          0xf0007000,   4 * KiB);
-    create_unimplemented_device("npcm7xx.adc",          0xf000c000,   4 * KiB);
     create_unimplemented_device("npcm7xx.gfxi",         0xf000e000,   4 * KiB);
     create_unimplemented_device("npcm7xx.gpio[0]",      0xf0010000,   4 * KiB);
     create_unimplemented_device("npcm7xx.gpio[1]",      0xf0011000,   4 * KiB);
diff --git a/include/hw/adc/npcm7xx_adc.h b/include/hw/adc/npcm7xx_adc.h
new file mode 100644
index 0000000000..7f9acbeaa1
--- /dev/null
+++ b/include/hw/adc/npcm7xx_adc.h
@@ -0,0 +1,72 @@ 
+/*
+ * Nuvoton NPCM7xx ADC Module
+ *
+ * Copyright 2020 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.
+ */
+#ifndef NPCM7XX_ADC_H
+#define NPCM7XX_ADC_H
+
+#include "qemu/osdep.h"
+#include "hw/clock.h"
+#include "hw/irq.h"
+#include "hw/sysbus.h"
+#include "qemu/timer.h"
+
+#define NPCM7XX_ADC_NUM_INPUTS      8
+/**
+ * This value should not be changed unless write_adc_calibration function in
+ * hw/arm/npcm7xx.c is also changed.
+ */
+#define NPCM7XX_ADC_NUM_CALIB       2
+
+/**
+ * struct NPCM7xxADCState - Analog to Digital Converter Module device state.
+ * @parent: System bus device.
+ * @iomem: Memory region through which registers are accessed.
+ * @conv_timer: The timer counts down remaining cycles for the conversion.
+ * @reset_timer: The timer counts down remaining cycles for reset.
+ * @irq: GIC interrupt line to fire on expiration (if enabled).
+ * @con: The Control Register.
+ * @data: The Data Buffer.
+ * @clock: The ADC Clock.
+ * @adci: The input voltage in units of uV. 1uv = 1e-6V.
+ * @vref: The external reference voltage.
+ * @iref: The internal reference voltage, initialized at launch time.
+ * @rv: The calibrated output values of 0.5V and 1.5V for the ADC.
+ */
+typedef struct {
+    SysBusDevice parent;
+
+    MemoryRegion iomem;
+
+    QEMUTimer    conv_timer;
+    QEMUTimer    reset_timer;
+
+    qemu_irq     irq;
+    uint32_t     con;
+    uint32_t     data;
+    Clock       *clock;
+
+    /* Voltages are in unit of uV. 1V = 1000000uV. */
+    uint32_t     adci[NPCM7XX_ADC_NUM_INPUTS];
+    uint32_t     vref;
+    uint32_t     iref;
+
+    uint16_t     calibration_r_values[NPCM7XX_ADC_NUM_CALIB];
+} NPCM7xxADCState;
+
+#define TYPE_NPCM7XX_ADC "npcm7xx-adc"
+#define NPCM7XX_ADC(obj) \
+    OBJECT_CHECK(NPCM7xxADCState, (obj), TYPE_NPCM7XX_ADC)
+
+#endif /* NPCM7XX_ADC_H */
diff --git a/include/hw/arm/npcm7xx.h b/include/hw/arm/npcm7xx.h
index 5469247e38..51e1c7620d 100644
--- a/include/hw/arm/npcm7xx.h
+++ b/include/hw/arm/npcm7xx.h
@@ -17,6 +17,7 @@ 
 #define NPCM7XX_H
 
 #include "hw/boards.h"
+#include "hw/adc/npcm7xx_adc.h"
 #include "hw/cpu/a9mpcore.h"
 #include "hw/gpio/npcm7xx_gpio.h"
 #include "hw/mem/npcm7xx_mc.h"
@@ -76,6 +77,7 @@  typedef struct NPCM7xxState {
     NPCM7xxGCRState     gcr;
     NPCM7xxCLKState     clk;
     NPCM7xxTimerCtrlState tim[3];
+    NPCM7xxADCState     adc;
     NPCM7xxOTPState     key_storage;
     NPCM7xxOTPState     fuse_array;
     NPCM7xxMCState      mc;
diff --git a/meson.build b/meson.build
index f344b25955..fb03cdbdcc 100644
--- a/meson.build
+++ b/meson.build
@@ -1435,6 +1435,7 @@  if have_system
     'chardev',
     'hw/9pfs',
     'hw/acpi',
+    'hw/adc',
     'hw/alpha',
     'hw/arm',
     'hw/audio',
diff --git a/tests/qtest/meson.build b/tests/qtest/meson.build
index 6a67c538be..955710d1c5 100644
--- a/tests/qtest/meson.build
+++ b/tests/qtest/meson.build
@@ -134,7 +134,8 @@  qtests_sparc64 = \
   ['prom-env-test', 'boot-serial-test']
 
 qtests_npcm7xx = \
-  ['npcm7xx_gpio-test',
+  ['npcm7xx_adc-test',
+   'npcm7xx_gpio-test',
    'npcm7xx_rng-test',
    'npcm7xx_timer-test',
    'npcm7xx_watchdog_timer-test']
diff --git a/tests/qtest/npcm7xx_adc-test.c b/tests/qtest/npcm7xx_adc-test.c
new file mode 100644
index 0000000000..e63c544e51
--- /dev/null
+++ b/tests/qtest/npcm7xx_adc-test.c
@@ -0,0 +1,400 @@ 
+/*
+ * QTests for Nuvoton NPCM7xx ADCModules.
+ *
+ * Copyright 2020 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 "qemu/bitops.h"
+#include "qemu/timer.h"
+#include "libqos/libqtest.h"
+#include "qapi/qmp/qdict.h"
+
+#define REF_HZ          (25000000)
+
+#define CON_OFFSET      0x0
+#define DATA_OFFSET     0x4
+
+#define NUM_INPUTS      8
+#define DEFAULT_IREF    2000000
+#define CONV_CYCLES     20
+#define RESET_CYCLES    10
+#define R0_INPUT        500000
+#define R1_INPUT        1500000
+#define MAX_RESULT      1023
+
+#define DEFAULT_CLKDIV  5
+
+#define FUSE_ARRAY_BA   0xf018a000
+#define FCTL_OFFSET     0x14
+#define FST_OFFSET      0x0
+#define FADDR_OFFSET    0x4
+#define FDATA_OFFSET    0x8
+#define ADC_CALIB_ADDR  24
+#define FUSE_READ       0x2
+
+/* Register field definitions. */
+#define CON_MUX(rv) ((rv) << 24)
+#define CON_INT_EN  BIT(21)
+#define CON_REFSEL  BIT(19)
+#define CON_INT     BIT(18)
+#define CON_EN      BIT(17)
+#define CON_RST     BIT(16)
+#define CON_CONV    BIT(14)
+#define CON_DIV(rv) extract32(rv, 1, 8)
+
+#define FST_RDST    BIT(1)
+#define FDATA_MASK  0xff
+
+#define MAX_ERROR   10000
+#define MIN_CALIB_INPUT 100000
+#define MAX_CALIB_INPUT 1800000
+
+static const uint32_t input_list[] = {
+    100000,
+    500000,
+    1000000,
+    1500000,
+    1800000,
+    2000000,
+};
+
+static const uint32_t vref_list[] = {
+    2000000,
+    2200000,
+    2500000,
+};
+
+static const uint32_t iref_list[] = {
+    1800000,
+    1900000,
+    2000000,
+    2100000,
+    2200000,
+};
+
+static const uint32_t div_list[] = {0, 1, 3, 7, 15};
+
+typedef struct ADC {
+    int irq;
+    uint64_t base_addr;
+} ADC;
+
+ADC adc = {
+    .irq        = 0,
+    .base_addr  = 0xf000c000
+};
+
+static uint32_t adc_read_con(QTestState *qts, const ADC *adc)
+{
+    return qtest_readl(qts, adc->base_addr + CON_OFFSET);
+}
+
+static void adc_write_con(QTestState *qts, const ADC *adc, uint32_t value)
+{
+    qtest_writel(qts, adc->base_addr + CON_OFFSET, value);
+}
+
+static uint32_t adc_read_data(QTestState *qts, const ADC *adc)
+{
+    return qtest_readl(qts, adc->base_addr + DATA_OFFSET);
+}
+
+static uint32_t adc_calibrate(uint32_t measured, uint32_t *rv)
+{
+    return R0_INPUT + (R1_INPUT - R0_INPUT) * (int32_t)(measured - rv[0])
+        / (int32_t)(rv[1] - rv[0]);
+}
+
+static void adc_qom_set(QTestState *qts, const ADC *adc,
+        const char *name, uint32_t value)
+{
+    QDict *response;
+    const char *path = "/machine/soc/adc";
+
+    g_test_message("Setting properties %s of %s with value %u",
+            name, path, value);
+    response = qtest_qmp(qts, "{ 'execute': 'qom-set',"
+            " 'arguments': { 'path': %s, 'property': %s, 'value': %u}}",
+            path, name, value);
+    /* The qom set message returns successfully. */
+    g_assert_true(qdict_haskey(response, "return"));
+}
+
+static void adc_write_input(QTestState *qts, const ADC *adc,
+        uint32_t index, uint32_t value)
+{
+    char name[100];
+
+    sprintf(name, "adci[%u]", index);
+    adc_qom_set(qts, adc, name, value);
+}
+
+static void adc_write_vref(QTestState *qts, const ADC *adc, uint32_t value)
+{
+    adc_qom_set(qts, adc, "vref", value);
+}
+
+static uint32_t adc_calculate_output(uint32_t input, uint32_t ref)
+{
+    uint32_t output;
+
+    g_assert_cmpuint(input, <=, ref);
+    output = (input * (MAX_RESULT + 1)) / ref;
+    if (output > MAX_RESULT) {
+        output = MAX_RESULT;
+    }
+
+    return output;
+}
+
+static uint32_t adc_prescaler(QTestState *qts, const ADC *adc)
+{
+    uint32_t div = extract32(adc_read_con(qts, adc), 1, 8);
+
+    return 2 * (div + 1);
+}
+
+static int64_t adc_calculate_steps(uint32_t cycles, uint32_t prescale,
+        uint32_t clkdiv)
+{
+    return (NANOSECONDS_PER_SECOND / (REF_HZ >> clkdiv)) * cycles * prescale;
+}
+
+static void adc_wait_conv_finished(QTestState *qts, const ADC *adc,
+        uint32_t clkdiv)
+{
+    uint32_t prescaler = adc_prescaler(qts, adc);
+
+    /*
+     * ADC should takes roughly 20 cycles to convert one sample. So we assert it
+     * should take 10~30 cycles here.
+     */
+    qtest_clock_step(qts, adc_calculate_steps(CONV_CYCLES / 2, prescaler,
+                clkdiv));
+    /* ADC is still converting. */
+    g_assert_true(adc_read_con(qts, adc) & CON_CONV);
+    qtest_clock_step(qts, adc_calculate_steps(CONV_CYCLES, prescaler, clkdiv));
+    /* ADC has finished conversion. */
+    g_assert_false(adc_read_con(qts, adc) & CON_CONV);
+}
+
+/* Check ADC can be reset to default value. */
+static void test_init(gconstpointer adc_p)
+{
+    const ADC *adc = adc_p;
+
+    QTestState *qts = qtest_init("-machine quanta-gsj");
+    adc_write_con(qts, adc, CON_REFSEL | CON_INT);
+    g_assert_cmphex(adc_read_con(qts, adc), ==, CON_REFSEL);
+    qtest_quit(qts);
+}
+
+/* Check ADC can convert from an internal reference. */
+static void test_convert_internal(gconstpointer adc_p)
+{
+    const ADC *adc = adc_p;
+    uint32_t index, input, output, expected_output;
+    QTestState *qts = qtest_init("-machine quanta-gsj");
+    qtest_irq_intercept_in(qts, "/machine/soc/a9mpcore/gic");
+
+    for (index = 0; index < NUM_INPUTS; ++index) {
+        for (size_t i = 0; i < ARRAY_SIZE(input_list); ++i) {
+            input = input_list[i];
+            expected_output = adc_calculate_output(input, DEFAULT_IREF);
+
+            adc_write_input(qts, adc, index, input);
+            adc_write_con(qts, adc, CON_MUX(index) | CON_REFSEL | CON_INT |
+                    CON_EN | CON_CONV);
+            adc_wait_conv_finished(qts, adc, DEFAULT_CLKDIV);
+            g_assert_cmphex(adc_read_con(qts, adc), ==, CON_MUX(index) |
+                    CON_REFSEL | CON_EN);
+            g_assert_false(qtest_get_irq(qts, adc->irq));
+            output = adc_read_data(qts, adc);
+            g_assert_cmpuint(output, ==, expected_output);
+        }
+    }
+
+    qtest_quit(qts);
+}
+
+/* Check ADC can convert from an external reference. */
+static void test_convert_external(gconstpointer adc_p)
+{
+    const ADC *adc = adc_p;
+    uint32_t index, input, vref, output, expected_output;
+    QTestState *qts = qtest_init("-machine quanta-gsj");
+    qtest_irq_intercept_in(qts, "/machine/soc/a9mpcore/gic");
+
+    for (index = 0; index < NUM_INPUTS; ++index) {
+        for (size_t i = 0; i < ARRAY_SIZE(input_list); ++i) {
+            for (size_t j = 0; j < ARRAY_SIZE(vref_list); ++j) {
+                input = input_list[i];
+                vref = vref_list[j];
+                expected_output = adc_calculate_output(input, vref);
+
+                adc_write_input(qts, adc, index, input);
+                adc_write_vref(qts, adc, vref);
+                adc_write_con(qts, adc, CON_MUX(index) | CON_INT | CON_EN |
+                        CON_CONV);
+                adc_wait_conv_finished(qts, adc, DEFAULT_CLKDIV);
+                g_assert_cmphex(adc_read_con(qts, adc), ==,
+                        CON_MUX(index) | CON_EN);
+                g_assert_false(qtest_get_irq(qts, adc->irq));
+                output = adc_read_data(qts, adc);
+                g_assert_cmpuint(output, ==, expected_output);
+            }
+        }
+    }
+
+    qtest_quit(qts);
+}
+
+/* Check ADC interrupt files if and only if CON_INT_EN is set. */
+static void test_interrupt(gconstpointer adc_p)
+{
+    const ADC *adc = adc_p;
+    uint32_t index, input, output, expected_output;
+    QTestState *qts = qtest_init("-machine quanta-gsj");
+
+    index = 1;
+    input = input_list[1];
+    expected_output = adc_calculate_output(input, DEFAULT_IREF);
+
+    qtest_irq_intercept_in(qts, "/machine/soc/a9mpcore/gic");
+    adc_write_input(qts, adc, index, input);
+    g_assert_false(qtest_get_irq(qts, adc->irq));
+    adc_write_con(qts, adc, CON_MUX(index) | CON_INT_EN | CON_REFSEL | CON_INT
+            | CON_EN | CON_CONV);
+    adc_wait_conv_finished(qts, adc, DEFAULT_CLKDIV);
+    g_assert_cmphex(adc_read_con(qts, adc), ==, CON_MUX(index) | CON_INT_EN
+            | CON_REFSEL | CON_INT | CON_EN);
+    g_assert_true(qtest_get_irq(qts, adc->irq));
+    output = adc_read_data(qts, adc);
+    g_assert_cmpuint(output, ==, expected_output);
+
+    qtest_quit(qts);
+}
+
+/* Check ADC is reset after setting ADC_RST for 10 ADC cycles. */
+static void test_reset(gconstpointer adc_p)
+{
+    const ADC *adc = adc_p;
+    QTestState *qts = qtest_init("-machine quanta-gsj");
+
+    for (size_t i = 0; i < ARRAY_SIZE(div_list); ++i) {
+        uint32_t div = div_list[i];
+
+        adc_write_con(qts, adc, CON_INT | CON_EN | CON_RST | CON_DIV(div));
+        qtest_clock_step(qts, adc_calculate_steps(RESET_CYCLES,
+                    adc_prescaler(qts, adc), DEFAULT_CLKDIV) - 1);
+        g_assert_true(adc_read_con(qts, adc) & CON_EN);
+        qtest_clock_step(qts, 1);
+        g_assert_false(adc_read_con(qts, adc) & CON_EN);
+    }
+    qtest_quit(qts);
+}
+
+/* Check ADC is not reset if we set ADC_RST for <10 ADC cycles. */
+static void test_premature_reset(gconstpointer adc_p)
+{
+    const ADC *adc = adc_p;
+    QTestState *qts = qtest_init("-machine quanta-gsj");
+
+    for (size_t i = 0; i < ARRAY_SIZE(div_list); ++i) {
+        uint32_t div = div_list[i];
+
+        adc_write_con(qts, adc, CON_INT | CON_EN | CON_RST | CON_DIV(div));
+        qtest_clock_step(qts, adc_calculate_steps(RESET_CYCLES,
+                    adc_prescaler(qts, adc), DEFAULT_CLKDIV) - 1);
+        g_assert_true(adc_read_con(qts, adc) & CON_EN);
+        adc_write_con(qts, adc, CON_INT | CON_EN | CON_DIV(div));
+        qtest_clock_step(qts, 1000);
+        g_assert_true(adc_read_con(qts, adc) & CON_EN);
+    }
+    qtest_quit(qts);
+}
+
+/* Check ADC Calibration works as desired. */
+static void test_calibrate(gconstpointer adc_p)
+{
+    int i, j;
+    const ADC *adc = adc_p;
+
+    for (j = 0; j < ARRAY_SIZE(iref_list); ++j) {
+        uint32_t iref = iref_list[j];
+        uint32_t expected_rv[] = {
+            adc_calculate_output(R0_INPUT, iref),
+            adc_calculate_output(R1_INPUT, iref),
+        };
+        char buf[100];
+        QTestState *qts;
+
+        sprintf(buf, "-machine quanta-gsj -global npcm7xx-adc.iref=%u", iref);
+        qts = qtest_init(buf);
+
+        /* Check the converted value is correct using the calibration value. */
+        for (i = 0; i < ARRAY_SIZE(input_list); ++i) {
+            uint32_t input;
+            uint32_t output;
+            uint32_t expected_output;
+            uint32_t calibrated_voltage;
+            uint32_t index = 0;
+
+            input = input_list[i];
+            /* Calibration only works for input range 0.1V ~ 1.8V. */
+            if (input < MIN_CALIB_INPUT || input > MAX_CALIB_INPUT) {
+                continue;
+            }
+            expected_output = adc_calculate_output(input, iref);
+
+            adc_write_input(qts, adc, index, input);
+            adc_write_con(qts, adc, CON_MUX(index) | CON_REFSEL | CON_INT |
+                    CON_EN | CON_CONV);
+            adc_wait_conv_finished(qts, adc, DEFAULT_CLKDIV);
+            g_assert_cmphex(adc_read_con(qts, adc), ==,
+                    CON_REFSEL | CON_MUX(index) | CON_EN);
+            output = adc_read_data(qts, adc);
+            g_assert_cmpuint(output, ==, expected_output);
+
+            calibrated_voltage = adc_calibrate(output, expected_rv);
+            g_assert_cmpuint(calibrated_voltage, >, input - MAX_ERROR);
+            g_assert_cmpuint(calibrated_voltage, <, input + MAX_ERROR);
+        }
+
+        qtest_quit(qts);
+    }
+}
+
+static void adc_add_test(const char *name, const ADC* wd,
+        GTestDataFunc fn)
+{
+    g_autofree char *full_name = g_strdup_printf("npcm7xx_adc/%s",  name);
+    qtest_add_data_func(full_name, wd, fn);
+}
+#define add_test(name, td) adc_add_test(#name, td, test_##name)
+
+int main(int argc, char **argv)
+{
+    g_test_init(&argc, &argv, NULL);
+
+    add_test(init, &adc);
+    add_test(convert_internal, &adc);
+    add_test(convert_external, &adc);
+    add_test(interrupt, &adc);
+    add_test(reset, &adc);
+    add_test(premature_reset, &adc);
+    add_test(calibrate, &adc);
+
+    return g_test_run();
+}