Arduino Bootloader Untangled

pile of boots
Ever try to study the Arduino Bootloader, but give up because the code seemed too confusing?

Fools ignore complexity. Pragmatists suffer it. Some can avoid it. Geniuses remove it.

-Alan Perlis

Not that I’m a genius, I was just getting tired of being a fool!

I was trying to study the inner workings of the Arduino bootloader, but gave up after a few minutes of tracing the confusing conditional compile commands and botched indentation format. However, I really wanted to understand what was going on in there, so I made a second attempt. I reformatted the code indentation and extracted all of the conditional commands with the exception of what is required for an ATmega168/328. I also rearranged the peculiar inline assembler into a format that makes more sense (at least to me).

What follows are my results.

Tracing the code makes more sense with these defines pulled from inside the makefile:

#define MAX_TIME_COUNT (F_CPU>>4)
#define F_CPU 16000000UL

/* Serial Bootloader for Atmel megaAVR Controllers        */
/*                                                        */
/* tested with ATmega8, ATmega128 and ATmega168           */
/* should work with other mega's, see code for details    */
/*                                                        */
/* ATmegaBOOT.c                                           */
/*                                                        */
/*                                                        */
/* 20090308: integrated Mega changes into main bootloader */
/*           source by D. Mellis                          */
/* 20080930: hacked for Arduino Mega (with the 1280       */
/*           processor, backwards compatible)             */
/*           by D. Cuartielles                            */
/* 20070626: hacked for Arduino Diecimila (which auto-    */
/*           resets when a USB connection is made to it)  */
/*           by D. Mellis                                 */
/* 20060802: hacked for Arduino by D. Cuartielles         */
/*           based on a previous hack by D. Mellis        */
/*           and D. Cuartielles                           */
/*                                                        */
/* Monitor and debug functions were added to the original */
/* code by Dr. Erik Lins, (See below)         */
/*                                                        */
/* Thanks to Karl Pitrich for fixing a bootloader pin     */
/* problem and more informative LED blinking!             */
/*                                                        */
/* For the latest version see:                            */
/*                                 */
/*                                                        */
/* ------------------------------------------------------ */
/*                                                        */
/* based on stk500boot.c                                  */
/* Copyright (c) 2003, Jason P. Kyle                      */
/* All rights reserved.                                   */
/* see for original file and information         */
/*                                                        */
/* This program is free software; you can redistribute it */
/* and/or modify it under the terms of the GNU General    */
/* Public License as published by the Free Software       */
/* Foundation; either version 2 of the License, or        */
/* (at your option) any later version.                    */
/*                                                        */
/* This program is distributed in the hope that it will   */
/* be useful, but WITHOUT ANY WARRANTY; without even the  */
/* implied warranty of MERCHANTABILITY or FITNESS FOR A   */
/* PARTICULAR PURPOSE.  See the GNU General Public        */
/* License for more details.                              */
/*                                                        */
/* You should have received a copy of the GNU General     */
/* Public License along with this program; if not, write  */
/* to the Free Software Foundation, Inc.,                 */
/* 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA */
/*                                                        */
/* Licence can be viewed at                               */
/*                    */
/*                                                        */
/* Target = Atmel AVR m128,m64,m32,m16,m8,m162,m163,m169, */
/* m8515,m8535. ATmega161 has a very small boot block so  */
/* isn't supported.                                       */
/*                                                        */
/* Tested with m168                                       */
#include <avr/io.h>
#include <avr/pgmspace.h>
#include <avr/interrupt.h>
#include <util/delay.h>
/* the current avr-libc eeprom functions do not support the ATmega168 */
/* own eeprom write/read functions are used instead */
#include <avr/eeprom.h>

/* after this many errors give up and launch application */

/* set the UART baud rate */
#define BAUD_RATE   19200

/* SW_MAJOR and MINOR needs to be updated from time to time to avoid warning message from AVR Studio */
/* never allow AVR Studio to do an update !!!! */
#define HW_VER   0x02
#define SW_MAJOR 0x01
#define SW_MINOR 0x10

/* other ATmegas have only one UART, so only one pin is defined to enter bootloader */
#define BL_DDR  DDRD
#define BL_PIN  PIND
#define BL      PIND6

/* onboard LED is used to indicate, that the bootloader was entered (3x flashing) */
/* if monitor functions are included, LED goes on after monitor was entered */
/* Onboard LED is connected to pin PB5 in Arduino NG, Diecimila, and Duomilanuove */
#define LED_DDR  DDRB
#define LED_PIN  PINB
#define LED      PINB5

/* define various device id's */
/* manufacturer byte is always the same */
#define SIG1  0x1E  // Yep, Atmel is the only manufacturer of AVR micros.  Single source 😦

#if defined __AVR_ATmega168__
#define SIG2  0x94
#define SIG3  0x06
#elif defined __AVR_ATmega328P__
#define SIG2  0x95
#define SIG3  0x0F
#define PAGE_SIZE  0x40U  //64 words

/* some variables */
union address_union {
  uint16_t word;
  uint8_t  byte[2];
} address;

union length_union {
  uint16_t word;
  uint8_t  byte[2];
} length;

struct flags_struct {
  unsigned eeprom : 1;
  unsigned rampz  : 1;
} flags;

uint8_t buff[256];
uint8_t address_high;
uint8_t pagesz=0x80;
uint8_t i;
uint8_t bootuart = 0;
uint8_t error_count = 0;
void (*app_start)(void) = 0x0000;

/* main program starts here */
int main(void) {
  uint8_t ch, ch2;
  uint16_t w;

  asm volatile("nop\n\t");

  /* check if flash is programmed already, if not start bootloader anyway */
  if (pgm_read_byte_near(0x0000) != 0xFF) {
    UBRR0L = (uint8_t)(F_CPU/(BAUD_RATE*16L) - 1);
    UBRR0H = (F_CPU/(BAUD_RATE*16L) - 1)>>8;
    UCSR0B = (1<    UCSR0C = (1< 0x85)

      /* AVR ISP/STK500 board requests */
      else if (ch == 'A') {
        ch2 = getch();
        if (ch2 == 0x80)
          byte_response(HW_VER);    //Hardware version
        else if (ch2 == 0x81)
          byte_response(SW_MAJOR);  //Software major version
        else if (ch2 == 0x82)
          byte_response(SW_MINOR);  //Software minor version
        else if (ch2 == 0x98)
          byte_response(0x03);      //Unknown but seems to be required by avr studio 3.56
          byte_response(0x00);      //Covers various unnecessary responses we don't care about

      /* Device Parameters  DON'T CARE, DEVICE IS FIXED  */
      else if (ch == 'B') {

      /* Parallel programming stuff  DON'T CARE  */
      else if (ch == 'E') {

      /* P: Enter programming mode  */
      /* R: Erase device, don't care as we will erase one page at a time anyway.*/
      else if (ch == 'P' || ch == 'R')

      /* Leave programming mode  */
      else if (ch == 'Q')

      /*Set address, little endian. EEPROM in bytes, FLASH in words  */
      /*Perhaps extra addr bytes may be added in future to support>128kB FLASH*/
      /*This might explain why little endian was used here, big endian used everywhere else.*/
      else if (ch == 'U') {
        address.byte[0] = getch();
        address.byte[1] = getch();

      /*Universal SPI programming command, disabled.  Would be used for fuses and lock bits. */
      else if (ch == 'V') {
        if (getch() == 0x30) {
          ch = getch();
          if (ch == 0)
          else if (ch == 1)
        } else {

      /* Write memory */
      else if (ch == 'd') {
        length.byte[1] = getch();         //length is big endian and is in bytes
        length.byte[0] = getch();
        flags.eeprom = 0;
        if (getch() == 'E')
          flags.eeprom = 1;
        for (w=0; w          buff[w] = getch(); //Store data in buffer,
                             //can't keep up with serial data stream whilst programming pages
        if (getch() == ' ') {
          if (flags.eeprom) {
            //Write to EEPROM one byte at a time
            address.word <            for (w=0; w              while (EECR & (1<<EEPE));
              EEAR = (uint16_t)(void *)address.word;
              EEDR = buff[w];
              EECR |= (1<<EEMPE);
              EECR |= (1<<EEPE);
          } else {
            //Write to FLASH one page at a time
            address.word = address.word< byte location
            if ((length.byte[0] & 0x01))
              length.word++;    //Even up an odd number of bytes
            cli();              //Disable interrupts, just to be sure
            while (bit_is_set(EECR, EEPE)); //Wait for previous EEPROM writes to complete
            asm volatile(

Translation of the inline assembly code here:

              clr  r17             //page_word_count
              lds  r30, address    //Address of FLASH location (in bytes)
              lds  r31, address+1
              ldi  r28, lo8(buff)  //Start of buffer array in RAM
              ldi  r29, hi8(buff)
              lds  r24, length     //Length of data to be written (in bytes)
              lds  r25, length+1
length_loop:                       //Main loop, repeat for number of words in block
              cpi  r17, 0x00       //If page_word_count=0 then erase page
              brne no_page_erase
              lds  r16, SPMCSR     //Wait for previous spm to complete
              andi r16, 1
              cpi  r16, 1
              breq wait_spm1
              ldi  r16, 0x03       //Erase page pointed to by Z
              sts  SPMCSR, r16
              lds  r16, SPMCSR     //Wait for previous spm to complete
              andi r16, 1
              cpi  r16, 1
              breq wait_spm2

              ldi  r16, 0x11       //Re-enable RWW section
              sts  SPMCSR, r16
              ld   r0, Y+          //Write 2 bytes into page buffer
              ld   r1, Y+

              lds  r16, SPMCSR     //Wait for previous spm to complete
              andi r16, 1
              cpi  r16, 1
              breq wait_spm3
              ldi  r16, 0x01       //Load r0,r1 into FLASH page buffer
              sts  SPMCSR, r16

              inc  r17             //page_word_count++
              cpi  r17, PAGE_SIZE
              brlo same_page       //Still same page in FLASH
              clr  r17             //New page, write current one first
              lds  r16, SPMCSR     //Wait for previous spm to complete
              andi r16, 1
              cpi  r16, 1
              breq wait_spm4
              ldi  r16, 0x05       //Write page pointed to by Z
              sts  SPMCSR, r16
              lds  r16, SPMCSR     //Wait for previous spm to complete
              andi r16, 1
              cpi  r16, 1
              breq wait_spm5
              ldi  r16, 0x11       //Re-enable RWW section
              sts  SPMCSR, r16
              adiw r30, 2          //Next word in FLASH
              sbiw r24, 2          //length-2
              breq final_write     //Finished
              rjmp length_loop
              cpi  r17,0
              breq block_done
              adiw r24, 2          //length+2,fool above check on length after short page write
              rjmp write_page
              clr  __zero_reg__    //restore zero register

The remainder of the bootloader code:

            /*Should really add a wait for RWW section to be enabled, don't actually need it since we never */
            /*exit the bootloader without a power cycle anyhow */
        } else
          if (++error_count == MAX_ERROR_COUNT)

      /* Read memory block mode, length is big endian.  */
      else if (ch == 't') {
        length.byte[1] = getch();
        length.byte[0] = getch();
        address.word = address.word< byte location
        if (getch() == 'E')
          flags.eeprom = 1;
          flags.eeprom = 0;
        if (getch() == ' ') {             // Command terminator
          for (w=0; w            if (flags.eeprom) {           // Byte access EEPROM read
              while(EECR & (1<<EEPE));
              EEAR = (uint16_t)(void *)address.word;
              EECR |= (1<= 'a')
    return (a - 'a' + 0x0a);
  else if (a >= '0')
    return(a - '0');
  return a;

char gethex(void) {
  return (gethexnib()<>4;
  if (ah >= 0x0a)
    ah = ah - 0x0a + 'a';
    ah += '0';
  ch &= 0x0f;
  if (ch >= 0x0a)
    ch = ch - 0x0a + 'a';
    ch += '0';

void putch(char ch) {
  while (!(UCSR0A & _BV(UDRE0)));
  UDR0 = ch;

char getch(void) {
  uint32_t count = 0;

  while (!(UCSR0A & _BV(RXC0))) {
    if (count > MAX_TIME_COUNT)
  return UDR0;

void getNch(uint8_t count) {

void byte_response(uint8_t val) {
  if (getch() == ' ') {
  } else
    if (++error_count == MAX_ERROR_COUNT)

void nothing_response(void) {
  if (getch() == ' ') {
  } else
    if (++error_count == MAX_ERROR_COUNT)

void flash_led(uint8_t count) {
  while (count--) {
    LED_PORT |= _BV(LED);
    LED_PORT &= ~_BV(LED);

About Jim Eli

µC experimenter
This entry was posted in Uncategorized. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s