Projects Research & Design Hub

Build a Self-Correcting LED Clock

MCU-Driven Art

In North America, most radio-controlled clocks use WWVB’s transmissions to set the correct time. WWVB is a Colorado-based time signal radio station. Learn how these two Cornell students designed and built a prototype of a Digital WWVB Clock. The project’s main components include a Microchip PIC32 MCU, an external oscillator and a display.

In this article we explain how we designed and built a prototype of a Digital WWVB Clock, based on the design of the building at 200 Water Street in Lower Manhattan (Figure 1). Jason has always wanted to pay homage to this clock, as he finds the design inspiring. In this, our final project at Cornell, we were determined to utilize the same design to create a functional piece of technology that adds color, vibrancy and time to any room.

FIGURE 1 – Digital WWVB Clock at 200 Water Street, Manhattan, NY (Photo Credit: Noel Y. Calingasan).

Making a clock requires a time base, a control circuit and a display. We implemented the time base and control on a Microchip PIC32MX250F128B microcontroller (MCU), using a development board built at Cornell University [1]. The clock has an external oscillator keeping time with an accuracy of ±1 minute per year, and a WWVB receiver for setting the time whenever the signal can be received. Time is displayed on a panel in front of 72 Adafruit DotStar LEDs. The clock takes input from three push buttons that select the mode of operation: Time, Date and Temperature. Additionally, because the project was inspired by a piece of visual art, we put extra effort into making it aesthetically pleasing, using wood instead of cardboard, and by eliminating the leakage of light between the LEDs by creating an individual housing for each one. Figure 2 shows the full electrical schematic of the clock and peripheral devices.

FIGURE 2 – Full electrical schematic of the clock and peripheral devices

We built the housing out of a combination of plywood sheets and softwood planks. The front panel was a piece of quarter inch plywood that was laser-cut at Cornell Rapid Prototyping Lab (RPL) [2] to represent the numbers. It was later drilled from the front and the back to install the buttons. The back face was a piece of quarter inch plywood that was drilled in the back for the ventilation of the power supply. The sides of the frame were 0.5” thick pieces of softwood, drilled on the bottom to hold the power supply in place, and on the left for the 120 VAC cord path.

We also secured the AC cord—consisting of line, neutral and ground cables—with zip-ties on either side of the left wall. We did this to ensure that if it were pulled, it wouldn’t come out of the power supply—which would cause electrical damage and endanger the user. We hot-glued the LED strips to the back face. To create a housing for each separate LED light and prevent light from leaking into other numbers, six long horizontal ribs were made out of quarter inch plywood and cut on a band-saw to fit into the box. Sixty-six short vertical ribs were laser-cut from pieces of leftover quarter inch plywood. The ribs were fitted to house the lights and hot-glued in place (Figure 3).

FIGURE 3 – Shown here is the design for the front panel of the clock. Dimensions were chosen to fit the LED strips and power supply.

We chose DotStar LED strips for their brilliant and vibrant colors. They had the added benefits of being easy to program and having good documentation, though they were somewhat costly. We chose strips that had 30 LEDs per meter, to space-out each LED in its own box. The DotStars support 8-bit RGB color, which allowed us extreme flexibility for color choice. Their implementation was complicated, as we had to solder them with short wires, based on research of the possible problems associated with using longer wires between cut strips.

We had the clock numbers displayed on the face of the clock ordered from left to right, top to bottom, wrapping around from the right end of one row to begin at the left end of the next (Table 1, red). However, we couldn’t have long wires stretching across the face, so we wrapped the LED strip in a “snake” pattern around the box, (Table 1, blue). Another good reason to use the DotStars was for their simple and reliable communication protocol via SPI. DotStars require 5 V power and 5 V command signals. Powering them was not an issue, because all our other components also used 5 V. To drive them from the PIC32 MCU, we used a voltage level translator to convert the 3.3 V PIC32 MCU signals to 5 V.

TABLE 1 – LED indexing. The clock digits are in red, and LED index numbers are in blue. LED indexing began next to the location of the PIC 32 and “snaked” upward.

We purchased a 25 W single output power supply made by Mean Well. It takes 120 VAC from a wall outlet and outputs 5 V DC. We chose a 25 W power supply to provide a maximum of 5 A of current to power the DotStar strip, which can pull a maximum of 4.32 A.

The SPI protocol used by DotStars transfers 32-bit LED data frames. The first frame is 32 bits of 0’s. Each subsequent LED has its own data frame, in which the first 3 bits are 1, followed by a 5-bit value for the intensity level, then an 8-bit value for blue, green and red. The last frame sent is 32 bits of “1”s, which indicate the end of transmission. A visual representation of the protocol is shown in Figure 4.

