Skip to content

Instantly share code, notes, and snippets.

@Crest
Created July 22, 2026 15:57
Show Gist options
  • Select an option

  • Save Crest/19a5f140f423614fdf5261539df9b525 to your computer and use it in GitHub Desktop.

Select an option

Save Crest/19a5f140f423614fdf5261539df9b525 to your computer and use it in GitHub Desktop.
Chromebook "cyan" keyboard driver for FreeBSD 15.1

cyan_kbdgpio for FreeBSD 15.1

Package revision: 2

This out-of-tree kernel module routes the Acer CB5-132T (Cyan) Chromebook's internal PS/2 keyboard interrupt from its ACPI-described Cherryview GPIO pin to FreeBSD's existing atkbd driver. It avoids a custom kernel and removes the need for 100 Hz keyboard polling.

The module discovers the route instead of hard-coding Cyan's pin number. It:

  1. finds a PNP0303 or PNP030B keyboard with a GpioInt() in _CRS;
  2. attaches only to the referenced INT33FF GPIO controller;
  3. preserves the firmware-programmed Cherryview INTSEL route;
  4. programs edge/level and polarity from ACPI, acknowledges and unmasks it;
  5. calls the already registered atkbd interrupt method when the GPIO IRQ arrives.

Important limitation

This is a deliberately narrow compatibility driver, not a full Cherryview GPIO driver. It owns the one INT33FF controller used by the keyboard, masks that controller's other interrupt lines, and does not create a gpiobus for that bank. Other INT33FF banks are left alone.

Do not load the stock chvgpio.ko first: once that driver has attached to the keyboard's GPIO bank, this module cannot take the device away from it. The module has a more specific probe priority, so preloading both should select this driver for the keyboard bank, but the simplest configuration is to load only cyan_kbdgpio.

Build and install

Install the FreeBSD 15.1 source tree at /usr/src, then run as root:

cd cyan-kbdgpio
make clean
make
make install

To test without rebooting, first make sure chvgpio is not loaded:

kldstat -n chvgpio
kldload ./cyan_kbdgpio.ko
dmesg | tail -30

An expected attach message resembles:

cyan_kbdgpio0: keyboard GpioInt pin 17 routed through line ...

Keep polling enabled for the first test so a failure does not strand the console. After the attach message appears, disable polling temporarily and test the internal keyboard:

sysctl hw.atkbd.hz=0

If that works, remove this line from /boot/device.hints:

hw.atkbd.hz="100"

Enable the module at boot in /boot/loader.conf:

cyan_kbdgpio_load="YES"

If you previously added chvgpio_load="YES", remove that line.

Locking model

The parent IRQ is registered without INTR_MPSAFE, so the FreeBSD interrupt framework holds Giant while the handler enters the legacy keyboard stack. The module's spin mutex protects only GPIO registers and callback state. Every spin-lock acquisition and release is in the same lexical block, and kbdd_intr() is called only after the spin mutex has been released.

Recovery

If the module does not attach, keep or restore hw.atkbd.hz="100", unload the module if present, and collect:

kldstat
devinfo -rv
acpidump -dt > cyan.asl
dmesg > dmesg.txt

If polling has already been disabled, re-enable it before unloading the module:

sysctl hw.atkbd.hz=100
kldunload cyan_kbdgpio

Detach masks and acknowledges the GPIO bank before releasing its interrupt and MMIO resources.

