V minulém článku STM32F0 a minimalizace kódu v C++ jsem se snažil ukázat, že C++ lze použít i pro malé uC stejně jako čisté C. Zde se pokusím tuto myšlenku rozvést trochu dál, ukážu použití kompozice, dědičnosti, polymorfizmu a jednoduché šablony. Objekty jsou i nadále statické.


Úvodem.

Ani zde se nebudeme snažit vytvářet objekty dynamicky. To má opravdu moc velkou režii a do malého uC se to prostě nehodí. Zdálo by se tedy, že tím padá hlavní výhoda použití C++. Ale podařilo se najít aplikaci, kde se nadstavba C++ s výhodou využije i se statickými objekty - protokolový stack, zpracování toku dat ve vrstvách, přičemž každá vrstva zajišťuje část funkcionality, nezávislou na vrstvách ostatních. To je úloha, která je v čistém C docela nepřehledná i když je to dobře navrženo. Příkladem může být TCP/IP, ale to už je moc složité, bez toho se obejdeme.

Vlastně zde nebudeme stavět úplný stack, pouze jednotlivé vrstvy napojíme na sebe vnořeným voláním, což sice více vyčerpá zásobník, ale je to mnohem jednodušší. Správně by bylo lépe postavit stack jako novou třídu a metody jednotlivých vrstev volat v cyklu. To se dobře dělá pod operačním systémem, kde operace čtení blokuje vlákno, zde však obsluhujeme přerušení a v něm vlastně probíhá celé zpracování dat. To vypadá na první pohled při příjmu dat na USARTu jako blbost, ale ne tak úplně. Fakticky postupujeme jeden přijatý znak jen o jednu vrstvu výše, tam se pak vyhodnotí celý rámec a až na jeho konci se provede nějaká akce, která může proces o něco zdržet. Pokud je protokol navržen tak, aby zde druhá strana čekala na odpověď, zdržení v přerušení nevadí, protože v tomto čase data nepřicházejí. Takže pokud se protokol přizpůsobí tomuto schématu (což ve velké části případů lze), je možné tento jednoduchý způsob použít. Pokud ne, je potřeba použít další Fifo, což věc dost zkomplikuje. Mám na tomto principu postaven uživatelský bootloader pro tento procesor (v čistém C) a funguje spolehlivě, i když v přerušení od příjmu USARTu čeká na vymazání bloku Flash, což opravdu chvíli trvá. A protože to v C není moc přehledné, zkouším to tímto způsobem přepsat do C++. To jen tak na okraj, tohle tedy není nějaký samoúčelný příklad, mělo by to být v budoucnu prakticky využito.

Popis činnosti programu.

Zde ukážeme jen velmi zjednodušený protokol na sériovém portu, který nebude dělat nic jiného, než že (opět na STM32F0 Discovery):

Pokud přijde zvějšku po USARTu do uC znak 'G' , pak rozsvítí zelenou led (ledg)
Pokud přijde zvějšku po USARTu do uC znak 'g' , pak zhasne zelenou led (ledg)
Pokud přijde zvějšku po USARTu do uC znak 'B' , pak rozbliká modrou led (ledb)
Pokud přijde zvějšku po USARTu do uC znak 'b' , pak zhasne modrou led (ledb)
Pokud zmáčkneme tlačítko, bude USARTem posílán střídavě znak 'B' a po dalším stisku 'b' atd.

Po resetu pošleme na USART obvyklý string "Hello World" jako důkaz, že to žije.

Takže pokud propojíme TxD a RxD (piny GPIOA9 a GPIOA10) - loopback - budeme mačkáním tlačítka střídavě spouštět a zastavovat blikání modré LED. Obrázek napoví více:

Čísla v levém sloupci budeme nadále používat jako odkazy na jednotlivé složky software. V main.cpp je pak následující:

#include "main.h"
static  CommandProcessor  command (true);       // obrázek - 1, 2
static  Usart1Class       serial  (9600);       // obrázek - 4, 5, 6
 
void SysTick_Handler (void) {
  command.Blinking();
}

