Introduction
Nous souhaitons réaliser une horloge "Persistance Of Vision": un support rectangulaire horizontal, fixé sur la partie tournante d'un ventilateur de PC 12V axe vertical (dont les pâles ont été ôtées). A l'extrémité du support sont fixées 12 petites Leds blanches verticales électriquement reliées, à travers, chacune, une résistance de 330 ohms aux 12 sorties d'un Arduino Nano fixé à plat sur le support.
L'ensemble tourne et, en envoyant des impulsions correctes dans les Leds, on peut afficher 3 fois par tour l'heure sous la forme 12:34:56 en faisant clignoter les ":" chaque seconde.
Une fourche optique ou un capteur à effet hall montés sur le support et une lame ou un aimant montés fixe sur le ventilateur permettraient de synchroniser la position de l'heure. Nous allons essayer, dans un premier temps de ne pas synchroniser mais de choisir la tempo et la vitesse de rotation pour un affichage à peu près stable. En fait l'affichage va forcément lentement défiler dans un sens ou dans l'autre.
Si le support vertical qui maintient les 16 Leds est petit et s'il est peint en noir, en rotation, il est quasi invisible : on voit donc "flotter" l'heure dans l'air et l'effet est relativement spectaculaire.
Il faut évidemment alimenter l'Arduino. Pour cela, plusieurs solutions :
2 inductances, une mobile et une fixe, proches, concentriques et un générateur "HF" et un récepteur
Ici, nous avons prévu de coller (Araldite) une vis M3x20 au centre de la partie mobile du ventilateur. Dans l'ordre: une rondelle métal, en entretoise laiton d3 intérieur, une rondelle plastique, une entretoise plastique à l'intérieur d'une entretoise laiton d4 intérieur, une rondelle plastique percée d'un trou dans lequel passe un fil de cuivre soudé à l'entretoise d4. Ainsi l'entretoise d4 est isolée. Coté Arduino, on se connecte, à travers un filtre RC à la vis et au fil. Coté fixe sur le ventilateur, 2 fils de cuivre rigides munis de ressort frottent sur les entretoises et sont reliés à la base du ventilateur.
Ceci constitue donc un joint électrique tournant pour alimenter l'Arduino pendant la rotation. Il est préférable d'avoir des joints tournants avec le plus petit diamètre, ce qui limite les vibrations, et surtout, permet d'appuyer relativement fort pour avoir un bon contact sans nécessiter trop de couple au moteur.
Nous allons probablement sous-alimenter le ventilateur 12V avec un régulateur LM317 ou une résistance de puissance pour diminuer le bruit les vibrations, l'usure et la consommation : il faut le faire tourner le plus lentement possible en conservant la persistance rétinienne pour l'affichage (pas trop de scintillement). On pourra en profiter pour prévoir un réglage de vitesse pour limiter le défilement trop rapide de l'affichage.
Le code : nous avons demandé à ChatGPT5 de nous générer le code : (345 lignes en 5 mn !)
Prompt :
écris le code C Arduino nano pour une horloge "pov" : 12 Leds verticales reliés avec chacune une résistance de 330 ohms sur les 12 sorties D2 à D13; montées sur un ventilateur de PC 12V qui tourne à 600 RPM. l'horloge doit afficher 3 fois l'heure (24h) avec le format : 13:24:56. Les : clignotent une fois par seconde. Il n'y a pas de réglages. L'horloge démarre à 12h. chaque chiffre est codé par 8x12 points.
Résultat :
#define VERSION 0.1
#define DATE "26/10/2025"
/*
POV Clock 12 LEDs (D2..D13), 600 RPM fan (100 ms/tour)
- 12 LED verticales, résistances 330 Ω, anodes sur D2..D13, cathodes au GND
- D2 en bas, D13 en haut
- Affiche 3 fois "HH:MM:SS" par tour
- Format chiffres 8x12 (8 colonnes, 12 lignes/LED)
- Deux-points clignotants chaque seconde
- Démarrage à 12:00:00 sans réglages
- Synchronisation temporelle basée sur 100 ms par révolution
- Microcontrôleur: Arduino Nano (ATmega328P)
Perplexity GPT5
ETAT : **********************************
compilation OK, pas testé
1 Fichier, Enregistrer
2 Fichier, Préférences, Paramètres, Afficher les résultats détaillés pendant, Téléversement, OK
3 Croquis, Vérifier, Compiler : pas d'erreurs
4 Brancher cordon USB C entre PC et Nano
5 Outils, type de carte Arduino Nano
6 Outils, Processeur, Atmega 328P (sinon erreur avrdude...)
7 Outils, Port, COM4
8 Croquis, Téléverser
9 Affiche Téléversement....
*/
#include <Arduino.h>
// Mapping des 12 LED: index 0..11 correspond aux sorties D2..D13
const uint8_t LED_PINS[12] = {2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};
// Dimensions des glyphes
const uint8_t DIGIT_COLS = 8; // 8 colonnes par chiffre
const uint8_t DIGIT_ROWS = 12; // 12 lignes (12 LED verticales)
const uint8_t COLS_PER_SYMBOL = DIGIT_COLS; // pour chiffres
const uint8_t COLS_COLON = 2; // 2 colonnes pour ":"
const uint8_t SYMBOLS_PER_BLOCK = 6; // H1 H2 : M1 M2 : S1 S2
const uint8_t REPEATS_PER_REV = 3; // 3 fois par tour
// Total colonnes par tour = 3 * (6 symboles chiffres/colon + 2x ":" à 2 colonnes) = 3 * (8+8+2+8+8+2+8+8) = 3 * 52 = 156
// Mais l’énoncé impose 8x12 par chiffre; pour simplicité temporelle, on montrera chaque répétition séquentiellement dans ~100/3 ms.
// On cadencera les colonnes par des délais microsecondes calculés sur la fenêtre de répétition.
const uint16_t RPM = 600; // vitesse cible
const uint32_t REV_US = 1000000UL * 60UL / RPM; // 100000 microsecondes à 600 RPM
const uint8_t REPS = REPEATS_PER_REV;
const uint32_t REP_WINDOW_US = REV_US / REPS; // ~33333 us par répétition
// Mise en page "HH:MM:SS"
struct Symbol
{
bool isColon;
uint8_t digit; // 0..9 si isColon == false
};
// Police 8x12 pour chiffres 0..9, bit 0 = LED rang 0 (en bas si vous câblez ainsi), bit 11 = LED rang 11 (haut)
//
// Chaque entrée est un tableau de 8 colonnes, chaque colonne est un uint16_t dont seuls 12 bits (0..11) sont utilisés.
// orientation suivant montage: si la première LED en bas doit être bit 11, inverser les bits lors de l’affichage.
const uint16_t font8x12[10][8] =
{
// 0
{
0b000111111110,
0b001111111111,
0b011100000111,
0b011000000011,
0b011000000011,
0b011100000111,
0b001111111111,
0b000111111110
},
// 1
{
0b000000110000,
0b000001110000,
0b000011110000,
0b000000110000,
0b000000110000,
0b000000110000,
0b001111111110,
0b001111111110
},
// 2
{
0b001111111100,
0b001111111110,
0b000000001110,
0b000000111100,
0b000011110000,
0b001111000000,
0b001111111110,
0b001111111110
},
// 3
{
0b001111111100,
0b001111111110,
0b000000001110,
0b000001111100,
0b000001111100,
0b000000001110,
0b001111111110,
0b001111111100
},
// 4
{
0b000000111100,
0b000001111100,
0b000011001100,
0b000110001100,
0b001100001100,
0b001111111110,
0b001111111110,
0b000000001100
},
// 5
{
0b001111111110,
0b001111111110,
0b001100000000,
0b001111111100,
0b000000001110,
0b000000001110,
0b001111111110,
0b001111111100
},
// 6
{
0b000111111110,
0b001111111110,
0b001100000000,
0b001111111100,
0b001100001110,
0b001100001110,
0b001111111110,
0b000111111100
},
// 7
{
0b001111111110,
0b001111111110,
0b000000001110,
0b000000011100,
0b000000111000,
0b000001110000,
0b000011100000,
0b000111000000
},
// 8
{
0b000111111100,
0b001111111110,
0b001100001110,
0b000111111100,
0b000111111100,
0b001100001110,
0b001111111110,
0b000111111100
},
// 9
{
0b000111111100,
0b001111111110,
0b001100001110,
0b001111111110,
0b000111111110,
0b000000001110,
0b001111111110,
0b001111111100
}
};
// Deux-points ":" en 12 rangées sur 2 colonnes (points vers rangs ~4 et ~7)
// Colonnes identiques pour simplifier, actives si blinkColon == true
const uint16_t colonCol = (1 << 4) | (1 << 5) | (1 << 7) | (1 << 8); // 2 points un peu épais (2 rangs chacun)
volatile uint32_t lastMicros = 0;
// Temps logiciel
uint8_t sec = 0, min_ = 0, hour = 12; //12:00:00 à la mise sous tension
// Orientation: si votre première LED (D2) est en haut au lieu d’en bas, mettez true
const bool invertRows = false;
// Allume les LED selon un masque 12 bits
inline void showColumn(uint16_t mask12)
{
if (invertRows)
{
// inverser les 12 bits
uint16_t inv = 0;
for (uint8_t i = 0; i < 12; i++)
{
if (mask12 & (1 << i)) inv |= (1 << (11 - i));
}
mask12 = inv;
}
for (uint8_t i = 0; i < 12; i++)
{
digitalWrite(LED_PINS[i], (mask12 & (1 << i)) ? HIGH : LOW);
}
}
// Éteint toutes les LED
inline void clearAll()
{
for (uint8_t i = 0; i < 12; i++)
{
digitalWrite(LED_PINS[i], LOW);
}
}
// Affiche une colonne d’un chiffre donné (0..9), index de colonne 0..7
inline void showDigitCol(uint8_t digit, uint8_t col)
{
uint16_t colMask = font8x12[digit][col];
showColumn(colMask);
}
// Affiche une colonne du ":" (2 colonnes identiques)
inline void showColonCol(bool blinkOn)
{
showColumn(blinkOn ? colonCol : 0);
}
// Convertit chiffre décimal 0..59 -> dizaines et unités
inline void split2(uint8_t v, uint8_t &tens, uint8_t &ones)
{
tens = v / 10;
ones = v % 10;
}
// Un bloc = "HH:MM:SS"
void displayBlock(uint32_t window_us, bool blinkColon)
{
// Répartition du temps: 6 symboles chiffres (8 colonnes chacun) + 2 colonnes ":" deux fois
// On affiche dans l’ordre: H1 H2 : M1 M2 : S1 S2
uint8_t hT, hO, mT, mO, sT, sO;
split2(hour, hT, hO);
split2(min_, mT, mO);
split2(sec, sT, sO);
// Définir séquence des symboles
Symbol seq[8] =
{
{false, hT}, {false, hO}, {true, 0}, {false, mT},
{false, mO}, {true, 0}, {false, sT}, {false, sO}
};
// Nombre de colonnes totales: 6*8 + 2*2 = 52 colonnes
const uint16_t totalCols = 52;
// Durée par colonne dans la fenêtre
// Note: utilisation de timing rigoureux en microsecondes
uint32_t tStart = micros();
uint32_t perCol = window_us / totalCols;
for (uint8_t si = 0; si < 8; si++)
{
if (seq[si].isColon)
{
for (uint8_t c = 0; c < COLS_COLON; c++)
{
showColonCol(blinkColon);
// attente jusqu’au prochain slot
uint32_t target = tStart + perCol;
while ((int32_t)(micros() - target) < 0) { /* busy wait */ }
tStart = target;
clearAll();
}
}
else
{
// chiffre
for (uint8_t c = 0; c < DIGIT_COLS; c++)
{
showDigitCol(seq[si].digit, c);
uint32_t target = tStart + perCol;
while ((int32_t)(micros() - target) < 0) { /* busy wait */ }
tStart = target;
clearAll();
}
}
}
}
// Avance l’horloge logicielle de 1 seconde
inline void tickOneSecond()
{
sec++;
if (sec >= 60)
{
sec = 0;
min_++;
if (min_ >= 60)
{
min_ = 0;
hour++;
if (hour >= 24) hour = 0;
}
}
}
void setup()
{
for (uint8_t i = 0; i < 12; i++)
{
pinMode(LED_PINS[i], OUTPUT);
digitalWrite(LED_PINS[i], LOW);
}
// Démarre à 12:00:00
hour = 12; min_ = 0; sec = 0;
lastMicros = micros();
}
void loop()
{
// À 600 RPM: 100 ms/tour
// On affiche 3 répétitions, chacune occupant ~33.33 ms
bool blinkColon = (sec % 2 == 0); // clignote chaque seconde (visible sur toutes les répétitions de cette seconde)
uint32_t repStart = micros();
for (uint8_t r = 0; r < REPS; r++)
{
displayBlock(REP_WINDOW_US, blinkColon);
}
// Ajuste le temps écoulé pour viser 100 ms par tour
uint32_t elapsed = micros() - repStart;
if (elapsed < REV_US)
{
delayMicroseconds(REV_US - elapsed);
}
// Comptage du temps réel: 1 tour = 100 ms; incrémenter 1 seconde toutes 10 révolutions.
// On peut compter les tours par cumul de 100 ms; ici on mesure par boucle:
static uint16_t msAccum = 0;
msAccum += 100; // ~100 ms par tour nominal
if (msAccum >= 1000)
{
msAccum -= 1000;
tickOneSecond();
}
}
Réalisation :
Il faut commencer par récupérer un ventilateur de PC 12V lent et silencieux et couper les pales. Notre ventilateur 12V tourne encore assez vite (et silencieusement) en 5V (il consomme alors moins de 40mA avec ses pales).
Il faut réaliser un support rectangulaire d'environ 25x100. A une extrémité est fixé l'Arduino (prise USB-C vers l'extérieur) et à l'autre le petit support vertical (le plus petit possible) qui maintient les 12 Leds. Quelque part (voir plus loin), le support rectangulaire est percé d'un trou Ø3 pour le fixer (équilibré) sur le centre du ventilateur.
Une partie du matériel nécessaire :
1 Ventilateur 12V
2 Circuits imprimés avec trous pour le support et le support Leds
Des Leds rouges
1 Arduino Nano USB-C
12 Résistances de 330 ohms
Un morceau de fil de cuivre 1.5²
Remarques :
Il faut que les Leds brillent suffisamment fort pour, qu'avec la persistance rétinienne, l'intensité de l'affichage soit correcte
Pour télécharger l'Arduino, il faut, évidemment que l'horloge ne tourne pas, ce qui peut être un problème si on veut ajuster la vitesse de l'affichage par logiciel (il est plus facile de régler la vitesse du moteur : résistance série ?).
Il est préférable de fabriquer le support complet avec l'Arduino, le filtre, les résistances, le support de Leds et les Leds AVANT de percer un trou pour le centrer sur l'axe du ventilateur, pour qu'il soit bien équilibré...
L'ensemble, devrait consommer moins de 100mA
Il faut éviter de toucher l'affichage pendant que l'horloge tourne ou alors il faut prévoir une protection transparente... C'est aussi une des raisons pour faire tourner le support "le moins vite possible"
On pourra monter le ventilateur à l'intérieur d'un support cylindrique suffisamment lourd, ouverture vers le bas pour : masquer le ventilateur, protéger un peu la partie tournante et lester l'ensemble pour améliorer la stabilité
Le ventilateur sans ses pales : nous avons creusé un petit trou au centre dans lequel est collé, à l'Araldite, à 50°C, la tête d'une vis M3x20 qui recevra les 2 joints électriques tournant (les entretoises laiton), le support rectangulaire (fixé avec un écrou et une rondelle à dents). Le support peut se desserrer essentiellement en cas de contact pendant la rotation (à éviter).
Réalisation des joints électriques tournant en laiton et cuivre pour transmettre le 5V pendant la rotation:
Le joint du bas est relié à la vis du moteur, il est connecté à l'Arduino par une cosse ronde sous l'écrou supérieur (son entretoise laiton Ø3 intérieur est trop longue)
Le joint du haut est constitué d'une entretoise laiton (un peu trop longue) Ø4 intérieur avec un tube plastique : elle est isolée par le tube et les 2 rondelles plastiques et reliées à l'Arduino par un fil de cuivre émaillé. (celui de gauche)
Nous suggérons de commencer par la réalisation de ces joints tournants. Pour les essais, on peut réaliser un support rectangulaire (fixé sur la vis du moteur) très simplifié : juste une Led à l'extrémité avec une résistance de 330 ohms. Sur la photo, le support provisoire est très déséquilibré et l'ensemble, évidemment, vibre un peu. Ça permet de vérifier que les Leds ne s'allument pas en pointillés. Ça donne aussi une idée de la luminosité finale. Et ça permet de régler la tension des 2 ressorts des joints tournants. Si la tension est trop élevée, le couple augmente et le moteur ralentit, voire, ne démarre pas...
Remarque : si on veut 2 bobines d'induction ou créer un joint tournant au-dessus du montage avec un diamètre plus petit, il faudrait une "fourche" rigide reliée à la partie fixe du ventilateur et qui passe derrière le passage des Leds : c'est un peu dommage pour l'esthétique, les dimensions...
Sur cette photo avec le support provisoire ci-dessus, c'est le temps d'ouverture de l'appareil photo qui donne l'impression que la Led n'est pas allumée sur tout le cercle.
Il reste à optimiser les contacts et les ressorts (qui ne sont pas assez souples) pour freiner le moins possible le moteur tout en garantissant un contact correct.
A noter que ce support provisoire n'est pas du tout équilibré...
Normalement, les joints tournants devraient s'améliorer avec le temps...
La photo montre bien que toute la partie mobile est à peu près invisible à part la Led...
Réalisation du support rectangulaire (en époxy). Une fois le support entièrement terminé avec le filtre, les Leds et leur support, les 12 résistances et le câblage, il faudra déterminer le centre de gravité pour y percer un trou Ø3 pour le fixer sur le ventilateur. On pourra peindre le dessus de la partie mobile en noir... On peut installer un "câble" entre le haut des Leds et l'intérieur du support rectangulaire pour renforcer la liaison entre le support des Leds et le support rectangulaire... (en cas de contact ou de choc avec la partie mobile pendant la rotation). Nous rappelons que la partie mobile qui maintient les Leds est relativement "invisible" pendant la rotation...
That's All Folks !
Commencé le 26/10/2025
A jour au 26/10/2025