Nixie Tube Clock
Near the end of 2016, I watched a show containing a very interesting and inspiring sci-fi electronic device based on real-world electrical components. Its function in the show is to display a number, which it does so by presenting its digits across eight Nixie tubes, officially named cold cathode display tubes, rather than using a modern digital display to print the numbers. Right after I saw the device for the first time I took to the internet, first to find out what the tubes were called, and then I hoped to find somewhere that I could purchase some for a reasonable price. I did a bit of research pertaining to the voltages necessary for driving them to ensure that I’d be able to power them easily using the 120-volt modern wall voltage. Once I was sure that I could, I purchased a set of six tubes, deciding shortly after that I would build a clock by combining them with an Arduino microcontroller and a power supply, then set out to construct it.
I’ve faced many trials since I first began building this Nixie-tube clock, but it has allowed me to become quite comfortable with the Arduino IDE, helped me to improve my circuit designing and soldering skills, and allowed me to work with new materials and types of machinery to bring my imagined design to life. The final clock, which is nearly completed and currently functions perfectly, will comprise the six tubes separated in pairs of two by colon tubes to display hours, minutes and seconds protruding from the top of a decorative base made from three sections of edge-routed bloodwood sandwiching two brass panels. I modeled the entirety of the clock in Creo Parametric to visualize it before settling on final dimensions for the structural components and to aid in performing the math involved in preparing the parts, so that they would fit well with each other, and in storing the resulting values.
In total, I spent hundreds of hours, possibly over a thousand, researching components and techniques, coding and debugging, wiring and soldering, replacing components and making the circuitry more efficient, machining the base components, modifying the Arduino board to allow my Arduino prototyping shield to fit with components soldered to it, shopping for materials and assembling everything into the final product. I have enjoyed every aspect involved in this project: coding, building, soldering the circuits, modeling and troubleshooting cover the majority of the tasks involved and are the reason that it has been one of my favorite hobby projects to work on thus far.
I’m in the home-stretch at this point and only need to do some final cutting of the brass sections, filing and sanding away blemishes in the brass and wood, applying a clear coat to the bloodwood and assembling the entire structure one final time. There are a few useful additions to the code that I will eventually include, having already added daylight saving time support even when the clock Is unplugged and a method of changing the time using buttons and a switch, also responsible for switching between 12 and 24-hour time.
Now that I have an extremely accurate clock that considers temperature fluctuations and can keep track of the date, I want to implement new code to allow the date to be changed using those same buttons and displayed on the tubes, as well as making the tubes display the temperature if requested. I also just decided now that I want to implement capacitive sensors connected to the brass sections, allowing the clock modes and settings to be switched simply by tapping the brass with a finger.
These ideas are all exciting to me and I know exactly how I want to bring them to the clock, so I can’t wait until I can start working on it again to add my new feature ideas. At one point I considered selling my clock once I completed it, but I have decided that I can’t place a monetary value on it since I’ve invested so much of my time and energy into it. It’s a clock that I will cherish forever, as weird as that sounds to say.
After spending a few weeks planning and researching the materials, circuitry and coding knowledge that would be required to build my clock, I purchased a few key components on eBay and got started constructing the physical clock. I first grabbed a set of six tubes, since I needed three sets of two-digit displays to print hours, minutes and seconds, and buying them in a bulk package of six saved me some money compared to buying them individually. I had never built a power supply circuit before, nor had I researched their composition and structure, and made the mistake of purchasing a DIY-assembly power supply kit for around 20 dollars instead of doing the small amount of research that it would have taken to know that there was a simpler and better method.
After assembling the kit, soldering the components in myself, I tested it with my new tubes, which for some reason were not the ones that I ordered. They were a smaller size, called IN-16 tubes, than the IN-14 tubes that I had purchased. My favorite size of Nixie tube is the IN-18 tube, which was used on the device that inspired my clock, however a set of six IN-18 tubes is over 200 dollars, as they are old stock from Russia and are no longer produced. The art of manufacturing them through the original methods was lost as well, with no remaining texts detailing the gas mixture, however there have been a few replica-candidates in the past few years who have reverse-engineered old tubes and developed new manufacturing methods, and who are now selling their superior modern tubes. If I ever attempt to make another Nixie-based device in the future, which I’m almost certain that I will, I plan to use IN-18 tubes because of their larger tube shells and much larger, easier to read numbers.
Once I realized that I had the wrong tubes, since IN-16 tubes are smaller than IN-14s, I contacted the seller and got the correct ones shipped to me. In the meantime, while testing the tubes that I had with my power supply, I found a few features that I didn’t like. First, the driver was limited to a 20-milliamp output current, which was only enough to drive a few tubes at the correct brightness level, and I desired for them to be bright. Second, and far more important to me, was the constant ringing that the relatively low-frequency voltage-transforming power supply produced. I’d place the tone to be audibly similar to a 14-kilohertz square-wave, as it sounded buzzy, which was a poor design choice in my opinion, and quickly becomes obnoxious. The voltage that the power supply expects as an input is between 12 and 18 volts, which I assume is filtered down to 12 and then switched repeatedly at that high frequency and driven through a transformer which converts it to 170 volts AC, where it is then rectified back to DC with a smoothing capacitor.
Nixie tubes require a high voltage to start them glowing, usually somewhere between 150 and 180 volts. I asked my electrical-engineering-major roommate if he had any ideas about driving the tubes with a quieter power supply or changing the frequency to be inaudible on mine, and he led me to full-bridge-rectifier circuits. He explained the basics to me and I set off to purchase the necessary components which were dirt cheap, as for about two dollars I purchased a hundred one-amp 200V Vrrm diodes with which to construct my rectifier.
At first, I didn’t fully understand the effects that adding a smoothing capacitor would have besides smoothing the voltage out, and I was convinced that the output voltage of my rectifier circuit would effectively be the 120-volt RMS output of the 120-volt outlet waveform, whose voltage would be too low to fire the tubes, which worried me. I eventually made the connection that the smoothing capacitor would draw extra current through the rectifier at first to bring its charge to the peak voltage in the waveform, which for a 120-volt RMS sine wave is 169.7 volts, almost exactly 170, the requested voltage of many Nixie tubes. Once the capacitor reaches that voltage, it very slowly discharges in wasted energy through the air and any imperfect circuit components, as the diodes prevent it from discharging, or since it’s an electrolytic capacitor, exploding.
When exposed to a load, like the tube-resistor combination, the capacitor supplies the voltage equal to the peak voltage of the wall-outlet waveform, 170 volts, slowly dropping as its energy is lost to the resistor and tubes. However, on each cycle of the 60-hz input to the capacitor, it pulls the current necessary to bring itself back to 170 volts across its terminals. This way the capacitor stays fairly steady near the peak voltage.
Tuning the capacitance value as a function of the voltage and expected resistive load of the circuit is necessary to balance the capacitor’s charging time with its ability to supply current to the tube without dropping its voltage too much between cycles, which would cause the current through the tubes and thus their brightness to decrease. Effectively, if tuned correctly for the circuit, the voltage stays very close to the peak voltage with a ripple-voltage of less than 10% of the peak voltage. The lower the ripple voltage is, the less visible the flickering will be in the tubes, in the case of driving Nixie tubes.
Armed with the knowledge of the structure and mechanisms behind full-bridge-rectifier circuits, I soldered one together on one of the many double-sided prototyping boards that I purchased around the same time on eBay. Since I still only had the IN-16 tubes that were sent to me by mistake, I had no qualms about accidentally damaging one electronically, so I tried driving it with my rectifier. My first rectifier circuit blew up the moment that its terminals touched those on the tube. At first, I chalked it up to improperly laying out the circuit or driving the tube backwards, but I was pretty sure that I had it right, so I soldered together another, sure to follow online diagrams this time. That one blew up as well, as did the following three. My roommate made one for me to test out using his own circuit components, in case mine were somehow all faulty, and his blew up as well. It was not until the seventh rectifier circuit that I realized what the problem was and moved to resolve it.
When the tubes fire, I had made the assumption that they draw their optimal operating current, which should be in the tens of milliamps. What I did not realize was that when the voltage across the gap between the metal cage cathode and the number anode exceeds the breakdown voltage of the gas within the tubes, the gas effectively acts as a wire, or a dead short, between the ground and high-voltage pins of the tube. This allows them to pull potentially a near-infinite current before blowing the driving circuit, which can only handle one amp, or the tube. To remedy this issue all that I had to do was to insert situationally tuned current-limiting resistors between the tubes and driving circuit, leading to an initial voltage across the tube of 170 volts and a current limited to around 4 milliamps.
The first method that came to mind when I decided to build the clock was to power the tubes through a microcontroller of some kind, which would need to be cheap, easy for me to learn to program, and have a sufficient number of input and output pins. My go-to solution was Arduino, since I knew that my clock program would be simple enough that an Arduino could easily run it, that they were cheap, and that I had used them before.
A small amount of research led me to the conclusion that the Arduino Mega was my best option of all Arduino models because of its large number of output pins, which I would need to drive the individual numbers on each tube. It still didn’t seem like an adequate selection to me, since the Mega had 54 input/output pins, which was not enough to supply for the six tubes each having 10 possible outputs, not to mention the other input and output connections for buttons and such that I might need to add. Luckily a solution existed for that problem in the form of a cheap integrated circuit called the k155id1, capable of driving up to 10 outputs at the high voltage necessary for controlling Nixie tubes. I bought a pack of six for a few dollars, one for each tube.
Soon after that I discovered that shields existed, which are essentially boards dedicated to a single purpose, like driving motors or communicating via Bluetooth, and can be plugged in temporarily to the multi-purpose Arduino’s input and output pins. This made designing my wiring circuit far easier than it otherwise would have been because once I eventually purchased a cheap prototyping shield, I had a whole board of available space to solder my components and wires to with direct access to all input and output pins on the attached Arduino.
Around the time that I got the shield and started mapping out my connections, I decided to get started learning to code in Arduino’s language within its IDE. This project occurred at about the same time as the door-lock project that I worked on with my roommates, which made both projects my first real introductions to the Arduino IDE and circuitry. Luckily for me, Arduino’s coding language feels extremely similar to C, which I am quite familiar with after my engineering courses taught me to navigate it, and because I worked on a few hobby projects involving C in my free time.
I was able to quickly whip up the logic part of the clock using for loops, if statements, the serial monitor for temporary display of outputs allowing me to troubleshoot, and the built in Micros and Millis functions for keeping track of time. I set up a for-loop to parse through every possible display option for each individual tube and update the binary output to the Nixie tube drivers, which took a four-bit input signal from four pins, to reflect the number that each was to display at a given time.
Pressing forward with the physical clock, I began purchasing all of the other required materials including small-gauge wire, the prototyping shield that I had selected which matched the Arduino Mega’s pinout, and the remaining resistors and transistors that the circuit required. I found that with the new Nixie drivers I now only needed to use 24 output pins rather than the 60 that I would have needed and not been able to supply, which left me with 30 open pins to potentially use for other purposes. This allowed me to individually control each of the four dots of the colon tubes, called IN-3 tubes, using transistors controlled by four of the leftover output pins, so that I could later add blinking functions if I wished to.
I added a small section to the code telling the colon tubes to wait to turn on until the program had started, which looked better in my opinion than having them turn on instantly and the tubes turn on later, as they now wait for the number tubes to display anything before turning on themselves which appears to happen simultaneously. After I had that completed, I added a new section of code to handle reading the values returned by a switch and two buttons, allowing the user to control whether the clock displayed in 12 or 24-hour time, and giving them the ability to change the clock time with momentary button presses.
Moving back to the clock itself, as all of the relevant code was in place before testing the interface and improving features could be performed, I decided the layout that I wanted the tubes to be positioned in and soldered them into place on three prototyping boards in groups of two. On the two boards that would carry the tube sets on the “outside”, for hours and seconds, I left a small space on the “inside” portion to which I soldered the IN-3 colon tubes. They were not powered by Nixie-driving integrated circuits, so I had to also solder their transistors and two resistors for each tube to the bottom of both boards, that would control the tubes individually.
I discovered that there were a few extra pins protruding from my Arduino Mega board in the center that interfered with the components that I was soldering to my Arduino shield, so I de-soldered those pins and removed them as they served no purpose. The same interference issue occurred in one other location on the Arduino with its two main capacitors, so I had no choice but to move them out of the way, as I had already made the poor decision to solder all of my Nixie drivers to the prototyping shield before checking that they wouldn’t interfere and was reluctant to remove and re-solder them. I was not using the built-in DC power jack for anything, as for the time being I was powering the Arduino through USB, so I removed it as well to make room for the shield.
To each of the 96 pins on the drivers, 60 outputs, six voltage-ins, six grounds and 24 input pins, I soldered lengths of color-coded wire that would later lead to the tubes and the locations on the shield paired with the Arduino’s output pins. The next logical step was to form those connections, to solder the other ends of the wires to their corresponding leads on the tubes and the communication lanes on the shield. Wire-stripping and soldering every one of those connections was a tedious task, but once completed, there was really nothing to physically work on besides designing a decorative frame for the clock’s electronics to reside in. I moved to Creo to put together a model of the clock, including potential designs for the base as well as the required features including the tubes and boards.
Much of my inspiration for the design came from clocks that I’d seen others make using Nixie tubes online, which as it turns out, is a very common thing that hobbyists do. I have found many online, offering to sell pre-made PCBs already designed to act as clocks, allowing people to focus on the design aspect surrounding Nixie tubes and get a free pass on the coding and wiring. Since I knew that I could code the clock myself, I was not interested in using anyone else’s circuitry or code, as I would have missed out on a great learning opportunity and it wouldn’t have felt like nearly as much of an accomplishment if my entire project was just decorating a circuit that someone else had designed and manufactured for me. Nevertheless, other people have taken that path, and there are thousands of images online of the clocks that people have made using Nixie tubes to display time. I found some that were quite appealing that combined wood and brass to form their structure, and I decided to follow a similar route after viewing them.
I was very much pleased with my first design featuring three layers of wood separated by two much thinner layers of brass, the brass polished and slightly rounded at the corners and the wood sections each shaped around the edges with a different routing-bit pattern. To narrow down the possible wood edge-patterns, I tried looking up routing bits, but ended up finding and using misleading bit cross-section images online that didn’t exist as purchasable bits. I later had to go back and modify the curves in my model to actually be manufacturable using existing simple-geometry routing bits.
I was interested in using a specific type of wood with a deep contrast in its grain that I had seen used for expensive decorative boxes in a movie, or at least replicating the look as best I could with high-quality wood, so I asked my uncle for ideas since he knows far more about wood selection than I do. He suggested that I look into rosewood to naturally achieve the grain and color that I was looking for, since I was against the idea of using a cheap wood and staining it to get the desired visual effect. I looked into rosewood and even attempted to buy a section at a nearby hardwood shop, but the planks that they had there were too small for my needs, and since trade of rosewood is currently illegal, they were all that they had left from before that change had been made. They directed me instead to the bloodwood that they had, called Cocobolo, whose dimensions were large enough for my needs, and which had a nicer color and grain pattern than the rosewood that they had in stock. I brought a very nice wood section home for half of the price of the rosewood and, with my uncle’s help and tools, cut the three precisely sized sections from it and carefully routed the patterns into their top edges.
Using my calipers, I marked out the hole locations onto the top wood section then drilled pilot holes and later widened those holes in 1/8-inch steps with wider bits to keep them aligned, and finally used my step drill bit set to bring the holes to their final 0.75-inch diameters, all on my drill press. I recently tried to cut the two brass sections from a single 1/8-inch thick sheet of brass that I purchased online, however in the hour that I spent cutting it with my piercing saw blades and jeweler’s saw, I was only able to cut through around two inches of material, broke a frustrating number of saw blades, and had trouble keeping the cut-path straight. Left tired of cutting by hand so carefully, and with so much left to cut, I have changed plans and now intend to use a scroll saw to slowly make those cuts, which will take away the manual load and hopefully make the cuts more accurate. I would be using a hacksaw if I could spare the material that I would lose, but at the moment that isn’t an option, at least for a few of the cuts.
Between when I modeled the clock in CAD and when I purchased the wood, I began testing the clock program’s interaction with the drivers and tubes. Since I didn’t want to risk damaging anything else if my wiring was incorrect in any way when plugging in the tubes for the first time, I used a different method to troubleshoot my code. I wired the Arduino to a breadboard prepared with colored LEDS, each color designated to a set of 4 output pins that drove each of the six Nixie drivers. This way I could visualize the outputs in binary to ensure that they were correct, and that time was being sent to be displayed correctly, exposing a few errors in my code.
To view every possible time that could be displayed, I modified my code to move forward one second every millisecond, which allowed it to pass through a full day in 86.4 seconds. I found a few pins that weren’t operating at all, which let me easily move wires on the shield and select different output pins since I had so many available.
Eventually I got up the courage to plug in the clock for the first time which I feared doing mostly because of the chance that it could fry components if I had anything wired incorrectly. I was thoroughly convinced at that point that I had done enough troubleshooting though, and that the connections were all fixed and no unintentional shorts were present. I plugged it in for the first time, hopeful that everything would work perfectly, and was moderately pleased to find that nothing fried in that moment, however there were a few problems.
The first thing that I noticed was that my second tube from the left had trouble displaying anything while it was supposed to be displaying a two. Similarly, the rightmost tube, displaying the ones digit of seconds from 0-9, struggled to display the top half of the number six. I had not realized that there would be this many quality-control issues with the tubes, especially after I had purchased them from a source that assured me that they had all been tested, but I took great care of my tubes while working with them, so I am certain that they were broken before I received them. I had no choice but to order a few new tubes since the defects that they had made the display both unusable and an eyesore. I also discovered that I had wired my colon tubes backwards, which was no real problem, so I switched where the wires led, quickly fixing that problem.
I was interested in finding which of my tubes were fully functional and which had defects, so I could either replace them or move the ones with defects to locations where those defects would never be seen, since the tube on the left was only ever expected to display 0-2 and the third and fifth tubes from the left only ever used 0-5. To do this I threw together a simple new program based on my clock program’s display function which simply parsed through the numbers from 0-9 and printed the same number to all tubes and updated twice per second. After watching the tubes parse through the numbers, I recorded which tubes could display each number well and which had defects, then decided where to move them to in order to hide their defects or whether to simply replace them. I swapped those tubes to their new locations requiring that I disconnect and re-solder at least half of the tubes and their wires.
When I received my two new tubes in the mail and tested them, only one worked at all, the other didn’t display anything no matter what number I tried displaying, so I was shipped another that I had to wait another few weeks to receive. This set the project back a bit but was a worthwhile purchase, since it fixed nearly every display issue that the clock was having.
Sadly, I then made a small but important mistake that required a lot of work to fix. I plugged the Arduino into the computer, then plugged the tubes, which shared a ground with the Arduino, into a separate wall outlet. The discrepancy between the two ground-voltages sent current through areas that shouldn’t have much current flow when the ground shell of the USB cable came into contact with the ground shell on the Arduino USB port. The discharge led to the explosion of my full-bridge-rectifier circuit and a few SMD resistors and capacitors connecting the USB port to the Arduino, which led to failure of the USB communication between Arduino and computer. As a result, I had the choice between buying, waiting for the delivery of and modifying a new Arduino Mega, which would have taken a large amount of time and put the project on hold, and figuring out how to upload and test my programs without using USB.
After some digging on the internet I finally found what seemed like a viable solution, suggesting that I use another Arduino board to interpret the USB commands and send them to the target board. This was only achievable because of an on-board FTDI component that Arduino boards utilize which converts the USB signal to serial information, then sends the serial data to be read by the Arduino processor and interpreted. By either removing the ATmega processor of the second Arduino board, which can be done on many Arduino Uno versions and Chinese knockoffs with DIP rather than SMD processors, or by wiring the ground and reset pins together, the communication between the FTDI circuit and the processor can be cut off, effectively transforming an Arduino board into a free FTDI breakout board.
The FTDI Arduino’s output pins GND, Tx and Rx can be wired to the same pin headers on the broken Arduino, GND, Tx and Rx, as well as pairing the reset and 5V connections to provide power to and synchronize both boards. When the Arduino IDE is commanded to upload a program to the FTDI Arduino, which is connected via USB to the computer and recognized as a unique COM port, it sends the compiled code through the FTDI circuit on that board. The circuit converts the information to serial data and sends it through its Rx and Tx pins to the broken board, which sees the information the same way that it would have through its own USB and FTDI circuit, had they been working.
Effectively, using this method, one can upload new programs to broken boards, which I was able to do for two years and troubleshoot my clock before my first Arduino Mega finally died in a different way. I’m not sure what led to its failure but the problem that I experienced was that the processor on the Mega would heat up very quickly and would never actually begin running the program, then repeatedly reset itself indefinitely. I found no solution to this problem besides guessing and checking, removing potentially broken circuit components and replacing them until it worked again, but for an eight-dollar board this seemed hardly worth my time.
I instead purchased a second board and made the same changes to it that I had made to the first, but more carefully, and it worked perfectly with the existing code. Before the first board finally died, though, I essentially got my clock’s code into a working state and didn’t need to perform much more troubleshooting. I had plenty of working diodes left over after the USB fried so I quickly re-soldered the rectifier circuit, however I then discovered a new problem with the circuit that took far too long to repair.
I had originally made the poor decision to solder my DIP 16 nixie drivers directly to the prototyping Arduino shield. Being a novice in circuit design, I had no idea at the time that static discharge could easily fry my circuit components, which it certainly did. Two of my Nixie-driving ICs no longer functioned at all, and their tubes displayed all numbers simultaneously when they were plugged in, meaning that they had failed closed. This meant a whole lot more de-soldering of components for me as I needed to remove the failed drivers and replace them with working ones. I used this opportunity to improve my design and opted to instead solder a DIP 16 socket to the board to allow me to easily remove and replace failed packages. By the end of all of my static discharge mistakes thus far, I have had to replace four packages, and thus also added four sockets to the board. This added more frustrating interference issues with the Arduino board below the shield, but I was able to move the capacitors that were in the way further away, resolving that issue.
After I replaced the drivers and tubes, everything functioned properly, and all that remained was to complete my code and combine the electronic components of the clock with the decorative enclosure. I’ve spent many days-worth of my free time sifting through my code, improving efficiency and simplicity and adding features, and now I finally have it at a point that I can be satisfied with.
Integrated-Timing Clock Code
When I began, I attempted to use the Arduino’s Millis function, which reports the unsigned integer value storing the number of milliseconds since program start, to count and display time. My thinking was that after every 1000 milliseconds counted by the Arduino it would roll to a new second and update the display and the stored time value. I was very excited when this code worked but quickly became disappointed when I saw how quickly my Arduino’s concept of time diverged from the accurate time kept by my phone and computer. Every 129 seconds, consistently, my clock would wrap around to be a full second behind real time as reported by the computer, which I had displaying the real time next to the serial monitor. This error must be due to an imprecise resonating frequency of the 16-MHz quartz crystal oscillator, which is likely slower than 16 MHz but designed to believe that it is exactly 16 Mhz.
The solution that I came up with was to determine the divergence factor between the two rates of time, the one that the Arduino experiences and real time based on my computer which tracks internet time, and use it to reverse the effects, effectively speeding up the time-trigger-rate of the Arduino. I wrote two programs, one to run on my computer and one to run on the Arduino, to view side by side. Both were designed to display the number of microseconds since the program began on the screen as often as possible in a simple unending while-loop. I wrote a third program, a batch script, to take screenshots of my computer screen once per hour and save them so I could come back later and enter the data into a spreadsheet. If I was able to read serial data from the COM ports through a batch script back when I performed this test I would have instead read both the computer time and the COM serial input from the Arduino in one program and printed the values to an output text file, but I learned that trick rather recently, so manual data-entry was the best that I could do.
I ran this program for 10.8 days straight and entered the data into an Excel spreadsheet, then analyzed the resulting data by plotting and correcting it. I discovered that twice during those ten days, my computer changed its time, which I believe can be attributed to the operating system correcting its stored time with internet time, adjusting it by slightly less than two seconds each time. I made linear corrections using Excel functions to these time-shifts in my data to correct for the error that they introduced to my graphs which took the form of sharp jumps in an otherwise continuous trend.
In the end, I made two separate graphs to visualize the divergence, the first of which tracked the divergence factor as it converged on a final value, taking an average over time, making the last value recorded on the graph the most accurate prediction. I found a much better visualization technique though that better allowed me to pick a final value, which I used in my second graph, instead tracking the difference between the two clocks’ reported microsecond values and applying a linear best fit trendline to the plot. The predicted final divergence in seconds between the two as predicted by that line divided by the number of seconds that passed over the entire data-collection period gives the divergence factor in seconds per second. I found that with that Arduino Mega, I had to multiply the number of microseconds that it believed had passed by the factor 1.00777660548660 in order for it to represent real time as closely as my 10.8-day trial results could predict. This was a great step forward but opened up a few new problems for me to solve.
First, after having learned the limitations of certain data types in my Numerical Analysis class, I discovered right away that I couldn’t use a floating-point variable to store time. The 32-bit number architecture of my Arduino would not be able to handle double precision 64-bit floating point numbers, and after 194 days, the number of seconds in storage would begin to be rounded to the nearest even digit, diverging in level-of-precision from then on. This would decrease the accuracy of the clock’s time-storage capability past that point which I was not interested in affecting, and would mess with the time-display, so I had to run with a 32-bit unsigned long integer to store elapsed time.
Having made this choice, I then had to determine how to perform lossless math functions on the stored time value when multiplying it by my correction factor, which needed to be as precise as possible. I made many adjustments to my code that I thought would solve the problems that it had with storing the correct time, but after looking back, I saw that most of them ended up causing more harm than good due to misunderstandings of math actually occurring between different data-types versus the math that I was expecting, so I returned those to how they were originally.
Using an unsigned long integer to store time in seconds presents a problem, as the maximum number that can be stored in its 32-bit mantissa, and thus the largest perfectly-precise number that can be stored on a 32-bit Arduino, is 4294967295. If I store time in seconds in this integer, it will be able to keep time for 136 years before overflowing and starting back at 0, potentially causing issues with the clock. However, 136 years is so far off that the likelihood of it being plugged in for that long makes solving that issue a bottom priority. However, if I wish to multiply it by a highly precise number as is the case in my code where I’d like to multiply it by 0.0077766054866001830434688829377 in order to adjust the stored time, things become more complicated.
The problem with a number containing so many digits of accuracy is that it can’t be stored at the necessary accuracy level to contain them all, much less allow math to be performed with them, especially not on a single-precision 32-bit Arduino. Sacrifices have to be made in this case, since a 32-bit float can only store this number to a precision of seven digits of accuracy with an eighth estimated digit, yielding 0.0077766052. Using this flawed factor for multiplication of the Arduino’s perceived time adds an additional error of +- one second between 68.4 years and 136.8 years after the clock is turned on, plus another 1.16 seconds per year, an error that I can easily live with, to whatever other error there is in the factor and resulting from environmental variables.
There is still one large problem, however. Time, as far as the display on the clock is concerned, only consists of seconds, and doesn’t care about microseconds. So, if I were to store the number of seconds since the program began and print an updated time whenever it rolled to a new second, as a result of the number of seconds being stored in an unsigned long which truncates decimals when math is performed with it, every 129 seconds the clock would visibly skip a second. This method of time correction, multiplying an integer by a float decimal with a new integer output, while successful at making time stored and kept by the Arduino as accurate as possible, does so in a way that visually flows quite poorly.
I didn’t like that the flow of time was seemingly interrupted so often, so I moved onto a new method of displaying the time that had an independent section for display from correct time storage, making rare interactions with the real stored time value. I used the factor to calculate how many of the Arduino’s perceived microseconds should pass within each real second, ending up with 992233.063 microseconds per second. With this, instead of updating the clock every 1000000 Arduino microseconds, I could use the new number of Arduino microseconds to effectively speed up the Arduino’s perception of time when updating the display. The Arduino would overshoot this value by sometimes up to a millisecond, but it would very consistently average out at some value, so again using Excel I fine-tuned the value that the Arduino would check for, starting with 992233, until the resulting time that took place between display updates was, on average, within one microsecond of the target value.
Since the Micros and Millis functions overflow back to 0 after 4294967295 micro or milliseconds, or every 71 minutes and every 49 days respectively, a problem arises that can grow over time, within the variable storing seconds. That variable which I named “T” is supposed to directly track the value of Millis, but to cover the event when Millis returns to 0, I had to code a small if statement to realize that rollover and deal with it accordingly. As soon as the rollover is detected, the code is set to compensate for the lost 295 milliseconds that elapsed at the end just before Millis rolls back to 0, as well as adding the product of the number of times that Millis has rolled over since the program began and 4294967, the number of seconds that occur in full before Millis rolls over each time. If those lost 295 milliseconds or microseconds are not counted, the error that they introduce grows over time, so I thought that it would be beneficial to solve that here along with dealing with rollover.
I added another variable to my code that stored time in seconds as well, however, rather than tracking real time, it stored the amount of time since the last moment when its value exceeded 86400 seconds, at which point it returns to 0 and triggers the clock to update the time displayed to match the true stored time. Once each day, where a day consists of 86400 seconds, the clock makes this correction, and if they disagree, any error that might have accumulated disappears all at once, which is very unlikely to be noticed.
Most of my code debugging and testing, especially when trying to figure out how the math between variables of different types would be performed, was done by displaying numbers on the serial monitor. For quite a long time my code was filled with commented serial-print statements that cluttered it quite a bit, which I finally removed once everything was working as intended. The one problem that using a lot of serial-print statements added was the lag that it caused in my loops, drastically decreasing the number of times the program would loop through per second when it had to rapidly send out serial signals. The more that I had, the more likely it was that the program would overshoot my designated microsecond display-update value, making my testing slightly less accurate. I minimized these in my final tests to maximize the accuracy of my results, which were to be used to determine the permanent factor constant.
One very useful feature in coding that I utilized multiple times is the modulo operation which can be combined with truncating division to pick out specific groups of digits from a known number. If time-of-day is represented in seconds, one can filter that through a modulo operation that leaves the remainder after dividing it by 60 which will yield the seconds elapsed past the current minute of the day. If the minutes in the current hour are needed, one can divide the day-time in seconds by 60 (if it is stored as an integer and yields an integer), which truncates seconds off of the end, then perform modulo to the result, which chops off every full hour. Simple division can be performed on the time-of-day seconds value to obtain the number of full hours elapsed, as by dividing by 3600, both minutes and seconds are truncated. Similar modulo and division operations can be separately performed on the resulting hour, minute and second values to split them further into their digit-counterparts, allowing those digits to be sent individually to be displayed by the tubes.
To handle allowing the user of the clock to adjust the time, I had a fourth prototyping board set up with two buttons and a two-position switch wired to some input pins on the Arduino together with pull-down and current-limiting resistors. At first, I didn’t have pulldown resistors on my momentary buttons, but when I tested it with that configuration, I found that the pins reading the button voltages would sometimes rapidly switch between high and low states thus messing with the time, so I added resistors pulling those pins to ground when they were not activated. In fact, even after I added the pulldown resistors, if I accidentally placed a finger on the contact monitoring the button-press state, the Arduino would interpret the voltage that it detected as a state-change and rapidly change the time.
I got some fun video recordings of that occurrence both with hours and minutes, with the clock changing hours or minutes at around 60 updates per second. I think that I can replace the pulldown resistors to a lower resistance value, which should counter that detection, but for now I’ve just placed tape over the contacts. In the final version of the clock they will not be exposed to the outside either way, and they never trigger randomly anymore, so I may not need to do anything else about that problem.
When the switch on the new board is flipped to one side, the clock changes the value of a flag variable to a 1, and any press of the two buttons will increase the minute and hour values. When the switch is flipped to the other side, the flag is changed to 0, and pressing the time changing buttons will instead decrease hour and minute values. To prevent the necessity of a third button with which to set seconds, I added code that would reset the seconds in the current minute to 0 whenever the minute value is adjusted, giving the user some kind of control over precise time without the use of another circuit component. I also set up the button-pressing mechanics so that when the button was held down rather than tapped, the time would change at a rate of 2 times per second, and after a few seconds of holding it down that frequency would increase to 6.7 times per second.
With all of the mechanics functioning as intended my only remaining tasks were manufacturing and assembling the base and ensuring that the clock held its time well with the divergence-correction factor that I selected. However, although the factor worked quite well to keep the clock near true time during the winter, losing only around 7 seconds in one month which was better than I had expected to see, that certainly did not carry over into the summer.
When I originally collected my 10 days of time-divergence data, I noticed a strange but very interesting trend, where the graph of the difference between my computer values and the Arduino Micros values would very clearly peak and valley once per day, though by only a tiny fraction of the total divergence. My first thought was that it had to be a function of the temperature in the house, since that was the only factor that I believed might influence the quartz oscillator frequency, so I checked the graph. Sure enough, multiple events on the graph supported my theory.
Using the locations of the peaks and valleys on the time-progression axis, I calculated the amount of time that passed between each peak and between each valley and found that it represented the length of a day consistently to within 20% of a day’s 86400 seconds. The graph would inflect, changing from increasing to decreasing, somewhere between 11 AM and 1 PM each day and begin to rise again somewhere generally between 12 AM and 4 AM. It can be calculated retroactively that the sun rose at 6:43 AM on the day that I began my data-recording, Sunday, November 5, 2017, and set at 4:56 PM. I’m sure that cloud cover, possible weather conditions like rain or wind, and temperature-change lag play a role in the temperature in the house and not just time of day, based on the strength of sunlight.
Overlaying those sunrise and sunset times on my data shows that sunrises occurred on each day at the peak of the derivative function drawn from the divergence graph, when the clock time was at its fastest, and that sunsets occurred at the derivative valleys, or when the clock was slowest. If I assume based on the original and derivative data that temperature instead tracks the highest sun-strength, at around noon, and coldest part of night, between 5 PM and 7 AM equaling 12 AM, those two times line up quite well with my graphs. The peaks of the divergence graph, and thus the inflection points on its derivative graph, occur predictably at those times.
On two separate occasions during that 10.8397 day stretch, the furnace was turned up quite high during the day by somebody in my house, which was visible as well as discontinuities in the peaks on the graph. I was convinced after working this all out that temperature was my problem. Counter to my expectations, and yet evident in the data, was the trend that my Arduino clock slowed down during the day and sped up at night, giving an apparent decrease in time-rate with an increase in temperature, and an increase in time rate with a decrease in temperature. I would expect that an increase in temperature and thus the kinetic energy of the crystal would increase its rate of oscillation, but what do I know. Maybe there’s another mechanism that I neglected to consider in my divergence graph and its derivative, or a huge amount of temperature-lag between solar flux hitting the earth and the Arduino experiencing the resulting temperature change.
When I came back to this project and ran my clock for one week during the summer, plugging it in freshly with no bias from previously accumulated error, the factor that I had selected during the winter was no longer good at all at describing the average of the temperature fluctuations occurring during the day combined with the error in the quartz oscillator’s frequency. In that time alone, it diverged by 30 seconds from my computer’s clock, which is far too much error for a useful clock to have. As I saw it I had two options, and one was far too involved and potentially unrewarding for me to truly consider. I was afraid that If I bought a temperature sensor and wired it up to the Arduino then developed my own curves for it to steer the quartz oscillator time, it would be too complex of a relationship for me to map with the knowledge and tools that I had.
DS3231 Real-Time-Clock Code
I finally gave in and went with the easier and far more rewarding option of purchasing a two-dollar module online which was reported to be a highly accurate time-keeping Arduino-compatible device with a built-in battery to prevent loss of time-keeping when the Arduino is unplugged. I was satisfied that I at least narrowed down the issues with my clock as far as I possibly could and was able to dig as deep as I did into the underlying mechanisms, finding subtle temperature curves buried in the rate of “time-passage” on a clock. I now know this to be true because following the arrival of my DS3231 module in the mail, the time-keeping component, I researched its specifications only to find that it had a thermometer built in and was designed to track temperature for use in correcting its oscillator’s time-update-frequency.
Much of my code had to be rewritten with the addition of the module, but at least with its addition I could be confident that the clock would track time well and didn’t have to use the factor that I went through so much work to determine, which ended up producing its own inaccuracies anyway. Time-keeping was no longer reliant on the quartz-oscillator in the Arduino Mega once I had rewritten the code, however I allowed it to continue to control the display updates, since I discovered that the DS3231 would overshoot a second by a millisecond every few tens of seconds, which I disliked. Using a consistent display trigger, updating the display with a consistent overshoot of between 10 and 15 microseconds, and adjusting that trigger value by the average overshoot from 992233 to 992220 microseconds, gave an average error of only 1.17 microseconds per second overshoot, far less than the DS3231-overshoot. This way, if the display pulls from the DS3231’s stored time every time it updates the display, the display should skip a second once every 9.815 days on average, assuming a moderate level of consistency in both quartz oscillators.
An added bonus to the proper time keeping that the DS3231 offers, on top of its temperature-reporting capability that I plan to utilize, is its ability to store and adjust the date, with a built-in concept of leap years. Both time and date continue to be updated while the module is isolated from the 5-volt input from an Arduino, so power outages or events where the clock is moved will not affect it, and it can pick up where it left off when plugged back in. The one thing that bugged me slightly was its inability to perform daylight-saving time adjustments, which meant that coding the math involved in determining when to trigger DST was the next step in building the clock. This included both tracking the time whenever the display is updated and checking to see if the month, day of the month, weekday, hour, minute and second matched the conditions needed to trigger daylight saving time on and off, and preparing the clock so that when it is plugged in with nothing but the date and time to work with, it can figure out the correct starting state of daylight saving time.
The first task was easier than the second was to complete, requiring four simple if statements asking relevant questions that confirm whether or not time has recently rolled past the trigger-point for activating or deactivating daylight-saving time. There are two rules governing DST; The first is that in March, if the day of the month is between the 8th and the 14th inclusive (during the second week), the weekday is a Sunday, and if the time rolls forward past 2:00 AM, DST is deactivated. The second says that in November, if the day of the month is between the 1st and 7th inclusive (the first week of the month), the weekday is a Sunday, and if the time rolls forward past 2:00 AM, DST is activated, creating an on-off toggle system between the two. These rules are covered by the first two if statements that I wrote, however I have two more to cover the cases in which the time is adjusted by the user and rolls backwards past either trigger point, ensuring that the clock realizes that the DST status flag reflects every possible case.
The second task required deeper thinking, and a lot more code, starting with changing the first action that the clock performs after initializing variables each time it is plugged in to be a check of the current time and date stored on the DS3231 module. Using the current date and time, the Arduino checks what the current month is in two independent if statements. These if statements need to cover every possible combination of months, month-days, weekdays and times.
The code then passes the month day and weekday through a function that I designed to determine exactly that. I used Excel and its conditional formatting to fill cells with a certain color depending on their respective values, using guess-and-check and tweaking my function as I went. I made modifications from my original guess at a useful function until, in every possible week scenario which I had mapped out in a 7x7 array, every cell marking a day that occurred after Sunday during that week lit up red, every Sunday lit up green, and every other cell remained white.
The function that I ended up with for November is if (15 – day-of-month – (7 – weekday) <= 0), then the present day is after daylight saving time was toggled on, paying no mind to Sundays which are represented with zeros since they are already covered in the other if statement, and if the function returns a positive value then the current day is before daylight saving time was toggled on. Similarly, in March, if (8 – day-of-month – (7 – weekday) <= 0), DST has already been toggled off, and the result is a positive value then it has yet to occur, again with Sundays represented by zeros already covered by a previous if statement.
The combination of these statements covers every possible date-and-time possibility that could occur and allows the clock to set it DST flag to the correct value the instant that it is plugged in, preventing it from performing the wrong action when either DST-toggle day occurs. I know that this can be an issue because, while I was testing my clock running through the March 2019 DST-switch, it rolled forward multiple hours instead of just one. When I checked the code, I realized that I had initialized the DST flag to be off when the clock is plugged in, however I plugged it in during a time when DST was active, and it performed the wrong action when it came upon 2:00 AM on March third, then got confused in a toggling match between conflicting if-statements that should never be simultaneously triggered. Once I added my new code to properly determine the DST state I tested the possible rollover dates multiple times to ensure that there would be no future issues, as well as testing my time-changing buttons to see if they handled changing or not changing the DST flag as intended.
One final bit of math that I coded into the clock took the date from the DS3231 and converted it with integer math to the Julian number representing the current Gregorian date to be manipulated with my date-changing clock functions. It allowed the clock to be able to determine on its own what the day of the week is based on the date alone, which is helpful when manipulating that date and trying to send all information engrained in it back to the DS3231 module for storage and usage.
Rather than tracking when adjusting the time progresses the stored weekday from a seven to an eight, where eight is not a useable integer representing a day of the week and should wrap back down to zero, I can simply apply a modulo function and set the day of the week to be the remainder of the Julian number mod seven, whose remainder is always less than seven and non-negative. It makes changing date and time much easier to code for in terms of both week and weekday and changing the date just by adding or subtracting from the Julian number, then running it through a function to be converted back to a Gregorian date. I did a lot of research on this topic and its integer-math requirement, yielding incorrect results if truncated integer values are not used after each division operation. This was somewhat difficult to solve in Excel, but I discovered the “floor. math” function which truncates decimal values following a calculation.
With all coding dealing with daylight saving time, date storage, and handling the DS3231 module completed, the final change to the code that I wanted to implement was to make it more efficient both to put less wear on the Arduino’s processor and to increase the number of times per second the program looped through its contents, thus decreasing the uncertainty in the display-update overshoot. I did this by adding if statements into sections that were performing unnecessary actions on every single loop, such as math, complex variable and function calls and variable-reassignment that was being executed regardless of whether it needed to be or not.
The if statements that I added, which checked if the conditions requiring those actions to be performed were met, and the reordering of certain conditional tasks, placing the least-likely-to-be-true if statement on the outside of other nested if statements and working in with increasing likelihood, drastically improved the loop-cycles per second. Where previously I mapped out the overshoot during every loop in an Excel document and found that it was sporadic, ranging in value from 0 to 2 milliseconds away from the target value, when I plotted new values after my reorganization, they landed consistently within 20 microseconds of the target value with an average overshoot of only 13 microseconds. This predicts that the loop cycles through a maximum of 77,000 times per second, where previously it could barely manage to loop through 700 times per second.
The improvement in both accuracy and precision made it possible to predict the average overshoot very precisely and use it to redefine the desired target value to include that overshoot, making it possible for the clock to display the correct time for a maximum predicted 10 days before skipping a second. I am very proud of minimizing that error. In the end I may switch to a different display-update method, instead checking if the DS3231 value differs from the currently displayed time and updating the display time whenever that occurs, as I probably should have done from the start, but I like the unnecessary precision of the method that I instead decided to use. Otherwise, using the module to update time directly not only requires repeatedly checking its stored value thousands of times per second for no reason, but also has the frustrating imprecision of skipping ahead one millisecond every 20 seconds, which I assume is its internal method of temperature-based time correction.
All that I am waiting for now is to find a better method of cutting the brass sections that I purchased which will most likely end up being somebody else’s scroll saw, then I can finally begin drilling the final fastener holes into the frame and electronic components. I will need to isolate the brass sections from one another and from the electronics if I plan to pair them with capacitive touch-sensors later on for the purpose of changing clock modes or triggering programmed events, which should be easy to do with plastic or rubber sleeves covering the small wood screws that will fasten everything together. I have the holes mapped out already in my Creo model of the clock so all that needs to be done before actually drilling is for me to mark the holes with my calipers, as I did before I routed out cavities in the wood sections into which the electronic components are intended to fit.
After the holes are all drilled I will apply a clear acrylic coating to the wood alone, assemble everything piece by piece and connecting the circuit boards to the wood in their final rigid positions, then begin to code my buttons and switch for date adjustment on the DS3231 and for the brass sections to communicate with the Arduino via capacitive sensing.
The diversity of the work-related components of this project, from woodworking to metalworking, programming, performing data analysis and soldering electronics together in circuits that I designed, paired with the beauty and elegance that the final product is taking on has made It such an enjoyable project for me to work on. I honestly believe that anyone wishing to challenge themselves and build a beautiful, functional electronic device to their own standards will have a great time doing it, learn a ton about the involved processes, and end up with a useful final product that they can be proud of. I greatly appreciate all of the help that has been given to me in my clock-building endeavors thus far and will carry everything that I learned from my mistakes and long periods of problem-solving thought into my future projects. This is the kind of work that I live for. It is hard and long work, but it can rarely be called tedious, and always has my full attention.
If the month is not March or November, the code asks if the month is after March and before November or after November and before March and decides DST from there. If it is March or November, the if March and else if November statements will proceed to ask if the date is between the 1st and 7th in March or the 8th and 14th in November. If it is not, determining the DST state is easy and the if statement assigns the flag variable accordingly using the day in the month. If it is within those dates then it is possible that the current day is DST trigger day, that it is yet to come during that week, or that it already passed, so the next if statement asks if the current day of the week is 7, indicating Sunday. If It is, DST is triggered at 2:00 AM that day, so the next if statement asks if it is before 2:00 AM, non-inclusive. DST is assigned accordingly depending on the month and the result of the time-check. If it is not before 2:00 AM, one more test has to be performed to find out if, during that week, the Sunday during which DST switches has or has not yet occurred.