65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 GFX 和 ESP32 驱动多个屏幕

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2022年4月22日

MIT

4分钟阅读

viewsIcon

8542

downloadIcon

85

如何在 ESP32 上使用 GFX 同时驱动多个屏幕。

multi_screen project

引言

GFX 是一个功能齐全的图形库,专为 ESP32 设计,但它也可以在其他地方运行。它提供了诸如 True Type 字体支持、JPG 加载和 Alpha 混合等功能。

我们将演示我刚才提到的所有功能,并且同时在两个屏幕上进行。当一个屏幕是用于补充大显示器的小状态屏幕时,或者当主显示器是电子墨水屏并且您需要一个响应式屏幕来进行用户输入时,驱动两个屏幕可能会特别有用。

概念

由于 GFX 的编排方式,它可以驱动您可以合理连接到 MCU 的任意数量的屏幕。您所要做的就是为每个屏幕声明一个总线类型和使用它的驱动程序类型实例化,就像您只使用一个屏幕一样,只不过您要执行两次。

更棘手的部分是实际绘图,因为尝试从多个线程使用 GFX 并不是一个好主意,因为在大多数情况下,GFX 并不排斥多线程,但它本身并没有做任何事情来使其成为线程安全的。它几乎是无状态的,所以在一定程度上是线程安全的,但由于您的总线接口可能不是,您仍然会遇到麻烦,试图从多个线程使用 GFX。

引入协作式多任务处理。我们将采用轮询调度方式安排我们的动画,使得奇数帧绘制到一个屏幕,偶数帧绘制到另一个屏幕。

构建这个大杂烩

它看起来是这样的

必备组件

您需要一个 ESP32 WROOM 或 WROVER。您需要 Platform IO。您需要一个 ILI9341 显示屏和一个 SSD1306。您可以更改这些 - 您所要做的就是更换驱动程序和可能的总线,然后您可以连接任何您想要的。所有的绘图代码本身几乎都是设备无关的。

接线

我们正在为我们的屏幕使用默认的 VSPI 主机和主 I2C 主机引脚。

将 ILI9341 的 SPI 连接到 MOSI 23、MISO 19 和 SCLK 18。DC 连接到 2,RST 连接到 4,BL 连接到 14。VCC 是 3.3v。

对于 SSD1306,将 SDA 连接到 21,SCL 连接到 22。VCC 是 3.3v。

platformio.ini

在这里,我们只包含我们的驱动程序,更新编译器使用的 C++ 标准,并包含一些其他用于串行监视器波特率等事项的样板代码。

[env:esp32-demo]
platform = espressif32
board = node32s
framework = arduino
monitor_speed = 115200
upload_speed = 921600
build_unflags=-std=gnu++11
build_flags=-std=gnu++14
lib_deps = 
    codewitch-honey-crisis/htcw_ili9341
    codewitch-honey-crisis/htcw_ssd1306
lib_ldf_mode = deep

main.cpp

这是我们所有工作的核心,所以我们将对其进行详细介绍。首先,我们包含头文件并导入命名空间 - 这都是相当直接的。

#include <Arduino.h>

// the TFT IO bus library
// used by the drivers below
#include <tft_io.hpp>

// the ILI9341 driver
#include <ili9341.hpp>
// the SSD1306 driver
#include <ssd1306.hpp>

// GFX (for C++14)
#include <gfx_cpp14.hpp>

// our truetype font
#include "DEFTONE.hpp"
// color jpg image
#include "image.h"
// b&w jpg image
#include "image3.h"

// import driver namespace
using namespace arduino;
// import GFX namespace
using namespace gfx;

现在 - 在您自己的项目中,这可以是可选的 - 有一些宏定义了我们的设备驱动程序的各种硬件映射和配置。

// wiring is as follows for the ILI9341 display
// MOSI 23
// MISO 19
// SCLK 18
// VCC 3.3v
// see below for additional pins:
#define LCD1_CS 5
#define LCD1_DC 2
#define LCD1_RST 4
#define LCD1_BL 14

#define LCD1_WRITE_SPEED 400 // 400% of 10MHz = 40MHz
#define LCD1_READ_SPEED 200 // 200% of 10MHz = 20MHz
// you may need to change this to 1 if your screen is upside down
#define LCD1_ROTATION 3
// if you don't see any backlight, or any display
// try changing this to false
#define LCD1_BL_HIGH true

// wiring is as follows for the SSD1306 display
// SCL 22
// SDA 21
// VCC 3.3v
#define LCD2_WIDTH 128
#define LCD2_HEIGHT 64
#define LCD2_3_3v true
#define LCD2_ADDRESS 0x3C
// if your screen isn't working, change
// this to 400:
#define LCD2_WRITE_SPEED 800 // 800% of 100KHz = 800KHz
// change this to 1 if your screen is upside down
#define LCD2_ROTATION 3
#define LCD2_BIT_DEPTH 8

写速度和读速度可以解释一下。我们总线的这些值以基础值的百分比指定。基础值因总线类型而异。对于 SPI,基础值是 10MHz。对于 I2C,基础值是 100KHz。请注意,许多(如果不是大多数)SSD1306 可以在 800KHz 的 I2C 上正常工作,但有些则不行。如果您在该屏幕上遇到问题,请尝试将速度减半。

接下来,我们声明我们的总线。我们需要一个用于 SPI,一个用于 I2C。由于我们使用的是默认引脚,并且我们没有进行 DMA 传输等高级操作,因此我们可以让这些声明保持简洁。

using ili9341_bus_t = tft_spi<VSPI, LCD1_CS>;
using ssd1306_bus_t = tft_i2c<>;

对于 SPI 主机,我们只需要告诉它使用 VSPI 和我们的 ILI9341 LCD 的 CS 线 (5),而对于 I2C,我们根本不需要指定任何参数!默认主机是零,也就是我们想要的。

现在我们声明我们的驱动程序,为了方便访问,声明了两个 color<> 伪枚举 - 每个屏幕在原生像素类型中都有一个。

using screen1_t = ili9341<LCD1_DC, 
                          LCD1_RST, 
                          LCD1_BL, 
                          ili9341_bus_t, 
                          LCD1_ROTATION, 
                          LCD1_BL_HIGH, 
                          LCD1_WRITE_SPEED, 
                          LCD1_READ_SPEED>;

using screen2_t = ssd1306<LCD2_WIDTH, 
                          LCD2_HEIGHT, 
                          ssd1306_bus_t, 
                          LCD2_ROTATION, 
                          LCD2_BIT_DEPTH, 
                          LCD2_ADDRESS, 
                          LCD2_3_3v, 
                          LCD2_WRITE_SPEED>;

// for easy access to x11 colors in the screen's native format
using color1_t = color<typename screen1_t::pixel_type>;
using color2_t = color<typename screen2_t::pixel_type>;

现在我们有了全局变量,包括两个屏幕的实例化。

// declare the screens
screen1_t screen1;
screen2_t screen2;

// frame counter
int frame;

// title text
const char* text = "ESP32";

现在是精彩部分!下面的例程会在屏幕上绘制一个随机颜色、随机形状,并进行 Alpha 混合。

// draw a random alpha blended shape
template <typename Destination>
void draw_alpha(Destination& lcd) {
    // randomize
    randomSeed(millis());
    // declare a pixel with an alpha channel
    rgba_pixel<32> px;
    // points for a triangle
    spoint16 tpa[3];
    // maximum shape width
    const uint16_t sw =
        min(lcd.dimensions().width, lcd.dimensions().height) / 4;
    // set each channel to a random value
    // note that the alpha channel is ranged differently
    px.channel<channel_name::R>((rand() % 256));
    px.channel<channel_name::G>((rand() % 256));
    px.channel<channel_name::B>((rand() % 256));
    px.channel<channel_name::A>(50 + rand() % 156);
    // create a rectangle of a random size bounding the shape 
    srect16 sr(0, 0, rand() % sw + sw, rand() % sw + sw);
    // offset it to a random location
    sr.offset_inplace(rand() % (lcd.dimensions().width - sr.width()),
                      rand() % (lcd.dimensions().height - sr.height()));
    // choose a random shape to draw
    switch (rand() % 4) {
        case 0:
            draw::filled_rectangle(lcd, sr, px);
            break;
        case 1:
            draw::filled_rounded_rectangle(lcd, sr, .1, px);
            break;
        case 2:
            draw::filled_ellipse(lcd, sr, px);
            break;
        case 3:
            // create a triangle polygon
            tpa[0] = {int16_t(((sr.x2 - sr.x1) / 2) + sr.x1), sr.y1};
            tpa[1] = {sr.x2, sr.y2};
            tpa[2] = {sr.x1, sr.y2};
            spath16 path(3, tpa);
            draw::filled_polygon(lcd, path, px);
            break;
    }
}

请注意,它是一个模板方法,接受 typename Destination 作为单个参数。这样它就可以与任何一个显示屏一起工作。GFX 中没有用于绘图目标(如显示屏)的通用基类型。为了接受一个绘图目标,比如一个绘图目的地,它必须是一个模板参数,并且必须作为相同类型的成员参数传递。注释应该清楚地说明它在做什么,特别是如果您之前使用过 GFX。

setup() 方法很简单。

void setup() {
    Serial.begin(115200);
    frame = 0;
    // fill the screens just so we know they're alive
    // (not really necessary)
    screen1.fill(screen1.bounds(), color1_t::white);
    screen2.fill(screen2.bounds(), color2_t::white);
}

最后,loop() 是我们执行所有工作的函数。

void loop() {
    if (!frame) { // first frame
        // prepare to draw the text for screen 1
        const float text_scale1 = DEFTONE_ttf.scale(80);
        // measure the text
        const ssize16 text_size1 = 
          DEFTONE_ttf.measure_text({32767, 32767}, 
                                    {0, 0}, 
                                    text, 
                                    text_scale1);
        // center it
        const srect16 text_rect1 = 
          text_size1.bounds().center((srect16)screen1.bounds());
        // prepare to draw the image for screen 1
        // ensure stream is at beginning since JPG loading doesn't seek
        image_jpg_stream.seek(0);
        size16 isz;
        if (gfx_result::success == 
              jpeg_image::dimensions(&image_jpg_stream, &isz)) {
            // start back at the beginning
            image_jpg_stream.seek(0);
            // draw them both
            draw::image(screen1, 
                        isz.bounds().center(screen1.bounds()), 
                        &image_jpg_stream);
            draw::text(screen1, 
                      text_rect1, 
                      {0, 0}, 
                      text, 
                      DEFTONE_ttf, 
                      text_scale1, 
                      color2_t::black, 
                      color2_t::white, 
                      true);
        }
        // prepare to draw the text for screen 2
        const float text_scale2 = DEFTONE_ttf.scale(30);
        // measure the text
        const ssize16 text_size2 = 
          DEFTONE_ttf.measure_text({32767, 32767}, 
                                    {0, 0}, 
                                    text, 
                                    text_scale2);
        // center it
        const srect16 text_rect2 = 
          text_size2.bounds().center((srect16)screen2.bounds());
        // prepare to draw the image for screen 1
        // ensure stream is at beginning since JPG loading doesn't seek
        image3_jpg_stream.seek(0);
        if (gfx_result::success == 
              jpeg_image::dimensions(&image3_jpg_stream, &isz)) {
            // start back at the beginning
            image3_jpg_stream.seek(0);
            // suspend so we don't see the JPG being painted
            draw::suspend(screen2);
            // draw them both
            draw::image(screen2, 
                        isz.bounds().center(screen2.bounds()), 
                        &image3_jpg_stream);
            draw::text(screen2, 
                        text_rect2, 
                        {0, 0}, 
                        text, 
                        DEFTONE_ttf, 
                        text_scale2, 
                        color2_t::black, 
                        color2_t::white, 
                        true, 
                        true);
            draw::resume(screen2);
        }
    }
    // on even frames we draw to screen 1
    // on odd frames we draw to screen 2
    if (frame & 1) {
        draw_alpha(screen2);
    } else {
        draw_alpha(screen1);
    }
    // once we have about 30 per screen start over
    if (frame < 60) {
        ++frame;
    } else {
        frame = 0;
    }
}

这里有几件事情正在发生,但有三个主要阶段:第一阶段,通过加载 JPG 和显示文本来绘制帧背景。之后,下一阶段是,每个连续的帧在其中一个屏幕上绘制一个随机的 Alpha 混合形状,这取决于帧是奇数还是偶数。60 帧后,最后一个阶段是简单地重置,我们重新开始。

结论

正如您所见,驱动一个屏幕与驱动两个屏幕在根本上没有区别,除了您的时间管理 - 如何在两个屏幕之间分配资源,使它们都显得响应迅速。

https://honeythecodewitch.com/gfx 获取 GFX 的最新文档。

历史

  • 2022 年 4 月 22 日 - 首次提交
© . All rights reserved.