C vs C++ en sistemes encastats

En aquest blog s’ha treballat exclusivament en llenguatge C (versió C99) i no s’ha parlat res de C++. Anem a fer-ho ara en aquest capítol.

La discussió sobre usar o no C++ en sistemes encastats deu ser tant antiga com l’aparició d’aquest llenguatge orientat a objectes. Si bé als seus inicis el llenguatge presentava força problemes, ja fa molts anys que és un llenguatge estable i candidat a ser usat en sistemes encastats. Tot i això, la seva popularitat ha estat desigual i encara hi ha molts equips de desenvolupadors de sistemes encastats que treballen exclusivament en C.

Els problemes habituals que s’ha acusat al C++ per no fer-lo servir en sistemes encastats són els següents:

  • codi més llarg: si bé això pot ser veritat, les mides de les memòries FLASH dels microcontroladors és cada cop més gran i els compiladors moderns generen codi força optimitzat, a més que es poden desactivar opcions del llenguatge que no es fan servir.
  • més lent: això era cert amb els primers compiladors de C++, però actualment el codi generat és de la mateixa qualitat que el generat pels compiladors de C.
  • més stack: seguint les mateixes normes que amb C, és possible tenir codi C++ que faci un ús correcte de l’stack

En canvi, els avantatges que ens pot proporcionar treballar amb C++ poden ser:

  • comprovació de tipus en temps de compilació. C és força laxe en aquest tema, i això pot conduir a errors. C++ és capaç de fer comprovacions en temps de compilació per avaluar la correcció de les conversions.
  • namespaces, que permeten classificar i organitzar el codi d’una forma intuïtiva i senzilla.
  • constructors i destructors permeten inicialitzar i destruir o netejar estructures de forma automàtica.
  • orientació a objectes, l’organització del codi en objectes pot ajudar a ordenar i simplificar el codi.
  • sobrecàrrega d’operadors, fent que operacions entre objectes sigui senzilla amb un codi resultant força senzill.

També cal recordar que no cal fer servir totes les noves capacitats de C++ respecte a C de cop, si no que es poden anar incorporant poc a poc al nostre codi conforme anem guanyant experiència i coneixements.

Dues de les característiques de C++ que ocupen força memòria són el RTTI i el control d’excepcions. RTTI dona informació del tipus de classes polimòrfiques (que tenen almenys un mètode virtual) i és una característica que es faci servir gaire en sistemes encastats. El control d’excepcions permet l’execució d’un mètode i capturar l’error que es pugui generar i tractar-lo fora de la funció i de forma controlada.

Aquestes dues característiques de C++ afegeixen força codi a qualsevol projecte amb el que treballem, fent que, per exemple, no puguem compilar un simple “Hello World embedded” per la nostra placa de desenvolupament ja que ocupa massa FLASH. Les opcions per deshabilitar aquestes funcions al compilador GNU (que és el compilador utilitza Simplicity Studio) son:

-fno-rtti -fno-exceptions
CXX_options.png
Configuració Simplicity Studio per deshabilitar RTTI i les excepcions

Primer exemple en C++

L’exemple CXX_1 és el típic “Hello World” per sistemes encastats escrit en C++.

Aquest exemple fa servir dues classes dins el namespace BSP.

LED

Com el seu nom indica, serveix per controlar l’únic LED de la PCB de prototipat. Està basada en una classe amb tres mètodes senzills per controlar un sol LED (LED::On(), LED::Off(), LED::Toggle()).
Dins el constructor s’activa el rellotge pel perifèric GPIO i es configura el pin corresponent al LED de la PCB:

LED::LED() {
  CMU_ClockEnable(cmuClock_GPIO, true);
  GPIO_PinModeSet(gpioPortD, 7, gpioModePushPullDrive, 0); /* LED */
}
...
void LED::On() {
  GPIO_PinOutSet(gpioPortD, 7);
}

Button

Aquesta classe gestiona el valor d’una entrada del GPIO d’una fora senzilla, la classe Button emmagatzema els paràmetres d’un pin d’E/S i abstreu les crides a la biblioteca emlib de Silicon Labs:

Button::Button(GPIO_Port_TypeDef port, int pin, bool pull, bool pullup) {

  CMU_ClockEnable(cmuClock_GPIO, true);

  m_port = port;
  m_pin = pin;
  m_pull = pull;
  m_pullup = pullup;

  if (m_pull == false) {
    GPIO_PinModeSet(port, pin, gpioModeInput, 0);
  } else {
    if (m_pullup == true) {
      GPIO_PinModeSet(port, pin, gpioModeInputPull, 1);
    } else {
      GPIO_PinModeSet(port, pin, gpioModeInputPull, 0);
    }
  }
}

bool Button::getValue() {
  unsigned int pin_value;

  pin_value = GPIO_PinInGet(m_port, m_pin);
  if (pin_value == 0) {
    return false;
  } else {
    return true;
  }
}

Un Hello World “més C++”

A continuació modifiquem l’exemple per donar-li una volta més i que sigui més “estil C++” (està al repositori). El que s’ha fet ha estat crear una nova classe Pin que abstrau la informació d’un pin GPIO d’EFM32. La classe Button fa servir Pin per obtenir les característiques del GPIO a controlar.

Mida dels executables

A l’exemple CXX_1 tenim el “Hello World embedded” fet en C++ de manera bàsica. A l’exemple CXX_2 s’ha fet una implementació “més C++” amb la mateixa funcionalitat. A la Taula es pot veure la quantitat de memòria de tot tipus que necessiten les dues aplicacions així com l’exemple bàsic en C.

Com a curiositat, l’ús de std::cout de la biblioteca iostream i l’operador << afegeix uns 150KB de codi FLASH (!!!), fent que sigui poc recomanable o impossible de fer servir en un sistema encastat actual.

Aplicació text data bss
GPIO_1 972 108 28
CXX_1 1836 112 32
CXX_2 2076 112 32

Un driver en C++

Com hem vist al llarg del blog, bona part del codi són drivers per controlar els diferents perifèrics o dispositius del nostre sistema encastat. Si treballem en C++, caldrà que aquest drivers els fem també en C++. Veurem ara un exemple amb la UART, escrivint un driver i un exemple igual al vist a l’exemple UART.

En aquest exemple tenim una classe UART que és la implementació del driver per la UART que es va veure a l’exemple de la UART Aquesta classe UART fa servir buffers circulars per emmagatzemar les dades que es reben o s’han d’enviar per la UART i té els mètodes AvailableData(), GetData()} i SendData() com ja tenia el mòdul UART de l’exemple en C. Aquests mètodes tant sols accedeixen al buffer circular adequat (de transmissió o recepció) que està implementat a la classe CircularBuffer.

Tal com es veu al Llistat s’ha sobrecarregat l’operador << per fer més fàcil l’ús de la classe a l’hora d’enviar dades i poder escriure codi com el del Llistat~\ref{operator_UARTCXX_example}.

class UART {
  ...
  UART& operator<<(char* str) {     for(char* it = str; *it; ++it) {       this->Tx(*it);
    }
    return *this;
  }

  UART& operator<<(std::string str) {     for(std::string::iterator it = str.begin(); it != str.end(); ++it) {       this->Tx(*it);
    }
    return *this;
  }
  ...

  void UART::Tx(unsigned char c) const {
    USART_Tx(m_uart, c);
  }
  ...
}

La resta del codi és prou autoexplicatiu a excepció de l’implementació de les ISR de la UART. En aquest cas ens trobem que les ISRs haurien d’estar encapsulades dins la pròpia classe UART però això no és possible, donat que la classe no és estàtica, i per tant “no existeix” fins que no es crea instanciant un objecte d’aquest tipus. Una possible solució a aquest problema és el que es veu al codi:

void UART::USART1_TX_IRQHandler(void) {
  USART_IntClear( USART1, USART_IEN_TXC);
  Send();
}

void UART::USART1_RX_IRQHandler(void) {
  char data;

  if (USART1->IF & LEUART_IF_RXDATAV) {
    data = USART_Rx(USART1);
    m_RX.PushData(data);
    USART_IntClear( USART1, USART_IEN_RXDATAV);
  }
}

class UART {
...
  friend void USART1_TX_IRQHandler();
  friend void USART1_RX_IRQHandler();

private:
  void USART1_TX_IRQHandler(void);
  void USART1_RX_IRQHandler(void);
...
}

Es té el codi pròpiament dit de la ISR a uns mètodes privats de la classe del driver (en aquest cas la classe UART) i en algun altre lloc del codi (en aquest exemple al fitxer main s’insereix la construcció que es veu al Llistat. D’aquesta manera les ISR criden als mètodes adequats de la classe pertinent.

static UART* helper_uart;

void USART1_TX_IRQHandler() {
  helper_uart->USART1_TX_IRQHandler();
}

void USART1_RX_IRQHandler() {
  helper_uart->USART1_RX_IRQHandler();
}

Ocupació de memòria

De nou, anem a analitzar l’espai de memòria necessari per aquest exemple comparat amb l’exemple escrit en C amb la mateixa funcionalitat.

El codi en C++ es compila amb 3 variants:

  • Sobrecarregant l’operador << que pugui rebre dades de tipus char.
  • Sobrecarregant l’operador << que pugui rebre dades de tipus std::string.
  • Sense sobrecarregar l’operador.

Els resultats es mostren a la Taula. Es pot veure que l’ús de l’operador que suporta std::string afegeix força codi ROM (segona columna a la Taula, uns 2 KB) i que, en general, l’ús de C++ afegeix un sobrecost en espai ROM al nostre codi. Potser el més destacable és que la quantitat de RAM necessària no s’incrementa de manera significativa, sent aquest recurs el més escàs en un microcontrolador.

184

Aplicació text data bss
Sense operador << 4636 120 40
Amb operador << i char 4644 120 40
Amb operador << i string 6796 128 168
Original en C 2620 116

Conclusions

Tot i que l’ús de C++ enlloc de C incrementa la mida de l’executable final i les seves necessitats de memòria, el seu ús pot estar justificat en casos on l’encapsulació que proporciona C++ ajudi a la claredat del codi o a la portabilitat del mateix a diferents plataformes.

En qualsevol cas, cal una expertesa en el llenguatge per fer-ne un bon ús per tenir en compte les particularitats d’escriure codi C++ per sistemes encastats.

Per saber-ne més

 

Anuncis

Deixa un comentari

Fill in your details below or click an icon to log in:

WordPress.com Logo

Esteu comentant fent servir el compte WordPress.com. Log Out /  Canvia )

Google photo

Esteu comentant fent servir el compte Google. Log Out /  Canvia )

Twitter picture

Esteu comentant fent servir el compte Twitter. Log Out /  Canvia )

Facebook photo

Esteu comentant fent servir el compte Facebook. Log Out /  Canvia )

S'està connectant a %s

Aquest lloc utilitza Akismet per reduir els comentaris brossa. Apreneu com es processen les dades dels comentaris.