Arnov Sharma
Published © MIT

VAULT-TEC Air Terminal

Made a Terminal PC from fallout, powered by Raspberry Pi 5

BeginnerFull instructions provided20 hours3,985
VAULT-TEC Air Terminal

Things used in this project

Hardware components

Raspberry Pi 5
Raspberry Pi 5
×1
LILYGO TTGO T DISPLAY S3 LONG
×1

Software apps and online services

Fusion
Autodesk Fusion
Arduino IDE
Arduino IDE

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)

Story

Read more

Custom parts and enclosures

KNOB FRONT

LED DIFFUSER

LID

ROBCO LOGO

HDMI SCREEN FRAME

TTGO SCREEN FRAME

SIDE KNOB

SPEAKER GRILL

101

MAIN BODY

FRONT VAULTEC LOGO

Schematics

LAYOUT

Code

MAIN CODE

C/C++
#include <Arduino.h>
#include <Wire.h>
#include "AXS15231B.h"
#include "pins_config.h"
#include <Adafruit_SGP40.h>
#include "fontFull.h"

/* ================= DISPLAY ================= */
#define SCREEN_W 640
#define SCREEN_H 180
uint16_t frameBuffer[SCREEN_W * SCREEN_H];

/* ================= COLORS ================= */
#define TERMINAL_YELLOW  0x07FF
#define BLACK            0x0000

/* ================= SGP40 ================= */
Adafruit_SGP40 sgp;
uint16_t vocRaw = 0;
uint16_t vocSmooth = 0;
uint16_t vocDisplay = 0;

/* ================= STATE ================= */
bool startupAnim = true;
int animLevel = 10;
int animDir = -1;
unsigned long animStart = 0;
unsigned long lastAnimStep = 0;

/* ================= ROTATED PUSH ================= */
void lcd_PushColors_rotated(uint16_t w, uint16_t h, uint16_t *data)
{
  static uint16_t line[SCREEN_H];
  for (int col = w - 1; col >= 0; col--) {
    lcd_address_set(0, (w - 1) - col, h - 1, (w - 1) - col);
    for (int row = 0; row < h; row++) {
      line[row] = data[row * w + col];
    }
    lcd_PushColors(line, h);
  }
}

/* ================= FRAMEBUFFER ================= */
void clearFB() { memset(frameBuffer, 0, sizeof(frameBuffer)); }

void fillRectFB(int x, int y, int w, int h, uint16_t c)
{
  for (int j = 0; j < h; j++) {
    uint16_t *row = &frameBuffer[(y + j) * SCREEN_W + x];
    for (int i = 0; i < w; i++) row[i] = c;
  }
}

/* ================= FONT ================= */
void drawCharRot90CCW_Fixed(int x, int y, char c, uint16_t col, uint8_t scale)
{
  const uint8_t* bmp = fontFull_getBitmap(c);
  for (int cx = 0; cx < 5; cx++) {
    uint8_t bits = pgm_read_byte(&bmp[cx]);
    for (int cy = 0; cy < 7; cy++) {
      if (bits & 0x01) {
        fillRectFB(x + (6 - cy) * scale, y + cx * scale, scale, scale, col);
      }
      bits >>= 1;
    }
  }
}

void drawTextVertical(int x, int y, const char* txt, uint16_t col, uint8_t scale)
{
  while (*txt) {
    drawCharRot90CCW_Fixed(x, y, *txt, col, scale);
    y += (5 * scale) + scale;
    txt++;
  }
}

/* ================= UI LAYOUT ================= */
#define BTN_PADDING 12
#define BTN_GAP     10
#define BTN_Y       BTN_PADDING
#define BTN_H       (SCREEN_H - BTN_PADDING * 2)
#define BTN_W       ((SCREEN_W - BTN_PADDING * 2 - BTN_GAP * 3) / 4)

/* ================= BARS ================= */
#define BAR_COUNT 10

uint8_t vocToBars(uint16_t voc)
{
  if (voc <= 100) return 2;
  if (voc <= 200) return 4;
  if (voc <= 400) return 6;
  if (voc <= 600) return 8;
  return 10;
}

void drawBars(uint8_t level)
{
  int areaX = BTN_PADDING;
  int areaY = BTN_Y;
  int areaW = (BTN_W * 3) + (BTN_GAP * 2);
  int areaH = BTN_H;

  fillRectFB(areaX, areaY, areaW, areaH, BLACK);

  int gap = 5;
  int barW = (areaW - (BAR_COUNT + 1) * gap) / BAR_COUNT;
  int barH = areaH - 24;

  for (int i = 0; i < level; i++) {
    int x = areaX + gap + i * (barW + gap);
    fillRectFB(x, areaY + 12, barW, barH, TERMINAL_YELLOW);
  }
}

