Skip to content

Devices

sy2002 edited this page Jun 14, 2026 · 1 revision

Devices

This page explains the device system of the MiSTer2MEGA65 (M2M) framework.

A device is a piece of FPGA logic that the QNICE Shell can read or write through a small address window. In practice, devices are how the Shell loads ROMs, fills RAMs, mounts disk images, writes large staging buffers, and talks to small control/status register blocks inside a core.

The C64 core is used as the running example because it is the reference M2M implementation. The page is still written at framework level: the goal is to show what a core porter can build with the device system, not to explain every C64-specific subsystem.

Scope of this page. We cover the QNICE device window (M2M$RAMROM_DEV, M2M$RAMROM_4KWIN, M2M$RAMROM_DATA), the qnice_dev_* ports of MEGA65_Core, device IDs, simple RAM/ROM devices, manual and automatic file loading, CSR-based loaders, virtual-drive device discovery, and the common traps. We do not cover the whole QNICE CPU, FAT32 implementation, OSM renderer, video pipeline, or virtual-drive write-back policy.


1. Why devices exist

On MiSTer, the ARM/Linux side loads files, mounts disks, stores settings, draws the menu, and talks to the FPGA core through hps_io.

On the MEGA65 there is no ARM/Linux side. M2M replaces those host services with a small on-FPGA helper computer: QNICE. QNICE runs the M2M Shell firmware. The Shell reads the SD card, shows the file browser, draws the OSM, loads files, and keeps settings.

The Shell still needs a clean way to put data into your FPGA core. That is what devices are for.

Typical jobs for devices:

You want the Shell to ... Device pattern you use
load a boot ROM before the core starts a RAM-like ROM device plus C_CRTROMS_AUTO
let the user load a cartridge or ROM from the OSM a manual load target plus OPTM_G_LOAD_ROM
inject a .PRG or similar file into core RAM a streaming loader device, often with a CSR window
mount a disk image vdrives.vhd plus one image-buffer device per drive
let QNICE inspect or patch RAM expose a dual-port RAM through a core-specific device ID
stage large data in HyperRAM framework HyperRAM access or a core-specific proxy/wrapper

For simple settings such as "PAL or NTSC" or "enable scanlines", do not invent a device. Use the OSM control vector (qnice_osm_control_i / main_osm_control_i) or a small dedicated control signal. Devices are for addressable data, RAM/ROM buffers, and register files.


2. The mental model: one aperture into many devices

Think of the device system as a movable aperture.

QNICE does not see all device memory at once. It sees one 4K QNICE-address window of one selected device at a time:

  1. Select a device ID.
  2. Select a 4K QNICE-address window inside that device.
  3. Read or write addresses inside the fixed data aperture 0x7000..0x7FFF.

The important QNICE addresses are:

QNICE address Name Meaning
0xFFF4 M2M$RAMROM_DEV 16-bit device ID selector
0xFFF5 M2M$RAMROM_4KWIN 16-bit 4K QNICE-address window selector
0x7000 M2M$RAMROM_DATA base address of the selected data window
0x7000..0x7FFF data window 4096 QNICE addresses inside the selected window

In QNICE assembly, a write to the first word of core device 0x0100 looks like this:

MOVE    M2M$RAMROM_DEV, R0      ; 0xFFF4: select a device ID
MOVE    0x0100, @R0             ; example: first core-specific device

MOVE    M2M$RAMROM_4KWIN, R0    ; 0xFFF5: select a 4K QNICE-address window
MOVE    0, @R0                  ; window 0

MOVE    M2M$RAMROM_DATA, R0     ; 0x7000: start of selected data window
MOVE    0x0041, @R0             ; write one QNICE word to offset 0

The VHDL side receives a flat 28-bit address:

qnice_dev_addr_i = selected_window * 4096 + offset_inside_0x7000_0x7FFF

For example, if QNICE selects window 3 and reads 0x7012:

offset = 0x7012 - 0x7000 = 0x012
address seen by the device = 3 * 4096 + 0x012 = 0x3012

This is why the bus address is 28 bits wide: 16 bits of window selector plus 12 bits of offset.

2.1 Address names that are easy to confuse

There are several address spaces in play. Keep them separate:

Name Example Meaning
QNICE address 0x7000, 0xFFF4 address as seen by the QNICE CPU
selected device ID 0x0100 which device the aperture currently points at
device-local address 0x3012 address your VHDL device sees on qnice_dev_addr_i
file byte offset byte 1234 of bios.bin position inside the file being streamed by the Shell
core CPU address C64 $E000, NES $8000, etc. address as seen by the emulated machine

0x7000 is a QNICE aperture address. It is not automatically a C64 address, a target-core address, or a file offset.

2.2 "4K QNICE-address window" means 4096 QNICE addresses

The QNICE data bus is 16 bits wide. Some devices use the full 16-bit word. Other devices use only the low byte and return data as x"00" & byte.

So a 4K QNICE-address window is not automatically 4 KiB of your memory. It is 4096 device addresses. The device decides what one address means.

Device style What one QNICE address usually means
byte-oriented RAM, like C64 RAM one byte
16-bit RAM or HyperRAM word access one 16-bit word
byte-lane split memory address bit 0 selects upper or lower byte lane
CSR/register file one register, often with only a few valid offsets

The bus gives you an address, a 16-bit write word, and a place to return a 16-bit read word. The packing is part of your device contract.

2.3 One access, step by step

A single Shell write to M2M$RAMROM_DATA + offset becomes:

Shell writes to 0x7000 + offset
        |
        v
QNICE bus logic notices "this is the device data aperture"
        |
        v
qnice.vhd combines selected 4K QNICE-address window and low 12 address bits
        |
        v
qnice_wrapper.vhd handles framework IDs or forwards core IDs
        |
        v
MEGA65_Core sees qnice_dev_id_i, qnice_dev_addr_i,
qnice_dev_data_i, qnice_dev_ce_i, and qnice_dev_we_i
        |
        v
your decoder enables the selected RAM, register, loader, or bridge

For reads, the path is the same in reverse, with your device returning qnice_dev_data_o. If the device is slow, it asserts qnice_dev_wait_o until the data or write completion is ready.


3. Device IDs and bus routing

For a first simple device, you normally touch only:

  • CORE/vhdl/globals.vhd, to define the device ID,
  • CORE/vhdl/mega65.vhd, to instantiate and decode the device,
  • CORE/vhdl/config.vhd, only if the OSM should expose a load or mount action,
  • CORE/m2m-rom/m2m-rom.asm, only if Shell callbacks need core-specific logic.

The device ID space is split by the framework:

Range Owner Use
0x0000..0x00FF framework OSM video RAM, config data, HyperRAM, sysinfo, etc.
0x0100..0xFFFF your core RAMs, ROMs, loaders, vdrives, core-specific devices

Rule: your first core-specific device can be x"0100". Do not use IDs below x"0100" for core devices.

The path from Shell code to core VHDL is:

QNICE Shell firmware
   writes M2M$RAMROM_DEV / M2M$RAMROM_4KWIN
   reads or writes M2M$RAMROM_DATA + offset
        |
        v
M2M/vhdl/QNICE/qnice.vhd
   turns that into ramrom_dev_o, ramrom_addr_o,
   ramrom_data_o, ramrom_ce_o, ramrom_we_o
        |
        v
M2M/vhdl/qnice_wrapper.vhd
   IDs below 0x0100: handled by the framework
   IDs 0x0100 and above: forwarded to the core
        |
        v
M2M/vhdl/top_mega65-rX.vhd
   wires qnice_ramrom_* to MEGA65_Core as qnice_dev_*
        |
        v
CORE/vhdl/mega65.vhd
   your core-specific device decoder

The MEGA65_Core device ports are:

Port Direction from core view Meaning
qnice_dev_id_i input selected 16-bit device ID
qnice_dev_addr_i input 28-bit address inside that device
qnice_dev_data_i input 16-bit data written by QNICE
qnice_dev_data_o output 16-bit data read by QNICE
qnice_dev_ce_i input QNICE is accessing the 0x7000..0x7FFF aperture
qnice_dev_we_i input this access is a write
qnice_dev_wait_o output stall QNICE until the device is ready

The data_i and data_o names are from the core device point of view: qnice_dev_data_i goes into your device; qnice_dev_data_o comes out of your device.


4. Your first tiny device

The simplest useful device is a small dual-port RAM:

  • QNICE writes one port.
  • The core reads the other port.
  • the QNICE monitor can inspect it.
  • the Shell can use it as an automatic startup load target.

This is the same idea used for C64 RAM and custom ROM buffers, just smaller. It is not yet a complete manual OSM load target. Standard OPTM_G_LOAD_ROM manual targets also need the load-status path described in the CSR section.

4.1 Pick a device ID

Define a core-specific ID in CORE/vhdl/globals.vhd:

constant C_DEV_MY_ROM : std_logic_vector(15 downto 0) := x"0100";

If x"0100" is already used, pick the next free ID. Keep all core-specific device IDs in one place.

4.2 Instantiate storage

For a byte-wide ROM/RAM visible to both QNICE and the core, use an existing dual-clock RAM building block such as M2M/vhdl/2port2clk_ram.vhd.

my_rom : entity work.dualport_2clk_ram
   generic map (
      ADDR_WIDTH => 14,
      DATA_WIDTH => 8,
      FALLING_A  => false, -- core port
      FALLING_B  => true   -- QNICE port, reference pattern
   )
   port map (
      -- Core side
      clock_a   => main_clk,
      address_a => core_rom_addr,
      data_a    => (others => '0'),
      wren_a    => '0',
      q_a       => core_rom_data,

      -- QNICE side
      clock_b   => qnice_clk_i,
      address_b => qnice_dev_addr_i(13 downto 0),
      data_b    => qnice_dev_data_i(7 downto 0),
      wren_b    => qnice_my_rom_we,
      q_b       => qnice_my_rom_data
   );

The reference designs normally use the falling edge on QNICE-visible BRAM ports. That gives the QNICE bus the expected read-data timing.

With ADDR_WIDTH => 14, this example has 2^14 device addresses. Because the RAM is byte-wide and the decoder uses only qnice_dev_data_i(7 downto 0), that is a 16 KiB buffer.

4.3 Add one decoder branch

Your MEGA65_Core should have one central decoder, usually in CORE/vhdl/mega65.vhd.

core_specific_devices : process(all)
begin
   -- Defaults first. This avoids latches and gives unmapped reads
   -- a recognizable value.
   qnice_dev_data_o <= x"EEEE";
   qnice_dev_wait_o <= '0';

   qnice_my_rom_we  <= '0';

   case qnice_dev_id_i is
      when C_DEV_MY_ROM =>
         qnice_my_rom_we  <= qnice_dev_we_i;
         qnice_dev_data_o <= x"00" & qnice_my_rom_data;

      when others =>
         null;
   end case;
end process core_specific_devices;

The rules are:

  1. Set defaults for every output and every write-enable before the case.
  2. Default unmapped reads to x"EEEE".
  3. Decode by qnice_dev_id_i.
  4. Use qnice_dev_addr_i as the address inside the selected device.
  5. Use qnice_dev_we_i for writes. It is already asserted only for data aperture writes.
  6. Qualify non-write side effects and register strobes with qnice_dev_ce_i.
  7. Drive qnice_dev_wait_o only when a selected device really needs to stall QNICE. This simple BRAM device does not need wait.

For a plain BRAM, it is normal that the address is driven continuously while the device ID is selected. For anything with side effects, qnice_dev_ce_i is the guard that says: "QNICE is actually accessing the data aperture now."

4.4 Inspect it from QNICE

Once synthesized with monitor or Shell access, the conceptual debug flow is:

  1. write your device ID to 0xFFF4,
  2. write the wanted 4K QNICE-address window to 0xFFF5,
  3. inspect or modify 0x7000..0x7FFF.

If an auto-loaded ROM should have landed in this device, select the device, select window 0, and inspect the first words at 0x7000. If the ROM contents are there, the Shell/device path worked and the bug is probably on the core side of the RAM. If the window is all zeroes or 0xEEEE, debug the declaration, filename, or device decode.

4.5 First practical use: auto-load a BIOS ROM

The easiest Shell integration for this tiny device is automatic startup loading. For example, a fictional core might load /mycore/bios.bin into C_DEV_MY_ROM before reset is released:

constant MYCORE_BIOS            : string := "/mycore/bios.bin" & ENDSTR;

constant C_CRTROMS_AUTO_NUM     : natural := 1;
constant C_CRTROMS_AUTO_NAMES   : string := MYCORE_BIOS;
constant C_CRTROMS_AUTO         : crtrom_buf_array := (
   C_CRTROMTYPE_DEVICE, C_DEV_MY_ROM,
   C_CRTROMTYPE_MANDATORY, 0,

   x"EEEE");

This tells the Shell: open /mycore/bios.bin, select C_DEV_MY_ROM, and stream the file into the device before the core starts. The target still needs capacity checking if the ROM must be exactly a certain size. Details and the C64 JiffyDOS example are in the automatic loading section below.


5. Device shapes

Not every device is just a RAM. Most M2M ports use a few recurring shapes.

Shape Description C64 reference example
byte RAM/ROM QNICE writes bytes into dual-port BRAM C_DEV_C64_RAM, custom Kernal ROMs
register/CSR file a small set of control and status registers qnice_csr.vhd based loaders
streaming loader QNICE streams a file and the device has side effects .PRG loader
protocol bridge QNICE fills a buffer, another interface consumes it virtual drives
HyperRAM proxy/wrapper QNICE stages large data outside BRAM C64 .CRT staging wrapper

Choosing the right shape matters. A byte ROM device is simple and fast. A parser or loader needs status registers. A large file may need HyperRAM. A time-critical core read path usually needs BRAM or a cache, not direct HyperRAM.


6. Core-specific devices in the C64 reference core

The C64 core defines its device IDs in CORE/vhdl/globals.vhd:

constant C_DEV_C64_RAM          : std_logic_vector(15 downto 0) := x"0100";
constant C_DEV_C64_VDRIVES      : std_logic_vector(15 downto 0) := x"0101";
constant C_DEV_C64_MOUNT        : std_logic_vector(15 downto 0) := x"0102";
constant C_DEV_C64_CRT          : std_logic_vector(15 downto 0) := x"0103";
constant C_DEV_C64_PRG          : std_logic_vector(15 downto 0) := x"0104";
constant C_DEV_C64_KERNAL_C64   : std_logic_vector(15 downto 0) := x"0105";
constant C_DEV_C64_KERNAL_C1541 : std_logic_vector(15 downto 0) := x"0106";

What each one teaches:

Device What it is Why it is a useful example
C_DEV_C64_RAM C64 main RAM simplest byte-addressed dual-port RAM device
C_DEV_C64_VDRIVES vdrives.vhd control/register device register device that bridges to MiSTer-style sd_* signals
C_DEV_C64_MOUNT disk-image buffer RAM bulk file data streamed into RAM by the Shell
C_DEV_C64_CRT simulated .CRT loader stream into HyperRAM plus CSR parse handshake
C_DEV_C64_PRG .PRG loader file stream with side effects: RAM injection, reset, autostart
C_DEV_C64_KERNAL_C64 custom C64 Kernal RAM optional auto-loaded ROM
C_DEV_C64_KERNAL_C1541 custom 1541 ROM RAM optional auto-loaded drive ROM

All of them are decoded in one place: CORE/vhdl/mega65.vhd, core_specific_devices.


7. How the Shell discovers your devices

The Shell does not scan the whole device ID space. It learns about devices from your configuration constants and, when needed, from core-specific callbacks.

7.1 Static discovery through sysinfo

The generic Shell reads these values through the framework sysinfo device:

Constant Meaning
C_VDNUM number of virtual drives
C_VD_DEVICE virtual-drive control device ID
C_VD_BUFFER buffer device IDs for disk-image contents
C_CRTROMS_MAN_NUM number of manual load slots
C_CRTROMS_MAN manual load target table
C_CRTROMS_AUTO_NUM number of startup auto-load slots
C_CRTROMS_AUTO startup auto-load target table
C_CRTROMS_AUTO_NAMES concatenated startup filenames

This is enough for standard virtual drives and standard ROM/file loading.

For the record tables, the counts are authoritative:

  • C_CRTROMS_MAN_NUM counts two-word manual records.
  • C_CRTROMS_AUTO_NUM counts four-word auto-load records.
  • The arrays must contain at least that many records plus the final x"EEEE" marker.
  • C_CRTROMS_AUTO_NAMES is different: it is a concatenated string whose entries end with ENDSTR, not with x"EEEE".

7.2 Core-specific callbacks

When generic behavior is not enough, add callbacks in CORE/m2m-rom/m2m-rom.asm.

Common callback points include:

Callback Typical use
SUBMENU_SUMMARY show a selected value in an OSM headline
FILTER_FILES show only .D64, .PRG, .CRT, etc. in the file browser
PREP_LOAD_IMAGE validate a selected file or set an image type before loading
PREP_START check optional ROMs and adjust settings before reset is released
OSM_SEL_PRE / OSM_SEL_POST react before or after a menu selection changes
CUSTOM_MSG provide custom user-facing Shell messages

The C64 uses callbacks to filter file extensions, validate disk-image sizes, and switch into simulated-cartridge mode before loading a .CRT.


8. Manual ROM/file loading

Manual loading is for files the user chooses from the OSM. The menu line is tagged with OPTM_G_LOAD_ROM in CORE/vhdl/config.vhd.

The pieces are:

  1. In config.vhd, create a menu line with OPTM_G_LOAD_ROM.
  2. In globals.vhd, add one target record to C_CRTROMS_MAN.
  3. In mega65.vhd or a helper wrapper, implement the target device.
  4. In CORE/m2m-rom/m2m-rom.asm, add callbacks if the generic flow needs help.
  5. Run CORE/m2m-rom/make_rom.sh if the number of manual load slots or menu positions changed.

The C64 declares two manual targets:

constant C_CRTROMS_MAN_NUM : natural := 2;
constant C_CRTROMS_MAN     : crtrom_buf_array := (
   C_CRTROMTYPE_DEVICE, C_DEV_C64_PRG,
   C_CRTROMTYPE_DEVICE, C_DEV_C64_CRT,
   x"EEEE");

C_CRTROMS_MAN_NUM is the number of manual load records. It must match the number of OPTM_G_LOAD_ROM menu lines, in the same order. Too few records means a menu line can point past the configured table. Too many records creates unreachable targets. The x"EEEE" marker remains in the array, but the count is what tells the Shell how many slots exist.

