-
Notifications
You must be signed in to change notification settings - Fork 13
Welcome and Help Screens
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, theWHSarray, 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 ofconfig.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.
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
WHSarray (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 theWHSarray 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; theWHSarray 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.
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.
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 !!!(theSEL_WHSselector, 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).
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:
-
The firmware draws no page numbers and no navigation hints. If you want
the user to see
(2 of 3)orPress Space to close, you type that into the page string yourself (see the real C64 help pages in §9). - 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).
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 outerWHSarray needs0 => …) andWHS_MAX_PAGES = 1(so eachpage_start/page_lengthneeds0 => …). 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 resetThat 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
\nand each visible line begins with a space — that is the standard one-character margin inside the frame. -
SCR_WELCOME'lengthis 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 theWHSarray correct.
Note on
WHS_MAX_PAGES. It is not a stylistic choice — it is the fixed width of thepage_start/page_lengtharrays 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).
A page is a plain string, printed character-for-character, with a few special rules the firmware applies as it draws.
A new line is the two characters \ and n — not 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.)
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.
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.
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"".)
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.
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" & ...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.
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_countis how many pages this screen has (here3). -
page_start(i)/page_length(i)describe page i (0-based). - The tuples have
WHS_MAX_PAGESslots. A screen with fewer pages pads the unused slots with0(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_PAGESvs.page_count.WHS_MAX_PAGESis one global maximum (the array width for every record);page_countis the actual number of pages for one record.WHS_MAX_PAGESmust be ≥ the largestpage_countyou use, or the page tuples will not have room and the file will not compile.
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.
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.
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.
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 = truesimply tellsRP_WELCOMEto 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
WHSelement 0 — an ordinaryWHS_RECORD_TYPE— so it may havepage_count > 1and 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.
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_HELPopensWHSelement N. The first help item opensWHS(1), the second opensWHS(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:
- The firmware works out which help item this is (1st, 2nd, …) by counting the
help-tagged lines, and opens the matching
WHSelement. - The user browses the pages and presses Space / Run/Stop.
- 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.
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 |
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.
-
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.";
-
Append them to
WHS_DATA.constant WHS_DATA : string := SCR_WELCOME & HELP_1 & HELP_2 & HELP_3 & HELP_CTRL_1 & HELP_CTRL_2;
-
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;
-
Add a
WHSelement and growWHS_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_PAGESstays 3 — still ≥ our biggest screen.) -
Add a
OPTM_G_HELPmenu line. The 2nd help-tagged line maps toWHS(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, …).
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_PAGEStoo small for your longest screen. Compile error: the page tuples won’t have enough slots. (This one does fail loudly — good.) -
page_countwrong. 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'lengthof 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
WHSelements. If the menu has moreOPTM_G_HELPitems than theWHSarray 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.) KeepWHS_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).
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:
- Line break = lower-case
\n(two chars); blank line =\n\n. - Start each line with a space (the inner margin).
- No word-wrap and no clipping — hard-wrap every line yourself, keep pages short.
- Page numbers / “Press Space …” footers are your text; the firmware draws none.
-
<and>render as arrow glyphs;""for a literal quote. - 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.
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.vhddescribes 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’spage_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 atpage_start(page) + offsetwithinWHS_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.