Programming with Ophis | ||
---|---|---|
<<< Previous | Next >>> |
This essay discusses the machine language equivalents of the basic "structured programming" concepts that are part of the "imperative" family of programming languages: if/then/else, for/next, while loops, and procedures. It also discusses basic use of variables, as well as arrays, multi-byte data types (records), and sub-byte data types (bitfields). It closes by hand-compiling pseudo-code for an insertion sort on linked lists into assembler. A complete Commodore 64 application is included as a sample with this essay.
This is almost the most basic control construct. The most basic is if x then y, which is a simple branch instruction (bcc/bcs/beq/bmi/bne/bpl/bvc/bvs) past the "then" clause if the conditional is false:
iny bne no'overflow inx no'overflow: ;; rest of code |
This increments the value of the y register, and if it just wrapped back around to zero, it increments the x register too. It is basically equivalent to the C statement if ((++y)==0) ++x;. We need a few more labels to handle else clauses as well.
;; Computation of the conditional expression. ;; We assume for the sake of the example that ;; we want to execute the THEN clause if the ;; zero bit is set, otherwise the ELSE ;; clause. This will happen after a CMP, ;; which is the most common kind of 'if' ;; statement anyway. BNE else'clause ;; THEN clause code goes here. JMP end'of'if'stmt else'clause: ;; ELSE clause code goes here. end'of'if'stmt: ;; ... rest of code. |
A free loop is one that might execute any number of times. These are basically just a combination of if and goto. For a "while x do y" loop, that executes zero or more times, you'd have code like this...
loop'begin: ;; ... computation of condition, setting zero ;; bit if loop is finished... beq loop'done ;; ... loop body goes here jmp loop'begin loop'done: ;; ... rest of program. |
If you want to ensure that the loop body executes at least once (do y while x), just move the test to the end.
loop'begin: ;; ... loop body goes here ;; ... computation of condition, setting zero ;; bit if loop is finished... bne loop'begin ;; ... rest of program. |
The choice of zero bit is kind of arbitrary here. If the condition involves the carry bit, or overflow, or negative, then replace the beq with bcs/bvs/bmi appropriately.
A special case of loops is one where you know exactly how many times you're going through it—this is called a bounded loop. Suppose you're copying 16 bytes from $C000 to $D000. The C code for that would look something like this:
int *a = 0xC000; int *b = 0xD000; int i; for (i = 0; i < 16; i++) { a[i] = b[i]; } |
C doesn't directly support bounded loops; its for statement is just "syntactic sugar" for a while statement. However, we can take advantage of special purpose machine instructions to get very straightforward code:
ldx #$00 loop: lda $c000, x sta $d000, x inx cpx #$10 bmi loop |
However, remember that every arithmetic operation, including inx and dex, sets the various flags, including the Zero bit. That means that if we can make our computation end when the counter hits zero, we can shave off some bytes:
ldx #$10 loop: lda #$bfff, x sta #$cfff, x dex bne loop |
Notice that we had to change the addresses we're indexing from, because x takes a slightly different range of values. The space savings is small here, and it's become slightly more unclear. (It also hasn't actually saved any time, because the lda and sta instructions are crossing a page boundary where they weren't before—but if the start or end arrays began at $b020 or something this wouldn't be an issue.) This tends to work better when the precise value of the counter isn't used in the computation—so let us consider the NES, which uses memory location $2007 as a port to its video memory. Suppose we wish to jam 4,096 copies of the hex value $20 into the video memory. We can write this very cleanly, using the X and Y registers as indices in a nested loop.
ldx #$10 ldy #$00 lda #$20 loop: sta $2007 iny bne loop dex bne loop |
Work through this code. Convince yourself that the sta is executed exactly 16*256 = 4096 times.
This is an example of a nested loop: a loop inside a loop. Since our internal loop didn't need the X or Y registers, we got to use both of them, which is nice, because they have special incrementing and decrementing instructions. The accumulator lacks these instructions, so it is a poor choice to use for index variables. If you have a bounded loop and don't have access to registers, use memory locations instead:
lda #$10 sta counter ; loop 16 times loop: ;; Do stuff that trashes all the registers dec counter bne loop |
That's it! These are the basic control constructs for using inside of procedures. Before talking about how to organize procedures, I'll briefly cover the way the 6502 handles its stack, because stacks and procedures are very tightly intertwined.
<<< Previous | Home | Next >>> |
16-bit comparisons | Up | The stack |