Showing posts with label AVR. Show all posts
Showing posts with label AVR. Show all posts

Saturday, November 21, 2020

Mini EPD Cal (Part 1)

Introduction

As some of you might know, there is a display technology that only uses energy when updating the picture (and not while displaying it). Actually there are multiple such technologies as I've recently learned because of my thesis. However I'm going to talk only about the electronic paper type of display here, since these are nowadays relatively cheap and make it possible to build devices powered only by batteries. I've chosen to use "e-Paper" displays from Waveshare, but interestingly there are other companies providing displays with almost exact dimensions (and possibly driver chips). I'm not blaming anyone of copying here, however I simply don't know what's the real source of these panels/drivers.

The main point of this project is this exact display type. More specifically I've chosen the 2.9" display with the resolution of 296 x 128 pixels. I've actually made a table of displays from Waveshare and have concluded that this one has the lowest DPI value, which is good, because the DPI values are already ridiculously high for home made devices of such type. Also this display has an integrated driver which made it possible to build such project quite easily. I could have used a bare display panel but that would require a relatively specific voltage regulator (not really, see schematic from second link below) and thus take much longer to build. Some such regulators might also be difficult to source. Examples of such existing projects can be found here, here and here in chronological order. Another important detail is that the display is quite small, but so is the price. The next display bigger in size would cost double and I didn't want to do such an investment right now since I would like to build multiple units of the device explained here. Obviously the bare panel would have been much cheaper, but again, no driver.

So what uses such a peculiar display could actually have? Obviously there are the electronic readers (Kindle and such) and electronic price tags, but I already got a Kindle and have no need for price tags. First of all it's important to think what this kind of display is good for. I suppose it's good for relatively slowly updating information and for battery powered devices, which is in turn good for portable devices. I thought about this for a longer while and came up with a few good uses: an electronic calendar, a weather display and simple indoor temperature/humidity display. These devices don't really need to be portable, but with batteries they are easier to place in any desired location.

My first project with such a display was actually a completely hard-coded calendar to which I later added a temperature graph. That is actually where this project name comes from, since the hardware is very similar to the original one and the name just stuck because I couldn't come up with a better one. I also already have an internet connected calendar with a bigger display that could be powered by batteries but I made a few mistakes in the PCB design and it will require a major update, so that's a topic for another day. Anyways this post is about a simple device that should measure temperature & relative humidity, log these and draw a graph on the display. The idea is that the battery life of this device should simply be long. Below is an early sketch of the device.

Original sketch of the design

Personally this is the project for me. This is something big. I'm not talking about the size of the schematic, size of the PCB or number of lines of code. This is a project that can actually be useful and not only to me. This is finally something that doesn't just blink colourful lights (although there is one blinking LED of course..) or display basic information (that a normal person would not need). This is something that can be considered an actual product, although there are many shortcomings in the implementation as can be seen later on. And this is something that I can share with my father even though at this point he might need a magnifying glass to read the display..

The device

Target

Obviously when doing a project with this scale, it's important to set objectives which the device should fulfil. It's also important to discard objectives that are impossible or difficult to fulfil. There's always room for improvement and such, but sometimes it's best to leave some stuff for the next revision. Anyways here's what I wanted the device to be:

  • battery powered (alkaline batteries)
  • long battery life
  • PCB as small as possible
  • minimalist design
  • use an ATmega328PB
  • use TMHU33 as user input (my new favourite)
  • shorter and longer graphs for both temperature & humidity
  • test points, programming connector, activity LED, reset button
Let's elaborate the minimalist design a bit more:
  • no voltage regulator
  • no external memory
  • no external RTC
And also some extras:

Shortcomings

A few shortcomings can be already seen at this point:
  • I don't have any case planned
  • The display requires an input voltage of 2.4-3.6V which limits the device to 2x battery configuration and the device will be able to use only part of the energy stored in batteries
  • The microcontroller has only 2KB of memory and it should be used for storing the measurements and the picture buffer, however the buffer size required for the whole display would be around 4.7K, so some trickery is required

The microcontroller selection is a clear shortcoming in this case, but that was a personal choice. I know this microcontroller pretty well and I knew from the start that this will not be a big issue. It would have been an issue however to use some new microcontroller that I'm not used to. Lack of a voltage regulator is also a very clear issue. However a linear regulator would not really work here and adding a buck/boost circuit would have made this device more expensive and complicated. Additionally there is not much free space left on the board as can be seen later on. I'm also quite sure that this list of shortcomings will grow as the project develops.

Future improvement

There are some rather obvious and some less obvious ideas for improvement:

  • Making a case of some kind
  • Using a solar panel *
  • Using super caps / rechargeable battery *
  • Using a voltage regulator
  • Using a microSD for storing the data, so that the device could store huge amount of data and the data could also be extracted
  • Adding a connection for data extraction via USB
  • Front light (pointing at the display) that can be manually enabled for a short period
  • Using BME280 from Bosch to also measure barometric pressure
  • Using BME680 from Bosch to also measure "air quality" or something

* Depends on the current consumption of the device, that is yet to be measured.

Similarly this list will probably grow as the project develops. The case is of course the most important part if this device is to be shared with other people. Data extraction on the other hand would turn this device into an actual logging device instead of just a simple graph display. And adding a solar panel would make the project really fun, especially if it could work "forever" without any batteries. Using super capacitors would then be a smart move if the solar panel would provide a reasonable amount of energy indoors. However I don't have any actual experience about solar panels, so it's difficult to estimate anything at this point.

Schematic

Display

The display is the most important part of this device. Without this display I wouldn't be able to make such a battery powered device, unless I would use an ChLCD display of course, but it's too expensive for such use. Nevertheless I will start with the schematic of the display, since this display does require some electronics even though it has a controller/driver. More specifically the display requires an external boost circuit and some filtering caps. The display contains a driver for the boost circuit, but obviously the inductor and the caps cannot be integrated. In this case also the diodes and the FET have to be added externally. Below is the circuit in question. This circuit is done based on the one found in the official datasheet. However the schematic in the official datasheet is utter, unreadable, bullshit compared to this one.

Boost circuit for the EPD

The VCI input seen above is the input voltage, GDR is Gate DRive, RESE is REsistor SEnse and PREVGH/GL are the unregulated high and low voltages for the EPD. The device will generate approximately +20 V and -20 V voltages with this circuit so the capacitors should be selected appropriately. Note that this is the same circuit as is used for larger displays from Waveshare and only the sense resistor (R8 in the picture) varies. I should also add that the sense resistor is actually 3R (at least in the Waveshare module) and not 2.7R or 3.2R. For the sake of completeness, I will also add the filtering capacitors and other connections here.

Display connector and filter caps

The circuit above is mostly self explanatory. I've created a connector component specifically for this display and here can be seen for example that the VCI and VDDIO are both connected to VCC. The VSS on the other hand is connected to ground in addition to BS (Bus Select) which sets the input as a 4-wire SPI bus. The four wires in question are chip select, data/command, serial data and serial clock. Additionally the microcontroller should control the reset and respond to the busy signal. More on that later.

Microcontroller

As already mentioned, the heart of the circuit is an ATmega328PB. The microcontroller circuit is quite simple. Display is directly connected to the microcontroller pins, as well as the clock crystal and the programming connector. The TMHU33 user input is connected with low pass filters like in the binary clock that I've shared previously. Additionally there is an activity led, reset button and some test points.

Microcontroller circuit

One thing to note here is the PU (Pull Up) connection. Since the microcontroller is directly powered from the battery, we can measure the battery voltage from a microcontroller pin. This way the resistor connection (R1 and R2) is not constantly connected to the battery and can be switched off when not needed. There is of course a small voltage drop because of the internal FET of the microcontroller pin, but that effect is quite insignificant. The resistor values are also chosen to be as large as possible without being too big for the ADC input impedance (impedance should be 10k or less). The capacitor (C9) is there to smooth out any noise from the measurement. The clock crystal also has to be such that the circuit does not require additional capacitors (max 6pF according to spec).

The rest

The rest of the circuit is shown below in a schematic that is so compact that it's almost uncomfortable. I will share the full schematic later on in any case. There are a few things someone might notice here. First of all there is a signal called INT. That is a legacy thing that is not really needed here, however the serial line could technically be connected to the programming connector, since SPI0 block shares the same pins as the UART1 block on the used microcontroller. Using this serial line the data could be dumped from the device to the PC. For that reason there is a connection to the INT0 pin to wake up the microcontroller.

The rest of the schematic

Another detail is the unconnected test point. This is the middle connection in case I want to power the device from two supercapacitors. This way I could solder the two capacitors in series directly to the PCB. Additionally there are two large capacitors (C21 and C22). These are for a bit of trialling how long the device could possibly operate from such capacitors in sleep mode. If the current consumption is small enough, it could be possible to change the batteries without losing the measurement data. This way the device could store really long graphs (a year?) without any interruptions.

PCB

Now the PCB is the fun part. I've spent really a lot of time making this design. Since I lack some imagination, I decided to make the PCB the same size as the Waveshare module as I've already mentioned. This includes the mounting holes in the corners and a notch on the bottom of the PCB for the flex cable of the display. The size of the PCB eventually caused some issues since there isn't really much space for all the components as can be seen in the pictures below. Another detail is that for some (historical) reasons I've decided to make the design vertical.. I actually didn't question it at all before writing this post, so I don't really have a better explanation.

Final PCB design, top side on the left, bottom on the right

As can hopefully be seen in the pictures, most of the components are located on the bottom side of the PCB, except for the programming connector and the activity LED. Additionally the reset button and the TMHU33 switch are on the edges of the board. Originally I wanted to use 2xAA battery configuration, but then I realised that there simply isn't enough space for that. I mean perhaps it could be done, but that is already a bit inhumane. I usually try to follow basic design rules regarding the layout and using 2xAA would require breaking a lot of those. Also there was already very little space for my name. :)

The circuit layout can easily be split into logical groups. The connector for the display is located at the bottom of the PCB and the boost circuit & filter caps are located in the near vicinity of the connector. I've decided to locate the battery holder on the right side of the PCB so that the the right edge will point downwards if the display is placed sideways, that way the joystick is on the right side and the battery holder can act as a stand. The joystick itself is located on the upper edge with all its circuitry. The power connectors and capacitors are also grouped into one place. The rest, which is mostly the microcontroller, clock crystal and the temperature & humidity sensor, are located on the left of the battery holder. The routing here is done manually and in my opinion it actually looks quite cool.

ps. Right here is the first issue in the design. I've broken my own rules and failed to add test points to the top side of the PCB. There isn't really much space on the top side, but there is a place for at least one test point on both sides of the flex cable. It might be difficult to see from the design, but it's easier to see from the photo of the final assembly.

Result

This post was mostly about hardware and in my opinion the result is very good in that regard. I wanted to add a diode and test points for a solar panel but eventually forgot to do that. Also there are no test points on the top side as mentioned above. Below are some pictures of the current state of the device. It's very much operational but the code was very quickly adopted from the previous revision, so it's quite ugly and unorganised. The time is still hard-coded, the measurement interval cannot be selected and there is no longer graph stored (because it will not fit into RAM memory).

Assembled unit number 2

There is also some errata that I've collected into the EasyEDA project. In addition to the issues above, the pads of the SW2 footprint are too small and the resistors around the temperature & humidity sensor are too close to said sensor. Also it seems that the connector for the display is a tiny bit offset, so the display cannot be exactly where it is marked on the PCB, because the short flex cable doesn't allow any adjustment on that axis (vertical in the picture above).

In the next part I will explain the basic code to drive this display, draw basic figures and print text. After that I shall explain how the temperature/humidity data is measured, stored and displayed. After that comes the fun part of adding input to actually set the time and the measurement interval as well as some other things (display modes?). Then there's of course the part of optimizing this code for maximum battery life and also measuring the actual battery life. So apparently this will not be a short project. I also hope I will not come up with too many improvement ideas along the way.. :)

Final words

I would say this project is progressing nicely so far. I've managed to design a PCB without any major problems or at least I haven't found them yet. The only issue has been my previous project that has taken more time than I would have hoped for. Also I have other projects that I want to start doing before this one is done. But I guess this fine. Also I have some other issues which might delay the next part of this topic.

After some coding I already regret the microcontroller choice. I might remake this later on with some better microcontroller, but I'm still determined to make this work using the chosen microcontroller. Though I've already got some "feature requests" that will make it tricky. Also originally I wanted to use the EEPROM memory, but now I kinda don't want to do that anymore. So everything will have to fit into RAM. That should be doable, since the EPD has built in memory for the picture data.

Another issue is the speed of the display update. I didn't check the datasheet well enough and apparently this smaller display is lacking some features that exist in the larger 4.2" display. I'm not sure if those features would really help with the speed and I will have to research that later on. The result is however that for example setting the time will be very slow on this device, but I'm quite sure I will come up with some solution to circumvent this issue.

ps. For some time I thought that EPD stands for Electronic Paper Display, but apparently it actually stands for Electrophoretic Display. Which kinda makes sense since that name actually represents the technology that makes the display possible and isn't any arbitrary name like "electronic paper".

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.

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

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