Arduino 專案實作:整合 DHT11 與 SSD1306 OLED 顯示器 (進階篇)

本篇教學將深入探討 U8g2 程式庫的強大功能,帶您學習如何設計更豐富的資訊介面、使用圖形化元素,甚至顯示部分中文。我們將透過一系列螺旋式範例,從最基礎的文字顯示開始,逐步疊加功能,最終打造一個專業且具視覺吸引力的溫濕度儀表板。


學習路徑圖 (Learning Roadmap)

本教學文件採用螺旋式上升的結構設計,您可以依照下圖的路徑,從基礎概念開始,逐步完成實作,並深入理解其核心原理。

graph TD subgraph "A. 基礎知識 (Foundation)" A1[1. U8g2 程式庫介紹] --> A2[1.2 OLED 座標系統] A2 --> A3[1.4 U8g2 建構式詳解] end subgraph "B. 核心技能練習 (Core Skills)" B1[1.3 函數應用練習] B1 --> B1a(練習一: 繪製圖形) B1 --> B1b(練習二: 文字與線條) end subgraph "C. 螺旋式專案實作 (Spiral Project)" C1[範例一: 基本文字] --> C2[範例二: 加入圖示] C2 --> C3[範例三: 使用中文] C3 --> C4[範例四: 資料視覺化] C4 --> C5[範例五: 綜合儀表板] end subgraph "D. 深入理解 (Deeper Understanding)" D1[4. U8g2 核心優勢] D1 --> D2(Page Buffer 機制) end subgraph "E. 學習成果 (Outcome)" E1[🎉 掌握客製化介面設計] end A3 --> B1 B1b --> C1 C5 --> D1 D2 --> E1

1. U8g2 程式庫深度解析

U8g2 不僅僅是個顯示文字的工具,它是一個功能完整的圖形函式庫。理解其核心功能是創造優質介面的關鍵。

1.1. 核心功能與常用函數

U8g2 的強大之處在於其豐富的繪圖 API。以下是一些最常用的函數:

類別 函數 功能說明
基礎控制 u8g2.begin() 初始化顯示器。
u8g2.firstPage() 開始一個頁面繪圖循環。
u8g2.nextPage() 繼續繪製下一個頁面,直到完成。
文字處理 u8g2.setFont(font_name) 設定要使用的字型。這是改變外觀最重要的指令。
u8g2.setCursor(x, y) 設定下一次 print 指令的起始座標。
u8g2.print(value) 在當前游標位置印出文字、數字或變數。
u8g2.drawStr(x, y, "text") 在指定座標繪製一個字串。
圖形繪製 u8g2.drawBox(x, y, w, h) 畫一個實心矩形。
u8g2.drawFrame(x, y, w, h) 畫一個空心矩形。
u8g2.drawRFrame(x, y, w, h, r) 畫一個圓角的空心矩形。
u8g2.drawGlyph(x, y, char_code) 繪製特定字型中的單一圖形或字元。

1.2. OLED 顯示規劃:座標系統詳解

在開始繪製畫面之前,最重要的一步是理解 OLED 螢幕的座標系統。對於一塊 128x64 解析度的螢幕:
- 原點 (0,0):位於螢幕的左上角
- X 軸:從左到右,範圍是 0127
- Y 軸:從上到下,範圍是 063

所有 draw...setCursor 相關的函數,其 (x, y) 座標都遵循這個規則。

(0,0)
  +-----------------------------------------------------> X (0 to 127)
  |
  |
  |
  |
  |
  |
  V Y (0 to 63)

例如,u8g2.drawPixel(64, 32) 會在螢幕的正中央畫一個點。

1.3. 函數應用練習

在進入複雜的專案前,讓我們先透過幾個簡單的練習來熟悉這些繪圖函數。

練習一:繪製基本圖形

練習二:文字與線條

1.4. 關鍵指令:U8g2 建構式詳解

