f4fd82bd7e
The driver is based on the construction of IL3820 (SSD1608), which is very similar from command point of view. It is tested on ESP32-S2 MCU and GoodDisplay GDEY029T94.
374 lines
11 KiB
C
374 lines
11 KiB
C
/**
|
|
@file ssd1680.c
|
|
@brief Tested with GoodDisplay GDEY029T94 e-paper 2.9" monochrome display
|
|
@version 1.0
|
|
@date 2022-09-20
|
|
@author Aram Vartanyan, based on the il3820 driver by Juergen Kienhoefer
|
|
*/
|
|
|
|
/*********************
|
|
* INCLUDES
|
|
*********************/
|
|
#include "disp_spi.h"
|
|
#include "driver/gpio.h"
|
|
#include "esp_log.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/task.h"
|
|
|
|
#include "ssd1680.h"
|
|
|
|
/*********************
|
|
* DEFINES
|
|
*********************/
|
|
#define TAG "SSD1680"
|
|
|
|
/**
|
|
* ssd1680 compatible EPD controller driver.
|
|
*/
|
|
|
|
#define BIT_SET(a,b) ((a) |= (1U<<(b)))
|
|
#define BIT_CLEAR(a,b) ((a) &= ~(1U<<(b)))
|
|
|
|
/* Number of pixels? */
|
|
#define SSD1680_PIXEL (LV_HOR_RES_MAX * LV_VER_RES_MAX)
|
|
|
|
#define EPD_PANEL_NUMOF_COLUMS EPD_PANEL_WIDTH
|
|
#define EPD_PANEL_NUMOF_ROWS_PER_PAGE 8
|
|
|
|
/* Are pages the number of bytes to represent the panel width? in bytes */
|
|
#define EPD_PANEL_NUMOF_PAGES (EPD_PANEL_HEIGHT / EPD_PANEL_NUMOF_ROWS_PER_PAGE)
|
|
|
|
#define SSD1680_PANEL_FIRST_PAGE 0
|
|
#define SSD1680_PANEL_LAST_PAGE (EPD_PANEL_NUMOF_PAGES - 1)
|
|
#define SSD1680_PANEL_FIRST_GATE 0
|
|
#define SSD1680_PANEL_LAST_GATE (EPD_PANEL_NUMOF_COLUMS - 1)
|
|
|
|
#define SSD1680_PIXELS_PER_BYTE 8
|
|
#define EPD_PARTIAL_CNT 5
|
|
|
|
//uint8_t ssd1680_scan_mode = SSD1680_DATA_ENTRY_XIYDX; //another approach
|
|
uint8_t ssd1680_scan_mode = SSD1680_DATA_ENTRY_XIYIY; //as per the il3820 driver
|
|
|
|
static uint8_t partial_counter = 0;
|
|
|
|
static uint8_t ssd1680_border_init[] = {0x05}; //init
|
|
static uint8_t ssd1680_border_part[] = {0x80}; //partial update
|
|
//A2 - 1 Follow LUT; A1:A0 - 01 LUT1;
|
|
//static uint8_t ssd1680_border[] = {0x03}; //old LUT3
|
|
|
|
/* Static functions */
|
|
|
|
static void ssd1680_update_display(bool isPartial);
|
|
static inline void ssd1680_set_window( uint16_t sx, uint16_t ex, uint16_t ys, uint16_t ye);
|
|
static inline void ssd1680_set_cursor(uint16_t sx, uint16_t ys);
|
|
static inline void ssd1680_waitbusy(int wait_ms);
|
|
static inline void ssd1680_hw_reset(void);
|
|
static inline void ssd1680_command_mode(void);
|
|
static inline void ssd1680_data_mode(void);
|
|
static inline void ssd1680_write_cmd(uint8_t cmd, uint8_t *data, size_t len);
|
|
static inline void ssd1680_send_cmd(uint8_t cmd);
|
|
static inline void ssd1680_send_data(uint8_t *data, uint16_t length);
|
|
|
|
/* Required by LVGL */
|
|
void ssd1680_flush(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_map)
|
|
{
|
|
/* Each byte holds the data of 8 pixels, linelen is the number of bytes
|
|
* we need to cover a line of the display. */
|
|
size_t linelen = EPD_PANEL_WIDTH / 8; //SSD1680_COLUMNS = (EPD_PANEL_WIDTH / 8)
|
|
uint8_t *buffer = (uint8_t *) color_map;
|
|
uint16_t x_addr_counter = 0;
|
|
uint16_t y_addr_counter = 0;
|
|
|
|
/* Set the cursor at the beginning of the graphic RAM */
|
|
#if defined (CONFIG_LV_DISPLAY_ORIENTATION_PORTRAIT)
|
|
x_addr_counter = EPD_PANEL_WIDTH - 1;
|
|
y_addr_counter = EPD_PANEL_HEIGHT - 1;
|
|
#endif
|
|
ssd1680_init();
|
|
|
|
if (!partial_counter) {
|
|
ESP_LOGD(TAG, "Refreshing in FULL");
|
|
ssd1680_send_cmd(SSD1680_CMD_WRITE1_RAM);
|
|
for(size_t row = 0; row <= (EPD_PANEL_HEIGHT - 1); row++){
|
|
ssd1680_send_data(buffer, linelen);
|
|
buffer += SSD1680_COLUMNS;
|
|
}
|
|
ssd1680_send_cmd(SSD1680_CMD_WRITE2_RAM);
|
|
for(size_t row = 0; row <= (EPD_PANEL_HEIGHT - 1); row++){
|
|
ssd1680_send_data(buffer, linelen);
|
|
buffer += SSD1680_COLUMNS;
|
|
}
|
|
ssd1680_update_display(false);
|
|
partial_counter = EPD_PARTIAL_CNT;
|
|
} else {
|
|
//update partial
|
|
ssd1680_hw_reset();
|
|
ssd1680_write_cmd(SSD1680_CMD_BWF_CTRL, ssd1680_border_part, 1);
|
|
ssd1680_set_window(area->x1, area->x2, area->y1, area->y2);
|
|
ssd1680_set_cursor(x_addr_counter, y_addr_counter);
|
|
|
|
ssd1680_send_cmd(SSD1680_CMD_WRITE1_RAM);
|
|
for(size_t row = 0; row <= (EPD_PANEL_HEIGHT - 1); row++) {
|
|
ssd1680_send_data(buffer, linelen);
|
|
buffer += SSD1680_COLUMNS; //(128/8)x296 = 4736
|
|
}
|
|
ssd1680_update_display(true);
|
|
partial_counter--;
|
|
}
|
|
ssd1680_deep_sleep();
|
|
/* IMPORTANT!!!
|
|
* Inform the graphics library that you are ready with the flushing */
|
|
lv_disp_flush_ready(drv);
|
|
}
|
|
|
|
/* Specify the start/end positions of the window address in the X and Y
|
|
* directions by an address unit.
|
|
*
|
|
* @param sx: X Start position.
|
|
* @param ex: X End position.
|
|
* @param ys: Y Start position.
|
|
* @param ye: Y End position.
|
|
*/
|
|
static inline void ssd1680_set_window( uint16_t sx, uint16_t ex, uint16_t ys, uint16_t ye)
|
|
{
|
|
uint8_t tmp[4] = {0};
|
|
|
|
tmp[0] = sx / 8;
|
|
tmp[1] = ex / 8; //0x0F-->(15+1)*8=128 -> ex = EPD_PANEL_WIDTH
|
|
//tmp[1] = (ex / 8) - 1;
|
|
|
|
/* Set X address start/end */
|
|
ssd1680_write_cmd(SSD1680_CMD_RAM_XPOS_CTRL, tmp, 2);
|
|
|
|
//0x0127-->(295+1)=296
|
|
//a % b = a - (a/b)*b
|
|
tmp[0] = ys % 256;
|
|
tmp[1] = ys / 256;
|
|
tmp[2] = ye % 256;
|
|
tmp[3] = ye / 256;
|
|
/* Set Y address start/end */
|
|
ssd1680_write_cmd(SSD1680_CMD_RAM_YPOS_CTRL, tmp, 4);
|
|
}
|
|
|
|
/* Make initial settings for the RAM X and Y addresses in the address counter
|
|
* (AC).
|
|
*
|
|
* @param sx: RAM X address counter.
|
|
* @param ys: RAM Y address counter.
|
|
*/
|
|
static inline void ssd1680_set_cursor(uint16_t sx, uint16_t ys)
|
|
{
|
|
uint8_t tmp[2] = {0};
|
|
|
|
tmp[0] = sx / 8;
|
|
ssd1680_write_cmd(SSD1680_CMD_RAM_XPOS_CNTR, tmp, 1);
|
|
|
|
tmp[0] = ys % 256;
|
|
tmp[1] = ys / 256;
|
|
ssd1680_write_cmd(SSD1680_CMD_RAM_YPOS_CNTR, tmp, 2);
|
|
}
|
|
|
|
/* After sending the RAM content we need to send the commands:
|
|
* - Display Update Control 2
|
|
* - Master Activation
|
|
*/
|
|
static void ssd1680_update_display(bool isPartial)
|
|
{
|
|
uint8_t tmp = 0;
|
|
|
|
if(isPartial) {
|
|
tmp = 0xFF; //Display mode 2 - Partial update
|
|
} else {
|
|
tmp = 0xF7; //Display mode 1 - Full update
|
|
}
|
|
/* Display Update Control */
|
|
ssd1680_write_cmd(SSD1680_CMD_UPDATE_CTRL2, &tmp, 1);
|
|
/* Activate Display Update Sequence */
|
|
ssd1680_write_cmd(SSD1680_CMD_MASTER_ACTIVATION, NULL, 0);
|
|
/* Poll BUSY signal. */
|
|
ssd1680_waitbusy(SSD1680_WAIT);
|
|
}
|
|
|
|
/* Rotate the display by "software" when using PORTRAIT orientation.
|
|
* BIT_SET(byte_index, bit_index) clears the bit_index pixel at byte_index of
|
|
* the display buffer.
|
|
* BIT_CLEAR(byte_index, bit_index) sets the bit_index pixel at the byte_index
|
|
* of the display buffer. */
|
|
void ssd1680_set_px_cb(lv_disp_drv_t * disp_drv, uint8_t* buf,
|
|
lv_coord_t buf_w, lv_coord_t x, lv_coord_t y,
|
|
lv_color_t color, lv_opa_t opa)
|
|
{
|
|
uint16_t byte_index = 0;
|
|
uint8_t bit_index = 0;
|
|
|
|
#if defined (CONFIG_LV_DISPLAY_ORIENTATION_PORTRAIT)
|
|
//This part doesn't work for now. Display prints random dots.
|
|
byte_index = x + ((y >> 3) * EPD_PANEL_HEIGHT);
|
|
bit_index = y & 0x7;
|
|
|
|
if (color.full) {
|
|
BIT_SET(buf[byte_index], 7 - bit_index);
|
|
} else {
|
|
uint16_t mirrored_idx = (EPD_PANEL_HEIGHT - x) + ((y >> 3) * EPD_PANEL_HEIGHT);
|
|
BIT_CLEAR(buf[mirrored_idx], 7 - bit_index);
|
|
}
|
|
#elif defined (CONFIG_LV_DISPLAY_ORIENTATION_LANDSCAPE)
|
|
byte_index = y + ((x >> 3) * EPD_PANEL_HEIGHT);
|
|
bit_index = x & 0x7;
|
|
|
|
if (color.full) {
|
|
BIT_SET(buf[byte_index], 7 - bit_index);
|
|
} else {
|
|
BIT_CLEAR(buf[byte_index], 7 - bit_index);
|
|
}
|
|
#else
|
|
#error "Unsupported orientation used"
|
|
#endif
|
|
}
|
|
|
|
/* Required by LVGL */
|
|
void ssd1680_rounder(lv_disp_drv_t * disp_drv, lv_area_t *area)
|
|
{
|
|
area->x1 = area->x1 & ~(0x7);
|
|
area->x2 = area->x2 | (0x7);
|
|
|
|
/* Update the areas as needed.
|
|
* For example it makes the area to start only on 8th rows and have Nx8 pixel height.*/
|
|
}
|
|
|
|
/* main initialization routine */
|
|
void ssd1680_init(void)
|
|
{
|
|
uint8_t tmp[3] = {0};
|
|
|
|
/* Initialize non-SPI GPIOs */
|
|
gpio_pad_select_gpio(SSD1680_DC_PIN);
|
|
gpio_set_direction(SSD1680_DC_PIN, GPIO_MODE_OUTPUT);
|
|
|
|
gpio_pad_select_gpio(SSD1680_BUSY_PIN);
|
|
gpio_set_direction(SSD1680_BUSY_PIN, GPIO_MODE_INPUT);
|
|
|
|
#if SSD1680_USE_RST
|
|
gpio_pad_select_gpio(SSD1680_RST_PIN);
|
|
gpio_set_direction(SSD1680_RST_PIN, GPIO_MODE_OUTPUT);
|
|
|
|
/* Harware reset */
|
|
ssd1680_hw_reset();
|
|
#endif
|
|
|
|
/* Busy wait for the BUSY signal to go low */
|
|
ssd1680_waitbusy(SSD1680_WAIT);
|
|
|
|
/* Software reset */
|
|
ssd1680_write_cmd(SSD1680_CMD_SW_RESET, NULL, 0);
|
|
|
|
ssd1680_waitbusy(SSD1680_WAIT);
|
|
|
|
/* Driver output control */
|
|
tmp[0] = (EPD_PANEL_HEIGHT - 1) & 0xFF; //0x27
|
|
tmp[1] = (EPD_PANEL_HEIGHT >> 8 ); //0x01
|
|
tmp[2] = 0x00; // GD = 0; SM = 0; TB = 0; //0x00
|
|
ssd1680_write_cmd(SSD1680_CMD_GDO_CTRL, tmp, 3);
|
|
|
|
/* Configure entry mode */
|
|
ssd1680_write_cmd(SSD1680_CMD_ENTRY_MODE, &ssd1680_scan_mode, 1);
|
|
|
|
/* Configure the window */
|
|
ssd1680_set_window(0, EPD_PANEL_WIDTH - 1, 0, EPD_PANEL_HEIGHT - 1);
|
|
|
|
/* Select border waveform for VBD */
|
|
ssd1680_write_cmd(SSD1680_CMD_BWF_CTRL, ssd1680_border_init, 1);
|
|
|
|
/* Display update control 1 */
|
|
tmp[0] = 0x00; //A7:0 Normal RAM content option
|
|
tmp[1] = 0x80; //B7 Source Output Mode: Available source from S8 to S167
|
|
tmp[2] = 0x00; //do not send
|
|
ssd1680_write_cmd(SSD1680_CMD_UPDATE_CTRL1, tmp, 2);
|
|
|
|
/* Read Build-in Temperature sensor */
|
|
tmp[0] = 0x80; //A7:0 Internal sensor
|
|
tmp[1] = 0x00; //do not send
|
|
ssd1680_write_cmd(SSD1680_CMD_READ_INT_TEMP, tmp, 1);
|
|
|
|
/*set RAM x (0) and y (295) address count */
|
|
ssd1680_set_cursor(0, EPD_PANEL_HEIGHT - 1);
|
|
ssd1680_waitbusy(SSD1680_WAIT);
|
|
}
|
|
|
|
/* Enter deep sleep mode */
|
|
void ssd1680_deep_sleep(void)
|
|
{
|
|
uint8_t data[] = {0x01};
|
|
|
|
/* Wait for the BUSY signal to go low */
|
|
ssd1680_waitbusy(SSD1680_WAIT);
|
|
|
|
ssd1680_write_cmd(SSD1680_CMD_SLEEP_MODE1, data, 1);
|
|
vTaskDelay(100 / portTICK_RATE_MS); // 100ms delay
|
|
}
|
|
|
|
static inline void ssd1680_waitbusy(int wait_ms)
|
|
{
|
|
int i = 0;
|
|
|
|
vTaskDelay(10 / portTICK_RATE_MS); // 10ms delay
|
|
|
|
for(i = 0; i < (wait_ms * 10); i++) {
|
|
if(gpio_get_level(SSD1680_BUSY_PIN) != SSD1680_BUSY_LEVEL) {
|
|
return;
|
|
}
|
|
vTaskDelay(10 / portTICK_RATE_MS);
|
|
}
|
|
ESP_LOGE( TAG, "busy exceeded %dms", i*10 );
|
|
}
|
|
|
|
/* Set HWRESET */
|
|
static inline void ssd1680_hw_reset(void)
|
|
{
|
|
gpio_set_level(SSD1680_RST_PIN, 0);
|
|
vTaskDelay(SSD1680_RESET_DELAY / portTICK_RATE_MS);
|
|
gpio_set_level(SSD1680_RST_PIN, 1);
|
|
vTaskDelay(SSD1680_RESET_DELAY / portTICK_RATE_MS);
|
|
}
|
|
|
|
/* Set DC signal to command mode */
|
|
static inline void ssd1680_command_mode(void)
|
|
{
|
|
gpio_set_level(SSD1680_DC_PIN, 0);
|
|
}
|
|
|
|
/* Set DC signal to data mode */
|
|
static inline void ssd1680_data_mode(void)
|
|
{
|
|
gpio_set_level(SSD1680_DC_PIN, 1);
|
|
}
|
|
|
|
static inline void ssd1680_write_cmd(uint8_t cmd, uint8_t *data, size_t len)
|
|
{
|
|
disp_wait_for_pending_transactions();
|
|
ssd1680_command_mode();
|
|
disp_spi_send_data(&cmd, 1);
|
|
|
|
if (data != NULL) {
|
|
ssd1680_data_mode();
|
|
disp_spi_send_data(data, len);
|
|
}
|
|
}
|
|
|
|
/* Send cmd to the display */
|
|
static inline void ssd1680_send_cmd(uint8_t cmd)
|
|
{
|
|
disp_wait_for_pending_transactions();
|
|
ssd1680_command_mode();
|
|
disp_spi_send_data(&cmd, 1);
|
|
}
|
|
|
|
/* Send length bytes of data to the display */
|
|
static inline void ssd1680_send_data(uint8_t *data, uint16_t length)
|
|
{
|
|
disp_wait_for_pending_transactions();
|
|
ssd1680_data_mode();
|
|
disp_spi_send_colors(data, length);
|
|
}
|
|
|