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.
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
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










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