/* 
 * Project: Micronucleus -  v2.0
 *
 * Micronucleus V2.0             (c) 2014 Tim Bo"scke - cpldcpu@gmail.com
 *                               (c) 2014 Shay Green
 * Original Micronucleus         (c) 2012 Jenna Fox
 *
 * Based on USBaspLoader-tiny85  (c) 2012 Louis Beaudoin
 * Based on USBaspLoader         (c) 2007 by OBJECTIVE DEVELOPMENT Software GmbH
 *
 * License: GNU GPL v2 (see License.txt)
 */
 
#define MICRONUCLEUS_VERSION_MAJOR 2
#define MICRONUCLEUS_VERSION_MINOR 0

#include <avr/io.h>
#include <avr/pgmspace.h>
#include <avr/wdt.h>
#include <avr/boot.h>
#include <util/delay.h>

#include "bootloaderconfig.h"
#include "usbdrv/usbdrv.c"

// verify the bootloader address aligns with page size
#if (defined __AVR_ATtiny841__)||(defined __AVR_ATtiny441__)  
  #if BOOTLOADER_ADDRESS % ( SPM_PAGESIZE * 4 ) != 0
    #error "BOOTLOADER_ADDRESS in makefile must be a multiple of chip's pagesize"
  #endif
#else
  #if BOOTLOADER_ADDRESS % SPM_PAGESIZE != 0
    #error "BOOTLOADER_ADDRESS in makefile must be a multiple of chip's pagesize"
  #endif  
#endif

#if SPM_PAGESIZE>256
  #error "Micronucleus only supports pagesizes up to 256 bytes"
#endif

// Device configuration reply
// Length: 6 bytes
//   Byte 0:  User program memory size, high byte
//   Byte 1:  User program memory size, low byte   
//   Byte 2:  Flash Pagesize in bytes
//   Byte 3:  Page write timing in ms. 
//    Bit 7 '0': Page erase time equals page write time
//    Bit 7 '1': Page erase time equals page write time divided by 4
//   Byte 4:  SIGNATURE_1
//   Byte 5:  SIGNATURE_2 

PROGMEM const uint8_t configurationReply[6] = {
  (((uint16_t)PROGMEM_SIZE) >> 8) & 0xff,
  ((uint16_t)PROGMEM_SIZE) & 0xff,
  SPM_PAGESIZE,
  MICRONUCLEUS_WRITE_SLEEP,
  SIGNATURE_1,
  SIGNATURE_2
};  

  typedef union {
    uint16_t w;
    uint8_t b[2];
  } uint16_union_t;
  
#if OSCCAL_RESTORE
  register uint8_t      osccal_default  asm("r2");
#endif 

register uint16_union_t currentAddress  asm("r4");  // r4/r5 current progmem address, used for erasing and writing 
register uint16_union_t idlePolls       asm("r6");  // r6/r7 idlecounter

// command system schedules functions to run in the main loop
enum {
  cmd_local_nop=0, 
  cmd_device_info=0,
  cmd_transfer_page=1,
  cmd_erase_application=2,
  cmd_write_data=3,
  cmd_exit=4,
  cmd_write_page=64  // internal commands start at 64
};
register uint8_t        command         asm("r3");  // bind command to r3 

// Definition of sei and cli without memory barrier keyword to prevent reloading of memory variables
#define sei() asm volatile("sei")
#define cli() asm volatile("cli")
#define nop() asm volatile("nop")
#define wdr() asm volatile("wdr")

// Use the old delay routines without NOP padding. This saves memory.
#define __DELAY_BACKWARD_COMPATIBLE__   

/* ------------------------------------------------------------------------ */
static inline void eraseApplication(void);
static void writeFlashPage(void);
static void writeWordToPageBuffer(uint16_t data);
static uint8_t usbFunctionSetup(uint8_t data[8]);
static inline void leaveBootloader(void);

// This function is never called, it is just here to suppress a compiler warning.
USB_PUBLIC usbMsgLen_t usbFunctionDescriptor(struct usbRequest *rq) { return 0; }

// erase all pages until bootloader, in reverse order (so our vectors stay in place for as long as possible)
// to minimise the chance of leaving the device in a state where the bootloader wont run, if there's power failure
// during upload
static inline void eraseApplication(void) {
  uint16_t ptr = BOOTLOADER_ADDRESS;

  while (ptr) {
#if (defined __AVR_ATtiny841__)||(defined __AVR_ATtiny441__)    
    ptr -= SPM_PAGESIZE * 4;        
#else
    ptr -= SPM_PAGESIZE;        
#endif    
    boot_page_erase(ptr);
  }
  
  // Reset address to ensure the reset vector is written first.
  currentAddress.w = 0;   
}

