樹莓派基金會在 2021 年初推出了微控制器 Raspberry Pi pico,當時覺得很新奇想說樹莓派之前沒有這樣子的產線,買了一片之後就先放著沒有多加研究。
仔細看了一下才發現規格其實還蠻不錯的:
Raspberry Pi pico | 規格 | Arduino nano |
---|---|---|
ARM M0+ dual core | MCU | ATmega328p |
133MHz | 主頻率 | 16MHz |
32-bit | bit | 8-bit |
264kB | SRAM | 2.5kB |
2MB (RP2040 本身無 flash) | Flash | 32kB |
UARTx2 USB host 1.1 SPI Timer RTC | 外設 | UARTx1 SPI Timer |
有雙核心、32bit、記憶體 264kB、2MB 的 flash memory,對比 arduino nano 實在好太多,甚至贏過部分的 STM32。價格也相當便宜,100 多台幣就能入手。除了 C/C++ 之外還支援 MircoPython,甚至提供 debug 功能可以設定斷點透過 gdb 觀察記憶體或變數狀態。
雖然 pico 跟 Raspberry Pi 有點像,但是在功能上還是有差別。像是 Raspberry Pi 的 CPU 是用 SoC(system on chip),這個晶片會去集成很多常見的功能,例如影片編解碼、USB、乙太網路、藍芽等等。由於功能比較複雜,通常這類型的晶片會直接搭配作業系統使用,並且提供一些 GPIO 的介面給開發者操作。
而 pico 這類型的板子,通常功能會比較少一些,也不會有作業系統在裡面,開發者可以直接撰寫需要的功能,而不用透過作業系統來分配任務。
補充:目前也有一些專門針對嵌入式應用的作業系統,例如 RTOS。
另外一個讓我驚豔的功能是 PIO,Programmable I/O。不過在繼續介紹之前,我們先來談談 GPIO 以及 PIO 嘗試解決的問題是什麼吧!
GPIO 全文為 General purpose input/output,在微控制器當中通常具有控制引腳輸出或輸入的功能,可以透過程式控制某一腳位的輸出為高電位或低電位。
一個最簡單的例子可以用 LED 燈來舉例,假設今天想要實作 LED 閃爍功能,我們可以將 LED 的一個接腳接地之後,另一接腳接到 GPIO 腳位,並透過程式控制輸出的電位高低,這樣就可以做到閃爍效果。除了控制 LED 閃爍之外,GPIO 也會被用來當作資料傳輸使用,例如 I2C 或是 UART。
為了讓微控制器能夠與外部設備溝通,通常微控制器裡頭也會內建一些常見的傳輸協定,例如 Arduino 就有支援 UART;如果使用 Pro Micro 的話,裡頭的 AVR chip ATmega32U4 還有內建 USB 功能可以直接使用。
但是缺點是,如果微控制器沒有內建這些傳輸協定功能,開發者就需要自行購買對應的 IC 來實作,不然就是透過 GPIO 引腳自行實作通訊協定。這個概念有點像是硬體解碼跟軟體解碼的差別。
舉例來說,在 Arduino 當中我們可以使用 SoftwareSerial 在軟體層做到 UART 協定,我在去年撰寫的 Arduino 二氧化碳感測器實作1(https://blog.kalan.dev/2020-07-24-arduino-esp32-co2-sensor-2/)當中就有使用到:
// https://github.com/kjj6198/MH-Z14A-arduino/blob/master/co2.ino#L14
...
SoftwareSerial co2Serial(3, 4); // RX, TX
co2Serial.write(commands, 9); // send command
co2Serial.readBytes(response, 9);
SoftwareSerial 的實作背後就是使用 GPIO 引腳來實作 UART 協定。使用 SoftwareSerial 的好處在於可以讓Arduino 原生的 UART 與電腦溝通方便 debug,並透過 software serial 讓 arduino 與其他外部設備溝通。
在硬體的資料傳輸中相當仰賴 timing,甚至需要精準到算 CPU 的 cycle 才能避免錯誤。在 SoftwareSerial 的實作中:
void SoftwareSerial::begin(long speed)
{
// 略
// Precalculate the various delays, in number of 4-cycle delays
uint16_t bit_delay = (F_CPU / speed) / 4;
// 12 (gcc 4.8.2) or 13 (gcc 4.3.2) cycles from start bit to first bit,
// 15 (gcc 4.8.2) or 16 (gcc 4.3.2) cycles between bits,
// 12 (gcc 4.8.2) or 14 (gcc 4.3.2) cycles from last bit to stop bit
// These are all close enough to just use 15 cycles, since the inter-bit
// timings are the most critical (deviations stack 8 times)
_tx_delay = subtract_cap(bit_delay, 15 / 4);
// Only setup rx when we have a valid PCINT for this pin
if (digitalPinToPCICR((int8_t)_receivePin)) {
#if GCC_VERSION > 40800
// Timings counted from gcc 4.8.2 output. This works up to 115200 on
// 16Mhz and 57600 on 8Mhz.
//
// When the start bit occurs, there are 3 or 4 cycles before the
// interrupt flag is set, 4 cycles before the PC is set to the right
// interrupt vector address and the old PC is pushed on the stack,
// and then 75 cycles of instructions (including the RJMP in the
// ISR vector table) until the first delay. After the delay, there
// are 17 more cycles until the pin value is read (excluding the
// delay in the loop).
// We want to have a total delay of 1.5 bit time. Inside the loop,
// we already wait for 1 bit time - 23 cycles, so here we wait for
// 0.5 bit time - (71 + 18 - 22) cycles.
_rx_delay_centering = subtract_cap(bit_delay / 2, (4 + 4 + 75 + 17 - 23) / 4);
// There are 23 cycles in each loop iteration (excluding the delay)
_rx_delay_intrabit = subtract_cap(bit_delay, 23 / 4);
// There are 37 cycles from the last bit read to the start of
// stopbit delay and 11 cycles from the delay until the interrupt
// mask is enabled again (which _must_ happen during the stopbit).
// This delay aims at 3/4 of a bit time, meaning the end of the
// delay will be at 1/4th of the stopbit. This allows some extra
// time for ISR cleanup, which makes 115200 baud at 16Mhz work more
// reliably
_rx_delay_stopbit = subtract_cap(bit_delay * 3 / 4, (37 + 11) / 4);
#else // Timings counted from gcc 4.3.2 output
// Note that this code is a _lot_ slower, mostly due to bad register
// allocation choices of gcc. This works up to 57600 on 16Mhz and
// 38400 on 8Mhz.
_rx_delay_centering = subtract_cap(bit_delay / 2, (4 + 4 + 97 + 29 - 11) / 4);
_rx_delay_intrabit = subtract_cap(bit_delay, 11 / 4);
_rx_delay_stopbit = subtract_cap(bit_delay * 3 / 4, (44 + 17) / 4);
#endif
...
tunedDelay(_tx_delay); // if we were low this establishes the end
}
...
}
程式碼不多,但為了計算正確的 timing,甚至還計算了每個 gcc 版本會需要花上的 CPU cycle 數,扣掉之後才做 delay,可見 timing 對資料傳輸的重要性。雖然也可以改用 timer 以及中斷機制來實作,然而硬體的 timer 數量也是有限的。
能夠用程式碼實作出資料通訊協定很方便,但壞處在於這樣的溝通非常吃處理器的資源,當溝通頻率越高,處理器就要花更多資源在處理 timing 的計算上。因此如果需要精確時間的輸出,或是要避免處理器耗費太多資源在通訊協定上,就可以使用 PIO 來幫助達成。
我們剛剛有提到,問題出在於通訊協定所要求的 timing 需要耗費處理器的資源,PIO 能夠在不消耗處理器資源的前提下用最高與處理器同樣的頻率(133MHz)達成要求。我們可以將 PIO 想像成在 GPIO 當中又有一個小處理器,這個小處理器不會佔用主處理器的資源,專門設計給 GPIO 使用,同時又可以搭配 FIFO 跟 IRQ 與主處理器溝通。
一個 RP2040 裡頭有兩個 PIO blocks,一個 block 裡頭有 4 個 state machine。每個 state machine 都可以透過程式重新設定,在動態時期實作不同的通訊介面。
PIO 提供了一個簡易版的組合語言,總共只有 9 個指令、兩個暫存器,最多只能執行 32 個 instruction。雖然看起來很精簡,但這樣子的功能已經可以滿足大部分的通訊協定需求。
(圖片取自 RP2040 資料表)
從這個圖片可以看出四個 State machine 會共享同一份程式碼,而且 instruction memory 具有四個 read ports,所以每個 state machine 都可以同時存取程式碼而不會造成 blocking。
每一個 PIO block 裡頭都會有四個 state machine,會共享同一個 program memory,不過每個 state machine 都可以針對不同的 GPIO 腳位作設定,例如今天實作了 UART,4 個 state machine 可以讓我們設定最多四個完全獨立的 UART。
State Machine 由以下幾個部分構成:
IO mapping 比其他微控制器來得複雜一些,剛開始會覺得有點繞,一旦理解了之後會覺得這樣設計相當有道理。每個 IO 可以有四個狀態:input、output、set、sideset。
digitalRead
)digitalWrite
)其中 set 與 sideset 可能會是比較難理解的地方,這點我們等下會再深入討論。同一個 GPIO 可以同時有複數個狀態,例如我可以同時設定一個 GPIO 為 input,同時也設定為 output。
每個 IO mapping 的設定方式可以透過 base pin 以及 pin count 達成。例如我想要將 GPIO0、GPIO1 設為 SET,可以將 base pin 設為 GPIO0,count 為 2。從這邊可以知道每個狀態的腳位都會是連續的,也就是說不會有 OUTPUT 腳位是 GPIO0、GPIO3、GPIO5 的情況發生。
INPUT 與 OUTPUT 最多可以支援 32 個腳位,雖然在 pico 上只有 30 個腳位。set 與 sideset 最大只支援 5 個腳位。
總結來說,IO mapping 有幾個特色:
可以透過 IRQ flags 來觸發 interrupt 或是同步 state machine 之間的狀態。
PIO 提供了簡單卻強大的組合語言使用,總共只有 9 個指令,分別為:
基本上撰寫方式與一般組合語言相同,語法上就不多加介紹,不過在 PIO 組合語言當中有幾個變數需要先記起來:
input
,1 為 output
有暫存器也有 jmp,算是達成了圖靈完備的基本要件,理論上可以用 PIO 來做加減乘除運算,不過 PIO 的設計本來就不是拿來做運算的,可以當作實驗來玩。
搭配 PIO 的功能,Raspberry pi pico 會是一個非常好玩的開發板,例如有開發者透過 PIO 的功能實作出 HDMI 介面,或是寫出 UART 的通訊界面等等。
Rasbperry Pi pico 有 debug 介面,只要搭配 debugger,就可以直接在板子上設定斷點等等,開發起來更加方便!