Falar com o hardware
Portas de entrada e saída, memória mapeada, máscaras e polling com limites.
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:
| Acesso | Espaço de endereçamento | Operação do processador |
|---|---|---|
| Memory-mapped I/O | Parte do espaço de memória | Instruções de acesso à memória |
| Port-mapped I/O | Espaço de I/O separado | Instruçõ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.
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ção | Expressã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 m | r | m |
Limpar os bits de m | r & ~m |
Inverter os bits de m | r ^ m |
Nomear o bit n como máscara | BIT(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:
| Passo | Byte | Campo 4:2 |
|---|---|---|
| Registo inicial | 1010 0111 | 001 |
Limpar 0x1C | 1010 0011 | 000 |
Inserir 5 << 2 | 1011 0111 | 101 |
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ção | Porta | Valor | Significado |
|---|---|---|---|
| Escrever | 0x23 | 0x91 | Timer 2, alarme, milissegundos |
| Escrever | 0x22 | 0xE8 | Byte menos significativo de 1000 |
| Escrever | 0x22 | 0x03 | Byte 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.
#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;
}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 , um evento que fica disponível até ser lido espera entre aproximadamente 0 e antes de ser detetado. Se os instantes forem uniformes, a espera média é . 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
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.
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.
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
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.
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.
- 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?
- Num segundo ensaio, a terceira leitura falha na kernel call. Deve continuar a esperar e depois indicar timeout?
Pista
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.
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.
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.
