
In Ho provato a spiegare agli hobbisti dell'elettronica, cresciuti con l'Arduino, come e perché dovrebbero leggere i datasheet e altra documentazione sui microcontrollori. Il testo è venuto lungo, quindi ho promesso di mostrare esempi pratici in un articolo separato. Bene, ho accettato la sfida…
Oggi mostrerò come, usando i datasheet, risolvere compiti piuttosto semplici ma necessari per molti progetti sui controller STM32 (Blue Pill) e STM8. Tutti i progetti dimostrativi sono dedicati ai miei amati LED, che accenderemo in grandi quantità, per cui sarà necessario utilizzare vari dispositivi periferici interessanti.
Il testo è di nuovo venuto enorme, quindi per comodità faccio un sommario:
Dichiarazione: non sono un ingegnere, non pretendo di avere profonde conoscenze in elettronica, l'articolo è destinato a chi, come me, è un appassionato. In realtà, la mia audience ideale era io stesso di due anni fa. Se qualcuno mi avesse detto all'epoca che leggere i datasheet su un chip sconosciuto non è spaventoso, non avrei speso tanto tempo a cercare frammenti di codice in internet e ad inventare soluzioni di fortuna con forbici e cerotti.
Il fulcro di questo articolo sono i datasheet, non i progetti, quindi il codice potrebbe non essere troppo rifinito e spesso improvvisato. I progetti stessi sono molto semplici, sebbene validi per una prima esperienza con un nuovo chip.
Spero che il mio articolo possa aiutare qualcuno in una fase simile di immersione nell'hobby.
STM32
16 LED con DM634 e SPI
Un piccolo progetto utilizzando il Blue Pill (STM32F103C8T6) e il driver LED DM634. Utilizzando i datasheet, analizzeremo il driver, le porte IO STM e imposteremo lo SPI.
DM634
Chip taiwanese con 16 uscite PWM a 16 bit, è possibile collegarli in catena. Il modello a 12 bit è noto per un progetto domestico. . A suo tempo, scegliendo tra DM63x e il noto TLC5940, ho optato per DM per diversi motivi: 1) il TLC su AliExpress è sicuramente un falso, mentre questo no; 2) il DM ha un PWM autonomo con il proprio generatore di frequenza; 3) potevo acquistarlo a buon prezzo a Mosca, senza dover aspettare il pacco da Ali. E, naturalmente, mi interessava imparare a gestire il chip da solo, piuttosto che usare una libreria già pronta. Attualmente i chip sono principalmente disponibili nella confezione SSOP24, non è difficile saldarli su un adattatore.
Poiché il produttore è taiwanese, il chip è descritto in un inglese cinese, il che significa che sarà divertente. Iniziamo a guardare la pinout (Pin Connection), per capire a quale piedino collegare cosa, e la descrizione dei pin (Pin Description). 16 uscite:

Fonti di corrente continua in ingresso (drain aperto)
Sink / Output open-drain – drain; fonte di corrente in ingresso; uscita attivamente collegata a terra, – i LED si collegano ai driver tramite i catodi. Elettricamente, ovviamente, non è affatto un «drain aperto» (drain aperto), ma nelle schede tecniche questa designazione per i pin in modalità drain si incontra spesso.
![]()
Resistori esterni tra REXT e GND per impostare il valore della corrente di uscita
Tra il pin REXT e la terra viene installato un resistore di riferimento che controlla la resistenza interna delle uscite, vedi il grafico a pagina 9 della scheda tecnica. Nel DM634, questa resistenza può anche essere gestita a livello software, impostando la luminosità globale (global brightness); in questo articolo non entrerò nei dettagli, metterò semplicemente qui un resistore da 2.2 – 3 kOhm.
Per capire come gestire il chip, diamo un'occhiata alla descrizione dell'interfaccia del dispositivo:

Ecco, l'inglese cinese in tutto il suo splendore. Tradurre questo è problematico, si può capire se si vuole, ma c'è un altro modo: vedere come si descrive il collegamento nella scheda tecnica del TLC5940 funzionalmente simile:

… Per immettere dati nel dispositivo sono necessari solo tre pin. Il fronte di salita del segnale SCLK sposta i dati dal pin SIN nel registro interno. Dopo che tutti i dati sono stati caricati, un breve segnale alto XLAT fissa i dati trasmessi nell'ordine interno. I registri interni sono flip-flop attivati dal livello del segnale XLAT. Tutti i dati vengono trasmessi dal bit più significativo.
Latch – flip-flop / fermo / blocco.
Rising edge – fronte di salita dell'impulso
MSB first – il bit più significativo (estremamente sinistro) in avanti.
per sincronizzare i dati – trasmettere dati in modo sequenziale (bit per bit).
La parola grilletto si incontra spesso nella documentazione dei chip e viene tradotta in vari modi, quindi per chiarezza permettetemi di
fare un breve approfondimentoIl driver LED è essenzialmente un registro a scorrimento. "Scorrimento" (shift) nel nome implica il trasferimento bit per bit dei dati all'interno del dispositivo: ogni nuovo bit inserito spinge tutta la catena davanti a sé. Poiché durante lo scorrimento nessuno vuole osservare un lampeggiamento caotico dei LED, il processo avviene in registri buffer, separati da una barriera (grilletto) – è una sorta di anticamera, dove i bit si allineano nella giusta sequenza. Quando tutto è pronto, la barriera si apre e i bit vengono inviati a lavorare, sostituendo il lotto precedente. La parola grilletto nella documentazione dei microchip implica quasi sempre tale barriera, indipendentemente dalle combinazioni in cui viene utilizzata.
Quindi, la trasmissione dei dati nel DM634 avviene così: impostiamo l'ingresso DAI sul valore del bit più significativo del LED più lontano, facciamo oscillare DCK su e giù; impostiamo l'ingresso DAI sul valore del bit successivo, facciamo oscillare DCK; e così via, fino a quando tutti i bit non sono stati trasmessi (clocked in), dopodiché facciamo oscillare LAT. Questo può essere fatto manualmente (bit-bang), ma è meglio utilizzare un'interfaccia appositamente progettata per questo scopo, fortunatamente disponibile sul nostro STM32 in due esemplari.
La Blue Pill STM32F103
Intro: i controller STM32 sono notevolmente più complessi dell'Atmega328, più di quanto possa sembrare. Per motivi di risparmio energetico, all'avvio la maggior parte della periferica è disattivata e la frequenza di clock è di 8 MHz da una fonte interna. Fortunatamente, i programmatori STM hanno scritto codice che porta il chip ai "calcolati" 72 MHz, e gli sviluppatori di tutti gli IDE noti hanno incluso questa procedura di inizializzazione, quindi non è necessario caricarlo (ma ). Tuttavia, sarà necessario attivare la periferia.
Documentazione: sulla Blue Pill è installato il popolare chip STM32F103C8T6, per il quale ci sono due documenti utili:
- per i microcontrollori STM32F103x8 e STM32F103xB;
- per l'intera gamma STM32F103 e oltre.
Nel datasheet potrebbero interessarci:
- Pinouts – layout dei piedini dei chip – nel caso decidessimo di realizzare le schede noi stessi;
- Mappa della memoria – una mappa della memoria per un chip specifico. Nel Manuale di riferimento è presente una mappa per l'intera gamma, in cui sono menzionati i registri che non sono presenti nel nostro caso.
- Tabella delle definizioni dei pin – un elenco delle funzioni principali e alternative dei pin; per la "pillola blu" si possono trovare online immagini più comode con un elenco dei pin e delle loro funzioni. Perciò, cerchiamo subito su Google il pinout della Blue Pill e teniamo a portata di mano un'immagine del genere:

NB: nell'immagine trovata su internet c'era un errore, segnalato nei commenti, che ringraziamo. L'immagine è stata sostituita, ma è una lezione: è meglio verificare le informazioni non provenienti dai datasheet.
Mettiamo via il datasheet, apriamo il Manuale di riferimento, e da ora in poi utilizziamo solo quello.
Procedura: ci occupiamo dell'input/output standard, configuriamo SPI, attiviamo le periferiche necessarie.
Input-output
Su Atmega328, l'input-output è implementato in modo molto semplice, il che può confondere a causa dell'abbondanza di opzioni STM32. Al momento ci interessano solo i pin di uscita, ma perfino questi hanno quattro varianti:

uscita a collettore aperto, uscita "push-pull", uscita "push-pull" alternativa, uscita a collettore aperto alternativa
"Push-pull" (push-pull) – è l'uscita standard di Arduino, il pin può assumere il valore HIGH o LOW. Ma ci sono delle difficoltà con l'uscita "a collettore aperto" , anche se in realtà qui è tutto molto semplice:


Configurazione dell'uscita / quando la porta è impostata come uscita: / buffer di uscita attivato: / – modalità collettore aperto: "0" nel registro di uscita attiva N-MOS, "1" nel registro di uscita mantiene la porta in modalità Hi-Z (P-MOS non si attiva) / – modalità "push-pull": "0" nel registro di uscita attiva N-MOS, "1" nel registro di uscita attiva P-MOS.
La differenza tra il collettore aperto (drain aperto) e il "push-pull" (push-pull) è che nel primo caso il pin non può assumere lo stato HIGH: quando si scrive un uno nel registro di uscita, passa in modalità ad alta impedenza (high impedance, Hi-Z). Quando si scrive uno zero, il pin si comporta allo stesso modo in entrambe le modalità, sia logicamente che elettricamente.
In modalità di uscita normale, il pin trasmette semplicemente il contenuto del registro di uscita. In modalità "alternativa", è controllato dalla periferica appropriata (vedi 9.1.4):
![]()
Se il bit della porta è configurato come uscita di funzione alternativa, il registro di uscita viene disattivato e il pin è collegato al segnale di uscita della periferica.
La funzionalità alternativa di ogni pin è descritta in Definizioni dei pin Il datasheet è presente nell'immagine scaricata. Alla domanda su cosa fare se un pin ha più funzioni alternative, la nota nel datasheet fornisce la risposta:
![]()
Se diversi moduli periferici utilizzano lo stesso pin, per evitare conflitti tra le funzioni alternative, è necessario utilizzare contemporaneamente solo un modulo periferico, passando da un altro utilizzando il bit di attivazione del clock delle periferiche (nel registro RCC corrispondente).
Infine, i pin in modalità output hanno anche una velocità di clock. Questa è un'altra caratteristica di risparmio energetico; nel nostro caso, semplicemente impostiamo al massimo e dimentichiamo.
Quindi: stiamo usando SPI, il che significa che due pin (con i dati e con il segnale di clock) devono essere 'funzione alternativa push-pull', mentre un altro (LAT) deve essere 'push-pull standard'. Ma prima di assegnarli, facciamo chiarezza sul SPI.
SPI
Un'altra piccola lezione
SPI o Serial Peripheral Interface (interfaccia periferica seriale) è un'interfaccia semplice e molto efficace per la comunicazione tra microcontrollori e il mondo esterno. Il suo funzionamento è già stato descritto sopra, dove si parla del driver LED cinese (nel manuale di riferimento vedi sezione 25). SPI può operare in modalità master ('maestro') e slave ('schiavo'). SPI ha quattro canali di base, di cui non tutti possono essere utilizzati:
- MOSI, Master Output / Slave Input: questo pin in modalità master trasmette, mentre in modalità slave riceve dati;
- MISO, Master Input / Slave Output: al contrario, in master riceve, in slave trasmette;
- SCK, Serial Clock: imposta la frequenza di trasmissione dati nel master o riceve il segnale di clock nello slave. In sostanza, scandisce i bit;
- SS, Slave Select: tramite questo canale, lo slave capisce che ci si aspetta qualcosa da lui. Su STM32 è chiamato NSS, dove N = negativo, cioè il controller diventa uno slave quando questo canale è a terra. Funziona bene in combinazione con la modalità Open Drain Output, ma questa è un'altra storia.
Come tutto il resto, SPI su STM32 è ricco di funzionalità, il che complica un po' la sua comprensione. Ad esempio, può funzionare non solo come SPI, ma anche come interfaccia I2S, e nella documentazione le descrizioni di queste ultime si intrecciano, quindi è necessario filtrare il superfluo in tempo. La nostra attività è molto semplice: dobbiamo semplicemente trasmettere dati utilizzando solo MOSI e SCK. Andiamo alla sezione 25.3.4 (comunicazione half-duplex), dove troviamo 1 clock and 1 unidirectional data wire (1 segnale clock e 1 flusso di dati unidirezionale):

In questa modalità, l'applicazione utilizza SPI sia in modalità solo trasmissione che in modalità solo ricezione. / La modalità solo trasmissione è simile alla modalità duplex: i dati vengono trasmessi attraverso il pin di trasmissione (MOSI in modalità master o MISO in modalità slave), mentre il pin di ricezione (MISO o MOSI rispettivamente) può essere utilizzato come un normale pin di ingresso/uscita. In questo caso, l'applicazione può semplicemente ignorare il buffer Rx (se lo si legge, non ci saranno dati trasmessi).
Ottimo, il pin MISO è libero, colleghiamoci ad esso il segnale LAT. Scopriamo lo Slave Select, il quale su STM32 può essere controllato tramite software, il che è estremamente conveniente. Leggiamo il paragrafo corrispondente della sezione 25.3.1 Descrizione generale di SPI:

Controllo software di NSS (SSM = 1) / Le informazioni sulla selezione slave sono contenute nel bit SSI del registro SPI_CR1. Il pin esterno NSS rimane libero per altre necessità dell'applicazione.
È il momento di scrivere nei registri. Ho deciso di utilizzare SPI2, cerchiamo nel datasheet il suo indirizzo di base – nella sezione 3.3 Mappa della memoria:
![]()
Iniziamo:
#define _SPI2_(mem_offset) (*(volatile uint32_t *)(0x40003800 + (mem_offset)))Apriamo la sezione 25.3.3 con il titolo esplicativo "Configurazione di SPI in modalità master":

1. Imposta la frequenza di clock dell'interfaccia seriale con i bit BR[2:0] nel registro SPI_CR1.
I registri sono raccolti nella sezione omonima del manuale di riferimento. L'offset dell'indirizzo (Address offset) per CR1 è 0x00, per impostazione predefinita tutti i bit sono resettati (Reset value 0x0000):

I bit BR impostano il divisore della frequenza di clock del controller, determinando così la frequenza alla quale funzionerà SPI. La frequenza STM32 sarà di 72 MHz, mentre il driver LED, secondo il suo datasheet, funziona a una frequenza di fino a 25 MHz, quindi dobbiamo dividere per quattro (BR[2:0] = 001).
#define _SPI_CR1 0x00
#define BR_0 0x0008
#define BR_1 0x0010
#define BR_2 0x0020
_SPI2_ (_SPI_CR1) |= BR_0;// pclk/42. Imposta i bit CPOL e CPHA per definire le relazioni tra la trasmissione dei dati e il clock dell'interfaccia seriale (vedi schema a pag. 240)
Poiché stiamo leggendo il datasheet e non esaminando schemi, è meglio studiare la descrizione testuale dei bit CPOL e CPHA a pag. 704 (Descrizione generale di SPI):

Fase e polarità del segnale di clock
Con i bit CPOL e CPHA del registro SPI_CR1 è possibile selezionare programmando quattro varianti di timing. Il bit CPOL (polarità del segnale di clock) controlla lo stato del segnale di clock quando i dati non vengono trasmessi. Questo bit controlla le modalità master e slave. Se CPOL è resettato, il pin SCK in modalità inattiva è a livello basso. Se il bit CPOL è impostato, il pin SCK in modalità inattiva è a livello alto.
Se è impostato il bit CPHA (fase del segnale di clock), il secondo fronte del segnale SCK funge da strobe per il bit più significativo (discendente se CPOL è resettato, oppure ascendente se CPOL è impostato). I dati vengono registrati al secondo cambiamento del segnale di clock. Se il bit CPHA è resettato, il fronte anteriore del segnale SCK funge da strobe per il bit più significativo (discendente se CPOL è impostato, oppure ascendente se CPOL è resettato). I dati vengono registrati al primo cambiamento del segnale di clock.
Dando un'occhiata a queste informazioni, giungiamo alla conclusione che entrambi i bit devono rimanere a zero, poiché abbiamo bisogno che il segnale SCK rimanga basso quando non utilizzato, e i dati vengano trasmessi al fronte anteriore dell'impulso (vedi Rising Edge nel datasheet DM634).
Tra l'altro, qui ci siamo imbattuti per la prima volta in una peculiarità lessicale nei datasheet ST: in essi la frase «azzerare un bit» viene scritta to reset a bit, e non to clear a bit, come ad esempio per l'Atmega.
3. Impostare il bit DFF per determinare il formato del blocco dati a 8 bit o 16 bit
Ho scelto appositamente il DM634 a 16 bit, per non dover gestire la trasmissione dei dati a 12 bit PWM, come nel caso del DM633. È sensato impostare DFF a uno:
#define DFF 0x0800
_SPI2_ (_SPI_CR1) |= DFF; // 16-bit mode4. Configurare il bit LSBFIRST nel registro SPI_CR1 per determinare il formato del blocco
LSBFIRST, come si evince dal suo nome, configura la trasmissione con il bit meno significativo in avanti. Ma il DM634 desidera ricevere i dati a partire dal bit più significativo. Pertanto, lo lasciamo resettato.
5. In modalità hardware, se è richiesta l'entrata dal pin NSS, fornire un segnale alto al pin NSS durante l'intera sequenza di trasmissione dei byte. In modalità software imposta i bit SSM e SSI nel registro SPI_CR1. Se il pin NSS deve funzionare come uscita, occorre impostare solo il bit SSOE.
Impostiamo SSM e SSI per dimenticare la modalità hardware NSS:
#define SSI 0x0100
#define SSM 0x0200
_SPI2_ (_SPI_CR1) |= SSM | SSI; //enable software control of SS, SS high6. Devono essere impostati i bit MSTR e SPE (restano impostati solo se sul NSS viene fornito un segnale alto)
In effetti, con questi bit stiamo designando il nostro SPI come master e attivandolo:
#define MSTR 0x0004
#define SPE 0x0040
_SPI2_ (_SPI_CR1) |= MSTR; //SPI master
//когда все готово, включаем SPI
_SPI2_ (_SPI_CR1) |= SPE;SPI impostato, scriviamo subito le funzioni per inviare byte al driver. Continuiamo a leggere 25.3.3 «Configurazione SPI in modalità master»:

Ordine di trasmissione dei dati
La trasmissione inizia quando un byte viene scritto nel buffer Tx.
Il byte di dati viene caricato nel registro di scorrimento in modalità parallela (dallo bus interno) durante la trasmissione del primo bit, dopo di che viene trasmesso in sequenziale sequenziale in modalità pin MOSI, il primo o l'ultimo bit viene inviato in base all'impostazione del bit LSBFIRST nel registro CPI_CR1. Il flag TXE viene impostato dopo la trasmissione dei dati dal buffer Tx al registro di scorrimento, e viene anche generata un'interruzione se il bit TXEIE è impostato nel registro CPI_CR1.
Ho evidenziato alcune parole nella traduzione per attirare l'attenzione su una particolare caratteristica dell'implementazione SPI nei controllori STM. Sull'Atmega, il flag TXE (Tx vuoto, Tx è vuoto e pronto a ricevere dati) viene impostato solo dopo che l'intero byte è stato inviato all'esterno. Qui, invece, questo flag viene impostato dopo che il byte è stato inserito nel registro di scorrimento interno. Poiché viene spinto lì tutti i bit contemporaneamente (in parallelo), e poi i dati vengono trasferiti in modo sequenziale, TXE viene impostato prima che il byte venga completamente inviato. Questo è importante perché, nel caso del nostro driver LED, dobbiamo attivare il pin LAT dopo l'invio tutti dei dati, cioè, solo il flag TXE non ci basterà.
Il che significa che ci serve un altro flag. Controlliamo il 25.3.7 - "Flag di stato":

<…>

Flag BUSY
Il flag BSY viene impostato e resettato in modo hardware (la scrittura in esso non ha effetto). Il flag BSY indica lo stato dello strato di comunicazione SPI.
Viene resettato:
quando la trasmissione è completata (eccetto in modalità master, se la trasmissione è continua)
quando SPI è disabilitato
quando si verifica un errore in modalità master (MODF=1)
Se la trasmissione non è continua, il flag BSY viene resettato tra ogni trasmissione di dati
Ok, ci sarà utile. Scopriamo dove si trova il buffer Tx. Per farlo, leggiamo "Registro dati SPI":

Bit 15:0 DR[15:0] Registro dati
Dati ricevuti o dati da trasmettere.
Il registro dei dati è suddiviso in due buffer: uno per la scrittura (buffer di trasmissione) e l'altro per la lettura (buffer di ricezione). La scrittura nel registro dei dati scrive nel buffer Tx, mentre la lettura dal registro dei dati restituirà il valore contenuto nel buffer Rx.
E il registro degli stati, dove troveremo i flag TXE e BSY:

Scriviamo:
#define _SPI_DR 0x0C
#define _SPI_SR 0x08
#define BSY 0x0080
#define TXE 0x0002
void dm_shift16(uint16_t value)
{
_SPI2_(_SPI_DR) = value; //send 2 bytes
while (!(_SPI2_(_SPI_SR) & TXE)); //wait until they're sent
}E poiché dobbiamo trasmettere 16 volte due byte, secondo il numero di uscite del driver LED, procederemo così:
void sendLEDdata()
{
LAT_low();
uint8_t k = 16;
do
{ k--;
dm_shift16(leds[k]);
} while (k);
while (_SPI2_(_SPI_SR) & BSY); // termina la trasmissione
LAT_pulse();
}Ma attualmente non sappiamo come attivare il pin LAT, quindi torniamo all'I/O.
Assegniamo i pin
Nei registri STM32F1, responsabili dello stato dei pin, sono piuttosto insoliti. È chiaro che ce ne sono di più rispetto all'Atmega, ma si differenziano anche da altri chip STM. Sezione 9.1 Descrizione generale del GPIO:

Ciascuna delle porte di ingresso/uscita generiche (GPIO) dispone di due registri di configurazione a 32 bit (GPIOx_CRL e GPIOx_CRH), due registri di dati a 32 bit (GPIOx_IDR e GPIOx_ODR), un registro di impostazione/reset a 32 bit (GPIOx_BSRR), un registro di reset a 16 bit (GPIOx_BRR) e un registro di blocco a 32 bit (GPIOx_LCKR).
Sono insoliti e anche abbastanza scomodi, qui i primi due registri, poiché 16 pin della porta sono distribuiti su di essi in un formato "quattro bit per fratello". Cioè, i pin da zero a sette si trovano in CRL, mentre gli altri sono in CRH. Nel frattempo, gli altri registri possono contenere i bit di tutti i pin della porta, spesso rimanendo a metà "riservati".
Per semplicità, iniziamo dalla fine dell’elenco.
Non abbiamo bisogno del registro di blocco.
I registri di impostazione e reset sono piuttosto divertenti in quanto si duplicano parzialmente: si può scrivere tutto solo in BSRR, dove i 16 bit superiori azzerano il pin e i bit inferiori lo impostano a 1, oppure utilizzare anche BRR, i cui 16 bit inferiori azzerano solo il pin. A me piace la seconda opzione. Questi registri sono importanti in quanto forniscono accesso atomico ai pin:

![]()
Impostazione o reset atomico
Non è necessario disattivare le interruzioni durante la programmazione di GPIOx_ODR a livello di bit: si possono modificare uno o più bit in un'unica operazione atomica di scrittura APB2. Questo si ottiene scrivendo "1" nel registro di impostazione/reset (GPIOx_BSRR o, solo per il reset, in GPIOx_BRR) per il bit che si desidera modificare. Gli altri bit rimarranno invariati.
I registri di dati hanno nomi piuttosto evocativi - IDR = Input Registro di direzione, registro di ingresso; ODR = Output Registro di direzione, registro di uscita. Nel progetto attuale non ci serviranno.
Infine, i registri di controllo. Poiché ci interessano i pin del secondo SPI, ovvero PB13, PB14 e PB15, guardiamo subito a CRH:

E vediamo che sarà necessario scrivere qualcosa nei bit dalla venti alla trentaquattro.
Abbiamo già esaminato in precedenza ciò che vogliamo dai pin, quindi qui mi asterrò dallo screenshot, dirò semplicemente che MODE definisce la direzione (ingresso, se entrambi i bit sono impostati su 0) e la velocità del pin (abbiamo bisogno di 50 MHz, cioè entrambi i pin su «1»), mentre CNF definisce la modalità: normale «pull-up/pull-down» – 00, «alternativa» – 10. Come abbiamo visto sopra, di default, tutti i pin sono impostati con il terzo bit in basso (CNF0), che li mette in modalità floating input.
Poiché ho intenzione di fare qualcos'altro con questo chip, per semplicità ho definito tutti i possibili valori di MODE e CNF per entrambi i registri di controllo inferiori e superiori.
Ecco, più o meno, così.
#define CNF0_0 0x00000004
#define CNF0_1 0x00000008
#define CNF1_0 0x00000040
#define CNF1_1 0x00000080
#define CNF2_0 0x00000400
#define CNF2_1 0x00000800
#define CNF3_0 0x00004000
#define CNF3_1 0x00008000
#define CNF4_0 0x00040000
#define CNF4_1 0x00080000
#define CNF5_0 0x00400000
#define CNF5_1 0x00800000
#define CNF6_0 0x04000000
#define CNF6_1 0x08000000
#define CNF7_0 0x40000000
#define CNF7_1 0x80000000
#define CNF8_0 0x00000004
#define CNF8_1 0x00000008
#define CNF9_0 0x00000040
#define CNF9_1 0x00000080
#define CNF10_0 0x00000400
#define CNF10_1 0x00000800
#define CNF11_0 0x00004000
#define CNF11_1 0x00008000
#define CNF12_0 0x00040000
#define CNF12_1 0x00080000
#define CNF13_0 0x00400000
#define CNF13_1 0x00800000
#define CNF14_0 0x04000000
#define CNF14_1 0x08000000
#define CNF15_0 0x40000000
#define CNF15_1 0x80000000
#define MODE0_0 0x00000001
#define MODE0_1 0x00000002
#define MODE1_0 0x00000010
#define MODE1_1 0x00000020
#define MODE2_0 0x00000100
#define MODE2_1 0x00000200
#define MODE3_0 0x00001000
#define MODE3_1 0x00002000
#define MODE4_0 0x00010000
#define MODE4_1 0x00020000
#define MODE5_0 0x00100000
#define MODE5_1 0x00200000
#define MODE6_0 0x01000000
#define MODE6_1 0x02000000
#define MODE7_0 0x10000000
#define MODE7_1 0x20000000
#define MODE8_0 0x00000001
#define MODE8_1 0x00000002
#define MODE9_0 0x00000010
#define MODE9_1 0x00000020
#define MODE10_0 0x00000100
#define MODE10_1 0x00000200
#define MODE11_0 0x00001000
#define MODE11_1 0x00002000
#define MODE12_0 0x00010000
#define MODE12_1 0x00020000
#define MODE13_0 0x00100000
#define MODE13_1 0x00200000
#define MODE14_0 0x01000000
#define MODE14_1 0x02000000
#define MODE15_0 0x10000000
#define MODE15_1 0x20000000I nostri pin si trovano sulla porta B (indirizzo base – 0x40010C00), codice:
#define _PORTB_(mem_offset) (*(volatile uint32_t *)(0x40010C00 + (mem_offset)))
#define _BRR 0x14
#define _BSRR 0x10
#define _CRL 0x00
#define _CRH 0x04
//используем стандартный SPI2: MOSI на B15, CLK на B13
//LAT пусть будет на неиспользуемом MISO – B14
//очищаем дефолтный бит, он нам точно не нужен
_PORTB_ (_CRH) &= ~(CNF15_0 | CNF14_0 | CNF13_0 | CNF12_0);
//альтернативные функции для MOSI и SCK
_PORTB_ (_CRH) |= CNF15_1 | CNF13_1;
//50 МГц, MODE = 11
_PORTB_ (_CRH) |= MODE15_1 | MODE15_0 | MODE14_1 | MODE14_0 | MODE13_1 | MODE13_0;E, di conseguenza, possiamo scrivere le definizioni per LAT, che sarà controllato dai registri BRR e BSRR:
/*** LAT pulse – high, then low */
#define LAT_pulse() _PORTB_(_BSRR) = (1<<14); _PORTB_(_BRR) = (1<<14)
#define LAT_low() _PORTB_(_BRR) = (1<<14)(LAT_low è rimasto così, un'abitudine, lasciamolo così)
Ora è tutto fantastico, solo che non funziona. Perché questo è STM32, qui si risparmia energia, quindi è necessario attivare l'orologio della periferica necessaria.
Attiviamo il clock
Il clock è gestito dagli orologi, conosciuti anche come Clock. E abbiamo già notato l'abbreviazione RCC. Cerchiamo nella documentazione: si tratta di Reset and Clock Control (Gestione del reset e del clock).
Come accennato sopra, per fortuna, la parte più complessa del clock è stata gestita da persone della STM, a cui va un grande grazie (darò di nuovo il link al , per capire quanto sia complicato). Abbiamo solo bisogno dei registri che controllano l'attivazione del clock delle periferiche (Peripheral Clock Enable Registers). Per iniziare, troviamo l'indirizzo base RCC, che si trova all'inizio della «Mappa della memoria»:
![]()
#define _RCC_(mem_offset) (*(volatile uint32_t *)(0x40021000 + (mem_offset)))E poi o clicchiamo sul link, cercando di trovare qualcosa nella tabella, oppure, molto meglio, sfogliamo le descrizioni dei registri attivatori nelle sezioni riguardanti enable registers. Dove troveremo RCC_APB1ENR e RCC_APB2ENR:


E in essi, rispettivamente, i bit per attivare il clock SPI2, IOPB (I/O Port B) e funzioni alternative (AFIO).
#define _APB2ENR 0x18
#define _APB1ENR 0x1C
#define IOPBEN 0x0008
#define SPI2EN 0x4000
#define AFIOEN 0x0001
//включаем тактирование порта B и альт. функций
_RCC_(_APB2ENR) |= IOPBEN | AFIOEN;
//включаем тактирование SPI2
_RCC_(_APB1ENR) |= SPI2EN;Il codice finale può essere trovato .
Se c'è la possibilità e la voglia di testare, colleghiamo DM634 in questo modo: DAI a PB15, DCK a PB13, LAT a PB14. Alimentiamo il driver con 5 volt, non dimentichiamo di unire le masse.

STM8 PWM
PWM su STM8
Quando ho iniziato a pianificare questo articolo, ho deciso di provare a padroneggiare qualche funzionalità di un chip a me sconosciuto solo con il datasheet, per non trasformarmi in un ciabattino senza scarpe. L'STM8 si adattava perfettamente a questo ruolo: da un lato avevo un paio di schede cinesi con STM8S103, dall'altro non è molto popolare, quindi la tentazione di barare e trovare una soluzione su internet si scontra con l'assenza di quest'ultime.
Al chip è anche associato e , il primo descrive il pinout e gli indirizzi dei registri, il secondo – tutto il resto. L'STM8 si programma in C su un IDE piuttosto semplice .
Cronometraggio e input-output
Di default l'STM8 funziona a una frequenza di 2 MHz, questo deve essere corretto subito.

Il segnale di clock HSI (interno veloce)
Il segnale di clock HSI proviene da un generatore RC interno da 16 MHz con un divisore programmabile (da 1 a 8). È impostato nel registro del divisore di clock (CLK_CKDIVR).
Nota: all'avvio, la fonte principale del segnale di clock viene scelta come generatore RC HSI con un divisore di 8.
Troviamo l'indirizzo del registro nel datasheet, la descrizione nel refman e vediamo che il registro deve essere pulito:
#define CLK_CKDIVR *(volatile uint8_t *)0x0050C6
CLK_CKDIVR &= ~(0x18);Poiché intendiamo attivare il PWM e collegare i LED, controlliamo il pinout:

Il chip è piccolo, molte funzioni condividono gli stessi pin. Ciò che è tra parentesi quadre è una 'funzionalità alternativa', che viene commutata tramite 'option bytes' (option bytes) – in un certo senso simile ai fusibili dell'Atmega. È possibile modificare i loro valori tramite programmazione, ma non è necessario, poiché il nuovo funzionalità si attiva solo dopo un riavvio. È più semplice utilizzare ST Visual Programmer (disponibile con Visual Develop), che è in grado di modificare questi byte. Nel pinout si può vedere che i pin CH1 e CH2 del primo timer sono nascosti tra parentesi quadre; bisogna impostare i bit AFR1 e AFR0 in STVP, e il secondo trasferirà anche il pin CH1 del secondo timer da PD4 a PC5.
In questo modo, il controllo dei LED avverrà tramite 6 pin: PC6, PC7 e PC3 per il primo timer, PC5, PD3 e PA3 per il secondo.
La configurazione dei pin di input-output sull'STM8 è più semplice e logica rispetto all'STM32:
- il registro della direzione dei dati DDR (Data Direction Register): 1 = output;
- il primo registro di controllo CR1, in output, determina il modo 'push-pull' (1) o open drain (0); poiché collego i LED ai pin del chip con i catodi, lascio qui zeri;
- il secondo registro di controllo CR2, in output, imposta la velocità di clock: 1 = 10 MHz
#define PA_DDR *(volatile uint8_t *)0x005002
#define PA_CR2 *(volatile uint8_t *)0x005004
#define PD_DDR *(volatile uint8_t *)0x005011
#define PD_CR2 *(volatile uint8_t *)0x005013
#define PC_DDR *(volatile uint8_t *)0x00500C
#define PC_CR2 *(volatile uint8_t *)0x00500E
PA_DDR = (1<<3); //output
PA_CR2 |= (1<<3); //fast
PD_DDR = (1<<3); //output
PD_CR2 |= (1<<3); //fast
PC_DDR = ((1<<3) | (1<<5) | (1<<6) | (1<<7)); //output
PC_CR2 |= ((1<<3) | (1<<5) | (1<<6) | (1<<7)); //fastImpostazione PWM
Iniziamo definendo i termini:
- Frequenza PWM – frequenza con cui il timer ticchetta;
- Auto-reload, AR – valore di caricamento automatico, fino al quale il timer conterà (periodo dell'impulso);
- Evento di aggiornamento, UEV – evento che si verifica quando il timer raggiunge AR;
- Ciclo di lavoro PWM – coefficiente di riempimento PWM, spesso chiamato 'duty cycle';
- Valore di cattura/comparazione – valore per cattura/comparazione, fino al quale il timer farà qualcosa (nel caso del PWM – inverterà il segnale d'uscita);
- Valore di pre-caricamento – valore pre-caricato. Il valore di comparazione non può essere modificato mentre il timer ticchetta, altrimenti il ciclo PWM si interrompe. Pertanto, i nuovi valori trasmessi vengono memorizzati in un buffer e prelevati da lì quando il timer raggiunge la fine del conteggio e si resetta;
- Allineato ai bordi e Modalità allineata al centro – allineamento ai bordi e al centro, lo stesso delle modalità Fast PWM e Phase-correct PWM.
- OCiREF, Segnale di confronto d'uscita – segnale di riferimento d'uscita, in effetti, ciò che in modalità PWM appare sul pin corrispondente.
Come già chiaro dalla pinout, le capacità PWM sono presenti in due timer – il primo e il secondo. Entrambi sono a 16 bit, il primo ha molte funzionalità aggiuntive (in particolare può contare sia in su che in giù). Abbiamo bisogno che entrambi funzionino allo stesso modo, quindi ho deciso di iniziare con il secondo, consapevolmente più limitato, per non rischiare di usare accidentalmente qualcosa che non ha. Un certo problema è che la descrizione delle funzionalità PWM di tutti i timer nel manuale di riferimento si trova nel capitolo sul primo timer (17.5.7 Modalità PWM), quindi bisogna saltare continuamente avanti e indietro nel documento.
Il PWM su STM8 ha un'importante vantaggio rispetto al PWM di Atmega:

PWM con allineamento ai bordi
Configurazione del conteggio dal basso verso l'alto
Il conteggio dal basso verso l'alto è attivo se il bit DIR nel registro TIM_CR1 è azzerato
Esempio
L'esempio utilizza la prima modalità PWM. Il segnale di riferimento PWM OCiREF rimane alto finché TIM1_CNT < TIM1_CCRi. Altrimenti, assume un livello basso. Se il valore di comparazione nel registro TIM1_CCRi è maggiore del valore di caricamento automatico (registro TIM1_ARR), il segnale OCiREF rimane a 1. Se il valore di comparazione è 0, OCiREF rimane a zero.…
Il timer STM8 durante l' evento di aggiornamento controlla prima il valore di comparazione, e solo allora emette il segnale di riferimento. Nel caso di Atmega, il timer prima esegue l'operazione e poi confronta, il che porta a risultati in cui il valore di comparazione == 0 alla fine otteniamo un ago, con cui bisogna combattere in qualche modo (ad esempio, invertendo logicamente tramite programmazione).
Quindi, cosa vogliamo fare: PWM a 8 bit (AR == 255), contando dal basso verso l'alto, allineamento per il confine. Poiché le lampadine sono collegate ai chip con i catodi, il PWM deve restituire 0 (LED acceso) fino a il valore di comparazione e 1 dopo.
Abbiamo già letto di alcuni PWM mode, quindi troviamo il registro necessario del secondo timer cercando nel manuale di riferimento con questa frase (18.6.8 – TIMx_CCMR1):

110: Primo modo PWM – contando dal basso verso l'alto, il primo canale è attivo finché TIMx_CNT < TIMx_CCR1. Altrimenti, il primo canale è inattivo. [in seguito nel documento c'è un errato copia e incolla dal timer 1]
111: Secondo modo PWM – contando dal basso verso l'alto, il primo canale è inattivo finché TIMx_CNT < TIMx_CCR1. Altrimenti, il primo canale è attivo.
Poiché i LED sono collegati al microcontrollore con i catodi, ci si adatta al secondo modo (anche il primo, ma per ora non lo sappiamo).

Bit 3 OC1PE: Attivare il caricamento dell'uscita 1
0: Il registro di caricamento per TIMx_CCR1 è disattivato. Puoi scrivere in TIMx_CCR1 in qualsiasi momento. Il nuovo valore funziona immediatamente.
1: Il registro di caricamento per TIMx_CCR1 è attivato. Le operazioni di lettura/scrittura si riferiscono al registro di caricamento. Il valore pre-caricato di TIMx_CCR1 viene caricato nel registro secondario durante ogni evento di aggiornamento.
*Nota: per il corretto funzionamento della modalità PWM, i registri di caricamento devono essere attivi. Questo non è necessario in modalità segnale singolo (nel registro TIMx_CR1 il bit OPM è impostato).
Ok, attiviamo tutto ciò che serve per i tre canali del secondo timer:
#define TIM2_CCMR1 *(volatile uint8_t *)0x005307
#define TIM2_CCMR2 *(volatile uint8_t *)0x005308
#define TIM2_CCMR3 *(volatile uint8_t *)0x005309
#define PWM_MODE2 0x70 //PWM mode 2, 0b01110000
#define OCxPE 0x08 //preload enable
TIM2_CCMR1 = (PWM_MODE2 | OCxPE);
TIM2_CCMR2 = (PWM_MODE2 | OCxPE);
TIM2_CCMR3 = (PWM_MODE2 | OCxPE);AR consiste in due registri da otto bit, qui è tutto semplice:
#define TIM2_ARRH *(volatile uint8_t *)0x00530F
#define TIM2_ARRL *(volatile uint8_t *)0x005310
TIM2_ARRH = 0;
TIM2_ARRL = 255;Il secondo timer può contare solo dal basso verso l'alto, allineamento per il confine, non è necessario cambiare nulla. Imposteremo il divisore di frequenza, ad esempio a 256. Per il secondo timer il divisore è impostato nel registro TIM2_PSCR e rappresenta una potenza di due:
#define TIM2_PSCR *(volatile uint8_t *)0x00530E
TIM2_PSCR = 8;Rimane da attivare le uscite e il secondo timer stesso. La prima operazione viene eseguita tramite i registri Capture/Compare Abilita: ce ne sono due, tre canali sono sparsi in modo asimmetrico. Qui possiamo anche scoprire che è possibile cambiare la polarità del segnale, quindi in linea di principio avremmo potuto usare anche il PWM Mode 1. Scriviamo:
#define TIM2_CCER1 *(volatile uint8_t *)0x00530A
#define TIM2_CCER2 *(volatile uint8_t *)0x00530B
#define CC1E (1<<0) // CCER1
#define CC2E (1<<4) // CCER1
#define CC3E (1<<0) // CCER2
TIM2_CCER1 = (CC1E | CC2E);
TIM2_CCER2 = CC3E;E infine, avviamo il timer nel registro TIMx_CR1:

#define TIM2_CR1 *(volatile uint8_t *)0x005300
TIM2_CR1 |= 1;Scriviamo una semplice analogia di AnalogWrite(), che trasmetterà al timer i valori effettivamente per il confronto. I registri si chiamano prevedibilmente Capture/Compare registers, due sono assegnati a ciascun canale: i 8 bit inferiori in TIM2_CCRxL e i bit superiori in TIM2_CCRxH. Poiché abbiamo impostato un PWM a 8 bit, è sufficiente scrivere solo i bit inferiori:
#define TIM2_CCR1L *(volatile uint8_t *)0x005312
#define TIM2_CCR2L *(volatile uint8_t *)0x005314
#define TIM2_CCR3L *(volatile uint8_t *)0x005316
void setRGBled(uint8_t r, uint8_t g, uint8_t b)
{
TIM2_CCR1L = r;
TIM2_CCR2L = g;
TIM2_CCR3L = b;
}Il lettore attento noterà che abbiamo ottenuto un PWM leggermente difettoso, incapace di fornire un riempimento del 100% (a un valore massimo di 255, il segnale si inverte per un ciclo del timer). Per i LED questo non è un problema, e il lettore attento può già capire come risolverlo.
Il PWM sul secondo timer funziona, passiamo al primo.
Il primo timer ha esattamente gli stessi bit negli stessi registri (solo quelli che erano stati "riservati" nel secondo timer, nel primo vengono utilizzati attivamente per varie funzionalità avanzate). Pertanto, è sufficiente trovare gli indirizzi di questi stessi registri nel datasheet e copiare il codice. E anche cambiare il valore del divisore di frequenza, poiché il primo timer desidera un valore esatto a 16 bit in due registri e non una potenza di due. Prescaler High e Basso. Facciamo tutto e… il primo timer non funziona. Qual è il problema?
Il problema può essere risolto solo esaminando l'intera sezione sui registri di controllo del timer 1, dove cerchiamo quello che non è presente nel secondo timer. Sarà trovato 17.7.30 Registro di interruzione (TIM1_BKR), dove c'è un bit del genere:
![]()
Abilitare l'uscita principale
#define TIM1_BKR *(volatile uint8_t *)0x00526D
TIM1_BKR = (1<<7);Ora, sicuramente è tutto, codice .

STM8 Multiplex
Multiplexing su STM8
Il terzo mini-progetto consiste nel collegare otto LED RGB in modalità PWM al secondo timer e farli mostrare colori diversi. Alla base c'è il concetto del multiplexing LED, secondo cui se accendiamo e spegniamo i LED molto rapidamente, ci sembrerà che siano accesi continuamente (persistence of vision, inerzia della percezione visiva). Una volta ho fatto .
L'algoritmo di funzionamento è il seguente:
- collegare l'anodo del primo LED RGB;
- accenderlo, inviando i segnali necessari ai catodi;
- attendere la fine del ciclo PWM;
- collegare l'anodo del secondo LED RGB;
- accenderlo…
E così via. Naturalmente, per un funzionamento elegante è necessario che il collegamento dell'anodo e l'accensione del LED avvengano simultaneamente. O quasi. In ogni caso, dobbiamo scrivere un codice che fornisca valori su tre canali del secondo timer, cambiandoli al raggiungimento di UEV e modificando simultaneamente il LED RGB attualmente attivo.
Poiché il passaggio dei LED avviene automaticamente, è necessario creare una «memoria video» da cui il gestore delle interruzioni riceverà i dati. Si tratta di un semplice array:
uint8_t colors[8][3];Per cambiare il colore di un LED specifico, basta scrivere i valori necessari in questo array. La variabile responsabile del numero del LED attivo sarà
uint8_t cnt;Demux
Per un corretto multiplexing, abbiamo bisogno, strano ma vero, del demultiplexer CD74HC238. Il demultiplexer è un chip che implementa hardware l'operatore <<. Attraverso tre pin di ingresso (bit 0, 1 e 2) gli forniamo un numero a tre bit X, e lui attiva in risposta l'uscita numero (1<<X). Gli altri ingressi del chip vengono utilizzati per scalare l'intera costruzione. Questo chip ci serve non solo per ridurre il numero di pin occupati dal microcontrollore, ma anche per sicurezza, per evitare di accendere accidentalmente più LED di quanto sia consentito e di bruciare il microcontrollore. Il chip costa poco, quindi è utile averlo sempre nel kit di pronto soccorso di casa.
Il CD74HC238 sarà responsabile dell'alimentazione della tensione all'anodo del LED desiderato. In un completo multiplexing alimenterebbe la tensione attraverso un P-MOSFET, ma in questa dimostrazione è possibile farlo direttamente, dato che consuma 20 mA, secondo absolute maximum ratings il datasheet. Dal abbiamo bisogno della pinout e di questo promemoria:

H = livello alto di tensione, L = livello basso di tensione, X = indifferente
Colleghiamo E2 e E1 a terra, E3, A0, A1 e A3 ai pin PD5, PC3, PC4 e PC5 STM8. Poiché la tabella sopra contiene sia livelli bassi che alti, configuriamo questi pin come uscite push-pull.
PWM
La PWM sul secondo timer viene configurata esattamente come nella storia precedente, con due differenze:
In primo luogo, dobbiamo abilitare l'interruzione per Update Event (UEV), che chiamerà la funzione che attiva il LED attivo. Questo si fa modificando il bit Update Interrupt Enable nel registro con un nome appropriato,

Registro di abilitazione delle interruzioni
#define TIM2_IER *(volatile uint8_t *)0x005303
//enable interrupt
TIM2_IER = 1;La seconda differenza riguarda un fenomeno del multiplexing noto come ghosting – luminosità parassitaria dei LED. Nel nostro caso, può apparire perché il timer, che ha causato l'interruzione all'UEV, continua a ticchettare e il gestore delle interruzioni non riesce a commutare il LED prima che il timer inizi a scrivere qualcosa sulle uscite. Per affrontare questo problema, sarà necessario invertire la logica (0 = massima luminosità, 255 = niente che brilla) e non permettere valori estremi del duty cycle. Cioè, dobbiamo assicurarci che dopo l'UEV i LED si spengano completamente per un ciclo PWM.
Invertiamo la polarità:
//set polarity
TIM2_CCER1 |= (CC1P | CC2P);
TIM2_CCER2 |= CC3P;Evitiamo di impostare r, g e b a 255 e ricordiamoci di invertirli durante l'uso.
Interruzioni
Il concetto di interruzione è che, in determinate circostanze, il chip smette di eseguire il programma principale e chiama qualche funzione esterna. Le interruzioni possono verificarsi a causa di eventi esterni o interni, compresi quelli causati da un timer.
Quando abbiamo creato per la prima volta il progetto in ST Visual Develop, oltre a main.c abbiamo ricevuto una finestra con un misterioso file stm8_interrupt_vector.c, incluso automaticamente nel progetto. In questo file, a ogni interruzione è associata una funzione NonHandledInterrupt. Dobbiamo legare la nostra funzione all'interruzione desiderata.
Nel datasheet c'è una tabella dei vettori di interruzione, dove troviamo quelli necessari:

13 aggiornamento/overflow TIM2
14 cattura/comparazione TIM2
Dobbiamo cambiare il LED all'UEV, quindi abbiamo bisogno dell'interruzione n. 13.
Di conseguenza, innanzitutto, nel file stm8_interrupt_vector.c cambiamo il nome della funzione che gestisce l'interruzione n. 13 (IRQ13) di default con il nostro:
{0x82, TIM2_Overflow}, /* irq13 */In secondo luogo, dovremo creare un file main.h con il seguente contenuto:
#ifndef __MAIN_H
#define __MAIN_H
@far @interrupt void TIM2_Overflow (void);
#endifE infine, dobbiamo implementare questa funzione nel nostro main.c:
@far @interrupt void TIM2_Overflow (void)
{
PD_ODR &= ~(1<<5); // spegniamo il demultiplexor
PC_ODR = (cnt<<3); // scriviamo un nuovo valore nel demultiplexor
PD_ODR |= (1<<5); // accendiamo il demultiplexor
TIM2_SR1 = 0; // reimpostiamo il flag Update Interrupt Pending
cnt++;
cnt &= 7; // muoviamo il contatore LED
TIM2_CCR1L = ~colors[cnt][0]; // passiamo al buffer i valori invertiti
TIM2_CCR2L = ~colors[cnt][1]; // per il ciclo PWM successivo
TIM2_CCR3L = ~colors[cnt][2]; //
return;
}Rimane da attivare le interruzioni. Questo si fa con il comando assembler rim – dovrai cercarlo in :
//enable interrupts
_asm("rim");Un altro comando assembler è sim – disattiva le interruzioni. Devono essere disattivate durante la scrittura di nuovi valori nella 'memoria video', in modo che un'interruzione causata in un momento inopportuno non danneggi l'array.
Tutto il codice è .

Se anche a qualcuno questa articolo sarà utile, significa che non l'ho scritto invano. Sarò felice di ricevere commenti e osservazioni, cercherò di rispondere a tutti.
Fonte: habr.com
