PIC-TECH
PICマイコンアセンブラテクニック
[Junk box]
2001/09/04更新
?
● このページについて ● ビット間のコピー ● 大小比較(符号無し8bit) ● ゼロチェック ● N回ループ ● 定数テーブルの参照 ● mpasm の 日本語DOS窓使用 ● 配列変数(間接アドレッシング)の実装 ● and/or/xor のよくある使い方 ● 変数の値によるプログラムの分岐 ● PICにおけるプログラムカウンタ(PC)の操作 ● 16ビットの加減算 ● 2のN乗の乗除算 ● 符号無し乗算(8ビットx8ビット=16ビット) ● 符号付き乗算(8ビットx8ビット=16ビット) ● 擬似乱数の発生

● このページについて ■先頭に戻る▲1つ戻る
このページは、マイクロチップ社の PICマイコンでよく使うテクニックなどをまとめたものです。 掲載しているコードはささおの許可なく使用してかまいませんし、 各項目への直リンクも自由におこなってかまいません(URLは変更しません)が、 無保証ってことでよろしくお願いします。 ご意見・バグ報告・ネタ提供などありましたら、メールTwitterでどうぞ。おまちしています。
対象PIC: ミッドレンジのPIC (PIC16F84, PIC16F876, PIC16F877, PIC16F628など)
アセンブラ: MPASM 02.30.11 (MPLABに同梱。マイクロチップ社のページから無償ダウンロードできます。)

● ビット間のコピー ■先頭に戻る▲1つ戻る
;
; _DATA1 の bit 3 を _DATA2 の bit 7 にコピー
;
	btfss	_DATA1,3
	bcf	_DATA2,7
	btfsc	_DATA1,3
	bsf	_DATA2,7
PICでは、RAMの総容量が数十〜400バイト程度しかないので、 RAM の1バイトは血の1滴です。true/false しか必要ないフラグ等は、 1バイトに8個ずつまとめてしまいましょう。
こんなときによく使うビット間のコピーは左のような感じで実装します。

● 大小比較(符号無し8bit) ■先頭に戻る▲1つ戻る
;[ if _DATA1 = _DATA2 then goto Xxx ]
	movf	_DATA2,W
	xorwf	_DATA1,W
	btfsc	STATUS,Z
	goto	Xxx

;[ if _DATA1 < _DATA2 then goto Xxx ] movf _DATA2,W subwf _DATA1,W btfss STATUS,C goto Xxx
;[ if _DATA1 <= _DATA2 then goto Xxx ] movf _DATA1,W subwf _DATA2,W btfsc STATUS,C goto Xxx
;[ if 'A' <= _DATA and _DATA <= 'Z' then goto Xxx else goto Yyy ] ; ≡ if _DATA < 'A' or 'Z' < _DATA then goto Yyy else goto Xxx movlw 'A' subwf _DATA,W btfss STATUS,C goto Yyy ; if _DATA < 'A' then goto Yyy movf _DATA,W sublw 'Z' btfss STATUS,C goto Yyy ; if 'Z' < _DATA then goto Yyy goto Xxx
PICは、〜の条件のときXxxへジャンプのような命令が存在しませんので、 既存の命令を組み合わせて作ります。通常このような場合は引き算命令のフラグ変化を 利用するのがセオリーですが、PICは引き算の方向が通常のCPUとは逆だったり、 条件判定には xxxフラグが立っていない場合には次の命令を実行しない (btfsc) などという二重否定な命令を使わなければならないので、 頭が痛くなってしまいます。 そこで、よく使うものを左にまとめてみました。

関連事項: ● ゼロチェック, ● and/or/xorのよくある使い方

● ゼロチェック ■先頭に戻る▲1つ戻る
;[ if _DATA = 0 then goto Xxx ]
	movf	_DATA,F
	btfsc	STATUS,Z
	goto	Xxx

; ; 24bit数のゼロチェック ; if _DATA = 0 then goto Xxx ; ; _DATA_H (bit 23..16) ; _DATA_M (bit 15.. 8) ; _DATA_L (bit 7.. 0) ; movf _DATA_L,W iorwf _DATA_M,W iorwf _DATA_H,W btfsc STATUS,Z goto Xxx
movf 命令は Zフラグを変化させます。 これを利用することで W レジスタを破壊することなく、 ある変数が0であるかをチェックできます。

