micropython: add micropython component

This commit is contained in:
KY-zhang-X
2022-09-29 12:10:37 +08:00
parent 1514f1cb9b
commit dd76146324
2679 changed files with 354110 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
Arithmetic instructions
=======================
Document conventions
--------------------
Notation: ``Rd, Rm, Rn`` denote ARM registers R0-R7. ``immN`` denotes an immediate
value having a width of N bits e.g. ``imm8``, ``imm3``. ``carry`` denotes
the carry condition flag, ``not(carry)`` denotes its complement. In the case of instructions
with more than one register argument, it is permissible for some to be identical. For example
the following will add the contents of R0 to itself, placing the result in R0:
* add(r0, r0, r0)
Arithmetic instructions affect the condition flags except where stated.
Addition
--------
* add(Rdn, imm8) ``Rdn = Rdn + imm8``
* add(Rd, Rn, imm3) ``Rd = Rn + imm3``
* add(Rd, Rn, Rm) ``Rd = Rn +Rm``
* adc(Rd, Rn) ``Rd = Rd + Rn + carry``
Subtraction
-----------
* sub(Rdn, imm8) ``Rdn = Rdn - imm8``
* sub(Rd, Rn, imm3) ``Rd = Rn - imm3``
* sub(Rd, Rn, Rm) ``Rd = Rn - Rm``
* sbc(Rd, Rn) ``Rd = Rd - Rn - not(carry)``
Negation
--------
* neg(Rd, Rn) ``Rd = -Rn``
Multiplication and division
---------------------------
* mul(Rd, Rn) ``Rd = Rd * Rn``
This produces a 32 bit result with overflow lost. The result may be treated as
signed or unsigned according to the definition of the operands.
* sdiv(Rd, Rn, Rm) ``Rd = Rn / Rm``
* udiv(Rd, Rn, Rm) ``Rd = Rn / Rm``
These functions perform signed and unsigned division respectively. Condition flags
are not affected.

View File

@@ -0,0 +1,90 @@
Comparison instructions
=======================
These perform an arithmetic or logical instruction on two arguments, discarding the result
but setting the condition flags. Typically these are used to test data values without changing
them prior to executing a conditional branch.
Document conventions
--------------------
Notation: ``Rd, Rm, Rn`` denote ARM registers R0-R7. ``imm8`` denotes an immediate
value having a width of 8 bits.
The Application Program Status Register (APSR)
----------------------------------------------
This contains four bits which are tested by the conditional branch instructions. Typically a
conditional branch will test multiple bits, for example ``bge(LABEL)``. The meaning of
condition codes can depend on whether the operands of an arithmetic instruction are viewed as
signed or unsigned integers. Thus ``bhi(LABEL)`` assumes unsigned numbers were processed while
``bgt(LABEL)`` assumes signed operands.
APSR Bits
---------
* Z (zero)
This is set if the result of an operation is zero or the operands of a comparison are equal.
* N (negative)
Set if the result is negative.
* C (carry)
An addition sets the carry flag when the result overflows out of the MSB, for example adding
0x80000000 and 0x80000000. By the nature of two's complement arithmetic this behaviour is reversed
on subtraction, with a borrow indicated by the carry bit being clear. Thus 0x10 - 0x01 is executed
as 0x10 + 0xffffffff which will set the carry bit.
* V (overflow)
The overflow flag is set if the result, viewed as a two's compliment number, has the "wrong" sign
in relation to the operands. For example adding 1 to 0x7fffffff will set the overflow bit because
the result (0x8000000), viewed as a two's complement integer, is negative. Note that in this instance
the carry bit is not set.
Comparison instructions
-----------------------
These set the APSR (Application Program Status Register) N (negative), Z (zero), C (carry) and V
(overflow) flags.
* cmp(Rn, imm8) ``Rn - imm8``
* cmp(Rn, Rm) ``Rn - Rm``
* cmn(Rn, Rm) ``Rn + Rm``
* tst(Rn, Rm) ``Rn & Rm``
Conditional execution
---------------------
The ``it`` and ``ite`` instructions provide a means of conditionally executing from one to four subsequent
instructions without the need for a label.
* it(<condition>) If then
Execute the next instruction if <condition> is true:
::
cmp(r0, r1)
it(eq)
mov(r0, 100) # runs if r0 == r1
# execution continues here
* ite(<condition>) If then else
If <condtion> is true, execute the next instruction, otherwise execute the
subsequent one. Thus:
::
cmp(r0, r1)
ite(eq)
mov(r0, 100) # runs if r0 == r1
mov(r0, 200) # runs if r0 != r1
# execution continues here
This may be extended to control the execution of upto four subsequent instructions: it[x[y[z]]]
where x,y,z=t/e; e.g. itt, itee, itete, ittte, itttt, iteee, etc.

View File

@@ -0,0 +1,36 @@
Assembler directives
====================
Labels
------
* label(INNER1)
This defines a label for use in a branch instruction. Thus elsewhere in the code a ``b(INNER1)``
will cause execution to continue with the instruction after the label directive.
Defining inline data
--------------------
The following assembler directives facilitate embedding data in an assembler code block.
* data(size, d0, d1 .. dn)
The data directive creates n array of data values in memory. The first argument specifies the
size in bytes of the subsequent arguments. Hence the first statement below will cause the
assembler to put three bytes (with values 2, 3 and 4) into consecutive memory locations
while the second will cause it to emit two four byte words.
::
data(1, 2, 3, 4)
data(4, 2, 100000)
Data values longer than a single byte are stored in memory in little-endian format.
* align(nBytes)
Align the following instruction to an nBytes value. ARM Thumb-2 instructions must be two
byte aligned, hence it's advisable to issue ``align(2)`` after ``data`` directives and
prior to any subsequent code. This ensures that the code will run irrespective of the
size of the data array.

View File

@@ -0,0 +1,77 @@
Floating point instructions
===========================
These instructions support the use of the ARM floating point coprocessor
(on platforms such as the Pyboard which are equipped with one). The FPU
has 32 registers known as ``s0-s31`` each of which can hold a single
precision float. Data can be passed between the FPU registers and the
ARM core registers with the ``vmov`` instruction.
Note that MicroPython doesn't support passing floats to
assembler functions, nor can you put a float into ``r0`` and expect a
reasonable result. There are two ways to overcome this. The first is to
use arrays, and the second is to pass and/or return integers and convert
to and from floats in code.
Document conventions
--------------------
Notation: ``Sd, Sm, Sn`` denote FPU registers, ``Rd, Rm, Rn`` denote ARM core
registers. The latter can be any ARM core register although registers
``R13-R15`` are unlikely to be appropriate in this context.
Arithmetic
----------
* vadd(Sd, Sn, Sm) ``Sd = Sn + Sm``
* vsub(Sd, Sn, Sm) ``Sd = Sn - Sm``
* vneg(Sd, Sm) ``Sd = -Sm``
* vmul(Sd, Sn, Sm) ``Sd = Sn * Sm``
* vdiv(Sd, Sn, Sm) ``Sd = Sn / Sm``
* vsqrt(Sd, Sm) ``Sd = sqrt(Sm)``
Registers may be identical: ``vmul(S0, S0, S0)`` will execute ``S0 = S0*S0``
Move between ARM core and FPU registers
---------------------------------------
* vmov(Sd, Rm) ``Sd = Rm``
* vmov(Rd, Sm) ``Rd = Sm``
The FPU has a register known as FPSCR, similar to the ARM core's APSR, which stores condition
codes plus other data. The following instructions provide access to this.
* vmrs(APSR\_nzcv, FPSCR)
Move the floating-point N, Z, C, and V flags to the APSR N, Z, C, and V flags.
This is done after an instruction such as an FPU
comparison to enable the condition codes to be tested by the assembler
code. The following is a more general form of the instruction.
* vmrs(Rd, FPSCR) ``Rd = FPSCR``
Move between FPU register and memory
------------------------------------
* vldr(Sd, [Rn, offset]) ``Sd = [Rn + offset]``
* vstr(Sd, [Rn, offset]) ``[Rn + offset] = Sd``
Where ``[Rn + offset]`` denotes the memory address obtained by adding Rn to the offset. This
is specified in bytes. Since each float value occupies a 32 bit word, when accessing arrays of
floats the offset must always be a multiple of four bytes.
Data comparison
---------------
* vcmp(Sd, Sm)
Compare the values in Sd and Sm and set the FPU N, Z,
C, and V flags. This would normally be followed by ``vmrs(APSR_nzcv, FPSCR)``
to enable the results to be tested.
Convert between integer and float
---------------------------------
* vcvt\_f32\_s32(Sd, Sm) ``Sd = float(Sm)``
* vcvt\_s32\_f32(Sd, Sm) ``Sd = int(Sm)``

View File

@@ -0,0 +1,244 @@
Hints and tips
==============
The following are some examples of the use of the inline assembler and some
information on how to work around its limitations. In this document the term
"assembler function" refers to a function declared in Python with the
``@micropython.asm_thumb`` decorator, whereas "subroutine" refers to assembler
code called from within an assembler function.
Code branches and subroutines
-----------------------------
It is important to appreciate that labels are local to an assembler function.
There is currently no way for a subroutine defined in one function to be called
from another.
To call a subroutine the instruction ``bl(LABEL)`` is issued. This transfers
control to the instruction following the ``label(LABEL)`` directive and stores
the return address in the link register (``lr`` or ``r14``). To return the
instruction ``bx(lr)`` is issued which causes execution to continue with
the instruction following the subroutine call. This mechanism implies that, if
a subroutine is to call another, it must save the link register prior to
the call and restore it before terminating.
The following rather contrived example illustrates a function call. Note that
it's necessary at the start to branch around all subroutine calls: subroutines
end execution with ``bx(lr)`` while the outer function simply "drops off the end"
in the style of Python functions.
::
@micropython.asm_thumb
def quad(r0):
b(START)
label(DOUBLE)
add(r0, r0, r0)
bx(lr)
label(START)
bl(DOUBLE)
bl(DOUBLE)
print(quad(10))
The following code example demonstrates a nested (recursive) call: the classic
Fibonacci sequence. Here, prior to a recursive call, the link register is saved
along with other registers which the program logic requires to be preserved.
::
@micropython.asm_thumb
def fib(r0):
b(START)
label(DOFIB)
push({r1, r2, lr})
cmp(r0, 1)
ble(FIBDONE)
sub(r0, 1)
mov(r2, r0) # r2 = n -1
bl(DOFIB)
mov(r1, r0) # r1 = fib(n -1)
sub(r0, r2, 1)
bl(DOFIB) # r0 = fib(n -2)
add(r0, r0, r1)
label(FIBDONE)
pop({r1, r2, lr})
bx(lr)
label(START)
bl(DOFIB)
for n in range(10):
print(fib(n))
Argument passing and return
---------------------------
The tutorial details the fact that assembler functions can support from zero to
three arguments, which must (if used) be named ``r0``, ``r1`` and ``r2``. When
the code executes the registers will be initialised to those values.
The data types which can be passed in this way are integers and memory
addresses. With current firmware all possible 32 bit values may be passed and
returned. If the return value may have the most significant bit set a Python
type hint should be employed to enable MicroPython to determine whether the
value should be interpreted as a signed or unsigned integer: types are
``int`` or ``uint``.
::
@micropython.asm_thumb
def uadd(r0, r1) -> uint:
add(r0, r0, r1)
``hex(uadd(0x40000000,0x40000000))`` will return 0x80000000, demonstrating the
passing and return of integers where bits 30 and 31 differ.
The limitations on the number of arguments and return values can be overcome by means
of the ``array`` module which enables any number of values of any type to be accessed.
Multiple arguments
~~~~~~~~~~~~~~~~~~
If a Python array of integers is passed as an argument to an assembler
function, the function will receive the address of a contiguous set of integers.
Thus multiple arguments can be passed as elements of a single array. Similarly a
function can return multiple values by assigning them to array elements.
Assembler functions have no means of determining the length of an array:
this will need to be passed to the function.
This use of arrays can be extended to enable more than three arrays to be used.
This is done using indirection: the ``uctypes`` module supports ``addressof()``
which will return the address of an array passed as its argument. Thus you can
populate an integer array with the addresses of other arrays:
::
from uctypes import addressof
@micropython.asm_thumb
def getindirect(r0):
ldr(r0, [r0, 0]) # Address of array loaded from passed array
ldr(r0, [r0, 4]) # Return element 1 of indirect array (24)
def testindirect():
a = array.array('i',[23, 24])
b = array.array('i',[0,0])
b[0] = addressof(a)
print(getindirect(b))
Non-integer data types
~~~~~~~~~~~~~~~~~~~~~~
These may be handled by means of arrays of the appropriate data type. For
example, single precision floating point data may be processed as follows.
This code example takes an array of floats and replaces its contents with
their squares.
::
from array import array
@micropython.asm_thumb
def square(r0, r1):
label(LOOP)
vldr(s0, [r0, 0])
vmul(s0, s0, s0)
vstr(s0, [r0, 0])
add(r0, 4)
sub(r1, 1)
bgt(LOOP)
a = array('f', (x for x in range(10)))
square(a, len(a))
print(a)
The uctypes module supports the use of data structures beyond simple
arrays. It enables a Python data structure to be mapped onto a bytearray
instance which may then be passed to the assembler function.
Named constants
---------------
Assembler code may be made more readable and maintainable by using named
constants rather than littering code with numbers. This may be achieved
thus:
::
MYDATA = const(33)
@micropython.asm_thumb
def foo():
mov(r0, MYDATA)
The const() construct causes MicroPython to replace the variable name
with its value at compile time. If constants are declared in an outer
Python scope they can be shared between multiple assembler functions and
with Python code.
Assembler code as class methods
-------------------------------
MicroPython passes the address of the object instance as the first argument
to class methods. This is normally of little use to an assembler function.
It can be avoided by declaring the function as a static method thus:
::
class foo:
@staticmethod
@micropython.asm_thumb
def bar(r0):
add(r0, r0, r0)
Use of unsupported instructions
-------------------------------
These can be coded using the data statement as shown below. While
``push()`` and ``pop()`` are supported the example below illustrates the
principle. The necessary machine code may be found in the ARM v7-M
Architecture Reference Manual. Note that the first argument of data
calls such as
::
data(2, 0xe92d, 0x0f00) # push r8,r9,r10,r11
indicates that each subsequent argument is a two byte quantity.
Overcoming MicroPython's integer restriction
--------------------------------------------
The Pyboard chip includes a CRC generator. Its use presents a problem in
MicroPython because the returned values cover the full gamut of 32 bit
quantities whereas small integers in MicroPython cannot have differing values
in bits 30 and 31. This limitation is overcome with the following code, which
uses assembler to put the result into an array and Python code to
coerce the result into an arbitrary precision unsigned integer.
::
from array import array
import stm
def enable_crc():
stm.mem32[stm.RCC + stm.RCC_AHB1ENR] |= 0x1000
def reset_crc():
stm.mem32[stm.CRC+stm.CRC_CR] = 1
@micropython.asm_thumb
def getval(r0, r1):
movwt(r3, stm.CRC + stm.CRC_DR)
str(r1, [r3, 0])
ldr(r2, [r3, 0])
str(r2, [r0, 0])
def getcrc(value):
a = array('i', [0])
getval(a, value)
return a[0] & 0xffffffff # coerce to arbitrary precision
enable_crc()
reset_crc()
for x in range(20):
print(hex(getcrc(0)))

View File

