Início Artigos Técnicos Microcontroladores
Microcontroladores

PIC Assembly: otimizando loops com técnicas de Dattalo

Aplicando as técnicas de Scott Dattalo para criar loops eficientes em Assembly PIC — redução de ciclos, delays precisos, loops aninhados e uso inteligente da memória de programa.

PIC Assembly DECFSZ Loops Otimização Dattalo
SD
Este artigo aplica e expande técnicas documentadas por Scott Dattalo — engenheiro eletrônico e uma das maiores referências em algoritmos para microcontroladores PIC. Seus artigos originais estão reunidos em camposlh.com/dattalo.

Introdução: cada ciclo conta

Programar em Assembly para microcontroladores PIC é, em essência, um exercício de economia. Com um clock de 4 MHz e um ciclo de instrução de 1 µs, cada instrução desperdiçada tem um custo real e mensurável. Em sistemas de tempo real — controle de motores, geração de sinais, comunicação serial por software — a diferença entre um loop de 5 ciclos e um de 8 ciclos pode significar a diferença entre um sistema que funciona e um que falha.

Scott Dattalo, em seus artigos técnicos sobre PIC Assembly, demonstrou repetidamente que a forma mais eficiente de resolver um problema raramente é a mais óbvia. Suas técnicas para delays, PWM e geração de sinais dependem fundamentalmente de loops bem construídos. Este artigo extrai e aprofunda esses padrões, apresentando-os de forma sistemática com exemplos práticos e análise de ciclos.

📐
Referência de ciclos de instrução no PIC (baseline/midrange)
A maioria das instruções PIC executa em 1 ciclo de instrução = 4 ciclos de clock. Instruções de desvio (GOTO, CALL, RETURN) e instruções de skip quando o skip é tomado executam em 2 ciclos de instrução. Quando o skip não é tomado, executa em 1 ciclo. Isso é fundamental para calcular delays precisos.

1. As instruções de loop do PIC

O PIC midrange (PIC16Fxxx) não possui uma instrução de loop dedicada como o DJNZ do 8051. Em vez disso, os loops são construídos com duas instruções fundamentais que, combinadas, formam o núcleo de quase todo loop em Assembly PIC.

DECFSZ — Decrement and Skip if Zero

A instrução DECFSZ f, d decrementa o registrador f e pula a próxima instrução se o resultado for zero. É a instrução de loop mais importante do PIC. Sua semântica exata é:

PIC Assembly — DECFSZ 1 ciclo (sem skip) / 2 ciclos (com skip)
; DECFSZ f, d
; d = W (0): resultado vai para W
; d = F (1): resultado fica em f (mais comum)

DECFSZ  COUNT, F       ; COUNT-- ; se COUNT==0, pula próxima instrução
GOTO    loop_inicio   ; 2 ciclos (se não pulado)
; continua aqui quando COUNT chega a 0

Um detalhe crítico: DECFSZ decrementa antes de testar. Isso significa que um registrador inicializado com 0 executará o loop 256 vezes (de 0 → 255 → 254 → ... → 1 → 0), não zero vezes. Inicializar com N executa o loop exatamente N vezes.

INCFSZ — Increment and Skip if Zero

A instrução complementar INCFSZ f, d incrementa e pula se o resultado for zero (overflow de 8 bits). É menos comum para loops de contagem simples, mas útil em padrões específicos de otimização que veremos adiante.

InstruçãoOperaçãoSkip quandoCiclos (sem skip)Ciclos (com skip)
DECFSZ f, df = f − 1resultado = 012
INCFSZ f, df = f + 1resultado = 0 (overflow)12
BTFSC f, btesta bit b de fbit = 012
BTFSS f, btesta bit b de fbit = 112
GOTO labelPC ← label2

2. O loop básico e seu custo real

O loop mais simples em PIC Assembly — contar de N até 0 — parece trivial, mas já revela a primeira oportunidade de otimização. Compare as duas versões abaixo:

✗ Versão ingênua — 4 ciclos/iteração
; Inicialização
    MOVLW  D'10'
    MOVWF  COUNT

loop:
    ; ... trabalho aqui ...
    DECF   COUNT, F    ; 1 ciclo
    BTFSS  STATUS, Z  ; 1 ciclo
    GOTO   loop        ; 2 ciclos
; Total overhead: 4 ciclos/iteração
✓ Com DECFSZ — 3 ciclos/iteração
; Inicialização
    MOVLW  D'10'
    MOVWF  COUNT

loop:
    ; ... trabalho aqui ...
    DECFSZ COUNT, F    ; 1 ciclo (2 na última)
    GOTO   loop        ; 2 ciclos