在所有 U8g2 程式碼中,最關鍵的一行就是建構式。它告訴程式庫你正在使用哪種硬體。讓我們來拆解它:
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

1.5. U8g2 的字型系統與中文支援

U8g2 內建了數百種字型。最好的方法是參考 U8g2 官方字型圖庫 來預覽所有字型的外觀。

如何使用中文?
Arduino Uno 的記憶體有限,無法容納完整的 CJK (中日韓) 字庫。因此,U8g2 採取了收錄部分常用字的策略。您需要找到名稱包含 chinese 的字型,並在程式碼中啟用 UTF-8 編碼。
u8g2.enableUTF8Print();

注意:使用中文字型會佔用大量的 Flash 儲存空間,可能會讓您的專案無法上傳至 Arduino Uno。在資源有限的開發板上,使用圖示或英文是更穩妥的選擇。


2. 螺旋式範例實作 (128x64 OLED)

接下來,我們將透過五個由簡入繁的範例,一步步建構出最終的儀表板。每個範例都包含顯示規劃、給 AI 的指令範例以及完整程式碼

範例一:基本文字顯示

a. 顯示規劃

在 128x64 的 OLED 螢幕上,上半部顯示溫度,下半部顯示濕度。
- 第一行 (y=20): Temp: XX.X C
- 第二行 (y=50): Humi: XX.X %

ASCII 模擬圖:

+--------------------------+
|                          |
| Temp: 25.5 C             |
|                          |
|                          |
| Humi: 60.1 %             |
|                          |
|                          |
+--------------------------+

b. AI 生成程式的 Prompt 範例

"請為 Arduino Uno 撰寫一支完整的程式碼,使用 U8g2 程式庫驅動 SSD1306 I2C 128x64 OLED 顯示器。程式需連接 DHT11 感測器 (DATA 腳位連接到 Arduino 的 Pin 2)。在 OLED 螢幕上,請使用 u8g2_font_ncenB10_tr 字型,在 y=20 處顯示溫度資訊,格式為 'Temp: XX.X C';在 y=50 處顯示濕度資訊,格式為 'Humi: XX.X %'。程式應使用非阻塞延遲 (millis()) 每 2 秒更新一次 DHT11 的數據並顯示在螢幕上。"

c. 程式碼範例

#include <Arduino.h>
#include <U8g2lib.h>
#include <Wire.h>
#include "DHT.h"

#define DHTPIN 2
#define DHTTYPE DHT11

U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
DHT dht(DHTPIN, DHTTYPE);

unsigned long previousMillis = 0;
const long interval = 2000;
float humidity, temp_c;

void setup(void) {
  dht.begin();
  u8g2.begin();
}

void loop(void) {
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;
    humidity = dht.readHumidity();
    temp_c = dht.readTemperature();
  }

  u8g2.firstPage();
  do {
    u8g2.setFont(u8g2_font_ncenB10_tr); // 選擇一個大小為 10 的粗體字型

    // 顯示溫度
    u8g2.setCursor(0, 20);
    u8g2.print("Temp: ");
    u8g2.print(temp_c, 1);
    u8g2.print(" C");

    // 顯示濕度
    u8g2.setCursor(0, 50);
    u8g2.print("Humi: ");
    u8g2.print(humidity, 1);
    u8g2.print(" %");
  } while (u8g2.nextPage());
}

範例二:加入圖示

a. 顯示規劃

在 128x64 的 OLED 螢幕上,於溫度和濕度文字左側分別加入溫度計和水滴圖示。
- 溫度計圖示 (x=5, y=22),文字 (x=30, y=20): Temp: XX.X C
- 水滴圖示 (x=5, y=52),文字 (x=30, y=50): Humi: XX.X %

ASCII 模擬圖:

+--------------------------+
|                          |
|  [T] Temp: 25.5 C        |
|                          |
|                          |
|  [D] Humi: 60.1 %        |
|                          |
|                          |
+--------------------------+
([T]: 溫度計圖示, [D]: 水滴圖示)

