Thursday, February 10, 2011

USB Doodad 6: Bootloader

Now that we know USB is working, it's time to get the bootloader working.  The bootloader is called "bootloadHID", and can be found here:

  http://www.obdev.at/products/vusb/bootloadhid.html

I downloaded bootloadHID and extracted it:

[mjd@onza src]$ wget -q http://www.obdev.at/downloads/vusb/bootloadHID.2010-07-29.tar.gz
[mjd@onza src]$ tar -xzf bootloadHID.2010-07-29.tar.gz
[mjd@onza src]$

I have made some changes to the bootloader source and config:
  • We're using different pins for the USB.
  • I want the LEDs to blink to show something's happening.  The blinking is different depending on whether the bootloader is in program mode, or has handed control to the application.
  • Our bootloader program mode switch is connected to an analog input rather than a digital input.  Therefore I had to add code to read an analog value. 
  • Once the bootloader is in program mode, I want it to stay in program mode after the button is released.  (In the stock code, you have to hold down the button constantly in order to program the MCU).
  •  I've changed the MCU type, the fuses, the execution start address and the right command to run avrdude for my USB programmer:

--- bootloadHID.2010-07-29.orig/firmware/bootloaderconfig.h
+++ bootloadHID.2010-07-29/firmware/bootloaderconfig.h
@@ -43,7 +43,7 @@
 /* This is the port where the USB bus is connected. When you configure it to
  * "B", the registers PORTB, PINB and DDRB will be used.
  */
-#define USB_CFG_DMINUS_BIT      0
+#define USB_CFG_DMINUS_BIT      3
 /* This is the bit number in USB_CFG_IOPORT where the USB D- line is connected.
  * This may be any bit in the port.
  */
@@ -106,6 +106,7 @@
 #ifndef __ASSEMBLER__   /* assembler cannot parse function definitions */
 #include <util/delay.h>

+#if 0
 static inline void  bootLoaderInit(void)
 {
     PORTD = 1 << 3; /* activate pull-up for key */
@@ -113,6 +114,7 @@
 }

 #define bootLoaderCondition()   ((PIND & (1 << 3)) == 0)   /* True if jumper is set */
+#endif

 #endif

--- bootloadHID.2010-07-29.orig/firmware/main.c
+++ bootloadHID.2010-07-29/firmware/main.c
@@ -81,8 +81,15 @@

 static void (*nullVector)(void) __attribute__((__noreturn__));

+static inline void setLed(const int i)
+{
+    PORTC &= ~(_BV(PC0) | _BV(PC1) | _BV(PC2) | _BV(PC3));
+    PORTC |= i & (_BV(PC0) | _BV(PC1) | _BV(PC2) | _BV(PC3));
+}
+
 static void leaveBootloader()
 {
+    setLed(15);
     DBG1(0x01, 0, 0);
     cli();
     boot_rww_enable();
@@ -215,9 +222,68 @@
     sei();
 }

+#define ADC_CHANNEL                    6
+#define ADC_ENABLED                     (1 << ADEN)
+#define ADC_SINGLE_CONVERSION           (0 << ADATE)
+#define ADC_CONV_COMPLETE_INTR_OFF      (0 << ADIE)
+#define ADC_PRESCALE_128                ((1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0))
+#define ADC_REFERENCE_AVCC              (0 << REFS1 | 1 << REFS0)
+#define ADC_RIGHT_ADJUSTED              (0 << ADLAR)
+#define ADC_START_CONV                  (1 << ADSC)
+#define ADC_CONV_IN_PROGRESS            (1 << ADSC)
+#define ADC_CONV_INTFLAG                (1 << ADIF)
+
+static inline void ADC_Init()
+{
+    // Write 0 to power up ADC, 1 to power down
+    PRR &= ~PRADC;
+
+    ADCSRA = ADC_ENABLED | ADC_SINGLE_CONVERSION | ADC_CONV_COMPLETE_INTR_OFF | ADC_PRESCALE_128;
+
+    ADMUX = ADC_REFERENCE_AVCC | ADC_RIGHT_ADJUSTED | ADC_CHANNEL;
+
+    _delay_us(10);  /* wait for levels to stabilize */
+}
+
+static inline uint16_t ADC_Read()
+{
+    // TODO: Start conversion with ADSC=1
+    ADCSRA |= ADC_START_CONV;
+
+    // Wait for the conversion to complete
+    while (ADCSRA & ADC_CONV_IN_PROGRESS)
+        ;
+
+    ADCSRA |= ADC_CONV_INTFLAG;
+
+    return ADC;
+}
+
+static inline void ADC_StopReading()
+{
+    // ADEN in ADSRA enables ADC
+    ADCSRA &= ~ADEN;
+}
+
+static inline void  bootLoaderInit(void)
+{
+    ADC_Init();
+}
+
+static inline char bootLoaderCondition()
+{
+    uint16_t v = ADC_Read();
+
+    ADC_StopReading();
+
+    return v < 512;
+}
+
 int __attribute__((noreturn)) main(void)
 {
     /* initialize hardware */
+    DDRC |= _BV(PC0) | _BV(PC1) | _BV(PC2) | _BV(PC3) | _BV(PC4) | _BV(PC5);
+    PORTC = _BV(5);
     bootLoaderInit();
     odDebugInit();
     DBG1(0x00, 0, 0);
@@ -229,7 +295,14 @@
         GICR = (1 << IVSEL); /* move interrupts to boot flash section */
 #endif
         initForUsbConnectivity();
+        unsigned int a = 0;
+        unsigned int b = 0;
         do{ /* main event loop */
+            if (++a == 32)
+            {
+                a = 0;
+                setLed(++b >> 11);
+            }
             wdt_reset();
             usbPoll();
 #if BOOTLOADER_CAN_EXIT
@@ -243,7 +316,7 @@
                 }
             }
 #endif
