GPIO linky jsou buď nastaveny jako vstupní anebo jako výstupní. O elektronice pojednáváme v tomto článku. Zaměříme se na softwarovou stránku řešení úkolů za pomocí GPIO linek ve výstupním režimu.

Programování výstupu je poměrně jednoduché. V programu můžeme zvolit čas změny stavu linky kdy potřebujete a můžete používat systémové hodiny, které pracují velmi přesně. Problém může nastat jenom tehdy, pokud potřebujeme měnit stav několika linek synchronně. To vyvolává otázku, jak rychle může Pico měnit řadu GPIO linek, protože to trochu omezuje snadnost, jak to můžeme dělat.

Základní funkce

Existuje několik základních funkcí, které pracují s jednou GPIO linkou. Některé už jsme vysvětlovali v předchozím článku.

GPIO linku můžeme inicializovat dvěma způsoby. Číslo pinu je určeno parametrem gpio.

Inicializace GPIO, aby fungovalo jako softwarově řízené
gpio_set_function( uint gpio, GPIO_FUNC_SIO) (1)
gpio_init( uint gpio)   (2)
1 Prostá inicializace.
2 Inicializuje GPIO a nastavuje linku jako vstupní.
Směr linky nastavuje funkce
gpio_set_dir( uint gpio, bool out) (1)
1 Pokud je parametr out false linka je vstupní. Je-li out true je linka výstupní.
Zjistění směru linky
static uint gpio_get_dir( uint gpio )       (1)
static bool gpio_is_dir_out( uint gpio )    (2)
1 Funkce vrací 0 — linka je vstupní nebo 1 — linka je výstupní
2 Funkce vrací false — linka je vstupní a true — linka je výstupní

Jakmile je linka nastavena můžeme použít:

static bool gpio_get( uint gpio ) (1)
gpio_put( uint gpio, bool value ) (2)
1 Přečte stav linky.
2 Nastaví stav linky na hodnotu, danou parametrem value (0 nebo 1).

Obě dvě funkce pracují s oběma směry linky, je jedno je-li linka vstupní nebo výstupní.

V SDK je i funkce:

gpio_set_input_enabled( uint gpio, bool enabled )

která funguje tak, že pokud je enabled je false, nastaví linku tak, že přestane odpovídat na vstup. Není však k dispozici podobná funkce pro výstup, přestože to hardware umožňuje.

Práce s několika GPIO linkami současně

Dosud jsme se bavili o funkcích, které nám umožňovaly měnit jenom jednu GPIO linku. Chcete-li měnit více než jednu linku, existuje řada funkcí maskování, které ovlivňují jenom GPIO linky, zadané v binární masce.

Maska je jednoduchá; každý bit v masce představuje jednu linku z 30, bit 0 pro GP0, bit 1 pro GP1 atd.

Konstrukce masky se dělá takto: předpokládejme, že chceme mít v masce pin GP3, nastavíme v masce třetí bit. To uděláme tak, že jedničku posuneme třikrát doleva 1 << 3. Obecný algoritmus pro n-tý pin je 1 << n.

Nyní chceme maskovat n-tý a m-tý pin, to uděláme pomocí operátoru | (bitwise OR), takže celá maska bude:

( 1 << n ) | ( 1 << m )

Například pro třetí a pátý GPIO pin to bude:

uint32_t mask = (1 << 3) | ( 1 << 5 );

Masku máme připravenou, můžeme ji použít v tzv. maskovacích funkcích

Funkce gpio_init_mask nastavuje maskované GPIO linky jako vstupní:

gpio_init_mask( uint gpio_mask ) (1)
1 Tady by měl spíš být typ uint32_t než uint ?

U inicializovaných linek můžeme nastavit směr:

gpio_set_dir_out_masked( uint32_t mask ) (1)
gpio_set_dir_in_masked( uint32_t mask ) (2)
gpio_set_dir_masked( uint32_t mask, uint32_t value ) (3)
1 Nastavuje maskované linky jako výstupní
2 Nastavuje maskované linky jako vstupní
3 Nastavuje maskované linky podle masky value, bit 0 v masce value znamená vstup, bit 1 výstup

Po nastavení směru můžeme nastavovat výstupní stav pomocí:

gpio_set_mask( uint32_t mask ) (1)
gpio_clr_mask( uint32_t mask ) (2)
gpio_xor_mask( uint32_t mask ) (3)
gpio_put_masked( uint32_t mask, uint32_t value ) (4)
1 Nastaví maskované linky na 1
2 Nastaví maskované linky na 0
3 Obrátí stav maskovaných linek
4 Nastaví maskované linky podle bitů value.

Všechny funkce pracují samozřejmě jenom s GPIO linkami specifikované v masce.

Příklad
uint32_t mask  = ( 1 << 3 ) | ( 1 << 5 );
uint32_t value = ( 1 << 5 );
gpio_set_dir_masked( mask, value ); (1)
1 Nastaví pin 3 a pin 5, protože maska specifikuje tyto piny. Pin 3 nastaví na 0, protože 3.bit value je nulový a pin 5 nastaví na 1, protože 5.bit value je jedničkový. Stav bitu ve value, který nekoresponduje s maskou mask nemá vliv.

Maskovací funkce jsou velmi užitečné, chceme-li synchronně nastavovat více pinů.

Máme k dispozici ještě 3 funkce, které pracují se všemi piny:

static void gpio_set_dir_all_bits( uint32_t values ) (1)
static uint23_t gpio_get_all() (2)
static void gpio_put_all( uint32_t value ) (3)
1 Nastavuje směr všch pinů.
2 Vrací aktualní stav pinů jako 29 bitovou hodnotu.
3 Nastavuje stav všech 29 pinů podle bitu v hodnotě value.

Tyto funkce použijeme jenom zřídka kdy.

Jak rychle můžeme měnit GPIO

Základní otázkou, na kterou potřebujeme najít odpověď u libovolného procesoru, který funguje v embedded nebo IoT je, jak rychle mohou GPIO pracovat? Někdy nás odpověď nemusí příliš zajímat, protože pracujeme docela pomalu (např. blikání LEDkou). Libovolná aplikace, která potřebuje časy okolo desítek milisekund bude pracovat téměř na každém procesoru. Avšak jakmile chceme implementovat třeba vlastní protokol nebo cokoliv, co vyžaduje mikrosekundy, ba dokonce nanosekundy, tak nás rychlost bude zajímat velmi.

Je docela jednoduché zjistit, jak rychle jsme schopni řídit jednu GPIO linku, pokud máme k dispozici logický analyzátor nebo osciloskop. Uděláme jednoduchý prográmek a změříme si to:

gpio_speed.c
#include "pico/stdlib.h"

int main()
{
    gpio_set_function( 22, GPIO_FUNC_SIO );
    gpio_set_dir( 22, true );

    while( true ) {
        gpio_put( 22, 1 );
        gpio_put( 22, 0 );
    }
}

Program bychom měli překládat bez debuggovacích informací.

$ mkdir build
$ cd build
$ cmake -DCMAKE_BUILD_TYPE=Release ..
$ make -j $(nproc)

Měření osciloskopem

Je to nějaké divné: 22ns je hodně rychlé a pulsy jsou divné tvarově a nedosahují 3.3V

IMG 20240907 151836

Tady to vypadá na nějakou chybu měření anebo osciloskop nestíhá. Zkusím program trochu upravit:

#include "pico/stdlib.h"

int main()
{
    gpio_set_function( 16, GPIO_FUNC_SIO );
    gpio_set_dir( 16, true );

    while( true ) {
        gpio_put( 16, 1 );
        sleep_us(1);        (1)
        gpio_put( 16, 0 );
        sleep_us(1);        (2)
    }
}
1 Pauza 1 ms.
2 Pauza 1 ms.
Pulsy jsou v pořádku

IMG 20240907 155825

Ještě zkusím použít funkci busy_wait_us_32(1), která by měla udělat pauzu přesně 1 ms.

#include "pico/stdlib.h"

int main()
{
    gpio_set_function( 16, GPIO_FUNC_SIO );
    gpio_set_dir( 16, true );

    while( true ) {
        gpio_put( 16, 1 );
        busy_wait_us_32(1);
        gpio_put( 16, 0 );
        busy_wait_us_32(1);
    }
}
Pulsy jsou v pořádku, perioda je trochu kratší

IMG 20240907 161413

Budu muset sehnat lepší osciloskop, můj kapesní osciloskop z Číny za pár korun je do 100 MHz, Pico beží v základu na 133 MHz. Pokud by gpio_put() trvala 1 instrukci, tak to nezměřím.

Vkládání prodlev do programu

Abychomo mohli vytvářet pulsy známé časové délky, potřebujeme pozastavi program mezi změnami stavu.

Máme k dispozici funkci sleep_ms( pocet_ms ), kterou můžeme zastavit program na zadaný počet milisekund. V C SDK jsou k dispozici další funkce, které mohou vkládat do programu kratší prodlevy. Jsou tu dvě skupiny funkcí sleep funkce a busy_wait funkce. Sleep funkce funguje tak, že přepne procesor do stavu s nízkou spotřebou dokud neuplyne čas, který jsme nastavili, poté je procesor probuzen a program pokračuje. Malý problém je v tom, že sleep funkce potřebuje určitý čas na probuzení procesoru a je garantováno, že prodleva není kratší než nastavený čas. Může být o trochu delší. Busy_wait funkce neuspává procesor a výsledná prodleva je přesnější. Obě funkce jsou velmi podobné:

sleep_us( uint64_t us )
busy_wait_us( uint64_t delay_us )

Obě funkce čekají po stanovený čas v mikrosekundách, sleep_us šetří příkonem, busy_wait_us nikoliv, ale je přesnější. Existuje i 32-bitová verze busy_wait která je šikovnější:

busy_wait_us_32( uint32_t delay_us )

Rozdíly jsou vidět na obrázcích měření osciloskopem.

Pokud potřebujeme vložit prodlevu delší než 1 ms, je lepší použít funkci:

sleep_ms( uint32_t ms )

Neexistuje busy_wait alternativa pro milisekundy, protože jesliže chcete dělat delší prodlevy, tak určitě chcete šetřit napájecí příkon a prodleva bude předná na milisekundy.

V SDK jsou i funkce, kterými můžete nastavovat prodlevu až dotud, než systémový čas dosáhne určitou hodnotu:

sleep_until( absolute_time_t target)
busy_wait_until( absolute_time_t t )

Příklady použití funkcí prodlevy

gpio_speed_10.c
#include "pico/stdlib.h"

int main()
{
    gpio_set_function( 16, GPIO_FUNC_SIO );
    gpio_set_dir( 16, true );

    while( true ) {
        gpio_put( 16, 1 );
        gpio_put( 16, 1 );
        gpio_put( 16, 1 );
        gpio_put( 16, 1 );
        gpio_put( 16, 1 );
        gpio_put( 16, 1 );
        gpio_put( 16, 1 );
        gpio_put( 16, 1 );
        gpio_put( 16, 1 );
        gpio_put( 16, 1 );
        gpio_put( 16, 0 );
        gpio_put( 16, 0 );
        gpio_put( 16, 0 );
        gpio_put( 16, 0 );
        gpio_put( 16, 0 );
        gpio_put( 16, 0 );
        gpio_put( 16, 0 );
        gpio_put( 16, 0 );
        gpio_put( 16, 0 );
        gpio_put( 16, 0 );
    }
}

Tady už jsem změřil rychlost osciloskopem, v programu žádné prodlevy pomocí sleep nebo busy_wait nejsou, ale pulsy jsou 10x pomalejší díky desetinásobnému použití gpio_put( 16, 1 ) a gpio_put( 16, 0 ). Pin 16 jsem zvolil jenom kvůli pohodlnějšímu měření.

Rychlé GPIO

IMG 20240907 172051

Teď mi docvaklo, proč jsem naměřil při měření rychlosti GPIO nesmysly. Parazitní kapacita (ať už to je sonda k osciloskopu nebo nějaké vodiče na bradboardu) zaobluje náběžné a sestupné hrany impulsů. Na oscilogramu je vidět frekvence 5.67 MHz, jeden dílek na vodorovné stupnici je 25 ns a celý puls trvá 79 ns (zaokrouhlím na 80 ns). Než naběhne napětí na GPIO16 na plných 3.3 V, tak to trvá cca 30 ns. To znamená, že s jedním voláním gpio_put(pin, hodnota); dostaneme kraťoučký puls 8 ns, což nestačí naběhnout naplno a proto byl první obrázek tak mizerný. Frekvence byla 125 MHz a to mi osciloskop nezměří.

Měřeno Tekronixem 2465, sonda je s pružinkou (malá zemní smyčka)

IMG 20240907 190520 BURST004

Praktické použití

Nejkratší pulsy, které se dají prakticky použít jsou 3 volání gpio_put() za sebou bez pauzy.

#include "pico/stdlib.h"

int main()
{
    gpio_set_function( 16, GPIO_FUNC_SIO );
    gpio_set_dir( 16, true );

    while( true ) {
        gpio_put( 16, 1 );
        gpio_put( 16, 1 );
        gpio_put( 16, 1 );

        gpio_put( 16, 0 );
        gpio_put( 16, 0 );
        gpio_put( 16, 0 );

    }
}
Tři gpio_put za sebou

IMG 20240907 192220

Je vidět, že do obdélníku má průběh daleko, ale dosahuje horní úrovně 3.3 V, puls má 30 ns.

Použití prodlev

gpio_pauza1.c
#include "pico/stdlib.h"

int main()
{
    gpio_set_function( 16, GPIO_FUNC_SIO );
    gpio_set_dir( 16, true );

    while( true ) {
        gpio_put( 16, 1 );
        busy_wait_us_32(1); (1)
        gpio_put( 16, 0 );
        busy_wait_us_32(1); (2)
    }
}
1 Prodleva 1 ms, pin 16 bude mít hodnotu 1 po celou tuto prodlevu.
2 Další prodleva 1 ms, pin 16 bude mít hodnotu 0 po celou tuto prodlevu.
Tady je průběh pulsů v pořádku

IMG 20240907 195444

Je vidět, že obdélník trvá přesně 1 milisekundu. Zákmity nahoře a dole jsou normální, nejsme schopni udělat čistý obdélník.

gpio_pauza1.c
#include "pico/stdlib.h"

int main()
{
    gpio_set_function( 16, GPIO_FUNC_SIO );
    gpio_set_dir( 16, true );

    while( true ) {
        gpio_put( 16, 1 );
        sleep_us(1); (1)
        gpio_put( 16, 0 );
        sleep_us(1); (2)
    }
}
1 Prodleva 1 ms, pin 16 bude mít hodnotu 1 po celou tuto prodlevu.
2 Další prodleva 1 ms, pin 16 bude mít hodnotu 0 po celou tuto prodlevu.

Pulsy jsou malinko delší.

Prodleva pomocí smyčky for()

Je ještě jeden způsob jako udělat čekací smyčku, prostě uděláme smyčku for kde se nebude dělat nic.

Tento program bude dělat pulsy v závislosti na hodnotě n.

#include "pico/stdlib.h"

int main()
{
    gpio_set_function(16, GPIO_FUNC_SIO);
    gpio_set_dir(16, true);
    volatile int i;
    int n = 1;
    while (true) {
        for( i = 0; i < n; i++ ) {} (1)
        gpio_put( 16, 1 );
        for( i = 0; i < n; i++ ) {} (2)
        gpio_put( 16, 0 );
    }
}
1 V této smyčce se záměrně nedělá nic, slouží jen k prodloužení délky trvání nízké úrovně pulsu.
2 V této smyčce se záměrně nedělá nic, slouží jen k prodloužení délky trvání vysoké úrovně pulsu.
n délka pulsu v ns naměřeno ns

1

100 — 150 ns

134 ns

2

200 — 250 ns

218 ns

3

250 — 300 ns

293 ns

4

350 — 400 ns

375 ns

5

450 — 500 ns

456 ns

6

500 — 550 ns

538 ns

7

600 — 650 ns

620 ns

8

700 — 750 ns

701 ns

9

750 — 800 ns

783 ns

10

850 — 900 ns

864 ns

11

900 — 950 ns

946 ns

12

950 ns — 1050 ns

1020 ns

Prodleva trvající přesný čas

Obecný problém který budeme občas řešit je ten, že v programu v nějakém úseku něco děláme a nevíme přesně jak dlouho to trvá, avšak potřebujeme, aby celý úsek trval přesně zadaný čas.

Prozkoumejme úsek programu. Záměrem je, aby puls, kdy je GPIO16 na vysoké úrovni, trval přesně \(5 \mu s\).

gpio_put( 16, 1);
for( i = 0; i < n; i++) {
    // tady se něco dělá
}
sleep_us(5);
gpio_put( 16, 0);

Jelikož nevíme jak dlouho bude trvat smyčka for, což závisí jednak na počtu cyklů n a ještě na trvání kódu uvnitř smyčky, tak to uděláme trochu jinak.

Místo funkce sleep_ms(), která trvá vždy stejný čas, použijeme tyto funkce:

sleep_until( absolute_time_t target )
busy_wait_until( absolute_time_t t )

Tyto funkce pozastaví provádění programu dokud systémový čas nedosáhne zadaného času. Ještě potřebujeme funkci, kterou zjistíme aktuální systémový čas:

uint64_t time_us_64( void )

Tato funkce vrací 64 bitový timestamp (čas) v mikrosekundách. (Nulu by vrátila tato funkce jenom okamžitě po startu Pica.)

Můžeme náš přechozí úsek programu přepsat takto:

1
2
3
4
5
6
uint64_t t;
gpio_put( 16, 1 );
t = time_us_64() + 5;               (1)
for( i = 0; i < n; i++ ) {};
sleep_until( (absolute_time_t) {t} ); (2)
gpio_put( 16, 0 );
1 Získáme timstamp a přičteme k němu \(5 \mu s\).
2 sleep_until pozastaví program tak, že od začátku řádku 4 do konce řádku 5 uběhne přesně \( 5 \mu s\), bez ohledu na to, jak dlouho trvá cyklus for. (Je jasné, že cyklus for nesmí trvat více než \(5 \mu s\), jinak celá konstrukce ztrácí smysl.). Funkce sleep_until požaduje strukturu absolute_time_t, tímto způsobem: (absolute_time_t) {t} zkonverujeme číslo uint64_t na strukturu.
types.h — typ absolute_time_t
typedef struct {
     uint64_t _private_us_since_boot;
 } absolute_time_t;

Synchronizace pulsů

Napišme jednoduchý program, který bude dělat pulsy nahoru a dolů, které chceme mít sfázované.

pulses_outfase.c
#include "pico/stdlib.h"

int main( void )
{
    gpio_set_function(22, GPIO_FUNC_SIO);
    gpio_set_dir(22, true);
    gpio_set_function(21, GPIO_FUNC_SIO);
    gpio_set_dir(21, true);
    while (true) {
        gpio_put(22, 0);
        gpio_put(21, 1);
        gpio_put(22, 1);
        gpio_put(21, 0);
    }
}

V programu nejsou vložena žádná zpoždění, tak že pulsy jedou nejvyšší možnou rychlostí. Logický analyzátor nám odhalí, že výsledek není takový, jak bychom očekávali.

Nesfázované pulsy

Screenshot 20240916 203055 xyz.fhdm.scoppy

Lepší program pro generování sfázovaných pulsů používá maskovací funkce. Upravíme trochu předchozí program:

pulses_infase.c
#include "pico/stdlib.h"

int main( void )
{
    gpio_set_function(22, GPIO_FUNC_SIO);
    gpio_set_dir(22, true);
    gpio_set_function(21, GPIO_FUNC_SIO);
    gpio_set_dir(21, true);
    uint32_t mask = (1 << 22) | (1 << 21);
    uint32_t value1 = 1 << 21;
    uint32_t value2 = 1 << 22;
    while (true) {
        gpio_put_masked(mask, value1);
        gpio_put_masked(mask, value2);
    }
}
Sfázované pulsy

Screenshot 20240916 204524 xyz.fhdm.scoppy

Je vidět, že pulsy jsou krásně sfázované.

Logický analyzátor je nezbytný pomocník, pokud se chceme vážně zabývat programováním Pica. Zde je ukázána stavba docela pěkného a levného osciloskopu a logického analyzátoru pomocí Pica, drátků a Android telefonu.

Přenastavení

Máme k dispozici tři funkce, kterými můžeme měnit způsob práce výstupních GPIO linek. Přenastavují normální funkci, proto se tak jmenují:

gpio_set_outover( uint gpio, uint value ) (1)
gpio_set inover( uint gpio, uint value ) (2)
gpio_set_oeover( uint gpio, uint value ) (3)
1 Přenastavuje výstupní chování.
2 Přenastavuje vstupní chování.
3 Přenastavuje, zda pin funguje nebo ne.

Kde hodnota value může nabývat:

  • GPIO_OVERRIDE_NORMAL

  • GPIO_OVERRIDE_INVERT — změní chování pinů, úrovně se změní místo vysoké bude nízká a místo nízké vysoká.

  • GPIO_OVERRIDE_LOW — vypne výstupní pin

  • GPIO_OVERRIDE_HIGH — zapne výstupní pin

Například:

gpio_set_outover( 22, GPIO_OVERRIDE_INVERT ); (1)
1 Nastaví pin 22 tak, že bude pracovat obráceně. Pokud zavoláme gpio_put( 22, 1);, pin 22 bude na nízké úrovni a pokud zavoláme gpio_put( 22 , 0 ); pin 22 bude na vysoké úrovní.

Zdroje a odkazy