llpy 开发基础
llpy 是一款本项目专用的自研 Python 解释运行引擎,也可以叫做 Python 虚拟机。它和 PC 上标准版的 Python 以及 ARM 的 MicroPython 都兼容 Python 3.x 语法,但又有着本质的不同:
- llpy 从一开始就是为 ESP32 或类似低内存、低性能嵌入式硬件开发的,对 ESP32 上极为敏感的内存使用以及代码效率做了很多特殊设计。
- llpy 的设计目标是希望将 ESP32 用于控制面板、交互式设备、AI 玩具、人机智能等更复杂的应用领域,而非传统 IoT 小节点。因此重点支持了 LVGL、音频、摄像头等能力。
与 MicroPython 的差异
- Python 3.x 绝大部分的语法和语言特性都实现了,应该是目前支持得较为全面的引擎,但同时又有着不输于标准版 Python 的性能。
- 借鉴了 FFI 的理念并将其完全融合进了 Python 的语法,使得 C 语言 API 的导入变得简单,内存和 Bin 占用都不大。
语言支持情况
- 所有一元和二元数学运算全部支持。整数默认为 32 位整型数,但支持 64 位数的表示、传参、存储和展示。如果要进行数学计算,需要使用内置的
bigint库(无须 import 直接使用),如bigint.lsh、bigint.rsh、bigint.or、bigint.and、bigint.invert、bigint.fdiv、bigint.mod等。 def定义的函数可以拥有一个层级的闭包函数,但是闭包函数中不可以再定义闭包函数。闭包捕捉的宏默认未开启,因此闭包函数不可以对父级函数域的变量进行写操作。try...except不支持跨函数捕捉异常,except Exception as e这个e变量仅仅是一个字符串,就是异常的文本信息。- 所有内置常用库的名称、函数名、参数数量、用途等,都和标准版 Python 3.x 一样。可用的内置库有:
date、datetime、dict、list、tuple、json、math、random、os、string、sys、threading、time。 - 全局变量需预先定义,在 module 范围内赋任意初值即创建了全局变量,不能在运行时使用
global动态创建新的全局变量。 - 可以在任意位置进行
import,但在 import 所在的函数退出的时候模块就会被销毁。 - 赋值语句只支持单对单
a = b、多对多a,b,c = c,b,a和自动解包a,b = c。 - 字符串全部默认为 UTF-8 编码。
- 使用
match...case代替if...elif,尤其是 match 下所有 case 的比较值全部都是常量的时候,性能相比if...elif组合快不少。 list/tuple/str/bytes这些支持下标访问的类型仅在 get/set/数组 slice 时可以使用负数索引。*vargs和**kargs需要写在最后面,如果有**kargs则必须是最后一个参数,否则*vargs必须是最后一个参数。
未支持或部分支持的语法
- List 和 Dict 的推导式暂不支持。
@staticmethod、@classmethod等绝大多数装饰器暂不支持。isinstance(x, object)会报错,因为不是所有的类型都派生自object。如果想要判断一个变量是否是某类的实例,使用isinstance(x, instance),这个instance是本引擎独有的类型。
性能提示
- 如果只是简单的计数循环,
while的性能比for更好。 - 直接使用变量或全局变量,肯定比把变量放在 Dict 或 Class 中再通过间接操作效率要高得多。
FFI 与指针
- llpy 添加了一种新的变量类型名为
pointer,也就是指针。该变量类型是为了与 C 语言更好的交互而设计的。它支持了多数 C 语言中指针变量的特性,可以参与二元计数、通过指针操作结构体成员、作为参数直接传给 C 函数等。 - 有大量的函数是通过 C 语言直接映射的方式导入 llpy 的,最典型的是
lvgl库以及 ESP-IDF 框架的大部分 API。这部分类和函数的手册请参考 API 文档。 - 可以在 Python 代码中使用
'''!C {block} '''来直接嵌入 C 语言的头文件,或者使用#!include "{C文件路径}"来包含 C 语言头文件。C 头文件中被处理的只有常量(#define、static/const以及enum定义的量)和struct/union,函数定义会被忽略。
示例:
python
'''!C
typedef struct {
int16_t x;
int16_t y;
} lv_point_t;
'''
pt = malloc(C.lv_point_t)
pt.C.x = 100 # 写值
print(pt.C.x) # 读值规则就是所有的 C 成员变量的使用前面必须加上特殊属性 C。如果不加 C 而直接访问如 pt.x 也是可以的,但是性能会有略微损失。同一个 .py 文件中的所有 C 语言代码,其 struct 成员变量名称都是全局的,可能需要对 struct 成员进行改名以防止重名。
RGB565/RGB565Swap/membersizeof/offsetof这 4 个函数是编译期函数,其值在编译后就直接被替换为常量了。