David Lin

David Lin

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

10 Jul 2021

Futaba VFD - Bad Apple (Part 3)

系列索引

  1. Part 1
  2. Part 2
  3. Part 3

前言

一直想拿 Futaba VFD 去播放 Bad Apple ,當時 Arduino 的記憶體太小裝不下大約 8MB 大小之處理過後的影片,所以改用 Raspberry Pi 4B 去實現了。

與以往 VFD 文章不同點:

  1. 使用 3.3V Logic,而不是 Arduino Nano (ATmega328) 的 5V Logic
  2. 在 Linux 下跑 Python 與 OpenCV 去再生影像與控制 VFD
  3. 同時使用3個 VFD,且減少連接線路

連接 VFD

因為有三個 VFD 模組要同時接,而且每一個模組有七個接腳(包含 VCC, EN, GND),所以我參考了 SPI 的作法設計了以下接法以節省 GPIO 接腳:

wiring

說明:

  • GND:接樹莓派的 GND pin
  • VCC:接樹莓派的 5V pin
  • EN:實際上就是 VDD,所以接樹莓派的 3.3V pin
  • RST:選一個 GPIO pin 就好,用來重設 VFD 模組狀態
  • CLK:對應 SPI 之 SCLK
  • SDI:對應 SPI 的 MOSI
  • 每一個 VFD 模組的 CS (Chip Select) Pin 都是彼此獨立控制,不共用 GPIO 接腳

P.S. 我懶著用 Fritzing 畫,用筆畫比較快 XD 但是爆墨水很嚴重,需要換一支工程繪圖筆。

用 Python 控制 GPIO

這裡我使用 Raspberry Pi 官方推薦的 Python 套件 gpiozero 來控制 GPIO,它的好處是可以不用以 root 身份去執行程式,會比較安全些。 在官方的 Raspberry Pi OS 已經有預裝了 python3-gpiozero,所以不需要自己用 pip 下載安裝。

開始匯入一些 modules:

import gpiozero as gz
from time import sleep

然後初始化一些 GPIO pins:

VFD_RST_PIN     = 5
VFD_MOSI_PIN    = 20
VFD_SCLK_PIN    = 21
VFD_CS0_PIN     = 18
VFD_CS1_PIN     = 17
VFD_CS2_PIN     = 16

VFD_RST     = gz.DigitalOutputDevice(pin = VFD_RST_PIN, active_high = False)
VFD_MOSI    = gz.DigitalOutputDevice(pin = VFD_MOSI_PIN)
VFD_SCLK    = gz.DigitalOutputDevice(pin = VFD_SCLK_PIN, active_high = False)
VFD_CS0     = gz.DigitalOutputDevice(pin = VFD_CS0_PIN, active_high = False)
VFD_CS1     = gz.DigitalOutputDevice(pin = VFD_CS1_PIN, active_high = False)
VFD_CS2     = gz.DigitalOutputDevice(pin = VFD_CS2_PIN, active_high = False)

在這裡 RST, SCLK, CS# 都是反轉的訊號,所以初始化用 active_high = False

啟動與初始化

啟動程序第一階段就是用 RST 訊號重設所有 VFD modules:

def vfd_reset():
    VFD_RST.on()
    sleep(0.1)
    VFD_RST.off()
    sleep(0.1)

# called in main()
vfd_reset()

然後啟動第二階段就是分別設定 VFD 模組的 config:

def vfd_init(cs):
    # set display timing
    vfd_send_bytes(cs, 0xE0, [0x07])
    # set diming data
    vfd_send_bytes(cs, 0xE4, [0xFF])

# called in main()
vfd_init(VFD_CS0)
vfd_init(VFD_CS1)
vfd_init(VFD_CS2)

訊號傳輸

在剛剛的啟動第二階段程序中,可以看到我還定義了 vfd_send_bytes, 用來傳送指令與資料給 VFD 模組。

def _vfd_transfer_bit(x):
    VFD_SCLK.on()
    if x == 0:
        VFD_MOSI.off()
    else:
        VFD_MOSI.on()
    VFD_SCLK.off()

def _vfd_transfer_byte(x):
    for _ in range(8):
        _vfd_transfer_bit(x & 0x01)
        x = x >> 1

def vfd_send_bytes(cs, cmd, data):
    cs.on()
    _vfd_transfer_byte(cmd)
    for x in data:
        _vfd_transfer_byte(x)
    cs.off()

vfd_send_bytes 基本上是一個 wrapper function, 控制了 CS 訊號來決定是哪一個 VFD 模組要傳輸, 然後傳輸的訊號分成 cmddata,分別用同一個管道做傳輸。 此外,我是使用 GPIO 而不是硬體 SPI (spidev),所以需要自己模擬 SPI 傳輸。

顯示 ASCII 文字

要顯示文字,需要分別做以下步驟:

  1. vfd_wrtie_dcram: 寫入 DCRAM 去指定要顯示哪些字元
  2. vfd_show: 點亮螢幕
def vfd_write_dcram(cs, addr, data):
    cmd = 0x20 | (addr & 0x1F)
    vfd_send_bytes(cs, cmd, data)

def vfd_show(cs):
    vfd_send_bytes(cs, 0xE8, [])

def vfd_text(cs, s):
    vfd_write_dcram(cs, 0, s)

測試

main() 裡面顯示文字,測試看看能不能正常顯示:

vfd_text(VFD_CS0, b"HELLO")
vfd_show(VFD_CS0)
vfd_text(VFD_CS1, b"WORLD")
vfd_show(VFD_CS1)
vfd_text(VFD_CS2, b"RASPI")
vfd_show(VFD_CS2)

結果如下:

hello

hello-wiring

看來 VFD 模組已經可以運作了~

用 OpenCV 讀取與轉換影片

既然都在樹莓派 + Linux 上了,所以就用 OpenCV 去弄比較方便些。(懶)

首先匯入一下 OpenCV 與 NumPy

import cv2
import numpy as np

然後準備一下常數,方便做後面的計算:

# VFD 每一個字元都是 5x7 點陣
# 而每一個模組有八個字元
VFD_CHAR_W = 5
VFD_CHAR_H = 7
VFD_COLS = 8
VFD_ROWS = 3

# 定義縮圖後的大小
IMG_W = VFD_COLS * VFD_CHAR_W
IMG_H = VFD_ROWS * VFD_CHAR_H

然後在 main() 裡面加入 OpenCV 的主迴圈, 而關於 OpenCV 與 NumPy 的函數用法可以在參考資料裡面找。

# Open the video
cap = cv2.VideoCapture("bad_apple.mp4")
# Play the video
while cv2.waitKey(1) != 27:
    # Read a frame
    retval, frame = cap.read()
    if not retval:
        break
    # Display the frame
    cv2.imshow("Bad Apple - Original", frame)
    # Process the frame
    image = process_frame(frame)
    show_processed_image("Bad Apple - Processed", image)
    # Send data to the VFD
    for i, row in generate_rows(image):
        display_row(i, row)

主迴圈基本上有三個步驟:

  1. 擷取影像
  2. 處理影像(縮圖與後處理)
  3. 顯示到 VFD 模組上

處理影像

處理影像就只是做縮放與 thresholding:

def process_frame(frame):
    image = frame[:,:,0]
    image = cv2.resize(image, (IMG_W, IMG_H), interpolation = cv2.INTER_AREA)
    retval, image = cv2.threshold(image, 128, 1, 0)
    return image

轉換影像成 bitmap 區塊

然後在顯示 VFD 之前,我會先把處理後的影像分割成 3 個 rows:

def generate_rows(image):
    for i in range(VFD_ROWS):
        ys = i * VFD_CHAR_H
        ye = ys + VFD_CHAR_H
        yield (i, image[ys:ye,:])

最後把每一個 row 做轉換,送到所對應的 VFD 模組去:

def display_row(i, row):
    # 因為 row 是 7x40,需要在下面加一個 1x40 zeros,湊足 8x40
    pad = np.zeros(row.shape[1], dtype=np.uint8)
    row = np.vstack((row, pad))
    # 因為 np.packbits 是 big-endian,所以需要先把 row 上下翻轉
    np.flipud(row)
    # 把每一組 row[0:8,j] 編碼成一個 byte,得到 1x40 bytes
    data = np.packbits(row, 0)
    # 把 data 的 shape 從 (1,40) 轉成 (40,)
    data = data.flatten()
    # 選擇對應的 VFD 模組
    cs = VFD_CSs[i]
    # 寫入到 CGRAM
    vfd_write_cgram(cs, 0, data)
    # 寫入到 DCRAM
    vfd_write_dcram(cs, 0, [0,1,2,3,4,5,6,7])
    # 打開螢幕
    vfd_show(cs)

把 bitmap 寫入 CGRAM

在這裡,可以把自訂 bitmap 寫入到 CGRAM 去,最多八個自訂字元:

def vfd_write_cgram(cs, addr, data):
    cmd = 0x40 | (addr & 0x07)
    vfd_send_bytes(cs, cmd, data)

然後透過寫入 DCRAM 去告訴 VFD 說要讀取哪些 bitmaps:

vfd_write_dcram(cs, 0, [0,1,2,3,4,5,6,7])

CGRAM 與 CGROM 都在同一個位址空間,只是不同範圍而已:

  • CGRAM: 0x00 ~ 0x07
  • CGROM: 0x08 ~ 0xFF,其中 0x20 ~ 0x7F 是 ASCII 字元的 bitmaps。

而 DCRAM 是用來記錄要顯示對應的 CGRAM/CGROM 的位址,從而讀取對應的 bitmaps。 所以,可以在 CGRAM 上塞一些自訂的 bitmap, 然後把 DCRAM 設定成讀取 0x00 ~ 0x07 就可以對應到 CGRAM 了。

至於 CGRAM 的寫入訊號格式,可以參考下圖:

cgram-write

可以看到每一個 bitmap 是以 column 為單位傳輸的,所以我才會把每一個 row 縱向打包成一個 byte array。

寫入完成 CGRAM 與 DCRAM 之後,不要忘記打開螢幕喔:

vfd_show(cs)

結果

成功在 VFD 顯示器上播放 Bad Apple 了~~~ 不枉費我花一週在查資料與測試了。 程式碼在 github 上。

result

result

雜談

因為 Raspberry Pi 的 GPIO pins 沒有照順序排列, 每次連接 breakouts 都要核對一下圖表很麻煩,所以我就買了一個 HAT ,上頭把 GPIO 接腳都有標示清楚,比較便利些了。

gpio-hat

此外,Raspberry Pi 4B 有令人詬病的過熱問題,所以需要加裝風扇避免機器壞掉 T_T

參考資料

comments powered by Disqus