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 theusb_interface
pointer using theinterface_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 thename
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