An attempt to mount an SD/MMC connected at parallel port of PC (a linux device driver)

Introduction:

Hi, for the last few days, I was engaged on a mad project, ie I was trying to access as SD card directly via parallel port of my desktop PC with linux. At first, I successfully did the SD/MMC initialization and tested it by reading and writing few sectors. Later I got a simple demo block driver code (kernel module) from net. Then I modified it and used my SD card code along with it and made a driver for the parallel port - SD/MMC card. Then after connecting the card to parallel port and inserting the module, a new device file is generated (/dev/sbd). After that I successfully mounted it and accessed it similar to that of a usb pendrive or any other external memory device. I have tested it with 64MB MMC, 512MB SD and a 1GB MMC and all worked fine.. But the only drawback is the speed of data transfer is around 200kbps only..:-( It takes 1 minute for transferring 1.5MB file to or from the card. Also the entire system may get hanged while the data is transferring...
But any way, it worked, that's enough for me..... :-)






Working:
I have already done an MMC wav player few months ago in which I did all the low level MMC accessing codes. Then I just modified the code to adapt it with the parallel port. Here, I have implemented a software SPI (bit banging) on few I/O pins of parallel port. Also I made 3 additional functions ie an n-sector multiple read and write function (which is required in a block device driver) and a function to find the maximum size of the SD/MMC. Here I used "inb" and "outb" to control the pins used for SPI.

to write 0xAA to an 8 bit port address 0x378 of the parallel port,
 I can use 

outb(0xAA, 0x378);

The main problem I observed is that the above operation will take more time to get completed. Similarly the "inb" also.. I have tested a bit toggling on one pin of parallel port using below loop:

while(1) {
     outb(0x00, 0x378);
     outb(0xff, 0x378);
}

It generates a square wave of around 350KHz, so I can confirm that the maximum possible SPI clock frequency is about 350KHz, but since the SPI is a software one, the effective SPI clock will be less compared to the maximum possible value obtained from the above while loop... So this will be the bottleneck in this project because SD/MMC can support around 20MHz or greater(depends on the type) but here we cannot make such a high spi clock using parallel port and thus the data transfer speed will be less. In my case, the speed is around 20 to 30KB/sec..;-)

Now, I have already explained in my old post that how to use as MMC card in SPI mode, It's initialization, reading, writing etc etc. I don't want to repeat the contents posted there. So Here is the link towards my old blog post, where I used a PIC microcontroller to access an MMC card. SD card (both SD and SDHC) initialization is little bit different from that of MMC (in few commands). I didn't included an SD/SDHC initialization in my old post. But I have included it in this project. The method to find the size of SDHC is little bit different compared to that of SD/MMC since it uses 22 bits for C_SIZE. You can notice it in the function read_card_size() in my sd.c code. Software SPI is very easy to implement. But it will not be efficient like a hardware SPI module seen in microcontroller and such devices. You can see spi_write(xx) inside SD.c which shows the software SPI implementation.
      Now, here is the link towards a sample block device driver code, which I used as a reference. In that example code, there are two functions ie,

memcpy(dev->data + offset, buffer, nbytes);
memcpy(buffer, dev->data + offset, nbytes);

Which copies the data from buffer to disk and vice versa on a request. So, I replaced this two functions with my own functions(as below) which do the job of reading and writing to the memory card.  This functions are there in my SD.c file.

mmc_write_multiple_sector(sector, buffer, nsect);
mmc_read_multiple_sector(sector, buffer, nsect);

Also I made a function to find the size of the inserted SD/MMC card, it is done by reading the 16byte CSD of the card and then extracting the size information from the bit field.... 
To know more about block device drivers or linux kernel programming, you can refer above link or can find any alternate source because I am not so good in kernel programming and I am just a beginner...;-)

 Source code:
sd.h

#define MOSI 7  //378h
#define MISO 3  //379h
#define SCK 2  //37ah
#define CHIP_SELECT 3  //37ah
#define MMC_NO_RESPONSE -1

#define SET_CHIPSELECT outb(inb(0x37a) & ~(1<<CHIP_SELECT), 0x37a)
#define CLEAR_CHIPSELECT outb(inb(0x37a) | (1<<CHIP_SELECT), 0x37a)
#define spi_read() spi_write(0xff)
#define SPI_CLOCK outb(~(1<<SCK) & 0x0f, 0x37a);outb((~0x00) & 0x0f, 0x37a)
#define CHECK_MISO (inb(0x379) & 1<<MISO)
#define SET_MOSI outb(1<<MOSI,0x378)
#define CLEAR_MOSI outb(0<<MOSI,0x378)
#define SPI_CLOCK_initial outb(inb(0x37a) & ~(1<<SCK), 0x37a);outb(inb(0x37a) | 1<<SCK , 0x37a)

unsigned char spi_write(unsigned char);
unsigned char command(unsigned char, uint32_t, unsigned char);
int mmc_init();
void spi_write_32(uint32_t argument);
int mmc_read_multiple_sector(unsigned long int sector, unsigned char *buffer, unsigned int nsect);
int mmc_write_multiple_sector(unsigned long int sector, unsigned char *buffer, unsigned int nsect);
unsigned long int read_card_size(void); 
 
sd.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/unistd.h>
#include <linux/delay.h>
#include <asm/io.h>
#include "sd.h"

unsigned long int arg = 0;
char IS_SDHC;
unsigned long int read_card_size(void)
{
    int i,j,k;
    int count = 0;
    unsigned int C_SIZE = 0;
    unsigned int BLOCK_LEN = 0;
    unsigned long int BLOCKNR = 0;
    unsigned int MULT = 0;
    unsigned int C_SIZE_MULT = 0;
    unsigned int READ_BL_LEN = 0;
    unsigned char bf[200];
    unsigned char csd_byte = 0;
    if (command(9, 1,0xff) != 0)
    while(spi_read() != 0 && count++ <100);
    if(count < 90){
        count = 0;
        while(spi_read() != 0xfe && count++ <100);
        if(count >90) {printk(KERN_ALERT "NO TOCKEN!!!!\n"); return 0;}
        printk(KERN_ALERT "GOTTT TOCKEN!!!!\n");
        k = 127;
        for(i = 0; i < 16; i++) {
            csd_byte = spi_read();
            for(j = 0; j < 8; j++) {
                if(csd_byte & (1<<7))
                bf[k--] = 1;
                else
                bf[k--] = 0;
                csd_byte <<= 1;
            }
        }
        if(!IS_SDHC) {
            for(i = 0; i < 128; i++) {
                printk(KERN_ALERT "%d - %c", i, bf[i]);
            }
            for(i = 83; i>=80; i--) {
                READ_BL_LEN <<= 1;
                if(bf[i]) READ_BL_LEN++;
            }
            for(i = 73; i>=62; i--) {
                C_SIZE <<= 1;
                if(bf[i]) C_SIZE++;
            }
            for(i = 49; i>=47; i--) {
                C_SIZE_MULT <<= 1;
                if(bf[i]) C_SIZE_MULT++;
            }
            //printk("READ_BL_LEN = %d\n",READ_BL_LEN); //USED FOR TESTING WHILE CODING
            //printk("C_SIZE = %d\n",C_SIZE);
            //printk("C_SIZE_MULT = %d\n",C_SIZE_MULT);
            MULT = 1;
            for(i = 0; i < C_SIZE_MULT + 2; i++)
            MULT *= 2;
            BLOCKNR = (C_SIZE+1)*MULT;
            BLOCK_LEN = 1;
            for(i = 0; i<READ_BL_LEN; i++)
            BLOCK_LEN *= 2;
            //printk("MULT = %d\n",MULT); //USED FOR TESTING WHILE CODING
            //printk("BLOCK_LEN = %d\n",BLOCK_LEN);
            //printk("BLOCKNR = %d\n",BLOCKNR);
            return (BLOCKNR*BLOCK_LEN);
            } else {
            for(i = 69; i>=48; i--) {
                C_SIZE <<= 1;
                if(bf[i]) C_SIZE++;
            }
            return ( (((unsigned long int)C_SIZE +1)*512)*1024 );
        }
    }
    return 0;
}


int mmc_write_multiple_sector(unsigned long int sector, unsigned char *buffer, unsigned int nsect)
{
    int i, cnt;
    sector *= 512;
    if(command(25, sector, 0xff) != 0)
    while (spi_read() != 0 );
    
    while(nsect) {
        spi_write(0xff);
        spi_write(0xff);
        spi_write(0b11111100); //write token
        for(i = 0; i < 512; i++)
        spi_write(*buffer++);
        spi_write(0xff);
        spi_write(0xff);
        while (((spi_read() & 0b00011111) != 0x05));
        while ((spi_read() != 0xff));
        nsect--;
    }
    spi_write(0xff);
    spi_write(0xff);
    spi_write(0b11111101); //stop token
    spi_write(0xff);
    while ((spi_read() != 0xff));
    return 0;
}

int mmc_read_multiple_sector(unsigned long int sector, unsigned char *buffer, unsigned int nsect)
{
    int i, cnt;
    sector *= 512;
    cnt = 0;
    if(command(18, sector, 0xff) != 0 )
    while (spi_read() != 0 && cnt++ <1000 );
    if(cnt >980) return MMC_NO_RESPONSE;
    cnt = 0;
    while(nsect) {
        while (spi_read() != 0xfe && cnt++ <1000 );
        if(cnt >980) return MMC_NO_RESPONSE;
        cnt = 0;
        for(i = 0; i < 512; i++)
        *buffer++ = spi_read();
        spi_write(0xff);
        spi_write(0xff);
        nsect--;
    }
    if(command(12, sector, 0xff) != 0)
    while (spi_read() != 0 && cnt++ <1000 );
    if(cnt >980) return MMC_NO_RESPONSE;
    cnt = 0;
    while (spi_read() != 0xff && cnt++ <1000 );
    if(cnt >980) return MMC_NO_RESPONSE;
    return 0;
}

/* //SINGLE SECTOR READING CODE, NOT USED HERE
void mmc_read_sector(unsigned int sector)
{
    int i;
    sector *= 512;
    if(command(17, sector, 0xff) != 0)
    while (spi_read() != 0);
    while (spi_read() != 0xfe);
    for(i = 0; i < 512; i++)
    mmc_buf[i] = spi_read();
    spi_write(0xff);
    spi_write(0xff);
}
*/

int mmc_init()
{
    int u = 0;
    unsigned int count;
    //clear_pin(LP_PIN[16]);
    unsigned char ocr[10];
    SET_CHIPSELECT;
    for (u = 0; u < 50*8; u++) {
        SPI_CLOCK_initial;
    }
    CLEAR_CHIPSELECT;
    msleep(1);
    count = 0;
    while (command(0, 0, 0x95) != 1 && (count++ < 100));
    if (count > 90) {
        printk( KERN_ALERT "CARD ERROR-CMD0 \n");
        msleep(10);
        return 1;
    }
    //printk( KERN_ALERT "CMD0 PASSED!\n");
    if (command(8, 0x1AA, 0x87) == 1) { /* SDC ver 2.00 */
        for (u = 0; u < 4; u++) ocr[u] = spi_read();
        if (ocr[2] == 0x01 && ocr[3] == 0xAA) { /* The card can work at vdd range of 2.7-3.6V */
            count = 0;
            do
            command(55,0,0xff);
            while (command(41, 1UL << 30, 0xff) && count++ < 1000); /* ACMD41 with HCHIP_SELECT bit */
            if(count > 900) {printk( KERN_ALERT "ERROR SDHC 41"); return 1;}
            count = 0;
            if (command(58, 0, 0xff) == 0 && count++ <100) { /* Check CCHIP_SELECT bit */
                for (u = 0; u < 4; u++) ocr[u] = spi_read();
            }
            IS_SDHC = 1;
        }
        } else {
        command(55, 0, 0xff);
        if(command(41, 0, 0xff) >1) {
            count = 0;
            while ((command(1, 0, 0xff) != 0) && (count++ < 100));
            if (count > 90) {
                printk( KERN_ALERT "CARD ERROR-CMD1 \n");
                msleep(10);
                return 1;
            }
            } else {
            count = 0;
            do {
                command(55, 0, 0xff);
            } while(command(41, 0, 0xff) != 0 && count< 100);
        }
        
        if (command(16, 512, 0xff) != 0) {
            printk( KERN_ALERT "CARD ERROR-CMD16 \n");
            msleep(10);
            return 1;
        }
    }
    printk( KERN_ALERT "CARD INITIALIZED!\n");
    return 0;
}

