Skip to content

3. "Hello World" Tutorial

sy2002 edited this page Dec 30, 2022 · 32 revisions

This tutorial is actually a bit more than "just" the classical "Hello World". It contains six small sub-tutorials to get you started:

  1. Classical "Hello World" using the Welcome-Screen
  2. Add a new menu-item and show an "About & Help" menu
  3. Add two menu-items: Flip the joystick ports and mute the siren
  4. Use multi-select menu items to change the color of the "ball"
  5. Filter files: Only show *.txt files in the file browser
  6. Understanding the QNICE debug console

[@lydon: Here we could advertize your YouTube video]

Please make sure that you have completed the First Steps before proceeding with the tutorials shown here. Use the MyFirstM2M clone you made there for the following exercises.

"Hello World!" using the Welcome-Screen

By default, the Welcome-Screen is shown after power-on (and/or loading the core, respectively) and after each reset. You can configure this behavior in the file CORE/vhdl/config.vhd:

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

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

For now, do not change anything here, yet, because we want to use the Welcome-Screen to show our "Hello World" message.

Search for the constant SCR_WELCOME in config.vhd. You will notice that this is quite a long, multi-line string constant. You will recognize the contents of the Welcome-Screen that the demo-core is showing. Please be aware that you can and need to use \n to add newlines so that you can format your welcome screen.

Go to the lines that contains this segment:

   "\n\nEdit config.vhd to modify welcome screen.\n\n" &
   "You can for example show the keyboard map.\n" &
   "Look at this example from Game Boy Color:\n\n\n" &

Replace the segment with your personal "Hello World!" message for example:

    "\n\nHello World! Hello M2M! Hello MEGA65!\n\n\n\n" &

Generate a bitstream and run the core.

After that, you might want to set the above-mentioned flags to false so that the Welcome-Screen disappears completely: It will neither be shown upon the initial startup of the core then nor will it be shown after a reset.

New menu-item to show "About & Help"

This mini-tutorial catches two birds with one stone:

  1. You'll get a first sense for how the on-screen-menu works
  2. You'll understand the help-system

M2M contains a help-system that allows you to have up to 15 help topics, where each help topic can have 256 screens. Let's add a menu item that shows some demo help content.

Search for the constant OPTM_SIZE in config.vhd and change its value from 27 to 29.

Add this to the string constant OPTM_ITEMS before the "Close Menu" item:

" About & Help\n"        &
"\n"                     &

The string constant should now look like this:

constant OPTM_ITEMS        : string :=

   " Demo Headline A\n"     &
   "\n"                     & 
   " Item A.1\n"            &
   " Item A.2\n"            &
   " Item A.3\n"            &
   " Item A.4\n"            &
   "\n"                     &
   " HDMI Mode\n"           &
   "\n"                     &
   " 720p 50 Hz 16:9\n"     &
   " 720p 60 Hz 16:9\n"     &
   " 576p 50 Hz 4:3\n"      &
   " 576p 50 Hz 5:4\n"      &
   "\n"                     &
   " Drives\n"              &
   "\n"                     &
   " Drive X:%s\n"          &
   " Drive Y:%s\n"          &
   " Drive Z:%s\n"          &
   "\n"                     &
   " Another Headline\n"    &
   "\n"                     &
   " HDMI: CRT emulation\n" &
   " HDMI: Zoom-in\n"       &
   " Audio improvements\n"  &
   "\n"                     &
   " About & Help\n"        &
   "\n"                     &
   " Close Menu\n";

Now go to this block of constants:

constant OPTM_G_Demo_A     : integer := 1;
constant OPTM_G_HDMI       : integer := 2;
constant OPTM_G_Drive_X    : integer := 3;
constant OPTM_G_Drive_Y    : integer := 4;
constant OPTM_G_Drive_Z    : integer := 5;
constant OPTM_G_CRT        : integer := 6;
constant OPTM_G_Zoom       : integer := 7;
constant OPTM_G_Audio      : integer := 8;

And add one more:

constant OPTM_G_AboutHelp  : integer := 9;

After that, locate the constant OPTM_GROUPS and add two more line items right before this block:

     OPTM_G_LINE,                              -- Line
     OPTM_G_CLOSE                              -- Close Menu
   );

The line items that you should add are these:

     OPTM_G_LINE,                              -- Line that separates the Help menu
     OPTM_G_AboutHelp + OPTM_G_HELP,           -- Show help topic #1

As a result, OPTM_GROUPS will look like this:

constant OPTM_GROUPS       : OPTM_GTYPE := ( OPTM_G_TEXT + OPTM_G_HEADLINE,            -- Headline "Demo Headline"
                                             OPTM_G_LINE,                              -- Line
                                             OPTM_G_Demo_A + OPTM_G_START,             -- Item A.1, cursor start position
                                             OPTM_G_Demo_A + OPTM_G_STDSEL,            -- Item A.2, selected by default
                                             OPTM_G_Demo_A,                            -- Item A.3
                                             OPTM_G_Demo_A,                            -- Item A.4
                                             OPTM_G_LINE,                              -- Line
                                             OPTM_G_TEXT,                              -- Headline "HDMI Mode"
                                             OPTM_G_LINE,                              -- Line
                                             OPTM_G_HDMI + OPTM_G_STDSEL,              -- 720p 50 Hz 16:9, selected by default
                                             OPTM_G_HDMI,                              -- 720p 60 Hz 16:9
                                             OPTM_G_HDMI,                              -- 576p 50 Hz 4:3
                                             OPTM_G_HDMI,                              -- 576p 50 Hz 5:4
                                             OPTM_G_LINE,                              -- Line
                                             OPTM_G_TEXT,                              -- Headline "Drives"
                                             OPTM_G_LINE,                              -- Line
                                             OPTM_G_Drive_X + OPTM_G_MOUNT_DRV,        -- Drive X
                                             OPTM_G_Drive_Y + OPTM_G_MOUNT_DRV,        -- Drive Y
                                             OPTM_G_Drive_Z + OPTM_G_MOUNT_DRV,        -- Drive Z
                                             OPTM_G_LINE,                              -- Line
                                             OPTM_G_TEXT,                              -- Headline "Another Headline"
                                             OPTM_G_LINE,                              -- Line
                                             OPTM_G_CRT     + OPTM_G_SINGLESEL,        -- On/Off toggle ("Single Select")
                                             OPTM_G_Zoom    + OPTM_G_SINGLESEL,        -- On/Off toggle ("Single Select")
                                             OPTM_G_Audio   + OPTM_G_SINGLESEL,        -- On/Off toggle ("Single Select")
                                             OPTM_G_LINE,                              -- Line that separates the Help menu
                                             OPTM_G_AboutHelp + OPTM_G_HELP,           -- Show help topic #1
                                             OPTM_G_LINE,                              -- Line
                                             OPTM_G_CLOSE                              -- Close Menu
                                           );

Please note that the items in OPTM_GROUPS are having a 1-to-1 relationship to each line (separated by \n) in OPTM_ITEMS. Also please note that OPTM_ITEMS now has 27 lines and OPTM_GROUPS has 27 array elements, just as specified by OPTM_SIZE.

That's it! You now have an "About & Help" menu item that opens demo content that is three screens long.

Try it: Generate a bitstream and play with it. You can open the "About & Help" menu using Return and you can browse through the multiple help pages using Cursor Left and Cursor Right. Press Space to close the help menu.

How it works

You will find a more in-depth description of how the Shell's menu system works here @TODO and all details about the help-system here @TODO. Nevertheless, here is the condensed overview:

  • OPTM_ITEMS defines the names of the menu items and OPTM_GROUPS defines the properties and behaviour of the menu items. There is a 1-to-1 relationship between both: A line in OPTM_ITEMS corresponds to an array element in OPTM_GROUPS.
  • Empty lines in OPTM_ITEMS will be shown as lines, when the corresponding entry in OPTM_GROUPS is set to OPTM_G_LINE.
  • Each menu item that is supposed to "do something" needs to be part of a "Menu Group". This is why you defined the constant OPTM_G_AboutHelp while following the recipe above.
  • Multi-select menu items need all to be in the same Menu Group. See how all the "Item A.*" menu items are part of OPTM_G_Demo_A.
  • Single-select menu items - such as the help menu item - need to have a unique identifier ("Group"). This is why you gave your constant OPTM_G_AboutHelp the unique value 9.
  • Attributes and properties can be added to Menu Groups using the + operator because the values are arranged in a way that the Shell can recognize single bits. This is why you added the OPTM_G_HELP attribute to the OPTM_G_AboutHelp menu item identifier.
  • The help system counts: The first menu item that has a OPTM_G_HELP attribute will show the first help topic. The second menu item that has a OPTM_G_HELP attribute will show the second help topic, and so on. The various help menu items do not need to be in proximity.
  • The example config.vhd file provided with M2M contains a three-page (three-screen) demo help topic. This tutorial does not explain more details how the help-system itself works, but you can look at the constants WHS_DATA and WHS to get a first overview or go to the reference page @TODO to learn more.

Add two menu-items: Flip the joystick ports and mute the siren

Adding the menu-items to config.vhd

Now we are ready to add two menu items that change the actual state of the core: The first one will flip the joystick ports. The demo core's "BreakOut paddle" can not only be controlled by the cursor keys Left and Right but they can be also controlled by a joystick that sits in port #1. If you did not try this, yet, please do right now. After that, connect your joystick to port #2 and see how it is not working. We will fix that with our new menu-item.

The second menu item that we will add will be used to toggle the siren. By default it will be on so that the siren will be on. The flip joystick ports menu item will be off by default; so we will also learn how to work with default values.

If you followed the mini tutorial above, then you already know the trick of how to add menu items to OPTM_ITEMS and OPTM_GROUPS. Let's get started:

Here are two new constants for our list of Menu Group constants:

constant OPTM_G_FlipJoys   : integer := 10;
constant OPTM_G_Siren      : integer := 11;

Before we add the two menu items for the joystick flipping and for controlling the siren, we are removing the "Demo Headline" to ensure that also on a VGA screen we have enough vertical space to show the whole menu. Please double-check that the first 6 lines of OPTM_ITEMS look like this:

constant OPTM_ITEMS        : string :=

   " Flip Joysticks\n"      &
   " Siren\n"               &
   "\n"                     &
   " Item A.1\n"            &
   " Item A.2\n"            &
   " Item A.3\n"            &

Before we proceed, let's count how many lines we added: We removed one line ("Demo Headline") and added two new lines for joystick flipping and the siren. So that is a net +1 for OPTM_SIZE:

constant OPTM_SIZE         : natural := 28;  -- amount of items including empty lines:

Last-but-not least, we need to define the semantics. Make sure that the first 6 lines of OPTM_GROUPS look like this (and make sure that you remove the + OPTM_G_HEADLINE after the very first OPTM_G_TEXT just as shown here):

constant OPTM_GROUPS       : OPTM_GTYPE := ( OPTM_G_FlipJoys + OPTM_G_SINGLESEL,       -- Single select item: Flip joysticks; not selected by default
                                             OPTM_G_Siren + OPTM_G_SINGLESEL + OPTM_G_STDSEL, -- Single select item: Siren; selected by default
                                             OPTM_G_LINE,                              -- Line
                                             OPTM_G_Demo_A + OPTM_G_START,             -- Item A.1, cursor start position
                                             OPTM_G_Demo_A + OPTM_G_STDSEL,            -- Item A.2, selected by default
                                             OPTM_G_Demo_A,                            -- Item A.3

A single-select menu item is being created by using OPTM_G_SINGLESEL, and adding OPTM_G_STDSEL makes sure that a menu item is on by default.

Synthesize the core, run it and check - using the Help key - that the menu is showing the expected menu items. Please note, that the headline "Demo Headline" is no longer shown in yellow (as we removed + OPTM_G_HEADLINE) while "My Switches" is indeed shown in yellow. As soon as this is successful, please continue with the next steps.

Applying the user's on-screen-menu choices to the core in mega65.vhd

The user's menu choices are available in real-time in mega65.vhd via the register qnice_osm_control_i, which is an input port to mega65.vhd:

-- On-Screen-Menu selections
qnice_osm_control_i     : in std_logic_vector(255 downto 0);

The prefix qnice_ means, that this signal / register is in the "Clock Domain" of the QNICE System-on-a-Chip. At this moment you neither need to understand what a "Clock Domain" (or what Clock Domain Crossing (CDC)) actually is, nor do you need to fully understand how QNICE works. The only important thing for now to know right now is:

  1. Always connect signals of the same clock domain with each other.
  2. Do not connect signals of different clock domains with each other.

In our very example this means: It is no problem to wire qnice_osm_control_i to the flip-joystick-port signal called qnice_flip_joyports_o. But it would be a problem, if you propagated qnice_osm_control_i into for example main.vhd that runs in the core's clock domain, and used it there. (Actually it is not difficult to do that, if you work out the Clock Domain Crossing (CDC), but for now, just ignore this and continue with the tutorial's track.)

Go back to the file config.vhd and count (starting from zero) where the two menu items we added are located. You need to count each line of OPTM_ITEMS including text-only items and empty lines. After that, create two constants in mega65.vhd that contain the position. Don't forget to adjust the positions of the other, already existing demo-core menu items: They changed when you added your new menu items. When you are done, the section -- Democore menu items in mega65.vhd should look like this:

-- Democore menu items
constant C_MENU_FLIPJOYS      : natural := 0;
constant C_MENU_SIREN         : natural := 1;
constant C_MENU_HDMI_16_9_50  : natural := 10;
constant C_MENU_HDMI_16_9_60  : natural := 11;
constant C_MENU_HDMI_4_3_50   : natural := 12;
constant C_MENU_HDMI_5_4_50   : natural := 13;
constant C_MENU_CRT_EMULATION : natural := 23;
constant C_MENU_HDMI_ZOOM     : natural := 24;
constant C_MENU_IMPROVE_AUDIO : natural := 25;

Now let's make sure that M2M is informed about the user's intention to flip the joystick ports; then M2M will do the rest for you. The qnice_osm_control_i contains this information at position C_MENU_FLIPJOYS so all we need to do is is to change this code here

-- Flip joystick ports (i.e. the joystick in port 2 is used as joystick 1 and vice versa)
qnice_flip_joyports_o      <= '0';

into this code here:

-- Flip joystick ports (i.e. the joystick in port 2 is used as joystick 1 and vice versa)
qnice_flip_joyports_o      <= qnice_osm_control_i(C_MENU_FLIPJOYS);

Next, let's make sure that the siren can be muted. Find the line that assigns a zero to qnice_audio_mute_o and change it to this code:

qnice_audio_mute_o         <= not qnice_osm_control_i(C_MENU_SIREN);

We are using not because the semantics is: If the Siren menu item is on then the siren should be audible.

Done. Pretty elegant, isn't it? Make a bitstream, run it and test it:

  1. The siren is on. Press Space and play a bit. Your joystick should be in port #2, so you can move the paddle using the cursor keys but not using the joystick.

  2. Open the menu by pressing Help and unselect "Siren": The siren should be immediately muted.

  3. Choose "Flip Joysticks" and note, how the menu item is visually selected in the menu. If you move your joystick now, it will not yet work. The reason is: The menu is still open and JOY_1_AT_OSD in config.vhd is set to false which means: While the menu is open, the joystick is disconnected from the core.

  4. Close the menu by pressing Help. Move your joystick to the left and to the right and note how the joystick moves the paddle.

Use multi-select menu items to change the color of the "ball"

The standard color of the ball is orange. Open main.vhd and search for this line of code inside the instantiation of the entity work.democore:

ball_col_rgb_i       => x"EE4020",                          -- ball color (RGB): orange         

What you are seeing here is, that the demo core actually allows you to specify the color of the ball in the RGB color format. RGB stands for red, green and blue. If you enter rgb to hex in Google, then Google will present you a small converter app. Copy the value shown above, i.e. EE4020 into the "HEX" field of Google's app and you will immediately see the shade of orange that is the default color of the ball.

You can now play around with the app's controls to find colors you like and then copy the hexadecimal value. For the sake of simplicity, we are just using red, green and blue, plus the original orange.

In main.vhd, between the keywords architecture and begin, add these constants that either use your own colors - in this case you might want to rename them - or just proceed with orange, red, green, and blue:

-- Color constants for the ball
constant BALLCOL_ORANGE    : std_logic_vector(23 downto 0) := x"EE4020";
constant BALLCOL_RED       : std_logic_vector(23 downto 0) := x"FF0000";
constant BALLCOL_GREEN     : std_logic_vector(23 downto 0) := x"00FF00";
constant BALLCOL_BLUE      : std_logic_vector(23 downto 0) := x"0000FF";

The next step is something that you will very likely do more often than not when porting MiSTer cores: You will add some signals to the interface of the entity main. In some cases this might be due to additional clocks that the core needs. In other cases you might need to add RAM or ROM signals where these entities are managed inside mega65.vhd due to Clock Domain Crossing topics. There might be control signals that you want to share. And so on. The bottom-line is, that it is pretty normal and part of M2M's architecture that you as a user of the framework add signals so that entity main can communicate with entity MEGA65_Core (in mega65.vhd) and vice versa.

Remember: entity main is meant to be the place where the "MiSTer core can peacefully live in its own little bubble and clocking domain and whatnot without ever knowing anything about the MEGA65". This quote is from the First Steps chapter of this Wiki; you might want to review the section main.vhd: i_main.

In this step of our tutorial, our goal is to let the user choose the color of the ball using the on-screen-menu. So our entity main needs to become aware of what the user selected. One way of doing this is to provide entity main with the vector that contains the on-screen-menu (OSM) selections. Let's do that by adding these two lines after the pause_i input port:

-- On-screen-menu selection
osm_i                   : in  std_logic_vector(255 downto 0);

The first 12 lines of entity main's definition should now look like this:

entity main is
   generic (
      G_VDNUM                 : natural                     -- amount of virtual drives     
   );
   port (
      clk_main_i              : in  std_logic;
      reset_soft_i            : in  std_logic;
      reset_hard_i            : in  std_logic;
      pause_i                 : in  std_logic;
      
      -- On-screen-menu selection
      osm_i                   : in  std_logic_vector(255 downto 0);

Now we need to ensure that the entity MEGA65_Core (in mega65.vhd) is actually sharing the OSM information with entity main. If you are using the Vivado IDE, then you will already see a visual indicator of an error when opening mega65.vhd and scrolling down to the module instantiation i_main : entity work.main:

In other words: Vivado rightfully complains that we are not providing a value for a mandatory input. We can correct this by passing the OSM status vector to osm_i. As already explained above: Always connect signals of the same clock domain with each other. Since the demo core (and all MiSTer cores) are running in the main clock domain, we must not use the OSM vector qnice_osm_control_i that we used above. The qnice_osm_control_i vector is consisting of Flip-flops that are updated by the QNICE clock. Instead, we need to use main_osm_control_i.

Add the following two lines of code in mega65.vhd after the pause_i port mapping:

-- On-screen-menu selection
osm_i                => main_osm_control_i,

The first 12 lines of the instantiation of work.main should now look like this:

i_main : entity work.main
  generic map (
     G_VDNUM              => C_VDNUM
  )
  port map (
     clk_main_i           => main_clk,
     reset_soft_i         => main_reset_core_i,
     reset_hard_i         => main_reset_m2m_i,
     pause_i              => main_pause_core_i,
     
     -- On-screen-menu selection
     osm_i                => main_osm_control_i,

We now need some menu items that the user of our demo core can select to change the ball's color. So let's open config.vhd and re-use the multi-select menu that is already there: Change the menu items "Item A.1" to "Item A.4" into "Orange", "Red", "Green" and "Blue". The first 7 lines of the definition of OPTM_ITEMS should now look like this:

constant OPTM_ITEMS        : string :=

   " Flip Joysticks\n"      &
   " Siren\n"               &
   "\n"                     &
   " Orange\n"              &
   " Red\n"                 &
   " Green\n"               &
   " Blue\n"                &

As you already learned above, the semantics - i.e. the meaning - of the menu items are defined in OPTM_GROUPS. Let's review the entries of OPTM_GROUPS that are associated to "Orange", "Red", "Green" and "Blue":

OPTM_G_Demo_A + OPTM_G_START,             -- Item A.1, cursor start position
OPTM_G_Demo_A + OPTM_G_STDSEL,            -- Item A.2, selected by default
OPTM_G_Demo_A,                            -- Item A.3
OPTM_G_Demo_A,                            -- Item A.4

We can see, that the "menu group" that represents our color selection is called OPTM_G_Demo_A. If the framework encounters a group of menu items that share the same "menu group" and if there is no other attribute that changes the semantics, then the framework automatically generates a multi-select menu section, in our case "Orange", "Red", "Green" and "Blue". The name of the constant that we use to denote that menu group, in our case it is OPTM_G_Demo_A, does not matter. But it is a best practice to use meaningful names. Go to the definition of our menu group constants and change the first constant's name to OPTM_G_BallCol, so that the first three lines of the menu group constant definition now look like this:

constant OPTM_G_BallCol    : integer := 1;
constant OPTM_G_HDMI       : integer := 2;
constant OPTM_G_Drive_X    : integer := 3;

Next, change the OPTM_GROUPS array and the comments like this:

OPTM_G_BallCol + OPTM_G_START,            -- Organge, menu's cursor start position
OPTM_G_BallCol + OPTM_G_STDSEL,           -- Red, selected by default
OPTM_G_BallCol,                           -- Green
OPTM_G_BallCol,                           -- Blue

The meaning of the OPTM_G_START start flag is: This is the menu item, where the selection cursor should be placed when opening the menu by pressing the Help key. In our example this means: The selection cursor will be placed over "Orange". But orange will not be the default color that the ball will have when you start the core. Instead, "Red" will be the default: Within a group of menu items that form a multi-select group, the flag OPTM_G_STDSEL defines the very item that is selected by default. In our case we chose "Red" just to demonstrate the fact, that OPTM_G_START (cursor default position) and OPTM_G_STDSEL (standard selection) do not necessarily need to be united at the same menu item.

As a final step, we need to add some logic that translates menu selections into a color input for the demo core. Open main.vhd and add constants that represent the positions of the menu items for "Red", "Green" and "Blue". We will not need the position for "Orange" because we will utilize the orange color as our internal default value (independent from the settings above). You might recall: You need to count the position of the menu line item starting from zero in OPTM_ITEMS in config.vhd. Doing so yields the following positions, given that you followed the whole tutorial from the beginning:

-- Position constants for osm_i
constant C_MENU_RED        : natural := 4;
constant C_MENU_GREEN      : natural := 5;
constant C_MENU_BLUE       : natural := 6;

Make sure that you are adding these constant definitions in main.vhd (not in mega65.vhd). A good place to put them is right below the color constants.

Next, we need a signal that represents the selected color. Add the following two lines below the position constants that you just added in main.vhd:

-- Signal that contains currently used color constant
signal   ball_color        : std_logic_vector(23 downto 0);

Now we change the hard coded color value EE4020 to the dynamically evaluated color value ball_color by changing the input of the demo core's port ball_col_rgb_i that you have seen at the very beginning of this section like this:

ball_col_rgb_i       => ball_color,                         -- ball color chosen via OSM         

What is still missing is the actual logic that translates the OSM selection into a color value. Add the following process below the begin keyword that follows the architecture keyword but before the instantiation of the demo core i_democore : entity work.democore:

select_ball_color : process(all)
begin
  ball_color <= BALLCOL_ORANGE;    -- default value; make sure that no latch is synthesized
  
  if osm_i(C_MENU_RED) then
     ball_color <= BALLCOL_RED;
  elsif osm_i(C_MENU_GREEN) then
     ball_color <= BALLCOL_GREEN;
  elsif osm_i(C_MENU_BLUE) then
     ball_color <= BALLCOL_BLUE;
  end if;
end process;

Most of this code is straightforward: We have a purely combinatorial process. If a certain bit is set in the OSM vector then assign the appropriate color constant to the color signal. A question might arise around the first assignment:

ball_color <= BALLCOL_ORANGE;    -- default value; make sure that no latch is synthesized

The reason why we are using such a default value (which could also have been wrapped inside a final else clause) is: osm_i has many more states than just the few states we are interested in here. If we would not specify any else or default value in the process then a latch would be synthesized to hold the value of ball_color steady in all the cases that are not "mentioned" in the process. This is not what we want. Instead, what we want here is that ball_color is a purely combinatorial signal. And this is also why we did not need any position constant for "Orange" because "Orange" is the default case of the process.

Now it is time to synthesize and run the core. You will notice that:

  • Below the "Siren" menu item and the line there are four menu items called "Orange", "Red", "Green" and "Blue".
  • The menu selection cursor when you press Help is located over "Orange", that means if you wanted to use the "Flip Joysticks" menu item, you would need to press Cursor Up two times. Reason: We have chosen "Orange" as the menu item, that is selected by default when opening the menu for the first time using the flag OPTM_G_START.
  • The color of the ball is red from the start. Reason: We have chosen red as the default color using the flag OPTM_G_STDSEL.
  • You are able to switch the ball's color in real-time by choosing the different menu item and then pressing Return. This is expected because most things inside an FPGA happen in parallel to each other. The demo core is not aware that a QNICE core is running which shows any menu. The demo core is just aware of the input port that specifies the ball color.

Filter files: Only show *.txt files in the file browser

When you use any of the three OSM items "Drive : " (=X, Y or Z), then M2M's file- and directory browser is shown and you can use the cursor keys and Return to "mount" a drive. Actually, the demo core does not support any drives, so this functionality is just there to show you how the file- and directory browser works (learn more about it here).

Let's suppose we only wanted to show *.txt files in the file browser. To tell the framework that this is what we want, we need to edit a so called "callback function" inside the file m2m-rom.asm. You find this file in the folder CORE/m2m-rom.

Before we do that, let's make sure we have a bunch of *.txt files on our SD card. It is recommended that you use the external SD card (at the back of the MEGA65) for this experiment, because the external SD card takes precedence over the internal SD card (located in the bottom tray). This means that the M2M file- and directory browser will always start at the external SD card, if it is inserted. Learn more about how to switch between the internal and external SD card here. Copy at least one *.txt file on the SD card. Long file names and directories are supported, so you can experiment a bit by copying several files with long and short file names to different locations/folders. After you prepared your SD card like this, insert it to the external slot and run the core to test if you can see your *.txt files together with all the other files that might be on your SD card.

Now let's extend m2m-rom.asm to filter for *.txt files. Locate the label FILTER_FILES. This is a "callback function", i.e. the M2M framework will call this function to learn more about valid file- and directory names. In general this is not only about filtering unwanted file-extensions. You could also filter unwanted files and also directories. So our tutorial use case is a very simple example.

The existing callback function looks like this:

FILTER_FILES    XOR     R8, R8                  ; R8 = 0 = do not filter file
                RET

The comment right above the FILTER_FILES label, gives you some context how the callback functions works: The callback function is called for each and every file- and directory name that M2M discovers in the currently active folder, which means that the callback function is supposed to decide for each and every file and folder if it should be shown to the user or not.

M2M provides a pointer to a zero-terminated string in register R8 which is the file- or folder name. And there is a flag in R9 that states if the currently provided string in R8 is the name of a file (then R9 is zero) or a folder (then R9 is one). If you return the value zero in the QNICE register R8, then the file is shown (i.e. "not filtered"). Any other value in R8 means the file will be filtered.

Now replace the two assembly lines shown above by the following code segment:

FILTER_FILES    INCRB
                MOVE    R9, R0
                
                CMP     1, R9                   ; do not filter directories
                RBRA    _FFILES_RET_0, Z

                ; does this file have the ".TXT" file extension?
                MOVE    TXT_EXTENSION, R9
                RSUB    M2M$CHK_EXT, 1
                RBRA    _FFILES_RET_0, C        ; yes: do not filter it

                MOVE    1, R8                   ; no: filter it
                RBRA    _FFILES_RET, 1

_FFILES_RET_0   XOR     R8, R8

_FFILES_RET     MOVE    R0, R9
                DECRB
                RET

Teaching you QNICE assembly is beyond the scope of this tutorial, but let's nevertheless go through the code line by line:

  • INCRB increases the current "register bank" of QNICE so that the existing values in registers R0 to R7 are saved and a fresh set of R0 to R7 can be used.
  • MOVE R9, R0 copies the value of register R9, which is the flag that states if we are looking at a file or a folder, to R0, so that we can restore the original value of R9 later. This is strictly speaking not necessary in this very situation, but it is a best practice for robust coding.
  • CMP 1, R9 checks, if we look at a folder/directory at this moment.
  • RBRA _FFILES_RET_0, Z jumps to the label _FFILES_RET_0 in case R9 equals 1, which will end the callback function by returning a zero.
  • MOVE TXT_EXTENSION, R9 moves the address of the label TXT_EXTENSION to the register R9. Please note that we did not define this label, yet, so if we would try to assemble this code, we would get an error. Also please note that at this point in time, R8 still contains the pointer to the zero-terminated string that contains the name of the file. At this moment we can be sure that we are talking about a file, because the CMP and RBRA commands above ensured this.
  • RSUB M2M$CHK_EXT, 1 performs a sub-routine call: The M2M framework provides a function called M2M$CHK_EXT. As the name might suggest, it checks if a certain file (string pointer to name given in R8) ends with a certain file extension (string pointer to extension given in R9). It sets the carry flag if the name has the given extension and it deletes the carry flag otherwise. You need to ensure that R9points to am uppercase string.
  • RBRA _FFILES_RET_0, C jumps to the label _FFILES_RET_0, if the carry flag is set, i.e. if the file name has the file extension *.txt that we are looking for. Remember that returning a zero means we are not filtering so the file is shown to the user.
  • MOVE 1, R8 is executed if the carry flag was not set, i.e. if the file has any other extension than *.txt. So copying the value 1 to R8 allows us to tell this to the M2M framework.
  • RBRA _FFILES_RET, 1 is an unconditional jump to the label _FFILES_RET.
  • _FFILES_RET_0 XOR R8, R8: This code defines the label _FFILES_RET_0, which we need above in case we do not want to filter a file. It also executes XOR R8, R8 which is equal to MOVE 0, R8 but executes a bit faster.
  • _FFILES_RET MOVE R0, R9: The label _FFILES_RET is defined. We use this label to "jump over" the XOR R8, R8 statement in case we want to filter a certain file. The MOVE R0, R9 restores the original value of R9 since we have overwritten R9 above, when we moved the string pointer to the file extension to R9. As stated above: In this very case it would not be necessary to save and restore R9 so that we leave it unchanged after the callback function call; it is just a best practice.
  • DECRB restore the lower register bank R0 to R7. In contrast to the above-mentioned "optional" best practice of leaving the upper registers untouched, it is mandatory to leave the lower registers untouched. So the INCRB and DECRB pair is how you begin and end sub-routines.
  • RET returns control to the caller, which is the M2M framework.

Have a look at qnice_intro.pdf, if you would like to learn more about the QNICE Instruction Set Architecture (ISA) and about QNICE assembly. The MiSTer2MEGA65 framework uses a QNICE System-on-a-Chip (SoC) plus custom peripherals to provide many of its services. Read the QNICE section of this wiki for a deep dive.

If you use the Vivado IDE, you normally choose the menu item "Generate Bitstream" to synthesize a new version of the tutorial core and to create a bitstream. If you try this now, you will see that Vivado did not notice that anything changed. This is logical, because the assembly file m2m-rom.asm that you modified throughout this section is not directly "known" by Vivado. So what you need to do to generate a new bitstream is:

  1. Assemble m2m-rom.asm to generate a new version of m2m-rom.rom
  2. Choose the menu item "Run Synthesis" in Vivado
  3. After (2) completes, choose the menu item "Generate Bitstream"

To perform step (1), open a terminal window, go to the folder CORE/m2m-rom and run the bash script make_rom.sh. You should see an output similar to this one, while the line number might differ:

assemble: Line 11272: Unresolved label or equ >>TXT_EXTENSION<<!
Offending line:
                MOVE TXT_EXTENSION, R9

main: There were 1 errors during assembly! No files written!
An unrevoverable error occured!

The reason for this error message is, that we did not yet define any label inside m2m-rom.asm that points to a zero-terminated string which represents the *.txt file extension we are looking for (and remember it needs to be specified in upper-case). Let's do that now: Search for the section called "Core specific constants and strings" in m2m-rom.asm and add this code:

TXT_EXTENSION    .ASCII_W ".TXT"

Retry building m2m-rom.rom by re-running make_rom.sh. You should see an output similar to this (while the amount of lines might differ):

qasm2rom: 14210 ROM lines written.

Now you can build the new version of the core by executing the above-mentioned steps (2) and (3).

Hint: Step (1) is always executed automatically by Vivado. It nevertheless makes sense to manually use make_rom.sh during development because it outputs the error message faster and in a better readable form, so you will be able to debug your code quicker.

When you run the core and "mount a drive" using the OSM, you will now see that the file- and directory browser is only showing the *.txt files that are on the SD card. You still can browse all directories.

Understanding the QNICE debug console

The M2M framework contains a built-in QNICE debug console that might become handy at certain stages of your MiSTer porting journey. You can for example use it to read and modify the contents RAMs and ROMs or to read and modify the registers of core-specific peripherals. In M2M terminology, all of these examples are Devices.

In this part of the tutorial, we will use QNICE's general purpose 256-bit register to modify the speed of the demo core's paddle.

Let's get acquainted with the general workings of the QNICE debug console:

  1. Start the demo core
  2. Connect a serial terminal to the MEGA65 while making sure that the serial terminal's parameters are set to 115,200 baud 8-N-1, no flow control such as XON/XOFF, RTS/CTS, DTR/DSR. Set any terminal emulation to "None" and if you can configure it, set the send mode to "Interactive" (instead of things like "Line buffered").
  3. While the core is running, first press and hold Run/Stop and Cursor Up and while holding these, press Help.

You should see an output like this on your serial terminal window (while the hexadecimal number after press C R might differ):

Entering MiSTer2MEGA65 debug mode.
Press H for help and press C R 1C1F to return to where you left off
and press C R 1957 to restart the Shell.

QMON> 

You have now entered the debug console. Try to move the paddle using the cursor keys and note, how you are still able to do it. This means that the demo core itself is still running. Now press Help and note that the on-screen-menu is not opening: QNICE is now in debug mode and therefore the M2M firmware is not running any more.

In your terminal window, press C, then R and then input the hexadecimal number shown above (in this example 1C1F) and then press the Enter key of your PC. In your terminal you should see something like this:

QMON> CONTROL/RUN ADDRESS=1C1F

Now go back to your MEGA65 and note, how the Help menu is working again and how you are "back to normal". You have successfully entered the QNICE debug console and by letting the CPU jump back to the M2M firmware ROM address 1C1F (or whatever the address is on your system), you resumed the execution of the M2M firmware.

Enter the debug console again by pressing the keys described in step (3) above. When you see the QMON> prompt, enter F and then D. You will see the listing of the root directory of the currently active SD card. The example SD card we have shows this listing:

QMON> FILE/LIST DIRECTORY
<DIR>            2022-04-03 18:22 games
<DIR>            2022-04-03 18:35 demos
<DIR>            2022-04-03 21:09 mh-collection
<DIR>            2022-04-04 01:14 Input 64
<DIR>            2022-04-04 01:14 Magic Disk D64
            7089 2022-07-04 14:23 test2.txt
<DIR>            2022-06-24 00:05 D64
<DIR>            2022-06-24 09:42 CRT
<DIR>            2022-06-23 00:20 Disk Mags

Now press H to learn more about the commands that the QNICE debug console accepts. The output might look similar to this:

QMON> HELP:

    C(control group):
        C(old start) H(alt) R(un) Clear(S)creen
    H(elp)
    M(emory group):
        C(hange) D(ump) E(xamine) F(ill) L(oad) M(ove)
        di(S)assemble
    F(ile group):
        List (D)irectory C(hange directory) L(oad) R(un)

    General: CTRL-E performs a warm start whenever an
        input from keyboard is expected.

    M(emory)L(oad) can be used to load assembler output
        by pasting it to the terminal. CTRL-E terminates.

    Scrolling (VGA): CTRL-(F)orward or (CsrDown) / CTRL-(B)ackward or (CsrUp)
        One page: (PgDown), (PgUp)  /  10 lines: CTRL-(PgDown), CTRL-(PgUp)
        First page: (Home) / last page: (End)

It is beyond the scope of this tutorial to learn how the M2M firmware works or what all QNICE registers mean. Nevertheless it makes a lot of sense to have a quick look at the two assembly files that contain all registers and memory mapped IO devices as well as the "magic" values you can use to control them:

  1. QNICE general
  2. M2M specific

In the M2M specific sysdef.asm you will find this section:

; 256 bits directly controlled by the programmer (not used by the Shell)
; Select a window between 0 and 15 in M2M$CFD_ADDR and access the control
; flags sliced into 16-bit chunks via M2M$CFD_DATA
; exposed by QNICE via control_d_o
M2M$CFD_ADDR        .EQU 0xFFF0
M2M$CFD_DATA        .EQU 0xFFF1

So we have two 16-bit QNICE registers used to read and write from and to a 256 bit wide internal register that is exposed from the M2M framework to the core: You select the 16-bit "window" within the 256 bits by writing to 0xFFF0. If you want to read/write to the lower 16 bits then the window we need is 0. And then, we can access these lower 16 bits using the address 0xFFF1. By default, this QNICE "general purpose" register is not used by the M2M framework. We will now use it to change the speed of the demo core's paddle - but you could also invent other ingenious uses for this register when doing your own cores.

Open the file main.vhd and note, that there is a 4-bit wide constant used within the demo core's instantiation that specifies the speed of the paddle:

paddle_speed_i       => x"1",                               -- paddle speed is about 50 pixels / sec (due to 50 Hz) 

The standard frame rate of the core is 50 Hz and since the position of the paddle is updated once per frame, a 1 means that the paddle is moving with a speed of about 50 pixels per second. A 2 means 100 pixels per second, a 3 means 150 pixels per second and so on.

paddle_speed_i is a 4-bit std_logic_vector so let's connect the lower 4 bit of the QNICE 256-bit general purpose register to paddle_speed_i. For doing so, we enhance the interface of entity main in main.vhd by a 4-bit std_logic_vector input. Add it below osm_i:

-- Paddle speed
paddle_speed_i          : in  std_logic_vector(3 downto 0);

The first 15 lines of the definition of entity main should look like this:

entity main is
   generic (
      G_VDNUM                 : natural                     -- amount of virtual drives     
   );
   port (
      clk_main_i              : in  std_logic;
      reset_soft_i            : in  std_logic;
      reset_hard_i            : in  std_logic;
      pause_i                 : in  std_logic;
      
      -- On-screen-menu selection
      osm_i                   : in  std_logic_vector(255 downto 0);
      
      -- Paddle speed
      paddle_speed_i          : in  std_logic_vector(3 downto 0);

Next, replace the constant x"1" in the demo core's instantiation by a connection to this newly defined input so that the first 9 lines look like this:

i_democore : entity work.democore
  port map (
     clk_main_i           => clk_main_i,
     
     reset_i              => reset_soft_i or reset_hard_i,       -- long and short press of reset button mean the same
     pause_i              => pause_i,
     
     ball_col_rgb_i       => ball_color,                         -- ball color chosen via OSM
     paddle_speed_i       => paddle_speed_i,                     -- paddle speed defined by QNICE's GP register

Now open mega65.vhd and connect the lower 4 bits of the QNICE general purpose register with the newly defined paddle_speed_i while instantiating entity work.main. The first 15 lines should look like this:

i_main : entity work.main
  generic map (
     G_VDNUM              => C_VDNUM
  )
  port map (
     clk_main_i           => main_clk,
     reset_soft_i         => main_reset_core_i,
     reset_hard_i         => main_reset_m2m_i,
     pause_i              => main_pause_core_i,
     
     -- On-screen-menu selection
     osm_i                => main_osm_control_i,
     
     -- Paddle speed
     paddle_speed_i       => main_qnice_gp_reg_i(3 downto 0), 

Please note that we again use the register that is located within the core's clock domain, i.e. the one with the main_ prefix instead of the one with the qnice_ prefix. Also note that we are only using the lower 4 bits:

main_qnice_gp_reg_i(3 downto 0)

Synthesize and run the new core. What you will notice immediately is that you will not be able to move the paddle at all any more. Neither with the cursor keys nor using the joystick in port #1. The reason for that is, that QNICE happens to initialize its 256 bit general purpose register with zero on startup and reset. So you will need to write a paddle speed greater than zero to the QNICE memory location 0xFFF1 after having selected the lowest window using 0xFFF0 to enable the paddle.

Open the QNICE debug console as described above and enter H to see what key strokes you need. Then execute these commands and make sure you are not entering the 0x prefix but just the raw 16-bit hexadecimal number. In case you have a typo, press Ctrl+E to retry:

QMON> MEMORY/CHANGE ADDRESS=FFF0 CURRENT VALUE=0000 NEW VALUE=0000
QMON> MEMORY/CHANGE ADDRESS=FFF1 CURRENT VALUE=0000 NEW VALUE=000F

After this, move the paddle using the cursor keys or the joystick in port #1. The paddle moves now pretty fast. You can experiment with different speeds by writing values between 0000 and 000F to address FFF1. The default speed is 0001. While experimenting, you are not able to use the OSM as described above (because the M2M firmware is not running), but since the demo core itself is running, you can move the paddle and play.

Resume the firmware to have access to the menu again (you might need to use another value than 1C1F; use the value that the system outputs when you enter the debug console):

QMON> CONTROL/RUN ADDRESS=1C1F

Next Steps

If you followed the track from the beginning to here, you are now ready to start to port your first MiSTer core. We recommend you continue with the next chapter called Understand the MiSTer core.