从慢到快:MicroPython 性能优化的完整步骤与实战案例

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 文件。

开发高性能代码的过程包括以下应按所列顺序执行的阶段:

  1. 确定代码中最慢的部分(性能分析) :这是优化的第一步,也是最关键的一步 ------ 如果盲目优化,可能会花大量时间优化本来就很快的代码,毫无意义。我们用 utime 模块的计时功能测量每个函数的执行时间,找到耗时最长的 "性能瓶颈"。
  2. 提高 Python 代码的效率(基础优化):在不改变代码运行方式的前提下,优化 Python 代码的写法(比如用列表推导式代替 for 循环、用局部变量代替全局变量 ------MicroPython 中局部变量访问更快、减少不必要的函数调用等)。这一步最简单,且能解决大部分性能问题。
  3. 使用 native 代码发射器(中级优化) :如果性能还不够,就用 MicroPython 的 @micropython.native 装饰器将函数编译成原生机器码,执行速度会显著提升,且几乎不需要修改代码。
  4. 使用 viper 代码发射器(高级优化) :如果 native 编译后还是不够快,就用 @micropython.viper 装饰器,它能让代码执行速度接近 C 语言,但需要注意语法限制(比如不能使用 Python 的复杂数据结构)。
  5. 使用 mpy-cross 编译为字节码(预编译优化) :如果前面优化后,脚本首次运行的加载时间仍较长(尤其是大脚本),就用 mpy-cross 在电脑上把 .py 文件预编译为 .mpy 字节码文件,再上传到设备。这一步无需修改代码,仅改变文件的加载方式,能显著减少设备的编译开销。
  6. 使用特定于硬件的优化(终极优化):这是最后一步,利用微控制器的硬件外设代替软件模拟,比如用硬件 DMA 传输数据、用硬件定时器代替软件延时,这是提升性能的终极方案。

3.优化方法

3.1 识别最慢的代码

在识别最慢的代码部分,我们通常可以通过明智地使用的定时来建立 ticks 的中记录的功能组 utime。代码执行时间可以 ms(毫秒)、us(微秒)或 CPU 周期来衡量。

以下允许通过添加 @timed_function 装饰器对任何函数或方法进行计时:

Python 复制代码
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 中,按回车执行(此时装饰器已定义完成):

接着,进行定义一个测试函数(模拟耗时操作):

Python 复制代码
# 用@timed_function装饰器修饰测试函数
@timed_function
def slow_function():
    for i in range(10000):
        pass
    return "Done"

我们将其粘贴到 REPL 中:

接着调用函数,查看计时结果:

Plain 复制代码
slow_function()

我们看到,输出了函数执行的耗时结果:

3.2 性能优化的具体措施

3.2.1 MicroPython 的性能瓶颈与核心基础概念

3.2.1.1 核心基础概念
3.2.1.1.1 变量与常量

这里,首先我们需要明白变量与常量之间的区别,这是编程的基础,二者之间核心区别在于何时确定值:

  • 变量:运行时动态赋值,值可以改变。MicroPython 在访问变量时,需要从全局 / 局部字典中查找,会产生少量开销(尤其是在循环中频繁访问时,开销会被放大)。
Plain 复制代码
# 变量,运行时存储在字典中
max_count = 10000  
for i in range(max_count):
    # 每次循环都要查字典找max_count的值
    pass
  • 常量 :编译时确定值,值不可改变。MicroPython 提供 const() 声明(类似 C 语言的 #define),编译字节码时会直接将标识符替换为数值,完全消除运行时的字典查找开销
Plain 复制代码
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 引用与拷贝

所谓引用,相当于对象的 ​"门牌号"(比喻:电脑里的快捷方式),变量存储的不是数据本身,而是数据在内存中的地址。操作引用不会复制数据,开销极小。

Plain 复制代码
# 在堆上创建字节数组对象,a存储的是对象的地址(门牌号)
a = bytearray(10)  
# b是a的引用,指向同一个对象,无新分配
b = a  
# 操作b会改变a的内容,因为是同一个对象
b[0] = 1  
# 输出:1
print(a[0])

在终端中,运行结果如下:

那么对于拷贝(这里指的是深拷贝,关于浅拷贝和深拷贝区别,这里不做过多解释)来说,它会复制整个数据,在堆上创建新对象,存储新的数据集,开销极大(尤其是大数据):

Plain 复制代码
# 大字节数组(堆上10KB)
a = bytearray(10000)  
# 切片操作,创建新的字节数组(堆上~2KB),属于深拷贝
b = a[30:2000]

为了避免拷贝大字节数组时产生过大的堆分配问题,我们可以使用 memoryview 内存视图,它是 MicroPython 提供的浅引用工具,本质是对缓冲区对象(字节数组、数组、bytes 等)的 "只读 / 可写门牌号",切片时不会复制数据,仅传递地址,完全避免堆分配。

Plain 复制代码
# 大字节数组
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() 将数据读入已有的缓冲区)。

