2026年2月15日 星期日

[OTTO GO] 動畫測試,使用RAW和SdFat 版



Python:BMP轉檔成RAW

 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
from PIL import Image, ImageOps
import struct
import glob
import os

W, H = 320, 240

def rgb_to_565(r, g, b):
    return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)

def convert_one(bmp_path):
    img = Image.open(bmp_path).convert("RGB")

    # 轉成剛好 320x240:置中裁切/縮放(避免尺寸不符)
    img = ImageOps.fit(img, (W, H), method=Image.Resampling.LANCZOS, centering=(0.5, 0.5))

    raw_path = os.path.splitext(bmp_path)[0] + ".raw"
    pix = img.load()

    with open(raw_path, "wb") as f:
        # 逐像素寫入 RGB565(Little-endian:低位元組在前)
        for y in range(H):
            for x in range(W):
                r, g, b = pix[x, y]
                v = rgb_to_565(r, g, b)
                f.write(struct.pack("<H", v))

    print(f"OK: {bmp_path} -> {raw_path}")

def main():
    # 轉換 tile*.bmp(例如 tile000.bmp)
    bmps = sorted(glob.glob("tile*.bmp"))
    if not bmps:
        print("找不到 tile*.bmp,請確認檔名例如 tile000.bmp")
        return
    for p in bmps:
        convert_one(p)

if __name__ == "__main__":
    main()

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
// ESP32 + TFT_eSPI + microSD (SdFat)
// ST7789 320x240 : RAW565 player (FAST SD read)
// Profile: SD read vs TFT push
#include <TFT_eSPI.h>
#include <SPI.h>
#include <SdFat.h>

TFT_eSPI tft = TFT_eSPI();

// ===== SD pins =====
#define SD_CS   33
#define SD_MOSI 26
#define SD_MISO 36
#define SD_SCLK 25
SPIClass sdSPI(HSPI);

// SdFat objects
SdFat sd;
FsFile f;

// ===== Display size =====
static const int W = 320;
static const int H = 240;

// ===== Playback =====
const int totalFrames = 4;
int frameDelay = 10;
const char *framePrefix = "tile";
const int digits = 3;

// ===== Block push settings =====
static const int BLOCK_H = 40;
static uint16_t blockBuf[W * BLOCK_H];

static bool drawRaw565_Profile_SdFat(const char *filename) {
  if (!f.open(filename, O_RDONLY)) {
    Serial.println("  開檔失敗");
    return false;
  }

  const uint32_t expected = (uint32_t)W * (uint32_t)H * 2u;
  if (f.fileSize() != expected) {
    Serial.printf("  檔案大小不符:%lu (應為 %lu)\n",
                  (unsigned long)f.fileSize(), (unsigned long)expected);
    f.close();
    return false;
  }

  uint32_t tReadUs = 0, tPushUs = 0;

  tft.startWrite();

  for (int y = 0; y < H; y += BLOCK_H) {
    int thisH = (y + BLOCK_H <= H) ? BLOCK_H : (H - y);
    int pxCount = W * thisH;
    int byteCount = pxCount * 2;

    uint32_t a = micros();
    int got = f.read((uint8_t*)blockBuf, byteCount);
    uint32_t b = micros();
    tReadUs += (b - a);

    if (got != byteCount) {
      Serial.println("  讀取不足");
      tft.endWrite();
      f.close();
      return false;
    }

    a = micros();
    tft.setAddrWindow(0, y, W, thisH);
    tft.pushPixels(blockBuf, pxCount);
    b = micros();
    tPushUs += (b - a);
  }

  tft.endWrite();
  f.close();

  Serial.printf("  SD讀取: %lu ms, TFT推畫: %lu ms\n",
                (unsigned long)(tReadUs / 1000),
                (unsigned long)(tPushUs / 1000));
  return true;
}

void setup() {
  Serial.begin(115200);
  delay(300);
  Serial.println("\nSdFat RAW565 開始(cfg 修正版)");

  tft.init();
  tft.setRotation(3);
  tft.setSwapBytes(false);

  sdSPI.begin(SD_SCLK, SD_MISO, SD_MOSI, SD_CS);

  // 先試 40MHz
  {
    SdSpiConfig cfg40(SD_CS, DEDICATED_SPI, SD_SCK_MHZ(40), &sdSPI);
    if (sd.begin(cfg40)) {
      Serial.println("SdFat 初始化成功!(40MHz)");
      Serial.printf("TFT: %dx%d\n", tft.width(), tft.height());
      return;
    }
  }

  // 失敗再試 20MHz
  Serial.println("SdFat 40MHz 失敗,改用 20MHz...");
  {
    SdSpiConfig cfg20(SD_CS, DEDICATED_SPI, SD_SCK_MHZ(20), &sdSPI);
    if (sd.begin(cfg20)) {
      Serial.println("SdFat 初始化成功!(20MHz)");
      Serial.printf("TFT: %dx%d\n", tft.width(), tft.height());
      return;
    }
  }

  Serial.println("SdFat 初始化失敗!");
  while (1) delay(1000);
}

void loop() {
  for (int i = 0; i < totalFrames; i++) {
    char filename[32];
    snprintf(filename, sizeof(filename), "/%s%0*d.raw", framePrefix, digits, i);

    Serial.printf("\n幀 %d/%d → %s\n", i + 1, totalFrames, filename);
    unsigned long start = millis();

    bool ok = drawRaw565_Profile_SdFat(filename);
    if (!ok) {
      tft.fillScreen(TFT_BLACK);
      tft.setTextColor(TFT_RED, TFT_BLACK);
      tft.setTextDatum(MC_DATUM);
      tft.drawString("RAW LOAD FAIL", W / 2, H / 2);
    }

    unsigned long end = millis();
    Serial.printf("  完成,花費 %lu ms\n", end - start);

    delay(frameDelay);
  }

  Serial.println("一輪結束...");
  delay(300);
}


沒有留言:

張貼留言