FIGURE 4 – DotStar SPI Communication Protocol is used to light the LEDs. Good documentation was part of the reason DotStars were chosen.

We used code from Bruce Land’s Pixel Strip Displays example. The code contained a working SPI function to communicate with the DotStar LED strip of specified size and a data structure, to format the color and intensity information of the LEDs. The SPI channel also outputs square wave clock signals for the SK9822 control IC in the DotStars can synchronize with the SPI. Once the LED data array is filled, write_pixels() is called and the DotStars are set.

Before write_pixels() can be called, the LED data array needs to be filled. We used three functions, time_to_led()date_to_led() and temp_to_led() to display the time, date, and temperature, respectively, on the DotStars. For time_to_led(), parameters for the hour, minute and second are sent. Next, we calculated which LEDs are the hour, minute and second and wrote a unique color value to each of them, while giving all the unused LEDs a different color to form a background. The function is date_to_led(), which is identical to time_to_led(), except it only takes parameters for month and day. The LED for month is set the same way that hour is set, and day is the same way minute is set. We blacked out numbers 00 and 32-59, because those are not possible dates.


Advertise Here

The last LED function is temp_to_led(), which receives a single parameter for temperature. The LED for temperature in degrees Celsius is then set the same way as minute and day. An added feature of the temperature LED function is that the temperature “flickers” between the integer values to indicate the value that is nearest.

We used a 32.768 kHz oscillator to implement the Real Time Clock and Calendar RTCC functionality (Digi-Key part number DS32KHZS#-ND). This specific value is convenient because it is exactly 215—in other words, 215=32,768. Therefore, it makes a 16-bit timer overflow exactly once a second. The specifications of the oscillator indicate that its error is only ±1 minute per year while operating under moderate climate temperatures (0 to 40°C). The oscillator outputs a 5 V square wave, which is connected in series with a 620 Ω resistor to pin 12 on the small board. Despite being a 5 V signal, the current in the oscillator’s output is only 150 µA. We decided not to use a voltage divider or level shifter to guarantee the fastest rise time.

The RTCC peripheral on the PIC32 MCU is used to keep the time and date. It keeps accurate time using an external 32.678 kHz crystal oscillator. Without the external oscillator, RTCC would need to use the internal oscillator, which is not accurate. In the config.h, we added #pragma config FSOSCEN = ON so the PIC32 knows to use the external oscillator. The RTCC peripheral is initialized in the main() function.

We wanted to add more functionality to time/date keeping. We settled on temperature because it is easy to display room temperature in degrees Celsius using the minute boxes. We used a Texas Instruments LM35 analog temperature sensor to read ambient temperature. The LM35 uses a linear scale factor of +10 mV per °C, and it has ensured accuracy of 0.5°C at normal room temperatures (25°C).

User input is received via three buttons connected to the development board [1]. When depressed, the buttons shorted the PIC pins to VCC through a voltage divider 680 Ω/1,680 Ω from the 5 V provided by the power supply, resulting in approximately 3 V. A protothread [4] was used to “debounce” the buttons, to make sure that the input is read correctly. Every 30 ms, the buttons were polled to assess whether they had been pressed. The pins were set as digital inputs. Values from the previous poll were also kept to implement the logic as shown in Table 2. Three buttons were implemented in this manner. The debounce functionality ended up being more robust than necessary, because only the button presses were used to switch between the three different modes.

Previous Value Current Value Logic
Off Off None
Off On Press
On Off Release
On On Hold

TABLE 2 – Debouncing logic for the buttons. Some of the functionality implemented here can be used for future additions.


Advertise Here

The main() function was used to set up the peripherals and protothreads [4] that were used in a voluntary yield-scheduling scheme to run the necessary functions in parallel. We set up the ADC channel (for the temperature sensor), SPI channel (to send the DotStar LEDs lighting data), Timer2 and InputCapture1 (to calculate WWVB Pulses described below), the RTCC (to keep correct time using the external oscillator) and system-wide interrupts. The SPI was set up for channel 2 and configured to send 32- bit values. The SPI clock speed is divided by 16 (to operate within the range of DotStars) to give small relative rise and fall times after the signal is level-shifted to 5 V. Finally, we initialized two protothreads [4], pt_timer and pt_buttons, and scheduled them to run in a round-robin format.

After much research and testing, it was clear that the WWVB was unreliable because it was a weak signal and too much noise was injected into that bandwidth by our location on a technically advanced campus. The only antenna/receiver available for purchase was chosen. There were no other hardware options to explore at that point. For testing the updating of the clock, the WWVB signal was provided by an Arduino that sent the correct time in WWVB format to the PIC32 MCU.

The WWVB signal is transmitted 1 bps, taking 60 s to send the current time of day and date within a century. Information is encoded using amplitude modulation with the scheme shown in Figure 5. If power is reduced for one-fifth of a second (0.2 s), this is a data bit with value zero. If power is reduced for one-half of a second (0.5 s), this is a data bit with value one. If power is reduced for four-fifths of a second (0.8 s), this is a special non-data “marker,” used for framing. Markers are sent during seconds 0, 9, 19, 29, 39, 49 and 59 of each whole packet. The start of the second of two consecutive markers indicates the top of the minute and serves as the on-time marker for the next frame of time code. Markers are important to allow receivers to frame the time code properly.

FIGURE 5 – WWVB signal representation and the breakdown of the decoding. This radio signal can be used to correct the time on the clock when available.

To receive the WWVB signal, we purchased a 60 kHz WWVB atomic radio-controlled clock receiver from Universal-Solder. It is the only WWVB receiver currently available on the market. The receiver was attached to a 100 mm ferrite-core antenna.


Advertise Here

The WWVB transmits data by sending an amplitude-modulated signal. The length of time the pulse is low determines the value of the symbol being transmitted. We used an input capture module to trigger on the first falling edge and then on every edge thereafter. The input capture begins on a falling edge because the length of a WWVB pulse is determined by the time the signal is at -17 dB, which in our case was a digital low value. We set up an interrupt service routine that records the time for a falling edge (the first signal that causes the input capture to trigger) and stores it into an integer capture1. The input capture will then wait until it sees the next edge—which will be a rising edge—and it records the current time and stores it into another integer capture2. Then we subtracted capture1 from capture2 to calculate the length of the pulse pulse_time. Additionally, we increment the counter that keeps track of the symbol number in the current transmission. Several flags are also set here. For the falling edge we set ready to 0, indicating that time is not ready to be changed. When it receives the rising edge, ready is back to 1. In the timer thread, the new symbol and time can only be set when ready is 1.

The input capture module cannot determine whether the edge it is capturing on is high or low. Therefore, we toggled a bit back and forth to indicate which type of edge was next. This was possible because we defined the first trigger to be on a falling edge.

The input capture is useful for calculating the time of small pulses using the internal timers. Because we are calculating times in hundreds of milliseconds, even setting up an internal timer with the largest prescaler value when the processor is running at 40 MHz is not sufficient. We therefore opted to use the millisecond timer from protothreads [4], PT_GET_TIME(), to calculate the time pulses. We believe that PT_GET_TIME() is sufficient because it takes approximately 5 weeks to overflow. If it does overflow while decoding pulses, that pulse will be an error, and the frame will need to restart.

The timer thread begins by checking to see if the input capture has triggered twice, starting with a falling edge followed by a rising edge. If it has, it will then use pulse_time to determine the symbol: 100 ms < pulse_time < 300 ms is a 0; 400 ms < pulse_time < 600 ms is a 1; 700 ms < pulse_time < 900 ms is a marker. Otherwise it sends an error. The symbols are then stored into an integer. If a marker symbol is received, we check if the previous symbol was also a marker, which indicates the start of the next frame. If this is the case, we set the bit number bit_n, to 0 to keep track of the position in the frame. We also check the capture_count to determine if time has been set. If it has not been (capture_count == 0), we set capture_count to 1 and call input_time(). Calling this first input_time() sets the time and date to default values (date: 1/1; time 1:02). The yellow seconds indicator begins to run, and it can be used as a countdown to when the entire dataframe is received and the proper time and date are set. If the time is being recorded but has not been set (capture_count == 1), we set capture_count to 2 and call input_time(). Calling input_time() now sets the time and date to value it decoded from the WWVB signal.

After the symbol is determined from the pulse duration, the time is set by calling set_time(). The set_time() function takes in two parameters–the symbol and its number in the frame. We use 60 individual case statements that are based on the symbol number to decode time. For example, bits 12, 13, 15, 16, 17 and 18 in the frame are used for the hour values, 20, 10, 8, 4, 2 and 1, respectively (bit 14 is left at 0). The example sequence is depicted in Table 3.

Bit number 12 13 14 15 16 17 18
Symbol 0 1 0 1 0 0 1

TABLE 3 – A possible partial sequence of bits from the WWVB signal to be decoded. This will read 7 o’clock p.m.

The hour will be 20×0 + 10×1 + 8×1 + 4×0 + 2×0 + 1×1 = 19 or 7 o’clock PM. We also use the set_time() function to check for errors in the received symbols.

The input_time() function mentioned above is what assigns the time and date to the RTCC structures (tm and dt). The function begins by converting the day of the year value to month and day values. Then the RTCC structures for time and date are assigned using the values from the set_time() function and a macro that converts integers to BCD format: #define int2BCD(a) ((a/10)<<4)|(a%10).

Next, we call RtccSetTimeDate(tm.l, dt.l) to set the time and date for the RTCC peripheral. Last, we clear all the global time and date variables for the next time set_time() is run.

The final part of the thread is a case statement based the on the mode of operation (time, date, or temperature). Depending on which mode is chosen, a corresponding LED function is called to set the DotStar LEDs. Last, we call write_pixels() to send the LED color and brightness data via SPI. PT_YIELD_TIME_msec(10) is called to allow the Button thread time to execute.

The laser cut numbers on the panel of the display make the numbers sharp and easy to read (Figure 6). The DotStars were a great choice. In Figure 6, the blue digits are at quarter power because the background, and the LEDs carrying the information are at half power. The wood box was strong, but the ribs that separate the individual number boxes and prevent light from leaking were not totally even. This presented no visual problem, but it would be preferable for front and back panels to be completely flat. Also, the numbers could be laser cut more precisely by leaving little connector pieces for digits 0, 4, 6, 8 and 9.

FIGURE 6 – Final design of the clock with minimal image processing, showing the color and diffusion of the LEDs. The clock is on “Time” setting reading 1:39:37.

It turned out that the PIC32 MCU is able to keep accurate time. We let the time run side-by-side with other clocks for several-hour periods, and did not see any difference in the time. This was important to our goal of making a fully functional and useful clock, which must remain powered on and accurate for long intervals. You can see our clock by view the YouTube video of our project below (Figure 7).

After waiting for more than a month to receive the WWVB receiver, it finally arrived. Unfortunately, we had great difficulty using it, and were unable to pick up any trace of a signal in our building (Phillips Hall at Cornell) (Figure 8). Because the deadline for the project was approaching, we decided to use an Arduino board to simulate the WWVB signal. We found code online to do this. The Arduino code sends a perfectly clear pulse-modulated signal that the PIC32 MCU has no trouble decoding.

FIGURE 8 – This screen shot shows the Arduino-produced signal to model the WWVB radio signal. A simulated signal had to be used because the WWVB signal was weak and the noise was high on Cornell’s EE Campus.

From a black-box perspective, the signal from the Arduino and the signal from the WWVB receiver look identical. The Arduino is a powerful tool that we used to test all types of time/ date inputs and to fix any errors we found. Unfortunately, we were not able to build a clock that sets the time with absolutely no user input. We plan to continue working on the clock and find a solution for receiving a WWVB signal in perhaps a less tech-centric location.

Like any good engineering product, we designed the clock so it can be easily upgraded and improved. We compiled a list of additional functionalities that we would like to implement in the future include:

• Self-calibration for the oscillator— further improves accuracy
• Add better error correction—for instance, if a marker is the symbol that is supposed to be present at a specific bit, an error will be thrown and the entire data frame will be discarded.
• More details using the LEDs—such as AM/PM lighting effects and decimal place for the temperature sensor
• Games—Tetris or Connect 4.

For detailed article references and additional resources go to:
References [1] through [4] as marked in the article can be found there.

All Electronics |
Digi-Key |
Microchip Technology |
Mean Well USA |
Texas Instruments |


Keep up-to-date with our FREE Weekly Newsletter!

Don't miss out on upcoming issues of Circuit Cellar.

Note: We’ve made the Dec 2022 issue of Circuit Cellar available as a free sample issue. In it, you’ll find a rich variety of the kinds of articles and information that exemplify a typical issue of the current magazine.

Would you like to write for Circuit Cellar? We are always accepting articles/posts from the technical community. Get in touch with us and let's discuss your ideas.

Sponsor this Article
+ posts

Eldar Slobodyan graduated with a B.S. in Electrical and Computer Engineering from Cornell ECE in May 2018. He now works at Curtiss-Wright Sensors & Controls and consults at Cornell Tech. He is passionate about embedded systems, robotics, machine learning, product development and entrepreneurship. He is the CTO of a startup developing smart vehicle technology that will facilitate more efficient urban transportation. His email is

Jason Ben Nathan is originally from North Caldwell, NJ. He received his B.S. in Electrical and Computer Engineering from Cornell in December 2017. Jason is currently working as an Electrical Engineer at The Johns Hopkins Applied Physics Laboratory in Laurel, MD. He is also in the process of creating an upgraded version of this project. His email is

Supporting Companies

Upcoming Events

Copyright © KCK Media Corp.
All Rights Reserved

Copyright © 2024 KCK Media Corp.

Build a Self-Correcting LED Clock

by Eldar Slobodyan & Jason Ben Nathan time to read: 15 min