Skip to content

Welcome and Help Screens

sy2002 edited this page Jun 13, 2026 · 1 revision

How to build the Welcome & Help screens in config.vhd

This guide explains, from the ground up, how the Welcome screen and the Help screens of a MiSTer2MEGA65 (M2M) core are defined. These are the full-screen text pages your users see — the splash screen that can greet them when the core starts, and the help pages they open from the On-Screen Menu. You author all of them in a single VHDL file, config.vhd — no firmware programming required.

Scope of this guide. We focus on defining the screens in config.vhd: the screen text, the WHS array, multi-page screens, the navigation keys, and the two flags that turn the welcome screen on. We deliberately do not cover the On-Screen Menu itself (item types, submenus, %s, smart dependencies — that is the “How to build an On-Screen Menu (OSM)” guide), nor the rest of config.vhd (clocks, resets, ascal, …). The single point where the two topics touch — tagging a menu item as a help item — is covered in §8. Everything here is real and matches the shipped C64 core, which we use as the running example.


1. The mental model: screens, pages, and the WHS array

Three words carry the whole system. Learn them first and everything else falls into place.

  • A screen is one full-screen overlay of text — for example your welcome splash, or the help text behind a menu entry.
  • A screen is made of one or more pages. The user flips through the pages of one screen with Cursor Left / Cursor Right, and closes the screen with Space.
  • The WHS array (Welcome & Help Screens) is the table that lists every screen. Array element 0 is always the Welcome screen. Elements 1, 2, 3 … are the Help screens, in menu order.

The text of every page is just an ordinary VHDL string. The trick that makes it all fit into the FPGA is this:

All your pages are concatenated into one big string (WHS_DATA), and the WHS array only stores, per page, where that page starts in the big string and how long it is. The big string becomes one string-ROM at synthesis; the WHS array is the index into it.

   WHS_DATA  (one big string ROM — every page of every screen, concatenated)
   ┌───────────────┬──────────────┬──────────────┬──────────────┐
   │  SCR_WELCOME  │    HELP_1    │    HELP_2    │    HELP_3    │
   └───────────────┴──────────────┴──────────────┴──────────────┘
   └── element 0 ──┘└──────── the 3 pages of element 1 ─────────┘
      (its 1 page)            (HELP_1/2/3 are ONE Help screen)

   WHS array (the index into WHS_DATA)
   ┌───────────────────────────────────────────────────────────────┐
   │ [0] Welcome : page_count=1, page_start=(0),       length=(…)  │
   │ [1] Help #1 : page_count=3, page_start=(s1,s2,s3),length=(…)  │
   └───────────────────────────────────────────────────────────────┘
     element 0 = the Welcome     elements 1..15 = Help screens,
     screen (always)             one per "Help" menu item, in menu order

Note the trap the picture is built to defuse: HELP_1, HELP_2, HELP_3 are three pages of a single Help screen (element 1), not three separate screens. One screen = one WHS element = one or more pages.

That is the entire data model. The rest of this guide is just how to fill in that table correctly and which flags decide when a screen is shown.

Where the screens live in the bigger picture

   config.vhd  (you edit this)         QNICE "Shell" firmware          the user's screen
  ┌───────────────────────────┐       ┌──────────────────────┐       ┌────────────────────┐
  │ SCR_WELCOME, HELP_1, …    │ reads │ at startup, shows the│ draws │  full-screen       │
  │ (the page texts)          │──────►│ welcome screen;      │──────►│  overlay with a    │
  │ WHS_DATA (concatenated)   │       │ on a "Help" menu pick│       │  frame; user flips │
  │ WHS array (start/length)  │       │ shows the matching   │       │  pages with the    │
  │ WELCOME_ACTIVE / _AT_RESET│       │ help screen; handles │       │  cursor keys and   │
  │ (no firmware code at all) │       │ page-flipping & close│       │  closes with Space │
  └───────────────────────────┘       └──────────────────────┘       └────────────────────┘

