/*
 * ----------------------------------------------------------------------------
 * "THE BEER-WARE LICENSE" (Revision 42):
 * <joerg@FreeBSD.ORG> wrote this file.  As long as you retain this notice you
 * can do whatever you want with this stuff. If we meet some day, and you think
 * this stuff is worth it, you can buy me a beer in return.        Joerg Wunsch
 * ----------------------------------------------------------------------------
 *
 * More advanced AVR demonstration.  Controls a LED attached to OCR1A.
 * The brightness of the LED is controlled with the PWM.  A number of
 * methods are implemented to control that PWM.
 */

#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>

#include <avr/eeprom.h>
#include <avr/interrupt.h>
#include <avr/io.h>
#include <avr/pgmspace.h>
#include <avr/sleep.h>
#include <avr/wdt.h>
#include <avr/power.h>
#include <util/atomic.h>

/* Part 1: Macro definitions */

#define CONTROL_PORT PORTD
#define CONTROL_DDR  DDRD
#define CONTROL_PIN  PIND

#if defined(__AVR_ATtiny2313__)
/* no PD7 and no ADC available on ATtiny2313 */
#  define TRIGGER_DOWN PD2
#  define TRIGGER_UP   PD3
#  define FLASH        PD4
#  define CLOCKOUT     PD6
#  define PIN_TOGGLES_PORT
#else
#  define TRIGGER_DOWN PD2
#  define TRIGGER_UP   PD3
#  define TRIGGER_ADC  PD4
#  define CLOCKOUT     PD6
#  define FLASH        PD7
#endif

#if defined(__AVR_ATmega16__)
#  define PWMDDR     DDRD
#  define PWMOUT     PD5
#elif defined(__AVR_ATmega8__) || defined(__AVR_ATmega48__) \
      || defined(__AVR_ATmega88__) || defined(__AVR_ATmega168__) \
      || defined(__AVR_ATmega328P__)
#  define PWMDDR     DDRB
#  define PWMOUT     PB1
#elif defined(__AVR_ATtiny2313__)
#  define PWMDDR     DDRB
#  define PWMOUT     PB3
#  define HAVE_ADC   0
#  define USART_RXC_vect USART_RX_vect
#  define MCUCSR     MCUSR
#else
#  error "Unsupported MCU type"
#endif

#if defined(__AVR_ATmega48__) || defined(__AVR_ATmega88__) \
    || defined(__AVR_ATmega168__)|| defined(__AVR_ATmega328P__)
/* map ATmega8/16 names to ATmegaX8 names */
#  define USART_RXC_vect USART_RX_vect
#  define UDR     UDR0
#  define UCSRA   UCSR0A
#  define UCSRB   UCSR0B
#  define FE      FE0
#  define TXEN    TXEN0
#  define RXEN    RXEN0
#  define RXCIE   RXCIE0
#  define UDRE    UDRE0
#  define U2X     U2X0
#  define UBRRL   UBRR0L

#  define TIMSK   TIMSK1
#  define MCUCSR  MCUSR
#  define PIN_TOGGLES_PORT
#endif

#if !defined(HAVE_ADC)
#  define HAVE_ADC 1
#endif

#define F_CPU 1000000UL /* CPU clock in Hertz */

#define SOFTCLOCK_FREQ 100      /* internal software clock */

/*
 * Timeout to wait after last PWM change till updating the EEPROM.
 * Measured in internal clock ticks (approx. 100 Hz).
 */
#define EE_UPDATE_TIME (3 * SOFTCLOCK_FREQ) /* ca. 3 seconds */

/*
 * Timer1 overflow interrupt will be called with F_CPU / 2048
 * frequency.  This interrupt routine further divides that value,
 * resulting in an internal update interval of approx. 10 ms.
 * (The complicated looking scaling by 10 / addition of 9 is
 * poor man's fixed-point rounding algorithm...)
 */
#define TMR1_SCALE ((F_CPU * 10) / (2048UL * SOFTCLOCK_FREQ) + 9) / 10

/* Part 2: Variable definitions */

/*
 * Bits that are set inside interrupt routines, and watched outside in
 * the program's main loop.
 */
volatile struct
{
  uint8_t tmr_int: 1;
  uint8_t adc_int: 1;
  uint8_t rx_int: 1;
}
intflags;

