Support for ST7567 display 128x64 (I2C, SPI) (#5952)

This commit is contained in:
Anton Viktorov 2023-12-27 02:01:15 +01:00 committed by GitHub
parent 93ac765425
commit 3be97868fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 572 additions and 0 deletions

View file

@ -316,6 +316,9 @@ esphome/components/ssd1331_base/* @kbx81
esphome/components/ssd1331_spi/* @kbx81
esphome/components/ssd1351_base/* @kbx81
esphome/components/ssd1351_spi/* @kbx81
esphome/components/st7567_base/* @latonita
esphome/components/st7567_i2c/* @latonita
esphome/components/st7567_spi/* @latonita
esphome/components/st7735/* @SenexCrenshaw
esphome/components/st7789v/* @kbx81
esphome/components/st7920/* @marsjan155

View file

@ -0,0 +1,55 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.components import display
from esphome.const import (
CONF_LAMBDA,
CONF_RESET_PIN,
)
CODEOWNERS = ["@latonita"]
st7567_base_ns = cg.esphome_ns.namespace("st7567_base")
ST7567 = st7567_base_ns.class_("ST7567", cg.PollingComponent, display.DisplayBuffer)
ST7567Model = st7567_base_ns.enum("ST7567Model")
# todo in future: reuse following constants from const.py when they are released
CONF_INVERT_COLORS = "invert_colors"
CONF_TRANSFORM = "transform"
CONF_MIRROR_X = "mirror_x"
CONF_MIRROR_Y = "mirror_y"
ST7567_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend(
{
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_INVERT_COLORS, default=False): cv.boolean,
cv.Optional(CONF_TRANSFORM): cv.Schema(
{
cv.Optional(CONF_MIRROR_X, default=False): cv.boolean,
cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean,
}
),
}
).extend(cv.polling_component_schema("1s"))
async def setup_st7567(var, config):
await display.register_display(var, config)
if CONF_RESET_PIN in config:
reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN])
cg.add(var.set_reset_pin(reset))
cg.add(var.init_invert_colors(config[CONF_INVERT_COLORS]))
if CONF_TRANSFORM in config:
transform = config[CONF_TRANSFORM]
cg.add(var.init_mirror_x(transform[CONF_MIRROR_X]))
cg.add(var.init_mirror_y(transform[CONF_MIRROR_Y]))
if CONF_LAMBDA in config:
lambda_ = await cg.process_lambda(
config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void
)
cg.add(var.set_writer(lambda_))

View file

@ -0,0 +1,152 @@
#include "st7567_base.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace st7567_base {
static const char *const TAG = "st7567";
void ST7567::setup() {
this->init_internal_(this->get_buffer_length_());
this->display_init_();
}
void ST7567::display_init_() {
ESP_LOGD(TAG, "Initializing ST7567 display...");
this->display_init_registers_();
this->clear();
this->write_display_data();
this->command(ST7567_DISPLAY_ON);
}
void ST7567::display_init_registers_() {
this->command(ST7567_BIAS_9);
this->command(this->mirror_x_ ? ST7567_SEG_REVERSE : ST7567_SEG_NORMAL);
this->command(this->mirror_y_ ? ST7567_COM_NORMAL : ST7567_COM_REMAP);
this->command(ST7567_POWER_CTL | 0x4);
this->command(ST7567_POWER_CTL | 0x6);
this->command(ST7567_POWER_CTL | 0x7);
this->set_brightness(this->brightness_);
this->set_contrast(this->contrast_);
this->command(ST7567_INVERT_OFF | this->invert_colors_);
this->command(ST7567_BOOSTER_ON);
this->command(ST7567_REGULATOR_ON);
this->command(ST7567_POWER_ON);
this->command(ST7567_SCAN_START_LINE);
this->command(ST7567_PIXELS_NORMAL | this->all_pixels_on_);
}
void ST7567::display_sw_refresh_() {
ESP_LOGD(TAG, "Performing refresh sequence...");
this->command(ST7567_SW_REFRESH);
this->display_init_registers_();
}
void ST7567::request_refresh() {
// as per datasheet: It is recommended to use the refresh sequence regularly in a specified interval.
this->refresh_requested_ = true;
}
void ST7567::update() {
this->do_update_();
if (this->refresh_requested_) {
this->refresh_requested_ = false;
this->display_sw_refresh_();
}
this->write_display_data();
}
void ST7567::set_all_pixels_on(bool enable) {
this->all_pixels_on_ = enable;
this->command(ST7567_PIXELS_NORMAL | this->all_pixels_on_);
}
void ST7567::set_invert_colors(bool invert_colors) {
this->invert_colors_ = invert_colors;
this->command(ST7567_INVERT_OFF | this->invert_colors_);
}
void ST7567::set_contrast(uint8_t val) {
this->contrast_ = val & 0b111111;
// 0..63, 26 is normal
// two byte command
// first byte 0x81
// second byte 0-63
this->command(ST7567_SET_EV_CMD);
this->command(this->contrast_);
}
void ST7567::set_brightness(uint8_t val) {
this->brightness_ = val & 0b111;
// 0..7, 5 normal
//********Adjust display brightness********
// 0x20-0x27 is the internal Rb/Ra resistance
// adjustment setting of V5 voltage RR=4.5V
this->command(ST7567_RESISTOR_RATIO | this->brightness_);
}
bool ST7567::is_on() { return this->is_on_; }
void ST7567::turn_on() {
this->command(ST7567_DISPLAY_ON);
this->is_on_ = true;
}
void ST7567::turn_off() {
this->command(ST7567_DISPLAY_OFF);
this->is_on_ = false;
}
void ST7567::set_scroll(uint8_t line) { this->start_line_ = line % this->get_height_internal(); }
int ST7567::get_width_internal() { return 128; }
int ST7567::get_height_internal() { return 64; }
// 128x64, but memory size 132x64, line starts from 0, but if mirrored then it starts from 131, not 127
size_t ST7567::get_buffer_length_() {
return size_t(this->get_width_internal() + 4) * size_t(this->get_height_internal()) / 8u;
}
void HOT ST7567::draw_absolute_pixel_internal(int x, int y, Color color) {
if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) {
return;
}
uint16_t pos = x + (y / 8) * this->get_width_internal();
uint8_t subpos = y & 0x07;
if (color.is_on()) {
this->buffer_[pos] |= (1 << subpos);
} else {
this->buffer_[pos] &= ~(1 << subpos);
}
}
void ST7567::fill(Color color) { memset(buffer_, color.is_on() ? 0xFF : 0x00, this->get_buffer_length_()); }
void ST7567::init_reset_() {
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(true);
delay(1);
// Trigger Reset
this->reset_pin_->digital_write(false);
delay(10);
// Wake up
this->reset_pin_->digital_write(true);
}
}
const char *ST7567::model_str_() { return "ST7567 128x64"; }
} // namespace st7567_base
} // namespace esphome

View file

@ -0,0 +1,100 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/display/display_buffer.h"
namespace esphome {
namespace st7567_base {
static const uint8_t ST7567_BOOSTER_ON = 0x2C; // internal power supply on
static const uint8_t ST7567_REGULATOR_ON = 0x2E; // internal power supply on
static const uint8_t ST7567_POWER_ON = 0x2F; // internal power supply on
static const uint8_t ST7567_DISPLAY_ON = 0xAF; // Display ON. Normal Display Mode.
static const uint8_t ST7567_DISPLAY_OFF = 0xAE; // Display OFF. All SEGs/COMs output with VSS
static const uint8_t ST7567_SET_START_LINE = 0x40;
static const uint8_t ST7567_POWER_CTL = 0x28;
static const uint8_t ST7567_SEG_NORMAL = 0xA0; //
static const uint8_t ST7567_SEG_REVERSE = 0xA1; // mirror X axis (horizontal)
static const uint8_t ST7567_COM_NORMAL = 0xC0; //
static const uint8_t ST7567_COM_REMAP = 0xC8; // mirror Y axis (vertical)
static const uint8_t ST7567_PIXELS_NORMAL = 0xA4; // display ram content
static const uint8_t ST7567_PIXELS_ALL_ON = 0xA5; // all pixels on
static const uint8_t ST7567_INVERT_OFF = 0xA6; // normal pixels
static const uint8_t ST7567_INVERT_ON = 0xA7; // inverted pixels
static const uint8_t ST7567_SCAN_START_LINE = 0x40; // scrolling = 0x40 + (0..63)
static const uint8_t ST7567_COL_ADDR_H = 0x10; // x pos (0..95) 4 MSB
static const uint8_t ST7567_COL_ADDR_L = 0x00; // x pos (0..95) 4 LSB
static const uint8_t ST7567_PAGE_ADDR = 0xB0; // y pos, 8.5 rows (0..8)
static const uint8_t ST7567_BIAS_9 = 0xA2;
static const uint8_t ST7567_CONTRAST = 0x80; // 0x80 + (0..31)
static const uint8_t ST7567_SET_EV_CMD = 0x81;
static const uint8_t ST7567_SET_EV_PARAM = 0x00;
static const uint8_t ST7567_RESISTOR_RATIO = 0x20;
static const uint8_t ST7567_SW_REFRESH = 0xE2;
class ST7567 : public display::DisplayBuffer {
public:
void setup() override;
void update() override;
void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; }
void init_mirror_x(bool mirror_x) { this->mirror_x_ = mirror_x; }
void init_mirror_y(bool mirror_y) { this->mirror_y_ = mirror_y; }
void init_invert_colors(bool invert_colors) { this->invert_colors_ = invert_colors; }
void set_invert_colors(bool invert_colors); // inversion of screen colors
void set_contrast(uint8_t val); // 0..63, 27-30 normal
void set_brightness(uint8_t val); // 0..7, 5 normal
void set_all_pixels_on(bool enable); // turn on all pixels, this doesn't affect RAM
void set_scroll(uint8_t line); // set display start line: for screen scrolling w/o affecting RAM
bool is_on();
void turn_on();
void turn_off();
void request_refresh(); // from datasheet: It is recommended to use the refresh sequence regularly in a specified
// interval.
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
void fill(Color color) override;
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; }
protected:
virtual void command(uint8_t value) = 0;
virtual void write_display_data() = 0;
void init_reset_();
void display_init_();
void display_init_registers_();
void display_sw_refresh_();
void draw_absolute_pixel_internal(int x, int y, Color color) override;
int get_height_internal() override;
int get_width_internal() override;
size_t get_buffer_length_();
int get_offset_x_() { return mirror_x_ ? 4 : 0; };
const char *model_str_();
GPIOPin *reset_pin_{nullptr};
bool is_on_{false};
// float contrast_{1.0};
// float brightness_{1.0};
uint8_t contrast_{27};
uint8_t brightness_{5};
bool mirror_x_{true};
bool mirror_y_{true};
bool invert_colors_{false};
bool all_pixels_on_{false};
uint8_t start_line_{0};
bool refresh_requested_{false};
};
} // namespace st7567_base
} // namespace esphome

View file

@ -0,0 +1 @@
CODEOWNERS = ["@latonita"]

View file

@ -0,0 +1,29 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import st7567_base, i2c
from esphome.const import CONF_ID, CONF_LAMBDA, CONF_PAGES
CODEOWNERS = ["@latonita"]
AUTO_LOAD = ["st7567_base"]
DEPENDENCIES = ["i2c"]
st7567_i2c = cg.esphome_ns.namespace("st7567_i2c")
I2CST7567 = st7567_i2c.class_("I2CST7567", st7567_base.ST7567, i2c.I2CDevice)
CONFIG_SCHEMA = cv.All(
st7567_base.ST7567_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(I2CST7567),
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x3F)),
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await st7567_base.setup_st7567(var, config)
await i2c.register_i2c_device(var, config)

View file

@ -0,0 +1,60 @@
#include "st7567_i2c.h"
#include "esphome/core/log.h"
namespace esphome {
namespace st7567_i2c {
static const char *const TAG = "st7567_i2c";
void I2CST7567::setup() {
ESP_LOGCONFIG(TAG, "Setting up I2C ST7567 display...");
this->init_reset_();
auto err = this->write(nullptr, 0);
if (err != i2c::ERROR_OK) {
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed();
return;
}
ST7567::setup();
}
void I2CST7567::dump_config() {
LOG_DISPLAY("", "I2CST7567", this);
LOG_I2C_DEVICE(this);
ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_());
LOG_PIN(" Reset Pin: ", this->reset_pin_);
ESP_LOGCONFIG(TAG, " Mirror X: %s", YESNO(this->mirror_x_));
ESP_LOGCONFIG(TAG, " Mirror Y: %s", YESNO(this->mirror_y_));
ESP_LOGCONFIG(TAG, " Invert Colors: %s", YESNO(this->invert_colors_));
LOG_UPDATE_INTERVAL(this);
if (this->error_code_ == COMMUNICATION_FAILED) {
ESP_LOGE(TAG, "Communication with I2C ST7567 failed!");
}
}
void I2CST7567::command(uint8_t value) { this->write_byte(0x00, value); }
void HOT I2CST7567::write_display_data() {
// ST7567A has built-in RAM with 132x65 bit capacity which stores the display data.
// but only first 128 pixels from each line are shown on screen
// if screen got flipped horizontally then it shows last 128 pixels,
// so we need to write x coordinate starting from column 4, not column 0
this->command(esphome::st7567_base::ST7567_SET_START_LINE + this->start_line_);
for (uint8_t y = 0; y < (uint8_t) this->get_height_internal() / 8; y++) {
this->command(esphome::st7567_base::ST7567_PAGE_ADDR + y); // Set Page
this->command(esphome::st7567_base::ST7567_COL_ADDR_H); // Set MSB Column address
this->command(esphome::st7567_base::ST7567_COL_ADDR_L + this->get_offset_x_()); // Set LSB Column address
static const size_t BLOCK_SIZE = 64;
for (uint8_t x = 0; x < (uint8_t) this->get_width_internal(); x += BLOCK_SIZE) {
this->write_register(esphome::st7567_base::ST7567_SET_START_LINE, &buffer_[y * this->get_width_internal() + x],
this->get_width_internal() - x > BLOCK_SIZE ? BLOCK_SIZE : this->get_width_internal() - x,
true);
}
}
}
} // namespace st7567_i2c
} // namespace esphome

View file

@ -0,0 +1,23 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/st7567_base/st7567_base.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace st7567_i2c {
class I2CST7567 : public st7567_base::ST7567, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
protected:
void command(uint8_t value) override;
void write_display_data() override;
enum ErrorCode { NONE = 0, COMMUNICATION_FAILED } error_code_{NONE};
};
} // namespace st7567_i2c
} // namespace esphome

View file

@ -0,0 +1 @@
CODEOWNERS = ["@latonita"]

View file

@ -0,0 +1,34 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.components import spi, st7567_base
from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES
CODEOWNERS = ["@latonita"]
AUTO_LOAD = ["st7567_base"]
DEPENDENCIES = ["spi"]
st7567_spi = cg.esphome_ns.namespace("st7567_spi")
SPIST7567 = st7567_spi.class_("SPIST7567", st7567_base.ST7567, spi.SPIDevice)
CONFIG_SCHEMA = cv.All(
st7567_base.ST7567_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(SPIST7567),
cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(spi.spi_device_schema()),
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await st7567_base.setup_st7567(var, config)
await spi.register_spi_device(var, config)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View file

@ -0,0 +1,66 @@
#include "st7567_spi.h"
#include "esphome/core/log.h"
namespace esphome {
namespace st7567_spi {
static const char *const TAG = "st7567_spi";
void SPIST7567::setup() {
ESP_LOGCONFIG(TAG, "Setting up SPI ST7567 display...");
this->spi_setup();
this->dc_pin_->setup();
if (this->cs_)
this->cs_->setup();
this->init_reset_();
ST7567::setup();
}
void SPIST7567::dump_config() {
LOG_DISPLAY("", "SPI ST7567", this);
ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_());
LOG_PIN(" CS Pin: ", this->cs_);
LOG_PIN(" DC Pin: ", this->dc_pin_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
ESP_LOGCONFIG(TAG, " Mirror X: %s", YESNO(this->mirror_x_));
ESP_LOGCONFIG(TAG, " Mirror Y: %s", YESNO(this->mirror_y_));
ESP_LOGCONFIG(TAG, " Invert Colors: %s", YESNO(this->invert_colors_));
LOG_UPDATE_INTERVAL(this);
}
void SPIST7567::command(uint8_t value) {
if (this->cs_)
this->cs_->digital_write(true);
this->dc_pin_->digital_write(false);
delay(1);
this->enable();
if (this->cs_)
this->cs_->digital_write(false);
this->write_byte(value);
if (this->cs_)
this->cs_->digital_write(true);
this->disable();
}
void HOT SPIST7567::write_display_data() {
// ST7567A has built-in RAM with 132x65 bit capacity which stores the display data.
// but only first 128 pixels from each line are shown on screen
// if screen got flipped horizontally then it shows last 128 pixels,
// so we need to write x coordinate starting from column 4, not column 0
this->command(esphome::st7567_base::ST7567_SET_START_LINE + this->start_line_);
for (uint8_t y = 0; y < (uint8_t) this->get_height_internal() / 8; y++) {
this->dc_pin_->digital_write(false);
this->command(esphome::st7567_base::ST7567_PAGE_ADDR + y); // Set Page
this->command(esphome::st7567_base::ST7567_COL_ADDR_H); // Set MSB Column address
this->command(esphome::st7567_base::ST7567_COL_ADDR_L + this->get_offset_x_()); // Set LSB Column address
this->dc_pin_->digital_write(true);
this->enable();
this->write_array(&this->buffer_[y * this->get_width_internal()], this->get_width_internal());
this->disable();
}
}
} // namespace st7567_spi
} // namespace esphome

View file

@ -0,0 +1,29 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/st7567_base/st7567_base.h"
#include "esphome/components/spi/spi.h"
namespace esphome {
namespace st7567_spi {
class SPIST7567 : public st7567_base::ST7567,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING,
spi::DATA_RATE_8MHZ> {
public:
void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; }
void setup() override;
void dump_config() override;
protected:
void command(uint8_t value) override;
void write_display_data() override;
GPIOPin *dc_pin_;
};
} // namespace st7567_spi
} // namespace esphome

View file

@ -3259,6 +3259,25 @@ display:
inverted: true
lambda: |-
it.rectangle(0, 0, it.get_width(), it.get_height());
- platform: st7567_i2c
id: st7735_display_i2c
address: 0x3F
i2c_id: i2c_bus
lambda: |-
it.rectangle(0, 0, it.get_width(), it.get_height());
- platform: st7567_spi
id: st7735_display_spi
cs_pin:
allow_other_uses: true
number: GPIO5
dc_pin:
allow_other_uses: true
number: GPIO16
reset_pin:
allow_other_uses: true
number: GPIO23
lambda: |-
it.rectangle(0, 0, it.get_width(), it.get_height());
- platform: st7735
id: st7735_display
model: INITR_BLACKTAB