; Total overhead: 3 ciclos/iteração
; Economia: 25% menos overhead

A versão com DECFSZ elimina uma instrução por iteração. Para um loop de 256 iterações, isso representa 256 ciclos economizados — com clock de 4 MHz, são 64 µs a menos de overhead puro. Em loops internos de algoritmos críticos, essa diferença se multiplica.

💡
Técnica de Dattalo: eliminar o GOTO com NOP
Em alguns casos, é possível eliminar completamente o GOTO usando a estrutura inversa do loop: em vez de pular o GOTO quando zero, pular o NOP (ou a próxima instrução útil) quando não zero. Isso funciona quando o corpo do loop cabe em um bloco de código linear. Veremos essa técnica na seção de delays.

3. Delays precisos: a técnica de Dattalo

Gerar delays precisos sem usar timers de hardware é um dos problemas mais clássicos do Assembly PIC. A abordagem ingênua — um loop simples de decremento — funciona, mas desperdiça ciclos e dificulta o cálculo exato do tempo. Dattalo documentou uma família de técnicas que permitem delays arbitrariamente precisos com código mínimo.

A fórmula fundamental do delay

Um loop de delay com DECFSZ e GOTO tem o seguinte custo exato:

Tdelay = 3 × N − 1 ciclos de instrução

Onde N é o valor inicial do contador (1 a 256). O "−1" vem do fato de que na última iteração o DECFSZ executa em 2 ciclos (skip tomado) mas o GOTO não é executado, economizando 1 ciclo. Para N = 0 (256 iterações): T = 3 × 256 − 1 = 767 ciclos.

PIC Assembly — Delay básico calibrado 3N − 1 ciclos
; Delay de exatamente (3*N - 1) ciclos de instrução
; Para N=100: 299 ciclos = 74.75 µs @ 4MHz
; Para N=0 (256): 767 ciclos = 191.75 µs @ 4MHz

delay_3N:
    MOVLW  N              ; 1 ciclo (fora do loop)
    MOVWF  COUNT          ; 1 ciclo (fora do loop)
delay_loop:
    DECFSZ COUNT, F      ; 1 ciclo (2 na última)
    GOTO   delay_loop    ; 2 ciclos
    RETURN               ; 2 ciclos (se chamado via CALL)

Ajuste fino: delays não múltiplos de 3

O problema com o loop de 3 ciclos é que só gera delays múltiplos de 3 (menos 1). Para delays arbitrários, Dattalo usa a técnica de preenchimento com NOPs antes ou depois do loop:

PIC Assembly — Delay de exatamente 100 ciclos 100 ciclos exatos
; Queremos exatamente 100 ciclos.
; 3*N - 1 = 100  →  N = 33.67 (não inteiro!)
; Solução: usar N=33 → 98 ciclos, + 2 NOPs = 100 ciclos
; OU: usar N=34 → 101 ciclos, mas isso é 1 a mais...
; Melhor: N=34 → 101 ciclos, remover 1 NOP do overhead

delay_100:
    MOVLW  D'33'          ; 1 ciclo
    MOVWF  COUNT          ; 1 ciclo
    NOP                  ; 1 ciclo  ← ajuste fino
    NOP                  ; 1 ciclo  ← ajuste fino
delay_100_loop:
    DECFSZ COUNT, F      ; 1 ciclo (2 na última)
    GOTO   delay_100_loop ; 2 ciclos
; Total: 2 (setup) + 2 (NOPs) + (3*33 - 1) = 4 + 98 = 102
; Hmm, 2 a mais. Ajuste: N=33, 1 NOP
; 2 (setup) + 1 (NOP) + (3*33 - 1) = 3 + 98 = 101
; Para 100 exatos sem setup no caminho crítico:

delay_100_v2:
    MOVLW  D'32'          ; 1 ciclo
    MOVWF  COUNT          ; 1 ciclo
    GOTO   $+1            ; 2 ciclos (NOP de 2 ciclos)
delay_100_v2_loop:
    DECFSZ COUNT, F      ; 1 ciclo (2 na última)
    GOTO   delay_100_v2_loop ; 2 ciclos
; Total: 1+1+2 + (3*32 - 1) = 4 + 95 = 99 ciclos
; + 1 NOP = 100 ciclos exatos ✓
💬
Insight de Dattalo: GOTO $+1 como NOP de 2 ciclos
O truque GOTO $+1 (salto para o endereço seguinte) é um NOP de 2 ciclos — útil quando você precisa adicionar exatamente 2 ciclos de ajuste sem usar dois NOPs separados. Dattalo usa esse padrão extensivamente nos artigos de PWM e delays para alinhar o timing sem desperdiçar memória de programa.

4. Loops aninhados: multiplicando o alcance

Um único loop de 8 bits gera no máximo 767 ciclos de delay (N=256). Para delays maiores — milissegundos ou segundos — são necessários loops aninhados. A técnica de Dattalo para loops aninhados é elegante: cada nível externo multiplica o alcance do loop interno.

Delay de milissegundos

Com clock de 4 MHz (1 ciclo de instrução = 1 µs), um delay de 1 ms requer 1000 ciclos. Um loop simples de 8 bits chega a no máximo 767 ciclos, então precisamos de dois níveis:

PIC Assembly — Delay de 1 ms @ 4 MHz ≈ 1000 ciclos
; Delay de aproximadamente 1 ms com clock de 4 MHz
; Estratégia: loop externo (N2) × loop interno (N1)
; Custo por iteração externa: 3*N1 - 1 + 3 ciclos (overhead externo)
; Total ≈ (3*N1 + 2) * N2
;
; Para 1000 ciclos: N1=100, N2=3 → (3*100+2)*3 = 916 (muito pouco)
; N1=110, N2=3 → (332)*3 = 996 ≈ 1000 ✓

delay_1ms:
    MOVLW  D'3'
    MOVWF  OUTER          ; loop externo: 3 iterações
outer_loop:
    MOVLW  D'110'
    MOVWF  INNER          ; loop interno: 110 iterações
inner_loop:
    DECFSZ INNER, F      ; 1 ciclo (2 na última)
    GOTO   inner_loop    ; 2 ciclos → 3 ciclos/iteração interna
    DECFSZ OUTER, F      ; 1 ciclo (2 na última)
    GOTO   outer_loop    ; 2 ciclos → 3 ciclos/iteração externa
    RETURN               ; 2 ciclos

; Cálculo exato:
; Setup: 2 ciclos (MOVLW + MOVWF do outer)
; Por iteração externa:
; - 2 ciclos (MOVLW + MOVWF do inner)
; - (3*110 - 1) = 329 ciclos (loop interno)
; - 3 ciclos (DECFSZ outer + GOTO, exceto última)
; Total por iteração externa: 334 ciclos
; 3 iterações externas: 3 * 334 - 1 = 1001 ciclos ≈ 1 ms ✓

Delay paramétrico: passando N via W

Uma variação muito útil documentada por Dattalo é o delay paramétrico — onde o número de milissegundos é passado no registrador W antes da chamada. Isso permite um único sub-rotina de delay reutilizável:

PIC Assembly — Delay paramétrico (N × 1ms)
; Chame com: MOVLW N ; CALL delay_Nms
; Executa delay de N milissegundos (N = 1..255)

delay_Nms:
    MOVWF  MS_COUNT       ; salva N
ms_outer:
    MOVLW  D'110'
    MOVWF  INNER
ms_inner:
    DECFSZ INNER, F
    GOTO   ms_inner      ; loop de ~1ms
    DECFSZ MS_COUNT, F   ; decrementa contador de ms
    GOTO   ms_outer
    RETURN

; Uso:
    MOVLW  D'250'         ; 250 ms
    CALL   delay_Nms

    MOVLW  D'1'           ; 1 ms
    CALL   delay_Nms

5. O loop sem GOTO: a técnica de Dattalo para loops ultra-eficientes

A técnica mais elegante de Dattalo para loops é a eliminação completa do GOTO. Em vez de usar DECFSZ + GOTO (3 ciclos/iteração), ele usa a estrutura de skip invertido para criar um loop de apenas 2 ciclos por iteração — uma redução de 33%.

A ideia é: em vez de pular o GOTO quando o contador chega a zero, pular o DECFSZ (ou equivalente) quando o contador ainda não chegou a zero. Isso é possível quando o corpo do loop tem exatamente 1 instrução:

PIC Assembly — Loop de 2 ciclos (sem GOTO) 2N ciclos
; Técnica: INCFSZ com contador inicializado em (256 - N)
; O loop executa N vezes em exatamente 2N ciclos
; (vs 3N-1 ciclos com DECFSZ+GOTO)

; Para N=100 iterações: inicializar com 256-100 = 156
    MOVLW  256 - 100      ; = 156 = 0x9C
    MOVWF  COUNT

loop_2cycles:
    INCFSZ COUNT, F      ; 1 ciclo (2 na última: overflow para 0)
    GOTO   loop_2cycles  ; 2 ciclos... espera, ainda é 3!

; A versão realmente de 2 ciclos usa RETLW (para tabelas):
; Ou usa o padrão de "unrolled loop" de Dattalo:

; PADRÃO DATTALO: loop de 2 ciclos com DECFSZ + NOP
; Funciona quando o "trabalho" do loop é exatamente 1 instrução

loop_dattalo_2:
    NOP                  ; 1 ciclo ← trabalho do loop
    DECFSZ COUNT, F      ; 1 ciclo (2 na última)
    GOTO   loop_dattalo_2 ; 2 ciclos
; Ainda 3 ciclos... A verdadeira economia vem do loop desenrolado

Loop desenrolado (loop unrolling)

A técnica mais poderosa de Dattalo para loops de alto desempenho é o loop desenrolado (loop unrolling): em vez de executar N iterações de um loop, duplicar o corpo do loop N vezes no código. Isso elimina completamente o overhead de controle (DECFSZ + GOTO = 3 ciclos) ao custo de mais memória de programa.

PIC Assembly — Comparação: loop vs. desenrolado
; Tarefa: executar "BSF PORTB, 0" 4 vezes

; VERSÃO COM LOOP: 4 * 3 - 1 = 11 ciclos + 3 setup = 14 ciclos
; Memória: 4 instruções
    MOVLW  D'4'
    MOVWF  COUNT
loop_v:
    BSF    PORTB, 0
    DECFSZ COUNT, F
    GOTO   loop_v

; VERSÃO DESENROLADA: 4 ciclos, sem overhead
; Memória: 4 instruções (mesmo custo!)
    BSF    PORTB, 0      ; 1 ciclo
    BSF    PORTB, 0      ; 1 ciclo
    BSF    PORTB, 0      ; 1 ciclo
    BSF    PORTB, 0      ; 1 ciclo
; Economia: 10 ciclos (71% mais rápido!)
⚠️
Loop unrolling: quando vale a pena?
O loop desenrolado é vantajoso quando N é pequeno (≤ 8) e o corpo do loop é simples. Para N grande, o custo de memória de programa supera o ganho de velocidade. O PIC16F tem 2K ou 8K palavras de programa — use loop unrolling criteriosamente em seções críticas de tempo, não como regra geral.

6. Tabelas com RETLW: o loop que não parece loop

Uma das técnicas mais elegantes do PIC Assembly, amplamente usada por Dattalo em seus artigos de geração de sinais, é a tabela de lookup implementada com RETLW. Ela é, na essência, um "loop" de acesso indexado à memória de programa — mas sem nenhum overhead de controle.

PIC Assembly — Tabela com RETLW (técnica de Dattalo) 3 ciclos por acesso (CALL + RETLW)
; Tabela de seno com 16 amostras (0 a 90 graus)
; Valores em ponto fixo Q0.8 (0 a 255 = 0.0 a 1.0)

; Acesso: W = índice (0..15), CALL sine_table → W = valor
sine_table:
    ADDWF  PCL, F         ; PC = PC + W (salta para entrada)
    RETLW  0x00           ; sin(0°)   = 0
    RETLW  0x1B           ; sin(6°)   ≈ 0.105
    RETLW  0x36           ; sin(12°)  ≈ 0.208
    RETLW  0x50           ; sin(18°)  ≈ 0.309
    RETLW  0x67           ; sin(24°)  ≈ 0.407
    RETLW  0x7B           ; sin(30°)  = 0.500
    RETLW  0x8D           ; sin(36°)  ≈ 0.588
    RETLW  0x9B           ; sin(42°)  ≈ 0.669
    RETLW  0xA7           ; sin(48°)  ≈ 0.743
    RETLW  0xB0           ; sin(54°)  ≈ 0.809
    RETLW  0xB8           ; sin(60°)  ≈ 0.866
    RETLW  0xBD           ; sin(66°)  ≈ 0.914
    RETLW  0xC0           ; sin(72°)  ≈ 0.951
    RETLW  0xC1           ; sin(78°)  ≈ 0.978
    RETLW  0xC2           ; sin(84°)  ≈ 0.995
    RETLW  0xC3           ; sin(90°)  = 1.000

; Uso: W = índice → CALL sine_table → W = valor
    MOVLW  D'7'           ; índice 7 = sin(42°)
    CALL   sine_table     ; W = 0x9B ≈ 0.669

O truque ADDWF PCL, F soma o índice ao contador de programa, saltando diretamente para a entrada correta da tabela. Cada RETLW carrega o valor em W e retorna em 2 ciclos. O custo total é 3 ciclos por acesso (1 para ADDWF + 2 para RETLW) — independente do tamanho da tabela.

⚠️
Cuidado com o limite de página
A instrução ADDWF PCL, F modifica apenas os 8 bits baixos do PC. Se a tabela cruzar um limite de 256 palavras (página de 256 endereços), o acesso será incorreto. Dattalo recomenda sempre alinhar tabelas ao início de uma página usando a diretiva ORG ou garantir que a tabela inteira caiba dentro de 256 endereços.

7. Loops em geração de PWM: aplicando Dattalo

O artigo de PWM de Dattalo (disponível em camposlh.com/dattalo/pwm.html) demonstra como loops bem construídos são a base da geração de PWM por software. A técnica de phase-shifted counters usa dois contadores que incrementam no mesmo loop, com o output sendo controlado pelos overflows.

PIC Assembly — PWM por software (técnica Dattalo) Loop de 8 ciclos por iteração
; PWM por software com phase-shifted counters
; Resolução: 8 bits (256 níveis de duty cycle)
; Frequência PWM = Fclk / (4 * 256 * ciclos_loop)

; Registradores:
; RISING  - contador de borda de subida
; FALLING - contador de borda de descida = 256 - duty_cycle
; PORTB,0 - saída PWM

pwm_init:
    CLRF   RISING         ; rising edge counter = 0
    MOVLW  256 - 64       ; duty cycle = 64/256 = 25%
    MOVWF  FALLING

pwm_loop:
    INCFSZ RISING, F      ; 1 ciclo (2 no overflow)
    GOTO   $+2            ; 2 ciclos (pula BSF)
    BSF    PORTB, 0      ; seta saída na borda de subida

    INCFSZ FALLING, F     ; 1 ciclo (2 no overflow)
    GOTO   $+2            ; 2 ciclos (pula BCF)
    BCF    PORTB, 0      ; limpa saída na borda de descida

    GOTO   pwm_loop       ; 2 ciclos

; Custo por iteração normal: 1+2+1+2+2 = 8 ciclos
; Custo na iteração de overflow: 2+1+2+1+2 = 8 ciclos (simétrico!)
; Frequência PWM @ 4MHz: 4MHz / (4 * 8 * 256) ≈ 488 Hz

8. Comparativo de técnicas: ciclos e memória

TécnicaCiclos/iteraçãoInstruções de overheadMemóriaMelhor uso
DECF + BTFSS + GOTO 4 3 Baixa Código legado, compatibilidade
DECFSZ + GOTO 3 2 Baixa Loop geral, padrão mais comum
INCFSZ + GOTO $+2 3–4 2–3 Baixa PWM, contadores de overflow
Loop desenrolado 0 0 Alta (N × corpo) N pequeno, timing crítico
Tabela RETLW 3 (acesso) 1 (ADDWF PCL) N + 1 palavras Lookup de valores, geração de sinais
Loop aninhado 2 níveis ~3 (interno) 4 (2 por nível) Baixa Delays longos, N > 256

Conclusão

As técnicas de Dattalo para loops em PIC Assembly não são apenas truques de otimização — são a expressão de um entendimento profundo da arquitetura Harvard do PIC e do custo real de cada instrução. A filosofia subjacente é sempre a mesma: entender o que o hardware faz em nível de ciclo e explorar isso ao máximo.

Os padrões apresentados aqui — DECFSZ + GOTO como estrutura base, ajuste fino com NOPs e GOTO $+1, loops aninhados para delays longos, loop unrolling para seções críticas e tabelas RETLW para lookup — formam um vocabulário completo para escrever Assembly PIC eficiente. Combinados com as técnicas de PWM e geração de sinais documentadas por Dattalo, eles permitem implementar algoritmos sofisticados em microcontroladores com apenas 2K palavras de programa e 68 bytes de RAM.

📚
Artigos relacionados de Dattalo no site
PWM Techniques — Técnicas de geração de PWM por software usando phase-shifted counters e phase accumulators.
Generating Sine Waves in Software — 10 métodos para geração de senoides, incluindo tabelas RETLW e o algoritmo de Goertzel.
Square Root Theory and Algorithms — Algoritmos de raiz quadrada para microcontroladores de 8 bits.
LH
Luis H. Campos
Engenheiro Eletricista · camposlh.com
Engenheiro Eletricista com mais de 10 anos de experiência em projetos eletrônicos, PCB design e sistemas embarcados. Especializado em microcontroladores PIC, ESP32 e STM32, desenvolvimento de firmware e hardware para aplicações industriais e IoT.