/*
 * Last character read from the UART.
 */
volatile char rxbuff;

/*
 * Last value read from ADC.
 */
volatile uint16_t adcval;

/*
 * Where to store the PWM value in EEPROM.  This is used in order
 * to remember the value across a RESET or power cycle.
 */
uint16_t ee_pwm EEMEM = 42;

/*
 * Current value of the PWM.
 */
int16_t pwm;

/*
 * EEPROM backup timer.  Bumped by the PWM update routine.  If it
 * expires, the current PWM value will be written to EEPROM.
 */
int16_t pwm_backup_tmr;

/*
 * Mirror of the MCUCSR register, taken early during startup.
 */
uint8_t mcucsr __attribute__((section(".noinit")));

/* Part 3: Interrupt service routines */

ISR(TIMER1_OVF_vect)
{
  static uint8_t scaler = TMR1_SCALE;

  if (--scaler == 0)
    {
      scaler = TMR1_SCALE;
      intflags.tmr_int = 1;
    }
}

#if HAVE_ADC
/*
 * ADC conversion complete.  Fetch the 10-bit value, and feed the
 * PWM with it.
 */
ISR(ADC_vect)
{
  adcval = ADCW;
  ADCSRA &= ~_BV(ADIE);         /* disable ADC interrupt */
  intflags.adc_int = 1;
}
#endif /* HAVE_ADC */

/*
 * UART receive interrupt.  Fetch the character received and buffer
 * it, unless there was a framing error.  Note that the main loop
 * checks the received character only once per 10 ms.
 */
ISR(USART_RXC_vect)
{
  uint8_t c;

  c = UDR;
  if (bit_is_clear(UCSRA, FE))
    {
      rxbuff = c;
      intflags.rx_int = 1;
    }
}

/* Part 4: Auxiliary functions */

/*
 * Read out and reset MCUCSR early during startup.
 * This function is run by the startup code and
 * never called explicitly.
 */
__attribute__((used, unused, naked, section(".init3")))
static void handle_mcucsr(void)
{
  mcucsr = MCUCSR;
  MCUCSR = 0;
}

/*
 * Do all the startup-time peripheral initializations.
 */
static void
ioinit(void)
{
#if defined(__AVR_ATmega328P__) // Arduino Nano
  // pre-scale clock from 16 MHz to 1 MHz
  clock_prescale_set(clock_div_16);
#endif

  /*
   * Set up the 16-bit timer 1.
   *
   * Timer 1 will be set up as a 10-bit phase-correct PWM (WGM10 and
   * WGM11 bits), with OC1A used as PWM output.  OC1A will be set when
   * up-counting, and cleared when down-counting (COM1A1|COM1A0), this
   * matches the behaviour needed by the STK500's low-active LEDs.
   * The timer will runn on full MCU clock (1 MHz, CS10 in TCCR1B).
   */
  TCCR1A = _BV(WGM10) | _BV(WGM11) | _BV(COM1A1) | _BV(COM1A0);
  TCCR1B = _BV(CS10);

  OCR1A = 0;                    /* set PWM value to 0 */

  /* enable pull-ups for pushbuttons */
#if HAVE_ADC
  CONTROL_PORT = _BV(TRIGGER_DOWN) | _BV(TRIGGER_UP) | _BV(TRIGGER_ADC);
#else
  CONTROL_PORT = _BV(TRIGGER_DOWN) | _BV(TRIGGER_UP);
#endif

  /*
   * Enable Port D outputs: PD6 for the clock output, PD7 for the LED
   * flasher.  PD1 is UART TxD but not DDRD setting is provided for
   * that, as enabling the UART transmitter will automatically turn
   * this pin into an output.
   */
  CONTROL_DDR = _BV(CLOCKOUT) | _BV(FLASH);

  /*
   * As the location of OC1A differs between supported MCU types, we
   * enable that output separately here.  Note that the DDRx register
   * *might* be the same as CONTROL_DDR above, so make sure to not
   * clobber it.
   */
  PWMDDR |= _BV(PWMOUT);

  UCSRA = _BV(U2X);             /* improves baud rate error @ F_CPU = 1 MHz */
  UCSRB = _BV(TXEN)|_BV(RXEN)|_BV(RXCIE); /* tx/rx enable, rx complete intr */
  UBRRL = (F_CPU / (8 * 9600UL)) - 1;  /* 9600 Bd */

#if HAVE_ADC
  /*
   * enable ADC, select ADC clock = F_CPU / 8 (i.e. 125 kHz)
   */
  ADCSRA = _BV(ADEN) | _BV(ADPS1) | _BV(ADPS0);
#endif
#if defined(__AVR_ATmega328P__)
  /* Arduino Nano uses AVcc as reference rather than AREF input */
  ADMUX = _BV(REFS0);
#endif


  TIMSK = _BV(TOIE1);
  sei();                        /* enable interrupts */

  /*
   * Enable the watchdog with the largest prescaler.  Will cause a
   * watchdog reset after approximately 2 s @ Vcc = 5 V
   */
  wdt_enable(WDTO_2S);

  /*
   * Read the value from EEPROM.  If it is not 0xffff (erased cells),
   * use it as the starting value for the PWM.
   */
  uint16_t pwm_from_eeprom = eeprom_read_word(&ee_pwm);
  if (pwm_from_eeprom != 0xffff)
    OCR1A = (pwm = pwm_from_eeprom);
}

/*
 * Some simple UART IO functions.
 */

/*
 * Send character c down the UART Tx, wait until Tx holding register is empty.
 * The prototype is such that it can be used in FDEV_SETUP_STREAM.
 */
static int
uart_putchar(char c, FILE *stream)
{
  (void) stream;
  if (c == '\n')
    uart_putchar('\r', NULL);
  loop_until_bit_is_set(UCSRA, UDRE);
  UDR = c;
  return 0;
}

/*
 * A stream writing its characters to the UART.
 */
static FILE uartout = FDEV_SETUP_STREAM(uart_putchar, NULL, _FDEV_SETUP_WRITE);


/*
 * Send a C (NUL-terminated) string down the UART Tx.
 */
#define printstr(S) fputs(S, &uartout)


/*
 * Same as above, but the string is located in program memory,
 * so "lpm" instructions are needed to fetch it.
 */
#define printstr_p(S) fputs_P(PSTR(S), &uartout)


/*
 * Update the PWM value.  If it has changed, send the new value down
 * the serial line.
 */
static void
set_pwm(int16_t new)
{
  if (new < 0)
    new = 0;
  else if (new > 1000)
    new = 1000;

  if (new != pwm)
    {
      char s[8];

      OCR1A = (pwm = new);
      /*
       * Calculate a "percentage".  We just divide by 10, as we
       * limited the max value of the PWM to 1000 above.
       */
      new /= 10;
      itoa(new, s, 10);
      printstr(s);
      printstr_p(" ");

      pwm_backup_tmr = EE_UPDATE_TIME;
    }
}

/* Part 5: main() */

int
main(void)
{
  /*
   * Our modus of operation.  MODE_UPDOWN means we watch out for
   * either PD2 or PD3 being low, and increase or decrease the
   * PWM value accordingly.  This is the default.
   * MODE_ADC means the PWM value follows the value of ADC0 (PA0).
   * This is enabled by applying low level to PC1.
   * MODE_SERIAL means we get commands via the UART.  This is
   * enabled by sending a valid V.24 character at 9600 Bd to the
   * UART.
   */
  enum
  {
    MODE_UPDOWN,
    MODE_ADC,
    MODE_SERIAL
  } mode = MODE_UPDOWN;
  uint8_t flash = 0;

  ioinit();

  if ((mcucsr & _BV(WDRF)) == _BV(WDRF))
    printstr_p("\nOoops, the watchdog bit me!");

  printstr_p("\nHello, this is the avr-gcc/libc demo running on an ");
  /* The MCU_NAME is a macro defined in the Makefile as:
     MCU_TARGET = atmega328p
     DEFS       = -DMCU_NAME='"$(MCU_TARGET)"'
  */
  printstr_p(MCU_NAME);
  printstr_p (" ");

  for (;;)
    {
      wdt_reset();

      if (intflags.tmr_int)
        {
          /*
           * Our periodic 10 ms interrupt happened.  See what we can
           * do about it.  As .tmr_int is just one bit in intflags, the
           * following is a read-modify-write operation, and hence it must
           * be made atomic.  The same applies to similar accesses below.
           */
          ATOMIC_BLOCK (ATOMIC_FORCEON)
            {
              intflags.tmr_int = 0;
            }

#ifdef PIN_TOGGLES_PORT
          /* Toggling a PORT pin on ATmega328 / ATtiny2313 can be achieved
           * by writing a '1' to the respective bit in the PIN register.
           * See "I/O-Ports -> Toggling the Pin" in the device manual.
           * A single bit can be toggled with a SBI instruction.
           */
          CONTROL_PIN |= _BV(CLOCKOUT);
#else
          /*
           * Toggle PD6, just to show the internal clock; should
           * yield ~ 48 Hz on PD6.  Since the operation itself is not
           * atomic, we wrap it it an atomic block so that it still
           * works when some ISR changes a different bit of the port.
           */
          ATOMIC_BLOCK (ATOMIC_FORCEON)
            {
              CONTROL_PORT ^= _BV(CLOCKOUT);
            }
#endif /* PIN_TOGGLES_PORT */

          /*
           * Flash LED on PD7, approximately once per second
           */
          flash++;
          if (flash == 5)
            CONTROL_PORT |= _BV(FLASH);
          else if (flash == 100)
            {
              flash = 0;
              CONTROL_PORT &= ~_BV(FLASH);
            }

          switch (mode)
            {
            case MODE_SERIAL:
              /*
               * In serial mode, there's nothing to do anymore here.
               */
              break;

            case MODE_UPDOWN:
              /*
               * Query the pushbuttons.
               *
               * NB: watch out to use PINx for reading, as opposed
               * to using PORTx which would be the mirror of the
               * _output_ latch register (resp. pullup configuration
               * bit for input pins)!
               */
              if (bit_is_clear(PIND, TRIGGER_DOWN))
                set_pwm(pwm - 10);
              else if (bit_is_clear(PIND, TRIGGER_UP))
                set_pwm(pwm + 10);
#if HAVE_ADC
              else if (bit_is_clear(PIND, TRIGGER_ADC))
                mode = MODE_ADC;
#endif
              break;

            case MODE_ADC:
#if HAVE_ADC
              if (bit_is_set(PIND, TRIGGER_ADC))
                mode = MODE_UPDOWN;
              else
                {
                  /*
                   * Start one conversion.
                   */
                  ADCSRA |= _BV(ADIE);
                  ADCSRA |= _BV(ADSC);
                }
#endif /* HAVE_ADC */
              break;
            }

          if (pwm_backup_tmr && --pwm_backup_tmr == 0)
            {
              /*
               * The EEPROM backup timer expired.  Save the current
               * PWM value in EEPROM.  Note that this function might
               * block for a few milliseconds (after writing the
               * first byte).
               */
              eeprom_write_word(&ee_pwm, pwm);
              printstr_p("[EEPROM updated] ");
            }
        }

#if HAVE_ADC
      if (intflags.adc_int)
        {
          ATOMIC_BLOCK (ATOMIC_FORCEON)
            {
              intflags.adc_int = 0;
            }
          set_pwm(adcval);
        }
#endif /* HAVE_ADC */

      if (intflags.rx_int)
        {
          ATOMIC_BLOCK (ATOMIC_FORCEON)
            {
              intflags.rx_int = 0;
            }

          if (rxbuff == 'q')
            {
              printstr_p("\nThank you for using serial mode."
                         "  Good-bye!\n");
              mode = MODE_UPDOWN;
            }
          else
            {
              if (mode != MODE_SERIAL)
                {
                  printstr_p("\nWelcome at serial control, "
                             "type +/- to adjust, or 0/1 to turn on/off\n"
                             "the LED, q to quit serial mode, "
                             "r to demonstrate a watchdog reset\n");
                  mode = MODE_SERIAL;
                }
              switch (rxbuff)
                {
                case '+':
                  set_pwm(pwm + 10);
                  break;

                case '-':
                  set_pwm(pwm - 10);
                  break;

                case '0':
                  set_pwm(0);
                  break;

                case '1':
                  set_pwm(1000);
                  break;

                case 'r':
                  printstr_p("\nzzzz... zzz...");
                  for (;;)
                    ;
                }
            }
        }
      sleep_mode();
    }
}
