GPS Wall Clock

Time from space on an ATTiny13A

Similar projects worth following

After reading Eric Schlosser's Command and Control: Nuclear Weapons, the Damascus Accident, and the Illusion of Safety a year ago and searching related topics, I found myself watching the video Rocket Sled Impact Test In Slow-Motion from Sandia National Labs. In the background of a couple of shots in this video, a large digital clock in the background caught my eye:

My initial "oh, that's cool" thought turned a few days later into buying a bunch of 1.8" 7-segment digits. When these arrived a month later in April 2018, I put together a frame in Fusion360 to mount them on the wall (sans any other plan, which is typical for me). The resulting 28 x 8cm design was far too big to print in one go, so I split it into 6 pieces joined with dovetails:

After printing in ABS, test fitting and bonding the joints using acetone, I gave the frame a quick sand and a paint job:

Ready for assembly.
First attempt sanding and painting

The initial results weren't ideal; the digits fit perfectly, but the dovetail joints were too obvious for my liking. I did another round of sanding and paint, then the frame and digits sat on my bench for a few months:

Bench in June 2018

In July I had another go at sanding and painting the frame, this time using painters gap filler to fill in the gaps around the part joints. This worked much better:

Halfway through sanding and painting with the help of gap filler. The middle joins are almost invisible now.

With the frame looking more polished, I wired up the displays using a home-made wire-wrapping tool:

Wiring segments together using wire-wrapping as a non-permanent way to make connections. My "tool" is the 0.1 inch aluminum tube on left with a small hole drilled in closest end.
Close-up of wire wrapping from a make-shift tool.

For the next while, this project lived on my desk driven by an STM8S and MAX7219 on a breadboard. Initially this only counted seconds (roughly) from the power on time, but later I calibrated the timing to have a semi-accurate clock that maintained the time given at compile+flash time. Code for this STM8 prototype is on Github for reference.

