-
Notifications
You must be signed in to change notification settings - Fork 13
On‐Screen‐Menu (OSM)
This guide explains, from the ground up, how the On-Screen Menu (OSM) of a
MiSTer2MEGA65 (M2M) core is defined. The OSM is the overlay menu the user opens
with the Help key to mount disk images, switch options, load ROMs and
so on. You author the whole menu in a single VHDL file, config.vhd — no
firmware programming required.
Scope of this guide. We focus on defining the menu in
config.vhd: the menu text, the item types, submenus, the%splaceholder, conditional (“smart”) visibility, and how a menu choice reaches your core. We deliberately do not cover how the QNICE “Shell” firmware draws the menu, nor the welcome & help screens, nor the rest ofconfig.vhd(clocks, resets, ascal, …). Those are separate topics. Everything here is real and matches the shipped C64 core, which we use as the running example.
The single most important idea: a menu is two arrays that run in lockstep.
-
OPTM_ITEMS— one big string. Every menu line is separated by a\n. This is what the user sees. -
OPTM_GROUPS— an array with one entry per menu line. Each entry is a number that encodes what that line is (a headline? a toggle? a submenu? …). This is the meaning behind each line.
Line i of OPTM_ITEMS is described by entry i of OPTM_GROUPS. They must
have the same number of lines/entries, and that number is OPTM_SIZE.
OPTM_ITEMS (text) OPTM_GROUPS (meaning)
line 0 " C64 for MEGA65\n" <-> OPTM_G_HEADLINE
line 1 "\n" <-> OPTM_G_LINE
line 2 " 8:%s\n" <-> OPTM_G_MOUNT_8 + OPTM_G_MOUNT_DRV + OPTM_G_START
line 3 " PRG:%s\n" <-> OPTM_G_LOAD_PRG + OPTM_G_LOAD_ROM
... ...
(Don’t worry about the exact constant names yet — every one is explained in the sections that follow. For now just see that each line of text on the left pairs with one entry of meaning on the right.)
That is the entire data model. Everything else in this guide is just which
numbers you can put into OPTM_GROUPS, and a few supporting constants.
config.vhd (you edit this) QNICE "Shell" firmware your core, e.g. mega65.vhd
┌───────────────────────────┐ ┌──────────────────────┐ ┌─────────────────────────┐
│ OPTM_ITEMS (the text) │ reads │ draws the menu, │write │ 256-bit "osm_control" │
│ OPTM_GROUPS (the meaning) │──────►│ moves the cursor, │─────►│ vector: bit i = line i │
│ OPTM_SIZE, OPTM_DX/DY, … │ │ remembers selections,│ │ is currently selected. │
│ (no firmware code at all) │ │ saves them to SD card│ │ Your logic reads it via │
└───────────────────────────┘ └──────────────────────┘ │ the C_MENU_* constants. │
└─────────────────────────┘
You only ever edit config.vhd. The firmware is generic and shipped with M2M;
the connection to your hardware is a handful of C_MENU_* constants in your
core’s top-level VHDL (section 11).
config.vhd has two clearly marked regions:
- a “START YOUR CONFIGURATION BELOW THIS LINE” region, where you do all your work, and
- a “!!! DO NOT TOUCH ANYTHING BELOW THIS LINE !!!” region at the bottom (the address decoder, see the Appendix).
Stay in the first region. The framework constants marked !!! DO NOT TOUCH !!!
(the SEL_OPTM_* selectors and the OPTM_G_* flag values) must keep their
values, because the firmware depends on them — but you use them constantly.
Let’s build the smallest useful menu: a title, one on/off option, and a way to close the menu.
-- 1) How many menu lines are there in total?
constant OPTM_SIZE : natural := 5;
-- 2) Define an id for every selectable GROUP you invent (more on ids in §5).
constant OPTM_G_FAST : integer := 1; -- our one toggle gets group id 1
-- 3) The text the user sees (one line per \n, lower-case \n, trailing \n!):
constant OPTM_ITEMS : string :=
" My Core\n" & -- line 0: a title
"\n" & -- line 1: a blank separator line
" Fast mode\n" & -- line 2: an on/off option
"\n" & -- line 3: a blank separator line
" Close Menu\n"; -- line 4: closes the OSM
-- 4) The meaning of each line, in the SAME order:
constant OPTM_GROUPS : OPTM_GTYPE := (
OPTM_G_HEADLINE, -- line 0
OPTM_G_LINE, -- line 1
OPTM_G_FAST + OPTM_G_SINGLESEL + OPTM_G_START,-- line 2 (toggle, cursor starts here)
OPTM_G_LINE, -- line 3
OPTM_G_CLOSE -- line 4
);That is a fully working menu. Read it top to bottom:
- Line 0 is a headline: bright, non-selectable text.
-
Line 1 is a separator: a thin horizontal line. Its text is just
"\n". -
Line 2 is a single-select toggle (
OPTM_G_SINGLESEL) in group1. The user moves the cursor here and presses a key to turn it on/off.OPTM_G_STARTsays “put the cursor here when the menu first opens”. - Line 3 is another separator.
- Line 4 closes the menu when selected.
Two formatting rules you must follow (they are easy to get wrong):
-
Every line ends with a lower-case
\n— including the last one. - Every line that contains a real, selectable item starts with a space. (Decoration lines like headlines/separators conventionally do too.) Forgetting the leading space causes visual glitches.
The
\nis two characters in the VHDL string (\andn); it is not a control character. Always lower-case. An empty/separator line is literally the two characters"\n".
To make this option actually do something, you wire one bit to your logic — that is section 11. For now, focus on the menu structure.
Every OPTM_GROUPS entry is built by adding named constants together.
Because each constant occupies its own disjoint bits, + behaves like a bitwise
OR — you are simply switching attributes on. Here is the full toolbox; the rest
of the guide explains each one:
| You write … | … and the line becomes |
|---|---|
OPTM_G_TEXT (value 0) |
plain, non-selectable text |
OPTM_G_HEADLINE |
non-selectable text in a brighter colour |
OPTM_G_LINE |
a horizontal separator line |
<your id> + OPTM_G_SINGLESEL |
an on/off toggle |
<your id> (shared by ≥2 lines) |
one member of a radio group (pick exactly one) |
… + OPTM_G_STDSEL |
this item is selected by default |
… + OPTM_G_START |
the cursor starts here (use on exactly one line) |
OPTM_G_SUBMENU |
opens a submenu (this line is its label) |
OPTM_G_SUBMENU + OPTM_G_CLOSE |
closes a submenu (a “Back” line) |
<id> + OPTM_G_MOUNT_DRV |
a mount-a-disk-image line |
<id> + OPTM_G_LOAD_ROM |
a load-a-ROM/cartridge line |
<id> + OPTM_G_HELP |
opens a help screen |
OPTM_G_CLOSE |
closes the whole OSM |
… + OPTM_DEP(mother, item) |
this line is only visible under a condition (§9) |
The named constants and their exact bit values are defined for you in the
framework part of config.vhd. You normally never touch the values — you just
combine the names.
These lines structure the menu visually; the cursor skips over them.
-- OPTM_ITEMS: -- OPTM_GROUPS:
" C64 Configuration\n" OPTM_G_HEADLINE -- a bright section title
"\n" OPTM_G_LINE -- a thin separator line
" Right SID Port\n" OPTM_G_TEXT -- normal-colour caption-
OPTM_G_HEADLINE— a section title, drawn brighter. -
OPTM_G_TEXT— plain caption text. Its value is literally0, so it is the “neutral” entry; a line with no special attributes isOPTM_G_TEXT. -
OPTM_G_LINE— a horizontal rule. Its text line is just"\n".
None of these can be selected and none of them have a state.
This is the heart of the system, so read slowly.
The low byte (bits 7…0) of every OPTM_GROUPS entry is a “group id”.
-
Group id 0 means “this line belongs to no group” — that is decoration (
OPTM_G_TEXT,OPTM_G_LINE,OPTM_G_HEADLINE) or a structural line. -
Group ids 1…254 are yours to invent. You declare them as plain constants in the configuration region, e.g.:
constant OPTM_G_EXP_PORT : integer := 4; constant OPTM_G_KERNAL_MODES : integer := 12; constant OPTM_G_MACHINE_MODE : integer := 22;
-
Group id 255 is reserved for
OPTM_G_CLOSE.
Lines that share the same group id form one selectable group. There are exactly two flavours:
Give several lines the same group id and you get a radio group: the user can select exactly one member, and selecting a new one automatically deselects the old one.
-- OPTM_ITEMS: -- OPTM_GROUPS:
" Standard\n" OPTM_G_KERNAL_MODES + OPTM_G_STDSEL -- default choice
" Games System\n" OPTM_G_KERNAL_MODES
" Japanese\n" OPTM_G_KERNAL_MODES
" JiffyDOS\n" OPTM_G_KERNAL_MODESAll four lines carry group id 12 (OPTM_G_KERNAL_MODES). Exactly one of them
must carry OPTM_G_STDSEL (the default). Keep the members of a group together
and inside a single menu level (don’t split a group across a submenu boundary).
Add OPTM_G_SINGLESEL to a line with its own unique group id and you get an
independent on/off switch:
" Simulate 1750 REU 512KB\n" OPTM_G_REU + OPTM_G_SINGLESEL
" Flip joystick ports\n" OPTM_G_FLIP_JOYS + OPTM_G_SINGLESELA toggle is its own group (one line). Add OPTM_G_STDSEL if it should default
to on.
You can stop reading this sub-section if “add the named constants” is enough for
you. For the curious, here is the full bit layout of one OPTM_GROUPS element.
The type allows OPTM_GTC significant bits (currently 30):
| Bits | Meaning |
|---|---|
| 7…0 | group id (0 = none, 1…254 = your groups, 255 = OPTM_G_CLOSE) |
| 8 |
OPTM_G_STDSEL (selected by default) |
| 9 |
OPTM_G_LINE (separator) |
| 10 |
OPTM_G_START (initial cursor position) |
| 11 | mount-drive marker (part of OPTM_G_MOUNT_DRV) |
| 12 | OPTM_G_HEADLINE |
| 13 | help marker (part of OPTM_G_HELP) |
| 14 | submenu marker (part of OPTM_G_SUBMENU) |
| 15 | OPTM_G_SINGLESEL |
| 16 | load-ROM marker (part of OPTM_G_LOAD_ROM) |
| 24…17 | dependency mother group id (8 bits; set by OPTM_DEP, see §9) |
| 28…25 | dependency mother item index (4 bits, 0…15; set by OPTM_DEP) |
| 29 |
OPTM_G_DEPENDENT flag (set by OPTM_DEP) |
The composite constants are just convenient bit combinations, e.g.
OPTM_G_SUBMENU = 16#0C000# is bit 14 + bit 15, and
OPTM_G_MOUNT_DRV = 16#08800# is bit 11 + bit 15. You never need to compute
these — always use the named constants and +.
Three small but essential attributes:
-
OPTM_G_STDSEL— marks the default selection. A radio group needs exactly one member withOPTM_G_STDSEL; a toggle uses it to default to on. Defaults are what the user gets on first boot (or when settings can’t be loaded from SD, see §13). -
OPTM_G_START— marks where the cursor sits when the OSM opens. Put it on exactly one selectable line that is visible in the main menu. (Forgetting it, or hiding it inside a submenu, is a boot error — see §14.) -
OPTM_G_CLOSE— a line that closes the whole OSM when selected. By convention this is a" Close Menu\n"line at the end of the main menu.
Long menus become submenus. A submenu is a bracketed range:
- it opens with a line carrying
OPTM_G_SUBMENU, and - it closes with a line carrying
OPTM_G_SUBMENU + OPTM_G_CLOSE.
The opener line is special: it is also the submenu’s label in the parent
menu. Selecting it enters the submenu; the closer line (typically " Back\n")
returns to the parent.
-- OPTM_ITEMS: -- OPTM_GROUPS:
" Kernal: %s\n" OPTM_G_SUBMENU -- opens "Kernal" (label shows in parent)
" Kernal Selection\n" OPTM_G_HEADLINE
"\n" OPTM_G_LINE
" Standard\n" OPTM_G_KERNAL_MODES + OPTM_G_STDSEL
" Games System\n" OPTM_G_KERNAL_MODES
" Japanese\n" OPTM_G_KERNAL_MODES
" JiffyDOS\n" OPTM_G_KERNAL_MODES
"\n" OPTM_G_LINE
" Back\n" OPTM_G_SUBMENU + OPTM_G_CLOSE -- closes "Kernal"In the main menu the user sees one line, " Kernal: %s" (the %s shows the
current choice, see §8). Entering it reveals the four options.
Nesting. A submenu may contain another submenu — just place a complete
open…close block inside another one. Nesting depth is effectively unlimited
(bounded only by QNICE stack space). " Back\n" (or the Run/Stop key)
always returns exactly one level up.
" Advanced Settings\n" OPTM_G_SUBMENU -- open "Advanced"
...
" OSM: %s\n" OPTM_G_SUBMENU -- open "OSM Scaling" INSIDE Advanced
... options ...
" Back\n" OPTM_G_SUBMENU + OPTM_G_CLOSE -- close "OSM Scaling"
...
" Back\n" OPTM_G_SUBMENU + OPTM_G_CLOSE -- close "Advanced"The opener/closer markers must be balanced — every open needs a matching close, like parentheses. The firmware checks this at boot (§14).
If a menu line’s text contains %s, the firmware replaces it at runtime with a
piece of live text. Where the line gets that text from depends on the line type:
-
On a submenu opener (
OPTM_G_SUBMENU):%sbecomes the label of the first currently-selected radio item found inside that submenu (in flat order, skipping any nested child submenus). So" Kernal: %s\n"showsKernal: JiffyDOSwhen JiffyDOS is selected. The submenu must contain a radio group of its own; otherwise the firmware aborts with a fatal error the moment it first tries to render that%s. (A submenu that holds several radio groups summarises only its first one.) -
On a mount line (
OPTM_G_MOUNT_DRV):%sbecomes the mounted disk image’s filename, or a placeholder when nothing is mounted. The placeholder is the string constantOPTM_S_MOUNT(e.g."<Mount Drive>"). While the write cache is being flushed to SD it briefly showsOPTM_S_SAVING("<Saving>"). -
On a load line (
OPTM_G_LOAD_ROM):%sbecomes the loaded file’s name, orOPTM_S_CRTROM("<Load>") when nothing is loaded.
constant OPTM_S_MOUNT : string := "<Mount Drive>"; -- shown for an empty drive
constant OPTM_S_CRTROM : string := "<Load>"; -- shown for an empty ROM/cart slot
constant OPTM_S_SAVING : string := "<Saving>"; -- shown while flushing to SDA line may contain at most one %s. Lines without %s are shown verbatim.
Sometimes a line only makes sense in a certain context. Example: a C64 can run in PAL or NTSC; the sensible HDMI resolutions differ between the two. Smart dependencies let you list all variants and show only the relevant ones.
Tag a line with OPTM_DEP(mother, item) and it becomes visible only while a
specific item of a specific “mother” group is selected:
-- the "mother": a radio group that chooses PAL (item 0) or NTSC (item 1)
" PAL\n" OPTM_G_MACHINE_MODE + OPTM_G_STDSEL -- item 0
" NTSC\n" OPTM_G_MACHINE_MODE -- item 1
-- dependent lines elsewhere in the menu (e.g. inside the HDMI submenu):
" 16:9 720p 50 Hz\n" OPTM_G_HDMI_MODES_PAL + OPTM_G_STDSEL + OPTM_DEP(OPTM_G_MACHINE_MODE, 0)
" 16:9 720p 59.94 Hz\n" OPTM_G_HDMI_MODES_NTSC + OPTM_G_STDSEL + OPTM_DEP(OPTM_G_MACHINE_MODE, 1)
" 4:3 576p 50 Hz\n" OPTM_G_HDMI_MODES_PAL + OPTM_DEP(OPTM_G_MACHINE_MODE, 0)
" 4:3 480p 59.94 Hz\n" OPTM_G_HDMI_MODES_NTSC + OPTM_DEP(OPTM_G_MACHINE_MODE, 1)While PAL is selected the user sees only the 50 Hz lines; switch to NTSC
and the menu instantly reflows to show only the 59.94 Hz lines. The hidden lines
keep their own state, so a PAL choice and an NTSC choice are each remembered.
The two forms of OPTM_DEP(mother, item):
-
Radio mother:
itemis the 0-based index of the controlling member (counting that group’s lines top to bottom), in the range 0…15 (the field is 4 bits, so a radio mother used as a dependency source may have at most 16 members). Visible while that member is selected. Above, PAL is item 0 and NTSC is item 1. -
Single-select (toggle) mother:
item = 1means visible while the toggle is ON;item = 0means visible while the toggle is OFF.
Rules (enforced at boot — see §14):
- Each variant set should be its own group (e.g. the PAL HDMI modes are one group, the NTSC HDMI modes another). That way each remembers its own choice.
-
All members of one group must carry the same
OPTM_DEPtag (or none). - A “mother” group must not itself be dependent (no chains).
- You may not put
OPTM_DEPon a submenu opener/closer, a mount line, a load-ROM line, a help line, the close line, or the start line.
Compatibility. Smart dependencies are invisible to your core: a dependent line keeps its own state bit, its default and its saved byte. Older cores whose
config.vhdpredates this feature keep working unchanged — the firmware detects the feature is absent and behaves exactly as before.
Layout tip. Place a mother group and the lines that depend on it so the cursor can never sit on a dependent line at the instant its mother changes — the easiest way is to keep them in different submenus (the C64 keeps PAL/NTSC in the Model submenu and the dependent display modes in the HDMI submenu).
A few line types connect the menu to other M2M subsystems. You declare them like any group, plus the matching attribute. The detailed behaviour of these subsystems is out of scope here; what matters for the menu is how you mark the line:
-
Mount a disk image:
<id> + OPTM_G_MOUNT_DRV. The N-th such line in the menu corresponds to virtual drive N (first occurrence = drive 0, etc.). Usually paired with%sto show the mounted filename. -
Load a ROM / cartridge:
<id> + OPTM_G_LOAD_ROM. The N-th such line is load slot N. Also usually paired with%s. -
Open a help screen:
<id> + OPTM_G_HELP. The first such line opens the first help screen, the second opens the next, and so on. (The help/welcome screens themselves are configured elsewhere inconfig.vhdand are a separate topic.)
" 8:%s\n" OPTM_G_MOUNT_8 + OPTM_G_MOUNT_DRV + OPTM_G_START
" CRT:%s\n" OPTM_G_MOUNT_CRT + OPTM_G_LOAD_ROM
" About & Help\n" OPTM_G_ABOUT_HELP + OPTM_G_HELPDefining the menu is only half the story — a toggle does nothing until your hardware reads it. Adding one option touches three places:
-
Declare a group id in
config.vhd’s configuration region —constant OPTM_G_FAST : integer := 1; -
Add the line to
OPTM_ITEMSandOPTM_GROUPS, and bumpOPTM_SIZE. -
Read the bit in your core’s top-level VHDL —
CORE/vhdl/mega65.vhdfor the C64 — via aC_MENU_*constant. This step is what the rest of this section is about. (Note the split: theOPTM_G_*group ids live inconfig.vhd; theC_MENU_*line indices live inmega65.vhd.)
The firmware maintains a 256-bit vector of the menu state, one bit per menu
line (not per group): bit i is 1 when line i is currently selected. The
framework hands you this vector in both clock domains — qnice_osm_control_i
(QNICE clock) and main_osm_control_i (your core’s clock), with identical bit
layout. Read whichever copy matches the clock domain of the logic that consumes
it.
To read a specific option you need its flat line index — exactly what the
C_MENU_* constants in mega65.vhd provide:
-- in mega65.vhd: the flat line index (0-based) of each option you care about
constant C_MENU_KERNAL_JIFFY : natural := 107; -- " JiffyDOS" is line 107
constant C_MENU_FLIP_JOYS : natural := 32; -- " Flip joystick ports" is line 32
-- core-clock logic reads the core-clock copy:
if main_osm_control_i(C_MENU_KERNAL_JIFFY) = '1' then
kernal <= KERNAL_JIFFYDOS;
end if;
-- logic that runs in the QNICE clock domain reads the QNICE copy instead:
joyport_flip <= qnice_osm_control_i(C_MENU_FLIP_JOYS);How to find a flat index. Count lines from 0, including blank, headline and
submenu lines — and note that a submenu’s interior lines occupy indices too,
even though they are hidden at the main level. In the §15 slice, " Model: %s"
(a submenu opener) is line 14; the Model submenu’s entire interior then consumes
the next indices, so the following main-menu entry, " HDMI: %s", is line 33,
not 15. Hand-counting around submenu brackets is the easiest place to slip.
Key points:
-
A
C_MENU_*constant is a line index intoOPTM_ITEMS/OPTM_GROUPS(0-based, counting every line). Insert a line and all indices below it shift — so re-check yourC_MENU_*values whenever you edit the menu. - For a radio group you typically define one
C_MENU_*per member and test the bits (or use a small priority chain). For a toggle, one bit is enough. - This is the only place the menu is interpreted positionally. To avoid silent
mistakes the C64 core ships a checker that re-derives every
C_MENU_*from a model of the menu and fails ifconfig.vhdandmega65.vhddisagree:python3 M2M/rom/tests/menu_test.py verify.
A handful of constants control size:
-
OPTM_SIZE— the number of menu lines. Must equal the number of\n-lines inOPTM_ITEMSand the number of entries inOPTM_GROUPS. Maximum 254. -
OPTM_DX— the menu’s inner width in characters. -
OPTM_DY— the menu’s inner height in lines. With submenus, set this to the height of the tallest single view, not the grand total — only one level is on screen at a time. Count one line per item visible at that level, including one line for each submenu label, but excluding the contents of submenus.
The firmware draws a frame around the menu, so the on-screen size is
OPTM_DX + 2 by OPTM_DY + 2. Keep OPTM_DY + 2 within the overlay height or
the menu draws past the frame (the boot checker warns about this on the serial
console).
Set constant SAVE_SETTINGS : boolean := true; and the firmware will persist the
user’s choices to a file on the SD card and reload them next time.
- The file is exactly
OPTM_SIZEbytes — one byte per menu line (0 or 1), matching the state bits. - Its name is derived from the core version, so different versions keep separate settings files and an old file never corrupts a new layout.
- The firmware falls back to the
OPTM_G_STDSELdefaults in three cases: the file is missing, its size does not matchOPTM_SIZE, or its first byte is0xFF— the explicit “use defaults” marker that a freshly generated settings file carries.
The practical consequence for you: whenever you change OPTM_SIZE (add/remove
lines), regenerate the settings file (the M2M tools include a helper for that),
because the old file no longer matches the new line count.
At boot the firmware validates your menu and shows a fatal error screen if something is wrong. Knowing these helps you debug fast:
-
Menu size out of range —
OPTM_SIZEmust be 1…254. -
No start item — exactly one line must carry
OPTM_G_START. -
Start item not visible at the main level —
OPTM_G_STARTmust be on a main-menu line (or a top-level submenu label), not on a line inside a submenu. -
Unbalanced submenus — every
OPTM_G_SUBMENUopener needs a matchingOPTM_G_SUBMENU + OPTM_G_CLOSEcloser; the error reports the offending line index. -
No selectable item inside a
%ssubmenu — a%ssubmenu label needs a radio group of its own to summarise. (Unlike the other checks here, this one is not part of the boot validation pass — it fires the first time that submenu’s%sis drawn.) -
Smart-dependency errors (
OPTM_DEP): a missing/invalid mother group, an out-of-range item index, a group whose members carry inconsistent tags, a dependency chain, orOPTM_DEPon a forbidden line type (§9).
These checks exist precisely because the two parallel arrays are easy to get subtly out of sync.
Here is a slice of the actual C64 main menu, annotated. Compare the text on the left with the meaning on the right and the concepts above should click:
idx OPTM_ITEMS OPTM_GROUPS what it is
0 " C64 for MEGA65\n" OPTM_G_HEADLINE bright title
1 "\n" OPTM_G_LINE separator
2 " 8:%s\n" OPTM_G_MOUNT_8 + OPTM_G_MOUNT_DRV + OPTM_G_START drive 0 mount + cursor
3 " PRG:%s\n" OPTM_G_LOAD_PRG + OPTM_G_LOAD_ROM load a .prg
4 "\n" OPTM_G_LINE
5 " Expansion Port\n" OPTM_G_HEADLINE
6 "\n" OPTM_G_LINE
7 " Use hardware slot\n" OPTM_G_EXP_PORT + OPTM_G_STDSEL radio member (default)
8 " Simulate cartridge:\n" OPTM_G_EXP_PORT radio member
9 " CRT:%s\n" OPTM_G_MOUNT_CRT + OPTM_G_LOAD_ROM load a .crt
10 " Simulate 1750 REU 512KB\n" OPTM_G_REU + OPTM_G_SINGLESEL on/off toggle
...
14 " Model: %s\n" OPTM_G_SUBMENU opens "Model" submenu
...
33 " HDMI: %s\n" OPTM_G_SUBMENU opens "HDMI" submenu
Lines 7 and 8 share group id OPTM_G_EXP_PORT (= 4), so they are one radio group
— “Use hardware slot” vs. “Simulate cartridge”, with the former as default. Line
10 is an independent toggle. Lines 14 and 33 are submenu labels whose %s shows
the current sub-choice. Line 2 both mounts drive 0 and is where the cursor
starts.
The two arrays (must be the same length = OPTM_SIZE):
OPTM_ITEMS = text, one line per \n. OPTM_GROUPS = meaning, one entry per
line, built by +-combining named constants.
Line types:
| Goal |
OPTM_GROUPS entry |
|---|---|
| section title | OPTM_G_HEADLINE |
| separator line |
OPTM_G_LINE (text is "\n") |
| caption / plain text |
OPTM_G_TEXT (= 0) |
| on/off toggle | <id> + OPTM_G_SINGLESEL |
| radio group (≥2 lines) |
<id> on each member; one gets + OPTM_G_STDSEL
|
| default-on | … + OPTM_G_STDSEL |
| initial cursor (exactly one) | … + OPTM_G_START |
| open / close a submenu |
OPTM_G_SUBMENU / OPTM_G_SUBMENU + OPTM_G_CLOSE
|
| mount a drive / load a ROM |
<id> + OPTM_G_MOUNT_DRV / <id> + OPTM_G_LOAD_ROM
|
| help screen | <id> + OPTM_G_HELP |
| close the OSM | OPTM_G_CLOSE |
| conditional visibility | … + OPTM_DEP(mother_group, item) |
Golden rules:
-
OPTM_ITEMSlines,OPTM_GROUPSentries andOPTM_SIZEmust all agree. - Lower-case
\nat the end of every line; lead selectable lines with a space. - Group ids 1…254 are yours; declare them as
constant OPTM_G_… : integer. - Radio group = same id on several lines; toggle = unique id +
OPTM_G_SINGLESEL. - Exactly one
OPTM_G_STDSELper radio group; exactly oneOPTM_G_STARToverall, visible at the main level. - Submenu open/close brackets must balance; keep a group inside one level.
- Re-derive
C_MENU_*in your core’s VHDL after editing the menu, and runmenu_test.py verify. - Stay above the
!!! DO NOT TOUCH !!!banner; never edit the address decoder.
Below the configuration region, config.vhd contains an addr_decode process.
This is framework code — do not edit it. Its job is purely mechanical: when
the QNICE firmware asks for a piece of the configuration (selected by the upper
bits of an address), the decoder returns it 16 bits at a time. For example it
serves OPTM_ITEMS character by character, and serves OPTM_GROUPS through
several “views” — a masked group word plus one-bit-per-line views for flags such
as default-selected, separator and start. The smart-dependency words and the
feature-probe magic value are served the same way. You benefit from all of this
for free by editing only the arrays above; you never call or modify the decoder.