On Fri, Jul 10, 2026 at 12:18:44PM +0800, Kangjie Huang wrote:
> Add a sysbus model for the K230 IOMUX MMIO range at
> 0x91105000-0x911057ff. The model exposes the Function IO registers with
> the reset defaults documented by the K230 Technical Reference Manual and
> keeps 32-bit guest writes visible to firmware and the operating system.
> 
> This is enough for SDK U-Boot's pinctrl-single driver, which programs
> the board pin configuration with 32-bit read-modify-write accesses.
> 
> Wire the device into the K230 SoC and add qtest coverage for reset
> values, read/write storage and system reset.
> 
> Signed-off-by: Kangjie Huang <[email protected]>
> ---
> RFC notes:
> - This is my first QEMU upstream submission; feedback on patch structure
>   and modelling boundaries would be appreciated.
> - The model covers the IOMUX MMIO range 0x91105000-0x911057ff.
> - It is intended to satisfy SDK U-Boot pinctrl-single register accesses.
> - PMU IOMUX and physical pin routing are intentionally not modeled.
> - Tested with qtest and SDK U-Boot smoke test.
> 
>  docs/system/riscv/k230.rst    |   1 +
>  hw/misc/Kconfig               |   3 +
>  hw/misc/k230_iomux.c          | 138 ++++++++++++++++++++++++++++++++++
>  hw/misc/meson.build           |   2 +
>  hw/misc/trace-events          |   4 +
>  hw/riscv/Kconfig              |   1 +
>  hw/riscv/k230.c               |  11 ++-
>  include/hw/misc/k230_iomux.h  |  34 +++++++++
>  include/hw/riscv/k230.h       |   2 +
>  tests/qtest/k230-iomux-test.c | 107 ++++++++++++++++++++++++++
>  tests/qtest/meson.build       |   5 +-
>  11 files changed, 304 insertions(+), 4 deletions(-)
>  create mode 100644 hw/misc/k230_iomux.c
>  create mode 100644 include/hw/misc/k230_iomux.h
>  create mode 100644 tests/qtest/k230-iomux-test.c
> 
> diff --git a/docs/system/riscv/k230.rst b/docs/system/riscv/k230.rst
> index cea8202e55..b160c401fd 100644
> --- a/docs/system/riscv/k230.rst
> +++ b/docs/system/riscv/k230.rst
> @@ -20,6 +20,7 @@ The ``k230`` machine supports the following devices:
>  * Platform-Level Interrupt Controller (PLIC)
>  * 2 K230 Watchdog Timer
>  * 5 UART
> +* K230 IOMUX register block
>  
>  Boot options
>  ------------
> diff --git a/hw/misc/Kconfig b/hw/misc/Kconfig
> index 1543ee6653..573d24a9df 100644
> --- a/hw/misc/Kconfig
> +++ b/hw/misc/Kconfig
> @@ -257,4 +257,7 @@ config XLNX_VERSAL_TRNG
>  config XLNX_ZYNQ_DDRC
>      bool
>  
> +config K230_IOMUX
> +    bool
> +
>  source macio/Kconfig
> diff --git a/hw/misc/k230_iomux.c b/hw/misc/k230_iomux.c
> new file mode 100644
> index 0000000000..5f5e2aac51
> --- /dev/null
> +++ b/hw/misc/k230_iomux.c
> @@ -0,0 +1,138 @@
> +/*
> + * K230 IOMUX register block compatible with Kendryte K230 SDK
> + *
> + * Copyright (c) 2026 Kangjie Huang <[email protected]>
> + *
> + * SPDX-License-Identifier: GPL-2.0-or-later
> + *
> + * Provides the low IOMUX register block at 0x91105000 with the reset
> + * defaults documented by the K230 Technical Reference Manual. The model 
> keeps
> + * guest 32-bit register writes visible to firmware and the operating system,
> + * but does not model physical pin routing, pad electrical effects, or the
> + * separate PMU IOMUX block.
> + *
> + * K230 Technical Reference Manual V0.3.1 (2024-11-18):
> + * 
> https://github.com/revyos/external-docs/blob/master/K230/en-us/K230_Technical_Reference_Manual_V0.3.1_20241118.pdf
> + *
> + * For more information, see <https://www.kendryte.com/en/proDetail/230>
> + */
> +
> +#include "qemu/osdep.h"
> +#include "hw/misc/k230_iomux.h"
> +#include "migration/vmstate.h"
> +#include "qemu/module.h"
> +#include "qemu/log.h"
> +#include "trace.h"
> +
> +/* TRM V0.3.1 section 12.9.2.1 Function IO register list. */
> +static const uint32_t k230_iomux_reset_values[] = {
> +    0x944, 0x944, 0x929, 0x908, 0x888, 0x908, 0x948, 0x890,
> +    0x890, 0x890, 0x910, 0x890, 0x890, 0x8a9, 0xa9e, 0xabf,
> +    0xb9e, 0xb9e, 0xb9e, 0xb9e, 0xb9e, 0xb9e, 0xb9e, 0xb9e,
> +    0xb1e, 0xa90, 0xabf, 0xb9e, 0xb9e, 0xb9e, 0xb9e, 0xb9e,
> +    0xbd0, 0xbd0, 0xbd0, 0xbd0, 0xbd0, 0xbd0, 0x890, 0x910,
> +    0x890, 0x910, 0x890, 0x910, 0x890, 0x910, 0x890, 0x910,
> +    0x890, 0x910, 0x890, 0x910, 0x890, 0x910, 0x89e, 0x89f,
> +    0x99e, 0x99e, 0x99e, 0x99e, 0x890, 0x890, 0x8a9, 0x8a9,
> +};
> +
> +static void k230_iomux_reset(DeviceState *dev)
> +{
> +    K230IomuxState *s = K230_IOMUX(dev);
> +
> +    memset(s->regs, 0, sizeof(s->regs));
> +    memcpy(s->regs, k230_iomux_reset_values,
> +           sizeof(k230_iomux_reset_values));
> +}
> +
> +static uint64_t k230_iomux_read(void *opaque, hwaddr offset, unsigned size)
> +{
> +    K230IomuxState *s = K230_IOMUX(opaque);
> +    uint32_t value;
> +
> +    if (offset + size > K230_IOMUX_MMIO_SIZE || (offset & 3)) {
> +        qemu_log_mask(LOG_GUEST_ERROR,
> +                      "%s: invalid access offset 0x%" HWADDR_PRIx
> +                      " size %u\n", __func__, offset, size);
> +        return 0;
> +    }
> +
> +    value = s->regs[offset >> 2];
> +    trace_k230_iomux_read(offset, value);
> +    return value;
> +}
> +
> +static void k230_iomux_write(void *opaque, hwaddr offset,
> +                             uint64_t value, unsigned size)
> +{
> +    K230IomuxState *s = K230_IOMUX(opaque);
> +
> +    if (offset + size > K230_IOMUX_MMIO_SIZE || (offset & 3)) {
> +        qemu_log_mask(LOG_GUEST_ERROR,
> +                      "%s: invalid access offset 0x%" HWADDR_PRIx
> +                      " size %u\n", __func__, offset, size);
> +        return;
> +    }
> +
> +    trace_k230_iomux_write(offset, value);
> +    s->regs[offset >> 2] = value;
The IOMUX register only allows access to bit 31 and bits 0 through 13.

Please see the link provided below. Additionally, you should pay attention to 
the read/write
permissions for every bit within each register.

https://www.kendryte.com/k230_linux/en/main/app_develop_guide/driver/iomux.html#registers
> +}
> +
> +static const MemoryRegionOps k230_iomux_ops = {
> +    .read = k230_iomux_read,
> +    .write = k230_iomux_write,
> +    .endianness = DEVICE_LITTLE_ENDIAN,
> +    .valid = {
> +        .min_access_size = 4,
> +        .max_access_size = 4,
> +        .unaligned = false,
> +    },
> +    .impl = {
> +        .min_access_size = 4,
> +        .max_access_size = 4,
> +        .unaligned = false,
> +    },
> +};
> +
> +static void k230_iomux_init(Object *obj)
> +{
> +    K230IomuxState *s = K230_IOMUX(obj);
> +
> +    memory_region_init_io(&s->mmio, obj, &k230_iomux_ops, s,
> +                          TYPE_K230_IOMUX, K230_IOMUX_MMIO_SIZE);
> +    sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->mmio);
> +}
> +
> +static const VMStateDescription vmstate_k230_iomux = {
> +    .name = "k230.iomux",
> +    .version_id = 1,
> +    .minimum_version_id = 1,
> +    .fields = (const VMStateField[]) {
> +        VMSTATE_UINT32_ARRAY(regs, K230IomuxState, K230_IOMUX_NUM_REGS),
> +        VMSTATE_END_OF_LIST()
> +    }
> +};
> +
> +static void k230_iomux_class_init(ObjectClass *klass, const void *data)
> +{
> +    DeviceClass *dc = DEVICE_CLASS(klass);
> +
> +    dc->desc = "Kendryte K230 IOMUX";
> +    dc->vmsd = &vmstate_k230_iomux;
> +    device_class_set_legacy_reset(dc, k230_iomux_reset);
> +}
> +
> +static const TypeInfo k230_iomux_info = {
> +    .name          = TYPE_K230_IOMUX,
> +    .parent        = TYPE_SYS_BUS_DEVICE,
> +    .instance_size = sizeof(K230IomuxState),
> +    .instance_init = k230_iomux_init,
> +    .class_init    = k230_iomux_class_init,
> +};
> +
> +static void k230_iomux_register_types(void)
> +{
> +    type_register_static(&k230_iomux_info);
> +}
> +
> +type_init(k230_iomux_register_types)
> diff --git a/hw/misc/meson.build b/hw/misc/meson.build
> index 23265f6035..31e5145a35 100644
> --- a/hw/misc/meson.build
> +++ b/hw/misc/meson.build
> @@ -168,3 +168,5 @@ system_ss.add(when: 'CONFIG_SBSA_REF', if_true: 
> files('sbsa_ec.c'))
>  
>  # HPPA devices
>  system_ss.add(when: 'CONFIG_LASI', if_true: files('lasi.c'))
> +
> +system_ss.add(when: 'CONFIG_K230_IOMUX', if_true: files('k230_iomux.c'))
> diff --git a/hw/misc/trace-events b/hw/misc/trace-events
> index c9a868b3ef..388967c9e1 100644
> --- a/hw/misc/trace-events
> +++ b/hw/misc/trace-events
> @@ -405,6 +405,10 @@ djmemc_write(int reg, uint64_t value, unsigned int size) 
> "reg=0x%x value=0x%"PRI
>  iosb_read(int reg, uint64_t value, unsigned int size) "reg=0x%x 
> value=0x%"PRIx64" size=%u"
>  iosb_write(int reg, uint64_t value, unsigned int size) "reg=0x%x 
> value=0x%"PRIx64" size=%u"
>  
> +# k230_iomux.c
> +k230_iomux_read(uint64_t offset, uint32_t value) "offset=0x%" PRIx64 " 
> value=0x%" PRIx32
> +k230_iomux_write(uint64_t offset, uint64_t value) "offset=0x%" PRIx64 " 
> value=0x%" PRIx64
> +
>  # aspeed_sli.c
>  aspeed_sli_write(uint64_t offset, unsigned int size, uint32_t data) "To 0x%" 
> PRIx64 " of size %u: 0x%" PRIx32
>  aspeed_sli_read(uint64_t offset, unsigned int size, uint32_t data) "To 0x%" 
> PRIx64 " of size %u: 0x%" PRIx32
> diff --git a/hw/riscv/Kconfig b/hw/riscv/Kconfig
> index de37c08cae..4c9fc3e7ad 100644
> --- a/hw/riscv/Kconfig
> +++ b/hw/riscv/Kconfig
> @@ -162,3 +162,4 @@ config K230
>      select SERIAL_MM
>      select UNIMP
>      select K230_WDT
> +    select K230_IOMUX
> diff --git a/hw/riscv/k230.c b/hw/riscv/k230.c
> index 656f28190c..430c1646b5 100644
> --- a/hw/riscv/k230.c
> +++ b/hw/riscv/k230.c
> @@ -110,6 +110,7 @@ static void k230_soc_init(Object *obj)
>      object_initialize_child(obj, "c908-cpu", cpu0, TYPE_RISCV_HART_ARRAY);
>      object_initialize_child(obj, "k230-wdt0", &s->wdt[0], TYPE_K230_WDT);
>      object_initialize_child(obj, "k230-wdt1", &s->wdt[1], TYPE_K230_WDT);
> +    object_initialize_child(obj, "k230-iomux", &s->iomux, TYPE_K230_IOMUX);
>  
>      qdev_prop_set_uint32(DEVICE(cpu0), "hartid-base", 0);
>      qdev_prop_set_string(DEVICE(cpu0), "cpu-type", 
> TYPE_RISCV_CPU_THEAD_C908);
> @@ -198,6 +199,11 @@ static void k230_soc_realize(DeviceState *dev, Error 
> **errp)
>          }
>      }
>  
> +    /* IOMUX */
> +    if (!sysbus_realize(SYS_BUS_DEVICE(&s->iomux), errp)) {
> +        return;
> +    }
> +
>      sysbus_mmio_map(SYS_BUS_DEVICE(&s->wdt[0]), 0, 
> memmap[K230_DEV_WDT0].base);
>      sysbus_connect_irq(SYS_BUS_DEVICE(&s->wdt[0]), 0,
>                         qdev_get_gpio_in(DEVICE(s->c908_plic), 
> K230_WDT0_IRQ));
> @@ -206,6 +212,8 @@ static void k230_soc_realize(DeviceState *dev, Error 
> **errp)
>      sysbus_connect_irq(SYS_BUS_DEVICE(&s->wdt[1]), 0,
>                         qdev_get_gpio_in(DEVICE(s->c908_plic), 
> K230_WDT1_IRQ));
>  
> +    sysbus_mmio_map(SYS_BUS_DEVICE(&s->iomux), 0, 
> memmap[K230_DEV_IOMUX].base);
> +
>      /* unimplemented devices */
>      create_unimplemented_device("kpu.l2-cache",
>                                  memmap[K230_DEV_KPU_L2_CACHE].base,
> @@ -280,9 +288,6 @@ static void k230_soc_realize(DeviceState *dev, Error 
> **errp)
>      create_unimplemented_device("ipcm", memmap[K230_DEV_MAILBOX].base,
>                                  memmap[K230_DEV_MAILBOX].size);
>  
> -    create_unimplemented_device("iomux", memmap[K230_DEV_IOMUX].base,
> -                                memmap[K230_DEV_IOMUX].size);
> -
>      create_unimplemented_device("timer", memmap[K230_DEV_TIMER].base,
>                                  memmap[K230_DEV_TIMER].size);
>  
> diff --git a/include/hw/misc/k230_iomux.h b/include/hw/misc/k230_iomux.h
> new file mode 100644
> index 0000000000..15e4bb339d
> --- /dev/null
> +++ b/include/hw/misc/k230_iomux.h
> @@ -0,0 +1,34 @@
> +/*
> + * Kendryte K230 IOMUX register block
> + *
> + * This models the low IOMUX register block. Physical pin routing, pad
> + * electrical effects, and the separate PMU IOMUX block are out of scope.
> + *
> + * K230 Technical Reference Manual V0.3.1 (2024-11-18):
> + * 
> https://github.com/revyos/external-docs/blob/master/K230/en-us/K230_Technical_Reference_Manual_V0.3.1_20241118.pdf
> + *
> + * Copyright (c) 2026 Kangjie Huang <[email protected]>
> + *
> + * SPDX-License-Identifier: GPL-2.0-or-later
> + */
> +
> +#ifndef HW_MISC_K230_IOMUX_H
> +#define HW_MISC_K230_IOMUX_H
> +
> +#include "hw/core/sysbus.h"
> +#include "qom/object.h"
> +
> +#define TYPE_K230_IOMUX "riscv.k230.iomux"
> +OBJECT_DECLARE_SIMPLE_TYPE(K230IomuxState, K230_IOMUX)
> +
> +#define K230_IOMUX_MMIO_SIZE 0x800
> +#define K230_IOMUX_NUM_REGS (K230_IOMUX_MMIO_SIZE / sizeof(uint32_t))
> +
> +struct K230IomuxState {
> +    SysBusDevice parent_obj;
> +
> +    MemoryRegion mmio;
> +    uint32_t regs[K230_IOMUX_NUM_REGS];
> +};
> +
> +#endif /* HW_MISC_K230_IOMUX_H */
> diff --git a/include/hw/riscv/k230.h b/include/hw/riscv/k230.h
> index 592e1c26bf..223e76b420 100644
> --- a/include/hw/riscv/k230.h
> +++ b/include/hw/riscv/k230.h
> @@ -18,6 +18,7 @@
>  #include "hw/core/boards.h"
>  #include "hw/riscv/riscv_hart.h"
>  #include "hw/watchdog/k230_wdt.h"
> +#include "hw/misc/k230_iomux.h"
>  
>  #define C908_CPU_HARTID   (0)
>  
> @@ -33,6 +34,7 @@ typedef struct K230SoCState {
>      RISCVHartArrayState c908_cpu; /* Small core */
>  
>      K230WdtState wdt[2];
> +    K230IomuxState iomux;
>      MemoryRegion sram;
>      MemoryRegion bootrom;
>  
> diff --git a/tests/qtest/k230-iomux-test.c b/tests/qtest/k230-iomux-test.c
> new file mode 100644
> index 0000000000..ca2d557955
> --- /dev/null
> +++ b/tests/qtest/k230-iomux-test.c
> @@ -0,0 +1,107 @@
> +/*
> + * QTest testcase for K230 IOMUX
> + *
> + * Copyright (c) 2026 Kangjie Huang <[email protected]>
> + *
> + * SPDX-License-Identifier: GPL-2.0-or-later
> + *
> + * Provides test coverage for the low IOMUX register block compatible with 
> the
> + * Kendryte K230 SDK.
> + *
> + * K230 Technical Reference Manual V0.3.1 (2024-11-18):
> + * 
> https://github.com/revyos/external-docs/blob/master/K230/en-us/K230_Technical_Reference_Manual_V0.3.1_20241118.pdf
> + *
> + * For more information, see <https://www.kendryte.com/en/proDetail/230>
> + */
> +
> +#include "qemu/osdep.h"
> +#include "libqtest.h"
> +#include "hw/misc/k230_iomux.h"
> +
> +#define K230_IOMUX_BASE 0x91105000
> +#define K230_IOMUX_IO0  (K230_IOMUX_BASE + 0x00)
> +#define K230_IOMUX_IO1  (K230_IOMUX_BASE + 0x04)
> +#define K230_IOMUX_LAST (K230_IOMUX_BASE + K230_IOMUX_MMIO_SIZE - 4)
> +
> +static const uint32_t k230_iomux_reset_values[] = {
> +    0x944, 0x944, 0x929, 0x908, 0x888, 0x908, 0x948, 0x890,
> +    0x890, 0x890, 0x910, 0x890, 0x890, 0x8a9, 0xa9e, 0xabf,
> +    0xb9e, 0xb9e, 0xb9e, 0xb9e, 0xb9e, 0xb9e, 0xb9e, 0xb9e,
> +    0xb1e, 0xa90, 0xabf, 0xb9e, 0xb9e, 0xb9e, 0xb9e, 0xb9e,
> +    0xbd0, 0xbd0, 0xbd0, 0xbd0, 0xbd0, 0xbd0, 0x890, 0x910,
> +    0x890, 0x910, 0x890, 0x910, 0x890, 0x910, 0x890, 0x910,
> +    0x890, 0x910, 0x890, 0x910, 0x890, 0x910, 0x89e, 0x89f,
> +    0x99e, 0x99e, 0x99e, 0x99e, 0x890, 0x890, 0x8a9, 0x8a9,
> +};
> +
> +static void test_reset_values(void)
> +{
> +    QTestState *qts = qtest_init("-machine k230");
> +
> +    for (size_t i = 0; i < G_N_ELEMENTS(k230_iomux_reset_values); i++) {
> +        g_assert_cmphex(qtest_readl(qts, K230_IOMUX_BASE +
> +                                    i * sizeof(uint32_t)), ==,
> +                        k230_iomux_reset_values[i]);
> +    }
> +
> +    qtest_quit(qts);
> +}
> +
> +static void test_rw(void)
> +{
> +    QTestState *qts = qtest_init("-machine k230");
> +
> +    qtest_writel(qts, K230_IOMUX_IO0, 0x12345678);
> +    g_assert_cmphex(qtest_readl(qts, K230_IOMUX_IO0), ==, 0x12345678);
> +
> +    qtest_writel(qts, K230_IOMUX_IO1, 0xa5a5a5a5);
> +    g_assert_cmphex(qtest_readl(qts, K230_IOMUX_IO1), ==, 0xa5a5a5a5);
> +    g_assert_cmphex(qtest_readl(qts, K230_IOMUX_IO0), ==, 0x12345678);
> +
> +    qtest_quit(qts);
> +}
> +
> +static void test_last_reg_rw(void)
> +{
> +    QTestState *qts = qtest_init("-machine k230");
> +
> +    qtest_writel(qts, K230_IOMUX_LAST, 0x00000abc);
> +    g_assert_cmphex(qtest_readl(qts, K230_IOMUX_LAST), ==, 0x00000abc);
> +
> +    qtest_quit(qts);
> +}
> +
> +static void test_reset_after_write(void)
> +{
> +    QTestState *qts = qtest_init("-machine k230");
> +
> +    qtest_writel(qts, K230_IOMUX_IO0, 0x12345678);
> +    qtest_writel(qts, K230_IOMUX_IO1, 0xa5a5a5a5);
> +    qtest_writel(qts, K230_IOMUX_LAST, 0x00000abc);
> +
> +    g_assert_cmphex(qtest_readl(qts, K230_IOMUX_IO0), ==, 0x12345678);
> +    g_assert_cmphex(qtest_readl(qts, K230_IOMUX_IO1), ==, 0xa5a5a5a5);
> +    g_assert_cmphex(qtest_readl(qts, K230_IOMUX_LAST), ==, 0x00000abc);
> +
> +    qtest_system_reset(qts);
> +
> +    g_assert_cmphex(qtest_readl(qts, K230_IOMUX_IO0), ==,
> +                    k230_iomux_reset_values[0]);
> +    g_assert_cmphex(qtest_readl(qts, K230_IOMUX_IO1), ==,
> +                    k230_iomux_reset_values[1]);
> +    g_assert_cmphex(qtest_readl(qts, K230_IOMUX_LAST), ==, 0);
> +
> +    qtest_quit(qts);
> +}
> +
> +int main(int argc, char **argv)
> +{
> +    g_test_init(&argc, &argv, NULL);
> +
> +    qtest_add_func("/k230-iomux/reset", test_reset_values);
> +    qtest_add_func("/k230-iomux/rw", test_rw);
> +    qtest_add_func("/k230-iomux/last-reg-rw", test_last_reg_rw);
> +    qtest_add_func("/k230-iomux/reset-after-write", test_reset_after_write);
> +
> +    return g_test_run();
> +}
> diff --git a/tests/qtest/meson.build b/tests/qtest/meson.build
> index 56ff860e21..cb8001ecbf 100644
> --- a/tests/qtest/meson.build
> +++ b/tests/qtest/meson.build
> @@ -297,7 +297,10 @@ qtests_riscv64 = ['riscv-csr-test'] + \
>    (config_all_devices.has_key('CONFIG_IOMMU_TESTDEV') and
>     config_all_devices.has_key('CONFIG_RISCV_IOMMU') ?
>     ['iommu-riscv-test'] : []) + \
> -  (config_all_devices.has_key('CONFIG_K230') ? ['k230-wdt-test'] : [])
> +  (config_all_devices.has_key('CONFIG_K230') ? [
> +    'k230-wdt-test',
> +    'k230-iomux-test',
> +  ] : [])
>  
>  qtests_hexagon = ['boot-serial-test']
>  
> -- 
> 2.43.0
> 

Reply via email to