Pojďme programovat elektroniku: Naše laserové ukazovátko už komunikuje rychleji než GPRS

  • Vzpomínáte na naše laserové blikátko?
  • V létě zvládlo rychlost 2 kb/s
  • Dnes dává i stovky kb/s

Žádný začínající bastlíř se neobejde bez multimetru, který jej nejednou zachrání před zkratem nebo třeba příliš vysokým napětím z vadného měniče. Funkce voltmetru poslouží jako kontrola, že je na pinech adekvátní napětí a připojená součástka jednoduše nefunguje, protože je vadná, a ampérmetr vám dá zase jasně najevo, proč jsou všechny ty titěrné svítící LED diody na každé druhé destičce hotové peklo, protože odebírají zbytečně vysoký proud, což pocítíte zejména v okamžiku, když začnete stavět obvody napájené malou baterií.

Dříve či později však základní multimetr přestane stačit – třeba v okamžiku, když začnete pátrat po tom, jak v nitru funguje hojně používaná pulzně-šířková modulace PWM, jednotlivé komunikační protokoly jako UART, SPI a I2C, nebo když se budete snažit z obvodu odfiltrovat všemožné ruchy.

Přesně pro tyto případy se hodí mít v domácí bastlírně i malý a spíše orientační osciloskop za pár stovek a logický analyzátor.

Ukázka levného osciloskopu v akci při analýze pulzně-šířkové modulace

Zatímco ty nejlevnější osciloskopy bez paměti a počítačového rozhraní zpravidla jen vykreslují v grafu aktuální elektrický signál, logický analyzátor s USB nahraje třeba předem stanovený časový úsek, signál zobrazí v aplikaci na počítači a vy jej můžete prakticky libovolně přibližovat, měřit a dále analyzovat.

K čemu je to dobré? Vzpomeňte si na náš díl z konce prázdnin, když jsme rozblikávali laserové ukazovátko a přenášeli data rychlostí několika kilobitů za sekundu. Zpráva „Ahoj“ se přenesla za zlomek času, pozorovatel si tedy jen stěží všiml slabého bliknutí.

Když bych však k pinu optického přijímače připojil logický analyzátor, na počítači by se v aplikaci vykreslila podrobná křivka elektrického signálu po přijetí zprávy, kterou bych mohl dále studovat.

Analyzátor za stovku

Dnes si to vyzkoušíme, jeden takový analyzátor mi totiž před pár dny dorazil poštou v typické žluté bublinkové obálce. Podle inzerátu na eBay se mělo jednat o osmikanálovou krabičku s rychlostí až 24 MHz (reálně dává 8 MHz) a hlavně kompatibilní s ovládacím softwarem od výrobce Saleae.

Klepněte pro větší obrázekKlepněte pro větší obrázekKlepněte pro větší obrázekKlepněte pro větší obrázek
Levný čínský klon osmikanálového analyzátoru Saleae

V Saleae podobné klony pochopitelně nevidí rádi, cena jejich značkových analyzátorů totiž začíná na 109 dolarech, zatímco mě čínský padělek přišel na necelých 140 korun. Pokud překousnete toto morální dilema, máte téměř vyhráno, samotný software je totiž k dispozici zdarma, a přestože Saleae právem hrozí, že se pokusí podobné klony blokovat, zatím fungují.

Klepněte pro větší obrázekKlepněte pro větší obrázekKlepněte pro větší obrázek
Program Saleae Logic zobrazí zaznamenaný digitální i analogový signál, umožní přibližování, měření i detekci hromady populárních protokolů. Program si můžete vyzkoušet i bez samotného analyzátoru, obsahuje totiž simulátor.

V obálce tedy dorazila krabička velikosti flešky. Na jednom konci trčí USB konektor pro spojení s počítačem a na druhém pak deset pinů pro klasickou bastlířskou kabeláž. Jelikož je analyzátor osmikanálový, může v jeden okamžik sbírat data až z osmi obvodů. Kdybych tedy chtěl na počítači vykreslit třeba křivku PWM modulace signálu, který bude nastavovat jas LED diody zapíchnuté do nepájivého pole, stačí k obvodu paralelně připojit analyzátor stejným způsobem jako voltmetr a spustit nahrávání.

