DCF 77

17 octobre 2025

Il m'a fallu plusieurs mois pour concevoir et programmer mon horloge définitive. Voici le résultat.


J'avais commencé avec un prototype pour débroussailler le terrain, et les leçons furent pertinentes.


Sans plus attendre, voilà à quoi ça ressemble.
Les LED "blanches" ne le sont (généralement) pas, c'est la caméra qui sature.
Et l'afficheur principal est vraiment bien rouge, pas orangé.

Synoptique

J'ai rationalisé l'affichage en utilisant uniquement deux lignes, les données étant transmises en série :

J'ai utilisé une puce MSP430g2955, une version upgradée en taille mémoire, me permettant de mettre autant de texte que je veux, ce qui rend l'affichage commode, j'ai ainsi pu entrer divers anniversaires.

Très peu de fils, et pourtant vraiment beaucoup de LEDs (1706 chips).

Outils

Rien de bien transcendant, d'autant plus que je n'utilise que des outils gratuits, et que je dépense le moins possible.

  • Saisie de schéma et layout : EasyEDA, l’outil gratuit fourni par JCLPCB
    • JLC est mon fournisseur chinois de circuits imprimés, que je recommande car pas cher et vraiment pro, avec une qualité au top. J’ai juste dû ruser un peu pour mettre mes petits modules sur le même projet, surtout que la commande minimale est de 5 cartes.
    • Il a fallu travailler de manière ordonnée car EasyEDA, dans sa version basique, ne connait pas la hiérarchie = copier/coller et renommer les fils de routage (avec une routine adaptée). Un peu casse-pied, mais finalement pas si compliqué que ça.
  • Soudure : à la main uniquement, avec un fer à souder. Pas de four à refusion, ce qui a été pénible avec l’IS31, mais bon, j’y suis arrivé en redessinant les plots de soudure.
  • Programmation : Code Composer Studio version 6 sur mon PC windows XP. Eh oui, une vieillerie mais largement suffisante pour ce que je fais. Il y a une ruse pour obtenir la version débridée, un fichier de licence qui est donné par Texas.

Schémas

J'ai réalisé des petits modules afin de limiter les problèmes en cas d'erreur, ça évite de relancer une grande plaque, et cela permet de mettre au point incrémentalement.

Je ne mets ici que les schémas éventuellement intéressants.

Afficheur alphanumérique

Après diverses recherches infructueuses, j’ai terminé sur un afficheur à matrices 5x7 LEDs, mais pas les afficheurs rétro que je visais initialement car ils sont vraiment petits, chers et introuvables.

2 lignes de texte, de 90 pixels de large et 7 pixels de haut avec un afficheur élémentaire de 5x7 pixels, ça nous fait 36 matrices à piloter (et à souder), soit 18 Lumissil IS31FL3730 qui peut piloter 2 matrices 5x7, avec un minimum de câblage :

Le Lumissil IS31FL3730 peut piloter 2 matrices 8x8 en jouant sur les diodes

Mais on ne peut programmer que 4 adresses I2C différentes sur l'IS31FL3730. Pour contourner cela, j’ai utilisé un PCA9547 de NXP, un multiplexeur 8 canaux I2C vraiment simple (mais j’ai reçu en 2025 une notification de fin de vie 🙁).

Le PCA9547 peut rediriger le signal I2C, dans les deux sens (mais je n'en ai pas besoin), en adaptant les tensions, depuis une ligne vers une des 8 disponibles. Derrière chacune des 8 lignes, on peut mettre 4 IS31FL3730, mais je n'ai besoin que de 4+4+1+4+4+1.

Afficheur neopixels

J'ai utilisé la ruse du WS2814 pour piloter les 8 LEDs d'un afficheur 7 segments :

2 puces pour piloter les 8 LEDs d'un afficheur 7 segments.

Et en plus, avec cette puce, je peux ajuster l'intensité de l'afficheur par programmation !


J'ai réalisé plusieurs modules :

  • à 2 digits
  • à 3 digits
  • heures/minutes/secondes/dixième
  • et même 2 rangées de LEDs pour donner une indication de la qualité de réception !
Je me retrouve avec une simple chaine à un fil pour piloter tous mes 7 segments et autres neopixels.

À côté de la date alphanumérique du genre « Dimanche 12 octobre 2025 », j'ai finalement décidé d'afficher :

  • heure : minute   seconde . dixième
  • Les 60 bits d'information reçus en cercle, éclairement fort si c'est « 1 », faible si c'est « 0 », avec une couleur spécifique suivant la signification, et « blanc » si la valeur est différente de la minute précédente. Avec le décodage instantané (donc de la prochaine minute à afficher, incluant les possibles erreurs) de :
    • heure d'été / d'hiver
    • minutes
    • heures
    • jour du mois
    • jour de la semaine (7 LEDs colorées, 1 seule possible)
    • numéro de mois
    • année -mais sans le centenaire, non envoyé-
  • numéro du bit lu, et sa longueur en millisecondes
  • deux barres de LED colorées pour indiquer le niveau de bruit instantané, ainsi que le décalage de phase
  • deux fois 3 digits pour donner des informations sur l'état du système
  • deux LEDs colorées pour indiquer si l'heure DCF est OK, et si l'horloge RTC est OK

Ce qui finalement fait pas mal d'information en instantané, que je n'afficherai pas tout le temps car c'est trop distrayant. Restons sobre la plupart du temps, j'ai mis un bouton pour forcer l'affichage pendant quelques minutes.

J'ai décompté :

  • 1260 LEDs pour l'afficheur alphanumérique
  • 443 LEDs « neopixels » (3 LEDs par neo)
  • 3 LEDs sur la carte MSP430 (pour indiquer la présence de l'alimentation)

Soit un total de 1706 LEDs. Certainement mon plus gros afficheur. Et piloté avec si peu de fils !

Circuit principal

Le schéma de l'ensemble devient particulièrement simple :

  • le pilotage de l'afficheur alphanumérique en I2C
  • la sortie « neopixel », un seul fil !
  • l'entrée DCF77, avec génération d'une tension réduite à 1.6 V, avec un simple pont de résistance (ça consomme si peu qu'il est inutile de faire raffiné avec un régulateur, surtout à côté de l'ampère que vont consommer les LEDs...).
  • une photodiode (deux emplacements, au cas où), qui permettra d'ajuster l'intensité lumineuse en fonction de la lumière ambiante
  • Un bouton poussoir pour afficher des informations à la demande (principalement l'état système, et pouvoir lancer un auto-test au démarrage).
  • Quelques switchs pour pouvoir configurer quelque chose si besoin
  • Un bouton de reset (que je n'ai pas mis, il suffit de débrancher pour faire un reset)
  • Diverses prises pour l'alimentation 5 V (USB-C ou USB à l'ancienne).

Programmation

S'il m'a fallu quelques semaines pour faire l'électronique, le plus long étant le trajet entre la Chine et mon domicile vu que je n'ai payé que le transport de base, la programmation fut nettement plus longue, j'y ai passé une bonne partie de l'été 2025. La faute à tout ce que je voulais mettre dans mon horloge.

Voici quelques détails concernant la programmation, ceux qui me paraissaient d'intérêt.

Interruptions

Le quartz connecté au MSP430 permet d'avoir une horloge locale suffisamment précise par rapport aux futures histoires de verrouillage de phase. Le MSP430 tourne à fond, à 16 MHz (soit généralement à 16 instructions par seconde).

Je me sers du watchdog comme horloge, en faisant claquer 512 fois par seconde une interruption, toutes les 1.95 ms. À chaque interruption :

  • j'incrémente un compteur de base (pour mesurer la dérive),
  • j'incrémente un second compteur qui me sert d'horloge RTC locale, où une seconde pourra être rallongée ou raccourcie suivant les évènements,
  • l'état du signal DCF77 est lu, 0 ou 1, et le niveau de bruit = le nombre de transitions par seconde est évalué. 0 transitions signifie un signal collé à 0 ou 1, idéalement deux transitions = 1 créneau par seconde, au-dessus c'est anormal.

Normalement, une interruption doit être aussi courte que possible, mais là, il s'agit d'une horloge, et c'est le cœur du système, aussi les 512 interruptions par seconde peuvent être parfois assez longues, (quelques centaines de microsecondes). Ensuite le programme principal traite les évènements à sa vitesse, mais la puissance CPU est largement suffisante pour faire des calculs parfois compliqués (comme le nombre de secondes écoulées entre deux dates). J'ai quand même quasiment évité toutes les divisions, il suffit de réfléchir un peu et d'organiser correctement les données.

Il existe quelques instants critiques en calculs :

  • Je mets à jour l'affichage tous les dixièmes de seconde (100 ms)
  • À la seconde entière, et plus particulièrement à la minute entière, je mets impérativement l'affichage à jour, car c'est le « quatrième top » du DCF77, l'instant où l'heure est atomiquement exacte (certes avec le retard de propagation des ondes).
  • Juste après 200 ms après le début de la seconde, j'évalue la valeur du bit. L'affichage des bits reçus est mis à jour sur le cercle des 60 bits.

Verrouillage de phase

Le verrouillage de phase est de loin l'opération la plus critique. Pour cela, il faut pouvoir détecter la phase, ce qui est fait grâce à une convolution, un calcul permettant de connaitre la position du pic de corrélation, et sa puissance, qui permet de savoir si le pic est fort ou faible, en particulier en présence de bruit trop important.

J'ai convergé, après quelques essais, vers 64 paquets par seconde, autrement dit une donnée est évaluée d'après 4 acquisitions du signal DCF77.

Le noyau de convolution est simple :

  • facteur 1 sur les 100 premières ms (le signal doit être à 1)
  • facteur -1 entre 200 et 1000 ms (le signal doit être à 0 : si c'est 1, ce n'est pas bon)
  • 0 entre 100 et 200 ms, puisque le signal est variable

J'évite ainsi toutes les multiplications, il ne reste que des additions.

Il faut faire glisser le noyau sur les données pour trouver la valeur maximale. Pour aller vite et éviter de se retaper la sommation, j'effectue une différence entre deux intégrales consécutives. C'est peut-être la ruse la plus complexe, et encore.

void dcf77_phase_detect(unsigned int counter)
{
    static signed char pbin[BIN_NUMBER];
    unsigned char tick = (counter>>3) & (BIN_NUMBER-1);
    if ( dcf.input_acc > SAMPLE_PER_BIN-1 ) pbin[tick]++;
    else pbin[tick]--;
    #define MAX 4
    if (pbin[tick] >  MAX) pbin[tick]= MAX;
    if (pbin[tick] < -MAX) pbin[tick]=-MAX;
    int integral=0;
    unsigned char i;
    signed char *pbini = pbin;
    i=6; while (i--) integral += *pbini++;
    pbini = pbin + 13;
    i=BIN_NUMBER-13; while (i--) integral -= *pbini++;
    int max = integral;
    unsigned char tick_max = 0;
    for (i=0; i<BIN_NUMBER-1; i++)
    {
        integral -= pbin[ i ]<<1;
        integral += pbin[(i+ 6)&(BIN_NUMBER-1)];
        integral += pbin[(i+13)&(BIN_NUMBER-1)];
        if (integral > max) { max = integral; tick_max=i+1; }
    }
    if (tick==BIN_NUMBER-1)
        { dcf.tick_max = tick_max; dcf.corr_max = max; }
    dcf.input_acc = 0;
}

La routine de corrélation est exécutée 64 fois par seconde, et dure 184 μs


Le résultat, donné en fin de seconde, donne la position du pic en « tick » (64 ticks par seconde), un nombre entre 0 et 63, le milieu étant 32. Si c'est supérieur à 32, il faudra décaler dans l'autre sens.

L'intégrale vaut généralement 180 au plus bas, 288 au maximum avec un signal hyper-propre, en dehors de ce créneau 180-288, c'est qu'on a un bruit infernal et on ignorera ce résultat.


Chaque seconde, un décalage temporel = je raccourcis ou je rallonge la seconde en-cours est appliqué de manière à obtenir un pic de corrélation à 0. Il faut limiter la vitesse de décalage au cas où du bruit arrive.

Dès que l'on a un peu de signal, cette routine marche très bien, c'est même étonnant.

Évaluation du bit, de la trame

Une fois que l'horloge locale RTC est calée sur le signal DCF77, l'évaluation d'un bit devient vraiment triviale en comptant le nombre de fois que le signal DCF77 est à 1 sur les 200 premières millisecondes :

  • Au-dessus de 126, c'est un « 1 »
  • En dessous de 26, c'est un « vide », autrement dit le bit 59
  • Sinon c'est « 0 »

Valeurs limites trouvées par expérimentation.


Il suffit ensuite d'interpréter les bits pour voir si la trame est cohérente. J'ai exigé d'avoir quelques trames entièrement bonnes, en particulier il faut qu'une trame ait exactement une minute de plus que la précédente, pour valider l'heure DCF77.

Machine d'état

Comme pour le prototype, j'ai utilisé une machine d'état, quasiment la même au bémol près que lorsque je perds le signal DCF77, je retourne simplement au début, avec une différence : je sais que mon horloge RTC a été synchronisée parfaitement, et donc je continue d'afficher l'heure. Surtout que j'ai ajouté une procédure de calibration.

Siècle

La diffusion de la date DCF77 ne contient pas d'information concernant le siècle, autrement dit on ne sait pas si on est en 1915, 2025 ou 2125.

C'est peut-être un poil optimiste de ma part, mais je me suis arrangé pour que l'horloge survive aux siècles sans erreur, en stockant dans la flash la dernière année valide rencontrée. Du coup, il faudrait que l'horloge ne soit jamais démarrée pendant cent ans pour qu'elle se trompe. En fonctionnement continu, chaque premier janvier, la nouvelle année écrasera la dernière année connue.

Mais évidemment que le stockage en flash a mis la zone dans mon beau programme.

En effet, pour graver dans le marbre, il faut modifier la vitesse du MSP430, donc son horloge, et là, c'est le drame. J'effectue donc l'écriture en flash après modification de la vitesse d'horloge, puis j'exécute un redémarrage complet.

La loi de l'emmerdement maximum s'applique toujours.

Calibration

Disposant d'une horloge atomique, ce serait stupide de ne pas en profiter pour calibrer le quartz attaché au MSP430, qui sert d'horloge RTC.

La procédure est bête et méchante : je compte le nombre total d'interruptions (les 512 interruptions par seconde), je note l'heure de départ, et je compare à l'heure courante.

J'ai constaté 20 secondes de décalage en une dizaine de jours, soit 23 ppm, que je compense régulièrement. Cela améliore la précision, mais bon, ça ne sera jamais parfait, les dérives étant liés à divers paramètres comme la température.

Je stocke en flash la valeur de la dérive. Plus le temps passe, plus la mesure de la dérive sera précise. Là j'ai atteint 10 jours, j'attends à chaque fois le double de la mesure précédente, soit 20 jours, puis ce sera 40. Il faut évidemment que l'horloge ne s'arrête pas (et elle le fait au moment de l'écriture en flash).

Anniversaires

Ce serait bête d'avoir une horloge totalement programmée par mes soins, qui connait la date avec précision, et qu'elle ne me souhaite pas mon anniversaire 😁

J'ai poussé le vice jusqu'à afficher le nombre de jours depuis la naissance, ainsi que le nombre de secondes.
1 milliard de secondes, c'est environ 33 ans.

Pour faire ça, j'ai marné un moment avec les procédures fournies par le compilateur la routine mktime() car il existe un problème de « bug de l'an 2000 », en l'occurrence 2036.

En effet, on a trouvé le moyen de définir la date « zéro » comme étant le premier janvier 1900. Et quand on compte les secondes avec un entier sur 32 bits, eh bien ça boucle à zéro à 136 ans. et c'est dans pas si longtemps, il fallait prendre 64 bits. Dans le compilateur TI, on peut forcer ce genre de choses avec _TI_TIME_USES_64, mais méfiant comme je suis, j'ai vérifié en détail.

Autre détail casse-pied : la routine mktime() prend 0 pour le mois de janvier. Foutus programmeurs pour qui la première case est zéro, et pas un. DCF77 met 1 pour le mois de janvier...

Galerie

Vue générale
Le microcontrôleur est dans le trou où on voit un circuit imprimé vert.
Je voulais voir les bits reçus, et leur interprétation en temps réel
Chaque groupe à nombre est relié à un affichage à 7 segments
Le bit 32 a été reçu, c'est un zéro car la largeur est de l'ordre de 100 ms. Le bit 33 est en réception, l'heure n'est pas complète, il manque le premier chiffre.
L'alignement a été simplement fait en soudant avec les sous-circuits à l'envers.
La grande carte principale ne contient pratiquement pas de composants.
Les afficheurs sont de petits modules, eux-mêmes très simples, qu'il suffit de chainer car ce sont tous des neopixels.
La grande carte alphanumérique est certainement la plus complexe, tout en restant simple, on voit bien la répétition, et l'utilisation d'un IS31FL3730 pour deux digits 5x7.
Et le multiplexeur I2C, car il n'existe que 4 adresses possibles pour les IS31FL3730.
Des petits modules à 2 ou 3 digits suivant le cas.
Un module plus complexe pour heure/minute/seconde/dixième.



Fonctionnement basique de l'horloge.
L'horloge est capable d'afficher pas mal d'information sur son état, d'exécuter un self-test qui m'a permis de mettre au point, ainsi que de me souhaiter mon anniversaire...
Au bout d'un moment, je n'affiche plus que l'heure si tout est OK, par sobriété d'affichage. Mais il suffit que j'appuie sur l'unique bouton poussoir pour afficher la totalité des infos.

Il ne me reste plus qu'à lui trouver un joli cadre.

Projets similaires

Citons les réalisations apparemment basées sur le travail d'Erik de Ruiter, c'est ce que j'ai trouvé de plus proche par rapport à ma proposition :

Ces horloges sont basées sur une carte Arduino, avec beaucoup de circuits électroniques, j'ai fait nettement plus simple et plus efficace (enfin, je trouve).

Ils utilisent le "superfiltre" d'Udo Klein, et comme je l'ai expliqué, il faut d'abord faire un verrouillage de phase, ensuite la lecture des bits devient assez triviale, et effectivement, on réalise un second niveau de verrouillage de phase en ayant lu une trame complète, mais c'est très secondaire. Et on peut tout faire sur un seul microcontrôleur.


Et quelques autres réalisations :


Voilà, j'ai maintenant l'heure atomique, avec une précision meilleure que la centaine de millisecondes en permanence.