16bit 以上の数のゼロチェックをする場合は、すべての桁の OR を取り 結果が 0であることを利用するのが早いです。

関連事項: ● N回ループ, ● and/or/xorのよくある使い方

● N回ループ ■先頭に戻る▲1つ戻る
;
; (例1) 100 回ループ(カウンタ減少型, 8ビットの場合)
;
	movlw	D'100'	; 0の場合256回ループ
	movwf	_COUNTER
Loop
;	ループさせる処理をここに書く

	; ループ終了処理
	decfsz	_COUNTER,F
	goto	Loop
Next


; ; (例2) 0x123456 回ループ(カウンタ増加型) ; clrf _COUNTER_H ;[23..16 bit用] clrf _COUNTER_M ;[15.. 8 bit用] clrf _COUNTER_L ;[ 7.. 0 bit用] Loop ; ループさせる処理をここに書く ; ループ終了処理 incf _COUNTER_L,F btfsc STATUS,Z incf _COUNTER_M,F btfsc STATUS,Z incf _COUNTER_H,F ; _COUNTER++ movf _COUNTER_L,W xorlw H'56' btfss STATUS,Z goto Loop movf _COUNTER_M,W xorlw H'34' btfss STATUS,Z goto Loop movf _COUNTER_H,W xorlw H'12' btfss STATUS,Z goto Loop Next
N回ループしたいという状況は頻繁に生じます。 decfsz命令を使用するとループのためのコードを節約することができます(例1)。 ただし、_COUNTERは 100, 99, .... , 1 と変化していくので、_COUNTERの 値をつかって何かしたい場合にはご注意。

さらにループ回数を増やしたい場合や、0,1,2,...というように カウンタを増やしていきたい場合、あんまりややこしいことを考えたくない場合など (つまり大抵の場合)では (例2) の方法がいいかと思います。
関連事項: ● and/or/xorのよくある使い方

● 定数テーブルの参照 ■先頭に戻る▲1つ戻る
;
; 定数テーブルの参照
; _DATA に 0〜7 を入れてこのルーチンを call すると、
; W に あらかじめ設定しておいた値が入ります
;
	org	0x100
GetTable
	movlw	H'01'
	movwf	PCLATH	; テーブルは 0x100〜0x1FF番地内に存在
	movf	_DATA,W
	andlw	B'00000111'	; _DATA の下位 3bitのみを参照
	addwf	PCL,F		; PCL ← (PCL + W) & 0xFF
				; PCH ← PCLATH
				; の2つの動作が実行されます
	; テーブル
	retlw	B'00000001'	; _DATA = 0 のとき
	retlw	B'00000011'	; _DATA = 1
	retlw	B'00000111'	; _DATA = 2
	retlw	B'00001111'	; _DATA = 3
	retlw	B'00011111'	; _DATA = 4
	retlw	B'00111111'	; _DATA = 5
	retlw	B'01111111'	; _DATA = 6
	retlw	B'11111111'	; _DATA = 7
定数テーブルを作るときは PC(プログラムカウンタ)を操作して、 retlw 命令と組み合わせて使うのが早いです。 ただし、PCを操作する上で、次の点に注意してください。

1.あらかじめ決めたテーブルの範囲を超えないようにすること
左の例では、どんな数値を入れても 0〜7 の範囲に収まるように 修正をしています。こういったことをしておかないと、予想外の値が 入力された場合に暴走しますよ。

2. retlw の配置されるアドレスを意識すること
すべての retlw が、あらかじめ PCLATH レジスタで指定した範囲に 配置されているかを必ずチェックしてください。 面倒な場合には、org などを使ってデータの配置されるアドレスを 固定してしまうのも手です。
関連事項: ● and/or/xorのよくある使い方 , ● PIC におけるプログラムカウンタ(PC)の操作

● mpasm の 日本語DOS窓使用 ■先頭に戻る▲1つ戻る
@echo off
c:\progra~1\mplab\mpasm /w1 /q %1.asm
type %1.err
日本語 DOS窓環境で mpasm (microchip純正アセンブラ)を使用するためには、 左の内容をテキストエディタで作成し、mpasm.bat というファイル名で保存後、 パスの通ったディレクトリに配置してください。

mpasm ファイル名 (拡張子は不要)

でアセンブルできます。

● 配列変数(間接アドレッシング)の実装 ■先頭に戻る▲1つ戻る
1. メモリの確保

_DATA	res	D'10'	; 10バイト分確保
_POS	res	1	; _DATAの先頭アドレスからの相対位置
_BUF	res	1	; データの読み書き用

2-a. 配列への書き込み (_DATA[_POS] = _BUF) movlw _DATA ; W ← _DATAの先頭アドレス addwf _POS,W ; _POS 分だけポインタを進める movwf FSR ; FSR にアドレスをセット movf _BUF,W ; (FSR) 番地に movwf INDF ; _BUF の値を書き込み
2-b. 配列からの読み出し (_BUF = _DATA[_POS]) movlw _DATA ; W ← _DATAの先頭アドレス addwf _POS,W ; _POS 分だけポインタを進める movwf FSR ; FSR にアドレスをセット movf INDF,W ; (FSR) 番地から movwf _BUF ; _BUF に値を取り出す
RAMの少ないPICでも、配列変数くらいは使いたいものです。 PICでは、

1. FSR レジスタに読み書きしたいRAMのアドレスを設定する。

    (PIC16F877などBank 2,3(100h以降)にもRAMが実装されている場合には、 STATUS, IRP (0:Bank 0,1, 1:Bank 2,3) も指定)
2. INDF レジスタに対して値を読み書きを行う

とすることで、FSRレジスタの指すアドレスに対してデータの読み書きが行えます。
配列変数は、これらのレジスタを使って左の例のように実装することができます。

● and/or/xor のよくある使い方 ■先頭に戻る▲1つ戻る
 and の よくある使い方

・ビットマスク

           11010101
      and) 00011110   1とした部分だけ
           00010100   ← 値が素通り。残りは0。

・2のべき乗 (2, 4, 8, ...)での割り算の余りの計算

	A ÷  8 の 余り ≡ A and  7(= B'00000111')
	A ÷ 64 の 余り ≡ A and 63(= B'00111111')
	などなど

or の よくある使い方 ・ビットセット 11010101 or) 11110000 1とした部分を 11110101 ←強制的に1 ・合成 10100000 ← あらかじめ and で必要な or) 00011001   部分だけ取り出しておいて 10111001 ← 合成
xor の よくある使い方 ・特定ビットの反転 11010101 xor) 11110000 1とした部分だけ 00100101 ←値を反転 ・イコールのチェック 11010101 xor) 11010101 ← 同じ値を xor すると 00000000 ← どのビットも 0
A and B = Y
ABY
000
010
100
111
A or B = Y
ABY
000
011
101
111
A xor B = Y
ABY
000
011
101
110

真理表(↑)だけ眺めていても、いまいちピンとこない論理演算。 よくある使い方をまとめてみました。

and
・一つのI/Oポートに入力と出力を設定しているときなど、8bitのうち特定の部分のみを 取り出したいときがあります。こういった場合には、取り出したい部分のビットを 1 にした値で and をとればOK。
・割り算の余り(BASIC では mod, C では %)を求めたい機会がしばしばあります。 一般的に、この計算には時間がかかりますし、 PICにはそれに該当する命令はありませんが、割る数が 2のべき乗である場合に限り 左の例のように (割る数-1) で and をとることで簡単に余りを求めることができます。 そのため、割られる数の範囲をわざと2のべき乗にしておく、といった手も よく用いられます。
関連事項: ● 定数テーブルの参照

or
or は 特定のビットのみを 1 にしたい時に用いられますが、あらかじめ and を とり必要な部分だけ取り出しておいた2数を合成する、 といったテクも多用されます。
関連事項: ● ゼロチェック

xor
xor は LEDの点滅など、特定のビットを反転(0→1 または 1→0)したい時に用いられます。また、PIC のように 2つのレジスタの値が同じであるかどうかを調べる命令が 無い場合には 2数の xor を取り、ゼロフラグ(STATUS,Z)の変化を調べる というのも常套手段です。
関連事項: ● 大小比較(8bit)

● 変数の値によるプログラムの分岐 ■先頭に戻る▲1つ戻る
;
; 変数の値によるプログラムの分岐
; _DATA に 0〜4 を入れてこのルーチンに goto すると、
; _DATA の値に応じたプログラムへジャンプします
; 範囲外の値の場合には Default にジャンプします
;
	org	0x100
OnGoto
	movf	_DATA,W
	sublw	D'4'
	btfss	STATUS,C
	goto	Default		; if 4 < _DATA goto Default

	movlw	H'01'
	movwf	PCLATH	; テーブルは 0x100〜0x1FF番地内に存在
	movf	_DATA,W
	addwf	PCL,F		; PCL ← (PCL + W) & 0xFF
				; PCH ← PCLATH
				; の2つの動作が実行されます
	; テーブル
	goto	Routine0	; _DATA = 0のとき
	goto	Routine1	; _DATA = 1のとき
	goto	Routine2	; _DATA = 2のとき
	goto	Routine3	; _DATA = 3のとき
	goto	Routine4	; _DATA = 4のとき
変数の値に応じてプログラムを分岐したい場合 (on xxx goto ..) には、 PC(プログラムカウンタ)を操作するのが簡単です。但し、 すべてのテーブルジャンプ先(goto Routine...)が必ず PCLATHで指定した範囲に収まっているかを .lstファイルで確認してください。 PC の操作については、下記の関連事項に目を通しておくことをお勧めします。 PCの動作を理解していないと「いままで普通に動いてたのにプログラムが 大きくなってきたらときどき変な動作をするようになった」 というワナに陥りがちです。
関連事項: ● PIC におけるプログラムカウンタ(PC)の操作

● PIC におけるプログラムカウンタ(PC)の操作 ■先頭に戻る▲1つ戻る
;
; 0x800 の壁を考慮したプログラム
;
	org	0x100
Sub1
	movf	PCLATH,W
	movwf	_PCLATH_BUF	; PCLATHを退避

	movlw	H'08'
	movwf	PCLATH	; call を実行すると(現在のPC+1)が
			; 保存された後
	call	Sub2	; PC<12:11>←PCLATH<4:3>
			; PC<10: 0>←Sub2の下位11ビット
			; がセットされます

	movf	_PCLATH_BUF,W
	movwf	PCLATH		; PCLATHを復帰

	return

	org	0x800
Sub2
	:
	:
	return
プログラムの先読みや同時に複数の命令を実行するような 最近の超高速なCPUでは、プログラムが現在どの部分を実行しているかを つかさどるプログラムカウンタ(PC)をユーザが直接書き換える ということはご法度ですが、 PICでは PC を直接操作することを前提にした命令セットになっています。 PICでは、13 bit のメモリ空間 ( 0x0000 〜 0x1FFF ) がありますが、 扱いはやや複雑です。

PC, PCH, PCL, PCLATH
PC の上位 5bit、下位8bit をそれぞれ PCH, PCLとよびます。 このうち、PCH はユーザが直接操作することができませんテーブル参照テーブルジャンプで PCを操作する場合には、PCLATHレジスタにジャンプ先アドレスの上位 5ビットを あらかじめセットした後、PCLを書き換えてください。PCLの書き換えと同時に、 PCLATH の値が PCH にロードされ、目的のアドレスにジャンプします。 ここで、add命令で PCLに加算を行った場合、PCHへの繰り上がりが考慮されないので、ジャンプ先が 0x100 の境界をまたがないようプログラムの配置を工夫してください。

0x0800 の壁
PIC での ジャンプ命令 (goto) やサブルーチンコール命令 (call) は、 11 bit ( 0x000 〜 0x7FF ) のアドレス空間を指します。それ以上の範囲へ goto, call したい場合には、左の例のようにあらかじめ PCLATH レジスタに ジャンプ先の上位5bitのアドレス(実際に参照されるのは上位2bitのみ)を 指定しておく必要があります。 PCLATHレジスタはPC によって自動的に書き換えられることはないため、 0x800の壁を越える goto/call を行う場合には、PCLATH を保存しておいたほうが無難です。

● 2のN乗の乗除算 ■先頭に戻る▲1つ戻る
;  _A(8bit) を 4=2*2 倍する

	bcf	STATUS,C
	rlf	_A,F	; _A = _A*2
	bcf	STATUS,C
	rlf	_A,F	; _A = _A*2

