Tutorials: Microcontroller systems (MICSY)

Addressing modes

last updated: 2021-05-18

Quick links to the subchapters

Song of this chapter: Ben Platt > Sing To Me Instead > Share your address

When loading and storing data, there are several addressing methods available in Assembly language. The AVR microcontroller support 13 different address modes for accessing the Flash and SRAM.

We distinguish the following addressing modes:

Addressing the data memory (SRAM)

Immediate addressing

The operands (register and constant values) are part of the instruction. These instructions have often an i for immediate in their opcode and are used with the working registers r16-r31.


    ldi   r18,0xA3        ; load immediate 0xA3 (163) to r18
    andi  r24,254         ; logical and immediate with mask 0b11111110
    subi  r25,0b00001111  ; subtract immediate r25 = r25 - 15

addressing immediate

Direct addressing

Direct addressing of the working registers (one or two register)
    inc    r23      ; r23 = r23 + 1
    com    r16      ; one's complement of r16
    mov    r5,r20   ; copy content of r20 to r5
    sub    r5,r20   ; r5 = r5 - r20
    cp     r18,r22  ; compare r18 with r22 (same as sub)

addressing direct 1 reg

addressing direct 2 reg.

Direct addressing of special purpose register

We have six different instructions to access the 64 SPR:

    in    r19,0x09       ; read PIND to r19 (0x09 = address of PIND)
    out   PORTD,r20
    sbi   PORTD,4        ; set bit 4 in SPR PORTD
    cbi   0x05,PB7       ; clear bit 7 in PORTD (0x05 address PORTB, PB7 = 7)
    sbic  PINA,2         ; skip if bit in register cleared
    sbis  0x03,PB3       ; skip if bit in register set

addressing direct SPR

Direct addressing of the data memory

Only two instructions (lds (load direct from data space) and sts (store direct to data space)) allow direct addressing of the SRAM:

    sts   0x0100,r18    ; store content of r18 direct to SRAM
    lds   r16,0x0AAA    ; load value of SRAM address 0x0AAA to r16

addressing direct SRAM

Indirect addressing

Indirect addressing of the SRAM

When using an indirect addressing mode, the address is not constant but variable!

The address can be changed, because it is located in a register. It can be easily e.g. incremented in a loop to access all the bytes in a table. The AVR controller have 6 working register that can be used as 16 bit register to hold the 16 bit addresses. They are called in X (r26:r27), Y (r28:r29) and Z (r31:r30) in AVR Assembly language.

Before using indirect addressing in Assembly language, the address pointer must be initialised!

Here an example:

    ldi  XL,0x02    ;initialise X with the address 0x0102
    ldi  XH,0x01

With the help of indirect addressing we can address all bytes of the SRAM (GPR, SPR, data and stack)!

Two instructions (ld (load) and st(store)) allow direct addressing of the SRAM:

    st  X,r17       ; store content of r17 to data space (address in X)
    ld  r16,Y       ; load content from data space to r16 (address in Y)

addressing direct SRAM

Indirect addressing of the SRAM with with post-increment or pre-decrement

There are 12 convenient instructions to automatically increment or decrement the address pointer when storing or loading. Here two examples:

    st  Y+,r16  ; store content of r16 to SRAM (address in Y) and then increment
                ; the address pointer Y (Y=Y+1)
    ld  r16,-X  ; first decrement the address pointer X (X=X-1) and the load
                ; the content of the data memory (address in Y) to r16
Indirect addressing of the SRAM with constant displacement

Other convenient instructions allow to add a constant displacement (0-63) to the address pointer, and so simplifying the access of tables with data sets.

Here two examples:

    std  Y+25,r16  ; store the content of r16 to the data memory
                   ; the address is the sum of the address pointer and a
                   ; constant (here 25)
    ldd  r17,Y+12  ; load the content of the SRAM-address (Y+12) to r17
Indirect addressing of the stack with push and pop.

As seen in the previous chapter, the address pointer is called stack pointer and resides in the SRAM SPR data range at the addresses 0x3D for SPL, and 0x3E for SPH. The stack pointer must be initialised, but is for the rest administrated by the hardware.

Addressing the program memory (Flash)

We distinguish:

Let's take a closer look at the last point.

Indirect addressing of constants in the Flash

As the Flash is a read only memory, we have only instructions to load data from Flash to a working register lpm (load program memory). The indirect address has to be stored in Z. The addressing will be byte-wise!, so the Flash address has to be multiplied by 2.

Initializing the Z-pointer:

    ldi     ZL,LOW(TAB*2)   ; initialize the address pointer with the
    ldi     ZH,HIGH(TAB*2)  ;  Flash address (TAB) * 2

Get data from Flash:

    lpm    r20,Z   ; load the content from Flash (address in Z) to r20
    lpm    r20,Z+  ; load from Flash (address in Z) to r20 and increment address

addressing direct SRAM

Indirect addressing in Arduino

Dynamic memory allocation

In other high-level programming languages then C (e.g. java) it is normal to create objects at runtime, meaning during the running of the program. The garbage collector will free the memory automatically when it is no more needed by the program. C has no such garbage collector, so dynamic memory allocation must be done manually. Because this is prone to error, and because of our very limited memory, dynamic memory allocation is not recommended when programming in Arduino.

Static memory allocation

We have already used arrays in our exercises. Here four examples of creating a static allocated array;

byte my_1_array[] = {1,2,3};
char my_2_array[] = "Hello"; // C-string 0 terminated
char * my_3_array = "Text";  // C-string 0 terminated
byte my_4_array[5];
"Just do it" AM1:
    ------------- different_arrays -----------------------
    *** byte my_1_array[] = {1,2,3}; ***

    sizeof(my_1_array):                             3
    pointer -> Serial.println(int(my_1_array),HEX): 106
    my 1. array:                                    1 2 3

    *** char my_2_array[] = "Hello"; // C-string ***

    sizeof(my_2_array):                             6
    string size -> strlen(my_2_array):              5
    pointer -> Serial.println(int(my_2_array),HEX): 100
    my 2. array:                                    Hello0

    *** char * my_3_array = "Text";  // C-string ***

    pointer size! -> sizeof(my_3_array):            2
    pointer -> Serial.println(int(my_3_array),HEX): 31C
    string size -> strlen(my_3_array):              4
    my 3. array:                                    Text0

    *** byte my_4_array[5]; ***

    sizeof(my_4_array):                             5
    pointer -> Serial.println(int(my_4_array),HEX): 4B0
    my 4. array:                                    2 5 0 0 7

In the third array (my_3_array) we initialize only a pointer (address register) to the array. Because we assign a text to the pointer and use the char type, the compiler knows, a null terminated C-string is needed and reserves the corresponding memory before runtime. We could also initialize the pointer without assignment:

    byte * my_5_array; // pointer, but memory not allocated!

Now the compiler does not know the size of the memory to allocate at compile time and we have to do it later in the program (runtime) before using the array with the malloc() function:

    my_5_array = (byte *)malloc(5);
    my_5_array[2] = 22;
    my_5_array[4] = 99;
"Just do it" AM2:
    *** byte * my_5_array; // memory not allocated! ***

    pointer size! -> sizeof(my_5_array):            2
    pointer -> Serial.println(int(my_5_array),HEX): 563
    my 5. array:                                    5 0 22 5 99
"Just do it" AM3:

Interesting links