Switch to Normal Style Sheet
Site Icon The Lab Book Pages Andrew Greensted (Modified: 17 June 2008)
Circuits > MAX3420 Maxim USB Peripheral Controller

MAX3420 - Maxim USB Peripheral Controller

The MAX3420 is a USB peripheral controller chip with an SPI bus. This page hopefully contains enough information to help you easily make use of the device in your projects.

Divider Bar Go to page top

• MAX3420 Introduction

The MAX3420 provides a very simple approach to adding a USB interface to a circuit. It uses a SPI bus to connect to your system. It does require a reasonable amount of configuration and control, so you'll need to connect it to some form of microprocessor/microcontroller.

The photos below show a PCB containing a MAX3420 device. The board is designed to connect to a Digilent XUP-V2Pro FPGA development board.

MAX3420 USB Board MAX3420 USB Board Connected
Divider Bar Go to page top

• Circuit & PCB

Below are the Schematic and PCB layouts for the board. Use the link below to download the Eagle CAD files.

Eagle icon eagle.tar.gz

The board is driven by a 3.3V supply. The connector pin arrangement allows it to be plugged directly into header J5 of a Diglent XUP-V2Pro FPGA development board, as shown in the right-hand photo above.

MAX3420 USB PCB Schematic
MAX3420 USB PCB (top) MAX3420 USB PCB (bottom)
Top LayerBottom Layer
Divider Bar Go to page top

• Example System

Below is an example setup of using the MAX3420 to connect a PC and a microcontroller embedded on an FPGA. There are 3 main custom parts to this system.

  • The MAX3420 PCB. This uses USB to connect to the PC and SPI to connect to the FPGA.
  • The Linux device driver. This creates a device file, /dev/usbDIO that user applications can read from and write to.
  • The EDK peripheral that connects the SPI to the OPB bus
Example MAX3420 System

Information on the MAX3420 has already been given above. Details on the linux device driver and the EDK Peripheral are given below.

Divider Bar Go to page top

• Linux Host Driver

You will need a kernel driver in order to send and receive data from the MAX3420 chip. Below is a basic usb driver that will do this. Hopefully all you'll need to do is download the gzippped tar, uncompress, and type make.

TAR icon usbDIO.tar.gz
> tar -zxf usbDIO.tar.gz
> cd usbDIO
> make
> insmod usbDIO.ko

Note: I'm not in any way an expert at Linux kernel development. Although the driver seems to work fine, I can't say how efficient or reliable it is.

• File: usbDIO.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/usb.h>
#include <asm/uaccess.h>
#include <linux/slab.h>
#include <linux/mutex.h>

#define USBDIO_VENDOR_ID   0x6666
#define USBDIO_PRODUCT_ID  0x1ABC

// Minor number for misc USB devices
#define USBDIO_MINOR 132

// Maximum number of writes at one time
#define MAX_WRITE_TRANSFER 256
#define MAX_WRITE_IN_FLIGHT 16

static int usbDIO_open(struct inode *inode, struct file *file);
static int usbDIO_release(struct inode *inode, struct file *file);
static ssize_t usbDIO_read(struct file *file, char __user *buffer, size_t count, loff_t *ppos);
static ssize_t usbDIO_write(struct file *file, const char __user *user_buffer, size_t count, loff_t *ppos);
static void usbDIO_write_callback(struct urb *urb);

static int usbDIO_probe(struct usb_interface *interface, const struct usb_device_id *id);
static void usbDIO_disconnect(struct usb_interface *interface);
static void usbDIO_delete(struct kref *kref);

// ****************************************************************************************************
// Stuctures
// ****************************************************************************************************

// Table of devices that work with this driver
static struct usb_device_id usbDIO_id_table [] = {
   { USB_DEVICE(USBDIO_VENDOR_ID, USBDIO_PRODUCT_ID) },
   { },  // Terminating Entry
};

MODULE_DEVICE_TABLE (usb, usbDIO_id_table);

// Strucutre for device specific data
struct usbDIO_deviceData {
   struct usb_device      *device;                //
   struct usb_interface   *interface;             // The interface for this device

   unsigned char       *bulk_in_buffer;        // The buffer to receive data
   size_t              bulk_in_size;           // The size of the receive buffer
   __u8                 bulk_in_endpointAddr;   // The address of the bulk in endpoint
   __u8                 bulk_out_endpointAddr;  // The address of the bulk out endpoint

   struct kref            kref;                   // Reference Counter
   struct semaphore    limitSem;               // Limiting the number of writes in progress
   struct mutex        ioMutex;                // Synchronize I/O with disconnect
};

static struct usb_driver usbDIO_driver = {
   .name =        "usbDIO",
   .id_table =    usbDIO_id_table,
   .probe =       usbDIO_probe,
   .disconnect =  usbDIO_disconnect,
};

static struct file_operations usbDIO_fops = {
   .owner =    THIS_MODULE,
   .open =     usbDIO_open,
   .release =  usbDIO_release,
   .read =     usbDIO_read,
   .write =    usbDIO_write,
// .ioctl =    ioctl_rio,
};

// USB class driver info in order to get a minor number from the usb core,
// and to have the device registered with the driver core
static struct usb_class_driver usbDIO_class = {
   .name =        "usbDIO%d",
   .fops =        &usbDIO_fops,
   .minor_base =  USBDIO_MINOR,
};


// ****************************************************************************************************
// File Operations
// ****************************************************************************************************

static int usbDIO_open(struct inode *inode, struct file *file)
{
   struct usbDIO_deviceData *deviceData;
   struct usb_interface *interface;
   int subminor;
   int retval = 0;

   subminor = iminor(inode);

   interface = usb_find_interface(&usbDIO_driver, subminor);
   if (!interface)
   {
      err ("usbDIO: %s - error, can't find device for minor %d", __FUNCTION__, subminor);
      return -ENODEV;
   }

   // Get the device data structure from the interface
   deviceData = usb_get_intfdata(interface);
   if (!deviceData) return -ENODEV;

   // Prevent the device from being autosuspended
   retval = usb_autopm_get_interface(interface);
   if (retval) return retval;

   // Increment our usage count for the device
   kref_get(&deviceData->kref);

   // Save the device data structure in the file's private structure
   file->private_data = deviceData;

   return 0;
}

static int usbDIO_release(struct inode *inode, struct file *file)
{
   struct usbDIO_deviceData *deviceData;

   deviceData = (struct usbDIO_deviceData *)file->private_data;
   if (deviceData == NULL) return -ENODEV;

   // Allow the device to be autosuspended
   mutex_lock(&deviceData->ioMutex);
   if (deviceData->interface) usb_autopm_put_interface(deviceData->interface);
   mutex_unlock(&deviceData->ioMutex);

   // Decrement the count on our device
   kref_put(&deviceData->kref, usbDIO_delete);

   return 0;
}

static ssize_t usbDIO_read(struct file *file, char __user *buffer, size_t count, loff_t *ppos)
{
   struct usbDIO_deviceData *deviceData;
   int retval = 0;
   int bytesRead;

   deviceData = (struct usbDIO_deviceData *)file->private_data;

   mutex_lock(&deviceData->ioMutex);

   if (!deviceData->interface)
   {
      // disconnect() was called
      retval = -ENODEV;
      goto exit;
   }

   // Do a blocking bulk read to get data from the device
   retval = usb_bulk_msg(deviceData->device,
                        usb_rcvbulkpipe(deviceData->device, deviceData->bulk_in_endpointAddr),
                        deviceData->bulk_in_buffer,
                        min(deviceData->bulk_in_size, count),
                        &bytesRead,
                        HZ*10);

   // If the read was successful (retval != 0), copy the data to userspace
   if (!retval)
   {
      if (copy_to_user(buffer, deviceData->bulk_in_buffer, count)) retval = -EFAULT;
      else retval = bytesRead;
   }

exit:
   mutex_unlock(&deviceData->ioMutex);
   return retval;
}

static ssize_t usbDIO_write(struct file *file, const char __user *user_buffer, size_t count, loff_t *ppos)
{
   struct usbDIO_deviceData *deviceData;
   int retval = 0;
   struct urb *urb = NULL;
   char *buf = NULL;
   size_t writeSize = min(count, (size_t) MAX_WRITE_TRANSFER);

   deviceData = (struct usbDIO_deviceData *)file->private_data;

   // Verify that there is actually some data to write
   if (count == 0) goto exit;

   // Limit the number of URBs in flight to stop a user from using up all RAM
   if (down_interruptible(&deviceData->limitSem))
   {
      retval = -ERESTARTSYS;
      goto exit;
   }

   mutex_lock(&deviceData->ioMutex);

   if (!deviceData->interface)
   {
      // Disconnect() was called - Setting interface to NULL
      retval = -ENODEV;
      goto error;
   }

   // Create an URB
   urb = usb_alloc_urb(0, GFP_KERNEL);
   if (!urb)
   {
      retval = -ENOMEM;
      goto error;
   }

   // Create a buffer
   buf = usb_buffer_alloc(deviceData->device, writeSize, GFP_KERNEL, &urb->transfer_dma);
   if (!buf)
   {
      retval = -ENOMEM;
      goto error;
   }

   // Copy the data to the urb
   if (copy_from_user(buf, user_buffer, writeSize))
   {
      retval = -EFAULT;
      goto error;
   }

   // Initialize the urb
   usb_fill_bulk_urb(urb, deviceData->device,
                     usb_sndbulkpipe(deviceData->device, deviceData->bulk_out_endpointAddr),
                     buf, writeSize, usbDIO_write_callback, deviceData);

   urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;

   // Send the data out the bulk port
   retval = usb_submit_urb(urb, GFP_KERNEL);
   if (retval)
   {
      err("usbDIO: %s - failed submitting write urb, error %d", __FUNCTION__, retval);
      goto error;
   }

   // Release the URB reference, the USB core will eventually free it entirely
   usb_free_urb(urb);

   mutex_unlock(&deviceData->ioMutex);
   return writeSize;

error:
   if (urb)
   {
      usb_buffer_free(deviceData->device, writeSize, buf, urb->transfer_dma);
      usb_free_urb(urb);
   }

   mutex_unlock(&deviceData->ioMutex);
   up(&deviceData->limitSem);

exit:
   return retval;
}

static void usbDIO_write_callback(struct urb *urb)
{
   struct usbDIO_deviceData *deviceData = (struct usbDIO_deviceData *) urb->context;

   // sync/async unlink faults aren't errors
   if (urb->status &&
       !(urb->status == -ENOENT || urb->status == -ECONNRESET || urb->status == -ESHUTDOWN))
   {
      err("%s - nonzero write bulk status received: %d", __FUNCTION__, urb->status);
   }

   // free up our allocated buffer
   usb_buffer_free(urb->dev, urb->transfer_buffer_length, urb->transfer_buffer, urb->transfer_dma);

   up(&deviceData->limitSem);
}


// ****************************************************************************************************
// USB Operations
// ****************************************************************************************************

// Called when a device is installed that the USB core thinks this driver should handle;
// Local structures used by the USB driver should initialize here.
// Information that is needed about the device should be saved
static int usbDIO_probe(struct usb_interface *interface, const struct usb_device_id *id)
{
   struct usbDIO_deviceData *deviceData = NULL;
   int i;
   struct usb_host_interface *interface_desc;
   struct usb_endpoint_descriptor *endpoint;
   size_t buffer_size;
   int retval;

   // Allocate memory for a device state structure
   deviceData = kzalloc(sizeof(*deviceData), GFP_KERNEL);

   // Check memory allocation worked
   if (deviceData == NULL)
   {
      err("usbDIO_probe: No memory to allocate device structure");
      retval = -ENOMEM;
      goto error;
   }

   // Initialise the device structure's reference counter
   kref_init(&deviceData->kref);
   sema_init(&deviceData->limitSem, MAX_WRITE_IN_FLIGHT);
   mutex_init(&deviceData->ioMutex);

   // Store the USB Device and interface pointers
   deviceData->device = usb_get_dev(interface_to_usbdev(interface));
   deviceData->interface = interface;

   // XXX Get altsetting?
   interface_desc = interface->cur_altsetting;

   for (i = 0; i < interface_desc->desc.bNumEndpoints; ++i)
   {
      endpoint = &interface_desc->endpoint[i].desc;

      // Check for Bulk IN endpoint
      if (!deviceData->bulk_in_endpointAddr && usb_endpoint_is_bulk_in(endpoint))
      {
         deviceData->bulk_in_endpointAddr = endpoint->bEndpointAddress;

         // Allocate memory for IN buffer
         buffer_size = le16_to_cpu(endpoint->wMaxPacketSize);
         deviceData->bulk_in_size = buffer_size;
         deviceData->bulk_in_buffer = kmalloc(buffer_size, GFP_KERNEL);

         if (!deviceData->bulk_in_buffer)
         {
            err("usbDIO_probe: No memory to allocate bulk_in_buffer");
            retval = -ENOMEM;
            goto error;
         }
      }

      // Check for Bulk OUT endpoint
      if (!deviceData->bulk_out_endpointAddr && usb_endpoint_is_bulk_out(endpoint))
      {
         deviceData->bulk_out_endpointAddr = endpoint->bEndpointAddress;
      }
   }

   // Check that both endpoints have been found
   if (!deviceData->bulk_in_endpointAddr || !deviceData->bulk_out_endpointAddr)
   {
      err("Could not find both bulk-in and bulk-out endpoints");
      retval = -1; // XXX Need better error number
      goto error;
   }

   // Save our device pointer in this interface device
   usb_set_intfdata(interface, deviceData);

   // Register the device, asking for minor number
   // Creates the devfs file for the usb device, if devfs is enabled, and creates a usb class device in the sysfs tree.
   retval = usb_register_dev(interface, &usbDIO_class);

   if (retval)
   {
      // Something prevented the driver being registered
      err("usbDIO_probe: Not able to register the device.");
      usb_set_intfdata(interface, NULL);
      goto error;
   }

   // Get Device release number (Binary code decimal)
   i = le16_to_cpu(deviceData->device->descriptor.bcdDevice);

   // Display Endpoint numbers
   info("usbDIO: Bulk IN EP:0x%X(%lu), Bulk OUT EP:0x%X", deviceData->bulk_in_endpointAddr,
      deviceData->bulk_in_size, deviceData->bulk_out_endpointAddr);

   info("usbDIO: Version %1d%1d.%1d%1d found at address %d",
      (i>>12)&0xF, (i>>8)&0xF, (i>>4)&0xF, i&0xF, deviceData->device->devnum);

   // Let the user know what node this device is now attached to
   info("usbDIO: device now attached, minor number:%d", interface->minor);

   return 0;

error:
   // Decrement the ref counter (Calls usbDIO_delete if counter reaches 0)
   if (deviceData) kref_put(&deviceData->kref, usbDIO_delete);

   return retval;
}

