Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[arduino code examples for CO16]-13 ST7789 TFT LCD Flash MJPEG Player
#1
Code:
/*
* Made by KinCony IoT: https://www.kincony.com
* CO16 Flash MJPEG Player: 320x240, 10 fps target, 120 frames, silent loop.
* No SD card and no filesystem upload required. Remove SD card before use.
* Video is embedded in KinConyVideo.h and uploaded with this sketch.
* Libraries: Adafruit GFX Library, Adafruit BusIO, JPEGDEC by Larry Bank.
* Board: ESP32S3 Dev Module, Flash 16MB, OPI PSRAM, Arduino-ESP32 3.x.
* Choose an application partition >= 3MB (e.g. Huge APP).
* Serial 115200: p=pause/resume, r=restart. Relays commanded OFF at startup.
*/
#include <Arduino.h>
#include <Wire.h>
#include <JPEGDEC.h>
#include <cstring>
#include "KinConyVideo.h"

#include <Adafruit_GFX.h>
#include <driver/gpio.h>
#include <driver/spi_master.h>
#include <esp_heap_caps.h>


// CO16 ST7789 (GMT020-02-7P) pin mapping from kcs_co16.h.
constexpr gpio_num_t kTftSclk = GPIO_NUM_11;
constexpr gpio_num_t kTftMosi = GPIO_NUM_10;
constexpr gpio_num_t kTftMiso = GPIO_NUM_12;
constexpr gpio_num_t kTftCs = GPIO_NUM_4;
constexpr gpio_num_t kTftDc = GPIO_NUM_0;
constexpr gpio_num_t kTftReset = GPIO_NUM_5;
constexpr gpio_num_t kTftBacklight = GPIO_NUM_40;

constexpr int16_t kDisplayWidth = 320;
constexpr int16_t kDisplayHeight = 240;
constexpr int16_t kTransferChunkHeight = 10;
constexpr uint32_t kDisplaySpiFrequency = 20 * 1000 * 1000;
constexpr uint8_t kCo16Madctl = 0x68;  // MX | MV | BGR.

constexpr uint16_t kBlack = 0x0000;
constexpr uint16_t kBlue = 0x001F;
constexpr uint16_t kRed = 0xF800;
constexpr uint16_t kGreen = 0x07E0;
constexpr uint16_t kCyan = 0x07FF;
constexpr uint16_t kMagenta = 0xF81F;
constexpr uint16_t kYellow = 0xFFE0;
constexpr uint16_t kWhite = 0xFFFF;

class Co16St7789 : public Adafruit_GFX {
public:
  Co16St7789() : Adafruit_GFX(kDisplayWidth, kDisplayHeight) {}

  bool begin() {
    gpio_reset_pin(kTftCs);
    gpio_set_direction(kTftCs, GPIO_MODE_OUTPUT);
    gpio_set_level(kTftCs, 1);

    gpio_reset_pin(kTftDc);
    gpio_set_direction(kTftDc, GPIO_MODE_OUTPUT);
    gpio_set_level(kTftDc, 1);

    gpio_reset_pin(kTftBacklight);
    gpio_set_direction(kTftBacklight, GPIO_MODE_OUTPUT);
    gpio_set_level(kTftBacklight, 0);

    // SPI3 is initialized once by setup(); only the display uses it.
    esp_err_t err;

    spi_device_interface_config_t deviceConfig = {};
    deviceConfig.clock_speed_hz = kDisplaySpiFrequency;
    deviceConfig.mode = 0;
    deviceConfig.spics_io_num = -1;
    deviceConfig.queue_size = 1;

    err = spi_bus_add_device(SPI3_HOST, &deviceConfig, &spiDevice_);
    if (err != ESP_OK) {
      Serial.printf("ST7789 SPI device setup failed: %s\n",
                    esp_err_to_name(err));
      return false;
    }

    drawBuffer_ = static_cast<uint8_t*>(heap_caps_malloc(
        kDisplayWidth * kTransferChunkHeight * sizeof(uint16_t),
        MALLOC_CAP_DMA));
    if (drawBuffer_ == nullptr) {
      Serial.println("ST7789 DMA buffer allocation failed");
      return false;
    }

    // GPIO5 is shared with the optional LoRa module. Match the working KCS
    // firmware and issue one reset pulse before initializing the controller.
    gpio_reset_pin(kTftReset);
    gpio_set_direction(kTftReset, GPIO_MODE_OUTPUT);
    gpio_set_level(kTftReset, 1);
    delay(1);
    gpio_set_level(kTftReset, 0);
    delay(1);
    gpio_set_level(kTftReset, 1);
    delay(10);

    if (!initializeController()) {
      return false;
    }

    fillScreen(kBlack);
    delay(120);
    if (!writeCommand(0x29)) {  // Display on.
      return false;
    }
    delay(120);

    gpio_set_level(kTftBacklight, 1);
    initialized_ = true;
    return true;
  }

  void drawPixel(int16_t x, int16_t y, uint16_t color) override {
    fillRect(x, y, 1, 1, color);
  }

  void writePixel(int16_t x, int16_t y, uint16_t color) override {
    fillRect(x, y, 1, 1, color);
  }

  void fillRect(int16_t x, int16_t y, int16_t width, int16_t height,
                uint16_t color) override {
    if (x < 0) {
      width += x;
      x = 0;
    }
    if (y < 0) {
      height += y;
      y = 0;
    }
    if (x + width > kDisplayWidth) {
      width = kDisplayWidth - x;
    }
    if (y + height > kDisplayHeight) {
      height = kDisplayHeight - y;
    }
    if (drawBuffer_ == nullptr || width <= 0 || height <= 0) {
      return;
    }

    const uint8_t high = color >> 8;
    const uint8_t low = color;
    for (int16_t row = y; row < y + height;
         row += kTransferChunkHeight) {
      const int16_t chunkHeight =
          min<int16_t>(kTransferChunkHeight, y + height - row);
      const size_t pixelCount = width * chunkHeight;
      for (size_t index = 0; index < pixelCount; ++index) {
        drawBuffer_[index * 2] = high;
        drawBuffer_[index * 2 + 1] = low;
      }

      if (!setWindow(x, row, x + width, row + chunkHeight) ||
          !writePixels(drawBuffer_, pixelCount * sizeof(uint16_t))) {
        Serial.println("ST7789 pixel transfer failed");
        return;
      }
    }
  }

  void writeFillRect(int16_t x, int16_t y, int16_t width, int16_t height,
                     uint16_t color) override {
    fillRect(x, y, width, height, color);
  }

  // Copy a fully rendered RGB565 frame into the DMA staging buffer.
  bool present(const uint16_t* pixels) {
    if (!initialized_ || pixels == nullptr) return false;
    for (int y = 0; y < kDisplayHeight; y += kTransferChunkHeight) {
      const int rows = min<int>(kTransferChunkHeight, kDisplayHeight - y);
      const int count = kDisplayWidth * rows;
      for (int i = 0; i < count; ++i) {
        uint16_t c = pixels[y * kDisplayWidth + i];
        drawBuffer_[2 * i] = c >> 8;
        drawBuffer_[2 * i + 1] = c & 0xFF;
      }
      if (!setWindow(0, y, kDisplayWidth, y + rows) ||
          !writePixels(drawBuffer_, count * 2)) return false;
    }
    return true;
  }

  bool initialized() const { return initialized_; }

private:
  bool transmit(const void* data, size_t length, int dcLevel) {
    if (data == nullptr || length == 0) {
      return true;
    }

    gpio_set_level(kTftDc, dcLevel);
    spi_transaction_t transaction = {};
    transaction.length = length * 8;
    transaction.tx_buffer = data;
    const esp_err_t err = spi_device_polling_transmit(spiDevice_, &transaction);
    if (err != ESP_OK) {
      Serial.printf("ST7789 SPI transfer failed: %s\n", esp_err_to_name(err));
      return false;
    }
    return true;
  }

  bool writeCommand(uint8_t command) {
    gpio_set_level(kTftCs, 0);
    const bool success = transmit(&command, sizeof(command), 0);
    gpio_set_level(kTftCs, 1);
    return success;
  }

  bool writeCommandData(uint8_t command, const uint8_t* data, size_t length) {
    gpio_set_level(kTftCs, 0);
    const bool success = transmit(&command, sizeof(command), 0) &&
                         transmit(data, length, 1);
    gpio_set_level(kTftCs, 1);
    return success;
  }

  bool setWindow(int16_t x1, int16_t y1, int16_t x2, int16_t y2) {
    const uint8_t columns[] = {
        static_cast<uint8_t>(x1 >> 8), static_cast<uint8_t>(x1),
        static_cast<uint8_t>((x2 - 1) >> 8), static_cast<uint8_t>(x2 - 1),
    };
    const uint8_t rows[] = {
        static_cast<uint8_t>(y1 >> 8), static_cast<uint8_t>(y1),
        static_cast<uint8_t>((y2 - 1) >> 8), static_cast<uint8_t>(y2 - 1),
    };
    return writeCommandData(0x2A, columns, sizeof(columns)) &&
           writeCommandData(0x2B, rows, sizeof(rows));
  }

  bool writePixels(const uint8_t* data, size_t length) {
    constexpr uint8_t kRamWrite = 0x2C;
    gpio_set_level(kTftCs, 0);
    const bool success = transmit(&kRamWrite, sizeof(kRamWrite), 0) &&
                         transmit(data, length, 1);
    gpio_set_level(kTftCs, 1);
    return success;
  }

  bool initializeController() {
    const uint8_t madctl[] = {kCo16Madctl};
    const uint8_t displayFunction[] = {0x0A, 0x82};
    const uint8_t pixelFormat[] = {0x55};
    const uint8_t porchControl[] = {0x0C, 0x0C, 0x00, 0x33, 0x33};
    const uint8_t gateControl[] = {0x35};
    const uint8_t vcom[] = {0x28};
    const uint8_t lcmControl[] = {0x0C};
    const uint8_t vdvVrhEnable[] = {0x01, 0xFF};
    const uint8_t vrh[] = {0x10};
    const uint8_t vdv[] = {0x20};
    const uint8_t frameRate[] = {0x0F};
    const uint8_t powerControl[] = {0xA4, 0xA1};
    const uint8_t positiveGamma[] = {
        0xD0, 0x00, 0x02, 0x07, 0x0A, 0x28, 0x32,
        0x44, 0x42, 0x06, 0x0E, 0x12, 0x14, 0x17,
    };
    const uint8_t negativeGamma[] = {
        0xD0, 0x00, 0x02, 0x07, 0x0A, 0x28, 0x31,
        0x54, 0x47, 0x0E, 0x1C, 0x17, 0x1B, 0x1E,
    };

    if (!writeCommand(0x11)) {  // Sleep out.
      return false;
    }
    delay(120);

    return writeCommand(0x13) &&
           writeCommandData(0x36, madctl, sizeof(madctl)) &&
           writeCommandData(0xB6, displayFunction, sizeof(displayFunction)) &&
           writeCommandData(0x3A, pixelFormat, sizeof(pixelFormat)) &&
           (delay(10), true) &&
           writeCommandData(0xB2, porchControl, sizeof(porchControl)) &&
           writeCommandData(0xB7, gateControl, sizeof(gateControl)) &&
           writeCommandData(0xBB, vcom, sizeof(vcom)) &&
           writeCommandData(0xC0, lcmControl, sizeof(lcmControl)) &&
           writeCommandData(0xC2, vdvVrhEnable, sizeof(vdvVrhEnable)) &&
           writeCommandData(0xC3, vrh, sizeof(vrh)) &&
           writeCommandData(0xC4, vdv, sizeof(vdv)) &&
           writeCommandData(0xC6, frameRate, sizeof(frameRate)) &&
           writeCommandData(0xD0, powerControl, sizeof(powerControl)) &&
           writeCommandData(0xE0, positiveGamma, sizeof(positiveGamma)) &&
           writeCommandData(0xE1, negativeGamma, sizeof(negativeGamma)) &&
           writeCommand(0x21);  // Display inversion on.
  }

  spi_device_handle_t spiDevice_ = nullptr;
  uint8_t* drawBuffer_ = nullptr;
  bool initialized_ = false;
};

constexpr uint32_t VIDEO_FPS = 10;
constexpr uint32_t FRAME_INTERVAL_MS = 1000 / VIDEO_FPS;
static_assert(VIDEO_FPS > 0 && VIDEO_FPS <= 30, "Choose FPS between 1 and 30");
Co16St7789 display;
JPEGDEC jpeg;
uint16_t* frameBuffer = nullptr;
bool paused = false;
uint32_t frameCount = 0, reportStart = 0, reportFrames = 0;

void haltPlayer(const char* title, const char* hint) {
  Serial.printf("%s: %s\n", title, hint);
  if (display.initialized()) {
    display.fillScreen(kBlack);
    display.setTextWrap(true);
    display.setTextSize(2);
    display.setTextColor(kRed);
    display.setCursor(8, 30);
    display.print(title);
    display.setTextSize(1);
    display.setTextColor(kWhite);
    display.setCursor(8, 85);
    display.print(hint);
  }
  while (true) delay(1000);
}

void* allocateVideoMemory(size_t size) {
  void* p = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
  if (!p) p = heap_caps_malloc(size, MALLOC_CAP_8BIT);
  return p;
}

int decodedPixels(JPEGDRAW* block) {
  if (!frameBuffer || !block || block->iBpp != 16 || block->x < 0 || block->y < 0)
    return 0;
  if (block->x >= 320 || block->y >= 240) return 0;
  int width = min<int>(block->iWidthUsed, 320 - block->x);
  int height = min<int>(block->iHeight, 240 - block->y);
  if (width <= 0 || height <= 0) return 0;
  for (int row = 0; row < height; ++row) {
    memcpy(frameBuffer + (block->y + row) * 320 + block->x,
           block->pPixels + row * block->iWidth, width * sizeof(uint16_t));
  }
  return 1;
}

void setup() {
  Serial.begin(115200);
  Wire.begin(8, 18);
  Wire.setClock(100000);
  Wire.setTimeOut(20);
  Wire.beginTransmission(0x22);
  Wire.write(uint8_t(0xFF));
  Wire.write(uint8_t(0xFF));
  if (Wire.endTransmission() != 0) Serial.println("Warning: relay OFF command not acknowledged.");

  // Keep unused SPI peripherals deselected. Remove the SD card for this demo.
  const int csPins[] = {4, 9, 13, 14, 42};
  for (int pin : csPins) { digitalWrite(pin, HIGH); pinMode(pin, OUTPUT); }
  pinMode(40, OUTPUT);
  digitalWrite(40, LOW);
  spi_bus_config_t bus = {};
  bus.sclk_io_num = 11;
  bus.mosi_io_num = 10;
  bus.miso_io_num = 12;
  bus.quadwp_io_num = -1;
  bus.quadhd_io_num = -1;
  bus.max_transfer_sz = 320 * kTransferChunkHeight * 2;
  esp_err_t err = spi_bus_initialize(SPI3_HOST, &bus, SPI_DMA_CH_AUTO);
  if (err != ESP_OK) haltPlayer("SPI init failed", esp_err_to_name(err));

  if (!display.begin()) haltPlayer("LCD init failed", "Check serial output and CO16 board selection.");
  display.setTextWrap(false);
  display.setTextSize(2);
  display.setTextColor(kCyan);
  display.setCursor(30, 90);
  display.print("KinCony MJPEG");
  display.setTextSize(1);
  display.setCursor(30, 122);
  display.print("Loading slogan video ...");

  frameBuffer = static_cast<uint16_t*>(allocateVideoMemory(320 * 240 * 2));
  if (!frameBuffer)
    haltPlayer("Not enough RAM", "Enable OPI PSRAM for ESP32-S3 N16R8 and upload again.");
  Serial.printf("Embedded video: %u bytes, %lu frames\n",
                unsigned(sizeof(kVideoData)), static_cast<unsigned long>(kVideoFrameCount));
  reportStart = millis();
  Serial.println("Player ready. 10 fps target; p=pause/resume, r=restart.");
}

void loop() {
  while (Serial.available()) {
    char c = Serial.read();
    if (c == 'p') paused = !paused;
    if (c == 'r') {
      frameCount = 0;
      paused = false;
    }
  }
  if (paused) {
    reportStart = millis(); reportFrames = 0;
    delay(10); return;
  }
  uint32_t started = millis();
  if (frameCount >= kVideoFrameCount) frameCount = 0;
  uint32_t offset = kFrameOffsets[frameCount];
  uint32_t end = kFrameOffsets[frameCount + 1];
  if (end <= offset || end > sizeof(kVideoData))
    haltPlayer("Bad frame index", "Re-upload the complete firmware package.");
  if (!jpeg.openFLASH(kVideoData + offset, int(end - offset), decodedPixels))
    haltPlayer("JPEG open failed", "Check JPEGDEC library and embedded video data.");
  if (jpeg.getWidth() != 320 || jpeg.getHeight() != 240) {
    jpeg.close();
    haltPlayer("Wrong frame size", "Convert the video to exactly 320x240 pixels.");
  }
  jpeg.setPixelType(RGB565_LITTLE_ENDIAN);
  memset(frameBuffer, 0, 320 * 240 * 2);
  int decoded = jpeg.decode(0, 0, 0);
  int jpegError = jpeg.getLastError();
  jpeg.close();
  if (!decoded) {
    Serial.printf("JPEG decode error: %d\n", jpegError);
    haltPlayer("JPEG decode failed", "Check video integrity and JPEGDEC library.");
  }
  // Present the decoded full frame to the display.
  if (!display.present(frameBuffer)) haltPlayer("LCD write failed", "Check power and SPI connection.");
  ++frameCount;
  ++reportFrames;
  uint32_t elapsed = millis() - started;
  if (elapsed < FRAME_INTERVAL_MS) delay(FRAME_INTERVAL_MS - elapsed);
  else delay(1); // Yield; do not skip frames or run catch-up bursts.
  uint32_t now = millis();
  if (now - reportStart >= 5000) {
    Serial.printf("Playback %.1f fps, read/decode/display %lu ms, frame %lu\n",
                  reportFrames * 1000.0f / (now - reportStart),
                  static_cast<unsigned long>(elapsed), static_cast<unsigned long>(frameCount));
    reportStart = now; reportFrames = 0;
  }
}
arduino ino file download: 

.zip   13-CO16_Flash_MJPEG_Player.zip (Size: 8.21 MB / Downloads: 2)
BIN file (you can use esp32 download tool download to ESP32-S3 with address 0x0 then directly to use) download:

.zip   13-CO16_Flash_MJPEG_Player.ino.merged.zip (Size: 1.05 MB / Downloads: 4)
Reply


Forum Jump:


Users browsing this thread:
1 Guest(s)