Y2S2-LabComputadores

i8042, the PC’s Keyboard

Tópicos

Teclado

Como vimos no capítulo anterior há sempre necessidade de generalizar o hardware para poder ser usado com diversas máquinas. O teclado comum não foge da regra. Desta vez o desafio é diferente: há no mundo imensos fabricantes de teclados, com diferentes teclas, diferentes línguas e constituições. Como é possível generalizar todas estas combinações?

Quando pressionamos ou soltamos uma tecla geramos um scancode, que é um código geralmente de 8 bits (1 byte) que caracteriza não o significado da tecla mas sim a posição desta no teclado. Independentemente do idioma, do fabricante ou do número de teclas, a mesma posição gera sempre o mesmo scancode.
Este código é depois processado pelo i8042/KBC e interpretado pelo CPU. Antes de mostrar o output o CPU consulta o idioma selecionado no sistema para fazer a tradução. Assim, quando trocamos o idioma do teclado com recurso a software estamos na realidade a trocar o keymap do idioma recorrendo por exemplo a hashmaps com chaves de bytes e valores em ASCII:

Tratamento do output do teclado


Um scancode pode ter duas formas:

Geralmente o breakcode difere apenas no bit mais significativo do makecode original. Esta diferença fica mais nítida no caso anterior:

uint8_t A_makecode  = 0x1E // 00011110
uint8_t A_breakcode = 0x9E // 10011110

Uma possível implementação de uma função que faz a distinção dos códigos:

void evaluate_scancode(uint8_t scancode) {
    if (scancode & BIT(7)) {
        printf("%02x, breakcode!\n", scancode);
    } else {
        printf("%02x, makecode!\n", scancode);
    }
}

Algumas teclas geram um scancode de 2 bytes. Nesse caso, é quase certo que o primeiro byte recebido seja 0xE0. Na versão que se vai utilizar em LCOM o Minix está configurado com o idioma Português.

i8042 KBC

O i8042 ou KBC (KeyBoard Controller) é o controlador do teclado do computador e do rato com dois botões. Neste lab focaremos apenas na funcionalidade do teclado. É importante desde já termos uma boa estrutura e hierarquia de dependências entre os vários ficheiros para que o sistema seja modular e reutilizável para o lab seguinte:

Estrutura do código esperada


Este dispositivo funciona de forma parecida ao timer, havendo interação a partir da escrita e leitura de registos:

Funcionamento do i8042


Desta vez temos a possibilidade de ler diretamente o status do dispositivo. Esse estado é constituído por 8 bits (uint8_t) e permite obter algumas informações relevantes:

Exemplo 1

Pretende-se saber se num determinado momento, o buffer de input está cheio. Uma possível implementação seria a seguinte:

uint8_t status;
util_sys_inb(0x64, &status);
if (status & BIT(1)) {
    printf("Full input buffer\n");
}

Dá ainda para enviar comandos a partir do input buffer disponível no endereço 0x64 mas de uma forma diferente do timer. Neste caso, há dois problemas a ter em conta:

Uma boa solução é criar um ciclo de tentativas finitas e esperar alguns milissegundos entre fracassos. Normalmente 10 tentativas e 20 milissegundos entre cada uma é suficiente para ter sucesso na operação. Só é possível escrever no controlador quando o buffer de entrada não está cheio:

int write_KBC_command(uint8_t port, uint8_t commandByte) {

    uint8_t status;
    uint8_t attemps = 10;

    while (attemps) {

        if (read_KBC_status(&status) != 0){             // lê o status
            printf("Error: Status not available!\n");
            return 1;
        }

        if ((status & BIT(1)) == 0){                    // o input buffer não está cheio, posso escrever
            if(sys_outb(port, commandByte) != 0){       // tentativa de escrita
                printf("Error: Could not write commandByte!\n");
                return 1;
            }
            return 0; // sucesso: comando inserido no i8042
        }
        tickdelay(micros_to_ticks(20000));              
        attemps--;
    }
    return 1; // se ultrapassar o número de tentativas lança um erro
}

A função tickdelay() assegura o intervalo correcto entre tentativas de acordo os ticks do processador. A função micros_to_ticks() traduz um número inteiro de microsseguros em ticks.

Da mesma forma, dá para ler os caracteres pressionados no teclado graças ao buffer de saída. Neste caso a informação disponibilizada pelo i8042 é só fiável quando estiver completamente no buffer, ou seja, só deve ser lida quando o output buffer estiver cheio. O status indica se há algum erro ao nível da paridade ou de timeout. Nesse caso os bytes devem ser lidos para sairem da fila/buffer mas serem descartados:

int read_KBC_output(uint8_t port, uint8_t *output) {

    uint8_t status;
    uint8_t attemps = 10;
    
    while (attemps) {

        if (read_KBC_status(&status) != 0) {                // lê o status
            printf("Error: Status not available!\n");
            return 1;
        }

        if ((status & BIT(0)) != 0) {                       // o output buffer está cheio, posso ler
            if(util_sys_inb(port, output) != 0){            // leitura do buffer de saída
                printf("Error: Could not read output!\n");
                return 1;
            }
            if((status & BIT(7)) != 0){                     // verifica erro de paridade
                printf("Error: Parity error!\n");           // se existir, descarta
                return 1;
            }
            if((status & BIT(6)) != 0){                     // verifica erro de timeout
                printf("Error: Timeout error!\n");          // se existir, descarta
                return 1;
            }
            return 0; // sucesso: output lido sem erros de timeout ou de paridade
        }
        tickdelay(micros_to_ticks(20000));
        attemps--;
    }
    return 1; // se ultrapassar o número de tentativas lança um erro
}

Interrupções

O i8042 também dá origem a interrupções sempre que uma tecla é pressionada. O dispositivo está disponível na IRQ_LINE 1 e as interrupções são ativadas e eliminadas com comandos semelhantes ao Timer:

/* ------ i8042.h ------ */
#define KEYBOARD_IRQ 1;   

/* ------ keyboard.c ------ */
int keyboard_hook_id = 1;

// subscribe interrupts
int keyboard_subscribe_int (uint8_t *bit_no) {
  if(bit_no == NULL) return 1;      // o apontador tem de ser válido
  *bit_no = BIT(keyboard_hook_id);  // a função que chamou esta deve saber qual é a máscara a utilizar
                                    // para detectar as interrupções geradas
  // subscrição das interrupções em modo exclusivo
  return sys_irqsetpolicy(KEYBOARD_IRQ, IRQ_REENABLE | IRQ_EXCLUSIVE, &keyboard_hook_id);
}

// unsubscribe interrupts
int keyboard_unsubscribe_int () {
  return sys_irqrmpolicy(&keyboard_hook_id); // desligar as interrupções
}

Modo exclusivo


Note-se a diferença no segundo argumento da policy. O Minix tem um controlador próprio que recebe primeiro do que a nossa função as interrupções do teclado. Como não queremos isso, ou seja, queremos que a IRQ_LINE seja exclusivamente tratada pela nossa função, então declaramos estas interrupções como exclusivas, com a flag IRQ_EXCLUSIVE.

Como detetar e trabalhar com interrupções de vários dispositivos ao mesmo tempo? Ver apontamentos do lab anterior.

Polling

Neste atividade também teremos a oportunidade de trabalhar com o teclado em modo Polling. Como já vimos esta técnica é pouco eficiente pelo que aconselha-se a usar apenas na função kbd_test_poll() e não como solução a implementar no futuro projeto.

Nesta função em específico para o Minix permitir o Polling desativa todas as suas interrupções internas. Assim, é de extrema relevância antes de terminar a execução da mesma ativar novamente tudo, caso contrário o teclado deixará de responder aos comandos seguintes e a única solução será voltar a abrir o Minix. A função que trata dessa parte será a kbc_restore().
Tal como aconteceu no Timer, não queremos mexer em toda a configuração do teclado mas sim numa parte que faça ativar as interrupções. Dados relevantes a reter:

Uma possível implementação da função é a seguinte:

int kbc_restore() {

    uint8_t commandWord;

    // Leitura da configuração atual
    if (write_KBC_command(0x64, 0x20) != 0) return 1;           // avisar o i8042 da leitura
    if (read_KBC_command(0x60, &commandWord) != 0) return 1;    // ler a configuração

    // Ativar o bit das interrupções
    commandWord = commandWord | BIT(0);

    // Escrita da nova configuração
    if (write_KBC_command(0x64, 0x60) != 0) return 1;           // avisar o i8042 da escrita
    if (write_KBC_command(0x60, commandWord) != 0) return 1;    // escrever a configuração
    return 0;
}

Compilação do código

Ao longo do Lab3 programamos em 4 ficheiros:

Ainda importamos os ficheiros utils.c, timer.c e i8254.h do lab anterior. Em LCOM o processo de compilação é simples pois existe sempre um makefile que auxilia na tarefa. Para compilar basta correr os seguintes comandos:

minix$ make clean # apaga os binários temporários
minix$ make       # compila o programa

Testagem do código

A biblioteca LCF (LCOM Framework) disponível nesta versão do Minix3 tem um conjunto de testes para cada função a implementar em lab3.c. Assim é simples verificar se o programa corre como esperado para depois ser usado sem problemas no projeto. Para saber o conjunto dos testes disponíveis basta consultar:

minix$ lcom_run lab3

Neste caso em concreto estão disponíveis algumas combinações:

minix$ lcom_run lab3 "scan -t <0,1,2,3,4,5>"
minix$ lcom_run lab3 "poll -t <0,1,2,3,4,5>"
minix$ lcom_run lab3 "timed <time> -t <0,1,2,3,4,5>"

Dica

Para implementação do futuro projeto não temos disponível nenhuma tabela que relacione os scancodes às respetivas teclas do teclado. Aconselho por isso a correr livremente o comando seguinte e ir apontando os valores resultantes (makecodes preferencialmente) para todas as teclas que necessitem:

minix$ lcom_run lab3 "scan"

Desafio #2 - Makecode SPAM

Este desafio foi inspirado num jogo da Microsoft Arcade e tem dificuldade 3/10. O jogo apresentado consiste em clicar numa tecla maior número de vezes durante um determinado tempo. O clique é considerado válido quando pressionamos e soltamos a tecla, ou seja, detectando um makecode seguido de um breakcode.

A função a implementar tem três argumentos:

int spam(uint8_t time, uint8_t makecode, uint8_t record) {
    // to implement
    return 1;
}

A invocação da função resulta numa jogada onde pode ser batido o record anterior. Se não for batido o record a função deve retornar 0, caso contrário deve retornar o novo valor. A solução reune os conhecimentos obtidos nas aulas Lab2 sobre o Timer e Lab3 sobre o Teclado.

Desafio #3 - Teclas Presas

O Sistema Operativo Windows contém uma ferramenta de acessibilidade chamada “Teclas Presas”. Este recurso faz com que uma tecla modificadora fique ativa até que outra seja pressionada, permitindo aos utilizadores pressionarem uma tecla de cada vez para usar um atalho de teclado. Por exemplo, com esta opção ativa é possível tirar uma screenshot clicando nas teclas “Win”, “Shift” e “S” sem ser ao mesmo tempo, desde que sejam cliques consecutivos.

A função a desenvolver em ambiente Minix deve retornar apenas quando for interpretado o comando “LCOM”:

int read_lcom_command() {
    // 
    return 1;
}

A solução reune os conhecimentos obtidos nas aula Lab3 sobre o Teclado. Igualmente é necessário algum conhecimento prévio de Máquinas de Estado, por exemplo através da Unidade Curricular de Teoria da Computação, para simplificar o código.

Dica

Um estado em C pode ser implementado recorrendo a enumerações:

typedef enum {
    START,
    LETTER_L, LETTER_C, LETTER_O, LETTER_M,
    END
} CommandState;

Uma máquina de estado pode ser implementada por um ciclo que só acaba quando chegamos ao estado terminal. As transições correspondem a mudanças na variável que guarda o estado atual da máquina:

CommandState state = START; // estado inicial

while (state != END) {  // enquanto não chegar ao estado terminal

    get_scancode(&scancode); // lê o próximo caracter do teclado

    switch (state) {
        case START:     
            // se a tecla pressionada for "L" então avança de estado, senão continua no estado inicial
            if (scancode == L_MAKECODE) { 
                state = LETTER_L;
            }
            break;
        
        case LETTER_L:
            if (scancode == L_MAKECODE || scancode == L_BREAKCODE) {
                // se a tecla continuar a ser "L", continua neste estado
                continue;
            } else if (scancode == C_MAKECODE) {
                // se a tecla for "C", avança de estado
                state = LETTER_C;
            } else {
                // se não for "L" ou "C", então recomeça o comando, volta ao estado inicial
                state = START;
            }

        // ...

        case END:
            break;
    }
}

A implementação de Máquinas de Estado em C será de extrema importância para a realização do Projeto final.


@ Fábio Sá
@ Fevereiro de 2023