Writing a USB Video Camera Driver, Part 1
By Todd Thomas <tt@be.com>

Beginning this week and continuing next week, I'm going to show you, function-by-function, how to write a BeOS driver for a USB video camera -- or at least how I wrote one. Before you get too excited, let me say that the cameras this driver will support are getting a bit long in the tooth, and may be hard to find in retail stores these days. Also, this driver requires support for isochronous transactions in the USB stack, so it will only work with the upcoming Release 5 of BeOS (a.k.a Maui).

Vision VLSI Ltd. <http://www.vvl.co.uk> has made documentation of their Colour Processor Interface ASIC (CPiA) available to parties that would like to write drivers for it. This documentation has been used to create a CPiA driver for Linux, and you can find links to the docs we'll use to write the BeOS driver at <http://webcam.sourceforge.net/#info>. The most important document is the Software Developer's Guide for CPiA Cameras <http://webcam.sourceforge.net/docs/developer.pdf>. You can also find a list of supported cameras at <http://webcam.sourceforge.net/#cams>. I'm using the Ezonics EZCam USB (not the Pro or Plus version), which I picked up at Fry's about a month ago. I'm not sure about the availability of any of the other cameras. Vision VLSI Ltd. is still around, still making ASICs that appear to use some version of the CPiA command set, so there may be some new cameras available that this driver could support with little or no modification. We'll see.

In this first article, I'll discuss how to implement basic driver functionality, and how to send CPiA commands to the camera so its various parameters and modes can be set. I'll cover some material that's been covered before, because hearing the same thing two different ways can be helpful. To quote our esteemed QA Manager <http://www.catastropherecords.com/StThomas.mp3>, "This is the long boring part." Next week, I'll tackle getting and processing video data from the camera. You can find the code for this week's incarnation of the driver at <ftp://ftp.be.com/pub/samples/drivers/CPiA.zip>. Grab it and follow along.

The driver comprises 3 source files: CPiA.c contains all function implementations; CPiA.h contains definitions that will be required by public clients of the driver, such as opcode constants and a parameter struct for ioctl(); and CPiA_priv.h contains typedefs and constants used privately by CPiA.c. Since there is only one source file, it it not strictly necessary for CPiA_priv.h to be a separate file, but it keeps things organized.

Exported Symbols

All BeOS drivers -- USB or not -- must export certain certain functions and values in order for the kernel to be able to load and interact with them. I'll refer to these as the exported symbols. They've been discussed several times before, but let's review them in the context of USB drivers and the CPiA driver in particular.

api_version

The first exported symbol is the easiest to implement: it's the int32 value api_version, and a driver simply needs to include the line

int32 api_version = B_CUR_DRIVER_API_VERSION;

to fulfill its obligation. B_CUR_DRIVER_API_VERSION is defined in <Drivers.h>, and as you might have guessed, it lets the kernel know which version of the driver API a driver uses.

init_hardware()

init_hardware() is called only once in a particular runtime session (i.e., from boot to shutdown). For devices that remain connected to the computer for an entire runtime session -- such as ISA and PCI cards -- it may make sense to use init_hardware() to see if the device is indeed connected and put it into a known initial state. However, it's not too useful in drivers for devices that can be hot-plugged while BeOS is running -- for example, USB devices -- because the device may not be plugged until after the first time the kernel loads its driver. Therefore, If you're one of those short attention span types who's been looking for init_hardware() in CPiA.c, stop: it's not there. Implementing init_hardware() is optional, and I chose not to for this driver.

init_driver()

init_driver(), on the other hand, is called every time a driver is loaded by the kernel, and must be implemented by all drivers. init_driver() should allocate resources that will be needed as long as the driver remains loaded. USB drivers should take this opportunity to introduce themselves to the USB (Bus) Manager like so:

/* declare these globally */	
static char* usb_name = B_USB_MODULE_NAME;
static usb_module_info *usb;

usb_support_descriptor supported_devices[] =
{
  0, 0, 0, VENDOR_ID, PRODUCT_ID }
};	

...

/* do this in init_driver() */	
if (get_module(usb_name,(module_info**)&usb) != B_OK) {
  dprintf(ID "cannot get module \"%s\"\n", usb_name);
  return B_ERROR;
}
dprintf(ID "usb module at %p\n", usb);
	
usb->register_driver(DEVICE_NAME, supported_devices, 1,
                     NULL);
usb->install_notify(DEVICE_NAME, &notify_hooks);

(Okay, you don't have to use dprintf() so gratuitously, but I like to. To review, dprintf() works just like good 'ol printf(), but it spews its bits to COM 1 so you can catch them on another computer using a null modem cable and a terminal program set to 19200 baud, 8 data bits, no parity, 1 stop bit, and no flow control. This is serial debugging, and it's pretty much indispensible for driver development. You can turn on serial debugging by hitting the delete key as soon as the BeOS boot screen comes up, or during runtime by hitting alt-printscreen-d to jump into the kernel debugger, then typing c to continue.)

The code above gets a pointer to the USB Manager module by calling get_module() with the name B_USB_MODULE_NAME. It then calls the USB Manager's register_driver() function to tell the Manager which devices it would like to receive notifications about. For CPiA, I specify the vendor and product fields of the usb_support_descriptor explicitly; for more generic drivers, you can specify something other than the wildcard 0 for dev_class, dev_subclass, and/or dev_protocol fields. How can you find out what the vendor and product IDs of some random USB device are? It's as simple as plugging the device in with serial debugging turned on. Among lots of other useful output, you'll see a line like

vendor/product id:		0x0553 / 0x0002

Next, the USB Manager's install_notify() function is called, which provides the addresses of the hook functions that will be called when various events occur on the USB bus. Currently there are only two hooks defined:

typedef struct usb_notify_hooks {
  status_t (*device_added)(const usb_device *device,
                           void **cookie);
  status_t (*device_removed)(void *cookie);	
} usb_notify_hooks;

As you may have guessed, device_added() is called whenever a device matching the usb_support_descriptor provided in register_driver() is added to the USB, and device_removed() is called when it is removed. You may not have guessed that device_added() is also called after you call install_notify(), and device_removed() is called after you call uninstall_notify(), if the device is connected at those times.

Lastly, CPiA's implementation of init_driver() creates a semaphore to synchronize access to the information about the currently connected camera (remember that multiple threads can be executing a driver's code, so protect that global data!). The semaphore name refers to a "table" of devices, but currently the CPiA driver only supports one camera as a simplification. However, next's week's version may support more than one camera, and there is some code in this version that is designed for that possibility. The number of devices a driver may support has no artificial limit in BeOS, but there are some practical concerns specific to each device that should be considered. For example, USB bandwidth is theoretically 12Mbps, so supporting two 10Mbps USB devices may not work out well.

publish_devices()

publish_devices() is called by the kernel after init_driver() to get the names by which the driver wishes to be known in the /dev heirarchy. CPiA is a video device, and a USB one at that, so it returns "video/usb/CPiA/0" from this function if there is a device connected at the time it's called. If the driver supported more than one device, they would be published as /dev/video/usb/CPiA/0, /dev/video/usb/CPiA/1, and so on. Furthermore, a single driver can publish multiple logical devices of different functionality if the physical device has such capabilities. For example, many sound cards also have a MIDI port; a driver for such a card might publish devices in both /dev/audio/ and /dev/midi/.

Before the dev_names string array is filled out, though, it is freed if it remains filled out from a previous call. The only safe times to free this array are in publish_devices() after the first time it is called, and in uninit_driver(). At any other time the array may still be in use.

find_device()

find_device() is called by the kernel when a potential client calls the POSIX function open() on a driver, so the driver can provide its implementation of open() and its friends close(), ioctl(), read(), write(), etc. It does this by returning a device_hooks structure filled out with the addresses of its implementations of those functions. If a device supports multiple logical devices of different functionality as described above, it should check the argument of find_device() to see which logical device is being opened, and provide the corresponding set of hooks. The implementation of these hook functions will be discussed next week.

uninit_driver()

uninit_driver() is called just before a driver is unloaded by the kernel. It's the last chance to free resources allocated in init_driver() and elsewhere. In the CPiA implementation, the dev_names string array used in publish_devices() is freed, the USB Manager function uninstall_notify() is called to inform the Manager that the driver will no longer be requiring notifications, the  dev_table_lock semaphore is recycled, and -- lastly -- the "connection" to the USB Manager is severed. At this point, that certain lady has finished singing for the CPiA driver.

USB Notification Hooks

As introduced in init_driver() above, USB drivers must implement a set of hook functions that are called when certain events occur on the USB.

device_added()

The main tasks to accomplish in device_added() are creating and initializing a cookie containing device state information and resources, and verifying that the device has the capabilities we expect and that it is working properly.

The CPiA device cookie is defined in CPiA_priv.h and looks like so:

typedef struct device device;
struct device {
  usb_device*  dev; /* opaque handle */
  bool         connected; /* is the device plugged into the
                          ** USB? */
  int32        open_count; /* number of clients of the
                           ** device */
  int32        num; /* instance number of the device */
  uchar        vers[4]; /* data returned from
                        ** GetCPIAVersion command */
  sem_id       lock; /* synchronize access to the device */
  sem_id       cmd_complete; /* available on completion
                             ** of CPiA command */
  area_id      buf_id; /* ID of buffer for data returned
                       ** from CPiA command */
  void*        buf; /* data returned from CPiA command */
  size_t       actual_len; /* length of data returned by
                           ** CPiA command */
  status_t     cmd_status; /* result of command */
  /* temp */
  usb_pipe*    video_in;
  uint16       max_packet_size;
};

The comments should give you a rudimentary idea what each field is used for. Perhaps the most important field is dev, which is the handle to the USB stack's notion of the device. It's opaque to clients of the stack, but must be passed to any USB Manager functions that need to know which device you'd like to operate on. The fields after /* temp */ will most likely be moved to another cookie next week.

For CPiA, I've broken out the cookie creation and initialization process into two additional functions. add_device() allocates  memory for a device structure and adds it to the device "table", which is currently a single pointer named connected_device. Again, only one connected device is supported right now for simplicity. add_device() in turn calls init_device(), which sets the fields to sane values and allocates the resources they represent.

If add_device() succeeds, device_added()'s cookie parameter is set to point to the newly created device structure. This pointer will be passed to the device_removed() USB notification hook so its resources can be properly reclaimed. It will also be passed to each of the device hooks described below. Any device-specific data that needs to persist between USB notification hook calls and device hook calls needs to live in the cookie.

After successfully setting up the cookie, the CPiA driver issues two CPiA commands to the camera, to get its version information and to check it for any errors that may have been discovered during its Power On Self Test (POST). The workings of issue_cpia_command() will be discussed in the next section.

device_removed()

device_removed() marks the device as being removed. If the device's ref count (dev->open_count) is zero, no clients have the device open, so its cookie is freed by calling remove_device(), which in turn calls deinit_device() to free the cookie's dynamically allocated resources.

Issuing CPiA Commands

Vision VLSI thoughtfully gave CPiA commands the same format as USB SETUP packets, which means it's a cinch to issue them to the camera using the USB Manager's queue_request() function.

First, a bit of terminology. USB is completely host-centric and host-controlled, where "host" means the USB controller to which USB devices are connected. All movement of data is described with reference to the host. OUT always means movement from the host to a device on the bus; IN always means movement from a device to the host. Devices cannot initiate data transfer on their own, nor can they communicate directly with each other. All data transfer is initiated by the host.

There are 4 types of USB data transfers: bulk, interrupt, isochronous, and control. All USB devices support control transfers and have an endpoint (a data sink and/or source) dedicated to them. A control transfer begins with the host sending an 8-byte SETUP packet to the device. The device may then respond with a DATA IN packet.

As documented in the Software Developer's Guide for CPiA Cameras, each CPiA command consists of 8 bytes in the SETUP packet format. The first byte indicates the type of command (whether or not a DATA IN packet will follow the SETUP packet). The second byte uniquely identifies the CPiA command. The next 4 bytes can be used individually or in various aggregations as parameters to the command. The last 2 bytes give the size of the following DATA IN packet, if any.

The function issue_cpia_command() in CPiA.c makes it convenient to issue a command exactly as it is presented in the Developer's Guide, because it takes each of the 8 bytes of the command as a separate parameter. It's really just a wrapper for the USB Manager's queue_request() function. queue_request() takes the same 8 bytes as parameters, although it slightly complicates matters by combining the latter six bytes into 3 shorts.

The "queue" in its name should be a clue that queue_request() operates asynchronously; consequently, it also takes as a parameter a function to be called when the transaction is complete. To wait for that completion before returning, issue_cpia_command() immediately tries to acquire a semaphore in the device cookie that has been initialized to 0 -- thus the call to acquire_sem() blocks. That semaphore is only released in the completion routine cpia_command_callback(), after the requested data (if any) has been copied into the cookie's buffer.

The strategy of using a zero-initialized semaphore to wait for completion of an asynchronous function will come in handy again next week when we start streaming video data from the camera. The CPiA uses isochronous transfers for that purpose, and the USB manager has a queue_isochronous() function we'll use to fill our buffers with pixels.

Device Hooks

In addition to the exported symbols, all BeOS drivers must also implement certain functions that provide the functionality of the familiar POSIX functions open(), close(), ioctl(), read(), write(), etc. I'll call these functions the device hook functions. As they are accessed through the function pointers in the device_hooks structure returned by find_device(), you can name them anything you want (unlike the exported functions), but it is customary to name them <logical device>_<hook name>. For example, the hook functions for the MIDI part of a sound card driver might be named midi_open(), midi_write(), etc. Since CPiA only publishes one kind of logical device, I'll just use device_open(), etc.

Our time's almost up for this week, however, so we'll discuss the implementation of the device hook functions next week.

So What?

The CPiA driver is not very exciting yet. Well, there's a bit more implemented in the sample code than what I've covered so far, including the device hooks. Feel free to explore those, especially device_control(), where you may find some more interesting stuff. The file test.cpp also contains the beginnings of a basic test harness we will use to exercise the driver. 'Til next week...