@@ -0,0 +1,73 @@
.. _asm_thumb2_index:
Inline assembler for Thumb2 architectures
=========================================
This document assumes some familiarity with assembly language programming and should be read after studying
the :ref:`tutorial <pyboard_tutorial_assembler>`. For a detailed description of the instruction set consult the
Architecture Reference Manual detailed below.
The inline assembler supports a subset of the ARM Thumb-2 instruction set described here. The syntax tries
to be as close as possible to that defined in the above ARM manual, converted to Python function calls.
Instructions operate on 32 bit signed integer data except where stated otherwise. Most supported instructions
operate on registers ``R0-R7`` only: where ``R8-R15`` are supported this is stated. Registers ``R8-R12`` must be
restored to their initial value before return from a function. Registers ``R13-R15`` constitute the Link Register,
Stack Pointer and Program Counter respectively.
Document conventions
--------------------
Where possible the behaviour of each instruction is described in Python, for example
* add(Rd, Rn, Rm) ``Rd = Rn + Rm``
This enables the effect of instructions to be demonstrated in Python. In certain case this is impossible
because Python doesn't support concepts such as indirection. The pseudocode employed in such cases is
described on the relevant page.
Instruction categories
----------------------
The following sections details the subset of the ARM Thumb-2 instruction set supported by MicroPython.
.. toctree::
:maxdepth: 1
:numbered:
asm_thumb2_mov.rst
asm_thumb2_ldr.rst
asm_thumb2_str.rst
asm_thumb2_logical_bit.rst
asm_thumb2_arith.rst
asm_thumb2_compare.rst
asm_thumb2_label_branch.rst
asm_thumb2_stack.rst
asm_thumb2_misc.rst
asm_thumb2_float.rst
asm_thumb2_directives.rst
Usage examples
--------------
These sections provide further code examples and hints on the use of the assembler.
.. toctree::
:maxdepth: 1
:numbered:
asm_thumb2_hints_tips.rst
References
----------
- :ref:`Assembler Tutorial <pyboard_tutorial_assembler>`
- `Wiki hints and tips
<http://wiki.micropython.org/platforms/boards/pyboard/assembler>`__
- `uPy Inline Assembler source-code,
emitinlinethumb.c <https://github.com/micropython/micropython/blob/master/py/emitinlinethumb.c>`__
- `ARM Thumb2 Instruction Set Quick Reference
Card <http://infocenter.arm.com/help/topic/com.arm.doc.qrc0001l/QRC0001_UAL.pdf>`__
- `RM0090 Reference
Manual <http://www.google.ae/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&cad=rja&uact=8&sqi=2&ved=0CBoQFjAA&url=http%3A%2F%2Fwww.st.com%2Fst-web-ui%2Fstatic%2Factive%2Fen%2Fresource%2Ftechnical%2Fdocument%2Freference_manual%2FDM00031020.pdf&ei=G0rSU66xFeuW0QWYwoD4CQ&usg=AFQjCNFuW6TgzE4QpahO_U7g3f3wdwecAg&sig2=iET-R0y9on_Pbflzf9aYDw&bvm=bv.71778758,bs.1,d.bGQ>`__
- ARM v7-M Architecture Reference Manual (Available on the
ARM site after a simple registration procedure. Also available on academic sites but beware of out of date versions.)

View File

@@ -0,0 +1,85 @@
Branch instructions
===================
These cause execution to jump to a target location usually specified by a label (see the ``label``
assembler directive). Conditional branches and the ``it`` and ``ite`` instructions test
the Application Program Status Register (APSR) N (negative), Z (zero), C (carry) and V
(overflow) flags to determine whether the branch should be executed.
Most of the exposed assembler instructions (including move operations) set the flags but
there are explicit comparison instructions to enable values to be tested.
Further detail on the meaning of the condition flags is provided in the section
describing comparison functions.
Document conventions
--------------------
Notation: ``Rm`` denotes ARM registers R0-R15. ``LABEL`` denotes a label defined with the
``label()`` assembler directive. ``<condition>`` indicates one of the following condition
specifiers:
* eq Equal to (result was zero)
* ne Not equal
* cs Carry set
* cc Carry clear
* mi Minus (negative)
* pl Plus (positive)
* vs Overflow set
* vc Overflow clear
* hi > (unsigned comparison)
* ls <= (unsigned comparison)
* ge >= (signed comparison)
* lt < (signed comparison)
* gt > (signed comparison)
* le <= (signed comparison)
Branch to label
---------------
* b(LABEL) Unconditional branch
* beq(LABEL) branch if equal
* bne(LABEL) branch if not equal
* bge(LABEL) branch if greater than or equal
* bgt(LABEL) branch if greater than
* blt(LABEL) branch if less than (<) (signed)
* ble(LABEL) branch if less than or equal to (<=) (signed)
* bcs(LABEL) branch if carry flag is set
* bcc(LABEL) branch if carry flag is clear
* bmi(LABEL) branch if negative
* bpl(LABEL) branch if positive
* bvs(LABEL) branch if overflow flag set
* bvc(LABEL) branch if overflow flag is clear
* bhi(LABEL) branch if higher (unsigned)
* bls(LABEL) branch if lower or equal (unsigned)
Long branches
-------------
The code produced by the branch instructions listed above uses a fixed bit width to specify the
branch destination, which is PC relative. Consequently in long programs where the
branch instruction is remote from its destination the assembler will produce a "branch not in
range" error. This can be overcome with the "wide" variants such as
* beq\_w(LABEL) long branch if equal
Wide branches use 4 bytes to encode the instruction (compared with 2 bytes for standard branch instructions).
Subroutines (functions)
-----------------------
When entering a subroutine the processor stores the return address in register r14, also
known as the link register (lr). Return to the instruction after the subroutine call is
performed by updating the program counter (r15 or pc) from the link register, This
process is handled by the following instructions.
* bl(LABEL)
Transfer execution to the instruction after ``LABEL`` storing the return address in
the link register (r14).
* bx(Rm) Branch to address specified by Rm.
Typically ``bx(lr)`` is issued to return from a subroutine. For nested subroutines the
link register of outer scopes must be saved (usually on the stack) before performing
inner subroutine calls.

View File

@@ -0,0 +1,23 @@
Load register from memory
=========================
Document conventions
--------------------
Notation: ``Rt, Rn`` denote ARM registers R0-R7 except where stated. ``immN`` represents an immediate
value having a width of N bits hence ``imm5`` is constrained to the range 0-31. ``[Rn + immN]`` is the contents
of the memory address obtained by adding Rn and the offset ``immN``. Offsets are measured in
bytes. These instructions affect the condition flags.
Register Load
-------------
* ldr(Rt, [Rn, imm7]) ``Rt = [Rn + imm7]`` Load a 32 bit word
* ldrb(Rt, [Rn, imm5]) ``Rt = [Rn + imm5]`` Load a byte
* ldrh(Rt, [Rn, imm6]) ``Rt = [Rn + imm6]`` Load a 16 bit half word
Where a byte or half word is loaded, it is zero-extended to 32 bits.
The specified immediate offsets are measured in bytes. Hence in the case of ``ldr`` the 7 bit value
enables 32 bit word aligned values to be accessed with a maximum offset of 31 words. In the case of ``ldrh`` the
6 bit value enables 16 bit half-word aligned values to be accessed with a maximum offset of 31 half-words.

View File

@@ -0,0 +1,53 @@
Logical & bitwise instructions
==============================
Document conventions
--------------------
Notation: ``Rd, Rn`` denote ARM registers R0-R7 except in the case of the
special instructions where R0-R15 may be used. ``Rn<a-b>`` denotes an ARM register
whose contents must lie in range ``a <= contents <= b``. In the case of instructions
with two register arguments, it is permissible for them to be identical. For example
the following will zero R0 (Python ``R0 ^= R0``) regardless of its initial contents.
* eor(r0, r0)
These instructions affect the condition flags except where stated.
Logical instructions
--------------------
* and\_(Rd, Rn) ``Rd &= Rn``
* orr(Rd, Rn) ``Rd |= Rn``
* eor(Rd, Rn) ``Rd ^= Rn``
* mvn(Rd, Rn) ``Rd = Rn ^ 0xffffffff`` i.e. Rd = 1's complement of Rn
* bic(Rd, Rn) ``Rd &= ~Rn`` bit clear Rd using mask in Rn
Note the use of "and\_" instead of "and", because "and" is a reserved keyword in Python.
Shift and rotation instructions
-------------------------------
* lsl(Rd, Rn<0-31>) ``Rd <<= Rn``
* lsr(Rd, Rn<1-32>) ``Rd = (Rd & 0xffffffff) >> Rn`` Logical shift right
* asr(Rd, Rn<1-32>) ``Rd >>= Rn`` arithmetic shift right
* ror(Rd, Rn<1-31>) ``Rd = rotate_right(Rd, Rn)`` Rd is rotated right Rn bits.
A rotation by (for example) three bits works as follows. If Rd initially
contains bits ``b31 b30..b0`` after rotation it will contain ``b2 b1 b0 b31 b30..b3``
Special instructions
--------------------
Condition codes are unaffected by these instructions.
* clz(Rd, Rn) ``Rd = count_leading_zeros(Rn)``
count_leading_zeros(Rn) returns the number of binary zero bits before the first binary one bit in Rn.
* rbit(Rd, Rn) ``Rd = bit_reverse(Rn)``
bit_reverse(Rn) returns the bit-reversed contents of Rn. If Rn contains bits ``b31 b30..b0`` Rd will be set
to ``b0 b1 b2..b31``
Trailing zeros may be counted by performing a bit reverse prior to executing clz.

View File

@@ -0,0 +1,13 @@
Miscellaneous instructions
==========================
* nop() ``pass`` no operation.
* wfi() Suspend execution in a low power state until an interrupt occurs.
* cpsid(flags) set the Priority Mask Register - disable interrupts.
* cpsie(flags) clear the Priority Mask Register - enable interrupts.
* mrs(Rd, special_reg) ``Rd = special_reg`` copy a special register to a general register. The special register
may be IPSR (Interrupt Status Register) or BASEPRI (Base Priority Register). The IPSR provides a means of determining
the exception number of an interrupt being processed. It contains zero if no interrupt is being processed.
Currently the ``cpsie()`` and ``cpsid()`` functions are partially implemented.
They require but ignore the flags argument and serve as a means of enabling and disabling interrupts.

View File

@@ -0,0 +1,27 @@
Register move instructions
==========================
Document conventions
--------------------
Notation: ``Rd, Rn`` denote ARM registers R0-R15. ``immN`` denotes an immediate
value having a width of N bits. These instructions affect the condition flags.
Register moves
--------------
Where immediate values are used, these are zero-extended to 32 bits. Thus
``mov(R0, 0xff)`` will set R0 to 255.
* mov(Rd, imm8) ``Rd = imm8``
* mov(Rd, Rn) ``Rd = Rn``
* movw(Rd, imm16) ``Rd = imm16``
* movt(Rd, imm16) ``Rd = (Rd & 0xffff) | (imm16 << 16)``
movt writes an immediate value to the top halfword of the destination register.
It does not affect the contents of the bottom halfword.
* movwt(Rd, imm32) ``Rd = imm32``
movwt is a pseudo-instruction: the MicroPython assembler emits a ``movw`` followed
by a ``movt`` to move a 32-bit value into Rd.

View File

@@ -0,0 +1,20 @@
Stack push and pop
==================
Document conventions
--------------------
The ``push()`` and ``pop()`` instructions accept as their argument a register set containing
a subset, or possibly all, of the general-purpose registers R0-R12 and the link register (lr or R14).
As with any Python set the order in which the registers are specified is immaterial. Thus the
in the following example the pop() instruction would restore R1, R7 and R8 to their contents prior
to the push():
* push({r1, r8, r7}) Save three registers on the stack.
* pop({r7, r1, r8}) Restore them
Stack operations
----------------
* push({regset}) Push a set of registers onto the stack
* pop({regset}) Restore a set of registers from the stack

View File

@@ -0,0 +1,21 @@
Store register to memory
========================
Document conventions
--------------------
Notation: ``Rt, Rn`` denote ARM registers R0-R7 except where stated. ``immN`` represents an immediate
value having a width of N bits hence ``imm5`` is constrained to the range 0-31. ``[Rn + imm5]`` is the
contents of the memory address obtained by adding Rn and the offset ``imm5``. Offsets are measured in
bytes. These instructions do not affect the condition flags.
Register Store
--------------
* str(Rt, [Rn, imm7]) ``[Rn + imm7] = Rt`` Store a 32 bit word
* strb(Rt, [Rn, imm5]) ``[Rn + imm5] = Rt`` Store a byte (b0-b7)
* strh(Rt, [Rn, imm6]) ``[Rn + imm6] = Rt`` Store a 16 bit half word (b0-b15)
The specified immediate offsets are measured in bytes. Hence in the case of ``str`` the 7 bit value
enables 32 bit word aligned values to be accessed with a maximum offset of 31 words. In the case of ``strh`` the
6 bit value enables 16 bit half-word aligned values to be accessed with a maximum offset of 31 half-words.

View File

