diff --git a/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/board.overlay b/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/board.overlay index e8e0189ce2b..fedbc1b0429 100644 --- a/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/board.overlay +++ b/ports/zephyr-cp/boards/adafruit/clue_nrf52840_zephyr/board.overlay @@ -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 { diff --git a/ports/zephyr-cp/cptools/tests/test_zephyr2cp.py b/ports/zephyr-cp/cptools/tests/test_zephyr2cp.py index eeb0d3f998e..c4208ad692a 100644 --- a/ports/zephyr-cp/cptools/tests/test_zephyr2cp.py +++ b/ports/zephyr-cp/cptools/tests/test_zephyr2cp.py @@ -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, @@ -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.""" diff --git a/ports/zephyr-cp/cptools/zephyr2cp.py b/ports/zephyr-cp/cptools/zephyr2cp.py index 260410d2c41..e80f542f086 100644 --- a/ports/zephyr-cp/cptools/zephyr2cp.py +++ b/ports/zephyr-cp/cptools/zephyr2cp.py @@ -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"] @@ -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): @@ -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") @@ -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. @@ -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() @@ -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, []) @@ -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 = [] @@ -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) {{ @@ -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 @@ -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) {{