// simply write currently stored page in to already erased flash memory
static inline void writeFlashPage(void) {
  if (currentAddress.w - 2 <BOOTLOADER_ADDRESS)
      boot_page_write(currentAddress.w - 2);   // will halt CPU, no waiting required
}

// Write a word into the page buffer.
// Will patch the bootloader reset vector into the main vectortable to ensure
// the device can not be bricked. Saving user-reset-vector is done in the host 
// tool, starting with firmware V2
static void writeWordToPageBuffer(uint16_t data) {
    
#if BOOTLOADER_ADDRESS < 8192
  // rjmp
  if (currentAddress.w == RESET_VECTOR_OFFSET * 2) {
    data = 0xC000 + (BOOTLOADER_ADDRESS/2) - 1;
  }
#else
  // far jmp
  if (currentAddress.w == RESET_VECTOR_OFFSET * 2) {
    data = 0x940c;
  } else if (currentAddress.w == (RESET_VECTOR_OFFSET +1 ) * 2) {
    data = (BOOTLOADER_ADDRESS/2);
  }    
#endif

#if OSCCAL_SAVE_CALIB
   if (currentAddress.w == BOOTLOADER_ADDRESS - TINYVECTOR_OSCCAL_OFFSET) {
      data = OSCCAL;
   }     
#endif
  
  boot_page_fill(currentAddress.w, data);
  currentAddress.w += 2;
}

/* ------------------------------------------------------------------------ */
static uint8_t usbFunctionSetup(uint8_t data[8]) {
  usbRequest_t *rq = (void *)data;
 
  if (rq->bRequest == cmd_device_info) { // get device info
    usbMsgPtr = (usbMsgPtr_t)configurationReply;
    return sizeof(configurationReply);      
  } else if (rq->bRequest == cmd_transfer_page) { 
      // Set page address. Address zero always has to be written first to ensure reset vector patching.
      // Mask to page boundary to prevent vulnerability to partial page write "attacks"
        if ( currentAddress.w != 0 ) {
            currentAddress.b[0]=rq->wIndex.bytes[0] & (~ (SPM_PAGESIZE-1));     
            currentAddress.b[1]=rq->wIndex.bytes[1];     
        }        
    } else if (rq->bRequest == cmd_write_data) { // Write data
      writeWordToPageBuffer(rq->wValue.word);
      writeWordToPageBuffer(rq->wIndex.word);
      if ((currentAddress.b[0] % SPM_PAGESIZE) == 0)
          command=cmd_write_page; // ask runloop to write our page       
  } else {
    // Handle cmd_erase_application and cmd_exit
    command=rq->bRequest&0x3f;
  }
  return 0;
}

static void initHardware (void)
{
  // Disable watchdog and set timeout to maximum in case the WDT is fused on 
#ifdef CCP
  // New ATtinies841/441 use a different unlock sequence and renamed registers
  MCUSR=0;    
  CCP = 0xD8; 
  WDTCSR = 1<<WDP2 | 1<<WDP1 | 1<<WDP0; 
#else
  MCUSR=0;    
  WDTCR = 1<<WDCE | 1<<WDE;
  WDTCR = 1<<WDP2 | 1<<WDP1 | 1<<WDP0; 
#endif  

  
  usbDeviceDisconnect();  /* do this while interrupts are disabled */
  _delay_ms(300);  
  usbDeviceConnect();

  usbInit();    // Initialize INT settings after reconnect
}

/* ------------------------------------------------------------------------ */
// reset system to a normal state and launch user program
static void leaveBootloader(void) __attribute__((__noreturn__));
static inline void leaveBootloader(void) {
 
  bootLoaderExit();

  usbDeviceDisconnect();  /* Disconnect micronucleus */

  USB_INTR_ENABLE = 0;
  USB_INTR_CFG = 0;       /* also reset config bits */

#if OSCCAL_RESTORE_DEFAULT
  OSCCAL=osccal_default;
  nop(); // NOP to avoid CPU hickup during oscillator stabilization
#endif
    
 asm volatile ("rjmp __vectors - 4"); // jump to application reset vector at end of flash
  
 for (;;); // Make sure function does not return to help compiler optimize
}

