4 — Exemplo prático: “Hello World” em assembly para Cortex-M3 com UART hipotética
Nesta seção, vamos unir os conceitos discutidos até aqui em um exemplo concreto e didático. O objetivo é mostrar como um firmware mínimo em assembly Thumb-2 pode inicializar o sistema, escrever caracteres em uma UART hipotética e encerrar a execução. O foco não é em um microcontrolador específico de fabricante, mas no modelo mental e na estrutura típica de um programa bare-metal para Cortex-M3.
Premissas do exemplo
Assumiremos uma UART mapeada em memória com dois registradores simples:
UART_DR— Data Register (escrita envia um byte)UART_SR— Status Register- bit 0 =
TX_READY(1 quando pode transmitir)
- bit 0 =
Endereços hipotéticos:
UART_DR = 0x4000_0000UART_SR = 0x4000_0004
O código é escrito em Thumb-2, usa apenas instruções disponíveis na ARMv7-M, e segue o fluxo típico:
- Reset
- Carregamento do endereço da string
- Loop de transmissão byte a byte
- Espera por
TX_READY
Vetor de interrupções e ponto de entrada
.syntax unified
.cpu cortex-m3
.thumb
.section .isr_vector, "a"
.word _stack_top /* Ponteiro inicial da pilha */
.word Reset_Handler /* Endereço do reset */
Aqui vemos um ponto importante da arquitetura Cortex-M: o vetor de interrupções já contém o endereço da pilha inicial. Não há instrução explícita para configurar SP; o hardware faz isso automaticamente no reset.
Rotina de reset
.section .text
.thumb_func
Reset_Handler:
ldr r0, =hello_str /* r0 aponta para a string */
A instrução ldr r0, =hello_str é um pseudo-opcode. O montador decide se isso vira uma instrução de 16 ou 32 bits, ou se gera um literal pool. Esse é um exemplo prático de como o Thumb-2 abstrai detalhes de codificação, mantendo o código legível.
Loop principal de transmissão
send_loop:
ldrb r1, [r0] /* Carrega um byte da string */
cmp r1, #0 /* Verifica fim da string */
beq end /* Se zero, termina */
wait_tx:
ldr r2, =UART_SR
ldr r3, [r2]
tst r3, #1 /* Testa TX_READY */
beq wait_tx /* Aguarda UART pronta */
ldr r2, =UART_DR
strb r1, [r2] /* Envia o byte */
adds r0, r0, #1 /* Próximo caractere */
b send_loop
Esse trecho concentra vários conceitos-chave:
- Acesso a periféricos por memória mapeada, típico de microcontroladores.
- Uso de
ldrb/strb, reforçando que estamos transmitindo bytes, não palavras. - Estrutura de busy-wait, comum em exemplos iniciais e drivers simples.
- Instruções curtas e previsíveis, todas adequadas ao pipeline simples do Cortex-M3.
Observe que não há nenhuma instrução “especial” de I/O. Para o processador, UART é apenas memória, o que simplifica enormemente o modelo de programação.
Finalização
end:
b end /* Loop infinito */
Em sistemas embarcados bare-metal, é comum finalizar com um loop infinito. Não existe “retorno ao sistema operacional”. Esse detalhe reforça a diferença conceitual entre firmware e software de propósito geral.
String em memória
.section .rodata
hello_str:
.asciz "Hello World\r\n"
A diretiva .asciz adiciona automaticamente o byte nulo (\0) ao final da string, facilitando o controle do loop. Esse padrão é amplamente utilizado em exemplos de baixo nível por sua simplicidade e clareza.
O que esse exemplo ensina, de fato
Mais do que imprimir “Hello World”, esse código demonstra:
- Como o Thumb-2 se encaixa naturalmente em firmware Cortex-M.
- Por que o modelo de execução é determinístico e previsível.
- Como a ausência de modos ARM clássicos simplifica o hardware e o raciocínio.
- Como assembly, mesmo simples, revela claramente a arquitetura subjacente.
Esse tipo de exemplo é extremamente valioso para quem deseja entender o que o compilador C faz por baixo dos panos e para depurar problemas reais de inicialização, comunicação serial e falhas de boot.