b. AI 生成程式的 Prompt 範例

"請為 Arduino Uno 撰寫一支完整的程式碼,使用 U8g2 程式庫驅動 SSD1306 I2C 128x64 OLED 顯示器。程式需連接 DHT11 感測器 (DATA 腳位連接到 Arduino 的 Pin 2)。在 OLED 螢幕上,請在溫度數值左側加入一個溫度計圖示,在濕度數值左側加入一個水滴圖示。請使用 u8g2_font_open_iconic_weather_2x_t 圖示字型,其中 'A' 為溫度計,'E' 為水滴。文字部分請使用 u8g2_font_ncenB10_tr 字型。確保圖示與文字排版對齊,溫度計圖示位於 (5, 22),溫度文字從 (30, 20) 開始;水滴圖示位於 (5, 52),濕度文字從 (30, 50) 開始。程式應使用非阻塞延遲 (millis()) 每 2 秒更新一次 DHT11 的數據並顯示在螢幕上。"

c. 程式碼範例

#include <Arduino.h>
#include <U8g2lib.h>
#include <Wire.h>
#include "DHT.h"

#define DHTPIN 2
#define DHTTYPE DHT11

U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
DHT dht(DHTPIN, DHTTYPE);

unsigned long previousMillis = 0;
const long interval = 2000;
float humidity, temp_c;

void setup(void) {
  dht.begin();
  u8g2.begin();
}

void loop(void) {
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;
    humidity = dht.readHumidity();
    temp_c = dht.readTemperature();
  }

  u8g2.firstPage();
  do {
    // --- 繪製溫度 ---
    u8g2.setFont(u8g2_font_open_iconic_weather_2x_t); // 設定 2x 大小的圖示字型
    u8g2.drawGlyph(5, 22, 'A'); // 繪製溫度計圖示

    u8g2.setFont(u8g2_font_ncenB10_tr); // 切換回文字字型
    u8g2.setCursor(30, 20);
    u8g2.print("Temp: ");
    u8g2.print(temp_c, 1);
    u8g2.print(" C");

    // --- 繪製濕度 ---
    u8g2.setFont(u8g2_font_open_iconic_weather_2x_t);
    u8g2.drawGlyph(5, 52, 'E'); // 繪製水滴圖示

    u8g2.setFont(u8g2_font_ncenB10_tr);
    u8g2.setCursor(30, 50);
    u8g2.print("Humi: ");
    u8g2.print(humidity, 1);
    u8g2.print(" %");
  } while (u8g2.nextPage());
}

範例三:使用中文

a. 顯示規劃

在 128x64 的 OLED 螢幕上,將標籤 "Temp:" 和 "Humi:" 替換為中文 "溫度:" 和 "濕度:"。
- 第一行 (y=20): 溫度: XX.X C
- 第二行 (y=50): 濕度: XX.X %

ASCII 模擬圖:

+--------------------------+
|                          |
| 溫度: 25.5 C             |
|                          |
|                          |
| 濕度: 60.1 %             |
|                          |
|                          |
+--------------------------+

b. AI 生成程式的 Prompt 範例

"請為 Arduino Uno 撰寫一支完整的程式碼,使用 U8g2 程式庫驅動 SSD1306 I2C 128x64 OLED 顯示器。程式需連接 DHT11 感測器 (DATA 腳位連接到 Arduino 的 Pin 2)。在 OLED 螢幕上,請將溫度標籤 'Temp:' 改為 '溫度:',濕度標籤 'Humi:' 改為 '濕度:'。請使用 u8g2_font_wqy12_t_chinese2 字型顯示中文,並啟用 UTF-8 支援。溫度資訊顯示在 y=20 處,濕度資訊顯示在 y=50 處。程式應使用非阻塞延遲 (millis()) 每 2 秒更新一次 DHT11 的數據並顯示在螢幕上。請在程式碼註解中提醒使用者在 Arduino Uno 上使用中文字型可能導致儲存空間不足的問題。"

c. 程式碼範例

#include <Arduino.h>
#include <U8g2lib.h>
#include <Wire.h>
#include "DHT.h"

#define DHTPIN 2
#define DHTTYPE DHT11

U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
DHT dht(DHTPIN, DHTTYPE);

unsigned long previousMillis = 0;
const long interval = 2000;
float humidity, temp_c;

void setup(void) {
  dht.begin();
  u8g2.begin();
  // 注意:使用中文字型會大幅增加程式碼大小,可能不適用於 Flash 空間小的開發板
  u8g2.enableUTF8Print(); // 啟用 UTF-8 編碼以支援中文
}

void loop(void) {
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;
    humidity = dht.readHumidity();
    temp_c = dht.readTemperature();
  }

  u8g2.firstPage();
  do {
    u8g2.setFont(u8g2_font_wqy12_t_chinese2); // 選擇一個包含常用字的 12px 中文字型

    // 顯示溫度
    u8g2.setCursor(0, 20);
    u8g2.print("溫度: ");
    u8g2.print(temp_c, 1);
    u8g2.print(" C");

    // 顯示濕度
    u8g2.setCursor(0, 50);
    u8g2.print("濕度: ");
    u8g2.print(humidity, 1);
    u8g2.print(" %");
  } while (u8g2.nextPage());
}

範例四:資料視覺化 (長條圖)

a. 顯示規劃

在 128x64 的 OLED 螢幕上,上半部顯示溫度,下半部顯示濕度數值,並在濕度數值下方增加一個水平長條圖。
- 第一行 (y=20): Temp: XX.X C
- 第二行 (y=45): Humi: XX.X %
- 濕度長條圖: 位於 (0, 55) 處,外框寬度 102 像素,高度 8 像素。

ASCII 模擬圖:

+--------------------------+
|                          |
| Temp: 25.5 C             |
|                          |
| Humi: 60.1 %             |
| [===========     ]       |
|                          |
|                          |
+--------------------------+

b. AI 生成程式的 Prompt 範例

"請為 Arduino Uno 撰寫一支完整的程式碼,使用 U8g2 程式庫驅動 SSD1306 I2C 128x64 OLED 顯示器。程式需連接 DHT11 感測器 (DATA 腳位連接到 Arduino 的 Pin 2)。在 OLED 螢幕上,請使用 u8g2_font_ncenB10_tr 字型,在 y=20 處顯示溫度資訊,格式為 'Temp: XX.X C';在 y=45 處顯示濕度資訊,格式為 'Humi: XX.X %'。此外,在濕度數值下方 (從 (0, 55) 座標開始),加入一個水平長條圖來視覺化呈現濕度百分比 (0-100%)。長條圖的外框寬度為 102 像素,高度為 8 像素,實心長條的寬度應根據濕度值 (0-100%) 映射到 0-100 像素。程式應使用非阻塞延遲 (millis()) 每 2 秒更新一次 DHT11 的數據並顯示在螢幕上。"

c. 程式碼範例

#include <Arduino.h>
#include <U8g2lib.h>
#include <Wire.h>
#include "DHT.h"

#define DHTPIN 2
#define DHTTYPE DHT11

U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
DHT dht(DHTPIN, DHTTYPE);

unsigned long previousMillis = 0;
const long interval = 2000;
float humidity, temp_c;

void setup(void) {
  dht.begin();
  u8g2.begin();
}

void loop(void) {
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;
    humidity = dht.readHumidity();
    temp_c = dht.readTemperature();
  }

  u8g2.firstPage();
  do {
    u8g2.setFont(u8g2_font_ncenB10_tr);

    // 顯示溫度
    u8g2.setCursor(0, 20);
    u8g2.print("Temp: ");
    u8g2.print(temp_c, 1);
    u8g2.print(" C");

    // 顯示濕度文字
    u8g2.setCursor(0, 45);
    u8g2.print("Humi: ");
    u8g2.print(humidity, 1);
    u8g2.print(" %");

    // 繪製濕度長條圖
    int barWidth = map(humidity, 0, 100, 0, 100); // 將 0-100% 映射到 0-100 像素寬度
    u8g2.drawFrame(0, 55, 102, 8); // 繪製長條圖外框 (x, y, width, height)
    u8g2.drawBox(1, 56, barWidth, 6); // 根據濕度繪製實心長條 (x, y, width, height)
  } while (u8g2.nextPage());
}

範例五:綜合儀表板介面

a. 顯示規劃

設計一個專業的儀表板介面,將 128x64 的 OLED 螢幕畫面分為上下兩區,結合圖示與更美觀的字型,並優化排版。
- 溫度區:左側為大型溫度計圖示,右側為標籤與數值。
- 濕度區:左側為大型水滴圖示,右側為標籤與數值。

ASCII 模擬圖:

+--------------------------+
|      / \   Temp:         |
|     | o |  25.5 C         |
|      \ /                 |
|--------------------------|
|                          |
|      / \   Humi:         |
|     ( o )  60.1 %         |
|      \ /                 |
+--------------------------+

b. AI 生成程式的 Prompt 範例

"請為 Arduino Uno 撰寫一支完整的程式碼,使用 U8g2 程式庫驅動 SSD1306 I2C 128x64 OLED 顯示器。程式需連接 DHT11 感測器 (DATA 腳位連接到 Arduino 的 Pin 2)。請設計一個專業的溫濕度儀表板介面,畫面分為上下兩區:
1. 溫度區:左側在 (5, 30) 處顯示一個大型溫度計圖示 (使用 u8g2_font_open_iconic_weather_4x_t 字型,字元 'A')。右側在 (40, 20) 處顯示標籤 'Temp:',在 (40, 40) 處顯示溫度數值 (XX.X),並在 (95, 40) 處顯示 '°C' 單位。數值和標籤請使用 u8g2_font_logisoso16_tr 字型,'°' 符號請使用 drawGlyph(x, y, 176) 繪製。
2. 濕度區:左側在 (5, 62) 處顯示一個大型水滴圖示 (使用 u8g2_font_open_iconic_weather_4x_t 字型,字元 'E')。右側在 (40, 60) 處顯示標籤 'Humi:',在 (40, 80) 處顯示濕度數值 (XX.X),並在 (95, 80) 處顯示 '%' 單位。數值和標籤請使用 u8g2_font_logisoso16_tr 字型。
程式應使用非阻塞延遲 (millis()) 每 2 秒更新一次 DHT11 的數據並顯示在螢幕上。"

c. 程式碼範例

#include <Arduino.h>
#include <U8g2lib.h>
#include <Wire.h>
#include "DHT.h"

#define DHTPIN 2
#define DHTTYPE DHT11

U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
DHT dht(DHTPIN, DHTTYPE);

unsigned long previousMillis = 0;
const long interval = 2000;
float humidity, temp_c;

void setup(void) {
  dht.begin();
  u8g2.begin();
}