Use a deliberate low-byte group ID for every OPTM_G_LOAD_ROM line. The Shell callbacks receive that group ID, so it is how your callback code can distinguish "load PRG" from "load cartridge" from "load some other ROM".

The positional link is important:

OSM line order C_CRTROMS_MAN record C64 target
first OPTM_G_LOAD_ROM line record 0 C_DEV_C64_PRG
second OPTM_G_LOAD_ROM line record 1 C_DEV_C64_CRT

Each manual record has two words:

Field Meaning
storage type usually C_CRTROMTYPE_DEVICE; C_CRTROMTYPE_HYPERRAM exists but is not a generic drop-in target
target device ID for DEVICE; 4K QNICE-address HyperRAM window for HYPERRAM

For a C_CRTROMTYPE_DEVICE target, the Shell selects the device and streams the file through the data aperture. For standard manual load targets created with OPTM_G_LOAD_ROM, the target must also provide the expected load-status path: normally the CSR window described below. If the device has no parser, it can still implement a minimal CSR response that reports "ready" after the stream.

8.1 Manual HyperRAM targets need care

C_CRTROMTYPE_HYPERRAM exists, but do not treat the framework HyperRAM device as a ready-made replacement for a manual OSM load target.

The generic Shell writes file data one QNICE address at a time. The framework HyperRAM device is a 16-bit word-oriented access path. A byte-oriented file loader often needs a wrapper that packs byte writes into the intended HyperRAM byte lanes and exposes the expected CSR/status behavior.

The C64 .CRT path is exactly such a wrapper: sw_cartridge_csr.vhd accepts the Shell stream, maps it into HyperRAM correctly, and provides parse/status registers for the Shell.


9. The CSR window

CSR means control/status register. M2M uses a conventional CSR window for manual file-loading devices that need a status handshake after a file stream.

Historically, the constants are named CRTROM_CSR_*, because the mechanism was introduced for cartridge/ROM loading. They are useful for any similar loader, not only for .CRT files.

The CSR window is selected by using 4K QNICE-address window 0xFFFF on the same device:

MOVE    M2M$RAMROM_DEV, R0
MOVE    C_DEV_SOME_LOADER, @R0

MOVE    M2M$RAMROM_4KWIN, R0
MOVE    CRTROM_CSR_4KWIN, @R0      ; 0xFFFF

MOVE    CRTROM_CSR_FS_LO, R0       ; 0x7001
MOVE    filesize_low, @R0

The standard CSR offsets are defined in M2M/rom/sysdef.asm:

Offset in CSR window Name Direction Meaning
0x7000 CRTROM_CSR_STATUS QNICE -> device load request/status
0x7001 CRTROM_CSR_FS_LO QNICE -> device file size, low 16 bits
0x7002 CRTROM_CSR_FS_HI QNICE -> device upper 7 bits of the 23-bit file size
0x7010 CRTROM_CSR_PARSEST device -> QNICE parser status
0x7011 CRTROM_CSR_PARSEE1 device -> QNICE parser error code
0x7012 CRTROM_CSR_ADDR_LO device -> QNICE low 16 bits of an approximate error address
0x7013 CRTROM_CSR_ADDR_HI device -> QNICE upper 7 bits of a 23-bit error address
0x7100..0x71FF CRTROM_CSR_ERR_STRT device -> QNICE error string area

The reusable VHDL helper is M2M/vhdl/qnice_csr.vhd. It exposes 23-bit file length and address fields, not a full 32-bit address.

The standard manual-load lifecycle is:

  1. The Shell selects the payload window and streams the file into the target.
  2. The Shell selects CSR window 0xFFFF.
  3. The Shell writes the file length through CRTROM_CSR_FS_LO and CRTROM_CSR_FS_HI.
  4. The Shell writes CRTROM_CSR_STATUS to tell the device the stream is complete.
  5. The device parses, validates, or finalizes the data.
  6. The Shell polls CRTROM_CSR_PARSEST until the device reports success or an error.

A manual target with no real parser still needs to implement this status path. The smallest useful behavior is: accept the file size, report parse-ready/no error after the stream completes, and optionally reject wrong sizes before claiming success.

The C64 uses this pattern twice:

  • prg_loader.vhd: QNICE streams a .PRG, the loader reads the load address from the first two bytes, writes the rest into C64 RAM, then triggers the start behavior.
  • sw_cartridge_csr.vhd: QNICE streams a .CRT into HyperRAM, then the parser validates the cartridge header and fills the cartridge cache metadata.

Use a CSR window when your manual load target needs the Shell to know whether the streamed file was accepted, rejected, or parsed with a useful error message.


10. Automatic ROM loading

Automatic loading is for files that should be loaded at startup before the core is released from reset.

Use it for:

  • required boot ROMs that cannot be distributed with the bitstream,
  • optional enhancement ROMs,
  • user-supplied ROM replacements.

The declaration lives in CORE/vhdl/globals.vhd. The C64 uses optional auto-load ROMs for JiffyDOS:

constant JIFFY_DOS_C64         : string := "/c64/jd-c64.bin" & ENDSTR;
constant JIFFY_DOS_C1541       : string := "/c64/jd-c1541.bin" & ENDSTR;

constant C_CRTROMS_AUTO_NUM    : natural := 2;
constant C_CRTROMS_AUTO_NAMES  : string := JIFFY_DOS_C64 & JIFFY_DOS_C1541;
constant C_CRTROMS_AUTO        : crtrom_buf_array := (
   C_CRTROMTYPE_DEVICE, C_DEV_C64_KERNAL_C64,
   C_CRTROMTYPE_OPTIONAL, JIFFY_DOS_C64_START,

   C_CRTROMTYPE_DEVICE, C_DEV_C64_KERNAL_C1541,
   C_CRTROMTYPE_OPTIONAL, JIFFY_DOS_C1541_START,

   x"EEEE");

C_CRTROMS_AUTO_NUM is the number of auto-load records. Each record has four words. The current standard and reference-safe path is C_CRTROMTYPE_DEVICE: the Shell streams the file into the listed device ID.

Field Meaning
storage type C_CRTROMTYPE_DEVICE for the supported standard path
target target device ID
requirement C_CRTROMTYPE_MANDATORY or C_CRTROMTYPE_OPTIONAL
filename start offset into C_CRTROMS_AUTO_NAMES

Mandatory and optional only describe what happens if the file is missing:

  • mandatory: if the file is missing, the Shell shows a fatal error and the core does not start.
  • optional: if the file is missing, the Shell continues.

They do not validate the file length or contents. The Shell streams until end of file. If your target has a fixed capacity, the target device or a callback must reject wrong sizes. Otherwise address slicing can wrap, truncate, or overwrite unintended data.

Although framework constants for HyperRAM-style load targets exist, current portable auto-load examples should use C_CRTROMTYPE_DEVICE. If you want to auto-load into HyperRAM, use a core-specific wrapper or confirm the Shell path you are using handles that storage type correctly.

C_CRTROMS_AUTO_NAMES is one concatenated string. Every filename must end with & ENDSTR, and every *_START offset must point to the first character of the intended filename.


11. Virtual drives are devices plus a protocol bridge

Virtual drives use the device system, but they are not just a RAM.

They have three parts:

  1. M2M/vhdl/vdrives.vhd: hardware bridge that speaks the MiSTer-style img_mounted, sd_lba, sd_rd, sd_wr, sd_ack, sd_buff_* protocol.
  2. A control/register device, named by C_VD_DEVICE.
  3. One disk-image buffer device per drive, listed in C_VD_BUFFER.

The C64 setup is:

constant C_VDNUM     : natural := 1;
constant C_VD_DEVICE : std_logic_vector(15 downto 0) := C_DEV_C64_VDRIVES;
constant C_VD_BUFFER : vd_buf_array := (
   C_DEV_C64_MOUNT,
   x"EEEE");

Runtime flow:

  1. The Shell reads C_VDNUM, C_VD_DEVICE, and C_VD_BUFFER through sysinfo.
  2. The user selects a disk image in the file browser.
  3. The Shell streams the whole image into the buffer device.
  4. The Shell writes size, type, read-only state, and a mount strobe to vdrives.vhd.
  5. The MiSTer drive model asks for blocks through sd_rd/sd_wr.
  6. The virtual-drive bridge serves those requests from the buffer and writes dirty data back to SD later.

This page covers only the device-system side. The full mount, cache, write-back, and anti-thrashing behavior belongs in Virtual Drives.


12. HyperRAM-backed devices

Some data is too large for BRAM. HyperRAM is useful for staging that data, but it is not a drop-in replacement for a timing-critical ROM.

The C64 .CRT path is the reference example:

  • QNICE writes the raw .CRT file through a core-specific device window.
  • sw_cartridge_csr.vhd packs those writes into the intended HyperRAM layout.
  • the cartridge parser reads the staged file from HyperRAM.
  • the cartridge cache fills fast BRAM banks from HyperRAM when the C64 needs a bank.

This works because the C64 CPU does not execute cartridge bytes directly from HyperRAM. It executes from BRAM cache, and the CPU can be paused briefly while a missing bank is fetched.

Use a HyperRAM-backed device when:

  • QNICE must stream a large file into a staging area,
  • BRAM would be too expensive,
  • the core can tolerate a parser, cache, FIFO, or stall protocol between HyperRAM and the timing-critical logic.

Do not wire a timing-critical core read path directly to HyperRAM and hope it behaves like MiSTer SDRAM. HyperRAM latency and contention are real.


13. Framework devices

Core ports mostly use framework devices; they do not implement them. The current framework device IDs decoded in M2M/vhdl/qnice_wrapper.vhd are:

ID Framework device Purpose
0x0000 VRAM data OSM text/video RAM characters
0x0001 VRAM attributes OSM text/video RAM attributes
0x0002 config read-only data generated from CORE/vhdl/config.vhd
0x0003 ascal polyphase write polyphase filter coefficients
0x0004 HyperRAM QNICE access to MEGA65 HyperRAM
0x0005 I2C framework I2C access
0x0006 RTC framework RTC access
0x00FF sysinfo read-only framework/core metadata

Treat the whole 0x0000..0x00FF range as reserved. The current RTL is the authority: qnice_wrapper.vhd decodes 0x0005 as I2C and 0x0006 as RTC.

At the time of writing, M2M/rom/sysdef.asm still contains the stale name M2M$SDRAM for 0x0005 and does not provide matching I2C/RTC constants. Do not use that symbol as proof that 0x0005 is safe for a core device or still means SDRAM.

13.1 Config device (0x0002)

The config device is a read-only view of CORE/vhdl/config.vhd.

The Shell reads it to learn:

  • menu text (OPTM_ITEMS),
  • menu group meanings (OPTM_GROUPS),
  • welcome and help screens,
  • default reset/pause behavior,
  • config-file path,
  • OSM dimensions and other static settings.

You normally edit only the configuration region of config.vhd. The address decoder at the bottom is framework ABI.

13.2 Sysinfo device (0x00FF)

The sysinfo device exposes framework and core metadata to the generic Shell. That includes virtual-drive declarations, ROM-loading declarations, video geometry, board facts, and HyperRAM statistics.

You do not write a sysinfo device in your core. The framework serves it from the constants and ports already connected to qnice_wrapper.vhd.

13.3 HyperRAM device (0x0004)

The framework HyperRAM device lets QNICE read and write MEGA65 HyperRAM through qnice2hyperram.vhd.

For core-owned real-time access to HyperRAM, use the separate hr_core_* Avalon-style interface provided to MEGA65_Core. Do not confuse that with the framework device window. The device window is the Shell/QNICE view; hr_core_* is the core-side memory interface.


14. Generated firmware constants and rebuilds

CORE/m2m-rom/make_rom.sh generates several assembly files from VHDL sources:

Generated file Purpose
osm_const.asm menu line and menu group constants derived from config.vhd and mega65.vhd
globals.asm counts such as VDRIVES_MAX, CRTROM_MAN_MAX, CRTROM_AUT_MAX
shell_fhandles.asm FAT32 file-handle storage for drives and manual ROM slots
shell_fh_ptrs.asm pointer tables to those file handles

Run CORE/m2m-rom/make_rom.sh after changing firmware-visible counts, menu positions, or core-specific Shell callbacks. Examples:

  • C_VDNUM
  • C_CRTROMS_MAN_NUM
  • C_CRTROMS_AUTO_NUM
  • the number or order of OPTM_G_LOAD_ROM or OPTM_G_MOUNT_DRV lines
  • callback code in CORE/m2m-rom/m2m-rom.asm
cd CORE/m2m-rom
./make_rom.sh

Then re-synthesize, because m2m-rom.rom is read by QNICE VHDL at synthesis time.

make_rom.sh requires the QNICE assembler/toolchain. If that toolchain is not built on your machine yet, build it before regenerating the Shell ROM.

If you only change a VHDL target device ID, a filename string, or device logic without changing generated firmware counts, you may not need to regenerate the Shell ROM, but you still need to re-synthesize. If you add a new VHDL helper file, add it to the Vivado projects.

Do not hand-edit the generated .asm files. Fix the source VHDL or m2m-rom.asm, then run make_rom.sh.


15. Debugging checklist

When a device does not behave, reduce the problem to one link at a time.

15.1 Is the device selected and decoded?

  • Is the device ID >= x"0100"?
  • Is the ID defined once in globals.vhd?
  • Does core_specific_devices have a when branch for it?
  • Do unmapped reads return x"EEEE"?

15.2 Is the address packing what you think it is?

  • Did you subtract the 0x7000 aperture base when reasoning about offsets?
  • Does one QNICE address mean one byte, one word, one byte lane, or one register?
  • Are you slicing enough address bits for the actual target capacity?

15.3 Are side effects gated correctly?

  • Writes can normally use qnice_dev_we_i; it is already data-aperture qualified.
  • Non-write register strobes and other side effects should use qnice_dev_ce_i.
  • A selected qnice_dev_id_i alone is not an access.

