Skip to content

Commit 5ba2f44

Browse files
committed
doc/configuration: document FTDI GPIO outputs
Document FTDIGPIO and NetworkFTDIGPIO resources for FTDI asynchronous bit-bang data-bus outputs. Signed-off-by: Ozan Durgut <ozan.durgut@analog.com>
1 parent 7d103c2 commit 5ba2f44

2 files changed

Lines changed: 211 additions & 0 deletions

File tree

doc/configuration.rst

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,44 @@ Arguments:
575575
Used by:
576576
- `HIDRelayDriver`_
577577

578+
FTDIGPIO
579+
++++++++
580+
An :class:`~labgrid.resource.udev.FTDIGPIO` resource describes a single FTDI
581+
data-bus GPIO line.
582+
This uses FTDI bit-bang mode for the 8-bit data bus exposed by the selected
583+
interface, not CBUS ``ftdi-cbus`` gpiochips or EEPROM-configured ACBUS
584+
functions.
585+
586+
Supported devices are FT2232-style devices (``0403:6010``, interfaces 1..2),
587+
FT4232H (``0403:6011``, interfaces 1..4), and FT232H (``0403:6014``,
588+
interface 1). For each interface, ``index`` selects bit ``0`` to ``7`` on
589+
that interface's data bus.
590+
Writes use a read-modify-write cycle on the selected 8-bit interface to avoid
591+
changing unrelated bits managed by labgrid.
592+
593+
.. code-block:: yaml
594+
595+
FTDIGPIO:
596+
index: 0
597+
interface: 1
598+
invert: false
599+
match:
600+
ID_PATH: 'pci-0000:00:14.0-usb-0:2'
601+
602+
Arguments:
603+
- index (int): GPIO bit number to use, from ``0`` to ``7``
604+
- interface (int, default=1): FTDI interface/channel number to use
605+
- invert (bool, default=False): whether to invert the logical GPIO value
606+
- match (dict): key and value pairs for a udev match, see `udev Matching`_
607+
608+
Used by:
609+
- `FTDIGPIODriver`_
610+
611+
NetworkFTDIGPIO
612+
+++++++++++++++
613+
A :any:`NetworkFTDIGPIO` describes an `FTDIGPIO`_ resource available on a
614+
remote computer.
615+
578616
HttpDigitalOutput
579617
+++++++++++++++++
580618
An :any:`HttpDigitalOutput` resource describes a generic digital output that
@@ -2584,6 +2622,28 @@ Implements:
25842622
Arguments:
25852623
- None
25862624

2625+
FTDIGPIODriver
2626+
~~~~~~~~~~~~~~
2627+
The :any:`FTDIGPIODriver` controls one `FTDIGPIO`_ or `NetworkFTDIGPIO`_ line
2628+
via FTDI bit-bang GPIO output mode.
2629+
2630+
The FTDI serial kernel driver must not be bound to the same interface.
2631+
2632+
Binds to:
2633+
gpio:
2634+
- `FTDIGPIO`_
2635+
- `NetworkFTDIGPIO`_
2636+
2637+
Implements:
2638+
- :any:`DigitalOutputProtocol`
2639+
2640+
.. code-block:: yaml
2641+
2642+
FTDIGPIODriver: {}
2643+
2644+
Arguments:
2645+
- None
2646+
25872647
SerialPortDigitalOutputDriver
25882648
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
25892649
The :any:`SerialPortDigitalOutputDriver` makes it possible to use a UART

tests/test_ftdigpiodriver.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,38 @@ def device(vendor_id, model_id):
4242
},
4343
)
4444

45+
def child_device(parent):
46+
return types.SimpleNamespace(
47+
device_type="usb_interface",
48+
find_parent=lambda subsystem, device_type: parent,
49+
)
50+
4551
assert resource.filter_match(device("0403", "6014")) is True
52+
assert resource.filter_match(child_device(device("0403", "6014"))) is True
53+
assert resource.filter_match(child_device(None)) is False
4654
assert resource.filter_match(device("0403", "6010")) is True
4755
assert resource.filter_match(device("0403", "6011")) is True
4856
assert resource.filter_match(device("0403", "6001")) is False
4957

5058

59+
def test_network_ftdigpio_resource_validates_range(target, monkeypatch):
60+
monkeypatch.setattr(NetworkFTDIGPIO, "manager_cls", ResourceManager)
61+
kwargs = {
62+
"host": "exporter",
63+
"busnum": 1,
64+
"devnum": 39,
65+
"path": "1-12.4.2",
66+
"vendor_id": 0x0403,
67+
"model_id": 0x6014,
68+
}
69+
70+
with pytest.raises(ValueError):
71+
NetworkFTDIGPIO(target, name=None, index=8, interface=1, **kwargs)
72+
73+
with pytest.raises(ValueError):
74+
NetworkFTDIGPIO(target, name=None, index=0, interface=0, **kwargs)
75+
76+
5177
def test_ftdigpio_driver_create(target, monkeypatch):
5278
monkeypatch.setattr(NetworkFTDIGPIO, "manager_cls", ResourceManager)
5379
NetworkFTDIGPIO(
@@ -122,6 +148,12 @@ def close(self):
122148
target.deactivate(driver)
123149

124150

151+
def test_ftdigpio_driver_release_ignores_unknown_agent():
152+
ftdigpiodriver._shared_agents.clear()
153+
154+
ftdigpiodriver._release_agent("exporter", 1, 39, 1)
155+
156+
125157
def test_ftdigpio_agent(monkeypatch):
126158
from labgrid.util.agents import ftdigpio
127159

@@ -259,6 +291,125 @@ def ctrl_transfer(self, request_type, request, value, index, data, timeout=None)
259291
ftdigpio.handle_get(0x0403, 0x6014, 1, 39, 1, 3)
260292

261293

294+
def test_ftdigpio_agent_configures_device_after_usb_error(monkeypatch):
295+
from labgrid.util.agents import ftdigpio
296+
297+
class FakeEndpoint:
298+
bEndpointAddress = 0x02
299+
300+
def write(self, data, timeout=None):
301+
pass
302+
303+
class FakeConfig:
304+
def __getitem__(self, item):
305+
assert item == (0, 0)
306+
return [FakeEndpoint()]
307+
308+
class FakeDevice:
309+
bus = 1
310+
address = 39
311+
312+
def __init__(self):
313+
self.active_config_attempts = 0
314+
self.configured = False
315+
self.detached = []
316+
317+
def set_configuration(self):
318+
self.configured = True
319+
320+
def is_kernel_driver_active(self, interface):
321+
assert interface == 0
322+
return True
323+
324+
def detach_kernel_driver(self, interface):
325+
self.detached.append(interface)
326+
327+
def get_active_configuration(self):
328+
self.active_config_attempts += 1
329+
if self.active_config_attempts == 1:
330+
raise ftdigpio.usb.core.USBError("not configured")
331+
return FakeConfig()
332+
333+
def ctrl_transfer(self, request_type, request, value, index, data, timeout=None):
334+
if request_type == ftdigpio.IN_REQTYPE and request == ftdigpio.SIO_READ_PINS:
335+
return b"\x00"
336+
return None
337+
338+
device = FakeDevice()
339+
monkeypatch.setattr(ftdigpio.usb.core, "find", lambda **kwargs: [device])
340+
monkeypatch.setattr(ftdigpio.usb.util, "claim_interface", lambda dev, interface: None)
341+
monkeypatch.setattr(ftdigpio.usb.util, "release_interface", lambda dev, interface: None)
342+
monkeypatch.setattr(ftdigpio.usb.util, "dispose_resources", lambda dev: None)
343+
344+
assert ftdigpio.handle_get(0x0403, 0x6014, 1, 39, 1, 0) is False
345+
assert device.configured is True
346+
assert device.detached == [0, 0]
347+
348+
349+
def test_ftdigpio_agent_rejects_missing_output_endpoint(monkeypatch):
350+
from labgrid.util.agents import ftdigpio
351+
352+
class FakeEndpoint:
353+
bEndpointAddress = 0x81
354+
355+
class FakeConfig:
356+
def __getitem__(self, item):
357+
assert item == (0, 0)
358+
return [FakeEndpoint()]
359+
360+
class FakeDevice:
361+
bus = 1
362+
address = 39
363+
364+
def set_configuration(self):
365+
pass
366+
367+
def is_kernel_driver_active(self, interface):
368+
return False
369+
370+
def get_active_configuration(self):
371+
return FakeConfig()
372+
373+
device = FakeDevice()
374+
monkeypatch.setattr(ftdigpio.usb.core, "find", lambda **kwargs: [device])
375+
monkeypatch.setattr(ftdigpio.usb.util, "claim_interface", lambda dev, interface: None)
376+
377+
with pytest.raises(ValueError, match="output endpoint"):
378+
ftdigpio.FTDIGPIO(0x0403, 0x6014, 1, 39, 1)
379+
380+
381+
def test_ftdigpio_agent_close_ignores_release_error(monkeypatch):
382+
from labgrid.util.agents import ftdigpio
383+
384+
released = []
385+
disposed = []
386+
device = types.SimpleNamespace()
387+
gpio = object.__new__(ftdigpio.FTDIGPIO)
388+
gpio._dev = device
389+
gpio._interface = 0
390+
391+
def release_interface(dev, interface):
392+
released.append((dev, interface))
393+
raise ftdigpio.usb.core.USBError("already released")
394+
395+
monkeypatch.setattr(ftdigpio.usb.util, "release_interface", release_interface)
396+
monkeypatch.setattr(ftdigpio.usb.util, "dispose_resources", lambda dev: disposed.append(dev))
397+
398+
gpio.close()
399+
400+
assert released == [(device, 0)]
401+
assert disposed == [device]
402+
403+
404+
def test_ftdigpio_agent_rejects_missing_device(monkeypatch):
405+
from labgrid.util.agents import ftdigpio
406+
407+
monkeypatch.setattr(ftdigpio.usb.core, "find", lambda **kwargs: [])
408+
409+
with pytest.raises(ValueError, match="not found"):
410+
ftdigpio.FTDIGPIO._find_device(0x0403, 0x6014, 1, 39)
411+
412+
262413
def test_ftdigpio_agent_rejects_unsupported_index():
263414
from labgrid.util.agents import ftdigpio
264415

0 commit comments

Comments
 (0)