Skip to content

ESP32: new/reworked GPIO hold functionality #8284

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed

Conversation

jonathanhogg
Copy link
Contributor

@jonathanhogg jonathanhogg commented Feb 9, 2022

The current pull=Pin.PULL_HOLD argument doesn't make a lot of sense in the context of what it actually does vs what the ESP32 quickref document says it does.

This patch proposes dumping this and adding a new hold=True|False keyword argument to Pin()/Pin.init(). Setting this to True will cause the ESP32 to lock the configuration of the pin – including direction, output value, drive strength, pull-up/-down – such that it can't be accidentally changed and will be retained through a watchdog or internal reset.

In addition, a new gpio_deep_sleep_hold(True|False) function in the esp32 module allows this behaviour to be extended to entering and waking from deep-sleep.

Support for configuring output drive strength is also added through the standard drive keyword argument and constants:

  • Pin.DRIVE_WEAK
  • Pin.DRIVE_LOW
  • Pin.DRIVE_MED (default pin drive strength)
  • Pin.DRIVE_HIGH

Edit: see comment below about moving of drive strength commits

Will write documentation if this looks like being accepted.

I'm also happy to put back in some kind of legacy compatibility for pull=Pin.PULL_HOLD if there's concern that this is being used in the wild for what it actually does (rather than what it's documented as doing).

Fixes #8283

@jonathanhogg
Copy link
Contributor Author

Note, that if the hold keyword argument is given at all then my implementation disables hold, makes any config changes and then applies the hold if hold=True. This allows you to do things like:

pin = Pin(NUMBER, Pin.OUT, value=1, hold=True)
...
pin.init(value=0, hold=True)

and have these always work – including the original config working on a watchdog/internal reset. Without this, one would have to explicitly disable the hold before doing anything else, i.e.:

pin = Pin(NUMBER, hold=False)
pin.init(Pin.OUT, value=1, hold=True)
...
pin.init(hold=False)
pin.init(value=0, hold=True)

which both looks clunky to me and feels like it'd be forgotten and result in subtle bugs creeping in.

I also fear crazy race conditions like a MicroPython task switch occurring between the line releasing the hold and the one changing the config and reapplying the hold, then that other task triggering a reset. This way isn't foolproof – it doesn't use a critical section – but it feels safer.

However, it still means that casual use of something like:

Pin(NUMBER, Pin.IN)

or:

pin.value(0)

on a held pin would be (silently) ignored.

I am basically saying that if you use the hold keyword, I will assume that you really meant this config to apply.

@jonathanhogg
Copy link
Contributor Author

I'm also happy to put back in some kind of legacy compatibility for pull=Pin.PULL_HOLD if there's concern that this is being used in the wild for what it actually does (rather than what it's documented as doing).

Actually, I thought a bit more about this but I can't see a clean way of adding this compatibility behaviour that doesn't defeat the new hold keyword.

Making pull=Pin.PULL_HOLD the same as hold=True is easy, but the only way to currently disable the hold is to set pull to some other value, and having that imply hold=False is a bit messier. It would mean that:

Pin(NUMBER, Pin.IN, pull=Pin.PULL_UP)

would reconfigure a held pin, but:

Pin(NUMBER, Pin.IN)

would be ignored.

@jonathanhogg jonathanhogg force-pushed the esp32_gpio_deep_sleep_hold branch from 2b1bf67 to 5904db9 Compare February 10, 2022 13:40
@DvdGiessen
Copy link
Contributor

DvdGiessen commented Feb 14, 2022

Useful to note: iirc it is possible (and IDF docs seem to confirm) to change the pin configuration on a held pin, then call gpio_hold_dis(), which will cause the configured values to be applied immediately.

So there is no need to call gpio_hold_dis() in the init helper before configuring the pin; we can do so at the end just before (re-)enabling the hold.

It would probably also be very nice to add an extra method to allow setting the hold after the Pin is initialized, which would be more explicit than having to re-init the entire pin. So we could do things like:

# Initialize an (possibly already held) pin
mypin = Pin(0, Pin.OUT)

# Set it how I want it
mypin.value(1)

# Apply the value and hold again
mypin.hold(False)
mypin.hold(True)

As for PULL_HOLD, while I agree that removing the constant would be cleaner than just changing its behaviour, since at least we'll get a clear error message instead of silently changed behaviour, a quick search across all of GitHub shows PULL_HOLD being used, even in this repository.

@jonathanhogg jonathanhogg force-pushed the esp32_gpio_deep_sleep_hold branch from 5904db9 to e331b84 Compare February 14, 2022 17:39
@jonathanhogg
Copy link
Contributor Author

So there is no need to call gpio_hold_dis() in the init helper before configuring the pin; we can do so at the end just before (re-)enabling the hold.

Ah! Good spot. I hadn't realised that the docs were implying this behaviour, but have been able to confirm it in my hardware. So I've simplified my code to move the disable back to the end. I'm still doing it even if hold is then to be immediately re-enabled so that any config changes will be latched.

It would probably also be very nice to add an extra method to allow setting the hold after the Pin is initialized, which would be more explicit than having to re-init the entire pin

The Pin.init() method doesn't reinitialise anything that you don't specify in the arguments so pin.init(hold=True) is equivalent to your pin.hold(False); pin.hold(True).

I thought about adding a method, but felt that being able to do everything in one hit felt more useful. One could obviously add a method anyway the same as pin.value(level) is (almost) equivalent to pin.init(value=level).

As for PULL_HOLD, while I agree that removing the constant would be cleaner than just changing its behaviour, since at least we'll get a clear error message instead of silently changed behaviour, a quick search across all of GitHub shows PULL_HOLD being used, even in this repository.

Yeah, looking at that code I can't see what purpose the use of PULL_HOLD is serving. Unless I'm missing something, it looks like it has been added based on the current documentation in the belief that it will affect deep-sleep current draw.

I'm gonna have a hunt around and see if I can find anyone using PULL_HOLD for its actual pad holding behaviour…

@DvdGiessen
Copy link
Contributor

DvdGiessen commented Feb 14, 2022

The Pin.init() method doesn't reinitialise anything that you don't specify in the arguments so pin.init(hold=True) is equivalent to your pin.hold(False); pin.hold(True).

My bad, I didn't read the read the code before commenting and blindly assumed there'd be a downside.

I thought about adding a method, but felt that being able to do everything in one hit felt more useful. One could obviously add a method anyway the same as pin.value(level) is (almost) equivalent to pin.init(value=level).

A better reason might be parity (in addition to .value()) with the .mode() and .pull() methods. Which I just noticed are not implemented in the esp32 port either, currently, but they do exist in other ports. So perhaps that's something for a separate change.

@jonathanhogg
Copy link
Contributor Author

jonathanhogg commented Feb 14, 2022

A better reason might be parity (in addition to .value()) with the .mode() and .pull() methods. Which I just noticed are not implemented in the esp32 port either, currently, but they do exist in other ports. So perhaps that's something for a separate change.

Interesting. I'd never noticed those methods before (I mainly use MicroPython on ESP32).

In the docs it says:

The following methods are not part of the core Pin API and only implemented on certain ports.

So does this indicate that they are counted as legacy? Or incomplete implementations in the other ports?

Other than endless forks of the tinypico.py module, I've only found a few other GitHub repos using PULL_HOLD. Most of these seem to be for the "save power in deep-sleep" reason, but one looks tantalisingly like it is trying to actually latch the pin state.

It looks like it wants to hold the level of a bunch of keypad row output pins through deep-sleep to allow the column input pins to wake the chip on an ext1 interrupt. However, in my own tests this won't work without that new method I've added to call gpio_deep_sleep_hold_en(). I'm tempted to track down the repo owner and ask them where they are getting to with this code…

Note to self: me2d13/cml@40dc5b0

@jonathanhogg
Copy link
Contributor Author

I've pulled the drive strength commits out of here into #8313 in the hope that they can be accepted faster on their own.

@jonathanhogg jonathanhogg force-pushed the esp32_gpio_deep_sleep_hold branch from 8a22f61 to 87a7c4c Compare February 16, 2022 16:32
@jonathanhogg jonathanhogg changed the title ESP32: new GPIO hold and drive strength functionality ESP32: new/reworked GPIO hold functionality Feb 16, 2022
@jonathanhogg jonathanhogg force-pushed the esp32_gpio_deep_sleep_hold branch from 87a7c4c to 653f304 Compare February 17, 2022 15:09
@jonathanhogg
Copy link
Contributor Author

I've put the PULL_HOLD constant back in so that existing code won't break. It will behave exactly the same as using pull=None, which is sufficient to disable RTC-pin pull resistors going into deep-sleep (see discussion in linked issue). I've also updated all of the associated documents.

I've tried to do all of this as separate commits that can be cherry-picked – e.g., the new power-saving documentation in 41c86f7 stands alone even if nothing else is accepted. However, my documentation of gpio_deep_sleep_hold() assumes that the pin hold keyword is there.

@jonathanhogg jonathanhogg force-pushed the esp32_gpio_deep_sleep_hold branch 3 times, most recently from bf13807 to ccef05f Compare February 26, 2022 17:30
@jonathanhogg
Copy link
Contributor Author

Rebased this on top of #8313 as the merge conflicts in my local repo were driving me crazy.

@jonathanhogg jonathanhogg marked this pull request as ready for review February 28, 2022 19:41
@jonathanhogg
Copy link
Contributor Author

jonathanhogg commented Feb 28, 2022

I'm now using this in the wild for a project that requires careful control over pins in deep-sleep to correctly disable external hardware and avoid unnecessary power drain.

@jonathanhogg jonathanhogg force-pushed the esp32_gpio_deep_sleep_hold branch from ccef05f to d99f23b Compare March 7, 2022 09:22
@dpgeorge
Copy link
Member

dpgeorge commented Mar 7, 2022

Please rebase on latest master, to pick up the merge of the pin drive strength commits.

@jonathanhogg jonathanhogg force-pushed the esp32_gpio_deep_sleep_hold branch from d99f23b to 32f151b Compare March 7, 2022 12:55
@dpgeorge
Copy link
Member

dpgeorge commented Mar 8, 2022

I've put the PULL_HOLD constant back in so that existing code won't break. It will behave exactly the same as using pull=None, which is sufficient to disable RTC-pin pull resistors going into deep-sleep (see discussion in linked issue). I've also updated all of the associated documents.

If PULL_HOLD doesn't work as expected (as it was previously documented in the quickref before this PR) then IMO we should just remove it completely. Then anyone using it will see their code break very visibly and they can investigate and fix it properly. If we leave PULL_HOLD and subtly change its behaviour then that is worse.

@jonathanhogg jonathanhogg force-pushed the esp32_gpio_deep_sleep_hold branch from 32f151b to 02ece2b Compare March 9, 2022 14:28
@jonathanhogg
Copy link
Contributor Author

If PULL_HOLD doesn't work as expected (as it was previously documented in the quickref before this PR) then IMO we should just remove it completely. Then anyone using it will see their code break very visibly and they can investigate and fix it properly. If we leave PULL_HOLD and subtly change its behaviour then that is worse.

Fine with me! I've removed the constant completely.

I think I'd be happier if this code – and my thinking – could be reviewed by someone with knowledge of the ESP32 architecture. What I've documented seems to work for me in my current project, but I'm nervous about having missed something key in my understanding.

@jonathanhogg
Copy link
Contributor Author

Quick search for PULL_HOLD on the repo to check for other uses:

  • PULL_HOLD is actually listed in the machine.Pin docs, though no behaviour is given for it.
  • The mimxrt port uses the PULL_HOLD constant. On that port it is used to enable the pad "keeper". As far as I can make out this is a mechanism for disabling the output driver while keeping a weak pull up/down to hold the output level. It seems to be a power-reduction device allowing the pad to be connected to a capacitive load (including a long wire I guess) that requires a fair bit of current when transitioning level, but none after that to "keep" it. PULL_HOLD makes some degree of sense here as configuration of the pad keepers is bound up in the pull configuration.
  • Also, obviously, my patch here will break the tinypico.py module. Should I add a fix for that?

@jonathanhogg jonathanhogg force-pushed the esp32_gpio_deep_sleep_hold branch from 02ece2b to d1fe0b6 Compare March 10, 2022 12:54
@jonathanhogg jonathanhogg force-pushed the esp32_gpio_deep_sleep_hold branch from d1fe0b6 to 2e3fc57 Compare March 10, 2022 13:00
This attempts to better explain how pull-ups and pull-downs operate in
deep-sleep.
Move pad holding functionality from `pull=PULL_HOLD` constant to new
`hold=` argument. `PULL_HOLD` constant is retained for compatibility,
but is now equivalend to `pull=None`.
Document new `hold` keyword argument that replaces `pull=Pin.PULL_HOLD`.
Add a new function to control whether held pins will retain their 
function through deep-sleep.
@jonathanhogg jonathanhogg force-pushed the esp32_gpio_deep_sleep_hold branch from 2e3fc57 to 04e2ceb Compare March 10, 2022 14:16
Document new `esp32.gpio_deep_sleep_hold()` function and explain how to
use this in quickref to retain pin configuration during deep-sleep.
Change APA102 power handling to not use the (now removed) PULL_HOLD 
constant.
@jonathanhogg jonathanhogg force-pushed the esp32_gpio_deep_sleep_hold branch from 04e2ceb to fbabdbd Compare March 10, 2022 14:29
dpgeorge pushed a commit that referenced this pull request Mar 21, 2022
The current pull=Pin.PULL_HOLD argument doesn't make a lot of sense in the
context of what it actually does vs what the ESP32 quickref document says
it does.

This commit removes PULL_HOLD and adds a new hold=True|False keyword
argument to Pin()/Pin.init().  Setting this to True will cause the ESP32 to
lock the configuration of the pin – including direction, output value,
drive strength, pull-up/-down – such that it can't be accidentally changed
and will be retained through a watchdog or internal reset.

Fixes issue #8283, and see also #8284.
@dpgeorge
Copy link
Member

Thanks @jonathanhogg for all the effort on this, with nice clean commits and documentation.

I have re-read through this PR and issue #8283, and also looked again at the IDF, and I think your reasoning and implementation here are correct. The API here exposes the IDF functionality in a clean and orthogonal manner, you can enable and disable hold on the GPIO, and enable and disable the hold-during-deep-sleep.

Merged in 5887dfe through 3b9de19

@dpgeorge dpgeorge closed this Mar 21, 2022
@jonathanhogg jonathanhogg deleted the esp32_gpio_deep_sleep_hold branch March 21, 2022 13:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ESP32: Pin.PULL_HOLD does not do what the quickref says it does
3 participants