-
Notifications
You must be signed in to change notification settings - Fork 13
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), theqnice_dev_*ports ofMEGA65_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.
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.
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:
- Select a device ID.
- Select a 4K QNICE-address window inside that device.
- 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 0The 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.
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.
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.
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.
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.
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.
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.
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.
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:
- Set defaults for every output and every write-enable before the
case. - Default unmapped reads to
x"EEEE". - Decode by
qnice_dev_id_i. - Use
qnice_dev_addr_ias the address inside the selected device. - Use
qnice_dev_we_ifor writes. It is already asserted only for data aperture writes. - Qualify non-write side effects and register strobes with
qnice_dev_ce_i. - Drive
qnice_dev_wait_oonly 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."
Once synthesized with monitor or Shell access, the conceptual debug flow is:
- write your device ID to
0xFFF4, - write the wanted 4K QNICE-address window to
0xFFF5, - 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.
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.
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.
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.
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.
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_NUMcounts two-word manual records. -
C_CRTROMS_AUTO_NUMcounts four-word auto-load records. - The arrays must contain at least that many records plus the final
x"EEEE"marker. -
C_CRTROMS_AUTO_NAMESis different: it is a concatenated string whose entries end withENDSTR, not withx"EEEE".
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.
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:
- In
config.vhd, create a menu line withOPTM_G_LOAD_ROM. - In
globals.vhd, add one target record toC_CRTROMS_MAN. - In
mega65.vhdor a helper wrapper, implement the target device. - In
CORE/m2m-rom/m2m-rom.asm, add callbacks if the generic flow needs help. - Run
CORE/m2m-rom/make_rom.shif 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.
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.
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, @R0The 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:
- The Shell selects the payload window and streams the file into the target.
- The Shell selects CSR window
0xFFFF. - The Shell writes the file length through
CRTROM_CSR_FS_LOandCRTROM_CSR_FS_HI. - The Shell writes
CRTROM_CSR_STATUSto tell the device the stream is complete. - The device parses, validates, or finalizes the data.
- The Shell polls
CRTROM_CSR_PARSESTuntil 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.CRTinto 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.
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.
Virtual drives use the device system, but they are not just a RAM.
They have three parts:
-
M2M/vhdl/vdrives.vhd: hardware bridge that speaks the MiSTer-styleimg_mounted,sd_lba,sd_rd,sd_wr,sd_ack,sd_buff_*protocol. - A control/register device, named by
C_VD_DEVICE. - 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:
- The Shell reads
C_VDNUM,C_VD_DEVICE, andC_VD_BUFFERthrough sysinfo. - The user selects a disk image in the file browser.
- The Shell streams the whole image into the buffer device.
- The Shell writes size, type, read-only state, and a mount strobe to
vdrives.vhd. - The MiSTer drive model asks for blocks through
sd_rd/sd_wr. - 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.
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
.CRTfile through a core-specific device window. -
sw_cartridge_csr.vhdpacks 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.
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.
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.
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.
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.
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_VDNUMC_CRTROMS_MAN_NUMC_CRTROMS_AUTO_NUM- the number or order of
OPTM_G_LOAD_ROMorOPTM_G_MOUNT_DRVlines - callback code in
CORE/m2m-rom/m2m-rom.asm
cd CORE/m2m-rom
./make_rom.shThen 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.
When a device does not behave, reduce the problem to one link at a time.
- Is the device ID
>= x"0100"? - Is the ID defined once in
globals.vhd? - Does
core_specific_deviceshave awhenbranch for it? - Do unmapped reads return
x"EEEE"?
- Did you subtract the
0x7000aperture 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?
- 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_ialone is not an access.
- For manual loading, does
C_CRTROMS_MAN_NUMmatch the number ofOPTM_G_LOAD_ROMlines? - For automatic loading, do
C_CRTROMS_AUTO_NUM,C_CRTROMS_AUTO, andC_CRTROMS_AUTO_NAMESagree? - For virtual drives, do
C_VDNUM,C_VD_DEVICE, andC_VD_BUFFERagree? - Did you run
make_rom.shif counts, menu positions, or callbacks changed?
- Plain BRAM usually returns immediately.
- HyperRAM, parsers, FIFOs, and cross-domain loaders may need
qnice_dev_wait_o. - Standard manual
OPTM_G_LOAD_ROMtargets need the expected load-status path, usually the CSR window at 4K QNICE-address window0xFFFF.
IDs below x"0100" belong to M2M. Start core-specific devices at x"0100".
Every signal assigned in a when branch needs a default before the case.
Otherwise you risk latches or stale write strobes.
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.
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.
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.
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.
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.
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.
If firmware-visible counts, menu item positions, or callbacks changed, run
CORE/m2m-rom/make_rom.sh and re-synthesize.
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.
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 |
- Pick an unused ID
>= x"0100". - Define it in
CORE/vhdl/globals.vhd. - Decide the address/data packing: byte, word, byte lanes, registers, or CSR.
- Instantiate the storage or helper module in
mega65.vhdor the appropriate wrapper. - Add one branch to
core_specific_devices. - Default all decoder outputs before the
case. - Use
qnice_dev_we_ifor writes, andqnice_dev_ce_ifor non-write strobes and side effects. - Drive
qnice_dev_wait_oif the device is multi-cycle. - If the Shell must discover it, add the proper
C_VD*orC_CRTROMS_*declaration. - If the OSM should trigger it, add the matching
OPTM_G_MOUNT_DRVorOPTM_G_LOAD_ROMline inconfig.vhd. - If needed, add callbacks in
CORE/m2m-rom/m2m-rom.asm. - Run
CORE/m2m-rom/make_rom.shif firmware-visible counts, menu positions, or callbacks changed. - Re-synthesize.
- Use the QNICE monitor or debug logging to verify the device contents.
- QNICE - short WIP note about the helper CPU.
-
On‐Screen‐Menu (OSM) - how
OPTM_G_LOAD_ROMandOPTM_G_MOUNT_DRVare placed in the menu. - File- and Directory Browser - user-facing file selection and filters.
- Virtual Drives - the deeper disk-image mount/write-back story.
- config.vhd Switches and Settings - static configuration exposed through the config device.
- globals.vhd - core-level constants, including device IDs and loader tables.
- m2m-rom.asm - core-specific Shell callbacks.
- Clock Domain Crossing (CDC) - crossing data between QNICE, core, and HyperRAM clock domains.
- The Ultimate MiSTer2MEGA65 Porting Guide - full porting workflow.