Basic AVR assembly language

Read time: 22 minutes (5665 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 mark a line of code with a label. This is just a name beginning on column 1

  • 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.

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.

blink.S
#include "config.h"

Here is the configuration file we will use:

Note

There is code at the end of this file that makes writing real assembly language for the AVR using avr-gcc a bit easier. Some instructions only had enough room for a limited number of bits to specify operands. The number you enter in the instruction is actually off by 0x20 from the real number used in the instruction. The avr-gcc folks solved this problem by making you specify the register you want (usually an internal register with a name, not one of those r0-r31 registers), and use an awful line for specifying that register. I figured out an easier way, they looks nicer in your code. You just wrap up the offending name in parentheses, and stick an underscore in front of that. This preprocessor macro solves the problem.

Data layout

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

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.

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 this 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 a later AVR 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

The real stack pointer register is a 16-bit one, so we need to set it in twp steps, one for the low byte, and one for the high byte.

RAMEND is set by avr-gcc based on the processor you specify when you build your code. The stack will start at the top of available data memory, and grow downward.

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. We can alter the speed of the processor by configuring a special counter used to send “tick” signals every so many physicals oscillations of the hardware clock on the board. We can make the processor go slower when we do not need it to run so fast (odd idea!)

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.

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
        ; enter the blink loop
1:      call        toggle
        call        delay
        rjmp        1b

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. You get to tweak that code for a homework project.

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)