V-USB with ATtiny45 / ATtiny85 without a crystal

One guy at Hack a Day remarked on the long wire runs in my V-USB tutorial breadboard setup. So I thought I’d build upon the part 4 of the tutorial but modify the setup a bit to run the AVR at 5 volts and use zener diodes to drop D+/D- voltage, thus eliminating the need for a regulator. And why not stop there. ATtiny45 and ATtiny85 are smaller than ATtiny2313 and have an internal oscillator that can be calibrated to provide 16.5 MHz clock, accurate enough for V-USB to do its magic. I challenge anyone to drastically shorten these wire runs!

In the photo, I used a 4-pin header to show the place of the USB cable so the zener diodes would not get obstructed. Note that due to the angle it can seem like the 0.1 uF tantalum cap (light brown one) is wired to PB4 when it really is going to GND pin! Here’s the schematic, heavily borrowed from V-USB’s EasyLogger reference implementation:

Note: 2.2 kohm pullup for D- was missing in the schematic, it has now been corrected.

Main code changes needed are the change of pins to PORTB. Also, the oscillator calibration routine needs a hook added to the config. Here are the changes I made to usbconfig.h:

#define USB_CFG_DMINUS_BIT      1
#define USB_CFG_DPLUS_BIT       2
#define USB_CFG_CLOCK_KHZ       (F_CPU/1000)

// The following needs to be uncommented
#define USB_RESET_HOOK(resetStarts)     if(!resetStarts){hadUsbReset();}

// Add the following after USB_RESET_HOOK
#ifndef __ASSEMBLER__
extern void hadUsbReset(void); // define the function for usbdrv.c

Note that USB_CFG_CLOCK_KHZ now relies on F_CPU constant. Furthermore, F_CPU can be set in the Makefile by passing -DF_CPU=16500000 as a command-line argument to gcc, so defining it can also be omitted from main.c. Another thing we need to change in the makefile is to set mcu/part code to attiny85 for both avrdude and gcc.

In main.c, we only need to remove definition of F_CPU, change the LED pin from PB0 to PB3, and add the calibration routine. The calibration routine in EasyLogger has a slightly mind-bending binary search which, while short, does not take into account that ATtiny45 and ATtiny85 have OSCCAL which works in two overlapping frequency regions, 0..127 and 128..255. So I made my own which is as short but does search the two spaces separately for the optimum value (on the other hand, my routine does not descend to 0 or 128, which should not be an issue).

#define abs(x) ((x) > 0 ? (x) : (-x))

