Linux SPI Device Driver for W25Q64 Flash Memory
Linux SPI Device Driver for W25Q64 Flash Memory
SPI Interface
  • Serial peripheral interface (SPI)  is a synchronous, full-duplex master-slave-based interface.
  • The data from the master or the slave is synchronized on the rising or falling clock edge. Both master and slave can transmit data at the same time.
  • The SPI interface can be either 3-wire or 4-wire.
  • 4-wire SPI devices have four signals:
    • Clock (SPI CLK, SCLK)
    • Chip select or Slave Select (CS or SS)
    • Master Out, Slave In (MOSI)
    • Master In, Slave Out (MISO)
  • The device that generates the clock signal is called the master. Data transmitted between the master and the slave is synchronized to the clock generated by the master.
  • SPI interfaces can have only one master and can have one or multiple slaves.
  • The chip select signal from the master is used to select the slave.
  • When multiple slaves are used, an individual chip select signal for each slave is required from the master.
  • MOSI and MISO are the data lines. MOSI transmits data from the master to the slave and MISO transmits data from the slave to the master.
  • In SPI, the master can select the clock polarity (CPOL) and clock phase (CPHA). Depending on the CPOL and CPHA selection, four SPI modes are available.
SPI Protocol Block Diagram

In this post, I am going to write the Linux device driver for W25Q64 SPI flash memory interfaced with the I.MX6Solox based Udoo development board.

Hardware used in this tutorial.
  1. NXP® i.MX 6SoloX based Udoo neo full with 1Gb of DDR memory.
  2. W25Q64 SPI flash memory breakout board and some jumper wires.

Here the SPI device driver for W25Q64 is compiled as an out-of-tree module, which can be loaded into the target Udoo board during runtime. The driver is compiled with Freescale community Linux kernel version “Kernel_4.17.x+fslc” and can be used for all kernel versions above 4.1.

Implementation

An SPI device is represented in the kernel as an instance of spi_device. The instance of the driver that manages them is struct spi_driver structure. Header responsible for SPI subsystem is Linux kernel is <linux/spi/spi.h>.

struct spi_device structure represents an SPI device. This structure defines master bus, maximum clock rate used by the slave device, Enable and disable of the chip select, SPI mode selection by specifying CPOL and CPHA and much more.

struct spi_driver represents the driver you develop to manage your SPI device.

static struct spi_driver w25q_driver = { 
    .driver = { 
        .owner = THIS_MODULE,
        .name = "w25q64",
        .of_match_table = of_match_ptr(w25q_of),
    },  
    .probe = w25q_probe,
    .remove = w25q_remove,
    .id_table = w25q_id,
};
module_spi_driver(w25q_driver);

The binding will happen based on the w25q_id table or device tree compatible string. The SPI core first tries to match the device by compatible string (OF style, which is device tree), and if it fails, it then tries to match the device by id table.

struct of_device_id structures that hold the compatible strings supported by the driver. The .compatible strings should store the same value of the Device Tree device node´s compatible property.

static const struct spi_device_id w25q_id[] = {
    { "w25q64" },
    { },
};

static const struct of_device_id w25q_of[] = { 
    { .compatible = "w25q64" },
    { },
};

In the Udoo board device tree dts/imx6sx-udoo-neo-full.dts a new node is created for w25q64 under ecspi5 parent node. In the node cs-gpios represents chip-select pin, reg represents the index of chip select pin connected to the slave device.

&ecspi5 {
        cs-gpios = <&gpio4 28 GPIO_ACTIVE_LOW>;
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_ecspi5>;
        status = "okay";
       spi_flash: w25q_spiflash {
                   compatible = "w25q64";
                   spi-max-frequency = <20000000>;
                   reg = <0>;              
               };
};

&iomux{
.....
        pinctrl_ecspi5: ecspi5grp {
                fsl,pins = <
                        MX6SX_PAD_QSPI1A_SS1_B__ECSPI5_MISO     0x100b1
                        MX6SX_PAD_QSPI1A_DQS__ECSPI5_MOSI       0x100b1
                        MX6SX_PAD_QSPI1B_SS1_B__ECSPI5_SCLK     0x100b1
                        MX6SX_PAD_QSPI1B_DQS__GPIO4_IO_28       0x0b0b1
                >;
        };
.....
}

The driver´s probe() function is called when the compatible field in one of the of_device_id entries matches with the compatible property of a DT device node. The probe() function is responsible for initializing the device with the configuration values obtained from the matching DT device node and also registering the device to the appropriate kernel framework.

static int w25q_probe(struct spi_device *spi)
{
    int8_t ret;
    int err;
    dev_t dev_no;
    struct device *w25q_device=NULL;

    struct w25q_data *w25q;

    uint8_t in_addr[4],mf_id[2] = {0,};
    pr_info("%s\n",__func__);

    spi->mode = SPI_MODE_0;
    spi->max_speed_hz = 2000000;
    spi->bits_per_word = 8;
    ret = spi_setup(spi);
    ret = spi_setup(spi);
    
    if(ret < 0){
	pr_err("spi_setup error\n");
    }

    in_addr[0] = 0x90;
    in_addr[1] = 0x00;
    in_addr[2] = 0x00;
    in_addr[3] = 0x00;
    
    spi_write_then_read(spi,in_addr,4,mf_id,2);
    pr_info("spi_read = %x %x\n",mf_id[0],mf_id[1]);
    
    if(mf_id[0] != 0xEF || mf_id[1] != 0x16)
    {
	pr_info("w25q64 not found!!\n");
    }
    
    pr_info("w25q64 found!!\n");

    /* Allocate the major and minor number */
    err = alloc_chrdev_region(&dev_no,0,1,DEVICE_NAME);
    if(err < 0){ 
        pr_err("alloc_chrdev_region fail\n");
        return err;
    }   
    w25q_major = MAJOR(dev_no);
    w25q_minor = MINOR(dev_no);
    pr_info("%s major num = %d -- minor num = %d\n",DEVICE_NAME,w25q_major,w25q_minor);

    /* Create the sysfs class */
    w25q_class = class_create(THIS_MODULE,DEVICE_NAME);
    if(IS_ERR(w25q_class)){
        err = PTR_ERR(w25q_class);
        goto fail;
    }   
    
    /* Allocating memory for device specific data */
    w25q = kmalloc(sizeof(struct w25q_data),GFP_KERNEL);
    if(w25q == NULL){
        pr_err("kmalloc fail\n");
        err = -ENOMEM;
        return err;
    }
    w25q->spi = spi;

    /* Initilize and register character device with kernel */
    cdev_init(&w25q->dev,&w25q_fops);
    w25q->dev.owner = THIS_MODULE;
    err = cdev_add(&w25q->dev,dev_no,1);

    if(err){
        pr_err("Adding cdev fail\n");
        goto fail;
    }

    /* create the device so that user can access the slave device from user space */
    w25q_device = device_create(w25q_class,
                NULL,
                dev_no,
                NULL,
                DEVICE_NAME);

    if(IS_ERR(w25q_device)) {
        err = PTR_ERR(w25q_device);
        pr_err("Device creation fail\n");
        cdev_del(&w25q->dev);
        goto fail;
    }
    spi_set_drvdata(spi,w25q);
    return 0;
fail:
   /* if anything fails undo everything */
    if(w25q_class != NULL){
        class_destroy(w25q_class);
    }
    unregister_chrdev_region(MKDEV(w25q_major,w25q_minor),1);
    if(w25q != NULL){
        kfree(w25q);
    }
    return err;
}

Probe function will implements following

  1. Reserve a major and a range of minors.
  2. Create a class for your devices visible in /sys/class/.
  3. Set up file operations for users to access using standard file manipulation functions.
  4. Making the device live for the users to access.
  5. Creating a device node each device.
  6. Setting the device-specific data.

The remove function used to unregister the device driver from the subsystem where we have registered in the probe() and also to clear the memory that has been assigned to driver during initilization.

static int w25q_remove(struct spi_device *spi)
{
    struct w25q_data *w25q = spi_get_drvdata(spi);
    
    pr_info("%s\n",__func__);
    
    device_destroy(w25q_class,MKDEV(w25q_major,w25q_minor));
    kfree(w25q);
    class_destroy(w25q_class);
    unregister_chrdev_region(MKDEV(w25q_major,w25q_minor),1);
    return 0;
}
File operations

