🎯 Aula 03 — Ponteiros, Aritmética de Endereços e Passagem por Referência
Navegação Rápida
📍 Disciplina: 03 Programação Aplicada a Sistemas Elétricos | 🗺️ Área: 06 Computação e Telecomunicações | 🏠 Hub Principal ⬅️ Aula Anterior: Aula 02 - Linguagem C e Bitwise | ➡️ Próxima Aula: Aula 04 - Memory-Mapped I/O e ISR
📌 Sumário da Aula
- 🧠 1. O Modelo de Memória Linear e a Natureza dos Ponteiros
- 📐 2. Aritmética Formal de Ponteiros e Escala de Tipos
- 🔄 3. Relação entre Vetores e Ponteiros (Array Decay)
- 🛡️ 4. Passagem por Referência e Qualificadores const
- ⚡ 5. Buffers Circulares e DMA em Aquisição de Sinais Elétricos
- 📊 6. Diagrama de Ponteiros, Aritmética e Buffer DMA Ping-Pong
- 📝 7. Questões de Concursos e Problemas Avançados Resolvidos
- 🔗 8. Conexões e Referências Cruzadas
🧠 1. O Modelo de Memória Linear e a Natureza dos Ponteiros
Em sistemas computacionais e microcontroladores baseados em arquiteturas de 32 bits (como a família ARM Cortex-M), a memória física é organizada como um espaço vetorial linear contíguo de bytes endereçáveis, variando do endereço 0x00000000 até 0xFFFFFFFF ().
Um Ponteiro (Pointer) é uma variável cujo valor numérico armazenado é o endereço de memória física de outra variável ou periférico.
flowchart LR subgraph RAM["Memória SRAM"] AddrVal["Endereço: 0x20000100<br/>Variável: uint32_t val<br/>Conteúdo: 0x0000002A (42)"] AddrPtr["Endereço: 0x20000104<br/>Ponteiro: uint32_t *ptr<br/>Conteúdo: 0x20000100"] end AddrPtr -->|Aponta para (*ptr)| AddrVal
1.1 Operadores Fundamentais de Ponteiros
- Operador Endereço-de (
&): Unário. Retorna o endereço numérico de memória onde a variável está alocada.uint32_t tensao_rede = 13800U; uint32_t *ptr_tensao = &tensao_rede; // ptr_tensao recebe o endereço da variável - Operador de Derreferenciação ou Indireção (
*): Unário. Acessa o conteúdo contido no endereço apontado pelo ponteiro, permitindo leitura ou escrita direta na posição de memória.*ptr_tensao = 14200U; // Modifica tensao_rede diretamente na memória!
1.2 Alinhamento de Memória Natural
Em processadores ARM, o acesso a tipos de dados deve respeitar o seu alinhamento natural:
uint8_t: pode residir em qualquer endereço de byte (múltiplo de 1);uint16_t: deve residir em endereços pares (múltiplos de 2);uint32_tefloat: devem residir em endereços múltiplos de 4 (terminados em0x0,0x4,0x8ou0xC).
Tentar derreferenciar um ponteiro de 32 bits em um endereço desalinhado (ex: 0x20000101) pode disparar uma exceção de hardware irreversível (UsageFault / HardFault) no microcontrolador, causando o travamento do relé de proteção em operação.
📐 2. Aritmética Formal de Ponteiros e Escala de Tipos
A aritmética de ponteiros na linguagem C não opera sobre bytes puros como números inteiros comuns; ela é estritamente escalonada pelo tamanho em bytes do tipo apontado (sizeof(T)).
Seja um ponteiro declarado como T *p apontando para o endereço numérico de memória . A adição de um deslocamento inteiro ao ponteiro gera um novo endereço efetivo calculado pela fórmula:
A tabela a seguir demonstra a progressão aritmética de endereços para diferentes tipos de dados em uma arquitetura de 32 bits:
Tipo do Ponteiro T* | sizeof(T) | Endereço Inicial | Expressão p + 1 | Expressão p + 4 |
|---|---|---|---|---|
uint8_t * | 1 Byte | 0x20000000 | 0x20000001 () | 0x20000004 () |
uint16_t * | 2 Bytes | 0x20000000 | 0x20000002 () | 0x20000008 () |
uint32_t * | 4 Bytes | 0x20000000 | 0x20000004 () | 0x20000010 () |
double * | 8 Bytes | 0x20000000 | 0x20000008 () | 0x20000020 () |
2.1 Subtração de Dois Ponteiros
A subtração de dois ponteiros do mesmo tipo e não retorna a distância em bytes, mas o número exato de elementos do tipo compreendidos entre os dois endereços:
O resultado dessa operação possui o tipo signed inteiro padrão ptrdiff_t definido em <stddef.h>.
🔄 3. Relação entre Vetores e Ponteiros (Array Decay)
Na linguagem C, existe uma equivalência direta e profunda entre a sintaxe de indexação de vetores e a aritmética de ponteiros.
Quando o nome de um vetor é utilizado em uma expressão (com raras exceções, como argumento do operador sizeof ou operador &), ele sofre o processo denominado Decaimento para Ponteiro (Array Decay): o identificador do vetor é implicitamente convertido em um ponteiro constante apontando para o seu primeiro elemento (&vetor[0]).
A regra de equivalência de indexação do padrão ISO C estabelece a Identidade Canônica:
Isso explica por que, em C, a bizarra expressão 3[vetor] é perfeitamente válida e idêntica a vetor[3], pois ambas compilam para a mesma adição comutativa de ponteiros *(vetor + 3).
🛡️ 4. Passagem por Referência e Qualificadores const
Em C, todas as funções utilizam estritamente o mecanismo de Passagem por Valor (Call by Value): os argumentos passados para uma função são copiados na pilha de execução (Stack).
Se um engenheiro deseja que uma função processe um vetor de amostragem de oscilografia contendo pontos de ponto flutuante (float amostras[2048], ocupando de RAM), passar o vetor por valor copiaria todos os na pilha a cada chamada. Isso esgotaria a memória SRAM do microcontrolador e consumiria centenas de ciclos de CPU indevidamente.
4.1 Passagem por Referência com Ponteiros
Ao passar o ponteiro para o início do vetor, transfere-se apenas uma única palavra de 4 bytes (o endereço de memória), com complexidade temporal e consumo de pilha :
/* Passagem Eficiente por Referência */
float calcular_rms(const float *buffer, uint16_t tamanho) {
float soma = 0.0f;
for (uint16_t i = 0; i < tamanho; i++) {
soma += buffer[i] * buffer[i]; // Acesso direto na SRAM original
}
return sqrtf(soma / (float)tamanho);
}4.2 A Matriz de Qualificadores const em Ponteiros
Para garantir segurança cibernética e integridade de memória, a colocação da palavra-chave const define diferentes níveis contratuais de imutabilidade:
- Ponteiro para Dado Constante (
const T *ptrouT const *ptr): O dado apontado não pode ser modificado através do ponteiro, mas o ponteiro pode ser redirecionado para outro endereço.const uint16_t *p = &amostra_A; *p = 500; // ERRO DE COMPILAÇÃO! Protege o dado contra escrita. p = &amostra_B; // VÁLIDO. - Ponteiro Constante para Dado Modificável (
T * const ptr): O endereço para o qual o ponteiro aponta é fixo e imutável, mas o conteúdo da memória pode ser alterado. Este é o padrão utilizado para registradores de periféricos:uint32_t * const REG_GPIO = (uint32_t *) 0x40020014; *REG_GPIO = 0x01; // VÁLIDO. Escreve no registrador. REG_GPIO = 0x40020018; // ERRO DE COMPILAÇÃO! Não pode mudar o endereço. - Ponteiro Constante para Dado Constante (
const T * const ptr): Nem o endereço nem o dado apontado podem sofrer qualquer modificação.
⚡ 5. Buffers Circulares e DMA em Aquisição de Sinais Elétricos
5.1 O Princípio do Buffer Circular (Ring Buffer)
Na amostragem contínua de sinais de tensão e corrente trifásicos em sistemas de proteção de , o conversor analógico-digital (ADC) gera novas amostras em intervalos de tempo ultraprecisos (ex: 64 amostras por ciclo , ou uma amostra a cada ).
Um Buffer Circular organiza uma região contígua de memória em um anel lógico usando dois ponteiros ou índices móveis:
head(cabeça): Posição onde o novo dado amostrado é gravado pelo hardware;tail(cauda): Posição de onde o algoritmo de proteção (ex: Transformada de Fourier) lê os dados para cálculo.
#define RING_BUFFER_SIZE 128U // OBRIGATÓRIO: Potência de 2 (2^7)
typedef struct {
uint16_t buffer[RING_BUFFER_SIZE];
volatile uint16_t head;
volatile uint16_t tail;
} RingBuffer_t;
void RingBuffer_Put(RingBuffer_t *rb, uint16_t amostra) {
rb->buffer[rb->head] = amostra;
// Otimização Crítica: Máscara bitwise substitui rb->head = (rb->head + 1) % RING_BUFFER_SIZE
rb->head = (rb->head + 1) & (RING_BUFFER_SIZE - 1);
}Vantagem Matemática: A operação de módulo % em microcontroladores sem instrução de divisão por hardware consome até de clock. Substituí-la pela operação bitwise & (SIZE - 1) (válida estritamente quando SIZE é potência de 2) reduz o tempo de atualização para de instrução.
5.2 Controlador DMA e Arquitetura Ping-Pong Buffer
Para eliminar completamente a sobrecarga de interrupções na CPU na aquisição do ADC, utiliza-se o controlador de Acesso Direto à Memória (DMA). O DMA transfere as amostras dos registradores do ADC diretamente para a SRAM de forma autônoma através dos barramentos internos.
Para permitir que a CPU execute algoritmos pesados (como FFT de 128 pontos) sem ser interrompida a cada amostra individual, implementa-se a técnica de Buffer Duplo (Ping-Pong Buffer):
flowchart TD ADC["Conversor ADC (Hardware)<br/>Amostragem a 3840 Hz"] -->|Transferência DMA Contínua| DMACtr["Controlador DMA em Modo Circular"] DMACtr -->|Grava Bloco A| BufA["Buffer Bloco A (Amostras 0..63)"] DMACtr -->|Grava Bloco B| BufB["Buffer Bloco B (Amostras 64..127)"] BufA -.->|Quando Bloco A enche: Interrupção Half-Transfer| CPU_ProcA["CPU Processa Bloco A (FFT)"] BufB -.->|Quando Bloco B enche: Interrupção Transfer-Complete| CPU_ProcB["CPU Processa Bloco B (FFT)"]
- Enquanto o DMA preenche as amostras do Bloco A, a CPU processa os dados estáveis do Bloco B;
- Quando o DMA atinge a metade do buffer, ele emite a interrupção de hardware
DMA_HalfTransfer_Interrupt. A CPU passa a processar o Bloco A enquanto o DMA começa a preencher o Bloco B; - Ao atingir o final, o DMA emite
DMA_TransferComplete_Interrupte reinicia circularmente no Bloco A sem perder uma única amostra de sinal elétrico.
📊 6. Diagrama de Ponteiros, Aritmética e Buffer DMA Ping-Pong
O diagrama abaixo apresenta visualmente a correspondência entre variáveis e ponteiros na SRAM, a lei de escala da aritmética de ponteiros e o fluxo concorrente do Buffer Duplo com DMA:
📝 7. Questões de Concursos e Problemas Avançados Resolvidos
Questão 1 (Banca CESPE/CEBRASPE - Engenheiro de Tecnologia da Informação e Automação)
Considere o seguinte trecho de código escrito na linguagem padrão ISO C99 para um microcontrolador com registradores de 32 bits:
uint32_t buffer_eventos[8] = {10, 20, 30, 40, 50, 60, 70, 80};
uint32_t *p1 = buffer_eventos;
uint32_t *p2 = p1 + 5;
uint32_t valor = *p2;
ptrdiff_t diff = p2 - p1;Assinale a opção que indica os valores decimais atribuídos, respectivamente, às variáveis valor e diff.
Alternativas:
A) valor = 50; diff = 20;
B) valor = 60; diff = 5;
C) valor = 60; diff = 20;
D) valor = 50; diff = 5;
E) valor = 70; diff = 4;
Gabarito Comentado:
- Alternativa correta: B.
- Justificativa Passo a Passo:
p1aponta para o início do vetor:buffer_eventos[0], cujo valor é10.- Pela regra de aritmética de ponteiros,
p2 = p1 + 5avança 5 elementos do tipouint32_tna memória. Portanto,p2passa a apontar exatamente parabuffer_eventos[5]. - O valor na posição de índice 5 do vetor é:
- Índice 0: 10
- Índice 1: 20
- Índice 2: 30
- Índice 3: 40
- Índice 4: 50
- Índice 5: 60
Logo,
valor = *p2 = 60.
- A subtração entre dois ponteiros do mesmo tipo (
p2 - p1) retorna a quantidade de elementos de separação entre eles, e não o número de bytes físicos. Portanto,diff = 5.
Questão 2 (Banca FCC - Engenheiro Eletricista de Manutenção e Automação)
Em um sistema de telemedição de grandezas fasoriais, a função a seguir foi elaborada para zerar o conteúdo de uma tabela de calibração alocada na memória:
void limpar_tabela(void *tabela, size_t num_bytes) {
uint8_t *p = (uint8_t *)tabela;
while (num_bytes--) {
*p++ = 0;
}
}Sobre essa implementação em linguagem C, é correto afirmar que:
A) O código provocará erro de compilação, pois não é permitido realizar a coerção (cast) de um ponteiro genérico void * para uint8_t *.
B) A instrução *p++ = 0 grava zero no endereço atual apontado por p e, em seguida, incrementa o ponteiro p em exatamente 1 byte na memória.
C) O código provoca vazamento de memória (memory leak) ao final da execução do laço while.
D) A instrução falhará se tabela estiver alocada na memória RAM dinâmica.
E) O ponteiro p avança de 4 em 4 bytes a cada iteração, deixando dados residuais na memória.
Gabarito Comentado:
- Alternativa correta: B.
- Justificativa: O operador de pós-incremento
++possui maior precedência que o operador de derreferenciação*, mas sua avaliação retorna o valor original do ponteiro antes do incremento. Portanto,*p++ = 0avalia-se como:- Derreferencia o endereço atual
pe grava o valor0naquele byte; - Incrementa o ponteiro
pemsizeof(uint8_t) = 1byte para a próxima iteração. Essa é a implementação clássica e ultracompacta da função padrãomemset()da bibliotecastring.h.
- Derreferencia o endereço atual
Questão 3 (Problema Prático de Engenharia - Cálculo de Endereço em Barramento Físico)
Um microcontrolador industrial de 32 bits possui um conversor ADC de 12 bits cuja tabela de amostras é um vetor de inteiros não assinados de 16 bits (uint16_t). O vetor amostras_adc está posicionado na memória SRAM exatamente no endereço físico hexadecimal:
Calcule:
- O endereço de memória físico hexadecimal exato da amostra de índice (
&amostras_adc[45]). - Se o vetor fosse declarado como
float amostras_float[100](ocupando 4 bytes por elemento) no mesmo endereço base0x20001000, qual seria o endereço hexadecimal da amostra de índice ?
Resolução Passo a Passo:
-
Cálculo para
uint16_t(sizeof = 2bytes): O deslocamento em bytes é dado por: \Delta_{\text{bytes}} = k \times \text{sizeof}(\text{uint16_t}) = 45 \times 2 = \mathbf{90\text{ bytes}}Convertendo decimal para hexadecimal:
Somando ao endereço base:
-
Cálculo para
float(sizeof = 4bytes): O deslocamento em bytes é dado por:Convertendo decimal para hexadecimal:
Somando ao endereço base:
Observação de Alinhamento: Note que ambos os endereços calculados (0x2000105A e 0x200010B4) são pares, e o endereço de float termina em 4 (múltiplo estrito de 4), respeitando o alinhamento de memória natural da arquitetura ARM.
🔗 8. Conexões e Referências Cruzadas
Disciplinas Vinculadas:
- Aula 02: Modificador
volatilee qualificadores de tipo. - Aula 04: Sobreposição de structs sobre endereços de hardware via ponteiros.
- 05 Microprocessadores: Controladores DMA e barramentos AHB/APB.
- 09 Sistemas de Proteção e Medição: Aquisição contínua de amostras fasoriais por amostragem síncrona.
Materiais Didáticos Oficiais da Disciplina:
- Arquivo de Curso:
02 - Linguagem C Recursos Básicos.pdf - Arquivo de Curso:
03 - Linguagem C Funções.pdf - Arquivo de Curso:
04 - Linguagem C Nos Sitemas Embarcados.pdf