Usb devices are far more complex than simply pipes you read and write. You'll have to write code to manipulate them. You don't need to write a kernel driver. See http://libusb.info (née libusb.org) and http://libusb.sourceforge.net/api-1.0. This claims to work with Linux, OSX, Windows, Android, OpenBSD, etc. Under Mac OS X there are user-level functions in I/O Kit that will let you access USB. Under Windows, you may be able to use WinUSB, but it's complicated.
Here's a little diagram I drew once to help me understand the architecture of USB:
┌──────┐ │ device ┌─────┐ ┌─────────┐ │
│ Port ├──┐ │ ┌─┤ EP0 ├──┤ control │ │
└──────┘ │ │ ┌────────┐ │ └─────┘ ├─────────┤ │
├────┤addr = 2├─┤ ┌─────┐ │ │ │
│ │ └────────┘ ├─┤ EP1 ├──┤interface│ │
│ │ │ └─────┘ │ #0 │ │
│ │ │ ┌─────┐ ├─────────┤ │
│ │ ├─┤ EP2 ├──┤ │ │
│ │ │ └─────┘ │interface│ │
│ │ │ ┌─────┐ │ #1 │ │
│ │ └─┤ EP3 ├──┤ │ │
│ │ └─────┘ └─────────┘ │
Executive summary: every device has an address (assigned by the O/S and subject to change), and up to (I think) 32 endpoints.
Within the device are one or more "interfaces". For example, a web cam might provide a "camera" interface and a "microphone" interface. A multi-function printer would provide several interfaces.
Endpoint 0 is used for control and configuration of the device, and the others are to access the various interfaces. Each interface has zero or more (usually more) endpoints.
Endpoints can be one of several transfer types:
- Control Transfers are used to query and configure the device. Every device is required to support a minimum set of control statements. I believe that control transfers are only used with endpoint 0.
- Bulk Transfers send or receive data at full bandwidth
- Interrupt transfers (I'm not really sure how this is different from bulk transfers; USB doesn't have interrupts). Examples include keyboard and mouse
- Isochronous transfers send or receive data at full bandwidth with realtime requirements but without reliability. Used for audio/video applications.
Also worth noting: a USB device can have multiple configurations, which control what interfaces are available and so forth. Changing a device configuration is almost like unplugging the device and plugging a different device in its place.
All of this information is laid out in device descriptors, config descriptors, interface descriptors, endpoint descriptors, etc., which can be queried via endpoint zero.
(Internally, data isn't a stream of bytes, it's packed into packets whose exact formats are part of the USB specification. For the most part, you don't need to worry about this as the controllers and drivers will manage this part for you.)
In practice, depending on your API library and operating system, you'll need to detect the device, read the various descriptors to find out what you're dealing with, optionally set its configuration (if the OS permits), open the interface, and open the endpoints.
For bulk endpoints, you can read and write raw data to them. For control transfers, the API library will provide function calls. I've never used interrupt or isochronous transfers; I'm sure your API library will have the relevant documentation.
More info: "Function" is a collection of interfaces that work together. It's not originally part of the USB spec, and it's up to the device driver to know which interfaces should be grouped together. The USB Working Group has defined device classes which support functions. This is done via the Interface Association Descriptor (IAD).