Fish Light

Code Explained

This section is more of the tutorial-like one to explain the meaning behind the code. The code is publically available on GitHub.

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.

Initialization

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 immediately recognize 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#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 settings
/* Four 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, hour)

uint8_t RGB_lvl_auto[4];
uint8_t RGB_lvl_manual[4];

Next, we define the an array containing 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 color values as it does limit the color 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:

  • We need to control, which pins should act as inputs and which as outputs.
  • We need to enable the interrupts and set the properly.
  • We will also need timers in order to generate the PWM signal.
1
2
3
4
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.

1
2
3
4
5
6
7
void InterruptInit(void){
    PCICR |= (1 << PCIE0);
    // pins 0:7 allowed to generate interrupts in general
    // pins RB[0,3,4,5] 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 remaining one will be used for tracking the time.

1
2
3
4
5
6
7
void TC2Init(void){
    ASSR |= (1 << AS2); // enable asynch mode for TC2
    TCCR2B |= (1 << CS22)|(1 << CS20); // div 128 (1Hz)
    TIMSK2 |= (1 << TOIE2); // interrupt generated on overflow
    TCNT2 = 0x00; // reset the register
    TCCR2A = 0x00; // normal model 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).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void TC0Init(void){
    TCCR0A |= (1 << COM0A1)|(1 << COM0A0); // inverting PWM for OC0A
    TCCR0A |= (1 << COM0B1)|(1 << COM0B0); // inverting PWM for OC0B
    TCCR0A |= (1 << WGM01)|(1 << WGM00); // PWM fast mode enabled
    TCCR0B |= (1 << CS00); // no prescaler (f_TCO = f_I/) /256)
    PRR &= ~(1 << PRTIM0); // enable TC0 module
}

void TC1Init(void){
    TCCR1A |= (1 << COM1A1)|(1 << COM1A0); // inverting PWM for OC1A
    TCCR1A |= (1 << COM1B1)|(1 << COM1B0); // inverting PWM for OC1B
    TCCR1A |= (1 << WGM10); // PWM fast mode enabled
    TCCR1B |= (1 << WGM12); // PWM fast mode enabled
    TCCR1B |= (1 << CS10); // no prescaler (f_TCO = 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void AutomaticModeInit(void){
    DDRB |= 0x06; // enable all channels: power, red
    DDRD |= 0x60; // enable all 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_MODEl
}

void ManualModeInit(void){
    DDRB |= 0x06; // enable all channels: power, red
    DDRD |= 0x60; // enable all channels: green, blue
    PCMSK0 |= (1 << PCINT5)|(1 << PCINT4)|(1 << PCINT3)|(1 << PCINT0);
    PRR &= ~((1 << PRTIM1)|(1 << PRTIM0)); // enable TC0 and TC1
    Mode = MANUAL_MODE;
}

void BatteryModeInit(void){
    DDRB &= ~(0x06); // disable all channels
    DDRD &= ~(0x60); // disable all channels
    DDRD &= ~(1 << battery);
    PCMSK0 &= ~((1 << PCINT5)|(1 << PCINT4)|(1 << PCINT3)|(1 << PCINT0));
    PRR |= (1 << PRTIM1)|(1 << PRTIM0); // disable TC0 and TC1
    Mode = BATTERY_MODE;
}

Function loading the color values

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
void FishLights(void){
    if (Mode == AUTO_MODE){
        // Returns the color 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 = timedex2/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;

        /* Adjusts 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 of 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 goes 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.

Main loop in the programme

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(void) {
    board_init();
    PortInit();
    InterruptInit();
    TC2Init();
    TC0Init();
    TC1Init();
    AutomaticModeInit(); // start automatic by default

    while (1) {
        TimeTrack(&dautime); // update the time
        FishLight(); // 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

Interrupt 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
ISR(PCINT0_vect) {
    if (Mode == AUTOMATIC_MODE) {
        /* If key is pressed */
        if ((PINB & (1 << MODE)) == 0) {
            ManualModeInit();
            return;
        }
    }
    if (Mode == MANUAL_MODE) {
        if ((PINB & (1 << MODE)) == 0) {
            AutomaticModeInit();
            return;
        }
        if ((PINB & (1 << COLOR)) == 0) {
            ColorSel = (ColorSel + 1) % 4;
            return;
        }
        if ((PINB & (1 << DOWN)) == 0) {
            RGB_lvl_manual[ColorSel] -= 0x01;
            return;
        }
        if ((PINB & (1 << UP)) == 0) {
            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 color channels to overflow (or underflow) when increasing (or decreasing) the value. This makes it possible to reach the full intensity faster.

1
2
3
4
/* 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.