The assembler syntax in a bit more detail #
Now we’ve got a working assembler, it’s worth taking a step back and documenting some basics on how to use it. A reasonable overview of addressing modes and instructions can be found in this document from TI, but many other resources are also available. What I want to do here is just cover some of the basics that are useful for using the assembler with Mecrisp Forth.
The shape of a code word #
From the previous article in this series, we created a way for the e4thcom resident assembler to assemble instructions from the code between code and end-code, remembering it must be included from a source file, not typed directly into the interpreter.
code add1 ( n -- n+1 )
#1 sp ) add \ ADD #1, 0(R4)
end-code
Because the source between code and end-code is interpreted as forth in the e4thcom editor, the instruction comes after the SRC and DST operands. This word then emits the actual hex code to be sent to the dictionary entry for our new word, much like our original hand-crafted version in the previous article.
Registers #
Mecrisp assigns most of the MSP430’s 16 registers specific jobs and the assembler has names for them:
| Name | Reg | Role |
|---|---|---|
pc |
R0 | program counter |
rp |
R1 | return-stack pointer (the hardware SP) |
sr |
R2 | status register |
cg |
R3 | constant generator |
sp |
R4 | data stack pointer — points at TOS |
index |
R5 | loop index |
limit |
R6 | loop limit |
w |
R7 | free scratch register |
r08–r15 |
R8–15 | general (R8/R9 free; R10–15 must be preserved) |
sp is probably the most important as this is a pointer to the top of the Forth data stack. Remember that the forth interpreter is using the CPU program counter and return stack to manage the flow of execution, so the data stack is where all the arguments and return values are passed.
Re-examining our previous example,
code add1 ( n -- n+1 )
#1 sp ) add \ ADD #1, 0(R4)
end-code
we can see that the sp ) is dereferencing the data stack pointer to get the current top of stack value, and then adding 1 to it. The result is then stored back at the same location on the data stack (the destination argument to the add instruction). This is a good time to introduce the addressing modes that are available in the assembler.
Addressing modes #
Addressing modes are postfix markers you attach to a register or value:
| Word | Signature | Assembly form | Meaning |
|---|---|---|---|
| (bare reg) | ( -- r ) |
sp |
register direct |
) |
( r -- op ) |
sp ) |
register indirect @r |
)+ |
( r -- op ) |
sp )+ |
indirect autoincrement @r+ (post-inc) |
x) |
( off r -- op ) |
2 sp x) |
indexed off(r) |
-) |
( r -- op ) |
rp -) |
destination auto-pre-decrement -(r) (emits a sub first) |
# |
( n -- op ) |
100 # |
immediate #n (appends an extra instruction word) |
& |
( addr -- op ) |
200 & |
absolute address &addr |
.b |
( -- ) |
… .b mov |
byte-size modifier (default is word) |
For the values 0, 1, 2, 4, 8 and −1 there are single-word shorthands, #0 #1 #2 #4 #8 #-1 that use the MSP430’s constant generator and cost no
extra word. #1 sp ) add results in less generated code than
1 # sp ) add.
The indexed and autoincrement modes are what make forth stack juggling natural.
2 sp x) is the second item on the data stack (next-of-stack), because each
cell is two bytes. sp )+ reads the top cell and pops it in one move — the
autoincrement bumps sp past the cell. That single mode is how !cell empties
its arguments:
code !cell ( n addr -- ) \ store n into the cell at addr
sp )+ w mov \ pop addr into w
sp )+ w ) mov \ pop n into mem[w]
end-code
Add .b before the opcode for byte-mode instead of the default 16-bit word:
BIT0 # P1OUT & .b bis sets one GPIO bit.
The instruction set #
We’ve only used the add instruction so far, but the msp430 has more in a small but powerful instruction set. It’s outside the scope of this article to go into detail on all the instructions, but below is a summary of the instructions available and how they are encoded by the e4thcom assembler.
Two-operand instructions — <src> <dst> op ⇒ dst = dst OP src
#
| Word | Signature | Purpose |
|---|---|---|
mov |
( src dst -- ) |
dst ← src |
add |
( src dst -- ) |
dst += src |
addc |
( src dst -- ) |
add with carry |
sub |
( src dst -- ) |
dst -= src |
subc |
( src dst -- ) |
subtract with borrow |
cmp |
( src dst -- ) |
flags for comparison (no store) |
dadd |
( src dst -- ) |
BCD add with carry |
bit |
( src dst -- ) |
test dst & src (flags only) |
bic |
( src dst -- ) |
dst &= ~src (bit clear) |
bis |
( src dst -- ) |
dst |= src (bit set) |
and> / bia |
( src dst -- ) |
dst &= src |
xor> / bix |
( src dst -- ) |
dst ^= src |
and> and xor> avoids a clash with Forth’s own and/xor;
bia and bix are aliases for the same two instructions.
Single-operand instructions — <operand> op
#
| Word | Signature | Purpose |
|---|---|---|
rrc |
( op -- ) |
rotate right through carry |
rra |
( op -- ) |
rotate right arithmetic (÷2 signed) |
swpb |
( op -- ) |
swap the two bytes |
sxt |
( op -- ) |
sign-extend byte → word |
push |
( op -- ) |
push onto return stack (rp) |
call |
( op -- ) |
call subroutine |
No-operand instructions / macros #
| Word | Signature | Purpose |
|---|---|---|
ret |
( -- ) |
return (@rp+ → pc) |
reti |
( -- ) |
return from interrupt |
next |
( -- ) |
Mecrisp end-of-word (defined as ret) |
setc / clrc |
( -- ) |
set / clear carry |
eint / dint |
( -- ) |
enable / disable interrupts (GIE) |
Loops and branches #
The assembler is structured, not label-based. Instead of naming targets, you
push a condition and close it with a matching word, exactly like Forth’s own
control flow but with a trailing comma: if, … then,, begin, … until,,
begin, … again,, and begin, … while, … repeat,. The loop-top “label” lives
on the assembler’s stack — begin, pushes it, until,/again, resolve it.
Condition codes (push a code for if, / until, / while,)
#
Place a condition after a cmp / bit / arithmetic op and before if,,
until,, or while,. Read left-to-right on the operands as written —
after a b cmp, >? means a > b .
| Word | Opcode | Flag | Loops / runs when… | After a b cmp |
|---|---|---|---|---|
=? / 0=? |
2000 | Z = 1 | equal / result zero | a = b |
<>? / 0<>? |
2400 | Z = 0 | not equal / non-zero | a ≠ b |
>? |
3400 | signed | signed greater | a > b (signed) |
<eq? |
3800 | signed | signed less-or-equal | a ≤ b (signed) |
u>? |
2C00 | unsigned | unsigned greater | a > b (unsigned) |
u<eq? |
2800 | unsigned | unsigned less-or-equal | a ≤ b (unsigned) |
pos? |
3000 | N = 0 | result non-negative | a − b ≥ 0 |
cs? |
2800 | C = 1 | carry set | — (raw carry) |
cc? |
2C00 | C = 0 | carry clear | — (raw carry) |
never |
3C00 | — | never (used by again, / ahead,) |
— |
How they attach to the control words:
<cmp> <cond> if, … then, \ run body when cond true
begin, … <cmp> <cond> until, \ loop until cond true (test at bottom)
begin, <cmp> <cond> while, … repeat, \ exit when cond false (test at top)
Here’s a counting loop, summing the integers from n down to 1. It accumulates in the scratch register w, decrements the counter in place, and exits the moment the counter hits zero:
code sumn ( n -- sum )
#0 w mov \ w = accumulator
begin,
sp ) w add \ w += current counter
#1 sp ) sub \ counter-- (sets Z flag when it reaches 0)
0=? until, \ exit when counter hits 0
w sp ) mov \ store the result back to TOS
end-code
The sub instruction sets the zero flag as a side effect, so 0=? until, needs no separate compare. Using an instruction like this that sets a flag just before a condition saves an explicit comparison instruction.
For a mid-loop exit, while, sits inside the loop and bails when its condition
fails; repeat, jumps back to begin,. And if, … then, gives you a plain
branch, as in this max that copies the larger of two stack items down:
code max ( a b -- max )
sp ) 2 sp x) cmp \ compare TOS (b) against NOS (a)
>? if, \ if b > a ...
sp ) 2 sp x) mov \ copy b into a's slot
then,
#2 sp add \ drop TOS; the larger value remains
end-code
Note the comment here on drop being implemented as #2 sp add, in Mecrisp Forth, the data stack grows downwards, so adding 2 to the stack pointer effectively drops the top two bytes (one cell) from the stack.
Ok, we’ve covered a lot of the basic syntax and shown a couple of simple examples. In the next article, we’ll look at a real-world example of using the assembler in an interrupt service routine to implement switch de-bouncing.