閱前提醒:本文需要您具有一定的 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 固件更新而不再準確,因此僅供參考。