Code:
/*
* Made by KinCony IoT: https://www.kincony.com
* CO16 Image Display - show the supplied photograph on the onboard LCD.
* Target: ESP32-S3 CO16, ST7789 320x240 landscape.
* Uses the display initialization from the validated CO16 Cyber Show sketch.
* Install Adafruit GFX Library and its Adafruit BusIO dependency.
* Select ESP32S3 Dev Module; retain your working CO16 board settings.
* Keep image_data.h in the same folder as this sketch.
* Image is stored in flash as RGB565; no SD card, JPEG decoder or Wi-Fi.
* Full image is resized to 320x240 with no margins; aspect ratio changes.
* PCF8575 relay outputs at 0x22 are commanded OFF at startup.
*/
#include <Arduino.h>
#include <Wire.h>
#include "image_data.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);
spi_bus_config_t busConfig = {};
busConfig.sclk_io_num = kTftSclk;
busConfig.mosi_io_num = kTftMosi;
busConfig.miso_io_num = kTftMiso;
busConfig.quadwp_io_num = -1;
busConfig.quadhd_io_num = -1;
busConfig.max_transfer_sz =
kDisplayWidth * kTransferChunkHeight * sizeof(uint16_t);
esp_err_t err =
spi_bus_initialize(SPI3_HOST, &busConfig, SPI_DMA_CH_AUTO);
if (err != ESP_OK) {
Serial.printf("SPI3 initialization failed: %s\n", esp_err_to_name(err));
return false;
}
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;
};
Co16St7789 display;
void setup() {
Serial.begin(115200);
Wire.begin(8, 18);
Wire.setClock(100000);
Wire.setTimeOut(20);
// Active-low PCF8575 relay outputs: HIGH on all channels means OFF.
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 at 0x22.");
}
// Deselect all other SPI peripherals before initializing the LCD bus.
const int inactiveCs[] = {9, 13, 14, 42};
for (int pin : inactiveCs) {
digitalWrite(pin, HIGH);
pinMode(pin, OUTPUT);
}
if (!display.begin()) {
Serial.println("ST7789 initialization failed.");
while (true) delay(1000);
}
// ESP32 flash is memory mapped. present() reads the constant image into
// a small DMA-capable staging buffer, then sends it to the ST7789.
if (!display.present(kImageRgb565)) {
Serial.println("Image transfer failed.");
while (true) delay(1000);
}
Serial.println("Image displayed: 320x240 RGB565, full-screen image.");
}
void loop() {
// The LCD retains its image; no continuous redraw is necessary.
delay(1000);
}
12-CO16_Image_Display.zip (Size: 729.92 KB / Downloads: 4)
BIN file (you can use esp32 download tool download to ESP32-S3 with address 0x0 then directly to use) download:
12-CO16_Image_Display.ino.merged.zip (Size: 273.66 KB / Downloads: 3)
YouTube: https://www.youtube.com/c/KinCony
Online Store: https://shop.kincony.com
Alibaba Store: https://kincony.en.alibaba.com/
Online Store: https://shop.kincony.com
Alibaba Store: https://kincony.en.alibaba.com/

