SDカードから1セクタ読み取るまでの手順解説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
CDのピットの数
6段のカレンダーが好きだ
PIC16のDhrystone MIPSを測ろうとしてみた
Twitterの画像の扱いがやっとまともになって嬉しい
ファミコンで9×9ドット文字表示(ほか)
謎の色名「honeydewtab」とlegacy color valueパース手順
6段のカレンダーが好きだ
PIC16のDhrystone MIPSを測ろうとしてみた
Twitterの画像の扱いがやっとまともになって嬉しい
ファミコンで9×9ドット文字表示(ほか)
謎の色名「honeydewtab」とlegacy color valueパース手順
この記事へのコメント
参考にさせていただきました、貴重な情報ありがとうございます!
Posted by 通りすがり与太郎 at 2020年01月08日 11:20