Manual de Referência: C para o PIC18F4550

Pense neste apêndice como o caderninho que você deixa aberto ao lado do teclado. Ele não ensina a teoria de cada módulo — ele responde, rápido, às perguntas práticas que sempre voltam: “como era mesmo aquele #pragma?”, “por que meu LED não acende?”, “qual a conta do baud rate?”. Leia uma vez no começo do semestre para saber o que tem aqui e, depois, volte sempre que travar. Tudo é para o PIC18F4550 no kit ACEPIC PRO V8.2, com o compilador XC8.

Por que C, e não só assembly

Olha, dá para fazer tudo em assembly no PIC18 — são só 75 instruções. Mas em assembly você precisa pensar, ao mesmo tempo, no problema (piscar o LED, ler o sensor) e na mecânica do processador (registradores, bancos, pilha). É atenção demais dividida. O compilador XC8 tira esse peso: você escreve LATD = 0xFF; e ele gera a instrução mais enxuta possível — neste caso, setf LATD. Você raciocina no nível do problema; ele raciocina no nível das instruções. Mesmo assim, parte do aprendizado é justamente olhar o assembly que o compilador produz — por isso o material de cada módulo mostra os dois lados.

Criando o projeto no MPLAB X

O fluxo é sempre o mesmo. Em File → New Project → Microchip Embedded → Standalone Project, escolha o dispositivo PIC18F4550 (cuidado para não pegar o PIC18F4520 ou o 4525, que têm periféricos diferentes). Na seleção de ferramenta (Tool), marque No Tool ou Simulator: como a gravação no kit é feita pelo bootloader via USB, o MPLAB X não precisa de um programador físico. No compilador, escolha XC8. Depois crie o main.c em Source Files → New → C Source File.

A estrutura mínima de todo programa

Esse esqueleto se repete em todos os exemplos da disciplina. Repare na ordem: cabeçalhos, fusíveis de configuração, a frequência do cristal e a função main com seu laço infinito.

#include <xc.h>       /* cabeçalho do processador selecionado */
#include <stdint.h>   /* uint8_t, uint16_t, etc. */

#pragma config FOSC   = HS      /* cristal externo de alta velocidade */
#pragma config WDT    = OFF     /* watchdog desligado */
#pragma config LVP    = OFF     /* obrigatório com o bootloader */
#pragma config MCLRE  = ON      /* pino MCLR como reset externo */
#pragma config PBADEN = OFF     /* PORTB<0:4> como digital */
#pragma config CPUDIV = OSC1_PLL2

#define _XTAL_FREQ 8000000UL    /* 8 MHz — usado por __delay_ms/__delay_us */

void main(void)
{
    /* inicialização dos periféricos */
    while (1)
    {
        /* laço principal */
    }
}
#include <xc.h>
#pragma config FOSC=HSPLL_HS, WDT=OFF, LVP=OFF, MCLRE=ON, PBADEN=OFF, CPUDIV=OSC1_PLL2
#define _XTAL_FREQ 8000000UL

void main(void) { while (1); }

Seu primeiro programa: piscar um LED

Antes de qualquer teoria, vamos fazer algo acender e apagar — é o “olá, mundo” do embarcado e o melhor jeito de confirmar que ambiente, compilação e gravação estão todos no lugar. Configure o PORTD como saída e alterne o LED de RD0 com meio segundo de intervalo:

#include <xc.h>
#pragma config FOSC=HSPLL_HS, WDT=OFF, LVP=OFF, MCLRE=ON, PBADEN=OFF, CPUDIV=OSC1_PLL2
#define _XTAL_FREQ 8000000UL

void main(void)
{
    TRISD = 0x00;          /* PORTD como saída (LEDs) */
    LATD  = 0x00;          /* começa apagado */
    while (1)
    {
        LATDbits.LATD0 = 1;   /* acende RD0 */
        __delay_ms(500);
        LATDbits.LATD0 = 0;   /* apaga RD0 */
        __delay_ms(500);
    }
}

Se o LED piscar, você acabou de validar o ciclo inteiro: editou, compilou, gerou o .hex, gravou pelo bootloader e o chip executou. A partir daqui, o resto do manual é só ir trocando o miolo do laço.

Os fusíveis de configuração (#pragma config)

Esses fusíveis são gravados junto com o código e decidem como o chip se comporta antes de a primeira instrução rodar. Um bit errado aqui faz o programa simplesmente não funcionar — e você procura o erro no lugar errado por horas. Por isso vale memorizar os valores certos para o kit.

Três deles merecem atenção especial. O FOSC = HSPLL_HS seleciona o oscilador de cristal de alta velocidade, que é o cristal de 8 MHz soldado no kit; o clock de instrução é sempre F_{OSC}/4, ou seja, 2 MHz. O LVP = OFF é, na prática, o mais perigoso de errar: com LVP = ON, o pino RB5 vira sinal de programação e o bootloader do kit deixa de responder — você precisaria de um gravador externo para recuperar. E o PBADEN = OFF garante que RB0–RB4 nasçam como pinos digitais; se ficar em ON, ler um botão em RB0 devolve lixo, porque o pino está ligado ao conversor analógico.

Campo Use Por quê
FOSC HS cristal de 8 MHz do kit
WDT OFF evita resets durante o desenvolvimento
LVP OFF obrigatório para o bootloader não travar
MCLRE ON botão de reset físico do kit
PBADEN OFF PORTB digital por padrão
CPUDIV OSC1_PLL2 divisor padrão com FOSC=HSPLL_HS

O bootloader e o mapa de memória

O kit não usa gravador de hardware: ele tem um bootloader gravado de fábrica que recebe seu programa pela própria USB. Em troca, ele ocupa uma faixa da Flash que você não pode pisar. A Flash do PIC18F4550 tem 32 KB (de 0x000000 a 0x007FFF); no kit, o bootloader fica nos primeiros 4 KB e seu código começa em 0x001000.

block-beta
    columns 1
    block:flash["Flash — 32 KB"]:1
        A["0x000000–0x000FFF<br/>Bootloader (não sobrescrever!)"]
        B["0x001000<br/>vetor de reset do usuário (GOTO main)"]
        C["0x001008 / 0x001018<br/>vetores de interrupção do usuário"]
        E["0x001020–0x007FFF<br/>seu código e dados const"]
    end

Se você não configurar o início do código em 0x001000 (no Project Properties → XC8 Linker), a ferramenta de gravação escreve a partir de 0x000000 e apaga o bootloader. O kit para de responder pela USB e só um gravador externo (PICkit) o recupera. Confira isso antes de gravar.

Para gravar: compile com F11, gere o .hex, segure o reset enquanto conecta o USB (o kit entra em modo bootloader e o LED pisca de um jeito característico), abra o software HID Bootloader, selecione o .hex e clique em Program. Reconecte o USB e seu programa roda.

Como o C enxerga a memória do chip

Vale ter na cabeça onde cada coisa mora, porque isso explica vários comportamentos que parecem mágica. A Flash (32 KB) guarda o código compilado e tudo que você declara com const — e aqui há um detalhe que aparece nos exercícios: ler um array const na Flash exige as instruções TBLRD, então o compilador gera código bem diferente para const int tabela[] e para um array normal na RAM. A RAM (2 KB) guarda as variáveis; as locais vão para a pilha de software, as globais e estáticas ficam em posições fixas que o linker escolhe.

Dentro da RAM há uma região especial, o Access Bank (0x0000x05F mais os SFRs em 0x0600x0FF), que o processador acessa sem trocar de banco — por isso é mais rápida. O XC8 tenta colocar ali as variáveis mais usadas; você pode forçar com o qualificador __near. Os SFRs (TRIS, LAT, PORT, ADCON1, RCSTA…) são posições de RAM mapeadas para os periféricos, e estão todos declarados em <xc.h> — por isso você os usa por nome, sem decorar endereço.

Região Endereço Conteúdo
Flash — bootloader 0x000000–0x000FFF bootloader (não tocar)
Flash — usuário 0x001000–0x007FFF código e dados const
RAM — Access Bank 0x000–0x05F variáveis de acesso rápido
RAM — SFRs 0x060–0x0FF periféricos mapeados
RAM — bancos 1–14 0x100–0xEFF variáveis normais
EEPROM de dados 256 bytes não voláteis

Entrada e saída: TRIS, LAT e PORT

Esse é o trio que mais confunde no início, então vamos com calma. Cada porta (PORTA até PORTE) tem três registradores. O TRIS define a direção de cada pino: bit 0 é saída, bit 1 é entrada (e tudo nasce como entrada, por segurança). O LAT é por onde você escreve a saída. O PORT é por onde você lê o nível real do pino.

A regra de ouro: para escrever, use LAT; para ler, use PORT. Escrever direto em PORT funciona, mas dispara um problema sutil chamado read-modify-write hazard — a escrita lê o nível físico do pino antes de gravar, e se ele ainda não estabilizou, você pega o valor errado. LAT guarda o que você quis impor, então nunca mente.

flowchart LR
    LAT["LAT (escrita)"] -->|"TRIS=0 (saída)"| PAD["pino físico"]
    PAD -->|"TRIS=1 (entrada)"| PORT_R["PORT (leitura)"]
    EXT["sinal externo"] --> PAD
    TRIS["TRIS (direção)"] -->|controla| PAD

E aqui mora o bug clássico da disciplina: na partida, RA0–RA5 e RB0–RB4 estão em modo analógico, e ler PORT nesses pinos devolve sempre 0, mesmo com 5 V no pino. A cura é desligar o analógico logo no começo: ADCON1 = 0x0F deixa todos os pinos de RA e RB digitais, e CMCON = 0x07 desliga os comparadores. Faça disso um reflexo.

void main(void)
{
    ADCON1 = 0x0F;   /* RA e RB como digital (senão PORT lê 0) */
    CMCON  = 0x07;   /* comparadores desligados */

    TRISD  = 0x00;   /* PORTD saída (LEDs) */
    TRISB  = 0xFF;   /* PORTB entrada (botões) */
    LATD   = 0x00;   /* começa com os LEDs apagados */

    INTCON2bits.RBPU = 0;  /* pull-ups internos de PORTB ligados */

    while (1)
    {
        if (PORTBbits.RB0 == 0)   /* botão em RB0 (lógica invertida) */
            LATD = 0xFF;          /* acende tudo */
        else
            LATD = 0x00;          /* apaga tudo */
    }
}
void main(void) {
    ADCON1 = 0x0F; CMCON = 0x07;
    TRISD = 0x00; TRISB = 0xFF; LATD = 0x00;
    INTCON2bits.RBPU = 0;
    while (1) LATD = PORTBbits.RB0 ? 0x00 : 0xFF;
}

Para mexer em um bit só, use a notação de campo: LATDbits.LATD3 = 1; acende o LED de RD3. O XC8 compila isso direto para bsf/bcf, que fazem leitura-modificação-escrita em um ciclo — bem mais barato que mexer no byte inteiro.

Tempo: __delay_ms e __delay_us

Para esperar, o XC8 dá duas macros: __delay_ms(t) e __delay_us(t). As duas dependem de _XTAL_FREQ estar definida com o clock certo (8 MHz no kit), porque é desse valor que o compilador calcula quantas voltas de laço gastam o tempo pedido. Com clock de instrução de 2 MHz, cada ciclo dura T_{CY} = 1/2\,\text{MHz} = 500\,\text{ns}, então 1 ms são 2000 ciclos.

Dois detalhes que pegam todo mundo: o argumento precisa ser uma constante (o cálculo é feito na compilação, não em tempo de execução), e a macro trava a CPU durante toda a espera. Para atraso variável, faça um laço for (i=0;i<ms;i++) __delay_ms(1);. Para tempo de verdade sem desperdiçar a CPU, use os Timers (módulos de periféricos).

Quando o atraso precisa variar em tempo de execução (o argumento de __delay_ms precisa ser constante), o jeito é envolver a macro num laço — cada volta gasta exatamente 1 ms:

void delay_ms_var(uint16_t ms) {   /* ms de 0 a 65535 */
    for (uint16_t i = 0; i < ms; i++)
        __delay_ms(1);
}

Lendo o assembly que o XC8 gera

Parte do que você aprende nesta disciplina é olhar por baixo do C. Sempre que compila, o MPLAB X pode mostrar o assembly produzido (na janela de desmontagem, ou no arquivo de listagem), e isso é ouro para entender custo e para depurar. Quando você escreve LATD = 0xFF;, o XC8 gera uma única setf LATD; quando escreve LATDbits.LATD3 = 1;, gera um bsf LATD, 3. Um if (PORTBbits.RB0 == 0) vira um par btfsc/bra. E o tal código diferente para arrays: um const int t[] (que mora na Flash) acessa elementos com a família tblrd, enquanto um array na RAM usa endereçamento indireto via FSR. Ver esse mapeamento explica por que duas linhas de C parecidas podem ter custos bem diferentes — e o Manual de Assembly deste apêndice é o seu dicionário para ler o que aparece ali.

Depurando sem gravador: as três técnicas que funcionam

No kit você não tem um depurador de hardware conectado (os pinos de ICSP são usados pelo bootloader). Então a gente depura por software, e funciona muito bem.

A primeira técnica é depurar com os LEDs. Mapeie estados do programa para padrões binários no PORTD: acenda 0x01 ao terminar a inicialização, 0x03 ao configurar os periféricos, 0xFF ao entrar no laço. Se o kit parou com 0x03 aceso e 0xFF nunca apareceu, você sabe exatamente entre quais etapas a coisa quebrou.

A segunda, mais poderosa, é mandar texto pela serial (USART). O PIC18F4550 transmite por RC6 (TX) e recebe por RC7 (RX). Redefinindo a função putch, você ganha printf direto para um terminal no PC (PuTTY, Tera Term). O ponto delicado é a conta do baud rate. No modo assíncrono com BRGH=1:

SPBRG = \frac{F_{OSC}}{16 \times BaudRate} - 1

Para 9600 bps com 8 MHz, dá SPBRG = 8\,000\,000 / (16 \times 9600) - 1 \approx 51. Com 51, o baud real é 8\,000\,000/(16 \times 52) = 9615 bps — erro de só 0,16%, bem dentro dos ±2% que o protocolo tolera.

void putch(char c) { while (!TXSTAbits.TRMT); TXREG = c; }

void uart_init(void) {
    TRISCbits.TRISC7 = 1;                  /* RX entrada */
    SPBRG = (_XTAL_FREQ / (16UL * 9600)) - 1;
    TXSTA = 0x24;   /* SYNC=0, BRGH=1, TXEN=1 */
    RCSTA = 0x90;   /* SPEN=1, CREN=1 */
}
/* depois disso, printf("contador=%u\r\n", c); funciona */

A terceira é o simulador do MPLAB X (Project Properties → Tool → Simulator). Ele roda o programa sem hardware, deixa pôr breakpoints e inspecionar variáveis e SFRs. É ótimo para validar a lógica, mas não emula bem o tempo real nem a USB — para timing e resposta do hardware, grave no kit e use LED/UART.

Em todos os casos, depure com método: isole o menor trecho que ainda mostra o problema, levante uma hipótese (“desconfio que contador não incrementa”), teste com uma das técnicas, e só então corrija. Tentativa e erro às cegas custa caro.

Um idioma que você vai repetir: debounce de botão

Botão mecânico “treme” ao ser pressionado (bounce), gerando várias transições para um único toque. Sem tratar isso, um toque vira vários eventos. O remédio simples é esperar um tempinho e confirmar:

#define BOTAO  PORTBbits.RB0
uint8_t pressionou(void) {
    if (BOTAO == 0) {            /* talvez pressionado */
        __delay_ms(20);          /* deixa o bounce passar */
        if (BOTAO == 0) {        /* confirmou */
            while (BOTAO == 0);  /* espera soltar */
            __delay_ms(20);
            return 1;
        }
    }
    return 0;
}

Referência rápida

Porta Direção Saída Leitura Observação
PORTA TRISA LATA PORTA RA0–RA5 analógicos por padrão (ADCON1)
PORTB TRISB LATB PORTB RB0–RB4 analógicos por padrão (PBADEN)
PORTC TRISC LATC PORTC RC6=TX, RC7=RX (USART)
PORTD TRISD LATD PORTD totalmente digital; LEDs do kit
PORTE TRISE LATE PORTE RE3=MCLR quando MCLRE=ON
SFR Valor Efeito
ADCON1 0x0F RA e RB digitais
CMCON 0x07 comparadores desligados
INTCON2 (RBPU) 0 pull-ups de PORTB ligados
TXSTA 0x24 assíncrono, BRGH=1, TX ligado
RCSTA 0x90 SPEN=1, CREN=1

Checklist antes de gravar no kit

Antes de cada gravação, passe os olhos por aqui — economiza muita depuração. Primeiro, confira os #pragma config (principalmente LVP=OFF). Segundo, _XTAL_FREQ é 8000000UL. Terceiro, o linker XC8 começa o código em 0x001000. Quarto, há ADCON1 = 0x0F e CMCON = 0x07 se você usa pinos de RA ou RB. Quinto, todo pino tem o TRIS ajustado antes de ser usado. Sexto, a compilação fechou sem erros nem avisos estranhos. Só então: kit em modo bootloader e gravar.

Para aprofundar

Quando quiser o nome exato de um campo de bit, abra o arquivo de definições do XC8 (.../xc8/vX.Y/pic/include/proc/pic18f4550.h): ele declara cada SFR e cada bit. A referência definitiva de todos os periféricos é o PIC18F4550 Data Sheet (DS39632E) da Microchip; as extensões do compilador (incluindo os #pragma config) estão no MPLAB XC8 C Compiler User’s Guide (DS50002053).