{"id":2230657,"url":"http://patchwork.ozlabs.org/api/patches/2230657/?format=json","web_url":"http://patchwork.ozlabs.org/project/qemu-devel/patch/20260429-pic32mk-board-support-v1-1-1dfac14f8e2d@voltumotor.com/","project":{"id":14,"url":"http://patchwork.ozlabs.org/api/projects/14/?format=json","name":"QEMU Development","link_name":"qemu-devel","list_id":"qemu-devel.nongnu.org","list_email":"qemu-devel@nongnu.org","web_url":"","scm_url":"","webscm_url":"","list_archive_url":"","list_archive_url_format":"","commit_url_format":""},"msgid":"<20260429-pic32mk-board-support-v1-1-1dfac14f8e2d@voltumotor.com>","list_archive_url":null,"date":"2026-04-29T21:51:17","name":"[RFC,1/3] hw/mips: add Microchip PIC32MK GPK/MCM board","commit_ref":null,"pull_url":null,"state":"new","archived":false,"hash":"70e357eebc8d3dc9b031690ea56583dac0f52f07","submitter":{"id":93284,"url":"http://patchwork.ozlabs.org/api/people/93284/?format=json","name":"Ericson Joseph","email":"ericsonjoseph@gmail.com"},"delegate":null,"mbox":"http://patchwork.ozlabs.org/project/qemu-devel/patch/20260429-pic32mk-board-support-v1-1-1dfac14f8e2d@voltumotor.com/mbox/","series":[{"id":502167,"url":"http://patchwork.ozlabs.org/api/series/502167/?format=json","web_url":"http://patchwork.ozlabs.org/project/qemu-devel/list/?series=502167","date":"2026-04-29T21:51:18","name":"hw/mips: add Microchip PIC32MK GPK/MCM board emulation","version":1,"mbox":"http://patchwork.ozlabs.org/series/502167/mbox/"}],"comments":"http://patchwork.ozlabs.org/api/patches/2230657/comments/","check":"pending","checks":"http://patchwork.ozlabs.org/api/patches/2230657/checks/","tags":{},"related":[],"headers":{"Return-Path":"<qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org>","X-Original-To":"incoming@patchwork.ozlabs.org","Delivered-To":"patchwork-incoming@legolas.ozlabs.org","Authentication-Results":["legolas.ozlabs.org;\n\tdkim=pass (2048-bit key;\n unprotected) header.d=gmail.com header.i=@gmail.com header.a=rsa-sha256\n header.s=20251104 header.b=Et3bubPy;\n\tdkim-atps=neutral","legolas.ozlabs.org;\n spf=pass (sender SPF authorized) smtp.mailfrom=nongnu.org\n (client-ip=209.51.188.17; helo=lists1p.gnu.org;\n envelope-from=qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org;\n receiver=patchwork.ozlabs.org)"],"Received":["from lists1p.gnu.org (lists1p.gnu.org [209.51.188.17])\n\t(using TLSv1.2 with cipher ECDHE-ECDSA-AES256-GCM-SHA384 (256/256 bits))\n\t(No client certificate requested)\n\tby legolas.ozlabs.org (Postfix) with ESMTPS id 4g5YbL1Zn9z1yHZ\n\tfor <incoming@patchwork.ozlabs.org>; Thu, 30 Apr 2026 09:36:14 +1000 (AEST)","from localhost ([::1] helo=lists1p.gnu.org)\n\tby lists1p.gnu.org with esmtp (Exim 4.90_1)\n\t(envelope-from <qemu-devel-bounces@nongnu.org>)\n\tid 1wIEQZ-0004pp-8S; Wed, 29 Apr 2026 19:34:47 -0400","from eggs.gnu.org ([2001:470:142:3::10])\n by lists1p.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)\n (Exim 4.90_1) (envelope-from <ericsonjoseph@gmail.com>)\n id 1wICoh-00010i-5l\n for qemu-devel@nongnu.org; Wed, 29 Apr 2026 17:51:35 -0400","from mail-dy1-x132c.google.com ([2607:f8b0:4864:20::132c])\n by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128)\n (Exim 4.90_1) (envelope-from <ericsonjoseph@gmail.com>)\n id 1wICob-0004HY-Dq\n for qemu-devel@nongnu.org; Wed, 29 Apr 2026 17:51:34 -0400","by mail-dy1-x132c.google.com with SMTP id\n 5a478bee46e88-2bdd40d3c61so397587eec.1\n for <qemu-devel@nongnu.org>; Wed, 29 Apr 2026 14:51:29 -0700 (PDT)","from [127.0.1.1] ([2803:9800:9001:c4f0:5d16:7dac:ff8:d8ff])\n by smtp.gmail.com with ESMTPSA id\n 5a478bee46e88-2ed1bf6d315sm4353878eec.1.2026.04.29.14.51.22\n (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n Wed, 29 Apr 2026 14:51:25 -0700 (PDT)"],"DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=gmail.com; s=20251104; t=1777499488; x=1778104288; darn=nongnu.org;\n h=cc:to:in-reply-to:references:message-id:content-transfer-encoding\n :mime-version:subject:date:from:from:to:cc:subject:date:message-id\n :reply-to; bh=UMLAZrBshk7lDmkr+EUehCw1mWT2Ux6/wmvWENJvKdo=;\n b=Et3bubPycNu71sUhI8J0aBr/Mt36qCRfENDBUxKQDk4SVJ09YL6F1rMuwmwKzP0W44\n r2pqEoj8b06euqMvgHNISJCKmwQcXErwfQkog8YeyeSp3JCi5EeY1CDNke0i9VZMkrNx\n +cPD+LhbtdmS+SYm4OJQ8eadt80Docj8p8ryv8LyP79+PNSftMKAkfErVTekLEqLVDx+\n KifDI7H/Qx8kjPIq/kG/R9niZIbLGO5Sx3S9+mYt79yRq0f3gRMLUoV9fI/GhChMvMSK\n R/K85BBTe3X+Tg+WELm3FTPcLrshrVEf68ZjLKFufHN2ee3mNT1+GZmTbYQgtL4cYI5s\n zBWQ==","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=1e100.net; s=20251104; t=1777499488; x=1778104288;\n h=cc:to:in-reply-to:references:message-id:content-transfer-encoding\n :mime-version:subject:date:from:x-gm-gg:x-gm-message-state:from:to\n :cc:subject:date:message-id:reply-to;\n bh=UMLAZrBshk7lDmkr+EUehCw1mWT2Ux6/wmvWENJvKdo=;\n b=kMeeFZA7RnrPgvE8CO5rsg8kcm8HGGJh24gW+KZRgpIDjvAW2Y+sT7AIotZJT4XCsp\n W96eG2a3sXdjNcbCfMJccfzTo5+O7dUVkVOjj4DcEK00h8RcKQxNJBzKCiPNBjHY1Wto\n HlL9g4r6Pmx2tn5yXfIpePvygbvfVrzfnGoBPKNWnyfcVxbVZkYC8Gf9K9likx5MtbiR\n 5yilm4TzqrYxHA87w5y1JS7B5U4mGLd7Z+Syxb/lFbSDyZHJ1yHUKNZSGJ/bM3FGddVP\n c6gFW+Naue8b9cmfnkV8V/JLVU5pZDBO5NUH/R1/9Y7X2pxtFzmc6nCwMy4UmYpSuU18\n RcCA==","X-Gm-Message-State":"AOJu0Yy7fV5bRH+UOxCbaNvSBK9u7XFVsdEiMy8Zmcc76xut2C6oX2RM\n ysUMDdx5xXPN2meZIHpuazOpS1x8qWRhSOrtzvwI7HtE+ByDZKR4CCyt","X-Gm-Gg":"AeBDieuBhEvwpBUGWOtEiQKXaCsj5D0l6VQ4mSUQRVmGoVy66bmvcw/OV4EZ0qmrhZi\n EiE5TX59DXSFgZ2Ttaos0pt67bbFDLWH70m/f5FQtPqeIQpdWau++rMlbrIojKxjYSDi7uMbLbj\n EqnHZ4N4J5iVM+Q+Iieva5SU5CqUITaG38FZiCvj+7uF4F6WBqXl0Xcn1fjr+OhBjgi7WXlMFag\n UZia6gyqnXqze+r5NP94ZzfX9l3h35/p11LAwnRLZ/QYGAR73hpGIDNLd9dYzSXE5NyMzPQVAgC\n gfEoha4OtaWMbzh55oKLW0GC0EuGihumhDdIFpICTHsOSV56K3+do9kFHA1WvLQg2uZDgcG3nvB\n vir3VUTv5I4jM5Wr50F1mwoDbNoASZij8QzYq0QnXk9tpbKgu2PrbkPk/JsRCNR4asIw1OrTzmG\n 2ngZgSZDxPFRwgyHpWrPuWjrRS1vniq/FzVAKWE0y9","X-Received":"by 2002:a05:7301:3d11:b0:2d1:9b35:4ed3 with SMTP id\n 5a478bee46e88-2ed3e67f822mr46261eec.28.1777499486963;\n Wed, 29 Apr 2026 14:51:26 -0700 (PDT)","From":"Ericson Joseph <ericsonjoseph@gmail.com>","Date":"Wed, 29 Apr 2026 18:51:17 -0300","Subject":"[PATCH RFC 1/3] hw/mips: add Microchip PIC32MK GPK/MCM board","MIME-Version":"1.0","Content-Type":"text/plain; charset=\"utf-8\"","Content-Transfer-Encoding":"8bit","Message-Id":"<20260429-pic32mk-board-support-v1-1-1dfac14f8e2d@voltumotor.com>","References":"<20260429-pic32mk-board-support-v1-0-1dfac14f8e2d@voltumotor.com>","In-Reply-To":"<20260429-pic32mk-board-support-v1-0-1dfac14f8e2d@voltumotor.com>","To":"qemu-devel@nongnu.org","Cc":"=?utf-8?q?Philippe_Mathieu-Daud=C3=A9?= <philmd@linaro.org>,\n  Jiaxun Yang <jiaxun.yang@flygoat.com>, Paolo Bonzini <pbonzini@redhat.com>,\n  Fabiano Rosas <farosas@suse.de>, Laurent Vivier <lvivier@redhat.com>,\n  Pierrick Bouvier <pierrick.bouvier@oss.qualcomm.com>,\n  Ericson Joseph <ericsonjoseph@gmail.com>","X-Mailer":"b4 0.15.2","Received-SPF":"pass client-ip=2607:f8b0:4864:20::132c;\n envelope-from=ericsonjoseph@gmail.com; helo=mail-dy1-x132c.google.com","X-Spam_score_int":"-20","X-Spam_score":"-2.1","X-Spam_bar":"--","X-Spam_report":"(-2.1 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1,\n DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, FREEMAIL_FROM=0.001,\n RCVD_IN_DNSWL_NONE=-0.0001, SPF_HELO_NONE=0.001,\n SPF_PASS=-0.001 autolearn=ham autolearn_force=no","X-Spam_action":"no action","X-Mailman-Approved-At":"Wed, 29 Apr 2026 19:34:45 -0400","X-BeenThere":"qemu-devel@nongnu.org","X-Mailman-Version":"2.1.29","Precedence":"list","List-Id":"qemu development <qemu-devel.nongnu.org>","List-Unsubscribe":"<https://lists.nongnu.org/mailman/options/qemu-devel>,\n <mailto:qemu-devel-request@nongnu.org?subject=unsubscribe>","List-Archive":"<https://lists.nongnu.org/archive/html/qemu-devel>","List-Post":"<mailto:qemu-devel@nongnu.org>","List-Help":"<mailto:qemu-devel-request@nongnu.org?subject=help>","List-Subscribe":"<https://lists.nongnu.org/mailman/listinfo/qemu-devel>,\n <mailto:qemu-devel-request@nongnu.org?subject=subscribe>","Errors-To":"qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org","Sender":"qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org"},"content":"Add emulation for the Microchip PIC32MK GPK/MCM family of 32-bit MIPS\nmicrocontrollers (DS60001519E). The family uses a MIPS32 microAptiv MCU\ncore running at up to 120 MHz with 256 KB SRAM and 1 MB program flash.\n\nThe following peripherals are modelled:\n\n  - EVIC  — 216-source interrupt controller with SET/CLR/INV registers,\n             single-vector and multi-vector modes, and OFFx vector offsets\n  - UART  — 6 instances with TX/RX FIFOs and interrupt generation;\n             UART1 wired to the first serial port\n  - Timer — 9 instances using QEMU ptimer; Timer1 drives a 1 kHz tick\n  - GPIO  — 7 ports (A–G) with TRIS/LAT/PORT/ANSEL/CNPU/CNPD registers\n  - SPI   — 6 instances including master and slave modes\n  - I2C   — 4 instances (register-level stub, interrupt capable)\n  - DMA   — 8 channels with CELL/BLOCK/PATTERN transfer modes\n  - CAN FD — 4 instances exposed via QEMU can-bus; SocketCAN accessible\n             with -object can-bus,id=canbus<n>\n  - USB   — 2 Full-Speed OTG instances exposed as chardev PTY\n  - ADC   — ADCHS high-speed 12-bit ADC with 7 cores and FIFO\n  - NVM   — Flash controller with backed host-file support for persistence\n  - DataEE — Software data EEPROM layer over program flash\n  - OC    — 16 Output Compare units\n  - IC    — 16 Input Capture units\n  - CRU   — Clock and Reset Unit (SYSCLK, PBCLK, RCON, RSWRST)\n  - WDT   — Watchdog timer (write-once WDTKEY protocol)\n  - CFG   — Configuration registers and SYSKEY unlock sequence\n\nAll unimplemented SFR ranges log LOG_UNIMP rather than silently ignoring\naccesses. The SET (+4), CLR (+8), and INV (+C) sub-register convention\nis implemented for all peripheral register banks.\n\nThe boot ROM at 0xBFC00000 contains a j 0xBFC40000 trampoline; firmware\nis loaded into Boot Flash 1 at physical 0x1FC40000 via -bios.\n\nReference: Microchip DS60001519E (publicly available from microchip.com)\n\nSigned-off-by: Ericson Joseph <ericsonjoseph@gmail.com>\n---\n MAINTAINERS                                |    9 +\n configs/targets/mipsel-softmmu.mak         |    1 +\n hw/mips/Kconfig                            |    8 +\n hw/mips/meson.build                        |   21 +\n hw/mips/pic32mk.c                          |  815 ++++++++++++++++++++\n hw/mips/pic32mk_adchs.c                    |  583 ++++++++++++++\n hw/mips/pic32mk_canfd.c                    | 1143 ++++++++++++++++++++++++++++\n hw/mips/pic32mk_cfg.c                      |  247 ++++++\n hw/mips/pic32mk_cru.c                      |  375 +++++++++\n hw/mips/pic32mk_dataee.c                   |  463 +++++++++++\n hw/mips/pic32mk_dma.c                      |  255 +++++++\n hw/mips/pic32mk_evic.c                     |  399 ++++++++++\n hw/mips/pic32mk_gpio.c                     |  418 ++++++++++\n hw/mips/pic32mk_i2c.c                      |  184 +++++\n hw/mips/pic32mk_ic.c                       |  384 ++++++++++\n hw/mips/pic32mk_nvm.c                      |  572 ++++++++++++++\n hw/mips/pic32mk_oc.c                       |  293 +++++++\n hw/mips/pic32mk_spi.c                      |  532 +++++++++++++\n hw/mips/pic32mk_timer.c                    |  294 +++++++\n hw/mips/pic32mk_uart.c                     |  334 ++++++++\n hw/mips/pic32mk_usb.c                      | 1033 +++++++++++++++++++++++++\n hw/mips/pic32mk_wdt.c                      |  230 ++++++\n include/hw/mips/pic32mk.h                  |  952 +++++++++++++++++++++++\n include/hw/mips/pic32mk_adchs.h            |   87 +++\n include/hw/mips/pic32mk_canfd.h            |  210 +++++\n include/hw/mips/pic32mk_evic.h             |   45 ++\n include/hw/mips/pic32mk_usb.h              |  181 +++++\n tests/functional/mipsel/pic32mk_test_fw.S  |   64 ++\n tests/functional/mipsel/pic32mk_test_fw.ld |   20 +\n tests/functional/mipsel/test_pic32mk.py    |   92 +++\n tests/qtest/pic32mk-canfd-test.c           |  411 ++++++++++\n tests/qtest/pic32mk-test.c                 |  457 +++++++++++\n 32 files changed, 11112 insertions(+)","diff":"diff --git a/MAINTAINERS b/MAINTAINERS\nindex 49f9bce818..5321eea58d 100644\n--- a/MAINTAINERS\n+++ b/MAINTAINERS\n@@ -1506,6 +1506,15 @@ F: hw/mips/boston.c\n F: hw/pci-host/xilinx-pcie.c\n F: include/hw/pci-host/xilinx-pcie.h\n \n+Microchip PIC32MK GPK/MCM\n+M: Ericson Joseph <ericsonjoseph@gmail.com>\n+S: Maintained\n+F: hw/mips/pic32mk*.c\n+F: hw/mips/pic32mk*.h\n+F: include/hw/mips/pic32mk*.h\n+F: tests/functional/mipsel/test_pic32mk.py\n+F: tests/qtest/pic32mk*.c\n+\n OpenRISC Machines\n -----------------\n or1k-sim\ndiff --git a/configs/targets/mipsel-softmmu.mak b/configs/targets/mipsel-softmmu.mak\nindex b0fba8a9d0..b2cbb3222d 100644\n--- a/configs/targets/mipsel-softmmu.mak\n+++ b/configs/targets/mipsel-softmmu.mak\n@@ -1,3 +1,4 @@\n TARGET_ARCH=mips\n TARGET_LONG_BITS=32\n TARGET_NOT_USING_LEGACY_LDST_PHYS_API=y\n+CONFIG_PIC32MK=y\ndiff --git a/hw/mips/Kconfig b/hw/mips/Kconfig\nindex b59cb2f111..a9766519f9 100644\n--- a/hw/mips/Kconfig\n+++ b/hw/mips/Kconfig\n@@ -84,5 +84,13 @@ config MIPS_BOSTON\n     select AHCI_ICH9\n     select SERIAL_MM\n \n+config PIC32MK\n+    bool\n+    default y\n+    depends on MIPS && !TARGET_BIG_ENDIAN\n+    select UNIMP\n+    select PTIMER\n+    select CAN_BUS\n+\n config FW_CFG_MIPS\n     bool\ndiff --git a/hw/mips/meson.build b/hw/mips/meson.build\nindex 390f0fd7f9..40364638a5 100644\n--- a/hw/mips/meson.build\n+++ b/hw/mips/meson.build\n@@ -12,4 +12,25 @@ mips_ss.add(when: 'CONFIG_FULOONG', if_true: files('fuloong2e.c'))\n mips_ss.add(when: 'CONFIG_MIPS_BOSTON', if_true: files('boston.c'))\n endif\n \n+mips_ss.add(when: 'CONFIG_PIC32MK', if_true: files(\n+  'pic32mk.c',\n+  'pic32mk_evic.c',\n+  'pic32mk_uart.c',\n+  'pic32mk_timer.c',\n+  'pic32mk_gpio.c',\n+  'pic32mk_spi.c',\n+  'pic32mk_i2c.c',\n+  'pic32mk_dma.c',\n+  'pic32mk_canfd.c',\n+  'pic32mk_usb.c',\n+  'pic32mk_wdt.c',\n+  'pic32mk_cru.c',\n+  'pic32mk_cfg.c',\n+  'pic32mk_adchs.c',\n+  'pic32mk_dataee.c',\n+  'pic32mk_nvm.c',\n+  'pic32mk_oc.c',\n+  'pic32mk_ic.c',\n+))\n+\n hw_arch += {'mips': mips_ss}\ndiff --git a/hw/mips/pic32mk.c b/hw/mips/pic32mk.c\nnew file mode 100644\nindex 0000000000..e3a8019507\n--- /dev/null\n+++ b/hw/mips/pic32mk.c\n@@ -0,0 +1,815 @@\n+/*\n+ * Microchip PIC32MK GPK/MCM with CAN FD — board emulation\n+ * Datasheet: DS60001519E\n+ *\n+ * Phase 2: CPU core, EVIC, UART×6, Timers×9, GPIO A-G, SPI×6, I2C×4, DMA×8\n+ *\n+ * Copyright (c) 2026 QEMU contributors\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+#include \"qemu/units.h\"\n+#include \"qemu/log.h\"\n+#include \"qemu/datadir.h\"\n+#include \"qapi/error.h\"\n+#include \"qemu/error-report.h\"\n+#include \"hw/core/boards.h\"\n+#include \"hw/core/loader.h\"\n+#include \"hw/core/clock.h\"\n+#include \"hw/core/qdev-properties.h\"\n+#include \"hw/mips/mips.h\"\n+#include \"hw/mips/pic32mk.h\"\n+#include \"hw/mips/pic32mk_evic.h\"\n+#include \"hw/mips/pic32mk_usb.h\"\n+#include \"system/address-spaces.h\"\n+#include \"system/system.h\"\n+#include \"net/can_emu.h\"\n+#include \"chardev/char.h\"\n+#include \"cpu.h\"\n+\n+/* Device type strings from our peripheral files */\n+#define TYPE_PIC32MK_UART   \"pic32mk-uart\"\n+#define TYPE_PIC32MK_TIMER  \"pic32mk-timer\"\n+#define TYPE_PIC32MK_GPIO   \"pic32mk-gpio\"\n+#define TYPE_PIC32MK_SPI    \"pic32mk-spi\"\n+#define TYPE_PIC32MK_I2C    \"pic32mk-i2c\"\n+#define TYPE_PIC32MK_DMA    \"pic32mk-dma\"\n+#define TYPE_PIC32MK_CANFD  \"pic32mk-canfd\"\n+#define TYPE_PIC32MK_USB    \"pic32mk-usb\"\n+#define TYPE_PIC32MK_WDT    \"pic32mk-wdt\"\n+#define TYPE_PIC32MK_CRU    \"pic32mk-cru\"\n+#define TYPE_PIC32MK_CFG    \"pic32mk-cfg\"\n+#define TYPE_PIC32MK_ADCHS  \"pic32mk-adchs\"\n+#define TYPE_PIC32MK_NVM    \"pic32mk-nvm\"\n+#define TYPE_PIC32MK_DATAEE \"pic32mk-dataee\"\n+#define TYPE_PIC32MK_OC     \"pic32mk-oc\"\n+#define TYPE_PIC32MK_IC     \"pic32mk-ic\"\n+\n+/*\n+ * Board state.\n+ */\n+typedef struct {\n+    MIPSCPU        *cpu;\n+    MemoryRegion    boot_rom;\n+    MemoryRegion    pflash;\n+    MemoryRegion    bflash1;\n+    MemoryRegion    bflash2;\n+    MemoryRegion    sfr;\n+    MemoryRegion    sfr_unimpl;\n+    MemoryRegion    pps_stub;   /* PPS input/output regs 0xBF801400-0xBF8017FF */\n+\n+    DeviceState    *evic;\n+} PIC32MKState;\n+\n+/*\n+ * SFR catch-all stub — logs every unimplemented register access.\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static uint64_t sfr_unimpl_read(void *opaque, hwaddr addr, unsigned size)\n+{\n+    qemu_log_mask(LOG_UNIMP,\n+                  \"pic32mk: unimplemented SFR read  @ 0x%08\" HWADDR_PRIx\n+                  \" (size %u)\\n\",\n+                  (hwaddr)(PIC32MK_SFR_BASE + addr), size);\n+    return 0;\n+}\n+\n+static void sfr_unimpl_write(void *opaque, hwaddr addr, uint64_t val,\n+                             unsigned size)\n+{\n+    qemu_log_mask(LOG_UNIMP,\n+                  \"pic32mk: unimplemented SFR write @ 0x%08\" HWADDR_PRIx\n+                  \" = 0x%08\" PRIx64 \" (size %u)\\n\",\n+                  (hwaddr)(PIC32MK_SFR_BASE + addr), val, size);\n+}\n+\n+static const MemoryRegionOps sfr_unimpl_ops = {\n+    .read       = sfr_unimpl_read,\n+    .write      = sfr_unimpl_write,\n+    .endianness = DEVICE_LITTLE_ENDIAN,\n+    .valid = {\n+        .min_access_size = 1,\n+        .max_access_size = 4,\n+    },\n+};\n+\n+/*\n+ * Silent stub — used for PPS and other write-only config registers that\n+ * need no emulation but should not generate unimplemented warnings.\n+ */\n+static uint64_t sfr_ignore_read(void *opaque, hwaddr addr, unsigned size)\n+{\n+    return 0;\n+}\n+static void sfr_ignore_write(void *opaque, hwaddr addr, uint64_t val,\n+                             unsigned size)\n+{\n+    /* silently accept */\n+}\n+static const MemoryRegionOps sfr_ignore_ops = {\n+    .read       = sfr_ignore_read,\n+    .write      = sfr_ignore_write,\n+    .endianness = DEVICE_LITTLE_ENDIAN,\n+    .valid = {\n+        .min_access_size = 4,\n+        .max_access_size = 4,\n+    },\n+};\n+\n+\n+\n+/*\n+ * Helper: create a peripheral SysBusDevice, map its MMIO into the SFR\n+ * window at the given offset (overriding the catch-all at priority 1).\n+ * Returns the DeviceState for further property/IRQ wiring.\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static DeviceState *sfr_device_create(MemoryRegion *sfr, const char *type,\n+                                      hwaddr sfr_offset, Error **errp)\n+{\n+    DeviceState *dev = qdev_new(type);\n+    if (!sysbus_realize_and_unref(SYS_BUS_DEVICE(dev), errp)) {\n+        return NULL;\n+    }\n+\n+    MemoryRegion *mr = sysbus_mmio_get_region(SYS_BUS_DEVICE(dev), 0);\n+    memory_region_add_subregion_overlap(sfr, sfr_offset, mr, 1);\n+    return dev;\n+}\n+\n+/*\n+ * Memory map initialisation\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void pic32mk_memory_init(PIC32MKState *s, MachineState *machine)\n+{\n+    MemoryRegion *sys_mem = get_system_memory();\n+\n+    /* 256 KB SRAM */\n+    memory_region_add_subregion(sys_mem, PIC32MK_RAM_BASE, machine->ram);\n+\n+    /* 1 MB Program Flash — RAM-backed so NVM controller can write */\n+    memory_region_init_ram(&s->pflash, NULL, \"pic32mk.pflash\",\n+                           PIC32MK_PFLASH_SIZE, &error_fatal);\n+    memory_region_add_subregion(sys_mem, PIC32MK_PFLASH_BASE, &s->pflash);\n+\n+    /* Boot Flash 1 — firmware loaded here via -bios */\n+    memory_region_init_rom(&s->bflash1, NULL, \"pic32mk.bflash1\",\n+                           PIC32MK_BFLASH1_SIZE, &error_fatal);\n+    memory_region_add_subregion(sys_mem, PIC32MK_BFLASH1_BASE, &s->bflash1);\n+\n+    /* Boot Flash 2 */\n+    memory_region_init_rom(&s->bflash2, NULL, \"pic32mk.bflash2\",\n+                           PIC32MK_BFLASH2_SIZE, &error_fatal);\n+    memory_region_add_subregion(sys_mem, PIC32MK_BFLASH2_BASE, &s->bflash2);\n+\n+    /*\n+     * Boot vector ROM — physical 0x1FC00000 to 0x1FC3FFFF.\n+     * Contains a two-instruction trampoline:\n+     *   j  0xBFC40000   (Boot Flash 1, KSEG1)\n+     *   nop             (branch delay slot)\n+     *\n+     * j-encoding when PC = 0xBFC00000:\n+     *   instr_index = (0xBFC40000 >> 2) & 0x3FFFFFF = 0x03F10000\n+     *   word = (2 << 26) | 0x03F10000 = 0x0BF10000\n+     */\n+    memory_region_init_rom(&s->boot_rom, NULL, \"pic32mk.boot-rom\",\n+                           PIC32MK_BOOTVEC_SIZE, &error_fatal);\n+    memory_region_add_subregion(sys_mem, PIC32MK_BOOTVEC_BASE, &s->boot_rom);\n+    {\n+        uint32_t *p = memory_region_get_ram_ptr(&s->boot_rom);\n+        p[0] = 0x0BF10000;  /* j 0xBFC40000 */\n+        p[1] = 0x00000000;  /* nop (delay slot) */\n+    }\n+\n+    /* SFR window: 1 MB container */\n+    memory_region_init(&s->sfr, NULL, \"pic32mk.sfr\", PIC32MK_SFR_SIZE);\n+    memory_region_add_subregion(sys_mem, PIC32MK_SFR_BASE, &s->sfr);\n+\n+    /* Catch-all at priority 0 */\n+    memory_region_init_io(&s->sfr_unimpl, NULL, &sfr_unimpl_ops, s,\n+                          \"pic32mk.sfr-unimpl\", PIC32MK_SFR_SIZE);\n+    memory_region_add_subregion_overlap(&s->sfr, 0, &s->sfr_unimpl, 0);\n+\n+    /* PPS (Peripheral Pin Select) 0xBF801400–0xBF8017FF — silent stub */\n+    memory_region_init_io(&s->pps_stub, NULL, &sfr_ignore_ops, NULL,\n+                          \"pic32mk.pps\", PIC32MK_PPS_SIZE);\n+    memory_region_add_subregion_overlap(&s->sfr, PIC32MK_PPS_OFFSET,\n+                                        &s->pps_stub, 1);\n+}\n+\n+/*\n+ * CPU initialisation\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void pic32mk_cpu_init(PIC32MKState *s, MachineState *machine)\n+{\n+    Clock *cpuclk;\n+\n+    cpuclk = clock_new(OBJECT(machine), \"cpu-refclk\");\n+    clock_set_hz(cpuclk, PIC32MK_CPU_HZ);\n+\n+    s->cpu = mips_cpu_create_with_clock(machine->cpu_type, cpuclk, false);\n+    if (!s->cpu) {\n+        error_report(\"pic32mk: failed to create CPU '%s'\", machine->cpu_type);\n+        exit(1);\n+    }\n+\n+    /*\n+     * Allocate the 8 CPU interrupt lines (env->irq[0..7]).\n+     * Must be called before wiring EVIC to CPU pins.\n+     */\n+    cpu_mips_irq_init_cpu(s->cpu);\n+    cpu_mips_clock_init(s->cpu);\n+}\n+\n+/*\n+ * EVIC initialisation — create device, map MMIO, wire CPU pins\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void pic32mk_evic_init(PIC32MKState *s)\n+{\n+    DeviceState *evic = qdev_new(TYPE_PIC32MK_EVIC);\n+    sysbus_realize_and_unref(SYS_BUS_DEVICE(evic), &error_fatal);\n+\n+    MemoryRegion *evic_mr = sysbus_mmio_get_region(SYS_BUS_DEVICE(evic), 0);\n+    memory_region_add_subregion_overlap(&s->sfr, PIC32MK_EVIC_OFFSET,\n+                                        evic_mr, 1);\n+    s->evic = evic;\n+\n+    /*\n+     * Wire EVIC output pins to CPU interrupt inputs.\n+     * The EVIC device stores these handles and calls qemu_set_irq()\n+     * when a pending+enabled interrupt is found at a given priority level.\n+     */\n+    PIC32MKEVICState *evic_s = PIC32MK_EVIC(evic);\n+    CPUMIPSState *env = &s->cpu->env;\n+    for (int i = 0; i < 8; i++) {\n+        evic_s->cpu_irq[i] = env->irq[i];\n+    }\n+\n+    /*\n+     * Intercept the CP0 Core Timer's interrupt output (normally fires on\n+     * env->irq[7]) and redirect it through EVIC source 0 (CT = Core Timer).\n+     *\n+     * This makes CP0 timer interrupts subject to IEC0[0] (CTIE) and\n+     * IPC0 priority configuration, as on real hardware.\n+     *\n+     * cp0_timer.c calls: qemu_irq_raise(env->irq[IPTI & 7])\n+     * With IPTI=7 (M14K default, CP0_IntCtl = 0xe0000000), that is irq[7].\n+     * We replace irq[7] with the EVIC's irq_in[PIC32MK_IRQ_CT].\n+     */\n+    env->irq[7] = qdev_get_gpio_in(evic, PIC32MK_IRQ_CT);\n+\n+    /*\n+     * Intercept CP0 Software Interrupts 0 and 1 (env->irq[0..1]) and\n+     * redirect them through the EVIC (sources CS0=1, CS1=2).\n+     *\n+     * On real PIC32MK hardware, writing Cause.IP0 triggers EVIC source\n+     * \"Core Software Interrupt 0\" (CS0) at whatever priority is configured\n+     * in IPC0.  The EVIC then delivers the interrupt through the normal\n+     * priority comparison, which prevents same-priority nesting.\n+     *\n+     * In QEMU VEIC mode the pending-vs-status comparison is\n+     *   (Cause & 0xFF00) > (Status & 0xFF00)\n+     * If IP0 (bit 8) is left in Cause while the tick ISR sets IPL=1\n+     * (bit 10), 0x0500 > 0x0400 causes an unwanted nested interrupt.\n+     *\n+     * The custom handler below:\n+     *   1) routes SW0/SW1 through the EVIC input lines, and\n+     *   2) clears the direct Cause.IP0/IP1 bit so the VEIC comparison\n+     *      only sees the EVIC-asserted priority pin (bit 10+).\n+     */\n+    env->irq[0] = qdev_get_gpio_in(evic, PIC32MK_IRQ_CS0);\n+    env->irq[1] = qdev_get_gpio_in(evic, PIC32MK_IRQ_CS1);\n+\n+    /*\n+     * Store a reference to the CPU env in the EVIC state so the\n+     * evic_set_irq handler can clear the direct Cause.IP bits for\n+     * software interrupt sources routed through the EVIC.\n+     */\n+    evic_s->cpu = s->cpu;\n+}\n+\n+/*\n+ * Peripheral initialisation helpers\n+ * -----------------------------------------------------------------------\n+ */\n+\n+/*\n+ * Create a UART instance, attach a chardev, map into the SFR window,\n+ * and connect its RX/TX/error IRQ outputs to the EVIC input lines.\n+ */\n+static void pic32mk_uart_create(PIC32MKState *s, int index,\n+                                hwaddr sfr_offset,\n+                                int irq_rx, int irq_tx, int irq_err,\n+                                Chardev *chr)\n+{\n+    DeviceState *dev = qdev_new(TYPE_PIC32MK_UART);\n+    if (chr) {\n+        qdev_prop_set_chr(dev, \"chardev\", chr);\n+    }\n+    sysbus_realize_and_unref(SYS_BUS_DEVICE(dev), &error_fatal);\n+\n+    MemoryRegion *mr = sysbus_mmio_get_region(SYS_BUS_DEVICE(dev), 0);\n+    memory_region_add_subregion_overlap(&s->sfr, sfr_offset, mr, 1);\n+\n+    /* Connect UART IRQ outputs → EVIC inputs */\n+    sysbus_connect_irq(SYS_BUS_DEVICE(dev), 0,\n+                       qdev_get_gpio_in(s->evic, irq_rx));\n+    sysbus_connect_irq(SYS_BUS_DEVICE(dev), 1,\n+                       qdev_get_gpio_in(s->evic, irq_tx));\n+    sysbus_connect_irq(SYS_BUS_DEVICE(dev), 2,\n+                       qdev_get_gpio_in(s->evic, irq_err));\n+}\n+\n+/*\n+ * Create a Timer instance, map into the SFR window, connect IRQ to EVIC.\n+ */\n+static void pic32mk_timer_create(PIC32MKState *s,\n+                                 hwaddr sfr_offset, int irq_src, bool type_a)\n+{\n+    DeviceState *dev = qdev_new(TYPE_PIC32MK_TIMER);\n+    qdev_prop_set_bit(dev, \"type-a\", type_a);\n+    sysbus_realize_and_unref(SYS_BUS_DEVICE(dev), &error_fatal);\n+    memory_region_add_subregion_overlap(&s->sfr, sfr_offset,\n+        sysbus_mmio_get_region(SYS_BUS_DEVICE(dev), 0), 1);\n+    sysbus_connect_irq(SYS_BUS_DEVICE(dev), 0,\n+                       qdev_get_gpio_in(s->evic, irq_src));\n+}\n+\n+static void pic32mk_oc_create(PIC32MKState *s, int index,\n+                              hwaddr sfr_offset, int irq_src)\n+{\n+    DeviceState *dev = qdev_new(TYPE_PIC32MK_OC);\n+    qdev_prop_set_uint8(dev, \"index\", (uint8_t)index);\n+    sysbus_realize_and_unref(SYS_BUS_DEVICE(dev), &error_fatal);\n+    memory_region_add_subregion_overlap(&s->sfr, sfr_offset,\n+        sysbus_mmio_get_region(SYS_BUS_DEVICE(dev), 0), 1);\n+    sysbus_connect_irq(SYS_BUS_DEVICE(dev), 0,\n+                       qdev_get_gpio_in(s->evic, irq_src));\n+    /* Optional chardev for waveform event streaming */\n+    Chardev *chr = qemu_chr_find(\"oc-events\");\n+    if (chr) {\n+        pic32mk_oc_set_chardev(dev, chr);\n+    }\n+}\n+\n+/*\n+ * pic32mk_ic_create — instantiate one IC peripheral and map it into the SFR\n+ * window.  irq_cap = capture IRQ index, irq_err = error IRQ index (Table 8-3).\n+ * The optional chardev \"ic-events\" is used to inject capture events from the\n+ * host; must be set before sysbus_realize_and_unref.\n+ */\n+static void pic32mk_ic_create(PIC32MKState *s, int index,\n+                              hwaddr sfr_offset, int irq_cap, int irq_err)\n+{\n+    DeviceState *dev = qdev_new(TYPE_PIC32MK_IC);\n+    qdev_prop_set_uint8(dev, \"index\", (uint8_t)index);\n+    /*\n+     * Only IC1 owns the \"ic-events\" chardev; it dispatches to all instances\n+     * via a global routing table registered during realize.\n+      */\n+    if (index == 1) {\n+        Chardev *chr = qemu_chr_find(\"ic-events\");\n+        if (chr) {\n+            qdev_prop_set_chr(dev, \"chardev\", chr);\n+        }\n+    }\n+    sysbus_realize_and_unref(SYS_BUS_DEVICE(dev), &error_fatal);\n+    memory_region_add_subregion_overlap(&s->sfr, sfr_offset,\n+        sysbus_mmio_get_region(SYS_BUS_DEVICE(dev), 0), 1);\n+    sysbus_connect_irq(SYS_BUS_DEVICE(dev), 0,\n+                       qdev_get_gpio_in(s->evic, irq_cap));\n+    sysbus_connect_irq(SYS_BUS_DEVICE(dev), 1,\n+                       qdev_get_gpio_in(s->evic, irq_err));\n+}\n+\n+static const char * const pic32mk_gpio_port_names[PIC32MK_GPIO_NPORTS] = {\n+    \"gpio-portA\", \"gpio-portB\", \"gpio-portC\", \"gpio-portD\",\n+    \"gpio-portE\", \"gpio-portF\", \"gpio-portG\",\n+};\n+\n+/*\n+ * Create a GPIO port instance, map into the SFR window.\n+ * The CN interrupt output (sysbus IRQ 0) is wired to the EVIC.\n+ * The device is registered as a named child of the machine object so that\n+ * QOM paths are predictable: /machine/gpio-portA … /machine/gpio-portG.\n+ */\n+static void pic32mk_gpio_create(PIC32MKState *s, MachineState *machine,\n+                                int port_idx, hwaddr sfr_offset, int cn_irq,\n+                                Chardev *gpio_chr)\n+{\n+    /*\n+     * Create the device first, register it as a named child of the machine\n+     * BEFORE calling sysbus_realize_and_unref().  If the object already has\n+     * a parent when device_realize() runs it will NOT be placed under the\n+     * anonymous machine/unattached container, so object_property_add_child()\n+     * won't hit the \"!child->parent\" assertion.\n+     */\n+    DeviceState *dev = qdev_new(TYPE_PIC32MK_GPIO);\n+    qdev_prop_set_uint8(dev, \"port-index\", (uint8_t)port_idx);\n+    object_property_add_child(OBJECT(machine),\n+                              pic32mk_gpio_port_names[port_idx], OBJECT(dev));\n+    if (!sysbus_realize_and_unref(SYS_BUS_DEVICE(dev), &error_fatal)) {\n+        return;\n+    }\n+\n+    /* Shared chardev for GPIO event streaming (all ports write to same chardev) */\n+    if (gpio_chr) {\n+        pic32mk_gpio_set_chardev(dev, gpio_chr);\n+    }\n+\n+    MemoryRegion *mr = sysbus_mmio_get_region(SYS_BUS_DEVICE(dev), 0);\n+    memory_region_add_subregion_overlap(&s->sfr, sfr_offset, mr, 1);\n+\n+    sysbus_connect_irq(SYS_BUS_DEVICE(dev), 0,\n+                       qdev_get_gpio_in(s->evic, cn_irq));\n+}\n+\n+/*\n+ * Create a SPI instance, map into the SFR window.\n+ */\n+static void pic32mk_spi_create(PIC32MKState *s, int index, hwaddr sfr_offset,\n+                               int irq_rx, int irq_tx, int irq_err)\n+{\n+    DeviceState *dev = qdev_new(TYPE_PIC32MK_SPI);\n+    char chr_name[16];\n+\n+    qdev_prop_set_uint8(dev, \"spi-index\", (uint8_t)index);\n+    snprintf(chr_name, sizeof(chr_name), \"spi%d\", index);\n+    Chardev *chr = qemu_chr_find(chr_name);\n+    if (chr) {\n+        qdev_prop_set_chr(dev, \"chardev\", chr);\n+    }\n+\n+    sysbus_realize_and_unref(SYS_BUS_DEVICE(dev), &error_fatal);\n+\n+    MemoryRegion *mr = sysbus_mmio_get_region(SYS_BUS_DEVICE(dev), 0);\n+    memory_region_add_subregion_overlap(&s->sfr, sfr_offset, mr, 1);\n+\n+    if (irq_rx >= 0) {\n+        sysbus_connect_irq(SYS_BUS_DEVICE(dev), 0,\n+                           qdev_get_gpio_in(s->evic, irq_rx));\n+    }\n+    if (irq_tx >= 0) {\n+        sysbus_connect_irq(SYS_BUS_DEVICE(dev), 1,\n+                           qdev_get_gpio_in(s->evic, irq_tx));\n+    }\n+    if (irq_err >= 0) {\n+        sysbus_connect_irq(SYS_BUS_DEVICE(dev), 2,\n+                           qdev_get_gpio_in(s->evic, irq_err));\n+    }\n+}\n+\n+/*\n+ * Create an I2C instance, map into the SFR window.\n+ */\n+static void pic32mk_i2c_create(PIC32MKState *s, hwaddr sfr_offset)\n+{\n+    sfr_device_create(&s->sfr, TYPE_PIC32MK_I2C, sfr_offset, &error_fatal);\n+}\n+\n+/*\n+ * pic32mk_find_canbus — look up a can-bus object created with\n+ *   -object can-bus,id=canbus<idx>\n+ * Returns NULL if no such object exists (CAN instance runs standalone).\n+ */\n+static CanBusState *pic32mk_find_canbus(int idx)\n+{\n+    char path[32];\n+    snprintf(path, sizeof(path), \"/objects/canbus%d\", idx);\n+    Object *obj = object_resolve_path_type(path, TYPE_CAN_BUS, NULL);\n+    return obj ? CAN_BUS(obj) : NULL;\n+}\n+\n+/*\n+ * Create a CAN FD instance:\n+ *  - SFR region (mmio 0) mapped into the SFR window at sfr_offset\n+ *  - Message RAM (mmio 1) mapped into system memory at msgram_phys_base\n+ *  - Single IRQ wired to EVIC input irq_src\n+ *  - canbus: optional virtual bus (NULL = standalone/loopback only)\n+ */\n+static void pic32mk_canfd_create(PIC32MKState *s,\n+                                 hwaddr sfr_offset,\n+                                 hwaddr msgram_phys_base,\n+                                 int irq_src,\n+                                 CanBusState *canbus,\n+                                 uint32_t instance_id)\n+{\n+    DeviceState *dev = qdev_new(TYPE_PIC32MK_CANFD);\n+    qdev_prop_set_uint32(dev, \"msg-ram-base\", (uint32_t)msgram_phys_base);\n+    qdev_prop_set_uint32(dev, \"instance-id\", instance_id);\n+    if (canbus) {\n+        object_property_set_link(OBJECT(dev), \"canbus\",\n+                                 OBJECT(canbus), &error_fatal);\n+    }\n+    sysbus_realize_and_unref(SYS_BUS_DEVICE(dev), &error_fatal);\n+\n+    /* SFR block overrides the catch-all at priority 1 */\n+    memory_region_add_subregion_overlap(&s->sfr, sfr_offset,\n+        sysbus_mmio_get_region(SYS_BUS_DEVICE(dev), 0), 1);\n+\n+    /* Message RAM mapped into the flat system address space */\n+    memory_region_add_subregion(get_system_memory(), msgram_phys_base,\n+        sysbus_mmio_get_region(SYS_BUS_DEVICE(dev), 1));\n+\n+    sysbus_connect_irq(SYS_BUS_DEVICE(dev), 0,\n+                       qdev_get_gpio_in(s->evic, irq_src));\n+}\n+\n+/*\n+ * Create a USB OTG instance, map SFR into the SFR window, connect IRQ.\n+ */\n+static void pic32mk_usb_create(PIC32MKState *s,\n+                               hwaddr sfr_offset, int irq_src,\n+                               const char *chardev_id)\n+{\n+    DeviceState *dev = qdev_new(TYPE_PIC32MK_USB);\n+    if (chardev_id) {\n+        Chardev *chr = qemu_chr_find(chardev_id);\n+        if (chr) {\n+            qdev_prop_set_chr(dev, \"chardev\", chr);\n+        }\n+    }\n+    sysbus_realize_and_unref(SYS_BUS_DEVICE(dev), &error_fatal);\n+    memory_region_add_subregion_overlap(&s->sfr, sfr_offset,\n+        sysbus_mmio_get_region(SYS_BUS_DEVICE(dev), 0), 1);\n+    sysbus_connect_irq(SYS_BUS_DEVICE(dev), 0,\n+                       qdev_get_gpio_in(s->evic, irq_src));\n+}\n+\n+/*\n+ * Firmware loading\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void pic32mk_load_firmware(MachineState *machine)\n+{\n+    if (!machine->firmware) {\n+        return;\n+    }\n+\n+    char *filename = qemu_find_file(QEMU_FILE_TYPE_BIOS, machine->firmware);\n+    if (!filename) {\n+        error_report(\"pic32mk: could not find firmware '%s'\",\n+                     machine->firmware);\n+        exit(1);\n+    }\n+\n+    ssize_t bios_size = load_image_targphys(filename,\n+                                            PIC32MK_BFLASH1_BASE,\n+                                            PIC32MK_BFLASH1_SIZE,\n+                                            NULL);\n+    g_free(filename);\n+\n+    if (bios_size < 0) {\n+        error_report(\"pic32mk: could not load firmware '%s'\",\n+                     machine->firmware);\n+        exit(1);\n+    }\n+}\n+\n+/*\n+ * Machine entry point\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void pic32mk_machine_init(MachineState *machine)\n+{\n+    PIC32MKState *s = g_new0(PIC32MKState, 1);\n+\n+    pic32mk_cpu_init(s, machine);\n+    pic32mk_memory_init(s, machine);\n+\n+    /* EVIC — must come before peripherals so we can wire IRQs */\n+    pic32mk_evic_init(s);\n+\n+    /* DMA — mapped into the EVIC's 4 KB page (DMA_OFFSET = EVIC+0x1000) */\n+    {\n+        DeviceState *dma = qdev_new(TYPE_PIC32MK_DMA);\n+        sysbus_realize_and_unref(SYS_BUS_DEVICE(dma), &error_fatal);\n+        MemoryRegion *mr = sysbus_mmio_get_region(SYS_BUS_DEVICE(dma), 0);\n+        memory_region_add_subregion_overlap(&s->sfr, PIC32MK_DMA_OFFSET, mr, 1);\n+        for (int i = 0; i < PIC32MK_DMA_NCHANNELS; i++) {\n+            sysbus_connect_irq(SYS_BUS_DEVICE(dma), i,\n+                               qdev_get_gpio_in(s->evic,\n+                                                PIC32MK_IRQ_DMA0 + i));\n+        }\n+    }\n+\n+    /* UART1 on serial_hd(0), UART2–6 without chardev (stub only) */\n+    pic32mk_uart_create(s, 1, PIC32MK_UART1_OFFSET,\n+                        PIC32MK_IRQ_U1RX, PIC32MK_IRQ_U1TX, PIC32MK_IRQ_U1E,\n+                        serial_hd(0));\n+    pic32mk_uart_create(s, 2, PIC32MK_UART2_OFFSET,\n+                        PIC32MK_IRQ_U2RX, PIC32MK_IRQ_U2TX, PIC32MK_IRQ_U2E,\n+                        serial_hd(1));\n+    pic32mk_uart_create(s, 3, PIC32MK_UART3_OFFSET,\n+                        PIC32MK_IRQ_U3RX, PIC32MK_IRQ_U3TX, PIC32MK_IRQ_U3E,\n+                        serial_hd(2));\n+    pic32mk_uart_create(s, 4, PIC32MK_UART4_OFFSET,\n+                        PIC32MK_IRQ_U4RX, PIC32MK_IRQ_U4TX, PIC32MK_IRQ_U4E,\n+                        serial_hd(3));\n+    pic32mk_uart_create(s, 5, PIC32MK_UART5_OFFSET,\n+                        PIC32MK_IRQ_U5RX, PIC32MK_IRQ_U5TX, PIC32MK_IRQ_U5E,\n+                        serial_hd(4));\n+    pic32mk_uart_create(s, 6, PIC32MK_UART6_OFFSET,\n+                        PIC32MK_IRQ_U6RX, PIC32MK_IRQ_U6TX, PIC32MK_IRQ_U6E,\n+                        serial_hd(5));\n+\n+    /* Timers 1–9: Timer1 is Type A (2-bit TCKPS {1,8,64,256}); 2–9 are Type B/C */\n+    pic32mk_timer_create(s, PIC32MK_T1_OFFSET, PIC32MK_IRQ_T1, true);\n+    pic32mk_timer_create(s, PIC32MK_T2_OFFSET, PIC32MK_IRQ_T2, false);\n+    pic32mk_timer_create(s, PIC32MK_T3_OFFSET, PIC32MK_IRQ_T3, false);\n+    pic32mk_timer_create(s, PIC32MK_T4_OFFSET, PIC32MK_IRQ_T4, false);\n+    pic32mk_timer_create(s, PIC32MK_T5_OFFSET, PIC32MK_IRQ_T5, false);\n+    pic32mk_timer_create(s, PIC32MK_T6_OFFSET, PIC32MK_IRQ_T6, false);\n+    pic32mk_timer_create(s, PIC32MK_T7_OFFSET, PIC32MK_IRQ_T7, false);\n+    pic32mk_timer_create(s, PIC32MK_T8_OFFSET, PIC32MK_IRQ_T8, false);\n+    pic32mk_timer_create(s, PIC32MK_T9_OFFSET, PIC32MK_IRQ_T9, false);\n+\n+    /* Output Compare OC1–OC16 (diagnostic emulation) */\n+    pic32mk_oc_create(s, 1,  PIC32MK_OC1_OFFSET,  PIC32MK_IRQ_OC1);\n+    pic32mk_oc_create(s, 2,  PIC32MK_OC2_OFFSET,  PIC32MK_IRQ_OC2);\n+    pic32mk_oc_create(s, 3,  PIC32MK_OC3_OFFSET,  PIC32MK_IRQ_OC3);\n+    pic32mk_oc_create(s, 4,  PIC32MK_OC4_OFFSET,  PIC32MK_IRQ_OC4);\n+    pic32mk_oc_create(s, 5,  PIC32MK_OC5_OFFSET,  PIC32MK_IRQ_OC5);\n+    pic32mk_oc_create(s, 6,  PIC32MK_OC6_OFFSET,  PIC32MK_IRQ_OC6);\n+    pic32mk_oc_create(s, 7,  PIC32MK_OC7_OFFSET,  PIC32MK_IRQ_OC7);\n+    pic32mk_oc_create(s, 8,  PIC32MK_OC8_OFFSET,  PIC32MK_IRQ_OC8);\n+    pic32mk_oc_create(s, 9,  PIC32MK_OC9_OFFSET,  PIC32MK_IRQ_OC9);\n+    pic32mk_oc_create(s, 10, PIC32MK_OC10_OFFSET, PIC32MK_IRQ_OC10);\n+    pic32mk_oc_create(s, 11, PIC32MK_OC11_OFFSET, PIC32MK_IRQ_OC11);\n+    pic32mk_oc_create(s, 12, PIC32MK_OC12_OFFSET, PIC32MK_IRQ_OC12);\n+    pic32mk_oc_create(s, 13, PIC32MK_OC13_OFFSET, PIC32MK_IRQ_OC13);\n+    pic32mk_oc_create(s, 14, PIC32MK_OC14_OFFSET, PIC32MK_IRQ_OC14);\n+    pic32mk_oc_create(s, 15, PIC32MK_OC15_OFFSET, PIC32MK_IRQ_OC15);\n+    pic32mk_oc_create(s, 16, PIC32MK_OC16_OFFSET, PIC32MK_IRQ_OC16);\n+\n+    /* Input Capture IC1–IC16 (full register model with FIFO) */\n+    pic32mk_ic_create(s, 1,  PIC32MK_IC1_OFFSET,  PIC32MK_IRQ_IC1,  PIC32MK_IRQ_IC1E);\n+    pic32mk_ic_create(s, 2,  PIC32MK_IC2_OFFSET,  PIC32MK_IRQ_IC2,  PIC32MK_IRQ_IC2E);\n+    pic32mk_ic_create(s, 3,  PIC32MK_IC3_OFFSET,  PIC32MK_IRQ_IC3,  PIC32MK_IRQ_IC3E);\n+    pic32mk_ic_create(s, 4,  PIC32MK_IC4_OFFSET,  PIC32MK_IRQ_IC4,  PIC32MK_IRQ_IC4E);\n+    pic32mk_ic_create(s, 5,  PIC32MK_IC5_OFFSET,  PIC32MK_IRQ_IC5,  PIC32MK_IRQ_IC5E);\n+    pic32mk_ic_create(s, 6,  PIC32MK_IC6_OFFSET,  PIC32MK_IRQ_IC6,  PIC32MK_IRQ_IC6E);\n+    pic32mk_ic_create(s, 7,  PIC32MK_IC7_OFFSET,  PIC32MK_IRQ_IC7,  PIC32MK_IRQ_IC7E);\n+    pic32mk_ic_create(s, 8,  PIC32MK_IC8_OFFSET,  PIC32MK_IRQ_IC8,  PIC32MK_IRQ_IC8E);\n+    pic32mk_ic_create(s, 9,  PIC32MK_IC9_OFFSET,  PIC32MK_IRQ_IC9,  PIC32MK_IRQ_IC9E);\n+    pic32mk_ic_create(s, 10, PIC32MK_IC10_OFFSET, PIC32MK_IRQ_IC10, PIC32MK_IRQ_IC10E);\n+    pic32mk_ic_create(s, 11, PIC32MK_IC11_OFFSET, PIC32MK_IRQ_IC11, PIC32MK_IRQ_IC11E);\n+    pic32mk_ic_create(s, 12, PIC32MK_IC12_OFFSET, PIC32MK_IRQ_IC12, PIC32MK_IRQ_IC12E);\n+    pic32mk_ic_create(s, 13, PIC32MK_IC13_OFFSET, PIC32MK_IRQ_IC13, PIC32MK_IRQ_IC13E);\n+    pic32mk_ic_create(s, 14, PIC32MK_IC14_OFFSET, PIC32MK_IRQ_IC14, PIC32MK_IRQ_IC14E);\n+    pic32mk_ic_create(s, 15, PIC32MK_IC15_OFFSET, PIC32MK_IRQ_IC15, PIC32MK_IRQ_IC15E);\n+    pic32mk_ic_create(s, 16, PIC32MK_IC16_OFFSET, PIC32MK_IRQ_IC16, PIC32MK_IRQ_IC16E);\n+\n+    /* GPIO ports A–G: CN interrupt vectors 44–50 (_CHANGE_NOTICE_x_VECTOR) */\n+    static const int cn_irqs[PIC32MK_GPIO_NPORTS] = {\n+        PIC32MK_IRQ_CNA, PIC32MK_IRQ_CNB, PIC32MK_IRQ_CNC, PIC32MK_IRQ_CND,\n+        PIC32MK_IRQ_CNE, PIC32MK_IRQ_CNF, PIC32MK_IRQ_CNG,\n+    };\n+    /* Optional: look up shared chardev \"gpio-events\" for GUI event streaming */\n+    Chardev *gpio_chr = qemu_chr_find(\"gpio-events\");\n+    for (int port = 0; port < PIC32MK_GPIO_NPORTS; port++) {\n+        pic32mk_gpio_create(s, machine, port,\n+                            PIC32MK_GPIO_OFFSET\n+                            + (hwaddr)port * PIC32MK_GPIO_PORT_SIZE,\n+                            cn_irqs[port], gpio_chr);\n+    }\n+\n+    /* SPI 1–6 */\n+    pic32mk_spi_create(s, 1, PIC32MK_SPI1_OFFSET,\n+                       PIC32MK_IRQ_SPI1_RX,\n+                       PIC32MK_IRQ_SPI1_TX,\n+                       PIC32MK_IRQ_SPI1_FAULT);\n+    pic32mk_spi_create(s, 2, PIC32MK_SPI2_OFFSET,\n+                       PIC32MK_IRQ_SPI2_RX,\n+                       PIC32MK_IRQ_SPI2_TX,\n+                       PIC32MK_IRQ_SPI2_FAULT);\n+    pic32mk_spi_create(s, 3, PIC32MK_SPI3_OFFSET,\n+                       PIC32MK_IRQ_SPI3_RX,\n+                       PIC32MK_IRQ_SPI3_TX,\n+                       PIC32MK_IRQ_SPI3_FAULT);\n+    pic32mk_spi_create(s, 4, PIC32MK_SPI4_OFFSET,\n+                       PIC32MK_IRQ_SPI4_RX,\n+                       PIC32MK_IRQ_SPI4_TX,\n+                       PIC32MK_IRQ_SPI4_FAULT);\n+    pic32mk_spi_create(s, 5, PIC32MK_SPI5_OFFSET,\n+                       PIC32MK_IRQ_SPI5_RX,\n+                       PIC32MK_IRQ_SPI5_TX,\n+                       PIC32MK_IRQ_SPI5_FAULT);\n+    pic32mk_spi_create(s, 6, PIC32MK_SPI6_OFFSET,\n+                       PIC32MK_IRQ_SPI6_RX,\n+                       PIC32MK_IRQ_SPI6_TX,\n+                       PIC32MK_IRQ_SPI6_FAULT);\n+\n+    /* I2C 1–4 */\n+    pic32mk_i2c_create(s, PIC32MK_I2C1_OFFSET);\n+    pic32mk_i2c_create(s, PIC32MK_I2C2_OFFSET);\n+    pic32mk_i2c_create(s, PIC32MK_I2C3_OFFSET);\n+    pic32mk_i2c_create(s, PIC32MK_I2C4_OFFSET);\n+\n+    /* ADCHS — High-Speed ADC at 0xBF887000 */\n+    {\n+        DeviceState *adc = qdev_new(TYPE_PIC32MK_ADCHS);\n+        object_property_add_child(OBJECT(machine), \"adchs\", OBJECT(adc));\n+        sysbus_realize_and_unref(SYS_BUS_DEVICE(adc), &error_fatal);\n+        MemoryRegion *mr = sysbus_mmio_get_region(SYS_BUS_DEVICE(adc), 0);\n+        memory_region_add_subregion_overlap(&s->sfr, PIC32MK_ADC_OFFSET,\n+                                            mr, 1);\n+        /* IRQ 0 = EOS (101), IRQ 1 = main ADC (92) */\n+        sysbus_connect_irq(SYS_BUS_DEVICE(adc), 0,\n+                           qdev_get_gpio_in(s->evic, PIC32MK_IRQ_ADC_EOS));\n+        sysbus_connect_irq(SYS_BUS_DEVICE(adc), 1,\n+                           qdev_get_gpio_in(s->evic, PIC32MK_IRQ_ADC));\n+    }\n+\n+    /* CAN FD 1–4 */\n+    pic32mk_canfd_create(s, PIC32MK_CAN1_OFFSET,\n+                         PIC32MK_CAN1_MSGRAM_BASE, PIC32MK_IRQ_CAN1,\n+                         pic32mk_find_canbus(0), 0);\n+    pic32mk_canfd_create(s, PIC32MK_CAN2_OFFSET,\n+                         PIC32MK_CAN2_MSGRAM_BASE, PIC32MK_IRQ_CAN2,\n+                         pic32mk_find_canbus(1), 1);\n+    pic32mk_canfd_create(s, PIC32MK_CAN3_OFFSET,\n+                         PIC32MK_CAN3_MSGRAM_BASE, PIC32MK_IRQ_CAN3,\n+                         pic32mk_find_canbus(2), 2);\n+    pic32mk_canfd_create(s, PIC32MK_CAN4_OFFSET,\n+                         PIC32MK_CAN4_MSGRAM_BASE, PIC32MK_IRQ_CAN4,\n+                         pic32mk_find_canbus(3), 3);\n+\n+    /* USB OTG 1–2 (Phase 4A — register-file stub) */\n+    pic32mk_usb_create(s, PIC32MK_USB1_OFFSET, PIC32MK_IRQ_USB1, \"usbcdc\");\n+    pic32mk_usb_create(s, PIC32MK_USB2_OFFSET, PIC32MK_IRQ_USB2, NULL);\n+\n+    /* WDT — register-file stub; absorbs WDTCON reads/writes and clear-key */\n+    sfr_device_create(&s->sfr, TYPE_PIC32MK_WDT, PIC32MK_WDT_OFFSET,\n+                      &error_fatal);\n+\n+    /* CFG / PMD / SYSKEY — register block at 0xBF800000 */\n+    sfr_device_create(&s->sfr, TYPE_PIC32MK_CFG, PIC32MK_CFG_OFFSET,\n+                      &error_fatal);\n+\n+    /* CRU — Clock Reference Unit at 0xBF801200 (includes RCON/RSWRST) */\n+    sfr_device_create(&s->sfr, TYPE_PIC32MK_CRU, PIC32MK_CRU_OFFSET,\n+                      &error_fatal);\n+\n+    /* NVM / Flash Controller at 0xBF800A00 */\n+    {\n+        DeviceState *nvm = qdev_new(TYPE_PIC32MK_NVM);\n+        object_property_set_link(OBJECT(nvm), \"pflash\",\n+                                 OBJECT(&s->pflash), &error_fatal);\n+        sysbus_realize_and_unref(SYS_BUS_DEVICE(nvm), &error_fatal);\n+        MemoryRegion *mr = sysbus_mmio_get_region(SYS_BUS_DEVICE(nvm), 0);\n+        memory_region_add_subregion_overlap(&s->sfr, PIC32MK_NVM_OFFSET,\n+                                            mr, 1);\n+        sysbus_connect_irq(SYS_BUS_DEVICE(nvm), 0,\n+                           qdev_get_gpio_in(s->evic, PIC32MK_IRQ_FCE));\n+    }\n+\n+    /* Data EEPROM at 0xBF829000 — 4 KB, optionally backed by host file */\n+    {\n+        DeviceState *ee = qdev_new(TYPE_PIC32MK_DATAEE);\n+        /* Backing file: use -global pic32mk-dataee.filename=<path> */\n+        sysbus_realize_and_unref(SYS_BUS_DEVICE(ee), &error_fatal);\n+        MemoryRegion *mr = sysbus_mmio_get_region(SYS_BUS_DEVICE(ee), 0);\n+        memory_region_add_subregion_overlap(&s->sfr, PIC32MK_DATAEE_OFFSET,\n+                                            mr, 1);\n+        sysbus_connect_irq(SYS_BUS_DEVICE(ee), 0,\n+                           qdev_get_gpio_in(s->evic, PIC32MK_IRQ_DATAEE));\n+    }\n+\n+    pic32mk_load_firmware(machine);\n+}\n+\n+/*\n+ * MachineClass registration\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void pic32mk_machine_class_init(MachineClass *mc)\n+{\n+    mc->desc           = \"Microchip PIC32MK GPK/MCM with CAN FD\";\n+    mc->init           = pic32mk_machine_init;\n+    mc->max_cpus       = 1;\n+    /*\n+     * microAptiv: MIPS32r2 + FPU + DSP R2 + microMIPS + MCU ASE,\n+     * fixed-mapping MMU, VEIC — matches PIC32MK DS60001519E §3.\n+     */\n+    mc->default_cpu_type = MIPS_CPU_TYPE_NAME(\"microAptiv\");\n+    mc->default_ram_id   = \"pic32mk.ram\";\n+    mc->default_ram_size = PIC32MK_RAM_SIZE;\n+    mc->no_parallel      = 1;\n+    mc->no_floppy        = 1;\n+    mc->no_cdrom         = 1;\n+}\n+\n+DEFINE_MACHINE(\"pic32mk\", pic32mk_machine_class_init)\ndiff --git a/hw/mips/pic32mk_adchs.c b/hw/mips/pic32mk_adchs.c\nnew file mode 100644\nindex 0000000000..64d1079c02\n--- /dev/null\n+++ b/hw/mips/pic32mk_adchs.c\n@@ -0,0 +1,583 @@\n+/*\n+ * PIC32MK ADCHS — High-Speed ADC peripheral emulation\n+ * Datasheet: DS60001519E, §22\n+ *\n+ * Emulates the 12-bit pipelined SAR ADC found on PIC32MK GPK/MCM devices.\n+ * 46 data channels (0–27, 33–41, 45–53), 7 ADC modules (0–5, 7).\n+ *\n+ * Key features implemented:\n+ *   - All SFR registers with SET/CLR/INV sub-register support\n+ *   - Software-triggered conversions (GSWTRG, GLSWTRG, RQCNVRT)\n+ *   - Instant conversion model (no timing delays)\n+ *   - Host-injectable analog values via QOM properties (adc-ch0 … adc-ch53)\n+ *   - End-of-Scan (EOS) + main ADC interrupt outputs to EVIC\n+ *   - BGVRRDY and WKRDYx bits always report ready (emulation shortcut)\n+ *\n+ * Stubs (LOG_UNIMP): digital filters, digital comparators, DMA, early IRQ.\n+ *\n+ * Copyright (c) 2026 QEMU contributors\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+#include \"qemu/log.h\"\n+#include \"qemu/module.h\"\n+#include \"qapi/error.h\"\n+#include \"qapi/visitor.h\"\n+#include \"hw/core/sysbus.h\"\n+#include \"hw/core/irq.h\"\n+#include \"hw/core/qdev-properties.h\"\n+#include \"hw/mips/pic32mk.h\"\n+#include \"hw/mips/pic32mk_adchs.h\"\n+\n+/*\n+ * Channel validity table — true for channels present on PIC32MK1024MCM100\n+ * Channels: 0–27, 33–41, 45–53.  Missing: 28–32, 42–44.\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static bool adchs_channel_valid(unsigned ch)\n+{\n+    if (ch <= 27) {\n+        return true;\n+    }\n+    if (ch >= 33 && ch <= 41) {\n+        return true;\n+    }\n+    if (ch >= 45 && ch <= 53) {\n+        return true;\n+    }\n+    return false;\n+}\n+\n+/*\n+ * Map channel number to its data-ready IRQ number.\n+ * DATA0=106, DATA1=107, …, DATA27=133, (gap), DATA33=139, …, DATA41=147,\n+ * (gap), DATA45=151, …, DATA53=159.\n+ */\n+static int G_GNUC_UNUSED adchs_channel_irq(unsigned ch)\n+{\n+    if (ch <= 27) {\n+        return PIC32MK_IRQ_ADC_DATA0 + (int)ch;\n+    }\n+    if (ch >= 33 && ch <= 41) {\n+        return 139 + (int)(ch - 33);\n+    }\n+    if (ch >= 45 && ch <= 53) {\n+        return 151 + (int)(ch - 45);\n+    }\n+    return -1;\n+}\n+\n+/*\n+ * SET/CLR/INV helper (PIC32MK convention: +0=REG, +4=CLR, +8=SET, +C=INV)\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void apply_sci(uint32_t *reg, uint32_t val, int sub)\n+{\n+    switch (sub) {\n+    case 0:\n+        *reg  = val;\n+        break;\n+    case 4:\n+        *reg &= ~val;\n+        break;\n+    case 8:\n+        *reg |= val;\n+        break;\n+    case 12:\n+        *reg ^= val;\n+        break;\n+    }\n+}\n+\n+/*\n+ * Conversion engine\n+ * -----------------------------------------------------------------------\n+ */\n+\n+/*\n+ * Perform conversion for a single channel: copy the host-injected analog\n+ * value into the data register, set the data-ready status bit, and fire\n+ * the per-channel IRQ if the global interrupt-enable bit is set.\n+ */\n+static void adchs_convert_channel(PIC32MKADCHSState *s, unsigned ch)\n+{\n+    if (ch >= PIC32MK_ADC_MAX_CH || !adchs_channel_valid(ch)) {\n+        return;\n+    }\n+\n+    /* Store 12-bit result in data register (upper bits zero) */\n+    s->adcdata[ch] = s->analog_input[ch] & 0xFFF;\n+\n+    /* Set data-ready status bit */\n+    if (ch < 32) {\n+        s->adcdstat[0] |= (1u << ch);\n+    } else {\n+        s->adcdstat[1] |= (1u << (ch - 32));\n+    }\n+\n+    /* Fire per-channel IRQ if ADCGIRQEN bit is set */\n+    bool girq_en;\n+    if (ch < 32) {\n+        girq_en = !!(s->adcgirqen[0] & (1u << ch));\n+    } else {\n+        girq_en = !!(s->adcgirqen[1] & (1u << (ch - 32)));\n+    }\n+\n+    if (girq_en) {\n+        /*\n+         * Per-channel data IRQs go directly to EVIC IFS bits.\n+         * We pulse the main ADC IRQ output for simplicity — the EVIC\n+         * will latch IFS[irq_num].  Individual data channel IRQ wiring\n+         * can be extended if firmware requires it.\n+         */\n+        qemu_irq_pulse(s->irq_main);\n+    }\n+}\n+\n+/*\n+ * Scan conversion: iterate over all channels enabled in ADCCSS1/2,\n+ * convert each, then signal End-of-Scan.\n+ */\n+static void adchs_scan_convert(PIC32MKADCHSState *s)\n+{\n+    /* Only convert if module is ON */\n+    if (!(s->adccon1 & PIC32MK_ADCCON1_ON)) {\n+        return;\n+    }\n+\n+    /* Scan CSS1 (channels 0–31) */\n+    for (int ch = 0; ch < 32; ch++) {\n+        if (s->adccss[0] & (1u << ch)) {\n+            adchs_convert_channel(s, ch);\n+        }\n+    }\n+\n+    /* Scan CSS2 (channels 32–53) */\n+    for (int ch = 0; ch < 22; ch++) {\n+        if (s->adccss[1] & (1u << ch)) {\n+            adchs_convert_channel(s, ch + 32);\n+        }\n+    }\n+\n+    /* Signal End-of-Scan */\n+    qemu_irq_pulse(s->irq_eos);\n+}\n+\n+/*\n+ * Register dispatch\n+ * -----------------------------------------------------------------------\n+ */\n+\n+/*\n+ * Map an offset (base, i.e. aligned to 0x10) to a register pointer.\n+ * Returns NULL for offsets that are read-only, data, or unimplemented.\n+ */\n+static uint32_t *adchs_find_reg(PIC32MKADCHSState *s, hwaddr base)\n+{\n+    switch (base) {\n+    case PIC32MK_ADCCON1:\n+        return &s->adccon1;\n+    case PIC32MK_ADCCON2:\n+        return &s->adccon2;\n+    case PIC32MK_ADCCON3:\n+        return &s->adccon3;\n+    case PIC32MK_ADCTRGMODE:\n+        return &s->adctrgmode;\n+\n+    case PIC32MK_ADCIMCON1:\n+        return &s->adcimcon[0];\n+    case PIC32MK_ADCIMCON2:\n+        return &s->adcimcon[1];\n+    case PIC32MK_ADCIMCON3:\n+        return &s->adcimcon[2];\n+    case PIC32MK_ADCIMCON4:\n+        return &s->adcimcon[3];\n+\n+    case PIC32MK_ADCGIRQEN1:\n+        return &s->adcgirqen[0];\n+    case PIC32MK_ADCGIRQEN2:\n+        return &s->adcgirqen[1];\n+\n+    case PIC32MK_ADCCSS1:\n+        return &s->adccss[0];\n+    case PIC32MK_ADCCSS2:\n+        return &s->adccss[1];\n+\n+    case PIC32MK_ADCDSTAT1:\n+        return &s->adcdstat[0];\n+    case PIC32MK_ADCDSTAT2:\n+        return &s->adcdstat[1];\n+\n+    case PIC32MK_ADCCMPEN1:\n+        return &s->adccmpen[0];\n+    case PIC32MK_ADCCMPEN2:\n+        return &s->adccmpen[1];\n+    case PIC32MK_ADCCMPEN3:\n+        return &s->adccmpen[2];\n+    case PIC32MK_ADCCMPEN4:\n+        return &s->adccmpen[3];\n+\n+    case PIC32MK_ADCCMP1:\n+        return &s->adccmp[0];\n+    case PIC32MK_ADCCMP2:\n+        return &s->adccmp[1];\n+    case PIC32MK_ADCCMP3:\n+        return &s->adccmp[2];\n+    case PIC32MK_ADCCMP4:\n+        return &s->adccmp[3];\n+\n+    case PIC32MK_ADCFLTR1:\n+        return &s->adcfltr[0];\n+    case PIC32MK_ADCFLTR2:\n+        return &s->adcfltr[1];\n+    case PIC32MK_ADCFLTR3:\n+        return &s->adcfltr[2];\n+    case PIC32MK_ADCFLTR4:\n+        return &s->adcfltr[3];\n+\n+    case PIC32MK_ADCTRG1:\n+        return &s->adctrg[0];\n+    case PIC32MK_ADCTRG2:\n+        return &s->adctrg[1];\n+    case PIC32MK_ADCTRG3:\n+        return &s->adctrg[2];\n+    case PIC32MK_ADCTRG4:\n+        return &s->adctrg[3];\n+    case PIC32MK_ADCTRG5:\n+        return &s->adctrg[4];\n+    case PIC32MK_ADCTRG6:\n+        return &s->adctrg[5];\n+    case PIC32MK_ADCTRG7:\n+        return &s->adctrg[6];\n+\n+    case PIC32MK_ADCCMPCON1:\n+        return &s->adccmpcon[0];\n+    case PIC32MK_ADCCMPCON2:\n+        return &s->adccmpcon[1];\n+    case PIC32MK_ADCCMPCON3:\n+        return &s->adccmpcon[2];\n+    case PIC32MK_ADCCMPCON4:\n+        return &s->adccmpcon[3];\n+\n+    case PIC32MK_ADCBASE:\n+        return &s->adcbase;\n+    case PIC32MK_ADCTRGSNS:\n+        return &s->adctrgsns;\n+\n+    case PIC32MK_ADC0TIME:\n+        return &s->adctime[0];\n+    case PIC32MK_ADC1TIME:\n+        return &s->adctime[1];\n+    case PIC32MK_ADC2TIME:\n+        return &s->adctime[2];\n+    case PIC32MK_ADC3TIME:\n+        return &s->adctime[3];\n+    case PIC32MK_ADC4TIME:\n+        return &s->adctime[4];\n+    case PIC32MK_ADC5TIME:\n+        return &s->adctime[5];\n+\n+    case PIC32MK_ADCEIEN1:\n+        return &s->adceien[0];\n+    case PIC32MK_ADCEIEN2:\n+        return &s->adceien[1];\n+    case PIC32MK_ADCEISTAT1:\n+        return &s->adceistat[0];\n+    case PIC32MK_ADCEISTAT2:\n+        return &s->adceistat[1];\n+\n+    case PIC32MK_ADCANCON:\n+        return &s->adcancon;\n+\n+    case PIC32MK_ADC0CFG:\n+        return &s->adccfg[0];\n+    case PIC32MK_ADC1CFG:\n+        return &s->adccfg[1];\n+    case PIC32MK_ADC2CFG:\n+        return &s->adccfg[2];\n+    case PIC32MK_ADC3CFG:\n+        return &s->adccfg[3];\n+    case PIC32MK_ADC4CFG:\n+        return &s->adccfg[4];\n+    case PIC32MK_ADC5CFG:\n+        return &s->adccfg[5];\n+    case PIC32MK_ADC6CFG:\n+        return &s->adccfg[6];\n+    case PIC32MK_ADC7CFG:\n+        return &s->adccfg[7];\n+\n+    case PIC32MK_ADCSYSCFG0:\n+        return &s->adcsyscfg[0];\n+    case PIC32MK_ADCSYSCFG1:\n+        return &s->adcsyscfg[1];\n+\n+    default:\n+        return NULL;\n+    }\n+}\n+\n+/*\n+ * MMIO read\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static uint64_t adchs_read(void *opaque, hwaddr addr, unsigned size)\n+{\n+    PIC32MKADCHSState *s = opaque;\n+    hwaddr base = addr & ~(hwaddr)0xF;\n+\n+    /*\n+     * ADCCON2: always report reference voltage ready and no fault.\n+     * Firmware polls these bits during initialization.\n+     */\n+    if (base == PIC32MK_ADCCON2) {\n+        return (s->adccon2 | PIC32MK_ADCCON2_BGVRRDY)\n+               & ~PIC32MK_ADCCON2_REFFLT;\n+    }\n+\n+    /*\n+     * ADCANCON: mirror ANENx bits into WKRDYx positions.\n+     * Firmware enables ANENx then polls WKRDYx until ready.\n+     */\n+    if (base == PIC32MK_ADCANCON) {\n+        uint32_t anen = s->adcancon & 0xFFu;\n+        return (s->adcancon & ~0xFF00u) | (anen << 8);\n+    }\n+\n+    /* ADCDATA registers (0x600–0x950 range, stride 0x10) */\n+    if (base >= PIC32MK_ADCDATA_BASE &&\n+        base < PIC32MK_ADCDATA_BASE + PIC32MK_ADC_MAX_CH * PIC32MK_ADCDATA_STRIDE) {\n+        unsigned ch = (base - PIC32MK_ADCDATA_BASE) / PIC32MK_ADCDATA_STRIDE;\n+        if (ch < PIC32MK_ADC_MAX_CH && adchs_channel_valid(ch)) {\n+            /* Auto-clear data-ready status on read */\n+            if (ch < 32) {\n+                s->adcdstat[0] &= ~(1u << ch);\n+            } else {\n+                s->adcdstat[1] &= ~(1u << (ch - 32));\n+            }\n+            return s->adcdata[ch];\n+        }\n+    }\n+\n+    /* Standard register dispatch */\n+    uint32_t *reg = adchs_find_reg(s, base);\n+    if (reg) {\n+        return *reg;\n+    }\n+\n+    qemu_log_mask(LOG_UNIMP,\n+                  \"pic32mk_adchs: unimplemented read @ 0x%04\" HWADDR_PRIx \"\\n\",\n+                  addr);\n+    return 0;\n+}\n+\n+/*\n+ * MMIO write\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void adchs_write(void *opaque, hwaddr addr, uint64_t val, unsigned size)\n+{\n+    PIC32MKADCHSState *s = opaque;\n+    int sub     = (int)(addr & 0xF);\n+    hwaddr base = addr & ~(hwaddr)0xF;\n+\n+    /* ADCDATA registers are read-only from firmware perspective */\n+    if (base >= PIC32MK_ADCDATA_BASE &&\n+        base < PIC32MK_ADCDATA_BASE + PIC32MK_ADC_MAX_CH * PIC32MK_ADCDATA_STRIDE) {\n+        qemu_log_mask(LOG_GUEST_ERROR,\n+                      \"pic32mk_adchs: write to read-only ADCDATA @ 0x%04\"\n+                      HWADDR_PRIx \"\\n\", addr);\n+        return;\n+    }\n+\n+    /* Special handling for ADCCON3 — trigger bits */\n+    if (base == PIC32MK_ADCCON3) {\n+        uint32_t old = s->adccon3;\n+        apply_sci(&s->adccon3, (uint32_t)val, sub);\n+\n+        /* GSWTRG: global software trigger → scan conversion */\n+        if ((s->adccon3 & PIC32MK_ADCCON3_GSWTRG) &&\n+            !(old & PIC32MK_ADCCON3_GSWTRG)) {\n+            adchs_scan_convert(s);\n+            /* GSWTRG is self-clearing */\n+            s->adccon3 &= ~PIC32MK_ADCCON3_GSWTRG;\n+        }\n+\n+        /* GLSWTRG: global level software trigger → also scan */\n+        if ((s->adccon3 & PIC32MK_ADCCON3_GLSWTRG) &&\n+            !(old & PIC32MK_ADCCON3_GLSWTRG)) {\n+            adchs_scan_convert(s);\n+        }\n+\n+        /* RQCNVRT: request single-channel conversion */\n+        if ((s->adccon3 & PIC32MK_ADCCON3_RQCNVRT) &&\n+            !(old & PIC32MK_ADCCON3_RQCNVRT)) {\n+            unsigned ch = s->adccon3 & PIC32MK_ADCCON3_ADINSEL_MASK;\n+            adchs_convert_channel(s, ch);\n+            /* RQCNVRT is self-clearing */\n+            s->adccon3 &= ~PIC32MK_ADCCON3_RQCNVRT;\n+        }\n+        return;\n+    }\n+\n+    /* Standard register dispatch */\n+    uint32_t *reg = adchs_find_reg(s, base);\n+    if (reg) {\n+        apply_sci(reg, (uint32_t)val, sub);\n+        return;\n+    }\n+\n+    qemu_log_mask(LOG_UNIMP,\n+                  \"pic32mk_adchs: unimplemented write @ 0x%04\" HWADDR_PRIx\n+                  \" = 0x%08\" PRIx64 \"\\n\",\n+                  addr, val);\n+}\n+\n+static const MemoryRegionOps adchs_ops = {\n+    .read       = adchs_read,\n+    .write      = adchs_write,\n+    .endianness = DEVICE_LITTLE_ENDIAN,\n+    .valid = {\n+        .min_access_size = 4,\n+        .max_access_size = 4,\n+    },\n+};\n+\n+/*\n+ * QOM properties — host-side analog value injection\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void adchs_prop_ch_get(Object *obj, Visitor *v, const char *name,\n+                              void *opaque, Error **errp)\n+{\n+    PIC32MKADCHSState *s = PIC32MK_ADCHS(obj);\n+    unsigned ch = (unsigned)(uintptr_t)opaque;\n+    int64_t val = s->analog_input[ch];\n+    visit_type_int(v, name, &val, errp);\n+}\n+\n+static void adchs_prop_ch_set(Object *obj, Visitor *v, const char *name,\n+                              void *opaque, Error **errp)\n+{\n+    PIC32MKADCHSState *s = PIC32MK_ADCHS(obj);\n+    unsigned ch = (unsigned)(uintptr_t)opaque;\n+    int64_t val;\n+\n+    if (!visit_type_int(v, name, &val, errp)) {\n+        return;\n+    }\n+    if (val < 0 || val > 4095) {\n+        error_setg(errp, \"adc-ch%u value must be 0–4095 (12-bit)\", ch);\n+        return;\n+    }\n+    s->analog_input[ch] = (uint16_t)val;\n+    s->adcdata[ch]      = (uint16_t)(val & 0xFFF);\n+}\n+\n+static void adchs_prop_data_get(Object *obj, Visitor *v, const char *name,\n+                                void *opaque, Error **errp)\n+{\n+    PIC32MKADCHSState *s = PIC32MK_ADCHS(obj);\n+    unsigned ch = (unsigned)(uintptr_t)opaque;\n+    int64_t val = s->adcdata[ch];\n+    visit_type_int(v, name, &val, errp);\n+}\n+\n+/*\n+ * Device lifecycle\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void pic32mk_adchs_reset(DeviceState *dev)\n+{\n+    PIC32MKADCHSState *s = PIC32MK_ADCHS(dev);\n+\n+    s->adccon1    = 0;\n+    s->adccon2    = 0;\n+    s->adccon3    = 0;\n+    s->adctrgmode = 0;\n+\n+    memset(s->adcimcon, 0, sizeof(s->adcimcon));\n+    memset(s->adcgirqen, 0, sizeof(s->adcgirqen));\n+    memset(s->adccss, 0, sizeof(s->adccss));\n+    memset(s->adcdstat, 0, sizeof(s->adcdstat));\n+    memset(s->adccmpen, 0, sizeof(s->adccmpen));\n+    memset(s->adccmp, 0, sizeof(s->adccmp));\n+    memset(s->adccmpcon, 0, sizeof(s->adccmpcon));\n+    memset(s->adcfltr, 0, sizeof(s->adcfltr));\n+    memset(s->adctrg, 0, sizeof(s->adctrg));\n+    s->adctrgsns = 0;\n+    memset(s->adctime, 0, sizeof(s->adctime));\n+    memset(s->adceien, 0, sizeof(s->adceien));\n+    memset(s->adceistat, 0, sizeof(s->adceistat));\n+    s->adcancon  = 0;\n+    s->adcbase   = 0;\n+    memset(s->adccfg, 0, sizeof(s->adccfg));\n+    memset(s->adcsyscfg, 0, sizeof(s->adcsyscfg));\n+    memset(s->adcdata, 0, sizeof(s->adcdata));\n+    /* analog_input[] is NOT reset — host injections persist across resets */\n+}\n+\n+static void pic32mk_adchs_init(Object *obj)\n+{\n+    PIC32MKADCHSState *s = PIC32MK_ADCHS(obj);\n+\n+    memory_region_init_io(&s->mr, obj, &adchs_ops, s,\n+                          TYPE_PIC32MK_ADCHS, PIC32MK_ADC_SIZE);\n+    sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->mr);\n+\n+    /* IRQ outputs: 0=EOS, 1=main */\n+    sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq_eos);\n+    sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq_main);\n+\n+    /*\n+     * QOM properties for host-side analog value injection.\n+     * adc-ch<N>   — int r/w — set the 12-bit analog value for channel N\n+     * adc-data<N> — int r/o — read the last conversion result for channel N\n+     */\n+    for (unsigned ch = 0; ch < PIC32MK_ADC_MAX_CH; ch++) {\n+        if (!adchs_channel_valid(ch)) {\n+            continue;\n+        }\n+\n+        char name_ch[16], name_data[16];\n+        snprintf(name_ch, sizeof(name_ch), \"adc-ch%u\", ch);\n+        snprintf(name_data, sizeof(name_data), \"adc-data%u\", ch);\n+\n+        object_property_add(obj, name_ch, \"int\",\n+                            adchs_prop_ch_get,\n+                            adchs_prop_ch_set,\n+                            NULL, (void *)(uintptr_t)ch);\n+\n+        object_property_add(obj, name_data, \"int\",\n+                            adchs_prop_data_get,\n+                            NULL,   /* read-only */\n+                            NULL, (void *)(uintptr_t)ch);\n+    }\n+}\n+\n+static void pic32mk_adchs_class_init(ObjectClass *klass, const void *data)\n+{\n+    DeviceClass *dc = DEVICE_CLASS(klass);\n+    device_class_set_legacy_reset(dc, pic32mk_adchs_reset);\n+}\n+\n+static const TypeInfo pic32mk_adchs_info = {\n+    .name          = TYPE_PIC32MK_ADCHS,\n+    .parent        = TYPE_SYS_BUS_DEVICE,\n+    .instance_size = sizeof(PIC32MKADCHSState),\n+    .instance_init = pic32mk_adchs_init,\n+    .class_init    = pic32mk_adchs_class_init,\n+};\n+\n+static void pic32mk_adchs_register_types(void)\n+{\n+    type_register_static(&pic32mk_adchs_info);\n+}\n+\n+type_init(pic32mk_adchs_register_types)\ndiff --git a/hw/mips/pic32mk_canfd.c b/hw/mips/pic32mk_canfd.c\nnew file mode 100644\nindex 0000000000..eb2a89a2ce\n--- /dev/null\n+++ b/hw/mips/pic32mk_canfd.c\n@@ -0,0 +1,1143 @@\n+/*\n+ * Microchip PIC32MK CAN FD controller emulation\n+ * Based on PIC32MK GPK/MCM with CAN FD Family Datasheet (DS60001519E)\n+ * and Microchip CAN FD Controller Reference Manual (DS60001507).\n+ *\n+ * Implements:\n+ *  - Two MemoryRegions per instance: SFR block + Message RAM\n+ *  - Operating mode transitions (Config / Normal / Internal Loopback / others)\n+ *  - TX Queue (TXQ) and up to 31 configurable FIFOs\n+ *  - UINC pointer-advance protocol (head/tail management)\n+ *  - Internal loopback: TX → acceptance filter → RX FIFO\n+ *  - CiINT two-level interrupt aggregator → single EVIC IRQ line\n+ *  - 32 acceptance filters with mask\n+ *  - SET/CLR/INV register aliasing for key registers\n+ *\n+ * Not yet implemented:\n+ *  - CiFIFOBA register (message RAM base set by firmware; needed for\n+ *    Harmony3-generated drivers that call CFD1FIFOBA = KVA_TO_PA(buf))\n+ *  - TEF (TX Event FIFO) population on TX complete\n+ *  - CiTBC free-running counter (timestamp)\n+ *\n+ * Copyright (c) 2026 QEMU contributors\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+#include \"qemu/log.h\"\n+#include \"qemu/units.h\"\n+#include \"qapi/error.h\"\n+#include \"hw/core/qdev-properties.h\"\n+#include \"hw/core/sysbus.h\"\n+#include \"hw/core/irq.h\"\n+#include \"hw/mips/pic32mk.h\"\n+#include \"hw/mips/pic32mk_canfd.h\"\n+\n+/*\n+ * DLC / PLSIZE helpers\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static const uint8_t dlc_to_bytes_fd[16] = {\n+    0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64\n+};\n+\n+static uint8_t canfd_dlc_bytes(uint8_t dlc, bool fdf)\n+{\n+    if (dlc > 8 && !fdf) {\n+        return 8;\n+    }\n+    return dlc_to_bytes_fd[dlc & 0xF];\n+}\n+\n+/* PLSIZE field → payload bytes */\n+static const uint8_t plsize_to_bytes[8] = { 8, 12, 16, 20, 24, 32, 48, 64 };\n+\n+static uint32_t canfd_obj_size(uint32_t fifocon)\n+{\n+    uint8_t plsize = (fifocon >> CANFD_FIFO_PLSIZE_SHIFT) & 0x7u;\n+    return 8u + plsize_to_bytes[plsize];   /* 2 header words + payload */\n+}\n+\n+/*\n+ * Message RAM layout helpers\n+ *\n+ * Layout (sequential from msg_ram_phys):\n+ *   [TXQ  region] if TXQEN\n+ *   [TEF  region] if STEF\n+ *   [FIFO 1..31]\n+ *\n+ * For simplicity we pre-allocate the maximum per region and compute\n+ * offsets statically from the FIFO configuration registers.\n+ * -----------------------------------------------------------------------\n+ */\n+\n+#define CANFD_MAX_OBJ_SIZE  72u   /* 8 header + 64 payload */\n+#define CANFD_MAX_DEPTH     32u   /* FSIZE+1 max */\n+\n+static uint32_t canfd_txq_ram_base(PIC32MKCANFDState *s)\n+{\n+    return s->msg_ram_phys;\n+}\n+\n+static uint32_t canfd_txq_obj_size(PIC32MKCANFDState *s)\n+{\n+    return canfd_obj_size(s->txqcon);\n+}\n+\n+static uint32_t canfd_txq_depth(PIC32MKCANFDState *s)\n+{\n+    return ((s->txqcon >> CANFD_FIFO_FSIZE_SHIFT) & 0x1Fu) + 1u;\n+}\n+\n+/* Region base for FIFO n (1-based) within the message RAM buffer */\n+static uint32_t canfd_fifo_ram_base(PIC32MKCANFDState *s, int n)\n+{\n+    /* TXQ region */\n+    uint32_t off = 0;\n+    if (s->con & CANFD_CON_TXQEN) {\n+        off += canfd_txq_obj_size(s) * canfd_txq_depth(s);\n+    }\n+    /* TEF region (each TEF object = 8 bytes, no payload) */\n+    if (s->con & CANFD_CON_STEF) {\n+        uint32_t tef_depth = ((s->tefcon >> CANFD_FIFO_FSIZE_SHIFT) & 0x1Fu) + 1u;\n+        off += 8u * tef_depth;\n+    }\n+    /* FIFOs 1..n-1 */\n+    for (int i = 1; i < n; i++) {\n+        uint32_t depth = ((s->fifocon[i] >> CANFD_FIFO_FSIZE_SHIFT) & 0x1Fu) + 1u;\n+        off += canfd_obj_size(s->fifocon[i]) * depth;\n+    }\n+    return s->msg_ram_phys + off;\n+}\n+\n+static uint32_t canfd_fifo_slot_ua(PIC32MKCANFDState *s, int n, uint8_t slot)\n+{\n+    return canfd_fifo_ram_base(s, n) + (uint32_t)slot * canfd_obj_size(s->fifocon[n]);\n+}\n+\n+static uint32_t canfd_txq_slot_ua(PIC32MKCANFDState *s, uint8_t slot)\n+{\n+    return canfd_txq_ram_base(s) + (uint32_t)slot * canfd_txq_obj_size(s);\n+}\n+\n+/*\n+ * IRQ update — recomputes CiINT status bits and asserts/deasserts IRQ\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void canfd_update_irq(PIC32MKCANFDState *s)\n+{\n+    bool fire = false;\n+\n+    /* RXIF: any RX FIFO has data */\n+    if (s->cint & CANFD_INT_RXIE) {\n+        if (s->rxif) {\n+            s->cint |= CANFD_INT_RXIF;\n+            fire = true;\n+        } else {\n+            s->cint &= ~CANFD_INT_RXIF;\n+        }\n+    }\n+\n+    /* TXIF: any TX FIFO / TXQ completed */\n+    if (s->cint & CANFD_INT_TXIE) {\n+        if (s->txif) {\n+            s->cint |= CANFD_INT_TXIF;\n+            fire = true;\n+        } else {\n+            s->cint &= ~CANFD_INT_TXIF;\n+        }\n+    }\n+\n+    /* MODIF: mode changed and MODIE enabled */\n+    if ((s->cint & CANFD_INT_MODIE) && (s->cint & CANFD_INT_MODIF)) {\n+        fire = true;\n+    }\n+\n+    /* RXOVIF: any RX overflow and RXOVIE enabled */\n+    if ((s->cint & CANFD_INT_RXOVIE) && (s->cint & CANFD_INT_RXOVIF)) {\n+        fire = true;\n+    }\n+\n+    qemu_set_irq(s->irq, fire ? 1 : 0);\n+}\n+\n+/*\n+ * Acceptance filter matching\n+ * Returns destination FIFO index (1-based) or -1 if no match.\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static int canfd_find_fifo(PIC32MKCANFDState *s, uint32_t id, bool xtd)\n+{\n+    for (int n = 0; n < 32; n++) {\n+        /* Each CiFLTCON register holds 4 filter bytes */\n+        uint8_t fltcon_byte = (s->fltcon[n / 4] >> ((n % 4) * 8)) & 0xFFu;\n+        if (!(fltcon_byte & 0x80u)) {\n+            continue;   /* FLTEN = 0 */\n+        }\n+\n+        uint32_t obj  = s->fltobj[n];\n+        uint32_t msk  = s->mask[n];\n+\n+        /*\n+         * MIDE bit (bit 29 of CiMASKn): when set, the filter only matches\n+         * frames whose IDE bit equals FLTOBJ.IDE (bit 30).\n+         * When MIDE=0, the filter accepts both standard and extended frames.\n+         */\n+        if (msk & (1u << 29u)) {\n+            bool obj_xtd = (obj >> 30) & 1u;\n+            if (obj_xtd != xtd) {\n+                continue;\n+            }\n+        }\n+\n+        /* (frame_id XOR filter_id) AND mask == 0 means match */\n+        if ((id ^ (obj & 0x1FFFFFFFu)) & (msk & 0x1FFFFFFFu)) {\n+            continue;\n+        }\n+\n+        return fltcon_byte & 0x1Fu;   /* destination FIFO */\n+    }\n+    return -1;\n+}\n+\n+/*\n+ * RX deliver — write an incoming frame into the matching RX FIFO\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void canfd_rx_deliver(PIC32MKCANFDState *s,\n+                             uint32_t id, bool xtd, bool fdf,\n+                             uint8_t dlc, const uint8_t *data, int len,\n+                             int filter_hit)\n+{\n+    int dest = filter_hit;\n+    if (dest < 1 || dest > 31) {\n+        return;\n+    }\n+\n+    uint32_t depth = ((s->fifocon[dest] >> CANFD_FIFO_FSIZE_SHIFT) & 0x1Fu) + 1u;\n+    if (s->fifo_count[dest] >= (uint8_t)depth) {\n+        /* Overflow */\n+        s->rxovif |= (1u << dest);\n+        s->cint   |= CANFD_INT_RXOVIF;\n+        canfd_update_irq(s);\n+        return;\n+    }\n+\n+    /* Write message object to tail slot */\n+    uint32_t ua = canfd_fifo_slot_ua(s, dest, s->fifo_tail[dest]);\n+    uint32_t ram_off = ua - s->msg_ram_phys;\n+    uint8_t *obj = s->msg_ram_buf + ram_off;\n+    uint32_t payload_cap = canfd_obj_size(s->fifocon[dest]) - 8u;\n+    uint8_t dlc_len = canfd_dlc_bytes(dlc, fdf);\n+    uint8_t copy_len = (uint8_t)MIN((uint32_t)MAX(len, 0), payload_cap);\n+    if (copy_len > dlc_len) {\n+        copy_len = dlc_len;\n+    }\n+\n+    /*\n+     * RX message object R0/R1 layout (DS60001507 Table 38-2):\n+     *   R0[10:0]  : SID[10:0]  (or upper 11 of 29-bit EID)\n+     *   R0[28:11] : EID[17:0]  (lower 18 bits of 29-bit extended ID)\n+     *   R0[30]    : EXIDE      (1 = extended frame) — matches CiFLTOBJ layout\n+     *   R1[4]     : IDE        (1 = extended frame) — this is what plib_canfd checks\n+     * Note: TX T0 has the same SID/EID layout but IDE is in T1[4], not T0[30].\n+     */\n+    uint32_t r0;\n+    if (xtd) {\n+        /* 29-bit extended: SID = ID[28:18], EID = ID[17:0] */\n+        r0 = ((id >> 18) & 0x7FFu)          /* SID → r0 bits [10:0]  */\n+           | ((id & 0x3FFFFu) << 11)         /* EID → r0 bits [28:11] */\n+           | (1u << 30u);                    /* IDE = 1                */\n+    } else {\n+        r0 = id & 0x7FFu;                   /* SID → r0 bits [10:0]  */\n+    }\n+    *(uint32_t *)(obj + 0) = r0;\n+\n+    /* R1: DLC, flags, FILHIT, RXTS=0 */\n+    uint32_t r1 = (uint32_t)dlc\n+                | (xtd ? (1u << 4) : 0u)\n+                | (fdf ? (1u << 7) : 0u)\n+                | ((uint32_t)(filter_hit & 0x1F) << 11);\n+    *(uint32_t *)(obj + 4) = r1;\n+\n+    /*\n+     * RX message data area layout (DS60001507 Figure 3-2):\n+     *   data[0..3] : RXMSGTS — 32-bit receive timestamp (always present when\n+     *                RXTSEN=1 in FIFOCONn; firmware expects this field)\n+     *   data[4..]  : Payload bytes\n+     *\n+     * Harmony3 plib_canfd always reads payload from data[4] onwards, so we\n+     * must write a timestamp (even if 0) before the payload.\n+     */\n+    memset(obj + 8, 0, payload_cap);           /* Clear timestamp + payload */\n+    *(uint32_t *)(obj + 8) = s->tbc;           /* Write timestamp at data[0..3] */\n+    if (copy_len > 0 && data) {\n+        memcpy(obj + 12, data, copy_len);      /* Payload starts at data[4] */\n+    }\n+\n+    /* Advance tail */\n+    s->fifo_tail[dest] = (s->fifo_tail[dest] + 1u) % (uint8_t)depth;\n+    s->fifo_count[dest]++;\n+\n+    /*\n+     * Set interrupt flags — TFNRFNIF signals data present.\n+     * Only assert RXIF if TFNRFNIE (per-FIFO RX interrupt enable) is set;\n+     * otherwise the frame waits silently until firmware arms reception\n+     * via CAN_MessageReceive() which re-enables TFNRFNIE.\n+      */\n+    s->fifosta[dest] |= CANFD_FIFOSTA_TFNRFNIF;\n+    if (s->fifocon[dest] & CANFD_FIFO_TFNRFNIE) {\n+        s->rxif |= (1u << dest);\n+    }\n+\n+    /*\n+     * Update UA: RX has no write slot; keep head slot for firmware read.\n+     */\n+    s->fifoua[dest] = canfd_fifo_slot_ua(s, dest, s->fifo_head[dest]);\n+\n+    /* CiVEC.ICODE — firmware RX ISR reads this to learn which FIFO fired */\n+    s->vec = (uint32_t)dest & 0x7Fu;\n+\n+    canfd_update_irq(s);\n+}\n+\n+/*\n+ * TX processing — triggered when TXREQ is set for a FIFO or TXQ\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void canfd_process_tx(PIC32MKCANFDState *s, int fifo)\n+{\n+    uint32_t ua;\n+    if (fifo == 0) {\n+        /* TXQ: transmit from head (oldest queued frame) */\n+        ua = canfd_txq_slot_ua(s, s->txq_head);\n+    } else {\n+        /* TX FIFO: transmit from head (oldest pending frame) */\n+        ua = canfd_fifo_slot_ua(s, fifo, s->fifo_head[fifo]);\n+    }\n+\n+    uint32_t ram_off = ua - s->msg_ram_phys;\n+    if (ram_off >= PIC32MK_CAN_MSGRAM_SIZE) {\n+        qemu_log_mask(LOG_GUEST_ERROR,\n+                      \"pic32mk-canfd: TX UA 0x%08x out of message RAM\\n\", ua);\n+        return;\n+    }\n+\n+    const uint8_t *obj = s->msg_ram_buf + ram_off;\n+    uint32_t t0  = *(const uint32_t *)(obj + 0);\n+    uint32_t t1  = *(const uint32_t *)(obj + 4);\n+    uint8_t  dlc = t1 & 0xFu;\n+    bool     fdf = (t1 >> 7) & 1u;\n+    bool     xtd = (t1 >> 4) & 1u;   /* IDE in T1[4], not T0[30] per DS60001507 s3.1 */\n+    /* T0 ID layout: SID in bits[10:0], EID in bits[28:11] (same as R0/CiFLTOBJ) */\n+    uint32_t id  = xtd ? (((t0 & 0x7FFu) << 18) | ((t0 >> 11) & 0x3FFFFu))\n+                       : (t0 & 0x7FFu);\n+    int      len = canfd_dlc_bytes(dlc, fdf);\n+    const uint8_t *data = obj + 8;\n+\n+    uint8_t opmod = (s->con >> CANFD_CON_OPMOD_SHIFT) & 0x7u;\n+    if (opmod == CANFD_OPMOD_INT_LOOP) {\n+        /* Internal loopback: deliver frame through acceptance filters */\n+        int dest = canfd_find_fifo(s, id, xtd);\n+        if (dest >= 1) {\n+            canfd_rx_deliver(s, id, xtd, fdf, dlc, data, len, dest);\n+        }\n+    } else {\n+        /* External TX: send frame to the virtual CAN bus */\n+        qemu_can_frame frame = {};\n+        frame.can_id  = xtd ? (id | QEMU_CAN_EFF_FLAG) : id;\n+        frame.can_dlc = (uint8_t)len;\n+        frame.flags   = fdf ? QEMU_CAN_FRMF_TYPE_FD : 0u;\n+        memcpy(frame.data, data, (size_t)len);\n+        if (s->canbus) {\n+            can_bus_client_send(&s->bus_client, &frame, 1);\n+        } else {\n+            qemu_log_mask(LOG_UNIMP,\n+                          \"pic32mk-canfd[%u]: no canbus connected, \"\n+                          \"dropping TX frame id=0x%08x\\n\", s->instance_id, id);\n+        }\n+    }\n+\n+    /* TX complete bookkeeping */\n+    if (fifo == 0) {\n+        /* TXQ: advance head (oldest pending frame consumed) */\n+        uint8_t depth = (uint8_t)canfd_txq_depth(s);\n+        s->txq_head = (s->txq_head + 1u) % depth;\n+        if (s->txq_count > 0) {\n+            s->txq_count--;\n+        }\n+        s->txqcon  &= ~CANFD_FIFO_TXREQ;\n+        s->txqsta  |= CANFD_TXQSTA_TXQNIF;  /* slot available */\n+        s->txif    |= 1u;          /* bit 0 = TXQ */\n+        s->vec      = 0u;          /* ICODE=0 for TXQ */\n+    } else {\n+        uint32_t depth = ((s->fifocon[fifo] >> CANFD_FIFO_FSIZE_SHIFT) & 0x1Fu) + 1u;\n+        s->fifo_head[fifo] = (s->fifo_head[fifo] + 1u) % (uint8_t)depth;\n+        if (s->fifo_count[fifo] > 0) {\n+            s->fifo_count[fifo]--;\n+        }\n+        s->fifocon[fifo] &= ~CANFD_FIFO_TXREQ;\n+        s->fifosta[fifo] |= CANFD_FIFOSTA_TXATIF;\n+        /* TX slot freed — TX FIFO is no longer full */\n+        s->fifosta[fifo] |= CANFD_FIFOSTA_TFNRFNIF;\n+        s->txif |= (1u << fifo);\n+        s->vec   = (uint32_t)fifo & 0x7Fu;  /* ICODE = FIFO number */\n+    }\n+\n+    canfd_update_irq(s);\n+}\n+\n+/* Forward declaration — defined below canfd_receive() */\n+static void canfd_bus_buf_drain(PIC32MKCANFDState *s);\n+\n+/*\n+ * UINC — advance head (RX) or tail (TX) pointer, clear UINC bit\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void canfd_uinc_fifo(PIC32MKCANFDState *s, int n)\n+{\n+    bool is_tx = (s->fifocon[n] & CANFD_FIFO_TXEN) != 0u;\n+    uint32_t depth = ((s->fifocon[n] >> CANFD_FIFO_FSIZE_SHIFT) & 0x1Fu) + 1u;\n+\n+    if (is_tx) {\n+        /* Firmware finished writing a TX object — advance tail */\n+        s->fifo_tail[n] = (s->fifo_tail[n] + 1u) % (uint8_t)depth;\n+        s->fifo_count[n]++;\n+        s->fifoua[n] = canfd_fifo_slot_ua(s, n, s->fifo_tail[n]);\n+        /* TX FIFO full — clear \"not full\" flag so firmware won't overwrite */\n+        if (s->fifo_count[n] >= (uint8_t)depth) {\n+            s->fifosta[n] &= ~CANFD_FIFOSTA_TFNRFNIF;\n+        }\n+    } else {\n+        /* Firmware finished reading an RX object — advance head */\n+        if (s->fifo_count[n] > 0) {\n+            s->fifo_count[n]--;\n+        }\n+        s->fifo_head[n] = (s->fifo_head[n] + 1u) % (uint8_t)depth;\n+        s->fifoua[n] = canfd_fifo_slot_ua(s, n, s->fifo_head[n]);\n+\n+        if (s->fifo_count[n] == 0) {\n+            s->rxif     &= ~(1u << n);\n+            s->fifosta[n] &= ~CANFD_FIFOSTA_TFNRFNIF;\n+        }\n+        canfd_update_irq(s);\n+        /*\n+         * Drain bus_buf after UINC: firmware just freed a FIFO slot, so the\n+         * next buffered frame can be delivered immediately.  In polling mode\n+         * the firmware checks FIFOSTA.TFNRFNIF directly after UINC; delivering\n+         * here ensures that flag is set before the next poll.  In interrupt\n+         * mode with TFNRFNIE enabled, canfd_rx_deliver will re-raise the IRQ\n+         * for the newly delivered frame.\n+          */\n+        canfd_bus_buf_drain(s);\n+    }\n+}\n+\n+static void canfd_uinc_txq(PIC32MKCANFDState *s)\n+{\n+    uint8_t depth = (uint8_t)canfd_txq_depth(s);\n+    /* Advance tail — firmware finished writing a TX object */\n+    s->txq_tail = (s->txq_tail + 1u) % depth;\n+    s->txq_count++;\n+    /* UA now points at the next empty write slot */\n+    s->txqua = canfd_txq_slot_ua(s, s->txq_tail);\n+}\n+\n+/*\n+ * FRESET — reset a FIFO to empty state\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void canfd_freset_fifo(PIC32MKCANFDState *s, int n)\n+{\n+    s->fifo_head[n]  = 0;\n+    s->fifo_tail[n]  = 0;\n+    s->fifo_count[n] = 0;\n+    s->rxif         &= ~(1u << n);\n+    s->txif         &= ~(1u << n);\n+    s->fifosta[n]    = 0;\n+    /* TX FIFO: empty after reset means \"not full\" → TFNRFNIF = 1 */\n+    if (s->fifocon[n] & CANFD_FIFO_TXEN) {\n+        s->fifosta[n] |= CANFD_FIFOSTA_TFNRFNIF;\n+    }\n+    s->fifoua[n]     = canfd_fifo_slot_ua(s, n, 0);\n+    canfd_update_irq(s);\n+}\n+\n+/*\n+ * Abort all pending TX (CiCON.ABAT)\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void canfd_abort_all_tx(PIC32MKCANFDState *s)\n+{\n+    /* TXQ */\n+    s->txqcon  &= ~CANFD_FIFO_TXREQ;\n+    s->txqsta  |= CANFD_TXQSTA_TXQNIF;\n+\n+    /* TX FIFOs */\n+    for (int i = 1; i < 32; i++) {\n+        if (s->fifocon[i] & CANFD_FIFO_TXEN) {\n+            s->fifocon[i] &= ~CANFD_FIFO_TXREQ;\n+            s->fifosta[i] |= CANFD_FIFOSTA_TXATIF;\n+            s->txif |= (1u << i);\n+        }\n+    }\n+    canfd_update_irq(s);\n+}\n+\n+/*\n+ * SocketCAN virtual bus callbacks\n+ *\n+ * When a can-bus object is linked (via the \"canbus\" QOM property), the\n+ * device behaves as a bus participant:\n+ *   TX (non-loopback): canfd_process_tx() calls can_bus_client_send()\n+ *   RX (from bus):     canfd_receive() delivers frames through the\n+ *                      acceptance filter into an RX FIFO\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static bool canfd_can_receive(CanBusClientState *client)\n+{\n+    PIC32MKCANFDState *s = container_of(client, PIC32MKCANFDState, bus_client);\n+    uint8_t opmod = (s->con >> CANFD_CON_OPMOD_SHIFT) & 0x7u;\n+    /*\n+     * Accept incoming frames in Normal, External Loopback, Listen-only,\n+     * and Restricted modes — not in Config or Internal Loopback mode.\n+      */\n+    return opmod == CANFD_OPMOD_NORMAL\n+        || opmod == CANFD_OPMOD_EXT_LOOP\n+        || opmod == CANFD_OPMOD_LISTEN\n+        || opmod == CANFD_OPMOD_RESTRICTED;\n+}\n+\n+/*\n+ * Drain the software bus buffer into hardware RX FIFOs.\n+ * Called after each RX UINC — the firmware just freed a FIFO slot so we can\n+ * deliver at most one buffered frame per UINC (matching real FIFO depth-1\n+ * behaviour).  Stops when the head frame's target FIFO is still full.\n+ */\n+static void canfd_bus_buf_drain(PIC32MKCANFDState *s)\n+{\n+    while (s->bus_buf_count > 0) {\n+        int n = s->bus_buf_head;\n+        int dest = s->bus_buf_dest[n];\n+        uint32_t depth = ((s->fifocon[dest] >> CANFD_FIFO_FSIZE_SHIFT) & 0x1Fu) + 1u;\n+        if (s->fifo_count[dest] >= (uint8_t)depth) {\n+            break;  /* target FIFO still full — wait for next UINC */\n+        }\n+        canfd_rx_deliver(s,\n+                         s->bus_buf_id[n], s->bus_buf_xtd[n],\n+                         s->bus_buf_fdf[n], s->bus_buf_dlc[n],\n+                         s->bus_buf_data[n], s->bus_buf_len[n], dest);\n+        s->bus_buf_head = (s->bus_buf_head + 1) % 64;\n+        s->bus_buf_count--;\n+    }\n+}\n+\n+static ssize_t canfd_receive(CanBusClientState *client,\n+                             const qemu_can_frame *frames, size_t frames_cnt)\n+{\n+    PIC32MKCANFDState *s = container_of(client, PIC32MKCANFDState, bus_client);\n+\n+    for (size_t i = 0; i < frames_cnt; i++) {\n+        const qemu_can_frame *f = &frames[i];\n+        bool     xtd = (f->can_id & QEMU_CAN_EFF_FLAG) != 0;\n+        uint32_t id  = xtd ? (f->can_id & QEMU_CAN_EFF_MASK)\n+                           : (f->can_id & QEMU_CAN_SFF_MASK);\n+        bool     fdf = (f->flags & QEMU_CAN_FRMF_TYPE_FD) != 0;\n+        uint8_t  dlc = can_len2dlc(f->can_dlc);\n+        uint8_t  rx_len = MIN((uint8_t)f->can_dlc, canfd_dlc_bytes(dlc, fdf));\n+        int      dest = canfd_find_fifo(s, id, xtd);\n+\n+        /* Log received frame with full payload hex dump */\n+\n+        if (dest < 1) {\n+            continue;  /* no filter match */\n+        }\n+\n+        uint32_t depth = ((s->fifocon[dest] >> CANFD_FIFO_FSIZE_SHIFT) & 0x1Fu) + 1u;\n+        if (s->fifo_count[dest] < (uint8_t)depth) {\n+            /* FIFO has space — deliver directly */\n+            canfd_rx_deliver(s, id, xtd, fdf, dlc, f->data, rx_len, dest);\n+        } else if (s->bus_buf_count < 64) {\n+            /* FIFO full — buffer for later delivery after UINC */\n+            int slot = s->bus_buf_tail;\n+            s->bus_buf_id[slot]  = id;\n+            s->bus_buf_xtd[slot] = xtd;\n+            s->bus_buf_fdf[slot] = fdf;\n+            s->bus_buf_dlc[slot] = dlc;\n+            s->bus_buf_len[slot] = rx_len;\n+            s->bus_buf_dest[slot] = dest;\n+            memcpy(s->bus_buf_data[slot], f->data, rx_len);\n+            s->bus_buf_tail = (s->bus_buf_tail + 1) % 64;\n+            s->bus_buf_count++;\n+        } else {\n+            /* Software buffer also full — real bus overflow */\n+            s->rxovif |= (1u << dest);\n+            s->cint   |= CANFD_INT_RXOVIF;\n+            canfd_update_irq(s);\n+        }\n+    }\n+    return (ssize_t)frames_cnt;\n+}\n+\n+static CanBusClientInfo canfd_bus_client_info = {\n+    .can_receive = canfd_can_receive,\n+    .receive     = canfd_receive,\n+};\n+\n+/*\n+ * SFR MMIO read\n+ *\n+ * Each logical register occupies 0x10 bytes: +0=base, +4=SET, +8=CLR, +C=INV.\n+ * Reads always return the base register value regardless of sub-offset.\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static uint64_t canfd_sfr_read(void *opaque, hwaddr offset, unsigned size)\n+{\n+    PIC32MKCANFDState *s = opaque;\n+    /* Strip SET/CLR/INV sub-offset: base is at (offset & ~0xCu) */\n+    hwaddr base_off = offset & ~0xCu;\n+\n+    switch (base_off) {\n+    case CANFD_CiCON:\n+        return s->con;\n+    case CANFD_CiNBTCFG:\n+        return s->nbtcfg;\n+    case CANFD_CiDBTCFG:\n+        return s->dbtcfg;\n+    case CANFD_CiTDC:\n+        return s->tdc;\n+    case CANFD_CiTBC:\n+        return s->tbc;\n+    case CANFD_CiTSCON:\n+        return s->tscon;\n+    case CANFD_CiVEC:\n+        return s->vec;\n+    case CANFD_CiINT:\n+        return s->cint;\n+    case CANFD_CiRXIF:\n+        return s->rxif;\n+    case CANFD_CiTXIF:\n+        return s->txif;\n+    case CANFD_CiRXOVIF:\n+        return s->rxovif;\n+    case CANFD_CiTXATIF:\n+        return 0;\n+    case CANFD_CiTXREQ:\n+        return s->txreq;\n+    case CANFD_CiTREC:\n+        return s->trec;\n+    case CANFD_CiTEFCON:\n+        return s->tefcon;\n+    case CANFD_CiTEFSTA:\n+        return s->tefsta;\n+    case CANFD_CiTEFUA:\n+        return s->tefua;\n+    case CANFD_CiFIFOBA:\n+        return 0;\n+    case CANFD_CiTXQCON:\n+        return s->txqcon;\n+    case CANFD_CiTXQSTA:\n+        return s->txqsta;\n+    case CANFD_CiTXQUA:\n+        return s->txqua;\n+    default:\n+        break;\n+    }\n+\n+    /*\n+     * FIFO registers: base 0x170, stride 0x30 per FIFO (n=1..31)\n+     *   +0x00: CiFIFOCONn, +0x10: CiFIFOSTAn, +0x20: CiFIFOUAn\n+      */\n+    if (base_off >= CANFD_CiFIFOCON(1) && base_off <= CANFD_CiFIFOCON(31) + 0x20u) {\n+        int idx = (int)((base_off - 0x170u) / 0x30u) + 1;\n+        int sub = (int)((base_off - 0x170u) % 0x30u);\n+        if (idx >= 1 && idx <= 31) {\n+            if (sub == 0x00) {\n+                return s->fifocon[idx];\n+            }\n+            if (sub == 0x10) {\n+                return s->fifosta[idx];\n+            }\n+            if (sub == 0x20) {\n+                return s->fifoua[idx];\n+            }\n+        }\n+    }\n+\n+    /* Filter control: stride 0x10 per register (r=0..7) */\n+    if (base_off >= CANFD_CiFLTCON(0) && base_off <= CANFD_CiFLTCON(7)) {\n+        return s->fltcon[(base_off - 0x740u) / 0x10u];\n+    }\n+\n+    /* Filter obj/mask: stride 0x20 per pair (n=0..31) */\n+    if (base_off >= CANFD_CiFLTOBJ(0) && base_off <= CANFD_CiMASK(31)) {\n+        if (base_off >= 0x7C0u && base_off < 0x7C0u + 32u * 0x20u) {\n+            int fn = (int)((base_off - 0x7C0u) / 0x20u);\n+            return s->fltobj[fn];\n+        }\n+        if (base_off >= 0x7D0u && base_off < 0x7D0u + 32u * 0x20u) {\n+            int fn = (int)((base_off - 0x7D0u) / 0x20u);\n+            return s->mask[fn];\n+        }\n+    }\n+\n+    qemu_log_mask(LOG_UNIMP,\n+                  \"pic32mk-canfd: unimplemented SFR read @ +0x%03\" HWADDR_PRIx \"\\n\",\n+                  offset);\n+    return 0;\n+}\n+\n+/*\n+ * Apply a CLR/SET/INV write to a register value.\n+ * PIC32MK convention: sub=0 → plain write, +4 → CLR, +8 → SET, +0xC → INV\n+ * -----------------------------------------------------------------------\n+ */\n+static uint32_t apply_sci(uint32_t cur, uint32_t val, int sub)\n+{\n+    switch (sub) {\n+    case 0:\n+        return val;\n+    case 4:\n+        return cur & ~val;\n+    case 8:\n+        return cur | val;\n+    case 0xC:\n+        return cur ^ val;\n+    default:\n+        return cur;\n+    }\n+}\n+\n+/*\n+ * SFR MMIO write\n+ *\n+ * Each logical register occupies 0x10 bytes: +0=base, +4=SET, +8=CLR, +C=INV.\n+ * UINC, TXREQ, FRESET are pulse bits — handled then stripped from storage.\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void canfd_sfr_write(void *opaque, hwaddr offset, uint64_t val,\n+                            unsigned size)\n+{\n+    PIC32MKCANFDState *s = opaque;\n+    uint32_t v32     = (uint32_t)val;\n+    hwaddr   base_off = offset & ~0xCu;\n+    int      sub     = (int)(offset & 0xCu);\n+    uint8_t  opmod   = (s->con >> CANFD_CON_OPMOD_SHIFT) & 0x7u;\n+\n+    switch (base_off) {\n+\n+    /* ---- CiCON ---- */\n+    case CANFD_CiCON: {\n+        uint8_t old_opmod = opmod;\n+        uint32_t new_con  = apply_sci(s->con, v32, sub);\n+\n+        uint8_t reqop = (new_con >> CANFD_CON_REQOP_SHIFT) & 0x7u;\n+        /* Reflect REQOP into OPMOD immediately; BUSY always 0 */\n+        new_con = (new_con & ~CANFD_CON_OPMOD_MASK)\n+                | ((uint32_t)reqop << CANFD_CON_OPMOD_SHIFT);\n+        s->con = new_con;\n+\n+        if (reqop != old_opmod) {\n+            s->cint |= CANFD_INT_MODIF;\n+            canfd_update_irq(s);\n+        }\n+\n+        /* Handle ABAT */\n+        if (new_con & CANFD_CON_ABAT) {\n+            canfd_abort_all_tx(s);\n+            s->con &= ~CANFD_CON_ABAT;\n+        }\n+\n+        /* Initialise TXQ UA when TXQEN becomes set */\n+        if ((new_con & CANFD_CON_TXQEN) && !(s->txqua)) {\n+            s->txqua = canfd_txq_slot_ua(s, s->txq_tail);\n+        }\n+        return;\n+    }\n+\n+    /* ---- Bit-time / TDC — only writable in Config mode ---- */\n+    case CANFD_CiNBTCFG:\n+        if (opmod == CANFD_OPMOD_CONFIG) {\n+            s->nbtcfg = apply_sci(s->nbtcfg, v32, sub);\n+        }\n+        return;\n+    case CANFD_CiDBTCFG:\n+        if (opmod == CANFD_OPMOD_CONFIG) {\n+            s->dbtcfg = apply_sci(s->dbtcfg, v32, sub);\n+        }\n+        return;\n+    case CANFD_CiTDC:\n+        if (opmod == CANFD_OPMOD_CONFIG) {\n+            s->tdc = apply_sci(s->tdc, v32, sub);\n+        }\n+        return;\n+    case CANFD_CiTSCON:\n+        s->tscon = apply_sci(s->tscon, v32, sub);\n+        return;\n+\n+    /* CiINT: enable bits [31:16] writable; status [15:0] cleared by fw */\n+    case CANFD_CiINT:\n+        s->cint = apply_sci(s->cint, v32, sub);\n+        canfd_update_irq(s);\n+        return;\n+\n+    /* ---- CiRXIF / CiTXIF / CiRXOVIF — firmware clears by writing 0 ---- */\n+    case CANFD_CiRXIF:\n+        s->rxif = apply_sci(s->rxif, v32, sub);\n+        canfd_update_irq(s);\n+        return;\n+    case CANFD_CiTXIF:\n+        s->txif = apply_sci(s->txif, v32, sub);\n+        canfd_update_irq(s);\n+        return;\n+    case CANFD_CiRXOVIF:\n+        s->rxovif = apply_sci(s->rxovif, v32, sub);\n+        return;\n+\n+    /* ---- CiTXREQ — writing 1 to a bit requests TX for that FIFO ---- */\n+    case CANFD_CiTXREQ: {\n+        uint32_t new_req = apply_sci(s->txreq, v32, sub);\n+        uint32_t newly   = new_req & ~s->txreq;\n+        s->txreq = new_req;\n+        for (int i = 1; i < 32; i++) {\n+            if (newly & (1u << i)) {\n+                canfd_process_tx(s, i);\n+            }\n+        }\n+        return;\n+    }\n+\n+    /* ---- CiTXQCON ---- */\n+    case CANFD_CiTXQCON: {\n+        uint32_t new_con = apply_sci(s->txqcon, v32, sub);\n+        bool uinc = (new_con & CANFD_FIFO_UINC)  != 0u;\n+        bool req  = (new_con & CANFD_FIFO_TXREQ) != 0u;\n+        s->txqcon = new_con & ~(CANFD_FIFO_UINC | CANFD_FIFO_TXREQ\n+                                | CANFD_FIFO_FRESET);\n+        /*\n+         * Clearing TXQEIE (bit 4) acknowledges TX completion — deassert TXQ\n+         * source in txif so TXIF is not re-asserted on the next irq update.\n+          */\n+        if (!(s->txqcon & (1u << 4u))) {\n+            s->txif &= ~1u;\n+            canfd_update_irq(s);\n+        }\n+        if (uinc) {\n+            canfd_uinc_txq(s);\n+        }\n+        if (req) {\n+            canfd_process_tx(s, 0);\n+        }\n+        return;\n+    }\n+\n+    /* ---- CiTXQSTA / CiTXQUA — hardware-managed, ignore firmware writes ---- */\n+    case CANFD_CiTXQSTA:\n+    case CANFD_CiTXQUA:\n+        return;\n+\n+    /* ---- TEF (stub) ---- */\n+    case CANFD_CiTEFCON:\n+        s->tefcon = apply_sci(s->tefcon, v32, sub);\n+        return;\n+    case CANFD_CiTEFSTA:\n+        s->tefsta = apply_sci(s->tefsta, v32, sub);\n+        return;\n+    case CANFD_CiTEFUA:\n+        return;\n+\n+    /* ---- CiFIFOBA — Phase 3C stub: log and ignore ---- */\n+    case CANFD_CiFIFOBA:\n+        if (sub == 0) {\n+            qemu_log_mask(LOG_UNIMP,\n+                \"pic32mk-canfd: CiFIFOBA write 0x%08x (Phase 3C stub, \"\n+                \"using fixed msg RAM base 0x%08x)\\n\",\n+                v32, s->msg_ram_phys);\n+        }\n+        return;\n+\n+    default:\n+        break;\n+    }\n+\n+    /*\n+     * ---- FIFO registers: base 0x170, stride 0x30 per FIFO (n=1..31)\n+     *   +0x00: CiFIFOCONn, +0x10: CiFIFOSTAn, +0x20: CiFIFOUAn ----\n+      */\n+    if (base_off >= CANFD_CiFIFOCON(1) && base_off <= CANFD_CiFIFOCON(31) + 0x20u) {\n+        int idx      = (int)((base_off - 0x170u) / 0x30u) + 1;\n+        int field    = (int)((base_off - 0x170u) % 0x30u);\n+        if (idx < 1 || idx > 31) {\n+            return;\n+        }\n+\n+        if (field == 0x00) {\n+            /* CiFIFOCONn */\n+            uint32_t new_con = apply_sci(s->fifocon[idx], v32, sub);\n+\n+            if (new_con & CANFD_FIFO_FRESET) {\n+                s->fifocon[idx] = new_con & ~CANFD_FIFO_FRESET;\n+                canfd_freset_fifo(s, idx);\n+                return;\n+            }\n+\n+            bool uinc  = (new_con & CANFD_FIFO_UINC)  != 0u;\n+            bool txreq = (new_con & CANFD_FIFO_TXREQ)  != 0u;\n+            s->fifocon[idx] = new_con & ~(CANFD_FIFO_UINC | CANFD_FIFO_TXREQ);\n+\n+            /*\n+             * First-time TX FIFO configuration (Harmony CAN1..4 init pattern):\n+             * firmware writes FIFOCONn with TXEN set; at this point the FIFO\n+             * is empty so it is \"not full\" and FIFOUA must point to slot 0.\n+             * Mirror the TXQ treatment: set TFNRFNIF=1 and initialise UA.\n+              */\n+            if ((s->fifocon[idx] & CANFD_FIFO_TXEN) && s->fifoua[idx] == 0) {\n+                s->fifosta[idx] |= CANFD_FIFOSTA_TFNRFNIF;\n+                s->fifoua[idx] = canfd_fifo_slot_ua(s, idx, 0);\n+            }\n+\n+            /*\n+             * Clearing TFERFFIE (bit 4) acknowledges TX completion for this\n+             * FIFO — deassert its txif source bit.\n+              */\n+            if (!(s->fifocon[idx] & (1u << 4u))) {\n+                s->txif &= ~(1u << idx);\n+                canfd_update_irq(s);\n+            }\n+            if (uinc) {\n+                canfd_uinc_fifo(s, idx);\n+            }\n+            if (txreq && (s->fifocon[idx] & CANFD_FIFO_TXEN)) {\n+                canfd_process_tx(s, idx);\n+            }\n+\n+            /*\n+             * For RX FIFOs, honour TFNRFNIE: if the firmware just toggled it,\n+             * update rxif to match the real hardware behaviour.\n+             * Enabling  → IRQ rises  (frame already in FIFO fires interrupt)\n+             * Disabling → IRQ falls  (flow-control)\n+              */\n+            if (!(s->fifocon[idx] & CANFD_FIFO_TXEN)) {\n+                bool new_ie   = (s->fifocon[idx] & CANFD_FIFO_TFNRFNIE) != 0;\n+                bool has_data = (s->fifosta[idx] & CANFD_FIFOSTA_TFNRFNIF) != 0;\n+                if (has_data && new_ie) {\n+                    s->rxif |= (1u << idx);\n+                } else {\n+                    s->rxif &= ~(1u << idx);\n+                }\n+                canfd_update_irq(s);\n+            }\n+\n+        } else if (field == 0x10) {\n+            /* CiFIFOSTAn — firmware can clear status flags */\n+            s->fifosta[idx] = apply_sci(s->fifosta[idx], v32, sub);\n+            if (!(s->fifosta[idx] & CANFD_FIFOSTA_TFNRFNIF)) {\n+                if (s->fifo_count[idx] == 0) {\n+                    s->rxif &= ~(1u << idx);\n+                }\n+            }\n+            canfd_update_irq(s);\n+        }\n+        /* CiFIFOUA (+0x20) is hardware-owned, ignore writes */\n+        return;\n+    }\n+\n+    /* ---- Filter control: stride 0x10 per register (r=0..7) ---- */\n+    if (base_off >= CANFD_CiFLTCON(0) && base_off <= CANFD_CiFLTCON(7)) {\n+        int reg = (int)((base_off - 0x740u) / 0x10u);\n+        s->fltcon[reg] = apply_sci(s->fltcon[reg], v32, sub);\n+        return;\n+    }\n+\n+    /* ---- Filter object / mask — only writable in Config mode ---- */\n+    if (base_off >= CANFD_CiFLTOBJ(0) && base_off < 0x7D0u + 32u * 0x20u) {\n+        if (opmod != CANFD_OPMOD_CONFIG) {\n+            return;\n+        }\n+        if (base_off >= 0x7C0u && base_off < 0x7C0u + 32u * 0x20u) {\n+            int fn = (int)((base_off - 0x7C0u) / 0x20u);\n+            s->fltobj[fn] = apply_sci(s->fltobj[fn], v32, sub);\n+        } else if (base_off >= 0x7D0u) {\n+            int fn = (int)((base_off - 0x7D0u) / 0x20u);\n+            if (fn < 32) {\n+                s->mask[fn] = apply_sci(s->mask[fn], v32, sub);\n+            }\n+        }\n+        return;\n+    }\n+\n+    qemu_log_mask(LOG_UNIMP,\n+                  \"pic32mk-canfd: unimplemented SFR write @ +0x%03\" HWADDR_PRIx\n+                  \" = 0x%08x\\n\", offset, v32);\n+}\n+\n+static const MemoryRegionOps canfd_sfr_ops = {\n+    .read       = canfd_sfr_read,\n+    .write      = canfd_sfr_write,\n+    .endianness = DEVICE_LITTLE_ENDIAN,\n+    .valid = {\n+        .min_access_size = 1,\n+        .max_access_size = 4,\n+    },\n+};\n+\n+/*\n+ * Device lifecycle\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void pic32mk_canfd_reset(DeviceState *dev)\n+{\n+    PIC32MKCANFDState *s = PIC32MK_CANFD(dev);\n+\n+    s->con     = CANFD_CON_RESET;    /* OPMOD=100 (Config), REQOP=100 */\n+    s->nbtcfg  = 0;\n+    s->dbtcfg  = 0;\n+    s->tdc     = 0;\n+    s->tbc     = 0;\n+    s->tscon   = 0;\n+    s->vec     = 0;\n+    s->cint    = 0;\n+    s->rxif    = 0;\n+    s->txif    = 0;\n+    s->rxovif  = 0;\n+    s->txreq   = 0;\n+    s->trec    = 0;\n+    s->tefcon  = 0;\n+    s->tefsta  = 0;\n+    s->tefua   = 0;\n+    s->txqcon  = 0;\n+    s->txqsta   = CANFD_TXQSTA_TXQNIF;  /* TXQNIF=1 (bit 0) — slot available on reset */\n+    s->txq_head  = 0;\n+    s->txq_tail  = 0;\n+    s->txq_count = 0;\n+    s->txqua    = 0;          /* will be computed when TXQEN is set */\n+\n+    for (int i = 0; i < 32; i++) {\n+        s->fifocon[i]   = 0;\n+        s->fifosta[i]   = 0;\n+        s->fifoua[i]    = 0;\n+        s->fifo_head[i] = 0;\n+        s->fifo_tail[i] = 0;\n+        s->fifo_count[i] = 0;\n+        s->fltobj[i]    = 0;\n+        s->mask[i]      = 0;\n+    }\n+    for (int i = 0; i < 8; i++) {\n+        s->fltcon[i] = 0;\n+    }\n+\n+    /* Clear message RAM */\n+    if (s->msg_ram_buf) {\n+        memset(s->msg_ram_buf, 0, PIC32MK_CAN_MSGRAM_SIZE);\n+    }\n+\n+    /* Reset software bus buffer */\n+    s->bus_buf_head  = 0;\n+    s->bus_buf_tail  = 0;\n+    s->bus_buf_count = 0;\n+\n+    qemu_irq_lower(s->irq);\n+}\n+\n+static void pic32mk_canfd_instance_init(Object *obj)\n+{\n+    PIC32MKCANFDState *s = PIC32MK_CANFD(obj);\n+    object_property_add_link(obj, \"canbus\", TYPE_CAN_BUS,\n+                             (Object **)&s->canbus,\n+                             qdev_prop_allow_set_link_before_realize,\n+                             0);\n+}\n+\n+static void pic32mk_canfd_realize(DeviceState *dev, Error **errp)\n+{\n+    PIC32MKCANFDState *s = PIC32MK_CANFD(dev);\n+\n+    /* SFR region (index 0) */\n+    memory_region_init_io(&s->sfr_mmio, OBJECT(s), &canfd_sfr_ops, s,\n+                          \"pic32mk-canfd-sfr\", PIC32MK_CAN_SFR_SIZE);\n+    sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->sfr_mmio);\n+\n+    /* Message RAM backing store */\n+    s->msg_ram_buf = g_malloc0(PIC32MK_CAN_MSGRAM_SIZE);\n+\n+    /* Message RAM region (index 1) — RAM-ptr so CPU can read/write freely */\n+    memory_region_init_ram_ptr(&s->msg_ram, OBJECT(s),\n+                               \"pic32mk-canfd-msgram\",\n+                               PIC32MK_CAN_MSGRAM_SIZE,\n+                               s->msg_ram_buf);\n+    sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->msg_ram);\n+\n+    sysbus_init_irq(SYS_BUS_DEVICE(dev), &s->irq);\n+\n+    /* Connect to virtual CAN bus if one was linked */\n+    s->bus_client.info    = &canfd_bus_client_info;\n+    s->bus_client.fd_mode = true;\n+    if (s->canbus) {\n+        if (can_bus_insert_client(s->canbus, &s->bus_client) < 0) {\n+            error_setg(errp, \"pic32mk-canfd: can_bus_insert_client failed\");\n+            return;\n+        }\n+    }\n+}\n+\n+static void pic32mk_canfd_unrealize(DeviceState *dev)\n+{\n+    PIC32MKCANFDState *s = PIC32MK_CANFD(dev);\n+    can_bus_remove_client(&s->bus_client);\n+    g_free(s->msg_ram_buf);\n+    s->msg_ram_buf = NULL;\n+}\n+\n+/*\n+ * Properties\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static const Property pic32mk_canfd_props[] = {\n+    DEFINE_PROP_UINT32(\"msg-ram-base\", PIC32MKCANFDState, msg_ram_phys, 0),\n+    DEFINE_PROP_UINT32(\"instance-id\", PIC32MKCANFDState, instance_id, 0),\n+};\n+\n+/*\n+ * Class / type registration\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void pic32mk_canfd_class_init(ObjectClass *klass, const void *data)\n+{\n+    DeviceClass *dc = DEVICE_CLASS(klass);\n+\n+    dc->realize   = pic32mk_canfd_realize;\n+    dc->unrealize = pic32mk_canfd_unrealize;\n+    device_class_set_legacy_reset(dc, pic32mk_canfd_reset);\n+    device_class_set_props(dc, pic32mk_canfd_props);\n+    set_bit(DEVICE_CATEGORY_MISC, dc->categories);\n+    dc->desc = \"Microchip PIC32MK CAN FD controller\";\n+}\n+\n+static const TypeInfo pic32mk_canfd_info = {\n+    .name          = TYPE_PIC32MK_CANFD,\n+    .parent        = TYPE_SYS_BUS_DEVICE,\n+    .instance_size = sizeof(PIC32MKCANFDState),\n+    .instance_init = pic32mk_canfd_instance_init,\n+    .class_init    = pic32mk_canfd_class_init,\n+};\n+\n+static void pic32mk_canfd_register_types(void)\n+{\n+    type_register_static(&pic32mk_canfd_info);\n+}\n+\n+type_init(pic32mk_canfd_register_types)\ndiff --git a/hw/mips/pic32mk_cfg.c b/hw/mips/pic32mk_cfg.c\nnew file mode 100644\nindex 0000000000..736f0e84fd\n--- /dev/null\n+++ b/hw/mips/pic32mk_cfg.c\n@@ -0,0 +1,247 @@\n+/*\n+ * Microchip PIC32MK — Configuration / PMD / SYSKEY registers\n+ * Datasheet: DS60001519E §6\n+ *\n+ * Models the CFG register block at 0xBF800000–0xBF80011F:\n+ *   CFGCON, SYSKEY, PMD1–PMD7, CFGCON2.\n+ *\n+ * SYSKEY implements the unlock state machine:\n+ *   Write 0x00000000 → LOCKED\n+ *   Write 0xAA996655 → FIRST_KEY\n+ *   Write 0x556699AA when FIRST_KEY → UNLOCKED\n+ *   Any wrong write → LOCKED\n+ *   SYSKEY always reads 0.\n+ *\n+ * PMD1–PMD7 writes are gated by CFGCON.PMDLOCK (bit 29).\n+ * When PMDLOCK=1, writes to PMDx are silently ignored.\n+ *\n+ * Copyright (c) 2026 QEMU contributors\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+#include \"qemu/log.h\"\n+#include \"qemu/module.h\"\n+#include \"hw/core/sysbus.h\"\n+#include \"hw/mips/pic32mk.h\"\n+\n+#define TYPE_PIC32MK_CFG \"pic32mk-cfg\"\n+OBJECT_DECLARE_SIMPLE_TYPE(PIC32MKCFGState, PIC32MK_CFG)\n+\n+/* SYSKEY unlock state machine */\n+enum {\n+    SYSKEY_LOCKED = 0,\n+    SYSKEY_FIRST_KEY,\n+    SYSKEY_UNLOCKED,\n+};\n+\n+typedef struct PIC32MKCFGState {\n+    SysBusDevice parent_obj;\n+    MemoryRegion mmio;\n+\n+    uint32_t cfgcon;\n+    uint32_t cfgcon2;\n+    uint32_t checon;                    /* Prefetch Cache Control */\n+    uint32_t pmd[PIC32MK_PMD_COUNT];    /* PMD1–PMD7 */\n+    uint8_t  syskey_state;              /* SYSKEY unlock FSM */\n+} PIC32MKCFGState;\n+\n+/* SET/CLR/INV helper */\n+static uint32_t apply_sci(uint32_t old, uint32_t val, unsigned sub)\n+{\n+    switch (sub) {\n+    case 0x0:\n+        return val;\n+    case 0x4:\n+        return old | val;\n+    case 0x8:\n+        return old & ~val;\n+    case 0xC:\n+        return old ^ val;\n+    default:\n+        return old;\n+    }\n+}\n+\n+/*\n+ * Read handler\n+ * -----------------------------------------------------------------------\n+ */\n+static uint64_t pic32mk_cfg_read(void *opaque, hwaddr addr, unsigned size)\n+{\n+    PIC32MKCFGState *s = PIC32MK_CFG(opaque);\n+    hwaddr base = addr & ~0xFu;\n+\n+    switch (base) {\n+    case PIC32MK_CFGCON:\n+        return s->cfgcon;\n+\n+    case PIC32MK_SYSKEY:\n+        /* SYSKEY always reads 0 */\n+        return 0;\n+\n+    case PIC32MK_PMD1:\n+        return s->pmd[0];\n+    case PIC32MK_PMD2:\n+        return s->pmd[1];\n+    case PIC32MK_PMD3:\n+        return s->pmd[2];\n+    case PIC32MK_PMD4:\n+        return s->pmd[3];\n+    case PIC32MK_PMD5:\n+        return s->pmd[4];\n+    case PIC32MK_PMD6:\n+        return s->pmd[5];\n+    case PIC32MK_PMD7:\n+        return s->pmd[6];\n+\n+    case PIC32MK_CFGCON2:\n+        return s->cfgcon2;\n+\n+    case PIC32MK_CHECON:\n+        return s->checon;\n+\n+    default:\n+        qemu_log_mask(LOG_UNIMP,\n+                      \"pic32mk-cfg: unimplemented read @ 0x%03\" HWADDR_PRIx \"\\n\",\n+                      addr);\n+        return 0;\n+    }\n+}\n+\n+/*\n+ * Write handler\n+ * -----------------------------------------------------------------------\n+ */\n+static void pic32mk_cfg_write(void *opaque, hwaddr addr, uint64_t val,\n+                               unsigned size)\n+{\n+    PIC32MKCFGState *s = PIC32MK_CFG(opaque);\n+    hwaddr base = addr & ~0xFu;\n+    unsigned sub = addr & 0xCu;\n+    uint32_t v32 = (uint32_t)val;\n+\n+    /* Handle SYSKEY — no SET/CLR/INV, always direct write */\n+    if (base == PIC32MK_SYSKEY) {\n+        switch (s->syskey_state) {\n+        case SYSKEY_LOCKED:\n+            if (v32 == 0xAA996655u) {\n+                s->syskey_state = SYSKEY_FIRST_KEY;\n+            }\n+            break;\n+        case SYSKEY_FIRST_KEY:\n+            if (v32 == 0x556699AAu) {\n+                s->syskey_state = SYSKEY_UNLOCKED;\n+            } else {\n+                s->syskey_state = SYSKEY_LOCKED;\n+            }\n+            break;\n+        case SYSKEY_UNLOCKED:\n+            /* Any write re-locks (clearing 0x00000000 or random) */\n+            if (v32 != 0xAA996655u && v32 != 0x556699AAu) {\n+                s->syskey_state = SYSKEY_LOCKED;\n+            }\n+            break;\n+        }\n+        return;\n+    }\n+\n+    switch (base) {\n+    case PIC32MK_CFGCON:\n+        s->cfgcon = apply_sci(s->cfgcon, v32, sub);\n+        break;\n+\n+    case PIC32MK_CFGCON2:\n+        s->cfgcon2 = apply_sci(s->cfgcon2, v32, sub);\n+        break;\n+\n+    case PIC32MK_CHECON:\n+        s->checon = apply_sci(s->checon, v32, sub);\n+        break;\n+\n+    /* PMD1–PMD7: gated by PMDLOCK */\n+    case PIC32MK_PMD1:\n+    case PIC32MK_PMD2:\n+    case PIC32MK_PMD3:\n+    case PIC32MK_PMD4:\n+    case PIC32MK_PMD5:\n+    case PIC32MK_PMD6:\n+    case PIC32MK_PMD7:\n+        if (s->cfgcon & PIC32MK_CFGCON_PMDLOCK) {\n+            /* Writes rejected when PMDLOCK = 1 */\n+            return;\n+        }\n+        s->pmd[(base - PIC32MK_PMD1) / 0x10] =\n+            apply_sci(s->pmd[(base - PIC32MK_PMD1) / 0x10], v32, sub);\n+        break;\n+\n+    default:\n+        qemu_log_mask(LOG_UNIMP,\n+                      \"pic32mk-cfg: unimplemented write @ 0x%03\" HWADDR_PRIx\n+                      \" = 0x%08x\\n\", addr, v32);\n+        break;\n+    }\n+}\n+\n+static const MemoryRegionOps pic32mk_cfg_ops = {\n+    .read       = pic32mk_cfg_read,\n+    .write      = pic32mk_cfg_write,\n+    .endianness = DEVICE_LITTLE_ENDIAN,\n+    .valid = {\n+        .min_access_size = 4,\n+        .max_access_size = 4,\n+    },\n+};\n+\n+/*\n+ * Realize / Reset\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void pic32mk_cfg_realize(DeviceState *dev, Error **errp)\n+{\n+    PIC32MKCFGState *s = PIC32MK_CFG(dev);\n+    memory_region_init_io(&s->mmio, OBJECT(s), &pic32mk_cfg_ops, s,\n+                          \"pic32mk-cfg\", PIC32MK_CFG_SIZE);\n+    sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->mmio);\n+}\n+\n+static void pic32mk_cfg_reset_hold(Object *obj, ResetType type)\n+{\n+    PIC32MKCFGState *s = PIC32MK_CFG(obj);\n+    s->cfgcon  = 0;\n+    s->cfgcon2 = 0;\n+    s->checon  = 0;\n+    s->syskey_state = SYSKEY_LOCKED;\n+    for (int i = 0; i < PIC32MK_PMD_COUNT; i++) {\n+        s->pmd[i] = 0;\n+    }\n+}\n+\n+/*\n+ * QOM boilerplate\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void pic32mk_cfg_class_init(ObjectClass *klass, const void *data)\n+{\n+    DeviceClass *dc = DEVICE_CLASS(klass);\n+    ResettableClass *rc = RESETTABLE_CLASS(klass);\n+    dc->realize = pic32mk_cfg_realize;\n+    dc->desc    = \"PIC32MK Configuration / PMD / SYSKEY\";\n+    rc->phases.hold = pic32mk_cfg_reset_hold;\n+}\n+\n+static const TypeInfo pic32mk_cfg_info = {\n+    .name          = TYPE_PIC32MK_CFG,\n+    .parent        = TYPE_SYS_BUS_DEVICE,\n+    .instance_size = sizeof(PIC32MKCFGState),\n+    .class_init    = pic32mk_cfg_class_init,\n+};\n+\n+static void pic32mk_cfg_register_types(void)\n+{\n+    type_register_static(&pic32mk_cfg_info);\n+}\n+\n+type_init(pic32mk_cfg_register_types)\ndiff --git a/hw/mips/pic32mk_cru.c b/hw/mips/pic32mk_cru.c\nnew file mode 100644\nindex 0000000000..3dead5107d\n--- /dev/null\n+++ b/hw/mips/pic32mk_cru.c\n@@ -0,0 +1,375 @@\n+/*\n+ * Microchip PIC32MK — Clock Reference Unit (CRU)\n+ * Datasheet: DS60001519E §9\n+ *\n+ * Models the CRU register block at 0xBF801200–0xBF80139F:\n+ *   OSCCON, OSCTUN, SPLLCON, UPLLCON, RCON, RSWRST, RNMICON, PWRCON,\n+ *   REFO1–4 CON/TRIM, PB1–7 DIV, CLKSTAT.\n+ *\n+ * All oscillator ready bits are returned set immediately — no PLL lock\n+ * timing is simulated.  Register values are stored but no dynamic clock\n+ * frequency propagation is performed; peripherals use the fixed\n+ * PIC32MK_CPU_HZ constant.\n+ *\n+ * RCON (§7) is also handled here since it falls within the CRU address\n+ * range (offset 0x40 = physical 0xBF801240).\n+ *\n+ * Copyright (c) 2026 QEMU contributors\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+#include \"qemu/log.h\"\n+#include \"qemu/module.h\"\n+#include \"hw/core/sysbus.h\"\n+#include \"hw/mips/pic32mk.h\"\n+#include \"system/runstate.h\"\n+\n+#define TYPE_PIC32MK_CRU \"pic32mk-cru\"\n+OBJECT_DECLARE_SIMPLE_TYPE(PIC32MKCRUState, PIC32MK_CRU)\n+\n+typedef struct PIC32MKCRUState {\n+    SysBusDevice parent_obj;\n+    MemoryRegion mmio;\n+\n+    /* Oscillator / PLL */\n+    uint32_t osccon;\n+    uint32_t osctun;\n+    uint32_t spllcon;\n+    uint32_t upllcon;\n+\n+    /* Reset control (RCON / RSWRST / RNMICON / PWRCON) */\n+    uint32_t rcon;\n+    uint32_t rswrst;\n+    uint32_t rnmicon;\n+    uint32_t pwrcon;\n+\n+    /* Reference clock outputs 1–4 */\n+    uint32_t refo_con[PIC32MK_CRU_NREFO];\n+    uint32_t refo_trim[PIC32MK_CRU_NREFO];\n+\n+    /* Peripheral bus clock dividers 1–7 */\n+    uint32_t pbdiv[PIC32MK_CRU_NPB];\n+} PIC32MKCRUState;\n+\n+/*\n+ * SET/CLR/INV helper — returns the new register value without applying it,\n+ * so the caller can enforce read-only masks.\n+ * sub == 0x0: direct write, 0x4: SET, 0x8: CLR, 0xC: INV\n+ * -----------------------------------------------------------------------\n+ */\n+static uint32_t apply_sci(uint32_t old, uint32_t val, unsigned sub)\n+{\n+    switch (sub) {\n+    case 0x0:\n+        return val;\n+    case 0x4:\n+        return old | val;\n+    case 0x8:\n+        return old & ~val;\n+    case 0xC:\n+        return old ^ val;\n+    default:\n+        return old;\n+        /* unreachable */;\n+    }\n+}\n+\n+/*\n+ * Read handler\n+ * -----------------------------------------------------------------------\n+ */\n+static uint64_t pic32mk_cru_read(void *opaque, hwaddr addr, unsigned size)\n+{\n+    PIC32MKCRUState *s = PIC32MK_CRU(opaque);\n+    hwaddr base = addr & ~0xFu;     /* register base (strip SET/CLR/INV) */\n+\n+    switch (base) {\n+    case PIC32MK_CRU_OSCCON:\n+        return s->osccon;\n+    case PIC32MK_CRU_OSCTUN:\n+        return s->osctun;\n+    case PIC32MK_CRU_SPLLCON:\n+        return s->spllcon;\n+    case PIC32MK_CRU_UPLLCON:\n+        return s->upllcon;\n+\n+    case PIC32MK_CRU_RCON:\n+        return s->rcon;\n+    case PIC32MK_CRU_RSWRST:\n+        return s->rswrst;\n+    case PIC32MK_CRU_RNMICON:\n+        return s->rnmicon;\n+    case PIC32MK_CRU_PWRCON:\n+        return s->pwrcon;\n+\n+    /* Reference clock outputs 1–4: CON then TRIM, 0x20 stride */\n+    case PIC32MK_CRU_REFO1CON:\n+        return s->refo_con[0];\n+    case PIC32MK_CRU_REFO1TRIM:\n+        return s->refo_trim[0];\n+    case PIC32MK_CRU_REFO2CON:\n+        return s->refo_con[1];\n+    case PIC32MK_CRU_REFO2TRIM:\n+        return s->refo_trim[1];\n+    case PIC32MK_CRU_REFO3CON:\n+        return s->refo_con[2];\n+    case PIC32MK_CRU_REFO3TRIM:\n+        return s->refo_trim[2];\n+    case PIC32MK_CRU_REFO4CON:\n+        return s->refo_con[3];\n+    case PIC32MK_CRU_REFO4TRIM:\n+        return s->refo_trim[3];\n+\n+    /* Peripheral bus dividers — 0x10 stride starting at 0x100 */\n+    case PIC32MK_CRU_PB1DIV:\n+        return s->pbdiv[0] | PIC32MK_PBDIV_PBDIVRDY;\n+    case PIC32MK_CRU_PB2DIV:\n+        return s->pbdiv[1] | PIC32MK_PBDIV_PBDIVRDY;\n+    case PIC32MK_CRU_PB3DIV:\n+        return s->pbdiv[2] | PIC32MK_PBDIV_PBDIVRDY;\n+    case PIC32MK_CRU_PB4DIV:\n+        return s->pbdiv[3] | PIC32MK_PBDIV_PBDIVRDY;\n+    case PIC32MK_CRU_PB5DIV:\n+        return s->pbdiv[4] | PIC32MK_PBDIV_PBDIVRDY;\n+    case PIC32MK_CRU_PB6DIV:\n+        return s->pbdiv[5] | PIC32MK_PBDIV_PBDIVRDY;\n+    case PIC32MK_CRU_PB7DIV:\n+        return s->pbdiv[6] | PIC32MK_PBDIV_PBDIVRDY;\n+\n+    /* CLKSTAT — always report all clocks ready */\n+    case PIC32MK_CRU_CLKSTAT:\n+        return PIC32MK_CLKSTAT_ALL_RDY;\n+\n+    default:\n+        qemu_log_mask(LOG_UNIMP,\n+                      \"pic32mk-cru: unimplemented read @ 0x%03\" HWADDR_PRIx \"\\n\",\n+                      addr);\n+        return 0;\n+    }\n+}\n+\n+/*\n+ * Write handler\n+ * -----------------------------------------------------------------------\n+ */\n+static void pic32mk_cru_write(void *opaque, hwaddr addr, uint64_t val,\n+                               unsigned size)\n+{\n+    PIC32MKCRUState *s = PIC32MK_CRU(opaque);\n+    hwaddr base = addr & ~0xFu;\n+    unsigned sub = addr & 0xCu;\n+    uint32_t v32 = (uint32_t)val;\n+\n+    switch (base) {\n+    case PIC32MK_CRU_OSCCON:\n+        s->osccon = apply_sci(s->osccon, v32, sub);\n+        /*\n+         * Mirror NOSC → COSC immediately and set SLOCK.\n+         * Real hardware would do an oscillator switch sequence; in emulation\n+         * the switch completes instantly.\n+         */\n+        if (s->osccon & PIC32MK_OSCCON_OSWEN) {\n+            uint32_t nosc = (s->osccon & PIC32MK_OSCCON_NOSC_MASK)\n+                            >> PIC32MK_OSCCON_NOSC_SHIFT;\n+            s->osccon &= ~(PIC32MK_OSCCON_COSC_MASK | PIC32MK_OSCCON_OSWEN);\n+            s->osccon |= nosc << PIC32MK_OSCCON_COSC_SHIFT;\n+        }\n+        break;\n+\n+    case PIC32MK_CRU_OSCTUN:\n+        s->osctun = apply_sci(s->osctun, v32, sub);\n+        break;\n+\n+    case PIC32MK_CRU_SPLLCON:\n+        s->spllcon = apply_sci(s->spllcon, v32, sub);\n+        break;\n+\n+    case PIC32MK_CRU_UPLLCON:\n+        s->upllcon = apply_sci(s->upllcon, v32, sub);\n+        break;\n+\n+    case PIC32MK_CRU_RCON:\n+        s->rcon = apply_sci(s->rcon, v32, sub);\n+        break;\n+\n+    case PIC32MK_CRU_RSWRST:\n+        s->rswrst = apply_sci(s->rswrst, v32, sub);\n+        if (s->rswrst & 1u) {\n+            qemu_log_mask(LOG_GUEST_ERROR,\n+                          \"pic32mk-cru: software reset triggered (RSWRST)\\n\");\n+            qemu_system_reset_request(SHUTDOWN_CAUSE_GUEST_RESET);\n+        }\n+        break;\n+\n+    case PIC32MK_CRU_RNMICON:\n+        s->rnmicon = apply_sci(s->rnmicon, v32, sub);\n+        break;\n+\n+    case PIC32MK_CRU_PWRCON:\n+        s->pwrcon = apply_sci(s->pwrcon, v32, sub);\n+        break;\n+\n+    /* Reference clock outputs 1–4 */\n+    case PIC32MK_CRU_REFO1CON:\n+        s->refo_con[0] = apply_sci(s->refo_con[0], v32, sub);\n+        if (s->refo_con[0] & PIC32MK_REFOCON_ON) {\n+            s->refo_con[0] |= PIC32MK_REFOCON_ACTIVE;\n+        } else {\n+            s->refo_con[0] &= ~PIC32MK_REFOCON_ACTIVE;\n+        }\n+        break;\n+    case PIC32MK_CRU_REFO1TRIM:\n+        s->refo_trim[0] = apply_sci(s->refo_trim[0], v32, sub);\n+        break;\n+\n+    case PIC32MK_CRU_REFO2CON:\n+        s->refo_con[1] = apply_sci(s->refo_con[1], v32, sub);\n+        if (s->refo_con[1] & PIC32MK_REFOCON_ON) {\n+            s->refo_con[1] |= PIC32MK_REFOCON_ACTIVE;\n+        } else {\n+            s->refo_con[1] &= ~PIC32MK_REFOCON_ACTIVE;\n+        }\n+        break;\n+    case PIC32MK_CRU_REFO2TRIM:\n+        s->refo_trim[1] = apply_sci(s->refo_trim[1], v32, sub);\n+        break;\n+\n+    case PIC32MK_CRU_REFO3CON:\n+        s->refo_con[2] = apply_sci(s->refo_con[2], v32, sub);\n+        if (s->refo_con[2] & PIC32MK_REFOCON_ON) {\n+            s->refo_con[2] |= PIC32MK_REFOCON_ACTIVE;\n+        } else {\n+            s->refo_con[2] &= ~PIC32MK_REFOCON_ACTIVE;\n+        }\n+        break;\n+    case PIC32MK_CRU_REFO3TRIM:\n+        s->refo_trim[2] = apply_sci(s->refo_trim[2], v32, sub);\n+        break;\n+\n+    case PIC32MK_CRU_REFO4CON:\n+        s->refo_con[3] = apply_sci(s->refo_con[3], v32, sub);\n+        if (s->refo_con[3] & PIC32MK_REFOCON_ON) {\n+            s->refo_con[3] |= PIC32MK_REFOCON_ACTIVE;\n+        } else {\n+            s->refo_con[3] &= ~PIC32MK_REFOCON_ACTIVE;\n+        }\n+        break;\n+    case PIC32MK_CRU_REFO4TRIM:\n+        s->refo_trim[3] = apply_sci(s->refo_trim[3], v32, sub);\n+        break;\n+\n+    /* Peripheral bus dividers (PBDIVRDY is read-only, masked out) */\n+    case PIC32MK_CRU_PB1DIV:\n+        s->pbdiv[0] = apply_sci(s->pbdiv[0], v32, sub) & ~PIC32MK_PBDIV_PBDIVRDY;\n+        break;\n+    case PIC32MK_CRU_PB2DIV:\n+        s->pbdiv[1] = apply_sci(s->pbdiv[1], v32, sub) & ~PIC32MK_PBDIV_PBDIVRDY;\n+        break;\n+    case PIC32MK_CRU_PB3DIV:\n+        s->pbdiv[2] = apply_sci(s->pbdiv[2], v32, sub) & ~PIC32MK_PBDIV_PBDIVRDY;\n+        break;\n+    case PIC32MK_CRU_PB4DIV:\n+        s->pbdiv[3] = apply_sci(s->pbdiv[3], v32, sub) & ~PIC32MK_PBDIV_PBDIVRDY;\n+        break;\n+    case PIC32MK_CRU_PB5DIV:\n+        s->pbdiv[4] = apply_sci(s->pbdiv[4], v32, sub) & ~PIC32MK_PBDIV_PBDIVRDY;\n+        break;\n+    case PIC32MK_CRU_PB6DIV:\n+        s->pbdiv[5] = apply_sci(s->pbdiv[5], v32, sub) & ~PIC32MK_PBDIV_PBDIVRDY;\n+        break;\n+    case PIC32MK_CRU_PB7DIV:\n+        s->pbdiv[6] = apply_sci(s->pbdiv[6], v32, sub) & ~PIC32MK_PBDIV_PBDIVRDY;\n+        break;\n+\n+    case PIC32MK_CRU_CLKSTAT:\n+        /* Read-only register */\n+        qemu_log_mask(LOG_GUEST_ERROR,\n+                      \"pic32mk-cru: write to read-only CLKSTAT ignored\\n\");\n+        break;\n+\n+    default:\n+        qemu_log_mask(LOG_UNIMP,\n+                      \"pic32mk-cru: unimplemented write @ 0x%03\" HWADDR_PRIx\n+                      \" = 0x%08x\\n\", addr, v32);\n+        break;\n+    }\n+}\n+\n+static const MemoryRegionOps pic32mk_cru_ops = {\n+    .read       = pic32mk_cru_read,\n+    .write      = pic32mk_cru_write,\n+    .endianness = DEVICE_LITTLE_ENDIAN,\n+    .valid = {\n+        .min_access_size = 4,\n+        .max_access_size = 4,\n+    },\n+};\n+\n+/*\n+ * Realize / Reset\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void pic32mk_cru_realize(DeviceState *dev, Error **errp)\n+{\n+    PIC32MKCRUState *s = PIC32MK_CRU(dev);\n+    memory_region_init_io(&s->mmio, OBJECT(s), &pic32mk_cru_ops, s,\n+                          \"pic32mk-cru\", PIC32MK_CRU_SIZE);\n+    sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->mmio);\n+}\n+\n+static void pic32mk_cru_reset_hold(Object *obj, ResetType type)\n+{\n+    PIC32MKCRUState *s = PIC32MK_CRU(obj);\n+\n+    /* OSCCON: COSC = 001 (SPLL), clock locked */\n+    s->osccon = (1u << PIC32MK_OSCCON_COSC_SHIFT);\n+    s->osctun = 0;\n+    s->spllcon = 0;\n+    s->upllcon = 0;\n+\n+    /* RCON: Power-on Reset + Brown-out Reset flags */\n+    s->rcon = PIC32MK_RCON_POR | PIC32MK_RCON_BOR;\n+    s->rswrst  = 0;\n+    s->rnmicon = 0;\n+    s->pwrcon  = 0;\n+\n+    for (int i = 0; i < PIC32MK_CRU_NREFO; i++) {\n+        s->refo_con[i]  = 0;\n+        s->refo_trim[i] = 0;\n+    }\n+\n+    /* PB1 is always on; PB2–7 default ON */\n+    for (int i = 0; i < PIC32MK_CRU_NPB; i++) {\n+        s->pbdiv[i] = PIC32MK_PBDIV_ON;\n+    }\n+}\n+\n+/*\n+ * QOM boilerplate\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void pic32mk_cru_class_init(ObjectClass *klass, const void *data)\n+{\n+    DeviceClass *dc = DEVICE_CLASS(klass);\n+    ResettableClass *rc = RESETTABLE_CLASS(klass);\n+    dc->realize = pic32mk_cru_realize;\n+    dc->desc    = \"PIC32MK Clock Reference Unit\";\n+    rc->phases.hold = pic32mk_cru_reset_hold;\n+}\n+\n+static const TypeInfo pic32mk_cru_info = {\n+    .name          = TYPE_PIC32MK_CRU,\n+    .parent        = TYPE_SYS_BUS_DEVICE,\n+    .instance_size = sizeof(PIC32MKCRUState),\n+    .class_init    = pic32mk_cru_class_init,\n+};\n+\n+static void pic32mk_cru_register_types(void)\n+{\n+    type_register_static(&pic32mk_cru_info);\n+}\n+\n+type_init(pic32mk_cru_register_types)\ndiff --git a/hw/mips/pic32mk_dataee.c b/hw/mips/pic32mk_dataee.c\nnew file mode 100644\nindex 0000000000..c35a8326ad\n--- /dev/null\n+++ b/hw/mips/pic32mk_dataee.c\n@@ -0,0 +1,463 @@\n+/*\n+ * PIC32MK Data EEPROM (DATAEE) emulation\n+ * Datasheet: DS60001519E, §11\n+ *\n+ * 4 KB (1024 × 32-bit words) of on-chip data EEPROM emulated via SFR\n+ * control registers EECON, EEKEY, EEADDR, EEDATA at 0xBF829000.\n+ *\n+ * Optional host-file backing: pass -global pic32mk-dataee.filename=eeprom.bin\n+ * to persist EEPROM contents across QEMU sessions.  Without a backing file\n+ * the data lives in RAM only and is lost on exit.\n+ *\n+ * Copyright (c) 2026 QEMU contributors\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+#include \"qemu/log.h\"\n+#include \"qemu/module.h\"\n+#include \"qapi/error.h\"\n+#include \"hw/core/sysbus.h\"\n+#include \"hw/core/irq.h\"\n+#include \"hw/core/qdev-properties.h\"\n+#include \"hw/mips/pic32mk.h\"\n+\n+#include <fcntl.h>\n+#include <unistd.h>\n+\n+#define TYPE_PIC32MK_DATAEE \"pic32mk-dataee\"\n+OBJECT_DECLARE_SIMPLE_TYPE(PIC32MKDataEEState, PIC32MK_DATAEE)\n+\n+/* EECON error codes (bits [5:4]) */\n+#define EECON_ERR_NONE      0u\n+#define EECON_ERR_VERIFY    1u\n+#define EECON_ERR_INVALID   2u\n+#define EECON_ERR_BOR       3u\n+\n+/* EEKEY unlock FSM states */\n+#define EEKEY_LOCKED        0\n+#define EEKEY_KEY1_OK       1\n+#define EEKEY_UNLOCKED      2\n+\n+/* Number of DEVEE config words written during EEPROM_Initialize() */\n+#define DATAEE_NUM_CONFIG   4\n+\n+struct PIC32MKDataEEState {\n+    SysBusDevice parent_obj;\n+    MemoryRegion mr;\n+\n+    /* SFR registers */\n+    uint32_t eecon;\n+    uint32_t eeaddr;\n+    uint32_t eedata;\n+    int      eekey_state;       /* unlock FSM */\n+\n+    /* EEPROM storage */\n+    uint32_t data[PIC32MK_DATAEE_WORDS];\n+    uint32_t config[DATAEE_NUM_CONFIG]; /* DEVEE0–3 shadow */\n+\n+    /* Host backing file */\n+    char    *filename;          /* qdev string property */\n+    int      backing_fd;\n+    bool     dirty;\n+\n+    /* IRQ output → EVIC vector 186 */\n+    qemu_irq irq;\n+};\n+\n+/*\n+ * Backing file helpers\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void dataee_load_backing(PIC32MKDataEEState *s)\n+{\n+    if (s->backing_fd < 0) {\n+        return;\n+    }\n+    lseek(s->backing_fd, 0, SEEK_SET);\n+    ssize_t n = read(s->backing_fd, s->data, sizeof(s->data));\n+    if (n < 0) {\n+        qemu_log_mask(LOG_GUEST_ERROR,\n+                      \"pic32mk_dataee: backing file read error\\n\");\n+    } else if ((size_t)n < sizeof(s->data)) {\n+        /* Short read — zero-pad remainder (first use: file is empty) */\n+        memset((uint8_t *)s->data + n, 0xFF, sizeof(s->data) - (size_t)n);\n+    }\n+}\n+\n+static void dataee_flush_backing(PIC32MKDataEEState *s)\n+{\n+    if (s->backing_fd < 0 || !s->dirty) {\n+        return;\n+    }\n+    lseek(s->backing_fd, 0, SEEK_SET);\n+    ssize_t n = write(s->backing_fd, s->data, sizeof(s->data));\n+    if (n < 0) {\n+        qemu_log_mask(LOG_GUEST_ERROR,\n+                      \"pic32mk_dataee: backing file write error\\n\");\n+    }\n+    fdatasync(s->backing_fd);\n+    s->dirty = false;\n+}\n+\n+/*\n+ * SET/CLR/INV helper (PIC32MK convention: +0=REG, +4=CLR, +8=SET, +C=INV)\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void apply_sci(uint32_t *reg, uint32_t val, int sub)\n+{\n+    switch (sub) {\n+    case 0x0:\n+        *reg  = val;\n+        break;\n+    case 0x4:\n+        *reg &= ~val;\n+        break;\n+    case 0x8:\n+        *reg |= val;\n+        break;\n+    case 0xC:\n+        *reg ^= val;\n+        break;\n+    }\n+}\n+\n+/*\n+ * Command execution — triggered when RW transitions 0 → 1\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void dataee_set_err(PIC32MKDataEEState *s, uint32_t code)\n+{\n+    s->eecon = (s->eecon & ~PIC32MK_EECON_ERR_MASK)\n+             | ((code << PIC32MK_EECON_ERR_SHIFT) & PIC32MK_EECON_ERR_MASK);\n+}\n+\n+static void dataee_execute_cmd(PIC32MKDataEEState *s)\n+{\n+    uint32_t cmd  = s->eecon & PIC32MK_EECON_CMD_MASK;\n+    uint32_t addr = s->eeaddr & PIC32MK_EEADDR_MASK;\n+    uint32_t word_idx = addr >> 2;\n+\n+    /* Clear previous error */\n+    dataee_set_err(s, EECON_ERR_NONE);\n+\n+    switch (cmd) {\n+    case PIC32MK_EECMD_WORD_READ:\n+        /* Read does NOT require WREN or unlock */\n+        if (word_idx < PIC32MK_DATAEE_WORDS) {\n+            s->eedata = s->data[word_idx];\n+        } else {\n+            s->eedata = 0xFFFFFFFFu;\n+            dataee_set_err(s, EECON_ERR_INVALID);\n+        }\n+        break;\n+\n+    case PIC32MK_EECMD_WORD_WRITE:\n+        if (!(s->eecon & PIC32MK_EECON_WREN)) {\n+            dataee_set_err(s, EECON_ERR_INVALID);\n+            break;\n+        }\n+        if (s->eekey_state != EEKEY_UNLOCKED) {\n+            dataee_set_err(s, EECON_ERR_INVALID);\n+            break;\n+        }\n+        if (word_idx < PIC32MK_DATAEE_WORDS) {\n+            s->data[word_idx] = s->eedata;\n+            s->dirty = true;\n+            dataee_flush_backing(s);\n+        } else {\n+            dataee_set_err(s, EECON_ERR_INVALID);\n+        }\n+        break;\n+\n+    case PIC32MK_EECMD_PAGE_ERASE: {\n+        if (!(s->eecon & PIC32MK_EECON_WREN)) {\n+            dataee_set_err(s, EECON_ERR_INVALID);\n+            break;\n+        }\n+        if (s->eekey_state != EEKEY_UNLOCKED) {\n+            dataee_set_err(s, EECON_ERR_INVALID);\n+            break;\n+        }\n+        /* Page-align the address */\n+        uint32_t page_start = (word_idx / PIC32MK_DATAEE_PAGE_WORDS)\n+                            * PIC32MK_DATAEE_PAGE_WORDS;\n+        for (uint32_t i = 0; i < PIC32MK_DATAEE_PAGE_WORDS; i++) {\n+            if (page_start + i < PIC32MK_DATAEE_WORDS) {\n+                s->data[page_start + i] = 0xFFFFFFFFu;\n+            }\n+        }\n+        s->dirty = true;\n+        dataee_flush_backing(s);\n+        break;\n+    }\n+\n+    case PIC32MK_EECMD_BULK_ERASE:\n+        if (!(s->eecon & PIC32MK_EECON_WREN)) {\n+            dataee_set_err(s, EECON_ERR_INVALID);\n+            break;\n+        }\n+        if (s->eekey_state != EEKEY_UNLOCKED) {\n+            dataee_set_err(s, EECON_ERR_INVALID);\n+            break;\n+        }\n+        memset(s->data, 0xFF, sizeof(s->data));\n+        s->dirty = true;\n+        dataee_flush_backing(s);\n+        break;\n+\n+    case PIC32MK_EECMD_CONFIG_WRITE:\n+        if (!(s->eecon & PIC32MK_EECON_WREN)) {\n+            dataee_set_err(s, EECON_ERR_INVALID);\n+            break;\n+        }\n+        if (s->eekey_state != EEKEY_UNLOCKED) {\n+            dataee_set_err(s, EECON_ERR_INVALID);\n+            break;\n+        }\n+        /* Config writes target DEVEE0–3 shadow (addr 0x00/04/08/0C) */\n+        if (word_idx < DATAEE_NUM_CONFIG) {\n+            s->config[word_idx] = s->eedata;\n+        } else {\n+            dataee_set_err(s, EECON_ERR_INVALID);\n+        }\n+        break;\n+\n+    default:\n+        qemu_log_mask(LOG_UNIMP,\n+                      \"pic32mk_dataee: unimplemented CMD %u\\n\", cmd);\n+        dataee_set_err(s, EECON_ERR_INVALID);\n+        break;\n+    }\n+\n+    /* Operation complete — clear RW, reset unlock FSM */\n+    s->eecon &= ~PIC32MK_EECON_RW;\n+    s->eekey_state = EEKEY_LOCKED;\n+\n+    /* Pulse IRQ to signal completion */\n+    qemu_irq_pulse(s->irq);\n+}\n+\n+/*\n+ * MMIO read\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static uint64_t dataee_read(void *opaque, hwaddr addr, unsigned size)\n+{\n+    PIC32MKDataEEState *s = opaque;\n+    hwaddr base = addr & ~(hwaddr)0xF;\n+\n+    switch (base) {\n+    case PIC32MK_EECON:\n+        return s->eecon;\n+\n+    case PIC32MK_EEKEY:\n+        /* Write-only register — reads return 0 */\n+        return 0;\n+\n+    case PIC32MK_EEADDR:\n+        return s->eeaddr;\n+\n+    case PIC32MK_EEDATA_REG:\n+        return s->eedata;\n+\n+    default:\n+        qemu_log_mask(LOG_UNIMP,\n+                      \"pic32mk_dataee: unimplemented read @ 0x%04\"\n+                      HWADDR_PRIx \"\\n\", addr);\n+        return 0;\n+    }\n+}\n+\n+/*\n+ * MMIO write\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void dataee_write(void *opaque, hwaddr addr, uint64_t val,\n+                         unsigned size)\n+{\n+    PIC32MKDataEEState *s = opaque;\n+    int sub     = (int)(addr & 0xF);\n+    hwaddr base = addr & ~(hwaddr)0xF;\n+    uint32_t old_eecon;\n+\n+    switch (base) {\n+    case PIC32MK_EECON:\n+        old_eecon = s->eecon;\n+        apply_sci(&s->eecon, (uint32_t)val, sub);\n+\n+        /* RDY is automatically set when ON is set (no startup delay in emu) */\n+        if (s->eecon & PIC32MK_EECON_ON) {\n+            s->eecon |= PIC32MK_EECON_RDY;\n+        } else {\n+            s->eecon &= ~PIC32MK_EECON_RDY;\n+        }\n+\n+        /* Detect RW 0→1 transition: execute command */\n+        if (!(old_eecon & PIC32MK_EECON_RW) &&\n+             (s->eecon & PIC32MK_EECON_RW)) {\n+            dataee_execute_cmd(s);\n+        }\n+        break;\n+\n+    case PIC32MK_EEKEY:\n+        /*\n+         * Unlock FSM: firmware writes 0xEDB7 then 0x1248.\n+         * Any other value resets the FSM.  SET/CLR/INV are not\n+         * meaningful for EEKEY — always treat as direct write.\n+         */\n+        {\n+            uint32_t key = (uint32_t)val & 0xFFFFu;\n+            if (s->eekey_state == EEKEY_LOCKED &&\n+                key == PIC32MK_EEKEY1) {\n+                s->eekey_state = EEKEY_KEY1_OK;\n+            } else if (s->eekey_state == EEKEY_KEY1_OK &&\n+                       key == PIC32MK_EEKEY2) {\n+                s->eekey_state = EEKEY_UNLOCKED;\n+            } else {\n+                s->eekey_state = EEKEY_LOCKED;\n+            }\n+        }\n+        break;\n+\n+    case PIC32MK_EEADDR:\n+        apply_sci(&s->eeaddr, (uint32_t)val, sub);\n+        s->eeaddr &= PIC32MK_EEADDR_MASK;\n+        break;\n+\n+    case PIC32MK_EEDATA_REG:\n+        apply_sci(&s->eedata, (uint32_t)val, sub);\n+        break;\n+\n+    default:\n+        qemu_log_mask(LOG_UNIMP,\n+                      \"pic32mk_dataee: unimplemented write @ 0x%04\"\n+                      HWADDR_PRIx \" = 0x%08\" PRIx64 \"\\n\",\n+                      addr, val);\n+        break;\n+    }\n+}\n+\n+static const MemoryRegionOps dataee_ops = {\n+    .read       = dataee_read,\n+    .write      = dataee_write,\n+    .endianness = DEVICE_LITTLE_ENDIAN,\n+    .valid = { .min_access_size = 4, .max_access_size = 4 },\n+};\n+\n+/*\n+ * Device lifecycle\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void pic32mk_dataee_reset(DeviceState *dev)\n+{\n+    PIC32MKDataEEState *s = PIC32MK_DATAEE(dev);\n+\n+    s->eecon       = 0;\n+    s->eeaddr      = 0;\n+    s->eedata      = 0;\n+    s->eekey_state = EEKEY_LOCKED;\n+\n+    /* Erased state = all 0xFF */\n+    memset(s->data, 0xFF, sizeof(s->data));\n+    memset(s->config, 0xFF, sizeof(s->config));\n+    s->dirty = false;\n+\n+    /* Load backing file contents (overwrites erased state if file exists) */\n+    dataee_load_backing(s);\n+\n+    qemu_irq_lower(s->irq);\n+}\n+\n+static void pic32mk_dataee_realize(DeviceState *dev, Error **errp)\n+{\n+    PIC32MKDataEEState *s = PIC32MK_DATAEE(dev);\n+\n+    s->backing_fd = -1;\n+\n+    if (s->filename && s->filename[0] != '\\0') {\n+        /* Open or create the backing file (read-write) */\n+        s->backing_fd = open(s->filename, O_RDWR | O_CREAT, 0644);\n+        if (s->backing_fd < 0) {\n+            error_setg_errno(errp, errno,\n+                             \"pic32mk_dataee: cannot open '%s'\",\n+                             s->filename);\n+            return;\n+        }\n+\n+        /*\n+         * If the file is new or undersized, initialize it with 0xFF\n+         * (the erased state for EEPROM).  This avoids the user having\n+         * to pre-create the file with the right content.\n+         */\n+        off_t fsize = lseek(s->backing_fd, 0, SEEK_END);\n+        if (fsize < (off_t)sizeof(s->data)) {\n+            uint8_t ff_buf[PIC32MK_DATAEE_WORDS * 4];\n+            memset(ff_buf, 0xFF, sizeof(ff_buf));\n+            lseek(s->backing_fd, 0, SEEK_SET);\n+            if (write(s->backing_fd, ff_buf, sizeof(ff_buf)) < 0) {\n+                qemu_log_mask(LOG_GUEST_ERROR,\n+                              \"pic32mk_dataee: cannot init '%s'\\n\",\n+                              s->filename);\n+            }\n+            fdatasync(s->backing_fd);\n+        }\n+    }\n+}\n+\n+static void pic32mk_dataee_unrealize(DeviceState *dev)\n+{\n+    PIC32MKDataEEState *s = PIC32MK_DATAEE(dev);\n+\n+    dataee_flush_backing(s);\n+\n+    if (s->backing_fd >= 0) {\n+        close(s->backing_fd);\n+        s->backing_fd = -1;\n+    }\n+}\n+\n+static void pic32mk_dataee_init(Object *obj)\n+{\n+    PIC32MKDataEEState *s = PIC32MK_DATAEE(obj);\n+\n+    memory_region_init_io(&s->mr, obj, &dataee_ops, s,\n+                          TYPE_PIC32MK_DATAEE, PIC32MK_DATAEE_SIZE);\n+    sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->mr);\n+    sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq);\n+}\n+\n+static const Property pic32mk_dataee_props[] = {\n+    DEFINE_PROP_STRING(\"filename\", PIC32MKDataEEState, filename),\n+};\n+\n+static void pic32mk_dataee_class_init(ObjectClass *klass, const void *data)\n+{\n+    DeviceClass *dc = DEVICE_CLASS(klass);\n+\n+    dc->realize   = pic32mk_dataee_realize;\n+    dc->unrealize = pic32mk_dataee_unrealize;\n+    device_class_set_legacy_reset(dc, pic32mk_dataee_reset);\n+    device_class_set_props(dc, pic32mk_dataee_props);\n+}\n+\n+static const TypeInfo pic32mk_dataee_info = {\n+    .name          = TYPE_PIC32MK_DATAEE,\n+    .parent        = TYPE_SYS_BUS_DEVICE,\n+    .instance_size = sizeof(PIC32MKDataEEState),\n+    .instance_init = pic32mk_dataee_init,\n+    .class_init    = pic32mk_dataee_class_init,\n+};\n+\n+static void pic32mk_dataee_register_types(void)\n+{\n+    type_register_static(&pic32mk_dataee_info);\n+}\n+\n+type_init(pic32mk_dataee_register_types)\ndiff --git a/hw/mips/pic32mk_dma.c b/hw/mips/pic32mk_dma.c\nnew file mode 100644\nindex 0000000000..c1fd503b16\n--- /dev/null\n+++ b/hw/mips/pic32mk_dma.c\n@@ -0,0 +1,255 @@\n+/*\n+ * PIC32MK DMA Controller × 8 channels\n+ * Datasheet: DS60001519E, §26\n+ *\n+ * Phase 2B stub: accepts all register reads/writes, logs unimplemented\n+ * channel transfers.  Actual DMA transfer engine is deferred.\n+ *\n+ * Memory map (base physical 0x1F811000 = SFR_BASE + DMA_OFFSET):\n+ *   +0x000: DMACON (global control)\n+ *   +0x010: DMASTAT\n+ *   +0x020: DMAADDR\n+ *   +0x060: Channel 0 registers (DCH0CON, DCH0ECON, DCH0INT, ...)\n+ *   +0x120: Channel 1 registers\n+ *   ...  (stride 0xC0 per channel)\n+ *\n+ * Copyright (c) 2026 QEMU contributors\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+#include \"qemu/log.h\"\n+#include \"qemu/module.h\"\n+#include \"hw/core/sysbus.h\"\n+#include \"hw/core/irq.h\"\n+#include \"hw/mips/pic32mk.h\"\n+\n+/* Total MMIO size: channel 7 ends at 0x060 + 7*0xC0 + 0xC0 = 0x060 + 0x780 = 0x7E0 */\n+#define DMA_MMIO_SIZE   0x1000u  /* full 4 KB page */\n+\n+#define TYPE_PIC32MK_DMA    \"pic32mk-dma\"\n+OBJECT_DECLARE_SIMPLE_TYPE(PIC32MKDMAState, PIC32MK_DMA)\n+\n+/* Per-channel register state */\n+typedef struct {\n+    uint32_t con;   /* DCHxCON */\n+    uint32_t econ;  /* DCHxECON */\n+    uint32_t ireg;  /* DCHxINT */\n+    uint32_t ssa;   /* source start address */\n+    uint32_t dsa;   /* destination start address */\n+    uint32_t ssiz;  /* source size */\n+    uint32_t dsiz;  /* destination size */\n+    uint32_t sptr;  /* source pointer */\n+    uint32_t dptr;  /* destination pointer */\n+    uint32_t csiz;  /* cell size */\n+    uint32_t cptr;  /* cell pointer */\n+    uint32_t dat;   /* pattern data */\n+} DMAChannel;\n+\n+struct PIC32MKDMAState {\n+    SysBusDevice parent_obj;\n+    MemoryRegion mr;\n+\n+    uint32_t   con;     /* DMACON */\n+    uint32_t   stat;    /* DMASTAT */\n+    uint32_t   addr;    /* DMAADDR */\n+\n+    DMAChannel ch[PIC32MK_DMA_NCHANNELS];\n+    qemu_irq   irq[PIC32MK_DMA_NCHANNELS];\n+};\n+\n+/*\n+ * Helpers\n+ * -----------------------------------------------------------------------\n+ */\n+\n+/* PIC32MK: base+0=REG, +4=CLR, +8=SET, +0xC=INV */\n+static void apply_sci(uint32_t *reg, uint32_t val, int sub)\n+{\n+    switch (sub) {\n+    case 0:\n+        *reg  = val;\n+        break;\n+    case 4:\n+        *reg &= ~val;\n+        break;\n+    case 8:\n+        *reg |= val;\n+        break;\n+    case 12:\n+        *reg ^= val;\n+        break;\n+    }\n+}\n+\n+/* Map channel register offset to state field */\n+static uint32_t *dma_ch_reg(DMAChannel *ch, hwaddr off)\n+{\n+    switch (off) {\n+    case PIC32MK_DCHxCON:\n+        return &ch->con;\n+    case PIC32MK_DCHxECON:\n+        return &ch->econ;\n+    case PIC32MK_DCHxINT:\n+        return &ch->ireg;\n+    case PIC32MK_DCHxSSA:\n+        return &ch->ssa;\n+    case PIC32MK_DCHxDSA:\n+        return &ch->dsa;\n+    case PIC32MK_DCHxSSIZ:\n+        return &ch->ssiz;\n+    case PIC32MK_DCHxDSIZ:\n+        return &ch->dsiz;\n+    case PIC32MK_DCHxSPTR:\n+        return &ch->sptr;\n+    case PIC32MK_DCHxDPTR:\n+        return &ch->dptr;\n+    case PIC32MK_DCHxCSIZ:\n+        return &ch->csiz;\n+    case PIC32MK_DCHxCPTR:\n+        return &ch->cptr;\n+    case PIC32MK_DCHxDAT:\n+        return &ch->dat;\n+    default:\n+        return NULL;\n+    }\n+}\n+\n+/*\n+ * MMIO\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static uint64_t dma_read(void *opaque, hwaddr addr, unsigned size)\n+{\n+    PIC32MKDMAState *s = opaque;\n+    int sub     = (int)(addr & 0xF);\n+    hwaddr base = addr & ~(hwaddr)0xF;\n+\n+    (void)sub;  /* reads always return base register value */\n+\n+    /* Global registers */\n+    if (base == PIC32MK_DMACON_OFFSET) {\n+        return s->con;\n+    }\n+    if (base == PIC32MK_DMASTAT_OFFSET) {\n+        return s->stat;\n+    }\n+    if (base == PIC32MK_DMAADDR_OFFSET) {\n+        return s->addr;\n+    }\n+\n+    /* Channel registers */\n+    if (addr >= PIC32MK_DMA_CH_BASE) {\n+        hwaddr ch_off = base - PIC32MK_DMA_CH_BASE;\n+        int ch_idx = (int)(ch_off / PIC32MK_DMA_CH_STRIDE);\n+        hwaddr reg_off = ch_off % PIC32MK_DMA_CH_STRIDE;\n+\n+        if (ch_idx < PIC32MK_DMA_NCHANNELS) {\n+            uint32_t *reg = dma_ch_reg(&s->ch[ch_idx], reg_off);\n+            if (reg) {\n+                return *reg;\n+            }\n+        }\n+    }\n+\n+    qemu_log_mask(LOG_UNIMP,\n+                  \"pic32mk_dma: unimplemented read @ 0x%04\" HWADDR_PRIx \"\\n\",\n+                  addr);\n+    return 0;\n+}\n+\n+static void dma_write(void *opaque, hwaddr addr, uint64_t val, unsigned size)\n+{\n+    PIC32MKDMAState *s = opaque;\n+    int sub     = (int)(addr & 0xF);\n+    hwaddr base = addr & ~(hwaddr)0xF;\n+\n+    /* Global registers */\n+    if (base == PIC32MK_DMACON_OFFSET) {\n+        apply_sci(&s->con, (uint32_t)val, sub);\n+        return;\n+    }\n+    if (base == PIC32MK_DMASTAT_OFFSET || base == PIC32MK_DMAADDR_OFFSET) {\n+        return;  /* read-only */\n+    }\n+\n+    /* Channel registers */\n+    if (addr >= PIC32MK_DMA_CH_BASE) {\n+        hwaddr ch_off  = base - PIC32MK_DMA_CH_BASE;\n+        int    ch_idx  = (int)(ch_off / PIC32MK_DMA_CH_STRIDE);\n+        hwaddr reg_off = ch_off % PIC32MK_DMA_CH_STRIDE;\n+\n+        if (ch_idx < PIC32MK_DMA_NCHANNELS) {\n+            uint32_t *reg = dma_ch_reg(&s->ch[ch_idx], reg_off);\n+            if (reg) {\n+                apply_sci(reg, (uint32_t)val, sub);\n+                /* TODO: trigger DMA transfer if CHEN and FORCE bits set */\n+                return;\n+            }\n+        }\n+    }\n+\n+    qemu_log_mask(LOG_UNIMP,\n+                  \"pic32mk_dma: unimplemented write @ 0x%04\"\n+                  HWADDR_PRIx \" = 0x%08\" PRIx64 \"\\n\",\n+                  addr, val);\n+}\n+\n+static const MemoryRegionOps dma_ops = {\n+    .read       = dma_read,\n+    .write      = dma_write,\n+    .endianness = DEVICE_LITTLE_ENDIAN,\n+    .valid = { .min_access_size = 4, .max_access_size = 4 },\n+};\n+\n+/*\n+ * Device lifecycle\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void pic32mk_dma_reset(DeviceState *dev)\n+{\n+    PIC32MKDMAState *s = PIC32MK_DMA(dev);\n+    s->con  = 0x80000000u;  /* DMABUSY=0, ON=1 on reset per datasheet */\n+    s->stat = 0;\n+    s->addr = 0;\n+    memset(s->ch, 0, sizeof(s->ch));\n+    for (int i = 0; i < PIC32MK_DMA_NCHANNELS; i++) {\n+        qemu_irq_lower(s->irq[i]);\n+    }\n+}\n+\n+static void pic32mk_dma_init(Object *obj)\n+{\n+    PIC32MKDMAState *s = PIC32MK_DMA(obj);\n+\n+    memory_region_init_io(&s->mr, obj, &dma_ops, s,\n+                          TYPE_PIC32MK_DMA, DMA_MMIO_SIZE);\n+    sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->mr);\n+\n+    for (int i = 0; i < PIC32MK_DMA_NCHANNELS; i++) {\n+        sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq[i]);\n+    }\n+}\n+\n+static void pic32mk_dma_class_init(ObjectClass *klass, const void *data)\n+{\n+    DeviceClass *dc = DEVICE_CLASS(klass);\n+    device_class_set_legacy_reset(dc, pic32mk_dma_reset);\n+}\n+\n+static const TypeInfo pic32mk_dma_info = {\n+    .name          = TYPE_PIC32MK_DMA,\n+    .parent        = TYPE_SYS_BUS_DEVICE,\n+    .instance_size = sizeof(PIC32MKDMAState),\n+    .instance_init = pic32mk_dma_init,\n+    .class_init    = pic32mk_dma_class_init,\n+};\n+\n+static void pic32mk_dma_register_types(void)\n+{\n+    type_register_static(&pic32mk_dma_info);\n+}\n+\n+type_init(pic32mk_dma_register_types)\ndiff --git a/hw/mips/pic32mk_evic.c b/hw/mips/pic32mk_evic.c\nnew file mode 100644\nindex 0000000000..214fe7dd96\n--- /dev/null\n+++ b/hw/mips/pic32mk_evic.c\n@@ -0,0 +1,399 @@\n+/*\n+ * PIC32MK Enhanced Vectored Interrupt Controller (EVIC)\n+ * Datasheet: DS60001519E, §8 (pp. 121–166)\n+ *\n+ * 216 interrupt sources, 7 priority levels × 4 subpriority levels.\n+ * Supports single-vector (INTCON.MVEC=0) and multi-vector (MVEC=1) modes.\n+ * All register banks support atomic SET/CLR/INV sub-registers (+4/+8/+C).\n+ *\n+ * Copyright (c) 2026 QEMU contributors\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+#include \"qemu/log.h\"\n+#include \"qemu/module.h\"\n+#include \"hw/mips/pic32mk_evic.h\"\n+\n+/*\n+ * EVIC MMIO layout (offsets from EVIC base 0x1F810000)\n+ *\n+ * Registers that have SET/CLR/INV sub-regs use a 16-byte stride:\n+ *   REG at base+0, SET at base+4, CLR at base+8, INV at base+C\n+ *\n+ * OFFx registers are plain 4-byte (no SET/CLR/INV).\n+ * -----------------------------------------------------------------------\n+ */\n+\n+#define EVIC_MMIO_SIZE      0x1000u  /* 4 KB page */\n+\n+/* Number of IFS/IEC words (8 × 32 = 256 bits covers 216 sources + spare) */\n+#define EVIC_IFS_COUNT      8\n+#define EVIC_IEC_COUNT      8\n+#define EVIC_IPC_COUNT      64\n+#define EVIC_OFF_COUNT      191\n+\n+/* INTCON bits */\n+#define INTCON_MVEC         (1u << 12)   /* multi-vector mode */\n+\n+/*\n+ * Priority extraction helpers\n+ * IPC register format: 4 sources per register, 8 bits per source\n+ *   bits[4:2] = IP (priority, 3 bits)\n+ *   bits[1:0] = IS (subpriority, 2 bits)\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static int evic_get_priority(PIC32MKEVICState *s, int src)\n+{\n+    int reg   = src >> 2;\n+    int shift = (src & 3) << 3;   /* 0, 8, 16, or 24 */\n+    return (s->ipcreg[reg] >> (shift + 2)) & 0x7;\n+}\n+\n+/*\n+ * IRQ routing — called whenever IFS, IEC, or IPC registers change.\n+ *\n+ * Algorithm: find the highest pending+enabled priority level and assert\n+ * the matching CPU interrupt pin.  Deassert all pins that have no\n+ * pending work at that priority.\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void evic_update(PIC32MKEVICState *s)\n+{\n+    int best_prio = 0;\n+    int best_src  = -1;\n+\n+    for (int i = 0; i < PIC32MK_NUM_IRQ_SOURCES; i++) {\n+        int word = i >> 5;\n+        int bit  = i & 31;\n+        if ((s->ifsreg[word] >> bit & 1) && (s->iecreg[word] >> bit & 1)) {\n+            int prio = evic_get_priority(s, i);\n+            if (prio > best_prio) {\n+                best_prio = prio;\n+                best_src  = i;\n+            }\n+        }\n+    }\n+\n+    /* Update INTSTAT: bits[7:0] = last interrupt source, bits[15:8] = priority */\n+    if (best_src >= 0) {\n+        s->intstat = ((uint32_t)best_prio << 8) | (uint32_t)(best_src & 0xFF);\n+    }\n+\n+    /*\n+     * Assert/deassert each CPU interrupt pin.\n+     *\n+     * VEIC RIPL encoding: portSAVE_CONTEXT does:\n+     *   k0 = Cause >> 10          (extracts bits[17:10] as RIPL)\n+     *   Status[16:10] = k0[6:0]   (sets IPL = RIPL in ISR)\n+     *\n+     * cpu_mips_irq_request(cpu, N, 1) sets Cause.bit(N+8).\n+     * For portSAVE_CONTEXT to extract RIPL = priority P:\n+     *   need Cause.bit(10+P) set, so Cause >> 10 = 1 << (P-1)... but actually\n+     *   we want (Cause >> 10) to equal P so Status[16:10] = P = IPL.\n+     *   Cause.bit(10+P) >> 10 = 1 << P, not P itself.\n+     *\n+     * Correct approach: priority P → cpu_irq[P+1] → Cause.bit(P+9).\n+     *   Cause.bit(P+9) >> 10 = 1 << (P-1) (not P).\n+     *\n+     * Simplest correct mapping: encode priority as a single bit position.\n+     * cpu_irq[2] → Cause.bit(10) → k0 = 1 after >>10 → IPL=1 → blocks\n+     * same-priority re-entry (pending=0x0400 > status=0x0400 = FALSE).\n+     * portDISABLE with IPL=3 → status=3<<10=0x0C00 → blocks prio1(0x0400)\n+     * and prio2(0x0800) but not prio3(0x1000).  For our firmware only\n+     * priority 1 is used, so this is sufficient.\n+     *\n+     * Mapping: EVIC priority pin → cpu_irq[pin+1].\n+     *   pin=1 → cpu_irq[2] → Cause.bit(10) → RIPL = Cause>>10 = 1 ✓\n+     *   pin=2 → cpu_irq[3] → Cause.bit(11) → RIPL = 2 ✓\n+     *   pin=3 → cpu_irq[4] → Cause.bit(12) → RIPL = 4 (power-of-2, not equal)\n+     *   ...\n+     * This works perfectly for priority 1 and FreeRTOS configKERNEL_INTERRUPT_PRIORITY=1.\n+     */\n+    for (int pin = 1; pin <= 6; pin++) {\n+        int cpu_pin = pin + 1;  /* priority P → cpu_irq[P+1] for correct RIPL */\n+        int assert = 0;\n+        if (s->cpu_irq[cpu_pin]) {\n+            for (int i = 0; i < PIC32MK_NUM_IRQ_SOURCES && !assert; i++) {\n+                int word = i >> 5, bit = i & 31;\n+                if ((s->ifsreg[word] >> bit & 1) &&\n+                    (s->iecreg[word] >> bit & 1) &&\n+                    evic_get_priority(s, i) == pin) {\n+                    assert = 1;\n+                }\n+            }\n+            qemu_set_irq(s->cpu_irq[cpu_pin], assert);\n+        }\n+    }\n+    /* Priority 7 has no available cpu_irq slot (would need irq[8]); unused. */\n+}\n+\n+/*\n+ * IRQ input handler — called when a peripheral asserts/deasserts its IRQ.\n+ *\n+ * On the real device, raising the IRQ sets the corresponding IFSx bit.\n+ * Lowering it does NOT clear IFSx — that is software's responsibility\n+ * (write to IFSxCLR in the interrupt service routine).\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void evic_set_irq(void *opaque, int irq, int level)\n+{\n+    PIC32MKEVICState *s = opaque;\n+\n+    if (irq < 0 || irq >= PIC32MK_NUM_IRQ_SOURCES) {\n+        return;\n+    }\n+\n+    int word = irq >> 5;\n+    uint32_t mask = 1u << (irq & 31);\n+\n+    /* Track the current hardware level of this source */\n+\n+    if (level) {\n+        s->irq_level[word] |= mask;\n+        s->ifsreg[word]    |= mask;\n+    } else {\n+        s->irq_level[word] &= ~mask;\n+    }\n+\n+    if (level) {\n+        /*\n+         * Software interrupts CS0 (source 1) and CS1 (source 2) arrive here\n+         * because board code redirected env->irq[0..1] to EVIC inputs.\n+         * cpu_mips_store_cause() already set the direct Cause.IP0/IP1 bit\n+         * before calling us.  Clear it so the VEIC pending-vs-status\n+         * comparison only sees the priority pin asserted by evic_update().\n+         * This prevents same-priority nesting in VEIC mode.\n+         *\n+         * Also clear irq_level for these sources — they are edge/pulse\n+         * triggered (software writes to Cause), not level-sensitive like\n+         * hardware peripherals.  Without this, IFS re-assertion after\n+         * firmware IFSxCLR would immediately re-set the flag.\n+         */\n+        if (s->cpu && (irq == PIC32MK_IRQ_CS0 || irq == PIC32MK_IRQ_CS1)) {\n+            CPUMIPSState *env = &s->cpu->env;\n+            int ip_bit = (irq == PIC32MK_IRQ_CS0) ? 0 : 1;\n+            env->CP0_Cause &= ~(1u << (ip_bit + 8));\n+            /* Treat as one-shot: IFS is set above, but don't persist level */\n+            s->irq_level[word] &= ~mask;\n+        }\n+    }\n+\n+    evic_update(s);\n+}\n+\n+/*\n+ * Register pointer helper for SET/CLR/INV registers (base addresses only)\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static uint32_t *evic_find_reg(PIC32MKEVICState *s, hwaddr base)\n+{\n+    if (base == PIC32MK_EVIC_INTCON) {\n+        return &s->intcon;\n+    }\n+    if (base == PIC32MK_EVIC_PRISS) {\n+        return &s->priss;\n+    }\n+    if (base == PIC32MK_EVIC_INTSTAT) {\n+        return &s->intstat;\n+    }\n+    if (base == PIC32MK_EVIC_IPTMR) {\n+        return &s->iptmr;\n+    }\n+\n+    if (base >= PIC32MK_EVIC_IFS0 && base < PIC32MK_EVIC_IEC0) {\n+        int i = (int)((base - PIC32MK_EVIC_IFS0) >> 4);\n+        return (i < EVIC_IFS_COUNT) ? &s->ifsreg[i] : NULL;\n+    }\n+    if (base >= PIC32MK_EVIC_IEC0 && base < PIC32MK_EVIC_IPC0) {\n+        int i = (int)((base - PIC32MK_EVIC_IEC0) >> 4);\n+        return (i < EVIC_IEC_COUNT) ? &s->iecreg[i] : NULL;\n+    }\n+    if (base >= PIC32MK_EVIC_IPC0 && base < PIC32MK_EVIC_OFF0) {\n+        int i = (int)((base - PIC32MK_EVIC_IPC0) >> 4);\n+        return (i < EVIC_IPC_COUNT) ? &s->ipcreg[i] : NULL;\n+    }\n+    return NULL;\n+}\n+\n+/* Apply SET/CLR/INV operation (sub = byte offset within 16-byte group) */\n+/* PIC32MK: base+0=REG, +4=CLR, +8=SET, +0xC=INV */\n+static void apply_sci(uint32_t *reg, uint32_t val, int sub)\n+{\n+    switch (sub) {\n+    case 0:\n+        *reg  = val;\n+        break;\n+    case 4:\n+        *reg &= ~val;\n+        break;\n+    case 8:\n+        *reg |= val;\n+        break;\n+    case 12:\n+        *reg ^= val;\n+        break;\n+    }\n+}\n+\n+/*\n+ * MMIO read/write\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static uint64_t evic_read(void *opaque, hwaddr addr, unsigned size)\n+{\n+    PIC32MKEVICState *s = opaque;\n+\n+    /* OFFx: plain 4-byte registers, no SET/CLR/INV */\n+    if (addr >= PIC32MK_EVIC_OFF0) {\n+        int idx = (int)((addr - PIC32MK_EVIC_OFF0) >> 2);\n+        if (idx < EVIC_OFF_COUNT) {\n+            return s->offreg[idx];\n+        }\n+    } else {\n+        /* All other registers: 16-byte stride, all sub-addrs read base */\n+        hwaddr base = addr & ~(hwaddr)0xF;\n+        uint32_t *reg = evic_find_reg(s, base);\n+        if (reg) {\n+            return *reg;\n+        }\n+    }\n+\n+    qemu_log_mask(LOG_UNIMP,\n+                  \"pic32mk_evic: unimplemented read @ 0x%04\" HWADDR_PRIx \"\\n\",\n+                  addr);\n+    return 0;\n+}\n+\n+static void evic_write(void *opaque, hwaddr addr, uint64_t val, unsigned size)\n+{\n+    PIC32MKEVICState *s = opaque;\n+    bool need_update = false;\n+\n+    /* OFFx: plain 4-byte writes */\n+    if (addr >= PIC32MK_EVIC_OFF0) {\n+        int idx = (int)((addr - PIC32MK_EVIC_OFF0) >> 2);\n+        if (idx < EVIC_OFF_COUNT) {\n+            s->offreg[idx] = (uint32_t)val;\n+        }\n+        return;\n+    }\n+\n+    int sub       = (int)(addr & 0xF);\n+    hwaddr base   = addr & ~(hwaddr)0xF;\n+    uint32_t *reg = evic_find_reg(s, base);\n+\n+    if (!reg) {\n+        qemu_log_mask(LOG_UNIMP,\n+                      \"pic32mk_evic: unimplemented write @ 0x%04\"\n+                      HWADDR_PRIx \" = 0x%08\" PRIx64 \"\\n\",\n+                      addr, val);\n+        return;\n+    }\n+\n+    /* INTSTAT is read-only hardware status */\n+    if (reg == &s->intstat) {\n+        return;\n+    }\n+\n+    apply_sci(reg, (uint32_t)val, sub);\n+\n+    /*\n+     * After any IFS write (especially CLR), re-assert bits where the\n+     * hardware source is still active.  On real Microchip devices, IFS\n+     * flags are level-sensitive: if the peripheral still drives the\n+     * interrupt line high, the flag is immediately re-set even after the\n+     * firmware clears it.\n+     */\n+    if (base >= PIC32MK_EVIC_IFS0 && base < PIC32MK_EVIC_IEC0) {\n+        int idx = (int)((base - PIC32MK_EVIC_IFS0) >> 4);\n+        if (idx < EVIC_IFS_COUNT) {\n+            s->ifsreg[idx] |= s->irq_level[idx];\n+        }\n+    }\n+\n+    /* IFS or IEC or IPC write → re-evaluate interrupt routing */\n+    if (base >= PIC32MK_EVIC_IFS0 &&\n+        base <  PIC32MK_EVIC_IPC0 + (hwaddr)(EVIC_IPC_COUNT * 0x10)) {\n+        need_update = true;\n+    }\n+\n+    if (need_update) {\n+        evic_update(s);\n+    }\n+}\n+\n+static const MemoryRegionOps evic_ops = {\n+    .read       = evic_read,\n+    .write      = evic_write,\n+    .endianness = DEVICE_LITTLE_ENDIAN,\n+    .valid = {\n+        .min_access_size = 4,\n+        .max_access_size = 4,\n+    },\n+};\n+\n+/*\n+ * Device lifecycle\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void pic32mk_evic_reset(DeviceState *dev)\n+{\n+    PIC32MKEVICState *s = PIC32MK_EVIC(dev);\n+\n+    s->intcon  = 0;\n+    s->priss   = 0;\n+    s->intstat = 0;\n+    s->iptmr   = 0;\n+    memset(s->ifsreg, 0, sizeof(s->ifsreg));\n+    memset(s->iecreg, 0, sizeof(s->iecreg));\n+    memset(s->ipcreg, 0, sizeof(s->ipcreg));\n+    memset(s->offreg, 0, sizeof(s->offreg));\n+    memset(s->irq_level, 0, sizeof(s->irq_level));\n+\n+    /* Deassert all CPU interrupt pins */\n+    for (int i = 0; i < 8; i++) {\n+        if (s->cpu_irq[i]) {\n+            qemu_irq_lower(s->cpu_irq[i]);\n+        }\n+    }\n+}\n+\n+static void pic32mk_evic_init(Object *obj)\n+{\n+    PIC32MKEVICState *s = PIC32MK_EVIC(obj);\n+\n+    memory_region_init_io(&s->mr, obj, &evic_ops, s,\n+                          TYPE_PIC32MK_EVIC, EVIC_MMIO_SIZE);\n+    sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->mr);\n+\n+    /* 216 GPIO input lines — one per interrupt source */\n+    qdev_init_gpio_in(DEVICE(obj), evic_set_irq, PIC32MK_NUM_IRQ_SOURCES);\n+}\n+\n+static void pic32mk_evic_class_init(ObjectClass *klass, const void *data)\n+{\n+    DeviceClass *dc = DEVICE_CLASS(klass);\n+    device_class_set_legacy_reset(dc, pic32mk_evic_reset);\n+}\n+\n+static const TypeInfo pic32mk_evic_info = {\n+    .name          = TYPE_PIC32MK_EVIC,\n+    .parent        = TYPE_SYS_BUS_DEVICE,\n+    .instance_size = sizeof(PIC32MKEVICState),\n+    .instance_init = pic32mk_evic_init,\n+    .class_init    = pic32mk_evic_class_init,\n+};\n+\n+static void pic32mk_evic_register_types(void)\n+{\n+    type_register_static(&pic32mk_evic_info);\n+}\n+\n+type_init(pic32mk_evic_register_types)\ndiff --git a/hw/mips/pic32mk_gpio.c b/hw/mips/pic32mk_gpio.c\nnew file mode 100644\nindex 0000000000..ddbb6e6cd5\n--- /dev/null\n+++ b/hw/mips/pic32mk_gpio.c\n@@ -0,0 +1,418 @@\n+/*\n+ * PIC32MK GPIO — PORTA through PORTG (7 ports)\n+ * Datasheet: DS60001519E, §12\n+ *\n+ * Each port is a SysBusDevice that exposes a PIC32MK_GPIO_PORT_SIZE (0x100)\n+ * byte MMIO region containing ANSEL, TRIS, PORT, LAT, ODC, CN* registers.\n+ *\n+ * All registers support SET/CLR/INV sub-registers (+4/+8/+C).\n+ *\n+ * External input model:\n+ *   - 16 qdev GPIO input lines per port — driven by board wiring or test code.\n+ *   - 16 qdev GPIO output lines per port — assert when an output pin's LAT changes.\n+ *   - One IRQ output line → EVIC CN interrupt (fires when CNCON.ON and CNEN0\n+ *     enabled bits detect a PORT change).\n+ *\n+ * QOM properties (accessible via qom-get / qom-set from HMP/QMP):\n+ *   pin0..pin15 - bool, r/w - inject external input (write) or read PORT bit\n+ *   lat-state     — uint32, r  — current LAT register value\n+ *   port-state    — uint32, r  — current PORT register value (mix of ext & output)\n+ *   tris-state    — uint32, r  — current TRIS register value\n+ *\n+ * Chardev event stream (optional \"chardev\" qdev property):\n+ *   When connected, each state change emits a 13-byte little-endian message:\n+ *     [port_idx:1] [tris:4] [lat:4] [port:4]\n+ *   Enables event-driven host-side GPIO monitoring (gpio_tool.py gui).\n+ *\n+ * Copyright (c) 2026 QEMU contributors\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+#include \"qemu/log.h\"\n+#include \"qemu/module.h\"\n+#include \"qapi/error.h\"\n+#include \"qapi/visitor.h\"\n+#include \"hw/core/irq.h\"\n+#include \"hw/core/sysbus.h\"\n+#include \"hw/core/qdev-properties.h\"\n+#include \"chardev/char.h\"\n+#include \"hw/mips/pic32mk.h\"\n+#include \"qom/object.h\"\n+\n+/*\n+ * Device state — one instance per port (A–G)\n+ * -----------------------------------------------------------------------\n+ */\n+\n+#define TYPE_PIC32MK_GPIO   \"pic32mk-gpio\"\n+OBJECT_DECLARE_SIMPLE_TYPE(PIC32MKGpioState, PIC32MK_GPIO)\n+\n+struct PIC32MKGpioState {\n+    SysBusDevice parent_obj;\n+    MemoryRegion mr;\n+\n+    /* SFR registers */\n+    uint32_t ansel;\n+    uint32_t tris;    /* 1 = input (reset default) */\n+    uint32_t port;    /* pin state: input bits from ext_input, output bits from lat */\n+    uint32_t lat;     /* output latch */\n+    uint32_t odc;\n+    uint32_t cnpu;\n+    uint32_t cnpd;\n+    uint32_t cncon;\n+    uint32_t cnen0;\n+    uint32_t cnstat;\n+    uint32_t cnen1;\n+    uint32_t cnf;\n+\n+    /* External input pin levels (driven by qdev GPIO input lines or QOM set) */\n+    uint32_t ext_input;\n+\n+    /* CN reference: last PORT value read by firmware — used for mismatch detect */\n+    uint32_t cn_ref;\n+\n+    /* IRQ output → EVIC CN interrupt */\n+    qemu_irq cn_irq;\n+\n+    /* qdev GPIO output lines (16 per port): asserted when output LAT changes */\n+    qemu_irq output[16];\n+\n+    /*\n+     * Optional shared chardev for streaming state-change events to host tools.\n+     * Multiple GPIO ports write to the same Chardev* (not CharFrontend,\n+     * which enforces 1:1 ownership). Set by board init if chardev exists.\n+      */\n+    Chardev *chr_out;\n+    uint8_t  port_idx;   /* 0=A, 1=B, …, 6=G */\n+};\n+\n+/*\n+ * MMIO helpers\n+ * -----------------------------------------------------------------------\n+ */\n+\n+/* PIC32MK: base+0=REG, +4=CLR, +8=SET, +0xC=INV */\n+static void apply_sci(uint32_t *reg, uint32_t val, int sub)\n+{\n+    switch (sub) {\n+    case 0:\n+        *reg  = val;\n+        break;\n+    case 4:\n+        *reg &= ~val;\n+        break;\n+    case 8:\n+        *reg |= val;\n+        break;\n+    case 12:\n+        *reg ^= val;\n+        break;\n+    }\n+}\n+\n+static uint32_t *gpio_find_reg(PIC32MKGpioState *s, hwaddr base)\n+{\n+    switch (base) {\n+    case PIC32MK_ANSEL:\n+        return &s->ansel;\n+    case PIC32MK_TRIS:\n+        return &s->tris;\n+    case PIC32MK_PORT:\n+        return &s->port;\n+    case PIC32MK_LAT:\n+        return &s->lat;\n+    case PIC32MK_ODC:\n+        return &s->odc;\n+    case PIC32MK_CNPU:\n+        return &s->cnpu;\n+    case PIC32MK_CNPD:\n+        return &s->cnpd;\n+    case PIC32MK_CNCON:\n+        return &s->cncon;\n+    case PIC32MK_CNEN0:\n+        return &s->cnen0;\n+    case PIC32MK_CNSTAT:\n+        return &s->cnstat;\n+    case PIC32MK_CNEN1:\n+        return &s->cnen1;\n+    case PIC32MK_CNF:\n+        return &s->cnf;\n+    default:\n+        return NULL;\n+    }\n+}\n+\n+/* Recompute s->port from current lat, tris, and ext_input. */\n+static void gpio_update_port(PIC32MKGpioState *s)\n+{\n+    /* Input pins reflect external levels; output pins reflect LAT. */\n+    s->port = (s->ext_input & s->tris) | (s->lat & ~s->tris);\n+}\n+\n+/*\n+ * Emit a 13-byte state-change event on the optional chardev.\n+ * Format (little-endian): [port_idx:1] [tris:4] [lat:4] [port:4]\n+ */\n+static void gpio_notify_state(PIC32MKGpioState *s)\n+{\n+    if (!s->chr_out) {\n+        return;\n+    }\n+    uint8_t msg[13];\n+    msg[0] = s->port_idx;\n+    memcpy(&msg[1],  &s->tris, 4);\n+    memcpy(&msg[5],  &s->lat,  4);\n+    memcpy(&msg[9],  &s->port, 4);\n+    qemu_chr_write_all(s->chr_out, msg, sizeof(msg));\n+}\n+\n+/* Drive the 16 qdev output GPIO lines to match current LAT (output pins only). */\n+static void gpio_drive_outputs(PIC32MKGpioState *s)\n+{\n+    uint32_t out = s->lat & ~s->tris;\n+    for (int i = 0; i < 16; i++) {\n+        qemu_set_irq(s->output[i], (out >> i) & 1);\n+    }\n+}\n+\n+/*\n+ * Evaluate CN interrupt.\n+ *\n+ * PIC32MK CN is mismatch-based: CNSTAT bits are set when the current PORT\n+ * value differs from the value captured at the last PORT read (cn_ref).\n+ * The interrupt line is held HIGH as long as any enabled mismatch exists;\n+ * reading PORT (in the ISR) updates cn_ref and clears the mismatch, which\n+ * deasserts the interrupt so the EVIC doesn't immediately re-set IFS.\n+ */\n+static void gpio_eval_cn(PIC32MKGpioState *s)\n+{\n+#define CNCON_ON  (1u << 15)\n+    if (!(s->cncon & CNCON_ON)) {\n+        return;\n+    }\n+    /* Mismatch between current port and the reference snapshot */\n+    uint32_t mismatch = (s->port ^ s->cn_ref) & s->cnen0;\n+    s->cnstat = mismatch;\n+\n+    if (mismatch) {\n+        qemu_set_irq(s->cn_irq, 1);   /* assert — EVIC sets IFS */\n+    } else {\n+        qemu_set_irq(s->cn_irq, 0);   /* deassert — no more mismatch */\n+    }\n+}\n+\n+/*\n+ * qdev GPIO input handler (external pin injection)\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void gpio_set_input(PIC32MKGpioState *s, int pin, int level)\n+{\n+    uint32_t mask = 1u << pin;\n+\n+    if (level) {\n+        s->ext_input |= mask;\n+    } else {\n+        s->ext_input &= ~mask;\n+    }\n+\n+    gpio_update_port(s);\n+    gpio_eval_cn(s);\n+    gpio_notify_state(s);\n+}\n+\n+static void gpio_set_input_line(void *opaque, int pin, int level)\n+{\n+    gpio_set_input(PIC32MK_GPIO(opaque), pin, level);\n+}\n+\n+/*\n+ * MMIO read/write\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static uint64_t gpio_read(void *opaque, hwaddr addr, unsigned size)\n+{\n+    PIC32MKGpioState *s = opaque;\n+    hwaddr base = addr & ~(hwaddr)0xF;\n+    uint32_t *reg = gpio_find_reg(s, base);\n+\n+    if (reg) {\n+        /*\n+         * Reading PORT captures the current pin state into cn_ref.\n+         * This clears the CN mismatch and deasserts the interrupt line,\n+         * just like real PIC32MK hardware.\n+         */\n+        if (base == PIC32MK_PORT) {\n+            s->cn_ref = s->port;\n+            gpio_eval_cn(s);    /* deassert if no longer mismatching */\n+        }\n+        return *reg;\n+    }\n+\n+    qemu_log_mask(LOG_UNIMP,\n+                  \"pic32mk_gpio: unimplemented read @ 0x%04\" HWADDR_PRIx \"\\n\",\n+                  addr);\n+    return 0;\n+}\n+\n+static void gpio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size)\n+{\n+    PIC32MKGpioState *s = opaque;\n+    int sub       = (int)(addr & 0xF);\n+    hwaddr base   = addr & ~(hwaddr)0xF;\n+    uint32_t *reg = gpio_find_reg(s, base);\n+\n+    if (!reg) {\n+        qemu_log_mask(LOG_UNIMP,\n+                      \"pic32mk_gpio: unimplemented write @ 0x%04\"\n+                      HWADDR_PRIx \" = 0x%08\" PRIx64 \"\\n\",\n+                      addr, val);\n+        return;\n+    }\n+\n+    apply_sci(reg, (uint32_t)val, sub);\n+\n+    /* Side-effects after write */\n+    if (base == PIC32MK_LAT || base == PIC32MK_TRIS) {\n+        /* Output pin levels or direction changed — update PORT and drive lines */\n+        gpio_update_port(s);\n+        gpio_drive_outputs(s);\n+        gpio_eval_cn(s);\n+        gpio_notify_state(s);\n+    } else if (base == PIC32MK_ANSEL) {\n+        gpio_notify_state(s);\n+    }\n+    if (base == PIC32MK_CNCON || base == PIC32MK_CNEN0 || base == PIC32MK_CNEN1) {\n+        gpio_eval_cn(s);\n+    }\n+}\n+\n+static const MemoryRegionOps gpio_ops = {\n+    .read       = gpio_read,\n+    .write      = gpio_write,\n+    .endianness = DEVICE_LITTLE_ENDIAN,\n+    .valid = {\n+        .min_access_size = 4,\n+        .max_access_size = 4,\n+    },\n+};\n+\n+/*\n+ * QOM properties — host access via qom-get / qom-set\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void gpio_prop_pin_get(Object *obj, Visitor *v, const char *name,\n+                              void *opaque, Error **errp)\n+{\n+    PIC32MKGpioState *s = PIC32MK_GPIO(obj);\n+    int pin = (int)(intptr_t)opaque;\n+    bool val = (s->port >> pin) & 1;\n+    visit_type_bool(v, name, &val, errp);\n+}\n+\n+static void gpio_prop_pin_set(Object *obj, Visitor *v, const char *name,\n+                              void *opaque, Error **errp)\n+{\n+    PIC32MKGpioState *s = PIC32MK_GPIO(obj);\n+    int pin = (int)(intptr_t)opaque;\n+    bool val;\n+\n+    if (!visit_type_bool(v, name, &val, errp)) {\n+        return;\n+    }\n+    gpio_set_input(s, pin, val ? 1 : 0);\n+}\n+\n+/*\n+ * Device lifecycle\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void pic32mk_gpio_reset(DeviceState *dev)\n+{\n+    PIC32MKGpioState *s = PIC32MK_GPIO(dev);\n+\n+    s->ansel     = 0xFFFF;   /* all pins analog on reset */\n+    s->tris      = 0xFFFF;   /* all pins input on reset */\n+    s->port      = 0;\n+    s->lat       = 0;\n+    s->odc       = 0;\n+    s->cnpu      = 0;\n+    s->cnpd      = 0;\n+    s->cncon     = 0;\n+    s->cnen0     = 0;\n+    s->cnstat    = 0;\n+    s->cnen1     = 0;\n+    s->cnf       = 0;\n+    s->ext_input = 0;\n+    s->cn_ref    = 0;\n+}\n+\n+static void pic32mk_gpio_init(Object *obj)\n+{\n+    PIC32MKGpioState *s = PIC32MK_GPIO(obj);\n+    DeviceState *dev = DEVICE(obj);\n+\n+    memory_region_init_io(&s->mr, obj, &gpio_ops, s,\n+                          TYPE_PIC32MK_GPIO, PIC32MK_GPIO_PORT_SIZE);\n+    sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->mr);\n+\n+    /* IRQ output → EVIC CN interrupt */\n+    sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->cn_irq);\n+\n+    /* 16 qdev GPIO input lines: board/test code drives external pin levels */\n+    qdev_init_gpio_in(dev, gpio_set_input_line, 16);\n+\n+    /* 16 qdev GPIO output lines: driven when LAT output pins change */\n+    qdev_init_gpio_out(dev, s->output, 16);\n+\n+    /* Per-pin bool QOM properties: pin0 … pin15 */\n+    for (int i = 0; i < 16; i++) {\n+        char name[8];\n+        snprintf(name, sizeof(name), \"pin%d\", i);\n+        object_property_add(obj, name, \"bool\",\n+                            gpio_prop_pin_get,\n+                            gpio_prop_pin_set,\n+                            NULL, (void *)(intptr_t)i);\n+    }\n+\n+    /* Whole-port read-only convenience properties */\n+    object_property_add_uint32_ptr(obj, \"lat-state\",  &s->lat,  OBJ_PROP_FLAG_READ);\n+    object_property_add_uint32_ptr(obj, \"port-state\", &s->port, OBJ_PROP_FLAG_READ);\n+    object_property_add_uint32_ptr(obj, \"tris-state\", &s->tris, OBJ_PROP_FLAG_READ);\n+}\n+\n+void pic32mk_gpio_set_chardev(DeviceState *dev, Chardev *chr)\n+{\n+    PIC32MKGpioState *s = PIC32MK_GPIO(dev);\n+    s->chr_out = chr;\n+}\n+\n+static const Property pic32mk_gpio_props[] = {\n+    DEFINE_PROP_UINT8(\"port-index\", PIC32MKGpioState, port_idx, 0),\n+};\n+\n+static void pic32mk_gpio_class_init(ObjectClass *klass, const void *data)\n+{\n+    DeviceClass *dc = DEVICE_CLASS(klass);\n+    device_class_set_legacy_reset(dc, pic32mk_gpio_reset);\n+    device_class_set_props(dc, pic32mk_gpio_props);\n+}\n+\n+static const TypeInfo pic32mk_gpio_info = {\n+    .name          = TYPE_PIC32MK_GPIO,\n+    .parent        = TYPE_SYS_BUS_DEVICE,\n+    .instance_size = sizeof(PIC32MKGpioState),\n+    .instance_init = pic32mk_gpio_init,\n+    .class_init    = pic32mk_gpio_class_init,\n+};\n+\n+static void pic32mk_gpio_register_types(void)\n+{\n+    type_register_static(&pic32mk_gpio_info);\n+}\n+\n+type_init(pic32mk_gpio_register_types)\ndiff --git a/hw/mips/pic32mk_i2c.c b/hw/mips/pic32mk_i2c.c\nnew file mode 100644\nindex 0000000000..b8c7220d92\n--- /dev/null\n+++ b/hw/mips/pic32mk_i2c.c\n@@ -0,0 +1,184 @@\n+/*\n+ * PIC32MK I2C × 4 (I2C1–I2C4)\n+ * Datasheet: DS60001519E, §24\n+ *\n+ * Each I2C instance is a SysBusDevice that stubs the register file and\n+ * attaches a QEMU i2c_bus for downstream device models.\n+ * Master-mode state machine is Phase 2B; firmware init (I2CxCON writes)\n+ * succeeds silently.\n+ *\n+ * Copyright (c) 2026 QEMU contributors\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+#include \"qemu/log.h\"\n+#include \"qemu/module.h\"\n+#include \"hw/core/sysbus.h\"\n+#include \"hw/core/irq.h\"\n+#include \"hw/i2c/i2c.h\"\n+#include \"hw/mips/pic32mk.h\"\n+\n+#define TYPE_PIC32MK_I2C    \"pic32mk-i2c\"\n+OBJECT_DECLARE_SIMPLE_TYPE(PIC32MKI2CState, PIC32MK_I2C)\n+\n+struct PIC32MKI2CState {\n+    SysBusDevice parent_obj;\n+    MemoryRegion mr;\n+\n+    uint32_t con;   /* I2CxCON */\n+    uint32_t stat;  /* I2CxSTAT */\n+    uint32_t add;   /* I2CxADD */\n+    uint32_t msk;   /* I2CxMSK */\n+    uint32_t trn;   /* I2CxTRN — TX data */\n+    uint32_t rcv;   /* I2CxRCV — RX data (read-only) */\n+\n+    I2CBus  *bus;\n+    qemu_irq irq_master;   /* master interrupt */\n+    qemu_irq irq_slave;    /* slave interrupt */\n+    qemu_irq irq_bus;      /* bus-collision interrupt */\n+};\n+\n+/* I2CxSTAT bits */\n+#define I2C_STAT_TRSTAT     (1u << 14)  /* transmit status */\n+#define I2C_STAT_ACKSTAT    (1u << 15)  /* ACK status */\n+\n+/* PIC32MK: base+0=REG, +4=CLR, +8=SET, +0xC=INV */\n+static void apply_sci(uint32_t *reg, uint32_t val, int sub)\n+{\n+    switch (sub) {\n+    case 0:\n+        *reg  = val;\n+        break;\n+    case 4:\n+        *reg &= ~val;\n+        break;\n+    case 8:\n+        *reg |= val;\n+        break;\n+    case 12:\n+        *reg ^= val;\n+        break;\n+    }\n+}\n+\n+static uint32_t *i2c_find_reg(PIC32MKI2CState *s, hwaddr base)\n+{\n+    switch (base) {\n+    case PIC32MK_I2CxCON:\n+        return &s->con;\n+    case PIC32MK_I2CxSTAT:\n+        return &s->stat;\n+    case PIC32MK_I2CxADD:\n+        return &s->add;\n+    case PIC32MK_I2CxMSK:\n+        return &s->msk;\n+    case PIC32MK_I2CxTRN:\n+        return &s->trn;\n+    case PIC32MK_I2CxRCV:\n+        return &s->rcv;\n+    default:\n+        return NULL;\n+    }\n+}\n+\n+static uint64_t i2c_read(void *opaque, hwaddr addr, unsigned size)\n+{\n+    PIC32MKI2CState *s = opaque;\n+    hwaddr base = addr & ~(hwaddr)0xF;\n+    uint32_t *reg = i2c_find_reg(s, base);\n+\n+    if (reg) {\n+        return *reg;\n+    }\n+\n+    qemu_log_mask(LOG_UNIMP,\n+                  \"pic32mk_i2c: unimplemented read @ 0x%04\" HWADDR_PRIx \"\\n\",\n+                  addr);\n+    return 0;\n+}\n+\n+static void i2c_write(void *opaque, hwaddr addr, uint64_t val, unsigned size)\n+{\n+    PIC32MKI2CState *s = opaque;\n+    int sub     = (int)(addr & 0xF);\n+    hwaddr base = addr & ~(hwaddr)0xF;\n+    uint32_t *reg = i2c_find_reg(s, base);\n+\n+    if (!reg) {\n+        qemu_log_mask(LOG_UNIMP,\n+                      \"pic32mk_i2c: unimplemented write @ 0x%04\"\n+                      HWADDR_PRIx \" = 0x%08\" PRIx64 \"\\n\",\n+                      addr, val);\n+        return;\n+    }\n+\n+    /* TRN write: transmit byte — stub logs but does not transfer */\n+    if (base == PIC32MK_I2CxTRN) {\n+        s->trn = (uint32_t)val;\n+        qemu_log_mask(LOG_UNIMP,\n+                      \"pic32mk_i2c: TX write 0x%02x (I2C master not implemented)\\n\",\n+                      (uint32_t)val & 0xFF);\n+        return;\n+    }\n+\n+    /* STAT is partially read-only; accept writes for CLR operations */\n+    apply_sci(reg, (uint32_t)val, sub);\n+}\n+\n+static const MemoryRegionOps i2c_ops = {\n+    .read       = i2c_read,\n+    .write      = i2c_write,\n+    .endianness = DEVICE_LITTLE_ENDIAN,\n+    .valid = { .min_access_size = 4, .max_access_size = 4 },\n+};\n+\n+static void pic32mk_i2c_reset(DeviceState *dev)\n+{\n+    PIC32MKI2CState *s = PIC32MK_I2C(dev);\n+    s->con  = 0;\n+    s->stat = 0;\n+    s->add  = 0;\n+    s->msk  = 0;\n+    s->trn  = 0xFF;\n+    s->rcv  = 0;\n+    qemu_irq_lower(s->irq_master);\n+    qemu_irq_lower(s->irq_slave);\n+    qemu_irq_lower(s->irq_bus);\n+}\n+\n+static void pic32mk_i2c_init(Object *obj)\n+{\n+    PIC32MKI2CState *s = PIC32MK_I2C(obj);\n+\n+    memory_region_init_io(&s->mr, obj, &i2c_ops, s,\n+                          TYPE_PIC32MK_I2C, PIC32MK_I2C_BLOCK_SIZE);\n+    sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->mr);\n+\n+    sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq_master);  /* index 0 */\n+    sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq_slave);   /* index 1 */\n+    sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq_bus);     /* index 2 */\n+\n+    s->bus = i2c_init_bus(DEVICE(obj), \"i2c\");\n+}\n+\n+static void pic32mk_i2c_class_init(ObjectClass *klass, const void *data)\n+{\n+    DeviceClass *dc = DEVICE_CLASS(klass);\n+    device_class_set_legacy_reset(dc, pic32mk_i2c_reset);\n+}\n+\n+static const TypeInfo pic32mk_i2c_info = {\n+    .name          = TYPE_PIC32MK_I2C,\n+    .parent        = TYPE_SYS_BUS_DEVICE,\n+    .instance_size = sizeof(PIC32MKI2CState),\n+    .instance_init = pic32mk_i2c_init,\n+    .class_init    = pic32mk_i2c_class_init,\n+};\n+\n+static void pic32mk_i2c_register_types(void)\n+{\n+    type_register_static(&pic32mk_i2c_info);\n+}\n+\n+type_init(pic32mk_i2c_register_types)\ndiff --git a/hw/mips/pic32mk_ic.c b/hw/mips/pic32mk_ic.c\nnew file mode 100644\nindex 0000000000..5b709841fa\n--- /dev/null\n+++ b/hw/mips/pic32mk_ic.c\n@@ -0,0 +1,384 @@\n+/*\n+ * PIC32MK Input Capture × 16 (IC1–IC16)\n+ * Datasheet: DS60001519E, §18\n+ *\n+ * Emulation model: full register model with 4-entry FIFO.\n+ * Two IRQ outputs per instance: capture IRQ and overflow-error IRQ.\n+ *\n+ * Capture injection via optional chardev (\"ic-events\"):\n+ *   The chardev is receive-side: the host writes 8-byte packets to\n+ *   inject capture events into the FIFO.\n+ *\n+ *   Packet format (8 bytes, little-endian):\n+ *     [0]   index   — IC instance (1–16); packet ignored if != this instance\n+ *     [1]   flags   — bit0=error inject, bit1=hi16 valid (32-bit capture pair)\n+ *     [2–3] val_lo  — 16-bit capture value (low word), LE\n+ *     [4–5] val_hi  — 16-bit capture value (high word), LE (C32 mode only)\n+ *     [6–7] reserved\n+ *\n+ *   On inject: push val_lo to FIFO (and val_hi if flags.bit1 and C32=1),\n+ *   set ICBNE, assert capture IRQ (if ICI != 0).  If FIFO full: set ICOV,\n+ *   assert error IRQ.  Error-inject (flags.bit0=1): set ICOV, assert error IRQ.\n+ *\n+ * Register layout within each 0x200-byte block\n+ * (SET/CLR/INV sub-regs at +4/+8/+C relative to register base):\n+ *   ICxCON  +0x00  Control: ON, SIDL, FEDGE, C32, ICTMR, ICI, ICOV, ICBNE, ICM\n+ *   ICxBUF  +0x10  Capture buffer (read-only FIFO pop; write ignored)\n+ *\n+ * FIFO: 4 entries × 16-bit.  ICBNE cleared when FIFO empties.\n+ *       In C32 mode, two consecutive ICxBUF reads assemble one 32-bit value.\n+ *\n+ * Copyright (c) 2026 QEMU contributors\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+#include \"qemu/log.h\"\n+#include \"qemu/module.h\"\n+#include \"hw/core/sysbus.h\"\n+#include \"hw/core/qdev-properties.h\"\n+#include \"hw/core/qdev-properties-system.h\"\n+#include \"hw/core/irq.h\"\n+#include \"hw/mips/pic32mk.h\"\n+#include \"chardev/char.h\"\n+#include \"chardev/char-fe.h\"\n+\n+/*\n+ * ICM mode names (ICxCON bits 2:0)\n+ * -----------------------------------------------------------------------\n+ */\n+static const char *icm_mode_names[] = {\n+    [0] = \"Disabled\",\n+    [1] = \"Every edge\",\n+    [2] = \"Every 2nd rising edge\",\n+    [3] = \"Every 3rd rising edge\",\n+    [4] = \"Every 4th rising edge\",\n+    [5] = \"Every 5th rising edge (mode5)\",\n+    [6] = \"Every 6th rising edge (mode6)\",\n+    [7] = \"Every 7th rising edge (mode7)\",\n+};\n+\n+/*\n+ * Global instance table — lets IC1 (the sole chardev owner) dispatch\n+ * packets to any of the 16 IC instances by index.\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static struct PIC32MKICState *g_ic_instances[17]; /* index 1–16 */\n+\n+/*\n+ * Device state\n+ * -----------------------------------------------------------------------\n+ */\n+\n+#define TYPE_PIC32MK_IC  \"pic32mk-ic\"\n+OBJECT_DECLARE_SIMPLE_TYPE(PIC32MKICState, PIC32MK_IC)\n+\n+#define PIC32MK_IC_FIFO_DEPTH  4\n+\n+struct PIC32MKICState {\n+    SysBusDevice parent_obj;\n+    MemoryRegion mr;\n+\n+    uint32_t con;                          /* ICxCON */\n+    uint16_t fifo[PIC32MK_IC_FIFO_DEPTH]; /* capture FIFO */\n+    uint8_t  fifo_head;                    /* read pointer  */\n+    uint8_t  fifo_tail;                    /* write pointer */\n+    uint8_t  fifo_count;                   /* entries in use */\n+\n+    qemu_irq irq_capture;   /* normal capture IRQ → EVIC */\n+    qemu_irq irq_error;     /* overflow / error IRQ → EVIC */\n+    uint8_t  index;         /* 1–16 */\n+\n+    CharFrontend chr;       /* optional chardev for capture injection */\n+\n+    /* Partial-packet accumulator for chardev receive */\n+    uint8_t  pkt_buf[8];\n+    int      pkt_len;\n+};\n+\n+/*\n+ * FIFO helpers\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void ic_fifo_reset(PIC32MKICState *s)\n+{\n+    s->fifo_head  = 0;\n+    s->fifo_tail  = 0;\n+    s->fifo_count = 0;\n+    s->con &= ~(PIC32MK_ICCON_ICBNE | PIC32MK_ICCON_ICOV);\n+}\n+\n+static void ic_fifo_push(PIC32MKICState *s, uint16_t val)\n+{\n+    if (s->fifo_count == PIC32MK_IC_FIFO_DEPTH) {\n+        /* Overflow: set ICOV, pulse error IRQ */\n+        s->con |= PIC32MK_ICCON_ICOV;\n+        qemu_irq_pulse(s->irq_error);\n+        qemu_log_mask(LOG_GUEST_ERROR,\n+                      \"pic32mk_ic: IC%u FIFO overflow\\n\", s->index);\n+        return;\n+    }\n+    s->fifo[s->fifo_tail] = val;\n+    s->fifo_tail = (s->fifo_tail + 1) % PIC32MK_IC_FIFO_DEPTH;\n+    s->fifo_count++;\n+    s->con |= PIC32MK_ICCON_ICBNE;\n+}\n+\n+static uint16_t ic_fifo_pop(PIC32MKICState *s)\n+{\n+    if (s->fifo_count == 0) {\n+        return 0;\n+    }\n+    uint16_t val = s->fifo[s->fifo_head];\n+    s->fifo_head = (s->fifo_head + 1) % PIC32MK_IC_FIFO_DEPTH;\n+    s->fifo_count--;\n+    if (s->fifo_count == 0) {\n+        s->con &= ~PIC32MK_ICCON_ICBNE;\n+    }\n+    return val;\n+}\n+\n+/*\n+ * Capture injection — called from chardev receive handler\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void ic_inject_capture(PIC32MKICState *s, uint8_t flags,\n+                               uint16_t val_lo, uint16_t val_hi)\n+{\n+    if (!(s->con & PIC32MK_ICCON_ON)) {\n+        return;  /* IC not enabled — ignore */\n+    }\n+\n+    if (flags & 1) {\n+        /* Error inject: set ICOV, pulse error IRQ */\n+        s->con |= PIC32MK_ICCON_ICOV;\n+        qemu_irq_pulse(s->irq_error);\n+        return;\n+    }\n+\n+    ic_fifo_push(s, val_lo);\n+    if ((flags & 2) && (s->con & PIC32MK_ICCON_C32)) {\n+        ic_fifo_push(s, val_hi);\n+    }\n+\n+    /*\n+     * ICI<1:0>: 00=every 1st, 01=every 2nd, 10=every 3rd, 11=every 4th capture.\n+     * Simplified model: always fire on every capture (ICI threshold ignored).\n+      */\n+    if (s->fifo_count > 0) {\n+        qemu_irq_pulse(s->irq_capture);\n+    }\n+}\n+\n+/*\n+ * Chardev receive handler\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static int ic_chr_can_receive(void *opaque)\n+{\n+    /* Accept up to one full packet at a time */\n+    return 8;\n+}\n+\n+static void ic_chr_receive(void *opaque, const uint8_t *buf, int size)\n+{\n+    PIC32MKICState *s = opaque;\n+    int i = 0;\n+\n+    while (i < size) {\n+        int need = 8 - s->pkt_len;\n+        int avail = size - i;\n+        int copy = (avail < need) ? avail : need;\n+\n+        memcpy(s->pkt_buf + s->pkt_len, buf + i, copy);\n+        s->pkt_len += copy;\n+        i += copy;\n+\n+        if (s->pkt_len == 8) {\n+            uint8_t  inst    = s->pkt_buf[0];\n+            uint8_t  flags   = s->pkt_buf[1];\n+            uint16_t val_lo, val_hi;\n+            memcpy(&val_lo, &s->pkt_buf[2], 2);\n+            memcpy(&val_hi, &s->pkt_buf[4], 2);\n+            val_lo = le16_to_cpu(val_lo);\n+            val_hi = le16_to_cpu(val_hi);\n+            s->pkt_len = 0;\n+\n+            /* Route to target instance via global table */\n+            if (inst >= 1 && inst <= 16 && g_ic_instances[inst]) {\n+                ic_inject_capture(g_ic_instances[inst], flags, val_lo, val_hi);\n+            }\n+        }\n+    }\n+}\n+\n+/*\n+ * MMIO helpers\n+ * -----------------------------------------------------------------------\n+ */\n+\n+/* PIC32MK: base+0=REG, +4=CLR, +8=SET, +0xC=INV */\n+static void apply_sci(uint32_t *reg, uint32_t val, int sub)\n+{\n+    switch (sub) {\n+    case 0:\n+        *reg  = val;\n+        break;\n+    case 4:\n+        *reg &= ~val;\n+        break;\n+    case 8:\n+        *reg |= val;\n+        break;\n+    case 12:\n+        *reg ^= val;\n+        break;\n+    }\n+}\n+\n+/*\n+ * MMIO read / write\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static uint64_t ic_read(void *opaque, hwaddr addr, unsigned size)\n+{\n+    PIC32MKICState *s = opaque;\n+    hwaddr base = addr & ~(hwaddr)0xF;\n+\n+    switch (base) {\n+    case PIC32MK_ICxCON:\n+        return s->con;\n+    case PIC32MK_ICxBUF:\n+        return ic_fifo_pop(s);\n+    default:\n+        qemu_log_mask(LOG_UNIMP,\n+                      \"pic32mk_ic: IC%u unimplemented read @ 0x%04\"\n+                      HWADDR_PRIx \"\\n\", s->index, addr);\n+        return 0;\n+    }\n+}\n+\n+static void ic_write(void *opaque, hwaddr addr, uint64_t val, unsigned size)\n+{\n+    PIC32MKICState *s = opaque;\n+    int    sub  = (int)(addr & 0xF);\n+    hwaddr base = addr & ~(hwaddr)0xF;\n+\n+    switch (base) {\n+    case PIC32MK_ICxCON: {\n+        bool was_on = !!(s->con & PIC32MK_ICCON_ON);\n+        apply_sci(&s->con, (uint32_t)val, sub);\n+        bool now_on = !!(s->con & PIC32MK_ICCON_ON);\n+\n+        if (!was_on && now_on) {\n+            /* ON 0→1: reset FIFO, log enable */\n+            ic_fifo_reset(s);\n+            uint32_t icm  = (s->con & PIC32MK_ICCON_ICM_MASK);\n+            uint32_t ici  = (s->con & PIC32MK_ICCON_ICI_MASK) >> PIC32MK_ICCON_ICI_SHIFT;\n+            bool     c32  = !!(s->con & PIC32MK_ICCON_C32);\n+            bool     ictmr = !!(s->con & PIC32MK_ICCON_ICTMR);\n+            bool     fedge = !!(s->con & PIC32MK_ICCON_FEDGE);\n+            qemu_log(\"pic32mk_ic: IC%u enabled — mode=%s, ICI=%u, \"\n+                     \"C32=%u, ICTMR=%u, FEDGE=%u\\n\",\n+                     s->index, icm_mode_names[icm], ici, c32, ictmr, fedge);\n+        } else if (was_on && !now_on) {\n+            qemu_log(\"pic32mk_ic: IC%u disabled\\n\", s->index);\n+        }\n+        break;\n+    }\n+    case PIC32MK_ICxBUF:\n+        /* ICxBUF is read-only; writes are ignored */\n+        break;\n+    default:\n+        qemu_log_mask(LOG_UNIMP,\n+                      \"pic32mk_ic: IC%u unimplemented write @ 0x%04\"\n+                      HWADDR_PRIx \" = 0x%08\" PRIx64 \"\\n\",\n+                      s->index, addr, val);\n+        break;\n+    }\n+}\n+\n+static const MemoryRegionOps ic_ops = {\n+    .read       = ic_read,\n+    .write      = ic_write,\n+    .endianness = DEVICE_LITTLE_ENDIAN,\n+    .valid = {\n+        .min_access_size = 4,\n+        .max_access_size = 4,\n+    },\n+};\n+\n+/*\n+ * Device lifecycle\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void pic32mk_ic_reset(DeviceState *dev)\n+{\n+    PIC32MKICState *s = PIC32MK_IC(dev);\n+    s->con     = 0;\n+    s->pkt_len = 0;\n+    ic_fifo_reset(s);\n+}\n+\n+static void pic32mk_ic_realize(DeviceState *dev, Error **errp)\n+{\n+    PIC32MKICState *s = PIC32MK_IC(dev);\n+\n+    /* Register in global table so the chardev dispatcher can find us */\n+    if (s->index >= 1 && s->index <= 16) {\n+        g_ic_instances[s->index] = s;\n+    }\n+\n+    /* Only IC1 owns the chardev — it dispatches packets to all instances */\n+    if (qemu_chr_fe_backend_connected(&s->chr)) {\n+        qemu_chr_fe_set_handlers(&s->chr,\n+                                 ic_chr_can_receive,\n+                                 ic_chr_receive,\n+                                 NULL, NULL, s, NULL, true);\n+    }\n+}\n+\n+static void pic32mk_ic_init(Object *obj)\n+{\n+    PIC32MKICState *s = PIC32MK_IC(obj);\n+\n+    memory_region_init_io(&s->mr, obj, &ic_ops, s,\n+                          TYPE_PIC32MK_IC, PIC32MK_IC_BLOCK_SIZE);\n+    sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->mr);\n+    sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq_capture);\n+    sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq_error);\n+}\n+\n+static Property pic32mk_ic_properties[] = {\n+    DEFINE_PROP_UINT8(\"index\",   PIC32MKICState, index, 1),\n+    DEFINE_PROP_CHR(\"chardev\",   PIC32MKICState, chr),\n+};\n+\n+static void pic32mk_ic_class_init(ObjectClass *klass, const void *data)\n+{\n+    DeviceClass *dc = DEVICE_CLASS(klass);\n+    dc->realize = pic32mk_ic_realize;\n+    device_class_set_legacy_reset(dc, pic32mk_ic_reset);\n+    device_class_set_props(dc, pic32mk_ic_properties);\n+}\n+\n+static const TypeInfo pic32mk_ic_info = {\n+    .name          = TYPE_PIC32MK_IC,\n+    .parent        = TYPE_SYS_BUS_DEVICE,\n+    .instance_size = sizeof(PIC32MKICState),\n+    .instance_init = pic32mk_ic_init,\n+    .class_init    = pic32mk_ic_class_init,\n+};\n+\n+static void pic32mk_ic_register_types(void)\n+{\n+    type_register_static(&pic32mk_ic_info);\n+}\n+\n+type_init(pic32mk_ic_register_types)\ndiff --git a/hw/mips/pic32mk_nvm.c b/hw/mips/pic32mk_nvm.c\nnew file mode 100644\nindex 0000000000..c7538ee1cf\n--- /dev/null\n+++ b/hw/mips/pic32mk_nvm.c\n@@ -0,0 +1,572 @@\n+/*\n+ * PIC32MK NVM / Flash Controller emulation\n+ * Datasheet: DS60001519E, §10\n+ *\n+ * Mediates program/erase operations on the 1 MB Program Flash\n+ * (0x1D000000–0x1D0FFFFF) through the NVMCON register interface at\n+ * SFR offset 0x0A00 (virtual 0xBF800A00).\n+ *\n+ * The flash memory region must be created as RAM (not ROM) by the board\n+ * init code so that this device can write into it.  A QOM link property\n+ * \"pflash\" connects to the MemoryRegion backing the program flash.\n+ *\n+ * Copyright (c) 2026 QEMU contributors\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+#include \"qemu/log.h\"\n+#include \"qemu/module.h\"\n+#include \"qapi/error.h\"\n+#include \"hw/core/sysbus.h\"\n+#include \"hw/core/irq.h\"\n+#include \"hw/core/qdev-properties.h\"\n+#include \"hw/mips/pic32mk.h\"\n+#include \"system/dma.h\"\n+\n+#include <fcntl.h>\n+#include <unistd.h>\n+\n+#define TYPE_PIC32MK_NVM \"pic32mk-nvm\"\n+OBJECT_DECLARE_SIMPLE_TYPE(PIC32MKNVMState, PIC32MK_NVM)\n+\n+/* NVMKEY unlock FSM states */\n+#define NVMKEY_LOCKED       0\n+#define NVMKEY_KEY1_OK      1\n+#define NVMKEY_UNLOCKED     2\n+\n+struct PIC32MKNVMState {\n+    SysBusDevice parent_obj;\n+    MemoryRegion mr;\n+\n+    /* SFR registers */\n+    uint32_t nvmcon;\n+    uint32_t nvmaddr;\n+    uint32_t nvmdata[4];        /* NVMDATA0–3 */\n+    uint32_t nvmsrcaddr;\n+    uint32_t nvmpwp;\n+    uint32_t nvmbwp;\n+    uint32_t nvmcon2;\n+    int      nvmkey_state;      /* unlock FSM */\n+\n+    /* QOM link to program flash MemoryRegion (must be RAM-backed) */\n+    MemoryRegion *pflash_mr;\n+\n+    /* IRQ output → EVIC vector 31 (_FLASH_CONTROL_VECTOR) */\n+    qemu_irq irq;\n+\n+    /* Host backing file for flash persistence */\n+    char    *filename;          /* qdev string property */\n+    int      backing_fd;\n+};\n+\n+/*\n+ * SET/CLR/INV helper (PIC32MK convention: +0=REG, +4=CLR, +8=SET, +C=INV)\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void apply_sci(uint32_t *reg, uint32_t val, int sub)\n+{\n+    switch (sub) {\n+    case 0x0:\n+        *reg  = val;\n+        break;\n+    case 0x4:\n+        *reg &= ~val;\n+        break;\n+    case 0x8:\n+        *reg |= val;\n+        break;\n+    case 0xC:\n+        *reg ^= val;\n+        break;\n+    }\n+}\n+\n+/*\n+ * Flash access helpers\n+ * -----------------------------------------------------------------------\n+ */\n+\n+/*\n+ * Convert a physical NVMADDR value to an offset within the program flash\n+ * region.  Returns true on success, false if the address is out of range.\n+ */\n+static bool nvm_addr_to_offset(uint32_t phys_addr, uint32_t *offset)\n+{\n+    if (phys_addr >= PIC32MK_PFLASH_BASE &&\n+        phys_addr < PIC32MK_PFLASH_BASE + PIC32MK_PFLASH_SIZE) {\n+        *offset = phys_addr - PIC32MK_PFLASH_BASE;\n+        return true;\n+    }\n+    return false;\n+}\n+\n+static uint8_t *nvm_flash_ptr(PIC32MKNVMState *s)\n+{\n+    if (!s->pflash_mr) {\n+        return NULL;\n+    }\n+    return memory_region_get_ram_ptr(s->pflash_mr);\n+}\n+\n+/*\n+ * Backing file helpers\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void nvm_load_backing(PIC32MKNVMState *s)\n+{\n+    if (s->backing_fd < 0 || !s->pflash_mr) {\n+        return;\n+    }\n+    uint8_t *flash = nvm_flash_ptr(s);\n+    if (!flash) {\n+        return;\n+    }\n+    lseek(s->backing_fd, 0, SEEK_SET);\n+    ssize_t n = read(s->backing_fd, flash, PIC32MK_PFLASH_SIZE);\n+    if (n < 0) {\n+        qemu_log_mask(LOG_GUEST_ERROR,\n+                      \"pic32mk_nvm: backing file read error\\n\");\n+    } else if ((size_t)n < PIC32MK_PFLASH_SIZE) {\n+        /* Short read — pad with 0xFF (erased flash) */\n+        memset(flash + n, 0xFF, PIC32MK_PFLASH_SIZE - (size_t)n);\n+    }\n+}\n+\n+static void nvm_flush_backing(PIC32MKNVMState *s)\n+{\n+    if (s->backing_fd < 0 || !s->pflash_mr) {\n+        return;\n+    }\n+    uint8_t *flash = nvm_flash_ptr(s);\n+    if (!flash) {\n+        return;\n+    }\n+    lseek(s->backing_fd, 0, SEEK_SET);\n+    ssize_t n = write(s->backing_fd, flash, PIC32MK_PFLASH_SIZE);\n+    if (n < 0) {\n+        qemu_log_mask(LOG_GUEST_ERROR,\n+                      \"pic32mk_nvm: backing file write error\\n\");\n+    }\n+    fdatasync(s->backing_fd);\n+}\n+\n+/*\n+ * Command execution — triggered when WR transitions 0 → 1\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void nvm_execute_cmd(PIC32MKNVMState *s)\n+{\n+    uint32_t op = s->nvmcon & PIC32MK_NVMCON_NVMOP_MASK;\n+    uint32_t offset;\n+    uint8_t *flash;\n+\n+    /* Clear previous errors */\n+    s->nvmcon &= ~(PIC32MK_NVMCON_WRERR | PIC32MK_NVMCON_LVDERR);\n+\n+    /* All write/erase operations require WREN + unlock */\n+    if (op != PIC32MK_NVMOP_NOP) {\n+        if (!(s->nvmcon & PIC32MK_NVMCON_WREN)) {\n+            qemu_log_mask(LOG_GUEST_ERROR,\n+                          \"pic32mk_nvm: operation 0x%x without WREN\\n\", op);\n+            s->nvmcon |= PIC32MK_NVMCON_WRERR;\n+            goto done;\n+        }\n+        if (s->nvmkey_state != NVMKEY_UNLOCKED) {\n+            qemu_log_mask(LOG_GUEST_ERROR,\n+                          \"pic32mk_nvm: operation 0x%x without unlock\\n\", op);\n+            s->nvmcon |= PIC32MK_NVMCON_WRERR;\n+            goto done;\n+        }\n+    }\n+\n+    flash = nvm_flash_ptr(s);\n+    if (!flash && op != PIC32MK_NVMOP_NOP) {\n+        qemu_log_mask(LOG_GUEST_ERROR,\n+                      \"pic32mk_nvm: no pflash region linked\\n\");\n+        s->nvmcon |= PIC32MK_NVMCON_WRERR;\n+        goto done;\n+    }\n+\n+    switch (op) {\n+    case PIC32MK_NVMOP_NOP:\n+        break;\n+\n+    case PIC32MK_NVMOP_WORD_PROG:\n+        if (!nvm_addr_to_offset(s->nvmaddr, &offset)) {\n+            s->nvmcon |= PIC32MK_NVMCON_WRERR;\n+            break;\n+        }\n+        if (offset + 4 > PIC32MK_PFLASH_SIZE) {\n+            s->nvmcon |= PIC32MK_NVMCON_WRERR;\n+            break;\n+        }\n+        /* Word-aligned write */\n+        offset &= ~3u;\n+        memcpy(flash + offset, &s->nvmdata[0], 4);\n+        memory_region_set_dirty(s->pflash_mr, offset, 4);\n+        break;\n+\n+    case PIC32MK_NVMOP_QUAD_WORD_PROG:\n+        if (!nvm_addr_to_offset(s->nvmaddr, &offset)) {\n+            s->nvmcon |= PIC32MK_NVMCON_WRERR;\n+            break;\n+        }\n+        /* Must be 16-byte aligned */\n+        offset &= ~0xFu;\n+        if (offset + 16 > PIC32MK_PFLASH_SIZE) {\n+            s->nvmcon |= PIC32MK_NVMCON_WRERR;\n+            break;\n+        }\n+        memcpy(flash + offset, s->nvmdata, 16);\n+        memory_region_set_dirty(s->pflash_mr, offset, 16);\n+        break;\n+\n+    case PIC32MK_NVMOP_ROW_PROG: {\n+        if (!nvm_addr_to_offset(s->nvmaddr, &offset)) {\n+            s->nvmcon |= PIC32MK_NVMCON_WRERR;\n+            break;\n+        }\n+        /* Row-aligned destination */\n+        offset &= ~(PIC32MK_NVM_ROW_SIZE - 1);\n+        if (offset + PIC32MK_NVM_ROW_SIZE > PIC32MK_PFLASH_SIZE) {\n+            s->nvmcon |= PIC32MK_NVMCON_WRERR;\n+            break;\n+        }\n+        /*\n+         * Copy ROW_SIZE bytes from guest physical address NVMSRCADDR\n+         * into the program flash at the destination offset.\n+         */\n+        {\n+            uint8_t row_buf[PIC32MK_NVM_ROW_SIZE];\n+            MemTxResult res = dma_memory_read(\n+                &address_space_memory, s->nvmsrcaddr,\n+                row_buf, PIC32MK_NVM_ROW_SIZE, MEMTXATTRS_UNSPECIFIED);\n+            if (res != MEMTX_OK) {\n+                qemu_log_mask(LOG_GUEST_ERROR,\n+                              \"pic32mk_nvm: row read from 0x%08x failed\\n\",\n+                              s->nvmsrcaddr);\n+                s->nvmcon |= PIC32MK_NVMCON_WRERR;\n+                break;\n+            }\n+            memcpy(flash + offset, row_buf, PIC32MK_NVM_ROW_SIZE);\n+            memory_region_set_dirty(s->pflash_mr, offset,\n+                                    PIC32MK_NVM_ROW_SIZE);\n+        }\n+        break;\n+    }\n+\n+    case PIC32MK_NVMOP_PAGE_ERASE:\n+        if (!nvm_addr_to_offset(s->nvmaddr, &offset)) {\n+            s->nvmcon |= PIC32MK_NVMCON_WRERR;\n+            break;\n+        }\n+        /* Page-align */\n+        offset &= ~(PIC32MK_NVM_PAGE_SIZE - 1);\n+        if (offset + PIC32MK_NVM_PAGE_SIZE > PIC32MK_PFLASH_SIZE) {\n+            s->nvmcon |= PIC32MK_NVMCON_WRERR;\n+            break;\n+        }\n+        memset(flash + offset, 0xFF, PIC32MK_NVM_PAGE_SIZE);\n+        memory_region_set_dirty(s->pflash_mr, offset, PIC32MK_NVM_PAGE_SIZE);\n+        break;\n+\n+    case PIC32MK_NVMOP_PFM_ERASE:\n+        memset(flash, 0xFF, PIC32MK_PFLASH_SIZE);\n+        memory_region_set_dirty(s->pflash_mr, 0, PIC32MK_PFLASH_SIZE);\n+        break;\n+\n+    case PIC32MK_NVMOP_LOWER_PFM_ERASE:\n+    case PIC32MK_NVMOP_UPPER_PFM_ERASE:\n+        qemu_log_mask(LOG_UNIMP,\n+                      \"pic32mk_nvm: unimplemented NVMOP 0x%x\\n\", op);\n+        break;\n+\n+    default:\n+        qemu_log_mask(LOG_UNIMP,\n+                      \"pic32mk_nvm: unknown NVMOP 0x%x\\n\", op);\n+        s->nvmcon |= PIC32MK_NVMCON_WRERR;\n+        break;\n+    }\n+\n+done:\n+    /* Operation complete — clear WR, reset unlock FSM */\n+    s->nvmcon &= ~PIC32MK_NVMCON_WR;\n+    s->nvmkey_state = NVMKEY_LOCKED;\n+\n+    /* Pulse IRQ to signal completion */\n+    qemu_irq_pulse(s->irq);\n+\n+    /* Flush to backing file after any successful write/erase */\n+    if (op != PIC32MK_NVMOP_NOP &&\n+        !(s->nvmcon & (PIC32MK_NVMCON_WRERR | PIC32MK_NVMCON_LVDERR))) {\n+        nvm_flush_backing(s);\n+    }\n+}\n+\n+/*\n+ * MMIO read\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static uint64_t nvm_read(void *opaque, hwaddr addr, unsigned size)\n+{\n+    PIC32MKNVMState *s = opaque;\n+    hwaddr base = addr & ~(hwaddr)0xF;\n+\n+    switch (base) {\n+    case PIC32MK_NVMCON:\n+        return s->nvmcon;\n+    case PIC32MK_NVMKEY:\n+        return 0;\n+        /* write-only */;\n+    case PIC32MK_NVMADDR:\n+        return s->nvmaddr;\n+    case PIC32MK_NVMDATA0:\n+        return s->nvmdata[0];\n+    case PIC32MK_NVMDATA1:\n+        return s->nvmdata[1];\n+    case PIC32MK_NVMDATA2:\n+        return s->nvmdata[2];\n+    case PIC32MK_NVMDATA3:\n+        return s->nvmdata[3];\n+    case PIC32MK_NVMSRCADDR:\n+        return s->nvmsrcaddr;\n+    case PIC32MK_NVMPWP:\n+        return s->nvmpwp;\n+    case PIC32MK_NVMBWP:\n+        return s->nvmbwp;\n+    case PIC32MK_NVMCON2:\n+        return s->nvmcon2;\n+    default:\n+        qemu_log_mask(LOG_UNIMP,\n+                      \"pic32mk_nvm: unimplemented read @ 0x%04\"\n+                      HWADDR_PRIx \"\\n\", addr);\n+        return 0;\n+    }\n+}\n+\n+/*\n+ * MMIO write\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void nvm_write(void *opaque, hwaddr addr, uint64_t val, unsigned size)\n+{\n+    PIC32MKNVMState *s = opaque;\n+    int sub     = (int)(addr & 0xF);\n+    hwaddr base = addr & ~(hwaddr)0xF;\n+    uint32_t old_nvmcon;\n+\n+    switch (base) {\n+    case PIC32MK_NVMCON:\n+        old_nvmcon = s->nvmcon;\n+        apply_sci(&s->nvmcon, (uint32_t)val, sub);\n+\n+        /* Detect WR 0→1 transition: execute command */\n+        if (!(old_nvmcon & PIC32MK_NVMCON_WR) &&\n+             (s->nvmcon & PIC32MK_NVMCON_WR)) {\n+            nvm_execute_cmd(s);\n+        }\n+        break;\n+\n+    case PIC32MK_NVMKEY:\n+        /*\n+         * Unlock FSM: firmware writes 0xAA996655 then 0x556699AA.\n+         * Any other value resets the FSM.  SET/CLR/INV are not\n+         * meaningful for NVMKEY — always treat as direct write.\n+         */\n+        {\n+            uint32_t key = (uint32_t)val;\n+            if (s->nvmkey_state == NVMKEY_LOCKED &&\n+                key == PIC32MK_NVMKEY1) {\n+                s->nvmkey_state = NVMKEY_KEY1_OK;\n+            } else if (s->nvmkey_state == NVMKEY_KEY1_OK &&\n+                       key == PIC32MK_NVMKEY2) {\n+                s->nvmkey_state = NVMKEY_UNLOCKED;\n+            } else {\n+                s->nvmkey_state = NVMKEY_LOCKED;\n+            }\n+        }\n+        break;\n+\n+    case PIC32MK_NVMADDR:\n+        apply_sci(&s->nvmaddr, (uint32_t)val, sub);\n+        break;\n+\n+    case PIC32MK_NVMDATA0:\n+        apply_sci(&s->nvmdata[0], (uint32_t)val, sub);\n+        break;\n+\n+    case PIC32MK_NVMDATA1:\n+        apply_sci(&s->nvmdata[1], (uint32_t)val, sub);\n+        break;\n+\n+    case PIC32MK_NVMDATA2:\n+        apply_sci(&s->nvmdata[2], (uint32_t)val, sub);\n+        break;\n+\n+    case PIC32MK_NVMDATA3:\n+        apply_sci(&s->nvmdata[3], (uint32_t)val, sub);\n+        break;\n+\n+    case PIC32MK_NVMSRCADDR:\n+        apply_sci(&s->nvmsrcaddr, (uint32_t)val, sub);\n+        break;\n+\n+    case PIC32MK_NVMPWP:\n+        apply_sci(&s->nvmpwp, (uint32_t)val, sub);\n+        break;\n+\n+    case PIC32MK_NVMBWP:\n+        apply_sci(&s->nvmbwp, (uint32_t)val, sub);\n+        break;\n+\n+    case PIC32MK_NVMCON2:\n+        apply_sci(&s->nvmcon2, (uint32_t)val, sub);\n+        break;\n+\n+    default:\n+        qemu_log_mask(LOG_UNIMP,\n+                      \"pic32mk_nvm: unimplemented write @ 0x%04\"\n+                      HWADDR_PRIx \" = 0x%08\" PRIx64 \"\\n\",\n+                      addr, val);\n+        break;\n+    }\n+}\n+\n+static const MemoryRegionOps nvm_ops = {\n+    .read       = nvm_read,\n+    .write      = nvm_write,\n+    .endianness = DEVICE_LITTLE_ENDIAN,\n+    .valid = { .min_access_size = 4, .max_access_size = 4 },\n+};\n+\n+/*\n+ * Device lifecycle\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void pic32mk_nvm_reset(DeviceState *dev)\n+{\n+    PIC32MKNVMState *s = PIC32MK_NVM(dev);\n+\n+    s->nvmcon      = 0;\n+    s->nvmaddr     = 0;\n+    s->nvmdata[0]  = 0;\n+    s->nvmdata[1]  = 0;\n+    s->nvmdata[2]  = 0;\n+    s->nvmdata[3]  = 0;\n+    s->nvmsrcaddr  = 0;\n+    s->nvmpwp      = 0;\n+    s->nvmbwp      = 0;\n+    s->nvmcon2     = 0;\n+    s->nvmkey_state = NVMKEY_LOCKED;\n+\n+    qemu_irq_lower(s->irq);\n+}\n+\n+static void pic32mk_nvm_realize(DeviceState *dev, Error **errp)\n+{\n+    PIC32MKNVMState *s = PIC32MK_NVM(dev);\n+\n+    if (!s->pflash_mr) {\n+        error_setg(errp, \"pic32mk_nvm: 'pflash' link property not set\");\n+        return;\n+    }\n+\n+    s->backing_fd = -1;\n+\n+    if (s->filename && s->filename[0] != '\\0') {\n+        s->backing_fd = open(s->filename, O_RDWR | O_CREAT, 0644);\n+        if (s->backing_fd < 0) {\n+            error_setg_errno(errp, errno,\n+                             \"pic32mk_nvm: cannot open '%s'\",\n+                             s->filename);\n+            return;\n+        }\n+\n+        /*\n+         * If the file is new or undersized, initialize it with 0xFF\n+         * (erased flash state).\n+         */\n+        off_t fsize = lseek(s->backing_fd, 0, SEEK_END);\n+        if (fsize < (off_t)PIC32MK_PFLASH_SIZE) {\n+            /* Extend with 0xFF in chunks */\n+            uint8_t ff_buf[4096];\n+            memset(ff_buf, 0xFF, sizeof(ff_buf));\n+            lseek(s->backing_fd, (fsize > 0) ? fsize : 0, SEEK_SET);\n+            size_t remaining = PIC32MK_PFLASH_SIZE - (size_t)((fsize > 0) ? fsize : 0);\n+            while (remaining > 0) {\n+                size_t chunk = (remaining < sizeof(ff_buf)) ? remaining : sizeof(ff_buf);\n+                if (write(s->backing_fd, ff_buf, chunk) < 0) {\n+                    qemu_log_mask(LOG_GUEST_ERROR,\n+                                  \"pic32mk_nvm: cannot init '%s'\\n\",\n+                                  s->filename);\n+                    break;\n+                }\n+                remaining -= chunk;\n+            }\n+            fdatasync(s->backing_fd);\n+        }\n+\n+        /* Load backing file into pflash RAM */\n+        nvm_load_backing(s);\n+    }\n+}\n+\n+static void pic32mk_nvm_unrealize(DeviceState *dev)\n+{\n+    PIC32MKNVMState *s = PIC32MK_NVM(dev);\n+\n+    nvm_flush_backing(s);\n+\n+    if (s->backing_fd >= 0) {\n+        close(s->backing_fd);\n+        s->backing_fd = -1;\n+    }\n+}\n+\n+static void pic32mk_nvm_init(Object *obj)\n+{\n+    PIC32MKNVMState *s = PIC32MK_NVM(obj);\n+\n+    memory_region_init_io(&s->mr, obj, &nvm_ops, s,\n+                          TYPE_PIC32MK_NVM, PIC32MK_NVM_SIZE);\n+    sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->mr);\n+    sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq);\n+}\n+\n+static const Property pic32mk_nvm_props[] = {\n+    DEFINE_PROP_LINK(\"pflash\", PIC32MKNVMState, pflash_mr,\n+                     TYPE_MEMORY_REGION, MemoryRegion *),\n+    DEFINE_PROP_STRING(\"filename\", PIC32MKNVMState, filename),\n+};\n+\n+static void pic32mk_nvm_class_init(ObjectClass *klass, const void *data)\n+{\n+    DeviceClass *dc = DEVICE_CLASS(klass);\n+\n+    dc->realize   = pic32mk_nvm_realize;\n+    dc->unrealize = pic32mk_nvm_unrealize;\n+    device_class_set_legacy_reset(dc, pic32mk_nvm_reset);\n+    device_class_set_props(dc, pic32mk_nvm_props);\n+}\n+\n+static const TypeInfo pic32mk_nvm_info = {\n+    .name          = TYPE_PIC32MK_NVM,\n+    .parent        = TYPE_SYS_BUS_DEVICE,\n+    .instance_size = sizeof(PIC32MKNVMState),\n+    .instance_init = pic32mk_nvm_init,\n+    .class_init    = pic32mk_nvm_class_init,\n+};\n+\n+static void pic32mk_nvm_register_types(void)\n+{\n+    type_register_static(&pic32mk_nvm_info);\n+}\n+\n+type_init(pic32mk_nvm_register_types)\ndiff --git a/hw/mips/pic32mk_oc.c b/hw/mips/pic32mk_oc.c\nnew file mode 100644\nindex 0000000000..df47cba59f\n--- /dev/null\n+++ b/hw/mips/pic32mk_oc.c\n@@ -0,0 +1,293 @@\n+/*\n+ * PIC32MK Output Compare × 16 (OC1–OC16)\n+ * Datasheet: DS60001519E, §19\n+ *\n+ * Diagnostic-only emulation: when firmware enables an OC instance\n+ * (OCxCON.ON goes 0→1), the emulator emits a 12-byte binary event\n+ * to an optional chardev (\"oc-events\") for GUI waveform rendering,\n+ * and also writes a human-readable fallback to qemu_log().\n+ *\n+ * Event message format (16 bytes, little-endian):\n+ *   [0]     index   — OC instance number (1–16)\n+ *   [1]     enabled — 1 = enabled, 0 = disabled\n+ *   [2]     ocm     — OCM mode bits [2:0]\n+ *   [3]     flags   — bit0=OC32, bit1=OCTSEL\n+ *   [4..7]  ocr     — OCxR  (primary compare), uint32 LE\n+ *   [8..11] ocrs    — OCxRS (secondary compare), uint32 LE\n+ *   [12..15] pr     — timer PRx period register, uint32 LE (0 on disable)\n+ *\n+ * Consumer duty/pulse-width formula by mode:\n+ *   PWM (ocm=6,7):              pulse_pct = ocrs / pr * 100\n+ *   Single/continuous (ocm=4,5): pulse_pct = (ocrs - ocr) / pr * 100\n+ *\n+ * Register layout within each 0x200-byte block\n+ * (registers have SET/CLR/INV sub-regs at +4/+8/+C):\n+ *   OCxCON  +0x00  Control: ON, SIDL, OC32, OCFLT, OCTSEL, OCM\n+ *   OCxR    +0x10  Primary compare value\n+ *   OCxRS   +0x20  Secondary compare value\n+ *\n+ * Copyright (c) 2026 QEMU contributors\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+#include \"qemu/log.h\"\n+#include \"qemu/module.h\"\n+#include \"hw/core/sysbus.h\"\n+#include \"hw/core/qdev-properties.h\"\n+#include \"hw/mips/pic32mk.h\"\n+#include \"chardev/char.h\"\n+#include \"system/address-spaces.h\"\n+\n+/* OCM mode decode table (bits 2:0 of OCxCON) */\n+static const char *ocm_mode_names[] = {\n+    [0] = \"Disabled\",\n+    [1] = \"High on match, then low\",\n+    [2] = \"Init low, force high\",\n+    [3] = \"Toggle on match\",\n+    [4] = \"Single pulse\",\n+    [5] = \"Continuous pulses\",\n+    [6] = \"PWM (no fault)\",\n+    [7] = \"PWM (fault enabled)\",\n+};\n+\n+/*\n+ * Timer PRx KSEG1 addresses for OCTSEL=0 (Timer2) and OCTSEL=1 (Timer3).\n+ * OC1-OC9 use the peripheral-1 timer block (SFR base 0xBF820000).\n+ * T2 base = 0xBF820200, PR2 = T2 base + 0x20 = 0xBF820220\n+ * T3 base = 0xBF820400, PR3 = T3 base + 0x20 = 0xBF820420\n+ * TODO: OC10-OC16 use peripheral-2 block timers (T4-T9) — extend table if needed.\n+ */\n+static const uint32_t oc_timer_pr_addr[2] = {\n+    0x1F820220u,   /* OCTSEL=0 → Timer2 PRx (physical: SFR_BASE + T2_OFFSET + 0x20) */\n+    0x1F820420u,   /* OCTSEL=1 → Timer3 PRx (physical: SFR_BASE + T3_OFFSET + 0x20) */\n+};\n+\n+/*\n+ * Device state\n+ * -----------------------------------------------------------------------\n+ */\n+\n+#define TYPE_PIC32MK_OC  \"pic32mk-oc\"\n+OBJECT_DECLARE_SIMPLE_TYPE(PIC32MKOCState, PIC32MK_OC)\n+\n+struct PIC32MKOCState {\n+    SysBusDevice parent_obj;\n+    MemoryRegion mr;\n+\n+    uint32_t con;   /* OCxCON */\n+    uint32_t r;     /* OCxR   */\n+    uint32_t rs;    /* OCxRS  */\n+\n+    qemu_irq irq;\n+    uint8_t  index;    /* 1-16 */\n+    Chardev *chr_out;  /* optional chardev for waveform event streaming */\n+};\n+\n+/*\n+ * MMIO helpers\n+ * -----------------------------------------------------------------------\n+ */\n+\n+/* PIC32MK: base+0=REG, +4=CLR, +8=SET, +0xC=INV */\n+static void apply_sci(uint32_t *reg, uint32_t val, int sub)\n+{\n+    switch (sub) {\n+    case 0:\n+        *reg  = val;\n+        break;\n+    case 4:\n+        *reg &= ~val;\n+        break;\n+    case 8:\n+        *reg |= val;\n+        break;\n+    case 12:\n+        *reg ^= val;\n+        break;\n+    }\n+}\n+\n+static uint32_t *oc_find_reg(PIC32MKOCState *s, hwaddr base)\n+{\n+    switch (base) {\n+    case PIC32MK_OCxCON:\n+        return &s->con;\n+    case PIC32MK_OCxR:\n+        return &s->r;\n+    case PIC32MK_OCxRS:\n+        return &s->rs;\n+    default:\n+        return NULL;\n+    }\n+}\n+\n+/*\n+ * Timer PRx read — needed for duty-cycle in event messages\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static uint32_t oc_read_pr(PIC32MKOCState *s)\n+{\n+    int sel = !!(s->con & PIC32MK_OCCON_OCTSEL);\n+    MemTxResult res;\n+    return address_space_ldl_le(&address_space_memory,\n+                                oc_timer_pr_addr[sel],\n+                                MEMTXATTRS_UNSPECIFIED, &res);\n+}\n+\n+/*\n+ * Event emission — qemu_log fallback + optional chardev binary stream\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void oc_emit_event(PIC32MKOCState *s, bool enabled)\n+{\n+    uint32_t ocm   = (s->con & PIC32MK_OCCON_OCM_MASK) >> PIC32MK_OCCON_OCM_SHIFT;\n+    bool     oc32  = !!(s->con & PIC32MK_OCCON_OC32);\n+    bool     octsel = !!(s->con & PIC32MK_OCCON_OCTSEL);\n+\n+    /* Human-readable fallback always goes to qemu_log */\n+    if (enabled) {\n+        qemu_log(\"pic32mk_oc: OC%u enabled — mode=%s, OC32=%u, OCTSEL=%u, \"\n+                 \"OCxR=0x%08X, OCxRS=0x%08X\\n\",\n+                 s->index, ocm_mode_names[ocm], oc32, octsel, s->r, s->rs);\n+    } else {\n+        qemu_log(\"pic32mk_oc: OC%u disabled\\n\", s->index);\n+    }\n+\n+    if (!s->chr_out) {\n+        return;\n+    }\n+\n+    uint32_t pr = enabled ? oc_read_pr(s) : 0;\n+\n+    uint8_t  msg[16];\n+    uint32_t ocr_le  = cpu_to_le32(s->r);\n+    uint32_t ocrs_le = cpu_to_le32(s->rs);\n+    uint32_t pr_le   = cpu_to_le32(pr);\n+\n+    msg[0] = s->index;\n+    msg[1] = enabled ? 1 : 0;\n+    msg[2] = (uint8_t)ocm;\n+    msg[3] = (uint8_t)(oc32 | (octsel << 1));\n+    memcpy(&msg[4],  &ocr_le,  4);\n+    memcpy(&msg[8],  &ocrs_le, 4);\n+    memcpy(&msg[12], &pr_le,   4);\n+    qemu_chr_write_all(s->chr_out, msg, sizeof(msg));\n+}\n+\n+/* Public setter called by pic32mk.c after device realisation. */\n+void pic32mk_oc_set_chardev(DeviceState *dev, Chardev *chr)\n+{\n+    PIC32MK_OC(dev)->chr_out = chr;\n+}\n+\n+/*\n+ * MMIO read/write\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static uint64_t oc_read(void *opaque, hwaddr addr, unsigned size)\n+{\n+    PIC32MKOCState *s = opaque;\n+    hwaddr base = addr & ~(hwaddr)0xF;\n+    uint32_t *reg = oc_find_reg(s, base);\n+\n+    if (reg) {\n+        return *reg;\n+    }\n+\n+    qemu_log_mask(LOG_UNIMP,\n+                  \"pic32mk_oc: OC%u unimplemented read @ 0x%04\" HWADDR_PRIx \"\\n\",\n+                  s->index, addr);\n+    return 0;\n+}\n+\n+static void oc_write(void *opaque, hwaddr addr, uint64_t val, unsigned size)\n+{\n+    PIC32MKOCState *s = opaque;\n+    int sub       = (int)(addr & 0xF);\n+    hwaddr base   = addr & ~(hwaddr)0xF;\n+    uint32_t *reg = oc_find_reg(s, base);\n+\n+    if (!reg) {\n+        qemu_log_mask(LOG_UNIMP,\n+                      \"pic32mk_oc: OC%u unimplemented write @ 0x%04\"\n+                      HWADDR_PRIx \" = 0x%08\" PRIx64 \"\\n\",\n+                      s->index, addr, val);\n+        return;\n+    }\n+\n+    bool was_on = !!(s->con & PIC32MK_OCCON_ON);\n+    apply_sci(reg, (uint32_t)val, sub);\n+\n+    /* Handle ON bit transitions for OCxCON writes */\n+    if (base == PIC32MK_OCxCON) {\n+        bool now_on = !!(s->con & PIC32MK_OCCON_ON);\n+        if (!was_on && now_on) {\n+            oc_emit_event(s, true);\n+        } else if (was_on && !now_on) {\n+            oc_emit_event(s, false);\n+        }\n+    }\n+}\n+\n+static const MemoryRegionOps oc_ops = {\n+    .read       = oc_read,\n+    .write      = oc_write,\n+    .endianness = DEVICE_LITTLE_ENDIAN,\n+    .valid = {\n+        .min_access_size = 4,\n+        .max_access_size = 4,\n+    },\n+};\n+\n+/*\n+ * Device lifecycle\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void pic32mk_oc_reset(DeviceState *dev)\n+{\n+    PIC32MKOCState *s = PIC32MK_OC(dev);\n+    s->con = 0;\n+    s->r   = 0;\n+    s->rs  = 0;\n+}\n+\n+static void pic32mk_oc_init(Object *obj)\n+{\n+    PIC32MKOCState *s = PIC32MK_OC(obj);\n+\n+    memory_region_init_io(&s->mr, obj, &oc_ops, s,\n+                          TYPE_PIC32MK_OC, PIC32MK_OC_BLOCK_SIZE);\n+    sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->mr);\n+    sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq);\n+}\n+\n+static Property pic32mk_oc_properties[] = {\n+    DEFINE_PROP_UINT8(\"index\", PIC32MKOCState, index, 1),\n+};\n+\n+static void pic32mk_oc_class_init(ObjectClass *klass, const void *data)\n+{\n+    DeviceClass *dc = DEVICE_CLASS(klass);\n+    device_class_set_legacy_reset(dc, pic32mk_oc_reset);\n+    device_class_set_props(dc, pic32mk_oc_properties);\n+}\n+\n+static const TypeInfo pic32mk_oc_info = {\n+    .name          = TYPE_PIC32MK_OC,\n+    .parent        = TYPE_SYS_BUS_DEVICE,\n+    .instance_size = sizeof(PIC32MKOCState),\n+    .instance_init = pic32mk_oc_init,\n+    .class_init    = pic32mk_oc_class_init,\n+};\n+\n+static void pic32mk_oc_register_types(void)\n+{\n+    type_register_static(&pic32mk_oc_info);\n+}\n+\n+type_init(pic32mk_oc_register_types)\ndiff --git a/hw/mips/pic32mk_spi.c b/hw/mips/pic32mk_spi.c\nnew file mode 100644\nindex 0000000000..339a790a58\n--- /dev/null\n+++ b/hw/mips/pic32mk_spi.c\n@@ -0,0 +1,532 @@\n+/*\n+ * PIC32MK SPI × 6 (SPI1–SPI6)\n+ * Datasheet: DS60001519E, §23\n+ *\n+ * Each SPI instance is a SysBusDevice providing a stub register file.\n+ * Full transfer emulation (loopback, slave select, IRQs) is Phase 2B.\n+ * Firmware doing register-level init (SPIxCON, SPIxBRG) will succeed;\n+ * actual data transfers log LOG_UNIMP.\n+ *\n+ * Copyright (c) 2026 QEMU contributors\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+#include \"qemu/log.h\"\n+#include \"qemu/module.h\"\n+#include \"hw/core/sysbus.h\"\n+#include \"hw/core/irq.h\"\n+#include \"hw/core/qdev-properties.h\"\n+#include \"hw/core/qdev-properties-system.h\"\n+#include \"chardev/char-fe.h\"\n+#include \"hw/mips/pic32mk.h\"\n+\n+#define TYPE_PIC32MK_SPI    \"pic32mk-spi\"\n+OBJECT_DECLARE_SIMPLE_TYPE(PIC32MKSpiState, PIC32MK_SPI)\n+\n+struct PIC32MKSpiState {\n+    SysBusDevice parent_obj;\n+    MemoryRegion mr;\n+\n+    uint32_t con;   /* SPIxCON */\n+    uint32_t stat;  /* SPIxSTAT */\n+    uint32_t buf;   /* SPIxBUF — TX write / RX read */\n+    uint32_t brg;   /* SPIxBRG */\n+    uint32_t con2;  /* SPIxCON2 */\n+\n+    uint8_t index;  /* SPI instance index (1..6) */\n+\n+    uint8_t tx_fifo[8];\n+    uint8_t rx_fifo[8];\n+    uint8_t tx_head;\n+    uint8_t tx_tail;\n+    uint8_t tx_count;\n+    uint8_t rx_head;\n+    uint8_t rx_tail;\n+    uint8_t rx_count;\n+\n+    uint8_t frame_buf[260];\n+    uint16_t frame_pos;\n+    uint16_t frame_len;\n+\n+    bool cs_active;\n+\n+    CharFrontend chr;\n+\n+    qemu_irq irq_rx;   /* RX interrupt */\n+    qemu_irq irq_tx;   /* TX interrupt */\n+    qemu_irq irq_err;  /* Fault/overflow */\n+};\n+\n+/* CON bits (subset used by emulation + Harmony plib) */\n+#define SPI_CON_ON             (1u << 15)\n+#define SPI_CON_MSTEN          (1u << 5)\n+#define SPI_CON_MODE16         (1u << 10)\n+#define SPI_CON_MODE32         (1u << 11)\n+#define SPI_CON_ENHBUF         (1u << 16)\n+#define SPI_CON_SRXISEL_MASK   (3u << 0)\n+#define SPI_CON_STXISEL_MASK   (3u << 2)\n+#define SPI_CON_SRXISEL_SHIFT  0\n+#define SPI_CON_STXISEL_SHIFT  2\n+\n+/* STAT bits (subset used by emulation + Harmony plib) */\n+#define SPI_STAT_SPIRBF  (1u << 0)   /* RX buffer full */\n+#define SPI_STAT_SPITBF  (1u << 1)   /* TX buffer full */\n+#define SPI_STAT_SPITBE  (1u << 3)   /* TX buffer empty */\n+#define SPI_STAT_SPIRBE  (1u << 5)   /* RX buffer empty */\n+#define SPI_STAT_SPIROV  (1u << 6)   /* RX overflow */\n+#define SPI_STAT_SRMT    (1u << 11)  /* Shift register empty */\n+\n+/* Chardev frame flags */\n+#define SPI_FRAME_CS_ASSERT    (1u << 0)\n+#define SPI_FRAME_CS_DEASSERT  (1u << 1)\n+\n+/* PIC32MK: base+0=REG, +4=CLR, +8=SET, +0xC=INV */\n+static void apply_sci(uint32_t *reg, uint32_t val, int sub)\n+{\n+    switch (sub) {\n+    case 0:\n+        *reg  = val;\n+        break;\n+    case 4:\n+        *reg &= ~val;\n+        break;\n+    case 8:\n+        *reg |= val;\n+        break;\n+    case 12:\n+        *reg ^= val;\n+        break;\n+    }\n+}\n+\n+static int spi_fifo_depth(PIC32MKSpiState *s)\n+{\n+    return (s->con & SPI_CON_ENHBUF) ? 8 : 1;\n+}\n+\n+static void spi_update_stat(PIC32MKSpiState *s)\n+{\n+    const int depth = spi_fifo_depth(s);\n+\n+    if (s->rx_count > 0) {\n+        s->stat |= SPI_STAT_SPIRBF;\n+        s->stat &= ~SPI_STAT_SPIRBE;\n+    } else {\n+        s->stat &= ~SPI_STAT_SPIRBF;\n+        s->stat |= SPI_STAT_SPIRBE;\n+    }\n+\n+    if (s->tx_count == 0) {\n+        s->stat |= SPI_STAT_SPITBE;\n+        s->stat &= ~SPI_STAT_SPITBF;\n+    } else if (s->tx_count >= depth) {\n+        s->stat |= SPI_STAT_SPITBF;\n+        s->stat &= ~SPI_STAT_SPITBE;\n+    } else {\n+        s->stat &= ~SPI_STAT_SPITBF;\n+        s->stat &= ~SPI_STAT_SPITBE;\n+    }\n+\n+    if (s->tx_count == 0) {\n+        s->stat |= SPI_STAT_SRMT;\n+    } else {\n+        s->stat &= ~SPI_STAT_SRMT;\n+    }\n+}\n+\n+static void spi_update_irq(PIC32MKSpiState *s)\n+{\n+    if (!(s->con & SPI_CON_ON)) {\n+        qemu_set_irq(s->irq_rx, 0);\n+        qemu_set_irq(s->irq_tx, 0);\n+        qemu_set_irq(s->irq_err, 0);\n+        return;\n+    }\n+\n+    /* SRXISEL: 0=empty, 1=not-empty, 2=half-full, 3=full */\n+    int srxisel = (s->con & SPI_CON_SRXISEL_MASK) >> SPI_CON_SRXISEL_SHIFT;\n+    const int depth = spi_fifo_depth(s);\n+    bool rx_pending;\n+    switch (srxisel) {\n+    case 0:\n+        rx_pending = (s->rx_count == 0);\n+        break;\n+    case 1:\n+        rx_pending = (s->rx_count > 0);\n+        break;\n+    case 2:\n+        rx_pending = (s->rx_count >= depth / 2);\n+        break;\n+    case 3:\n+        rx_pending = (s->rx_count >= depth);\n+        break;\n+    default:\n+        rx_pending = false;\n+        break;\n+    }\n+\n+    /* STXISEL: 0=empty, 1=empty, 2=half-empty, 3=not-full */\n+    int stxisel = (s->con & SPI_CON_STXISEL_MASK) >> SPI_CON_STXISEL_SHIFT;\n+    bool tx_pending;\n+    switch (stxisel) {\n+    case 0:\n+        tx_pending = (s->tx_count == 0);\n+        break;\n+    case 1:\n+        tx_pending = (s->tx_count == 0);\n+        break;\n+    case 2:\n+        tx_pending = (s->tx_count <= depth / 2);\n+        break;\n+    case 3:\n+        tx_pending = (s->tx_count < depth);\n+        break;\n+    default:\n+        tx_pending = false;\n+        break;\n+    }\n+\n+    bool err_pending = (s->stat & SPI_STAT_SPIROV) != 0;\n+\n+    qemu_set_irq(s->irq_rx, rx_pending ? 1 : 0);\n+    qemu_set_irq(s->irq_tx, tx_pending ? 1 : 0);\n+    qemu_set_irq(s->irq_err, err_pending ? 1 : 0);\n+}\n+\n+static void spi_rx_push(PIC32MKSpiState *s, uint8_t val)\n+{\n+    const int depth = spi_fifo_depth(s);\n+    if (s->rx_count >= depth) {\n+        s->stat |= SPI_STAT_SPIROV;\n+        spi_update_irq(s);\n+        return;\n+    }\n+\n+    s->rx_fifo[s->rx_head] = val;\n+    s->rx_head = (uint8_t)((s->rx_head + 1) % depth);\n+    s->rx_count++;\n+}\n+\n+static bool spi_rx_pop(PIC32MKSpiState *s, uint8_t *val)\n+{\n+    const int depth = spi_fifo_depth(s);\n+    if (s->rx_count == 0) {\n+        *val = 0;\n+        return false;\n+    }\n+\n+    *val = s->rx_fifo[s->rx_tail];\n+    s->rx_tail = (uint8_t)((s->rx_tail + 1) % depth);\n+    s->rx_count--;\n+    return true;\n+}\n+\n+static void spi_tx_push(PIC32MKSpiState *s, uint8_t val)\n+{\n+    const int depth = spi_fifo_depth(s);\n+    if (s->tx_count >= depth) {\n+        return;\n+    }\n+\n+    s->tx_fifo[s->tx_head] = val;\n+    s->tx_head = (uint8_t)((s->tx_head + 1) % depth);\n+    s->tx_count++;\n+}\n+\n+static bool spi_tx_pop(PIC32MKSpiState *s, uint8_t *val)\n+{\n+    const int depth = spi_fifo_depth(s);\n+    if (s->tx_count == 0) {\n+        *val = 0xFFu;\n+        return false;\n+    }\n+\n+    *val = s->tx_fifo[s->tx_tail];\n+    s->tx_tail = (uint8_t)((s->tx_tail + 1) % depth);\n+    s->tx_count--;\n+    return true;\n+}\n+\n+static void spi_send_frame(PIC32MKSpiState *s, uint8_t flags,\n+                           const uint8_t *payload, uint8_t len)\n+{\n+    uint8_t hdr[2];\n+\n+    if (!qemu_chr_fe_backend_connected(&s->chr)) {\n+        return;\n+    }\n+\n+    hdr[0] = flags;\n+    hdr[1] = len;\n+    qemu_chr_fe_write_all(&s->chr, hdr, 2);\n+    if (len) {\n+        qemu_chr_fe_write_all(&s->chr, payload, len);\n+    }\n+}\n+\n+static void spi_handle_frame(PIC32MKSpiState *s, uint8_t flags,\n+                             const uint8_t *payload, uint8_t len)\n+{\n+    uint8_t response[255];\n+    uint8_t i;\n+\n+    if (flags & SPI_FRAME_CS_ASSERT) {\n+        s->cs_active = true;\n+    }\n+\n+    for (i = 0; i < len; i++) {\n+        spi_rx_push(s, payload[i]);\n+        (void)spi_tx_pop(s, &response[i]);\n+    }\n+\n+    if (len) {\n+        spi_send_frame(s, 0, response, len);\n+    }\n+\n+    if (flags & SPI_FRAME_CS_DEASSERT) {\n+        s->cs_active = false;\n+    }\n+\n+    spi_update_stat(s);\n+    spi_update_irq(s);\n+}\n+\n+static int spi_chr_can_receive(void *opaque)\n+{\n+    PIC32MKSpiState *s = opaque;\n+    return qemu_chr_fe_backend_connected(&s->chr) ? 1 : 0;\n+}\n+\n+static void spi_chr_receive(void *opaque, const uint8_t *buf, int size)\n+{\n+    PIC32MKSpiState *s = opaque;\n+\n+    for (int i = 0; i < size; i++) {\n+        s->frame_buf[s->frame_pos++] = buf[i];\n+        if (s->frame_pos == 2) {\n+            s->frame_len = (uint16_t)s->frame_buf[1];\n+        }\n+        if (s->frame_pos >= 2 && s->frame_pos == (uint16_t)(2 + s->frame_len)) {\n+            uint8_t flags = s->frame_buf[0];\n+            uint8_t len = (uint8_t)s->frame_len;\n+            spi_handle_frame(s, flags, &s->frame_buf[2], len);\n+            s->frame_pos = 0;\n+            s->frame_len = 0;\n+        }\n+    }\n+}\n+\n+static void spi_chr_event(void *opaque, QEMUChrEvent event)\n+{\n+    PIC32MKSpiState *s = opaque;\n+\n+    if (event == CHR_EVENT_CLOSED) {\n+        s->frame_pos = 0;\n+        s->frame_len = 0;\n+    }\n+}\n+\n+static uint32_t *spi_find_reg(PIC32MKSpiState *s, hwaddr base)\n+{\n+    switch (base) {\n+    case PIC32MK_SPIxCON:\n+        return &s->con;\n+    case PIC32MK_SPIxSTAT:\n+        return &s->stat;\n+    case PIC32MK_SPIxBRG:\n+        return &s->brg;\n+    case PIC32MK_SPIxCON2:\n+        return &s->con2;\n+    default:\n+        return NULL;\n+    }\n+}\n+\n+static uint64_t spi_read(void *opaque, hwaddr addr, unsigned size)\n+{\n+    PIC32MKSpiState *s = opaque;\n+    hwaddr base = addr & ~(hwaddr)0xF;\n+\n+    /* BUF read: pop from RX FIFO */\n+    if (base == PIC32MK_SPIxBUF) {\n+        uint32_t val = 0;\n+        uint8_t b0 = 0;\n+        uint8_t b1 = 0;\n+        uint8_t b2 = 0;\n+        uint8_t b3 = 0;\n+\n+        if (s->con & SPI_CON_MODE32) {\n+            spi_rx_pop(s, &b0);\n+            spi_rx_pop(s, &b1);\n+            spi_rx_pop(s, &b2);\n+            spi_rx_pop(s, &b3);\n+            val = (uint32_t)b0 | ((uint32_t)b1 << 8) |\n+                  ((uint32_t)b2 << 16) | ((uint32_t)b3 << 24);\n+        } else if (s->con & SPI_CON_MODE16) {\n+            spi_rx_pop(s, &b0);\n+            spi_rx_pop(s, &b1);\n+            val = (uint32_t)b0 | ((uint32_t)b1 << 8);\n+        } else {\n+            spi_rx_pop(s, &b0);\n+            val = b0;\n+        }\n+\n+        spi_update_stat(s);\n+        spi_update_irq(s);\n+        return val;\n+    }\n+\n+    uint32_t *reg = spi_find_reg(s, base);\n+    if (reg) {\n+        if (base == PIC32MK_SPIxSTAT) {\n+            spi_update_stat(s);\n+            spi_update_irq(s);\n+        }\n+        return *reg;\n+    }\n+\n+    qemu_log_mask(LOG_UNIMP,\n+                  \"pic32mk_spi: unimplemented read @ 0x%04\" HWADDR_PRIx \"\\n\",\n+                  addr);\n+    return 0;\n+}\n+\n+static void spi_write(void *opaque, hwaddr addr, uint64_t val, unsigned size)\n+{\n+    PIC32MKSpiState *s = opaque;\n+    int sub     = (int)(addr & 0xF);\n+    hwaddr base = addr & ~(hwaddr)0xF;\n+\n+    /* BUF write: transmit */\n+    if (base == PIC32MK_SPIxBUF) {\n+        uint8_t bytes[4];\n+        int count = 1;\n+\n+        bytes[0] = (uint8_t)val;\n+        bytes[1] = (uint8_t)(val >> 8);\n+        bytes[2] = (uint8_t)(val >> 16);\n+        bytes[3] = (uint8_t)(val >> 24);\n+\n+        if (s->con & SPI_CON_MODE32) {\n+            count = 4;\n+        } else if (s->con & SPI_CON_MODE16) {\n+            count = 2;\n+        }\n+\n+        for (int i = 0; i < count; i++) {\n+            spi_tx_push(s, bytes[i]);\n+\n+            if (s->con & SPI_CON_MSTEN) {\n+                if (qemu_chr_fe_backend_connected(&s->chr)) {\n+                    uint8_t flags = SPI_FRAME_CS_ASSERT | SPI_FRAME_CS_DEASSERT;\n+                    spi_send_frame(s, flags, &bytes[i], 1);\n+                } else {\n+                    spi_rx_push(s, bytes[i]);\n+                }\n+                spi_tx_pop(s, &bytes[i]);\n+            }\n+        }\n+\n+        spi_update_stat(s);\n+        spi_update_irq(s);\n+        return;\n+    }\n+\n+    uint32_t *reg = spi_find_reg(s, base);\n+    if (reg) {\n+        apply_sci(reg, (uint32_t)val, sub);\n+        if (base == PIC32MK_SPIxCON && !(s->con & SPI_CON_ON)) {\n+            s->tx_head = s->tx_tail = s->tx_count = 0;\n+            s->rx_head = s->rx_tail = s->rx_count = 0;\n+            s->stat = SPI_STAT_SPITBE | SPI_STAT_SPIRBE | SPI_STAT_SRMT;\n+        }\n+        spi_update_stat(s);\n+        spi_update_irq(s);\n+        return;\n+    }\n+\n+    qemu_log_mask(LOG_UNIMP,\n+                  \"pic32mk_spi: unimplemented write @ 0x%04\"\n+                  HWADDR_PRIx \" = 0x%08\" PRIx64 \"\\n\",\n+                  addr, val);\n+}\n+\n+static const MemoryRegionOps spi_ops = {\n+    .read       = spi_read,\n+    .write      = spi_write,\n+    .endianness = DEVICE_LITTLE_ENDIAN,\n+    .valid = { .min_access_size = 4, .max_access_size = 4 },\n+};\n+\n+static void pic32mk_spi_reset(DeviceState *dev)\n+{\n+    PIC32MKSpiState *s = PIC32MK_SPI(dev);\n+    s->con  = 0;\n+    s->stat = SPI_STAT_SPITBE | SPI_STAT_SPIRBE | SPI_STAT_SRMT;\n+    s->buf  = 0;\n+    s->brg  = 0;\n+    s->con2 = 0;\n+    s->tx_head = s->tx_tail = s->tx_count = 0;\n+    s->rx_head = s->rx_tail = s->rx_count = 0;\n+    s->frame_pos = 0;\n+    s->frame_len = 0;\n+    s->cs_active = false;\n+    qemu_irq_lower(s->irq_rx);\n+    qemu_irq_lower(s->irq_tx);\n+    qemu_irq_lower(s->irq_err);\n+}\n+\n+static void pic32mk_spi_init(Object *obj)\n+{\n+    PIC32MKSpiState *s = PIC32MK_SPI(obj);\n+\n+    memory_region_init_io(&s->mr, obj, &spi_ops, s,\n+                          TYPE_PIC32MK_SPI, PIC32MK_SPI_BLOCK_SIZE);\n+    sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->mr);\n+    sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq_rx);\n+    sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq_tx);\n+    sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq_err);\n+}\n+\n+static void pic32mk_spi_realize(DeviceState *dev, Error **errp)\n+{\n+    PIC32MKSpiState *s = PIC32MK_SPI(dev);\n+\n+    qemu_chr_fe_set_handlers(&s->chr,\n+                             spi_chr_can_receive,\n+                             spi_chr_receive,\n+                             spi_chr_event,\n+                             NULL,\n+                             s,\n+                             NULL,\n+                             true);\n+}\n+\n+static const Property pic32mk_spi_props[] = {\n+    DEFINE_PROP_UINT8(\"spi-index\", PIC32MKSpiState, index, 0),\n+    DEFINE_PROP_CHR(\"chardev\", PIC32MKSpiState, chr),\n+};\n+\n+static void pic32mk_spi_class_init(ObjectClass *klass, const void *data)\n+{\n+    DeviceClass *dc = DEVICE_CLASS(klass);\n+    device_class_set_legacy_reset(dc, pic32mk_spi_reset);\n+    device_class_set_props(dc, pic32mk_spi_props);\n+    dc->realize = pic32mk_spi_realize;\n+}\n+\n+static const TypeInfo pic32mk_spi_info = {\n+    .name          = TYPE_PIC32MK_SPI,\n+    .parent        = TYPE_SYS_BUS_DEVICE,\n+    .instance_size = sizeof(PIC32MKSpiState),\n+    .instance_init = pic32mk_spi_init,\n+    .class_init    = pic32mk_spi_class_init,\n+};\n+\n+static void pic32mk_spi_register_types(void)\n+{\n+    type_register_static(&pic32mk_spi_info);\n+}\n+\n+type_init(pic32mk_spi_register_types)\ndiff --git a/hw/mips/pic32mk_timer.c b/hw/mips/pic32mk_timer.c\nnew file mode 100644\nindex 0000000000..d53d647d33\n--- /dev/null\n+++ b/hw/mips/pic32mk_timer.c\n@@ -0,0 +1,294 @@\n+/*\n+ * PIC32MK General Purpose Timers × 9 (T1–T9)\n+ * Datasheet: DS60001519E, §14\n+ *\n+ * Timer types:\n+ *   Type A: Timer 1 only — 16-bit, asynchronous clock option\n+ *   Type B: Timer 2, 4, 6, 8 — 16-bit, 32-bit pairable with Type C\n+ *   Type C: Timer 3, 5, 7, 9 — 16-bit, pairs with preceding Type B\n+ *\n+ * Each timer instance is a SysBusDevice with:\n+ *   - One MMIO region (PIC32MK_TIMER_BLOCK_SIZE bytes)\n+ *   - QEMU ptimer for timing\n+ *   - One IRQ output (connected to EVIC input)\n+ *\n+ * Register layout within each 0x200-byte block\n+ * (registers have SET/CLR/INV sub-regs at +4/+8/+C):\n+ *   TxCON  +0x00  Control: ON, TCKPS, T32(Type B only), TCS\n+ *   TMRx   +0x10  Current count value (16 or 32-bit)\n+ *   PRx    +0x20  Period register — fires IRQ when TMR == PR\n+ *\n+ * Copyright (c) 2026 QEMU contributors\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+#include \"qemu/log.h\"\n+#include \"qemu/module.h\"\n+#include \"hw/core/sysbus.h\"\n+#include \"hw/core/irq.h\"\n+#include \"hw/core/ptimer.h\"\n+#include \"hw/core/qdev-properties.h\"\n+#include \"hw/mips/pic32mk.h\"\n+\n+/*\n+ * Prescaler lookup tables:\n+ * Type A (Timer1): 2-bit TCKPS at bits[5:4], values 0..3 → {1,8,64,256}\n+ * Type B/C:        3-bit TCKPS at bits[6:4], values 0..7 → {1,2,4,8,16,32,64,256}\n+ */\n+static const uint32_t timer_prescalers_a[4] = { 1, 8, 64, 256 };\n+static const uint32_t timer_prescalers_bc[8] = { 1, 2, 4, 8, 16, 32, 64, 256 };\n+\n+/*\n+ * Device state\n+ * -----------------------------------------------------------------------\n+ */\n+\n+#define TYPE_PIC32MK_TIMER  \"pic32mk-timer\"\n+OBJECT_DECLARE_SIMPLE_TYPE(PIC32MKTimerState, PIC32MK_TIMER)\n+\n+struct PIC32MKTimerState {\n+    SysBusDevice parent_obj;\n+    MemoryRegion mr;\n+\n+    uint32_t con;   /* TxCON */\n+    uint32_t tmr;   /* TMRx  */\n+    uint32_t pr;    /* PRx   — period (0xFFFF on reset for 16-bit) */\n+\n+    ptimer_state *ptimer;\n+    qemu_irq irq;\n+    bool type_a;    /* true = Timer1 (Type A, 2-bit TCKPS {1,8,64,256}) */\n+};\n+\n+/*\n+ * ptimer callback — fires when TMRx wraps (hits PRx)\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void pic32mk_timer_expired(void *opaque)\n+{\n+    PIC32MKTimerState *s = opaque;\n+\n+    /*\n+     * The ptimer already reloaded with the period; just assert the IRQ.\n+     * The EVIC input is level-sensitive: a pulse is sufficient because\n+     * the EVIC latches IFSx on the rising edge.\n+      */\n+    qemu_irq_pulse(s->irq);\n+}\n+\n+/*\n+ * Start/stop/reload helpers\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void timer_reload(PIC32MKTimerState *s)\n+{\n+    uint32_t prescale_idx = (s->con & PIC32MK_TCON_TCKPS_MASK)\n+                            >> PIC32MK_TCON_TCKPS_SHIFT;\n+    uint32_t prescale = s->type_a\n+        ? timer_prescalers_a[prescale_idx & 3]\n+        : timer_prescalers_bc[prescale_idx & 7];\n+    uint32_t period       = (s->pr == 0) ? 0xFFFFu : s->pr;\n+\n+    ptimer_transaction_begin(s->ptimer);\n+    ptimer_set_freq(s->ptimer, PIC32MK_CPU_HZ / prescale);\n+    ptimer_set_limit(s->ptimer, period, 1);\n+    ptimer_run(s->ptimer, 0);   /* 0 = periodic */\n+    ptimer_transaction_commit(s->ptimer);\n+}\n+\n+static void timer_stop(PIC32MKTimerState *s)\n+{\n+    ptimer_transaction_begin(s->ptimer);\n+    ptimer_stop(s->ptimer);\n+    ptimer_transaction_commit(s->ptimer);\n+}\n+\n+/*\n+ * MMIO helpers\n+ * -----------------------------------------------------------------------\n+ */\n+\n+/* PIC32MK: base+0=REG, +4=CLR, +8=SET, +0xC=INV */\n+static void apply_sci(uint32_t *reg, uint32_t val, int sub)\n+{\n+    switch (sub) {\n+    case 0:\n+        *reg  = val;\n+        break;\n+    case 4:\n+        *reg &= ~val;\n+        break;\n+    case 8:\n+        *reg |= val;\n+        break;\n+    case 12:\n+        *reg ^= val;\n+        break;\n+    }\n+}\n+\n+static uint32_t *timer_find_reg(PIC32MKTimerState *s, hwaddr base)\n+{\n+    switch (base) {\n+    case PIC32MK_TxCON:\n+        return &s->con;\n+    case PIC32MK_TMRx:\n+        return &s->tmr;\n+    case PIC32MK_PRx:\n+        return &s->pr;\n+    default:\n+        return NULL;\n+    }\n+}\n+\n+/*\n+ * MMIO read/write\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static uint64_t timer_read(void *opaque, hwaddr addr, unsigned size)\n+{\n+    PIC32MKTimerState *s = opaque;\n+    hwaddr base = addr & ~(hwaddr)0xF;\n+\n+    /* TMRx: return ptimer current count when timer is running */\n+    if (base == PIC32MK_TMRx) {\n+        if (s->con & PIC32MK_TCON_ON) {\n+            return (uint32_t)ptimer_get_count(s->ptimer);\n+        }\n+        return s->tmr;\n+    }\n+\n+    uint32_t *reg = timer_find_reg(s, base);\n+    if (reg) {\n+        return *reg;\n+    }\n+\n+    qemu_log_mask(LOG_UNIMP,\n+                  \"pic32mk_timer: unimplemented read @ 0x%04\" HWADDR_PRIx \"\\n\",\n+                  addr);\n+    return 0;\n+}\n+\n+static void timer_write(void *opaque, hwaddr addr, uint64_t val, unsigned size)\n+{\n+    PIC32MKTimerState *s = opaque;\n+    int sub       = (int)(addr & 0xF);\n+    hwaddr base   = addr & ~(hwaddr)0xF;\n+    uint32_t *reg = timer_find_reg(s, base);\n+\n+    if (!reg) {\n+        qemu_log_mask(LOG_UNIMP,\n+                      \"pic32mk_timer: unimplemented write @ 0x%04\"\n+                      HWADDR_PRIx \" = 0x%08\" PRIx64 \"\\n\",\n+                      addr, val);\n+        return;\n+    }\n+\n+    bool was_on = !!(s->con & PIC32MK_TCON_ON);\n+    apply_sci(reg, (uint32_t)val, sub);\n+\n+    /* TMRx write: update count when timer is stopped */\n+    if (base == PIC32MK_TMRx) {\n+        if (!was_on) {\n+            ptimer_transaction_begin(s->ptimer);\n+            ptimer_set_count(s->ptimer, s->tmr);\n+            ptimer_transaction_commit(s->ptimer);\n+        }\n+        return;\n+    }\n+\n+    /* TxCON write: handle ON bit transitions */\n+    if (base == PIC32MK_TxCON) {\n+        bool now_on = !!(s->con & PIC32MK_TCON_ON);\n+        if (!was_on && now_on) {\n+            timer_reload(s);\n+        } else if (was_on && !now_on) {\n+            timer_stop(s);\n+            /* Save current count */\n+            s->tmr = (uint32_t)ptimer_get_count(s->ptimer);\n+        } else if (now_on) {\n+            /* Prescaler may have changed; reload */\n+            timer_reload(s);\n+        }\n+    }\n+\n+    /* PRx write: update period on the fly */\n+    if (base == PIC32MK_PRx && (s->con & PIC32MK_TCON_ON)) {\n+        timer_reload(s);\n+    }\n+}\n+\n+static const MemoryRegionOps timer_ops = {\n+    .read       = timer_read,\n+    .write      = timer_write,\n+    .endianness = DEVICE_LITTLE_ENDIAN,\n+    .valid = {\n+        .min_access_size = 4,\n+        .max_access_size = 4,\n+    },\n+};\n+\n+/*\n+ * Device lifecycle\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void pic32mk_timer_reset(DeviceState *dev)\n+{\n+    PIC32MKTimerState *s = PIC32MK_TIMER(dev);\n+\n+    timer_stop(s);\n+    s->con = 0;\n+    s->tmr = 0;\n+    s->pr  = 0xFFFF;   /* 16-bit period register reset value */\n+    qemu_irq_lower(s->irq);\n+}\n+\n+static void pic32mk_timer_init(Object *obj)\n+{\n+    PIC32MKTimerState *s = PIC32MK_TIMER(obj);\n+\n+    memory_region_init_io(&s->mr, obj, &timer_ops, s,\n+                          TYPE_PIC32MK_TIMER, PIC32MK_TIMER_BLOCK_SIZE);\n+    sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->mr);\n+\n+    sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq);\n+\n+    s->ptimer = ptimer_init(pic32mk_timer_expired, s, PTIMER_POLICY_NO_IMMEDIATE_TRIGGER);\n+}\n+\n+static void pic32mk_timer_finalize(Object *obj)\n+{\n+    PIC32MKTimerState *s = PIC32MK_TIMER(obj);\n+    ptimer_free(s->ptimer);\n+}\n+\n+static Property pic32mk_timer_properties[] = {\n+    DEFINE_PROP_BOOL(\"type-a\", PIC32MKTimerState, type_a, false),\n+};\n+\n+static void pic32mk_timer_class_init(ObjectClass *klass, const void *data)\n+{\n+    DeviceClass *dc = DEVICE_CLASS(klass);\n+    device_class_set_legacy_reset(dc, pic32mk_timer_reset);\n+    device_class_set_props(dc, pic32mk_timer_properties);\n+}\n+\n+static const TypeInfo pic32mk_timer_info = {\n+    .name          = TYPE_PIC32MK_TIMER,\n+    .parent        = TYPE_SYS_BUS_DEVICE,\n+    .instance_size = sizeof(PIC32MKTimerState),\n+    .instance_init = pic32mk_timer_init,\n+    .instance_finalize = pic32mk_timer_finalize,\n+    .class_init    = pic32mk_timer_class_init,\n+};\n+\n+static void pic32mk_timer_register_types(void)\n+{\n+    type_register_static(&pic32mk_timer_info);\n+}\n+\n+type_init(pic32mk_timer_register_types)\ndiff --git a/hw/mips/pic32mk_uart.c b/hw/mips/pic32mk_uart.c\nnew file mode 100644\nindex 0000000000..e2e83a2b7e\n--- /dev/null\n+++ b/hw/mips/pic32mk_uart.c\n@@ -0,0 +1,334 @@\n+/*\n+ * PIC32MK UART × 6 (U1–U6)\n+ * Datasheet: DS60001519E, §21 (pp. 475–552)\n+ *\n+ * Each UART instance is a SysBusDevice with:\n+ *   - One MMIO region (PIC32MK_UART_BLOCK_SIZE bytes)\n+ *   - CharFrontend for TX/RX via QEMU backend (-serial stdio, etc.)\n+ *   - Three IRQ outputs: index 0 = RX, 1 = TX, 2 = Error\n+ *\n+ * Register layout (all 4-byte registers with SET/CLR/INV at +4/+8/+C):\n+ *   UxMODE  +0x00  Mode (UARTEN/ON bits, parity, stop bits)\n+ *   UxSTA   +0x10  Status + TX/RX interrupt select\n+ *   UxTXREG +0x20  TX data (write-only)\n+ *   UxRXREG +0x30  RX data (read-only)\n+ *   UxBRG   +0x40  Baud rate generator\n+ *\n+ * Copyright (c) 2026 QEMU contributors\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+#include \"qemu/log.h\"\n+#include \"qemu/module.h\"\n+#include \"hw/core/sysbus.h\"\n+#include \"hw/core/irq.h\"\n+#include \"hw/core/qdev-properties.h\"\n+#include \"hw/core/qdev-properties-system.h\"\n+#include \"chardev/char-fe.h\"\n+#include \"hw/mips/pic32mk.h\"\n+\n+/*\n+ * Device state\n+ * -----------------------------------------------------------------------\n+ */\n+\n+#define TYPE_PIC32MK_UART   \"pic32mk-uart\"\n+OBJECT_DECLARE_SIMPLE_TYPE(PIC32MKUartState, PIC32MK_UART)\n+\n+struct PIC32MKUartState {\n+    SysBusDevice parent_obj;\n+    MemoryRegion mr;\n+\n+    /* Registers (base values; SET/CLR/INV are decoded in the write handler) */\n+    uint32_t mode;  /* UxMODE */\n+    uint32_t sta;   /* UxSTA  */\n+    uint32_t brg;   /* UxBRG  */\n+\n+    /* RX single-byte buffer */\n+    uint8_t  rxbuf;\n+    bool     rxbuf_full;\n+\n+    CharFrontend chr;\n+\n+    /* IRQ outputs: 0=RX, 1=TX, 2=Error */\n+    qemu_irq irq_rx;\n+    qemu_irq irq_tx;\n+    qemu_irq irq_err;\n+};\n+\n+/*\n+ * UxSTA status helpers\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void uart_update_sta(PIC32MKUartState *s)\n+{\n+    /* TRMT (bit 8): TX shift register empty — always 1 (we forward immediately) */\n+    s->sta |= PIC32MK_USTA_TRMT;\n+    /* UTXBF (bit 9): TX buffer full — always 0 */\n+    s->sta &= ~PIC32MK_USTA_UTXBF;\n+\n+    /* URXDA (bit 0): RX data available */\n+    if (s->rxbuf_full) {\n+        s->sta |= PIC32MK_USTA_URXDA;\n+    } else {\n+        s->sta &= ~PIC32MK_USTA_URXDA;\n+    }\n+}\n+\n+static void uart_update_irq(PIC32MKUartState *s)\n+{\n+    /* RX IRQ: assert while URXDA=1 and URXEN is set */\n+    bool rx_pending = s->rxbuf_full && (s->sta & PIC32MK_USTA_URXEN);\n+    qemu_set_irq(s->irq_rx, rx_pending ? 1 : 0);\n+\n+    /*\n+     * TX IRQ: level-based.  In QEMU the TX path is instantaneous — chars\n+     * go out immediately via qemu_chr_fe_write_all(), so TRMT=1 and\n+     * UTXBF=0 at all times.  Regardless of UTXISEL[1:0], the TX\n+     * interrupt condition is always met whenever ON=1 and UTXEN=1.\n+     *\n+     * The EVIC tracks source levels (irq_level[]) and re-asserts IFS\n+     * bits after firmware clears them, so keeping irq_tx asserted at\n+     * level=1 causes the IFS flag to be re-set immediately after clear\n+     * — matching real Microchip behavior.  The ISR must disable IEC\n+     * (the enable bit) to stop re-entry, which is exactly what the\n+     * Microchip plib does.\n+     */\n+    bool tx_active = (s->mode & PIC32MK_UMODE_ON) &&\n+                     (s->sta & PIC32MK_USTA_UTXEN);\n+    qemu_set_irq(s->irq_tx, tx_active ? 1 : 0);\n+}\n+\n+/*\n+ * CharFrontend callbacks (RX path)\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static int uart_can_receive(void *opaque)\n+{\n+    PIC32MKUartState *s = opaque;\n+    return s->rxbuf_full ? 0 : 1;\n+}\n+\n+static void uart_receive(void *opaque, const uint8_t *buf, int size)\n+{\n+    PIC32MKUartState *s = opaque;\n+\n+    if (size == 0) {\n+        return;\n+    }\n+\n+    if (s->rxbuf_full) {\n+        /* Overrun: set OERR bit */\n+        s->sta |= PIC32MK_USTA_OERR;\n+        qemu_set_irq(s->irq_err, 1);\n+        return;\n+    }\n+\n+    s->rxbuf      = buf[0];\n+    s->rxbuf_full = true;\n+    uart_update_sta(s);\n+    uart_update_irq(s);\n+}\n+\n+/*\n+ * MMIO helpers\n+ * -----------------------------------------------------------------------\n+ */\n+\n+/* Apply SET/CLR/INV operation based on sub-register offset */\n+/* PIC32MK: base+0=REG, +4=CLR, +8=SET, +0xC=INV */\n+static void apply_sci(uint32_t *reg, uint32_t val, int sub)\n+{\n+    switch (sub) {\n+    case 0:\n+        *reg  = val;\n+        break;\n+    case 4:\n+        *reg &= ~val;\n+        break;\n+    case 8:\n+        *reg |= val;\n+        break;\n+    case 12:\n+        *reg ^= val;\n+        break;\n+    }\n+}\n+\n+/* Map UART register base address to state field */\n+static uint32_t *uart_find_reg(PIC32MKUartState *s, hwaddr base)\n+{\n+    switch (base) {\n+    case PIC32MK_UxMODE:\n+        return &s->mode;\n+    case PIC32MK_UxSTA:\n+        return &s->sta;\n+    case PIC32MK_UxBRG:\n+        return &s->brg;\n+    default:\n+        return NULL;\n+    }\n+}\n+\n+/*\n+ * MMIO read/write\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static uint64_t uart_read(void *opaque, hwaddr addr, unsigned size)\n+{\n+    PIC32MKUartState *s = opaque;\n+\n+    uart_update_sta(s);\n+\n+    /* RX register is special: reading consumes the byte */\n+    if ((addr & ~(hwaddr)0xF) == PIC32MK_UxRXREG) {\n+        if (s->rxbuf_full) {\n+            uint32_t val  = s->rxbuf;\n+            s->rxbuf_full = false;\n+            uart_update_sta(s);\n+            uart_update_irq(s);\n+            qemu_chr_fe_accept_input(&s->chr);\n+            return val;\n+        }\n+        return 0;\n+    }\n+\n+    /* TX register is write-only */\n+    if ((addr & ~(hwaddr)0xF) == PIC32MK_UxTXREG) {\n+        return 0;\n+    }\n+\n+    hwaddr base   = addr & ~(hwaddr)0xF;\n+    uint32_t *reg = uart_find_reg(s, base);\n+    if (reg) {\n+        return *reg;\n+    }\n+\n+    qemu_log_mask(LOG_UNIMP,\n+                  \"pic32mk_uart: unimplemented read @ 0x%04\" HWADDR_PRIx \"\\n\",\n+                  addr);\n+    return 0;\n+}\n+\n+static void uart_write(void *opaque, hwaddr addr, uint64_t val, unsigned size)\n+{\n+    PIC32MKUartState *s = opaque;\n+\n+    /* TX register: writing transmits a byte */\n+    if ((addr & ~(hwaddr)0xF) == PIC32MK_UxTXREG) {\n+        uint8_t ch = (uint8_t)(val & 0xFF);\n+        qemu_chr_fe_write_all(&s->chr, &ch, 1);\n+        return;\n+    }\n+\n+    int sub       = (int)(addr & 0xF);\n+    hwaddr base   = addr & ~(hwaddr)0xF;\n+    uint32_t *reg = uart_find_reg(s, base);\n+\n+    if (!reg) {\n+        qemu_log_mask(LOG_UNIMP,\n+                      \"pic32mk_uart: unimplemented write @ 0x%04\"\n+                      HWADDR_PRIx \" = 0x%08\" PRIx64 \"\\n\",\n+                      addr, val);\n+        return;\n+    }\n+\n+    apply_sci(reg, (uint32_t)val, sub);\n+\n+    /* Keep status bits consistent after firmware writes to STA */\n+    if (base == PIC32MK_UxSTA) {\n+        /* Firmware clears OERR by writing 0 to that bit */\n+        if (!(s->sta & PIC32MK_USTA_OERR)) {\n+            qemu_set_irq(s->irq_err, 0);\n+        }\n+        uart_update_irq(s);\n+    }\n+    if (base == PIC32MK_UxMODE) {\n+        uart_update_irq(s);\n+    }\n+}\n+\n+static const MemoryRegionOps uart_ops = {\n+    .read       = uart_read,\n+    .write      = uart_write,\n+    .endianness = DEVICE_LITTLE_ENDIAN,\n+    .valid = {\n+        .min_access_size = 1,\n+        .max_access_size = 4,\n+    },\n+};\n+\n+/*\n+ * Device lifecycle\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void pic32mk_uart_reset(DeviceState *dev)\n+{\n+    PIC32MKUartState *s = PIC32MK_UART(dev);\n+\n+    s->mode       = 0;\n+    /* Reset STA: TRMT=1, TX buffer not full */\n+    s->sta        = PIC32MK_USTA_TRMT;\n+    s->brg        = 0;\n+    s->rxbuf_full = false;\n+    s->rxbuf      = 0;\n+\n+    qemu_set_irq(s->irq_rx,  0);\n+    qemu_set_irq(s->irq_tx,  0);\n+    qemu_set_irq(s->irq_err, 0);\n+}\n+\n+static void pic32mk_uart_realize(DeviceState *dev, Error **errp)\n+{\n+    PIC32MKUartState *s = PIC32MK_UART(dev);\n+\n+    qemu_chr_fe_set_handlers(&s->chr,\n+                             uart_can_receive, uart_receive,\n+                             NULL, NULL,\n+                             s, NULL, true);\n+}\n+\n+static void pic32mk_uart_init(Object *obj)\n+{\n+    PIC32MKUartState *s = PIC32MK_UART(obj);\n+\n+    memory_region_init_io(&s->mr, obj, &uart_ops, s,\n+                          TYPE_PIC32MK_UART, PIC32MK_UART_BLOCK_SIZE);\n+    sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->mr);\n+\n+    sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq_rx);   /* index 0 */\n+    sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq_tx);   /* index 1 */\n+    sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq_err);  /* index 2 */\n+}\n+\n+static const Property pic32mk_uart_props[] = {\n+    DEFINE_PROP_CHR(\"chardev\", PIC32MKUartState, chr),\n+};\n+\n+static void pic32mk_uart_class_init(ObjectClass *klass, const void *data)\n+{\n+    DeviceClass *dc = DEVICE_CLASS(klass);\n+    device_class_set_legacy_reset(dc, pic32mk_uart_reset);\n+    device_class_set_props(dc, pic32mk_uart_props);\n+    dc->realize = pic32mk_uart_realize;\n+}\n+\n+static const TypeInfo pic32mk_uart_info = {\n+    .name          = TYPE_PIC32MK_UART,\n+    .parent        = TYPE_SYS_BUS_DEVICE,\n+    .instance_size = sizeof(PIC32MKUartState),\n+    .instance_init = pic32mk_uart_init,\n+    .class_init    = pic32mk_uart_class_init,\n+};\n+\n+static void pic32mk_uart_register_types(void)\n+{\n+    type_register_static(&pic32mk_uart_info);\n+}\n+\n+type_init(pic32mk_uart_register_types)\ndiff --git a/hw/mips/pic32mk_usb.c b/hw/mips/pic32mk_usb.c\nnew file mode 100644\nindex 0000000000..24a877fbeb\n--- /dev/null\n+++ b/hw/mips/pic32mk_usb.c\n@@ -0,0 +1,1033 @@\n+/*\n+ * Microchip PIC32MK USB OTG Full-Speed controller emulation\n+ * Datasheet: DS60001519E §25\n+ * Register addresses verified against p32mk1024mcm100.h (XC32 v4.60 pack).\n+ *\n+ * Phase 4A — register-file stub (complete)\n+ * Phase 4B — USB enumeration simulation + CDC TX chardev output:\n+ *   • QEMUTimer drives the EP0 state machine (USB Reset → enumerate)\n+ *   • SETUP packet injection via BDT write + TRNIF interrupt\n+ *   • CDC TX polling: EP1-IN BDT entry harvested → chardev write\n+ *   • chardev property \"chardev\" exposes CDC output as host PTY/socket\n+ *\n+ * BDT layout (PIC32MK device mode, ping-pong off / PPBRST):\n+ *   Each endpoint has 4 BDT entries (RX-even, RX-odd, TX-even, TX-odd).\n+ *   Entry size = 8 bytes (4-byte ctrl word + 4-byte buffer address).\n+ *   EP0-OUT-even = offset 0x00, EP0-IN-even = offset 0x10,\n+ *   EP1-OUT-even = offset 0x20, EP1-IN-even = offset 0x30.\n+ *\n+ * Copyright (c) 2026 QEMU contributors\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+#include \"qemu/log.h\"\n+#include \"qemu/timer.h\"\n+#include \"qapi/error.h\"\n+#include \"hw/core/sysbus.h\"\n+#include \"hw/core/irq.h\"\n+#include \"hw/mips/pic32mk.h\"\n+#include \"hw/mips/pic32mk_usb.h\"\n+#include \"exec/cpu-common.h\"   /* cpu_physical_memory_read/write */\n+#include \"chardev/char-fe.h\"\n+#include \"hw/core/qdev-properties.h\"\n+#include \"hw/core/qdev-properties-system.h\"\n+\n+/*\n+ * Helpers\n+ * -----------------------------------------------------------------------\n+ */\n+\n+/*\n+ * Apply SET / CLR / INV operation.\n+ * sub = addr & 0xF: 0 → write, 4 → CLR, 8 → SET, C → INV\n+ */\n+static void apply_sci(uint32_t *reg, uint32_t val, int sub)\n+{\n+    switch (sub) {\n+    case 0x0:\n+        *reg  = val;\n+        break;\n+    case 0x4:\n+        *reg &= ~val;\n+        break;\n+    case 0x8:\n+        *reg |= val;\n+        break;\n+    case 0xC:\n+        *reg ^= val;\n+        break;\n+    }\n+}\n+\n+/*\n+ * Apply write-1-clear (W1C) operation.\n+ */\n+static void apply_w1c(uint32_t *reg, uint32_t val, int sub)\n+{\n+    if (sub == 0x0 || sub == 0x4) {\n+        *reg &= ~val;\n+    }\n+}\n+\n+static void usb_update_irq(PIC32MKUSBState *s)\n+{\n+    /*\n+     * SESSION_VALID (OTGIR bit 3 / SESVDIF):\n+     *\n+     * On real PIC32MK hardware, SESVDIF is edge-triggered: it fires once when\n+     * SESVD transitions.  To prevent an infinite ISR loop (the Harmony ISR\n+     * clears SESVDIF but VBUS stays valid), we use an edge-latch flag:\n+     * sesvd_edge_latched is set when SESVDIF is first asserted, and cleared\n+     * when firmware W1C-clears SESVDIF from OTGIR.  We only include SESVDIF\n+     * in the IRQ level calculation while the edge latch is active.\n+     */\n+    uint32_t otg_pending = s->otgir & s->otgie;\n+    if (!s->sesvd_edge_latched) {\n+        otg_pending &= ~USB_OTG_IR_SESSION_VALID;\n+    }\n+\n+    bool fire = ((s->uir  & s->uie)  != 0)\n+             || ((s->ueir & s->ueie) != 0)\n+             || (otg_pending != 0);\n+    qemu_set_irq(s->irq, fire ? 1 : 0);\n+}\n+\n+/*\n+ * USTAT FIFO helpers (PIC32MK has a 4-deep hardware FIFO)\n+ *\n+ * On real hardware, each completed transaction pushes an USTAT value onto\n+ * the FIFO and asserts TRNIF.  When firmware W1C-clears TRNIF, the FIFO\n+ * pops; if more entries remain, TRNIF is re-asserted immediately.\n+ * Without this FIFO, two transactions completing in quick succession\n+ * (e.g. chardev RX + timer TX poll) overwrite the single USTAT register,\n+ * causing the firmware to miss one transaction entirely — the root cause\n+ * of the write stall.\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void usb_stat_fifo_push(PIC32MKUSBState *s, uint32_t stat_val)\n+{\n+    if (s->stat_fifo_count >= 4) {\n+        qemu_log_mask(LOG_GUEST_ERROR,\n+                      \"pic32mk_usb: USTAT FIFO overflow (dropped 0x%02x)\\n\",\n+                      stat_val);\n+        return;\n+    }\n+    s->stat_fifo[s->stat_fifo_head] = stat_val;\n+    s->stat_fifo_head = (s->stat_fifo_head + 1) & 3;\n+    s->stat_fifo_count++;\n+    /* Expose the front of the FIFO via the USTAT register */\n+    s->ustat = s->stat_fifo[s->stat_fifo_tail];\n+    s->uir |= USB_IR_TRNIF;\n+}\n+\n+/*\n+ * Pop the front entry.  Called when firmware W1C-clears TRNIF.\n+ * If more entries remain, re-assert TRNIF with the next USTAT value.\n+ */\n+static void usb_stat_fifo_pop(PIC32MKUSBState *s)\n+{\n+    if (s->stat_fifo_count == 0) {\n+        return;\n+    }\n+    s->stat_fifo_tail = (s->stat_fifo_tail + 1) & 3;\n+    s->stat_fifo_count--;\n+    if (s->stat_fifo_count > 0) {\n+        s->ustat = s->stat_fifo[s->stat_fifo_tail];\n+        s->uir |= USB_IR_TRNIF;\n+    }\n+}\n+\n+/*\n+ * BDT helpers\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static hwaddr usb_bdt_phys(PIC32MKUSBState *s)\n+{\n+    /*\n+     * BDT base = (BDTP3[7:0] << 24) | (BDTP2[7:0] << 16) | (BDTP1[7:1] << 8)\n+     * BDTP1 bits [7:1] map to BDT address bits [15:9].\n+      */\n+    return ((hwaddr)(s->bdtp3 & 0xFFu) << 24)\n+         | ((hwaddr)(s->bdtp2 & 0xFFu) << 16)\n+         | ((hwaddr)(s->bdtp1 & 0xFEu) << 8);\n+}\n+\n+/*\n+ * Inject an 8-byte SETUP packet into the EP0-OUT-even BDT buffer.\n+ * The BDT entry at offset 0x00 from BDT base must be UOWN=1 (firmware armed).\n+ * On success: writes the SETUP data to the buffer, clears UOWN, sets\n+ * TOK_PID=SETUP (0xD) in byte[0] bits[5:2]=0x34, bc=8 in shortWord[1]\n+ * (bits[31:16]), sets ustat=EP0/OUT/EVEN, fires TRNIF.\n+ *\n+ * BDT 32-bit control word layout (Harmony DRV_USBFS_BDT_ENTRY union):\n+ *   bits[ 7: 0] = byte[0]:  UOWN(7), DATA01(6), DTSEN(4), BSTALL(2), TOK_PID[5:2]\n+ *   bits[15: 8] = byte[1]:  (reserved / padding)\n+ *   bits[31:16] = shortWord[1]: byte count (BC)\n+ *   word[1]                  = buffer physical address\n+ *\n+ * Harmony TRNIF switch uses (byte[0] & 0x3C):\n+ *   0x34 → SETUP (TOK_PID=0xD)\n+ *   0x04 → OUT   (TOK_PID=0x1)\n+ *   0x24 → IN    (TOK_PID=0x9)\n+ */\n+static bool usb_inject_setup(PIC32MKUSBState *s, const uint8_t setup[8])\n+{\n+    hwaddr bdt = usb_bdt_phys(s);\n+    if (!bdt) {\n+        return false;\n+    }\n+\n+    /* Check both ping-pong entries for EP0-OUT (even=BDT+0, odd=BDT+8). */\n+    for (int ppbi = 0; ppbi < 2; ppbi++) {\n+        hwaddr out_entry = bdt + (ppbi ? 8u : 0u);\n+        uint8_t entry[8];\n+        cpu_physical_memory_read(out_entry, entry, 8);\n+\n+        uint32_t ctrl = le32_to_cpu(*(uint32_t *)entry);\n+        uint32_t addr = le32_to_cpu(*(uint32_t *)(entry + 4));\n+\n+        if (!(ctrl & BDT_UOWN) || !addr) {\n+            continue;   /* not armed — try other ping-pong */\n+        }\n+\n+        /* Write SETUP packet into the EP0-OUT buffer */\n+        cpu_physical_memory_write(addr, setup, 8);\n+\n+        /*\n+         * Return BDT entry to firmware:\n+         *   byte[0] = 0x34: TOK_PID=SETUP(0xD) in bits[5:2], UOWN=0\n+         *   shortWord[1] = 8: byte count\n+         */\n+        ctrl = 0x00000034u | (8u << 16);\n+        *(uint32_t *)entry = cpu_to_le32(ctrl);\n+        cpu_physical_memory_write(out_entry, entry, 8);\n+\n+        /* UxSTAT: EP=0, DIR=OUT(0), PPBI=ppbi */\n+        usb_stat_fifo_push(s, (uint32_t)(ppbi ? 0x04u : 0x00u));\n+        usb_update_irq(s);\n+        return true;\n+    }\n+    return false;   /* neither entry armed yet */\n+}\n+\n+/*\n+ * Accept an EP0-IN response from the firmware: read the data the firmware\n+ * placed in EP0-IN-even (BDT offset 0x10), set TOK_PID=IN in byte[0],\n+ * clear UOWN, preserve bc in shortWord[1], fire TRNIF.\n+ * Returns true if an IN entry was accepted, false if not armed yet.\n+ */\n+static bool usb_accept_ep0_in(PIC32MKUSBState *s)\n+{\n+    hwaddr bdt = usb_bdt_phys(s);\n+    if (!bdt) {\n+        return false;\n+    }\n+\n+    /* Check both ping-pong entries for EP0-IN (even=BDT+0x10, odd=BDT+0x18). */\n+    for (int ppbi = 0; ppbi < 2; ppbi++) {\n+        hwaddr in_entry = bdt + 0x10u + (ppbi ? 8u : 0u);\n+        uint8_t entry[8];\n+        cpu_physical_memory_read(in_entry, entry, 8);\n+\n+        uint32_t ctrl = le32_to_cpu(*(uint32_t *)entry);\n+        if (!(ctrl & BDT_UOWN)) {\n+            continue;   /* not armed */\n+        }\n+\n+        /*\n+         * Return BDT entry to firmware:\n+         *   byte[0] = 0x24: TOK_PID=IN(0x9), UOWN=0\n+         *   shortWord[1]: preserve bc\n+         */\n+        ctrl = (ctrl & 0xFFFF0000u) | 0x24u;\n+        *(uint32_t *)entry = cpu_to_le32(ctrl);\n+        cpu_physical_memory_write(in_entry, entry, 8);\n+\n+        /* UxSTAT: EP=0, DIR=IN(bit3), PPBI=ppbi(bit2) */\n+        usb_stat_fifo_push(s, 0x08u | (uint32_t)(ppbi ? 0x04u : 0x00u));\n+        usb_update_irq(s);\n+        return true;\n+    }\n+    return false;\n+}\n+\n+/*\n+ * Simulate the STATUS phase OUT for a control read (host→device ZLP).\n+ * Write EP0-OUT-even BDT entry: TOK_PID=OUT(0x1)→byte[0]=0x04, UOWN=0,\n+ * bc=0 in shortWord[1].  Then fire TRNIF so the Harmony TRNIF handler\n+ * hits case 0x04, sees shortWord[1]=0 < maxPacketSize, marks the IRP\n+ * complete, invokes the callback, and re-arms EP0-OUT for the next SETUP.\n+ */\n+static void usb_send_status_out(PIC32MKUSBState *s)\n+{\n+    hwaddr bdt = usb_bdt_phys(s);\n+    if (!bdt) {\n+        return;\n+    }\n+\n+    /*\n+     * Find the armed EP0-OUT ping-pong entry (even=BDT+0, odd=BDT+8).\n+     * Write: byte[0]=0x04 (TOK_PID=OUT), UOWN=0, shortWord[1]=0 (ZLP).\n+     */\n+    for (int ppbi = 0; ppbi < 2; ppbi++) {\n+        hwaddr out_entry = bdt + (ppbi ? 8u : 0u);\n+        uint8_t entry[8];\n+        cpu_physical_memory_read(out_entry, entry, 8);\n+        uint32_t ctrl = le32_to_cpu(*(uint32_t *)entry);\n+\n+        if (ctrl & BDT_UOWN) {\n+            ctrl = 0x00000004u;   /* TOK_PID=OUT, UOWN=0, bc=0 */\n+            *(uint32_t *)entry = cpu_to_le32(ctrl);\n+            cpu_physical_memory_write(out_entry, entry, 8);\n+            usb_stat_fifo_push(s, (uint32_t)(ppbi ? 0x04u : 0x00u));\n+            usb_update_irq(s);\n+            return;\n+        }\n+    }\n+\n+    /* Neither armed — fire even anyway (fallback; should not happen) */\n+    uint8_t entry[8];\n+    cpu_physical_memory_read(bdt, entry, 8);\n+    *(uint32_t *)entry = cpu_to_le32(0x00000004u);\n+    cpu_physical_memory_write(bdt, entry, 8);\n+    usb_stat_fifo_push(s, USB_STAT_EP0_OUT_EVEN);\n+    usb_update_irq(s);\n+}\n+\n+/*\n+ * Chardev RX callbacks (CDC Host→Device path, EP2-OUT)\n+ * -----------------------------------------------------------------------\n+ */\n+\n+/*\n+ * Returns 64 (max bulk packet) when enumeration is done and EP2-OUT BDT\n+ * is armed by the firmware (UOWN=1). Returns 0 otherwise so the chardev\n+ * layer does not deliver data before the firmware is ready.\n+ */\n+static int usb_chr_can_receive(void *opaque)\n+{\n+    PIC32MKUSBState *s = opaque;\n+    if (s->ep0_sim != EP0_SIM_DONE) {\n+        return 0;\n+    }\n+    hwaddr bdt = usb_bdt_phys(s);\n+    if (!bdt) {\n+        return 0;\n+    }\n+    /* EP2-OUT-even = BDT+0x40, EP2-OUT-odd = BDT+0x48 */\n+    for (int ppbi = 0; ppbi < 2; ppbi++) {\n+        uint8_t entry[4];\n+        cpu_physical_memory_read(bdt + 0x40u + (ppbi ? 8u : 0u), entry, 4);\n+        uint32_t ctrl = le32_to_cpu(*(uint32_t *)entry);\n+        if (ctrl & BDT_UOWN) {\n+            return 64;\n+        }\n+    }\n+    return 0;\n+}\n+\n+/*\n+ * Called by the QEMU chardev layer when the host sends data over the PTY/socket.\n+ * Writes the payload into the firmware's EP2-OUT buffer, returns the BDT entry\n+ * to firmware (UOWN=0, TOK_PID=OUT, bc=n), and fires TRNIF so the Harmony ISR\n+ * picks it up via USB_ReadByte() → circular_buf_get().\n+ */\n+static void usb_chr_receive(void *opaque, const uint8_t *buf, int size)\n+{\n+    PIC32MKUSBState *s = opaque;\n+    hwaddr bdt = usb_bdt_phys(s);\n+    if (!bdt) {\n+        return;\n+    }\n+\n+    for (int ppbi = 0; ppbi < 2; ppbi++) {\n+        hwaddr ep2_out = bdt + 0x40u + (ppbi ? 8u : 0u);\n+        uint8_t entry[8];\n+        cpu_physical_memory_read(ep2_out, entry, 8);\n+\n+        uint32_t ctrl = le32_to_cpu(*(uint32_t *)entry);\n+        uint32_t addr = le32_to_cpu(*(uint32_t *)(entry + 4));\n+\n+        if (!(ctrl & BDT_UOWN) || !addr) {\n+            continue;   /* not armed — try other ping-pong */\n+        }\n+\n+        int n = MIN(size, 64);\n+        cpu_physical_memory_write(addr, buf, n);\n+\n+        /*\n+         * Return BDT entry to firmware:\n+         *   byte[0] = 0x04: TOK_PID=OUT(0x1) in bits[5:2], UOWN=0\n+         *   shortWord[1] = n: byte count received\n+         */\n+        ctrl = 0x00000004u | ((uint32_t)n << 16);\n+        *(uint32_t *)entry = cpu_to_le32(ctrl);\n+        cpu_physical_memory_write(ep2_out, entry, 8);\n+\n+        /* UxSTAT: EP=2(bits[7:4]=0x20), DIR=OUT(bit3=0), PPBI=ppbi(bit2) */\n+        usb_stat_fifo_push(s, 0x20u | (uint32_t)(ppbi ? 0x04u : 0x00u));\n+        usb_update_irq(s);\n+        return;\n+    }\n+}\n+\n+/*\n+ * Poll the CDC TX endpoint (EP2-IN) and the CDC notification endpoint (EP1-IN).\n+ * If the firmware has queued data (UOWN=1), drain it, release the BDT entry,\n+ * and fire TRNIF so the firmware can refill.\n+ *\n+ * EP1-IN (BDT+0x30): CDC Serial State Notification — data discarded (host-side\n+ * notification only), but BDT must be returned to prevent firmware stall.\n+ * EP2-IN (BDT+0x50): CDC bulk TX — data forwarded to chardev.\n+ */\n+static void usb_check_cdc_bdt(PIC32MKUSBState *s)\n+{\n+    hwaddr bdt = usb_bdt_phys(s);\n+    if (!bdt) {\n+        return;\n+    }\n+\n+    /*\n+     * EP1-IN = CDC Serial State Notification (interrupt IN, 16 bytes max).\n+     * BDT: EP1-IN-even = BDT+0x30, EP1-IN-odd = BDT+0x38.\n+     * Drain and return to firmware without forwarding to chardev.\n+     */\n+    for (int ppbi = 0; ppbi < 2; ppbi++) {\n+        hwaddr ep1_in = bdt + 0x30u + (ppbi ? 8u : 0u);\n+        uint8_t entry[8];\n+        cpu_physical_memory_read(ep1_in, entry, 8);\n+\n+        uint32_t ctrl = le32_to_cpu(*(uint32_t *)entry);\n+        if (!(ctrl & BDT_UOWN)) {\n+            continue;\n+        }\n+\n+        /* Return BDT entry: TOK_PID=IN, UOWN=0, preserve bc */\n+        ctrl = (ctrl & 0xFFFF0000u) | 0x24u;\n+        *(uint32_t *)entry = cpu_to_le32(ctrl);\n+        cpu_physical_memory_write(ep1_in, entry, 8);\n+\n+        /* UxSTAT: EP=1(bits[7:4]=0x10), DIR=IN(bit3=0x08), PPBI=ppbi(bit2) */\n+        usb_stat_fifo_push(s, 0x18u | (uint32_t)(ppbi ? 0x04u : 0x00u));\n+        usb_update_irq(s);\n+        break;   /* one notification per tick is enough */\n+    }\n+\n+    /*\n+     * EP2-IN = CDC bulk TX.  BDT: EP2-IN-even = BDT+0x50, EP2-IN-odd = BDT+0x58.\n+     * Forward data to chardev backend (PTY / socket / file).\n+     */\n+    for (int ppbi = 0; ppbi < 2; ppbi++) {\n+        hwaddr ep2_in = bdt + 0x50u + (ppbi ? 8u : 0u);\n+        uint8_t entry[8];\n+        cpu_physical_memory_read(ep2_in, entry, 8);\n+\n+        uint32_t ctrl = le32_to_cpu(*(uint32_t *)entry);\n+        if (!(ctrl & BDT_UOWN)) {\n+            continue;\n+        }\n+\n+        uint32_t bc   = ctrl >> 16;\n+        uint32_t addr = le32_to_cpu(*(uint32_t *)(entry + 4));\n+\n+        if (bc > 0 && addr) {\n+            uint8_t txbuf[64];\n+            bc = MIN(bc, sizeof(txbuf));\n+            cpu_physical_memory_read(addr, txbuf, bc);\n+            if (qemu_chr_fe_backend_connected(&s->chr)) {\n+                qemu_chr_fe_write_all(&s->chr, txbuf, bc);\n+            }\n+        }\n+\n+        /* Return BDT entry: TOK_PID=IN, UOWN=0, preserve bc */\n+        ctrl = (ctrl & 0xFFFF0000u) | 0x24u;\n+        *(uint32_t *)entry = cpu_to_le32(ctrl);\n+        cpu_physical_memory_write(ep2_in, entry, 8);\n+\n+        /* UxSTAT: EP=2(bits[7:4]=0x20), DIR=IN(bit3=0x08), PPBI=ppbi(bit2) */\n+        usb_stat_fifo_push(s, 0x28u | (uint32_t)(ppbi ? 0x04u : 0x00u));\n+        usb_update_irq(s);\n+        return;   /* drain one packet per tick; FIFO handles concurrency */\n+    }\n+}\n+\n+/*\n+ * EP0 enumeration timer\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void usb_timer_cb(void *opaque)\n+{\n+    PIC32MKUSBState *s = opaque;\n+\n+    /* Standard USB SETUP packets for enumeration sequence */\n+    static const uint8_t setup_get_dev_desc[8] = {\n+        0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 18, 0x00\n+    };  /* GET_DESCRIPTOR(Device, length=18) */\n+\n+    static const uint8_t setup_set_address[8] = {\n+        0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00\n+    };  /* SET_ADDRESS(1) */\n+\n+    static const uint8_t setup_get_cfg_desc[8] = {\n+        0x80, 0x06, 0x00, 0x02, 0x00, 0x00, 67, 0x00\n+    };  /* GET_DESCRIPTOR(Configuration, length=67) */\n+\n+    static const uint8_t setup_set_config[8] = {\n+        0x00, 0x09, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00\n+    };  /* SET_CONFIGURATION(1) */\n+\n+    switch (s->ep0_sim) {\n+\n+    case EP0_SIM_IDLE:\n+        /* Do nothing until USBPWR is set */\n+        break;\n+\n+    case EP0_SIM_RESET:\n+        /*\n+         * Wait until firmware enables URSTIE (bit 0) in UIE.\n+         * The UIE write handler fires URSTIF immediately when URSTIE is first\n+         * enabled (edge-detected).  This path is a poll fallback: if URSTIE\n+         * is already set by the time we arrive here, fire now; otherwise retry\n+         * every 5ms.  The 50ms initial delay usually isn't long enough for the\n+         * Harmony stack to complete Init→Attach→EnableInterrupts.\n+          */\n+        if (!(s->uie & USB_IR_URSTIF)) {\n+            /*\n+             * SESSION_VALID fallback: if the initial SESVDIF pulse was missed\n+             * (e.g. IEC not enabled yet when pulse fired), re-latch SESVDIF\n+             * every few retries so the EVIC can deliver the interrupt once\n+             * IEC is enabled.\n+              */\n+            s->sesvd_retry_count++;\n+            /*\n+             * Only re-latch if firmware has never acknowledged SESVDIF.\n+             * Once sesvd_acked is true, the Attach→URSTIE chain has\n+             * started and retries would cause an IRQ storm.\n+              */\n+            if (!s->sesvd_acked &&\n+                (s->sesvd_retry_count % 3) == 0 &&\n+                (s->otgstat & USB_OTG_IR_SESSION_VALID) &&\n+                (s->otgie & USB_OTG_IR_SESSION_VALID)) {\n+                s->otgir |= USB_OTG_IR_SESSION_VALID;\n+                s->sesvd_edge_latched = true;\n+                usb_update_irq(s);\n+            }\n+            timer_mod(s->usb_timer,\n+                      qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + 5 * SCALE_MS);\n+            return;\n+        }\n+        s->uir |= USB_IR_URSTIF;\n+        usb_update_irq(s);\n+        s->ep0_sim = EP0_SIM_GET_DEV_DESC;\n+        timer_mod(s->usb_timer,\n+                  qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + 10 * SCALE_MS);\n+        return;\n+\n+    case EP0_SIM_GET_DEV_DESC:\n+        if (usb_inject_setup(s, setup_get_dev_desc)) {\n+            s->ep0_sim = EP0_SIM_WAIT_DEV_DESC;\n+            timer_mod(s->usb_timer,\n+                      qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + 5 * SCALE_MS);\n+        } else {\n+            /* Retry every 2 ms until BDT is armed */\n+            timer_mod(s->usb_timer,\n+                      qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + 2 * SCALE_MS);\n+        }\n+        return;\n+\n+    case EP0_SIM_WAIT_DEV_DESC:\n+        /* Consume the EP0-IN response(s) — may be multi-packet (64+3 bytes) */\n+        if (usb_accept_ep0_in(s)) {\n+            /* Give firmware 2ms to re-arm IN or finish */\n+            timer_mod(s->usb_timer,\n+                      qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + 2 * SCALE_MS);\n+        } else {\n+            /* IN response not ready yet, or we just consumed last packet */\n+            usb_send_status_out(s);  /* status phase complete */\n+            s->ep0_sim = EP0_SIM_SET_ADDRESS;\n+            timer_mod(s->usb_timer,\n+                      qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + 5 * SCALE_MS);\n+        }\n+        return;\n+\n+    case EP0_SIM_SET_ADDRESS:\n+        if (usb_inject_setup(s, setup_set_address)) {\n+            s->uaddr = 1;   /* pretend host acknowledged new address */\n+            s->ep0_sim = EP0_SIM_WAIT_ADDRESS;\n+            timer_mod(s->usb_timer,\n+                      qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + 5 * SCALE_MS);\n+        } else {\n+            timer_mod(s->usb_timer,\n+                      qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + 2 * SCALE_MS);\n+        }\n+        return;\n+\n+    case EP0_SIM_WAIT_ADDRESS:\n+        /* Consume status-phase ZLP IN from SET_ADDRESS */\n+        if (usb_accept_ep0_in(s)) {\n+            s->ep0_sim = EP0_SIM_GET_CFG_DESC;\n+            timer_mod(s->usb_timer,\n+                      qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + 5 * SCALE_MS);\n+        } else {\n+            /* Retry */\n+            timer_mod(s->usb_timer,\n+                      qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + 2 * SCALE_MS);\n+        }\n+        return;\n+\n+    case EP0_SIM_GET_CFG_DESC:\n+        if (usb_inject_setup(s, setup_get_cfg_desc)) {\n+            s->ep0_sim = EP0_SIM_WAIT_CFG_DESC;\n+            timer_mod(s->usb_timer,\n+                      qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + 5 * SCALE_MS);\n+        } else {\n+            timer_mod(s->usb_timer,\n+                      qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + 2 * SCALE_MS);\n+        }\n+        return;\n+\n+    case EP0_SIM_WAIT_CFG_DESC:\n+        /* May be multi-packet; keep accepting until no more IN pending */\n+        if (usb_accept_ep0_in(s)) {\n+            timer_mod(s->usb_timer,\n+                      qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + 2 * SCALE_MS);\n+        } else {\n+            usb_send_status_out(s);  /* status phase */\n+            s->ep0_sim = EP0_SIM_SET_CONFIG;\n+            timer_mod(s->usb_timer,\n+                      qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + 5 * SCALE_MS);\n+        }\n+        return;\n+\n+    case EP0_SIM_SET_CONFIG:\n+        if (usb_inject_setup(s, setup_set_config)) {\n+            s->ep0_sim = EP0_SIM_WAIT_CONFIG;\n+            timer_mod(s->usb_timer,\n+                      qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + 5 * SCALE_MS);\n+        } else {\n+            timer_mod(s->usb_timer,\n+                      qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + 2 * SCALE_MS);\n+        }\n+        return;\n+\n+    case EP0_SIM_WAIT_CONFIG:\n+        /* Consume status-phase ZLP IN from SET_CONFIGURATION */\n+        if (usb_accept_ep0_in(s)) {\n+            s->configured = true;\n+            s->ep0_sim = EP0_SIM_DONE;\n+            qemu_log_mask(LOG_UNIMP, \"pic32mk_usb: USB enumeration DONE\\n\");\n+            timer_mod(s->usb_timer,\n+                      qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + 5 * SCALE_MS);\n+        } else {\n+            timer_mod(s->usb_timer,\n+                      qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + 2 * SCALE_MS);\n+        }\n+        return;\n+\n+    case EP0_SIM_DONE:\n+        /*\n+         * Poll CDC TX every 1 ms (was 5 ms — too slow for bootloader\n+         * throughput; each WRITE command needs a response drained from\n+         * EP2-IN before the firmware can process the next one).\n+          */\n+        usb_check_cdc_bdt(s);\n+        /* Advance the frame counter (1 frame = 1 ms at full-speed) */\n+        {\n+            uint32_t frm = ((s->ufrmh & 0x07u) << 8) | s->ufrml;\n+            frm = (frm + 1) & 0x7FFu;\n+            s->ufrml = frm & 0xFFu;\n+            s->ufrmh = (frm >> 8) & 0x07u;\n+        }\n+        timer_mod(s->usb_timer,\n+                  qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + 1 * SCALE_MS);\n+        return;\n+    }\n+}\n+\n+/*\n+ * MMIO read\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static uint64_t usb_read(void *opaque, hwaddr addr, unsigned size)\n+{\n+    PIC32MKUSBState *s = opaque;\n+    hwaddr base = addr & ~(hwaddr)0xFu;   /* strip sub-register bits */\n+\n+    switch (base) {\n+    /* OTG block */\n+    case PIC32MK_UxOTGIR:\n+        return s->otgir;\n+    case PIC32MK_UxOTGIE:\n+        return s->otgie;\n+    case PIC32MK_UxOTGSTAT:\n+        return s->otgstat;\n+    case PIC32MK_UxOTGCON:\n+        return s->otgcon;\n+    case PIC32MK_UxPWRC:\n+        return s->pwrc;\n+\n+    /* Core registers */\n+    case PIC32MK_UxIR:\n+        return s->uir;\n+    case PIC32MK_UxIE:\n+        return s->uie;\n+    case PIC32MK_UxEIR:\n+        return s->ueir;\n+    case PIC32MK_UxEIE:\n+        return s->ueie;\n+    case PIC32MK_UxSTAT:\n+        return s->ustat;\n+    case PIC32MK_UxCON:\n+        return s->ucon;\n+    case PIC32MK_UxADDR:\n+        return s->uaddr;\n+    case PIC32MK_UxBDTP1:\n+        return s->bdtp1;\n+    case PIC32MK_UxFRML:\n+        return s->ufrml;\n+    case PIC32MK_UxFRMH:\n+        return s->ufrmh;\n+    case PIC32MK_UxTOK:\n+        return s->utok;\n+    case PIC32MK_UxSOF:\n+        return s->usof;\n+    case PIC32MK_UxBDTP2:\n+        return s->bdtp2;\n+    case PIC32MK_UxBDTP3:\n+        return s->bdtp3;\n+    case PIC32MK_UxCNFG1:\n+        return s->cnfg1;\n+\n+    default:\n+        break;\n+    }\n+\n+    /* Endpoint control registers UxEP0–UxEP15 */\n+    if (base >= PIC32MK_UxEP_BASE &&\n+        base < PIC32MK_UxEP_BASE + PIC32MK_USB_NEPS * PIC32MK_UxEP_STRIDE) {\n+        unsigned ep = (base - PIC32MK_UxEP_BASE) / PIC32MK_UxEP_STRIDE;\n+        return s->uep[ep];\n+    }\n+\n+    qemu_log_mask(LOG_UNIMP,\n+                  \"pic32mk_usb: unimplemented read @ 0x%04\" HWADDR_PRIx \"\\n\",\n+                  addr);\n+    return 0;\n+}\n+\n+/*\n+ * MMIO write\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void usb_write(void *opaque, hwaddr addr, uint64_t val, unsigned size)\n+{\n+    PIC32MKUSBState *s = opaque;\n+    hwaddr base = addr & ~(hwaddr)0xFu;\n+    int    sub  = (int)(addr & 0xFu);\n+    uint32_t v  = (uint32_t)val;\n+\n+    switch (base) {\n+\n+    /* ----- OTG block ----- */\n+    case PIC32MK_UxOTGIR:\n+        /*\n+         * Clear the edge latch before W1C so that usb_update_irq() deasserts\n+         * the IRQ line when SESVDIF is cleared by firmware.\n+          */\n+        if (v & USB_OTG_IR_SESSION_VALID) {\n+            s->sesvd_edge_latched = false;\n+            s->sesvd_acked = true;   /* firmware processed SESSION_VALID */\n+        }\n+        apply_w1c(&s->otgir, v, sub);\n+        usb_update_irq(s);\n+        return;\n+\n+    case PIC32MK_UxOTGIE: {\n+        uint32_t old_otgie = s->otgie;\n+        apply_sci(&s->otgie, v, sub);\n+        /*\n+         * SESSION_VALID edge simulation:\n+         * Latch SESVDIF ONLY on the 0→1 transition of the SESSION_VALID enable\n+         * bit, and only if firmware hasn't already acknowledged it.  Without\n+         * edge detection, any OTGIE write (e.g. Harmony's Attach() enabling\n+         * all OTG ints from inside the SESSION_VALID ISR) would re-latch\n+         * SESVDIF and cause an infinite ISR loop.\n+          */\n+        if (!(old_otgie & USB_OTG_IR_SESSION_VALID) &&\n+            (s->otgie & USB_OTG_IR_SESSION_VALID) &&\n+            (s->otgstat & USB_OTG_IR_SESSION_VALID) &&\n+            !s->sesvd_acked) {\n+            s->otgir |= USB_OTG_IR_SESSION_VALID;\n+            s->sesvd_edge_latched = true;\n+            fprintf(stderr, \"pic32mk_usb: SESSION_VALID latched → IRQ (persistent)\\n\");\n+        }\n+        usb_update_irq(s);\n+        return;\n+    }\n+\n+    case PIC32MK_UxOTGSTAT:\n+        return;   /* read-only */\n+\n+    case PIC32MK_UxOTGCON:\n+        apply_sci(&s->otgcon, v, sub);\n+        return;\n+\n+    case PIC32MK_UxPWRC: {\n+        uint32_t old_pwrc = s->pwrc;\n+        apply_sci(&s->pwrc, v, sub);\n+        /* When USBPWR first asserted: schedule USB Reset after 50 ms */\n+        if ((s->pwrc & USB_PWRC_USBPWR) && !(old_pwrc & USB_PWRC_USBPWR)\n+            && s->ep0_sim == EP0_SIM_IDLE) {\n+            fprintf(stderr, \"pic32mk_usb: USBPWR set — scheduling USB Reset in 50ms\\n\");\n+            s->ep0_sim = EP0_SIM_RESET;\n+            timer_mod(s->usb_timer,\n+                      qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + 50 * SCALE_MS);\n+        }\n+        return;\n+    }\n+\n+    /* ----- Core interrupt registers ----- */\n+    case PIC32MK_UxIR:\n+        /*\n+         * TRNIF W1C: pop the USTAT FIFO.  On real PIC32MK hardware, clearing\n+         * TRNIF advances the 4-deep STAT FIFO.  If more entries remain,\n+         * TRNIF is re-asserted immediately with the next USTAT value, so the\n+         * Harmony ISR's while(TRNIF) loop processes all queued transactions.\n+         */\n+        if (v & USB_IR_TRNIF) {\n+            usb_stat_fifo_pop(s);\n+        }\n+        apply_w1c(&s->uir, v, sub);\n+        /*\n+         * Opportunistic CDC TX check: firmware just cleared TRNIF, meaning it\n+         * finished processing a transaction.  If it also armed EP2-IN (TX\n+         * response) during that processing, drain it NOW instead of waiting\n+         * for the next timer tick.  This brings TX latency close to zero.\n+         */\n+        if (s->configured) {\n+            usb_check_cdc_bdt(s);\n+        }\n+        usb_update_irq(s);\n+        return;\n+\n+    case PIC32MK_UxIE: {\n+        uint32_t old_uie = s->uie;\n+        apply_sci(&s->uie, v, sub);\n+        /*\n+         * When firmware enables URSTIE (bit 0) for the first time and the EP0\n+         * state machine is armed (USBPWR was set earlier), fire URSTIF now.\n+         * On real hardware the host drives USB Reset after D+ pullup is seen.\n+         * We use the same edge-detection pattern as SESSION_VALID: pulse assert\n+         * + deassert so EVIC latches IFS exactly once, then let usb_update_irq\n+         * handle the persistent URSTIF level (UIR & UIE != 0).\n+          */\n+        if (!(old_uie & USB_IR_URSTIF) && (s->uie & USB_IR_URSTIF)\n+            && s->ep0_sim == EP0_SIM_RESET) {\n+            fprintf(stderr, \"pic32mk_usb: URSTIE enabled → firing USB Reset\\n\");\n+            s->uir |= USB_IR_URSTIF;\n+        }\n+        usb_update_irq(s);\n+        return;\n+    }\n+\n+    case PIC32MK_UxEIR:\n+        apply_w1c(&s->ueir, v, sub);\n+        usb_update_irq(s);\n+        return;\n+\n+    case PIC32MK_UxEIE:\n+        apply_sci(&s->ueie, v, sub);\n+        usb_update_irq(s);\n+        return;\n+\n+    case PIC32MK_UxSTAT:\n+        return;   /* read-only */\n+\n+    case PIC32MK_UxCON:\n+        apply_sci(&s->ucon, v, sub);\n+        return;\n+\n+    case PIC32MK_UxADDR:\n+        apply_sci(&s->uaddr, v, sub);\n+        return;\n+\n+    case PIC32MK_UxBDTP1:\n+        apply_sci(&s->bdtp1, v, sub);\n+        return;\n+\n+    case PIC32MK_UxFRML:\n+    case PIC32MK_UxFRMH:\n+        return;   /* read-only */\n+\n+    case PIC32MK_UxTOK:\n+        apply_sci(&s->utok, v, sub);\n+        return;\n+\n+    case PIC32MK_UxSOF:\n+        apply_sci(&s->usof, v, sub);\n+        return;\n+\n+    case PIC32MK_UxBDTP2:\n+        apply_sci(&s->bdtp2, v, sub);\n+        return;\n+\n+    case PIC32MK_UxBDTP3:\n+        apply_sci(&s->bdtp3, v, sub);\n+        return;\n+\n+    case PIC32MK_UxCNFG1:\n+        apply_sci(&s->cnfg1, v, sub);\n+        return;\n+\n+    default:\n+        break;\n+    }\n+\n+    /* Endpoint control registers */\n+    if (base >= PIC32MK_UxEP_BASE &&\n+        base < PIC32MK_UxEP_BASE + PIC32MK_USB_NEPS * PIC32MK_UxEP_STRIDE) {\n+        unsigned ep = (base - PIC32MK_UxEP_BASE) / PIC32MK_UxEP_STRIDE;\n+        apply_sci(&s->uep[ep], v, sub);\n+        return;\n+    }\n+\n+    qemu_log_mask(LOG_UNIMP,\n+                  \"pic32mk_usb: unimplemented write @ 0x%04\" HWADDR_PRIx\n+                  \" = 0x%08x\\n\", addr, v);\n+}\n+\n+static const MemoryRegionOps usb_ops = {\n+    .read       = usb_read,\n+    .write      = usb_write,\n+    .endianness = DEVICE_LITTLE_ENDIAN,\n+    .valid = {\n+        .min_access_size = 1,\n+        .max_access_size = 4,\n+    },\n+};\n+\n+/*\n+ * Device lifecycle\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void pic32mk_usb_reset(DeviceState *dev)\n+{\n+    PIC32MKUSBState *s = PIC32MK_USB(dev);\n+\n+    /* Reset values: all zero unless specified in DS60001519E §25 */\n+    s->otgir   = 0;\n+    s->otgie   = 0;\n+    /*\n+     * SESVD (bit 3): simulate VBUS present from power-on.\n+     * The Harmony driver polls PLIB_USB_OTG_SessionValid() (reads SESVD) to\n+     * decide whether to call USB_DEVICE_Attach().  If SESVD=0 at boot the\n+     * driver never calls Attach(), USBPWR is never set, and our timer-based\n+     * EP0 simulation never starts — a permanent deadlock.  Pre-setting SESVD\n+     * breaks the cycle: firmware sees VBUS valid, calls Attach(), sets USBPWR,\n+     * and our EP0 timer fires as expected.\n+      */\n+    s->otgstat = 0x08u;\n+    s->otgcon  = 0;\n+    s->pwrc    = 0;\n+    s->uir     = 0;\n+    s->uie     = 0;\n+    s->ueir    = 0;\n+    s->ueie    = 0;\n+    s->ustat   = 0;\n+    s->ucon    = 0;\n+    s->uaddr   = 0;\n+    s->bdtp1   = 0;\n+    s->bdtp2   = 0;\n+    s->bdtp3   = 0;\n+    s->ufrml   = 0;\n+    s->ufrmh   = 0;\n+    s->utok    = 0;\n+    s->usof    = 0x4Bu;\n+    s->cnfg1   = 0;\n+    memset(s->uep, 0, sizeof(s->uep));\n+\n+    s->ep0_sim   = EP0_SIM_IDLE;\n+    s->configured = false;\n+    s->sesvd_edge_latched = false;\n+    s->sesvd_acked = false;\n+    s->sesvd_retry_count = 0;\n+\n+    /* USTAT FIFO reset */\n+    memset(s->stat_fifo, 0, sizeof(s->stat_fifo));\n+    s->stat_fifo_head  = 0;\n+    s->stat_fifo_tail  = 0;\n+    s->stat_fifo_count = 0;\n+\n+    if (s->usb_timer) {\n+        timer_del(s->usb_timer);\n+    }\n+\n+    qemu_set_irq(s->irq, 0);\n+}\n+\n+static void pic32mk_usb_init(Object *obj)\n+{\n+    PIC32MKUSBState *s = PIC32MK_USB(obj);\n+\n+    memory_region_init_io(&s->sfr_mmio, obj, &usb_ops, s,\n+                          TYPE_PIC32MK_USB, PIC32MK_USB_SFR_SIZE);\n+    sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->sfr_mmio);\n+    sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq);\n+\n+    s->usb_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, usb_timer_cb, s);\n+    s->ep0_sim   = EP0_SIM_IDLE;\n+    s->configured = false;\n+    s->sesvd_edge_latched = false;\n+    s->sesvd_acked = false;\n+    s->sesvd_retry_count = 0;\n+    s->stat_fifo_head  = 0;\n+    s->stat_fifo_tail  = 0;\n+    s->stat_fifo_count = 0;\n+}\n+\n+static void pic32mk_usb_realize(DeviceState *dev, Error **errp)\n+{\n+    PIC32MKUSBState *s = PIC32MK_USB(dev);\n+    if (qemu_chr_fe_backend_connected(&s->chr)) {\n+        qemu_chr_fe_set_handlers(&s->chr,\n+                                 usb_chr_can_receive,\n+                                 usb_chr_receive,\n+                                 NULL, NULL,\n+                                 s, NULL, true);\n+    }\n+}\n+\n+static const Property pic32mk_usb_properties[] = {\n+    DEFINE_PROP_CHR(\"chardev\", PIC32MKUSBState, chr),\n+};\n+\n+static void pic32mk_usb_class_init(ObjectClass *klass, const void *data)\n+{\n+    DeviceClass *dc = DEVICE_CLASS(klass);\n+    device_class_set_legacy_reset(dc, pic32mk_usb_reset);\n+    device_class_set_props(dc, pic32mk_usb_properties);\n+    dc->realize = pic32mk_usb_realize;\n+    dc->desc = \"PIC32MK USB OTG Full-Speed\";\n+}\n+\n+static const TypeInfo pic32mk_usb_info = {\n+    .name          = TYPE_PIC32MK_USB,\n+    .parent        = TYPE_SYS_BUS_DEVICE,\n+    .instance_size = sizeof(PIC32MKUSBState),\n+    .instance_init = pic32mk_usb_init,\n+    .class_init    = pic32mk_usb_class_init,\n+};\n+\n+static void pic32mk_usb_register_types(void)\n+{\n+    type_register_static(&pic32mk_usb_info);\n+}\n+\n+type_init(pic32mk_usb_register_types)\ndiff --git a/hw/mips/pic32mk_wdt.c b/hw/mips/pic32mk_wdt.c\nnew file mode 100644\nindex 0000000000..93d61bf55f\n--- /dev/null\n+++ b/hw/mips/pic32mk_wdt.c\n@@ -0,0 +1,230 @@\n+/*\n+ * Microchip PIC32MK — Watchdog Timer (WDT)\n+ * Datasheet: DS60001519E §17, Register 17-1\n+ *\n+ * Models the WDTCON register, the clear-key mechanism (write 0x5743\n+ * to upper 16 bits via 16-bit write to WDTCON+2), and a real countdown\n+ * timer that triggers a system reset if firmware fails to clear the\n+ * WDT in time.\n+ *\n+ * WDTCON layout:\n+ *   bits[31:16] WDTCLRKEY  — write 0x5743 here to clear the counter\n+ *   bit  15     ON         — WDT enable\n+ *   bits[12:8]  RUNDIV     — read-only post-scaler (reflects config bits)\n+ *   bits[5:1]   SLPDIV     — read-only post-scaler (reflects config bits)\n+ *   bit  0      WDTWINEN   — windowed mode enable\n+ *\n+ * The timeout period is derived from the LPRC (32 kHz) oscillator\n+ * and the RUNDIV post-scaler.  Default RUNDIV=0 ≈ 1 ms base period,\n+ * giving a ~1 s timeout with the default post-scaler (WDTPS ≈ 20).\n+ * In emulation we use a configurable period (default 2 s) that can\n+ * be tuned via the \"timeout-ms\" qdev property.\n+ *\n+ * Copyright (c) 2026 QEMU contributors\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+#include \"qemu/log.h\"\n+#include \"qemu/module.h\"\n+#include \"qemu/timer.h\"\n+#include \"hw/core/sysbus.h\"\n+#include \"hw/core/qdev-properties.h\"\n+#include \"hw/mips/pic32mk.h\"\n+#include \"system/runstate.h\"\n+\n+/*\n+ * WDTCON register bit positions (DS60001519E §17, Register 17-1)\n+ * -----------------------------------------------------------------------\n+ */\n+#define WDTCON_ON       (1u << 15)  /* WDT enable */\n+#define WDTCON_WDTWINEN (1u << 0)   /* Windowed mode enable — bit 0 */\n+#define WDTCON_WR_MASK  (WDTCON_ON | WDTCON_WDTWINEN)  /* writable bits */\n+\n+/* Upper-halfword clear key written by firmware WDT_Clear() */\n+#define WDT_CLRKEY      0x5743u\n+\n+/* Default timeout in milliseconds (~2 s, conservative for emulation) */\n+#define WDT_DEFAULT_TIMEOUT_MS  2000\n+\n+#define TYPE_PIC32MK_WDT \"pic32mk-wdt\"\n+OBJECT_DECLARE_SIMPLE_TYPE(PIC32MKWDTState, PIC32MK_WDT)\n+\n+typedef struct PIC32MKWDTState {\n+    SysBusDevice parent_obj;\n+    MemoryRegion  mmio;\n+    uint32_t      wdtcon;       /* WDTCON register value */\n+    QEMUTimer     timer;        /* Countdown timer */\n+    uint32_t      timeout_ms;   /* Timeout period (ms), qdev property */\n+} PIC32MKWDTState;\n+\n+/*\n+ * Timer helpers\n+ * -----------------------------------------------------------------------\n+ */\n+\n+static void pic32mk_wdt_rearm(PIC32MKWDTState *s)\n+{\n+    int64_t now = qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL);\n+    timer_mod(&s->timer, now + s->timeout_ms);\n+}\n+\n+static void pic32mk_wdt_stop(PIC32MKWDTState *s)\n+{\n+    timer_del(&s->timer);\n+}\n+\n+static void pic32mk_wdt_update(PIC32MKWDTState *s, uint32_t old)\n+{\n+    bool was_on = (old & WDTCON_ON) != 0;\n+    bool is_on  = (s->wdtcon & WDTCON_ON) != 0;\n+\n+    if (is_on && !was_on) {\n+        /* Turning ON — start countdown */\n+        pic32mk_wdt_rearm(s);\n+    } else if (!is_on && was_on) {\n+        /* Turning OFF — stop countdown */\n+        pic32mk_wdt_stop(s);\n+    }\n+}\n+\n+/* Timer callback — WDT expired, trigger system reset */\n+static void pic32mk_wdt_expire(void *opaque)\n+{\n+    PIC32MKWDTState *s = PIC32MK_WDT(opaque);\n+\n+    if (!(s->wdtcon & WDTCON_ON)) {\n+        return; /* Race: turned off between schedule and callback */\n+    }\n+\n+    qemu_log_mask(LOG_GUEST_ERROR,\n+                  \"pic32mk-wdt: watchdog timeout — system reset\\n\");\n+    qemu_system_reset_request(SHUTDOWN_CAUSE_GUEST_RESET);\n+}\n+\n+/*\n+ * Read handler — WDTCON + SET/CLR/INV aliases\n+ * -----------------------------------------------------------------------\n+ */\n+static uint64_t pic32mk_wdt_read(void *opaque, hwaddr addr, unsigned size)\n+{\n+    PIC32MKWDTState *s = PIC32MK_WDT(opaque);\n+\n+    switch (addr & ~0x3u) {\n+    case 0x00:\n+        /* WDTCON (base, +4 SET, +8 CLR, +C INV all read same) */;\n+    case 0x04:\n+    case 0x08:\n+    case 0x0C:\n+        return s->wdtcon;\n+    default:\n+        qemu_log_mask(LOG_UNIMP,\n+                      \"pic32mk-wdt: unimplemented read @ 0x%\" HWADDR_PRIx \"\\n\",\n+                      addr);\n+        return 0;\n+    }\n+}\n+\n+/*\n+ * Write handler — WDTCON + SET/CLR/INV; clear-key on upper 16 bits\n+ * -----------------------------------------------------------------------\n+ */\n+static void pic32mk_wdt_write(void *opaque, hwaddr addr, uint64_t val,\n+                              unsigned size)\n+{\n+    PIC32MKWDTState *s = PIC32MK_WDT(opaque);\n+    uint32_t old = s->wdtcon;\n+\n+    /*\n+     * firmware WDT_Clear() does a 16-bit write of 0x5743 to WDTCON+2\n+     * (the upper halfword).  Detect by: addr==2, size==2, val==0x5743.\n+     * Restart the countdown timer.\n+     */\n+    if (addr == 2 && size == 2 && (uint16_t)val == WDT_CLRKEY) {\n+        if (s->wdtcon & WDTCON_ON) {\n+            pic32mk_wdt_rearm(s);\n+        }\n+        return;\n+    }\n+\n+    switch (addr & 0xCu) {\n+    case 0x0:\n+        /* WDTCON write */;\n+        s->wdtcon = (uint32_t)val & WDTCON_WR_MASK;\n+        break;\n+    case 0x4:\n+        /* SET */;\n+        s->wdtcon |= (uint32_t)val & WDTCON_WR_MASK;\n+        break;\n+    case 0x8:\n+        /* CLR */;\n+        s->wdtcon &= ~((uint32_t)val & WDTCON_WR_MASK);\n+        break;\n+    case 0xC:\n+        /* INV */;\n+        s->wdtcon ^= (uint32_t)val & WDTCON_WR_MASK;\n+        break;\n+    }\n+\n+    pic32mk_wdt_update(s, old);\n+}\n+\n+static const MemoryRegionOps pic32mk_wdt_ops = {\n+    .read       = pic32mk_wdt_read,\n+    .write      = pic32mk_wdt_write,\n+    .endianness = DEVICE_LITTLE_ENDIAN,\n+    .valid = {\n+        .min_access_size = 2,\n+        .max_access_size = 4,\n+    },\n+    .impl = {\n+        .min_access_size = 2,\n+        .max_access_size = 4,\n+    },\n+};\n+\n+static void pic32mk_wdt_realize(DeviceState *dev, Error **errp)\n+{\n+    PIC32MKWDTState *s = PIC32MK_WDT(dev);\n+    memory_region_init_io(&s->mmio, OBJECT(s), &pic32mk_wdt_ops, s,\n+                          \"pic32mk-wdt\", PIC32MK_WDT_SIZE);\n+    sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->mmio);\n+    timer_init_ms(&s->timer, QEMU_CLOCK_VIRTUAL,\n+                  pic32mk_wdt_expire, s);\n+}\n+\n+static void pic32mk_wdt_reset_hold(Object *obj, ResetType type)\n+{\n+    PIC32MKWDTState *s = PIC32MK_WDT(obj);\n+    timer_del(&s->timer);\n+    s->wdtcon = 0;\n+}\n+\n+static const Property pic32mk_wdt_properties[] = {\n+    DEFINE_PROP_UINT32(\"timeout-ms\", PIC32MKWDTState, timeout_ms,\n+                       WDT_DEFAULT_TIMEOUT_MS),\n+};\n+\n+static void pic32mk_wdt_class_init(ObjectClass *klass, const void *data)\n+{\n+    DeviceClass *dc = DEVICE_CLASS(klass);\n+    ResettableClass *rc = RESETTABLE_CLASS(klass);\n+    dc->realize = pic32mk_wdt_realize;\n+    dc->desc    = \"PIC32MK Watchdog Timer\";\n+    device_class_set_props(dc, pic32mk_wdt_properties);\n+    rc->phases.hold = pic32mk_wdt_reset_hold;\n+}\n+\n+static const TypeInfo pic32mk_wdt_info = {\n+    .name          = TYPE_PIC32MK_WDT,\n+    .parent        = TYPE_SYS_BUS_DEVICE,\n+    .instance_size = sizeof(PIC32MKWDTState),\n+    .class_init    = pic32mk_wdt_class_init,\n+};\n+\n+static void pic32mk_wdt_register_types(void)\n+{\n+    type_register_static(&pic32mk_wdt_info);\n+}\n+\n+type_init(pic32mk_wdt_register_types)\ndiff --git a/include/hw/mips/pic32mk.h b/include/hw/mips/pic32mk.h\nnew file mode 100644\nindex 0000000000..e1317183d0\n--- /dev/null\n+++ b/include/hw/mips/pic32mk.h\n@@ -0,0 +1,952 @@\n+/*\n+ * PIC32MK GPK/MCM with CAN FD — shared constants\n+ * Datasheet: DS60001519E\n+ *\n+ * Copyright (c) 2026 QEMU contributors\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#ifndef HW_MIPS_PIC32MK_H\n+#define HW_MIPS_PIC32MK_H\n+\n+/*\n+ * Physical memory map (§4, pp. 70-73)\n+ * -----------------------------------------------------------------------\n+ */\n+\n+#define PIC32MK_RAM_BASE        0x00000000u   /* 256 KB SRAM */\n+#define PIC32MK_RAM_SIZE        (256 * 1024)\n+\n+#define PIC32MK_PFLASH_BASE     0x1D000000u   /* 1 MB Program Flash */\n+#define PIC32MK_PFLASH_SIZE     (1 * 1024 * 1024)\n+\n+/*\n+ * Boot vector ROM — fills the gap between the MIPS reset vector (0x1FC00000)\n+ * and Boot Flash 1 (0x1FC40000). Contains a trampoline that jumps to BFlash1.\n+ */\n+#define PIC32MK_BOOTVEC_BASE    0x1FC00000u   /* physical reset vector */\n+#define PIC32MK_BOOTVEC_SIZE    0x00040000u   /* 256 KB gap to BFlash1 */\n+\n+#define PIC32MK_BFLASH1_BASE    0x1FC40000u   /* Boot Flash 1 — enlarged for firmware */\n+#define PIC32MK_BFLASH1_SIZE    (256 * 1024)\n+\n+#define PIC32MK_BFLASH2_BASE    0x1FC60000u   /* Boot Flash 2 ~20 KB */\n+#define PIC32MK_BFLASH2_SIZE    (20 * 1024)\n+\n+#define PIC32MK_SFR_BASE        0x1F800000u   /* SFR window, 1 MB */\n+#define PIC32MK_SFR_SIZE        (1 * 1024 * 1024)\n+\n+/* Reset vector (KSEG1 uncached alias of 0x1FC00000) */\n+#define PIC32MK_RESET_VECTOR    0xBFC00000u\n+\n+/*\n+ * SFR sub-block bases (offsets within the 1 MB SFR window, Table 4-2)\n+ * All addresses below are KSEG1 virtual (0xBF800000 + offset).\n+ * -----------------------------------------------------------------------\n+ */\n+\n+/* CFG / PMD block (0xBF800000)                                           */\n+#define PIC32MK_CFG_OFFSET      0x000000u\n+#define PIC32MK_CFG_SIZE        0x000900u   /* CFGCON..CFGCON2+INV + CHECON@0x800 */\n+\n+/* CRU — Clock Reference Unit (0xBF801200)                                 */\n+#define PIC32MK_CRU_OFFSET      0x001200u\n+#define PIC32MK_CRU_SIZE        0x0001A0u   /* OSCCON..CLKSTAT+INV inclusive */\n+\n+/* PPS (Peripheral Pin Select) input/output registers (0xBF801400–0xBF8017FF) */\n+#define PIC32MK_PPS_OFFSET      0x001400u\n+#define PIC32MK_PPS_SIZE        0x000400u\n+\n+/* WDT register block (0xBF800C00) */\n+#define PIC32MK_WDT_OFFSET      0x000C00u\n+#define PIC32MK_WDT_SIZE        0x000040u   /* WDTCON + SET/CLR/INV + padding */\n+\n+/* EVIC                                         0xBF810000 */\n+#define PIC32MK_EVIC_OFFSET     0x010000u\n+\n+/* DMA (shares page with EVIC)                  0xBF811000 */\n+#define PIC32MK_DMA_OFFSET      0x011000u\n+\n+/* Timers / IC / OC / I2C1-2 / SPI1-2 / UART1-2 / PWM / QEI / CMP / CDAC1 */\n+#define PIC32MK_PER1_OFFSET     0x020000u   /* 0xBF820000, size 0xD000 */\n+\n+/* I2C3-4 / SPI3-6 / UART3-6 / CDAC2-3         0xBF840000 */\n+#define PIC32MK_PER2_OFFSET     0x040000u\n+\n+/* GPIO PORTA-PORTG                             0xBF860000 */\n+#define PIC32MK_GPIO_OFFSET     0x060000u\n+\n+/* CAN1-4 / ADC                                 0xBF880000 */\n+#define PIC32MK_CAN_OFFSET      0x080000u\n+\n+/*\n+ * CAN1–4 SFR offsets from SFR base\n+ * Verified against DS60001519E / p32mk1024mcm100.h:\n+ *   CFD1CON @ 0xBF880000, CFD2CON @ 0xBF881000,\n+ *   CFD3CON @ 0xBF884000, CFD4CON @ 0xBF885000\n+ */\n+#define PIC32MK_CAN1_OFFSET     0x080000u   /* physical 0x1F880000 */\n+#define PIC32MK_CAN2_OFFSET     0x081000u   /* physical 0x1F881000 */\n+#define PIC32MK_CAN3_OFFSET     0x084000u   /* physical 0x1F884000 */\n+#define PIC32MK_CAN4_OFFSET     0x085000u   /* physical 0x1F885000 */\n+#define PIC32MK_CAN_SFR_SIZE    0x1000u     /* 4 KB SFR block per instance */\n+\n+/*\n+ * Message RAM physical base per instance.\n+ * Allocated just above the SFR window, beyond the ADC / USB blocks.\n+ * ⚠  Verify exact addresses from DS60001519E §4 before final integration.\n+ * Each instance gets 76 KB (75 KB data + alignment).\n+ */\n+#define PIC32MK_CAN1_MSGRAM_BASE 0x1F900000u\n+#define PIC32MK_CAN2_MSGRAM_BASE 0x1F913000u\n+#define PIC32MK_CAN3_MSGRAM_BASE 0x1F926000u\n+#define PIC32MK_CAN4_MSGRAM_BASE 0x1F939000u\n+#define PIC32MK_CAN_MSGRAM_SIZE  (75u * 1024u)  /* max 74 KB, rounded up */\n+\n+/*\n+ * USB OTG 1   _USB_BASE_ADDRESS = 0xBF889040 (p32mk1024mcm100.h / xc.h)\n+ * struct usb_registers_t pointer starts at 0xBF889040 (= device_base + 0x040).\n+ * Device MMIO window starts at 0xBF889000 so UxOTGIR lands at offset 0x040.\n+ */\n+#define PIC32MK_USB1_OFFSET     0x089000u\n+#define PIC32MK_USB_OFFSET      PIC32MK_USB1_OFFSET   /* legacy alias */\n+\n+/* USB OTG 2   _USB2_BASE_ADDRESS = 0xBF88A040 */\n+#define PIC32MK_USB2_OFFSET     0x08A000u\n+\n+/* RTCC                                         0xBF8C0000 */\n+#define PIC32MK_RTCC_OFFSET     0x0C0000u\n+\n+/*\n+ * Reset-control registers (§7, p. 114) — offsets from SFR base\n+ * -----------------------------------------------------------------------\n+ */\n+\n+#define PIC32MK_RCON_OFFSET     0x001240u   /* Reset Control */\n+#define PIC32MK_RSWRST_OFFSET   0x001250u   /* Software Reset trigger */\n+#define PIC32MK_RNMICON_OFFSET  0x001260u   /* NMI Control */\n+#define PIC32MK_PWRCON_OFFSET   0x001270u   /* Power Control */\n+\n+/* RCON reset flags (§7 register map) */\n+#define PIC32MK_RCON_POR        (1u << 2)   /* Power-on Reset */\n+#define PIC32MK_RCON_BOR        (1u << 3)   /* Brown-out Reset */\n+\n+/*\n+ * CFG / PMD registers (§6) — offsets from PIC32MK_CFG_OFFSET (0xBF800000)\n+ * -----------------------------------------------------------------------\n+ */\n+\n+#define PIC32MK_CFGCON          0x0000u     /* Configuration control */\n+#define PIC32MK_SYSKEY          0x0030u     /* System key unlock */\n+#define PIC32MK_PMD1            0x0040u     /* Peripheral Module Disable 1 */\n+#define PIC32MK_PMD2            0x0050u\n+#define PIC32MK_PMD3            0x0060u\n+#define PIC32MK_PMD4            0x0070u\n+#define PIC32MK_PMD5            0x0080u\n+#define PIC32MK_PMD6            0x0090u\n+#define PIC32MK_PMD7            0x00A0u\n+#define PIC32MK_CFGCON2         0x0110u     /* Extended configuration control */\n+#define PIC32MK_CHECON          0x0800u     /* Prefetch Cache Control (CHECON) */\n+\n+/* CFGCON bits */\n+#define PIC32MK_CFGCON_PGLOCK   (1u << 28)  /* Permission-group lock */\n+#define PIC32MK_CFGCON_PMDLOCK  (1u << 29)  /* PMD lock */\n+#define PIC32MK_CFGCON_IOLOCK   (1u << 30)  /* I/O lock */\n+\n+/* Number of PMD registers */\n+#define PIC32MK_PMD_COUNT       7\n+\n+/*\n+ * CRU registers (§9) — offsets from PIC32MK_CRU_OFFSET (0xBF801200)\n+ * Each register has SET/CLR/INV aliases at +4/+8/+C.\n+ * -----------------------------------------------------------------------\n+ */\n+\n+/* Oscillator / PLL registers */\n+#define PIC32MK_CRU_OSCCON      0x00u       /* Oscillator Control */\n+#define PIC32MK_CRU_OSCTUN      0x10u       /* Oscillator Tuning */\n+#define PIC32MK_CRU_SPLLCON     0x20u       /* System PLL Control */\n+#define PIC32MK_CRU_UPLLCON     0x30u       /* USB PLL Control */\n+\n+/* Reset-control registers (formerly in separate RCON stub) */\n+#define PIC32MK_CRU_RCON        0x40u       /* Reset Control */\n+#define PIC32MK_CRU_RSWRST      0x50u       /* Software Reset trigger */\n+#define PIC32MK_CRU_RNMICON     0x60u       /* NMI Control */\n+#define PIC32MK_CRU_PWRCON      0x70u       /* Power Control */\n+\n+/* Reference clock outputs 1–4 (CON + TRIM pairs, 0x20 stride) */\n+#define PIC32MK_CRU_REFO1CON    0x80u\n+#define PIC32MK_CRU_REFO1TRIM   0x90u\n+#define PIC32MK_CRU_REFO2CON    0xA0u\n+#define PIC32MK_CRU_REFO2TRIM   0xB0u\n+#define PIC32MK_CRU_REFO3CON    0xC0u\n+#define PIC32MK_CRU_REFO3TRIM   0xD0u\n+#define PIC32MK_CRU_REFO4CON    0xE0u\n+#define PIC32MK_CRU_REFO4TRIM   0xF0u\n+\n+/* Peripheral bus clock dividers 1–7 (0x10 stride) */\n+#define PIC32MK_CRU_PB1DIV      0x100u\n+#define PIC32MK_CRU_PB2DIV      0x110u\n+#define PIC32MK_CRU_PB3DIV      0x120u\n+#define PIC32MK_CRU_PB4DIV      0x130u\n+#define PIC32MK_CRU_PB5DIV      0x140u\n+#define PIC32MK_CRU_PB6DIV      0x150u\n+#define PIC32MK_CRU_PB7DIV      0x160u\n+\n+/* Clock status */\n+#define PIC32MK_CRU_CLKSTAT     0x190u\n+\n+/* Number of reference clocks and peripheral buses */\n+#define PIC32MK_CRU_NREFO       4\n+#define PIC32MK_CRU_NPB         7\n+\n+/* OSCCON bits (§9, Register 9-1) */\n+#define PIC32MK_OSCCON_OSWEN    (1u << 0)\n+#define PIC32MK_OSCCON_SOSCEN   (1u << 1)\n+#define PIC32MK_OSCCON_CF       (1u << 3)\n+#define PIC32MK_OSCCON_SLPEN    (1u << 4)\n+#define PIC32MK_OSCCON_CLKLOCK  (1u << 7)\n+#define PIC32MK_OSCCON_NOSC_MASK  0x00000700u   /* bits [10:8] */\n+#define PIC32MK_OSCCON_NOSC_SHIFT 8\n+#define PIC32MK_OSCCON_COSC_MASK  0x00007000u   /* bits [14:12] */\n+#define PIC32MK_OSCCON_COSC_SHIFT 12\n+#define PIC32MK_OSCCON_FRCDIV_MASK  0x07000000u /* bits [26:24] */\n+#define PIC32MK_OSCCON_FRCDIV_SHIFT 24\n+\n+/* SPLLCON bits (§9, Register 9-4) */\n+#define PIC32MK_SPLLCON_PLLRANGE_MASK  0x00000007u /* bits [2:0] */\n+#define PIC32MK_SPLLCON_PLLICLK  (1u << 7)\n+#define PIC32MK_SPLLCON_PLLIDIV_MASK   0x00000700u /* bits [10:8] */\n+#define PIC32MK_SPLLCON_PLLMULT_MASK   0x007F0000u /* bits [22:16] */\n+#define PIC32MK_SPLLCON_PLLODIV_MASK   0x07000000u /* bits [26:24] */\n+\n+/* UPLLCON bits (§9) — same layout as SPLLCON plus UPOSCEN */\n+#define PIC32MK_UPLLCON_UPOSCEN  (1u << 25)\n+\n+/* REFOxCON bits (§9, Register 9-16) */\n+#define PIC32MK_REFOCON_ROSEL_MASK  0x0000000Fu /* bits [3:0] */\n+#define PIC32MK_REFOCON_ACTIVE   (1u << 8)\n+#define PIC32MK_REFOCON_DIVSWEN  (1u << 9)\n+#define PIC32MK_REFOCON_RSLP     (1u << 11)\n+#define PIC32MK_REFOCON_OE       (1u << 12)\n+#define PIC32MK_REFOCON_SIDL     (1u << 13)\n+#define PIC32MK_REFOCON_ON       (1u << 15)\n+#define PIC32MK_REFOCON_RODIV_MASK  0xFFFF0000u /* bits [31:16] */\n+\n+/* PBxDIV bits (§9) */\n+#define PIC32MK_PBDIV_PBDIV_MASK  0x0000007Fu  /* bits [6:0] */\n+#define PIC32MK_PBDIV_PBDIVRDY   (1u << 11)\n+#define PIC32MK_PBDIV_ON         (1u << 15)\n+\n+/* CLKSTAT bits (§9, Register 9-31) */\n+#define PIC32MK_CLKSTAT_FRCRDY   (1u << 0)\n+#define PIC32MK_CLKSTAT_POSCRDY  (1u << 2)\n+#define PIC32MK_CLKSTAT_SOSCRDY  (1u << 4)\n+#define PIC32MK_CLKSTAT_LPRCRDY  (1u << 5)\n+#define PIC32MK_CLKSTAT_SPLLRDY  (1u << 7)\n+#define PIC32MK_CLKSTAT_UPLLRDY  (1u << 8)\n+#define PIC32MK_CLKSTAT_ALL_RDY  (PIC32MK_CLKSTAT_FRCRDY  | \\\n+                                  PIC32MK_CLKSTAT_POSCRDY  | \\\n+                                  PIC32MK_CLKSTAT_SOSCRDY  | \\\n+                                  PIC32MK_CLKSTAT_LPRCRDY  | \\\n+                                  PIC32MK_CLKSTAT_SPLLRDY  | \\\n+                                  PIC32MK_CLKSTAT_UPLLRDY)\n+\n+/*\n+ * EVIC registers (§8, p. 159) — offsets from 0xBF810000\n+ * -----------------------------------------------------------------------\n+ */\n+\n+#define PIC32MK_EVIC_INTCON     0x0000u     /* MVEC, TPC, INTxEP */\n+#define PIC32MK_EVIC_PRISS      0x0010u     /* Priority shadow reg select */\n+#define PIC32MK_EVIC_INTSTAT    0x0020u     /* Last IRQ serviced, SRIPL */\n+#define PIC32MK_EVIC_IPTMR      0x0030u     /* Interrupt proximity timer */\n+#define PIC32MK_EVIC_IFS0       0x0040u     /* Interrupt flag status [0..7] */\n+#define PIC32MK_EVIC_IEC0       0x00C0u     /* Interrupt enable control [0..7] */\n+#define PIC32MK_EVIC_IPC0       0x0140u     /* Interrupt priority control [0..63] */\n+#define PIC32MK_EVIC_OFF0       0x0540u     /* Vector address offsets [0..190] */\n+\n+/*\n+ * Number of interrupt sources / vectors.\n+ * The EVIC IFS/IEC registers span 8×32 = 256 bits; USB1 (244) and USB2 (246)\n+ * are in IFS7.  Use 256 to cover the full IPC/IFS table.\n+ */\n+#define PIC32MK_NUM_IRQ_SOURCES 256\n+#define PIC32MK_NUM_VECTORS     190\n+\n+/*\n+ * UART1 registers (§21, Table 21-2) — U1MODE base 0xBF828000\n+ * Offset from SFR base: 0x028000\n+ * -----------------------------------------------------------------------\n+ */\n+\n+#define PIC32MK_UART1_OFFSET    0x028000u   /* SFR offset → physical 0x1F828000 */\n+#define PIC32MK_UART1_SIZE      0x000050u   /* U1MODE..U1BRG+INV */\n+\n+/* Register offsets within the UART block (same layout for all UARTs) */\n+#define PIC32MK_UxMODE          0x00u       /* Mode */\n+#define PIC32MK_UxSTA           0x10u       /* Status & control */\n+#define PIC32MK_UxTXREG         0x20u       /* TX data */\n+#define PIC32MK_UxRXREG         0x30u       /* RX data */\n+#define PIC32MK_UxBRG           0x40u       /* Baud rate */\n+\n+/* U1STA bits relevant to TX polling */\n+#define PIC32MK_USTA_TRMT       (1u << 8)   /* TX shift register empty (1=empty) */\n+#define PIC32MK_USTA_UTXBF      (1u << 9)   /* TX buffer full (0=not full) */\n+\n+/*\n+ * CPU clock (§3)\n+ * -----------------------------------------------------------------------\n+ */\n+\n+#define PIC32MK_CPU_HZ          120000000u  /* 120 MHz max */\n+\n+/*\n+ * Interrupt source numbers (§8, Table 8-1, DS60001519E)\n+ * TODO: verify each number against the actual datasheet table.\n+ * -----------------------------------------------------------------------\n+ */\n+\n+#define PIC32MK_IRQ_CT          0    /* Core Timer */\n+#define PIC32MK_IRQ_CS0         1    /* Core Software Interrupt 0 */\n+#define PIC32MK_IRQ_CS1         2    /* Core Software Interrupt 1 */\n+#define PIC32MK_IRQ_INT0        3    /* External Interrupt 0 */\n+#define PIC32MK_IRQ_T1          4    /* Timer 1 */\n+#define PIC32MK_IRQ_T2          9    /* Timer 2 */\n+#define PIC32MK_IRQ_T3          14   /* Timer 3 */\n+#define PIC32MK_IRQ_T4          19   /* Timer 4 */\n+#define PIC32MK_IRQ_T5          24   /* Timer 5 */\n+#define PIC32MK_IRQ_T6          28   /* Timer 6 */\n+#define PIC32MK_IRQ_T7          32   /* Timer 7 */\n+#define PIC32MK_IRQ_T8          36   /* Timer 8 */\n+#define PIC32MK_IRQ_T9          40   /* Timer 9 */\n+/* UART1: error=38, RX=39, TX=40  (IFS1 bits 6/7/8, p32mk1024mcm100.h) */\n+#define PIC32MK_IRQ_U1E         38\n+#define PIC32MK_IRQ_U1RX        39\n+#define PIC32MK_IRQ_U1TX        40\n+#define PIC32MK_IRQ_U2E         115\n+#define PIC32MK_IRQ_U2RX        116\n+#define PIC32MK_IRQ_U2TX        117\n+#define PIC32MK_IRQ_U3E         118\n+#define PIC32MK_IRQ_U3RX        119\n+#define PIC32MK_IRQ_U3TX        120\n+#define PIC32MK_IRQ_U4E         121\n+#define PIC32MK_IRQ_U4RX        122\n+#define PIC32MK_IRQ_U4TX        123\n+#define PIC32MK_IRQ_U5E         124\n+#define PIC32MK_IRQ_U5RX        125\n+#define PIC32MK_IRQ_U5TX        126\n+#define PIC32MK_IRQ_U6E         127\n+#define PIC32MK_IRQ_U6RX        128\n+#define PIC32MK_IRQ_U6TX        129\n+/* CAN FD — single IRQ per instance (§8, Table 8-1, DS60001519E) */\n+#define PIC32MK_IRQ_CAN1        167\n+#define PIC32MK_IRQ_CAN2        168\n+#define PIC32MK_IRQ_CAN3        187\n+#define PIC32MK_IRQ_CAN4        188\n+\n+/*\n+ * USB OTG — interrupt vector numbers from XC32 p32mk1024mcm100.h\n+ * USB1: vector 34 → IFS1 bit 2, IPC8[18:16]\n+ * USB2: vector 244 → IFS7 bit 20, IPC61[2:0]\n+ */\n+#define PIC32MK_IRQ_USB1        34\n+#define PIC32MK_IRQ_USB2        244\n+\n+/* DMA channels 0-7 */\n+#define PIC32MK_IRQ_DMA0        134\n+#define PIC32MK_IRQ_DMA1        135\n+#define PIC32MK_IRQ_DMA2        136\n+#define PIC32MK_IRQ_DMA3        137\n+#define PIC32MK_IRQ_DMA4        138\n+#define PIC32MK_IRQ_DMA5        139\n+#define PIC32MK_IRQ_DMA6        140\n+#define PIC32MK_IRQ_DMA7        141\n+\n+/*\n+ * GPIO Change-Notice interrupt vectors (DS60001519E Table 8-1,\n+ * _CHANGE_NOTICE_x_VECTOR from p32mk1024mcm100.h)\n+ */\n+#define PIC32MK_IRQ_CNA         44\n+#define PIC32MK_IRQ_CNB         45\n+#define PIC32MK_IRQ_CNC         46\n+#define PIC32MK_IRQ_CND         47\n+#define PIC32MK_IRQ_CNE         48\n+#define PIC32MK_IRQ_CNF         49\n+#define PIC32MK_IRQ_CNG         50\n+\n+/*\n+ * Timer peripheral registers (§14, DS60001519E)\n+ * Offsets from PIC32MK_PER1_OFFSET (0xBF820000).\n+ * Each timer block is 0x200 bytes; register stride 0x10 (with SET/CLR/INV).\n+ * TODO: verify exact base offsets for PIC32MK GPK from Table 4-2.\n+ * -----------------------------------------------------------------------\n+ */\n+\n+/* Timer base offsets from SFR base */\n+#define PIC32MK_T1_OFFSET       0x020000u   /* Timer 1 (Type A, 16-bit) */\n+#define PIC32MK_T2_OFFSET       0x020200u   /* Timer 2 (Type B) */\n+#define PIC32MK_T3_OFFSET       0x020400u   /* Timer 3 (Type C) */\n+#define PIC32MK_T4_OFFSET       0x020600u   /* Timer 4 (Type B) */\n+#define PIC32MK_T5_OFFSET       0x020800u   /* Timer 5 (Type C) */\n+#define PIC32MK_T6_OFFSET       0x020A00u   /* Timer 6 (Type B) */\n+#define PIC32MK_T7_OFFSET       0x020C00u   /* Timer 7 (Type C) */\n+#define PIC32MK_T8_OFFSET       0x020E00u   /* Timer 8 (Type B) */\n+#define PIC32MK_T9_OFFSET       0x021000u   /* Timer 9 (Type C) */\n+\n+#define PIC32MK_TIMER_BLOCK_SIZE    0x200u  /* per-timer SFR block */\n+\n+/* Timer register offsets within block (with SET/CLR/INV at +4/+8/+C) */\n+#define PIC32MK_TxCON           0x00u   /* Control: ON, TCKPS, T32, TCS... */\n+#define PIC32MK_TMRx            0x10u   /* Current count */\n+#define PIC32MK_PRx             0x20u   /* Period register */\n+\n+/* TxCON bits */\n+#define PIC32MK_TCON_ON         (1u << 15)  /* Timer ON */\n+#define PIC32MK_TCON_T32        (1u << 3)   /* 32-bit mode (Type B only) */\n+#define PIC32MK_TCON_TCS        (1u << 1)   /* Clock source select */\n+#define PIC32MK_TCON_TCKPS_MASK 0x0070u     /* Prescaler bits [6:4] */\n+#define PIC32MK_TCON_TCKPS_SHIFT    4\n+\n+/*\n+ * OC (Output Compare) register offsets from SFR base (§19, DS60001519E)\n+ * OC1-OC9:  0xBF824000..0xBF825000 (0x200 stride)\n+ * OC10-OC16: 0xBF845200..0xBF845E00 (0x200 stride)\n+ * -----------------------------------------------------------------------\n+ */\n+#define PIC32MK_OC1_OFFSET      0x024000u\n+#define PIC32MK_OC2_OFFSET      0x024200u\n+#define PIC32MK_OC3_OFFSET      0x024400u\n+#define PIC32MK_OC4_OFFSET      0x024600u\n+#define PIC32MK_OC5_OFFSET      0x024800u\n+#define PIC32MK_OC6_OFFSET      0x024A00u\n+#define PIC32MK_OC7_OFFSET      0x024C00u\n+#define PIC32MK_OC8_OFFSET      0x024E00u\n+#define PIC32MK_OC9_OFFSET      0x025000u\n+#define PIC32MK_OC10_OFFSET     0x045200u\n+#define PIC32MK_OC11_OFFSET     0x045400u\n+#define PIC32MK_OC12_OFFSET     0x045600u\n+#define PIC32MK_OC13_OFFSET     0x045800u\n+#define PIC32MK_OC14_OFFSET     0x045A00u\n+#define PIC32MK_OC15_OFFSET     0x045C00u\n+#define PIC32MK_OC16_OFFSET     0x045E00u\n+\n+#define PIC32MK_OC_BLOCK_SIZE   0x200u\n+\n+/* OC register offsets within each 0x200-byte block */\n+#define PIC32MK_OCxCON          0x00u   /* Control */\n+#define PIC32MK_OCxR            0x10u   /* Primary compare value */\n+#define PIC32MK_OCxRS           0x20u   /* Secondary compare value */\n+\n+/* OCxCON bits (Register 19-1) */\n+#define PIC32MK_OCCON_ON        (1u << 15)\n+#define PIC32MK_OCCON_SIDL      (1u << 13)\n+#define PIC32MK_OCCON_OC32      (1u << 5)\n+#define PIC32MK_OCCON_OCFLT     (1u << 4)\n+#define PIC32MK_OCCON_OCTSEL    (1u << 3)\n+#define PIC32MK_OCCON_OCM_MASK  0x0007u\n+#define PIC32MK_OCCON_OCM_SHIFT 0\n+\n+/* OC interrupt vector numbers */\n+#define PIC32MK_IRQ_OC1         7\n+#define PIC32MK_IRQ_OC2         12\n+#define PIC32MK_IRQ_OC3         17\n+#define PIC32MK_IRQ_OC4         22\n+#define PIC32MK_IRQ_OC5         27\n+#define PIC32MK_IRQ_OC6         79\n+#define PIC32MK_IRQ_OC7         83\n+#define PIC32MK_IRQ_OC8         87\n+#define PIC32MK_IRQ_OC9         91\n+#define PIC32MK_IRQ_OC10        199\n+#define PIC32MK_IRQ_OC11        202\n+#define PIC32MK_IRQ_OC12        205\n+#define PIC32MK_IRQ_OC13        208\n+#define PIC32MK_IRQ_OC14        211\n+#define PIC32MK_IRQ_OC15        214\n+#define PIC32MK_IRQ_OC16        217\n+\n+/*\n+ * Input Capture (IC1–IC16) — §18, DS60001519E\n+ * IC1–IC9:   SFR bank 1, base 0xBF822000, stride 0x200\n+ * IC10–IC16: SFR bank 2, base 0xBF843200, stride 0x200\n+ * -----------------------------------------------------------------------\n+ */\n+#define PIC32MK_IC1_OFFSET      0x022000u\n+#define PIC32MK_IC2_OFFSET      0x022200u\n+#define PIC32MK_IC3_OFFSET      0x022400u\n+#define PIC32MK_IC4_OFFSET      0x022600u\n+#define PIC32MK_IC5_OFFSET      0x022800u\n+#define PIC32MK_IC6_OFFSET      0x022A00u\n+#define PIC32MK_IC7_OFFSET      0x022C00u\n+#define PIC32MK_IC8_OFFSET      0x022E00u\n+#define PIC32MK_IC9_OFFSET      0x023000u\n+#define PIC32MK_IC10_OFFSET     0x043200u\n+#define PIC32MK_IC11_OFFSET     0x043400u\n+#define PIC32MK_IC12_OFFSET     0x043600u\n+#define PIC32MK_IC13_OFFSET     0x043800u\n+#define PIC32MK_IC14_OFFSET     0x043A00u\n+#define PIC32MK_IC15_OFFSET     0x043C00u\n+#define PIC32MK_IC16_OFFSET     0x043E00u\n+\n+#define PIC32MK_IC_BLOCK_SIZE   0x200u\n+\n+/* IC register offsets within each block */\n+#define PIC32MK_ICxCON          0x00u   /* Control (+ SET/CLR/INV at +4/+8/+C) */\n+#define PIC32MK_ICxBUF          0x10u   /* Capture buffer (read-only FIFO pop) */\n+\n+/* ICxCON bits (Register 18-1, DS60001519E) */\n+#define PIC32MK_ICCON_ON        (1u << 15)  /* Module enable */\n+#define PIC32MK_ICCON_SIDL      (1u << 13)  /* Stop in idle */\n+#define PIC32MK_ICCON_FEDGE     (1u << 9)   /* First edge select */\n+#define PIC32MK_ICCON_C32       (1u << 8)   /* 32-bit capture mode */\n+#define PIC32MK_ICCON_ICTMR     (1u << 7)   /* Timer source select */\n+#define PIC32MK_ICCON_ICI_MASK  0x0060u     /* Interrupt on every Nth capture [6:5] */\n+#define PIC32MK_ICCON_ICI_SHIFT 5\n+#define PIC32MK_ICCON_ICOV      (1u << 4)   /* Overflow (buffer full) */\n+#define PIC32MK_ICCON_ICBNE     (1u << 3)   /* Buffer not empty */\n+#define PIC32MK_ICCON_ICM_MASK  0x0007u     /* Capture mode [2:0] */\n+\n+/* IC IRQ numbers (Table 8-3, DS60001519E) — paired: error IRQ, capture IRQ */\n+#define PIC32MK_IRQ_IC1E        5\n+#define PIC32MK_IRQ_IC1         6\n+#define PIC32MK_IRQ_IC2E        10\n+#define PIC32MK_IRQ_IC2         11\n+#define PIC32MK_IRQ_IC3E        15\n+#define PIC32MK_IRQ_IC3         16\n+#define PIC32MK_IRQ_IC4E        20\n+#define PIC32MK_IRQ_IC4         21\n+#define PIC32MK_IRQ_IC5E        25\n+#define PIC32MK_IRQ_IC5         26\n+#define PIC32MK_IRQ_IC6E        77\n+#define PIC32MK_IRQ_IC6         78\n+#define PIC32MK_IRQ_IC7E        81\n+#define PIC32MK_IRQ_IC7         82\n+#define PIC32MK_IRQ_IC8E        85\n+#define PIC32MK_IRQ_IC8         86\n+#define PIC32MK_IRQ_IC9E        89\n+#define PIC32MK_IRQ_IC9         90\n+#define PIC32MK_IRQ_IC10E       197\n+#define PIC32MK_IRQ_IC10        198\n+#define PIC32MK_IRQ_IC11E       200\n+#define PIC32MK_IRQ_IC11        201\n+#define PIC32MK_IRQ_IC12E       203\n+#define PIC32MK_IRQ_IC12        204\n+#define PIC32MK_IRQ_IC13E       206\n+#define PIC32MK_IRQ_IC13        207\n+#define PIC32MK_IRQ_IC14E       209\n+#define PIC32MK_IRQ_IC14        210\n+#define PIC32MK_IRQ_IC15E       212\n+#define PIC32MK_IRQ_IC15        213\n+#define PIC32MK_IRQ_IC16E       215\n+#define PIC32MK_IRQ_IC16        216\n+\n+/* SPI1-6 IRQ numbers (Table 8-3, DS60001519E) */\n+#define PIC32MK_IRQ_SPI1_FAULT  35\n+#define PIC32MK_IRQ_SPI1_RX     36\n+#define PIC32MK_IRQ_SPI1_TX     37\n+#define PIC32MK_IRQ_SPI2_FAULT  53\n+#define PIC32MK_IRQ_SPI2_RX     54\n+#define PIC32MK_IRQ_SPI2_TX     55\n+#define PIC32MK_IRQ_SPI3_FAULT  154\n+#define PIC32MK_IRQ_SPI3_RX     155\n+#define PIC32MK_IRQ_SPI3_TX     156\n+#define PIC32MK_IRQ_SPI4_FAULT  166\n+#define PIC32MK_IRQ_SPI4_RX     167\n+#define PIC32MK_IRQ_SPI4_TX     168\n+#define PIC32MK_IRQ_SPI5_FAULT  169\n+#define PIC32MK_IRQ_SPI5_RX     170\n+#define PIC32MK_IRQ_SPI5_TX     171\n+#define PIC32MK_IRQ_SPI6_FAULT  172\n+#define PIC32MK_IRQ_SPI6_RX     173\n+#define PIC32MK_IRQ_SPI6_TX     174\n+\n+/*\n+ * UART2–6 register base offsets from SFR base\n+ * UART1 is already defined as PIC32MK_UART1_OFFSET = 0x028000\n+ * TODO: verify exact offsets against DS60001519E Table 4-2.\n+ * -----------------------------------------------------------------------\n+ */\n+\n+#define PIC32MK_UART2_OFFSET    0x028200u   /* UART2 (PER1 block) */\n+#define PIC32MK_UART3_OFFSET    0x048400u   /* UART3 (PER2 block, 0xBF848400) */\n+#define PIC32MK_UART4_OFFSET    0x048600u   /* UART4 (0xBF848600) */\n+#define PIC32MK_UART5_OFFSET    0x048800u   /* UART5 (0xBF848800) */\n+#define PIC32MK_UART6_OFFSET    0x048A00u   /* UART6 (0xBF848A00) */\n+\n+#define PIC32MK_UART_BLOCK_SIZE 0x200u      /* per-UART SFR block */\n+\n+/* Additional UxSTA bits */\n+#define PIC32MK_USTA_URXDA      (1u << 0)   /* RX data available */\n+#define PIC32MK_USTA_OERR       (1u << 1)   /* Overrun error */\n+#define PIC32MK_USTA_FERR       (1u << 2)   /* Framing error */\n+#define PIC32MK_USTA_PERR       (1u << 3)   /* Parity error */\n+#define PIC32MK_USTA_RIDLE      (1u << 4)   /* Receiver idle */\n+#define PIC32MK_USTA_UTXEN      (1u << 10)  /* TX enable */\n+#define PIC32MK_USTA_UTXISEL1   (1u << 14)  /* TX interrupt select */\n+#define PIC32MK_USTA_URXEN      (1u << 12)  /* RX enable */\n+#define PIC32MK_USTA_URXISEL1   (1u << 6)   /* RX interrupt select */\n+#define PIC32MK_UMODE_ON        (1u << 15)  /* UART enable */\n+\n+/*\n+ * SPI peripheral registers (§23, DS60001519E)\n+ * TODO: verify exact base offsets.\n+ * -----------------------------------------------------------------------\n+ */\n+\n+#define PIC32MK_SPI1_OFFSET     0x021800u\n+#define PIC32MK_SPI2_OFFSET     0x021A00u\n+#define PIC32MK_SPI3_OFFSET     0x040800u\n+#define PIC32MK_SPI4_OFFSET     0x040A00u\n+#define PIC32MK_SPI5_OFFSET     0x047800u   /* 0xBF847800 */\n+#define PIC32MK_SPI6_OFFSET     0x040E00u\n+#define PIC32MK_SPI_BLOCK_SIZE  0x200u\n+\n+/* SPI register offsets within block */\n+#define PIC32MK_SPIxCON         0x00u\n+#define PIC32MK_SPIxSTAT        0x10u\n+#define PIC32MK_SPIxBUF         0x20u\n+#define PIC32MK_SPIxBRG         0x30u\n+#define PIC32MK_SPIxCON2        0x40u\n+\n+/*\n+ * I2C peripheral registers (§24, DS60001519E)\n+ * TODO: verify exact base offsets.\n+ * -----------------------------------------------------------------------\n+ */\n+\n+#define PIC32MK_I2C1_OFFSET     0x026000u   /* 0xBF826000 — §24, Table 4-2 */\n+#define PIC32MK_I2C2_OFFSET     0x026200u   /* 0xBF826200 */\n+#define PIC32MK_I2C3_OFFSET     0x046400u   /* 0xBF846400 (PER2 bank) */\n+#define PIC32MK_I2C4_OFFSET     0x046600u   /* 0xBF846600 */\n+#define PIC32MK_I2C_BLOCK_SIZE  0x200u\n+\n+/* I2C register offsets within block */\n+#define PIC32MK_I2CxCON        0x00u\n+#define PIC32MK_I2CxSTAT       0x10u\n+#define PIC32MK_I2CxADD        0x20u\n+#define PIC32MK_I2CxMSK        0x30u\n+#define PIC32MK_I2CxTRN        0x40u\n+#define PIC32MK_I2CxRCV        0x50u\n+\n+/*\n+ * GPIO peripheral registers (§12, DS60001519E)\n+ * Base: PIC32MK_GPIO_OFFSET = 0x060000 (0xBF860000)\n+ * Each port (A–G) occupies 0x100 bytes.\n+ * -----------------------------------------------------------------------\n+ */\n+\n+#define PIC32MK_GPIO_PORT_SIZE  0x100u      /* per-port register block */\n+\n+/* GPIO register offsets within each port block */\n+#define PIC32MK_ANSEL           0x00u   /* Analog select */\n+#define PIC32MK_TRIS            0x10u   /* Direction (1=input) */\n+#define PIC32MK_PORT            0x20u   /* Read pin state */\n+#define PIC32MK_LAT             0x30u   /* Latch (write output) */\n+#define PIC32MK_ODC             0x40u   /* Open-drain control */\n+#define PIC32MK_CNPU            0x50u   /* Change-notice pull-up */\n+#define PIC32MK_CNPD            0x60u   /* Change-notice pull-down */\n+#define PIC32MK_CNCON           0x70u   /* Change-notice control */\n+#define PIC32MK_CNEN0           0x80u   /* CN edge enable 0 */\n+#define PIC32MK_CNSTAT          0x90u   /* CN status */\n+#define PIC32MK_CNEN1           0xA0u   /* CN edge enable 1 */\n+#define PIC32MK_CNF             0xB0u   /* CN flag */\n+\n+/* Number of GPIO ports (A–G) */\n+#define PIC32MK_GPIO_NPORTS     7\n+\n+/*\n+ * Set shared chardev for GPIO state-change event streaming.\n+ * Called from board init; multiple ports share the same Chardev*.\n+ */\n+void pic32mk_gpio_set_chardev(DeviceState *dev, Chardev *chr);\n+void pic32mk_oc_set_chardev(DeviceState *dev, Chardev *chr);\n+\n+/*\n+ * DMA controller registers (§26, DS60001519E)\n+ * Base: PIC32MK_DMA_OFFSET = 0x011000 (within EVIC page, 0xBF811000)\n+ * Global registers at base, then 8 channel blocks at +0x60 each.\n+ * -----------------------------------------------------------------------\n+ */\n+\n+/* DMA global registers */\n+#define PIC32MK_DMACON_OFFSET   0x00u   /* DMA control */\n+#define PIC32MK_DMASTAT_OFFSET  0x10u   /* DMA status */\n+#define PIC32MK_DMAADDR_OFFSET  0x20u   /* DMA address */\n+\n+/* DMA channel registers base: +0x60 + channel * 0xC0 */\n+#define PIC32MK_DMA_CH_BASE     0x60u\n+#define PIC32MK_DMA_CH_STRIDE   0xC0u\n+#define PIC32MK_DMA_NCHANNELS   8\n+\n+/* DMA channel register offsets within each channel block */\n+#define PIC32MK_DCHxCON         0x00u\n+#define PIC32MK_DCHxECON        0x10u\n+#define PIC32MK_DCHxINT         0x20u\n+#define PIC32MK_DCHxSSA         0x30u\n+#define PIC32MK_DCHxDSA         0x40u\n+#define PIC32MK_DCHxSSIZ        0x50u\n+#define PIC32MK_DCHxDSIZ        0x60u\n+#define PIC32MK_DCHxSPTR        0x70u\n+#define PIC32MK_DCHxDPTR        0x80u\n+#define PIC32MK_DCHxCSIZ        0x90u\n+#define PIC32MK_DCHxCPTR        0xA0u\n+#define PIC32MK_DCHxDAT         0xB0u\n+\n+/*\n+ * ADCHS peripheral registers (§22, DS60001519E)\n+ * Base: 0xBF887000 (SFR offset 0x087000)\n+ * Register block spans ~4 KB (0x000–0xE1C).\n+ * Each register has SET/CLR/INV aliases at +4/+8/+C.\n+ * -----------------------------------------------------------------------\n+ */\n+\n+#define PIC32MK_ADC_OFFSET      0x087000u   /* 0xBF887000 */\n+#define PIC32MK_ADC_SIZE        0x001000u   /* 4 KB register window */\n+\n+/* Control registers */\n+#define PIC32MK_ADCCON1         0x000u\n+#define PIC32MK_ADCCON2         0x010u\n+#define PIC32MK_ADCCON3         0x020u\n+#define PIC32MK_ADCTRGMODE      0x030u\n+\n+/* Input mode control (signed/unsigned per channel group) */\n+#define PIC32MK_ADCIMCON1       0x040u\n+#define PIC32MK_ADCIMCON2       0x050u\n+#define PIC32MK_ADCIMCON3       0x060u\n+#define PIC32MK_ADCIMCON4       0x070u\n+\n+/* Global interrupt enable (result ready, 2 × 32 bits) */\n+#define PIC32MK_ADCGIRQEN1      0x080u\n+#define PIC32MK_ADCGIRQEN2      0x090u\n+\n+/* Channel scan select */\n+#define PIC32MK_ADCCSS1         0x0A0u\n+#define PIC32MK_ADCCSS2         0x0B0u\n+\n+/* Data ready status */\n+#define PIC32MK_ADCDSTAT1       0x0C0u\n+#define PIC32MK_ADCDSTAT2       0x0D0u\n+\n+/* Compare enable */\n+#define PIC32MK_ADCCMPEN1       0x0E0u\n+#define PIC32MK_ADCCMPEN2       0x100u\n+#define PIC32MK_ADCCMPEN3       0x120u\n+#define PIC32MK_ADCCMPEN4       0x140u\n+\n+/* Compare values */\n+#define PIC32MK_ADCCMP1         0x0F0u\n+#define PIC32MK_ADCCMP2         0x110u\n+#define PIC32MK_ADCCMP3         0x130u\n+#define PIC32MK_ADCCMP4         0x150u\n+\n+/* Digital filter registers */\n+#define PIC32MK_ADCFLTR1        0x1A0u\n+#define PIC32MK_ADCFLTR2        0x1B0u\n+#define PIC32MK_ADCFLTR3        0x1C0u\n+#define PIC32MK_ADCFLTR4        0x1D0u\n+\n+/* Trigger configuration */\n+#define PIC32MK_ADCTRG1         0x200u\n+#define PIC32MK_ADCTRG2         0x210u\n+#define PIC32MK_ADCTRG3         0x220u\n+#define PIC32MK_ADCTRG4         0x230u\n+#define PIC32MK_ADCTRG5         0x240u\n+#define PIC32MK_ADCTRG6         0x250u\n+#define PIC32MK_ADCTRG7         0x260u\n+\n+/* Compare control */\n+#define PIC32MK_ADCCMPCON1      0x280u\n+#define PIC32MK_ADCCMPCON2      0x290u\n+#define PIC32MK_ADCCMPCON3      0x2A0u\n+#define PIC32MK_ADCCMPCON4      0x2B0u\n+\n+/* Misc registers */\n+#define PIC32MK_ADCBASE         0x300u\n+#define PIC32MK_ADCTRGSNS       0x340u\n+\n+/* Sampling time (per-module) */\n+#define PIC32MK_ADC0TIME        0x350u\n+#define PIC32MK_ADC1TIME        0x360u\n+#define PIC32MK_ADC2TIME        0x370u\n+#define PIC32MK_ADC3TIME        0x380u\n+#define PIC32MK_ADC4TIME        0x390u\n+#define PIC32MK_ADC5TIME        0x3A0u\n+\n+/* Early interrupt enable / status */\n+#define PIC32MK_ADCEIEN1        0x3C0u\n+#define PIC32MK_ADCEIEN2        0x3D0u\n+#define PIC32MK_ADCEISTAT1      0x3E0u\n+#define PIC32MK_ADCEISTAT2      0x3F0u\n+\n+/* Analog module enable / warm-up control */\n+#define PIC32MK_ADCANCON        0x400u\n+\n+/* Conversion data registers (stride 0x10 per channel index) */\n+#define PIC32MK_ADCDATA_BASE    0x600u\n+#define PIC32MK_ADCDATA_STRIDE  0x010u\n+\n+/* Per-module configuration */\n+#define PIC32MK_ADC0CFG         0xD00u\n+#define PIC32MK_ADC1CFG         0xD10u\n+#define PIC32MK_ADC2CFG         0xD20u\n+#define PIC32MK_ADC3CFG         0xD30u\n+#define PIC32MK_ADC4CFG         0xD40u\n+#define PIC32MK_ADC5CFG         0xD50u\n+#define PIC32MK_ADC6CFG         0xD60u\n+#define PIC32MK_ADC7CFG         0xD70u\n+\n+/* System configuration */\n+#define PIC32MK_ADCSYSCFG0      0xE00u\n+#define PIC32MK_ADCSYSCFG1      0xE10u\n+\n+/* Maximum channel index (0–53, with gaps) */\n+#define PIC32MK_ADC_MAX_CH      54\n+\n+/* ADCCON1 bits */\n+#define PIC32MK_ADCCON1_ON      (1u << 15)\n+\n+/* ADCCON2 bits */\n+#define PIC32MK_ADCCON2_BGVRRDY (1u << 31)  /* Band-gap voltage ref ready */\n+#define PIC32MK_ADCCON2_REFFLT  (1u << 30)  /* Reference fault */\n+\n+/* ADCCON3 bits */\n+#define PIC32MK_ADCCON3_ADINSEL_MASK  0x3Fu        /* bits [5:0] channel select */\n+#define PIC32MK_ADCCON3_RQCNVRT (1u << 8)          /* Request conversion */\n+#define PIC32MK_ADCCON3_GSWTRG  (1u << 6)          /* Global software trigger */\n+#define PIC32MK_ADCCON3_GLSWTRG (1u << 5)          /* Global level SW trigger */\n+#define PIC32MK_ADCCON3_DIGEN_SHIFT  16             /* DIGENx at bits [23:16] */\n+\n+/* ADCANCON bits — ANENx and WKRDYx (modules 0–5, 7) */\n+#define PIC32MK_ADCANCON_ANEN_SHIFT   0             /* ANENx at bits [7:0] */\n+#define PIC32MK_ADCANCON_WKRDY_SHIFT  8             /* WKRDYx at bits [15:8] */\n+\n+/*\n+ * ADCHS interrupt source numbers (§8, Table 8-1) — vector/IRQ numbers\n+ * -----------------------------------------------------------------------\n+ */\n+\n+#define PIC32MK_IRQ_ADC         92   /* Main ADC interrupt */\n+#define PIC32MK_IRQ_ADC_DC1     94   /* Digital comparator 1 */\n+#define PIC32MK_IRQ_ADC_DC2     95   /* Digital comparator 2 */\n+#define PIC32MK_IRQ_ADC_DF1     96   /* Digital filter 1 */\n+#define PIC32MK_IRQ_ADC_DF2     97   /* Digital filter 2 */\n+#define PIC32MK_IRQ_ADC_DF3     98   /* Digital filter 3 */\n+#define PIC32MK_IRQ_ADC_DF4     99   /* Digital filter 4 */\n+#define PIC32MK_IRQ_ADC_FAULT   100  /* ADC fault */\n+#define PIC32MK_IRQ_ADC_EOS     101  /* End of scan */\n+#define PIC32MK_IRQ_ADC_ARDY    102  /* Analog ready */\n+#define PIC32MK_IRQ_ADC_URDY    103  /* Update ready */\n+#define PIC32MK_IRQ_ADC_DMA     104  /* DMA */\n+#define PIC32MK_IRQ_ADC_EARLY   105  /* Early interrupt */\n+#define PIC32MK_IRQ_ADC_DATA0   106  /* Data ready channel 0 (base) */\n+/* DATA1..DATA27 = 107..133, DATA33..41 = 139..147, DATA45..53 = 151..159 */\n+#define PIC32MK_IRQ_ADC_DC3     245  /* Digital comparator 3 */\n+#define PIC32MK_IRQ_ADC_DC4     246  /* Digital comparator 4 */\n+\n+/*\n+ * NVM / Flash Controller (§10, DS60001519E)\n+ * Base: 0xBF800A00 (SFR offset 0x000A00)\n+ * Register block: NVMCON, NVMKEY, NVMADDR, NVMDATA0–3, NVMSRCADDR,\n+ * NVMPWP, NVMBWP, NVMCON2 (each 0x10 stride with SET/CLR/INV aliases,\n+ * except NVMKEY which is write-only, no aliases).\n+ * -----------------------------------------------------------------------\n+ */\n+\n+#define PIC32MK_NVM_OFFSET      0x000A00u   /* 0xBF800A00 */\n+#define PIC32MK_NVM_SIZE        0x0000B0u   /* NVMCON..NVMCON2+INV inclusive */\n+\n+/* Register offsets within the NVM block */\n+#define PIC32MK_NVMCON          0x00u       /* +CLR/SET/INV at +4/+8/+C */\n+#define PIC32MK_NVMKEY          0x10u\n+#define PIC32MK_NVMADDR         0x20u       /* +CLR/SET/INV */\n+#define PIC32MK_NVMDATA0        0x30u       /* +CLR/SET/INV */\n+#define PIC32MK_NVMDATA1        0x40u\n+#define PIC32MK_NVMDATA2        0x50u\n+#define PIC32MK_NVMDATA3        0x60u\n+#define PIC32MK_NVMSRCADDR      0x70u       /* +CLR/SET/INV */\n+#define PIC32MK_NVMPWP          0x80u\n+#define PIC32MK_NVMBWP          0x90u\n+#define PIC32MK_NVMCON2         0xA0u\n+\n+/* NVMCON bits */\n+#define PIC32MK_NVMCON_NVMOP_MASK  0x000Fu  /* bits [3:0] */\n+#define PIC32MK_NVMCON_BFSWAP     (1u << 6)\n+#define PIC32MK_NVMCON_PFSWAP     (1u << 7)\n+#define PIC32MK_NVMCON_LVDERR     (1u << 12)\n+#define PIC32MK_NVMCON_WRERR      (1u << 13)\n+#define PIC32MK_NVMCON_WREN       (1u << 14)\n+#define PIC32MK_NVMCON_WR         (1u << 15)\n+\n+/* NVM operation codes (NVMOP field, bits [3:0]) */\n+#define PIC32MK_NVMOP_NOP              0x0u\n+#define PIC32MK_NVMOP_WORD_PROG        0x1u\n+#define PIC32MK_NVMOP_QUAD_WORD_PROG   0x2u\n+#define PIC32MK_NVMOP_ROW_PROG         0x3u\n+#define PIC32MK_NVMOP_PAGE_ERASE       0x4u\n+#define PIC32MK_NVMOP_LOWER_PFM_ERASE  0x5u\n+#define PIC32MK_NVMOP_UPPER_PFM_ERASE  0x6u\n+#define PIC32MK_NVMOP_PFM_ERASE        0x7u\n+\n+/* Unlock keys (written sequentially to NVMKEY) */\n+#define PIC32MK_NVMKEY1         0xAA996655u\n+#define PIC32MK_NVMKEY2         0x556699AAu\n+\n+/* Flash geometry */\n+#define PIC32MK_NVM_PAGE_SIZE   4096u       /* erase granularity */\n+#define PIC32MK_NVM_ROW_SIZE    512u        /* write-row granularity */\n+\n+/* Interrupt — Flash Control Error vector 31 */\n+#define PIC32MK_IRQ_FCE         31\n+\n+/*\n+ * Data EEPROM (§11, DS60001519E)\n+ * Base: 0xBF829000 (SFR offset 0x029000)\n+ * Register block: EECON, EEKEY, EEADDR, EEDATA (each 0x10 stride with\n+ * SET/CLR/INV aliases, except EEKEY which is write-only, no aliases).\n+ * -----------------------------------------------------------------------\n+ */\n+\n+#define PIC32MK_DATAEE_OFFSET   0x029000u   /* 0xBF829000 */\n+#define PIC32MK_DATAEE_SIZE     0x000040u   /* EECON..EEDATA+INV inclusive */\n+\n+/* Register offsets within the DATAEE block */\n+#define PIC32MK_EECON           0x00u\n+#define PIC32MK_EEKEY           0x10u\n+#define PIC32MK_EEADDR          0x20u\n+#define PIC32MK_EEDATA_REG      0x30u       /* \"_REG\" avoids clash with EEDATA macro */\n+\n+/* EECON bits (p32mk1024mcm100.h) */\n+#define PIC32MK_EECON_CMD_MASK  0x00000007u /* bits [2:0] */\n+#define PIC32MK_EECON_CMD_SHIFT 0\n+#define PIC32MK_EECON_ILW       (1u << 3)\n+#define PIC32MK_EECON_ERR_MASK  0x00000030u /* bits [5:4] */\n+#define PIC32MK_EECON_ERR_SHIFT 4\n+#define PIC32MK_EECON_WREN      (1u << 6)\n+#define PIC32MK_EECON_RW        (1u << 7)\n+#define PIC32MK_EECON_ABORT     (1u << 12)\n+#define PIC32MK_EECON_SIDL      (1u << 13)\n+#define PIC32MK_EECON_RDY       (1u << 14)\n+#define PIC32MK_EECON_ON        (1u << 15)\n+\n+/* EEADDR valid bits — 14-bit, word-aligned */\n+#define PIC32MK_EEADDR_MASK     0x00003FFCu\n+\n+/* EEPROM storage geometry */\n+#define PIC32MK_DATAEE_WORDS        1024    /* 4 KB = 1024 × 32-bit words */\n+#define PIC32MK_DATAEE_PAGE_WORDS   32      /* 128-byte page */\n+\n+/* Unlock keys (written sequentially to EEKEY) */\n+#define PIC32MK_EEKEY1          0xEDB7u\n+#define PIC32MK_EEKEY2          0x1248u\n+\n+/* EECON CMD field values */\n+#define PIC32MK_EECMD_WORD_READ     0\n+#define PIC32MK_EECMD_WORD_WRITE    1\n+#define PIC32MK_EECMD_PAGE_ERASE    2\n+#define PIC32MK_EECMD_BULK_ERASE    3\n+#define PIC32MK_EECMD_CONFIG_WRITE  4\n+\n+/* Interrupt — Data EEPROM vector 186 */\n+#define PIC32MK_IRQ_DATAEE      186\n+\n+#endif /* HW_MIPS_PIC32MK_H */\ndiff --git a/include/hw/mips/pic32mk_adchs.h b/include/hw/mips/pic32mk_adchs.h\nnew file mode 100644\nindex 0000000000..2c4149c3a3\n--- /dev/null\n+++ b/include/hw/mips/pic32mk_adchs.h\n@@ -0,0 +1,87 @@\n+/*\n+ * PIC32MK ADCHS (High-Speed ADC) peripheral — device interface\n+ * Datasheet: DS60001519E, §22\n+ *\n+ * Copyright (c) 2026 QEMU contributors\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#ifndef HW_MIPS_PIC32MK_ADCHS_H\n+#define HW_MIPS_PIC32MK_ADCHS_H\n+\n+#include \"hw/core/sysbus.h\"\n+#include \"qom/object.h\"\n+#include \"hw/mips/pic32mk.h\"\n+\n+#define TYPE_PIC32MK_ADCHS  \"pic32mk-adchs\"\n+OBJECT_DECLARE_SIMPLE_TYPE(PIC32MKADCHSState, PIC32MK_ADCHS)\n+\n+struct PIC32MKADCHSState {\n+    SysBusDevice parent_obj;\n+    MemoryRegion mr;\n+\n+    /* Control registers */\n+    uint32_t adccon1;\n+    uint32_t adccon2;\n+    uint32_t adccon3;\n+    uint32_t adctrgmode;\n+\n+    /* Input mode control */\n+    uint32_t adcimcon[4];\n+\n+    /* Interrupt enable (result ready) */\n+    uint32_t adcgirqen[2];\n+\n+    /* Channel scan select */\n+    uint32_t adccss[2];\n+\n+    /* Data ready status */\n+    uint32_t adcdstat[2];\n+\n+    /* Compare enable & values */\n+    uint32_t adccmpen[4];\n+    uint32_t adccmp[4];\n+    uint32_t adccmpcon[4];\n+\n+    /* Digital filters */\n+    uint32_t adcfltr[4];\n+\n+    /* Trigger configuration */\n+    uint32_t adctrg[7];\n+    uint32_t adctrgsns;\n+\n+    /* Sampling time per module */\n+    uint32_t adctime[6];   /* ADC0TIME … ADC5TIME */\n+\n+    /* Early interrupt enable / status */\n+    uint32_t adceien[2];\n+    uint32_t adceistat[2];\n+\n+    /* Analog module control */\n+    uint32_t adcancon;\n+\n+    /* Misc */\n+    uint32_t adcbase;\n+\n+    /* Per-module configuration */\n+    uint32_t adccfg[8];    /* ADC0CFG … ADC7CFG */\n+\n+    /* System configuration */\n+    uint32_t adcsyscfg[2];\n+\n+    /* Conversion data registers (indexed by channel 0–53) */\n+    uint32_t adcdata[PIC32MK_ADC_MAX_CH];\n+\n+    /*\n+     * Host-injectable analog input values (12-bit, 0–4095).\n+     * When a conversion is triggered, analog_input[ch] is copied to\n+     * adcdata[ch].  Set via QOM property \"adc-ch<N>\".\n+     */\n+    uint16_t analog_input[PIC32MK_ADC_MAX_CH];\n+\n+    /* IRQ outputs to EVIC */\n+    qemu_irq irq_eos;     /* End-of-scan  (IRQ 101) */\n+    qemu_irq irq_main;    /* Main ADC     (IRQ 92)  */\n+};\n+\n+#endif /* HW_MIPS_PIC32MK_ADCHS_H */\ndiff --git a/include/hw/mips/pic32mk_canfd.h b/include/hw/mips/pic32mk_canfd.h\nnew file mode 100644\nindex 0000000000..3408c54e31\n--- /dev/null\n+++ b/include/hw/mips/pic32mk_canfd.h\n@@ -0,0 +1,210 @@\n+/*\n+ * Microchip PIC32MK CAN FD controller — device interface\n+ * Based on PIC32MK GPK/MCM with CAN FD Family Datasheet (DS60001519E)\n+ * and Microchip CAN FD Controller Reference Manual (DS60001507).\n+ *\n+ * Copyright (c) 2026 QEMU contributors\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#ifndef HW_MIPS_PIC32MK_CANFD_H\n+#define HW_MIPS_PIC32MK_CANFD_H\n+\n+#include \"hw/core/sysbus.h\"\n+#include \"net/can_emu.h\"\n+#include \"qom/object.h\"\n+\n+#define TYPE_PIC32MK_CANFD  \"pic32mk-canfd\"\n+OBJECT_DECLARE_SIMPLE_TYPE(PIC32MKCANFDState, PIC32MK_CANFD)\n+\n+/*\n+ * CAN FD SFR register offsets (from instance base).\n+ *\n+ * Layout verified against XC32 device header p32mk1024mcm100.h.\n+ * Each logical register occupies 0x10 bytes:\n+ *   +0x0 = base, +0x4 = SET, +0x8 = CLR, +0xC = INV\n+ * -----------------------------------------------------------------------\n+ */\n+\n+#define CANFD_CiCON         0x000u  /* Control */\n+#define CANFD_CiNBTCFG      0x010u  /* Nominal bit-time config */\n+#define CANFD_CiDBTCFG      0x020u  /* Data bit-time config */\n+#define CANFD_CiTDC         0x030u  /* Transmitter delay compensation */\n+#define CANFD_CiTBC         0x040u  /* Time base counter */\n+#define CANFD_CiTSCON       0x050u  /* Timestamp control */\n+#define CANFD_CiVEC         0x060u  /* Interrupt flag code */\n+#define CANFD_CiINT         0x070u  /* Interrupt aggregator */\n+#define CANFD_CiRXIF        0x080u  /* RX interrupt flags per FIFO */\n+#define CANFD_CiTXIF        0x090u  /* TX interrupt flags per FIFO */\n+#define CANFD_CiRXOVIF      0x0A0u  /* RX overflow flags per FIFO */\n+#define CANFD_CiTXATIF      0x0B0u  /* TX attempts interrupt flags */\n+#define CANFD_CiTXREQ       0x0C0u  /* TX request bits per FIFO */\n+#define CANFD_CiTREC        0x0D0u  /* TX/RX error counters */\n+#define CANFD_CiBDIAG0      0x0E0u  /* Bus diagnostic 0 */\n+#define CANFD_CiBDIAG1      0x0F0u  /* Bus diagnostic 1 */\n+#define CANFD_CiTEFCON      0x100u  /* TX Event FIFO control */\n+#define CANFD_CiTEFSTA      0x110u  /* TX Event FIFO status */\n+#define CANFD_CiTEFUA       0x120u  /* TX Event FIFO user address */\n+#define CANFD_CiFIFOBA      0x130u  /* Message RAM base address (Phase 3C) */\n+#define CANFD_CiTXQCON      0x140u  /* TX Queue control */\n+#define CANFD_CiTXQSTA      0x150u  /* TX Queue status */\n+#define CANFD_CiTXQUA       0x160u  /* TX Queue user address */\n+/* FIFO n (n=1..31): stride 0x30 (3 registers × 0x10 each) */\n+#define CANFD_CiFIFOCON(n)  (0x170u + ((n) - 1u) * 0x30u)\n+#define CANFD_CiFIFOSTA(n)  (0x180u + ((n) - 1u) * 0x30u)\n+#define CANFD_CiFIFOUA(n)   (0x190u + ((n) - 1u) * 0x30u)\n+/* Filter control: stride 0x10 per register, 8 registers */\n+#define CANFD_CiFLTCON(r)   (0x740u + (r) * 0x10u)\n+/* Filter object/mask pairs: stride 0x20 per pair */\n+#define CANFD_CiFLTOBJ(n)   (0x7C0u + (n) * 0x20u)\n+#define CANFD_CiMASK(n)     (0x7D0u + (n) * 0x20u)\n+\n+/*\n+ * CiCON bit fields\n+ * -----------------------------------------------------------------------\n+ */\n+/*\n+ * CiCON field positions verified against DS60001519E / MCP251xFD IP core:\n+ *   reset value 0x04980760 → REQOP=4 (Config), OPMOD=4 (Config)\n+ */\n+#define CANFD_CON_REQOP_SHIFT   24u\n+#define CANFD_CON_REQOP_MASK    (0x7u << CANFD_CON_REQOP_SHIFT)\n+#define CANFD_CON_OPMOD_SHIFT   21u\n+#define CANFD_CON_OPMOD_MASK    (0x7u << CANFD_CON_OPMOD_SHIFT)\n+#define CANFD_CON_TXQEN         (1u << 20u)\n+#define CANFD_CON_STEF          (1u << 19u)\n+#define CANFD_CON_ABAT          (1u << 27u)\n+#define CANFD_CON_RESET         0x04980760u  /* Config mode on reset */\n+\n+/* OPMOD values */\n+#define CANFD_OPMOD_NORMAL      0u\n+#define CANFD_OPMOD_RESTRICTED  1u\n+#define CANFD_OPMOD_LISTEN      2u\n+#define CANFD_OPMOD_CONFIG      4u\n+#define CANFD_OPMOD_EXT_LOOP    5u\n+#define CANFD_OPMOD_INT_LOOP    7u\n+\n+/*\n+ * CiINT bit fields — verified against XC32 p32mk1024mcm100.h\n+ * Lower 16 bits = status flags, upper 16 bits = enable bits.\n+ * -----------------------------------------------------------------------\n+ */\n+#define CANFD_INT_TXIF      (1u << 0u)   /* TX FIFO interrupt flag */\n+#define CANFD_INT_RXIF      (1u << 1u)   /* RX FIFO interrupt flag */\n+#define CANFD_INT_MODIF     (1u << 3u)   /* Mode change flag */\n+#define CANFD_INT_TEFIF     (1u << 4u)   /* TEF interrupt flag */\n+#define CANFD_INT_TXATIF    (1u << 10u)  /* TX attempts flag */\n+#define CANFD_INT_RXOVIF    (1u << 11u)  /* RX overflow flag */\n+#define CANFD_INT_TXIE      (1u << 16u)  /* TX IRQ enable */\n+#define CANFD_INT_RXIE      (1u << 17u)  /* RX IRQ enable */\n+#define CANFD_INT_MODIE     (1u << 19u)  /* Mode change IRQ enable */\n+#define CANFD_INT_RXOVIE    (1u << 27u)  /* RX overflow IRQ enable */\n+#define CANFD_INT_SERRIE    (1u << 28u)  /* System error IRQ enable */\n+#define CANFD_INT_CERRIE    (1u << 29u)  /* CAN bus error IRQ enable */\n+#define CANFD_INT_IVMIE     (1u << 31u)  /* Invalid message IRQ enable */\n+\n+/*\n+ * CiFIFOCONn / CiTXQCON bit fields\n+ * Bit positions verified against XC32 p32mk1024mcm100.h device header.\n+ * -----------------------------------------------------------------------\n+ */\n+#define CANFD_FIFO_PLSIZE_SHIFT 29u\n+#define CANFD_FIFO_PLSIZE_MASK  (0x7u << CANFD_FIFO_PLSIZE_SHIFT)\n+#define CANFD_FIFO_FSIZE_SHIFT  24u\n+#define CANFD_FIFO_FSIZE_MASK   (0x1Fu << CANFD_FIFO_FSIZE_SHIFT)\n+#define CANFD_FIFO_TFNRFNIE     (1u << 0u)   /* RX: FIFO Not Empty interrupt enable */\n+#define CANFD_FIFO_TXEN         (1u << 7u)   /* TX enable (1=TX FIFO, 0=RX FIFO) */\n+#define CANFD_FIFO_UINC         (1u << 8u)   /* User increment (pulse bit) */\n+#define CANFD_FIFO_TXREQ        (1u << 9u)   /* TX request (pulse bit) */\n+#define CANFD_FIFO_FRESET       (1u << 10u)  /* FIFO reset (pulse bit) */\n+\n+/* CiFIFOSTAn bit fields — verified against XC32 device header */\n+#define CANFD_FIFOSTA_TFNRFNIF  (1u << 0u)  /* TX not full / RX not empty */\n+#define CANFD_FIFOSTA_TXATIF    (1u << 4u)  /* TX attempts exhausted */\n+#define CANFD_TXQSTA_TXQNIF     (1u << 0u)  /* CiTXQSTA: TX Queue not full */\n+\n+/*\n+ * Device state\n+ * -----------------------------------------------------------------------\n+ */\n+\n+struct PIC32MKCANFDState {\n+    SysBusDevice parent_obj;\n+\n+    /* Two MemoryRegions per instance */\n+    MemoryRegion sfr_mmio;   /* SFR registers: PIC32MK_CAN_SFR_SIZE bytes */\n+    MemoryRegion msg_ram;    /* Message RAM: PIC32MK_CAN_MSGRAM_SIZE bytes */\n+\n+    /* Key SFRs (reset values per DS60001507 §3) */\n+    uint32_t con;            /* CiCON  — reset 0x04980760 */\n+    uint32_t nbtcfg;         /* CiNBTCFG */\n+    uint32_t dbtcfg;         /* CiDBTCFG */\n+    uint32_t tdc;            /* CiTDC */\n+    uint32_t tbc;            /* CiTBC (free-running) */\n+    uint32_t tscon;          /* CiTSCON */\n+    uint32_t vec;            /* CiVEC */\n+    uint32_t cint;           /* CiINT */\n+    uint32_t rxif;           /* CiRXIF */\n+    uint32_t txif;           /* CiTXIF */\n+    uint32_t rxovif;         /* CiRXOVIF */\n+    uint32_t txreq;          /* CiTXREQ */\n+    uint32_t trec;           /* CiTREC */\n+\n+    /* TXQ */\n+    uint32_t txqcon;\n+    uint32_t txqsta;\n+    uint32_t txqua;\n+    uint8_t  txq_head;       /* oldest pending frame (TX processes from here) */\n+    uint8_t  txq_tail;       /* next empty slot firmware will write to */\n+    uint8_t  txq_count;      /* number of frames queued */\n+\n+    /* TEF */\n+    uint32_t tefcon;\n+    uint32_t tefsta;\n+    uint32_t tefua;\n+\n+    /* Per-FIFO state (index 1–31; [0] unused) */\n+    uint32_t fifocon[32];\n+    uint32_t fifosta[32];\n+    uint32_t fifoua[32];\n+    uint8_t  fifo_head[32];\n+    uint8_t  fifo_tail[32];\n+    uint8_t  fifo_count[32];\n+\n+    /* Acceptance filters */\n+    uint32_t fltcon[8];      /* 4 filters per reg × 8 = 32 filters */\n+    uint32_t fltobj[32];\n+    uint32_t mask[32];\n+\n+    /* Message RAM backing store */\n+    uint8_t  *msg_ram_buf;\n+    uint32_t  msg_ram_phys;  /* physical base (injected via property) */\n+\n+    /* Instance index (0–3 for CAN1–4) */\n+    uint32_t  instance_id;\n+\n+    /* Single IRQ line to EVIC */\n+    qemu_irq irq;\n+\n+    /* SocketCAN virtual bus (Phase 3B) */\n+    CanBusClientState bus_client;\n+    CanBusState      *canbus;   /* linked can-bus object, or NULL */\n+\n+    /*\n+     * Bus-side software ring buffer — decouples SocketCAN delivery timing\n+     * from the guest CPU.  Frames land here when the target RX FIFO is full;\n+     * canfd_bus_buf_drain() moves them into the FIFO after each UINC.\n+      */\n+    uint32_t bus_buf_id[64];\n+    bool     bus_buf_xtd[64];\n+    bool     bus_buf_fdf[64];\n+    uint8_t  bus_buf_dlc[64];\n+    uint8_t  bus_buf_data[64][64];\n+    int      bus_buf_len[64];\n+    int      bus_buf_dest[64];\n+    int      bus_buf_head;\n+    int      bus_buf_tail;\n+    int      bus_buf_count;\n+};\n+\n+#endif /* HW_MIPS_PIC32MK_CANFD_H */\ndiff --git a/include/hw/mips/pic32mk_evic.h b/include/hw/mips/pic32mk_evic.h\nnew file mode 100644\nindex 0000000000..207309719c\n--- /dev/null\n+++ b/include/hw/mips/pic32mk_evic.h\n@@ -0,0 +1,45 @@\n+/*\n+ * PIC32MK EVIC — public API header\n+ *\n+ * Copyright (c) 2026 QEMU contributors\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#ifndef HW_MIPS_PIC32MK_EVIC_H\n+#define HW_MIPS_PIC32MK_EVIC_H\n+\n+#include \"qom/object.h\"\n+#include \"hw/core/sysbus.h\"\n+#include \"hw/core/irq.h\"\n+#include \"hw/mips/pic32mk.h\"\n+#include \"cpu.h\"\n+\n+#define TYPE_PIC32MK_EVIC   \"pic32mk-evic\"\n+OBJECT_DECLARE_SIMPLE_TYPE(PIC32MKEVICState, PIC32MK_EVIC)\n+\n+struct PIC32MKEVICState {\n+    SysBusDevice parent_obj;\n+    MemoryRegion mr;\n+\n+    uint32_t intcon;\n+    uint32_t priss;\n+    uint32_t intstat;\n+    uint32_t iptmr;\n+\n+    uint32_t ifsreg[8];\n+    uint32_t iecreg[8];\n+    uint32_t ipcreg[64];\n+    uint32_t offreg[191];\n+\n+    /*\n+     * Current hardware IRQ levels for each source.  Microchip IFS flags\n+     * are level-sensitive: if the source still asserts its line after\n+     * firmware clears IFSx, the flag is immediately re-set.\n+     */\n+    uint32_t irq_level[8];\n+\n+    qemu_irq cpu_irq[8];\n+    MIPSCPU *cpu;    /* set by board code for SW-IRQ Cause.IP fixup */\n+};\n+\n+#endif /* HW_MIPS_PIC32MK_EVIC_H */\ndiff --git a/include/hw/mips/pic32mk_usb.h b/include/hw/mips/pic32mk_usb.h\nnew file mode 100644\nindex 0000000000..342444e9db\n--- /dev/null\n+++ b/include/hw/mips/pic32mk_usb.h\n@@ -0,0 +1,181 @@\n+/*\n+ * Microchip PIC32MK USB OTG Full-Speed controller — device model header\n+ * Datasheet: DS60001519E §25; register addresses from p32mk1024mcm100.h\n+ *\n+ * Copyright (c) 2026 QEMU contributors\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#ifndef HW_MIPS_PIC32MK_USB_H\n+#define HW_MIPS_PIC32MK_USB_H\n+\n+#include \"hw/core/sysbus.h\"\n+#include \"qom/object.h\"\n+#include \"chardev/char-fe.h\"\n+#include \"qemu/timer.h\"\n+\n+/*\n+ * Register offsets from the USB instance SFR base (e.g. 0xBF889000)\n+ * Derived from p32mk1024mcm100.h register addresses.\n+ * All registers with CLR/SET/INV aliases follow the standard PIC32 convention\n+ * (+4 = CLR, +8 = SET, +C = INV), except where noted.\n+ * -----------------------------------------------------------------------\n+ */\n+\n+/* OTG registers */\n+#define PIC32MK_UxOTGIR     0x040u  /* OTG interrupt flags  (W1C, CLR alias only) */\n+#define PIC32MK_UxOTGIE     0x050u  /* OTG interrupt enable (CLR/SET/INV) */\n+#define PIC32MK_UxOTGSTAT   0x060u  /* OTG status           (read-only) */\n+#define PIC32MK_UxOTGCON    0x070u  /* OTG control          (CLR/SET/INV) */\n+#define PIC32MK_UxPWRC      0x080u  /* Power control        (CLR/SET/INV) */\n+\n+/* USB core registers */\n+#define PIC32MK_UxIR        0x200u  /* Interrupt flags  (W1C, CLR alias only) */\n+#define PIC32MK_UxIE        0x210u  /* Interrupt enable (CLR/SET/INV) */\n+#define PIC32MK_UxEIR       0x220u  /* Error int flags  (W1C, CLR alias only) */\n+#define PIC32MK_UxEIE       0x230u  /* Error int enable (CLR/SET/INV) */\n+#define PIC32MK_UxSTAT      0x240u  /* Status           (read-only) */\n+#define PIC32MK_UxCON       0x250u  /* Control          (CLR/SET/INV) */\n+#define PIC32MK_UxADDR      0x260u  /* Device address   (CLR/SET/INV) */\n+#define PIC32MK_UxBDTP1     0x270u  /* BDT ptr [15:9]   (CLR/SET/INV) */\n+#define PIC32MK_UxFRML      0x280u  /* Frame# low       (read-only) */\n+#define PIC32MK_UxFRMH      0x290u  /* Frame# high      (read-only) */\n+#define PIC32MK_UxTOK       0x2A0u  /* Token register   (CLR/SET/INV) */\n+#define PIC32MK_UxSOF       0x2B0u  /* SOF threshold    (CLR/SET/INV) */\n+#define PIC32MK_UxBDTP2     0x2C0u  /* BDT ptr [23:16]  (CLR/SET/INV) */\n+#define PIC32MK_UxBDTP3     0x2D0u  /* BDT ptr [31:24]  (CLR/SET/INV) */\n+#define PIC32MK_UxCNFG1     0x2E0u  /* Config 1         (CLR/SET/INV) */\n+\n+/* Endpoint control registers UxEPn: base + 0x300 + n * 0x10, n = 0..15 */\n+#define PIC32MK_UxEP_BASE   0x300u\n+#define PIC32MK_UxEP_STRIDE 0x010u\n+#define PIC32MK_USB_NEPS    16\n+\n+/* MMIO size per instance — covers all registers up to UxEP15+CLR/SET/INV */\n+#define PIC32MK_USB_SFR_SIZE 0x1000u\n+\n+/* UxCON bits */\n+#define USB_CON_USBEN   (1u << 0)   /* USB enable (device) / SOF (host) */\n+#define USB_CON_PPBRST  (1u << 1)   /* Ping-pong buffer reset */\n+#define USB_CON_RESUME  (1u << 3)   /* Resume signaling */\n+#define USB_CON_HOSTEN  (1u << 4)   /* Host mode enable */\n+#define USB_CON_RESET   (1u << 4)   /* Bus reset (host) */\n+\n+/* UxOTGSTAT bits */\n+#define USB_OTGSTAT_SESVD  (1u << 3)  /* Session valid (VBUS > V_A_SESS_VLD) */\n+\n+/* UxOTGIR / UxOTGIE bits (Harmony: USB_OTG_INTERRUPTS enum) */\n+#define USB_OTG_IR_SESSION_VALID   (1u << 3)  /* VBUS session valid change */\n+#define USB_OTG_IR_ACTIVITY_DETECT (1u << 4)  /* USB activity detected */\n+\n+/* UxPWRC bits */\n+#define USB_PWRC_USBPWR  (1u << 0)  /* USB power enable */\n+#define USB_PWRC_USUSPND (1u << 1)  /* Suspend */\n+\n+/* UxIR interrupt bits (same layout for UxIE) */\n+#define USB_IR_URSTIF   (1u << 0)   /* USB Reset */\n+#define USB_IR_UERRIF   (1u << 1)   /* Error */\n+#define USB_IR_SOFIF    (1u << 2)   /* Start-of-frame */\n+#define USB_IR_TRNIF    (1u << 3)   /* Token done */\n+#define USB_IR_IDLEIF   (1u << 4)   /* Idle detect */\n+#define USB_IR_RESUMEIF (1u << 5)   /* Resume */\n+#define USB_IR_ATTACHIF (1u << 6)   /* Attach (host mode) */\n+#define USB_IR_STALLIF  (1u << 7)   /* Stall sent */\n+\n+/* UxSTAT encoding: EP[7:4] | DIR[3] | PPBI[2] */\n+#define USB_STAT_EP0_OUT_EVEN   0x00u\n+#define USB_STAT_EP0_IN_EVEN    0x08u\n+#define USB_STAT_EP1_IN_EVEN    0x18u\n+\n+/* BDT entry control word bits */\n+#define BDT_UOWN   (1u << 7)    /* USB owns this buffer (firmware arms it) */\n+\n+/*\n+ * Phase 4B EP0 enumeration state machine\n+ * -----------------------------------------------------------------------\n+ */\n+\n+typedef enum {\n+    EP0_SIM_IDLE,            /* waiting for USBPWR / USBEN */\n+    EP0_SIM_RESET,           /* fire USB Reset interrupt */\n+    EP0_SIM_GET_DEV_DESC,    /* inject GET_DESCRIPTOR(Device, 18) */\n+    EP0_SIM_WAIT_DEV_DESC,   /* waiting for firmware to fill EP0-IN */\n+    EP0_SIM_SET_ADDRESS,     /* inject SET_ADDRESS(1) */\n+    EP0_SIM_WAIT_ADDRESS,    /* waiting for status phase */\n+    EP0_SIM_GET_CFG_DESC,    /* inject GET_DESCRIPTOR(Config, 67) */\n+    EP0_SIM_WAIT_CFG_DESC,   /* waiting for firmware */\n+    EP0_SIM_SET_CONFIG,      /* inject SET_CONFIGURATION(1) */\n+    EP0_SIM_WAIT_CONFIG,     /* waiting for status phase */\n+    EP0_SIM_DONE,            /* fully enumerated — poll CDC TX BDT */\n+} EP0SimState;\n+\n+/*\n+ * QEMU device model\n+ * -----------------------------------------------------------------------\n+ */\n+\n+#define TYPE_PIC32MK_USB \"pic32mk-usb\"\n+OBJECT_DECLARE_SIMPLE_TYPE(PIC32MKUSBState, PIC32MK_USB)\n+\n+struct PIC32MKUSBState {\n+    SysBusDevice parent_obj;\n+\n+    MemoryRegion sfr_mmio;   /* 0x1000 bytes */\n+\n+    /* OTG / Power registers */\n+    uint32_t otgir;          /* UxOTGIR — OTG interrupt flags */\n+    uint32_t otgie;          /* UxOTGIE */\n+    uint32_t otgstat;        /* UxOTGSTAT (read-only) */\n+    uint32_t otgcon;         /* UxOTGCON */\n+    uint32_t pwrc;           /* UxPWRC */\n+\n+    /* Core interrupt registers */\n+    uint32_t uir;            /* UxIR  — USB interrupt flags */\n+    uint32_t uie;            /* UxIE */\n+    uint32_t ueir;           /* UxEIR — USB error interrupt flags */\n+    uint32_t ueie;           /* UxEIE */\n+\n+    /* Control / Status */\n+    uint32_t ustat;          /* UxSTAT (read-only, updated by emulator) */\n+    uint32_t ucon;           /* UxCON */\n+    uint32_t uaddr;          /* UxADDR */\n+\n+    /* BDT address pages */\n+    uint32_t bdtp1;          /* UxBDTP1 — bits [15:9] of BDT base */\n+    uint32_t bdtp2;          /* UxBDTP2 — bits [23:16] */\n+    uint32_t bdtp3;          /* UxBDTP3 — bits [31:24] */\n+\n+    /* Frame counter (read-only; advanced by timer) */\n+    uint32_t ufrml;\n+    uint32_t ufrmh;\n+\n+    /* Token / SOF */\n+    uint32_t utok;\n+    uint32_t usof;\n+\n+    /* Config */\n+    uint32_t cnfg1;\n+\n+    /* Endpoint control UxEP0–UxEP15 */\n+    uint32_t uep[PIC32MK_USB_NEPS];\n+\n+    /* IRQ line to EVIC */\n+    qemu_irq irq;\n+\n+    /* ---- Phase 4B additions ---- */\n+    QEMUTimer   *usb_timer;     /* drives enumeration + BDT poll */\n+    CharFrontend chr;           /* virtual serial port for CDC data */\n+    EP0SimState  ep0_sim;       /* enumeration state machine */\n+    bool         configured;    /* SET_CONFIGURATION accepted */\n+    bool         sesvd_edge_latched; /* SESSION_VALID edge latch for IRQ */\n+    bool         sesvd_acked;        /* firmware has W1C-cleared SESVDIF at least once */\n+    int          sesvd_retry_count;  /* fallback re-fire counter */\n+\n+    /* ---- USTAT FIFO (PIC32MK has a 4-deep hardware FIFO) ---- */\n+    uint32_t stat_fifo[4];      /* circular buffer of pending USTAT values */\n+    int      stat_fifo_head;    /* next write position */\n+    int      stat_fifo_tail;    /* next read position */\n+    int      stat_fifo_count;   /* number of entries in FIFO */\n+};\n+\n+#endif /* HW_MIPS_PIC32MK_USB_H */\ndiff --git a/tests/functional/mipsel/pic32mk_test_fw.S b/tests/functional/mipsel/pic32mk_test_fw.S\nnew file mode 100644\nindex 0000000000..4e63bbd698\n--- /dev/null\n+++ b/tests/functional/mipsel/pic32mk_test_fw.S\n@@ -0,0 +1,64 @@\n+/*\n+ * pic32mk_test_fw.S — Minimal boot firmware for PIC32MK functional test\n+ *\n+ * Loaded by QEMU at physical 0x1FC40000 (KSEG1: 0xBFC40000) via -bios.\n+ * The board reset vector at 0xBFC00000 has a j 0xBFC40000 trampoline.\n+ *\n+ * Writes \"Hello, PIC32MK!\\r\\n\" to UART1 TX register then spins.\n+ * The functional test waits for this string on the serial console.\n+ *\n+ * UART1 SFR addresses (DS60001519E §21, Table 21-2):\n+ *   U1MODE  0xBF828000  bit 15 = UARTEN\n+ *   U1STA   0xBF828010  bit 10 = UTXEN\n+ *   U1TXREG 0xBF828020  TX data register (write-only)\n+ *   U1BRG   0xBF828040  baud rate divisor\n+ *\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+    .section .text\n+    .set    noreorder\n+    .set    mips32r2\n+    .global _start\n+    .type   _start, @function\n+_start:\n+    /* Disable all interrupts, enter kernel mode */\n+    mtc0    $zero, $12          /* CP0 Status = 0 */\n+    nop\n+\n+    /* Stack pointer = top of KSEG1 RAM (physical 256 KB = 0x00040000) */\n+    lui     $sp, 0xA004         /* 0xA0040000 */\n+    addiu   $sp, $sp, -8\n+\n+    /* ---- UART1 init ---- */\n+    lui     $t0, 0xBF82\n+    ori     $t0, $t0, 0x8000   /* t0 = 0xBF828000 (U1MODE base) */\n+\n+    addiu   $t1, $zero, 64\n+    sw      $t1, 0x40($t0)     /* U1BRG  = 64   (baud ignored by emulator) */\n+    ori     $t1, $zero, 0x0400\n+    sw      $t1, 0x10($t0)     /* U1STA  = UTXEN (bit 10) */\n+    lui     $t1, 0x8000\n+    sw      $t1, 0x00($t0)     /* U1MODE = UARTEN (bit 15) */\n+\n+    /* ---- Transmit string byte-by-byte ---- */\n+    addiu   $t2, $t0, 0x20     /* t2 = U1TXREG = 0xBF828020 */\n+    la      $t1, _msg\n+_putc:\n+    lbu     $t3, 0($t1)\n+    beq     $t3, $zero, _halt\n+    nop\n+    sw      $t3, 0($t2)\n+    addiu   $t1, $t1, 1\n+    b       _putc\n+    nop\n+\n+_halt:\n+    wait\n+    b       _halt\n+    nop\n+\n+    .size _start, . - _start\n+\n+_msg:\n+    .ascii  \"Hello, PIC32MK!\\r\\n\\0\"\ndiff --git a/tests/functional/mipsel/pic32mk_test_fw.ld b/tests/functional/mipsel/pic32mk_test_fw.ld\nnew file mode 100644\nindex 0000000000..7e817f276f\n--- /dev/null\n+++ b/tests/functional/mipsel/pic32mk_test_fw.ld\n@@ -0,0 +1,20 @@\n+/*\n+ * pic32mk_test_fw.ld — Linker script for PIC32MK functional test firmware\n+ *\n+ * Links everything at Boot Flash 1 (KSEG1 0xBFC40000, physical 0x1FC40000).\n+ * QEMU loads the raw binary there via load_image_targphys().\n+ *\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+OUTPUT_ARCH(mips)\n+ENTRY(_start)\n+\n+SECTIONS {\n+    . = 0xBFC40000;\n+    .text : {\n+        *(.text*)\n+        *(.rodata*)\n+    }\n+    /DISCARD/ : { *(*) }\n+}\ndiff --git a/tests/functional/mipsel/test_pic32mk.py b/tests/functional/mipsel/test_pic32mk.py\nnew file mode 100644\nindex 0000000000..4af91c4f88\n--- /dev/null\n+++ b/tests/functional/mipsel/test_pic32mk.py\n@@ -0,0 +1,92 @@\n+#!/usr/bin/env python3\n+#\n+# Functional tests for the PIC32MK board emulation\n+#\n+# Copyright (c) 2026 QEMU contributors\n+# SPDX-License-Identifier: GPL-2.0-or-later\n+\n+import os\n+\n+from qemu_test import QemuSystemTest\n+from qemu_test import wait_for_console_pattern\n+\n+\n+class PIC32MKMachine(QemuSystemTest):\n+    \"\"\"\n+    Tests for the PIC32MK MCU board (-M pic32mk).\n+\n+    The FreeRTOS firmware is expected to be pre-built at\n+    tests/freertos/hello_freertos.bin relative to the source tree.\n+    \"\"\"\n+\n+    timeout = 30\n+\n+    def get_firmware_path(self):\n+        \"\"\"Locate the pre-built FreeRTOS firmware binary.\"\"\"\n+        # data_file() resolves relative to tests/functional/\n+        # We need to go up two levels to reach the source root\n+        src_root = os.path.join(\n+            os.path.dirname(__file__), '..', '..', '..')\n+        fw_path = os.path.realpath(\n+            os.path.join(src_root, 'tests', 'freertos', 'hello_freertos.bin'))\n+        if not os.path.isfile(fw_path):\n+            self.skipTest(\n+                f'FreeRTOS firmware not built: {fw_path}\\n'\n+                'Run: make -C tests/freertos')\n+        return fw_path\n+\n+    def test_boot_message(self):\n+        \"\"\"PIC32MK boots and prints the FreeRTOS banner.\"\"\"\n+        self.set_machine('pic32mk')\n+        self.vm.set_console()\n+        self.vm.add_args('-bios', self.get_firmware_path(),\n+                         '-nographic')\n+        self.vm.launch()\n+        wait_for_console_pattern(self,\n+                                 'PIC32MK QEMU booting FreeRTOS...')\n+\n+    def test_freertos_hello_task(self):\n+        \"\"\"FreeRTOS scheduler starts and the Hello task fires.\"\"\"\n+        self.set_machine('pic32mk')\n+        self.vm.set_console()\n+        self.vm.add_args('-bios', self.get_firmware_path(),\n+                         '-nographic')\n+        self.vm.launch()\n+        # First: boot banner\n+        wait_for_console_pattern(self,\n+                                 'PIC32MK QEMU booting FreeRTOS...')\n+        # Second: at least one Hello message (proves Timer1 tick + UART TX IRQ)\n+        wait_for_console_pattern(self,\n+                                 'Hello from FreeRTOS!')\n+\n+    def test_freertos_recurring_ticks(self):\n+        \"\"\"Timer1 delivers recurring ticks — multiple Hello messages appear.\"\"\"\n+        self.set_machine('pic32mk')\n+        self.vm.set_console()\n+        self.vm.add_args('-bios', self.get_firmware_path(),\n+                         '-nographic')\n+        self.vm.launch()\n+        wait_for_console_pattern(self,\n+                                 'PIC32MK QEMU booting FreeRTOS...')\n+        # Wait for a second Hello — proves the tick ISR fires repeatedly\n+        # and context switches work (CS0 yield + Timer1 preemption).\n+        wait_for_console_pattern(self,\n+                                 'Hello from FreeRTOS!')\n+        wait_for_console_pattern(self,\n+                                 'Hello from FreeRTOS!')\n+\n+    def test_freertos_ping_task(self):\n+        \"\"\"Second FreeRTOS task (Ping) also runs — proves multitasking.\"\"\"\n+        self.set_machine('pic32mk')\n+        self.vm.set_console()\n+        self.vm.add_args('-bios', self.get_firmware_path(),\n+                         '-nographic')\n+        self.vm.launch()\n+        wait_for_console_pattern(self,\n+                                 'PIC32MK QEMU booting FreeRTOS...')\n+        # Ping task fires every 5 seconds\n+        wait_for_console_pattern(self, 'Ping')\n+\n+\n+if __name__ == '__main__':\n+    QemuSystemTest.main()\ndiff --git a/tests/qtest/pic32mk-canfd-test.c b/tests/qtest/pic32mk-canfd-test.c\nnew file mode 100644\nindex 0000000000..eafb28b3d6\n--- /dev/null\n+++ b/tests/qtest/pic32mk-canfd-test.c\n@@ -0,0 +1,411 @@\n+/*\n+ * QTest for PIC32MK CAN FD controller emulation (pic32mk_canfd.c)\n+ *\n+ * Exercises:\n+ *   - Reset state of CiCON, CiINT, CiTXQSTA\n+ *   - Mode transitions: Config → Normal, Config → Internal Loopback\n+ *   - CiINT.MODIF assertion and firmware clear via read-modify-write\n+ *   - TXQ / FIFO configuration and UA register computation\n+ *   - Internal loopback: TX frame → acceptance filter → RX FIFO\n+ *   - UINC pointer protocol (TX and RX sides)\n+ *   - RX overflow detection via CiRXOVIF / CiINT.RXOVIF\n+ *   - IRQ flag propagation (CiRXIF, CiTXIF, CiINT.RXIF, CiINT.TXIF)\n+ *\n+ * All addresses are physical (QTest bypasses the MIPS KSEG1 mapping).\n+ * CAN FD SFR registers use 0x10-byte blocks: base/+4(SET)/+8(CLR)/+C(INV).\n+ * Tests use plain writes to the base address (sub=0).\n+ *\n+ * Register offsets verified against XC32 p32mk1024mcm100.h device header.\n+ *\n+ * Copyright (c) 2026 QEMU contributors\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+#include \"libqtest-single.h\"\n+\n+/*\n+ * Address map\n+ * Physical SFR base = 0x1F800000\n+ * CAN1 SFR  = SFR + 0x080000 = 0x1F880000\n+ * CAN1 MSGRAM = 0x1F900000  (placeholder, Phase 3C will use CiFIFOBA)\n+ * -----------------------------------------------------------------------\n+  */\n+\n+#define SFR_BASE            0x1F800000u\n+#define CAN1_BASE           (SFR_BASE + 0x080000u)\n+#define CAN1_MSGRAM_BASE    0x1F900000u\n+\n+/* CiXxx register physical addresses for CAN1 (base of each 0x10-byte block) */\n+#define CiCON               (CAN1_BASE + 0x000u)\n+#define CiNBTCFG            (CAN1_BASE + 0x010u)\n+#define CiDBTCFG            (CAN1_BASE + 0x020u)\n+#define CiINT               (CAN1_BASE + 0x070u)\n+#define CiRXIF              (CAN1_BASE + 0x080u)\n+#define CiTXIF              (CAN1_BASE + 0x090u)\n+#define CiRXOVIF            (CAN1_BASE + 0x0A0u)\n+#define CiTXREQ             (CAN1_BASE + 0x0C0u)\n+#define CiTEFCON            (CAN1_BASE + 0x100u)\n+#define CiFIFOBA            (CAN1_BASE + 0x130u)\n+#define CiTXQCON            (CAN1_BASE + 0x140u)\n+#define CiTXQSTA            (CAN1_BASE + 0x150u)\n+#define CiTXQUA             (CAN1_BASE + 0x160u)\n+\n+/* FIFO1 registers: base = 0x170 + (1-1)*0x30 = 0x170 */\n+#define CiFIFOCON1          (CAN1_BASE + 0x170u)\n+#define CiFIFOSTA1          (CAN1_BASE + 0x180u)\n+#define CiFIFOUA1           (CAN1_BASE + 0x190u)\n+\n+/* FIFO2 registers: base = 0x170 + (2-1)*0x30 = 0x1A0 */\n+#define CiFIFOCON2          (CAN1_BASE + 0x1A0u)\n+#define CiFIFOSTA2          (CAN1_BASE + 0x1B0u)\n+#define CiFIFOUA2           (CAN1_BASE + 0x1C0u)\n+\n+/* Filter control: stride 0x10 per reg (r=0..7) */\n+#define CiFLTCON0           (CAN1_BASE + 0x740u)\n+\n+/* Filter object/mask: stride 0x20 per pair */\n+#define CiFLTOBJ0           (CAN1_BASE + 0x7C0u)\n+#define CiMASK0             (CAN1_BASE + 0x7D0u)\n+\n+/* SET/CLR aliases for key registers (used in some tests) */\n+#define CiCON_SET           (CAN1_BASE + 0x004u)\n+#define CiCON_CLR           (CAN1_BASE + 0x008u)\n+\n+/*\n+ * CiCON field masks — verified against reset value 0x04980760\n+ * -----------------------------------------------------------------------\n+  */\n+#define CON_REQOP_SHIFT     24u\n+#define CON_REQOP_MASK      (0x7u << CON_REQOP_SHIFT)\n+#define CON_OPMOD_SHIFT     21u\n+#define CON_OPMOD_MASK      (0x7u << CON_OPMOD_SHIFT)\n+#define CON_TXQEN           (1u << 20u)\n+#define CON_RESET_VAL       0x04980760u\n+\n+#define OPMOD_NORMAL        0u\n+#define OPMOD_CONFIG        4u\n+#define OPMOD_INT_LOOP      7u\n+\n+/* CiINT bits — verified against XC32 device header */\n+#define INT_TXIF            (1u << 0u)\n+#define INT_RXIF            (1u << 1u)\n+#define INT_MODIF           (1u << 3u)   /* bit 3 on real hardware */\n+#define INT_RXOVIF          (1u << 11u)\n+#define INT_TXIE            (1u << 16u)  /* bit 16 on real hardware */\n+#define INT_RXIE            (1u << 17u)  /* bit 17 on real hardware */\n+#define INT_MODIE           (1u << 19u)  /* bit 19 on real hardware */\n+#define INT_RXOVIE          (1u << 27u)\n+\n+/* CiFIFOCONn / CiTXQCON bits — verified against XC32 device header */\n+#define FIFO_PLSIZE_SHIFT   29u\n+#define FIFO_FSIZE_SHIFT    24u\n+#define FIFO_TXEN           (1u << 7u)   /* bit 7 on real hardware */\n+#define FIFO_UINC           (1u << 8u)   /* bit 8 on real hardware */\n+#define FIFO_TXREQ          (1u << 9u)   /* bit 9 on real hardware */\n+#define FIFO_FRESET         (1u << 10u)  /* bit 10 on real hardware */\n+\n+/* CiFIFOSTAn bits */\n+#define FIFOSTA_TFNRFNIF    (1u << 0u)   /* TX not full / RX not empty */\n+#define FIFOSTA_TXATIF      (1u << 4u)\n+\n+/* CiTXQSTA bits */\n+#define TXQSTA_TXQNIF       (1u << 0u)   /* bit 0 on real hardware */\n+\n+/*\n+ * Helpers\n+ * -----------------------------------------------------------------------\n+  */\n+\n+/* Enter the requested mode; asserts OPMOD matches after write */\n+static void set_mode(uint32_t opmod_val)\n+{\n+    uint32_t con = readl(CiCON);\n+    con = (con & ~CON_REQOP_MASK) | (opmod_val << CON_REQOP_SHIFT);\n+    writel(CiCON, con);\n+    uint32_t result = readl(CiCON);\n+    g_assert_cmphex((result & CON_OPMOD_MASK) >> CON_OPMOD_SHIFT,\n+                    ==, opmod_val);\n+}\n+\n+/* Clear specific bits in CiINT via read-modify-write */\n+static void clear_cint_bits(uint32_t mask)\n+{\n+    writel(CiINT, readl(CiINT) & ~mask);\n+}\n+\n+/*\n+ * T1: Reset state\n+ * -----------------------------------------------------------------------\n+  */\n+\n+static void test_reset_state(void)\n+{\n+    /* CiCON must come up in Config mode */\n+    uint32_t con = readl(CiCON);\n+    g_assert_cmphex(con, ==, CON_RESET_VAL);\n+\n+    uint8_t opmod = (con & CON_OPMOD_MASK) >> CON_OPMOD_SHIFT;\n+    g_assert_cmpuint(opmod, ==, OPMOD_CONFIG);\n+\n+    /* CiINT must be clear */\n+    g_assert_cmphex(readl(CiINT), ==, 0u);\n+\n+    /* CiTXQSTA: TXQNIF (bit 0) should be set (slot available) */\n+    g_assert_true((readl(CiTXQSTA) & TXQSTA_TXQNIF) != 0u);\n+}\n+\n+/*\n+ * T2: Mode transition Config → Normal, MODIF assertion, and clear\n+ * -----------------------------------------------------------------------\n+  */\n+\n+static void test_mode_transition(void)\n+{\n+    /* Start in Config mode (reset state) */\n+    g_assert_cmpuint((readl(CiCON) & CON_OPMOD_MASK) >> CON_OPMOD_SHIFT,\n+                     ==, OPMOD_CONFIG);\n+\n+    /* Request Normal mode */\n+    set_mode(OPMOD_NORMAL);\n+\n+    /* MODIF (bit 3) must be set in CiINT */\n+    g_assert_true((readl(CiINT) & INT_MODIF) != 0u);\n+\n+    /* Clear MODIF via read-modify-write */\n+    clear_cint_bits(INT_MODIF);\n+    g_assert_false((readl(CiINT) & INT_MODIF) != 0u);\n+\n+    /* Transition back to Config mode for subsequent tests */\n+    set_mode(OPMOD_CONFIG);\n+    clear_cint_bits(INT_MODIF);\n+}\n+\n+/*\n+ * T3: TXQ configuration and UA register computation\n+ * -----------------------------------------------------------------------\n+  */\n+\n+static void test_txq_configure(void)\n+{\n+    /* Must be in Config mode */\n+    set_mode(OPMOD_CONFIG);\n+\n+    /*\n+     * Configure TXQ: PLSIZE=0 (8-byte payload), FSIZE=0 (1 entry).\n+     * Enable TXQ in CiCON via read-modify-write.\n+     */\n+    uint32_t txqcon = (0u << FIFO_PLSIZE_SHIFT) | (0u << FIFO_FSIZE_SHIFT);\n+    writel(CiTXQCON, txqcon);\n+    writel(CiCON, readl(CiCON) | CON_TXQEN);\n+\n+    /* UA must point to the message RAM base for CAN1 */\n+    uint32_t ua = readl(CiTXQUA);\n+    g_assert_cmphex(ua, ==, CAN1_MSGRAM_BASE);\n+}\n+\n+/*\n+ * T4: Internal loopback — TX a frame via TXQ, receive it in FIFO1\n+ * -----------------------------------------------------------------------\n+  */\n+\n+static void test_loopback_tx_rx(void)\n+{\n+    /* --- Configure in Config mode --- */\n+    set_mode(OPMOD_CONFIG);\n+\n+    /* TXQ: 8-byte payload, depth 2 (FSIZE=1) */\n+    uint32_t txqcon = (0u << FIFO_PLSIZE_SHIFT) | (1u << FIFO_FSIZE_SHIFT);\n+    writel(CiTXQCON, txqcon);\n+    writel(CiCON, readl(CiCON) | CON_TXQEN);\n+\n+    /* FIFO1 as RX: TXEN=0, PLSIZE=0 (8B), FSIZE=1 (2 entries), FRESET */\n+    uint32_t fifocon1 = (0u << FIFO_PLSIZE_SHIFT) | (1u << FIFO_FSIZE_SHIFT)\n+                      | FIFO_FRESET;\n+    writel(CiFIFOCON1, fifocon1);\n+\n+    /*\n+     * Filter 0: match SID 0x123 exactly.\n+     * CiFLTOBJ0: standard ID in bits [10:0] (XTD=0).\n+     * CiMASK0: 0x1FFFFFFF forces all SID bits to match.\n+     * CiFLTCON0 byte 0: FLTEN=1 (bit7), FIFO=1 (bits [4:0]) → 0x81.\n+     */\n+    writel(CiFLTOBJ0, 0x00000123u);  /* SID=0x123, XTD=0 */\n+    writel(CiMASK0,   0x1FFFFFFFu);  /* exact match */\n+    /* CiFLTCON0: byte 0 = 0x81 (FLTEN=1, FIFO destination=1) */\n+    writel(CiFLTCON0, 0x00000081u);\n+\n+    /* Enable RXIE and TXIE */\n+    writel(CiINT, readl(CiINT) | INT_RXIE | INT_TXIE);\n+\n+    /* Enter internal loopback mode */\n+    set_mode(OPMOD_INT_LOOP);\n+    clear_cint_bits(INT_MODIF);\n+\n+    /* --- Write TX object at TXQ UA --- */\n+    uint32_t txqua = readl(CiTXQUA);\n+    g_assert_cmphex(txqua, !=, 0u);\n+\n+    /*\n+     * T0: Standard ID 0x123 in bits [28:18] (shifted left by 18).\n+     * T1: DLC=2 (2 bytes), classic CAN (FDF=0).\n+     * Payload: 0xDEAD in first two bytes (little-endian word).\n+     */\n+    uint32_t t0 = 0x123u << 18u;\n+    uint32_t t1 = 2u;\n+    uint32_t payload = 0x0000DEADu;\n+\n+    writel(txqua + 0u, t0);\n+    writel(txqua + 4u, t1);\n+    writel(txqua + 8u, payload);\n+\n+    /* UINC — advance write pointer (hardware clears the bit) */\n+    writel(CiTXQCON, readl(CiTXQCON) | FIFO_UINC);\n+    g_assert_false((readl(CiTXQCON) & FIFO_UINC) != 0u);\n+\n+    /* TXREQ — trigger transmission */\n+    writel(CiTXQCON, readl(CiTXQCON) | FIFO_TXREQ);\n+\n+    /* --- Verify RX delivery --- */\n+\n+    /* CiRXIF bit 1 (FIFO1) must be set */\n+    g_assert_true((readl(CiRXIF) & (1u << 1u)) != 0u);\n+\n+    /* CiINT.RXIF must be set */\n+    g_assert_true((readl(CiINT) & INT_RXIF) != 0u);\n+\n+    /* CiINT.TXIF must be set */\n+    g_assert_true((readl(CiINT) & INT_TXIF) != 0u);\n+\n+    /* Read RX object from FIFO1 UA */\n+    uint32_t fifoua1 = readl(CiFIFOUA1);\n+    g_assert_cmphex(fifoua1, !=, 0u);\n+\n+    uint32_t r0    = readl(fifoua1 + 0u);\n+    uint32_t r1    = readl(fifoua1 + 4u);\n+    uint32_t rdata = readl(fifoua1 + 8u);\n+\n+    /* R0: standard ID in [28:18] */\n+    uint32_t rx_sid = (r0 >> 18u) & 0x7FFu;\n+    g_assert_cmphex(rx_sid, ==, 0x123u);\n+\n+    /* R1: DLC=2 */\n+    g_assert_cmpuint(r1 & 0xFu, ==, 2u);\n+\n+    /* FILHIT in R1[15:11] should be 1 (filter 0 maps to FIFO 1) */\n+    uint32_t filhit = (r1 >> 11u) & 0x1Fu;\n+    g_assert_cmpuint(filhit, ==, 1u);\n+\n+    /* Payload: first word */\n+    g_assert_cmphex(rdata & 0x0000FFFFu, ==, 0x0000DEADu);\n+\n+    /* UINC on FIFO1 — firmware done reading, advance read pointer */\n+    writel(CiFIFOCON1, readl(CiFIFOCON1) | FIFO_UINC);\n+    g_assert_false((readl(CiFIFOCON1) & FIFO_UINC) != 0u);\n+\n+    /* CiRXIF bit 1 must now be clear (FIFO empty) */\n+    g_assert_false((readl(CiRXIF) & (1u << 1u)) != 0u);\n+\n+    /* CiINT.RXIF must be clear */\n+    g_assert_false((readl(CiINT) & INT_RXIF) != 0u);\n+\n+    /* --- Cleanup --- */\n+    clear_cint_bits(INT_RXIE | INT_TXIE | INT_TXIF | INT_RXIF | INT_MODIF);\n+    set_mode(OPMOD_CONFIG);\n+    clear_cint_bits(INT_MODIF);\n+}\n+\n+/*\n+ * T5: RX overflow — FIFO depth 1, deliver 2 frames, verify RXOVIF\n+ * -----------------------------------------------------------------------\n+  */\n+\n+static void test_rx_overflow(void)\n+{\n+    set_mode(OPMOD_CONFIG);\n+\n+    /* TXQ: 8-byte payload, depth 4 (FSIZE=3) */\n+    uint32_t txqcon = (0u << FIFO_PLSIZE_SHIFT) | (3u << FIFO_FSIZE_SHIFT);\n+    writel(CiTXQCON, txqcon);\n+    writel(CiCON, readl(CiCON) | CON_TXQEN);\n+\n+    /* FIFO2 as RX, depth 1 (FSIZE=0) — will overflow on 2nd frame */\n+    writel(CiFIFOCON2, (0u << FIFO_PLSIZE_SHIFT) | (0u << FIFO_FSIZE_SHIFT)\n+                     | FIFO_FRESET);\n+\n+    /*\n+     * Filter 1 (byte 1 in CiFLTCON0): match all IDs → FIFO2.\n+     * byte0 = 0x00 (filter 0 disabled), byte1 = 0x82 (FLTEN=1, FIFO=2).\n+     */\n+    writel(CiFLTOBJ0, 0x00000000u);\n+    writel(CiMASK0,   0x00000000u);  /* no bits required to match */\n+    writel(CiFLTCON0, 0x00008200u);\n+\n+    /* Enable RXOVIE */\n+    writel(CiINT, readl(CiINT) | INT_RXOVIE | INT_RXIE);\n+\n+    set_mode(OPMOD_INT_LOOP);\n+    clear_cint_bits(INT_MODIF);\n+\n+    /* Send two frames via TXQ */\n+    for (int i = 0; i < 2; i++) {\n+        uint32_t ua = readl(CiTXQUA);\n+        writel(ua + 0u, 0u);           /* T0: SID=0, XTD=0 */\n+        writel(ua + 4u, 1u);           /* T1: DLC=1 */\n+        writel(ua + 8u, (uint32_t)i);  /* payload */\n+        writel(CiTXQCON, readl(CiTXQCON) | FIFO_UINC);\n+        writel(CiTXQCON, readl(CiTXQCON) | FIFO_TXREQ);\n+    }\n+\n+    /* CiRXOVIF bit 2 (FIFO2) must be set */\n+    g_assert_true((readl(CiRXOVIF) & (1u << 2u)) != 0u);\n+\n+    /* CiINT.RXOVIF must be set */\n+    g_assert_true((readl(CiINT) & INT_RXOVIF) != 0u);\n+\n+    /* Cleanup */\n+    writel(CiRXOVIF, 0u);\n+    clear_cint_bits(INT_RXOVIE | INT_RXIE | INT_RXOVIF | INT_RXIF | INT_MODIF);\n+    set_mode(OPMOD_CONFIG);\n+    clear_cint_bits(INT_MODIF);\n+}\n+\n+/*\n+ * T6: CiCON TXQEN bit set/clear via read-modify-write\n+ * -----------------------------------------------------------------------\n+  */\n+\n+static void test_con_rmw(void)\n+{\n+    set_mode(OPMOD_CONFIG);\n+\n+    /* Clear TXQEN */\n+    writel(CiCON, readl(CiCON) & ~CON_TXQEN);\n+    g_assert_false((readl(CiCON) & CON_TXQEN) != 0u);\n+\n+    /* Set TXQEN */\n+    writel(CiCON, readl(CiCON) | CON_TXQEN);\n+    g_assert_true((readl(CiCON) & CON_TXQEN) != 0u);\n+}\n+\n+/*\n+ * Main\n+ * -----------------------------------------------------------------------\n+  */\n+\n+int main(int argc, char **argv)\n+{\n+    g_test_init(&argc, &argv, NULL);\n+    qtest_start(\"-machine pic32mk\");\n+\n+    qtest_add_func(\"/pic32mk-canfd/reset_state\",      test_reset_state);\n+    qtest_add_func(\"/pic32mk-canfd/mode_transition\",  test_mode_transition);\n+    qtest_add_func(\"/pic32mk-canfd/txq_configure\",    test_txq_configure);\n+    qtest_add_func(\"/pic32mk-canfd/loopback_tx_rx\",   test_loopback_tx_rx);\n+    qtest_add_func(\"/pic32mk-canfd/rx_overflow\",      test_rx_overflow);\n+    qtest_add_func(\"/pic32mk-canfd/con_rmw\",          test_con_rmw);\n+\n+    int r = g_test_run();\n+    qtest_end();\n+    return r;\n+}\ndiff --git a/tests/qtest/pic32mk-test.c b/tests/qtest/pic32mk-test.c\nnew file mode 100644\nindex 0000000000..6f7a1088ba\n--- /dev/null\n+++ b/tests/qtest/pic32mk-test.c\n@@ -0,0 +1,457 @@\n+/*\n+ * QTest for PIC32MK peripheral device models\n+ *\n+ * Tests MMIO register access, SET/CLR/INV operations, IRQ assertion,\n+ * and timer tick generation for the PIC32MK board emulation.\n+ *\n+ * Peripherals tested:\n+ *   - UART  (pic32mk_uart.c) — register reset values, SET/CLR, TX, IRQ\n+ *   - Timer (pic32mk_timer.c) — ON/OFF, prescaler, period match, IRQ\n+ *   - EVIC  (pic32mk_evic.c) — IFS/IEC/IPC, interrupt routing\n+ *\n+ * Copyright (c) 2026 QEMU contributors\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+#include \"libqtest-single.h\"\n+\n+/*\n+ * PIC32MK physical addresses (QTest uses physical, not KSEG1 virtual)\n+ * SFR base = 0x1F800000\n+ * -----------------------------------------------------------------------\n+  */\n+\n+#define SFR_BASE            0x1F800000u\n+\n+/* UART1 base = SFR + 0x028000 */\n+#define UART1_BASE          (SFR_BASE + 0x028000u)\n+#define UART1_MODE          (UART1_BASE + 0x00u)\n+#define UART1_MODECLR       (UART1_BASE + 0x04u)\n+#define UART1_MODESET       (UART1_BASE + 0x08u)\n+#define UART1_MODEINV       (UART1_BASE + 0x0Cu)\n+#define UART1_STA           (UART1_BASE + 0x10u)\n+#define UART1_STACLR        (UART1_BASE + 0x14u)\n+#define UART1_STASET        (UART1_BASE + 0x18u)\n+#define UART1_STAINV        (UART1_BASE + 0x1Cu)\n+#define UART1_TXREG         (UART1_BASE + 0x20u)\n+#define UART1_RXREG         (UART1_BASE + 0x30u)\n+#define UART1_BRG            (UART1_BASE + 0x40u)\n+\n+/* UART bits */\n+#define UMODE_ON            (1u << 15)\n+#define USTA_URXDA           (1u << 0)\n+#define USTA_OERR            (1u << 1)\n+#define USTA_TRMT            (1u << 8)\n+#define USTA_UTXBF           (1u << 9)\n+#define USTA_UTXEN           (1u << 10)\n+#define USTA_URXEN           (1u << 12)\n+\n+/* Timer1 base = SFR + 0x020000 */\n+#define T1_BASE             (SFR_BASE + 0x020000u)\n+#define T1CON               (T1_BASE + 0x00u)\n+#define T1CONCLR            (T1_BASE + 0x04u)\n+#define T1CONSET            (T1_BASE + 0x08u)\n+#define TMR1                (T1_BASE + 0x10u)\n+#define PR1                 (T1_BASE + 0x20u)\n+\n+/* Timer bits */\n+#define TCON_ON             (1u << 15)\n+\n+/* EVIC base = SFR + 0x010000 */\n+#define EVIC_BASE           (SFR_BASE + 0x010000u)\n+#define EVIC_INTCON         (EVIC_BASE + 0x0000u)\n+#define EVIC_INTSTAT        (EVIC_BASE + 0x0020u)\n+#define EVIC_IFS0           (EVIC_BASE + 0x0040u)\n+#define EVIC_IFS0CLR        (EVIC_BASE + 0x0044u)\n+#define EVIC_IFS0SET        (EVIC_BASE + 0x0048u)\n+#define EVIC_IFS1           (EVIC_BASE + 0x0050u)\n+#define EVIC_IFS1CLR        (EVIC_BASE + 0x0054u)\n+#define EVIC_IFS1SET        (EVIC_BASE + 0x0058u)\n+#define EVIC_IEC0           (EVIC_BASE + 0x00C0u)\n+#define EVIC_IEC0CLR        (EVIC_BASE + 0x00C4u)\n+#define EVIC_IEC0SET        (EVIC_BASE + 0x00C8u)\n+#define EVIC_IEC1           (EVIC_BASE + 0x00D0u)\n+#define EVIC_IEC1CLR        (EVIC_BASE + 0x00D4u)\n+#define EVIC_IEC1SET        (EVIC_BASE + 0x00D8u)\n+#define EVIC_IPC0           (EVIC_BASE + 0x0140u)\n+#define EVIC_IPC1           (EVIC_BASE + 0x0150u)\n+\n+/* EVIC source bit positions */\n+#define IRQ_T1_BIT          (1u << 4)       /* Timer1 = source 4, IFS0 bit 4 */\n+#define IRQ_U1E_BIT         (1u << 6)       /* U1E = source 38 = IFS1 bit 6 */\n+#define IRQ_U1RX_BIT        (1u << 7)       /* U1RX = source 39 = IFS1 bit 7 */\n+#define IRQ_U1TX_BIT        (1u << 8)       /* U1TX = source 40 = IFS1 bit 8 */\n+\n+/*\n+ * ===================================================================\n+ * UART1 register tests\n+ * ===================================================================\n+  */\n+\n+static void test_uart_reset_values(void)\n+{\n+    /* After machine reset: MODE=0, STA=TRMT (shift register empty), BRG=0 */\n+    g_assert_cmphex(readl(UART1_MODE), ==, 0x00000000);\n+    g_assert_cmphex(readl(UART1_STA) & (USTA_TRMT | USTA_UTXBF), ==, USTA_TRMT);\n+    g_assert_cmphex(readl(UART1_BRG), ==, 0x00000000);\n+}\n+\n+static void test_uart_set_clr_inv(void)\n+{\n+    /* Write BRG via direct write */\n+    writel(UART1_BRG, 29);\n+    g_assert_cmphex(readl(UART1_BRG), ==, 29);\n+\n+    /* SET operation: set bit 0 of MODE (STSEL) */\n+    writel(UART1_MODESET, 0x1);\n+    g_assert_cmphex(readl(UART1_MODE) & 0x1, ==, 0x1);\n+\n+    /* CLR operation: clear bit 0 */\n+    writel(UART1_MODECLR, 0x1);\n+    g_assert_cmphex(readl(UART1_MODE) & 0x1, ==, 0x0);\n+\n+    /* INV operation: toggle bit 3 (BRGH) */\n+    uint32_t before = readl(UART1_MODE);\n+    writel(UART1_MODEINV, 0x8);\n+    g_assert_cmphex(readl(UART1_MODE), ==, before ^ 0x8);\n+\n+    /* INV again to toggle back */\n+    writel(UART1_MODEINV, 0x8);\n+    g_assert_cmphex(readl(UART1_MODE), ==, before);\n+\n+    /* Clean up */\n+    writel(UART1_MODE, 0);\n+    writel(UART1_BRG, 0);\n+}\n+\n+static void test_uart_enable(void)\n+{\n+    /* Enable UART: MODESET ON, STASET UTXEN | URXEN */\n+    writel(UART1_MODESET, UMODE_ON);\n+    g_assert_cmphex(readl(UART1_MODE) & UMODE_ON, ==, UMODE_ON);\n+\n+    writel(UART1_STASET, USTA_UTXEN | USTA_URXEN);\n+    g_assert_cmphex(readl(UART1_STA) & (USTA_UTXEN | USTA_URXEN),\n+                    ==, USTA_UTXEN | USTA_URXEN);\n+\n+    /* TRMT should still be 1 (TX buffer always empty in QEMU) */\n+    g_assert_cmphex(readl(UART1_STA) & USTA_TRMT, ==, USTA_TRMT);\n+\n+    /* UTXBF should be 0 (transmission is instantaneous) */\n+    g_assert_cmphex(readl(UART1_STA) & USTA_UTXBF, ==, 0);\n+\n+    /* Disable UART */\n+    writel(UART1_MODECLR, UMODE_ON);\n+    g_assert_cmphex(readl(UART1_MODE) & UMODE_ON, ==, 0);\n+}\n+\n+static void test_uart_tx_write(void)\n+{\n+    /* Enable UART TX */\n+    writel(UART1_MODESET, UMODE_ON);\n+    writel(UART1_STASET, USTA_UTXEN);\n+\n+    /* Write a character — should not crash, TRMT stays 1 */\n+    writel(UART1_TXREG, 'A');\n+    g_assert_cmphex(readl(UART1_STA) & USTA_TRMT, ==, USTA_TRMT);\n+\n+    /* Write multiple characters */\n+    writel(UART1_TXREG, 'B');\n+    writel(UART1_TXREG, 'C');\n+    g_assert_cmphex(readl(UART1_STA) & USTA_TRMT, ==, USTA_TRMT);\n+\n+    /* Clean up */\n+    writel(UART1_MODECLR, UMODE_ON);\n+}\n+\n+static void test_uart_rx_not_ready(void)\n+{\n+    /* No byte received yet: URXDA should be 0 */\n+    g_assert_cmphex(readl(UART1_STA) & USTA_URXDA, ==, 0);\n+\n+    /* Reading RXREG when empty returns 0 */\n+    g_assert_cmphex(readl(UART1_RXREG), ==, 0);\n+}\n+\n+/*\n+ * ===================================================================\n+ * EVIC register tests\n+ * ===================================================================\n+  */\n+\n+static void test_evic_reset_values(void)\n+{\n+    /* After reset: IFS0=0, IEC0=0, INTCON=0 */\n+    g_assert_cmphex(readl(EVIC_IFS0), ==, 0x00000000);\n+    g_assert_cmphex(readl(EVIC_IEC0), ==, 0x00000000);\n+    g_assert_cmphex(readl(EVIC_INTCON), ==, 0x00000000);\n+}\n+\n+static void test_evic_ifs_set_clr(void)\n+{\n+    /* SET a flag in IFS0 — e.g. Timer1 (bit 4) */\n+    writel(EVIC_IFS0SET, IRQ_T1_BIT);\n+    g_assert_cmphex(readl(EVIC_IFS0) & IRQ_T1_BIT, ==, IRQ_T1_BIT);\n+\n+    /* CLR the flag */\n+    writel(EVIC_IFS0CLR, IRQ_T1_BIT);\n+    g_assert_cmphex(readl(EVIC_IFS0) & IRQ_T1_BIT, ==, 0);\n+}\n+\n+static void test_evic_iec_set_clr(void)\n+{\n+    /* Enable Timer1 interrupt */\n+    writel(EVIC_IEC0SET, IRQ_T1_BIT);\n+    g_assert_cmphex(readl(EVIC_IEC0) & IRQ_T1_BIT, ==, IRQ_T1_BIT);\n+\n+    /* Disable it */\n+    writel(EVIC_IEC0CLR, IRQ_T1_BIT);\n+    g_assert_cmphex(readl(EVIC_IEC0) & IRQ_T1_BIT, ==, 0);\n+}\n+\n+static void test_evic_ipc_priority(void)\n+{\n+    /* IPC1 holds Timer1 priority (source 4 → IPC1, shift=0, bits[4:2]=IP) */\n+    /* Set T1 priority to 3: write (3 << 2) = 0x0C into IPC1 bits [4:2] */\n+    writel(EVIC_IPC1, 0x0C);\n+    g_assert_cmphex(readl(EVIC_IPC1) & 0x1F, ==, 0x0C);\n+\n+    /* Clear it back */\n+    writel(EVIC_IPC1, 0x00);\n+    g_assert_cmphex(readl(EVIC_IPC1) & 0x1F, ==, 0x00);\n+}\n+\n+static void test_evic_uart_ifs1(void)\n+{\n+    /* UART1 sources are in IFS1: U1E=bit6, U1RX=bit7, U1TX=bit8 */\n+    writel(EVIC_IFS1SET, IRQ_U1TX_BIT);\n+    g_assert_cmphex(readl(EVIC_IFS1) & IRQ_U1TX_BIT, ==, IRQ_U1TX_BIT);\n+\n+    writel(EVIC_IFS1CLR, IRQ_U1TX_BIT);\n+    g_assert_cmphex(readl(EVIC_IFS1) & IRQ_U1TX_BIT, ==, 0);\n+\n+    /* Set all three UART1 flags */\n+    writel(EVIC_IFS1SET, IRQ_U1E_BIT | IRQ_U1RX_BIT | IRQ_U1TX_BIT);\n+    g_assert_cmphex(readl(EVIC_IFS1) & (IRQ_U1E_BIT | IRQ_U1RX_BIT | IRQ_U1TX_BIT),\n+                    ==, IRQ_U1E_BIT | IRQ_U1RX_BIT | IRQ_U1TX_BIT);\n+\n+    /* Clear all */\n+    writel(EVIC_IFS1CLR, IRQ_U1E_BIT | IRQ_U1RX_BIT | IRQ_U1TX_BIT);\n+    g_assert_cmphex(readl(EVIC_IFS1) & (IRQ_U1E_BIT | IRQ_U1RX_BIT | IRQ_U1TX_BIT),\n+                    ==, 0);\n+}\n+\n+static void test_evic_intstat_readonly(void)\n+{\n+    /* Write to INTSTAT should be ignored (read-only) */\n+    uint32_t before = readl(EVIC_INTSTAT);\n+    writel(EVIC_INTSTAT, 0xDEADBEEF);\n+    g_assert_cmphex(readl(EVIC_INTSTAT), ==, before);\n+}\n+\n+/*\n+ * ===================================================================\n+ * Timer1 register tests\n+ * ===================================================================\n+  */\n+\n+static void test_timer_reset_values(void)\n+{\n+    /* After reset: T1CON=0 (OFF), TMR1=0, PR1=0xFFFF (16-bit max) */\n+    g_assert_cmphex(readl(T1CON), ==, 0x00000000);\n+    g_assert_cmphex(readl(TMR1), ==, 0x00000000);\n+    g_assert_cmphex(readl(PR1), ==, 0x0000FFFF);\n+}\n+\n+static void test_timer_on_off(void)\n+{\n+    /* Set period */\n+    writel(PR1, 14999);\n+\n+    /* Turn ON via SET register */\n+    writel(T1CONSET, TCON_ON);\n+    g_assert_cmphex(readl(T1CON) & TCON_ON, ==, TCON_ON);\n+\n+    /* Turn OFF via CLR register */\n+    writel(T1CONCLR, TCON_ON);\n+    g_assert_cmphex(readl(T1CON) & TCON_ON, ==, 0);\n+\n+    /* Clean up */\n+    writel(PR1, 0);\n+}\n+\n+static void test_timer_period_register(void)\n+{\n+    /* Write period value and read back */\n+    writel(PR1, 14999);\n+    g_assert_cmpuint(readl(PR1), ==, 14999);\n+\n+    /* Different value */\n+    writel(PR1, 999);\n+    g_assert_cmpuint(readl(PR1), ==, 999);\n+\n+    /* Clean up */\n+    writel(PR1, 0);\n+}\n+\n+static void test_timer_prescaler_config(void)\n+{\n+    /*\n+     * Timer1 (Type A) prescaler: bits[5:4] of T1CON\n+     * 00=1:1, 01=1:8, 10=1:64, 11=1:256\n+      */\n+    writel(T1CONSET, (1u << 4));  /* TCKPS=01 → 1:8 */\n+    g_assert_cmphex(readl(T1CON) & 0x30, ==, 0x10);\n+\n+    writel(T1CON, 0);  /* reset */\n+    writel(T1CONSET, (3u << 4));  /* TCKPS=11 → 1:256 */\n+    g_assert_cmphex(readl(T1CON) & 0x30, ==, 0x30);\n+\n+    writel(T1CON, 0);\n+}\n+\n+static void test_timer_tick_fires_irq(void)\n+{\n+    /*\n+     * Configure Timer1:\n+     *   Prescaler 1:1 (default), PR1=9 → fires after 10 ticks\n+     *   Timer clock = 120 MHz internal / 2 (ptimer counts in ns)\n+     *   ptimer period = (PR1+1) ticks at system clock\n+     *\n+     * Timer1 is wired to EVIC source 4 (IFS0 bit 4).\n+     * Enable IEC0.T1IE and set IPC1.T1IP=1 so the EVIC routes it.\n+     */\n+\n+    /* Ensure timer is off and clean */\n+    writel(T1CON, 0);\n+    writel(TMR1, 0);\n+\n+    /* Set period to 9 (fires after 10 timer ticks) */\n+    writel(PR1, 9);\n+\n+    /* Enable T1 interrupt in EVIC: IEC0 bit 4, priority 1 in IPC1 */\n+    writel(EVIC_IFS0CLR, IRQ_T1_BIT);\n+    writel(EVIC_IEC0SET, IRQ_T1_BIT);\n+    writel(EVIC_IPC1, (1u << 2));  /* T1IP = 1 */\n+\n+    /* IFS0 should be clear before starting */\n+    g_assert_cmphex(readl(EVIC_IFS0) & IRQ_T1_BIT, ==, 0);\n+\n+    /* Start Timer1: prescaler 1:8 (Type A: TCKPS=01), ON */\n+    writel(T1CON, (1u << 4) | TCON_ON);\n+\n+    /*\n+     * Timer clock = 120 MHz / 8 = 15 MHz → 66.67 ns per tick.\n+     * With ptimer driven by per-CPU clock, the exact virtual time\n+     * depends on ptimer frequency setting.  Step enough virtual time\n+     * for the timer to expire: (PR1+1) * tick_period + margin.\n+     *\n+     * ptimer is typically configured at 15 MHz (120/8), so period\n+     * of 10 ticks = 10 * 66.67ns ≈ 667 ns.  Step 2000 ns to be safe.\n+     */\n+    clock_step(2000);\n+\n+    /* Timer should have fired: IFS0.T1IF should be set */\n+    g_assert_cmphex(readl(EVIC_IFS0) & IRQ_T1_BIT, ==, IRQ_T1_BIT);\n+\n+    /* Clear it */\n+    writel(EVIC_IFS0CLR, IRQ_T1_BIT);\n+    g_assert_cmphex(readl(EVIC_IFS0) & IRQ_T1_BIT, ==, 0);\n+\n+    /* Stop timer, clean up */\n+    writel(T1CON, 0);\n+    writel(EVIC_IEC0CLR, IRQ_T1_BIT);\n+    writel(EVIC_IPC1, 0);\n+}\n+\n+/*\n+ * ===================================================================\n+ * UART1 TX IRQ integration test\n+ *\n+ * When UART1 is enabled (ON + UTXEN), the TX IRQ line is asserted.\n+ * The EVIC should latch IFS1.U1TXIF.\n+ * ===================================================================\n+  */\n+\n+static void test_uart_tx_irq_asserts_ifs(void)\n+{\n+    /* Clean initial state */\n+    writel(EVIC_IFS1CLR, IRQ_U1TX_BIT);\n+    writel(UART1_MODE, 0);\n+\n+    /* IFS1.U1TXIF should be clear before UART is enabled */\n+    g_assert_cmphex(readl(EVIC_IFS1) & IRQ_U1TX_BIT, ==, 0);\n+\n+    /* Enable UART1: ON + UTXEN */\n+    writel(UART1_MODESET, UMODE_ON);\n+    writel(UART1_STASET, USTA_UTXEN);\n+\n+    /*\n+     * The UART TX IRQ is level-based — enabling ON+UTXEN asserts it.\n+     * The EVIC should have set IFS1.U1TXIF.\n+      */\n+    g_assert_cmphex(readl(EVIC_IFS1) & IRQ_U1TX_BIT, ==, IRQ_U1TX_BIT);\n+\n+    /* Clear the flag in IFS1 */\n+    writel(EVIC_IFS1CLR, IRQ_U1TX_BIT);\n+\n+    /* Since IRQ line is still high (level-based), EVIC re-asserts IFS */\n+    g_assert_cmphex(readl(EVIC_IFS1) & IRQ_U1TX_BIT, ==, IRQ_U1TX_BIT);\n+\n+    /* Disable UART TX — IRQ should deassert */\n+    writel(UART1_STACLR, USTA_UTXEN);\n+\n+    /* Now clear IFS1 — it should stay clear since the line is low */\n+    writel(EVIC_IFS1CLR, IRQ_U1TX_BIT);\n+    g_assert_cmphex(readl(EVIC_IFS1) & IRQ_U1TX_BIT, ==, 0);\n+\n+    /* Clean up */\n+    writel(UART1_MODECLR, UMODE_ON);\n+}\n+\n+/*\n+ * ===================================================================\n+ * main\n+ * ===================================================================\n+  */\n+\n+int main(int argc, char **argv)\n+{\n+    int r;\n+\n+    g_test_init(&argc, &argv, NULL);\n+\n+    qtest_start(\"-machine pic32mk\");\n+\n+    /* UART1 tests */\n+    qtest_add_func(\"/pic32mk/uart/reset-values\", test_uart_reset_values);\n+    qtest_add_func(\"/pic32mk/uart/set-clr-inv\", test_uart_set_clr_inv);\n+    qtest_add_func(\"/pic32mk/uart/enable\", test_uart_enable);\n+    qtest_add_func(\"/pic32mk/uart/tx-write\", test_uart_tx_write);\n+    qtest_add_func(\"/pic32mk/uart/rx-not-ready\", test_uart_rx_not_ready);\n+\n+    /* EVIC tests */\n+    qtest_add_func(\"/pic32mk/evic/reset-values\", test_evic_reset_values);\n+    qtest_add_func(\"/pic32mk/evic/ifs-set-clr\", test_evic_ifs_set_clr);\n+    qtest_add_func(\"/pic32mk/evic/iec-set-clr\", test_evic_iec_set_clr);\n+    qtest_add_func(\"/pic32mk/evic/ipc-priority\", test_evic_ipc_priority);\n+    qtest_add_func(\"/pic32mk/evic/uart-ifs1\", test_evic_uart_ifs1);\n+    qtest_add_func(\"/pic32mk/evic/intstat-readonly\", test_evic_intstat_readonly);\n+\n+    /* Timer1 tests */\n+    qtest_add_func(\"/pic32mk/timer/reset-values\", test_timer_reset_values);\n+    qtest_add_func(\"/pic32mk/timer/on-off\", test_timer_on_off);\n+    qtest_add_func(\"/pic32mk/timer/period-register\", test_timer_period_register);\n+    qtest_add_func(\"/pic32mk/timer/prescaler-config\", test_timer_prescaler_config);\n+    qtest_add_func(\"/pic32mk/timer/tick-fires-irq\", test_timer_tick_fires_irq);\n+\n+    /* Integration: UART TX IRQ → EVIC */\n+    qtest_add_func(\"/pic32mk/integration/uart-tx-irq\", test_uart_tx_irq_asserts_ifs);\n+\n+    r = g_test_run();\n+\n+    qtest_end();\n+\n+    return r;\n+}\n","prefixes":["RFC","1/3"]}