当前位置: 华文问答 > 数码

KMK 固件 RGB 自定义动画

2024-01-15数码

阅前提醒:本文需要您具有一定的 Python 基础并了解 CircuitPython

KMK 官方的 RGB 文档在 http://kmkfw.io/docs/rgb

KMK 的 RGB 使用 HSV (色相、饱和度、亮度)色彩模式。

启用 RGB

首先需要在开发板中安装 Adafruit_CircuitPython_NeoPixel (https://github.com/adafruit/Adafruit_CircuitPython_NeoPixel)方法是:打开链接,找到旁边的 Release,下载与你的开发板安装的 circuitpyrhon 版本相对应的 zip 文件,解压,复制 lib/ 文件夹中的全部文件到开发板目录的 lib/ 文件夹中。

然后在 main.py 中添加

import board from kmk.extensions.RGB import RGB,AnimationModes rgb = RGB(pixel_pin=board.GP14, num_pixels=27, animation_mode=AnimationModes.STATIC) keyboard.extensions.append(rgb)

RGB 类的参数有:

rgb = RGB(pixel_pin=board.GP14, # 连接到 RGB 灯的引脚 num_pixels=0, # 灯的数量 rgb_order=(1, 0, 2), # 灯的 RGB 顺序,对于 WS2812 是 GRB, 通常不需要修改 val_limit=255, # 亮度限制,0~255 hue_default=0, # 默认色相,0~255 sat_default=255, # 默认饱和度,0~255 val_default=255, # 默认亮度,0~255 hue_step=4, # 色相的变化步进 sat_step=13, # 饱和度的变化步进 val_step=13, # 亮度的变化步进 animation_speed=1, # 动画速度 breathe_center=1, # 呼吸动画的配置参数,用于计算呼吸动画曲线,1.0~2.7 knight_effect_length=3, # 骑士动画的配置参数,骑士动画中点亮的 LED 灯的个数 animation_mode=AnimationModes.STATIC, # 动画,可用值见下文 effect_init=False, # 设置为 True 时将在下一轮动画函数运行前中重置位置和反转动画两个参数, # 通常在切换动画时才需要修改,初始化时不需要动这个参数 #(通常这种东西不需要写成构造函数的参数吧,这里不知道为什么要写到这儿) reverse_animation=False, # 反转动画,仅部分动画支持 user_animation=None, # 自定义动画函数 pixels=None, # 当使用多个 PixelBuffer 时,请设置 pixel_pin=None, num_pixels=0 并设置该参数为由多个 Pixelbuf 对象组成的元组 # 具体使用方法请查看文档,通常不需要修改 refresh_rate=60, # 刷新率,可看作动画帧率,决定动画函数的运行间隔,不能设置的太高,我个人推荐设成 30 )

参数 animation_mode 可用的值包括:

AnimationModes.OFF # 关闭 AnimationModes.STATIC # 静态 AnimationModes.BREATHING # 呼吸 AnimationModes.RAINBOW # 彩虹 AnimationModes.BREATHING_RAINBOW # 彩虹呼吸 AnimationModes.KNIGHT # 骑士 AnimationModes.USER # 自定义动画,需要同时设置 user_animation

自定义动画

KMK 的 RGB 灯效,或者应该叫动画,下文将介绍自定义动画的细节。

编写动画函数

就像普通的动画一样,KMK 的 RGB 动画也是一帧一帧展示出来的。动画是由一个函数控制的,这里暂且把这个函数叫做动画函数,动画函数会每隔 1000/refresh_rate 毫秒执行一次,每次执行控制 RGB 展示一帧。注意,动画函数的执行时间不能超过这个时间间隔,并推荐不要超过这个时间间隔的 2/3,否则将导致极大的输入延迟。所以也不能将 refresh_rate 设置为很高的值。

(默认的 refresh_rate 是 60, 也就是间隔为 16 毫秒,在灯的数量为 86 颗的情况下,有几个官方动画执行耗时是超过这个时间的,所以我的键盘将 refresh_rate 设置为 30)

(事实上 RGB 动画本身都会导致额外的输入延迟,这是由于 circuitpython 本身不支持多线程和其本身效率较低导致的)

官方的动画函数在文件 kmk/extensions/rgb.py 的 480~529 行之间,而自定义动画就是编写一个动画函数并将 RGB 对象的参数中的 user_animation 设置为编写的动画函数。

首先,定义一个函数,这个函数接受一个参数,我推荐将参数命名为 rgb

def animation_name(rgb):

然后就是编写函数内容了。在这个函数运行时,将使用整个 RGB 对象将作为参数,所以可以通过变量 rgb 使用 RGB 对象的成员变量和成员函数来读写状态、控制 LED 灯展示一帧。

尽管理论上可以从使用 RGB 对象的所有成员变量或成员函数,但是常用的也就这些(不过我推荐还是阅读一下 rgb.py

# 常用成员函数 rgb.set_hsv_fill(hue, sat, val) # 使用 HSV 值设置所有 LED 灯 rgb.set_hsv(hue, sat, val, index) # 将第 index 个 LED 灯的值设置为 HSV rgb.set_rgb_fill((r, g, b)) # 使用 RGB(W) 值设置所有 LED 灯,不推荐使用 rgb.set_rgb((r, g, b), index) # 将第 index 个 LED 灯的值设置为 RGB(W),不推荐使用 rgb.increase_hue(step) # 按给定的步长提高色相,即下文中的 rgb.hue rgb.decrease_hue(step) # 按给定的步长降低色相,即下文中的 rgb.hue rgb.increase_sat(step) # 按给定的步长提高饱和度,即下文中的 rgb.sat rgb.decrease_sat(step) # 按给定的步长降低饱和度,即下文中的 rgb.sat rgb.increase_val(step) # 按给定的步长提高亮度,即下文中的 rgb.val rgb.decrease_val(step) # 按给定的步长提高亮度,即下文中的 rgb.val rgb.increase_ani() # 动画速度+1, 最大到10,即下文中的 rgb.animation_speed rgb.decrease_ani() # 动画速度-1, 最小到0,即下文中的 rgb.animation_speed rgb.off() # 关闭 RGB rgb.show() # 刷新 RGB,动画函数完成后 RGB 会自动刷新,因此通常情况下不要使用这个函数 # 常用成员变量,不推荐直接修改这些变量,推荐使用上面的函数操作这些变量 rgb.num_pixels # 灯的数量 rgb.hue # 当前色相 rgb.hue_step # 色相变化步长 rgb.sat # 当前饱和度 rgb.sat_step # 饱和度变化步长 rgb.val # 当前亮度 rgb.val_step # 亮度变化步长 rgb.reverse_animation # 是否反转动画 rgb.animation_mode # 当前动画模式 rgb.animation_speed # 当前动画速度 rgb._step # 该变量的值受到 rgb.animation_speed 控制而在动画函数执行前发生变化,使用该值作为步长可以让自定义动画接受动画速度的控制,具体可参考 rgb.py 的 463~466 行及下文的分析

写一个简单的,比如我在这个键盘中使用的彩色流动动画:

def stream(rgb): for i in range(rgb.num_pixels): rgb.set_hsv((rgb.hue + rgb.hue_step * i) % 256, rgb.sat, rgb.val, i) rgb.increase_hue(rgb._step) # 这里使用 rgb._step 可以使动画速度可控 这个动画的效果图


启用自定义动画

把 RGB 对象的参数 animation_mode 设置为 AnimationModes.USER 并把 user_animation 设置为动画函数的函数名即可启用自定义动画(注意是函数名,后面不能加括号)

例如

rgb = RGB(pixel_pin=board.GP14, num_pixels=27, animation_mode=AnimationModes.STATIC)

改成:

rgb = RGB(pixel_pin=board.GP14, num_pixels=27, animation_mode=AnimationModes.USER, user_animation=stream)

即可启用自定义动画。

测量耗时

启用动画并不是结束,正如上文所说,动画函数的执行用时不能超过 1000/refresh_rate 毫秒,并推荐不要超过该值的 2/3,所以我们还需要测量动画函数的耗时以确定这个自定义动画是否可以在键盘上正常工作。

需要导入 supervisor 这个模块,并在函数的第一句前加上:

s_time = supervisor.ticks_ms()

在最后一句后加上:

print('timeuse: ', supervisor.ticks_ms() - s_time)

以上文中的彩色流动动画为例,改完之后将变成:

import supervisor def stream(rgb): s_time = supervisor.ticks_ms() for i in range(rgb.num_pixels): rgb.set_hsv((rgb.hue + rgb.hue_step * i) % 256, rgb.sat, rgb.val, i) rgb.increase_hue(rgb._step) # 这里使用 rgb._step 可以使动画速度可控 print('timeuse: ', supervisor.ticks_ms() - s_time)

即可在串口输出中查看函数耗时。

或者也可以用更高级的做法,比如装饰器:

import supervisor def get_time(f): def inner(*arg, **kwarg): s_time = supervisor.ticks_ms() res = f(*arg, **kwarg) e_time = supervisor.ticks_ms() print('timeuse: {}'.format(e_time - s_time)) return res return inner @get_time def stream(rgb): for i in range(rgb.num_pixels): rgb.set_hsv((rgb.hue + rgb.hue_step * i) % 256, rgb.sat, rgb.val, i) rgb.increase_hue(rgb._step) # 这里使用 rgb._step 可以使动画速度可控

通常我们会发现大多数时候函数执行时间不会超过限制但偶尔有几次会超过,比如在我的键盘上实测 stream() 函数一般耗时 20 毫秒左右,偶尔会到 40 毫秒,这种是正常情况,不会导致问题。(所以应该是平均耗时不能超过限制)

如果超过时间限制了该怎么办

首先要看当前函数的耗时是什么一个情况。

如果超过 100 毫秒,那么就不需要考虑了,请放弃这个动画,或者尝试更换实现方式。如果你的动画很简单但是还是出现了超过 100 毫秒的执行时间,那么你应该重点排查函数里是不是有死循环、过多循环、 time.sleep() rgb.show() 等高耗时内容。

如果在 50 毫秒左右,可以考虑分区刷新,比如函数执行一次只修改一半的 LED 灯。

如果在 16 毫秒到 30 毫秒之间,可以考虑降低 refresh_rate 到 30,或者在不到 20 毫秒时可以尝试超频。(警告:超频可能会导致失去保修、设备损坏等严重后果,请谨慎超频)(我的键盘上在用的固件超频到了 150 MHz, 并加入了尚未完善的部分功能,这些还没合并到主分支,可以到我的 github 项目的 lcd 分支 https://github.com/calico-cat-3333/calicocat-keyboard/tree/lcd 中查看)

我对 KMK 内部实现的一些理解。

RGB 类继承自 Extension 类(见 kmk/extensions/__init__.py ),KMK 固件的主循环会在固定阶段执行该类中的特定成员函数,这是 KMK 扩展和模块的实现基础。

其中有一个成员函数是 during_bootup() , 这个函数会在 KMK 固件启动时执行,在 RGB 类中,是在文件 kmk/extensions/rgb.py 的第 207 行,这个函数的前面就是初始化硬件什么的,重点在最后一句,也就是 234 行

self._task = create_task(self.animate, period_ms=(1000 // self.refresh_rate))

这里的 create_task() 函数来自 kmk/scheduler.py ,其实现了一个简单的优先级任务调度器,关于调度器的实现细节这里不过多讨论,总之就是 create_task() 函数在参数 period_ms 不为 0 时会创建一个每隔 period_ms 毫秒执行一次的任务。在这里,任务为 animate() 函数,间隔为 1000/refresh_rate 毫秒。

animate() 函数在 rgb.py 的第 426 行,它会先根据 effect_init 决定是否执行 _init_effect() ,这个函数会重置 pos reverse_animation effect_init 三个变量,然后判断当前动画的启用状态来决定是否继续,然后执行 _animation_step() ,它会修改 _step ,然后就是一堆 if else 根据 animation_mode 决定执行哪个函数,如果是 AnimationModes.USER ,那就会执行 self.user_animation(self) 执行自定义动画函数实现自定义动画功能。

_animation_step() 函数的内容是:

def _animation_step(self): self._substep += self.animation_speed / 4 self._step = int(self._substep) self._substep -= self._step

它先让 _substep 增加 animation_speed/4 然后取整赋给 _step 最后再减去 _step ,例如当 animation_speed=1 时, _substep 每次增加 0.25,使 _step 按照 0, 0, 0, 1 的规律变化,这样,当动画函数中使用 _step 作为步长时,就会每执行四次才增加 1,相当于其增长速度变为原来的四分之一。

注:文中的行数是以 KMK 截止本文所撰写时的北京时间 2024 年 1 月 12 日的最新版本为基准,可能因后续 KMK 固件更新而不再准确,因此仅供参考。