Hello World Tutorial

From Intellivision Wiki
Jump to: navigation, search


This tutorial serves as a quick-start, quickly introducing a simple program that displays the famous phrase "Hello World" on an Intellivision title screen. In this tutorial, you will see how to invoke the assembler and how to call a library function from SDK-1600 named PRINT. This particular tutorial does not go into all the details underlying how it works. Dig into the more specific tutorials for deeper dives on all the topics this example touches.

Example 1: Hello World!

Let's start with the first example, and then pull it apart. Here is hello1.asm:

        ROMW    16      
        ORG     $5000

;------------------------------------------------------------------------------
; EXEC-friendly ROM header.
;------------------------------------------------------------------------------
ROMHDR: BIDECLE ZERO            ; MOB picture base   (points to NULL list)
        BIDECLE ZERO            ; Process table      (points to NULL list)
        BIDECLE MAIN            ; Program start address
        BIDECLE ZERO            ; Bkgnd picture base (points to NULL list)
        BIDECLE ONES            ; GRAM pictures      (points to NULL list)
        BIDECLE TITLE           ; Cartridge title/date
        DECLE   $03C0           ; Flags:  No ECS title, run code after title,
                                ; ... no clicks
ZERO:   DECLE   $0000           ; Screen border control
        DECLE   $0000           ; 0 = color stack, 1 = f/b mode
        BIDECLE ONES            ; GRAM pictures      (points to NULL list)
ONES:   DECLE   1, 1, 1, 1, 1   ; Color stack initialization
;------------------------------------------------------------------------------

TITLE   DECLE   107, "Hello World!", 0

MAIN    EIS                     ; Enable interrupts
here    B       here            ; Spin forever.

ROM Width and Origin

The program starts off with a couple directives. These are commands to the assembler that tell it to do various things.

        ROMW    16      
        ORG     $5000

The "ROMW" directive sets the "ROM width." Traditional Intellivision games used 10-bit wide ROMs. Modern games can use a full 16-bit wide ROM. Although the assembler defaults to 16 bits, it doesn't hurt to make it explicit.


The "ORG" directive sets the current "origin". What is this, and why does it matter? The assembler is not a compiler. Compilers take high level code, rearrange it and translate it, and ultimately bake it down into a series of instructions that run on the CPU. An assembler does much, much less than that. It merely assembles (as in "puts together") what you've written. The assembler internally keeps an image of what should be in memory based on what you've told it. When the assembler starts, that image is empty. As you give it instructions to assemble, the assembler converts these to machine code and inserts them into this virtual memory image. The ORG directive tells it where to start writing—a "cursor," if you will.

A program can have more than one ORG statement. The ORG statement moves the assembler's cursor (aka. the "SPC" or "Section Program Counter"), to a new spot. This is useful in cases where not all of your ROM is contiguous. That's a topic for another tutorial.

So why "ORG $5000"? The Intellivision's EXEC expects most cartridges to start at this address. It looks for a cartridge header there, and expects that header to tell it something interesting and useful about the program, which leads us to the next part of the program, the cartridge header.

Cartridge Header

;------------------------------------------------------------------------------
; EXEC-friendly ROM header.
;------------------------------------------------------------------------------
ROMHDR: BIDECLE ZERO            ; MOB picture base   (points to NULL list)
        BIDECLE ZERO            ; Process table      (points to NULL list)
        BIDECLE MAIN            ; Program start address
        BIDECLE ZERO            ; Bkgnd picture base (points to NULL list)
        BIDECLE ONES            ; GRAM pictures      (points to NULL list)
        BIDECLE TITLE           ; Cartridge title/date
        DECLE   $03C0           ; Flags:  No ECS title, run code after title,
                                ; ... no clicks
ZERO:   DECLE   $0000           ; Screen border control
        DECLE   $0000           ; 0 = color stack, 1 = f/b mode
ONES:   DECLE   C_BLU, C_BLU    ; Initial color stack 0 and 1: Blue
        DECLE   C_BLU, C_BLU    ; Initial color stack 2 and 3: Blue
        DECLE   C_BLU           ; Initial border color: Blue
;------------------------------------------------------------------------------

I'm not actually going to explain the entire contents of the EXEC header, because quite honestly, I don't know (or care) what most of the fields do. You're welcome to use this header in your own programs. It passes muster with the EXEC, and it bypasses the ECS title screen on machines with an ECS. The main point of this particular header is to provide the EXEC the minimum amount of information it needs to jump to your code and get everything started. My examples will bypass the EXEC whenever possible.

The most important fields to worry about are the one labeled "Cartridge title/date", and the one labeled "Flags". The first one points to the cartridge's copyright date as well as the title string. The second one holds a set of flags. Here's what they do:

bit 15..10Unused.
bit 9..8Unknown. Set both to 1 to skip ECS title screen.
bit 7Run code that appears after title string.
bit 6Set to 1 to work on Intellivision 2.
bit 5..0What inputs the EXEC will make "click" sounds for

You can ignore bits 5..0 if you don't use the EXEC. The "keyclick" functionality only works if you rely on the EXEC for keypad input. None of my programs uses the EXEC.

There is a third field of interest, "MAIN". If you bypass the EXEC as I do, and don't rely on the EXEC to handle starting the game after the title screen, then this field is never actually used. Otherwise, this field points to the first line of program code that the EXEC will call after the user dismisses the EXEC's title screen.

As shown, the header above will bypass most of the EXEC's automatic setup, which is fine. None of my examples will rely on the EXEC to do anything. (Well, other than put up the title screen in this case.) This brings us to the next piece of code, the date and title.

Date and Title

The program indicates the date for the program's copyright as well as its title in a simple manner:

TITLE   DECLE   107, "Hello World!", 0

As I noted before, the "title" field points at the copyright date and the title string. The copyright date is merely the year minus 1900. (Keith Robinson mentioned that the original documentation actually said "last two digits of the copyright year." Given that we're past Y2K, let's stick with "year - 1900". Programs such as jzIntv expect the date in that format.) The copyright string itself is NUL-terminated, which means simply that it's followed by a word containing "0".

The DECLE directive tells the assembler to insert the data that follows it into the ROM. DECLE originally referred to the 10-bit words that the old games had to work with in their 10-bit wide ROMs. as1600 expanded the concept to mean "words that are the width of the ROM, as set by ROMW." Since we set the ROM width to 16 up front, the DECLE directive inserts a series of 16-bit words into memory.

A note on memory here: The CP-1600 is what's known as a "word addressable" machine. What this means is that each location in the address map holds a single word. Words are 16 bits wide. Narrower memory, such as 10-bit ROMs and 8-bit RAM work by filling the upper bits of the word with 0s. So, if you read the EXEC ROM, you'll find bits 10-15 always read as 0. If we had set "ROMW 10" instead of "ROMW 16", the DECLE directive would expand our data to 10 bit words, filling the upper 6 bits with 0s.

The assembler also has some other directives for initializing memory, such as BYTE, WORD and BIDECLE. I discourage using "WORD", as it doesn't work how you might expect. I personally use DECLE and BIDECLE for everything. Take a look at the assembler documentation for more information on these directives.

Code After Title String

In the header, we set a bit that said "Run the code after the title string." For now, we'll just go to an infinite loop after the title screen, since there's no program to go to right now:

MAIN    EIS                     ; Enable interrupts
here    B       here            ; Spin forever.

What this does is, as the comments say, enable interrupts and spin forever. The code enables interrupts so that the display itself is enabled. The STIC chip inside the Intellivision needs to be told, on a frame by frame basis, to enable the display. This happens in an interrupt service routine (ISR). The EXEC's initialization code installs a basic ISR that, among other things, keeps the display enabled. In a future tutorial, I will show how to replace that ISR with a custom one.

Assembling and Running

To assemble the example, copy Example 1 above into a file named "hello1.asm". Then run the following command to assemble it:

    as1600 -o hello1 -l hello1.lst hello1.asm

This will write out 4 files:

hello1.romThe program in Intellicart .ROM format
hello1.binThe same program in BIN+CFG format
hello1.cfgThe config file to go with hello1.bin
hello1.lstThe listing file

You can now run hello1.rom or hello1.bin in your favorite Intellivision emulator or on a real Intellivision by way of a Cuttle Cart 3, Intellicart, Foom Board or any other hardware that happens to let you run on the real hardware. You should see:

hello1.asm screen shot

(Note: The screen will look different if you run this on a Sears machine, since the Sears units omit the "Mattel Electronics" string.)

Tada! Notice the broken year, and incorrect copyright attribution. We will fix that in a minute. Before we do, let's step back a minute.

Examining the Listing File

The listing file is a useful file while you're developing. It contains a detailed account of what the assembler did with your program. It shows the complete symbol table -- the list of all the symbols you used in your program and the values they ended up with -- as well as an account of what values were put in what locations in the memory space. Let's look at the listing file for hello1.asm:

00005000 ROMHDR                     0000500d ZERO                       
00005022 MAIN                       0000500f ONES                       
00005014 TITLE                      00005023 here                       
�                              ROMW    16      
 0x5000                         ORG     $5000

                        ;------------------------------------------------------------------------------
                        ; EXEC-friendly ROM header.
                        ;------------------------------------------------------------------------------
5000 000d 0050          ROMHDR: BIDECLE ZERO            ; MOB picture base   (points to NULL list)
5002 000d 0050                  BIDECLE ZERO            ; Process table      (points to NULL list)
5004 0022 0050                  BIDECLE MAIN            ; Program start address
5006 000d 0050                  BIDECLE ZERO            ; Bkgnd picture base (points to NULL list)
5008 000f 0050                  BIDECLE ONES            ; GRAM pictures      (points to NULL list)
500a 0014 0050                  BIDECLE TITLE           ; Cartridge title/date
500c 03c0                       DECLE   $03C0           ; Flags:  No ECS title, run code after title,
                                                        ; ... no clicks
500d 0000               ZERO:   DECLE   $0000           ; Screen border control
500e 0000                       DECLE   $0000           ; 0 = color stack, 1 = f/b mode
500f 0001 0001 0001     ONES:   DECLE   1, 1, 1, 1, 1   ; Color stack initialization
5012 0001 0001 
                        ;------------------------------------------------------------------------------

5014 006b 0048 0065     TITLE   DECLE   107, "Hello World!", 0
5017 006c 006c 006f 0020 0057 006f 0072 006c 
501f 0064 0021 0000 

5022 0002               MAIN    EIS                 ; Enable interrupts
5023 0220 0001          here    B       here        ; Spin forever.
 ERROR SUMMARY - ERRORS DETECTED 0
               -  WARNINGS       0

The first part of the file is the symbol table. This is a list of all symbols (aka. labels) defined in the file. Symbols are simply character strings that refer to numbers. In this case, they are all program labels, and they refer to locations in memory:

ROMHDRRefers to the start of the ROM header
ZERORefers to the "DECLE $0000" directive in the header
ONESRefers to the "DECLE 1, 1, 1, 1, 1" directive in the header
TITLERefers to our date/title string
MAINRefers to the location of our first program instruction
hereRefers to the address of the infinite loop

When you write something like "MAIN EIS", the assembler binds the address of the instruction to the label. In this case, it binds "MAIN" to the address of the EIS instruction. According to the symbol table, that address is $5022.

After the symbol table comes the core of the assembly listing -- an accounting of how each instruction and directive turned into words in memory. At the far left is the address. Immediately after that are the actual values that the assembler put into the memory image.

For such a short program, the listing file isn't particularly interesting. For a larger program, it can be a lifesaver. If there are assembly errors, the assembler will mark them in the listing file next to the code that caused them. If you're trying to debug your code in an emulator's debugger, the listing file will tell you the addresses that everything ended up at so you can figure out where to set your breakpoints and watchpoints. All in all, it's indispensable for serious program development.

Example 2: Fixing up the title screen

Now, let's set about patching up that title screen, shall we? Before we start, copy hello1.asm to hello2.asm. This segment extends the example from the previous segment.

Let's first fix up the copyright date. One easy way to do this is to simply overwrite the entire string with one of our own. We'll use SDK-1600's PRINT function to do that.

Calling PRINT

The SDK-1600 PRINT function accepts its arguments multiple ways. Take a look at print.asm in SDK-1600 for a complete listing. For now, we will use the "PRINT.FLS" variant, which expects the following:

;;  PRINT.FLS     Format, location, and string itself follows CALL          ;;

...

;;  INPUTS for PRINT.FLS:                                                   ;;
;;      R5 -- Invocation record, followed by return code.                   ;;
;;              Screen format word          1 DECLE                         ;;
;;              Ptr to display location     1 DECLE                         ;;
;;              String                      n DECLEs (NUL terminated)       ;;

The CP-1610's CALL instruction works by copying the return address (the address immediately following the CALL) into the CPU register R5, and then jumping to the label or address that appears after the CALL. So "CALL PRINT.FLS" will jump to the label "PRINT.FLS". When it gets there, R5 points just after the CALL instruction.

Because of this setup, it's extremely common to pass arguments for a CALL in the words that follow the CALL. In this case, PRINT.FLS expects 3 things to appears after the CALL: A "screen format word", a pointer to where to print, and the actual NUL-terminated string to print.

The "screen format word" is a word that describes the formatting to apply to the string. Typically this is just a color number, although you can do fancier things with this. (See print.asm for details.) In our case, we'd like to print in white, and white is color #7.

The "pointer to display location" is the address on the display to which PRINT will print the string. Display memory starts at location $200. Location $200 corresponds to the upper left hand corner. Each row is 20 characters long. Thus, $200 + 19 is the rightmost character in the top row, and $200 + 20 is the leftmost character in the second row. I'll refer to the top row as row 0 and the leftmost column as column 0. This makes the math easier.

Hello1 scrshot grid.gif

We wish to replace the "Copr @ 19:7 Mattel" string at the bottom of the screen with our own. If you look closely at the screen shot above, you'll see that it begins in row 10, column 1. The address of this location is given by the expression: $200 + 10*20 + 1.

The assembler will evaluate expressions for you, so you can put that entire expression in there as-is and let the assembler work it out for you. It makes the code easier to understand and easier to write. Also, in some cases, it can make otherwise very complex and tricky things easy, especially if it all gets hidden in a macro. But that's for a later tutorial.

With our format word and display pointer in hand, we can add the following code to our program between the "EIS" and infinite loop:

MAIN    EIS                     ; Enable interrupts

        CALL    PRINT.FLS
        DECLE   7               ; 7 is the color number for "white"
        DECLE   $200 + 10*20 + 1
        DECLE   "  Copyright 2007  ", 0

here    B       here            ; Spin forever.

So far, so good. Now how about that PRINT function? Where does that come in? Take the file "print.asm" from the "examples/library" directory and copy it into the same directory as your Hello World program. At the end of the code, add this line:

        INCLUDE "print.asm"

The Complete hello2.asm

The complete program in hello2.asm looks like this:

        ROMW    16      
        ORG     $5000

;------------------------------------------------------------------------------
; EXEC-friendly ROM header.
;------------------------------------------------------------------------------
ROMHDR: BIDECLE ZERO            ; MOB picture base   (points to NULL list)
        BIDECLE ZERO            ; Process table      (points to NULL list)
        BIDECLE MAIN            ; Program start address
        BIDECLE ZERO            ; Bkgnd picture base (points to NULL list)
        BIDECLE ONES            ; GRAM pictures      (points to NULL list)
        BIDECLE TITLE           ; Cartridge title/date
        DECLE   $03C0           ; Flags:  No ECS title, run code after title,
                                ; ... no clicks
ZERO:   DECLE   $0000           ; Screen border control
        DECLE   $0000           ; 0 = color stack, 1 = f/b mode
ONES:   DECLE   1, 1, 1, 1, 1   ; Color stack initialization
;------------------------------------------------------------------------------

TITLE   DECLE   107, "Hello World!", 0

MAIN    EIS                     ; Enable interrupts

        CALL    PRINT.FLS
        DECLE   7               ; 7 is the color number for "white"
        DECLE   $200 + 10*20 + 1
        DECLE   "  Copyright 2007  ", 0

here    B       here            ; Spin forever.


        INCLUDE "print.asm"

A note on INCLUDE

If you're accustomed to programming in a language like C or C++, you are now probably wondering "Why do I put the INCLUDE line at the end rather than the beginning?" That's a great question!

The INCLUDE directive copies in the contents of the specified file at the exact location it appears in your source file. It's as if you cut and pasted that file in at that point. The file "print.asm" contains the actual assembly code for the PRINT function. That means that the code gets assembled right there.

In a C or C++ program, the #include directive works similarly in that it copies in the requested file. The difference is that C and C++ header files typically only contain declaration information. They don't actually contain the program code that implements the functions you'll call. For example, when you do a "#include <stdio.h>", the file stdio.h describes what printf() looks like, but doesn't actually have an implementation of printf() in there. That comes from a library somewhere. That library gets linked in later.

In an as1600 program, the entire program gets presented to the assembler at one time. There is no separate linking step. When you include a library function such as PRINT, you're actually including the program code right there. You can see this in the listing file after you assemble your program.

If you had INCLUDEd print.asm at the top of the program, before the ORG directive, the PRINT function would get assembled at location $0000 -- not a good thing, since that's where the STIC's registers live. If you included it after the ORG directive but before the ROM header, then it would get assembled at location $5000, which is where th ROM header needs to be. You *could* put the INCLUDE directive after the header and before "TITLE". Often, though, it's just easiest to put it at the end.

Note that some header files, such as files containing MACRO definitions and other useful definitions need to be INCLUDEd before they can be used. These files you'll want to INCLUDE at the top of your source. So far, we haven't used any of these features yet, but I thought I'd mention it.

Assembling and Running

Assemble hello2.asm with the following command:

as1600 -o hello2 -l hello2.lst hello2.asm

Then, run hello2 as you did earlier. You should see a screen similar to the following:

Hello2 scrshot.gif

Ta da!

Next Steps

See if you can take what you've learned and modify the title screen even more. For instance, try replacing "Mattel Electronics" with something else. Good luck!


Back to Programming Tutorials.