2018年03月25日 03:01
ファミコンやゲームボーイなど昔のゲームソフトにはデータの保持にボタン電池が使われている。
(最初に知った時には驚いた。壊れなければ永遠に使えると思っていたゲームソフトにまさか寿命のある電池が入っているとは。しかもちっぽけなボタン電池。すぐにでも切れてしまいそうではないか。)
古いソフトの電池を交換する上で何が問題かといえば配線の接続方法だ。
元々はスポット溶接で留められている。これなら確実に導通がとれるだろう。しかし家庭にスポット溶接機はない。どうやって留めるか。
まっとうな方法は既にタブが溶接された電池を買うことだ。電池ホルダーを使うのもよいだろう。
しかしなぜか買う気にならない。その辺で売ってないとか割高とかの理由もあるが、たぶん専用品を使うのは負けた気がするという気持ちの問題が大きいと思う。
溶接以外の方法をいろいろ試してみるのだが、なかなかうまくいかない。溶接の素晴らしさが分かる。
リード線をテープで留めただけだと、すぐに接触不良でデータが飛ぶ。
何かしっかりとテンションを掛けられる手段がないだろうか。
リード線を熱収縮チューブで固定してテンションを掛ければどうかと試してみたが、駄目であった。しばらくしてデータが飛んだ。
また厚みでケースに微妙に収まらず、無理やり閉じたらケースが膨らんだ。(もっともこれはCR2032を使ったのが悪かった。1616にすれば大丈夫だったかもしれない)
もっとしっかりとテンションが掛かる物を考えるとクリップが思いつくが、GBソフトの狭いスペースに入るものはなかなかないだろう。
電池に直接ハンダ付けするという最終手段もあるが、アルカリ電解液の詰まった電池に高熱を与えるのは大変危険なのであまりやりたくない。またそもそも材質がハンダをはじくのであまりしっかりと付いてくれない。
そんなわけでなかなかこれといった方法が見つからなかったのだが、ふと導電インクが使えるのではないかと思い立った。
以前何かに使えるかと思って買ったものの何にも使っていなかったものだ。
「Bare Conductive Electric Paint」という名のもので、調べたら同じものがAmazonでも売っている。
試してみると、予想以上によい。
この種のカーボンタイプの導電インクは回路を描くには少々抵抗が大きいのだが、今回導線と電池表面の間のわずかな隙間を埋めるだけなので抵抗は無視できる。
無視できる…と思うが念のため計ってみると30Ω。思ったよりはあるな。
しかし電流はわずかなので抵抗による電圧降下は気にする必要はないだろう。計算したら1mVもないくらいだし実際計っても分からない。
そして接着力だが、結構ある。剥がそうと思って力を入れれば線を引っ張って剥がしたり爪で引っ掻いて剥がしたりはできるが、意図せず剥がれるようなことは無さそうな程度にある。
付きにくいハンダ付けよりはるかに信頼できる。
最初に試したものがこちらだ。
導線の太さで厚みが出て、熱収縮チューブのときほどではないものの微妙にケースが膨らんでしまった。
このあたりでそもそもGBソフトに使われている電池はCR2032でなくCR1616であったことに気づいたが、買いに行くのは面倒だし、うまくすれば入りそうなのでそのまま2032を使い続けることにした。
もう1点問題があり、基板上に電池を固定するために斜めにテープを張ったのだが、カートリッジの上蓋は枠が出っ張っているので閉める時引っかかってしまう。
それを踏まえて2個め。
厚みを抑えるために芯線をバラして接着することにした。カプトンテープで絶縁をとってある。
この方が抵抗をより抑えられそうな気がするし。しかし残念ながらこちらの抵抗は測り忘れてしまった…。
基板にはんだ付け。カートリッジのケースに入れてみると問題なく閉まった。
固定はカプトンテープでは上手くできず、グルーガンで留めようかと思っていたのだが、ただ閉じるだけで振ってもガタつくこと無く一応固定されているようなのでそのままにしてしまった。
1個めが2ヶ月半、2個めも1ヶ月ほど経つがどちらもデータを保持できている。
2018/12/22:
ふと思い出して確認したところどちらもまだデータを保持できている。
2020/11/16:
ふと思い出して確認したところ、1個めはデータが消えてしまっていた…。カートリッジコネクタの接触不良で何度か挿し直したのが悪影響を与えたかもしれない。新しくデータを作成し数分後確認すると正常。
2個めはデータを保持できている。
2021/10/21:
ふと思い出して確認したところどちらもデータを保持できている。(1個めのは2020/11/16に消えて作り直したデータ)
何もしなければデータがもつことの確認はもう十分かな。せっかくなので開けて見てみよう。
3年半経ってどう変わったか。
うーん…見てもよく分からない。
テープも取ってみるか…
うむ、特に接着面が剥がれかけてるとかそういうことは無さそう。(テープに付着しているが、乾く前にテープを貼ったので不思議はない)
バックアップ電池の交換にカーボン系導電インク、なかなか良さそうだ。
(最初に知った時には驚いた。壊れなければ永遠に使えると思っていたゲームソフトにまさか寿命のある電池が入っているとは。しかもちっぽけなボタン電池。すぐにでも切れてしまいそうではないか。)
古いソフトの電池を交換する上で何が問題かといえば配線の接続方法だ。
元々はスポット溶接で留められている。これなら確実に導通がとれるだろう。しかし家庭にスポット溶接機はない。どうやって留めるか。
まっとうな方法は既にタブが溶接された電池を買うことだ。電池ホルダーを使うのもよいだろう。
しかしなぜか買う気にならない。その辺で売ってないとか割高とかの理由もあるが、たぶん専用品を使うのは負けた気がするという気持ちの問題が大きいと思う。
溶接以外の方法をいろいろ試してみるのだが、なかなかうまくいかない。溶接の素晴らしさが分かる。
リード線をテープで留めただけだと、すぐに接触不良でデータが飛ぶ。
何かしっかりとテンションを掛けられる手段がないだろうか。
リード線を熱収縮チューブで固定してテンションを掛ければどうかと試してみたが、駄目であった。しばらくしてデータが飛んだ。
また厚みでケースに微妙に収まらず、無理やり閉じたらケースが膨らんだ。(もっともこれはCR2032を使ったのが悪かった。1616にすれば大丈夫だったかもしれない)
もっとしっかりとテンションが掛かる物を考えるとクリップが思いつくが、GBソフトの狭いスペースに入るものはなかなかないだろう。
電池に直接ハンダ付けするという最終手段もあるが、アルカリ電解液の詰まった電池に高熱を与えるのは大変危険なのであまりやりたくない。またそもそも材質がハンダをはじくのであまりしっかりと付いてくれない。
そんなわけでなかなかこれといった方法が見つからなかったのだが、ふと導電インクが使えるのではないかと思い立った。
以前何かに使えるかと思って買ったものの何にも使っていなかったものだ。
「Bare Conductive Electric Paint」という名のもので、調べたら同じものがAmazonでも売っている。
試してみると、予想以上によい。
この種のカーボンタイプの導電インクは回路を描くには少々抵抗が大きいのだが、今回導線と電池表面の間のわずかな隙間を埋めるだけなので抵抗は無視できる。
無視できる…と思うが念のため計ってみると30Ω。思ったよりはあるな。
しかし電流はわずかなので抵抗による電圧降下は気にする必要はないだろう。計算したら1mVもないくらいだし実際計っても分からない。
そして接着力だが、結構ある。剥がそうと思って力を入れれば線を引っ張って剥がしたり爪で引っ掻いて剥がしたりはできるが、意図せず剥がれるようなことは無さそうな程度にある。
付きにくいハンダ付けよりはるかに信頼できる。
最初に試したものがこちらだ。
導線の太さで厚みが出て、熱収縮チューブのときほどではないものの微妙にケースが膨らんでしまった。
このあたりでそもそもGBソフトに使われている電池はCR2032でなくCR1616であったことに気づいたが、買いに行くのは面倒だし、うまくすれば入りそうなのでそのまま2032を使い続けることにした。
もう1点問題があり、基板上に電池を固定するために斜めにテープを張ったのだが、カートリッジの上蓋は枠が出っ張っているので閉める時引っかかってしまう。
それを踏まえて2個め。
厚みを抑えるために芯線をバラして接着することにした。カプトンテープで絶縁をとってある。
この方が抵抗をより抑えられそうな気がするし。しかし残念ながらこちらの抵抗は測り忘れてしまった…。
基板にはんだ付け。カートリッジのケースに入れてみると問題なく閉まった。
固定はカプトンテープでは上手くできず、グルーガンで留めようかと思っていたのだが、ただ閉じるだけで振ってもガタつくこと無く一応固定されているようなのでそのままにしてしまった。
1個めが2ヶ月半、2個めも1ヶ月ほど経つがどちらもデータを保持できている。
2018/12/22:
ふと思い出して確認したところどちらもまだデータを保持できている。
2020/11/16:
ふと思い出して確認したところ、1個めはデータが消えてしまっていた…。カートリッジコネクタの接触不良で何度か挿し直したのが悪影響を与えたかもしれない。新しくデータを作成し数分後確認すると正常。
2個めはデータを保持できている。
2021/10/21:
ふと思い出して確認したところどちらもデータを保持できている。(1個めのは2020/11/16に消えて作り直したデータ)
何もしなければデータがもつことの確認はもう十分かな。せっかくなので開けて見てみよう。
3年半経ってどう変わったか。
うーん…見てもよく分からない。
テープも取ってみるか…
うむ、特に接着面が剥がれかけてるとかそういうことは無さそう。(テープに付着しているが、乾く前にテープを貼ったので不思議はない)
バックアップ電池の交換にカーボン系導電インク、なかなか良さそうだ。
2017年01月31日 01:54
以前、秋月で超小型のI2Cキャラクタ液晶(I2C接続小型キャラクタLCDモジュール 8x2行)を買い、PIC10F200で色々と表示していた。
ある時思いついたネタを実装しようとしたところ10F200ではROMが足りなかったので、石をPIC12F510に変えることにした。
移植作業はわりと楽であった。ADコンバータ関連を無効化すればあとはだいたい似たようなものである。
しかし動かない。
しかも不思議な事に信号線にLEDやテスターをつなぐと動く。つまり原因は単純なプログラムのミスではなくアナログ的な微妙な差異によるもののようだ。
色々考えたが分からないのでその時はあきらめた。一昨年の5月ころだ。
(昨年の4月に)その原因が判明したので顛末をここに書き留める。何かの参考になれば幸いだ。
まず最初に言っておくと、このプログラムで出していた信号は正常なI2Cではない。
第一に、本来I2CはLowとプルアップで駆動するものをHighとLowで駆動している。プログラムの簡略化と高速化の為だ。
第二に、クロック立ち下がりからデータ変化の時間、tHD:DATはデータシートによれば最小0・最大0.9μs(後述)のところ、最小ギリギリの0にしている。10F200で可能な間隔が1μsの為だ。またデータシートによればこれでも300nsの余裕があるように読める(後述)。
もっとまともな信号にすれば動くかもしれなかったが、それは解決にならない。
問題は動かない事ではなく、動かない理由が分からない事である。
規格外や規格ギリギリではあるものの、これでも動くはずだと考えてやっているのだし、実際10F200ではほぼ確実に動くのである。
この状態で適当に変更して動くようになったとしても本当にその変更が正しかったのか分からない。何か重大な思い違いがあったら危険だ。
またなにより、何が問題なのか分からないままというのは気持ち悪い。
移植して動かなかった時から(覚えている限り)時系列に沿って状況を書く。
MPLABのシミュレータで見た波形は全く同じ。
怪しいのは実際に信号が出ているかだ。TRISの設定を間違ったり、間違って別のピンにつないでいたりすることはしょっちゅうだ。
なので信号が出ているかどうかを確認するため、信号線に青LEDをつないだ。(LEDをつなぐと点滅具合で色々分かる。青なのは電源が電池2本の時抵抗を入れなくても大丈夫で便利だからだ)
すると…不思議なことにほぼ確実に動作するようになった。
最初はデータとクロック両方に付けていたが、データ線だけでも動作する。
赤LED(+抵抗)だと駄目。電圧の問題かと思い赤LED+ダイオード1、2本で試すも駄目。また、データ線にテスターを当てるとほぼ確実に動作する。
この時点で問題はアナログ的なものと分かった。
アナログ的な仕様に何か違いがあるのではないかとデータシートを見る。ほとんど同じだったが、1つだけ、デバイスリセットタイマ(DRT)の時間が異なっていた。早速その分の待ちを入れてみる。
…が、変わらず。
もはやこれはオシロスコープを買うしかない。
2万円の機種と迷ったが3万円のものを購入。
…と思ったがよく見るとそれはACKが返っていないだけであった。
分かったことは、最初の1バイト(アドレス)から既にACKが返っていないということだけ。
あとはノイズだが、電源投入からI2C送信までの間に目立ったノイズは無く、電源投入時のノイズはわりと酷いがどちらも同様に酷い。
そして10F200と12F510を比較してみると、I2C信号を送信するところは一切違いが見当たらないが、リセット直後の挙動が違っていた。
調べてみると、デバイスリセットタイマ相当の待ちを入れた時、待ちの後にHigh-ZからHighにするつもりのところを間違って待ちの前にHighにしてしまっていた。
…しかしこれを直しても変わらず。
もしかしてデバイスリセットタイマ中とプログラムが走っている時では何か違いがあるのだろうか。そうなると12F510で動かすのは不可能ということに。
この辺でできることも無くなり諦めてしばらく経った。
ある日、なんとなくI2Cについて考えていて思った。なぜtHD:DATに上限があるのだろう。
液晶コントローラのデータシートに上限があるように書かれていたので不審に思いながらも従っていたのだが、やはりおかしい。
I2Cのデータシートを見ると、違う書かれ方をしている。(なお後で知ったが古いデータシートでは上限0.9μsになっていた)
上限はなし、その上で注釈として、
しかしさらに考えてやっと分かった。この0.9μsという上限の値は、スレーブに対する制限なのだ。
「マスターがクロックを生成しスレーブがデータを送信するとき、マスターがクロックをLowにしている間にスレーブはデータを変化させなければならない」という当たり前の制限について言っているだけだ。
クロックがLowの間にデータを変化させるには、データのホールド時間(tHD:DAT)・データの立ち上がり時間(tr)または立ち下がり時間(tf)・データのセットアップ時間(tSU:DAT)の和がクロックのLow期間(tLOW)より短くなければならない。
ファーストモードではクロックのLow期間の最小値が1.3μs、立ち上がり時間の最大値が300ns、データセットアップ時間の最小値が100nsなので、
1.3-0.3-0.1=0.9
となり注記の値に一致する。
スタンダードモードでも同様に
4.7-1-0.25=3.45
となる。
今回データ送信はマスター側からのみ行うため、スレーブの制限は無関係だ。よって最大0.9μsは無視してもっと長い時間とれば余裕を持った通信ができる。
解決方法は見えた。あとは原因の方だ。10F200と12F510、LEDやテスタリードを付けた12F510で何が違うのか。
もう一度オシロで波形を見てみよう。
ここまでI2Cの通信内容が分かる程度の縮尺で見ていたのだが、限界までサンプリング速度を上げて拡大して見てみる。すると波形が明らかに違っていた!
10F200はシンクが強いのか立ち下がりが速い。
これによって、SCK・SDAを同時に操作してもSCKの立ち下がりの後にSDAの立ち上がりが来てくれていたようなのだ。それが12F510では逆になっている。
この違いで動作が分かれていたのだろう。
なおこの3万円のオシロのサンプリング速度は250Msps(2ch時)、帯域は60MHz。迷っていた2万円の方の機種は100Msps・25MHzである。
1/(60MHz)=17ns、1/(25MHz)=40nsであり、25MHzの帯域で今回の波形の違いを捉えられたかはかなり怪しい。
高い方を選んで良かった。
さて最後に、冒頭で言った300nsの余裕の件だ。
tHD:DATの下限について、以下のように注釈がある。バージョンによって言い回しが少々変わっているが内容は同じようだ。
「SCLの立ち下がりの不定区間を橋渡しするため、デバイスはSDA信号に対して内部的に最低300nsのホールド時間を(SCL信号のVIH(min)を基準に)用意しなければならない。」
internally・reffered・with respect to・bridgeあたりの意味がよく分からない。
なお誤訳の可能性を考えてオランダ語の原文があるかと探したのだがどうやら英語が原文のようである。
これを自分はこう理解した。
受信側はSDA線の立ち下がりから300ns間内部的にHighと扱う。
よってSCKよりSDAが(300ns-立ち下がり時間)だけ早くてもよい。
立ち下がり時間の最大が300nsなので、都合余裕は0。
これは12F510で動作しない現状と食い違うが、上記の文章の解釈は他に思いつかない。
(「送信側がSDA線を300ns遅らせて駆動しなければならない」という説も見かけたが、それではinternallyでないのと、それはtHD:DATの下限が300nsと書けばよいことなので、別に注釈として書かれている説明がつかない。)
自分の解釈が間違っているのか、このデバイスが仕様に従っていないのか分からない。
まあどちらにせよ、諸悪の根源はI2Cの仕様書が分かりづらいせいだ。マスターの条件とスレーブの条件が混ざって書かれているし、0.9μsだの300nsだのと注釈で変な条件を追加するし、その注釈は文章の意味が取れない。
とそんなわけで不可解な点は残ったものの、まとめると、
・最大値が0.9μsだと思っていたが制限は無かった。
・最小値が0だが300nsの余裕があると思っていたが、文の解釈はよく分からず、現に余裕は無かった。
・石を変えて動かなくなったのは立ち下がりの速さの差のせい。
tHD:DATを長くしたところ、安定して動くようになった。
(なお表示は温度計を作ろうとしていた時のダミー表示)
ある時思いついたネタを実装しようとしたところ10F200ではROMが足りなかったので、石をPIC12F510に変えることにした。
移植作業はわりと楽であった。ADコンバータ関連を無効化すればあとはだいたい似たようなものである。
しかし動かない。
しかも不思議な事に信号線にLEDやテスターをつなぐと動く。つまり原因は単純なプログラムのミスではなくアナログ的な微妙な差異によるもののようだ。
色々考えたが分からないのでその時はあきらめた。一昨年の5月ころだ。
(昨年の4月に)その原因が判明したので顛末をここに書き留める。何かの参考になれば幸いだ。
まず最初に言っておくと、このプログラムで出していた信号は正常なI2Cではない。
第一に、本来I2CはLowとプルアップで駆動するものをHighとLowで駆動している。プログラムの簡略化と高速化の為だ。
第二に、クロック立ち下がりからデータ変化の時間、tHD:DATはデータシートによれば最小0・最大0.9μs(後述)のところ、最小ギリギリの0にしている。10F200で可能な間隔が1μsの為だ。またデータシートによればこれでも300nsの余裕があるように読める(後述)。
もっとまともな信号にすれば動くかもしれなかったが、それは解決にならない。
問題は動かない事ではなく、動かない理由が分からない事である。
規格外や規格ギリギリではあるものの、これでも動くはずだと考えてやっているのだし、実際10F200ではほぼ確実に動くのである。
この状態で適当に変更して動くようになったとしても本当にその変更が正しかったのか分からない。何か重大な思い違いがあったら危険だ。
またなにより、何が問題なのか分からないままというのは気持ち悪い。
移植して動かなかった時から(覚えている限り)時系列に沿って状況を書く。
MPLABのシミュレータで見た波形は全く同じ。
怪しいのは実際に信号が出ているかだ。TRISの設定を間違ったり、間違って別のピンにつないでいたりすることはしょっちゅうだ。
なので信号が出ているかどうかを確認するため、信号線に青LEDをつないだ。(LEDをつなぐと点滅具合で色々分かる。青なのは電源が電池2本の時抵抗を入れなくても大丈夫で便利だからだ)
すると…不思議なことにほぼ確実に動作するようになった。
最初はデータとクロック両方に付けていたが、データ線だけでも動作する。
赤LED(+抵抗)だと駄目。電圧の問題かと思い赤LED+ダイオード1、2本で試すも駄目。また、データ線にテスターを当てるとほぼ確実に動作する。
この時点で問題はアナログ的なものと分かった。
アナログ的な仕様に何か違いがあるのではないかとデータシートを見る。ほとんど同じだったが、1つだけ、デバイスリセットタイマ(DRT)の時間が異なっていた。早速その分の待ちを入れてみる。
…が、変わらず。
もはやこれはオシロスコープを買うしかない。
2万円の機種と迷ったが3万円のものを購入。
さて。オッシロスコォォォォォォォォォォォォォォォォォォォォ pic.twitter.com/sfKxocEENH
— 雷更新世 (@pleist) 2015年5月30日
オシロスコープで12F510で動く時(青LED付き)と動かない時を確認すると信号に違いが!ォォォォォゥプ!!! pic.twitter.com/z07c7Gccbp
— 雷更新世 (@pleist) 2015年5月30日
…と思ったがよく見るとそれはACKが返っていないだけであった。
分かったことは、最初の1バイト(アドレス)から既にACKが返っていないということだけ。
あとはノイズだが、電源投入からI2C送信までの間に目立ったノイズは無く、電源投入時のノイズはわりと酷いがどちらも同様に酷い。
そして10F200と12F510を比較してみると、I2C信号を送信するところは一切違いが見当たらないが、リセット直後の挙動が違っていた。
調べてみると、デバイスリセットタイマ相当の待ちを入れた時、待ちの後にHigh-ZからHighにするつもりのところを間違って待ちの前にHighにしてしまっていた。
…しかしこれを直しても変わらず。
もしかしてデバイスリセットタイマ中とプログラムが走っている時では何か違いがあるのだろうか。そうなると12F510で動かすのは不可能ということに。
この辺でできることも無くなり諦めてしばらく経った。
ある日、なんとなくI2Cについて考えていて思った。なぜtHD:DATに上限があるのだろう。
液晶コントローラのデータシートに上限があるように書かれていたので不審に思いながらも従っていたのだが、やはりおかしい。
I2Cのデータシートを見ると、違う書かれ方をしている。(なお後で知ったが古いデータシートでは上限0.9μsになっていた)
上限はなし、その上で注釈として、
The maximum tHD:DAT could be 3.45 μs and 0.9 μs for Standard-mode and Fast-mode, but must be less than the maximum of tVD:DAT or tVD:ACK by a transition time.CBUSとの兼ね合いかとも思って調べようともしていたが、どうも死んだ規格らしく情報がない。その辺で当時は諦めていた気がする。
最大のtHD:DATはスタンダードモードで3.45μs、ファースト(fast)モードで0.9μ秒になりうるが、tVD:DATまたはtVD:ACKの最大値よりトランジション時間だけ短くなくてはならない。
This maximum must only be met if the device does not stretch the LOW period (tLOW) of the SCL signal.
この最大値はデバイスがSCL信号のLOW期間(tLOW)をストレッチしないときのみ満たす必要がある。
If the clock stretches the SCL, the data must be valid by the set-up time before it releases the clock.
もしクロックがSCLをストレッチするなら(訳注: 意味が分からない。SCLはクロックだ)、データはそれ(訳注: どれ? クロック?)がクロックを解放する前にセットアップ時間だけ有効でなければならない。
しかしさらに考えてやっと分かった。この0.9μsという上限の値は、スレーブに対する制限なのだ。
「マスターがクロックを生成しスレーブがデータを送信するとき、マスターがクロックをLowにしている間にスレーブはデータを変化させなければならない」という当たり前の制限について言っているだけだ。
クロックがLowの間にデータを変化させるには、データのホールド時間(tHD:DAT)・データの立ち上がり時間(tr)または立ち下がり時間(tf)・データのセットアップ時間(tSU:DAT)の和がクロックのLow期間(tLOW)より短くなければならない。
ファーストモードではクロックのLow期間の最小値が1.3μs、立ち上がり時間の最大値が300ns、データセットアップ時間の最小値が100nsなので、
1.3-0.3-0.1=0.9
となり注記の値に一致する。
スタンダードモードでも同様に
4.7-1-0.25=3.45
となる。
今回データ送信はマスター側からのみ行うため、スレーブの制限は無関係だ。よって最大0.9μsは無視してもっと長い時間とれば余裕を持った通信ができる。
解決方法は見えた。あとは原因の方だ。10F200と12F510、LEDやテスタリードを付けた12F510で何が違うのか。
もう一度オシロで波形を見てみよう。
ここまでI2Cの通信内容が分かる程度の縮尺で見ていたのだが、限界までサンプリング速度を上げて拡大して見てみる。すると波形が明らかに違っていた!
10F200はシンクが強いのか立ち下がりが速い。
これによって、SCK・SDAを同時に操作してもSCKの立ち下がりの後にSDAの立ち上がりが来てくれていたようなのだ。それが12F510では逆になっている。
この違いで動作が分かれていたのだろう。
なおこの3万円のオシロのサンプリング速度は250Msps(2ch時)、帯域は60MHz。迷っていた2万円の方の機種は100Msps・25MHzである。
1/(60MHz)=17ns、1/(25MHz)=40nsであり、25MHzの帯域で今回の波形の違いを捉えられたかはかなり怪しい。
高い方を選んで良かった。
さて最後に、冒頭で言った300nsの余裕の件だ。
tHD:DATの下限について、以下のように注釈がある。バージョンによって言い回しが少々変わっているが内容は同じようだ。
・A device must internally provide a hold time of at least 300 ns for the SDA signal (referred to the VIHmin of the SCL signal) to bridge the undefined region of the falling edge of SCL.
・A device must internally provide a hold time of at least 300 ns for the SDA signal (with respect to the VIH(min) of the SCL signal) to bridge the undefined region of the falling edge of SCL.この文章も微妙に意味が取れないのだが、精一杯の訳が以下だ。
「SCLの立ち下がりの不定区間を橋渡しするため、デバイスはSDA信号に対して内部的に最低300nsのホールド時間を(SCL信号のVIH(min)を基準に)用意しなければならない。」
internally・reffered・with respect to・bridgeあたりの意味がよく分からない。
なお誤訳の可能性を考えてオランダ語の原文があるかと探したのだがどうやら英語が原文のようである。
これを自分はこう理解した。
受信側はSDA線の立ち下がりから300ns間内部的にHighと扱う。
よってSCKよりSDAが(300ns-立ち下がり時間)だけ早くてもよい。
立ち下がり時間の最大が300nsなので、都合余裕は0。
これは12F510で動作しない現状と食い違うが、上記の文章の解釈は他に思いつかない。
(「送信側がSDA線を300ns遅らせて駆動しなければならない」という説も見かけたが、それではinternallyでないのと、それはtHD:DATの下限が300nsと書けばよいことなので、別に注釈として書かれている説明がつかない。)
自分の解釈が間違っているのか、このデバイスが仕様に従っていないのか分からない。
まあどちらにせよ、諸悪の根源はI2Cの仕様書が分かりづらいせいだ。マスターの条件とスレーブの条件が混ざって書かれているし、0.9μsだの300nsだのと注釈で変な条件を追加するし、その注釈は文章の意味が取れない。
とそんなわけで不可解な点は残ったものの、まとめると、
・最大値が0.9μsだと思っていたが制限は無かった。
・最小値が0だが300nsの余裕があると思っていたが、文の解釈はよく分からず、現に余裕は無かった。
・石を変えて動かなくなったのは立ち下がりの速さの差のせい。
tHD:DATを長くしたところ、安定して動くようになった。
(なお表示は温度計を作ろうとしていた時のダミー表示)
2017年01月16日 22:44
前編の続き。プログラム側について。
まずは単純に読むことを試みる。
手持ちの中でバンク切り替えなし(32kB)のソフトとしてDr.マリオを選択。
バンク切り替えが無ければアドレスを出してデータを読むだけ。
…とはいえCLKとかCSとかRDとかどう制御すればいいのか。
GBのカートリッジの仕様くらいいくらでも見つかると思っていたのだが、ROMの読み出し方法は常識なのか、細かい解説が意外と見つからない。
結局、分かってみると単純で、
/RDがLにアサートされている間、アドレスピンで指定されたアドレスのデータが、データピンに出る
というだけのことだった。
つまり読み出すには、/RDをLにアサートしたままアドレスを順次変え、データピンを読むだけでよい。
これが分かるまでに/RDをH/L切り替えてしていた。
あと/CSはSRAMを読む時アサートするものだった。
プログラミング自体もなんだかんだで苦労した。
やはり合っているか分からない操作を正しく組めているか分からない機械にプログラムするのは疑心暗鬼に陥ってだいぶ精神を消耗する。
まずシリアル通信するところからしてうまくいかない。
シリアルポートが全く反応しなくて焦った挙句Windows10へアップグレードした時にケーブル類全部抜いてたのを戻してなかっただけだったりもした。
何か出るようになったと思えば文字化けしている。これはどうもTeraTermのバグかWindows10との相性のようで、最新版を使ったら正常であった。
新しいPICを使う際には毎回のようにGPIO以外の機能を切っていなくて問題が起こるのだが、今回もまんまとその罠にはまった。
ADコンバータとコンパレータを切ったまでは良かったのだが、それで読もうとしても何も読めない(FFが読める)。
ポートのレジスタを直接インクリメントしてたのがまずかったかと思い、別の変数をインクリメントしてそれを出力するようにしたところ何かは読めるようになったが、全体にわたりほぼ確実に8バイトづつ同じ値が取れる。
つまりアドレスの下3bitが何かおかしい。そのピンを調べると、LCDドライバの電圧生成機能がデフォルトでONであった。
ということはポートのレジスタを直接インクリメントすること自体は問題なく、最下位bitの書き込みが無視されていたせいでインクリメントできていなかったんだな。
というわけでついにDr.マリオが読めた。
次はバンク切り替え…の前に色々なソフトをバンク0だけ読んでみることにした。
するとポケモンYellowやポケモンカードなど読めるものもあるが、ポケモンSilverが読めない。
読めないというのは、ほぼ全てFFが返ってくる。ごく稀にFF以外のものが返ってくることもあるのがまた不可解である。
FFでない箇所のパターンは規則的で、なにかありそうである。2進数で「xxxx xxx1 0000 000x」と「xx00 0110 0000 0001」、つまり0100, 0101, 0300, 0301,…と0601, 4601がFFでない。
不要なはずのクロックだがMBCを積んでいることもあり何か変わるのではないかと入れてみる。当然変わらず。
…散々悩んだ挙句、電圧不足であった。
いつもPICを動かす時はEneloop2本(2.6V程度)を使っていて、使いやすい5V電源を持っていないこともあり、とりあえずそれでやっていたのだが、GBの電圧は5Vなので動かなくてなんの不思議も無いのであった。
Dr.マリオを始めいくつかのソフトで(バンク0は)読めていたので発覚が遅れた。
改めてバンク切り換えだ。
バンクの少ない(最少の4バンク)ものとしてQIXを選択。
バンク切り換えの手順はMBCによって少々違うが、基本的に特定のアドレスにデータを書き込むだけである。
書き込むべきデータを入れていなかったり、PICのIOを入力のまま出力したつもりになっていたり、書き込むアドレスを間違ったりして手間取ったが、まあまあすんなりと読めた。
さて続いて本命のポケモンSilverだ。
128バンクあるが、バンク切り換えのやり方は同じなので、単純に数が多いだけ。難しいことは何もない。
しかしなぜか途中でデータが飛ぶ。読み取ったデータを見るとファイルサイズが想定より小さい。
今までこのような大量のデータをシリアルで受信したことは無かった。シリアル通信の信頼性はこの程度なのだろうか。
だがそれは想定の内。1バンクごとに目印を入れてあるのでどこで抜けたかは分かる。何度か読んで正常な部分を切り貼りすればよいだろう。
…と思っていたのだが、不思議な事に常にエラーが出ている場所がある。
バッファ切れを疑ってバッファ量を変えたりウェイトを入れたりしてもなんとなく変化はあるものの直らず。
データの問題かと思いXOR 0x55したデータを送ってみると抜けの量はほぼ変わらず、抜けの位置が変わった。特定のデータが来ると問題が起こるのだろうか。
速度を落としてみるとだいぶ改善した。抜けが6バンクまで減ったので試しに起動してみると、一応起動はした。
このような分かりやすいバグった表示になるものなんだなあ。
なお部屋に出口がないので進めなかった。その後もう1バンク正常に取れたのでそれと合わせると部屋から出られたがBGMが異常になったりする。
しかしここで何度とってもほぼ同じ場所でエラーを起こす。
やはり速度を落とすだけでは解決しない。特定のデータが問題という線で考えてみよう。
改行の処理に時間が掛かっている可能性を考えてCRの後にウェイトを入れてみる。むしろ悪化。
あとは…エスケープシーケンス。何らかのエスケープシーケンスが来るとそれの処理に時間がかかってデータを取りこぼすのではないか。
ここでTeraTermを調べて、受信した文字をすべて表示するデバッグモードがあることを知る。
デバッグモードで受信するようにしたところ、一切取りこぼさなくなった!
後で調べたところ、制御シーケンスに「1B 63: 端末リセット」というものがあるらしい。
つまりこのバイト列が来るとTeraTermがリセット動作を起こし、その間に来たデータを取りこぼしていたようだ。
調べてみると「1B 63」は最後まで読めなかった5バンク中の3ヶ所にあった。残り2ヶ所やそれ以前のエラー箇所は分からないままだがたぶん他のエスケープシーケンスだろう。
デバッグモード以外にエスケープシーケンスを無視する方法がないか調べたのだが、見つからなかった。
人が読む文字列を出す時は改行は使いたいのだが、致し方ない。
次に困ったのがX(エックス)だ。バンク切り替えができない。
調べてみるとこれに使われているMBC2はバンク切り替え時に書き込むアドレスに制限があり、今まで使っていた0x2000では駄目だった。
と0x2100に変えてみたが、やはりバンクは切り替わらない。
そこで読み取れたバンク0のコードを見てみることにした。この中にバンク切り替えのコードがあるはずである。
するとやはり0x2100に書き込んでいる。
合っているのにおかしいなと思い更に調べると、バンク1に変更する時は0x2100だったのだが、バンク2では0x2101、バンク3では0x2102…と、バンクNに変更する時0x2100+(N-1)に書き込むようになっていた。
1少ないのは書き込み後のインクリメントの関係だろう。ということでバンクNに変更する時は0x2100+Nに書き込むようにコードを書き換えてみると、見事読み取りに成功した。
なんだろう。バスコンフリクトだろうか。ファミコンのMMCでバスコンフリクトを起こすものがあるという情報はあるが、GBで起こるというのは見たことがなかった。
さて次はニンテンドウパワーのGBメモリの読み取りを試みている。
これは複数のMBCの動作を再現する特殊なコントローラを積んでおり、普通のバンク切り換えとは異なるコマンドを入れる必要があるらしい。
色々試しているのだがまだ一切反応がない。一番つらい時期だ。
読み取りができたら、どうも書き込みも出来るらしいのでやりたい。自作ソフトを実機で動かすのは夢である。
ただその前に、どうも読み取りが安定しないのでどうにかしたい。
今まで読めていたソフトでも読めなくなったりしている。
断線しかかっているとかだろうか…。
まずは単純に読むことを試みる。
手持ちの中でバンク切り替えなし(32kB)のソフトとしてDr.マリオを選択。
バンク切り替えが無ければアドレスを出してデータを読むだけ。
…とはいえCLKとかCSとかRDとかどう制御すればいいのか。
GBのカートリッジの仕様くらいいくらでも見つかると思っていたのだが、ROMの読み出し方法は常識なのか、細かい解説が意外と見つからない。
結局、分かってみると単純で、
/RDがLにアサートされている間、アドレスピンで指定されたアドレスのデータが、データピンに出る
というだけのことだった。
つまり読み出すには、/RDをLにアサートしたままアドレスを順次変え、データピンを読むだけでよい。
これが分かるまでに/RDをH/L切り替えてしていた。
あと/CSはSRAMを読む時アサートするものだった。
プログラミング自体もなんだかんだで苦労した。
やはり合っているか分からない操作を正しく組めているか分からない機械にプログラムするのは疑心暗鬼に陥ってだいぶ精神を消耗する。
まずシリアル通信するところからしてうまくいかない。
シリアルポートが全く反応しなくて焦った挙句Windows10へアップグレードした時にケーブル類全部抜いてたのを戻してなかっただけだったりもした。
何か出るようになったと思えば文字化けしている。これはどうもTeraTermのバグかWindows10との相性のようで、最新版を使ったら正常であった。
新しいPICを使う際には毎回のようにGPIO以外の機能を切っていなくて問題が起こるのだが、今回もまんまとその罠にはまった。
ADコンバータとコンパレータを切ったまでは良かったのだが、それで読もうとしても何も読めない(FFが読める)。
ポートのレジスタを直接インクリメントしてたのがまずかったかと思い、別の変数をインクリメントしてそれを出力するようにしたところ何かは読めるようになったが、全体にわたりほぼ確実に8バイトづつ同じ値が取れる。
つまりアドレスの下3bitが何かおかしい。そのピンを調べると、LCDドライバの電圧生成機能がデフォルトでONであった。
ということはポートのレジスタを直接インクリメントすること自体は問題なく、最下位bitの書き込みが無視されていたせいでインクリメントできていなかったんだな。
というわけでついにDr.マリオが読めた。
次はバンク切り替え…の前に色々なソフトをバンク0だけ読んでみることにした。
するとポケモンYellowやポケモンカードなど読めるものもあるが、ポケモンSilverが読めない。
読めないというのは、ほぼ全てFFが返ってくる。ごく稀にFF以外のものが返ってくることもあるのがまた不可解である。
FFでない箇所のパターンは規則的で、なにかありそうである。2進数で「xxxx xxx1 0000 000x」と「xx00 0110 0000 0001」、つまり0100, 0101, 0300, 0301,…と0601, 4601がFFでない。
不要なはずのクロックだがMBCを積んでいることもあり何か変わるのではないかと入れてみる。当然変わらず。
…散々悩んだ挙句、電圧不足であった。
いつもPICを動かす時はEneloop2本(2.6V程度)を使っていて、使いやすい5V電源を持っていないこともあり、とりあえずそれでやっていたのだが、GBの電圧は5Vなので動かなくてなんの不思議も無いのであった。
Dr.マリオを始めいくつかのソフトで(バンク0は)読めていたので発覚が遅れた。
改めてバンク切り換えだ。
バンクの少ない(最少の4バンク)ものとしてQIXを選択。
バンク切り換えの手順はMBCによって少々違うが、基本的に特定のアドレスにデータを書き込むだけである。
書き込むべきデータを入れていなかったり、PICのIOを入力のまま出力したつもりになっていたり、書き込むアドレスを間違ったりして手間取ったが、まあまあすんなりと読めた。
さて続いて本命のポケモンSilverだ。
128バンクあるが、バンク切り換えのやり方は同じなので、単純に数が多いだけ。難しいことは何もない。
しかしなぜか途中でデータが飛ぶ。読み取ったデータを見るとファイルサイズが想定より小さい。
今までこのような大量のデータをシリアルで受信したことは無かった。シリアル通信の信頼性はこの程度なのだろうか。
だがそれは想定の内。1バンクごとに目印を入れてあるのでどこで抜けたかは分かる。何度か読んで正常な部分を切り貼りすればよいだろう。
…と思っていたのだが、不思議な事に常にエラーが出ている場所がある。
バッファ切れを疑ってバッファ量を変えたりウェイトを入れたりしてもなんとなく変化はあるものの直らず。
データの問題かと思いXOR 0x55したデータを送ってみると抜けの量はほぼ変わらず、抜けの位置が変わった。特定のデータが来ると問題が起こるのだろうか。
速度を落としてみるとだいぶ改善した。抜けが6バンクまで減ったので試しに起動してみると、一応起動はした。
このような分かりやすいバグった表示になるものなんだなあ。
なお部屋に出口がないので進めなかった。その後もう1バンク正常に取れたのでそれと合わせると部屋から出られたがBGMが異常になったりする。
しかしここで何度とってもほぼ同じ場所でエラーを起こす。
やはり速度を落とすだけでは解決しない。特定のデータが問題という線で考えてみよう。
改行の処理に時間が掛かっている可能性を考えてCRの後にウェイトを入れてみる。むしろ悪化。
あとは…エスケープシーケンス。何らかのエスケープシーケンスが来るとそれの処理に時間がかかってデータを取りこぼすのではないか。
ここでTeraTermを調べて、受信した文字をすべて表示するデバッグモードがあることを知る。
デバッグモードで受信するようにしたところ、一切取りこぼさなくなった!
後で調べたところ、制御シーケンスに「1B 63: 端末リセット」というものがあるらしい。
つまりこのバイト列が来るとTeraTermがリセット動作を起こし、その間に来たデータを取りこぼしていたようだ。
調べてみると「1B 63」は最後まで読めなかった5バンク中の3ヶ所にあった。残り2ヶ所やそれ以前のエラー箇所は分からないままだがたぶん他のエスケープシーケンスだろう。
デバッグモード以外にエスケープシーケンスを無視する方法がないか調べたのだが、見つからなかった。
人が読む文字列を出す時は改行は使いたいのだが、致し方ない。
次に困ったのがX(エックス)だ。バンク切り替えができない。
調べてみるとこれに使われているMBC2はバンク切り替え時に書き込むアドレスに制限があり、今まで使っていた0x2000では駄目だった。
と0x2100に変えてみたが、やはりバンクは切り替わらない。
そこで読み取れたバンク0のコードを見てみることにした。この中にバンク切り替えのコードがあるはずである。
するとやはり0x2100に書き込んでいる。
合っているのにおかしいなと思い更に調べると、バンク1に変更する時は0x2100だったのだが、バンク2では0x2101、バンク3では0x2102…と、バンクNに変更する時0x2100+(N-1)に書き込むようになっていた。
1少ないのは書き込み後のインクリメントの関係だろう。ということでバンクNに変更する時は0x2100+Nに書き込むようにコードを書き換えてみると、見事読み取りに成功した。
なんだろう。バスコンフリクトだろうか。ファミコンのMMCでバスコンフリクトを起こすものがあるという情報はあるが、GBで起こるというのは見たことがなかった。
さて次はニンテンドウパワーのGBメモリの読み取りを試みている。
これは複数のMBCの動作を再現する特殊なコントローラを積んでおり、普通のバンク切り換えとは異なるコマンドを入れる必要があるらしい。
色々試しているのだがまだ一切反応がない。一番つらい時期だ。
読み取りができたら、どうも書き込みも出来るらしいのでやりたい。自作ソフトを実機で動かすのは夢である。
ただその前に、どうも読み取りが安定しないのでどうにかしたい。
今まで読めていたソフトでも読めなくなったりしている。
断線しかかっているとかだろうか…。
2016年05月28日 14:43
はじめました。2013年6月頃に。なぜ今頃書くかというと、9割書き上げて放置されていたのを今発掘したためだ。
その頃、LPC810というARMマイコンがでた。
8ピンDIPなのでブレッドボードに直接挿せるという。
これが秋葉原のマルツに入荷したというので早速買いに行った。
売り切れだった。
しかし上位種のLPC812は残っていたのでこれを買ってきた。
LPC812はピン数やメモリ容量、周辺機能といった全ての面でLPC810を上回っており、値段もあまり変わらない。今見ると25円差。当時もたぶんそれくらいだったと思う。
ただ1つだけ重大な欠点があり、パッケージがDIPではなくSOPで扱いづらい。
ハーフピッチのユニバーサル基板でピッチ変換してみたが、後悔した。めっちゃめんどい。
秋月でちょうどいい変換基板が80円で売られているのでお勧めだ。
さてこの石はPICやAVRと違って専用の書き込み機が必要ないので出費が少なくて嬉しい。
書き込み用の接続は開発環境が無くても試せるのでまずこれを試す。
用意する物はシリアルポートと7404(NOT)か何かとあと抵抗。
自分はこんなこともあろうかとシリアルポート付きのPCを買っているが、きっとUSB-シリアル変換ケーブルでも大丈夫だと思う。
7404は要するに論理を反転できればいいので7400(NAND)とか7402(NOR)でもよい。自分が使ったのもNANDだ。
シリアルポートとの接続はこのページを参考に直結した。
軽く説明すると、シリアルポートの受信側インピーダンスは3kΩ~7kΩらしいのでそのための4.7kΩと、電圧が高いので電流を落とすための100kΩである。
この回路をLPC812のUSART0ポートに接続、そしてブートローダーを立ち上げるためにPIO0_1をGNDに落とす。
以上で準備は完了。
電源を繋ぎ、適当なターミナルソフトからデータ8bit、パリティなし、ストップ1bit、速度任意の設定で「?」の1文字を送信する。
成功すれば「Synchronized<改行>」が表示されるはずだ。
接続に成功したので次は開発環境のLPCXpressoIDEをいれる。登録とか要るのでまあ適宜なんとかする。
さらに書き込みソフトのFlashMagicをいれる。
IDEを立ち上げる
左下に「Quickstart Panel」があり、その中の「Start here」の中に「Import project(s)」
Examplesフォルダが開いてるのでその下から
\NXP\LPC800\NXP_LPC8xx_SampleCodeBundle.zip
のようなものを探してあとは適当に全部入れる。
Project Explorerを見ると色々入っている。BlinkyというモノがLEDを点滅させてくれそうだ。してみるとこれだけ入れればよかったか。まあいいや。
Blinkyを選んだ状態でメニューからProject→Build Projectを選択。
すると何やら動きだし、デバッグビルドができたようだ。リリースビルドの方が良かったかと切り替えてみたら何やらエラーが出た。まあデバッグで困ることもないしいいや。
さてビルドができたのでFlashMagicから書き込もうとしてみるが、できたファイルはaxfで書き込むためのファイルはhexのようだ。
調べてみるとどうやらaxfファイルはIDEから直接書き込んでデバッグとかするためのファイルのようで、hexファイルを出力するにはaxfから変換する必要があるようだ。めんどい。
http://support.code-red-tech.com/CodeRedWiki/OutputFormatsを参考に
下のバーの右手側にあるというプロジェクト名を探したら左側にあったが、気にせずCtrlクリック。
intel形式のhexファイルにしたいので
arm-none-eabi-objcopy -O ihex blinky.axf blinky.hex
を実行。失敗。ファイルが無いと言われる。
パスが違ってた。こうだな
arm-none-eabi-objcopy -O ihex debug/blinky.axf blinky.hex
今度は成功。思っていたところの1階層上に出力されたがまあいいや。
あとはFlashMagicで適に設定して書き込み。これは難なく成功した。
ブートローダー設定用のGNDに落としたピンを外して電源を入れると
…動いた!
3つのLEDが順番に消灯する。多分順番に点灯するべきなので、+-を間違っているようだがまあいいや。
その後TVにカラー出力など試していた
が、命令プリフェッチの関係かクロック数がどう数えても合わないので力尽きた。
より困難な(プリフェッチ幅が広く、I/O命令が遅い)LPC1114でNTSCカラー出力を実現したPancake(IchigoJam周辺機器)はすごいと思う。尊敬する。
その頃、LPC810というARMマイコンがでた。
8ピンDIPなのでブレッドボードに直接挿せるという。
これが秋葉原のマルツに入荷したというので早速買いに行った。
売り切れだった。
しかし上位種のLPC812は残っていたのでこれを買ってきた。
LPC812はピン数やメモリ容量、周辺機能といった全ての面でLPC810を上回っており、値段もあまり変わらない。今見ると25円差。当時もたぶんそれくらいだったと思う。
ただ1つだけ重大な欠点があり、パッケージがDIPではなくSOPで扱いづらい。
ハーフピッチのユニバーサル基板でピッチ変換してみたが、後悔した。めっちゃめんどい。
秋月でちょうどいい変換基板が80円で売られているのでお勧めだ。
さてこの石はPICやAVRと違って専用の書き込み機が必要ないので出費が少なくて嬉しい。
書き込み用の接続は開発環境が無くても試せるのでまずこれを試す。
用意する物はシリアルポートと7404(NOT)か何かとあと抵抗。
自分はこんなこともあろうかとシリアルポート付きのPCを買っているが、きっとUSB-シリアル変換ケーブルでも大丈夫だと思う。
7404は要するに論理を反転できればいいので7400(NAND)とか7402(NOR)でもよい。自分が使ったのもNANDだ。
シリアルポートとの接続はこのページを参考に直結した。
軽く説明すると、シリアルポートの受信側インピーダンスは3kΩ~7kΩらしいのでそのための4.7kΩと、電圧が高いので電流を落とすための100kΩである。
この回路をLPC812のUSART0ポートに接続、そしてブートローダーを立ち上げるためにPIO0_1をGNDに落とす。
以上で準備は完了。
電源を繋ぎ、適当なターミナルソフトからデータ8bit、パリティなし、ストップ1bit、速度任意の設定で「?」の1文字を送信する。
成功すれば「Synchronized<改行>」が表示されるはずだ。
接続に成功したので次は開発環境のLPCXpressoIDEをいれる。登録とか要るのでまあ適宜なんとかする。
さらに書き込みソフトのFlashMagicをいれる。
IDEを立ち上げる
左下に「Quickstart Panel」があり、その中の「Start here」の中に「Import project(s)」
Examplesフォルダが開いてるのでその下から
\NXP\LPC800\NXP_LPC8xx_SampleCodeBundle.zip
のようなものを探してあとは適当に全部入れる。
Project Explorerを見ると色々入っている。BlinkyというモノがLEDを点滅させてくれそうだ。してみるとこれだけ入れればよかったか。まあいいや。
Blinkyを選んだ状態でメニューからProject→Build Projectを選択。
すると何やら動きだし、デバッグビルドができたようだ。リリースビルドの方が良かったかと切り替えてみたら何やらエラーが出た。まあデバッグで困ることもないしいいや。
さてビルドができたのでFlashMagicから書き込もうとしてみるが、できたファイルはaxfで書き込むためのファイルはhexのようだ。
調べてみるとどうやらaxfファイルはIDEから直接書き込んでデバッグとかするためのファイルのようで、hexファイルを出力するにはaxfから変換する必要があるようだ。めんどい。
http://support.code-red-tech.com/CodeRedWiki/OutputFormatsを参考に
下のバーの右手側にあるというプロジェクト名を探したら左側にあったが、気にせずCtrlクリック。
intel形式のhexファイルにしたいので
arm-none-eabi-objcopy -O ihex blinky.axf blinky.hex
を実行。失敗。ファイルが無いと言われる。
パスが違ってた。こうだな
arm-none-eabi-objcopy -O ihex debug/blinky.axf blinky.hex
今度は成功。思っていたところの1階層上に出力されたがまあいいや。
あとはFlashMagicで適に設定して書き込み。これは難なく成功した。
ブートローダー設定用のGNDに落としたピンを外して電源を入れると
…動いた!
3つのLEDが順番に消灯する。多分順番に点灯するべきなので、+-を間違っているようだがまあいいや。
その後TVにカラー出力など試していた
が、命令プリフェッチの関係かクロック数がどう数えても合わないので力尽きた。
より困難な(プリフェッチ幅が広く、I/O命令が遅い)LPC1114でNTSCカラー出力を実現したPancake(IchigoJam周辺機器)はすごいと思う。尊敬する。
2015年10月05日 01:09
数年前からマイコンでSDカードを読みたいと思い続けていたが、やっと実行に移した。(そしてそれをブログに書くまでに1ヶ月…)
使うマイコンはもちろん慣れ親しんだPIC。中でも最近一番のお気に入りの10F200…といきたかったのだが多少無理があったので12F510に。
主に問題なのはI/Oピン数。SDの操作に4ピン使うので、10F200では他にピンが余らない。またFlash容量も心許ない。読み取りだけならなんとかなるにしてもデバッグ用に出力などする余裕が全くないだろう。
回路はこちら。単純につないだだけ。
現在、第0セクタ512バイトを読んだところである。
次はファイルシステムを解釈してファイルの読み書きをしようと思っているが、その前に今回得た知見をブログにまとめておくことにする。
まず参考資料。
1. https://www.sdcard.org/downloads/pls/simplified_specs/
数年前からSDカードの仕様は公開されている。完全な仕様書ではなく「Simplified」ではあるものの、れっきとした公式資料。
これが公開される前はマルチメディアカード(MMC)としてのコマンドで扱うしかなかった。SPIモード部分についてはMMCとSDは互換性があるのでそれでも扱えるのだが、4ピンを使う高速なSDカード本来のプロトコルは使えない。
この仕様書の公開によってついにSDカード本来のプロトコルが使えるように…なったかと思いきや実は足りない部分があってMMCの仕様書を見て埋めないといけないとも聞く。
まあとりあえず今のところSPIモードしか使うつもりはないので困らない。
2. MMC/SDCの使いかた
ChaNさんのサイト。日本語のサイトの中ではもっとも詳しく説明されていると思う。何度見たか分からない。
この2つの資料で分からない事はそのたびに適当に検索して調べていたのでどのサイトを見たかあまり覚えていない。
思い出せるところでこのへんか。
http://bluefish.orz.hm/sdoc/psoc_mmc.html
http://bitcraft.web.fc2.com/embedded/microchip/microchip.html
次にSDから1セクタ読むまでの手順…の前に色々解説。
SDのSPIモードでの操作は、(当たり前ではあるが)すべてSPIのプロトコルに則った通信で行われる。つまりすべての通信は1バイト単位で行われる。
そしてすべての通信は機器側がマスター、SDカードがスレーブで行われる。つまりSDカードが自発的にデータを送ってくることはなく、常にマスターがクロックを送ることでデータを読みだすことになる。
なおSPIは全二重の通信ができる規格だが、SDカードの操作では大抵どちらかがデータを出すときはもう一方はデータを出さない(0xFFを出す)。両方がデータを出しているのはデータパケットの受信を止める時くらいか。
SDモードでは1本のデータ線で半二重通信をするのでそれと合わせたのかもしれない。あるいはそもそも必要ないしそっちの方が楽ということかもしれない。
SPIモードに入ってからの通信はすべてコマンドを送信して返答を読み取るトランザクション単位で行われる。
そしてトランザクションはパケット単位で行われる。1つのトランザクションの流れは次のようになる。
・コマンド送信
・レスポンス受信
・(あれば)データ送信or受信
なおコマンド送信後すぐにレスポンスが読み取れるとは限らない。0xFFが読めた場合それを読み飛ばす必要がある。
1つのトランザクションが終わった後には1バイト以上のクロックを送信しなければならないという奇妙な仕様がある。
マスターはクロックをいつでも中断できる(ACMD41の最中を除く)。
コマンドは、6バイト固定で次のような構造をしている。
・1バイトのコマンド番号
うち頭2bitはスタートビットと、通信方向を示すTransmission Bit
・4バイトの引数
・1バイトのCRC
うち末尾1bitはEnd Bit(って書いてあるけどストップビットって名前のほうが馴染みがある)
1バイト単位なのにスタート・ストップビットがあるという奇妙な構造だが、1bit単位で通信を行うSDモードと共通化したためであろう。通信方向を示すのもSPIモードでは送受信に別の線が割り当てられているしCSもあるのだから不要である。
CRCはSPIモードでは基本的にはチェックされないため適当な値を入れればよいが、そうでないところもあり要注意だ。
レスポンスには以下の種類がある。
・R1: 1バイトで、単純な状態を返す。
・R1b: R1の後にビジー応答(連続したLowレベル)を返す。
・R2: 2バイトで、R1に加え詳細なエラー状態を返す。
・R3/R7: R1に加え4バイトの情報を返す。
・R4/R5: SDIOのための予約らしい。
・R6: どうやらSDモード専用のようだ。
データパケットは、任意バイト数のデータを1バイトのヘッダと2バイトのCRCで挟んだ構造である。
コマンドと異なりデータパケットのヘッダは先頭bitが0ではない。こちらはSPIモード専用なので1バイト単位で読む前提のようだ。
改めて、SDから1セクタ読むまでの手順。
・コマンド送信前の儀式
まずSDカードの電源を投入してから1ms待ち、74個以上のクロックを送ることでコマンドを受け付ける準備が完了する。
74とは中途半端だが、この時点ではSDモードなのでクロック数が8の倍数である必要がないためだ。もっともSPIで制御するなら切りよく80クロック送るのが楽だろう。
これ以下はSPIモードと考えて良い。
・CMD0 リセット
このコマンドは本来の機能の他にSPIモードへの切り替え機能を持っており、CSをLowにした状態で送ることでSDカードをSPIモードにすることができる。このコマンドを送信する時点ではSPIモードではないのだが、コマンド送信についてはSDモードとSPIモードでほぼ同じ形式なのでSPIモードのつもりで問題ない。
SDモードで使うときにはCSをHighでこのコマンドを送ることで、SPIモードのレスポンスとは違う何かが起こる(よく知らない)。
引数は任意と書かれているところと0固定と書かれているところがあってよく分からない。たぶん任意だと思うが、まああえて0以外を送る理由もない。
SPIモードでないためCRC必須。引数が0の場合CRCは0x95である(ストップビット含む)。
返答はidleフラグのみの立った「0x01」であるはずである。
・CMD8 インターフェースコンディション確認
引数は、0x000001XX。
XXは任意。0xAAが推奨されているが、私は0xABをおすすめする。これにすると、CRCの値をCMD0と同じにすることができる。
仕様書にはSPIモードでもCRC必須とある。
ネット上の情報では「CMD0のみCRC必須」と書かれているものがあるが、どうもCMD8はSDカードのver.2からできたコマンドのようで、それ以前はCMD0のみがCRC必須のコマンドだったせいで混乱しているように見える。
このコマンドは対応している電圧範囲を取得するコマンドであり、レスポンスはR7で引数に指定された電圧範囲に対応しているかと、上で「任意」と書いたチェックパターンがそのまま返ってくる。
ただ、これは全電圧範囲に対応しているか否かを調べるだけで、普通は対応しているし、対応していなかったらどうしようもないので、調べる意味はあまりない。
が、これを送ることでSDHC/SDXCはCMD58とCMD41を有効化するということなので、送信は必須のようだ。
なおこれはver2以降のカードの場合であり、それより前のカードでは不正コマンドとなり、返答はR1で「0x05」となる。つまりver2以降か否かを調べるという効果もある。
1バイトしか返答がないところ5バイト読むと、残りはデータ線がHighのままなので0xFFが読める(害はない)。
・CMD1またはACMD41 初期化
初期化コマンドは仕様書を読む限り、「CMD1を使うとMMCとSDの区別がつかないので非推奨」と読めるのだが、手元のいくつかのカードで試してみたところ、CMD1を受け付けないカードがいくつかあった。ACMD41を使わなければならないのだろうか。しかしそれではSDのSimplified仕様書が公開される前にMMCとして扱えていたという情報と矛盾するように思う。
なんにせよ今回使ったSDはCMD1に反応したので今回のコードではCMD1のままである。
CMD1は引数任意でレスポンスはR1。初期化が完了するまではidleを示す0x01、完了したらエラー無しの0x00が返る。
ACMD41の引数はホストのSDHC/XCサポートの有無を示すHCSの1bitのみ(他のbitは予約)、応答はR3でOCRレジスタの内容が返る。
idleでなくなるまでCMD1またはACMD41を送り続ける。
・CMD58 OCRレジスタ読み取り
レスポンスはR3で、32bitのOperation Conditions Registerの値を読み取る。内容は対応している電圧範囲、SDHC/SDXCか否かを示すbit、ビジーフラグ。電圧範囲はCMD8でも確認してこちらでも確認するのだろうか。よく分からない仕様だ。
なお今書いていて気づいたのだが、このコマンドはSDカードでは予約扱いになっており、OCR読み取りにはACMD41を使う前提のようだ。しかし使ったカードではきちんとレスポンスを返してくれた。よく分からない。
・CMD9 CSDレジスタ読み取り
Card-Specific Dataレジスタの値を読み取る。カードの容量など様々な情報が含まれている。
このレジスタはOCRレジスタと異なり16バイトあるためレスポンスの中身ではなくデータパケットとして送られてくる。
なお今回のコードではこの内容は読むだけで使っていない。
・CMD16 ブロックサイズ指定
SDHC/SDXCではこのコマンドは無視され512バイト固定だが、SDの一部でデフォルトが512バイトでない場合がありうるので必要らしい。
面倒なので今回のコードでは省略した。
・CMD17 1ブロック読み取り
このコマンドでカード本体の内容を読める。
引数は読み取り開始アドレス。
無印SDカードは1バイト単位、SDHCおよびSDXCはセクタ(512バイト)単位で指定する。
1ブロック分のデータパケットが得られる。
以下自分の悩んだ点をまとめておく
・CSの操作
1回のトランザクションごとにCSをHighに戻す。
つまり、CSをLowにアサート、コマンド送信、レスポンス受信、(データパケットがあれば送受信、)CSをHighに。
CSの動作として当たり前なのだろうが、これが確信が持てなくてだいぶ悩んだ。
・ダミークロック
トランザクションごとに1バイト分のダミークロックが必要。
ダミークロックについては仕様書の(SPIモードでなく)SDモードの部分に書いてある。
CSはHighでもLowでも構わないと書いてあるサイトがあった。とりあえずHighにしている。
・ACMDの処理
CMD55,CMDnのシークエンスをACMDnという。
それはいいのだが、CMD55と次のコマンドの間の処理がよくわからなかった。
CMD55を送って返答の1バイトが返ってくるまでで1トランザクション、ここでダミークロックまで送り、次のコマンド本体とその返答がまた1トランザクションということのようだ。
・クロック周波数
初期化中のクロック周波数は100kHz~400kHzと書かれているが、仕様書を読むともっと詳しく書かれている。
ACMD41を発行しカードの初期化が完了するまで、ホストは次の2つのうちどちらかを行わなければならない。
1) 周波数100kHz~400kHzの連続的なクロックを出力する
2) ホストがクロックを停止したいならば、50ms以内の間隔でACMD41によりビジービットをポーリングする
マイコンでSPIモードを使うならふつうクロックは断続的になるだろうから見るべきは2番の方である。
ところで50ms以内の間隔ならクロックは100kHzを下回ってもよいのだろうか。
よいならば最低クロックはACMD41とレスポンスで50msになる値、レスポンスが即時に返ってくるとして6+1+6+5=18バイト分なので、(18*8)/0.05=2880Hzということに…
なるかと思いきや、よく図を見るとトランザクション中の時間は50msに含まれていない。
するとクロックが50ms以上止まらなければよいのだろうか。であれば1/(50ms*2)=10Hz…?
これはちょっと実験してみたいところだ。
なお、ACMD41を除いてクロックは好きな時に停止してよいようだ。これは助かる。
・CRC
分からない。
色々な所を見たが、見る所ごとに書いてあることが違う(ように感じる)。
結局、ここのコードを中身を理解しないままJavaScriptに移植して使ったところ、思ったのと1bit違うところで答えが出たのでそのまま使った。
http://nabe.blog.abk.nu/0355
・自分のプログラムのバグ
他人の役に立つとは思えないが、面白いバグだったので書き留めておく。
起こった現象
初期化シーケンスに対する返答が順に以下のようであった。
CMD0に0x01が返る。idleフラグのみ立った状態であり、これは正しい。
CMD8に0x00FFFFFFFFが返る。つまり1バイト目はエラー無しを示しているのにその後に続くはずのデータが来ない。古いSDでCMD8に非対応だからだろうか。
CMD1に常に1回で0x00が返る。idle待ちがあるはずだが…まあきっと通信が遅いせいだろう。たぶんおかしくない。
CMD58に0x00FFFFFFFFが返る。つまりCMD8と同様エラー無しなのにデータが無い。
別のカードを使ってみると違う結果が返るものがあり、
CMD8に0x00000001AAが返るものがある。idleでないのが不可解だがデータは正しい。
CMD58に0x0080FF8000や0x0000FF80が返るものがある。これは正しい。
さて困った。正しいデータが返るものもあるのだから、SPIやシリアル通信のプログラムが間違っているとも思えない。
CMD58に返答が返るものを使えばとりあえず先に進めるのだからよく分からないがこれを使っておくか…
…と思ったが、色々考えてみるとやはりCMD8に0x00が返ってくるのはおかしい。非対応ならエラーフラグの立った0x05が返るはずである。
CMD1のidle待ちが無いのも不可解だ。いくら遅いとはいえ1ループ数ミリ秒で回しているのだが、このidle待ちは「数百msかかることがある」というもので、早くても数十msはかかるのではないか。
つまり、頭の1バイトだけなぜか0x00に化けているのではないか。しかしそんな都合のいいバグが…あった。
どういうバグかというと、
ダミークロックを送る関数でバンク0に用意したループカウンタを回すつもりだがバンクを変えておらず、
運良くシリアル送信との兼ね合いでバンク0にしていた1回目のCMD0を除き、みなバンク1の変数を回していた。
そしてそこにちょうど読んだデータの1バイト目があったため、カウントダウンしてループを抜けるときには常に0になっていたというわけだ。
ミッドレンジのPICならバンク切り替えの必要は無かったので、もっと余裕をもった石を使うべきだったかもしれない。(もっとも10F200でなく12F510にした時点で余裕をもったつもりだったのだが)
最後にコード
ページ0/1を両方使っているが、ページ1はメッセージだけなのでそれを除けば512バイトのPICにも収めることができる。まあそんなPICはほとんど無いのでできてどうするという話ではあるが。
・コード内訳
000-1FF メインコード・サブルーチン
200-2FF メッセージ
300-3FF 未使用
なおベースラインPICの仕様上ページの前半にしかCALLで飛べないため、サブルーチンの本体は後半に置きつつ前半に一度CALLしてからGOTOで本体に飛ばす構造になっている。
list p=12F510
#include p12F510.inc
radix dec
; 12F510
; Vdd+-v-+Vss
; GP5| |GP0 DAT
; GP4| |GP1 CLK
; (GP3)+---+GP2
;- /--
;CS |
;DI |
;Vss |
;Vcc | 表
;SCL |
;Vss |
;CO |
;- +---
variable env
env= _IOSCFS_ON
env&= _MCLRE_OFF
env&= _CP_OFF
env&= _WDT_OFF
env&= _IntRC_OSC
__config env
cblock 0x0A ;バンク共通
bitcnt, bytecnt, skipcnt
commdat, spioutbuf
;バンク0
flag
cnt0, cnt1, cnt2, cnt3
temp, temp2
arg0, arg1, arg2, arg3
msgptr
endc
;バンク1は読み取ったデータの保存領域
;ram alias
;bytecnt equ bytecnt
;flag bit
RXFLAG equ 0
TIMEOUT equ 1
MOSIPIN equ 0
MISOPIN equ 3
SCLKPIN equ 1
CSPIN equ 2
RXPIN equ 4
TXPIN equ 5
MORSEPIN equ 5
MOSIBIT equ 1<<MOSIPIN
MISOBIT equ 1<<MISOPIN
SCLKBIT equ 1<<SCLKPIN
CSBIT equ 1<<CSPIN
RXBIT equ 1<<RXPIN
TXBIT equ 1<<TXPIN
CMD equ 0x40
CRC_CMD08 equ 0x95
;CMD0: 40 00 00 00 00 と
;CMD8: 48 00 00 01 AB で兼用
#define flagWAIT STATUS,GPWUF
#define flagCMD8 STATUS,CWUF
;##### EntryPoint #####
org 0x00
goto init
;##### Subroutine Entry #####
;関数のcall先。ベースラインではページの前半しかcallできないためここにまとめてある。
;関数の本体はメインコードの後に置いてある。
txmsg232:
goto txmsg232core
getmsgbyte: ;これのみページ1へ飛ばす
bsf STATUS,PA0
movwf PCL
skipandread1: ;Wに1を入れてskipandreadn。すなわち1バイト読む
movlw .1
skipandreadn: ;0xFFのバイトを読み飛ばした後にNバイト読む
bsf flagWAIT
readn: ;Nバイト読む
goto readncore
skip2np1:
goto skip2np1core
skip2n:
goto skip2ncore
tx232hex:
goto tx232hexcore
;readresponse1:
; bsf respcnt,0
;readresponse:
; goto readresponsecore
sendnibble:
goto sendnibblecore
sendsdcmdarg0:
goto sendsdcmdarg0core
sendsdcmd:
goto sendsdcmdcore
;morse8:
; movwf dat
; goto f1
;morse:
; goto morsecore
;morsespace:
; ;interword(7)-interchar(3) =4
; movlw .8
; goto waithalfdit
readspi: ;SPIで0xFFを送信する
movlw 0xFF
spi:
goto spicore
tx232:
bsf GPIO,TXPIN ;0
bcf flag,RXFLAG
movwf commdat
goto tx232core
rx232:
btfss GPIO,RXPIN ;[0,3)
goto rx232
goto rx232core
rx232withtimeout:
bcf flag,TIMEOUT
rx232waitloop:
btfsc GPIO,RXPIN ;[0,5)
goto rx232core
incfsz cnt0,F
goto rx232waitloop
incf cnt1,F ;待ち時間を正しくカウントするために必要
btfsc GPIO,RXPIN
goto rx232core
incfsz cnt1,F
goto rx232waitloop
incf cnt2,F ;
btfsc GPIO,RXPIN
goto rx232core
incfsz cnt2,F
goto rx232waitloop
bsf flag,TIMEOUT
retlw 0
;##### Main Code #####
init
movwf OSCCAL
movlw b'10001000'
; /GPWU
; /GPPU=0: PullUp ON
; T0CS=0: Fosc/4,
; T0SE Don'tCare
; PSA Don'tCare
; PS2:0 Don'tCare
option
clrf ADCON0
bcf CM1CON0,C1ON
movlw MOSIBIT|CSBIT
movwf GPIO
movlw MISOBIT|RXBIT
tris GPIO
movlw 0x2A
movwf FSR
b12:
clrf INDF
bcf FSR,5
clrf INDF
bsf FSR,5
incfsz FSR,F
goto b12
initsd: ;1ms待ってから74個以上のクロックを出力
;wait 1ms
clrf cnt0
b9:
goto $+1
goto $+1
nop
decfsz cnt0,F
goto b9
movlw .5 ;10byte>74
call skip2np1
test:
;メッセージ出力
movlw msg_cmd0
call txmsg232
bcf GPIO,CSPIN
;CMD0を引数0で送信
movlw CMD|.0
call sendsdcmdarg0
;受信しつつ応答が来るまでスキップし、応答を1バイト読む
call skipandread1
;応答が来るまでにスキップした回数と受信内容を出力
decf skipcnt,W
call tx232hex
movlw ','
call tx232
movlw 0x30
movwf FSR
movf INDF,W
bcf FSR,5
call tx232hex
;CSをHighに
bsf GPIO,CSPIN
;1バイトダミークロック出力
movlw .0
call skip2np1
;CMD8で同様
movlw msg_cmd8
call txmsg232
bcf GPIO,CSPIN
movlw 0x01
movwf arg2
movlw 0xAB
movwf arg3
movlw CMD|.8
call sendsdcmd
;CMD8の応答は5バイト
movlw .5
call skipandreadn
bsf GPIO,CSPIN
movlw .0
call skip2np1
decf skipcnt,W
call tx232hex
movlw ','
call tx232
;5バイト出力。もうちょっとまともにやりたい。
movlw 0x30
movwf FSR
movf INDF,W
bcf FSR,5
call tx232hex
incf FSR,F
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
incf FSR,F
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
incf FSR,F
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
incf FSR,F
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
;CMD1。ACMD41を試してた名残りがある。
movlw msg_cmd1
call txmsg232
b13:
; movlw CMD|.55
; call sendsdcmdarg0
;
; call skipandread1
; bsf GPIO,CSPIN
; movlw 0x30
; movwf FSR
; movf INDF,W
; bcf FSR,5
; call tx232hex
bcf GPIO,CSPIN
movlw CMD|.1
call sendsdcmdarg0
call skipandread1
bsf GPIO,CSPIN
movlw .0
call skip2np1
movlw '/'
call tx232
decf skipcnt,W
call tx232hex
movlw ','
call tx232
movlw 0x30
movwf FSR
movf INDF,W
bcf FSR,5
movwf temp2
call tx232hex
movf temp2,W
btfss STATUS,Z
goto b13
;CMD58
movlw msg_cmd58
call txmsg232
bcf GPIO,CSPIN
movlw CMD|.58
call sendsdcmdarg0
movlw .5
call skipandreadn
bsf GPIO,CSPIN
movlw .0
call skip2np1
decf skipcnt,W
call tx232hex
movlw ','
call tx232
movlw 0x30
movwf FSR
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
incf FSR,F
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
incf FSR,F
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
incf FSR,F
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
incf FSR,F
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
;CMD9
movlw msg_cmd9
call txmsg232
bcf GPIO,CSPIN
movlw CMD|.9
call sendsdcmdarg0
call skipandread1
decf skipcnt,W
call tx232hex
movlw ','
call tx232
movlw 0x30
movwf FSR
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
movlw .16
call skipandreadn
decf skipcnt,W
call tx232hex
movlw ','
call tx232
movlw 0x30
b18:
movwf FSR
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
incf FSR,F
movf FSR,W
andlw 0x1F
btfss STATUS,Z
goto b18
movlw .3
call readn
movlw 0x30
movwf FSR
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
incf FSR,F
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
incf FSR,F
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
bsf GPIO,CSPIN
movlw .0
call skip2np1
;---
;CMD17
movlw msg_cmd17
call txmsg232
bcf GPIO,CSPIN
movlw CMD|.17
call sendsdcmdarg0
call skipandread1
decf skipcnt,W
call tx232hex
movlw ','
call tx232
movlw 0x30
movwf FSR
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
movlw '/'
call tx232
call skipandread1
decf skipcnt,W
call tx232hex
movlw ','
call tx232
movlw 0x30
movwf FSR
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
;512バイトを16バイトごとに受信して表示
movlw .512/.16
movwf cnt0
b17:
movlw msg_nl
call txmsg232
movlw .16
call skipandreadn
movlw 0x30
b16:
movwf FSR
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
incf FSR,F
movf FSR,W
andlw 0x1F
btfss STATUS,Z
goto b16
bcf FSR,5
decfsz cnt0,F
goto b17
goto $
busychk: ;あれ、なんでこんなところに置いてあるんだろう。ページ前半でないとcallできない。
goto busychkcore
;##### Subroutine Core #####
busychkcore: ;ビジーチェック…のはずだけどどうやら使っていない。
call readspi
movf commdat,W
btfsc STATUS,Z
goto busychkcore
retlw 0
readncore: ;Nバイト読み取り
movwf bytecnt
movlw 0x30
movwf FSR
clrf skipcnt
readloop:
call readspi
btfss flagWAIT
goto f2
incf skipcnt,F
incf commdat,W
btfsc STATUS,Z
goto readloop
bcf flagWAIT
f2:
movf commdat,W
movwf INDF
incf FSR,F
decfsz bytecnt,F
goto readloop
bcf FSR,5
retlw 0
sendsdcmdarg0core: ;SDのコマンドを引数を0にして送信
clrf arg0
clrf arg1
clrf arg2
clrf arg3
sendsdcmdcore: ;SDのコマンドを送信
call spi
movf arg0,W
call spi
movf arg1,W
call spi
movf arg2,W
call spi
movf arg3,W
call spi
movlw CRC_CMD08
call spi
retlw 0
skip2np1core: ;2N+1バイト分のダミークロックを出力
bsf bitcnt,3
b14:
bsf GPIO,SCLKPIN
goto $+1
bcf GPIO,SCLKPIN
decfsz bitcnt,F
goto b14
skip2ncore: ;2Nバイト分のダミークロックを出力
movwf bytecnt
movf bytecnt,W
btfsc STATUS,Z
retlw 0
b11:
bsf bitcnt,3
b10:
bsf GPIO,SCLKPIN
goto $+1
bcf GPIO,SCLKPIN
goto $+1
bsf GPIO,SCLKPIN
goto $+1
bcf GPIO,SCLKPIN
decfsz bitcnt,F
goto b10
decfsz bytecnt,F
goto b11
retlw 0
spicore: ;SPI 1バイト送受信
movwf commdat
rrf GPIO,W
andlw ~(0x80|SCLKBIT>>1|CSBIT>>1|MISOBIT>>1)
movwf spioutbuf
; bcf GPIO,CSPIN
bcf STATUS,C
bsf bitcnt,3
spiloop:
rlf commdat,F
rlf spioutbuf,W ;CS:L SCLK:L MOSI<-C / c0
movwf GPIO
goto $+1
bsf GPIO,SCLKPIN
btfsc GPIO,MISOPIN
bsf commdat,0
decfsz bitcnt,F
goto spiloop
bcf GPIO,SCLKPIN
retlw 0
;### RS232C
;RS232Cシリアル関連のコード。
txmsg232core: ;文字列を送信
movwf msgptr
msgloop:
call getmsgbyte
bcf STATUS,PA0
movwf temp
andlw 0x7F
call tx232
btfsc temp,7
retlw 0
incf msgptr,F
movf msgptr,W
goto msgloop
retlw 0
tx232hexcore: ;1バイトを16進表記で送信
movwf temp
swapf temp,W
call sendnibble
movf temp,W
call sendnibble
retlw 0
sendnibblecore: ;下位ニブルを送信
andlw 0x0F
movwf commdat
movlw .6
addwf commdat,F
movlw '0'-.6
btfsc STATUS,DC
movlw 'A'-.10-.6
bsf GPIO,TXPIN ;0
bcf flag,RXFLAG
addwf commdat,F
goto tx232core
rx232core: ;1バイト受信。使っていない。
bsf flag,RXFLAG ;[4,7) mean5.5
movlw 0xFF
movwf commdat
goto $+1
; btfsc flag,BAUD300
; goto wait0r5a
;waitend0r5a:
goto $+1
goto $+1
goto $+1
goto $+1
goto $+1
;tx232:
; bsf GPIO,TXPIN ;0
; bcf flag,RXFLAG
; movwf commdat
; goto tx232core
tx232core: ;1バイト送信
nop ;5
bsf bitcnt,3
loop232:
goto $+1
; btfsc flag,BAUD300 ;ボーレートを115200と300で選べるようにしようと思ったが結局115200固定に。
; goto wait1b
;waitend1b:
goto $+1
nop
bcf STATUS,C
btfss GPIO,RXPIN ;read 28.5 +17n
bsf STATUS,C
rrf commdat,F
movf GPIO,W
andlw ~TXBIT
btfss STATUS,C
iorlw TXBIT
movwf GPIO ;send 20 +17n
decfsz bitcnt,F
goto loop232
; btfsc flag,BAUD300
; goto wait1c
;waitend1c:
;連続受信を極力早くするためここでreturn(2命令挟んで次のcallでストップビット中央)
btfsc flag,RXFLAG
retlw 0
goto $+1
goto $+1
goto $+1
goto $+1
goto $+1
goto $+1
bcf GPIO,TXPIN ;send stop +17
goto $+1
; btfsc flag,BAUD300
; goto wait1d
;waitend1d:
goto $+1
goto $+1
goto $+1
goto $+1
nop
;連続送信で安全な時間空ける(1命令挟んで次のcallでストップビットが17サイクル)
retlw 0
;### morse
;デバッグ出力用にモールス信号を出力するコードを書いたが結局使わなかった。
;morsecore:
; movwf dat
;b3:
; decf cnt0,F
; rlf dat,F
; btfss STATUS,C
; goto b3
;f1:
; movlw .8
; addwf cnt0,F
;
;b4:
; ;bcf GPIO,OUTPIN
; rlf dat,F
; movlw .6 ;dah
; btfss STATUS,C
; movlw .2 ;dit
; movwf cnt2
;
;b5: ;2MHz,4*256cyc: ~1kHz
; movlw 1<<MORSEPIN
; decfsz cnt3,F
; goto b5
; xorwf GPIO,F
; decfsz cnt1,F
; goto b5
; decfsz cnt2,F
; goto b5
;
;;bsf GPIO,OUTPIN
;
;;1点間隔
; movlw .2
; movwf cnt3
;b7:
; ;4cyc,2MHz: .13sec
; nop
; decfsz cnt1,F
; goto b7
; decfsz cnt2,F
; goto b7
; decfsz cnt3,F
; goto b7
;
; decfsz cnt0,F
; goto b4
;
;;interchar(3)-interelem(1) =2
; movlw .4
;; goto waithalfdit
;
;waithalfdit:
; movwf cnt3
;b6:
; ;4cyc,2MHz: .13sec
; nop
; decfsz cnt0,F
; goto b6
; decfsz cnt2,F
; goto b6
; decfsz cnt3,F
; goto b6
; retlw 0
;メッセージ。最上位bitを立てることで終端を示している。ASCIIしか使えないが、\0終端と比べ1メッセージあたり1バイト節約できるのが利点。
;GBのアセンブラにあった機能を真似してみた。
org 0x200
msg_cmd0:
dt "\r\nCMD0 reset&SPI mode\r", '\n'|0x80
msg_cmd8:
dt "\r\nCMD8 >ver.2?\r", '\n'|0x80
msg_cmd1:
dt "\r\nCMD1 initialize\r", '\n'|0x80
msg_cmd58:
dt "\r\nCMD58 read OCR register\r", '\n'|0x80
msg_cmd9:
dt "\r\nCMD9 read CSD register\r", '\n'|0x80
msg_cmd17:
dt "\r\nCMD17 read single block\r", '\n'|0x80
msg_nl:
dt "\r", '\n'|0x80
end
使うマイコンはもちろん慣れ親しんだPIC。中でも最近一番のお気に入りの10F200…といきたかったのだが多少無理があったので12F510に。
10F200 | 12F510 | |
Flash | 256ワード | 1024ワード |
RAM | 16バイト | 38バイト |
I/Oピン | 4本 | 6本 |
回路はこちら。単純につないだだけ。
現在、第0セクタ512バイトを読んだところである。
次はファイルシステムを解釈してファイルの読み書きをしようと思っているが、その前に今回得た知見をブログにまとめておくことにする。
まず参考資料。
1. https://www.sdcard.org/downloads/pls/simplified_specs/
数年前からSDカードの仕様は公開されている。完全な仕様書ではなく「Simplified」ではあるものの、れっきとした公式資料。
これが公開される前はマルチメディアカード(MMC)としてのコマンドで扱うしかなかった。SPIモード部分についてはMMCとSDは互換性があるのでそれでも扱えるのだが、4ピンを使う高速なSDカード本来のプロトコルは使えない。
この仕様書の公開によってついにSDカード本来のプロトコルが使えるように…なったかと思いきや実は足りない部分があってMMCの仕様書を見て埋めないといけないとも聞く。
まあとりあえず今のところSPIモードしか使うつもりはないので困らない。
2. MMC/SDCの使いかた
ChaNさんのサイト。日本語のサイトの中ではもっとも詳しく説明されていると思う。何度見たか分からない。
この2つの資料で分からない事はそのたびに適当に検索して調べていたのでどのサイトを見たかあまり覚えていない。
思い出せるところでこのへんか。
http://bluefish.orz.hm/sdoc/psoc_mmc.html
http://bitcraft.web.fc2.com/embedded/microchip/microchip.html
次にSDから1セクタ読むまでの手順…の前に色々解説。
SDのSPIモードでの操作は、(当たり前ではあるが)すべてSPIのプロトコルに則った通信で行われる。つまりすべての通信は1バイト単位で行われる。
そしてすべての通信は機器側がマスター、SDカードがスレーブで行われる。つまりSDカードが自発的にデータを送ってくることはなく、常にマスターがクロックを送ることでデータを読みだすことになる。
なおSPIは全二重の通信ができる規格だが、SDカードの操作では大抵どちらかがデータを出すときはもう一方はデータを出さない(0xFFを出す)。両方がデータを出しているのはデータパケットの受信を止める時くらいか。
SDモードでは1本のデータ線で半二重通信をするのでそれと合わせたのかもしれない。あるいはそもそも必要ないしそっちの方が楽ということかもしれない。
SPIモードに入ってからの通信はすべてコマンドを送信して返答を読み取るトランザクション単位で行われる。
そしてトランザクションはパケット単位で行われる。1つのトランザクションの流れは次のようになる。
・コマンド送信
・レスポンス受信
・(あれば)データ送信or受信
なおコマンド送信後すぐにレスポンスが読み取れるとは限らない。0xFFが読めた場合それを読み飛ばす必要がある。
1つのトランザクションが終わった後には1バイト以上のクロックを送信しなければならないという奇妙な仕様がある。
マスターはクロックをいつでも中断できる(ACMD41の最中を除く)。
コマンドは、6バイト固定で次のような構造をしている。
・1バイトのコマンド番号
うち頭2bitはスタートビットと、通信方向を示すTransmission Bit
・4バイトの引数
・1バイトのCRC
うち末尾1bitはEnd Bit(って書いてあるけどストップビットって名前のほうが馴染みがある)
1バイト単位なのにスタート・ストップビットがあるという奇妙な構造だが、1bit単位で通信を行うSDモードと共通化したためであろう。通信方向を示すのもSPIモードでは送受信に別の線が割り当てられているしCSもあるのだから不要である。
CRCはSPIモードでは基本的にはチェックされないため適当な値を入れればよいが、そうでないところもあり要注意だ。
レスポンスには以下の種類がある。
・R1: 1バイトで、単純な状態を返す。
・R1b: R1の後にビジー応答(連続したLowレベル)を返す。
・R2: 2バイトで、R1に加え詳細なエラー状態を返す。
・R3/R7: R1に加え4バイトの情報を返す。
・R4/R5: SDIOのための予約らしい。
・R6: どうやらSDモード専用のようだ。
データパケットは、任意バイト数のデータを1バイトのヘッダと2バイトのCRCで挟んだ構造である。
コマンドと異なりデータパケットのヘッダは先頭bitが0ではない。こちらはSPIモード専用なので1バイト単位で読む前提のようだ。
改めて、SDから1セクタ読むまでの手順。
・コマンド送信前の儀式
まずSDカードの電源を投入してから1ms待ち、74個以上のクロックを送ることでコマンドを受け付ける準備が完了する。
74とは中途半端だが、この時点ではSDモードなのでクロック数が8の倍数である必要がないためだ。もっともSPIで制御するなら切りよく80クロック送るのが楽だろう。
これ以下はSPIモードと考えて良い。
・CMD0 リセット
このコマンドは本来の機能の他にSPIモードへの切り替え機能を持っており、CSをLowにした状態で送ることでSDカードをSPIモードにすることができる。このコマンドを送信する時点ではSPIモードではないのだが、コマンド送信についてはSDモードとSPIモードでほぼ同じ形式なのでSPIモードのつもりで問題ない。
SDモードで使うときにはCSをHighでこのコマンドを送ることで、SPIモードのレスポンスとは違う何かが起こる(よく知らない)。
引数は任意と書かれているところと0固定と書かれているところがあってよく分からない。たぶん任意だと思うが、まああえて0以外を送る理由もない。
SPIモードでないためCRC必須。引数が0の場合CRCは0x95である(ストップビット含む)。
返答はidleフラグのみの立った「0x01」であるはずである。
・CMD8 インターフェースコンディション確認
引数は、0x000001XX。
XXは任意。0xAAが推奨されているが、私は0xABをおすすめする。これにすると、CRCの値をCMD0と同じにすることができる。
仕様書にはSPIモードでもCRC必須とある。
ネット上の情報では「CMD0のみCRC必須」と書かれているものがあるが、どうもCMD8はSDカードのver.2からできたコマンドのようで、それ以前はCMD0のみがCRC必須のコマンドだったせいで混乱しているように見える。
このコマンドは対応している電圧範囲を取得するコマンドであり、レスポンスはR7で引数に指定された電圧範囲に対応しているかと、上で「任意」と書いたチェックパターンがそのまま返ってくる。
ただ、これは全電圧範囲に対応しているか否かを調べるだけで、普通は対応しているし、対応していなかったらどうしようもないので、調べる意味はあまりない。
が、これを送ることでSDHC/SDXCはCMD58とCMD41を有効化するということなので、送信は必須のようだ。
なおこれはver2以降のカードの場合であり、それより前のカードでは不正コマンドとなり、返答はR1で「0x05」となる。つまりver2以降か否かを調べるという効果もある。
1バイトしか返答がないところ5バイト読むと、残りはデータ線がHighのままなので0xFFが読める(害はない)。
・CMD1またはACMD41 初期化
初期化コマンドは仕様書を読む限り、「CMD1を使うとMMCとSDの区別がつかないので非推奨」と読めるのだが、手元のいくつかのカードで試してみたところ、CMD1を受け付けないカードがいくつかあった。ACMD41を使わなければならないのだろうか。しかしそれではSDのSimplified仕様書が公開される前にMMCとして扱えていたという情報と矛盾するように思う。
なんにせよ今回使ったSDはCMD1に反応したので今回のコードではCMD1のままである。
CMD1は引数任意でレスポンスはR1。初期化が完了するまではidleを示す0x01、完了したらエラー無しの0x00が返る。
ACMD41の引数はホストのSDHC/XCサポートの有無を示すHCSの1bitのみ(他のbitは予約)、応答はR3でOCRレジスタの内容が返る。
idleでなくなるまでCMD1またはACMD41を送り続ける。
・CMD58 OCRレジスタ読み取り
レスポンスはR3で、32bitのOperation Conditions Registerの値を読み取る。内容は対応している電圧範囲、SDHC/SDXCか否かを示すbit、ビジーフラグ。電圧範囲はCMD8でも確認してこちらでも確認するのだろうか。よく分からない仕様だ。
なお今書いていて気づいたのだが、このコマンドはSDカードでは予約扱いになっており、OCR読み取りにはACMD41を使う前提のようだ。しかし使ったカードではきちんとレスポンスを返してくれた。よく分からない。
・CMD9 CSDレジスタ読み取り
Card-Specific Dataレジスタの値を読み取る。カードの容量など様々な情報が含まれている。
このレジスタはOCRレジスタと異なり16バイトあるためレスポンスの中身ではなくデータパケットとして送られてくる。
なお今回のコードではこの内容は読むだけで使っていない。
・CMD16 ブロックサイズ指定
SDHC/SDXCではこのコマンドは無視され512バイト固定だが、SDの一部でデフォルトが512バイトでない場合がありうるので必要らしい。
面倒なので今回のコードでは省略した。
・CMD17 1ブロック読み取り
このコマンドでカード本体の内容を読める。
引数は読み取り開始アドレス。
無印SDカードは1バイト単位、SDHCおよびSDXCはセクタ(512バイト)単位で指定する。
1ブロック分のデータパケットが得られる。
以下自分の悩んだ点をまとめておく
・CSの操作
1回のトランザクションごとにCSをHighに戻す。
つまり、CSをLowにアサート、コマンド送信、レスポンス受信、(データパケットがあれば送受信、)CSをHighに。
CSの動作として当たり前なのだろうが、これが確信が持てなくてだいぶ悩んだ。
・ダミークロック
トランザクションごとに1バイト分のダミークロックが必要。
ダミークロックについては仕様書の(SPIモードでなく)SDモードの部分に書いてある。
CSはHighでもLowでも構わないと書いてあるサイトがあった。とりあえずHighにしている。
・ACMDの処理
CMD55,CMDnのシークエンスをACMDnという。
それはいいのだが、CMD55と次のコマンドの間の処理がよくわからなかった。
CMD55を送って返答の1バイトが返ってくるまでで1トランザクション、ここでダミークロックまで送り、次のコマンド本体とその返答がまた1トランザクションということのようだ。
・クロック周波数
初期化中のクロック周波数は100kHz~400kHzと書かれているが、仕様書を読むともっと詳しく書かれている。
ACMD41を発行しカードの初期化が完了するまで、ホストは次の2つのうちどちらかを行わなければならない。
1) 周波数100kHz~400kHzの連続的なクロックを出力する
2) ホストがクロックを停止したいならば、50ms以内の間隔でACMD41によりビジービットをポーリングする
マイコンでSPIモードを使うならふつうクロックは断続的になるだろうから見るべきは2番の方である。
ところで50ms以内の間隔ならクロックは100kHzを下回ってもよいのだろうか。
よいならば最低クロックはACMD41とレスポンスで50msになる値、レスポンスが即時に返ってくるとして6+1+6+5=18バイト分なので、(18*8)/0.05=2880Hzということに…
なるかと思いきや、よく図を見るとトランザクション中の時間は50msに含まれていない。
するとクロックが50ms以上止まらなければよいのだろうか。であれば1/(50ms*2)=10Hz…?
これはちょっと実験してみたいところだ。
なお、ACMD41を除いてクロックは好きな時に停止してよいようだ。これは助かる。
・CRC
分からない。
色々な所を見たが、見る所ごとに書いてあることが違う(ように感じる)。
結局、ここのコードを中身を理解しないままJavaScriptに移植して使ったところ、思ったのと1bit違うところで答えが出たのでそのまま使った。
http://nabe.blog.abk.nu/0355
・自分のプログラムのバグ
他人の役に立つとは思えないが、面白いバグだったので書き留めておく。
起こった現象
初期化シーケンスに対する返答が順に以下のようであった。
CMD0に0x01が返る。idleフラグのみ立った状態であり、これは正しい。
CMD8に0x00FFFFFFFFが返る。つまり1バイト目はエラー無しを示しているのにその後に続くはずのデータが来ない。古いSDでCMD8に非対応だからだろうか。
CMD1に常に1回で0x00が返る。idle待ちがあるはずだが…まあきっと通信が遅いせいだろう。たぶんおかしくない。
CMD58に0x00FFFFFFFFが返る。つまりCMD8と同様エラー無しなのにデータが無い。
別のカードを使ってみると違う結果が返るものがあり、
CMD8に0x00000001AAが返るものがある。idleでないのが不可解だがデータは正しい。
CMD58に0x0080FF8000や0x0000FF80が返るものがある。これは正しい。
さて困った。正しいデータが返るものもあるのだから、SPIやシリアル通信のプログラムが間違っているとも思えない。
CMD58に返答が返るものを使えばとりあえず先に進めるのだからよく分からないがこれを使っておくか…
…と思ったが、色々考えてみるとやはりCMD8に0x00が返ってくるのはおかしい。非対応ならエラーフラグの立った0x05が返るはずである。
CMD1のidle待ちが無いのも不可解だ。いくら遅いとはいえ1ループ数ミリ秒で回しているのだが、このidle待ちは「数百msかかることがある」というもので、早くても数十msはかかるのではないか。
つまり、頭の1バイトだけなぜか0x00に化けているのではないか。しかしそんな都合のいいバグが…あった。
どういうバグかというと、
ダミークロックを送る関数でバンク0に用意したループカウンタを回すつもりだがバンクを変えておらず、
運良くシリアル送信との兼ね合いでバンク0にしていた1回目のCMD0を除き、みなバンク1の変数を回していた。
そしてそこにちょうど読んだデータの1バイト目があったため、カウントダウンしてループを抜けるときには常に0になっていたというわけだ。
ミッドレンジのPICならバンク切り替えの必要は無かったので、もっと余裕をもった石を使うべきだったかもしれない。(もっとも10F200でなく12F510にした時点で余裕をもったつもりだったのだが)
最後にコード
ページ0/1を両方使っているが、ページ1はメッセージだけなのでそれを除けば512バイトのPICにも収めることができる。まあそんなPICはほとんど無いのでできてどうするという話ではあるが。
・コード内訳
000-1FF メインコード・サブルーチン
200-2FF メッセージ
300-3FF 未使用
なおベースラインPICの仕様上ページの前半にしかCALLで飛べないため、サブルーチンの本体は後半に置きつつ前半に一度CALLしてからGOTOで本体に飛ばす構造になっている。
list p=12F510
#include p12F510.inc
radix dec
; 12F510
; Vdd+-v-+Vss
; GP5| |GP0 DAT
; GP4| |GP1 CLK
; (GP3)+---+GP2
;- /--
;CS |
;DI |
;Vss |
;Vcc | 表
;SCL |
;Vss |
;CO |
;- +---
variable env
env= _IOSCFS_ON
env&= _MCLRE_OFF
env&= _CP_OFF
env&= _WDT_OFF
env&= _IntRC_OSC
__config env
cblock 0x0A ;バンク共通
bitcnt, bytecnt, skipcnt
commdat, spioutbuf
;バンク0
flag
cnt0, cnt1, cnt2, cnt3
temp, temp2
arg0, arg1, arg2, arg3
msgptr
endc
;バンク1は読み取ったデータの保存領域
;ram alias
;bytecnt equ bytecnt
;flag bit
RXFLAG equ 0
TIMEOUT equ 1
MOSIPIN equ 0
MISOPIN equ 3
SCLKPIN equ 1
CSPIN equ 2
RXPIN equ 4
TXPIN equ 5
MORSEPIN equ 5
MOSIBIT equ 1<<MOSIPIN
MISOBIT equ 1<<MISOPIN
SCLKBIT equ 1<<SCLKPIN
CSBIT equ 1<<CSPIN
RXBIT equ 1<<RXPIN
TXBIT equ 1<<TXPIN
CMD equ 0x40
CRC_CMD08 equ 0x95
;CMD0: 40 00 00 00 00 と
;CMD8: 48 00 00 01 AB で兼用
#define flagWAIT STATUS,GPWUF
#define flagCMD8 STATUS,CWUF
;##### EntryPoint #####
org 0x00
goto init
;##### Subroutine Entry #####
;関数のcall先。ベースラインではページの前半しかcallできないためここにまとめてある。
;関数の本体はメインコードの後に置いてある。
txmsg232:
goto txmsg232core
getmsgbyte: ;これのみページ1へ飛ばす
bsf STATUS,PA0
movwf PCL
skipandread1: ;Wに1を入れてskipandreadn。すなわち1バイト読む
movlw .1
skipandreadn: ;0xFFのバイトを読み飛ばした後にNバイト読む
bsf flagWAIT
readn: ;Nバイト読む
goto readncore
skip2np1:
goto skip2np1core
skip2n:
goto skip2ncore
tx232hex:
goto tx232hexcore
;readresponse1:
; bsf respcnt,0
;readresponse:
; goto readresponsecore
sendnibble:
goto sendnibblecore
sendsdcmdarg0:
goto sendsdcmdarg0core
sendsdcmd:
goto sendsdcmdcore
;morse8:
; movwf dat
; goto f1
;morse:
; goto morsecore
;morsespace:
; ;interword(7)-interchar(3) =4
; movlw .8
; goto waithalfdit
readspi: ;SPIで0xFFを送信する
movlw 0xFF
spi:
goto spicore
tx232:
bsf GPIO,TXPIN ;0
bcf flag,RXFLAG
movwf commdat
goto tx232core
rx232:
btfss GPIO,RXPIN ;[0,3)
goto rx232
goto rx232core
rx232withtimeout:
bcf flag,TIMEOUT
rx232waitloop:
btfsc GPIO,RXPIN ;[0,5)
goto rx232core
incfsz cnt0,F
goto rx232waitloop
incf cnt1,F ;待ち時間を正しくカウントするために必要
btfsc GPIO,RXPIN
goto rx232core
incfsz cnt1,F
goto rx232waitloop
incf cnt2,F ;
btfsc GPIO,RXPIN
goto rx232core
incfsz cnt2,F
goto rx232waitloop
bsf flag,TIMEOUT
retlw 0
;##### Main Code #####
init
movwf OSCCAL
movlw b'10001000'
; /GPWU
; /GPPU=0: PullUp ON
; T0CS=0: Fosc/4,
; T0SE Don'tCare
; PSA Don'tCare
; PS2:0 Don'tCare
option
clrf ADCON0
bcf CM1CON0,C1ON
movlw MOSIBIT|CSBIT
movwf GPIO
movlw MISOBIT|RXBIT
tris GPIO
movlw 0x2A
movwf FSR
b12:
clrf INDF
bcf FSR,5
clrf INDF
bsf FSR,5
incfsz FSR,F
goto b12
initsd: ;1ms待ってから74個以上のクロックを出力
;wait 1ms
clrf cnt0
b9:
goto $+1
goto $+1
nop
decfsz cnt0,F
goto b9
movlw .5 ;10byte>74
call skip2np1
test:
;メッセージ出力
movlw msg_cmd0
call txmsg232
bcf GPIO,CSPIN
;CMD0を引数0で送信
movlw CMD|.0
call sendsdcmdarg0
;受信しつつ応答が来るまでスキップし、応答を1バイト読む
call skipandread1
;応答が来るまでにスキップした回数と受信内容を出力
decf skipcnt,W
call tx232hex
movlw ','
call tx232
movlw 0x30
movwf FSR
movf INDF,W
bcf FSR,5
call tx232hex
;CSをHighに
bsf GPIO,CSPIN
;1バイトダミークロック出力
movlw .0
call skip2np1
;CMD8で同様
movlw msg_cmd8
call txmsg232
bcf GPIO,CSPIN
movlw 0x01
movwf arg2
movlw 0xAB
movwf arg3
movlw CMD|.8
call sendsdcmd
;CMD8の応答は5バイト
movlw .5
call skipandreadn
bsf GPIO,CSPIN
movlw .0
call skip2np1
decf skipcnt,W
call tx232hex
movlw ','
call tx232
;5バイト出力。もうちょっとまともにやりたい。
movlw 0x30
movwf FSR
movf INDF,W
bcf FSR,5
call tx232hex
incf FSR,F
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
incf FSR,F
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
incf FSR,F
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
incf FSR,F
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
;CMD1。ACMD41を試してた名残りがある。
movlw msg_cmd1
call txmsg232
b13:
; movlw CMD|.55
; call sendsdcmdarg0
;
; call skipandread1
; bsf GPIO,CSPIN
; movlw 0x30
; movwf FSR
; movf INDF,W
; bcf FSR,5
; call tx232hex
bcf GPIO,CSPIN
movlw CMD|.1
call sendsdcmdarg0
call skipandread1
bsf GPIO,CSPIN
movlw .0
call skip2np1
movlw '/'
call tx232
decf skipcnt,W
call tx232hex
movlw ','
call tx232
movlw 0x30
movwf FSR
movf INDF,W
bcf FSR,5
movwf temp2
call tx232hex
movf temp2,W
btfss STATUS,Z
goto b13
;CMD58
movlw msg_cmd58
call txmsg232
bcf GPIO,CSPIN
movlw CMD|.58
call sendsdcmdarg0
movlw .5
call skipandreadn
bsf GPIO,CSPIN
movlw .0
call skip2np1
decf skipcnt,W
call tx232hex
movlw ','
call tx232
movlw 0x30
movwf FSR
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
incf FSR,F
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
incf FSR,F
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
incf FSR,F
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
incf FSR,F
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
;CMD9
movlw msg_cmd9
call txmsg232
bcf GPIO,CSPIN
movlw CMD|.9
call sendsdcmdarg0
call skipandread1
decf skipcnt,W
call tx232hex
movlw ','
call tx232
movlw 0x30
movwf FSR
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
movlw .16
call skipandreadn
decf skipcnt,W
call tx232hex
movlw ','
call tx232
movlw 0x30
b18:
movwf FSR
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
incf FSR,F
movf FSR,W
andlw 0x1F
btfss STATUS,Z
goto b18
movlw .3
call readn
movlw 0x30
movwf FSR
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
incf FSR,F
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
incf FSR,F
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
bsf GPIO,CSPIN
movlw .0
call skip2np1
;---
;CMD17
movlw msg_cmd17
call txmsg232
bcf GPIO,CSPIN
movlw CMD|.17
call sendsdcmdarg0
call skipandread1
decf skipcnt,W
call tx232hex
movlw ','
call tx232
movlw 0x30
movwf FSR
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
movlw '/'
call tx232
call skipandread1
decf skipcnt,W
call tx232hex
movlw ','
call tx232
movlw 0x30
movwf FSR
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
;512バイトを16バイトごとに受信して表示
movlw .512/.16
movwf cnt0
b17:
movlw msg_nl
call txmsg232
movlw .16
call skipandreadn
movlw 0x30
b16:
movwf FSR
bsf FSR,5
movf INDF,W
bcf FSR,5
call tx232hex
incf FSR,F
movf FSR,W
andlw 0x1F
btfss STATUS,Z
goto b16
bcf FSR,5
decfsz cnt0,F
goto b17
goto $
busychk: ;あれ、なんでこんなところに置いてあるんだろう。ページ前半でないとcallできない。
goto busychkcore
;##### Subroutine Core #####
busychkcore: ;ビジーチェック…のはずだけどどうやら使っていない。
call readspi
movf commdat,W
btfsc STATUS,Z
goto busychkcore
retlw 0
readncore: ;Nバイト読み取り
movwf bytecnt
movlw 0x30
movwf FSR
clrf skipcnt
readloop:
call readspi
btfss flagWAIT
goto f2
incf skipcnt,F
incf commdat,W
btfsc STATUS,Z
goto readloop
bcf flagWAIT
f2:
movf commdat,W
movwf INDF
incf FSR,F
decfsz bytecnt,F
goto readloop
bcf FSR,5
retlw 0
sendsdcmdarg0core: ;SDのコマンドを引数を0にして送信
clrf arg0
clrf arg1
clrf arg2
clrf arg3
sendsdcmdcore: ;SDのコマンドを送信
call spi
movf arg0,W
call spi
movf arg1,W
call spi
movf arg2,W
call spi
movf arg3,W
call spi
movlw CRC_CMD08
call spi
retlw 0
skip2np1core: ;2N+1バイト分のダミークロックを出力
bsf bitcnt,3
b14:
bsf GPIO,SCLKPIN
goto $+1
bcf GPIO,SCLKPIN
decfsz bitcnt,F
goto b14
skip2ncore: ;2Nバイト分のダミークロックを出力
movwf bytecnt
movf bytecnt,W
btfsc STATUS,Z
retlw 0
b11:
bsf bitcnt,3
b10:
bsf GPIO,SCLKPIN
goto $+1
bcf GPIO,SCLKPIN
goto $+1
bsf GPIO,SCLKPIN
goto $+1
bcf GPIO,SCLKPIN
decfsz bitcnt,F
goto b10
decfsz bytecnt,F
goto b11
retlw 0
spicore: ;SPI 1バイト送受信
movwf commdat
rrf GPIO,W
andlw ~(0x80|SCLKBIT>>1|CSBIT>>1|MISOBIT>>1)
movwf spioutbuf
; bcf GPIO,CSPIN
bcf STATUS,C
bsf bitcnt,3
spiloop:
rlf commdat,F
rlf spioutbuf,W ;CS:L SCLK:L MOSI<-C / c0
movwf GPIO
goto $+1
bsf GPIO,SCLKPIN
btfsc GPIO,MISOPIN
bsf commdat,0
decfsz bitcnt,F
goto spiloop
bcf GPIO,SCLKPIN
retlw 0
;### RS232C
;RS232Cシリアル関連のコード。
txmsg232core: ;文字列を送信
movwf msgptr
msgloop:
call getmsgbyte
bcf STATUS,PA0
movwf temp
andlw 0x7F
call tx232
btfsc temp,7
retlw 0
incf msgptr,F
movf msgptr,W
goto msgloop
retlw 0
tx232hexcore: ;1バイトを16進表記で送信
movwf temp
swapf temp,W
call sendnibble
movf temp,W
call sendnibble
retlw 0
sendnibblecore: ;下位ニブルを送信
andlw 0x0F
movwf commdat
movlw .6
addwf commdat,F
movlw '0'-.6
btfsc STATUS,DC
movlw 'A'-.10-.6
bsf GPIO,TXPIN ;0
bcf flag,RXFLAG
addwf commdat,F
goto tx232core
rx232core: ;1バイト受信。使っていない。
bsf flag,RXFLAG ;[4,7) mean5.5
movlw 0xFF
movwf commdat
goto $+1
; btfsc flag,BAUD300
; goto wait0r5a
;waitend0r5a:
goto $+1
goto $+1
goto $+1
goto $+1
goto $+1
;tx232:
; bsf GPIO,TXPIN ;0
; bcf flag,RXFLAG
; movwf commdat
; goto tx232core
tx232core: ;1バイト送信
nop ;5
bsf bitcnt,3
loop232:
goto $+1
; btfsc flag,BAUD300 ;ボーレートを115200と300で選べるようにしようと思ったが結局115200固定に。
; goto wait1b
;waitend1b:
goto $+1
nop
bcf STATUS,C
btfss GPIO,RXPIN ;read 28.5 +17n
bsf STATUS,C
rrf commdat,F
movf GPIO,W
andlw ~TXBIT
btfss STATUS,C
iorlw TXBIT
movwf GPIO ;send 20 +17n
decfsz bitcnt,F
goto loop232
; btfsc flag,BAUD300
; goto wait1c
;waitend1c:
;連続受信を極力早くするためここでreturn(2命令挟んで次のcallでストップビット中央)
btfsc flag,RXFLAG
retlw 0
goto $+1
goto $+1
goto $+1
goto $+1
goto $+1
goto $+1
bcf GPIO,TXPIN ;send stop +17
goto $+1
; btfsc flag,BAUD300
; goto wait1d
;waitend1d:
goto $+1
goto $+1
goto $+1
goto $+1
nop
;連続送信で安全な時間空ける(1命令挟んで次のcallでストップビットが17サイクル)
retlw 0
;### morse
;デバッグ出力用にモールス信号を出力するコードを書いたが結局使わなかった。
;morsecore:
; movwf dat
;b3:
; decf cnt0,F
; rlf dat,F
; btfss STATUS,C
; goto b3
;f1:
; movlw .8
; addwf cnt0,F
;
;b4:
; ;bcf GPIO,OUTPIN
; rlf dat,F
; movlw .6 ;dah
; btfss STATUS,C
; movlw .2 ;dit
; movwf cnt2
;
;b5: ;2MHz,4*256cyc: ~1kHz
; movlw 1<<MORSEPIN
; decfsz cnt3,F
; goto b5
; xorwf GPIO,F
; decfsz cnt1,F
; goto b5
; decfsz cnt2,F
; goto b5
;
;;bsf GPIO,OUTPIN
;
;;1点間隔
; movlw .2
; movwf cnt3
;b7:
; ;4cyc,2MHz: .13sec
; nop
; decfsz cnt1,F
; goto b7
; decfsz cnt2,F
; goto b7
; decfsz cnt3,F
; goto b7
;
; decfsz cnt0,F
; goto b4
;
;;interchar(3)-interelem(1) =2
; movlw .4
;; goto waithalfdit
;
;waithalfdit:
; movwf cnt3
;b6:
; ;4cyc,2MHz: .13sec
; nop
; decfsz cnt0,F
; goto b6
; decfsz cnt2,F
; goto b6
; decfsz cnt3,F
; goto b6
; retlw 0
;メッセージ。最上位bitを立てることで終端を示している。ASCIIしか使えないが、\0終端と比べ1メッセージあたり1バイト節約できるのが利点。
;GBのアセンブラにあった機能を真似してみた。
org 0x200
msg_cmd0:
dt "\r\nCMD0 reset&SPI mode\r", '\n'|0x80
msg_cmd8:
dt "\r\nCMD8 >ver.2?\r", '\n'|0x80
msg_cmd1:
dt "\r\nCMD1 initialize\r", '\n'|0x80
msg_cmd58:
dt "\r\nCMD58 read OCR register\r", '\n'|0x80
msg_cmd9:
dt "\r\nCMD9 read CSD register\r", '\n'|0x80
msg_cmd17:
dt "\r\nCMD17 read single block\r", '\n'|0x80
msg_nl:
dt "\r", '\n'|0x80
end
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
2014年10月30日 01:12
「プロ生ちゃんカレンダー プログラミング プチコンテスト 2014」という面白そうなものを知った。
カレンダーに載せる、"カレンダーを表示するソースコード"を募集するコンテストである。
思えばカレンダーのプログラムを書いたことが無かったので自分も何か書いてみたくなり、さて何で書くかと考えたところ、やはり最近マイブームなPIC10F200でいくことにした。
(その後よく考えてみたらJavaScriptでカレンダーを書いたことがあったが、まあ自力で閏年判定とかが初めてということで)
コードは末尾に載せたが、中でも今回自分で書いてて気に入っているポイントをいくつか挙げると、
・数値を受け取るそばから要らない部分を捨てて保存する(年の1000の位は2倍して100の位に足す)
・不正な値は完全無視(月の10の位は最下位bitしか見てないとか)
・6命令で閏年判定
movf yearl,W
btfsc STATUS,Z
movf yearh,W
andlw 0x03
btfss STATUS,Z
goto notleap
といったあたり。
あとはいつも使ってるコードだけどシリアル送受信は自信作。
使い方は、シリアルポートに抵抗だけ挟んで写真のように直結し、電源ON。
「Y?>」に対して年を4桁で、「M?>」に対して月を2桁で入力すると、当該月のカレンダーを出力してまた「Y?>」から入力待ちとなる。
表示はこんな感じ。(ローカルエコーON)
プログラム容量は余裕があるのでもうちょっとまともなメッセージにしてもよかったかもしれない。
以下プログラム。
プログラム容量は188ワード。PIC10F200の容量256ワードのうち使用率75%ほど。
(なお最初書き上げたときは201ワードだったのだが、コードにコメントなどつけて整理していたらいつの間にかここまで縮んでしまった。)
使用メモリは11バイト。16バイトの7割。
どちらも余裕であった。
おまけで機械語。1ワード(=1命令)が12bitで、16進数にすると3文字という独特の見た目が気に入っている。
カレンダーに載せる、"カレンダーを表示するソースコード"を募集するコンテストである。
思えばカレンダーのプログラムを書いたことが無かったので自分も何か書いてみたくなり、さて何で書くかと考えたところ、やはり最近マイブームなPIC10F200でいくことにした。
(その後よく考えてみたらJavaScriptでカレンダーを書いたことがあったが、まあ自力で閏年判定とかが初めてということで)
コードは末尾に載せたが、中でも今回自分で書いてて気に入っているポイントをいくつか挙げると、
・数値を受け取るそばから要らない部分を捨てて保存する(年の1000の位は2倍して100の位に足す)
・不正な値は完全無視(月の10の位は最下位bitしか見てないとか)
・6命令で閏年判定
movf yearl,W
btfsc STATUS,Z
movf yearh,W
andlw 0x03
btfss STATUS,Z
goto notleap
といったあたり。
あとはいつも使ってるコードだけどシリアル送受信は自信作。
使い方は、シリアルポートに抵抗だけ挟んで写真のように直結し、電源ON。
「Y?>」に対して年を4桁で、「M?>」に対して月を2桁で入力すると、当該月のカレンダーを出力してまた「Y?>」から入力待ちとなる。
表示はこんな感じ。(ローカルエコーON)
プログラム容量は余裕があるのでもうちょっとまともなメッセージにしてもよかったかもしれない。
以下プログラム。
プログラム容量は188ワード。PIC10F200の容量256ワードのうち使用率75%ほど。
(なお最初書き上げたときは201ワードだったのだが、コードにコメントなどつけて整理していたらいつの間にかここまで縮んでしまった。)
使用メモリは11バイト。16バイトの7割。
どちらも余裕であった。
;### PIC10F200 Calendar###
;by Ikadzuchi (@Pleist)
list p=10F200
#include p10f200.inc
radix dec
__CONFIG _MCLRE_OFF & _CP_OFF & _WDT_OFF
cblock 0x10
txdat, rxdat
yearh, yearl, month
cnt, dowcnt, daycnt, cnt232
day
temp
endc
;ピン
#define TXPIN 0
#define 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 rxdat,W
movwf yearh ;*1
addwf yearh,F ;*2
call rx232
movf rxdat,W
addwf yearh,F
;年下位 = 10c+d
call rx232
movf rxdat,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 rxdat,W
addwf yearl,F
;"M?>"
call newline
movlw 'M'
call tx232
movlw '?'
call tx232
movlw '>'
call tx232
;月2桁受信: ef
call rx232
movf rxdat,W
andlw 0x01 ;1っぽければ10をセット
btfss STATUS,Z
movlw .10
movwf month
call rx232
movf rxdat,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 getdaysinmonth
movwf daycnt
;当月の曜日の差分を取得し基準に足す
decf month,W
call getfirstdayinmonth
addwf doworg,F
;%=7
movlw .7
mod7loop:
subwf doworg,F
btfsc STATUS,C
goto mod7loop
addwf doworg,F
;ループ用に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
;当該月の日数を返す
getdaysinmonth:
addwf PCL,F
dt .31, .28, .31, .30, .31, .30 ;1~12月
dt .31, .31, .30, .31, .30, .31
dt .31, .29 ;閏年1,2月
#define ADJ .7 ;負にならないよう補正するため
;基準の年の当該月1日の曜日を返す
getfirstdayinmonth:
addwf PCL,F
dt 0+ADJ, 3+ADJ, 3+ADJ, 6+ADJ, 1+ADJ, 4+ADJ ;1~12月
dt 6+ADJ, 2+ADJ, 5+ADJ, 0+ADJ, 3+ADJ, 5+ADJ
dt 6+ADJ, 2+ADJ ;閏年1,2月
;RS232Cで1バイト送信する
tx232:
movwf txdat
bsf GPIO,TXPIN ;start 0
bsf cnt232,3
tx232loop:
rrf txdat,F
movf GPIO,W
andlw ~(1<btfss STATUS,C
iorlw 1<movwf GPIO ;write 7+9n
decfsz cnt232,F
goto tx232loop
goto $+1
goto $+1
nop
bcf GPIO,TXPIN ;stop
retlw 0
;RS232Cで1バイト受信し、下位ニブルのみ返す
rx232:
btfss GPIO,3 ;start [0,3)
goto $-1
movlw 0x80
movwf rxdat
bcf STATUS,C
rxloop:
goto $+1
movlw 0x0F
rrf rxdat,F
btfss GPIO,RXPIN ;read [9,12)+9n
bsf rxdat,7
btfss STATUS,C
goto rxloop
andwf rxdat,F
retlw 0
end
おまけで機械語。1ワード(=1命令)が12bitで、16進数にすると3文字という独特の見た目が気に入っている。
0 1 2 3 4 5 6 7 8 9 A B C D E F
0x 025 CC8 066 006 002 075 078 C59 99E C3F 99E C3E 99E 9AE 211 032
1x 1F2 9AE 211 1F2 9AE 211 033 403 373 373 1F3 373 9AE 211 1F3 97B
2x C4D 99E C3F 99E C3E 99E 9AE 211 E01 743 C0A 034 9AE 211 E0F 1F4
3x 213 643 212 E03 743 A3B C03 094 C0C 703 1F4 313 03A 31A E3F 1F3
4x 352 E06 0B3 0D4 980 037 0D4 98F 1F3 C07 0B3 603 A4A 1F3 C08 036
5x 293 0B6 035 97B A5B C20 99E C20 99E C20 99E 2F5 A55 079 C07 1F9
6x C06 723 0B9 C20 99E 399 E0F 643 C20 743 D30 99E 219 E0F D30 99E
7x 0F7 643 A79 2F6 A5E C07 036 97B A5E 97B A05 C0D 99E C0A 99E 800
8x 1E2 81F 81C 81F 81E 81F 81E 81F 81F 81E 81F 81E 81F 81F 81D 1E2
9x 807 80A 80A 80D 808 80B 80D 809 80C 807 80A 80C 80D 809 030 506
Ax 578 330 206 EFE 703 D01 026 2F8 AA1 AAA AAB 000 406 800 766 AAE
Bx C80 031 403 AB4 C0F 331 766 5F1 703 AB3 171 800 --- --- --- ---
Cx --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
Dx --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
Ex --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
Fx --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- Cxx
2014年02月12日 00:29
PICの周波数調整用コードを書いた。
軽く説明しておくと、PICには(大抵)内蔵発振器があり、その周波数は(物によって)OSCCALレジスタの設定値で128段階などに微調整出来る(する必要があると言うべきか)。
周波数調整で問題なのが何を基準に合わせるかだが、今回はシリアル通信を使うことにした。
シリアル通信のボーレートを自動調整する事がよくあるが、それを逆に利用することで、クロック周波数が分かる。
動作のアルゴリズムは単純で、"?"の文字を受信するとそのパルス幅を数え、その数値を送信するだけである。それにボーレートを掛ければクロック周波数が分かるという仕組みだ。
なぜ"?"なのかだが、これは世のボーレート自動設定機能も大抵"?"を使っているのだが、実は作ってみるまで理由が分かっていなかった。
"?"のASCIIコードは0x3Fで、シリアルではスタート・ストップビットが付き、最下位bitから送信するので、0111111001となる。
この1が6連続と長く並んでいるので、この部分の長さを計って6で割るのだろうと最初は思っていたのだが、6は切りが悪いし、"@"なら0000000101と0が7つ並ぶのでこちらのほうが長い。また、0→1と1→0の遷移で時間が同じ保証がない。
ではどこを見るかというと、0→1の遷移が最初と最後にあるが、この間がちょうど8ビット時間と切りが良い。さらにその間に余計な0→1の遷移がない。
この条件を満たすには下位bitが1・上位bitが0でその間に切り替わりが1回のみである必要があるので、0x7F、0x3F、0x1F、0x0F、0x07、0x03、0x01のいずれかとなり、そのうち印字可能文字は"?"のみである。
世のボーレート自動設定のコードを見たことはないが、おそらくこれが"?"を使っている理由なのではないかと思っている。
概要を説明する。
まず使うPICは10F200である。これは最も機能の少ない石なので、プログラムを直して他の石で動くようにするのは簡単だろう。
ボーレートは300とした。これは10F200のタイマーで計れる最長時間がプリスケーラーを使って65536サイクルなので、8ビット時間がこれ以下になる値ということで決めた気がするが、実際の計り方を考えるとこれはあまり意味がなかった。まあいいや。
65536サイクルを計るわけだが、計数精度が1バイトでは心もとない。しかし2バイトで数えようとすると繰り上げ処理に時間を食ったりで途端に大変になるのでどうしようかと考えた結果、面白い方法を思いついた(のでブログに書きたくなった)。
アイディアの骨子はこうだ。
・まずオーバーフローしない程度の低分解能で大体の時間を計る
・もう一度今度は高分解能でオーバーフローを気にせず計る
・それを統合する
ここで上位バイトと下位バイトは1bit分重ねておく必要がある。そうでないと2回の誤差でちょうど繰り上がった時に判断ができない。
実際のコードでは、8ビット時間分の計測対象の前にちょうど1ビット時間分のスタートビットがあるので、これを低分解能で計っている。
あとは1回毎にOSCCALの設定値を2づつ(最小単位が2)変えて、計測結果とその時のOSCCALの値をシリアルで送信…しようと思ったが面倒なので1つ前のOSCCALの値とその時の結果を送信している。
シリアル送信のボーレートをきちんと合わせるのも面倒なので、1つ前の、しかも低分解能の計測結果を使っている。
なので正常に通信できないことを考えて、クロックが最大11%ずれていても送信できるモードを考えた。
・16進数を送信したいだけなので、下位4bitが正常に送れれば良い。
・しかし、ストップビットが正しくないと受け付けられない。
・であれば、上位bitをストップビットと同じ"1"にしておけば良い。
・16進数の0-Fを0xC0-0xCFとして送ることにしよう。
・0xC0は「タ」、0xCFは「マ」である。
・これをタマモードと名付けよう。
しかしなんか普通に送れたので使う機会がなかった。一応コードには残してあるが未チェックである。使うならストップビットの時間も長くしたほうがいい。
さてそれで計った生データがこちら。
グラフにしたのがこちらである。
これを見ると面白いのが、32個ごと(値は64ごと)に不連続な点が見える。折角なので差分もグラフにしてみると、16個ごとや8個ごとにも値が飛んでいるのが分かる。
たぶん、周波数の微調整のためにはCR発振器のCやRを変化させるために抵抗ラダーのような構成にしているのだろうが、その特性のずれが見えているのだろう。
コードは以下のとおり。無保証あずいず。
シリアルポートとの結線はこちらを参考にしました。
RS-232C - TTLレベルの簡易変換方法 (ELM by ChaN)
軽く説明しておくと、PICには(大抵)内蔵発振器があり、その周波数は(物によって)OSCCALレジスタの設定値で128段階などに微調整出来る(する必要があると言うべきか)。
周波数調整で問題なのが何を基準に合わせるかだが、今回はシリアル通信を使うことにした。
シリアル通信のボーレートを自動調整する事がよくあるが、それを逆に利用することで、クロック周波数が分かる。
動作のアルゴリズムは単純で、"?"の文字を受信するとそのパルス幅を数え、その数値を送信するだけである。それにボーレートを掛ければクロック周波数が分かるという仕組みだ。
なぜ"?"なのかだが、これは世のボーレート自動設定機能も大抵"?"を使っているのだが、実は作ってみるまで理由が分かっていなかった。
"?"のASCIIコードは0x3Fで、シリアルではスタート・ストップビットが付き、最下位bitから送信するので、0111111001となる。
この1が6連続と長く並んでいるので、この部分の長さを計って6で割るのだろうと最初は思っていたのだが、6は切りが悪いし、"@"なら0000000101と0が7つ並ぶのでこちらのほうが長い。また、0→1と1→0の遷移で時間が同じ保証がない。
ではどこを見るかというと、0→1の遷移が最初と最後にあるが、この間がちょうど8ビット時間と切りが良い。さらにその間に余計な0→1の遷移がない。
この条件を満たすには下位bitが1・上位bitが0でその間に切り替わりが1回のみである必要があるので、0x7F、0x3F、0x1F、0x0F、0x07、0x03、0x01のいずれかとなり、そのうち印字可能文字は"?"のみである。
世のボーレート自動設定のコードを見たことはないが、おそらくこれが"?"を使っている理由なのではないかと思っている。
概要を説明する。
まず使うPICは10F200である。これは最も機能の少ない石なので、プログラムを直して他の石で動くようにするのは簡単だろう。
ボーレートは300とした。これは10F200のタイマーで計れる最長時間がプリスケーラーを使って65536サイクルなので、8ビット時間がこれ以下になる値ということで決めた気がするが、実際の計り方を考えるとこれはあまり意味がなかった。まあいいや。
65536サイクルを計るわけだが、計数精度が1バイトでは心もとない。しかし2バイトで数えようとすると繰り上げ処理に時間を食ったりで途端に大変になるのでどうしようかと考えた結果、面白い方法を思いついた(のでブログに書きたくなった)。
アイディアの骨子はこうだ。
・まずオーバーフローしない程度の低分解能で大体の時間を計る
・もう一度今度は高分解能でオーバーフローを気にせず計る
・それを統合する
ここで上位バイトと下位バイトは1bit分重ねておく必要がある。そうでないと2回の誤差でちょうど繰り上がった時に判断ができない。
実際のコードでは、8ビット時間分の計測対象の前にちょうど1ビット時間分のスタートビットがあるので、これを低分解能で計っている。
あとは1回毎にOSCCALの設定値を2づつ(最小単位が2)変えて、計測結果とその時のOSCCALの値をシリアルで送信…しようと思ったが面倒なので1つ前のOSCCALの値とその時の結果を送信している。
シリアル送信のボーレートをきちんと合わせるのも面倒なので、1つ前の、しかも低分解能の計測結果を使っている。
なので正常に通信できないことを考えて、クロックが最大11%ずれていても送信できるモードを考えた。
・16進数を送信したいだけなので、下位4bitが正常に送れれば良い。
・しかし、ストップビットが正しくないと受け付けられない。
・であれば、上位bitをストップビットと同じ"1"にしておけば良い。
・16進数の0-Fを0xC0-0xCFとして送ることにしよう。
・0xC0は「タ」、0xCFは「マ」である。
・これをタマモードと名付けよう。
しかしなんか普通に送れたので使う機会がなかった。一応コードには残してあるが未チェックである。使うならストップビットの時間も長くしたほうがいい。
さてそれで計った生データがこちら。
1667D014678612663E1065F90E65B30C647B0A64350863F00663A904625E0262190061D4FE618EFC6079FA6035F85FF0F65FAAF45E62F25E1BF05DD6EE5D90EC5C5AEA5C13E85BD0E65B8BE45A40E259FBE059B7DE5872DC5823DA57DFD8579CD65658D4560FD255CAD05586CE5441CC540ACA53C6C85383C6523FC451F6C251B2C0506FBE502BBC4FB9BA4E76B84E32B64DEFB44DA8B24C63B04C23AE4BDEAC4BA8AA4A64A84A24A649E0A4499AA24858A048169E47D29C47849A46449846029645C09444799244399043F58E43B58C427D8A423D8841FB8641BA844072824030803FF0823FAF843FEE8640308840728A41BA8C41FB8E423D90427D9243B59443F79644379844799A45C09C46029E4644A04786A247D4A44814A64858A8499AAA49E2AC4A24AE4A66B04BA8B24BDFB44C21B64C65B84DA7BA4DEFBC4E32BE4E76C04FB8C2502BC4506FC651B2C851F6CA523FCC5383CE53C6D0540AD25441D45586D655CAD8560FDA5658DC579CDE57E1E05824E25871E459B7E659FCE85A41EA5B89EC5BCEEE5C15F05C5AF25D90F45DD6F65E1BF85E60FA5FAAFC5FEFFE603400607A02618F0461D30662190862600A63A90C63F00E643610647B1265B41465F916663E1867861A67D01C68161E685D2069A32269F1246A39266A7E286BC52A6C112C6C582E6D9E306DE5326E1E346E66366FAB386FF33A703C3C71843E71CB4072134273884473D046741648745E4A75AB4C75F34E76385077805277BA5478015678495879915A79DD5C7A255E7A6D607BB4627C03647C4B667D93687DDB6A7E286C7E706E7FB6707FFD72803674807E7681C478820B7A82577C839E7E83E47C842B7A83E478839E76825774820B7281C470807E6E80366C7FFD6A7FB5687E6E667E28647DDB627D93607C4B5E7C055C7BB45A7A6D587A255679DC5479915278495078024E77BA4C77814A763B4875F34675AB44745E4274164073D03E73883C72133A71CC38718436703C346FF1326FAB306E642E6E1E2C6DE62A6D9E286C58266C11246BC6226A7E206A381E69F11C69A31A685B1868161667D01467861266401065F90E65B30C647A0A64350863F00663A904625E0262190061D4FE618EFC6079FA6035F85FEFF65FAAF45E60F25E1BF05DD6EE5D91EC5C5AEA5C15E85BD0E65B8BE45A41E259FCE059B7DE5872DC5824DA57DFD8579CD65658D4560FD255CAD05586CE5441CC540ACA53C6C85383C6523FC451F7C251B2C0506FBE502BBC4FB9BA4E76B84E32B64DEFB44DA8B24C66B04C23AE4BDFAC4BA9AA4A67A84A24A649E2A4499AA24858A048169E47D59C47879A46459846029645C1
グラフにしたのがこちらである。
これを見ると面白いのが、32個ごと(値は64ごと)に不連続な点が見える。折角なので差分もグラフにしてみると、16個ごとや8個ごとにも値が飛んでいるのが分かる。
たぶん、周波数の微調整のためにはCR発振器のCやRを変化させるために抵抗ラダーのような構成にしているのだろうが、その特性のずれが見えているのだろう。
コードは以下のとおり。無保証あずいず。
シリアルポートとの結線はこちらを参考にしました。
RS-232C - TTLレベルの簡易変換方法 (ELM by ChaN)
list p=10F200
#include p10f200.inc
radix dec
; N/C+-v-+(GP3)
; Vdd| |Vss
; GP2| |N/C
; GP1+---+GP0
__CONFIG _MCLRE_OFF & _CP_OFF & _WDT_OFF
cblock 0x10
tmr32
tmr2
flag
cnt
waitcnt
waitcnt2
txdat
endc
;レジスタ使いまわし
#define temp waitcnt
#define temp2 waitcnt2
;プリスケーラー設定用
#define OPTVAL2 b'10010000' ;2:1
#define OPTVAL32 b'10010100' ;32:1
#define OPTVAL64 b'10010101' ;64:1
; ^/GPWU: DontCare
; ^/GPPU DontCare
; ^T0CS=0: Fosc/4,GP2=I/O
; ^T0SE Don'tCare
; ^PSA TMR0
; ^^^PS
#define FCLK .4000000 ;クロック周波数
#define BAUD .300 ;ボーレート
#define ASCEND 0
#define TAMA 0 ;タマモード(未使用)
#define SERIAL_WIDTH (FCLK/4 / BAUD / 32) ;シリアルの1ビット時間(プリスケーラー32)
init:
org 0x00
movwf OSCCAL
movlw SERIAL_WIDTH
movwf tmr32
clrf cnt
clrw
movwf GPIO
tris GPIO
bcf flag,ASCEND
mainloop:
movlw OPTVAL64
option ;ps=64
wait2bhigh: ;wait 2bit high pulse
btfss GPIO,3 ; wait ^
goto $-1
clrf TMR0
bcf STATUS,C
rrf tmr32,W
movwf temp
bcf STATUS,C
rrf temp,W
subwf tmr32,W; W=tmr32*0.75
btfsc GPIO,3 ; wait v
goto $-1
subwf TMR0,W
btfss STATUS,C
goto wait2bhigh
movlw OPTVAL32
option ;ps=32
count1bit:
btfss GPIO,3 ;wait ^ [0,3)
goto $-1
clrf TMR0 ;[2,5)+2
btfsc GPIO,3 ;wait v [0,3)
goto $-1
goto $+1
movf TMR0,W ;[4,7)
movwf tmr32
movlw OPTVAL2 ;ps=2
option
clrf TMR0 ;[8,11)
count8bit:
btfss GPIO,3 ; wait ^
goto $-1
btfsc GPIO,3 ; wait v [0,3)
goto $-1
nop
movlw 2 ;(8-4)/2
addwf TMR0,W ;[4,7)
movwf tmr2
movlw 2
btfss flag,ASCEND
movlw -2
addwf OSCCAL,F
;方向転換
movlw 0x80
subwf OSCCAL,W
btfsc STATUS,Z
bsf flag,ASCEND
movlw 0x7E
subwf OSCCAL,W
btfsc STATUS,Z
bcf flag,ASCEND
;カウント上位を補正
rlf tmr2,W ;tmr2,7 -> C
rlf 0x0F,W ;Cが欲しいだけ
xorwf tmr32,W
movwf temp
btfss temp,0 ;if tmr2,7!=tmr32,0 then do
goto skipadj
;補正実行
btfsc tmr2,6
decf tmr32
btfss tmr2,6
incf tmr32
skipadj:
;結果送信
swapf OSCCAL,W
call tx232
movf OSCCAL,W
call tx232
swapf tmr32,W
call tx232
movf tmr32,W
call tx232
swapf tmr2,W
call tx232
movf tmr2,W
call tx232
goto mainloop
;シリアル送信
tx232:
andlw 0x0F
movwf txdat
#if TAMA
movlw 0xC0
#else
movlw 6
addwf txdat,W
movlw '0'
btfsc STATUS,DC
movlw 'A'-10
#endif
bsf GPIO,2 ;#
addwf txdat,F
nop
call wait32n
bsf cnt,3
txloop:
movf GPIO,W
andlw 0x0B
rrf txdat,F
btfss STATUS,C ;極性反転
iorlw 0x04
movwf GPIO ;# >9, >9>
call wait32n
decfsz cnt,F
goto txloop
call wait6
bcf GPIO,2 ;# >3
call wait32n
retlw 0
wait32n: ;wait 3+32n
movf tmr32,W
movwf waitcnt
waitloop:
nop
movlw .9
movwf waitcnt2
waitloop2:
decfsz waitcnt2,F
goto waitloop2
decfsz waitcnt,F
goto waitloop
retlw 0
wait6:
goto $+1
retlw 0
end
2013年08月14日 02:04
最弱のPICマイコン、PIC10F200でまた電子オルガンを作ってみた。
I/Oピン4本の10F200で、前回は追加部品なしで8キーの入力をとる方法を示したが、ダイオードを使えばなんと28入力まで可能となる。
【ニコニコ動画】最弱のPICマイコンでまた電子オルガンを作ってみた(実演編)
【ニコニコ動画】最弱のPICマイコンでまた電子オルガンを作ってみた(解説編) (※縮小表示になってるので埋め込みプレイヤーでの視聴は非推奨)
まず参考になるのがこちらのページ。
ELM - ポートが足りないときは
3ピンの場合で描いた図がこちら。(プルアップ抵抗は省略)
左3つのキーは普通にそれぞれ1,2,3番のピンのみをGNDに落とすが、他のキーは複数のピンを同時にGNDに落とす。ダイオードは逆流防止だ。
マイコン側では3本のピンの値の組み合わせを見ることで、
の2^3=8通りを判別できる。このうち何も押されていないHHHを除いた7通りで7キーまで扱える。
4ピンなら2^4-1=15通りとなる。
なおマイコンへの入力のありうる全組み合わせがそれぞれのキーに対応していることからも分かるように、キーの同時押しは判別不可能である。同時押しはどこか別のキーとして読み取られる。
さらにこれにキーマトリクスの原理を組み合わせる。
ピンを2本追加した場合を考えると、
ピン4とピン5の出力をどちらをLowにするかで、左右の7つづつのキーをピン1-3の入力側で上と同じように判別することができる。
これをさらにCharlieplexingのように入力側と出力側を兼用する配線にしたのが最終的な今回の回路である。
基板上の配線は、秋月の細長い基板に綺麗に収まった。なかなかの自信作である。
押されたキーを判定するには、
・全ピンがHighの時、ピン0,1,3の値
・ピン2がLowの時、ピン0,1,3の値
・ピン0,2がLowの時、ピン1,3の値
・ピン1,2がLowの時、ピン0,3の値
の計10個の値を判断する必要がある。
10bitのテーブルを引ければそのままテーブルのキーにするだけだが、Flashが256ワードしかないためテーブル引きをするには8bitしか扱えない。
そこでどうするかだが、この10bitのテーブルは空きが多いので、2つの値を1つにまとめてしまっても運よく別のエントリと衝突しないことがある。
言葉で説明しづらいので例を挙げる。
このような4bitのテーブルがあったとする。
この場合、AとBの代わりにA|Bという値を使っても、全てのエントリが区別できる。
これにより4bitから3bitにキーのビット数を減らすことができた。
実際には、
A + B<<1 + C<<1 + D<<2 + E<<2 + F<<3 + G<<4 + H<<4 + I<<6 + J<<7
のような計算で10bitを8bitに畳んでいる。
テーブルの値は8bitなので、次のような2段階のテーブル引きで11bitの値を取得している。
キー押下パターンをキーにして引く第1のテーブルの値は、「音高データの下位3bit」「8va 1bit」「第2テーブルを引くキー4bit」という構造をしている。
次にそのキーで第2のテーブルを引き、音高データの上位8bitを得る。
最後に、8vaが1ならば、右シフトして値を半分にする。
なお細かいことだが、ここで「8vaが0」で判定すると余計なbitを消すのに手間がかかる。1で判定すれば8vaが0の時も1の時も下位バイトの下位4bitを消すだけで済む。
第1のテーブルに入れるキーと音高データを兼用することで同じ2段階のテーブルでもっと精度を上げることもできる。ただ11bitあれば人間に判別できない精度にはなるようなので今回はやらなかった。
--15/06/15
前回へのリンク追加
I/Oピン4本の10F200で、前回は追加部品なしで8キーの入力をとる方法を示したが、ダイオードを使えばなんと28入力まで可能となる。
【ニコニコ動画】最弱のPICマイコンでまた電子オルガンを作ってみた(実演編)
【ニコニコ動画】最弱のPICマイコンでまた電子オルガンを作ってみた(解説編) (※縮小表示になってるので埋め込みプレイヤーでの視聴は非推奨)
28キーの配線
解説しよう。まず参考になるのがこちらのページ。
ELM - ポートが足りないときは
3ピンの場合で描いた図がこちら。(プルアップ抵抗は省略)
左3つのキーは普通にそれぞれ1,2,3番のピンのみをGNDに落とすが、他のキーは複数のピンを同時にGNDに落とす。ダイオードは逆流防止だ。
マイコン側では3本のピンの値の組み合わせを見ることで、
H | H | H |
H | H | L |
H | L | H |
L | H | H |
H | L | L |
L | H | L |
L | L | H |
L | L | L |
4ピンなら2^4-1=15通りとなる。
なおマイコンへの入力のありうる全組み合わせがそれぞれのキーに対応していることからも分かるように、キーの同時押しは判別不可能である。同時押しはどこか別のキーとして読み取られる。
さらにこれにキーマトリクスの原理を組み合わせる。
ピンを2本追加した場合を考えると、
ピン4とピン5の出力をどちらをLowにするかで、左右の7つづつのキーをピン1-3の入力側で上と同じように判別することができる。
これをさらにCharlieplexingのように入力側と出力側を兼用する配線にしたのが最終的な今回の回路である。
基板上の配線は、秋月の細長い基板に綺麗に収まった。なかなかの自信作である。
テーブルキーのbit数
さて25個のキーを扱うにはもうひとつ問題がある。押されたキーを判定するには、
・全ピンがHighの時、ピン0,1,3の値
・ピン2がLowの時、ピン0,1,3の値
・ピン0,2がLowの時、ピン1,3の値
・ピン1,2がLowの時、ピン0,3の値
の計10個の値を判断する必要がある。
10bitのテーブルを引ければそのままテーブルのキーにするだけだが、Flashが256ワードしかないためテーブル引きをするには8bitしか扱えない。
そこでどうするかだが、この10bitのテーブルは空きが多いので、2つの値を1つにまとめてしまっても運よく別のエントリと衝突しないことがある。
言葉で説明しづらいので例を挙げる。
このような4bitのテーブルがあったとする。
A・B | 1 | 1 | 0 | 0 | |
C・D\ | 1 | 0 | 1 | 0 | |
1 | 1 | a | b | ||
1 | 0 | c | d | ||
0 | 1 | e | |||
0 | 0 | f |
A|B C・D\ | 1 | 0 | |
1 | 1 | a | b |
1 | 0 | c | d |
0 | 1 | e | |
0 | 0 | f |
これにより4bitから3bitにキーのビット数を減らすことができた。
実際には、
A + B<<1 + C<<1 + D<<2 + E<<2 + F<<3 + G<<4 + H<<4 + I<<6 + J<<7
のような計算で10bitを8bitに畳んでいる。
音高データのbit数
ついでに今回は音高の精度を11bitに上げた。テーブルの値は8bitなので、次のような2段階のテーブル引きで11bitの値を取得している。
キー押下パターンをキーにして引く第1のテーブルの値は、「音高データの下位3bit」「8va 1bit」「第2テーブルを引くキー4bit」という構造をしている。
次にそのキーで第2のテーブルを引き、音高データの上位8bitを得る。
最後に、8vaが1ならば、右シフトして値を半分にする。
なお細かいことだが、ここで「8vaが0」で判定すると余計なbitを消すのに手間がかかる。1で判定すれば8vaが0の時も1の時も下位バイトの下位4bitを消すだけで済む。
第1のテーブルに入れるキーと音高データを兼用することで同じ2段階のテーブルでもっと精度を上げることもできる。ただ11bitあれば人間に判別できない精度にはなるようなので今回はやらなかった。
ソース
以下のとおり。なお前回の8キー版で詳しく説明した部分はコメントをサボっているので前回のものも見た方がよい。list p=10F200
#include p10f200.inc
radix dec ;プログラム中では一応16進も10進も明記するつもりで10進はよく忘れるのでデフォルトは10進
; N/C+-v-+(GP3)
; Vdd| |Vss
; GP2| |N/C
;CLK GP1+---+GP0 DAT
__CONFIG _MCLRE_OFF & _CP_OFF & _WDT_ON
;GPIO3を使いたいので_MCLRE_OFF
;消費電力低減のためWDTを使う
cblock 0x10
waitcount ;ウェイト用カウンタ
index ;テーブルを引くためのキー
;(ここにあった不要な変数を消したので、このソースのアセンブル結果は動画の最後のものと一致しない)
tonel ;音の周期上位
toneh ;音の周期下位
acc ;周期の微調整のためのアキュムレータ
endc
#define origin 169 ;テーブルの一番大きな隙間が0番地~に来る位置
;音階に対応するサイクル数と、マクロで扱うための順序数(第2テーブルのキー)
;配列で定義したいところだがそういう機能が無いので桁で分けて強引に2つをまとめた
do equ .1911 + 0x0000
di equ .1804 + 0x1000
re equ .1703 + 0x2000
ri equ .1607 + 0x3000
mi equ .1517 + 0x4000
fa equ .1432 + 0x5000
fi equ .1351 + 0x6000
so equ .1276 + 0x7000
si equ .1204 + 0x8000
la equ .1136 + 0x9000
li equ .1073 + 0xA000
ti equ .1012 + 0xB000
doh equ .956 + 0xC000
;低いオクターブでは1周期がこの値(HighとLowでこの半分づつ)
;高いオクターブではその半分
;例: 1000000/1136=880.28169 ≒ 880Hz
;上で定義した音階名と1オクターブ上げる指示(以下8va)から第1テーブルの値を生成するマクロ
tone macro name, high
retlw low(name<<5) + (high<<4) + (name>>12)
; 音高の下3bit 8va 第2テーブルのキー
endm
#define ADJUST 57 ;音高データを補正するための、地のプログラムで消費するサイクル数
init
org 0x00
movwf OSCCAL ;補正
;スリープからの復帰の場合は不要な初期化処理を飛ばそうとしたが、
;いまいち仕様がよく分からなかったため、やめた
; btfss STATUS, NOT_PD ; /PD=0 then wake up from sleep
; goto afterSleep
movlw b'10000000'
; ^/GPWU :ピン変化によるウェイクアップはWDTによるウェイクアップと競合しそうなのでOFF
; ^/GPPU=0: PullUp ON
; ^T0CS=0: Don'tCare
; ^T0SE : Don'tCare
; ^PSA=0: WDTを最速で動かすためプリスケーラをTMR0に割り当て
; ^^^PS2: タイマは使用しないためDon'tCare
option
main
movlw b'00001011' ;とりあえずピン2以外Z
tris GPIO
mainloop
call readkey ;スイッチに応じたビットパターンを生成
call gettone ;スイッチに応じた音階データを取得(第1のテーブル引き)
movwf tonel
incfsz tonel,W ;無音を分岐; if FF then sleep
goto soundOn
sleep ;無音ならスリープ、WDTで18ms後にリセット
soundOn
andlw 0x0F
call gettoneh ;C=0 ;第2のテーブル引き
movwf toneh
;8va処理
btfsc tonel,4
rrf toneh,F
btfsc tonel,4
rrf tonel,F
;音高データのサイクル数から地のプログラムで消費するサイクル数を引く
movlw (ADJUST&3)<<6
subwf tonel,F
movlw ADJUST>>2
btfss STATUS,C
decf toneh,F
subwf toneh,F
call wait ;指定サイクル数のウェイト
clrwdt
bsf GPIO,2 ;圧電スピーカーへの出力をH
call adjustHL
call wait
goto mainloop
;H側とL側のサイクル数の差を補正
;無駄の多いコードだが、Flashには余裕があるのでよしとする
adjustHL:
goto $+1
goto $+1
goto $+1
goto $+1
goto $+1
goto $+1
goto $+1
retlw 0
;スイッチを読み、10bitを8bitに畳んでテーブルを読むためのキーとする
;テーブルを引くキーと鍵盤のキーが紛らわしいので後者をスイッチって呼ぼうとしたけど関数名はkeyのままだった
readkey
;全てHで読む
;xxx3x10x ←この位置にピンを読んだ結果を入れる
rlf GPIO,W
andlw b'00010110'
movwf index
movlw b'00001010'
tris GPIO
bcf GPIO,0
nop ;プルアップでHighになるまでの猶予
;0がLで読む
;xxxxx31x
rrf GPIO,W
andlw b'00000101'
addwf index,F
andlw 1
addwf index,F
movlw b'00001001'
tris GPIO
bcf GPIO,1
nop
;1がLで読む
;xxxx3xx0
movf GPIO,W
bcf GPIO,2 ;下で読むためにGP2をL (圧電スピーカーの容量で時間がかかりそうなので早めに)
andlw b'00001001'
addwf index,F
movlw b'00001011'
tris GPIO
nop
;2がLで読む
;3x10xxxx
swapf GPIO,W
addwf index,F
retlw 0
;テーブルの隙間にコードを書いているので場所が飛ぶ
org low(origin+.214)
;toneh:tonelの値に応じてウェイト
wait:
;toneh*4
movf toneh,W
movwf waitcount
nop
decfsz waitcount,f
goto $-2
;tonel[7:6]
btfsc tonel,6
goto $+1
btfsc tonel,7
goto $+1
btfsc tonel,7
goto $+1
;tonel[5:4]/4 (ディザリング)
movf tonel,W
andlw 0x30
addwf acc,F
btfsc acc,6
goto $+1
bcf acc,6
retlw 0
gettoneh ;第2のテーブルから音高データその2を取得
addwf PCL,F
nop
retlw low(do>>3) ;音高データの上位8bit
retlw low(di>>3)
retlw low(re>>3)
retlw low(ri>>3)
retlw low(mi>>3)
retlw low(fa>>3)
retlw low(fi>>3)
retlw low(so>>3)
retlw low(si>>3)
retlw low(la>>3)
retlw low(li>>3)
retlw low(ti>>3)
retlw low(doh>>3)
org origin-2
gettone ;第1のテーブルから音高データを取得
movf index,W
addwf PCL,F
;第1のテーブルはFlash全域にわたる
org low(origin+.179)
tone do, 0
org low(origin+.204)
tone di, 0
org low(origin+.212)
tone re, 0
org low(origin+.85)
tone ri, 0
org low(origin+.165)
tone mi, 0
org low(origin+.38)
tone fa, 0
org low(origin+.81)
tone fi, 0
org low(origin+.209)
tone so, 0
org low(origin+.197)
tone si, 0
org low(origin+.181)
tone la, 0
org low(origin+.53)
tone li, 0
org low(origin+.69)
tone ti, 0
org low(origin+.196)
tone do, 1
org low(origin+.207)
tone di, 1
org low(origin+.211)
tone re, 1
org low(origin+.57)
tone ri, 1
org low(origin+.156)
tone mi, 1
org low(origin+.77)
tone fa, 1
org low(origin+.201)
tone fi, 1
org low(origin+.205)
tone so, 1
org low(origin+.194)
tone si, 1
org low(origin+.175)
tone la, 1
org low(origin+.19)
tone li, 1
org low(origin+.0)
tone ti, 1
org low(origin+.37)
tone doh, 1
org low(origin+.213)
retlw 0xFF ;無音
end
--15/06/15
前回へのリンク追加