電子工作

PIC-TIPS

PICに関するよしなし事。纏めている途中なので、検証は後から。


代替プログラム

PICはご存知のように縮退した命令系であるがゆえ、必要な命令がありません。そこで、「あらまほしき」命令を代替してみます。

  1. 符号無しバイト比較

    ただ減算すればよいのですが、その後の判定が問題になります。とりあえず、レジスタ間の比較を例に取ると…

    	MOVF	reg2,W
    	SUBWF	reg1,W

    面倒なのは、その後の条件の判定です。フラグの変化は、次のようになります。

    resultZC
    reg1>reg201
    reg1=reg211
    reg1<reg200

    これから、条件分岐を作ってみます。

    1. BEQ (reg1=reg2)
      	MOVF	reg2,W
      	SUBWF	reg1,W
      	BTFSC	STATUS,Z
      	GOTO	label
    2. BNE (reg1!=reg2)
      	MOVF	reg2,W
      	SUBWF	reg1,W
      	BTFSS	STATUS,Z
      	GOTO	label
    3. BHI (reg1>reg2)
      	MOVF	reg2,W
      	SUBWF	reg1,W
      	BTFSC	STATUS,Z
      	BCF	STATUS,C
      	BTFSC	STATUS,C
      	GOTO	label

      これは、条件を逆にすると簡単になります。(reg2<reg1)

      	MOVF	reg1,W
      	SUBWF	reg2,W
      	BTFSS	STATUS,C
      	GOTO	label
    4. BLO (reg1<reg2)
      	MOVF	reg2,W
      	SUBWF	reg1,W
      	BTFSS	STATUS,C
      	GOTO	label
    5. BHE (reg1>=reg2)
      	MOVF	reg2,W
      	SUBWF	reg1,W
      	BTFSC	STATUS,C
      	GOTO	label
    6. BLE (reg1<=reg2)
      	MOVF	reg2,W
      	SUBWF	reg1,W
      	BTFSS	STATUS,Z
      	BTFSS	STATUS,C
      	GOTO	label

      これは、条件を逆にすると簡単になります(reg2>=reg1)。

      	MOVF	reg1,W
      	SUBWF	reg2,W
      	BTFSC	STATUS,C
      	GOTO	label
  2. レジスタの正負ゼロ判定分岐

    単純ですが、単独のレジスタの値を判定するものです。

    1. BZ(reg==0)
      	MOVF	reg,W
      	BTFSC	STATUS,Z
      	GOTO	label
    2. BNZ(reg!=0)
      	MOVF	reg,W
      	BTFSS	STATUS,Z
      	GOTO	label
    3. BP(reg>0)
      	MOVF	reg,W
      	BTFSC	reg,7
      	BSF	STATUS,Z
      	BTFSZ	STATUS,Z
      	GOTO	label
    4. BPZ(reg>=0)
      	BTFSS	reg,7
      	GOTO	label
    5. BM(reg<0)
      	BTFSC	reg,7
      	GOTO	label
    6. BMZ(reg<=0)
      	MOVF	reg,W
      	BTFSS	reg,7
      	BTFSS	STATUS,Z
      	GOTO	label
  3. 符号付きバイト比較

    PICには符号付バイトのロジックが用意されていません。STATUSレジスタにオーバーフロー・ビットがあればよかったのですが…

    符号付演算そのものは符号無し演算で行えるのですが、大小判断が面倒で、プログラムも長くなり気味。現実的な選択肢は、符号なし数に変換してから比較するというものです。

    符号なし数への変換は、単順で、比較の対象の双方に0x80を加えるだけ(128の下駄を履かせるわけです)。後は符号無しバイトの比較を行えばOK。

    符号付演算について、ちょっと、書いておきます。符号付減算の結果は以下のようになります。

    reg1符号reg2符号結果の範囲
    +,0+,0-127〜127結果は表現範囲内
    +,0-1〜255常に正のはずだが、表現範囲を超える。
    -+,0-255〜-1常に負のはずだが、表現範囲を超える。
    ---127〜127結果は表現範囲内

    結局のところ、reg1、reg2が同符号の場合は、そのまま結果が使えます。

    異符号の場合は実は最初から大小が決まっています(reg1の符号と一致する)。ですから演算の前に異符号の場合だけ取り除くのも一つの方法です。

    異符号の場合は、結果がバイト範囲を超えるので、オーバーフローが生じます(結果の符合がreg1の符号と異なる)。ここの(他のCPUなら行ってくれる)検出を行うと、プログラムが長くなるわけです。

    そこで、オーバーフローが生じないように、2バイト長に符号拡張してから演算し、2バイト減算の結果を正負判断するのも一つの方法です(オーバーフローは生じない)。しかしながら、これもそこそこプログラムは長くなります(2バイト符号無し減算があれば、それを利用)。

  4. バイトシフト

    もともと用意されているのは、左右の1ビットローテーション(Cを含む9ビット長)。簡単ですが、通常のシフト処理を書いてみます。

    1. LSR 論理右シフト

      msbには0が入ります。Cフラグにはlsbが入ります。

      	BCF	STATUS,C
      	RRF	reg,F
    2. ASR 算術右シフト

      msbには元のmsbが入ります。Cフラグにはlsbが入ります。

      	RLF	reg,W
      	RRF	reg,F
    3. LSL/ASL 左シフト

      lsbには0が入ります。Cフラグにはmsbが入ります。

      	BCF	STATUS,C
      	RLF	reg,F
    4. ROL 左回転(8ビット)

      lsbには元のmsbが入ります。Cフラグにはmsbが入ります。

      	RLF	reg,W
      	RLF	reg,F
    5. ROR 右回転(8ビット)

      msbには元のlsbが入ります。Cフラグにはlmsbが入ります。

      	RRF	reg,W
      	RRF	reg,F
  5. キャリー付きバイト加算

    ADC reg1,reg2 とでも言いましょうか。当然、存在すべきキャリー付き加算です。とてもエレガントなプログラムです。

    	MOVF	reg2,W
    	BTFSC	STATUS,C
    	INCFSZ	reg2,W
    	ADDWF	reg1,F

    内容は、reg1 = reg1 + reg2。キャリーの分を3行目で加算しているのですが、ここでINCFSZを使っているのが秀逸。インクリメントして結果がゼロになれば、加算の必要が無いので次の加算は実行しない。演算上は桁上げが出ているのでキャリーがセットして欲しい場合なので、確認すると、INCFSZはフラグを変更しないので、現状でオンされているCフラグは保持。ただし、Zフラグは信用できない。

    この処理での結果のキャリーフラグは信用ができるので、多桁加算などで使用できます。

  6. キャリー付きバイト減算

    SBC reg1,reg2 相当の、上のバリエーション。これも、当然、存在すべきキャリー付き減算です。

    	MOVF	reg2,W
    	BTFSS	STATUS,C
    	INCFSZ	reg2,W
    	SUBWF	reg1,F

    内容は、reg1 = reg1 - reg2。PICではボロー(桁借)は、Cフラグが0になります。ボローが無い場合にはスキップしてSUBWFを実行。ボローがある場合は、引く数を一つ多くします。このとき、引く数が0になる場合は、減算を実行しない。しかも、Cフラグは保存される。結果的に、Cフラグは保持。ただし、Zフラグは信用できない。

    この処理もキャリー(ボロー)フラグは信用できるので、多桁減算などに使用できます。

  7. レジスタとWの入れ替え

    任意のレジスタとWレジスタを交換するもの。MPLABのアセンブラのヘルプに書かれているものです。マクロなのでSWAPWF myregのように命令が拡張されたように使えます。ちょっと判りにくいのですが、作業域を用意して入れ替えるよりも高速です。

    SWAPWF  MACRO  REG
            XORWF  REG,F
            XORWF  REG,W
            XORWF  REG,F
            ENDM
    
    命令Wの内容regの内容
    最初AB
    XORWF REG,FAA xor B
    XORWF REG,WA xor (A xor B)
    →B
    A xor B
    XORWF REG,FB(A xor B) xor B
    →A
    結果BA

    レジスタの内容は左の図のように変化します。排他的論理和(XOR)では、同じものを作用させると元に戻ることを利用したものです。

    ABA XOR B(A XOR B) XOR B
    TTFT
    TFTT
    FTTF
    FFFF

    論理値表は左のとおりです。「A」の値と「B」の値が同じものになっています。

    XORは便利な命令で、ビット毎の反転にも使用できます。「A」に「B」を作用させると考えると、「B」がTの場合は、結果は「A」の反転、Fの場合は「A」の値そのままです。反転させたいビットを1にしてXORをかけてやれば、そのビットだけが反転します。次の例はregのビット3を反転する例です。

    	MOVLW	B'00001000'
    	XORWF	reg,F
    

    本来のXORの機能は、「値が違っていればT」ですから、ビットの比較ができます。不要のビットは演算後マスクします。次の例は、regのビット1〜3が「101」と等しいか確かめる例です。等しい場合はWレジスタが0になります(Zフラグがセット)。

    	MOVLW	B'00001010'
    	XORWF	reg,W
    	ANDLW	B'00001110'
    
  8. せこい乗算

    符号無しのバイト乗算です。通常なら、次のとおり。

    乗数、被乗数をsrc1、src2に格納。結果はresultH/resultLに。src2は壊れます。

    umul88
    	clrf    resultH
    	clrf    resultL
    	movlw   d'8'
    	movwf   loopcount
    	movf    src1,w
    umul88_loop:
    	rrf     src2,f
    	btfsc   STATUS,C
    	addwf   resultH,f
    	rrf     resultH,f
    	rrf     resultL,f
    	decfsz  loopcount,f
    	goto    umul88_loop
    	return
    

    addwf resulttH,f で出るキャリーが次の rrf の頭に入るところがうまくできています。また、addwfを実行しない場合には、キャリーは0なので、rrfの頭には0が入ります。

    次が、せこい計算。乗算の結果、上位の1バイトしか使わない場合の例です。ループをマクロ展開して速度を稼いでいます。全部で34命令。20MHzのPICならば、6.8μSで実行します。

    		clrf	result
    		movf	src1,w
    x=8
    	while x > 0
    		rrf	src2,f
    		btfsc	STATUS,C
    		addwf	result,f
    		rrf	result,f
    x=x-1
    	endw
    
  9. パリティ計算

    バイトのパリティ計算のプログラムです。srcに値を入れ、結果は、srcのbit0に入ります。

    	swapf	src,w
    	xorwf	src,f	;valid b3-b0
    	rrf	src,w
    	xorwf	src,f	;valid b0=b0^b1 b2=b2^b3
    	btfsc	src,2
    	incf	src,f	;valid b0
    

    まじめに考えれば、パリティの計算は、値が1のビットの数を数え、カウントのlsbを取るのですが、ここでは、XOR演算を利用しています。

    ABA XOR B
    000
    011
    101
    110

    左がXORの表です。A、Bが対象のビットとすれば、値が1のビットが奇数ある場合には、1。偶数の場合には0となることがわかります。1を奇数、0を偶数と読み替えれば、奇数と偶数→奇数。奇数と奇数→偶数。偶数と偶数→偶数となる演算機能です。

    ABCDの四つが対象ならば、 (A XOR B) XOR (C XOR D) とすれば、パリティが求まります。もちろん、 ((A XOR B) XOR C) XOR D としても、同様です。

    プログラムの、 swapf src,w / xorwf src,f は、8ビットを演算して4ビットにする処理。swapfで上下4ビットを入れ替えて、自分と演算した結果、b3=b7 xor b3、b2=b6 xor b2、b1=b5 xor b1、b0=b4 xor b0 の四つの演算を一度に行っています。

    次の rrf src,w / xorwf src,f は、1ビットずらして、自分自身と絵算。b2=b3 xor b2、b0=b1 xor b0。これで、二つにまで畳んだことになります。

    最後の、 btfsc src,2 / incf src,f は、b0=b2 xor b0の置き換え。b2=0なら、b0は変化しない。b2=1なら、b0はビット反転。incf は +1 なので、b0だけに注目すれば、1→0、0→1となるわけです。


間接ジャンプ

PICでは、間接ジャンプが命令でサポートされていませんが、レジスター操作で代替できます。ジャンプ先のアドレスを特定レジスタに書き込むというワイルドな仕様です。プログラム用のアドレス空間は、仮想的には16ビット長なので、最初にアドレスの上位バイトをPCLATHに書いておき、次にPCLにアドレスの下位バイトを書き込むという仕掛けです。

単純な例を見てみましょう。

	MOVLW	high label
	MOVWF	PCLATH
	MOVLW	low label
	MOVWF	PCL

飛び先のアドレスが、レジスタに格納されている場合、当然、上位、下位が生き別れで格納されるのですが、次のようになります。ここで上位がregh、下位がreghlにあるものとします。

	MOVF	regh,W
	MOVWF	PCLATH
	MOVF	regl,W
	MOVWF	PCL

これが基本形になるわけです。

次にテーブル・ジャンプを考えます。与えられた値が0のとき、処理A、1のとき、処理Bといった具合で分岐させる処理です。テーブルにはGOTO命令を並べておきます。テーブルの該当位置へジャンプし、そこから、本来の処理へ分岐するという遠回りな方法を取ります。

	MOVLW	high vec_table
	MOVWF	PCLATH
	MOVF	reg_ix,W
	ADDLW	low vec_table
	BTFSC	STATUS,C
	INCF	PCLATH,F
	MOVWF	PCL
