Napisano dnia 2.09.2019 r. o godzinie 21:10
Autor: Piotr Sperka
Recently I’ve been working on bootloaders for microcontrollers based on the ARM architecture. In my free time I decided to write a bootloader for ATTiny13, which is one of the smallest MCUs in the AVR family. Unlike larger AVRs, ATTiny13 doesn’t have hardware support for booting from the bootloader sector, it doesn’t have the ability to reallocate the interrupt table. The MCU does not even have a hardware UART, which I decided to use as a communication interface. μBoot-AvrTiny (as I called my bootloader) in its simplest form takes only 80 words (160 bytes) of FLASH and allows to use of the UART routines in the user program.
For communication with the computer I’ve decided to use UART. Mainly because of the huge popularity of the interface and its ease of use from virtually any computer, tablet or smartphone with a cheap USB adapter. As I mentioned, ATTiny13 isn’t equipped with hardware UART. However, many years ago Atmel released the AVR305 note, which describes optimised implementation of software UART. So I used the code from that note with a slight modification allowing using only one pin for half-duplex transmission. That kind of connection also provides a “hardware” echo (everything we send via the Tx line automatically returns to via the Rx line). With a 1.2MHz clock, a rate of 9600 baud/s is easily achieved in the standard 8N1 configuration. Bit rate can be adjusted using the baud constant at the beginning of the code. The formula for its calculation can be found in mentioned AVR305 note.
AVR microcontrollers allow self-programming, which is feature that is necessary to make a bootloader. Remember that this feature must be activated in fuse-bits, and isn’t active in factory default settings. Programming FLASH memory must be done by pages, in addition, before saving data on the page, you must clear it – this is due to the way FLASH memory works. For ATTiny13, the page has 32 bytes. EEPROM doesn’t have such restrictions and programming can be done both page by page (in this case page is 4 bytes long) or byte by byte.
The programming of each page of the FLASH memory contains:
An appropriately commented code snippet responsible for writing a FLASH page looks like this:
_PROGRAM_PAGE: ldi R25, 0x10 ; 16 words to write into flash ldi R24, 0x01 ; New value for SPMCSR register rcall getchar ; Get more significant part of Z register mov R31, Rxbyte rcall getchar ; Get less significant part of Z register mov R30, Rxbyte _PP_GDL: rcall getchar ; Get Data Loop, get more significant byte of word mov R0, Rxbyte rcall getchar ; Get less significant byte of word mov R1, Rxbyte out SPMCSR, R24 spm ; Write word to temp buffer dec R25 breq _PP_ERASE inc R30 ; Increment PCWORD by 2 (Z reg) inc R30 rjmp _PP_GDL ; Erase page _PP_ERASE: ldi R24, 0x03 ; New value for SPMCSR out SPMCSR, R24 spm ; Write flash ldi R24, 0x05 ; New value for SPMCSR andi R30, 0xE0 ; Zero PCWORD in Z out SPMCSR, R24 spm ; Send response ldi Txbyte, 'Y' rcall putchar
Reading data from the FLASH memory is much simpler. The following code should explain itself:
_READ_FLASH: ldi R24, LOW(2*(FLASHEND + 1)) ldi R25, HIGH(2*(FLASHEND + 1)) clr R30 clr R31 _RFL: lpm Txbyte, Z+ rcall putchar sbiw R24, 1 brne _RFL
In a more extensive version of the bootloader it is also possible to read and write EEPROM. The code is much simpler than in the case of program memory:
_READ_EEPROM: clr R25 _REL: sbic EECR, EEPE ; Wait for completion of previous write rjmp _READ_EEPROM out EEARL, R25 ; Set up address (r17) in address register sbi EECR,EERE ; Start eeprom read by writing EERE in Txbyte, EEDR ; Read data from data register rcall putchar inc R25 cpi R25, (EEPROMEND + 1) ; EEPROM is 64 bytes, so we need only one register to count brne _REL rjmp _READ_LOOP _WRITE_EEPROM: sbic EECR, EEPE ; Wait for completion of previous write rjmp _WRITE_EEPROM ldi R16, (0<<EEPM1)|(0<<EEPM0) ; Set Programming mode out EECR, R16 rcall getchar ; Set up address (r17) in address register out EEARL, Rxbyte rcall getchar ; Write data (r16) to data register out EEDR, Rxbyte sbi EECR, EEMPE ; Write logical one to EEMPE sbi EECR, EEPE ; Start eeprom write by setting EEPE ; Send response ldi Txbyte, 'Y' rcall putchar rjmp _READ_LOOP
Our microcontroller is equipped with only 1kB of program memory, hence I put great emphasis on the smallest size of the bootloader. This in turn leads to the conclusion that the communication protocol must be really simple. To read the entire FLASH memory, send the “R” character. In response, you will receive 1024 bytes corresponding to the contents of the FLASH. Writing is a bit more complex. First, send the “P” character followed by the frame:
Success is confirmed by the “Y” character sent by AVR. If microcontroller doesn’t respond, it means something went wrong (ex. you sent too little data).
When EEPROM support is compiled in, reading works similarly as for FLASH memory. After receiving the “E” character, the device will send all 64 bytes of EEPROM. Writing can be done by sending the “F” character, and then sending two more characters: address (0-63) and data (0-255). Success will be confirmed by the returned “Y” character.
Because bootloader uses software UART, the AVR is not able to receive new bits when processing previous data. The result is that sending data from the computer in the form of a byte array (which produces data with no time gaps between bytes) usually results in transmission errors. The remedy for this problem is sending data byte by byte, calling the flush function each time.
Above, I have provided an example schematic. The diode connected to PB3 is only for testing purposes, while C1 and R2 can be theoretically eliminated. To test bootloader operation and make it easier to use, I wrote a very simple Python program. This program allows reading the contents of both memories to a binary file, as well as writing to memory from binary files. Hex files are not currently supported, however, firstly, programs converting hex to bin are available, and secondly, you can add such a function if you need.
The program also takes care to not overwrite the bootloader with the uploaded binary, it also takes care of splitting the binary file into appropriate pages and sending data to the bootloader. It also ensures that the only necessary change in the binary file is made – it modifies the rjmp instruction at 0x00 (the address from which the microcontroller starts after reset) so that microcontroller jumps to the bootloader. Normally, we place the jump instruction to the beginning of the program here.
To start the bootloader, short the data line to ground (Boot button), reset the microcontroller (Reset button), release the Reset button, and finally release the Boot button. Restarting in normal mode is done by resetting the microcontroller with the data line in a high state.
μBoot-AvrTiny is very simple, and although it fulfills its function, you must be aware of certain restrictions. First, the program must start at 0x0A (directly after the interrupt table), because the bootloader jumps to this address to start the user application. This can of course be changed by compiling your bootloader version. Secondly, it is possible to overwrite (damage) the bootloader code by attempting to write the pages on which it is located. It’s on the computer side (ex. mentioned Python script) to make sure that only those pages that are intended for it are overwritten. It is also on the computer side to modify the binary the way it jumps to the bootloader after reset.
To sum up, I was able to write a simple but fully functional bootloader for the ATTiny13 microcontroller, containing only 80 words of code. In addition, part of the bootloader (routines for sending and receiving data via UART) can be used in the user application. If the topic interested you and you want to try it out, go to my GitHub. There you will find sources and compiled versions of the bootloader along with test applications (LED flashing and the use of the UART functions from the bootloader) and Python script: https://github.com/PiotrSperka/avrTinyBootloader.
Finally, if you have any comments, questions or suggestions, please contact me. See you in the next article!