Conversation
This is the inverse of mp_hal_get_pin_obj(): it converts a low-level mp_hal_pin_obj_t back into the corresponding machine.Pin object. Ports that store a pointer to their pin object get a simple cast, while ports that store a pin index look the object up in their pin table. It is added to all ports that build extmod/machine_i2c.c with SoftI2C enabled, so that shared code does not have to know how a given port represents a pin. Signed-off-by: Sebastian Romero <s.romero@arduino.cc>
These return the machine.Pin objects that the bus is using, which makes it possible for code to reuse the pins of an existing I2C instance without having to be told what they are. The methods are backed by two new optional entries in the I2C protocol struct. SoftI2C implements them on all ports via mp_hal_pin_to_obj(); hardware I2C implementations that do not provide them return None. Signed-off-by: Sebastian Romero <s.romero@arduino.cc>
Report the SCL and SDA pins of a hardware I2C instance. Signed-off-by: Sebastian Romero <s.romero@arduino.cc>
Report the SCL and SDA pins of a hardware I2C instance. Signed-off-by: Sebastian Romero <s.romero@arduino.cc>
Report the SCL and SDA pins of a hardware I2C instance. Signed-off-by: Sebastian Romero <s.romero@arduino.cc>
Report the SCL and SDA pins of a hardware I2C instance. Signed-off-by: Sebastian Romero <s.romero@arduino.cc>
Report the SCL and SDA pins of a hardware I2C instance. Signed-off-by: Sebastian Romero <s.romero@arduino.cc>
Tests that SoftI2C and hardware I2C report their SCL and SDA pins, and that the reported pins follow a subsequent init(). Pins are selected per target, following extmod_hardware/machine_i2c_target.py. Signed-off-by: Sebastian Romero <s.romero@arduino.cc>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #19672 +/- ##
=======================================
Coverage 98.58% 98.59%
=======================================
Files 182 182
Lines 23322 23335 +13
Branches 5 5
=======================================
+ Hits 22993 23006 +13
Misses 328 328
Partials 1 1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Code size report: |
|
What stops you from just remembering the pins that you use for I2C in your Python code? |
|
@robert-hh Because this is being used in a library. The end user just passes an We would have to force the user to figure out the pins used on the QWIIC port(s) and then pass those to the library. Not exactly the user experience we're aiming for. |
|
Regarding the Python API, You left it undocumented, so that is an open item, |
|
@Josverl Thank you for your feedback. The API is documented in docs/library/machine.I2C.rst On the API shape: On repr(): I should have been clearer, and I'll add it to the PR as a rejected alternative. It isn't only that regex-parsing is expensive — repr is a debug representation rather than an API contract, and it doesn't carry the information everywhere: mimxrt prints |
Sorry , my mistake, I completely overlooked that :| Your reasoning on the API makes sense to me. |

Summary
This adds
I2C.scl()andI2C.sda(), which return themachine.Pinobjects a bus is using.The motivating use case is I2C bus recovery. In the Arduino Modulino package, Modulino nodes have their own MCU, so resetting the host board or hot-swapping a node mid-transaction can leave the bus stuck with a peripheral holding SDA low. Recovering from that means driving SCL for up to nine cycles while SDA is held high, then re-initialising the bus object. To do this the code has to know which pins the bus is on, and there is currently no API that answers that question. The library just receives an
I2Cobject. This way we don't have to ask the library users to explicitly provide (and know) the pins used for the I2C bus. (The target audience is beginners).The workaround in use today is to take
repr()of the I2C object and pull the pin names out with a regular expression. That is expensive (it formats and allocates a string on a code path that runs at startup), and it depends on the exact layout of a debug representation that is port-specific and not an API contract.The methods are backed by two new optional entries in the
mp_machine_i2c_p_tprotocol struct:
machine.SoftI2Cimplements them on all ports.None, which is documented.To keep the shared code portable this also adds
mp_hal_pin_to_obj()to the C-levelpin HAL of every port that builds
extmod/machine_i2c.cwith SoftI2C enabled. SeeTrade-offs and Alternatives for why that is needed.
docs/library/machine.I2C.rstdocuments both methods, including theNonecase andwhich ports report hardware pins.
Testing
A new hardware test,
tests/extmod_hardware/machine_i2c_pins.py, checks that bothSoftI2C and hardware I2C report their pins, that the reported pins follow a subsequent
init(), and that a port which does not report hardware pins returnsNoneconsistently for both. It needs no external wiring, and selects pins per target the
same way
extmod_hardware/machine_i2c_target.pydoes.Tested on hardware, 3/3 tests passing on each, covering all three ways a port can
represent a pin:
mp_hal_pin_obj_tuint(GPIO number)gpio_num_tconst machine_pin_obj_t *const machine_pin_obj_t *Beyond the test, the returned objects were checked to be the same canonical
Pinobjects reachable by name on each board - e.g. on the Nano RP2040 Connect,
I2C(1).scl() is Pin('A1'), and on the Nicla Vision the hardware buses reportPin(Pin.cpu.B8, mode=ALT_OPEN_DRAIN, pull=PULL_UP, alt=AF4_I2C1)and friends forI2C1/2/3, matching the board definition.
Build-tested only: samd (ADAFRUIT_ITSYBITSY_M4_EXPRESS) and unix.
Not tested at all, and where I would appreciate CI or another contributor's boards:
esp8266, nrf, mimxrt, alif, zephyr, psoc-edge. These receive only the one-line
mp_hal_pin_to_objaddition. For five of them it is the same pointer cast that stm32and renesas-ra use, both of which are hardware-tested. esp8266 is the one that
differs materially - it indexes the port's pin table rather than casting a pointer -
and I have no toolchain for it here, so it is unverified. nrf could not be built
locally either (its build needs SoftDevice headers that
make submodulesdoes notfetch).
Trade-offs and Alternatives
Code size. Measured on PYBV11, the whole series costs +104 bytes of text, with
no change to data or bss (376360 -> 376464). Per-symbol on that build: the two shared
dispatchers in extmod are 20 bytes each, the two stm32 accessors are 4 bytes each
(the HAL conversion is a cast, so the compiler emits almost nothing), the two function
objects are 8 bytes each, plus two locals-dict entries and two protocol-struct slots.
Ports with integer pin types pay ~32 bytes more for the
mp_hal_pin_to_objlookupfunction. Every port pays for the two dict entries even if its hardware I2C returns
None; that could be avoided with a config option, but a two-entry gate did not seemworth the
mpconfigsurface.Alternative considered and rejected: standardising
repr()instead. Since theexisting workaround parses
repr(), one option is to make that output complete anduniform across ports rather than add an API. Rejected for three reasons. First,
repr()is a debug representation, not an API contract - code that parses it relies onsomething no port promises to keep stable, and changing the output to standardise it
would itself break anyone already parsing it. Second, it is not merely inconsistent, it
is absent: mimxrt prints
I2C(%u, freq=%u, timeout=%u)and zephyr prints<I2C %s>, neither of which contains the pins - and neither port stores the pins inits I2C object, so completing their
repr()needs exactly the same underlying changeas implementing the accessors. Third, where pins are printed the formats differ
(integers on esp32/rp2, quoted names on stm32/renesas-ra/alif/samd), so a parser still
needs per-port handling, and it pays string formatting, allocation and regex matching
at runtime for information the object already holds.
API shape: two methods, rather than one
config()/dict getter or attributes.Per-parameter accessor methods are what
machineperipheral classes already use -PWM.freq(),PWM.duty_u16(),PWM.duty_ns()- whereas theconfig('param')formbelongs to the network and Bluetooth classes (
WLAN.config(),BLE.config()). Twomethods therefore follow the convention for this class, reuse the existing
scl/sdaQSTRs so they add no new strings, and return the pin objects without allocating; a dict
would build and allocate one on every call, on a path that may run precisely when a bus
is already wedged.
The counter-argument is real, though: constructors take more than these two parameters
(
id,freq,timeout), and a general getter would scale to all of them where addingone method per parameter does not. I think that is worth doing as its own discussion
rather than here, because it is a cross-class decision -
SPI,UARTandPWMwouldall want the same shape - and because
scl/sdaare the parameters with a concreteuse case today. If maintainers would rather have
config()onmachineclasses, I amhappy to rework this on top of that decision. Either way the API shape set here is a
precedent for the other bus classes, so it is worth settling explicitly.
Why not store
mp_obj_tin the SoftI2C object and avoid the conversion entirely?mp_machine_soft_i2c_obj_tstoresmp_hal_pin_obj_tbecause the bit-banging codecalls
mp_hal_pin_od_low(),mp_hal_pin_read()and friends on it once per bit.Storing the Python object would move a conversion into that hot path, change a struct that is public to every
port's soft-I2C code, and add GC-visible references to a struct that has none today.
Why is
mp_hal_pin_to_obja macro on some ports and a function on others? Itfollows what
mp_hal_pin_obj_tactually is, and matches how each port already definesmp_hal_get_pin_objin the same header:alif, zephyr), the conversion is a pure cast -
MP_OBJ_FROM_PTR- so a macro emitsno code at all.
that raises on failure. That cannot go in the header: the tables and helpers are
declared in
machine_pin.h/pin_af.h, whichmphalport.hdoes not include, andmphalport.his pulled in extremely early by nearly every file - adding thoseincludes invites include cycles. The body also raises, which would drag
py/runtime.hinto every consumer.esp_mphal.h, the table index equalsphys_portfor every valid entry, pin ids onlyever arrive from the validating
mp_obj_get_pin(), and that port is the mostROM-constrained in the tree.
Each port's helper therefore raises that port's own existing message for a bad pin
("invalid pin" on esp32/rp2, "not a Pin" on samd, via
pin_find_by_id()) rather than anew uniform one. In practice these paths are unreachable through this feature, since
stored pins always came from
mp_hal_get_pin_obj()or a board's compile-time defaults;the checks are for future callers of the HAL helper.
The cost of the mixed macro/function form is that macros get no type checking, and a
new port that omits the definition finds out only when something compiles
machine_i2c.cwith SoftI2C enabled - a loud build error rather than silent breakage.Making all ten real functions would be more uniform, at the price of adding a
.cdefinition to six ports that currently need no code; happy to change it if preferred.
Hardware I2C returning
Noneon some ports. Filling the gapis not uniform work: alif already stores its pins and would be the same two-liner, but
mimxrt and zephyr do not keep the pins in their I2C object (so it means adding fields or
recovering them from the peripheral/devicetree config), and nrf and psoc-edge have no
ports/*/machine_i2c.cat all. Since I cannot test any of those boards, I would ratherleave them to people who can than add unverified code. The alternative - raising
NotImplementedErrorinstead of returningNone- would make the gap louder but costsan error message on every affected port; happy to switch if that is preferred.
Generative AI
I used generative AI tools when creating this PR, but a human has checked the
code and is responsible for the code and the description above.