When loading and storing data, there are several addressing methods available for use. The arduino’s AVR microcontroller supports 13 address modes for accessing the Program memory (Flash) and Data memory (SRAM, Register file, I/O Memory, and Extended I/O Memory). Six modes use “direct addressing”, and as such are very basic. The direct modes are generally inherent in the assembly instruction. The good news is that, we covered all six in past tutorials, so there is no need to address them here (pun intended). Four additional modes incorporate indirect addressing, and will be the focus of this tutorial.
*Register Direct, Single Register-
*Register Direct, Two Registers
Data Indirect w/Displacement
Data Indirect w/Pre-Decrement
Data Indirect w/Post-Increment
Program Memory Constant
Program Memory w/Post-Inc
* denotes previously covered.
Indirect addressing can be said to involve “pointers”. In the C language, the word “pointer” scares people. Hopefully we can calm these irrational fears, by coding an assortment of string routines using simple indirect addressing modes. By the end of this tutorial, we should have a good basis for a library of string functions.
X, Y AND Z
The six registers, r26 through r31 can be paired together and referenced using the letters X, Y and Z. (the X register is r27:r26, the Y register is r29:r28, and the Z register is r31:r30). When combined, these registers are 16-bit “address pointers” for indirect addressing of the data space. In use, the X,Y and Z register pairs are loaded with an address of interest.
The three indirect address registers X, Y, and Z are defined as described here:
Previously we used the LDS instruction to load the value stored inside SRAM memory. For example, this code loads the number 42 into register r24:
volatile uint8_t x=42; asm ( "lds r24, (x) \n" );
But with the X, Y and Z pointer registers, we load the SRAM address into the register pairs (not the value stored there). Hence, we use the the term “indirect addressing”. For example, the following code loads the “address” of the string, (src) into the X register pair via the constraint, “x” (src). When we want the first character of the string, or as in this case, ‘a’, we load it “indirectly” from the X register pair (address) like so:
const char src = "abc"; asm ( "ld __tmp_reg__, X \n" : : "x" (src) );
Fetch Me Z Pointer
Here is an example directly out of the AVR Inline Assembler Cookbook involving a true C-pointer. In this code snippet, ptr is a pointer to variable number. The ‘e’ constraint requests that ptr (which is the address of variable number) be loaded into one of the X, Y or Z register pairs, at the assembler’s choice.
Then, the value at the “address” inside the pointer register pair (or 0x11) is loaded into the temporary register (__tmp_reg__). It is incremented, and finally stored back through the pointer ptr into the variable number. At the completion of this inline code, number = 0x12, and of course, the value of ptr hasn’t changed.
volatile uint8_t number=0x11, *ptr = &number; asm volatile( "ld __tmp_reg__, %a0 \n" "inc __tmp_reg__ \n" "st %a0, __tmp_reg__ \n" : : "e" (ptr) : "memory" );
If you have don’t have a good grasp of C pointers, this could be slightly confusing. It might be helpful to examine the assembler code produced to see exactly what is happening here (note the compiler selected the Z register pair for the pointer, ptr):
0000029E e0.91.00.01 LDS R30, 0x0100 //load address into ptr (0x0102) 000002A0 f0.91.01.01 LDS R31, 0x0101 000002A2 00.80 LDD R0, Z+0 //load number into r0 (0x11) 000002A3 03.94 INC R0 //increment r0 to 0x12 000002A4 00.82 STD Z+0, R0 //store back into number (0x0102) Address locations: ptr: 0x0102 uint8_t* @0x0100 p: 0x11 uint8_t @0x0102
How Long is a String?
Now, onto strings. The following code calculates the length of the string str, not including the terminating NUL, or ‘\0’ character. It places the number of characters inside str into len:
const char src = "abc"; volatile uint8_t len; asm ( "_loop: \n" "ld __tmp_reg__, Z+ \n" "tst __tmp_reg__ \n" "brne _loop \n" //Z points one character past the terminating NUL "subi %A1, 1 \n" //subtract post-increment "sbci %B1, 0 \n" "sub %A1, %A2 \n" //length = end - start "sbc %B1, %B2 \n" "mov %0, %A1 \n" //save len (uint8_t) : "=r" (len) : "z" (src), "x" (src) : "memory" );
First, notice we define input constraints for the string (str) twice, using both X and Z pairs. These constraints place the address of the string inside of the r30:r31 and r26:r27 register pairs. The reason for this will become clear in a moment.
Studying the code further, notice we load the first character of the string (pointed to by the “Z” register), placing it into the temp register (__tmp_reg__). Further, take note that the instruction has a plus sign ‘+‘ appended to the ‘Z’. This means the Z register is incremented by 1 after the load operation. It’s as if we combine two instructions into one! This is termed “Indirect Addressing with Post-Increment”.
Next, the temp register is tested (tst __tmp_reg__), and if it is NOT zero, execution will loop back and fetch another character. This repeats until finding the NUL character at the end of the string. This terminates the loop, however at this point, because of the post-increment operation, the Z register points one location past the end of the string.
We complete the routine by subtracting 1 for extra post-increment, and then subtract the ending string address from the start address. The result of this math is the length of the string.
Here is a slightly more efficient version, but I will leave it to you to determine the details of the shortened arithmetic (the embedded comment explains the math in cryptic fashion):
const char src = "abc"; volatile uint8_t len; asm ( "_loop: \n" "ld __tmp_reg__, Z+ \n" "tst __tmp_reg__ \n" "brne _loop \n" //len=Z - 1 – src = (-1 - src) + Z = ~src + Z "com %A2 \n" "com %B2 \n" "add %A2, %A1 \n" "adc %B2, %B1 \n" : "=r" (len) : "z" (src), "x" (src) );
Zerox a String
Lets do another one. This code copies the src string (including the terminating NUL character) to the array pointed to by dst. However, the strings may not overlap, and the dst string must be large enough to receive the copy. If the destination string is not large enough, anything could happen…
const char src = "abc"; char dst = " "; asm ( "_copy: \n" "ld __tmp_reg__, Z+ \n" //load tmp reg w/src char "st X+, __tmp_reg__ \n" //store tmp reg to dst "tst __tmp_reg__ \n" //check if 0 (end) "brne _copy \n" : : "x" (dst) , "z" (src) );
Wow, only 4-lines of inline assembly code can copy a string! As you can see, this is very straight forward and quite simple. It utilizes the X and Z register pairs, incorporating post-increment addressing with both.
Who’s String is Bigger?
This code compares the two strings s1 and s2. It returns an integer (in result) less than, equal to, or greater than zero if s1 is found to be less than, to match, or be greater than s2. Again, it utilizes the X and Z register pairs, incorporating post-increment addressing with both. Hopefully you are starting to recognize the power of “indirect addressing” combined with “post-indexing”.
char s1 = "abc"; char s2 = "xyz"; volatile int16_t result; asm ( "_compare: \n" "ld %A0, X+ \n" "ld __tmp_reg__, Z+ \n" "sub %A0, __tmp_reg__ \n" "cpse __tmp_reg__, __zero_reg__\n" "breq _compare \n" "sbc %B0, %B0 \n" : "=&r" (result) : "x" (s1) , "z" (s2) );
This code appends the src string to the dst string overwriting the NUL character at the end of dst, and then adds a terminating NUL character. The strings may not overlap, and the dst string must have enough space for the result. This example is slightly more involved, but with a little study the details should become clear.
const char src = "def"; char dst = "abc"; asm ( "_dst: \n" //find end of destination "ld __tmp_reg__, X+ \n" "tst __tmp_reg__ \n" "brne _dst \n" "sbiw %A0, 1 \n" //undo post-increment "_src: \n" //X==end of dst string "ld __tmp_reg__, Z+ \n" //copy src to dst "st X+, __tmp_reg__ \n" "tst __tmp_reg__ \n" //test for 0 (end) "brne _src \n" : : "x" (dst), "z" (src) : "memory" );
Finally, this code finds the first occurrence of the character val in the string src. Here “character” means “byte” (no wide or multi-byte characters allowed). The location of the matched character is placed in a pointer (c) or a NUL if the character is not found.
const char s = "abc", *c; volatile int16_t val = 0x63; asm ( "_loop: \n" "ld %A0, Z+ \n" //fetch char from string "cp %A0, %A2 \n" //compare char with val "breq _found \n" "tst %A0 \n" //end of string (0)? "brne _loop \n" //not at end "clr %B0 \n" //not found, NULL pointer "rjmp _end \n" "_found: \n" "sbiw %A1, 1 \n" //undo post-increment "movw %A0, %A1 \n" //save pointer "_end: \n" : "=x" (c) : "z" (s), "r" (val) );
C Programming and Strings
Further information on addressing modes can be found in Section 2 of the AVR Instruction Set Manual
AVR 8-bit Instruction Set
AVR-GCC Inline Assembler Cookbook
Extended Asm – Assembler Instructions with C Expression Operands
AVRLibc String Functions
Also available as a book, with greatly expanded coverage!