David Lin

David Lin

一個軟體工程師的隨意筆記

25 Jun 2021

Raspberry Pi Pico - Happy Pride Month!

每年六月是彩虹驕傲月,很多公司都紛紛把 Logo 換成六色彩虹旗大放異彩(?)。 我就用 Raspberry Pi Pico + Pimoroni Pico Explorer Base 跟風一下好了 XD

IMG_5188

P.S. 這個文章在6月開始寫,但是拖到7月初才發表 QQ

Code

在這裡我使用 Pico 的 C++ SDK 來實作,使用 CMake 編譯。 它的設定還蠻好上手的,只要熟悉 CMake 操作基本上就會做。 詳情可以參考 Github 上的 pico-sdk 看看要怎麼設定環境。

它的主程式就像一般的 C 語言程式那樣以 main 為進入點:

#include <pico/stdio.h>

int main()
{
    stdio_init_all();
    // do something
    return 0;
}

所以,只要 include 一些 headers 去呼叫一些 API 函數, 就可以控制 Pico 的行為並做一些想要的功能了。

SPI

SPI 是一種很常用的 serial bit-bang 通訊方式,常見於 Arduino 與其他模組溝通。 它定義了兩種通訊的 role:master device 與 slave device, 且定義了四個訊號:

  1. SCLK: Serial Clock (output from master)
  2. MOSI: Master Out Slave In (data output from master)
  3. MISO: Master In Slave Out (data output from slave)
  4. CS / SS: Chip/Slave Select (often active low, output from master to indicate that data is being sent)

由於篇幅,在此不贅述,有興趣可以去以下網站暸解一下 SPI:

根據 Pico 的 API Documentation, RP2040 有內建兩個硬體 SPI 控制器,分別定義為 spi0spi1, 而且 SPI 的操作也已經包好函數了,所以不需要再造輪子。

在這裡會用到的函數只有如下兩個:

  • spi_init
  • spi_write_blocking

要使用硬體 SPI 控制,就需要把 SPI 訊號 pins 接到適合的 Pico 接腳,並不是能隨便接的,需要參考下面的 pinout 圖去連接 SPI 接腳才能對應到 spi0spi1 硬體控制器。

pico-pinout (圖片來源:raspberrypi.org)

當然,因為 Pimoroni Pico Explorer Base 已經幫你把 LCD 接上去了,所以不用自己傷腦筋接腳問題。詳細的接腳對應表格可以看一下 Explorer Base 的背面:

IMG_5190

ST7789 LCD

Pimoroni Pico Explorer Base 內建的 TFT LCD 螢幕的控制晶片是 ST7789, 所以需要查一下 ST7789 晶片的 datasheet,了解一下要怎麼傳輸資料到螢幕上。

1. SPI 通訊協定

它的 SPI 通訊介面有四個 pins:

  1. DC: 用來告訴 device 說傳輸的訊號類型,0 代表 command,1 代表 data。
  2. CS: 用來表示訊號的狀態,平常不傳輸時候為 1,傳輸期間則是 0
  3. SCK: 即 Serial Clock,用來同步訊號傳輸。
  4. MOSI: 即 Master Out Slave In,用來傳送內容,且方向是從 pico 傳到 device 去。

由於裝置是螢幕,所以不需要額外拉 MISO pin(雖然 ST7789 有提供讀取資料的 command)。 而且還多了 DC 訊號 pin 用來告訴 device 說 MOSI 的訊號是屬於 data 還是 command。

其中只有 SCKMOSI 是 Pico 的硬體 SPI 之接腳,包在 spi0 裡面了。 其餘 DCCS 是以 GPIO (Output) 定義的。

傳輸時候可以分成四個步驟

  1. 開始傳輸
    • CS = 0
  2. 指令模式
    • DC = 0
    • 傳送 1 byte 的 command value
  3. 資料模式(如果沒有要傳資料,應跳過)
    • DC = 1
    • 傳送 n bytes 的 data
  4. 結束傳輸
    • CS = 1

寫成 C 語言函數如下:

static void st7789_cmd(uint8_t cmd, const void* data, size_t nbytes)
{
    // begin transmission
    gpio_put(SPI_CS_PIN, 0);

    // command mode
    gpio_put(SPI_DC_PIN, 0);
    spi_write_blocking(spi0, &cmd, 1);

    // data mode
    if (data && nbytes) {
        gpio_put(SPI_DC_PIN, 1);
        spi_write_blocking(spi0, (const uint8_t*)data, nbytes);
    }

    // end transmission
    gpio_put(SPI_CS_PIN, 1);
}

由於我使用 pico 的硬體 SPI controller,所以 SCK 與 MOSI 接腳基本上已經包在 spi0 handle 了。

2. 初始化 (SPI)

在系統初始化時候,需要先定義好要使用的 GPIO 與 SPI 接腳:

#define SPI_MOSI_PIN        19 // PICO_DEFAULT_SPI_TX_PIN
#define SPI_MISO_PIN        16 // PICO_DEFAULT_SPI_RX_PIN
#define SPI_SCK_PIN         18 // PICO_DEFAULT_SPI_SCK_PIN
#define SPI_DC_PIN          SPI_MISO_PIN
#define SPI_CS_PIN          17 // PICO_DEFAULT_SPI_CSN_PIN
#define SPI_BAUD            16000000

spi_init(spi0, SPI_BAUD);
gpio_set_function(SPI_DC_PIN, GPIO_FUNC_SIO);
gpio_set_function(SPI_CS_PIN, GPIO_FUNC_SIO);
gpio_set_function(SPI_SCK_PIN,  GPIO_FUNC_SPI);
gpio_set_function(SPI_MOSI_PIN, GPIO_FUNC_SPI);
gpio_set_dir(SPI_DC_PIN, GPIO_OUT);
gpio_set_dir(SPI_CS_PIN, GPIO_OUT);
  1. SCKMOSI 都在 spi0 定義的接腳上,所以定義為 GPIO_FUNC_SPI
  2. DC 借用了 MISO 接腳,所以定義為 GPIO_FUNC_SIO
  3. CS 雖然在 spi0 定義接腳上,但是目前程式碼是當 GPIO 控制,所以定義為 GPIO_FUNC_SIO

3. 初始化 (TFT Device)

初始化 pins 之後,接下來就是把螢幕開機了,要跑完以下程序才能開始傳輸與顯示影像:

  1. Software reset
  2. Set display mode (color, inversion, ram access)
  3. Turn on display

除了看 datasheet 上的定義以外,還需要動手做實驗驗證看看初始化結果是否符合預期, 總是會有意想不到的狀況發生(請見程式碼註解)。

// Reset the device
st7789_cmd(ST7789_SWRESET, NULL, 0);
sleep_ms(150);

// Set color mode to 16-bit per pixel
//   這個螢幕本來就可以設定成 16-bit or 18-bit color mode
//   為了方便我設定成 16-bit (R5G6B6)
st7789_cmd(ST7789_COLMOD, "\x55", 1);

// Set inversion mode
//   根據 datasheet,這個應該要把螢幕的顏色亮度反轉(負片模式)
//   但是實際上,不知道為什麼這螢幕一開始就是負片模式,
//   要打開 inversion mode 才能回到正常顏色 Orz
//
//   P.S. 我有把螢幕的 ram 清成 0 ,驗證這個問題
st7789_cmd(ST7789_INVON, NULL, 0);

// Set memory access control (direction)
//   這個是用來設定螢幕內部的 ram 的存取方式,
//   預設 (0) 為:
//     a. 從左上角為起點,從左往右,從上往下掃描
//     b. 每一個 pixel 以 RGB 排列
st7789_cmd(ST7789_MADCTL, &madctl, 1);

// Set column range
//   代表在 ram 上 x 軸的範圍
uint8_t caset[4] = {
    0, 0,
    (((width-1) >> 8) & 0xFF),
    ((width-1) & 0xFF)
};
st7789_cmd(ST7789_CASET, caset, 4);

// Set row range
//   代表在 ram 上 y 軸的範圍
uint8_t raset[4] = {
    0, 0,
    (((height-1) >> 8) & 0xFF),
    ((height-1) & 0xFF)
};
st7789_cmd(ST7789_RASET, raset, 4);

// Wake up the device
//   叫醒沈睡的獅子..XD
st7789_cmd(ST7789_SLPOUT, NULL, 0);

// Turn on display
//   打開螢幕
st7789_cmd(ST7789_DISPON, NULL, 0);

4. 傳輸影像資料

設定完成螢幕之後,接下來就可以傳輸資料了。

首先宣告一下 Pico 的 framebuffer:

#define TFT_WIDTH       240
#define TFT_HEIGHT      240
#define TFT_BPP         2

#define FRAMEBUFFER_SIZE (TFT_BPP * TFT_WIDTH * TFT_HEIGHT)
static uint16_t framebuffer[TFT_WIDTH * TFT_HEIGHT];

假設 framebuffer 資料已經填充好了,要傳輸到螢幕上:

size_t nbytes = FRAMEBUFFER_SIZE;

// Swap endian
//   因為 ST7789 的資料是以 big endian 組織,
//   而 RP2040 是用 little endian,所以傳輸前需要先轉換一下
for (size_t i = 0, n = nbytes >> 1; i < n; ++i) {
    uint16_t* p = framebuffer + i;
    *p = __builtin_bswap16(*p);
}

// Wrtie to frame memory
//   夠簡單吧,只要把資料透過 SPI 傳送到螢幕就可以了
st7789_cmd(ST7789_RAMWR, framebuffer, nbytes);

成果

編譯上傳到 Raspberry Pi Pico 之後,就可以得到一個彩虹旗動畫(很粗糙 XD)

參考資料

comments powered by Disqus