Saturday, October 31, 2020

Binary clock (Part 3)

Introduction

I was going to name this post "binary clock and practical examples of over-engineering" but perhaps this isn't exactly over-engineering. Over-engineering implies that something simple is done in a very complicated manner. Here I'm going to describe some simple implementations for rather simple ideas. It's just that the ideas are either very genius or very stupid. Don't really know which yet. Perhaps it's both?

Apparently there's another meaning for the word over-engineering. It also means designing a product that is more robust or has more features than necessary and I guess the second part applies here. So I guess this is over-engineering after all. Anyways, more features!

(more)

Code

As previously, these new features require only new code. Well, the brightness correction would require some hardware modifications, but I'll leave that for another time.

Low Battery Text

I spent an awful lot of time to come up with a way to indicate the low battery situation. I mean, the display could just blink, or the picture could be inverted or even rotated randomly, but none of those feel intuitive to indicate the low battery situation. There are also so few pixels that there couldn't really be a picture of a battery. Later I realised that there are still enough pixels to show a scrolling text "battery low". For this I didn't really bother making code for forming text on the display so I simply hard-coded the picture with said text. This picture is then scrolled on the display and is more or less comprehensible to the user. Below is a picture that I've used to hard-code the text into the program.

Picture used for the scrolling text to indicate low battery situation

The code required for this feature is relatively simple. The first part is to store the image that will be scrolled. For simplicity's sake I've decided to store it in such a way that the 4 LSBs of each byte represent one row of the picture shown above and the MSBs are unused. Below is the beginning of the picture as it is in the code. Try to see the connection between the code and the letter b in the picture above. The picture (in the code) is both mirrored (vertically) and rotated (90 degrees CW) because that's how the display is oriented. The rotation and mirroring could be done in software but I would say that that's completely overkill for this kind of purpose. The array should also be 4 rows bigger than the picture, so that the display is completely empty before and after the scrolling text.

const __flash uint8_t low_bat_text[] = {
   0, 0, 0, 0,
   0b1111,
   0b0101,
   0b0010,
   0b0000,
   // ...
   0, 0, 0, 0
};

The code responsible for displaying the given picture is also quite simple. I've added the low battery situation as a state to the device. The low battery detection explained in part 2 sets the current state to LOWBAT_ST which enables the code below. After that the low_bat_display_counter represents the position in the scrolling picture and also resets the state back to TIME_ST after the whole picture has done scrolling. The picture is also transferred to the display_bits variable so that there's always 4 rows in the picture and the values from the low_bat_text are shifted appropriately. The MSBs are zero, so they don't require any kind of masking and the resulting code is very simple. Note that the device should actually return to the previous state which might be something else than TIME_ST.

if(low_bat_display_counter < sizeof(low_bat_text) - 4) {
   display_bits = (low_bat_text[low_bat_display_counter] << 12) |
   
               (low_bat_text[low_bat_display_counter+1] << 8) |
                  (low_bat_text[low_bat_display_counter+2] << 4) |
                  (low_bat_text[low_bat_display_counter+3] << 0);
   low_bat_display_counter++;
} else {
   display_bits = 0;
   low_bat_display_counter = 0;
   current_st = TIME_ST;
}

ps. I've mentioned the rotating display and would also like to mention that at some point I had a round shaped clock from Philips that had a rotating display. I mean the display would not rotate, only the picture. It was quite a funny device since the display was in no way symmetrical, but still it could rotate to 0, 90, 180 or  270 degrees and would still be readable. The funniest part was the sensor which was responsible for the detection of the rotation. It was actually a gold plated metal ball in a plastic cage that would roll freely and short different gold plated metal bars depending on the orientation. Needless to say, the sensor didn't work very well.. it started failing relatively soon. The clock would still work but it would require a lot of shaking to change the orientation of the display. Perhaps I could take a picture of that sensor if I still have the clock and share it here later on.

Temperature Compensation

