-
Notifications
You must be signed in to change notification settings - Fork 0
Hardware API
The aim is to make the pyb module obsolete, make all its functionality cleaner and more board-generic, and move such functionality elsewhere. Below is the current proposal, very much a work in progress.
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).
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 onlyCreate 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
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(). -
If a periph is constructed just using its id then it is not (re-)initialised. This allows to reference existing periphs without changing them.
-
When connecting a periph to some pins, one uses the
pins=keywoard 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.
The classes to control the peripherals of the board will reside in a new module called hardware, therefore, by doing:
import hardware
dir(hardware)one can easily see what's supported on the board.
Peripherals that support interrupts provide the irq method which returns an irq object.
peripheral.irq(*, trigger=None, priority=1, handler=None, wake=None)- All arguments are keyworkd only, to be able to adapt to the different nature of each peripheral.
-
triggeris what causes the interrupt, for instance aPinobject acceptsPin.INT_RISING,Pin.INT_FALLING, etc. Trigger sources can also be ORed together, likePin.INT_RISING | Pin.INT_FALLING. -
priorityit'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. -
handleris the function that gets called when the interrupt is fired. -
wakespecifies if the irq can wake the device from any of the sleep modes, e.g.machine.SLEEPormachine.DEEPSLEEP, and they can also be ORed together.
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()configure theirq. Same as constructor. If called with no arguments simply re-enables the irq with the previous configuration. -
irq.deinit()disable the callback. -
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.INT_RISING:
# handle rising edge
else:
# handle falling edge
# disable the interrupt
pin.irq().deinit()pin = Pin(id, mode, pull=Pin.PULL_NONE, *, value, drive, slew, alt, ...)
-
id, andmodeare mandatory and positional (modecan kw). -
pullis optional and positional (also can be named). - The rest of args are kwonly.
- Only
valueis required for a port to implement (initial value if given). - The rest are optional, and a port can define them and also define others.
-
altspecifies 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.
Do we really need 3 ways to set the value?
-
pin.init(...)re init. -
pin.high()NOHEAP; set high. -
pin.low()NOHEAP; set low. -
pin.value()NOHEAP; get value. -
pin.value(x)NOHEAP; set value. -
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
-
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: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 -
for interrupt (callback) mode:
pin.INT_RISING,pin.INT_FALLING,pin.INT_RISING_FALLING,pin.INT_HIGH_LEVEL,pin.INT_LOW_LEVEL
uart = UART(id, baudrate=9600, bits=8, parity=None, stop=0, *, pins, ...)
-
pinsis a 4 or 2 item list indicating the TX, RX, RTS and CTS pins (in that order). Any of the pins can beNoneif one wants the UART to operate with limited functionality. If theRTSpin is given the theRXpin must be given as well. The same applies toCTS. When no pins are given (or all areNone), then the default set ofTXandRXpins is taken, and hardware flow control will be disabled.
Methods:
uart.init(...)uart.read([nbytes])-
uart.readinto(buf[, nbytes])NOHEAP uart.readall()uart.readline([max_size])-
uart.write(buf)NOHEAP -
uart.sendbreak()NOHEAP
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 transfers:
i2c.readfrom(addr, nbytes)-
i2c.readfrom_into(addr, buf)NOHEAP -
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, memaddr, *, 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
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
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 -
adc.readchannel(channel)NOHEAP; read a specific channel -
apin = adc(pin)make an analog pin (will choose the correct channel for the pin) -
apin.read()NOHEAP? depends what the return value is, probably should be integer apin.readtimed(buf, timer)
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.
Constructor:
rtc = RTC(id, [time_ms]) create an RTC instance and optionally set the current time. time_ms is an integer which represents the current time in milliseconds since Jan 1, 2000.
Methods:
-
rtc.init(time_ms)re-init -
rtc.time()returns the current time in milliseconds, since Jan 1, 2000. -
rtc.alarm(alarm_id, [time_ms or time_tuple], *, mode=RTC.ONE_SHOT)gets or 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 time_tuple with a future time. If the time_tuple contains a past time, the alarm will expire immediately and also triggering any enabled IRQ. The same applies when passing a 0 or negativetime_msvalue.-
modecan be either RTC.ONE_SHOT or RTC.PERIODIC (raises if combined with a time tuple).
-
-
rtc.calibration([cal_value])get or set the RTC calibration value. Might not be supported by all platforms. -
rtc.callback(*, handler, priority, wake)calls the handler function once the alarm expires. See the IRQ section for details.
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.kick()kick the watchdog. -
wdt.deinit()disable the WDT (again, might not be possible on some platforms, raiseOSErrorin that case).
-
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, might returnNoneif now sleep-wake cycle has occurred ??
To specify from which sleep mode a callback can wake the machine:
machine.SLEEPmachine.DEEPSLEEP
Reset causes:
machine.PWR_ON_RESETmachine.HARD_RESETmachine.SOFT_RESETmachine.WDT_RESETmachine.DEEPSLEEP_RESET
Wake reason:
machine.NETWORK_WAKEmachine.PIN_WAKEmachine.TIMER_WAKE
The sys module has uPy specific functions:
-
sys.dup_stdio([stream_obj])get or set duplication of global stdio, 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)
The time module has uPy specific functions:
-
time.sleep_ms(ms)NOHEAP -
time.sleep_us(us)NOHEAP -
time.ticks_ms()NOHEAP -
time.ticks_us()NOHEAP -
time.ticks_cpu()NOHEAP -
time.ticks_diff(t0, t1)NOHEAP -
time.attach_rtc(rtc_obj)attach a RTC clock object to use as the time reference fortime.time()andtime.localtime(), or should we do this from the RTC class instead ??
The os module has uPy specific functions:
os.mount(block_dev, mount_point, *, readonly)os.umount(mount_point)os.mkfs(device or path, *, options...)