iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0

一樣丟給claude code, 說明手邊有個0.96吋I2C小螢幕, 然後還有主板,已接上PC,請幫我做個表情的專案:

5分鐘搞定!!

Claude Code想了想,印出下列文字:

接線(SuperMini + 0.96 吋 SSD1306)

OLED 腳位-> 接到 SuperMini

GND -> GND

VCC -> 3V3(不要接 5V)

SCL -> GPIO9(板子上印 9)

SDA -> GPIO8(板子上印 8)

表情程式已經燒進板子,正在跑。序列埠顯示有找到螢幕,表情也在照順序切換。但我看不到螢幕實際畫面,請你看一下有沒有正常顯示。

這次做了:

  1. 讀晶片資訊:ESP32-C3 rev v0.4,內建 4 MB flash,MAC 是 44:b1:76:07:b3:fc。
  2. 備份原廠韌體:整片 4 MB 存在 Documents\ESP32C3\backup\factory_flash_20261002.bin,旁邊附校驗檔。之後想還原原廠狀態,可以直接刷回去。
  3. 燒錄表情程式:寫入後有比對過資料,沒有錯誤。
  4. 板子回報:在 I2C 位址 0x3C 找到螢幕,表情從「待機」切到「聆聽」,依序輪播。

備註:我想這次比較難的,就是接線的部份了,其他完全沒看到怎麼做的,連code都寫好了,Claude Code給我的結果還蠻令人滿意的!!

// ESP32-C3 SuperMini + 0.96" SSD1306 (I2C) — 動畫表情 demo
// 移植自 TechTalkies/Face-for-Xiaozhi(LVGL 版)→ Arduino + U8g2
//
// 接線:OLED VCC→3V3, GND→GND, SDA→GPIO8, SCL→GPIO9
// 序列埠指令(115200):i=待機 l=聆聽 s=說話 h=開心 d=難過 z=睡覺 a=自動輪播

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

#define PIN_SDA 8
#define PIN_SCL 9

U8G2_SSD1306_128X64_NONAME_F_HW_I2C oled(U8G2_R0, U8X8_PIN_NONE);

enum FaceState { IDLE, LISTENING, SPEAKING, HAPPY, SAD, SLEEPY, STATE_COUNT };
const char* STATE_NAME[] = {"Idle", "Listening", "Speaking", "Happy", "Sad", "Sleepy"};

const int CX = 64, CY = 32;
const int EYE_SIZE = 30;
const int EYE_OFFSET_X = 5, EYE_OFFSET_Y = 4;
const uint32_t FRAME_MS = 60;
const uint32_t AUTO_MS = 5000;

FaceState state = IDLE;
bool autoCycle = true;
uint32_t lastFrame = 0, lastSwitch = 0;

int idleOx = 0, idleOy = 0;
int blinkPhase = 0;
uint32_t speakLast = 0;
int mouthTarget = 4, mouthCur = 4;

// 以中心點畫圓角實心矩形;尺寸太小時退回直角,避免 U8g2 畫壞
void box(int cx, int cy, int w, int h, int r) {
  if (w < 1 || h < 1) return;
  int x = cx - w / 2, y = cy - h / 2;
  int maxR = min(w, h) / 2 - 1;
  if (r > maxR) r = maxR;
  if (r < 1) oled.drawBox(x, y, w, h);
  else oled.drawRBox(x, y, w, h, r);
}

void drawEyes(int eyeH, int dx, int dy, int leftShrink = 0) {
  int lh = eyeH - leftShrink;
  box(CX - EYE_SIZE - EYE_OFFSET_X + dx, CY - EYE_OFFSET_Y + dy, lh * 0.65, lh, 2);
  box(CX + EYE_SIZE + EYE_OFFSET_X + dx, CY - EYE_OFFSET_Y + dy, eyeH * 0.65, eyeH, 2);
}

void idleFace(int eyeH) {
  if (random(40) == 0) {
    idleOx = random(5) - 2;
    idleOy = random(3) - 1;
  }
  // 原版待機時眼距較窄(不加 EYE_OFFSET_X)
  box(CX - EYE_SIZE + idleOx, CY - 5 + idleOy, eyeH * 0.65, eyeH, 2);
  box(CX + EYE_SIZE + idleOx, CY - 5 + idleOy, eyeH * 0.65, eyeH, 2);
  box(CX + idleOx, CY + 17 + idleOy, 20, 7, 4);
}

void listeningFace(int eyeH) {
  drawEyes(eyeH, 0, 0, 2);  // 左眼小一點=歪頭專心聽
  box(CX - 4, CY + 17, 10, 2, 0);
}

void speakingFace(int eyeH) {
  drawEyes(eyeH, 0, 0);
  uint32_t now = millis();
  if (now - speakLast > 90 + random(60)) {
    speakLast = now;
    int r = random(100);
    mouthTarget = r < 20 ? 2 : r < 50 ? 6 : r < 80 ? 10 : 16;
  }
  if (mouthCur < mouthTarget) mouthCur += 2;
  else if (mouthCur > mouthTarget) mouthCur -= 2;
  box(CX, CY + 18, 15, mouthCur, 8);
}

// 開心:眼睛畫成 ^ ^ 的彎月(白圓減掉偏下的黑圓),嘴巴笑弧
void happyFace(int eyeH) {
  int ex[2] = {CX - EYE_SIZE - EYE_OFFSET_X, CX + EYE_SIZE + EYE_OFFSET_X};
  int ey = CY - EYE_OFFSET_Y;
  for (int i = 0; i < 2; i++) {
    if (eyeH < EYE_SIZE) {  // 眨眼時照樣壓扁
      box(ex[i], ey, 19, max(eyeH, 1), 0);
      continue;
    }
    oled.setDrawColor(1);
    oled.drawDisc(ex[i], ey + 4, 11);
    oled.setDrawColor(0);
    oled.drawDisc(ex[i], ey + 10, 11);
    oled.setDrawColor(1);
  }
  oled.drawDisc(CX, CY + 14, 9, U8G2_DRAW_LOWER_LEFT | U8G2_DRAW_LOWER_RIGHT);
}

// 難過:眼睛內側上方切掉一角(八字眉),嘴巴往下彎
void sadFace(int eyeH) {
  drawEyes(eyeH, 0, 2);
  int lx = CX - EYE_SIZE - EYE_OFFSET_X, rx = CX + EYE_SIZE + EYE_OFFSET_X;
  int top = CY - EYE_OFFSET_Y + 2 - eyeH / 2;
  oled.setDrawColor(0);
  oled.drawTriangle(lx - 12, top - 1, lx + 12, top - 1, lx + 12, top + 12);
  oled.drawTriangle(rx + 12, top - 1, rx - 12, top - 1, rx - 12, top + 12);
  oled.setDrawColor(1);
  oled.drawCircle(CX, CY + 26, 8, U8G2_DRAW_UPPER_LEFT | U8G2_DRAW_UPPER_RIGHT);
  oled.drawCircle(CX, CY + 27, 8, U8G2_DRAW_UPPER_LEFT | U8G2_DRAW_UPPER_RIGHT);
}

// 睡覺:閉眼細線 + 慢慢飄的 z
void sleepyFace() {
  drawEyes(3, 0, 4);
  box(CX, CY + 18, 8, 4, 2);
  oled.setFont(u8g2_font_6x10_tf);
  int t = (millis() / 120) % 24;
  oled.drawStr(96 + t / 3, 18 - t / 2, "z");
  if (t > 8) oled.drawStr(104 + (t - 8) / 3, 10 - (t - 8) / 3, "Z");
}

void renderFrame() {
  if (blinkPhase == 0 && random(120) == 0) blinkPhase = 1;
  int eyeH = EYE_SIZE;
  switch (blinkPhase) {
    case 1: eyeH = 4; blinkPhase = 2; break;
    case 2: eyeH = 1; blinkPhase = 3; break;
    case 3: eyeH = 6; blinkPhase = 0; break;
  }

  oled.clearBuffer();
  switch (state) {
    case IDLE:      idleFace(eyeH); break;
    case LISTENING: listeningFace(eyeH); break;
    case SPEAKING:  speakingFace(eyeH); break;
    case HAPPY:     happyFace(eyeH); break;
    case SAD:       sadFace(eyeH); break;
    case SLEEPY:    sleepyFace(); break;
    default: break;
  }
  oled.sendBuffer();
}

void setState(FaceState s) {
  state = s;
  lastSwitch = millis();
  Serial.printf("Face -> %s\n", STATE_NAME[s]);
}

void handleSerial() {
  while (Serial.available()) {
    char c = Serial.read();
    switch (c) {
      case 'i': autoCycle = false; setState(IDLE); break;
      case 'l': autoCycle = false; setState(LISTENING); break;
      case 's': autoCycle = false; setState(SPEAKING); break;
      case 'h': autoCycle = false; setState(HAPPY); break;
      case 'd': autoCycle = false; setState(SAD); break;
      case 'z': autoCycle = false; setState(SLEEPY); break;
      case 'a': autoCycle = true; Serial.println("Auto cycle ON"); break;
    }
  }
}

void setup() {
  Serial.begin(115200);
  Wire.begin(PIN_SDA, PIN_SCL);
  Wire.setClock(400000);

  // 先掃 I2C,接錯線時序列埠會直接告訴你
  delay(500);
  bool found = false;
  for (uint8_t a = 1; a < 127; a++) {
    Wire.beginTransmission(a);
    if (Wire.endTransmission() == 0) {
      Serial.printf("I2C device at 0x%02X\n", a);
      found = true;
    }
  }
  if (!found) Serial.println("No I2C device found - check wiring (SDA=GPIO8, SCL=GPIO9)");

  oled.begin();
  randomSeed(esp_random());
  Serial.println("Commands: i l s h d z a");
  setState(IDLE);
}

void loop() {
  handleSerial();
  uint32_t now = millis();
  if (autoCycle && now - lastSwitch > AUTO_MS) {
    setState((FaceState)((state + 1) % STATE_COUNT));
  }
  if (now - lastFrame >= FRAME_MS) {
    lastFrame = now;
    renderFrame();
  }
}

https://ithelp.ithome.com.tw/upload/images/20261002/201189008hcmB6adHV.jpg
Figure 1:Smile face of ESP32-C3


上一篇
[Day 17] 迷你板 ESP32-C3 Super Mini
系列文
用 Claude 帶我 Bring Up AI 開發板:30 天人機協作實錄 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言