// EveryTimerB library.
// by Kees van der Oord Kees.van.der.Oord@inter.nl.net

// Timer library for the TCB timer of the AtMega4809 processor.
// tested on the Arduino Nano Every (AtMega4809) and the Arduino 1.8.12 IDE
// support for the Every is the 'Arduino MegaAVR' boards module (Tools | Board | Boards Manager)

// usage:
/*
#ifdef ARDUINO_ARCH_MEGAAVR
#include "EveryTimerB.h"
#define Timer1 TimerB2    // use TimerB2 as a drop in replacement for Timer1
#else // assume architecture supported by TimerOne library ....
#include "TimerOne.h"
#endif

// code below will now work both on the MegaAVR and AVR processors

void setup() {
  Timer1.initialize();
  Timer1.attachInterrupt(myisr);
  Timer1.setPeriod(1000000UL);     // like the TimerOne library this will start the timer as well
}

void myisr() {
  // do something useful every second
}
*/
// clock source options:
// The TCB clock source is specified in the initialize() function with default value EveryTimerB_CLOCMODE.
// define this macro before including this file to use a different default clock mode
// e.g.:
// #define EveryTimerB_CLOCMODE TCB_CLKSEL_CLKTCA_gc  // 250 kHz ~ 4 us
// #define EveryTimerB_CLOCMODE TCB_CLKSEL_CLKDIV2_gc //   8 MHz ~ 0.125 us
// #define EveryTimerB_CLOCMODE TCB_CLKSEL_CLKDIV_gc  //  16 MHz ~ 0.0625 us

// timer options
// The 4809 has one A timer (TCA) and four B timers (TCB).
// TCA and TCB3 are used by the arduino core to generate the clock used by millis() and micros().
// TCB0 generates the PWM timing for pin D6, TCB1 for pin D3.
// By default Timer Control B2 is defined as TimerB2 in the EveryTimerB library.
// If you would like to use the TCB0 and TCB1 as well you have to copy the code
// from the EveryTimerB.cpp into your product file and adapt for B0 and B1 timers.
//
// for information on the 4809 TCA and TCB timers:
// http://ww1.microchip.com/downloads/en/AppNotes/TB3217-Getting-Started-with-TCA-90003217A.pdf
// http://ww1.microchip.com/downloads/en/Appnotes/TB3214-Getting-Started-with-TCB-90003214A.pdf
// %LOCALAPPDATA%\Arduino15\packages\arduino\hardware\megaavr\1.8.5\cores\arduino\wiring.c
// %LOCALAPPDATA%\Arduino15\packages\arduino\hardware\megaavr\1.8.5\variants\nona4809\variant.c
// %LOCALAPPDATA%\Arduino15\packages\arduino\tools\avr-gcc\7.3.0-atmel3.6.1-arduino5\avr\include\avr\iom4809.h

// 20 MHz system clock
// to run the Every at 20 MHz, add the lines below to the nona4809 section of the boards.txt file
// in %LOCALAPPDATA%\Arduino15\packages\arduino\hardware\megaavr\1.8.5.
// they add the sub menu 'Tools | Clock' to choose between 16MHz and 20MHz.
/*
menu.clock=Clock
nona4809.menu.clock.16internal=16MHz
nona4809.menu.clock.16internal.build.f_cpu=16000000L
nona4809.menu.clock.16internal.bootloader.OSCCFG=0x01
nona4809.menu.clock.20internal=20MHz
nona4809.menu.clock.20internal.build.f_cpu=20000000L
nona4809.menu.clock.20internal.bootloader.OSCCFG=0x02
*/
// On 20Mhz, the 1.8.12 IDE MegaAvr core library implementation
// of the millis() and micros() functions is not accurate.
// the file "MegaAvr20MHz.h" implements a quick hack to correct for this
//
// to do:
// there is no range check on the 'period' arguments of setPeriod ...
// check if it is necessary to set the CNT register to 0 in start()

#ifndef EveryTimerB_h_
#define EveryTimerB_h_
#ifdef ARDUINO_ARCH_MEGAAVR

#ifndef EveryTimerB_CLOCMODE
#define EveryTimerB_CLOCMODE TCB_CLKSEL_CLKTCA_gc
#endif

#if defined(ARDUINO) && ARDUINO >= 100
#include "Arduino.h"
#else
#include "WProgram.h"
#endif
#include "MegaAvr20MHz.h"
#include "pins_arduino.h"

#define TCB_RESOLUTION 65536UL // TCB is 16 bit
// CLOCK   F_CPU  DIV  TICK      OVERFLOW  OVERFLOW/s
// CLKTCA  16MHz  64   4000  ns  262144us    3.8 Hz
// CLKDIV2 16MHz   2    125  ns    8192us  122 Hz
// CLKDIV1 16MHz   1     62.5ns    4096us  244 Hz
// CLKTCA  20MHz  64   3200  ns  209716us    4.8 Hz
// CLKDIV2 20MHz   2    100  ns    6554us  153 Hz
// CLKDIV1 20MHz   1     50  ns    3277us  305 Hz

class EveryTimerB
{
public:
  // The AtMega Timer Control B clock sources selection:
  // TCB_CLKSEL_CLKTCA_gc,  // Timer Controller A, Arduino framework sets TCA to F_CPU/64 = 250kHz (4us) @ 16MHz or 312.5kHz (3.2us) @ 20MHz
  // TCB_CLKSEL_CLKDIV2_gc, // CLK_PER/2 Peripheral Clock / 2: 8MHz @ 16Mhz or 10MHz @ 20MHz
  // TCB_CLKSEL_CLKDIV1_gc  // CLK_PER Peripheral Clock: 16MHz @ 16Mhz or 20MHz @ 20MHz

  // intialize: sets the timer compare mode and the clock source
  void initialize(TCB_t *timer_ = &TCB2, TCB_CLKSEL_t clockSource = EveryTimerB_CLOCMODE, unsigned long period = 1000000UL) __attribute__((always_inline))
  {
    timer = timer_;
#if defined(MegaAvr20MHzCorrected)
    corrected20MHzInit(); // see commment in MegaAvr20MHz_h
#endif
    stop();
    timer->CTRLB = TCB_CNTMODE_INT_gc & ~TCB_CCMPEN_bm; // timer compare mode with output disabled
    if (clockSource)
      setClockSource(clockSource);
    if (period)
      setPeriod(period);
  }

  void setClockSource(TCB_CLKSEL_t clockSource) __attribute__((always_inline))
  {
    timer->CTRLA = clockSource; // this stops the clock as well ...
    switch (clockSource)
    {
#if F_CPU == 20000000UL
    case TCB_CLKSEL_CLKTCA_gc:
      maxTimeWithoutOverflow = 209715;
      break; // (TCB_RESOLUTION * 64) / 20
    case TCB_CLKSEL_CLKDIV2_gc:
      maxTimeWithoutOverflow = 6553;
      break; // (TCB_RESOLUTION *  2) / 20
    case TCB_CLKSEL_CLKDIV1_gc:
      maxTimeWithoutOverflow = 3276;
      break; // (TCB_RESOLUTION *  1) / 20
#else
    case TCB_CLKSEL_CLKTCA_gc:
      maxTimeWithoutOverflow = 262144;
      break;
    case TCB_CLKSEL_CLKDIV2_gc:
      maxTimeWithoutOverflow = 8192;
      break;
    case TCB_CLKSEL_CLKDIV1_gc:
      maxTimeWithoutOverflow = 4096;
      break;
#endif
    }
  }

  TCB_CLKSEL_t getClockSource()
  {
    return (TCB_CLKSEL_t)(timer->CTRLA & (TCB_CLKSEL_CLKTCA_gc | TCB_CLKSEL_CLKDIV2_gc | TCB_CLKSEL_CLKDIV1_gc));
  }

  double getFrequencyOfClock(TCB_CLKSEL_t clock)
  {
    switch (clock)
    {
    // suppose nobody touched the default TCA configuration ...
    case TCB_CLKSEL_CLKTCA_gc:
      return double(F_CPU / 64);
      break;
    case TCB_CLKSEL_CLKDIV2_gc:
      return double(F_CPU / 2);
      break;
    case TCB_CLKSEL_CLKDIV1_gc:
      return double(F_CPU);
      break;
    }
    return 0.0;
  }

  double getClockFrequency()
  {
    return getFrequencyOfClock(getClockSource());
  }

  // setPeriod: sets the period
  // note: max and min values are different for each clock
  // CLKTCA: conversion from us to ticks multiplies 'period' first with 10, so max value is MAX_ULONG/10 ~ 1 hr 11 minutes 34 seconds
  // CLKDIV2: conversion from us to ticks is a *10 multiplication, so max value is 420M us (~ 7 minutes)
  // CLKDIV1: conversion from us to ticks is a *20 multiplication, so max value is 210M us (~ 3.5 minutes)
  void setPeriod(unsigned long period /* us */) __attribute__((always_inline))
  {
    timer->CTRLA &= ~TCB_ENABLE_bm;
    // conversion from us to ticks depends on the clock
    switch (timer->CTRLA & TCB_CLKSEL_gm)
    {
    case TCB_CLKSEL_CLKTCA_gc:
#if F_CPU == 20000000UL
      period = (period * 10) / 32; // 20Mhz / 64x clock divider of TCA => 3.2 us / tick
#else                              // 16000000UL
      period /= 4; // 16MHz / 64x clock divider of TCA => 4 us / tock
#endif
      break;
    case TCB_CLKSEL_CLKDIV2_gc:
#if F_CPU == 20000000UL
      period *= 10; // 20MHz / 2x clock divider => 10 ticks / us
#else               // 16000000UL
      period *= 8; // 16MHz / 2x clock divider => 8 ticks / us
#endif
      break;
    case TCB_CLKSEL_CLKDIV1_gc:
#if F_CPU == 20000000UL
      period *= 20; // 20MHz: 20 ticks / us
#else               // 16000000UL
      period *= 16; // 16MHz: 16 ticks / u3
#endif
      break;
    }

    // to support longer than TCB_RESOLUTION ticks,
    // this class supports first waiting for N 'overflowCounts'
    // and next program the timer the remaining 'remainder' ticks:
    countsPerOverflow = TCB_RESOLUTION;
    overflowCounts = period / TCB_RESOLUTION;
    remainder = period % TCB_RESOLUTION;

    // the timer period is always one tick longer than programmed,
    // so a remainder of 1 is not possible. reduce the length of
    // the 'overflow' cycles to get a remainder that is not 1
    if (overflowCounts)
    {
      while (remainder == 1)
      {
        --countsPerOverflow;
        overflowCounts = period / countsPerOverflow;
        remainder = period % countsPerOverflow;
      }
    }

    // the timer period is always one tick longer than programmed
    --countsPerOverflow;
    if (remainder)
      --remainder;

    // let's go
    start();
  }

  void start() __attribute__((always_inline))
  {
    stop();
    overflowCounter = overflowCounts;
    timer->CCMP = overflowCounts ? countsPerOverflow : remainder;
    timer->CNT = 0;
    timer->CTRLA |= TCB_ENABLE_bm;
  }

  void stop() __attribute__((always_inline))
  {
    timer->CTRLA &= ~TCB_ENABLE_bm;
    timer->INTFLAGS = TCB_CAPT_bm; // writing to the INTFLAGS register will clear the interrupt request flag
  }

  bool isEnabled(void) __attribute__((always_inline))
  {
    return timer->CTRLA & TCB_ENABLE_bm ? true : false;
  }

  void enable(void) __attribute__((always_inline))
  {
    timer->CTRLA |= TCB_ENABLE_bm;
  }

  bool disable(void) __attribute__((always_inline))
  {
    timer->CTRLA &= ~TCB_ENABLE_bm;
  }

  void attachInterrupt(void (*isr)()) __attribute__((always_inline))
  {
    isrCallback = isr;
    timer->INTFLAGS = TCB_CAPT_bm; // clear interrupt request flag
    timer->INTCTRL = TCB_CAPT_bm;  // Enable the interrupt
  }

  void attachInterrupt(void (*isr)(), unsigned long microseconds) __attribute__((always_inline))
  {
    if (microseconds > 0)
      stop();
    attachInterrupt(isr);
    if (microseconds > 0)
      setPeriod(microseconds);
  }

  void detachInterrupt() __attribute__((always_inline))
  {
    timer->INTCTRL &= ~TCB_CAPT_bm; // Disable the interrupt
    isrCallback = isrDefaultUnused;
  }

  void enableInterrupt() __attribute__((always_inline))
  {
    timer->INTFLAGS = TCB_CAPT_bm; // clear interrupt request flag
    timer->INTCTRL = TCB_CAPT_bm;  // Enable the interrupt
  }

  void disableInterrupt() __attribute__((always_inline))
  {
    timer->INTCTRL &= ~TCB_CAPT_bm; // Enable the interrupt
  }

  TCB_CNTMODE_enum getMode() __attribute__((always_inline))
  {
    return (TCB_CNTMODE_enum)(timer->CTRLB & 0x7);
  }

  void setMode(TCB_CNTMODE_enum mode) __attribute__((always_inline))
  {
    timer->CTRLB = (timer->CTRLB & ~0x7) | mode;
  }

