AVR assembler examples

ATmega1284P I/O examples

The AVR assembler makes it easy to create Lisp functions that manipulate the ports and other peripherals on the ATmega1284P, AVR128DA48, or AVR128DB48.

On the ATmega1284P the most useful ports have single byte addresses so they can be used with the efficient $sbi$sci$sbic$sbis$in, and $out instructions.

Here are the addresses for the I/O ports accessible on the ATmega1284P:

  PIN DDR PORT
PORTA #x00 #x01 #x02
PORTB #x03 #x04 #x05
PORTC #x06 #x07 #x08
PORTD #x09 #x0a #x0b

PIN is the input port, PORT is the output port, and DDR is the data direction port.

LED

The following examples show how to turn on and off the LED provided on most ATmega1284P boards. The LED is usually on PB0, specified by these variables:

(defvar port 3) ; PORTB
(defvar led 0) ; PB0

(defcode on ()
  ($sbi (+ 1 port) led) ; Set LED bit in DDR
  ($sbi (+ 2 port) led) ; Set LED bit in PORT
  ($ret))

(defcode off ()
  ($cbi (+ 2 port) led) ; Clear LED bit in PORT
  ($ret))

The (on) routine defines the pin as an output and takes it low to turn the LED on. The (off) routine takes the pin high.

AVR128DA48 and AVR128DB48 I/O examples

On the AVR128DA48 and AVR128DB48 the ports generally have addresses that are larger than one byte, but the I/O ports and several other useful peripherals are mirrored to single byte addresses in I/O memory, called Virtual Ports, so you can use the efficient $sbi$sci$sbic$sbis$in, and $out instructions with them.

Here are the addresses for the ports accessible on the AVR128DA48 and AVR128DB48 chips:

  DIR OUT IN INTFLAGS
VPORTA #x00 #x01 #x02 #x03
VPORTB #x04 #x05 #x06 #x07
VPORTC #x08 #x09 #x0a #x0b
VPORTD #x0c #x0d #x0e #x0f
VPORTE #x10 #x11 #x12 #x13
VPORTF #x14 #x15 #x16 #x17

DIR is the data direction port, OUT is the output port, and IN is the input port.

Writing to all the outputs in a port

This function dira defines the state of all the pins on PORTA:

(defcode dira (x)
  ($out #x00 'r24)
  ($ret))

To set all the pins as outputs call:

(dira #xff)

This function outa writes a byte to all the pins on PORTA:

(defcode outa (x)
  ($out #x01 'r24)
  ($ret))

For example, to set alternate pins high call:

(outa #x55)

This is equivalent to eight calls to digitalwrite.

LED

The following examples show how to turn on and off the LED provided on PC6 on the Curiosity Nano DA board:

(defvar vport 8)
(defvar led 6)

(defcode on ()
  ($sbi vport led) ; Set LED bit in DIR
  ($cbi (+ vport 1) led) ; Clear LED bit in OUT
  ($ret))

(defcode off ()
  ($sbi (+ vport 1) led) ; Set LED bit in OUT
  ($ret))

The (on) routine defines the pin as an output and takes it low to turn the LED on. The (off) routine takes the pin high.

On the Curiosity Nano DB board the LED is on PB3, so change the defvar statements to:

(defvar vport 4)
(defvar led 3)

Blink

The following blink routine generates a delay using three nested loops. The innermost loop takes 3 cycles, so the total delay is 3 x 256 x 256 x 122 cycles, or approximately one second with a 24MHz clock. The parameter specifies the number of toggles:

(defvar vport 8)
(defvar led 6)

(defcode blink (n)
  ($sbi vport led) #| LED output |#
  j4
  ($ldi 'r20 122)
  j3
  ($ldi 'r19 0)
  j2
  ($ldi 'r18 0)
  j1
  ($dec 'r18)
  ($br 'ne j1)
  ($dec 'r19)
  ($br 'ne j2)
  ($dec 'r20)
  ($br 'ne j3)
  ($sbi (+ vport 2) led) #| Toggle LED |#
  ($dec 'r24)
  ($br 'ne j4)
  ($ret))

On the Curiosity Nano DB board the LED is on PB3, so change the defvar statements to:

(defvar vport 4)
(defvar led 3)

Timer

The following program configures Timer/Counter TCA1 to blink the LED on pin PC6 on the Curiosity Nano DA board. It's equivalent to the following C program:

PORTMUX.TCAROUTEA = PORTMUX_TCA1_PORTC_gc;                               // WO2 at PC6
PORTC.DIRSET = PIN6_bm;                                                  // LED output
TCA1.SINGLE.CTRLD = 0;                                                   // Single mode
TCA1.SINGLE.CTRLB = TCA_SINGLE_CMP2EN_bm | TCA_SINGLE_WGMODE_FRQ_gc;     // Frequency mode
TCA1.SINGLE.CMP0 = 23436;                                                // Period ~0.5 Hz
TCA1.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1024_gc | TCA_SINGLE_ENABLE_bm; // Prescale 1024

To write the assembler routine we first need to find the appropriate addresses for the peripheral registers, and the values needed to configure them, from the AVR DA/DB datasheet:

Symbol Value Description
portmux #x05E0 Base address of the Port Multiplexer
  6 Offset of the PORTMUX TCAROUTEA register
  8 Value to select PORTC for TCA1
vportc 8 Virtual PORTC
  6 Bit to set in vportc to make PC6 an output
tca1 #x0A40 Base address of Timer/Counter TCA1
  3 Offset of the TCA1 CTRLD register
  #b1000001 Value to set frequency mode on CMP2
  1 Offset of the TCA1 CTRLB register
  #x28, #x29 Offset to the 16-bit CMP0 compare register
  0 Offset of the TCA1 CTRLA register
  #b1111 Value to set DIV1024 and ENABLE

Here's the equivalent assembler routine. The compare value is passed in the parameter to blink:

(defvar portmux #x05E0)
(defvar tca1 #x0A40)
(defvar vportc 8)

(defcode blink (per)
  ($ldi 'r18 8)
  ($sts (+ portmux 6) 'r18)
  ($sbi vportc 6)
  ($sts (+ tca1 3) 'r1)
  ($ldi 'r18 #b1000001)
  ($sts (+ tca1 1) 'r18)
  ($sts (+ tca1 #x28) 'r24)
  ($sts (+ tca1 #x29) 'r25)
  ($ldi 'r18 #b1111)
  ($sts tca1 'r18)
  ($ret))

To run the program with the traditional 0.5 Hz blink rate call:

(blink 23436)

because, with a 24MHz clock, the LED is toggled every 24000000/1024/(23436+1) = 1 second.

On the Curiosity Nano DB the LED is on a different pin, so you will need to connect a multimeter or LED+resistor to PC6 to see the result.

Because the LED is being blinked by a timer it will continue blinking after you return to uLisp.

DAC

The following example generates a sawtooth waveform of about 2.3 kHz on the Curiosity Nano DA board's 10-bit DAC output, PD6, by incrementing a 16-bit counter and writing it to the DAC register:

(defvar port #x0440)
(defvar vport 8)
(defvar btn 7)
(defvar dac #x06A0)

(defcode saw ()
  ($ldi 'r24 #b01000001)
  ($sts dac 'r24) ; Enable DAC and buffer
  ($ldi 'r24 #b1000)
  ($sts (+ port (+ #x10 btn)) 'r24)
  wave
  ($sts (+ dac 2) 'r24) ; Low byte
  ($sts (+ dac 3) 'r25) ; High byte
  ($adiw 'r24 32)
  ($adiw 'r24 32)
  ($sbic (+ vport 2) btn) ; Skip if button low
  ($rjmp wave)
  ($ret))

The program runs in a loop, but you can press the button on the Curiosity Nano AVR128DA48, which is connected to PC7, to return to Lisp. 

Here's the waveform it generates:

Sawtooth.png

On the Curiosity Nano DB board the button is on PB2, so change the defvar statements to:

(defvar port #x0440)
(defvar vport 8)
(defvar btn 7)
(defvar dac #x06A0)

Numeric examples

The following examples illustrate the use of the assembler with integer parameters. Many of them are based on the uLisp Benchmarks. They work equally well on the ATmega1284P or the AVR DA/DB boards.

Fibonacci sequence

The Fibonacci sequence is:

1, 1, 2, 3, 5, 8, 13, 21 ...

where the first two terms are 1, and each subsequent term is the sum of the two previous terms. The following recursive function finds the nth term, counting from 0:

(defun fib (n)
  (if (< n 3) 1
    (+ (fib (- n 1)) (fib (- n 2)))))

Running the Lisp version on a Curiosity Nano DA or DB board:

> (fib 23)
28657

Here's the assembler version:

; Fibonacci sequence
(defcode fib (x)
  fib
  ($cpi 'r24 3)
  ($br 'cc cont)
  ($ldi 'r24 1)
  ($ret)
  cont
  ($push 'r24)
  ($dec 'r24)
  ($rcall fib)
  ($movw 'r22 'r24)
  ($pop 'r24)
  ($push 'r22)
  ($push 'r23)
  ($subi 'r24 2)
  ($ldi 'r25 0)
  ($rcall fib)
  ($pop 'r23)
  ($pop 'r22)
  ($add 'r24 'r22)
  ($adc 'r25 'r23)
  ($ret))

Running the assembler version on a Curiosity Nano DA or DB board:

> (for-millis () (print (fib 23)))

28657 
49

Takeuchi function

The Takeuchi function is a classic benchmark for comparing implementations of Lisp, originally used by Ikuo Takeuchi of Japan. Here's the Lisp version:

(defun tak (x y z)
  (if (not (< y x))
      z
    (tak
     (tak (1- x) y z)
     (tak (1- y) z x)
     (tak (1- z) x y))))

On a Curiosity Nano DA board (tak 18 12 6) takes 31 seconds:

 (for-millis () (print (tak 18 12 6)))

7 
31214

Here's the assembler version:

(defcode tak (x y z)
  tak
  ($cp 'r22 'r24)
  ($br 'lt less)
  ($mov 'r24 'r20)
  ($ret)
  less
  ($push 'r20)
  ($push 'r22)
  ($push 'r24)
  ($dec 'r24)
  ($rcall tak)
  ($mov 'r18 'r24)
  ($pop 'r20)
  ($pop 'r24)
  ($pop 'r22)
  ($push 'r18)
  ($push 'r20)
  ($push 'r22)
  ($push 'r24)
  ($dec 'r24)
  ($rcall tak)
  ($mov 'r18 'r24)
  ($pop 'r20)
  ($pop 'r24)
  ($pop 'r22)
  ($dec 'r24)
  ($push 'r18)
  ($rcall tak)
  ($mov 'r20 'r24)
  ($pop 'r22)
  ($pop 'r24)
  ($rjmp tak))

Run it as follows:

> (for-millis () (print (tak 18 12 6)))

7 
42

The assembler version is almost a factor of 750 faster.

Hofstadter Q sequence

This is one of several recursive sequences described in Douglas Hofstadter's book "Gödel, Escher, Bach: an Eternal Golden Braid". It is related to the Fibonacci sequence, except that in this case the two preceding terms specify how far to go back in the sequence to find the two terms to be summed:

(defun q (n)
  (if (<= n 2) 1
    (+
     (q (- n (q (- n 1))))
     (q (- n (q (- n 2)))))))

Running the Lisp version:

> (q 21)
12
This takes 32 seconds.

Here's the assembler version:

; Hofstadter Q sequence
(defcode q (x)
  q
  ($cpi 'r24 3)
  ($br 'cc cont)
  ($ldi 'r24 1)
  ($ret)
  cont
  ($push 'r24)
  ($dec 'r24)
  ($rcall q)
  ($movw 'r22 'r24)
  ($pop 'r24)
  ($push 'r24)
  ($sub 'r24 'r22)
  ($rcall q)
  ($movw 'r22 'r24)
  ($pop 'r24)
  ($push 'r22)
  ($push 'r24)
  ($subi 'r24 2)
  ($rcall q)
  ($movw 'r22 'r24)
  ($pop 'r24)
  ($push 'r24)
  ($sub 'r24 'r22)
  ($rcall q)
  ($pop 'r22)
  ($pop 'r22)
  ($add 'r24 'r22)
  ($ret))

Running the assembler version:

> (for-millis () (print (q 21)))

12 
152

List examples

Any of the arguments to a machine-code function can be a list, in which case the address of the list is passed to the routine in the corresponding register pair. These examples work equally well on the ATmega1284P or the AVR DA/DB boards.

For example, if the list is the first parameter its address will be in  r25 (high byte) and r24 (low byte), and you can then load the car of the list into z with:

  ($movw 'y 'r24)
  ($ld 'zl 'y)
  ($ldd 'zh 'y 1)

Summing the integers in a list

To demonstrate list handling the following routine sum returns the sum of the integers in a list:

(defcode sum (lst)
  ($movw 'x 'r24)
  ($eor 'r24 'r24)
  ($eor 'r25 'r25)
  test
  ($sbiw 'x 0) ; Test if x is nil
  ($br 'eq ret)
  ($ld 'zl 'x+)
  ($ld 'zh 'x+)
  ($ldd 'r18 'z 2)
  ($ldd 'r19 'z 3)
  ($add 'r24 'r18)
  ($adc 'r25 'r19)
  ($ld 'r18 'x+)
  ($ld 'r19 'x+)
  ($movw 'x 'r18) ; Point to next item
  ($rjmp test)
  ret
  ($ret))

For example:

> (sum '(1 2 3 4 5))
15