0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ILI9488 3.5" 480x320 TFTモジュール上で迷路作成と探索。ムズムズ動きます。

Last updated at Posted at 2023-06-20

 SPI仕様の大画面?・高解像度(3.5" 320x480)のディスプレイをAmazonで衝動買い。せっかくなのでESP32に繋いで迷路作成と探索をさせてみました。
Maze1.jpg

ボード--- Heltec WiFi Kit 32(Heltec WiFi LoRa 32 V2)
ディスプレイ --- Dovhmoh 3.5インチ 480x320 SPIシリアルTFT LCDモジュールディスプレイ ILI9488付き(2500円弱)

ディスプレイモジュールを買ったのはいいけど動かせない💦。(ロジックIOポートの入力電圧が3.3V(TTL)ということで UNO などでは動かないようです。)色々探して、TFT_eSPIライブラリに辿り着きました。有難く参考にさせていただきます。

詳しいことは上のサイトを見てください。私の場合は次の設定で動きました。なお、ここでは関係ないですが、230行目の設定でタッチパネルとして使えました(MISOを繋ぐと動かない?とか要留意です)。

User_Setup.h
(line 54)
#define ILI9488_DRIVER     // WARNING: Do not connect ILI9488 display SDO to MISO if other devices share the SPI bus (TFT SDO does NOT 
...
// ###### EDIT THE PIN NUMBERS IN THE LINES FOLLOWING TO SUIT YOUR ESP32 SETUP   ######

// For ESP32 Dev board (only tested with ILI9341 display)
// The hardware SPI can be mapped to any pins
(line 212)
#define TFT_MISO 19
#define TFT_MOSI 23
#define TFT_SCLK 18
#define TFT_CS    5  // Chip select control pin
#define TFT_DC   27  // Data Command control pin
#define TFT_RST  32  // Reset pin (could connect to RST pin)
//#define TFT_RST  -1  // Set TFT_RST to -1 if display RESET is connected to ESP32 board RST

// For ESP32 Dev board (only tested with GC9A01 display)
// The hardware SPI can be mapped to any pins

//#define TFT_MOSI 15 // In some display driver board, it might be written as "SDA" and so on.
//#define TFT_SCLK 14
//#define TFT_CS   5  // Chip select control pin
//#define TFT_DC   27  // Data Command control pin
//#define TFT_RST  33  // Reset pin (could connect to Arduino RESET pin)
//#define TFT_BL   22  // LED back-light

#define TOUCH_CS 4     // Chip select pin (T_CS) of touch screen

なお、LEDは3.3Vに接続しました。
 肝心の迷路生成・探索アルゴリズムですが、PythonとかJavaScriptとか私には解らなくて、これまた色々探しました。やっとC言語のシンプルなものを載せてくださっているサイトを見つけました。大変感謝です。
http://studio.s1.xrea.com/prog/maze.html
このプログラムを ArduinoIDE2.1.0 に移植、ディスプレイ上で迷路作成と探索の様子がヌルヌルと観察できるようにしました。ただ、やはり再起呼び出しの階層が深くなるとスタックがオーバーフローするのか、時々

Guru Meditation Error: Core 1 panic'ed (Unhandled debug exception).
Debug exception reason: Stack canary watchpoint triggered (loopTask)

となりリセットされます。アリさんのように動き続けゴール目前でリセットがかかると何か切なさを感じます。少し長いですが以下がコードです。

9488_eSPI_Maze.ino
#include <TFT_eSPI.h>
TFT_eSPI tft = TFT_eSPI();

#define SizeX 25                     // 迷路の横幅 奇数
#define SizeY 37                     // 迷路の縦幅 奇数
int MAP[SizeX][SizeY];               // 座標属性格納用
int X[SizeX * SizeY];                // 候補座標格納用
int Y[SizeX * SizeY];                // 候補座標格納用
int SX = 1, SY = 1;                  // スタート奇数
int GX = SizeX - 2, GY = SizeY - 2;  // ゴール奇数

int i;  // 候補座標選定用カウンタ
bool a; // ゴールしたか?
int x, y;
int r;
int dx, dy;
int c;

// 迷路掘り
void dig(int x, int y) {
  c = 0;                  // 検索実行回数カウンタ
  r = random(2 ^ 32 - 1); // ランダム方向 0~2147483647

  while (c < 4) {
    switch ((r + 4 + c) % 4) {
      case 0:
        dx = 0;
        dy = -1;
        break;
      case 1:
        dx = -1;
        dy = 0;
        break;
      case 2:
        dx = 0;
        dy = 1;
        break;
      case 3:
        dx = 1;
        dy = 0;
        break;
    }

    if (x + dx * 2 <= 0 || y + dy * 2 <= 0 || x + dx * 2 >= SizeX - 1 || y + dy * 2 >= SizeY - 1 || MAP[x + dx * 2][y + dy * 2] == 0) {
      c++;                                          // 他の方向へ
    } else if (MAP[x + dx * 2][y + dy * 2] == 1) {  // 2個先壁なら掘り進む
      MAP[x + dx][y + dy] = 0;
      tft.fillRect(11 + (x + dx) * 12, 32 + (y + dy) * 12, 13, 13, TFT_WHITE);
      delay(50);
      MAP[x + dx * 2][y + dy * 2] = 0;
      x = x + dx * 2;
      y = y + dy * 2;
      tft.fillRect(11 + x * 12, 32 + y * 12, 13, 13, TFT_WHITE);
      delay(50);

      c = 0;  // 移動しそこから新たに探索する
      r = random(2 ^ 32 - 1);
    }
  }
}

void MakeMaze() {
  // マップ初期化
  for (y = 0; y < SizeY; y++) {
    for (x = 0; x < SizeX; x++) {
      MAP[x][y] = 1;  // すべて壁で埋める
    }
  }
  MAP[SX][SY] = 0;    // 通路. 最初の穴
  tft.fillRect(11 + SX * 12, 32 + SY * 12, 13, 13, TFT_WHITE);
  delay(100);
  while (1) {
    i = 0;
    for (y = 1; y <= SizeY - 2; y += 2) {
      for (x = 1; x <= SizeX - 2; x += 2) {
        if (MAP[x][y] == 0) {
          if ((y == SizeY - 2) && (x == SizeX - 2)) {
            break;
          } else if (x - 2 >= 0 && MAP[x - 2][y] == 1) {
            X[i] = x;
            Y[i] = y;
            i++;
          } else if (y - 2 >= 0 && MAP[x][y - 2] == 1) {
            X[i] = x;
            Y[i] = y;
            i++;
          } else if (x + 2 < SizeX && MAP[x + 2][y] == 1) {
            X[i] = x;
            Y[i] = y;
            i++;
          } else if (y + 2 < SizeY && MAP[x][y + 2] == 1) {
            X[i] = x;
            Y[i] = y;
            i++;
          }
        }
      }
    }

    if (i == 0) {
      break;
    } else {
      randomSeed(analogRead(37)); // pin37の情報で初期化
      r = random(2 ^ 32 - 1) % i;
      x = X[r];
      y = Y[r];
      dig(x, y);
    }
  }
}

/*経路探索*/
void Root(int x, int y) {
  MAP[x][y] = 2;  // まずは経路とする
  tft.setTextColor(TFT_MAGENTA);
  tft.setCursor(12 * (x + 1), 31 + y * 12);
  tft.print("+");
  delay(100);

  if (x == GX && y == GY) a = true;  // ゴール到達。探索終了
  
  if (a == false && MAP[x][y - 1] == 0) Root(x, y - 1); // 上
  if (a == false && MAP[x][y + 1] == 0) Root(x, y + 1); // 下
  if (a == false && MAP[x - 1][y] == 0) Root(x - 1, y); // 左
  if (a == false && MAP[x + 1][y] == 0) Root(x + 1, y); // 右

  if (a == false) { // ゴールではないし、袋小路
    MAP[x][y] = 0;  // 経路から外す
    tft.setTextColor(TFT_WHITE);
    tft.setCursor(12 * (x + 1), 31 + y * 12);
    tft.print("+");
    delay(100);
  }
}

void DrawScale() {
  tft.setTextSize(1);
  tft.setTextColor(TFT_BLACK);
  tft.fillRect(11, 32, 12 * SizeX, 12 * SizeY, TFT_BLACK);
  for (y = 0; y < SizeY; y++) {
    tft.setCursor(0, 35 + y * 12);
    tft.printf("%d", y);
  }
  for (x = 0; x < SizeX; x++) {
    tft.setCursor(12 + x * 12, 24);
    tft.printf("%d", x);
  }
}

void DrawSG() {
  tft.setTextSize(2);
  tft.setTextColor(TFT_RED);
  tft.fillRect(10 + SX * 12, 29 + SY * 12, 14, 16, TFT_GREEN);
  tft.setCursor(12 * (SX + 1), 30 + SY * 12);
  tft.print("S");
  tft.fillRect(10 + GX * 12, 31 + GY * 12, 14, 17, TFT_GREEN);
  tft.setCursor(12 * (GX + 1), 33 + GY * 12);
  tft.print("G");
}

void setup(void) {
  tft.init();
  tft.setRotation(2);
  tft.fillScreen(TFT_WHITE);
  tft.setTextColor(TFT_BLACK);
  // TFT_eSPI のライブラリ
  tft.drawString("Making & Solving Maze", 0, 0, 4);
  DrawScale();
}

void loop() {
  Serial.begin(115200);
  while (!Serial) {}
  tft.fillRect(11, 32, 12 * SizeX, 12 * SizeY, TFT_BLACK);
  while (Serial.available() == 0) {}
  String str = Serial.readStringUntil('\n');
  Serial.println(str);  // (SX,SY,GX,GY)を(3,3,21,31)等とすべて奇数で指定
                        // リターンetc.でデフォルト値
  int data[] = { 1, 1, SizeX - 2, SizeY - 2 }; // 初期化
  int vals = stringToIntValues(str, data, ',');

  Serial.printf("---------values %d\r\n", vals);
  for (int i = 0; i < vals; i++) {
    Serial.print("data[");
    Serial.print(i);
    Serial.print("]=");
    Serial.println(data[i]);
  }
  // スタート・ゴール地点セット
  if ((data[0] > 0 && data[0] < SizeX - 1) && (data[1] > 0 && data[1] < SizeY - 1) && (data[2] > 0 && data[2] < SizeX - 1) && (data[3] > 0 && data[3] < SizeY - 1)) {
    SX = data[0];
    SY = data[1];
    GX = data[2];
    GY = data[3];
  } else {
    Serial.read();
    Serial.println("Inputed data are incorrect.");
    SX = 1;
    SY = 1;
    GX = SizeX - 2;
    GY = SizeY - 2;
  }

  MakeMaze();  // 穴掘り迷路作成
  
  for (y = 0; y < SizeY; y++) { // 作成迷路描写
    for (x = 0; x < SizeX; x++) {
      if (x == SX && y == SY) {
        Serial.print("S");
      } else if (x == GX && y == GY) {
        Serial.print("G");
      } else if (MAP[x][y] == 0) {  // 通路
        Serial.print(" ");
      } else if (MAP[x][y] == 1) {  // 壁
        Serial.print("@");
      }
    }
    Serial.println("");
  }
  DrawSG();
  Serial.println("------------------Maze Producted.");
  while (Serial.read() != 'i') {}  // iキー入力を待つ
  delay(200);

  a = false; // ゴール未到達フラッグ
  Root(SX, SY);  // スタート(SX,SY)から探索開始

  for (y = 0; y < SizeY; y++) {  // 探索結果表示
    for (x = 0; x < SizeX; x++) {
      if (x == SX && y == SY) {
        Serial.print("S");
      } else if (x == GX && y == GY) {
        Serial.print("G");
      } else if (MAP[x][y] == 0) {  // 通路
        Serial.print(" ");
      } else if (MAP[x][y] == 1) {  // 壁
        Serial.print("@");
      } else if (MAP[x][y] == 2) {  // 経路
        Serial.print("+");
        tft.setTextSize(2);
        tft.setTextColor(TFT_MAGENTA);
        tft.setCursor(12 * (x + 1), 31 + y * 12);
        tft.print("+");
      }
    }
    Serial.println("");
  }
  DrawSG();
  Serial.println("----------------------Solved");
  while (Serial.read() != 'k') {}  // kキー入力を待つ
  Serial.end();                    // Clearing Buffer.
}

int stringToIntValues(String str, int val[], char delim) {
  int y = 0;  // value counter
  int x = 0;  // char counter
  char buf[8];
  char c;
  for (int i = 0; i <= str.length(); i++) {
    c = str.charAt(i);
    if (c == delim || i == str.length()) {
      buf[x] = '\0';
      val[y++] = atoi(buf); // set val. and go to next value
      x = 0;
    } else {
      buf[x++] = c;         //set char. and go to next char
    }
  }
  return y;
}

何かお気づきの点が御座いましたらお教えくださるとうれしいです。最後までご覧いただきありがとうございました。

0
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?