Educational Binary Clock (ATmega1284 + DS3231) – fully documented project

Hello everyone,

I would like to share an educational electronics project that I built in 2024: a standalone binary clock based on an ATmega1284 and a DS3231 RTC module.

At the time, the goal was not only to build a working binary clock, but also to create a clear and pedagogical example showing how such a system works, from hardware wiring to code logic.

I recently completed a full documentation of the project and decided to share it.

The clock displays hours, minutes, and seconds using LEDs that represent the binary decomposition of each decimal digit.

What makes this project a bit different from a typical build is that the documentation is intentionally very detailed and educational. The repository includes explanations about:

  • how binary time representation works

  • how decimal digits are converted into binary LED patterns

  • why only the useful bits are displayed

  • memory-safe string handling using C-strings

  • the overall program architecture

The hardware is fully standalone:

  • ATmega1284 running at 8 MHz internal clock

  • DS3231 RTC for accurate timekeeping

  • direct LED driving without shift registers

  • ISP programming (no bootloader required)

The complete project, including schematics, photos, and a fully documented README (available in both English and French), can be found here:

:backhand_index_pointing_right: GitHub repository:

I hope this project may be useful for beginners who want to understand how hardware, binary representation, and embedded programming interact in a real-world example.

Any feedback or suggestions are very welcome :slightly_smiling_face:

Any especial reason for not using Arduino UNO R3 (ATmega328P) as a development tool for the RTC Clock and then migrating the whole project on a stand-alone MCU?

I opened your zip file and loaded the ino file. Is it the clock sketch?

/*  les fonctions 'dizaine' et 'unite' déclarées en tant que 'constexpr' permettent d'extraire les dizaines et unités pour les heures minutes et secondes en provenance du module DS3231
    Ici on ne travaille qu'avec des chaines AZT pour en extraire entre autre des quartets.
    Toutes les secondes, les dizaines et unités des heures, minutes et secondes sont transformées en groupe de bits avec la fonction 'binaire'
    Enfin dans la loop (avec ma fonction : void concatener(), je concatène sous la forme d'une chaine AZT tous les bits constitutifs.
    Au final j'ai une chaine AZT 'binaire', l'opérateur ternaire fait le travail pour allumer ou éteindre les leds.
*/

#include <Wire.h>
#include "simpleRTC.h" // pin 16 : SCL - pin 17 SDA - sur uC SCL : 22 - SDA : 23

constexpr  uint8_t dizaine(uint8_t x) {
  return  ((x) / 10);
}
constexpr  uint8_t unite(uint8_t x) {
  return ((x) % 10);
}

unsigned long tempsPrecedent;

char tabHeuresDizaine [2 + 1];
char tabHeuresUnite [4 + 1];
char tabMinutesDizaine [3 + 1];
char tabMinutesUnite [4 + 1];
char tabSecondesDizaine [3 + 1];
char tabSecondesUnite [4 + 1];
char tabConcatenation [20 + 1];


void concatener(char *dst, uint8_t dstSize,
                const char *a, const char *b, const char *c,
                const char *d, const char *e, const char *f)
{
  uint8_t i = 0;

  const char *srcs[6] = { a, b, c, d, e, f };

  for (uint8_t s = 0; s < 6; s++) {
    const char *p = srcs[s];
    while (*p) {
      if (i + 1 >= dstSize) {  // garder une place pour '\0'
        dst[i] = '\0';
        return;
      }
      dst[i++] = *p++;
    }
  }
  dst[i] = '\0';
}

uint8_t w_from_bits(uint8_t bits) {
  // bits = 2,3,4
  // bits ecrits = 7 - w  =>  w = 7 - bits
  return (uint8_t)(7 - bits);
}

char *binaire (uint8_t n, uint8_t w, char *tab) {
  uint8_t y = 0;

  for ( uint8_t x = 0 ; x < 8; x++) {
    if (x > w ) { // w = 3 donne 4 elements
      tab[y] = n & 0x80 ? '1' : '0';
      y++;
    }
    n <<= 1;
  }
  tab[y] = '\0';
  return tab;
}

void setup() {
  Wire.begin();
  for (uint8_t x = 8; x < 14; x++) {// HEURES
    pinMode(x, OUTPUT);
    digitalWrite(x, LOW);
  }
  for (uint8_t x = 18; x < 24; x++) {// Secondes
    pinMode(x, OUTPUT);
    digitalWrite(x, LOW);
  }
  for (uint8_t x = 24; x < 31; x++) {// Minutes
    pinMode(x, OUTPUT);
    digitalWrite(x, LOW);
  }

  pinMode(15, OUTPUT);
  digitalWrite(15, LOW); // dernière Led des secondes

  tempsPrecedent = millis();

}

void loop() {


    if (millis() - tempsPrecedent >= 1000UL) {

      if (RTC.actualiser()) {

      uint8_t h = RTC.heure();
      uint8_t m = RTC.minute();
      uint8_t s = RTC.seconde();
      uint8_t heureDizaine = dizaine(h);
      uint8_t heureUnite = unite(h);
      uint8_t minuteDizaine = dizaine(m);
      uint8_t minuteUnite = unite(m);
      uint8_t secondeDizaine = dizaine(s);
      uint8_t secondeUnite = unite(s);

      binaire(heureDizaine,  w_from_bits(2), tabHeuresDizaine);
      binaire(heureUnite,    w_from_bits(4), tabHeuresUnite);
      binaire(minuteDizaine, w_from_bits(3), tabMinutesDizaine);
      binaire(minuteUnite,   w_from_bits(4), tabMinutesUnite);
      binaire(secondeDizaine,w_from_bits(3), tabSecondesDizaine);
      binaire(secondeUnite,  w_from_bits(4), tabSecondesUnite);


      concatener(tabConcatenation, sizeof(tabConcatenation),
                 tabHeuresDizaine, tabHeuresUnite,
                 tabMinutesDizaine, tabMinutesUnite,
                 tabSecondesDizaine, tabSecondesUnite);

      for (uint8_t x = 0; x < 6; x++) {
        tabConcatenation[x] == '1' ? digitalWrite( x + 8, HIGH) : digitalWrite( x + 8, LOW) ; // pins 8 - 9 - 10 - 11 - 12 - 13 (HEURES)
      } // 14 - 15 - 16 - 17 - 18 - 19 sur uC
      for (uint8_t x = 6; x < 13; x++) {
        tabConcatenation[x] == '1' ? digitalWrite( x + 18, HIGH) : digitalWrite( x + 18, LOW) ; // pins 24 - 25 - 26 - 27 - 28 - 29 - 30 (Minutes)
      }// 40 - 39 - 38 - 37 - 36 - 35 - 34 sur uC
      for (uint8_t x = 13; x < 19; x++) {
        tabConcatenation[x] == '1' ? digitalWrite( x + 5, HIGH) : digitalWrite( x + 5, LOW) ; // pins 18 - 19 - 20 - 21 - 22 - 23  (Secondes)
      }// 24 - 25 - 26 - 27 - 28 - 29 sur uC
      tabConcatenation[19] == '1' ? digitalWrite( 15, HIGH) : digitalWrite( 15, LOW) ; // pin 15  (dernier bit des Secondes) - pin 21 sur uC
      
      //tempsPrecedent = millis();
      tempsPrecedent += 1000; 

    }
  }
}

Thank you for your question :slightly_smiling_face:

There were two main reasons for choosing the ATmega1284 instead of an Arduino UNO (ATmega328P).

First, the number of I/O pins.
A binary clock with hours, minutes, and seconds requires many LEDs, and I wanted to drive them directly from the microcontroller without using shift registers or multiplexing. The ATmega328P simply does not provide enough pins for that.

The second reason was educational.
I deliberately wanted to build the project completely from scratch, using a standalone microcontroller rather than a ready-made board, in order to better understand the hardware aspects (power supply, fuse settings, ISP programming, etc.).

And yes, the .ino file you opened is the main clock sketch. It only requires the simpleRTC library to compile and run.


As a "pedagogical example", the code contains some very strange decisions.

For example, why are the dizaine() and unite() functions declared as constexpr if they are only applied to runtime variables in the code?
Does the author himself know what the constexpr keyword means?
What can this example teach a beginners?

constexpr  uint8_t dizaine(uint8_t x) {
  return  ((x) / 10);
}
constexpr  uint8_t unite(uint8_t x) {
  return ((x) % 10);
}

....


      uint8_t h = RTC.heure();
      
      uint8_t heureDizaine = dizaine(h);
      uint8_t heureUnite = unite(h);

In addition to above, I would say that using functions for a single operation makes no sense in this case. The using division or remainder directly in the main program would be clearer.
The same applies to the function w_from_bits...
Why do we need a separate function for subtracting from seven? :)

Thank you for your detailed comments.

You are right about the use of constexpr. In my original version, the functions dizaine() and unite() were only applied to runtime values coming from the RTC, so using constexpr did not provide any real compile-time benefit. For this reason, I removed it.

Regarding the use of small helper functions for simple operations, I understand your point that expressions such as / 10, % 10, or 7 - bits could be written directly in the main code.

In this project, the choice to keep functions like dizaine(), unite(), and w_from_bits() was mainly pedagogical. The intention was to give explicit names to these operations so that beginners can more easily understand what each step represents, rather than having to interpret the arithmetic expressions.

I agree that for experienced readers this may look unnecessary, but for an educational example I preferred readability and explicit meaning over conciseness.

Thank you again for taking the time to review the code and share your observations.