The "Hello, World!" first subroutine

To demonstrate how to create a subroutine or program in a new environment, the customary and ubiquitous "Hello, World!" is usually presented. We'll abide by that tradition and show how to create a subroutine in MIRTOS that outputs that string to the serial port.

Start by creating a new file, which we can call, very simply, "HelloWorld.Asm". It should contain just the string to print, specified using the ".STRING" Assembler directive, which is NOT case sensitive. To go on a bunny trail for a moment, you may notice that our examples CAPITALIZE Assembler directives (.ASCII, .BALIGN, .BYTE, .WORD, .ORG, .PRINT, .STRING, .WARNING, as a few examples.) This is mostly out of the habit of capitalizing keywords and constants, while using mixed case for variables.

Open a command prompt and enter the single line shown below, followed by Enter. This places the cursor on the next line. Enter Ctrl-Z, which means press the "Control" key then "Z". This usually displays as ^Z as shown below. Unless modified, the F6 key usually also does this. The Control-Z character marks the end of a text file, and in the case of console input, tells the Operating System that file input is complete. Assemble the one-line "program". There should be no errors, but six (6) output files are generated, as shown with the "Dir" command.

Notice that the string entered below ends with "\r\n". Those are the characters Carriage Return (= 13 = 0x0D) and Line Feed (= 10 = 0x0A), respectively. They simply place the cursor at the start the next line after our string is output.

C:\TestDir>Copy CON HelloWorld.Asm
.STRING "Hello, World!\r\n"
^Z
        1 file(s) copied.

C:\TestDir>Assemble HelloWorld

C:\TestDir>Dir /B HelloWorld.*
HelloWorld.Asm
HelloWorld.Elf
HelloWorld.Hex
HelloWorld.Trm
HelloWorld.Sym
HelloWorld.Dmp
HelloWorld.Ptr

C:\TestDir>                                                                     

Well, that 's a good start, but incomplete, since we've just specified WHAT is to be output, but not given any instruction on HOW to it is to be output. It's similar to an incomplete sentence. It has a subject, but no verb. So, the next thing is to open a text editor and add both a string title, so the Assembler knows how to reference it by name, and the command that will cause it to be output.

Open HelloWorld.Asm in a text editor. We'll place the CALL to PrintFLASHASCIIz, the MIRTOS system function that performs the printing. Upon opening that webpage and scrolling to the bottom, there is a little section called "Dropin: (setup code)", which we'll copy and paste into our file just above the CALL to PrintFLASHASCIIz. After that, place a RETurn statement to return control to the routine which will be calling our Hello, World! subroutine. Our file might well look something like this now:

.STRING "Hello, World!\r\n"

;   Setup for the PrintFLASHASCIIz() call:
; Passed:  R25:R24 CHAR * szPtrBuffer pointer to the string to print
; Returns: R25:R24 CHAR * szPtrBuffer pointer to the string, unmodified
;              R22 BYTE   bPrintCount number of BYTEs placed in the print queue
; Alters:  R20 and the FLAGs, in addition to R22, R24 and R25 returned
   CALL    APrintFLASHASCIIz
   RET

But notice that the only requirement is that the R25:R24 Register pair specify the FLASH address of the string to print. OK, since it is at the start of the memory, we'll assume that it's FLASH address is 0x0000 and initialize both R25 and R24 to 0 using the LDILoad Immediate (load a Register with a constant) instruction.

.STRING "Hello, World!\r\n"

;   Setup for the PrintFLASHASCIIz() call:
; Passed:  R25:R24 CHAR * szPtrBuffer pointer to the string to print
; Returns: R25:R24 CHAR * szPtrBuffer pointer to the string, unmodified
;              R22 BYTE   bPrintCount number of BYTEs placed in the print queue
; Alters:  R20 and the FLAGs, in addition to R22, R24 and R25 returned
   LDI     R25, 0
   LDI     R24, 0
   CALL    PrintFLASHASCIIz
   RET

Assemble HelloWorld.Asm again. Observe that it generated no errors. Let's examine the listing (.DMP) file. One easy way is to use TYPE.

C:\TestDir>Assemble HelloWorld

C:\TestDir>Type HelloWorld.Dmp

HelloWorld.Elf:     file format elf32-avr


Disassembly of section .text:

00000000 <.text>:
   0:   48 65           ori     r20, 0x58       ; 88
   2:   6c 6c           ori     r22, 0xCC       ; 204
   4:   6f 2c           mov     r6, r15
   6:   20 57           subi    r18, 0x70       ; 112
   8:   6f 72           andi    r22, 0x2F       ; 47
   a:   6c 64           ori     r22, 0x4C       ; 76
   c:   21 0d           add     r18, r1
   e:   0a 00           .word   0x000a  ; ????
  10:   90 e0           ldi     r25, 0x00       ; 0
  12:   80 e0           ldi     r24, 0x00       ; 0
  14:   0e 94 00 00     call    0       ; 0x0 <.text>
  18:   08 95           ret

C:\TestDir>                                                                     

Yecch! It's all run together. It isn't real obvious where the string ends and where the code begins. If we assign labels to both the starting locations of the string and the code, it should help to identify each section. Labels are case sensitive, are placed just prior to the start of the string or code they are to identify, and end with a colon. The label can be on the same line as the Assembler directive or instruction. There is one done each way in the changes below.

HelloWorldString:  .STRING "Hello, World!\r\n"

;   Setup for the PrintFLASHASCIIz() call:
; Passed:  R25:R24 CHAR * szPtrBuffer pointer to the string to print
; Returns: R25:R24 CHAR * szPtrBuffer pointer to the string, unmodified
;              R22 BYTE   bPrintCount number of BYTEs placed in the print queue
; Alters:  R20 and the FLAGs, in addition to R22, R24 and R25 returned
HelloWorld:
   LDI     R25, 0
   LDI     R24, 0
   CALL    PrintFLASHASCIIz
   RET

Let's go ahead and Assemble and check the listing again.

C:\TestDir>Assemble HelloWorld

C:\TestDir>Type HelloWorld.Dmp

HelloWorld.Elf:     file format elf32-avr


Disassembly of section .text:

00000000 <HelloWorldString>:
   0:   48 65           ori     r20, 0x58       ; 88
   2:   6c 6c           ori     r22, 0xCC       ; 204
   4:   6f 2c           mov     r6, r15
   6:   20 57           subi    r18, 0x70       ; 112
   8:   6f 72           andi    r22, 0x2F       ; 47
   a:   6c 64           ori     r22, 0x4C       ; 76
   c:   21 0d           add     r18, r1
   e:   0a 00           .word   0x000a  ; ????

00000010 <HelloWorld>:
  10:   90 e0           ldi     r25, 0x00       ; 0
  12:   80 e0           ldi     r24, 0x00       ; 0
  14:   0e 94 00 00     call    0       ; 0x0 <HelloWorldString>
  18:   08 95           ret

C:\TestDir>                                                                     

That listing is much clearer. Notice, however, that the address of the MIRTOS system function PrintFLASHASCIIz resolved to zero. To remedy that, we'll need to use .INCLUDE to pull in a file which has the addresses of all of the MIRTOS system functions. The 328P, 1284P, and 2560 each have their own file, since the system function addresses differ among them.

Upon examining the Addresses328.Def file, you'll notice that there is no "PrintFLASHASCIIz", but that there is "APrintFLASHASCIIz". In fact, all of the system functions in the file begin with 'A'. The 'A' means "address of" and is prepended to all of the labels by the MakePtr program contained in the Assemble.Bat script. So, we'll simply change PrintFLASHASCIIz to APrintFLASHASCIIz.

Next, if we check our MIRTOS device, there is existing code already at address zero, but none at address 0x0800. The .ORG directive allows us to tell the Assembler to advance its code pointer to an address at or past the next FLASH address that it would use if the .ORG was not present. So, we'll tell it to start at FLASH address 0x0800. That means that the address we placed in R25:R24 must be changed to 0x0800 as well. The High order portion of the address is in R25, so it changes to 8 while the Low order portion in R24 remains unchanged. Here is our file's contents:

.ORG 0x800

.INCLUDE "Addresses328.Def"

HelloWorldString:  .STRING "Hello, World!\r\n"

;   Setup for the PrintFLASHASCIIz() call:
; Passed:  R25:R24 CHAR * szPtrBuffer pointer to the string to print
; Returns: R25:R24 CHAR * szPtrBuffer pointer to the string, unmodified
;              R22 BYTE   bPrintCount number of BYTEs placed in the print queue
; Alters:  R20 and the FLAGs, in addition to R22, R24 and R25 returned
HelloWorld:
   LDI     R25, 8
   LDI     R24, 0
   CALL    APrintFLASHASCIIz
   RET

Let's Assemble the file again, and examine the listing as before:

C:\TestDir>Assemble HelloWorld

C:\TestDir>Type HelloWorld.Dmp

HelloWorld.Elf:     file format elf32-avr


Disassembly of section .text:

00000000 <HelloWorldString-0x800>:
        ...

00000800 <HelloWorldString>:
 800:   48 65           ori     r20, 0x58       ; 88
 802:   6c 6c           ori     r22, 0xCC       ; 204
 804:   6f 2c           mov     r6, r15
 806:   20 57           subi    r18, 0x70       ; 112
 808:   6f 72           andi    r22, 0x2F       ; 47
 80a:   6c 64           ori     r22, 0x4C       ; 76
 80c:   21 0d           add     r18, r1
 80e:   0a 00           .word   0x000a  ; ????

00000810 <HelloWorld>:
 810:   98 e0           ldi     r25, 0x08       ; 8
 812:   80 e0           ldi     r24, 0x00       ; 0
 814:   0e 94 49 30     call    0x6092  ; 0x6092 <APrintASCIIz>
 818:   08 95           ret

C:\TestDir>                                                                     

No errors and the listing has three (3) code values correctly resolved. Next, we'll load and test our routine in device memory. HelloWorld.Trm has the loadable code in iHex format.

>DF+ 0x800 64
FLASH (program memory) contents:
0800:  FF FF FF FF FF FF FF FF-FF FF FF FF FF FF FF FF  ................
0810:  FF FF FF FF FF FF FF FF-FF FF FF FF FF FF FF FF  ................
0820:  FF FF FF FF FF FF FF FF-FF FF FF FF FF FF FF FF  ................
0830:  FF FF FF FF FF FF FF FF-FF FF FF FF FF FF FF FF  ................
>  ; Use the '$' first character to enter Manager mode
m>:1008000048656C6C6F2C20576F726C64210D0A0068
m>:0A08100098E080E00E94493008954E
m>:00000001FF
m>DF+ 0x800 64
FLASH (program memory) contents:
0800:  48 65 6C 6C 6F 2C 20 57-6F 72 6C 64 21 0D 0A 00  Hello, World!...
0810:  98 E0 80 E0 0E 94 49 30-08 95 FF FF FF FF FF FF  ......I0........
0820:  FF FF FF FF FF FF FF FF-FF FF FF FF FF FF FF FF  ................
0830:  FF FF FF FF FF FF FF FF-FF FF FF FF FF FF FF FF  ................
m>@ 0x810   ; Call the subroutine from the command line
Hello, World!
m>                                                                              

Although our Hello, World! subroutine works as expected, it would be better if the subroutine started at the known address of 0x0800 rather than having to determine from the listing that it starts at 0x0810. Also, it would be much better to have the program resolve and insert the string's address into R25 and R24 rather than having to load each with a numeric constant.

The first part is easy; just move the string to the bottom. Loading R25:R24 takes two steps: the first is to .INCLUDE the .PTR file, which has been built during each Assembly. It, like Addresses328.Def, also prepends 'A' to set its label address constants. The second step is to change the LDI to load the High 8-BIT portion of that 16-BIT constant into R25 using "constant >> 8" which means "shift the constant right 8 bit positions" and "constant & 0xFF" which means "leave only the low 8 BITs of the constant." For the 1284P and 2560, there is a third line to set R26. Our file revisions should leave it something like this:

.ORG 0x800

.INCLUDE "Addresses328.Def"
.INCLUDE "HelloWorld.Ptr"

;   Setup for the PrintFLASHASCIIz() call:
; Passed:  R25:R24 CHAR * szPtrBuffer pointer to the string to print
; Returns: R25:R24 CHAR * szPtrBuffer pointer to the string, unmodified
;              R22 BYTE   bPrintCount number of BYTEs placed in the print queue
; Alters:  R20 and the FLAGs, in addition to R22, R24 and R25 returned
HelloWorld:
   LDI     R25, AHelloWorldString >> 8
   LDI     R24, AHelloWorldString & 0xFF
   CALL    APrintFLASHASCIIz
   RET

HelloWorldString:  .STRING "Hello, World!\r\n"

This time, we must Assemble twice. The first time both checks for any errors and updates the .PTR file, since the addresses of both the subroutine entry point and string changed. The second time applies the constants to the program. Here is the result:

C:\TestDir>Assemble HelloWorld

C:\TestDir>Assemble HelloWorld

C:\TestDir>Type HelloWorld.Dmp

HelloWorld.Elf:     file format elf32-avr


Disassembly of section .text:

00000000 <HelloWorld-0x800>:
        ...

00000800 <HelloWorld>:
 800:   98 e0           ldi     r25, 0x08       ; 8
 802:   8a e0           ldi     r24, 0x0A       ; 10
 804:   0e 94 49 30     call    0x6092  ; 0x6092 <APrintASCIIz>
 808:   08 95           ret

0000080a <HelloWorldString>:
 80a:   48 65           ori     r20, 0x58       ; 88
 80c:   6c 6c           ori     r22, 0xCC       ; 204
 80e:   6f 2c           mov     r6, r15
 810:   20 57           subi    r18, 0x70       ; 112
 812:   6f 72           andi    r22, 0x2F       ; 47
 814:   6c 64           ori     r22, 0x4C       ; 76
 816:   21 0d           add     r18, r1
 818:   0a 00           .word   0x000a  ; ????

C:\TestDir>                                                                     

OK, everything is in place. R25 and R24 are being loaded correctly and the MIRTOS PrintFLASHASCIIz function address is correct. Let's reload the iHex image into our device and test it again:

m>:1008000098E08AE00E944930089548656C6C6F2C2E
m>:0A08100020576F726C64210D0A007E
m>:00000001FF
m>DF+ 0x800 64
FLASH (program memory) contents:
0800:  98 E0 8A E0 0E 94 49 30-08 95 48 65 6C 6C 6F 2C  ......I0..Hello,
0810:  20 57 6F 72 6C 64 21 0D-0A 00 FF FF FF FF FF FF   World!.........
0820:  FF FF FF FF FF FF FF FF-FF FF FF FF FF FF FF FF  ................
0830:  FF FF FF FF FF FF FF FF-FF FF FF FF FF FF FF FF  ................
m>@ 0x800   ; Call it again from the command line
Hello, World!
m>                                                                              

We could get rid of the setup notes, but it's OK this way as well. There are a couple ways of coding to eliminate the need to manually .INCLUDE Addresses328.Def, or Addresses1284.Def, or Addresses2560.Def, however. The first is by using the subroutine index. This would take replacing two lines and adding two, as shown below:

The second way is with a sequence of .IF conditional assembly statements. It is shown in the multiple ".IF PROCESSOR == ..." lines in the Sequencer and Secret Password examples.

SoftVector:                          ; MIRTOS system routine at FLASH address 0

.ORG 0x800

.INCLUDE "Indices.Def"               ; Replaced "Addresses328"
.INCLUDE "HelloWorld.Ptr"

;   Setup for the PrintFLASHASCIIz() call:
; Passed:  R25:R24 CHAR * szPtrBuffer pointer to the string to print
; Returns: R25:R24 CHAR * szPtrBuffer pointer to the string, unmodified
;              R22 BYTE   bPrintCount number of BYTEs placed in the print queue
; Alters:  R20 and the FLAGs, in addition to R22, R24 and R25 returned
HelloWorld:
   LDI     R25, AHelloWorldString >> 8
   LDI     R24, AHelloWorldString & 0xFF
   LDI     R30, I_PrintFLASHASCIIz   ; This is the line added
   CALL    ASoftVector               ; This invokes the indexed call
   RET

HelloWorldString:  .STRING "Hello, World!\r\n"

Finally, our subroutine's code could be trimmed by 2 BYTEs by changing the end of it so that the PrintFLASHASCIIz routine returns directly to the caller. This is done by simply changing the CALL and immediate RET to a JMP. Everything else remains the same.

HelloWorld:
   LDI     R25, AHelloWorldString >> 8
   LDI     R24, AHelloWorldString & 0xFF
   LDI     R30, I_PrintFLASHASCIIz   ; This is the line added
   JMP     ASoftVector               ; This invokes the indexed jump