Skip to content
Paul Sokolovsky edited this page Oct 17, 2015 · 115 revisions

This is the current proposal for a new hardware API. It is a work in progress.

Design goals and priciples

The main aim is to provide Python modules/functions/classes that abstract the hardware in a Pythonic way.

The API should be Pythonic, obvious and relatively minimal. There should be a close mapping from functions to hardware and there should be as little magic as possible. The API should be as consistent across peripherals (Pin, UART, I2C, ADC, etc) as possible. There should usually be only one way to do something. A method name should do exactly what it says and no more (ie it shouldn't be heavily overloaded).

The existing pyb module already provides such abstraction but it is not as clean or consistent or general as it could be. The new hardware API will co-exist alongside the pyb module (only for stmhal port, eg pyboard) so that existing scripts still run. The pyb module will eventually be deprecated, but only after all functionality finds another home (which may be some time).

Use cases

Create a UART with various pin configurations:

uart = UART(0) # reference (existing) UART(0) entity, without (re-)initialising it
uart = UART(0, 9600) # create and initialise UART(0) using default pins, no flow control
uart = UART(1, 9600, 8, pins=('GP1', 'GP2')) # specified pins, no hardware flow control
uart = UART(1, 9600, 8, pins=('GP1', 'GP2', 'GP7', 'GP6')) # RTS/CTS flow control
uart = UART(1, 9600, 8, pins=('GP1', None)) # Tx only
uart = UART(1, 9600, 8, pins=(None, 'GP2')) # Rx only
uart = UART(1, 9600, 8, pins=('GP1')) # raise 
uart = UART(1, 9600, 8, pins=('GP1', 'GP2', 'GP7')) # raise
uart = UART(1, 9600, 8, pins=('GP1', 'GP2', 'GP7', None)) # OK, RTS only

Create a UART and enable pull-up on the pins:

# create and initialize UART1 with TX and RX on GP1 and GP2 respectively.
UART(1, 9600, pins=('GP1', 'GP2')) 
# enable the pull-ups on both UART1 pins
Pin('GP1', mode=Pin.ALT, pull=Pin.PULL_UP)
Pin('GP2', mode=Pin.ALT, pull=Pin.PULL_UP)

These use-cases need to be written:

  • Basic I/O on a pin
  • PWM on a pin
  • ADC on a pin
  • Using a Timer to do a one-shot callback
  • Using a Timer to do a repeated callback

General conventions of the API

You make objects corresponding to physical entities in the MCU (eg Pin, UART, Timer). Some entities like UART can be connected to Pin's.

Conventions are:

  • Selection of a peripheral/entity is done by an id. A port must allow to specify ids by a sequential integer starting at 0 and going to MAX (MAX is a constant defined in the periph class). Ports can optionally allow to specify periphs by a string (this allows to write portable code across boards that provide similar periphs, specify by MCU name, etc).

  • All periphs provide a constructor, .init() and .deinit(). The Pin and the IRQ class are the exception here, Pin doesn't provide .deinit() because a single pin cannot be completely de-initalized, and also because of the dynamic nature of pins which can be used as GPIO and also by other peripherals.

  • Peripherals should provide default values for all the initialization arguments, this way when calling .init() with no params, or the constructor with only the peripheral id (or no id at all, then the default one is used), it will be initialized with the default configuration.

  • If a peripheral is in the non-initialized state, any operation on it except for .init() shall raise OSError with EBUSY code.

  • When connecting a periph to some pins, one uses the pins= keyword in the constructor/init function. This is a tuple/list of pins, where each pin can be an integer, string or Pin instance.

In the method specs below, NOHEAP means the function cannot allocate on the heap. For some ports this may be difficult if the function needs to return an integer value that does not fit in a small integer. Don't know what to do about this.

OSError exceptions regarding peripherals

  • In case that the peripheral is not available (invalid id) ??
  • Operations on non-initialized peripherals ??

New machine module

The classes to control the peripherals of the board will reside in a new module called machine, therefore, by doing:

import machine
dir(machine)

one can easily see what's supported on the board.

IRQs

An interrupt request (IRQ) is an asynchronous and pre-emptive action triggered by a peripheral. Peripherals that support interrupts provide the irq method which returns an irq object. This can be used to execute a function when an IRQ is triggered, or wake up the device, or both.

peripheral.irq(*, trigger, priority=1, handler=None, wake=None)
  • All arguments are keyworkd only, to be able to adapt to the different nature of each peripheral.
  • trigger is what causes the interrupt, for instance a Pin object accepts Pin.IRQ_RISING, Pin.IRQ_FALLING, etc. Trigger sources can also be ORed together, like Pin.IRQ_RISING | Pin.IRQ_FALLING.
  • priority it's just a number from 1 to N (should each port define it's own N?). The higher the number, the more priority it gets.
  • handler is the function that gets called when the interrupt is fired.
  • wake specifies if the irq can wake the device from any of the sleep modes, e.g. machine.SLEEP or machine.DEEPSLEEP, and they can also be ORed together.

The irq is always enabled when created. Calling the irq method with no arguments simply returns the existing object (or creates it for the first time) without re-configuring it, just as with any other constructor.

The created irq object supports the following methods:

  • irq.init() re-init. The interrupt will be automatically enabled.
  • irq.enable() enable the interrupt.
  • irq.disable() disable the interrupt.
  • irq() manually call the irq handler.
  • irq.flags() get the triggers that caused the current irq. Only returns useful values when called inside the irq handler. The flags are cleared automatically when leaving the handler, therefore, this method always returns 0 when called outside.

Signature of an irq handler: def my_handler(peripheral)

Example:

def pin_handler(pin):
    print('Interrupt from pin {}'.format(pin.id()))
    flags = pin.irq().flags()
    if flags & Pin.IRQ_RISING:
        # handle rising edge
    else:
        # handle falling edge
    # disable the interrupt
    pin.irq().deinit()

The Pin class

Create and init a pin

pin = Pin(id, mode, pull=Pin.PULL_NONE, *, value, drive, slew, alt, ...)

  • id, and mode are mandatory and positional (mode can kw).
  • pull is optional and positional (also can be named).
  • The rest of args are kwonly.
  • Only value is required for a port to implement (initial value if given).
  • The rest are optional, and a port can define them and also define others.
  • alt specifies the alternate function, it is optional and the values it takes vary per port. Exists to allow advanced pin operations for ports that support it.

Methods

Do we really need 3 ways to set the value?

  • pin.init(...) re init.
  • pin.value() NOHEAP; get value (returns 1 or 0)
  • pin.value(x) NOHEAP; set value (value can be any valid Boolean expression)
  • pin() NOHEAP; fast method to get the value of the pin.
  • pin(value) NOHEAP; fast method to set the value of the pin.
  • pin.toggle() NOHEAP

Getters and setters

  • pin.id() NOHEAP; get only. - need to clarify what this really returns. I'm assuming a board ID.
  • pin.mode([mode]) NOHEAP
  • pin.pull([pull]) NOHEAP
  • pin.drive([drive]) NOHEAP
  • pin.slew([slew]) NOHEAP

Constants

  • for mode: Pin.IN, Pin.OUT, Pin.OPEN_DRAIN, Pin.ALT, Pin.ALT_OPEN_DRAIN
  • for pull: Pin.PULL_UP, Pin.PULL_DOWN, optional: 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
  • IRQ triggers: pin.IRQ_RISING, pin.IRQ_FALLING, pin.IRQ_HIGH_LEVEL, pin.IRQ_LOW_LEVEL

The UART class

uart = UART(id, baudrate=9600, bits=8, parity=None, stop=1, *, pins, ...)

  • pins is a 4 or 2 item list indicating the TX, RX, RTS and CTS pins (in that order). Any of the pins can be None if one wants the UART to operate with limited functionality. If the RTS pin is given the the RX pin must be given as well. The same applies to CTS. When no pins are given, then the default set of TX and RX pins is taken, and hardware flow control will be disabled. If pins=None, no pin assignment will be made.

