Iniciando C++ em sistemas embarcados

cpp

 Objetivo

O objetivo deste artigo é apresentar uma aplicação fazendo-se o uso da linguagem C++ em sistemas embarcados, sem se preocupar em abordar todas as características da linguagem, muito menos o ambiente de desenvolvimento e o hardware. Ou seja, o intuito principal é passar a filosofia de encapsulamento, levando o código ao mais alto nível possível. Entendendo esta essência, pode-se conseguir boa estruturação e reutilização do código sem muitas preocupações. Vale lembrar que é necessário que o leitor tenha conhecimentos sobre linguagens de orientação a objetos (OOP – Object-Oriented Programming).

Aplicação

A ferramenta utilizada para desenvolvimento desta aplicação foi o Keil uVision 5.20, o qual pode ser adquirido no próprio site, bastando apenas preencher um simples formulário. A versão vigente é v5.21a e o hardware utilizado é o kit STM32F0 Discovery, o qual já utilizei em Primeiros passos com a Placa STM32F0 Discovery.

Código

O código da aplicação será apresentado em três partes. Na primeira será apresentado o arquivo “main.cpp”, cujo conteúdo é os includes, a inicialização do hardware, as declarações dos objetos e a aplicação. A segunda parte apresenta o arquivo “GPIO.cpp” o qual contém o código de todos os métodos da classe. E, por fim, na terceira parte é apresentado o arquivo “GPIO.h”, onde temos a declaração de uma variável global, enumerações e a criação da classe.

No arquivo “main.cpp” podemos ver que dentro da rotina principal há a criação do objeto “Obj_GPIO” que será alvo da aplicação, a rotina de inicialização/configuração do clock do microcontrolador “vSystemCoreClockSetHSE()”, a inicialização da instância por meio da função-membro construtora e, por último, o loop já com a aplicação em uso. Vide código 1 logo abaixo.

Código 1: 

/*
	Brief: using C++ programming language to develop embedded systems.
	Author: Eder Tadeu
	Date: August 4th, 2016
*/


#include "GPIO.h"

void vDummyDelay(void)
{
	const unsigned long cuiDelay = 4800000;	
	unsigned long ulCounter = 0;
	
	while(ulCounter++ <= cuiDelay){}
}

void vSystemCoreClockSetHSE(void)
{
	/* Habilita o HSE como fonte de clock */
	RCC->CR |= ((uint32_t)RCC_CR_HSEON);
	/* Aguarda pela estabilização do HSE */
	while ((RCC->CR & RCC_CR_HSERDY) == 0);                  

	/* HSE selecionado como "system clock */
	RCC->CFGR = RCC_CFGR_SW_HSE;  
	/* Aguardando HSE para ser usado como "system clock" */
	while ((RCC->CFGR & RCC_CFGR_SWS) == RESET);   

	/* Habilita o Buffer de Pré-Busca */
	FLASH->ACR  = FLASH_ACR_PRFTBE;    
	/* Aguarda um cliclo */	
	FLASH->ACR |= FLASH_ACR_LATENCY;                          
 
	/* Fator de divisão do clock do barramento AHB (:1), AHB = SYSCLK */
	RCC->CFGR |= RCC_CFGR_HPRE_DIV1;                          
	/* Fator de divisão do clock do barramento APB (:1), APB = AHB*/
	RCC->CFGR |= RCC_CFGR_PPRE_DIV1;                          

	/* Desabilita o PLL para configuração */
	RCC->CR &= ~RCC_CR_PLLON;                                 
	
	/* Configuração do PLL = HSE * 6 = 48 MHz */
	RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE | RCC_CFGR_PLLMULL));
	RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLSRC_PREDIV1 | RCC_CFGR_PLLXTPRE_PREDIV1 | RCC_CFGR_PLLMULL6);
            
	/* Habilita o PLL */
	RCC->CR |= RCC_CR_PLLON;   
	/* Aguarda o PLL estar pronto, equanto isso não faz nada! */
	while((RCC->CR & RCC_CR_PLLRDY) == 0) __NOP();            

	/* Seleciona PLL como "system clock" */
	RCC->CFGR &= ~RCC_CFGR_SW;                                
	RCC->CFGR |=  RCC_CFGR_SW_PLL;    

	/* Aguarda PLL ser o "system clock" */
	while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);   
}

int main(void)
{
	/* Creation of GPIO object */
	class GPIOClass Obj_GPIO;
	
	/* Inicializa a fonte de clock HSE - High Speed External */
	vSystemCoreClockSetHSE();	
	
	/* Init ports, pins and their modes */
	Obj_GPIO.vGPIOInit(GPIO_C, OutputMode, GPIO_PIN_8);
	Obj_GPIO.vGPIOInit(GPIO_C, OutputMode, GPIO_PIN_9);	
	
	for(;;)
	{
		Obj_GPIO.vGPIOWriteOutput(GPIO_C, GPIO_PIN_8, SET);
		Obj_GPIO.vGPIOWriteOutput(GPIO_C, GPIO_PIN_9, SET);
		vDummyDelay();
		
		Obj_GPIO.vGPIOWriteOutput(GPIO_C, GPIO_PIN_8, RESET);
		Obj_GPIO.vGPIOWriteOutput(GPIO_C, GPIO_PIN_9, RESET);
		vDummyDelay();		
	}	
}
 
Percebe-se a nomeação dos métodos (construtora e rotinas de aplicação) que remetem claramente às suas respectivas funções, sem ao menos sabermos o que de fato é feito dentro deles. Isso nos permite entender rapidamente o código, ou seja: a inicialização dos ports “8” e “9” como saídas, ambos do port “C”. Já na aplicação, a qual é nada mais do que um blink LED (pisca-pisca), vemos ambos os “GPIO_PIN_8” e “GPIO_PIN_9” alternarem seus respectivos valores por meio do método “vGPIOWriteOutput()” quando o objeto “Obj_GPIO” utiliza o operador de acesso “.” para utilizar os recursos disponíveis da classe em que ele foi instanciado. Neste caso, “Obj_GPIO.vGPIOWriteOutput()”.
 
 
 
O ponto mais importante é que somente objetos instanciados a partir da “GPIOClass” podem acessar os métodos e dados dessa classe, remetendo ao encapsulamento. Vale ressaltar (antes que polêmicas sejam feitas) que esta classe poderia ser acessada por outro objeto instanciado à outra classe, desde que esta outra constasse em sua declaração “friend GPIOClass“. Todavia, isto elimina o conceito de encapsulamento e isto não queremos e muitos menos o faremos aqui!
 
 
A classe “GPIOClass” é constituída por cinco métodos:
  • a construtora GPIOClass(), a qual inicializa o dado pGPIO com “NULL”;
  • vGPIOEnable(), que habilita o clock do GPIO informado como parâmetro;
  • vGPIOGetAddress(), que obtêm o endereço do port que se deseja inicializar;
  • o vGPIOInit(), que realiza a inicialização do pino e, por fim;
  • vGPIOWriteOutput(), que altera o nível lógico do(s) pino(s) informado(s).

O código de cada um pode ser visto abaixo.

Código 2: 

#include "GPIO.h"
#include "stddef.h"

/*	Brief		: Construction function.
 *	Arguments	: None.	
 *	Return		: None.
 */ 

GPIOClass::GPIOClass(void)
{
  /* Init stGPIO pointer with NULL value */
  pGPIO = NULL;
}

/*	Brief		: Enable GPIO bus clock.
 *	Arguments	: Port.	
 *	Return		: None.
 */ 
void GPIOClass::vGPIOEnable(unsigned char Port)
{
  switch(Port)
  {	 
		case GPIO_A:  RCC->AHBENR |=  (1UL << 17);  /* Enable GPIOA clock */
		              break;

		case GPIO_B:  RCC->AHBENR |=  (1UL << 18);  /* Enable GPIOB clock */
		              break;

		case GPIO_C:  RCC->AHBENR |=  (1UL << 19);  /* Enable GPIOC clock */
		              break;

		case GPIO_D:  RCC->AHBENR |=  (1UL << 20);  /* Enable GPIOD clock */
		              break;

		case GPIO_F:  RCC->AHBENR |=  (1UL << 22);  /* Enable GPIOF clock */
		              break;

		default:      break;
	}	
}

/*	Brief		: Get the informed GPIO address.
 *	Arguments	: Port.	
 *	Return		: None.
 */ 
void GPIOClass::vGPIOGetAddress(unsigned char Port)
{		
	switch(Port)
	{	 
		case GPIO_A:  pGPIO = GPIOA;	/* Load the pointer with GPIOA adress */
		              break;

		case GPIO_B:  pGPIO = GPIOB;	/* Load the pointer with GPIOB address */
		              break;

		case GPIO_C:  pGPIO = GPIOC;	/* Load the pointer with GPIOC address */
		              break;

		case GPIO_D:  pGPIO = GPIOD;	/* Load the pointer with GPIOD address */
		              break;

		case GPIO_F:  pGPIO = GPIOF;	/* Load the pointer with GPIOF address */
		              break;

		default:      break;
	}
}

/*	Brief		: Initialization of pins.
 *	Arguments	: Port, IO mode and pin.	
 *	Return		:	None.
 */ 
void GPIOClass::vGPIOInit(unsigned char Port, unsigned char IOType, unsigned char Pin)
{	
	vGPIOEnable(Port);
	vGPIOGetAddress(Port);	

	switch(IOType)
	{				
		case InputMode:  pGPIO->MODER  &= ~(3UL << 2 * Pin);	/* Reset - input */
		                 break;

		case OutputMode:  pGPIO->MODER   &= ~(3UL << 2 * Pin);	/* Reset - input */
		                  pGPIO->MODER   |=  (1UL << 2 * Pin);	/* Output 	 */
		                  pGPIO->OTYPER  &= ~(1UL << Pin);      /* Push-pull     */
		                  break;

		case AltFuncMode: pGPIO->MODER   &= ~(3UL << 2 * Pin);	/* Reset - input */
		                  pGPIO->MODER   |=  (2UL << 2 * Pin);	/* Alternate 	 */
		                  break;

		case AnalogMode:  pGPIO->MODER   &= ~(3UL << 2 * Pin);	/* Reset - input */
                                  pGPIO->MODER   |=  (3UL << 2 * Pin);	/* Analog	 */
		                  break;

		default:          break;			
	}

	pGPIO->OSPEEDR &= ~(3UL << 2 * Pin);/* Reset - Low speed */
	pGPIO->OSPEEDR |=  (1UL << 2 * Pin);/* Medium speed 	 */	
	pGPIO->PUPDR   &= ~(3UL << 2 * Pin);/* No pull-up, no pull-down */
}


void GPIOClass::vGPIOWriteOutput(unsigned char Port, unsigned char Pin, bool Value)
{
	vGPIOGetAddress(Port);	

	if(Value)
	{
		pGPIO->BSRR |= (GPIO_Mask[Pin]);	
	}
	else
	{
		pGPIO->BSRR |= (GPIO_Mask[Pin] << 16);
	}		
}
 
E por fim, temos o arquivo “GPIO.h”, onde declaramos a classe “GPIOClass”, um vetor mascarável para acessar os pinos de cada port e alguns “enums”. Vide código 3.
 
Código 3:
 
#ifndef __GPIO_H
	#define __GPIO_H
#endif

/* User includes */
#include "stm32f0xx.h"

/* User defines */
#define QTY_GPIO_PINS	(unsigned char)16
#define RESET	(bool)0
#define SET	!(RESET)

/* User enums */
 enum IOType
 {
		InputMode  ,
		OutputMode ,
		AltFuncMode,
		AnalogMode ,
 };
 
 enum PIN
 {
	 GPIO_PIN_0, 
	 GPIO_PIN_1,
	 GPIO_PIN_2,
	 GPIO_PIN_3,
	 GPIO_PIN_4,		
	 GPIO_PIN_5,
	 GPIO_PIN_6,
	 GPIO_PIN_7,
	 GPIO_PIN_8,
	 GPIO_PIN_9, 
	 GPIO_PIN_10,
	 GPIO_PIN_11,
	 GPIO_PIN_12,
	 GPIO_PIN_13,
	 GPIO_PIN_14, 
	 GPIO_PIN_15,
	 GPIO_PIN_NONE
 };
 
 enum PORT
 {
	 GPIO_A,
	 GPIO_B,
	 GPIO_C,
	 GPIO_D,
	 GPIO_E,
	 GPIO_F,
	 GPIO_NONE
 };

 /* User variables */
 const unsigned long GPIO_Mask[QTY_GPIO_PINS] = {
1UL << GPIO_PIN_0, 
1UL << GPIO_PIN_1,
1UL << GPIO_PIN_2,
1UL << GPIO_PIN_3,
1UL << GPIO_PIN_4,
1UL << GPIO_PIN_5,
1UL << GPIO_PIN_6,
1UL << GPIO_PIN_7,
1UL << GPIO_PIN_8,
1UL << GPIO_PIN_9,
1UL << GPIO_PIN_10,
1UL << GPIO_PIN_11,
1UL << GPIO_PIN_12,
1UL << GPIO_PIN_13,
1UL << GPIO_PIN_14,
1UL << GPIO_PIN_15};
 
 /* User class */
class GPIOClass
{
	private: GPIO_TypeDef *pGPIO;
		
	public:         
            GPIOClass();
			void vGPIOInit       (unsigned char Port, unsigned char IOType, unsigned char Pin);
			void vGPIOWriteOutput(unsigned char Port, unsigned char Pin, bool Value);
			void vGPIOClearOutput(unsigned char Port, unsigned char Pin);			
			void vGPIOGetAddress (unsigned char Port);
			void vGPIOEnable     (unsigned char Port);
};
 
Depois de verificar os arquivos “GPIO.cpp” e “GPIO.h”, entendemos o que acontece quando os pinos alternam seus níveis lógicos e, consequentemente, piscam o LED. Exatamente neste ponto em que chegamos e, se relermos a aplicação na “main.cpp”, vemos que criamos nada mais do que uma abstração de camada além do encapsulamento. Isso de fato é muito bom desde que tanto a classe quanto seus respectivos conteúdos (dados e métodos) sejam bem elaborados e amplamente testados, possibilitando o bom uso e a reutilização em outros projetos, além de servir de base para uma biblioteca (“lib”).
 
 

Conclusão

Vimos que o exemplo apresentado cria um encapsulamento para o GPIO do microcontrolador, consequentemente, uma abstração de camada. Porém, este exemplo não determina nenhuma regra, assim o leitor poderá realizar seu encapsulamento da forma que lhe for conveniente. Além disso, poderá também estender a outros periféricos. Realizando este trabalho aos poucos, pode-se conseguir uma grande herança no futuro, pois ao criar classes para certos tipos de periféricos, cria-se, assim, uma biblioteca. 

Saiba mais

Orientação a objeto em C: Encapsulamento com estruturas opacas

Orientação a objeto em C: Polimorfismo

Referências 

A Complete Guide to Programming C++ –  Ulla Kirch-Prinz/Peter Prinz

Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.
Comentários:
Inscrever-se
Notificar de
6 Comentários
mais recentes
mais antigos Mais votado
Mauricio Oliveira Costa
Mauricio Oliveira Costa
19/09/2016 12:58

Muito bacana iniciar esta conversa sobre C/C++. O mito de que C/C++ infla o código ou mesmo complica demais é para aqueles que estão acomodados com o C. Uma vez iniciado no C/C++, você não volta mais atrás e começa a ver o C como uma linguagem tão limitada quanto o Basic, principalmente no quesito reuso de código e sucessão do conhecimento.

Rafael Dias
Rafael Dias
Responder para  Mauricio Oliveira Costa
19/09/2016 14:05

Embora que dá para usar boas práticas de OOP em C. A meu ver, OOP está mais ligada a metodologia de desenvolvimento de que a linguagem propriamente dita.

Eder
Eder
Responder para  Rafael Dias
16/08/2017 14:56

Sim, e também a capacidade do compilador.

Rafael Gebert
Rafael Gebert
Responder para  Mauricio Oliveira Costa
21/09/2016 09:05

Amigo Mauricio, acomodado é uma palavra muito forte… Até porque você está precisando estudar um pouco mais… Pergunto, se C++ não gasta processo extra e não “infla” conforme você comentou então me explique porque o uso de C++ exige memória heap? É porque ele usa alocação dinâmica de memória…. então as estruturas, ah desculpa, os objetos são alocados dinamicamente.

Rafael Dias
Rafael Dias
19/09/2016 12:08

Há uma lenda acerca do overhead de C++ em sistemas embarcados baremetal. Eu mesmo não tenho opinião formada a respeito.

Eder
Eder
Responder para  Rafael Dias
16/08/2017 15:02

Sim, porém esse “overhead” está mais ligado ao “leakage of memory” (RAM) devido a criação de objetos e o não “free()” deles, já que, eles são alocados no heap. Ou seja, falta de gerenciamento ou “carinho” no código, leva ao desperdício de memória e “bugs”.

Home » Software » Linguagem C » Iniciando C++ em sistemas embarcados

EM DESTAQUE

WEBINARS

VEJA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste: