Obsah
1. Využití TinyGo při programování Raspberry Pi Pico (2. část)
2. Využití dalších kanálů analogově-digitálního převodníku
3. Přečtení a zobrazení teploty změřené senzorem na čipu
4. Zabránění častému „přeskakování“ teploty
5. Korektní zobrazení desetinné tečky
7. Zapojení maticového displeje
8. Softwarové řízení posuvných registrů přes GPIO
9. Základní tvar SW smyčky pro obnovu informací na maticovém displeji
10. Demonstrační příklad: vyplnění celé plochy rozsvícenými pixely
11. Vykreslení šachovnicového vzorku
12. Otestování, zda je časování sloupců při ovládání displeje korektní
14. Vykreslení celé bitmapy 32×8 pixelů
15. Scrolling obrazu s využitím potenciometru
16. Raspberry Pi Pico připojené ve funkci terminálu
19. Repositář s demonstračními příklady
1. Využití TinyGo při programování Raspberry Pi Pico (2. část)
Na předchozí článek o překladači TinyGo použitém pro tvorbu aplikací pro Raspberry Pi Pico dnes navážeme. Ukážeme si, jakým způsobem je možné přečíst teplotu změřenou interním senzorem připojeným na pátý kanál analogově-digitálního převodníku. Dále se budeme zabývat tím, jak lze čistě softwarově ovládat maticový displej s rozlišením 32×8 pixelů, přičemž každý pixel je tvořen LED. Dále se zmíníme o připojení Raspberry Pi Pico k počítači v režimu terminálu, což mj. umožňuje spouštět programy, které používají standardní vstupní a výstupní proudy (input stream a output stream). A v závěrečné části článku si popíšeme způsob práce se sériovými sběrnicemi a rozhraními. Raspberry Pi Pico totiž podporuje jak rozhraní SPI, tak i sběrnici I2C. Sériový způsob přenosu dat je podporován mnoha periferními zařízeními, například řadiči LCD atd.
Obrázek 1: Přední strana desky, kterou použijeme v dnešním článku. Vlevo dole je běžné Raspberry Pi Pico W.
2. Využití dalších kanálů analogově-digitálního převodníku
Již v úvodním článku jsme si řekli, že mikrořadiče RP2040 i RP2350 jsou mj. vybaveny i analogově-digitálním převodníkem. Typicky se jedná o čtyři (vstupní) kanály s rozlišením dvanácti bitů, ovšem RP2350 v pouzdře QFN-80EP nabízí celých osm kanálů (a současně i osmnáct GPIO). I když má analogově-digitální převodník rozlišení dvanácti bitů, prakticky všechna programová rozhraní vrací šestnáctibitové hodnoty, což znamená, že spodní čtyři bity přečtených výsledků je možné ignorovat. Navíc je možné u každého ze vstupů do analogově-digitálního převodníku zvolit, zda budou použity nebo zda se má příslušný pin použít pro jiné účely (tedy například jako GPIO, SCL apod.).
To se ovšem týká pouze prvních tří kanálů ADC0, ADC1 a ADC2, protože čtvrtý kanál se používá pro čtení úrovně referenčního napětí (není vyveden na piny RPi). Pátý kanál čte hodnoty získané teplotním senzorem (a není tedy vyveden na piny RPi). Popis pinů naleznete na adrese https://www.raspberrypi.com/documentation/microcontrollers/pico-series.html#pico-2-family.
A právě pátý kanál ADC nás bude zajímat, protože nám umožní zjistit aktuální teplotu čipu. V případě, že používáme TinyGo a knihovny, které jsou jeho součástí, není nutné provádět ruční převod přečtené hodnoty na teplotu, protože je k dispozici pro tento účel určená funkce, která přečte hodnotu z ADC a provede konverzi automaticky:
// ReadTemperature does a one-shot sample of the internal temperature sensor and returns a milli-celsius reading.
func ReadTemperature() (millicelsius int32) {
if rp.ADC.CS.Get()&rp.ADC_CS_EN == 0 {
InitADC()
}
thermChan, _ := ADC{Pin: thermADC}.GetADCChannel()
// Enable temperature sensor bias source
rp.ADC.CS.SetBits(rp.ADC_CS_TS_EN)
// T = 27 - (ADC_voltage - 0.706)/0.001721
// 1/0.001721 ≈ 581
return int32(((int64(27000) << 16) - ((int64(thermChan.getVoltage()) - (int64(706) << 16)) * 581)) >> 16)
}
Jak je z dokumentačního řetězce patrné, je výsledek reprezentován v tisícinách stupně Celsia.
3. Přečtení a zobrazení teploty změřené senzorem na čipu
Přečtení teploty změřené senzorem a převedené do číslicové podoby analogově-digitálním převodníkem je velmi jednoduché, takže snadno můžeme realizovat programovou smyčku, která teplotu přečte a zobrazí ji na displeji. K zobrazení lze použít funkci pojmenovanou displayNumber, kterou jsme si popsali v předchozím článku. Ovšem vzhledem k tomu, že se (teoreticky) vrací teplota v rozsahu od 0°C až 65,536°C, ale displej má pouze čtyři cifry, nejprve vrácenou teplotu podělíme deseti. Ostatně to je více než dostačující; není zapotřebí zobrazovat setiny a tisíciny stupně Celsia:
for {
temperature := machine.ReadTemperature()
temperature /= 10
displayNumber(int(temperature))
}
Výsledek bude vypadat následovně:
Ukažme si ještě úplný zdrojový kód tohoto demonstračního příkladu:
package main
import (
"machine"
"time"
)
const SleepAmount = time.Millisecond * 1
var controls [4]machine.Pin
var pins [8]machine.Pin
var digits [][]bool = [][]bool{
{true, true, true, true, true, true, false, false},
{false, true, true, false, false, false, false, false},
{true, true, false, true, true, false, true, false},
{true, true, true, true, false, false, true, false},
{false, true, true, false, false, true, true, false},
{true, false, true, true, false, true, true, false},
{true, false, true, true, true, true, true, false},
{true, true, true, false, false, false, false, false},
{true, true, true, true, true, true, true, false},
{true, true, true, true, false, true, true, false},
}
func init() {
controls[0] = machine.GP5
controls[1] = machine.GP6
controls[2] = machine.GP7
controls[3] = machine.GP8
for _, control := range controls {
control.Configure(machine.PinConfig{Mode: machine.PinOutput})
}
pins[0] = machine.GP11
pins[1] = machine.GP9
pins[2] = machine.GP13
pins[3] = machine.GP15
pins[4] = machine.GP16
pins[5] = machine.GP10
pins[6] = machine.GP12
pins[7] = machine.GP14
for _, pin := range pins {
pin.Configure(machine.PinConfig{Mode: machine.PinOutput})
}
}
func displaySegments(bits []bool) {
for i := range bits {
bit := bits[i]
pin := pins[i]
if bit {
pin.High()
} else {
pin.Low()
}
}
}
func displayNumber(number int) {
x := number
for i := range 4 {
digit := x % 10
x /= 10
bits := digits[digit]
control := controls[3-i]
control.High()
displaySegments(bits)
time.Sleep(SleepAmount)
control.Low()
}
}
func main() {
for {
temperature := machine.ReadTemperature()
temperature /= 10
displayNumber(int(temperature))
}
}
4. Zabránění častému „přeskakování“ teploty
V předchozím demonstračním příkladu se teplota z analogově-digitálního převodníku četla velmi často a ihned poté byla zobrazena na displeji. Ovšem kvůli různým vlivům se minimálně poslední dvě číslice (desetiny a setiny stupně Celsia) velmi často měnily, takže vlastně nebylo možné teplotu z displeje ani přečíst. Řešení tohoto problému je snadné – měření teploty se provede s relativně malou frekvencí (postačuje i jednou za několik sekund) a poté se stejné číslice opakovaně zobrazí na displeji (obnovu jeho obsahu provádíme programově). Nejjednodušší forma řešení může vypadat:
for {
temperature := machine.ReadTemperature()
temperature /= 10
for range 1000 {
displayNumber(int(temperature))
}
}
Nyní je výsledkem zobrazení teploty, které se ovšem mění s mnohem menší frekvencí a číslice lze tedy snadno přečíst:
Následuje výpis upraveného zdrojového kódu demonstračního příkladu:
package main
import (
"machine"
"time"
)
const SleepAmount = time.Millisecond * 1
var controls [4]machine.Pin
var pins [8]machine.Pin
var digits [][]bool = [][]bool{
{true, true, true, true, true, true, false, false},
{false, true, true, false, false, false, false, false},
{true, true, false, true, true, false, true, false},
{true, true, true, true, false, false, true, false},
{false, true, true, false, false, true, true, false},
{true, false, true, true, false, true, true, false},
{true, false, true, true, true, true, true, false},
{true, true, true, false, false, false, false, false},
{true, true, true, true, true, true, true, false},
{true, true, true, true, false, true, true, false},
}
func init() {
controls[0] = machine.GP5
controls[1] = machine.GP6
controls[2] = machine.GP7
controls[3] = machine.GP8
for _, control := range controls {
control.Configure(machine.PinConfig{Mode: machine.PinOutput})
}
pins[0] = machine.GP11
pins[1] = machine.GP9
pins[2] = machine.GP13
pins[3] = machine.GP15
pins[4] = machine.GP16
pins[5] = machine.GP10
pins[6] = machine.GP12
pins[7] = machine.GP14
for _, pin := range pins {
pin.Configure(machine.PinConfig{Mode: machine.PinOutput})
}
}
func displaySegments(bits []bool) {
for i := range bits {
bit := bits[i]
pin := pins[i]
if bit {
pin.High()
} else {
pin.Low()
}
}
}
func displayNumber(number int) {
x := number
for i := range 4 {
digit := x % 10
x /= 10
bits := digits[digit]
control := controls[3-i]
control.High()
displaySegments(bits)
time.Sleep(SleepAmount)
control.Low()
}
}
func main() {
for {
temperature := machine.ReadTemperature()
temperature /= 10
for range 1000 {
displayNumber(int(temperature))
}
}
}
5. Korektní zobrazení desetinné tečky
Každá cifra na sedmisegmentovém displeji ve skutečnosti ještě obsahuje jednu LED určenou pro zobrazení desetinné tečky. Toho pochopitelně můžeme využít, protože například hodnota 2050 zobrazená na displeji je poněkud zavádějící. Ovšem když zobrazíme 20.50, bude již zřejmé, že se jedná o skutečnou teplotu. Existuje mnoho možností úpravy existující funkce pro zobrazení celočíselné hodnoty na displeji; nejjednodušší je však předat do této funkce index desetinné tečky, tj. vlastně index číslice, za kterou bude tečka zobrazena. Pokud tento index bude ležet mimo rozsah 0..3, tečka zobrazena nebude. Jedno z možných řešení může vypadat následovně. Nevýhodou tohoto řešení je nutnost kopie všech osmi příznaků segmentů (nebo naopak modifikace původního globálního pole, což je možná ještě horší):
func displayNumber(number int, dp int) {
x := number
for i := range 4 {
digit := x % 10
x /= 10
bits := make([]bool, 8)
copy(bits, digits[digit])
if i == dp {
bits[7] = true
}
control := controls[3-i]
control.High()
displaySegments(bits)
time.Sleep(SleepAmount)
control.Low()
}
}
Volání funkce displayNumber bude v našem případě vypadat následovně. Zobrazujeme tisíciny stupně Celsia vydělené deseti, takže vlastně setiny. Tudíž musí být tečka zobrazena před posledními dvěma číslicemi:
for {
temperature := machine.ReadTemperature()
temperature /= 10
for range 1000 {
displayNumber(int(temperature), 2)
}
}
Výsledek získaný po spuštění tohoto příkladu na reálné vývojové desce:
Opět si pro úplnost ukážeme zdrojový kód tohoto demonstračního příkladu:
package main
import (
"machine"
"time"
)
const SleepAmount = time.Millisecond * 1
var controls [4]machine.Pin
var pins [8]machine.Pin
var digits [][]bool = [][]bool{
{true, true, true, true, true, true, false, false},
{false, true, true, false, false, false, false, false},
{true, true, false, true, true, false, true, false},
{true, true, true, true, false, false, true, false},
{false, true, true, false, false, true, true, false},
{true, false, true, true, false, true, true, false},
{true, false, true, true, true, true, true, false},
{true, true, true, false, false, false, false, false},
{true, true, true, true, true, true, true, false},
{true, true, true, true, false, true, true, false},
}
func init() {
controls[0] = machine.GP5
controls[1] = machine.GP6
controls[2] = machine.GP7
controls[3] = machine.GP8
for _, control := range controls {
control.Configure(machine.PinConfig{Mode: machine.PinOutput})
}
pins[0] = machine.GP11
pins[1] = machine.GP9
pins[2] = machine.GP13
pins[3] = machine.GP15
pins[4] = machine.GP16
pins[5] = machine.GP10
pins[6] = machine.GP12
pins[7] = machine.GP14
for _, pin := range pins {
pin.Configure(machine.PinConfig{Mode: machine.PinOutput})
}
}
func displaySegments(bits []bool) {
for i := range bits {
bit := bits[i]
pin := pins[i]
if bit {
pin.High()
} else {
pin.Low()
}
}
}
func displayNumber(number int, dp int) {
x := number
for i := range 4 {
digit := x % 10
x /= 10
bits := make([]bool, 8)
copy(bits, digits[digit])
if i == dp {
bits[7] = true
}
control := controls[3-i]
control.High()
displaySegments(bits)
time.Sleep(SleepAmount)
control.Low()
}
}
func main() {
for {
temperature := machine.ReadTemperature()
temperature /= 10
for range 1000 {
displayNumber(int(temperature), 2)
}
}
}
6. Řízení maticového displeje
Prozatím jsme si ukázali tři možnosti zobrazení informací na vývojové desce:
- Přímé řízení LED připojené na GPIO. Lze tak vizuálně zobrazit binární hodnotu ano/ne, ok/chyba atd.
- Řízení LED přes PWM (pulsní šířkovou modulaci). Již se nejedná o binární hodnotu, ale o rozsah intenzity od „nesvítí“ po „plný svit“.
- Řízení čtyřmístného sedmisegmentového displeje, což nám umožní zobrazit například dekadickou hodnotu 0 až 9999, hexadecimální hodnotu 0000 až FFFF, ale i některá další písmena či znaky.
To ovšem stále nemusí být pro mnoho účelů dostačující a proto je vývojová deska vybavena i maticovým displejem, resp. přesněji řečeno čtveřicí maticových displejů, které dohromady tvoří rastr 32×8 pixelů. Každý pixel je realizován jednou LED a podle způsobu řízení je možné zajistit jak zobrazení čistě binárních obrázků, tak i obrázků ve stupních … zelené.
7. Zapojení maticového displeje
Podívejme se na způsob zapojení maticového displeje na vývojové desce:
Ze schématu je patrné, že celá matice 32×8 pixelů je ve skutečnosti složena ze čtyř matic 8×8 pixelů. Ke každé z těchto matic je připojen osmibitový posuvný registr vybírající sloupce (každý posuvný registr má i svůj záchytný registr – latch). A LED z vybraného sloupce jsou řízeny přes posuvný registr umístěný do levého horního rohu schématu.
Z pohledu programátora tedy musíme ovládat dva posuvné registry:
- Výběr sloupců, což je de facto 32bitový posuvný registr (povšimněte si, že je čtveřice obvodů 4094N zapojena za sebou – výstup jednoho z nich je připojen na D dalšího registru atd.).
- Výběr osmi LED ve sloupci osmibitovým posuvným registrem. Nejprve se do tohoto registru nasune osm bitů a poté se přenesou ze záchytného registru na výstup.
Řízení je čistě věcí software, takže můžeme provádět různé optimalizace. Ovšem nejjednodušší je skutečně vždy vybrat pouze jediný sloupec, nasunout do osmice bitů pro sloupec hodnoty odpovídající LED ve sloupci, přenést tyto hodnoty ze záchytného registru na výstup a chvíli počkat. Takto se tedy provede vykreslování po sloupcích. Ovšem stejně tak je dobré vykreslovat po řádcích popř. například vykreslit stejný vzorek do více sloupců.
8. Softwarové řízení posuvných registrů přes GPIO
Aby bylo možné maticový displej ovládat, musíme mít možnost komunikovat se dvěma posuvnými registry. První z těchto registrů budeme řídit přes GPIO17, GPIO18 a GPIO19. Na první pin je připojen signál, který oznamuje, že se má obsah interního posuvného registru poslat na osmici výstupních pinů. Dalším pinem se do registru nasunuje bit a posledním pinem se provádí posun všech bitů v registru o jednu pozici:
rowsLatch := machine.GP19 rowsData := machine.GP18 rowsClock := machine.GP17
Druhý posuvný registr je 32bitový a fyzicky je vytvořen ze čtveřice osmibitových registrů. Řídit ho budeme přes GPIO20, GPIO21 a GPIO22:
colsLatch := machine.GP22 colsData := machine.GP21 colsClock := machine.GP20
Všechny tyto piny jsou výstupní, proto musíme provést jejich korektní konfiguraci:
rowsLatch.Configure(machine.PinConfig{Mode: machine.PinOutput})
rowsData.Configure(machine.PinConfig{Mode: machine.PinOutput})
rowsClock.Configure(machine.PinConfig{Mode: machine.PinOutput})
colsLatch.Configure(machine.PinConfig{Mode: machine.PinOutput})
colsData.Configure(machine.PinConfig{Mode: machine.PinOutput})
colsClock.Configure(machine.PinConfig{Mode: machine.PinOutput})
9. Základní tvar SW smyčky pro obnovu informací na maticovém displeji
Jak jsme si již řekli v předchozím textu, budeme maticový displej ovládat čistě softwarově, tj. bez pomoci specializovaných obvodů. Zobrazení údajů na displeji spočívá v neustálém opakování tohoto kódu:
for {
for x := range 32 {
for y := range 8 {
// výpočet hodnoty pro LED ve sloupci x na řádku y
// nasunutí hodnoty do sloupce
}
// přenos obsahu sloupcového záchytného registru na displej
// posun bitového vzorku v řádkovém záchytném registru
// přenos obsahu řádkového záchytného registru na displej
}
}
Ve vnějším cyklu se v posuvném 32bitovém registru COL posunuje nulový bit (výběr sloupce) postupně následovaný jedničkovými bity. Programová smyčka, která postupně povolí vysvícení LED v jednom sloupci, tedy může vypadat následovně:
for {
colsData.Low()
colsClock.High()
colsData.High()
for x := range 32 {
// zde se bude řešit nasouvání pixelů ve sloupci
// zde se bude řešit nasouvání pixelů ve sloupci
// zde se bude řešit nasouvání pixelů ve sloupci
colsClock.High()
time.Sleep(1 * time.Millisecond)
colsClock.Low()
colsLatch.High()
time.Sleep(1 * time.Millisecond)
colsLatch.Low()
}
time.Sleep(10 * time.Millisecond)
}
Samozřejmě musíme ještě dopsat logiku pro nasunutí osmi bitů do registru ROW s následným posláním obsahu tohoto posuvného registru na piny připojené k maticovému displeji. Musíme tedy osmkrát provést zápis bitu do registru+nastavení signálu hodin. Po osmici těchto operací se pošle jednotkový puls na pin LATCH:
for {
colsData.Low()
colsClock.High()
colsData.High()
for x := range 32 {
for y := range 8 {
rowsData.Set(true)
rowsClock.Low()
time.Sleep(1 * time.Millisecond)
rowsClock.High()
}
rowsLatch.High()
time.Sleep(1 * time.Millisecond)
rowsLatch.Low()
colsClock.High()
time.Sleep(1 * time.Millisecond)
colsClock.Low()
colsLatch.High()
time.Sleep(1 * time.Millisecond)
colsLatch.Low()
}
time.Sleep(10 * time.Millisecond)
}
10. Demonstrační příklad: vyplnění celé plochy rozsvícenými pixely
Vnořené programové smyčky, které byly popsány v deváté kapitole, nyní použijeme v demonstračním příkladu, který by po svém spuštění měl zapnout (a postupně obnovovat) všechny LED tvořící maticový displej:
package main
import (
"machine"
"time"
)
func main() {
rowsLatch := machine.GP19
rowsData := machine.GP18
rowsClock := machine.GP17
colsLatch := machine.GP22
colsData := machine.GP21
colsClock := machine.GP20
rowsLatch.Configure(machine.PinConfig{Mode: machine.PinOutput})
rowsData.Configure(machine.PinConfig{Mode: machine.PinOutput})
rowsClock.Configure(machine.PinConfig{Mode: machine.PinOutput})
colsLatch.Configure(machine.PinConfig{Mode: machine.PinOutput})
colsData.Configure(machine.PinConfig{Mode: machine.PinOutput})
colsClock.Configure(machine.PinConfig{Mode: machine.PinOutput})
for {
colsData.Low()
colsClock.High()
colsData.High()
for range 32 {
for range 8 {
rowsData.Set(true)
rowsClock.Low()
time.Sleep(1 * time.Millisecond)
rowsClock.High()
}
rowsLatch.High()
time.Sleep(1 * time.Millisecond)
rowsLatch.Low()
colsClock.High()
time.Sleep(1 * time.Millisecond)
colsClock.Low()
colsLatch.High()
time.Sleep(1 * time.Millisecond)
colsLatch.Low()
}
time.Sleep(10 * time.Millisecond)
}
}
Jak budou vypadat výsledky? Obnovování je provedeno relativně pomalu, takže vždy uvidíme pouze část rozsvícených LED (navíc lidské oko vnímá jinak, než CCD). Konkrétně uvidíme pruh několika sloupců LED, přičemž se tento pruh posunuje po displeji:
11. Vykreslení šachovnicového vzorku
Z předchozího pokusu je patrné, že musíme zkrátit doby trvání jednotlivých signálů (překlopení hodin a signálu pro ovládání záchytného registru), protože se displej neobnovuje dostatečně rychle. Namísto čekání jednu milisekundu budeme čekat minimálně jednu mikrosekundu (v praxi to však bude déle). To ovšem není vše. Namísto rozsvícení všech LED si necháme vykreslit šachovnicový vzorek. To vyžaduje zásah do smyčky, ve které se do posuvného registru (pro sloupec) nasunují jednotlivé bity.
Namísto předchozí varianty smyčky ve tvaru:
for range 32 {
for range 8 {
rowsData.Set(true)
použijeme počitadla smyček a rozhodnutí, které LED se mají zapnout a které nikoli:
for x := range 32 {
for y := range 8 {
value := (x+y)%2 == 0
rowsData.Set(value)
Výsledek bude vypadat takto:
Opět si ukažme úplný zdrojový kód tohoto demonstračního příkladu:
package main
import (
"machine"
"time"
)
func main() {
rowsLatch := machine.GP19
rowsData := machine.GP18
rowsClock := machine.GP17
colsLatch := machine.GP22
colsData := machine.GP21
colsClock := machine.GP20
rowsLatch.Configure(machine.PinConfig{Mode: machine.PinOutput})
rowsData.Configure(machine.PinConfig{Mode: machine.PinOutput})
rowsClock.Configure(machine.PinConfig{Mode: machine.PinOutput})
colsLatch.Configure(machine.PinConfig{Mode: machine.PinOutput})
colsData.Configure(machine.PinConfig{Mode: machine.PinOutput})
colsClock.Configure(machine.PinConfig{Mode: machine.PinOutput})
for {
colsData.Low()
colsClock.High()
colsData.High()
for x := range 32 {
for y := range 8 {
value := (x+y)%2 == 0
rowsData.Set(value)
rowsClock.Low()
time.Sleep(1 * time.Microsecond)
rowsClock.High()
time.Sleep(10 * time.Microsecond)
}
rowsLatch.High()
time.Sleep(1 * time.Microsecond)
rowsLatch.Low()
colsClock.High()
time.Sleep(1 * time.Microsecond)
colsClock.Low()
colsLatch.High()
time.Sleep(1 * time.Microsecond)
colsLatch.Low()
}
time.Sleep(10 * time.Microsecond)
}
}
12. Otestování, zda je časování sloupců při ovládání displeje korektní
V dalším demonstračním příkladu si ověříme, jestli je vůbec časování a synchronizace obsluhy obou sad posuvných registrů korektní. Začneme tím, že necháme rozsvícený pouze první sloupec diod, zatímco ostatních 31 sloupců bude zhasnutých:
for x := range 32 {
for range 8 {
value := x == 0
rowsData.Set(value)
// nasunutí hodnoty do sloupce
}
// přenos obsahu sloupcového záchytného registru na displej
// posun bitového vzorku v řádkovém záchytném registru
// přenos obsahu řádkového záchytného registru na displej
}
V případě, že je náš ovládací program napsán nekorektně, například pokud provádí posuny „off-by-one“ (což byl největší problém při ladění kódu), bude zobrazen odlišný sloupec, nebude rozsvícený žádný sloupec nebo bude docházet k různému posunu.
V praxi, tj. po překladu programu a jeho přenosu do Raspberry Pi Pico, bychom měli uvidět tento vzorek:
Následuje výpis celého zdrojového kódu takto upraveného demonstračního příkladu:
package main
import (
"machine"
"time"
)
func main() {
rowsLatch := machine.GP19
rowsData := machine.GP18
rowsClock := machine.GP17
colsLatch := machine.GP22
colsData := machine.GP21
colsClock := machine.GP20
rowsLatch.Configure(machine.PinConfig{Mode: machine.PinOutput})
rowsData.Configure(machine.PinConfig{Mode: machine.PinOutput})
rowsClock.Configure(machine.PinConfig{Mode: machine.PinOutput})
colsLatch.Configure(machine.PinConfig{Mode: machine.PinOutput})
colsData.Configure(machine.PinConfig{Mode: machine.PinOutput})
colsClock.Configure(machine.PinConfig{Mode: machine.PinOutput})
for {
colsData.Low()
colsClock.High()
colsData.High()
for x := range 32 {
for range 8 {
value := x == 0
rowsData.Set(value)
rowsClock.Low()
time.Sleep(1 * time.Microsecond)
rowsClock.High()
time.Sleep(10 * time.Microsecond)
}
rowsLatch.High()
time.Sleep(1 * time.Microsecond)
rowsLatch.Low()
colsClock.High()
time.Sleep(1 * time.Microsecond)
colsClock.Low()
colsLatch.High()
time.Sleep(1 * time.Microsecond)
colsLatch.Low()
}
time.Sleep(10 * time.Microsecond)
}
}
13. Otestování časování řádků
Ještě nám zbývá ověření časování řádků (což není zcela přesné, spíše bychom mohli říci, že se jedná o časování nasunování pixelů do aktivního sloupce). Pro tento účel si necháme rozsvítit celou plochu displeje, s výjimkou trojúhelníku v jeho levé horní části:
for x := range 32 {
for range 8 {
value := y < x
rowsData.Set(value)
// nasunutí hodnoty do sloupce
}
// přenos obsahu sloupcového záchytného registru na displej
// posun bitového vzorku v řádkovém záchytném registru
// přenos obsahu řádkového záchytného registru na displej
}
Vzorek zobrazený na maticovém displeji by měl vypadat takto:
Opět si ukážeme celý zdrojový kód takto upraveného demonstračního příkladu:
package main
import (
"machine"
"time"
)
func main() {
rowsLatch := machine.GP19
rowsData := machine.GP18
rowsClock := machine.GP17
colsLatch := machine.GP22
colsData := machine.GP21
colsClock := machine.GP20
rowsLatch.Configure(machine.PinConfig{Mode: machine.PinOutput})
rowsData.Configure(machine.PinConfig{Mode: machine.PinOutput})
rowsClock.Configure(machine.PinConfig{Mode: machine.PinOutput})
colsLatch.Configure(machine.PinConfig{Mode: machine.PinOutput})
colsData.Configure(machine.PinConfig{Mode: machine.PinOutput})
colsClock.Configure(machine.PinConfig{Mode: machine.PinOutput})
for {
colsData.Low()
colsClock.High()
colsData.High()
for x := range 32 {
for y := range 8 {
value := y < x
rowsData.Set(value)
rowsClock.Low()
time.Sleep(1 * time.Microsecond)
rowsClock.High()
time.Sleep(10 * time.Microsecond)
}
rowsLatch.High()
time.Sleep(1 * time.Microsecond)
rowsLatch.Low()
colsClock.High()
time.Sleep(1 * time.Microsecond)
colsClock.Low()
colsLatch.High()
time.Sleep(1 * time.Microsecond)
colsLatch.Low()
}
time.Sleep(10 * time.Microsecond)
}
}
14. Vykreslení celé bitmapy 32×8 pixelů
Prozatím jsme na maticovém displeji zobrazovali pouze jednoduché vzory. V praxi však budeme chtít zobrazit libovolnou bitmapu. Pro jednoduchost můžeme takovou bitmapu uložit do řezu řetězců. Tento řez bude obsahovat osm prvků, přičemž každý prvek bude řetězec se třiceti dvěma znaky. Mezera znamená „dioda nesvítí“, jakýkoli jiný znak „dioda svítí“:
var raster []string = []string{
//
"***** * * * * * ",
" * * ** * * * ",
" * * * * * * **** **** ",
" * * * ** * * * * *",
" * * * * * * * *",
" * ** * *",
" * * * *",
" **** **** ",
}
Ve skutečnosti je úprava vykreslovací smyčky triviální. Získáme hodnotu „pixelu“ (v tomto případě pixel=znak) a pokud se nejedná o mezeru, nastaví se bit nasunovaný do posuvného registru ROW na logickou jedničku:
for x := range 32 {
for y := range 8 {
pixel := raster[7-y][x]
value := pixel != ' '
rowsData.Set(value)
...
...
...
}
...
...
...
}
Výsledek by měl vypadat takto:
Obrázek 12: Vykreslená bitmapa. Povšimněte si „duchů“ (ghostingu), což je chyba, kterou lze opravit změnou časování signálů posuvných registrů. Ve skutečnosti je ghosting patrný jen na snímku a nikoli při pohledu na reálný displej.
Celý zdrojový kód takto upraveného demonstračního příkladu:
package main
import (
"machine"
"time"
)
var raster []string = []string{
//
"***** * * * * * ",
" * * ** * * * ",
" * * * * * * **** **** ",
" * * * ** * * * * *",
" * * * * * * * *",
" * ** * *",
" * * * *",
" **** **** ",
}
func main() {
rowsLatch := machine.GP19
rowsData := machine.GP18
rowsClock := machine.GP17
colsLatch := machine.GP22
colsData := machine.GP21
colsClock := machine.GP20
rowsLatch.Configure(machine.PinConfig{Mode: machine.PinOutput})
rowsData.Configure(machine.PinConfig{Mode: machine.PinOutput})
rowsClock.Configure(machine.PinConfig{Mode: machine.PinOutput})
colsLatch.Configure(machine.PinConfig{Mode: machine.PinOutput})
colsData.Configure(machine.PinConfig{Mode: machine.PinOutput})
colsClock.Configure(machine.PinConfig{Mode: machine.PinOutput})
for {
colsData.Low()
colsClock.High()
colsData.High()
for x := range 32 {
for y := range 8 {
pixel := raster[7-y][x]
value := pixel != ' '
rowsData.Set(value)
rowsClock.Low()
time.Sleep(1 * time.Microsecond)
rowsClock.High()
time.Sleep(10 * time.Microsecond)
}
rowsLatch.High()
time.Sleep(1 * time.Microsecond)
rowsLatch.Low()
colsClock.High()
time.Sleep(1 * time.Microsecond)
colsClock.Low()
colsLatch.High()
time.Sleep(1 * time.Microsecond)
colsLatch.Low()
}
time.Sleep(10 * time.Microsecond)
}
}
15. Scrolling obrazu s využitím potenciometru
Nyní, když dokážeme vykreslit prakticky jakýkoli rastrový obraz na maticovém displeji, můžeme spojit tuto schopnost s ovládáním posunu (scrollingu) přes potenciometr. Jak se čte hodnota potenciometru s využitím analogově-digitálního převodníku jsme si již ukázali minule. Můžeme se tedy pokusit o posun vykreslovaného rastrového obrázku o hodnotu 0–31 (pixelů), kterou nějakým způsobem získáme z aktuální orientace potenciometru.
Původně vypadala smyčka pro obnovení obsahu displeje takto:
for x := range 32 {
for y := range 8 {
pixel := raster[7-y][x]
value := pixel != ' '
rowsData.Set(value)
...
...
...
}
...
...
...
}
Úprava spočívá v tom, že přečteme hodnotu potenciometru (16 bitů), kterou posuneme doprava o 11 bitů. Výsledkem je pětibitová hodnota 0..31, o kterou se posuneme při čtení obsahu rastrového obrázku, který se má zobrazit (pochopitelně včetně řešení přetečení):
var scroll = int(adc2.Get()) >> 11
for x := range 32 {
for y := range 8 {
pixel := raster[7-y][(x+scroll)%32]
value := pixel != ' '
rowsData.Set(value)
Výsledky:
Opět si ukažme úplný zdrojový kód takto upraveného demonstračního příkladu:
package main
import (
"machine"
"time"
)
var raster []string = []string{
//
"***** * * * * * ",
" * * ** * * * ",
" * * * * * * **** **** ",
" * * * ** * * * * *",
" * * * * * * * *",
" * ** * *",
" * * * *",
" **** **** ",
}
func main() {
machine.InitADC()
adc0 := machine.ADC{Pin: machine.ADC0}
adc1 := machine.ADC{Pin: machine.ADC1}
adc2 := machine.ADC{Pin: machine.ADC2}
adc0.Configure(machine.ADCConfig{})
adc1.Configure(machine.ADCConfig{})
adc2.Configure(machine.ADCConfig{})
rowsLatch := machine.GP19
rowsData := machine.GP18
rowsClock := machine.GP17
colsLatch := machine.GP22
colsData := machine.GP21
colsClock := machine.GP20
rowsLatch.Configure(machine.PinConfig{Mode: machine.PinOutput})
rowsData.Configure(machine.PinConfig{Mode: machine.PinOutput})
rowsClock.Configure(machine.PinConfig{Mode: machine.PinOutput})
colsLatch.Configure(machine.PinConfig{Mode: machine.PinOutput})
colsData.Configure(machine.PinConfig{Mode: machine.PinOutput})
colsClock.Configure(machine.PinConfig{Mode: machine.PinOutput})
for {
colsData.Low()
colsClock.High()
colsData.High()
var scroll = int(adc2.Get()) >> 11
for x := range 32 {
for y := range 8 {
pixel := raster[7-y][(x+scroll)%32]
value := pixel != ' '
rowsData.Set(value)
rowsClock.Low()
time.Sleep(1 * time.Microsecond)
rowsClock.High()
time.Sleep(10 * time.Microsecond)
}
rowsLatch.High()
time.Sleep(1 * time.Microsecond)
rowsLatch.Low()
colsClock.High()
time.Sleep(1 * time.Microsecond)
colsClock.Low()
colsLatch.High()
time.Sleep(1 * time.Microsecond)
colsLatch.Low()
}
time.Sleep(10 * time.Microsecond)
}
}
16. Raspberry Pi Pico připojené ve funkci terminálu
Ve chvíli, kdy se do Raspberry Pi Pico přes USB přesune soubor s binárním spustitelným kódem, dojde k automatickému odpojení Raspberry (v režimu mass storage) a k opětovnému připojení v režimu terminálu (resp. přesněji řečeno v režimu sériového přenosu). To například znamená, že je možné v programech používat klasické vstupně-výstupní operace.
[2399478.090936] usb 3-1: new full-speed USB device number 106 using xhci_hcd [2399478.216206] usb 3-1: New USB device found, idVendor=2e8a, idProduct=000a, bcdDevice= 1.00 [2399478.216224] usb 3-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3 [2399478.216230] usb 3-1: Product: Pico [2399478.216235] usb 3-1: Manufacturer: Raspberry Pi [2399478.220389] cdc_acm 3-1:1.0: ttyACM0: USB ACM device
Způsob připojení (jméno zařízení se získá z dmesg):
$ minicom -D /dev/ttyACM0
Pokud se k Raspberry Pi Pico připojíme například přes výše zmíněný Minocom, lze sledovat zprávy tištěné aplikací. Příkladem je detekci stisku tlačítka na vývojové desce:
package main
import (
"machine"
"time"
)
const InputPin = machine.GP0
const OutputPin = machine.GP2
func main() {
led := OutputPin
led.Configure(machine.PinConfig{Mode: machine.PinOutput})
button := InputPin
button.Configure(machine.PinConfig{Mode: machine.PinInput})
for {
pressed := button.Get()
led.Set(pressed)
if pressed {
println("Button press detected")
const SleepAmount = time.Second * 1
time.Sleep(SleepAmount)
}
}
}
Taktéž je možné, aby aplikace čekala na data zadaná z klávesnice a poslaná přes Minicom do Raspberry Pi Pico:
package main
import "fmt"
func main() {
var name string
fmt.Print("Name: ")
fmt.Scanln(&name)
fmt.Printf("Hello, %s!\n", name)
}
17. Sériové rozhraní SPI
Sběrnice resp. přesněji řečeno rozhraní SPI (Serial Peripheral Interface) představuje jednu z forem sériových externích sběrnic sloužících pro vzájemné propojení dvou či více komunikujících uzlů, přičemž jeden uzel obvykle vystupuje v roli takzvaného řadiče sběrnice (master), ostatní uzly pracují v režimu slave. Uzel, který pracuje jako master, obsahuje generátor hodinového signálu, který je rozveden do všech ostatních uzlů, čímž je umožněn zcela synchronní (navíc ještě obousměrný) přenos dat. Hodinový signál je rozváděn vodičem označovaným symbolem SCK.
Kromě vodiče s hodinovým signálem jsou uzly propojeny dvojicí vodičů označovaných většinou symboly MISO (Master In, Slave Out) a MOSI (Master Out, Slave In), pomocí nichž se obousměrně (full duplex) přenáší data. Posledním signálem, který se u této sběrnice používá, je signál SSEL (Slave Select), jenž slouží – jak již jeho název napovídá – k výběru některého uzlu pracujícího v režimu slave. Všechny čtyři signály – SCK, MISO, MOSI i SSEL, pro svoji funkci vyžadují pouze jednosměrné porty, což přispívá k jednoduché a především levné implementaci této sběrnice.
Raspberry Pi Pico nabízí dvojici na sobě nezávislých SPI rozhraní, přičemž pro každé SPI může být rezervováno několik pinů: MOSI, MISO, SCLK (nebo SCK) a výstupní piny pro výběr obvodu CSn (jeden z těchto pinů je propojen na druhé straně s výběrovým pinem SSEL). Konkrétně se jedná o následující piny:
| Rozhraní | MISO | MOSI | SCLK | CSn |
|---|---|---|---|---|
| SPI0 | GP0/GP4/GP16 | GP3/GP7/GP19 | GP2/GP6/GP18 | GP1/GP5/GP17 |
| SPI1 | GP8/GP12 | GP11/GP15 | GP10/GP14 | GP9/GP13 |
18. Sériová sběrnice I2C
Další sériovou sběrnicí, kterou si dnes a především v navazující části tohoto seriálu popíšeme, je sběrnice označovaná poněkud neobvykle symbolem I2C, což je zkratka z celého názvu Inter-Integrated Circuit. V určitých ohledech se jedná o sběrnici podobnou rozhraní SPI (existence hodinového signálu, jediný uzel typu master), ovšem některé vlastnosti těchto sběrnic jsou odlišné. Zatímco u sběrnice SPI byl umožněn obousměrný přenos dat díky použití dvojice vodičů MISO a MOSI, je sběrnice I2C vybavena „pouze“ jedním datovým vodičem SDA, z čehož vyplývá, že se data přenáší poloduplexně (a ušetří se jak piny, tak i vodiče). Také to znamená poněkud složitější interní strukturu všech připojených zařízení, protože příslušné piny musí být možné přepínat ze vstupního režimu na režim výstupní.
Navíc zde není použit výběr zařízení typu slave pomocí zvláštních signálů, protože každému uzlu je přiřazena jednoznačná adresa – kromě elektrických charakteristik je totiž přesně stanoven i komunikační protokol, což je další rozdíl oproti výše popsanému rozhraní SPI. Obecně je možné říci, že I2C je sice poněkud složitější, ale zato flexibilnější sběrnice, která se velmi často používá i pro komunikaci na delší vzdálenosti (řádově metry, viz například DDC u monitorů), než tomu je u sběrnice SPI. V navazující části seriálu si řekneme, jakým způsobem komunikace probíhá a jak se vlastně jednotlivá zařízení adresují.
Pro I2C se na Raspberry Pi Pico mohou použít dvojice pinů SDA a SCL. Současně mohou být nakonfigurovány dvě sběrnice I2C0 a I2C1, což znamená čtveřici obsazených GPIO:
| Sběrnice | SDA | SCL |
|---|---|---|
| I2C0 | GP0/GP4/GP8/GP12/GP16/GP20 | GP1/GP5/GP9/GP13/GP17/GP21 |
| I2C1 | GP2/GP6/GP10/GP14/GP18/GP26 | GP3/GP7/GP11/GP15/GP19/GP27 |
19. Repositář s demonstračními příklady
Zdrojové kódy všech minule i dnes použitých demonstračních příkladů byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/go-root (stále na GitHubu :-). V případě, že nebudete chtít klonovat celý repositář, můžete namísto toho použít odkazy na jednotlivé demonstrační příklady, které naleznete v následující tabulce:
| # | Příklad | Stručný popis | Cesta |
|---|---|---|---|
| 1 | blink.go | blikání LED připojenou ke zvolenému GPIO | https://github.com/tisnik/go-root/blob/master/tinygo-rp2040/blink.go |
| 2 | button1.go | čtení tlačítka připojeného ke zvolenému GPIO, základní varianta | https://github.com/tisnik/go-root/blob/master/tinygo-rp2040/button1.go |
| 3 | button2.go | čtení tlačítka připojeného ke zvolenému GPIO, pojmenované konstanty s GPIO | https://github.com/tisnik/go-root/blob/master/tinygo-rp2040/button2.go |
| 4 | button3.go | čtení tlačítka připojeného ke zvolenému GPIO, vylepšení vizualizace stisku tlačítka | https://github.com/tisnik/go-root/blob/master/tinygo-rp2040/button3.go |
| 5 | 7segments1.go | ovládání sedmisegmentového displeje, základní varianta | https://github.com/tisnik/go-root/blob/master/tinygo-rp2040/7segments1.go |
| 6 | 7segments2.go | ovládání sedmisegmentového displeje s více číslicemi | https://github.com/tisnik/go-root/blob/master/tinygo-rp2040/7segments2.go |
| 7 | 7segments3.go | rozsvícení libovolné kombinace segmentů | https://github.com/tisnik/go-root/blob/master/tinygo-rp2040/7segments3.go |
| 8 | 7segments4.go | tisk hodnoty 0 až 9999 na čtyřmístném sedmisegmentovém displeji | https://github.com/tisnik/go-root/blob/master/tinygo-rp2040/7segments4.go |
| 9 | adc1.go | analogově digitální převodník: čtení stavu potenciometru s tiskem hodnoty odpovídající jeho natočení, základní varianta | https://github.com/tisnik/go-root/blob/master/tinygo-rp2040/adc1.go |
| 10 | adc2.go | čtení stavu potenciometru, varianta méně závislá na šumu ADC | https://github.com/tisnik/go-root/blob/master/tinygo-rp2040/adc2.go |
| 11 | pwm1.go | pulsně šířková modulace: příklad bez detekce chyb | https://github.com/tisnik/go-root/blob/master/tinygo-rp2040/pwm1.go |
| 12 | pwm2.go | pulsně šířková modulace: příklad s detekcí chyb (vizualizováno přes LED) | https://github.com/tisnik/go-root/blob/master/tinygo-rp2040/pwm2.go |
| 13 | temperature1.go | změření teploty senzorem na čipu | https://github.com/tisnik/go-root/blob/master/tinygo-rp2040/temperature1.go |
| 14 | temperature2.go | menší frekvence měření teploty | https://github.com/tisnik/go-root/blob/master/tinygo-rp2040/temperature2.go |
| 15 | temperature3.go | úprava pro zobrazení desetinné tečky | https://github.com/tisnik/go-root/blob/master/tinygo-rp2040/temperature3.go |
| 16 | matrix1.go | ovládání maticového LED displeje: pomalé časování signálů | https://github.com/tisnik/go-root/blob/master/tinygo-rp2040/matrix1.go |
| 17 | matrix2.go | zobrazení šachovnicového vzorku | https://github.com/tisnik/go-root/blob/master/tinygo-rp2040/matrix2.go |
| 18 | matrix3.go | test korektního načasování pro zobrazení prvního sloupce | https://github.com/tisnik/go-root/blob/master/tinygo-rp2040/matrix3.go |
| 19 | matrix4.go | test korektního načasování pro zobrazení řádků | https://github.com/tisnik/go-root/blob/master/tinygo-rp2040/matrix4.go |
| 20 | matrix5.go | zobrazení rastrového obrázku na maticovém LED displeji | https://github.com/tisnik/go-root/blob/master/tinygo-rp2040/matrix5.go |
| 21 | matrix6.go | skrolování informace na maticovém LED displeji pomocí potenciometru | https://github.com/tisnik/go-root/blob/master/tinygo-rp2040/matrix6.go |
| 22 | terminal1.go | výpis informací na terminál | https://github.com/tisnik/go-root/blob/master/tinygo-rp2040/terminal1.go |
| 23 | terminal2.go | přečtení informací z terminálu | https://github.com/tisnik/go-root/blob/master/tinygo-rp2040/terminal2.go |
20. Odkazy na Internetu
- TinyGo – A Go Compiler For Small Places
https://tinygo.org/ - Getting started
https://tinygo.org/getting-started/ - Go.dev (klasická varianta překladače jazyka Go)
https://go.dev/ - gccgo
https://gcc.gnu.org/onlinedocs/gccgo/ - Setting up and using gccgo
https://go.dev/doc/install/gccgo - Awesome Go
https://github.com/avelino/awesome-go - TinyGo: Inline assembly
https://tinygo.org/docs/concepts/compiler-internals/inline-assembly/ - Getting Started with TinyGo: Bringing Go to Microcontrollers and WebAssembly
https://dev.to/ekwoster/getting-started-with-tinygo-bringing-go-to-microcontrollers-and-webassembly-2pp0 - Optimizing Go code with GCCGO for improved performance
https://dev.to/parmcoder/optimizing-go-code-with-gccgo-for-improved-performance-2d3d - The Untold Power of TinyGo: How to Run Go on Microcontrollers and Supercharge Embedded Development
https://dev.to/ekwoster/the-untold-power-of-tinygo-how-to-run-go-on-microcontrollers-and-supercharge-embedded-development-2g7d - From Arduino to Mars: Why You Should Be Using TinyGo for Embedded Web Development
https://dev.to/ekwoster/from-arduino-to-mars-why-you-should-be-using-tinygo-for-embedded-web-development-54od - Optimizing binaries
https://tinygo.org/docs/guides/optimizing-binaries/ - Why TinyGo Might Be the Future of Embedded WebAssembly & How To Get Started Today
https://ekwoster.dev/post/-why-tinygo-might-be-the-future-of-embedded-webassembly-how-to-get-started-today/ - TinyGo na GitHubu
https://github.com/tinygo-org/tinygo - Compile Go directly to WebAssembly components with TinyGo and WASI P2
https://wasmcloud.com/blog/compile-go-directly-to-webassembly-components-with-tinygo-and-wasi-p2/ - Do you use gccgo?
https://www.reddit.com/r/golang/comments/j1g1z6/do_you_use_gccgo/ - Go v/s TinyGo: Which one is the best for you?
https://blog.nonstopio.com/go-v-s-tinygo-which-one-is-the-best-for-you-73cac3c7849e - Go Wiki: GccgoCrossCompilation
https://go.dev/wiki/GccgoCrossCompilation - Oficiální stránky Gccgo
https://gcc.gnu.org/onlinedocs/gccgo/index.html - What are the primary differences between ‚gc‘ and ‚gccgo‘?
https://stackoverflow.com/questions/25811445/what-are-the-primary-differences-between-gc-and-gccgo - Setting up and using gccgo
https://go.dev/doc/install/gccgo - Go (Arch Linux)
https://wiki.archlinux.org/title/Talk:Go - Why are binaries built with gccgo smaller (among other differences?)
https://stackoverflow.com/questions/27067112/why-are-binaries-built-with-gccgo-smaller-among-other-differences - Why Everyone Is Sleeping On TinyGo: Run Go on Microcontrollers and the Web (WASM) Today!
https://ekwoster.dev/post/-why-everyone-is-sleeping-on-tinygo-run-go-on-microcontrollers-and-the-web-wasm-today/ - Go (Golang) GOOS and GOARCH
https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63 - Externí sériové sběrnice SPI a I²C
https://www.root.cz/clanky/externi-seriove-sbernice-spi-a-i2c/ - Sedmisegmentový displej (Wikipedie)
https://cs.wikipedia.org/wiki/Sedmisegmentov%C3%BD_displej - A/D převodník (Wikipedie)
https://cs.wikipedia.org/wiki/A/D_p%C5%99evodn%C3%ADk - D/A převodník (Wikipedie)
https://cs.wikipedia.org/wiki/D/A_p%C5%99evodn%C3%ADk - Pulzně šířková modulace
https://cs.wikipedia.org/wiki/Pulzn%C4%9B_%C5%A1%C3%AD%C5%99kov%C3%A1_modulace - TinyGo: Using PWM
https://tinygo.org/docs/tutorials/pwm/ - General-purpose input/output
https://en.wikipedia.org/wiki/General-purpose_input/output - KWM-20881AGBLUCKYLIGHT-Display LED
https://www.tme.eu/en/details/kwm-20881agb/led-displays-matrix/luckylight/ - ESP-32 – LED Matrix
https://esp32io.com/tutorials/esp32-led-matrix - Seven-segment display character representations
https://en.wikipedia.org/wiki/Seven-segment_display_character_representations - Binary image
https://en.wikipedia.org/wiki/Binary_image












