Welcome to the Mike's homepage!

Various things I do outside of work: fun, boring, anything really.

View on GitHub
6 September 2020

FTDI I2C adapter

by Mike Krinkin

In the previous post I covered what USB data transfers we need to configure the FTDI MPSSE cable to work in the MPSSE mode. Now with this knowledge we can continue working on the USB-to-I2C bridge for the kernel.

All the sources used in this article are available on GitHub.

Probing

In one of the previous posts we covered a skeleton of the USB driver that did nothing, but detected that the device was connected. USB susbsystem used the generic interface that all the USB devices conform to to figure out what Vendor and Product IDs the newly connected device has and based on that found the right driver to call.

Inside the probing function of the driver we basically just logged some messages, but other than that we didn’t really do anything. Normally, in the probe function we should initialize the device, create and initialize all the bookkeeping data structures we need for the driver.

Driver Data

Let’s start from introducing the bookkeeping datastructure for the driver:

struct ftdi_usb {
	struct usb_device *udev;
	struct usb_interface *interface;
	u8 *buffer;
	size_t buffer_size;
	struct i2c_adapter adapter;
	int io_timeout;
	unsigned freq;
};

There are quite a few fields there, so let’s discuss them a little bit starting from the simplest. buffer and buffer_size represent an helper data buffer. This buffer will be used to accumulate MPSSE commands before sending them to the device, as well as, an intermediate buffer for receiving the data from the device.

The reason why we need an intermediate buffer when receiving the data is because as outlined in the previous post the first two bytes of the returned data will store some kind of status that we need to discard.

We will allocate the helper buffer once and then will reuse it for all kinds of IO operations multiple times.

The next relatively simple to understand field is io_timeout. It’s just the timeout for the USB data trnasfers in milliseconds.

The last simple to understand field is freq. It’s the desired frequency of the I2C bus. I2C master decides the frequency of the bus as the master is the one that controls the clock. Typical frequency for the I2C devices is 100kHZ.

NOTE: The frequency doesn’t have to be accurate due to the way the I2C protocol was designed. Probably, the biggest problem with using a too high frequency is that it might not give the slave device enough time to respond. Normally, I2C master would support a feature called clock stretching that would help to mitigate this issue somewhat, but FTDI MPSSE cable I have does not officially support this feature.

udev and interface are pointers to the Linux Kernel internal structures representing the device and the interface the driver binds to. To execute data transfers we need the pointer to the USB device (udev), that’s why we save it in our strtucture. The interface field is not really needed. So we can probably just keep just one of them.

NOTE: usb_device pointer can be obtained from the usb_interface pointer using the interface_to_usbdev function.

One important note about udev and interface fields is that they point to reference counted structures. So if we want to keep those pointers around we have to increment the reference counters for them. In order to do that we can use usb_get_dev and usb_get_intf functions.

Similarly, in our disconnect function we have to decrement the reference counters for both structures. The function that do that are called usb_put_dev and usb_put_intf.

Finally, the adapter field is an instance of i2c_adapter structure. This structure contains pointers to functions that impelement I2C related functions, like the actual data transfers. That’s the structure that I2C controllers need to register in the kernel. I’ll touch on how to initialize this structure later in this post.

Describing the driver data structure took a lot of words, but all operations of the driver will be centered around the access to this data structure, so it was important to cover it.

Initialiing the device

Besides creating the data structure for the device we also need to configure the device and put it in MPSSE mode. From the previous post we know what USB data transfers are needed to initialize the device, here we will cover how to do those in Linux Kernel.

In Linux Kernel all USB communications are centered around the urb structure. Normally, drivers to initiate a transfer would need to fill in this structure and submit it to the host controller driver, that will handle the trasfer and when it’s complete call the device driver back.

In our case however all we need is a simple syncrhonous communication interface. In such cases we don’t need to work with the urb structure directly or use callbacks. To start a control data transfer we can use usb_control_msg function and to start a bulk data transfer we can use usb_bulk_msg function. Those function in internally create the urb, submit it and wait until the transfer is complete.

For example, to do a device reset we need to do three control transfers. Here is how the code that does it might look:

static int ftdi_reset(struct ftdi_usb *ftdi)
{
	int ret;

	ret = usb_control_msg(
		ftdi->udev, usb_sndctrlpipe(ftdi->udev, 0),
		/* bRequest = */0x00,
		/* bRequestType = */0x40,
		/* wValue = */0x0000,
		/* wIndex = */0x0000,
		/* data = */NULL,
		/* size = */0,
		ftdi->io_timeout);
	if (ret < 0)
		return ret;

	ret = usb_control_msg(
		ftdi->udev, usb_sndctrlpipe(ftdi->udev, 0),
		/* bRequest = */0x00,
		/* bRequestType = */0x40,
		/* wValue = */0x00001,
		/* wIndex = */0x0000,
		/* data = */NULL,
		/* size = */0,
		ftdi->io_timeout);
	if (ret < 0)
		return ret;

	return usb_control_msg(
		ftdi->udev, usb_sndctrlpipe(ftdi->udev, 0),
		/* bRequest = */0x00,
		/* bRequestType = */0x40,
		/* wValue = */0x0002,
		/* wIndex = */0x0000,
		/* data = */NULL,
		/* size = */0,
		ftdi->io_timeout);
}

Most of the parameters are easy to understand. bRequest, bRequestType, wValue and wIndex are generic for all the USB control transfers. data and size allow to add additional payload, which we don’t need for our use case. ftdi->io_timeout is more or less self explanatory.

Other than those we need to specify the device and the endpoint on the device we want to communicate with. ftdi->udev and usb_sndctrlpipe(ftdi->udev, 0) are responsible for that.

usb_sndctrlpipe function is interesting, so let’s take a look a little bit at its name. First of all the pipe part means the endpoint, so those terms are basically synonims. snd part means that we are initiating a write transfer. In other words we want to send some data to the device. Finally, the ctrl part says that we want to do a control transfer.

The arguments of usb_sndctrlpipe function are the device pointer and the index of the endpoint/pipe. We got the right index from the USB snooping logs in the previous post.

There is a similar function that could be used for bulk data transfers. For example if we want to start a bulk write data transfer to the endpoint/pipe number two, we’d use usb_sndbulkpipe(ftdi->udev, 2) function.

Let’s look at how we can do bulk read and write data transfers to our device. In our case of we need to send write bulk transfers to the endpoint/pipe number two and read bulk data transfers to the endpoint/pipe number one to communciate with the MPSSE:

static int ftdi_mpsse_write(
	struct ftdi_usb *ftdi, u8 *data, size_t size, size_t *written)
{
	int actual_length;
	int ret;

	ret = usb_bulk_msg(
		ftdi->udev, usb_sndbulkpipe(ftdi->udev, 2),
		/* data = */data,
		/* len = */size,
		/* actual_length = */&actual_length,
		ftdi->io_timeout);
	*written = actual_length;
	return ret;
}

static int ftdi_mpsse_read(
	struct ftdi_usb *ftdi, u8 *data, size_t size, size_t *read)
{
	int actual_length;
	int ret;

	ret = usb_bulk_msg(
		ftdi->udev, usb_sbdbulkpipe(ftdi->udev, 2),
		/* data = */data,
		/* len = */size,
		/* actual_length = */&actual_length,
		ftdi->io_timeout);
	*read = actual_length;
	return ret;
}

You may notice that in the example above we use usb_bulk_msg instead of usb_control_msg since we want to do a bulk data transfer. Bulk data transfers require just plain data and don’t have prescribed parameters as control data transfers (no bRequest, bRequestType, ‘wValue’ and wIndex).

NOTE: at this point even if you’re not familiar with USB specificaltion it’s not really hard to roughly figure out what a different kinds of USB data transfers are used for. That being said, we only touched on two simplest to understand data transfers in the USB specification.

Described control and bulk data structure are enough to implement all the functionality we need. I will not provide the complete code of the probe function, as it’s available on GitHub.

I2C Controller Interface

Since we want the FTDI MPSSE cable to act as an I2C controller we need to initialize and register with the kernel an instance of i2c_adapter structure.

There are a few fileds in that structure that we need to initialize. The actual implementation of the I2C is described by i2c_algorithm structure. i2c_adapter algo fields must store a pointer to a valid i2c_algorithm. It’s our responsibility to create the i2c_algorithm instance and store the pointer in the i2c_adapter structure.

Here is how the i2c_algorithm structure looks in my case:

static const struct i2c_algorithm ftdi_usb_i2c_algo = {
	.master_xfer = ftdi_usb_i2c_xfer,
	.functionality = ftdi_usb_i2c_func,
};

The master_xfer field should point to the function that actually implements data transfers. In our case it’s the function that will talk to the FTDI MPSSE cable to do I2C data transfers.

The signature of the function is defined as follows:

static int ftdi_usb_i2c_xfer(struct i2c_adapter *adapter,
			     struct i2c_msg *msg, int num);

The adapter paramter is the i2c_adapter structure that we register with the kernel. msg and num describe a sequence of actual data transfers that we need to perform.

One question that may arise here is how can we get the ftdi_usb to request the USB data transfers as described above. The i2c_adapter structure contains a void * field called algo_data specifically for that puporse. When we initialize i2c_adapter structure we can write any data we want into that field to be used later. Specifically, we can write the a pointer to ftdi_usb structure and then use it:

static int ftdi_usb_i2c_xfer(struct i2c_adapter *adapter,
			     struct i2c_msg *msg, int num)
{
	struct ftdi_usb *ftdi = (struct ftdi_usb *)adapter->algo_data;

	/* the rest of the implementation can use ftdi pointer */
}

The functionality file should point to function returning supported features of the I2C controller. In our case we support only plain I2C data transfers, so the function returns just I2C_FUNC_I2C constant defined by the Linux Kernel.

Besides the fields relevant for the actual I2C implementation there are a few other fields inside i2c_adapter that we need to initialize. One of them is owner field. This field is used for reference counting when work with modules. If your kernel module registered a structure with the kernel, you don’t want to allow the module to be unloaded before the structure was unregistered. The owner field should be initialized using THIS_MODULE macro, to prevent unloading the module.

Finally, all the devices inside Linux Kernel are supposed to have an associated device structure. That’s why i2c_adapter has the device structure embedded in it. Devices are linked together in a form of a tree or forest. For example, roughly a USB device has a USB bus as a parent, a USB bus might have a PCI bus as a parent, etc. This way, when the parent device goes away, kernel will also know to disconnect all the children of the parent. In case of I2C adapter we need to tell the kernel what device is the parent as it has no means to figure it out on its own.

Despite the long explanation, the complete initialization code would look rather simple:

ftdi->adapter.owner = THIS_MODULE;
ftdi->adapter.algo = &ftdi_usb_i2c_algo;
ftdi->adapter.algo_data = ftdi;
ftdi->adapter.dev.parent = &interface->dev;
snptrinf(ftdi->adapter.name, sizeof(ftdi->adapter.name),
	 "FTDI USB-to-I2C at bus %03d device %03d",
	 dev->bus->busnum, dev->devnum);

To register the initialized I2C adapter structure with the kernel all we need is to call i2c_add_adapter function. To unregister the registered adapter we need to call i2c_del_adapter.

I put the I2C adapter initialization and registration inside the USB probe function. Unregistration happens in the USB disconnect callback.

Error handling

One final question worth considering is what to do with the device when an error happens? The answer to this question would depend on the device and the type of the error. In my case since I don’t have the complete specification, it’s hard to device a smart error handling strategy. As a result I went with a relatively simple one.

USB probe function of the driver resets the device and then initializes it to use use MPSSE mode of operation. I made an assumption that this reset and initialize sequence should be able to return the device into a working state if something happens. Similarly to the way it sets the device into the working state once it’s connected.

The the error handling strategy I adopted was to propagate any IO errors to the caller, but before returning the error I’m trying to reset the device in hope that it will make the subsequent IO operations succeed.

Testing

In previous posts I covered an input device driver for Nintendo Wiichuk controller. The driver was tested on BeagleBone Black Wireless board. The driver itself however is not board or architecture specific, so we can reuse it.

One question is how can we tell the kernel that we connected an I2C device? I2C bus does not provide capablities for device enumeration, like, for example, USB does. With BeagleBone Black Wireless we added the Nintendo Wiichuk node to the device tree that kernel parsed, however not all platforms use device tree.

On laptops, we generally have plenty of I2C controllers:

$ ls /sys/bus/i2c/devices/
i2c-0  i2c-1  i2c-2  i2c-3  i2c-4  i2c-5  i2c-6  i2c-7 i2c-8  i2c-DELL07E6:00

Those are likely hardcoded in the kernel. Say if you have a laptop or a mother board from a particular vendor, that vendor may have contributed a platform driver to the Linux Kernel. That platform driver would know what I2C devices are supposed to be attached to it, so they have an option to hardcode the list of devices.

For the USB-to-I2C controller it doesn’t make terribly a lot of sense, since it’s kind of the point that we can connect anything to it. Fortunately enough, there is another mechanism you can use to manually instantiate a I2C device.

For example, my FTDI-based I2C controller shows up in the list of I2C devices (when the driver is loaded) under the name i2c-8.

NOTE: I don’t think that i2c-8 is always guaranteed to be the name, it’s just happens in my system that it’s always the case. In general you can try to plug and unplug the cable to see what I2C device would appear/disappear from the list. Alternatively, you can just read the name file and see what it contains.

I can trigger instantion of I2C device attached to the controller in the following way:

echo wiichuk_i2c 0x52 > /sys/bus/i2c/devices/new_device

For this command to work I need a driver for the Nintendo Wiichuk to be loaded. The wiichuk_i2c part helps kernel to find the right driver for the device. wiichuk_i2c is the name recorded in the i2c_device_id structure in my case. The 0x52 part is just the I2C address of the device. Similarly, I can delete the device by writing 0x52 to the delete_device file.

Instead of conclusion

Finally I managed to make the platform and architecture independent device driver for Nintendo Wiichuk controller work on my laptop. Looking back at the whole path I can say, that I2C does not seem like a good option for pluggable/unpluggable devices as it lacks enumeration capabilities (though it’s likely cheaper if you consider USB licensing fees).

Aside from that, it doesn’t seem like using an FTDI MPSSE cable was a particularly wise choise economically speaking. The cable and delivery was pretty expensive, plus the amount of work required to make it do what I want is too much.

It was a fun educational exercise, but for anybody who just needs an I2C controller I’d recommend to buy a ready solution.

tags: usb - linux-kernel - ftdi - i2c