Treballant amb C++

Sobre la qüestió de si es pot treballar en C++ per sistemes encastats, ja en vem parlar en aquesta entrada. Després de mirar-me uns quants videos, blogs i i xerrades (Video1, Video2, Xerrada1, Blog) vaig veure que hi ha gent treballant en quest tema i que sembla que és possible tenir una aplicació encastada escrita en C++.

El primer en adonar-me’n va ser que tenir codi complicat en C++ no vol dir que s’acabi generant codi assemblador especialment complicat, ja que el compilador fa una molt bona feina optimitzant el codi i extraient la lògica que hi ha.

L’altra és que als exemples que posen l’ús de memòria RAM és mínim (molts cops és de zero bytes!!) i la quantitat de FLASH necessària no és especialment més gran que la que tindríem amb una implementació en C.

Així doncs, per intentar fer una mena de comparativa i fer una prova més completa que no pas la que vaig fer a l’article anterior, vaig decidir fer una aplicació més complexa i completa tant en C com en C++ per tal de poder comparar els resultats. La versió en C++ es troba en aquest enllaç i la versió en C en aquest.

Vull fer menció especial a l’eina Compiler Explorer que és essencial per anar fent proves de concepte de diferents implementacions i veure quin codi genera el compilador.

L’aplicació

L’aplicació que he triat l’he basat en un dispositiu que llegeix un sensor I2C de forma periòdica (cada X segons) i activa unes sortides segons uns llindars màxims i mínims de dita lectura. A més el dispositiu també és un esclau MODBUS (enllaç a explicació del protocol) per on es pot accedir a les dades del sensor, modificar els llindars i el temps de la lectura, etc.

Tot i ser una aplicació senzilla, necessita d’uns quants perifèrics del sistema (UART, I2C, Timers, GPIOs) i te una part programàtica (la implementació del protocol MODBUS, tot i que és força senzill).

El sensor triat torna a ser el sensor de proximitat per infraroigs ADPS9960 que ja vem fer servir a l’exemple d’una aplicació completa. És un sensor amb un bus I2C i força senzill de controlar que ja ens serveix per l’aplicació que estem fent. Recordar que la dada que ens dona és un enter sense signe de 8 bits proporcional a la distància a la que es troba l’objecte.

El protocol MODBUS el que permet és publicar una sèrie de registres que s’hi pot accedir per llegir o escriure. Del protocol implementarem la versió RTU que utilitza un port sèrie i codificació binaria per enviar diferents paquets de petició de lectura o escriptura. També farem que segons quina petició de lectura o d’escriptura provoquin accions dins el nostre dispositiu: per exemple, quan s’escrigui a un registre concret, provoqui una lectura del sensor i una actualització del registre amb la distància. A la banda del PC faré servir el programa QModBus (enllaç a github).

AdreçaNom registreAccésComentari
1Distància llegidaRValor en cm (?)
2Threshold lowR/WValor en cm (?)
3Threshold highR/WValor en cm (?)
Duty cycleR/WValor en segons (1 seg)
5OffsetRValor en counts (-, +) a C2
6
7Llegir distanciaWProvoca una lectura del sensor -> Registre 1
8LED On/OffR/W0 LED Off, 1 LED On
9Relay 1R/W0 Sortida Off, 1 Sortida On
10Relay 2R/W0 Sortida Off, 1 Sortida On
Mapa de registres publicats al MODBUS

Per últim, volem que el dispositiu faci una lectura del sensor cada X temps i la resta de temps es mantingui en baix consum. Això ho farem amb un Timer que generi una interrupció cada aquests X segons. Durant la resta del temps, el Timer estarà funcionant i tota la resta del microcontrollador romandrà en sleep mode. Per motius de baix consum, farem servir el timer de baix consum LETIMER i la UART de baix consum LEUART. Tots dos dispositiu estaran activats i despertaran el processador en cas de que s’arribi al temps establert o es rebi una petició MODBUS pel port sèrie.

Sensor

Com ja hem dit, el sensor que utilitzarem és el ADPS-9960 que ja hem utilitzat en altres ocasions.

Per la versió en C de l’aplicació he reutilitzat bona part del codi de l’aplicació ja feta i només te alguns canvis menors. Només hi tinc les funcions necessàries per llegir el registre en qüestió del sensor i poc més. Hi he afegit una funció de callback que es cridarà en quan s’escrigui al registre MODBUS (registre 7) i que ha de provocar una lectura del sensor i una actualització del registre 1.

