Neste artigo, apresentaremos de forma didática como funciona o endereçamento de memória em microcontroladores STM32 que utilizam endereçamento de 4 em 4 bytes (32 bits). Iremos explorar o mapa de memória, mostrar como definimos ponteiros para acessar posições de periféricos e memória interna, e detalhar a aritmética de ponteiros que permite calcular endereços de registradores de forma clara e sistemática. Ao longo do texto, usaremos símbolos gregos nomeados para representar conceitos-chave, como α\alphaα para o endereço-base, β\betaβ para deslocamentos e γ\gammaγ para o endereço resultante.
Em seguida, demonstraremos exemplos práticos de declaração de ponteiros em C para acessar registradores de GPIO, UART e outros dispositivos, explicando como cada offset é obtido a partir do endereço-base do bloco de periféricos. Apresentaremos as fórmulas fundamentais que auxiliam na compreensão do cálculo de endereços: \[\gamma = \alpha + N \times \beta\]
onde N é o índice do registrador e β=4\beta=4β=4 bytes. Também detalharemos como a aritmética de ponteiros em C já incorpora esse fator de 4, de modo que, ao incrementar um ponteiro de tipo uint32_t*
, ele avança exatamente 4 bytes na memória.
Por fim, abordaremos boas práticas de definição de ponteiros simbólicos, com uso de macros e typedef
, para tornar o código mais legível e livre de “números mágicos”. Será uma visão completa, desde o fundamento teórico até exemplos práticos que você poderá copiar direto para seus projetos em STM32.
Mapa de Memória do STM32
No universo STM32, todo o espaço de endereçamento de 32 bits é dividido em grandes “zonas” fixas, cada uma com um endereço-base definido em hexadecimal. Chamemos cada endereço-base de α\alphaα, β, δ etc., conforme o bloco:
- Memória Flash
- Endereço-base (α):
0x0800 0000
- Tamanho típico: até alguns megabytes (dependendo da família)
- Usada para armazenar o firmware.
- Endereço-base (α):
- SRAM
- Endereço-base (β):
0x2000 0000
- Geralmente entre 64 KB e 512 KB em STM32 modernos.
- Endereço-base (β):
- Periféricos (APB e AHB)
- Endereço-base principal (γ):
0x4000 0000
- Dentro desse bloco, há subdivisões para barramentos AHB1, AHB2, APB1, APB2, cada uma com seu próprio base offset. Por exemplo:
- AHB1:
\(\gamma + 0x0002 0000 = 0x4002 0000
\) - APB2: \(
\gamma + 0x0001 0000 = 0x4001 0000
\)
- AHB1:
- Endereço-base principal (γ):
- Memória de Depuração e Reservada
- ST-LINK e ETM:
0xE004 2000
em diante
- ST-LINK e ETM:
Cada bloco α,β,γ é apenas o ponto de partida. Para acessar um registrador N-ésimo dentro do bloco de periféricos, usamos a fórmula \[\delta = \gamma + N \times \beta_{\text{offset}}\]
onde βoffset=4\beta_{\text{offset}} = 4βoffset=4 bytes (tamanho de um registrador de 32 bits).
Por exemplo, para o registrador GPIOA_MODER (modo de pinos de GPIOA), cujo offset oficial é 0x00
a partir de GPIOA_BASE = 0x4002 0000
: \[ \text{GPIOA\_MODER} = \gamma + 0x0002\,0000 + (0 \times 4) = 0x4002\,0000.\]
Já o registrador GPIOA_ODR (data output register), offset 0x14
, fica em \[\text{GPIOA\_ODR} = 0x4002\,0000 + (0x14) = 0x4002\,0014\].
Em resumo, o mapa de memória STM32 é uma sequência de blocos alinhados em fronteiras de 1 MB ou mais, cada um com um endereço-base α,β,γ. Nos próximos exemplos práticos veremos como declarar ponteiros em C usando essas constantes e como a aritmética de ponteiros já “sabe” que cada incremento de uint32_t*
pula 4 bytes automaticamente.
Declaração de Ponteiros e Boas Práticas
No código C para STM32, cada bloco de registradores tem um endereço-base que chamaremos de α\alphaα. Para tornar tudo mais legível e evitar “números mágicos”, recomendamos definir macros e typedef
que explicitem a intenção:
// 1) Definição de tipos e macros úteis
typedef volatile uint32_t vuint32_t; // acesso a registradores de 32 bits
#define GPIOA_BASE ((uintptr_t)0x40020000) // α = endereço-base do bloco GPIOA
#define GPIOA_MODER_OFFSET (0x00u) // β₁ = offset do registrador MODER
#define GPIOA_ODR_OFFSET (0x14u) // β₂ = offset do registrador ODR
A partir daí, para criar um ponteiro para cada registrador, usamos a conversão de ponteiro em C, que chamaremos de γ\gammaγ. Por exemplo:
// 2) Declaração de ponteiros simbólicos
#define GPIOA_MODER ((vuint32_t*)(GPIOA_BASE + GPIOA_MODER_OFFSET)) // γ₁
#define GPIOA_ODR ((vuint32_t*)(GPIOA_BASE + GPIOA_ODR_OFFSET)) // γ₂
- Ao escrever
GPIOA_MODER
, você tem já um ponteiro para o registrador de modo de pino. - A aritmética de ponteiros em C incorpora automaticamente o fator “4 bytes”: incrementar
GPIOA_MODER
em 1 avança exatamente 4 bytes na memória.
Esse padrão — usar um typedef
para o tipo de registrador e macros para base (α) e offset (β) — é a forma mais clara de identificar cada endereço de dispositivo. Nos próximos exemplos, veremos como usar a aritmética de ponteiros para iterar sobre grupos de registradores semelhantes sem recalcular manualmente cada deslocamento.
Aritmética de Ponteiros
Na aritmética de ponteiros em C, o incremento de um ponteiro já considera o tamanho do tipo apontado. No nosso caso, como usamos vuint32_t*
(ponteiro para uint32_t
), cada incremento avança 4 bytes. Vamos nomear:
- Endereço-base do ponteiro: γ
- Passo do incremento: \(\rho = sizeof(uint32\_t) = 4\)
- Índice de deslocamento: N
Então, para obter o NNN-ésimo registrador a partir de γ, usamos a fórmula: \(\gamma_{(N)} \;=\; \gamma + N \times \rho\)
Em código C, ao fazer:
vuint32_t *p = GPIOA_MODER; // p aponta para γ (GPIOA_MODER)
p + 0
→ \(\gamma_{(0)} = \gammaγ\) → primeiro registrador (MODER
)p + 1
→ \(\gamma_{(1)} = \gamma + 1\times4\) → segundo registrador (offset +0x04)- …
p + N
→ \(\gamma_{(N)}\) → registrador emGPIOA_BASE + (MODER_OFFSET + N*4)
Exemplo prático
Suponha que dentro de cada porta GPIO existam 8 registradores consecutivos a partir de GPIOA_MODER
:
for (uint32_t i = 0; i < 8; i++) {
vuint32_t *reg = GPIOA_MODER + i;
// Isso acessa: GPIOA_BASE + (GPIOA_MODER_OFFSET + i*4)
uint32_t valor = *reg; // lê o valor do registrador i
*reg = valor | 0x01; // só um exemplo de operação bit a bit
}
Aqui, cada iteração leva reg
a um novo endereço: \[\gamma_{(i)} = \underbrace{GPIOA\_BASE + GPIOA\_MODER\_OFFSET}_{\gamma} + i \times \rho\]
e ρ já é 4 bytes, portanto não precisamos fazer multiplicação manual de 4*i
no cálculo de ponteiro — o compilador cuida disso.
Fórmulas Fundamentais de Endereçamento
Nesta seção reunimos as expressões matemáticas essenciais para calcular e entender como obter qualquer endereço de registrador em um STM32 que endereça de 4 em 4 bytes (32 bits). Vamos nomear:
- α = endereço-base do bloco de periféricos (por exemplo,
GPIOA_BASE
=0x40020000
) - β = tamanho de cada registrador em bytes = 4
- N = índice (inteiro) do registrador desejado dentro do bloco
- γ = endereço resultante do registrador
- Cálculo direto de offset \(\gamma \;=\; \alpha \;+\; \text{Offset}_{\text{Registro}\) Em que \(\text{Offset}_{\text{Registro}} \;=\; N \;\times\;\beta\) Logo, juntando: \( \gamma = \alpha + N\,\beta\).
- Forma geral em notação de ponteiro
Se definirmos um ponteiro simbólico p=(vuint32_t∗) α p = (\mathtt{vuint32\_t}^*)\,\alphap=(vuint32_t∗)α então o N-ésimo registrador é simplesmente \(\quad\Longrightarrow\quad \gamma = \alpha + N \times \beta\), pois o compilador faz \((p+N)\times4\) bytes automaticamente. - Cálculo reverso (índice a partir de um endereço)
Às vezes precisamos descobrir N dado um endereço γ. Basta inverter: \(N \;=\; \frac{\gamma – \alpha}{\beta}\). Como γ e α são múltiplos de 4, N será inteiro. - Exemplo completo
- Suponha \(\alpha = 0x4002\,0000\) (GPIOA_BASE)
- Queremos o registrador cujo offset oficial é
0x14
→ \(N = 0x14/4 = 5\) - Aplicando a fórmula: \(\gamma = 0x4002\,0000 + 5\times4 = 0x4002\,0014\)
- Para descobrir N se só tivermos \(\gamma = 0x4002\,0014\): \( N = \tfrac{0x4002\,0014 – 0x4002\,0000}{4} = \tfrac{0x14}{4} = 5.\)
- Resumo das expressões \[\boxed{ \begin{aligned} \gamma &= \alpha + N\,\beta,\\ N &= \dfrac{\gamma – \alpha}{\beta},\\ \beta &= \text{sizeof(uint32\_t)} = 4\;\text{bytes}. \end{aligned} }\]
Com essas fórmulas em mãos, você pode derivar qualquer endereço de registrador, definir ponteiros de forma simbólica e até calcular índices de forma reversa quando precisar depurar ou validar offsets.
Exemplos Avançados de Aritmética de Ponteiros e Uso em Periféricos
Nesta seção, vamos ver cenários práticos em que a aritmética de ponteiros facilita o acesso a blocos sequenciais de registradores, como temporizadores (TIM), controladores DMA e unidades UART. Manteremos a notação grega para clareza:
- α: endereço-base do bloco de periféricos (por exemplo,
TIM2_BASE
) - β=4: tamanho de cada registrador em bytes
- N: índice do registrador desejado
- γ: endereço resultante
- Iteração sobre registradores de um temporizador (TIM2)
A família STM32 define o bloco TIM2 em \(\alpha_{\text{TIM2}} = 0x4000\,0000 + 0x0000\,0000 = 0x4000\,0000\). Os registradores estão dispostos em sequência de 4 em 4 bytes, começando emTIM2_CR1
(offset0x00
),TIM2_CR2
(0x04
),TIM2_SMCR
(0x08
) etc. Supondo que haja 10 registradores consecutivos, podemos fazer:
#define TIM2_BASE ((uintptr_t)0x40000000) // α₁
vuint32_t *pTIM2 = (vuint32_t*)TIM2_BASE; // ponteiro para α₁
for (uint32_t i = 0; i < 10; i++) {
// γ = α₁ + i*β
vuint32_t *reg = pTIM2 + i;
uint32_t val = *reg; // lê registrador i
*reg = val & 0xFFFF; // exemplo de operação
}
Aqui, cada pTIM2 + i
gera o endereço \[\gamma_i = \alpha_{\text{TIM2}} + i \times \beta\].
- Acesso a canais DMA com stride (passo) múltiplo
Em muitos microcontroladores STM32, os canais DMA possuem blocos de registradores com “stride” maior que 4 bytes (por exemplo, 0x14 bytes entre as estruturas de cada canal). Se quisermos acessar o canal kkk do DMA1, definimos:- \(\alpha_{\text{DMA1}} = 0x4002\,6000\)\(\beta_{\text{canal}} = 0x14\) bytesN=k
#define DMA1_BASE ((uintptr_t)0x40026000) // α₂
#define DMA1_CH_OFFSET (0x14u) // β₂ = 20 bytes
// ponteiro genérico ao canal k
static inline vuint32_t* DMA1_Channel(uint32_t k) {
return (vuint32_t*)(DMA1_BASE + k * DMA1_CH_OFFSET);
}
// Exemplo: configurar o registrador CCR do canal 3
vuint32_t *pCCR3 = DMA1_Channel(3) + (0x08u/4);
// offset 0x08 dentro do bloco do canal (CCR), dividido por β=4
*pCCR3 |= (1 << 0); // habilita o canal
- Blocos de registradores em interfaces UART
Em UARTs, o bloco UART1 (\(\alpha_{\text{UART1}} = 0x4001\,1000
\)) é composto por registradores de status, data e controle. Suponha que queremos varrer o registrador de dados (offset0x24
) e o de status (offset0x1C
) de forma programática:
#define UART1_BASE ((uintptr_t)0x40011000) // α₃
#define UART_SR_OFFSET (0x1Cu) // β₃₁
#define UART_DR_OFFSET (0x24u) // β₃₂
vuint32_t *pUART_SR = (vuint32_t*)(UART1_BASE + UART_SR_OFFSET);
vuint32_t *pUART_DR = (vuint32_t*)(UART1_BASE + UART_DR_OFFSET);
// Exemplo: ler e reescrever o dado se o status indicar transmissor pronto
if ((*pUART_SR & (1 << 7)) != 0) {
uint32_t dado = *pUART_DR;
*pUART_DR = dado; // ecoa o dado
}
- Cálculo reverso de índice para depuração
Se você souber apenas o endereço γ e quiser descobrir N, aplique: \( N = \frac{\gamma – \alpha}{\beta}\). Por exemplo, se você encontra em log o endereço0x40011024
, então \(N = \frac{0x40011024 – 0x40011000}{4} = \frac{0x24}{4} = 9.\). Assim, você sabe que esse é o 9º registrador a partir deUART1_BASE
.
Esses exemplos mostram a flexibilidade da aritmética de ponteiros no acesso a qualquer bloco de registradores, seja ele linear (stride = 4 bytes) ou composto (stride > 4 bytes). Com as fórmulas \(\gamma = \alpha + N\,\beta \quad\text{e}\quad N = \frac{\gamma – \alpha}{\beta}\),
você consegue tanto gerar ponteiros de forma simbólica quanto inferir índices para validação e depuração de maneira sistemática.
Boas Práticas de Organização de Código e Macros Dinâmicas
Para manter o código enxuto e evitar dispersão de “números mágicos”, adote sempre uma camada de abstração sobre os endereços-base e offsets. Use typedef
e macros parametrizadas para criar macros dinâmicas que recebam como argumento o índice NNN ou o nome do periférico. Por exemplo, em vez de ter dezenas de definições fixas, poderíamos criar:
// Macro genérica para obter ponteiro a registrador
#define REG32(BASE, OFFSET) ((vuint32_t*)((uintptr_t)(BASE) + (uintptr_t)(OFFSET)))
// Macro dinâmica para blocos com stride maior
#define REG32_STRIDE(BASE, STRIDE, IDX, SUBOFFSET) \
((vuint32_t*)((uintptr_t)(BASE) + ((uintptr_t)(STRIDE) * (IDX)) + (uintptr_t)(SUBOFFSET)))
Aqui, λ\lambdaλ (lambda) poderia simbolizar a função de mapeamento que, dado um bloco e um índice, retorna o endereço correto. Em C, essa “função” é a macro REG32_STRIDE
, que une o endereço-base α\alphaα, o stride σ\sigmaσ (que pode ser 4 ou outro valor), o índice NNN e um suboffset adicional. Isso reduz drasticamente a repetição de código e facilita futuras manutenções: se o stride ou a organização de blocos mudar, basta ajustar a macro, e todo o projeto se adapta automaticamente.
Além disso, organize as macros e typedef
em um único arquivo de cabeçalho, como stm32_memmap.h
. Dentro dele, defina seções claras para cada tipo de periférico (Φ para GPIO, Ψ para TIM, Ω para UART, etc.), de modo que o programador simplesmente inclua o cabeçalho certo e tenha acesso a todas as definições necessárias, sem precisar decorar endereços. Essa convenção modulariza o projeto e deixa explícita a responsabilidade de cada bloco.
Por fim, crie checks de compilação usando static_assert
(C11) ou #if
/#error
para garantir que offsets e strides sejam múltiplos de 4 bytes. Dessa forma, caso alguém tente definir um offset incorreto, o compilador já gera um erro antes mesmo de subir o firmware. Isso dá robustez ao seu sistema embarcado e evita bugs sutis de alinhamento de memória.
Conclusão
Neste artigo vimos como o endereçamento de memória em STM32 se baseia em blocos de 32 bits que avançam de 4 em 4 bytes. Aprendemos a usar as letras gregas α\alphaα, β\betaβ e γ para representar, respectivamente, o endereço-base, o tamanho do passo (stride) e o endereço calculado. Exploramos a aritmética de ponteiros, que já incorpora o fator de 4 bytes, e conferimos fórmulas diretas e inversas para gerar e interpretar endereços de registradores. Por fim, discutimos boas práticas de organização de código, com macros dinâmicas e validações em tempo de compilação, garantindo clareza e segurança nos acessos a periféricos.
Com esse conjunto de conceitos e ferramentas — que vão de λ como função de mapeamento até verificações de alinhamento — você está apto a desenvolver drivers e módulos para STM32 com confiança, legibilidade e sem “números mágicos”. Agora é só aplicar esses padrões nos seus projetos e aproveitar a simplicidade e potência oferecidas pela aritmética de ponteiros em C.