By David Beals [DBeals at transdyn.com]
A 16 MB MMC card is plugged into a simple logic level translator and into the parallel port of a PC.
A Delphi 2 program operates the card. The program initializes the card to SPI mode.
It can then read registers, like the CSD, "Card Specific Device", from which the card capacity can be derived.
The included file "readResults.txt" shows the results of reading the CID, CSD and sector at address 0 of a sample MMC.
(A Perl script "mmc.pl" is included that demonstrates decoding the card size from the CSD. That was not done in the Delphi code.)
After init, Delphi can read any individual data block and display the contents.
Or it can read consecutive data blocks continuously until an error is returned, hopefully signalling the end of the card's storage.
It can demo a simple block write, where a few hardcoded characters are written to a hardcoded data block.
The block write performs an implicit erase; the program does not attempt any explicit block or multiblock erases, although it should be able to do those.
Later I'll forward a schematic and photo of the MMC logic translator board. But basically, it consists of a string of diodes that provides about 3.3 volts from a 5 volt supply. Three PC parallel port outputs go to three Schmidt trigger inverters (a CMOS 74HCT14) which run three transistors running from the 3.3 volts, which drive three MMC input lines. One MMC output directly drives a Schmidt trigger which feeds an input bit of the parallel port.
Disclaimers - I'm a DBA, not an electrical engineer! The logic inverter seems to work fine but is a slapped-together design. Also, I would not call myself a software engineer; the Delphi and Perl seem to do what they are supposed to but were not designed to be any sort of finished product; they are slapped together, too! This was all just for fun! So do with it what you will!
David Beals
Dec. 3, 2004
#!/usr/bin/perl -w #------------------------------------------------------------------------ # mmc.pl # # Demo of decoding the memory size from the bit array from # a CSD register of an MMC card. # # This follows the algorithm on page 3-15 of the PDF document # "MultiMediaCard Product Manual" rev 5.1, SanDisk corp. # Perl uses the same mnemonics like "MULT", ... as SanDisk, # to help follow this convoluted decoding process. # # card specific device register: 16 bytes, CRC and trailing FFs: # CSD: 48,0E,01,2A,0F,F9,81,EA,EC,B1,01,E1,8A,40,00,BB,82,9C,FF,FF # # Results of running this script for 16M card: 16089088 = 0xF58000 bytes. # # This agrees with experimental reads until an out-of-bounds error # is returned - success in accessing from 00000000 to 00F57E00, then # the read returns an error, as expected. # #------------------------------------------------------------------------ use strict; my(@csd, $memcap); my($MULT, $BLOCK_LEN, $READ_BL_LEN, $BLOCK_NR, $C_SIZE, $C_SIZE_MULT); @csd = (72,14,01,42,15,249,129,234,236,177,01,225,138,64,00,187); $C_SIZE_MULT = &bits(49, 47); $MULT = $C_SIZE_MULT << 3; $READ_BL_LEN = &bits(83, 80); $BLOCK_LEN = 1 << $READ_BL_LEN; $C_SIZE = &bits(73, 62); $BLOCK_NR = ($C_SIZE+1) * $MULT; $memcap = $BLOCK_NR * $BLOCK_LEN; print "Mem cap: $memcap\n"; #------- end of main ------------------------------ # # Return the number consisting of the specified bits sub bits{ my($i, $start, $end, $result, $shift, $bit); $start = $_[0]; $end = $_[1]; $result = 0; $i = $start; $shift = $start - $end; while($i >= $end){ $bit = &bit($i); $result = $result + ($bit << $shift); $i--; $shift--; } $result; } #-------------------------------------------------- # # Return the bit (0 or 1) at the argument location from the CSD array sub bit{ my($bitofbyte, $bit, $byte, $bytenumber, $bitnumber); $bitnumber = $_[0]; $bytenumber = 15 - (int($bitnumber/8)); $byte = $csd[$bytenumber]; $bitofbyte = 7 - ((127 - $bitnumber) - (8 * $bytenumber)); $bit = ($byte >> $bitofbyte) & 1; if ($bit > 0){ $bit = 1; } $bit; } #--------------------------------------------------
unit mmc; // access MMC adaptor plugged into parallel port // // First init the MMC, then do whatever you want - read and // display the contents of any single sector, read sectors // until there aren't any more, write a sample sector, read // the contents of several registers. // You can re-init the MMC at any time, if you feel like it. // I cannot guarantee that this is exactly correct in the way that // it operates the MMC but it seems to work flawlessly at the moment... interface uses Windows, Messages, SysUtils, Classes, Controls, Forms, StdCtrls; type TForm1 = class(TForm) Button1: TButton; Edit2: TEdit; Button2: TButton; Memo1: TMemo; Button9: TButton; Button5: TButton; Button8: TButton; Button10: TButton; Button11: TButton; Edit1: TEdit; Edit3: TEdit; Button21: TButton; Edit4: TEdit; Button15: TButton; Button17: TButton; Edit5: TEdit; Button3: TButton; procedure FormCreate(Sender: TObject); procedure Button1Click(Sender: TObject); procedure Button2Click(Sender: TObject); procedure Button5Click(Sender: TObject); procedure Button8Click(Sender: TObject); procedure Button9Click(Sender: TObject); procedure Button10Click(Sender: TObject); procedure Button11Click(Sender: TObject); procedure Button21Click(Sender: TObject); procedure Button15Click(Sender: TObject); procedure Button17Click(Sender: TObject); procedure Button3Click(Sender: TObject); procedure CShi; procedure CSlo; procedure DThi; procedure DTlo; procedure CKhi; procedure CKlo; procedure readRegister(reg: byte; length: byte); procedure CloseCommand; procedure initMMC; procedure writeSector; procedure outb(Value: Byte); function inb: Byte; function dataResponse: byte; function sendgetByte(b: byte): byte; function Command(Cmd: byte; address: longint):byte; function readSector(address: longint): byte; private { Private declarations } public { Public declarations } end; var Form1: TForm1; byte378: Word; Globaladdress: longint; Globalbuffer: array [1..640] of byte; implementation {$R *.DFM} //------------------------------------------------ procedure TForm1.FormCreate(Sender: TObject); begin // Initialize the ports to the same value // as the hardware boots, so the MMC interface control bits // have no surprises when the program starts. byte378 := 0; outb(byte378); end; //----------------------------------------------------- // this has to be the first thing to run on the MMC procedure TForm1.initMMC; var res, i: word; begin CShi; for i := 1 to 10 do begin // send 80 clocks sendgetByte($FF); end; Command(0, 0); // Init into SPI mode sendgetByte($FF); // finish clocking this command CShi; i := 0; res := 1; while ((i < 100) and (res <> 0)) do begin Inc(i); res := Command($1, 0); end; CShi; sendgetByte($FF); end; //------------------------------------------------------------ // read sector at address. // return: // 0 on success. // 1 command error // 2 data error function TForm1.readSector(address: longint): byte; var res: byte; i: word; begin res := Command(17, address); if ((res <> $FE) and (res <> 0)) then begin Result := 1; exit; end; res := dataResponse; if (res = $FF) then begin Result := 2; exit; end; for i := 1 to 514 do globalBuffer[i] := sendgetByte($FF); CloseCommand; Result := 0; end; //----------------------------------------------------- // Send command. Command ranges from 0 to like 30 or so. // Command 0 returns no response, and requires genuine CRC (always $95) // Only the middle two bytes of the 32 bit address are non-zero. function TForm1.Command(Cmd: byte; address: longint): byte; var i, addr1, addr2, CRC: byte; begin Result := 0; if (Cmd = 0) then CRC := $95 else CRC := $FF; addr1 := ((address and $FF00) shr 8); addr2 := ((address and $FF0000) shr 16); CSlo; sendgetByte($40 or Cmd); sendgetByte($00); sendgetByte(addr2); sendgetByte(addr1); sendgetByte($00); sendgetByte(CRC); if (Cmd = 0) then exit; // after sending the command, get the response to this command. Result := $FF; i := 0; while ((i < 100) and (Result = $FF)) do begin Result := sendgetByte($FF); Inc(i); end; end; //------------------------------------------------------------ // Wait for the command to be processed. FF means busy. // Processing complete is acknowledged with "FE". function TForm1.dataResponse: byte; var i: word; begin Result := $FF; i := 0; while ((i < 1000) and (Result = $FF)) do begin Result := sendgetByte($FF); Inc(i); end; // Did we get the expected FE? if (Result <> $FE) then begin Edit1.text := 'Received error: ' + IntToStr(Result); Memo1.Lines.Add( 'error received after ' + IntToStr(i) + ' rcv loops'); end; end; //----------------------------------------------------- function TForm1.sendGetByte(b: byte): byte; var i, value: byte; begin value := 0; // the incoming bits arrive MSB first. for i := 0 to 7 do begin // (pre-shift the incoming data so the final bit is not shifted) value := value shl 1; // the MMC data bit connects to bit 4 (at $10) of the parallel port if ((inb and $10) = $10) then value := value or 1; // place the outgoing bit onto the data line if ((b and $80) = 0) then DTlo else DThi; b := b shl 1; CKhi; CKlo; end; Result := value; end; //--------------------------------------------------------- // The low-level bit controls procedure TForm1.outb(Value: Byte); begin asm mov al, Value mov dx, $378 out dx, al end end; function TForm1.inb: Byte; begin asm mov dx, $379 in al, dx mov @result, al end end; procedure TForm1.CSlo; begin byte378 := byte378 and not 1; outb(byte378); end; procedure TForm1.CShi; begin byte378 := byte378 or 1; outb(byte378); end; procedure TForm1.DTlo; begin byte378 := byte378 and not 2; outb(byte378); end; procedure TForm1.DThi; begin byte378 := byte378 or 2; outb(byte378); end; procedure TForm1.CKlo; begin byte378 := byte378 and not 4; outb(byte378); end; procedure TForm1.CKhi; begin byte378 := byte378 or 4; outb(byte378); end; //-------------------------------------------------- // various MMC register read commands procedure TForm1.Button5Click(Sender: TObject); // CSD begin readRegister(9, 16); end; procedure TForm1.Button8Click(Sender: TObject); // CID begin readRegister(10, 16); end; procedure TForm1.Button11Click(Sender: TObject); // OCR begin readRegister(58, 5); end; procedure TForm1.Button3Click(Sender: TObject); // CMD_STATUS begin readRegister(13, 2); end; //-------------------------------------------------- procedure TForm1.readRegister(reg: byte; length: byte); var val: array [1..64] of byte; res, i,j: word; s: string; begin res := command(reg, 0); if ((res <> 254) and (res <> 0)) then begin Edit1.text := 'Received cmd error: ' + IntToStr(res); exit; end; res := dataResponse; if (res <> 254) then begin Edit1.text := 'Received data error: ' + IntToStr(res); exit; end; // Registers vary from 4 to 16 bytes. // Read more to read 2 byte CRC and two $FF to make sure we're at the end. i := 1; for j := 1 to length+4 do begin val[j] := sendgetByte($FF); i := j; end; CShi; sendgetByte($FF); Edit1.text := 'Break at index ' + IntToStr(i) + ' because result is ' + IntToStr(res); // Done MMC process. Display the results Memo1.clear; for i := 1 to length+4 do begin s := 'index ' + IntToStr(i) + ' ' + IntToHex(val[i], 2); if ((val[i] > 20) and (val[i] < 128)) then s := s + ' ' + char(val[i]); Memo1.Lines.Add(s); end; end; //----------------------------------------------------- // initialize the MMC to SPI mode and set GUI global address procedure TForm1.Button2Click(Sender: TObject); var addr1, addr2: word; begin initMMC; Memo1.clear; Memo1.Lines.Add('MMC initialized'); addr2 := StrToInt(Edit4.text); addr1 := StrToInt(Edit3.text); Globaladdress := addr2 shl 16 + addr1 shl 8; end; //------------------------------------------------------------ procedure TForm1.Button9Click(Sender: TObject); // read specific sector var res: byte; s, msg: string; i, line, ptr: word; addr1, addr2: word; begin Memo1.clear; addr2 := StrToInt(Edit4.text); addr1 := StrToInt(Edit3.text); Globaladdress := addr2 shl 16 + addr1 shl 8; msg := 'read sector ' + Edit4.text + Edit3.text + Edit2.text; msg := msg + ' address ' + IntToHex(Globaladdress, 4); Memo1.Lines.Add(msg); readSector(Globaladdress); ptr := 1; for i := 1 to 16 do begin s := ''; for line := 1 to 32 do begin res := globalBuffer[ptr]; Inc(ptr); if ((res > 20) and (res < 128)) then s := s + ' ' + char(res) else s := s + IntToHex(res, 2); end; Memo1.Lines.Add(s); end; Edit1.text := msg; end; //-------------------------------------------------------- procedure TForm1.Button10Click(Sender: TObject); begin Memo1.clear; end; //-------------------------------------------------------- // read next sector procedure TForm1.Button21Click(Sender: TObject); var res: byte; i: word; addr: byte; begin Inc(Globaladdress, 512); addr := ((Globaladdress and $FF00) shr 8); Edit3.text := '$' + IntToHex(addr, 2); addr := ((Globaladdress and $FF0000) shr 16); Edit4.text := '$' + IntToHex(addr, 2); Memo1.clear; Memo1.Lines.Add('Reading address ' + IntToHex(Globaladdress, 4)); i := 0; res := 1; while ((i < 10) and (res <> 0)) do begin res := readSector(Globaladdress); Inc(i); end; end; //------------------------------------------------------------- // write a test sector // 16 meg card: nearly 32768 512-byte sectors (16,777,216 bytes) // = 0x8000 sectors // = a million bytes [ $10 00 00 00 ] procedure TForm1.Button15Click(Sender: TObject); begin writeSector; end; //------------------------------------------------------------- procedure TForm1.writeSector; var res: byte; s, msg: string; i: word; status: byte; addr1, addr2: byte; address: longint; begin Memo1.clear; addr2 := $E2; addr1 := $00; address := addr2 shl 16 + addr1 shl 8; msg := 'write sector 00 E2 00 00'; Memo1.Lines.Add(msg); // This sends only the 6 byte command. res := Command(24, address); if (res > 0) then begin Edit1.text := 'Received error: ' + IntToStr(res); exit; end; // Send the start token "$FE": sendgetByte($FE); // 512 bytes of data follow, then 2 CRC bytes. for i := 1 to 128 do begin sendgetByte(73); sendgetByte(74); sendgetByte(75); sendgetByte(76); end; // send dummy CRC sendgetByte($FF); sendgetByte($FF); // get a response? res := $FF; i := 0; while ((i < 10000) and (res = $FF)) do begin res := sendgetByte($FF); Inc(i); end; // check results: status := res and $F; s := 'Write delay of ' + IntToStr(i); if (status = 5) then s := s + ' Write success' else if (status = $B) then s := s + 'Rejected: CRC error' else if (status = $B) then s := s + 'Rejected: write error'; Edit1.text := s; CloseCommand; end; //--------------------------------------------------- // read continuous procedure TForm1.Button17Click(Sender: TObject); var res: byte; i: word; done: byte; errorcounts: word; begin done := 0; errorcounts := 0; Globaladdress := $00000; while (done = 0) do begin Inc(Globaladdress, 512); i := 0; res := 1; while ((i < 2) and (res <> 0)) do begin res := readSector(Globaladdress); if (res > 0) then begin Memo1.Lines.Add('Read ' + IntToHex(Globaladdress, 4) + ' errs: ' + IntToStr(errorcounts)); Inc(errorcounts); end; Inc(i); end; if (errorcounts > 3) then done := 1; end; // endless loop end; //--------------------------------------------------- procedure TForm1.Button1Click(Sender: TObject); begin halt; end; //--------------------------------------------------- procedure TForm1.CloseCommand; begin sendgetByte($FF); sendgetByte($FF); CShi; sendgetByte($FF); sendgetByte($FF); end; //----------------------------------------------------- end. //------------------------------------------------------------
Comments: