← Back to Home

ELEC3020 - Lecture 5
Assembly Language Programming - Slides

AVR Microcontrollers

AVR is a family of microcontrollers originally developed by Atmel in 1996. They are 8-bit RISC single-chip microcontrollers based off modified Harvard architecture.

AVR microcontrollers are used numerously as embedded systems. They are commonly used as the chip for Arduino boards.


Machine Code

Programming of a computer system can be broken into three levels:

  • Machine Code — direct execution
  • Assembly Language — human-readable mnemonics that map 1:1 to machine instructions, machine dependent
  • High Level Programming Language — machine-independent, needs translation by a compiler

Machine code is the lowest level a computer can be programmed in. It involves specifying the program in byte codes corresponding to the CPU’s instruction set. For example, 05A1 -> LOAD mem[A1]. The instruction format is machine dependent and cannot be ported.

centre
Programming in machine code is difficult and error-prone and therefore is not used in practice. Instead, we use name abbreviations that make programming easier (e.g. ADD for adding). This is known as assembly language and is highly preferred over writing direct machine code.


Assembler

Assembly language requires a translation tool known as an
assembler to translate our abbreviations into the corresponding machine code.

centre
An assembler is machine dependent and translates code line-by-line.


AVR Assembly

Each line in AVR assembly is made up of four parts:

Label: COMMAND  operand1, operand2  ; comment
  • Label: — an optional named address, used as a branch target
  • Command — the opcode
  • Operands — arguments to operate on
  • Comment — should describe what process accomplishes

Each instruction is 16-bits in size. The data is typically split into three parts:

  • Opcode
  • Destination Register — operand1
  • Source Register — operand2 (if applicable)

The size of these parts can vary depending on the type of instruction. For example, a single operand instruction only needs 5-bits to select a destination register, thus it has 11-bits to represent more opcodes.

A two operand instruction only has 6-bits to contain its opcode. Thus is more limited in the number of potential operations for two operands.

AVR Assembly Commands

Command TypeExamplesFunction
ArithmeticADD, SUB, MULadd, subtract, multiply
Data MovementLD, ST, MOV
LDS, STS, IN, OUT
move data between
registers and memory
Logic and ShiftAND, OR, COM, EOR
LSL, LSR, ROL, ROR
and, or, not, xor
shift left/right,
rotate left/reight
Compare and BranchCP, TST
JMP, BREQ, BRLT
compare
jump always, branch
if-equal/if-less-than

AVR Immediate Operations

Some operations known as immediate operations load a hardcoded number straight out of the program code instead from a specified address.

For example: LDI R16, 100 -> R16 := 100

These operations only work with R16-R31. This is because 8-bits are dedicated to the constant, leaving only 8-bits for the opcode and register ID.

Using 4-bits for the register ID is not enough to represent all 32 registers. Instead, we limit the selected register to be between R16-R31 by setting the source bit 4 to 1 (0000 -> 10000).

The ‘immediate’ property is indicated by the I following most standard commands (e.g. LDI, SUBI, ANDI, CPI)

AVR Operands

Each operand can be a constant, register or a memory value/location. Registers are labelled by number from R0 to R32 for an 8-bit microcontroller. Memory values are usually specified by a label (variable name) or a pointer register pair (X, Y, Z).

Constants need to have a prefix to indicate what type they are. Decimal is the standard type, and has no prefix. Hexadecimal (Base-16) is prefixed with 0x or $. Binary is prefixed with 0b. Standard (ASCII) characters must be wrapped in single quotes.

DecimalHexadecimalBinaryCharacter
100xD7 or $D70b0110’Z’

AVR Registers

AVR architecture uses 32 working 8-bit registers (R0...R31) connected to the ALU. The CPU can pull two values from any of these registers, perform an operation and write the result back in a single clock cycle.

The last six general-purpose registers (R26 through R31) also double as three 16-bit pointer/index registers by pairing up. This is because 8-bit registers alone are not large enough to hold memory addresses, which are usually larger than 255 bytes.

X Pointer: R27 (High byte) + R26 (Low byte)
Y Pointer: R29 (High byte) + R28 (Low byte)
Z Pointer: R31 (High byte) + R30 (Low byte)

The high byte register holds the left 8-bits while the low byte register holds the right 8-bits. Together, they can store a 16-bit number.

There is also 64 special registers for control and I/O.

Some key special registers include:

  • SP -> stack pointer
  • SMCR (Sleep Mode Control Register) -> configures power-saving modes
  • GPIO0/GPOIO1 -> general purpose I/O registers

AVR Jump and Branching

See: Branching

Unconditional jump:

JMP  next      ; always jump
  ...
next: ADD ...  ; destination

Conditional branch:

CP   R1,R2    ; compare reg.
BREQ next     ; branch if equal
CLR  R7       ; else do this
  ...
next: ADD ... ; destination

The conditional branch works by using flags from the status register. For example: a Z flag, (i.e. a zero) will mean that the condition is false and no branching will occur. This is also how a CPU memorises the result from the proceeding operation.

The most important flag conditions are:

  • Z — zero
  • N — negative
  • C — carry
  • V — overflow

If a conditional branch fails, the program just moves to the next instruction step (PC := PC + inc).

Branching is the standard way to implement if-then-else functionality:

	  CPI  R16, 10 
	  BREQ then 
else: SUB  R16, R2 
      JMP  next 
then: ADD  R16, R1 
next: … 

AVR Addressing Modes

All assembly commands need operands and there are several ways you can declare an operand. These methods depend on the type of operand you want to declare and where it is stored.

ModeExampleMeaning
ImmediateSUBI R16, 5operand is a literal constant
Direct (register)ADD R0, R1operand is a register’s contents
Absolute (memory)LDS R0, nameoperand is at a fixed
labelled memory address
IndirectLD R0, Xoperand is at the address
stored in a pointer register

AVR Subroutines and Stack

Stack & Subroutine Commands

  • PUSH / POP : Stack operations
  • CALL / RET : Subroutine jump / return

The command CALL is similar to JMP, however it preserves the current program counter on stack so that execution can return later using RET (return from subroutine).

centre
See: Stack / CITS2002 - Lecture 19 > Stack (FILO)

For the CALL command to work, we need to store the address of the main program somewhere so we can jump back upon RET. If we stored this address in a register, than we would run into issues upon calling a subroutine within a subroutine.

Instead, we must use a stack which holds all the addresses of the parent program in RAM for later. We have a dedicated stack register (SP) which holds a RAM address that points to where the last return address is stored.

centre
Note that the SP increments by 2 bytes as the PC counts in 2-byte words. On most architectures, the stack grows downwards in memory as it takes up the last section of RAM. Thus, CALL decrements SP by 2 and RET increments SP by 2.

RET pops the address from the stack and puts in the PC for us to jump back. The SP then increments to leave it pointing at whichever address was pushed previously.

A closeup look at CALL and RET:

CALL:
	1. PUSH PC+2
		1.a Mem[SP] <- PC + 2
		1.b SP <- SP - 2
	2. PC <- address
 
RET:
	1. POP PC
		1.a SP <- SP + 2
		1.b PC <- Mem[SP]

Save and Restore Registers

Registers are a shared resource, used by the main program and subroutines. As such, you don’t want a subroutine to override data stored in a register that your main program is using.

We can address this issue by pushing register data onto the stack and then popping it at the end of the subroutine’s execution.

For example, if a subroutine uses R1 and R2:

mysub:  PUSH R1 ; save registers
        PUSH R2
		...     ; subroutine function
        POP R2  ; restore registers
        POP R1  ; in reverse order
        RET

You must POP registers in reverse order, otherwise the data they stored will swap positions.

AVR Input/Output Ports

Traditional processors have a fixed number of inputs and outputs.
Modern processors on the other hand can configure each I/O line to either be input or output.

Pins are grouped into 8-bit registers called ports. The ATmega328P has bidirectional Ports B, C and D.

centre
Not all the port pins are accessible however, (PB6, PB7 and PC7) and some ports have multiple functions such as PC6 (reset), PD0 (RX) and PD1 (TX) which should not be used for general I/O.

Each port uses three specific 8-bit registers to control its behaviour:
RegisterNameFunctionAssembly Instruction
DDRxData Direction RegisterConfigure
pin direction
OUT DDRD, R16
PORTxPort Data RegisterSet output
pin values
OUT PORTD, R16
PINxPort Input Pins AddressRead pin inputIN R16, PINB

Before reading or writing, the DDRx register must be initialised. Writing 0xFF sets all 8 pins to outputs, while 0x00 sets all pins to input. You store the I/O instruction inside your declared DDRx register (R16 in table above).


ELEC3020 - Lecture 7