vec_table:
	GOTO	proc_a
	GOTO	proc_b
	GOTO	proc_c
	GOTO	proc_d
	GOTO	proc_e
	.....

reg_ixの値に応じてジャンプします。下位のアドレスの加算のときの桁上がりの処理をしています。テーブルの位置そのサイズに応じて、2の冪の都合の良い位置に置くと、プログラムが単純になります。このとき、ORG擬似命令を使用して、位置を制御します。まず、256の倍数の位置におく例です。このとき、テーブル先頭のアドレスの下位は必ず0になります。

	MOVLW	high vec_table
	MOVWF	PCLATH
	MOVF	reg_ix,W
	MOVWF	PCL
	ORG		($-1)/.256*.256+.256
vec_table:
	GOTO	proc_a
	GOTO	proc_b
	GOTO	proc_c
	GOTO	proc_d
	GOTO	proc_e
	.....

非常にすっきりします。ORG ($-1)/.256*.256+.256がおまじないで、256の倍数にテーブルの位置を置く擬似命令です。プログラムは単純になったのですが、コードの隙間が最大で255ワードできてしまいます。

これをちょっと変更すると、コードの隙間が少ないものができます。位置合わせの数字として、テーブルサイズが超えない2の冪の数字(2,4,8,16,32,64,128)を使用します。例えばテーブルサイズが7ならば8を使用します。

	MOVLW	high vec_table
	MOVWF	PCLATH
	MOVF	reg_ix,W
	ADDLW	low vec_table
	MOVWF	PCL
	ORG		($-1)/.8*.8+.8
vec_table:
	GOTO	proc_a
	GOTO	proc_b
	GOTO	proc_c
	GOTO	proc_d
	GOTO	proc_e
	.....

この場合、コードの隙間は最大で7ワードです。ORG擬似命令の数字を2の冪に応じて変更すれば、必要なサイズの処理に置き換えることができます。

プログラムメモリ内のテーブルから値を取り出す場合などにも、この手法が必要になります。PICではデータを置く場所は、RAM(レジスタ)、EEPROM、プログラムメモリが考えられます。

  1. RAM

    容量が少なく、電源を切ると保存されないので初期化が必要。また、残念ながらアドレスが飛んでいるので、扱いが面倒です。しかし、可変データをおくことができます。当然書き換えも高速。

  2. EEPROM

    容量は少ないですが、電源を切っても保存されます。アドレスは連続しているので扱いやすい。書き換えはできますが、書き換え動作は遅いです。読み込みは速いので意外に使えます。

  3. プログラムメモリ

    容量はプログラムメモリの制限によります。。当然、電源を切っても保存されますが、書き換えはできません。プログラム中に埋め込むので、取り扱いやすい。

一般的には、テーブルをプログラムメモリに格納することが多いのですが、PICはプログラムメモリを直接読むことができません。そこで、ここで紹介したような手法で間接ジャンプをし、戻り値を得るという手法を使います。

1バイトをWレジスタに戻す場合には、ジャンプテーブルに戻り値付きのRETURN value命令を書き込んでおきます。この命令を使ったテーブルはDT擬似命令を使って、自動的に生成することもできます。ただ、1バイトに対して1命令=1ワードですから効率はよくありません。

gettbl:
	MOVLW	high table
	MOVWF	PCLATH
	MOVF	reg_ix,W
	ADDLW	low table
	MOVWF	PCL
	ORG		($-1)/.8*.8+.8
table:
	RETURN	val_a
	RETURN	val_b
	RETURN	val_c
	RETURN	val_d
	RETURN	val_e
	.....

こうするとCALL gettblとすればreg_ixの値に応じて、val_aval_bなどがWレジスタにセットされます。

DT擬似命令を使用すると、見た目もすっきりします。

gettbl:
	MOVLW	high table
	MOVWF	PCLATH
	MOVF	reg_ix,W
	ADDLW	low table
	MOVWF	PCL
	ORG		($-1)/.8*.8+.8
table:
	DT		val_a,val_b,val_c
	DT		val_d,val_e
	.....

テーブル引きのサンプルで、PCLATHにビット操作をする例がありますが、PICに対する操作としては等価です。しかし、直接アドレスでPCLATHにセットする方が、間違いも少なく、柔軟だと思うので、いつも、ここで紹介した方法を使用しています。

テーブルの要素が1バイトではない場合や、テーブルのサイズが大きい(256を超える)場合には、アドレス計算が多少厄介になるので、事前に計算してレジスタに格納しておいてから。間接ジャンプした方がわかりやすそうです。また、アドレス計算で乗算を使用するのは、なかなかつらいので、無駄であっても、要素のサイズは2の冪に伸ばしておけば、シフト演算を利用して乗算を回避できます。


割り込み

PICは最も初期の頃のものを除くと、割り込みがサポートされています。この割り込みがかなりワイルドで、ほとんど何もしないのです。これは、RISCと言っているチップ全般に言えるのですが、ステータスやコンテクストの保存もしません。

まず、割り込みがかかるとソフトウェア的には、次のような動作をします。

  1. INTCONGIEビットをクリアする。(割り込み禁止)
  2. スタックへアドレスを保存。おそらく、次の命令のアドレス。
  3. 固定番地(0x04)へジャンプする。

そして、割り込み終了時には、RETFIE命令で復帰しますが、このときは、次のとおりです。

  1. INTCONGIEビットをセットする。(割り込み許可)
  2. スタックからアドレスを取り出す。
  3. 取り出したアドレスへジャンプ。

要約すれば、CPUは各命令毎に割り込み要求をスキャンし、割り込みがあれば、GIEビットをクリアしてCALL 0x04を行う。復帰のRETFIE命令は、GIEセット付きのRETURN命令。

これだけしか実行されないので、割り込み内で使用して内容を破壊しては困るものは、自力で保存、回復をしなければなりません。最低限WレジスタとSTATUSレジスタは保存します。また、保存領域も自分で確保しておく必要があります。

