Understanding the Raspberry Pi – FreeBSD gpio device

Recently I found myself wanting to understand how I2C for FreeBSD ARM, on the Pi works.  The first step is to understand the GPIO implementation on Pi, since on a Pi, I2C is implemented on the GPIO.

So, here's the basic summary.

Firstly, the FreeBSD source browser is here.

In the directory /sys/ is the kernel code for all platforms, and in /sys/sys/ is the core kernel code.  In /sys/sys/gpio.h, you'll find the header which declares the GPIO API for FreeBSD, which must be supported by all FreeBSD GPIO devices on all platforms.

Here's the important parts:

/* GPIO pin states */
#define GPIO_PIN_LOW		0x00	/* low level (logical 0) */
#define GPIO_PIN_HIGH		0x01	/* high level (logical 1) */

/* Max name length of a pin */
#define GPIOMAXNAME		64

/* GPIO pin configuration flags */
#define GPIO_PIN_INPUT		0x0001	/* input direction */
#define GPIO_PIN_OUTPUT		0x0002	/* output direction */
#define GPIO_PIN_OPENDRAIN	0x0004	/* open-drain output */
#define GPIO_PIN_PUSHPULL	0x0008	/* push-pull output */
#define GPIO_PIN_TRISTATE	0x0010	/* output disabled */
#define GPIO_PIN_PULLUP		0x0020	/* internal pull-up enabled */
#define GPIO_PIN_PULLDOWN	0x0040	/* internal pull-down enabled */
#define GPIO_PIN_INVIN		0x0080	/* invert input */
#define GPIO_PIN_INVOUT		0x0100	/* invert output */
#define GPIO_PIN_PULSATE	0x0200	/* pulsate in hardware */

struct gpio_pin {
	uint32_t gp_pin;			/* pin number */
	char gp_name[GPIOMAXNAME];		/* human-readable name */
	uint32_t gp_caps;			/* capabilities */
	uint32_t gp_flags;			/* current flags */
};

/* GPIO pin request (read/write/toggle) */
struct gpio_req {
	uint32_t gp_pin;			/* pin number */
	uint32_t gp_value;			/* value */
};

/*
 * ioctls
 */
#define GPIOMAXPIN		_IOR('G', 0, int)
#define	GPIOGETCONFIG		_IOWR('G', 1, struct gpio_pin)
#define	GPIOSETCONFIG		_IOW('G', 2, struct gpio_pin)
#define	GPIOGET			_IOWR('G', 3, struct gpio_req)
#define	GPIOSET			_IOW('G', 4, struct gpio_req)
#define	GPIOTOGGLE		_IOWR('G', 5, struct gpio_req)

So, this defines what a GPIO device looks like to FreeBSD and the ioctls it supports, on all platforms.

For the ARM kernel, the source code is in /sys/arm/, and for Raspberry Pi, which based on a Broadcom BCM 2835 chip the kernel sources are at /sys/arm/broadcom/bcm2835/.

In /sys/arm/broadcom/bcm2835/bcm2835_gpio.c, you'll find the implementation which exposes the GPIO device, for a Raspberry Pi.  You should notice that the header "gpio_if.h" is included.  This is a generated file, and the source is in /sys/dev/gpio_if.m.   The important parts of this are here:

INTERFACE gpio;

#
# Get total number of pins
#
METHOD int pin_max {
	device_t dev;
	int *npins;
};

#
# Set value of pin specifed by pin_num 
#
METHOD int pin_set {
	device_t dev;
	uint32_t pin_num;
	uint32_t pin_value;
};

#
# Get value of pin specifed by pin_num 
#
METHOD int pin_get {
	device_t dev;
	uint32_t pin_num;
	uint32_t *pin_value;
};

#
# Toggle value of pin specifed by pin_num 
#
METHOD int pin_toggle {
	device_t dev;
	uint32_t pin_num;
};

#
# Get pin capabilities
#
METHOD int pin_getcaps {
	device_t dev;
	uint32_t pin_num;
	uint32_t *caps;
};

#
# Get pin flags
#
METHOD int pin_getflags {
	device_t dev;
	uint32_t pin_num;
	uint32_t *flags;
};

#
# Get pin name
#
METHOD int pin_getname {
	device_t dev;
	uint32_t pin_num;
	char *name;
};

#
# Set current configuration and capabilities
#
METHOD int pin_setflags {
	device_t dev;
	uint32_t pin_num;
	uint32_t flags;
};

An important piece of code to notice in "bcm2835_gpio.c" is the device driver method "probe":

static int
bcm_gpio_probe(device_t dev)
{
	if (!ofw_bus_is_compatible(dev, "broadcom,bcm2835-gpio"))
		return (ENXIO);

	device_set_desc(dev, "BCM2708/2835 GPIO controller");
	return (BUS_PROBE_DEFAULT);
}

This is the method FreeBSD calls to ask the driver if a specific piece of hardware is appropriate for that driver.  The ARM GPIO driver looks for "broadcom,bcm2835-gpio", which is the exact name defined in the Raspberry Pi FDT.

Another method which  if the "attach" method, which initializes the driver and adds it to the bus.  This line gets a "bcm_gpio_softc" struct from the device_t passed by the kernel.  The method "device_get_softc" is forward declared in /sys/sys/bus.h, and implemented in /sys/kern/subr_bus.c

struct bcm_gpio_softc *sc = device_get_softc(dev);

This method initializes a mutex for the driver:

mtx_init(&sc->sc_mtx, "bcm gpio", "gpio", MTX_DEF);

These macros

#define	BCM_GPIO_LOCK(_sc)	mtx_lock(&_sc->sc_mtx)
#define	BCM_GPIO_UNLOCK(_sc)	mtx_unlock(&_sc->sc_mtx)
#define	BCM_GPIO_LOCK_ASSERT(_sc)	mtx_assert(&_sc->sc_mtx, MA_OWNED)

Are used to serialize access in certain methods in the driver.

These lines allocate memory and IRQs for the driver, as specified in the FTD:

rid = 0;
	sc->sc_mem_res = bus_alloc_resource_any(dev, SYS_RES_MEMORY, &rid,
	    RF_ACTIVE);
	if (!sc->sc_mem_res) {
		device_printf(dev, "cannot allocate memory window\n");
		return (ENXIO);
	}

	sc->sc_bst = rman_get_bustag(sc->sc_mem_res);
	sc->sc_bsh = rman_get_bushandle(sc->sc_mem_res);

	rid = 0;

 

sc->sc_irq_res = bus_alloc_resource_any(dev, SYS_RES_IRQ, &rid,
	    RF_ACTIVE);
	if (!sc->sc_irq_res) {
		bus_release_resource(dev, SYS_RES_MEMORY, 0, sc->sc_mem_res);
		device_printf(dev, "cannot allocate interrupt\n");
		return (ENXIO);
	}

These lines query the Open Firmware and verify the that device is indeed a GPIO controller, according to the FTD:

/* Find our node. */
gpio = ofw_bus_get_node(sc->sc_dev);
if (!OF_hasprop(gpio, "gpio-controller"))
	/* Node is not a GPIO controller. */
	goto fail;

These lines set up the initial state of the GPIO controller pins:

	if (bcm_gpio_get_reserved_pins(sc) == -1)
		goto fail;

	/* Initialize the software controlled pins. */
	for (i = 0, j = 0; j < BCM_GPIO_PINS - 1; j++) {
		if (bcm_gpio_pin_is_ro(sc, j))
			continue;
		snprintf(sc->sc_gpio_pins[i].gp_name, GPIOMAXNAME,
		    "pin %d", j);
		func = bcm_gpio_get_function(sc, j);
		sc->sc_gpio_pins[i].gp_pin = j;
		sc->sc_gpio_pins[i].gp_caps = BCM_GPIO_DEFAULT_CAPS;
		sc->sc_gpio_pins[i].gp_flags = bcm_gpio_func_flag(func);
		i++;
	}
	sc->sc_gpio_npins = i;

	bcm_gpio_sysctl_init(sc);

Finally, these lines add two devices "gpioc" and "gpiocbus", and then attach the dev to the main bus

device_add_child(dev, "gpioc", device_get_unit(dev));
device_add_child(dev, "gpiobus", device_get_unit(dev));
return (bus_generic_attach(dev));

The function "device_add_child" is implemented in /sys/kern/subr_bus.c.  The devices "gpio" and "gpioc" are generic devices, which work for all architectures, and they are in:

/sys/dev/gpio/gpioc.c and /sys/dev/gpio/gpiobus.c.  The reason the kernel is able to find them by their device class names "gpio" and "gpiobus" is because they are registered with the kernel via these lines of code in gpioc.c and gpiobus.c.

driver_t gpioc_driver = {
	"gpioc",
	gpioc_methods,
	sizeof(struct gpioc_softc)
};

devclass_t	gpioc_devclass;

DRIVER_MODULE(gpioc, gpio, gpioc_driver, gpioc_devclass, 0, 0);
MODULE_VERSION(gpioc, 1);
driver_t gpiobus_driver = {
	"gpiobus",
	gpiobus_methods,
	sizeof(struct gpiobus_softc)
};

devclass_t	gpiobus_devclass;

DRIVER_MODULE(gpiobus, gpio, gpiobus_driver, gpiobus_devclass, 0, 0);
MODULE_VERSION(gpiobus, 1);

The gpio driver which will finally appear on the Pi as "/dev/gpio0", includes the header system header "/sys/gpio.h" and implements all its methods via that API, which ultimately comes back to /sys/arm/broadcom/bcm2835/bcm2835_gpio.c.

 

Leave a Reply