Falar com o hardware

Portas de entrada e saída, memória mapeada, máscaras e polling com limites.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

Ler 0x60 no i8042 pode retirar um byte recebido. Ler uma variável C limita-se a obter o valor guardado. Esta diferença importa: os registos de um controlador ligam o programa aos dados, ao estado e às ordens de um periférico, e os acessos podem alterar esse estado.

Registos e espaços de endereçamento

Há registos de dados, de estado e de controlo. A classificação indica a função, mas não obriga cada um a ter um endereço distinto. O mesmo endereço pode selecionar registos diferentes conforme leias ou escrevas.

No i8042, ler 0x64 consulta o estado; escrever 0x64 envia um comando ao controlador. Ler 0x60 retira dados do output buffer; escrever 0x60 envia um byte pelo input buffer. Os nomes input e output referem-se ao ponto de vista do controlador, não do processador.

Há duas maneiras de colocar esses endereços no sistema:

AcessoEspaço de endereçamentoOperação do processador
Memory-mapped I/OParte do espaço de memóriaInstruções de acesso à memória
Port-mapped I/OEspaço de I/O separadoInstruções próprias, como IN e OUT em x86

As portas do timer, do i8042 e da UART são port-mapped I/O. O framebuffer é memória mapeada. Não chames memory-mapped I/O a todas as leituras de portas. A arquitetura ARM usa memória mapeada para este acesso; x86 também suporta memória mapeada, além do seu espaço de portas de 16 bits.

Privilégios e Minix

Um processo normal não pode executar qualquer operação de hardware, porque o sistema limita o acesso para proteger os outros processos. No Minix, os drivers são processos em espaço de utilizador com permissões próprias. O programa de LC usa kernel calls para pedir operações privilegiadas ao microkernel, por exemplo sys_inb e sys_outb.

Uma máquina virtual fornece um PC emulado. O Minix acede ao hardware da máquina virtual, não recebe por isso acesso livre aos dispositivos físicos do computador anfitrião. O código dos laboratórios depende da imagem Minix e das bibliotecas de apoio fornecidas para LC.

VirtualBox com Linux, como complemento

Este vídeo de Rodrigo Dias apresenta Linux numa VM VirtualBox em Windows, não a instalação de Minix nem as bibliotecas Minix/LCF. Para preparar o laboratório, segue a imagem e as instruções da tua edição de LC.

Abrir no YouTube

sys_inb escreve o resultado numa variável de 32 bits, mesmo quando lê apenas um byte. Passar o endereço de um uint8_t através de um cast não aumenta o objeto: permitiria escrever para lá do seu fim. Esta função completa faz a adaptação segura no ambiente Minix/LCF:

#include <lcom/lcf.h>
#include <stdint.h>

int read_port_byte(int port, uint8_t *value) {
  if (value == NULL) return 1;
  uint32_t raw;
  int result = sys_inb(port, &raw);
  if (result != OK) return result;
  *value = (uint8_t)raw;
  return OK;
}

O chamador recebe um byte apenas após uma leitura bem-sucedida. A função não é executável no navegador: precisa dos cabeçalhos, serviços e permissões Minix do laboratório. Os nomes de tipos antigos u32_t e u8_t aparecem em algumas APIs com o mesmo papel.

Testar e alterar campos

Uma máscara tem bits a 1 nas posições que interessam. Num byte r, as operações usuais são:

IntençãoExpressão
Saber se algum bit da máscara m está ativo(r & m) != 0
Saber se todos estão ativos(r & m) == m
Ligar os bits de mr | m
Limpar os bits de mr & ~m
Inverter os bits de mr ^ m
Nomear o bit n como máscaraBIT(n)

Os materiais da cadeira definem o bit n como #define BIT(n) (1u << (n)). BIT(7) vale 0x80: é a máscara, não a posição. Quando uma API pede o número do bit, como o hook de sys_irqsetpolicy, passa-se 7 e guarda-se BIT(7) como máscara a testar. Passar BIT(7) onde se pede o número escolheria um bit inexistente.

& faz AND posição a posição. && testa a verdade das expressões e devolve 0 ou 1. Para r = 0x20 e m = 0x01, r & m é 0, mas r && m é 1, por isso o segundo teste não diz se o bit 0 está ativo.

Para substituir um campo, limpa primeiro o campo antigo e insere o novo. Se os bits 4 a 2 contêm um modo, a máscara é 0x1C. Com r = 0xA7 e o novo modo 5:

uint8_t next = (uint8_t)((r & ~0x1Cu) | (5u << 2));

Os três passos conservam os bits exteriores ao campo:

PassoByteCampo 4:2
Registo inicial1010 0111001
Limpar 0x1C1010 0011000
Inserir 5 << 21011 0111101

O resultado é 0xB7. Usar apenas OR deixaria bits do modo anterior ligados. Usa valores sem sinal nas máscaras e garante que a contagem de um deslocamento é menor do que a largura do operando.

Esta leitura, alteração e escrita só é válida se o registo for legível e permitir essa semântica. Um registo write-only, ou com bits que se limpam ao escrever 1, exige a sequência descrita pelo dispositivo.

Um endereço não garante um registo legível

O PP1 de 2020 descreve um dispositivo fictício, L3223, para testar a leitura de uma especificação nova. Tem três timers e difere do i8254: as portas 0x20, 0x21 e 0x22 recebem contagens; escrever 0x23 configura, mas ler 0x23 consulta estado. Os bits lidos não são uma cópia da configuração que podemos preservar com read-modify-write.

A palavra escrita usa bits 7:6 para escolher timer 0, 1 ou 2, bit 4 para alarme e bits 1:0 para a unidade: 0 é microssegundo, 1 milissegundo e 2 segundo. Os restantes bits devem ser zero. Em alarme, timer 2 e milissegundos dão (2 << 6) | (1 << 4) | 1, ou 0x91.

Para uma contagem de 1000, que cabe em 16 bits, a sequência é:

OperaçãoPortaValorSignificado
Escrever0x230x91Timer 2, alarme, milissegundos
Escrever0x220xE8Byte menos significativo de 1000
Escrever0x220x03Byte mais significativo de 1000

Esta contagem representa 1000 ms, ou um segundo; não se calcula TIMER_FREQ / intervalo. Essa fórmula pertence a outro dispositivo e a outro significado da contagem.

O exemplo seguinte apenas constrói e imprime o plano de escritas. Não executa I/O, nem demonstra que um timer real disparou. Define para esta aplicação intervalos de 1 a 65535, timers 0 a 2 e os três códigos de unidade; não atribui ao valor zero a convenção de 65536 do i8254.

Construir a sequência a partir da especificação
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>

struct escrita { uint16_t porta; uint8_t valor; };

static bool preparar(unsigned timer, uint32_t intervalo, unsigned unidade,
                     struct escrita plano[3]) {
  if (timer > 2 || unidade > 2 || intervalo == 0 || intervalo > UINT16_MAX)
    return false;
  const uint16_t porta = (uint16_t)(0x20u + timer);
  const uint8_t controlo = (uint8_t)((timer << 6) | (1u << 4) | unidade);
  plano[0] = (struct escrita){0x23u, controlo};
  plano[1] = (struct escrita){porta, (uint8_t)(intervalo & 0xFFu)};
  plano[2] = (struct escrita){porta, (uint8_t)(intervalo >> 8)};
  return true;
}

int main(void) {
  struct escrita plano[3];
  if (!preparar(2, 1000, 1, plano)) return 1;
  for (unsigned i = 0; i < 3; ++i)
    printf("porta=0x%02X valor=0x%02X\n",
           (unsigned)plano[i].porta, (unsigned)plano[i].valor);
  return 0;
}
Resultado

Executa o código para ver a saída e os gráficos.

Se o estado lido em 0x23 for 0x05, há pedidos dos timers 0 e 2, pelos bits 0 e 2. Isto é estado do dispositivo, diferente da máscara da notificação Minix. A IRQ partilhada avisa que é preciso consultar a causa; não escolhe sozinha qual timer originou o pedido. Não inventes uma escrita para limpar flags quando a especificação não define essa operação.

Polling e timeout

Em polling, o programa consulta o estado até a condição ser satisfeita, porque ainda não recebeu nenhuma notificação. Antes de ler o output buffer do i8042, espera OBF = 1. Antes de escrever uma ordem, espera IBF = 0. São condições diferentes.

Numa leitura concreta, lê 0x64. Se OBF estiver a 0, espera e volta a ler. Quando OBF for 1, lê 0x60 uma vez e interpreta esse byte. Se o estado assinalar erro de paridade ou timeout, consome na mesma o byte e descarta-o, para não deixar lixo no buffer a bloquear a entrega seguinte.

Um ciclo sem limite pode nunca terminar. Define uma política de falha e deixa o controlador ter tempo para responder. Esta função completa do ambiente Minix espera até cinco tentativas, espaçadas por 20 ms:

#include <lcom/lcf.h>
#include <stdint.h>

int wait_kbc_input_empty(void) {
  const unsigned attempts = 5;
  const unsigned delay_us = 20000;
  for (unsigned i = 0; i < attempts; ++i) {
    uint32_t status;
    int result = sys_inb(0x64, &status);
    if (result != OK) return result;
    if ((status & (1u << 1)) == 0) return OK;
    if (i + 1 < attempts)
      tickdelay(micros_to_ticks(delay_us));
  }
  return 1;
}

O resultado esperado é OK assim que IBF estiver a 0, ou erro se a leitura falhar ou a condição não surgir. São cinco observações e no máximo quatro atrasos, cerca de 80 ms mais o custo das chamadas e o arredondamento a ticks. Isto é uma política para este exemplo, não uma constante do dispositivo.

Polling não implica sempre espera ativa sem corpo, porque o programa pode consultar periodicamente e executar outra tarefa entre consultas. Com período de consulta PP, um evento que fica disponível até ser lido espera entre aproximadamente 0 e PP antes de ser detetado. Se os instantes forem uniformes, a espera média é P/2P/2. Um buffer pequeno pode perder eventos durante esse intervalo.

Quando escolher polling

Polling evita o custo de configurar e receber interrupções, por isso pode servir para uma operação curta e esperada, como obter uma resposta a um comando. Interrupções permitem bloquear à espera de eventos pouco frequentes. A escolha depende da frequência dos eventos, do tempo de resposta permitido, da capacidade do buffer e do custo de cada consulta ou atendimento.

Não concluas que uma das opções é sempre mais rápida: o pedido de interrupção do dispositivo, o atendimento no kernel e a notificação recebida pelo programa são etapas distintas, explicadas em Interrupções.

Exercícios

Substituir um campo sem alterar os outros

Um registo legível e de escrita normal vale 0xD3. Os bits 4 a 2 contêm um modo. Substitui esse modo por 2 sem alterar os outros bits. Dá o novo byte em decimal.

Pista
A máscara do campo é 0x1C.
Solução

0xD3 & ~0x1C conserva 0xC3. O novo campo é 0x08. O OR dá 0xCB, que vale 203. Os bits fora de 0x1C não mudam.

Erros frequentes

Usar apenas OR conserva bits do modo antigo. && não substitui campos.

Voltar à explicação
Classificar dois acessos

Classifica uma leitura de estado do i8042 com sys_inb(0x64, ...) e uma escrita no framebuffer obtido por vm_map_phys. Por que não se deve converter simplesmente o endereço físico da VRAM para uint8_t *?

Pista

Um endereço de porta não é um endereço no espaço virtual do processo.

Mais uma pista

O sistema tem de criar o mapeamento e conceder o acesso.

Solução

sys_inb consulta o espaço separado de portas de I/O, por intermédio de uma kernel call. O framebuffer mapeado usa acessos à memória virtual do processo, ligados à região física de VRAM. Um cast não cria esse mapeamento nem as permissões necessárias.

Confere a tua resposta:

  • Classifiquei a leitura de 0x64 como port-mapped I/O.
  • Classifiquei o framebuffer como memória mapeada.
  • Distingui endereço físico de apontador virtual.

Erros frequentes

Chamar memory-mapped I/O a ambas as operações ou confundir uma conversão de tipo com uma operação do sistema.

Voltar à explicação
Estimar a espera por consulta

O programa consulta um dispositivo a cada 8 ms. Um evento permanece disponível até ser lido e chega num instante uniforme entre consultas. Qual é a espera média até à deteção, ignorando o tempo da chamada?

Pista
A espera varia entre aproximadamente 0 e um período.
Solução

A espera média é 8/2 = 4 ms. A pior espera aproxima-se de 8 ms. A hipótese de o evento permanecer disponível impede perder o evento antes da consulta.

Erros frequentes

Usar 8 ms como média ou aplicar a conta a impulsos que desaparecem antes de serem consultados.

Voltar à explicação
Limite de tentativas de escrita

