Arduino:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | #include <SPI.h> #include <Adafruit_GFX.h> #include <Adafruit_ST7789.h> #include "driver/i2s.h" // ===== TFT pins (OTTO GO) ===== #define TFT_MOSI 19 #define TFT_SCLK 18 #define TFT_CS 5 #define TFT_DC 16 #define TFT_RST 23 // ===== I2S MEMS Mic pins (OTTO GO) ===== #define MIC_I2S_SCK 13 // I2S BCLK/SCK #define MIC_I2S_WS 15 // I2S WS/LRCK #define MIC_I2S_SD 12 // I2S SD (DATA IN) Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST); static inline uint16_t RGB565(uint8_t r, uint8_t g, uint8_t b) { return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3); } // ================== 虎嘴繪圖 ================== void drawMuzzleBase(int cx, int cy) { uint16_t furLight = RGB565(235, 228, 210); uint16_t shadow = RGB565(160, 150, 135); uint16_t dotDark = RGB565(70, 70, 70); tft.fillScreen(ST77XX_BLACK); tft.fillCircle(cx - 55, cy + 10, 55, furLight); tft.fillCircle(cx + 55, cy + 10, 55, furLight); tft.fillRoundRect(cx - 90, cy - 25, 180, 110, 28, furLight); tft.fillRoundRect(cx - 85, cy + 60, 170, 22, 12, shadow); tft.fillRoundRect(cx - 40, cy + 55, 80, 10, 5, RGB565(120,110,100)); int dotY1 = cy + 6; int dotY2 = cy + 26; for (int k = 0; k < 3; k++) { tft.fillCircle(cx - 55 - k * 12, dotY1 + k * 2, 2, dotDark); tft.fillCircle(cx - 55 - k * 12, dotY2 + k * 2, 2, dotDark); tft.fillCircle(cx + 55 + k * 12, dotY1 + k * 2, 2, dotDark); tft.fillCircle(cx + 55 + k * 12, dotY2 + k * 2, 2, dotDark); } } void drawMouthClosed(int cx, int cy) { uint16_t lineDark = RGB565(40, 40, 40); tft.drawLine(cx, cy - 5, cx, cy + 20, lineDark); tft.drawLine(cx - 1, cy - 5, cx - 1, cy + 20, RGB565(80,80,80)); int mouthY = cy + 18; for (int i = 0; i < 34; i++) { int y = mouthY + (i * i) / 95; tft.drawPixel(cx - i, y, lineDark); tft.drawPixel(cx - i, y + 1, lineDark); tft.drawPixel(cx + i, y, lineDark); tft.drawPixel(cx + i, y + 1, lineDark); } tft.fillCircle(cx - 36, mouthY + 12, 3, lineDark); tft.fillCircle(cx + 36, mouthY + 12, 3, lineDark); tft.fillRoundRect(cx - 10, mouthY + 20, 20, 4, 2, RGB565(90,80,70)); } void drawMouthOpen(int cx, int cy) { uint16_t lineDark = RGB565(40, 40, 40); uint16_t mouthIn = RGB565(15, 15, 15); uint16_t tongue = RGB565(210, 120, 120); tft.drawLine(cx, cy - 5, cx, cy + 10, lineDark); tft.drawLine(cx - 1, cy - 5, cx - 1, cy + 10, RGB565(80,80,80)); int mouthY = cy + 16; for (int i = 0; i < 36; i++) { int y = mouthY + (i * i) / 85; tft.drawPixel(cx - i, y, lineDark); tft.drawPixel(cx - i, y + 1, lineDark); tft.drawPixel(cx + i, y, lineDark); tft.drawPixel(cx + i, y + 1, lineDark); } tft.fillCircle(cx - 38, mouthY + 14, 3, lineDark); tft.fillCircle(cx + 38, mouthY + 14, 3, lineDark); int w = 90, h = 45; int x = cx - w / 2; int y0 = cy + 28; tft.fillRoundRect(x, y0, w, h, 18, mouthIn); tft.fillCircle(cx, y0 + h, 22, mouthIn); tft.fillRoundRect(cx - 22, y0 + 18, 44, 20, 10, tongue); tft.fillCircle(cx, y0 + 36, 14, tongue); } bool mouthOpen = false; void drawTigerMouth(bool open) { int cx = tft.width() / 2; int cy = tft.height() / 2; drawMuzzleBase(cx, cy); if (open) drawMouthOpen(cx, cy); hookupLabel(open); } void hookupLabel(bool open){ tft.setTextSize(2); tft.setTextColor(ST77XX_YELLOW, ST77XX_BLACK); tft.setCursor(10, 10); tft.print(open ? "MOUTH: OPEN " : "MOUTH: CLOSE"); } // ================== 麥克風 I2S 讀取 + 拍手偵測 ================== // 使用 I2S_NUM_0 做 RX const i2s_port_t MIC_PORT = I2S_NUM_0; // 取樣參數 static const int SAMPLE_RATE = 16000; static const int FRAME_SAMPLES = 256; // 約 16ms static int32_t micBuf[FRAME_SAMPLES]; // 拍手偵測狀態 float noiseFloor = 2000.0f; // 自動學習的環境噪音能量 unsigned long lastClapMs = 0; const unsigned long CLAP_COOLDOWN_MS = 350; // 拍手冷卻,避免連發 void micInit() { i2s_config_t cfg = { .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX), .sample_rate = SAMPLE_RATE, .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT, // 很多 MEMS I2S 是 24-bit/32-bit 輸出 .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, .communication_format = I2S_COMM_FORMAT_I2S_MSB, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 6, .dma_buf_len = 256, .use_apll = false, .tx_desc_auto_clear = false, .fixed_mclk = 0 }; i2s_pin_config_t pin_cfg = { .bck_io_num = MIC_I2S_SCK, .ws_io_num = MIC_I2S_WS, .data_out_num = I2S_PIN_NO_CHANGE, .data_in_num = MIC_I2S_SD }; i2s_driver_install(MIC_PORT, &cfg, 0, NULL); i2s_set_pin(MIC_PORT, &pin_cfg); i2s_zero_dma_buffer(MIC_PORT); } // 讀一個 frame,回傳「能量」(平均絕對值) float readMicFrameEnergy() { size_t bytesRead = 0; i2s_read(MIC_PORT, (void*)micBuf, sizeof(micBuf), &bytesRead, portMAX_DELAY); int n = bytesRead / 4; // int32 if (n <= 0) return 0; // 轉成 16-bit 有效值(很多 I2S MEMS 會左對齊) // 用 abs 平均值做能量指標 uint64_t sumAbs = 0; for (int i = 0; i < n; i++) { int32_t s32 = micBuf[i]; int16_t s16 = (int16_t)(s32 >> 16); // 取高 16 位 sumAbs += (uint16_t)abs(s16); } return (float)sumAbs / (float)n; } // 簡單拍手偵測: // 1) 能量 > 動態門檻(noiseFloor 倍數 + 固定值) // 2) 且是突發(相對前一幀上升很快) // 3) 冷卻時間避免連續觸發 bool detectClap() { static float prevE = 0; float e = readMicFrameEnergy(); // 更新環境噪音(只在相對安靜時慢慢跟隨) // 避免拍手時把噪音底拉太高 if (e < noiseFloor * 1.5f) { noiseFloor = noiseFloor * 0.98f + e * 0.02f; } // 動態門檻 float thr = noiseFloor * 4.0f + 800.0f; // 你覺得太敏感/不敏感就改這兩個係數 // 突發條件 bool spike = (e > thr) && (e > prevE * 1.8f); prevE = e; unsigned long now = millis(); if (spike && (now - lastClapMs > CLAP_COOLDOWN_MS)) { lastClapMs = now; return true; } return false; } // ================== 主程式 ================== void setup() { // TFT SPI.begin(TFT_SCLK, -1, TFT_MOSI, TFT_CS); tft.init(240, 320); // 若你確定是 170x320 再改 tft.setRotation(1); // 麥克風 I2S micInit(); // 初始畫面:嘴巴閉合 mouthOpen = false; drawTigerMouth(mouthOpen); } void loop() { if (detectClap()) { mouthOpen = !mouthOpen; drawTigerMouth(mouthOpen); } } |
沒有留言:
張貼留言