Když budete programovat více vstupů a sledovat více věcí najednou, zpracování vstupu bude pro vás docela výzva. Složité věci můžete řídit pomocí konečných automatů a podobných algoritmů, ale dříve nebo později narazíte na problém, že musíte reagovat na vstup, ale váš program na to není zrovna připraven. Události (events) a přerušení (interrupts) jsou dvě vzájemně svázané věci, které mohou pomoci řešit tento problém. Pico SDK neumí ve skutečnosti zpracovávat události, raději používá přerušení, ale hardware Pica je připravené na zpracování událostí a není těžké naprogramovat software, které to bude dělat. Podíváme se na nejprve na události a potom na přerušení.

Události

Událost je jako západka co zaklapne nebo paměť o tom, že se něco již stalo. Představte si, že existuje příznak (flag), který bude automaticky nastaven na 1, jestliže vstupní GPIO linka změní svůj stav. Tento příznak se ale nenastavuje softwarem nebo alespoň pomocí softwaru, nad kterým máte kontrolu. Můžeme si to představit jako hardwarové nastavení i když tomu vždy tak není.

Pomocí událostí se můžeme vyhnout tomu, že zmeškáme nějaké čtení vstupu, když naše čtecí smyčka, která čte vstup je zaneprázdněna něčím jiným. Čtecí smyčka (polling loop) bude raději číst příznaky než současný stav vstupní GPIO linky a má tak možnost detekovat, zda se něco změnilo od předchozího čtení. Čtecí smyčka potom vynuluje příslušné příznaky a bude se zabývat zpracováním události, která nastala. Pochopitelně nemůžeme zcela přesně vědět, kdy která událost nastala, ale zato nepropustíme žádnou událost, která nastala.

Jsou přerušení škodlivá?

Přerušení (IRQ) jsou často zaměňovány s událostmi, ale je to něco úplně jiného. Přerušení je hardwarový mechanismus, který přerušuje počítač ať už dělá cokoliv a přenáší práci jeho programu do funkce, které se říká obsluha přerušení (interrupt handler). Přerušení si můžete představit jako událost, na kterou se reaguje okamžitě.

Pokud budeme používat mechanismus přerušení, okolní svět bude rozhodovat o tom, kdy má počítač reagovat na vstup a nepotřebuje tudíž čtecí smyčku. Mnozí hardwaroví inženýři mají za to, že přerušení jsou receptem na všechno a čtecí smyčky sjou neelegantní řešení a mají být používany jenom tehdy, když nechceme používat přerušení. Ale to je docela daleko od reality. Panuje všeobecné mínění, že programování v reálném čase a přerušení jsou "in" a pokud nepoužíváte přerušení, tak určitě děláte něco špatně. Pravda je ovšem jinde, právě pokud používáte přerušení, tak to děláte špatně. Je to až takové, že některé organizace jsou přesvědčeny, že používání přerušení je nebezpečné, takže je všeobecně zakazují přerušení používat.

Přerušení jsou užitečné jenom tehdy, když zpracováváte podmínky s nízkou frekvencí a musíte na ně reagovat s vysokou prioritou, t.j. velmi rychle. Potom mohou přerušení zjednodušit váš program, ale zřídka kdy používání přerušení věci zrychlí vcelku, protože zátěž spojená se zpracováním přerušení je obvykle docela veliká.

Máte-li čtecí smyčku, které trvá 100 ms, aby přečetla všechny události a máte-li vstup, který vyžaduje pozornost každých 60 ms, potom zcela jasně čtecí smyčka není dost rychlá. Přerušení dovoluje událostem s vysokou prioritou přerušit smyčku a tyto vysoce prioritní události budou zpracovány pod 100 ms. Avšak, jestliže se události s vysokou prioritou budou vyskytovat příliš často, pak čtecí smyčka nebude pracovat tak, jak se očekává. Dá se to vylepšit třeba tak, že čtecí smyčka bude testovat vstup dvakrát za celý cyklus.

