Sériová linka je nejstarší způsob propojení zařízení mezi sebou, která je stále velmi užitečná, protože poskytuje docela rychlý komunikační kanál, který může být použit na delší vzdálenosti než jiná připojení, jako USB. Dnes se sériová linka používá převážně k připojení malých počítadel a jednoduchých zařízení. Může být také použita jako domácí dekódovač signálů, viz níže.

Sériový protokol

Sériový protokol je velmi jednoduchý. Je to proto, že byl vynalezen v dobách, kdy se používaly převážně elektromechanické součátsky, relé a motory a podobně. Byl vynalezen, aby umožňoval propojení dálnopisů (ang. teletype) a proto najdeme zkratku TTY, když se sériovou linkou někam připojujeme. Elektrické zařízení spojované sériovou linkou se nazývá universální asynchronní přijímač/vysílač (ang. Universal Asynchronous Receiver/Transmiter) a proto se používá také termín UART.

Původní standarty protokolu jsou V24 a RS232. Původní sériová komunikace probíhala mezi +24V a -24V, později mezi +-12V. Dnes se používá TTL logika s úrovněmi 0V a 5V nebo 0V a 3.3V. Bez ohledu na napěťové úrovně je protokol stále stejný.

Podívejme se nyní na samotný protokol. Klidový stav linky je vysoká úroveň a vysoká úroveň značí hodnotu 0 (nízká úroveň pak hodnotu 1). Tomuto se říká invertovaná logika. Když chce zařízení vysílat, nejprve stáhne linku na nízkou úroveň a tím generuje startovací bit. Čas trvání startovacího bitu nastavuje rychlost komunikace, všechny ostatní bity mají stejnou délku trvání. Po startovacím bitu následuje obvykle 8 datových bitů (někdy 7 bitů) a někdy paritní bit a nakonec jeden nebo dva stop bity.

Původně bylo účelem startovacího bitu umožnit roztočit motory a umožnit přijímači provést korekci časování. Stop bit byl k tomu, aby se motor dostal do klidu. To bylo v době, kdy byl protokol používán při velmi nízkých rychlostech 300 baudů, tj. 300 bitů za sekundu.

Dnes je protokol prakticky stejný, ale není potřeba více stop bitů a komunikace je často tak spolehlivá, že se obejdeme i bez paritních bitů. Přenosové rychlosti jsou také vyšší, typicky 9600 až 115200 baudů. Obvykle je vyšší rychlost vždy dvojnásobkem předchozí. Nízké rychlosti mají význam při hodně dlouhém vedení nebo u pomalých zařízení.

Tabulka 1. Používané rychlosti sériové linky na PC podle standardu RS232
rychlost v baudech rychlost v bitech za sekundu

2400

2400

4800

4800

9600

9600

19200

19200

38400

38400

57600

57600

115200

115200

Abychom přesně určili jaký konkrétní protokol používáme, uvidíte často zkrácený zápis třeba 9600 8n1. To znamená přenosovou rychlost 9600 baudů, 8 data bitů, žádný paritní bit a jeden stop bit. Protokol posílá nejdříve nejméně významný bit (little endian).

Přenos 9600 baud, 8 data bitů, sudá parita, jeden stop bit

prenos 9600 8O1

První nízká úroveň je start bit, potom osm teček ukazuje ideální časovou pozici pro přijímač. Základní dékódovací algoritmus při příjmu sériových dat je detekovat začátek start bitu, potom určovat hodnotu datových bitů uprostřed časového úseku. Poznamenejme, že závěrečná vysoká úroveň napravo je stop bit.

Co to je baud? Rychlost v baudech odpovídá přenosové rychlosti v bitech za sekundu. To znamená, že při rychlosti 300 baudů je start bit 1/300s široký. To znamená, že při 9600 baudech je přenosový bit 1/9600s široký, což je \(104 \mu s\) a při 115200 badech je bit široký 1/115200s, což je \(8.6 \mu s\). Rychlost přenosu neodpovídá přesně rychlosti posílání, protože musíme z kalkulace rychlosti přenosu dat odečíst start a stop bity, případně paritní bit.

Co to je parita? Parita není nic jiného, než zabezpečení dat při přenosu. Jsou dva druhy parity, sudá (even) a lichá (odd).

  • Sudá parita (even parity) znamená, že pokud máme ve vysílaném bitovém toku lichý počet jedniček, tak paritní bit bude 1. Pokud máme v vysílaném bitovém toku sudý počet jedniček, paritní bit bude 0. To znamená, že mezi start bitem a stop bitem je celkový počet jedniček sudý.

  • Lichá parita (odd parity) znamená, že pokud máme ve vysílaném bitovém toku lichý počet jedniček, tak paritní bit bude 0. Pokud máme v vysílaném bitovém toku sudý počet jedniček, paritní bit bude 1. To znamená, že mezi start bitem a stop bitem je celkový počet jedniček lichý.

Pokud se při přenosu znaku změní nějaký bit, tak je přijímací strana schopna poznat chybu tak, že si spočítá počet jedniček celého přenosu znaku a porovná to s nastavením komunikace.

Obě zařízení musí mít nastavené stejnou rychlost přenosu v baudech, stejný počet datových bitů, stejnou paritu a stejný počet stop bitů. Jinak to nebude fungovat.

Hardware UART

Jednoduché zařízení pro sériovou linku má vysílací pin (transmit TX) a přijímací pin (receive RX) a zem. To znamená, že uplné sériové zařízení potřebuje tři dráty k obousměrné komunikaci. Obvykle TX drátek z jednoho zařízení se připojí na RX pin druhého zařízení a obráceně. Jediný problém bývá v tom, že někteří výrobci označují piny ne podle funkce na daném zařízení, ale podle toho, jak mají být zapojeny na druhém zařízení (prostě mají prohozené TX a RX). Pokud máme pochybnosti, který pin je který, dá se to zjistit pomocí osciloskopu, logickým analyzátorem nebo multimetrem. Jeden drátek odpojíme, necháme zařízení vysílat a pokud vidíme pulsy, tak je to TX pin.

Úplné sériové zařízení má ještě několik řídicích pinů (vodičů). Byly určeny k tomu, aby pomáhaly starým dálnopisům fungovat a již se moc nepužívají. Například RTS — Request To Send je linka, která se používá k žádosti o možnost vysílat z jednoho zařízení na druhé, CTS — Clear To Send je linka, která říká: můžeš nebo nemůžeš vysílat. Obvykle CTS nastavuje samotný hardware automaticky, když je přijímací buffer plný nebo prázdný.

RTS a CTS se dají použít k hardwarovému řízení toku dat. Existuje i softwarový standard, který používá znaky XON a XOFF k označení začátku a konce vysílání. Tohle asi dnes nebudete moc potřebovat, jedině pokud byste chtěli implementovat plné sériové rozharní a potom si na to budete muset napsat svůj program.

Pico ná dva UARTy, které mohou být používany společně s USB zařízením. Oba mají buffer na 32 8bitových znaků a umožnují používat jak bufferovaný tak nebufferovaný přenos a také mohou používat harwarové řízení toku pomocí RTS a CTS. Každý z nich může být připojen ke skupině 4 pinů podle následující tabulky:

Tabulka 2. UART0
piny pro UART0

TX

GP0

GP12

GP16

GP28 ?

RX

GP1

GP13

GP17

GP29 ?

CTS

GP2

GP14

GP18

RTS

GP3

GP15

GP19

Tabulka 3. UART1
piny pro UART1

TX

GP4

GP8

GP20

GP24 ?

RX

GP5

GP9

GP21

GP25 ?

CTS

GP6

GP10

GP22

GP26 ?

RTS

GP7

GP11

GP23

GP27 ?

Piny v posledním sloupci bych raději nepoužíval, protože se na RaspberryPi Pico překrývají s jinými funkcemi. Např. na GP25 je připojena vestavěná LED.

Lepší je se orientovat podle obrázku:

Piny Raspberry Pi Pico

pico pinout

Abychom mohli používat GPIO linku jako UART, je potřeba zavolat funkci:

gpio_set_function(UART_TX_PIN, GPIO_FUNC_UART);

Pokud chcete připojit Pico RX a TX piny na sériovou linku k počítači, tak je tu malý problém. Sériové porty na PC pracují na jiných napěťových úrovních než Pico. RS232 používá +13V a -13V, což je nekompatibilní s Picem (+3.3V a 0V) i třeba Arduinem (+5V a 0V). Řeší se to obvykle převodníkem úrovní z TTL na RS232 s obvodem MAX3232 nebo převodníkem z TTL na USB.

Převodník TTL na RS232

587 3 rs232 2

UART USB

Připojení USB - UART převodníku k Picu

pico uart

IMG 20240817 030138

Vodiče jsou zapojeny takto:

  • červený (+5V) — Pico VBUS (pin 40), budeme Pico zároveň napájet kabelem

  • černý (zem) — Pico GND (třeba pin 38)

  • bílý (TX) — Pico GP0 (pin 1)

  • zelený (RX) — Pico GP1 (pin 2)

Na straně PC pak můžeme komunikovat pohodlně pomocí třeba sudo minicom -b 115200 -c on -D /dev/ttyUSB0

Nastavení UARTu

Jsou dva základní přístupy k nastavení sériového zařízení, buď přímo nebo pomocí knihovny stdio. V mnoha případech potřebujete vědět jak fungují oba přístupy, začneme s funkcemi SDK, které nastavují UART.

Všechny funkce potřebují vědět adresu UARTu, který nastavujeme. Máme k dispozici dvě konstanty uart0 a uart1 a funkci, která převádí hardwarovou adresu na index 0 nebo 1.

static uint uart_get_index( uart_inst_t *uart )

Jednoduché nastavení je pomocí funkce:

uint uart_init( uart_inst_t *uart, uint baudrate )

Ukončení práce UARTu se provádí funkcí:

void uart_deinit( uart_inst_t *uart )

Funkce:

static bool uart_is_enabled( uart_inst_t *uart )

se používá k zjištění zda je příslušný UART nastaven.

Můžeme také samostatně nastavit rychlost:

uint uart_set_baudrate( uart_inst_t *uart, uint baudrate )

Kompletní nastavení UARTu, kde nastavujeme počet datových bitů, počet stop bitů a paritu je:

uart_set_format( uart_inst_t *uart, uint data_bits, uint stop_bits, uart_parity_t parity )

data_bits mohou mít hodnotu 5, 6, 7 nebo 8, stop_bits mohou být 1 nebo 2 a parity může být nastavena na jednu z těchot hodnot:

  • UART_PARITY_NONE — nepoužíváme paritní bit

  • UART_PARITY_EVEN — sudá parita

  • UART_PARITY_ODD — lichá parita

Někdy mohou být užitečné funkce pro nastavení RTS a CTS pinů, pokud je musíte použít ke komunikaci s hardwarem, který RTS a CTS potřebuje na hardwarové řízení toku.

uart_set_hw_flow( uart_inst_t *uart, boot cts, boot rts )

Je-li parametr cts nastaven na true potom jenom když je CTS linka na vysoké úrovni, tak se povoluje UARTu vysílat data, jinak se nepovoluje vysílat. Je-li parametr rts nastaven na true potom RTS linka bude na vysoké úrovni, když jsou k dispozici nějaká data k poslání. RTS na nízké úrovni znamená, že žádná data k poslání zatím nejsou k dispozici.

UART má na straně TX i RX vyrovnávací paměť (buffer) o velikosti 32 8bitových znaků. To je obvykle dostatečné na věci, které potřebujeme. Pokud potřebujeme okamžitě reagovat na jednotlivé znaky a bufferování vypneme, riskujeme, že můžeme některé znaky ztratit. Při vypnutí bufferu je v paměti jenom jeden znak a jakmile přijde další, tak se přepíše.

Funkce pro zapnutí buferování:

uart_set_fifo_enabled( uart_inst_t *uart, bool enabled )

Je-li parametr enabled true, je zapnutý buffer, pro enabled false je vypnutý buffer.

Nakonec můžeme ještě nastavit přerušení, které můžeme potřebovat jak pro příjem, tak i pro vysílání:

uart_set_irq_enables( uart_inst_t *uart, bool rx_has_data, bool tx_has_data )

Obluhu přerušení nastavíme funkcí:

irq_set_exclusive_handler( uartIRQ, handler); (1)
irq_set_anabled( uartIRQ, true ); (2)
1 Určení funkce obsluhy přerušení. Parametr handler je ukazatel na funkci obsluhy. Parametr uartIRQ může být UART0_IRQ nebo UART1_IRQ.
2 Zapnutí přerušení na UARTu. Parametr uartIRQ může být UART0_IRQ nebo UART1_IRQ.

Přenos dat

Poté co nastavíme UAER můžeme posílat nebo přijímat data pomocí funkcí z SDK.

uart_write_blocking( uart_inst_t *uart, const uint8_t *src, size_t len ) (1)
uart_read_blocking( uart_inst_t *uart, uint8_t *dst, size_t len )        (2)
1 Funkce pro posílání dat. Pošle len bajtů z pole src přes UART uart
2 Funkce pro příjem dat. Přijme len bajtů z UARTU uart a zapíše je do pole dst.

Obě dvě funkce mají data v poli bajtů. Ukazatel na začátek pole je src nebo dst. Je na programátorovi, aby zajistil dostatečnou velikost pole dst. Budeme-li posílat větší množství dat, může se stát, že se vysílací buffer UARTu zaplní a potom se funkce zablokuje, t.j. zastaví se do té doby, než se vysílací buffer vyprázdní a bude možné posílat další bajty. V případě funkce uart_read_blocking parametr len určuje, kolik bajtů má funkce přečíst. Pokud nebude UARTem přijato dostatek bajtů, funkce se opět zablokuje, t.j. bude čekat na další data. Návrat z funkce proběhne až tehdy, pokud bude přečteno len dat.

