[sh1122] Add OLED display driver (#15295)

Co-authored-by: Nicolas Reinecke <nr@das-labor.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
pl4nkton
2026-10-12 13:11:51 -10:00
committed by GitHub
co-authored by Nicolas Reinecke J. Nick Koston
parent 73d5bb3c97
commit 0c43d689d7
12 changed files with 398 additions and 0 deletions
+2
View File
@@ -522,6 +522,8 @@ esphome/components/sfa30/* @ghsensdev
esphome/components/sfa40/* @NoQuarrel
esphome/components/sgp40/* @SenexCrenshaw
esphome/components/sgp4x/* @martgras @SenexCrenshaw
esphome/components/sh1122_base/* @pl4nkton
esphome/components/sh1122_spi/* @pl4nkton
esphome/components/sha256/* @esphome/core
esphome/components/shelly_dimmer/* @edge90 @rnauber
esphome/components/sht3xd/* @mrtoy-me
@@ -0,0 +1,43 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import display
import esphome.config_validation as cv
from esphome.const import CONF_BRIGHTNESS, CONF_LAMBDA, CONF_MODEL, CONF_RESET_PIN
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
CODEOWNERS = ["@pl4nkton"]
DOMAIN = "sh1122_base"
sh1122_base_ns = cg.esphome_ns.namespace("sh1122_base")
SH1122 = sh1122_base_ns.class_("SH1122", cg.PollingComponent, display.DisplayBuffer)
SH1122Model = sh1122_base_ns.enum("SH1122Model")
MODELS = {
"SH1122_256X64": SH1122Model.SH1122_MODEL_256_64,
}
SH1122_MODEL = cv.enum(MODELS, upper=True, space="_")
SH1122_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend(
{
cv.Optional(CONF_MODEL, default="SH1122_256X64"): SH1122_MODEL,
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage,
}
).extend(cv.polling_component_schema("1s"))
async def setup_sh1122(var: MockObj, config: ConfigType) -> None:
await display.register_display(var, config)
cg.add(var.set_model(config[CONF_MODEL]))
if (reset_pin := config.get(CONF_RESET_PIN)) is not None:
reset = await cg.gpio_pin_expression(reset_pin)
cg.add(var.set_reset_pin(reset))
cg.add(var.init_brightness(config[CONF_BRIGHTNESS]))
if (lambda_config := config.get(CONF_LAMBDA)) is not None:
lambda_ = await cg.process_lambda(
lambda_config, [(display.DisplayRef, "it")], return_type=cg.void
)
cg.add(var.set_writer(lambda_))
@@ -0,0 +1,146 @@
#include "sh1122_base.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cstring>
namespace esphome::sh1122_base {
ESPHOME_LOG_TAG(TAG, "sh1122");
static constexpr uint8_t SH1122_MAX_CONTRAST = 255;
static constexpr uint8_t SH1122_COLORMASK = 0x0f;
static constexpr uint8_t SH1122_COLORSHIFT = 4;
static constexpr uint8_t SH1122_PIXELSPERBYTE = 2;
static constexpr uint8_t SH1122_SETLOWCOLUMNADDRESS = 0x00;
static constexpr uint8_t SH1122_SETHIGHCOLUMNADDRESS = 0x10;
static constexpr uint8_t SH1122_SETDISCHARGEVSLLEVEL = 0x30;
static constexpr uint8_t SH1122_SETDISPLAYSTARTLINE = 0x40;
static constexpr uint8_t SH1122_SETCONTRASTCURRENT = 0x81;
static constexpr uint8_t SH1122_SETSEGMENTREMAP = 0xA0;
static constexpr uint8_t SH1122_SETDISPLAYOFFON = 0xA4;
static constexpr uint8_t SH1122_SETNORMALDISPLAY = 0xA6;
static constexpr uint8_t SH1122_SETMULTIPLEXRATIO = 0xA8;
static constexpr uint8_t SH1122_DCDCSETTING = 0xAD;
static constexpr uint8_t SH1122_SETDISPLAYOFF = 0xAE;
static constexpr uint8_t SH1122_SETDISPLAYON = 0xAF;
static constexpr uint8_t SH1122_SETROWADDRESS = 0xB0;
static constexpr uint8_t SH1122_SETSCANDIRECTION = 0xC0;
static constexpr uint8_t SH1122_SETDISPLAYOFFSET = 0xD3;
static constexpr uint8_t SH1122_SETCLOCKDIVIDER = 0xD5;
static constexpr uint8_t SH1122_SETDISCHARGEPRECHARGEPERIOD = 0xD9;
static constexpr uint8_t SH1122_SETVCOMDESELECTLEVEL = 0xDB;
static constexpr uint8_t SH1122_SETVSEGMLEVEL = 0xDC;
void SH1122::setup() {
this->init_internal_(this->get_buffer_length_());
if (this->buffer_ == nullptr) {
this->mark_failed();
return;
}
this->turn_off();
this->command2_(SH1122_SETCLOCKDIVIDER, 0x50);
this->command2_(SH1122_SETMULTIPLEXRATIO, 0x3F);
this->command2_(SH1122_SETDISPLAYOFFSET, 0x00);
this->command2_(SH1122_SETROWADDRESS, 0x00);
this->command_(SH1122_SETDISPLAYSTARTLINE | 32); // the panel's rows start half way down the 128 line RAM
this->command_(SH1122_SETDISCHARGEVSLLEVEL);
this->command2_(SH1122_DCDCSETTING, 0x81);
this->command_(SH1122_SETSEGMENTREMAP | 0x01); // column 0 on the right
this->command_(SH1122_SETSCANDIRECTION | 0x08); // rows scanned bottom up
this->command2_(SH1122_SETDISCHARGEPRECHARGEPERIOD, 0x28);
this->command2_(SH1122_SETVCOMDESELECTLEVEL, 0x35);
this->command2_(SH1122_SETVSEGMLEVEL, 0x35);
this->command_(SH1122_SETNORMALDISPLAY);
this->command_(SH1122_SETHIGHCOLUMNADDRESS);
this->command_(SH1122_SETLOWCOLUMNADDRESS);
this->command_(SH1122_SETDISPLAYOFFON);
this->set_brightness(this->brightness_);
this->display(); // the buffer starts cleared; push it so no power-on garbage shows
this->turn_on();
}
void SH1122::display() {
// Return the RAM pointer to the top left corner in one transfer, then stream the frame
const uint8_t set_address[] = {SH1122_SETHIGHCOLUMNADDRESS, SH1122_SETLOWCOLUMNADDRESS, SH1122_SETROWADDRESS, 0x00};
this->write_command(set_address, sizeof(set_address));
this->write_display_data();
}
void SH1122::update() {
this->do_update_();
this->display();
}
void SH1122::set_brightness(float brightness) {
this->brightness_ = clamp(brightness, 0.0F, 1.0F);
this->command2_(SH1122_SETCONTRASTCURRENT, int(SH1122_MAX_CONTRAST * (this->brightness_)));
}
void SH1122::turn_on() {
this->command_(SH1122_SETDISPLAYON);
this->is_on_ = true;
}
void SH1122::turn_off() {
this->command_(SH1122_SETDISPLAYOFF);
this->is_on_ = false;
}
int SH1122::get_height_internal() {
switch (this->model_) {
case SH1122_MODEL_256_64:
return 64;
default:
return 0;
}
}
int SH1122::get_width_internal() {
switch (this->model_) {
case SH1122_MODEL_256_64:
return 256;
default:
return 0;
}
}
size_t SH1122::get_buffer_length_() {
return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / SH1122_PIXELSPERBYTE;
}
void HOT SH1122::draw_absolute_pixel_internal(int x, int y, Color color) {
const int width = this->get_width_internal();
if (x >= width || x < 0 || y >= this->get_height_internal() || y < 0)
return;
// Two pixels per byte, the left one in the high nibble
const uint32_t color4 = display::ColorUtil::color_to_grayscale4(color) & SH1122_COLORMASK;
const uint16_t pos = (x / SH1122_PIXELSPERBYTE) + (y * width / SH1122_PIXELSPERBYTE);
const uint8_t shift = (1u - (x % SH1122_PIXELSPERBYTE)) * SH1122_COLORSHIFT;
this->buffer_[pos] = (this->buffer_[pos] & (static_cast<uint8_t>(~SH1122_COLORMASK) >> shift)) | (color4 << shift);
}
void SH1122::fill(Color color) {
// If clipping is active, fall back to base implementation
if (this->get_clipping().is_set()) {
Display::fill(color);
return;
}
const uint32_t color4 = display::ColorUtil::color_to_grayscale4(color);
const uint8_t fill = (color4 & SH1122_COLORMASK) | ((color4 & SH1122_COLORMASK) << SH1122_COLORSHIFT);
memset(this->buffer_, fill, this->get_buffer_length_());
}
void SH1122::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 LogString *SH1122::model_str_() {
switch (this->model_) {
case SH1122_MODEL_256_64:
return LOG_STR("SH1122 256x64");
default:
return LOG_STR("Unknown");
}
}
} // namespace esphome::sh1122_base
@@ -0,0 +1,59 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/components/display/display_buffer.h"
namespace esphome::sh1122_base {
enum SH1122Model : uint8_t {
SH1122_MODEL_256_64 = 0,
};
class SH1122 : public display::DisplayBuffer {
public:
void setup() override;
void display();
void update() override;
void set_model(SH1122Model model) { this->model_ = model; }
void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; }
void init_brightness(float brightness) { this->brightness_ = brightness; }
void set_brightness(float brightness);
bool is_on() { return this->is_on_; }
void turn_on();
void turn_off();
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_GRAYSCALE; }
protected:
/// Sends a command byte and its arguments; the SH1122 takes command arguments with D/C low as well
virtual void write_command(const uint8_t *bytes, size_t len) = 0;
virtual void write_display_data() = 0;
void command_(uint8_t value) { this->write_command(&value, 1); }
void command2_(uint8_t value, uint8_t data) {
const uint8_t bytes[2] = {value, data};
this->write_command(bytes, 2);
}
void init_reset_();
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_();
const LogString *model_str_();
GPIOPin *reset_pin_{nullptr};
float brightness_{1.0};
SH1122Model model_{SH1122_MODEL_256_64};
bool is_on_{false};
};
} // namespace esphome::sh1122_base
@@ -0,0 +1 @@
DOMAIN = "sh1122_spi"
+37
View File
@@ -0,0 +1,37 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import sh1122_base, spi
import esphome.config_validation as cv
from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES
from esphome.types import ConfigType
from . import DOMAIN
CODEOWNERS = ["@pl4nkton"]
AUTO_LOAD = ["sh1122_base"]
DEPENDENCIES = ["spi"]
sh1122_spi = cg.esphome_ns.namespace("sh1122_spi")
SPISH1122 = sh1122_spi.class_("SPISH1122", sh1122_base.SH1122, spi.SPIDevice)
CONFIG_SCHEMA = cv.All(
sh1122_base.SH1122_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(SPISH1122),
cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
}
).extend(spi.spi_device_schema(cs_pin_required=False)),
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
)
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
DOMAIN, require_miso=False, require_mosi=True
)
async def to_code(config: ConfigType) -> None:
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
var = cg.new_Pvariable(config[CONF_ID], dc)
await sh1122_base.setup_sh1122(var, config)
await spi.register_spi_device(var, config, write_only=True)
@@ -0,0 +1,42 @@
#include "sh1122_spi.h"
#include "esphome/core/log.h"
namespace esphome::sh1122_spi {
ESPHOME_LOG_TAG(TAG, "sh1122_spi");
void SPISH1122::setup() {
this->spi_setup();
this->dc_pin_->setup();
this->init_reset_();
delay(500); // NOLINT
SH1122::setup();
}
void SPISH1122::dump_config() {
LOG_DISPLAY("", "SPI SH1122", this);
ESP_LOGCONFIG(TAG,
" Model: %s\n"
" Initial Brightness: %.2f",
LOG_STR_ARG(this->model_str_()), this->brightness_);
LOG_PIN(" CS Pin: ", this->cs_);
LOG_PIN(" DC Pin: ", this->dc_pin_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
LOG_UPDATE_INTERVAL(this);
}
// enable() and disable() drive the CS pin. The D/C setup time is a few tens of nanoseconds, so the
// GPIO write followed by the CS edge already satisfies it; no delay is needed.
void SPISH1122::write_command(const uint8_t *bytes, size_t len) {
this->dc_pin_->digital_write(false);
this->enable();
this->write_array(bytes, len);
this->disable();
}
void HOT SPISH1122::write_display_data() {
this->dc_pin_->digital_write(true);
this->enable();
this->write_array(this->buffer_, this->get_buffer_length_());
this->disable();
}
} // namespace esphome::sh1122_spi
@@ -0,0 +1,25 @@
#pragma once
#include "esphome/components/sh1122_base/sh1122_base.h"
#include "esphome/components/spi/spi.h"
namespace esphome::sh1122_spi {
class SPISH1122 final : public sh1122_base::SH1122,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH,
spi::CLOCK_PHASE_TRAILING, spi::DATA_RATE_8MHZ> {
public:
explicit SPISH1122(GPIOPin *dc_pin) : dc_pin_(dc_pin) {}
void setup() override;
void dump_config() override;
protected:
void write_command(const uint8_t *bytes, size_t len) override;
void write_display_data() override;
GPIOPin *dc_pin_;
};
} // namespace esphome::sh1122_spi
+19
View File
@@ -0,0 +1,19 @@
display:
- platform: sh1122_spi
cs_pin: ${cs_pin}
dc_pin: ${dc_pin}
reset_pin: ${reset_pin}
brightness: 80%
pages:
- id: sh1122_spi_page1
lambda: |-
it.rectangle(0, 0, it.get_width(), it.get_height());
- id: sh1122_spi_page2
lambda: |-
it.rectangle(0, 0, it.get_width(), it.get_height());
on_page_change:
from: sh1122_spi_page1
to: sh1122_spi_page2
then:
lambda: |-
ESP_LOGD("display", "1 -> 2");
@@ -0,0 +1,8 @@
substitutions:
cs_pin: GPIO12
dc_pin: GPIO13
reset_pin: GPIO14
packages:
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
sh1122_spi: !include common.yaml
@@ -0,0 +1,8 @@
substitutions:
cs_pin: GPIO5
dc_pin: GPIO15
reset_pin: GPIO16
packages:
spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml
sh1122_spi: !include common.yaml
@@ -0,0 +1,8 @@
substitutions:
cs_pin: GPIO20
dc_pin: GPIO21
reset_pin: GPIO22
packages:
spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml
sh1122_spi: !include common.yaml