; _A(8bit) を 1/4=(1/2)*(1/2) 倍する bcf STATUS,C rrf _A,F ; _A = _A/2 bcf STATUS,C rrf _A,F ; _A = _A/2
; _A(16bit; _AH, _AL) を 4=2*2 倍する bcf STATUS,C rlf _AL,F ; 下位から処理 rlf _AH,F ; _A = _A*2 bcf STATUS,C rlf _AL,F rlf _AH,F ; _A = _A*2
; _A(16bit; _AH, _AL) を 1/4=(1/2)*(1/2) 倍する bcf STATUS,C rrf _AH,F ; 上位から処理 rrf _AL,F ; _A = _A/2 bcf STATUS,C rrf _AH,F rrf _AL,F ; _A = _A/2
十進数で10倍,100倍,... あるいは、1/10倍, 1/100倍の計算が簡単なように、 二進数では 2倍, 4倍, 8倍,... あるいは 1/2倍, 1/4倍,... の計算が簡単にできます。

十進数で桁を一桁上げることは、二進数では1ビット分左にシフト ( STATUS,C をリセットして左回転 )、 十進数で一桁下げることは、二進数では1ビット分右にシフト ( STATUS,C をリセットして右回転 ) にそれぞれ対応します。

  2倍                         1/2倍
    (二進数)         右シフト
    11010011  (= 211)   →    01101001.1 (=105.5)
       ↓ 左シフト                     ↓
   110100110  (= 422)                小数点部は
  ↓                                 STATUS,Cに
  8bitからあふれた分は               反映される
  STATUS,C に反映される
16bit以上の数値を扱いたい場合には、乗算の場合は下位から、除算の場合は上位の桁から計算していきます。十進数の乗除算と同じですね。

● 符号無し乗算(8ビットx8ビット=16ビット) ■先頭に戻る▲1つ戻る
;
;  RAMの割り当て
;
_H	res	1
_L	res	1
_A	res	1
_B	res	1

; _HL = _A x _B (符号無し乗算) ; * _B の値は破壊されます Mul88 clrf _H movf _B,W movwf _L movlw D'8' movwf _B Mul88loop movf _A,W bcf STATUS,C btfsc _L,0 addwf _H,F rrf _H,F rrf _L,F decfsz _B,F goto Mul88loop return
PICには乗算命令がありません(AVRにはあります)。 よって、PICでは乗算は極力使わないようにするか、 2のN乗の乗算を組み合わせる (たとえば10倍したければ、8倍と2倍を求めて和をとる) 等の工夫をして高速化します。 でも、それでもどうしても一般的な乗算がやりたい場合は 左のようなプログラムで計算できます。 ちなみに計算終了まで 80命令程度の時間が必要です。
関連事項: ● 符号付き乗算(8ビットx8ビット=16ビット)

● 符号付き乗算(8ビットx8ビット=16ビット) ■先頭に戻る▲1つ戻る
;
;  RAMの割り当て
;
_H	res	1
_L	res	1
_A	res	1
_B	res	1

; _HL = _A x _B (符号付き乗算) ; * _B の値は破壊されます Muls88 clrf _H movf _B,W movwf _L movlw D'8' movwf _B bcf STATUS,C Muls88loop movf _A,W btfsc STATUS,C goto Muls88loop2 btfsc _L,0 subwf _H,F goto Muls88loop3 Muls88loop2 btfss _L,0 addwf _H,F Muls88loop3 rlf _H,W ; 符号付の rrf _H,F ; 右シフト rrf _L,F decfsz _B,F goto Muls88loop movlw H'80' ; _A = -128の xorwf _A,W ; 場合の例外 btfss STATUS,Z ; 処理 return comf _H,F ; 正負の反転 comf _L,F incf _L,F btfsc STATUS,Z incf _H,F return
PICには乗算命令がないどころか符号付き演算用命令がありませんので、 符号付きの数同士の演算はBASICやC言語においてもやらないほうがいいです。 符号無しの計算でうまく処理できないかどうか考えましょう。 でも、どうしても必要というのなら左のような方法で符号付の2数の 乗算をすることができます。左の例では Boothのアルゴリズムを用いています。 ちなみに計算終了まで 120命令程度の時間が必要です。
関連事項: ● 符号無し乗算(8ビットx8ビット=16ビット)

● 16ビットの加減算 ■先頭に戻る▲1つ戻る
;
;  RAMの割り当て
;
_AH	res	1	; _AH,_AL と _BH, _BL 
_AL	res	1	; をそれぞれ 16 bit レジスタ
_BH	res	1	; _A, _B とみなして計算
_BL	res	1

; ; _A = _A + _B ; movf _BL,W ; 下位 8bitの計算 addwf _AL,F ; _AL = _AL + _BL movf _BH,W ; 上位 8bitの計算 btfsc STATUS,C incf _BH,W ; _AH = _AH + _BH (+ 1) addwf _AH,F ; * (+1)は繰り上がり時のみ
; ; _A = _A - _B ; movf _BL,W ; 下位 8bitの計算 subwf _AL,F ; _AL = _AL - _BL movf _BH,W ; 上位 8bitの計算 btfss STATUS,C incf _BH,W ; _AH = _AH - {_BH (+1)} subwf _AH,F ; * (+1)は桁借り時のみ
8ビットの加減算命令しかないPICで16ビットの加減算をする時には キャリ/ボロービット (STATUS,C) を利用します(赤字部分)。
このビットは、計算結果が 8bit に収まらないような繰り上がり、 桁借りの演算で次のように変化します。
addwf reg,F の演算の例

(1) 繰り上がり無し    (2) あり
      H'23'  ← W        H'F0'  ← W
   +) H'56'  ← reg   +) H'25'  ← reg
      H'79'  ← reg      H'15'  ← reg
      STATUS,C = 0       STATUS,C = 1

subwf reg,F の演算の例

(1) 桁借り無し        (2) あり
      H'83'  ← reg      H'11'  ← reg
   -) H'56'  ← W     -) H'30'  ← W
      H'2D'  ← reg      H'E1'  ← reg
      STATUS,C = 1       STATUS,C = 0
結果の格納先が F ではなく W の時や、addlw, sublw 命令の場合にもSTATUS,C は同様の変化をします。

● 擬似乱数の発生 ■先頭に戻る▲1つ戻る
;
; 擬似乱数の発生
;
; (1) RAMの割り当て
_RNDH	res	1	; _RND:乱数用
_RNDL	res	1	;     (16bit)
_BH	res	1	; _B:中間処理用
_BL	res	1	;     (16bit)

; ; (2) 乱数の種をセット (_RND = 0x0005) ; *値によっては周期が短くなります RandInit movlw H'00' movwf _RNDH movlw H'05' movwf _RNDL
;---------------------------------------- ; (3) 呼び出すたびに乱数(_RND)を更新する ;---------------------------------------- ; _RND を3倍して 0x79 を加え、 ; 上位8bitと下位8bitを入れ替える ;---------------------------------------- Rand movf _RNDL,W movwf _BL movf _RNDH,W movwf _BH ; _B = _RND bcf STATUS,C rlf _RNDL,F rlf _RNDH,F ; _RND = _RND * 2 movf _RNDL,W addwf _BL,F movf _RNDH,W btfsc STATUS,C incf _RNDH,W addwf _BH,F ; _B = _RND + _B movlw H'79' addwf _BL,F btfsc STATUS,C incf _BH,F ; _B = _B + 0x79 movf _BL,W ; _Bの上位8bitと movwf _RNDH ; 下位8bitを movf _BH,W ; 入れ替えて movwf _RNDL ; _RND に入れる return
上の実行結果 (_RNDLのはじめの 64回) 00 98 6D C4 42 50 BE 3D 1A 90 57 7C 7B C8 C0 75 2C 88 F9 36 2D 52 01 4D 75 21 88 96 35 B2 4A AE 07 89 AC 3E 78 9A A5 D7 3A FB 78 3F A4 A4 31 30 25 1B B8 60 E4 CE 71 AA 66 66 02 01 7D 75 D1 8A
'本当の'乱数を作るのは大変ですが、ほぼランダム、というのなら 元の数をxxx倍してxxを加えてごにょごにょという感じで作ることができます。 左の例では、61124回の周期をもち、_RNDLは 0〜255の範囲をほぼ偏りなくばらつきます。
ただし、この場合では電源を入れるたびに毎回同じパターンが出現しますので。 それがイヤな場合には、_RNDLの値に対して、I/OピンやRTCの値と適当なタイミングで XOR をとったりするといいでしょう。
関連事項: ● 16ビットの加減算, ● 2のN乗の乗除算

[Junk box]

管理人 ささお