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.