15.4 Does the Shell know about it?

  • For manual loading, does C_CRTROMS_MAN_NUM match the number of OPTM_G_LOAD_ROM lines?
  • For automatic loading, do C_CRTROMS_AUTO_NUM, C_CRTROMS_AUTO, and C_CRTROMS_AUTO_NAMES agree?
  • For virtual drives, do C_VDNUM, C_VD_DEVICE, and C_VD_BUFFER agree?
  • Did you run make_rom.sh if counts, menu positions, or callbacks changed?

15.5 Does the target need wait or CSR?

  • Plain BRAM usually returns immediately.
  • HyperRAM, parsers, FIFOs, and cross-domain loaders may need qnice_dev_wait_o.
  • Standard manual OPTM_G_LOAD_ROM targets need the expected load-status path, usually the CSR window at 4K QNICE-address window 0xFFFF.

16. Common traps

Using framework-reserved IDs

IDs below x"0100" belong to M2M. Start core-specific devices at x"0100".

Forgetting decoder defaults

Every signal assigned in a when branch needs a default before the case. Otherwise you risk latches or stale write strobes.

Treating all addresses as bytes

The bus address is just a 28-bit device address. Your device decides whether address N means byte N, word N, upper byte lane, lower byte lane, or register N.

Forgetting qnice_dev_ce_i

qnice_dev_id_i stays at the last selected device until QNICE selects another one. A selected ID alone does not mean a data-window access is happening. Use qnice_dev_ce_i for side effects.

Never asserting wait on a slow device

If a device cannot return read data or complete a write in the expected access, assert qnice_dev_wait_o until it can. HyperRAM bridges, PRG loaders, and CRT loaders are examples of devices that may stall QNICE.

Adding QNICE ports to huge BRAMs

QNICE-visible BRAM ports have timing cost. If QNICE never needs to load, save, or inspect a memory, do not wire it to the device bus.

Forgetting array counts

Several arrays keep an x"EEEE" end marker, but the Shell also uses explicit counts such as C_CRTROMS_MAN_NUM, C_CRTROMS_AUTO_NUM, and C_VDNUM. Keep the count, array contents, and menu lines in sync.

Assuming optional ROMs are validated

C_CRTROMTYPE_OPTIONAL only means "continue if the file is missing." It does not mean "accept only the right file size." Validate fixed-size ROMs yourself.

Rebuilding VHDL but not the Shell ROM

If firmware-visible counts, menu item positions, or callbacks changed, run CORE/m2m-rom/make_rom.sh and re-synthesize.


17. What is not a device

It is tempting to put everything behind qnice_dev_*. Resist that. M2M already has other paths for other kinds of information.

Need Use this instead
menu choices while the core runs qnice_osm_control_i / main_osm_control_i
simple custom firmware flags qnice_gp_reg_i or dedicated control signals
core video/audio mode outputs dedicated qnice_*_o ports from MEGA65_Core
keyboard and joystick connection framework keyboard/joystick ports
core real-time HyperRAM master access hr_core_*, not the QNICE device aperture
static menu/help/config data config.vhd and the framework config device

Use a device when QNICE needs addressable data, a RAM/ROM buffer, or a register file.


18. Minimal checklist for adding a device

Quick map by goal:

Goal Files/constants to touch make_rom.sh?
expose RAM for debug or direct Shell access globals.vhd, mega65.vhd no, unless firmware-visible tables changed
auto-load a boot ROM globals.vhd, target device in mega65.vhd yes if C_CRTROMS_AUTO_NUM changed
manual OSM file load config.vhd, globals.vhd, target device, usually CSR, callbacks if needed yes
virtual drive globals.vhd, buffer/control devices, vdrives.vhd wiring yes if drive count changed
HyperRAM staging target wrapper plus HyperRAM bridge/cache logic only if Shell-visible tables or callbacks changed
  1. Pick an unused ID >= x"0100".
  2. Define it in CORE/vhdl/globals.vhd.
  3. Decide the address/data packing: byte, word, byte lanes, registers, or CSR.
  4. Instantiate the storage or helper module in mega65.vhd or the appropriate wrapper.
  5. Add one branch to core_specific_devices.
  6. Default all decoder outputs before the case.
  7. Use qnice_dev_we_i for writes, and qnice_dev_ce_i for non-write strobes and side effects.
  8. Drive qnice_dev_wait_o if the device is multi-cycle.
  9. If the Shell must discover it, add the proper C_VD* or C_CRTROMS_* declaration.
  10. If the OSM should trigger it, add the matching OPTM_G_MOUNT_DRV or OPTM_G_LOAD_ROM line in config.vhd.
  11. If needed, add callbacks in CORE/m2m-rom/m2m-rom.asm.
  12. Run CORE/m2m-rom/make_rom.sh if firmware-visible counts, menu positions, or callbacks changed.
  13. Re-synthesize.
  14. Use the QNICE monitor or debug logging to verify the device contents.

19. Related pages

Clone this wiki locally