Creation and addressing arrays in AVR Assembly (Using the ATMega8535)

13,360

Solution 1

If your array is read-only, you do not need to copy it to RAM. You can keep it in Flash and read it from there when needed. This will save you precious RAM, at the cost of slower access (read from RAM is 2 cycles, read from flash is 3 cycles).

You can declare your array like this:

.global my_array
.type   my_array, @object
my_array:
    .byte 12, 34, 56, 78

Then, to read a member of the array, you have to compute:

adress of member = array base address + member index

If your members were more than one byte, you would have to also multiply the index by the size, but this is not the case here. Then, you put the address of the required member in the Z register and issue an lpm instruction. Here is a function implementing this logic:

.global read_data
; input:    r24 = array index, r1 = 0
; output:   r24 = array value
; clobbers: r30, r31
read_data:
    ldi r30, lo8(my_array)  ; load Z = address of my_array
    ldi r31, hi8(my_array)  ; ...high byte also
    add r30, r24            ; add the array index
    adc r31, r1             ; ...and add 0 to propagate the carry
    lpm r24, Z
    ret

@scottt advised you to first write in C, then look at the generated assembly. I consider this very good advice, let's follow it:

#include <stdint.h>

__flash const uint8_t my_array[] = {12, 34, 56, 78};

uint8_t read_data(uint8_t index)
{
    return my_array[index];
}

The __flash keyword identifying a “named address space” is an embedded C extension supported by gcc. The generated assembly is slightly different from the previous one: instead of computing base_address + index, gcc does index − (−base_address):

read_data:
    mov r30, r24                ; load Z = array index
    ldi r31, 0                  ; ...high byte of index is 0
    subi r30, lo8(-(my_array))  ; subtract -(address of my array)
    sbci r31, hi8(-(my_array))  ; ...high byte also
    lpm r24, Z
    ret

This is just as efficient as the previous hand-rolled assembly, except that it does not need the r1 register to be initialized to zero. But keeping r1 to zero is part of the gcc ABI anyway, so it should make no difference.

The role of the linker

This section is meant to answer the question in the comment: how can we access the array if we do not know its address? The answer is: we access it by its name, just like in the code snippets above. Choosing the final address for the array, as well as replacing the name by the appropriate address, is the linker’s job.

Assembling (with avr-gcc -c) and disassembling (with avr-objdump -d) the first code snippet gives this:

my_array.o, section .text:
00000000 <my_array>:
   0:   0c 22 38 4e        ."8N

If we were compiling from C, gcc would have put the array in the .progmem.data section instead of .text, but it makes little difference. The numbers “0c 22 38 4e” are the array contents, in hex. The characters to the right are the ASCII equivalents, ‘.’ being the placeholder for non printing characters.

The object file also carries this symbol table, shown by avr-nm:

my_array.o:
00000000 T my_array

meaning the symbol “my_array” has been defined as referring to offset 0 into the .text section (implied by “T”) of this object.

Assembling and disassembling the second code snippet gives this:

read_data.o, section .text:
00000000 <read_data>:
   0:   e0 e0        ldi r30, 0x00
   2:   f0 e0        ldi r31, 0x00
   4:   e8 0f        add r30, r24
   6:   f1 1d        adc r31, r1
   8:   84 91        lpm r24, Z
   a:   08 95        ret

Comparing the disassembly with the actual source code, it can be seen that the assembler replaced the address of my_array with 0x00, which is almost guaranteed to be wrong. But it also left a note to the linker in the form of “relocation records”, shown by avr-objdump -r:

read_data.o, RELOCATION RECORDS FOR [.text]:
OFFSET   TYPE              VALUE 
00000000 R_AVR_LO8_LDI     my_array
00000002 R_AVR_HI8_LDI     my_array

This tells the linker that the ldi instructions at offsets 0x00 and 0x02 are intended to load the low byte and the high byte (respectively) of the final address of my_array. The object file also carries this symbol table:

read_data.o:
         U my_array
00000000 T read_data

where the “U” line means the file makes use of an undefined symbol named “my_array”.

Linking these pieces together, with a suitable main(), yields a binary containing the C runtime from avr-lbc, together with our code:

0000003c <my_array>:
  3c:   0c 22 38 4e        ."8N

00000040 <read_data>:
  40:   ec e3        ldi r30, 0x3C
  42:   f0 e0        ldi r31, 0x00
  44:   e8 0f        add r30, r24
  46:   f1 1d        adc r31, r1
  48:   84 91        lpm r24, Z
  4a:   08 95        ret

It should be noted that, not only has the linker moved the pieces around to their final addresses, it has also fixed the arguments of the ldi instructions so that they now point to the correct address of my_array.

Solution 2

The code should look something like this:

    .section    .text
    .global main
main:
    ldi r30,lo8(data)
    ldi r31,hi8(data)
    ldd r24,Z+3
    sts output,r24
    ld r24,Z
    sts output,r24
    ldi r24,0
    ldi r25,0
    ret
    .global data
    .data
data:
    .byte   1, 2, 3, 4
    .comm   output,1,1

Explanation

For people who have programmed in assembler using the GNU toolchain before, there are lessons that are transferable even to unfamiliar instruction sets:

  1. You reserve space for an array with the assembler directives .byte 1, 2, 3, 4, .word 1, 2 (.word is 16 bits for AVR) or .space 100.
  2. When learning a new instruction set, write C programs and ask the C compiler to generate assembler output. Find a good assembler programming reference for the instruction set as you read the assembler code.

Applying this trick below.

byte-array.c

/* volatile our code doesn't get optimized out even when compiler optimization is on */
volatile char output;

char data[] = { 1, 2, 3, 4 };

int main(void)
{
    output = data[3];
    output = data[0];
    return 0;
}

Generate Assembler from C

avr-gcc -mmcu=atmega8 -Wall -Os -S byte-array.c

This will generate the assembler file byte-array.s.

byte-array.s

    .file   "byte-array.c"
__SP_H__ = 0x3e
__SP_L__ = 0x3d
__SREG__ = 0x3f
__tmp_reg__ = 0
__zero_reg__ = 1
    .section    .text.startup,"ax",@progbits
.global main
    .type   main, @function
main:
/* prologue: function */
/* frame size = 0 */
/* stack size = 0 */
.L__stack_usage = 0
    ldi r30,lo8(data)
    ldi r31,hi8(data)
    ldd r24,Z+3
    sts output,r24
    ld r24,Z
    sts output,r24
    ldi r24,0
    ldi r25,0
    ret
    .size   main, .-main
.global data
    .data
    .type   data, @object
    .size   data, 4
data:
    .byte   1
    .byte   2
    .byte   3
    .byte   4
    .comm   output,1,1
    .ident  "GCC: (Fedora 4.9.2-1.fc21) 4.9.2"
.global __do_copy_data
.global __do_clear_bss

Read this explanation of Pointer Registers to see how the AVR instruction set uses the r30, r31 register pair as the pointer register Z. Read up on the ld, st, ldi, ldd, sts and std instructions.

Implementation Notes

If you link the program then disassemble it:

avr-gcc -mmcu=atmega8 -Os byte-array.c -o byte-array.elf
avr-objdump -d byte-array.elf

00000000 <__vectors>:
   0:   12 c0           rjmp    .+36        ; 0x26 <__ctors_end>
   2:   2c c0           rjmp    .+88        ; 0x5c <__bad_interrupt>
   4:   2b c0           rjmp    .+86        ; 0x5c <__bad_interrupt>
   6:   2a c0           rjmp    .+84        ; 0x5c <__bad_interrupt>
   8:   29 c0           rjmp    .+82        ; 0x5c <__bad_interrupt>
   a:   28 c0           rjmp    .+80        ; 0x5c <__bad_interrupt>
   c:   27 c0           rjmp    .+78        ; 0x5c <__bad_interrupt>
   e:   26 c0           rjmp    .+76        ; 0x5c <__bad_interrupt>
  10:   25 c0           rjmp    .+74        ; 0x5c <__bad_interrupt>
  12:   24 c0           rjmp    .+72        ; 0x5c <__bad_interrupt>
  14:   23 c0           rjmp    .+70        ; 0x5c <__bad_interrupt>
  16:   22 c0           rjmp    .+68        ; 0x5c <__bad_interrupt>
  18:   21 c0           rjmp    .+66        ; 0x5c <__bad_interrupt>
  1a:   20 c0           rjmp    .+64        ; 0x5c <__bad_interrupt>
  1c:   1f c0           rjmp    .+62        ; 0x5c <__bad_interrupt>
  1e:   1e c0           rjmp    .+60        ; 0x5c <__bad_interrupt>
  20:   1d c0           rjmp    .+58        ; 0x5c <__bad_interrupt>
  22:   1c c0           rjmp    .+56        ; 0x5c <__bad_interrupt>
  24:   1b c0           rjmp    .+54        ; 0x5c <__bad_interrupt>

00000026 <__ctors_end>:
  26:   11 24           eor r1, r1
  28:   1f be           out 0x3f, r1    ; 63
  2a:   cf e5           ldi r28, 0x5F   ; 95
  2c:   d4 e0           ldi r29, 0x04   ; 4
  2e:   de bf           out 0x3e, r29   ; 62
  30:   cd bf           out 0x3d, r28   ; 61

00000032 <__do_copy_data>:
  32:   10 e0           ldi r17, 0x00   ; 0
  34:   a0 e6           ldi r26, 0x60   ; 96
  36:   b0 e0           ldi r27, 0x00   ; 0
  38:   e4 e8           ldi r30, 0x84   ; 132
  3a:   f0 e0           ldi r31, 0x00   ; 0
  3c:   02 c0           rjmp    .+4         ; 0x42 <__SREG__+0x3>
  3e:   05 90           lpm r0, Z+
  40:   0d 92           st  X+, r0
  42:   ac 36           cpi r26, 0x6C   ; 108
  44:   b1 07           cpc r27, r17
  46:   d9 f7           brne    .-10        ; 0x3e <__SP_H__>

00000048 <__do_clear_bss>:
  48:   10 e0           ldi r17, 0x00   ; 0
  4a:   ac e6           ldi r26, 0x6C   ; 108
  4c:   b0 e0           ldi r27, 0x00   ; 0
  4e:   01 c0           rjmp    .+2         ; 0x52 <.do_clear_bss_start>

00000050 <.do_clear_bss_loop>:
  50:   1d 92           st  X+, r1

00000052 <.do_clear_bss_start>:
  52:   ad 36           cpi r26, 0x6D   ; 109
  54:   b1 07           cpc r27, r17
  56:   e1 f7           brne    .-8         ; 0x50 <.do_clear_bss_loop>
  58:   02 d0           rcall   .+4         ; 0x5e <main>
  5a:   12 c0           rjmp    .+36        ; 0x80 <_exit>

0000005c <__bad_interrupt>:
  5c:   d1 cf           rjmp    .-94        ; 0x0 <__vectors>

0000005e <main>: ...

00000080 <_exit>:
  80:   f8 94           cli

00000082 <__stop_program>:
  82:   ff cf           rjmp    .-2         ; 0x82 <__stop_program>

You can see avr-gcc automatically generates startup code, including:

  • the interrupt vector (__vectors), which uses rjmp to jump to the Interrupt Service Routines.
  • initialize the status register, SREG , and the stack pointer, SPL/SPH (__ctors_end)
  • copies the data segment content from FLASH to RAM for initialized, writable global variables (__do_copy_data)
  • clears the BSS segment for uninitialized writable global variables (__do_clear_bss etc)
  • calls our main() function
  • calls _exit() if main() ever returns
  • _exit() is just a cli to disable interrupts
  • and an infinite loop (__stop_program)
Share:
13,360
Admin
Author by

Admin

Updated on June 04, 2022

Comments

  • Admin
    Admin almost 2 years

    I am having trouble with the creation and addressing of an array created purely in assembly using the instruction set for the Atmel ATMega8535.

    What I understand so far is as follows:

    • The array contains contiguous data that is equal in length.
    • The creation of the array involves defining the beginning and end locations of the array (much like you would the stack).
    • You would address an index in the array by adding an offset of the base address of the array.

    What I am looking to do specifically is create a 1-D array of 8-bit integers with predefined values populating it during initialization it does not have to be written to, only addressed when needed. The problem ultimately lying in not being able to translate the logic into the assembly code.

    I have tried with little progress to do so using support from the following books:

    • Some Assembly Required: Assembly Language Programming with the AVR Microcontroller by Timothy S Margush
    • Get Going with...AVR Microcontrollers by Peter Sharpe

    Any help, advice or further resources would be greatly appreciated.