MMC FLASH Cards

Accessing MMC from a PC parallel port with Delphi Source code

Accessing MMC from a PC parallel port with Delphi Source code

By David Beals [DBeals at transdyn.com]

screen shot "mmcscrnshot.rtf" of the Delphi program displaying the contents of MMC block zero, showing the DOS FAT header, with the legacy DOS error messages displayed.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

MMC.PL

#!/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;
}

#--------------------------------------------------

MMC.PAS

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: