Thursday, October 15, 2020

Binary clock (Part 2)

Introduction

The hardware described in the first part of this instalment makes it possible to implement quite many interesting features as I've already tried to explain. Furthermore most of the features require very simple code so it would be a crime not to add more features to the device. Combining the features however tends to get messy, but I'll try to explain all the features and their necessity as simply as possible. 


Code

In the first part I've explained the basic clock function, how to update the display and how to read the input from the "lever and push action" thingy as well as the sleep mode. Here I'm going to explain some more basic clock features and some other things. Again all the register writes are pretty much specific to the used microcontroller, but all the other code should be quite universal.

Setting the Time

The clock part was already discussed previously, but there are a few details that are necessary to make a usable clock. First of all the user should be able to set the time. This was pretty much implemented in the first part. However the device should somehow indicate which value the user is adjusting (whether it's hours or minutes or anything else). I suppose the universal way of indicating that is simply by blinking the value that is being adjusted. The question is then what frequency and what duty cycle the blinking should have. For this I've simply checked the commercial devices that I happen to have and I suppose the fitting values are simply 1 Hz and 50%.

Implementing this in software is quite easy. First of all we should have the display update frequency faster or as fast as the blinking frequency. We should also have a counter to indicate when the value should be blinked. The last thing is to blank the respective part of the display (hours or minutes) whenever that value is being adjusted and the counter has reached some value. Below is a simple code example assuming the update interval is 200 ms and only for setting the time (hours and minutes). It's not exactly 1 Hz, but it's good enough. Instead of blanking the LEDs the device could also invert that part of the display, which would make the device look interesting and also quite psychedelic. :)

// Blink part of display when setting values
if(current_st == SETHR_ST && blink_counter >= 2)
  display_bits &= 0x00ff;
if(current_st == SETMIN_ST && blink_counter >= 2)
  display_bits &= 0xff00;
if(++blink_counter >= 4)
  blink_counter = 0;

Another issue that was not represented previously is that the user might want to set the time as precisely as possible (without any kind of automation). In my opinion a satisfactory result can be obtained with two actions. First the clock has to be stopped when the time is adjusted, so that the time doesn't change just as you're finishing the adjustment. Second the seconds should also be set. Setting seconds is usually done in a way, that the seconds can be zeroed by a press of a button. Or alternatively the seconds will be zeroed automatically when the hours and minutes have been set. These two features are quite easy to add to the current code. The code below represents a snippet of the button processing code shown in the first part. When entering the time setting state, the RTC timer is stopped, the timer counter is cleared and the seconds are also cleared. When exiting the time setting mode (after the hours and minutes have been set and the trigger key is pressed again) the timer is restarted.

if(*current_st == TIME_ST) {
   if(buttons == SWTL){ *current_st = SETHR_ST; TCCR2B = 0; TCNT2 = 0; time->sec = 0;}
} else if(*current_st == SETMIN_ST) {
   if(buttons == SWT){ *current_st = TIME_ST; TCCR2B = (1<<CS22)|(1<<CS21)|(1<<CS20);}
}

Clearing the timer counter is necessary above, since the smallest time step in this device is 8 seconds. If the value were not cleared, the time would technically be anything between 0 and 8 when the timer is restarted. It's also important to re-enable the timer if the current state is changed somewhere else, like for example in an automatic timeout as explained below.

Frequency Correction (theory)

I'll admit here that this is not my favourite topic, but it's something that has to be done and I'm trying my best here. This is not the first time I've been thinking about this, but this time I'm going to write down my thoughts. Also this topic is so long that I've split it in two parts. :)

Another thing that should be clear to anyone who has ever made a clock is that no clock crystal is 100% accurate. This means that the clock will most definitely drift in some direction. An obvious solution to this is to measure the actual frequency and make a correction factor for each device. However it's neither easy to (directly) measure the frequency nor make a correction to the frequency. The typical accuracy for a clock crystal is ±20 ppm (parts-per million). This means that the maximum error is 0.00002, which is ~0.6 Hz in case of the 32768 Hz clock crystal. I do not possess such tools that would allow me to measure that kind of inaccuracy. Also we cannot really compensate the frequency directly. The error is of course relative and we cannot really adjust for 0.00002 seconds or less every second (unless we use floating point maths but that would be simply stupid).

