- Funcionamento do Rato
- i8042 Mouse
- Descarte mútuo no i8042
- O comando 0xD4
- Interrupções
- Máquinas de Estado em C
- Compilação do código
- Testagem do código
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
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, ¤t_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++;
}
}
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;
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;
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.
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.
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
}
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;
}
}
//...
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
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