MyToyOS: O controlador de interrupções programável

Já falei por aqui o que é uma interrupção, certo? Acho que falei… Mas, ai vai, de novo: Uma interrupção é um sinal elétrico que é colocado num pino do processador por um circuito externo na intenção de pedir que o processador interrompa o fluxo de processamento normal e execute uma rotina de tratamento, em benefício do circuito que requisitou essa “interrupção”… Ou seja: O processador pára o que está fazendo e executa uma rotininha para satisfazer as necessidades do circuito externo.

Esse “circuito externo” pode ser qualquer coisa. Por exemplo, um HD pode pedir uma interrupção para que o processador leia um bloco de dados que ele mesmo requisitou anteriormente… A porta serial pode pedir uma interrupção para que o processador leia um byte que foi recebido… Uma placa de som pode pedir uma interrupção para informar que o buffer de dados está pronto para receber mais samples… etc.

Repare, no entanto, que o dispositivo pode pedir uma interrupção (Interrupt Request ou IRQ), mas tem que esperar que o processador a aceite (Interrupt Acknowledge). Ou seja, o processador não interrompe o processamento normal só porque o dispositivo externo quer. Só atenderá a interrupção quando o processador mesmo quiser…

Na arquitetura de processadores Intel x86, além do sinal de pedido de interrupção (Interrupt Request ou IRQ), o dispositivo requisitante tem que enviar um valor de 8 bits informando o número da interrupção que lhe convém… Em teoria, o processador pode atender a 224 interrupções diferentes (seriam 256, mas 32 delas são reservadas pela Intel). Na prática, o circuito dos PCs permite apenas 15 interrupções diferentes, numeradas de IRQ0 até IRQ15 (onde a IRQ2 é reservada, explico porquê mais adiante!).

Esse esquema de receber um pedido de interrupção (sinal INT vai para nível alto) e enviar um “aceite” (sinal INTA# vai para nível baixo!) gera uma complicação: Esses sinais não podem ser compartilhados por todos os circuitos, externos ao processador, ao mesmo tempo. Se fosse o caso, dois circuitos poderiam colocar o sinal INT em nível alto e ambos, depois de receberem o INTA#, tentariam enviar, no barramento de dados, o número da interrupção desejada… Esse é o problema enfrentado pelos projetistas de microcomputadores baseados na arquitetura Intel… Daí a necessidade de um controlador de interrupções… Um circuito integrado que coordena os pedidos e atendimentos à interrupções de vários dispositivos, mas que seja a única comunicação com o processador!

Sempre que você topar com a palavra “controlador”, assuma algum tipo de compartilhamento… Esse sujeito, assim como na vida real, é o “gerente” de vários trabalhadores (dispositivos) em benefício da empresa (processador)… Por isso temos o PIC (Programmable Interrupt Controller) e o DMAC (Direct Memory Access Controller), por exemplo… Todos os dispositivos não podem requisitar esses recursos ao mesmo tempo. Tem que haver alguém controlando quem pede o que, quando e a quem…

O PIC 8259A:

Embora as arquiteturas de PCs modernas, desde o lançamento do Pentium, possuam um controlador mais moderno, chamado de APIC (Advanced Programmable Interrupt Controller), o PIC padrão (o circuito integrado 8259A da Intel – baixe o datasheet aqui) ainda está disponível de forma emulada nesses novos controladores. O APIC está disponível em dois sabores: LAPIC e I/O APIC. O primeiro está incorporado no processador e existe um para cada processador lógico (ou para um par deles, falarei disso em outro artigo). O ‘L’ de LAPIC significa “Local”.

O I/O APIC faz o trabalho do antigo PIC e é externo ao processador.

Pra que diabos existe um LAPIC então? Well… é complicado: Mas, resumindo, algumas coisas como multitarefa simétrica precisa lidar com interrupções que não vẽm de dispositivos…

Como o PIC funciona?

O 8259A permite a coordenação de até 8 requisições de interrupções simultâneas. Ele possui 8 entradas de requisição, numeradas de IRQ0 até IRQ7 e o chip possui a lógica para lidar com os sinais INT e INTA# do processador, bem como com o barramento de dados (para enviar “qual” interrupção está sendo pedida).

Quando queremos tratar interrupções vindas de um dispositivo, tudo o que temos que fazer é escrever uma rotina de tratamento de interrupções e apontar para ela num vetor da tabela de vetores de interrupções. Depois, temos que habilitar o reconhecimento da interrupção no PIC… O primeiro passo é simples no modo real: O primeiro bloco de 1 KiB da memória contém os ponteiros “far” para cada uma das 256 rotinas de tratamento de interrupções possíveis no formato segmento:offset… A diferença dessas rotinas para as funções tradicionais de nossos códigos é que elas terminam com a instrução IRET, ao invés do simples RET.

Só por curiosidade, no velho MS-DOS, usando o Turbo C, podíamos escrever algo assim:

 void interrupt (*old_isr)(void);
void interrupt isr_serial(void)
{
  ...
}

/* Guarda a velha ISR (Interrupt Service Routine) e
   ajusta o novo... */
old_isr = getvect(11);
setvect(11, isr_serial);

...

/* Recupera a velha ISR... */
setvect(11, old_isr);

O modificador interrupt tratava de colocar um IRET no final da rotina. E as funções getvect() e setvect do MS-DOS ajustavam os ponteiros na tabela de vetores de interrupções para nós… Fique avisado: Isso não funciona mais assim nos sistemas operacionais modernos! Vale apenas lembrar que o atributo interrupt também existe para o modo protegido no GCC e tem a mesmíssima função. Somente o método de setup e o funcionamento das interrupções é que são um pouco diferentes no modo protegido.

Como estamos lidando com um controlador, temos que informá-lo quando o processador terminou a interrupção… Isso é feito enviando um comando EOI (End of Interruption) para o PIC. Mais adiante mostro como isso é feito, mas, olhando para o datasheet do 8259A você verá que existem dois tipos de EOI… Um que diz ao controlador qual é a interrupção que terminou e outro que diz “se vira ai, PIC… terminei uma interrupção”… Esse último tipo, o unspecified, é o mais comum, uma vez que, geralmente, o processador atenderá uma interrupção de cada vez… Isso não quer dizer que, em certos casos, uma interrupção não possa ser requisitada enquanto um tratador ainda não tiver terminado o seu trabalho… Só que isso não é tão comum.

Depois ajustado o ponteiro para o tratador de interrupção temos que configurar a correspondente IRQ com o número do vetor, no PIC. Geralmente isso é feito pela BIOS: A IRQ0, por exemplo, está associada ao vetor 8, a IRQ1 ao 9 e assim por diante… Nada nos impede de mudar essa sequencia, mas eu não aconselho fazê-lo.

Uma vez que o vetor esteja configurado, precisamos habilitar o tratamento da interrupção no PIC. Isso é feito por um registrador de “máscara”… A analogia da “máscara” aqui é a de que uma interrupção pode ser “escondida” do processador e jamais ser requisitada através do PIC. Esse registrador tem 8 bits, um para cada IRQ. Se o bit estiver setado a interrupção estará “mascarada” (não será tratada), se for 0, a máscara é retirada e, se o sinal IRQ correspondente sinalizar a requisição, o PIC a repassará para o processador.

Os sinais IRQ:

A maioria dos dispositivos sinaliza seu desejo por uma requisição na subida do pulso do sinal IRQ… É o que você pode ler na documentação sobre o PIC e os APICs, chamada de edge assertion. Mas, podem existir casos que o sinal IRQ é sinalizado durante todo o tempo em que permanecer em nível alto (isso é raro, na arquitetura do PC). É o que é chamado de level assertion.

Os registradores IRR e ISR:

Esses são dois registradores de status do PIC que informam quais interrupções foram requisitadas (IRR de Interrupts Requested Register) e quais estão “em serviço” (ISR de In Service Register). As interrupções “em serviço” são aquelas que ainda não receberam o EOI… Por isso o envio do EOI, por parte do processado, é importante… O PIC só aceitará outra interrupção do mesmo dispositivo depois se ela não estiver “em serviço”…

Pera ai… mas o PC aceita 15 interrupções!!

Pois é… O PIC consegue tratar apenas 8. Então, como as outras 7 se encaixam nisso? Acontece que o PIC 8259A é feito para trabalhar, se necessário, “em cascata”. Ou seja, existe um PIC mestre e podem existir PICs escravos. Um PIC escravo pode atender um conjunto de IRQs e repassá-las para o mestre. Somente o mestre tem acesso ao processador.

No caso do PC temos dois 8259A. O primeiro trata diretamente as IRQs de 0 até 7, exceto a IRQ2, que é “cascateada” para o PIC escravo, que tratará as IRQs 8 até 15.

Assim, a IRQ2 é inutilizada… No caso do PC a prioridade das IRQs passa a ser: IRQ0, IRQ1, IRQ8 até IRQ15, IRQ3, IRQ4, … IRQ7. A lista abaixo mostra quais dispositivos estão ligados a cada uma dessas IRQs, no PC:

  • IRQ0 – Timer (PIT)
  • IRQ1 – Teclado (KBDC)
  • IRQ2 – cascateada para o PIC2 (indisponível para dispositovos)
  • IRQ3 – Serial 2 (COM2 no velho DOS)
  • IRQ4 – Serial 1 (COM1 no velho DOS)
  • IRQ5 – Porta paralela 2 ou placa de som (SoundBlaster)
  • IRQ6 – Diskette (FDC)
  • IRQ7 – Porta paralela 1 ou placa de som (SoundBlaster)
  • IRQ8 – Relógio de tempo real (RTC)
  • IRQ9 – ACPI
  • IRQ10 – livre (algumas controladoras SCSI usam)
  • IRQ11 – livre (algumas controladoras SCSI usam)
  • IRQ12 – mouse
  • IRQ13 – Coprocessador 8087 (quando existia)
  • IRQ14 – HDC 1
  • IRQ15 – HDC 2

Repare que, na sequencia das prioridades, a interrupção do RTC é atendido antes da do diskette, por exemplo, graças ao esquema de cascateamento dos PICs.

A confusa sequência de inicialização e operação do PIC:

Infelizmente, lidar com o PIC não é a tarefa trivial de ajustar valores em registradores e deixar o “pau quebrar”. A inicialização e operação do 8259A é feita de forma sequencial, com comandos enviados um depois do outro, numa sequencia correta. Existem dois grupos de sequências possíveis: ICWs de Initialization Command Words e OCWs de Operation Control Words.

Na sequência de inicialização, dois bytes são obrigatórios, chamados de ICW1 e ICW2. O byte ICW1 diz, entre outras coisas, se o PIC tem um escravo e se o vetor de interrupção trata IRQ sinalizada por edge ou level. O byte ICW2 informa o vetor da IRQ.

No caso do PC, como temos um PIC escravo, a palavra ICW3 é obrigatória também, assim como a ICW4… Deixo os detalhes de como configurar esses 4 bytes para leitura que você fará do datasheet, mas o importante é saber que sempre que for necessária uma inicialização do PIC, 4 bytes têm que ser escritos em sequência!

Nos PCs as OCWs são mais simples… a OCW1 é a máscara das interrupções do PIC. A OCW2 é mais usada para receber o EOI.

A diferença do velho MSDOS e os sistemas modernos:

Além de termos que lidar com o APIC (noutro texto falo disso), o modo protegido impõe várias outras coisinhas… Por exemplo: Não existe mais uma tabela de vetores de interrupções centralizada onde só precisamos informar os ponteiros para as rotinas de tratamento… Agora temos uma tabela de descritores de interrupções (IDT, Interrupt Descriptor Table) e cada entrada pode conter um de três “tipos” de interrupções diferentes: Task, Interrput ou Trap gates (consulte o volume 3 dos manuais de desenvolvimento da Intel)… Ainda, cada uma das entradas possui um nível de privilégio (geralmente para o ring 0).

Outra complicação é que as entradas na IDT contém seletor para onde o processador saltará… Em essência, é como se cada entrada contivesse o par segmento:offset da antiga tabela de vetores, mas seletor tem que estar descrito na tabela de descritores globais ou locais (GDT ou LDT)… que também têm lá suas características de proteção…

Anúncios