Methods:

  • uart.init(...)
  • uart.any() return the number of characters available for reading.
  • uart.read([nbytes])
  • uart.readinto(buf[, nbytes]) NOHEAP
  • uart.readall()
  • uart.readline([max_size])
  • uart.write(buf) NOHEAP
  • uart.sendbreak() NOHEAP

The I2C class

i2c = I2C(id, mode, *, baudrate, addr, pins)

pins is a tuple/list of SDA,SCL pins in that order (or should it be SCL, SDA to be consistent with the clock first for SPI?).

Master mode:

  • i2c.scan() search for devices present on the bus.

Master mode transfers:

  • i2c.readfrom(addr, nbytes, *, stop=True)
  • i2c.readfrom_into(addr, buf, *, stop=True) NOHEAP; stop is if we want to send a stop bit at the end
  • i2c.writeto(addr, buf, *, stop=True) NOHEAP; stop is if we want to send a stop bit at the end

Master mode mem transfers:

  • i2c.readfrom_mem(addr, memaddr, nbytes, *, addrsize=8)
  • i2c.readfrom_mem_into(addr, memaddr, buf, *, addrsize=8) NOHEAP
  • i2c.writeto_mem(addr, memaddr, buf, *, addrsize=8) NOHEAP

For master transfers we don't use read/write names because the type signature here is different to standard read/write (here we need to specify the address of the target slave). Other option would be to provide i2c.set_addr(addr) to set the slave address and then we can simply use standard read/readinto/write methods. But that introduces state into the I2C master (being the slave address) and really turns it into an endpoint, which is a higher level concept than simply providing basic methods to read/write on the I2C bus. An endpoint wrapper can very easily be written in Python.

Slave mode:

  • i2c.read(nbytes)
  • i2c.readinto(buf) NOHEAP
  • i2c.write(buf) NOHEAP

The SPI class

spi = SPI(id, mode, *, baudrate, polarity=1, phase=0, bits=8, firstbit=SPI.MSB, pins)

pins is a tuple/list of SCK,MOSI,MISO pins in that order. Optionally the list can also have NSS at the end.

Methods:

  • spi.write(buf) NOHEAP
  • spi.read(nbytes, *, write=0x00) write is the byte to output on MOSI for each byte read in
  • spi.readinto(buf, *, write=0x00) NOHEAP
  • spi.write_readinto(write_buf, read_buf) NOHEAP; write_buf and read_buf can be the same

The I2S class

**Note that I2S has not yet officially supported by any existing MicroPython port; support for stmhal (pyboard) is currently under development by @blmorris. The proposed API for I2S is based on the new model for SPI, with certain modifications based on the I2S development discussions on GitHub, and will provide guidance as the I2S code is prepared for merging. **

i2s = I2S(id, mode, dataformat=I2S_DATAFORMAT_16B_EXTENDED, standard=I2S_STANDARD_PHILIPS, polarity=0, audiofreq=I2S_AUDIOFREQ_48K, clksrc=I2S_CLOCK_PLL, mclkout=0, pins)

pins is a tuple/list of BCK,WS,TX,RX pins in that order. BCK, WS, and at least one of either TX or RX are required. If Both TX and RX are provided, the I2S port is initialized in duplex mode, otherwise it initilizes as simplex in the direction specified by the provided pin.

Methods

All write and read methods for I2S will utilize DMA and be non-blocking when IRQ's are enabled.

Buffer oriented:

  • i2s.write(buf) NOHEAP
  • i2s.read(buf) ??
  • i2s.write_readinto(write_buf, read_buf)
  • i2s.write_callback(func)
  • i2s.read_callback(func)

Stream oriented:

  • i2s.stream_out(stream)
  • i2s.stream_in(stream)
  • i2s.pause()
  • i2s.resume()
  • i2s.stop()

Constants

  • for mode: I2S.MASTER, I2S.SLAVE
  • for standard: I2S.PHILIPS, I2S.MSB, I2S.LSB, I2S.PCM_SHORT, I2S.PCM_LONG
  • for clksrc: I2S.PLL, I2S.EXTERNAL
  • for dataformat: default is 0 for 16bit extended; otherwise valid values are 16, 24, or 32 for these respective sample widths
  • for audiofreq: default is 48000 for 48kHz sample rate; otherwise 44100, 88200, and 96000 are valid (TODO: provide a complete list of valid sample rates...)
  • for mclkout: Enable Master Clock output; 0 or 1, True or False

The ADC class

Need to decide on standard for return value (eg always 12-bits maximum value?). See discussion in https://github.com/micropython/micropython/pull/1130 for suggestion to always use 14- or 30-bit value (maximum MicroPython unsigned value which fits into 16- or 32-bit machine word).

Constructor:

adc = ADC(id, *, bits, ...)

Methods:

  • adc.init(...) re-init
  • apin = adc.channel(id, *, pin=...) make an analog pin from a channel and optionally specify a pin. If only the pin is given, the right channel for that pin will be selected.
  • apin.value() NOHEAP? depends what the return value is, probably should be integer.
  • apin() fast method to read the value of the pin.
  • apin.readtimed(buf, timer)

The Timer class

Constructor:

timer = Timer(id, *, freq, ...)

Methods:

  • timer.source_freq() NOHEAP; get the source frequency of the clock (needed?)
  • timer.counter([counter]) NOHEAP; get/set the timer counter
  • timer.freq([freq]) NOHEAP*; get/set frequency in Hz (may be a floating point number)
  • timer.prescaler([prescaler]) NOHEAP; get/set the prescaler
  • timer.period([period]) NOHEAP; get/set the period

There will probably also need to be timer channels, which can be attached to pins and used for PWM etc.

The RTC (Real Time Clock) class

Constructor:

rtc = RTC(id=0, datetime=(year, month, day, hour=0, minute=0, second=0, microsecond=0, tzinfo=None)) create an RTC object instance and set the current time.

Methods:

  • rtc.init() re-init
  • rtc.now() returns a datetime tuple with the current time and date.
  • rtc.alarm(alarm_id, time=time_ms or datetime_tuple, *, repeat=False) sets the RTC alarm. If the alarm has already expired the returned value will be 0. An alarm can be set both via passing an integer with the number of milliseconds or a datetime tuple with a future time. If the datetime tuple contains a past time, the alarm will expire immediately triggering any enabled IRQ. The same applies when passing a 0 or negative time_ms value.
  • rtc.alarm_left(alarm_id) get the number of milliseconds left before the alarm expires. Returns 0 if the alarm is already expired.
  • rtc.alarm_cancel(alarm_id) cancel a running alarm.
  • rtc.calibration([cal_value]) get or set the RTC calibration value. Platform dependent.
  • rtc.irq(*, trigger, handler, priority, wake) calls the handler function once the alarm expires. See the IRQ section for details.

Constants:

  • RTC.ALARM0

The WDT class

Constructor:

wdt = WDT(id, [timeout]) instantiate and optionally enable the WDT with the specified timeout.

Methods:

  • wdt.init(...) re-init (on some platforms re-initializing the WDT might not be allowed, for security reasons).
  • wdt.feed() feed the watchdog.
  • wdt.deinit() disable the WDT (again, might not be possible on some platforms, raise OSError in that case).

The WLAN class

The WLAN class belongs to the network module.

Constructor:

wlan = WLAN(id, mode=WLAN.STA, *, ssid='wlan', auth=None, channel=1, iface=None)

Methods:

  • wlan.init() re-init.
  • wlan.deinit() disable the NIC. NOHEAP.
  • wlan.mode([mode]) set or get the mode. NOHEAP
  • wlan.ssid([ssid]) set or get our own SSID name
  • wlan.auth([(sec, key)]) set or get the authentication tuple.
  • wlan.channel([channel]) set or get the channel. NOHEAP
  • wlan.scan() perform a network scan and return a named tuple of the form: (ssid, bssid, sec, channel, rssi)
  • wlan.mac([mac]) get or set the MAC address. The MAC address is a bytes object of length 6.
  • wlan.connect(ssid, auth=None, *, bssid, timeout=None) Connect to the network specified by the SSID using the given authentication. Optionally specify the BSSID and a timeout.
  • wlan.disconnect() Closes the current connection. NOHEAP
  • wlan.isconnected() returns True if connected and IP address has been assigned. NOHEAP
  • wlan.ifconfig(id=0, config=[(ip, netmask, gateway, dns) or 'dhcp']) get or set the IP configuration. The id is the interface id, and defaults to zero. In the case of AP+STA mode, the NIC effectively has 2 interfaces that can be configured independently using id=0 for the AP and id=1 for the STA. Alternatively, a port can choose to use string names for the id, e.g. id='STA' and id='AP'.

Constants:

  • WLAN.STA Station mode
  • WLAN.AP Access point mode
  • WLAN.STA_AP Station mode + Access point mode.
  • WLAN.P2P Peer to peer (also called WiFi-direct or Ad-Hoc mode)
  • WLAN.WEP WEP security
  • WLAN.WPA WPA security
  • WLAN.WPA2 WPA2 security
  • WLAN.WPA_ENT WPA Enterprise security

machine module functions

  • machine.reset() perform a hard reset (same as pressing the reset switch on the board)
  • machine.enable_irq() enable the interrupts
  • machine.disable_irq() disable the interrupts
  • machine.freq([freq, ...]) NOHEAP; get or set CPU and/or bus frequencies
  • machine.idle([... options]) NOHEAP; idle the CPU, may require external event to leave idle mode
  • machine.sleep([... options]) NOHEAP; enter sleep mode that retains RAM and continues execution when woken
  • machine.deepsleep([... options]) NOHEAP; enter sleep mode that may not retain RAM and may reset when woken
  • machine.reset_cause() get the reset cause
  • machine.wake_reason() get the wake reason (from SLEEP or DEEPSLEEP modes). None will be returned if no sleep-wake cycle has occurred

Constants

To specify from which sleep mode a callback can wake the machine:

  • machine.IDLE
  • machine.SLEEP
  • machine.DEEPSLEEP

Reset causes:

  • machine.PWR_ON_RESET
  • machine.HARD_RESET
  • machine.SOFT_RESET
  • machine.WDT_RESET
  • machine.DEEPSLEEP_RESET

Wake reason:

  • machine.NETWORK_WAKE
  • machine.PIN_WAKE
  • machine.RTC_WAKE

time module additions

The time module has uPy specific functions:

  • time.sleep_ms(ms) NOHEAP - Delay for given number of milliseconds, should be positive or 0
  • time.sleep_us(us) NOHEAP - Delay for given number of microseconds, should be positive or 0
  • time.ticks_ms() NOHEAP - Return increasing millisecond counter with arbitrary reference point, and wrapping after some (unspecified) value. The value should be treated as opaque, suitable for use only with ticks_diff().
  • time.ticks_us() NOHEAP - As above, but microsecond counter.
  • time.ticks_cpu() NOHEAP - As above, but highest resolution available (usually CPU clocks).
  • time.ticks_diff(old, new) NOHEAP - Measure period between consecutive calls to ticks_ms(), ticks_us(), or ticks_cpu(). Value returned by these functions may wrap around at any time, so directly subtracting them is not supported. ticks_diff() should be used instead. "old" value should actually precede "new" value in time, or result is undefined. This function should not be used to measure arbitrarily long periods of time (because ticks_*() functions wrap around and usually would have short period). The expected usage pattern is implementing event polling with timeout:
# Wait for GPIO pin to be asserted, but at most 500us
start = time.ticks_us()
while pin.value() == 0:
    if time.ticks_diff(start, time.ticks_us()) > 500:
        raise TimeoutError
  • time.source(rtc_or_timer_obj) attach a RTC to use as the time reference for time.time() and time.localtime().

os module additions

The os module has uPy specific functions:

  • os.mount(block_dev, mount_point, *, readonly)
  • os.umount(mount_point)
  • os.mkfs(device or path, *, options...)
  • os.dupterm([stream_obj]) get or set duplication of controlling terminal, so that REPL can be redirected to UART or other (note: could allow to duplicate to multiple stream objs but that's arguably overkill and could anyway be done in Python by making a stream multiplexer)

micropython module

  • micropython.main(path) set the location of the main script.

Clone this wiki locally