-
Notifications
You must be signed in to change notification settings - Fork 107
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
RP2040 I2C Refactor #213
Merged
ikskuh
merged 6 commits into
ZigEmbeddedGroup:main
from
haydenridd:rp2040-i2c-hal-improvements
Jul 29, 2024
Merged
RP2040 I2C Refactor #213
ikskuh
merged 6 commits into
ZigEmbeddedGroup:main
from
haydenridd:rp2040-i2c-hal-improvements
Jul 29, 2024
Conversation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
After some feedback:
|
ikskuh
reviewed
Jul 27, 2024
@ikskuh good to merge? |
…/when polling occurs - Simplified API so that all reading/writing happens in 3 functions - Corrected tranlation of baud rate to register settings - Tweaked how I2C is "initalized" and "deinitialized" - Moved most of the error handling responsibility onto the caller of the I2C API, for example code no longer calls `panic()`, but rather bubbles up an error to the caller for them to make a decision on - Added "const correctness" to API - Added comments being explicit about what features of the hardware this API doesn't support
- Changed from_instance_number namespace - Changed translate_baudrate namespace + moved tests
… further discussion
EliSauder
pushed a commit
to EliSauder/microzig
that referenced
this pull request
Aug 27, 2024
* Various performance improvements to I2C reads/writes related to how/when polling occurs * Simplified API so that all reading/writing happens in 3 functions * Corrected tranlation of baud rate to register settings * Tweaked how I2C is "initalized" and "deinitialized" * Moved most of the error handling responsibility onto the caller of the I2C API, for example code no longer calls `panic()`, but rather bubbles up an error to the caller for them to make a decision on * Added "const correctness" to API * Added comments being explicit about what features of the hardware this API doesn't support * Removed const correctness stuff * Changed from_instance_number namespace * Changed translate_baudrate namespace + moved tests * Changed some terminology! * Updated some function names and reverted some design patterns after further discussion * Fixed example and changed how instances are accessed * Removed redundant calls to get_regs() --------- Co-authored-by: Hayden Riddiford <hayden@terrakaffe.com>
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
I2C Refactor
There's enough changes here that I felt it deserved a mini writeup. There was a couple of opinionated decisions I made as well to the API that I wanted to call out and get others opinions on.
High Level
panic()
, but rather bubbles up an error to the caller for them to make a decision onVerification Method
My "self test" firmware for the RP2040 is still very much a WIP, but I used it to verify all the changes I made to the I2C driver. Check that code out here to see what exactly I check, and a good example of using the new API:
https://github.com/haydenridd/microzig/tree/rp2040-selftest/test_apps/rp2040
Performance Improvements
Two main points here:
IC_RAW_INTR_STAT.TX_EMPTY
to check when TX was finished. This is fine, the issue was it did it after every write to the TX FIFO, leading to occasional small gaps between write transactions at higher I2C frequencies. There's no need to poll on this after every byte desired to be transmitted is written toIC_DATA_CMD
. Polling onIC_TXFLR
during the write loop prevents any accidentally overflowing the TX FIFO, and is faster than waiting for the actual transaction to finish for the previous byte written to the FIFO.write_then_read_blocking()
was introduced to give a performant method for handling the common I2C use case of writing one or more bytes to a device representing an internal register address, then immediately reading one or more bytes representing the data held at that internal address. This allows that to be accomplished in a single transaction (a repeated start occurs between the write bytes and read bytes) where previously this behavior required two transactions with a full stop/start + time gap in between.The following oscilloscope shots show the performance gains for each:
Old successive writes at 400kHz:
New successive writes at 400kHz:
Old write + read at 100kHz:
New write + read at 100kHz:
Initialization
I modified the structure of
I2C
slightly from anenum
to astruct
. This is so it can hold SDA + SCLgpio.Pin
instances, as well as a pointer to the raw registers it controls. This enables there to be aninit()
function bound as a method to this struct. Given that this is indeed initializing resources, it seemed appropriate to follow the Zig convention ofinit()
/deinit()
for structs that manage resources. Note I've also added adeinit()
function that does something equally un-surprising.Something I don't love, but don't see another way around, is the addition of an
initialized: bool
field in theI2C
struct. There are no constructors in Zig, which is a good thing. But, this means it's on the user to handle initializing and deinitializing resources explicitly. This field blocks the user from calling the methods that depend on I2C being initialized before use. For instance, if not for this, when you callwrite_blocking
without a timeout (null
), you end up in an infinite loop on the polling call towhile (i2c.tx_fifo_available_spaces() == 0)
. This is completely reasonable, as when the peripheral isn't enabled/configured, that register will constantly read0
. Personal opinion is I would rather have this small amount of boilerplate and shield a HAL user from footguns that accompany using a peripheral API without configuring the peripheral appropriately. I've seen very similar boilerplate in vendor HALs.Finally, I felt
from_instance_number()
conforms more to a slightly lesser used but still present Zig convention of naming a constructor that creates a struct from some otherSomething
,fromSomething(something: Something) StructType
. The original constructor functionnum()
, I felt, was a little bit ambiguous what exactly it did.Baud Rate Translation
This was by far the most confusing section of the RP2040 datasheet I've had to figure out thus far. So confusing, in fact, it appears from comments the Rpi Pico's own SDK gave up on actually configuring this 100% correctly. My changes are subtle but:
IC_FS_SCL_LCNT
/IC_FS_SCL_HCNT
(See section 4.3.14.1.)Const Correctness
This is up for debate, but I'm of the opinion any HAL API method that changes (writes/reads with side effects) a device register should have the following "non-const" method signature:
Of course, this requires HAL designers to pay careful attention to what does and does not mutate device state. There are often registers that have "clear on read" behavior, that despite being a "read" should indeed be considered mutating behavior. However, register reads that don't mutate state (for instance some device ID, or current peripheral configuration), would be allowed under a
const
method.I found the way the existing API was set up confusing because you could create a
const
instance ofI2C
, and yet have full access to all sorts of functions that configure the peripheral, write bytes, generally mutate device state. Sure,const
was valid because the struct itself wasn't mutating, but wouldn't it be nice if a user could write a function like this:And have it fail to compile because they accidentally call a function that mutates device state when their input is
const
?Misc Changes
gpio.zig
+ added support for slew-rate/schmitt trigger to configure I2C pins per datasheet recommendations