iT邦幫忙

2023 iThome 鐵人賽

DAY 27
0

樹莓派基金會在 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 觀察記憶體或變數狀態。

Raspberry Pi pico 跟 Raspberry Pi 的區別是什麼

雖然 pico 跟 Raspberry Pi 有點像,但是在功能上還是有差別。像是 Raspberry Pi 的 CPU 是用 SoC(system on chip),這個晶片會去集成很多常見的功能,例如影片編解碼、USB、乙太網路、藍芽等等。由於功能比較複雜,通常這類型的晶片會直接搭配作業系統使用,並且提供一些 GPIO 的介面給開發者操作。

而 pico 這類型的板子,通常功能會比較少一些,也不會有作業系統在裡面,開發者可以直接撰寫需要的功能,而不用透過作業系統來分配任務。

補充:目前也有一些專門針對嵌入式應用的作業系統,例如 RTOS。

GPIO 是什麼?

另外一個讓我驚豔的功能是 PIO,Programmable I/O。不過在繼續介紹之前,我們先來談談 GPIO 以及 PIO 嘗試解決的問題是什麼吧!

GPIO 全文為 General purpose input/output,在微控制器當中通常具有控制引腳輸出或輸入的功能,可以透過程式控制某一腳位的輸出為高電位或低電位。

一個最簡單的例子可以用 LED 燈來舉例,假設今天想要實作 LED 閃爍功能,我們可以將 LED 的一個接腳接地之後,另一接腳接到 GPIO 腳位,並透過程式控制輸出的電位高低,這樣就可以做到閃爍效果。除了控制 LED 閃爍之外,GPIO 也會被用來當作資料傳輸使用,例如 I2C 或是 UART。

Peripherals

為了讓微控制器能夠與外部設備溝通,通常微控制器裡頭也會內建一些常見的傳輸協定,例如 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 與其他外部設備溝通。

Arduino UART

資料傳輸仰賴精確的時間控制

在硬體的資料傳輸中相當仰賴 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 數量也是有限的。

Bit Banging

能夠用程式碼實作出資料通訊協定很方便,但壞處在於這樣的溝通非常吃處理器的資源,當溝通頻率越高,處理器就要花更多資源在處理 timing 的計算上。因此如果需要精確時間的輸出,或是要避免處理器耗費太多資源在通訊協定上,就可以使用 PIO 來幫助達成。

PIO(Programmable GPIO)

簡介

我們剛剛有提到,問題出在於通訊協定所要求的 timing 需要耗費處理器的資源,PIO 能夠在不消耗處理器資源的前提下用最高與處理器同樣的頻率(133MHz)達成要求。我們可以將 PIO 想像成在 GPIO 當中又有一個小處理器,這個小處理器不會佔用主處理器的資源,專門設計給 GPIO 使用,同時又可以搭配 FIFO 跟 IRQ 與主處理器溝通。

一個 RP2040 裡頭有兩個 PIO blocks,一個 block 裡頭有 4 個 state machine。每個 state machine 都可以透過程式重新設定,在動態時期實作不同的通訊介面。

PIO 提供了一個簡易版的組合語言,總共只有 9 個指令、兩個暫存器,最多只能執行 32 個 instruction。雖然看起來很精簡,但這樣子的功能已經可以滿足大部分的通訊協定需求。

PIO 架構圖

(圖片取自 RP2040 資料表)

從這個圖片可以看出四個 State machine 會共享同一份程式碼,而且 instruction memory 具有四個 read ports,所以每個 state machine 都可以同時存取程式碼而不會造成 blocking。

State Machine 介紹

每一個 PIO block 裡頭都會有四個 state machine,會共享同一個 program memory,不過每個 state machine 都可以針對不同的 GPIO 腳位作設定,例如今天實作了 UART,4 個 state machine 可以讓我們設定最多四個完全獨立的 UART。

State Machine 由以下幾個部分構成:

  • OSR(Output shift register):32bit,可以從主處理器當中透過 FIFO 傳入資料
  • ISR(Input Shift Register):32bit,可以將資料透過 FIFO 傳給主處理器
  • X、Y 暫存器:每個 state machine 有兩個通用暫存器
  • PC:program counter
  • clock divider:每個 state machine 最高可以到主處理器的頻率,對大部分的通訊協定來說太快了,可透過 clock divider 調整頻率。(範圍從 1 ~ 65536)
  • 程式碼

arduino2.drawio

IO mapping

IO mapping 比其他微控制器來得複雜一些,剛開始會覺得有點繞,一旦理解了之後會覺得這樣設計相當有道理。每個 IO 可以有四個狀態:input、output、set、sideset。

  • input:可以讀取外部感測器、外部設備的資料(類似 arduino 中的 digitalRead
  • ouptut:可以由程式控制電位高低(類似 arduino 中的 digitalWrite
  • set:可以設定腳位的電位高低(跟 output 有點像,但有些差異)
  • sideset:可以在執行指令的同時改變其他腳位的電位或方向

其中 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 個腳位。

pico-io-mapping

總結來說,IO mapping 有幾個特色:

  • 同一個腳位可以同時具備複數個狀態,例如同時是 set 又是 output
  • input、output 最大可支援 32 個腳位;set 與 sideset 最多支援 5 個腳位
  • 腳位必須連續,例如從 GPIO0 ~ GPIO3

IRQ(Interrupt Request)

可以透過 IRQ flags 來觸發 interrupt 或是同步 state machine 之間的狀態。

PIO 組合語言介紹

PIO 提供了簡單卻強大的組合語言使用,總共只有 9 個指令,分別為:

  • SET
  • IN
  • OUT
  • PULL
  • PUSH
  • JMP
  • WAIT
  • MOV
  • IRQ

基本上撰寫方式與一般組合語言相同,語法上就不多加介紹,不過在 PIO 組合語言當中有幾個變數需要先記起來:

  • pins:代表此 PIO 選取到的腳位。例如我從 GPIO0 開始,則 pin0 就是 GPIO0;如果從 GPIO2 開始,那麼 pin0 就是 GPIO2
  • pindirs:設定腳位的方向。0 為 input,1 為 output
  • X、Y:暫存器
  • osr:output shift register
  • isr:input shift register
  • data:可以 immediate 最多 5 bit,也就是 0 ~ 32

有暫存器也有 jmp,算是達成了圖靈完備的基本要件,理論上可以用 PIO 來做加減乘除運算,不過 PIO 的設計本來就不是拿來做運算的,可以當作實驗來玩。

總結

搭配 PIO 的功能,Raspberry pi pico 會是一個非常好玩的開發板,例如有開發者透過 PIO 的功能實作出 HDMI 介面,或是寫出 UART 的通訊界面等等。

Rasbperry Pi pico 有 debug 介面,只要搭配 debugger,就可以直接在板子上設定斷點等等,開發起來更加方便!


上一篇
[Day26] Coinhive 與無限 alert 事件
下一篇
[Day28] 密碼學、hash 與編碼
系列文
從電子元件到傅立葉轉換 - 那些我有興趣的主題30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言