こんにちは。Yukiです。
今回はCH32V203でSDを使ってみたいと思います。
目標
今回の目標として、SDをWriteとReadを自由にできるようになることです。
FATとかそういう難しいことに関しては実装しません。
(でかい512byte単位でしか書き込み・読み込み出来ないEEPROMみたいなのを想像すればいいとおもいます。)
今回はSDHCを対象とします。SDSC(2GB以下のSD)やSDXCに関しては対象としません。(SDXCは動作するとは思いますが、保証はできません)
回路図

みにくいかもしれませんが、こんな感じの配線にしました。
SDは結構電力消費が大きいので、今回はWCH-LinkEから給電はせず、USBから供給するようにしました。また、CH32V203とSDは3.3V駆動であることから、LM2940-3.3で5V→3.3Vに降圧しています。
前段階 - SPIとSD
まずSDカードについて簡単に知っておく必要があります。
実は昔、PICでSDを使ったことがあるので軽く知っているのですが、まずSDにはデータのアクセス手段が2つあります。SDIOとSPIです。SDIOは内部規格が公開されておらず、一切わかりません。そのためESP32とかについているSDIOと一緒に使うことになります。CH32V203にはSDIOはありませんから、この時点でSPI一択になります。
次にSPI通信についてです。

上の画像はCH32V203のReference Manualから持ってきたものです。
ここでポイントとなるのが、Shift Registerであるという点です。
シフトレジスタはクロックが入ると1つずれるようなレジスタで、今回の例だと、1クロック与えられるとMOSIが1bit分送信 MISOが1bit分受信するようなかたちです。
ということで、例えば16byte分受信したいなと思ったら、ダミーのデータ(0xFF)を16byte分送信することにより取得するような形です。
CH32V203はバッファ等はついていないので、送信したら必ず受信する処理が必要です。シフトレジスタは8bitですから、16byte受信したいと思ったら、0xFF(8bit)送信。その後受信されたか確認するレジスタを確認して変数に入れる、また0xFFを送信...を16回繰り返します。
次に、SPIの設定ですが、SPOL=0 SPHA=0 (いわゆるMODE0)でOKです。
SDの初期化手順
1. SS端子をHIGHにした状態で74クロック以上送信します。
これは0xFFを送信すればいいです。
2. SSをLOWにする
3. CMD0を送信する
0x40 00 00 00 00 95 を送信します。
送信後、レスポンス1byte 0x01を待ちます。0x01が返ってくるまで0xFFを送信し続けます。
4. SSをHIGHにする
5. 0xFFを送信する
6.SSをLOWにする
7. CMD8を送信する
0x48 00 00 01 AA 87 を送信します。
送信後、レスポンス5byteを受信します。
0xFFを送信して、レスポンスに0xFF以外が返ってきたタイミングから5byte取得する形になります。
0x01 00 00 01 AA なら成功です。
8. SSをHIGHにする
9. 0xFFを送信する
10. SSをLOWにする
11. CMD55を送信する
0x77 00 00 00 00 65 を送信します。
送信後、何らかのレスポンス1byteが返ってくるまで待ちます。(0x00または0x01が返ってきます)
12. ACMD41を送信する
0x69 40 00 00 00 77 を送信します。
送信後、1byteのレスポンスを待ちます。 返ってくるまで0xFFを送信し続けてください。
13. SSをHIGHにする
14. 0xFFを送信する
15. ACMD41のレスポンスで条件分岐
ACMD41のレスポンスが0x01であれば、10. に戻ります。
0x00であれば、16. に進みます。
16. SSをLOWにする
17. CMD59を送信する
0x7B 00 00 00 00 91 を送信します。
送信後、1byteのレスポンスを待ちます。0x00であれば成功です。
18. SSをHIGHにする
19. 0xFFを送信する
初期化時のCMDの説明
この欄は、各コマンドの役割などを解説します。
CMDxは 0x40をorすることにより生成されます。
例えばCMD8であれば、 8 | 0x40 = 0x48となります。
レスポンス(応答)はR1とR3とR7があります。
R1は1byte
R3は1byte(R1レスポンス) + 32byte
R7は1byte(R1レスポンス)+4byte
で構成されています。詳しく知りたい場合は、別サイトで調べてみてください。(ちゃんとビットごとに意味があります)
CMD0:
ソフトウェアリセット 応答はR1
CMD8:
動作電圧確認コマンド 実SDHC/SDXCしか使わないのであれば、実行する必要はありません。
このコマンドはSDSC(2GB)は持っていないコマンドなので、ここで弾かれます。
応答はR7です。
CMD55:
ACMDを動かす前に送信するコマンドです。
今回はACMD41の前に動かしています。
応答はR1です。
ACMD41:
SD初期化開始のコマンドです。
応答はR1です。
(ちなみにですが、初期化シーケンスだとここが一番長いです)
CMD59:
SPIモードでCRCモードを有効・無効にするコマンドです。
応答はR1です。(実は初期状態からCRCは無効のカードが多いのでやらなくても大丈夫かもしれません)
SDの読み込み手順
次にSDの読み込み手順について説明します。
SDHCからは、512byteを1ブロックとして扱う方式になりました。
よって、
0x0000000を指定すると0byte~511byte
0x0000001を指定すると512byte~1023byte
という風になっています。
また、SDには1ブロックずつ読み込むモードと、まとめて複数ブロック読み込むモードがありますが、今回は1ブロックずつ読み込むモードのみ実装します。
1. SSをLOWにします。
2. CMD17を送信します
例えば読み込みたいブロック番地が 0xvv xx yy zzだとしたら
0x51 vv xx yy zz FF
を送信します。(最後のやつは0xFFです)
送信後、レスポンスが返ってくるまで待ちます。0x00であればOKです。
3. スタートトークンを待ちます
スタートトークン 0xFE が返ってくるまで待ちます。
4. データを受信します。
スタートトークン直後からデータが来るので、512byte分受信します。
5. CRCを破棄します。
CRCは無効化されていますが、一応CRCが来ます。なので0xFF FFを送信して2byte流します。
6. SSをHIGHにします。
7. 0xFFを送信します

SDの書き込み手順
書き込みも同様に、1ブロックずつ書き込むモードと、まとめて複数ブロック書き込むモードがありますが、今回は1ブロックずつ書き込むモードのみ実装します。
(ちなみにですが、SDの書き込みは低速なので、本当はまとめて書き込んだ方が高速です)
1. SSをLOWにします。
2. CMD24を送信します。
例えば書き込みたいブロック番地が 0xvv xx yy zzだとしたら
0x58 vv xx yy zz FF を送信します。
送信後、レスポンスが戻ってくるまで待ちます。0x00だったらOKです。
3. 0xFFを送信します。
4. 0xFE(スタートトークン)を送信します。
5. 512byte分のデータを送信します。
6. 0xFF FF と2byte送信します。(ダミーCRC)
7. レスポンスが来るまで待ちます
書き込み中を示す0x05が返ってくるまで待機します。
なお0x05以外が返ってきたらエラーです。
8. レスポンスが変化するまで待ちます
返ってくるレスポンスが0x05が0xFFになるまで待機します。
9. SSをHIGHにします
10. 0xFFを送信します。

ソースコード - ライブラリ
今回は容量が大きくなったのでGoogle Driveでの配布です。
(今後FAT32に対応させたときにGitHubに上げようと思っています)
SPI_Mylib.h / SPI_mylib.c : SPI通信やSDなどに関することが書いてあります。
XXXXX_debug()関数はUARTを使うので、このソースコードはUART_mylib.h/UART_mylib.cが必須になります。
timer_mylib.h / timer_mylib.c : タイマー関連のライブラリです。今のところはdelayしか実装していません。
UART_Mylib.h / UART_Mylib.c UART1でPCにデバッグ文字列を送信するためのライブラリです。今回はこれも使ってください。
ソースコード - main.c
前提として、回路図通りの配線をしていると仮定しています。
また冒頭にも書きましたが、このプログラムではSDSC(2GB以下)の製品に関しては読み込めません。
ここはmain.cに書き込みます。
ここは共通のコードです。
#include "ch32v20x.h"
#include <stdio.h>
#include <stdlib.h>
#include "SPI_Mylib.h"
#include "UART_Mylib.h"
#define STR_BUF 512
void Binary_512byte_print(const uint8_t data[]);
//HSI(8MHz)を使用
//PLL x18により
//SYSCLK 144MHz
//HCLK 144MHz
//APB1 144MHz
//APB2 144MHz
//ADC 72MHz
//(状況により変更あり)
void osc_144MHz_init(void) {
//PLL x18に設定
RCC->CFGR0 |= (1 << 21);
RCC->CFGR0 |= (1 << 20);
RCC->CFGR0 |= (1 << 19);
RCC->CFGR0 |= (1 << 18);
//PLL HSI->そのまま入力 (/2しない)
EXTEN->EXTEN_CTR |= (1 << 4);
//PLL有効 (input clock x 18)
RCC->CTLR |= (1 << 24);
//PLLがReadyになるまで待機
while((RCC->CTLR & (1 << 25)) == 0);
//システムクロックをPLL入力にセット
RCC->CFGR0 |= (1 << 1);
}
int main(void){
osc_144MHz_init();
SysTick_init();
RCC->APB2PCENR |= RCC_IOPAEN;
RCC->APB2PCENR |= RCC_AFIOEN;
SD_SS_HIGH();
//PA9 UART1 TX
//Alternate Function Push-pull
GPIOA->CFGHR |= GPIO_CFGHR_MODE9;
GPIOA->CFGHR |= GPIO_CFGHR_CNF9_1;
GPIOA->CFGHR &= ~GPIO_CFGHR_CNF9_0;
//PA10 UART1 RX
//Floating Input
GPIOA->CFGHR &= ~GPIO_CFGHR_MODE10;
GPIOA->CFGHR &= ~GPIO_CFGHR_CNF10_1;
GPIOA->CFGHR |= GPIO_CFGHR_CNF10_0;
//PA4 SPI1 SS (SD)
//General Push-pull Output
GPIOA->CFGLR |= GPIO_CFGLR_MODE4;
GPIOA->CFGLR &= ~GPIO_CFGLR_CNF4;
//PA5 SPI1 CLK
//Alternate Function Push-pull
GPIOA->CFGLR |= GPIO_CFGLR_MODE5;
GPIOA->CFGLR |= GPIO_CFGLR_CNF5_1;
GPIOA->CFGLR &= ~GPIO_CFGLR_CNF5_0;
//PA6 SPI1 MISO
//Floating Input
GPIOA->CFGLR &= ~GPIO_CFGLR_MODE6;
GPIOA->CFGLR &= ~GPIO_CFGLR_CNF6_1;
GPIOA->CFGLR |= GPIO_CFGLR_CNF6_0;
//PA7 SPI1 MOSI
//Alternate Function Push-pull
GPIOA->CFGLR |= GPIO_CFGLR_MODE7;
GPIOA->CFGLR |= GPIO_CFGLR_CNF7_1;
GPIOA->CFGLR &= ~GPIO_CFGLR_CNF7_0;
us_delay(100*1000); //100ms待機
UART1_Setting(9600);
SPI1_init();
//////この下に書いていく///////////
//////////////////////////////////
while(1){
}
}
void Binary_512byte_print(const uint8_t data[]){
printf(" 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F\r\n");
for(int i = 0; i < 32;i++){
printf("%02x :", i);
for(int j = 0; j < 16; j++){
printf("%02x ", data[16 * i + j]);
}
printf("\r\n");
}
}
これから下に書くコードは、
//////この下に書いていく///////////
//////////////////////////////////
の中に入れてください。
初期化 + 読み込み
初期化と読み込みのセットです。
uint8_t data[512];
SPI1_LowSpeed();
SD_init_debug();
SPI1_HighSpeed();
SD_Read_debug(0,data);
Binary_512byte_print(data);
SDを差し込んで実行すると、ログはこんな感じです。

今回は0x00000000番地を読み込んだので、MBR領域を読み込んだことになります。
ちなみに、関数に XXXX_debug();とついてますが、debugを消せばログは出なくなります。(要は高速化します)
初期化 + 書き込み + 読み込み
この事項は書き換えるアドレスによってはデータ破損することがあります。
uint8_t write_data[512];
uint8_t read_data[512];
SPI1_LowSpeed();
SD_init_debug();
SPI1_HighSpeed();
for(int i = 0; i < 512; i++){
write_data[i] = i % 0x100;
}
SD_Write_debug(0x123123, write_data);
SD_Read_debug(0x123123,read_data);
Binary_512byte_print(read_data);

書き換え&読み出しができていることがわかると思います。
データ容量確認
これは一部のソフトウェアが必要とするデータ容量を確認する方法です。

CMD9のデータ容量は1024ブロックを1と返すため、関数内部で1024をかけています。
1ブロック512byteですから、61021184 * 512 = 31.24*10^9 ということになります。
この値を31.24*10^9 / 1024/1024/1024すると29.09GiBになります。
ということで、約32GBということですね。
読み込めてよかった
ちょっと前にやったときは全然うまくいかなかった記憶があったのですが、ちゃんとうまくいってよかったです。
やはりSSをHIGHにした後の0xFF送信が非常に大切なようです。
手持ちに2GB以下のSDがなかったので対応できませんでしたが、もし持っていたら今後やってみようかなと思います。(SDとSDHC/SDXCはコマンドがかなり違うので対応化は面倒ではあるのですが)
次はFAT32に対応させてみようと思います。(と言っているのですが、実はこの記事を書いている時点で実装できています)