root/src/add-ons/kernel/bus_managers/ps2/ps2_common.cpp
/*
 * Copyright 2004-2009 Haiku, Inc.
 * Distributed under the terms of the MIT License.
 *
 * Authors (in chronological order):
 *              Stefano Ceccherini (burton666@libero.it)
 *              Axel Dörfler, axeld@pinc-software.de
 *      Marcus Overhagen <marcus@overhagen.de>
 */

/*! PS/2 bus manager */


#include <string.h>

#include "ps2_common.h"
#include "ps2_service.h"
#include "ps2_dev.h"


//#define TRACE_PS2_COMMON
#ifdef TRACE_PS2_COMMON
#       define TRACE(x...) dprintf(x)
#       define TRACE_ONLY
#else
#       define TRACE(x...)
#       define TRACE_ONLY __attribute__((unused))
#endif


isa_module_info *gIsa = NULL;
bool gActiveMultiplexingEnabled = false;
bool gSetupComplete = false;
mutex gControllerLock;

static int32 sIgnoreInterrupts = 0;


uint8
ps2_read_ctrl(void)
{
        return gIsa->read_io_8(PS2_PORT_CTRL);
}


uint8
ps2_read_data(void)
{
        return gIsa->read_io_8(PS2_PORT_DATA);
}


void
ps2_write_ctrl(uint8 ctrl)
{
        TRACE("ps2: ps2_write_ctrl 0x%02x\n", ctrl);

        gIsa->write_io_8(PS2_PORT_CTRL, ctrl);
}


void
ps2_write_data(uint8 data)
{
        TRACE("ps2: ps2_write_data 0x%02x\n", data);

        gIsa->write_io_8(PS2_PORT_DATA, data);
}


status_t
ps2_wait_read(void)
{
        int i;
        for (i = 0; i < PS2_CTRL_WAIT_TIMEOUT / 50; i++) {
                if (ps2_read_ctrl() & PS2_STATUS_OUTPUT_BUFFER_FULL)
                        return B_OK;
                snooze(50);
        }
        return B_ERROR;
}


status_t
ps2_wait_write(void)
{
        int i;
        for (i = 0; i < PS2_CTRL_WAIT_TIMEOUT / 50; i++) {
                if (!(ps2_read_ctrl() & PS2_STATUS_INPUT_BUFFER_FULL))
                        return B_OK;
                snooze(50);
        }
        return B_ERROR;
}


//      #pragma mark -


void
ps2_flush(void)
{
        int i;

        mutex_lock(&gControllerLock);
        atomic_add(&sIgnoreInterrupts, 1);

        for (i = 0; i < 64; i++) {
                uint8 ctrl;
                uint8 data TRACE_ONLY;
                ctrl = ps2_read_ctrl();
                if (!(ctrl & PS2_STATUS_OUTPUT_BUFFER_FULL))
                        break;
                data = ps2_read_data();
                TRACE("ps2: ps2_flush: ctrl 0x%02x, data 0x%02x (%s)\n", ctrl, data, (ctrl & PS2_STATUS_AUX_DATA) ? "aux" : "keyb");
                snooze(100);
        }

        atomic_add(&sIgnoreInterrupts, -1);
        mutex_unlock(&gControllerLock);
}


static status_t
ps2_selftest()
{
        status_t res;
        uint8 in;
        res = ps2_command(PS2_CTRL_SELF_TEST, NULL, 0, &in, 1);
        if (res != B_OK || in != 0x55) {
                INFO("ps2: controller self test failed, status 0x%08" B_PRIx32 ", data "
                        "0x%02x\n", res, in);
                return B_ERROR;
        }
        return B_OK;
}


static status_t
ps2_setup_command_byte(bool interruptsEnabled)
{
        status_t res;
        uint8 cmdbyte;

        res = ps2_command(PS2_CTRL_READ_CMD, NULL, 0, &cmdbyte, 1);
        TRACE("ps2: get command byte: res 0x%08" B_PRIx32 ", cmdbyte 0x%02x\n",
                res, cmdbyte);
        if (res != B_OK)
                cmdbyte = 0x47;

        cmdbyte |= PS2_BITS_TRANSLATE_SCANCODES;
        cmdbyte &= ~(PS2_BITS_KEYBOARD_DISABLED | PS2_BITS_MOUSE_DISABLED);

        if (interruptsEnabled)
                cmdbyte |= PS2_BITS_KEYBOARD_INTERRUPT | PS2_BITS_AUX_INTERRUPT;
        else
                cmdbyte &= ~(PS2_BITS_KEYBOARD_INTERRUPT | PS2_BITS_AUX_INTERRUPT);

        res = ps2_command(PS2_CTRL_WRITE_CMD, &cmdbyte, 1, NULL, 0);
        TRACE("ps2: set command byte: res 0x%08" B_PRIx32 ", cmdbyte 0x%02x\n",
                res, cmdbyte);

        return res;
}


static status_t
ps2_setup_active_multiplexing(bool *enabled)
{
        status_t res;
        uint8 in, out;

        // Disable the keyboard port to avoid any interference with the keyboard
        ps2_command(PS2_CTRL_KEYBOARD_DISABLE, NULL, 0, NULL, 0);

        out = 0xf0;
        res = ps2_command(PS2_CTRL_AUX_LOOPBACK, &out, 1, &in, 1);
        if (res)
                goto fail;
        // Step 1, if controller is good, in does match out.
        // This test failes with MS Virtual PC.
        if (in != out)
                goto no_support;

        out = 0x56;
        res = ps2_command(PS2_CTRL_AUX_LOOPBACK, &out, 1, &in, 1);
        if (res)
                goto fail;
        // Step 2, if controller is good, in does match out.
        if (in != out)
                goto no_support;

        out = 0xa4;
        res = ps2_command(PS2_CTRL_AUX_LOOPBACK, &out, 1, &in, 1);
        if (res)
                goto fail;
        // Step 3, if the controller doesn't support active multiplexing,
        // then in data does match out data (0xa4), else it's version number.
        if (in == out)
                goto no_support;

        // With some broken USB legacy emulation, it's 0xac, and with
        // MS Virtual PC, it's 0xa6. Since current active multiplexing
        // specification version is 1.1 (0x11), we validate the data.
        if (in > 0x9f) {
                TRACE("ps2: active multiplexing v%d.%d detected, but ignored!\n", (in >> 4), in & 0xf);
                goto no_support;
        }

        INFO("ps2: active multiplexing v%d.%d detected\n", (in >> 4), in & 0xf);

        // Additional check to make sure multiplexing is actually working:
        // Send a byte using the local loopback feature. When the byteis read back, it should have the
        // system/error bit set to 0 if active multiplexing is enabled and working.
        // The flow is similar to a ps2_command, but we need to check this specific bit in the middle
        // of the operation (before reading the data byte).
        mutex_lock(&gControllerLock);
        atomic_add(&sIgnoreInterrupts, 1);

        res = ps2_wait_write();
        if (res != B_OK) {
                INFO("ps2: active multiplexing command write check fail: %s\n", strerror(res));
                goto no_support_unlock;
        }
        ps2_write_ctrl(PS2_CTRL_AUX_LOOPBACK);

        res = ps2_wait_write();
        if (res != B_OK) {
                INFO("ps2: active multiplexing data write check fail: %s\n", strerror(res));
                goto no_support_unlock;
        }

        ps2_write_data(0xf0);

        res = ps2_wait_read();

        if (res != B_OK) {
                INFO("ps2: active multiplexing data read check fail: %s\n", strerror(res));
                goto no_support_unlock;
        }

        res = ps2_read_ctrl();
        in = ps2_read_data();

        if (in != 0xf0) {
                INFO("ps2: active multiplexing loopback check fail: %s\n", strerror(res));
                goto no_support_unlock;
        }

        if ((res & 0x25) == 0x25) {
                INFO("ps2: active multiplexing error bit is stuck\n");
                goto no_support_unlock;
        }

        atomic_add(&sIgnoreInterrupts, -1);
        mutex_unlock(&gControllerLock);
        *enabled = true;
        goto done;

no_support_unlock:
        atomic_add(&sIgnoreInterrupts, -1);
        mutex_unlock(&gControllerLock);
no_support:
        TRACE("ps2: active multiplexing not supported\n");
        *enabled = false;

done:
        // Some controllers get upset by the d3 command and will continue data
        // loopback, thus we need to send a harmless command (enable keyboard
        // interface) next.
        // This fixes bug report #1175
        res = ps2_command(PS2_CTRL_KEYBOARD_ENABLE, NULL, 0, NULL, 0);
        if (res != B_OK) {
                INFO("ps2: active multiplexing d3 workaround failed, status 0x%08"
                        B_PRIx32 "\n", res);
        }
        return B_OK;

fail:
        TRACE("ps2: testing for active multiplexing failed\n");
        *enabled = false;
        // this should revert the controller into legacy mode,
        // just in case it has switched to multiplexed mode
        return ps2_selftest();
}


status_t
ps2_command(uint8 cmd, const uint8 *out, int outCount, uint8 *in, int inCount)
{
        status_t res;
        int i;

        mutex_lock(&gControllerLock);
        atomic_add(&sIgnoreInterrupts, 1);

#ifdef TRACE_PS2_COMMON
        TRACE("ps2: ps2_command cmd 0x%02x, out %d, in %d\n", cmd, outCount, inCount);
        for (i = 0; i < outCount; i++)
                TRACE("ps2: ps2_command out 0x%02x\n", out[i]);
#endif

        res = ps2_wait_write();
        if (res == B_OK)
                ps2_write_ctrl(cmd);

        for (i = 0; res == B_OK && i < outCount; i++) {
                res = ps2_wait_write();
                if (res == B_OK)
                        ps2_write_data(out[i]);
                else
                        TRACE("ps2: ps2_command out byte %d failed\n", i);
        }

        for (i = 0; res == B_OK && i < inCount; i++) {
                res = ps2_wait_read();
                if (res == B_OK)
                        in[i] = ps2_read_data();
                else
                        TRACE("ps2: ps2_command in byte %d failed\n", i);
        }

#ifdef TRACE_PS2_COMMON
        for (i = 0; i < inCount; i++)
                TRACE("ps2: ps2_command in 0x%02x\n", in[i]);
        TRACE("ps2: ps2_command result 0x%08" B_PRIx32 "\n", res);
#endif

        atomic_add(&sIgnoreInterrupts, -1);
        mutex_unlock(&gControllerLock);

        return res;
}


//      #pragma mark -


static int32
ps2_interrupt(void* cookie)
{
        uint8 ctrl;
        uint8 data;
        bool error;
        ps2_dev *dev;

        ctrl = ps2_read_ctrl();
        if (!(ctrl & PS2_STATUS_OUTPUT_BUFFER_FULL)) {
                TRACE("ps2: ps2_interrupt unhandled, OBF bit unset, ctrl 0x%02x (%s)\n",
                                ctrl, (ctrl & PS2_STATUS_AUX_DATA) ? "aux" : "keyb");
                return B_UNHANDLED_INTERRUPT;
        }

        if (atomic_get(&sIgnoreInterrupts)) {
                TRACE("ps2: ps2_interrupt ignoring, ctrl 0x%02x (%s)\n", ctrl,
                        (ctrl & PS2_STATUS_AUX_DATA) ? "aux" : "keyb");
                return B_HANDLED_INTERRUPT;
        }

        data = ps2_read_data();

        if ((ctrl & PS2_STATUS_AUX_DATA) != 0) {
                uint8 idx;
                if (gActiveMultiplexingEnabled) {
                        idx = ctrl >> 6;
                        error = (ctrl & 0x04) != 0;
                        TRACE("ps2: ps2_interrupt ctrl 0x%02x, data 0x%02x (mouse %d)\n",
                                ctrl, data, idx);
                } else {
                        idx = 0;
                        error = (ctrl & 0xC0) != 0;
                        TRACE("ps2: ps2_interrupt ctrl 0x%02x, data 0x%02x (aux)\n", ctrl,
                                data);
                }
                dev = &ps2_device[PS2_DEVICE_MOUSE + idx];
        } else {
                TRACE("ps2: ps2_interrupt ctrl 0x%02x, data 0x%02x (keyb)\n", ctrl,
                        data);

                dev = &ps2_device[PS2_DEVICE_KEYB];
                error = (ctrl & 0xC0) != 0;
        }

        dev->history[1] = dev->history[0];
        dev->history[0].time = system_time();
        dev->history[0].data = data;
        dev->history[0].error = error;

        return ps2_dev_handle_int(dev);
}


//      #pragma mark - driver interface


status_t
ps2_init(void)
{
        status_t status;

        TRACE("ps2: init\n");

        status = get_module(B_ISA_MODULE_NAME, (module_info **)&gIsa);
        if (status < B_OK)
                return status;

        mutex_init(&gControllerLock, "ps/2 keyb ctrl");

        ps2_flush();

        status = ps2_dev_init();
        if (status < B_OK)
                goto err1;

        status = ps2_service_init();
        if (status < B_OK)
                goto err2;

        status = install_io_interrupt_handler(INT_PS2_KEYBOARD, &ps2_interrupt,
                NULL, 0);
        if (status)
                goto err3;

        status = install_io_interrupt_handler(INT_PS2_MOUSE, &ps2_interrupt, NULL,
                0);
        if (status)
                goto err4;

        // While this might have fixed bug #1185, we can't do this unconditionally
        // as it obviously messes up many controllers which couldn't reboot anymore
        // after that
        //ps2_selftest();

        // Setup the command byte with disabled keyboard and AUX interrupts
        // to prevent interrupts storm on some KBCs during active multiplexing
        // activation procedure. Fixes #7635.
        status = ps2_setup_command_byte(false);
        if (status) {
                INFO("ps2: initial setup of command byte failed\n");
                goto err5;
        }

        ps2_flush();
        status = ps2_setup_active_multiplexing(&gActiveMultiplexingEnabled);
        if (status) {
                INFO("ps2: setting up active multiplexing failed\n");
                goto err5;
        }

        status = ps2_setup_command_byte(true);
        if (status) {
                INFO("ps2: setting up command byte with enabled interrupts failed\n");
                goto err5;
        }

        if (gActiveMultiplexingEnabled) {
                // The multiplexing spec recommends to leave device 0 unconnected because it saves some
                // confusion with the use of the D3 command which appears as if the replied data was
                // coming from device 0. So we enable it only if it really looks like there is a device
                // connected there.
                if (ps2_dev_command_timeout(&ps2_device[PS2_DEVICE_MOUSE],
                                PS2_CMD_MOUSE_SET_SCALE11, NULL, 0, NULL, 0, 100000) == B_TIMED_OUT) {
                        INFO("ps2: accessing multiplexed mouse port 0 timed out, ignoring it!\n");
                } else {
                        ps2_service_notify_device_added(&ps2_device[PS2_DEVICE_MOUSE]);
                }

                for (int idx = 1; idx <= 3; idx++) {
                        ps2_service_notify_device_added(&ps2_device[PS2_DEVICE_MOUSE + idx]);
                }
                ps2_service_notify_device_added(&ps2_device[PS2_DEVICE_KEYB]);
        } else {
                ps2_service_notify_device_added(&ps2_device[PS2_DEVICE_MOUSE]);
                ps2_service_notify_device_added(&ps2_device[PS2_DEVICE_KEYB]);
        }

        gSetupComplete = true;

        TRACE("ps2: init done!\n");
        return B_OK;

err5:
        remove_io_interrupt_handler(INT_PS2_MOUSE, &ps2_interrupt, NULL);
err4:
        remove_io_interrupt_handler(INT_PS2_KEYBOARD, &ps2_interrupt, NULL);
err3:
        ps2_service_exit();
err2:
        ps2_dev_exit();
err1:
        mutex_destroy(&gControllerLock);
        put_module(B_ISA_MODULE_NAME);
        TRACE("ps2: init failed!\n");
        return B_ERROR;
}


void
ps2_uninit(void)
{
        TRACE("ps2: uninit\n");
        remove_io_interrupt_handler(INT_PS2_MOUSE,    &ps2_interrupt, NULL);
        remove_io_interrupt_handler(INT_PS2_KEYBOARD, &ps2_interrupt, NULL);
        ps2_service_exit();
        ps2_dev_exit();
        mutex_destroy(&gControllerLock);
        put_module(B_ISA_MODULE_NAME);
}