/* ================= RADS PANEL ================= */
void drawRADS(uint16_t voc)
{
  int panelX = BTN_PADDING + (BTN_W + BTN_GAP) * 3;
  int panelY = BTN_Y;
  int panelW = BTN_W;
  int panelH = BTN_H;

  fillRectFB(panelX, panelY, panelW, panelH, BLACK);

  int bw = 3;
  fillRectFB(panelX, panelY, panelW, bw, TERMINAL_YELLOW);
  fillRectFB(panelX, panelY + panelH - bw, panelW, bw, TERMINAL_YELLOW);
  fillRectFB(panelX, panelY, bw, panelH, TERMINAL_YELLOW);
  fillRectFB(panelX + panelW - bw, panelY, bw, panelH, TERMINAL_YELLOW);

  int padTop = 43;
  int radsX = panelX + panelW - 38;
  int numberX = radsX - 80;

  int radY = panelY + padTop;
  drawTextVertical(radsX, radY, "RADS", TERMINAL_YELLOW, 3);

  int lineX = radsX - 20;
  int lineY = panelY + bw + 2;
  int lineH = panelH - (bw * 2) - 4;
  fillRectFB(lineX, lineY, 2, lineH, TERMINAL_YELLOW);

  int numberY = radY + (4 * (5 * 3 + 1)) + 6 - 90;
  char buf[8];
  snprintf(buf, sizeof(buf), "%u", voc);
  drawTextVertical(numberX, numberY, buf, TERMINAL_YELLOW, 6);
}

/* ================= SETUP ================= */
void setup()
{
  pinMode(TFT_BL, OUTPUT);
  digitalWrite(TFT_BL, HIGH);

  Wire.begin(15, 10);
  axs15231_init();
  sgp.begin();

  animStart = millis();
}

/* ================= LOOP ================= */
void loop()
{
  clearFB();

  // -------- SENSOR STABILIZATION --------
  vocRaw = sgp.measureVocIndex();

  // Exponential smoothing
  vocSmooth = (vocSmooth * 7 + vocRaw) / 8;

  // Peak-hold with slow decay
  if (vocSmooth > vocDisplay) {
    vocDisplay = vocSmooth;
  } else if (vocDisplay > 0) {
    vocDisplay--;   // slow decay
  }

  drawRADS(vocDisplay);

  if (startupAnim) {
    if (millis() - lastAnimStep > 350) {
      animLevel += animDir;
      if (animLevel <= 1 || animLevel >= BAR_COUNT)
        animDir = -animDir;
      lastAnimStep = millis();
    }
    drawBars(animLevel);
    if (millis() - animStart > 7000)
      startupAnim = false;
  } else {
    drawBars(vocToBars(vocDisplay));
  }

  lcd_PushColors_rotated(SCREEN_W, SCREEN_H, frameBuffer);
  delay(30);
}

AXS15231B.cpp

C Header File
#include "AXS15231B.h"
#include "SPI.h"
#include "Arduino.h"
#include "driver/spi_master.h"

static volatile bool lcd_spi_dma_write = false;
extern void my_print(const char *buf);
uint32_t transfer_num = 0;
size_t lcd_PushColors_len = 0;

const static lcd_cmd_t axs15231b_qspi_init[] = {
    {0x28, {0x00}, 0x40},
    {0x10, {0x00}, 0x20},
    {0x11, {0x00}, 0x80},
    {0x29, {0x00}, 0x00}, 
};