Smyčka
#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/gpio.h"

int main()
{
    stdio_init_all();
    uart_init( uart1, 9600 );
    gpio_set_function( 4, GPIO_FUNC_UART );
    gpio_set_function( 5, GPIO_FUNC_UART );
    uart_set_format( uart1, 8, 1, UART_PARITY_EVEN);

    uint8_t SendData[] = "Ahoj studenti";
    uint8_t RecData[20];
    uart_write_blocking( uart1, SendData, 14 );
    uart_read_blocking( uart1, RecData, 14 );
    RecData[14] = '\0';
    printf("%s", RecData);
}

Čtení a zápis znaků a řetězců

stdio

Pico má verzi Céčkové stdio knihovny a ta může být použita k posílání a příjem dat přes UART nebo přes USB port. Má také některá nestandardní rozšíření k jednodušší práci s hardware. Hlavní výhodou, kterou máme pokud použijeme stdio, je možnost použití funkce printf().

Práce s malými buffery

Příklad použití — komunikace dvou RaspberryPi Pico

Potřeboval jsem zkalibrovat teploměr s velmi nepřesným čidlem (RaspberryPi Pico s vestavěným termistorem) pomocí velmi přesného teploměru RaspberryPi Pico s čidlem BMP280. Zde je ukázka komunikace kalibrovaného teploměru s etalonovým teploměrem.

Kalibrovaný teploměr
    // nastavení UART
    uart_init(uart1,115200);
    gpio_set_function(8,GPIO_FUNC_UART); // TX
    gpio_set_function(9,GPIO_FUNC_UART); // RX
    uart_set_format(uart1, 8, 1, UART_PARITY_EVEN);

    // ...

    uint32_t raw=0;
    float temp=0;
    char txdata[] = "T";
    char rxdata[32];

    for(i=0;i<64;i++) {
       raw = cteni_teploty(); // čtení naměřené teploty
        nap[i] = (float) raw;
        // posílám žádost o získání teploty z etalonového teploměru
        uart_write_blocking(uart1,txdata,strlen(txdata));
        while( ! uart_is_readable(uart1) ) { // čekáme na odpověď
            gpio_put(25,1);
            sleep_ms(1);
            gpio_put(25,0);
            sleep_ms(1);
        }
#if TESTOVANI
        printf("Čtu etalon\n");
#endif
        // odpověď je připravená, nevíme jak bude dlouhá
        for( int j = 0; j<31 && uart_is_readable(uart1); j++) {
            uart_read_blocking(uart1,rxdata+j,1);
            rxdata[j+1] = '\0';
            if( rxdata[j] == '\n') {
                break;
            }
        }
        // v rxdata je teplota z etalonu jako řetězec znaků
        // převedeme z řetězce znaků na float
        temp = strtof(rxdata,NULL);
Etalonový teploměr
	// nastavení UART
	uart_init(uart1,115200);
	gpio_set_function(8, GPIO_FUNC_UART); // TX
	gpio_set_function(9, GPIO_FUNC_UART); // RX
	uart_set_format(uart1, 8, 1, UART_PARITY_EVEN);

    // ...

	if( kalibrace ) {
	    while( 1 ) {
#if TESTOVANI
		printf("Čekám na žádost o měření.\n");
#endif
		while(! uart_is_readable(uart1) ) {
		    // čekání na port a blikání LEDkou
		    gpio_put(25,1);
		    sleep_ms(1);
		    gpio_put(25,0);
		    sleep_ms(1);
		}
		// dostal jsem pokyn, změřím teplotu
		// čtení teploty z přesného čidla a konverze
    	bmp280_read_raw(&raw_temperature, &raw_pressure);
    	int32_t temperature = bmp280_convert_temp(raw_temperature, &params);
    	int32_t pressure = bmp280_convert_pressure(raw_pressure, raw_temperature, &params);
    	// přečtu 1 znak žádosti aby nezavazel v RX bufferu a zahodím ho
		uart_read_blocking(uart1, rcvdata, 1);
		// převod float hodnoty na řetězec znaků
		sprintf(txdata,"%f\n",temperature / 100.f);
		// poslání řetězce znaků na kalibrovaný teploměr
		uart_write_blocking(uart1, txdata, strlen(txdata));
#if TESTOVANI
		printf("Poslal jsem: Teplota = %.2f %cC\n", temperature / 100.f, 0xb0);
#endif
		}
    }

Shrnutí