Come ho trasformato una sveglia economica in un heads-up display per le mie corse (Parte 1)
Il problema: i miei dati arrivavano sempre un secondo troppo tardi
Corro. Tanto. E come la maggior parte dei runner, i due numeri a cui tengo mentre mi muovo sono semplicissimi: tempo trascorso e frequenza cardiaca.
Il problema è che ottenere quei numeri non è mai stato semplice come dovrebbe essere. Ero a metà corsa, polmoni al lavoro e gambe che macinavano strada, e mi sarebbe bastata un’occhiata veloce all’orologio. Peccato che quella “veloce” non lo fosse mai. Alzavo il polso, lo giravo per vedere lo schermo, e poi… aspettavo. Perché nove volte su dieci lo schermo era spento. Così restavo lì impalato (o peggio, continuavo a correre rischiando di inciampare) ad aspettare che il display si svegliasse, cercare la schermata giusta e, alla fine, leggere una frequenza cardiaca già vecchia di qualche secondo.
Quello che doveva essere un’occhiata era diventato un piccolo rituale: solleva, ruota, aspetta, strizza gli occhi. Mi spezzava il ritmo ogni singola volta.
Allora ho iniziato a pensarci come un ingegnere pensa a qualsiasi cosa gli dia fastidio: l’interfaccia è sbagliata. Non volevo essere io ad andare verso il dato. Volevo che fosse il dato a venire verso di me, piazzato proprio davanti agli occhi, sempre acceso, senza bisogno di alzare il polso né di risvegliare uno schermo.
L’idea ha preso forma: proiettare i numeri di cui ho bisogno direttamente davanti a me. Tempo trascorso e frequenza cardiaca, sospesi lì, sempre visibili.
La metà facile
La frequenza cardiaca si è rivelata la parte banale di tutto il progetto: il merito va tutto a uno standard fatto come si deve.
Quasi tutte le fasce cardio moderne parlano BLE (Bluetooth Low Energy) e, cosa fondamentale, la maggior parte implementa lo standard Heart Rate Service. Questo significa che non ho dovuto fare reverse engineering di niente: esiste un profilo GATT pubblicato, un UUID di servizio noto (0x180D) e una caratteristica nota (0x2A37) che trasmette i battiti al minuto. Plug-and-play, più o meno.
Così sono andato da Decathlon e ho preso una fascia cardio BLE di base. Niente di speciale, la più economica che dichiarasse supporto Bluetooth.
Un ESP32, con la sua radio Bluetooth integrata, si collega a questo aggeggio quasi subito, a patto di avere il codice giusto. Ecco un esempio volutamente ridotto all’osso, giusto per mostrare quanto poco serva per iscriversi alle notifiche di frequenza cardiaca usando la libreria NimBLE:
#include <NimBLEDevice.h>
// Standard BLE Heart Rate Service + Measurement characteristic
static NimBLEUUID hrServiceUUID("180D");
static NimBLEUUID hrCharUUID("2A37");
uint16_t currentHR = 0;
// Called every time the strap pushes a new measurement
void hrNotifyCB(NimBLERemoteCharacteristic* chr,
uint8_t* data, size_t len, bool isNotify) {
if (len < 2) return;
// First byte = flags. Bit 0 tells us if HR is 8-bit or 16-bit.
if (data[0] & 0x01) {
currentHR = data[1] | (data[2] << 8); // 16-bit value
} else {
currentHR = data[1]; // 8-bit value
}
Serial.printf("HR: %d bpm\n", currentHR);
}
void setup() {
Serial.begin(115200);
NimBLEDevice::init("");
// Scan, connect to the first device advertising the HR service,
// grab the characteristic, and subscribe to notifications.
NimBLEScan* scan = NimBLEDevice::getScan();
NimBLEScanResults results = scan->getResults(5 * 1000);
for (int i = 0; i < results.getCount(); i++) {
const NimBLEAdvertisedDevice* dev = results.getDevice(i);
if (dev->isAdvertisingService(hrServiceUUID)) {
NimBLEClient* client = NimBLEDevice::createClient();
if (client->connect(dev)) {
auto* svc = client->getService(hrServiceUUID);
auto* chr = svc ? svc->getCharacteristic(hrCharUUID) : nullptr;
if (chr && chr->canNotify()) {
chr->subscribe(true, hrNotifyCB);
}
}
break;
}
}
}
void loop() {
// currentHR is now updated in the background, every heartbeat-ish.
delay(1000);
}
Tutto qui. La fascia trasmette, l’ESP32 ascolta, e currentHR resta sempre lì con un numero fresco. Metà del progetto, risolta praticamente il primo giorno.
L’altra metà, però, cioè come diavolo faccio a proiettare questi numeri proprio sotto gli occhi, è dove le cose si sono fatte interessanti.
La metà divertente
Ho rimuginato per un po’ sul problema del “display”. Uno schermo normale non andava bene: sarei tornato esattamente al punto di partenza, a sollevare il polso e strizzare gli occhi. Mi serviva qualcosa che mettesse i numeri là fuori, su una superficie, grandi e sempre accesi.
E poi è riaffiorato un ricordo. Da bambino avevamo una di quelle sveglie stravaganti che proiettavano l’ora sul soffitto: grandi numeri rossi che brillavano lassù, così potevi girarti alle 3 di notte e leggere l’ora senza muoverti.
Ecco. Era esattamente il meccanismo di cui avevo bisogno. Un dispositivo che sa già come prendere delle cifre e lanciarle, luminose, su una superficie davanti a me.
Così sono andato su Amazon e ho comprato la sveglia proiettore più economica che ho trovato.
Aprirla
Ecco la prima piacevole sorpresa. Quando ho smontato la sveglia, ho scoperto che non era un’unica unità integrata: erano di fatto due moduli separati imbullonati insieme:
- La sveglia vera e propria, con il proprio display LCD frontale.
- Il modulo proiettore, un gruppo distinto con il LED, la lente e un proprio display numerico che si proietta sul muro.
Quel secondo modulo è il pezzo che mi interessa davvero. Non mi importa niente dello schermo frontale della sveglia. Mi interessa l’oggetto che proietta le cifre. Se fossi riuscito a dirottare solo quel modulo e dargli in pasto i miei numeri, avrei avuto il mio heads-up display.
Quando ho separato il modulo proiettore dalla scheda principale, ho scoperto che i due erano collegati da un piccolo cavetto a nastro: sette fili.
Sette fili. Tutta la conversazione tra il cervello della sveglia e il proiettore passava lì. Se fossi riuscito a capire cosa si dicevano quei sette fili, avrei potuto dirlo io stesso.
Leggere i fondi di caffè
Questa è la parte di qualsiasi progetto di reverse engineering in cui o sei fortunato, oppure tiri fuori l’oscilloscopio e ti prepari a soffrire.
Sono stato fortunato.
Stampate direttamente sul PCB, accanto al connettore, c’erano le etichette serigrafate per ogni pin. Chiunque avesse progettato quella scheda mi aveva lasciato le briciole di pane:
- GND, massa, il riferimento rispetto a cui si misura tutto il resto.
- 4.7V, l’alimentazione del modulo proiettore.
- SDA, dati seriali.
- SCL, clock seriale.
- CS, chip select.
- DIM-K, controllo della dimmerazione (luminosità del display proiettato).
- LED, controllo del LED di proiezione / retroilluminazione.
Nel momento in cui ho letto quelle etichette, la nebbia si è diradata. GND e 4.7V sono solo alimentazione. DIM-K e LED riguardano quanto brilla l’oggetto. Restano tre pin, SDA, SCL e CS, che fanno il lavoro vero di comunicazione. E quei tre nomi li tradiscono subito: siamo di fronte a un bus seriale sincrono, dati più clock più chip-select. Su quale variante esattamente, ci torniamo tra un attimo.
Seguendo quelle linee dati, convergono tutte su un singolo chip: un TM1621D.
Conosciamo il TM1621D
Il TM1621D è un driver per LCD a segmenti, un chip il cui unico compito è prendere comandi da un microcontrollore e trasformarli nei segmenti giusti accesi su un LCD. Il dettaglio chiave: ha una propria RAM di display interna. Non sei tu a dover aggiornare continuamente il display. Scrivi i dati dei segmenti a un indirizzo di memoria, il chip li memorizza, e l’LCD si aggiorna restando così finché non scrivi qualcosa di nuovo. Comunica esattamente sulle tre linee che abbiamo trovato: dati, clock e chip-select.
Qualsiasi cifra appaia in quella proiezione è decisa interamente da cosa viene scritto in quella RAM. Controlla le scritture, controlla il display.
SPI (più o meno)
Date le tre linee di segnale, dati, clock e chip-select, la comunicazione assomiglia a SPI, e per i nostri scopi possiamo trattarla come tale.
Un ripasso veloce di SPI (Serial Peripheral Interface) per chi non ne ha mai bit-bangato uno alle 2 di notte: è un protocollo seriale sincrono, il che significa che i dati escono un bit alla volta, in sequenza, e ogni singolo bit è accoppiato a un impulso di una linea di clock, così entrambe le parti sanno esattamente quando leggerlo. Le linee SPI classiche sono:
- una linea dati, su cui il controller invia i bit uno dopo l’altro,
- una linea clock (
SCL/SCK, un impulso per bit), e - una linea chip-select (
CS, portata al suo livello attivo per dire “ehi, questo messaggio è per te, inizia ad ascoltare”).
Il balletto è sempre lo stesso: attiva il chip-select, poi per ogni bit metti il bit sulla linea dati e genera un impulso di clock così il chip lo memorizza, ripeti per tutti i bit, poi rilascia il chip-select per chiudere il messaggio. Non c’è indirizzamento, non ci sono conferme, non c’è handshaking: solo “ecco i bit, ecco il clock, prendili”.
L’interfaccia del TM1621D non è uno SPI da manuale in senso strettissimo (ha una propria strutturazione comando/dati, ed è tecnicamente una variante a 3 fili), ma il meccanismo di fondo è identico: far uscire i bit uno alla volta scandendoli con il clock, partendo dal più significativo, con il chip-select sempre attivo.
Il che mi porta alla funzione più soddisfacente dell’intero progetto, il Santo Graal. Una volta che questa funzionava, tutto il resto era solo capire quali bit mandare. Questa è la funzione che fisicamente sposta un blocco di dati verso il chip:
void lcdShift(unsigned long data, int bits) {
unsigned long bitmask = 1UL << (bits - 1);
digitalWrite(latchPin, LOW);
for (int i = 0; i < bits; i++) {
digitalWrite(clkPin, LOW);
digitalWrite(dataPin, (data & bitmask) ? HIGH : LOW);
delayMicroseconds(2);
digitalWrite(clkPin, HIGH);
delayMicroseconds(10);
bitmask >>= 1;
}
digitalWrite(latchPin, HIGH);
delayMicroseconds(2);
}
La funzione abbassa il chip-select (“messaggio in arrivo”), poi percorre ogni bit dal più significativo al meno significativo: abbassa il clock, imposta la linea dati, alza il clock. Quel fronte di salita è il momento in cui il TM1621D cattura il bit. Una volta inviati tutti i bit, il chip-select torna alto e la transazione è completa. Sto pilotando quei pin a mano con digitalWrite() invece di usare la periferica SPI hardware dell’ESP32, una tecnica chiamata bit-banging. Più lenta, ma mi ha dato il controllo totale sui tempi mentre stavo ancora capendo un protocollo sconosciuto.
Il motivo per cui la chiamo il Santo Graal è semplice: nel momento in cui questa funzione ha funzionato, ho potuto mettere qualsiasi pattern di bit in quel chip. L’hardware non era più una scatola misteriosa: era un dispositivo di output che ora controllavo completamente.
A che punto siamo, e cosa viene dopo
Resta un problema. Poter inviare qualsiasi pattern di bit non è la stessa cosa che sapere quale pattern di bit accende quale segmento. So parlare la lingua del chip, ma non ho ancora il dizionario. Ed è di questo che parla la Parte 2.
Prossimamente, Parte 2: mappare i segmenti, ovvero come ho insegnato a una sveglia rotta a contare.
Lodovico Cortelazzo
Ex atleta della nazionale. Scienze motorie e ingegneria. Sto costruendo ODKI.
Quindici anni di allenamento, otto a livello nazionale, mi danno uno sguardo specifico su cosa fa davvero la tecnologia sportiva rispetto a cosa promette. Unisco questo a esperienza pratica di ingegneria e prodotto costruendo ODKI, per capire non solo come costruire sport-tech, ma cosa vale davvero la pena costruire. Se stai lavorando a qualcosa in questo ambito e vuoi questo punto di vista, scrivimi.
Lavora con me →