2024年10月13日 星期日

MicroPython 學習筆記 : 用 SSD1306 顯示氣象資訊 (二)

在前一篇測試中我們已將從 OpenWeatherMap 取得的氣象資料成功地顯示在 SSD1306 顯示器上, 但由於 SSD1306 只能顯示 ASCII 字元, 所以無法顯示傳回值中以 Unicode 表示的天氣描述. 替代方案是可以利用傳回值中的 icon 編號來顯示天氣概況圖, 然後用 MicroPython 內建的 framebuf 模組顯示該像素圖於 OLED 螢幕. 

本篇先來研究 SSD1306 顯示原理以及如何利用 framebuf 模組來操作顯示緩衝區. 

本系列之前的文章參考 :

MicroPython 相關文章索引 :



此實驗主要使用 MicroPython 內建的 framebuf 模組來操作顯示緩衝區的 Bytes 類型資料, 以便能在 SSD1306 OLED 螢幕上顯示圖形與圖形化的文字. 


1. 顯示自訂字元 : 

SSD1306 是尺寸為 0.96 吋, 解析度為 128*64 畫素的 OLED 螢幕, 驅動程式 ssd1306.py 透過內建模組 framebuf 可以顯示 ASCII 字元, 每個 ASCII 字元佔用 8*8 像素區域, 但實際使用 6*8 像素 (左右各保留 1px 作為間距), 例如下面為 '0' 的像素圖 (bitmap) :




因此, 128*64 的解析度每列可顯示 128/8=16 個字, 共可顯示 64/8=8 列字, 整個螢幕可顯示 16*8=128 個字. 

如果要顯示 ASCII 字元以外的符號, 例如表示溫度的右上角小圈圈, 就要自行製作像素圖 (bitmap) 利用內建模組 framebuf 寫入顯示緩衝區, 除了自訂字元外, 顯示圖片也是使用此方法, 原理都是一樣利用 bitmap : 



顯示一個像素可以用 1 表示, 不顯示則為 0, 因此一張像素圖可用元素為 0 或 1 的二維陣列表示, 以上面溫度符號的 8*8 的像素圖而言, 最上面一列可以用二進位的 0b00110000 表示, 或者用 16 進位的 0x30 表示, 因此整張像素圖可以用下列串列表示 :

degree=[0x30, 0x48, 0x48, 0x30, 0x00, 0x00, 0x00, 0x00]  

其中元素是由上而下順序排列, 最後四列都是不顯示的 0, 所以都是 0x00. 這種表示法稱為 HLSB (水平最低有效位元, Horrizontal Least Significant Bit), 即不論行列都是先從 LSB (即座標索引 0) 開始排列.

另外一種像素圖表示方式稱為 HMSB (水平最大有效位元, Horrizontal Maximum Significant Bit), 列仍是從最小列索引 0 開始, 但行卻由最大索引 7 開始排, 也就是列仍由上而下, 但行改為由右而左, 這樣上面的溫度符號第一列就要表示成 0b00001100, 或 16 進位的 0x0c; 第二列是 0b00010010 或 0x12; 依此類推, 整個像素圖用 HMSB 排列表示如下 :

degree=[0x0c, 0x10, 0x10, 0x0c, 0x00, 0x00, 0x00, 0x00]  

還有一種表示法稱為 VLSB (垂直最低有效位元, Vertical Least Significant Bit), 它是列由下而上, 行由左而右排列, 即由左下角像素往上跑到頂後再往右跑, 故第一個 byte 是 0b00000000 或 0x00, 第二個 byte 是 0b00000110 或 0x06, 依此類推, 整個像素圖用 VLSB 排列表示如下 :

degree=[0x00, 0x06, 0x09, 0x09, 0x06, 0x00, 0x00, 0x00] 

不論哪一種像素圖排列方式, framebuf 模組都支援, 只要指定好排列方式它就能正確解讀. 但上面這些像素圖串列其實都是整數串列, 必須用內建函式 bytearray() 轉成 bytes 類型的串列才能放進顯示緩衝器為 framebuf 所用, 例如 :