@@ -0,0 +1,462 @@
.. _constrained:
MicroPython on microcontrollers
===============================
MicroPython is designed to be capable of running on microcontrollers. These
have hardware limitations which may be unfamiliar to programmers more familiar
with conventional computers. In particular the amount of RAM and nonvolatile
"disk" (flash memory) storage is limited. This tutorial offers ways to make
the most of the limited resources. Because MicroPython runs on controllers
based on a variety of architectures, the methods presented are generic: in some
cases it will be necessary to obtain detailed information from platform specific
documentation.
Flash memory
------------
On the Pyboard the simple way to address the limited capacity is to fit a micro
SD card. In some cases this is impractical, either because the device does not
have an SD card slot or for reasons of cost or power consumption; hence the
on-chip flash must be used. The firmware including the MicroPython subsystem is
stored in the onboard flash. The remaining capacity is available for use. For
reasons connected with the physical architecture of the flash memory part of
this capacity may be inaccessible as a filesystem. In such cases this space may
be employed by incorporating user modules into a firmware build which is then
flashed to the device.
There are two ways to achieve this: frozen modules and frozen bytecode. Frozen
modules store the Python source with the firmware. Frozen bytecode uses the
cross compiler to convert the source to bytecode which is then stored with the
firmware. In either case the module may be accessed with an import statement:
.. code::
import mymodule
The procedure for producing frozen modules and bytecode is platform dependent;
instructions for building the firmware can be found in the README files in the
relevant part of the source tree.
In general terms the steps are as follows:
* Clone the MicroPython `repository <https://github.com/micropython/micropython>`_.
* Acquire the (platform specific) toolchain to build the firmware.
* Build the cross compiler.
* Place the modules to be frozen in a specified directory (dependent on whether
the module is to be frozen as source or as bytecode).
* Build the firmware. A specific command may be required to build frozen
code of either type - see the platform documentation.
* Flash the firmware to the device.
RAM
---
When reducing RAM usage there are two phases to consider: compilation and
execution. In addition to memory consumption, there is also an issue known as
heap fragmentation. In general terms it is best to minimise the repeated
creation and destruction of objects. The reason for this is covered in the
section covering the `heap`_.
Compilation phase
~~~~~~~~~~~~~~~~~
When a module is imported, MicroPython compiles the code to bytecode which is
then executed by the MicroPython virtual machine (VM). The bytecode is stored
in RAM. The compiler itself requires RAM, but this becomes available for use
when the compilation has completed.
If a number of modules have already been imported the situation can arise where
there is insufficient RAM to run the compiler. In this case the import
statement will produce a memory exception.
If a module instantiates global objects on import it will consume RAM at the
time of import, which is then unavailable for the compiler to use on subsequent
imports. In general it is best to avoid code which runs on import; a better
approach is to have initialisation code which is run by the application after
all modules have been imported. This maximises the RAM available to the
compiler.
If RAM is still insufficient to compile all modules one solution is to
precompile modules. MicroPython has a cross compiler capable of compiling Python
modules to bytecode (see the README in the mpy-cross directory). The resulting
bytecode file has a .mpy extension; it may be copied to the filesystem and
imported in the usual way. Alternatively some or all modules may be implemented
as frozen bytecode: on most platforms this saves even more RAM as the bytecode
is run directly from flash rather than being stored in RAM.
Execution phase
~~~~~~~~~~~~~~~
There are a number of coding techniques for reducing RAM usage.
**Constants**
MicroPython provides a ``const`` keyword which may be used as follows:
.. code::
from micropython import const
ROWS = const(33)
_COLS = const(0x10)
a = ROWS
b = _COLS
In both instances where the constant is assigned to a variable the compiler
will avoid coding a lookup to the name of the constant by substituting its
literal value. This saves bytecode and hence RAM. However the ``ROWS`` value
will occupy at least two machine words, one each for the key and value in the
globals dictionary. The presence in the dictionary is necessary because another
module might import or use it. This RAM can be saved by prepending the name
with an underscore as in ``_COLS``: this symbol is not visible outside the
module so will not occupy RAM.
The argument to ``const()`` may be anything which, at compile time, evaluates
to a constant e.g. ``0x100``, ``1 << 8`` or ``(True, "string", b"bytes")``
(see section below for details). It can even include other const
symbols that have already been defined, e.g. ``1 << BIT``.
**Constant data structures**
Where there is a substantial volume of constant data and the platform supports
execution from Flash, RAM may be saved as follows. The data should be located in
Python modules and frozen as bytecode. The data must be defined as `bytes`
objects. The compiler 'knows' that `bytes` objects are immutable and ensures
that the objects remain in flash memory rather than being copied to RAM. The
`struct` module can assist in converting between `bytes` types and other
Python built-in types.
When considering the implications of frozen bytecode, note that in Python
strings, floats, bytes, integers, complex numbers and tuples are immutable.
Accordingly these will be frozen into flash (for tuples, only if all their
elements are immutable). Thus, in the line
.. code::
mystring = "The quick brown fox"
the actual string "The quick brown fox" will reside in flash. At runtime a
reference to the string is assigned to the *variable* ``mystring``. The reference
occupies a single machine word. In principle a long integer could be used to
store constant data:
.. code::
bar = 0xDEADBEEF0000DEADBEEF
As in the string example, at runtime a reference to the arbitrarily large
integer is assigned to the variable ``bar``. That reference occupies a
single machine word.
Tuples of constant objects are themselves constant. Such constant tuples are
optimised by the compiler so they do not need to be created at runtime each time
they are used. For example:
.. code::
foo = (1, 2, 3, 4, 5, 6, 100000, ("string", b"bytes", False, True))
This entire tuple will exist as a single object (potentially in flash if the
code is frozen) and referenced each time it is needed.
**Needless object creation**
There are a number of situations where objects may unwittingly be created and
destroyed. This can reduce the usability of RAM through fragmentation. The
following sections discuss instances of this.
**String concatenation**
Consider the following code fragments which aim to produce constant strings:
.. code::
var = "foo" + "bar"
var1 = "foo" "bar"
var2 = """\
foo\
bar"""
Each produces the same outcome, however the first needlessly creates two string
objects at runtime, allocates more RAM for concatenation before producing the
third. The others perform the concatenation at compile time which is more
efficient, reducing fragmentation.
Where strings must be dynamically created before being fed to a stream such as
a file it will save RAM if this is done in a piecemeal fashion. Rather than
creating a large string object, create a substring and feed it to the stream
before dealing with the next.
The best way to create dynamic strings is by means of the string ``format()``
method:
.. code::
var = "Temperature {:5.2f} Pressure {:06d}\n".format(temp, press)
**Buffers**
When accessing devices such as instances of UART, I2C and SPI interfaces, using
pre-allocated buffers avoids the creation of needless objects. Consider these
two loops:
.. code::
while True:
var = spi.read(100)
# process data
buf = bytearray(100)
while True:
spi.readinto(buf)
# process data in buf
The first creates a buffer on each pass whereas the second re-uses a pre-allocated
buffer; this is both faster and more efficient in terms of memory fragmentation.
**Bytes are smaller than ints**
On most platforms an integer consumes four bytes. Consider the three calls to the
function ``foo()``:
.. code::
def foo(bar):
for x in bar:
print(x)
foo([1, 2, 0xff])
foo((1, 2, 0xff))
foo(b'\1\2\xff')
In the first call a `list` of integers is created in RAM each time the code is
executed. The second call creates a constant `tuple` object (a `tuple` containing
only constant objects) as part of the compilation phase, so it is only created
once and is more efficient than the `list`. The third call efficiently
creates a `bytes` object consuming the minimum amount of RAM. If the module
were frozen as bytecode, both the `tuple` and `bytes` object would reside in flash.
**Strings Versus Bytes**
Python3 introduced Unicode support. This introduced a distinction between a
string and an array of bytes. MicroPython ensures that Unicode strings take no
additional space so long as all characters in the string are ASCII (i.e. have
a value < 126). If values in the full 8-bit range are required `bytes` and
`bytearray` objects can be used to ensure that no additional space will be
required. Note that most string methods (e.g. :meth:`str.strip()`) apply also to `bytes`
instances so the process of eliminating Unicode can be painless.
.. code::
s = 'the quick brown fox' # A string instance
b = b'the quick brown fox' # A bytes instance
Where it is necessary to convert between strings and bytes the :meth:`str.encode`
and the :meth:`bytes.decode` methods can be used. Note that both strings and bytes
are immutable. Any operation which takes as input such an object and produces
another implies at least one RAM allocation to produce the result. In the
second line below a new bytes object is allocated. This would also occur if ``foo``
were a string.
.. code::
foo = b' empty whitespace'
foo = foo.lstrip()
**Runtime compiler execution**
The Python funcitons `eval` and `exec` invoke the compiler at runtime, which
requires significant amounts of RAM. Note that the ``pickle`` library from
`micropython-lib` employs `exec`. It may be more RAM efficient to use the
`json` library for object serialisation.
**Storing strings in flash**
Python strings are immutable hence have the potential to be stored in read only
memory. The compiler can place in flash strings defined in Python code. As with
frozen modules it is necessary to have a copy of the source tree on the PC and
the toolchain to build the firmware. The procedure will work even if the
modules have not been fully debugged, so long as they can be imported and run.
After importing the modules, execute:
.. code::
micropython.qstr_info(1)
Then copy and paste all the Q(xxx) lines into a text editor. Check for and
remove lines which are obviously invalid. Open the file qstrdefsport.h which
will be found in ports/stm32 (or the equivalent directory for the architecture in
use). Copy and paste the corrected lines at the end of the file. Save the file,
rebuild and flash the firmware. The outcome can be checked by importing the
modules and again issuing:
.. code::
micropython.qstr_info(1)
The Q(xxx) lines should be gone.
.. _heap:
The heap
--------
When a running program instantiates an object the necessary RAM is allocated
from a fixed size pool known as the heap. When the object goes out of scope (in
other words becomes inaccessible to code) the redundant object is known as
"garbage". A process known as "garbage collection" (GC) reclaims that memory,
returning it to the free heap. This process runs automatically, however it can
be invoked directly by issuing `gc.collect()`.
The discourse on this is somewhat involved. For a 'quick fix' issue the
following periodically:
.. code::
gc.collect()
gc.threshold(gc.mem_free() // 4 + gc.mem_alloc())
Fragmentation
~~~~~~~~~~~~~
Say a program creates an object ``foo``, then an object ``bar``. Subsequently
``foo`` goes out of scope but ``bar`` remains. The RAM used by ``foo`` will be
reclaimed by GC. However if ``bar`` was allocated to a higher address, the
RAM reclaimed from ``foo`` will only be of use for objects no bigger than
``foo``. In a complex or long running program the heap can become fragmented:
despite there being a substantial amount of RAM available, there is insufficient
contiguous space to allocate a particular object, and the program fails with a
memory error.
The techniques outlined above aim to minimise this. Where large permanent buffers
or other objects are required it is best to instantiate these early in the
process of program execution before fragmentation can occur. Further improvements
may be made by monitoring the state of the heap and by controlling GC; these are
outlined below.
Reporting
~~~~~~~~~
A number of library functions are available to report on memory allocation and
to control GC. These are to be found in the `gc` and `micropython` modules.
The following example may be pasted at the REPL (``ctrl e`` to enter paste mode,
``ctrl d`` to run it).
.. code::
import gc
import micropython
gc.collect()
micropython.mem_info()
print('-----------------------------')
print('Initial free: {} allocated: {}'.format(gc.mem_free(), gc.mem_alloc()))
def func():
a = bytearray(10000)
gc.collect()
print('Func definition: {} allocated: {}'.format(gc.mem_free(), gc.mem_alloc()))
func()
print('Func run free: {} allocated: {}'.format(gc.mem_free(), gc.mem_alloc()))
gc.collect()
print('Garbage collect free: {} allocated: {}'.format(gc.mem_free(), gc.mem_alloc()))
print('-----------------------------')
micropython.mem_info(1)
Methods employed above:
* `gc.collect()` Force a garbage collection. See footnote.
* `micropython.mem_info()` Print a summary of RAM utilisation.
* `gc.mem_free()` Return the free heap size in bytes.
* `gc.mem_alloc()` Return the number of bytes currently allocated.
* ``micropython.mem_info(1)`` Print a table of heap utilisation (detailed below).
The numbers produced are dependent on the platform, but it can be seen that
declaring the function uses a small amount of RAM in the form of bytecode
emitted by the compiler (the RAM used by the compiler has been reclaimed).
Running the function uses over 10KiB, but on return ``a`` is garbage because it
is out of scope and cannot be referenced. The final `gc.collect()` recovers
that memory.
The final output produced by ``micropython.mem_info(1)`` will vary in detail but
may be interpreted as follows:
====== =================
Symbol Meaning
====== =================
. free block
h head block
= tail block
m marked head block
T tuple
L list
D dict
F float
B byte code
M module
====== =================
Each letter represents a single block of memory, a block being 16 bytes. So each
line of the heap dump represents 0x400 bytes or 1KiB of RAM.
Control of garbage collection
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A GC can be demanded at any time by issuing `gc.collect()`. It is advantageous
to do this at intervals, firstly to pre-empt fragmentation and secondly for
performance. A GC can take several milliseconds but is quicker when there is
little work to do (about 1ms on the Pyboard). An explicit call can minimise that
delay while ensuring it occurs at points in the program when it is acceptable.
Automatic GC is provoked under the following circumstances. When an attempt at
allocation fails, a GC is performed and the allocation re-tried. Only if this
fails is an exception raised. Secondly an automatic GC will be triggered if the
amount of free RAM falls below a threshold. This threshold can be adapted as
execution progresses:
.. code::
gc.collect()
gc.threshold(gc.mem_free() // 4 + gc.mem_alloc())
This will provoke a GC when more than 25% of the currently free heap becomes
occupied.
In general modules should instantiate data objects at runtime using constructors
or other initialisation functions. The reason is that if this occurs on
initialisation the compiler may be starved of RAM when subsequent modules are
imported. If modules do instantiate data on import then `gc.collect()` issued
after the import will ameliorate the problem.
String operations
-----------------
MicroPython handles strings in an efficient manner and understanding this can
help in designing applications to run on microcontrollers. When a module
is compiled, strings which occur multiple times are stored once only, a process
known as string interning. In MicroPython an interned string is known as a ``qstr``.
In a module imported normally that single instance will be located in RAM, but
as described above, in modules frozen as bytecode it will be located in flash.
String comparisons are also performed efficiently using hashing rather than
character by character. The penalty for using strings rather than integers may
hence be small both in terms of performance and RAM usage - a fact which may
come as a surprise to C programmers.
Postscript
----------
MicroPython passes, returns and (by default) copies objects by reference. A
reference occupies a single machine word so these processes are efficient in
RAM usage and speed.
Where variables are required whose size is neither a byte nor a machine word
there are standard libraries which can assist in storing these efficiently and
in performing conversions. See the `array`, `struct` and `uctypes`
modules.
Footnote: gc.collect() return value
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
On Unix and Windows platforms the `gc.collect()` method returns an integer
which signifies the number of distinct memory regions that were reclaimed in the
collection (more precisely, the number of heads that were turned into frees). For
efficiency reasons bare metal ports do not return this value.

View File

@@ -0,0 +1,303 @@
.. _filesystem:
Working with filesystems
========================
.. contents::
This tutorial describes how MicroPython provides an on-device filesystem,
allowing standard Python file I/O methods to be used with persistent storage.
MicroPython automatically creates a default configuration and auto-detects the
primary filesystem, so this tutorial will be mostly useful if you want to modify
the partitioning, filesystem type, or use custom block devices.
The filesystem is typically backed by internal flash memory on the device, but
can also use external flash, RAM, or a custom block device.
On some ports (e.g. STM32), the filesystem may also be available over USB MSC to
a host PC. :ref:`pyboard_py` also provides a way for the host PC to access to
the filesystem on all ports.
Note: This is mainly for use on bare-metal ports like STM32 and ESP32. On ports
with an operating system (e.g. the Unix port) the filesystem is provided by the
host OS.
VFS
---
MicroPython implements a Unix-like Virtual File System (VFS) layer. All mounted
filesystems are combined into a single virtual filesystem, starting at the root
``/``. Filesystems are mounted into directories in this structure, and at
startup the working directory is changed to where the primary filesystem is
mounted.
On STM32 / Pyboard, the internal flash is mounted at ``/flash``, and optionally
the SDCard at ``/sd``. On ESP8266/ESP32, the primary filesystem is mounted at
``/``.
Block devices
-------------
A block device is an instance of a class that implements the
:class:`os.AbstractBlockDev` protocol.
Built-in block devices
~~~~~~~~~~~~~~~~~~~~~~
Ports provide built-in block devices to access their primary flash.
On power-on, MicroPython will attempt to detect the filesystem on the default
flash and configure and mount it automatically. If no filesystem is found,
MicroPython will attempt to create a FAT filesystem spanning the entire flash.
Ports can also provide a mechanism to "factory reset" the primary flash, usually
by some combination of button presses at power on.
STM32 / Pyboard
...............
The :ref:`pyb.Flash <pyb.Flash>` class provides access to the internal flash. On some
boards which have larger external flash (e.g. Pyboard D), it will use that
instead. The ``start`` kwarg should always be specified, i.e.
``pyb.Flash(start=0)``.
Note: For backwards compatibility, when constructed with no arguments (i.e.
``pyb.Flash()``), it only implements the simple block interface and reflects the
virtual device presented to USB MSC (i.e. it includes a virtual partition table
at the start).
ESP8266
.......
The internal flash is exposed as a block device object which is created in the
``flashbdev`` module on start up. This object is by default added as a global
variable so it can usually be accessed simply as ``bdev``. This implements the
extended interface.
ESP32
.....
The :class:`esp32.Partition` class implements a block device for partitions
defined for the board. Like ESP8266, there is a global variable ``bdev`` which
points to the default partition. This implements the extended interface.
Custom block devices
~~~~~~~~~~~~~~~~~~~~
The following class implements a simple block device that stores its data in
RAM using a ``bytearray``::
class RAMBlockDev:
def __init__(self, block_size, num_blocks):
self.block_size = block_size
self.data = bytearray(block_size * num_blocks)
def readblocks(self, block_num, buf):
for i in range(len(buf)):
buf[i] = self.data[block_num * self.block_size + i]
def writeblocks(self, block_num, buf):
for i in range(len(buf)):
self.data[block_num * self.block_size + i] = buf[i]
def ioctl(self, op, arg):
if op == 4: # get number of blocks
return len(self.data) // self.block_size
if op == 5: # get block size
return self.block_size
It can be used as follows::
import os
bdev = RAMBlockDev(512, 50)
os.VfsFat.mkfs(bdev)
os.mount(bdev, '/ramdisk')
An example of a block device that supports both the simple and extended
interface (i.e. both signatures and behaviours of the
:meth:`os.AbstractBlockDev.readblocks` and
:meth:`os.AbstractBlockDev.writeblocks` methods) is::
class RAMBlockDev:
def __init__(self, block_size, num_blocks):
self.block_size = block_size
self.data = bytearray(block_size * num_blocks)
def readblocks(self, block_num, buf, offset=0):
addr = block_num * self.block_size + offset
for i in range(len(buf)):
buf[i] = self.data[addr + i]
def writeblocks(self, block_num, buf, offset=None):
if offset is None:
# do erase, then write
for i in range(len(buf) // self.block_size):
self.ioctl(6, block_num + i)
offset = 0
addr = block_num * self.block_size + offset
for i in range(len(buf)):
self.data[addr + i] = buf[i]
def ioctl(self, op, arg):
if op == 4: # block count
return len(self.data) // self.block_size
if op == 5: # block size
return self.block_size
if op == 6: # block erase
return 0
As it supports the extended interface, it can be used with :class:`littlefs
<os.VfsLfs2>`::
import os
bdev = RAMBlockDev(512, 50)
os.VfsLfs2.mkfs(bdev)
os.mount(bdev, '/ramdisk')
Once mounted, the filesystem (regardless of its type) can be used as it
normally would be used from Python code, for example::
with open('/ramdisk/hello.txt', 'w') as f:
f.write('Hello world')
print(open('/ramdisk/hello.txt').read())
Filesystems
-----------
MicroPython ports can provide implementations of :class:`FAT <os.VfsFat>`,
:class:`littlefs v1 <os.VfsLfs1>` and :class:`littlefs v2 <os.VfsLfs2>`.
The following table shows which filesystems are included in the firmware by
default for given port/board combinations, however they can be optionally
enabled in a custom firmware build.
==================== ===== =========== ===========
Board FAT littlefs v1 littlefs v2
==================== ===== =========== ===========
pyboard 1.0, 1.1, D Yes No Yes
Other STM32 Yes No No
ESP8266 (1M) No No Yes
ESP8266 (2M+) Yes No Yes
ESP32 Yes No Yes
==================== ===== =========== ===========
FAT
~~~
The main advantage of the FAT filesystem is that it can be accessed over USB MSC
on supported boards (e.g. STM32) without any additional drivers required on the
host PC.
However, FAT is not tolerant to power failure during writes and this can lead to
filesystem corruption. For applications that do not require USB MSC, it is
recommended to use littlefs instead.
To format the entire flash using FAT::
# ESP8266 and ESP32
import os
os.umount('/')
os.VfsFat.mkfs(bdev)
os.mount(bdev, '/')
# STM32
import os, pyb
os.umount('/flash')
os.VfsFat.mkfs(pyb.Flash(start=0))
os.mount(pyb.Flash(start=0), '/flash')
os.chdir('/flash')
Littlefs
~~~~~~~~
Littlefs_ is a filesystem designed for flash-based devices, and is much more
resistant to filesystem corruption.
.. note:: There are reports of littlefs v1 and v2 failing in certain
situations, for details see `littlefs issue 347`_ and
`littlefs issue 295`_.
To format the entire flash using littlefs v2::
# ESP8266 and ESP32
import os
os.umount('/')
os.VfsLfs2.mkfs(bdev)
os.mount(bdev, '/')
# STM32
import os, pyb
os.umount('/flash')
os.VfsLfs2.mkfs(pyb.Flash(start=0))
os.mount(pyb.Flash(start=0), '/flash')
os.chdir('/flash')
A littlefs filesystem can be still be accessed on a PC over USB MSC using the
`littlefs FUSE driver`_. Note that you must specify both the ``--block_size``
and ``--block_count`` options to override the defaults. For example (after
building the littlefs-fuse executable)::
$ ./lfs --block_size=4096 --block_count=512 -o allow_other /dev/sdb1 mnt
This will allow the board's littlefs filesystem to be accessed at the ``mnt``
directory. To get the correct values of ``block_size`` and ``block_count`` use::
import pyb
f = pyb.Flash(start=0)
f.ioctl(1, 1) # initialise flash in littlefs raw-block mode
block_count = f.ioctl(4, 0)
block_size = f.ioctl(5, 0)
.. _littlefs FUSE driver: https://github.com/littlefs-project/littlefs-fuse
.. _Littlefs: https://github.com/littlefs-project/littlefs
.. _littlefs issue 295: https://github.com/littlefs-project/littlefs/issues/295
.. _littlefs issue 347: https://github.com/littlefs-project/littlefs/issues/347
Hybrid (STM32)
~~~~~~~~~~~~~~
By using the ``start`` and ``len`` kwargs to :class:`pyb.Flash`, you can create
block devices spanning a subset of the flash device.
For example, to configure the first 256kiB as FAT (and available over USB MSC),
and the remainder as littlefs::
import os, pyb
os.umount('/flash')
p1 = pyb.Flash(start=0, len=256*1024)
p2 = pyb.Flash(start=256*1024)
os.VfsFat.mkfs(p1)
os.VfsLfs2.mkfs(p2)
os.mount(p1, '/flash')
os.mount(p2, '/data')
os.chdir('/flash')
This might be useful to make your Python files, configuration and other
rarely-modified content available over USB MSC, but allowing for frequently
changing application data to reside on littlefs with better resilience to power
failure, etc.
The partition at offset ``0`` will be mounted automatically (and the filesystem
type automatically detected), but you can add::
import os, pyb
p2 = pyb.Flash(start=256*1024)
os.mount(p2, '/data')
to ``boot.py`` to mount the data partition.
Hybrid (ESP32)
~~~~~~~~~~~~~~
On ESP32, if you build custom firmware, you can modify ``partitions.csv`` to
define an arbitrary partition layout.
At boot, the partition named "vfs" will be mounted at ``/`` by default, but any
additional partitions can be mounted in your ``boot.py`` using::
import esp32, os
p = esp32.Partition.find(esp32.Partition.TYPE_DATA, label='foo')
os.mount(p, '/foo')

View File

@@ -0,0 +1,200 @@
Glossary
========
.. glossary::
baremetal
A system without a (full-fledged) operating system, for example an
:term:`MCU`-based system. When running on a baremetal system,
MicroPython effectively functions like a small operating system,
running user programs and providing a command interpreter
(:term:`REPL`).
buffer protocol
Any Python object that can be automatically converted into bytes, such
as ``bytes``, ``bytearray``, ``memoryview`` and ``str`` objects, which
all implement the "buffer protocol".
board
Typically this refers to a printed circuit board (PCB) containing a
:term:`microcontroller <MCU>` and supporting components.
MicroPython firmware is typically provided per-board, as the firmware
contains both MCU-specific functionality but also board-level
functionality such as drivers or pin names.
bytecode
A compact representation of a Python program that generated by
compiling the Python source code. This is what the VM actually
executes. Bytecode is typically generated automatically at runtime and
is invisible to the user. Note that while :term:`CPython` and
MicroPython both use bytecode, the format is different. You can also
pre-compile source code offline using the :term:`cross-compiler`.
callee-owned tuple
This is a MicroPython-specific construct where, for efficiency
reasons, some built-in functions or methods may re-use the same
underlying tuple object to return data. This avoids having to allocate
a new tuple for every call, and reduces heap fragmentation. Programs
should not hold references to callee-owned tuples and instead only
extract data from them (or make a copy).
CircuitPython
A variant of MicroPython developed by `Adafruit Industries
<https://circuitpython.org>`_.
CPython
CPython is the reference implementation of the Python programming
language, and the most well-known one. It is, however, one of many
implementations (including Jython, IronPython, PyPy, and MicroPython).
While MicroPython's implementation differs substantially from CPython,
it aims to maintain as much compatibility as possible.
cross-compiler
Also known as ``mpy-cross``. This tool runs on your PC and converts a
:term:`.py file` containing MicroPython code into a :term:`.mpy file`
containing MicroPython bytecode. This means it loads faster (the board
doesn't have to compile the code), and uses less space on flash (the
bytecode is more space efficient).
driver
A MicroPython library that implements support for a particular
component, such as a sensor or display.
FFI
Acronym for Foreign Function Interface. A mechanism used by the
:term:`MicroPython Unix port` to access operating system functionality.
This is not available on :term:`baremetal` ports.
filesystem
Most MicroPython ports and boards provide a filesystem stored in flash
that is available to user code via the standard Python file APIs such
as ``open()``. Some boards also make this internal filesystem
accessible to the host via USB mass-storage.
frozen module
A Python module that has been cross compiled and bundled into the
firmware image. This reduces RAM requirements as the code is executed
directly from flash.
Garbage Collector
A background process that runs in Python (and MicroPython) to reclaim
unused memory in the :term:`heap`.
GPIO
General-purpose input/output. The simplest means to control electrical
signals (commonly referred to as "pins") on a microcontroller. GPIO
typically allows pins to be either input or output, and to set or get
their digital value (logical "0" or "1"). MicroPython abstracts GPIO
access using the :class:`machine.Pin` and :class:`machine.Signal`
classes.
GPIO port
A group of :term:`GPIO` pins, usually based on hardware properties of
these pins (e.g. controllable by the same register).
heap
A region of RAM where MicroPython stores dynamic data. It is managed
automatically by the :term:`Garbage Collector`. Different MCUs and
boards have vastly different amounts of RAM available for the heap, so
this will affect how complex your program can be.
interned string
An optimisation used by MicroPython to improve the efficiency of
working with strings. An interned string is referenced by its (unique)
identity rather than its address and can therefore be quickly compared
just by its identifier. It also means that identical strings can be
de-duplicated in memory. String interning is almost always invisible to
the user.
MCU
Microcontroller. Microcontrollers usually have much less resources
than a desktop, laptop, or phone, but are smaller, cheaper and
require much less power. MicroPython is designed to be small and
optimized enough to run on an average modern microcontroller.
micropython-lib
MicroPython is (usually) distributed as a single executable/binary
file with just few builtin modules. There is no extensive standard
library comparable with :term:`CPython`'s. Instead, there is a related,
but separate project `micropython-lib
<https://github.com/micropython/micropython-lib>`_ which provides
implementations for many modules from CPython's standard library.
Some of the modules are are implemented in pure Python, and are able to
be used on all ports. However, the majority of these modules use
:term:`FFI` to access operating system functionality, and as such can
only be used on the :term:`MicroPython Unix port` (with limited support
for Windows).
Unlike the :term:`CPython` stdlib, micropython-lib modules are
intended to be installed individually - either using manual copying or
using :term:`upip`.
MicroPython port
MicroPython supports different :term:`boards <board>`, RTOSes, and
OSes, and can be relatively easily adapted to new systems. MicroPython
with support for a particular system is called a "port" to that
system. Different ports may have widely different functionality. This
documentation is intended to be a reference of the generic APIs
available across different ports ("MicroPython core"). Note that some
ports may still omit some APIs described here (e.g. due to resource
constraints). Any such differences, and port-specific extensions
beyond the MicroPython core functionality, would be described in the
separate port-specific documentation.
MicroPython Unix port
The unix port is one of the major :term:`MicroPython ports
<MicroPython port>`. It is intended to run on POSIX-compatible
operating systems, like Linux, MacOS, FreeBSD, Solaris, etc. It also
serves as the basis of Windows port. The Unix port is very useful for
quick development and testing of the MicroPython language and
machine-independent features. It can also function in a similar way to
:term:`CPython`'s ``python`` executable.
.mpy file
The output of the :term:`cross-compiler`. A compiled form of a
:term:`.py file` that contains MicroPython bytecode instead of Python
source code.
native
Usually refers to "native code", i.e. machine code for the target
microcontroller (such as ARM Thumb, Xtensa, x86/x64). The ``@native``
decorator can be applied to a MicroPython function to generate native
code instead of bytecode for that function, which will likely be
faster but use more RAM.
port
Usually short for :term:`MicroPython port`, but could also refer to
:term:`GPIO port`.
.py file
A file containing Python source code.
REPL
An acronym for "Read, Eval, Print, Loop". This is the interactive
Python prompt, useful for debugging or testing short snippets of code.
Most MicroPython boards make a REPL available over a UART, and this is
typically accessible on a host PC via USB.
stream
Also known as a "file-like object". A Python object which provides
sequential read-write access to the underlying data. A stream object
implements a corresponding interface, which consists of methods like
``read()``, ``write()``, ``readinto()``, ``seek()``, ``flush()``,
``close()``, etc. A stream is an important concept in MicroPython;
many I/O objects implement the stream interface, and thus can be used
consistently and interchangeably in different contexts. For more
information on streams in MicroPython, see the `io` module.
UART
Acronym for "Universal Asynchronous Receiver/Transmitter". This is a
peripheral that sends data over a pair of pins (TX & RX). Many boards
include a way to make at least one of the UARTs available to a host PC
as a serial port over USB.
upip
(Literally, "micro pip"). A package manager for MicroPython, inspired
by :term:`CPython`'s pip, but much smaller and with reduced
functionality.
upip runs both on the :term:`Unix port <MicroPython Unix port>` and on
:term:`baremetal` ports which offer filesystem and networking support.

View File

@@ -0,0 +1,33 @@
MicroPython language and implementation
=======================================
MicroPython aims to implement the Python 3.4 standard (with selected
features from later versions) with respect to language syntax, and most
of the features of MicroPython are identical to those described by the
"Language Reference" documentation at
`docs.python.org <https://docs.python.org/3/reference/index.html>`_.
The MicroPython standard library is described in the
:ref:`corresponding chapter <micropython_lib>`. The :ref:`cpython_diffs`
chapter describes differences between MicroPython and CPython (which
mostly concern standard library and types, but also some language-level
features).
This chapter describes features and peculiarities of MicroPython
implementation and the best practices to use them.
.. toctree::
:maxdepth: 1
glossary.rst
repl.rst
mpremote.rst
mpyfiles.rst
isr_rules.rst
speed_python.rst
constrained.rst
manifest.rst
packages.rst
asm_thumb2_index.rst
filesystem.rst
pyboard.py.rst

View File

@@ -0,0 +1,414 @@
.. _isr_rules:
Writing interrupt handlers
==========================
On suitable hardware MicroPython offers the ability to write interrupt handlers in Python. Interrupt handlers
- also known as interrupt service routines (ISR's) - are defined as callback functions. These are executed
in response to an event such as a timer trigger or a voltage change on a pin. Such events can occur at any point
in the execution of the program code. This carries significant consequences, some specific to the MicroPython
language. Others are common to all systems capable of responding to real time events. This document covers
the language specific issues first, followed by a brief introduction to real time programming for those new to it.
This introduction uses vague terms like "slow" or "as fast as possible". This is deliberate, as speeds are
application dependent. Acceptable durations for an ISR are dependent on the rate at which interrupts occur,
the nature of the main program, and the presence of other concurrent events.
Tips and recommended practices
------------------------------
This summarises the points detailed below and lists the principal recommendations for interrupt handler code.
* Keep the code as short and simple as possible.
* Avoid memory allocation: no appending to lists or insertion into dictionaries, no floating point.
* Consider using ``micropython.schedule`` to work around the above constraint.
* Where an ISR returns multiple bytes use a pre-allocated ``bytearray``. If multiple integers are to be
shared between an ISR and the main program consider an array (``array.array``).
* Where data is shared between the main program and an ISR, consider disabling interrupts prior to accessing
the data in the main program and re-enabling them immediately afterwards (see Critical Sections).
* Allocate an emergency exception buffer (see below).
MicroPython issues
------------------
The emergency exception buffer
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If an error occurs in an ISR, MicroPython is unable to produce an error report unless a special buffer is created
for the purpose. Debugging is simplified if the following code is included in any program using interrupts.
.. code:: python
import micropython
micropython.alloc_emergency_exception_buf(100)
The emergency exception buffer can only hold one exception stack trace. This means that if a second exception is
thrown during the handling of an exception while the heap is locked, that second exception's stack trace will
replace the original one - even if the second exception is cleanly handled. This can lead to confusing exception
messages if the buffer is later printed.
Simplicity
~~~~~~~~~~
For a variety of reasons it is important to keep ISR code as short and simple as possible. It should do only what
has to be done immediately after the event which caused it: operations which can be deferred should be delegated
to the main program loop. Typically an ISR will deal with the hardware device which caused the interrupt, making
it ready for the next interrupt to occur. It will communicate with the main loop by updating shared data to indicate
that the interrupt has occurred, and it will return. An ISR should return control to the main loop as quickly
as possible. This is not a specific MicroPython issue so is covered in more detail :ref:`below <ISR>`.
Communication between an ISR and the main program
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Normally an ISR needs to communicate with the main program. The simplest means of doing this is via one or more
shared data objects, either declared as global or shared via a class (see below). There are various restrictions
and hazards around doing this, which are covered in more detail below. Integers, ``bytes`` and ``bytearray`` objects
are commonly used for this purpose along with arrays (from the array module) which can store various data types.
The use of object methods as callbacks
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
MicroPython supports this powerful technique which enables an ISR to share instance variables with the underlying
code. It also enables a class implementing a device driver to support multiple device instances. The following
example causes two LED's to flash at different rates.
.. code:: python
import pyb, micropython
micropython.alloc_emergency_exception_buf(100)
class Foo(object):
def __init__(self, timer, led):
self.led = led
timer.callback(self.cb)
def cb(self, tim):
self.led.toggle()
red = Foo(pyb.Timer(4, freq=1), pyb.LED(1))
green = Foo(pyb.Timer(2, freq=0.8), pyb.LED(2))
In this example the ``red`` instance associates timer 4 with LED 1: when a timer 4 interrupt occurs ``red.cb()``
is called causing LED 1 to change state. The ``green`` instance operates similarly: a timer 2 interrupt
results in the execution of ``green.cb()`` and toggles LED 2. The use of instance methods confers two
benefits. Firstly a single class enables code to be shared between multiple hardware instances. Secondly, as
a bound method the callback function's first argument is ``self``. This enables the callback to access instance
data and to save state between successive calls. For example, if the class above had a variable ``self.count``
set to zero in the constructor, ``cb()`` could increment the counter. The ``red`` and ``green`` instances would
then maintain independent counts of the number of times each LED had changed state.
Creation of Python objects
~~~~~~~~~~~~~~~~~~~~~~~~~~
ISR's cannot create instances of Python objects. This is because MicroPython needs to allocate memory for the
object from a store of free memory block called the heap. This is not permitted in an interrupt handler because
heap allocation is not re-entrant. In other words the interrupt might occur when the main program is part way
through performing an allocation - to maintain the integrity of the heap the interpreter disallows memory
allocations in ISR code.
A consequence of this is that ISR's can't use floating point arithmetic; this is because floats are Python objects. Similarly
an ISR can't append an item to a list. In practice it can be hard to determine exactly which code constructs will
attempt to perform memory allocation and provoke an error message: another reason for keeping ISR code short and simple.
One way to avoid this issue is for the ISR to use pre-allocated buffers. For example a class constructor
creates a ``bytearray`` instance and a boolean flag. The ISR method assigns data to locations in the buffer and sets
the flag. The memory allocation occurs in the main program code when the object is instantiated rather than in the ISR.
The MicroPython library I/O methods usually provide an option to use a pre-allocated buffer. For
example ``pyb.i2c.recv()`` can accept a mutable buffer as its first argument: this enables its use in an ISR.
A means of creating an object without employing a class or globals is as follows:
.. code:: python
def set_volume(t, buf=bytearray(3)):
buf[0] = 0xa5
buf[1] = t >> 4
buf[2] = 0x5a
return buf
The compiler instantiates the default ``buf`` argument when the function is
loaded for the first time (usually when the module it's in is imported).
An instance of object creation occurs when a reference to a bound method is
created. This means that an ISR cannot pass a bound method to a function. One
solution is to create a reference to the bound method in the class constructor
and to pass that reference in the ISR. For example:
.. code:: python
class Foo():
def __init__(self):
self.bar_ref = self.bar # Allocation occurs here
self.x = 0.1
tim = pyb.Timer(4)
tim.init(freq=2)
tim.callback(self.cb)
def bar(self, _):
self.x *= 1.2
print(self.x)
def cb(self, t):
# Passing self.bar would cause allocation.
micropython.schedule(self.bar_ref, 0)
Other techniques are to define and instantiate the method in the constructor
or to pass :meth:`Foo.bar` with the argument *self*.
Use of Python objects
~~~~~~~~~~~~~~~~~~~~~
A further restriction on objects arises because of the way Python works. When an ``import`` statement is executed the
Python code is compiled to bytecode, with one line of code typically mapping to multiple bytecodes. When the code
runs the interpreter reads each bytecode and executes it as a series of machine code instructions. Given that an
interrupt can occur at any time between machine code instructions, the original line of Python code may be only
partially executed. Consequently a Python object such as a set, list or dictionary modified in the main loop
may lack internal consistency at the moment the interrupt occurs.
A typical outcome is as follows. On rare occasions the ISR will run at the precise moment in time when the object
is partially updated. When the ISR tries to read the object, a crash results. Because such problems typically occur
on rare, random occasions they can be hard to diagnose. There are ways to circumvent this issue, described in
:ref:`Critical Sections <Critical>` below.
It is important to be clear about what constitutes the modification of an object. An alteration to a built-in type
such as a dictionary is problematic. Altering the contents of an array or bytearray is not. This is because bytes
or words are written as a single machine code instruction which is not interruptible: in the parlance of real time
programming the write is atomic. A user defined object might instantiate an integer, array or bytearray. It is valid
for both the main loop and the ISR to alter the contents of these.
MicroPython supports integers of arbitrary precision. Values between 2**30 -1 and -2**30 will be stored in
a single machine word. Larger values are stored as Python objects. Consequently changes to long integers cannot
be considered atomic. The use of long integers in ISR's is unsafe because memory allocation may be
attempted as the variable's value changes.
Overcoming the float limitation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In general it is best to avoid using floats in ISR code: hardware devices normally handle integers and conversion
to floats is normally done in the main loop. However there are a few DSP algorithms which require floating point.
On platforms with hardware floating point (such as the Pyboard) the inline ARM Thumb assembler can be used to work
round this limitation. This is because the processor stores float values in a machine word; values can be shared
between the ISR and main program code via an array of floats.
Using micropython.schedule
~~~~~~~~~~~~~~~~~~~~~~~~~~
This function enables an ISR to schedule a callback for execution "very soon". The callback is queued for
execution which will take place at a time when the heap is not locked. Hence it can create Python objects
and use floats. The callback is also guaranteed to run at a time when the main program has completed any
update of Python objects, so the callback will not encounter partially updated objects.
Typical usage is to handle sensor hardware. The ISR acquires data from the hardware and enables it to
issue a further interrupt. It then schedules a callback to process the data.
Scheduled callbacks should comply with the principles of interrupt handler design outlined below. This is to
avoid problems resulting from I/O activity and the modification of shared data which can arise in any code
which pre-empts the main program loop.
Execution time needs to be considered in relation to the frequency with which interrupts can occur. If an
interrupt occurs while the previous callback is executing, a further instance of the callback will be queued
for execution; this will run after the current instance has completed. A sustained high interrupt repetition
rate therefore carries a risk of unconstrained queue growth and eventual failure with a ``RuntimeError``.
If the callback to be passed to `schedule()` is a bound method, consider the
note in "Creation of Python objects".
Exceptions
----------
If an ISR raises an exception it will not propagate to the main loop. The interrupt will be disabled unless the
exception is handled by the ISR code.
Interfacing to uasyncio
-----------------------
When an ISR runs it can preempt the `uasyncio` scheduler. If the ISR performs a `uasyncio`
operation the scheduler's operation can be disrupted. This applies whether the interrupt is hard
or soft and also applies if the ISR has passed execution to another function via
`micropython.schedule`. In particular creating or cancelling tasks is invalid in an ISR context.
The safe way to interact with `uasyncio` is to implement a coroutine with synchronisation performed by
`uasyncio.ThreadSafeFlag`. The following fragment illustrates the creation of a task in response
to an interrupt:
.. code:: python
tsf = uasyncio.ThreadSafeFlag()
def isr(_): # Interrupt handler
tsf.set()
async def foo():
while True:
await tsf.wait()
uasyncio.create_task(bar())
In this example there will be a variable amount of latency between the execution of the ISR and the execution
of ``foo()``. This is inherent to cooperative scheduling. The maximum latency is application
and platform dependent but may typically be measured in tens of ms.
General issues
--------------
This is merely a brief introduction to the subject of real time programming. Beginners should note
that design errors in real time programs can lead to faults which are particularly hard to diagnose. This is because
they can occur rarely and at intervals which are essentially random. It is crucial to get the initial design right and
to anticipate issues before they arise. Both interrupt handlers and the main program need to be designed
with an appreciation of the following issues.
.. _ISR:
Interrupt handler design
~~~~~~~~~~~~~~~~~~~~~~~~
As mentioned above, ISR's should be designed to be as simple as possible. They should always return in a short,
predictable period of time. This is important because when the ISR is running, the main loop is not: inevitably
the main loop experiences pauses in its execution at random points in the code. Such pauses can be a source of hard
to diagnose bugs particularly if their duration is long or variable. In order to understand the implications of
ISR run time, a basic grasp of interrupt priorities is required.
Interrupts are organised according to a priority scheme. ISR code may itself be interrupted by a higher priority
interrupt. This has implications if the two interrupts share data (see Critical Sections below). If such an interrupt
occurs it interposes a delay into the ISR code. If a lower priority interrupt occurs while the ISR is running, it
will be delayed until the ISR is complete: if the delay is too long, the lower priority interrupt may fail. A
further issue with slow ISR's is the case where a second interrupt of the same type occurs during its execution.
The second interrupt will be handled on termination of the first. However if the rate of incoming interrupts
consistently exceeds the capacity of the ISR to service them the outcome will not be a happy one.
Consequently looping constructs should be avoided or minimised. I/O to devices other than to the interrupting device
should normally be avoided: I/O such as disk access, ``print`` statements and UART access is relatively slow, and
its duration may vary. A further issue here is that filesystem functions are not reentrant: using filesystem I/O
in an ISR and the main program would be hazardous. Crucially ISR code should not wait on an event. I/O is acceptable
if the code can be guaranteed to return in a predictable period, for example toggling a pin or LED. Accessing the
interrupting device via I2C or SPI may be necessary but the time taken for such accesses should be calculated or
measured and its impact on the application assessed.
There is usually a need to share data between the ISR and the main loop. This may be done either through global
variables or via class or instance variables. Variables are typically integer or boolean types, or integer or byte
arrays (a pre-allocated integer array offers faster access than a list). Where multiple values are modified by
the ISR it is necessary to consider the case where the interrupt occurs at a time when the main program has
accessed some, but not all, of the values. This can lead to inconsistencies.
Consider the following design. An ISR stores incoming data in a bytearray, then adds the number of bytes
received to an integer representing total bytes ready for processing. The main program reads the number of bytes,
processes the bytes, then clears down the number of bytes ready. This will work until an interrupt occurs just
after the main program has read the number of bytes. The ISR puts the added data into the buffer and updates
the number received, but the main program has already read the number, so processes the data originally received.
The newly arrived bytes are lost.
There are various ways of avoiding this hazard, the simplest being to use a circular buffer. If it is not possible
to use a structure with inherent thread safety other ways are described below.
Reentrancy
~~~~~~~~~~
A potential hazard may occur if a function or method is shared between the main program and one or more ISR's or
between multiple ISR's. The issue here is that the function may itself be interrupted and a further instance of
that function run. If this is to occur, the function must be designed to be reentrant. How this is done is an
advanced topic beyond the scope of this tutorial.
.. _Critical:
Critical sections
~~~~~~~~~~~~~~~~~
An example of a critical section of code is one which accesses more than one variable which can be affected by an ISR. If
the interrupt happens to occur between accesses to the individual variables, their values will be inconsistent. This is
an instance of a hazard known as a race condition: the ISR and the main program loop race to alter the variables. To
avoid inconsistency a means must be employed to ensure that the ISR does not alter the values for the duration of
the critical section. One way to achieve this is to issue ``pyb.disable_irq()`` before the start of the section, and
``pyb.enable_irq()`` at the end. Here is an example of this approach:
.. code:: python
import pyb, micropython, array
micropython.alloc_emergency_exception_buf(100)
class BoundsException(Exception):
pass
ARRAYSIZE = const(20)
index = 0
data = array.array('i', 0 for x in range(ARRAYSIZE))
def callback1(t):
global data, index
for x in range(5):
data[index] = pyb.rng() # simulate input
index += 1
if index >= ARRAYSIZE:
raise BoundsException('Array bounds exceeded')
tim4 = pyb.Timer(4, freq=100, callback=callback1)
for loop in range(1000):
if index > 0:
irq_state = pyb.disable_irq() # Start of critical section
for x in range(index):
print(data[x])
index = 0
pyb.enable_irq(irq_state) # End of critical section
print('loop {}'.format(loop))
pyb.delay(1)
tim4.callback(None)
A critical section can comprise a single line of code and a single variable. Consider the following code fragment.
.. code:: python
count = 0
def cb(): # An interrupt callback
count +=1
def main():
# Code to set up the interrupt callback omitted
while True:
count += 1
This example illustrates a subtle source of bugs. The line ``count += 1`` in the main loop carries a specific race
condition hazard known as a read-modify-write. This is a classic cause of bugs in real time systems. In the main loop
MicroPython reads the value of ``count``, adds 1 to it, and writes it back. On rare occasions the interrupt occurs
after the read and before the write. The interrupt modifies ``count`` but its change is overwritten by the main
loop when the ISR returns. In a real system this could lead to rare, unpredictable failures.
As mentioned above, care should be taken if an instance of a Python built in type is modified in the main code and
that instance is accessed in an ISR. The code performing the modification should be regarded as a critical
section to ensure that the instance is in a valid state when the ISR runs.
Particular care needs to be taken if a dataset is shared between different ISR's. The hazard here is that the higher
priority interrupt may occur when the lower priority one has partially updated the shared data. Dealing with this
situation is an advanced topic beyond the scope of this introduction other than to note that mutex objects described
below can sometimes be used.
Disabling interrupts for the duration of a critical section is the usual and simplest way to proceed, but it disables
all interrupts rather than merely the one with the potential to cause problems. It is generally undesirable to disable
an interrupt for long. In the case of timer interrupts it introduces variability to the time when a callback occurs.
In the case of device interrupts, it can lead to the device being serviced too late with possible loss of data or
overrun errors in the device hardware. Like ISR's, a critical section in the main code should have a short, predictable
duration.
An approach to dealing with critical sections which radically reduces the time for which interrupts are disabled is to
use an object termed a mutex (name derived from the notion of mutual exclusion). The main program locks the mutex
before running the critical section and unlocks it at the end. The ISR tests whether the mutex is locked. If it is,
it avoids the critical section and returns. The design challenge is defining what the ISR should do in the event
that access to the critical variables is denied. A simple example of a mutex may be found
`here <https://github.com/peterhinch/micropython-samples.git>`_. Note that the mutex code does disable interrupts,
but only for the duration of eight machine instructions: the benefit of this approach is that other interrupts are
virtually unaffected.
Interrupts and the REPL
~~~~~~~~~~~~~~~~~~~~~~~
Interrupt handlers, such as those associated with timers, can continue to run
after a program terminates. This may produce unexpected results where you might
have expected the object raising the callback to have gone out of scope. For
example on the Pyboard:
.. code:: python
def bar():
foo = pyb.Timer(2, freq=4, callback=lambda t: print('.', end=''))
bar()
This continues to run until the timer is explicitly disabled or the board is
reset with ``ctrl D``.

View File

@@ -0,0 +1,145 @@
MicroPython manifest files
==========================
When building firmware for a device the following components are included in
the compilation process:
- the core MicroPython virtual machine and runtime
- port-specific system code and drivers to interface with the
microcontroller/device that the firmware is targeting
- standard built-in modules, like ``sys``
- extended built-in modules, like ``json`` and ``machine``
- extra modules written in C/C++
- extra modules written in Python
All the modules included in the firmware are available via ``import`` from
Python code. The extra modules written in Python that are included in a build
(the last point above) are called *frozen modules*, and are specified by a
``manifest.py`` file. Changing this manifest requires rebuilding the firmware.
It's also possible to add additional modules to the filesystem of the device
once it is up and running. Adding and removing modules to/from the filesystem
does not require rebuilding the firmware so is a simpler process than rebuilding
firmware. The benefit of using a manifest is that frozen modules are more
efficient: they are faster to import and take up less RAM once imported.
MicroPython manifest files are Python files and can contain arbitrary Python
code. There are also a set of commands (predefined functions) which are used
to specify the Python source files to include. These commands are described
below.
Freezing source code
--------------------
.. function:: freeze(path, script=None, opt=0)
Freeze the input specified by *path*, automatically determining its type. A
``.py`` script will be compiled to a ``.mpy`` first then frozen, and a
``.mpy`` file will be frozen directly.
*path* must be a directory, which is the base directory to begin searching
for files. When importing the resulting frozen modules, the name of the
module will start after *path*, i.e. *path* is excluded from the module
name.
If *path* is relative, it is resolved to the current ``manifest.py``. Use
``$(MPY_DIR)``, ``$(MPY_LIB_DIR)``, ``$(PORT_DIR)``, ``$(BOARD_DIR)`` if you
need to access specific paths.
If *script* is None, all files in *path* will be frozen.
If *script* is an iterable then ``freeze()`` is called on all items of the
iterable (with the same *path* and *opt* passed through).
If *script* is a string then it specifies the file or directory to freeze,
and can include extra directories before the file or last directory. The
file or directory will be searched for in *path*. If *script* is a
directory then all files in that directory will be frozen.
*opt* is the optimisation level to pass to mpy-cross when compiling ``.py``
to ``.mpy``. These levels are described in :func:`micropython.opt_level`.
.. function:: freeze_as_str(path)
Freeze the given *path* and all ``.py`` scripts within it as a string, which
will be compiled upon import.
.. function:: freeze_as_mpy(path, script=None, opt=0)
Freeze the input by first compiling the ``.py`` scripts to ``.mpy`` files,
then freezing the resulting ``.mpy`` files. See ``freeze()`` for further
details on the arguments.
.. function:: freeze_mpy(path, script=None, opt=0)
Freeze the input, which must be ``.mpy`` files that are frozen directly.
See ``freeze()`` for further details on the arguments.
Including other manifest files
------------------------------
.. function:: include(manifest, **kwargs)
Include another manifest.
The *manifest* argument can be a string (filename) or an iterable of
strings.
Relative paths are resolved with respect to the current manifest file.
Optional *kwargs* can be provided which will be available to the included
script via the *options* variable.
For example:
.. code-block:: python3
include("path.py", extra_features=True)
then in path.py:
.. code-block:: python3
options.defaults(standard_features=True)
# freeze minimal modules.
if options.standard_features:
# freeze standard modules.
if options.extra_features:
# freeze extra modules.
Examples
--------
To freeze a single file which is available as ``import mydriver``, use:
.. code-block:: python3
freeze(".", "mydriver.py")
To freeze a set of files which are available as ``import test1`` and
``import test2``, and which are compiled with optimisation level 3, use:
.. code-block:: python3
freeze("/path/to/tests", ("test1.py", "test2.py"), opt=3)
To freeze a module which can be imported as ``import mymodule``, use:
.. code-block:: python3
freeze(
"../relative/path",
(
"mymodule/__init__.py",
"mymodule/core.py",
"mymodule/extra.py",
),
)
To include a manifest from the MicroPython repository, use:
.. code-block:: python3
include("$(MPY_DIR)/extmod/uasyncio/manifest.py")

View File

@@ -0,0 +1,250 @@
MicroPython remote control: mpremote
====================================
The ``mpremote`` command line tool provides an integrated set of utilities to
remotely interact with and automate a MicroPython device over a serial
connection.
To use mpremote install it via ``pip``:
.. code-block:: bash
$ pip install mpremote
The simplest way to use this tool is just by invoking it without any arguments:
.. code-block:: bash
mpremote
This command automatically detects and connects to the first available serial
device and provides an interactive REPL. Serial ports are opened in exclusive
mode, so running a second (or third, etc) instance of ``mpremote`` will connect
to subsequent serial devices, if any are available.
Commands
--------
For REPL access, running ``mpremote`` without any arguments is usually all that
is needed. ``mpremote`` also supports a set of commands given at the command
line which will perform various actions on remote MicroPython devices.
The full list of supported commands are:
- connect to a specified device via a device-name shortcut:
.. code-block:: bash
$ mpremote <device-shortcut>
- connect to specified device via name:
.. code-block:: bash
$ mpremote connect <device>
``<device>`` may be one of:
- ``list``: list available devices
- ``auto``: connect to the first available device
- ``id:<serial>``: connect to the device with USB serial number
``<serial>`` (the second entry in the output from the ``connect list``
command)
- ``port:<path>``: connect to the device with the given path
- any valid device name/path, to connect to that device
- disconnect current device:
.. code-block:: bash
$ mpremote disconnect
After a disconnect, auto soft-reset is enabled.
- resume a previous ``mpremote`` session:
.. code-block:: bash
$ mpremote resume
This disables auto soft-reset.
- perform a soft-reset of the device:
.. code-block:: bash
$ mpremote soft-reset
This will clear out the Python heap and restart the interpreter. It also
disables auto soft-reset.
- enter the REPL on the connected device:
.. code-block:: bash
$ mpremote repl [options]
Options are:
- ``--capture <file>``, to capture output of the REPL session to the given
file
- ``--inject-code <string>``, to specify characters to inject at the REPL when
Ctrl-J is pressed
- ``--inject-file <file>``, to specify a file to inject at the REPL when
Ctrl-K is pressed
- evaluate and print the result of a Python expression:
.. code-block:: bash
$ mpremote eval <string>
- execute the given Python code:
.. code-block:: bash
$ mpremote exec <string>
- run a script from the local filesystem:
.. code-block:: bash
$ mpremote run <file>
- execute filesystem commands on the device:
.. code-block:: bash
$ mpremote fs <command>
``<command>`` may be:
- ``cat <file..>`` to show the contents of a file or files on the device
- ``ls`` to list the current directory
- ``ls <dirs...>`` to list the given directories
- ``cp [-r] <src...> <dest>`` to copy files; use ":" as a prefix to specify
a file on the device
- ``rm <src...>`` to remove files on the device
- ``mkdir <dirs...>`` to create directories on the device
- ``rmdir <dirs...>`` to remove directories on the device
- mount the local directory on the remote device:
.. code-block:: bash
$ mpremote mount [options] <local-dir>
During usage, Ctrl-D will soft-reboot and normally reconnect the mount automatically.
If the unit has a main.py running at startup however the remount cannot occur.
In this case a raw mode soft reboot can be used: Ctrl-A Ctrl-D to reboot,
then Ctrl-B to get back to normal repl at which point the mount will be ready.
Options are:
- ``-l``, ``--unsafe-links``: By default an error will be raised if the device
accesses a file or directory which is outside (up one or more directory levels) the
local directory that is mounted. This option disables this check for symbolic
links, allowing the device to follow symbolic links outside of the local directory.
- unmount the local directory from the remote device:
.. code-block:: bash
$ mpremote umount
Multiple commands can be specified and they will be run sequentially.
Auto connection and soft-reset
------------------------------
Connection and disconnection will be done automatically at the start and end of
the execution of the tool, if such commands are not explicitly given. Automatic
connection will search for the first available serial device. If no action is
specified then the REPL will be entered.
Once connected to a device, ``mpremote`` will automatically soft-reset the
device if needed. This clears the Python heap and restarts the interpreter,
making sure that subsequent Python code executes in a fresh environment. Auto
soft-reset is performed the first time one of the following commands are
executed: ``mount``, ``eval``, ``exec``, ``run``, ``fs``. After doing a
soft-reset for the first time, it will not be done again automatically, until a
``disconnect`` command is issued.
Auto soft-reset behaviour can be controlled by the ``resume`` command. And the
``soft-reset`` command can be used to perform an explicit soft reset.
Shortcuts
---------
Shortcuts can be defined using the macro system. Built-in shortcuts are::
- ``devs``: list available devices (shortcut for ``connect list``)
- ``a0``, ``a1``, ``a2``, ``a3``: connect to /dev/ttyACM?
- ``u0``, ``u1``, ``u2``, ``u3``: connect to /dev/ttyUSB?
- ``c0``, ``c1``, ``c2``, ``c3``: connect to COM?
- ``cat``, ``ls``, ``cp``, ``rm``, ``mkdir``, ``rmdir``, ``df``: filesystem
commands
- ``reset``: reset the device
- ``bootloader``: make the device enter its bootloader
Any user configuration, including user-defined shortcuts, can be placed in the file
``.config/mpremote/config.py``. For example:
.. code-block:: python3
commands = {
"c33": "connect id:334D335C3138",
"bl": "bootloader",
"double x=4": "eval x*2", # x is an argument, with default 4
"wl_scan": ["exec", """
import network
wl = network.WLAN()
wl.active(1)
for ap in wl.scan():
print(ap)
""",],
"test": ["mount", ".", "exec", "import test"],
}
Examples
--------
.. code-block:: bash
mpremote
mpremote a1
mpremote connect /dev/ttyUSB0 repl
mpremote ls
mpremote a1 ls
mpremote exec "import micropython; micropython.mem_info()"
mpremote eval 1/2 eval 3/4
mpremote mount .
mpremote mount . exec "import local_script"
mpremote ls
mpremote cat boot.py
mpremote cp :main.py .
mpremote cp main.py :
mpremote cp -r dir/ :

View File

@@ -0,0 +1,194 @@
.. _mpy_files:
MicroPython .mpy files
======================
MicroPython defines the concept of an .mpy file which is a binary container
file format that holds precompiled code, and which can be imported like a
normal .py module. The file ``foo.mpy`` can be imported via ``import foo``,
as long as ``foo.mpy`` can be found in the usual way by the import machinery.
Usually, each directory listed in ``sys.path`` is searched in order. When
searching a particular directory ``foo.py`` is looked for first and if that
is not found then ``foo.mpy`` is looked for, then the search continues in the
next directory if neither is found. As such, ``foo.py`` will take precedence
over ``foo.mpy``.
These .mpy files can contain bytecode which is usually generated from Python
source files (.py files) via the ``mpy-cross`` program. For some architectures
an .mpy file can also contain native machine code, which can be generated in
a variety of ways, most notably from C source code.
Versioning and compatibility of .mpy files
------------------------------------------
A given .mpy file may or may not be compatible with a given MicroPython system.
Compatibility is based on the following:
* Version of the .mpy file: the version of the file must match the version
supported by the system loading it.
* Small integer bits: the .mpy file will require a minimum number of bits in
a small integer and the system loading it must support at least this many
bits.
* Native architecture: if the .mpy file contains native machine code then
it will specify the architecture of that machine code and the system
loading it must support execution of that architecture's code.
If a MicroPython system supports importing .mpy files then the
``sys.implementation._mpy`` field will exist and return an integer which
encodes the version (lower 8 bits), features and native architecture.
Trying to import an .mpy file that fails one of the first four tests will
raise ``ValueError('incompatible .mpy file')``. Trying to import an .mpy
file that fails the native architecture test (if it contains native machine
code) will raise ``ValueError('incompatible .mpy arch')``.
If importing an .mpy file fails then try the following:
* Determine the .mpy version and flags supported by your MicroPython system
by executing::
import sys
sys_mpy = sys.implementation._mpy
arch = [None, 'x86', 'x64',
'armv6', 'armv6m', 'armv7m', 'armv7em', 'armv7emsp', 'armv7emdp',
'xtensa', 'xtensawin'][sys_mpy >> 10]
print('mpy version:', sys_mpy & 0xff)
print('mpy flags:', end='')
if arch:
print(' -march=' + arch, end='')
print()
* Check the validity of the .mpy file by inspecting the first two bytes of
the file. The first byte should be an uppercase 'M' and the second byte
will be the version number, which should match the system version from above.
If it doesn't match then rebuild the .mpy file.
* Check if the system .mpy version matches the version emitted by ``mpy-cross``
that was used to build the .mpy file, found by ``mpy-cross --version``.
If it doesn't match then recompile ``mpy-cross`` from the Git repository
checked out at the tag (or hash) reported by ``mpy-cross --version``.
* Make sure you are using the correct ``mpy-cross`` flags, found by the code
above, or by inspecting the ``MPY_CROSS_FLAGS`` Makefile variable for the
port that you are using.
The following table shows the correspondence between MicroPython release
and .mpy version.
=================== ============
MicroPython release .mpy version
=================== ============
v1.19 and up 6
v1.12 - v1.18 5
v1.11 4
v1.9.3 - v1.10 3
v1.9 - v1.9.2 2
v1.5.1 - v1.8.7 0
=================== ============
For completeness, the next table shows the Git commit of the main
MicroPython repository at which the .mpy version was changed.
=================== ========================================
.mpy version change Git commit
=================== ========================================
5 to 6 f2040bfc7ee033e48acef9f289790f3b4e6b74e5
4 to 5 5716c5cf65e9b2cb46c2906f40302401bdd27517
3 to 4 9a5f92ea72754c01cc03e5efcdfe94021120531e
2 to 3 ff93fd4f50321c6190e1659b19e64fef3045a484
1 to 2 dd11af209d226b7d18d5148b239662e30ed60bad
0 to 1 6a11048af1d01c78bdacddadd1b72dc7ba7c6478
initial version 0 d8c834c95d506db979ec871417de90b7951edc30
=================== ========================================
Binary encoding of .mpy files
-----------------------------
MicroPython .mpy files are a binary container format with code objects (bytecode
and native machine code) stored internally in a nested hierarchy. The code for
the outer module is stored first, and then its children follow. Each child may
have further children, for example in the case of a class having methods, or a
function defining a lambda or comprehension. To keep files small while still
providing a large range of possible values it uses the concept of a
variably-encoded-unsigned-integer (vuint) in many places. Similar to utf-8
encoding, this encoding stores 7 bits per byte with the 8th bit (MSB) set
if one or more bytes follow. The bits of the unsigned integer are stored
in the vuint in LSB form.
The top-level of an .mpy file consists of three parts:
* The header.
* The global qstr and constant tables.
* The raw-code for the outer scope of the module.
This outer scope is executed when the .mpy file is imported.
You can inspect the contents of a .mpy file by using ``mpy-tool.py``, for
example (run from the root of the main MicroPython repository)::
$ ./tools/mpy-tool.py -xd myfile.mpy
The header
~~~~~~~~~~
The .mpy header is:
====== ================================
size field
====== ================================
byte value 0x4d (ASCII 'M')
byte .mpy version number
byte feature flags
byte number of bits in a small int
====== ================================
The global qstr and constant tables
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
An .mpy file contains a single qstr table, and a single constant object table.
These are global to the .mpy file, they are referenced by all nested raw-code
objects. The qstr table maps internal qstr number (internal to the .mpy file)
to the resolved qstr number of the runtime that the .mpy file is imported into.
This links the .mpy file with the rest of the system that it executes within.
The constant object table is populated with references to all constant objects
that the .mpy file needs.
====== ================================
size field
====== ================================
vuint number of qstrs
vuint number of constant objects
... qstr data
... encoded constant objects
====== ================================
Raw code elements
~~~~~~~~~~~~~~~~~
A raw-code element contains code, either bytecode or native machine code. Its
contents are:
====== ================================
size field
====== ================================
vuint type, size and whether there are sub-raw-code elements
... code (bytecode or machine code)
vuint number of sub-raw-code elements (only if non-zero)
... sub-raw-code elements
====== ================================
The first vuint in a raw-code element encodes the type of code stored in this
element (the two least-significant bits), whether this raw-code has any
children (the third least-significant bit), and the length of the code that
follows (the amount of RAM to allocate for it).
Following the vuint comes the code itself. Unless the code type is viper code
with relocations, this code is constant data and does not need to be modified.
If this raw-code has any children (as indicated by a bit in the first vuint),
following the code comes a vuint counting the number of sub-raw-code elements.
Finally any sub-raw-code elements are stored, recursively.

View File

@@ -0,0 +1,314 @@
.. _packages:
Distribution packages, package management, and deploying applications
=====================================================================
Just as the "big" Python, MicroPython supports creation of "third party"
packages, distributing them, and easily installing them in each user's
environment. This chapter discusses how these actions are achieved.
Some familiarity with Python packaging is recommended.
Overview
--------
Steps below represent a high-level workflow when creating and consuming
packages:
1. Python modules and packages are turned into distribution package
archives, and published at the Python Package Index (PyPI).
2. :term:`upip` package manager can be used to install a distribution package
on a :term:`MicroPython port` with networking capabilities (for example,
on the Unix port).
3. For ports without networking capabilities, an "installation image"
can be prepared on the Unix port, and transferred to a device by
suitable means.
4. For low-memory ports, the installation image can be frozen as the
bytecode into MicroPython executable, thus minimizing the memory
storage overheads.
The sections below describe this process in details.
Distribution packages
---------------------
Python modules and packages can be packaged into archives suitable for
transfer between systems, storing at the well-known location (PyPI),
and downloading on demand for deployment. These archives are known as
*distribution packages* (to differentiate them from Python packages
(means to organize Python source code)).
The MicroPython distribution package format is a well-known tar.gz
format, with some adaptations however. The Gzip compressor, used as
an external wrapper for TAR archives, by default uses 32KB dictionary
size, which means that to uncompress a compressed stream, 32KB of
contiguous memory needs to be allocated. This requirement may be not
satisfiable on low-memory devices, which may have total memory available
less than that amount, and even if not, a contiguous block like that
may be hard to allocate due to memory fragmentation. To accommodate
these constraints, MicroPython distribution packages use Gzip compression
with the dictionary size of 4K, which should be a suitable compromise
with still achieving some compression while being able to uncompressed
even by the smallest devices.
Besides the small compression dictionary size, MicroPython distribution
packages also have other optimizations, like removing any files from
the archive which aren't used by the installation process. In particular,
:term:`upip` package manager doesn't execute ``setup.py`` during installation
(see below), and thus that file is not included in the archive.
At the same time, these optimizations make MicroPython distribution
packages not compatible with :term:`CPython`'s package manager, ``pip``.
This isn't considered a big problem, because:
1. Packages can be installed with :term:`upip`, and then can be used with
CPython (if they are compatible with it).
2. In the other direction, majority of CPython packages would be
incompatible with MicroPython by various reasons, first of all,
the reliance on features not implemented by MicroPython.
Summing up, the MicroPython distribution package archives are highly
optimized for MicroPython's target environments, which are highly
resource constrained devices.
``upip`` package manager
------------------------
MicroPython distribution packages are intended to be installed using
the :term:`upip` package manager. :term:`upip` is a Python application which is
usually distributed (as frozen bytecode) with network-enabled
:term:`MicroPython ports <MicroPython port>`. At the very least,
:term:`upip` is available in the :term:`MicroPython Unix port`.
On any :term:`MicroPython port` providing :term:`upip`, it can be accessed as
following::
import upip
upip.help()
upip.install(package_or_package_list, [path])
Where *package_or_package_list* is the name of a distribution
package to install, or a list of such names to install multiple
packages. Optional *path* parameter specifies filesystem
location to install under and defaults to the standard library
location (see below).
An example of installing a specific package and then using it::
>>> import upip
>>> upip.install("micropython-pystone_lowmem")
[...]
>>> import pystone_lowmem
>>> pystone_lowmem.main()
Note that the name of Python package and the name of distribution
package for it in general don't have to match, and oftentimes they
don't. This is because PyPI provides a central package repository
for all different Python implementations and versions, and thus
distribution package names may need to be namespaced for a particular
implementation. For example, all packages from `micropython-lib`
follow this naming convention: for a Python module or package named
``foo``, the distribution package name is ``micropython-foo``.
For the ports which run MicroPython executable from the OS command
prompts (like the Unix port), `upip` can be (and indeed, usually is)
run from the command line instead of MicroPython's own REPL. The
commands which corresponds to the example above are::
micropython -m upip -h
micropython -m upip install [-p <path>] <packages>...
micropython -m upip install micropython-pystone_lowmem
[TODO: Describe installation path.]
Cross-installing packages
-------------------------
For :term:`MicroPython ports <MicroPython port>` without native networking
capabilities, the recommend process is "cross-installing" them into a
"directory image" using the :term:`MicroPython Unix port`, and then
transferring this image to a device by suitable means.
Installing to a directory image involves using ``-p`` switch to :term:`upip`::
micropython -m upip install -p install_dir micropython-pystone_lowmem
After this command, the package content (and contents of every dependency
packages) will be available in the ``install_dir/`` subdirectory. You
would need to transfer contents of this directory (without the
``install_dir/`` prefix) to the device, at the suitable location, where
it can be found by the Python ``import`` statement (see discussion of
the :term:`upip` installation path above).
Cross-installing packages with freezing
---------------------------------------
For the low-memory :term:`MicroPython ports <MicroPython port>`, the process
described in the previous section does not provide the most efficient
resource usage,because the packages are installed in the source form,
so need to be compiled to the bytecome on each import. This compilation
requires RAM, and the resulting bytecode is also stored in RAM, reducing
its amount available for storing application data. Moreover, the process
above requires presence of the filesystem on a device, and the most
resource-constrained devices may not even have it.
The bytecode freezing is a process which resolves all the issues
mentioned above:
* The source code is pre-compiled into bytecode and store as such.
* The bytecode is stored in ROM, not RAM.
* Filesystem is not required for frozen packages.
Using frozen bytecode requires building the executable (firmware)
for a given :term:`MicroPython port` from the C source code. Consequently,
the process is:
1. Follow the instructions for a particular port on setting up a
toolchain and building the port. For example, for ESP8266 port,
study instructions in ``ports/esp8266/README.md`` and follow them.
Make sure you can build the port and deploy the resulting
executable/firmware successfully before proceeding to the next steps.
2. Build :term:`MicroPython Unix port` and make sure it is in your PATH and
you can execute ``micropython``.
3. Change to port's directory (e.g. ``ports/esp8266/`` for ESP8266).
4. Run ``make clean-frozen``. This step cleans up any previous
modules which were installed for freezing (consequently, you need
to skip this step to add additional modules, instead of starting
from scratch).
5. Run ``micropython -m upip install -p modules <packages>...`` to
install packages you want to freeze.
6. Run ``make clean``.
7. Run ``make``.
After this, you should have the executable/firmware with modules as
the bytecode inside, which you can deploy the usual way.
Few notes:
1. Step 5 in the sequence above assumes that the distribution package
is available from PyPI. If that is not the case, you would need
to copy Python source files manually to ``modules/`` subdirectory
of the port directory. (Note that upip does not support
installing from e.g. version control repositories).
2. The firmware for baremetal devices usually has size restrictions,
so adding too many frozen modules may overflow it. Usually, you
would get a linking error if this happens. However, in some cases,
an image may be produced, which is not runnable on a device. Such
cases are in general bugs, and should be reported and further
investigated. If you face such a situation, as an initial step,
you may want to decrease the amount of frozen modules included.
Creating distribution packages
------------------------------
Distribution packages for MicroPython are created in the same manner
as for CPython or any other Python implementation, see references at
the end of chapter. Setuptools (instead of distutils) should be used,
because distutils do not support dependencies and other features. "Source
distribution" (``sdist``) format is used for packaging. The post-processing
discussed above, (and pre-processing discussed in the following section)
is achieved by using custom ``sdist`` command for setuptools. Thus, packaging
steps remain the same as for the standard setuptools, the user just
needs to override ``sdist`` command implementation by passing the
appropriate argument to ``setup()`` call::
from setuptools import setup
import sdist_upip
setup(
...,
cmdclass={'sdist': sdist_upip.sdist}
)
The sdist_upip.py module as referenced above can be found in
`micropython-lib`:
https://github.com/micropython/micropython-lib/blob/master/sdist_upip.py
Application resources
---------------------
A complete application, besides the source code, oftentimes also consists
of data files, e.g. web page templates, game images, etc. It's clear how
to deal with those when application is installed manually - you just put
those data files in the filesystem at some location and use the normal
file access functions.
The situation is different when deploying applications from packages - this
is more advanced, streamlined and flexible way, but also requires more
advanced approach to accessing data files. This approach is treating
the data files as "resources", and abstracting away access to them.
Python supports resource access using its "setuptools" library, using
``pkg_resources`` module. MicroPython, following its usual approach,
implements subset of the functionality of that module, specifically
``pkg_resources.resource_stream(package, resource)`` function.
The idea is that an application calls this function, passing a
resource identifier, which is a relative path to data file within
the specified package (usually top-level application package). It
returns a stream object which can be used to access resource contents.
Thus, the ``resource_stream()`` emulates interface of the standard
`open()` function.
Implementation-wise, ``resource_stream()`` uses file operations
underlyingly, if distribution package is install in the filesystem.
However, it also supports functioning without the underlying filesystem,
e.g. if the package is frozen as the bytecode. This however requires
an extra intermediate step when packaging application - creation of
"Python resource module".
The idea of this module is to convert binary data to a Python bytes
object, and put it into the dictionary, indexed by the resource name.
This conversion is done automatically using overridden ``sdist`` command
described in the previous section.
Let's trace the complete process using the following example. Suppose
your application has the following structure::
my_app/
__main__.py
utils.py
data/
page.html
image.png
``__main__.py`` and ``utils.py`` should access resources using the
following calls::
import pkg_resources
pkg_resources.resource_stream(__name__, "data/page.html")
pkg_resources.resource_stream(__name__, "data/image.png")
You can develop and debug using the :term:`MicroPython Unix port` as usual.
When time comes to make a distribution package out of it, just use
overridden "sdist" command from sdist_upip.py module as described in
the previous section.
This will create a Python resource module named ``R.py``, based on the
files declared in ``MANIFEST`` or ``MANIFEST.in`` files (any non-``.py``
file will be considered a resource and added to ``R.py``) - before
proceeding with the normal packaging steps.
Prepared like this, your application will work both when deployed to
filesystem and as frozen bytecode.
If you would like to debug ``R.py`` creation, you can run::
python3 setup.py sdist --manifest-only
Alternatively, you can use tools/mpy_bin2res.py script from the
MicroPython distribution, in which can you will need to pass paths
to all resource files::
mpy_bin2res.py data/page.html data/image.png
References
----------
* Python Packaging User Guide: https://packaging.python.org/
* Setuptools documentation: https://setuptools.readthedocs.io/
* Distutils documentation: https://docs.python.org/3/library/distutils.html

View File

@@ -0,0 +1,146 @@
.. _pyboard_py:
The pyboard.py tool
===================
This is a standalone Python tool that runs on your PC that provides a way to:
* Quickly run a Python script or command on a MicroPython device. This is useful
while developing MicroPython programs to quickly test code without needing to
copy files to/from the device.
* Access the filesystem on a device. This allows you to deploy your code to the
device (even if the board doesn't support USB MSC).
Despite the name, ``pyboard.py`` works on all MicroPython ports that support the
raw REPL (including STM32, ESP32, ESP8266, NRF).
You can download the latest version from `GitHub
<https://github.com/micropython/micropython/blob/master/tools/pyboard.py>`_. The
only dependency is the ``pyserial`` library which can be installed from PiPy or
your system package manager.
Running ``pyboard.py --help`` gives the following output:
.. code-block:: text
usage: pyboard [-h] [-d DEVICE] [-b BAUDRATE] [-u USER] [-p PASSWORD]
[-c COMMAND] [-w WAIT] [--follow | --no-follow] [-f]
[files [files ...]]
Run scripts on the pyboard.
positional arguments:
files input files
optional arguments:
-h, --help show this help message and exit
-d DEVICE, --device DEVICE
the serial device or the IP address of the pyboard
-b BAUDRATE, --baudrate BAUDRATE
the baud rate of the serial device
-u USER, --user USER the telnet login username
-p PASSWORD, --password PASSWORD
the telnet login password
-c COMMAND, --command COMMAND
program passed in as string
-w WAIT, --wait WAIT seconds to wait for USB connected board to become
available
--follow follow the output after running the scripts
[default if no scripts given]
-f, --filesystem perform a filesystem action: cp local :device | cp
:device local | cat path | ls [path] | rm path | mkdir
path | rmdir path
Running a command on the device
-------------------------------
This is useful for testing short snippets of code, or to script an interaction
with the device.::
$ pyboard.py --device /dev/ttyACM0 -c 'print(1+1)'
2
If you are often interacting with the same device, you can set the environment
variable ``PYBOARD_DEVICE`` as an alternative to using the ``--device``
command line option. For example, the following is equivalent to the previous
example::
$ export PYBOARD_DEVICE=/dev/ttyACM0
$ pyboard.py -c 'print(1+1)'
Similarly, the ``PYBOARD_BAUDRATE`` environment variable can be used
to set the default for the ``--baudrate`` option.
Running a script on the device
------------------------------
If you have a script, ``app.py`` that you want to run on a device, then use::
$ pyboard.py --device /dev/ttyACM0 app.py
Note that this doesn't actually copy app.py to the device's filesystem, it just
loads the code into RAM and executes it. Any output generated by the program
will be displayed.
If the program app.py does not finish then you'll need to stop ``pyboard.py``,
eg with Ctrl-C. The program ``app.py`` will still continue to run on the
MicroPython device.
Filesystem access
-----------------
Using the ``-f`` flag, the following filesystem operations are supported:
* ``cp src [src...] dest`` Copy files to/from the device.
* ``cat path`` Print the contents of a file on the device.
* ``ls [path]`` List contents of a directory (defaults to current working directory).
* ``rm path`` Remove a file.
* ``mkdir path`` Create a directory.
* ``rmdir path`` Remove a directory.
The ``cp`` command uses a ``ssh``-like convention for referring to local and
remote files. Any path starting with a ``:`` will be interpreted as on the
device, otherwise it will be local. So::
$ pyboard.py --device /dev/ttyACM0 -f cp main.py :main.py
will copy main.py from the current directory on the PC to a file named main.py
on the device. The filename can be omitted, e.g.::
$ pyboard.py --device /dev/ttyACM0 -f cp main.py :
is equivalent to the above.
Some more examples::
# Copy main.py from the device to the local PC.
$ pyboard.py --device /dev/ttyACM0 -f cp :main.py main.py
# Same, but using . instead.
$ pyboard.py --device /dev/ttyACM0 -f cp :main.py .
# Copy three files to the device, keeping their names
# and paths (note: `lib` must exist on the device)
$ pyboard.py --device /dev/ttyACM0 -f cp main.py app.py lib/foo.py :
# Remove a file from the device.
$ pyboard.py --device /dev/ttyACM0 -f rm util.py
# Print the contents of a file on the device.
$ pyboard.py --device /dev/ttyACM0 -f cat boot.py
...contents of boot.py...
Using the pyboard library
-------------------------
You can also use ``pyboard.py`` as a library for scripting interactions with a
MicroPython board.
.. code-block:: python
import pyboard
pyb = pyboard.Pyboard('/dev/ttyACM0', 115200)
pyb.enter_raw_repl()
ret = pyb.exec('print(1+1)')
print(ret)
pyb.exit_raw_repl()

View File

@@ -0,0 +1,300 @@
The MicroPython Interactive Interpreter Mode (aka REPL)
=======================================================
This section covers some characteristics of the MicroPython Interactive
Interpreter Mode. A commonly used term for this is REPL (read-eval-print-loop)
which will be used to refer to this interactive prompt.
Auto-indent
-----------
When typing python statements which end in a colon (for example if, for, while)
then the prompt will change to three dots (...) and the cursor will be indented
by 4 spaces. When you press return, the next line will continue at the same
level of indentation for regular statements or an additional level of indentation
where appropriate. If you press the backspace key then it will undo one
level of indentation.
If your cursor is all the way back at the beginning, pressing RETURN will then
execute the code that you've entered. The following shows what you'd see
after entering a for statement (the underscore shows where the cursor winds up):
>>> for i in range(30):
... _
If you then enter an if statement, an additional level of indentation will be
provided:
>>> for i in range(30):
... if i > 3:
... _
Now enter ``break`` followed by RETURN and press BACKSPACE:
>>> for i in range(30):
... if i > 3:
... break
... _
Finally type ``print(i)``, press RETURN, press BACKSPACE and press RETURN again:
>>> for i in range(30):
... if i > 3:
... break
... print(i)
...
0
1
2
3
>>>
Auto-indent won't be applied if the previous two lines were all spaces. This
means that you can finish entering a compound statement by pressing RETURN
twice, and then a third press will finish and execute.
Auto-completion
---------------
While typing a command at the REPL, if the line typed so far corresponds to
the beginning of the name of something, then pressing TAB will show
possible things that could be entered. For example, first import the machine
module by entering ``import machine`` and pressing RETURN.
Then type ``m`` and press TAB and it should expand to ``machine``.
Enter a dot ``.`` and press TAB again. You should see something like:
>>> machine.
__name__ info unique_id reset
bootloader freq rng idle
sleep deepsleep disable_irq enable_irq
Pin
The word will be expanded as much as possible until multiple possibilities exist.
For example, type ``machine.Pin.AF3`` and press TAB and it will expand to
``machine.Pin.AF3_TIM``. Pressing TAB a second time will show the possible
expansions:
>>> machine.Pin.AF3_TIM
AF3_TIM10 AF3_TIM11 AF3_TIM8 AF3_TIM9
>>> machine.Pin.AF3_TIM
Interrupting a running program
------------------------------
You can interrupt a running program by pressing Ctrl-C. This will raise a KeyboardInterrupt
which will bring you back to the REPL, providing your program doesn't intercept the
KeyboardInterrupt exception.
For example:
>>> for i in range(1000000):
... print(i)
...
0
1
2
3
...
6466
6467
6468
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
KeyboardInterrupt:
>>>
Paste mode
----------
If you want to paste some code into your terminal window, the auto-indent feature
will mess things up. For example, if you had the following python code: ::
def foo():
print('This is a test to show paste mode')
print('Here is a second line')
foo()
and you try to paste this into the normal REPL, then you will see something like
this:
>>> def foo():
... print('This is a test to show paste mode')
... print('Here is a second line')
... foo()
...
Traceback (most recent call last):
File "<stdin>", line 3
IndentationError: unexpected indent
If you press Ctrl-E, then you will enter paste mode, which essentially turns off
the auto-indent feature, and changes the prompt from ``>>>`` to ``===``. For example:
>>>
paste mode; Ctrl-C to cancel, Ctrl-D to finish
=== def foo():
=== print('This is a test to show paste mode')
=== print('Here is a second line')
=== foo()
===
This is a test to show paste mode
Here is a second line
>>>
Paste Mode allows blank lines to be pasted. The pasted text is compiled as if
it were a file. Pressing Ctrl-D exits paste mode and initiates the compilation.
Soft reset
----------
A soft reset will reset the python interpreter, but tries not to reset the
method by which you're connected to the MicroPython board (USB-serial, or Wifi).
You can perform a soft reset from the REPL by pressing Ctrl-D, or from your python
code by executing: ::
machine.soft_reset()
For example, if you reset your MicroPython board, and you execute a dir()
command, you'd see something like this:
>>> dir()
['__name__', 'pyb']
Now create some variables and repeat the dir() command:
>>> i = 1
>>> j = 23
>>> x = 'abc'
>>> dir()
['j', 'x', '__name__', 'pyb', 'i']
>>>
Now if you enter Ctrl-D, and repeat the dir() command, you'll see that your
variables no longer exist:
.. code-block:: python
MPY: sync filesystems
MPY: soft reboot
MicroPython v1.5-51-g6f70283-dirty on 2015-10-30; PYBv1.0 with STM32F405RG
Type "help()" for more information.
>>> dir()
['__name__', 'pyb']
>>>
The special variable _ (underscore)
-----------------------------------
When you use the REPL, you may perform computations and see the results.
MicroPython stores the results of the previous statement in the variable _ (underscore).
So you can use the underscore to save the result in a variable. For example:
>>> 1 + 2 + 3 + 4 + 5
15
>>> x = _
>>> x
15
>>>
Raw mode and raw-paste mode
---------------------------
Raw mode (also called raw REPL) is not something that a person would normally use.
It is intended for programmatic use and essentially behaves like paste mode with
echo turned off, and with optional flow control.
Raw mode is entered using Ctrl-A. You then send your python code, followed by
a Ctrl-D. The Ctrl-D will be acknowledged by 'OK' and then the python code will
be compiled and executed. Any output (or errors) will be sent back. Entering
Ctrl-B will leave raw mode and return the the regular (aka friendly) REPL.
Raw-paste mode is an additional mode within the raw REPL that includes flow control,
and which compiles code as it receives it. This makes it more robust for high-speed
transfer of code into the device, and it also uses less RAM when receiving because
it does not need to store a verbatim copy of the code before compiling (unlike
standard raw mode).
Raw-paste mode uses the following protocol:
#. Enter raw REPL as usual via ctrl-A.
#. Write 3 bytes: ``b"\x05A\x01"`` (ie ctrl-E then "A" then ctrl-A).
#. Read 2 bytes to determine if the device entered raw-paste mode:
* If the result is ``b"R\x00"`` then the device understands the command but
doesn't support raw paste.
* If the result is ``b"R\x01"`` then the device does support raw paste and
has entered this mode.
* Otherwise the result should be ``b"ra"`` and the device doesn't support raw
paste and the string ``b"w REPL; CTRL-B to exit\r\n>"`` should be read and
discarded.
#. If the device is in raw-paste mode then continue, otherwise fallback to
standard raw mode.
#. Read 2 bytes, this is the flow control window-size-increment (in bytes)
stored as a 16-bit unsigned little endian integer. The initial value for the
remaining-window-size variable should be set to this number.
#. Write out the code to the device:
* While there are bytes to send, write up to the remaining-window-size worth
of bytes, and decrease the remaining-window-size by the number of bytes
written.
* If the remaining-window-size is 0, or there is a byte waiting to read, read
1 byte. If this byte is ``b"\x01"`` then increase the remaining-window-size
by the window-size-increment from step 5. If this byte is ``b"\x04"`` then
the device wants to end the data reception, and ``b"\x04"`` should be
written to the device and no more code sent after that. (Note: if there is
a byte waiting to be read from the device then it does not need to be read
and acted upon immediately, the device will continue to consume incoming
bytes as long as reamining-window-size is greater than 0.)
#. When all code has been written to the device, write ``b"\x04"`` to indicate
end-of-data.
#. Read from the device until ``b"\x04"`` is received. At this point the device
has received and compiled all of the code that was sent and is executing it.
#. The device outputs any characters produced by the executing code. When (if)
the code finishes ``b"\x04"`` will be output, followed by any exception that
was uncaught, followed again by ``b"\x04"``. It then goes back to the
standard raw REPL and outputs ``b">"``.
For example, starting at a new line at the normal (friendly) REPL, if you write::
b"\x01\x05A\x01print(123)\x04"
Then the device will respond with something like::
b"\r\nraw REPL; CTRL-B to exit\r\n>R\x01\x80\x00\x01\x04123\r\n\x04\x04>"
Broken down over time this looks like::
# Step 1: enter raw REPL
write: b"\x01"
read: b"\r\nraw REPL; CTRL-B to exit\r\n>"
# Step 2-5: enter raw-paste mode
write: b"\x05A\x01"
read: b"R\x01\x80\x00\x01"
# Step 6-8: write out code
write: b"print(123)\x04"
read: b"\x04"
# Step 9: code executes and result is read
read: b"123\r\n\x04\x04>"
In this case the flow control window-size-increment is 128 and there are two
windows worth of data immediately available at the start, one from the initial
window-size-increment value and one from the explicit ``b"\x01"`` value that
is sent. So this means up to 256 bytes can be written to begin with before
waiting or checking for more incoming flow-control characters.
The ``tools/pyboard.py`` program uses the raw REPL, including raw-paste mode, to
execute Python code on a MicroPython-enabled board.

View File

@@ -0,0 +1,348 @@
.. _speed_python:
Maximising MicroPython speed
============================
.. contents::
This tutorial describes ways of improving the performance of MicroPython code.
Optimisations involving other languages are covered elsewhere, namely the use
of modules written in C and the MicroPython inline assembler.
The process of developing high performance code comprises the following stages
which should be performed in the order listed.
* Design for speed.
* Code and debug.
Optimisation steps:
* Identify the slowest section of code.
* Improve the efficiency of the Python code.
* Use the native code emitter.
* Use the viper code emitter.
* Use hardware-specific optimisations.
Designing for speed
-------------------
Performance issues should be considered at the outset. This involves taking a view
on the sections of code which are most performance critical and devoting particular
attention to their design. The process of optimisation begins when the code has
been tested: if the design is correct at the outset optimisation will be
straightforward and may actually be unnecessary.
Algorithms
~~~~~~~~~~
The most important aspect of designing any routine for performance is ensuring that
the best algorithm is employed. This is a topic for textbooks rather than for a
MicroPython guide but spectacular performance gains can sometimes be achieved
by adopting algorithms known for their efficiency.
RAM allocation
~~~~~~~~~~~~~~
To design efficient MicroPython code it is necessary to have an understanding of the
way the interpreter allocates RAM. When an object is created or grows in size
(for example where an item is appended to a list) the necessary RAM is allocated
from a block known as the heap. This takes a significant amount of time;
further it will on occasion trigger a process known as garbage collection which
can take several milliseconds.
Consequently the performance of a function or method can be improved if an object is created
once only and not permitted to grow in size. This implies that the object persists
for the duration of its use: typically it will be instantiated in a class constructor
and used in various methods.
This is covered in further detail :ref:`Controlling garbage collection <controlling_gc>` below.
Buffers
~~~~~~~
An example of the above is the common case where a buffer is required, such as one
used for communication with a device. A typical driver will create the buffer in the
constructor and use it in its I/O methods which will be called repeatedly.
The MicroPython libraries typically provide support for pre-allocated buffers. For
example, objects which support stream interface (e.g., file or UART) provide ``read()``
method which allocates new buffer for read data, but also a ``readinto()`` method
to read data into an existing buffer.
Floating point
~~~~~~~~~~~~~~
Some MicroPython ports allocate floating point numbers on heap. Some other ports
may lack dedicated floating-point coprocessor, and perform arithmetic operations
on them in "software" at considerably lower speed than on integers. Where
performance is important, use integer operations and restrict the use of floating
point to sections of the code where performance is not paramount. For example,
capture ADC readings as integers values to an array in one quick go, and only then
convert them to floating-point numbers for signal processing.
Arrays
~~~~~~
Consider the use of the various types of array classes as an alternative to lists.
The `array` module supports various element types with 8-bit elements supported
by Python's built in `bytes` and `bytearray` classes. These data structures all store
elements in contiguous memory locations. Once again to avoid memory allocation in critical
code these should be pre-allocated and passed as arguments or as bound objects.
When passing slices of objects such as `bytearray` instances, Python creates
a copy which involves allocation of the size proportional to the size of slice.
This can be alleviated using a `memoryview` object. The `memoryview` itself
is allocated on the heap, but is a small, fixed-size object, regardless of the size
of slice it points too. Slicing a `memoryview` creates a new `memoryview`, so this
cannot be done in an interrupt service routine. Further, the slice syntax ``a:b``
causes further allocation by instantiating a ``slice(a, b)`` object.
.. code:: python
ba = bytearray(10000) # big array
func(ba[30:2000]) # a copy is passed, ~2K new allocation
mv = memoryview(ba) # small object is allocated
func(mv[30:2000]) # a pointer to memory is passed
A `memoryview` can only be applied to objects supporting the buffer protocol - this
includes arrays but not lists. Small caveat is that while memoryview object is live,
it also keeps alive the original buffer object. So, a memoryview isn't a universal
panacea. For instance, in the example above, if you are done with 10K buffer and
just need those bytes 30:2000 from it, it may be better to make a slice, and let
the 10K buffer go (be ready for garbage collection), instead of making a
long-living memoryview and keeping 10K blocked for GC.
Nonetheless, `memoryview` is indispensable for advanced preallocated buffer
management. ``readinto()`` method discussed above puts data at the beginning
of buffer and fills in entire buffer. What if you need to put data in the
middle of existing buffer? Just create a memoryview into the needed section
of buffer and pass it to ``readinto()``.
Identifying the slowest section of code
---------------------------------------
This is a process known as profiling and is covered in textbooks and
(for standard Python) supported by various software tools. For the type of
smaller embedded application likely to be running on MicroPython platforms
the slowest function or method can usually be established by judicious use
of the timing ``ticks`` group of functions documented in `time`.
Code execution time can be measured in ms, us, or CPU cycles.
The following enables any function or method to be timed by adding an
``@timed_function`` decorator:
.. code:: python
def timed_function(f, *args, **kwargs):
myname = str(f).split(' ')[1]
def new_func(*args, **kwargs):
t = time.ticks_us()
result = f(*args, **kwargs)
delta = time.ticks_diff(time.ticks_us(), t)
print('Function {} Time = {:6.3f}ms'.format(myname, delta/1000))
return result
return new_func
MicroPython code improvements
-----------------------------
The const() declaration
~~~~~~~~~~~~~~~~~~~~~~~
MicroPython provides a ``const()`` declaration. This works in a similar way
to ``#define`` in C in that when the code is compiled to bytecode the compiler
substitutes the numeric value for the identifier. This avoids a dictionary
lookup at runtime. The argument to ``const()`` may be anything which, at
compile time, evaluates to an integer e.g. ``0x100`` or ``1 << 8``.
.. _Caching:
Caching object references
~~~~~~~~~~~~~~~~~~~~~~~~~~
Where a function or method repeatedly accesses objects performance is improved
by caching the object in a local variable:
.. code:: python
class foo(object):
def __init__(self):
self.ba = bytearray(100)
def bar(self, obj_display):
ba_ref = self.ba
fb = obj_display.framebuffer
# iterative code using these two objects
This avoids the need repeatedly to look up ``self.ba`` and ``obj_display.framebuffer``
in the body of the method ``bar()``.
.. _controlling_gc:
Controlling garbage collection
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When memory allocation is required, MicroPython attempts to locate an adequately
sized block on the heap. This may fail, usually because the heap is cluttered
with objects which are no longer referenced by code. If a failure occurs, the
process known as garbage collection reclaims the memory used by these redundant
objects and the allocation is then tried again - a process which can take several
milliseconds.
There may be benefits in pre-empting this by periodically issuing `gc.collect()`.
Firstly doing a collection before it is actually required is quicker - typically on the
order of 1ms if done frequently. Secondly you can determine the point in code
where this time is used rather than have a longer delay occur at random points,
possibly in a speed critical section. Finally performing collections regularly
can reduce fragmentation in the heap. Severe fragmentation can lead to
non-recoverable allocation failures.
The Native code emitter
-----------------------
This causes the MicroPython compiler to emit native CPU opcodes rather than
bytecode. It covers the bulk of the MicroPython functionality, so most functions will require
no adaptation (but see below). It is invoked by means of a function decorator:
.. code:: python
@micropython.native
def foo(self, arg):
buf = self.linebuf # Cached object
# code
There are certain limitations in the current implementation of the native code emitter.
* Context managers are not supported (the ``with`` statement).
* Generators are not supported.
* If ``raise`` is used an argument must be supplied.
The trade-off for the improved performance (roughly twice as fast as bytecode) is an
increase in compiled code size.
The Viper code emitter
----------------------
The optimisations discussed above involve standards-compliant Python code. The
Viper code emitter is not fully compliant. It supports special Viper native data types
in pursuit of performance. Integer processing is non-compliant because it uses machine
words: arithmetic on 32 bit hardware is performed modulo 2**32.
Like the Native emitter Viper produces machine instructions but further optimisations
are performed, substantially increasing performance especially for integer arithmetic and
bit manipulations. It is invoked using a decorator:
.. code:: python
@micropython.viper
def foo(self, arg: int) -> int:
# code
As the above fragment illustrates it is beneficial to use Python type hints to assist the Viper optimiser.
Type hints provide information on the data types of arguments and of the return value; these
are a standard Python language feature formally defined here `PEP0484 <https://www.python.org/dev/peps/pep-0484/>`_.
Viper supports its own set of types namely ``int``, ``uint`` (unsigned integer), ``ptr``, ``ptr8``,
``ptr16`` and ``ptr32``. The ``ptrX`` types are discussed below. Currently the ``uint`` type serves
a single purpose: as a type hint for a function return value. If such a function returns ``0xffffffff``
Python will interpret the result as 2**32 -1 rather than as -1.
In addition to the restrictions imposed by the native emitter the following constraints apply:
* Functions may have up to four arguments.
* Default argument values are not permitted.
* Floating point may be used but is not optimised.
Viper provides pointer types to assist the optimiser. These comprise
* ``ptr`` Pointer to an object.
* ``ptr8`` Points to a byte.
* ``ptr16`` Points to a 16 bit half-word.
* ``ptr32`` Points to a 32 bit machine word.
The concept of a pointer may be unfamiliar to Python programmers. It has similarities
to a Python `memoryview` object in that it provides direct access to data stored in memory.
Items are accessed using subscript notation, but slices are not supported: a pointer can return
a single item only. Its purpose is to provide fast random access to data stored in contiguous
memory locations - such as data stored in objects which support the buffer protocol, and
memory-mapped peripheral registers in a microcontroller. It should be noted that programming
using pointers is hazardous: bounds checking is not performed and the compiler does nothing to
prevent buffer overrun errors.
Typical usage is to cache variables:
.. code:: python
@micropython.viper
def foo(self, arg: int) -> int:
buf = ptr8(self.linebuf) # self.linebuf is a bytearray or bytes object
for x in range(20, 30):
bar = buf[x] # Access a data item through the pointer
# code omitted
In this instance the compiler "knows" that ``buf`` is the address of an array of bytes;
it can emit code to rapidly compute the address of ``buf[x]`` at runtime. Where casts are
used to convert objects to Viper native types these should be performed at the start of
the function rather than in critical timing loops as the cast operation can take several
microseconds. The rules for casting are as follows:
* Casting operators are currently: ``int``, ``bool``, ``uint``, ``ptr``, ``ptr8``, ``ptr16`` and ``ptr32``.
* The result of a cast will be a native Viper variable.
* Arguments to a cast can be a Python object or a native Viper variable.
* If argument is a native Viper variable, then cast is a no-op (i.e. costs nothing at runtime)
that just changes the type (e.g. from ``uint`` to ``ptr8``) so that you can then store/load
using this pointer.
* If the argument is a Python object and the cast is ``int`` or ``uint``, then the Python object
must be of integral type and the value of that integral object is returned.
* The argument to a bool cast must be integral type (boolean or integer); when used as a return
type the viper function will return True or False objects.
* If the argument is a Python object and the cast is ``ptr``, ``ptr``, ``ptr16`` or ``ptr32``,
then the Python object must either have the buffer protocol (in which case a pointer to the
start of the buffer is returned) or it must be of integral type (in which case the value of
that integral object is returned).
Writing to a pointer which points to a read-only object will lead to undefined behaviour.
The following example illustrates the use of a ``ptr16`` cast to toggle pin X1 ``n`` times:
.. code:: python
BIT0 = const(1)
@micropython.viper
def toggle_n(n: int):
odr = ptr16(stm.GPIOA + stm.GPIO_ODR)
for _ in range(n):
odr[0] ^= BIT0
A detailed technical description of the three code emitters may be found
on Kickstarter here `Note 1 <https://www.kickstarter.com/projects/214379695/micro-python-python-for-microcontrollers/posts/664832>`_
and here `Note 2 <https://www.kickstarter.com/projects/214379695/micro-python-python-for-microcontrollers/posts/665145>`_
Accessing hardware directly
---------------------------
.. note::
Code examples in this section are given for the Pyboard. The techniques
described however may be applied to other MicroPython ports too.
This comes into the category of more advanced programming and involves some knowledge
of the target MCU. Consider the example of toggling an output pin on the Pyboard. The
standard approach would be to write
.. code:: python
mypin.value(mypin.value() ^ 1) # mypin was instantiated as an output pin
This involves the overhead of two calls to the :class:`~machine.Pin` instance's :meth:`~machine.Pin.value()`
method. This overhead can be eliminated by performing a read/write to the relevant bit
of the chip's GPIO port output data register (odr). To facilitate this the ``stm``
module provides a set of constants providing the addresses of the relevant registers.
A fast toggle of pin ``P4`` (CPU pin ``A14``) - corresponding to the green LED -
can be performed as follows:
.. code:: python
import machine
import stm
BIT14 = const(1 << 14)
machine.mem16[stm.GPIOA + stm.GPIO_ODR] ^= BIT14