Skip to content
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

A common high-level API for GPIO handling? #1

Open
Klafyvel opened this issue Dec 26, 2024 · 5 comments
Open

A common high-level API for GPIO handling? #1

Klafyvel opened this issue Dec 26, 2024 · 5 comments

Comments

@Klafyvel
Copy link

Hi there, I'm starting to write a high-level API for the MCP2221 chip (I already have the low-level API working). Some of this involves GPIO handling, that is going to be very similar to what's done here (setting them as input/output, querying their current state, maybe analog IO, etc...). What do you think about specifying a common abstract API for this? Do you know any other projects that may be interesting to add to such a kind of API?

@Seelengrab
Copy link
Owner

Hi! Thanks for the suggestion - I've experimented with this a little bit a while ago, using register definitions generated from an SVD with https://github.com/Seelengrab/DeviceDefinitions.jl. The main thing to do with those register definitions is to create a struct representing the device, and use that for configuration & data transfor (getindex/setindex! felt nice to me). You can find an (incomplete - I never got to finish this properly, due to issues with getting the chip initialized properly) example of what that looks like at https://github.com/Seelengrab/RP2040.jl/blob/main/src/gpio.jl.

There are unfortunately a few difficulties here (not unsolvable, mostly annoyances):

  • The generated code takes a long time to lower to Julia code. Parsing itself is fast, but lowering such a huge project (RP2040 ends up with just shy of 70k LOC, and that's on the small end - I have projects with north of 300k generated LOC alone) takes its toll. This makes prototyping extremely annoying & slow. The upside is that the compilation model of GPUCompiler only has to infer & compile what it sees, so if it's not used, it doesn't further prolong compilation.
  • There's often very little overlap between the GPIO implementations found in hardware. Some chips have only very basic pull up/down support for GPIO, some are programmable, etc. Finding a common interface is highly nontrivial, which is why the old basic example for the ATmega328p in this repo just tries to be a IO.
  • Speaking of IO: There are problems with method ambiguities that ideally need to be resolved better. See Unclear ambiguity when one argument is more specific in one method and another argument in the other JuliaLang/julia#47385 for some more information.

In general, I very much subscribe to the philosophy of having a struct representing the device, and manipulating that in favor of just calling some magic functions (as is usually done in C). I find that this leads to more ergonomic APIs that are harder to misuse, because you can (in principle) check that what the user is intending to do is actually safe & makes sense to do with the current state of the device.

Hope that helps! :)

@Klafyvel
Copy link
Author

Klafyvel commented Jan 6, 2025

Wow, generating a chip's driver from the SVD file sounds like a super powerful idea.

I was more thinking about something more on the high-level side of things (for example the MCP2221 chip is not a microcontroller, it's a USB to I2C/UART converter that happens to have GPIOs), in order to have a roughly unified interface for that kind of things. We could have something that's quite permissive. On the user-side it could look like (obviously very inspired from the Arduino API, something for interrupts should also be added I guess)

function pinmode(device, pin) end
function pinmode!(device, pin, mode) end
function Base.read(device, pin) end
function Base.write(device, pin data) end

where the mode definition is let to the implementer of the device driver. For example the MCP2221 we could have:

@enum PinMode DigitalIn DigitalOut AnalogIn AnalogOut SSPNDOutput ClockOutput USBCFGOutput LEDI2COutput LEDURxOutput LEDUTxOutput Interrupt

The interface could also provide a few helpers to the implementer of the device driver to signal capabilities of the device (in the style of what Tables.jl does) and simplify implementations:

function iscompatible(device, pin, mode) end
function isreadable(device, pin) end
function iswriteable(device, pin) end
function hasinterrupts(device, pin) end
function write(device, pin, pinmode, data) end # or something smarter to find a type-stable way to allow writing small functions for writing/reading from a pin that's in `pinmode`.

Something similar could probably be devised for I2C, UART, etc., maybe drawing inspiration from CMSIS?

For the method ambiguities coming from IO, isn't it mostly due to inheriting from Base.IO? Cannot we simply lift that requirement without committing type-piracy, since we own the type? For example AbstractTrees.jl does not explicitely require trees to inherit AbstractNode.

@Seelengrab
Copy link
Owner

Wow, generating a chip's driver from the SVD file sounds like a super powerful idea.

It's not going to generate the entire driver for you (that would be a bit much, considering how many variations of chips there are), but it does give you register definitions and an API for accessing/setting their state.

On the user-side it could look like (obviously very inspired from the Arduino API, something for interrupts should also be added I guess)

I definitely want to have a higher level API at some point, and what you're proposing looks similar to what I've done for the RP2040. Like I said though, I just haven't gotten further in designing this because of issues with getting the chip to initialize properly.

That being said, if I were to implement this multi-function GPIO from scratch, I'd attempt more of an RAII-style interface (kinda like what I describe here), where setting the GPIO function invalidates the previous state of the GPIO, passing a new token object into the following code. I'd have to experiment a bit, since the API would end up very nested, due to all of the involved function calls. Maybe a macro to delineate the thunk would be more appropriate?

The reasoning behind this is that it would help prevent trying to write the wrong kind of data when the GPIO is using a function that's different from what is expected, which would help prevent bugs.

The interface could also provide a few helpers to the implementer of the device driver to signal capabilities of the device (in the style of what Tables.jl does) and simplify implementations:

Some of these should be fairly trivial to implement, though things like iscompatible sounds like a better fit for simply making sure that only devices of a certain type can accept a given (typed) mode to operate in at all. This way, you get a compile time error rather than broken functionality due to misuse.

For the method ambiguities coming from IO, isn't it mostly due to inheriting from Base.IO?

Yes, that's the entire reason :)

Cannot we simply lift that requirement without committing type-piracy, since we own the type?

Subtyping IO is not piracy precisely because we own this specific subtype, and neither is defining a write or read method. The "ambiguity" occurring there is just a bug in the dispatch algorithm in Julia. The reason I'd like to just subtype IO is because it gets us a bunch of printing/writing machinery (also from external packages, like various loggers or StyledStrings.jl) for free. I'd rather not reimplement all those niceties for every kind of embedded IO :)

@Klafyvel
Copy link
Author

Klafyvel commented Jan 6, 2025

That being said, if I were to implement this multi-function GPIO from scratch, I'd attempt more of an RAII-style interface (kinda like what I describe here), where setting the GPIO function invalidates the previous state of the GPIO, passing a new token object into the following code. I'd have to experiment a bit, since the API would end up very nested, due to all of the involved function calls. Maybe a macro to delineate the thunk would be more appropriate?

That's interesting. I'll have to study a bit more, but I have a few atmegas and attinys at home so I think I'll try experimenting a bit in the coming weeks/months on that depending on how much free time I can spend on it. Do you mind if I come back to ask for your opinion after a few trials? It seems you have thought about it a lot.

About the IO inheritance thing, I see your point. It feels like it would be solved if the IO interface was properly defined in Julia, a bit like the iterable interface is. In the mean time maybe we can do without the bonuses it brings.

@Seelengrab
Copy link
Owner

Do you mind if I come back to ask for your opinion after a few trials?

Sure - actual experiments with code are bound to be more fruitful than whatever I could come up with theoretically :) One problem I can foresee for example is lots of dynamic dispatches messing up the compilation, since that bit me in the past as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants