Prime Time
4th March 2025
This is a digital clock based on two 8x8 bi-colour LED dot matrix displays controlled by an AVR128DA32 or ATmega1608 microcontroller. It normally displays the time in red:
The Prime Time clock displays the time on two bi-colour dot matrix displays.
However, it changes to green if the time is a prime number:
The Prime Time clock display changes from red to green
if the time in hours and minutes is a prime number.
It's designed for anyone who is interested in prime numbers, or as an amusing conversation piece: "can you guess why the time sometimes changes to green?". You can also use it as an excuse to procrastinate: "I'll do it next time the time is a prime number" [1].
Introduction
I recently found that a good way to get back to sleep after waking up at night was to check the time on my digital watch, and then try to work out whether the time was a prime number. That gave me idea for this clock.
It uses two bi-colour dot matrix displays. Each of the pixels in the 8 x 8 matrix is lit by a red and/or a green LED, so you can actually get three colours: red, green, or yellow if both LEDs are lit. I've tested the project with three different types of display; see below.
The displays each have 24 pins on a 0.1" pitch and so they would fit on a breadboard, but the wiring would be horrendous, so I decided to design a PCB for the project. Adafruit recommend using two 24-pin MAX7219 display drivers which will do the multiplexing, and interface to a microcontroller via SPI, but I decided that it would be possible to drive both displays using a single 32-pin AVR32DA32 using charlieplexing. An ATmega1608 can also be used as it has an identical pinout, and is code compatible.
Display options
The clock provides the following display options, which you can select by commenting out one or more of the directives at the start of the program:
Prime Time
By default the clock displays the time in red, which changes to green if the time is a prime number. Alternatively, if you don't like this feature, comment out #define PRIMETIME to display the time with the hours in red and the minutes in green.
12-hour or 24-hour
The digits use a 7 x 3 pixel character set, allowing the four digits for a 24-hour clock to fit on the 16 pixel wide displays, with a blank column between adjacent digits. Alternatively the time can be displayed in 12-hour format, in which case a pulsing yellow colon can be displayed between the hours and minutes by fitting the leftmost '1' digit into two columns.
By default the clock is in 12-hour format, but comment out #define TWELVEHOUR for 24-hour format:
The Prime Time clock displaying the time in 24-hour format.
Square or rounded digits
By default the digits are square-shaped, but you can comment out #define SQUARED for an alternative character set with rounded digits:
The Prime Time clock displaying the 24-hour time using the alternative rounded digits.
Setting the time
A push button on the back panel allows you to set the time. The first time you hold it down the hours advance twice a second. Releasing it and pressing it again advances the minutes.
Charlieplexing
The clock uses charlieplexing to allow the two displays to be driven from just 24 I/O lines; for more information about how this works see Driving LED Displays with Fewer I/O Lines. Here's a diagram showing how the two displays are connected up:
The following explanation assumes that the displays are common cathode, but the circuit works equally well with common anode displays.
The rows and columns represent the 24 I/O lines we are using, and the cell at the intersection of each row and column is a position where we can connect an LED. Because LEDs only conduct in one direction we can connect two LEDs to each pair of I/O lines, one with each polarity. For example, the top left red LED in one display has its cathode connected to PA0 and its anode to PD0, and the top left red LED in the other display has its anode connected to PA0 and its cathode to PD0. You obviously can't connect the anode and cathode of an LED to the same pin, as indicated by the diagonal line of grey cells.
With common cathode displays the cathodes of the red and green LEDs at each position are connected. I've chosen to have the common cathodes for each display on a single port so the program can write the column data to the display in a single instruction. The anodes will be selected in turn, using multiplexing.
Incidentally, you can see from the above diagram that there's potentially room to fit a third bi-colour display, with its cathodes on PC0-PC3 and PF2-PF5, the red anodes on PA0-PA7, and the green anodes on PD0-PD7. So you could drive three 8x8 bi-colour displays from the same 32-pin processor; however, the PCB layout would be pretty challenging, and I didn't need it for this application.
The circuit
Here's the circuit of the Prime Time digital clock:
The circuit of the Prime Time clock, based on an AVR128DA32 or ATmega1608.
Microcontroller
I tested the circuit with a 32-pin AVR128DA32; the other memory versions of this chip could also be used: the AVR32DA32 or AVR64DA32.
I also tested the circuit with an ATmega1608 which has an identical pinout, and is code compatible; the other memory versions of that chip would also be suitable: the ATmega808, ATmega3208, or ATmega4808.
The AVR32DB32 range, or AVR16DD32 range, are not suitable, since they use an additional pin to support Multi-Voltage I/O (MVIO), and so don't provide all the I/O pins needed by the clock.
LED displays
The LED displays need to be 32 x 32mm bi-colour 8x8 dot matrix type. The program will work with either common-cathode or common-anode displays by changing a single #define.
I've tested the board with four different bi-colour red/green displays:
- YSM-1288CR3G2C (common cathode), with round diffused pixels. I bought these from a Sparkfun distributor several years ago and they no longer seem to be available. They are bright even on a 3.7V Lipo battery.
- KYX-1088AHG (common cathode) and KYX-1088BHG (common anode) called Red Green, with round diffused pixels. Available from AliExpress [2]. These work best with a 5V supply.
- 1388CURPG (common cathode) and 1388AURPG (common anode) called Red Bright Green, with round clear pixels. Available from AliExpress [3]. They are very bright even on 3.7V.
- BL-M12A883DUG-11 (common cathode) with square diffused pixels. Available from Adafruit and their distributors [4]. These work best on 5V.
Push button
The circuit uses PF6 as an input to detect the push button for setting the time; the default behaviour of this pin is Reset, so you need to configure it as an input when programming the chip; see Compiling the program below.
Power
The clock can be powered from between 3V and 5V. The current consumption is about 6mA at 3.7V and 10mA at 5V. A 5V USB power supply is a good option; alternatively a portable option is a 3.7V 18650 Li-Ion rechargable battery, which will power the clock for over 2 weeks.
Here's the full parts list (click to expand):
► Parts list
Construction
I designed a PCB in Eagle and sent it to JLCPCB for production. There's a link at the end of the article if you want to make yourself some boards.
All the resistors and capacitors are 0805 size, and the processor chip is a 32-pin TQFP package. It might be possible to solder these with a fine-tipped soldering iron, but I used a Youyue 858D+ hot air gun at 275°C with Chip Quik SMD291AX10 solder paste:
The reverse of the Prime Time printed-circuit board showing the AVR128DA32 and
other surface-mount components.
LED displays
The LED displays are mounted on the front of the board, with the components on the back. They must be mounted in the correct orientation with the part number towards the bottom of the board.
I recommend mounting the displays in header sockets, rather than soldering them in, because once you've soldered them in it will be very hard to remove them. I used low-profile header sockets known as Swiss Pins [5]. I've put two dummy holes in the PCB so you can use two 17-way strips to hold both displays. Note that to break the Swiss Pin strips into shorter lengths you have to sacrifice one pin, and cut the strip at that point.
Crystal
The crystal I used is a 32.768kHz 3.2mm x 1.5mm SMD type with an accuracy of 20ppm and a load capacitance of 6pF [6]. To calculate the capacitor values I used the formula C = 2(CL - CS), where CL is the load capacitance 6pF, and CS is the stray capacitance which is usually estimated to be 2.5pF on a PCB. This gives C=7pF. I used the closest available value, 6pF.
Backplate
I used a spare PCB as a backplate, attached to the main PCB with eight M2.5 screws and four 10mm pillars.
The program
The program uses some of the same code as my earlier clock project, Big Time.
Multiplexing the display
The display is multiplexed by Timer/Counter TCB0, which is set up by calling DisplaySetup():
void DisplaySetup () { TCB0.CCMP = 1249; // Divide 4MHz by 1250 = 3.2kHz TCB0.CTRLA = TCB_CLKSEL_CLKDIV1_gc | TCB_ENABLE_bm; // Enable timer, divide by 1 TCB0.CTRLB = 0; // Periodic Interrupt Mode CPUINT.LVL1VEC = 12; // Give TCB0 high priority TCB0.INTCTRL = TCB_CAPT_bm; // Enable interrupt }
This divides the 4MHz clock by 1250 to give a 3.2kHz interrupt. This will then be used to step between the 32 column select I/O lines, so each column lights up at a frequency of 3200/32 or 100Hz, which is safely above the 50Hz rate at which flicker is perceived.
Display shimmer
With an earlier version of the program there was a slight but noticeable display shimmer that I was determined to try and eliminate. I tried several changes to the program, none of which had any effect. Finally I deduced that it was caused by the TCB0 interrupt, used to multiplex the display, being interrupted by the RTC interrupt that provides the 2Hz timing.
The solution was to define the TCB0 interrupt vector, which is vector number 12, as a Priority Level 1 vector, with the statement:
CPUINT.LVL1VEC = 12;
After making this change the display was rock solid. You can see the effect by commenting out this line.
Timer TCB0 interrupt service routine
The TCB0 interrupt simply calls the routine DisplayNextPin():
ISR(TCB0_INT_vect) { TCB0.INTFLAGS = TCB_CAPT_bm; // Clear the interrupt flag DisplayNextPin(); }
DisplayNextPin() steps between the 32 column anodes, 8 for each display and each colour. When a particular column is enabled, the appropriate 8-bit data is written to the row cathodes, which are either on port A or on port D:
void DisplayNextPin() { static uint8_t pin; PORTA.DIRCLR = 0xFF; // All I/O pins as inputs PORTC.DIRCLR = 0x0F; PORTD.DIRCLR = 0xFF; PORTF.DIRCLR = 0x3C; pin = (pin+1) % 32; uint8_t band = pin / 8; uint8_t slice = pin % 8; uint8_t pin2 = pin; switch (band) { // Handle the charlieplexing case 0: // Red RH display if (Colour[Scheme][8+slice] & RED) { PORTD.OUTCLR = Column[8+slice]; PORTD.DIRSET = Column[8+slice]; } break; case 1: // Red LH display if (Colour[Scheme][slice] & RED) { PORTA.OUTCLR = Column[slice]; PORTA.DIRSET = Column[slice]; } break; case 2: // Green LH display if (Colour[Scheme][slice] & GREEN) { PORTA.OUTCLR = Column[slice]; PORTA.DIRSET = Column[slice]; } break; case 3: // Green RH display if (Colour[Scheme][8+slice] & GREEN) { PORTD.OUTCLR = Column[8+slice]; PORTD.DIRSET = Column[8+slice]; } pin2 = pin - 16; } pinMode(Pin[pin2], OUTPUT); digitalWrite(Pin[pin2], HIGH); // Take this column high }
The 8-bit data for each column is specified by the array Column[]. For simplicity the eight dots in each column are the same colour, defined by the array Colour[][]. The Colour[][] array has multiple rows to support alternative colour schemes; the current scheme is defined by the global variable Scheme.
Real-time clock
The clock uses the Real-Time Clock peripheral to do the timing, using the external 32.768kHz crystal to provide a 2Hz interrupt. The interrupt is set up by the routine RTCSetup():
void RTCSetup () { uint8_t temp; temp = CLKCTRL.XOSC32KCTRLA & ~CLKCTRL_ENABLE_bm; // Disable oscillator: CPU_CCP = CCP_IOREG_gc; // Write to protected register CLKCTRL.XOSC32KCTRLA = temp; while (CLKCTRL.MCLKSTATUS & CLKCTRL_XOSC32KS_bm); // Wait until XOSC32KS is 0 temp = CLKCTRL.XOSC32KCTRLA & ~CLKCTRL_SEL_bm; // Use External Crystal CPU_CCP = CCP_IOREG_gc; // Write to protected register CLKCTRL.XOSC32KCTRLA = temp; temp = CLKCTRL.XOSC32KCTRLA | CLKCTRL_ENABLE_bm; // Enable oscillator CPU_CCP = CCP_IOREG_gc; // Write to protected register CLKCTRL.XOSC32KCTRLA = temp; while (RTC.STATUS > 0); // Synchronize registers RTC.CLKSEL = RTC_CLKSEL_TOSC32K_gc; // 32.768kHz External Crystal RTC.PITINTCTRL = RTC_PI_bm; // Enable periodic interrupt RTC.PITCTRLA = RTC_PERIOD_CYC16384_gc | RTC_PITEN_bm; }
This is more complicated than you might expect because some of the registers are protected, and you have to unlock them before you can write to them.
The interrupt service routine increments the hours and minutes counters as appropriate, and handles the push button to allow you to set the time. It also handles the differences between a 12-hour display and a 24-hour display:
ISR(RTC_PIT_vect) { static unsigned long time; // In half seconds uint8_t minutes, hours; RTC.PITINTFLAGS = RTC_PI_bm; // Clear interrupt flag minutes = (time / 120) % 60; hours = (time / 7200) % 24; // Internal time 24-hour if (ButtonDown()) { if (ButtonState == 1 || ButtonState == 3) { ButtonState = (ButtonState + 1) % 4; } if (ButtonState == 0) { // Advance hours hours = (hours + 1) % 24; } else { // Advance minutes minutes = (minutes + 1) % 60; } time = (unsigned long)hours * 7200 + minutes * 120; } else { // Button up if (ButtonState == 0 || ButtonState == 2) { ButtonState = (ButtonState + 1) % 4; } time = (time + 1) % 172800; // Wrap around after 24 hours } #ifdef TWELVEHOUR if (hours > 12) Digits = (hours-12)*100 + minutes; else if (hours < 1) Digits = (hours+12)*100 + minutes; else Digits = hours*100 + minutes; PlotTime12hr(Digits); if (time & 2) Column[7] = Colon; else Column[7] = 0; // Pulsing colon #else Digits = hours*100 + minutes; PlotTime24hr(Digits); #endif }
The time-setting button increments the hours or minutes at 2Hz; hence the reason for making the Real-Time Clock interrupt 2Hz rather than 1Hz.
Plotting the time
The digits for the 12-hour clock time are written into the Column[] display from the bitmap character definitions in CharMap[][] by the routine PlotTime12hr():
void PlotTime12hr (int digits) { if (digits/1000 == 1) { Column[0] = CharMap[1][1]; Column[1] = CharMap[1][2]; } else { Column[0] = 0; Column[1] = 0; } for (uint8_t col=0; col<3; col++) { Column[col+3] = CharMap[(digits/100)%10][col]; Column[col+9] = CharMap[(digits/10)%10][col]; Column[col+13] = CharMap[digits%10][col]; } }
The leftmost digit is handled separately; it's only displayed if it's a '1', and it's fitted into two columns. The remaining three digits are each plotted into three columns.
A similar routine PlotTime24hr() plots the time for the 24-hour clock.
Prime test
The program determines whether the current time is a prime number by calling the function Prime():
bool Prime (int n) { if (n % 2 == 0 || n % 3 == 0) return nil; int d = 5, i = 2; while (d * d <= n) { if (n % d == 0) return nil; d = d + i; i = 6 - i; } return t; }
This works by checking the divisibility of n by candidate factors up to the square root of the number. It takes advantage of the fact that all prime numbers greater than 2 or 3 have the form 6i + 1 or 6i + 5, where i is an integer.
Each time the minute changes the main loop in loop() calls Prime() to check whether it's a prime number, and sets Scheme to GREEN or RED accordingly.
Compiling and uploading
UPDI programmer
To program the processor the recommended option is to use a 5V or 3.3V USB to Serial board, such as the SparkFun FTDI Basic board [7], or a USB to Serial cable [8], connected with a Schottky diode as follows. You can substitute a 4.7kΩ resistor for the Schottky diode:
- Set the options in the appropriate section below for the processor you're using.
- Set Programmer to the "SerialUPDI - 230400 baud" option.
- Select the USB port corresponding to the USB to Serial board in the Port menu.
- Choose Burn Bootloader to set the fuses.
- Then choose Upload from the Arduino IDE Tools menu to upload the program.
AVR128DA32
If you're using an AVR128DA32 compile the program using Spence Konde's Dx Core on GitHub (I used 1.5.11). Choose the AVR DA-series (no bootloader) option as appropriate under the DxCore heading on the Board menu. Check that the subsequent options are set as follows (ignore any other options):
Chip: "AVR128DA32"
Clock Speed: "4 MHz internal"
Reset pin function: "Input (no output, ever)"
Using a clock speed of 4MHz reduces the processor power consumption from 4.7mA to 1.1mA.
The program is small enough to fit on any processor in the range, from the AVR32DA32 up to the AVR128DA32.
ATmega1608
If you're using an ATmega1608, compile the program using MCUdude's MegaCoreX on GitHub. Choose the ATmega1608 option as appropriate under the MegaCoreX heading on the Board menu. Check that the subsequent options are set as follows (ignore any other options):
Clock: "Internal 4 MHz"
BOD: "BOD disabled"
Pinout: "32 pin standard"
Reset pin: "GPIO"
Bootloader: "No bootloader"
On the ATmega1608, choosing a clock speed of 4MHz ensures that the processor will run down to the minimum voltage of 1.8V, for maximum battery life.
The program is small enough to fit on any processor in the range, from the ATmega808 up to the ATmega4808.
Resources
Here's the Prime Time program: Prime Time Program.
Get the Eagle or Gerber files for the PCB on GitHub here: https://github.com/technoblogy/prime-time.
Or order boards from OSH Park here: Prime Time.
Update
8th March 2025: I've updated the program to cater for either common cathode or common anode displays, and added details of a fourth type of display.
Other suggestions
When the time is a composite number I thought about making the clock show a factorisation of the time for the last three seconds of each minute; perhaps this is taking things a bit too far.
Currently, for simplicity, I've restricted the clock to having each column of pixels the same colour, but you could easily change this to make the clock display arbitrary patterns of colours.
- ^ With the clock in 12-hour mode you might buy yourself an additional 22 minutes if the time is 11:29; the next prime number isn't until 11:51. In 24-hour mode you could have an extra 42 minutes, because at 13:27 the next prime isn't until 14:09!
- ^ 1088AHG on AliExpress.
- ^ Red Bright Green on AliExpress.
- ^ Small 1.2" 8x8 Bi-Color (Red/Green) Square LED Matrix on Adafruit.
- ^ 36-pin Swiss Female Socket Headers on Adafruit.
- ^ ABS07-120-32.768kHz-T on Farnell.
- ^ SparkFun FTDI Basic Breakout - 5V on Sparkfun.
- ^ FTDI Serial TTL-232 USB Cable on Adafruit.
blog comments powered by Disqus