Login
1 branch 0 tags
Ben (Desktop/Arch) Limited podcast episode list to 16 entries b36bce8 29 days ago 80 Commits
moon / esp32 / main / st7735.c
#include "st7735.h"
#include "backlight.h"
#include "pinout.h"

#include <string.h>
#include "driver/gpio.h"
#include "driver/spi_master.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

// ST7735 commands
#define ST7735_SWRESET 0x01
#define ST7735_SLPIN 0x10
#define ST7735_SLPOUT 0x11
#define ST7735_INVON 0x21
#define ST7735_DISPOFF 0x28
#define ST7735_DISPON 0x29
#define ST7735_CASET 0x2A
#define ST7735_RASET 0x2B
#define ST7735_RAMWR 0x2C
#define ST7735_MADCTL 0x36
#define ST7735_COLMOD 0x3A

// Display offsets (adjust for your specific module)
#define X_OFFSET 0
#define Y_OFFSET 0

static spi_device_handle_t spi_dev;
static bool display_sleeping = false;

static void st7735_write_cmd(uint8_t cmd);

void st7735_sleep(void) {
	if (display_sleeping) {
		return;
	}
	st7735_write_cmd(ST7735_DISPOFF);
	st7735_write_cmd(ST7735_SLPIN);
	vTaskDelay(pdMS_TO_TICKS(120));
	display_sleeping = true;
}

void st7735_wake(void) {
	if (!display_sleeping) {
		return;
	}
	st7735_write_cmd(ST7735_SLPOUT);
	vTaskDelay(pdMS_TO_TICKS(120));
	st7735_write_cmd(ST7735_DISPON);
	display_sleeping = false;
}

bool st7735_is_sleeping(void) {
	return display_sleeping;
}

// Pre-transfer callback to set DC pin
static void IRAM_ATTR spi_pre_transfer_cb(spi_transaction_t* t) {
	int dc = (int)t->user;
	gpio_set_level(PIN_DC, dc);
}

static void st7735_write_cmd(uint8_t cmd) {
	spi_transaction_t t = {
	    .length = 8,
	    .tx_buffer = &cmd,
	    .user = (void*)0,  // DC=0 for command
	};
	spi_device_polling_transmit(spi_dev, &t);
}

static void st7735_write_data(const uint8_t* data, size_t len) {
	if (len == 0) {
		return;
	}
	spi_transaction_t t = {
	    .length = len * 8,
	    .tx_buffer = data,
	    .user = (void*)1,  // DC=1 for data
	};
	spi_device_polling_transmit(spi_dev, &t);
}

static void st7735_write_data_byte(uint8_t data) {
	st7735_write_data(&data, 1);
}

void st7735_init(void) {
	// Configure GPIO pins
	gpio_config_t io_conf = {
	    .pin_bit_mask = (1ULL << PIN_DC) | (1ULL << PIN_RST),
	    .mode = GPIO_MODE_OUTPUT,
	    .pull_up_en = GPIO_PULLUP_DISABLE,
	    .pull_down_en = GPIO_PULLDOWN_DISABLE,
	    .intr_type = GPIO_INTR_DISABLE,
	};
	gpio_config(&io_conf);
	backlight_init();

	// Hardware reset
	gpio_set_level(PIN_RST, 0);
	vTaskDelay(pdMS_TO_TICKS(1));
	gpio_set_level(PIN_RST, 1);
	vTaskDelay(pdMS_TO_TICKS(5));

	// Configure SPI bus
	spi_bus_config_t buscfg = {
	    .mosi_io_num = PIN_MOSI,
	    .miso_io_num = -1,
	    .sclk_io_num = PIN_SCK,
	    .quadwp_io_num = -1,
	    .quadhd_io_num = -1,
	    .max_transfer_sz =
	        160 * 128 * 2,  // Full frame at 2 bytes/pixel (RGB565)
	};
	spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO);

	// Configure SPI device
	spi_device_interface_config_t devcfg = {
	    .clock_speed_hz = 20 * 1000 * 1000,  // 20 MHz (APB_CLK/4)
	    .mode = 0,
	    .spics_io_num = PIN_CS,
	    .queue_size = 7,
	    .pre_cb = spi_pre_transfer_cb,
	};
	spi_bus_add_device(SPI2_HOST, &devcfg, &spi_dev);

	// ST7735 initialization sequence
	st7735_write_cmd(ST7735_SWRESET);
	vTaskDelay(pdMS_TO_TICKS(5));

	st7735_write_cmd(ST7735_SLPOUT);
	vTaskDelay(pdMS_TO_TICKS(10));

	// Set color mode to RGB565 (16-bit, 2 bytes per pixel)
	st7735_write_cmd(ST7735_COLMOD);
	st7735_write_data_byte(0x05);  // 16-bit RGB565

	// Set display orientation (MV + MX for landscape 160x128)
	st7735_write_cmd(ST7735_MADCTL);
	st7735_write_data_byte(0x60);

	// Enable display
	st7735_write_cmd(ST7735_DISPON);
}

void st7735_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) {
	// Column address set
	st7735_write_cmd(ST7735_CASET);
	uint8_t caset_data[] = {0, (uint8_t)(x0 + X_OFFSET), 0,
	                        (uint8_t)(x1 + X_OFFSET)};
	st7735_write_data(caset_data, 4);

	// Row address set
	st7735_write_cmd(ST7735_RASET);
	uint8_t raset_data[] = {0, (uint8_t)(y0 + Y_OFFSET), 0,
	                        (uint8_t)(y1 + Y_OFFSET)};
	st7735_write_data(raset_data, 4);

	// Write to RAM
	st7735_write_cmd(ST7735_RAMWR);
}

void st7735_write_pixels(const uint16_t* data, size_t len) {
	st7735_write_data((const uint8_t*)data, len * 2);
}

// Static transaction pool and buffers for fully-async flush pipeline
// ESP32-S3 SPI hardware max is 2^18 bits = 32768 bytes per transaction
#define SPI_MAX_DMA_BYTES 32768

static spi_transaction_t win_trans[5];
static spi_transaction_t px_trans[2];
static int px_trans_count;
static uint8_t cmd_caset = ST7735_CASET;
static uint8_t cmd_raset = ST7735_RASET;
static uint8_t cmd_ramwr = ST7735_RAMWR;
static uint8_t caset_data[4];
static uint8_t raset_data[4];

void st7735_flush_async(uint16_t x0,
                        uint16_t y0,
                        uint16_t x1,
                        uint16_t y1,
                        const uint16_t* pixels,
                        size_t pixel_count) {
	// Fill coordinate buffers
	caset_data[0] = 0;
	caset_data[1] = (uint8_t)(x0 + X_OFFSET);
	caset_data[2] = 0;
	caset_data[3] = (uint8_t)(x1 + X_OFFSET);

	raset_data[0] = 0;
	raset_data[1] = (uint8_t)(y0 + Y_OFFSET);
	raset_data[2] = 0;
	raset_data[3] = (uint8_t)(y1 + Y_OFFSET);

	// CASET command
	memset(&win_trans[0], 0, sizeof(spi_transaction_t));
	win_trans[0].length = 8;
	win_trans[0].tx_buffer = &cmd_caset;
	win_trans[0].user = (void*)0;  // DC=0

	// CASET data
	memset(&win_trans[1], 0, sizeof(spi_transaction_t));
	win_trans[1].length = 32;
	win_trans[1].tx_buffer = caset_data;
	win_trans[1].user = (void*)1;  // DC=1

	// RASET command
	memset(&win_trans[2], 0, sizeof(spi_transaction_t));
	win_trans[2].length = 8;
	win_trans[2].tx_buffer = &cmd_raset;
	win_trans[2].user = (void*)0;  // DC=0

	// RASET data
	memset(&win_trans[3], 0, sizeof(spi_transaction_t));
	win_trans[3].length = 32;
	win_trans[3].tx_buffer = raset_data;
	win_trans[3].user = (void*)1;  // DC=1

	// RAMWR command
	memset(&win_trans[4], 0, sizeof(spi_transaction_t));
	win_trans[4].length = 8;
	win_trans[4].tx_buffer = &cmd_ramwr;
	win_trans[4].user = (void*)0;  // DC=0

	// Split pixel data into chunks that fit the SPI hardware limit
	size_t total_bytes = pixel_count * 2;
	size_t offset = 0;
	px_trans_count = 0;

	while (offset < total_bytes) {
		size_t chunk = total_bytes - offset;
		if (chunk > SPI_MAX_DMA_BYTES) {
			chunk = SPI_MAX_DMA_BYTES;
		}

		memset(&px_trans[px_trans_count], 0, sizeof(spi_transaction_t));
		px_trans[px_trans_count].length = chunk * 8;
		px_trans[px_trans_count].tx_buffer = (const uint8_t*)pixels + offset;
		px_trans[px_trans_count].user = (void*)1;  // DC=1
		px_trans_count++;
		offset += chunk;
	}

	// Queue window setup + pixel transactions
	for (int i = 0; i < 5; i++) {
		spi_device_queue_trans(spi_dev, &win_trans[i], portMAX_DELAY);
	}
	for (int i = 0; i < px_trans_count; i++) {
		spi_device_queue_trans(spi_dev, &px_trans[i], portMAX_DELAY);
	}
}

void st7735_flush_wait(void) {
	spi_transaction_t* result;
	for (int i = 0; i < 5 + px_trans_count; i++) {
		spi_device_get_trans_result(spi_dev, &result, portMAX_DELAY);
	}
}