タイマー割り込み


概要

シリアル通信でデバッグする手段が整ったので,次はスレッド切り替えの契機として利用する予定のタイマ割り込みを実装する.Arduinoは割り込みベクタのアドレスを変更することができ,そのアドレスに合わせて割り込みの種類に合わせた割り込みハンドラを設定する必要がある.そして,タイマーを有効とし,どのくらいの間隔で割り込みを入れるかを設定する.

割り込みベクタ

Atmega328pの割り込みベクタは,次のように決められている

割り込みベクタの定義

割り込みベクタの仕様を見るとわかるように,0x0000がRESETで,電源が入る,あるいはリセットボタンを押されるとここに飛んできてプログラムが起動する(Notesにあるように,これはBOOTRSTヒューズビットの設定に依存するが,ここでは割愛).
この中で,Timerと書いてあるのは9個で,3つ一組で1つのタイマの設定だということが想像できる.想像通り,Atmega328pには3つのタイマが内蔵されている.この決められたアドレスに関数のアドレスを設定することで,ここの割り込みが発生した時(割り込みが有効になっている必要があるが),設定した関数が呼び出されることになる.

割り込みベクタの変更

割り込みベクタが配置されるべき位置(アドレス)は,いかのようにIVSELビットの影響を受ける.

これをみると,IVSELが0のときには(Reset Address)の位置にかかわらず0x0002となり,1のときにはBoot Reset Address + 0x0002となっている.これはつまり,1のときにはリセットしたときにプログラムが開始したアドレス+0x0002の位置にに割り込みベクタが配置されるということで,リセットアドレスはBOOTRSTで変更可能となっている.
Arduinoは,リセットされるとブートローダが実行されるため,リセットアドレスはBOOTRSTは0になっており,従って,IVSELが1だと割り込みベクタはブートローダの領域に設置されることになる.

これは困るので,プログラムが起動した時,直ちにIVSELを0にして,割り込みベクタを一般のプログラムが制御可能な0x0002に移動しなければならない.このIVSELは,MCUCRレジスタの1ビット目にある.

IVSELの設定方法は,仕様書に書いてある.

つまり,

  1. IVCEビットを1にする.
  2. CPUが4サイクル回る前に,IVSELを設定

ということらしい.この4サイクルがなぜ必要かは,IVCEの説明に書いてある

これを読むと,IVCEはハードウェアで4サイクルごとに0にされるらしい.よって,IVCEが1にした後,4サイクル以内にIVSELを設定しなければならない,ということになる.

タイマ割り込みの設定

Atmega328pには3種類のタイマー,Timer0, Timer1, Timer2があり,それぞれ8ビット,16ビット,8ビットとなっている.タイマーの原理は,クロックが入るたびにカウンターを加算していき,その値が設定した値と同じくなったら,あるいはオーバフローしたら,割り込みを入れる,という仕組みになっている.そして,タイマーのビット数は,今カウンターのビット数であり,ビット数が多ければ多いほど,長い時間待てることになる.

例えば,1MHzのクロックで,8ビットカウンターを使った場合,

(1/10000000) x 255 = 0.000255(s) = 0.225(ms)

となるので,最大でも0.225(ms)しか待てないことになる.

これでは困るので,分周率というものが設定でき,本来1クロックでカウントアップするところを,分周率で設定した値までからやっとクロックを1つ加算する,ということができる.
例えば,分周率64にすると,

0.225 x 64 = 14.4(ms)

となり,最大で14.4(ms)待てる.

ここでは,Timer0の設定を使ってみる.

まず,Tiemr0の定義から

このTOPみると,TOPは0xFFか,OCR0Aレジスタの値,と書かれている.そして,どちらを使うかはmodeによる,とされている.モードは次の8種類ある.

ここでは,割り込みの時間を任意に設定したいので,自分で上限を設定できるCTCモードを使いたい.すると,その設定はWGM0[0-2]ビットの組み合わせで設定することがわかる.
これらのビットは,以下に示す.TCCR0A, TCCR0Bレジスタに存在する.


分周率の設定は,次のようになっている.

これをみると,分周率は8, 64, 256, 1024で,CS[0-2]ビットの組み合わせで設定する事がわかり,これらのビットはTCCR0Bの下位3ビットであることがわかる.

最後に,このタイマーを有効にする必要がある.

このように,タイマーの有効/無効はTIMSK0レジスタで行うことがわかる.


この仕様を見ると,OCRAとマッチしたときにタイマー割り込みを入れるには,OCIE0Aを1にすれば良いことがわかる.

割り込みベクタの実装

IVSELを0にするとして,atmega328pの仕様で決められたとおりに割り込みベクタに対応する関数のアドレスを配置する必要がある.そこで,vector.sを書く.

vector.s
    .section .text
    .global start
    .type   start,@function
reset:          jmp start
int0:           jmp nointr
int1:           jmp nointr
pcint0:         jmp nointr
pcint1:         jmp nointr
pcint2:         jmp nointr
wdt:            jmp nointr
timer2_compa:   jmp nointr
timer2_compb:   jmp nointr
timer2_ovf:     jmp nointr
timer1_capt:    jmp nointr
timer1_compa:   jmp nointr
timer1_compb:   jmp nointr
timer1_ovf:     jmp nointr
timer0_compa:   jmp intr_time // Timer0CTCモードの割り込みハンドラ
timer0_compb:   jmp nointr
timer0_ovf:     jmp nointr
spi_stc:        jmp nointr
usart_rx:       jmp nointr
usart_udre:     jmp nointr
usart_tx:       jmp nointr
adc:            jmp nointr
ee_rdy:         jmp nointr
analog_comp:    jmp nointr
twi:            jmp nointr
spm_ready:      jmp nointr

nointr:         jmp nointr

今回はTimer0を使い,CTCモードでOCR0Aレジスタの値と比較するので,timer0_compaの位置に割り込みハンドラのアドレスを設定する.

さらに,これをアセンブルしてできるvector.oは,できるバイナリの先頭に配置しなければならない.そのため,リンカスクリプトを変更する

ld.scr
OUTPUT_FORMAT("elf32-avr")
OUTPUT_ARCH(avr)
ENTRY("start")

SECTIONS
{
    . = 0x0;

    .vectors : {
        vector.o(.text)
    }

    .text : {
        *(.text)
    }
    .rodata : {
        *(.strings)
        *(.rodata)
        *(.rodata.*)
    }

    .data : {
        *(.data)
    }

    .bss : {
        *(.bss)
        *(COMMON)
    }

    . = 0x8007fc;
    /*. = 0x0007fc;*/ /* the uppper is also OK */

    .bootstack : {
        _bootstack = .;
    }
}

追加したのは,

    . = 0x0;
    .vectors : {
        vector.o(.text)
    }

で,これでvector.oの中に含まれるテキストセグメントを.vectorsの位置に配置する,ということができる.その後,リンカはその他のテキストやデータを配置していく.

次は,割り込みハンドラの実装.とりあえず,startup.sに追加してしまう.

startup.s

     .global intr_time
     .type   intr_time, @function
intr_time:
     cli
     rcall t0a
     reti

これは,cliで割り込み禁止し,t0aを呼び出した後,retiでもとに戻ると同時に割り込み可能にする,という処理をしている.これは,割り込みハンドラが呼ばれると,その途中で割り込みが発生してしまうことを防ぎ,ハンドラの終了時には再度割り込み可能に戻す必要があるためである.
以下が,割り込みハンドラから呼ばれる関数t0a

main.c

void t0a() {
    putc('A');
    putc('B');
    return;
}

これで,タイマー割り込みが入ると,シリアルから"AB"が出力されるようになる(はず).

タイマの設定

タイマ割り込みのために,TCCR0A, TCCR0B, OCR0Aを設定し,最後にOCIE0Aを1にセットして,OCR0Aで設定した値とマッチしたときの割り込みを有効化する.

serial.c
#define IO_OFFSET 0x20
#define MEM8(addr) (*(volatile unsigned char *)(addr))
#define IO8(addr)  MEM8((addr) + IO_OFFSET)
#define TCCR0A  IO8(0x24)
#define TCCR0B  IO8(0x25)
#define TIMSK0  MEM8(0x6E)
#define OCR0A   IO8(0X27)
#define OCR0B   IO8(0X28)


void timer_init() {
    TCCR0A = 0b00000010; // CTCでOCR0Aとの一致で割り込み設定
    TCCR0B = 0b00000101; // 分周率1024
    OCR0A = 235;  // 15msごとに割り込み
    TIMSK0 = 0b0000010;    //比較A一致割り込み有効
}

とりあえず,15msくらいで割り込みを入れるため,仕様に従い,CTCモードで設定し,分周率は1024を使う.
OCR0Aは,次の式を解いて近似値を求める

OCR0A = (16000000 / 1024) * 0.0015 = 234.375

割り込みベクタの移動

仕様に従い,IVCEを1にした後,IVSELを0にする.

serial.c
#define IO_OFFSET 0x20
#define MEM8(addr) (*(volatile unsigned char *)(addr))
#define MCUCR_IO    IO8(0x35)
#define MCUCR_IVCE  (1<<0)
#define MCUCR_IVSEL (1<<1)


void vector_init() {
  MCUCR_IO = MCUCR_IVCE;
  MCUCR_IO = 0;
}

割り込みの実行

これで必要なすべての準備が整ったので,タイマー割り込みを実行してみる

main.c
#define INTR_ENABLE  asm volatile ("sei")
#define INTR_DISABLE asm volatile ("cli")

int main(void) {
    INTR_DISABLE;   // 割り込み禁止

    vector_init();  // 割り込みベクタの移動
    serial_init();  // シリアルの初期化
    timer_init();   // タイマーの設定

    INTR_ENABLE;    // 割り込み有効
    return 0;
}

単に,ここまで定義した関数を順番に呼び出すだけ.ただ,割り込み禁止にして呼び出し,最後に割り込みを有効にする.

できてます.

以下で実行できます.

>git clone https://github.com/hiro4669/iosv.git
>cd iosv
>git branch timer_ver1 origin/timer_ver1
>git checkout timer_ver1
>cd timer
>make
>make write