drivers: ssd16xx: Add support for partial refresh profiles

Add support for partial refresh profiles. This makes it possible to
use partial refresh on generation 2 devices which are able to store
partial refresh LUTs in OTP.

Partial refresh is only enabled if a partial profile has been
provided. The display will use the full refresh profile if in this
case.

Devices that need custom LUTs and voltages can specify them separately
for the full and partial profiles. The controller will be reset when
changing profiles which means that profiles always override the
default reset values. This means that it is, for example, possible to
use default values and LUTs from OTP for a full refresh and a custom
profile for partial refreshes.

For example, to use a GoodDisplay GDEY027T91 with partial refresh
simply use the following device tree fragment:

display: ssd1680@0 {
	compatible = "solomon,ssd1680";

	spi-max-frequency = <4000000>;
	duplex = <SPI_HALF_DUPLEX>;
	reg = <0>;

	dc-gpios = <&arduino_header 15 GPIO_ACTIVE_LOW>;
	reset-gpios = <&arduino_header 14 GPIO_ACTIVE_LOW>;
	busy-gpios = <&arduino_header 13 GPIO_ACTIVE_HIGH>;

	/* Enable the built-in temperature sensor */
	tssv = <0x80>;

	width = <264>;
	height = <176>;

        /* Enable partial refresh using built-in LUT */
	partial {
	};
};

Signed-off-by: Andreas Sandberg <andreas@sandberg.uk>
This commit is contained in:
Andreas Sandberg 2022-07-21 22:07:13 +01:00 committed by Marti Bolivar
parent 40437c675c
commit 5ca33e20a8
9 changed files with 165 additions and 111 deletions

View file

@ -81,11 +81,19 @@
];
};
lut-default = [
18 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
0F 01 00 00 00 00 00 00
00 00 00 00 00
];
partial {
gdv = [10 0a];
sdv = [19];
vcom = <0xa8>;
border-waveform = <0x71>;
dummy-line = <0x1a>;
gate-line-width = <0x08>;
lut = [
18 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
0F 01 00 00 00 00 00 00
00 00 00 00 00
];
};
};
};

View file

@ -93,19 +93,27 @@
];
};
lut-default = [
00 00 00 00 00 00 00 /* LUT0: BB: VS0..6 */
80 00 00 00 00 00 00 /* LUT1: BW: VS0..6 */
40 00 00 00 00 00 00 /* LUT2: WB: VS0..6 */
80 00 00 00 00 00 00 /* LUT3: WW: VS0..6 */
00 00 00 00 00 00 00 /* LUT4: VCOM: VS0..6 */
0A 00 00 00 04 /* TP0A TP0B TP0C TP0D RP0 */
00 00 00 00 00 /* TP1A TP1B TP1C TP1D RP1 */
00 00 00 00 00 /* TP2A TP2B TP2C TP2D RP2 */
00 00 00 00 00 /* TP3A TP3B TP3C TP3D RP3 */
00 00 00 00 00 /* TP4A TP4B TP4C TP4D RP4 */
00 00 00 00 00 /* TP5A TP5B TP5C TP5D RP5 */
00 00 00 00 00 /* TP6A TP6B TP6C TP6D RP6 */
];
partial {
gdv = [15];
sdv = [41 a8 32];
vcom = <0x26>;
border-waveform = <0x01>;
dummy-line = <0x30>;
gate-line-width = <0x0a>;
lut = [
00 00 00 00 00 00 00 /* LUT0: BB: VS0..6 */
80 00 00 00 00 00 00 /* LUT1: BW: VS0..6 */
40 00 00 00 00 00 00 /* LUT2: WB: VS0..6 */
80 00 00 00 00 00 00 /* LUT3: WW: VS0..6 */
00 00 00 00 00 00 00 /* LUT4: VCOM: VS0..6 */
0A 00 00 00 04 /* TP0A TP0B TP0C TP0D RP0 */
00 00 00 00 00 /* TP1A TP1B TP1C TP1D RP1 */
00 00 00 00 00 /* TP2A TP2B TP2C TP2D RP2 */
00 00 00 00 00 /* TP3A TP3B TP3C TP3D RP3 */
00 00 00 00 00 /* TP4A TP4B TP4C TP4D RP4 */
00 00 00 00 00 /* TP5A TP5B TP5C TP5D RP5 */
00 00 00 00 00 /* TP6A TP6B TP6C TP6D RP6 */
];
};
};
};

