[TUT][ASM][CODE][PWM] A MORON'S GUIDE TO TIMERS & PWM

7 posts / 0 new
Author
Message

A MORON'S GUIDE TO TIMER/COUNTERS v1.6
=======================================

1. THE PAUSE ROUTINE
2. WAIT-FOR-TIMER "NORMAL" MODE
3. WAIT-FOR-TIMER "NORMAL" MODE (Modified)
4. THE TIMER-COMPARE METHOD
5. THE TIMER OVER-FLOW INTERUPT METHOD
6. THE TIMER OVER-FLOW INTERUPT METHOD (Modified)
7. THE TIMER COMPARE-MATCH INTERUPT METHOD
8. THE CTC "AUTOMATIC" WAVE-FORM MODE
9. THE FAST PWM MODE 7
10. THE PWM FAST MODE 3 WITH DUTY CYCLE
11. THE PWM PHASE CORRECT MODE
12. TIMER MATHEMATICS
13. FINAL PWM PROJECT: DIM AN LED

Computer timer/counters are used to perform tasks at a set interval. A timer that increments a counter
once every second could be used to make a real-time clock by keeping track of the hours, minutes &
seconds that have elapsed.

For this primer I use the AVR ATtiny13 for its simplicity & small number of pins. This chip has one
timer and one I/O port (Port B) of which Bits 0-4 can be used for output. The ATtiny13 runs at 1.2MHz
( 9.6MHz Oscillator/8 ). The example programs should run on the Attiny25, Attiny45 or Attiny85. The
circuits & code should be easy to adapt to most any AVR Core chips.

To show our progress we connect an LED on Port B, Pin 0 to ground and making it blink. If you use
more than 3 volts, insert a 100-220 Ohm resistor in-line with the LED.

The circuit diagram for our ATtiny13 is:

```                   +3VDC      PINOUTS: 1-PORTB,5 (/RESET)
ATTINY13  |                   2-PORTB,3
.-----.  |                   3-PORTB,4
--|1 A 8|--'                   4-GROUND
--|2 T 7|--                    5 PORTB,0
--|3 N 6|--                    6-PORTB,1
.--|4 Y 5|--D LED (PORTB0)      7-PORTB,2
|  `-----'  |                   8-Vcc
|           |
|           |
`-----------+ GND
```

CHAPTER 1: A PAUSE ROUTINE
===========================

A simple timer is a software pause (or wait) routine. They go around a software loop and do nothing
but waste time. By changing the number of times the program goes around the loop we can adjust the
time it spends there.

```                PAUSE         PAUSE       PAUSE
ON:         +------+      +------+      +----->
|      |      |      |      |     ETC.
|      |      |      |      |
OFF: -+-----+      +------+      +------+
PAUSE         PAUSE         PAUSE

```

Here is a program that blinks the LED about twice per second. (An explanation follows the listing).

```        ;--------------------------------------------------;
; AUTHOR: DANIEL J. DOREY (RETRODAN@GMAIL.COM)     ;
;--------------------------------------------------;

.INCLUDE "TN13DEF.INC"   ;(ATTINY13 DEFINITIONS)

.DEF A = R16             ;GENERAL PURPOSE ACCUMULATOR
.DEF I = R20             ;INDEX
.DEF N = R22             ;COUNTER

.ORG 0000
ON_RESET:
SBI DDRB,0           ;SET PORTB0 FOR OUTPUT

;--------------;
; MAIN ROUTINE ;
;--------------;
MAIN_LOOP:
SBI   PINB,0       ;TOGGLE THE 0 BIT
RCALL PAUSE        ;WAIT/PAUSE
RJMP MAIN_LOOP    ;GO BACK AND DO IT AGAIN

;----------------;
;PAUSE ROUTINES  ;
;----------------;
PAUSE: LDI N,0           ;DO NOTHING LOOP
PLUPE: RCALL MPAUSE      ;CALLS ANOTHER DO NOTHING LOOP
DEC N             ;CHECK IF WE COME BACK TO ZERO
BRNE PLUPE       ;IF NOT LOOP AGAIN
RET             ;RETURN FROM CALL

MPAUSE:LDI I,0           ;START AT ZERO
MPLUP: DEC I             ;SUBTRACT ONE
BRNE MPLUP       ;KEEP LOOPING UNTIL WE HIT ZERO
RET             ;RETURN FROM CALL
```

First the assembler reads the file TN13DEF.INC that contains standard definitions for the Attiny13,
then we add some of our own definitions for registers that we use.

```        .INCLUDE "TN13DEF.INC"   ;(ATTINY13 DEFINITIONS)
.DEF A = R16             ;GENERAL PURPOSE ACCUMULATOR
.DEF I = R20             ;INDEX
.DEF N = R22             ;COUNTER
```

.ORG tells the assembler we want our program at bottom of memory (since we have no interrupts):

```        .ORG 0000
```

We use bit zero of Port B as output, so we write a one to the first bit of the Data Direction Register for
Port B (DDRB):

```     ON_RESET:
SBI DDRB,0           ;SET PORTB0 FOR OUTPUT
```

In the main loop we flip the 0-bit of Port B then call the pause routine in an infinite loop:

```        MAIN_LOOP:
SBI   PINB,0       ;FLIP THE 0 BIT
RCALL PAUSE        ;WAIT/PAUSE
RJMP MAIN_LOOP    ;GO BACK AND DO IT AGAIN
```

The next two subroutines waste time by going in loops. The first PAUSE routine calls the second
routine (MPAUSE) to slow things down so we can see the LED blinking. Both routines load zero into a
register then repeatedly subtracts one until it hits zero again (subtracting one from zero gives 255):

```        PAUSE: LDI N,0           ;DO NOTHING LOOP
PLUPE: RCALL MPAUSE      ;CALLS ANOTHER DO NOTHING LOOP
DEC N             ;CHECK IF WE COME BACK TO ZERO
BRNE PLUPE       ;IF NOT LOOP AGAIN
RET             ;RETURN FROM CALL

MPAUSE:LDI I,0           ;START AT ZERO
MPLUP: DEC I             ;SUBTRACT ONE
BRNE MPLUP       ;KEEP LOOPING UNTIL WE HIT ZERO
RET             ;RETURN FROM CALL
```

CHAPTER 2: WAIT-FOR-TIMER "NORMAL" MODE
===========================================

Next we use the chip's timer. When operated in "normal" mode, it will count up from zero to 255 then
set an over-flow flag. So we wait for this flag to be set each time. I think a lot of the confusion about
timers could be eliminated if everyone just called them counters instead. They are counting circuits that
people use to create timers.

```
FLAG             FLAG
0      255      0       255
ON:        +-------+       +-------+
|       |       |       |
|       |       |       |
OFF: ------+       +-------+       +----> ETC.
0     255     0       255     0    255
FLAG            FLAG         FLAG
```

We set a "pre-scaler" to 1024 that divides-down the system clock by 1024 so we can see the LED
blinking. I think a lot of confusion about pre-scalers could be avoided if they were just called dividers
instead. Note, setting the pre-scaler also activates the timer/counter.

```            LDI A,0b0000_0101    ;SET TIMER PRESCALER TO /1024
OUT TCCR0B,A
```

The rest of the code is the same as our last program, except for the PAUSE routine. This time we wait
for the timer to overflow past 255 (and the overflow flag gets set). Then we reset the flag. To reset the
flag we write a one (not a zero):

```         .INCLUDE "TN13DEF.INC"   ;(ATTINY13 DEFINITIONS)
.DEF A = R16             ;GENERAL PURPOSE ACCUMULATOR

.ORG 0000
ON_RESET:
SBI DDRB,0           ;SET PORTB0 FOR OUTPUT
LDI A,0b0000_0101    ;SET TIMER PRESCALER TO /1024
OUT TCCR0B,A

MAIN_LOOP:
SBI   PINB,0       ;FLIP THE 0 BIT
RCALL PAUSE        ;WAIT
RJMP MAIN_LOOP    ;GO BACK AND DO IT AGAIN

PAUSE:
PLUPE: IN   A,TIFR0        ;WAIT FOR TIMER
ANDI A,0b0000_0010  ;(1<<TOV0)
BREQ PLUPE
LDI  A,0b0000_0010  ;RESET FLAG
OUT  TIFR0,A        ;NOTE: WRITE A 1 (NOT ZERO)
RET
```

CHAPTER 3: MODIFIED WAIT-FOR-TIMER "NORMAL" MODE

The last method provides us with only five speeds, because the pre-scalers can only be set to five
values (1,8,64,256,1024). For more control we can pre-load the counter to any value from zero to 255,
this will shorten the time it takes to reach 255 and over-flow. In the example below we pre-load timer
with 128 each time.

```
FLAG        FLAG
128   255   128   255
ON:       +-----+     +-----+
|     |     |     |
|     |     |     |
OFF: -----+     +-----+     +---> ETC.
128   255   128   255   128   255
FLAG        FLAG        FLAG
```

For example, to blink the LED twice as fast we pre-load the timer to 128 with the following two lines:

```              LDI   A,128        ;PRELOAD TIMER WITH 128
OUT   TCNT0,A
```

Our modified program goes twice as fast and our MAIN routine now looks like this:

```         ;--------------;
; MAIN ROUTINE ;
;--------------;
MAIN_LOOP:
SBI   PINB,0       ;FLIP THE 0 BIT
RCALL PAUSE        ;WAIT
LDI   A,128        ;PRELOAD TIMER WITH 128
OUT   TCNT0,A
RJMP MAIN_LOOP    ;GO BACK AND DO IT AGAIN
```

CHAPTER 4: THE TIMER-COMPARE METHOD
(CTC MODE CLEAR-TIMER-on-COMPARE)
=======================================

The next method is very similar to the last. This time we set a "compare" value and a flag goes off
when the timer reaches this value instead of 255. This simplified our program because we don't have to

```                       MATCH       MATCH
0     128   0     128
ON:       +-----+     +-----+
|     |     |     |
|     |     |     |
OFF: -----+     +-----+     +---> ETC.
0    128   0     128   0     128
MATCH       MATCH       MATCH
```

First we configure the timer into "CTC" mode and set the pre-scaler to 1024:

```            LDI A,0b0100_0010    ;SET TO CTC MODE
OUT TCCR0A,A
LDI A,0b0000_0101    ;SET PRESCALER TO /1024 AND
OUT TCCR0B,A
```

We need to set the compare register to the value we want. Once again we use a value of 128:

```            LDI A,128            ;OUR COMPARE VALUE
OUT OCR0A,A          ;INTO THE COMPARE REGISTER
```

Then we wait for the compare flag to be set. (Note that we are waiting for a different flag this time):

```            PAUSE:
PLUPE: IN   A,TIFR0        ;WAIT FOR TIMER
ANDI A,0B0000_0100  ;(1<<OCF0A)
BREQ PLUPE
```

Finally we reset the compare flag after it is triggered, (to be ready for the next compare):

```          LDI  A,0b0000_0100  ;CLEAR FLAG
OUT  TIFR0,A        ;NOTE: WRITE A 1 (NOT 0)
RET
```

The resulting code should now look like this:

```        .INCLUDE "TN13DEF.INC"   ;(ATTINY13 DEFINITIONS)
.DEF A = R16             ;GENERAL PURPOSE ACCUMULATOR

.ORG 0000
ON_RESET:
SBI DDRB,0           ;SET PORTB0 FOR OUTPUT
LDI A,0b0100_0010    ;ACTIVATE CTC MODE
OUT TCCR0A,A         ;SET FLAG ON COMPARE MATCH
LDI A,0b0000_0101    ;
OUT TCCR0B,A         ;SET PRESCALER TO /1024
LDI A,128            ;OUR COMPARE VALUE
OUT OCR0A,A          ;INTO THE COMPARE REGISTER
MAIN_LOOP:
SBI   PINB,0       ;FLIP THE 0 BIT
RCALL PAUSE        ;WAIT
RJMP MAIN_LOOP    ;GO BACK AND DO IT AGAIN

PAUSE:
PLUPE: IN   A,TIFR0        ;WAIT FOR TIMER
ANDI A,0B0000_0100  ;(1<<OCF0A)
BREQ PLUPE
LDI  A,0b0000_0100  ;CLEAR FLAG
OUT  TIFR0,A        ;NOTE: WRITE A 1 (NOT 0)
RET
```

(You must Login to see the Files)

Attachment(s):

Visit AVR Assembler site http://avr-asm.tripod.com

Last Edited: Sun. Sep 19, 2010 - 03:01 AM

.org 0 I was wondering about that. I knew that interrupt vectors where around down there. I tend to just let the assembler gavrasm deal with the org issue. Another great tutorial. Even though I use a atmega8 the code works fine with it, just needs a few weaks here and there.

Cheer
Rich

very nice tutorial i had trouble with timers but it really helped

eager to learn

Awesome some example ASM code! Can I use inline assembler in the C dev tools? I havn't installed them yet........... Also I don't understand BREQ PLUPE is this the cpu function that does the compare? I'll have to read up on AVR asm.........

Last Edited: Sun. Mar 29, 2020 - 08:20 AM

AS7 comes with two assemblers, use the Atmel one if you want to do Asm alone. Only if you want to mix C/C++ and asm then use avr-as instead.
.
Get the AVR Opcode Manual which will explain BREQ (Branch if equal to 0) and all other AVR opcodes.
.

The 2.2 version has a bug when resetting/checking OCF0A I think:

```ANDI A,0B0000_0100  ;(1<<OCF0A)
```

The bits are wrong. From the datasheet:

Bit 4 – OCF0A: Output Compare Flag 0 A

The correct bits sould be like this according to the datasheet:

ANDI A,0B0001_0000 ;(1<<OCF0A)

Doesn't this simply prove why you should not use binary constants? If the author had used (as in the comment!!):

`ANDI A, (1 << OCF0A)`

it would have assembled the right bit whether it were 2, 4 or whatever. It also aids portability - OCF0A is not always in the same bit position:

```C:\Program Files (x86)\Atmel\Studio\7.0\packs\atmel\ATtiny_DFP\1.8.332\avrasm\inc>grep OCF0A *
tn102def.inc:.equ       OCF0A   = 1 ; Timer Output Compare Flag 0A
tn104def.inc:.equ       OCF0A   = 1 ; Timer Output Compare Flag 0A
tn10def.inc:.equ        OCF0A   = 1     ; Timer Output Compare Flag 0A
tn13Adef.inc:.equ       OCF0A   = 2     ; Timer/Counter0 Output Compare Flag 0A
tn13def.inc:.equ        OCF0A   = 2     ; Timer/Counter0 Output Compare Flag 0A
tn1634def.inc:.equ      OCF0A   = 0     ; Timer/Counter0 Output Compare Flag 0A
tn167def.inc:.equ       OCF0A   = 1     ; Output Compare Flag 0A
tn20def.inc:.equ        OCF0A   = 1     ; Output Compare Flag 0 A
tn20def.inc:;.equ       OCF0A   = 1     ; Output Compare Flag 0 A
tn2313Adef.inc:.equ     OCF0A   = 0     ; Timer/Counter0 Output Compare Flag 0A
tn2313def.inc:.equ      OCF0A   = 0     ; Timer/Counter0 Output Compare Flag 0A
tn24Adef.inc:.equ       OCF0A   = 1     ; Timer/Counter0 Output Compare Flag A
tn24def.inc:.equ        OCF0A   = 1     ; Timer/Counter0 Output Compare Flag A
tn25def.inc:.equ        OCF0A   = 4     ; Timer/Counter0 Output Compare Flag 0A
tn261Adef.inc:.equ      OCF0A   = 4     ; Timer/Counter0 Output Compare Flag 0A
tn261def.inc:.equ       OCF0A   = 4     ; Timer/Counter0 Output Compare Flag 0A
tn40def.inc:.equ        OCF0A   = 1     ; Output Compare Flag 0 A
tn4313def.inc:.equ      OCF0A   = 0     ; Timer/Counter0 Output Compare Flag 0A
tn43Udef.inc:.equ       OCF0A   = 1     ; Timer/Counter0 Output Compare Flag A
tn441def.inc:.equ       OCF0A   = 1 ; Timer/Counter0 Output Compare Flag A
tn44Adef.inc:.equ       OCF0A   = 1     ; Timer/Counter0 Output Compare Flag A
tn44def.inc:.equ        OCF0A   = 1     ; Timer/Counter0 Output Compare Flag A
tn45def.inc:.equ        OCF0A   = 4     ; Timer/Counter0 Output Compare Flag 0A
tn461Adef.inc:.equ      OCF0A   = 4     ; Timer/Counter0 Output Compare Flag 0A
tn461def.inc:.equ       OCF0A   = 4     ; Timer/Counter0 Output Compare Flag 0A
tn48def.inc:.equ        OCF0A   = 1     ; Timer/Counter0 Output Compare Flag 0A
tn4def.inc:.equ OCF0A   = 1     ; Timer Output Compare Flag 0A
tn5def.inc:.equ OCF0A   = 1     ; Timer Output Compare Flag 0A
tn828def.inc:.equ       OCF0A   = 1 ; Timer/Counter0 Output Compare Flag 0A
tn841def.inc:.equ       OCF0A   = 1 ; Timer/Counter0 Output Compare Flag A
tn84Adef.inc:.equ       OCF0A   = 1     ; Timer/Counter0 Output Compare Flag A
tn84def.inc:.equ        OCF0A   = 1     ; Timer/Counter0 Output Compare Flag A
tn85def.inc:.equ        OCF0A   = 4     ; Timer/Counter0 Output Compare Flag 0A
tn861Adef.inc:.equ      OCF0A   = 4     ; Timer/Counter0 Output Compare Flag 0A
tn861def.inc:.equ       OCF0A   = 4     ; Timer/Counter0 Output Compare Flag 0A
tn87def.inc:.equ        OCF0A   = 1     ; Output Compare Flag 0A
tn88def.inc:.equ        OCF0A   = 1     ; Timer/Counter0 Output Compare Flag 0A
tn9def.inc:.equ OCF0A   = 1     ; Timer Output Compare Flag 0A```

In fact that shows you are quite wrong - the value is 2 for the tiny13 that the code was written for. So if you insist on using binary constants it is 0b0000_0100 and not 0b0001_0000.

In fact this:

```C:\Program Files (x86)\Atmel\Studio\7.0\packs\atmel\ATtiny_DFP\1.8.332\avrasm\inc>grep -C 2 OCF0A tn13def.inc
; TIFR0 - Timer/Counter0 Interrupt Flag register
.equ    TOV0    = 1     ; Timer/Counter0 Overflow Flag
.equ    OCF0A   = 2     ; Timer/Counter0 Output Compare Flag 0A
.equ    OCF0B   = 3     ; Timer/Counter0 Output Compare Flag 0B
```

shows that the only active bits in TIFR0 for Tiny13 are 1, 2 and 3. Your bit 4 is not valid.

Are you talking about a different AVR than the Tiny13 in this article perhaps? As my output above shows it is bit 4 for tiny25, tiny261, tiny45, tiny461, tiny 85, tiny861.

If it is a different AVR that just further highlights why (1 << OCF0A) is a much better idea !

EDIT: I see in another thread you posted on that this is likely a tiny85? That is why it's different between tiny13 and tiny85.

Last Edited: Wed. Feb 17, 2021 - 10:08 AM