mirror of
https://github.com/sle118/squeezelite-esp32.git
synced 2026-03-22 14:29:26 +00:00
297 lines
9.0 KiB
C
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;
|
|
} |