Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/board.overlay
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@
/delete-property/ zephyr,bt-mon-uart;
/delete-property/ zephyr,bt-c2h-uart;
};

// The STEMMA QT connector is on SCL/SDA, so board.STEMMA_I2C is the same
// bus as board.I2C(). The upstream board has no node for it.
stemma_connector: stemma_connector {
compatible = "stemma-qt-connector";
#gpio-cells = <2>;
gpio-map-mask = <0xffffffff 0xffffffc0>;
gpio-map-pass-thru = <0 0x3f>;
gpio-map = <0 0 &gpio0 25 0>, /* SCL */
<1 0 &gpio0 24 0>; /* SDA */
};
};

&zephyr_udc0 {
Expand Down
51 changes: 51 additions & 0 deletions ports/zephyr-cp/cptools/tests/test_zephyr2cp.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@
find_ram_regions,
BLOCKED_FLASH_COMPAT,
MINIMUM_RAM_SIZE,
STEMMA_I2C_PINS,
add_toml_pin_names,
_board_i2c_buses,
_fastest_first,
_named_pins,
_standard_bus_pins,
_toml_status_pin,
_used_by_zephyr,
Expand Down Expand Up @@ -900,6 +903,54 @@ def test_unused(self):
assert not self._used("uart1")


class TestStemmaI2cPins:
"""The pins of board.STEMMA_I2C, named by the stemma-qt-connector entry."""

def test_same_pins_as_i2c(self):
names = {("gpio0", 3): ["SCL", "STEMMA_SCL"], ("gpio0", 2): ["SDA", "STEMMA_SDA"]}
assert _named_pins(names, STEMMA_I2C_PINS) == {
"scl": ("gpio0", 3),
"sda": ("gpio0", 2),
}

def test_own_pins(self):
names = {
("gpio0", 25): ["SCL"],
("gpio0", 24): ["SDA"],
("gpio0", 23): ["SCL1", "STEMMA_SCL"],
("gpio0", 22): ["SDA1", "STEMMA_SDA"],
}
assert _named_pins(names, STEMMA_I2C_PINS) == {
"scl": ("gpio0", 23),
"sda": ("gpio0", 22),
}

def test_no_connector(self):
assert _named_pins({("gpio0", 3): ["SCL"], ("gpio0", 2): ["SDA"]}, STEMMA_I2C_PINS) is None


class TestBoardI2cBuses:
"""The board I2C buses for board.I2C() and board.STEMMA_I2C."""

I2C = {"scl": ("gpio0", 25), "sda": ("gpio0", 24)}
STEMMA = {"scl": ("gpio0", 23), "sda": ("gpio0", 22)}

def test_no_stemma_connector(self):
assert _board_i2c_buses(self.I2C, None) == ([self.I2C], None)

def test_stemma_on_the_i2c_pins(self):
assert _board_i2c_buses(self.I2C, dict(self.I2C)) == ([self.I2C], 0)

def test_stemma_on_other_pins(self):
assert _board_i2c_buses(self.I2C, self.STEMMA) == ([self.I2C, self.STEMMA], 1)

def test_stemma_without_standard_i2c(self):
assert _board_i2c_buses(None, self.STEMMA) == ([self.STEMMA], 0)

def test_neither(self):
assert _board_i2c_buses(None, None) == ([], None)


class TestFastestFirst:
"""Ordering of bus instance pools by devicetree max-frequency."""

Expand Down
165 changes: 123 additions & 42 deletions ports/zephyr-cp/cptools/zephyr2cp.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,9 @@
["GP27_A1", "GP27", "A1"],
["GP28_A2", "GP28", "A2"],
],
# STEMMA QT connectors, and Qwiic connectors, which use the same binding.
# board.STEMMA_I2C is on these pins (see STEMMA_I2C_PINS).
"stemma-qt-connector": ["STEMMA_SCL", "STEMMA_SDA"],
}

EXCEPTIONAL_DRIVERS = ["entropy", "gpio", "led"]
Expand Down Expand Up @@ -789,6 +792,9 @@ def _toml_status_pin(mpconfigboard, board_names, key):
# - bus kind: a BOARD_BUS_PINS key, the board bus that such devices provide
# ("uart" for board.UART()).
BOARD_BUS_OF_DRIVER = {"i2c": "i2c", "spi": "spi", "serial": "uart"}
# The pins of board.STEMMA_I2C, as fields of board_i2c_pin_t (see
# BOARD_BUS_PINS). The stemma-qt-connector entry in CONNECTORS names them.
STEMMA_I2C_PINS = {"scl": "STEMMA_SCL", "sda": "STEMMA_SDA"}


def _board_pin_named(board_names, name):
Expand All @@ -799,21 +805,49 @@ def _board_pin_named(board_names, name):
return None


def _named_pins(board_names, fields):
"""Return ``{field: pin}`` for ``fields``, a ``{field: board pin name}``
dict, or None if the board lacks any of the names. ``pin`` is an
``(ioport, num)`` key of ``board_names``.
"""
pins = {field: _board_pin_named(board_names, name) for field, name in fields.items()}
if None in pins.values():
return None
return pins


def _standard_bus_pins(board_names):
"""Find the standard pins of each board bus class.

A class has standard pins when the board names every pin it needs with
the standard names in BOARD_BUS_PINS. Returns ``{bus: {field: pin}}``,
with ``pin`` an ``(ioport, num)`` key of ``board_names``.
the standard names in BOARD_BUS_PINS. Returns ``{bus: {field: pin}}``.
"""
standard_pins = {}
for bus, fields in BOARD_BUS_PINS.items():
pins = {field: _board_pin_named(board_names, name) for field, name in fields.items()}
if None not in pins.values():
pins = _named_pins(board_names, fields)
if pins is not None:
standard_pins[bus] = pins
return standard_pins


def _board_i2c_buses(i2c_pins, stemma_pins):
"""Lay out the board I2C buses for board.I2C() and board.STEMMA_I2C.

``i2c_pins`` are board.I2C()'s pins and ``stemma_pins`` the STEMMA QT
connector's, each a ``{field: pin}`` dict or None. STEMMA_I2C is the same
bus as I2C when its connector uses the same pins, and its own bus
otherwise. Returns ``(buses, stemma_index)``: the pin dicts of the board
I2C buses, in instance order, and the instance that STEMMA_I2C names (or
None).
"""
buses = [i2c_pins] if i2c_pins else []
if stemma_pins is None:
return buses, None
if stemma_pins not in buses:
buses.append(stemma_pins)
return buses, buses.index(stemma_pins)


def _node_enabled(node):
"""Return True if a devicetree node's status is "okay" (or unset)."""
status = node.props.get("status")
Expand Down Expand Up @@ -1325,6 +1359,7 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig
pwm_emul = line.strip().endswith("=y")
board_info["neopixel_write"] = pwm_nrfx
standard_bus_pins = _standard_bus_pins(board_names)
stemma_i2c_pins = _named_pins(board_names, STEMMA_I2C_PINS)
pin_object_of = {}
# GPIO pad number -> pad: every (label, num) name and alias the
# devicetree walk produced keys into one pin object per pad.
Expand Down Expand Up @@ -1393,7 +1428,13 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig
mcu_pin_mapping.append(
f"{{ MP_ROM_QSTR(MP_QSTR_{pin_object_name}), MP_ROM_PTR(&pin_{pin_object_name}) }},"
)
board_pin_names = board_names.get((ioport, num), [])
# A pin prints as its first board name, so list the STEMMA QT
# names last: board.SCL should print as board.SCL, not
# board.STEMMA_SCL.
board_pin_names = sorted(
board_names.get((ioport, num), []),
key=lambda name: name.upper() in STEMMA_I2C_PINS.values(),
)
for board_pin_name in board_pin_names:
board_pin_name = (
board_pin_name.upper()
Expand Down Expand Up @@ -1472,7 +1513,9 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig
for driver in BOARD_BUS_OF_DRIVER
for labels in active_zephyr_devices.get(driver, [])
)
# The pin dicts of each kind's board buses, in instance order.
board_buses = {}
stemma_i2c_index = None
if uses_iobroker:
for driver, bus in BOARD_BUS_OF_DRIVER.items():
instances = active_zephyr_devices.get(driver, [])
Expand All @@ -1488,10 +1531,16 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig
# so log the reason too.
logger.error(message)
raise RuntimeError(message)
if bus in standard_bus_pins and any(
labels[0] in iobroker_labels for labels in instances
):
board_buses[bus] = standard_bus_pins[bus]
if not any(labels[0] in iobroker_labels for labels in instances):
continue
if bus == "i2c":
buses, stemma_i2c_index = _board_i2c_buses(
standard_bus_pins.get("i2c"), stemma_i2c_pins
)
if buses:
board_buses["i2c"] = buses
elif bus in standard_bus_pins:
board_buses[bus] = [standard_bus_pins[bus]]

zephyr_binding_headers = []
zephyr_binding_objects = []
Expand Down Expand Up @@ -1532,30 +1581,40 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig
main = next((labels for labels in candidates if priority_label in labels), None)
if main is not None:
break
if main is None:
continue

instance_name = f"{driver.replace('/', '_')}_{main[0]}"
c_function_name = f"_{instance_name}"
singleton_ptr = f"{c_function_name}_singleton"
function_object = f"{c_function_name}_obj"
obj_type = f"{module}_{driverclass.lower()}"

# Handle special cases for different drivers
if driver == "serial":
# UART needs a receiver buffer
buffer_decl = f"static byte {instance_name}_buffer[128];"
construct_call = f"common_hal_busio_uart_construct_from_device(&{instance_name}_obj, DEVICE_DT_GET(DT_NODELABEL({main[0]})), 128, {instance_name}_buffer, false)"
else:
# Default case (I2C, SPI, I2S)
buffer_decl = ""
construct_call = f"common_hal_{module}_{driverclass.lower()}_construct_from_device(&{instance_name}_obj, DEVICE_DT_GET(DT_NODELABEL({main[0]})))"
# The board names of each instance that gets a board object.
names_of = {}
if main is not None:
names_of[main[0]] = [driverclass.upper()]
# On an all-static board with a STEMMA QT (or Qwiic) connector,
# board.STEMMA_I2C is the instance labeled zephyr_i2c, Zephyr's label
# for the bus wired to such connectors. Its pins aren't compared
# with the connector's: that would mean decoding each SoC's pinctrl.
if driver == "i2c" and not uses_iobroker and stemma_i2c_pins is not None:
stemma = next((labels for labels in candidates if "zephyr_i2c" in labels), None)
if stemma is not None:
names_of.setdefault(stemma[0], []).append("STEMMA_I2C")
for label, names in names_of.items():
instance_name = f"{driver.replace('/', '_')}_{label}"
c_function_name = f"_{instance_name}"
singleton_ptr = f"{c_function_name}_singleton"
function_object = f"{c_function_name}_obj"
obj_type = f"{module}_{driverclass.lower()}"

# Handle special cases for different drivers
if driver == "serial":
# UART needs a receiver buffer
buffer_decl = f"static byte {instance_name}_buffer[128];"
construct_call = f"common_hal_busio_uart_construct_from_device(&{instance_name}_obj, DEVICE_DT_GET(DT_NODELABEL({label})), 128, {instance_name}_buffer, false)"
else:
# Default case (I2C, SPI, I2S)
buffer_decl = ""
construct_call = f"common_hal_{module}_{driverclass.lower()}_construct_from_device(&{instance_name}_obj, DEVICE_DT_GET(DT_NODELABEL({label})))"

if driver == "i2s":
i2sout_instance_names.append(instance_name)
if driver == "i2s":
i2sout_instance_names.append(instance_name)

zephyr_binding_objects.append(
f"""{buffer_decl}
zephyr_binding_objects.append(
f"""{buffer_decl}
static {obj_type}_obj_t {instance_name}_obj;
static mp_obj_t {singleton_ptr} = mp_const_none;
static mp_obj_t {c_function_name}(void) {{
Expand All @@ -1566,26 +1625,47 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig
return {singleton_ptr};
}}
static MP_DEFINE_CONST_FUN_OBJ_0({function_object}, {c_function_name});""".lstrip()
)
zephyr_binding_labels.append(
f"{{ MP_ROM_QSTR(MP_QSTR_{driverclass.upper()}), MP_ROM_PTR(&{function_object}) }},"
)
)
for name in names:
zephyr_binding_labels.append(
f"{{ MP_ROM_QSTR(MP_QSTR_{name}), MP_ROM_PTR(&{function_object}) }},"
)
zephyr_binding_headers = "\n".join(zephyr_binding_headers)
zephyr_binding_objects = "\n".join(zephyr_binding_objects)
zephyr_binding_labels = "\n".join(zephyr_binding_labels)

# Board buses on the standard pins: the CIRCUITPY_BOARD_* pin tables for
# shared-module/board, and the board name for each bus.
# Board buses: the CIRCUITPY_BOARD_* pin tables for shared-module/board,
# and the board name for each bus. board.I2C(), board.SPI() and
# board.UART() are instance 0 when the board has their standard pins.
# board.STEMMA_I2C is the same object as board.I2C() when it uses the
# same pins, and its own I2C instance otherwise.
board_bus_defines = []
board_bus_objects = []
board_bus_labels = []
for bus, pins in board_buses.items():
fields = ", ".join(f".{field} = &pin_{pin_object_of[pin]}" for field, pin in pins.items())
board_bus_defines.append(f"#define CIRCUITPY_BOARD_{bus.upper()} (1)")
board_bus_defines.append(f"#define CIRCUITPY_BOARD_{bus.upper()}_PIN {{{{{fields}}}}}")
for bus, buses in board_buses.items():
tables = ", ".join(
"{"
+ ", ".join(f".{field} = &pin_{pin_object_of[pin]}" for field, pin in pins.items())
+ "}"
for pins in buses
)
board_bus_defines.append(f"#define CIRCUITPY_BOARD_{bus.upper()} ({len(buses)})")
board_bus_defines.append(f"#define CIRCUITPY_BOARD_{bus.upper()}_PIN {{{tables}}}")
if bus in standard_bus_pins:
board_bus_labels.append(
f"{{ MP_ROM_QSTR(MP_QSTR_{bus.upper()}), MP_ROM_PTR(&board_{bus}_obj) }},"
)
if stemma_i2c_index == 0:
board_bus_labels.append("{ MP_ROM_QSTR(MP_QSTR_STEMMA_I2C), MP_ROM_PTR(&board_i2c_obj) },")
elif stemma_i2c_index is not None:
board_bus_objects.append(
f"CIRCUITPY_BOARD_BUS_SINGLETON(stemma_i2c, i2c, {stemma_i2c_index})"
)
board_bus_labels.append(
f"{{ MP_ROM_QSTR(MP_QSTR_{bus.upper()}), MP_ROM_PTR(&board_{bus}_obj) }},"
"{ MP_ROM_QSTR(MP_QSTR_STEMMA_I2C), MP_ROM_PTR(&board_stemma_i2c_obj) },"
)
board_bus_defines = "\n".join(board_bus_defines)
board_bus_objects = "\n".join(board_bus_objects)
board_bus_labels = "\n".join(board_bus_labels)

# Generate tables of allocatable bus instances for the iobroker
Expand Down Expand Up @@ -1934,6 +2014,7 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig
{pin_defs}

{zephyr_binding_objects}
{board_bus_objects}
{iobroker_tables}

void board_init(void) {{
Expand Down
Loading