Příklad z reálného světa. Předpokládejme, že máme reagovat na tlačítko domovního zvonku. Můžete napsat čtecí smyčku, která bude jednoduše opakovaně hlídat stav tlačítka donekonečna nebo můžete napsat obsluhu přerušení (interrupt service routine ISR), která bude odpovídat na tlačítko zvonku. Procesor se nebude muset neustále zabývat stavem tlačítka a bude moct dělat jiné věci dokud někdo tlačítko nestiskne. Potom procesot pozastaví to, co právě dělal, a přenese pozornost do obsluhy přerušení (ISR).

Jak dobrý je tento design záleží na tom, jak často bude zvonkové tlačítko interagovat s programem a kolik stlačení očekáváme. Zpracování obsluhy přerušení trvá nějaký čas než se dokončí — co se stane, když budeme mít další tlačítko a to bude stisknuto, zatímco je zpracováváno přerušení od prvního tlačítka? Některé procesory poskytují něco, čemu se říká fronta přerušení, což ale nepomáhá v tom, že můžeme zracovávat v čase pouze jedno přerušení. To samé platí pročtecí smyčku, ale pokud neumíme obsloužit proud událostí pomocí čtecí smyčky, tak to nebudeme umět ani pomocí zpracování přerušení, protože přerušení přidává další čas na přenos řízení do ISR a zpátky.

Než definitivně zavrhneme myšlenku, že procesor nebude dělat nic jiného než opakovaně poptávat "je zvonkové tlačítko stisknuto", uvažujme, co máme ještě k dispozcici za řešení. Je-li odpověď "nic dalšího", potom může být čtecí smyčka nejjednodušší volbou. Také, pokud má procesor více jader, nejrychlejší cestou, jak se vypořádat s množstvím vstupních událostí je použít jedno jádro na rychlou čtecí smyčku. To může být považováno za softwarovou emulaci hardwarových přerušení. Nezaměňujte to se softwarovými přerušeními, což jsou hardwarová přerušení vyvolaná softwarem.

Chystáte-li se používat přerušení k obsloužení vstupu, potom dobrý návrh je použít obsluhu přerušení k plnění fronty událostí. Tam je šance, že žádný vstup nepropustíte.

Přes svoji atraktivnost, přerušení jsou obvykle špatnou volbou při čemkoliv jiném, než co má nízkou frekvenci a je potřeba to zpracovat rychle.

Hardwarové události

Pico SDK nepodporuje události, ale hardwarová část ano v implementaci GPIO přerušení. Každá GPIO linka zaznamenává čtyři události:

  • GPIO_IRQ_LEVEL_LOW (nízká úroveň)

  • GPIO_IRQ_LEVEL_HIGH (vysoká úroveň)

  • GPIO_IRQ_EDGE_FALL (sestupná hrana)

  • GPIO_IRQ_EDGE_RISE (náběžná hrana)

Události GPIO_IRQ_LEVEL_LOW a GPIO_IRQ_LEVEL_HIGH nejsou nijak zaznamenány, jednoduše odpovídají současnému stavu GPIO linky. Události GPIO_IRQ_EDGE_FALL a GPIO_IRQ_EDGE_RISE zůstávají zaznamenány, doku je nevynulujem. V praxi jsou události náběžná a setupná hrana daleko užitečnější.

Každá z těchto událostí může být nastavena, aby vyvolala přerušení a na to se podíváme trochu později. Zatím, vše, co se týká událostí považujme za "zápis" že se něco stalo.

SDK nemá dosud žádnou funkci pro zpracování událostí, je však docela lehké naprogramovat dvě funkce, které tuto práci budou dělat:

Funkce pro práci s událostmi
uint32_t gpio_get_events(uint gpio)
{
    int32_t mask = 0xF << 4 * (gpio % 8);
    return (io_bank0_hw->intr[gpio / 8] & mask) >> 4 * ( gpio % 8);
}

void gpio_clear_events(uint gpio, uint32_t events)
{
    gpio_acknowledge_irq(gpio, events);
}

Jak to pracuje je popsáno dále (Pico — přímo k hardware), teď potřebujete vědět, že gpio_get_events vrací čtyři bity, které odpovídají čtyřem událostem na GPIO lince. Podobně gpio_clear_events se používá k výmazu zaznamenaných událostí na GPIO. Zaznamenané události odpovídají konstantám:
GPIO_IRQ_EDGE_FALL (náběžná hrana)
GPIO_IRQ_EDGE_RISE (sestupná hrana)

Například kousek kódu:

int32_t event = gpio_get_events(22);
if(event & GPIO_IRQ_EDGE_RISE)

testuje, zda se na GPIO22 objevila náběžná hrana.

Dáme-li všechny věci dohromady, musíme udělat tři kroky, abychom mohli používat v programu události:

  1. Nastavíme GPIO linku jako vstupní
    gpio_set_function( 22, GPIO_FUNC_SIO);
    gpio_set_dir( 22, false); nebo to samé čitelněji gpio_set_dir( 22, GPIO_IN );

  2. Vynulujeme událost
    gpio_clear_events( 22, GPIO_IRQ_EDGE_RISE);

  3. Poté můžeme dělat jiné věci a eventuelně číst stavový bit, abychom zjistili, zda událost nastala v daném časovém intervalu, poté musíme událost vynulovat, abychom mohli zaznamenat další:
    int32_t event = gpio_get_events( 22 );
    gpio_clear_events(22, GPIO_IRQ_EDGE_RISE);

Nervózní tlačítko

Abychom pochopili rozdíl mezi čtením stavu linky a změnou jejího stavu stavu a použitím událostí, uvažujme jinou verzi programu s tlačítkem, který jsme již programovali. V této verzi je GPIO linka nastavena na vstup a při stisknutí tlačítka je zaslána zpráva o stlačení tlačítka. Potom program čeká 20 sekund a následně testuje stav linky. I když uživatel stiskne tlačítko několikrát během dvacetisekundové pauzy výsledek bude, že přečtený stav linky bude takový, jaký je přesně po uplynutí sleep_ms(20000) (20 sekund):

Hloupý program
#include <stdio.h>
#include "pico/stdlib.h"

int main( void )
{
    stdio_init_all();
    gpio_set_function(22, GPIO_FUNC_SIO);
    gpio_set_dir(22, GPIO_IN);
    gpio_pull_down(22);

    printf("Stiskněte tlačítko\n");
    sleep_ms(20000);
    if( gpio_get(22) ) {
        printf("Tlačítko stisknuto.\n");
    }
    else {
        printf("Tlačítko není stisknuto.\n");
    }
}

jinými slovy, tento program propustí libovolné stisknutí během 20 sekundové pauzy. Poměrně hloupý program, porovnáme ho s verzí, používající události. (Zachytí pouze stisk tlačítka, které začne někdy během sleep_ms(20000) a skončí po volání funkce gpio_get(22). )

Chytrý program
#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/structs/iobank0.h"

uint32_t gpio_get_events(uint gpio)
{
    int32_t mask = 0xF << 4 * ( gpio % 8 );
return (io_bank0_hw->intr[gpio / 8] & mask) >> 4 * ( gpio % 8 );
}

void gpio_clear_events(uint gpio, uint32_t events)
{
    gpio_acknowledge_irq(gpio, events);
}

int main( void )
{
    stdio_init_all();
    gpio_set_function( 22, GPIO_FUNC_SIO );
    gpio_set_dir( 22, GPIO_IN );
    gpio_pull_down( 22 );

    printf("Stiskněte tlačítko.\n");
    gpio_clear_events( 22, GPIO_IRQ_EDGE_RISE );
    sleep_ms( 20000 );
    int32_t event = gpio_get_events( 22 );
    gpio_clear_events( 22, GPIO_IRQ_EDGE_RISE );
    if( event & GPIO_IRQ_EDGE_RISE ) {
        print("Tlačítko stisknuto.\n");
    }
    else {
        printf("Tlacitko nebylo stisknuto.\n");
    }
}

V tomto programu je GPIO linka nastavena jako vstupní s pull-down rezistorem a testujeme raději náběžnou hranu než stav linky. Rozdíl je v tom, že jestliže uživatel stiskne tlačítko kdykoliv během dvacetisekundové pauze, flag je nastaven a program registruje stisknutí tlačítka. Flag je nastaven bez ohledu na to, co program zrovna dělá, takže místo prostého spaní může dělat něco jiného a být si jistý tím, že nepropustí stisknutí tlačítka. Avšak neví, kdy přesně bylo tlačítko stisknuto.

Události dovolují nepropásnout jeden vstup, když čteme ve smyčce, ale neumožňují obsluhovat více vstupních událostí. Když uživatel stiskne tlačítko několikrát, tak bude detekováno jenom jedno stisknutí. Můžete implementovat plné zpracování událostí pomocí fronty, ale to nebude stát za to. Lepší variantou je použít schopnosti GPIO linky generovat přerušení, bude to za chvíli.

Pokud chceme, aby byly potlačeny zákmity tlačítka, tak můžeme použít jednoduchý obvod, kterým budou zákmity tlačítka odfiltrovány. Příklad je pro tlačítko zapojené na GPIO8, bude to fungovat na libovolném pinu.

Ošetření zákmitů tlačítka v režimu pull up

tlacitko low pass filter pull up

Ošetření zákmitů tlačítka v režimu pull down

tlacitko low pass filter pull down

Měření pulsů pomocí událostí

Máme hotové všechny funkce, které potřebujeme, abychom se mohli pustit do měření pulsů za pomoci událostí. Budeme měřit šířku každého pulsu jako časovou vzdálenost mezi náběžnou a sestupnou hranou pulsu a mezi sestupnou a náběžnou hranou pulsu. Hlavní rozdíl mezi tímto programem a předchozím je v tom, že měření šířky je uskutečňováno hardwarem, čili hardware detekuje přechodové stavy neboli hrany signálu.

Měření délky pulsů pomocí událostí  — udalost.c
#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/structs/iobank0.h"

uint32_t gpio_get_events(uint gpio)
{
    int32_t mask = 0xF << 4 * ( gpio % 8 );
return (io_bank0_hw->intr[gpio / 8] & mask) >> 4 * ( gpio % 8 );
}

void gpio_clear_events(uint gpio, uint32_t events)
{
    gpio_acknowledge_irq(gpio, events);
}

int main( void )
{
uint64_t t;
    stdio_init_all();
    gpio_set_function( 22, GPIO_FUNC_SIO );
    gpio_set_dir( 22, GPIO_IN );
    gpio_pull_down( 22 );

    while( true ) {
        gpio_clear_events( 22, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL );
        while( !(gpio_get_events(22) & GPIO_IRQ_EDGE_RISE)) { };
        t = time_us_64();
        gpio_clear_events( 22, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL );
        while( !(gpio_get_events( 22 ) & GPIO_IRQ_EDGE_FALL )) { };
        t = (uint64_t)( time_us_64() - t);
        printf("%llu\n", t);
        sleep_ms(1000);
    }
}

Po vynulování obou událostí čekáme na náběžnou hranu, vynulujeme ji a čekáme na sestupnou hranu a počítáme rozdíl času mezi oběma událostmi.

Tento program dává velmi podobné výsledky, jako předchozí program, který jednoduše četl vstup na GPIO lince. V této verzi nemáme žádnou výhodu z toho že čteme události před čtecí smyčkou předchozího programu, protože čteme data stejně rychle. Avšak jakmile budeme mít za úkol sledovat více GPIO linek a tak budeme muset testovat více podmínek, můžeme přečíst najednou všechny události co potřebujeme, a pak snadno vidět, co se všechno stalo.

CMakeLists.txt pro měření událostí
# cmake version
cmake_minimum_required(VERSION 3.13)

# include the sdk.cmake file
include($ENV{PICO_SDK_PATH}/external/pico_sdk_import.cmake)

# give the project a name (anything you want)
project(udalost C CXX ASM)

# initialize the sdk
pico_sdk_init()

set( executable "udalost" )
add_executable( ${executable} )


target_sources(${executable} PRIVATE udalost.c)

target_link_libraries(${executable} PRIVATE pico_stdlib hardware_gpio )

# enable usb output, disable uart output
pico_enable_stdio_usb( ${executable} 1)
pico_enable_stdio_uart( ${executable} 0)
pico_add_extra_outputs(${executable})
Měření délky pulsů pomocí událostí můžeme proužít třeba k rozchození ultrazvukového čidla HC-SR04.

Přerušení

Pico má 32 různých přerušení, ale používá se jich jenom 26. Všechny uživatelské GPIO linky fungují společně a vytvářejí jedno IO přerušení. Detail je v tom, že každé z jader procesoru RP2040 může odpovídat v ten samý čas na IO přerušení způsobené jinou GPIO linkou, to znamená že IO přerušení nejsou sdílena mezi jádry. Ve všech ostatních případech přerušení může být aktivováno pouze na jednom jádru.

Jelikož všechny GPIO linky generují jedno přerušení, je na obsluze přerušení (interrupt handler), aby rozlišila, která linka toto přerušení spustila a posléze jej vynulovala. Která GPIO linka spustila přerušení je zapsáno v tom samém bitu registru, který jsme používali jako indikátor události v předchozí části (iobank_hw). Je-li přerušení nastaveno pro danou linku a danou událost, potom přerušení se objeví, je-li bit nastaven na 1.

Události, které můžeme použít jsou stejné jako předtím:

  • GPIO_IRQ_LEVEL_LOW

  • GPIO_IRQ_LEVEL_HIGH

  • GPIO_IRQ_EDGE_FALL

  • GPIO_IRQ_EDGE_RISE

SDK poskytuje tři základní funkce pro práci s GPIO přerušeními. Nejdůležitejší je:

gpio_set_irq_enabled_with_callback( uint gpio, uint32_t events, bool enabled, gpio_irq_callback_t callback )

Tato funkce nastavuje nebo ruší přerušení spřažené s danou GPIO linkou a danou událostí. Funkce callback je zavolána, pokud se objeví přerušení, čili je to obsluha přerušení. Přestože to vypadá, jako by bylo možné spřáhnout různou obsluhu přerušení s každou z GPIO linek, v době psaní tohoto článku tomu tak není. Komentáře uvnitř SDK sice naznačují, že možná někdy v budoucnosti bude obsluha přerušení pro všechny GPIO linky, tak zatím nikoliv. Jestliže se pokusíte nastavit několik obsluh přerušení pro několik různých GPIO linek, tak bude použita jenom poslední z nich. Což znamená, že je na vaší obsluze přerušení, aby zjistila, na které z GPIO linek nastala událost, která způsobila přerušení. To se dá zařídit dvěma parametry, předanými do obsluhy přerušení:

void gpio_callback( uint gpio, uint32_t events )

První parametr gpio udává GPIO linku, která způsobila přerušení a druhý parametr events udává událost, která nastala.

Jakmile máme nastavené přerušení, tak ho můžeme začít používat:

gpio_set_irq_enabled( uint gpio, uint32_t events, bool enabled )

Tato funkce zapíná nebo vypíná schopnost GPIO linky způsobit přerušení, když se objeví zadaná událost.

Nejčasteji se používají přerušení typu GPIO_IRQ_EDGE_…​ a jakmile je zaznamenáno musíte v obsluze přerušení potvrdit zpracování přerušení pomocí:

void gpio_acknowledge_irq( uint gpio, uint32_t events )

Je k dispozici také funkce gpio_clear_events, kterou můžete vynulovat událost na dané GPIO lince.

A to je všechno, co potřebujeme k vytváření a zpracování přerušení. V praxi jsou věci trochu komplikovanější než očekáváte, aby byly udělány správně. Je otázka, co se stane, jestliže se přerušení objeví, když jste zrovna v obsluze přerušení a co se stane, když zrovna během přerušení přistupujete k nějakému sdílenému zdroji (nějaké proměnné)?

První jednoduchý příklad:

#include <stdio.h>
#include <pico/stdlib.h>

static uint64_t t; // čas

void MyIRQHandler( uint gpio, uint32_t events )
{
    t = time_us_64() - t;
    printf( "GPIO %d %X %d \n", gpio, events, t );
}

int main()
{
    stdio_init_all();
    gpio_set_function( 22, GPIO_FUNC_SIO );
    gpio_set_dir( 22, GPIO_IN );
    gpio_pull_down( 22 );

    gpio_set_irq_enabled_with_callback( 22, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, true, &MyIRQHandler );

    while(1) {}
return 0;
}

Tento program nastaví obsluhu přerušení, která vytiskne číslo GPIO linky, událost a čas od předchozího přerušení. Jestliže se budou události objevovat dostatečně pomalu, tak uvidíme každou událost jako příčinu přerušení, ale jestliže se budou objevovat rychleji, než je funkce print stihne vytisknout, uvidíme obě hrany současně. Pouze jedna událost způsobí přerušení, další jednoduše bude zaznamenána zatímco poběží obsluha přerušení.

Pokud budete experimentovat s programem, objevíte, že přerušení budou nulována, takže nemusíte je nulovat nakonci rutiny obsluhy přerušení. Dále nebude vyvoláno přerušení, zatímco je program ve funkci obsluhy tohoto přerušení. Částečně je to dáno tím, že SDK nedoručuje přerušení napřímo. SDK si nastaví svoji obsluhu přerušení, která udělá úklid a potom zavolá vaši rutinu obsluhy přerušení:

Obsluha přerušení z SDK
static void gpio_irq_handler( void )
{
    io_irq_ctrl_hw_t *irq_ctrl_base = get_core_num() ? iobank0_hw->proc1_irq_ctrl : &iobank0_hw->proc0_irq_ctrl;
    for( uint gpio = 0; gpio < NUM_BANK0_GPIOS; gpio++ )
    {
        io_rw_32 *status_reg = &irq_ctrl_base->ints[gpio/8];
        uint events = (*status_reg >> 4 * (gpio % 8)) & 0xf;

        if( events ) {
            // TODO: If both cores case about this event then
            // the second core wont't get the irq?
            gpio_acknowledge_irq( gpio, events );
            gpio_irq_calback_t calback = _callbacks[get_core_num()];

            if( callback ) {
                callback( gpio, events ); (1)
            }
        }
    }
}
1 Tady se volá naše obsluha přerušení MyIRQHandler.

Nejzajímavější část této funkce je smyčka for, která testuje u každé GPIO linky stavový registr IRQ, aby zjistila, která linka způsobila přerušení. Nejdříve resetuje přerušení a potom volá naši obsluhu přerušení s argumenty gpio a event. To znamená, že nemusíte potvrdit obsluhu přerušení ve vaší vlastní obsluze přerušení, ale poznamenejme, že prohledávání GPIO registrů ve smyčce trvá nějaký čas a vaše obsluha přerušení dostane GPIO linku, která přerušení způsobila. To se ale v budoucnu může změnit.

Jak rychlé je přerušení?

Zkusme zjistit jaké je zatížení v souvislosti s používáním přerušení při opakovaném měření. Tentokrát nebudeme jednoduše tisknout výsledky, protože by nám to mohlo zastavit zpracování přerušení. Kompromisně uložíme 20 hodnot načtených z obsluhy přerušení do pole, a až bude pole plné, tak ho vytiskneme. Je také důležité mít funkci obsluhy přerušení co nejkratší, protože jak dlouho trvá určuje, jak rychle bude moct být následující přerušení obslouženo.

Nová obsluha přerušení
uint64_t t[20]
int count=0;

void myHandler( int fd )
{
    struct gpioevent_data edata;
    read(fd, &edata, sizeof( edata ));
    t[count++] = edata.timestamp;
    if( count < 19 ) {
        for( int i=1; i<19; i++ ) {
            printf("%llu \n\r", t[i]-t[i-1]);
            fflush(stdout);
        }
        count = 0;
    }
}

Tato vylepšená obsluha přerušení zaznamenává poměrně přesně časy pulsů pro pulsy delší než \(6 \mu s\). Je to o něco horší, než čtecí smyčka, která pracuje lépe, až do \(1 \mu s\), ale rozdíl není velký. To znamená, že i s zatížením SDK, přerušení jsou poměrně užitečná záležitost. Hodně však záleží na době, kterou obsluha přerušení potřebuje k tomu, aby se vzdala řízení.

Závodění a hladovění

Jedním z velkých problémů při používání přerušení na malých procesorech jako RP2040 je, že zde není žádný operační systém, který by zajišťoval férový přístup k procesoru pro různé běžící úlohy. Je zcela možné, že pokud je rutina obsluhy přerušení volána často, hlavní program nebo jiné funkce nedostanou šanci běžet.

Například jestliže se pokusíte aby program tisknul zprávy ve funkci main a současně odpovídal na přerušení, tak můžete pozorovat hladovění (starvation) funkce main velmi lehce při 8 KHz vstupu:

Hladovění funkce main
#include <stdio.h>
#include "pico/stdlib.h"

uint64_t t;

void MyIRQHandler( uint gpio, uint32_t events )
{
    t = time_us_64();
    printf("GPIO %d %X %d \n", gpio, events, t);
}

int main()
{
    stdio_init_all();
    gpio_set_function( 22, GPIO_FUNC_SIO );
    gpio_set_dir( 22, false );
    gpio_pull_down( 22 );

    gpio_set_irq_enabled_with_callback( 22, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, true, &MyIRQHandler );

    while( 1 )
    {
        printf("delam cosi\n\r");
    };
return 0;
}

Pokud přivedeme na vstup GPIO 22 obdélníkovou vlnu, tak můžeme vidět na výpisu (třeba v minicomu) obě hlášení "delam cosi" a zprávu z obsluhy přerušení až do 400 Hz celkem pěkně. Nad frekvencí 4 kHz je rutina přerušení (na 8 kKz je to očividné) volána téměř okamžite znovu jak se vrátí do hlavního programu. Výsledkem je, že hlavní program (funkce main) neukazuje téměř nic — nedostane se k procesoru, je hladový pro procesorový čas. Výsledky měření si můžete prohlédnout zde. Důvodem po celkem nízkou rychlost provádějí obsluhy přerušení je to, že funkce printf je hodně pomalá. Rychlejší obsluha přerušení bude pracovat při vyšších frekvencích, než se objeví hladovění, ale je to stálý program všech systémů řízených pomocí přerušení.

Pokud se podíváte blíže na výstup programu, když má hlavní program šanci běžet, uvidíte druhý problém, kterým trpí jednoduchý systém řízený pomocí přerušení — je to závodění (race conditions).

Výstup není čistý, je promíchán mezi sebou.

Race conditions
delam cosi
GPIO 22 8 16930496
delam cosi
delam cosiGPIO 22 8 16930747 (1)

GPIO 22 4 16930874
delam cosi
GPIO 22 8 16930994
1 Zde se funkce main a obsluha přerušení perou mezi sebou o výstup programu.

Důvod je prostý, přerušení se může objevit zatímco program je někde uprostřed volání funkce printf. To znamená, že funkce printf není "atomická" (nedělitelná) a může být rozdělena příchodem přerušení. Klíčové je, že k přerušením může dojít kdekoli ve vašem programu, dokonce i v rámci akcí, které považujete za atomické, tj. které nelze přerušit.

Co je ještě více matoucí, že často při pokusu odstranic problém závodění, docházíme k hladovění. Například, můžete se domnívat, že řešení je zakázat přerušení pěhem tisku:

while( 1 )
{
    gpio_set_irq_enabled( 22, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, false ); (1)
    printf("Delam cosi...\n\r");
    gpio_set_irq_enabled( 22, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, true ); (2)
};
1 Zakázání přerušení.
2 Povolení přerušení.

Když program skusíte spustit, tak zjistíte, že vůbec nevidíte výpisy funkce printf v rutině přerušení. Důvodem je, že čas, po který je povoleno přerušení je velmi krátký a jenom přerušení, které se trefí do tohoto krátkého okamžiku uvidíte, že něco dělá. Nyní je to rutina obsluhy přerušení, která je hladová pro procesoru. Jestliže přidáte do smyčky while třeba:
sleep_ms(1000); na konec smyčky, tak uvidíte oba výpisy a nebudou se překrývat.

V tomto případě je ovšem problém mnohem horší, protože funkce gpio_set_irq_enabled vždycky vynuluje všechny nezpracovaná přerušení. Pokud se přerušení objeví během funkce printf, tak není vůbec zaznamenáno a je resetováno, když je přerušení opět povoleno.

Pokud potřebujete zakázat přerušení a přitom si pamatovat přerušení, která během zákazu nastala, potřebujete přidat do programu:

#include "hardware/structs/iobank0.h"
#include "hardware/sync.h"

void gpio_set_irq_active( uint gpio, uint32_t events, bool enabled )
{
    io_irq_ctrl_hw_t *irq_ctrl_base = get_core_num() ?
                                        &iobank0_hw->proc1_irq_ctrl : &iobank0_hw->proc0_irq_ctrl;
    io_rw_32 *en_reg = &irq_ctrl_base->inte[ gpio / 8 ];
    events <<= 4 * (gpio % 8);
    if(enabled)
        hw_set_bits(en_reg, events);
    else
        hw_clear_bits(en_reg, events);
}

Dále je potřeba změnit ve smyčce while hlavního programu:

while( 1 )
{
    gpio_set_irq_active( 22, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, false );
    printf("Delam cosi ...\n\r");
    gpio_set_irq_active( 22, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, true );
}

Nyní uvidíte obě zprávy (jak z main tak z obsluhy přerušení) bez pauzy. Řešení není perfektní, protože některá přerušení mohou být stále propuštěna, ale v mnoha případech je to mnohem lepší řešení, které funguje.

Pokud se setkáte s problémy tohoto ražení, můžete přemýšlet o tradičních řešeních jako jsou mutexy, semafory a kritické sekce. Tyto všechny jsou dostupné v SDK, ale bez vláken jsou skutečně nezbytné, když budete používat druhé jádro procesoru. Ve většině případů si vystačíte s pozastavením přerušení.

Odpovídání na vstup

Pohled na metody, které nám umožňují se zabývat vstupem není vyčerpávající — vždycky existují nové způsoby jak to udělat, ale to pokrývá ve všeobecnosti jak implementovat vstupy. Jak už bylo řečeno, problém týkajících se vstupu je v tom, že nevíte, kdy se stane. Co je všeobecne důležité, je rychlost odezvy.

Pro vstupy s nízkou frekvencí, řešení pomocí přerušení stojí za to. Umožňuje to osvobodit váš program zabývání se jinými věcmi a zjednodušuje strukturu vašeho programu.

Pro vstupy s vyšší frekvencí, které musí být pravidelně obsluhovány je čtecí smyčka stále nejlepší metodou, jak získat co nejlepší propustnost. Jak rychle můžete odpovědět na vstup závisí na tom, jak dlouhá je čtecí smyčka a kolikrát během ní testujete.

Zdroje a odkazy