-
Notifications
You must be signed in to change notification settings - Fork 206
refactor: add drivers.Pin interface #749
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
…y to do so Signed-off-by: deadprogram <[email protected]>
I think defining |
I prefer a leaner and clearer API. I arrived at an API which is functionally equivalent but splits input from outputs: // PinInput is hardware abstraction for a pin which receives a
// digital signal and reads it (high or low voltage).
type PinInput func() (level bool)
// PinOutput is hardware abstraction for a pin which outputs a
// digital signal (high or low voltage).
type PinOutput func(level bool) Cons:
Pros:
pin := func(level bool) {
reg := (*volatile.Register32)(unsafe.Pointer(rp.RP_BANK_SIO))
reg.Set(1<<level)
}
dev := pca1x.New(pin) Note on compatibility of change: We are breaking API compatibility with any change we perform since lots of drivers call |
Hmm, not too sure that sounds like a very good idea.... |
Help me out Ron. The more I think about this the more this is outweighed by the benefits of function pointer API. We are very lucky to be using a statically typed language. It will notify users of API breakage and where and if we add context, we can also let users know how to fix it. I'd go as far as to say the risk is less with breaking users at compile time. If we preserve API compatibility of machine.Pin we are breaking our users silently. This is perhaps the worst breakage. Programs will stop working. Over the coming years users will drop in to slack noting their programs stopped working in the strangest ways. Peripherals stopped responding after running go mod tidy. Maybe they didn't notice the peripheral stopped working (we are lacking in the error handling department in many drivers) until something physical breaks. I can't imagine how this is better than breaking the API and giving users context in the API on how to fix it... I mean, they have to fix it one way or another, best let them know from the get go! Unless I am missing something big API breakage in this case is better than preserving API compatibility since we break our users regardless due to the missing |
I think the idea with introducing "backwards compatible" interface was exactly not to break things for people? I see typical (yet unconventional in world of classic embedded tinygo) use case to be people write their code for specific platform and implement abstractions, like working with pins. I did it for my RPi project. For example, take |
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.
I like this proposal better than function handlers. Also consistent with other interfaces.
(See code below if this is not clear) Before this change drivers were initialized with
Yes, this is not an issue for users who are working with the
The change required for your project is as follows:
dev := st7735.New(spi, rst, dc, cs, bl)
// By making pin HAL users are now required to initialize their pins. This is the breaking change.
bl.Configure(machine.PinConfig{Mode: machine.PinOutput})
dc.Configure(machine.PinConfig{Mode: machine.PinOutput})
cs.Configure(machine.PinConfig{Mode: machine.PinOutput})
rst.Configure(machine.PinConfig{Mode: machine.PinOutput})
dev := st7735.New(spi, rst.Set, dc.Set, cs.Set, bl.Set) // Set implements function interface. Let's sit down and ask ourselves the cost of this change before pondering the benefits (which are great!). Can we mitigate these costs? I've proposed one way of doing so with a different API which would notify users of a breakage that would otherwise go unnoticed, which is to say the only Con I've written down above is doing its part as a benefit to our users. |
I think we should always keep TinyGO as simple as possible. If the goal is to isolate drivers from machine, Although function handlers approach may be more powerful, I'm not sure this justifies the extra complexity. |
My main problem with function handlers approach is it is not consistent with other abstractions that we have already, like SPI, I2C, Displayer interfaces, et al. Having two types of abstractions brings much more confusion in long run, imho. Yes, I hear argument about breaking users silently, it's an important one indeed. |
Another thought. For pins it's usually just one, a write or a read, of course. |
I've gained another worry in the last few hours regarding this PR. I've recently been looking at lots of C drivers to write up my driver design document. When looking for a C driver for a device it is common to find something in the order of 3-4 drivers for a single peripheral. More often than not you'l find the quality of these drivers and the abstractions they use vary greatly. Luckily today the Go community has rallied around the tinygo/drivers repository as a central source of drivers it seems. The HAL designed by TinyGo is second to none, it really got it right in many aspects, all tracing the roots back to:
I worry that the And if we really do go the way of having both We seem to be worried about users having to correctly set the function pointer which means basically "replace Interface vs function pointer benchmarkpackage main
import (
"machine"
"time"
)
func main() {
// Register more types with drivers.Pin interface to measure impact of virtual method call with many implementing types.
impl1 := &pinimpl[uint8]{}
d2 := driver{ipin: impl1}
d2.doIface(10)
impl2 := &pinimpl[uint16]{}
d3 := driver{ipin: impl2}
d3.doIface(10)
p := machine.LED
p.Configure(machine.PinConfig{Mode: machine.PinOutput})
d := driver{ipin: p, fpin: p.Set}
const N = 10000
fstart := time.Now()
d.doFunc(N)
felapse := time.Since(fstart)
istart := time.Now()
d.doIface(N)
ielapse := time.Since(istart)
for {
println("per func call", (felapse / N).String())
println("per interface call", (ielapse / N).String())
time.Sleep(time.Second)
}
}
type driver struct {
ipin ipin
fpin fpin
}
type ipin interface {
Set(b bool)
Get() bool
High()
Low()
}
type fpin func(bool)
func (d *driver) doIface(n int) {
k := true
for i := 0; i < n; i++ {
d.ipin.Set(k)
k = !k
}
}
func (d *driver) doFunc(n int) {
k := true
for i := 0; i < n; i++ {
d.fpin(k)
k = !k
}
}
type pinimpl[T ~uint8 | ~uint16 | ~uint32 | ~uint64] struct {
k T
}
func (p *pinimpl[T]) Get() bool { return p.k != T(0) }
func (p *pinimpl[T]) Set(b bool) {
if b {
p.k = 1
} else {
p.k = 0
}
}
func (p *pinimpl[T]) High() { p.Set(true) }
func (p *pinimpl[T]) Low() { p.Set(false) } Results:
It seems like when there is only one interface implementation the compiler can optimize to a static function call which is even faster than the dynamic function call. As soon as another interface implementation is added the speed drops dramatically, over x3 slowdown and linear scaling slowdown for every implementation added. |
Great write up, @soypat ! And thanks for actual benchmark, super! I don't see anyone having multiple Pin interface implementations for any real use case? |
I've worked on codebases with 3 pin implementations, and that was a small project by the comapany's standards:
The device wasn't even something strange, it was a commonplace industrial machine that is even present in many homes: a 3D printer. All pins controlled motors and an impact on the speed of the I'll note I've been using the pin HAL for the better part of 3 years to solve a great range of company problems in both C and TinyGo. From my viewpoint it feels the pin HAL is in it's infancy in mainstream TinyGo, just look at the drivers, most of them do the same thing, just toggle an peripheral control pin to then do heavy I/O, of course these drivers could do just fine if the pin HAL was excruciatingly slow. Also, saying most people just need a single pin abstraction is jumping the gun a bit, don't you think? |
Heck I forgot the board was going to add a fifth pin HAL as well with a GPIO multiplexer. And I can't guarantee it would've stopped there. |
Kept thinking and there are several use cases in the TinyGo ecosystem that would be affected. Use cases which would negatively be affected by the interface slowdown :
There's certainly more cases. These are just the ones I've run into. |
So... If interface approach can be slow and, worst of all, its speed depends on number of potential matches a compiler can find... I wonder, how this nice little softspi can reliably work then, for example?.. A problem indeed... Can we How you ask? Well, what if we let drivers authors decide how they want it; they are better suited to understand needs for their driver. Internally then, it's up to driver author on how to implement this -- they may accept Good news, btw, in at least several cases touched by this PR the driver constructors return objects, the old way, rather than pointers to objects. We could convert these constructors to return pointers and intentionally break client code to alert users about breaking changes (the need to initialize pins). |
OK, nice! I feel we are reaching some common ground! 😄 I feel like we are close to a compromise between PinFuncs and drivers.Pin but that there are details being overlooked. I suggest all interested parties jump in a call. Best way to organize I feel is through slack on the And juuuuust for completeness...
Well that example is curious indeed. Almost as if it did not use interfaces for pins... which it doesn't 😅 |
This PR is to add a
drivers.Pin
interface and use it everywhere that is easy to do so.Related to work in PR #742 this is intended to help disconnect the drivers repo even more from the
machine
package.The remaining drivers that use
machine.Pin
directly have various complications such as changing the pin config from input to output and back, which will have to be addressed in some different way.