ATtiny85 USB Boot Loader: Details
This is the first solo embedded project I've done in about five years. After doing less and less low-level embedded work at my job and doing more high-level design and project management, I felt I needed to do some embedded projects of my own. I saw the vusbtiny programmer project and wanted to port the JTAG programming functionality in my SP Duo programmer to the ATtiny85 using USB. With JTAG and USB on the tiny85, I'll be using all the IO pins including the reset line, and wanted a boot loader to make updating the programmer possible without a High Voltage Programmer, and I was surprised to see there was no USB boot loader available for the tiny85. When I looked into the details I understood more about why there was no USB boot loader , but still thought it was possible.
VUSB seemed like the obvious choice for adding USB to the tiny85, as there are projects that use the tiny85 already, and there is oscillator calibration support built in. I selected the USBaspLoader project as a base for a few reasons:
- It uses VUSB
- I don't want to write a PC-side loader to send the application to the boot loader , and USBaspLoader uses existing AVRDUDE program
- I want to use the USBasp programmer as a base for my JTAG programmer project, so using the same commands for the boot loader makes sense
There are several challenges to creating a boot loader for the ATtiny85
Challenge 1: No Read-While-Write Support
The ATtiny architecture (at least in the tinyX5 series) doesn't have Read-While-Write support, meaning the CPU is halted while flash is being erased or written. With the CPU halted, the USB code can't handle communication with the PC. An erase operation or write operation takes 4.5ms maximum. The USB host polls a low-speed device every 1ms, so I originally thought this would be disastrous and planned to handle this by transferring as much data as possible, disconnecting from the PC, and reconnecting after writing the data.
I found that when AVRDUDE wasn't actively transferring data to the boot loader , the boot loader could ignore the USB interrupts and as long as interrupts were enabled before the next time AVRDUDE sent a message, the boot loader would remain connected to the PC. I needed to make a simple modification to AVRDUDE to support this, changing the block size sent to the usbasp programmer when programming flash and adding a delay between blocks. I made the block size equal to the parts flash page size, and gave a 10ms delay, enough time for an erase and write cycle per flash page.
Challenge 2: No Boot Loader Vector Table
On most of the ATmega parts, when the boot loader is running, the vector table can be moved to start where the boot loader code is stored in the part, so the reset and/or interrupts jump to sections in the boot loader code instead of application code. The ATtiny85 doesn't have this vector table, so the reset and interrupt vector will always jump to code in the application space. Because the boot loader and application will need interrupts to support USB, the table will have to be shared between application and boot loader .
I wanted to store the boot loader in the upper section of flash, the same as a part with a boot loader section. When first programming the part, the lower pages of flash (including the application vector table) will contain 0xFF's, which are handled as NOPs by the CPU, so the CPU will eventually end up at the reset vector in the boot loader, and the boot loader will start executing. To connect via USB however, the interrupt vector for the D+ line needs to jump to the boot loader section. To handle this, I have the boot loader write an rjmp instruction to the application reset vector and external interrupt vector, jumping from to the appropriate vector in the boot loader section. With this in place, the boot loader can now communicate with the PC.
After transferring the application and writing it to flash, either the vectors will point to the application, disabling the boot loader, or point to the boot loader , preventing the application from directly using the vectors. Disabling the boot loader is unacceptable, as to get back to the boot loader, the application would have to both be aware of the boot loaders presence, and jump to it at the appropriate time. If there was an error with the application, we could be locked out from the part, unable to replace the application. We need to make the boot loader share the vectors with the application.
It's common for the boot loader to be run first when the CPU starts, and for the boot loader to jump to the application reset vector when done. We can do the same thing, only bypassing the application reset vector and jumping to the application init code instead. By looking at the rjmp instruction stored at the application reset vector, we know the address of the init code, and we can make our own rjmp instruction to get to that same address. Storing that information into flash in the boot loader section is either risky or ineffiencient: risky if it's modifying a page in flash that stores some boot loader code, or inefficient if we reserve an entire page for this information. Instead, I decided to create a mini application vector table at the end of the application section with an rjmp to the application init code, and later, for the application external interrupt handler. To jump to the application from the boot loader, the boot loader just jumps to the reset vector in the mini vector table.
Sharing the external interrupt vector is tricker. I figured if the external interrupt vector always points to the boot loader, and the boot loader ISR can determine if the application is running, the boot loader ISR can jump to the application ISR instead of continuing with the boot loader ISR. It's easy to determine if it's the application running versus the boot loader by reading the program counter off the stack in the ISR. If the program counter is less than the start of the boot loader, the interrupt happened while running the application.
Just like the application reset vector, I created a vector in the mini vector table for the external interrupt, and the boot loader external interrupt handler jumps to the vector table to get to the application external interrupt handler.
While writing the application to flash, the boot loader replaces the application reset and external interrupt vectors with rjmps to the boot loader vector table, and fills in the mini vector table.
I found that this worked to run a simple application with an external interrupt handler, but didn't work for USB. There were too many instructions between when the USB D+ line triggered the interrupt, and when the application ISR started. VUSB has clear notes on how much latency there can be between the interrupt condition and the start of the application interrupt handler, and retrieving the program counter from the stack was taking too long. I came up with a temporary solution that had the boot loader init code store unique values in two IO registers, and the boot loader ISR would jump to the application ISR unless these two values were set. This worked, but felt a little too much like a hack, with a small chance that the application would use these values and break the ISR.
I found that VUSB supported using the pin change interrupt instead of external interrupt for the USB D+ line. If we assume that most tiny85 USB applications use the external interrupt and not PCINT for USB D+, then we can have the boot loader use pin change, and have the slow program counter comparison code run in the pin change ISR, leaving the external interrupt to be used just by the application. The application can use the pin change interrupt for something other than USB, as long as it accepts a little extra latency. This should be acceptable, as the application should already expect some latency from the higher priority external interrupt used by USB.
** Updated as of 2012-05-13 release **
I came up with a cleaner way to quickly determine if the application or bootloader is running, that didn’t feel like as much of a hack. The bootloader pushes a “magic word” (0xB007 for “BOOT”) onto the stack right after the stack is initialized. This word is always at a fixed location at the end of RAM, so the ISR can do two quick reads from RAM. The ISR compares the word in RAM to the magic word, and if the values match, the bootloader is running, if not, the application is running. This is fast enough to allow the same interrupt to be used for USB in the application and bootloader, allowing the application to use the pin change interrupt for USB.
Challenge 3: Logic to enter boot loader mode with limited IO
On other USB boot loader designs I've seen, the boot loader checks a jumper upon reset to see if it should remain in boot loader mode or jump to the application instead. On the ATtiny85 with only 3-4 IO available after USB is added to the part, dedicating an IO to the boot loader may not work.
Instead of looking for a specific start condition, I have the boot loader enumerate with the PC as a usbasp programmer, and only start the application only after a delay, giving a short window for the PC to update the application before the application starts. In addition, to prevent running a partial or corrupted application, the boot loader calculates an application checksum and compares it to a value previously stored in flash. If the application isn't valid, it stays in boot loader mode.
Losing vector table can lock out application and boot loader
Though the reset and USB D+ interrupt vectors are written at boot loader startup, erasing the first page of flash is risky, as if the vector table is lost we may not be able to get to the boot loader at reset. To be safe, the full application should be erased starting from the last application page down to the first page. Sending the usbasp erase command erases the application section of flash in this manner. In this case, if the boot loader is interrupted between erasing the first page and writing the vectors, on next reset the application will be fully erased and the CPU will run through the NOPs until it reaches the boot loader.
No Boot Loader Section Protection
Because the tinyX5 architecture wasn't designed with full boot loader support, there is no way to disable self programming support through firmware, or to lock the boot loader section from being modified. If an application bug (or boot loader bug for that matter) inadvertently jumped to boot loader code that erases or writes to a page in flash, it could corrupt the boot loader.
Data corruption between USB and PC
VUSB doesn't verify data on incoming packets, so the application must verify data on its own. Usbasp doesn't have an application level checksum or CRC, so potentially data could get corrupted between the PC and the boot loader. Our checksum is calculated based on data received, so an application binary that was corrupted during transport over USB would get a valid checksum and the boot loader would jump to its reset vector. Given the previously mentioned risk of a buggy application erasing the boot loader, this could result in a part that needs to be reloaded through an external programmer (or high voltage programmer if the reset pin is used for IO).
The boot loader currently works, allowing for the few attiny85 applications I tried to load and run successfully.
- hid-mouse from the vusb distribution examples, modified to run on the tiny85 with oscillator calibration
- The same hid-mouse application, but modified to use pin change interrupt instead of INT0
- USB Business Card by Frank Zhao
- My Tiny AVR JTAG Programmer
With all options compiled in it takes up 2822 bytes, leaving 5370 bytes for the application
Almost 3k is a lot of space to give up from the application, there could be some optimization left to give a little more space back to the application.
Allow for use of other pins for usb through Pin Change interrupt
If INT0 is used for USB, the hardware peripherals for SPI and I2C as well as some other things can't be used. There's a solution for this, but it hasn't been integrated into the project yet.
Better checksum/crc calculation
An 8-bit checksum is being used right now, though a 16-bit checksum would be more robust. A CRC could be used instead to be even more robust, but this would take up even more space from the application.
See "Data corruption" section above under "Risks"
Protect boot loader against corruption from accidental jump to boot loader section
Some ideas: Do comparison of address byte to start of boot loader section before executing a program or erase command. Set magic word in SRAM or IO register at start of boot loader, clear before jumping to application, verify word is present before executing program or erase command.