In Bookworm (and latest Bullseye) the kernel device driver for GPIO has been updated so GPIO settings are **persistent** and it is now possible to specify **pulls**.  
This means `libgpiod` behaves similarly to `sysfs` (and most tools accessing registers).

There is kernel documentation and a python module `gpiod` but it is difficult to follow (unless you are a kernel developer) and no User Documentation.

There are a few examples e.g. https://www.tomshardware.com/how-to/control-raspberry-pi-5-gpio-with-python-3

The current libgpiod is old (v1.6.3 in Bookworm and even older in Bullseye) the v2 code is significantly different but it unclear if or when this may be included in *Raspberry Pi OS*.

I decided to test the `gpiod` module and write a wrapper which uses the same functions as my `pi_gpio` library.  

This obviously excludes *I²C*, *SPI* or *Hardware PWM* but I have code implementing these using kernel drivers which will be included in a final library as will *Information* functions. 

This code has been updated and tested on a Pi5 and allows existing code to run with minimal change, but if writing new code use `gpiod` or `lgpio` (which includes additional functionality).

```
#! /usr/bin/env python3
"""
p_gpio is a Python module for the RaspberryPi
Using gpiod Python Interface
This is a wrapper which uses the same functions as pi_gpio library.

OVERVIEW

  *GPIO*
    setup_gpio - Set gpio as an input or an output

    input_gpio - Returns the GPIO level__plibrary

    output_gpio - Output to a GPIO channel

    input_28 - Returns value of GPIO 0-27

    output_28 - Sets value of GPIO 0-27

    gpio_function - The current GPIO mode

    get_pullupdn - The current GPIO pull/up down

"""
# 2024-04-04
import gpiod

VERSION='0.2'

chip = gpiod.find_line('ID_SDA').owner()  # find chip with ID_SDA

gpiomap = {} # This is a dictionary to avoid repeated chip.get_line(gpio) calls

#  GPIO
#    NOTE all gpio use Broadcom BCM numbers

INPUT=0
OUTPUT=1
#     LINE_REQ_DIR_AS_IS = 1
#     LINE_REQ_DIR_IN = 2
#     LINE_REQ_DIR_OUT = 3
#     LINE_REQ_EV_BOTH_EDGES = 6
#     LINE_REQ_EV_FALLING_EDGE = 4
#     LINE_REQ_EV_RISING_EDGE = 5
#     LINE_REQ_FLAG_ACTIVE_LOW = 4
#     LINE_REQ_FLAG_BIAS_DISABLE = 8
#     LINE_REQ_FLAG_BIAS_PULL_DOWN = 16
#     LINE_REQ_FLAG_BIAS_PULL_UP = 32
#     LINE_REQ_FLAG_OPEN_DRAIN = 1
#     LINE_REQ_FLAG_OPEN_SOURCE = 2

#     ACTIVE_HIGH = 1
#     ACTIVE_LOW = 2
#     BIAS_AS_IS = 1
#     BIAS_DISABLE = 2
#     BIAS_PULL_DOWN = 4
#     BIAS_PULL_UP = 3
#     DIRECTION_INPUT = 1
#     DIRECTION_OUTPUT = 2

def setup_gpio(gpio, direction, pud):
# def setup_gpio(gpio, direction, pud=0):  # optional pud
  """
  Set gpio as an input or an output
  direction: 0=IN, 1=OUT
  pud: 0=None 1=Up 2=Down
  """
  line = chip.get_line(gpio)
  gpiomap[gpio] = line
  if direction:
    line.request(consumer="p_gpio", type=gpiod.LINE_REQ_DIR_OUT)
  else:
    f = gpiod.LINE_REQ_FLAG_BIAS_DISABLE
    if pud==1:
      f = gpiod.LINE_REQ_FLAG_BIAS_PULL_UP
    if pud==2:
      f = gpiod.LINE_REQ_FLAG_BIAS_PULL_DOWN
    line.request("p_gpio", gpiod.LINE_REQ_DIR_IN, f)
    print('pud= ', pud, 'bias= ', line.bias())

def input_gpio(gpio):
  """
  Input from a GPIO channel.
  Returns HIGH=1=True or LOW=0=False
  """
  return gpiomap[gpio].get_value()

def output_gpio(gpio, value):
  """
  Output to a GPIO channel.
  value - 0/1 or False/True or LOW/HIGH
  """
  gpiomap[gpio].set_value(value)

def gpio_function(gpio):
  """
  Returns the current GPIO direction
  Only works if gpio in use
  Returns 0,1 (IN, OUT)
  """
  if gpio in gpiomap:
    return gpiomap[gpio].direction() - 1
  return 0

def get_pullupdn(gpio):
  """
  Return the current GPIO pull
  Only works if gpio in use
  Returns
  0:None/Unknown
  1:Up
  2:Down
  """
  if gpio in gpiomap:
    return gpiomap[gpio].bias()-2
  return 0

 #  Dummy functions
def setup():
  pass

def cleanup():
  pass

#------------- 
import time

def main():
  global chip
  SigOUT = 12
  SigIN = 13
  LOOPS = 20000
  setup_gpio(SigOUT, 1, 0)
#   setup_gpio(SigIN, 0, 0)
#   setup_gpio(SigIN, 0, 1)
  setup_gpio(SigIN, 0, 2)

  t0 = time.time()

  for i in range(LOOPS):
    output_gpio(SigOUT, 1)
    output_gpio(SigOUT, 0)

  t1 = time.time()

  print("gpiod Python\t{:>10.0f} toggles per second".format((1.0 * LOOPS) / (t1 - t0)))
  output_gpio(SigOUT, 1)
  print("{}".format(input_gpio(SigIN)))
  print('SigIN function= ', gpio_function(SigIN))
  print('SigOUT function= ', gpio_function(SigOUT))
  print('pud= ', get_pullupdn(SigIN))
  print('pud= ', get_pullupdn(4))

if __name__ == '__main__':
	main()
```


----------
`gpiod` V2 can be installed with `pip` in a Python `venv` and is much improved (but NOT backward compatible with V1).