Mit V-USB von Objective Development ist es ohne weitere Hardware möglich, mit einem AVR Mikrocontroller ein USB-Gerät aufzubauen.
Inhaltsverzeichnis
Hardware Voraussetzungen des AVRs
Spannungspegel-Problem
Da der AVR mit anderen Spannungen als die des USB-Standards arbeitet, müssen diese Angepasst werden. Der Computer und der AVR-Mikrocontroller haben beide bestimmte Anforderungen an die Spannung für einen High- bzw. Low-Pegel. Da wir die Computerseite nicht verändern können, müssen die Anpassungen am AVR stattfinden. Vom PC wird ein Low-Pegel als 0V, ein High-Pegel als 3,3V übertragen. Jedoch ist dies das kleinere Problem, da die AVR-Mikrocontroller selbst bei 5V diesen Pegel sicher als High erkennen. Die andere Richtung ist ein größeres Problem. Der Computer erwartet als Low-Pegel eine Spannung von 0V - 0,8V und 2V - 3,6V als High. Um diese Voraussetzungen zu erfüllen, muss entweder der AVR mit einer Spannung von 2V - 3,6V betreiben werden oder die Pegel müssen an D+ und D- des AVRs angepasst werden.
Lösung A: Verringern der Betriebspannung des AVRs
Für diese Lösung wird die Betriebsspannung des AVRs auf 3,3V - 3,6V herunter gesetzt. Dies ist mit einem 3,3V-Spannungsregler (z.B. LE33CZ, LT1761ES5-3.3) möglich.
Vorteile
- Saubere Lösung. Schnelle Übergänge auf D+ und D-
- Gute Störfestigkeit am Signaleingang
- Stromverbrauch des µC ist bei 3,3 V geringer als bei 5 V
Nachteile
- 3,3V-Spannungsregler sind oft teuer und schwer zu beschaffen
- Viele ältere AVRs funktionieren bei 3,3 V nicht sicher beim geforderten Takt (12 MHz). Auch bei neueren sind bei 3.3 V kaum mehr als 13 MHz garantiert.
- Ruhestrom für den Spannungsregler (Forderung nach sparsamem Regler)
Ebenso ist es möglich, die Spannung mit Gleichrichterdioden zu reduzieren. Hierfür werden einfach zwei oder drei Gleichrichterdioden in Reihe vor VCC des AVRs geschaltet.
Zusätzliche Vorteile
- Niedrige Kosten / Bauteile sind leicht zu bekommen.
- Kein Ruhestrom --> USB-Konformer Standby-Modus
Zusätzliche Nachteile
- Keine Spannungsregelung --> Spannung schwankt, das Geschwindigkeitsprobelm wird dadurch noch größer. Eventuell zu viel Spannung im Leerlauf.
- Die unregulierte Spannung ist problematisch für Analoge Schaltungen (ADC,...)
Lösung B: Anpassen der Spannung an D+ und D-
Ebenso kann man die Spannung an den Datenleitungen (D+ und D-) mit Zenerdioden verringern. Es werden 3,6V Low-Power Zenerdioden (wie 1N4148, 500mW oder weniger) empfohlen, da diese eine geringe Kapazität haben und somit weniger Störungen auf den Datenleitungen verursachen.
Wenn diese Lösung verwendet wird, stellen sie vor Benutzung sicher, dass die Spannungspegel mit den geforderten Pegeln(LOW:0V-0,8V HIGH:2V-3,6V) übereinstimmen.
Vorteile
- Niedrige Kosten
- Leicht zu bekommen
- Ganze Schaltung kann mit 5V betrieben werden --> Hohe Taktraten möglich
- zusätzlicher Schutz vor Überspannungen / ESD
Nachteile
- Keine saubere Lösung: Es muss ein Kompromiss zwischen allen Möglichkeiten gefunden werden.
- Zener-Dioden kommen mit einem breiten Spektrum an Merkmalen. Deshalb könnten die Ergebnisse nicht reproduzierbar sein.
- Hohe Ströme beim Senden von High-Leveln
- Nicht viel Reserve beim Empfangen
Allgemeine Anmerkungen zur Hardware
Es wird bei jeder der oben genannten Schaltungen ein PullUp-Widerstand (1,5k - 10k) von D- nach Vcc benötigt. Ebenso muss zwischen AVR und Die Datenleitungen ein Widerstand von je 68 Ohm.
Welche Taktraten können verwendet werden?
Momentan unterstützt V-USB Taktraten von 12 MHz, 12.8 MHz, 15 MHz, 16 MHz, 16.5 MHz, 18 MHz und 20 MHz. Diese Taktraten sind präzise, dies bedeutet, dass ein Quarz mit 11.9 MHz nicht funktionieren würde. Nur bei den 16.5 und 12.8 MHz Varianten ist eine Toleranz von 1%.16.5 MHz kann z.B. mit dem internen RC Oszillator von AVRs wie dem ATTiny25/45/85 oder dem ATTiny26 erreicht werden. Entscheidungshilfe
- Möchten sie den internen RC Oszillator benutzen? Dann nehmen sie die 16.5MHz-Variante.
- Ist sehr wenig Speicher vorhanden? Verwenden sie am besten die 16MHz oder 20Mhz Variante.
- Wird der AVR mit einer niedrigen Spannung betrieben? Dann verwenden sie die 12MHz-Variante.
- Wollen sie CRC-Prüfsummen verwenden? Benutzen sie die 18Mhz-Variante.
Anschluss an den AVR
Die Datenleitung D+ muss über den 68 Ohm-Widerstand mit dem Port INT0 des AVRs verbunden werden. D- kann an einen beliebigen Port. Diese Ports müssen dann in der "usbconfig.h" angepasst werden.
Software des AVR: Die Firmware
Als erstes muss man sich die neueste Version von V-USB von folgender Seite herunterladen: [1] Nachdem sie das Archiv heruntergeladen haben, entpacken sie es und kopieren sie den Ordner "usbdrv" in ihr Projektverzeichnis. Kopieren bzw. erstellen sie das Makefile und passen darin die Taktrate und den verwendeten Controller an. Ebenso wird eine "usbconfig.h" benötigt, diese kann z.B. aus dem tests-Ordner kopiert werden. In dieser müssen nun die Punkte USB_CFG_IOPORTNAME, USB_CFG_DMINUS_BIT und USB_CFG_DPLUS_BIT angepasst werden.
In der main.c müssen immer als erstes folgende Dateien importiert werden:
#include <avr/io.h> //IO-Zugriff: Braucht man immer #include <avr/interrupt.h> // V-USB braucht interrupts #include <avr/pgmspace.h> #include <avr/wdt.h> //Watchdog: Sollte immer aktiv sein. #include "usbdrv.h" //Der USB-Treiber #include <util/delay.h> //Wird später für Timing etc. benötigt
Gerät mit selbst geschriebenem PC-Treiber (kein HID)
Der Code für ein eigenes Gerät wird hier am Beispiel eines Gerätes veranschaulicht, bei dem sich über USB der PWM-Kanal steuern lässt. Damit kann man zum Beispiel eine LED dimmen.
Nach den Includes komm nun die Funktion zum Verarbeiten eingehender Daten. Das erste Empfangene Byte, also der Befehl wird mit rq->bRequest abgefragt. In diesem Fall ist dieser entweder 0 oder 1. 0 bedeutet den PWM-Wert zu setzen, 1 bedeutet den Status (PWM-Wert) zurückzusenden. Die einzelnen Parameter des Aufrufs können mit rq->wValue.bytes[id] abgefragt werden. Bei Befehl 0 wird in rq->wValue.bytes[0] der neue PWM-Wert übertragen. bytes[1] ist das 2. byte von wValue, welches beim Senden von Zahlen > 8bit entsteht. Ebenso gibt es noch rq->wIndex[id], welches gleich funktioniert. Durch setzen von replyBuf kann festgelegt werden, welche Daten zurückgesendet werden sollen. Dabei muss immer replyBuf[id] verwendet werden. Bei Befehl 1 wird mit replyBuf[0] = OCR1A der aktuelle PWM-Wert zurückgesendet. Mit return 2 wird dann noch mitgeteilt, dass überhaupt was zurückgesendet werden soll (2 Bytes). Mit return 0 wird nichts zurückgesendet. Das ist in diesem Fall der Fall, wenn ein ungültiger Befehl übermittelt wurde.
USB_PUBLIC uchar usbFunctionSetup(uchar data[8]) { usbRequest_t *rq = (void *)data; static uchar replyBuf[2]; usbMsgPtr = replyBuf; if(rq->bRequest == 0){ replyBuf[0] = rq->wValue.bytes[0]; replyBuf[1] = 0x0; OCR1A=rq->wValue.bytes[0]; eeprom_write_byte(0,OCR1A); return 2; } else if (rq->bRequest == 1) { replyBuf[0] = OCR1A; replyBuf[1] = 0x0; return 2; } return 0; }
Als nächstes komm void main:
Hier schalten wir als erstes den Watchdog-Timer ein. Danach setzen wir alle Ports und DDR so wie wir sie brauchen. Es ist nur darauf zu achten, dass INT0 ein Eingang ist. Bei einem Atmega8 ist dies PORTD.2. Für unser Beispiel setzen wir TCCR1A als 8_bit PWM-Timer. Danach laden wir den letzten PWM-Wert aus dem EEPROM (Wird immer beim Ändern gespeichert). Nun trennen wir die Verbindung für > 500ms, dies wird benötigt, um nach einen Neustart des Mikrocontrollers (z.B. durch Watchdog) dem PC mitzuteilen, dass er neugestartet ist. Danach Verbinden wir uns wieder mit usbDeviceConnect(). Nun muss die Verbindung mit usbInit() initialisiert werden ud die Interrupts mit sei() eingeschaltet werden. Nun muss nur noch im Mainloop der Watchdog zurückgesetzt werden und usbPoll() aufgerufen werden.
int main(void) { uchar i; wdt_enable(WDTO_1S); DDRD = ~(1 << 2); /* all outputs except PD2 = INT0 */ DDRB = 0xff; /* output pins setzen */ PORTD = 0; /* USB-Verbindung */ PORTB = 0; /* output pins aus*/ TCCR1A = (1<<WGM10)|(1<<COM1A1); // PWM, phase correct, 8 bit. TCCR1B = (1<<CS11) |(1<<CS10); // set clock/prescaler 1/64 -> enable counter OCR1A=eeprom_read_byte(0); /* We fake an USB disconnect by pulling D+ and D- to 0 during reset. This is * necessary if we had a watchdog reset or brownout reset to notify the host * that it should re-enumerate the device. Otherwise the host's and device's * concept of the device-ID would be out of sync. */ usbDeviceDisconnect(); /* enforce re-enumeration, do this while interrupts are disabled! */ i = 0; while(--i){ /* fake USB disconnect for > 500 ms */ wdt_reset(); _delay_ms(2); } usbDeviceConnect(); usbInit(); sei(); for(;;){ /* main event loop */ wdt_reset(); usbPoll(); } return 0; }
Damit ist die Firmware fertig. Hier noch ein Makefile für avr-gcc: (Chip: Mega8, Clock: 12MHz, Programmer: Ponyser)
# Name: Makefile # Project: edited PowerSwitch # Author: # Creation Date: # Tabsize: 4 # Copyright: (c) 2005 by OBJECTIVE DEVELOPMENT Software GmbH # License: GNU GPL v2 (see License.txt) or proprietary (CommercialLicense.txt) # This Revision: $Id: Makefile 277 2007-03-20 10:53:33Z cs $ DEVICE = atmega8 AVRDUDE = avrdude -c ponyser -P /dev/ttyS0 -p $(DEVICE) # Choose your favorite programmer and interface above. COMPILE = avr-gcc -Wall -Os -Iusbdrv -I. -mmcu=$(DEVICE) #-DDEBUG_LEVEL=2 # NEVER compile the final product with debugging! Any debug output will # distort timing so that the specs can't be met. OBJECTS = usbdrv/usbdrv.o usbdrv/usbdrvasm.o usbdrv/oddebug.o main.o # symbolic targets: all: main.hex .c.o: $(COMPILE) -c $< -o $@ .S.o: $(COMPILE) -x assembler-with-cpp -c $< -o $@ # "-x assembler-with-cpp" should not be necessary since this is the default # file type for the .S (with capital S) extension. However, upper case # characters are not always preserved on Windows. To ensure WinAVR # compatibility define the file type manually. .c.s: $(COMPILE) -S $< -o $@ flash: all $(AVRDUDE) -U flash:w:main.hex:i # Fuse low byte: # 0xef = 1 1 1 0 1 1 1 1 # ^ ^ \+/ \--+--/ # | | | +------- CKSEL 3..0 (clock selection -> crystal @ 12 MHz) # | | +--------------- SUT 1..0 (BOD enabled, fast rising power) # | +------------------ CKOUT (clock output on CKOUT pin -> disabled) # +-------------------- CKDIV8 (divide clock by 8 -> don't divide) # # Fuse high byte: # 0xdb = 1 1 0 1 1 0 1 1 # ^ ^ ^ ^ \-+-/ ^ # | | | | | +---- RSTDISBL (disable external reset -> enabled) # | | | | +-------- BODLEVEL 2..0 (brownout trigger level -> 2.7V) # | | | +-------------- WDTON (watchdog timer always on -> disable) # | | +---------------- SPIEN (enable serial programming -> enabled) # | +------------------ EESAVE (preserve EEPROM on Chip Erase -> not preserved) # +-------------------- DWEN (debug wire enable) #fuse_tiny2313: # only needed for attiny2313 # $(AVRDUDE) -U hfuse:w:0xdb:m -U lfuse:w:0xef:m clean: rm -f main.hex main.lst main.obj main.cof main.list main.map main.eep.hex main.bin *.o usbdrv/*.o main.s usbdrv/oddebug.s usbdrv/usbdrv.s # file targets: main.bin: $(OBJECTS) $(COMPILE) -o main.bin $(OBJECTS) main.hex: main.bin rm -f main.hex main.eep.hex avr-objcopy -j .text -j .data -O ihex main.bin main.hex ./checksize main.bin # do the checksize script as our last action to allow successful compilation # on Windows with WinAVR where the Unix commands will fail. disasm: main.bin avr-objdump -d main.bin cpp: $(COMPILE) -E main.c
D+ ist in diesem Beispiel mit PD2 verbunden, D- mit PD0. Die LED hängt an PB1. (Schaltplan noch in Arbeit.) Hier gehts zur PC-Seitigen Programmierung dieses Projekts
HID-Gerät (Tastatur/Maus/...)
[noch in Arbeit]
PC-Software bei nicht HID-Projekten (Treiber)
Wenn man nun ein Gerät mit einer Firmware hat, muss man als nächstes den Treiber für den PC schreiben. Dies werde ich nun am obigen Beispiel erläutern.
Als erstes sollte man sich überlegen, was die Software können muss. In unserem Beispiel wären das:
- Den PWM-Wert setzen
- Befehl ein/aus
- aktuellen Wert auslesen
Die Befehle ein bzw. aus müssen dabei keine eigenen Befehle werden, da man dafür den Wert 255(an) bzw. 0(aus) senden kann.
Um nun überhaupt einen USB-Treiber schreiben zu können, muss man sich als erstes "libusb" für c bzw. "libusb++" für c++ herunterladen. Unter Linux kann dies über den Paketmanager (Synaptic/Aptitude) geschehen. Unter Windows muss man sich die Bibliothek von folgender Seite herunterladen: http://www.libusb.org//http://libusb.sourceforge.net/. Stellen sie sicher, dass wenn sie unter Windows arbeiten, die Win32-Version herunterladen.
Als erstes muss man auch wieder einige Header importieren:
#include <stdio.h> /* Brauchen wir für Ein-/ bzw. Ausgabe */ #include <string.h> /* Brauchen wir für die Auswertung von Parametern */ #include <usb.h> /* Die Bibliothek - Liegt diese im Projektverzeichnis so muss #include "usb.h" verwendet werden*/
Als nächstes müssen die VendorID und PID definiert werden. Diese können aus der Datei "USBID-License.txt" entnommen werden. WICHTIG: Zuerst ganz durchlesen um die Richtige auszuwählen. Diese definiert man dann mit:
#define USBDEV_SHARED_VENDOR 0x16C0 /* VOTI */ #define USBDEV_SHARED_PRODUCT 0x05DC /* Obdev's free shared PID */
In diesem Beispiel wurde die VendorID/PID so gewählt, dass es keine Klasse hat, also ein eigenes Gerät ist.
Danach sollte man der Übersichtlichkeit halber, die einzelnen Befehle (welche einzelne Bytes sind) definieren. Für unser Beispiel heißt das:
#define CMD_SETPWM 0 #define CMD_GETSTATUS 1
Da dieses Projekt eine Konsolen-basierte Anwendung ist, ist es immer wichtig eine Hilfe mit den befehlen einzubauen. Nennen wir diese Funktion einfach usage: (Es kann selbstverständlich auch auf stdout ausgegeben werden.)
static void usage(char *name) { fprintf(stderr, "usage:\n"); fprintf(stderr, " %s on\n", name); fprintf(stderr, " %s off\n", name); fprintf(stderr, " %s pwm [0-255]\n", name); fprintf(stderr, " %s status\n", name); }
Als nächstes brauchen wir Funktionen zum Öffnen und Kommunizieren mit dem Gerät. Diese wurden aus dem Beispiel "PowerSwitch" von obdev kopiert:
static int usbGetStringAscii(usb_dev_handle *dev, int index, int langid, char *buf, int buflen) { char buffer[256]; int rval, i; if((rval = usb_control_msg(dev, USB_ENDPOINT_IN, USB_REQ_GET_DESCRIPTOR, (USB_DT_STRING << 8) + index, langid, buffer, sizeof(buffer), 1000)) < 0) return rval; if(buffer[1] != USB_DT_STRING) return 0; if((unsigned char)buffer[0] < rval) rval = (unsigned char)buffer[0]; rval /= 2; /* lossy conversion to ISO Latin1 */ for(i=1;i<rval;i++){ if(i > buflen) /* destination buffer overflow */ break; buf[i-1] = buffer[2 * i]; if(buffer[2 * i + 1] != 0) /* outside of ISO Latin1 range */ buf[i-1] = '?'; } buf[i-1] = 0; return i-1; } #define USB_ERROR_NOTFOUND 1 #define USB_ERROR_ACCESS 2 #define USB_ERROR_IO 3 static int usbOpenDevice(usb_dev_handle **device, int vendor, char *vendorName, int product, char *productName) { struct usb_bus *bus; struct usb_device *dev; usb_dev_handle *handle = NULL; int errorCode = USB_ERROR_NOTFOUND; static int didUsbInit = 0; if(!didUsbInit){ didUsbInit = 1; usb_init(); } usb_find_busses(); usb_find_devices(); for(bus=usb_get_busses(); bus; bus=bus->next){ for(dev=bus->devices; dev; dev=dev->next){ if(dev->descriptor.idVendor == vendor && dev->descriptor.idProduct == product){ char string[256]; int len; handle = usb_open(dev); /* we need to open the device in order to query strings */ if(!handle){ errorCode = USB_ERROR_ACCESS; fprintf(stderr, "Warning: cannot open USB device: %s\n", usb_strerror()); continue; } if(vendorName == NULL && productName == NULL){ /* name does not matter */ break; } /* now check whether the names match: */ len = usbGetStringAscii(handle, dev->descriptor.iManufacturer, 0x0409, string, sizeof(string)); if(len < 0){ errorCode = USB_ERROR_IO; fprintf(stderr, "Warning: cannot query manufacturer for device: %s\n", usb_strerror()); }else{ errorCode = USB_ERROR_NOTFOUND; /* fprintf(stderr, "seen device from vendor ->%s<-\n", string); */ if(strcmp(string, vendorName) == 0){ len = usbGetStringAscii(handle, dev->descriptor.iProduct, 0x0409, string, sizeof(string)); if(len < 0){ errorCode = USB_ERROR_IO; fprintf(stderr, "Warning: cannot query product for device: %s\n", usb_strerror()); }else{ errorCode = USB_ERROR_NOTFOUND; /* fprintf(stderr, "seen product ->%s<-\n", string); */ if(strcmp(string, productName) == 0) break; } } } usb_close(handle); handle = NULL; } } if(handle) break; } if(handle != NULL){ errorCode = 0; *device = handle; } return errorCode; }
Als letztes kommt nun void main. In main muss als erstes ein Handle für das USB-Gerät definiert werden. Danach brauchen wir noch einen Buffer für Dinge, die vom Gerät kommen und dann noch einen Zähler für empfangene Bytes. Danach prüfen wir, ob der Programmaufruf korrekt ist. (Parameterzahl):
int main(int argc, char **argv) { usb_dev_handle *handle = NULL; unsigned char buffer[8]; int nBytes; if(argc < 2){ usage(argv[0]); exit(1); }
Danach muss libusb initialisiert werden dann das Gerät mit der obigen Funktion geöffnet werden. Dabei musst du natürlich Email/Homepage durch deine Homepage bzw. E-Mail ersetzen. Ebenso muss der Name, in diesem Fall PWMControl angepasst werden. Die beiden Werte wurden vorher in der Datei "usbconfig.h" festgelegt. Lesen sie dazu jedoch die Datei "USBID-License.txt". Diese Werte können natürlich auch als Defines gesetzt werden.
usb_init(); if(usbOpenDevice(&handle, USBDEV_SHARED_VENDOR, "EMail/Homepage", USBDEV_SHARED_PRODUCT, "PWMControl") != 0){ fprintf(stderr, "Could not find USB device \"PWMControl\" with vid=0x%x pid=0x%x\n", USBDEV_SHARED_VENDOR, USBDEV_SHARED_PRODUCT); exit(1); }
Als nächstes kommt unser eigentlicher Programmcode. In diesem wird zuerst überprüft ob on/off/pwm oder status übergeben wurde. Bei PWM wird zusätzlich geprüft, ob noch der PWM-Wert mit angegeben wurde. Mit dem befehl nBytes = usb_control_msg(handle, USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN, [Befehl], [Param1], [Param2], (char *)buffer, sizeof(buffer), 5000); kann man nun Daten an das Gerät schicken. Sendet das Gerät Daten zurück, so sind diese in buffer abrufbar. Ebenfalls wird noch überprüft, ob ein fehler auftrat. Dies erfolgt mit if(nBytes < 0).
if(strcmp(argv[1], "pwm") == 0){ if(argc < 3){ usage(argv[0]); exit(1); } nBytes = usb_control_msg(handle, USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN, CMD_SETPWM, atoi(argv[2]), 0, (char *)buffer, sizeof(buffer), 5000); }else if(strcmp(argv[1], "off") == 0){ nBytes = usb_control_msg(handle, USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN, CMD_SETPWM, 0, 255, (char *)buffer, sizeof(buffer), 5000); }else if(strcmp(argv[1], "on") == 0){ nBytes = usb_control_msg(handle, USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN, CMD_SETPWM, 255, 255, (char *)buffer, sizeof(buffer), 5000); }else if(strcmp(argv[1], "status") == 0){ nBytes = usb_control_msg(handle, USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN, CMD_GETSTATUS, 0, 0, (char *)buffer, sizeof(buffer), 5000); if(nBytes < 2){ if(nBytes < 0) fprintf(stderr, "USB error: %s\n", usb_strerror()); fprintf(stderr, "only %d bytes status received\n", nBytes); exit(1); } printf("%i\n", buffer[0]); }else{ nBytes = 0; usage(argv[0]); exit(1); } if(nBytes < 0) fprintf(stderr, "USB error: %s\n", usb_strerror());
Letztendlich muss man das USB-gerät nur noch schließen und den void main beenden.
usb_close(handle); //USB-Gerät schließen return 0; }
Quellen
- V-USB Homepage by Objective Development
- V-USB Wiki
- Alle drei Schaltpläne wurden aus der Wiki von V-USB entnommen.
Dieser Artikel ist noch lange nicht vollständig. Der Auto/Initiator hofft das sich weitere User am Ausbau des Artikels beteiligen.
Das Ergänzen ist also ausdrücklich gewünscht! Besonders folgende Dinge würden noch fehlen: Das Thema HID-Geräte fehlt noch komplett. |