Introduction: Controlling LEDs Over USB With VUSB

There are plenty of instructions on this site for building LED mood lights, but I wanted a mood light that I could set to an arbitrary color. I also wanted to be able to control it through my computer, instead of using knobs or dials. This project shows you how to control LEDs (or anything else for that matter) via USB.

Step 1: V-USB

What's the problem?

Many AVR microcontrollers feature a UART, which allows serial communication with other chips or a computer via the serial port. However, not many newer computers come equipped with a serial port these days, fitted instead with USB ports. It's possible to communicate with an AVR chip over USB using a converting chip like the FTDI FT232RL found on the Arduino, but whether you use a board or a cable, you are adding cost and complexity to your project. If only there was an easy way to talk to the AVR chip directly over USB...

Enter V-USB

So what is V-USB? From their website (http://www.obdev.at/products/vusb/index.html):

V-USB is a software-only implementation of a low-speed USB device for Atmel’s AVR® microcontrollers, making it possible to build USB hardware with almost any AVR® microcontroller, not requiring any additional chip.

Sounds great! So what do we need to get started?

  • A copy of the V-USB code, which you can grab from the company's download page or their github page.
  • A version of the usbconfig.h file, modified for our specific project (more on this later).
  • An AVR microcontroller with at least 2 kB of Flash memory, 128 bytes RAM, that can run at 12 MHz, 15 MHz, 16 MHz or 20 MHz (with a crystal) or at 12.8 MHz or 16.5 MHz (with an internal RC oscillator).

Choosing a microcontroller

In addition to the requirements above, our microcontroller also needs 3 PWM channels, to control each of the RGB colors. I chose to use the ATmega168 because I had one lying around, but there are a couple different options (such as the ATtiny2313). If you do choose a different microcontroller, be sure to change the provided firmware code.

Step 2: Bill of Materials

What you need:

- a microcontroller that meets the criteria in the previous step

- RGB LEDs (see next step for what I used)

- an appropriate power supply to run your LEDs

- an old USB cable

For the circuit:

- 2x 3.6V Zener diode

- 2x 68Ω resistor

- 4x 1.5kΩ resistor

- 3x N-Channel MOSFET

- 16Mhz Crystal

- 2x 18pF capacitor

- Switch, SPST

- 2.1mm Coax Jack

- 5V Regulator

- 10uF capacitor

- 1uF capacitor

Also:

- a project box

- copper clad board and your etchant of choose

Attachments

Step 3: About the Lightbar

I forget where I picked up this lightbar (online somewhere). It features 18 groupings of a red, green, and blue LEDs. There is a wire for a common power source, and a separate ground wire for each of the colors. I run this lightbar off of a 12V wall wart. You can use any configuration of LEDs you want, but be sure to choose an appropriate power supply to go with it.

Step 4: The Circuit

The circuit is made of a couple different sections, each of which are pretty straightforward. EagleCAD files are attached.

Power Supply

The LEDs are powered by a 12V wall wart, and so we need a 5V regulator to step down the voltage for the micro. There is also a spot for two jumper wires, so that you can include a separate power switch.

LED Control

The three PWM channels on the micro are used to toggle three FETs, which in turn drive the LEDs.

USB Interface

The USB protocol uses two signal wires (D+ and D-) to produce a differential signal. That means, when one wire is pulled high, the other is pulled low, and vice versa. USB recognizes a signal of 0.0V - 0.3V as 'low' and a signal of 2.8V - 3.6V as 'high'. Sending information from a computer to the micro is no problem, as the micro can read 3.3V as 'high'. There is a problem, however, in getting the micro to send a 3.3V signal back to the computer. One solution is to run your micro on 3.3V. Another is to pull down the voltage on the signal wires. The VUSB wiki has a couple options for how to interface the computer's USB port with a micro. I chose Solution B so that I could run the micro (and thus toggle the FETs) with 5V instead of 3.3V.

Step 5: VUSB Files

Hop on over to the VUSB download page and grab the latest version of their library. Unzip the file and copy the usbdrv folder into your project folder (the same place where you are putting your source code). Open up the usbdrv folder, and there should be a file called usbconfig-prototype.h. Make a copy of the file and call it usbconfig.h. We have to modify this file to match our project. Luckily, all the VUSB files are well documented, so this isn't too difficult. I've attached the usbconfig.h file I used for this project, but you'll have to modify it if you use a different micro. Let's take a look at what you need to change.

usbconfig.h

The first thing we need to do is tell the software where the USB D+ and D- line are connected to the micro:

#define USB_CFG_IOPORTNAME      D
#define USB_CFG_DMINUS_BIT      3
#define USB_CFG_DPLUS_BIT       2

In our case, it's Port D, bits 2 and 3, which correspond to pins 4 and 5. The D- line can be connected to any bit in the port, but D+ must be connected to INT0. Comment from usbconfig.h:

/* This is the bit number in USB_CFG_IOPORT where the USB D+ line is connected.
 * This may be any bit in the port. Please note that D+ must also be connected
 * to interrupt pin INT0! [You can also use other interrupts, see section
 * "Optional MCU Description" below, or you can connect D- to the interrupt, as
 * it is required if you use the USB_COUNT_SOF feature. If you use D- for the
 * interrupt, the USB interrupt will also be triggered at Start-Of-Frame
 * markers every millisecond.]
 */

Next we have to make sure that the USB clockrate matches our AVR clockrate:

#define USB_CFG_CLOCK_KHZ       16000

Each USB device is hard coded with both a vendor ID (VID) and a product ID (PID). This allows a computer to easily find and assign the right drivers for each device. Since we don't need any drivers for our project, we could choose random numbers for the VID and PID. However, we run the (small) risk of accidentally choosing an existing ID, and confusing the computer. Luckily, you can register a unique VID with usb.org for the low low price of $5000. If you don't have that kind of cash on hand, VUSB provides a VID/PID pair that you can use for your project. Note that by using this VID and PID, this project falls under GPL. You can change these ID's if you wish, but note that these ID's must match whatever is in your code.

#define  USB_CFG_VENDOR_ID       0xc0, 0x16 /* = 0x16c0 = 5824 = voti.nl */
#define  USB_CFG_DEVICE_ID       0xdc, 0x05 /* = 0x05dc = 1500 */

Even though we are using a preexisting VID and PID, we can still provide a custom string for both the vendor name and device name:

#define USB_CFG_VENDOR_NAME     'e', 'x', 'a', 'm', 'p', 'l', 'e', '.', 'c', 'o', 'm'
#define USB_CFG_VENDOR_NAME_LEN 11

#define USB_CFG_DEVICE_NAME     'M', 'o', 'o', 'd', ' ', 'L', 'i', 'g', 'h', 't'
#define USB_CFG_DEVICE_NAME_LEN 10

Feel free to use your own email/website as the vendor name.

Step 6: Firmware Code

NOTE: Much of the code for this project came either wholly or in part from an excellent VUSB tutorial on the website codeandlife.com.

Make sure that your firmware code is saved in the same directory as the usbdrv folder you downloaded from VUSB (the usbdrv folder, in turn, should contain the modified usbconfig.h).

I won't go into the code too much. There are plenty of other 'ibles about PWMing LEDs, or setting up color changing mood lights. The only thing that is probably new is the code for interacting with the USB port. There are a couple different ways to transfer data across USB. The simplest way is by using a control transfer. A control transfer is typically used for configuration or status commands, but there is enough room to send all our data. A control transfer begins with a setup packet. From the libusb documentation, the setup packet has the following structure:

uint8_t bmRequestType	//Request type
uint8_t bRequest	//Request
uint16_t wValue		//Value
uint16_t wIndex		//Index
uint16_t wLength	//Number of bytes to transfer

bRequest, wValue, and wLength can all be application-specific. For our purposes, we will have a unique bRequest number for each different RGB channel (and one for fade mode). In the attached code it looks like this:

// USB control messages (same as PC software)
#define RED     0
#define GREEN   1
#define BLUE    2
#define FADE    3

We will use wValue to hold the user-specified intensity level (0-255). So for example, if we wanted to fully turn on the red LEDs, we would send a message with bRequest set to '0' and wValue set to '255'. If we wanted to turn off the green LEDs, we would send a message with bRequest set to '1' and wValue set to '0'. The VUSB code provides a nice way for our micro to extract this information from the setup packet in the form of the usbFunctionSetup() function, which is automatically called whenever the micro sees a setup packet.

Attachments

Step 7: Building the Unit

No pics from the build process, sorry. I only have pics of the completed unit.

At this point, you have a functioning mood light. When you turn on the unit, the firmware starts in 'fade' mode, and will work fine without a computer. You only need a computer if you want to manually control to color of the LEDs.

Step 8: PC-Side Software

NOTE: I am running linux on my home computer (Ubuntu 12.04), so these instructions will not work for a Windows machine as written. It is possible (though I haven't tried) to run this on Windows using something like MinGW. You will also need the Windows port of the USB library, libusb-win32.

NOTE: Much of the code for this project came either wholly or in part from an excellent VUSB tutorial on the website codeandlife.com.

The PC-side software is pretty straightforward. It takes input from the user, and attempts to send a USB control message to the connected USB device. libusb has a function usb_control_msg(), which we can use to create custom control messages. Here we specify bRequest to match the color channels in our firmware code, and set wValue to the value specified by the user. The code sends a separate control message for each RGB color channel.

MAKE SURE THE FOLLOWING CODE MATCHES WHATEVER IS IN YOUR FIRMWARE CODE:

// Same as in uC code 
#define RED 0 #define GREEN 1 #define BLUE 2 #define FADE 3 // used for helper function usbOpenDevice #define VENDOR 0x16C0 #define VENDOR_NAME "example.com" #define PRODUCT 0x05DC #define PRODUCT_NAME "Mood Light"

Step 9: Compiling PC-Side Software

NOTE: I am running linux on my home computer (Ubuntu 12.04), so these instructions will not work for a Windows machine as written. It is possible (though I haven't tried) to run this on Windows using something like MinGW. You will also need the Windows port of the USB library, libusb-win32.

Before we get to the code, we need to install the USB library:

sudo apt-get install libusb-dev

This will install the header file usb.h which is referenced by the code. Make sure that both the c file and header file attached in the previous step are saved in the same directory. Compile the code with:

gcc mood.c -lusb -o mood

Be sure to link to the USB library with -lusb. If everything went well, you should now have a new program called mood. Try running it:

./mood -r 255

Uh-oh, we get an error message saying Operation not permitted. This is because by default, all the USB ports are owned by 'root'. You can verify this by checking the file permissions of your USB ports (mine were located under /dev/bus/usb/bus#/device#). Rerun the mood program with root permissions:

sudo ./mood -r 255

The lightbar should turn bright red. Success!!! As one final step, you can copy the mood program to your /usr/bin/ folder, so you can run the program from anywhere on the computer (i.e. in a terminal you can run mood from anywhere, instead of running /path/to/wherever/mood):

sudo cp mood /usr/bin/

Step 10: Extra Credit: Udev Rules!!!

You might have noticed that you need to use 'sudo' to run the PC-side program. This is because by default, all the USB ports are owned by 'root'. You can verify this by checking the file permissions of your USB ports (mine were located under /dev/bus/usb/bus#/device#). What we need is a way to tell the computer to treat our new device differently. Luckily, there's an easy way to do this with udev rules.

First, create a new group for the device (and any future USB devices you might want to access):

sudo addgroup usb

Then, add yourself as a member of the new usb group:

sudo usermod yourusername -a -G usb

You might have to logout and log back in for the change to take effect. You can check all the groups that you belong to with the command:

groups

Now we can tell the computer how to single out our device. Create a new file in the /etc/udev/rules.d directory:

sudo touch /etc/udev/rules.d/usb.rules

Using your favourite editor, add the following line to the new file:

ATTR{idVendor}=="16c0", ATTR{idProduct}=="05dc", GROUP="usb", MODE="0664"

Translation: if the computer sees a device with our vendor id and product id (the same numbers from the firmware code and the PC-side code), then assign that device to group usb with privileges of 0664. Restart udev with

sudo restart udev

then unplug and replug the device's USB cable. Verify that the privileges changed (it might have gotten a new device# when you plug it back in). Congratulations!!! Now you don't need 'sudo' to run the program!!!