You only ever edit config.vhd. The firmware (M2M/rom/whs.asm and friends) is generic and shipped with M2M; it reads your arrays and does all the drawing, page-flipping and key handling for you.

The golden rule about config.vhd

config.vhd has two clearly marked regions in every section:

  • a “START YOUR CONFIGURATION BELOW THIS LINE” region, where you do all your work, and
  • lines marked !!! DO NOT TOUCH !!! (the SEL_WHS selector, the record type declarations, and the address decoder at the bottom — see the Appendix).

Stay in the configuration region. The framework declarations marked !!! DO NOT TOUCH !!! must keep their values, because the firmware depends on them — but you use them (the WHS_RECORD_ARRAY_TYPE, for instance).


2. The runtime experience (what the user actually sees)

Both the welcome screen and every help screen are drawn by the same firmware routine, so they behave identically:

  • The core’s video is overlaid by a full-screen window with a frame around it. (This is the big window, not the small On-Screen Menu box.)
  • The current page’s text is printed inside the frame.
  • Cursor Right shows the next page, Cursor Left the previous one. At the first/last page the respective key simply does nothing.
  • Space or Run/Stop closes the screen and returns the user to where they were (the running core for the welcome screen, the On-Screen Menu for a help screen).

That is the whole interaction model. Two consequences worth internalizing now, because they shape how you write the text:

  1. The firmware draws no page numbers and no navigation hints. If you want the user to see (2 of 3) or Press Space to close, you type that into the page string yourself (see the real C64 help pages in §9).
  2. There is no automatic word-wrap. A line is printed verbatim until you insert a line break. You control the layout completely — and you are responsible for not running off the edge of the frame (§4).

3. Your first screen (a minimal complete example)

Let’s build the smallest useful welcome screen: a one-page splash that greets the user when the core starts. Everything below goes into the Welcome and Help Screens section of config.vhd.

-- 1) How many WHS array elements (screens) are there? (between 1 and 16)
--    Element 0 is the Welcome screen, so a welcome-only setup needs exactly 1.
constant WHS_RECORDS   : natural := 1;

-- 2) What is the largest number of pages any single screen has? (between 1 and 256)
--    Our welcome screen has 1 page, so 1 is enough.
constant WHS_MAX_PAGES : natural := 1;

-- (the SEL_WHS selector and the WHS record types are declared just above this
--  line and are marked "DO NOT TOUCH" — leave them as they are.)

-- 3) Write the screen text as a normal VHDL string. \n is a line break.
constant SCR_WELCOME : string :=
   "\n Welcome to My Core!\n\n" &
   " A MiSTer port powered by MiSTer2MEGA65.\n\n" &
   " Press Space to continue.";

-- 4) Concatenate every screen into one big string (here there is only one).
constant WHS_DATA : string := SCR_WELCOME;

-- 5) Record, per page, where it starts inside WHS_DATA.
constant SCR_WELCOME_START : natural := 0;     -- the first page always starts at 0

-- 6) Fill the WHS array. Element 0 is the Welcome screen.
constant WHS : WHS_RECORD_ARRAY_TYPE := (
   0 => (page_count  => 1,
         page_start  => (0 => SCR_WELCOME_START),  -- one page, starting at 0
         page_length => (0 => SCR_WELCOME'length)) -- its length in characters
);

Why all those 0 => …? This is a VHDL quirk worth meeting once. A single-element array aggregate cannot be written positionally(X) is just a parenthesised expression, not a one-element array — so you must name the index: (0 => X). It bites twice in this minimal example, because both the array and the page tuples have one element here: WHS_RECORDS = 1 (so the outer WHS array needs 0 => …) and WHS_MAX_PAGES = 1 (so each page_start / page_length needs 0 => …). As soon as you have two or more elements you switch to the plain positional form (a, b, c) you will see from §5 onward — it is the same types, only the way of writing the literal differs. (others => works too, e.g. page_length => (others => SCR_WELCOME'length).)

Then, in the General configuration section further down, switch it on:

constant WELCOME_ACTIVE   : boolean := true;    -- show the welcome screen at startup
constant WELCOME_AT_RESET : boolean := false;   -- but not again after every reset

That is a fully working welcome screen. When the core boots, the user sees your text; pressing Space dismisses it and the core runs.

Two things to notice already, both explained in the next section:

  • The string begins with \n and each visible line begins with a space — that is the standard one-character margin inside the frame.
  • SCR_WELCOME'length is VHDL’s built-in “length of this string”. Letting the synthesis tool compute the lengths and start addresses for you (instead of hand-counting bytes) is the single most important habit for keeping the WHS array correct.

Note on WHS_MAX_PAGES. It is not a stylistic choice — it is the fixed width of the page_start / page_length arrays inside each record (Vivado does not allow unconstrained arrays in a record). It must be at least as large as the page count of your longest screen. Set it once to your maximum and pad shorter screens with zeros (§6).


4. Anatomy of a screen string (text format & gotchas)

A page is a plain string, printed character-for-character, with a few special rules the firmware applies as it draws.

Line breaks: lower-case \n

A new line is the two characters \ and nnot a control character, and always lower-case. After a \n, the cursor returns to the inner-left edge of the frame (one character in from the border). A blank line is simply two \n in a row.

"\n Title line\n\n Body starts here, after one empty line.\n"

The backslash is special only when it is immediately followed by n. A backslash anywhere else (a stray \, a \t, a Windows path) is printed literally together with the next character — there is no other escape sequence, and no error. (For completeness: the print routine also treats a raw CR/LF byte pair as a line break, but you cannot conveniently put one inside a VHDL string literal, so \n is the form every shipped core uses.)

The leading space is your left margin

Because text restarts hard against the inner-left border, every real line conventionally starts with a single space, giving a tidy one-character margin. Lines without it touch the frame and look cramped.

There is no word-wrap — you hard-wrap, the firmware does not

The firmware prints until it hits a \n; it will happily draw past the right edge of the frame if your line is too long. You must insert the line breaks yourself. Look at the real C64 help text: every line is manually broken to fit, and continued lines are indented by hand:

" * Create a /c64 folder on your SD card &\n" &
"   place your D64, CRT and PRG files there\n" &

How wide is “too wide”? The screen uses the full-screen overlay window, whose size the firmware derives from the active video mode at boot (around 80 columns on the standard 720-pixel-wide modes). There is no config.vhd constant you can read for it, so the safe approach is empirical: the C64 core’s pages keep lines to roughly 40 characters and screens to a couple of dozen lines, which fits every supported output. If your core offers narrower video modes, test there. When in doubt, keep lines short.

< and > are not literal — they become arrow glyphs

The print routine substitutes < and > with the two directory-arrow characters of the Anikki font (the same glyphs the file browser uses to bracket a name). So if you write <Mount> it renders as arrowMountarrow, not with literal angle brackets. This is occasionally handy and occasionally a surprise — if you genuinely need an ASCII <, you cannot get it through this path. (Plain quotation marks need VHDL’s usual doubling, "", as in the C64’s ""Pseudo-stereo"".)

Page numbers and prompts are your text

As noted in §2, the firmware draws nothing but your string and the frame. The familiar footers are authored by hand. From the real C64 help pages:

" Cursor right to learn more.       (1 of 3)\n" &
" Press Space to close the help screen."

Write whatever guidance fits your screen — (1 of 3), Crsr left: Prev, etc.

Reuse the version constant

config.vhd defines a single CORE_VERSION constant and reuses it everywhere — including in the screen text — so you update the version in exactly one place:

constant CORE_VERSION : string := "WIP-V6-A17";
...
constant SCR_WELCOME : string :=
   "\n Commodore 64 for MEGA65 Version " & CORE_VERSION & "\n\n" & ...

Size limits per page

Each page is a zero-terminated string. The firmware serves a page out of a 4 KB window whose very last slot is reserved for the page count (§ Appendix), so a single page can hold up to 4094 characters (4095 bytes including the terminator). In practice you will hit the screen’s visible size long before this limit; if a topic does not fit, split it into another page, not a longer string.


5. Multi-page screens

A screen with more than one page is the normal case for help. You write each page as its own string constant, concatenate them into WHS_DATA, and list all of them in one WHS array element.

constant WHS_MAX_PAGES : natural := 3;          -- our biggest screen has 3 pages

constant HELP_1 : string := "\n ... page one ...\n Cursor right for more (1 of 3)";
constant HELP_2 : string := "\n ... page two ...\n Crsr left/right (2 of 3)";
constant HELP_3 : string := "\n ... page three ...\n Cursor left to go back (3 of 3)";

constant WHS_DATA : string := HELP_1 & HELP_2 & HELP_3;

Now the crucial part — telling the array where each page starts. Do it with 'length arithmetic so the compiler does the counting:

constant HELP_1_START : natural := 0;                          -- first page at 0
constant HELP_2_START : natural := HELP_1_START + HELP_1'length;
constant HELP_3_START : natural := HELP_2_START + HELP_2'length;

Each page-start is “where the previous page ended”. And the WHS element lists the three starts and the three lengths, in page order:

(page_count  => 3,
 page_start  => (HELP_1_START,  HELP_2_START,  HELP_3_START),
 page_length => (HELP_1'length, HELP_2'length, HELP_3'length))
  • page_count is how many pages this screen has (here 3).
  • page_start(i) / page_length(i) describe page i (0-based).
  • The tuples have WHS_MAX_PAGES slots. A screen with fewer pages pads the unused slots with 0 (see the welcome screen in §6).

At runtime the firmware reads page_count to know how far Cursor Right may go, and uses page_start/page_length to fetch the current page out of WHS_DATA. Browsing wraps nowhere: at page 0 Cursor Left is ignored; at the last page Cursor Right is ignored.

WHS_MAX_PAGES vs. page_count. WHS_MAX_PAGES is one global maximum (the array width for every record); page_count is the actual number of pages for one record. WHS_MAX_PAGES must be ≥ the largest page_count you use, or the page tuples will not have room and the file will not compile.


6. The WHS array in depth

The WHS array is the single source of truth that ties screens to pages. Its shape is fixed by the framework (the !!! DO NOT TOUCH !!! block just above your configuration):

type WHS_INDEX_TYPE is array (0 to WHS_MAX_PAGES - 1) of natural;
type WHS_RECORD_TYPE is record
   page_count  : natural;          -- number of pages in this screen
   page_start  : WHS_INDEX_TYPE;   -- start offset of each page within WHS_DATA
   page_length : WHS_INDEX_TYPE;   -- length of each page
end record;
type WHS_RECORD_ARRAY_TYPE is array (0 to WHS_RECORDS - 1) of WHS_RECORD_TYPE;

You fill in one WHS_RECORD_TYPE per screen. The position in the array is meaningful:

WHS index Meaning
0 The Welcome screen. Always. (Shown per §7.)
1 The screen for the 1st menu item tagged as Help (§8).
2 The screen for the 2nd Help menu item.
up to 15 The 15th Help item (the array index field is 4 bits → 0…15).

So the two sizing constants are bounded as:

  • WHS_RECORDS — number of screens, 1 … 16. (One welcome slot + up to 15 help slots.)
  • WHS_MAX_PAGES — max pages per screen, 1 … 256.

What if you don’t want a welcome screen?

Element 0 is reserved for the welcome screen structurally, even if you never show one. If you only want help screens:

  • set WELCOME_ACTIVE := false (so the firmware never displays element 0), and
  • keep a placeholder element 0 so your first help screen is still at index 1.

A zero placeholder is fine:

constant WHS : WHS_RECORD_ARRAY_TYPE := (
   --- element 0: Welcome (unused — WELCOME_ACTIVE is false), zero placeholder
   (page_count  => 1,
    page_start  => (others => 0),
    page_length => (others => 0)),

   --- element 1: first Help screen
   (page_count  => 3,
    page_start  => (HELP_1_START,  HELP_2_START,  HELP_3_START),
    page_length => (HELP_1'length, HELP_2'length, HELP_3'length))
);

The C64 core does something slightly different and instructive: it keeps a fully authored welcome screen in element 0 but sets WELCOME_ACTIVE := false. The text is compiled into the ROM but simply never shown — handy if you want to re-enable it later by flipping one boolean.

Padding shorter screens

Because every record’s tuples have WHS_MAX_PAGES slots, a screen with fewer pages pads the rest with 0. The C64’s welcome screen has one page while WHS_MAX_PAGES is 3, so:

(page_count  => 1,
 page_start  => (SCR_WELCOME_START,  0, 0),
 page_length => (SCR_WELCOME'length, 0, 0))

The padding values are never read (the firmware stops at page_count), so any value works; 0 is conventional and clear.


7. Turning the Welcome screen on (WELCOME_ACTIVE / WELCOME_AT_RESET)

The welcome screen — and only the welcome screen — is governed by two booleans in the General configuration section of config.vhd:

-- show the welcome screen in general
constant WELCOME_ACTIVE    : boolean := false;

-- shall the welcome screen also be shown after the core is reset?
-- (only relevant if WELCOME_ACTIVE is true)
constant WELCOME_AT_RESET  : boolean := false;

The behavior, exactly as the firmware implements it (RP_WELCOME in gencfg.asm):

  • WELCOME_ACTIVE = false — element 0 is never shown. (Help screens still work — they are governed only by the menu, §8.)
  • WELCOME_ACTIVE = true, WELCOME_AT_RESET = false — the welcome screen is shown once, at the first start after power-on. The firmware sets an internal “already shown” latch so that a later core reset does not bring it back.
  • WELCOME_ACTIVE = true, WELCOME_AT_RESET = true — the latch is bypassed, so the welcome screen appears at startup and again every time the core is reset.

A typical splash-on-every-reset core (e.g. Galaga, Apple-II) uses true / true; a core that greets the user only once per session uses true / false.

Mechanism, for the curious. The “already shown” latch is a single RAM flag (WELCOME_SHOWN) that is zero at cold start and set to one the first time the screen is displayed. WELCOME_AT_RESET = true simply tells RP_WELCOME to skip the check of that flag (the flag is still written, it is just never tested). You do not configure the flag directly; the two booleans are the whole interface.

The welcome screen is shown during shell startup, before the keyboard and joysticks are connected to the core, and the firmware additionally waits ~0.3 s after the screen is dismissed before connecting them (WAIT333MS in shell.asm). Together this ensures the keypress that dismisses the splash is fully released and cannot “leak” into the running core.

The welcome screen can be multi-page too. It is just WHS element 0 — an ordinary WHS_RECORD_TYPE — so it may have page_count > 1 and is paged with Cursor Left / Cursor Right exactly like a help screen. A multi-page splash (e.g. a “what’s new in this version” tour) is perfectly fine; the one-page welcome in the examples above is just the common case.


8. Help screens: linking a menu item to a screen

A help screen is opened from the On-Screen Menu. The link is made entirely on the menu side, with one attribute on a menu line — OPTM_G_HELP. (Defining menu lines is the OSM guide’s subject — see its §10 “Special function lines (mount, load, help)” for how to place and id such a line; here we only need the one tag.)

The rule is positional and simple:

The N-th menu item tagged OPTM_G_HELP opens WHS element N. The first help item opens WHS(1), the second opens WHS(2), and so on — matching the array layout in §6.

In the C64 menu there is exactly one such line:

-- in OPTM_ITEMS (the menu text):
" About & Help\n"
-- in OPTM_GROUPS (the menu meaning), the same line:
OPTM_G_ABOUT_HELP + OPTM_G_HELP        -- 1st help item  ->  WHS(1)

So selecting About & Help shows the help screen stored in WHS(1) — the 3-page screen we built in §5. Add a second help line anywhere in the menu and it automatically maps to WHS(2); a third to WHS(3); up to WHS(15).

What happens when the user selects the line:

  1. The firmware works out which help item this is (1st, 2nd, …) by counting the help-tagged lines, and opens the matching WHS element.
  2. The user browses the pages and presses Space / Run/Stop.
  3. The firmware returns to the On-Screen Menu and clears the item’s selection — a Help line is an action, not a toggle, so it never stays “on”. You do not have to do anything for this; it is automatic.

Mechanically a help line is a single-select item carrying an extra “help” bit, which is why the cursor can land on it and “select” it. The framework intercepts that selection, shows the screen, and un-selects the line again. There is nothing to wire into your core’s VHDL for a help item — unlike ordinary options, a help item produces no persistent state.


9. A guided tour of the real C64 configuration

Putting it all together, here is the actual structure the C64 core ships with (text trimmed for brevity). Read it against §§3–8 and it should all click.

The screen texts (each a string constant; version pulled from CORE_VERSION):

constant SCR_WELCOME : string :=
   "\n Commodore 64 for MEGA65 Version " & CORE_VERSION & "\n\n" &
   " MiSTer port 2026 by MJoergen & sy2002\n" &
   ...
   "\n\n Press Space to continue.";

constant HELP_1 : string := "\n ... Quickstart ...\n Cursor right to learn more.       (1 of 3)\n ...";
constant HELP_2 : string := "\n ... menu & browser keys ...\n Crsr left: Prev  Crsr right: Next (2 of 3)\n ...";
constant HELP_3 : string := "\n ... SID / IEC / saving ...\n Cursor left to go back.           (3 of 3)\n ...";

Concatenation and offsets:

constant WHS_DATA          : string  := SCR_WELCOME & HELP_1 & HELP_2 & HELP_3;

constant SCR_WELCOME_START : natural := 0;
constant HELP_1_START      : natural := SCR_WELCOME'length;
constant HELP_2_START      : natural := HELP_1_START + HELP_1'length;
constant HELP_3_START      : natural := HELP_2_START + HELP_2'length;

The array — element 0 is the (defined-but-not-shown) welcome screen, element 1 is the 3-page help screen:

constant WHS_RECORDS   : natural := 2;
constant WHS_MAX_PAGES : natural := 3;

constant WHS : WHS_RECORD_ARRAY_TYPE := (
   --- Welcome Screen (element 0)
   (page_count  => 1,
    page_start  => (SCR_WELCOME_START,  0, 0),
    page_length => (SCR_WELCOME'length, 0, 0)),

   --- Help pages (element 1)
   (page_count  => 3,
    page_start  => (HELP_1_START,  HELP_2_START,  HELP_3_START),
    page_length => (HELP_1'length, HELP_2'length, HELP_3'length))
);

The activation flags and the menu link:

constant WELCOME_ACTIVE   : boolean := false;          -- welcome authored but off
constant WELCOME_AT_RESET : boolean := false;
...
" About & Help\n"  ->  OPTM_G_ABOUT_HELP + OPTM_G_HELP  -- 1st help item -> WHS(1)

The complete picture, as a table:

WHS idx Screen Pages Shown when …
0 SCR_WELCOME 1 WELCOME_ACTIVE is true (currently off)
1 HELP_1..3 3 user picks About & Help in the menu

10. Recipe: add a new help screen

Adding a help screen touches five spots, all inside the Welcome and Help Screens section of config.vhd plus one menu line. Suppose you want a new 2-page “Controls” help behind a new menu item, in a core that already has the C64’s welcome + one help screen.

  1. Write the page strings.
    constant HELP_CTRL_1 : string := "\n Controls (1 of 2)\n ...\n Cursor right for more.";
    constant HELP_CTRL_2 : string := "\n Controls (2 of 2)\n ...\n Press Space to close.";
  2. Append them to WHS_DATA.
    constant WHS_DATA : string := SCR_WELCOME & HELP_1 & HELP_2 & HELP_3 &
                                  HELP_CTRL_1 & HELP_CTRL_2;
  3. Add their start offsets (continuing the chain).
    constant HELP_CTRL_1_START : natural := HELP_3_START + HELP_3'length;
    constant HELP_CTRL_2_START : natural := HELP_CTRL_1_START + HELP_CTRL_1'length;
  4. Add a WHS element and grow WHS_RECORDS. This becomes element 2 → the 2nd help item.
    constant WHS_RECORDS : natural := 3;                       -- was 2
    -- ... and add as the last element of the WHS array:
    (page_count  => 2,
     page_start  => (HELP_CTRL_1_START,  HELP_CTRL_2_START,  0),
     page_length => (HELP_CTRL_1'length, HELP_CTRL_2'length, 0))
    (WHS_MAX_PAGES stays 3 — still ≥ our biggest screen.)
  5. Add a OPTM_G_HELP menu line. The 2nd help-tagged line maps to WHS(2).
    " Controls\n"  ->  OPTM_G_SOME_NEW_ID + OPTM_G_HELP

Note the ordering rule in step 5: it is the order of help items in the menu that decides which WHS element each one opens, so keep the menu order and the WHS element order in agreement (1st help line ↔ element 1, 2nd ↔ element 2, …).


11. What can go wrong (there is no boot-time validation!)

This is the most important section to read before you ship. Unlike the On-Screen Menu — which the firmware validates at boot and rejects with a fatal-error screen — the Welcome & Help system is not validated. The firmware trusts your arrays and simply draws whatever they point at. Mistakes therefore surface as silent visual glitches at runtime, not as build errors or clear messages. The usual suspects:

  • WHS_MAX_PAGES too small for your longest screen. Compile error: the page tuples won’t have enough slots. (This one does fail loudly — good.)
  • page_count wrong. Too low and the last pages are unreachable; too high and Cursor Right walks into the page after yours — usually the start of the next screen, shown as garbage.
  • Wrong start/length arithmetic — e.g. forgetting to add a previous page’s 'length, or pointing a page at the wrong offset. The page shows the wrong text or runs together with its neighbor. Always build offsets from 'length of the preceding strings; never hand-count characters.
  • Forgetting to add a new screen to WHS_DATA (or adding it in a different order than the offsets assume). The offsets then index the wrong bytes.
  • Mismatch between help items and WHS elements. If the menu has more OPTM_G_HELP items than the WHS array has elements, selecting a help item with no matching element makes the decoder return a sentinel value (0xEEEE) for both the page count and the page text — so the user scrolls into an absurd page count of garbage. (The decoder guards the array index, so this is a sentinel, not an out-of-bounds read — but the screen is still broken.) Keep WHS_RECORDS ≥ (1 welcome + number of help items).
  • Element 0 not where you want it. Element 0 is always the welcome slot. If you forget the placeholder, your “first” help screen ends up at index 0 and is treated as the welcome screen (and your real welcome, if any, is gone).
  • A page longer than 4094 characters. The text is truncated/garbled because the page’s last window slot is the page count, not text. Split into more pages.
  • Lines too wide / screens too tall. No wrap, no clipping warning — the text just overruns the frame. Hard-wrap and keep pages short.
  • Expecting literal < > — they render as arrow glyphs (§4).

A good habit: after editing the screens, load the core and actually open every help screen and flip every page, watching for a stray garbage page at the end (the classic page_count-too-high symptom).


12. Cheat sheet

The data model: every page’s text lives in one big string WHS_DATA; the WHS array stores, per page, start offset and length into that string. Element 0 = Welcome; elements 1…15 = Help, in menu order.

The constants you set:

Constant What it is Range
WHS_RECORDS number of screens (welcome + help) 1…16
WHS_MAX_PAGES max pages of any one screen (array width) 1…256
WHS_DATA all page strings concatenated
WHS the index: page_count, page_start, page_length
WELCOME_ACTIVE show the welcome screen at all bool
WELCOME_AT_RESET also re-show it after a reset bool

Per page, in the WHS element: page_count = number of pages; page_start(i) / page_length(i) = offset & length of page i; pad unused slots to WHS_MAX_PAGES with 0.

Text rules:

  1. Line break = lower-case \n (two chars); blank line = \n\n.
  2. Start each line with a space (the inner margin).
  3. No word-wrap and no clipping — hard-wrap every line yourself, keep pages short.
  4. Page numbers / “Press Space …” footers are your text; the firmware draws none.
  5. < and > render as arrow glyphs; "" for a literal quote.
  6. Build offsets with 'length, never by counting bytes.

Navigation (firmware-provided, identical for all screens): Cursor Left/Right = prev/next page; Space or Run/Stop = close.

The menu link: tag the N-th menu line with OPTM_G_HELP → it opens WHS(N). Help items auto-deselect; no core wiring needed.

Golden rules: element 0 is always the welcome slot (use a zero placeholder if you have no welcome); WHS_MAX_PAGES ≥ your largest page_count; WHS_RECORDS ≥ 1 + number of help items; nothing is validated at boot — test by opening every screen.


Appendix: the “DO NOT TOUCH” address decoder

Below the configuration region, config.vhd contains an address decoder that serves the screens to the firmware. This is framework code — do not edit it. You benefit from it for free by filling in only the arrays above. For the curious, here is how it works.

The screens are reached through the configuration “device” using a 28-bit address whose upper nibble selects the Welcome & Help system (SEL_WHS = x"1000", i.e. address bits 27…24 = 0x1). The remaining bits split into three fields:

Address bits Field Meaning
23 … 20 whs_array_index which WHS element (screen), 0…15
19 … 12 whs_page_index which page within that screen, 0…255
11 … 0 byte offset which character within the page, 0…4095

Don’t be confused by the source comment. The inline comment in config.vhd describes the same addressing with different bit numbers (“bits 11 downto 8 select the WHS array position … bits 7 downto 0 are selecting the page”). That comment counts bits of the 16-bit 4 KB-window selector value (0x1000 + screen·0x100 + page), which is a pre-shifted coordinate space; the table above counts bits of the full 28-bit address the decoder actually sees. Both are consistent (the selector occupies address bits 27…12). The table above is the authoritative full-address view — do not “correct” it to match the comment.

For a given (screen, page, offset) the decoder returns:

  • offset 0xFFF (4095) → the screen’s page_count (so the firmware can learn how many pages a screen has by reading the very last slot of the window). This is why a page can hold at most 4094 characters plus a terminator.
  • offset < page_length → the character at page_start(page) + offset within WHS_DATA.
  • offset ≥ page_length (but not 4095) → 0, the zero terminator — which is how the firmware knows where a page ends.

The firmware side mirrors this exactly (whs.asm): it points the configuration 4 KB window at 0x1000 + screen·0x100 + page, reads the characters out of the data port, and reads the page count from the window’s last slot. (That last-slot read uses the constant M2M$WHS_PAGES = 0x7FFF, not 0x0FFF, because the QNICE data-port window is based at 0x7000; 0x7000 + 0xFFF lands on the very same offset-0xFFF slot the decoder serves the page count from.) The constants that encode this addressing (M2M$CFG_WHS, M2M$WHS_WELCOME, M2M$WHS_HELP_INDEX, M2M$WHS_HELP_NEXT, M2M$WHS_PAGE_NEXT, M2M$WHS_PAGES) live in M2M/rom/sysdef.asm — but you never need them to author screens. Edit the arrays above; the decoder does the rest.

Clone this wiki locally