Used to expose the driver’s methods to the user-space so that users can use this driver using standard system calls. The kernel driver will use the specific functions copy_from_user() and copy_to_user() to exchange data with userspace.

static struct file_operations w25q_fops = {
    .owner = THIS_MODULE,
    .open = w25q_open,
    .release = w25q_release,
    .read = w25q_read,
    .write = w25q_write,
    .unlocked_ioctl = w25q_ioctl,
};
  1. The w25q_open function will retrive the device specific data with help of container_of() macro. In w25q_open function, memory will be assigned for data read and write with the device.
  2. w25q_open will execute for each open call from userspace.
  3. In w25q_release the memory assigned in w25q_open will be deleted.
  4. w25q_write function is used to write data to flash memory using write system call from user-space.
  5. w25q_read function is used to read data to flash memory using read system call from user-space.
  6. w25q_ioctl is command used to implement w25q64 flash erase from user-space programs using standard ioctl functions.
  7. In the w25q_write function, the user data is copied from user-space kernel space using copy_from_user. In the w25q_read function, copy_to_user() function is used copy data from kernel space to user-space.

To compile the module type “make” inside the mcp9808 folder then mcp9808.ko file will be generated in same folder copy it to target board Udoo. To load the module into the kernel

To compile the module type “make” inside the w25q64/modules folder then w25q64.ko file will be generated in same folder copy it to target board Udoo. To load the module into the kernel.

root@jagguudoo:~# insmod w25q64.ko
root@jagguudoo:~# dmesg
[ 687.800639] w25q_probe
[ 687.802869] spi_read = ef 16
[ 687.802883] w25q64 found!!
[ 687.802905] w25q64 major num = 243 — minor num = 0

Simple user application to interact with w25q64 kernel module
int main(void)
{
    int file;
    char ch,choice,filename[] = "/dev/w25q64";
    char input[256],output[256];
    unsigned int len;

    file = open(filename,O_RDWR);
    if(file < 0){
	printf("Coudn't open /dev/w25q64\n");
	exit(0);
    }

    do
    {
	printf("Write - w\nRead - r\nErase - e\nChoice = ");
	scanf("%c",&choice);
	getchar();
	
	memset(input,'\0',sizeof(input));
	memset(output,'\0',sizeof(output));

	switch (choice)
	{
	    case 'w':
	    {
		printf("Enter data to store in w25q64 flash (Up to 255 characters)\n");
		scanf("%[^\n]s",input);
		getchar();
		len = strlen(input);

		if(write(file,input,len+1) != len+1)
		{
		    printf("Write fail\n");
		}
		break;
	    }

	    case 'r':
	    {
		read(file,output,255);
		printf("Data stored in w25q64 spi flash\n");
		printf("%s\n",output);
		break;
	    }

	    case 'e':
	    {
	        if(ioctl(file,W25Q_ERASE) != 0)
		{
		    printf("chip Erase Error !!\n");
		}
		printf("chip Erase successfull !!\n");
		break;
	    }
	}

	printf("Press y|Y to read another value = ");	
	scanf("%c",&ch);
	getchar();
    }while(ch == 'y' || ch == 'Y');

    close(file);
    return 0;
}

Once the driver successfully loaded then /dev/w25q64 device file will be created. User-space programs can use standard file operation function like open, close, read, write to interact with device drivers. This user-pace program will write and read data from the flash memory and can the erase the whole chip.

root@jagguudoo:~# ./w25q_user_mod 
Write - w
Read - r
Erase - e
Choice = w
Enter data to store in w25q64 flash (Up to 255 characters)
Embedded Diaries
Press y|Y to read another value = y
Write - w
Read - r
Erase - e
Choice = r
Data stored in w25q64 spi flash
Embedded Diaries
Press y|Y to read another value = y
Write - w
Read - r
Erase - e
Choice = e
chip Erase successfull !!
Press y|Y to read another value = n

Source code is available at GitHub link : https://github.com/jaggu777/w25q64