news 2026/5/16 18:54:09

CircuitPython实时数据可视化优化:利用Bitmap.fill()提升图表绘制性能

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CircuitPython实时数据可视化优化:利用Bitmap.fill()提升图表绘制性能

1. 项目概述与核心痛点

在嵌入式开发,尤其是基于MicroPython或CircuitPython的项目中,实时数据可视化一直是个既迷人又头疼的挑战。迷人之处在于,你能在一块小小的屏幕上亲眼看到传感器数据的脉动,比如温度曲线、心率波形或者电机转速;头疼之处在于,嵌入式设备的资源(CPU、内存)极其有限,一个低效的绘图操作就可能让整个界面卡成幻灯片,甚至拖垮主循环,影响核心控制逻辑。

我自己在做一个基于Adafruit PyPortal的智能温室监控项目时就深有体会。我需要同时绘制土壤湿度、环境温度和光照强度三条曲线,最初采用的方法是经典的“画新线,擦旧线”——也就是在下一个数据点到来时,先用背景色重新绘制上一段线段来“擦除”它,然后再绘制新的线段。在Mu编辑器的绘图面板上测试时,问题暴露无遗:图表刷新极其缓慢,有明显的闪烁和跳帧,当数据流稍快时,图表更新根本跟不上,导致可视化失去了“实时”的意义。问题的根源就在于这个“擦除再绘制”的过程太慢了。每一次局部的擦除和绘制,都涉及大量计算和屏幕缓冲区的频繁修改,在CircuitPython的解释执行环境下,这成了性能瓶颈。

后来,我发现了displayio模块中Bitmap对象的fill()方法。官方文档和社区零星提到它“更快”,但具体快多少、怎么用、能带来多大提升,缺少系统的实践指南。这促使我进行了一次深入的性能对比测试和优化实践。本文就将完整分享如何利用displayio.Bitmap.fill()方法,彻底重构绘图逻辑,从而在CircuitPython上实现流畅的实时数据可视化,并详细说明如何在Mu编辑器中设置和观察三通道图表的优化效果。无论你是在做物联网仪表盘、科学实验数据记录还是简单的系统监控,这套优化思路都能直接套用。

2. 核心原理:为什么传统“擦除”法这么慢?

在深入优化方案前,我们必须先理解瓶颈所在。这涉及到CircuitPython的displayio图形库的基本工作原理和屏幕渲染的底层逻辑。

2.1 displayio 显示架构简介

displayio是CircuitPython中用于管理复杂图形界面的高级模块。它的核心思想是“分层合成”。你可以想象它像Photoshop里的图层:

  • TileGrid: 好比一个画板,负责管理一个位图(Bitmap)在屏幕上的位置和缩放。
  • Bitmap: 就是画板上的像素数据,一个二维数组,存储每个像素的颜色索引值。
  • Palette: 调色板,将Bitmap中的颜色索引映射到实际的颜色值。
  • Group: 容器,可以嵌套多个TileGrid或其他Group,共同组成一个显示树。

最终,displayio引擎将这棵树合成一幅完整的图像,发送给屏幕驱动器。

2.2 低效绘图(慢速undrawing)的流程剖析

我最初使用的“画新线,擦旧线”方法,其伪代码如下:

# 假设已有 bitmap 和 screen_group 等显示对象 def draw_line_slow(x1, y1, x2, y2, color_index): # 绘制一条线段到Bitmap # ... 使用 Bresenham 等算法设置 bitmap[x, y] = color_index ... def undraw_line_slow(x1, y1, x2, y2): # “擦除”这条线段:用背景色再画一遍 draw_line_slow(x1, y1, x2, y2, BACKGROUND_COLOR_INDEX) # 主循环中 while True: new_value = read_sensor() x_new = get_new_x_position() # 1. 擦除旧的线段(从上一个点到上上个点) undraw_line_slow(x_old2, y_old2, x_old1, y_old1) # 2. 绘制新的线段(从上一个点到新点) draw_line_slow(x_old1, y_old1, x_new, new_value) # 3. 更新历史点 x_old2, y_old2 = x_old1, y_old1 x_old1, y_old1 = x_new, new_value

这种方法慢在哪儿?

  1. 双重计算: 每次更新,undraw_line_slowdraw_line_slow都要执行一次画线算法(如Bresenham),计算所有需要修改的像素坐标。这是两次完整的、计算密集型的遍历。
  2. 多次像素访问: 每个函数都会通过bitmap[x, y] = ...的方式逐个像素地修改Bitmap。在Python层面,每次这样的索引赋值都是一个相对较慢的操作,涉及边界检查、可能的内存管理等。
  3. 频繁局部更新: 显示系统可能因为Bitmap内容的频繁、零散更改而触发多次局部刷新,而不是一次高效的批量更新。

2.3 Bitmap.fill() 的高效之道

bitmap.fill(color_index)方法的作用是将整个Bitmap的所有像素一次性设置为同一种颜色。它的高效性源于:

  1. 底层原生操作: 这个方法很可能是通过C语言实现的底层内存操作(如memset)来完成的。它能够以接近机器码的速度,将一大块连续的内存区域(对应整个Bitmap)填充为同一个值。这比在Python解释器中用循环逐个像素赋值要快几个数量级。
  2. 单次操作,批量完成: 无论Bitmap有多大,fill()基本上是一个恒定时间的操作(O(1)复杂度),因为它只执行一个指令。而逐像素修改的时间复杂度是O(n),与像素数量成正比。

我们的优化思路正是源于此:与其费力地计算并擦除旧的、复杂的图形痕迹,不如直接清空整个绘图区域(或整个Bitmap),然后只绘制当前最新、最必要的图形。对于滚动图表来说,“清空”后我们只需要重画坐标轴、网格(如果是静态的,甚至可以预先画好)和最新的数据线段,工作量反而可能比精确擦除更少。

注意: 这里有一个关键的权衡。fill()清空的是整个Bitmap。如果你的图表区域只占屏幕的一部分,而其他部分有静态UI(如标题、按钮),那么你需要为图表创建一个独立的Bitmap和TileGrid。优化是针对这个独立的图表Bitmap进行的,避免影响到静态UI。

3. 优化方案设计与实现

理解了原理,我们来设计一个具体的、支持三通道数据绘图的优化方案。我们将创建一个RollingGraph类,它封装了高效绘图的所有逻辑。

3.1 硬件与软件环境准备

  • 硬件: 任何支持CircuitPython和displayio的开发板均可,例如Adafruit的PyPortal、CLUE、Feather系列搭配OLED屏等。本文示例基于Adafruit PyPortal Titano(大屏幕更适合绘图演示)。
  • 软件
    • CircuitPython 7.x 或更高版本(确保displayio稳定)。
    • Mu编辑器 1.2.x 或更高版本(其内置的绘图器功能是我们重要的调试工具)。
    • 必要的库:通常只需CircuitPython内置库。

3.2 RollingGraph 类设计

这个类的目标是管理一个可滚动的图表区域,支持最多三个数据通道(例如温度、湿度、压力)。我们将使用双缓冲技术来避免闪烁:一个“显示Bitmap”用于当前屏幕显示,一个“绘制Bitmap”用于准备下一帧图像。

import displayio import terminalio from adafruit_display_text import label from adafruit_display_shapes import line, rect class RollingGraph: def __init__(self, x=0, y=0, width=200, height=100, history_len=100, channels=3): """ 初始化一个滚动图表区域。 参数: x, y: 图表在父Group中的坐标。 width, height: 图表区域的像素尺寸。 history_len: 历史数据点缓存长度(决定图表横向“时间”跨度)。 channels: 数据通道数,最多3个,对应不同颜色。 """ self.width = width self.height = height self.history_len = history_len self.channels = min(channels, 3) # 限制最多3通道 # 颜色定义 (使用Palette索引) self.COLORS = [1, 2, 3] # 索引1,2,3分别对应通道0,1,2的颜色 self.BG_COLOR = 0 # 背景色索引 self.AXIS_COLOR = 4 # 坐标轴颜色索引 # 1. 创建两个Bitmap作为双缓冲 # display_bitmap 是当前显示在屏幕上的 # draw_bitmap 是我们正在绘制新帧的 self.display_bitmap = displayio.Bitmap(width, height, 5) # 5种颜色 self.draw_bitmap = displayio.Bitmap(width, height, 5) # 2. 为两个Bitmap创建共用的Palette self.palette = displayio.Palette(5) self.palette[0] = 0x000000 # 背景黑 self.palette[1] = 0xFF0000 # 通道1 - 红 self.palette[2] = 0x00FF00 # 通道2 - 绿 self.palette[3] = 0x0000FF # 通道3 - 蓝 self.palette[4] = 0xAAAAAA # 坐标轴 - 灰 # 3. 创建两个TileGrid,分别对应两个Bitmap self.display_tg = displayio.TileGrid(self.display_bitmap, pixel_shader=self.palette, x=x, y=y) self.draw_tg = displayio.TileGrid(self.draw_bitmap, pixel_shader=self.palette, x=x, y=y) # 4. 初始化数据历史缓冲区 self.data_history = [] for _ in range(self.channels): self.data_history.append([None] * history_len) self.current_index = 0 # 指向下一个要写入的历史数据位置 # 5. 绘制静态元素(坐标轴)到 draw_bitmap self._draw_static_elements() # 6. 初始时,让显示Bitmap和绘制Bitmap一致 self._swap_buffers() def _draw_static_elements(self): """在 draw_bitmap 上绘制静态的坐标轴和网格。""" # 清空绘制缓冲区为背景色 - 这就是性能关键! self.draw_bitmap.fill(self.BG_COLOR) # 绘制Y轴 (左侧) for y in range(0, self.height, 10): self.draw_bitmap[0, y] = self.AXIS_COLOR # 绘制X轴 (底部) for x in range(0, self.width, 10): self.draw_bitmap[x, self.height-1] = self.AXIS_COLOR # 可以在这里添加更复杂的网格或标签,但记住它们是静态的,只画一次。 def _swap_buffers(self): """交换显示缓冲区和绘制缓冲区。""" # 这个方法通过交换TileGrid的位图引用来实现“双缓冲” # 在CircuitPython中,更简单的方式是交换整个TileGrid对象在父Group中的位置。 # 但这里我们采用另一种思路:我们只更新数据,然后在每帧最后将draw_bitmap的内容复制到display_bitmap。 # 对于性能要求极高的场景,可以尝试直接交换Bitmap对象(如果API支持)。 for x in range(self.width): for y in range(self.height): self.display_bitmap[x, y] = self.draw_bitmap[x, y] # 注意:这个逐像素复制在Python层仍然较慢。但对于许多应用已足够。 # 真正的优化是让 _redraw_dynamic 直接在 display_bitmap 上操作, # 并依赖 fill() 快速清空。本例为演示双缓冲概念,保留此结构。 def _redraw_dynamic(self): """重绘动态部分(数据曲线)。""" # 清空绘制缓冲区的动态部分(我们保留静态的坐标轴吗?) # 不,更优的做法是:先清空整个绘制区域,再重画静态+动态。 # 但为了极致性能,我们可以只清空数据曲线区域(一个矩形)。 # 这里为了简单,我们采用“清空全部重画”策略,因为 fill() 很快。 # 实际上,我们可以定义一个“数据区域”矩形,只fill这个区域。 data_rect_top = 1 data_rect_height = self.height - 2 # 保留上下边框 # 在draw_bitmap上,将数据区域填充为背景色 # 注意:Bitmap没有直接的矩形填充,需要循环。对于窄矩形,这比全屏fill好。 for x in range(self.width): for y in range(data_rect_top, data_rect_top + data_rect_height): self.draw_bitmap[x, y] = self.BG_COLOR # 绘制每条通道的历史数据线 for ch in range(self.channels): color = self.COLORS[ch] history = self.data_history[ch] # 遍历历史数据点,绘制线段 for i in range(self.history_len - 1): idx1 = (self.current_index - i - 2) % self.history_len idx2 = (self.current_index - i - 1) % self.history_len val1 = history[idx1] val2 = history[idx2] if val1 is None or val2 is None: continue # 将数据值映射到Bitmap的Y坐标 y1 = self._value_to_y(val1, ch) y2 = self._value_to_y(val2, ch) x1 = self.width - i - 2 x2 = self.width - i - 1 # 确保坐标在Bitmap范围内 if 0 <= x1 < self.width and 0 <= x2 < self.width and 0 <= y1 < self.height and 0 <= y2 < self.height: self._draw_line(x1, y1, x2, y2, color) def _value_to_y(self, value, channel): """将数据值映射为Bitmap内的Y坐标。""" # 这里需要根据每个通道的数据范围进行映射。 # 简化:假设所有通道数据都已归一化到 [0, 1] 范围。 # 实际项目应维护每个通道的 min_val, max_val return int(self.height - 1 - value * (self.height - 1)) def _draw_line(self, x1, y1, x2, y2, color_index): """在 draw_bitmap 上绘制一条线段(Bresenham算法)。""" # Bresenham 直线算法实现 dx = abs(x2 - x1) dy = -abs(y2 - y1) sx = 1 if x1 < x2 else -1 sy = 1 if y1 < y2 else -1 err = dx + dy while True: if 0 <= x1 < self.width and 0 <= y1 < self.height: self.draw_bitmap[x1, y1] = color_index if x1 == x2 and y1 == y2: break e2 = 2 * err if e2 >= dy: err += dy x1 += sx if e2 <= dx: err += dx y1 += sy def update(self, new_values): """ 更新图表数据并刷新显示。 参数: new_values: 一个列表,包含各通道的新数据值。长度应与channels一致。 """ # 1. 将新数据存入历史缓冲区 for ch in range(self.channels): if ch < len(new_values): self.data_history[ch][self.current_index] = new_values[ch] self.current_index = (self.current_index + 1) % self.history_len # 2. 在绘制缓冲区上重绘动态内容 self._redraw_dynamic() # 3. 交换缓冲区,更新显示 self._swap_buffers() def get_display_group(self): """返回包含图表显示TileGrid的Group。""" group = displayio.Group() group.append(self.display_tg) return group

3.3 性能优化关键解析

  1. fill()的运用: 在_draw_static_elements()中,我们第一行就是self.draw_bitmap.fill(self.BG_COLOR)。这行代码以极快的速度将整个绘图区域重置为空白画布。这是替代无数个“擦除旧线段”操作的关键。
  2. 数据区域局部清空: 在_redraw_dynamic()中,我们并没有再次调用全屏的fill(),而是尝试只清空数据曲线所在的矩形区域。这是因为坐标轴等静态元素我们不想每次都重画。虽然这里用了循环,但清空的区域远小于全屏,且Bitmap的像素访问在C层有优化。如果图表区域不大,全屏fill()也是完全可以接受的,因为它真的很快。
  3. 双缓冲减少闪烁: 我们在draw_bitmap上完成所有绘制计算,然后通过_swap_buffers()将完整的一帧更新到display_bitmap。这避免了屏幕在绘制过程中显示不完整的中间状态,从而消除了闪烁。虽然我们的_swap_buffers实现是逐像素复制,但你可以将其优化为:在update()的最后,直接重新初始化display_bitmap并从draw_bitmap复制数据,或者探索是否有更底层的块复制方法。
  4. 历史数据与映射RollingGraph类维护了一个固定长度的历史数据队列。更新时,新数据覆盖最旧的数据,实现了图表的滚动效果。_value_to_y函数负责将物理数据值映射到屏幕像素坐标,这是图表正确显示的基础,你需要根据传感器的实际量程来完善它。

4. 与Mu编辑器集成与测试

Mu编辑器内置的“绘图器”是一个强大的实时数据可视化调试工具。它可以通过串口接收特定格式的数据,并自动绘制成曲线。我们将利用它来验证优化效果,并演示如何将三通道数据从设备发送到Mu。

4.1 配置Mu绘图器数据流

Mu绘图器期望通过串口(REPL)接收纯文本数据。每行数据代表一个时间点的所有通道值,数据之间用逗号或空格分隔。例如:

0.5,0.3,0.7 0.52,0.28,0.72 ...

这对应通道1、通道2、通道3的值。

在我们的CircuitPython代码中,除了在屏幕上绘图,我们还可以同时将数据打印到串口,供Mu绘图器捕获。

import time import board import analogio import microcontroller # 假设有三个模拟传感器连接到引脚A1, A2, A3 sensor_pins = [board.A1, board.A2, board.A3] sensors = [analogio.AnalogIn(pin) for pin in sensor_pins] # 初始化我们的滚动图表 # 假设屏幕是240x320,我们在(20,20)位置创建一个200x150的图表 graph = RollingGraph(x=20, y=20, width=200, height=150, history_len=100, channels=3) main_group = displayio.Group() main_group.append(graph.get_display_group()) board.DISPLAY.show(main_group) def read_normalized_sensors(): """读取所有传感器并归一化到[0, 1]范围。""" values = [] for sensor in sensors: # AnalogIn.value 范围是 0-65535 raw = sensor.value normalized = raw / 65535.0 values.append(normalized) return values while True: # 1. 读取数据 sensor_values = read_normalized_sensors() # 2. 更新本地屏幕图表(使用优化后的方法) graph.update(sensor_values) # 3. 同时将数据发送到串口,供Mu绘图器使用 # 格式化为字符串,用逗号分隔 data_str = ",".join("{:.3f}".format(v) for v in sensor_values) print(f"DATA,{data_str}") # 添加前缀“DATA,”便于Mu过滤(如果需要) # 或者直接打印数据,Mu可以识别 # print(data_str) # 4. 控制刷新率,例如10Hz time.sleep(0.1)

4.2 在Mu编辑器中观察与对比

  1. 打开绘图器: 在Mu编辑器中,点击顶部的“绘图器”按钮(图标通常是一个波形图)。
  2. 连接设备: 确保你的开发板通过USB连接到电脑,并且Mu已经连接到正确的串口。
  3. 观察数据流: 运行上面的代码。你会在绘图器窗口中看到三条实时滚动的曲线,分别对应三个传感器的数据。
  4. 性能对比测试
    • 优化前: 注释掉graph.update(sensor_values)中调用_redraw_dynamic()的部分,或者修改_redraw_dynamic(),用旧的“擦除旧线段”算法替换掉当前的清空重绘逻辑。观察Mu绘图器的曲线是否依然流畅?同时观察屏幕上的图表,很可能会出现严重的卡顿和闪烁。你可以在代码中打印每次update循环的耗时来量化。
    • 优化后: 换回优化后的代码。你应该能直观地感受到屏幕图表更新变得平滑、迅速。Mu绘图器中的曲线也会更连贯,因为主循环的延迟降低了,数据打印的间隔更稳定。

实操心得: Mu绘图器本身渲染能力很强,它主要受限于从串口接收数据的速率和稳定性。如果设备端因为绘图卡顿导致主循环变慢,那么打印数据的频率也会下降,Mu绘图器上的曲线就会出现“跳跃”或“卡住”的现象。因此,流畅的Mu绘图曲线间接证明了设备端代码的效率。你可以通过观察Mu中曲线的平滑度来定性判断优化效果。

4.3 定制发送给Mu的数据

有时你不想把所有原始数据都发给Mu,或者想发送计算后的衍生数据。print语句给了你最大的灵活性。

# 示例:发送原始值、滤波后的值、以及一个布尔报警状态 raw_vals = read_normalized_sensors() filtered_vals = low_pass_filter(raw_vals) # 假设有一个滤波函数 alarm = any(v > 0.8 for v in filtered_vals) # 发送自定义数据组合到Mu # 格式:原始值1,原始值2,原始值3,滤波值1,滤波值2,滤波值3,报警(0/1) output_data = raw_vals + filtered_vals + [1 if alarm else 0] print(",".join("{:.3f}".format(v) if isinstance(v, float) else str(v) for v in output_data))

在Mu绘图器中,你可以配置哪些列对应哪条曲线(颜色),从而同时监控原始信号、处理后的信号和系统状态。

5. 高级优化与疑难排解

即使使用了Bitmap.fill(),在资源极其受限的设备上绘制复杂图表仍可能遇到性能瓶颈。以下是一些进阶技巧和常见问题解决方法。

5.1 进一步性能提升技巧

  1. 减少重绘区域: 这是最有效的优化之一。如果你的图表只有一部分区域用于动态绘图(例如,右侧10%的宽度用于新数据),那么只清空和重绘这个狭窄的垂直条带,而不是整个图表宽度。将_redraw_dynamic中的清空循环范围从range(self.width)缩小到range(self.width - 10, self.width)

  2. 降低绘制分辨率: 不是每个历史数据点都需要绘制。如果history_len是100,但屏幕宽度只有200像素,那么每2个数据点绘制一条线段即可。在_redraw_dynamic的循环中,使用步长跳过一些点。

    # 每隔2个点绘制一次 step = 2 for i in range(0, self.history_len - step, step): idx1 = (self.current_index - i - step) % self.history_len idx2 = (self.current_index - i) % self.history_len # ... 绘制 idx1 到 idx2 的线段 ...
  3. 使用整数运算: 在映射函数_value_to_y和绘图算法中,尽量避免浮点数运算。使用整数运算能大幅提升速度。例如,将归一化的value范围从[0, 1]放大到[0, 1000]进行整数计算,最后再除以一个缩放因子得到像素坐标。

  4. 探索直接内存访问(高级): 对于极度追求性能的场景,可以研究displayio.Bitmap的底层内存缓冲区。有些版本CircuitPython的Bitmap对象支持memoryviewbytearray接口,允许你直接操作代表图像数据的字节数组。这样,清空操作可以变成对一个bytearrayfill(),可能更快。但这需要深入理解颜色索引的存储格式,且API可能不统一。

5.2 常见问题与解决方案

问题1:图表刷新导致主循环变慢,影响了传感器读取或控制逻辑。

  • 排查: 在循环开始和结束用time.monotonic()打印时间戳,计算graph.update()耗时。
  • 解决
    • 降低刷新率: 不是每次循环都需要更新图表。可以设置一个计数器,每N次循环更新一次图表。
    update_counter = 0 UPDATE_EVERY = 2 # 每2次循环更新一次图表 while True: # ... 读取传感器等 ... update_counter += 1 if update_counter >= UPDATE_EVERY: graph.update(sensor_values) update_counter = 0 else: # 即使不更新图表,也可以继续打印数据到Mu print(data_str) time.sleep(0.05)
    • 异步更新: 将绘图任务放在一个独立的中断或后台任务中。但CircuitPython对多线程/异步支持有限,需谨慎使用asyncio

问题2:屏幕闪烁依然存在。

  • 排查: 确认是否真正实现了双缓冲。我们的示例代码中_swap_buffers是逐像素复制,这本身可能成为瓶颈并导致闪烁,因为复制需要时间。
  • 解决
    • 尝试单缓冲+局部更新: 放弃双缓冲,直接在display_bitmap上操作。依靠fill()快速清空数据区域,然后立即绘制新数据。由于清空和绘制都很快,屏幕可能没有时间显示中间状态。这比慢速的双缓冲复制可能更好。
    • 优化交换逻辑: 如果硬件支持,探索是否有一次性交换两个Bitmap显示权的API。通常,这需要更底层的驱动支持。

问题3:Mu绘图器数据断断续续,曲线不连贯。

  • 排查: 这通常是串口通信或打印语句导致的延迟。print在CircuitPython中相对较慢,尤其是打印长字符串。
  • 解决
    • 精简打印数据: 减少小数点后的位数,例如用"{:.2f}"代替"{:.3f}"
    • 批量打印: 不是每次循环都打印,而是积累几次数据后一次性打印一行(用逗号分隔多次读数),但这会降低Mu绘图器的实时性。
    • 使用更快的传输协议: 考虑使用UDP或WebSocket通过网络发送数据到电脑上的自定义客户端,但这远超本文范围。

问题4:多通道曲线重叠时看不清。

  • 解决
    • 调整颜色: 使用对比度更高的颜色组合,避免同时使用低亮度的颜色。
    • 添加通道指示器: 在图表旁边或图例区域,用小块颜色显示当前哪个通道是激活的,或者显示每个通道的最新数值。
    • 分屏显示: 如果屏幕空间足够,为每个通道分配独立的图表区域。

5.3 内存不足的应对策略

Bitmap会消耗大量内存。一个200x150像素、16色(4位,即color_count=16)的Bitmap需要200 * 150 * 4 / 8 = 15,000字节(约14.6KB)。双缓冲就是两倍,约30KB。对于只有192KB RAM的SAMD51芯片,这占了不小比例。

  • 减小Bitmap尺寸: 这是最直接的方法。评估是否真的需要那么大的绘图区域。
  • 减少颜色数: 创建Bitmap时指定更少的颜色。例如,如果只需要4种颜色(背景、坐标轴、2条曲线),使用color_count=4,那么每个像素只占2位,内存减半。
  • 使用单缓冲: 如果闪烁可控,放弃双缓冲可以立即节省一半Bitmap内存。
  • 优化其他部分: 检查代码中是否有不必要的列表、字符串复制等,释放宝贵的内存。

经过这些优化,我的PyPortal温室项目图表刷新率从令人难以忍受的约2-3帧每秒(FPS),提升到了稳定的15-20 FPS,视觉上完全流畅。同时,主循环的周期也更加稳定,传感器读取和控制逻辑不再受绘图拖累。最关键的是,这套displayio.Bitmap.fill()结合双缓冲和局部更新的模式,已经成为我在所有CircuitPython可视化项目中的标准起手式。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/16 18:53:34

鸿蒙开发,抓包模拟器应用的网络请求

抓包工具&#xff1a;Reqable 下载地址&#xff1a;https://reqable.com/zh-CN/download/在鸿蒙模拟器上对 HTTPS 抓包&#xff0c;需满足三个条件&#xff1a; 1、网络代理连通 2、证书完整安装 3、模拟器网络代理1、配置 Reqable 与电脑端 Reqable 抓包地址及端口&#xff0c…

作者头像 李华
网站建设 2026/5/16 18:38:27

C++定时器实战:从线程轮询到时间轮算法的演进与选型

1. 定时器技术选型的核心痛点 当我们需要在C项目中实现定时任务调度时&#xff0c;最直观的做法可能就是直接开个线程轮询了。我刚开始做网络服务开发时也这么干过&#xff0c;结果上线后CPU直接飙到90%——这就是典型的"新手陷阱"。实际上&#xff0c;定时器的实现方…

作者头像 李华