5. Code Explained

This section is more of the tutorial-like one. If you are not skillful with C-programming and microcontrollers, it might hopefully make things a bot more clear. If yes... well, maybe you can correct me? By all means, do that!

The project is not complex. All can be written using just one file - main.c. However, I will split it into sections in order to make things easier to explain.

Initialisation

The first step is to set up all the necessary registers of our microcontrollers. Once again, the explanation of each and every register is done in the datasheet. However, since it is often preferred to start up with an example, let us see exactly how the code was implemented.

First, let us place some global definitions. Here, the <asf.h> is the only library that we actually need. At least life it is much easier when having it, since then we can use the registers' names. In other words, whenever we type for example TCNT2 the compiler with immiediately recognise the register's name. Late on, we assign the pins some specific names. It is a good practice. If we decide to change the function a pin, it is enough to edit this section. This way we can bring hardware into the software, which will make the code much easier to maintain.

#include   <asf.h>
#define  UP      PB0
#define  DOWN    PB3
#define  COLOR   PB4
#define  MODE    PB5
#define  BATTERY PD0

/* Three modes of operation */
enum OperationMode{
AUTO_MODE,
MANUAL_MODE,
BATTERY_MODE,
};

uint8_t Mode = 0x00; // contains the Operation Mode setting
/* Four output channels */
enum ColorSelections{
RED, GREEN, BLUE, LAMP
};

uint8_t ColorSel = 0x00; // contains the selected channel

struct time{
uint8_t hour;
uint8_t minute;
uint8_t second;
} daytime; // stores the time (hour, minute, second)

uint8_t RGB_lvl_auto[4]; // channels' levels in auto mode
uint8_t RGB_lvl_manual[4]; // channels levels in the manual mode

Next, we define the an array containig the precomputed values needed to feed the R, G, B channels. I agree that it might not be the most exact way to compute the right colour values as it does limit the colour resolution. On the positive side, however, it is a simple and efficient approach that is easy to implement, debug and it does consume the minimum resources from the microcontroller's side. As the table contains 3 times 256 values, let me skip it here. I will refer to it later as uint8_t color_temperature[256][3];. Let us move to the functions' prototypes. In principle, we need to ensure three things:

void PortInit(void){
DDRB = 0x06; // [tosc2, tosc1, Mode, Color, down, LAMP, RED, up]
DDRD |= 0x60; // [ --- , GREEN, BLUE, ---- , ----, ----, ---, --]
}

The choice of pins was not accidential. However, it enough at this point that you accept that the pins, whoose associated bit in the direction register DDRx is 0 will act as inputs and setting 1 will make them outputs. TOSC1 and TOSC2 are the two specific pins that are used to connect the external crystal oscillator. Obviously, they need to act as inputs.

Reading the keys with interrupts is much more efficient and at the same time simpler. ATmega88 allows to trigger a pin configured interrupt, whenever a pin changes its state.

void InterruptInit(void){
/* Configure PCINT[5,4,3,0] for reading the keys. */
PCICR |= (1 << PCIE0);
// pins 0-7 allowed to generate interrupts in general
// pins PB: 5,4,3,0 can generate interrupts in our program
PCMSK0 |= (1 << PCINT5)|(1 << PCINT4)|(1 << PCINT3)|(1 << PCINT0);
sei(); //global interrupt enable
}

The first line allows to generally use pin-controlled interrupts on pins from 0 to 7. The second line specifies, which are the pins. Finally, we need to allow the interrupts in general. As mentioned earlier, we need timer counters to allow us generating PWM signals. There exist three timer counters on ATmega88. Two of them are needed for the output channels. The remainig one will be used for tracking the time.

void TC2Init(void){
ASSR |= (1 << AS2); // enable asynch. mode for TC2
TCCR2B |= (1 << CS22) | (1 << CS20); // div 128, (1 Hz)
TIMSK2 |= (1 << TOIE2); // interrupt generated on overflow
TCNT2 = 0x00; // reset the register
TCCR2A = 0x00; //normal mode of operation
}

First, AS2 bit needs to be set in order to allow the TOSC1,2 pins to be used for an external crystal if the main CPU operates using the internal RC oscillator. Secondly, setting the TCCR2B as shown allows to prescale the external clock source. Having the oscillator operating on 32.768 kHz, prescalling it with 128 will make the 8-bit counter overflow exactly every second. Setting TOIE2 will trigger an interrupt when the overflow occurs. Finally, we reset the register and ensure the normal mode of operation. The next to counters will be used to generate the PWM signal on four channels (R, G, B and lamp).

void TC0Init(void){
TCCR0A |= (1 << COM0A1)|(1 << COM0A0); // inverting PWM mode for OC0A
TCCR0A |= (1 << COM0B1)|(1 << COM0B0); // inverting PWM mode for OC0B
TCCR0A |= (1 << WGM01)|(1 << WGM00); // PWM fast mode enabled
TCCR0B |= (1 << CS00); // no prescaler (f_TC0 = f_I/O / 256)
PRR &= ~(1 << PRTIM0); // enable TC0 module
}
void TC1Init(void){
TCCR1A |= (1 << COM1A1)|(1 << COM1A0); // inverting PWM mode for OC1A
TCCR1A |= (1 << COM1B1)|(1 << COM1B0); // inverting PWM mode for OC1B
TCCR1A |= (1 << WGM10); // PWM fast mode enabled (8-bit)
TCCR1B |= (1 << WGM12); // PWM fast mode enabled (8-bit)
TCCR1B |= (1 << CS10); // no prescaler (f_TC0 = f_I/O / 256)
OCR1AH = 0x00; // this part will not be used
PRR &= ~(1 << PRTIM1); // enable TC1 module
}

Here, we are using the inverted PWM mode, which means that we have a positive input after the overflow occurs, not before. If the non-inverted output was used and we wanted a channel to be off, it would take at least one clock cycle to clear the OCRx flag. That would make enough time for the microcontroller to emit a short pulse. Since LEDs respond very fast to the electrical signal, these short repeating pulses would appear as constant glowing. The inverted mode, for a change, acts in the opposite way setting the flag instead of clearing it, which solves the problem.

In addition to that, since we need 3 channels (R, G, B) to act at the same time, it really makes no sense to use the 16-bit feature offered by the timer counter 1 (TC1) OCRIAH = 0x00; ensures that the upper 8 bits are not used.

Finally, we need to ensure that the global register PRR allows the timer counter to operate.

Prototypes of the programme functions

/* Tracks the time flow (h,m,s) */
void TimeTrack(struct time *t){
  if (t->second == 60){
    t->second = 0x00;
    t->minute++;
  }
  if (t->minute == 60){
    t->minute = 0x00;
    t->hour++;
  }
  if (t->hour == 24){
    t->hour = 0x00;
  }
}

This is a simple function that helps to track the time. I chose this way, since it allows to use only 8-bit numbers to store the time.

void AutomaticModeInit(void){
  DDRB |= 0x06; // enable all light channels: power, RED
  DDRD |= 0x60; // enable all light channels: GREEN, BLUE
  PCMSK0 &= ~((1 << PCINT4)|(1 << PCINT3)|(1 << PCINT0)); // keys off
  PCMSK0 |= (1 << PCINT5); // enable keys: mode
  PRR &= ~((1 << PRTIM1)|(1 << PRTIM0)); // enable TC0 and TC1
  Mode = AUTO_MODE; // set to AUTOMATIC MODE
}
void ManualModeInit(void){
  DDRB |= 0x06; // enable all light channels: power, RED
  DDRD |= 0x60; // enable all light channels: GREEN, BLUE
  PCMSK0 |= (1 << PCINT5)|(1 << PCINT4)|(1 << PCINT3)|(1 << PCINT0);
  PRR &= ~((1 << PRTIM1)|(1 << PRTIM0)); // enable TC0 and TC1
  Mode = MANUAL_MODE; // set to MANUAL MODE
}
void BatteryModeInit(void){
  DDRB &= ~(0x06); // disable all light channels: power, RED
  DDRD &= ~(0x60); // disable all light channels: GREEN, BLUE
  DDRD &= ~(1< // disable all keys
  PCMSK0 &= ~((1<< PCINT5) \
      | (1 << PCINT4) \
      | (1 << PCINT3) \
      | (1 << PCINT0));
  PRR |= (1 << PRTIM1)|(1 << PRTIM0); // disable TC0 and TC1
  Mode = BATTERY_MODE; // set to BATTERY OPERATIED MODE
}

Function loading the colour values

void FishLights(void){
  if (Mode == AUTO_MODE)
  {
    /* Returns colour temperature index given time */
    uint8_t indexf;

    /* Calculates the time (dec) and shifts to start at 0:00 pm */
    float timedec = (float)(daytime.hour) \
       + (float)(daytime.minute)/60 \
       + (float)(daytime.second)/3600;
    uint32_t timedec2 = timedec*10000;

    /* morning */
    if ((timedec2 >= 0) & (timedec2 <= 35000))
    {
      indexf = timedec2/137;
    }
    /* day */
    if ((timedec2 > 35000) & (timedec2 < 100000))
    {
      indexf = 255;
    }
    /* evening */
    if ((timedec2 >= 100000) & (timedec2 <= 135000))
    {
      indexf = (135000 - timedec2)/137;
    }
    /* night */
    if (timedec2 > 135000)
    {
      indexf = 0;
    }

    /* Loads the channels’ values into the buffer */
    RGB_lvl_auto[RED]   = color_temperature[indexf][RED];
    RGB_lvl_auto[GREEN] = color_temperature[indexf][GREEN];
    RGB_lvl_auto[BLUE]  = color_temperature[indexf][BLUE];
    RGB_lvl_auto[LAMP]  = ~indexf;

    /* Adjucts the PWM prescalers for each pin */
    OCR1A = RGB_lvl_auto[RED];
    OCR0A = RGB_lvl_auto[GREEN];
    OCR0B = RGB_lvl_auto[BLUE];
    OCR1BL = RGB_lvl_auto[LAMP];

    /* In case power goes down */
    if((PIND & (1 << BATTERY)) == 0)
    {
      BatteryModeInit();
    }
  }
  if (Mode == MANUAL_MODE)
  {
    OCR1AL = ~RGB_lvl_manual[RED];
    OCR0A  = ~RGB_lvl_manual[GREEN];
    OCR0B  = ~RGB_lvl_manual[BLUE];
    OCR1BL = ~RGB_lvl_manual[LAMP];

    /* In case power is down */
    if((PIND & (1 << BATTERY)) == 0)
    {
      BatteryModeInit();
    }
  }
  if (Mode == BATTERY_MODE)
  {
    /* Check if the power is restored*/
    if((PIND & (1 << BATTERY)) != 0)
    {
      AutomaticModeInit();
    }
  }
}

As you can see, controlling of the channels is done by loading the correct values to the PWM prescalers OCR1AL, OCR0A, OCR0B and OCR1BL. Since we use the inverted PWM mode, we also invert the RGB_lvl_manual[...] value. In the automatic mode, the table used contains already inverted values.

The main loop in the programme

int main (void)
{
  board_init();
  PortInit();
  InterruptInit();
  TC2Init();
  TC0Init();
  TC1Init();
  AutomaticModeInit(); // starts with automatic settings by default

  while (1)
  {
    TimeTrack(&daytime); // Update on the time
    FishLights(); // Get the correct channel values based on the mode
  }
}

After all settings are initialised, the main function simply alternates between updating the time and changing the output accordingly.

Interrupts

nterrupt requests can be used for many things. Here we use them for allowing the microcontroller to respond to input signals. According to table 1 different keys are used to perform different functions.

/* Interrupt dedicated to respond to the keys */
ISR(PCINT0_vect)
{
  /* If in the AUTOMATIC MODE */
  if(Mode == AUTO_MODE)
  {
    /* If Mode key is pressed */
    if((PINB & (1 << MODE)) == 0)
    {
      ManualModeInit();
      return;
    }
  }
  /* If in the MANUAL MODE */
  if(Mode == MANUAL_MODE)
  {
    if((PINB & (1 << MODE)) == 0) // Return to the automatic mode
    {
      AutomaticModeInit();
      return;
    }
    if((PINB & (1 << COLOR)) == 0) // Select the colour to alter
    {
      ColorSel = (ColorSel + 1) % 4;
      return;
    }
    if((PINB & (1 << DOWN)) == 0) // Decrease the intensity
    {
      RGB_lvl_manual[ColorSel] -= 0x01;
    }
    return;
  }
  if((PINB & (1 << UP)) == 0) // Increase the intensity of the colour
  {
    RGB_lvl_manual[ColorSel] += 0x01;
  }
  return;
}

The PCINT0_vect is associated with the pin-controlled interrupt that is used to sense the keys' status. However, since the interrupt is triggered by the change of the state, every time we press (and release) a key the key, the interrupt will be executed twice. For this reason, the if statements check for the the zero value only. Finally, we allow the colour channels to overflow (or underflow) when increasing (or decreasing) the value. This makes it possible to reach the full intensity faster.

/* Interrupt dedicated to update the time */
{
  daytime.second++;
}

The last interrupt occurs whenever timer-counter 2 overflows. Naturally, it is used to update the daytime buffer every second.

left right