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.
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 é:
; 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ção | Operação | Skip quando | Ciclos (sem skip) | Ciclos (com skip) |
|---|---|---|---|---|
DECFSZ f, d | f = f − 1 | resultado = 0 | 1 | 2 |
INCFSZ f, d | f = f + 1 | resultado = 0 (overflow) | 1 | 2 |
BTFSC f, b | testa bit b de f | bit = 0 | 1 | 2 |
BTFSS f, b | testa bit b de f | bit = 1 | 1 | 2 |
GOTO label | PC ← label | — | 2 | — |
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:
; 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
; 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.
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:
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.
; 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:
; 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 ✓
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:
; 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:
; 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:
; 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.
; 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!)
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.
; 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.
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.
; 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écnica | Ciclos/iteração | Instruções de overhead | Memória | Melhor 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.
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.