moGPIO provides straight forward plug & play GPIO via USB
A lot of USB-to-GPIO adapters exist but I found none that is really plug & play, let alone standard libgpiod API compatible.
access GPIOs using:
- standard libgpiod API (usbio)
- interactive terminal (virtual serial port)
- reading/writing files on emulated mass storage device
Copy
- mogpio-intern-pico.uf2 to your pico.
- mogpio-intern-pico2.uf2 to your pico2
(s. latest release)
There are three ways moGPIO exposes GPIOs:
moGPIO will show up as /dev/gpiochipX1 once you plug it in, ready to use. It's compatible to most applications that build upon libgpiod. (e.g. https://docs.kernel.org/driver-api/gpio/drivers-on-gpio.html).
moGPIO uses the usbio driver that should come with any linux kernel >=6.18.x
- plug into USB port
- moGPIO will register as new
/dev/gpiochipXusing theusbiodriver. - Use commandline2 tools
gpiodetect,gpioinfo,gpioget,gpiosetor the libgpiod API bindings from the language of your choice
Example: $ gpioset /dev/gpiochip1 25=1
moGPIO provides a serial device you can connect to access a terminal. Available commands:
?orhelpprint usage informationlistto list configured GPIOs and their stateread <bank>:<pin>to read a GPIO input pin valuewrite <bank>:<pin> <0|1>to set a GPIO output pin valueconfig <bank>:<pin> <function> [<mode>]to configure a pin
The terminal uses microrl-remaster and provides
- line editing
- command history
- tab completion
Example: $ echo "write 0:25 1" > /dev/ttyACM0
moGPIO provides a small FAT16 partition with PINS.TXT and CONFIG.TXT files.
The files contents represent the state at time of read. Writes will affect the state on actual write. (Some OS' cache agressively and won't write until eject/unmount or explicit flush.)
Format:
<bank>:<pin>=<value>
Example:
0:0=0
0:1=0
0:2=0
0:3=0
0:4=0
0:5=0
0:6=0
0:7=0
Format:
<bank>:<pin>=<function>,<mode>
-
functions
-
IN(input pin) -
OUT(putput pin) -
modes
-
UP(pull up resistor) -
DOWN(pull down resistor) -
PUSHPULL(push-pull mode) -
DEFAULT(don't change default)
Example:
0:0=IN,DOWN
0:1=IN,DOWN
0:2=IN,DOWN
0:3=IN,DOWN
0:4=IN,DOWN
0:5=IN,DOWN
0:6=IN,DOWN
0:7=IN,DOWN
0:8=OUT,PUSHPULL
$ echo 0:25=1 > /mnt/moGPIO/PINS.TXT
$ sync
It should be easily possible to port moGPIO to other platforms or add different GPIO hardware layouts.
Currently there are 3 layouts:
- intern (internal GPIOs of platform)
- sipo-piso (bit-bang serial shift registers)
- intern-sipo-piso (combination of the above)
A new layout/driver can be implemented easily:
- create
driver/yourdriver.cto access the GPIOs and implement the following functions and exports them using ahal_gpio_driver_ops_tstructure:
static int init(void *vctx)
{
// init hardware ...
return HAL_GPIO_OK;
}
static int deinit(void *vctx)
{
// deinit hardware ...
return HAL_GPIO_OK;
}
static int set_function(void *vctx, size_t pin, hal_gpio_function_t function)
{
// set GPIO pin function (input/output/...)
return HAL_GPIO_OK;
}
static int set_mode(void *vctx, size_t pin, hal_gpio_mode_t mode)
{
// set GPIO pin mode (pull-up/down, ...)
return HAL_GPIO_OK;
}
static int get_function(void *vctx, size_t pin, hal_gpio_function_t *function)
{
// get current function of pin
return HAL_GPIO_OK;
}
static int get_mode(void *vctx, size_t pin, hal_gpio_mode_t *mode)
{
// get current mode of pin
return HAL_GPIO_OK;
}
static int read(void *vctx, size_t pin, bool *value)
{
// read value from (input) pin
return HAL_GPIO_OK;
}
static int write(void *vctx, size_t pin, bool value)
{
// write value to output pin or
return HAL_GPIO_ERR_UNSUPPORTED;
}
const hal_gpio_driver_ops_t hal_gpio_yourdriver_ops = {
.init = init,
.deinit = deinit,
.read = read,
.write = write,
.set_function = set_function,
.set_mode = set_mode,
.get_function = get_function,
.get_mode = get_mode
};- create a new
layouts/yourlayout.cor add your driver to an existing one:
static hal_gpio_yourdriver_ctx_t s_yourdriver_ctx = {
// holds stuff for this instance of your driver
}
/* GPIO hardware driver layout */
static const hal_gpio_driver_t s_drivers[] = {
{
.ops = &hal_gpio_yourdriver_ops, // yourdriver.c driver functions
.ctx = &s_yourdriver_ctx, // driver context
.pin_count = 16, // pin 0-15
},
...
};
/*
* bank/pin layout - this can be set arbitrary. Pins are mapped to driver pins
* in sequential order
*/
static const hal_gpio_bank_t s_banks[] = {
// ... other drivers before yours
{
.bank_id = 0,
.name = "yourdriver-chain",
.pin_count = 16,
},
// ... other drivers after yours
};
/* this layout */
const hal_gpio_layout_t g_hal_gpio_layout = {
.drivers = s_drivers,
.banks = s_banks,
.driver_count = sizeof(s_drivers) / sizeof(s_drivers[0]),
.bank_count = sizeof(s_banks) / sizeof(s_banks[0]),
};-
add your driver to
target_sources()in all supportedcmake/*.cmakeplatform files -
add your layout as
elseif(LAYOUT STREQUAL "yourlayout")to compile necessary drivers
- ESP32 port
- tests
- configure HAL dynamically + non-volatile config? Should be possible to compile all drivers, load them at runtime, configure pins where hardware is connected and store config in flash.
- support interrupts
- configure watching for rising/falling/both edges and generate CDC output
- onboard minimal documentation/URL for MSC (README.TXT)
- optimization
- use PIO for SIPO/PISO
- use separate core for USB?
- proper pin bit-mapping (usbio pinmask, first/last gpio etc.)
- would be nice but not even the usbio linux driver implements the protocol
Footnotes
-
As the time of writing, the usbio protocol supports 5 * 32 GPIO max per device. Any more pins can be controlled only with terminal or mass storage mode. Also limiting to 32 banks is a memory footprint compromise and can be increased in the future. (Please open an issue if you need more). ↩
-
gpiomoncommand and libgpiod events will fail as interrupts are not supported in the usbio driver (yet). ↩