Uma função permite cinco leituras de estado, com 20 ms entre tentativas, sem atraso depois da última. Só escreve em 0x64 se observar IBF a 0.

  1. Todas as leituras têm sucesso, mas mostram IBF a 1. Quantas chamadas de leitura, atrasos e escritas ocorrem? Que resultado deve indicar a função?
  2. Num segundo ensaio, a terceira leitura falha na kernel call. Deve continuar a esperar e depois indicar timeout?

Pista
Conta os intervalos entre observações, além das observações.
Solução

No primeiro caso há cinco leituras, quatro atrasos e nenhuma escrita. Os atrasos somam nominalmente 80 ms, além das chamadas e do arredondamento a ticks. A função indica timeout da espera por IBF vazio.

No segundo caso termina na falha da terceira leitura e devolve o erro de I/O. Houve duas esperas antes dela. Não se sabe o estado atual de IBF, pelo que não se deve escrever nem apresentar essa falha como uma espera esgotada.

Confere a tua resposta:

  • Obtive cinco leituras e quatro atrasos no caso de timeout.
  • Não escrevi com IBF ativo.
  • Distingui o timeout de uma falha da terceira leitura.
Voltar à explicação
A porta lida não devolve a configuração

Usa a especificação do dispositivo fictício L3223 apresentada na lição. Queremos um alarme no timer 1 depois de 500 ms. O índice do timer ocupa 7:6, alarme é o bit 4 e o código de milissegundos é 1 nos bits 1:0. Os restantes bits têm de ser zero.

Ler 0x23 devolveu estado 0x07. Um colega propõe preservar esse valor, acrescentar os bits do alarme por OR e calcular a contagem por TIMER_FREQ / 500. Constrói a palavra correta e a sequência de portas/bytes. Explica os dois erros da proposta.

Solução

A palavra é (1 << 6) | (1 << 4) | 1, ou 0x51. Escrevemos 0x51 em 0x23; a contagem 500 é 0x01F4, por isso escrevemos primeiro 0xF4 e depois 0x01 na mesma porta 0x21.

O valor 0x07 lido é estado, não a configuração anterior. Preservá-lo por OR acrescentaria bits reservados e o código de unidade 3, inválido na especificação dada. O controlo é construído do contrato com os reservados zero.

O L3223 recebe a contagem na unidade configurada: 500 milissegundos. Não recebe um divisor do relógio de entrada do i8254; essa fórmula e a convenção especial de zero não se transferem só por ambos se chamarem timers.

Confere a tua resposta:

  • Construí 0x51 para timer 1, alarme e milissegundos, com reservados zero.
  • Escrevi 0xF4 e 0x01 nessa ordem na porta 0x21.
  • Distingui estado lido e controlo escrito e não usei a fórmula do divisor i8254.
Voltar à explicação
Uma IRQ, dois timers com pedido

Os três timers do L3223 partilham IRQ 10. Pediste o bit de notificação Minix 7 e conservaste esse número antes de sys_irqsetpolicy. Recebeste uma notificação de hardware com interrupts igual a 0x80. Uma leitura bem-sucedida de 0x23 devolve 0x05: os bits 0, 1 e 2 indicam pedidos dos timers 0, 1 e 2.

Que máscara valida a notificação? Que timers têm pedido? Podes concluir que o alarme do timer 1 expirou? A especificação não diz como limpar esses flags; escrever 0x05 em 0x23 é uma limpeza justificada?

Solução

A máscara da notificação é 1u << 7, ou 0x80. A IRQ física 10 identifica a linha subscrita, não o bit que este processo pediu. O registo do dispositivo é outra máscara: 0x05 ativa os bits 0 e 2. Há pedidos desses timers, sem o bit do timer 1.

Não podemos concluir que o alarme de 1 expirou só porque a IRQ partilhada notificou. Verificamos a causa correspondente ao objetivo e tratamos as restantes segundo a política da aplicação.

Escrever 0x05 em 0x23 configura o dispositivo; não há uma operação de limpeza definida por essa informação. Não a inventamos. O exemplo interpreta causas e máscaras, sem afirmar que executou I/O ou que conhece todos os detalhes de reconhecimento do periférico fictício.

Confere a tua resposta:

  • Usei 0x80 como máscara do bit de notificação pedido 7, sem confundir com IRQ 10.
  • Identifiquei pedidos dos timers 0 e 2 no estado 0x05.
  • Não anunciei o alarme do timer 1 e não inventei uma escrita de limpeza não especificada.
Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.