Y2S2-LabComputadores

i8042, the PC’s Mouse

Tópicos

Rato

O Sistema Operativo por padrão atribui umas coordenadas iniciais no ecrã ao cursor, por esse motivo aparece sempre na mesma posição quando ligamos o computador. Depois disso o dispositivo emite bytes descrever o valor deslocamento no eixo X, o valor do deslocamento no eixo Y e se houve algum botão pressionado no processo. Todas as seguintes posições do rato são calculadas tendo por base a soma de vetores:

Interpretação da mudança das coordenadas do cursor


De P1 para P2 houve um deslocamento positivo nos dois eixos mas de P2 para P3 o deslocamento em Y foi negativo. O deslocamento do cursor para fora do quadrante positivo dos eixos não é permitido. Uma forma de controlar a situação é a seguinte:

int update_coordinates(int16_t *x, int16_t *y, int16_t delta_x, int16_t delta_y) {
  *x = max(0, *x + delta_x);
  *y = max(0, *y + delta_y);
  return 0;
}

A resolução padrão do Mouse do Minix é 4 contagens por milímetro percorrido. Não é uma contagem muito precisa e depende também do rato usado, pelo que este parâmetro não é explorado em LCOM.

A estrutura do código será semelhante ao Lab anterior:

Organização do código a implementar


i8042 Mouse

O rato é controlado pelo mesmo dispositivo do teclado: o i8042.

Funcionamento do i8042


Vamos portanto usar as mesmas funções para ler, escrever e consultar o status do controlador:

int read_KBC_status(uint8_t *status);
int write_KBC_command(uint8_t port, uint8_t commandByte);
int read_KBC_output(uint8_t port, uint8_t *output);

No entanto precisamos de mais uma verificação quando estivermos a ler o output do i8042. De facto, o controlador i8042 garante a leitura de bytes de ambos os dispositivos: teclado e rato. Se houver interrupções dos dois ao mesmo tempo não sabemos a quem pertence o output. Uma possível solução é adicionar um argumento booleano à função que lê o output para que funcione de acordo com o dispositivo que estamos a tratar. O bit 5 do status do KBC está a 1 quando o output é do rato e está a 0 quando o output é do teclado:

int read_KBC_output(uint8_t port, uint8_t *output, uint8_t mouse) {

    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;
            }
            if (mouse && !(status & BIT(5))) {              // está à espera do output do rato
                printf("Error: Mouse output not found\n");  // mas o output não é do rato
                return 1;
            } 
            if (!mouse && (status & BIT(5))) {                // está à espera do output do teclado
                printf("Error: Keyboard output not found\n"); // mas o output não é do teclado
                return 1;
            } 
            return 0; // sucesso: output correto 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
}

Agora a leitura e validação do output durante as interrupções pode ser realizada em separado.

if (msg.m_notify.interrupts & mouse_mask)
  read_KBC_output(0x60, &output, 1);
if (msg.m_notify.interrupts & keyboard_mask)
  read_KBC_output(0x60, &output, 0);

Mas esta solução cria outro problema: ver descarte mútuo no i8042.

Ao contrário do teclado, o rato em cada evento acaba por enviar 3 bytes de informação:

Constituição do CONTROL


O conjunto destes três bytes de informação ordenados chama-se packet ou pacote. No caso do sistema ser gerido por interrupções, lê-se sempre um byte por cada interrupção gerada. Se estiver em modo polling, lê-se um byte por cada iteração.

A principal dificuldade é saber onde começa e onde termina cada pacote de dados, já que o envio destes bytes é contínuo. Por simplicidade considera-se que o primeiro byte do pacote, o CONTROL, contém sempre o bit 3 ativo e assim é possível identificá-lo. É uma aproximação grosseira pois nada garante que os bytes seguintes (o deslocamento em X e o deslocamento em Y) também não possuam o mesmo bit ativo. No entanto para a LCF este truque funciona sempre.

Para controlar a sincronização dos bytes gerados precisamos de um conjunto de variáveis globais dentro do ficheiro mouse.c:

uint8_t byte_index = 0;       // [0..2]
uint8_t packet[3];            // pacote
uint8_t current_byte;         // o byte mais recente lido

A invocação da função mouse_ih() quando ocorrer uma interrupção provoca uma atualização no byte lido, que pode ser o primeiro do pacote ou não:

void mouse_ih() {
  read_KBC_output(KBC_WRITE_CMD, &current_byte, 1);
}

Para preenchermos o array packet corretamente podemos invocar em seguida a função mouse_sync_bytes(), que irá avaliar se estamos perante o byte CONTROL ou um byte de deslocamento, de acordo com o índice e o estado do terceiro bit:

void mouse_sync_bytes() {
  if (byte_index == 0 && (current_byte & BIT(3))) { // é o byte CONTROL, o bit 3 está ativo
    mouse_bytes[byte_index]= current_byte;
    byte_index++;
  }
  else if (byte_index == 3) {                            // completou o pacote
    do_something_with_packet(&packet);
    byte_index = 0;
  }
  else if (byte_index > 0) {                             // recebe os deslocamentos em X e Y
    mouse_bytes[byte_index] = current_byte;
    byte_index++;
  }
}

Descarte mútuo no i8042

1. Situação

Num sistema controlado por interrupções cada uma pode ser processada por uma cadeia que verifica a máscara atribuída durante a subscrição. Para ler os outputs de acordo com o dispositivo basta invocar a função criada no tópico anterior:

if (msg.m_notify.interrupts & mouse_mask) 
  // interrupção do rato
  read_KBC_output(0x60, &output, 1);         // Instrução A
if (msg.m_notify.interrupts & keyboard_mask) 
  // interrupção do teclado
  read_KBC_output(0x60, &output, 0);         // Instrução B

Num determinado instante pode haver interrupções por parte dos dois dispositivos controlados pelo i8042 ao mesmo tempo. O output buffer, que é na realidade uma fila (FIFO, first in, first out), pode ficar com o conteúdo seguinte:

Situação descrita


Assim é de prever que o byte 0x1E foi o primeiro a ser inserido na fila e portanto vai ser o primeiro a ser lido. O BIT 5 de cada status byte permite avaliar a proveniência dos dados:

2. Problema

Como o código é sequencial, o sistema irá analisar e processar primeiro a interrupção do rato (Instrução A):

Depois o sistema vai analisar e processar a interrupção do teclado (Instrução B):

De facto ocorreu um descarte mútuo, ou seja, cada um dos dispositivos invalidou os dados do outro. Este problema leva a comportamentos indesejados:

3. Solução

A solução para o problema é simples: podemos inverter a ordem das instruções que captam as interrupções:

if (msg.m_notify.interrupts & keyboard_mask) 
  // interrupção do teclado
  read_KBC_output(0x60, &output, 0);         // Instrução B
if (msg.m_notify.interrupts & mouse_mask) 
  // interrupção do rato
  read_KBC_output(0x60, &output, 1);         // Instrução A

Mas será que esta solução é viável? Ou seja, funciona para todas as composições do output buffer? De acordo com a teoria sim:

A IRQ_LINE tem índices de 0 a 15 que descrevem também a prioridade dos dispositivos entre si. Quanto menor o índice, maior prioridade.

Assim o teclado tem prioridade em relação ao rato. Quando ocorre uma interrupção, o output do teclado terá prioridade sob o output do rato, ficando em primeiro na fila de saída. Usando esta análise, e para não perder dados com verificações do status, é importante sempre avaliar as interrupções dos dispositivos por ordem de prioridade, que foi o que resolveu o problema descrito.

O comando 0xD4

Apesar do i8042 também ser o controlador do rato, não permite contactar diretamente com o dispositivo. Todos os comandos enviados para o input buffer do i8042 são interpretados e, se for o caso, só enviados para o teclado.

A ideia agora é inibir essa interpretação para conseguirmos mudar as configurações do rato. Ao injetar o comando 0xD4 no i8042 o próximo comando será enviado diretamente ao rato sem qualquer interpretação. Em consequência, o rato enviará uma resposta ao controlador que pode ser lida através do output buffer, em 0x60. Essa resposta pode ter dois formatos:

Funcionamento do comando 0xD4


Uma possível implementação do método:

int write_to_mouse(uint8_t command) {

  uint8_t attemps = 10;
  uint8_t mouse_response;

  // Enquanto houver tentativas e a resposta não for satisfatória
  do {
    attemps--;
    if (write_KBC_command(0x64, 0xD4)) return 1;              // Ativar do modo D4 do i8042
    if (write_KBC_command(0x60, command)) return 1;           // O comando para o rato é escrito na porta 0x60
    tickdelay(micros_to_ticks(20000));                        // Esperar alguns milissegundos
    if (util_sys_inb(0x60, &mouse_response)) return 1;        // Ler a resposta da porta do output buffer
    if (mouse_response == ACK) return 0;                      // Se a resposta for ACK, interromper o ciclo
  } while (mouse_response != 0xFA && attemps);       

  return 1;
}

O modo 0xD4 possui alguns comandos relevantes:

O Stream Mode é usado nas interrupções. O Remote Mode é usado no polling juntamente com o comando 0xEB para pedir dados a cada iteração.

Interrupções

O rato está presente na IRQ_LINE 12. As funções das interrupções são muito semelhantes às anteriores. De igual forma temos que declarar as interrupões como exclusivas:

/* ------ i8042.h ------ */
#define MOUSE_IRQ 12;   

/* ------ mouse.c ------ */
int mouse_hook_id = 2;

// subscribe interrupts
int mouse_subscribe_int (uint8_t *bit_no) {
  if(bit_no == NULL) return 1;   // o apontador tem de ser válido
  *bit_no = BIT(mouse_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(MOUSE_IRQ, IRQ_REENABLE | IRQ_EXCLUSIVE, &mouse_hook_id);
}

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

Máquinas de Estado em C

A gestão das interrupções geradas pelos dispositivos estudados até aqui pode constituir um modo de Event Driven Design. Nesse caso o fluxo do programa é controlado pelo ambiente onde está inserido, ou seja, é reativo na resposta aos eventos (interrupções) que poderão ocorrer de forma assíncrona. No entanto, para o contexto do Projeto de LCOM este design de código não é suficiente para garantirmos um código robusto, modular e facilmente manipulável. A função do Lab4 mouse_test_gesture(uint8_t x_len, uint8_t tolerance) é um exemplo bom e complexo para explorar.

A ideia é desenhar um símbolo AND (V invertido) com o rato garantindo várias restrições:

Este sistema tem demasiadas restrições para ser implementado com base em variáveis booleanas e cadeias de condições if-else. No entantanto podemos pensar no mesmo como um conjunto de estados:

A transição entre um estado e outro ocorre depois de cumprida uma determinada condição. Caso essa condição não seja cumprida o sistema ou permanece no mesmo estado ou deverá retornar ao estado inicial. De acordo com a matéria lecionada em Teoria da Computação o enunciado da função pode ser representado pela seguinte Máquina de Estados:

Máquina de Estados que representa o fluxo do programa


Descrição das transições:

Em C um conjunto de estados pode ser programado usando uma enumeração:

typedef enum {
  START,
  UP,
  VERTEX,
  DOWN,
  END
} SystemState;

Tendo duas variáveis globais no ficheiro lab4.c podemos guardar o estado do sistema e o valor total percorrido em x:

SystemState state = START;
uint16_t x_len_total = 0;

A implementação em C da máquina de estados representada na figura anterior pode ser um switch-case, onde as condições internas atualizam o estado global da máquina:

void update_state_machine(uint8_t tolerance) {

    switch (state) {

      case START:

          // transição I
          // se só o botão esquerdo estiver pressionado
          if (mouse_packet.lb && !mouse_packet.rb && !mouse_packet.mb) {
            state = UP;
          }

          break;

      case UP:
          //TODO: transições II, III e F
          break;

      case VERTEX:
          //TODO: transições IV e F
          break;

      case DOWN:
          //TODO: transições V, VI e F
          break;

      case END:
          break;
    }

    // Atualização do valor percorrido em X
    x_len_total = max(0, x_len_total + mouse_packet.delta_x);
}

A função anterior pode ser chamada após receber um pacote completo, por exemplo:

//...
if (msg.m_notify.interrupts & mouse_mask){  // Se for uma interrupção do rato
  mouse_ih();                               // Lemos mais um byte
  mouse_sync_bytes();                       // Sincronizamos esse byte no pacote respectivo
  if (byte_index == 3) {                    // Quando tivermos três bytes do mesmo pacote
    mouse_bytes_to_packet();                // Formamos o pacote
    update_state_machine(tolerance);        // Atualizamos a Máquina de Estados
    byte_index = 0;
  }
}
//...

Compilação do código

Ao longo do Lab4 programamos em 2 ficheiros:

Ainda importamos os ficheiros utils.c, timer.c, i8254.h, i8042.h e KBC.c 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 lab4.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 lab4

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

minix$ lcom_run lab4 "packet <NUMBER_PACKETS> -t <0,1,2,3,4,5>"
minix$ lcom_run lab4 "async <TIME_SECONDS> -t <0,1,2,3,4,5>"
minix$ lcom_run lab4 "remote <TIME_MILLISECONDS> -t <0,1,2,3,4,5>"
minix$ lcom_run lab4 "gesture <X_LENGTH> <TOLERANCE> -t <0,1,2,3,4,5>"

@ Fábio Sá
@ Março de 2023