  uint8_t isOutputEnabled() __attribute__((always_inline))
  {
    return timer->CTRLB & TCB_CCMPEN_bm;
  }

  uint8_t enableOutput() __attribute__((always_inline))
  {
    timer->CTRLB |= TCB_CCMPEN_bm;
  }

  uint8_t disableOutput() __attribute__((always_inline))
  {
    timer->CTRLB &= ~TCB_CCMPEN_bm;
  }

  // this will start PWM on pin 6 (TCB0) or pin 3 (TCB1)
  // set the pins to output with setMode(x,OUTPUT) before calling this function
  // period determines the clock ticks in one cycle:
  //  16MHz clock: slowest frequency at 255 = 62 kHz.
  //   8MHz clock: slowest frequency at 255 = 31 kHz.
  // 256kHz clock: slowest frequency at 255 =  1 kHz.
  // compare determines the duty cycle.
  // with a period of 255, set the compare to 128 to get 50% duty cycle.
  void setPwmMode(byte period, byte compare)
  {
    disableInterrupt();
    setMode(TCB_CNTMODE_PWM8_gc);
    timer->CCMPL = period;
    timer->CCMPH = compare;
    enableOutput();
    enable();
  }

  void getPwmMode(byte &period, byte &compare)
  {
    period = timer->CCMPL;
    compare = timer->CCMPH;
  }

  void setPwm(double frequency, double dutyCycle)
  {
    TCB_CLKSEL_t clockSource = TCB_CLKSEL_CLKDIV1_gc;
    double clockFrequency = getFrequencyOfClock(clockSource);
    if (frequency < (clockFrequency / 256.))
    {
      clockSource = TCB_CLKSEL_CLKDIV2_gc;
      clockFrequency = getFrequencyOfClock(clockSource);
    }
    if (frequency < (clockFrequency / 256.))
    {
      clockSource = TCB_CLKSEL_CLKTCA_gc;
      clockFrequency = getFrequencyOfClock(clockSource);
    }
    double period = (clockFrequency / frequency) - 1.0 + 0.5;
    if (period > 255.)
      period = 255.;
    if (period < 0.)
      period = 0.0;
    double compare = period * dutyCycle + 0.5;
    if (compare < 0.0)
      compare = 0.0;
    if (compare > period)
      compare = period;
    setPwmMode((byte)(period), (byte)(compare));
  }

  void getPwm(double &frequency, double &dutyCycle)
  {
    byte period, compare;
    getPwmMode(period, compare);
    frequency = getClockFrequency() / (((double)period) + 1);
    dutyCycle = (double)compare / (((double)period) + 1);
  }

  void setTimerMode()
  {
    disable();
    disableOutput();
    setMode(TCB_CNTMODE_INT_gc);
    if (isrCallback != isrDefaultUnused)
    {
      enableInterrupt();
    }
  }

  TCB_t *getTimer() { return timer; }
  long getOverflowCounts() { return overflowCounts; }
  long getRemainder() { return remainder; }
  long getOverflowCounter() { return overflowCounter; }
  long getOverflowTime() { return maxTimeWithoutOverflow; }

  // protected:
  //  the next_tick function is called by the interrupt service routine TCB0_INT_vect
  // friend extern "C" void TCB0_INT_vect(void);
  void next_tick() __attribute__((always_inline))
  {
    --overflowCounter;
    if (overflowCounter > 0)
    {
      return;
    }
    if (overflowCounter < 0)
    {
      // finished waiting for remainder
      if (overflowCounts)
      {
        // restart with a max counter
        overflowCounter = overflowCounts;
        timer->CCMP = countsPerOverflow;
      }
    }
    else
    {
      // overflowCounter == 0
      // the overflow series has finished: to the remainder if any
      if (remainder)
      {
        timer->CCMP = remainder;
        if (timer->CNT < remainder)
          return;
        // remainder is so short: already passed !
        timer->CCMP = countsPerOverflow;
      }
      // no remainder series: reset the overflow counter and do the callback
      overflowCounter = overflowCounts;
    }
    (*isrCallback)();
  }

private:
  TCB_t *timer = &TCB0;
  long overflowCounts = 0;
  long remainder = 10;
  long overflowCounter = 0;
  unsigned long countsPerOverflow = TCB_RESOLUTION - 1;
  void (*isrCallback)();
  static void isrDefaultUnused();
  unsigned long maxTimeWithoutOverflow;

}; // EveryTimerB

extern EveryTimerB TimerB2;

#endif // ARDUINO_ARCH_MEGAAVR
#endif // EveryTimerB_h_