unsigned char command(unsigned char command, uint32_t fourbyte_arg, unsigned char CRCbits)
{
    unsigned char retvalue,n;
    spi_write(0xff);
    spi_write(0b01000000 | command);
    spi_write_32(fourbyte_arg);
    spi_write(CRCbits);
    
    n = 10;
    do
    retvalue = spi_read();
    while ((retvalue & 0x80) && --n);
    
    return retvalue;
}

void spi_write_32(uint32_t argument)
{
    unsigned char bit;
    for (bit = 0; bit < 32; bit++) {
        
        if (argument & (1<<31))
        SET_MOSI;
        else
        CLEAR_MOSI;
        argument <<= 1;
        SPI_CLOCK;
    }
}
unsigned char spi_write(unsigned char byte)
{
    unsigned char bit;
    for (bit = 0; bit < 8; bit++) {
        outb(byte,0x378); //MOSI MSB PUSH
        byte <<= 1;
        SPI_CLOCK;
        if(CHECK_MISO)
        byte++;
    }
    return byte;
}

block.c
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/types.h>
#include <linux/genhd.h>
#include <linux/blkdev.h>
#include <linux/hdreg.h>

#include "parapin.h"
#include "sd.h"

MODULE_LICENSE("Dual BSD/GPL");


static int major_num = 0;
module_param(major_num, int, 0);
static int logical_block_size = 512;
module_param(logical_block_size, int, 0);
static int nsectors;
static int hardsect_size = 512;
module_param(nsectors, int, 0);

unsigned char PPORT_ACTIVATED = 0;
/*
* We can tweak our hardware sector size, but the kernel talks to us
* in terms of small sectors, always.
*/
#define KERNEL_SECTOR_SIZE 512

/*
* Our request queue.
*/
static struct request_queue *Queue;

/*
* The internal representation of our device.
*/
static struct sbd_device {
    unsigned long size;
    spinlock_t lock;
    u8 *data;
    struct gendisk *gd;
} Device;

/*
* Handle an I/O request.
*/
static void sbd_transfer(struct sbd_device *dev, sector_t sector,
unsigned long nsect, char *buffer, int write)
{
    unsigned long offset = sector*hardsect_size;
    unsigned long nbytes = nsect*hardsect_size;
    
    if ((offset + nbytes) > dev->size) {
        printk (KERN_NOTICE "sbd: Beyond-end write (%ld %ld)\n", offset, nbytes);
        return;
    }
    if (write)
    mmc_write_multiple_sector(sector, buffer, nsect);
    else
    mmc_read_multiple_sector(sector, buffer, nsect);
    
}

static void sbd_request(struct request_queue *q) {
    struct request *req;
    
    req = blk_fetch_request(q);
    while (req != NULL) {
        // blk_fs_request() was removed in 2.6.36 - many thanks to
        // Christian Paro for the heads up and fix...
        //if (!blk_fs_request(req)) {
            if (req == NULL || (req->cmd_type != REQ_TYPE_FS)) {
                printk (KERN_NOTICE "Skip non-CMD request\n");
                __blk_end_request_all(req, -EIO);
                continue;
            }
            sbd_transfer(&Device, blk_rq_pos(req), blk_rq_cur_sectors(req),
            req->buffer, rq_data_dir(req));
            if ( ! __blk_end_request_cur(req, 0) ) {
                req = blk_fetch_request(q);
            }
        }
    }
    
    /*
    * The HDIO_GETGEO ioctl is handled in blkdev_ioctl(), which
    * calls this. We need to implement getgeo, since we can't
    * use tools such as fdisk to partition the drive otherwise.
    */
    int sbd_getgeo(struct block_device * block_device, struct hd_geometry * geo) {
        long size;
        
        /* We have no real geometry, of course, so make something up. */
        size = Device.size * (logical_block_size / KERNEL_SECTOR_SIZE);
        geo->cylinders = (size & ~0x3f) >> 6;
        geo->heads = 4;
        geo->sectors = 16;
        geo->start = 4;
        return 0;
    }
    
    /*
    * The device operations structure.
    */
    static struct block_device_operations sbd_ops = {
        .owner = THIS_MODULE,
        .getgeo = sbd_getgeo
    };
    
    static int __init sbd_init(void){
        
        if (pin_init_kernel(0,NULL) < 0) {
            printk("LPT1 not available\n");
            return -ENOMEM;
            } else {
            printk("SUCCESS! LPT1 IS AVAILABLE!!!!\n");
            PPORT_ACTIVATED = 1;
            pin_output_mode(LP_DATA_PINS | LP_SWITCHABLE_PINS);
            if(mmc_init() != 0)
            if(mmc_init() != 0)
            if(mmc_init() != 0)
            goto out_unreg_pport;
            
        }
        /*
        * Set up our internal device.
        */
        Device.size = read_card_size()-(512);
        printk("\nMEMORY SIZE = %lu\n",Device.size);
        
        nsectors = Device.size / logical_block_size;
        
        spin_lock_init(&Device.lock);
        Queue = blk_init_queue(sbd_request, &Device.lock);
        if (Queue == NULL)
        goto out;
        blk_queue_logical_block_size(Queue, logical_block_size);
        /*
        * Get registered.
        */
        major_num = register_blkdev(major_num, "sbd");
        if (major_num <= 0) {
            printk(KERN_WARNING "sbd: unable to get major number\n");
            goto out;
        }
        /*
        * And the gendisk structure.
        */
        Device.gd = alloc_disk(16);
        if (!Device.gd)
        goto out_unregister;
        Device.gd->major = major_num;
        Device.gd->first_minor = 0;
        Device.gd->fops = &sbd_ops;
        Device.gd->private_data = &Device;
        strcpy(Device.gd->disk_name, "sbd");
        set_capacity(Device.gd, nsectors);
        Device.gd->queue = Queue;
        add_disk(Device.gd);
        
        return 0;
        
        out_unregister:
        unregister_blkdev(major_num, "sbd");
        out:
        vfree(Device.data);
        return -ENOMEM;
        out_unreg_pport:
        pin_release();
        return -ENOMEM;
    }
    
    static void __exit sbd_exit(void)
    {
        if(PPORT_ACTIVATED)
        pin_release();
        
        del_gendisk(Device.gd);
        put_disk(Device.gd);
        unregister_blkdev(major_num, "sbd");
        blk_cleanup_queue(Queue);
        vfree(Device.data);
    }
    
    module_init(sbd_init);
    module_exit(sbd_exit);

Additional files required:
Download parapin from parapin.sourceforge.net and extract the below 3 files to the build directory where the above 3 files are copied.
  1. parapin.c
  2. parapin.h
  3. parapin-linux.h
Now the next is a Makefile.
obj-m += mmc.o
mmc-objs :=parapin.o sd.o block.o
all:
 clear
 make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
 
clean:
 clear
 make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

How to make?
copy above makefile to "Makefile"
replace all space with tab (at beginning of 'clear', 'make')
simply type make in terminal

Now we will get mmc.ko which is the required kernel module to drive the SD/MMC card connected at parallel port.
Before inserting the module, we need to remove another existing module named "lp" otherwise we will not get accessed to the parallel port.
for that, we can type sudo rmmod lp
Now we need to make all connections as in the circuit diagram and need to make it sure that the power supply is in between 3.3 v and 3.6v and it should not cross this limit. 
Now we can insert the module by typing sudo insmod mmc.ko
 Now if it doesn't shows any error, then the module is inserted and the card in initialized! Then we can see a device file /dev/sbd and if there are partitions in the card, we can see /dev/sbd1, /dev/sbd2 etc.

Remember, any thing can happen after inserting the module..:-)
Some time, the system may get hanged for long time. But normally I feel it for the first few seconds after inserting the module. The reason is, we cannot access the parallel port very fast and also it require more CPU resource.

Now most probably, we could see a new volume in the "Devices" list where we can see other hard disk partitions...
Otherwise we can simply mount the /dev/sbdx to required region....

If the card is not formatted, we can do it by typing
sudo mkdosfs /dev/sbd

We can create partitions in the card by using fdisk tool
ie sudo fdisk /dev/sbd 

ScreenShots 
(Testing on ubuntu with linux kernel 3.2)
 Inserted the module and a 64MB MMC is detected.


Opening the 64MB filesystem (It is nothing but the MMC card connected at parallel port)


After unmounting the file system, the kernel module is removed. Then the 64MB file system is disappeared.


Using fdisk to view the partitions and the card details. We can create new partitions(sbd1, sbd2, sbd3, sbd4 etc) using this interface. In the above screen shot I was testing it with a 512MB SD . After that we can format it using mkdosfs.

Warning:
We should not try to copy large files because it may take hours to complete the operation and we may not be able to cancel the process and the system will get hanged until the process get completed and we should not remove the card before unmounting and removing the module. Also it will take more time to format the card with ext2, etx3 etc but we can easily format it with fat using mkdosfs program. Remember, to transfer 1.5MB it takes 1 minute and the system get hanged for the 1 minute. So guess what will happen if we tried to copy a 50MB file ;-) No doubt, we will press the RESET button.. Since it is too slow, I called it as a mad project from the beginning itself..;)

9 comments :

  1. Hi. I like this wery much.
    I have tried to copy it, but with no succes. Getting an error when i try to insmod.
    So i have a question.
    Do you get any warnings when compiling ?
    I get some from parapin, regarding symbols, and one from sd, regarding a unused variable.
    But it does compile.
    It CAN be that the laptop, just doesnt have the nescesarry "juice" to run the programmer.
    Br Browsem

    ReplyDelete
  2. what is the warning shown while inserting the module?

    at first clear the kernel messages by
    "sudo dmesg -c"

    then
    try to insert the module

    then
    see the error message shown on the terminal

    then type "dmesg"

    and see the kernel message.

    Are you trying it on a laptop? then most probably the error may be that it cannot find a parallel port, I guess...

    ReplyDelete
  3. Hello, is it possible to do the same read/write with a com port ? I'm trying to build an SD card Reader using a regular com port. If you can help me out I thank you

    ReplyDelete
  4. Thank you so much for publishing an account of how to actually BIT-BANG an MMC interface. I've been so frustrated trying to find information which doesn't eventually turn out to say "step 1: write to SPI register. step 2: use this library."

    It'll take me a bit to detangle the raw algorithm from the parallel-port stuff, but luckily that's nothing strange to me.

    ReplyDelete
  5. To make this idea really mad there is an extra idea - to boot from LPT-attached SD card the following manner:
    Take a (broken?) NIC with a spare socket for bootrom visible by PC BIOS at boot time, place some loader there, and boot this way.

    As the result, one can get a SD-bootable 386!

    ReplyDelete
  6. sudo insmod mmc.ko
    gives me an error:

    Error: could not insert module mmc.ko: Cannot allocate memory

    any idea what it could be?

    ReplyDelete
  7. I forgot to post the dmesg log :-/

    dmes gives the error: CARD ERROR-CMD0

    Do you think a PULLUP resistor on the chip select pin would help?

    ReplyDelete
  8. iam mr JOSE ,from Trichur people call me the great Josettan ,and dear king of electronics..
    my mmc card is of 1 gb ...well initialisation seems ok but after cmd(17,sector,0xff),it is waiting for somethin can you please please tell me what is matter with my little mmc .. void mmc_read_sector(unsigned long int sector)
    {
    send_string("reading mmc\n\r");
    int i;
    sector *= 512;
    if(sd_cmd(17, sector ,0xff) != 0)
    {
    send_string("cm17 ok\n\r");
    // while(spi_read() != 0);
    // send_string("read 0'ok\n\r");
    // while(spi_read() != 0xfe) ;
    // send_string("read 'fe' ok\n\r");
    send_string("procedloopread\n\r");
    for(i=0; i<=512; i++)
    {
    SPDR=0xFF;
    while(!(SPSR & (1<<SPIF)));
    mmc_buf[i] = SPDR;
    send_string("data");
    UDR=mmc_buf[i];
    while(!(UCSRA&(1<<TXC)));
    }
    spi_write(0xff);
    spi_write(0xff);
    }
    else
    send_string("couldnot read\n\r");
    }

    ReplyDelete
  9. Finally I have found what I have been looking for since 2 hours. I really like your blog and articles you are writing for us.Happy Independence Day Wallpaper Download

    ReplyDelete