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.
Endereços de porta
Seção intitulada “Endereços de porta”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 porta | Endereço-base | IRQ legada |
|---|---|---|
| COM1 | 0x3F8 | 4 |
| COM2 | 0x2F8 | 3 |
| COM3 | 0x3E8 | 4 |
| COM4 | 0x2E8 | 3 |
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.
Layout de registrador
Seção intitulada “Layout de registrador”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:
| Offset | Leitura | Escrita |
|---|---|---|
| +0 | Buffer de recepção (se DLAB=0) | Buffer de transmissão (se DLAB=0) |
| +0 | Byte baixo do divisor (se DLAB=1) | Byte baixo do divisor (se DLAB=1) |
| +1 | Habilitação de interrupção (se DLAB=0) | Habilitação de interrupção (se DLAB=0) |
| +1 | Byte alto do divisor (se DLAB=1) | Byte alto do divisor (se DLAB=1) |
| +2 | Identificação de interrupção | Controle de FIFO |
| +3 | Controle de linha | Controle de linha |
| +4 | Controle de modem | Controle de modem |
| +5 | Status 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.
Inicialização
Seção intitulada “Inicialização”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.
Transmitindo
Seção intitulada “Transmitindo”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);}Notas de implementação
Seção intitulada “Notas de implementação”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.
Referências
Seção intitulada “Referências”- ^ National Semiconductor, PC16550D Universal Asynchronous Receiver/Transmitter with FIFOs (o datasheet da variante de UART em que o layout de registrador acima se baseia).
- ^ OSDev Wiki, “Serial Ports” (uma referência hobbyist cobrindo I/O serial orientado a interrupção com mais profundidade do que cabe aqui).
Ver também
Seção intitulada “Ver também”- 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.