2026年1月17日 星期六

使用 Arduino 點亮 SSD1306 OLED 有這麼難嗎?

作者: Jack OmniXRI, 2026/01/15

20260115_SSD1306_Fig00

相信大家看到標題就會問「不就把 Adafruit_SSD1306 函式庫裝上就搞定了嗎?」,如果你是使用 UNO R3 的朋友,那麼確實只要三兩下就搞定。但要寫一個讓 Arduino UNO R3 / UNO R4 Minima / UNO R4 WIFI 同時能工作的程式,可就學問多多了,包括硬體接線、函式庫應用、顯示字串組合等。為了記取花費了好幾天才搞定的經驗,也為了避免下次再犯同樣的錯誤,所以趁著還有印象,趕緊寫下筆記。

1. OLED 顯示器及驅動晶片簡介

為了方便 Arduino 顯示漂亮的文字(含中文)及繪製圖形,通常會搭配一組小尺寸的 OLED 顯示器模組。一般這類顯示器都會搭配一個驅動晶片(Driver IC),方便設定連接不同點數、顏色、連接介面的 OLED 或 TFT LCD。

在 Arduino 上常見的串列通訊介面有二線式雙向雙工一對一的 UART、二線式雙向單工一對多的 I2C 及四線式雙向雙工一對多的 SPI,這些都可以用來連接顯示器模組,其通訊速度 SPI > I2C > UART。有時考慮到顯示內容多寡,通訊速度會影響到顯示內容更新頻率,因此會選用不同的介面連接。

而像 0.96吋 單色(或假雙色) 128x64 像素的 OLED 就很適合使用 I2C 來控制及傳輸顯示內容。一個畫面資料量為 128x64 (8192) bit,若一秒要更新30次(30 FPS),則串列傳輸速度只要大於 8192x30 = 245,760 bit/sec (約246KHz),就能完成常見的動畫效果,當然如果考慮 Arduino 工作效能,要減少資料儲存、處理及傳輸量,則 10 FPS 也可產生不錯動畫效果。而 Arduino UNO R3/R4 I2C 標準模式時脈為 100KHz, 快速模式則可高達 400KHz,可輕鬆搞定。

由於顯示器的種類非常多,如果要每一種都設計一個專用的控制晶片就有點不現實,於是有很多廠商設計了通用型顯示控制晶片以利兼容多種需求,包括顯示內容長寬像素、色彩數(1, 8, 16, 24 bit)、旋轉方向(0, 90, 180, 270度)、通訊介面(I2C, SPI, 8 bit)及局部滾動等等。常見用於小尺寸 OLED 的控制晶片有 SSD1306, SSD1315, SH1106 等等,以下簡單秀出幾款 0.96 吋 OLED。

20260115_SSD1306_Fig01
Fig. 1 不同介面及控制晶片的 0.96 吋 OLED 顯示器。(OmniXRI 整理製作, 2026/01/15)

以下就把此次實驗的 OLED (如Fig. 1 右二)的規格進行簡單說明:

  • 顯示尺寸: 0.96 吋
  • 解 析 度: 128x64 像素
  • 色 彩 數: 單色 (1 bit)
  • 通訊介面: 四線式 I2C (GND, VCC, SCL, SDA)
    (注意:不同廠家模組 GND, VCC 順序可能會顛倒,要注意接線以免燒毀)
  • 工作電壓: 3.3V ~ 5.0V (本實驗採VCC=3.3V, SCL & SDA = 3.3V 或 5.0V 皆可)
  • 驅動晶片: SSD1306

2. Arduino 連接 I2C OLED 方式

本次實驗共使用了三塊 Arduino 開發板,包括 UNO R3, UNO R4 Minia, UNO R4 WIFI。依原廠設計開發板右下的 A4 和左上角的 SDA 是相通的(短路),A5 和 SCL 相通,且預設就是用於 I2C,因此使用哪一組都可以,如 Fig.2 所示。

20260115_SSD1306_Fig02
Fig. 2 Aruino UNO R3/R4 Minima/R4 WIFI 與 I2C OLED 連接方式。(OmniXRI 整理製作, 2026/01/15)