We can however adjust the time with any accuracy simply by adjusting over a period that is long enough for the units that we can use. For example with the maximum ±20 ppm error, the clock could drift by ~12 seconds in one week. We can simply measure this drift and add or subtract the respective amount of seconds once a week. For this we obviously need a reference and previously I've simply used time.is since it can show the time with large numbers. I don't know about the accuracy of that site, but it has been good enough for my purposes.

There are two things that are necessary to make this kind of adjustments. First we need to know how long the device has been running since the last sync. Previously I've been simply writing down the timestamp when updating the time and then calculating the duration manually. But this could of course be calculated in the device itself. Secondly we need to know the exact time of the device including seconds. The error in time value could be quite small in a short period, but the cumulative error over a few months can be very annoying. The idea here is that the user (including me) will not want to spend a lot of time for this kind of time adjustment, so we need to see the time drift as accurately as possible in a relatively short time period.

The details above could be demonstrated with the following calculation. If the clock drifts for, let's say, 2 minutes in 3 months, we can calculate that the drift is 120 seconds in ~7776000 seconds (depending on which months are in question). From this we can calculate that the error is ~15 ppm and we can compensate for that by adjusting the device by one second in exactly 18 hours.

Frequency correction (practice)

Let's start by measuring how long the device has been running. First of all we need to decide the unit of the measurement, since the user wouldn't want to read the value with huge accuracy and also we don't need that much accuracy for this value. We only need an estimate of how long the device has been running, because probably the biggest error will anyway be in reading the exact time from the clock. I've decided to measure the operation time in minutes and store the value in a 16-bit wide unsigned variable. That means that the value will overflow in around 45 days, which is probably good enough. Aint nobody got time for... calibrating a binary clock for longer than 45 days. This variable will be simply incremented every minute. However the value should also be zeroed every time the time is set by the user. Also the value should not overflow to reduce the risk of confusion. The user should just know that the timer will stop when it reaches the maximum value and that the value is then meaningless.

Next comes the exact time. For this the user needs to be able to also read the seconds. For this the device should have a dedicated mode where it shows minutes and seconds instead of hours and minutes like it usually does. The device could actually be configured to update the time value and the display every second for this purpose. Originally I thought that this would require changing the RTC timer configurations but then I realised that it's not necessary. The timer counter value can just be read on the fly and applied to the current seconds value. It's just necessary to take into account that it might overflow, so the minutes have to be also updated. Below is a simple example code. It should be noted that this is ugly and that the time struct is not updated, this code is simply for displaying the values.

uint8_t sec = time.sec + TCNT2 * 8 / 256;
display_bits = (bin_to_bcd(time.min + sec / 60) << 8) | bin_to_bcd(sec % 60);


The last detail is the correction factor and that is not such a simple thing. We could just choose a time interval after which one second is added or subtracted from the time, but that doesn't give us a linear correction factor. If we assume that the time interval is chosen in units of hours and that the clock drifts for 1 second in 14.5 hours, we can make a 1 second correction in 15 hours. That would make an adjustment for ~18.5 ppm, which leaves us with ~1.5 ppm drift. This drift can still amount to one second in a week, which is perhaps not good. For this reason the best way to implement the correction factor would be to make it a fraction. In this example the adjustment could be made for two seconds in 29 hours.

The fractional correction factor is quite simple to make. There should be an 8-bit signed value to store how many seconds are added or subtracted and an 8-bit unsigned value to store the time interval. There should of course be the possibility to view these two values and to adjust them. Additionally there should be a counter to update the time according to this correction factor. And another detail is that the time calculation code should be able to handle "illegal" values of the seconds counter. It should be able to handle up to -128 and +127 adjustment of current value of seconds, which could in turn be 0..59 seconds. But I suppose the trickiest part here is setting the negative value in binary format. Regardless of that below is some example code. :)

// Updated time calculation
if(time.sec >= 60)  {time.sec-=60;  time.min++;  if(uptime < 0xffff)   uptime++;}
if(time.min >= 60)  {time.min-=60;  time.hr++;   corr_counter++;}
if(time.hr >= 24)   {time.hr-=24;}

// Time correction part
if(corr_counter == corr_interval) {
   corr_counter = 0;
   time.sec += corr_value;
}

The uptime variable above represents the time since the last sync. Notice how it's capped to it's maximum value. The corr_counter variable is updated every hour and then cleared whenever it's value matches the corr_interval. The corr_value can be either positive or negative. The code above might result in a negative seconds value, which is absolutely fine in case of this code as long as it's of signed type and wide enough to store all the possible values. It might be a good idea though, to set the minimum value of the interval to something like 14 hours, since that's the maximum error with 20 ppm accuracy. Additionally the corr_counter variable should be zeroed similarly to the uptime variable whenever the time is set. I would almost recommend adding all correction related variables to a single struct, especially if these values need to be transferred between functions.

Obviously these settings are not intended for every day use, so they should be hidden somehow. For this we could make the user disconnect the power cable and reconnect it while keeping some button pressed. I suppose this is not too much to ask, especially if it's enough to do it once in the best case. This kind of feature can be simply implemented by reading the respective pins directly whenever the device gains power after being powered from the back up battery. Below is a simple example how to do that.

// Reading the buttons when the power is connected
if(~PINB & (1<<SWUP_PIN))
   current_st = UPTIME_ST;
if(~PIND & (1<<SWT_PIN))
   current_st = MINSEC_ST;
if(~PINB & (1<<SWDW_PIN))
   current_st = CORR_ST;

I found an interesting issue while testing the code shown above. Its appears that the INT1 and PCINT0 interrupts behave a bit differently. As discussed in the first part, these interrupts are disabled in the INT0 interrupt whenever the power is lost. When the power is gained, the interrupts are again enabled. The issue here seems to be that the INT1 interrupt is triggered immediately after being enabled. This causes the device to exit the MINSEC_ST shown above and immediately show the time. A simple fix for this is to clear the respective interrupt before enabling it. This issue is not seen with the PCINT0 interrupt for some reason.

There's just one last thing to this (I promise). This calibration value is such a setting that the user would not like to set twice, unlike any other setting in the device. That's obviously because measuring the clock inaccuracy takes such a long time. The time interval has to be large enough because the human error factor is quite big when reading the values manually. And there is a possibility that the device loses all power or gets stuck so that the correction value set by the user is lost. Because of this it's good to store the correction value into EEPROM (Electronically Erasable Programmable Read Only Memory) of the microcontroller. Writing to this memory can be done easily with the help of ready made avr-libc EEPROM functions. The device should just check periodically if the values in RAM differ from the ones in EEPROM and update the latter one if they do. Obviously the values should be read from the EEPROM on device startup. It's also good to note that this memory is usually erased when the microcontroller is reprogrammed, but the memory contents can be saved if the EESAVE fuse is set to 0. This is very useful when the device is still in development.

ps. The clock frequency could also be slightly different when the device is running from the back up battery. This is because the input voltage is very different from 5 V. However the main usage of this device is to run from 5 V so let's just assume that the clock drift is constant. Similarly the clock frequency might change according to the ambient temperature. At this point someone might ask why don't I just use an RTC chip that can compensate for all these factors. The reason not to use an RTC chip here is to simplify the circuit and have some challenge. Also all the settings can be stored in RAM memory since it's not cleared on USB power loss in this device so why not also calculate the time?

ps. ps. Well that was a lot of text for a little piece of code..

Alarm

I don't see a reason why a clock couldn't have an alarm feature even if it's a binary clock, so I've also added the alarm feature. The feature itself is very simple and requires simply having another time variable and the code to compare the running time to the alarm time. I've decided that comparing the seconds is not necessary (and wouldn't really work here) so it's just comparing hours and minutes. After the match the device should make sound and the alarm feature should be disabled. I suppose this is how alarms were done in pre-smartphone era. The alarm should also have the info whether it's enabled or not and for this I've simply added a bit to the time struct shown in the first part. The binary display doesn't leave much choice for displaying whether the alarm is enabled or not, so I've decided to represent that as values 1 or 2 in the top left corner of the display. This value also blinks as the hours and minutes when being set, as shown at the beginning of this part 2.

// Sound the alarm
if(alarm.en == 1 && time.hr == alarm.hr && time.min == alarm.min) {
   beeps = 15;
   alarm.en = 0;
}

// Displaying the alarm time and enable bit
display_bits = (bin_to_bcd(alarm.hr)<<8) | bin_to_bcd(alarm.min) | ((alarm.en+1)<<14);

// Blinking the "alarm enable" display
if(current_st == ALENA_ST && blink_counter >= 2)
   display_bits &= 0x3fff;

// Process the buttons for the alarm enable
} else if(*current_st == ALENA_ST) {
   if(buttons == SWT)  *current_st = SETAHR_ST;
   if(buttons == SWUP) alarm->en = 1;
   if(buttons == SWDW) alarm->en = 0;

Obviously the code above will not work as is. I've only shown the interesting bits that apply to the code shown previously. For example there should be relevant code to actually set the alarm time. What is also missing here is the actual code to make the sound. A basic tone can be made by using PWM (Pulse Width Modulation) feature of the microcontroller. However we want multiple sounds instead of one continuous tone, so we need another timer which should also be set as PWM output. First we need to configure the timers as shown below.

// Timer for speaker tone
OCR0A = 125;         // -> 1kHz
OCR0B = 62;          // -> 50% duty

// Timer for speaker control
TCCR1A = (1<<WGM11); // Fast PWM
TCCR1B = (1<<WGM13) | (1<<WGM12) | (1<<CS12) | (1<<CS10); // -> 7812Hz
ICR1 = 7812;         // -> 1Hz
OCR1A = 3906;        // -> 50% duty
TIMSK1 = (1<<OCIE1A) | (1<<TOIE1); // Enable interrupts

There are two timers shown here. The first timer is configured (later) as an output and is set to fast PWM mode with 1 kHz frequency and 50% duty cycle. The frequency was chosen quite randomly and the loudest frequency for this tiny speaker would have been 4.5 kHz according to the spec (I suppose) or perhaps it was just measured at that frequency. The second timer is also set to fast PWM mode, but it's set to 1 Hz frequency with 50% duty cycle. The latter timer is also set to have two interrupts, one in the middle and one at the end. Hardware-wise the speaker is simply connected to a timer0 output with a single series resistor.

ISR(TIMER1_COMPA_vect) {
   if(beeps) { // Enable tone if supposed to make sound
      beeps--;
      TCCR0A = (1<<COM0B1) | (1<<WGM01) | (1<<WGM00); // Fast PWM
      TCCR0B = (1<<WGM02) | (1<<CS01) | (1<<CS00);
   }
}

ISR(TIMER1_OVF_vect) {
   // Disable tone
   TCCR0A = 0;   // Disable output
   TCCR0B = 0;   // Stop timer
}

And that's it basically. The timer0 will be enabled for half a second as long as the beeps variable is non zero. Clearing the TCCR0B register above stops the timer0 and is basically the only necessary register to clear. However this might result in a case where the timer0 is stopped but the respective output is high. This would be fine I suppose since at least I've understood that the speaker will not leak any current. What is absolutely not fine, is that the +5 V line has some noise, specifically 800 kHz noise because of the LED control. This noise in the power line is very small, but it is however quite audible from the speaker if that happens. This is why the TCCR0A register is also cleared.

ps. What is done here is basically modulating timer0 with timer1. I've recently learned that the used ATmega328PB microcontroller has two new timers (3 and 4) and an OCM (Output Compare Modulator). This feature could be used to modulate one timer with the other with a bit less code. With this feature there could be only one interrupt or perhaps it could be done without any interrupts. Unfortunately there is only one output pin for this module and this happens to be PD2 which is the same pin as the INT0. The INT0 pin was already in use in this project and it would have required some rewiring to use this compare module in this project so I decided not to use it. I will test this in case I make a new revision of this device or make any other project that needs modulated output.

Temperature & Humidity

I'm not going to go into details about the SHTC3 sensor or the I2C bus that is used to communicate with it. However there are some details that I would like to share. The sensor itself is very easy to use. Whenever the sensor powers up, it enters the idle mode. After that the sensor should be put into normal mode and then the values can simply be read from the sensor. The measurement takes around 10 ms in normal mode, but the waiting is handled by the sensor (clock stretching enabled I suppose, I wrote the code myself but don't remember anymore). The values should then be adjusted according to the formulas provided in the datasheet. There is no need to put the sensor to sleep (to save energy) when the device is powered by the backup battery since the sensor is disconnected from the said battery (again, see schematic). There's no need to put the sensor to sleep while powered from USB either, since the quiescent current of the sensor is so small.

The interesting part here is the way the measurements are averaged. One way would be of course to simply measure enough values to average from. I prefer the following implementation, which is called cumulative moving average. This way the value is updated more often than with simple averaging and the value doesn't change as much as without averaging. The formula however has to be modified a bit. The formula shown in the Wikipedia works well with floating point variables, but it loses much accuracy with integer type values. The trick is then to store the multiplied value (see _acc variables below) instead of the final result.

