Files
squeezelite-esp32/lib/display/ILI9488.c
2026-03-13 17:03:22 +00:00

297 lines
9.0 KiB
C

/**
* ILI9488 Display Driver for Guition JC4827W543C
* Supports QSPI interface for 480x272 resolution
* Based on ILI9341 driver adapted for ILI9488
*
* (c) Guition Support 2026
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include <esp_heap_caps.h>
#include <esp_log.h>
#include "gds.h"
#include "gds_private.h"
#define SHADOW_BUFFER
#define USE_IRAM
#define PAGE_BLOCK 4096
#define ENABLE_WRITE 0x2c
#define min(a,b) (((a) < (b)) ? (a) : (b))
static char TAG[] = "ILI9488";
struct PrivateSpace {
uint8_t *iRAM, *Shadowbuffer;
struct {
uint16_t Height, Width;
} Offset;
uint8_t MADCtl, PageSize;
uint8_t Model;
};
// ILI9488 Commands
static const uint8_t ILI9488_INIT_SEQUENCE[] = {
// Software reset
0x01, 0x80, 150, // SWRESET, delay 150ms
// Power control
0xD0, 3, 0x07, 0x42, 0x18, // Power Control
0xD1, 3, 0x00, 0x07, 0x10, // VCOM Control
0xD2, 1, 0x01, // Power Control for Normal Mode
0xC0, 2, 0x10, 0x3B, // Panel Driving Setting
0xC1, 1, 0x10, // Frame Rate Control
0xC5, 5, 0x0A, 0x3A, 0x28, 0x28, 0x02, // MCU Control
0xC6, 1, 0x00, // Frame Rate Control
0xB1, 2, 0x00, 0x1B, // Display Function Control
0xB4, 1, 0x02, // Inversion Control
0xB6, 3, 0x02, 0x02, 0x3B, // Display Function Control
0xB7, 1, 0xC6, // Entry Mode Set
0xE0, 16, 0x00, 0x07, 0x10, 0x0E, 0x09, 0x16, 0x06, 0x0A,
0x0E, 0x09, 0x15, 0x0D, 0x0E, 0x11, 0x0F, 0x12, // Positive Gamma Control
0xE1, 16, 0x00, 0x17, 0x1A, 0x04, 0x0E, 0x06, 0x2F, 0x24,
0x1B, 0x1B, 0x22, 0x1F, 0x1E, 0x37, 0x3F, 0x00, // Negative Gamma Control
0x36, 1, 0xE8, // Memory Access Control (MX, MY, RGB mode)
0x3A, 1, 0x55, // Interface Pixel Format (16bpp)
0x11, 0x80, 150, // Sleep Out, delay 150ms
0x29, 0x80, 50, // Display On, delay 50ms
0xFF, 0x00 // End of sequence
};
static void WriteByte( struct GDS_Device* Device, uint8_t Data ) {
Device->WriteData( Device, &Data, 1 );
}
static void SetColumnAddress( struct GDS_Device* Device, uint16_t Start, uint16_t End ) {
uint32_t Addr = __builtin_bswap16(Start) | (__builtin_bswap16(End) << 16);
Device->WriteCommand( Device, 0x2A );
Device->WriteData( Device, (uint8_t*) &Addr, 4 );
}
static void SetRowAddress( struct GDS_Device* Device, uint16_t Start, uint16_t End ) {
uint32_t Addr = __builtin_bswap16(Start) | (__builtin_bswap16(End) << 16);
Device->WriteCommand( Device, 0x2B );
Device->WriteData( Device, (uint8_t*) &Addr, 4 );
}
static void Update16( struct GDS_Device* Device ) {
struct PrivateSpace *Private = (struct PrivateSpace*) Device->Private;
#ifdef SHADOW_BUFFER
uint32_t *optr = (uint32_t*) Private->Shadowbuffer, *iptr = (uint32_t*) Device->Framebuffer;
int FirstCol = Device->Width / 2, LastCol = 0, FirstRow = -1, LastRow = 0;
for (int r = 0; r < Device->Height; r++) {
// look for change and update shadow (cheap optimization = width is always a multiple of 2)
for (int c = 0; c < Device->Width / 2; c++, iptr++, optr++) {
if (*optr != *iptr) {
*optr = *iptr;
if (c < FirstCol) FirstCol = c;
if (c > LastCol) LastCol = c;
if (FirstRow < 0) FirstRow = r;
LastRow = r;
}
}
// wait for a large enough window - careful that window size might increase by more than a line at once !
if (FirstRow < 0 || ((LastCol - FirstCol + 1) * (r - FirstRow + 1) * 4 < PAGE_BLOCK && r != Device->Height - 1)) continue;
FirstCol *= 2;
LastCol = LastCol * 2 + 1;
SetRowAddress( Device, FirstRow + Private->Offset.Height, LastRow + Private->Offset.Height);
SetColumnAddress( Device, FirstCol + Private->Offset.Width, LastCol + Private->Offset.Width );
Device->WriteCommand( Device, ENABLE_WRITE );
int ChunkSize = (LastCol - FirstCol + 1) * 2;
// own use of IRAM has not proven to be much better than letting SPI do its copy
if (Private->iRAM) {
uint8_t *optr = Private->iRAM;
for (int i = FirstRow; i <= LastRow; i++) {
memcpy(optr, Private->Shadowbuffer + (i * Device->Width + FirstCol) * 2, ChunkSize);
optr += ChunkSize;
if (optr - Private->iRAM <= (PAGE_BLOCK - ChunkSize) && i < LastRow) continue;
Device->WriteData(Device, Private->iRAM, optr - Private->iRAM);
optr = Private->iRAM;
}
} else for (int i = FirstRow; i <= LastRow; i++) {
Device->WriteData( Device, Private->Shadowbuffer + (i * Device->Width + FirstCol) * 2, ChunkSize );
}
FirstCol = Device->Width / 2;
LastCol = 0;
FirstRow = -1;
}
#endif
}
static void Clear( struct GDS_Device* Device ) {
memset( Device->Framebuffer, 0, Device->Width * Device->Height * 2 );
Device->Update( Device);
}
static void SetPixel( struct GDS_Device* Device, int X, int Y, uint32_t Color ) {
if( X < 0 || X >= Device->Width || Y < 0 || Y >= Device->Height) return;
*((uint16_t*) Device->Framebuffer + (Y * Device->Width) + X) = (uint16_t) Color;
}
static uint32_t GetPixel( struct GDS_Device* Device, int X, int Y ) {
if( X < 0 || X >= Device->Width || Y < 0 || Y >= Device->Height) return 0;
return *((uint16_t*) Device->Framebuffer + (Y * Device->Width) + X);
}
static void DrawPixel( struct GDS_Device* Device, int X, int Y, uint32_t Color ) {
SetPixel( Device, X, Y, Color );
}
static void DrawPixelFast( struct GDS_Device* Device, int X, int Y, uint32_t Color ) {
*((uint16_t*) Device->Framebuffer + (Y * Device->Width) + X) = (uint16_t) Color;
}
static void DrawCBR( struct GDS_Device* Device, int X, int Y, int Width, int Height, uint32_t Color ) {
uint16_t *fb = (uint16_t*) Device->Framebuffer + Y * Device->Width + X;
for( int y = 0; y < Height; y++) {
for( int x = 0; x < Width; x++) {
fb[x] = (uint16_t) Color;
}
fb += Device->Width;
}
}
static void DrawHLine( struct GDS_Device* Device, int X0, int X1, int Y, uint32_t Color ) {
if( Y < 0 || Y >= Device->Height) return;
if( X0 > X1) {
int Temp = X0;
X0 = X1;
X1 = Temp;
}
if( X0 < 0) X0 = 0;
if( X1 >= Device->Width) X1 = Device->Width - 1;
uint16_t *fb = (uint16_t*) Device->Framebuffer + Y * Device->Width + X0;
for( int x = X0; x <= X1; x++) {
*fb++ = (uint16_t) Color;
}
}
static void DrawVLine( struct GDS_Device* Device, int X, int Y0, int Y1, uint32_t Color ) {
if( X < 0 || X >= Device->Width) return;
if( Y0 > Y1) {
int Temp = Y0;
Y0 = Y1;
Y1 = Temp;
}
if( Y0 < 0) Y0 = 0;
if( Y1 >= Device->Height) Y1 = Device->Height - 1;
uint16_t *fb = (uint16_t*) Device->Framebuffer + Y0 * Device->Width + X;
for( int y = Y0; y <= Y1; y++) {
*fb = (uint16_t) Color;
fb += Device->Width;
}
}
static bool Init( struct GDS_Device* Device ) {
struct PrivateSpace *Private = (struct PrivateSpace*) Device->Private;
const uint8_t *p = ILI9488_INIT_SEQUENCE;
ESP_LOGI(TAG, "Initializing ILI9488 display %dx%d", Device->Width, Device->Height);
// Allocate IRAM buffer if available
Private->iRAM = (uint8_t*) heap_caps_malloc(PAGE_BLOCK, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);
if (!Private->iRAM) {
ESP_LOGW(TAG, "Could not allocate IRAM buffer, using direct write");
}
#ifdef SHADOW_BUFFER
Private->Shadowbuffer = (uint8_t*) heap_caps_malloc(Device->Width * Device->Height * 2, MALLOC_CAP_DMA);
if (!Private->Shadowbuffer) {
ESP_LOGE(TAG, "Could not allocate shadow buffer");
if (Private->iRAM) free(Private->iRAM);
return false;
}
memset(Private->Shadowbuffer, 0, Device->Width * Device->Height * 2);
#endif
// Send initialization sequence
while(*p != 0xFF) {
uint8_t cmd = *p++;
uint8_t len = (*p & 0x7F);
bool delay = (*p++ & 0x80);
Device->WriteCommand(Device, cmd);
if(len) Device->WriteData(Device, (uint8_t*)p, len);
p += len;
if(delay) {
uint8_t ms = *p++;
vTaskDelay(ms / portTICK_PERIOD_MS);
}
}
// Set orientation and layout
Private->MADCtl = 0xE8; // MX=1, MY=1, RGB=1, BGR=0
Device->WriteCommand(Device, 0x36);
Device->WriteData(Device, &Private->MADCtl, 1);
// Set pixel format to 16-bit
uint8_t fmt = 0x55;
Device->WriteCommand(Device, 0x3A);
Device->WriteData(Device, &fmt, 1);
// Turn on display
Device->WriteCommand(Device, 0x29);
ESP_LOGI(TAG, "ILI9488 initialization complete");
return true;
}
static void Deinit( struct GDS_Device* Device ) {
struct PrivateSpace *Private = (struct PrivateSpace*) Device->Private;
if (Private->iRAM) free(Private->iRAM);
#ifdef SHADOW_BUFFER
if (Private->Shadowbuffer) free(Private->Shadowbuffer);
#endif
}
struct GDS_Device* ILI9488_Detect( char *Driver, struct GDS_Device *Device ) {
if(strcasecmp(Driver, "ILI9488") != 0) return NULL;
Device->Private = calloc(1, sizeof(struct PrivateSpace));
if(!Device->Private) {
ESP_LOGE(TAG, "Cannot allocate private data");
return NULL;
}
Device->Mode = GDS_RGB565;
Device->Depth = 16;
Device->Update = Update16;
Device->Clear = Clear;
Device->SetPixel = SetPixel;
Device->GetPixel = GetPixel;
Device->DrawPixel = DrawPixel;
Device->DrawPixelFast = DrawPixelFast;
Device->DrawCBR = DrawCBR;
Device->DrawHLine = DrawHLine;
Device->DrawVLine = DrawVLine;
Device->Init = Init;
Device->Deinit = Deinit;
ESP_LOGI(TAG, "ILI9488 driver loaded");
return Device;
}