Check out the downloads page for a barebones USB script that lets you send commands over USB, and some basic code you can throw on an AVR chip to try this out!
I am currently taking a class called “The Principles of Engineering”, AKA POE. The name of the class is very old-school and almost baroque, but really what it boils down to is twenty engineers of various flavors and a professor sitting in a room for a few hours a week. There are no lectures, there is next to no instruction, and only four assignments. the first three are based on a PIC microcontroller, and the last assignment is an open ended project where students in teams of 4-5 build something with a “significant electrical and mechanical component”.
POE lab three “One Button, Two Blinky Lights, and the USB” was designed around using a PIC. Naturally, my team took this to be a challenge. Out of dislike for MPLab (PIC IDE), PICs, and generally being iconoclasts in POE, we decided to forgo the pre-written USB implementation on the PIC, the python that interfaced with it, and the GUI framework that was supplied to us, in favor of doing it all ourselves. We decided to do this, and then we waited until the day it was due to start working on it.
With t minus 10 hours, we decided to make a laser turret. Our reasoning was:
- We have servos
- We have a laser
It was around 12:30 when we got started implementing V-USB, PyUSB, a TKinter interface, and some AVR C (windows users: winavr) code to run the servos. I would like to give a huge thanks to Kevin Mehall here, who is a massive baller when it comes to things like V-USB and microcontrollers for helping us out with some bugs we were having. This writeup is so that you all don’t have to go through the same stuff we did to get V-USB working.
Implementing “The V-USB”
V-USB is a Virtual USB port for AVR chips. This lets you talk to them over USB, or in this case, to make them into a USB Device. It’s a beautiful thing, but is totally a hack. This project is a good example of the simplest thing you can do with V-USB, which is send some data to the device with a control transfer.
Start by making a directory for your project. The next step of getting V-USB to work is getting the header files from the downloads page of V-USB. For those of you on linux machines, just tar -xzfv filename. Then copy the files into the project folder. You will also need a makefile. You can get a makefile from the examples on the V-USB website, or you can get the one I used (originally from the V-USB website) on the downloads page, so you don’t have to download the whole thing.
Include the usbconfig.h, and usbdrv.h in your file. To get this to work, the file that you will have to play with to get V-USB working is called usbconfig.h. Find it, and open it in gedit or bluefish or your text editor of choice. MAKE SURE IT IS IN USBDRV. All of the options are described very clearly, but the ones that are important and that you should look at first are:
- All of the hardware config
- Optional hardware config
- #define USB_CFG_HAVE_INTRIN_ENDPOINT 0
- #define USB_CFG_IS_SELF_POWERED 0
- #define USB_CFG_MAX_BUS_POWER 400
- #define USB_CFG_VENDOR_ID 0xc0, 0x16
- #define USB_CFG_DEVICE_ID 0xFF, 0xFF
- #define USB_CFG_VENDOR_NAME ‘A’, ‘L’, ‘O’, ‘U’, ‘I’, ‘E’, ‘1’, ‘4’
#define USB_CFG_VENDOR_NAME_LEN 8
- #define USB_CFG_DEVICE_NAME ‘P’, ‘O’, ‘E’, ‘J’, ‘e’, ‘c’, ‘t’, ‘3’
#define USB_CFG_DEVICE_NAME_LEN 8
The hardware config will depend on your circuit, and I will talk about that in a moment. The other options (in order that they are listed) make it so you have an endpoint 0 (default), tell the USB controller that the device is powered by your USB port, set the max current the USB port will provide, set a vendor ID (I used the default), a device ID (anything can go here), a vendor name and length of name, and a device name and length of name.
Below is a diagram of the circuit I used. It is good for illustrating how to set up usbconfig.h based on your hardware, but you might not use the same AVR so your mileage may vary.
My hardware file looks like:
#define USB_CFG_IOPORTNAME D
As you can see, we are using pins PD2 and PD3 for D+ and D-. The port we are using is D, so we define USB_CFG_IOPORTNAME to be D.
#define USB_CFG_DMINUS_BIT 3
#define USB_CFG_DPLUS_BIT 2
These tell V-USB which pin on the chosen port (D) D+ and D- go to. Note, D+ goes to PD2, which is pin 4. We use the name of the port not the name of the pin, because the C code accesses the pins by port, not pin number.
#define USB_CFG_CLOCK_KHZ (F_CPU/1000)
#define USB_CFG_CHECK_CRC 0
These are set correctly.
#define USB_CFG_PULLUP_IOPORTNAME D
#define USB_CFG_PULLUP_BIT 4
This sets up a pullup resistor so we can connect or disconnect the device with software commands. The resistor is the 1.7 K ohm resistor in the schematic. If this is not set, that resistor should be connected to D- and 5V.
One last thing that I did, specifically for this hardware, was calibrate the internal RC oscillator so that we didn’t have to use an external crystal. This is achieved by copy-pasta into usbconfig and into your C code. documentation can be found here if you are interested: http://vusb.wikidot.com/examples. Look for “Clocking the AVR from the RC oscillator with auto calibration” on that page.
Now that that is done, you should be able to make (just go to the folder in the terminal and type make) your project. If you get some error about not having a clock frequency (F_CPU) add this line “-DF_CPU=12800000” to the COMPILE line in your makefile. Replace 12800000 with your clock frequency.
Now you probably have some bug complaining that “usbFunctionSetup” is not defined. We are going to need to define this, because V-USB needs this to be set up to function. This implements the ability to perform a control transfer, which is needed to set up anything over USB. My code looks like this:
usbMsgLen_t usbFunctionSetup(uchar setupData)
usbRequest_t *rq = (void *)setupData; // cast to structured data for parsing
This is just an example of totally abusing the control transfer. In this project, I just wanted to send a few bytes to the AVR to tell it how to control the servos and the laser, so I used the values for bRequest, wValue, and wIndex to hold the information. These are just part of the control transfer request, which has these arguments:
(bmRequestType, bRequest, wValue, wIndex, wLength)
These are sent from the computer (from a python program) to the AVR. bmRequestType tells the device what kind of request it is, bRequest is the name of the request, and wValue, wIndex I (ab)used to send data, and wLength, is the amount of data that is supposed to be sent over the control transfer.
Normally these are actually used to set up a data transfer, but I used them to send over a couple bytes of data in bRequest, wValue and wIndex. Here is an example (from my python interface) of a request:
This means that it is an device to host transfer (0x40 is the number for a vendor specific control transter) and that the request number is 1, and wValue and wIndex are 0. The last argument is None, because no data is being sent, but this is python (pyUSB) specific.
Implementing “The PyUSB”
Once I had the hardware usb code for the chip compiled and working, I started to work on the python that would talk to the chip. I had a lot of help from examples from Kevin, because the sum of the documentation for PyUSB that I could find was about 4 pages, and it was not very specific or well written. The important lines (for the whole dang thing, see downloads!) were:
This makes an object dev that is attached to a specific Vendor and Product id, provided usb.core.find can find it. If it can’t, you should catch that error with an if statement that looks something like “if not dev:”, or “if device is none”.
the other important line is:
print(‘laser cannon enumerated’)
except usb.core.USBError, e:
This tries to send a control transfer request out, and if it is successful it prints “laser cannon enumerated”. If we run into some sort of error, it prints “exception”, which lets us know our last command didn’t go through. This is useful for debugging, but once it was buried under the TKinterface, we commented those commands out. This try…except block is important because without it, if the transfer gets interrupted for some reason, the python program will freak out and stop working.
Again, if you want to get your feet wet, I have put some basic code on the downloads page to play with, modify, and learn from.