Úvod

Sériové periferní rozhraní (Serial Peripheral Interface) je protokol, kterým mohou komunikovat dvě nebo více zařízení. Je plně duplexní, což znamená, že můžete odesílat a přijímat data současně. Ve skutečnosti, jak uvidíme, musíte data odesílat, abyste je mohli přijímat. Pro komunikaci se používají 3 vodiče – jeden, který přenáší data z hlavního do sekundárního zařízení, druhý, který přenáší data ze sekundárního do hlavního, a hodinová linka, která tyto přenosy synchronizuje. Sekundární zařízení jsou adresována pomocí výběru čipu – čtvrtého vodiče, který samostatně připojuje každý sekundární vodič k hlavnímu. Celkem tedy 4 vodiče plus zem.

SPI je rychlé a optimalizované pro komunikaci mezi zařízeními, která jsou velmi blízko sebe (tj. na stejné desce plošných spojů).

Zapojení hardwaru

Zapojení SPI systému je velmi jednoduché. Obrázek znázorňuje propojení mezi hlavním zařízením (obvykle mikrokontrolérem) a několika sekundárními zařízeními (periferiemi). Vidíte, že všechna sekundární zařízení sdílejí společnou hodinovou linku, která vychází z hlavního zařízení. Také vidíte, že obě datové linky jsou sdíleny všemi sekundárními zařízeními. Ty jsou označeny MOSI (Main Out Secondary In) – která odesílá data z hlavního zařízení do sekundárního – a MISO (Main In Seconary Out), která odesílá data ze sekundárního zařízení do hlavního.

SCLK jsou hodiny, které synchonizují obě zařízení.

Pouze jedno zařízení může generovat hodinový signál na lince SCLK. Toto zařízení (jehož úlohou je generovat hodiny SPI) se nazývá Master nebo Centrální uzel (na obrázku SPI Main). Všechna ostatní zařízení se v tomto okamžiku chovají jako periferní (nebo podřízené) uzly. Pro fungování SPI je potřeba alespoň jeden centrální uzel a jeden periferní uzel. Dva periferní uzly propojené navzájem nebudou fungovat, protože žádná zařízení nemohou generovat hodiny. MISO, MOSI a SCLK linky jsou společné pro všechna zařízení (centrální a periferní uzly). Když chce centrální uzel odesílat/přijímat nějaká data, může na lince generovat hodiny se specifickou frekvencí na lince SCLK a aplikovat data na MOSI linku. Než to ale udělá, musí centrální uzel určit, které zařízení má tato data přijmout. To se provede tak, že se CS linka odpovídajícího periferního zařízení stáhne na nízkou úroveň, zatímco všechny ostatní CS linky se nastaví na vysokou úroveň.

Protože pouze centrální uzel může generovat hodiny, může pouze centrální uzel iniciovat přenos dat. Proto musí komunikace SPI využívat schéma příkaz-odezva. Centrální uzel může nastavit CS pin uzlu na nízkou úroveň a poté na něj odeslat nějaký příkaz. Po přijetí příkazu začne periferní uzel odesílat data odpovědi. Centrální uzel musí udržovat hodinový signál po dobu, kdy periferní zařízení odesílá data.

spi hardware

Jednou z trochu nepříjemných věcí na SPI je, že různí výrobci používají pro tyto linky různé názvy. Proto si musíte přečíst datový list, abyste se ujistili, že zapojujete správné piny mezi Pico a pariferii.

  • Místo MOSI (Master Out Slave In) se například může objevit COPI (Controller Out Peripheral In), SDO (SPI Data Out), nebo jen DO, nebo SO.

  • Místo MISO (Master In Slave Out) se může objevit CIPO (Controller In Peripheral Out), SDI nebo SI.

Musíte si přečíst datový list a podívat se na časové diagramy (na jeden se podíváme za chvíli), abyste si ověřili funkčnost každé datové linky.

Mimochodem, uvidíte také různé označení pro chipselect (výběr periferie).

  • Můžete vidět označení CS, nebo CSn, nebo SS, nebo CS

Všechny znamenají totéž.

Ale jak vidíte, zapojení je naprosto jednoduché. K jednomu hlavnímu obvodu můžete připojit mnoho sekundárních obvodů, omezujícím zdrojem se často stává dostatek vstupních a výstupních linek pro výběr čipu. Vzhledem k tomu, že tyto datové linky jsou sdíleny, můžete usoudit, že není dobrý nápad spouštět výběr čipu na dvou zařízeních současně.

Popis na vysoké úrovni

Během přenosu přes SPI se data přesouvají mezi dvěma posuvnými registry. Jeden z těchto posuvných registrů se nachází na hlavním obvodu a druhý na sekundárním. Jak velké jsou tyto posuvné registry? Budete si muset přečíst datový list pro konkrétní sekundární obvod, se kterým komunikujete. Může mít 8 bitů, 16 bitů nebo 32 bitů. Tyto dva posuvné registry tvoří mezičipovou kruhovou vyrovnávací paměť.

Když hlavní registr načte vysílací registr, začne přenos. Data jsou současně a synchronně odesílána z hlavního do sekundárního a ze sekundárního do hlavního registru přes linky MOSI/MISO. Synchronně znamená, že každý bit je vysílán synchronně s hodinami (možná na každé náběžné nebo sestupné hraně - to je konfigurovatelné, jak uvidíme). Současně znamená, že spodní část hlavního posuvného registru je současně posunuta do horní části sekundárního posuvného registru. Každý je pak samozřejmě posunut dolů a přenese se dalších 7 bitů, dokud se hodnoty v těchto dvou registrech nevymění.

spi high level

Časový diagram

Zde je příklad 8bitového přenosu do/z SPI zařízení. Vidíte, že pro zahájení transakce je linka pro výběr čipu přivedena na nízkou úroveň hlavním zařízením. Když sekundární zařízení zjistí, že tato linka je na nízké úrovni, ví, že má očekávat (v tomto případě) 8 hodinových impulsů. Mezi ukončením výběru čipu a spuštěním hodin může být určité zpoždění, podívejte se do datového listu.

Hodiny poté začnou vysílat impulsy. V tomto diagramu jsou hodiny v klidu na nízké úrovni a v aktivní vysoké úrovni. Svislé červené čáry jednoduše označují náběžné hrany každého hodinového impulsu. Vidíte, že data jsou platná na náběžných hranách a že se data mění na sestupných hranách každého hodinového impulsu. I toto je konfigurovatelné a budete si muset přečíst datový list sekundárního zařízení, abyste zkontrolovali, co je správné.

Polarita hodin 0, fáze hodin 0

SPI P0F0

Na základě tohoto diagramu můžete odvodit, že existují 4 různé režimy SPI. Dva stupně volnosti, které se mění pro každý režim, jsou polarita hodin a fáze hodin.

Na diagramu je polarita 0 a fáze 0. To znamená, že hodiny jsou v klidu na nízkém signálu a aktivní na vysokém signálu a data jsou platná na náběžných hranách.

Některá zařízení však budou mít fázi 1, což znamená, že data jsou platná na sestupných hranách a hodnota dat se mění na náběžných hranách.

Polarita hodin 0, fáze hodin 1

SPI P0F1

Některá mohou mít fázi 0 a polaritu 1, což znamená, že hodiny jsou v klidu na vysokém a jsou aktivní nízkém signálu.

Polarita hodin 1, fáze hodin 0

SPI P1F0

A konečně, můžete mít polaritu 1 a fázi 1, což znamená, že hodiny jsou v klidu na vysokém signálu a aktivním na nízkém signálu a data jsou platná na sestupných hranách.

Polarita hodin 1, fáze hodin 1

SPI P1F1

Jak poznáte, který z nich je správný? Přečtete si datový list!! (Nebo často provedete vyčerpávající vyhledávání. Ne, to není nejpřesnější metoda, ale 4 není příliš velké číslo na to, abychom jen zkoušeli režimy, dokud nebudou fungovat).

Pico SPI rozhraní

SPI hardware na Picu

Vnitřní struktura SPI řadiče na Picu

RPI SPI controller

Mikrokontrolér RP2040, který se nachází na desce Raspberry Pi Pico, má uvnitř dva identické SPI řadiče. SPI řadiče jsou založeny na synchronním sériovém portu (SSP) PrimeCell od společnosti ARM. Každý SPI řadič má následující funkce.

  • Režimy Master nebo Slave

  • Rozhraní kompatibilní s Motorola SPI

  • Synchronní sériové rozhraní Texas Instruments

  • Rozhraní National Semiconductor Microwire

  • 8 hlubokých Tx a Rx FIFO

  • Generování přerušení pro obsluhu FIFO nebo indikaci chybových stavů

  • Lze řídit z DMA

  • Programovatelná taktovací frekvence

  • Programovatelná velikost dat 4–16 bitů

FIFO paměti RX i TX (First In First Out, vyrovnávací paměť pro ukládání dat) mají šířku 16 bitů, což umožňuje přímý zápis 16bitových dat. FIFO paměti mají hloubku 8 pozic, což znamená celkem 32 bajtů. Piny obou SPI regulátorů lze přiřadit k více pinům GPIO pomocí bloku GPIO Mux čipu RP2040. Každý SPI blok může mít maximálně 8 signálových linek, jak je popsáno níže.

Jméno Směr Popis

SSPFSSOUT

výstup

Ekvivalen výstupu CS z centrálního uzlu

SSPCLKOUT

výstup

Výstup hodin (SCLK) z centrálního uzlu

SSPRXD

vstup

Linka pro příjem dat (MISO) pro centrální uzel

SSPTXD

výstup

Linka pro vysílání dat (MOSI) pro centrální uzel

nSSPCTLOE

výstup

Řídí *SSPCLKOUT signál. Aktivní je nízko. 1 = periférie, 0 = centrální uzel

SSPFSSIN

vstup

Ekvivalent vstupu CS pro periférii

SSPCLKIN

vstup

Hodinový vstup pro periférii

nSSPOE

výstup

Řídí linku SSPTXD, Aktivní je nízko. 1 = TX výstup je vypnut, 0 = TX výstup je zapnut.

To je více než čtyři signály, o kterých jsme hovořili dříve. Ale nebojte se. V závislosti na roli SPI rozhraní (centrální nebo periferní) budou tyto signály směrovány na příslušné piny. Pico C/C++ SDK má API (Application Programming Interface) pro ovládání a konfiguraci SPI řadičů, jak uvidíme později. Jedna věc, kterou musíte mít na paměti, je, že SPI řadič RP2040 nemůže dynamicky přepínat mezi rolí Centrální a Periferní. Uživatel musí režim deinicializovat a ručně znovu inicializovat SPI v jiném režimu. Datový list RP2040 na straně 501 obsahuje podrobný popis jeho SPI řadiče a souvisejících registrů.

Detaily lze nalézt na About the ARM PrimeCell SSP (PL022)

Programování SPI na Picu

Programátoři Raspberry Pi Pico mají k dispozici dva SPI řadiče, SPI0 a SPI1, které mohou fungovat v režimu nadřízený (master) nebo podřízený (slave). Zapojení obou řadičů může být směrováno k různým pinům.

řadič funkce sada 1 sada 2 sada 3 sada 4

SPI0

MISO

GP0

GP4

GP16

GP20

CSn

GP1

GP5

GP17

GP21

SCLK

GP2

GP6

GP18

GP22

MOSI

GP3

GP7

GP19

GP23

SPI1

MISO

GP8

GP12

GP24

GP28

CSn

GP9

GP13

GP25

GP29

SCLK

GP10

GP14

GP26

MOSI

GP11

GP15

GP27

SPI piny jsou označeny růžově a vínově je obarvené je implicitní SPI rozhraní

pico pinout

Pokud budeme pracovat se SPI sběrnicí nesmíme zapomenout do našeho programu vložit

#include "hardware/spi.h"

a do CMakeLists.txt dodat knihovnu hardware_spi

target_link_libraries( nas_projekt pico_stdlib hardware_spi )

Výběr pinu, aby pracoval v rámci SPI sběrnice se provádí funkcí:

gpio_set_function( pin, GPIO_FUNC_SPI );

kde pin musí odpovídat příslušnému rozhraní spi0 nebo spi1 podle výše uvedené tabulky. Takže například pro spi0 v implicitním nastavení to bude:

gpio_set_function( 16, GPIO_FUNC_SPI);
gpio_set_function( 17, GPIO_FUNC_SPI);
gpio_set_function( 18, GPIO_FUNC_SPI);
gpio_set_function( 19, GPIO_FUNC_SPI);

Dá se to zaprat i takto:

gpio_set_function (PICO_DEFAULT_SPI_RX_PIN, GPIO_FUNC_SPI);
gpio_set_function (PICO_DEFAULT_SPI_SCK_PIN, GPIO_FUNC_SPI);
gpio_set_function (PICO_DEFAULT_SPI_TX_PIN, GPIO_FUNC_SPI);
gpio_set_function (PICO_DEFAULT_SPI_CSN_PIN, GPIO_FUNC_SPI);

SPI funkce

SPI funkce v Pico C SDK lze rozdělit do tří kategorií. Na inicializaci, konfiguraci a na přenos dat. Podíváme se na ně. Všechny podrobnosti jsou v dokumentaci k SDK zde.

Inicializace

K zapnutí nebo vypnutí SPI sběrnice používáme funkce:

uint spi_init( spi_inst_t *spi, uint baudrate ); // zapnutí
void spi_deinit( spi_inst_t *spi); // vypnutí

Funkcí spi_init nastavujeme rychlost rozhraní v baudech, což je to samé jako nastavení tikání hodin v Hertzích.

Parametr spi typu spi_inst_t je ukazatel na hardwarovou adresu SPI řadiče a není to index 0 nebo 1, jak jsme zvyklí. Je však k dispozici užitečná makra, kterými si vybereme, zda budeme pracovat se SPI0 nebo SPI1, místo zadávání hardwarové adresy.

#define spi0 ((spi_inst_t *)spi0_hw)
#define spi1 ((spi_inst_t *)spi1_hw)

K získání indexu se dá použít funkce:

static uint spi_get_index( spi_inst_t *spi);

která konvertuje hardwarovou adresu na index 0 nebo 1.

Konfigurace

Nejlepším způsobem, jak nastavit rychlost SPi rozhraní je použít funkci spi_set_baudrate, protože nám vrátí nastvenou hadnotu, kte je nejblíže skutečné rychlosti hodin.

uint spi_set_baudrate( spi_inst_t *spi, uint baudrate );

Funkce spi_set_slave určuje, zda bude Pico fungovat jako řídící (master) nebo podřízený (slave) na sběrnici. Parametr slave roven false — Pico je řídící, slave rovno true — Pico je podřízené.

static void spi_set_slave( spi_inst_t *spi, bool slave );

Pokud nepoužijete funkci spi_set_slave, tak bude SPI funkcí spi_init nastaveno do režimu master.

Nejdůležitější funkcí je spi_set_format, která nastavuje polaritu a fázi hodin, počet datových bitů přenosu a SPI režim (Big-Endian nebo Little-Endian), což není nic jiného než způsob přenášení bitů (nejdříve MSB nebo nejdříve LSB).

static void spi_set_format( spi_inst_t *spi, uint data_bits, spi_cpol_t cpol, spi_cpha_t cpha, __unused spi_order_t order);

Parametry:

  • spi — příslušná instance buď spi0 nebo spi1

  • data_bits — počet přenesených bitů během transakce, hodnoty v rozsahu 4..16.

  • cpol — polarita hodin SSPCLKOUT. Musí být SPI_CPOL_0 pro polaritu 0 nebo SPI_CPOL_1 pro polaritu 1. Viz obrázky výše o polaritě a fázi.

  • cpha — fáze hodin SSPCLKOUT. Musí být SPI_CPHA_0 pro fázi 0 nebo SPI_CPHA_1 pro fázi 1. Viz obrázky výše o polaritě a fázi.

  • order-- pořadí přenášení bitů, musí být SPI_MSB_FIRST na PL022.

Funkce pro přenos dat

SPI sběrnice komunikuje obousměrně. Kvůli tomu je implementace komunikačních funkcí trochu jiná než u jiných sběrnic. Základní funkcí, kterou budeme používat je:

int spi_write16_read16_blocking( spi_inst_t *spi, const uint16_t *src, uint16_t *dst, size_t len );

Pole src a dst mají stejou velikost, která je v parametru len. Pole src se používá pro vysílaná data a pole dst je pro data přijímaná zpět. Obě pole mají prvky o velikost 16 bitů, avšak posílá se jenom počet bitů, který je zadán v konfiguraci instance SPI. To znamená, že pokud například nastavíme data_bits funkcí spi_set_format na velikost 8, bude se přenášet jenom vrchních 8 bitů z každého prvku pole src a dst. Funkce vrací počet zapsaných nebo přečtených bajtů.

Tady vzniká velmi zajímavá situace, funkce nám může vrátit nenulové číslo, přestože podřízené zařízení nekomunikuje a neposílá nám žádná data. Můžeme si to ověřit pokusem. Nemůžeme spoléhat podle návratové hodnoty na to, že počet přenesených dat podřízené zařízení skutečně obdrželo. Stejně tak nemůžeme spoléhat na to, že nadřízené zařízení skutečně přečetlo data z podřízeného zařízení. V případě poruchy podřízeného zažízení obdržíme v *dst nuly.

Pokud potřebujete data jenom zapisovat nebo jenom číst, tak použijte funkce:

int spi_write16_blocking( spi_inst_t *spi, const uint16_t *src, size_t len );
int spi_read16_blocking( spi_inst_t *spi, uint16_t *repeated_tx_data, uint16_t *dst, size_t len );

Funkce spi_write16_blocking přijatá data jednoduše zahazuje. Funkce spi_read16_blocking má zvláštní parametr repeated_tx_data, kde jsou obvykle samé nuly, ale některé periferie vyžadují, aby tam bylo něco jiného. Obě tyto funkce jsou jenom zjednodušením funkce spi_write16_read16_blocking.

Dají se používat i obdobné osmibitové verze těchto funkcí:

int spi_write_read_blocking( spi_inst_t *spi, const uint8_t *src, uint8_t *dst, size_t len );
int spi_write_blocking( spi_inst_t *spi, const uint8_t *src, size_t len );
int spi_read_blocking( spi_inst_t *spi, uint8_t repeated_tx_data, uint8_t *dst, size_t len );

Jsou to jednodušší verze funkce spi_write16_read16_blocking, které používají 8bitové pole místo 16bitových.

Je tady ještě jedna malá komplikace, kterou můžete téměř ignorovat. SPI hardware má buffery MISO a MOSI linky mají na sobě frontu FIFO 8 prvků dlouhou a to znamená, že můžete posílat a přijímat data rychleji, poku není fronta plná nebo prázdná. Stav této fronty lze zjistit funkcí:

static size_t spi_is_readable( spi_inst_t *spi);

která vrací nenulovou hodnotu, pokud jsou ve frontě nepřečtená data. A obdobně funkce:

static size_t spi_is_writable( spi_inst_t *spi );

vrací nenulovou hodnotu, pokud je ve frontě nějaké místo. Tyto funkce mužete použít, pokud chcete minimalizovat čekání při použití blokujících I/O funkcí.

Použití přenosových funkcí

Základní funkce pro přenos dat spi_write16_read16_blocking ukazuje jak se používá obousměrný přenos.

uint16_t  wBuff[] = {'A', 'B', 'C'};
uint16_t  rBuff[3];
int n = spi_write16_read16_blocking( spi0, wBuff, rBuff, 3);

Pošle tři prvky z wBuff a přijme zpět tři prvky do rBuff. Data v příjmovém bufferu rBuff budou mít nějaký význam tehdy, pokud periferie pošle něco smysluplného, co nás zajímá.

Nemají-li data pro nás význam nebo je nepořebujeme, naprogramujeme to raději takto:

uint16_t wBuff[] = {'A', 'B', 'C'};
int n = spi_write16_blocking( spi0, wBuff, 3);

U 8bitového přenosu to uděláme takto:

uint8_t wBuff[] = {'A', 'B', 'C'};
spi_write_blocking( spi0, wBuff, 3);

Podobně když nás nezajímají data poslaná na periferii a chceme jenom číst, napíšeme:

uint16_t wBuff = {0};
uint16_t rBuff[3];
int n = spi_write16_read16_blocking( spi0, wBuff, rBuff, 3);

nebo

uint16_t rBuff[3];
int n = spi_read16_blocking( spi0, rBuff, 3);

Pro 8bitový přenos to bude:

uint8_t rBuff[3];
spi_read_blocking( spi0, rBuff, 3);

Nyní jeden detail. Jaký je rozdíl mezi přenosem několika bajtů najednou a přenosem několika bajtů individuálně mnohonásobným voláním funkce spi_write16_read16_blocking()?

Přenos 3 prvků najednou
// přenos 3 bajtů najednou
uint16_t  wBuff[] = {'A', 'B', 'C'};
uint16_t  rBuff[3];
// CS se bude aktivovat
int n = spi_write16_read16_blocking( spi0, wBuff, rBuff, 3);
// CS se deaktivuje
Přenos po prvcích
// přenos 3 bajtů po prvcích
uint16_t  wBuff[] = {'A', 'B', 'C'};
uint16_t  rBuff[3];
// CS se bude aktivovat
int n = spi_write16_read16_blocking( spi0, wBuff, rBuff, 1);
// CS se deaktivuje
// CS se bude aktivovat
n = spi_write16_read16_blocking( spi0, wBuff+1, rBuff+1, 1);
// CS se deaktivuje
// CS se bude aktivovat
n = spi_write16_read16_blocking( spi0, wBuff+2, rBuff+2, 1);
// CS se deaktivuje

Před každým přenosem se aktivuje linku CS, potom se přenášejí data a potom se linka CS deaktivuje. Při přenášení několika bajtů najednou je linka CS aktivní po celou dobu přenosu, to znamená, že CS nemusíme deaktivovat mezi jednotlivými bajty. Někdy tento rozdíl v přenosu není důležitý a můžete přenos dělat třeba 3 bajty najednou nebo 3 bajty po jednom. Avšak některé periferie přeruší operaci přenosu, pokud je CS deaktivováno uprostřed přenosu (tj. přenášíme po jednotlivých bajtech). Protože Pico vyžaduje, abychom CS aktivovali a deaktivovali sami, je na vás, jak to uděláte.

Je důležité vědět, že přenos vždy probíhá tak, že jakmile je první prvek vyslán, tak je současně i první prvek přijat. Druhý prvek je vyslán a současně je druhý prvek přijmut, třetí prvek vyslán a současně třetí prvek přijmut a tak dále. To je zcela jiný přístup než u ostatních protokolů a pokud tomu neporozumíte, tak to může vést k některým opravdu zajímavým chybám.

Komunikace dvou Raspberry Pi Pico pomocí SPI sběrnice

Zkusíme rozchodit nejprve jednosměrnou komunikaci mezi dvěma RPi Pico. K tomu si založíme dva projekty. Projekt pro RPi Pico, které bude řídící, umístíme do adresáře master. Projekt pro RPi Pico, které bude podřízené, umístíme do adresáře slave.

IMG 20250809 090316

Komunikace dvou Raspberry Pi Pico pomocí SPI
Pico řídící master/master.c
#include <stdio.h>
#include "hardware/spi.h"
#include "pico/binary_info.h"
#include "pico/stdlib.h"

#define BUF_LEN 128

void printbuf(uint8_t buf [], size_t len)
{
  int i;
  for (i = 0; i < len; ++i) {
    if (i % 16 == 15)
      printf ("%02x\n", buf [i]);
    else
      printf ("%02x ", buf [i]);
  }
  // přidáme '\n'
  if (i % 16) {
    putchar ('\n');
  }
}

int main() {
  // inicializace stdio pře USB
  stdio_init_all();
  sleep_ms (21000);
  printf ("SPI ridici Pico\n");

  // SPI0 poběží na 1 MHz
  spi_init (spi_default, 1000000);

  // přiřadíme piny
  // (zde jsou implicitní pro SPI0)
  gpio_set_function(16, GPIO_FUNC_SPI);
  gpio_set_function(18, GPIO_FUNC_SPI);
  gpio_set_function(19, GPIO_FUNC_SPI);
  gpio_set_function(17, GPIO_FUNC_SPI);

  // Bufer pro posílání
  uint8_t out_buf[BUF_LEN];
  // Buffer pro příjem dat.
  uint8_t in_buf[BUF_LEN];

  // Vynulujeme buffery.
  for(uint8_t i = 0; i < BUF_LEN; ++i) {
    out_buf[i] = 0;
    in_buf[i] = 0;
  }

  for (uint8_t i = 0; ; ++i) {
    printf ("Posilam %d do 2. Pico\n", i);
    out_buf [0] = i;
    // Zapisujeme do výstupního buferu MOSI
    // a současně čteme z MISO bufferu.
    spi_write_read_blocking(
        spi_default,
        out_buf,
        in_buf,
        1);

    // Spíme 2s, abychom mohli číst
    // z stdio
    sleep_ms (2000);
  }
}
Pico podřízené slave/slave.c
#include <stdio.h>
#include "hardware/spi.h"
#include "pico/binary_info.h"
#include "pico/stdlib.h"

#define BUF_LEN 128

void printbuf (uint8_t buf [], size_t len)
{
  int i;
  for (i = 0; i < len; ++i) {
    if (i % 16 == 15)
      printf ("%02x\n", buf [i]);
    else
      printf ("%02x ", buf [i]);
  }
  // přidáme '\n'
  if (i % 16) {
    putchar ('\n');
  }
}

int main() {
  // inicializace stdio pře USB
  stdio_init_all();
  sleep_ms (2 * 1000);
  printf ("SPI podrizene Pico\n");

  // SPI0 poběží na 1 MHz (podřízený)
  spi_init( spi_default, 1000000);
  spi_set_slave( spi_default, true );

  // přiřadíme piny
  gpio_set_function(16, GPIO_FUNC_SPI);
  gpio_set_function(18, GPIO_FUNC_SPI);
  gpio_set_function(19, GPIO_FUNC_SPI);
  gpio_set_function(17, GPIO_FUNC_SPI);

  // Bufer pro posílání
  uint8_t out_buf[BUF_LEN];
  // Buffer pro příjem dat.
  uint8_t in_buf[BUF_LEN];

  // Vynulujeme buffery
  for (uint8_t i = 0; i < BUF_LEN; ++i) {
    out_buf [i] = 0;
    in_buf [i] = 0;
  }

  // čtecí smyčka
  while (1) {
    if (spi_is_readable(spi_default)) {
      printf ("Ctu data z SPI..\n");
      // Nezapisujeme nic do výstupního
      // buferu MOSI - out_buf je 0
      // a současně čteme z MISO bufferu.
      spi_read_blocking(
        spi_default,
        0,
        in_buf,
        1);
      printf ("Prijato: %d\n", in_buf [0]);
    }
  }
}
Protože podřízené Pico je v režimu slave, je nutné na něm prohodit drátky SPI0 TX a SPI0 RX oproti řídícímu Pico v režimu master. Při zapojování jiných periférií, které fungují v režimu podřízený se to nedělá.
Tabulka 1. Spojení dvou RPi Pico pomocí SPI0 sběrnice
Pico řídící barva drátu Pico podřízené

GPIO17 (SPI0 CSN)

zelená

GPIO17 (SPI0 CSn)

GPIO18 (SPI0 SCK)

modrá

GPIO18 (SPI0 CSn

GPIO19 (SPI0 TX)

čevená

GPIO16 (SPI0 RX)

GPIO16 (SPI0 RX)

žlutá

GPIO19 (SPI0 TX)

GND

černá

GND

Červený USB kabel je do řídícího Pico, bílý USB kabel je do podřízeného Pico.

zapojeni SPI komunikace 2Pico

Výstup minicomu
Pico řídící minicom -b 115200 -D /dev/ttyACM0
SPI ridici Pico
Posilam 0 do 2. Pico
Posilam 1 do 2. Pico
Posilam 2 do 2. Pico
Posilam 3 do 2. Pico
Posilam 4 do 2. Pico
Posilam 5 do 2. Pico
Posilam 6 do 2. Pico
Posilam 7 do 2. Pico
Posilam 8 do 2. Pico
Posilam 9 do 2. Pico
Posilam 10 do 2. Pico
Pico podřízené minicom -b 115200 -D /dev/ttyACM1
SPI podrizene Pico
Ctu data z SPI..
Prijato: 0
Ctu data z SPI..
Prijato: 1
Ctu data z SPI..
Prijato: 2
Ctu data z SPI..
Prijato: 3
Ctu data z SPI..
Prijato: 4
Ctu data z SPI..
Prijato: 5
Ctu data z SPI..
Prijato: 6
Ctu data z SPI..
Prijato: 7
Ctu data z SPI..
Prijato: 8
Ctu data z SPI..
Prijato: 9
Ctu data z SPI..
Prijato: 10
CMakeLists.txt pro naše dva projekty
Pico řídící master/CMakeLists.txt
cmake_minimum_required(VERSION 3.13)

include($ENV{PICO_SDK_PATH}/external/pico_sdk_import.cmake)

project(master C CXX ASM)

set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

pico_sdk_init()

add_executable(master
  master.c
)

pico_enable_stdio_usb(master 1)
pico_enable_stdio_uart(master 0)

pico_add_extra_outputs(master)

target_link_libraries(master pico_stdlib hardware_spi)
Pico podřízené slave/CMakeLists.txt
cmake_minimum_required(VERSION 3.13)

include($ENV{PICO_SDK_PATH}/external/pico_sdk_import.cmake)

project(slave C CXX ASM)

set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

pico_sdk_init()

add_executable(slave
  slave.c
)

pico_enable_stdio_usb(slave 1)
pico_enable_stdio_uart(slave 0)

pico_add_extra_outputs(slave)

target_link_libraries(slave pico_stdlib hardware_spi)

Problémy tohoto software jsou dva:

  • Komunikace je jednosměrná.

  • Ani jedno Pico neví o případné poruše druhého Pica.

Obousměrná komunikace

Udělat synchronní obousměrnou komunikaci je celkem jednoduché, stačí na obou RPi Pico používat funkci spi_write_read_blocking(). Komunikace obou strojků se zasynchronizuje automaticky.

Obousměrná komunikace
Pico řídící master_sync/master_sync.c
/* master_sync.c
 *
 */
 
#include <stdio.h>
#include "hardware/spi.h"
#include "pico/binary_info.h"
#include "pico/stdlib.h"

#define BUF_LEN 128


int main() {
  // inicializace stdio pře USB
  stdio_init_all();
  sleep_ms (2000);
  printf ("SPI ridici Pico\n");

  // SPI0 poběží na 1 MHz
  spi_init (spi_default, 1000000);

  // přiřadíme piny
  // (zde jsou implicitní pro SPI0)
  gpio_set_function(16, GPIO_FUNC_SPI);
  gpio_set_function(18, GPIO_FUNC_SPI);
  gpio_set_function(19, GPIO_FUNC_SPI);
  gpio_set_function(17, GPIO_FUNC_SPI);

  // Bufer pro posílání
  uint8_t out_buf[BUF_LEN];
  // Buffer pro příjem dat.
  uint8_t in_buf[BUF_LEN];

  // Vynulujeme buffery.
  for(uint8_t i = 0; i < BUF_LEN; ++i) {
    out_buf[i] = 0;
    in_buf[i] = 0;
  }

  int n = 0;
  for (uint8_t i = 0; ; ++i) {
    printf ("Posilam %d do 2. Pico\n", i);
    out_buf [0] = i;
    // Zapisujeme do výstupního buferu MOSI
    // a současně čteme z MISO bufferu.
    n = spi_write_read_blocking(
        spi_default,
        out_buf,
        in_buf,
        1);

    // Spíme 2s, abychom mohli číst stdio
    printf("Přečetli jsme: %d byte o hodnote %d z druhe strany\n",n,in_buf[0]);
    sleep_ms (2000);
  }
}
Pico podřízené slave_sync/slave_sync.c
/* slave_sync.c
 *
 */
 
#include <stdio.h>
#include "hardware/spi.h"
#include "pico/binary_info.h"
#include "pico/stdlib.h"

#define BUF_LEN 128

int main() {
  // inicializace stdio pře USB
  stdio_init_all();
  sleep_ms (2000);
  printf ("SPI podrizene Pico\n");

  // SPI0 poběží na 1 MHz (podřízený)
  spi_init( spi_default, 1000000);
  spi_set_slave( spi_default, true );

  // přiřadíme piny
  gpio_set_function(16, GPIO_FUNC_SPI);
  gpio_set_function(18, GPIO_FUNC_SPI);
  gpio_set_function(19, GPIO_FUNC_SPI);
  gpio_set_function(17, GPIO_FUNC_SPI);

  // Bufer pro posílání
  uint8_t out_buf[BUF_LEN];
  // Buffer pro příjem dat.
  uint8_t in_buf[BUF_LEN];

  // Vynulujeme buffery
  for (uint8_t i = 0; i < BUF_LEN; ++i) {
    out_buf [i] = 0;
    in_buf [i] = 0;
  }

  int n = 0;
  // čtecí a zapisovací smyčka
  for (uint8_t i = 0; ; ++i) {
    printf ("Posilam %d do 1. Pico. ", i);
    out_buf [0] = i;
    // Zapisujeme do výstupního buferu MOSI
    // a současně čteme z MISO bufferu.
    n = spi_write_read_blocking(
        spi_default,
        out_buf,
        in_buf,
        1);

    // Spíme 2s, abychom mohli číst z stdio
    printf("Přečetli jsme: %d byte %d z druhe strany\n",n,in_buf[0]);
    sleep_ms (2000);
  }  
}

Sestavení programů je stejné a CMakeLists.txt u obou částí jsou podobné, jako u předchozího programu.

Nevyřešeným problémem zůstává to, že pokud bude mít jedno Pico poruchu, tak druhé Pico uvidí při komunikaci jenom nulu, která však patří do množiny platných dat.

Obousměrná komunikace s ošetřením chyby komunikace

Pokud do komunikačního protokolu mezi Picy přidám k přenášenému bajtu ještě jeho bitovou negaci, tak bude snadné zjistit poruchu komunikace. Při správné komunikaci se nejprve přenese datový bajt a potom jeho negace. Porovnáním přeneseného datového bajtu a jeho přenesené bitové negace se na druhé straně zjistí chyba.

Při poruše obdrží protější Pico 0 jako datový bajt a také 0 jako bitovou negaci datového bajtu, což je špatně, protože bitová negace 0 je 255. Tak poznám chybu.

Obousměrná komunikace s ošetřením chyby komunikace
Pico řídící master_sync2/master_sync.c
/* master_sync.c s ošetřením chyby komunikace
 * (c) Jirka Chráska 2025; <jirka@lixis.cz>
 */
 
#include <stdio.h>
#include "hardware/spi.h"
#include "pico/binary_info.h"
#include "pico/stdlib.h"

#define BUF_LEN 128

void printBits8(uint8_t num) {
    int bits =  8;
    for (int i = bits - 1; i >= 0; i--) {
        printf("%d", (num >> i) & 1);
    }
    printf("\n");
}

int main() {
  // inicializace stdio pře USB
  stdio_init_all();
  gpio_init(25);
  gpio_set_dir(25,GPIO_OUT);
  gpio_put(25,1);
  sleep_ms (2000);
  printf ("SPI ridici Pico\n");

  // SPI0 poběží na 1 MHz
  spi_init (spi_default, 1000000);

  // přiřadíme piny
  // (zde jsou implicitní pro SPI0)
  gpio_set_function(16, GPIO_FUNC_SPI);
  gpio_set_function(18, GPIO_FUNC_SPI);
  gpio_set_function(19, GPIO_FUNC_SPI);
  gpio_set_function(17, GPIO_FUNC_SPI);

  // Bufer pro posílání
  uint8_t out_buf[BUF_LEN];
  // Buffer pro příjem dat.
  uint8_t in_buf[BUF_LEN];

  // Vynulujeme buffery.
  for(uint8_t i = 0; i < BUF_LEN; ++i) {
    out_buf[i] = 0;
    in_buf[i] = 0;
  }

  int n = 0;
  // čtecí a zapisovací smyčka
  for (uint8_t i = 0; ; ++i) {
    gpio_put(25,0);
    printf ("Posílám %d do podřízeného Pico\n", i);
    out_buf[0] = i;
    out_buf[1] = ~i;
    printBits8(out_buf[0]);
    printBits8(out_buf[1]);
    // Zapisujeme do výstupního buferu MOSI
    // a současně čteme z MISO bufferu.
    n = spi_write_read_blocking(
        spi_default,
        out_buf,
        in_buf,
        2);

    printBits8(in_buf[0]);
    printBits8(in_buf[1]);
    uint8_t data  =  in_buf[0];
    uint8_t ndata = ~in_buf[1];
    // test chyby
    if( data == ndata ) {
        printf("Přečetli jsme správně: %d byte o hodnote %d z druhé strany, negace %d\n",n,data,ndata);
        sleep_ms (2000);
        
    } else {
        // Při poruše blikneme LEDkou
        gpio_put(25,1);
        printf("Druhá strana nekomunikuje správně: %d byte o hodnote %d negace %d\n",n,data,ndata);
        sleep_ms(1000);
        gpio_put(25,0);
        sleep_ms(1000);
    }
  }
}
Pico podřízené slave_sync2/slave_sync.c
/* slave_sync.c s ošetřením chyby komunikace
 * (c) Jirka Chráska 2025; <jirka@lixis.cz>
 */
 
#include <stdio.h>
#include "hardware/spi.h"
#include "pico/binary_info.h"
#include "pico/stdlib.h"

#define BUF_LEN 128

void printBits8(uint8_t num) {
    int bits =  8;
    for (int i = bits - 1; i >= 0; i--) {
        printf("%d", (num >> i) & 1);
    }
    printf("\n");
}

int main() {
  // inicializace stdio pře USB
  stdio_init_all();
  gpio_init(25);
  gpio_set_dir(25,GPIO_OUT);
  gpio_put(25,1);
  sleep_ms (2000);
  printf ("SPI podrizene Pico\n");

  // SPI0 poběží na 1 MHz (podřízený)
  spi_init( spi_default, 1000000);
  spi_set_slave( spi_default, true );

  // přiřadíme piny
  gpio_set_function(16, GPIO_FUNC_SPI);
  gpio_set_function(18, GPIO_FUNC_SPI);
  gpio_set_function(19, GPIO_FUNC_SPI);
  gpio_set_function(17, GPIO_FUNC_SPI);

  // Bufer pro posílání
  uint8_t out_buf[BUF_LEN];
  // Buffer pro příjem dat.
  uint8_t in_buf[BUF_LEN];

  // Vynulujeme buffery
  for (uint8_t i = 0; i < BUF_LEN; ++i) {
    out_buf [i] = 0;
    in_buf [i] = 0;
  }

  int n = 0;
  // čtecí a zapisovací smyčka
  for (uint8_t i = 0; ; ++i) {
    gpio_put(25,0);
    printf ("Posílám %d do řídícího Pico\n", i);
    out_buf[0] = i;
    out_buf[1] = ~i;
    printBits8(out_buf[0]);
    printBits8(out_buf[1]);
    // Zapisujeme do výstupního buferu MOSI
    // a současně čteme z MISO bufferu.
    n = spi_write_read_blocking(
        spi_default,
        out_buf,
        in_buf,
        2);

    printBits8(in_buf[0]);
    printBits8(in_buf[1]);
    uint8_t data  =  in_buf[0];
    uint8_t ndata = ~in_buf[1];
    // test chyby
    if( data == ndata ) {
        printf("Přečetli jsme správně: %d byte o hodnote %d z druhe strany, negace %d\n",n,data,ndata);
        sleep_ms (2000);
        
    } else {
        // při poruše blikneme LEDkou
        gpio_put(25,1);
        printf("Druhá strana nekomunikuje správně: %d byte o hodnote %d negace %d\n",n,data,ndata);
        sleep_ms(1000);
        gpio_put(25,0);
        sleep_ms(1000);
    }
  }
}

Zdroje a odkazy