-
-
Notifications
You must be signed in to change notification settings - Fork 8.3k
ports/rp2: Prevent machine.SPI settings from persisting through a soft reset #16917
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
base: master
Are you sure you want to change the base?
Conversation
If the "spi_id" arg is not supplied and then the board default specified by PICO_DEFAULT_SPI will be used. If this is not set, it's defaulted to 0. Note: spi_id is still require when MICROPY_HW_SPI_NO_DEFAULT_PINS is set. Signed-off-by: Phil Howard <github@gadgetoid.com>
While testing this I realised that configured SPI pins survive a soft reset. That's quite a pitfall for beginners doing on device tinkering. I will add a commit to fix that, might as well go the distance with breaking Eg: here's me changing to a custom mosi pin, hitting "Stop" in Thonny and then trying to get the default SPI0 instance:
This bug also affects
This also means that since no arguments are being supplied, and since This is weird. Is anyone really using |
Code size report:
|
It's common with write-only SPI displays for MISO to be repurposed as a register select or data/command pin. While that was possible by setting up the pin after a call to `machine.SPI()` this change makes `machine.SPI(miso=None)` explicit. Signed-off-by: Phil Howard <github@gadgetoid.com>
f41fcb0
to
50968d4
Compare
Okay after way too much looking into this and feeling like I'm losing my mind For
This is not strictly true. SPI is initialised if it was not already initialised since the last hard reset. If extra parameters are given you will get a mix of default values, your custom values, and whatever state the SPI instance was last in. Additionally For
It does not under any circumstances initialise the bus, though it does change any parameters that are passed to it and has a sort of defacto shorthand where you can change any property without affecting the other... this is somewhat antithetical to what people would expect from an The docs also suggest Right now there is a significant portion of repeated c
8000
ode between In keeping with the lean toward shorthand methods I think that:
|
Re 3. There is a need to be able to change the baudrate without affecting other properties. An example is touch displays where an SPI bus is shared between a display and a touch controller, where these have radically different maximum baudrates. My library uses spi.init(baudrate=new_rate) # spi is an SPI instance |
Make `init()` init the SPI. Add pins arguments to `PI(n).init()`. Make `SPI(n, *args, *kwargs)` defer to (and match) `init(*args, *kwargs)`. Signed-off-by: Phil Howard <github@gadgetoid.com>
Thanks for chiming in, I knew there would be an edge case. My latest commit explicitly and intentionally breaks this behaviour. I guess in cases like yours the user is supplying an SPI instance and you don't necessarily know what the other settings are? It feels like Some code sharing between FWIW I don't expect any of this to be merged unchallenged 😆 Edit: It's clear this lies somewhere at the intersection between a code issue and a documentation issue and warrants further discussion about the shape of these functions so I should probably dial back my ambitions slightly. I've added another commit to revert
Hopefully I've managed to retain some of my code size and clarity improvements. Chiefly not having two completely different code paths for changing peripheral settings when one should do. |
Contrary to the documentation, `SPI(n).init()` is used widely for modifying an SPI instance passed in from user code to a Python driver. Drivers may depend upon the ability to change a single parameter while retaining other unknown parameters. Signed-off-by: Phil Howard <github@gadgetoid.com>
If you look at the various implementations in the ports of supporting devices like UART, SPI, ... you surely have noticed that it is consistently inconsistent. But there is a preference for a certain structure:
|
That's pretty much the structure I've been moving toward in my PR, with the exception that Oh and that I am being weird and passing MicroPython argument collections instead of regular C types around 😬 For a succinct example of where this goes awry consider the following:
Edit: I have changed the above proof to add a
All subsequent runs will return 1 because Quoting you from the issue:
I would tend to agree. With my current code that means adding Lines 168 to 171 in 3823aeb
And it would just need to roll through the |
Init is one aspect,
8000
deinit the other. If you look at the code in main after the label |
Add a simple init handler that marks the SPI instances as uninitialised so they are reconfigured on soft reset. Signed-off-by: Phil Howard <github@gadgetoid.com>
I think it's conceptually an The culprit is here: micropython/ports/rp2/machine_spi.c Lines 115 to 128 in 58f8229
These variables shadow the SPI instance's current state and are used to make it possible to reconfigure one setting without also changing the others. I think I should switch to the Edit: For posterity I have left my individual commits in place as I worked toward this... solution... if this is at all worth considering then I will squash those into something more sensible. |
Make consistent with machine_i2s by using a root pointer to manage SPI instance, and allocate them only when they are needed. Signed-off-by: Phil Howard <github@gadgetoid.com>
b2fb54f
to
c09a9bd
Compare
As far as I understand memory allocation, It is not needed to register the SPI objects with MP_STATE_PORT(). This is only needed if the allocated object is not directly returned to the calling Python code and must be protected from clearing though gc by registering it as root pointer. Not that it hurts, but it seems not to be needed. Besides that: why did you move away from the static SPI objects? RAM shortage is not a big issue with the RPI Pico. |
ports/rp2/machine_spi.c
Outdated
@@ -172,7 +180,12 @@ mp_obj_t machine_spi_make_new(const mp_obj_type_t *type, size_t n_args, size_t n | |||
} | |||
self->mosi = mosi; | |||
} | |||
if (args[ARG_miso].u_obj != mp_const_none) { | |||
|
|||
if (args[ARG_miso].u_obj == MP_ROM_INT(-1)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At this state of the commit series, this if clause is wrong. If the argument is not present, the content of the static SPI object must not be changed.
Looking at your PR your seem to change things which were not broken. The only required change would have been to skip the default miso pin initialization in case of Anything else like changing the behavior of the RP2 port's behavior on re-initialization should be deferred until there is a common policy about it. And then it affects not only SPI. |
AIUI if it’s allocated on MicroPython’s heap then you must store a pointer somewhere since unreachable allocs will be garbage collected eventually. This could be via a root pointed, tracked alloc, or if we expect the user to hang onto the SPI instance it would happen implicitly. Since the intent is a persistent singleton this is about the best of a bad bunch of ways to do it- in all fairness at least i2s has some real cleanup to do. It could he allocated in the C heap but insofar as MicroPython is concerned that’s a code smell if not an outright recipe for disaster (a port can be configured with no C heap and all sorts of fun ensues.)
Because it becomes easier to reason about. The state persistence between soft resets, was a (debatably) unwanted side-effect of the static objects not being managed by MicroPython itself. if they are allocated with “make_new” then it becomes clearer that the intent is a sort of singleton pattern rather than persistence.
it isn’t until it is, arguably any static should have some strong argumentation to support why it deserves to skirt runtime allocation- be that performance or simply because it’s used 100% of the time. SPI has no supporting argumentation it’s just - afaik - the coin toss the implementer made when writing it. (Or an explicit choice to support SPI config surviving soft reset in which case they were - arguably - wrong 🤣) edit: I appreciate your feedback and critique here, the argumentation is at least as important (if not more) than the code itself because it puts the reasoning behind these choices on permanent record, helps establish those choices as patterns and helps me understand which bits should maybe be elaborated in commit messages/comments. Good for MicroPython on the whole. That said I will no doubt find myself in trouble since I’ve been guessing and not looking up the original authors rationale 😬 |
“not broken” is extremely debatable given just how many quirks and pitfalls the current code permits, including code that behaves differently after a soft reset and no path back to default values shy of unplugging a Pico.
Where’s the common policy going to come from if it’s not advocated for in a pull request with code and rationale to back it up? (yes granted this all got a bit off piste because SPI was staggeringly weird and it seemed remiss not to address the other issues… fix creep? Is that a thing?) |
I don't want to comment at the moment on the soft reset behaviour of SPI. But I strongly suggest to split up this PR into one for the original intent (make MISO optional, make id optional) and one for the other changes. The first of those changes can be reviewed and merged much quicker than the latter. |
If that's the intent why not keep the static objects.
That is the usual Python behavior. Since with your implementation the SPI object is cleared on soft reset, the persistence would "only" allow to re-create the SPI object with the previous configuration. That's the way I2C onRP2 is implemented, because it lacks the
My impression is, that this effect was wanted. |
A diplomatic choice.
Agreed. It feels a bit like building on shaky foundations 😆 but at least something will get done. Since this PR has gone to the wolves with discussion around the former I will (rename and) leave it as-is until we resolve the questions around SPI soft reset, and raise a new PR to fix the original issue(s). |
Raised another PR for the original issues - #16922 - and updated the OG post to state my case more succinctly. |
Replaced by, original info has moved there: #16922
This has gone off the rails somewhat into a discussion about whether RP2'S SPI (and other peripherals on RP2 and other devices) should be fully reset upon a "soft reset" (in MicroPython terms this is basically just bailing out of user code, doing some teardown and set up and returning back to user code/REPL again)
To state my case in brief: A "reset" should, you know, reset things and an "init" should... "init" things.
For my part, I represent a vendor of "educational" electronics targeted at beginners/hobbyists. We have many MicroPython boards out in the wild and most of these have some self evident physical "default" bus connector: QwSt/Quiic/Stemma for I2C or SP/CE for SPI. Unsurprising and consistent behaviour of these default instances is therefore in my best interests.
In Detail
SPI Settings Persisting Across Soft Resets
Running
SPI(mosi=23)
will change the behaviour ofSPI()
, which is roughly what the documentation confirms- however this change persists across soft resets. IE:SPI()
will use pin 23 for MOSI until a new pin is specified, or until the board is hard reset. This happens because the current SPI pin configuration is saved into static RAM (not managed by MicroPython) and not re-initialised on soft reset.It's not obvious (read: not sufficiently documented) that this is the case. Anyone idly tinkering on the REPL or writing a script could find themselves unable to talk to their device because they've inadvertently made a persistent pin change.
I would be interested in feedback from anyone who relies on the persistence of SPI settings between soft resets since I'm struggling to find many reasons for it (not glitching sensitive external peripherals on reset?) but that may just be a failure of imagination on my part.
As a side-effect of this, SPI claims a small amount of RAM for these persistent instances whether you use it or not. That may not be a big deal on RP2 but reserving RAM for a peripheral that a user might not even use is an odd choice, especially in the absence of any good argumentation for why someone would want SPI settings to be persistent.
SPI().init() Does Not Initialise Anything
SPI().init()
(and other hardwareinit
functions) have been somewhat twisted in meaning. Despite the claims of the documentationSPI().init()
does not "init" the SPI.It seems "init" functions across various hardware peripherals have been misappropriated to mean "make some change to a user-supplied bus." Examples include changing the baud rate on SPI or UART (perhaps supplied by a user into a driver instance) without resetting ("initialising") other values. Again the documentation doesn't sufficiently explain this practice.
As a tertiary consequence of this, SPI's "init" on RP2 specifically doesn't contain all the parameters that
SPI()
has available; chiefly you cannot configure pins with it. As the docs suggest,SPI(id, *args, *kwargs)
should be equivalent toSPI(id).init(*args, *kwargs)
. It is not.