Programming the AVR Timers

Read time: 20 minutes (5239 words)

Let’s explore some “C” code and see if we can create a simple signal that can drive a Piezo buzzer.

The Piezo Buzzer

  • Here is the buzzer we will use
../_images/PiezoBuzzer1.jpg
  • a crystal inside of this flexes when a voltage is applied across the leads
    • when it flexes, it pushes air out of the way.
    • Flex it fast enough and those air pulses hit your ear making noise
    • You control the tone by the pulse rate

Buzzer Code

  • Here is the start of our code
// Example Arduino code to sound a Piezo buzzer

#include <avr/io.h>
#include <util/delay.h>

#define CLOCK_PRESCALE(n)   (CLKPR = 0x80, CLKPR = (n))
#define BUZZER_CONFIG       (DDRB |= (1<<4)) 
#define BUZZ_ON             (PORTB |= (1<<4))
#define BUZZ_OFF            (PORTB &= ~(1<<4))

This code sets up the chip clock so it runs at a slower than full speed. There are defines that set up the output pin and toggle that pin on and off. We will use these macros in the main code.

Chip setup

Next we configure the chip

int main(void){
    CLOCK_PRESCALE(0);
    BUZZER_CONFIG;

We are going to connect the buzzer between pins 12 (PORTB bit 4) and the one of the ground lines on the opposite side of the board.

The buzzer loop

Finally, we enter the main loop

    while(1) {
        BUZZ_ON;
        _delay_us(200);
        BUZZ_OFF;
        _delay_us(200);
    }

Connecting the buzzer

Here is how to hook up a simple Piezo buzzer to output pin 12 on the Arduino board (PB4 on the chip itself).

../_images/PiezoCircuit.png

Note

OK, this image is for a different board. You will need to examine the buzzer, plug it into the right holes on the breadboard, then connect the right pins up with two jumpers. It should not be too hard!

Here is a picture of the chip, with all of the pins identified. This will help in hooking up other stuff to the board:

../_images/Atmega328pPinMap.png

What is wrong with this?

Running this code makes noise (it did on my system, but not very loud!)

  • The program works, but it consumes all the processor’s attention
    • That wastes power we could use for other purposes.
  • One way to solve this is to us an internal Timer
    • Really just a simple counter that you can set up and let run.
    • The timer counts using the system clock
    • Once the timer reaches a max value, it rolls over and we can detect that!

Warning

There is one more problem with this code: IT WILL NOT SHUT UP! The code runs forever, generating an annoying tone. At least we know it works!

AVR Timers

The chip on the Arduino has four internal timers that go by these names:

  • Timer0 - 8 bit counter
  • Timer1 - 16 bit counter
  • Timer3 - 16 bit counter
  • Timer4 - 10 bit counter

Hmmm, wonder what happened to Timer2?

Clock source

  • The timer counts using the system clock
    • we can use a clock prescaler to extend the time it takes reach the max

The possible prescaler settings available on these timers are:

  • Clock / 8
  • Clock / 64
  • Clock / 256
  • Clock / 1024

Timer counting rates

On the Teensy2 we can increment the timer counter every:

  • 8/16000000 = 0.5us
  • 64/16000000 = 4us
  • 256/16000000 = 16us
  • 1024/16000000 = 64us

Setting the timer count

  • 8 bit timers run from 0 to 255
    • rolling over generates the signal
  • We can preload the counter to make it roll over faster
    • The counter begins at the preload value and counts up

Maximum timer times

  • The timer can generate a signal of some kind every:
    • 256 * 0.5us = 128us = 7812Hz
    • 256 * 4us = 1.024ms = 976Hz
    • 256 * 16us = 4.096ms = 244Hz
    • 256 * 64 = 16.384ms = 61Hz
  • Of course, we can adjust these numbers
    • slow the rate down by adjusting the clock
    • Speed it up by adjusting the count

Detecting the roll over

  • We can deal with the signal one of two ways
    • write code that checks for roll over every so often (polling)
    • configure the chip to generate an interrupt when it happens

Configuring the Timer

  • In order to use a timer, we must set it up and start it running.
    • set the prescaler value
    • preload the counter
  • for Timer0, the registers we use are:
    • Timer/Counter Control Register B - TCCR0B
      • Bit 0 = CS0
      • Bit 1 = CS1
      • BIT 2 = CS2

Setting the prescaler

CS2 CS1 CS0 Function
0 0 0 Timer stopped
0 0 1 clock/1
0 1 0 clock/8
0 1 1 clock/64
1 0 0 clock/256
1 0 1 clock/1024

Setting the preload count

  • Timer/Counter0 count register - TCNT0
    • This register is set with an initial counter value.
    • Setting the count starts the timer (if enabled)

Detecting the roll over

  • A timer register contains a bit that is set whenever the timer overflows
    • Timer Interrupt Flag Register - TIFR0
    • For Timer0, the bit we will examine is TOV0.

Polling code

polling:
    in      r16, TIFR0      ; copy the flag register into r16
    sbrs    r16, TOV0       ; skip the next instruction if count overflow
    rjmp    polling         ; loop back until bit it set

Skipping instructions

  • Some instructions are a bit funny
    • They test some condition
    • If the condition is met, they skip the next instruction
    • Otherwise, they execute the next instruction
      • Usually this is a jump of some kind
  • The result is like our conditional branch, but it takes two instructions

Is this better?

  • The processor is stuck in a loop until the overflow bit gets set.
    • However, we can add code inside this loop as we wish
    • If the code is too long, we will have a delay before we see the flag
  • Let’s write the code in assembly language

Building the Assembly Code

The Makefile shown here can build any project involving C or AVR Assembly code:

Makefile
 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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# source files
TARGET	:= $(shell basename $(PWD))
CSRCS	:= $(wildcard *.c)
COBJS	:= $(CSRCS:.c=.o)
SSRCS	:= $(wildcard *.S)
SOBJS	:= $(SSRCS:.S=.o)
OBJS	:= $(COBJS) $(SOBJS)

LST	:= $(TARGET).lst

# define the processor here
MCU		:= atmega328p
FREQ	:= 16000000L

# define the USB port on your system (this works on Linux)
PORT	:= /dev/ttyACM0
PGMR	:= arduino

# tools
GCC		:= avr-gcc
OBJDUMP	:= avr-objdump
OBJCOPY	:= avr-objcopy
DUDE	:= avrdude

UFLAGS	:=  -v -D -p$(MCU) -c$(PGMR)
UFLAGS		+= -P$(PORT)
UFLAGS		+= -b115200

CFLAGS	:=  -c -Os -mmcu=$(MCU)
CFLAGS		+= -DF_CPU=$(FREQ)

LFLAGS	:= -mmcu=$(MCU)
LFLAGS	+= -nostartfiles

.PHONY all:
all:	$(TARGET).hex $(LST)

# implicit build rules
%.hex:	%.elf
	$(OBJCOPY) -O ihex -R .eeprom $< $@

%.elf:	$(OBJS)
	$(GCC) $(LFLAGS) -o $@ $^ 

%.o:	%.c
	$(GCC) -c $(CFLAGS) -o $@ $^

%.o:	%.S
	$(GCC) -c $(CFLAGS) -o $@ $<

%.lst:	%.elf
	$(OBJDUMP) -C -d $< > $@

# utility targets
.PHONY:	load
load:
	$(DUDE) $(DUDECONF) $(UFLAGS) -Uflash:w:$(TARGET).hex:i

.PHONY:	clean
clean:
	$(RM) *.hex *.lst *.o *.elf

Note

We will use this Makefilein all AVR based projects. All project source files are in the same directory as this Makefile.

main.S: Setting up

To rewrite this in assembly language, we start off by writing the configuration file we will use for this project:

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

#define BUZZ_PIN     4
#define BUZZ_DIR     _(DDRB)
#define BUZZ_PORT    _(PORTB)

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

Next, we set up the processor with the same initial code we used in our Blink project:

main.S (15.1)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
; arduinoBuzz.S
; Author: Roie R. Black
; Date: Apr 30, 2015

#include "config.inc"

    .section    .text
    .org        0x00
    .extern     timer_init
    .extern     timer_delay

    .global     main

main:
; set stack to top of available ram
    ldi         r28, (RAMEND & 0x00ff)
    ldi         r29, (RAMEND >> 8)
    out         _(SPH), r29
    out         _(SPL), r28
    ;

main.S: Init Chip and Start polling

All additional chip initialization is done in a subroutine. We can call this safely, since we just set up the stack!

main.S (15.2)
    call        Init            ; complete initialization

And here is the polling loop:

main.S (15.3)
1
2
3
1:  call        Toggle          ; flip BUZZER on/off
    call        timer_delay     ; wait a bit
    rjmp        1b              ; loop forever

main.S: Initialize the chip

Here is the initialization code:

main.S (15.2)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
Init:
    ; set up a register with zero for convenience
    eor         r1, r1          ; cheap zero
    
    ; clear flag register
    out         _(SREG), r1

    ; set up the system clock
    ldi         r24, 0x80       ; set up prescaler
    sts         CLKPR, r24
    sts         CLKPR, r1       ; run at full speed

    ; setup the buzzer pin
    sbi         BUZZ_DIR, BUZZ_PIN  ; set pin 5 on port B for output
    cbi         BUZZ_PORT, BUZZ_PIN ; set pin 5 on port B off

    call        timer_init          ; set up the timer
    ret

Nothing new here. We are using the BUZZER definitions set up in the configuration file in this code, but I moved all the logic related to the timer into a second file named timer.S.

Here is the code to toggle the buzzer pin:

main.S (15.4)
1
2
3
4
5
6
Toggle:
    in          r24, BUZZ_PORT          ; get current port B values
    ldi         r25, (1 << BUZZ_PIN)    ; LED pin number
    eor         r24, r25                ; toggle bit
    out         BUZZ_PORT, r24          ; write it back in place
    ret

We have seen this pattern before.

timer.S: Setting up

The code needed to initialize the timer looks like this:

timer.S (15.1)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
; timer.S - routines for Piezo buzzer

#include "config.inc"

.equ    PRESET      ,1

        .section    .text

        .global     timer_delay
        .global     timer_init

timer_init:
        ldi     r16, (1 << CS01) | (1 << CS00)  ; divide by 64
        out     _(TCCR0B), r16                  ; set timer clock 
        ldi     r16, 1 << TOV0                  ; clear interrupt flag
        out     _(TIFR0), r16
        ldi     r16, PRESET                     ; initial value for timer
        out     _(TCNT0), r16
        ret

timer.S: Polling delay code

Finally, the timer delay code, using our polling logic, looks like this:

timer.S (15.2)
1
2
3
4
5
6
7
8
9
timer_delay:
        in      r16, _(TIFR0)                   ; check the flag 
        sbrs    r16, 1 << TOV0                  ; skip if overflow set
        rjmp    timer_delay                     ; wait for it
        ldi     r16, 1 << TOV0                  ; clear the interrupt flag
        out     _(TIFR0), r16                   ; clear the interrupt flag
        ldi     r16, PRESET                     ; reload the counter
        out     _(TCNT0), r16                   ; set preset in
        ret       

How it works

  • Main sets up the chip and timer system
    • It then loops toggling buzzer pins and calling the timer delay routine
  • timer.S has the timer init routine
    • It uses polling in the delay loop to wait for the timer overflow
  • Overall logic is identical to the “C” example

The output signal

../_images/BasicTimer.png

Making the delay shorter

  • Loading an initial value into the timer counter will shorten the delay
    • The timer counts from this value to its max before overflowing
  • We modify the delay code by adding the preload value as we reset

The modified delay code

All I needed to do to shorten the delay, and make the tone higher, was this:

.equ    PRESET      ,   128

Examining the new delay

../_images/TimerPreset.png

That is all we need to make a little noise. We have improved things a little, next time we will improve things a lot. We will free up the processor to do other work while the timer runs!

Examining the code

Before we leave this lecture, let’s try to examine the code produced by reverse engineering the .elf file. The current Makefile deletes the .elf file after it builds the final .hex file, but we can get it back by doing this:

make asmBuzzer.elf
avr-objdump -d asmBuzzer.elf

Here is the output from this run


asmBuzzer.elf:     file format elf32-avr


Disassembly of section .text:

00000000 <__ctors_end>:
   0:	00 e8       	ldi	r16, 0x80	; 128
   2:	10 e0       	ldi	r17, 0x00	; 0
   4:	00 93 61 00 	sts	0x0061, r16
   8:	10 93 61 00 	sts	0x0061, r17
   c:	24 9a       	sbi	0x04, 4	; 4
   e:	2c 98       	cbi	0x05, 4	; 5

00000010 <main_loop>:
  10:	05 d0       	rcall	.+10     	; 0x1c <timer_init>
  12:	2c 9a       	sbi	0x05, 4	; 5
  14:	0a d0       	rcall	.+20     	; 0x2a <timer_delay>
  16:	2c 98       	cbi	0x05, 4	; 5
  18:	08 d0       	rcall	.+16     	; 0x2a <timer_delay>
  1a:	fb cf       	rjmp	.-10     	; 0x12 <main_loop+0x2>

0000001c <timer_init>:
  1c:	03 e0       	ldi	r16, 0x03	; 3
  1e:	05 bd       	out	0x25, r16	; 37
  20:	01 e0       	ldi	r16, 0x01	; 1
  22:	05 bb       	out	0x15, r16	; 21
  24:	01 e0       	ldi	r16, 0x01	; 1
  26:	06 bd       	out	0x26, r16	; 38
  28:	08 95       	ret

0000002a <timer_delay>:
  2a:	05 b3       	in	r16, 0x15	; 21
  2c:	00 ff       	sbrs	r16, 0
  2e:	fd cf       	rjmp	.-6      	; 0x2a <timer_delay>
  30:	01 e0       	ldi	r16, 0x01	; 1
  32:	05 bb       	out	0x15, r16	; 21
  34:	01 e0       	ldi	r16, 0x01	; 1
  36:	06 bd       	out	0x26, r16	; 38
  38:	08 95       	ret

(You can capture this output by redirecting into a file). We can see all of the numbers from the included .def file in this, and see where all of the other routines ended up in the processor’s memory when loaded into the chip There will be other output files you can examine to figure out why things might be going wrong.