MicroPython是为在微控制器上运行而设计的,受微控制器硬件资源的限制,传统计算机程序员对这部分可能不太熟悉,特别是RAM和非易失性存储器“磁盘”(闪存)容量的限制,本文提供了一些方法,可以充分利用这些有限的资源。由于MicroPython运行于各种架构,因此提供的方法是通用的:在某些情况下可能需要上从特定平台的文档中获取详细信息。
闪存
在Pyboard上,解决容量问题最简单的方法就是安装一个Micro SD卡,但在某些情况可能会不太现实,比如没有SD卡插槽、成本或功耗过高等原因,因此必须使用芯片上的闪存。固件和MicroPython子系统都存储在板载闪存,剩余的容量可供程序使用,由于闪存物理体系结构的原因,这部分剩余容量可能无法作为文件系统访问,在这种情况下,可以将用户模块和固件合并构建来利用这部分空间,构建后把固件刷到设备上。
有两种方法可以实现合并构建:冻结模块和冻结字节码。冻结模块把Python源代码与固件存在一起,冻结字节码使用交叉编译器将源代码转换为字节码,然后与固件一起存储。这两种情况都可以使用import引入模块。
import mymodule
生成的冻结模块和字节码与平台相关,构建固件的说明可以在源代码树相关部分的 README文件中找到。
生成的步骤如下:
- 克隆MicroPython仓库
- 获取固件构建(平台专用)工具链
- 交叉编译
- 将需要冻结的模块放到指定的目录(取决于模块是比冻结为源代码还是字节码)。
- 构建固件,可能需要一个特定的命令来构建两种类型的冻结代码。
- 将固件刷入设备。
内存(RAM)
在减少内存使用量时,需要考虑两个阶段:编译和执行。除了内存消耗外,还有一个被称为堆碎片的问题。一般来说,最好尽量减少重复创建和销毁对象。其原因将在有关堆的章节中阐述。
编译阶段
模块被导入后,MircoPython将源码编译为字节码,然后由MicroPython虚拟机(VM)执行。字节码存储在内存中,所以编译器自身占用部分内存,当编译完成后这部分内存就会被释放。
如果导入大量的模块,可能会出现内存不足导致无法编译的情况,此时导入语句将抛出内存异常。
模块在导入时实例化全局对象会占用内存,这可能会影响到编译器的后续操作。比较好的做法是在所有模块导入完成后再执行代码,这样能让编译器最大化使用内存。
如果编译所有模块时仍然内存不足,方案之一是可以通过预编译模块来应对。MicroPython有对Python模块交叉编译的能力(请参考mpy-cross目录下的README文件内容),编译后的字节码文件以.mpy
为扩展名,可以将其复制到文件系统并以常规方式导入。另外,也可以把部分或全部模块作为冻结字节码来实现,因为字节码可以直接运行,所以在大多数平台会更节省内存。
执行阶段
下面是一些在执行阶段减少内存使用的技术:
常量
MicroPython提供了const
关键字,可以这样用:
from micropython import const
ROWS = const(33)
_COLS = const(0x10)
a = ROWS
b = _COLS
针对这两种把常量赋值给变量的方式,编译器会通过替换常量的字面值来避免对常量名称进行编码查询,这可以节省字节码从而节省内存。不过,ROWS
的值会至少占用两个机器字,在全局字典中键和值各占一个。这在字典中是必须得因为其他模块可能会引入并使用它。在名称前面加下划线可以节省内存,如:_COLS,该标识符在模块外不可见,因此不会占用内存。
const()
的参数可以任意值或表达式,在编译时会被计算为常量,如:0x100
、1<<8
或(True, "string", b"bytes")
,它甚至可以包含其他已定义的常量标识符,如:1 << BIT
。
常量数据结构
如果常量数据很多并且平台支持从闪存执行,可以考虑下面的方式节省内存:把数据放在Python模块可以找到的地方并且已经冻结为字节码,数据必须被定义为bytes
对象,编译器“知道”字节对象是不变的,并确保对象保留在闪存,而不是复制到内存,struct
模块可以辅助完成bytes
类型与Python其他内置类型的转换。
当考虑到冻结字节码的影响,注意在Python的字符串、浮点数、字节、整数、复数和元组不变。因此,他们将被冻结到闪存(对于元组,需要所有的元素不可变),在下面这行:
mystring = "The quick brown fox"
实际的字符串"The quick brown fox"存储在闪存,在运行时字符串的引用会关联给变量mystring
,应用会占用一个机器字。原则上长整数可用于存储常量数据:
bar = 0xDEADBEEF0000DEADBEEF
与字符串示例类似,在运行时,一个任意大的整数的引用被分配给变量bar,该引用占用一个机器字。
常量元组对象本身就是常量。编译器会对这些常量元组进行优化,所以使用时无需创建。例如:
foo = (1, 2, 3, 4, 5