一樣丟給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)
表情程式已經燒進板子,正在跑。序列埠顯示有找到螢幕,表情也在照順序切換。但我看不到螢幕實際畫面,請你看一下有沒有正常顯示。
這次做了:
備註:我想這次比較難的,就是接線的部份了,其他完全沒看到怎麼做的,連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();
}
}

Figure 1:Smile face of ESP32-C3