>>> degree=[0x00, 0x06, 0x09, 0x09, 0x06, 0x00, 0x00, 0x00]    # VLSB 排列模式
>>> degree   
[0, 6, 9, 9, 6, 0, 0, 0]
>>> type(degree)       # 整數串列
<class 'list'>
>>> type(degree[0])      
<class 'int'>
>>> buf=bytearray(degree)    # 轉成 bytearray 物件
>>> buf    
bytearray(b'\x00\x06\t\t\x06\x00\x00\x00')
>>> type(buf)   
<class 'bytearray'>

接下來匯入 framebuf 模組並檢視其成員 :

>>> import framebuf      
>>> dir(framebuf)    
['__class__', '__name__', 'FrameBuffer', 'FrameBuffer1', 'GS2_HMSB', 'GS4_HMSB', 'GS8', 'MONO_HLSB', 'MONO_HMSB', 'MONO_VLSB', 'MVLSB', 'RGB565', '__dict__']   

其中 FrameBuffer 類別就是操作顯示緩衝區記憶體的主角, 呼叫其建構式 FrameBuffer() 並傳入要存入顯示緩衝器的 bytearray 資料, 尺寸, 與像素圖表示方式即可建立一個 FrameBuffer 物件, 

fb=framebuf.FrameBuffer(buf, width, height, mode)    

其中 buf 為像素圖之 bytearray 物件, width 與 height 是像素圖之尺寸 (px), 而 mode 為像素圖的排列模式, 也就是 FrameBuffer 類別成員中的三個常數 MONO_HLSB, MONO_HMSB, 與 MONO_VLSB, 其值均為常數 : 

>>> framebuf.MONO_HLSB    
3
>>> framebuf.MONO_HMSB   
4
>>> framebuf.MONO_VLSB  
0

以上面的溫度符號為例, 如果以 VLSB 排列模式建立該字元的 FrameBuffer 物件 :

>>> fb=framebuf.FrameBuffer(buf, 8, 8, framebuf.MONO_VLSB)    
>>> type(fb)   
<class 'FrameBuffer'>

檢視 FrameBuffer 物件內容 :

>>> dir(fb)   
['__class__', 'blit', 'ellipse', 'fill', 'fill_rect', 'hline', 'line', 'pixel', 'poly', 'rect', 'scroll', 'text', 'vline'] 

可見 FrameBuffer 物件提供了 13 個方法來操控顯示緩衝區, 通常都是透過 SSD1306_I2C 物件的 framebuf 類別以靜態方法呼叫使用, 例如 :

>>> from machine import I2C, Pin
>>> import ssd1306   
>>> i2c=I2C(0, scl=Pin(22), sda=Pin(21)) # ESP32   
>>> oled=ssd1306.SSD1306_I2C(128, 64, i2c)     
>>> type(oled)    
<class 'SSD1306_I2C'>  

變數 oled 是一個 SSD1306_I2C 物件, 其成員包含一個 framebuf 類別 :

>>> dir(oled)  
['__class__', '__init__', '__module__', '__qualname__', '__dict__', 'addr', 'buffer', 'fill', 'framebuf', 'invert', 'pixel', 'scroll', 'text', 'width', 'height', 'external_vcc', 'pages', 'poweron', 'init_display', 'write_cmd', 'show', 'poweroff', 'contrast', 'write_framebuf', 'i2c', 'temp']
>>> dir(oled.framebuf)      
['__class__', 'blit', 'ellipse', 'fill', 'fill_rect', 'hline', 'line', 'pixel', 'poly', 'rect', 'scroll', 'text', 'vline']

可見與上面 FrameBuffer 物件的方法完全一樣, 所以這些方法可以做為 framebuf 的靜態方法呼叫, 例如要在 OLED 上顯示 27 度, 可以呼叫 oled 的 text() 先輸出兩個字元 27, 然後在第三個字元 (座標 x=16, y=0) 輸出儲存在 buf 緩衝區的度字元 :

>>> oled.text('27', 0, 0)   
>>> oled.framebuf.blit(fb, 16, 0)   
>>> oled.show()    

結果如下 :




不管像素圖排列模式為何, 只要在建立 FrameBuffer 物件時指定正確的 mode 即可, 例如下面是改用 HLSB 排列的像素圖 :

>>> degree=[0x30, 0x48, 0x48, 0x30, 0x00, 0x00, 0x00, 0x00]   
>>> buf=bytearray(degree)   
>>> buf    
bytearray(b'0HH0\x00\x00\x00\x00')
>>> fb=framebuf.FrameBuffer(buf, 8, 8, framebuf.MONO_HLSB)        
>>> oled.fill(0)       # 將 1 填滿顯示緩衝器 (點亮畫素)
>>> oled.show()   
>>> oled.text('27', 0, 0)    
>>> oled.framebuf.blit(fb, 16, 0)    
>>> oled.show()       

顯示的結果是一樣的. 


2. 顯示圖片 : 

在 SSD1306 上顯示圖片同樣是使用 bitmap 原理, 只是若要像上面製作溫度符號那樣一個一個像素去編輯串列就太累了, 有現成的線上軟體可以自動產生 bytesarray 資料, 例如 :


先用小畫家或其他圖片編輯軟體 (我使用抓圖軟體 picpick) 製作 bmp 圖檔後上傳, 指定排列模式即可產生 bytesarray 資料. 例如要顯示 32*16 解析度的中文 "溫度" 像素圖, 因解析度小不好編輯, 所以先用 Picpick 開啟一個 320*160 的新檔案 :




然後用大小為 60 的粗體微軟正黑體字型輸入 "溫度" :

 


點選 "常用/調整大小/調整圖片大小縮放" :




設定縮放比為 10% 將目前的 320*160 縮成 32*16 大小 : 




最後存檔為 temperature.bmp (注意, 必須為 .bmp). 

接下來到 image2cpp 網站將此像素檔上傳 :



第二步基本上不用設定, 預設即可 : 




第三步要選擇 Plain bytes, 挑選一個排列模式, 用 Horizontal 即可 (即 HLSB 模式), 然後按 Generate code 鈕就會在下方顯示 bytesarray 資料 : 




將這些像素資料複製起來 :




將其存入一個串列中後用 bytearray() 轉成 bytearray 類型 :

>>> temperature=[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe6, 0x03, 0xfc, 0xff, 0xf2, 0xdb, 0xc0, 0x07, 
0xfe, 0x9b, 0xdf, 0xff, 0xee, 0xab, 0xd0, 0x07, 0xe6, 0xf3, 0xdb, 0xdf, 0xff, 0xff, 0xd8, 0x1f, 
0xff, 0xff, 0xdf, 0xff, 0xfe, 0x03, 0xd0, 0x1f, 0xf7, 0xab, 0xdb, 0x9f, 0xf7, 0xab, 0xdd, 0x3f, 
0xe4, 0xab, 0xbc, 0x3f, 0xec, 0x01, 0xb3, 0xc7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]   
>>> buf=bytearray(temperature)   
>>> buf    
bytearray(b'\xff\xff\xff\xff\xff\xff\xff\xff\xe6\x03\xfc\xff\xf2\xdb\xc0\x07\xfe\x9b\xdf\xff\xee\xab\xd0\x07\xe6\xf3\xdb\xdf\xff\xff\xd8\x1f\xff\xff\xdf\xff\xfe\x03\xd0\x1f\xf7\xab\xdb\x9f\xf7\xab\xdd?\xe4\xab\xbc?\xec\x01\xb3\xc7\xff\xff\xff\xff\xff\xff\xff\xff')   

然後呼叫 framebuf.FrameBuffer() 並傳入此 bytearray 建立 FrameBuffer 物件, 注意, 上面用 image2cpp 產生像素圖時指定水平排列, 故此處要用 MONO_HLSB), 就可以呼叫 blit() 方法顯示在 OLED 上了 :

>>> fb=framebuf.FrameBuffer(buf, 32, 16, framebuf.MONO_HLSB)    
>>> oled.fill(0)     # 熄滅全部畫素    
>>> oled.show()    
>>> oled.framebuf.blit(fb, 0, 0)    
>>> oled.show()   

結果如下 : 




由於是使用 Windows 字型, 周圍有美化字型的像素調整, 經過縮小後會有些失真, 但基本上還算清楚可讀. 用同樣方法我硬製作了 "濕度" : 

>>> humidity=[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe4, 0x03, 0xfc, 0xff, 0xf4, 0xfb, 0xc0, 0x07, 
0xfc, 0xfb, 0xdf, 0xff, 0xfc, 0x03, 0xd0, 0x0f, 0xe6, 0xff, 0xdb, 0x9f, 0xf4, 0xab, 0xd8, 0x3f, 
0xfc, 0x67, 0xdf, 0xff, 0xf6, 0xe3, 0xd0, 0x1f, 0xf4, 0x21, 0xdb, 0x9f, 0xef, 0xff, 0xdd, 0xbf, 
0xed, 0x6b, 0xbc, 0x3f, 0xeb, 0x6d, 0xa3, 0x87, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]     
>>> buf=bytearray(humidity)    
>>> fb=framebuf.FrameBuffer(buf, 32, 16, framebuf.MONO_HLSB)    
>>> oled.framebuf.blit(fb, 0, 16)     # Y 軸往下挪 16 個畫素
>>> oled.show()    

以及 "氣壓" : 

>>> pressure=[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb, 0xff, 0x80, 0x0f, 0xf0, 0x03, 0xbf, 0xff, 
0xe7, 0xff, 0xa4, 0xdf, 0xef, 0xff, 0xa4, 0x8f, 0xf0, 0x07, 0xbf, 0xdf, 0xff, 0xf7, 0xa4, 0x9f, 
0xf5, 0xb7, 0xa5, 0xaf, 0xfc, 0xf7, 0xad, 0x6f, 0xec, 0xf7, 0xbf, 0xff, 0xfd, 0xf5, 0xa0, 0x0f, 
0xe5, 0x93, 0xbe, 0xff, 0xed, 0xf3, 0xe0, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]    
>>> buf=bytearray(pressure)     
>>> buf   
bytearray(b'\xff\xff\xff\xff\xff\xff\xff\xff\xfb\xff\x80\x0f\xf0\x03\xbf\xff\xe7\xff\xa4\xdf\xef\xff\xa4\x8f\xf0\x07\xbf\xdf\xff\xf7\xa4\x9f\xf5\xb7\xa5\xaf\xfc\xf7\xado\xec\xf7\xbf\xff\xfd\xf5\xa0\x0f\xe5\x93\xbe\xff\xed\xf3\xe0\x0f\xff\xff\xff\xff\xff\xff\xff\xff')   
>>> fb=framebuf.FrameBuffer(buf, 32, 16, framebuf.MONO_HLSB)     
>>> oled.framebuf.blit(fb, 0, 32)       # Y 軸再往下挪 16 個畫素
>>> oled.show()      

結果如下 : 




如果在這些中文像素圖後面用 oled.text() 顯示文字, 因為其大小為 8*8, 會顯得不搭配 :

>>> oled.text('27.5', 32, 0)   
>>> oled.text('89%', 32, 16)   
>>> oled.text('1012hPa', 32, 32)   
>>> oled.show()  

結果如下 : 




如果要讓標題與數值搭配, 則需要替 0~9 與 % 等符號製作 32*16 的像素圖, 利用 framebuf 建立顯示緩衝區輸出到 OLED 螢幕. 


3. 使用 bigSymbol.py 模組顯示氣象資訊 : 

下面利用趙英傑老師的 "超圖解 Python 物聯網實作入門" 這本書第 12-5 節提供的 bigSymbol.py 模組來顯示 32*16 解析度的氣象資料, 該模組提供  0~9, c (溫度符號), %, 小數點, 以及中文 "溫度" 與 "濕度" 等字元的 32*16 像素圖. 


不過此 bigSymbol.py 在新版韌體上無法執行, 因為 SSD1306_I2C 物件已不能直接呼叫 blit() 方法, 必須呼叫 framebuf 類別的靜態方法 blit() 才行, 所以我將此模組略加修改後改名為 big_symbol.py :

sefl.oled.blit(fb, x, y)  => sefl.oled.framebuf.blit(fb, x, y) 

另外我想顯示氣壓資訊, 所以將上面氣壓像素圖 pressure 的 bytearray 資料加進來新增了一個 _PRESS 變數 :

_PRESS = bytearray(b'\xff\xff\xff\xff\xff\xff\xff\xff\xfb\xff\x80\x0f\xf0\x03\xbf\xff\xe7\xff\xa4\xdf\xef\xff\xa4\x8f\xf0\x07\xbf\xdf\xff\xf7\xa4\x9f\xf5\xb7\xa5\xaf\xfc\xf7\xado\xec\xf7\xbf\xff\xfd\xf5\xa0\x0f\xe5\x93\xbe\xff\xed\xf3\xe0\x0f\xff\xff\xff\xff\xff\xff\xff\xff') 

以及一個顯示 "氣壓" 像素圖的 press() 方法

def press(self, x, y):
        fb = framebuf.FrameBuffer(self._PRESS, 32, 16, framebuf.MONO_HLSB)
        self.oled.framebuf.blit(fb, x, y)

注意, 因為我上面製作 "氣壓" 像素圖時使用水平排列模式, 所以這裡要指定用 MONO_HLSB 去讀取才會正確解碼, 而原 bigSymbol.py 模組裡面的像素圖則都使用 MONO_VLSB. 模式. 

氣壓單位為 hPa, 所以我也新增了 'h', 'P', 'a' 這三個字元像素圖的 bytearray : 

>>> h=[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xfc, 0x40, 0x40, 0x40, 0xc0, 0x80, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x1f, 0x00, 0x00, 0x00, 0x1f, 0x1f, 0x00, 0x00, 0x00]   
>>> buf=bytearray(h)   
>>> buf      
bytearray(b'\x00\x00\x00\x00\x00\x00\xfc\xfc@@@\xc0\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x1f\x00\x00\x00\x1f\x1f\x00\x00\x00')
>>> P=[0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x08, 0x08, 0x08, 0x18, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x03, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]   
>>> buf=bytearray(h)   
>>> buf   
bytearray(b'\x00\x00\x00\x00\x00\xf8\x08\x08\x08\x18\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x03\x01\x01\x01\x00\x00\x00\x00\x00\x00')
>>> a=[0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x90, 0x90, 0xe0, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x07, 0x09, 0x08, 0x00, 0x07, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
>>> buf=bytearray(h)   
>>> buf   
bytearray(b'\x00\x00\x00\x00\x000\x90\x90\xe0\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\t\x08\x00\x07\x0f\x00\x00\x00\x00\x00\x00')

注意, 因為數字符號字元都是暗底亮字顯示, 所以在製作 hPa 這三個字元的 bytearray 資料時, 要在第二步勾選 Invert image colors 這一項, 否則會變成亮底暗字, 跟 0~9 相反 :




其次, 為了跟原來 0~9 等字元一致, hPa 都使用垂直排列, 第四步要選 Vertical :




我將新版的 big_symbol.py 模組放在 GitHub :


將此 big_symbol.py 上傳開發板後重啟系統, 執行下列程式 :

# ssd1306_big_symbol.py 
from machine import I2C, Pin
import ssd1306
import big_symbol

i2c=I2C(0, scl=Pin(22), sda=Pin(21)) # ESP32
oled=ssd1306.SSD1306_I2C(128, 64, i2c)
sb=big_symbol.Symbol(oled)
sb.clear()
sb.temp(0, 0)
sb.text('27.85c', 32, 0)
sb.humid(0, 16)
sb.text('89.66%', 32, 16)
sb.press(0, 32)
sb.text('1021hPa', 32, 32)
oled.show()

結果如下 : 




可見當氣壓是 4 位數時, hPa 最後一個字元 a 會因為超出螢幕而消失. 如果是 3 位數就會顯現 :

>>> sb.text('782hPa', 32, 32)   
>>> oled.show()    




完整程式碼與上面所用檔案壓縮檔可在 GitHub 下載 :


終於在自請休假連休 4 天的結尾搞定顯示緩衝區 framebuf 的用法了, 好累累. 

沒有留言 :