Question: Is anyone able to advise on how to interleave operation of the RPi I2C hardware with general GPIO using libgpiod?
Summary: I'm experiencing some unexpected behaviour with RPi4, gpiod and /dev/i2c-1. Calls to write() on /dev/i2c-1 are successful as long as I do not use gpiod library before I open() the /dev/i2c device. I used strace. After using gpiod any calls to open() and ioctl() on /dev/i2c-1 succeed but a write() on /dev/i2c-1 will block for about 1 second then fail with ETIMEDOUT. Also the scope shows no activity on the SDA SCL lines. These symptoms are reproduced even if the gpiod usage took place in a process (program) that has already terminated.
Hardware: Raspberry Pi 4 Model B
debian_version: 11.3
os-release: 11 (bullseye)
Minimum Working Example:
Here is a minimal I2C application
#include <errno.h>
#include <error.h>
#include <fcntl.h>
#include <linux/i2c-dev.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <unistd.h>
int main(void)
{
int retval;
int fd;
const unsigned int i2c_addr = 5;
const unsigned char payload[] = { 0xAA, 0x55, 0xAA };
errno = 0;
fd = open("/dev/i2c-1", O_RDWR);
if(fd == -1){
error(1,errno,"Call to open(\"/dev/i2c-1\") returned -1 and set errno = %d", errno);
}
else{
fprintf(stdout,"i2c_app: Successful call of open(\"/dev/i2c-1\")\n");
}
errno = 0;
retval = ioctl(fd, I2C_SLAVE, i2c_addr);
if(retval == -1){
error(1,errno,"Call to ioctl(fd) returned -1 and set errno = %d", errno);
}
else{
fprintf(stdout,"i2c_app: Successful call of ioctl(fd)\n");
}
errno = 0;
retval = write(fd, &(payload[0]), sizeof(payload));
if(retval == -1){
error(1,errno,"Call to write(fd) returned -1 and set errno = %d", errno);
}
else{
if(retval != sizeof(payload)){
error(1,0,"Call to write(fd) failed. Expected = %zd Observed = %d", sizeof(payload), retval);
}
else{
fprintf(stdout,"i2c_app: Successful call of write(fd)\n");
}
}
errno = 0;
retval = close(fd);
if(retval == -1){
error(1,errno,"Call to close(fd) returned -1 and set errno = %d", errno);
}
else{
fprintf(stdout,"i2c_app: Successful call of close(fd)\n");
}
return 0;
}
Here is a minimal libgpiod application
int main(void)
{
int retval;
struct gpiod_chip * chip = NULL;
struct gpiod_line * gpio2 = NULL;
struct gpiod_line * gpio3 = NULL;
errno = 0;
chip = gpiod_chip_open("/dev/gpiochip0");
if(chip == NULL){
error(1,errno,"Call to gpiod_chip_open() returned NULL and set errno = %d", errno);
}
else{
fprintf(stdout,"gpio_app: Successful call of gpiod_chip_open(\"/dev/gpiochip0\")\n");
}
errno = 0;
gpio2 = gpiod_chip_get_line( chip, 2);
if(gpio2 == NULL){
error(1,errno,"Call to gpiod_chip_get_line(chip,2) returned NULL and set errno = %d", errno);
}
else{
fprintf(stdout,"gpio_app: Successful call of gpiod_chip_get_line(chip,2)\n");
}
errno = 0;
gpio3 = gpiod_chip_get_line( chip, 3);
if(gpio3 == NULL){
error(1,errno,"Call to gpiod_chip_get_line(chip,3) returned NULL and set errno = %d", errno);
}
else{
fprintf(stdout,"gpio_app: Successful call of gpiod_chip_get_line(chip,3)\n");
}
errno = 0;
retval = gpiod_line_request_output(gpio2, "gpio_app", 1);
if(retval == -1){
error(1,errno,"Call to gpiod_line_request_output(gpio3) returned -1 and set errno = %d", errno);
}
else{
fprintf(stdout,"gpio_app: Successful call of gpiod_line_request_output(gpio2)\n");
}
errno = 0;
retval = gpiod_line_request_output(gpio3, "gpio_app", 1);
if(retval == -1){
error(1,errno,"Call to gpiod_line_request_output(gpio3) returned -1 and set errno = %d", errno);
}
else{
fprintf(stdout,"gpio_app: Successful call of gpiod_line_request_output(gpio3)\n");
}
errno = 0;
retval = gpiod_line_set_value(gpio2, 1);
if(retval == -1){
error(1,errno,"Call to gpiod_line_set_value(gpio2) returned -1 and set errno = %d", errno);
}
else{
fprintf(stdout,"gpio_app: Successful call of gpiod_line_set_value(gpio2)\n");
}
errno = 0;
retval = gpiod_line_set_value(gpio3, 1);
if(retval == -1){
error(1,errno,"Call to gpiod_line_set_value(gpio3) returned -1 and set errno = %d", errno);
}
else{
fprintf(stdout,"gpio_app: Successful call of gpiod_line_set_value(gpio3)\n");
}
if(gpio2 != NULL){
gpiod_line_release(gpio2);
fprintf(stdout,"gpio_app: Called gpiod_line_release(gpio2)\n");
}
if(gpio3 != NULL){
gpiod_line_release(gpio3);
fprintf(stdout,"gpio_app: Called gpiod_line_release(gpio3)\n");
}
if(chip != NULL){
gpiod_chip_close(chip);
fprintf(stdout,"gpio_app: Called gpiod_chip_close(chip)\n");
}
return 0;
}
Here's a script to build them
#!/usr/bin/env sh
gcc -Wall -Werror -Wextra -Wno-misleading-indentation -o i2c_app i2c_app.c
gcc -Wall -Werror -Wextra -Wno-misleading-indentation -o gpio_app gpio_app.c -lgpiod
To install, create an empty directory, copy and paste the code as i2c_app.c and gpio_app.c, and run the build script. Use raspi-config to enable the I2C interface as documented at Raspberry Pi Documentation
To reproduce the problem (a) start with a freshly rebooted RPi4 (b) run gpio_app and ensure it reports no errors (c) run i2c_app and observe the failed call to write() with errno=110 "Connection timed out". Running the apps under strace shows more detail on the failure.
Research: The only related post I found was Trouble using spidev after RPi.GPIO but I didn't find it helpful.
Hypothesis: My program calls gpiod. It feels like the gpiod library is holding kernel resources open after my program has terminated. I'm hoping that turns out to be misapplication by me as this is my first experience with libgpiod.
Workaround: The workaround is to reboot the RPi4, perform the /dev/i2c-1 operations, then perform the gpiod operations, and to refrain from using /dev/i2c-1 until after the next reboot. A better workaround to somehow reset libgpiod would be great.
Solutions: Use gpiod only and create a bit-bang I2C implementation on top of gpiod. This would be a bad case of reinventing the wheel.
Background: My system comprises an RPi4, a device under test (DUT) and some software I've written in C running on the Rpi4. The RPi4 provides GND, 3.3V and there are 8 GPIO connected to the DUT. The DUT has two operating modes, mode #1 and mode #2. After power up the DUT is in mode #1 and it is an I2C slave on two of the GPIO lines. A command in mode #1 moves the DUT to mode #2. In mode #2 the DUT is controlled by a non-standard protocol on the 8 GPIO lines.
On the RPi4 I use /dev/i2c-1 to talk to the DUT in mode #1. For mode #2 I use gpiod aka libgpiod to implement the 8 GPIO protocol.
RPi4 controlling DUT in mode #1 over I2C works fine.
Sending the command to switch DUT from mode #1 to mode #2 works fine.
RPi4 controlling DUT in mode #2 over 8 GPIO lines works fine.
Cycling the power on DUT (to put it back in mode #1) and attempting to communicate over I2C fails as described in the summary above.
Cheers,
Colin.