8000
We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
The Viper code emitter uses special Viper native data types to get faster performance. The largest advantage is for integer arithmetic, bit manipulations and integer array operations.
Read the official documentation here: https://docs.micropython.org/en/latest/reference/speed_python.html
# Original Python function def add_to_array( a, n ): sum_array = 0 for i in range(len(a)): a[i] += n sum_array += a[i] return sum_array # This decorator allows taking advantage of the Viper data types: @micropython.viper # The function declaration uses type hints (type annotations # to cast parameters # and return value to/from viper data types. # pa is a pointer to memory (very fast) # Since pa does not carry information about the array length, # a third argument with the length is needed def viper_add_to_array( pa:ptr32, n:int, length:int)->int: sum_array = 0 i = 0 while i < length: # while is a bit faster than for...range # Same instructions now use fast integer arithmetic pa[i] += n # Pointers are used like arrays sum_array += pa[i] i += 1 return sum_array my_array = array.array("l", (i for i in range(10000))) add_to_array( my_array, 10 ) viper_add_to_array( my_array, 10, len(my_array) )
This Viper function is about 16 times faster on a ESP32-S3 with PSRAM wih an array of 10000.
In this example, the original add_to_array() function with @micropython.native decorator is about 1.6 times faster than the original function.
Some have reported much higher performance gains!
The @micropython.viper decorator is applied to functions, including nested functions and methods. It also can be applied to an entire class and to interrupt service routines (ISR).
Viper code is compiled and runs very fast, especially when using the Viper data types. However line by line error reporting and interrupt from the console via control-C do not work while in Viper code (no problem, just reset the microcontroller when stuck in a loop).
The @micropython.viper directive is a compile-time directive and activates the Viper code emitter. The Viper emits machine code and additonally does static (compile-time) analysis of the code to determine integer variables and emits special (very fast) machine code to handle integer operations. It also activates the very fast pointer data types.
All things nice that MicroPython does, will continue to work. What is affected is mostly how integer variables and access to arrays work.
The Viper extensions are Python language extensions and not fully compliant with Python, so differences are to be expected.
The @micropython.native decorator is another means to speed up code, but does not require special data types or constructs. It covers most of the MicroPython language functionality without change, except a very few restrictions.
When not using the Viper data types, performance of Viper and native is similar. In fact, the Viper code emitter is an extension of the native code emitter. However since most code has at least some integer variables, Viper code may be faster than native code, sometimes even without change.
Advantages of the @micropython.native decorator: no change to the code is needed.
Advantage of the @micropython.viper decorator: the result can be faster, especially if integer and array operations are involved. But it is necessary to change the code.
These data types are very fast. They are not implemented as an MicroPython object but as a raw variable. They can be only used within a Viper decorated function.
Most of the difficulties using the Viper code emitter are related to the use of these data types and their peculiarities. So here goes a lot of detail about these data types.
Viper variables are "raw" variables and are not stored as MicroPython objects. In contrast the string, tuple, list and integer variables we all know are always stored as MicroPython objects.
The Viper code emitter detects Viper variables at compile time, and generates very fast code for the operations. For example x = 0 or x = int(myfunction()) will make x Viper int variable. Now, x = x + 1 will be compiled around 2 or 3 machine code instructions!
x = 0
x = int(myfunction())
x
int
x = x + 1
Compile time means: when the .py file is analyzed by the MicroPython interpreter, or when mpy-cross is run.
Please note that once assigned, the type of a Viper variable cannot be changed (unlike regular Python), which is quite reasonable since there is no underlying object:
x = 1 x = "hello" # This changes the Viper int variable x to a string object, not allowed # The previous line raises a compile time error: # ViperTypeError: local 'x' has type 'int' but source is 'object' # The reverse order is also not allowed.
Be aware: The Viper code emitter analyzes of the code at compile time, determining the type of the variables. This is very unusual when coming from a Python background, where typing is dynamic and at runtime. On the other hand, most problems with Viper variables are detected at compile time, before the program even runs, which is very nice!
In case you are familiar with C: The Viper data types are similar to some C language data types:
long int
uint
unsigned long int
bool
ptr32
*long int
ptr16
*unsigned short int
ptr8
*unsigned char
int()
uint()
bool()
ptr8()
ptr16()
ptr32()
builtins.int
import builtins
The Viper intdata type in Viper code is a special data type for fast signed integer operations. A Viper int can hold values from -2**31 to 2**31-1, i.e. this is a 32 bit signed integer.
A Viper int is different to the int we know in MicroPython, which is still available in Viper decorated functions as builtins.int. Hence this document will make a difference between a "Viper int" opposed to a builtins.int.
It is advisable to be aware at all times that Viper int and builtins.int are different data types.
Viper int
Viper integer constants are in the range -2**29 to 2**29-1. When you assign a viper constant to a variable, it automatically is a Viper int.
Be aware: integer constants don't have the full range of values a Viper int value can hold, they are signed 30 bit integers.
Integer expressions are evaluated compile time and reduced to a constant.
As it is usual in Python, a Viper variable is of type Viper `int when you assign Viper intvalue, either as a constant, integer expression or with the int() function. for example:
x = 0 y = int(some_function_returning_an_integer()) z = 1 + y # now x, y and z are Viper int variables p = 2**3+1
If the variable is created by assigning an expression, the Viper code emitter will evaluate the expression at compile time.
Be aware: Integer constant expressions outside of what is called the "small integer" range of MicroPython are not Viper int but builtins.int. On most architectures a MicroPython small integer falls is -2**29 and 2**29-1.
For example:
@micropython.viper def myfunction(); x = 0xffffffff # this is not a Viper int y = 1<<30 # this is not a Viper int z = 2**31-1 # this is not a Viper int
In all these cases a builtins.int variable will be created. See here for a way prevent the problems described here.
A second way to get a Viper int is with a type hint (type annotation) of a function parameter:
@micropython.viper def myfunction(x:int):
With the type hint, x is converted on the fly to the Viper int data type using the Viper int() function (see "int() casting" below).
There is a possible source of problems: when you initialize a Viper int with a integer expression that falls outside of the Viper int range (which is not the 32 bit range!), a builtins.int will be created instead, no warning. The same happens if you try initialize a Viper int with a variable of type builtins.int. These errors can go unnoticed.
Solution: Except for very short Viper functions, you could initialize all Viper int variables at the beginning setting them to zero (just as you might do in C language):
@micropython.viper def myfunction(x:int)->int: # declare all my integer variables x = 0 limit = 0 step = 0
This defines the type of the variable clearly as Viper int. Any attempt to change the type later will give a nice compile-time message ViperTypeError: local 'x' has type 'int' but source is 'object', for example:
ViperTypeError: local 'x' has type 'int' but source is 'object'
x = 0 y = 0 ...some code ... x = 2**30 # 2**30 yields a builtins.int ... some more code ... y = "hello" # oh, some confusion here, can't change Viper int to string
Another way to make sure Viper variables are always of the intended type, is to use the type cast:
x = int(some expression)
But this is a perhaps a little bit less readable.
Viper int variables allow values from -2**31 to 2**31-1, whereas builtins.int variables have no practical range limit. For a builtins.int, if the value grows a lot, more memory will be allocated as needed.
As a result, arithmetic operations on Viper variables behave like operations in the C language 32 bit signed integer operations, for example:
131072*32768=0
x<<1
Arithmetic and logic operations for Viper int are very fast, since there is no need to check for data types, conversion rules and other conditions at runtime, and the necessary code can be generated at compile time.
Integer expressions that include Viper int are of type Viper int, example:
@micropython.viper def viper_expression(): x = 1 print(x<<31) # the value printed is -2147483648
Although x<<31 is not being assigned to a Viper int, the expression is truncated to the size of a Viper int before passing it to the called function (print). This is a behavior a bit different from integer constant expressions, where the expression is evaluated, and then tested if the result fits into a Viper int or builtins.int.
x<<31
There are no automatic conversion rules if a Viper int is used together with other data types. For example, this code will raise a compile time error: "ViperTypeError: can't do binary op between 'object' and 'int'":
@micropython.viper def myfunction(my_argument): x:int = 2 x = my_argument + 1 # <- ViperTypeError: local 'x' has type 'int' but source is 'object' my_float_variable = 1.0 my_float_variable = my_float_variable + x # <-- ViperTypeError: can't do binary op between 'object' and 'int' myfunction(1)
The 'object' in the error message refers to my_argument and my_float_variable. The 'int' in the error message refers to the 1 Viper int constant.
my_argument
my_float_variable
1
To avoid that error message, the Viper intvariable x must be converted explicitly to float, and my_argument cast to a Viper int.
@micropython.viper def myfunction(my_argument): x:int = 2 x = int(my_argument) + 1 # <- ViperTypeError: local 'x' has type 'int' but source is 'object' my_float_variable = 1.0 my_float_variable = my_float_variable + float(x) # <-- ViperTypeError: can't do binary op between 'object' and 'int' myfunction(1)
A Viper int is not an object, and thus does not support methods such as from_bytes()or to_bytes().
from_bytes()
to_bytes()
The ** operator (exponentiation, __pow__) is not implemented for Viper int.
__pow__
Be aware: In versions MicroPython 1.22 and prior, unary minus is not implemented, instead of x=-a use x=0-a. In version 1.23 the unary minus is being implemented, but not completely yet, so until further confirmation it's best to avoid unary minus, and use subtraction from zero instead.
x=-a
x=0-a
Be aware: Do not use shift left or right with a negative value, i.e. x<<(-1) or x>>(-1) should not be used because the result is undefined. This mirrors the C language definition for shifting. Unlike regular MicroPython, there is no check (no exception raised) for negative shift amounts.
x<<(-1)
x>>(-1)
Be aware: If you are using a ESP32 or ESP32-S3 (or any XTENSAWIN processor, in MicroPython parlance), do not shift left by more than 31 bits. The result should be zero, but isn't. The RP2040 is not affected. Not tested yet for other processors. The workaround is to check if the shift amount is larger than 31 before shifting.
Within Viper decorated functions, the int() function will cast an expression to a Viper int. Examples:
x = int(len(some_array)) # Many MicroPython functions return builtins.int x = int(2**30) # \*\* is not implemented for Viper int and returns a builtins.int x = int(1) # Here int() is not necessary x = int(1+2) # Here int() is not necessary, 1+2 is a Viper int expression x = int(my_int_function())+1 # Use int() for any external function that returns a integer
int("123") is rejected, the argument has to be a Viper uint or a builtins.int.
int("123")
The int() function will return the 4 least significant bytes of the integer, similar to a C language expression: x && 0xffffffff. If it is unclear that the input value is in the viper int range, the value has to be tested before casting. But in many practical applications, you can know beforehand the acceptable value ranges, and no additional overhead is incurred.
x && 0xffffffff
In other words, beware: int() just truncates values outside of the Viper int range chopping off the excessive bytes, no exception raised.
int() casting is very fast in Viper code.
This data type is in most aspects similar to Viper int but the range is 0 to 2**32-1, i.e. it's an unsigned 32 bit integer. Typical uses could be:
ptr
The uint() cast function will return the last 4 bytes of builtins.int as a unsigned 32 bit int.
Viper uint does not support // (integer division) nor % (module) operators
//
%
Casting from uint to int and back just changes the type. There is no change in the data itself, the int() and uint() functions are a no-op for this case. Example:
@micropython.viper def test_uint_int_assignments(): x = int(-1) y = uint(x) print(f"{x=} uint(x)={y=:08x}, expected 0xffffffff") z = int(y) print(f"{y=} int(y)={y=:08x}, expected 0xffffffff")
A bool Viper variable can be True or False. Boolean expressions are evaluated using the viper bool data type. This makes logic computations fast.
You create a bool Viper variable by assigning True, False or the result of a logical expression of constants and Viper bool variables that yields True or False, for example:
True
False
@micropython.viper def function_with_bools(): a = True y = 1 z = 2 b = y < z c = bool(y) # Now a, b and c are Viper bool variables # and a, b, and c are True
You can convert a int or uint to bool using the bool() cast function. A zero value is interpreted as False, any value different from zero is interpreted as True.
Similar to builtins.int for the Viper int data type, builtins.bool can be used to have the MicroPython boolean data type available.
builtins.bool
As a note of minor interest, the bool Viper variable is stored as a 32 bit integer:
@micropython.viper def cast_bools(): x = int(12345) b = bool(x) y = int(b) # Now y holds the same value as x
The explanation of this behavior is that similar to int() and uint(), bool() is a cast operator. If used on a Viper variable, only the type is changed, no conversion takes place (this is unlike the C (bool) cast, where the integer is converted to 0 or 1).
These data types are pointers to memory, similar to a C language long *p; or unsigned char *p. This is rather unusual for Python, where no pointers exist and memory access is well hidden within objects that protect that access.
long *p;
unsigned char *p
If x is for example a ptr32, x[0] is the four bytes at the address the pointer is pointing to, x[1] the next four bytes, etc.
You can assign to x[n], modifying the memory contents. There is no bounds checking, so a runaway index can destroy unintended memory locations. This could block the microcontroller. Don't panic: this is recoverable with a hard reset. In very bad cases, it might be required to flash the MicroPython image again, but there is nothing to worry: it´s not feasible to brick the microcontroller with a runaway pointer.
@micropython.viper def myfunction( x:ptr32, b:bool )->int: print(x[0], x[1], x[2] ) # will print 1, 2, 3 return x[1] myfunction( array.array("l", (1,2,3,4)))
@micropython.viper def myfunction( )->int: int32_array = array.array("l", (1,2,3,4)) x = ptr32( int32_array ) print(x[0], x[1], x[2] ) # this will print 1, 2, 3 ba = bytearray(10) y = ptr8(ba) y[0] = 1 # This will change ba[0] return x[1]
You can also cast a integer to a pointer:
@micropython.viper def myfunction()->int: GPIO_OUT = ptr32(0x60000300) # GPIO base register GPIO_OUT[2] = 0x10 # clear pin 4
The argument to ptr32(), ptr16() or ptr8() can be a Viper int, a uint or a bultins.int, no difference. Only the part needed for an address will be extracted.
You will have to search the microcontroller data sheet for the correct locations and meaning of each bit of the device registers. However, this type of manipulation can be very fast. Be aware: on a ESP32, MicroPython runs on top of FreeRTOS, which steals some CPU cycles every now and then, and can cause small but unwanted delays in Viper code.
The uctypes module has an addressof() function. The result can also be converted to a pointer:
uctypes
addressof()
import uctypes @micropython.viper def fun(): ba = bytearray(10) pba = ptr8( uctypes.addressof(ba) )
This also can be used to point at uctypes structures.
Be aware: A bytes object could be cast to a ptr8, but bytes objects are meant to be readonly, not to be modified.
bytes
If x is a ptr32, ptr16 or ptr8, x[n] will return a Viper 32 bit signed integer.
The type of the object pointed to by the ptr variable is irrelevant. You could, for example, retrieve two elements of a "h" array with a single ptr32 x[n] assignment.
If x is a ptr16, x[n] will always be between 0 and 2**16-1.
If x is a ptr8, x[n] will always be between 0 and 255.
If x is a ptr8, x[n] = v will extract the least significant byte of the Viper integer vand modify the byte at x[n]
x[n] = v
v
If x is a ptr16, x[n] = v will extract the least two significant bytes of the Viper integer vand modify the two byte at x[n]
If x is a ptr32, x[n] = v will modify the four bytes at x[n] with the Viper integer v.
x[n]
In all cases you will need to convert to a Viper int first.
These functions are similar to ptr8, ptr16 and ptr32, but the Viper pointers are significantly faster.
Viper pointers can be cast to a uint and back to ptr32, enabling to do pointer arithmetic. For example:
@micropython.viper def fun(): a = array("i", (11,22,33,44)) len_of_array:int = 4 x:ptr32 = ptr32(a) pointer_to_second_half_of_a:ptr32 = ptr32(uint(x) + (int(len(a))//2)*4 )
Note that since the array element length is 4 bytes, you have to multiply by 4 yourself. The ptr32, ptr16 and ptr8 addresses are byte addresses.
Be aware: Some architectures may reject ptr32 access of pointers that are not multiple of four. Accessing odd bytes will most probably crash the program, no way to trap that as an exception.
From the point of view of the caller, Viper functions behave just like any other MicroPython functions. The workings of the Viper variables is hidden from the caller. The Viper data types are not visible outside the Viper function.
The static analysis that MicroPython does, is Viper function by Viper function. No type hint information, nor the fact that they are Viper functions is carried over from the analysis of one function to another.
The call overhead for a Viper function is substantially lower than call overhead for a undecorated function. For example, for a function with 5 parameters, the call processing time with Viper may be 2 times faster than a undecorated function, including the time to convert to and from the Viper data types.
For integer parameters, use the int or uint type hint to get automatic conversion to a Viper int. The conversion is done internally by MicroPython using the int() or uint() cast operator respectively:
@micropython.viper def my_function( x:int, z:uint ): # now x and z are Viper data type variables ....
Similarly, a boolean parameter is cast automatically to a Viper bool:
@micropython.viper def my_function( b:bool ): # b is a Viper bool variable ....
For arrays and bytearrays, use the ptr32, ptr16 and ptr8 type hints in the function parameters to get fast access to the arrays. The cast from an array to a pointer is done automatically while processing the call, i.e. a ptr8(), ptr16() or ptr32() cast is applied automatically to the argument.
@micropython.viper def my_function( p:ptr32 ): .... a = array.array("l", (0 for x in range(100))) my_function( a )
Viper functions do not accept keyword arguments nor optional arguments.
Some older versions of the MicroPython docs state that there is a maximum of 4 arguments for a Viper function. That is not a restriction anymore (apparently since MicroPython 1.14 or so).
In a Viper decorated function, you can certainly call another function. The called function can be @micropython.viper decorated, @micropython.native decorated or plain (undecorated), a bound or unbound method and you can use a generator (however: no await of an async function inside a Viper function).
@micropython.viper
@micropython.native
If you pass a Viper variable as argument to a function, it gets converted to a builtins.int on the fly:
@micropython.viper def viperfun(): x = int(1) # x now is a Viper int some_function(x) # some_function will get 1 y = uint(0xffffffff) some_function(y) # some_function will get 0xffffffff == 4294967295 z = int(-1) some_function(z) # some_function will get a -1 ba = bytearray(10) pba = ptr8(ba) some_function(pba) # # # some_function will get a number like 1008145600, which is the address of ba, no sign
The rationale here is that the Viper data types don't make sense outside the Viper function, so they are converted to standard MicroPython builtins.int when passed as parameters. The pointers don't carry information about the type, so they can't be cast back to an array. If you wish to use a returned pointer, you have to cast it back to a pointer explicitly in a Viper function or use functions like machine.mem8(), machine.mem16() or machine.mem32().
A nice effect of this is that you can pass a pointer down to a Viper function:
@micropython.viper def fun1(): ba = bytearray(10) pba = ptr8(ba) # Call another Viper function, pass a pointer fun2(pba) @micropython.viper def fun2( mypointer:ptr8 ): # mypointer is now pointing to the bytearray ba x = mypointer[0]
A side effect of this behavior is that type(viper_variable) always returns class builtins.int, because the Viper variable is converted to a builtins.int during the call process.
type(viper_variable)
Talking about detecting type: inside a Viper function, isinstance(viper_variable,int) will give a compile-time error NotImplementedError: conversion to object, since int is a Viper data type, not a MicroPython class. However, isinstance(viper_variable, builtins.int) will return True since the viper_variable will be converted to a MicroPython builtins.int automatically during the call process. This also applies to bool.
isinstance(viper_variable,int)
NotImplementedError: conversion to object
isinstance(viper_variable, builtins.int)
viper_variable
If the function returns a Viper variable, a return type hint must be supplied, for example:
@micropython.viper def function_returns_integer(param1:int)->int: return 1 @micropython.viper def function_returns_bool(x:int)->bool: return True
The conversion of the return value back to builtins.bool is done automatically.
You can return a pointer in a Viper function, but you must add the return type hint as ->ptr8, ->ptr16 or ->ptr32. The pointer returned is converted to a builtins.int and it's value will be the memory address of the array. The addresses are always byte addresses. The function that uses that returned integer must cast it to a pointer of the correct type to make things work, for example:
@micropython.viper def function_returning_pointer()->ptr8: ba = bytearray(10) pointer_to_ba = ptr8(ba) pointer_to_ba[0] = 123 # Return a pointer to a bytearray return pointer_to_ba @micropython.viper def function_using_returned_pointer( ): mypointer = ptr8(function_returning_pointer()) # mypointer is now pointing to the bytearray ba x = int(mypointer[0]) print(f"x has the value 123: {x=}")
Returned pointers can also be used with machine.mem8 for ptr8, machine.mem16 for ptr16 and machine.mem32 for ptr32 addresses. The machine.mem objects are certainly slower than Viper pointer operations.
machine.mem8
machine.mem16
machine.mem32
machine.mem
If the value returned by the function is any other object (i.e. if the value returned is not a Viper data type), you do not need to specify a type hint. If you wish, you can use ->object as return type hint, for example:
->object
@micropython.viper # MicroPython object returned, no return type hint required def function_returns_something(x): if x > 10: return (1,2,3) if x > 0: return True if x < -10: return None return x @micropython.viper # ->object can be optionally used as return type hint # for any MicroPython object (except Viper data types) def function_returns_object(x)->object: return (1,2,3)
range() does work under Viper, so you could write: for x in range(10). It is a bit faster to use a while loop, with viper ints for j, limit and step.
range()
for x in range(10)
limit:int = 100 step:int = 2 j:int = start while j < limit: ...loop body.... j += step
You can also use pointer arithmetic to get rid of a counter to control the while loop, and that's even faster! Using pointer arithmetic also enables to use p[0] instead of p[i], again an improvement. This timing was done on a 80Mhz Cortex-M3 in the W600:
p[0]
p[i]
from array import array from time import ticks_us, ticks_diff a = array('i', (2*i+1 for i in range(10000))) @micropython.viper def vip_add1(p: ptr32, l: int) -> int: r:int = 0 for k in range(l): # range() works, but is not fastest r += p[k] # accessing with p[i] return r @micropython.viper def vip_add2(p: ptr32, l: uint) -> int: r:int = 0 k:uint = uint(0) while k < l: # a while loop is faster r += p[k] # accessing with p[i] k += 1 return r @micropython.viper def vip_add3(p: ptr32, l: uint) -> int: r:int = 0 pstop: uint = uint(p) + 4*l while uint(p) < pstop: # directly looping the pointer to the array and indexing r += p[0] # with p[0] is faster than accessing with p[i] p = ptr32(uint(p) + 4) # casts necessary because the types are fixed in viper return r t0 = ticks_us() sum1 = vip_add1(a, len(a)) # --> 100000000 t1 = ticks_us() sum2 = vip_add2(a, len(a)) # --> 100000000 t2 = ticks_us() sum3 = vip_add3(a, len(a)) # --> 100000000 t3 = ticks_us() print('vip_add1:', sum1, 'time:', ticks_diff(t1, t0)/10000, 'µs per it.') # --> 0.7064 µs print 8000 span>('vip_and2:', sum2, 'time:', ticks_diff(t2, t1)/10000, 'µs per it.') # --> 0.5767 µs print('vip_and3:', sum3, 'time:', ticks_diff(t3, t2)/10000, 'µs per it.') # --> 0.3635 µs
If you need to do integer arithmetic with a global variable, this works:
import builtins x = 1 g = None @micropython.viper def global_test(): global x, g viper_int:int = 333 g = viper_int x = x + builtins.int(10) print(x) # x now is 11 and g is now 333
You can assign a Viper integer to a global variable, it gets converted to a builtins.int.
The global variable x is of type builtins.int and you cannot mix Viper int with builtins.int. In the example, 10 is a Viper int constant and has to be converted to a builtins.int before operating.
10
For Viper bool variables, similar rules apply.
If you access nonlocal integer variables that belong to a non-Viper function, make sure the expression you assign to that is a builtin.int. Assigning a Viper int to a nonlocal variable does nothing.
builtin.int
Here is a working example of a closure:
import builtins def foo(): x = 0 @micropython.viper def inner() -> int: nonlocal x x = builtins.int( int(x)+1 ) return int(x) return inner bar = foo() bar() bar() bar()
Since x is a non-Viper integer, we have to use non-Viper arithmetic in the inner function to make this work.
In the previous example, if foo() is decorated with @micropython.viper, we get a compile time message complaining about x (ViperTypeError: local 'x' used before type known). Since x is not an object but a raw Viper variable, it cannot be referred to as a nonlocal.
foo()
nonlocal
You can't make a Viper variable nonlocal (compile-time error ViperTypeError: local 'x' used before type known)
ViperTypeError: local 'x' used before type known
Beware: You can't change the type of a nonlocal variable inside a viper function to an integer. Example:
def nonlocal_fails(): y = None @micropython.viper def internal_viper(): nonlocal y viperx:int = 111 y = viperx # <--- this assignment will not work! return y return internal_viper() print(nonlocal_fails(), "expected result 111")
The actual result is 55, but depends on the value assigned (111). The device may freeze or give any error, so don't do this.
A specific method (including __init__, @staticmethod and @classmethod) can have the @micropython.viper decorator.
__init__
@staticmethod
@classmethod
The complete class can be decorated:
@micropython.viper class MyClass: def __init__( self ): self.a = 10 # __init__ will be a Viper decorated function, by inclusion
Instance variables such as self.a can only be MicroPython objects and can never be of a Viper data type (remember that a Viper int is not an object).
self.a
You can assign a Viper int to a instance variable like self.x. The Viper int gets converted to a builtins.int automatically, Operations such self.x = self.x + viper_integer requiere to convert the Viper integer to a builtins.int: self.x = self.x + builtins.int(viper_integer)
self.x = self.x + viper_integer
self.x = self.x + builtins.int(viper_integer)
Viper integers cannot be used in slices. This is a restriction. The following code will not work:
x = bytearray((1,2,3,4,5)) print("function slice", x[0:2])
This is a workaround: x[builtins.int(0):builtins.int(2)]
x[builtins.int(0):builtins.int(2)]
Viper decorated functions cannot have the async attribute (it crashes) nor be generators (NotImplementedError: native yield compile time error`)
NotImplementedError: native yield
Workaround: async functions and generators can call Viper functions.
However, a Viper function can call a generator.
Type hints in the body of the of a Viper function are not required, but add nicely to readability. So although not mandatory, it's perhaps more readable to declare the variables with type hints:
@micropython.viper def myfunction(): # declare all my integer variables x:int = 0 limit:int = 0 step:int = 0
You can't use builtins.int as type hint, and there is no type statement in MicroPython. So builtins.int will be always written without type hint.
type
In compile time:
# Test if x is a Viper int variable x = "hello"
If x is a Viper variable, the assignment will fail at compile time.
In runtime, to distinguish between a Viper int and a builtins.int:
x = 1 if x << 31 < 0: print("x is a Viper int")
The expression in the if statement will be true if x is a signed Viper int, as opposed to a builtins.int. A builtins.int will remain positive, no matter how many times you shift the value.
The Viper code emitter is in the MicroPython code repository in py/emitnative.c, embedded in the native code emitter.
py/emitnative.c
This is a compile time error
In this context, 'int' means a Viper int and 'object' any other MicroPython object including a builtins.int. The most common cause is trying to do an arithmetic or logic operation of a Viper int and a builtins.int.
Another example for this error message is to combine a Viper int with a float: if x is a Viper int, then f = 1.0 + x will raise this error. Use f = 1.0 + float(x) instead. Similarly with
f = 1.0 + x
f = 1.0 + float(x)
This compile time error happens when x is of type Viper int but an object is later assigned to x, for example:
x = 0 x = "hello"
It's likely that the 'object' is builtins.int. You have to cast that with int() to a Viper int.
`
A cause of this can be doing, for example, int("1234"). The Viper int() is a casting operator and does not convert. A workaround could be int(builtins.int("1234"))
int("1234")
int(builtins.int("1234"))
The official documentation: https://docs.micropython.org/en/v1.9.3/pyboard/reference/speed_python.html
Damien George's talk on MicroPython performance: https://www.youtube.com/watch?v=hHec4qL00x0
Interesting discussion about Viper, parameters and optimization. Also see Damien George's comment on Viper data types and casting: https://forum.micropython.org/viewtopic.php?f=2&t=1382
How to use the Viper decorator. Several examples of Viper and GPIO registers. https://forum.micropython.org/viewtopic.php?f=6&t=6994
Closure and some interesting low level stuff: https://github.com/micropython/micropython/issues/8086
Slices and Viper: https://github.com/micropython/micropython/issues/6523
32 bit integer operations: https://github.com/orgs/micropython/discussions/11259
Another example manipulating manipulating GPIO: https://forum.micropython.org/viewtopic.php?f=18&t=8266
A TFT display driver using Viper code, look at TFT_io.py: https://github.com/robert-hh/SSD1963-TFT-Library-for-PyBoard-and-RP2040
Use of Viper decorator: https://github.com/orgs/micropython/discussions/11157
Step by step with a real problem: https://luvsheth.com/p/making-micropython-computations-run
The MicroPython tests for Viper have some examples, see all cases prefixed by "viper_": https://github.com/micropython/micropython/tree/master/tests/micropython
The issue that gave origin to this wiki page, with interesting discussions. This also is the source for some examples here: #14297
Search https://forum.micropython.org and https://github.com/orgs/micropython/discussions for Viper. There are many insights and examples.
Some Viper code examples here: https://github.com/bixb922/viper-examples, including a integer FFT (Fast Fourier Transform) and autocorrelation. Many examples to test or demonstrate how viper works.