一般 I2C 為了可以併接多組裝置,所以時脈信號(SCL)和資料信號(SDA)通常採用開漏極(Open-Drain)設計,若沒有配上拉(Pull-High)電阻 4.7KΩ 或 10KΩ 則信號無法傳遞。在 UNO R3 I2C 已有內建了一組上拉電阻,所以當 OLED 接上後就可直接使用。但 UNO R4 Minima/WIFI 並沒有,所以在板上留了空點,如 Fig. 2 左上角所示,讓使用者自行焊好上拉電阻後,才能使用 I2C。預設會上拉到 5V,這並不影響 OLED VCC 是接 3.3V 或 5.0V,有時反而可以協助通訊頻率過高時,修整 SDA/SCL 波形(電壓高低變化)不確實問題。由於 SMD 電阻一般使用者不易取得,可以改用常見的 1/4W 或 1/8W 穿孔式電阻替代,如 Fig.2 左下角所示。由於各家 OLED 模組板設計不同,如果你買的 OLED 上已有在 SDA/SCL 接上拉電阻,則可不用手動加入。

3. OLED 顯示函式庫 - Adafruit_SSD1306

本實驗採用的 OLED 顯示器並非 Arduino 標準元件,所以內建函式庫並沒有,需手動安裝。好在 SSD1306 是很常用的驅動晶片,所以已有很多人幫忙寫好函式庫,其中最多人使用莫過於「Adafruit SSD1306」。安裝方式很簡單,開啟 Arduino IDE 後,只要點擊主選單 Sketch - Include Library - Manage Libraries 即可開啟函式庫管理器,或者直接點擊左側工具快捷鍵亦可開啟,如 Fig. 3 所示。

接著在搜尋欄輸入「SSD1306」就會出現很多相關的函式庫,選擇 「Adafruit SSD1306 by Adafruit」 按下【Install】即可開始安裝。安裝過程中會提示要安裝「Adafruit GFX」,請一起安裝,後續繪圖相關函式會引用到。

完成安裝後點擊 IDE 主選單 File - Examples - Adafruit_SSD1306 - ssd1306_128x64_i2c 即可開啟範例程式。由於這個範例支援多種開發板、多種解析度及不同的 I2C 位址,所以開啟後,要將第35列位址修改為 0x3C 「#define SCREEN_ADDRESS 0x3C」,接著按下快捷功能鍵【上傳】(含編譯)就可看到 OLED 上顯示各種測試圖案及文字。

20260115_SSD1306_Fig03
Fig. 3 安裝 Adafruit SSD1306 函式庫及啟動測試程式。(OmniXRI 整理製作, 2026/01/15)

測試完 UNO R3 本來想說把 UNO R4 Minima/WIFI 的上拉電阻加上,就能愉快地收工,沒想到事情不像憨人想得這麼簡單,程式上傳完,只見上面幾列(ROW)勉強看得到內容,剩下的一片雪花(白點),心中百思不得其解,不禁反覆碎碎唸道「怎麼會這樣??」。

經詢問各家 AI 後,得知最大可能是 I2C 資料送太快太多造成塞車,後面的資料蓋到前面造成,或者是信號0/1不夠清楚,數值被誤判。於是把各家 AI 提供的建議,都試了一輪,並交叉、混合試了多種排列組合都無解,如下所示。

  • 換一家 OLED 模組。(手邊有二家模組試過都不行,但不想再買新的)
  • 上拉電阻從 10KΩ 改到 4.7KΩ。
  • 上拉電壓從 3.3V 改到 5.0V。
  • I2C SCL 時脈頻率從 400K 降到 100K 甚至 50K。
  • 宣告 Adafruit_SSD1306 oled()後,在 setup() 函式中宣告 oled.begin() 後加上 delay(500) 甚至到 delay(2000) 讓 oled 初始化能更穩定。
  • 在每個 oled.display() 後加上 delay(2000) 讓 I2C 緩衝可以消化完。

4. OLED 顯示函式庫 ─ U8G2

正想著要放棄時,一條 AI 建議給出了希望,就是不要使用「Adafruit SSD1306」函式庫,改用「U8G2」函式庫,它對記憶體的管理比較理想,於是繼續實驗。

