sortix-mirror/kernel/x86-family/ps2.cpp

467 lines
13 KiB
C++

/*
* Copyright (c) 2015, 2017 Jonas 'Sortie' Termansen.
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
* x86-family/ps2.cpp
* 8042 PS/2 Controller.
*/
#include <assert.h>
#include <stdint.h>
#include <sortix/kernel/interrupt.h>
#include <sortix/kernel/ioport.h>
#include <sortix/kernel/kthread.h>
#include <sortix/kernel/log.h>
#include <sortix/kernel/panic.h>
#include <sortix/kernel/ps2.h>
#include "ps2.h"
namespace Sortix {
namespace PS2 {
static const uint16_t REG_DATA = 0x0060;
static const uint16_t REG_COMMAND = 0x0064;
static const uint16_t REG_STATUS = 0x0064;
static const uint8_t REG_COMMAND_READ_RAM = 0x20;
static const uint8_t REG_COMMAND_WRITE_RAM = 0x60;
static const uint8_t REG_COMMAND_DISABLE_SECOND_PORT = 0xA7;
static const uint8_t REG_COMMAND_ENABLE_SECOND_PORT = 0xA8;
static const uint8_t REG_COMMAND_TEST_SECOND_PORT = 0xA9;
static const uint8_t REG_COMMAND_TEST_CONTROLLER = 0xAA;
static const uint8_t REG_COMMAND_TEST_FIRST_PORT = 0xAB;
static const uint8_t REG_COMMAND_DISABLE_FIRST_PORT = 0xAD;
static const uint8_t REG_COMMAND_ENABLE_FIRST_PORT = 0xAE;
static const uint8_t REG_COMMAND_SEND_TO_PORT_2 = 0xD4;
static const uint8_t REG_STATUS_OUTPUT = 1 << 0;
static const uint8_t REG_STATUS_INPUT = 1 << 1;
static const uint8_t REG_STATUS_SYSTEM = 1 << 2;
static const uint8_t REG_STATUS_COMMAND = 1 << 3;
static const uint8_t REG_STATUS_UNKNOWN1 = 1 << 4;
static const uint8_t REG_STATUS_UNKNOWN2 = 1 << 5;
static const uint8_t REG_STATUS_TIMEOUT = 1 << 6;
static const uint8_t REG_STATUS_PARITY = 1 << 7;
static const uint8_t REG_CONFIG_FIRST_INTERRUPT = 1 << 0;
static const uint8_t REG_CONFIG_SECOND_INTERRUPT = 1 << 1;
static const uint8_t REG_CONFIG_SYSTEM = 1 << 2;
static const uint8_t REG_CONFIG_ZERO1 = 1 << 3;
static const uint8_t REG_CONFIG_FIRST_CLOCK = 1 << 4;
static const uint8_t REG_CONFIG_SECOND_CLOCK = 1 << 5;
static const uint8_t REG_CONFIG_FIRST_TRANSLATION = 1 << 6;
static const uint8_t REG_CONFIG_ZERO2 = 1 << 7;
static const uint8_t DEVICE_RESET_OK = 0xAA;
static const uint8_t DEVICE_ECHO = 0xEE;
static const uint8_t DEVICE_ACK = 0xFA;
static const uint8_t DEVICE_RESEND = 0xFE;
static const uint8_t DEVICE_ERROR = 0xFF;
static const uint8_t DEVICE_CMD_ENABLE_SCAN = 0xF4;
static const uint8_t DEVICE_CMD_DISABLE_SCAN = 0xF5;
static const uint8_t DEVICE_CMD_IDENTIFY = 0xF2;
static const uint8_t DEVICE_CMD_RESET = 0xFF;
static const size_t DEVICE_RETRIES = 5;
static const unsigned int TIMEOUT_MS = 20;
static bool WaitInput()
{
return wait_inport8_clear(REG_STATUS, REG_STATUS_INPUT, false, TIMEOUT_MS);
}
static bool WaitOutput()
{
return wait_inport8_set(REG_STATUS, REG_STATUS_OUTPUT, false, TIMEOUT_MS);
}
static bool TryReadByte(uint8_t* result)
{
if ( !WaitOutput() )
return false;
*result = inport8(REG_DATA);
return true;
}
static bool TryWriteByte(uint8_t byte)
{
if ( !WaitInput() )
return false;
outport8(REG_DATA, byte);
return true;
}
static bool TryWriteCommand(uint8_t byte)
{
if ( !WaitInput() )
return false;
outport8(REG_COMMAND, byte);
return true;
}
static bool TryWriteToPort(uint8_t byte, uint8_t port)
{
if ( port == 2 && !TryWriteCommand(REG_COMMAND_SEND_TO_PORT_2) )
return false;
return TryWriteByte(byte);
}
static bool TryWriteCommandToPort(uint8_t command, uint8_t port, uint8_t* answer)
{
*answer = DEVICE_ERROR;
size_t unrelated = 0;
for ( size_t i = 0; i < DEVICE_RETRIES; i++ )
{
if ( !TryWriteToPort(command, port) )
return false;
again:
uint8_t byte;
if ( !TryReadByte(&byte) )
return false;
if ( byte == DEVICE_RESET_OK && !TryReadByte(&byte) )
return false;
*answer = byte;
if ( byte == DEVICE_ACK || byte == DEVICE_ECHO )
return true;
if ( byte != DEVICE_RESEND )
{
// We received a weird response, probably pending data, discard it
// and hope we receive a real acknowledgement.
if ( 1000 <= unrelated )
return false;
unrelated++;
goto again;
}
}
return false;
}
static bool IsKeyboardResponse(uint8_t* response, size_t size)
{
if ( size == 0 )
return true;
if ( size == 2 && response[0] == 0xAB && response[1] == 0x41 )
return true;
if ( size == 2 && response[0] == 0xAB && response[1] == 0xC1 )
return true;
if ( size == 2 && response[0] == 0xAB && response[1] == 0x83 )
return true;
return false;
}
static bool IsMouseResponse(uint8_t* response, size_t size)
{
if ( size == 1 && response[0] == 0x00 )
return true;
if ( size == 1 && response[0] == 0x03 )
return true;
if ( size == 1 && response[0] == 0x04 )
return true;
return false;
}
static void IRQ1Work(void* /*context*/);
static void IRQ12Work(void* /*context*/);
static struct interrupt_handler irq1_registration;
static struct interrupt_handler irq12_registration;
static struct interrupt_work irq1_work = { NULL, IRQ1Work, NULL };
static struct interrupt_work irq12_work = { NULL, IRQ12Work, NULL };
static PS2Device* irq1_device;
static PS2Device* irq12_device;
static unsigned char irq1_buffer[128];
static unsigned char irq12_buffer[128];
static size_t irq1_offset;
static size_t irq12_offset;
static size_t irq1_used;
static size_t irq12_used;
static bool irq1_working;
static bool irq12_working;
static void IRQ1Work(void* /*context*/)
{
Interrupt::Disable();
size_t todo = irq1_used;
for ( size_t i = 0; i < todo; i++ )
{
unsigned char byte = irq1_buffer[irq1_offset++];
if ( sizeof(irq1_buffer) <= irq1_offset )
irq1_offset = 0;
irq1_used--;
Interrupt::Enable();
if ( irq1_device )
irq1_device->PS2DeviceOnByte(byte);
Interrupt::Disable();
}
if ( irq1_used )
Interrupt::ScheduleWork(&irq1_work);
else
irq1_working = false;
Interrupt::Enable();
}
static void IRQ12Work(void* /*context*/)
{
Interrupt::Disable();
size_t todo = irq12_used;
for ( size_t i = 0; i < todo; i++ )
{
unsigned char byte = irq12_buffer[irq12_offset++];
if ( sizeof(irq12_buffer) <= irq12_offset )
irq12_offset = 0;
irq12_used--;
Interrupt::Enable();
if ( irq12_device )
irq12_device->PS2DeviceOnByte(byte);
Interrupt::Disable();
}
if ( irq12_used )
Interrupt::ScheduleWork(&irq12_work);
else
irq12_working = false;
Interrupt::Enable();
}
static void OnIRQ1(struct interrupt_context* /*intctx*/, void* /*user*/)
{
if ( inport8(REG_STATUS) & REG_STATUS_OUTPUT )
{
uint8_t byte = inport8(REG_DATA);
// TODO: This drops incoming bytes if the buffer is full. Instead make
// the device not send further interrupts until we have available bytes.
if ( irq1_used < sizeof(irq1_buffer) )
{
size_t index = irq1_offset + irq1_used++;
if ( sizeof(irq1_buffer) <= index )
index -= sizeof(irq1_buffer);
irq1_buffer[index] = byte;
if ( !irq1_working )
{
Interrupt::ScheduleWork(&irq1_work);
irq1_working = true;
}
}
}
}
static void OnIRQ12(struct interrupt_context* intctx, void* user)
{
(void) intctx;
(void) user;
if ( inport8(REG_STATUS) & REG_STATUS_OUTPUT )
{
uint8_t byte = inport8(REG_DATA);
// TODO: This drops incoming bytes if the buffer is full. Instead make
// the device not send further interrupts until we have available bytes.
if ( irq12_used < sizeof(irq12_buffer) )
{
size_t index = irq12_offset + irq12_used++;
if ( sizeof(irq12_buffer) <= index )
index -= sizeof(irq12_buffer);
irq12_buffer[index] = byte;
if ( !irq12_working )
{
Interrupt::ScheduleWork(&irq12_work);
irq12_working = true;
}
}
}
}
static kthread_mutex_t ps2_device_send_mutex = KTHREAD_MUTEX_INITIALIZER;
static bool PS2DeviceSend(void* ctx, uint8_t byte)
{
ScopedLock lock(&ps2_device_send_mutex);
uint8_t port = (uint8_t) (uintptr_t) ctx;
return TryWriteToPort(byte, port);
}
static bool DetectDevice(uint8_t port, uint8_t* response, size_t* response_size);
void Init(PS2Device* keyboard, PS2Device* mouse)
{
uint8_t byte;
uint8_t config;
if ( !TryWriteCommand(REG_COMMAND_DISABLE_FIRST_PORT) ||
!TryWriteCommand(REG_COMMAND_DISABLE_SECOND_PORT) )
return;
while ( inport8(REG_STATUS) & REG_STATUS_OUTPUT )
inport8(REG_DATA);
if ( !TryWriteCommand(REG_COMMAND_READ_RAM) ||
!TryReadByte(&config) )
return;
config &= ~REG_CONFIG_FIRST_INTERRUPT;
config &= ~REG_CONFIG_SECOND_INTERRUPT;
//config &= ~REG_CONFIG_FIRST_TRANSLATION; // TODO: Not ready for this yet.
if ( !TryWriteCommand(REG_COMMAND_WRITE_RAM) ||
!TryWriteByte(config) )
return;
bool dual = config & REG_CONFIG_SECOND_CLOCK;
if ( !TryWriteCommand(REG_COMMAND_TEST_CONTROLLER) ||
!TryReadByte(&byte) )
return;
if ( byte == 0xFF )
return;
if ( byte != 0x55 )
{
Log::PrintF("[PS/2 controller] Self-test failure resulted in "
"0x%02X instead of 0x55\n", byte);
return;
}
if ( dual )
{
uint8_t config;
if ( !TryWriteCommand(REG_COMMAND_ENABLE_SECOND_PORT) ||
!TryWriteCommand(REG_COMMAND_READ_RAM) ||
!TryReadByte(&config) )
return;
dual = !(config & REG_CONFIG_SECOND_CLOCK);
if ( dual && !TryWriteCommand(REG_COMMAND_DISABLE_SECOND_PORT) )
return;
}
bool port_1 = true;
bool port_2 = dual;
#if 0 // Disabled due to some emulated PS/2 controllers not handling this well.
if ( port_1 )
{
if ( !TryWriteCommand(REG_COMMAND_TEST_FIRST_PORT) ||
!TryReadByte(&byte) )
return;
port_1 = byte == 0x00;
}
if ( port_2 )
{
if ( !TryWriteCommand(REG_COMMAND_TEST_SECOND_PORT) ||
!TryReadByte(&byte) )
return;
port_2 = byte == 0x00;
}
#endif
size_t port_1_resp_size = 0;
uint8_t port_1_resp[2];
if ( port_1 && !DetectDevice(1, port_1_resp, &port_1_resp_size) )
port_1 = false;
size_t port_2_resp_size = 0;
uint8_t port_2_resp[2];
if ( port_2 && !DetectDevice(2, port_2_resp, &port_2_resp_size) )
port_2 = false;
if ( port_1 && !irq1_device &&
IsKeyboardResponse(port_1_resp, port_1_resp_size) )
irq1_device = keyboard;
if ( port_2 && !irq12_device &&
IsMouseResponse(port_2_resp, port_2_resp_size) )
irq12_device = mouse;
if ( port_1 && !irq1_device &&
IsMouseResponse(port_1_resp, port_1_resp_size) )
irq1_device = mouse;
if ( port_2 && !irq12_device &&
IsKeyboardResponse(port_2_resp, port_2_resp_size) )
irq12_device = keyboard;
port_1 = port_1 && irq1_device;
port_2 = port_2 && irq12_device;
if ( !TryWriteCommand(REG_COMMAND_READ_RAM) ||
!TryReadByte(&config) )
return;
irq1_registration.handler = OnIRQ1;
irq1_registration.context = NULL;
Interrupt::RegisterHandler(Interrupt::IRQ1, &irq1_registration);
irq12_registration.handler = OnIRQ12;
irq12_registration.context = NULL;
Interrupt::RegisterHandler(Interrupt::IRQ12, &irq12_registration);
config |= port_1 ? REG_CONFIG_FIRST_INTERRUPT : 0;
config |= port_2 ? REG_CONFIG_SECOND_INTERRUPT : 0;
if ( !TryWriteCommand(REG_COMMAND_WRITE_RAM) ||
!TryWriteByte(config) )
return;
if ( port_1 && !TryWriteCommand(REG_COMMAND_ENABLE_FIRST_PORT) )
return;
if ( port_2 && !TryWriteCommand(REG_COMMAND_ENABLE_SECOND_PORT) )
return;
if ( irq1_device )
irq1_device->PS2DeviceInitialize((void*) 1, PS2DeviceSend,
port_1_resp, port_1_resp_size);
if ( irq12_device )
irq12_device->PS2DeviceInitialize((void*) 2, PS2DeviceSend,
port_2_resp, port_2_resp_size);
}
static bool DetectDevice(uint8_t port, uint8_t* response, size_t* response_size)
{
uint8_t enable = port == 1 ? REG_COMMAND_ENABLE_FIRST_PORT :
REG_COMMAND_ENABLE_SECOND_PORT;
uint8_t disable = port == 1 ? REG_COMMAND_DISABLE_FIRST_PORT :
REG_COMMAND_DISABLE_SECOND_PORT;
uint8_t byte;
if ( !TryWriteCommand(enable) )
return false;
if ( !TryWriteCommandToPort(DEVICE_CMD_DISABLE_SCAN, port, &byte) )
{
if ( byte == DEVICE_RESEND )
{
// HARDWARE BUG:
// This may be incomplete PS/2 emulation that simulates the
// controller but the devices always responds with 0xFE to anything
// they receive. This happens on sortie's pc1, but the keyboard
// device still supplies IRQ1's and scancodes. Let's assume the
// devices are stil there even though we can't control them.
if ( port == 1 )
{
*response_size = 2;
response[0] = 0xAB;
response[1] = 0x83;
if ( !TryWriteCommand(disable) )
return false;
return true;
}
if ( port == 2 )
{
*response_size = 1;
response[0] = 0x00;
if ( !TryWriteCommand(disable) )
return false;
return true;
}
}
TryWriteCommand(disable);
return false;
}
// Empty pending buffer.
while ( TryReadByte(&byte) )
continue;
if ( !TryWriteCommandToPort(DEVICE_CMD_IDENTIFY, port, &byte) )
{
TryWriteCommand(disable);
return false;
}
*response_size = 0;
if ( TryReadByte(&byte) )
{
response[(*response_size)++] = byte;
if ( TryReadByte(&byte) )
response[(*response_size)++] = byte;
}
if ( !TryWriteCommand(disable) )
return false;
return true;
}
} // namespace PS2
} // namespace Sortix