/*-
* SPDX-License-Identifier: BSD-2-Clause
*
* Copyright (c) 2026 The cyan_kbdgpio contributors
* Copyright (c) 2017 Tom Jones <tj@enoti.me>
* Copyright (c) 2016 Mark Kettenis
*
* Portions of the register definitions and controller logic are derived
* from FreeBSD's chvgpio(4), which in turn incorporates the OpenBSD driver.
* The original copyright and permission notices are reproduced in LICENSE.
*/
/*
* Deliver an ACPI GpioInt() from a Cherryview INT33FF GPIO controller to
* FreeBSD's existing atkbd instance. This is intentionally a small,
* machine-specific bridge: it claims only the GPIO controller referenced by
* a PNP0303/PNP030B keyboard resource and does not replace the atkbd driver.
*/
#include "opt_acpi.h"
#include <sys/param.h>
#include <sys/systm.h>
#include <sys/bus.h>
#include <sys/kernel.h>
#include <sys/kbio.h>
#include <sys/module.h>
#include <sys/mutex.h>
#include <sys/rman.h>
#include <machine/bus.h>
#include <machine/resource.h>
#include <contrib/dev/acpica/include/acpi.h>
#include <dev/acpica/acpivar.h>
#include <dev/kbd/kbdreg.h>
#include "acpi_if.h"
#include "bus_if.h"
#include "device_if.h"
#define CYAN_NINTR 16
#define CYAN_INTERRUPT_STATUS 0x0300
#define CYAN_INTERRUPT_MASK 0x0380
#define CYAN_PAD_CFG0 0x4400
#define CYAN_PAD_CFG0_INTSEL_MASK 0xf0000000U
#define CYAN_PAD_CFG0_INTSEL_SHIFT 28
#define CYAN_PAD_CFG0_GPIOCFG_MASK (7U << 8)
#define CYAN_PAD_CFG0_GPIOCFG_GPI (2U << 8)
#define CYAN_PAD_CFG1_INTWAKECFG_MASK 0x00000007U
#define CYAN_PAD_CFG1_INTWAKECFG_FALLING 0x00000001U
#define CYAN_PAD_CFG1_INTWAKECFG_RISING 0x00000002U
#define CYAN_PAD_CFG1_INTWAKECFG_BOTH 0x00000003U
#define CYAN_PAD_CFG1_INTWAKECFG_LEVEL 0x00000004U
#define CYAN_PAD_CFG1_INVRXTX_MASK 0x000000f0U
#define CYAN_PAD_CFG1_INVRXTX_RXDATA 0x00000040U
#define CYAN_LOCK(sc) mtx_lock_spin(&(sc)->mtx)
#define CYAN_UNLOCK(sc) mtx_unlock_spin(&(sc)->mtx)
/* Number of valid pads in each 15-pad register group, indexed by _UID. */
static const u_int cyan_sw_pins[] = { 8, 8, 8, 8, 8, 8, 8 };
static const u_int cyan_n_pins[] = { 9, 13, 12, 12, 13 };
static const u_int cyan_e_pins[] = { 12, 12 };
static const u_int cyan_se_pins[] = { 8, 12, 6, 8, 10, 11 };
struct cyan_gpio_intr {
ACPI_HANDLE consumer;
uint16_t pin;
uint8_t triggering;
uint8_t polarity;
};
struct cyan_find_ctx {
ACPI_HANDLE controller;
ACPI_HANDLE candidate;
struct cyan_gpio_intr intr;
bool found;
};
struct cyan_softc {
device_t dev;
ACPI_HANDLE handle;
struct mtx mtx;
int mem_rid;
struct resource *mem_res;
int irq_rid;
struct resource *irq_res;
void *irq_cookie;
const u_int *pins;
u_int ngroups;
struct cyan_gpio_intr gpio;
keyboard_t *kbd;
u_int line;
bool configured;
struct intr_config_hook config_hook;
bool hook_established;
};
static int cyan_probe(device_t dev);
static int cyan_attach(device_t dev);
static int cyan_detach(device_t dev);
static int cyan_suspend(device_t dev);
static int cyan_resume(device_t dev);
static void cyan_intr(void *arg);
static void cyan_config_hook(void *arg);
static char *cyan_hids[] = {
"INT33FF",
NULL
};
static bool
cyan_is_at_keyboard(ACPI_HANDLE handle)
{
return (acpi_MatchHid(handle, "PNP0303") !=
ACPI_MATCHHID_NOMATCH ||
acpi_MatchHid(handle, "PNP030B") !=
ACPI_MATCHHID_NOMATCH);
}
static ACPI_STATUS
cyan_find_resource(ACPI_RESOURCE *res, void *context)
{
struct cyan_find_ctx *ctx;
ACPI_RESOURCE_GPIO *gpio;
ACPI_HANDLE controller;
ACPI_STATUS status;
u_int i;
ctx = context;
if (res->Type != ACPI_RESOURCE_TYPE_GPIO)
return (AE_OK);
gpio = &res->Data.Gpio;
if (gpio->ConnectionType != ACPI_RESOURCE_GPIO_TYPE_INT ||
gpio->PinTable == NULL || gpio->PinTableLength == 0 ||
gpio->ResourceSource.StringPtr == NULL)
return (AE_OK);
status = AcpiGetHandle(ctx->candidate,
gpio->ResourceSource.StringPtr, &controller);
if (ACPI_FAILURE(status) || controller != ctx->controller)
return (AE_OK);
/* One interrupt pin is expected, but accepting the first is sufficient. */
for (i = 0; i < gpio->PinTableLength; i++) {
ctx->intr.consumer = ctx->candidate;
ctx->intr.pin = gpio->PinTable[i];
ctx->intr.triggering = gpio->Triggering;
ctx->intr.polarity = gpio->Polarity;
ctx->found = true;
return (AE_CTRL_TERMINATE);
}
return (AE_OK);
}
static ACPI_STATUS
cyan_find_device(ACPI_HANDLE handle, UINT32 level, void *context,
void **return_value)
{
struct cyan_find_ctx *ctx;
UINT32 sta;
(void)level;
(void)return_value;
ctx = context;
if (!ACPI_FAILURE(acpi_GetInteger(handle, "_STA", &sta)) &&
!ACPI_DEVICE_PRESENT(sta))
return (AE_OK);
if (!cyan_is_at_keyboard(handle))
return (AE_OK);
ctx->candidate = handle;
(void)AcpiWalkResources(handle, "_CRS", cyan_find_resource, ctx);
return (ctx->found ? AE_CTRL_TERMINATE : AE_OK);
}
static int
cyan_find_keyboard_gpio(ACPI_HANDLE controller, struct cyan_gpio_intr *intr)
{
struct cyan_find_ctx ctx;
ACPI_STATUS status;
bzero(&ctx, sizeof(ctx));
ctx.controller = controller;
status = AcpiWalkNamespace(ACPI_TYPE_DEVICE, ACPI_ROOT_OBJECT,
ACPI_UINT32_MAX, cyan_find_device, NULL, &ctx, NULL);
if (ctx.found) {
*intr = ctx.intr;
return (0);
}
return (ACPI_FAILURE(status) ? ENXIO : ENOENT);
}
static int
cyan_select_bank(struct cyan_softc *sc)
{
UINT32 uid;
ACPI_STATUS status;
status = acpi_GetInteger(sc->handle, "_UID", &uid);
if (ACPI_FAILURE(status))
return (ENXIO);
switch (uid) {
case 1:
sc->pins = cyan_sw_pins;
sc->ngroups = nitems(cyan_sw_pins);
break;
case 2:
sc->pins = cyan_n_pins;
sc->ngroups = nitems(cyan_n_pins);
break;
case 3:
sc->pins = cyan_e_pins;
sc->ngroups = nitems(cyan_e_pins);
break;
case 4:
sc->pins = cyan_se_pins;
sc->ngroups = nitems(cyan_se_pins);
break;
default:
return (ENXIO);
}
return (0);
}
static int
cyan_valid_pin(struct cyan_softc *sc, u_int pin)
{
u_int group;
group = pin / 15;
if (group >= sc->ngroups || pin % 15 >= sc->pins[group])
return (EINVAL);
return (0);
}
static bus_size_t
cyan_pad_cfg0_offset(u_int pin)
{
return (CYAN_PAD_CFG0 + 1024 * (pin / 15) + 8 * (pin % 15));
}
static uint32_t
cyan_read_pad_cfg0(struct cyan_softc *sc, u_int pin)
{
return (bus_read_4(sc->mem_res, cyan_pad_cfg0_offset(pin)));
}
static void
cyan_write_pad_cfg0(struct cyan_softc *sc, u_int pin, uint32_t value)
{
bus_write_4(sc->mem_res, cyan_pad_cfg0_offset(pin), value);
}
static uint32_t
cyan_read_pad_cfg1(struct cyan_softc *sc, u_int pin)
{
return (bus_read_4(sc->mem_res, cyan_pad_cfg0_offset(pin) + 4));
}
static void
cyan_write_pad_cfg1(struct cyan_softc *sc, u_int pin, uint32_t value)
{
bus_write_4(sc->mem_res, cyan_pad_cfg0_offset(pin) + 4, value);
}
static int
cyan_interrupt_config(const struct cyan_gpio_intr *intr, uint32_t *config)
{
uint32_t value;
value = 0;
switch (intr->triggering) {
case ACPI_LEVEL_SENSITIVE:
if (intr->polarity == ACPI_ACTIVE_LOW)
value = CYAN_PAD_CFG1_INTWAKECFG_LEVEL |
CYAN_PAD_CFG1_INVRXTX_RXDATA;
else if (intr->polarity == ACPI_ACTIVE_HIGH)
value = CYAN_PAD_CFG1_INTWAKECFG_LEVEL;
else
return (EINVAL);
break;
case ACPI_EDGE_SENSITIVE:
switch (intr->polarity) {
case ACPI_ACTIVE_LOW:
value = CYAN_PAD_CFG1_INTWAKECFG_FALLING;
break;
case ACPI_ACTIVE_HIGH:
value = CYAN_PAD_CFG1_INTWAKECFG_RISING;
break;
case ACPI_ACTIVE_BOTH:
value = CYAN_PAD_CFG1_INTWAKECFG_BOTH;
break;
default:
return (EINVAL);
}
break;
default:
return (EINVAL);
}
*config = value;
return (0);
}
/*
* Resolve the already-attached atkbd and program its GPIO route. Giant is
* acquired unconditionally here and released on the same lexical path.
*/
static int
cyan_configure(struct cyan_softc *sc)
{
device_t atkbd, atkbdc;
keyboard_t *kbd;
uint32_t config, line, reg;
int error, index, unit;
error = cyan_interrupt_config(&sc->gpio, &config);
if (error != 0)
return (error);
mtx_lock(&Giant);
atkbdc = acpi_get_device(sc->gpio.consumer);
if (atkbdc == NULL) {
error = ENXIO;
goto out;
}
atkbd = device_find_child(atkbdc, "atkbd", -1);
unit = atkbd != NULL ? device_get_unit(atkbd) :
device_get_unit(atkbdc);
index = kbd_find_keyboard("atkbd", unit);
if (index < 0 || (kbd = kbd_get_keyboard(index)) == NULL ||
!KBD_IS_CONFIGURED(kbd)) {
error = ENXIO;
goto out;
}
CYAN_LOCK(sc);
if (sc->configured) {
reg = bus_read_4(sc->mem_res, CYAN_INTERRUPT_MASK);
bus_write_4(sc->mem_res, CYAN_INTERRUPT_MASK,
reg & ~(1U << sc->line));
bus_write_4(sc->mem_res, CYAN_INTERRUPT_STATUS,
1U << sc->line);
}
reg = cyan_read_pad_cfg0(sc, sc->gpio.pin);
line = (reg & CYAN_PAD_CFG0_INTSEL_MASK) >>
CYAN_PAD_CFG0_INTSEL_SHIFT;
KASSERT(line < CYAN_NINTR, ("invalid interrupt line %u", line));
/* Preserve the firmware-selected interrupt line and select GPIO input. */
reg &= ~CYAN_PAD_CFG0_GPIOCFG_MASK;
reg |= CYAN_PAD_CFG0_GPIOCFG_GPI;
cyan_write_pad_cfg0(sc, sc->gpio.pin, reg);
reg = cyan_read_pad_cfg1(sc, sc->gpio.pin);
reg &= ~(CYAN_PAD_CFG1_INTWAKECFG_MASK |
CYAN_PAD_CFG1_INVRXTX_MASK);
reg |= config;
cyan_write_pad_cfg1(sc, sc->gpio.pin, reg);
/* Publish the callback before acknowledging and unmasking the line. */
sc->kbd = kbd;
sc->line = line;
sc->configured = true;
bus_write_4(sc->mem_res, CYAN_INTERRUPT_STATUS, 1U << line);
reg = bus_read_4(sc->mem_res, CYAN_INTERRUPT_MASK);
bus_write_4(sc->mem_res, CYAN_INTERRUPT_MASK,
reg | (1U << line));
CYAN_UNLOCK(sc);
error = 0;
out:
mtx_unlock(&Giant);
return (error);
}
static void
cyan_config_hook(void *arg)
{
struct cyan_softc *sc;
int error;
sc = arg;
error = cyan_configure(sc);
if (error != 0)
device_printf(sc->dev,
"unable to connect GPIO interrupt to atkbd: %d\n", error);
else
device_printf(sc->dev,
"keyboard GpioInt pin %u routed through line %u\n",
sc->gpio.pin, sc->line);
/* Keep this as the final operation; detach drains this hook. */
config_intrhook_disestablish(&sc->config_hook);
}
static int
cyan_probe(device_t dev)
{
struct cyan_gpio_intr intr;
int rv;
if (acpi_disabled("cyan_kbdgpio"))
return (ENXIO);
rv = ACPI_ID_PROBE(device_get_parent(dev), dev, cyan_hids, NULL);
if (rv > 0)
return (rv);
if (cyan_find_keyboard_gpio(acpi_get_handle(dev), &intr) != 0)
return (ENXIO);
device_set_desc(dev, "Cherryview keyboard GPIO interrupt bridge");
return (BUS_PROBE_SPECIFIC);
}
static int
cyan_attach(device_t dev)
{
struct cyan_softc *sc;
bus_size_t end;
int error;
sc = device_get_softc(dev);
sc->dev = dev;
sc->handle = acpi_get_handle(dev);
error = cyan_find_keyboard_gpio(sc->handle, &sc->gpio);
if (error != 0)
return (error);
error = cyan_select_bank(sc);
if (error != 0) {
device_printf(dev, "unsupported or missing _UID\n");
return (error);
}
if (cyan_valid_pin(sc, sc->gpio.pin) != 0) {
device_printf(dev, "invalid GpioInt pin %u\n", sc->gpio.pin);
return (EINVAL);
}
mtx_init(&sc->mtx, device_get_nameunit(dev), "cyan-kbdgpio",
MTX_SPIN);
sc->mem_rid = 0;
sc->mem_res = bus_alloc_resource_any(dev, SYS_RES_MEMORY,
&sc->mem_rid, RF_ACTIVE);
if (sc->mem_res == NULL) {
device_printf(dev, "cannot allocate memory resource\n");
error = ENOMEM;
goto fail_mtx;
}
end = cyan_pad_cfg0_offset(sc->gpio.pin) + 8;
if (end > rman_get_size(sc->mem_res)) {
device_printf(dev, "GpioInt pin register is outside resource\n");
error = EINVAL;
goto fail_mem;
}
/* Prevent a stale status bit from firing before atkbd is resolved. */
CYAN_LOCK(sc);
bus_write_4(sc->mem_res, CYAN_INTERRUPT_MASK, 0);
bus_write_4(sc->mem_res, CYAN_INTERRUPT_STATUS, 0xffffU);
CYAN_UNLOCK(sc);
sc->irq_rid = 0;
sc->irq_res = bus_alloc_resource_any(dev, SYS_RES_IRQ,
&sc->irq_rid, RF_ACTIVE);
if (sc->irq_res == NULL) {
device_printf(dev, "cannot allocate interrupt resource\n");
error = ENOMEM;
goto fail_mem;
}
/*
* Deliberately omit INTR_MPSAFE. The interrupt framework therefore
* holds Giant while cyan_intr() calls the legacy keyboard stack.
*/
error = bus_setup_intr(dev, sc->irq_res, INTR_TYPE_TTY, NULL,
cyan_intr, sc, &sc->irq_cookie);
if (error != 0) {
device_printf(dev, "cannot set up parent interrupt: %d\n", error);
goto fail_irq;
}
sc->config_hook.ich_func = cyan_config_hook;
sc->config_hook.ich_arg = sc;
error = config_intrhook_establish(&sc->config_hook);
if (error == 0)
sc->hook_established = true;
else {
/* A duplicate hook is impossible normally; retain a safe fallback. */
error = cyan_configure(sc);
if (error != 0)
device_printf(dev,
"unable to connect GPIO interrupt to atkbd: %d\n",
error);
}
return (0);
fail_irq:
bus_release_resource(dev, SYS_RES_IRQ, sc->irq_rid, sc->irq_res);
fail_mem:
bus_release_resource(dev, SYS_RES_MEMORY, sc->mem_rid, sc->mem_res);
fail_mtx:
mtx_destroy(&sc->mtx);
return (error);
}
static void
cyan_intr(void *arg)
{
struct cyan_softc *sc;
keyboard_t *kbd;
uint32_t pending, status;
sc = arg;
mtx_assert(&Giant, MA_OWNED);
CYAN_LOCK(sc);
status = bus_read_4(sc->mem_res, CYAN_INTERRUPT_STATUS);
pending = status & bus_read_4(sc->mem_res, CYAN_INTERRUPT_MASK);
if (status != 0)
bus_write_4(sc->mem_res, CYAN_INTERRUPT_STATUS, status);
if (sc->configured && (pending & (1U << sc->line)) != 0)
kbd = sc->kbd;
else
kbd = NULL;
CYAN_UNLOCK(sc);
/* The callback may enter the console/keyboard stack; do not hold spin. */
if (kbd != NULL)
kbdd_intr(kbd, NULL);
}
static int
cyan_suspend(device_t dev)
{
struct cyan_softc *sc;
uint32_t reg;
sc = device_get_softc(dev);
mtx_lock(&Giant);
CYAN_LOCK(sc);
if (sc->configured) {
reg = bus_read_4(sc->mem_res, CYAN_INTERRUPT_MASK);
bus_write_4(sc->mem_res, CYAN_INTERRUPT_MASK,
reg & ~(1U << sc->line));
bus_write_4(sc->mem_res, CYAN_INTERRUPT_STATUS,
1U << sc->line);
}
CYAN_UNLOCK(sc);
mtx_unlock(&Giant);
return (0);
}
static int
cyan_resume(device_t dev)
{
struct cyan_softc *sc;
int error;
sc = device_get_softc(dev);
error = cyan_configure(sc);
if (error != 0)
device_printf(dev, "cannot restore keyboard GpioInt: %d\n", error);
return (error);
}
static int
cyan_detach(device_t dev)
{
struct cyan_softc *sc;
sc = device_get_softc(dev);
if (sc->hook_established)
(void)config_intrhook_drain(&sc->config_hook);
/* Serialize state removal with the non-MPSAFE interrupt handler. */
mtx_lock(&Giant);
CYAN_LOCK(sc);
bus_write_4(sc->mem_res, CYAN_INTERRUPT_MASK, 0);
bus_write_4(sc->mem_res, CYAN_INTERRUPT_STATUS, 0xffffU);
sc->configured = false;
sc->kbd = NULL;
CYAN_UNLOCK(sc);
mtx_unlock(&Giant);
if (sc->irq_cookie != NULL)
bus_teardown_intr(dev, sc->irq_res, sc->irq_cookie);
if (sc->irq_res != NULL)
bus_release_resource(dev, SYS_RES_IRQ, sc->irq_rid,
sc->irq_res);
if (sc->mem_res != NULL)
bus_release_resource(dev, SYS_RES_MEMORY, sc->mem_rid,
sc->mem_res);
mtx_destroy(&sc->mtx);
return (0);
}
static device_method_t cyan_methods[] = {
DEVMETHOD(device_probe, cyan_probe),
DEVMETHOD(device_attach, cyan_attach),
DEVMETHOD(device_detach, cyan_detach),
DEVMETHOD(device_suspend, cyan_suspend),
DEVMETHOD(device_resume, cyan_resume),
DEVMETHOD_END
};
static driver_t cyan_driver = {
.name = "cyan_kbdgpio",
.methods = cyan_methods,
.size = sizeof(struct cyan_softc),
};
DRIVER_MODULE(cyan_kbdgpio, acpi, cyan_driver, NULL, NULL);
MODULE_DEPEND(cyan_kbdgpio, acpi, 1, 1, 1);
MODULE_VERSION(cyan_kbdgpio, 1);
KMOD= cyan_kbdgpio
SRCS= cyan_kbdgpio.c
SRCS+= acpi_if.h bus_if.h device_if.h opt_acpi.h opt_kbd.h
.include <bsd.kmod.mk>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment