2021年2月11日 星期四

【Raspberry Pi Pico 開箱實作教學 #02】 OLED SSD1327 I2C MicroPython驅動程式開發與測試


在上一篇[1]已初步介紹如何安裝Raspberry Pi Pico (以下簡稱Pi Pico) MicroPython開發環境及基本數位輸出入(GPIO)、類比輸出入(ADC, PWM)用法。這一次就介紹如何自己寫一個OLED的驅動程式做為後續文字及圖形內容(GUI)顯示的介面。實驗用電路如Fig. 1所示。

 

Fig. 1 Pi Pico測試用電路板接線圖。(OmniXRI Feb. 2021設計/繪圖)(點擊圖放大)


大家會好奇為什麼要自己寫OLED驅動程式呢?因為目前Pi Pico不像Arduino,IDE就自帶各種I2C, SPI, UART, 8080介面的溫控、光電、運動、顯示等各種模組驅動程式及範例。而這些模組的供應商目前雖然有提供MicroPython驅動程式,但可能只有支援到Pyboard, ESP8266, ESP32, WiPy等這些常用的開發板,並不保證所有的驅動程式都能被Pi Pico直接使用。相信不久的將來這些模組供應商會很快提供相對應的驅動程式,所以想要用這些模組的朋友只能自己動手解決了。



目前Pi Pico官方MicroPython說明文件[2]及Github中僅提供非常基礎的通訊介面(I2C, SPI, UART)使用方式,對於市售模組僅提供SSD1306, SH1106兩款OLED及WS2812串列控制LED模組的參考程式[2b]。

本次實驗範例使用的是128x128像素,16灰階(4Bit),I2C介面,SSD1327驅動IC的OLED,雖然在PyPi上可以找到MicroPython[3]驅動程式,同一作者在Github上也有給出源碼[4],但初步測試後,確定無法直接用在Pi Pico上,於是只好參考這些源碼進行修改。

目前Pi Pico使用的MicroPython為V1.14版[5],至於是否所有指令都完全相容,則有待測試。本次實驗就發現MicroPython I2C好像不支援I2C.start() + I2C.write() + I2C.stop()用法,而只支援I2C.writeto(位置, 資料內容),所以相關程式就得修改,否則雖然可以執行,但OLED無法正確顯示。

接下來就依序解說OLED驅動原理及相關程式,本次先以文字型驅動為主,至於圖形介面則留待下一次說明。更多SSD1327硬體規格及驅動方式,可參考原廠技術文件說明[6]。不想理解細部驅動作法或只想直接使用的朋友,可跳過這段說明,直接看OLED測試程式說明即可。

【OLED驅動程式】
 

'''
ssda327_i2c.py by OmniXRI Jack, 11 Feb. 2021

OLED MicroPython Driver for Raspberry Pi Pico,
128x128 pixel, 4bit (16) Gray Level, I2C Interface.

https://github.com/OmniXRI/Pi_Pico_OLED_SSD1327_I2C
'''

from micropython import const # 從MicroPython中導入常數設定類別
import framebuf # 導入影格緩衝區類別

# SSD1327命令暫存器編號定義
SET_COL_ADDR          = const(0x15) # 設定行位址
SET_SCROLL_DEACTIVATE = const(0x2E) # 設定捲動不活動
SET_ROW_ADDR          = const(0x75) # 設定列位址
SET_CONTRAST          = const(0x81) # 設定顯示對比
SET_SEG_REMAP         = const(0xA0) # 設定顯示節段重新映射
SET_DISP_START_LINE   = const(0xA1) # 設定顯示起始列
SET_DISP_OFFSET       = const(0xA2) # 設定顯示偏移量
SET_DISP_MODE         = const(0xA4) # 設定顯示模式:一般0xA4, 全亮0xA5, 全滅0xA6, 反相0xA7
SET_MUX_RATIO         = const(0xA8) # 設定多重尋址
SET_FN_SELECT_A       = const(0xAB) # 設定功能選擇A
SET_DISP              = const(0xAE) # 設定顯示電源關閉0xAE,電源開啟0xAF
SET_PHASE_LEN         = const(0xB1) # 設定顯示節段波形長度
SET_DISP_CLK_DIV      = const(0xB3) # 設定顯示派波除數
SET_SECOND_PRECHARGE  = const(0xB6) # 設定第二段充電電壓
SET_GRAYSCALE_TABLE   = const(0xB8) # 設定灰階表
SET_GRAYSCALE_LINEAR  = const(0xB9) # 設定灰階線性度
SET_PRECHARGE         = const(0xBC) # 設定預充電電壓
SET_VCOM_DESEL        = const(0xBE) # 設定VCOM電壓
SET_FN_SELECT_B       = const(0xD5) # 設定功能選擇B
SET_COMMAND_LOCK      = const(0xFD) # 設定命令鎖定

# SSD1327指定命令/資料 (後六位為0)
REG_CMD  = const(0x80) # 指定為命令,Co=1, D/C#=0
REG_DATA = const(0x40) # 指定為資料,Co=0, D/C#=1

# SSD1327_I2C類別
class SSD1327_I2C:
    # 初始化函式,設定基本變數
    def __init__(self, i2c, width, height, addr=0x3C, external_VCC=False):
        self.i2c = i2c       # 指定I2C元件
        self.width = width   # 指定OLED顯示寬度
        self.height = height # 指定OLED顯示高度
        self.addr = addr     # 顯示I2C位置(預設0x3C)
        self.external_VCC = external_VCC # 指定外部供電(預設為否)
        self.buffer = bytearray(self.width * self.height // 2)  # 由於顯示像素為4bit(16灰階),所需記憶體數量為長x寬的一半
        self.framebuf = framebuf.FrameBuffer(self.buffer, self.width, self.height, framebuf.GS4_HMSB) # 配置所需記憶體,大小、長、寬及4bit顯示方式。
        self.temp = bytearray(2) # 配置給SSD1327命令緩衝區
        self.poweron()           # OLED電源開啟
        self.init_display()      # OLED進行初始化
    
    # 寫入命令函式
    def write_cmd(self, cmd):
        self.temp[0] = REG_CMD # Co=1, D/C#=0
        self.temp[1] = cmd     # 命令內容
        self.i2c.writeto(self.addr, self.temp) # 將命令寫到指定位址
        
    # 寫入資料函式
    # 不支援 I2C.start() + I2C.write() + I2C.stop()
    def write_data(self, buf):
        self.i2c.writeto(self.addr, b'\x40'+buf) # 指定寫入資料(REG_DATA 0x40)及寫入緩衝區內容
    
    # OLED初始化內容,依序執行命令完成初始化並清除OLED顯示區
    def init_display(self):
        for cmd in (
            SET_COMMAND_LOCK, 0x12, # 命令解鎖(Unlock)
            SET_DISP, # 關閉顯示(Display off)
            # 設定顯示解析度及排列(Resolution and layout)
            SET_DISP_START_LINE, 0x00, # 設定顯示啟始列(start line)
            SET_DISP_OFFSET, 0x0, # 設定顯示垂直偏移量(vertical offse)
            # 設定重新映射方式(Set re-map)
            # Enable column address re-map
            # Disable nibble re-map
            # Horizontal address increment
            # Enable COM re-map
            # Enable COM split odd even
            SET_SEG_REMAP, 0x51,
            SET_MUX_RATIO, self.height - 1,
            # 設定時序及驅動電路(Timing and driving scheme)
            SET_FN_SELECT_A, 0x01, # 致能內部供電(Enable internal VDD regulator)
            SET_PHASE_LEN, 0x51, # Phase 1: 1 DCLK, Phase 2: 5 DCLKs
            SET_DISP_CLK_DIV, 0x01, # 設定顯示脈波除數(Divide ratio: 1, Oscillator Frequency: 0)
            SET_PRECHARGE, 0x08, # 設定預充電電壓(Set pre-charge voltage level: VCOMH)
            SET_VCOM_DESEL, 0x07, # 設定VCOM電壓(Set VCOMH COM deselect voltage level: 0.86*Vcc)
            SET_SECOND_PRECHARGE, 0x01, # 設定第二段充電電壓(Second Pre-charge period: 1 DCLK)
            SET_FN_SELECT_B, 0x62, # 設定功能選擇B(Enable enternal VSL, Enable second precharge)
            # 設定顯示(Display)
            SET_GRAYSCALE_LINEAR, # 使用線性灰階對照表(Use linear greyscale lookup table)
            SET_CONTRAST, 0x7f, # 設定對比中間亮度(Medium brightness)
            SET_DISP_MODE, # 設定顯示模式一般不反相(Normal, not inverted)
            # 設定行列起始位置,以公式自動轉換96x96, 128x128像素OLED
            # 96x96:
            # SET_ROW_ADDR, 0 95,
            # SET_COL_ADDR, 8, 55,
            # 128x128:
            # SET_ROW_ADDR, 0 127,
            # SET_COL_ADDR, 0, 63,
            SET_ROW_ADDR, 0x00, self.height - 1,
            SET_COL_ADDR, ((128 - self.width) // 4), 63 - ((128 - self.width) // 4),

            SET_SCROLL_DEACTIVATE, # 設定捲動不啟動
            SET_DISP | 0x01): # 設定顯示開啟(Display on)
            self.write_cmd(cmd) # 依序寫入命令            
        self.fill(0) # 清除畫面(clear screen)
        self.show()  # 顯示內容

    # 關閉OLED電源
    def poweroff(self):
        self.write_cmd(SET_FN_SELECT_A)
        self.write_cmd(0x00) # Disable internal VDD regulator, to save power
        self.write_cmd(SET_DISP)

    # 關閉OLED電源
    def poweron(self):
        self.write_cmd(SET_FN_SELECT_A)
        self.write_cmd(0x01) # Enable internal VDD regulator
        self.write_cmd(SET_DISP | 0x01)
    
    # 顯示緩衝區內容    
    def show(self):
        self.write_cmd(SET_COL_ADDR) # 指定行啟始位置
        self.write_cmd((128 - self.width) // 4)
        self.write_cmd(63 - ((128 - self.width) // 4))
        self.write_cmd(SET_ROW_ADDR) # 指定列啟始位置
        self.write_cmd(0x00)
        self.write_cmd(self.height - 1)
        self.write_data(self.buffer) # 寫入緩衝區內容
        
    # 調整顯示對比
    def contrast(self, contrast):
        self.write_cmd(SET_CONTRAST)
        self.write_cmd(contrast) # 對比值 0-255

    # 顯示反相(Ture/False)
    def invert(self, invert): #
        self.write_cmd(SET_DISP_MODE | (invert & 1) << 1 | (invert & 1)) # 0xA4=Normal, 0xA7=Inverted

    # 將所有像素填入指定顏色,4bit灰階 (0 - 15)
    def fill(self, color):
        self.framebuf.fill(color)
     
    # 將特定位置像素填入指定顏色,4bit灰階 (0 - 15)   
    def pixel(self, x, y, color):
        self.framebuf.pixel(x, y, color)

    # 軟體顯示捲動
    def scroll(self, dx, dy):
        self.framebuf.scroll(dx, dy)        

    # 在指定位置顯示文字串及對應顏色,4bit灰階 (0 - 15),預設顏色15
    def text(self, string, x, y, color=15):
        self.framebuf.text(string, x, y, color)    

接下來就一用簡單程式來測試OLED ssd1327_i2c.py MicroPython驅動程式是否正常工作。在測試前需使用Thonny把ssd1327_i2c.py另存到裝置上(Save Copy / Raspberry Pi Pico)。若僅存在電腦上則會出現找不到模組的錯誤訊息ImportError: no module named 'ssd1327_i2c'。以下就對測試程式作更進一步說明。

測試程式啟動後會清除畫面,顯示標題字串"OLED-OmnixirJack",左右按鍵(Pb1, Pb2)狀態(on / off)及SVR1產生的類比信號(ADC0)電壓值。若有按鍵按下時對應的LED會亮起,同時OLED上按鍵對應的字串也會由"off"變成"on"。當調整SVR1時,亦會即時更新電壓值在OLED上。

在顯示內容前以fill(0)來清除顯示內容,再將欲顯示的內容繪在FrameBuf上,最後再調用show()來刷新顯示內容,如此可避免一直傳送資料給OLED,拖慢程式運作效能。

接下來就對範例程式進行完整說明。

【OLED測試程式】

'''
main.py by OmniXRI Jack, 11 Feb. 2021

OLED Test Sample

128x128 Pixel, 4bit (16) Gray Level, I2C Interface, SSD1327 Driver IC。

after system inital, OLED will show as blow, x.xxx is ADC input voltage.

OLE-OmnixriJack
L  Press : off
R  Press : off
ADC volt : x.xxx

When

Pb1 press, LED1 On, OLED display 'L Press : on'
Pb2 press, LED2 On, OLED display 'R Press : on'
SVR1 adjust, LED display 'ADC Volt : x.xxx'

https://github.com/OmniXRI/Pi_Pico_OLED_SSD1327_I2C
'''

from machine import I2C, Pin, ADC   # 從machine中導入I2C, Pin, ADC類別
from ssd1327_i2c import SSD1327_I2C # 從ssd1327_i2c中導入SSD1327_I2C類別

led1 = Pin(16, Pin.OUT) # 設定GP16為LED1輸出腳
led2 = Pin(17, Pin.OUT) # 設定GP17為LED2輸出腳
pb1  = Pin(18, Pin.IN, Pin.PULL_UP) # 設定GP18為PB1輸入腳,且自帶pull high電阻
pb2  = Pin(19, Pin.IN, Pin.PULL_UP) # 設定GP19為PB2輸入腳,且自帶pull high電阻

adc = ADC(0)       # 設定連接到ADC0(GP26),亦可寫成ADC(26)
factor = 3.3 / (65535) # 電壓轉換因子

# 設定I2C ID, SDA, SCL腳位對應,預設通訊頻率freq=400000
i2c = I2C(0, sda=Pin(20), scl=Pin(21))
print("I2C Address : " + hex(i2c.scan()[0]).upper()) # 於命令列顯示出I2C裝置位置
print("I2C Configuration: " + str(i2c))              # 於命令列顯示出I2C裝置基本參數

# 配置一個SSD1327_I2C實例,顯示區大小128x128,預設位址addr=0x3C
oled = SSD1327_I2C(i2c,128,128)

while True: # 若真則循環
    oled.fill(0) # 清除顯示區域
    oled.text("OLED-OmnixriJack", 0, 0, 15)    # 顯示字串於(0,0)位置,顏色15(最亮)
    
    if(pb1.value() == 0):                      # 若Pb1按下
        led1.value(1)                          # 點亮LED1
        oled.text("L  Press : On",  0, 20, 15) # 顯示字串於(0,20)位置,顏色15(最亮)
    else:
        led1.value(0)                          # 熄滅LED1
        oled.text("L  Press : Off", 0, 20, 15) # 顯示字串於(0,20)位置,顏色15(最亮)
        
    if(pb2.value() == 0):                      # 若Pb2按下
        led2.value(1)                          # 點亮LED2
        oled.text("R  Press :  On", 0, 40, 15) # 顯示字串於(0,40)位置,顏色15(最亮)
    else:
        led2.value(0)                          # 熄滅LED2
        oled.text("R  Press : Off", 0, 40, 15) # 顯示字串於(0,40)位置,顏色15(最亮)
        
    reading = adc.read_u16() # 讀取類比輸入值16bit無號整數
    volt = reading * factor  # 將輸入值轉成電壓值
    adc_str = "ADC Volt : " + str('%.3f' % volt) # 組成電壓值字串
    oled.text(adc_str, 0, 60, 15) # 顯示字串於(0,60)位置,顏色15(最亮)
    
    oled.show() # 刷新OLED顯示區

Fig. 2 OLED SSD1327 I2C實驗結果圖,左:按鍵、LED測試和狀態顯示,右:半固定電阻調整和電壓顯示。(OmniXRI Feb. 2021整理製作)(點擊圖放大)


小結


雖然樹莓派官方及OLED供應商目前都還沒有正式提供OLED SSD1327 I2C MicroPython驅動程式,但經過一番爬文及試錯後,終於搞定OLED SSD1327的文字顯示,希望能幫到大家。後續會接著介紹如何使用這個OLED進行圖形介面(GUI)控制,敬請期待下回分解。


參考文獻


[1] 許哲豪,【Raspberry Pi Pico 開箱實作教學 #01】 MicroPython開發環境安裝與GPIO數位/類比測試
https://omnixri.blogspot.com/2021/02/raspberry-pi-pico-01-micropythongpio.html

[2] Welcome to your Raspberry Pi Pico
    a. Board Sepecifications
    b. Getting started with MicroPython
    c. Getting started with C/C++
https://www.raspberrypi.org/documentation/pico/getting-started/

[3] PyPi - MicroPython library for SSD1327 based OLED displays.
https://pypi.org/project/micropython-ssd1327/

[4] Github - mcauser / micropython-ssd1327
https://github.com/mcauser/micropython-ssd1327

[5] MicroPython Documentation
https://docs.micropython.org/en/latest/

[6] SOLOMON SYSTECH SSD1327 Datasheet
https://www.generationrobots.com/media/Ecran_OLED_0_96/SSD1327_datasheet.pdf



1 則留言:

【頂置】簡報、源碼、系列文快速連結區

常有人反應用手機瀏覽本部落格時常要捲很多頁才能找到系列發文、開源專案、課程及活動簡報,為了方便大家快速查詢,特整理連結如下,敬請參考! Edge AI Taiwan 邊緣智能交流區 全像顯示與互動交流區 台科大(NTUST) 人工智慧與邊緣運算實務 開南大學...