void APDS9960Distance_cb(int reg_num) {
    uint8_t aux;

    aux = ADPS9960_GetDistance();
    register_direct_write (REGISTER_DISTANCE, aux);
}

En aquesta implementació que he fet, la biblioteca pel sensor incorpora la gestió del bus I2C internament. Això ho ho fet per simplicitat, però si s’hagués de canviar algun paràmetre del bus I2C caldria canviar la funció ADPS9960_Init().

Per la versió C++ he creat una classe (class ADPS9960) que instància una classe I2C (que crearé més endavant). La classe a crear rep el número de perifèric I2C a fer servir, la localització i l’adreça del sensor dins el bus I2C. La classe I2C ha de proporcionar els mètodes WriteRegister() i ReadRegister(). Els mètodes que implemento per la classe del sensor son només el constructor on inicialitzo el bus, es fa una comprovació de que el sensor està present i es configura el mode de treball i el mètode GetDistance() que fa la mateixa funció que a la versió en C.

uint8_t GetDistance(void) {
    uint8_t aux;
    uint8_t status;

    do {
        status = m_i2c.ReadRegister(m_addr, APDS_STATUS_REG);
    } while (status == 0x00);

    aux = m_i2c.ReadRegister(m_addr, APDS_PDATA_REG);
    return aux;
}

I2C

Ens cal una classe que controli el bus I2C i doni accés amb uns quants mètodes. La classe I2C la defineixo de la següent manera:

template<i2c_device dev, i2c_location loc>
class I2C : SerialIntf {
    I2C() {...}
    ~I2C() {}
    uint8_t ReadRegister(uint8_t addr, uint8_t reg) {...}
    bool WriteRegister(uint8_t addr, uint8_t reg, uint8_t data) {...}
private:
    constexpr I2C_TypeDef* getDevice(i2c_device i2c_dev) {...}
    constexpr uint32_t getLocation(int location) {...}
    constexpr CMU_Clock_TypeDef getClock(i2c_device i2c_dev) {...}
};

He definit la classe com a template perquè així ens estalviem uns quants bytes de memòria en guardar els paràmetres, ja que d’aquesta manera esdevenen constants dins el codi.

Les funcions getDevice(), getLocation() i getClock() fan la translació de les definicions genèriques de dispositiu i localització a les constants dependents de la implementació. Es defineixen com “constexpr” per a que s’avaluïn en temps de compilació, de manera que al codi final no hi seran i el compilador les substituirà pel resultat calculat. Els mètodes ReadRegister() i WriteRegister() no tenen més secret, de fet son iguals a la versió C.

Per últim, destacar que la classe deriva d’una interfície anomenada SerialIntf que es defineix aixi:

class SerialIntf {
public:
    virtual uint8_t ReadRegister(uint8_t addr, uint8_t reg) = 0;
    virtual bool WriteRegister(uint8_t addr, uint8_t reg, uint8_t data) = 0;
    virtual ~SerialIntf() =default;
};

Que com es veu conté només els dos mètodes per accedir a un registre i el destructor per defecte. Aquesta interfície també serviria per una classe que oferís el bus SPI o algun altre bus simple similar.

Aquesta interfície la defineixo per si cal passar com a paràmetre la classe I2C, que com és un template no és possible fer-ho de forma senzilla (almenys jo no en se més). A la versió C++20 es podrà fer servir “auto” com a tipus als paràmetres i ja serà senzill (en principi).

Cal a dir que l’ús d’aquest poliformisme (tenir un paràmetre que sigui un apuntador a la classe base interfície i passar-li la classe derivada) afegeix molt overhead de codi i potser no és recomanable en tots els casos.

He fet el mateix amb la classe ADPS9960, de manera que és un template que rep com a paràmetre el dispositiu I2C a utilitzar, la localització i l’adreça I2C del sensor. D’aquesta manera, de nou, s’estalvien bytes de memòria ja que no cal emmagatzemar aquests valors a la memòria.

template<i2c_device dev, i2c_location loc, uint8_t sensor_addr>
class ADPS9960 {
public:
    ADPS9960() {
        uint8_t ret_value;
	ret_value = m_i2c.ReadRegister(sensor_addr, 0x92);

	if (ret_value != ID_VALUE) {
		return;
	}

	/* Enable Proximity detection */
	/* ENABLE <- 5 & 2 & 0 bits */
	m_i2c.WriteRegister(sensor_addr, APDS_ENABLE_REG, 0x25);
	/* LED Strength to 100mA, Proximity Gain control to 8x */
	m_i2c.WriteRegister(sensor_addr, APDS_CTRL_1_REG, 0x0C);
	/* LED_BOOST 300% 0111_0001 */
	m_i2c.WriteRegister(sensor_addr, APDS_CTRL_2_REG, 0x71);
    }

    uint8_t GetDistance(void) {
	uint8_t aux;
	uint8_t status;

	do {
	    status = m_i2c.ReadRegister(sensor_addr, APDS_STATUS_REG);
	} while (status == 0x00);

	aux = m_i2c.ReadRegister(sensor_addr, APDS_PDATA_REG);		 
        return aux;
    }

private:
    const uint8_t APDS_ENABLE_REG {0x80};
    const uint8_t APDS_CTRL_1_REG {0x8F};
    const uint8_t APDS_CTRL_2_REG {0x90};
    const uint8_t APDS_STATUS_REG {0x93};
    const uint8_t APDS_PDATA_REG  {0x9C};
    const uint8_t ID_VALUE        {0xAB};
    I2C<dev, loc> m_i2c;
};

GPIO

Ens cal una manera senzilla de controlar les entrades i sortides del dispositiu i això vol dir algun controlador pel GPIO.

De nou i per estalviar memòria farem servir la mateixa tècnica amb el template per una classe Pin que rebrà com a paràmetres del template el port, el número de pin i el mode o tipus (entrada, sortida, open-collector, etc.).

template<IOPin::gpio_port port, IOPin::gpio_pin pin, IOPin::gpio_type type,
         IOPin::irq_enable irq_enable = IOPin::IRQ_DISABLE,
         IOPin::irq_mode irq_mode = IOPin::IRQ_RISING,
         isr_cb isr = nullptr>
class PIN: public PINBase, public Interrupt {
public:
    PIN() {
        CMU_ClockEnable(cmuClock_GPIO, true);
        GPIO_PinModeSet(EFM32TranslatePort(port), EFM32TranslatePin(pin), EFM32TranslateType(type), 0);
        ...
    }

    constexpr void Set() {
        GPIO_PinOutSet(EFM32TranslatePort(port), EFM32TranslatePin(pin));    
    }

    constexpr void Reset() {
        GPIO_PinOutClear(EFM32TranslatePort(port), EFM32TranslatePin(pin));
    }

    constexpr bool Get() {
        return GPIO_PinInGet(EFM32TranslatePort(port), EFM32TranslatePin(pin));
    }

    constexpr void Toggle() {
    GPIO_PinOutToggle(EFM32TranslatePort(port), EFM32TranslatePin(pin));
    }
...
};

Aquesta classe també derivarà d’una classe base anomenada PINBase que defineix els mètodes que cal implementar.

class PINBase {
public:
    virtual ~PINBase() {};
    virtual void Set() = 0;
    virtual void Reset() = 0;
    virtual bool Get() = 0;
    virtual void Toggle() = 0;
};

Per definir un pin caldrà instanciar aquesta classe passant-li els paràmetres adequats, com veiem a continuació

PIN<IOPin::PORTC, IOPin::PIN12, IOPin::PIN_OUT> LEDPin;
PIN<IOPin::PORTC, IOPin::PIN4, IOPin::PIN_OUT> RELAY1Pin;
PIN<IOPin::PORTC, IOPin::PIN5, IOPin::PIN_OUT> RELAY2Pin;
PIN<IOPin::PORTD, IOPin::PIN7, IOPin::PIN_I2C> SCLPin;
PIN<IOPin::PORTD, IOPin::PIN6, IOPin::PIN_I2C> SDAPin;

Tot i que encara no ho he fet servir, la classe també implementa interrupcions. Per això cal que la classe derivi també de la classe Interrupt que la definim així

class Interrupt{
public:
    Interrupt(void) {};
    static void Register(int interrupt_numberber, Interrupt* intThisPtr);

    static void TriggerInterrupt(int num) {
      if (ISRVectorTable[num] != nullptr) {
          ISRVectorTable[num]->ISR();
      }
    }

    virtual ~Interrupt() {};
    virtual void ISR(void) = 0;

private:
    static Interrupt* ISRVectorTable[MAX_IRQS];
};
Interrupt* Interrupt::ISRVectorTable[MAX_IRQS] = {nullptr};

void Interrupt::Register(int interrupt_number, Interrupt* intThisPtr)
{
    ISRVectorTable[interrupt_number] = intThisPtr;
}

void GPIO_EVEN_IRQHandler (void) {
  uint32_t aux = GPIO_IntGet();
  GPIO_IntClear(aux);

  std::bitset<16> pin = aux;
  for (std::size_t i = 0; i < pin.size(); i++) {
      if (pin[i] == true) {
          Interrupt::TriggerInterrupt(i);
      }
  }
}

void GPIO_ODD_IRQHandler (void) {
  uint32_t aux = GPIO_IntGet();
  GPIO_IntClear(aux);

  std::bitset<16> pin = aux;
  for (std::size_t i = 0; i < pin.size(); i++) {
      if (pin[i] == true) {
          Interrupt::TriggerInterrupt(i);
      }
  }
}

Aquesta classe manega les interrupcions d’I/O (potser caldria canviar-li el nom) i permet que un pin registri una funció per que es cridi quan hi ha una interrupció a aquell pin. Això es fa amb els últims paràmetres de la template, que se li pot passar si es vol activar o no la interrupció corresponent, si ha de ser per flanc de pujada o de baixa o tots dos, i la funció que es cridarà com a ISR. Aquest codi es basa en la proposta d’aquesta web.

MODBUS

El protocol MODBUS permet accedir a un conjunt de registres seguin un protocol senzill i, en aquest cas, el port sèrie. Hi ha una versió sobre TCP/IP però no la farem servir en aquest cas.

Per tant, la classe MODBUS li caldrà accedir a un port sèrie i a un conjunt de registres que li passarem per paràmetres del constructor.

Implementarem un mètode Slave() que s’haurà d’anar cridant per consultar si s’ha rebut un paquet demanant alguna acció. Aquesta classe no te gaire més secret si es coneix el protocol. Val la pena consultar la documentació aquí i anar seguint el codi, hauria de ser força auto-explicatiu.

UART

Aquí he implementat una classe UART amb la mateixa filosofia que les altres classes: un template on se li especifiquen certs paràmetres i un bon nombre de “constexpr” per configurar i inicialitzar el perifèric correcte.

Val a dir que en aquest cas la classe gestiona les interrupcions totalment ad-hoc a l’aplicació que estem escrivint. Caldria trobar la manera de gestionar les interrupcions d’alguna forma més genèrica; suposo que la manera hauria de ser molt similar a la que ja tenim a la classe PIN.

template<uart_peripheral device, int location>
class UART : public SerialUART {
public:
    UART() { ... }
...
}

En aquest cas també derivo la classe d’una interfície abstracte per tal de poder passar-la com a referència a una altra classe (la classe MODBUS).

class SerialUART {
public:
    virtual ~SerialUART() =default;
    virtual void send(const uint8_t *const pkt, int len) = 0;
    virtual int16_t RxTimeout() = 0;
};

Banc de registres

Cal tenir un banc de registres que seran els que accedeixi la classe MODBUS i ha de provocar canvis en l’aplicació. La classe Register s’encarrega d’això. També és l’encarregada de guardar a la memòria FLASH d’usuari del microcontrolador les dades que hagin de ser permanents i de cridar a una classe o funció de callback a cada accés a un registre si aquest te associada una funció de callback.

L’aplicació

Amb totes aquestes peces (i alguna més que no he explicat) ja es pot muntar una classe que encapsuli tota l’aplicació que volem fer. En aquest cas, a aquesta classe li he dit Application. Aquesta classe deriva de la classe Register_cb que és la interfície de callback del banc de registres.

class Application : Register_cb {
public:
    Application() : my_sensor(), mod_client(&leuart, &register_file) {

    letimer.SetDuty(3);

    register_file.register_wr_callback(REGISTER_DUTYCYLE, this);
    register_file.register_wr_callback(REGISTER_DOREAD, this);
    ...
    }
...
private:
    Timers letimer;
    Register register_file;
    PIN<IOPin::PORTC, IOPin::PIN12, IOPin::PIN_OUT> LEDPin;
    PIN<IOPin::PORTC, IOPin::PIN4, IOPin::PIN_OUT> RELAY1Pin;
    PIN<IOPin::PORTC, IOPin::PIN5, IOPin::PIN_OUT> RELAY2Pin;
    PIN<IOPin::PORTD, IOPin::PIN7, IOPin::PIN_I2C> SCLPin;
    PIN<IOPin::PORTD, IOPin::PIN6, IOPin::PIN_I2C> SDAPin;

    UART<LEUARTA, 0> leuart;
    ADPS9960<I2C_DEV0, LOC1, DEVICE_ADDR> my_sensor;
    Modbus mod_client;
};

El mètode Application::process() és el que activa l’esclau de MODBUS per rebre alguna comanda si la hi hagués, llegeix el sensor, comprova els llindars i activa les entrades que toqui.

D’aquesta manera, el nostre main queda molt senzill

int main(void) {
    /* Chip errata */
    CHIP_Init();

    CMU_ClockSelectSet(cmuClock_LFB, cmuSelect_LFXO);
    CMU_ClockSelectSet(cmuClock_LFA, cmuSelect_LFXO);
    CMU_ClockEnable(cmuClock_HFLE, true);

    SysTick_mng.Enable();

    Application app;

    while(1) {
        SysTick_mng.Disable();
        EMU_EnterEM2(true);
        SysTick_mng.Enable();
        app.process();
    }
}

Comparant

Un cop validat el codi i veient que fa la mateixa feina que la versió en C, cal comparar i treure conclusions.

Compilant els dos projectes amb els mateixos flags en versió Release:

Versió C: -mcpu=cortex-m3 -mthumb -std=c99 -O3 -pedantic -Wall -Wextra -ffunction-sections -fdata-sections -c -fmessage-length=0

Versió C++: -mcpu=cortex-m3 -mthumb -std=c++17 -O3 -pedantic -Wall -Wextra -ffunction-sections -fdata-sections -fno-rtti -fno-exceptions -c -fmessage-length=0

Els números que surten son els següents:

SeccióCC++Diferència (%)
.text79089684+1776 (+22.5%)
.data116124+8 (+6,9%)
.bss17272-100 (-58.1%)
.heap000 (0%)

Com es pot veure, la quantitat de memòria de programa (FLASH) augmenta en un ~20% i la quantitat de memòria de dades (RAM) disminueix considerablement (un global de prop del ~70%).

L’augment de la mida de codi pot ser deguda en part a que en la versió C++ alguns dels mòduls els he escrit genèrics per diferents perifèrics o configuracions mentre que a la versió C la gran majoria de configuracions estan hard-coded i son úniques.

Per altra banda, que les necessitats de memòria RAM disminueixin pot ser degut a la poca quantitat en global que cal per aquesta aplicació i a l’ús de templates i expressions constants (constexpr) al llarg de tot el codi.

Caldrà mirar-se aquesta part amb més detall, ja que si d’algo sempre anem curts en micros per encastats és de memòriaSecció RAM.

Més estàtic

Després d’investigar una mica més i llegir més documentació, m’he adonat que si fem les classes estàtiques (tots els seus mètodes static) enlloc de passar punter a classes base (com en el cas de la classe Modbus) podem crear un template que rebi les classes.

Així, la classe Modbus la podem escriure així:

template<class uart, class reg>
class Modbus {
public:
    Modbus () {
        uart();
        reg();
    }
...
    int do_func3(uint8_t *pkt) { 
        ...
        uart::send(modbus_resp_pkt, wr_idx);
        ...
    }
};

Llavors, a l’instanciar la classe Modbus hem de fer això

Modbus<UART<LEUARTA, 0>, Register> mod_client;

O ho podem simplificar d’aquesta manera:

typedef Register register_file;
typedef UART<LEUARTA, 0> leuart;
Modbus<leuart, register_file> mod_client;

Segons he pogut llegir i comprovar, al ser tot estàtic el compilador pot simplificar els mètodes que no es facin servir, cosa que no pot fer si els mètodes son virtuals.

Només modificant la classe UART, la Register i la Modbus perquè siguin estàtiques, el resultats son els següents

SeccióCC++C++ Estàtic
.text790896849388
.data116124124
.bss17272188
.heap000

Com es pot veure, augmenta significativament la quantitat de memòria RAM que necessita a canvi d’estalviar-nos memòria de programa (FLASH).

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.