Skip to content

config.vhd Switches and Settings

sy2002 edited this page Jun 13, 2026 · 6 revisions

config.vhd Switches and Settings

This guide explains the parts of a MiSTer2MEGA65 (M2M) core's config.vhd that are not the On-Screen Menu structure and not the Welcome & Help screen text.

Scope of this guide. We cover the remaining switches and settings in config.vhd: version strings, file-browser paths, the SD-card settings file, startup reset behavior, pause and input routing, HDMI scaler ownership, virtual-drive write-back tuning, and the serial-console core name. We deliberately do not re-teach OPTM_ITEMS, OPTM_GROUPS, submenus, %s, OPTM_DEP, or WHS_DATA/WHS. Those are covered by the two companion guides: "How to build an On-Screen Menu (OSM) in config.vhd" and "How to build the Welcome & Help screens in config.vhd". Everything here is based on the shipped C64 reference core and on the framework/Shell source that consumes these settings.


1. The mental model: config.vhd is a read-only table for the Shell

The useful way to think about config.vhd is this:

You author constants in VHDL. At synthesis time they become a little read-only ROM. At runtime the QNICE Shell reads that ROM and turns the values into live behavior.

The Shell does not parse VHDL. It sees config.vhd through a small addressable interface:

   QNICE Shell firmware          qnice_wrapper.vhd             CORE/vhdl/config.vhd
  ┌────────────────────┐       ┌────────────────────┐        ┌─────────────────────┐
  │ select device      │       │ exposes framework  │        │ address decoder     │
  │ M2M$CONFIG         │──────►│ device 0x0002      │───────►│ returns constants   │
  │ select a 4 KB      │       │ (C_DEV_OSM_CONFIG) │        │ as 16-bit words     │
  │ config "window"    │       └────────────────────┘        └─────────────────────┘
  │ read from 0x7000   │
  └────────────────────┘

On the VHDL side the address is split like this:

Address bits Meaning
27..12 the selector: which config block to read
11..0 the index inside that 4 KB config block

For example:

  • selector 0x0100 returns the default file-browser directory,
  • selector 0x0101 returns the SD-card config filename,
  • selector 0x0110 returns the general settings words,
  • selector 0x0200 returns the core name for the serial terminal.

String constants are served one character per 16-bit word and are zero-terminated. Boolean constants are served as 0 or 1. Unknown selectors return 0xEEEE, which is why a wrong selector usually shows up as a very obvious sentinel value in debugging.

The golden rule about config.vhd

config.vhd has two kinds of lines:

  • configuration lines, where you set constants such as DIR_START, RESET_COUNTER, SAVE_SETTINGS, and ASCAL_USAGE, and
  • framework lines marked !!! DO NOT TOUCH !!!, such as selector constants, helper types, and the address decoder at the bottom.

You edit the configuration constants. You do not edit the selector values or the address decoder. The firmware and VHDL framework agree on those numbers.


2. The big map of config.vhd

Here is the complete shape of the C64 reference config.vhd, grouped by topic. The rows marked "covered elsewhere" are intentionally only mentioned here.

Section in config.vhd You edit? Main constants Covered here?
Welcome & Help screens yes CORE_VERSION, WHS_*, WHS_DATA, WHS only CORE_VERSION; screen authoring is in Welcome and Help Screens
File browser / config file yes DIR_START, CFG_FILE yes
General configuration yes RESET_COUNTER, OPTM_PAUSE, welcome/input/ascal/vdrive/save flags yes, except the full Welcome-screen behavior in Welcome and Help Screens
Core name yes CORENAME yes
OSM menu yes OPTM_ITEMS, OPTM_GROUPS, OPTM_SIZE, OPTM_DX, OPTM_DY, OPTM_S_* mostly covered by On‐Screen‐Menu (OSM); persistence details are here
Address decoder no addr_decode, selectors, getGenConf reference only

The C64 reference is useful because it exercises almost all of the framework: SD-card persistence, file browsing, virtual drives, HDMI filters, simulated cartridges, help screens, and a large nested OSM. But the settings described in this page are framework-level settings; another M2M core will use the same mechanisms even when its own menu and core logic are completely different.


3. Version, filenames, and the one-line release habit

The C64 reference keeps the release version in one place:

constant CORE_VERSION : string := "WIP-V6-A17";

That single string is then reused in several places:

constant CFG_FILE : string := "/c64/c64mega65-" & CORE_VERSION;

constant CORENAME : string :=
   "Commodore 64 for MEGA65 Version " & CORE_VERSION;

The Welcome and Help screen strings also embed CORE_VERSION, so the version shown to the user and the version used for the settings filename stay in sync. The C64 release script checks this constant and, for release builds, generates a matching c64mega65-<version> settings file next to the .cor files.

Why the settings filename includes the version

The saved OSM settings file is one byte per menu line. That means the file layout depends on the exact OPTM_ITEMS / OPTM_GROUPS layout of the core version that created it.

If you ship version-specific filenames:

constant CFG_FILE : string := "/c64/c64mega65-" & CORE_VERSION;

then several core versions can live on one SD card at the same time:

/c64/c64mega65-V6
/c64/c64mega65-V6.1
/c64/c64mega65-WIP-V6-A17

Each version reads only its own settings file. This avoids a subtle class of bugs where an old settings file has the right length but the bits now mean different menu lines.

What to update when making a release

For the C64 reference workflow, update CORE_VERSION first. Everything derived from it follows:

  • welcome/help page version text,
  • the serial-terminal CORENAME,
  • the on-SD-card CFG_FILE,
  • the release-time generated config file name.

If your core does not use a release script like the C64 reference, still copy the same habit: make one version constant and derive user-facing text and config filenames from it.


4. File browser start path: DIR_START

The file browser's first directory comes from:

constant DIR_START : string := "/c64";

When the Shell opens the file browser for the first time, it reads selector 0x0100 (M2M$CFG_DIR_START) and passes that string to the directory browser. In the C64 reference this means the user starts in /c64, because that is where the C64 user files and config file are expected to live.

What happens if the directory does not exist?

The Shell tries DIR_START. If that path is not found, the file selector falls back to the root directory (/) instead of aborting. This is helpful for users with an empty or freshly formatted SD card.

That fallback is only for the "path not found" case. Other directory-read errors are handled as errors. So this is not a substitute for good release instructions: if your docs say "put files in /mycore", then DIR_START should match that path.

Good values

Use an absolute path with a leading slash:

constant DIR_START : string := "/c64";
constant DIR_START : string := "/amiga";
constant DIR_START : string := "/games";

Keep it short and boring. The path is stored as a zero-terminated string in the config ROM and handed directly to the FAT32 directory browser.


5. The settings file: CFG_FILE and SAVE_SETTINGS

Two constants work together:

constant CFG_FILE      : string  := "/c64/c64mega65-" & CORE_VERSION;
constant SAVE_SETTINGS : boolean := true;

CFG_FILE is the path of the SD-card file. SAVE_SETTINGS says whether the Shell should try to load and save OSM state through that file.

The important part is this:

The M2M FAT32 code can write existing file contents, but it does not create or grow files. So SAVE_SETTINGS = true is not enough. The file must already exist and must already have exactly the right size.

The file format

The file is exactly OPTM_SIZE bytes long:

File byte Meaning
byte 0 OSM line 0 selected? 0 or 1
byte 1 OSM line 1 selected? 0 or 1
byte 2 OSM line 2 selected? 0 or 1
... ...

It mirrors the runtime 256-bit OSM state vector: one bit per flat menu line, not one bit per group id. The OSM guide explains that flat index model in detail.

There is one special marker:

  • if the first byte is 0xFF, the Shell treats the file as freshly generated and uses the OPTM_G_STDSEL defaults from config.vhd.

The helper script writes exactly that kind of fresh file:

cd M2M/tools
./make_config.sh c64mega65-WIP-V6-A17 auto

With auto, the script reads OPTM_SIZE from ../../CORE/vhdl/config.vhd and writes that many bytes of 0xFF.

Load behavior at startup

At Shell startup, the menu state is initialized in one of two ways.

Case A: saved settings are loaded. This happens only when all of these are true:

  • SAVE_SETTINGS = true,
  • the SD card can be mounted,
  • CFG_FILE exists,
  • its file size is exactly OPTM_SIZE bytes,
  • the first byte is not 0xFF,
  • every byte read is either 0 or 1.

Case B: defaults are used. This happens when SAVE_SETTINGS = false, the file is missing, the size is wrong, the first byte is 0xFF, or the SD card cannot be mounted. Defaults come from OPTM_G_STDSEL.

This fallback is deliberate. A missing config file does not break the core; it only means settings do not persist.

There is one harsher case: if the file passes the size and first-byte checks but later contains a byte other than 0 or 1, the Shell treats it as a corrupt settings file and stops with a fatal error. A generated all-0xFF file avoids that because the first byte is tested before the rest of the file is read.

Save behavior when the OSM closes

When the user closes the OSM, the Shell saves only when it has a valid config file handle and the remembered settings actually changed.

There are three important guard rails:

  • Mount-drive lines are not saved. The Shell writes 0 for lines tagged OPTM_G_MOUNT_DRV, because a mounted disk image is runtime state, not a persistent menu preference.
  • Load-ROM lines are not saved. The Shell writes 0 for lines tagged OPTM_G_LOAD_ROM, for the same reason.
  • Saving is disabled after an SD-card switch. If the active SD card changes, the Shell clears the config-file handle so it cannot accidentally write settings to a different card than the one it loaded from.

Saving is also skipped while any virtual-drive write cache is dirty, because the same FAT32 machinery and hardware buffer are involved in disk-image write-back. This protects disk-image data.

When to regenerate the file

Regenerate and redistribute the settings file whenever the persistent OSM layout changes.

The obvious case is an OPTM_SIZE change. There is also a subtler case: a same-size reorder or semantic change of OPTM_ITEMS / OPTM_GROUPS can still make old saved bytes mean the wrong thing. The Shell only checks the file size, then maps saved bytes one-to-one onto the flat menu-line state.

Changing help text or welcome text does not change that settings format. Adding, removing, reordering, or reusing persistent menu lines does.

For the C64 reference release flow, make_release.py generates the matching c64mega65-<version> file automatically. During development, use M2M/tools/make_config.sh.


6. Startup reset: RESET_COUNTER

The framework starts with the core held in reset. The Shell then runs its initialization, loads menu defaults or saved settings, and finally releases the core.

This constant controls an extra delay while reset is still active:

constant RESET_COUNTER : natural := 100;

The value is not milliseconds. The source comment calls it a number of "QNICE loops". The firmware simply counts from zero to RESET_COUNTER inside RP_SYSTEM_START before clearing the reset bit in the QNICE control/status register.

Use it as a coarse startup stabilizer:

Value Effect
0 no extra reset-hold loop
100 C64 reference default
larger keep the core in reset longer before first run

What signal does it control?

The Shell writes the QNICE CSR reset bit. In VHDL that bit becomes:

reg_csr(0)
  -> csr_reset_o
  -> qnice_csr_reset
  -> main_qnice_reset_o
  -> your core's reset input path

In the C64 reference, that reset reaches main_reset_core_i and ultimately the soft-reset input of main.vhd. Other cores may route it differently, but the framework signal is the same.

Common choice

Most cores leave the core in reset until the Shell has loaded the menu state. This matters because OSM defaults can feed clock, video, memory, or ROM-selection logic before the emulated machine starts.


7. Pause while the Options menu is open: OPTM_PAUSE

The Options menu is drawn by QNICE as an overlay. From that menu the user can also open Help screens and the file browser. Some cores should keep running behind those menu-driven overlays; some should freeze.

That choice is:

constant OPTM_PAUSE : boolean := false;

When the user opens the Options menu with Help, the Shell calls RP_OPTM_START. That routine first clears pause, keyboard, and joystick CSR bits, then turns selected ones back on according to the config.vhd settings. If OPTM_PAUSE = true, it sets the CSR pause bit while that menu session is active. When the menu closes, the Shell clears pause again.

The startup Welcome screen is separate. It is also drawn by QNICE, but it is not entered through RP_OPTM_START; its behavior is controlled by WELCOME_ACTIVE / WELCOME_AT_RESET.

In VHDL the pause bit follows this path:

reg_csr(1)
  -> csr_pause_o
  -> qnice_csr_pause
  -> main_qnice_pause_o
  -> your core's pause input

The C64 core passes this to the MiSTer C64 core's pause input and to the simulated IEC drive. Another core can interpret pause however it needs to, but the framework signal is just "pause requested by Shell".

Choosing true or false

Use false when:

  • the user should be able to mount media while the machine keeps running,
  • the original system had background activity you do not want to freeze,
  • pausing the core would disturb audio/video or timing-sensitive behavior.

Use true when:

  • menu interaction should freeze gameplay,
  • accidental input during menus would be harmful,
  • the core has a clean pause implementation.

The C64 reference uses false.


8. Keyboard and joystick routing during reset and OSD

The MEGA65 keyboard and joystick ports are shared between the framework Shell and the running core. The Shell always needs to read keys for menus. The question is whether the core should also receive those inputs during reset or while an OSD is open.

There are six booleans:

constant KEYBOARD_AT_RESET : boolean := false;
constant JOY_1_AT_RESET    : boolean := false;
constant JOY_2_AT_RESET    : boolean := false;

constant KEYBOARD_AT_OSD   : boolean := false;
constant JOY_1_AT_OSD      : boolean := false;
constant JOY_2_AT_OSD      : boolean := false;

The OSD group is applied when the Options menu opens: RP_OPTM_START first clears pause, keyboard, and joystick CSR bits, then turns selected ones back on according to these *_AT_OSD flags.

The reset group is more limited in the current firmware. RP_SYSTEM_START turns keyboard or joystick bits on when a *_AT_RESET flag is true, but it does not clear those bits when a flag is false. Also note that the QNICE CSR default already has keyboard and joystick bits enabled. Treat the reset flags as "request input on during the startup reset window", not as a guaranteed input-disable switch.

What they physically do

The booleans set or clear CSR bits:

Setting CSR bit VHDL destination
KEYBOARD_* bit 3 m2m_keyb.enable_core_i
JOY_1_* bit 4 joystick port 1 enable in the debouncer
JOY_2_* bit 5 joystick port 2 enable in the debouncer

The Shell's own keyboard snapshot is independent. Even with KEYBOARD_AT_OSD = false, the Shell can still read Help, Cursor, Return, Space, Run/Stop, F1, and F3 for the menu and file browser. The setting only decides whether the core also sees those key presses.

Why the C64 reference keeps them false

The C64 reference sets all six to false. The most important effect is in the OSM path: while the menu is open, menu key presses do not leak into the C64. After the Shell closes the menu, it waits briefly and reconnects keyboard and joysticks.

This is usually the beginner-safe default. Turn the OSD flags on only when the core must keep receiving input while a menu or full-screen overlay is active.


9. Welcome flags in the general settings block

Two Welcome-screen booleans live in the same general settings block as reset, pause, input routing, Ascal, and virtual-drive tuning:

constant WELCOME_ACTIVE   : boolean := false;
constant WELCOME_AT_RESET : boolean := false;

They are indices 3 and 4 in selector 0x0110 (M2M$CFG_GENERAL). Their full behavior belongs to the Welcome & Help screen guide; they are mentioned here only so the general settings table is complete. These flags are interpreted by the Shell and do not feed a core VHDL signal directly.


10. HDMI scaler ownership: ASCAL_USAGE and ASCAL_MODE

ascal.vhd is the MiSTer scaler used for HDMI output. The framework exposes a 5-bit Ascal mode word to it:

Bits Meaning
2..0 scaler mode: nearest, bilinear, sharp bilinear, bicubic, polyphase
3 triple buffering
4 reserved in the current framework interface

config.vhd decides who owns that mode word:

constant ASCAL_USAGE : natural := 1;
constant ASCAL_MODE  : natural := 0;

There are three modes.

ASCAL_USAGE Name in firmware Meaning
0 M2M$CFG_AUSE_CFG fixed: Shell writes ASCAL_MODE from config.vhd into M2M$ASCAL_MODE at startup
1 M2M$CFG_AUSE_CUSTOM custom: generic Shell does nothing; your core-specific QNICE assembly may write M2M$ASCAL_MODE; the C64 core offers various HDMI filters by implementing custom code in m2m-rom.asm
2 M2M$CFG_AUSE_AUTO auto: QNICE sets CSR bit 11, making sure that ascal_mode_o that you configure in mega65.vhd defines ASCAL_MODE

Mode 0: fixed by config.vhd

Use this for a simple core with one scaler mode:

constant ASCAL_USAGE : natural := 0;
constant ASCAL_MODE  : natural := 0; -- nearest neighbor

At startup, ASCAL_INIT writes ASCAL_MODE to the QNICE register M2M$ASCAL_MODE. After that, the mode stays there unless some custom firmware changes it.

Mode 1: custom QNICE code owns Ascal

This is what the C64 reference uses:

constant ASCAL_USAGE : natural := 1;
constant ASCAL_MODE  : natural := 0; -- ignored in this mode

The generic Shell leaves the mode register alone. The C64-specific CORE/m2m-rom/m2m-rom.asm code reads the selected HDMI filter from the OSM state vector, writes M2M$ASCAL_MODE, and for polyphase filters also loads the matching coefficient tables into Ascal's polyphase RAM.

This is how the C64 menu can offer:

  • No Filter,
  • Sharp Bilinear,
  • Bicubic,
  • Smooth,
  • Lanczos,
  • Scanlines,
  • CRT (S-Video),
  • CRT (Composite).

The C64-specific code explicitly assumes ASCAL_USAGE = 1. If you change the C64 reference back to ASCAL_USAGE = 2, those firmware writes to M2M$ASCAL_MODE become ineffective because the register is auto-synced from the VHDL input instead.

Mode 2: core VHDL owns Ascal

Use this when the scaler mode should be a live VHDL signal from your core. In this mode the Shell sets CSR bit 11, and M2M/vhdl/QNICE/qnice.vhd takes the 5-bit Ascal mode from the VHDL input path instead of from the writable M2M$ASCAL_MODE register.

As an M2M core author, you configure this in your core top, CORE/vhdl/mega65.vhd, by driving the MEGA65_Core output ports:

qnice_ascal_mode_o      <= "...";
qnice_ascal_polyphase_o <= '0' or '1';
qnice_ascal_triplebuf_o <= '0' or '1';

Practical rule

Pick exactly one owner:

  • simple constant mode: ASCAL_USAGE = 0,
  • custom Shell logic or filter dispatcher: ASCAL_USAGE = 1,
  • live VHDL-controlled scaler mode: ASCAL_USAGE = 2.

Do not mix custom M2M$ASCAL_MODE writes with auto mode and expect both to win. Auto mode deliberately makes the register follow the VHDL input.


11. Virtual-drive write-back tuning

Virtual drives are deliberately buffered. The emulated core does not write directly to the FAT32 file on the SD card.

Instead, the framework exposes the familiar MiSTer-style sd_lba, sd_rd, sd_wr, sd_ack, and sd_buff_* signals to the core. The QNICE Shell services those requests. For writes, it copies the bytes from the core's small transfer buffer into the mounted disk image that lives in FPGA-accessible RAM, acknowledges the write, and only later writes the RAM image back to the SD-card file.

This is why saves can be fast enough for timing-sensitive cores such as the C64: the core sees a RAM-backed disk image first. Physical SD-card write-back happens afterward in the background.

Two config.vhd constants tune that background write-back:

constant VD_ANTI_THRASHING_DELAY : natural := 2000;
constant VD_ITERATION_SIZE       : natural := 100;

How a write becomes an SD-card update

The source-level flow is:

  1. The core raises a drive write request (sd_wr_i).
  2. HANDLE_IO in M2M/rom/shell.asm notices the request and calls HANDLE_DRV_WR.
  3. HANDLE_DRV_WR copies the requested byte range from the core-side transfer buffer into the full disk-image buffer in RAM.
  4. The Shell acknowledges the write by pulsing VD_ACK.
  5. M2M/vhdl/vdrives.vhd sees that a latched write was acknowledged and marks the drive cache dirty.
  6. While the cache is dirty, hardware counts down a quiet-time delay. Every new write clears the "flush started" state and restarts that delay.
  7. When the delay reaches zero, hardware raises VD_CACHE_FLUSH_ST.
  8. The next FLUSH_CACHE pass starts or continues the SD-card write-back.

Two details matter for tuning.

First, the delay is handled in hardware. The firmware does not guess how long ago the last write happened; it reads the VD_CACHE_FLUSH_ST flag from vdrives.vhd.

Second, the current firmware does not keep a changed-sector bitmap. Once a flush starts, it seeks to byte 0 of the mounted image file and writes the disk-image buffer back to the file until the file size is exhausted. It spreads that full write-back across many small iterations so the Shell can keep polling input, menu state, and new drive requests.

VD_ANTI_THRASHING_DELAY

This is the quiet time, in milliseconds, between the last acknowledged core write and the moment when hardware allows flushing to start.

constant VD_ANTI_THRASHING_DELAY : natural := 2000;

The default is two seconds. It is called "anti thrashing" because disk saves usually arrive as bursts. If the Shell started rewriting the SD-card file after the first sector, the next sector would make the cache dirty again and force the flush process to restart. Waiting until writes have been quiet for a short time avoids that repeated start-over behavior.

Lowering this value makes write-back begin sooner after the final write. That can make the visible dirty state shorter for small, simple saves. Lower it too far, though, and bursty save routines can repeatedly interrupt the flush before it has made useful progress.

Raising this value makes the framework more patient. That is useful if the core or emulated drive tends to produce several separated write bursts for one logical save. The tradeoff is that the cache remains dirty longer. During that dirty window the C64 reference keeps the drive LED on/yellow, protects soft reset through prevent_reset, and the Shell skips OSM settings persistence.

The VHDL virtual-drive device has one delay register per drive. config.vhd currently exposes one global value, and VD_INIT writes that same value to every drive.

VD_ITERATION_SIZE

This is how many bytes FLUSH_CACHE writes to the SD-card file in one background flush iteration after flushing has started.

constant VD_ITERATION_SIZE : natural := 100;

This is not a sector count, not a block count, and not a VHDL burst length. In the current firmware it is a byte count for one Shell pass through the flush continuation loop. The loop reads one byte from the disk-image RAM buffer, calls f32_fwrite, updates counters, and repeats until it has written VD_ITERATION_SIZE bytes or until the whole mounted image file has been written.

After each partial iteration, the Shell remembers:

  • the current 4 KB RAM window,
  • the offset inside that window,
  • the high and low words of the remaining byte count.

The next FLUSH_CACHE call resumes from that saved position. When the remaining count reaches zero, the Shell calls f32_fflush and writes 0 to VD_CACHE_DIRTY, which also clears the flushing and start flags in vdrives.vhd.

Larger values clear the dirty state in fewer Shell passes. The cost is that each pass spends longer inside the FAT32 write loop before returning to the main loop. Very large values can make the OSM, file browser, and drive-request polling feel less responsive during active write-back.

Smaller values return to the main loop more often. That improves responsiveness while flushing, but it takes more passes to rewrite the mounted image file, so the dirty state lasts longer.

The framework currently reads one value from config.vhd and copies it into the Shell's per-drive VDRIVES_ITERSIZ array for every drive.

What users see

For mount lines that use %s, the OSM can temporarily show the saving string:

constant OPTM_S_SAVING : string := "<Saving>";

Those %s replacement strings are explained in the OSM guide. They are relevant here because the dirty-cache state also affects settings persistence: the Shell does not save OSM settings while a virtual-drive cache is dirty.


12. CORENAME: serial-terminal identity

CORENAME is not the splash screen and not the OSM title. It is the name printed to the QNICE serial terminal when the Shell starts:

constant CORENAME : string :=
   "Commodore 64 for MEGA65 Version " & CORE_VERSION;

The Shell startup code calls LOG_CORENAME, which selects config window 0x0200 and prints this zero-terminated string.

For end users, use the Welcome screen, Help screens, and OSM text. CORENAME is for the debug console and logs.


13. Selector reference

You normally do not need this table to author a core. It is useful when reading the Shell source or debugging with the QNICE monitor.

All selectors are 4 KB windows. QNICE selects the config device (M2M$CONFIG, device 0x0002), writes one of these selectors to M2M$RAMROM_4KWIN, then reads from M2M$RAMROM_DATA (0x7000 + index).

Selector Firmware name Data served by config.vhd
0x0100 M2M$CFG_DIR_START DIR_START string
0x0101 M2M$CFG_CFG_FILE CFG_FILE string
0x0110 M2M$CFG_GENERAL general settings words, table below
0x0200 M2M$CFG_CORENAME CORENAME string
0x0300..0x030A M2M$CFG_OPTM_* main OSM data, covered by the OSM guide
0x0310..0x0313 M2M$CFG_OPTM_* OSM dependency data, covered by the OSM guide
0x1000.. M2M$CFG_WHS Welcome & Help screen data, covered by the WHS guide

General settings window (0x0110)

The general window is a small word table. Index 0 is unused and returns 0.

Index in config.vhd QNICE address Firmware name Constant
1 0x7001 M2M$CFG_RP_COUNTER RESET_COUNTER
2 0x7002 M2M$CFG_RP_PAUSE OPTM_PAUSE
3 0x7003 M2M$CFG_RP_WELCOME WELCOME_ACTIVE
4 0x7004 M2M$CFG_RP_WLCM_RST WELCOME_AT_RESET
5 0x7005 M2M$CFG_RP_KB_RST KEYBOARD_AT_RESET
6 0x7006 M2M$CFG_RP_J1_RST JOY_1_AT_RESET
7 0x7007 M2M$CFG_RP_J2_RST JOY_2_AT_RESET
8 0x7008 M2M$CFG_RP_KB_OSD KEYBOARD_AT_OSD
9 0x7009 M2M$CFG_RP_J1_OSD JOY_1_AT_OSD
10 0x700A M2M$CFG_RP_J2_OSD JOY_2_AT_OSD
11 0x700B M2M$CFG_ASCAL_USAGE ASCAL_USAGE
12 0x700C M2M$CFG_ASCAL_MODE ASCAL_MODE
13 0x700D M2M$CFG_VD_AT_DELAY VD_ANTI_THRASHING_DELAY
14 0x700E M2M$CFG_VD_ITERSIZE VD_ITERATION_SIZE
15 0x700F M2M$CFG_SAVEOSDCFG SAVE_SETTINGS

Source path from selector to behavior

For source reading, the path is:

CORE/vhdl/config.vhd
  -> M2M/vhdl/qnice_wrapper.vhd exposes it as C_DEV_OSM_CONFIG / M2M$CONFIG
  -> M2M/rom/sysdef.asm names the selector and index constants
  -> M2M/rom/gencfg.asm, options.asm, selectfile.asm, vdrives.asm, coreinfo.asm
     interpret the values
  -> M2M/vhdl/QNICE/qnice.vhd and M2M/vhdl/framework.vhd turn CSR/register writes
     into reset, pause, input-enable, OSM, and Ascal signals

This is why many config.vhd settings have no direct "consumer" in your core RTL. The Shell consumes them first, then writes live framework registers.


14. Recipes

Change the default folder for mounted files

Edit:

constant DIR_START : string := "/mycore";

Then make sure your user docs and release SD-card layout use the same directory. If the folder is not found, the browser falls back to /.

Disable persistent menu settings

Edit:

constant SAVE_SETTINGS : boolean := false;

The Shell will always use OPTM_G_STDSEL defaults at startup and will not try to open CFG_FILE. You can leave CFG_FILE defined; it is simply ignored.

Enable persistent menu settings

Edit:

constant SAVE_SETTINGS : boolean := true;
constant CFG_FILE      : string  := "/mycore/mycore-" & CORE_VERSION;

Then ship a file at that path whose size is exactly OPTM_SIZE bytes. During development:

cd M2M/tools
./make_config.sh mycore-WIP-A1 auto

Put the resulting file in the SD-card directory named by CFG_FILE.

Add menu lines without breaking saved settings

If you add, delete, or reorder OSM lines:

  1. Update OPTM_SIZE.
  2. Re-check OPTM_ITEMS and OPTM_GROUPS.
  3. Re-check any core-side flat line indices (C_MENU_* in the C64 reference).
  4. Run the menu checker if your core has one.
  5. Bump CORE_VERSION if you use versioned config filenames.
  6. Regenerate and ship the settings file.

The dangerous case is not only "wrong file size". A same-size file from an older layout can silently apply old bits to new menu lines. Versioned filenames are the safest defense.

Keep the core from seeing menu key presses

Use the C64 reference defaults:

constant KEYBOARD_AT_OSD : boolean := false;
constant JOY_1_AT_OSD    : boolean := false;
constant JOY_2_AT_OSD    : boolean := false;

The Shell still reads the keyboard. The core does not.

Freeze the core while the Options-menu session is open

Set:

constant OPTM_PAUSE : boolean := true;

Then confirm your core actually honors its pause input. The framework transports the pause request; the core decides what "paused" means.

Use a simple fixed HDMI scaler mode

Set:

constant ASCAL_USAGE : natural := 0;
constant ASCAL_MODE  : natural := 0;

Then no menu or custom QNICE code is needed for Ascal mode selection.

Let VHDL drive the HDMI scaler mode

Set:

constant ASCAL_USAGE : natural := 2;

Then drive these framework ports from your core:

qnice_ascal_mode_o      <= "...";
qnice_ascal_polyphase_o <= '0' or '1';
qnice_ascal_triplebuf_o <= '0' or '1';

Do not also rely on firmware writes to M2M$ASCAL_MODE; auto mode makes the register follow the VHDL input.

Tune virtual-drive save behavior

Start from the C64 defaults unless you have measured a problem:

constant VD_ANTI_THRASHING_DELAY : natural := 2000;
constant VD_ITERATION_SIZE       : natural := 100;

Think of a save as three phases:

core write -> RAM image is dirty -> quiet-time delay -> SD-card flush iterations

VD_ANTI_THRASHING_DELAY controls the middle part. It is measured in milliseconds and starts after an acknowledged core write. Every later write restarts the countdown. Lower it if SD-card write-back remains pending too long after the final write. Keep it at 2000, or raise it, if the emulated drive does several separated bursts for one save operation.

VD_ITERATION_SIZE controls the last part. Once flushing has started, the Shell rewrites the mounted image file from the RAM buffer to SD in chunks of this many bytes per FLUSH_CACHE continuation. The firmware writes one byte at a time and returns to the main loop after each chunk.

Raise VD_ITERATION_SIZE only if flushing itself takes too long after it has started. Larger chunks clear the dirty state in fewer passes, but each pass keeps QNICE inside the FAT32 write loop longer. Test while the core is still running, while the Options menu is open, and while using the file browser during an active write-back.

Do not tune only by watching whether the first save succeeds. Also test a second write while <Saving> is shown. A new write is supposed to cancel the current flush attempt, mark the RAM image dirty again, and wait for another quiet period before restarting the SD-card write-back.


15. Gotchas

SAVE_SETTINGS = true does not create a file. The file must already exist. Use M2M/tools/make_config.sh or your release script.

CFG_FILE must match the shipped filename exactly. If CFG_FILE says /c64/c64mega65-WIP-V6-A17, putting c64mega65-V6 on the SD card will not enable persistence.

OPTM_SIZE is both a menu size and a file format size. Changing the menu layout means changing the settings-file format.

The settings file stores flat line indices, not group ids. Byte 40 means "line 40", not "group id 40".

Mount and load lines are runtime actions, not saved preferences. The Shell writes 0 for those bytes when saving.

The Shell disables settings persistence after an SD-card switch. This is intentional data-corruption protection.

RESET_COUNTER is not a millisecond value. It is a simple QNICE loop count. Use it empirically.

KEYBOARD_AT_OSD = false does not disable menu input. It only disconnects keyboard events from the core. The Shell still sees them.

ASCAL_USAGE decides ownership. If it is 1, custom firmware may write M2M$ASCAL_MODE. If it is 2, the VHDL input wins and firmware writes do not control the scaler.

CORENAME is not user documentation. It is printed to the debug console. Use WHS and OSM text for users.


16. Cheat sheet

Version and paths:

Constant Typical value Purpose
CORE_VERSION "WIP-V6-A17" single version string to reuse
DIR_START "/c64" first file-browser directory
CFG_FILE "/c64/c64mega65-" & CORE_VERSION SD settings file
CORENAME "Commodore 64 for MEGA65 Version " & CORE_VERSION serial-terminal banner

General settings:

Constant C64 default Purpose
RESET_COUNTER 100 extra startup reset-hold loop count
OPTM_PAUSE false pause core while the Options menu session is open
WELCOME_ACTIVE false show WHS element 0 at startup
WELCOME_AT_RESET false repeat welcome after reset
KEYBOARD_AT_RESET false request keyboard enabled during startup reset
JOY_1_AT_RESET false request joystick 1 enabled during startup reset
JOY_2_AT_RESET false request joystick 2 enabled during startup reset
KEYBOARD_AT_OSD false allow keyboard through to core while OSD is open
JOY_1_AT_OSD false allow joystick 1 through while OSD is open
JOY_2_AT_OSD false allow joystick 2 through while OSD is open
ASCAL_USAGE 1 owner of HDMI scaler mode
ASCAL_MODE 0 fixed scaler mode, only used when ASCAL_USAGE = 0
VD_ANTI_THRASHING_DELAY 2000 ms before virtual-drive cache flush starts
VD_ITERATION_SIZE 100 bytes per background flush iteration
SAVE_SETTINGS true enable SD-card OSM persistence if CFG_FILE is valid

Ascal ownership:

ASCAL_USAGE Owner
0 ASCAL_MODE in config.vhd
1 custom QNICE assembly writes M2M$ASCAL_MODE
2 core VHDL drives qnice_ascal_*_o ports

Persistence rules:

  1. SAVE_SETTINGS must be true.
  2. CFG_FILE must exist on SD.
  3. File size must be exactly OPTM_SIZE bytes.
  4. First byte 0xFF means "use defaults, save real settings later".
  5. Bytes must be 0 or 1 once real settings are saved.
  6. Regenerate the file whenever the persistent OSM layout changes, including same-size reorders or semantic changes.

Source-reading path: config.vhd constants become selector windows; the Shell reads them via M2M$CONFIG; the Shell writes CSR, CFM, OSM, and Ascal registers; framework VHDL transports those live registers into reset, pause, input, overlay, scaler, and core-control signals.

Clone this wiki locally