第 6 回 割り込み

本日の内容


このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。

なお、この章で定義したマクロを含むマクロ集は gs25.inc とまとめてあります。

6-1. 割り込み

外部の刺激などに対して、CPU があらかじめ指定した番地を call するのを 割り込みと言います。 call される番地を割り込みベクタと言います。PIC では 4 番地 が割り込みベクタです。

ここでは前回の演習問題4-9 を改造して、タイマ割り込みにより一定周期でカ ウンタを増やすプログラムを作ることを考えます。

PIC に内蔵されているタイマ Timer0 は ffh から 00h に変わる時に割り込み が発生します。 この時呼び出される、counter を 1 増やし、 counter の値を表示するサブルー チンを呼ぶプログラムを作ります。

一方、PIC が起動する時に実行するのは初期設定のみ、つまり、counter を 0 にし、それを表示することだけです。 これを終えて、主プログラムは無限ループに入り、全ての処理を割り込みに委 ねます。

割り込みの設定

PIC の内部クロックは 4MHz です(データシート 14.2.4 p.98)。 4 クロックで1 命令実行しますので(データシート 3.1, 3.2 p.13)、PIC は内部クロックで一秒間に 100万命令実行します。 一つの命令が動作する最小時間を マシンサイクルと言います。 Timer0 を内部クロックで動作させると、 1 マシンサイクル毎に 1 つずつ 8bit のカウンタが増えて行きます。そして、オーバフローした時に割り込みが かかります。

Timer0 にはプリスケーラがあります(データシート 6.3 Timer0 Prescaler p.46)。 これは割り込み間隔を調整するため、Timer0 のカウンタを増やす間隔を 2 マ シンサイクル毎から 256 マシンサイクル毎までに調整するものです。

例6-1

演習 4-9 のプログラムを改造します。 counter という変数を用意し、次のサブルーチンを用意します。

  1. counter の値を表示するサブルーチン
  2. counter を 1 増やし、 counter の値を表示するサブルーチン

このような準備をした後、次のプログラムを作ります。

  1. 割り込みの設定をします。
  2. counterを 0 にし、LED を光らせます。
  3. 無限ループに入ります。

また、割り込みの処理は次のようにします。

  1. まず 4 番地から割り込み処理のプログラムに飛ぶようにします。
  2. レジスタを保護します。 割り込みが発生すると、処理が 4 番地に移ります。割り込みの 処理が終ると元の処理が再開されます。 その際、処理の途中でレジスタの値が予期せぬ値になっていては正常に動作が できなくなります。 今回の主プログラムは単なる無限ループですので、特に保護する必要はなく、 無駄な処理ではありますが、かならず保護する事にします。 ユーザ領域のファイルレジスタを共有する事はないですが、 W レジスタと STATUS レジスタは必ずプログラムで使用しますので、これだけ退避させます。
  3. 割り込みの原因を調べます。今回は Timer0 しか発生しないはずですが、 一応 Timer0 かどうかを関連するファイルレジスタの値により調べます。
  4. Timer0 の割り込みが発生した場合は、その処理を行います。
  5. 割り込みの処理が終ったら、Timer0 割り込みを初期化し、退避していた レジスタ値を戻し、 RETIE 命令により割り込みを終了します。

なお、割り込みの設定は OPTION_REG に Timer0 の仕様を入れます。 今回は内部クロックを使い、 Prescaler を 1:256 で使用します。 (デフォルト値は全て 1 にセットされています(データシート TABLE 6-1 p. 47)。 そして、 INTCON レジスタの割り込み許可 T0IE をセットし、割り込みフラグ T0IF をクリアし、全体の割り込み許可 GIE をセットします。

プログラムは次のようになります


;**********
;*  例6-1 *
;**********
        #include "gs25.inc"

	cblock 
	 savew
	 savest
	endc

	org	0x0000
	goto	start
	org	0x0004
	goto	intvec
	org	0x0008
start
        enable_PB
	clrwdt
	banksel OPTION_REG
;	movlw   b'11010000' ; 内部クロック、 PS=1:2
	movlw   b'11010111' ; 内部クロック、 PS=1:256
	movwf   OPTION_REG
	banksel INTCON
	bsf     INTCON,T0IE
	bcf	INTCON,T0IF
	bsf	INTCON,GIE
	counter_init
main
        goto    main

intvec  
        movwf   savew
	movf    STATUS,0
	movwf   savest
        btfss   INTCON,T0IF
	goto    nottimer0
	bcf     INTCON,T0IF
	call    inccounter
nottimer0
        movf    savest,0
	movwf	STATUS
	movf    savew,0
        retfie

	counter_prg pat_end-pat_start

	getpat_num_prg

	end

ここで、この割り込みに関する部分をマクロにしましょう。


timer0_init macro psmode
	cblock 
	 savew
	 savest
	endc
	clrwdt
	banksel OPTION_REG
	movlw   psmode
	movwf   OPTION_REG
	banksel INTCON
	bsf     INTCON,T0IE
	bcf	INTCON,T0IF
	bsf	INTCON,GIE
	endm

timer0_prg macro func
timer0 
        movwf   savew
	movf    STATUS,0
	movwf   savest
        btfss   INTCON,T0IF
	goto    nottimer0
	bcf     INTCON,T0IF
	call    func
nottimer0
        movf    savest,0
	movwf	STATUS
	movf    savew,0
        retfie
        endm

マクロ化したプログラムは次のようになります。


;************
;*  例6-1 2 *
;************
        #include "gs25.inc"


	org	0x0000
	goto	start
	org	0x0004
	goto	timer0
	org	0x0008
start
        enable_PB
	timer0_init b'11010111' ; 内部クロック、 PS=1:256
	counter_init
main
        goto    main

	timer0_prg inccounter

	counter_prg pat_end-pat_start

	getpat_num_prg

	end

例6-2

例6-1ではカウンタの動きが速すぎます。 Prescaler を使用しても最大で 1/256 にしかなりません。 内部クロック 4MHz では 1 マシンサイクルが 1μ秒なので、カウンタ一つ 増やす間隔は 256×256μ秒 ≒ 0.066 秒に一回になってます。 これを大体 1 秒の間隔にするにはさらに 15 倍程度遅くする必要があります。

そこで大体 1 秒毎に増えるカウンタを作りましょう。 そのためには割り込みがかかっても 15 回に一回しか反応しないようにします。 その機能を持つサブルーチンの名前を Postscalerと呼ぶ事にしま す。


;**********
;*  例6-2 *
;**********
        #include "gs25.inc"

	#define post_rate d'15'-1
	cblock
	 post_count
	endc

	org	0x0000
	goto	start
	org	0x0004
	goto	timer0
	org	0x0008
start
        enable_PB
	movlw	post_rate
	movwf	post_count
	timer0_init b'11010111' ; 内部クロック、 PS=1:256
	counter_init
main
        goto    main

	timer0_prg postscaler

postscaler
	decfsz	post_count,1
	return
	movlw	post_rate
	movwf	post_count
	call    inccounter
	return

	counter_prg pat_end-pat_start

	getpat_num_prg

	end

この postscaler もマクロ化しましょう。


postscaler_init macro rate
        #define post_rate rate
	cblock
	 post_count
	endc
	movlw	post_rate
	movwf	post_count
	endm

postscaler_prg macro func
postscaler
	decfsz	post_count,1
	return
	movlw	post_rate
	movwf	post_count
	call    func
	return
	endm

マクロ化すると次のようになります。


;************
;*  例6-2 2 *
;************
        #include "gs25.inc"

	org	0x0000
	goto	start
	org	0x0004
	goto	timer0
	org	0x0008
start
        enable_PB

	postscaler_init d'15'-1

	timer0_init b'11010111' ; 内部クロック、 PS=1:256

	counter_init
main
        goto    main

	timer0_prg postscaler

	postscaler_prg inccounter

	counter_prg pat_end-pat_start

	getpat_num_prg

	end

演習6-1

LED の小数点を約 0.5 秒程度で点滅させなさい。 さらに、 RA5 に接続したスイッチを押す度に LED の数字が増えるようにしな さい。

なお、レジスタ中の複数のビットを On, Off するには AND と OR 演算が有効で す。bit X を bit Y に変えるには (( X and 0) or Y) (C 言語だと X&=0; X|=Y;)とします。

6-2. LED の明るさを変える(DUTY 比)

LED の明るさを変えるには二つの方法があります。 今回は DUTY 比のコントロールについて考えます。 この方法は、早い周期で LED を点けたり消したりすることで、平均出力を下げる ことにより、人間の目には暗く見えるようにすることです。 この時、全体の中で ON になっている比率を DUTY 比と呼びます。

例6-3

演習 4-7 を思い出して下さい。 これは、RB0 から RB7 まで順番に点灯してくというプログラムでした。 その演習では、一つのポートの LED を点灯してから、次のポートを点灯させ るまで時間待ちをしました。 そこで、この waitを外したものを動かしてみましょう。 単純で改造がしやすい 4-7 2 のプログラムを使用します。 改造した物が次になります。 なお、全部消灯するパターンも取り除いています。


;**********
;*  例6-3 *
;**********

        #include "gs2.inc"

display macro pattern
        movlw   pattern
	movwf	PORTB
	endm

	org	0x0000
	goto	start
	org	0x0008
start
        enable_PB
main
        display b'00000001'
        display b'00000011'
        display b'00000111'
        display b'00001111'
        display b'00011111'
        display b'00111111'
        display b'01111111'
        display b'11111111'
        goto    main
	end

実行すると、 LED の明るさが揃ってないことがわかります。

この場合、RB0 の duty 比は 100% です。一方、 RB7 の duty 比は1/8 = 12.5% になります。 但し、実験してみれば分かるように duty 比の高い LED の明るさの違いはほ とんど分かりません。 人間の目は対数的な特性があるため、指数関数的な変化でないとはっきり把握 できません。 つまり、 duty 比を連続して変化させる時は、比率を連続的に変化させても効 果が薄いことに注意します。

例6-4

PORTA の表示パターンの明るさを変化させることを考えましょう。 DUTY比を変えるには PORTA の値を素早く ON, OFF させます。 つまり PORTA は常に同じ値ではなくなります。 そのため、 PORTA の表示パターンを A には書かず、別の領域に書くことにし ます。 そして、タイマ割り込みを使用し、 duty 比が 25% になるように 4 回に 1 回だけ表示するようにします。 なお比較のために、 PORTB は全て点灯させておきます。

プログラムにおいて、レジスタ crate の値は割り込みがかかる度に countrate, countrate-1, ... ,3,2,1, countrate, countrate-1, ... ,3,2,1, と周期的に変化して行きます。 そして、その度に あらかじめ W レジスタに入れた dutyrate の値から引き算します。 すると、crate - dutyrate < 0 となる時は C フラグが 0 になり、そうでなければ 1 になります。 ここで、 C フラグが 0 の時、つまり 1 ≤ crate < dutyrate の時に、PORTA を 光らせ、それ以外の時に消灯することで、 DUTY 比 = (dutyrate - 1)/countrate を実現します。 25% にするには countrate = 4, dutyrate = 2 とします。


;**********
;*  例6-4 *
;**********

	#include "gs25.inc"

	#define countrate d'4'
	#define dutyrate d'1'
	cblock 
	 crate
	 counter
	 patterna
	endc

	org	0x0000
	goto	start
	org	0x0004
	goto	timer0
	org	0x0008
start
        enable_PAPB
	timer0_init b'11010000' ; 内部クロック、 PS=1:2

	movlw   b'11111111'
	movwf	PORTB
	movwf	patterna
	movlw	countrate
	movwf	counter
	movlw	dutyrate
	movwf	crate

main
        goto    main

	timer0_prg duty

duty
	movf	counter,0
	subwf	crate,0
	btfsc	STATUS,C
	goto    raon
	clrf	PORTA
	goto	dend
raon	
        movf    patterna,0
	movwf	PORTA
dend
	decfsz	counter,1
	return
	movlw	countrate
	movwf	counter
	return

	end

なお、ここで duty もマクロ化しておきます。 on にする時と、 off にする時をサブルーチンで指定するようにします。 すると次のようにマクロ化できます。


duty_init macro 
	 cblock
	 duty_ratio,duty_period,duty_counter
	 endc
	 endm

duty_prg macro func_off, func_on
duty
	movf    duty_counter,0
	subwf	duty_ratio,0
	btfsc	STATUS,C
	goto    duty_on
	call	func_off
	goto	duty_end
duty_on
	call    func_on
duty_end
	decfsz	duty_counter,1
	return 
	movf     duty_period,0
	movwf	duty_counter
	return
	endm

これを使用すると、上記のプログラムは次のようになります。


;***********
;*  例6-4 2*
;***********

	#include "gs25.inc"

	cblock 
	 patterna
	endc

	org	0x0000
	goto	start
	org	0x0004
	goto	timer0
	org	0x0008
start
        enable_PAPB
	timer0_init b'11010000' ; 内部クロック、 PS=1:2

	duty_init
	movlw   d'4'
	movwf   duty_period
	movwf	duty_counter
	movlw   d'1'
	movwf	duty_ratio

	movlw   b'11111111'
	movwf	PORTB
	movwf	patterna
main
        goto    main

	timer0_prg duty

	duty_prg porta_off, porta_on

porta_off
	clrf	PORTA
	return

porta_on
        movf    patterna,0
	movwf	PORTA
	return

	end

例6-5

duty 比 1, 1/2, 1/4, 1/8, 1/16, 1/32, 1/64, 1/128 という階調を考えます。 これを RA5 に接続した sw により切替えることを考えます。 但し、これらは計算で求めるのではなく、以前に LED に数字を出した時のよ うにテーブルで参照するようにします。 W レジスタに求める duty 比を入れて、 getduty とすると必要な比 が W レジスタに得られるようにします。 但し、 0 の時、 128 を示し、 1 の時 64, 2 の時 32となる値、つまり最小 が 1 となる相対値を返します。

例6-4同様に PORTB は全て点灯させ、PORTA のみの明るさを変えます。

プログラムは基本的には 例6-4 と演習4-6 のプログラムを流用します。 但し duty 比はプログラムの中で 1 を加え、 duty 比 = (dutyrate - 1)/maxcount ではなく、 duty 比 = dutyrate/maxcount となるようにします。

このプログラムを実現するには、 counter の値により duty_ratio を変化させ る必要があります。 つまり、 sw が押されるたびに、 counter の値を変化させ、その値により duty_ratio を与える必要があります。 このサブルーチンを set_rate_by_counter という名前にしましょう。


;************
;*  例6-5 *
;************
        #include "gs25.inc"

	org	0x0000
	goto	start
	org	0x0004
	goto	timer0


	org	0x0008
start
        enable_PAPB
        timer0_init b'11010000' ; 内部クロック、 PS=1:2

	cblock
	patterna
	endc
	
	movlw   b'11111111'
	movwf	PORTB
	movwf	patterna

	duty_init

	cblock
	counter
	endc

	clrw
	movwf   counter
	call    getduty ; duty 0 を獲得
	movwf	duty_period
	movwf	duty_counter
	call    set_rate_by_counter
	sw_init

	sw_prg  swfunc

swfunc
	call    inccounter
	call    set_rate_by_counter
	return
	
inccounter
        incf    counter,1
	movlw   endduty-startduty 
	subwf   counter,0
	btfsc   STATUS,Z
        clrf    counter
	return

	timer0_prg duty

set_rate_by_counter
        movf    counter,0
	call    getduty
	movwf	duty_ratio
	return

	duty_prg porta_off, porta_on

porta_on
        movf    patterna,0
	movwf	PORTA
	return

porta_off
	clrf	PORTA
	return

getduty
        addwf   PCL,1
startduty
	retlw   d'128'
	retlw   d'64'
	retlw   d'32'
	retlw   d'16'
	retlw   d'8'
	retlw   d'4'
	retlw   d'2'
	retlw   d'1'
endduty
	end

なお、このプログラムだと、PORTA がちらついて見えます。 そのため、割り込みの周期を上げる必要があります。 一つは 1:2 である Prescaler を使わない(OPTION_REG<PSA> を 0 にす る)こと。もう一つは 128 をあきらめ、 64 や 32 にまでにするということが 考えられます。 ちらつきがなくなるよう、調整してみて下さい。

ちなみに筆者は下記のように PSA を on にして、 128 のみを削った状態で ちらつきがきにならなくなりました(個人差あり)。


;************
;*  例6-5-2 *
;************
	timer0_init   b'11011000' ; 内部クロック、 PS 不使用


getduty
        addwf   PCL,1
startduty
;	retlw   d'128'
	retlw   d'64'
	retlw   d'32'
	retlw   d'16'
	retlw   d'8'
	retlw   d'4'
	retlw   d'2'
	retlw   d'1'
endduty
	end

さて、この set_duty_by_counter, getduty, porta_on, porta_off もマクロ化しましょう。


set_rate_by_counter_prg macro
set_rate_by_counter
        movf    counter,0
	call    getduty
	movwf	duty_ratio
	return
	endm
getduty_prg macro
getduty
        addwf   PCL,1
startduty
;	retlw   d'128'
	retlw   d'64'
	retlw   d'32'
	retlw   d'16'
	retlw   d'8'
	retlw   d'4'
	retlw   d'2'
	retlw   d'1'
endduty
        endm
porta_init macro
        cblock
	  patterna
        endc
	movlw   b'11111111'
	movwf   patterna
	endm
porta_prg macro
porta_on
        movf    patterna,0
	movwf	PORTA
	return

porta_off
	clrf	PORTA
	return
	endm

マクロを使った例は以下の通り。


;************
;*  例6-5-2 *
;************
        #include "gs25.inc"

	org	0x0000
	goto	start
	org	0x0004
	goto	timer0
	org	0x0008
start
        enable_PAPB
        timer0_init b'11011000' ; 内部クロック、 PS 不使用
	porta_init
	movlw   b'11111111'
	movwf	PORTB
	duty_init

	cblock
	counter
	endc

	clrw
	movwf   counter
	call    getduty ; duty 0 を獲得
	movwf	duty_period
	movwf	duty_counter
	call    set_rate_by_counter
	sw_init

	sw_prg  swfunc

swfunc
	call    inccounter
	call    set_rate_by_counter
	return
	
inccounter
        incf    counter,1
	movlw   endduty-startduty 
	subwf   counter,0
	btfsc   STATUS,Z
        clrf    counter
	return

	set_rate_by_counter_prg

	timer0_prg duty
	duty_prg porta_off, porta_on
	porta_prg
	getduty_prg
	end

演習6-2

例6-5のプログラムで、 PORTB には counter の値を表示するように改造し なさい。

6-3. 複数の LED のコントロール

DUTY 比 50% の LED はそれほど暗くなく、これを標準の明るさとして用いて も問題ない明るさです。 したがって、二つの別の LED を高速に交互に点灯させると、人間の目には二 つの別々の LED が同時に光っているように見えます。

そこで、7 セグメント LED の二桁表示をすることを考えます。 カソードコモン LED の場合、カソードを GND に繋ぎますが、ここにスイッチ を付けることで、 LED の点灯、消灯をコントロールできます。 スイッチは高速で、 PIC の出力により ON, OFF が出来なければなりません。 そのためトランジスタのスイッチング回路を使います。 回路図を示します。

6-4. 演習の解説

演習6-1

始めに小数点を約 0.5 秒程度で点滅させることを考えます。 割り込みは Prescaler を 1:256 で使用し、さらに 7 回に一回毎に点滅させ ます。 点滅は点滅パターンを用意して PORTB に対して XOR をかけることで行います。 著者の回路では RB0 が小数点になってますので、 b'00000001' で XOR を行 います。

一方、 RA5 に接続したスイッチを押す度に LED の数字が増えるようにするに は、演習4-6 のプログラムを流用します。 但し、 PORTB にダイレクトに書き込むと小数点の点滅に影響してしまいます。 そこで、ビットパターンを特定の場所だけ上書きすることを考えます。 そのためには、「特定の場所を指定する」情報を考えなくてはいけません。 今、考えるのは PORTB の RA0 を温存したまま RA1 から RA7 までを変更する ことです。

  1. 特定のビットを 1 にする論理演算は or で行います。つまり前のビットが X だった時、 X or 1 = 1 となります。 したがって、パターン中の 1 は or により書き込むことが出来ます。
  2. 特定のビットを 0 にするには and を使います。前のビットが X だった時、 X and 0 = 0 となります。
  3. それでは特定のビットを Y(=0,1) にするにはどうすれば良いでしょうか? 0 と or を取れば Y になりますので、 X と 0 を and をとってから Y と or を取ればよいです。 つまり、 ( X and 0 ) or Y とすることで、 X を Y にすることが出来ます。

つまり、PORTB に pattern を書き込むには、書き込みたい部分を 0 にした マスクと and を取り、それから pattern と or を取れば良いで す。


;************
;*  演習6-1 *
;************
        #include "gs25.inc"
	org	0x0000
	goto	start
	org	0x0004
	goto	timer0
	org	0x0008
start
        enable_PAPB
        timer0_init b'11010111' ; 内部クロック、 PS=1:256
	postscaler_init d'7'-1
	counter_init
	sw_init

	sw_prg inccounter

	timer0_prg postscaler
	postscaler_prg flashdot

flashdot
	movlw	b'00000001'  ; 反転パターン
	xorwf	PORTB,1
	return

inccounter
	incf    counter,1
	movlw   pat_end-pat_start
	subwf   counter,0
	btfsc   STATUS,Z
        clrf    counter

dispcounter
        movf    counter,0
	call	getpat
	movwf	bout        ; 出力パターン仮置き
	movlw	b'00000001' ; マスク
	andwf	PORTB,0
	iorwf	bout,0
	movwf	PORTB
	return

	getpat_num_prg

	end

なお、この改造された dispcounter は従来のプログラムに対して互換性があ りますので、マクロを差し替えます。

演習6-2

例6-5のプログラムに、従来のプログラム中にある inccount, dispcount を 組み込むだけです。


;************
;*  演習6-2 *
;************
#include "gs25.inc"

	org	0x0000
	goto	start
	org	0x0004
	goto	timer0

	org	0x0008
start
        timer0_init b'11011000' ; 内部クロック、 PS 不使用
	enable_PAPB
	duty_init
	porta_init
	counter_init
	clrw
	call    getduty ; duty 0 を獲得
	movwf	duty_period
	movwf	duty_counter
	sw_init
main
        sw_prg  func
func
        call    inccounter
	call    set_rate_by_counter
	return 

	counter_prg endduty-startduty
	set_rate_by_counter_prg
	timer0_prg duty
	duty_prg porta_off, porta_on
	porta_prg
	getduty_prg
	getpat_num_prg

	end

坂本直志 <sakamoto@c.dendai.ac.jp>
東京電機大学工学部情報通信工学科