-        }while(bootLoaderCondition());
+        } while(1);
     }
     leaveBootloader();
 }
--- bootloadHID.2010-07-29.orig/firmware/Makefile
+++ bootloadHID.2010-07-29/firmware/Makefile
@@ -14,11 +14,11 @@
 #     make flash   # to load the boot loader into flash
 #     make lock    # to protect the boot loader from overwriting

-DEVICE = atmega8
-BOOTLOADER_ADDRESS = 1800
-F_CPU = 12000000
-FUSEH = 0xc0
-FUSEL = 0x9f
+DEVICE = atmega328p
+BOOTLOADER_ADDRESS = 7800
+F_CPU = 16000000
+FUSEH = 0xda
+FUSEL = 0xff
 # Fuse high byte:
 # 0xc0 = 1 1 0 0   0 0 0 0 <-- BOOTRST (boot reset vector at 0x1800)
 #        ^ ^ ^ ^   ^ ^ ^------ BOOTSZ0
@@ -38,7 +38,7 @@

 ###############################################################################

-AVRDUDE = avrdude -c stk500v2 -P avrdoper -p $(DEVICE)
+AVRDUDE = avrdude -c avrisp2 -P usb -p $(DEVICE)

 LDFLAGS += -Wl,--relax,--gc-sections -Wl,--section-start=.text=$(BOOTLOADER_ADDRESS)

A note about the changes to the Makefile:

"BOOTLOADER_ADDRESS = 7800": This is the start address of the bootloader.  There's 2k (0x800) bytes of program space allocated for the bootloader and the '328 has 32k (0x8000) bytes of program flash.  So the start of the bootloader is at 0x8000 - 0x800 = 0x7800.

"FUSEH = 0xda": For the blinkenled and the mouse demo, the high fuse was set to the '328's default of 0xd9.  With this fuse setting, execution will start at address 0.  In order to jump to the bootloader, we want to enable (=0) the BOOTRST fuse, and set the size of the bootloader to be 2048 bytes = 1024 words, and the start address to be 0x3C00 words = 0x7800 bytes.  (This use of words and bytes is quite confusing.  In the AVR and gcc world, some values are given in bytes and some are given in words.  I've had several puzzling moments getting all this straightened out...)

Once again I looked up the fuse calculator:

  http://frank.circleofcurrent.com/fusecalc/

Making these changes gives a high fuse value of 0xda.

Ok, back to the patch:

[mjd@onza src]$ cd bootloadHID.2010-07-29
[mjd@onza bootloadHID.2010-07-29]$ patch -p1 < ../bootloadHID.2010-07-29.diff
patching file firmware/bootloaderconfig.h
patching file firmware/main.c
patching file firmware/Makefile
[mjd@onza bootloadHID.2010-07-29]$

Then I compiled the software:

[mjd@onza bootloadHID.2010-07-29]$ cd firmware
[mjd@onza firmware]$ make
avr-gcc -Wall -Os -fno-move-loop-invariants -fno-tree-scev-cprop -fno-inline-small-functions -Iusbdrv -I. -mmcu=atmega328p -DF_CPU=16000000 -DDEBUG_LEVEL=0  -x assembler-with-cpp -c usbdrv/usbdrvasm.S -o usbdrv/usbdrvasm.o
avr-gcc -Wall -Os -fno-move-loop-invariants -fno-tree-scev-cprop -fno-inline-small-functions -Iusbdrv -I. -mmcu=atmega328p -DF_CPU=16000000 -DDEBUG_LEVEL=0  -c usbdrv/oddebug.c -o usbdrv/oddebug.o
avr-gcc -Wall -Os -fno-move-loop-invariants -fno-tree-scev-cprop -fno-inline-small-functions -Iusbdrv -I. -mmcu=atmega328p -DF_CPU=16000000 -DDEBUG_LEVEL=0  -c main.c -o main.o
usbdrv/usbdrv.h:213: warning: 'usbFunctionDescriptor' used but never defined
usbdrv/usbdrv.h:220: warning: 'usbSetInterrupt' declared 'static' but never defined
avr-gcc -Wall -Os -fno-move-loop-invariants -fno-tree-scev-cprop -fno-inline-small-functions -Iusbdrv -I. -mmcu=atmega328p -DF_CPU=16000000 -DDEBUG_LEVEL=0  -o main.bin usbdrv/usbdrvasm.o usbdrv/oddebug.o main.o -Wl,--relax,--gc-sections -Wl,--section-start=.text=7800
rm -f main.hex main.eep.hex
avr-objcopy -j .text -j .data -O ihex main.bin main.hex
avr-size main.hex
   text       data        bss        dec        hex    filename
      0       1926          0       1926        786    main.hex
[mjd@onza firmware]$

The warnings don't seem to matter, and I have ignored them.

Now I can program the Doodad:

[mjd@onza firmware]$ su
Password:
[root@onza firmware]# make flash
avrdude -c avrisp2 -P usb -p atmega328p -U flash:w:main.hex:i

avrdude: AVR device initialized and ready to accept instructions

Reading | ################################################## | 100% 0.00s

avrdude: Device signature = 0x1e950f
avrdude: NOTE: FLASH memory has been specified, an erase cycle will be performed
         To disable this feature, specify the -D option.
avrdude: erasing chip
avrdude: reading input file "main.hex"
avrdude: writing flash (32646 bytes):

Writing | ################################################## | 100% 0.60s

avrdude: 32646 bytes of flash written
avrdude: verifying flash memory against main.hex:
avrdude: load data flash data from input file main.hex:
avrdude: input file main.hex contains 32646 bytes
avrdude: reading on-chip flash data:

Reading | ################################################## | 100% 8.67s

avrdude: verifying ...
avrdude: 32646 bytes of flash verified

avrdude: safemode: Fuses OK

avrdude done.  Thank you.

[root@onza firmware]# make fuse
avrdude -c avrisp2 -P usb -p atmega328p -U hfuse:w:0xda:m -U lfuse:w:0xff:m

avrdude: AVR device initialized and ready to accept instructions

Reading | ################################################## | 100% 0.00s

avrdude: Device signature = 0x1e950f
avrdude: reading input file "0xda"
avrdude: writing hfuse (1 bytes):

Writing | ################################################## | 100% 0.00s

avrdude: 1 bytes of hfuse written
avrdude: verifying hfuse memory against 0xda:
avrdude: load data hfuse data from input file 0xda:
avrdude: input file 0xda contains 1 bytes
avrdude: reading on-chip hfuse data:

Reading | ################################################## | 100% 0.00s

avrdude: verifying ...
avrdude: 1 bytes of hfuse verified
avrdude: reading input file "0xff"
avrdude: writing lfuse (1 bytes):

Writing | ################################################## | 100% 0.00s

avrdude: 1 bytes of lfuse written
avrdude: verifying lfuse memory against 0xff:
avrdude: load data lfuse data from input file 0xff:
avrdude: input file 0xff contains 1 bytes
avrdude: reading on-chip lfuse data:

Reading | ################################################## | 100% 0.00s

avrdude: verifying ...
avrdude: 1 bytes of lfuse verified

avrdude: safemode: Fuses OK

avrdude done.  Thank you.

[root@onza firmware]#

Let me explain my strategy regarding the LEDs.  My changes mean that the bootloader lights up the last LED, and the mouse demo lights up the second last LED.  Both programs also run a binary counter on the next four LEDs.  
If the last LED is on, it means that the bootloader has run.  If the second last LED is on, it means the mouse demo has run.  After programming the Doodad as above, the bootloader LED is on, but the mouse demo LED isn't.  That makes sense because programming the bootloader erases all memory.  Note also the four LEDs before the mouse demo LED are all on.  That tells me the bootloader tried to jump to the mouse demo.

Also, when I run the lsusb command, I can't see any USB devices corresponding to the Doodad.

Next, pull out the Doodad and insert it with the button held down.  The bootloader LED is on, the mouse demo LED is on, and the next four LEDs are running a binary counter.  That tells me the bootloader is in program mode.  lsusb also shows this:


Bus 003 Device 010: ID 16c0:05df VOTI

Later on I'll show how to program an application such as the mouse demo over USB, but for the moment I want to prove that I can load the mouse demo and the bootloader into memory at the same time, and that the bootloader will fall through to the mouse demo if the bootloader button isn't pressed when the Doodad first starts.

So far, whenever avrdude has programmed the '328 program flash, that memory has first been erased.  The problem with this is that it erases any existing program.

In order to get both the bootloader and the mouse demo in flash at the same time, we need to program the bootloader with a flash erase, then program the mouse demo without erasing the flash.  (Note, the only time you can load a program with the programmer without doing an erase is if you're loading into memory which has already been erased).

  • Erase the flash
--- vusb-20100715-orig/examples/hid-mouse/firmware/Makefile
+++ vusb-20100715/examples/hid-mouse/firmware/Makefile
@@ -11,7 +11,7 @@
 F_CPU   = 16000000 # in Hz
 FUSE_L  = 0xff# see below for fuse values for particular devices
 FUSE_H  = 0xda
-AVRDUDE = avrdude -c avrisp2 -P usb -p $(DEVICE) # edit this line for your programmer
+AVRDUDE = avrdude -c avrisp2 -P usb -p $(DEVICE) -D # edit this line for your programmer

 CFLAGS  = -Iusbdrv -I. -DDEBUG_LEVEL=0
 OBJECTS = usbdrv/usbdrv.o usbdrv/usbdrvasm.o usbdrv/oddebug.o main.o


  And if that wasn't impressive enough, After I plugged it into my computer's USB port, the mouse cursor started moving around in a large circle, and the LEDs are counting!
Here's what lsusb shows:

[root@onza mjd]# lsusb
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 002: ID 1a40:0101 TERMINUS TECHNOLOGY INC.
Bus 001 Device 003: ID 0951:1607 Kingston Technology Data Traveler 2.0
Bus 001 Device 004: ID 0951:1607 Kingston Technology Data Traveler 2.0
Bus 001 Device 005: ID 0951:1607 Kingston Technology Data Traveler 2.0
Bus 002 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub
Bus 003 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub
Bus 003 Device 002: ID 16c0:03e8 VOTI free for internal lab use 1000

The mouse is the "VOTI" entry. The USB vendor ID and product ID can be set in the software.

Now to see the details:

[root@onza mjd]# lsusb -v

Bus 003 Device 002: ID 16c0:03e8 VOTI free for internal lab use 1000
Device Descriptor:
  bLength                18
  bDescriptorType         1
  bcdUSB               1.10
  bDeviceClass            0 (Defined at Interface level)
  bDeviceSubClass         0
  bDeviceProtocol         0
  bMaxPacketSize0         8
  idVendor           0x16c0 VOTI
  idProduct          0x03e8 free for internal lab use 1000
  bcdDevice            1.00
  iManufacturer           1 obdev.at
  iProduct                2 Mouse
  iSerial                 0
  bNumConfigurations      1
  Configuration Descriptor:
    bLength                 9
    bDescriptorType         2
    wTotalLength           34
    bNumInterfaces          1
    bConfigurationValue     1
    iConfiguration          0
    bmAttributes         0x80
      (Bus Powered)
    MaxPower              400mA
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        0
      bAlternateSetting       0
      bNumEndpoints           1
      bInterfaceClass         3 Human Interface Device
      bInterfaceSubClass      0 No Subclass
      bInterfaceProtocol      0 None
      iInterface              0
        HID Device Descriptor:
          bLength                 9
          bDescriptorType        33
          bcdHID               1.01
          bCountryCode            0 Not supported
          bNumDescriptors         1
          bDescriptorType        34 Report
          wDescriptorLength      52
         Report Descriptors:
           ** UNAVAILABLE **
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x81  EP 1 IN
        bmAttributes            3
          Transfer Type            Interrupt
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0008  1x 8 bytes
        bInterval             100
Device Status:     0x0000
  (Bus Powered)

[root@onza mjd]#

I'm not sure why it says the report descriptors are unavailable.  But at least it works.

Now I'll start working on the bootloader.  If I can get the bootloader working, I can take the three pins we're currently using for ISP, and make them LED outputs.

To be covered in the next article:

  • How to use the bootloader to program new apps to the Doodad without using an ISP.
  • How to use fuses to protect the bootloader.