int main(void) {
  SystemInit ();
  SystemCoreClockUpdate();          // Potřebné pro USART
  static_init();                    // Zde zavolám globální konstruktory
  // serial.SetHalfDuplex(true);    // Případná další nastavení
  // Top += ... += Bottom;          // Postav "Stack"
  command += serial;

  const char str[] = "Hello World\r\n";
  serial.Down ((char*) str, sizeof(str) - 1);
  // command.Down ((char*) str, sizeof(str) - 1);  // Taky možnost, ale delší kód
 
  // Zavedu SysTick
  SysTick_Config (4800000);         // po 100 ms
  // A spustím nekonečnou smyčku
  for (;;) {
    command.Pass ();
    __WFI(); // procházíme v rytmu systicku, zde probouzí ještě USART
  }
  return 0;
}


Probereme to trochu důkladněji. Základem je třída BaseLayer, její metody Up() a Down() a operátor += začleňující třídu do "stacku".

class BaseLayer {
  public:
    BaseLayer () {
      pUp   = NULL;
      pDown = NULL;
    };
    virtual uint32_t    Up   (char* data, uint32_t len) {
      if (pUp) return pUp->Up (data, len);
      return 0;
    };
    virtual uint32_t    Down (char* data, uint32_t len) {
      if (pDown) return pDown->Down (data, len);
      return 0;
    };
    virtual BaseLayer&  operator += (BaseLayer& bl) {
      bl.setUp (this);  // ta spodní bude volat při Up tuto třídu
      setDown  (&bl );  // a tato třída bude volat při Down tu spodní
      return *this;
    };
    BaseLayer* getDown (void) const { return pDown; };
  protected:
    void setUp   (BaseLayer* p) { pUp   = p; };
    void setDown (BaseLayer* p) { pDown = p; };
  private:
    // Ono to je vlastně oboustranně vázaný spojový seznam.
    BaseLayer*  pUp;        
    BaseLayer*  pDown;      
};


Z tohoto jsou odvozeny třídy Usart1Class (instance serial 4,5,6) a CommandProcessor (instance command 1,2) - na obrázku v modrém rámečku. Mezivrstva 3 zde není použita, na obrázku je jen jako náznak, jak se vrstvy řadí nad sebe.

1, 2. CommandProcessor

1. GpioClass instance ledb, ledg, button.

V zásadě stejné jako v minulém článku, jen jsem zkusil přetížit operátory, což je celkem zbytečné, ale funguje to.

   const GpioClass& operator+ (void) const {
      io->BSRR = pos;
      return *this;
    }
    const GpioClass& operator- (void) const {
      io->BRR  = pos;
      return *this;
    }
    const GpioClass& operator~ (void) const {
      io->ODR ^= pos;
      return *this;
    };
    const GpioClass& operator>> (bool& b) const {
      b = get();
      return *this;
    }


Návratové hodnoty jsou v tomto případě zase nanic, umožňují však řetězení, takže je možné napsat např.

  +-+-+-led;


a máme na led 3 pulsy. Všechny metody jsou konstantní, protože nemění data uvnitř třídy. Vlastně ani nemohou, protože data jsou konstantní.

2. Vlastní třída CommandProcessor, instance command.

Všechny instance GpioClass, tedy ledb, ledg, button jsou zapouzdřeny právě sem.

uint32_t CommandProcessor::Up (char* data, uint32_t len) {
  uint32_t res;
  // Ono to sem sice leze po 1 bytu, ale uděláme to obecně.
  for (res=0; res<len; res++) {
    char c = data[res];
    switch (c) {
      case 'b': blink = false; break;     // Vypne blikání
      case 'B': blink = true;  break;     // Zapne blikání
      case 'g': -ledg;         break;     // Zhasni  zelenou
      case 'G': +ledg;         break;     // Rozsviť zelenou
      default: break;
    }
  }
  // Protože je to Top, je to celkem zbytečné, ale pro názornost
  return BaseLayer::Up (data, res);
}
void CommandProcessor::Blinking (void) {
  if (blink) ~ledb;    // bude blikat modrou ledkou
  else       -ledb;    // nebo jí zhasne
}
void CommandProcessor::Pass (void) {
  bool b;
  butt >> b;    // Načtení stavu tlačítka - tohle přetížení operátoru je blbost, ukázka, že to jde.
  // Konečný automat  tlačítka - stisk změní stav
  switch (status) {
    case s0: if ( b) { status = s1; On();  } break;
    case s1: if (!b)   status = s2;          break;
    case s2: if ( b) { status = s3; Off(); } break;
    case s3: if (!b)   status = s0;          break;
  }
}
// A nakonec vlastní volání metody Down()
void CommandProcessor::On  (void) {
  BaseLayer::Down((char*)"B", 1);
}
void CommandProcessor::Off (void) {
  BaseLayer::Down((char*)"b", 1);
}


