PIO je zkratka z programovatelného vstupně výstupního zařízení (Programmable I/O). PIO je v podstatě velmi rychlý koprocesor se svými vlastními registry, pamětí, DMA a dokonce s vlastním programovacím jazykem (PIO Assembler). PIO pracuje nezávisle na hlavním procesoru. Kompletní popis funkce PIO s programátorskými příklady je popsán v 3. kapitole C SDK book.

Úvod

Na většině mikrokontrolérů používáte „bit-bang“, což znamená použití hlavního procesorového jádra k přímému zapínání a vypínání pinů. To může fungovat, ale může to způsobit problémy s načasováním (zejména pokud používáte přerušení) a může to vyžadovat spoustu prostředků na zpracování, které můžete potřebovat pro jiné věci. Pico má v rukávu malý trik: Programmable I/O (PIO). Kromě dvou hlavních procesorových jader Cortex-M0+ existují dva bloky PIO, z nichž každý má čtyři stavové stroje. Jedná se o skutečně okleštěná procesní jádra, která lze použít ke zpracování dat přicházejících do mikrokontroléru nebo z něj a odlehčit některé požadavky na zpracování pro implementaci komunikačních protokolů.

Interní schéma procesoru RP2040

RP2040 overview

Pro každý jednoduchý procesor (nazývaný stavový stroj) existují dvě struktury First-In-First-Out (FIFO): jedna pro data přicházející a jedna pro data odcházející. Ty se používají k propojení stavového automatu se vším ostatním. V podstatě se jedná pouze o typ paměti, kde jsou data načítána ve stejném pořadí, v jakém jsou zapsána – z tohoto důvodu se jim často říká fronty, protože fungují stejným způsobem jako fronta lidí. První část dat ve frontě (nebo první osoba ve frontě) je první, která se dostane na druhý konec. Když potřebujete odeslat data přes stavový automat PIO, vložíte je do FIFO, a když je váš stavový automat připraven na další část dat, stáhne je. Tímto způsobem váš stavový stroj PIO a program běžící na hlavním jádru nemusí být dokonale synchronizované. FIFO jsou pouze čtyři slova (32 bitů) dlouhá, ale můžete je propojit s přímým přístupem do paměti (DMA) a odesílat větší množství dat do stavového automatu PIO, aniž byste je museli neustále zapisovat z hlavního programu. FIFO se spojují se stavovým automatem PIO přes vstupní posuvný registr a výstupní posuvný registr. Existují také dva stírací registry zvané X a Y. Ty slouží k ukládání dočasných dat.

Schéma PIO

pio internal

  • Máme dva identické PIO bloky

  • Každý blok má 4 stavové stroje (state machines)

  • Stavový stroj je programovatelný v asembleru

  • Každý stavový stroj může nezávisle na jiném vykonávat sekvenční program k manipulaci s GPIO a přenosu dat

  • Používá se hlavně pro vstupně výstupní operace, je deterministický s přesným časováním.

  • Se systémem komunikuje pomocí FIFO (které mohou být připojeny k DMA kanálu) a přerušení (interrupt)

Stavový stroj

PIO prog model

  • Dva 32 bitové posuvné registry, které se mohou posouvat doleva nebo doprava o libovolný počet bitů (1 až 31)

  • Dva 32 bitové "scratch" registry

  • Dvě fronty (FIFO) o rozměrech 4x32 bitů v každém směru (TX,RX), které mohou být rekonfigurovány na jednu frontu 8x32 bitů v jednom směru.

  • Děličku hodin (clock divider)

  • Flexibilní mapování GPIO

  • DMA (direct memory access) rozhraní

  • Přerušení (IRQ) s flagy set, clear, status

  • Všechny stavové stroje mají k dispozici paměť, která může odsahovat až 32 instrukcí (instrukce má 16 bitů)

K programování stavového stroje (state machine) máme k dispozici devět instrukcí:

  • in přesune 1–32 bitů najednou do vstupního posuvného registru odněkud (jako je sada kolíků nebo škrábací registr)

  • out přesune 1–32 bitů z výstupního posuvného registru někam

  • push odesílá data do RX FIFO

  • pull získává data z TX FIFO

  • mov přesouvá data ze zdroje do cíle

  • irq nastavuje nebo ruší příznak přerušení

  • set zapíše data do cíle

  • wait pozastaví program, dokud se nestane konkrétní akce

  • jmp skok, který se přesune na jiné místo v kódu

V rámci nich existují možnosti, jak změnit chování a umístění dat. Úplný přehled najdete v datovém listu, kapitola 3. strana 309

Při každém tiku systémových hodin, každý staový stroj načte, dekóduje a vykoná jednu instrukci.

Instrukční sada

PIO instruction set

Kromě toho existuje několik funkcí, které vám usnadní život:

Side-setting je místo, kde můžete nastavit jeden nebo více pinů ve stejnou dobu, kdy běží další instrukce.

Ke každé instrukci lze přidat zpoždění (počet zpoždění hodinových cyklů v hranatých závorkách).

Podívejme se na opravdu jednoduchý příklad, obdélníkovou vlnu:

program squarewave_wrap
.wrap_target
	set pins, 1 [1]
	set pins, 0 [1]
.wrap

To využívá vnější smyčku. Návěští .wrap_target je speciální případ, protože na něj nepotřebujeme explicitně přeskakovat: můžeme použít .wrap ke skoku přímo na .wrap_target.

Na rozdíl od explicitní instrukce skoku to nezabere hodinový cyklus, takže v tomto případě vytváříme perfektní 50% obdélníkovou vlnu.

Štítky .wrap a .wrap_target lze vynechat. Každá instrukce má také zpoždění jednoho hodinového cyklu.

Hodnota pinů je nastavena při vytváření programu PIO, takže to může vytvořit obdélníkovou vlnu na libovolném I/O.

Příklad v C a PIO assembleru

Následující program bude generovat obdélníkovou vlnu o kmitočtu 12.5 MHz na pinu č.1 (GP0).

pinout

squarewave.c
/**
 * Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

// Output a 12.5 MHz square wave (if system clock frequency is 125 MHz).
//
// Note this program is accessing the PIO registers directly, for illustrative
// purposes. We pull this program into the datasheet so we can talk a little
// about PIO's hardware register interface. The `hardware_pio` SDK library
// provides simpler or better interfaces for all of these operations.
//
// _*This is not best practice! I don't want to see you copy/pasting this*_
//
// For a minimal example of loading and running a program using the SDK
// functions (which is what you generally want to do) have a look at
// `hello_pio` instead. That example is also the subject of a tutorial in the
// SDK book, which walks you through building your first PIO program.

#include "pico/stdlib.h"
#include "hardware/pio.h"

// Our assembled program:
#include "squarewave.pio.h"

int main() {
    // Pick one PIO instance arbitrarily. We're also arbitrarily picking state
    // machine 0 on this PIO instance (the state machines are numbered 0 to 3
    // inclusive).
    PIO pio = pio0;

    /// \tag::load_program[]
    // Load the assembled program directly into the PIO's instruction memory.
    // Each PIO instance has a 32-slot instruction memory, which all 4 state
    // machines can see. The system has write-only access.
    for (int i = 0; i < count_of(squarewave_program_instructions); ++i)
        pio->instr_mem[i] = squarewave_program_instructions[i];
    /// \end::load_program[]

    /// \tag::clock_divider[]
    // Configure state machine 0 to run at sysclk/2.5. The state machines can
    // run as fast as one instruction per clock cycle, but we can scale their
    // speed down uniformly to meet some precise frequency target, e.g. for a
    // UART baud rate. This register has 16 integer divisor bits and 8
    // fractional divisor bits.
    pio->sm[0].clkdiv = (uint32_t) (2.5f * (1 << 16));
    /// \end::clock_divider[]

    /// \tag::setup_pins[]
    // There are five pin mapping groups (out, in, set, side-set, jmp pin)
    // which are used by different instructions or in different circumstances.
    // Here we're just using SET instructions. Configure state machine 0 SETs
    // to affect GPIO 0 only; then configure GPIO0 to be controlled by PIO0,
    // as opposed to e.g. the processors.
    pio->sm[0].pinctrl =
            (1 << PIO_SM0_PINCTRL_SET_COUNT_LSB) |
            (0 << PIO_SM0_PINCTRL_SET_BASE_LSB);
    gpio_set_function(0, GPIO_FUNC_PIO0);
    /// \end::setup_pins[]

    /// \tag::start_sm[]
    // Set the state machine running. The PIO CTRL register is global within a
    // PIO instance, so you can start/stop multiple state machines
    // simultaneously. We're using the register's hardware atomic set alias to
    // make one bit high without doing a read-modify-write on the register.
    hw_set_bits(&pio->ctrl, 1 << (PIO_CTRL_SM_ENABLE_LSB + 0));
    /// \end::start_sm[]

    return 0;

}

V našem Céčkovém programu si všimněte, že program neobsahuje žádnou nekonečnou smyčku a normálně skončí, avšak Pico bude kmitat na frekvenci 12.5 MHz na pinu GP0 donekonečna. Je to důsledek toho, že PIO je samostatný koprocesor.

squarewave.pio
;
; Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
;
; SPDX-License-Identifier: BSD-3-Clause
;

.program squarewave
    set pindirs, 1   ; Set pin to output
again:
    set pins, 1 [1]  ; Drive pin high and then delay for one cycle
    set pins, 0      ; Drive pin low
    jmp again        ; Set PC to label `again`
CMakeLists.txt
add_executable(pio_squarewave)

pico_generate_pio_header(pio_squarewave ${CMAKE_CURRENT_LIST_DIR}/squarewave.pio)
pico_generate_pio_header(pio_squarewave ${CMAKE_CURRENT_LIST_DIR}/squarewave_wrap.pio)
pico_generate_pio_header(pio_squarewave ${CMAKE_CURRENT_LIST_DIR}/squarewave_fast.pio)

target_sources(pio_squarewave PRIVATE squarewave.c)

target_link_libraries(pio_squarewave PRIVATE pico_stdlib hardware_pio)
pico_add_extra_outputs(pio_squarewave)

# add url via pico_set_program_url
example_auto_set_url(pio_squarewave)

file(MAKE_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/generated)
# generate .hex file and .pio.h file for the RP2040 datasheet (to make sure
# the datasheet always shows the output of the latest pioasm version)
add_custom_target(pio_squarewave_datasheet DEPENDS
		${CMAKE_CURRENT_LIST_DIR}/generated/squarewave.hex
		${CMAKE_CURRENT_LIST_DIR}/generated/squarewave.pio.h
		${CMAKE_CURRENT_LIST_DIR}/generated/squarewave_wrap.pio.h
		)
add_custom_command(OUTPUT ${CMAKE_CURRENT_LIST_DIR}/generated/squarewave.hex
        DEPENDS ${CMAKE_CURRENT_LIST_DIR}/squarewave.pio
        COMMAND Pioasm -o hex ${CMAKE_CURRENT_LIST_DIR}/squarewave.pio ${CMAKE_CURRENT_LIST_DIR}/generated/squarewave.hex
        VERBATIM)
add_custom_command(OUTPUT ${CMAKE_CURRENT_LIST_DIR}/generated/squarewave.pio.h
        DEPENDS ${CMAKE_CURRENT_LIST_DIR}/squarewave.pio
        COMMAND Pioasm ${CMAKE_CURRENT_LIST_DIR}/squarewave.pio ${CMAKE_CURRENT_LIST_DIR}/generated/squarewave.pio.h
        VERBATIM)
add_custom_command(OUTPUT ${CMAKE_CURRENT_LIST_DIR}/generated/squarewave_wrap.pio.h
        DEPENDS ${CMAKE_CURRENT_LIST_DIR}/squarewave_wrap.pio
        COMMAND Pioasm ${CMAKE_CURRENT_LIST_DIR}/squarewave_wrap.pio ${CMAKE_CURRENT_LIST_DIR}/generated/squarewave_wrap.pio.h
        VERBATIM)
add_dependencies(pio_squarewave pio_squarewave_datasheet)
Výsledná vlna na pinu GP0 (do obdélníku to má daleko)

IMG 20240627 203822

Z obrázku je vidět, že výsledná vlna není moc obdélníková. 12.5 MHz je už docela vysoká frekvence a meření osciloskopen zkresluje.

Zkusíme snížit frekvenci na 625KHz:

   pio->sm[0].clkdiv = (uint32_t) (50.0f * (1 << 16)); // frekvence bude 625 kHz
Výsledná vlna na pinu GP0 při frekvenci 625kHz

IMG 20240627 205100

Tady už to je poměrně hezký obdélník.

Céčkový program je velmi jednoduchý, ale není moc hezký. Zatím nic nevíme o struktuře PIO.

Problémy měření

Parazitní kapacita osciloskopické sondy ovlivňuje měřený signál. Je potřeba použít co nejmenší zemní smyčku, potom je měření přesné.

Osciloskopická sonda nevhodná pro vysoké frekvence

IMG 20240627 233909

Jednoduchou úpravou (odebereme dlouhý drát s krokodýlem) uděláme malou zemní smyčku a budeme dostávat mnohem lepší výsledky měření.

Upravená sonda s malou parazitní kapacitou vhodná k měření vyšších frekvencí

IMG 20240627 234030

Zdroje a odkazy