void USB_INTR_VECTOR(void);
int main(void) {
  bootLoaderInit();

  /* save default OSCCAL calibration  */
#if OSCCAL_RESTORE_DEFAULT
  osccal_default = OSCCAL;
#endif
  
#if OSCCAL_SAVE_CALIB
  // adjust clock to previous calibration value, so bootloader starts with proper clock calibration
  unsigned char stored_osc_calibration = pgm_read_byte(BOOTLOADER_ADDRESS - TINYVECTOR_OSCCAL_OFFSET);
  if (stored_osc_calibration != 0xFF) {
    OSCCAL=stored_osc_calibration;
    nop();
  }
#endif
  
  if (bootLoaderStartCondition()||(pgm_read_byte(BOOTLOADER_ADDRESS - TINYVECTOR_RESET_OFFSET + 1)==0xff)) {
  
    initHardware();        
    LED_INIT();

    if (AUTO_EXIT_NO_USB_MS>0) {
      idlePolls.b[1]=((AUTO_EXIT_MS-AUTO_EXIT_NO_USB_MS)/5)>>8;
    } else {
      idlePolls.b[1]=0;
    }
    
    command=cmd_local_nop;     
    currentAddress.w = 0;
    
    do {
      // 15 clockcycles per loop.     
      // adjust fastctr for 5ms timeout
      
      uint16_t fastctr=(uint16_t)(F_CPU/(1000.0f*15.0f/5.0f));
      uint8_t resetctr=20;
  
      do {        
        if ((USBIN & USBMASK) !=0) resetctr=20;
        
        if (!--resetctr) { // reset encountered
           usbNewDeviceAddr = 0;   // bits from the reset handling of usbpoll()
           usbDeviceAddr = 0;
#if (OSCCAL_HAVE_XTAL == 0)           
           calibrateOscillatorASM();   
#endif           
        }
        
        if (USB_INTR_PENDING & (1<<USB_INTR_PENDING_BIT)) {
          USB_INTR_VECTOR();  // clears INT_PENDING (See se0: in asmcommon.inc)
          idlePolls.b[1]=0; // reset idle polls when we get usb traffic
         break;
        }
        
      } while(--fastctr);     
 
      wdr();
 
      // commands are only evaluated after next USB transmission or after 5 ms passed
      if (command==cmd_erase_application) 
        eraseApplication();
      if (command==cmd_write_page) 
        writeFlashPage();          
      if (command==cmd_exit) {
        if (!fastctr) break;  // Only exit after 5 ms timeout     
      } else {
        command=cmd_local_nop;     
      }  
 
      {
      // This is usbpoll() minus reset logic and double buffering
        int8_t  len;
        len = usbRxLen - 3;
        if(len >= 0){
            usbProcessRx(usbRxBuf + 1, len); // only single buffer due to in-order processing
            usbRxLen = 0;       /* mark rx buffer as available */
        }
        if(usbTxLen & 0x10){    /* transmit system idle */
            if(usbMsgLen != USB_NO_MSG){    /* transmit data pending? */
                usbBuildTxBlock();
            }
        }
      }
      
      idlePolls.w++;

      // Try to execute program when bootloader times out      
      if (AUTO_EXIT_MS&&(idlePolls.w==(AUTO_EXIT_MS/5))) {
         if (pgm_read_byte(BOOTLOADER_ADDRESS - TINYVECTOR_RESET_OFFSET + 1)!=0xff)  break;
      }
      
      LED_MACRO( idlePolls.b[0] );   

       // Test whether another interrupt occurred during the processing of USBpoll and commands.
       // If yes, we missed a data packet on the bus. Wait until the bus was idle for 10µs to 
       // allow synchronising to the next incoming packet.
       
       if (USB_INTR_PENDING & (1<<USB_INTR_PENDING_BIT))  // Usbpoll() collided with data packet
       {        
          uint8_t ctr;
         
          // loop takes 5 cycles
          asm volatile(      
          "         ldi  %0,%1 \n\t"        
          "loop%=:  sbic %2,%3  \n\t"        
          "         ldi  %0,%1  \n\t"
          "         subi %0,1   \n\t"        
          "         brne loop%= \n\t"   
          : "=&d" (ctr)
          :  "M" ((uint8_t)(10.0f*(F_CPU/1.0e6f)/5.0f+0.5)), "I" (_SFR_IO_ADDR(USBIN)), "M" (USB_CFG_DPLUS_BIT)
          );       
         USB_INTR_PENDING = 1<<USB_INTR_PENDING_BIT;                   
       }                        
    } while(1);  

    LED_EXIT();
  }
   
  leaveBootloader();
}
/* ------------------------------------------------------------------------ */