View file

@ -26,6 +26,10 @@
tssv = <0x80>;
full {
border-waveform = <0x05>;
};
partial {
border-waveform = <0x3c>;
};
};

View file

@ -38,11 +38,19 @@
];
};
lut-default = [
18 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
0F 01 00 00 00 00 00 00
00 00 00 00 00
];
partial {
gdv = [10 0a];
sdv = [19];
vcom = <0xa8>;
border-waveform = <0x71>;
dummy-line = <0x1a>;
gate-line-width = <0x08>;
lut = [
18 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
0F 01 00 00 00 00 00 00
00 00 00 00 00
];
};
};
};

View file

@ -26,7 +26,7 @@
full {
gdv = [15];
sdv = [41 a8 32];
vcom = <0x26>;
vcom = <0x55>;
border-waveform = <0x03>;
dummy-line = <0x30>;
gate-line-width = <0x0a>;
@ -46,19 +46,27 @@
];
};
lut-default = [
00 00 00 00 00 00 00
80 00 00 00 00 00 00
40 00 00 00 00 00 00
80 00 00 00 00 00 00
00 00 00 00 00 00 00
0A 00 00 00 04
00 00 00 00 00
00 00 00 00 00
00 00 00 00 00
00 00 00 00 00
00 00 00 00 00
00 00 00 00 00
];
partial {
gdv = [15];
sdv = [41 a8 32];
vcom = <0x26>;
border-waveform = <0x01>;
dummy-line = <0x30>;
gate-line-width = <0x0a>;
lut = [
00 00 00 00 00 00 00
80 00 00 00 00 00 00
40 00 00 00 00 00 00
80 00 00 00 00 00 00
00 00 00 00 00 00 00
0A 00 00 00 04
00 00 00 00 00
00 00 00 00 00
00 00 00 00 00
00 00 00 00 00
00 00 00 00 00
00 00 00 00 00
];
};
};
};

View file

@ -26,9 +26,7 @@
softstart = [d7 d6 9d];
full {
gdv = [16];
sdv = [0a];
vcom = <0xa8>;
vcom = <0x9a>;
border-waveform = <0x33>;
dummy-line = <0x1a>;
gate-line-width = <0x08>;
@ -40,11 +38,17 @@
];
};
lut-default = [
10 18 18 08 18 18 08 00
00 00 00 00 00 00 00 00
00 00 00 00 13 14 44 12
00 00 00 00 00 00
];
partial {
vcom = <0xa8>;
border-waveform = <0x01>;
dummy-line = <0x1a>;
gate-line-width = <0x08>;
lut = [
10 18 18 08 18 18 08 00
00 00 00 00 00 00 00 00
00 00 00 00 13 14 44 12
00 00 00 00 00 00
];
};
};
};

View file

@ -34,6 +34,7 @@ LOG_MODULE_REGISTER(ssd16xx);
enum ssd16xx_profile_type {
SSD16XX_PROFILE_FULL = 0,
SSD16XX_PROFILE_PARTIAL,
SSD16XX_NUM_PROFILES,
SSD16XX_PROFILE_INVALID = SSD16XX_NUM_PROFILES,
};
@ -47,12 +48,22 @@ struct ssd16xx_quirks {
uint8_t pp_width_bits;
/* Width (bits) of integer type representing a y coordinate */
uint8_t pp_height_bits;
/*
* Device specific flags to be included in
* SSD16XX_CMD_UPDATE_CTRL2 for a full refresh.
*/
uint8_t ctrl2_full;
/*
* Device specific flags to be included in
* SSD16XX_CMD_UPDATE_CTRL2 for a partial refresh.
*/
uint8_t ctrl2_partial;
};
struct ssd16xx_data {
bool read_supported;
uint8_t scan_mode;
uint8_t update_cmd;
bool blanking_on;
enum ssd16xx_profile_type profile;
};
@ -89,14 +100,15 @@ struct ssd16xx_config {
const struct ssd16xx_profile *profiles[SSD16XX_NUM_PROFILES];
struct ssd16xx_dt_array lut_default;
bool orientation;
uint16_t height;
uint16_t width;
uint8_t tssv;
};
static int ssd16xx_set_profile(const struct device *dev,
enum ssd16xx_profile_type type);
static inline void ssd16xx_busy_wait(const struct device *dev)
{
const struct ssd16xx_config *config = dev->config;
@ -298,9 +310,23 @@ static int ssd16xx_activate(const struct device *dev, uint8_t ctrl2)
static int ssd16xx_update_display(const struct device *dev)
{
struct ssd16xx_data *data = dev->data;
const struct ssd16xx_config *config = dev->config;
const struct ssd16xx_data *data = dev->data;
const struct ssd16xx_profile *p = config->profiles[data->profile];
const struct ssd16xx_quirks *quirks = config->quirks;
const bool load_lut = !p || p->lut.len == 0;
const bool load_temp = load_lut && config->tssv;
const bool partial = data->profile == SSD16XX_PROFILE_PARTIAL;
const uint8_t update_cmd =
SSD16XX_CTRL2_ENABLE_CLK |
SSD16XX_CTRL2_ENABLE_ANALOG |
(load_lut ? SSD16XX_CTRL2_LOAD_LUT : 0) |
(load_temp ? SSD16XX_CTRL2_LOAD_TEMPERATURE : 0) |
(partial ? quirks->ctrl2_partial : quirks->ctrl2_full) |
SSD16XX_CTRL2_DISABLE_ANALOG |
SSD16XX_CTRL2_DISABLE_CLK;
return ssd16xx_activate(dev, data->update_cmd);
return ssd16xx_activate(dev, update_cmd);
}
static int ssd16xx_blanking_off(const struct device *dev)
@ -319,6 +345,12 @@ static int ssd16xx_blanking_on(const struct device *dev)
{
struct ssd16xx_data *data = dev->data;
if (!data->blanking_on) {
if (ssd16xx_set_profile(dev, SSD16XX_PROFILE_FULL)) {
return -EIO;
}
}
data->blanking_on = true;
return 0;
@ -423,6 +455,19 @@ static int ssd16xx_write(const struct device *dev, const uint16_t x,
return -EINVAL;
}
if (!data->blanking_on) {
/*
* Blanking isn't on, so this is a partial
* refresh. Request the partial profile. This
* operation becomes a no-op if the profile is already
* active.
*/
err = ssd16xx_set_profile(dev, SSD16XX_PROFILE_PARTIAL);
if (err < 0 && err != -ENOENT) {
return -EIO;
}
}
err = ssd16xx_set_window(dev, x, y, desc);
if (err < 0) {
return err;
@ -620,28 +665,19 @@ static int ssd16xx_clear_cntlr_mem(const struct device *dev, uint8_t ram_cmd,
static inline int ssd16xx_load_ws_from_otp_tssv(const struct device *dev)
{
const struct ssd16xx_config *config = dev->config;
struct ssd16xx_data *data = dev->data;
int err;
/*
* Controller has an integrated temperature sensor or external
* temperature sensor is connected to the controller.
*/
LOG_INF("Select and load WS from OTP");
err = ssd16xx_write_uint8(dev, SSD16XX_CMD_TSENSOR_SELECTION,
config->tssv);
if (err == 0) {
data->update_cmd |= SSD16XX_CTRL2_LOAD_LUT |
SSD16XX_CTRL2_LOAD_TEMPERATURE;
}
return err;
return ssd16xx_write_uint8(dev, SSD16XX_CMD_TSENSOR_SELECTION,
config->tssv);
}
static inline int ssd16xx_load_ws_from_otp(const struct device *dev)
{
int16_t t = (SSD16XX_DEFAULT_TR_VALUE * SSD16XX_TR_SCALE_FACTOR);
struct ssd16xx_data *data = dev->data;
uint8_t tmp[2];
int err;
@ -664,8 +700,6 @@ static inline int ssd16xx_load_ws_from_otp(const struct device *dev)
return err;
}
data->update_cmd |= SSD16XX_CTRL2_LOAD_LUT;
return 0;
}
@ -674,12 +708,9 @@ static int ssd16xx_load_lut(const struct device *dev,
const struct ssd16xx_dt_array *lut)
{
const struct ssd16xx_config *config = dev->config;
struct ssd16xx_data *data = dev->data;
if (lut && lut->len) {
LOG_DBG("Using user-provided LUT");
/* Don't load the default LUT on the next refresh */
data->update_cmd &= ~SSD16XX_CTRL2_LOAD_LUT;
return ssd16xx_write_cmd(dev, SSD16XX_CMD_UPDATE_LUT,
lut->data, lut->len);
} else {
@ -807,20 +838,6 @@ static int ssd16xx_set_profile(const struct device *dev,
return 0;
}
static int ssd16xx_load_ws_default(const struct device *dev)
{
const struct ssd16xx_config *config = dev->config;
if (config->lut_default.len) {
return ssd16xx_write_cmd(dev, SSD16XX_CMD_UPDATE_LUT,
config->lut_default.data,
config->lut_default.len);
}
return 0;
}
static int ssd16xx_controller_init(const struct device *dev)
{
const struct ssd16xx_config *config = dev->config;
@ -851,12 +868,6 @@ static int ssd16xx_controller_init(const struct device *dev)
data->scan_mode = SSD16XX_DATA_ENTRY_XDYIY;
}
data->update_cmd = (SSD16XX_CTRL2_ENABLE_CLK |
SSD16XX_CTRL2_ENABLE_ANALOG |
SSD16XX_CTRL2_TO_PATTERN |
SSD16XX_CTRL2_DISABLE_ANALOG |
SSD16XX_CTRL2_DISABLE_CLK);
err = ssd16xx_set_profile(dev, SSD16XX_PROFILE_FULL);
if (err < 0) {
return err;
@ -873,12 +884,7 @@ static int ssd16xx_controller_init(const struct device *dev)
return err;
}
err = ssd16xx_load_ws_default(dev);
if (err < 0) {
return err;
}
return ssd16xx_clear_cntlr_mem(dev, SSD16XX_CMD_WRITE_RAM, true);
return 0;
}
static int ssd16xx_init(const struct device *dev)
@ -958,6 +964,8 @@ static struct ssd16xx_quirks quirks_solomon_ssd1608 = {
.max_height = 240,
.pp_width_bits = 16,
.pp_height_bits = 16,
.ctrl2_full = SSD16XX_GEN1_CTRL2_TO_PATTERN,
.ctrl2_partial = SSD16XX_GEN1_CTRL2_TO_PATTERN,
};
#endif
@ -967,6 +975,8 @@ static struct ssd16xx_quirks quirks_solomon_ssd1673 = {
.max_height = 150,
.pp_width_bits = 8,
.pp_height_bits = 8,
.ctrl2_full = SSD16XX_GEN1_CTRL2_TO_PATTERN,
.ctrl2_partial = SSD16XX_GEN1_CTRL2_TO_PATTERN,
};
#endif
@ -976,6 +986,8 @@ static struct ssd16xx_quirks quirks_solomon_ssd1675a = {
.max_height = 160,
.pp_width_bits = 8,
.pp_height_bits = 16,
.ctrl2_full = SSD16XX_GEN1_CTRL2_TO_PATTERN,
.ctrl2_partial = SSD16XX_GEN1_CTRL2_TO_PATTERN,
};
#endif
@ -985,15 +997,11 @@ static struct ssd16xx_quirks quirks_solomon_ssd1681 = {
.max_height = 200,
.pp_width_bits = 8,
.pp_height_bits = 16,
.ctrl2_full = SSD16XX_GEN2_CTRL2_DISPLAY,
.ctrl2_partial = SSD16XX_GEN2_CTRL2_DISPLAY | SSD16XX_GEN2_CTRL2_MODE2,
};
#endif
#define LUT_DEFAULT_ASSIGN(n) \
.lut_default = { \
.data = lut_default_##n, \
.len = sizeof(lut_default_##n), \
},
#define SOFTSTART_ASSIGN(n) \
.softstart = { \
.data = softstart_##n, \
@ -1038,7 +1046,6 @@ static struct ssd16xx_quirks quirks_solomon_ssd1681 = {
NULL)
#define SSD16XX_DEFINE(n, quirks_ptr) \
SSD16XX_MAKE_ARRAY_OPT(n, lut_default); \
SSD16XX_MAKE_ARRAY_OPT(n, softstart); \
\
DT_FOREACH_CHILD(n, SSD16XX_PROFILE); \
@ -1057,10 +1064,11 @@ static struct ssd16xx_quirks quirks_solomon_ssd1681 = {
.orientation = DT_PROP(n, orientation_flipped), \
.tssv = DT_PROP_OR(n, tssv, 0), \
.softstart = SSD16XX_ASSIGN_ARRAY(n, softstart), \
.lut_default = SSD16XX_ASSIGN_ARRAY(n, lut_default), \
.profiles = { \
[SSD16XX_PROFILE_FULL] = \
SSD16XX_PROFILE_PTR(DT_CHILD(n, full)), \
SSD16XX_PROFILE_PTR(DT_CHILD(n, full)), \
[SSD16XX_PROFILE_PARTIAL] = \
SSD16XX_PROFILE_PTR(DT_CHILD(n, partial)),\
}, \
}; \
\

View file

@ -69,11 +69,15 @@
#define SSD16XX_CTRL2_ENABLE_ANALOG 0x40
#define SSD16XX_CTRL2_LOAD_TEMPERATURE 0x20
#define SSD16XX_CTRL2_LOAD_LUT 0x10
#define SSD16XX_CTRL2_TO_INITIAL 0x08
#define SSD16XX_CTRL2_TO_PATTERN 0x04
#define SSD16XX_CTRL2_DISABLE_ANALOG 0x02
#define SSD16XX_CTRL2_DISABLE_CLK 0x01
#define SSD16XX_GEN1_CTRL2_TO_INITIAL 0x08
#define SSD16XX_GEN1_CTRL2_TO_PATTERN 0x04
#define SSD16XX_GEN2_CTRL2_MODE2 0x08
#define SSD16XX_GEN2_CTRL2_DISPLAY 0x04
#define SSD16XX_SLEEP_MODE_DSM 0x01
#define SSD16XX_SLEEP_MODE_PON 0x00

View file

@ -41,9 +41,6 @@ properties:
If connected directly the MCU pin should be configured
as active high.
lut-default:
type: uint8-array
tssv:
type: int
description: Temperature Sensor Selection Value
@ -59,9 +56,14 @@ child-binding:
cycle. Refresh profiles are optional and are used to override
defaults loaded from the controllers OTP memory.
Partial refresh will be disabled unless a partial refresh profile
has been specified. That profile may be empty to use the defaults
loaded from OTP if supported by the device.
The driver typically looks for the following child nodes:
- 'full' - Normal / full refresh.
- 'partial' - Partial refresh.
properties:
lut: