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.
; 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.
#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¶
.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¶
.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:
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:
; 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¶
; 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:
; 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!
; 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!
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:
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
|
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
|
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)
|