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.