たまりば

パソコン・インターネット パソコン・インターネット三鷹市 三鷹市

EEPROM書換え上限テスト
2014年11月17日 08:06

PICマイコンに内蔵されていたりするEEPROMには書換え回数に限界がある。
PIC16F628A内蔵のものでは、書換え耐性は最小10万回、標準100万回となっている。
これをテストしてみよう。

なおこれは既にやっている方がいる。
参考: 自己満足系 「PIC耐久試験」

同じことをやっても芸が無いので、書込みデータを変えてみた。
EEPROMの書き込み手順はまず0xFFにクリアしたのち、必要なbitのみ0にするという手順を踏むそうだ。
であれば「0」のみを書き込み続けた場合と「0」「1」を交互に書き込んだ場合では「0」のみの方が早く限界に達するはずだ。
これはMicrochip社のアプリケーションノート「AN537」を見てもそう書いてある。(figure4)
これを確かめるため、書込みデータは「0x5C」と「0xAC」の交互にした。上位(7bit目~4bit目)が0/1交互、3,2bit目が1固定、下位(1,0bit目)が0固定だ。
これが意外な結果になるのだが…。

プログラムは最後に載せるが、内容を説明すると以下のとおり。
・電源ONでまずEEPROMの内容を全て出力し、その後待機状態に入る
・スイッチを押すとLEDを点灯させて開始を示し、1秒後に消灯して処理を開始
・書込み後に読み出してベリファイし、値が異なっていればエラー表示をして終了
・0x5Cと0xACの2回の書き込みを1セットとし、256セットごとにEEPROMに書込み回数を保存
・256セットごとにスイッチを監視して、一時中断が可能

装置はこんな感じ。
EEPROM破壊装置
下の3つは置き場が無くて挿してあるPICで、回路に無関係である。上のコンデンサについては後述する。
橙の線はスイッチ代わり。LEDは開始・終了のインジケーター。もう1個エラー表示もつけようと思ってプログラムもそう書いたのだが、よく考えてみれば終了LEDだけでことが済んだのでつけなかった。
余談だが、LEDが青なのはこの試験中に青色LED開発者がノーベル賞を取ったのでなんとなく元は赤だったのを変えてみたためだ。

試験を開始する。
電源は仕様上3V以上必要なのだが、いつもEneloop2本で動かしているので今回もそれでやってしまった。どうせ厳密な試験をするつもりもなし、動いてるので問題ないだろう。
まず少々処理が進んだところで一時中断し、書き込み速度をチェックする。
どうやら1回あたり5msほど掛かっている。時間からして書き込みは行われているようだ。
(プログラムの製作中は書き込み操作をコメントアウトしていたが、この時はマイクロ秒単位でループが回っていた)
ここから破壊予定を計算してみよう。
100000*5/1000/60=8.33
1000000*5/1000/60/60=1.39
より、最小の10万回ならわずか8分強、標準の100万回でも1時間半ももたない計算になる。
上記の先人の使った16F84は標準1000万回のところ2300万回で破壊に至ったということなので、同じ割合なら3時間ほど。
何日も点けっぱなしにするのは面倒なのでこれは楽でいい。
と思ったのだが…。

試験はTwitterにつぶやきながら行っていたので、ログを見ながら進行状況を書いていく。
10/05 16:55 試験開始
10/05 17:13 開始20分で2*0x1A400回 (※書き込み2回ごとに1カウントしている)
10/05 18:43 標準値を突破、1.3M回超
10/05 20:04 標準寿命の倍を超えたがまだ動く
10/05 21:12 3.1M回を超える
10/05 22:15 2*1D6600回
10/05 23:56 5M回
10/06 01:51 6.5M回
 標準寿命の2,3倍程度だろうと思っていたため、寝る前に終わる予定でいたので困る。
10/06 06:04 結局点けっぱなしで朝、9.5M回
 標準寿命の10倍近くでまだ正常と主張しているのでそろそろプログラムのミスが疑われ始める。
 とりあえず中断し、電池を充電する。
(10/06 21:38 再開時刻記録忘れ。遅くともこの時刻)
10/07 00:22 11,661,312回
 本格的にプログラムのミスが心配になる。
10/07 01:44 ついに終了

以上、延べ17.25時間掛けて、12,663,170回目でエラー発生。なんと標準値の12倍ももつという記録が出た。
書込み回数の記録をグラフにするとこんな感じ。
EEPROM書込み回数グラフ
記録がいい加減(「標準の倍を超えた」とか)なわりには意外と綺麗なグラフになっている。温度で書込み時間に違いが出たりするかと期待していたが、少なくともこのレベルの記録には残らないようだ。

エラーの内容だが、8Cが読めているので、ACを書き込んで8Cが読めた、つまり5bit目が異常になったエラーのようだ。これは不思議なことで、前述のとおり常に0を書いている下位2bitが真っ先にやられるものと予想していた。
それはそうとここから先は未知の領域なのでとりあえず試験を続ける。
まず意外だったのが、一度8Cが読めた箇所を20回ほど読んでみても正しくACが読めることだ。どうやらEEPROMのエラーというものは、一度エラーが出たらそれっきりというものではなく、「書いた値が稀に正常に読めない」という症状を示すようだ。
その後再度同じプログラムで走らせても、しばらくの間は正常にベリファイが走る。やはりエラーは確率的なもののようだ。
これを考えると、テストのやり方は正しくなかったと言える。1千万回で初めてエラーが出たとして、それ以前から確率的に読み取れない状態になっていたのだろうから、ある程度の回数書き込むたびに読み出しが正しくできるかのテストを数百回なり数千回なり読み出してみて調べるべきだろう。まあこれは今後の研究課題ということで。

さてその後も同じプログラムで何度も走らせてみた。結果がこれだ。
EEPROM書込みエラー記録
プログラムを走らせ、エラーが発生して停止するまでに書込みが行われた回数を毎回記録している(最後除く)。
3色の◆の部分が最初と同じプログラムで走らせたものである。
傾向として最初の1266万回から回を追うごとに減っていき、15回目あたりからエラーまでの書き込み回数がほぼ一定になっているのが見て取れる。これは512回である。
最初に書いたとおり、このプログラムでは2回×256セットごと、すなわち512回ごとにEEPROMに書込み回数を保存している。つまりこの書込み直後の読み込みが失敗しやすいようである。
この原因として書込みによって電圧が下がっている可能性を考え、コンデンサを付けてみた(赤部分)。するとしばらくの間書き込みエラーが起こりにくくなったような気もするが、またコンデンサを外しても14万回とかなりの回数が出たのでよく分からない。
もう少し確かめてみたかったが、そうこうしている間に別のエラーが発生した。
部分が、読めた値が2Cであった部分である。AC→2Cになったものであろうから、7bit目が異常になったようだ。またも上位bitからエラーが発生した。このとき累計書込み回数1663万回。
その後数回2Cが続き、再び8Cでのエラーが発生。
ここで、プログラムを変更する。異常になった2つのbitをマスクして、他のbitのみを見るようにした。これが紫部分。
変更後最初のエラーまでに64万回、累計1765万回。1Cが読めたので、5C→1Cで6bit目が異常。
その後2回ほど試したのち、またマスクを変更。3つのbitをマスクする。これが水色部分。(なぜか凡例だけマークが「*」になっているが、Excelが異常で凡例のマークをどうやっても変更できなかった)
変更後最初のエラーまでに209万回、累計1974万回。4Cが読めたので、5C→4Cで4bit目が異常。
これで上位4bitが全て異常になり、下位4bitには異常が見られないという状況になった。
不可解だがとりあえず続けよう。マスクを変更。上位4bitをマスクするようにした。
すると、いつまで走らせても一向にエラーが起こらない。累計1億回、すなわち標準寿命の100倍に達してなおエラーが出なかったので、あきらめて試験を終了することにした。この時点が橙である。

以上まとめると、0/1を交互に書き込んだ場合は標準寿命の10~20倍で最初のエラーが発生し、0のみまたは1のみを書き込んだ場合は標準寿命の100倍を超えてもエラーが発生しないという結果になった。
資料から予想した結果と違っており不可解だが、この結果から考えられることとして、PICのEEPROMは前回と同じ値を書き込むときは消去・書込み動作を行わないのだろうか。
EEPROMはバイトごとに消去が行われるものと思っているのだが、bitごとの消去ができるものもあるのだろうか。この辺も今後の研究課題である。

また、エラー発生時の累計回数の生データを見ると1つ不思議なことが分かる。以下がそのデータだ。
609CC0, 6545D4, 675110, 67C8C0, 6A3690, 6A3700, 6AB598, 6AB600, 6AB710, 6AB800, 6AC59C, 6B08C0, 6B0D58, 6B14D0, 6B1500, 6B1600, 6B1700, 6B1800, 6B1900, 6B1A00, 6B1B00, 6B1C00, 6B1D00, 6B1E00, 6B1F84, 6B2048, 6B2100, 6B2200, 6B2304, 6B2400, 6B2500, 6B2600, 6B2700, 6B3380, 6CE7D8, 6CE800, 6D0904, 6D2590, 6D2654, 6D2710, 6D2800, 6D4184, 6D4200, 6EB540, 702D10, 702E10, 702F00, 703000, 745114, 7A5C14, 7A5D10, 7A5E04, 7D1404, 7D1504, 7E2B0C, 7E2C00, 7E2D00, 7EE104, 7EE200, 7EE30C, 7EED00, 81C300, 86A514, 86A600, 86A70C, 969F00
1の位に0,4,8,Cと4の倍数しか現れていない。00になるのはEEPROM書込み直後なので分かるとして、4の倍数、すなわち8回ごとにエラーが起こる理由が分からない。内部構造に原因があるのだろうか。今後の研究課題だ。研究課題多いなあ。

最後にコード。
    list p=16f628a
    #include p16f628a.inc
    radix dec
    __CONFIG _INTOSC_OSC_NOCLKOUT & _WDT_OFF & _PWRTE_ON & _CP_OFF & _LVP_OFF & _MCLRE_ON & _BODEN_OFF

    radix dec


;     RA2 +-v-+ RA1
;     RA3 |   | RA0
;     RA4 |   | RA7
; Vpp RA5 |   | RA6
;     GND |   | Vdd
;     RB0 |   | RB7 PGD
;     RB1 |   | RB6 PGC
;     RB2 |   | RB5
;     RB3 +---+ RB4

; EEPROM制御がしやすいよう、変数はbank1にとる
    cblock 0xA0
        targetaddr
        dataaddr
        cnt0,cnt1,cnt2
        cnt232
        dat,datbuf
        waitcnt0,waitcnt1
    endc
; 結局使わなかった割り込み退避用変数
    cblock 0x70
        wbuf
        sbuf
    endc


#define TESTPTR 0x00
#define MASK 0xFF ;結果をマスクするときここを変える

;pin
#define TXPIN232 0
#define BUTTON 1
#define ERRLED 2
#define STOPLED 3

    org 0
    goto init

    org 4
    retfie

init:

    bsf STATUS,RP0 ;BANK1
    movlw b'00000011'
    ;       ^/pull up enable
    ;        ^int edge
    ;         ^tmr0 clock source 0:internal
    ;          ^tmr0 source edge
    ;           ^prescaler assignment 0:tmr
    ;            ^^^prescaler rate
    movwf OPTION_REG
    movlw 1<<BUTTON
; 間違ってPORTBの値を設定する前にTRISBを設定しているのでたまにシリアル出力の先頭が化けることに後で気づいたが直し忘れた
    movwf TRISB
    bcf STATUS,RP0
    movlw 0x07
    movwf CMCON
; EEPROMの制御のためコードはすべて全てbank1で走らせる
; そのため変数はすべてbank1にとった
; bank0のレジスタでPORTBだけ必要なのでINDFをPORTBにしておく
    banksel 0x80 ;bank1
    movlw PORTB
    movwf FSR ;INDF=PORTB
    clrf INDF
    clrf cnt232
; EEPROMに保存してある書込み回数カウントを取得
    movlw (~TESTPTR)&0x7F
    movwf targetaddr
    movlw TESTPTR*4
    movwf dataaddr
    
    movf dataaddr,W
    movwf EEADR
    bsf EECON1,RD
    movf EEDATA,W
    movwf cnt2
    incf dataaddr,W
    movwf EEADR
    bsf EECON1,RD
    movf EEDATA,W
    movwf cnt1
    movf dataaddr,W
    addlw .2
    movwf EEADR
    bsf EECON1,RD
    movf EEDATA,W
    movwf cnt0
; 書込み回数カウントを…と思ったが面倒なのでEEPROM全体をシリアルに出力
    movlw '/'
    call tx232
showeep:
    clrf EEADR
showeeploop:
    bsf EECON1,RD
    movf EEDATA,W
    call tx232hex
    incf EEADR,F
    btfss EEADR,7
    goto showeeploop
; スタート待ち
    btfsc INDF,BUTTON
    goto $-1
; LEDを1秒点灯させスタート表示 (停止表示用だった名残が名前に見える)
    bsf INDF,STOPLED
    
    call wait1s
    
    bcf INDF,STOPLED

main:
    movf targetaddr,W
    movwf EEADR

mainloop:
; データAを書込み、読み取り、比較
    movlw 0x5C ;データA
    call writeeep
    clrf EEDATA
    bsf EECON1,RD
    movlw 0x5C
    xorwf EEDATA,W
    andlw MASK
    btfss STATUS,Z
    goto err

    movlw 0xAC ;データB
    call writeeep
    clrf EEDATA
    bsf EECON1,RD
    movlw 0xAC
    xorwf EEDATA,W
    andlw MASK
    btfss STATUS,Z
    goto err

    incfsz cnt0
    goto f
; 256カウントごとの操作
; 上位カウント
    incf cnt1,F
    btfsc STATUS,Z
    incf cnt2,F
    bcf STATUS,C

; カウント値を保存
    movf dataaddr,W
    movwf EEADR
    movf cnt2,W
    call writeeep
    
    incf dataaddr,W
    movwf EEADR
    movf cnt1,W
    call writeeep
    
    movlw 0x02
    addwf dataaddr,W
    movwf EEADR
    movf cnt0,W
    call writeeep

; ボタンを確認し、押されていれば中断
    btfss INDF,BUTTON
    goto stop

f:
    goto mainloop

; 書込みエラーが発生したら、カウント下位および読み取った値を書き込んで、LEDを点灯させ停止
err:
    bsf dataaddr,1
    incf dataaddr,W
    movwf EEADR
    movf EEDATA,W
    call writeeep
    movf dataaddr,W
    movwf EEADR
    movf cnt0,W
    call writeeep
    
    bsf INDF,ERRLED

stop:
    bsf INDF,STOPLED
    goto $

; EEPROM書込みサブルーチン
; WをEEADRで示すアドレスに書き込む
writeeep:
    movwf EEDATA
    bsf EECON1,WREN
    movlw 0x55
    movwf EECON2
    movlw 0xAA
    movwf EECON2
    bsf EECON1,WR
    btfsc EECON1,WR
    goto $-1
    return

; Wを16進表記でシリアル出力 (IOピン直結)
tx232hex:
    movwf datbuf
    swapf datbuf,W
    call sendnibble
    movf datbuf,W
    call sendnibble
    return

sendnibble:
    andlw 0x0F
    movwf dat
    movlw .6
    addwf dat,F
    movlw '0'-.6
    btfsc STATUS,DC
    movlw 'A'-.10-.6
    addwf dat,W
; Wをシリアル出力
tx232:
    movwf dat
    bsf INDF,TXPIN232
    bsf cnt232,3
tx232loop:
    rrf dat,F
    movf INDF,W
    andlw ~(1<<TXPIN232)
    btfss STATUS,C
    iorlw 1<<TXPIN232
    movwf INDF
    decfsz cnt232,F
    goto tx232loop
    goto $+1
    goto $+1
    nop
    bcf INDF,TXPIN232
    
    retlw 0

; 1秒くらい待つ
wait1s:
    clrf waitcnt0
    clrf waitcnt1
waitloop0:
waitloop1:
    goto $+1
    goto $+1
    goto $+1
    goto $+1
    goto $+1
    goto $+1
    decfsz waitcnt1
    goto waitloop1
    decfsz waitcnt0
    goto waitloop0
    
    return

; 初回のみここのコメントを外してビルドしEEPROMのデータ保存領域を0で初期化する
;    org 0x2100
;    dt 0,0,0,0,0,0
    
    end
  

  • 最弱のPICマイコンでカレンダー_ちょっと短縮
    2014年11月02日 04:03

    先日の「最弱のPICマイコンでカレンダー」だが、ちょっと思いついてコードを短縮してみた。いい感じにキモくなったと思う。

    変更箇所は2点。
    【シリアル通信】
    元のコードではRS232Cシリアルの送受信コードをそれぞれ書いていたが、これを1つにまとめることにより短縮した。
    コア部分がこちら。1つのループの中で1bitの送信と受信を同時に行うようになっている。
    loop232:
        movlw 0xF8
        addwf GPIO,W  ;0xF8を足すことでGPIO[3]をCに入れる
        movlw 1<<TXPIN
        rrf dat232,F  ;ローテートでCをバッファに、バッファの反対端をCに
        btfsc STATUS,C
        xorwf GPIO    ;送信データに応じてGPIOの送信ピンをトグル
        decfsz cnt232
        goto loop232
    具体的な挙動は次のようになる。
    ・送信時
    バッファに送信データを入れておき、スタートビットを送信してからこのループで1bitづつ送信する。
    (なお時間の都合でXORでトグルしているので、事前にバッファに入れる値を調整しておく必要がある。)
    同時に受信操作も行われるが、特にスタートビットに合わせているわけではないので、無意味な値が取れる。
    ・受信時
    バッファに全0を入れておき、スタートビットの受信を待ってからこのループで1bitづつ受信する。
    同時に送信操作も行われるが、スタートビットも出さずデータが全0なので何も起こらない。

    これで2つが1つになったので半分くらいに縮んでほしかったのだが、前処理が増えたせいで計30ワード→25ワードとわずかな短縮に留まった。まあ縮んだだけよしとしよう。

    【月データ】
    元のコードでは月ごとに「当該月の日数」と「基準の年の当該月1日の曜日」の2つのテーブルを持っていた。
    しかし月の日数は28~31、曜日は0~6しか値を取らないので、それぞれ1バイトとるのは無駄が多い。
    そこで2つのデータを1バイトにまとめてみたのだが、最初は単純に上下4bitづつに分け、月の下位4bitと曜日を表そうとしていた。
    しかし途中でもっといい方法を思いついた。
    ・下位bitで月を表す。
    ・残りのビットを適に設定して、全体でmod7で曜日を表す数にする。
    というものだ。なおmodの計算の都合上全体の数は132を超えられないので、最上位bitは実質0固定。そして曜日に3bit使うので、月には4bitしか残らず、あとで0x10を足す形になった。
    それでできた値がこれだ。
    getmonthattr:
        addwf PCL,F
        dt .95, .28, .63, .94, .47, .78,
        dt .31,.111, .30, .95,.126, .79,
        dt .31,.125

    さらにmodの計算ルーチンを変えて1命令短縮した。
    元は「7を引き続け、0を下回ったらループを抜け、答えを0~6にするため7を足して完了」というものだったが、最後の7を足す部分が無駄である。
    そこで「7を足し続け、オーバーフローしたら完了」に変えた。このため256%7=4だけ値を補正しておく必要がある。

    これにより、テーブルが1つ減って15ワード減、読み取り部分はなんだかんだで同命令数、mod7の1命令減があって計16ワードとかなりの短縮になった。


    この2点の変更で188ワード→167ワードとなった。
    PICのコードをバイトで数えることはあまりしないのだが、計算してみると167*1.5=250.5バイト。おお、256バイトを切っている。

    なお、感覚としてはもう数命令は縮められそうな気はするが、縮めたところで別に他に入れる機能もなし、面白いことは無いかなと思うので今のところやる気はしない。
    とりあえず「Y?>」「M?>」のメッセージを無くすだけで13ワード縮む。

    全コードは以下。
        list p=10F200
        #include p10f200.inc
        radix dec

        __CONFIG _MCLRE_OFF & _CP_OFF & _WDT_OFF

        cblock 0x10
            dat232
            yearh, yearl, month
            cnt, dowcnt, daycnt, cnt232
            day
            temp
        endc

    ;ピン
    #define TXPIN 0
    ;RXPINは3固定

        org 0
    init:
        movwf OSCCAL ;クロック補正値セット
        movlw b'11001000'
        ;設定データ。プルアップOFF、他Don'tCare
        ;ちょうど下位3bitがDon'tCareなのでI/O設定と共用
        clrf GPIO ;出力ピンを全Lowに
        tris GPIO ;I/Oモード設定
        option ;設定

    main:
        clrf cnt
        clrf cnt232

    ;"Y?>"
        movlw 'Y'
        call tx232
        movlw '?'
        call tx232
        movlw '>'
        call tx232
       
    ;年4桁受信: abcd
    ;年上位 = 10a+b ≡ 2a+b
        call rx232
        movf dat232,W
        movwf yearh ;*1
        addwf yearh,F ;*2
        call rx232
        movf dat232,W
        addwf yearh,F
    ;年下位 = 10c+d
        call rx232
        movf dat232,W
        movwf yearl ;*1
        bcf STATUS,C
        rlf yearl,F ;*2
        rlf yearl,F ;*4
        addwf yearl,F ;*5
        rlf yearl,F ;*10
        call rx232
        movf dat232,W
        addwf yearl,F

    ;"M?>"
        call newline
        movlw 'M'
        call tx232
        movlw '?'
        call tx232
        movlw '>'
        call tx232
       
    ;月2桁受信: ef
        call rx232
        movf dat232,W
        andlw 0x01 ;1っぽければ10をセット
        btfss STATUS,Z
        movlw .10
        movwf month
        call rx232
        movf dat232,W
        andlw 0x0F
        addwf month,F ;C0

    ;閏年判定
        movf yearl,W ;年下位を取り、
        btfsc STATUS,Z ;0なら
        movf yearh,W ;年上位を取り、
        andlw 0x03 ;下位2bitが
        btfss STATUS,Z ;0でなければ
        goto notleap ;平年。

    ;閏年1,2月を13,14月扱い
        movlw .3
        subwf month,W
        movlw .12
        btfss STATUS,C
        addwf month,F
    notleap:

    ;年ごとの曜日の基準を算出
    ;年下位 + 年下位/4 + (年上位%4)*2
        rrf yearl,W
        movwf temp
        rrf temp,W
        andlw 0x3F
        addwf yearl,F
        rlf yearh,W
        andlw 0x06
        subwf yearl,F
    #define doworg yearl ;名前変更: DoW origin

    ;当月の曜日の差分に相当する数を基準に足す
    ;および、当月日数を取得
        decf month,W
        call getmonthattr
        addwf doworg,F
        andlw 0x0F
        iorlw 0x10
        movwf daycnt

    ;mod7を求めるが、簡略化のため、
    ;7を加算していきキャリーが出た時点で終了する。
    ;256%7=4だけずれるのでデータ側で補正済
        movlw .7
    mod7loop:
        addwf doworg,F
        btfss STATUS,C
        goto mod7loop

    ;ループ用にNと(6-N)を生成、コードの都合上+1
        movlw .8
        movwf dowcnt
        incf doworg,W
        subwf dowcnt
        movwf cnt
       
        call newline

    ;月初めの日まで空白で埋める
        goto dayadjstart
    dayadjloop:
        movlw ' '
        call tx232
        movlw ' '
        call tx232
        movlw ' '
        call tx232
    dayadjstart:
        decfsz cnt,F
        goto dayadjloop

        clrf day
    dayoutloop:
    ;表示用日(BCD)を++
        movlw .7
        addwf day,F
        movlw .6
        btfss STATUS,DC
        subwf day,F
       
    ;1日分の表示
        movlw ' '
        call tx232
        swapf day,W
        andlw 0x0F
        btfsc STATUS,Z
        movlw ' '
        btfss STATUS,Z
        iorlw 0x30
        call tx232
        movf day,W
        andlw 0x0F
        iorlw 0x30
        call tx232

        decf daycnt,F
        btfsc STATUS,Z
        goto break ;1月分出力完了で抜ける

        decfsz dowcnt,F
        goto dayoutloop ;日ループ

        movlw .7
        movwf dowcnt
        call newline
        goto dayoutloop ;週ループ
    break:
        call newline

        goto main

    ;### subroutine ###

    ;改行を出力
    newline:
        movlw '\r'
        call tx232
        movlw '\n'
        call tx232
        retlw 0

    ;下位4bitが「当該月の日数」であり、
    ;全体が「基準の年の当該月1日の曜日+4」とmod7で等しいような数を返す
    getmonthattr:
        addwf PCL,F
        dt .95, .28, .63, .94, .47, .78,
        dt .31,.111, .30, .95,.126, .79,
        dt .31,.125

    ;RS232Cで1バイト受信し、下位ニブルのみ返す
    rx232:
        btfss GPIO,3 ;rx start [0,3)
        goto $-1
        clrf dat232
        goto $+1
        goto f
    ;RS232Cで1バイト送信する
    tx232:
        movwf dat232
        bcf STATUS,C
        rlf dat232,W
        xorwf dat232,F
        bsf GPIO,TXPIN ;tx start 0
    f:
        bsf cnt232,3
    loop232:
        movlw 0xF8
        addwf GPIO,W ;read [9,12)+9n
        movlw 1<     rrf dat232,F
        btfsc STATUS,C
        xorwf GPIO ;write 7+9n
        decfsz cnt232
        goto loop232

        movlw 0x0F
        andwf dat232,F
        xorwf dat232,F
        goto $+1
        bcf GPIO,TXPIN ;stop
        retlw 0

        end