-
Notifications
You must be signed in to change notification settings - Fork 230
PinInput+PinOutput HAL (#753, reloaded) #795
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: dev
Are you sure you want to change the base?
Conversation
Excuse my long comment, but I've tried to cover all aspects of this refactoring we are trying to make. ProblemPackage Requirements for a solution:
ContextGreat Public API is:
In drivers package now we have following bits of the public API:
Some drivers set pin mode (input/output) once in SolutionPin modes & existing codeWhile output mode is more or less same across hardware, input mode is trickier: it can be simple Proposal: Build tags trick (catch "baremetal", check for "machine.Pin" type and set pin mode) can be used to keep existing consumer code working. Breaking dependencyIt had been shown that it is possible to break dependency from the AlternativesAlternative 1:
Alternative 2:
SummaryProposal
Note on false impressionsA worry expressed about potential risk of misunderstanding and false impression of (Tiny)Go being slow and non-suitable for embedded development, I find ungrounded. Sloppy code can be written in any language. Code in C, too, can be slow, if written w/o understanding of system programming, complexity theory and use of proper data types. |
Supposition: Due to performance gains, we need a function HAL exposed to users. The question is if we also want to add an interface HAL for pins. Pros of interfaces
This is the one and only argument in favor of using interfaces I find the slightest bit compelling, though the argument I feel is very debilitated due to the call site usage being identical for PinOutput and even more readable for PinInput. Are we taking a decision because we prefer type definitions that say "interface" over "func" ? I'll also note that the argument "that's the way it's always been done" is not a great argument in the context of tech and embedded systems... especially when we ourselves are working on a project that defies everything about how embedded systems is done today in the industry. Empty arguments in favor of interfaces
We can still do this with an internal pin interface, this PR is such a demonstration of this. Arguments not mentioned against interfaces
We are also sacrificing API consistency if we have function and interface user-facing API. This is likely much more confusing for users than having just a function HAL: which HAL do they choose? We are exposing two HALs that have the exact same purpose. Recall we removed WriteRegister and ReadRegister methods from the I2C interface in the spirit of API consistency: You could do these operations with Tx already. Having two HALs for the same concept goes against the most basic design principles. It is confusing to have two things that overlap over the same concept. We don't have several HALs for other concepts in TinyGo. This PR shows we can literally do without one of these HALs... why are we still having this conversation? Functions: Brick wall preventing bad designs
Don't forget: Benefits of function HAL
ProposalWe know we want the function HAL. Baby steps. Let's start there and see if the interface HAL need be exported. It has currently been demonstrated it need not be exported in this PR. We can always export it in the future without breaking our users. We are compromising on nothing by taking this approach, just delaying the potential addition of the interface HAL if it be required. @ysoldak Insisting on adding the interface HAL by this point seems to me risking a whole lot of disadvantages for "I like when it says interface in the type definition because that's how it's always been done". If this proves to be the sentiment for the general public, we can always add the interface in the future. |
@soypat I think we all agree on this, the confusion is that @ysoldak and me think that function proposal will cause this inconsistency and not the interface proposal. For example, by your proposal, if we want to keep backward compatibility, old drivers will have following API:
and new drivers will have following API (there is no example of implementing new driver so I'm presuming this is your intention):
Both Whereas if interfaces are used, both new and old drivers would all have APIs like this:
I agree that downside of this is that driver developers are not forced to store interface methods in variables and use them for better performance, but if all drivers are rewritten in this fashion, it would be easier for them to copy this pattern. Another concern I have with function HAL is how would API for drivers that use the same pin for both input and output look like. Would the Driver constructor take two arguments of type drivers.PinInput and drivers.PinOutput like this:
and in user code both arguments would be created using the same pin? In interface case the API would look like this:
which in my opinion looks much more logical EDIT: added example of driver implementation |
@HattoriHanzo031 I'll try to address all your points:
The first sentence of my comment states the reasoning for this. We need to expose the function HAL so third parties who develop drivers are informed on TinyGo's preferred HAL (for performance reasons). We don't need to expose the interface HAL, as shown in this PR.
No. There is nothing in this PR that demands a new API for new drivers. Driver designers can choose the constructor API as they please.
From the perspective of driver users, they will be able to pass in a machine.Pin to the constructor, They need not know of the function API. These are the users who do not develop drivers. From the perspective of anyone else who uses TinyGo for driver development they will use the drivers package. They need the function HAL but nothing requires they use the interface HAL. This way we get a compromise:
Recall: the users of the "tinygo.org/x/drivers" package are driver developers, not driver users. We can serve both the best with this compromise, and not risk not being able to add the interface in the future if need be.
No one is suggesting breaking backwards compatibility. There is absolutely no reason to break backwards compatibility. We can still even choose to adopt this method of developing drivers with function HAL and not interfaces internally, but expose the pin.Input to users. This is a good compromise for the reasons detailed above.
Yes! Sounds great! This PR shows how this could work!
Here's a small demonstration of how that would work!
Please explain why |
Interface HAL in this PR is exposed to users even if it is internal because it is used in the user facing API. If those internal interfaces are changed they would break the API compatibility, hence they are exposed.
I am not sure I understand this statement, isn't there more people using than writing drivers? Maybe the main point of disagreement is should drivers API be optimised for implementers or driver users?
Now I'm not sure I understand the intention of making function HAL public. As I mentioned previously there is no example of implementing new drivers so I was presuming that the intention of making function HAL public is to use it in (some or all) future driver constructor APIs in this drivers package. If this is the case, when in future some driver designers choose constructors with interfaces and some choose constructors with functions, that would create inconsistency. If this is not the case and the function HAL is intended only to be used inside the drivers and not in APIs can you elaborate the reason for making function HAL public?
If the main reason to make function API public is to communicate to third parties how would TinyGo prefer they write drivers in their own repos, I don't see it as strong argument. Third parties do not have to follow any pattern from drivers package, or worry about compatibility with drivers package. They can develop the drivers any way they want, and also use some third solution (for example generics) that works the best for their use case. For example, in big Go there are many third party JSON libraries, but most of them do not have the same API as the one from standard library.
Your example is completely fine (and preferable) if the pin should only be used as input, but as I mentioned, my concern is about the drivers that must use the same pin for both input and output (like onewire and dht) in which case your example would not work because |
I've commented above but restating since the point seems to not be getting across: you comment is true for all of tinygo interfaces, Go standard library interfaces and third party interfaces. Any user facing interface which is modified has the potential to break user code. You aren't really saying anything that is not true for an arbitrary exported interface, like is the case for the interface you are also pushing for.
The point is illustrated with this PR explicitly. Someone who uses the uc151 driver in this PR has absolutely 0 contact with the drivers.PinInput HAL. They instead come in contact with In doing this we optimise for both- users can have the luxury of passing in a
To obtain benefits detailed so clearly above
There is a driver for the uc151 included and #753 contains over 20 drivers as an example.
A result of design decisions:
Function HAL can be used wherever users or driver developers want. One thing is for sure- it is the better HAL for now. In TinyGo we seem to prefer better, even when it "is not how it has always been done".
I disagree on the basis of collaboration and fostering a coherent ecosystem where code is reusable from project to project. The Pin abstraction is particularily suited to HAL composition. We could theoretically architect the following stack that sits on a single pin:
Ideally in the future we'd be comfortable with the idea of using a single Pin HAL for everything, so passing in machine.Pin.Set to constructors (come on guys, it is really not that bad). The function HAL is also more flexible and can receive a interface easily! The same does not go for a constructor that receives an interface. The list of reasons for a function HAL only get longer by the day. And even if you don't agree with this point, all the other benefits detailed in my comment above should be enough to convince you the function HAL is simply better in ever technical aspect. I'll remind the reader by now we are on the fence regarding function vs. interface because "interfaces are how it has always been done".
Your point illustrates perfectly why the interface HAL for constructors is not well suited for solving the more complex use cases easily without creating new types and adding their methods+data wheareas the function HAL can be solved inline at the call site. Maybe yet another point for function HAL. More to the list. Thank you all for your comments and for your valid worries with this PR. I hope I answered all your questions! |
If I was to summarise this discussion I would say (and correct me if I'm wrong) that in general we are all advocating for the same thing which is to introduce both function and interface pin abstractions, with major difference being which abstraction would be public and which would be internal. This PR suggests having function abstraction public (but mostly not used in the API functions) and interface abstraction internal (but used in the API functions), @ysoldak's PR have both abstractions public (with only interface abstraction being used in API functions) and I am advocating for having interface abstraction public (and used exclusively in the API) and function abstraction internal (used for performance optimisation in the driver implementations). For easier further discussion, I created draft PR with PoC of my suggestion which is based on this PR with function and interface HAL switched places (function HAL being moved to internal and interface HAL being public). I also refactored onewire, dht and uc8151 drivers in a way I think it would work with this suggestion. One thing that is still not clear to me is how would onewire driver be refactored to eliminate machine package and also preserve backward compatibility and performance after this PR is merged. @soypat can you please also refactor onewire driver in this PR (or a branch) so that is clear how do you suggest that this class of drivers (where the same pin is used both as input and output) should be implemented. I refactored onewire, dht and uc8151 drivers in my PR in a way I think it would work with my suggestion. |
Another concern, which is unrelated to function vs interface HAL discussion because it affects both equally, is the offload of pin configuration burden to user code. Again, this is not a big problem if each pin should be configured only once and then used like that in the driver, but what about the cases like onewire and dht where pin must be configured during the driver operation? If you look at the code, the pin Set and Get functions should be implemented differently for onewire and dht drivers. To demonstrate this I created new implementation of onewire driver (onewire_v2) simulating some future driver that has to have the same pin be input and output. As you can see from the example the user code becomes much more complex and as if user must understand inner workings of specific driver. We could create some helpers for this cases in drivers package, but since the configuration can vary from driver to driver, it could be challenging to cover all cases. I don't think we must have the solution for this problem right now, but maybe it would be good to at least have awareness of it and to have a general idea how to deal with it. |
This seems to be the major difference, yes.
Thank you for the PoC.
Sure thing. Here's the code for a bit-bang one-wire interface with the proposed HAL.
The main value proposition of this entire PR is for tinygo drivers to be usable with native Go code on other embedded system projects. This requires burdening the user with pin configuration, as you've correctly observed.
I'm not too familiar with the intricate differences between the one-wire and dht protocol, but a cursory search on google shows that the DHT sensor uses a proprietary protocol, so it would make sense to implement different HALs for each, at least at a glance. What is the issue?
Check out my one-wire implementation- it relies on the assumption the pin is configured as pull up when input called, otherwise respecting the simplicity of the documented pin HAL. As a result, my implementation is very simple and requires no explicit pin configuration, compared to yours. Also regarding your PR, we will most certainly not have a As a final noteCall for feedback: If you believe an interface is superior, please bring a concrete counter-example or benchmark (perf, code size, or ergonomics) that the function HAL can’t match. Otherwise, let’s move forward with the function HAL and keep interfaces as internal adapters. In the absence of concrete counter-examples, I'd suggest we all start to get comfortable with the idea of a function HAL and start thinking of the best way of adopting such a HAL going forward. |
This PR is a redo of #753 which was too damn long for any person to understand.
It now includes more comments detailing benefits and the why of some of the ideas in previous PR.