irq:
;
; 	入り口での定型処理
;
	MOVWF	irq_wback		;save wk
	SWAPF	STATUS,W		;save status
	MOVWF	irq_sback
;
;	必要ならばレジスタバンクを切り替える
	BCF		STATUS,RP0
	BCF		STATUS,RP1
	BCF		STATUS,IRP
;
;	必要ならばコンテクストの退避
;
;	割り込み処理をここに記述
	CALL	xxxx
;
;	必要ならばコンテクストの復帰
;
;
;	出口での定型処理
;
	SWAPF	irq_sback,W		;load status
	MOVWF	STATUS
	SWAPF	irq_wback,F		;load wk
	SWAPF	irq_wback,W
;
	RETFIE

順を追って説明します。

  1. まず、入り口での定型処理。
    1. MOVWF irq_wback

      何をするにもWレジスタを使うので保存。

    2. SWAPF STATUS,W

      STATUSを保存するためにSTATUSWレジスタに読み込むのですが、STATUSを変更せずに読み込める命令はこれしかありません。この命令は上下ニブル、つまり、上位4ビットと下位4ビットを入れ替える命令です。

    3. MOVWF irq_sback

      これを保存。上下がひっくり返ったままですが、復帰するときに元に戻します。

    ここまでの処理では、レジスタバンクの切り替えができないので、保存用のレジスタは、どのバンクからでも見えるレジスタを使用します。

  2. 次に、割り込みがかかったとき、どのレジスタバンクが指定されているか、わからないので、必要ならば、レジスタバンクの切り替えを行います。
  3. 必要な場合はコンテクストの退避を行います。ようするに、割り込み外の処理とバッティングするレジスターを決めた保存領域に格納する処理です。FSRPCLATCHなど、割り込み処理で使っていれば、忘れないで保存します。
  4. やっと、割り込み処理の本体。

    本体では、要因解析を行って、それぞれの処理。注意するのは、必ず、割り込み要求をクリアすることです。これは各要因毎に異なります。

  5. 必要ならばコンテクストを復帰。
  6. 出口の定型処理
    1. SWAPF irq_sback,W

      STATUSレジスタを復帰するのですが、MOVFではSTATUSレジスタが変わってしまいます。保存と同様に、仕方なく、SWAPFを使用します。保存するときに上下をひっくり返していたので、ここで上下が元に戻るのでちょうどいい具合。

    2. MOVWF STATUS

      で、保存。この命令はSTATUSレジスタが変わらないので安心。

    3. SWAPF irq_wback,F

      Wレジスタを復帰したいのですが、例によってMOVFが使えません。SWAPFで読み込むために、あらかじめ、ここで上下ニブルを反転しておきます。

    4. SWAPF irq_wback,W

      上下ニブルを反転しておいたデータをWに復帰。反転したものを、また、反転するので元に戻るという面倒なものです。

    K/li>
  7. 最後にRETFIEで割り込みから復帰。

STATUSレジスタを壊さないためにSWAPF命令を使わなければいけないのが痛いところです。


割り込み その2

実際にプログラムを書いていくと、多重割り込みをサポートしていないことがつらいです。

例えば、MIDI入力を処理しながら、タイマー割り込みなどを処理する場合です。タイマー割りj込みを100μ秒単位でかけながら、MIDI入力を処理するとMIDI入力を処理している間はタイマーの割り込みを処理できません。

この場合、通常ならば、多重割り込みをして、タイマー割り込みの優先度を高くしておけば何の問題が無いのですが、PICでは標準的に多重割り込みを処理する機能が無いので面倒になるわけです。結局根本的な解決策は無いのですが…

  1. 無理矢理、多重割り込みをサポートする。

    MIDI割り込み内で、割り込みを許可して、タイマー割り込みを受け付けるようにするのですが、レジスター退避その他も多重割り込みできるように擬似スタックにしなければなりません。

    また、ハードウェアスタックの段数そのものが少ないので、この辺の考慮も必要になります。

  2. (この例ならば)MIDI処理はポーリングで処理。

    とってもダサイ気がしますが…

  3. MIDI処理の入力部分だけ割り込み内で処理し(つまり、リングバッファでも設けて)、論理部分は地面のプログラムで処理する。

    これが一番、現実的な処理と思われます。最悪のケースでの遅れを意識しなければいけないのは変わらないのですが…

  4. 割り込みは一番速いものだけにして、割り込み処理内で他の要因をポーリングする。

    この例ならば、タイマー割り込みだけをかけ、タイマーの割り込みがかかったら、タイマーの処理をし、さらに、MIDIの入力があるか、ポーリングして処理します。これも現実的だけど、ダサイですね。上の方法より優れているのは、一番処理に対する影響が少ないことぐらいです。


割り込み その3

その2と似ている問題ですが、割り込みの優先順位の問題です。

PICには割り込みの優先順位機能がありません。

そこで、割り込みの優先順位を処理するためには、割り込み要因フラグの検査を優先順位の順番に行う必要があります。他方、もう少し凝ったCPUでは、割り込み優先順位を設定する機能があるのが普通です。

しかし、割り込み優先機能の有無は、結局のところ、要因解析の速度の問題であって、処理の本質が変わるわけではありません

たとえ、ハードウェアで割り込み優先度を設定していても、優先度の低い割り込みの処理の最中に、優先度の高い割り込み要因が発生しても、直ちに応答するわけではなく、通常は、割り込みが終了してから、優先度の高い割り込み処理が実行されます。これは、PICのように、ソフトウェアで優先度を解析するのと、事情は全く変わりません。

優先度の高い割り込みを、機械的に実行するには、多重割り込みの処理が必要になります。逆に言えば、単に、優先度設定をしただけでは、優先度の高い割り込みの実行を保証するものでなく、それぞれの割り込みの処理時間を注意深く、検討して最も短い割り込みにも応答できるように、それぞれが短時間で割り込み処理を終了するようにしなければなりません。

現実的な問題

PICで現実的な問題、例えば、CCPの割り込みを高速でかけながら、シリアル割り込み処理をする場合を考えてみます。

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

  1. CCPの割り込み要因を検査し、必要ならばCCPの処理を呼び出す。CCPの要因はクリアし、割り込みから復帰する。
  2. シリアルの割り込み要因を検査し、必要ならば、シリアルの処理を呼び出す。シリアルの要因をクリアする。…

こうすると、複合して割り込みがかかった場合でも、最初にCCPの割り込みが処理されます。処理としてはハードウェアの優先処理と同じことになります。

残念ながら、PICは割り込み処理のプロローグとエピローグが比較的長くなるので、これを節約するならば、

  1. CCPの割り込み要因を検査し、必要ならばCCPの処理を呼び出す。CCPの要因はクリアする。
  2. シリアルの割り込み要因を検査し、必要ならば、シリアルの処理を呼び出す。シリアルの要因をクリアする。

上のように、一つの割り込み呼び出し内で二つを連続して処理した方が現実的でしょう。CCPの処理中にシリアルの割り込みがかかっても、フォローできます。

さらに、シリアルの処理が比較的長くて、CCPの割り込みが間に合わない場合には、1のCCPの処理をシリアルの処理の中にばら撒いておく手があります。多重割り込み的な動作を無理矢理させるわけです。


出力ポートにご用心

PICのほとんどの端子は単純な出力用に設定できます。これが曲者で使用する命令に制限があったりします。いわゆるRead Modify Write問題です。

	movlw	b'00001111'
	movwf	PORTB
		……
	bsf	PORTB,7

全ビットを出力にセットしたとします。最初に PORTB を b'00001111'にセットします。次にしばらくしてから、PORTB を b'10001111'にセットしたいのが上のプログラムの趣旨なのですが…

bsf命令は、PORTBを読み出してから、b'10000000'と OR をとり、再び、PORTBに書き込みます。ここで最初の PORTB 読み出しのとき、バスの信号を読み出します。出力を b'00001111'にしていても、バスの負荷の状態によっては、b'00001111'が返ってきません。そこで、演算後、再びPORTBに書き戻すときに、誤った値を書いてしまう可能性があります。

要するに出力ポートに書いたビットは正しく読めないことがあるということ。このため、出力ポートに書き込み値は、別の作業領域に写しを取って、それを利用して演算する必要があります。出力ポートの各ビットを単独で書き換える場合には、必ず、行うべき処理です。そして、不幸なことに、各ビットはほとんどの場合単独で扱われます。便利な bsf命令、bcf命令は残念ながら出力ビット設定に使えません。

ということで、出力として書き込む場合には、movwf 命令のほかの命令(演算命令など)は使えないことになります。

また、出力ポートの読み出しが信用できないのですから、出力ビットをビットテストして分岐などは出来ません(入力ビットは大丈夫)。

これによって、割り込みを考えると、ちょっと面倒な問題が出てきます。割り込みの内部と地面の処理の双方でポート操作をする場合に問題があるのです。

	bsf	PORTB_BACK,7
	movf	PORTB_BACK,w
	movwf	PORTB

上のように、PORTB_BACK に PORTB の値を保持して書き込みを行うようにしたとき、この途中で割り込みがかかると、その時点で、PORTB_BACK と PORTB の値がずれた状態で割り込みが実行されます。この割り込みの中では、正しくPORTBに書き込めないことになります。

そこで、この間の割り込みを禁止しなければなりません。もし、bsf や bcf が正しく動作するならば、割り込み禁止は不要になるのですから、このデメリットは大きいです。

別のCPU、例えば、8051では、頻繁に使うビット操作命令に対しては Read Modify Writeに配慮がなされています。つまり、ビット操作命令のときの Read は出力ポートそのものを読み出すように工夫がされています。

また、AVRでは、同一ピンの出力ポートと入力ポートが別のアドレスに振られており、出力ポートを読み出した時に入力ではなく出力が読まれるので、Read Modify Write問題が起きません。


奇妙なプログラム空間

(14ビットアーキテクチャの)PICのプログラム空間の広さは、最大でも8kW。0x0000 - 0x1fffとなっていて、ちょっと見、リニア配列。全空間をアドレスするためには、13ビットの情報が必要になります。

ところで、PICのインストラクションの語長は、1ワードで14ビット。この中にオペレーションコード(命令の種類を指定)とオペランドが入るわけです。ここで問題になるインストラクションはCALLGOTO。この二つの命令のオペレーションコード部はビット。オペランドは飛び先を示しますが、11ビットしかありません。11ビットで指定できる範囲は0-7ffの2kW。これではすべてのアドレスに分岐できません。

そこで、GOTOCALL命令は、飛び先を決めるときに、PCLATHのb4とb3をアドレスのb12、b11として使用します。右の図がアドレスを合成する様子を表しています。2kWまでしかプログラムが存在しないPICではこの問題は意識をする必要がありませんが、それよりもプログラム領域が大きなPICでは(2kWより大きなプログラムを作る場合は)、CALLとGOTO命令を実行する前にPCLATHのb4b3を設定しなければなりません。

PCLとPCLATHの組合せで分岐をする間接ジャンプでは、右の図のように単純に上位バイト下位バイトを合成するだけです。しかし、PCLATHに書き込んだとき、当然ながらPCLATHのb4b3は変わる可能性があります。例えば、プログラムの大きなアドレスにある処理へ間接ジャンプした場合は、PCLATHのb4b3を適当に処理しないと、CALL、GOTOを使用したときに思わぬところへジャンプしてしまいます。

ここにも増築を繰り返したアーキテクチャの障害が出ているわけです。

メニューへ戻る
アルゴ算法堂へ戻る