Pular para o conteúdo

Porta Serial

A porta serial, controlada por um chip UART (Universal Asynchronous Receiver/Transmitter), é uma das peças de hardware mais simples com que um kernel x86 consegue se comunicar, o que é precisamente o que a torna valiosa muito antes de ser útil de outra forma: bem antes de um kernel ter um driver VGA funcionando, um sistema de arquivos, ou até mesmo tratamento confiável de interrupção, uma porta serial já pode estar transmitindo saída de depuração legível, tanto para um cabo serial físico quanto, dentro de praticamente todo emulador, diretamente para o terminal do host, sem cabeamento físico algum envolvido.

Portas seriais em uma máquina compatível com PC são convencionalmente expostas em um pequeno número de faixas fixas de porta de I/O, cada uma abrangendo oito portas consecutivas a partir de um endereço-base:

Nome da portaEndereço-baseIRQ legada
COM10x3F84
COM20x2F83
COM30x3E84
COM40x2E83

COM1, na base 0x3F8, é de longe a mais comumente usada para saída de depuração do kernel, tanto porque é a mais confiavelmente presente quanto porque o QEMU e a maioria dos outros emuladores a mapeiam diretamente para o terminal do host ou um arquivo de log por padrão, sem exigir configuração alguma do lado do guest além de programar a própria UART.

As oito portas começando no endereço-base de uma UART não são oito registradores independentes no sentido comum, já que várias são reutilizadas para propósitos diferentes dependendo de um bit de controle, um legado de encaixar a funcionalidade de um chip razoavelmente capaz em um pequeno número de endereços de I/O:

OffsetLeituraEscrita
+0Buffer de recepção (se DLAB=0)Buffer de transmissão (se DLAB=0)
+0Byte baixo do divisor (se DLAB=1)Byte baixo do divisor (se DLAB=1)
+1Habilitação de interrupção (se DLAB=0)Habilitação de interrupção (se DLAB=0)
+1Byte alto do divisor (se DLAB=1)Byte alto do divisor (se DLAB=1)
+2Identificação de interrupçãoControle de FIFO
+3Controle de linhaControle de linha
+4Controle de modemControle de modem
+5Status de linha(nenhuma)

O DLAB (Divisor Latch Access Bit), bit 7 do registrador Line Control, é o que governa essa reutilização: com ele definido, as duas portas mais baixas expõem temporariamente o divisor de baud rate em vez dos registradores de buffer de transmissão/recepção e habilitação de interrupção, e precisam ser limpas depois para restaurar a transferência normal de dados.

Uma sequência mínima de inicialização desabilita interrupções (para uma implementação apenas por polling), define o divisor de baud rate, e configura o formato de linha:

void serial_init(uint16_t port) {
outb(port + 1, 0x00); // desabilita interrupções
outb(port + 3, 0x80); // define DLAB para acessar o divisor
outb(port + 0, 0x03); // byte baixo do divisor: 3 -> 38400 baud (115200 / 3)
outb(port + 1, 0x00); // byte alto do divisor
outb(port + 3, 0x03); // 8 bits, sem paridade, 1 stop bit; limpa DLAB
outb(port + 2, 0xC7); // habilita FIFO, limpa, threshold de 14 bytes
outb(port + 4, 0x0B); // habilita pino de IRQ, RTS/DSR definidos
}

O divisor define a baud rate como uma fração do clock interno fixo da UART (convencionalmente 115200 para a UART padrão compatível com PC), de modo que um divisor de 3 resulta em 115200⁄3, ou 38400 baud; ambas as pontas de uma conexão serial precisam concordar na baud rate, e no formato de linha configurado no mesmo passo (bits de dados, paridade, stop bits), ou os dados recebidos sairão corrompidos, mesmo que a própria conexão elétrica esteja funcionando corretamente.

Enviar um byte exige esperar até que o registrador de transmissão da UART esteja de fato vazio antes de escrever nele, verificado através do bit 5 do registrador Line Status; escrever antes que o byte anterior tenha sido aceito o sobrescreve, em vez de enfileirar atrás dele, já que o buffer de transmissão bruto contém apenas um único byte fora do buffering de FIFO:

void serial_write_char(uint16_t port, char c) {
while (!(inb(port + 5) & 0x20)) { } // espera Transmit Holding Register Empty
outb(port, c);
}

Uma porta serial é uma ferramenta de depuração inicial incomumente boa precisamente porque não precisa de quase nada mais funcionando antes (sem tratamento de interrupção se uma implementação puramente por polling for usada, sem gerenciamento de memória além de alguns bytes de stack, e sem hardware de display algum), o que torna comum que a primeiríssima linha de saída de um kernel, seja onde for, saia pela COM1 em vez de um framebuffer. O QEMU em particular pode ser instruído a redirecionar a saída de COM1 de um guest diretamente para o terminal do host (-serial stdio ou equivalente), transformando o que de outra forma exigiria um cabo null-modem físico e uma segunda máquina em uma única flag de linha de comando, e tornando um driver serial funcional uma das peças de maior valor de código de kernel bem inicial para acertar.

Um erro inicial frequente é esquecer que os registradores de divisor fazem alias com os registradores de buffer de transmissão/recepção e habilitação de interrupção: código que define DLAB para configurar a baud rate e depois esquece de limpá-lo novamente antes de tentar enviar um byte vai, em vez disso, escrever no divisor, corrompendo silenciosamente a configuração de baud rate em vez de transmitir qualquer coisa.

  1. ^ National Semiconductor, PC16550D Universal Asynchronous Receiver/Transmitter with FIFOs (o datasheet da variante de UART em que o layout de registrador acima se baseia).
  2. ^ OSDev Wiki, “Serial Ports” (uma referência hobbyist cobrindo I/O serial orientado a interrupção com mais profundidade do que cabe aqui).
  • PIC & APIC: para controlar a porta serial através da IRQ4 em vez de por polling.
  • VGA & Framebuffers: o caminho de saída de display do qual um console serial costuma ser um precursor.
  • Console e Emulação de Terminal: a mesma análise de sequência de escape aplicada na direção oposta, a sequências que um console serial recebe.