Skip to content

Commit 6be3fb5

Browse files
committed
driver: add FTDI GPIO bit-bang output driver
Add FTDIGPIODriver and a labgrid agent for FTDI asynchronous bit-bang GPIO outputs. The agent opens the selected USB interface per operation, sets async bit-bang mode with the data bus configured as output, reads the current pin byte, and writes back only the requested bit change. Signed-off-by: Ozan Durgut <ozan.durgut@analog.com>
1 parent 7df9225 commit 6be3fb5

3 files changed

Lines changed: 251 additions & 0 deletions

File tree

labgrid/driver/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from .usbstoragedriver import USBStorageDriver, Mode
2929
from .resetdriver import DigitalOutputResetDriver
3030
from .gpiodriver import GpioDigitalOutputDriver
31+
from .ftdigpiodriver import FTDIGPIODriver
3132
from .filedigitaloutput import FileDigitalOutputDriver
3233
from .serialdigitaloutput import SerialPortDigitalOutputDriver
3334
from .xenadriver import XenaDriver

labgrid/driver/ftdigpiodriver.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# SPDX-License-Identifier: GPL-2.0-or-later
2+
"""FTDI GPIO driver using a labgrid agent."""
3+
4+
import threading
5+
6+
import attr
7+
8+
from ..factory import target_factory
9+
from ..protocol import DigitalOutputProtocol
10+
from ..resource.remote import NetworkFTDIGPIO, RemoteUSBResource
11+
from ..step import step
12+
from ..util.agentwrapper import AgentWrapper
13+
from .common import Driver
14+
15+
_shared_agents = {}
16+
_shared_lock = threading.Lock()
17+
18+
19+
def _acquire_agent(host, busnum, devnum, interface):
20+
key = (host, busnum, devnum, interface)
21+
with _shared_lock:
22+
entry = _shared_agents.get(key)
23+
if entry is None:
24+
wrapper = AgentWrapper(host)
25+
proxy = wrapper.load("ftdigpio")
26+
entry = {"wrapper": wrapper, "proxy": proxy, "refs": 0}
27+
_shared_agents[key] = entry
28+
entry["refs"] += 1
29+
return entry["proxy"]
30+
31+
32+
def _release_agent(host, busnum, devnum, interface):
33+
key = (host, busnum, devnum, interface)
34+
with _shared_lock:
35+
entry = _shared_agents.get(key)
36+
if entry is None:
37+
return
38+
entry["refs"] -= 1
39+
if entry["refs"] <= 0:
40+
del _shared_agents[key]
41+
try:
42+
entry["proxy"].close()
43+
finally:
44+
entry["wrapper"].close()
45+
46+
47+
@target_factory.reg_driver
48+
@attr.s(eq=False)
49+
class FTDIGPIODriver(Driver, DigitalOutputProtocol):
50+
"""Control one FTDI data-bus GPIO line through a labgrid agent."""
51+
52+
bindings = {
53+
"gpio": {"FTDIGPIO", NetworkFTDIGPIO},
54+
"networkservice": {"NetworkService", None},
55+
}
56+
57+
def __attrs_post_init__(self):
58+
super().__attrs_post_init__()
59+
self._proxy = None
60+
self._host = None
61+
62+
def on_activate(self):
63+
self._host = self.gpio.host if isinstance(self.gpio, RemoteUSBResource) else None
64+
if self.networkservice and self.networkservice.address == self._host:
65+
self._host = f"{self.networkservice.username}@{self._host}"
66+
self._proxy = _acquire_agent(self._host, self.gpio.busnum, self.gpio.devnum, self.gpio.interface)
67+
68+
def on_deactivate(self):
69+
self._proxy = None
70+
_release_agent(self._host, self.gpio.busnum, self.gpio.devnum, self.gpio.interface)
71+
self._host = None
72+
73+
@Driver.check_active
74+
@step(result=True)
75+
def get(self):
76+
status = bool(self._proxy.get(
77+
self.gpio.vendor_id,
78+
self.gpio.model_id,
79+
self.gpio.busnum,
80+
self.gpio.devnum,
81+
self.gpio.interface,
82+
self.gpio.index,
83+
))
84+
if self.gpio.invert:
85+
status = not status
86+
return status
87+
88+
@Driver.check_active
89+
@step(args=["status"])
90+
def set(self, status):
91+
if self.gpio.invert:
92+
status = not status
93+
self._proxy.set(
94+
self.gpio.vendor_id,
95+
self.gpio.model_id,
96+
self.gpio.busnum,
97+
self.gpio.devnum,
98+
self.gpio.interface,
99+
self.gpio.index,
100+
bool(status),
101+
)

labgrid/util/agents/ftdigpio.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# SPDX-License-Identifier: GPL-2.0-or-later
2+
"""Agent for controlling FTDI data-bus GPIOs via bit-bang mode."""
3+
4+
import threading
5+
6+
import usb.core
7+
import usb.util
8+
9+
SIO_SET_BITMODE = 11
10+
SIO_READ_PINS = 12
11+
BITMODE_ASYNC_BITBANG = 1
12+
OUT_REQTYPE = 0x40
13+
IN_REQTYPE = 0xC0
14+
15+
USB_TIMEOUT = 1000
16+
GPIO_MASK = 0xFF
17+
SUPPORTED_DEVICES = {
18+
0x6010: 2, # FT2232C/D/H Dual UART/FIFO IC
19+
0x6011: 4, # FT4232H Quad UART/MPSSE IC
20+
0x6014: 1, # FT232HL/Q
21+
}
22+
23+
24+
class FTDIGPIO:
25+
def __init__(self, vendor_id, model_id, busnum, devnum, interface):
26+
self._validate_device(vendor_id, model_id, interface)
27+
self._interface = interface - 1
28+
self._index = interface
29+
self._lock = threading.Lock()
30+
31+
self._dev = self._find_device(vendor_id, model_id, busnum, devnum)
32+
self._detach_kernel_driver()
33+
try:
34+
cfg = self._dev.get_active_configuration()
35+
except usb.core.USBError:
36+
self._dev.set_configuration()
37+
self._detach_kernel_driver()
38+
cfg = self._dev.get_active_configuration()
39+
40+
intf = cfg[(self._interface, 0)]
41+
usb.util.claim_interface(self._dev, self._interface)
42+
self._ep_out = usb.util.find_descriptor(
43+
intf,
44+
custom_match=lambda ep: usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_OUT,
45+
)
46+
if self._ep_out is None:
47+
raise ValueError("FTDI output endpoint not found")
48+
49+
def close(self):
50+
try:
51+
try:
52+
usb.util.release_interface(self._dev, self._interface)
53+
except usb.core.USBError:
54+
pass
55+
finally:
56+
usb.util.dispose_resources(self._dev)
57+
58+
def _detach_kernel_driver(self):
59+
if self._dev.is_kernel_driver_active(self._interface):
60+
self._dev.detach_kernel_driver(self._interface)
61+
62+
@staticmethod
63+
def _validate_device(vendor_id, model_id, interface):
64+
if vendor_id != 0x0403 or model_id not in SUPPORTED_DEVICES:
65+
raise ValueError("Unsupported FTDI GPIO device")
66+
if not 1 <= interface <= SUPPORTED_DEVICES[model_id]:
67+
raise ValueError("FTDI GPIO interface is not supported by this device")
68+
69+
@staticmethod
70+
def _find_device(vendor_id, model_id, busnum, devnum):
71+
for dev in usb.core.find(find_all=True, idVendor=vendor_id, idProduct=model_id):
72+
if dev.bus == busnum and dev.address == devnum:
73+
return dev
74+
raise ValueError("FTDI device not found")
75+
76+
def _ctrl_out(self, request, value):
77+
self._dev.ctrl_transfer(OUT_REQTYPE, request, value, self._index, None, USB_TIMEOUT)
78+
79+
def _ctrl_in(self, request, value, length):
80+
return bytes(self._dev.ctrl_transfer(IN_REQTYPE, request, value, self._index, length, USB_TIMEOUT))
81+
82+
def _set_bitmode(self, mask, mode):
83+
self._ctrl_out(SIO_SET_BITMODE, mask | (mode << 8))
84+
85+
def _write(self, data):
86+
self._ep_out.write(bytes(data), USB_TIMEOUT)
87+
88+
@staticmethod
89+
def _validate_index(index):
90+
if not 0 <= index <= 7:
91+
raise ValueError("FTDI bit-bang GPIO only supports indexes 0-7")
92+
93+
def _read_gpio_byte(self):
94+
data = self._ctrl_in(SIO_READ_PINS, 0, 1)
95+
if not data:
96+
raise TimeoutError("FTDI GPIO read returned no data")
97+
return data[0]
98+
99+
def get(self, index):
100+
self._validate_index(index)
101+
with self._lock:
102+
value = self._read_gpio_byte()
103+
return bool(value & (1 << (index % 8)))
104+
105+
def set(self, index, status):
106+
self._validate_index(index)
107+
mask = 1 << index
108+
with self._lock:
109+
output = self._read_gpio_byte()
110+
if status:
111+
output |= mask
112+
else:
113+
output &= ~mask
114+
self._set_bitmode(GPIO_MASK, BITMODE_ASYNC_BITBANG)
115+
self._write([output])
116+
117+
118+
def _run_with_device(vendor_id, model_id, busnum, devnum, interface, callback):
119+
device = FTDIGPIO(vendor_id, model_id, busnum, devnum, interface)
120+
try:
121+
return callback(device)
122+
finally:
123+
device.close()
124+
125+
126+
def handle_get(vendor_id, model_id, busnum, devnum, interface, index):
127+
return _run_with_device(
128+
vendor_id, model_id, busnum, devnum, interface,
129+
lambda device: device.get(int(index)),
130+
)
131+
132+
133+
def handle_set(vendor_id, model_id, busnum, devnum, interface, index, status):
134+
_run_with_device(
135+
vendor_id, model_id, busnum, devnum, interface,
136+
lambda device: device.set(int(index), bool(status)),
137+
)
138+
return True
139+
140+
141+
def handle_close():
142+
return True
143+
144+
145+
methods = {
146+
"get": handle_get,
147+
"set": handle_set,
148+
"close": handle_close,
149+
}

0 commit comments

Comments
 (0)