Counting up milliseconds using an STM8 and MAX7219 to demo (the P represents decimal 14 in the MAX7219's BCD decoding)

At this point I replaced the STM8 with an ATTiny13A and plugged in a NEO-6M GPS to read the actual time from. I'd worked out the Tiny could just support the number of I/O I needed, and some tinkering proved that the required C program could comfortably fit in the 1KB of available program space.

First pass and still have space to play with!

Working with the ATTiny13A for this was a bit of fun as it lacks hardware UART (for the GPS) and hardware SPI (for the MAX7219). Additionally, the NMEA sentences sent by the GPS are up to 79 bytes, which won't fit into the tiny's 64 bytes of SRAM.

The code has a routine to find RMC ("Recommended Minimum") sentences in the GPS's serial output and extract time and date information with only a 2 byte buffer for converting the numbers represented in ASCII to 8-bit integers.

Now with GPS time and red transparent acrylic to increase contrast (looks worse here as the acrylic isn't flush, but this really helps)

After turning the clock off for a week, my girlfriend mentioned that it'd been useful to have a clock (even if it looked like "a bomb timer from a movie"), so I thought I'd finish it off.

I didn't like the look with a single piece of acrylic across the front, so I broke that filter into smaller pieces and recessed them:

"It looks even more like a bomb with the wires hanging...
Read more »

GPS Wall Clock v1.1 schematic.pdf

Updated schematic with the timepulse bodged in from the GPS module

Adobe Portable Document Format - 119.37 kB - 08/07/2021 at 05:15


KiCAD schematic and board layout

Zip Archive - 34.01 kB - 02/10/2019 at 11:23


Clock frame dovetail split.f3d

Fusion360 model (split for printing)

f3d - 885.97 kB - 02/10/2019 at 11:22


Clock frame single piece.f3d

Fusion360 model (single piece, not split for printing)

f3d - 254.27 kB - 02/10/2019 at 11:22


  • Still room for improvement

    Stephen Holdaway08/07/2021 at 05:07 0 comments

    After honing the precision of the Tiny GPS Clock, I'd like to improve the GPS Wall Clock in the same way: using the GPS module's timepulse (1PPS) signal to synchronise display updates precisely with the top of each second.

    There's a couple of small problems however:

    1. There are exactly four bytes of program space left on the ATTiny13a's 1KB flash.
    2. All of the microcontroller's pins are in use.
    4 bytes free program space: challenge accepted

    This seems like an interesting challenge. Let's see how far we can get.

    Digging for more program space

    Going into probably the fifth or sixth code size reduction pass on this project, I really wasn't sure how much more could be done. Surprisingly, in a few evenings chipping away at the problem I managed to free up 180 bytes (17.5% of the total program space) without removing any functionality. Most of this was achieved by giving the compiler as much as possible to work on at once, with a sprinkling of other small optimisations:

    • Compiled as a single unit to maximise the compiler's ability to optimise - ie. including source files rather than headers in main.c. This is a little messy as it breaks the encapsulation allowed by separately compiled source files, but this alone saved 84 bytes of program space, so it was worth the trade-off for this project (commit).

    • Used -nostartfiles and provided a custom assembly entry point. This allows some redundant instructions GCC adds for software reset and "exit" to be removed, but most importantly lets us store code in the space normally reserved for the interrupt vector table, since interrupts aren't used at all. Freed up 24 bytes (commit).

      The assembly portion (startup.S) is fairly straight forward as all it needs to do is zero out memory and jump to main(). All variables in the C program are already initialised to zero to avoid the need for additional memory initialisation on reset.

    • Replaced eeprom_read_byte and eeprom_write_byte calls from avr/eeprom.h with C source implementations from the datasheet. Having these functions in-source allows inlining as they're only used once each, plus they could be slightly modified to strip out a wait loop that's irrelevant in this application. Freed up 36 bytes (commit).

    • Replaced two calls to spi_send with a single call where the address and data bytes to send are combined as a single 16-bit value. Combining these bytes takes the same number of instructions as making two function calls, but with only one function call the compiler can inline the function. Freed up 8 bytes (commit).

    • Replaced multiply by 10 with bit shifts equivalent to (x*8) + (x*2). There's no other multiplication in the code that uses non-power-of-two factors (that get compiled to bit shifts), so there's no need for a generic multiply routine. Freed up 6 bytes (commit).

    Some of these wins involved disassembling the firmware using avr-objdump to spot where space was being wasted:

    AVR assembly start-up code

    I initially looked into using a custom linker script, but using -nostartfiles ended up being a much cleaner solution. Labels in the disassembly like __trampolines_end and __ctors_end were a bit misleading - marking the end of sections which were actually empty and not relevant at all.

    I wanted to keep this as accessible and easy to work on as possible, so going to hand-written assembly wasn't really something I wanted to do outside of the short startup file. The compiler now optimises 90% of the code into a single function, which would be painful level of optimisation to achieve and maintain by hand.

    Avoiding interrupts

    Performing an action on a rising edge seems like a great case for interrupts, but given the code space constraints on this chip, it's not really practical. An interrupt handler doing anything more than flipping bits in register memory will need to push and pop registers to the stack, which quickly eats up program space at 4 bytes per register used, plus the instructions to do the required work.

    It ends up being more practical space-wise to avoid interrupts and just carefully...

    Read more »

  • Squeezing in timezone persistence

    Stephen Holdaway02/15/2020 at 11:00 0 comments

    For most of the last 9 months, I've successfully avoided unplugging this clock. It's something I've specifically avoided as every power cycle requires pulling the clock off the wall, holding the timezone button for 5 seconds and then remounting on the wall. This is not a terrible burden by any measure, but apparently it's enough of an annoyance to warrant spending multiple hours updating the firmware.

    Storing a single byte in eeprom on an AVR is straight forward - a call to eeprom_write_byte here, a call to eeprom_read_byte there. The challenge for this project is fitting another 72 bytes of code into the remaining 30 bytes of the ATtiny13a's flash space!

    Having already made multiple size-reducing passes on this code previously, I wasn't hopeful that significant savings could be made. Minor tweaks could be made, but It would be a mission to find 42 bytes worth of those. Thankfully I managed to extract the required space with just two changes (commit):

    • 30 bytes: Consolidated three places where digits were selectively blanked into a common "clear display" function
    • 14 bytes: Dropped the unused day, month and year fields from the NMEA string parsing

    With space freed up, the eeprom save/restore was an easy add (commit). Hopefully no further additions are needed as there's just two bytes of code space free.

    Edit: I noticed a stupid late-night bug immediately after posting this which required freeing a further 8 bytes (commit)! The code usage is now exactly 100% at 1024 bytes.

  • Maintenance and improvements

    Stephen Holdaway05/18/2019 at 22:51 0 comments

    Display with broken segments

    As winter approaches here in the southern hemisphere, the wire-wrapping from the original build of this display is proving to be be a bit unreliable. Without proper wire-wrap pins and the right tool, there's just not enough force to stop connections coming loose. The effectiveness of percussive maintenance has been dwindling, so it's time to for some upgrades.

    Better display connections

    I thought about simply adding solder to the wire-wrap joints to make them more robust, but the mess of overlapping wires meant I was likely to melt some insulation and short pins out. Instead I figured I'd completely replace the point-to-point wiring with some PCBs to make it tidier and stronger.

    After a few iterations of layout, I ended up with two small boards to attach to each pair of digits that connect all of the segments:

    Connector board layout

    I needed this fixed over the weekend, so the layout was designed for etching my own PCBs instead of ordering them. Ideally this would've been one long board to connect all digits, but the equipment I have makes it impractical to make a board larger than about 100mm square.

    These are fiddly to make, but they turned out ok:

    Home-made PCB

    Once constructed, the 6 boards were soldered into place and wired together with 0.5mm (22 AWG) solid copper wire. 16 wires was far more manageable than the 40 point-to-point connections when I wire-wrapped these originally:

    Multiple PCBs wired together on 7-segment display

    Once the driver was wired back in, I was pleased to find I hadn't made any mistakes or shorted or broken anything. The original build had the digits wired backwards which I'd neglected to fix in hardware at the time and was managing in my local copy of the code:

    7 segment clock on workbench
    The time is backwards but otherwise correct.

    With sturdy new connections, the clock should hopefully be maintenance-free for quite some time.

    As a finishing touch I added a 50uF electrolytic capacitor to the board to stop the ceramic caps whining when the display is being driven at a high brightness. This wasn't audible originally and I'm not sure why it's occurring after 3 months of use, but the extra cap has completely silenced it.

    Firmware improvements

    Over the last couple of months we've seen an unexpected timezone increment a number of times - usually by one or two steps, and usually overnight or when the house was dark in the evening.

    The most likely suspect was the shared ADC reading for the light sensor and timezone change button that only required a single sample below a set threshold to increment the timezone. I've changed this to require 5 readings 100ms apart before the timezone will be incremented (fix), putting the existing 500ms delay to use.

    The uncommanded timezone increments also revealed a bug where a negative value for hours wasn't being wrapped (fix). This resulted in some mild entertainment:

    The fun thing here is that the hour is correct in the one's column when the timezone is set to -12 due to the way the math works out:

    # Unsigned addition of -12 offset to UTC hour "09"
    9 + -12 = 253
    # Reduced by 24 as the value is greater than 23
    # This makes the ones column match the UTC hour
    253 - 24 = 229

    The "6" comes from the MAX7219's interpretation of the tens value:

    # The number of tens is calculated
    229 / 10 = 22
    # Displayed as "6" because the MAX7219 ignores the high nibble
    22 & 0x0F = 6

    I made these fixes a few weeks ago, but I'd been putting off updating the firmware for this as it's a bit of a pig to program: the footprint for the programming header is the wrong pitch, so instead of using a pogo-pin jig as intended, each connection has to be made manually with test clips:

    ... Read more »

View all 3 project logs

Enjoy this project?



Jan wrote 02/10/2019 at 11:30 point

Absolutely flawless execution! I'd give more than +1 if I could!

  Are you sure? yes | no

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates