Técnicas de Mapeamento de Memória em Linguagem C

Olá, caro leitor! Há um tempo escrevi um artigo sobre especificadores de acesso da linguagem C. Nesse artigo, apresentei os modificadores const e volatile, exemplificando algumas operações para acessar registradores de um microcontrolador. Tais operações são conhecidas como mapeamento de memória e podem ser realizadas de maneiras diferentes.

De modo geral, existem quatro maneiras de fazer o mapeamento de memória em um firmware: mapeamento direto; por ponteiro; por estruturas; vetor de ponteiros. Assim, este artigo tem como objetivo apresentar tais métodos, destacando as diferenças e comparado-os. Sempre que necessário será reforçado o uso dos modificadores de acesso.

Técnicas de mapeamento em memória

Como dito anteriormente, existem diversas técnicas que podem ser empregadas para fazer o mapeamento de memória. De modo geral, essas técnicas diferem nos seguintes aspectos [1]: densidade do código, tempo de execução, eficiência, portabilidade e grau de configuração.

Antes de descrever as técnicas é importante definir os seguintes conceitos:

Periférico mapeado em memória (Memory mapped I/O)

Periféricos estão mapeados em memória e ocupam o mesmo espaço de endereçamento do programa / dados, isto, é claro, depende da arquitetura. Assim, as operações de escrita e leitura são as mesmas definidas para qualquer movimentação de dados.

Porta isolada (Port mapped I/O)

Periféricos estão mapeados em um espaço de endereçamento que pode ser dedicado. No entanto, um conjunto específico de instruções é utilizado para acessá-los. Por exemplo, instruções IN e OUT.

Mapeamento de memória direto

É comum que fabricantes disponibilizem arquivos de cabeçalho com definição de nomes dos registradores. Ao analisar tais definições verifica-se que cada nome está relacionado a um endereço de memória. Geralmente, isso é feito utilizando diretivas de pré-processamento (#define). A seguir é mostrado a biblioteca io2313 com a definição dos registradores do microcontrolador ATtiny2313.

/* Input Pins, Port D */
#define PIND	_SFR_IO8(0x10)

/* Data Direction Register, Port D */
#define DDRD	_SFR_IO8(0x11)

/* Data Register, Port D */
#define PORTD	_SFR_IO8(0x12)

/* Input Pins, Port B */
#define PINB	_SFR_IO8(0x16)

/* Data Direction Register, Port B */
#define DDRB	_SFR_IO8(0x17)

/* Data Register, Port B */
#define PORTB	_SFR_IO8(0x18)

Um exemplo de acesso aos registradores é mostrado abaixo.

int main(void)
{
	DDRB = 0xFF;
		
    while (1) 
    {
	PORTB = ~PORTB;
    }
}

A princípio, ao utilizar o nome do registrador será efetuado o acesso direto ao endereço especificado. No entanto, é importante consultar a biblioteca. Por exemplo, a macro _SFR_IO8 é definida na biblioteca sfr_defs e determina se o endereço usado como argumento será deslocado em 0x20 bytes. Se deslocado, o endereço corresponde à área de I/O mapeada em memória e possui instruções específicas para acesso. Dito de outra maneira, no acesso direto são usadas as instruções LDS e STS para leitura e escrita, já no outro caso são usadas as instruções IN e OUT.

#ifndef __SFR_OFFSET
#  if __AVR_ARCH__ >= 100
#    define __SFR_OFFSET 0x00
#  else
#    define __SFR_OFFSET 0x20
#  endif
#endif

#define _SFR_MEM8(mem_addr) _MMIO_BYTE(mem_addr)
#define _SFR_MEM16(mem_addr) _MMIO_WORD(mem_addr)
#define _SFR_MEM32(mem_addr) _MMIO_DWORD(mem_addr)
#define _SFR_IO8(io_addr) _MMIO_BYTE((io_addr) + __SFR_OFFSET)
#define _SFR_IO16(io_addr) _MMIO_WORD((io_addr) + __SFR_OFFSET)

O conjunto de instruções gerado é mostrado a seguir. Verifique que a instrução OUT foi utilizada para acessar os registradores. O mesmo pode ser feito com a instrução STS usando o endereço direto (endereço de I/O + 0x20).

int main(void)
{
	DDRB = 0xFF;
  34:	8f ef       	ldi	r24, 0xFF	; 255
  36:	87 bb       	out	0x17, r24	; 23
		
    while (1) 
    {
		PORTB = ~PORTB;
  38:	88 b3       	in	r24, 0x18	; 24
  3a:	80 95       	com	r24
  3c:	88 bb       	out	0x18, r24	; 24
  3e:	fc cf       	rjmp	.-8      	; 0x38 <main+0x4>

Mapeamento de memória usando ponteiros

Nesta técnica um ponteiro é declarado com o endereço do registrador que será acessado. Considere, por exemplo, o acesso ao registrador de dados PORTB do microcontrolador ATtiny2313.

int main()
{
	uint8_t * PortBMap = 0x38;
	
	*PortBMap = 0x10;
}

Essa declaração define um ponteiro para um valor uint8_t. No entanto, é importante consultar a documentação do compilador utilizado, pois podem ocorrer algumas otimizações de código. Para este caso, o acesso direto usando o espaço de I/O continua sendo utilizado. Verifique o conjunto de operações gerado (mesmo modo de acesso do tópico anterior): 

int main(void)
{
    uint8_t * PortBMap = (uint8_t *)0x38;
    	
    *PortBMap = 0x10;
  34:	80 e1       	ldi	r24, 0x10	; 16
  36:	88 bb       	out	0x18, r24	; 24
  38:	ff cf       	rjmp	.-2      	; 0x38 <main+0x4>

Continuando este tópico, é importante considerar que um periférico pode ter valores alterados sem influência do software, por exemplo, campos de bits que representam flags de controle ou entradas digitais. Assim, é imprescindível que as operações de leitura sejam sempre realizadas no endereço indicado, obtendo o valor atual do registrador. Para forçar o compilador a realizar essas tarefas o modificador de acesso volatile deve ser utilizado.

volatile uint8_t * PinBMap = 0x36;

O código mostrado acima garante que a operação de leitura do registrador será realizada. Agora, considerando que o acesso – a princípio – é realizado por um ponteiro, tal variável pode ser manipulada acidentalmente, alterando o endereço apontado. Para evitar esse problema o modificador de acesso const deve ser utilizado.

int main()
{
	volatile uint8_t * const PortBMap = 0x38;
	
	*PortBMap = 0x10;
}

Essa declaração define um ponteiro constante para um valor uint8_t. Assim, o modificador volatile determina que o acesso ao registrador sempre será realizado, evitando problemas com otimizações do compilador. Já o modifcador const impede que o ponteiro seja manipulado.

Mapeamento de memória usando estruturas

Neste tipo de mapeamento é criada uma estrutura contendo membros com nome dos registradores. Tal declaração corresponde diretamente ao mapa de memória do microcontrolador. É importante notar que o tipo de dado deve ser compatível com a quantidade de bits do registrador. Por exemplo, considere o mapa de memória microcontrolador MKL25Z [2] mostrado na Figura 1.

Mapeamento de memória dos registradores de GPIO.
Figura 1: Mapa de memória dos registradores de GPIO.

No trecho de código abaixo é mostrado a definição de um tipo de dados para representar tais registradores de GPIO. Confira a ordem de declaração conforme a Figura 1.

/** GPIO - Register Layout Typedef */
typedef struct {
 __IO uint32_t PDOR; /**< Port Data Output Register, offset: 0x0 */
 __O uint32_t PSOR; /**< Port Set Output Register, offset: 0x4 */
 __O uint32_t PCOR; /**< Port Clear Output Register, offset: 0x8 */
 __O uint32_t PTOR; /**< Port Toggle Output Register, offset: 0xC */
 __I uint32_t PDIR; /**< Port Data Input Register, offset: 0x10 */
 __IO uint32_t PDDR; /**< Port Data Direction Register, offset: 0x14 */
} GPIO_Type, *GPIO_MemMapPtr;

A definição da estrutura não representa uma informação armazenada. É utilizada apenas como meio de referenciar endereços de memória. Para acessar os registradores deve ser declarado um ponteiro para o tipo de dado da estrutura.

Ao utilizar o nome do ponteiro, o acesso a um determinado membro representa apenas um deslocamento (offset) em relação ao endereço base. Por isso é importante a declaração dos registradores na ordem do mapa de memória, bem como da capacidade de armazenado (diretamente relacionada ao offset).

No trecho de código abaixo é mostrada a definição de dois pontos de acesso ao conjunto de registradores dos módulos GPIOA e GPIOB.

/* GPIO - Peripheral instance base addresses */
/** Peripheral GPIOA base address */
#define GPIOA_BASE                               (0x400FF000u)
/** Peripheral GPIOA base pointer */
#define GPIOA                                    ((GPIO_Type *)GPIOA_BASE)
#define GPIOA_BASE_PTR                           (GPIOA)
/** Peripheral GPIOB base address */
#define GPIOB_BASE                               (0x400FF040u)
/** Peripheral GPIOB base pointer */
#define GPIOB                                    ((GPIO_Type *)GPIOB_BASE)
#define GPIOB_BASE_PTR                           (GPIOB)

Já o acesso a um registrador do módulo de GPIO é determinado a partir do endereço base:

GPIOA->PSOR = <value>;

Mapeamento de memória usando vetor de ponteiros

A ultima técnica apresentada é utilizada para agrupar conjunto de registradores para relacioná-los a partir de um nome comum. Além de referenciá-los da mesma maneira, tais registradores também são agrupados por função. De modo geral, um vetor de ponteiros é criado para armazenar o endereço de tais registradores. Por exemplo, todos os registradores de saída (nos exemplos do artigo, PORTx ou GPIOx_PDOR) podem ser agrupados em um vetor.

A aplicação desta técnica torna-se interessante para criar soluções genéricas. Isto é, uma vez que os registradores estão agrupados por funções, o código pode ser estruturado para criar módulos. Por exemplo, no trecho de código abaixo os registradores foram agrupados em duas categorias, direção e valor de saída, e duas funções foram criadas para configurá-los. A indicação do registrador utilizado é determinada pelo nome do módulo (Port_t).

typedef enum{
	MCU_PORTB,
	MCU_PORTD,
	MCU_NUMBER_OF_PORTS
}Port_t;

static uint8_t volatile * const DataDirectionRegister[MCU_NUMBER_OF_PORTS] =
{
	(uint8_t * const)0x37, /*DDRB*/
	(uint8_t * const)0x31, /*DDRD*/
};

static uint8_t volatile * const DataRegister[MCU_NUMBER_OF_PORTS] =
{
	(uint8_t * const)0x38, /*PORTB*/
	(uint8_t * const)0x32, /*PORTD*/
};

static void PORT_Direction(Port_t port, uint8_t dir){
	if(port < MCU_NUMBER_OF_PORTS){
		*DataDirectionRegister[port] = dir;
	}
}

static void PORT_Toggle(Port_t port){
	if(port < MCU_NUMBER_OF_PORTS){
		*DataRegister[port] = ~(*DataRegister[port]);
	}
}

A aplicação fica limitada a este exemplo simples. Tabelas de configuração de periféricos podem ser criadas e passadas para uma função de inicialização. Por sua vez, tal função configura todos os elementos definidos na tabela com base no registradores declarados. Este princípio é aplicado na construção de drivers, em que somente a parte de código que acessa o hardware deve ser alterada. Isto é, as funções definem uma interface de acesso para um conjunto de recursos, já os recursos são configurados conforme o dispositivo.

Conclusão

Após apresentar as técnicas de mapeamento é importante fazer algumas comparações. Na ordem em que foram apresentadas, essas técnicas diminuem a densidade do código (aumenta o número de instruções) e acrescem o tempo de execução, isto é, reduzem a eficiência. No entanto, a portabilidade e grau de configuração dos periféricos aumentam substancialmente.

De fato, a aplicação destas técnicas dependerá do dispositivo utilizado, pois em alguns casos a quantidade de memória disponível é baixa. Assim, deve-se considerar esses fatores, balanceando portabilidade do código, facilidade de configuração (abstração do hardware) e dos recursos disponíveis.

Saiba mais

Modificadores de Acesso na Linguagem C

Objetos em Linguagem C

Struct – Registros em Linguagem C

Estilo de código – Boas práticas de programação em linguagem C

Referências

[1] BENINGO, J. Reusable Firmware Development: A Practical Approach to APIs, HALs, and Drivers.

[2] KL25 Sub-Family Reference Manual.

Fonte da Imagem destacada

Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.
Comentários:
Notificações
Notificar
1 Comentário
recentes
antigos mais votados
Inline Feedbacks
View all comments
Henrique
Henrique
08/07/2022 15:56

Quero usar uma matriz 10×10, mas o compilador não executa porque ele está solicitando + memória !
Como que eu resolvo isso ?

Home » Software » Técnicas de Mapeamento de Memória em Linguagem C

EM DESTAQUE

WEBINARS

VEJA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste: