This project demonstrates a novel repurposing of the existing display control lines present on many graphics cards as an inexpensive method of interfacing computers with a variety of sensors and devices.
I2C (Inter-Integrated Circuit) is a two-wire serial bus typically used inside computers for low-level communication between components, but it’s also seen in robotics and hobbyist electronics for interfacing all manner of sensors, displays and actuators. I2C connections are often readily available on microcontrollers and esoteric embedded systems, but there’s traditionally little call for end-user access to this bus on mainstream personal computers. Lately though, netbooks and other small form-factor systems are increasingly being put to use as the high-powered “brains” of many homebrew projects. To sense and react in the physical world, a USB to I2C adapter (or “bridge”) device is typically used, often at considerable expense.
DDC, supported by most graphics cards and monitors produced since the late 1990s, is a communication channel within a video cable that allows the computer and monitor to negotiate mutually compatible resolutions and permit software control of functions normally accessed with physical buttons on the display.
DDC is, in fact, simply an implementation of an I2C bus with a few established rules. By tapping into this connection between the computer and monitor (or better yet, making use of the DDC lines on an spare unused video port, such as the external monitor connection on a laptop), one can interface with many I2C devices at virtually no expense, bypassing the usual need for an adapter device entirely.
Commercial USB to I2C adapters can cost $100, $250, sometimes even more, and driver support is spotty for systems outside the popular Windows fold. Using an Arduino or other USB-capable microcontroller as an intermediary is one affordable alternative. But the method outlined here requires nothing more than a modified video cable…I just bought one at a garage sale for 50 cents, and that’s enough to make two such adapters!
Being a quick and dirty hack, there are some limitations to this approach:
- Most significantly, it does not work in Windows at present. I’ve pursued a number of avenues but have yet to produce a working equivalent for that operating system. Please see the notes at the end of this article regarding the current state of Windows affairs.
- Even among Macintosh and Linux systems, the compatible range of hardware is limited. The graphics card driver will determine whether application code is allowed access to the DDC lines at all.
- On the Mac, systems with Intel integrated graphics or ATI chipsets appear to support this scheme, while those with NVIDIA graphics are out of luck. As nearly the entire current Mac lineup has now transitioned to NVIDIA graphics, the usefulness of this hack is relegated mostly to earlier Mac models and just a handful of current iMac and Mac Pro configurations.
- I’ve had few opportunities to test with Linux systems thus far, but ATI and NVIDIA graphics seem to show solid support while Intel integrated (e.g. GMA 910) was a no-go.
- Not all I2C devices are compatible with this approach, and the selection varies somewhat by operating system. There seems to be some variation in the allowable range of serial timings sent or received by both the host and I2C device, and in some combinations these ranges just don’t overlap. For example, a Nintendo Wii Nunchuk controller will work with Linux but not Mac OS X.
- This is limited to being a “single-master” I2C bus; one can poll devices for their state, but there’s no means for the computer to respond to events that originate elsewhere on the bus. And available power is limited to 5 volts at a scant 50 milliamps.
- There’s always the possibility of damage by incorrect wiring or misplaced voltages. Attempting this hack at all is likely a warranty-voiding operation and I assume no liability for damage inflicted, so tread forth carefully and always mount a scratch monkey!
There are still plenty of fun projects that can be attempted, so with those caveats out of the way, let’s proceed! All that’s required is a modified video cable, or, if tapping an unused video port, simply the appropriate end connector (but it’s often cheaper to simply buy the full cable and cut it in half). If the port will still be used for a monitor connection, it’s necessary to create a “Y” or “hydra” cable that allows one’s I2C device(s) and the monitor to be attached at the same time (sharing the DDC lines).
|Single-Ended Cable||“Y” Cable|
|Connects to an unused video port.||Shares video port among monitor and I2C device(s).|
When modifying an existing cable, after removing just the outer shielding, the DDC wires are usually visible at this point, while the lines carrying video signals are wrapped inside further-shielded bundles. A multimeter or continuity tester will help correlate wires to pins. DDC establishes four wires: +5VDC, ground, serial data and serial clock, the pin numbers of which will vary depending on the type of video port used. Refer to pinouts.ru, Wikipedia or other sources if you need more help than the diagrams below can provide.
The pins of interest are as follows:
|VGA:||DVI:||HDMI (Type A):|
* Note that on some VGA cables (perhaps even a majority), pin 9 is not present (occasionally others as well). Just examime the male end connector to see if it’s even worth dissecting — the missing pins are quite apparent. You’ll want to use a cable with the full complement of pins, or work from just a bare D-sub 15 connector (if scrounging at the local swap meet for cheap cables is not your style, these connectors can be found at Radio Shack for $1.99).
The DDC clock and data lines correspond directly to I2C clock and data. Pull-up resistors are not required — that’s already implemented in the graphics card — so it’s just the cable, your I2C device(s) and a bit of wire.
Connect the corresponding I2C lines to your device as required, and the other end of the cable to the desired video port. If using a “Y” splitter cable with a monitor connected, one’s power budget should be trimmed by a few extra milliamps to ensure enough power for the DDC chip within the display. There should be enough current to drive a microcontroller and an LED or possibly two…but if the I2C device(s) to be connected are going to use any substantial amount of current, it may be wise to power the circuit externally and use a I2C buffer or, if the device operates at a different voltage, a logic level converter (such as SparkFun part BOB-08745). Also, if using with a display connected, two I2C addresses are reserved for the monitor and should not be used: 0×37 and 0×50.
While the gritty details of the I2C protocol are taken care of by the i2c.o library (provided below) and the OS-specific libraries on which it in turn depends, the specific message sequence required by any given device must be implemented within one’s own code and will vary from one device to the next; the library does not automatically handle all this. Manufacturers’ datasheets will document the I2C messages needed, and the amount of code required is not onerous. Several short example programs are included: reading a Nintendo Wii Nunchuk controller accessory (currently Linux only), reading a Microchip TCN75A temperature sensor, writing to a Microchip 256 Kbit serial EEPROM, flashing a BlinkM “smart LED,” and another that interfaces with a Mindsensors I2C-SC8 8 channel servo controller.
|Bringing it All Together|
|Here’s a quick test case involving multiple I2C devices: the computer (out of frame, to the right) reads the current temperature from a sensor, then updates a hobby servo being used as a makeshift dial indicator. The readings are also logged to EEPROM for posterity. The example code can do all of these functions, plus others.|
No special procedure is required for Mac systems, aside from having GCC installed in order to compile the code. GCC is one component of Xcode, an optional installer on your original Mac OS X DVD-ROM, or can be downloaded from Apple’s developer web site. Rather than get into the whole graphical IDE, I’m simply invoking the compiler from a Terminal window. If you have a supported video card and one of the compatible I2C devices attached, simply compile (“make all”) and run (e.g. “./temperature”). You can skip the rest of these directions and go right to the “Using the Library” section.
While the source code for Linux is simpler, the setup procedure unfortunately is not. You’ll need root access. Frequently. This is how I install it under Ubuntu 7.04 and later:
- Install the lm-sensors package:
sudo apt-get install lm-sensors
- Enable kernel modules for I2C and the specific video driver:
sudo modprobe i2c-dev
sudo modprobe radeonfb
The above is for an ATI Radeon. You might need a different module specific to your system. Use the command “sudo modprobe -l” (that’s a lowercase L, not a one) for a full list of kernel modules, and try to locate one that matches your graphics chip vendor.)
Alternately, to load the I2C modules automatically at boot-time, edit the file /etc/modules and add these lines:
Again, assuming Radeon graphics here; put your actual module name there.
Once the kernel modules are loaded, the files /dev/i2c-* will then correspond to each I2C bus on the system. Not all of these relate to video out though; some may correspond to internal I2C buses in the computer (e.g. system health, RAM controller, etc.). The i2cdetect program (installed as part of lm-sensors) will list and briefly describe each I2C interface present. Look for the one that matches the desired video port. On the ThinkPad I used for development, /dev/i2c-2 corresponds to the VGA port. If the output of i2cdetect does not mention any video ports (VGA, DVI, or HDMI), then it’s probable that the driver does not support I2C and this hack will not work, or a different video card module may simply need to be loaded.
- Edit the Makefile, commenting out the Mac-specific lines and enabling the Linux-related flags. Then “make all” to build the library and example programs.
- Access to the /dev/i2c-* devices is normally available only to the root user, so there are a few options for invoking the example programs. The most sensible and controlled is to run each with the sudo command, e.g. “sudo ./nunchuk”. If you’re on a personal system and can afford to be less pedantic about security, other options include logging in and running the programs as root, chown/chmod-ing the executables to root and enabling the setuid bit, or chmod-ing the /dev/i2c-* files to enable read/write access for all users (which would need to be reapplied after each reboot).
Using the Library
The C API is super-basic, with just three functions:
Opens the I2C (DDC) bus for subsequent communication. As some systems may support multiple “heads” and thus have multiple DDC interfaces, the code in i2c-osx.c or i2c-linux.c may to be adapted to your specific situation. On Macs the code is currently rigged to use the last connection found; on a single-head system (e.g. Mac mini), this would be the single video out port, while on a potentially two-headed system (e.g. MacBook, late-model iMac) this would be the video out port, regardless of whether there’s a monitor attached. Multi-headed systems (e.g. Mac Pro) may require some tweaks to the code to access a specific graphics card and port. For Linux, the code is rigged to open /dev/i2c-2, which corresponds to the VGA connector on my ThinkPad system used during development, but you’ll probably want to change this for your particular situation. Review the notes above regarding Linux installation.
I2Cmsg(short address, unsigned char *sendBuf, int sendBytes, unsigned char *replyBuf, int replyBytes)
Issues an I2C request and/or reads response over the previously-opened I2C bus. The address of the device is often factory-defined, and the size and content of the request data are entirely device-dependent (see the example code for several practical cases). You’ll need to work with the datasheet provided for your specific device.
An important point should be made here regarding the first parameter: there is some disagreement as to just what constitutes a valid I2C address. The I2C protocol defines data packets in 8-bit chunks, with the first byte of an I2C message containing a 7-bit device identifier and one bit indicating whether this is a read or write operation. Most presume the 7-bit value to define the address, but in some implementations they’ll include the read/write bit in referring the address, or — because the R/W flag is the least signficant bit — document the device as having two sequential addresses (one each for reading and writing). Linux and OSX disagree here; this API follows the 7-bit convention and adjusts the value passed as needed for the host operating system. If a device does not seem to be responding at the address given in the datasheet, try dividing the number by two, which shifts off the R/W bit to produce a 7-bit address.
|This example from a Microchip datasheet shows an 8-bit I2C address byte that includes a ‘write’ bit. But other documents (and this API) follow the convention of calling addresses by the 7-bit component only.|
Closes the previously-opened I2C bus.
Return values, defined in i2c.h, are currently very limited. All of the functions will return a value of zero (or I2C_ERR_NONE) on successful completion. The I2Copen() function may return I2C_ERR_OPEN if no available DDC bus can be found (or, on Linux, if not running as root). On the Mac, I2Cmsg() may return various error values defined in /System/Library/Frameworks/IOKit.framework/Headers/IOReturn.h (e.g. 0x2c0 or kIOReturnNoDevice).
The library is not especially comprehensive; there are quite a few limitations and assumptions made at present (for example, only one I2C bus can be open at a time). Consider this just a starting point for your own code. The core meat-and-potatoes of opening and communicating over the bus can be distilled to just a couple dozen essential lines of code, the rest being error handling, packaging and comments.
Microsoft Windows Status
As explained earlier in this article, I currently do not have a working implementation of this library available for any version of Windows yet. Honest, this is not just asinine OS bashing! Admittedly the platform is one of the less familiar among my development skills, but I have made several honest attempts, still to no avail. If Windows is your native tongue and you’d like to give it a try, I can offer some possible leads:
- In my earliest attempts, web searches for information suggested that the Windows API calls for handling DDC (and thus I2C) were undocumented and proprietary. More recently, reader S. Manabe brought this article at Windows Hardware Developer Central to my attention. The talk of WDM Streams and minidrivers quickly goes over my head, but perhaps Windows-savvy developers could make use of it.
- For users with NVIDIA graphics cards, N. Ekstrom alerted me of the existence of the NVIDIA API and corresponding documentation. I’ve only had time to briefly dabble with this and have produced no working results yet. The documentation also mentions a Mac version of the API, but it seems no library for that platform is available for download.
- Nicomsoft’s WinI2C-DDC appears to have comprehensively solved this problem across all manner of PC hardware, and continues to be actively developed. Unfortunately it costs $495 for a developer license (the $249 Lite version doesn’t support low-level I2C access). A 30-day time-limited evaluation can be downloaded from their web site free of charge. But in the long run, casual tinkerers might prefer a less expensive USB (or even parallel port, if you still have one) hardware solution (or getting the hang of Linux).
- On the hardware front: Aside from the obvious Arduino option, FTDI’s FT2232H and FT4232H multipurpose USB to Multi-Protocol Synchronous Serial Engine chips may provide an inexpensive hardware connection. Evaluation modules from FTDI and DLP Design can be acquired from the likes of Digi-Key or Mouser for under $30. I’ve not yet experimented with the I2C functionality specifically, but the datasheet looks promising and I’ve had tremendous success with other features of FTDI’s chips — watch for a follow-up article at some point.
Much thanks to Benny Hsieh for donating development hardware which made the Linux port possible, the authors of ddcci-tool (now ddccontrol) for Linux I2C programming insights, and Tod Kurt of ThingM for entrusting me with one of his BlinkMs for testing at the 2008 Bay Area Maker Faire.