memtest86plus/system/uhci.c
martinwhitaker 407fb811c2
Take ownership of all USB controllers before probing for devices. (#167)
When two controllers are attached to a physical port (e.g. in the
case of EHCI and its companion controllers, problems can occur if
the BIOS still has control of one controller when we try to use the
other one. So perform a first pass to scan the PCI bus and take
ownership of and reset all the controllers we find, and perform a
second pass to initialise the controllers and probe for attached
devices.

As we don't support hot plugging, split the second pass into two,
with the first probing the EHCI controllers and handing over any
low and full speed devices to the companion controllers, and the
second probing the remaining controller types.
2022-10-07 09:32:09 +01:00

583 lines
21 KiB
C

// SPDX-License-Identifier: GPL-2.0
// Copyright (C) 2021-2022 Martin Whitaker.
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "heap.h"
#include "io.h"
#include "memrw32.h"
#include "memsize.h"
#include "pci.h"
#include "usb.h"
#include "string.h"
#include "unistd.h"
#include "uhci.h"
//------------------------------------------------------------------------------
// Constants
//------------------------------------------------------------------------------
// Values defined by the UHCI specification.
// Basic limits
#define UHCI_FL_LENGTH 1024 // Maximum number of entries in periodic frame list
// Register addresses in PCI Config space
#define UHCI_LEGSUP 0xc0 // Legacy Support register
// Legacy Support register
#define UHCI_LEGSUP_TBY60R 0x0100 // Trap By 0x60 Read Status/Clear
#define UHCI_LEGSUP_TBY60W 0x0200 // Trap By 0x60 Write Status/Clear
#define UHCI_LEGSUP_TBY64R 0x0400 // Trap By 0x64 Read Status/Clear
#define UHCI_LEGSUP_TBY64W 0x0800 // Trap By 0x64 Write Status/Clear
#define UHCI_LEGSUP_A20PTS 0x8000 // End of A20GATE Pass Through Status/Clear
#define UHCI_LEGSUP_CLEAR (UHCI_LEGSUP_TBY60R | UHCI_LEGSUP_TBY60W | UHCI_LEGSUP_TBY64R | UHCI_LEGSUP_TBY64W | UHCI_LEGSUP_A20PTS)
// Register addresses in I/O space
#define UHCI_USBCMD (io_base+0x00) // USB Command register
#define UHCI_USBSTS (io_base+0x02) // USB Status register
#define UHCI_USBINTR (io_base+0x04) // USB Interrupt Enable register
#define UHCI_FRNUM (io_base+0x06) // Frame Number register
#define UHCI_FLBASE (io_base+0x08) // Frame List Base Address register
#define UHCI_SOF (io_base+0x0c) // Start of Frame modify register
#define UHCI_PORT_SC(n) (io_base+0x10+2*n) // Port n Status and Control register (n = 0..MaxPortIdx)
// USB Command register
#define UHCI_USBCMD_R_S 0x0001 // Run/Stop
#define UHCI_USBCMD_HCR 0x0002 // Host Controller Reset
#define UHCI_USBCMD_GR 0x0004 // Global Reset
#define UHCI_USBCMD_MAXP 0x0080 // Max Packet
// USB Status register
#define UHCI_USBSTS_INT 0x0001 // Interrupt
#define UHCI_USBSTS_ERR 0x0002 // Error interrupt
#define UHCI_USBSTS_RESUME 0x0004 // Resume Detect
#define UHCI_USBSTS_HSE 0x0008 // Host System Error
#define UHCI_USBSTS_HCPE 0x0010 // Host Controller Processor Error
#define UHCI_USBSTS_HCH 0x0020 // Host Controller Halted
// USB Interrupt Enable register
#define UHCI_USBINTR_NONE 0x0000 // No interrupts enabled
// USB SOF register
#define UHCI_SOF_DEFAULT 0x40 // Default timing (1ms with 12MHz clock)
// Port Status and Control register
#define UHCI_PORT_SC_CCS 0x0001 // Current Connect Status
#define UHCI_PORT_SC_CCSC 0x0002 // Current Connect Status Change
#define UHCI_PORT_SC_PED 0x0004 // Port Enable/Disable
#define UHCI_PORT_SC_PEDC 0x0008 // Port Enable/Disable Change
#define UHCI_PORT_SC_RESUME 0x0040 // Resume Detect
#define UHCI_PORT_SC_VALID 0x0080 // Reserved bit (always set)
#define UHCI_PORT_SC_LSDA 0x0100 // Low Speed Device Attached
#define UHCI_PORT_SC_PR 0x0200 // Port Reset
#define UHCI_PORT_SC_SUSPEND 0x1000 // Suspend
// Link Pointer
#define UHCI_LP_TERMINATE 0x00000001 // Terminate (T) bit
#define UHCI_LP_TYPE_TD (0 << 1) // Type is Transfer Descriptor
#define UHCI_LP_TYPE_QH (1 << 1) // Type is Queue Head
#define UHCI_LP_BREADTH_FIRST (0 << 2)
#define UHCI_LP_DEPTH_FIRST (1 << 2)
// Queue Element Transfer Descriptor data structure
// - control_status member (32 bits)
#define UHCI_TD_BS_ERR 0x00020000 // Bitstuff Error
#define UHCI_TD_CRC_TO 0x00040000 // CRC or Time Out Error
#define UHCI_TD_NAK_RX 0x00080000 // NAK received
#define UHCI_TD_BABBLE 0x00100000 // Babble Detected
#define UHCI_TD_DB_ERR 0x00200000 // Data Buffer Error
#define UHCI_TD_STALLED 0x00400000 // Halted
#define UHCI_TD_ACTIVE 0x00800000 // Active
#define UHCI_TD_IOC_N (0 << 24) // Interrupt On Completion is off
#define UHCI_TD_IOC_Y (1 << 24) // Interrupt On Completion is on
#define UHCI_TD_ISO (1 << 25) // Isochronous Select
#define UHCI_TD_FULL_SPEED (0 << 26) // Full Speed
#define UHCI_TD_LOW_SPEED (1 << 26) // Low Speed
#define UHCI_TD_CERR(n) ((n) << 27) // Error Counter = n (n = 1..3)
#define UHCI_TD_SPD (1 << 29) // Short Packet Detect
// - token member (32 bits)
#define UHCI_TD_PID_OUT 0x000000e1 // PID Code is OUT
#define UHCI_TD_PID_IN 0x00000069 // PID Code is IN
#define UHCI_TD_PID_SETUP 0x0000002d // PID Code is SETUP
#define UHCI_TD_DEVICE_ADDR(n) ((n) << 8) // Device Address = n (n = 0..127)
#define UHCI_TD_ENDPOINT(n) ((n) << 15) // Endpoint = n (n = 0..15)
#define UHCI_TD_DT(n) ((n) << 19) // Data Toggle = n (n = 0,1)
#define UHCI_TD_LENGTH(n) ((((n) - 1) & 0x7ff) << 21)
// Values specific to this driver.
#define MAX_UHCI_PORTS 8 // the UHCI spec. doesn't define this, so pick a sensible number
#define MAX_KEYBOARDS 8 // per host controller
#define MAX_PACKETS 32 // per data transfer (must be >= MAX_KEYBOARDS)
#define WS_QH_SIZE (1 + MAX_KEYBOARDS) // Queue Head Descriptors
#define WS_TD_SIZE (2 + MAX_PACKETS) // Queue Transfer Descriptors
#define MILLISEC 1000 // in microseconds
//------------------------------------------------------------------------------
// Types
//------------------------------------------------------------------------------
// Data structures defined by the UHCI specification.
typedef volatile struct {
uint32_t link_ptr;
uint32_t control_status;
uint32_t token;
uint32_t buffer_ptr;
uint32_t driver_data[4];
} uhci_td_t __attribute__ ((aligned (16)));
typedef volatile struct {
uint32_t qh_link_ptr;
uint32_t qe_link_ptr;
uint32_t padding[2];
} uhci_qh_t __attribute__ ((aligned (16)));
// Data structures specific to this implementation.
typedef struct {
hcd_workspace_t base_ws;
// System memory data structures used by the host controller.
uhci_qh_t qh[WS_QH_SIZE] __attribute__ ((aligned (16)));
uhci_td_t td[WS_TD_SIZE] __attribute__ ((aligned (16)));
// Keyboard data transfer buffers.
hid_kbd_rpt_t kbd_rpt[MAX_KEYBOARDS];
// Saved keyboard reports.
hid_kbd_rpt_t prev_kbd_rpt[MAX_KEYBOARDS];
// I/O base address of the host controller registers.
uint16_t io_base;
// Number of keyboards detected.
int num_keyboards;
} workspace_t __attribute__ ((aligned (256)));
//------------------------------------------------------------------------------
// Private Functions
//------------------------------------------------------------------------------
static bool io_wait_until_clr(uint16_t io_reg, uint16_t bit_mask, int max_time)
{
int timer = max_time >> 3;
while (inw(io_reg) & bit_mask) {
if (timer == 0) return false;
usleep(8);
timer--;
}
return true;
}
static bool io_wait_until_set(uint16_t io_reg, uint16_t bit_mask, int max_time)
{
int timer = max_time >> 3;
while (~inw(io_reg) & bit_mask) {
if (timer == 0) return false;
usleep(8);
timer--;
}
return true;
}
static bool reset_host_controller(uint16_t io_base)
{
outw(inw(UHCI_USBCMD) | UHCI_USBCMD_HCR, UHCI_USBCMD);
usleep(1*MILLISEC); // allow the controller some time to recover from reset
return io_wait_until_clr(UHCI_USBCMD, UHCI_USBCMD_HCR, 1000*MILLISEC);
}
static bool start_host_controller(uint16_t io_base)
{
outw(UHCI_USBCMD_R_S | UHCI_USBCMD_MAXP, UHCI_USBCMD);
return io_wait_until_clr(UHCI_USBSTS, UHCI_USBSTS_HCH, 1000*MILLISEC);
}
static bool halt_host_controller(uint16_t io_base)
{
outw(inw(UHCI_USBCMD) & ~UHCI_USBCMD_R_S, UHCI_USBCMD);
return io_wait_until_set(UHCI_USBSTS, UHCI_USBSTS_HCH, 1000*MILLISEC);
}
static bool reset_uhci_port(uint16_t io_base, int port_idx)
{
uint16_t port_status = inw(UHCI_PORT_SC(port_idx)) & ~UHCI_PORT_SC_PED;
outw(port_status | UHCI_PORT_SC_PR, UHCI_PORT_SC(port_idx));
usleep(50*MILLISEC); // USB port reset time
outw(port_status & ~UHCI_PORT_SC_PR, UHCI_PORT_SC(port_idx));
return io_wait_until_clr(UHCI_PORT_SC(port_idx), UHCI_PORT_SC_PR, 5*MILLISEC);
}
static bool enable_uhci_port(uint16_t io_base, int port_idx)
{
uint16_t port_status = inw(UHCI_PORT_SC(port_idx));
outw(port_status | UHCI_PORT_SC_PED, UHCI_PORT_SC(port_idx));
return io_wait_until_set(UHCI_PORT_SC(port_idx), UHCI_PORT_SC_PED, 1000*MILLISEC);
}
static void disable_uhci_port(uint16_t io_base, int port_idx)
{
uint16_t port_status = inw(UHCI_PORT_SC(port_idx));
outw(port_status & ~UHCI_PORT_SC_PED, UHCI_PORT_SC(port_idx));
(void)io_wait_until_clr(UHCI_PORT_SC(port_idx), UHCI_PORT_SC_PED, 1000*MILLISEC);
}
static void build_uhci_td(uhci_td_t *td, const usb_ep_t *ep, uint32_t pid, uint32_t dt, uint32_t options,
const void *buffer, size_t length)
{
uint32_t device_speed = (ep->device_speed == USB_SPEED_LOW) ? UHCI_TD_LOW_SPEED : UHCI_TD_FULL_SPEED;
if (options & UHCI_TD_IOC_Y) {
td->link_ptr = UHCI_LP_TERMINATE;
} else {
td->link_ptr = (uintptr_t)(td + 1) | UHCI_LP_TYPE_TD | UHCI_LP_DEPTH_FIRST;
}
td->control_status = UHCI_TD_CERR(3)
| device_speed
| options
| UHCI_TD_ACTIVE;
td->token = UHCI_TD_LENGTH(length)
| dt
| UHCI_TD_ENDPOINT(ep->endpoint_num)
| UHCI_TD_DEVICE_ADDR(ep->device_id)
| pid;
td->buffer_ptr = (uintptr_t)buffer;
}
static uint16_t get_uhci_done(workspace_t *ws)
{
uint16_t io_base = ws->io_base;
uint16_t status = inw(UHCI_USBSTS) & (UHCI_USBSTS_INT | UHCI_USBSTS_ERR);
if (status != 0) {
if (status & UHCI_USBSTS_ERR || ws->qh[0].qe_link_ptr != UHCI_LP_TERMINATE) {
#if 1
uintptr_t td_addr = ws->qh[0].qe_link_ptr & 0xfffffff0;
uhci_td_t *td = (uhci_td_t *)td_addr;
print_usb_info(" transfer failed TD %08x status %08x token %08x",
td_addr, (uintptr_t)td->control_status, (uintptr_t)td->token);
#endif
write32(&ws->qh[0].qe_link_ptr, UHCI_LP_TERMINATE);
status |= UHCI_USBSTS_ERR;
}
outw(UHCI_USBSTS_INT | UHCI_USBSTS_ERR, UHCI_USBSTS);
}
return status;
}
static bool wait_for_uhci_done(workspace_t *ws)
{
// Rely on the controller to timeout if the device doesn't respond.
uint16_t status = 0;
while (true) {
status = get_uhci_done(ws);
if (status != 0) break;
usleep(10);
}
return ~status & UHCI_USBSTS_ERR;
}
//------------------------------------------------------------------------------
// Driver Methods
//------------------------------------------------------------------------------
static bool reset_root_hub_port(const usb_hcd_t *hcd, int port_num)
{
const workspace_t *ws = (const workspace_t *)hcd->ws;
return reset_uhci_port(ws->io_base, port_num - 1);
}
static bool setup_request(const usb_hcd_t *hcd, const usb_ep_t *ep, const usb_setup_pkt_t *setup_pkt)
{
workspace_t *ws = (workspace_t *)hcd->ws;
write32(&ws->qh[0].qe_link_ptr, UHCI_LP_TERMINATE);
build_uhci_td(&ws->td[0], ep, UHCI_TD_PID_SETUP, UHCI_TD_DT(0), UHCI_TD_IOC_N, setup_pkt, sizeof(usb_setup_pkt_t));
build_uhci_td(&ws->td[1], ep, UHCI_TD_PID_IN, UHCI_TD_DT(1), UHCI_TD_IOC_Y, 0, 0);
write32(&ws->qh[0].qe_link_ptr, (uintptr_t)(&ws->td[0]) | UHCI_LP_TYPE_TD);
return wait_for_uhci_done(ws);
}
static bool get_data_request(const usb_hcd_t *hcd, const usb_ep_t *ep, const usb_setup_pkt_t *setup_pkt,
const void *buffer, size_t length)
{
workspace_t *ws = (workspace_t *)hcd->ws;
size_t packet_size = ep->max_packet_size;
if (length > (MAX_PACKETS * packet_size)) {
return false;
}
write32(&ws->qh[0].qe_link_ptr, UHCI_LP_TERMINATE);
int pkt_num = 0;
build_uhci_td(&ws->td[pkt_num], ep, UHCI_TD_PID_SETUP, UHCI_TD_DT(pkt_num & 1), UHCI_TD_IOC_N, setup_pkt, sizeof(usb_setup_pkt_t)); pkt_num++;
while (length > packet_size) {
build_uhci_td(&ws->td[pkt_num], ep, UHCI_TD_PID_IN, UHCI_TD_DT(pkt_num & 1), UHCI_TD_SPD, buffer, packet_size); pkt_num++;
buffer = (uint8_t *)buffer + packet_size;
length -= packet_size;
}
build_uhci_td(&ws->td[pkt_num], ep, UHCI_TD_PID_IN, UHCI_TD_DT(pkt_num & 1), UHCI_TD_IOC_N, buffer, length); pkt_num++;
build_uhci_td(&ws->td[pkt_num], ep, UHCI_TD_PID_OUT, UHCI_TD_DT(1), UHCI_TD_IOC_Y, 0, 0);
write32(&ws->qh[0].qe_link_ptr, (uintptr_t)(&ws->td[0]) | UHCI_LP_TYPE_TD);
return wait_for_uhci_done(ws);
}
static void poll_keyboards(const usb_hcd_t *hcd)
{
workspace_t *ws = (workspace_t *)hcd->ws;
uint16_t io_base = ws->io_base;
uint32_t status = inw(UHCI_USBSTS) & (UHCI_USBSTS_INT | UHCI_USBSTS_ERR);
if (status != 0) {
outw(UHCI_USBSTS_INT | UHCI_USBSTS_ERR, UHCI_USBSTS);
for (int kbd_idx = 0; kbd_idx < ws->num_keyboards; kbd_idx++) {
uhci_qh_t *kbd_qh = &ws->qh[1 + kbd_idx];
uhci_td_t *kbd_td = &ws->td[2 + kbd_idx];
status = kbd_td->control_status;
if (status & UHCI_TD_ACTIVE) continue;
hid_kbd_rpt_t *kbd_rpt = &ws->kbd_rpt[kbd_idx];
uint32_t error_mask = UHCI_TD_STALLED | UHCI_TD_DB_ERR | UHCI_TD_BABBLE | UHCI_TD_NAK_RX | UHCI_TD_CRC_TO | UHCI_TD_BS_ERR;
if (~status & error_mask) {
hid_kbd_rpt_t *prev_kbd_rpt = &ws->prev_kbd_rpt[kbd_idx];
if (process_usb_keyboard_report(hcd, kbd_rpt, prev_kbd_rpt)) {
*prev_kbd_rpt = *kbd_rpt;
}
write32(&kbd_td->token, read32(&kbd_td->token) ^ UHCI_TD_DT(1));
}
// Reenable the TD.
write32(&kbd_td->control_status, kbd_td->driver_data[0]);
write32(&kbd_qh->qe_link_ptr, (uintptr_t)kbd_td | UHCI_LP_TYPE_TD);
}
}
}
//------------------------------------------------------------------------------
// Driver Method Table
//------------------------------------------------------------------------------
static const hcd_methods_t methods = {
.reset_root_hub_port = reset_root_hub_port,
.allocate_slot = NULL,
.release_slot = NULL,
.assign_address = assign_usb_address, // use default method
.configure_hub_ep = NULL,
.configure_kbd_ep = NULL,
.setup_request = setup_request,
.get_data_request = get_data_request,
.poll_keyboards = poll_keyboards
};
//------------------------------------------------------------------------------
// Public Functions
//------------------------------------------------------------------------------
bool uhci_reset(int bus, int dev, int func, uint16_t io_base)
{
// Disable PCI and SMM interrupts.
pci_config_write16(bus, dev, func, UHCI_LEGSUP, UHCI_LEGSUP_CLEAR);
// Ensure the controller is halted and then reset it.
if (!halt_host_controller(io_base)) return false;
if (!reset_host_controller(io_base)) return false;
return true;
}
bool uhci_probe(uint16_t io_base, usb_hcd_t *hcd)
{
// Record the heap state to allow us to free memory.
uintptr_t initial_heap_mark = heap_mark(HEAP_TYPE_LM_1);
// Allocate the frame list. This needs to be aligned on a 4K page boundary.
uintptr_t fl_addr = heap_alloc(HEAP_TYPE_LM_1, UHCI_FL_LENGTH * sizeof(uint32_t), PAGE_SIZE);
if (fl_addr == 0) {
goto no_keyboards_found;
}
uint32_t *fl = (uint32_t *)fl_addr;
// Allocate and initialise a workspace for this controller. This needs to be permanently mapped into virtual memory.
uintptr_t workspace_addr = heap_alloc(HEAP_TYPE_LM_1, sizeof(workspace_t), PAGE_SIZE);
if (workspace_addr == 0) {
goto no_keyboards_found;
}
workspace_t *ws = (workspace_t *)workspace_addr;
memset(ws, 0, sizeof(workspace_t));
ws->io_base = io_base;
// Initialise the asynchronous queue head.
ws->qh[0].qh_link_ptr = UHCI_LP_TERMINATE;
ws->qh[0].qe_link_ptr = UHCI_LP_TERMINATE;
// Initialise the frame list to execute the asynchronous queue.
for (int i = 0; i < UHCI_FL_LENGTH; i++) {
fl[i] = (uintptr_t)(&ws->qh[0]) | UHCI_LP_TYPE_QH;
}
// Initialise the driver object for this controller.
hcd->methods = &methods;
hcd->ws = &ws->base_ws;
// Initialise the host controller.
outw(UHCI_USBINTR_NONE, UHCI_USBINTR);
outw(0, UHCI_FRNUM);
outl(fl_addr, UHCI_FLBASE);
outb(UHCI_SOF_DEFAULT, UHCI_SOF);
if (!start_host_controller(io_base)) {
goto no_keyboards_found;
}
// Construct a hub descriptor for the root hub.
usb_hub_t root_hub;
memset(&root_hub, 0, sizeof(root_hub));
root_hub.ep0 = NULL;
root_hub.num_ports = MAX_UHCI_PORTS;
usleep(100*MILLISEC); // USB maximum device attach time
// Scan the ports, looking for hubs and keyboards.
usb_ep_t keyboards[MAX_KEYBOARDS];
int num_keyboards = 0;
int num_devices = 0;
for (int port_idx = 0; port_idx < root_hub.num_ports; port_idx++) {
// If we've filled the keyboard info table, abort now.
if (num_keyboards >= MAX_KEYBOARDS) break;
// Check if we've passed the last port.
uint16_t port_status = inw(UHCI_PORT_SC(port_idx));
if ((~port_status & UHCI_PORT_SC_VALID) || port_status == 0xffff) {
root_hub.num_ports = port_idx;
break;
}
// Check if anything is connected to this port.
if (~port_status & UHCI_PORT_SC_CCS) continue;
// Reset the port.
if (!reset_uhci_port(io_base, port_idx)) continue;
usleep(10*MILLISEC); // USB reset recovery time
// Enable the port.
if (!enable_uhci_port(io_base, port_idx)) continue;
port_status = inw(UHCI_PORT_SC(port_idx));
// Check the port is active.
if (~port_status & UHCI_PORT_SC_PED) continue;
num_devices++;
// Get the port speed.
usb_speed_t port_speed = port_status & UHCI_PORT_SC_LSDA ? USB_SPEED_LOW : USB_SPEED_FULL;
// Look for keyboards attached directly or indirectly to this port.
if (find_attached_usb_keyboards(hcd, &root_hub, 1 + port_idx, port_speed, num_devices,
&num_devices, keyboards, MAX_KEYBOARDS, &num_keyboards)) {
continue;
}
// If we didn't find any keyboard interfaces, we can disable the port.
disable_uhci_port(io_base, port_idx);
}
print_usb_info(" Found %i device%s, %i keyboard%s",
num_devices, num_devices != 1 ? "s" : "",
num_keyboards, num_keyboards != 1 ? "s" : "");
if (num_keyboards == 0) {
(void)halt_host_controller(io_base);
goto no_keyboards_found;
}
ws->num_keyboards = num_keyboards;
// Initialise the interrupt QH and TD for each keyboard interface and find the minimum interval.
int min_interval = UHCI_FL_LENGTH;
uint32_t first_qh_ptr = UHCI_LP_TERMINATE;
for (int kbd_idx = 0; kbd_idx < num_keyboards; kbd_idx++) {
usb_ep_t *kbd = &keyboards[kbd_idx];
uhci_qh_t *kbd_qh = &ws->qh[1 + kbd_idx];
uhci_td_t *kbd_td = &ws->td[2 + kbd_idx];
hid_kbd_rpt_t *kbd_rpt = &ws->kbd_rpt[kbd_idx];
build_uhci_td(kbd_td, kbd, UHCI_TD_PID_IN, UHCI_TD_DT(0), UHCI_TD_IOC_Y, kbd_rpt, sizeof(hid_kbd_rpt_t));
kbd_td->driver_data[0] = kbd_td->control_status;
kbd_qh->qe_link_ptr = (uintptr_t)kbd_td | UHCI_LP_TYPE_TD;
kbd_qh->qh_link_ptr = first_qh_ptr;
first_qh_ptr = (uintptr_t)kbd_qh | UHCI_LP_TYPE_QH;
if (kbd->interval < min_interval) {
min_interval = kbd->interval;
}
}
// Re-initialise the frame list to execute the periodic schedule.
for (int i = 0; i < UHCI_FL_LENGTH; i++) {
write32(&fl[i], UHCI_LP_TERMINATE);
}
for (int i = 0; i < UHCI_FL_LENGTH; i += min_interval) {
write32(&fl[i], first_qh_ptr);
}
return true;
no_keyboards_found:
heap_rewind(HEAP_TYPE_LM_1, initial_heap_mark);
return false;
}