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セットごとにスイッチを監視して、一時中断が可能
装置はこんな感じ。
下の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倍ももつという記録が出た。
書込み回数の記録をグラフにするとこんな感じ。
記録がいい加減(「標準の倍を超えた」とか)なわりには意外と綺麗なグラフになっている。温度で書込み時間に違いが出たりするかと期待していたが、少なくともこのレベルの記録には残らないようだ。
エラーの内容だが、8Cが読めているので、ACを書き込んで8Cが読めた、つまり5bit目が異常になったエラーのようだ。これは不思議なことで、前述のとおり常に0を書いている下位2bitが真っ先にやられるものと予想していた。
それはそうとここから先は未知の領域なのでとりあえず試験を続ける。
まず意外だったのが、一度8Cが読めた箇所を20回ほど読んでみても正しくACが読めることだ。どうやらEEPROMのエラーというものは、一度エラーが出たらそれっきりというものではなく、「書いた値が稀に正常に読めない」という症状を示すようだ。
その後再度同じプログラムで走らせても、しばらくの間は正常にベリファイが走る。やはりエラーは確率的なもののようだ。
これを考えると、テストのやり方は正しくなかったと言える。1千万回で初めてエラーが出たとして、それ以前から確率的に読み取れない状態になっていたのだろうから、ある程度の回数書き込むたびに読み出しが正しくできるかのテストを数百回なり数千回なり読み出してみて調べるべきだろう。まあこれは今後の研究課題ということで。
さてその後も同じプログラムで何度も走らせてみた。結果がこれだ。
プログラムを走らせ、エラーが発生して停止するまでに書込みが行われた回数を毎回記録している(最後除く)。
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回ごとにエラーが起こる理由が分からない。内部構造に原因があるのだろうか。今後の研究課題だ。研究課題多いなあ。
最後にコード。
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セットごとにスイッチを監視して、一時中断が可能
装置はこんな感じ。
下の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倍ももつという記録が出た。
書込み回数の記録をグラフにするとこんな感じ。
記録がいい加減(「標準の倍を超えた」とか)なわりには意外と綺麗なグラフになっている。温度で書込み時間に違いが出たりするかと期待していたが、少なくともこのレベルの記録には残らないようだ。
エラーの内容だが、8Cが読めているので、ACを書き込んで8Cが読めた、つまり5bit目が異常になったエラーのようだ。これは不思議なことで、前述のとおり常に0を書いている下位2bitが真っ先にやられるものと予想していた。
それはそうとここから先は未知の領域なのでとりあえず試験を続ける。
まず意外だったのが、一度8Cが読めた箇所を20回ほど読んでみても正しくACが読めることだ。どうやらEEPROMのエラーというものは、一度エラーが出たらそれっきりというものではなく、「書いた値が稀に正常に読めない」という症状を示すようだ。
その後再度同じプログラムで走らせても、しばらくの間は正常にベリファイが走る。やはりエラーは確率的なもののようだ。
これを考えると、テストのやり方は正しくなかったと言える。1千万回で初めてエラーが出たとして、それ以前から確率的に読み取れない状態になっていたのだろうから、ある程度の回数書き込むたびに読み出しが正しくできるかのテストを数百回なり数千回なり読み出してみて調べるべきだろう。まあこれは今後の研究課題ということで。
さてその後も同じプログラムで何度も走らせてみた。結果がこれだ。
プログラムを走らせ、エラーが発生して停止するまでに書込みが行われた回数を毎回記録している(最後除く)。
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
2014年11月02日 04:03
先日の「最弱のPICマイコンでカレンダー」だが、ちょっと思いついてコードを短縮してみた。いい感じにキモくなったと思う。
変更箇所は2点。
【シリアル通信】
元のコードではRS232Cシリアルの送受信コードをそれぞれ書いていたが、これを1つにまとめることにより短縮した。
コア部分がこちら。1つのループの中で1bitの送信と受信を同時に行うようになっている。
・送信時
バッファに送信データを入れておき、スタートビットを送信してからこのループで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を足す形になった。
それでできた値がこれだ。
さらに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ワード縮む。
全コードは以下。
変更箇所は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