| /* |
| * 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")); |
| qobject_unref(response); |
| } |
| |
| 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)); |
| g_assert_false(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(calibrate, &adc); |
| |
| return g_test_run(); |
| } |