root/stand/efi/libefi/eficom.c
/*-
 * Copyright (c) 1998 Michael Smith (msmith@freebsd.org)
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <stand.h>
#include <sys/errno.h>
#include <bootstrap.h>
#include <stdbool.h>

#include <efi.h>
#include <efilib.h>
#include <Protocol/SerialIo.h>

static EFI_GUID serial = EFI_SERIAL_IO_PROTOCOL_GUID;

#define COMC_TXWAIT     0x40000         /* transmit timeout */

#define PNP0501         0x501           /* 16550A-compatible COM port */

struct serial {
        uint64_t        newbaudrate;
        uint64_t        baudrate;
        uint32_t        timeout;
        uint32_t        receivefifodepth;
        uint32_t        databits;
        EFI_PARITY_TYPE parity;
        EFI_STOP_BITS_TYPE stopbits;
        int             ioaddr;         /* index in handles array */
        EFI_HANDLE      currdev;        /* current serial device */
        EFI_HANDLE      condev;         /* EFI Console device */
        SERIAL_IO_INTERFACE *sio;
};

static void     comc_probe(struct console *);
static int      comc_init(int);
static void     comc_putchar(int);
static int      comc_getchar(void);
static int      comc_ischar(void);
static bool     comc_setup(void);
static int      comc_parse_intval(const char *, unsigned *);
static int      comc_port_set(struct env_var *, int, const void *);
static int      comc_speed_set(struct env_var *, int, const void *);

static struct serial    *comc_port;
extern struct console efi_console;

struct console eficom = {
        .c_name = "eficom",
        .c_desc = "serial port",
        .c_flags = 0,
        .c_probe = comc_probe,
        .c_init = comc_init,
        .c_out = comc_putchar,
        .c_in = comc_getchar,
        .c_ready = comc_ischar,
};

#if defined(__aarch64__) && __FreeBSD_version < 1500000
static void     comc_probe_compat(struct console *);
struct console comconsole = {
        .c_name = "comconsole",
        .c_desc = "serial port",
        .c_flags = 0,
        .c_probe = comc_probe_compat,
        .c_init = comc_init,
        .c_out = comc_putchar,
        .c_in = comc_getchar,
        .c_ready = comc_ischar,
};
#endif

static EFI_STATUS
efi_serial_init(EFI_HANDLE **handlep, int *nhandles)
{
        UINTN bufsz = 0;
        EFI_STATUS status;
        EFI_HANDLE *handles;

        /*
         * get buffer size
         */
        *nhandles = 0;
        handles = NULL;
        status = BS->LocateHandle(ByProtocol, &serial, NULL, &bufsz, handles);
        if (status != EFI_BUFFER_TOO_SMALL)
                return (status);

        if ((handles = malloc(bufsz)) == NULL)
                return (ENOMEM);

        *nhandles = (int)(bufsz / sizeof (EFI_HANDLE));
        /*
         * get handle array
         */
        status = BS->LocateHandle(ByProtocol, &serial, NULL, &bufsz, handles);
        if (EFI_ERROR(status)) {
                free(handles);
                *nhandles = 0;
        } else
                *handlep = handles;
        return (status);
}

/*
 * Find serial device number from device path.
 * Return -1 if not found.
 */
static int
efi_serial_get_index(EFI_DEVICE_PATH *devpath, int idx)
{
        ACPI_HID_DEVICE_PATH  *acpi;
        CHAR16 *text;

        while (!IsDevicePathEnd(devpath)) {
                if (DevicePathType(devpath) == MESSAGING_DEVICE_PATH &&
                    DevicePathSubType(devpath) == MSG_UART_DP)
                        return (idx);

                if (DevicePathType(devpath) == ACPI_DEVICE_PATH &&
                    (DevicePathSubType(devpath) == ACPI_DP ||
                    DevicePathSubType(devpath) == ACPI_EXTENDED_DP)) {

                        acpi = (ACPI_HID_DEVICE_PATH *)devpath;
                        if (acpi->HID == EISA_PNP_ID(PNP0501)) {
                                return (acpi->UID);
                        }
                }

                devpath = NextDevicePathNode(devpath);
        }
        return (-1);
}

/*
 * The order of handles from LocateHandle() is not known, we need to
 * iterate handles, pick device path for handle, and check the device
 * number.
 */
static EFI_HANDLE
efi_serial_get_handle(int port, EFI_HANDLE condev)
{
        EFI_STATUS status;
        EFI_HANDLE *handles, handle;
        EFI_DEVICE_PATH *devpath;
        int index, nhandles;

        if (port == -1)
                return (NULL);

        handles = NULL;
        nhandles = 0;
        status = efi_serial_init(&handles, &nhandles);
        if (EFI_ERROR(status))
                return (NULL);

        /*
         * We have console handle, set ioaddr for it.
         */
        if (condev != NULL) {
                for (index = 0; index < nhandles; index++) {
                        if (condev == handles[index]) {
                                devpath = efi_lookup_devpath(condev);
                                comc_port->ioaddr =
                                    efi_serial_get_index(devpath, index);
                                efi_close_devpath(condev);
                                free(handles);
                                return (condev);
                        }
                }
        }

        handle = NULL;
        for (index = 0; handle == NULL && index < nhandles; index++) {
                devpath = efi_lookup_devpath(handles[index]);
                if (port == efi_serial_get_index(devpath, index))
                        handle = (handles[index]);
                efi_close_devpath(handles[index]);
        }

        /*
         * In case we did fail to identify the device by path, use port as
         * array index. Note, we did check port == -1 above.
         */
        if (port < nhandles && handle == NULL)
                handle = handles[port];

        free(handles);
        return (handle);
}

static EFI_HANDLE
comc_get_con_serial_handle(const char *name)
{
        EFI_HANDLE handle;
        EFI_DEVICE_PATH *node;
        EFI_STATUS status;
        char *buf, *ep;
        size_t sz;

        buf = NULL;
        sz = 0;
        status = efi_global_getenv(name, buf, &sz);
        if (status == EFI_BUFFER_TOO_SMALL) {
                buf = malloc(sz);
                if (buf == NULL)
                        return (NULL);
                status = efi_global_getenv(name, buf, &sz);
        }
        if (status != EFI_SUCCESS) {
                free(buf);
                return (NULL);
        }

        ep = buf + sz;
        node = (EFI_DEVICE_PATH *)buf;
        while ((char *)node < ep) {
                status = BS->LocateDevicePath(&serial, &node, &handle);
                if (status == EFI_SUCCESS) {
                        free(buf);
                        return (handle);
                }

                /* Sanity check the node before moving to the next node. */
                if (DevicePathNodeLength(node) < sizeof(*node))
                        break;

                /* Start of next device path in list. */
                node = NextDevicePathNode(node);
        }
        free(buf);
        return (NULL);
}

/*
 * Called from cons_probe() to see if this device is available.
 * Return immediately on x86, except for hyperv, since it interferes with
 * common configurations otherwise (yes, this is just firewalling the bug).
 */
static void
comc_probe(struct console *sc)
{
        EFI_STATUS status;
        EFI_HANDLE handle;
        char name[20];
        char value[20];
        unsigned val;
        char *env, *buf, *ep;
        size_t sz;

#ifdef __amd64__
        /*
         * This driver tickles issues on a number of different firmware loads.
         * It is only required for HyperV, and is only known to work on HyperV,
         * so only allow it on HyperV.
         */
        env = getenv("smbios.bios.version");
        if (env == NULL || strncmp(env, "Hyper-V", 7) != 0) {
                return;
        }
#endif

        if (comc_port == NULL) {
                comc_port = calloc(1, sizeof (struct serial));
                if (comc_port == NULL)
                        return;
        }

        /* Use defaults from firmware */
        comc_port->databits = 8;
        comc_port->parity = DefaultParity;
        comc_port->stopbits = DefaultStopBits;

        handle = NULL;
        env = getenv("efi_com_port");
        if (comc_parse_intval(env, &val) == CMD_OK) {
                comc_port->ioaddr = val;
        } else {
                /*
                 * efi_com_port is not set, we need to select default.
                 * First, we consult ConOut variable to see if
                 * we have serial port redirection. If not, we just
                 * pick first device.
                 */
                handle = comc_get_con_serial_handle("ConOut");
                comc_port->condev = handle;
        }

        handle = efi_serial_get_handle(comc_port->ioaddr, handle);
        if (handle != NULL) {
                comc_port->currdev = handle;
                status = BS->OpenProtocol(handle, &serial,
                    (void**)&comc_port->sio, IH, NULL,
                    EFI_OPEN_PROTOCOL_GET_PROTOCOL);

                if (EFI_ERROR(status)) {
                        comc_port->sio = NULL;
                } else {
                        comc_port->newbaudrate =
                            comc_port->baudrate = comc_port->sio->Mode->BaudRate;
                        comc_port->timeout = comc_port->sio->Mode->Timeout;
                        comc_port->receivefifodepth =
                            comc_port->sio->Mode->ReceiveFifoDepth;
                        comc_port->databits = comc_port->sio->Mode->DataBits;
                        comc_port->parity = comc_port->sio->Mode->Parity;
                        comc_port->stopbits = comc_port->sio->Mode->StopBits;
                }
        }

        /*
         * If there's no sio, then the device isn't there, so just return since
         * the present flags aren't yet set.
         */
        if (comc_port->sio == NULL) {
                free(comc_port);
                comc_port = NULL;
                return;
        }

        if (env != NULL)
                unsetenv("efi_com_port");
        snprintf(value, sizeof (value), "%u", comc_port->ioaddr);
        env_setenv("efi_com_port", EV_VOLATILE, value,
            comc_port_set, env_nounset);

        env = getenv("efi_com_speed");
        if (env == NULL)
                /* fallback to comconsole setting */
                env = getenv("comconsole_speed");

        if (comc_parse_intval(env, &val) == CMD_OK)
                comc_port->newbaudrate = val;

        if (env != NULL)
                unsetenv("efi_com_speed");
        snprintf(value, sizeof (value), "%ju", (uintmax_t)comc_port->baudrate);
        env_setenv("efi_com_speed", EV_VOLATILE, value,
            comc_speed_set, env_nounset);

        if (comc_setup()) {
                sc->c_flags = C_PRESENTIN | C_PRESENTOUT;
        } else {
                sc->c_flags &= ~(C_PRESENTIN | C_PRESENTOUT);
                free(comc_port);
                comc_port = NULL;
        }
}

#if defined(__aarch64__) && __FreeBSD_version < 1500000
static void
comc_probe_compat(struct console *sc)
{
        comc_probe(&eficom);
        if (eficom.c_flags & (C_PRESENTIN | C_PRESENTOUT)) {
                printf("comconsole: comconsole device name is deprecated, switch to eficom\n");
        }
        /*
         * Note: We leave the present bits unset in sc to avoid ghosting.
         */
}
#endif

/*
 * Called when the console is selected in cons_change. If we didn't detect the
 * device, comc_port will be NULL, and comc_setup will fail. It may be called
 * even when the device isn't present as a 'fallback' console or when listed
 * specifically in console env, so we have to reset the c_flags in those case to
 * say it's not present.
 */
static int
comc_init(int arg __unused)
{
        if (comc_setup())
                return (0);

        eficom.c_flags &= ~(C_ACTIVEIN | C_ACTIVEOUT);
        return (1);
}

static void
comc_putchar(int c)
{
        int wait;
        EFI_STATUS status;
        UINTN bufsz = 1;
        char cb = c;

        if (comc_port->sio == NULL)
                return;

        for (wait = COMC_TXWAIT; wait > 0; wait--) {
                status = comc_port->sio->Write(comc_port->sio, &bufsz, &cb);
                if (status != EFI_TIMEOUT)
                        break;
        }
}

static int
comc_getchar(void)
{
        EFI_STATUS status;
        UINTN bufsz = 1;
        char c;


        /*
         * if this device is also used as ConIn, some firmwares
         * fail to return all input via SIO protocol.
         */
        if (comc_port->currdev == comc_port->condev) {
                if ((efi_console.c_flags & C_ACTIVEIN) == 0)
                        return (efi_console.c_in());
                return (-1);
        }

        if (comc_port->sio == NULL)
                return (-1);

        status = comc_port->sio->Read(comc_port->sio, &bufsz, &c);
        if (EFI_ERROR(status) || bufsz == 0)
                return (-1);

        return (c);
}

static int
comc_ischar(void)
{
        EFI_STATUS status;
        uint32_t control;

        /*
         * if this device is also used as ConIn, some firmwares
         * fail to return all input via SIO protocol.
         */
        if (comc_port->currdev == comc_port->condev) {
                if ((efi_console.c_flags & C_ACTIVEIN) == 0)
                        return (efi_console.c_ready());
                return (0);
        }

        if (comc_port->sio == NULL)
                return (0);

        status = comc_port->sio->GetControl(comc_port->sio, &control);
        if (EFI_ERROR(status))
                return (0);

        return (!(control & EFI_SERIAL_INPUT_BUFFER_EMPTY));
}

static int
comc_parse_intval(const char *value, unsigned *valp)
{
        unsigned n;
        char *ep;

        if (value == NULL || *value == '\0')
                return (CMD_ERROR);

        errno = 0;
        n = strtoul(value, &ep, 10);
        if (errno != 0 || *ep != '\0')
                return (CMD_ERROR);
        *valp = n;

        return (CMD_OK);
}

static int
comc_port_set(struct env_var *ev, int flags, const void *value)
{
        unsigned port;
        SERIAL_IO_INTERFACE *sio;
        EFI_HANDLE handle;
        EFI_STATUS status;

        if (value == NULL || comc_port == NULL)
                return (CMD_ERROR);

        if (comc_parse_intval(value, &port) != CMD_OK)
                return (CMD_ERROR);

        handle = efi_serial_get_handle(port, NULL);
        if (handle == NULL) {
                printf("no handle\n");
                return (CMD_ERROR);
        }

        status = BS->OpenProtocol(handle, &serial,
            (void**)&sio, IH, NULL, EFI_OPEN_PROTOCOL_GET_PROTOCOL);

        if (EFI_ERROR(status)) {
                printf("OpenProtocol: %lu\n", DECODE_ERROR(status));
                return (CMD_ERROR);
        }

        comc_port->currdev = handle;
        comc_port->ioaddr = port;
        comc_port->sio = sio;
        
        (void) comc_setup();

        env_setenv(ev->ev_name, flags | EV_NOHOOK, value, NULL, NULL);
        return (CMD_OK);
}

static int
comc_speed_set(struct env_var *ev, int flags, const void *value)
{
        unsigned speed;

        if (value == NULL || comc_port == NULL)
                return (CMD_ERROR);

        if (comc_parse_intval(value, &speed) != CMD_OK)
                return (CMD_ERROR);

        comc_port->newbaudrate = speed;
        if (comc_setup())
                env_setenv(ev->ev_name, flags | EV_NOHOOK, value, NULL, NULL);

        return (CMD_OK);
}

/*
 * In case of error, we also reset ACTIVE flags, so the console
 * framefork will try alternate consoles.
 */
static bool
comc_setup(void)
{
        EFI_STATUS status;
        char *ev;

        /*
         * If the device isn't active, or there's no port present.
         */
        if ((eficom.c_flags & (C_ACTIVEIN | C_ACTIVEOUT)) == 0 || comc_port == NULL)
                return (false);

        if (comc_port->sio->Reset != NULL) {
                status = comc_port->sio->Reset(comc_port->sio);
                if (EFI_ERROR(status))
                        return (false);
        }

        /*
         * Avoid setting the baud rate on Hyper-V. Also, only set the baud rate
         * if the baud rate has changed from the default. And pass in '0' or
         * DefaultFoo when we're not changing those values. Some EFI
         * implementations get cranky when you set things to the values reported
         * back even when they are unchanged.
         */
        if (comc_port->sio->SetAttributes != NULL &&
            comc_port->newbaudrate != comc_port->baudrate) {
                ev = getenv("smbios.bios.version");
                if (ev != NULL && strncmp(ev, "Hyper-V", 7) != 0) {
                        status = comc_port->sio->SetAttributes(comc_port->sio,
                            comc_port->newbaudrate, 0, 0, DefaultParity, 0,
                            DefaultStopBits);
                        if (EFI_ERROR(status))
                                return (false);
                        comc_port->baudrate = comc_port->newbaudrate;
                }
        }

#ifdef EFI_FORCE_RTS
        if (comc_port->sio->GetControl != NULL && comc_port->sio->SetControl != NULL) {
                UINT32 control;

                status = comc_port->sio->GetControl(comc_port->sio, &control);
                if (EFI_ERROR(status))
                        return (false);
                control |= EFI_SERIAL_REQUEST_TO_SEND;
                (void) comc_port->sio->SetControl(comc_port->sio, control);
        }
#endif
        /* Mark this port usable. */
        eficom.c_flags |= (C_PRESENTIN | C_PRESENTOUT);
        return (true);
}