// Called when the driver should no longer control the device for some reason and can do clean-up.
static void usbDIO_disconnect(struct usb_interface *interface)
{
   struct usbDIO_deviceData *deviceData;
   int minor = interface->minor;

   // Prevent usbDIO_open() from racing usbDIO_disconnect()
   lock_kernel();

   deviceData = usb_get_intfdata(interface);
   usb_set_intfdata(interface, NULL);

   // Give back the minor
   usb_deregister_dev(interface, &usbDIO_class);

   // Prevent more I/O from starting
   mutex_lock(&deviceData->ioMutex);
   deviceData->interface = NULL;
   mutex_unlock(&deviceData->ioMutex);

   unlock_kernel();

   // Decrement the usage count
   kref_put(&deviceData->kref, usbDIO_delete);

   info("usbDIO: #%d now disconnected", minor);
}

static void usbDIO_delete(struct kref *kref)
{
   // container_of(ptr, type, member)
   //   ptr      The pointer to the member.
   //   type  The type of the container struct this is embedded in.
   //   member   The name of the member within the struct.

   // Get pointer to struture (usb_usbDIO) from member (kref)
   struct usbDIO_deviceData *deviceData = container_of(kref, struct usbDIO_deviceData, kref);

   // Release the use of the usb device structure
   usb_put_dev(deviceData->device);

   // Free the allocated memory
   kfree(deviceData->bulk_in_buffer);
   kfree(deviceData);
}

// ****************************************************************************************************
// Module init and exit
// ****************************************************************************************************

static int __init usbDIO_init(void)
{
   int result = usb_register(&usbDIO_driver);

   // Register this driver with the USB subsystem
   if (result) err("usbDIO: usb_register failed. Error number %d", result);

   return result;
}

static void __exit usbDIO_exit(void)
{
   // Deregister this driver with the USB subsystem
   usb_deregister(&usbDIO_driver);
}

// ****************************************************************************************************

MODULE_LICENSE("GPL");
MODULE_AUTHOR("A.Greensted <ajg112@ohm.york.ac.uk>");
MODULE_DESCRIPTION("USB based Data IO driver");
MODULE_VERSION("0.1");

module_init(usbDIO_init);
module_exit(usbDIO_exit);
• File: Makefile
# usbDIO Makefile

ifneq ($(KERNELRELEASE),)
     obj-m := usbDIO.o

else
   KERNELDIR ?= /lib/modules/$(shell uname -r)/build
   PWD := $(shell pwd)

default:
   $(MAKE) -C $(KERNELDIR) M=$(PWD) modules

endif

Loading this module will create the file /dev/usbDIO0. Writing to and reading from this file will send and receive date from the MAX3420 device.

Divider Bar Go to page top

• EDK Peripheral & Driver Code

The EDK peripheral linked to below contains a SPI module that will talk to the MAX3420. Untar the file and place the max3420_v1_00_a directory into your EDK project's pcores directory.

TAR icon max3420_v1_00_a.tar.gz

The table below shows the pad LOC constraints for the XUP-V2Pro J5 Header. You'll need to put these into your system's UCF file.

Net LOC
usb_SCLK L5
usb_SS M2
usb_MOSI P9
usb_MISO N2
usb_GPX R9
usb_INT M4

The module only uses two register locations. The first is used to read the MAX3420 status bits and set the SPI data rate. The second is used to transfer data between the MAX3420 device and the embedded processor. These functions are summeriesed in the table below.

Register Read Write
0 (Base address) Device Status SPI speed
1 (Base address + 4) Data from MAX3420 Data to MAX3420
Divider Bar Go to page top