Laserové ukazovátko na steroidech

Kvůli podobné prkotině si ale asi analyzátor nikdo kupovat nebude. Vrátím se tedy opět k tomu svému laserovému ukazovátku, které jsem v létě proměnil v primitivní vysílač s rychlostí asi 2 kb/s.

Pokud si pamatujete, blikátko přenášelo bity podobným způsobem, jak to dělá morseovka. Bit 0 tedy představoval záblesk o délce asi 100 mikrosekund a bit 1 pak záblesk o délce 200 mikrosekund. Mezi každým přenášeným bitem byla ještě 50us mezera a celou zprávu uvádělo a zakončovalo mnohem delší bliknutí, aby přijímač ve formě fotocitlivého senzoru věděl, že přenos skončil.

Klepněte pro větší obrázekKlepněte pro větší obrázekKlepněte pro větší obrázek
Obvod s laserovým ukazovátkem, fotocitlivým senzorem, logickým analyzátorem a dvěma blue pilly – levnými armovými mikrokontrolery, které dnes budou sloužit jako vysílač a přijímač.

Tentokrát na to půjdu tradiční cestou, svůj blikátkonet jsem totiž upravil tak, aby měly všechny pulzy stejnou délku. Záblesk světla tedy představuje bit s hodnotou 1 a tma pak bit s hodnotou 0.

Kdybych chtěl tímto způsobem přenést třeba znak A, stačí postupně zablikat řadu 0, 1, 0, 0, 0, 0, 0, 1, protože se jedná o binární reprezentaci čísla 65 a znak velkého A je v tabulce ASCII právě na 65. místě.

Nejprve ale musím přijímači dát vědět, že přicházejí nějaká data. Na začátku řady tedy bude ještě startovací bit s hodnotou 1: 1, 0, 1, 0, 0, 0, 0, 0, 1.

Jakmile tuto řadu zablikám, přijímač nejprve zachytí záblesk startovacího bitu, postupně přečte osm následujících stavu na signálovém vodiči fotocitlivého senzoru a uloží je do celého bajtu. Bude-li následovat další záblesk, přijímač si jej vyloží jako další startovací bit a uloží opět dalších osm stavů a získá druhý celý bajt, pak třetí, čtvrtý a tak dále, dokud nepřenesu celou zprávu.

No dobrá, ale jak zajistím vzájemnou časovou synchronizaci? Jednoduše řečeno, vysílač musí blikat se stejnou frekvencí, s jakou zase bude přijímač číst hodnoty. Použiji k tomu hardwarový časovač přímo na čipu a jednu z mnoha dostupných knihoven, která bude s předem stanovenou frekvencí volat speciální funkci, přičemž při každém průchodu se zpracuje jeden bit – jeden záblesk.

Klepněte pro větší obrázek
Přenesení jednoho bajtu, čísla 65, kterému v tabulce ASCII odpovídá velké A. Úvodní bit 1 dává přijímači vědět, že budou následovat data. Všimněte si, že šířka jednoho bitu je asi 50 mikrosekund, o což se stará hardwarový časovač na čipu.

Dejme tomu, že nastavím časovač na 50 mikrosekund. Moji odesílací funkci tedy bude volat v ideálním případě 20 000× za sekundu. Z toho plyne, že čip má zároveň nejvýše 50 us na to, aby zpracoval všechny instrukce uvnitř funkce. Musím zde přečíst bit z aktuálního bajtu, zjistit, jestli se jedná o nulu, nebo jedničku a podle toho zhasnout, nebo naopak rozsvítit laserové ukazovátko.

Takhle stále dokola, dokud se neodešle celá zpráva.

Na mikrokontroleru přijímače běží časovač s identickou konfigurací. Uvnitř funkce ale tentokrát čtu napětí na signálním pinu fotosenzoru. Když zachytí záblesk z laserového ukazovátka, napětí se lehce zvýší a čip mikrokontroleru jej vyhodnotí jako logickou jedničku, kterou přečtu a postupně z těchto bitů skládám celý bajt, pak druhý, třetí a tak dále, dokud nedorazí celá zpráva. Pak ji vypíšu třeba do sériové linky.

Klepněte pro větší obrázek
Když zaznamenaný signál vysílače a přijímače přiblížím, je patrné zpoždění asi 1 us mezi zvýšením napětí na laserovém ukazovátku a na fotocitlivém přijímači. V této mikrosekundě je obsažené jak samotné rozsvícení ukazovátka, tak zpracování optického signálu na straně přijímače.

Tento tradiční přístup má oproti mé binární morseovce z první části jednu podstatnou výhodu. Poměrně snadno mohu upravovat rychlost a hledat třeba tu nejvyšší, kdy bude přenos ještě spolehlivě fungovat. Stačí na vysílači a přijímači nastavit stejnou frekvenci časovače.

Levný a výkonný „blue pill“

Abych měl po ruce dostatek výkonu, změnil jsem ještě jednu drobnost. Destičky Arduino jsem nahradil armovými drobečky, tzv. blue pilly, STM32F103C8 s taktem 72 MHz a 20 kB RAM, které lze programovat v prostředí Arduino mimo jiné díky projektu STM32duino. Ačkoliv tyto čipy disponují oproti jednoduchým Atmelům v základních Arduinech mnohonásobně vyšším výkonem, kusová cena prototypovacích desek na eBayi se pohybuje okolo čtyřiceti korun.

Klepněte pro větší obrázekKlepněte pro větší obrázekKlepněte pro větší obrázek
Armový blue pill s 20 kB RAM, 64 kB flash a taktem 72 MHz. Cena? Na eBayi okolo padesátikoruny za kus.

Díky změně hardwaru a optimalizaci kódu, kdy k zápisu a čtení nepoužívám standardní a pro tyto účely pomalé funkce digitalWrite a digitalRead, ale rychlejší nastavování a čtení stavů pomocí registrů na čipu, jsem postupně zvýšil úspěšný přenos textového pozdravu z 2 kb/s až na stovky kb/s a postupně pokořil hromadu vyzyvatelů počínaje klasickým vytáčeným modemem, GPRS a nakonec i EDGE. A věřte, že když laserové ukazovátko blikne třeba 200 000× za sekundu (200 kb/s), přijímač tyto bliky zachytí a váš program přeloží na bajty, bude to stát za to.

Klepněte pro větší obrázek
A konečně přenesení celé zprávy AHOJ. V sériové lince vysílače i přijímače se pro kontrolu vypisují i jednotlivé bity, které můžete číst i v grafu logického analyzátoru.

Nakupte tedy hromadu bluepillů, laserová ukazovátka, fotocitlivé senzory a nakonec logický analyzátor. Bude vás to dohromady stát nejvýše pár stovek, ale jak už jsem v našem seriálu napsal mnohokrát, za to, co se naučíte, to rozhodně stojí.


Na závěr nesmí chybět tradičně kód vysílače a přijímače, i když je třeba připomenout, že tentokrát se jedná o desky s armovým čipem STM32F103C8 a kód s využitím knihoven projektu STM32duino.

Kód vysílače:

/* Hardwarovy casovac zpracuje odesilaci funkci kazdych 50 us
   Jedna se tedy o frekvenci/rychlost 20 000 bitu za sekundu
*/
#define CLOCK 50
// Maximalni delka zpravy v bajtech
#define MAX 1000

// Pole se zpravou
uint8_t buffer[MAX];
// Velikost zpravy
uint16_t size = 0;
// Pomocne stavove promenne
volatile bool sending = false;
volatile bool start = false;
// Pocitadlo aktualniho odesilaneho bajtu
uint16_t byteI = 0;
// Pocitadlo aktualniho odesilaneho bitu
uint8_t bitI = 0;

// Knihovna hardwaroveho casovace z projektu STM32duino
// Cip ma nekolik casovacu, ja pouziji ten prvni
HardwareTimer timer(1);

void setup() {
  // Nastartovani seriove linky
  Serial.begin(115200);
  // Pin PC14, ke kteremu je pripojene laserove ukazovatko
  pinMode(PC14, OUTPUT);

  // Konfigurace a nastartovani casovace
  // Po vyprseni 50 us spusti funkci tick
  timer.pause();
  timer.setPeriod(CLOCK);
  timer.setChannel1Mode(TIMER_OUTPUT_COMPARE);
  timer.setCompare(TIMER_CH1, 1);
  timer.attachCompare1Interrupt(tick);
  timer.refresh();
  timer.resume();
}

void loop() {
  if (Serial.available()) {
    /* Pokud po seriove lince prichazeji nejaka textova data, precti je
       az k zalomeni radku. Tridu String je vhodne pouzivat vyhradne
       na cipech s dostatkem pameti RAM, coz je muj pripad
    */
    String s = Serial.readStringUntil('\n');
    size = s.length();

    // prevedeni textoveho retezce na pole bajtu
    s.getBytes(buffer, size + 1);
    Serial.print("Posilam ");
    Serial.print(size);
    Serial.println(" B: ");
    Serial.println(s + "\n");

    // Vypsani tabulky s bity jednotlivych bajtu
    // Lze pouzit i vestavne makro bitRead
    for (uint16_t i = 0; i < size; i++) {
      for (uint8_t y = 0; y < 8; y++) {
        if (buffer[i] & (128 >> y)) {
          Serial.print('1');
        }
        else {
          Serial.print('0');
        }
      }
      if ((i + 1) % 8 == 0) Serial.println();
      else Serial.print(" ");
    }

    Serial.println('\n');

    /* Nastavenim stavovych promennych davam casovaci vedet,
       ze muze zacit odesilat data
    */
    sending = start = true;

    /* Cekam, dokud casovac ve funkci tick postupne vse
       neodesle a nezmeni hodnotu promenne sending
    */
    while (sending) {
      delay(10);
    }

    // Data jsou odeslana, vypisu statistiku
    Serial.print("Odesilani trvalo ");
    Serial.print((size * 9 * CLOCK) / 1000.0, 2);
    Serial.print(" ms! (");
    Serial.print(1e6 / CLOCK, 0);
    Serial.print(" Hz, 1 b = ");
    Serial.print(CLOCK);
    Serial.println(" us)\n");
  }
}

// Klicova funkce tick, kterou casovac spousti kazdych 50 mikrosekund
void tick() {
  /* Pokud je povolene odesilani a jeste jsem neodeslal startovaci bit,
      odeslu jej. Nepouzivam funkci digitalWrite, ale rychlejsi pristup
      primo k registrum cipu
  */
  if (sending) {
    if (start) {
      gpio_write_bit(GPIOC, 14, HIGH);
      start = false;
    }
    /* Pokud jsem uz startovaci bit odeslal,
        poslu nyni aktualni bit v rade a zvysim citac
    */
    else {
      if (bitI < 8) {
        /* Pomoci operatoru pro praci s bity zjistim,
           jestli ma bit na dane pozici hodnotu 1, nebo 0.
           Podle toho laserove uakzovatko bud rozsvitim, nebo zhasnu
        */
        if (buffer[byteI] & (128 >> bitI)) {
          gpio_write_bit(GPIOC, 14, HIGH);
        }
        else {
          gpio_write_bit(GPIOC, 14, LOW);
        }
        bitI++;
      }
      /* Uz jsem odeslal vsech 8 bitu aktualniho bajtu,
         Pokud zadny dalsi bajt neni v rade, ukoncim odesilani,
         nebo poslu opet startovaci bit a v dalsim cyklu
         zacnu odesilat dalsi bajt
      */
      else {
        bitI = 0;
        if (byteI == (size - 1)) {
          gpio_write_bit(GPIOC, 14, LOW);
          sending = false;
          byteI = 0;
        }
        else {
          gpio_write_bit(GPIOC, 14, HIGH);
          byteI++;
        }
      }
    }
  }
}

Kód přijímače:

// Rychlost hodin musi byt stejna jako u vysilace
#define CLOCK 50
#define MAX 1000

uint8_t buffer[MAX] = {0};
uint16_t size = 0;
uint8_t b = 0;
bool recording = false;
uint16_t byteI = 0;
uint8_t bitI = 0;
volatile bool incoming = false;

HardwareTimer timer(1);

void setup() {
  Serial.begin(115200);
  pinMode(PC14, INPUT);
  timer.pause();
  timer.setPeriod(CLOCK);
  timer.setChannel1Mode(TIMER_OUTPUT_COMPARE);
  timer.setCompare(TIMER_CH1, 1);
  timer.attachCompare1Interrupt(tick);
  timer.refresh();
  timer.resume();
}

/* V nekonecne smycce loop kontroluji stav promenne incoming.
    Pokud se zmeni na true,vim, ze jsem obdrzel nejaka data a
    vypisu je do seriove linky.
*/
void loop() {
  if (incoming) {
    incoming = false;
    Serial.print("Dorazilo ");
    Serial.print(size);
    Serial.print(" B (");
    Serial.print(1e6 / CLOCK, 0);
    Serial.print(" Hz, 1 b = ");
    Serial.print(CLOCK);
    Serial.println(" us):");

    for (uint16_t i = 0; i < size; i++) {
      Serial.print((char)buffer[i]);
    }

    Serial.println('\n');

    // Pro kontrolu opet vypisu tabulku s jednotlivymi bity
    for (uint16_t i = 0; i < size; i++) {
      for (uint8_t y = 0; y < 8; y++) {
        if (buffer[i] & (128 >> y)) {
          Serial.print('1');
        }
        else {
          Serial.print('0');
        }
      }
      if ((i + 1) % 8 == 0) Serial.println();
      else Serial.print(" ");
    }

    Serial.println('\n');
  }
}

/* Funkce tick tentokrat nebude odesilat data,
   ale bude je cist a ukladat
*/
void tick() {
  if (!recording) {
    /* Pokud nenahravam, ale na pinu PC14 se objevila logicka jednicka,
       bude to asi startovaci bit. Spoustim nahravani. Vsimnete si, ze
       opet nepouzivam pro cteni funkci digitalRead, ale stav pinu ctu
       z registru. Pozor, postup, jak cist stav GPIO z registru, se
       cip od cipu lisi. Toto plati pro desky z projektu STM32duino.
    */
    if (GPIOC->regs->IDR & (1 << 14)) {
      recording = true;
    }
  }
  // Pokud nahravam, prichozi bity postupne ukladam do bajtu
  else {
    if (bitI < 8) {
      if (GPIOC->regs->IDR & (1 << 14)) {
        // Pro nastaveni bitu v bajtu bych mohl pouzit funkci bitSet
        b |= (128 >> bitI);
      }
      bitI++;
    }
    else {
      buffer[byteI] = b;
      b = 0;
      bitI = 0;
      byteI++;
      if (!(GPIOC->regs->IDR & (1 << 14))) {
        size = byteI;
        byteI = 0;
        recording = false;
        incoming = true;
      }
    }
  }
}

Témata článku: Pojďme programovat elektroniku, Programování, Arduino, ARM, C++, Laser, Nepájivé pole, STM, Komunikační protokol, Serial read, Hotové peklo, Celá zpráva, Armový čip, Buffer, Stanovený čas, Binární, Leaf, #define, Laserový vysílač, Modulace, Setup, Analogový, Ovládací software, Vodič

Určitě si přečtěte


Aktuální číslo časopisu Computer

26 procesorů v důkladném testu

Zhodnotili jsme 18 bezdrátových reproduktorů

Jak fungují cash back služby?

Pohlídejte své děti na internetu