Chapter 6 Constants and Literal Pools
6.5 Loading Addresses into Registers
At some point, you will need to load the address of a label or symbol into a register.
Usually you do this to give yourself a starting point of a table, a list, or maybe a set of coefficients that are needed in a digital filter. For example, consider the ARM7TDMI code fragment below.
SRAM_BASE EQU 0x04000000 AREA FILTER, CODE
dest RN0 ; destination pointer image RN1 ; image data pointer coeff RN2 ; coefficient table pointer pointer RN3 ; temporary pointer
ENTRY CODE32 Main
; initialization area
LDR dest, =#SRAM_BASE ; move memory base into dest MOV pointer, dest ; current pointer is destination ADR image, image_data ; load image data pointer
ADR coeff, cosines ; load coefficient pointer BL filter ; execute one pass of filter .
. . ALIGN image_data
DCW 0x0001,0x0002,0x0003,0x0004 DCW 0x0005,0x0006,0x0007,0x0008 .
. . cosines
DCW 0x3ec5,0x3537,0x238e,0x0c7c DCW 0xf384,0xdc72,0xcac9,0xc13b .
. . END
While the majority of the program is still to be written, you can see that if we were to set up an algorithm, say an FIR filter, where you had some data stored in memory and some coefficients stored in memory, you would want to set point- ers to the start of each set. This way, a register would hold a starting address. To access a particular data value, you would simply use that register with an offset of some kind.
We have seen the directives EQU and RN already in Chapter 4, but now we actu- ally start using them. The first line equates the label SRAM_BASE to a number, so that when we use it in the code, we don’t have to keep typing that long address,
similar to the #DEFINE statement in C. The RN directives give names to our reg- isters r0, r1, r2, and r3, so that we can refer to them by their function rather than by their number. You don’t have to do this, but often it’s helpful to know a register’s use while programming. The first two instructions load a known address (called an absolute address, since it doesn’t move if you relocate your code in memory) into registers r0 and r3. The third and fourth instructions are the pseudo-instruction ADR, which is particularly useful at loading addresses into a register. Why do it this way? Suppose that this section of code was to be used along with other blocks.
You wouldn’t necessarily know exactly where your data starts once the two sections are assembled, so it’s easier to let the assembler calculate the addresses for you. As an example, if image_data actually started at address 0x8000 in memory, then this address gets moved into register r1, which we’ve renamed. However, if we change the code, move the image data, or add another block of code that we write later, then this address will change. By using ADR, we don’t have to worry about the address.
EXAMPLE 6.4
Let’s examine another example, this time to see how the ADR pseudo-instruction actually gets converted into real ARM instructions. Again, the code in this exam- ple doesn’t actually do anything except set up pointers, but it will serve to illustrate how ADR behaves.
AREA adrlabel,CODE,READONLY
ENTRY ; mark first instruction to execute Start BL func ; branch to subroutine
stop B stop ; terminate
LTORG ; create a literal pool
func ADR r0, Start ; => SUB r0, PC, #offset to Start ADR r1, DataArea ; => ADD r1, PC, #offset to DataArea ;ADR r2, DataArea + 4300 ; This would fail because the offset ; cannot be expressed by operand2 of ADD ADRL r2, DataArea + 4300 ; => ADD r2, PC, #offset1
; ADD r2, r2, #offset2
BX lr ; return
DataArea
SPACE 8000 ; starting at the current location,
; clears an 8000-byte area of memory to 0
END
You will note that the program calls a subroutine called func, using a branch and link operation (BL). The next instruction is for ending the program, so we really only need to examine what happens after the LTORG directive. The sub- routine begins with a label, func, and an ADR pseudo-instruction to load the starting address of our main program into register r0. The assembler actually creates either an ADD or SUB instruction with the Program Counter to do this.
Similar to the LDR pseudo-instruction we saw previously, by knowing the value of the Program Counter at the time when this ADD or SUB reaches the execute stage of the pipeline, we can simply take that value and modify it to generate an address. The catch is that the offset must be a particular type of number. For ARM instructions, that number must be one that can be created using a byte value rotated by an even number of bits, exactly as we saw in Section 6.2 (if rejected by the assembler, it will generate an error message to indicate that an offset
cannot be represented by 0–255 and a rotation). For 32-bit Thumb instructions, that number must be within ±4095 bytes of a byte, half-word, or word-aligned address. If you notice the second ADR in this example, the distance between the instruction and the label DataArea is small enough that the assembler will use a simple ADD instruction to create the constant.
The third ADR tries to create an address where the label is on the other side of an 8000-byte block of memory. This doesn’t work, but there is another pseudo-instruction: ADRL. Using two operations instead of one, the ADRL will calculate an offset that is within a range based on the addition of two values now, both created by the byte rotation scheme mentioned above (for ARM instructions). There is a fixed range for 32-bit Thumb instructions of ±1MB. You should note that if you invoke an ADRL pseudo-instruction in your code, it will generate two operations even if it could be done using only one, so be careful in loops that are sensitive to cycle counts. One other important point worth mentioning is that the label used with ADR or ADRL must be within the same code section. If a label is out of range in the same section, the assembler faults the reference. As an aside, if a label is out of range in other code sections, the linker faults the reference.
There is yet another way of loading addresses into registers, and it is exactly the same as the LDR pseudo-instruction we saw earlier for loading constants. The syntax is
LDR <Rd>, =label
In this instance, the assembler will convert the pseudo-instruction into a load instruction, where the load reads the address from a literal pool that it creates. As with the case of loading constants, you must ensure that a literal pool is within range of the instruction. This pseudo-instruction differs from ADR and ADRL in that labels outside of a section can be referenced, and the linker will resolve the reference at link time.
EXAMPLE 6.5
The example below shows a few of the ways the LDR pseudo-instruction can be used, including using labels with their own offsets.
AREA LDRlabel, CODE, READONLY
ENTRY ; Mark first instruction to execute start
BL func1 ; branch to first subroutine BL func2 ; branch to second subroutine stop B stop ; terminate
func1
LDR r0, =start ;=> LDR R0, [PC, #offset into Literal Pool 1]
LDR r1, =Darea + 12 ;=> LDR R1, [PC, #offset into Lit. Pool 1]
LDR r2, =Darea + 6000 ;=> LDR R2, [PC, #offset into Lit. Pool 1]
BX lr ; return
LTORG func2
LDR r3, =Darea + 6000 ; => LDR R3, [PC, #offset into Lit. Pool 1]
; (sharing with previous literal)
; LDR r4, =Darea + 6004 ; if uncommented produces an error
; as literal pool 2 is out of range
BX lr ; return
Darea
SPACE 8000 ; starting at the current location, clears
; an 8000-byte area of memory to zero END ; literal pool 2 is out of range of the LDR
; instructions above
You can see the first three LDR statements in the subroutine func1 would actually be PC-relative loads from a literal pool that would exist in memory at the LTORG statement. Additionally, the first load statement in the second subroutine could use the same literal pool to create a PC-relative offset. As the SPACE directive has cleared an 8000-byte block of memory, the second load instruction cannot reach the second literal pool, since it must be within 4 kilobytes.
So to summarize:
Use the pseudo-instruction ADR <Rd>, label
to put an address into a register whenever possible. The address is created by adding or subtracting an offset to/from the PC, where the offset is calcu- lated by the assembler.
If the above case fails, use the ADRL pseudo-instruction, which will calcu- late an offset using two separate ADD or SUB operations. Note that if you invoke an ADRL pseudo-instruction in your code, it will generate two oper- ations even if it could be done using only one.
Use the pseudo-instruction LDR <Rd>, =label
if you plan to reference labels in other sections of code, or you know that a literal table will exist and you don’t mind the extra cycles used to fetch the literal from memory. Use the same caution with literal pools that you would for the construct
LDR <Rd>, =constant
Consult the Assembler User’s Guide (ARM 2008a) for more details on the use of ADR, ADRL and LDR for loading addresses.