3. Uživatelská vrstva Process.

Není v příkladu použita. Právě zde se budou zpracovávat data. Pokud si přetížíme Up() a Down() ve vlastní třídě, lze s daty dělat různé operace. Metody nejsou konstantní, ani nemusí vracet to, co do nich přijde. Takže je možné ořezávat a vkládat hlavičky, v příčce (označené Optional) je možné generovat např. potvrzení. Jako jednoduchý příklad je v adresáři test uvedena třída Echo - generuje (právě v příčce) lokální echo v terminálovém programu.

   uint32_t  Up (char* data, uint32_t len) {
      Down  (data, len);
      return BaseLayer::Up (data, len);
    };


Mimochodem - v tomto adresáři najdete trochu více o tom, jak třídy řadit za sebe a jak je ladit přímo na PC a teprve pak přesouvat do uC. Je to ale psáno pro Linux.

4, 5, 6. Usart.

4. Ukazuje použití jak dědičnosti - dědí vlastnosti třídy BaseLayer, tak polymorfizmu, protože BaseLayer používá virtuální veřejné metody pro začlenění do stacku. Kromě toho je sem zapouzdřena (kompozice) i třída Fifo, což je šablona. Takže deklarace na několika řádcích obsahuje snad všechny vymoženosti, které C++ nabízí a přitom bude generovat smysluplný a celkem použitelný kód.

class Usart1Class : public BaseLayer {
  public:
    Usart1Class         (uint32_t baud);
    uint32_t    Down    (char* data, uint32_t len);
    void        SetHalfDuplex (bool on) const;
  public:   // necháme veřejné
    Fifo<char>  tx;
};


Není to psáno obecně jako třeba periferní knihovny od ST, ale to je úmysl, kód by měl být co nejmenší a přesto opakovatelně použitelný. Prakticky je potřeba opravdu nastavit jen přenosovou rychlost, málokdy je potřeba měnit formát 8N1 nebo zapínat hardware flow control. A USART na tomto procesoru je tak složitý, že prakticky všechny jeho speciální módy činnosti knihovna ST stejně neumí postihnout. Jako příklad nastavení vlastností portu je přidána metoda SetHalfDuplex(bool) - podobných metod by se pak dalo do třídy jako knihovny přidat mnoho aniž by zbytečně rostla délka kódu - garbage collector (pokud je zapnutý) nepoužité metody odstraní.

5. Fifo použité v obsluze sériového portu zkusíme napsat jako šablonu.

template <class T> class Fifo {
  public:   // veřejné metody
    Fifo () { rdi = 0; wri = 0; len = 0; };
    bool Write (T& c) {
      if (len < FIFODEPTH) { buf [wri++] = c; saturate (wri); safeInc(); return true; }
      else return false;
    };
    bool Read  (T& c) {
      if (len > 0) {     c = buf [rdi++];     saturate (rdi); safeDec(); return true; }
      else return false;
    };
  protected:  // chráněné metody
    void saturate (volatile int& index) {
      index &= FIFOMASK;  // FIFODEPTH je rovno 2 ** n, kde n je celé číslo, rychlejší
      // if (index < FIFODEPTH) return; index = 0; // FIFODEPTH obecně int, netestováno
    };
    void safeInc (void) { Lock(); len++; UnLock(); };
    void safeDec (void) { Lock(); len--; UnLock(); };
  private:    // privátní data
    T            buf[FIFODEPTH];  
    volatile int rdi, wri, len;  
};


Fakt je, že v tomto případě kód neroste proti případu, že by místo T bylo použito přímo char. Zřejmě je překladač docela inteligentní. A jde tak ukládat do fifo různá data, včetně složitých objektů. Fifo je tak jednoduché, že může mít všechny metody v hlavičce, tedy default inline. Je však třeba zajistit atomičnost inkrementace a dekrementace délky dat. Použita je metoda se zákazem přerušení a snad funguje.

   static inline void Lock (void) {
      asm volatile ("cpsid i");
    }
    static inline void UnLock (void) {
      asm volatile ("cpsie i");
    }


Ona totiž pravděpodobnost, že se proces v přerušení zrovna trefí do té in(de)krementace je tak malá, že je problém to otestovat na selhání, takže to celkem spolehlivě funguje i bez ošetření té bezpečnosti. A u některých architektur je inkrementace či dekrementace buňky v paměti atomická v principu. U této však nikoli. Viz :

     ...    safeDec();  ...
    8000364:       b672            cpsid   i
    8000366:       6d44            ldr     r4, [r0, #84]   ; 0x54
    8000368:       3c01            subs    r4, #1
    800036a:       6544            str     r4, [r0, #84]   ; 0x54
    8000328:       b662            cpsie   i
 


6. Obsluha přerušení USARTu.

// V přerušení musíme třídu nějak zpřístupnit
// Protože může být jediná instance, nebudeme to komplikovat (nastavíme v konstruktoru).
static Usart1Class* pUsart1Instance;
// A jeho obsluha přerušení.
void USART1_IRQHandler (void) {
  if (!pUsart1Instance) return;
  volatile register uint32_t status;
  char rdata, tdata;
 
  status = USART1->ISR;                         // načti status přerušení
  if (status & USART_ISR_TXE) {                 // od vysílače
    if (pUsart1Instance->tx.Read (tdata))       // pokud máme data
      USART1->TDR = (uint32_t) tdata & 0xFF;      // zapíšeme do výstupu
    else                                        // pokud ne
      USART1->CR1 &= ~USART_CR1_TXEIE;            // je nutné zakázat přerušení od vysílače
  }
  if (status & USART_ISR_RXNE) {                // od přijímače
    rdata = (USART1->RDR) & 0xFF;               // načteme data
    pUsart1Instance->Up (&rdata, 1);            // a pošleme dál
  }
}


No a tím jsme se prokousali až na konec. Taková blbost a co to dalo práce, že. V C-čku a s použitím ST periferních knihoven by to bylo za chvíli. Jenže naklikat si vlastnosti projektu v IDE - hlavně že je hotovo a nevidět pod povrch věcí - to není cesta po které bych se chtěl vydat. Dnešní uC jsou sice dost složité, ale vidět trochu do střev jak hardware, tak nástrojů pro jeho ovládání se vyplatí. Přinejmenším při hledání chyb si tím ušetříte spoustu času.

Závěr.

Funguje to s gcc verze 4.7.2 (vlastní překlad) i 4.7.4 ze stránek launchpadu. Kód není proti programu podobné složitosti v čistém C o moc větší. I když je nutné konstatovat, že datové struktury použitím virtuálních metod opravdu trochu bobtnají a program si i sáhne do knihovny - zde používá memcpy() a u Cortex-M0 pochopitelně i celočíselné dělení.

Dalším problémem C++ je už poměrně velká komplikovanost jazyka samotného. Překladač toho sice dost uhlídá, ale není všemocný takže mohou vzniknout chyby, které jsou v čistém C neznámé. I z tohoto poměrně malého kousku kódu je patrné, že to není nic moc jednoduchého. Závěr bych z toho zatím nedělal, chce to dopsat alespoň ten bootloader. Ale vypadá to tak, že to budu používat. Ty periferní knihovny od ST se mi stejně moc nelíbily. Chápu sice důvod, proč jsou udělány tak, jak jsou udělány, ale při pohledu třeba na konkurenční NXP je vidět, že to jde udělat i jednodušeji. I když zase NXP bych za příklad moc nedával, tam zase dovedli jednoduchost ad absurdum, takže je to naopak naprosto nesrozumitelné. Tím mám na mysli nepoužívaní maker nebo enum typů pro definici bitů v registrech. Prostě to chce nějaký rozumný kompromis.

 arm-none-eabi-size firmware.elf
    text    data     bss     dec     hex filename
    1944      20     132    2096     830 firmware.elf