// Called by V-USB after device reset
void hadUsbReset() {
    int frameLength, targetLength = (unsigned)(1499 * (double)F_CPU / 10.5e6 + 0.5);
    int bestDeviation = 9999;
    uchar trialCal, bestCal, step, region;

    // do a binary search in regions 0-127 and 128-255 to get optimum OSCCAL
    for(region = 0; region <= 1; region++) {
        frameLength = 0;
        trialCal = (region == 0) ? 0 : 128;
        for(step = 64; step > 0; step >>= 1) { 
            if(frameLength < targetLength) // true for initial iteration
                trialCal += step; // frequency too low
                trialCal -= step; // frequency too high
            OSCCAL = trialCal;
            frameLength = usbMeasureFrameLength();
            if(abs(frameLength-targetLength) < bestDeviation) {
                bestCal = trialCal; // new optimum found
                bestDeviation = abs(frameLength -targetLength);

    OSCCAL = bestCal;

That’s it. Again, the full main.c and usbconfig.h along with modified makefile, updated schematic and rest of the needed stuff (driver, libusb, usbdrv folders) are available as one zip file released under GPL. Interface to the new device is unchanged, “usbtest on”, “usbtest out” etc. should work from command-line like they did with the ATtiny2313-powered device.

Fuse bits

Oh and one last thing – the fuse bits need of course be changed. Remove the 8x clock divider and switch to High Frequency PLL clock (CKSEL=0001) with slowly rising power (SUT=10). For ATtiny45/85 this should result in low fuse byte of 0xE1. You might also consider 2.7 V brown-out detection, making the high byte 0xDD:

avrdude -c usbtiny -p attiny85 -U lfuse:w:0xe1:m -U hfuse:w:0xdd:m

Published by

Joonas Pihlajamaa

Coding since 1990 in Basic, C/C++, Perl, Java, PHP, Ruby and Python, to name a few. Also interested in math, movies, anime, and the occasional slashdot now and then. Oh, and I also have a real life, but lets not talk about it!

125 thoughts on “V-USB with ATtiny45 / ATtiny85 without a crystal”

    1. I don’t know exactly myself, but because the clock is calibrated by measuring USB frame length, I’d wager that it’s something like “USB frame is 1499 clock cycles with rate of 10.5 MHz”, so with F_CPU double that, it would be 1499 * 2 CPU cycles (F_CPU / 10.5e6 = 2), and similarly for other values of F_CPU.

      The 0.5 is for rounding towards nearest integer, not flooring like it usually does ((int)4.9 = 4, but (int)(4.9+0.5)=5).

      1. thanks for reply,i found the word below
        /* This function MUST be called IMMEDIATELY AFTER USB reset and measures 1/7 of
        * the number of CPU cycles during one USB frame minus one low speed bit
        * length. In other words: return value = 1499 * (F_CPU / 10.5 MHz)
        and the usb1.1 speed is 1.5mbps,is that possiblely 10.5=7*1.5?

  1. Just in case anyone gets this in the future. When I tried the make file I got:

    usbdrv/usbdrv.h:455:6: error: variable 'usbDescriptorDevice' must be const in order to be put into read-only section by means of '__attribute__((progmem))'
    usbdrv/usbdrv.h:461:6: error: variable 'usbDescriptorConfiguration' must be const in order to be put into read-only section by means of '__attribute__((progmem))'
    usbdrv/usbdrv.h:467:6: error: variable 'usbDescriptorHidReport' must be const in order to be put into read-only section by means of '__attribute__((progmem))'
    usbdrv/usbdrv.h:473:6: error: variable 'usbDescriptorString0' must be const in order to be put into read-only section by means of '__attribute__((progmem))'
    usbdrv/usbdrv.h:479:5: error: variable 'usbDescriptorStringVendor' must be const in order to be put into read-only section by means of '__attribute__((progmem))'
    usbdrv/usbdrv.h:485:5: error: variable 'usbDescriptorStringDevice' must be const in order to be put into read-only section by means of '__attribute__((progmem))'
    usbdrv/usbdrv.h:491:5: error: variable 'usbDescriptorStringSerialNumber' must be const in order to be put into read-only section by means of '__attribute__((progmem))'
    usbdrv/usbdrv.c:70:14: error: variable 'usbDescriptorString0' must be const in order to be put into read-only section by means of '__attribute__((progmem))'
    usbdrv/usbdrv.c:80:14: error: variable 'usbDescriptorStringVendor' must be const in order to be put into read-only section by means of '__attribute__((progmem))'
    usbdrv/usbdrv.c:89:14: error: variable 'usbDescriptorStringDevice' must be const in order to be put into read-only section by means of '__attribute__((progmem))'
    usbdrv/usbdrv.c:111:14: error: variable 'usbDescriptorDevice' must be const in order to be put into read-only section by means of '__attribute__((progmem))'
    usbdrv/usbdrv.c:142:14: error: variable 'usbDescriptorConfiguration' must be const in order to be put into read-only section by means of '__attribute__((progmem))'

    To reason for it is the new avr-gcc needs progmem to be const. You just need to put const in front of every line that begins with PROGMEM . Sed fixes this nicely with sed -i 's/^PROGMEM/const PROGMEM/g' usbdrv/*.

  2. I failed and failed to get this working.
    I first tried a year ago. I finally came back to this.
    Still failed. Then, while reading your 2313 logic analyser post, I came across nerdralph’s reply-which had NOTHING to do with v-usb. I went to nerdralph’s blog and the answer was staring me in the face.

    http://nerdralph.blogspot.com USB interfacing for AVR microcontrollers
    too LOW value of D- pullup resistor.
    I was using 1k5. I changed to 2k5 and windows likes it.
    thanks joonas and thanks ralph

    1. Don’t know, they are from local electronics shop, not from the big mail vendors. I don’t think there are big differences, as long as they are small enough (some big ones require more current to start the cascade) — you can see the pictures. :)

      1. I was able to aquire some 3v9 Zener diodes locally, and was wondering if this configuration was still possible with that voltage on the d lines?

        1. I would give 50-50 odds for it working. 3v6 works closely enough to 3.3V (I recall some voltage drop happens with the resistor, so effective voltage is quite close to 3.3). 3v9 would result in a higher voltage, which at best case (say 50 % would work), or not (48 %) or even overvolt something on the other end (2 %).

  3. Quick question about your absolute value macro:
    #define abs(x) ((x) > 0 ? (x) : (-x))

    When you call it with
    won’t it substitute to
    ((frameLength-targetLength) > 0 ? (frameLength-targetLength) : (-frameLength-targetLength))

    Ideally, your macro would be defined
    #define abs(x) ((x) > 0 ? (x) : -(x))
    with the minus sign outside the parentheses, right?

  4. Great article. I just wanted to remark that the ATTiny85 datasheet says that you shouldn’t change the operating frequency by more than 2% at a time when changing OSCCAL. Your binary search method changes it by ~25% in the first step if I understand it correctly. If anyone runs into problems with this, a simple linear search may be the better option.

    1. I think Fritz is right, the steps should be limited (the limit is 2% for the frequency change and 32 for the OSCCAL value change for a single calibration step).

      Also I think it’d be safer to limit OSCCAL to 192 to avoid an out-of-spec system frequency (>20MHz). Although this shouldn’t happen normally if the USB frame measurement works correctly, that may not be guaranteed (especially during developement), so an explicit limit would better imo.

      Thanks again for this cool stuff!

  5. Hi, thanks, this is great stuff. I’ve switched the wiring of D- and D+ so that D- is connected to INT0 according to the in-source documentation, in order to be able to count the keep-alive messages (to detect when the host suspends the device). I had one problem with this: based on the documentation and also according to my understanding with this config the interrupt should be configured to trigger on falling edges as opposed to raising edges as for D+. However doing this resulted in instability, the host could rarely see a valid response to the get_descriptor and set_address requests. Configuring raising edge detection works fine, I haven’t seen any failures with it so far. But it’s less ideal (less time for the interrupt handler to do the phase lock), so I’d like to understand what’s the problem with the other config. Any ideas?

    1. After some debugging, I think I figured it out. During the OSCCAL calibration interrupts are enabled, so the SE0 used by the calibration loop to detect the start of calibration frame (SOF/EOP) will also generate an interrupt. The interrupt handler will in turn hide the SE0 state from the calibration loop causing the calibration to fail. Enabling interrupts after the calibration is complete solved the problem (moved sei() to the end of hadUsbReset()). Note that this isn’t a problem if the interrupt is triggered by D+, since that line won’t change during the calibration, hence it won’t trigger any interrupts.

  6. Hi all,great work done hear,I just have a question,I’ve seen some circuits using 27 ohm on the data lines,ime using 27 ohm on some of mine due to running out of 68 ohm,its works fine but why do some circuits use 27 ohm? Also I’ve 3.3 and 3.6 zenners both work a treat,I ordered wrong once and used the 3.3 zenners and found them to work great,are there any down sides to this value? Also with regards to USB lines I read that they have to use the same port only,is this true? I know about the INT0 side of things,reason I ask is I saw a design some one made,it was attiny 84 and they put the USB lines on separate ports,PB2 and PC0?? This won’t work with micrinucleus would it? Thanks folks for any questions answered.

    1. Thanks! The 27 ohm version might draw a bit more current (roughly twice as much), but generally signalling is faster with more current. In low-speed USB I’d use the higher value (I would wager 68 ohm might be what the specification requires). Quick googling (I recommend it :) with “usb 68 ohm” gave this:


      Zener voltage will affect the voltage of your signals. Depending on the device on the other end, lower voltage (or higher) might work just as well — generally lower voltage increases risk of lost signal, and higher frying the other device (some transistors designed for 3.3V VCC do not handle 5V for example without breaking). Actual voltage is slightly lower if I recall correctly. In case of 3.3V vs. 3.6V, the difference is small so probably 98 % of cases it works just fine.

      I think using the same port might be related to the fact, that I’ve noticed the V-USB devices are a bit specific with the drivers — if you install a driver for your V-USB device, it might not automatically work on another port. With USB HID devices such as keyboards and mice, this will not be an issue.

    1. I have a hazy recollection of this V-USB library after 7 years of posting this, but I think the hadUsbReset is meant to be a function provided by the user in their code, and therefore the usbdrv.c does not define it (that file is part of the V-USB library). The function is defined in main.c. Depending how you do the compilation (makefile, by hand at command line, AVR Studio) you may get error messages of missing stuff but you _should_ (no guarantees here after 7 years) be OK if you either use the makefile provided or adding all files to AVR Studio project.

      The world has moved on a bit so I recommend also checking out newer devboard with ready-made USB support. Many are Arduino compatible and making this stuff is a lot easier than my hardcore VUSB + makefiles tutorial. :)

  7. You do not reply instead of me asking questions…u r selfish.
    i am getting unknown reference error for hadUsbReset() instead of being defined in main.c. I have also added its prototype in usbconfig.h. why?

    1. Rasheed this is no helpline, it’s an old post in my personal blog, and although I did reply to quite many people’s questions back when the post was published, you’re probably better off asking this stuff on a forum with more active readership.

      Plus, calling the author of a tutorial selfish after he has not responded to a question to a 7 year old article in 30 minutes is not preferred behavior even in the internet!

  8. Please if anyone can help with Unknown reference HardUsbReset(). I will be grateful…i did put b/m to some method and it got resolved but i cant remember
    #ifdef __cplusplus
    extern “C” {USB_PUBLIC void FUNCTIONAME(void);

    why is nobody replying??????

    1. The reference error often happens if you try to compile a .c file into something “executable”, and are missing other .c files. You can either add all .c files into one command, or preferably compile all sources (in this case there is assembly file .S as well I recall) into individual .o files and link them in another command. The makefile provided should do that for you, just type “make” in the directory (you should have mingw and stuff installed, no tutorial on that one here).

Leave a Reply

Your email address will not be published. Required fields are marked *

Time limit is exhausted. Please reload the CAPTCHA.

This site uses Akismet to reduce spam. Learn how your comment data is processed.