以串口缓冲区为例:

Python 复制代码
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 可直接传递内存指针,无拷贝开销。

Python 复制代码
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.baobj_display.framebuffer)缓存到局部变量中,避免每次访问都进行属性查找(属性查找会涉及字典操作,耗时且可能触发分配)。

Python 复制代码
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)。

Python 复制代码
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() 将标识符替换为数值(编译时完成),避免运行时的字典查找,尤其是在循环中使用的常量,优化效果显著。

Python 复制代码
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 工具:

然后,用下面命令将 MicroPythonpy 文件编译为 mpy 文件:

我们可以看到,编译后的 mpy 文件更小,接下来使用 mpremote 工具,将其放到设备端就可以了。

3.2.3.3 使用代码发射器

当 MicroPython 编译代码时,它会单独处理每个函数(类是函数,lambda 和列表推导式也是函数)。 函数从解析阶段出来,然后进入编译器,编译器将 Python 函数传递 3 次:

  1. 在第一阶段,编译器收集有关变量及其作用域(本地或全局)的信息,并通过查找特殊类型的函数装饰器来确定发射器类型;
  2. 在第二阶段,它计算堆栈使用情况和代码偏移量;
  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 代码发射器通过函数装饰器调用:

Plain 复制代码
@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 快得多。它是使用装饰器调用的:

Plain 复制代码
@micropython.viper
def foo(self, arg: int) -> int:
    # code

Viper 支持它自己的一组类型,即 int, uint(无符号整数)ptr, ptr8, ptr16 和 ptr32:

  • ptr 指向对象的指针
  • ptr8 指向一个字节
  • ptr16 指向一个 16 位半字
  • ptr32 指向一个 32 位机器字

这里,我们测试一下大计算量整数累加:

Python 复制代码
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 不会为浮点运算生成优化的机器码。
Python 复制代码
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 编译后的函数和普通函数的浮点运算:

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

# 普通函数: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 的数组访问。

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] 是直接计算内存地址并访问字节,无额外开销。

接下来,我们对比一下将对象转为指针操作在循环内进行和开头进行的区别:

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

# 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(浮点协处理器)的芯片执行浮点运算极慢,性能关键部分用整数运算,非关键部分再转换为浮点数。

Python 复制代码
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 操作)。

Python 复制代码
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_SETGPIO_OUT_CLRPIN25_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 屏幕的优化

关于这个可以查看:

08 SPI 串行外设接口-文档教程初稿

4.2 DMA 相关优化

关于这个可以查看:

19 DMA 直接内存访问-文档教程初稿

5.参考资料

docs.micropython.org/en/latest/r...

相关推荐
大辉狼_音频架构1 天前
AudioReach Plugin 机制问题解答
嵌入式·音视频开发
大辉狼_音频架构1 天前
AudioReach:Tinyalsa PCM Plugin 机制
嵌入式·音视频开发
feasibility.1 天前
从数字智能到物理智能体:深度解构具身智能时代的边缘计算、嵌入式实时系统与多形态智能体
人工智能·机器人·自动驾驶·嵌入式·无人机·具身智能·智能体
ajassi20002 天前
AI语音智能体开发日记(一)如何为“小智”服务器启用并调试 License 功能
笔记·ai·嵌入式·ai编程
ARM+FPGA+AI工业主板定制专家3 天前
国产化RK3576+FPGA架构|晶圆传输机器人高速定位+AI瑕疵检测一体化方案
fpga开发·架构·机器人·嵌入式·fpga·工控·机器人运控
Discipline~Hai3 天前
ARM03-蜂鸣器和中断
c语言·arm开发·单片机·嵌入式硬件·嵌入式
一杯原谅绿茶3 天前
安卓烧录工具PhonixCard-4.2.8下载
嵌入式
青衫嵌入式4 天前
FreeRTOS中断优先级配置不对也会HardFault?这次用一个血的教训讲明白
嵌入式
零涂毕业设计5 天前
毕业设计模块开发-OLED显示屏(IIC协议0.96寸)STM32 ESP32 FPGA Linux驱动免费分享
linux·stm32·单片机·嵌入式·esp32·fpga·oled