仿照 Fig. 3 作法,安裝「u8g2 by oliver」。完成安裝後點擊 IDE 主選單 File - Examples - U8g2 - u8x8 ─ HelloWorld 即可開啟範例程式。由於這個範例支援多種開發板、多種解析度、不同介面,甚至有實體和軟體 I2C 的支援,所以開啟後,預設會註解掉所有驅動晶片的宣告,須手動去除指定項目的註解符號(//)才能正常編譯和執行範例。注意:一次只能去除其中一列的註解符號。

第56列為硬體 I2C,會直接使用原本板上指定的 SCL(A4), SDA(A5),無使用 RESET,工作時脈預設 400KHz。

U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(/* reset=*/ U8X8_PIN_NONE);

第58列為軟體 I2C,即可指定任意腳位作為 SCL(clock)和SDA(data),板上左上角 SCL 和右下角 A4 同為 D19, SDA 和 A5 同為 D18,為統一硬體接線可直接設為 19, 18。不過使用軟體 I2C會變得很慢,且在傳送及接收資料時會佔用 CPU,無法處理其它工作。

U8X8_SSD1306_128X64_NONAME_SW_I2C u8x8(/* clock=*/ 19, /* data=*/ 18, /* reset=*/ U8X8_PIN_NONE); 

原則上使用硬體 I2C 即可。接著按下快捷功能鍵【上傳】(含編譯)就可看到 OLED 上顯示二列測試文字,第0列為反白的數字,第1列為「Hello World!」。但是要注意,如 Fig. 4 所示,這裡繪製字串 u8x8.drawString() 的y軸座標表示方式和 Adafruit_SSD1306 的 setCursor() 加上 println() 文字座標表示方式不同,這裡是採文字高度的倍數而不是像素的數量。若 u8x8.drawString() 座標設為(0,1),字高8個像素,則第1列字串顯示的真正座標相當於 Adafruit_SSD1306 setCursor(0,8)。

20260115_SSD1306_Fig04
Fig. 4 不同繪圖函式庫顯示字串的座標表示方式。(OmniXRI 整理製作, 2026/01/15)

5. 字串組合顯示限制

本以為終於可以讓 UNO R3/R4 都能正常使用 U8g2 函式庫加上硬體 I2C 推動 SSD1306 的 OLED 了,於是寫了一個讀取 ADC0 的數值,再轉換成二位小數點的電壓值顯示在 OLED 上的範例。顯示內容包括一個抬頭(Title),再繪製一條分隔線,最後顯示電壓值。

但奇怪的事又發生了,在 UNO R4 Minima/WIFI 上正常顯示,在 UNO R3 卻只顯示了前兩項而第三項卻消失了,於是加上一行 Serial.println(待顯示字串) 來檢查,結果竟是空白字串。於是又開始向各家 AI 求解,終於找到問題,是因 UNO R3 對字串的組合方式有限制,導致結合不出來就直接給空白字串,但又不會造成編譯失敗。

整理後得到四種方式,前三種只適用 UNO R4 Minima/WIFI,第四種則 UNO R3/R4 都可支援,簡單說明如下:

  1. 宣告成 String 型別,將多個字串直接相加後,再以setCursor()和print()顯示。
  2. 宣告成 String 型別,將多個字串直接相加後,再以drawStr()顯示。但 String 需透過 c_str() 轉換成 C 語言格式字串。
  3. 宣告字元陣列空間,配合 sprintf() 產生 C 語言格式字串,再交由 drawStr() 顯示字串。
  4. 由於 UNO R3 使用 sprintf() 時不能使用 %.2f 來獲取電壓值,因此須宣告二組字元陣列空間來解決,一組負責儲存 dtostrf() 浮點數轉字串結果,另一組負責儲存 sprintf() 合併所有要輸出的字串,最後就能交給 drawStr() 來顯示了。

6. 完整範例程式

完整範例程式如下所示。

// 讀取 ADC0 電壓值並顯示在 4pin 式 I2C OLED 128x64 (SSD1306) // 本範例適用於 UNO R3 / UNO R4 Minima / UNO R4 WIFI // GND = 0V, VCC = 3.3V, SDA = A4, SCL = A5 // UNO R4 要在 SDA/CLK 上接上拉電阻 4.7K 到 3.3V // 作者:Jack OmniXRI, 2026/01/15 #include <Arduino.h> #include <U8g2lib.h> #include <Wire.h> // 使用硬體 I2C,不使用 Reset 腳位 U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); // 若在 UNO R4 硬體 I2C 速度過快,降低 CLK 速度也無法解決顯示產生大量雜點時 // 可使用軟體 I2C,指定A5為CLK, A4為SDA, 或其它接腳, 沒有硬體RESET //U8G2_SSD1306_128X64_NONAME_F_SW_I2C u8g2(U8G2_R0, /* clock=*/ A5, /* data=*/ A4, /* reset=*/ U8X8_PIN_NONE); int sensorValue = 0; // 感測器原始數據 float value = 0; // 感測器讀值轉換成電壓 char floatBuf[10]; // 用來存放轉換後的數字字串 for UNO R3 char msgBuf[20]; // 用來存放最終的完整訊息 for UNO R3 void setup() { Serial.begin(9600); // delay(1000); // 給 OLED 供電穩定時間(可略) u8g2.begin(); // u8g2.setBusClock(100000); // 強制設為 100kHz,確保訊號完整(可略) u8g2.clearBuffer(); // 清除內部緩衝 } void loop() { // 讀取 ADC0 原始數值 sensorValue = analogRead(A0); // 計算工作電壓 4.77V(5V) 在 10bit(1024) ADC 下的電壓值 value = sensorValue / 1024.0 * 4.77 ; //value = map(sensorValue, 0, 1023, 0, 5); // 使用 map() 轉換電壓值 // 將原始值及轉換值傳回電腦 Serial.print("Original: "); Serial.print(sensorValue); Serial.print(", Converted: "); Serial.print(value); Serial.println(" V"); // 利用 u8g2 函式庫將電壓值顯示在 SSD1306 OLED 上 u8g2.clearBuffer(); // 清除內部緩衝 u8g2.setFont(u8g2_font_ncenB08_tr); // 選擇字體 // 顯示標題 u8g2.drawStr(20, 8, "System Monitor"); // 寫入字串 y座標為字的左下(非左上) // 繪製分隔線 u8g2.drawLine(0, 12, 128, 12); // 顯示電壓值(四選一) // // 方法一:適用 UNO R4 Minima / UNO R4 WIFI,UNO R3 不支援 // String strVolt = "ADC: " + String(value,2) + "V"; // 保留 2 位小數) // u8g2.setCursor(0, 24); // 設定起始座標,y座標為字的左下 // u8g2.print(strVolt); // 列印字串 // // 方法二:適用 UNO R4 Minima / UNO R4 WIFI,UNO R3 不支援 // String strVolt = "ADC: " + String(value,2) + "V"; // 保留 2 位小數) // u8g2.drawStr(0, 24, strVolt.c_str()); // 繪製字串,y座標為字的左下 // // 方法三:適用 UNO R4 Minima / UNO R4 WIFI,UNO R3 不支援 // sprintf(msgBuf, "ADC: %.2fV", value); // u8g2.drawStr(0, 24, msgBuf); // 方法四:適用 UNO R3 / UNO R4 Minima / UNO R4 WIFI // UNO R3 在方法一到方法三會產生字串空白問題 dtostrf(value, 4, 2, floatBuf); // 把浮點數轉成字串 (變數, 總寬度, 小數點幾位, 存放處) sprintf(msgBuf, "ADC: %sV", floatBuf); // 組合字串 u8g2.drawStr(0, 24, msgBuf); // 繪製字串 u8g2.sendBuffer(); // 傳送到顯示器 delay(1000); // 暫停1秒 }

小結

「使用 Arduino 點亮 SSD1306 OLED 有這麼難嗎?」,以往只使用 UNO R3 時一點都不難,但加上 UNO R4 後就變得更複雜了,絕不是一句「不就把 Adafruit_SSD1306 函式庫裝上就搞定了嗎?」。此次費盡千辛萬苦,終於能讓一種程式在三種硬體上都順利執行,不知道是否我用錯方法?還是真的要注意的事項就是這麼多?還請大家多多指教!最後分享試錯經驗給大家參考,希望大家不要再踩坑。

參考文獻


沒有留言:

張貼留言

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

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