As mentioned earlier, the temperature measurement of the device is absolutely useless without any modifications. The measured temperature is at least one degree higher than the ambient temperature. I suppose this is because the device heats up even with reasonably low brightness. Originally I just thought to leave it as is, but then I started to think whether something can be done about it. I have to admit that I have studied something related to this but I've effectively forgotten this topic. However after some searching I found a very simple equation (the only equation on the page) that could be used here. In this case, the junction temperature is the measured one, the power is the power used by the whole device, the ambient temperature is what we want to be able to read and the thermal resistance is something that I'm just going to calculate/guess. This is in no way accurate, but it might make the temperature sensor even remotely useful. At least the equation is linear, and all linear things are good.

The first issue here is to measure the (relatively static) current consumption of the microcontroller. After that the current consumption of the LEDs should be calculated including their current consumption in idle state (with the light switched off). After measuring these values the device should be able to calculate the theoretical current consumption based on the LED states. The last part in this equation is to calculate the thermal resistance. This can be done simply by reading the measured temperature and comparing it to the ambient temperature measured with any other device. So the equation is:

T_a = T_s - P * R_t,

where T_a is ambient temperature, T_s is the temperature measured by the sensor, P is the power consumption and R_t is the thermal resistance of the system. I've measured that the static current consumption of the device (microcontroller and LEDs) is around 14.5 mA and dynamic current consumption is around 40.5 mA when a single LED is on full brightness. I've also measured that current consumption of each colour is quite equal. Calculating the total current consumption of the device then only requires sum of the colour intensities of each led and the following equation:

I_tot = 14.5 mA + 40.5 * sum / (255*3)

The last missing part is the thermal resistance. Originally I thought I would make one measurement and compare it against some reference. Then I remembered that I don't have a proper reference and also the effect is linear, so I can just make multiple measurements and compare them against each other. For this purpose I simply made a mode where all the pixels are enabled, but still the colour and the brightness can be set manually. I then set the device to each possible brightness, let it stabilise for a while and take the temperature measurement for each respective brightness. After that I plotted all the data in excel as shown in the picture below. I should have used any other software (I would prefer Gnuplot) but excel was much quicker since there's not so many data-points and this is not very scientific in any case..

Measured temperature against power consumption

From this we can conclude that the temperature at the moment of measurements was 24.6 C (the LP value in the table) and that the thermal resistance is 12.8 mOhm (I suppose). The T_real row represents the ambient temperature calculated from the measured temperature using these exact values. As we can see, it's not really foolproof and I really don't believe the temperature in this room was 24.6 C at the time of taking these measurements but I suppose it's fine. At least the graph looks linear as the equation suggests.

Following this measurement I implemented a correction factor in the code. The correction requires first calculating the current consumption and then the actual temperature correction factor. The example code is shown below. Note the unit conversions so that the calculations don't overflow.

#define THERMAL_RESISTANCE 128 // Unit is 10^-4

// Calculate the current consumption (in units of 0.1 mA)
uint16_t led_sum = 0;
for (uint8_t i = 0; i < LED_COUNT; i++)
   led_sum += colors[i].red + colors[i].green + colors[i].blue;
current_consumption = 145 + (uint32_t) (led_sum) * 405 / (3*256);

// Make adjustment to the measured temperature
// Temperature unit 0.01 C, current unit 0.1 mA, resistance unit 0.0001
shtc3_temperature -= (uint32_t) (current_consumption) * 5 * THERMAL_RESISTANCE / 1000;

One issue here is that the current consumption might fluctuate quite a lot. Imagine the temperature change from 27.77 degrees to 28. The first one requires ten LEDs to be on and the second one only two. For this purpose I thought about adding some averaging here, but I suppose the (already implemented) temperature averaging will be sufficient for this purpose. The correction of the temperature should of course be done before the averaging in this case and that's exactly how I've done it. Now the next question is whether this makes any sense, since the only reasonable brightness settings are a few lowest ones. But we can see from the graph and the calculations that even the lowest brightness setting does indeed increase the measured temperature by one degree and that's quite a lot. After this adjustment I've found that the device more or less agrees on the temperature with the other devices that I have here. So I guess that's good enough.

Note: The calculations above assume pretty much that the power used by the device is spread evenly around the PCB. That is probably not the case, but I don't really know to which extent. Similarly this adjustment assumes that the temperature of the device has stabilised. The device will obviously show erroneous temperature for a while after just being switched on. I could of course measure whether the first issue is really an issue and then more maths can be added to mitigate this problem. The other issue however cannot really be solved. Of course the device should always have power so it could just keep track of the current consumption also in sleep mode. But that's just.. over-engineering to such an extent where even I don't want to go right now. Also the temperature sensor is not connected to the backup battery, so it cannot be used with this hardware. The only viable solution in my point is to measure the slope of the temperature changes and blink the display to indicate to the user that the temperature measurement might show bogus values at the moment. After the temperature stabilises the display should stop blinking to indicate that the displayed value should represent the actual temperature.

ps. Someone might remember that there's also the relative humidity measurement in addition to the temperature measurement. Originally I thought that I shall also adjust that value. However there are two issues. First of all, the relation of relative humidity to the temperature is so complicated that I've just decided not to touch that topic. Also I'm not really sure whether the value of the relative humidity measurement should be adjusted.. I don't have a good reference for measuring the relative humidity, but the best thing I have, shows very similar results without any adjustments. So perhaps it's good as it is. I don't know much about the relative humidity and I don't really want to learn right now. After all this is just a fun project and I already have better devices for measuring relative humidity.

Brightness Compensation

The Nyan Cat display more explained in the second part has one major issue. The idea of that mode is to gradually change the colour of each pixel on the display, without changing the intensity of the light. However the intensity of the light does change even if the software is supposed to only change the colour. One simple explanation is that the intensity of each colour is different. That could be fixed with a simple correction factor (multiplier for each colour). The maximum intensities of the different colours of the WS2812B LED are actually specified in the datasheet. This way the correction factor can be added to the code.

Wavelengths and intensities of different colours in WS2812B LED

This however doesn't seem to be the only issue. The intensity seems to change also between colours. This means that the intensity doesn't change linearly to the values sent to the LED. Unfortunately this non-linearity is not documented in the datasheet. I almost gave up regarding this issue, but then I had an epiphany. The non-linearity could simply be measured empirically and compensated by using a hard-coded LUT (Look Up Table). Moreover the measurements could be done with the photo-diode that is actually already on the device.. The device should just be set to enable all the LEDs, get a reading using the photo-diode and output it via the serial line. This should then be looped over all the LED intensities and all the colours in case the sensor reacts differently to a specific colour. Actually the sensor does have different sensitivity for different wavelengths as can be seen in the picture below.

For the purpose of this measurement, I've also set a mirror at a suitable distance from the LEDs to reflect the light back to the sensor. I've also covered this setup with something dark to avoid disturbing the measurement with ambient light. Below is the result that I got. The absolute value of the y axis is quite meaningless here, were only comparing the different colours and looking at the linearity of the graph. The graph does look very much linear except for the leftmost part. And perhaps the leftmost part is the most important part in this project since the LEDs are so bright that they will hurt your eyes on any higher brightness setting. Right about here I realized that I could have simply made a google search regarding the LED linearity with PWM output. Also there are some small "holes" in the measured data and I have absolutely no idea what are those.

Light intensities of different colours at different PWM values

Let's think about this graph a bit. We know that the PWM output is by definition linear. The amount of the energy transferred is directly proportional to the pulse width. That is of course without taking the rest of the system into account. The rest of the system is the actual LED component here and it's kind of logical that it doesn't switch on immediately including any possible FET transistors involved. And this switching delay is what makes the PWM system nonlinear. Well at least according to this source, and at least to me it makes sense.

I tried to replicate this behaviour with some theoretical maths but that was just too much for me right now. Originally I wanted to delve a little bit deeper into this topic but honestly I really don't have the energy. The most important question is whether this non-linearity can be dealt with one way or another. And unfortunately at least I couldn't come up with anything. We need PWM values as low as 7 for sensible brightness of the display (especially with darker ambient light) and at that point we cannot really do any sensible dimming. So unfortunately the result will still stay nonlinear unless the LEDs are physically dimmed somehow, for example with some dark foil under the front plastic.

However we can at least try to balance the different colours using the datasheets and this measurement setup. The average intensities or RGB colours for the WS2812B are 405, 690 and 190 respectively. This means that we need to divide the PWM of the red value by 2 and the green by 3.5 so that they have the same intensities as the blue one. However before running the full measurement again, let's see how similar is the graph above to the theoretical values. The sensitivity of the BPW34 for respective wavelengths are approximately 0.65, 0.35 and 0.2. Let's just apply these to the intensities above so we get 263, 242 and ... 38. Well that doesn't look at all similar to the graph above. I suppose this is a good point to simply give up. This topic clearly requires more research and I'm not willing to do that right now.

Another good reason to give up at this point is because human eye reacts differently to different wavelengths. It means that the intensity written on the datasheet might not represent the intensity seen by a human eye. This would require even more adjustments and perhaps I will have to deal with this at some later point in some other incoming project. I do like colorful lights so I will most definitely have to think about this in the near future.

ps. Taking the intensity measurement didn't work so well at the first try. I didn't really understand why I was getting quite random results and I only understood it a few days later. The issue here is that there are 16 LEDs controlled by unsynchronized PWM.. It means that their PWM might drift randomly in respect to each other even if their pulse width is the same. I also checked the sensor voltage with an oscilloscope and the result was quite unintelligible as one might expect in this case. Unfortunately I don't have a screenshot of that and I've already disassembled the measurement setup, so I'm not going to provide a picture here.

Automagic Frequency Correction

In the second part I've described how to add a correction factor to a clock. It only requires a reference and a long enough measurement period. More specifically calculation of the correction factor requires two synchronisations to a known reference, time measure between these syncs and the time difference (to the reference) at the second sync. Then the user can calculate the correction factor and input it into the device. After that the user will also adjust the time as it should have drifted significantly to have any meaningful measurement.

Now someone might figure out that the device could calculate the correction factor by itself, since both values mentioned above can be calculated from the second sync. The rest is just maths, assuming that the user does both synchronisations correctly of course. Then we simply have to decide how the device should correct the time. Previously I've said that adjusting by one second in a specific time interval was not good. However it's not good only when the granularity of the interval selection is relatively large. Now that the user doesn't need to enter the value, it can easily be measured in seconds, which in turn makes it much more accurate. Of course the interval could be also set manually with the accuracy of one second but that's perhaps just .. too much.

This functionality would work as follows. First the time interval should be measured between two syncs, and this is already implemented. However this time interval should now be measured in seconds. Additionally it should calculate the time difference between the running time and the new set time. This will require some modifications to the previous code as I've decided to stop the clock while the time is being set. The difference itself is quite easy to calculate, first both time values should be converted to seconds and then simply subtracted. After that the interval between measurements should be divided by the time difference. This result will be the interval for time correction and the sign of the time difference will define whether one second will be added or subtracted from the time.

First things first. We need to solve the time stopping issue. To have the same effect and keep both values (running time and the one being adjusted), we can separate the active time and setting time. The timer would always keep running and update the active time. When the user wants to set the time, the active time would be copied to the setting time and and that will be displayed while the user is setting the time. When the device shall exit the time setting mode, first the difference between the setting time and active time should be calculated. After that the setting time should be copied to the active time, the timer counter should be cleared and the device should again display the active time.

The time interval measurement should be for example 32-bit wide unsigned variable and it should be incremented every second. This width will be enough for around 130 years so I guess that's enough. The time difference should then be calculated from the two timestamps (the active and setting times), that's the time at the sync and the time set by the user while syncing. The rest is a simple division and sign detection. I've decided to make all variables as signed to simplify the maths a bit.

// All values signed just for more simple maths
int32_t corr_conter = 0;
int32_t active_time = act.h * 3600 + act.min * 60 + act.sec;
int32_t setting_time = set.h * 3600 + set.min * 60 + set.sec;
int32_t corr_diff = (active_time - setting_time) % (24 * 3600);
// -> Replace with code shown below
int32_t corr_interval = abs(corr_counter / corr_diff);
int8_t corr_value = corr_diff < 0 ? 1 : -1;

There are, of course, a few things to note here. I suppose the first thing to note here is that the corr_diff might be equal to zero. That should technically be taken into account. Secondly this simple code would only work assuming there is no correction factor currently set. If there is already some kind of correction factor in place, it should be taken into account when calculating a new one. So in a way the code should revert the effect of the current correction factor to calculate the new one. Below is some example code.

// Revert the effect of existing correction factor
if(corr_value != 0) {
    corr_diff -= corr_value * (corr_diff/corr_interval);
}

// Leave the correction factor as is if there is no difference
if(corr_diff != 0) {
    corr_interval = abs(corr_counter / corr_diff);
    corr_value = corr_diff < 0 ? 1 : -1;
}

There is however one major issue here, which is perhaps the reason this kind of feature is not implemented in any commercial device. The issue is, of course, a possibility of user error. If we assume that the time is set incorrectly, the next sync will cause the device to calculate an erroneous correction factor. The next sync after that will also have the same effect because the previous one was the faulty sync. The fourth time will correct the issue if the time is set correctly. So in short, the device will require two proper syncs with any meaningful time difference between the device and the reference.

ps. Another issue not discussed here is DST (Daylight Saving Time). I'm sure everyone is familiar with that but nevertheless. This means that the time sync could have a difference of approximately one hour and of course any drift on top of that. I suppose that can be easily dealt with by simply detecting whether the difference is ~one hour and then adjusting for 3600 seconds which is one hour. For example if the difference is equal to 3607 seconds, the value used in calculations above should be 7 seconds. For this we need a simple piece of code, for example if the difference is more than 45 minutes and less than 75 minutes, subtract one hour to the difference. Similarly if the difference is bigger than smaller than -45 and bigger than -75, add one hour.

ps. ps. Obviously there might be something here that I haven't really thought of. For that reason it would be good to add some error checks, which would prevent the device from calculating some insane correction factor. However I would first just try it like this and see how it will go. What's the worst that can happen? :)

ps. ps. ps. This is starting to sound like I'm calling a cat. There is another very obvious issue here. When the device is started the first time, the clock is set to some time. And obviously this time should not be used for the first calculation. For that reason there should be some kind of time set bit that is set after the clock is synced the first time and only after that this correction should actually be enabled. As stated previously, it requires two proper syncs. So the startup time should not be used for the calculation.

Daylight Saving Time

Writing of this post happened to last so long that I actually had to adjust some clocks because of daylight saving time, including this device. After I realised that I have no less than 5 home made devices that didn't change to winter time automatically I realised that this clock could also be able to do it itself. For some reason it never occurred to me that this device could still have the date information even though it's not supposed to show it. It could be a hidden feature just like the correction factor things I explained in the previous part. For example at startup the date would be 0.1.2020 and if the user changes it to some meaningful date, it will enable the date calculation and automatic DST adjustment. The code for this feature is actually quite simple. I actually wrote it already some time ago, but apparently I forgot to add it to the latest of my devices. Update: this code was included in three devices of the four mentioned above, but the dst bit was set incorrectly. :)

// Daylight savings time
if(time.mday > 24 && time.wday == 6) {  // Last sunday of month
    // MARCH, 03:00 -> ++
    if(time.mon == 3 && 
time.hr == 3 && time.dst == 0) {
        time.dst = 1;
        time.hr++;
    }
    // OCTOBER, 04:00 -> --
    if(time.mon == 10 && 
time.hr == 4 && time.dst == 1) {
        time.dst = 0;
        time.hr--;
    }
}

The code should just have the information which month, day of month, weekday and the current time. Additionally there should be a dst bit so the device knows whether it make the specific adjustments. The dst bit is not really necessary when changing to summer time since one hour is added to the time and there can be no confusion, meaning that the time goes from 2:59 to 4:00. When changing to winter time however, the time goes from 3:59 to 3:00 and it's important to know whether it's "summer time" 3:00 or "winter time" 3:00, because otherwise the device will be stuck in a loop.

Speaking of dst bit and weekdays, I've never actually implemented calculation of these. It was either set by hand or received from the PC over a serial line. Now I thought about it and decided that the device could indeed calculate both of these by itself. The dst bit is a bit tricky to calculate, but it should be quite self explanatory compared to the code above. The idea is simply to check whether the date is between the last Sunday of March (3 o'clock) and last Sunday of October (4 o'clock). If it is, then the dst should be set to one. The weekday calculation is even more tricky. At least if the code below is considered. I've simply copied the code from here, which is in turn copied from here. I suppose it's worth checking out but I don't really recommend spending much time on it.

// Set the DST bit automagically
if(time->m < 3 || time->m > 10 || ((time->d - ((time->wd+1)%7) > 24) && ((time->m == 3 && time->hr < 3) || (time->m == 10 && time->hr > 4))))
    time->dst = 0;
else
    time->dst = 1;

// Set weekday automagically
int d = time->d, m = time->m, y = time->y;
time->wd = (d += m < 3 ? y-- : y - 2, 23*m/9 + d + 3 + y/4- y/100 + y/400)%7;

Automatic brightness

I've explained in the first part that the device has a light sensor that could be used for measuring the ambient light. I've also explained that it's facing the same direction as the LEDs and that it will most definitely pick up the light that is coming from the LEDs which will render the sensor absolutely useless. I've also used this to my advantage in this part but that was done with a mirror.

The main question right here is whether it is actually useless or not. The second question is how to evaluate that. For this I've set up the device to measure the voltage of the light sensor every 200 milliseconds and send it via serial line without any averaging or any other adjustments. I've set the reference voltage to 1.1 V because the measured voltage is quite small. After that I've assembled the "case" of the device and connected the serial cable with test clips to the test points, which required some skill. Then I simply set the device to a location where I've had it for a few months already and monitored the values seen on the serial line with different light setups and brightness settings.

This time of the year is not very convenient for this because there's hardly any light outside.. However I've concluded that in dark lighting the device will pick up almost no interference from it's own LEDs. Even with the maximum brightness, the maximum value measured by the ADC is 2 and it goes up to 4 if I put my hand in front of the display. However when I switch on my table lamp the value goes up to 10. The ceiling light however makes the ADC value go up to 4 as well as the sun did today even if it was quite dark in general. From this we could conclude that the self interference has a maximum value of 2 and the general lighting has values 4-10, which in my opinion makes this sensor actually useful!

Another less scientific way to investigate this is to have a flashlight with a relatively uniform light beam. This way the brightness of the light can be controlled simply by moving the flashlight closer to or further from the display. Now this is purely based on my personal feeling, but it really feels like the display is perfectly readable on lowest brightness when the ADC value is below 10. The next brightness level is then perfectly readable until around 30. At least when the used colour is red.

I have to say that I'm not very good at making "automatic brightness" features and I don't have much scientific background on this topic. I'm also not going to implement any such feature right now, but I can share some details how to get a decent result based on my earlier experience. I would recommend some kind of averaging of the measurement values and also a Schmitt trigger. For example in this case, the lowest brightness would be used when the ADC values are 0-7 and at 8 it would change to the next brightness. However when at the second brightness, it will stay there until the ADC value drops down to 4. This way the device will flicker much less in case there are small changes in the ambient light.

Results

The low battery text is working perfectly, the temperature compensation is working reasonably well and the brightness correction doesn't work at all. For obvious reasons I cannot really comment on the time correction code, since it might take some time before I see any results. However I had very good progress compared to the previous devices I've done. Lastly the brightness measurement is working quite nicely even though I thought it would be absolutely useless. The only thing that is worth showing here however is the low battery animation so here it is.

Final words

OMG it's finally done. It feels that I don't have enough friends to declare that IT'S FINALLY DONE so I'll just say it here. This has been quite a ride from the beginning til the end. I didn't really plan to do this much from the beginning. It was just supposed to be a simple clock with a few funny features. Well it's actually exactly that but it did take a lot of thinking and time. But again documenting own projects is good, so that at least (hopefully) I don't have to invent the same things over and over again. After all, many of the things described here can be reused in other projects.

I would say that this project is now finished even if I failed to implement some features. This reminds me of "Definition of Done" or DoD in short. I suppose DoD here is that I've implemented most of the things I was going to and even some that I wasn't going to. I have to say that I had a lot of fun and learned a lot of things and these two were the main objectives of this (mostly) useless device. Next I will do something more meaningful. Right now I will take a break however and read a book or something. Just kidding, I already have three new projects planned.

The EasyEDA project can be found here. Please note the errata in the schematic. I'm not going to fix the issues unless I make a new revision. I will also share the code in GitHub whenever I feel like or if anyone asks.

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