const static lcd_cmd_t axs15231b_qspi_init_new[] = {
    {0x28, {0x00}, 0x40},
    {0x10, {0x00}, 0x80},
    {0xbb, {0x00,0x00,0x00,0x00,0x00,0x00,0x5a,0xa5}, 0x08},   
    {0xa0, {0x00,0x30,0x00,0x02,0x00,0x00,0x05,0x3f,0x30,0x05,0x3f,0x3f,0x00,0x00,0x00,0x00,0x00}, 0x11},
    {0xa2, {0x30,0x04,0x14,0x50,0x80,0x30,0x85,0x80,0xb4,0x28,0xff,0xff,0xff,0x20,0x50,0x10,0x02,0x06,0x20,0xd0,0xc0,0x01,0x12,0xa0,0x91,0xc0,0x20,0x7f,0xff,0x00,0x06}, 0x1F}, 
    {0xd0, {0x80,0xb4,0x21,0x24,0x08,0x05,0x10,0x01,0xf2,0x02,0xc2,0x02,0x22,0x22,0xaa,0x03,0x10,0x12,0xc0,0x10,0x10,0x40,0x04,0x00,0x30,0x10,0x00,0x03,0x0d,0x12}, 0x1E},
    {0xa3, {0xa0,0x06,0xaa,0x00,0x08,0x02,0x0a,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x00,0x55,0x55}, 0x16},
    {0xc1, {0x33,0x04,0x02,0x02,0x71,0x05,0x24,0x55,0x02,0x00,0x01,0x01,0x53,0xff,0xff,0xff,0x4f,0x52,0x00,0x4f,0x52,0x00,0x45,0x3b,0x0b,0x04,0x0d,0x00,0xff,0x42}, 0x1E},
    {0xc4, {0x00,0x24,0x33,0x80,0x66,0xea,0x64,0x32,0xc8,0x64,0xc8,0x32,0x90,0x90,0x11,0x06,0xdc,0xfa,0x00,0x00,0x80,0xfe,0x10,0x10,0x00,0x0a,0x0a,0x44,0x50}, 0x1D},
    {0xc5, {0x18,0x00,0x00,0x03,0xfe,0xe8,0x3b,0x20,0x30,0x10,0x88,0xde,0x0d,0x08,0x0f,0x0f,0x01,0xe8,0x3b,0x20,0x10,0x10,0x00}, 0x17},
    {0xc6, {0x05,0x0a,0x05,0x0a,0x00,0xe0,0x2e,0x0b,0x12,0x22,0x12,0x22,0x01,0x03,0x00,0x02,0x6a,0x18,0xc8,0x22}, 0x14},
    {0xc7, {0x50,0x36,0x28,0x00,0xa2,0x80,0x8f,0x00,0x80,0xff,0x07,0x11,0x9c,0x6f,0xff,0x24,0x0c,0x0d,0x0e,0x0f,0x01,0x01,0x01,0x01,0x3f,0x07,0x00}, 0x1B},
    {0xc9, {0x33,0x44,0x44,0x01}, 0x04},
    {0xcf, {0x2c,0x1e,0x88,0x58,0x13,0x18,0x56,0x18,0x1e,0x68,0xf7,0x00,0x66,0x0d,0x22,0xc4,0x0c,0x77,0x22,0x44,0xaa,0x55,0x04,0x04,0x12,0xa0,0x08}, 0x1B},
    {0xd5, {0x30,0x30,0x8a,0x00,0x44,0x04,0x4a,0xe5,0x02,0x4a,0xe5,0x02,0x04,0xd9,0x02,0x47,0x03,0x03,0x03,0x03,0x83,0x00,0x00,0x00,0x80,0x52,0x53,0x50,0x50,0x00}, 0x1E},
    {0xd6, {0x10,0x32,0x54,0x76,0x98,0xba,0xdc,0xfe,0x34,0x02,0x01,0x83,0xff,0x00,0x20,0x50,0x00,0x30,0x03,0x03,0x50,0x13,0x00,0x00,0x00,0x04,0x50,0x20,0x01,0x00}, 0x1E},
    {0xd7, {0x03,0x01,0x09,0x0b,0x0d,0x0f,0x1e,0x1f,0x18,0x1d,0x1f,0x19,0x30,0x30,0x04,0x00,0x20,0x20,0x1f}, 0x13},
    {0xd8, {0x02,0x00,0x08,0x0a,0x0c,0x0e,0x1e,0x1f,0x18,0x1d,0x1f,0x19}, 0x0C},
    {0xdf, {0x44,0x33,0x4b,0x69,0x00,0x0a,0x02,0x90}, 0x06},
    {0xe0, {0x1f,0x20,0x10,0x17,0x0d,0x09,0x12,0x2a,0x44,0x25,0x0c,0x15,0x13,0x31,0x36,0x2f,0x02}, 0x11},
    {0xe1, {0x3f,0x20,0x10,0x16,0x0c,0x08,0x12,0x29,0x43,0x25,0x0c,0x15,0x13,0x32,0x36,0x2f,0x27}, 0x11},
    {0xe2, {0x3b,0x07,0x12,0x18,0x0e,0x0d,0x17,0x35,0x44,0x32,0x0c,0x14,0x14,0x36,0x3a,0x2f,0x0d}, 0x11},
    {0xe3, {0x37,0x07,0x12,0x18,0x0e,0x0d,0x17,0x35,0x44,0x32,0x0c,0x14,0x14,0x36,0x32,0x2f,0x0f}, 0x11},
    {0xe4, {0x3b,0x07,0x12,0x18,0x0e,0x0d,0x17,0x39,0x44,0x2e,0x0c,0x14,0x14,0x36,0x3a,0x2f,0x0d}, 0x11},
    {0xe5, {0x37,0x07,0x12,0x18,0x0e,0x0d,0x17,0x39,0x44,0x2e,0x0c,0x14,0x14,0x36,0x3a,0x2f,0x0f}, 0x11},
    {0xbb, {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, 0x06},
    {0x28, {0x00}, 0x40},
    {0x10, {0x00}, 0x80},
    {0x11, {0x00}, 0x80},
    {0x29, {0x00}, 0x00}, 
};

bool get_lcd_spi_dma_write(void)
{
    return lcd_spi_dma_write;
}

static spi_device_handle_t spi;

static void WriteComm(uint8_t data)
{
    TFT_CS_L;
    SPI.beginTransaction(SPISettings(SPI_FREQUENCY, MSBFIRST, TFT_SPI_MODE));
    SPI.write(0x00);
    SPI.write(data);
    SPI.write(0x00);
    SPI.endTransaction();
    TFT_CS_H;
}

static void WriteData(uint8_t data)
{
    TFT_CS_L;
    SPI.beginTransaction(SPISettings(SPI_FREQUENCY, MSBFIRST, TFT_SPI_MODE));
    SPI.write(data);
    SPI.endTransaction();
    TFT_CS_H;
}

static void lcd_send_cmd(uint32_t cmd, uint8_t *dat, uint32_t len)
{
#if LCD_USB_QSPI_DREVER == 1
    TFT_CS_L;
    spi_transaction_t t;
    memset(&t, 0, sizeof(t));
    t.flags = (SPI_TRANS_MULTILINE_CMD | SPI_TRANS_MULTILINE_ADDR);
    #ifdef LCD_SPI_DMA
        if(cmd == 0xff && len == 0x1f)
        {
            t.cmd = 0x02;
            t.addr = 0xffff;
            len = 0;
        }
        else if(cmd == 0x00)
        {
            t.cmd = 0X00;
            t.addr = 0X0000;
            len = 4;
        }
        else 
        {
            t.cmd = 0x02;
            t.addr = cmd << 8;
        }
    #else
        t.cmd = 0x02;
        t.addr = cmd << 8;
    #endif
    if (len != 0) {
        t.tx_buffer = dat; 
        t.length = 8 * len;
    } else {
        t.tx_buffer = NULL;
        t.length = 0;
    }
    spi_device_polling_transmit(spi, &t);
    TFT_CS_H;
    if(0)
    {
        WriteComm(cmd);
        if (len != 0) {
            for (int i = 0; i < len; i++)
                WriteData(dat[i]);
        }
    }
#else
    WriteComm(cmd);
    if (len != 0) {
        for (int i = 0; i < len; i++)
            WriteData(dat[i]);
    }
#endif
}

static void IRAM_ATTR spi_dma_cd(spi_transaction_t *trans)
{
    if(transfer_num > 0)
    {
        transfer_num--;
    }
        
    if(lcd_PushColors_len <= 0 && transfer_num <= 0)
    {
        if(lcd_spi_dma_write) {
            lcd_spi_dma_write = false;
            lv_disp_t * disp = _lv_refr_get_disp_refreshing();
            if(disp != NULL)
                lv_disp_flush_ready(disp->driver);

            TFT_CS_H;
        }
    }
}


void lcd_send_data8(uint8_t dat) {
	unsigned char i;
	for (i = 0; i < 8; i++) {
		if (dat & 0x80) {
		digitalWrite(TFT_QSPI_D0, 1);
		} else {
		digitalWrite(TFT_QSPI_D0, 0);
		}
		dat <<= 1;
		digitalWrite(TFT_QSPI_SCK, 0);
		digitalWrite(TFT_QSPI_SCK, HIGH);
	}
}

void axs15231_init(void)
{
    pinMode(TFT_QSPI_CS, OUTPUT);
    pinMode(TFT_QSPI_RST, OUTPUT);

    TFT_RES_H;
    delay(130);
    TFT_RES_L;
    delay(130);
    TFT_RES_H;
    delay(300);

#if LCD_USB_QSPI_DREVER == 1
    esp_err_t ret;

    spi_bus_config_t buscfg = {
        .data0_io_num = TFT_QSPI_D0,
        .data1_io_num = TFT_QSPI_D1,
        .sclk_io_num = TFT_QSPI_SCK,
        .data2_io_num = TFT_QSPI_D2,
        .data3_io_num = TFT_QSPI_D3,
        .max_transfer_sz = (SEND_BUF_SIZE * 16) + 8,
        .flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_GPIO_PINS /* |
                 SPICOMMON_BUSFLAG_QUAD */
        ,
    };
    spi_device_interface_config_t devcfg = {
        .command_bits = 8,
        .address_bits = 24,
        .mode = TFT_SPI_MODE,
        .clock_speed_hz = SPI_FREQUENCY,
        .spics_io_num = -1,
        // .spics_io_num = TFT_QSPI_CS,
        .flags = SPI_DEVICE_HALFDUPLEX,
        .queue_size = 17,
        .post_cb = spi_dma_cd,
    };
    ret = spi_bus_initialize(TFT_SPI_HOST, &buscfg, SPI_DMA_CH_AUTO);
    ESP_ERROR_CHECK(ret);
    ret = spi_bus_add_device(TFT_SPI_HOST, &devcfg, &spi);
    ESP_ERROR_CHECK(ret);

#else
    SPI.begin(TFT_SCK, -1, TFT_MOSI, TFT_CS);
    SPI.setFrequency(SPI_FREQUENCY);
    pinMode(TFT_DC, OUTPUT);
#endif
    // Initialize the screen multiple times to prevent initialization failure
    int i = 1;
    while (i--) {
#if LCD_USB_QSPI_DREVER == 1
        const lcd_cmd_t *lcd_init = axs15231b_qspi_init;
        for (int i = 0; i < sizeof(axs15231b_qspi_init) / sizeof(lcd_cmd_t); i++)
#else
        const lcd_cmd_t *lcd_init = axs15231_spi_init;
        for (int i = 0; i < sizeof(axs15231_spi_init) / sizeof(lcd_cmd_t); i++)
#endif
        {
            lcd_send_cmd(lcd_init[i].cmd,
                         (uint8_t *)lcd_init[i].data,
                         lcd_init[i].len & 0x3f);

            if (lcd_init[i].len & 0x80)
                delay(200);
            if (lcd_init[i].len & 0x40)
                delay(20);
        }
    }
}

void lcd_setRotation(uint8_t r)
{
    uint8_t gbr = TFT_MAD_RGB;

    switch (r) {
    case 0: // Portrait
        // WriteData(gbr);
        break;
    case 1: // Landscape (Portrait + 90)
        gbr = TFT_MAD_MX | TFT_MAD_MV | gbr;
        break;
    case 2: // Inverter portrait
        gbr = TFT_MAD_MX | TFT_MAD_MY | gbr;
        break;
    case 3: // Inverted landscape
        gbr = TFT_MAD_MV | TFT_MAD_MY | gbr;
        break;
    }
    lcd_send_cmd(TFT_MADCTL, &gbr, 1);
}

void lcd_address_set(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2)
{
    lcd_cmd_t t[3] = {
        {0x2a, {(uint8_t)(x1 >> 8), (uint8_t)x1, uint8_t(x2 >> 8), (uint8_t)(x2)}, 0x04},
        {0x2b, {(uint8_t)(y1 >> 8), (uint8_t)(y1), (uint8_t)(y2 >> 8), (uint8_t)(y2)}, 0x04},
    };

    for (uint32_t i = 0; i < 2; i++) {
        lcd_send_cmd(t[i].cmd, t[i].data, t[i].len);
    }
}

void lcd_fill(uint16_t xsta,
              uint16_t ysta,
              uint16_t xend,
              uint16_t yend,
              uint16_t color)
{

    uint16_t w = xend - xsta;
    uint16_t h = yend - ysta;
    uint16_t *color_p = (uint16_t *)heap_caps_malloc(w * h * 2, MALLOC_CAP_INTERNAL);
    int i = 0;
    for(i = 0; i < w * h ; i+=1)
    {
        color_p[i] = color;
    }

    lcd_PushColors(xsta, ysta, w, h, color_p);
    free(color_p);
}

void lcd_DrawPoint(uint16_t x, uint16_t y, uint16_t color)
{
    lcd_address_set(x, y, x + 1, y + 1);
    lcd_PushColors(&color, 1);
}

void spi_device_queue_trans_fun(spi_device_handle_t handle, spi_transaction_t *trans_desc, TickType_t ticks_to_wait)
{
    ESP_ERROR_CHECK(spi_device_queue_trans(spi, (spi_transaction_t *)trans_desc, portMAX_DELAY));
}

#ifdef LCD_SPI_DMA 
spi_transaction_ext_t t = {0};
void lcd_PushColors(uint16_t x,
                        uint16_t y,
                        uint16_t width,
                        uint16_t high,
                        uint16_t *data)
    {
        static bool first_send = 1;
        static uint16_t *p = (uint16_t *)data;
        static uint32_t transfer_num_old = 0;

        if(data != NULL && (width != 0) && (high != 0))
        {
            lcd_PushColors_len = width * high;
            p = (uint16_t *)data;
            first_send = 1;

            transfer_num = 0;
            lcd_address_set(x, y, x + width - 1, y + high - 1);
            TFT_CS_L;
        }

        for (int x = 0; x < (transfer_num_old - (transfer_num_old-(transfer_num_old-transfer_num))); x++) {
            spi_transaction_t *rtrans;
            esp_err_t ret = spi_device_get_trans_result(spi, &rtrans, portMAX_DELAY);
            if (ret != ESP_OK) {
            // ESP_LOGW(TAG, "1. transfer_num = %d", transfer_num_old);
            }
            assert(ret == ESP_OK);
        }
        transfer_num_old -= (transfer_num_old - (transfer_num_old-(transfer_num_old-transfer_num)));

        do {
            if(transfer_num >= 3 || ESP.getFreeHeap() <= 70000)
            {
                break;
            }
            size_t chunk_size = lcd_PushColors_len;

            memset(&t, 0, sizeof(t));
            if (first_send) {
                t.base.flags =
                    SPI_TRANS_MODE_QIO ;// | SPI_TRANS_MODE_DIOQIO_ADDR 
                t.base.cmd = 0x32 ;// 0x12 
                t.base.addr = 0x002C00;
                first_send = 0;
            } else {
                t.base.flags = SPI_TRANS_MODE_QIO | SPI_TRANS_VARIABLE_CMD |
                            SPI_TRANS_VARIABLE_ADDR | SPI_TRANS_VARIABLE_DUMMY;
                t.command_bits = 0;
                t.address_bits = 0;
                t.dummy_bits = 0;
            }
            if (chunk_size > SEND_BUF_SIZE) {
                chunk_size = SEND_BUF_SIZE;
            }
            t.base.tx_buffer = p;
            t.base.length = chunk_size * 16;

            lcd_spi_dma_write = true;

            transfer_num++;
            transfer_num_old++;
            lcd_PushColors_len -= chunk_size;
            esp_err_t ret;

            ESP_ERROR_CHECK(spi_device_queue_trans(spi, (spi_transaction_t *)&t, portMAX_DELAY));
            assert(ret == ESP_OK);

            p += chunk_size;
        } while (lcd_PushColors_len > 0);
    }
#if 0
    void lcd_PushColors(uint16_t x,
                        uint16_t y,
                        uint16_t width,
                        uint16_t high,
                        uint16_t *data)
    {
        bool first_send = 1;
        lcd_PushColors_len = width * high;
        uint16_t *p = (uint16_t *)data;

        spi_transaction_t *rtrans;

        for (int x = 0; x < transfer_num; x++) {
            esp_err_t ret = spi_device_get_trans_result(spi, &rtrans, portMAX_DELAY);
            if (ret != ESP_OK) {
            ESP_LOGW(TAG, "1. transfer_num = %d", transfer_num);
            }
            assert(ret == ESP_OK);
        }
        transfer_num = 0;

        lcd_address_set(x, y, x + width - 1, y + high - 1);
        TFT_CS_L;
        do {
            size_t chunk_size = lcd_PushColors_len;
            spi_transaction_ext_t t = {0};
            memset(&t, 0, sizeof(t));
            if (first_send) {
                t.base.flags =
                    SPI_TRANS_MODE_QIO /* | SPI_TRANS_MODE_DIOQIO_ADDR */;
                t.base.cmd = 0x32 /* 0x12 */;
                t.base.addr = 0x002C00;
                first_send = 0;
            } else {
                t.base.flags = SPI_TRANS_MODE_QIO | SPI_TRANS_VARIABLE_CMD |
                            SPI_TRANS_VARIABLE_ADDR | SPI_TRANS_VARIABLE_DUMMY;
                t.command_bits = 0;
                t.address_bits = 0;
                t.dummy_bits = 0;
            }
            if (chunk_size > SEND_BUF_SIZE) {
                chunk_size = SEND_BUF_SIZE;
            }
            t.base.tx_buffer = p;
            t.base.length = chunk_size * 16;

            lcd_spi_dma_write = true;

            transfer_num++;
            lcd_PushColors_len -= chunk_size;

            spi_device_queue_trans_fun(spi, (spi_transaction_t *)&t, portMAX_DELAY);

            p += chunk_size;
        } while (lcd_PushColors_len > 0);
    }
 #endif   
#else
    void lcd_PushColors(uint16_t x,
                        uint16_t y,
                        uint16_t width,
                        uint16_t high,
                        uint16_t *data)
    {
    #if LCD_USB_QSPI_DREVER == 1
        bool first_send = 1;
        size_t len = width * high;
        uint16_t *p = (uint16_t *)data;

        lcd_address_set(x, y, x + width - 1, y + high - 1);
        
        do {

            TFT_CS_L;
            size_t chunk_size = len;
            spi_transaction_ext_t t = {0};
            memset(&t, 0, sizeof(t));
            if (1) {
                t.base.flags =
                    SPI_TRANS_MODE_QIO /* | SPI_TRANS_MODE_DIOQIO_ADDR */;
                t.base.cmd = 0x32 /* 0x12 */;
                if(first_send)
                {
                    t.base.addr = 0x002C00;
                }
                else 
                    t.base.addr = 0x003C00;
                first_send = 0;
            } else {
                t.base.flags = SPI_TRANS_MODE_QIO | SPI_TRANS_VARIABLE_CMD |
                            SPI_TRANS_VARIABLE_ADDR | SPI_TRANS_VARIABLE_DUMMY;
                t.command_bits = 0;
                t.address_bits = 0;
                t.dummy_bits = 0;
            }
            if (chunk_size > SEND_BUF_SIZE) {
                chunk_size = SEND_BUF_SIZE;
            }
            t.base.tx_buffer = p;
            t.base.length = chunk_size * 16;
            int aaa = 0;
            aaa = aaa>>1;
            aaa = aaa>>1;
            aaa = aaa>>1;
            if(!first_send)
                TFT_CS_H;
            aaa = aaa>>1;
            aaa = aaa>>1;
            aaa = aaa>>1;
            aaa = aaa>>1;
            aaa = aaa>>1;
            TFT_CS_L;
            aaa = aaa>>1;
            aaa = aaa>>1;
            aaa = aaa>>1;
            spi_device_polling_transmit(spi, (spi_transaction_t *)&t);
            len -= chunk_size;
            p += chunk_size;
        } while (len > 0);
        TFT_CS_H;

    #else
        lcd_address_set(x, y, x + width - 1, y + high - 1);
        TFT_CS_L;
        SPI.beginTransaction(SPISettings(SPI_FREQUENCY, MSBFIRST, TFT_SPI_MODE));
        
        SPI.writeBytes((uint8_t *)data, width * high * 2);
        SPI.endTransaction();
        TFT_CS_H;
    #endif
    }
#endif

void lcd_PushColors(uint16_t *data, uint32_t len)
{
#if LCD_USB_QSPI_DREVER == 1
    bool first_send = 1;
    uint16_t *p = (uint16_t *)data;
    TFT_CS_L;
    do {
        size_t chunk_size = len;
        spi_transaction_ext_t t = {0};
        memset(&t, 0, sizeof(t));
        if (first_send) {
            t.base.flags =
                SPI_TRANS_MODE_QIO /* | SPI_TRANS_MODE_DIOQIO_ADDR */;
            t.base.cmd = 0x32 /* 0x12 */;
            t.base.addr = 0x002C00;
            first_send = 0;
        } else {
            t.base.flags = SPI_TRANS_MODE_QIO | SPI_TRANS_VARIABLE_CMD |
                           SPI_TRANS_VARIABLE_ADDR | SPI_TRANS_VARIABLE_DUMMY;
            t.command_bits = 0;
            t.address_bits = 0;
            t.dummy_bits = 0;
        }
        if (chunk_size > SEND_BUF_SIZE) {
            chunk_size = SEND_BUF_SIZE;
        }
        t.base.tx_buffer = p;
        t.base.length = chunk_size * 16;

        spi_device_polling_transmit(spi, (spi_transaction_t *)&t);
        len -= chunk_size;
        p += chunk_size;
    } while (len > 0);
    TFT_CS_H;

#else
    TFT_CS_L;
    SPI.beginTransaction(SPISettings(SPI_FREQUENCY, MSBFIRST, TFT_SPI_MODE));
     
    SPI.writeBytes((uint8_t *)data, len * 2);
    SPI.endTransaction();
    TFT_CS_H;
#endif
}

void lcd_sleep()
{
    lcd_send_cmd(0x10, NULL, 0);
}

AXS15231B.h

C Header File
#pragma once

#include "stdint.h"
#include "pins_config.h"
#include "lvgl.h"/* https://github.com/lvgl/lvgl.git */

#define LCD_SPI_DMA 
#define AX15231B

#define TFT_MADCTL 0x36
#define TFT_MAD_MY 0x80
#define TFT_MAD_MX 0x40
#define TFT_MAD_MV 0x20
#define TFT_MAD_ML 0x10
#define TFT_MAD_BGR 0x08
#define TFT_MAD_MH 0x04
#define TFT_MAD_RGB 0x00

#define TFT_INVOFF 0x20
#define TFT_INVON 0x21

#define TFT_SCK_H digitalWrite(TFT_SCK, 1);
#define TFT_SCK_L digitalWrite(TFT_SCK, 0);
#define TFT_SDA_H digitalWrite(TFT_MOSI, 1);
#define TFT_SDA_L digitalWrite(TFT_MOSI, 0);

#define TFT_RES_H digitalWrite(TFT_QSPI_RST, 1);
#define TFT_RES_L digitalWrite(TFT_QSPI_RST, 0);
#define TFT_DC_H digitalWrite(TFT_DC, 1);
#define TFT_DC_L digitalWrite(TFT_DC, 0);
#define TFT_CS_H digitalWrite(TFT_QSPI_CS, 1);
#define TFT_CS_L digitalWrite(TFT_QSPI_CS, 0);

typedef struct
{
    uint8_t cmd;
    uint8_t data[36];
    uint8_t len;
} lcd_cmd_t;

void axs15231_init(void);

// Set the display window size
void lcd_address_set(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2);
void lcd_setRotation(uint8_t r);
void lcd_DrawPoint(uint16_t x, uint16_t y, uint16_t color);
void lcd_fill(uint16_t xsta,
              uint16_t ysta,
              uint16_t xend,
              uint16_t yend,
              uint16_t color);
void lcd_PushColors(uint16_t x,
                    uint16_t y,
                    uint16_t width,
                    uint16_t high,
                    uint16_t *data);
void lcd_PushColors(uint16_t *data, uint32_t len);
void lcd_sleep();

bool get_lcd_spi_dma_write(void);

fontsFull.h

C Header File
#pragma once
#include <Arduino.h>

// 5x7 font: uppercase, lowercase, numbers, and space
// Index: ' ' = 0, 'A' = 1, ..., 'Z' = 26, 'a' = 27, ..., 'z' = 52, '0' = 53, ..., '9' = 62
const uint8_t fontFull[63][5] PROGMEM = {
  {0x00,0x00,0x00,0x00,0x00}, // space

  // Uppercase A-Z
  {0x7C,0x12,0x11,0x12,0x7C}, // A
  {0x7F,0x49,0x49,0x49,0x36}, // B
  {0x3E,0x41,0x41,0x41,0x22}, // C
  {0x7F,0x41,0x41,0x22,0x1C}, // D
  {0x7F,0x49,0x49,0x49,0x41}, // E
  {0x7F,0x09,0x09,0x09,0x01}, // F
  {0x3E,0x41,0x49,0x49,0x7A}, // G
  {0x7F,0x08,0x08,0x08,0x7F}, // H
  {0x00,0x41,0x7F,0x41,0x00}, // I
  {0x20,0x40,0x41,0x3F,0x01}, // J
    {0x7F,0x08,0x14,0x22,0x41}, // K
  {0x7F,0x40,0x40,0x40,0x40}, // L
  {0x7F,0x02,0x0C,0x02,0x7F}, // M
  {0x7F,0x04,0x08,0x10,0x7F}, // N
  {0x3E,0x41,0x41,0x41,0x3E}, // O
  {0x7F,0x09,0x09,0x09,0x06}, // P
  {0x3E,0x41,0x51,0x21,0x5E}, // Q
  {0x7F,0x09,0x19,0x29,0x46}, // R
  {0x26,0x49,0x49,0x49,0x32}, // S
  {0x01,0x01,0x7F,0x01,0x01}, // T
  {0x3F,0x40,0x40,0x40,0x3F}, // U
  {0x1F,0x20,0x40,0x20,0x1F}, // V
  {0x3F,0x40,0x38,0x40,0x3F}, // W
  {0x63,0x14,0x08,0x14,0x63}, // X
  {0x07,0x08,0x70,0x08,0x07}, // Y
  {0x61,0x51,0x49,0x45,0x43}, // Z

  // Lowercase a-z
  {0x20,0x54,0x54,0x54,0x78}, // a
  {0x7F,0x48,0x44,0x44,0x38}, // b
  {0x38,0x44,0x44,0x44,0x20}, // c
  {0x38,0x44,0x44,0x48,0x7F}, // d
  {0x38,0x54,0x54,0x54,0x18}, // e
  {0x08,0x7E,0x09,0x01,0x02}, // f
  {0x0C,0x52,0x52,0x52,0x3E}, // g
  {0x7F,0x08,0x04,0x04,0x78}, // h
  {0x00,0x44,0x7D,0x40,0x00}, // i
  {0x20,0x40,0x44,0x3D,0x00}, // j
  {0x7F,0x10,0x28,0x44,0x00}, // k
  {0x00,0x41,0x7F,0x40,0x00}, // l
  {0x7C,0x04,0x18,0x04,0x78}, // m
  {0x7C,0x08,0x04,0x04,0x78}, // n
  {0x38,0x44,0x44,0x44,0x38}, // o
  {0x7C,0x14,0x14,0x14,0x08}, // p
  {0x08,0x14,0x14,0x18,0x7C}, // q
  {0x7C,0x08,0x04,0x04,0x08}, // r
  {0x48,0x54,0x54,0x54,0x20}, // s
  {0x04,0x3F,0x44,0x40,0x20}, // t
  {0x3C,0x40,0x40,0x20,0x7C}, // u
  {0x1C,0x20,0x40,0x20,0x1C}, // v
  {0x3C,0x40,0x30,0x40,0x3C}, // w
  {0x44,0x28,0x10,0x28,0x44}, // x
  {0x0C,0x50,0x50,0x50,0x3C}, // y
  {0x44,0x64,0x54,0x4C,0x44}, // z

  // Numbers 0-9 (corrected)
  {0x3E,0x51,0x49,0x45,0x3E}, // 0
  {0x00,0x42,0x7F,0x40,0x00}, // 1
  {0x62,0x51,0x49,0x49,0x46}, // 2
  {0x22,0x41,0x49,0x49,0x36}, // 3
  {0x18,0x14,0x12,0x7F},       // 4
  {0x2F,0x49,0x49,0x49,0x31}, // 5
  {0x3E,0x49,0x49,0x49,0x32}, // 6
  {0x01,0x71,0x09,0x05,0x03}, // 7
  {0x36,0x49,0x49,0x49,0x36}, // 8
  {0x26,0x49,0x49,0x49,0x3E}  // 9
};

// Helper functions
inline uint8_t fontFull_getWidth(char c) {
  return 5; // fixed width 5px
}

inline const uint8_t* fontFull_getBitmap(char c) {
  if (c == ' ') return fontFull[0];
  if (c >= 'A' && c <= 'Z') return fontFull[1 + c - 'A'];
  if (c >= 'a' && c <= 'z') return fontFull[27 + c - 'a'];
  if (c >= '0' && c <= '9') return fontFull[53 + c - '0'];
  return fontFull[0]; // fallback space
}

pins_config.h

C Header File
#pragma once

/***********************config*************************/
#define LCD_USB_QSPI_DREVER   1

#define SPI_FREQUENCY           32000000
#define TFT_SPI_MODE          SPI_MODE0
#define TFT_SPI_HOST          SPI2_HOST

#define WIFI_SSID             "xinyuandianzi"
#define WIFI_PASSWORD         "AA15994823428"

#define WIFI_CONNECT_WAIT_MAX (30 * 1000)

#define NTP_SERVER1           "pool.ntp.org"
#define NTP_SERVER2           "time.nist.gov"
#define GMT_OFFSET_SEC        0
#define DAY_LIGHT_OFFSET_SEC  0

/* Automatically update local time */
#define GET_TIMEZONE_API      "https://ipapi.co/timezone/"

/***********************config*************************/

#define TFT_WIDTH             180
#define TFT_HEIGHT            640

#ifdef TFT_WIDTH
#define EXAMPLE_LCD_H_RES     TFT_WIDTH
#else
#define EXAMPLE_LCD_H_RES     180
#endif
#ifdef TFT_HEIGHT
#define EXAMPLE_LCD_V_RES     TFT_HEIGHT
#else
#define EXAMPLE_LCD_V_RES     640
#endif
#define LVGL_LCD_BUF_SIZE     (EXAMPLE_LCD_H_RES * EXAMPLE_LCD_V_RES)

#define SEND_BUF_SIZE         (28800/2) //16bit(RGB565)

#define TFT_QSPI_CS           12
#define TFT_QSPI_SCK          17
#define TFT_QSPI_D0           13
#define TFT_QSPI_D1           18
#define TFT_QSPI_D2           21
#define TFT_QSPI_D3           14
#define TFT_QSPI_RST          16
#define TFT_BL                1


#define PIN_BAT_VOLT          2

#define PIN_BUTTON_1          0
#define PIN_BUTTON_2          21

Credits

Arnov Sharma
374 projects • 389 followers
I'm Arnov. I build, design, and experiment with tech—3D printing, PCB design, and retro consoles are my jam.

Comments