Repository navigation
New MicroPython hardware API #1430
Description
Edit: for easy reference, the proposed new API is at https://github.com/micropython/micropython/wiki/Hardware-API
Since we have the WiPy TODO list (#1425) Damien and I spent some time today discussing how to do things properly, once and for all, and do it before the WiPy ships. It involves a redefinition of the API in many aspects and it has implications all over the place. I'll do my best to layout all the points we touched:
Use attributes instead of functions when we simply need to get/set things. I am going to quote @dhylands on issue Functions vs attributes in object API #378.
I think that to use attributes then you need to use nouns. If you can't represent it as a noun, then I think it should stay as a function
So for instance, pin.value() will change to pin.value to get it and pin.value = x to set it. This will be the way to go in other peripherals, e.g. timer.frequency, timer.period, etc.
Ruled out becaue attributes have a few drabacks which are a deal breaker.
- The whole
PinAPI will look like this:
# create & init a pin
# id, mode are mandatory and positional (also can be named)
# pull is optional and positional (also can be named)
# rest of args are kwonly
# only "value" is required for a port to implement (initial value if given)
# rest are optional, and a port can define them and also define others
pin = Pin(id, mode, pull=Pin.PULL_NONE, *, value, drive, slew, ...)
# constants
# OPEN_DRAIN is much more descriptive than OD
for mode: Pin.IN, Pin.OUT, Pin.OPEN_DRAIN, Pin.ALT, Pin.ALT_OPEN_DRAIN
for pull: Pin.PULL_UP, Pin.PULL_DOWN, Pin.PULL_NONE
optional pull: Pin.PULL_UP_STRONG, Pin.PULL_DOWN_WEAK, ...
for drive: Pin.LOW_POWER, Pin.MED_POWER, Pin.HIGH_POWER
for slew: Pin.FAST_RISE, Pin.SLOW_RISE
pin.init(...) # re init
pin.high() # set high
pin.low() # set low
pin.value() # get value
pin.value(x) # set value
# toggle
pin.toggle()
# functions to get/set most common settings
# this duplicates a bit what init can already do, but it is useful in the case of pins
# for instance when bit banging like it's done by the one-wire driver,
pin.id() # this one is only a getter
pin.mode()
pin.pull()
pin.drive()
...- In general then any extensions to a function/method can happen in 3 ways:
- the port provides extra constants for an existing arg (eg Pin.PULL_UP_WEAK).
- the port provides extra keyword args.
- the port provides extra functions/methods.
- A new module called
hardwarewill be created to replace thepybmodule.pybwill still exist in the pyboard in order to provide backwards compatibility and to avoid confusions between the old and the new API. - The new
hardwaremodule will containPin,SPI,UART,I2C,Timer,CAN, and so on. The new hardware classes will use attributes where applicable.- A new module named
machinewill hold all functions related to reset, interrupt and sleep management. - The
UARTalready supports the stream protocol and we would like to bring it toSPIandI2Cas well. Right now we have the recv functions that accepts either an integer or a buffer object, which is kind of weird and can get confusing. If we makeSPIandI2Cstream capable, then we haveread/readinto/readchar,write/writechar. This will make the API across communication peripherals consistent and homogeneous. One problem of the stream approach is how to select the slave (for SPI) or the destination address (for I2C). We had two ideas:- Simply add kwarg to read/write like
addr,memaddr,slave_addr, etc. This will deviate from the standard stream API. - Create and
endpointsubclass which is the one actually providing the read/write methods and holds the information relevant to the destination like chip select, i2c address, addres size, etc. Doesn't solve the issue of having to add kwarg to the read/write methods in the case of thememaddrfor I2C (not practical to have an endpoint for each memaddr).
- Simply add kwarg to read/write like
I prefer option 1...
It's a lot to discuss in one ticket, but we think it's better because all things are related and it shows the complete picture.
The idea is to have this new API implemented (for the WiPy at least) in 2 weeks...
Activity
dpgeorge commentedon Aug 16, 2015
There are still some leftover functions/classes in pyb that don't yet have a home (eg LED, Servo, Accel, USB stuff). But these are not present on the WiPy so deciding upon them is not urgent. They will stay in pyb for now. Most likely the USB stuff will move to a separate module, so that boards that have USB will have this module, and boards that don't, won't.
danicampora commentedon Aug 16, 2015
I think that the whole
hardwareword already lets you know that what you see there relates to the hardware you are running MicroPython on. Your board doesn't have USB? then it doesn't show in hardware. You can dodir(hardware)and see what's supported. So, USB and SD (SDcard) which are very common but some boards have it and some not, should fit well into thehardwaremodule. MaybeLEDtoo...Specific stuff like
ServoandAccelcan go into something likepyboard. The WiPy for instance has aHeartBeatclass. This could go intowipy.dhylands commentedon Aug 16, 2015
Things like UART and SPI are low level peripherals.
Things like Servo and LED are built on top of this. I'd like to see Servo and LED be much more flexible than they currently are. Currently it's impossible for the user to add additional Servos or LEDs to the ones which are "builtin". In theory I should be able to instantiate a Servo and associate it with a timer and/or a pin. For Servo and LED there are each a couple of different kinds.
1 - For Servo, you can have a Servo which is tied to a PWM output (much like the current pyb.Servo class). But you can also use a single timer to drive multiple servos which are connected to arbitrary GPIOs that don't necessarily have a PWM output (this is the model that teensy uses).
2 - Similarly, for LED, you can have a simple on/off LED, or you could have a PWM controlled LED.
I think that designing some of these "higher level" classes to be a bit more flexible makes them much more usable across various boards and board setups.
dpgeorge commentedon Aug 17, 2015
Regarding attributes vs functions (as previously discussed in #378, but this point I'm making here was not raised in that issue): the big downside of using attributes is that assignment is a statement and not an expression, so you would not be able to set the pin value (or do other attr assignments) in a lambda. Eg you can't do:
With functions acting as getters/setters (based on whether 0 or 1 argument is supplied), which is how things work at the moment, you can do:
I think this is a deal-breaker for using attributes. Ignoring the implementation details and/or efficiency, and looking instead from a user/programmability point of view, the API should always use functions because they are expressions (more powerful than statements) and you can take a "pointer" to a function (eg pin_value_fun = pin.value) but not an attribute.
So, even though it's more Pythonic to use attributes for things that are clearly accessed using a getter and a setter (eg pin.value=1), I propose that we instead use functions that take 0 or 1 args (eg pin.value(1)).
danicampora commentedon Aug 17, 2015
OK, let's only use functions then.
pfalcon commentedon Aug 17, 2015
I'm glad that we came to the same conclusion as we did before ;-).
dpgeorge commentedon Aug 17, 2015
Yes indeed. But since the topic here is to discuss a complete revamp of the API, I think it's useful that we had the discussion (again, but in a new light).
danicampora commentedon Aug 17, 2015
Just updated the original post scratching all the attribute ideas.
dpgeorge commentedon Aug 17, 2015
So do we then want to provide a "call" method as a faster way of getting/setting the pin:
(this will be in addition to having the pin.value method)?
Rationale: pin getting/setting is a very common operation so benefits from a shortcut (shorter to type, smaller script, smaller bytecode). Also, it's common to bit-bang using pins, and doing a call is much faster than looking up the method (pin() is roughly 3 times faster than pin.value()).
danicampora commentedon Aug 17, 2015
Even though it might seem a bit adhoc, and that it also duplicates what
pin.value()already does, given the advantages, I think that in this case is a good idea and we should have the call method to set/get the value of a pin.danicampora commentedon Aug 18, 2015
Right now there's a
pyb.mount()function which works like this:I propose to move this function to the
osmodule and remove themkfsoption. So it ends up being:and then add a separate function, also in the
osmodule, to make a file system, i.e.:The required parameter can be a device just like the one accepted by
os.mountor a path in case that the device is already mounted (like/flash). Maybe we need some keyword only arguments for extra option(s), maybe not.Thoughts?
danicampora commentedon Aug 20, 2015
@dpgeorge asked me to create a wiki page to show how the new API will look like. I created it, and added the
Pinclass: https://github.com/micropython/micropython/wiki/Hardware-APII've also implemented the new API for the WiPy. For now is in my personal repo:
danicampora@0061f9f
pfalcon commentedon Aug 20, 2015
So, I saw https://github.com/micropython/micropython/wiki/Hardware-API (is that right place to do documentation? docs should be in RST form in repo/on a site, RFC is in tracker, not sure how overflow to wiki happens), and wonder: how did we end up with yet another "hardware" module? Last time it was discussed we seemed to agree that "machine" would be a place to transition elaborated APIs to (from "pyb").
danicampora commentedon Aug 20, 2015
OK, sorry, I corrected my post. The documentation itself will be done via sphinx just as before, the idea of the wiki is to show how the new API will look like before actually implementing it. It's easier to see it there than via the discussion in this PR. We can remove that link when the API is finished.
Well, the new idea is to use
machinefor reset, interrupt, power management, and memory access.hardwarewill contain the peripherals.Don't you think it looks cleaner this way? ;-)
pfalcon commentedon Aug 20, 2015
Not sure, I don't see clear pattern what should go into one module vs another. And having 2 modules instead of 1 takes more memory for boilerplate code.
302 remaining items
tannewt commentedon Oct 12, 2016
@dhylands mbed support sounds really cool! I can't wait to see it.
deshipu commentedon Oct 12, 2016
@tannewt you gave me a crazy idea: write the whole
machinemodule in Python, using a_machineC module with very minimal and unpythonic API bindings for the hardware.tannewt commentedon Oct 12, 2016
@deshipu thats one way to do it! :-)
profra commentedon Oct 13, 2016
@dhylands ++1 for mbed and Teensy 3.5/3.6
dpgeorge commentedon Oct 18, 2016
@deshipu thanks for the feedback. Here are some comments.
This is "just" an implementation issue and we should separate this from the discussion of the user facing API. But see now that stmhal is using the common Python bindings from extmod for some of the machine.SPI methods.
The question here is: should extensions be done as extra methods / arguments to existing machine classes (as I proposed many times), or put in separate modules/classes. I think the only way the latter can work is if all extensions are functions that operate on objects from the machine module, eg
stm.background_dma_transfer(spi, write_buf, read_buf). To me that is quite ugly.I agree. This is how stmhal works and it should be how the other ports work as well. But for software implementations, you can create as many instances as you like (and they will all be independent).
stmhal works like this. But it's mostly an implementation detail. I agree though that the "truth" should always be displayed.
There is no doubt that sending/receiving a single byte is common. In the original stmhal pyb API it allows either buffers or integers (acting as single bytes) in transfer functions. When designing the machine API we thought it's best not to overload the types of the arguments like this, and also didn't want to add heaps of extra byte-methods like write_byte, read_byte. But maybe these decisions should be revisited. Probably allowing to overload the argument (either a buf or int) is a good way to do it, but that doesn't help with reading single bytes which would still need a separate method.
The idea of endpoints has been discussed a few times before. Having SPI and I2C endpoint classes that have equivalent method signatures and behaviour is a good idea. Putting these classes in a separate module sounds good, but then if all drivers use such a module, that module needs to be included in all ports anyway. It's important to find a "minimal common denominator" upon which drivers are written / rely upon, so that all ports can use the drivers (otherwise we are back at square one regarding complexity and may as well put everything in the machine.SPI / I2C class to begin with).
Yes I agree that it should not re-set all the settings. That's how some of the classes work (eg new stmhal machine.SPI, also esp8266 SPI).
Regarding the name: init() was chosen to be symmetric with deinit(), but I guess it could easily be config() and close(), and reset() could be added where appropriate.
The reason they shouldn't be called read/write is because of duck-typing: methods should be called the same only if they behave the same. In this case, read() should behave like file.read() and write, like file.write(). Since they take address arguments they can't have the names read/write. read/write can be used for the endpoint methods where the address is already specified in the constructor.
Yes I agree with this. I don't think there's any scenario where you send a start bit that's not followed by the address.
As mentioned by @tannewt these should probably go in the higher-level class (ie the endpoint). They could be replaced in the low-level I2C class by a single method that does write-followed-by-read: i2c.write_read(address, wr_buf, rd_buf).
Yes I agree, and this is how it now works (or should work). esp8266 and stmhal's machine module don't have a mode anymore (if they do it's a bug).
I think the way it's defined at the moment is pretty clean: machine.SPI(sck=p0, miso=p1, mosi=p2) creates an SPI instance on the given pins. It will use hardware if possible, else software. If you want a specific HW instance then name it, eg machine.SPI('X').
Yes RTC needs to be fixed. The problem is that old pyb.RTC propagated to machine.RTC and hasn't yet been cleaned up.
How to deal with channels on these objects, in a way that works across many ports, is still an unsolved problem.
Please raise issues specifically for this because I thought it was pretty much fixed now.
deshipu commentedon Oct 18, 2016
Well, I have seen several pull requests being shot down recently on the basis of not being compatible with the hardware API, precisely because they added extra methods, optional arguments or additional constants. If this is really the preferred way, perhaps it could be documented, so that authors of the pull requests can point to that in their defense?
As for putting the extensions in separate classes, I think that wrapping a class from
machinein an "advanced" class, adding the extra methods and arguments, could work quite well -- either through inheritance, or through composition (passing an instance of the wrapped class to the wrapper's constructor). Often the "advanced" versions wouldn't even share any code with the machine versions (like the DMA stuff, which would use different calls), so they would be completely separate classes.tannewt commentedon Oct 18, 2016
Theres a lot to talk about in the last few replies but I want to focus on the organization question only.
Putting all of the python related code into a single place that is port agnostic isn't just an implementation issue because it forces certain properties of an API. In particular, it forces the API to be identical across ports. I think that is a very good thing. Having identical APIs for modules of the same name is hugely beneficial in Python because it reduces the risk of RuntimeErrors. That is why I think all method and class definitions should be shared. Consistency is hugely important.
deshipu commentedon Oct 20, 2016
Duck typing applies to interfaces that do the same, or an equivalent thing. An
i2c.read()doesn't and can't possibly do the same thing asfile.read(). You can't just pass an I2C bus object to something expecting a file and expect that to work -- the whole protocol is based on transactions, and that doesn't map to UNIX file semantics. It could perhaps work if you had that "I2CDevice" endpoint class. Perhaps we should go back toi2c.recv()or something similar then? That's at least consistent with what Arduino had.felixdivo commentedon May 30, 2018
It would be great if
Micropythonwould use this opportunity to also introduce CAN (Controller Area Network) support. Over at python-can, we figured that our library is probably not the right place to provide that support, but this project is. (see python-can #93)mgiaco commentedon Jul 23, 2018
Hello,
Dose anyone see a chance to use any USB-TO-CAN adapter (maybe over python-can) on a micropython windows build?
many thanks
peterhinch commentedon Jul 24, 2018
These questions will find a bigger audience in the forum. There are people using CAN bus with the Pyboard which has two CAN channels.
mgiaco commentedon Jul 24, 2018
okay many thanks.
Merge pull request micropython#1430 from adafruit/header-logo
dpgeorge commentedon May 21, 2019
I'm going to close this issue, I don't think there's anything to gain from keeping it open. The
machinemodule has been around for a while and some classes are settled, while others are still being developed, and their development/discussion should happen in separate, dedicated issues.