#define AVERAGE_N 16
if(read_shtc3(&shtc3_temperature, &shtc3_humidity)) {
   temperature_acc = temperature_acc * (AVERAGE_N-1) / AVERAGE_N + shtc3_temperature;
   humidity_acc = humidity_acc * (AVERAGE_N-1) / AVERAGE_N + shtc3_humidity;
   shtc3_temperature = temperature_acc / AVERAGE_N;
   shtc3_humidity = humidity_acc / AVERAGE_N;
}

In the code above, first "one measurement" is removed from the accumulated variable (see acc*15/16 part) and a new measurement is added. This way no bits get lost and the precision is as good as it can be. Assuming the acc variables are wide enough of course. In this case the temperature_acc is 32-bits wide and that's very much enough. The shtc3_ variables then contain the final results after the acc variables are divided by the average count. There's probably lots of maths behind this, but I'm honestly not a huge fan of maths.

ps. The if statement in the code above checks if the read procedure was successful (including the CRC) and simply discards measurements that fail the check. This is very much necessary in this device, since it might lose power while reading the values from the sensor and end up reading some random values. This issue was seen as very high temperatures when connecting the power back after disconnecting it at a bad time.

Battery Voltage

I also decided to add battery voltage measurement as I've explained in the first part. In this case the implementation of such measurement is very simple. It only requires a direct connection from the battery to the ADC pin of the microcontroller. The reason for this is that the battery measurement is only done when the device has +5 V power so that that voltage can be used as a reference. Of course the USB voltage is specified to be 4.40 .. 5.25 V so that's not very precise.. but it's usually closer to 5 V than that. With this connection the voltage of the battery can then be measured and the user can be informed in case the battery voltage is too low. We know that the minimal working voltage for the microcontroller is 1.8 V and we can use that as the minimum with some margin. Additionally the device should also separate the cases where the voltage is 0 V simply because the battery is not installed. I wouldn't consider that as an issue, it's just the choice of the user not to use a backup battery.

By looking at the schematic, we can see however that there's a diode in the way of both +5 V and battery voltages. This means that both the reference and the measured voltage will be slightly lower than what is expected. I've measured empirically that the voltage drop of used diodes with small currents is around 250 mV. The value looks small, but we're talking about relatively small currents here. The LEDs are not connected to this diode and the microcontroller doesn't consume much even in active mode. With this information we can adjust both the reference and the measured value using this offset.

Another issue is that technically when the USB power is disconnected, the voltage on the ADC pin will be higher than the input voltage of the microcontroller. This is of course because of the diode mentioned above. However it should not be an issue, since the voltage difference is so small. The maximum allowed voltage on microcontrollers pins is Vcc + 0.5 V and in our case the voltage is only ~250 mV higher as already mentioned above.

Timeout from states

I suppose it is common for devices to have a timeout feature which will reset the device back to the normal state if, for example, the user doesn't finish setting the time in some time interval. I cannot really say what's the point of adding such feature but I've implemented it anyway. Mostly it has just disturbed testing the other features so I enclosed it into ifdef..endif block so I can simply disable it. The code in question is shown below.

// Timeout from any other view than time, temperature and humidity
static uint8_t timer = 0;
static state_t previous_st = TIME_ST;
if(current_st == TIME_ST || current_st == TEMP_ST || current_st == HUMID_ST || previous_st != current_st) {
   timer = 0;
   previous_st = current_st;
} else if(timer++ > TIMEOUT_DELAY_S) {
   current_st = TIME_ST;
   TCCR2B = (1<<CS22) | (1<<CS21) | (1<<CS20);
}

This code could be put for example into the TIMER1_OVF_vect ISR that was shown previously. This way the TIMEOUT_DELAY_S is simply a value that represents how many seconds this code waits until it resets the state to a previous state which can be time, temperature or humidity. This way the device will reset if it's been forgotten into any other state. This also doesn't take into account any presses the user makes, so it makes setting the time very annoying even with the 15 second delay. And here we can also see the timer2 enabling (in case it was stopped) that I mentioned previously. I can honestly say that this issue was very fun to debug.

Display Modes

And now we get to the most interesting part. Since the display is constructed out of 16 individually addressable RGB LEDs, the information it's supposed to show can be displayed in quite many different ways. I've thought about it with my (perhaps limited) imagination and came up with the following display modes:

  1. Normal mode, colour and brightness can be manually selected
  2. Time dependant mode, colour is selected depending on the time
  3. Rainbow, colour is selected depending on the LED position
  4. Moving rainbow (aka Nyan Cat -mode), the colour is selected depending on the LED position and it's constantly changing

Now let's start from something that actually affects all of these modes. First issue is how to select the colour. One option is to let the user set the intensity of each colour (red, green and blue), but that is not very convenient with this kind of hardware. It's also not very intuitive when you just want to choose the colour and the brightness. Of course there could be four "sliders", one for each colour and then one for the brightness, but that doesn't really make it a lot more convenient.

An obvious solution (in my opinion) is to make a linear space of colours, where they gradually change one into another. This could be for example red -> green -> blue -> red. This way the colour selection is intuitive and there are only two "sliders" including the brightness. Implementing this is quite simple, it's just perhaps difficult to understand the max values. If the max value of each colour is N, then the max linear value is N*3-1. I'm not going to explain this.. you just have to believe me again. Below is an example function how to transform this linear value into an RGB value. Additionally there is the brightness adjustment, which is implemented with bit shifting instead of division to speed up the calculation.

// color: [0:255*3-1] -> 3x[0:255]
// brightness: [0:8]
#define COLOR_MAX 255
void linear_value_to_rgb(uint16_t *src, rgb_color *dst, uint8_t *brightness) {
   *dst = (rgb_color) {0, 0, 0};

   if(*src >= 0 && *src <= COLOR_MAX) {
      dst->red = COLOR_MAX - *src;
      dst->green = *src;
   }

   if(*src > COLOR_MAX && *src <= COLOR_MAX*2) {
      dst->green = COLOR_MAX*2 - *src;
      dst->blue = *src - COLOR_MAX;
   }

   if(*src > COLOR_MAX*2 && *src <= COLOR_MAX*3) {
      dst->blue = COLOR_MAX*3 - *src;
      dst->red = *src - COLOR_MAX*2;
   }

   dst->red >>= (8 - *brightness);
   dst->green >>= (8 - *brightness);
   dst->blue >>= (8 - *brightness);
}

The function above is basically enough to implement the first mode. Of course the user should be able to select the linear colour and the brightness. I've implemented it so that the user does not see the actual values, but sees the result, the actual colour and brightness of anything that happens to be on the display. Basically in my implementation the only option is the current time. Additionally the value 8 in the code above is because the same function is used to calculate the colour of the passive LEDs and it should be possible to set these to be just black (completely off). Since the LED control is 8-bit wide, shifting by 8 will always result in zero value.

The second mode is based on the same function but requires additionally a transform function between time and the linear colour. Basically that's very easy, but there's an option of choosing direction of the colour change and the offset. The direction is simply which colour comes after any other colour. The function above has the direction of red -> green -> blue, but here I wanted it to be specifically red -> blue -> green, so I've changed the direction. Additionally there could be a shift, but I left the shift as zero, which gives purely red colour at exactly 0:00, blue colour at 8:00 and green colour at 16:00. This is represented in the code below.

// [0:24*60-1] -> [0:255*3-1]
#define LINEAR_COLOR_MAX (255*3-1)
void time_to_color(time_t *time, uint16_t *color) {
   const uint16_t SRC_MAX = 24*60-1, DST_MAX = LINEAR_COLOR_MAX;
   const uint16_t SHIFT = 0; // Shift specified in minutes

   uint16_t src = (SRC_MAX - (time->hr * 60 + time->min)) + SHIFT;
   *color = (uint32_t) (src) * DST_MAX / SRC_MAX;

   while(*color > DST_MAX)
      *color -= DST_MAX;
}

The third mode is even more simple. It's based on the first function, but the colour is chosen based on the LED index. The code in all it's simplicity is shown below. This line of code should be added to the loop which assigns the colours to each LED. The difference to all previous code is that it's updated for each LED.

linear_color = (uint32_t) (i) * LINEAR_COLOR_MAX / LED_COUNT;

The fourth mode is also quite simple. It's based on the code above, but there's an offset that is defined by the time. This on the other hand requires handling overflow which was not necessary in the third mode. To get the best effect I've had to reduce the update interval of the display to 20 ms and add a step of 8 to have the colour change smoothly. This looks equally amazing and useless, because it's very very annoying. However I just had to implement it since it was possible. Below is a snippet of the respective code. :)

// In the update loop
linear_color = (uint32_t) (i) * LINEAR_COLOR_MAX / LED_COUNT + nyan_cat_shift;
if(linear_color > LINEAR_COLOR_MAX) linear_color -= LINEAR_COLOR_MAX;

// At the end of the update
nyan_cat_shift += NYANCAT_STEP;
if(nyan_cat_shift > LINEAR_COLOR_MAX) nyan_cat_shift -= LINEAR_COLOR_MAX;

I obviously didn't want to hardcode any of the modes into the device, so I've made a state which let's the user choose the display mode. It has however an issue that there is kind of no difference between the first and the second mode. If the second mode is chosen, the values chosen for the first mode are overridden and thus changing back to first mode doesn't seem to change anything. Eventually I gave up trying to solve this issue and just left it as is. Obviously the colours could be stored separately for the first two modes so there wouldn't be such issue.

ps. There could of course be an option to choose different colours for different views (time, temperature, humidity) but I was simply too lazy to add that. It would be simple copy paste code and would just require many more states and variables.

Passive LEDs

As I've already mentioned, this clock has an issue for example at 0:00 o'clock when the whole display is switched off. Similarly without any light it's difficult to distinguish between 1:00 and 0:01 since there's only one LED switched on. For this reason I've enabled the "passive" LEDs to be something else than just switched off. The easiest way to implement this was to simply provide two brightness settings, both of which can be set by the user. I've simply restricted the active brightness to be 3 .. 7 and the passive brightness 0 .. 2. This way there is no confusion between which LEDs are actually active and which are passive. Both values are then used with the linear_value_to_rgb function shown above, which means that the colour of both LEDs is (almost) the same.

There should be some way to set this brightness of course. Originally I added a checker board display where the user could select the brightness of the passive LEDs only. This however is perhaps not very intuitive, so instead I've made it to be adjustable from the time mode like the active brightness. To enter the adjustment mode, the user should just long press the button which is responsible for entering the adjustment mode of the active brightness.

Result

Unfortunately I didn't have time to test the clock frequency correction. Ideally it would take a year to test it properly and I don't want to wait that long before posting this. I'm pretty sure the code is correct since I've already done something like this a long time ago. Unfortunately I do not have the code for that project anymore so I cannot see if there was anything else to that. I've also concluded that the timeout feature is absolutely useless. At least it should take key presses into account when resetting the timeout counter. Alternatively the timeout delay could be much longer.

Regardless of the previous issues, here's a video about the newly added features. The video is without any annotation so I'll explain here what it shows. First I set the time. Then I adjust the brightness of the active and passive LEDs (the latter might be difficult to see). After that I adjust the colour manually and then change to the time dependant mode, rainbow mode and changing rainbow mode. After that I loop through the time, temperature, humidity and alarm returning back to time. And yes, I should add annotations to the video itself, but that's perhaps a task for some later time.

Conclusions

There's one thing that can be learned from the first two posts about this project so far. As I've explained in the first part, I've chosen the 8 second interval for the RTC timer to increase the battery life when powered from the battery. This interval could be changed when the device is not powered from the battery but that would make things more complicated. The issue here is that assuming the device is started at 00:00:00, the first minute will last exactly 64 seconds and the second one 56 seconds. And this will continue til the end of time. The frequency correction represented here makes the situation even more complicated even if the minutes still last 64 and 56 seconds. The situation could be solved as was done with the minutes & seconds display, but calculating the exact time twice is just stupid. The situation gets even more silly since the formula shown in the first part can be used to calculate the average current consumption with a 1 second interval instead of 8 second one. And the current consumption will increase from 2.51 uA to an astonishing 2.58 uA.

I would conclude from this that it doesn't make any sense to use the 8 second interval for the RTC timer since the current consumption increase is absolutely insignificant and the code is much much more simple when using one second interval. The only issue here is that I've used the RTC timer counter for the long-press support, so that has to be changed somehow to use the one second interval instead. I suppose I will fix that before the next part. :)

Final words

Writing this post took me much longer than I anticipated. That is mostly because I came up with more features while writing this. So I was actually writing this post and coding in turns. I suppose that is normal for a fun project without a clear target. There were plenty of fun topics here, apart from the frequency correction of course. That is not fun, it's just necessary so that all clocks don't drift all the time. The store bought clocks already drift quite enough in this apartment and unfortunately I cannot fix them.. :)

No comments:

Post a Comment

Internet of Crosstrainers, part 2

Introduction As mentioned in the original post here , there were some issues in the described implementation. I had some difficulties to fin...