Basic AVR assembly language

Read time: 18 minutes (4565 words)

To get started writing AVR assembly language programs, you need a reference that details the instructions you can use. The chip we will be using is an Atmel ATmega328P, found on the Arduino Uno boards.

Here is the “datasheet” for this chip:

And an instruction set summary:

Now, we can lay out a code file.

Basic File Layout

The basic layout of a program file looks like this :

  • Comments begin with a semicolon
  • We will declare labels as usual
  • Directives tell the assembler what to do (usually start with a dot)
  • Assembly files are named something.S (note the capital S)

Headers

Use a comment block at the top of the file to identify the project, and who created it.

src/blink.S
; Hello Blinky World for AVR
; Author: Roie R. Black
; Date: July 14, 2015

Put any additional documentation you need here as well. For simple code files, letting the user know how to build the project is a good idea!

Chip Definitions

We need to define a few things for most programs we write for this chip. We will go over all of that later. For now, we will place all of this definition logic in a separate include file, to make it easy to change as needed.

src/blink.S
#include "config.h"

Data layout

src/blink.S
	.section .data
dummy: 	.byte 0		; dummy global variable

This is a dummy declaration, we will not need this variable in the code. It is here just to show you how to set up a variable.

Note

There are no other data types, only bytes! In general, we do not do number crunching in these machines!

Code layout

src/blink.S
        .section .text
        .global     main
        .extern     delay          
        .org        0x0000

main:

The line that defines the “org” (origin) of this program code, which is the address where code will be placed in the chip. Since there will be no other code in the chip when we run our program, we start this code at address zero in the program code area.

Before we can actually run any real processing code, we have some chip initialization to do!

Initialize the chip

The first chunk of code we need sets up the system stack pointer, so we can call procedures. We set up a stack at the top of available memery:

Warning

Pay attention to those names in all caps surrounded by parentheses, and with a leading underscore in front of them. This is a scheme I came up with to simplify something I have not liked in writing code for this chip. We will go over that issue in our next lecture. For now, the name in caps is a standard AVR register name, the underscore and parentheses have been added to streamline the code we write, to make it easier to read.

Clearing the Status register

The SREG register is used to record various conditions that are generated as we process instructions. We need to clear that flag before we start our real code:

src/blink.S
main:
	; clear the SREG register
        eor     r1, r1                  ; cheap zero
        out     SREG, r1                ; clear flag register

That line that uses the eor instruction is th fastest way to clear all the bits in a register. The XOR operator is applied bit by bit, and always results in a zero. It is common in AVR code to leave the zero in r1 for use whenever we need a zero * We have enough registers to sacrifice one!

Setting Up a Stack

We need a special area where we will save return addresses as we call procedures, We will look at that in our next lecture. For now, we just initialize a special register to so it is pointing to the top of available memory in this chip:

src/blink.S
        ; set up the stack
        ldi         r28, (RAMEND & 0x00ff)
        ldi         r29, (RAMEND >> 8)
        out         _(SPH), r29
        out         _(SPL), r28

Now that we have the stack set up, we are free to use procedures in our code.

Setting up the clock

src/blink.S
	; initialize the CPU clock to run at full speed
	ldi         r24, 0x80
        sts         CLKPR, r24              ; allow access to clock setup
        sts         CLKPR, r1               ; run at full speed

This code is tricky. There is a special lock on the register that controls the speed of the processor. Writing accidentally into this register could be dangerous. So, the designers made it hard to access. Basically, you write an 0x80 into the register, then immediately write in the value you want on the next instruction. The first write unlocks the register, and the next one puts in a new value. Right after that, the register is re-locked.

Initializing the LED pin

There is one more thing we need to do! Before we can make the LED blink, we need to set up the output pin on the chip that drives the LED so it is an output. Here is the code that does this:

src/blink.S
        ; set up the LED port
        sbi         LED_DIR, LED_PIN        ; set LED pin to output
        cbi         LED_PORT, LED_PIN       ; start with the LED off
       

The names used here are defined in our “config.h” file, so we can change them if needed.

Here is that file:

Hello, World - Arduino style

Finally, we can write the real code for this project. Everything up to this point is setup, and we need to do almost exactly that same thing for every project we build!

Since the Arduino has no console to display test on, the equivalent of “Hello, World” is simple” blink a light at some defined rate!

src/blink.S
        out         _(SPL), r28

	; initialize the CPU clock to run at full speed
	ldi         r24, 0x80

See the unusual label here? Labels can be numbers in this assembler. We can refer to a numbered label with an “f” (look forward in the code) or “b” (look backward in the code). The first such numbered label is the one we are referring to. This avoids making up silly labels to mark a line of code for later reference. * No more silly names needed

Toggling the LED on/off

Finally, we blink!

src/blink.S
toggle:
        in          r24, LED_PORT           ; get current bits
        ldi         r25, (1 << LED_PIN)     ; LED is pin 5
        eor         r24, r25                ; flip the bit
        out         LED_PORT, r24           ; write the bits back
        ret

The EOR instruction is the same as XOR in the pentium. We are “toggling” a single bit to make the light blink!

This is all the code we need - except for the delay code

Here is the complete project code:

src/blink.S
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
; Hello Blinky World for AVR
; Author: Roie R. Black
; Date: July 14, 2015

#include "config.h"

	.section .data
dummy: 	.byte 0		; dummy global variable

        .section .text
        .global     main
        .extern     delay          
        .org        0x0000

main:
	; clear the SREG register
        eor     r1, r1                  ; cheap zero
        out     SREG, r1                ; clear flag register


        ; set up the stack
        ldi         r28, (RAMEND & 0x00ff)
        ldi         r29, (RAMEND >> 8)
        out         _(SPH), r29
        out         _(SPL), r28

	; initialize the CPU clock to run at full speed
	ldi         r24, 0x80
        sts         CLKPR, r24              ; allow access to clock setup
        sts         CLKPR, r1               ; run at full speed
        
        ; set up the LED port
        sbi         LED_DIR, LED_PIN        ; set LED pin to output
        cbi         LED_PORT, LED_PIN       ; start with the LED off
       

        ; enter the blink loop
1:      call        toggle
        call        delay
        rjmp        1b

toggle:
        in          r24, LED_PORT           ; get current bits
        ldi         r25, (1 << LED_PIN)     ; LED is pin 5
        eor         r24, r25                ; flip the bit
        out         LED_PORT, r24           ; write the bits back
        ret
src/delay.S
1
2
3
4
5
6
7
8
9
#include "config.h"
    .global      delay
    .section    .text
delay:
        ldi      r26, 255
1:      dec      r26
        brne     1b
        ret

include/config.h
1
2
3
4
5
6
7
8
9
#include <avr/io.h>

#define LED_PIN     5
#define LED_PORT    _(PORTB)
#define LED_DIR     _(DDRB)

// include this line to avoid SFR_REG issues
#define _(s)    _SFR_IO_ADDR(s)