在這篇教程中,我們看到的代碼看起來與其他的部分的代碼非常不同。那是因為我們大多數時候不得不在 MCU 的底層處理事情。大多數時候,MicroPython 可以隱藏很多在微控制器上工作的復雜性。
當我們 print(“hello”) 的時候,我們不必擔心微控制器存儲字母的方式,或它們被發送到串行終端的格式,或串行終端所接受的時鐘周期的數量。這些都是在后臺處理的。然而,當我們進入可編程輸入和輸出時(PIO),我們需要在更低的層次上處理這些邏輯。
我們將簡要介紹 PIO,并介紹一些高級主題,以便你了解正在發生的事情,并希望了解 Pico 上的 PIO 相對于其他微控制器是如何突顯優勢的。但是,理解創建 PIO 程序所需的所有低級數據操作需要花時間來完全理解,所以如果它看起來有點不透明也不用擔心。如果你對處理這種低級編程感興趣,那么我們將幫助你入門。如果你更感興趣的是在更高的層次上工作,而寧愿把低層次的爭論留給其他人,我們將向你展示如何使用 PIO 程序。
數據輸入和數據輸出
樹莓派 Pico 不僅支持 SPI 和 I2C 控制器發送數據。它還有自己的特殊協議:可編程 IO。讓我們看一個例子:
from rp2 import PIO, StateMachine, asm_pio from machine import Pin import time @asm_pio(set_init=PIO.OUT_LOW) def led_quarter_brightness(): set(pins, 0)[2] set(pins, 1) @asm_pio(set_init=PIO.OUT_LOW) def led_half_brightness(): set(pins, 0) set(pins, 1) @asm_pio(set_init=PIO.OUT_HIGH) def led_full_brightness(): set(pins, 1) sm1 = StateMachine(1, led_quarter_brightness, freq=10000, set_base=Pin(25)) sm2 = StateMachine(2, led_half_brightness, freq=10000, set_base=Pin(25)) sm3 = StateMachine(3, led_full_brightness, freq=10000, set_base=Pin(25)) while(True): sm1.active(1) time.sleep(1) sm1.active(0) sm2.active(1) time.sleep(1) sm2.active(0) sm3.active(1) time.sleep(1) sm3.active(0)
這些方法實際上是運行在 PIO 狀態機上的小程序,且在不斷循環。例如,led_half_brightness() 會不斷地打開和關閉 LED,這樣 LED 就會有一半的時間是關閉的,一半的時間是打開的。led_full_brightness() 將類似地循環,但由于惟一的指令是打開 LED,這實際上并沒有改變任何東西。
這里稍微有點不尋常的是 led_quarter_brightness()。每個 PIO 指令只需要運行一個時鐘周期(可以通過設置頻率來更改時鐘周期的長度,稍后我們將看到)。但是,我們可以在一條指令之后用方括號添加一個 1 到 31 之間的數字,來告訴 PIO 狀態機在運行下一條指令之前暫停這個時鐘周期。
然后,在 led_quarter_brightness() 中,兩個 set 指令每個使用一個時鐘周期,延遲使用兩個時鐘周期,因此總循環使用四個時鐘周期。在第一行中,set 指令需要一個周期,延遲需要兩個周期,所以 GPIO 管腳在這四個周期中的三個是關閉的。這使得 LED 的亮度達到了持續亮燈的四分之一。
一旦設定了 PIO 程序,Pico 就需要將其加載到狀態機中。因為我們有三個程序,所以需要將它們加載到三個狀態機中(有 8 個狀態機可以使用,編號為 0-7)。這可以用下面一行來實現:
sm1 = StateMachine(1, led_quarter_brightness, freq=10000, set_base=Pin(25))
參數如下:
– 狀態機器編號
– PIO 程序加載
– 頻率(必須在 2000 到 125000000 之間)
– 狀態機器操縱的 GPIO 引腳
還有一些額外的參數,你會在其他程序中看到,我們這里不需要。一旦創建了狀態機,就可以使用 active 方法啟動和停止狀態機,1 表示啟動,0 表示停止。
在我們的循環中,我們循環三個不同的狀態機。
一個真實的例子
讓我們通過一個實際示例來看看使用 PIO 的方法。WS2812B LED 燈條是一種包含三個 LED(一個紅色、一個綠色、一個藍色)和一個小型微控制器的燈組。它們由一根數據線控制,帶有計時相關協議。
LED 的接線很簡單,可能有一個插座,可以把頭部電線推進去,或者你可能需要自己焊接它們。
你需要注意的是從 Pico 上的 5V 引腳獲得的功率是有限的,如果無限擴展這個燈條,則需要額外給燈條供電。
現在我們已經把燈條和 Pico 連接好了,讓我們看看如何用 PIO 來控制它:
import array, time from machine import Pin import rp2 from rp2 import PIO, StateMachine, asm_pio # Configure the number of WS2812 LEDs. NUM_LEDS = 10 @asm_pio(sideset_init=PIO.OUT_LOW, out_shiftdir=PIO.SHIFT_LEFT, autopull=True, pull_thresh=24) def ws2812(): T1 = 2 T2 = 5 T3 = 3 label("bitloop") out(x, 1).side(0)[T3 - 1] jmp(not_x, "do_zero").side(1)[T1 - 1] jmp("bitloop").side(1)[T2 - 1] label("do_zero").side(0)[T2 - 1] nop() # Create the StateMachine with the ws2812 program, outputting on Pin(22). sm = StateMachine(0, ws2812, freq=8000000, sideset_base=Pin(0)) # Start the StateMachine, it will wait for data on its FIFO. sm.active(1)
它的基本邏輯是每秒發送 800,000 位數據(注意頻率是 8000000,程序的每個周期是 10 個時鐘周期)。每一位數據都是一個脈沖——一個短脈沖表示 0,一個長脈沖表示 1。這個程序和我們之前的 程序之間的一個很大的區別是,MicroPython 需要能夠向這個程序發送數據 PIO 的程序。
數據進入狀態機有兩個階段。第一個是稱為先進先出(FIFO)的內存。這是我們的主 Python 程序發送數據到的地方。第二個是輸出移位寄存器(OSR)。這就是 out() 指令獲取數據的地方。兩者通過拉指令連接,拉指令從 FIFO 獲取數據并將其放在 OSR 中。然而,由于我們的程序設置了啟用 autopull 的閾值為 24,所以每次我們從 OSR 讀取 24 位時,它將從 FIFO 重新加載指令 out(x,1) 從 OSR 中獲取一位數據,并將其放入名為 x 的變量中(PIO 中只有兩個可用變量:x 和 y)。
jmp 指令告訴代碼直接移動到特定的標簽,但是它可以有一個條件。指令 jmp(not_x,”do_zero”) 告訴代碼,如果 x 的值為 0(或者,在邏輯術語中,如果 not_x 為真,并且 not_x 是 x 的 反面——在 pio 級別中,0 為假,任何其他數字為真),則移動到 do_zero。
這里我們一直忽略的一個方面是 .side() 位。它們與 set() 類似,但它們與另一條指令同時發生。這意味著 out(x,1) 發生時,.side(0) 設置側集引腳的值為 0。
哇,對于這么小的一個程序來說,這實在是太多了?,F在我們已經激活了它,讓我們看看如何使用它。下面的代碼需要在程序中位于上述代碼之下,以便將數據發送到 PIO 程序。
# Display a pattern on the LEDs via an array of LED RGB values. ar = array.array("I", [0 for _ in range(NUM_LEDS)]) print("blue") for j in range(0, 255): for i in range(NUM_LEDS): ar[i] = j sm.put(ar,8) time.sleep_ms(100) print("red") for j in range(0, 255): for i in range(NUM_LEDS): ar[i] = j<<8 sm.put(ar,8) time.sleep_ms(100) print("green") for j in range(0, 255): for i in range(NUM_LEDS): ar[i] = j<<16 sm.put(ar,8) time.sleep_ms(100) print("white") for j in range(0, 255): for i in range(NUM_LEDS): ar[i]=j<<16+j<<8+j sm.put(ar,8) time.sleep_ms(100)
在這里,我們跟蹤一個名為 ar 的數組,它保存了我們希望 LED 擁有的數據。數組中的每個數字都包含了一盞燈上所有三種顏色的數據。格式有點奇怪,因為它是二進制的。使用 PIO 的一個問題是,你經常需要處理單個數據位。每一位數據 都是 1 或 0,數字可以通過這種方式建立,所以以 10 為基數的 2 就是二進制的 10。以 10 為基數的 3 在二進制中等于 11。二進制數的 8 位中最大的數是 11111111,或者以 10 為基數的 255。我們不會在這里深入討論二進制。讓人更困惑的是,我們實際上把三個數字存儲在一個數字中。這是因為在 MicroPython 中,整數存儲在 32 位,但每個數字只需要 8 位。最后還有一點空閑空間,因為我們只需要 24 位,不過沒關系。
前八位是藍色,后八位是紅色,最后八位是綠色。8 位最多可以存儲 255 個數字,所以每個 LED 都有 255 個亮度級別。我們可以使用移位運算符 << 來實現這一點。這將在一個數字的末尾加上一 定數量的 0,所以如果我們想讓 LED 的紅色、綠色和藍色亮度達到 1 級,我們將每個值都設為 1,然后將它們移動到合適的位數。對于綠色,我們有:
1 << 16 = 10000000000000000
對于紅色,我們有:
1 << 8 = 100000000
對于藍色,我們根本不需要移位位,所以我們只有 1。如果我們把所有這些加在一起, 我們得到以下(如果我們把前面的位加起來,得到一個 24 位的數):
000000010000000100000001
最右邊的八位是藍色的,接下來的八位是紅色的,最左邊的八位是綠色的。最后一點可能看起來有點令人困惑的是這行:
ar = array.array("I", [0 for _ in range(NUM_LEDS)])
這創建了一個數組,第一個值是 I,然后每個 LED 都是 0。在開頭有一個I的原因是它告訴 MicroPython 我們使用的是一系列 32 位的值。但是,對于每個值,我們只需要將 24 位發送給 PIO,所以我們告訴 put 命令刪除 8 位:
sm.put(ar,8)
相關說明
PIO 狀態機使用的語言非常簡潔,所以只有少量的指令。除了我們已經看過的,你還可以使用:
in():移動 1 到 32 位到狀態機)與out()類似,但相反)。
push():將數據發送到連接狀態機和主存的內存中 MicroPython 程序。
pull():從連接狀態機和主存的內存塊中獲取數據 MicroPython 程序。這里我們沒有使用它,因為通過在程序中包含 autopull=True,當我們使用 out() 時,會自動發生這種情況。
mov():在兩個位置之間移動數據(例如 x 和 y 變量)。
irq():控制中斷。如果你需要觸發一個特定的東西以在程序的 MicroPython 端運行,就可以使用這些。
wait():暫停直到發生一些事情(例如 IO pin 更改了一個設定值或中斷發生)。
雖然只有少量的指令,但可以實現大量的通信協議。大多數指令都是用于以某種形式移動數據。如果你需要以任何特定的方式準備數據,例如操縱你希望 LED 的顏色,這應該在你的主 MicroPython 程序中完成,而不是在 PIO 程序中。
審核編輯:劉清
-
微控制器
+關注
關注
48文章
7489瀏覽量
151048 -
狀態機
+關注
關注
2文章
492瀏覽量
27478 -
I2C協議
+關注
關注
0文章
26瀏覽量
8456 -
樹莓派
+關注
關注
116文章
1699瀏覽量
105524
原文標題:樹莓派 Pico 之可編程 IO(PIO)
文章出處:【微信號:趣無盡,微信公眾號:趣無盡】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論