今回は PIC で microSDカードを使用する方法を紹介します。なお、microSDじゃなくて普通のSDカードでも同様にできるはずです。
以下 microSDカードも含めて SDカードと呼ぶことにします。
SDカードのメリットとして大量のデータを保存できることと、PCからも簡単にアクセスできることが挙げられます。
ぷちFatFsについて
SDカードとの通信プログラムを最初から作るのはとても大変ですが、ありがたいことに、SDカードを簡単に扱うことのできるライブラリが公開されています。
ChaN氏のFatFs、そしてそこから機能を限定して縮小したぷちFatFsです。
これらのライブラリは SDカードと直接やり取りする下位レイヤとアプリケーションの上位レイヤに分かれています。
そのため、使用するデバイスに合わせて下位レイヤの関数を作ればアプリケーションではデバイスに依存せず使用できるという特徴があります。
デバイスごとのサンプルプログラムも用意されているのでそれをベースに自分の環境に合わせてカスタマイズします。
ぷちFatFs は機能が制限されているのですが、ファイルの読み書きやブラウズといったことはできるので、基本的にはこちらで十分かと思います。
ライブラリの入手
ChaN氏のぷちFatFs のページ下部にある Resources>Downroad:Petit FatFs R0.03a (2020年9月現在)をクリックして zipファイルをダウンロードして解凍します。
解凍したファイルの内、diskio.c が先程紹介した下位レイヤの関数になっていて、中身は空っぽです。
ライブラリを使用するためにはこのファイルに disk_initialize(), disk_readp(), disk_writep() の3つの関数を記述する必要があります。
そのベースとなるサンプルプログラムも先程のライブラリのすぐ下でダウンロードできます。
※今回 PIC16F18857 に合わせて作成した diskio.c はこちらです。これをそのまま使用もしくは改造してもらって構いません。(開いたページで Ctrl + s で保存できると思います)
SPIの設定
SDカードとの通信には SPI が使われます。SPI についてはこちらの記事も参考にどうぞ。
ChaN氏の How to Use MMC/SDC と About SPI を参考に SPI の設定を行います。
SPI のモードは0または3と書かれているのですが、私が試したところ上手くいきませんでした。
そして理由はよく分かりませんが、モード2(Idle High/ Transmit on transition from active to Idle)で上手くいきました。
SDカードとの通信が上手くいかない場合は SPI の設定をいろいろ試してみると良いかもしれません。
diskio.c の作成
ぷちFatFs を利用するためには diskio.c を完成させる必要があります。
まずはサンプルプログラムを見てみます。
しかし、ダウンロードしたサンプルの PIC のフォルダを見てみると、diskio.c が見当たりません。
ファイルの中身を覗いてみた結果、pic24_mmcp.c にその内容が書かれていました。
これを diskio.c としてプロジェクトにインポートして修正を加えていきました。
L11~L47はデバイスに依存する部分なのでここを修正していきます。
修正点は以下の通りです。
- インクルードファイルの変更
- CSピンの変更
- UART関連の削除
- SPI関連の関数の変更
- 遅延関数の変更
修正後のプログラムはこのようになりました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
#include <xc.h> #define _XTAL_FREQ 32000000 #define CS_LOW() RC7 = 0 /* Set CS low */ #define CS_HIGH() RC7 = 1 /* Set CS high */ #define IS_CS_LOW !RC7 /* Test if CS is low */ static void init_spi (void) /* Initialize SPI port */ { SSP1STAT = 0b00000000; //SMP: middle/ CKE: IdleToActive SSP1CON1 = 0b00110000; //SSPEN/ CKP: Idle High/ SPIMaster SSP1DATPPS = 0x14; // SDI : RC4 RC3PPS = 0x14; // SCK : RC3 RC5PPS = 0x15; // SDO : RC5 } static void dly_100us (void) /* Delay 100 microseconds */ { __delay_us(100); } static void xmit_spi (BYTE d) /* Send a byte to the SDC/MMC */ { char dumy; dumy = SSP1BUF; //Clear buffer SSP1BUF = d; //Send while(!SSP1STATbits.BF); } static BYTE rcv_spi (void) /* Send a 0xFF to the SDC/MMC and get the received byte */ { char dumy; dumy = SSP1BUF; //Clear buffer SSP1BUF = 0xFF; //Send while(!SSP1STATbits.BF); return SSP1BUF; //Receive } |
それに加えて、FORWARD() が使用されていた L220 をコメントアウトしておきます。
続いて send_cmd() を修正する必要があります。この関数の中で再帰呼出しを行っている部分があるのですが、XC8 では再帰呼出しできないので、これを修正します。
send_cmd() を _send_cmd() に変更して新しく send_cmd() を作り、その中で _send_cmd() を2回呼ぶことにしました。
こんがらがりそうですが、実際に見てもらった方が早いと思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
static BYTE _send_cmd( BYTE cmd, /* 1st byte (Start + Index) */ DWORD arg /* Argument (32 bits) */ ){ BYTE n, res; // if (cmd & 0x80) { /* ACMD<n> is the command sequense of CMD55-CMD<n> */ //// cmd &= 0x7F; //// res = send_cmd(CMD55, 0); //// if (res > 1) return res; // *repeat = 1; // *cmd2 = cmd & 0x7F; // return 0xFF; // } /* Select the card */ CS_HIGH(); rcv_spi(); CS_LOW(); rcv_spi(); /* Send a command packet */ xmit_spi(cmd); /* Start + Command index */ xmit_spi((BYTE)(arg >> 24)); /* Argument[31..24] */ xmit_spi((BYTE)(arg >> 16)); /* Argument[23..16] */ xmit_spi((BYTE)(arg >> 8)); /* Argument[15..8] */ xmit_spi((BYTE)arg); /* Argument[7..0] */ n = 0x01; /* Dummy CRC + Stop */ if (cmd == CMD0) n = 0x95; /* Valid CRC for CMD0(0) */ if (cmd == CMD8) n = 0x87; /* Valid CRC for CMD8(0x1AA) */ xmit_spi(n); /* Receive a command response */ n = 10; /* Wait for a valid response in timeout of 10 attempts */ do { res = rcv_spi(); } while ((res & 0x80) && --n); return res; /* Return with the response value */ } static BYTE send_cmd ( BYTE cmd, /* 1st byte (Start + Index) */ DWORD arg /* Argument (32 bits) */ ){ char res; if (cmd & 0x80) { /* ACMD<n> is the command sequense of CMD55-CMD<n> */ cmd &= 0x7F; res = _send_cmd(CMD55, 0); if (res > 1) return res; } res = _send_cmd(cmd, arg); return res; } |
これで diskio.c は完成ですが、最後に pffconf.h の設定をしなければいけません。
pffconf.h の L12 からの部分で使用する関数を設定できます。
ただでさえ機能が限定されたぷちFatFs の中でさらに不要な関数は取り除いてコードサイズを小さくできるというわけです。
今回は lseek と write は使用しないので以下のようにしました。
1 2 3 4 |
#define PF_USE_READ 1 /* pf_read() function */ #define PF_USE_DIR 1 /* pf_opendir() and pf_readdir() function */ #define PF_USE_LSEEK 0 /* pf_lseek() function */ #define PF_USE_WRITE 0 /* pf_write() function */ |
接続
続いてSDカードの接続方法を紹介します。
今回は秋月電子で販売されているmicroSDカードDIP化キットを使用しました。
SDカードを SPI で接続する場合の接続方法はこのようになっています。
気を付けなければならない点は、SDカードの電源は 3.3V であるということです。
5V をかけると SDカードが破損する可能性があるので気を付けましょう。
また、DO (MISO) のラインにはプルアップ抵抗が必要な場合があるそうです。
アプリケーション
ぷちFatFs のライブラリの利用準備が整ったので、その使い方を見ていきましょう。
まずは pff.h, pffconf.h, diskio.h, pff.c, diskio.c をプロジェクトにインポートしますが、main.c からインクルードするのは pff.h だけで大丈夫です。
SDカードを操作する関数については、ぷちFatFs のページに詳しく書いてありますし、直感的で分かりやすいので割愛します。
SDカードのルートディレクトリに ”Hello World” という内容の text.txt を作成しておきました。今回はこのファイルの内容を読み取って LCD に表示させてみたいと思います。
ただし、ファイル名は8.3形式で扱われるので、プログラム中では TEXT.TXT と大文字になっています。ファイル名は9字以上だとルールに従って省略されるので注意してください。
プログラムは以下の通りです。ちなみにデバッグ用としてエラーの内容を表示する die() 関数を用意しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
#pragma config FEXTOSC = OFF // External Oscillator mode selection bits (Oscillator not enabled) #pragma config RSTOSC = HFINT32 // Power-up default value for COSC bits (HFINTOSC with OSCFRQ= 32 MHz and CDIV = 1:1) #pragma config CLKOUTEN = OFF // Clock Out Enable bit (CLKOUT function is disabled; i/o or oscillator function on OSC2) #pragma config CSWEN = ON // Clock Switch Enable bit (Writing to NOSC and NDIV is allowed) #pragma config FCMEN = ON // Fail-Safe Clock Monitor Enable bit (FSCM timer enabled) #pragma config MCLRE = ON // Master Clear Enable bit (MCLR pin is Master Clear function) #pragma config PWRTE = OFF // Power-up Timer Enable bit (PWRT disabled) #pragma config LPBOREN = OFF // Low-Power BOR enable bit (ULPBOR disabled) #pragma config BOREN = OFF // Brown-out reset enable bits (Brown-out reset disabled) #pragma config BORV = LO // Brown-out Reset Voltage Selection (Brown-out Reset Voltage (VBOR) set to 1.9V on LF, and 2.45V on F Devices) #pragma config ZCD = OFF // Zero-cross detect disable (Zero-cross detect circuit is disabled at POR.) #pragma config PPS1WAY = ON // Peripheral Pin Select one-way control (The PPSLOCK bit can be cleared and set only once in software) #pragma config STVREN = ON // Stack Overflow/Underflow Reset Enable bit (Stack Overflow or Underflow will cause a reset) #pragma config WDTCPS = WDTCPS_31// WDT Period Select bits (Divider ratio 1:65536; software control of WDTPS) #pragma config WDTE = OFF // WDT operating mode (WDT Disabled, SWDTEN is ignored) #pragma config WDTCWS = WDTCWS_7// WDT Window Select bits (window always open (100%); software control; keyed access not required) #pragma config WDTCCS = SC // WDT input clock selector (Software Control) #pragma config WRT = OFF // UserNVM self-write protection bits (Write protection off) #pragma config SCANE = available// Scanner Enable bit (Scanner module is available for use) #pragma config LVP = OFF // Low Voltage Programming Enable bit (High Voltage on MCLR/Vpp must be used for programming) #pragma config CP = OFF // UserNVM Program memory code protection bit (Program Memory code protection disabled) #pragma config CPD = OFF // DataNVM code protection bit (Data EEPROM code protection disabled) #include <xc.h> #include <stdio.h> #include "i2c.h" #include "lcd_i2c.h" #include "pff.h" #define _XTAL_FREQ 32000000 void Init(){ ANSELA = 0; TRISA = 0; ANSELB = 0; TRISB = 0b00000110; ANSELC = 0; TRISC = 0b00010000; } void putch(char c){ lcd_DATA(c); } void die(char mes, char val){ lcd_SetCursor(0,1); lcd_DATA(mes); printf("%d",val); while(1); } void main(void) { Init(); I2C_Master_Init(400000); lcd_Init(); FATFS fs; FRESULT res; BYTE buf[16]; UINT br; res = pf_mount(&fs); if(res) die('m',res); res = pf_open("TEXT.TXT"); if(res) die('o',res); res = pf_read(buf, 16, &br); if(res) die('r',res); lcd_SetCursor(0,0); printf(buf); while(1); } |
I2Cライブラリ
1 2 3 4 5 6 7 8 9 10 |
void I2C_Master_Init(const unsigned long c); void I2C_Master_Wait(); void I2C_Master_Start(); void I2C_Master_RepeatedStart(); void I2C_Master_Stop(); void I2C_Master_Write(unsigned d); unsigned short I2C_Master_Read(unsigned short a); void SendI2C(char adrs, char data); void CmdI2C(char adrs, char reg, char data); void GetDataI2C(char adrs, char* buf, char cnt); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
#include<xc.h> #define _XTAL_FREQ 32000000 void I2C_Master_Init(const unsigned long c){ SSP2CON1 = 0x28; //SSP2 Module as Master SSP2CON2 = 0x00; SSP2CON3 = 0x00; SSP2ADD = (_XTAL_FREQ/(4*c))-1; //Setting Clock Speed SSP2STAT = 0x80; SSP2CLKPPS = 0x09; //PPS Settings SSP2DATPPS = 0x0A; // SCL : RB1 RB1PPS = 0x16; // SDA : RB2 RB2PPS = 0x17; } void I2C_Master_Wait(){ while ((SSP2STAT & 0x04) || (SSP2CON2 & 0x1F)); //Transmit is in progress } void I2C_Master_Start(){ I2C_Master_Wait(); SSP2CON2bits.SEN = 1; //Initiate start condition } void I2C_Master_RepeatedStart(){ I2C_Master_Wait(); SSP2CON2bits.RSEN = 1; //Initiate repeated start condition } void I2C_Master_Stop(){ I2C_Master_Wait(); SSP2CON2bits.PEN = 1; //Initiate stop condition } void I2C_Master_Write(unsigned d){ I2C_Master_Wait(); SSP2BUF = d; //Write data to SSP2BUF } unsigned short I2C_Master_Read(unsigned short a){ unsigned short temp; I2C_Master_Wait(); SSP2CON2bits.RCEN = 1; I2C_Master_Wait(); temp = SSP2BUF; //Read data from SSP2BUF I2C_Master_Wait(); SSP2CON2bits.ACKDT = a; //Acknowledge bit 1:Not Ack 0:Ack SSP2CON2bits.ACKEN = 1; //Acknowledge sequence return temp; } void SendI2C(char adrs, char data){ I2C_Master_Start(); I2C_Master_Write(adrs<<1); //SlaveAdress + Write I2C_Master_Write(data); //mainly RegisterAdress I2C_Master_Stop(); } void CmdI2C(char adrs, char reg, char data){ I2C_Master_Start(); I2C_Master_Write(adrs<<1); //SlaveAdress + Write I2C_Master_Write(reg); //RegisterAdress I2C_Master_Write(data); I2C_Master_Stop(); } void GetDataI2C(char adrs, char* buf, char cnt){ I2C_Master_Start(); I2C_Master_Write((adrs<<1)+1); //SlaveAdress + Read for(char i=0; i<cnt-1; i++) buf[i] = I2C_Master_Read(0); //ACK buf[cnt-1] = I2C_Master_Read(1); //NACK I2C_Master_Stop(); } |
LCDライブラリ
1 2 3 4 |
void lcd_INST(char command); void lcd_DATA(char data); void lcd_Init(); void lcd_SetCursor(char x,char y); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
#include "i2c.h" #include <xc.h> #define _XTAL_FREQ 32000000 #define CONTRAST 0x20 //for 3.3V //#define CONTRAST 0x08 //for 5V const char lcd_DDRAM[4] = {0x00, 0x40, 0x14, 0x54}; void lcd_INST(char command){ I2C_Master_Start(); I2C_Master_Write(0x7C); //SlaveAdress + Write I2C_Master_Write(0x00); //Instruction I2C_Master_Write(command); //data byte I2C_Master_Stop(); __delay_us(30); } void lcd_DATA(char data){ I2C_Master_Start(); I2C_Master_Write(0x7C); //SlaveAdress + Write I2C_Master_Write(0x40); //Data I2C_Master_Write(data); //data byte I2C_Master_Stop(); __delay_us(30); } void lcd_Init(){ __delay_ms(40); lcd_INST(0x38); //Function Set lcd_INST(0x39); //Function Set lcd_INST(0x14); //Internal OSC Frequency lcd_INST(0x70 + (CONTRAST & 0x0f)); //Contrast Set lcd_INST(0x54 + ((CONTRAST & 0xf0)>>4)); //Power/ICON/Contrast control lcd_INST(0x6c); //Follower control __delay_ms(200); lcd_INST(0x38); //Function Set lcd_INST(0x0c); //Display ON/OFF control lcd_INST(0x01); //Clear Display __delay_ms(2); } void lcd_SetCursor(char x,char y){ char d = lcd_DDRAM[y] + x; lcd_INST( 0x80+d); } |
回路図
きちんと SDカードの中身を読み取ることができました。
今回の記事は以上となります。ご覧いただきありがとうございました。