這是探索使用 Micropython 編程 Raspberry Pi Pico PIO 的意外特性系列文章的第二部分。如果你錯過了第一部分,我們揭示了四個挑戰對寄存器數量、指令槽、拉取非阻塞行為以及智能但便宜的硬體的假設的 Wats。
現在,我們繼續我們的旅程,製作一個類似電子風琴的音樂儀器——這個項目揭示了 PIO 編程的一些特性和困惑。準備好以一種讓人想起莎士比亞悲劇的方式挑戰你對常數的理解。
Wat 5: 不穩定的常數
在 PIO 編程的世界裡,常數應該是可靠的、堅定的,嗯,就是常數。但是,如果它們不是呢?這引出了關於 PIO 中 set 指令如何運作——或者不運作——在處理較大常數時的困惑 Wat。
就像朱麗葉懷疑羅密歐的堅定一樣,你可能會想知道 PIO 的常數是否會像她所說的那樣“同樣變化無常”。
問題:常數並不像看起來那麼大
想像一下,你正在編程一個超聲波測距儀,需要從 500 開始倒數,同時等待回音信號從高電壓降到低電壓。為了在 PIO 中設置這個等待時間,你可能天真地嘗試直接使用 set 加載常數值:
; 在 Rust 中,確保 ‘config.shift_in.direction = ShiftDirection::Left;’
set y, 15 ; 加載高 5 位 (0b01111)
mov isr, y ; 傳輸到 ISR (清除 ISR)
set y, 20 ; 加載低 5 位 (0b10100)
in y, 5 ; 將低位移入以形成 ISR 中的 500
mov y, isr ; 傳輸回 y
附註:不要試圖理解這裡瘋狂的 jmp 操作。我們將在下一個 Wat 6 中討論這些。
但這裡有一個悲劇性的轉折:PIO 中的 set 指令僅限於 0 到 31 之間的常數。此外,這個命運多舛的 set 指令不會報告錯誤。相反,它默默地損壞整個 PIO 指令,這會產生一個無意義的結果。
解決不穩定常數的替代方案
為了解決這個限制,考慮以下幾種方法:
讀取值並將其存儲在寄存器中:我們在 Wat 1 中看到了這種方法。你可以將常數加載到 osr 寄存器中,然後轉移到 y。例如:
# 將最大回音等待讀入 OSR。
pull ; 與 pull 塊相同
mov y, osr ; 將最大回音等待加載到 Y
移位並組合較小的值:使用 isr 寄存器和 in 指令,你可以構建任何大小的常數。不過,這會消耗你 32 操作預算中的時間和操作(見第一部分,Wat 2)。
; 在 Rust 中,確保 ‘config.shift_in.direction = ShiftDirection::Left;’
set y, 15 ; 加載高 5 位 (0b01111)
mov isr, y ; 傳輸到 ISR (清除 ISR)
set y, 20 ; 加載低 5 位 (0b10100)
in y, 5 ; 將低位移入以形成 ISR 中的 500
mov y, isr ; 傳輸回 y
減慢計時:降低狀態機的頻率,以便在更多系統時鐘週期中延長延遲。例如,將狀態機速度從 125 MHz 降低到 343 kHz,將超時常數 182,216 減少到 500。
使用額外的延遲和(嵌套)循環:所有指令都支持可選的延遲,允許你添加最多 31 個額外的週期。(要生成更長的延遲,使用循環——甚至嵌套循環。)
; 生成 10μs 觸發脈衝(343_000Hz 下的 4 週期)
set pins, 1 [3] ; 將觸發引腳設置為高,添加 3 的延遲
set pins, 0 ; 將觸發引腳設置為低電壓
使用“減法技巧”生成最大 32 位整數:在 Wat 7 中,我們將探索一種通過減法生成 4,294,967,295(最大無符號 32 位整數)的方法。
就像朱麗葉警告不要以不穩定的月亮發誓一樣,我們發現 PIO 常數並不總是如它們看起來那麼堅定。然而,就像它們的故事出現意想不到的轉折,我們的故事也是如此,從常數的不穩定性轉向條件的不均勻性。在下一個 Wat 中,我們將探索 PIO 如何處理條件跳轉,讓你懷疑它對邏輯的忠誠。
Wat 6: 通過鏡子看條件
在大多數編程環境中,邏輯條件感覺是平衡的:你可以測試引腳是高還是低,或檢查寄存器的相等或不相等。在 PIO 中,這種對稱性破裂。你可以在引腳高時跳轉,但不能在引腳低時跳轉,並且在 x!=y 時可以跳轉,但在 x==y 時不能。這些規則是奇特的——就像《通過鏡子》中的哈姆提·丹提:“當我定義一個條件時,它的意思正是我選擇的意思——既不多也不少。”
這些特性迫使我們重寫代碼以適應不對稱的邏輯,創造了我們希望代碼能夠編寫的方式與我們必須編寫的方式之間的鴻溝。
問題:不對稱條件的實際情況
考慮一個簡單的場景:使用測距儀,你想從最大等待時間(y)倒數,直到超聲波回音引腳變為低電壓。直觀地,你可能會這樣編寫邏輯:
measure_echo_loop:
jmp !pin measurement_complete ; 如果回音電壓低,測量完成
jmp y– measure_echo_loop ; 除非超時,繼續倒數
當處理測量時,如果我們只希望輸出與先前值不同的值,我們會這樣寫:
measurement_complete:
jmp x==y cooldown ; 如果測量相同,跳過冷卻
mov isr, y ; 將測量存儲在 ISR 中
push ; 輸出 ISR
mov x, y ; 將測量存儲在 X 中
不幸的是,PIO 不允許你直接測試 !pin 或 x==y。你必須重構你的邏輯,以適應可用的條件,例如 pin 和 x!=y。
解決方案:必須的方式
鑑於 PIO 的限制,我們用兩步驟的方法來調整邏輯,以確保所需的行為,儘管缺少條件:
在相反的條件下跳轉,以跳過兩條指令。
接下來,使用無條件跳轉到達所需的目標。
這種變通方法增加了一個額外的跳轉(影響指令限制),但額外的標籤是免費的。
以下是重新編寫的代碼,用於倒數直到引腳變為低電壓:
measure_echo_loop:
jmp pin echo_active ; 如果回音電壓高,繼續倒數
jmp measurement_complete ; 如果回音電壓低,測量完成
echo_active:
jmp y– measure_echo_loop ; 除非超時,繼續倒數
以下是處理測量的代碼,這樣它只會輸出不同的值:
measurement_complete:
jmp x!=y send_result ; 如果測量不同,則發送。
jmp cooldown ; 如果測量相同,則不發送。
send_result:
mov isr, y ; 將測量存儲在 ISR 中
push ; 輸出 ISR
mov x, y ; 將測量存儲在 X 中
來自哈姆提·丹提的條件教訓
在《通過鏡子》中,愛麗絲學會了如何在哈姆提·丹提的奇特世界中導航——就像你將學會在 PIO 的不對稱條件的仙境中導航。
但一旦你掌握了一個特性,另一個特性就會顯現出來。在下一個 Wat 中,我們將揭示 jmp 的一個驚人行為,如果它是一名運動員,將打破世界紀錄。
在第一部分的 Wat 1 和 Wat 3 中,我們看到 jmp x– 或 jmp y– 通常用於通過減少寄存器直到它達到 0 來循環固定次數。這樣很簡單,對吧?但是當 y 為 0 時,我們運行以下指令會發生什麼?
jmp y– measure_echo_loop
如果你猜測它不會跳轉到 measure_echo_loop,而是直接執行下一條指令,那你完全正確。但是要獲得完整的分數,回答這個問題:指令執行後,y 的值是多少?
答案是:4,294,967,295。為什麼?因為 y 在測試為零後才會減少。Wat!?
附註:如果這讓你感到驚訝,那麼你可能對 C 或 C++ 有經驗,這些語言區分前增量(例如,++x)和後增量(例如,x++)操作。jmp y– 的行為相當於後減少,其中值在減少之前進行測試。
這個值 4,294,967,295 是 32 位無符號整數的最大值。就好像一名田徑運動員從起跳板起跳,但卻沒有落在沙坑裡,而是超過了沙坑,落在了另一個大陸上。
附註:如同在 Wat 5 中預示的,我們可以故意利用這種行為將寄存器設置為 4,294,967,295 的值。
現在我們已經學會了如何用 jmp 完成落地,讓我們看看如何避免被 PIO 讀取和設置的引腳所困。
在德克·蘇斯的《太多的戴夫》中,麥克維夫人有 23 個兒子,全部叫戴夫,這導致每當她叫出他們的名字時都會引起無盡的混淆。在 PIO 編程中,pin 和 pins 可以根據上下文指代完全不同的引腳範圍。很難知道你在和哪個戴夫或戴夫們說話。
問題:引腳範圍和子範圍
在 PIO 中,pin 和 pins 指令依賴於在 Rust 中定義的引腳範圍,這是在 PIO 之外的。然而,單個指令通常在這些引腳範圍的子範圍上操作。根據命令的不同,行為會有所不同:子範圍可能是範圍的前 n 個引腳、所有引腳,或者僅僅是由索引給定的特定引腳。為了澄清 PIO 的行為,我創建了以下表格:
這個表格顯示了 PIO 在不同指令中如何解釋 pin 和 pins 的術語,以及它們的相關上下文和配置。
示例:測距儀的距離程序
這是一個使用觸發和回音引腳測量物體距離的 PIO 程序。這個程序的主要特點是:
- 持續運行:測距儀在循環中以最快的速度運行。
- 最大範圍限制:測量被限制在給定距離內,如果未檢測到物體,則返回值為 4,294,967,295。
- 過濾輸出:僅發送與其直接前一個值不同的測量,減少輸出速率。
快速瀏覽這個程序,注意到雖然它在處理兩個引腳——觸發和回音——但在整個程序中我們只看到 pin 和 pins。
.program distance
; X 是最後發送的值。初始化為
; u32::MAX,這意味著“回音超時”
; (通過從 0 減去 1 將 X 設置為 u32::MAX)
set x, 0
subtraction_trick:
jmp x– subtraction_trick
; 將最大回音等待讀入 OSR
pull ; 與 pull 塊相同
; 主循環
.wrap_target
; 生成 10μs 觸發脈衝(343_000Hz 下的 4 週期)
set pins, 0b1 [3] ; 將觸發引腳設置為高,添加 3 的延遲
set pins, 0b0 ; 將觸發引腳設置為低電壓
; 當觸發引腳變高時,開始倒數直到變低
wait 1 pin 0 ; 等待回音引腳為高電壓
mov y, osr ; 將最大回音等待加載到 Y
measure_echo_loop:
jmp pin echo_active ; 如果回音電壓高,繼續倒數
jmp measurement_complete ; 如果回音電壓低,測量完成
echo_active:
jmp y– measure_echo_loop ; 除非超時,繼續倒數
; Y 告訴回音倒數停止的位置。
; 如果回音超時,它將是 u32::MAX。
measurement_complete:
jmp x!=y send_result ; 如果測量不同,則發送。
jmp cooldown ; 如果測量相同,則不發送。
send_result:
mov isr, y ; 將測量存儲在 ISR 中
push ; 輸出 ISR
mov x, y ; 將測量存儲在 X 中
; 在下一次測量之前的冷卻期
cooldown:
wait 0 pin 0 ; 等待回音引腳為低
.wrap ; 重新啟動測量循環
配置引腳
為了確保 PIO 程序按預期運行:
- set pins, 0b1 應該控制觸發引腳。
- wait 1 pin 0 應該監控回音引腳。
- jmp pin echo_active 也應該監控回音引腳。
以下是如何在 Rust 中配置這些引腳(後面是解釋):
let mut distance_state_machine = pio1.sm0;
let trigger_pio = pio1.common.make_pio_pin(hardware.trigger);
let echo_pio = pio1.common.make_pio_pin(hardware.echo);
distance_state_machine.set_pin_dirs(Direction::Out, &[&trigger_pio]);
distance_state_machine.set_pin_dirs(Direction::In, &[&echo_pio]);
distance_state_machine.set_config(&{
let mut config = Config::default();
config.set_set_pins(&[&trigger_pio]); // 用於 set 指令
config.set_in_pins(&[&echo_pio]); // 用於 wait 指令
config.set_jmp_pin(&echo_pio); // 用於 jmp 指令
let program_with_defines = pio_file!(“examples/distance.pio”);
let program = pio1.common.load_program(&program_with_defines.program);
config.use_program(&program, &[]); // 無側邊設置引腳
config
});
這裡的關鍵是 <strong>set_set_pins</strong>、<strong>set_in_pins</strong> 和 set_jmp_pin 方法在 Config 結構中。
- set_in_pins:指定用於輸入操作的引腳,例如 wait(1, pin, …)。 “in” 引腳必須是連續的。
- set_set_pins:配置用於 set 操作的引腳,例如 set(pins, 1)。 “set” 引腳也必須是連續的。
- set_jmp_pin:定義在條件跳轉中使用的單個引腳,例如 jmp(pin, …)。
如表中所述,其他可選輸入包括:
- set_out_pins:設置連續引腳用於輸出操作,例如 out(pins, …)。
- use_program:設置 a) 加載的程序和 b) 連續引腳用於側邊設置操作。側邊設置操作允許在其他指令執行期間同時切換引腳。
配置多個引腳
雖然這個程序不需要,但你可以通過提供連續引腳的切片來在 PIO 中配置一系列引腳。例如,假設我們有兩個超聲波測距儀:
let trigger_a_pio = pio1.common.make_pio_pin(hardware.trigger_a);
let trigger_b_pio = pio1.common.make_pio_pin(hardware.trigger_b);
config.set_set_pins(&[&trigger_a_pio, &trigger_b_pio]);
然後單個指令可以控制這兩個引腳:
set pins, 0b11 [3] # 將兩個觸發引腳(17, 18)設置為高,添加延遲
set pins, 0b00 # 將兩個觸發引腳設置為低
這種方法讓你能夠高效地將位模式應用於多個引腳,同時簡化了涉及多個輸出的應用控制。
附註:編程中的“設置”一詞
在編程中,“設置”一詞通常有多種含義。在 PIO 的上下文中,“設置”是指你可以分配值的東西——例如引腳的狀態。它並不意味著一組東西,正如它在其他編程上下文中經常所指的那樣。當 PIO 指的是一個集合時,通常會使用“範圍”這個術語。這一區分對於避免在使用 PIO 時的混淆至關重要。
來自麥克維夫人的教訓
在《太多的戴夫》中,麥克維夫人懊悔沒有給她的 23 個戴夫取更獨特的名字。你可以通過在註釋中清楚地記錄你的引腳,使用有意義的名稱——比如觸發和回音——來避免她的錯誤。
但是如果你認為處理這些引腳範圍很棘手,那麼調試 PIO 程序則增加了全新的挑戰。在下一個 Wat 中,我們將深入探討可用的調試方法。讓我們看看我們能推進多遠。
我喜歡在 VS Code 中使用互動式斷點進行調試。我還進行打印調試,插入臨時信息語句以查看代碼在做什麼以及變量的值。使用 Raspberry Pi Debug Probe 和 probe-rs,我可以在 Pico 上使用常規 Rust 代碼進行這兩種操作。
然而,在 PIO 編程中,我無法做到這兩點。
備用方案是推送到打印調試。在 PIO 中,你暫時輸出感興趣的整數值。然後,在 Rust 中,你使用 info! 來打印這些值以供檢查。
例如,在以下 PIO 程序中,我們暫時添加指令以推送 x 的值進行調試。我們還包括 set 和 out 以推送一個常數值,例如 7,這必須在 0 到 31 之間。
.program distance
; X 是最後發送的值。初始化為
; u32::MAX,這意味著“回音超時”
; (通過從 0 減去 1 將 X 設置為 u32::MAX)
set x, 0
subtraction_trick:
jmp x– subtraction_trick
; 調試:查看 x 的值
mov isr, x
push
; 將最大回音等待讀入 OSR
pull ; 與 pull 塊相同
; 調試:發送常數值
set y, 7 ; 推送 ‘7’ 以便我們知道我們已經到達這一點
mov isr, y
push
; …
回到 Rust 中,你可以讀取並打印這些值,以幫助理解 PIO 代碼中發生了什麼(完整代碼和項目):
// …
distance_state_machine.set_enable(true);
distance_state_machine.tx().wait_push(MAX_LOOPS).await;
loop {
let end_loops = distance_state_machine.rx().wait_pull().await;
info!(“end_loops: {}”, end_loops);
}
// …
輸出:
INFO Hello, debug!
└─ distance_debug::inner_main::{async_fn#0} @ examplesdistance_debug.rs:27
INFO end_loops: 4294967295
└─ distance_debug::inner_main::{async_fn#0} @ examplesdistance_debug.rs:57
INFO end_loops: 7
└─ distance_debug::inner_main::{async_fn#0} @ examplesdistance_debug.rs:57
當推送到打印調試不足以滿足需求時,你可以轉向硬體工具。我買了我的第一個示波器(FNIRSI DSO152,價格為 37 美元)。使用它,我能夠確認回音信號正常工作。然而,觸發信號對於這個便宜的示波器來說太快,無法清晰捕捉。
使用這些方法——特別是推送到打印調試——你可以追蹤 PIO 程序的流程,即使沒有傳統的調試器。
附註:在 C/C++(以及可能的 Rust)中,你可以更接近完整的 PIO 調試體驗,例如,通過使用 piodebug 項目。
這結束了九個 Wats,但讓我們在一個額外的 Wat 中將所有內容整合在一起。
現在所有組件都準備好了,是時候將它們組合成一個可工作的類似電子風琴的音樂儀器。我們需要一個 Rust 監控程序。這個程序啟動兩個 PIO 狀態機——一個用於測量距離,另一個用於生成音調。然後它等待新的距離測量,將該距離映射到音調,並將相應的音調頻率發送到播放音調的狀態機。如果距離超出範圍,它會停止音調。
Rust 的角色:在這個系統的核心是一個將距離(從 0 到 50 公分)映射到音調(大約 B2 到 F5)的函數。這個函數在 Rust 中很簡單,利用了 Rust 的浮點數學和指數運算。在 PIO 中實現這一點幾乎是不可能的,因為它的指令集有限,且不支持浮點數。
以下是運行電子風琴的核心監控程序(完整文件和項目):
sound_state_machine.set_enable(true);
distance_state_machine.set_enable(true);
distance_state_machine.tx().wait_push(MAX_LOOPS).await;
loop {
let end_loops = distance_state_machine.rx().wait_pull().await;
match loop_difference_to_distance_cm(end_loops) {
None => {
info!(“距離:超出範圍”);
sound_state_machine.tx().wait_push(0).await;
}
Some(distance_cm) => {
let tone_frequency = distance_to_tone_frequency(distance_cm);
let half_period = sound_state_machine_frequency / tone_frequency as u32 / 2;
info!(“距離:{} 公分,音調:{} Hz”, distance_cm, tone_frequency);
sound_state_machine.tx().push(half_period); // 非阻塞推送
Timer::after(Duration::from_millis(50)).await;
}
}
}
使用兩個 PIO 狀態機和一個 Rust 監控程序,讓你可以同時運行三個程序。這種設置本身就很方便,並且在需要嚴格計時或非常高頻的 I/O 操作時至關重要。
附註:或者,Rust Embassy 的異步任務讓你可以直接在單個主處理器上實現協作多任務。你用 Rust 編碼,而不是混合 Rust 和 PIO。雖然 Embassy 任務並不真的同時運行,但它們切換得足夠快,可以處理像電子風琴這樣的應用。以下是 theremin_no_pio.rs 中顯示的類似核心循環的片段:
loop {
match distance.measure().await {
None => {
info!(“距離:超出範圍”);
sound.rest().await;
}
Some(distance_cm) => {
let tone_frequency = distance_to_tone_frequency(distance_cm);
info!(“距離:{} 公分,音調:{} Hz”, distance_cm, tone_frequency);
sound.play(tone_frequency).await;
Timer::after(Duration::from_millis(50)).await;
}
}
}
請參閱我們最近的文章,了解 Rust Embassy 編程的更多細節。
現在我們已經組裝了所有組件,讓我們再次觀看我“演奏”這個音樂儀器的視頻。在監控屏幕上,你可以看到調試打印顯示距離測量和相應的音調。這種視覺連接突顯了系統如何實時響應。
結論
在 Raspberry Pi Pico 上的 PIO 編程是一種簡單與複雜的迷人結合,提供無與倫比的硬體控制,同時要求開發者轉變思維,適應更高層次的編程。通過我們探索的九個 Wats,PIO 既以其限制讓我們驚訝,也以其原始效率讓我們印象深刻。
雖然我們已經涵蓋了重要的內容——管理狀態機、引腳分配、計時細節和調試——但你仍然可以根據需要學習更多:DMA、IRQ、側邊設置引腳、Pico 1 和 Pico 2 之間的差異、自動推送和自動拉取、FIFO 連接等等。
推薦資源
在其核心,PIO 的特性反映了一種設計理念,優先考慮低級硬體控制,並將開銷降至最低。通過擁抱這些特徵,PIO 不僅能滿足你項目的需求,還能為嵌入式系統編程開啟新的可能性。
請關注 Carl 在 Towards Data Science 和 @carlkadie.bsky.social 的文章。我寫有關 Rust 和 Python 的科學編程、機器學習和統計的文章。每月我會寫一篇文章。
本文由 AI 台灣 運用 AI 技術編撰,內容僅供參考,請自行核實相關資訊。
歡迎加入我們的 AI TAIWAN 台灣人工智慧中心 FB 社團,
隨時掌握最新 AI 動態與實用資訊!