Skip to content

Latest commit

 

History

History

lab4

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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:

  • CONTROL, 8 bits sem sinal, indica o sinal da componente X, da componente Y, se houve overflow nalguma dessas componentes e algum clique em cada um dos três botões disponíveis no rato;
  • DELTA_X, 8 bits sem sinal, indica o valor absoluto do deslocamento em X;
  • DELTA_Y, 8 bits sem sinal, indica o valor absoluto do deslocamento em Y;

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:

  • 0x1E, bit 5 a zero, é um output do teclado;
  • 0xF8, bit 5 ativo, é um output do rato;

2. Problema

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

  • Lê o output do topo da fila (0x1E), mas como o status indica que o output é do teclado, então descarta o byte;

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

  • Lê o output do topo da fila (0xF8), mas como o status indica que o output é do rato, então descarta o byte;

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

  • No rato, não interpreta um movimento e/ou um clique;
  • No teclado, não interpreta o valor da tecla pressionada;

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.

  • timer, IRQ 0
  • keyboard, IRQ 1
  • mouse, IRQ 12

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:

  • ACK, byte 0xFA, quando o comando foi aceite;
  • NACK, byte 0xFE, quando ocorreu algum erro. Nesse caso todo o comando deve ser enviado novamente, esperando alguns milissegundos por causa do delay do controlador;

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:

  • 0xF4, ativa o data report;
  • 0xF5, desativa o data report;
  • 0xEA, ativa o stream mode;
  • 0xF0, ativa o remote mode;
  • 0xEB, manda um request de novos dados;

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:

  • A primeira linha deve ser desenhada SÓ com o botão esquerdo pressionado;
  • A segunda linha deve ser desenhada SÓ com o botão direito pressionado;
  • No vértice o botão esquerdo tem de deixar de ser pressionado antes do botão direito ser;
  • O valor absoluto da inclinação em cada linha deve ser maior que 1;
  • O deslocamento em X não pode ser inferior a x_len;
  • Há uma tolerância de algumas unidades, tolerance, em cada iteração para cada eixo;
  • O fim de cada linha é marcado pela libertação do botão;
  • Um erro na execução faz com que o sistema volte ao estado inicial;

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:

  • Estado inicial, Start (S)
  • Linha ascendente, Up (U)
  • Vértice, Vertex (V)
  • Linha descendente, Down (D)
  • Estado final, End (E)

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:

  • I: botão esquerdo é pressionado
  • II: o botão esquerdo está pressionado enquanto o movimento do rato é ascendente com declive superior a 1;
  • III: o botão esquerdo deixa de estar pressionado
  • IV: o botão direito é pressionado
  • V: o botão direito está pressionado enquanto o movimento do rato é descendente com declive inferior a -1;
  • VI: o botão direito deixa de estar pressionado e o deslocamento no eixo X foi superior ou igual a x_len;
  • F: corresponde à transição ELSE. Caso a condição atual não corresponda a uma manutenção do estado atual (self-transition) ou avanço para o seguinte estado, então o sistema deve voltar ao inicial.

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:

  • mouse.c, para implementação das funções referentes a interrupções, sincronização de pacotes e comandos enviados diretamente ao rato;
  • lab4.c, para implementação das funções de mais alto nível que usam as funções disponíveis no módulo do mouse;

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