diff options
Diffstat (limited to 'stand/efi/libefi/eficom.c')
-rw-r--r-- | stand/efi/libefi/eficom.c | 601 |
1 files changed, 601 insertions, 0 deletions
diff --git a/stand/efi/libefi/eficom.c b/stand/efi/libefi/eficom.c new file mode 100644 index 000000000000..d5f3f07e083f --- /dev/null +++ b/stand/efi/libefi/eficom.c @@ -0,0 +1,601 @@ +/*- + * 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> + +static EFI_GUID serial = SERIAL_IO_PROTOCOL; + +#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 (CMD_OK); + + eficom.c_flags &= ~(C_ACTIVEIN | C_ACTIVEOUT); + return (CMD_ERROR); +} + +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", EFI_ERROR_CODE(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); +} |