惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

阮一峰的网络日志
阮一峰的网络日志
D
Darknet – Hacking Tools, Hacker News & Cyber Security
S
Schneier on Security
The Last Watchdog
The Last Watchdog
Cyberwarzone
Cyberwarzone
S
Securelist
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
C
Cyber Attacks, Cyber Crime and Cyber Security
L
Lohrmann on Cybersecurity
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 司徒正美
The Cloudflare Blog
V
V2EX
博客园_首页
博客园 - 聂微东
Vercel News
Vercel News
人人都是产品经理
人人都是产品经理
G
GRAHAM CLULEY
T
Tenable Blog
Last Week in AI
Last Week in AI
Y
Y Combinator Blog
L
LINUX DO - 最新话题
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
SecWiki News
SecWiki News
博客园 - 三生石上(FineUI控件)
S
Secure Thoughts
N
News | PayPal Newsroom
T
The Blog of Author Tim Ferriss
The GitHub Blog
The GitHub Blog
T
Troy Hunt's Blog
博客园 - 【当耐特】
Forbes - Security
Forbes - Security
H
Hacker News: Front Page
A
About on SuperTechFans
B
Blog RSS Feed
Engineering at Meta
Engineering at Meta
MongoDB | Blog
MongoDB | Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
罗磊的独立博客
D
DataBreaches.Net
P
Privacy & Cybersecurity Law Blog
Schneier on Security
Schneier on Security
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Google DeepMind News
Google DeepMind News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Jina AI
Jina AI
D
Docker
P
Proofpoint News Feed

Electrical Engineering - 标签 - This Cute World

EE 入门(一) - 电子电路基础知识
EE 入门(二) - 使用 ESP32 与 SPI 显示屏绘图、显示图片、跑贪吃蛇
於清樂 · 2023-03-05 · via Electrical Engineering - 标签 - This Cute World

之前淘货买了挺多显示屏的,本文使用的是这一块:

开发板是 ESP-WROOM-32 模组开发板。其他需要的东西:杜邦线、面包板、四个 10 K$\Omega$ 电阻、四个按键。

至于需要的依赖库,我找到如下几个 stars 数较高的支持 ILI9488 + ESP32 的显示屏驱动库:

  • Bodmer/TFT_eSPI: 一个基于 Arudino 框架的 tft 显示屏驱动,支持 STM32/ESP32 等多种芯片。
  • lv_port_esp32: lvgl 官方提供的 esp32 port,但是几百年不更新了,目前仅支持到 esp-idf v4,试用了一波被坑了,不建议使用。
  • esp-idf/peripherals/lcd: ESP 官方的 lcd 示例,不过仅支持部分常见显示屏驱动,比如我这里用的 ili9488 官方就没有。

总之强烈推荐 TFT_eSPI 这个库,很好用,而且驱动支持很齐全。

ESP32 开发有好几种方式:

  1. vscode 的 esp-idf 插件 + 官方的 esp-idf 工具
  2. vscode 的 platformio 插件 + arudino 框架

Bodmer/TFT_eSPI 这个依赖库两种方式都支持,不过看了下官方文档,仓库作者表示 ESP-IDF 的支持是其他人提供的,他不保证能用,所以稳妥起见我选择了 PlatformIO + Arduino 框架作为开发环境。

首先当然是创建一个空项目,点击 VSCode 侧栏的 PlatformIO 图标,再点击列表中的PlatformIO Core CLI 选项进入 shell 执行如下命令:

pio project init --ide=vscode -d tft_esp32_arduino

这条命令会创建一个空项目,并配置好 vscode 插件相关配置,这样就算完成了一个空的项目框架。

网上简单搜了下 ESP32 pinout,找到这张图,引脚定义与我的 ESP32 开发板完全一致,用做接线参考:

可以看到这块 ESP32 开发板有两个 SPI 端口:HSPI 跟 VSPI,这里我们使用 HSPI,那么 MOSI/MISO/SCK 三个引脚的接线必须与上图的定义完全一致。而其他引脚随便找个普通 GPIO 口接上就行。

此外背光灯的线我试了下接 GPIO 口不好使,建议直接接在 3V3 引脚上(缺点就是没法通过程序关闭背光,问题不大)。

我的接线如下:

使用 wokwi.com 制作的示意图

接线实操

线接好后需要更新下 PlatformIO 项目根目录 platformio.ini 的配置,使其显示屏引脚相关的参数与我们的接线完全对应起来,这样才能正常驱动这个显示屏。

这里我以驱动库官方提供的模板Bodmer/TFT_eSPI/docs/PlatformIO 为基础,更新了其构建参数对应的引脚,加了点注释,得到的内容如下(如果你的接线与我一致,直接抄就行):

[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
lib_deps =
  bodmer/TFT_eSPI@^2.5.0
  Bodmer/TFT_eWidget@^0.0.5
monitor_speed = 115200
build_flags =
  -Os
  -DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG
  -DUSER_SETUP_LOADED=1

  ; Define the TFT driver, pins etc here:
  ; 显示屏驱动要对得上
  -DILI9488_DRIVER=1
  # 宽度与高度
  -DTFT_WIDTH=480
  -DTFT_HEIGHT=320
  # SPI 引脚的接线方式,
  -DTFT_MISO=12
  -DTFT_MOSI=13
  # SCLK 在显示屏上对应的引脚可能叫 SCK,是同一个东西
  -DTFT_SCLK=14
  -DTFT_CS=15
  # DC 在显示屏上对应的引脚可能叫 RS 或者 DC/RS,是同一个东西
  -DTFT_DC=4
  -DTFT_RST=2
  # 背光暂时直接接在 3V3 上
  ; -DTFT_BL=27
  # 触摸,暂时不用
  ;-DTOUCH_CS=22
  -DLOAD_GLCD=1
  # 其他配置,保持默认即可
  -DLOAD_FONT2=1
  -DLOAD_FONT4=1
  -DLOAD_FONT6=1
  -DLOAD_FONT7=1
  -DLOAD_FONT8=1
  -DLOAD_GFXFF=1
  -DSMOOTH_FONT=1
  -DSPI_FREQUENCY=27000000

修好后保存修改,platformio 将会自动检测到配置文件变更,并根据配置文件下载 Arduino/ESP32 工具链,更新构建配置、拉取依赖库(建议开个全局代理,不然下载会贼慢)。

现在找几个 demo 跑跑看,新建文件 src/main.ino,从如下文件夹中随便找个 demo copy 进去然后编译上传,看看效果:

可以直接从 libdeps 中 copy examples 代码过来测试:cp .pio/libdeps/esp32dev/TFT_eSPI/examples/480\ x\ 320/TFT_Meters/TFT_Meters.ino src/main.ino

我跑出来的效果:

这需要首先将图片/文字转换成 bitmap 格式的 C 代码,可使用在线工具javl/image2cpp 进行转换,简单演示下:

注意高度与宽度调整为与屏幕大小一致,设置放缩模式,然后色彩改为 RGB565,最后上传图片、生成代码。

将生成好的代码贴到 src/test_img.h 中:

// We need this header file to use FLASH as storage with PROGMEM directive:

// Icon width and height
const uint16_t imgWidth = 480;
const uint16_t imgHeight = 320;

// 'evt_source', 480x320px
const uint16_t epd_bitmap_evt_source [] PROGMEM = {
  // 这里省略掉图片内容......
}

然后写个主程序 src/main.ino 显示图像:

#include <TFT_eSPI.h>       // Hardware-specific library

TFT_eSPI tft = TFT_eSPI();  // Invoke custom library

// Include the header files that contain the icons
#include "test_img.h"

void setup()
{
  Serial.begin(115200);
  tft.begin();
  tft.setRotation(1);	// landscape

  tft.fillScreen(TFT_BLACK);
  // Swap the colour byte order when rendering
  tft.setSwapBytes(true);

  // 显示图片
  tft.pushImage(0, 0, imgWidth, imgHeight, epd_bitmap_evt_source);

  delay(2000);
}

void loop() {}

编译上传,效果如下:

N 年前我写的第一篇博客文章,是用 C 语言写一个贪吃蛇,这里把它移植过来玩玩看~

我的旧文章地址为:贪吃蛇—C—基于easyx图形库(下):从画图程序到贪吃蛇【自带穿墙术】 , 里面详细介绍了程序的思路。

那么现在开始代码移植,TFT 屏幕前面已经接好了不需要动,要改的只有软件部分,还有就是添加上下左右四个按键的电路。

首先清空 src 文件夹,新建文件 src/main.ino,内容如下,其中主要逻辑均移植自我前面贴的文章:

#include <math.h>
#include <stdio.h>
#include <TFT_eSPI.h> // Hardware-specific library

#define WIDTH 480
#define HEIGHT 320

// 四个方向键对应的 GPIO 引脚
#define BUTTON_UP_PIN     5
#define BUTTON_LEFT_PIN   18
#define BUTTON_DOWN_PIN   19
#define BUTTON_RIGHT_PIN  21

TFT_eSPI tft = TFT_eSPI(); // Invoke custom library

typedef struct Position // 坐标结构
{
  int x;
  int y;
} Pos;

Pos SNAKE[3000] = {0};
Pos DIRECTION;
Pos EGG;
long SNAKE_LEN;

void setup()
{
  Serial.begin(115200);
  tft.begin();
  tft.setRotation(1); // landscape

  tft.fillScreen(TFT_BLACK);
  // Swap the colour byte order when rendering
  tft.setSwapBytes(true);

  // initialize the pushbutton pin as an input: the default state is LOW
  pinMode(BUTTON_UP_PIN, INPUT);
  pinMode(BUTTON_LEFT_PIN, INPUT);
  pinMode(BUTTON_DOWN_PIN, INPUT);
  pinMode(BUTTON_RIGHT_PIN, INPUT);

  init_game();
}

void loop()
{
  command(); // 获取按键消息
  move();    // 修改头节点坐标-蛇的移动
  eat_egg();
  draw(); // 作图
  eat_self();
  delay(100);
}

void init_game() {
  // 初始化小蛇
  SNAKE_LEN = 1;
  SNAKE[0].x =  random(50, WIDTH - 50); // 头节点位置随机化
  SNAKE[0].y =  random(50, HEIGHT - 50);
  DIRECTION.x = pow(-1, random()); // 初始化方向向量
  DIRECTION.y = 0;
  creat_egg();

  Serial.println("GAM STARTED, Having Fun~");
}

void creat_egg()
{
  while (true)
  {
    int ok = 0;
    EGG.x = random(50, WIDTH - 50); // 头节点位置随机化
    EGG.y = random(50, HEIGHT - 50);
    for (int i = 0; i < SNAKE_LEN; i++)
    {
      if (SNAKE[i].x == 0 && SNAKE[i].y == 0)
        continue;
      if (fabs(SNAKE[i].x - EGG.x) <= 10 && fabs(SNAKE[i].y - EGG.y) <= 10)
        ok = -1;
      break;
    }
    if (ok == 0)
      return;
  }
}

void command() // 获取按键命令命令
{
  if (digitalRead(BUTTON_LEFT_PIN) == HIGH) {
      if (DIRECTION.x != 1 || DIRECTION.y != 0)
      { // 如果不是反方向,按键才有效
        Serial.println("Turn Left!");
        DIRECTION.x = -1;
        DIRECTION.y = 0;
      }
  } else if (digitalRead(BUTTON_RIGHT_PIN) == HIGH) {
      if (DIRECTION.x != -1 || DIRECTION.y != 0)
      {
        Serial.println("Turn Right!");
        DIRECTION.x = 1;
        DIRECTION.y = 0;
      }
  } else if (digitalRead(BUTTON_UP_PIN) == HIGH) {
      if (DIRECTION.x != 0 || DIRECTION.y != 1)
      {  // 注意 Y 轴,向上是负轴,因为屏幕左上角是原点 (0,0)
        Serial.println("Turn Up!");
        DIRECTION.x = 0;
        DIRECTION.y = -1;
      }
  } else if (digitalRead(BUTTON_DOWN_PIN) == HIGH) {
      if (DIRECTION.x != 0 || DIRECTION.y != -1)
      {
        Serial.println("Turn Down!");
        DIRECTION.x = 0;
        DIRECTION.y = 1;
      }
  }
}

void move() // 修改各节点坐标以达到移动的目的
{
  // 覆盖尾部走过的痕迹
  tft.drawRect(SNAKE[SNAKE_LEN - 1].x - 5, SNAKE[SNAKE_LEN - 1].y - 5, 10, 10, TFT_BLACK);

  for (int i = SNAKE_LEN - 1; i > 0; i--)
  {
    SNAKE[i].x = SNAKE[i - 1].x;
    SNAKE[i].y = SNAKE[i - 1].y;
  }
  SNAKE[0].x += DIRECTION.x * 10; // 每次移动10pix
  SNAKE[0].y += DIRECTION.y * 10;

  if (SNAKE[0].x >= WIDTH) // 如果越界,从另一边出来
    SNAKE[0].x = 0;
  else if (SNAKE[0].x <= 0)
    SNAKE[0].x = WIDTH;
  else if (SNAKE[0].y >= HEIGHT)
    SNAKE[0].y = 0;
  else if (SNAKE[0].y <= 0)
    SNAKE[0].y = HEIGHT;
}

void eat_egg()
{
  if (fabs(SNAKE[0].x - EGG.x) <= 5 && fabs(SNAKE[0].y - EGG.y) <= 5)
  {
    // shade old egg
    tft.drawCircle(EGG.x, EGG.y, 5, TFT_BLACK);
    creat_egg();
    // add snake node
    SNAKE_LEN += 1;
    for (int i = SNAKE_LEN - 1; i > 0; i--)
    {
      SNAKE[i].x = SNAKE[i - 1].x;
      SNAKE[i].y = SNAKE[i - 1].y;
    }
    SNAKE[0].x += DIRECTION.x * 10; // 每次移动10pix
    SNAKE[0].y += DIRECTION.y * 10;
  }
}

void draw() // 画出蛇和食物
{
  for (int i = 0; i < SNAKE_LEN; i++)
  {
    tft.drawRect(SNAKE[i].x - 5, SNAKE[i].y - 5, 10, 10, TFT_BLUE);
  }
  tft.drawCircle(EGG.x, EGG.y, 5, TFT_RED);
}

void eat_self()
{
  if (SNAKE_LEN == 1)
    return;
  for (int i = 1; i < SNAKE_LEN; i++)
    if (fabs(SNAKE[i].x - SNAKE[0].x) <= 5 && fabs(SNAKE[i].y - SNAKE[0].y) <= 5)
    {
      delay(1000);
      tft.setTextColor(TFT_RED, TFT_BLACK);
      tft.drawString("GAME OVER!", 200, 150, 4);
      delay(3000);

      setup();
      break;
    }
}

代码就这么点,没几行,接下来我们来接一下按键电路,这部分是参考了 arduino 的官方文档How to Wire and Program a Button

接线方式如下,主要原理就是通过 GND 接线,使四个方向键对应的 GPIO 口默认值为低电平。当按键按下时,GPIO 口会被拉升成高电平,从而使程序识别到该按键被按下。

接线示意图如下(简单起见,省略了前面的显示屏接线部分):

使用 wokwi.com 制作的示意图

现在运行程序,效果如下(手上只有两个按键,所以是双键模式请见谅…):