void loop(void) {
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;
    float h = dht.readHumidity();
    float t = dht.readTemperature();
    if (!isnan(h) && !isnan(t)) {
      humidity = h;
      temp_c = t;
    }
  }

  u8g2.firstPage();
  do {
    // --- 繪製溫度區 ---
    u8g2.setFont(u8g2_font_open_iconic_weather_4x_t);
    u8g2.drawGlyph(5, 30, 'A'); // 溫度計圖示

    u8g2.setFont(u8g2_font_logisoso16_tr); // 使用較大的藝術字體
    char tempString[10];
    dtostrf(temp_c, 4, 1, tempString); 

    u8g2.drawStr(40, 20, "Temp:");
    u8g2.drawStr(40, 40, tempString);
    u8g2.drawGlyph(95, 40, 176); // '°' 符號
    u8g2.drawStr(105, 40, "C");

    // --- 繪製濕度區 ---
    u8g2.setFont(u8g2_font_open_iconic_weather_4x_t);
    u8g2.drawGlyph(5, 62, 'E'); // 水滴圖示

    u8g2.setFont(u8g2_font_logisoso16_tr);
    char humiString[10];
    dtostrf(humidity, 4, 1, humiString);

    u8g2.drawStr(40, 60, "Humi:");
    u8g2.drawStr(40, 80, humiString); // 此處 y 座標調整以配合字體
    u8g2.drawStr(95, 80, "%");

  } while (u8g2.nextPage());
}

3. 總結

透過以上五個螺旋式範例,我們從最簡單的文字顯示開始,逐步加入了圖示、中文、視覺化圖表,最終完成了一個精美的儀表板。這個過程不僅讓您學會了 U8g2 的各種功能,更重要的是,它展示了一種迭代開發的思維模式:從一個最小可行性產品 (MVP) 開始,逐步增加功能,直到達成最終目標。

現在,您已經具備了使用 Arduino 和 OLED 顯示器創造各種客製化資訊面板的能力。下一步,您可以嘗試結合這些範例的元素,例如,創造一個帶有中文標籤和長條圖的介面,將您的創客專案提升到新的水平。


4. 深入探討:U8g2 的核心優勢

4.1. Page Buffer 機制:為何使用 u8g2.firstPage() / nextPage() 迴圈?

您可能已經注意到,所有 U8g2 的繪圖指令都必須放在一個 do-while 迴圈中。這並非偶然,而是 U8g2 設計的核心,也是它能在像 Arduino Uno 這樣記憶體極小的微控制器上高效運作的秘密。

問題:有限的記憶體
一台 Arduino Uno 只有 2KB (2048 bytes) 的 SRAM (靜態隨機存取記憶體)。如果要一次性在記憶體中準備好整個 128x64 畫面的資料,需要 (128 * 64) / 8 = 1024 bytes 的空間。這就佔掉了整台裝置一半的記憶體!剩餘的空間非常小,很容易導致程式不穩定或崩潰。

解決方案:Page Buffer 機制
為了解決這個問題,U8g2 採用了巧妙的 Page Buffer (頁面緩衝) 機制。它不在 Arduino 的記憶體中建立一個完整的畫面緩衝區,而是只建立一個小得多的「頁面」緩衝區(例如 128 或 256 bytes)。

u8g2.firstPage()u8g2.nextPage() 就是這個機制的執行者。整個繪圖過程如下:
1. u8g2.firstPage():準備繪製第一個頁面。
2. do { ... }:您放在迴圈中的所有繪圖程式碼會被執行一次。U8g2 會計算哪些繪圖指令的結果落在目前這個小小的頁面範圍內,並將其存入緩-衝區。
3. while (u8g2.nextPage()):將目前這個頁面的緩衝區內容傳送到 OLED 螢幕上,然後準備下一個頁面,並再次回到 do 開頭,重新執行一次所有的繪圖程式碼
4. 這個過程會不斷重複,直到整個螢幕的所有頁面都被繪製完成為止。

您可以把它想像成用一個小小的「郵票」去蓋滿一張大海報。您每次只在郵票大小的範圍內作畫,蓋完一格後,再移動到下一格,重新作畫,直到蓋滿整張海報。

結論:
U8g2 透過「犧牲 CPU 運算時間,來換取極低的記憶體佔用」。它透過重複執行繪圖指令,來避免一次性佔用大量 RAM。這就是為什麼我們能在一台小小的 Arduino Uno 上,流暢地驅動圖形化螢幕,實現複雜介面的關鍵所在。