1.最大化 MicroPython 运行速度
优化思路:按 “从简单到复杂、从软件到硬件” 的顺序优化,先定位性能瓶颈,再逐步调整,以低成本获最大性能提升。
核心工具 / 方法包括:
- 计时工具:用
time模块(如time.ticks_us())做微秒级计时,便于定位瓶颈; - 装饰器:通过
@符号给函数加额外功能(如计时),提升代码复用性; - 代码发射器:
native发射器将 Python 编译为原生机器码(速度快);viper发射器更激进,支持操作硬件寄存器(速度接近 C); - 交叉编译器:用
mpy-cross在电脑将.py预编译为.mpy字节码,减少设备编译耗时。
优化方向:
- 代码执行效率:用
const声明常量、预编译字节码等; - 内存与对象:合理分配内存、使用数组替代列表等;
- 运算与硬件:用整数替代浮点运算、借助硬件外设(如 DMA、硬件 SPI)替代软件模拟操作(性能提升最显著)。
2.优化步骤
我们可以把 MicroPython 的代码优化过程比作“给自行车提速的步骤”:先找到自行车跑得慢的核心原因(比如轮胎没气、链条卡顿),再从简单的调整开始(充气、上油),最后再考虑更换高端零件(轻量化车架、碳纤维轮组)。MicroPython 的代码优化也遵循 “从简单到复杂、从软件到硬件”的顺序,这样能以最低的成本获得最大的性能提升,避免一开始就陷入复杂的底层优化而浪费时间。
在开始优化前,先搞懂几个核心基础概念:
- 计时工具(utime 模块):MicroPython 专门用于嵌入式系统的时间处理模块,
utime.ticks_us()能获取微秒级系统时间戳(1 微秒 = 1/1000 毫秒 = 1/1000000 秒),utime.ticks_diff()用于计算两个时间戳的差值。这比普通的time模块更适配嵌入式场景,因为嵌入式开发常需要毫秒 / 微秒级的高精度计时。 - 装饰器(@ 符号):Python 的语法糖,能在不修改函数本身代码的前提下,给函数添加额外功能(比如这里的计时功能)。就像给礼物包上包装纸,礼物本身不变,但多了装饰效果,装饰器让代码复用性更高。
- native/viper 代码发射器:MicroPython 的专属编译工具(扩展:这是 MicroPython 区别于普通 Python 的关键优化特性):
- native 发射器:把 Python 代码编译成微控制器的原生机器码,执行速度比 MicroPython 的字节码快数倍,且几乎兼容所有 Python 语法。
- viper 发射器:比 native 更激进的编译器,支持直接操作硬件寄存器,执行速度接近 C 语言,但语法有一定限制(比如只支持基本数据类型)。
- 硬件特定优化:利用微控制器的硬件外设(如 DMA 直接内存访问、硬件 PWM、硬件 SPI)代替软件模拟操作(扩展:这是效率提升最大的优化方式,比如软件模拟 SPI 传输每秒几千次,硬件 SPI 能达到每秒上百万次)。
- mpy-cross(MicroPython 交叉编译器):MicroPython 的官方交叉编译工具,能在电脑(PC)上将
.py脚本预编译为.mpy字节码文件,再将.mpy文件上传到微控制器运行。相比在设备上动态编译.py文件,预编译的.mpy字节码有三个优势:减少设备上的编译时间(尤其是首次运行脚本时,避免设备浪费算力在编译上);.mpy文件体积更小,节省微控制器的闪存空间;字节码加载和执行速度略快于原始.py文件。
开发高性能代码的过程包括以下应按所列顺序执行的阶段:
- 确定代码中最慢的部分(性能分析):这是优化的第一步,也是最关键的一步 —— 如果盲目优化,可能会花大量时间优化本来就很快的代码,毫无意义。我们用
utime模块的计时功能测量每个函数的执行时间,找到耗时最长的 “性能瓶颈”。 - 提高 Python 代码的效率(基础优化):在不改变代码运行方式的前提下,优化 Python 代码的写法(比如用列表推导式代替 for 循环、用局部变量代替全局变量 ——MicroPython 中局部变量访问更快、减少不必要的函数调用等)。这一步最简单,且能解决大部分性能问题。
- 使用 native 代码发射器(中级优化):如果性能还不够,就用 MicroPython 的
@micropython.native装饰器将函数编译成原生机器码,执行速度会显著提升,且几乎不需要修改代码。 - 使用 viper 代码发射器(高级优化):如果 native 编译后还是不够快,就用
@micropython.viper装饰器,它能让代码执行速度接近 C 语言,但需要注意语法限制(比如不能使用 Python 的复杂数据结构)。 - 使用 mpy-cross 编译为字节码(预编译优化):如果前面优化后,脚本首次运行的加载时间仍较长(尤其是大脚本),就用 mpy-cross 在电脑上把
.py文件预编译为.mpy字节码文件,再上传到设备。这一步无需修改代码,仅改变文件的加载方式,能显著减少设备的编译开销。 - 使用特定于硬件的优化(终极优化):这是最后一步,利用微控制器的硬件外设代替软件模拟,比如用硬件 DMA 传输数据、用硬件定时器代替软件延时,这是提升性能的终极方案。
3.优化方法
3.1 识别最慢的代码
在识别最慢的代码部分,我们通常可以通过明智地使用的定时来建立 ticks 的中记录的功能组 utime。代码执行时间可以 ms(毫秒)、us(微秒)或 CPU 周期来衡量。
以下允许通过添加 @timed_function 装饰器对任何函数或方法进行计时:
import time
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
这里,我们使用 mpremote 工具连接树莓派 Pico,将上面的代码复制到 REPL 中,按回车执行(此时装饰器已定义完成):
接着,进行定义一个测试函数(模拟耗时操作):
# 用@timed_function装饰器修饰测试函数
@timed_function
def slow_function():
for i in range(10000):
pass
return "Done"
我们将其粘贴到 REPL 中:
接着调用函数,查看计时结果:
slow_function()
我们看到,输出了函数执行的耗时结果:
3.2 性能优化的具体措施
3.2.1 MicroPython 的性能瓶颈与核心基础概念
3.2.1.1 核心基础概念
3.2.1.1.1 变量与常量
这里,首先我们需要明白变量与常量之间的区别,这是编程的基础,二者之间核心区别在于何时确定值:
- 变量:运行时动态赋值,值可以改变。MicroPython 在访问变量时,需要从全局 / 局部字典中查找,会产生少量开销(尤其是在循环中频繁访问时,开销会被放大)。
# 变量,运行时存储在字典中
max_count = 10000
for i in range(max_count):
# 每次循环都要查字典找max_count的值
pass
- 常量:编译时确定值,值不可改变。MicroPython 提供
const()声明(类似 C 语言的#define),编译字节码时会直接将标识符替换为数值,完全消除运行时的字典查找开销。
from micropython import const
# 常量,编译时直接替换为10000
MAX_COUNT = const(10000)
for i in range(MAX_COUNT):
# 循环中直接使用10000,无需查字典
pass
3.2.1.1.2 内存三分区
嵌入式微控制器(如 RP2040)的内存资源极少(比如只有 264KB RAM),而 MicroPython 的对象存储直接依赖内存分区,堆的分配和回收是最大的性能瓶颈。我们用 “仓库管理” 的比喻来理解三个分区:
对于嵌入式开发中的内存管理来说,往往面临下面几个难点:
- 堆的分配需要遍历空闲内存块,耗时;
- 堆中不再使用的对象需要垃圾收集(GC)来清理,这个过程会阻塞程序,耗时几毫秒(实时场景中致命);
- 栈空间小,若局部变量过多 / 函数嵌套过深,会触发栈溢出(直接崩溃)。
3.2.1.1.3 引用与拷贝
所谓引用,相当于对象的 “门牌号”(比喻:电脑里的快捷方式),变量存储的不是数据本身,而是数据在内存中的地址。操作引用不会复制数据,开销极小。
# 在堆上创建字节数组对象,a存储的是对象的地址(门牌号)
a = bytearray(10)
# b是a的引用,指向同一个对象,无新分配
b = a
# 操作b会改变a的内容,因为是同一个对象
b[0] = 1
# 输出:1
print(a[0])
在终端中,运行结果如下:
那么对于拷贝(这里指的是深拷贝,关于浅拷贝和深拷贝区别,这里不做过多解释)来说,它会复制整个数据,在堆上创建新对象,存储新的数据集,开销极大(尤其是大数据):
# 大字节数组(堆上10KB)
a = bytearray(10000)
# 切片操作,创建新的字节数组(堆上~2KB),属于深拷贝
b = a[30:2000]
为了避免拷贝大字节数组时产生过大的堆分配问题,我们可以使用 memoryview 内存视图,它是 MicroPython 提供的浅引用工具,本质是对缓冲区对象(字节数组、数组、bytes 等)的 “只读 / 可写门牌号”,切片时不会复制数据,仅传递地址,完全避免堆分配。
# 大字节数组
a = bytearray(10000)
# 创建内存视图(仅分配小对象,几十字节)
mv = memoryview(a)
# 切片内存视图,无新分配,仅传递地址
b = mv[30:2000]
# 操作b会改变a的内容,因为是同一个数据
b[0] = 1
print(a[30])
运行结果如下:
这里,需注意的是 memoryview 仅支持缓冲区协议对象(字节数组、array、bytes),不支持列表(列表存储的是对象引用,不是连续数据)。
在掌握以上基础后,再理解这些嵌入式 MicroPython 的进阶概念:
- 垃圾收集(
GC) :MicroPython 自动清理堆中不再被引用的对象,这个过程会阻塞程序。我们可以手动调用gc.collect()控制清理时机,避免在性能关键段触发。 - native/viper 代码发射器:将 Python 代码编译为ARM-Thumb 机器码(而非字节码),执行速度远快于解释执行(native 速度是字节码的 2 倍,viper 更激进,支持指针操作,接近 C 语言速度)。
- 寄存器直接操作:绕过 MicroPython 的硬件抽象层(如
machine.Pin),直接读写芯片的寄存器地址,消除方法调用的额外开销(适合高频硬件操作)。 - mpy-cross:MicroPython 交叉编译器,在电脑上将
.py脚本预编译为.mpy字节码,减少设备上的编译开销,加快脚本加载速度。
3.2.1.2 MicroPython 的性能瓶颈
MicroPython 的性能瓶颈主要来自三个核心方面:堆内存分配与垃圾收集(GC)的开销、Python 字节码的解释执行开销、低效的运算 / 硬件操作方式。
因此,我们可以从下面几个方面进行优化:
- 内存与对象优化(优先级最高,成本最低):解决堆分配和 GC 的开销问题,这是 MicroPython 嵌入式场景中最常见的性能瓶颈。
- 代码执行效率优化(优先级中等):提升代码本身的运行速度,从字节码层面到机器码层面优化。
- 运算与硬件优化(优先级最低,适合性能关键场景):利用硬件特性减少低效运算 / 操作,是终极优化手段。
3.2.2 内存与对象优化
这类优化的核心是“尽量避免在运行时动态创建对象、减少堆分配,从而降低 GC 的触发频率和耗时”,是嵌入式 MicroPython 性能优化的首要步骤。
3.2.2.1 预分配内存与固定对象大小
对象只创建一次(如在类的构造函数中实例化),不允许其大小动态增长(如列表 append、字典新增键值对)。尤其是缓冲区(如串口通信的缓冲区),要预分配并复用。
我们可以使用 readinto() 代替 read()(read() 会每次分配新缓冲区,readinto() 将数据读入已有的缓冲区)。
以串口缓冲区为例:
from machine import UART, Pin
# 1. 预分配缓冲区(只创建一次,避免动态分配)
buf = bytearray(64) # 预分配64字节的缓冲区
# 初始化UART
uart = UART(0, baudrate=9600, tx=Pin(0), rx=Pin(1))
# 2. 使用readinto()读入预分配的缓冲区(无新分配)
while True:
if uart.any():
# 数据读入buf,返回读取的字节数
n = uart.readinto(buf)
# 对比:uart.read(64) 会每次创建新的字节对象,触发堆分配
print("recv data:", buf[:n])
3.2.2.2 使用数组替代列表 + memoryview 避免数据拷贝
列表存储的是对象引用,内存不连续,且动态增长会触发堆分配;array 模块(或 bytearray)存储连续的基本类型数据,预分配后性能更高;同时切片操作(如 ba[30:2000])会创建数据副本,触发堆分配;使用 memoryview 可直接传递内存指针,无拷贝开销。
import array
import time
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
# 1. 用array替代列表(存储整数,连续内存)
# 预分配1000个int型元素的数组
arr = array.array('i', [0]*1000)
# 2. 用memoryview避免切片拷贝
# 大字节数组
ba = bytearray(10000)
# 直接切片:会创建副本,触发~2K的堆分配
@timed_function
def func(data):
pass
# 测试切片拷贝(耗时且占内存)
func(ba[30:2000])
# 使用memoryview:只分配小对象,无数据拷贝
mv = memoryview(ba)
# 传递的是内存指针,无分配
func(mv[30:2000])
终端输出如下:
3.2.2.3 缓存对象引用
将频繁访问的对象(如 self.ba、obj_display.framebuffer)缓存到局部变量中,避免每次访问都进行属性查找(属性查找会涉及字典操作,耗时且可能触发分配)。
import time
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
class Foo:
def __init__(self):
self.ba = bytearray(100)
@timed_function
def bar(self):
ba_ref = self.ba
for i in range(100):
ba_ref[i] = i % 256
class Foo_compare:
def __init__(self):
self.ba = bytearray(100)
@timed_function
def bar(self):
for i in range(100):
self.ba[i] = i % 256
# 测试
f = Foo()
f.bar()
f_c = Foo()
f_c.bar()
点击运行,终端输出如下:
3.2.2.4 手动控制垃圾收集
定期调用 gc.collect() 手动触发 GC,避免 GC 在性能关键的代码段中随机触发(手动 GC 可控制时机,且频繁小 GC 的耗时远小于单次大 GC)。
import gc
import time
# 启用GC(默认启用,可手动关闭/开启)
gc.enable()
# 性能关键循环前,手动触发GC
# 提前清理内存,耗时约1ms
gc.collect()
# 性能关键代码段
start = time.ticks_us()
for i in range(10000):
pass
end = time.ticks_us()
print(f"耗时:{utime.ticks_diff(end, start)/1000}ms")
# 定期在非关键段触发GC
# gc.collect()
3.2.3 代码执行效率优化
这类优化是在内存优化的基础上,进一步提升代码的执行速度,从字节码层面到机器码层面优化。
3.2.3.1 使用 const () 声明常量
const() 将标识符替换为数值(编译时完成),避免运行时的字典查找,尤其是在循环中使用的常量,优化效果显著。
from micropython import const
import time
# 声明常量(编译时替换为数值)
MAX_COUNT = const(100000)
# 二进制常量也支持
PIN_BIT = const(1 << 2)
MAX_COUNT_NOT_USE_CONST = 100000
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
@timed_function
def use_const():
total = 0
for i in range(MAX_COUNT):
total += i
return total
# 对比:不用const(),每次访问都会查字典,耗时更长
@timed_function
def no_const():
global MAX_COUNT_NOT_USE_CONST
total = 0
for i in range(MAX_COUNT_NOT_USE_CONST):
total += i
return total
result1 = use_const()
result2 = no_const()
终端中运行结果如下:
我们可以看到,二者其实运行时间相差无几,这是因为在 REPL 中,代码是解释执行的,而
const 的真正优势在预编译的字节码中才明显。
3.2.3.2 mpy-cross 编译字节码
在电脑上用 mpy-cross 将 .py 脚本预编译为 .mpy 字节码,再上传到设备。
我们可以使用 pip 命令安装 mpy-cross 工具:
然后,用下面命令将
MicroPython 的 py 文件编译为 mpy 文件:
我们可以看到,编译后的
mpy 文件更小,接下来使用 mpremote 工具,将其放到设备端就可以了。
3.2.3.3 使用代码发射器
当 MicroPython 编译代码时,它会单独处理每个函数(类是函数,lambda 和列表推导式也是函数)。 函数从解析阶段出来,然后进入编译器,编译器将 Python 函数传递 3 次:
- 在第一阶段,编译器收集有关变量及其作用域(本地或全局)的信息,并通过查找特殊类型的函数装饰器来确定发射器类型;
- 在第二阶段,它计算堆栈使用情况和代码偏移量;
- 第三阶段根据所需的发射器发出代码。
目前,代码发射器有四种:
- 字节码发射器:函数默认使用字节码发射器,生成的字节代码在 MicroPython 的内置虚拟机上运行。虚拟机非常简单:它解码每个字节代码(及其参数,如果有的话)并调用适当的 C 运行时函数;
- native 代码发射器:native 代码发射器获取每个字节代码并将其转换为等效的 ARM-Thumb 机器代码。此类函数使用普通的 C 堆栈来存储局部变量并直接调用 C 运行时函数;但注意在上下文管理器(with 语句)和生成器中无法使用,并且如果 raise 使用,则必须提供参数;
- Viper 代码发射器:编译为优化后的机器码,支持指针操作,速度接近 C 语言(兼容性限制更多)。
- 内联汇编器:暂不讨论,可看链接 www.86x.org/en/latet/re…
3.2.3.3.1 native 代码发射器
native 代码发射器获取每个字节代码并将其转换为等效的 ARM-Thumb 机器代码。此类函数使用普通的 C 堆栈来存储局部变量并直接调用 C 运行时函数。
native 代码发射器通过函数装饰器调用:
@micropython.native
def foo(self, arg):
buf = self.linebuf # Cached object
# code
native 代码发射器的当前实现存在某些限制:
- 不支持上下文管理器(with 语句)
- 不支持生成器
- 如果 raise 使用,则必须提供参数
提高性能(大约是字节码的两倍)的代价是编译代码大小的增加。
3.2.3.3.2 Viper 代码发射器
上面讨论的优化涉及符合标准的 Python 代码。Viper 代码发射器不完全兼容。它支持特殊的 Viper 本地数据类型以追求性能。整数处理是不合规的,因为它使用机器字:32 位硬件上的算术以 2**32 为模执行。
Viper 代码发射器会为每个字节代码发出 ARM-Thumb 机器代码,并进一步优化某些内容,例如整数运算。对于两个整数的相加,viper 发射器不调用 C 运行时函数 rt_binary_op,而是发出机器指令“adds”来直接将两个数字相加。这比调用 rt_binary_op 快得多。它是使用装饰器调用的:
@micropython.viper
def foo(self, arg: int) -> int:
# code
Viper 支持它自己的一组类型,即 int, uint(无符号整数)ptr, ptr8, ptr16 和 ptr32:
- ptr 指向对象的指针
- ptr8 指向一个字节
- ptr16 指向一个 16 位半字
- ptr32 指向一个 32 位机器字
这里,我们测试一下大计算量整数累加:
import time
import micropython
def timed_function(name):
def decorator(f):
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(name, delta/1000))
return result
return new_func
return decorator
# 普通Python函数:100万次累加(手动指定函数名'normal_add_loop')
@timed_function('normal_add_loop')
def normal_add_loop(n: int) -> int:
total = 0
for i in range(n):
total += i * 2 + 5
return total
# Viper优化函数:相同计算量(手动指定函数名'viper_add_loop')
@timed_function('viper_add_loop')
@micropython.viper
def viper_add_loop(n: int) -> int:
total = 0
for i in range(n):
total += i * 2 + 5
return total
# 调用测试(100万次运算,正常计时输出)
normal_add_loop(1000000)
viper_add_loop(1000000)
两个函数执行完全相同的 100 万次整数运算(i*2+5 累加),但 normal_add_loop 是纯解释执行,viper_add_loop 是机器码直接执行,终端运行结果如下:
我们在树莓派 Pico 上进行测试,
normal_add_loop 的耗时是 viper_add_loop 的几十倍。
Viper 有两个关键限制:
- 不允许默认参数:函数参数不能设置
a: int = 10这种默认值,否则会报错。 - 浮点运算可使用但无优化:浮点计算的耗时和普通 Python 函数几乎一致,因为 Viper 不会为浮点运算生成优化的机器码。
import micropython
# 错误示例(REPL运行会报错:Viper does not support default arguments)
# @micropython.viper
# def viper_default(a: int = 10) -> int:
# total = 0
# for i in range(a):
# total += i
# return total
# 正确示例(无默认参数,100万次运算,计时)
@micropython.viper
@timed_function
def viper_no_default(a: int) -> int:
total = 0
for i in range(a):
total += i
return total
# 调用测试
viper_no_default(1000000)
运行结果如下:
我们再测试一下 Viper 编译后的函数和普通函数的浮点运算:
import micropython
def timed_function(name):
def decorator(f):
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(name, delta/1000))
return result
return new_func
return decorator
# 普通函数:10万次浮点乘法累加
@timed_function('normal_float_calc')
def normal_float_calc(n: int) -> float:
total = 0.0
for i in range(n):
total += float(i) * 3.14159
return total
# Viper函数:相同的10万次浮点运算(无优化)
@timed_function('viper_float_calc')
@micropython.viper
def viper_float_calc(n: int):
total = 0.0
for i in range(n):
total += float(i) * 3.14159
return total
# 调用测试(计时结果几乎一致)
normal_float_calc(100000)
viper_float_calc(100000)
运行结果如下:
Viper 的指针类型(
ptr8/ptr16/ptr32)用于直接访问连续内存(如 bytearray),无边界检查,支持下标单个访问(不支持切片)。关键优化技巧是:将对象转为指针的操作放在函数开头(而非循环内),因为转换操作耗时几微秒,大循环中会被放大。
指针的优势在大数组遍历场景下尤为明显,远快于普通 Python 的数组访问。
import micropython
def timed_function(name):
def decorator(f):
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(name, delta/1000))
return result
return new_func
return decorator
# 准备1万长度的bytearray(大数组)
ba = bytearray(10000)
for i in range(10000):
ba[i] = i % 256
# 普通函数:遍历10万bytearray,累加值
@timed_function('normal_bytearray_access')
def normal_bytearray_access(ba: bytearray) -> int:
total = 0
for i in range(10000):
total += ba[i]
return total
# Viper函数:ptr8指针访问,累加值(转换放在开头)
@timed_function('viper_ptr8_access')
@micropython.viper
def viper_ptr8_access(ba) -> int:
buf = ptr8(ba)
total = 0
for i in range(10000):
total += buf[i]
return total
# 调用测试(指针访问速度远超普通访问)
normal_bytearray_access(ba)
viper_ptr8_access(ba)
终端输出结果如下:
viper_ptr8_access 的耗时仅为 normal_bytearray_access 的三十分之一:普通函数的 ba[i] 需要经过 Python 的边界检查、对象属性查找等步骤;Viper 的 buf[i] 是直接计算内存地址并访问字节,无额外开销。
接下来,我们对比一下将对象转为指针操作在循环内进行和开头进行的区别:
import micropython
def timed_function(name):
def decorator(f):
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(name, delta/1000))
return result
return new_func
return decorator
# 准备1万长度的bytearray(大数组)
ba = bytearray(10000)
for i in range(10000):
ba[i] = i % 256
# Viper函数:循环内重复转换ptr8(低效)
@timed_function('viper_bad_convert')
@micropython.viper
def viper_bad_convert(ba) -> int:
total = 0
for i in range(10000):
buf = ptr8(ba)
total += buf[i]
return total
# Viper函数:开头一次性转换ptr8(高效)
@timed_function('viper_good_convert')
@micropython.viper
def viper_good_convert(ba) -> int:
buf = ptr8(ba)
total = 0
for i in range(10000):
total += buf[i]
return total
# 调用测试(低效版耗时远高于高效版)
viper_bad_convert(ba)
viper_good_convert(ba)
运行结果如下:
viper_bad_convert 在 100 万次循环中,每次都执行 ptr8(ba) 的类型转换(每次耗时几微秒,累计耗时显著);viper_good_convert 仅在开头执行一次转换,避免了重复开销。
Viper 的整数是机器字级别,32 位硬件上算术运算 2**32 为模执行(溢出后会截断),这是为性能牺牲兼容性的体现,大计算量下这种特性会更明显。
3.2.4 运算与硬件优化
3.2.4.1 用整数运算替代浮点数运算
无 FPU(浮点协处理器)的芯片执行浮点运算极慢,性能关键部分用整数运算,非关键部分再转换为浮点数。
from machine import ADC, Pin
import array
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
# 1. 纯整数运算:预分配数组+ADC读数(无浮点)
# 预分配100个int型元素的数组(连续内存,无动态分配)
# 纯整数运算:读取16位整数ADC值
@timed_function
def adc_read_integer():
adc_data = array.array('i', [0]*100)
adc = ADC(Pin(26))
for i in range(100):
adc_data[i] = adc.read_u16()
return adc_data
# 2. 包含浮点运算:整数读数+浮点转换(电压计算)
# 第一步:整数读数(和上面一致)
# 第二步:浮点运算转换为电压(读数/65535*3.3)
@timed_function
def adc_read_float():
adc_data = array.array('i', [0]*100)
adc = ADC(Pin(26))
for i in range(100):
adc_data[i] = adc.read_u16()
voltage_data = [x/65535*3.3 for x in adc_data]
return voltage_data
# 执行测试,对比耗时
print("=== ADC读数性能对比 ===")
integer_data = adc_read_integer()
float_data = adc_read_float()
# 打印前5个电压值(验证功能)
print("\n前5个电压值:", float_data[:5])
终端运行:
我们可以看到,浮点运算的耗时是大于纯整数运算的。
3.2.4.2 直接读写寄存器
绕过 MicroPython 的硬件抽象层,直接读写芯片寄存器,消除方法调用的开销(如 LED 快速闪烁、高频 GPIO 操作)。
from machine import Pin, mem32
import time
from micropython import const
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
# --------------------------
# 配置SIO寄存器(树莓派Pico专属)
# --------------------------
# SIO模块基地址(RP2040固定)
# SIO GPIO核心寄存器(正确地址,修正之前的错误)
SIO_BASE = const(0xD0000000)
# 一次性写入所有GPIO输出值
GPIO_OUT = SIO_BASE + 0x010
# 原子置位GPIO
GPIO_OUT_SET = SIO_BASE + 0x014
# 原子清零GPIO
GPIO_OUT_CLR = SIO_BASE + 0x018
# 原子翻转GPIO
GPIO_OUT_XOR = SIO_BASE + 0x01C
# 原子设置GPIO为输出模式# GPIO25(板载LED)的位掩码(bit25对应GPIO25)
GPIO_OE_SET = SIO_BASE + 0x024
# 初始化:将GPIO25设为输出模式(仅执行一次,原子操作)
PIN25_MASK = const(1 << 25)
mem32[GPIO_OE_SET] = PIN25_MASK
# 初始化Pin对象(仅执行一次)
led_pin = Pin(25, Pin.OUT)
# 方式1:普通machine.Pin操作GPIO25(硬件抽象层,有开销)
@timed_function
def led_pin_loop(loop_count):
for _ in range(loop_count):
led_pin.value(1)
led_pin.value(0)
# 方式2:SIO寄存器操作GPIO25(无抽象层,极致高效)
@timed_function
def led_sio_set_clr_loop(loop_count):
set_reg = GPIO_OUT_SET
clr_reg = GPIO_OUT_CLR
mask = PIN25_MASK
for _ in range(loop_count):
mem32[set_reg] = mask
mem32[clr_reg] = mask
# 测试运行速度
loop_count = 1000
led_pin_loop(loop_count)
led_sio_set_clr_loop(loop_count)
运行结果如下:
可以看到,在 SIO 循环函数中,将
GPIO_OUT_SET、GPIO_OUT_CLR、PIN25_MASK 缓存到局部变量,减少全局变量查找的开销,让 SIO 的性能优势更突出。
3.2.4.3 DMA 相关操作
在运算与硬件数据交互的场景中(如批量 ADC 数据采集、高频 GPIO 信号输出、传感器数据流读取),CPU 往往需要花费大量时间执行数据传输操作(如从外设寄存器读取数据到内存、将运算结果写入 GPIO 寄存器),挤占了运算所需的资源。RP2040 的 DMA(Direct Memory Access,直接内存访问)控制器可脱离 CPU 干预,自主完成内存与外设 / 寄存器之间的批量数据传输,其核心优化价值在于:将 CPU 从繁琐的数据传输任务中解放出来,使其专注于核心运算逻辑。
从开发与性能层面来看,MicroPython 对 RP2040 DMA 的支持较为基础,仅能实现简单的批量数据传输;而 C 语言(Pico SDK)可充分配置 DMA 的传输模式、触发条件与数据处理规则,结合直接寄存器操作,能实现运算与硬件数据交互的无缝优化,是高吞吐量、低延迟场景的最优选择。
4.优化实验
4.1 LCD 屏幕的优化
关于这个可以查看:
4.2 DMA 相关优化
关于这个可以查看: