Repurposing I2C Pins without Device PM

I'm working to write a Zephyr-compatible driver for the LP5814 on a board with a nRF5340. The LP5814 is an LED controller that receives commands over an I2C interface. There's a feature of the LP5814 that is proving to be difficult to implement because of how Zephyr controls access to the I2C bus.

The feature in question is the LP5814's low power mode. Entering it requires a simple I2C transaction, but leaving it involves a special sequence of edges on the SDA and SCL pins that belong to the I2C bus. See the timing diagram:

There's an alternate way to enter and exit low power mode, but it looks like it could be vulnerable to being activated with I2C traffic meant for other devices.

Multiple threads in my application access I2C devices. They go through the usual Zephyr-nrfx glue code, which involves a synchronization object so that only one thread can do a transaction at a time. Unfortunately, there isn't a way to acquire this lock without also enabling the I2C peripheral. It would be desired to acquire the lock while keeping the I2C peripheral disabled so that no other I2C-driven device driver could interrupt the wakeup procedure of the LP5814.

We're currently on Zephyr 3.4, so I looked ahead to newer releases to see if such functionality was added. I see that an "exclusive access" pair of functions was added in Zephyr's PR 86721, but it still uses the Device Power Manager get/put functions to drive pinctrl. If I'm not mistaken, I can't finesse pinctrl into respecting the GPIO configuration required for the LP5814 wakeup procedure.

github.com/.../86721

Currently, my work-around is to access the nrfx driver directly so I can shut off the I2C peripheral and reconfigure the pins to my desires, restoring the configurations and reactivating the I2C peripheral when finished. This workaround has to be wrapped in something like a scheduler disable, which feels awful.

I plan to make my application request all I2C operations through a new layer of firmware with a lock that doesn't interact with the Device Power Manager. In-between my main application logic and device drivers are HALs. I will make those HALs use the new layer so the lock is respected and so that I don't have to edit any of the drivers that comes with Zephyr or NCS. I'm worried that System Work Queue work from those drivers will not be able to respect the new design, though.

Is there something I seem to be missing? Is Zephyr/NCS just not really equipped to handle an I2C device that needs the SDA and SCL pins to be repurposed on occasion?

Parents
  • Hi,

    The best way (in my opinion) is to use PM_DEVICE to quickly suspend the I2C, use the pins as GPIOs, and resume the I2C device.

    I quickly made a minimal application to show how to implement it : test364177.zip

    This example application does not contain any error handling, please be sure to add proper error handling to it. I just wanted to go straight to the point, so I did not put any.

    I used a nRF54LM20DK and built the app with NCS v3.4.1

    And here is the result that I got:

    Best regards,

    Simon

  • Thanks for that sample application, Simon.

    This closely models my workaround, but is this reliable when I2C transactions can be initiated from more than one thread? Looking at the source for Device Power Manager, I don't see anything that allows a caller to wait for the get/put usage counter to become empty, then block access so that I could do the suspend action and subsequent GPIO operations.

    Consider this example.

    Thread A communicates with the LP5814. Thread B communicates with another device on the same I2C bus. The Device PM currently has the I2C bus in the suspended state. Both threads are not doing anything.

    - Thread A begins to wake the LP5814.
    - Halfway through the LP5814 wakeup procedure, Thread B is scheduled to run. It begins an I2C transaction. The Device PM enables the I2C bus, which overrides the GPIO configurations. The wakeup procedure stops.
    - The I2C transaction finishes, and control returns to Thread A. It resumes the GPIO operations, but the GPIO configurations are likely not correct, and since the procedure was interrupted with I2C traffic, the wakeup will likely not work anyway.

    This is why I mentioned that I probably have to wrap the wakeup procedure with a scheduler lock or something similar.

Reply
  • Thanks for that sample application, Simon.

    This closely models my workaround, but is this reliable when I2C transactions can be initiated from more than one thread? Looking at the source for Device Power Manager, I don't see anything that allows a caller to wait for the get/put usage counter to become empty, then block access so that I could do the suspend action and subsequent GPIO operations.

    Consider this example.

    Thread A communicates with the LP5814. Thread B communicates with another device on the same I2C bus. The Device PM currently has the I2C bus in the suspended state. Both threads are not doing anything.

    - Thread A begins to wake the LP5814.
    - Halfway through the LP5814 wakeup procedure, Thread B is scheduled to run. It begins an I2C transaction. The Device PM enables the I2C bus, which overrides the GPIO configurations. The wakeup procedure stops.
    - The I2C transaction finishes, and control returns to Thread A. It resumes the GPIO operations, but the GPIO configurations are likely not correct, and since the procedure was interrupted with I2C traffic, the wakeup will likely not work anyway.

    This is why I mentioned that I probably have to wrap the wakeup procedure with a scheduler lock or something similar.

Children
  • Hi,

    This is not exactly right. The semaphore used inside the nrfx API to limit access to the i2c is still active, and it correctly returns an error (-ETIMEOUT) when the device is not available.

    When suspending the device with PM_DEVICE, it takes the semaphore, thus rejecting any concurrent usage.

    You can configure how much time your application should wait for this semaphore to unlock by setting CONFIG_I2C_NRFX_TRANSFER_TIMEOUT to your desired value in the Kconfigs of your project.(Timeout in milliseconds used for each I2C transfer. 0 means that the driver should use the K_FOREVER value). 

    You can easily test this by trying to do an i2c_write_dt() when the i2c device has been suspended. It should return an error.

    Best regards,

    Simon

  • Which semaphore are you referring to here? Are you talking about the one inside i2c_nrfx_twim.c, or the lock in device_runtime.c?

    I don't see either of those being an adequate block. Since we're using the runtime version of the device power manager, I don't think I can ask for a suspension directly, can I? That is, calling pm_device_action_run() with the "suspend" action will ignore the usage count and therefore create confusion on whether the bus is used. Calling put() is not guaranteed to make the usage count go to 0, and even if it did, there's no guarantee it will be bumped back to a positive number by the time my LED controller thread tries the wakeup procedure. I need to wait for and hold onto the semaphore the whole time I'm executing the wakeup procedure, not just when I'm changing power states.

Related