MAX6921: basic working approach

Using PollingComponent as refresh cylce which leads to display flicker and
interruptions by Wifi operations.
This commit is contained in:
endym 2024-03-03 15:15:56 +01:00
parent 61ce15db44
commit 959d232160
4 changed files with 384 additions and 46 deletions

View file

@ -1,29 +0,0 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import spi
from esphome.const import CONF_ID
from esphome import pins
DEPENDENCIES = ["spi"]
# MULTI_CONF = True
CODEOWNERS = ["@endym"]
CONF_LOAD_PIN = "load_pin"
max6921_ns = cg.esphome_ns.namespace("max6921")
MAX6921 = max6921_ns.class_("MAX6921", cg.Component, spi.SPIDevice)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(MAX6921),
cv.Required(CONF_LOAD_PIN): pins.gpio_input_pin_schema,
}
).extend(spi.spi_device_schema(cs_pin_required=False))
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await spi.register_spi_device(var, config)
pin = await cg.gpio_pin_expression(config[CONF_LOAD_PIN])
cg.add(var.set_load_pin(pin))

View file

@ -0,0 +1,55 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import display, spi
from esphome.const import CONF_ID, CONF_LAMBDA
from esphome import pins
# from esphome.const import CONF_INTENSITY
DEPENDENCIES = ["spi"]
CODEOWNERS = ["@endym"]
CONF_LOAD_PIN = "load_pin"
CONF_BLANK_PIN = "blank_pin"
CONF_NUM_DIGITS = "num_digits"
# CONF_REVERSE_ENABLE = "reverse_enable"
max6921_ns = cg.esphome_ns.namespace("max6921")
MAX6921Component = max6921_ns.class_(
"MAX6921Component", cg.PollingComponent, spi.SPIDevice
)
MAX6921ComponentRef = MAX6921Component.operator("ref")
CONFIG_SCHEMA = (
display.BASIC_DISPLAY_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(MAX6921Component),
cv.Required(CONF_LOAD_PIN): pins.gpio_input_pin_schema,
cv.Required(CONF_BLANK_PIN): pins.gpio_input_pin_schema,
cv.Required(CONF_NUM_DIGITS): cv.int_range(min=1, max=20),
# cv.Optional(CONF_INTENSITY, default=15): cv.int_range(min=0, max=15),
# cv.Optional(CONF_REVERSE_ENABLE, default=False): cv.boolean,
}
)
.extend(cv.polling_component_schema("1ms"))
.extend(spi.spi_device_schema(cs_pin_required=False))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await spi.register_spi_device(var, config)
await display.register_display(var, config)
load_pin = await cg.gpio_pin_expression(config[CONF_LOAD_PIN])
cg.add(var.set_load_pin(load_pin))
blank_pin = await cg.gpio_pin_expression(config[CONF_BLANK_PIN])
cg.add(var.set_blank_pin(blank_pin))
cg.add(var.set_num_digits(config[CONF_NUM_DIGITS]))
# cg.add(var.set_intensity(config[CONF_INTENSITY]))
# cg.add(var.set_reverse(config[CONF_REVERSE_ENABLE]))
if CONF_LAMBDA in config:
lambda_ = await cg.process_lambda(
config[CONF_LAMBDA], [(MAX6921ComponentRef, "it")], return_type=cg.void
)
cg.add(var.set_writer(lambda_))

View file

@ -8,35 +8,316 @@
namespace esphome {
namespace max6921 {
#define ARRAY_ELEM_COUNT(array) (sizeof(array)/sizeof(array[0]))
static const char *const TAG = "max6921";
float MAX6921::get_setup_priority() const { return setup_priority::HARDWARE; }
// display segment to DOUTx mapping...
static const uint8_t DISP_SEG_TO_OUT[] = {
// a b c d e f g dp
0, 2, 5, 6, 4, 1, 3, 7
};
void MAX6921::setup() {
// display character position (left to right) to DOUTx mapping...
static const uint8_t DISP_POS_TO_OUT[] = {
// left 1 2 3 4 5 6 7 8 right
15, 14, 13, 16, 12, 17, 11, 18
};
// segments of 7-segment character
static const uint8_t SEG_A = (1<<0);
static const uint8_t SEG_B = (1<<1);
static const uint8_t SEG_C = (1<<2);
static const uint8_t SEG_D = (1<<3);
static const uint8_t SEG_E = (1<<4);
static const uint8_t SEG_F = (1<<5);
static const uint8_t SEG_G = (1<<6);
static const uint8_t SEG_DP = (1<<7);
static const uint8_t SEG_UNSUPPORTED_CHAR = 0;
// ASCII table from 0x20..0x7E
const uint8_t ASCII_TO_SEG[95] PROGMEM = {
0, // ' ', (0x20)
SEG_B|SEG_C|SEG_DP, // '!', (0x21)
SEG_B|SEG_F, // '"', (0x22)
SEG_UNSUPPORTED_CHAR, // '#', (0x23)
SEG_UNSUPPORTED_CHAR, // '$', (0x24)
SEG_UNSUPPORTED_CHAR, // '%', (0x25)
SEG_UNSUPPORTED_CHAR, // '&', (0x26)
SEG_F, // ''', (0x27)
SEG_A|SEG_D|SEG_E|SEG_F, // '(', (0x28)
SEG_A|SEG_B|SEG_C|SEG_D, // ')', (0x29)
SEG_UNSUPPORTED_CHAR, // '*', (0x2A)
SEG_UNSUPPORTED_CHAR, // '+', (0x2B)
SEG_DP, // ',', (0x2C)
SEG_A, // '-', (0x2D)
SEG_DP, // '.', (0x2E)
SEG_UNSUPPORTED_CHAR, // '/', (0x2F)
SEG_A|SEG_B|SEG_C|SEG_D|SEG_E|SEG_F, // '0', (0x30)
SEG_B|SEG_C, // '1', (0x31)
SEG_A|SEG_B|SEG_D|SEG_E|SEG_G, // '2', (0x32)
SEG_A|SEG_B|SEG_C|SEG_D|SEG_G, // '3', (0x33)
SEG_B|SEG_C|SEG_F|SEG_G, // '4', (0x34)
SEG_A|SEG_C|SEG_D|SEG_F|SEG_G, // '5', (0x35)
SEG_A|SEG_C|SEG_D|SEG_E|SEG_F|SEG_G, // '6', (0x36)
SEG_A|SEG_B|SEG_C, // '7', (0x37)
SEG_A|SEG_B|SEG_C|SEG_D|SEG_E|SEG_F|SEG_G, // '8', (0x38)
SEG_A|SEG_B|SEG_C|SEG_D|SEG_F|SEG_G, // '9', (0x39)
SEG_UNSUPPORTED_CHAR, // ':', (0x3A)
SEG_UNSUPPORTED_CHAR, // ';', (0x3B)
SEG_UNSUPPORTED_CHAR, // '<', (0x3C)
SEG_D|SEG_G, // '=', (0x3D)
SEG_UNSUPPORTED_CHAR, // '>', (0x3E)
SEG_A|SEG_B|SEG_E|SEG_G, // '?', (0x3F)
SEG_A|SEG_B|SEG_D|SEG_E|SEG_F|SEG_G, // '@', (0x40)
SEG_A|SEG_B|SEG_C|SEG_E|SEG_F|SEG_G, // 'A', (0x41)
SEG_C|SEG_D|SEG_E|SEG_F|SEG_G, // 'B', (0x42)
SEG_A|SEG_D|SEG_E|SEG_F, // 'C', (0x43)
SEG_B|SEG_C|SEG_D|SEG_E|SEG_G, // 'D', (0x44)
SEG_A|SEG_D|SEG_E|SEG_F|SEG_G, // 'E', (0x45)
SEG_A|SEG_E|SEG_F|SEG_G, // 'F', (0x46)
SEG_A|SEG_C|SEG_D|SEG_E|SEG_F, // 'G', (0x47)
SEG_B|SEG_C|SEG_E|SEG_F|SEG_G, // 'H', (0x48)
SEG_B|SEG_C, // 'I', (0x49)
SEG_B|SEG_C|SEG_D|SEG_E, // 'J', (0x4A)
SEG_UNSUPPORTED_CHAR, // 'K', (0x4B)
SEG_D|SEG_E|SEG_F, // 'L', (0x4C)
SEG_UNSUPPORTED_CHAR, // 'M', (0x4D)
SEG_C|SEG_E|SEG_G, // 'N', (0x4E)
SEG_A|SEG_B|SEG_C|SEG_D|SEG_E|SEG_F, // 'O', (0x4F)
SEG_A|SEG_B|SEG_E|SEG_F|SEG_G, // 'P', (0x50)
SEG_UNSUPPORTED_CHAR, // 'Q', (0x51)
SEG_E|SEG_G, // 'R', (0x52)
SEG_A|SEG_C|SEG_D|SEG_F|SEG_G, // 'S', (0x53)
SEG_UNSUPPORTED_CHAR, // 'T', (0x54)
SEG_B|SEG_C|SEG_D|SEG_E|SEG_F, // 'U', (0x55)
SEG_UNSUPPORTED_CHAR, // 'V', (0x56)
SEG_UNSUPPORTED_CHAR, // 'W', (0x57)
SEG_UNSUPPORTED_CHAR, // 'X', (0x58)
SEG_B|SEG_E|SEG_F|SEG_G, // 'Y', (0x59)
SEG_UNSUPPORTED_CHAR, // 'Z', (0x5A)
SEG_A|SEG_D|SEG_E|SEG_F, // '[', (0x5B)
SEG_UNSUPPORTED_CHAR, // '\', (0x5C)
SEG_A|SEG_B|SEG_C|SEG_D, // ']', (0x5D)
SEG_UNSUPPORTED_CHAR, // '^', (0x5E)
SEG_D, // '_', (0x5F)
SEG_F, // '`', (0x60)
SEG_A|SEG_B|SEG_C|SEG_E|SEG_F|SEG_G, // 'a', (0x61)
SEG_C|SEG_D|SEG_E|SEG_F|SEG_G, // 'b', (0x62)
SEG_D|SEG_E|SEG_G, // 'c', (0x63)
SEG_B|SEG_C|SEG_D|SEG_E|SEG_G, // 'd', (0x64)
SEG_A|SEG_D|SEG_E|SEG_F|SEG_G, // 'e', (0x65)
SEG_A|SEG_E|SEG_F|SEG_G, // 'f', (0x66)
SEG_A|SEG_C|SEG_D|SEG_E|SEG_F, // 'g', (0x67)
SEG_C|SEG_E|SEG_F|SEG_G, // 'h', (0x68)
SEG_C, // 'i', (0x69)
SEG_B|SEG_C|SEG_D|SEG_E, // 'j', (0x6A)
SEG_UNSUPPORTED_CHAR, // 'k', (0x6B)
SEG_D|SEG_E|SEG_F, // 'l', (0x6C)
SEG_UNSUPPORTED_CHAR, // 'm', (0x6D)
SEG_C|SEG_E|SEG_G, // 'n', (0x6E)
SEG_C|SEG_D|SEG_E|SEG_G, // 'o', (0x6F)
SEG_A|SEG_B|SEG_E|SEG_F|SEG_G, // 'p', (0x70)
SEG_UNSUPPORTED_CHAR, // 'q', (0x71)
SEG_E|SEG_G, // 'r', (0x72)
SEG_A|SEG_C|SEG_D|SEG_F|SEG_G, // 's', (0x73)
SEG_UNSUPPORTED_CHAR, // 't', (0x74)
SEG_C|SEG_D|SEG_E, // 'u', (0x75)
SEG_UNSUPPORTED_CHAR, // 'v', (0x76)
SEG_UNSUPPORTED_CHAR, // 'w', (0x77)
SEG_UNSUPPORTED_CHAR, // 'x', (0x78)
SEG_B|SEG_E|SEG_F|SEG_G, // 'y', (0x79)
SEG_UNSUPPORTED_CHAR, // 'z', (0x7A)
SEG_B|SEG_C|SEG_G, // '{', (0x7B)
SEG_UNSUPPORTED_CHAR, // '|', (0x7C)
SEG_E|SEG_F|SEG_G, // '}', (0x7D)
SEG_UNSUPPORTED_CHAR, // '~', (0x7E)
};
void MAX6921Component::init_display_(void) {
this->display_.out_buf_size_ = this->display_.num_digits * 3;
this->display_.out_buf_ = new uint8_t[this->display_.out_buf_size_]; // NOLINT
memset(this->display_.out_buf_, 0, this->display_.out_buf_size_);
this->display_.current_text = new char[this->display_.num_digits + 1]; // NOLINT
this->display_.current_text[0] = 0;
this->display_.current_pos = 1;
// find smallest segment DOUT number...
this->display_.seg_out_smallest = 19;
for (uint8_t i=0; i<ARRAY_ELEM_COUNT(DISP_SEG_TO_OUT); i++) {
if (DISP_SEG_TO_OUT[i] < this->display_.seg_out_smallest)
this->display_.seg_out_smallest = DISP_SEG_TO_OUT[i];
}
}
void MAX6921Component::init_font_(void) {
uint8_t seg_data;
this->ascii_out_data_ = new uint8_t[ARRAY_ELEM_COUNT(ASCII_TO_SEG)]; // NOLINT
for (size_t ascii_idx=0; ascii_idx<ARRAY_ELEM_COUNT(ASCII_TO_SEG); ascii_idx++) {
this->ascii_out_data_[ascii_idx] = 0;
seg_data = progmem_read_byte(&ASCII_TO_SEG[ascii_idx]);
if (seg_data & SEG_A)
this->ascii_out_data_[ascii_idx] |= (1 << (DISP_SEG_TO_OUT[0] % 8));
if (seg_data & SEG_B)
this->ascii_out_data_[ascii_idx] |= (1 << (DISP_SEG_TO_OUT[1] % 8));
if (seg_data & SEG_C)
this->ascii_out_data_[ascii_idx] |= (1 << (DISP_SEG_TO_OUT[2] % 8));
if (seg_data & SEG_D)
this->ascii_out_data_[ascii_idx] |= (1 << (DISP_SEG_TO_OUT[3] % 8));
if (seg_data & SEG_E)
this->ascii_out_data_[ascii_idx] |= (1 << (DISP_SEG_TO_OUT[4] % 8));
if (seg_data & SEG_F)
this->ascii_out_data_[ascii_idx] |= (1 << (DISP_SEG_TO_OUT[5] % 8));
if (seg_data & SEG_G)
this->ascii_out_data_[ascii_idx] |= (1 << (DISP_SEG_TO_OUT[6] % 8));
if (seg_data & SEG_DP)
this->ascii_out_data_[ascii_idx] |= (1 << (DISP_SEG_TO_OUT[7] % 8));
}
}
float MAX6921Component::get_setup_priority() const { return setup_priority::BUS; }
void MAX6921Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up MAX6921...");
this->spi_setup();
this->load_pin_->setup();
this->disable_load();
this->load_pin_->pin_mode(gpio::FLAG_OUTPUT);
this->blank_pin_->setup();
this->blank_pin_->pin_mode(gpio::FLAG_OUTPUT);
this->disable_load(); // disable output latch
this->enable_blank(); // disable display
this->init_display_();
this->init_font_();
this->disable_blank(); // enable display (max. intensity)
}
void MAX6921::dump_config() {
void MAX6921Component::dump_config() {
ESP_LOGCONFIG(TAG, "MAX6921:");
LOG_PIN(" LOAD Pin: ", this->cs_);
LOG_PIN(" LOAD Pin: ", this->load_pin_);
LOG_PIN(" BLANK Pin: ", this->blank_pin_);
}
/*
* Clocks data into MAX6921 via SPI.
* Clocks data into MAX6921 via SPI (MSB first).
* Data must contain 3 bytes with following format:
* bit | 23 | 22 | ... | 4 | 3 | 2 | 1 | 0
* ----------------------------------------
* DOUT | 19 | 18 | ... | 0 | x | x | x | x
* bit | 23 | 22 | 21 | 20 | 19 | 18 | ... | 1 | 0
* ------------------------------------------------
* DOUT | x | x | x | x | 19 | 18 | ... | 1 | 0
*/
void MAX6921::write_data(uint8_t *ptr, size_t length) {
void MAX6921Component::write_data(uint8_t *ptr, size_t length) {
uint8_t data[3];
static bool once = false;
assert(length == 3);
this->disable_load(); // set LOAD to low
this->transfer_array(ptr, length);
memcpy(data, ptr, sizeof(data)); // make copy of data, because transfer buffer will be overwritten with SPI answer
if (!once)
ESP_LOGD(TAG, "SPI(%u): 0x%02x%02x%02x", length, data[0], data[1], data[2]);
this->transfer_array(data, sizeof(data));
this->enable_load(); // set LOAD to high to update output latch
// delayMicroseconds(10);
once = true;
}
void MAX6921Component::update() {
/*
if (this->intensity_changed_) {
this->send_to_all_(MAX6921_REGISTER_INTENSITY, this->intensity_);
this->intensity_changed_ = false;
}
for (uint8_t i = 0; i < this->num_chips_ * 8; i++)
this->buffer_[i] = 0;
*/
if (this->writer_.has_value())
(*this->writer_)(*this);
this->display();
}
void MAX6921Component::display() {
static uint count = 0;
if (count < this->display_.num_digits) {
count++;
ESP_LOGD(TAG, "%s(): SPI transfer for position %u: 0x%02x%02x%02x", __func__,
this->display_.current_pos,
this->display_.out_buf_[(this->display_.current_pos-1)*3],
this->display_.out_buf_[(this->display_.current_pos-1)*3+1],
this->display_.out_buf_[(this->display_.current_pos-1)*3+2]);
}
// write MAX9621 data of current position...
this->write_data(&this->display_.out_buf_[(this->display_.current_pos-1)*3], 3);
// next display position...
if (++this->display_.current_pos > this->display_.num_digits)
this->display_.current_pos = 1;
}
uint8_t MAX6921Component::print(uint8_t start_pos, const char *str) {
uint8_t pos = start_pos;
// ESP_LOGD(TAG, "%s(): str=%s, prev=%s", __func__, str, this->display_.current_text);
if (strcmp(str, this->display_.current_text) == 0) // display text not changed?
return strlen(str); // yes -> abort
strncpy(this->display_.current_text, str, this->display_.num_digits);
this->display_.current_text[this->display_.num_digits] = 0;
for (; *str != '\0'; str++) {
uint32_t out_data;
if (pos >= this->display_.num_digits) {
ESP_LOGE(TAG, "MAX6921 string too long or invalid position for the display!");
break;
}
// create segment data...
ESP_LOGD(TAG, "%s(): pos: %u, char: '%c' (0x%02x)", __func__, pos+1, *str, *str);
if ((*str >= ' ') &&
((*str - ' ') < ARRAY_ELEM_COUNT(ASCII_TO_SEG))) // supported char?
out_data = this->ascii_out_data_[*str - ' ']; // yes ->
else
out_data = SEG_UNSUPPORTED_CHAR;
ESP_LOGD(TAG, "%s(): segment data: 0x%06x", __func__, out_data);
if (out_data == SEG_UNSUPPORTED_CHAR) {
ESP_LOGW(TAG, "Encountered character '%c' with no display representation!", *str);
}
if ((out_data == (uint32_t)SEG_DP) && (pos > start_pos)) // is point/comma?
pos--; // yes -> modify display buffer of previous position
else
memset(&this->display_.out_buf_[pos*3], 0, 3); // no -> clear display buffer of current position (for later OR operation)
// shift data to the smallest segment OUT position...
out_data <<= (this->display_.seg_out_smallest);
ESP_LOGD(TAG, "%s(): segment data shifted to first segment bit (OUT%u): 0x%06x",
__func__, this->display_.seg_out_smallest, out_data);
// add position data...
out_data |= (1 << DISP_POS_TO_OUT[pos]);
ESP_LOGD(TAG, "%s(): OUT data with position: 0x%06x", __func__, out_data);
// write to appropriate position of display buffer...
this->display_.out_buf_[pos*3+0] |= (uint8_t)((out_data >> 16) & 0xFF);
this->display_.out_buf_[pos*3+1] |= (uint8_t)((out_data >> 8) & 0xFF);
this->display_.out_buf_[pos*3+2] |= (uint8_t)(out_data & 0xFF);
ESP_LOGD(TAG, "%s(): display buffer of position %u: 0x%02x%02x%02x",
__func__, pos+1, this->display_.out_buf_[pos*3+0],
this->display_.out_buf_[pos*3+1], this->display_.out_buf_[pos*3+2]);
pos++;
}
return pos - start_pos;
}
uint8_t MAX6921Component::print(const char *str) { return this->print(0, str); }
void MAX6921Component::set_writer(max6921_writer_t &&writer) { this->writer_ = writer; }
} // namespace max6921
} // namespace esphome

View file

@ -2,26 +2,57 @@
#include "esphome/core/component.h"
#include "esphome/core/time.h"
#include "esphome/components/spi/spi.h"
namespace esphome {
namespace max6921 {
class MAX6921 : public Component,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> {
class MAX6921Component;
using max6921_writer_t = std::function<void(MAX6921Component &)>;
typedef struct {
uint num_digits;
uint8_t *out_buf_;
size_t out_buf_size_;
uint seg_out_smallest;
char *current_text;
uint current_pos;
}display_t;
class MAX6921Component : public PollingComponent,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_4MHZ> {
public:
void setup() override;
void display();
void dump_config() override;
float get_setup_priority() const override;
void write_data(uint8_t *ptr, size_t length);
uint8_t print(uint8_t pos, const char *str);
uint8_t print(const char *str);
void set_blank_pin(GPIOPin *blank) { this->blank_pin_ = blank; }
void set_load_pin(GPIOPin *load) { this->load_pin_ = load; }
void set_num_digits(uint8_t num_digits) { this->display_.num_digits = num_digits; }
void set_writer(max6921_writer_t &&writer);
void setup() override;
void write_data(uint8_t *ptr, size_t length);
void update() override;
protected:
GPIOPin *load_pin_{};
GPIOPin *blank_pin_{};
display_t display_;
uint8_t *ascii_out_data_;
void enable_load() { this->load_pin_->digital_write(true); }
void disable_load() { this->load_pin_->digital_write(false); }
void enable_blank() { this->blank_pin_->digital_write(true); } // display off
void disable_blank() { this->blank_pin_->digital_write(false); } // display on
optional<max6921_writer_t> writer_{};
private:
void init_display_(void);
void init_font_(void);
};
} // namespace max6921