Add stable CYD firmware, USB calibration wizard and complete setup guide

This commit is contained in:
Max Gramser
2026-09-12 10:18:49 +02:00
parent 1eb65d8f71
commit 94546cf727
43 changed files with 9312 additions and 45 deletions
@@ -0,0 +1,43 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import spi, touchscreen
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_THRESHOLD
CODEOWNERS = ["@numo68", "@nielsnl68"]
DEPENDENCIES = ["spi"]
XPT2046_ns = cg.esphome_ns.namespace("xpt2046")
XPT2046Component = XPT2046_ns.class_(
"XPT2046Component",
touchscreen.Touchscreen,
spi.SPIDevice,
)
CONFIG_SCHEMA = cv.All(
touchscreen.touchscreen_schema(calibration_required=True)
.extend(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(XPT2046Component),
cv.Optional(CONF_INTERRUPT_PIN): cv.All(
pins.internal_gpio_input_pin_schema
),
cv.Optional(CONF_THRESHOLD, default=400): cv.int_range(min=0, max=4095),
},
)
)
.extend(spi.spi_device_schema()),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await spi.register_spi_device(var, config)
await touchscreen.register_touchscreen(var, config)
cg.add(var.set_threshold(config[CONF_THRESHOLD]))
if CONF_INTERRUPT_PIN in config:
pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN])
cg.add(var.set_irq_pin(pin))
@@ -0,0 +1,68 @@
#pragma once
#include <cstdlib>
#include <algorithm>
#include <cmath>
#include <cstdint>
namespace esphome::xpt2046 {
// Optional affine calibration corrects skew as well as scale/offset.
// Identity by default; coefficients belong to an individual physical panel.
class RawCorrection {
public:
void set(float xx, float xy, float xc, float yx, float yy, float yc) {
xx_ = xx; xy_ = xy; xc_ = xc; yx_ = yx; yy_ = yy; yc_ = yc;
}
int16_t x(int raw_x, int raw_y) const { return bound(xx_ * raw_x + xy_ * raw_y + xc_); }
int16_t y(int raw_x, int raw_y) const { return bound(yx_ * raw_x + yy_ * raw_y + yc_); }
private:
static int16_t bound(float v) { return static_cast<int16_t>(std::clamp(std::lround(v), 0L, 4095L)); }
float xx_{1}, xy_{0}, xc_{0}, yx_{0}, yy_{1}, yc_{0};
};
// XPT2046 ADC values can be wildly wrong as a finger first contacts/releases
// the resistive sheet. Do not expose a lone sample as a new LVGL target.
class TouchFilter {
public:
enum Result { HOLD, POSITION, RELEASE };
Result sample(bool pressed, int16_t raw_x, int16_t raw_y) {
if (!pressed) {
candidate_count_ = 0;
if (active_ && ++release_count_ < 2) return HOLD;
active_ = false;
release_count_ = 0;
return RELEASE;
}
release_count_ = 0;
if (active_ && near(raw_x, raw_y, x_, y_, 300)) {
candidate_count_ = 0;
x_ = (x_ + raw_x) / 2;
y_ = (y_ + raw_y) / 2;
return POSITION;
}
// Initial contact or a sudden jump: require three consistent samples.
if (!candidate_count_ || !near(raw_x, raw_y, candidate_x_, candidate_y_, 100)) {
candidate_x_ = raw_x;
candidate_y_ = raw_y;
candidate_count_ = 1;
return HOLD;
}
candidate_x_ = (candidate_x_ * candidate_count_ + raw_x) / (candidate_count_ + 1);
candidate_y_ = (candidate_y_ * candidate_count_ + raw_y) / (candidate_count_ + 1);
if (++candidate_count_ < 3) return HOLD;
x_ = candidate_x_;
y_ = candidate_y_;
active_ = true;
candidate_count_ = 0;
return POSITION;
}
int16_t x() const { return x_; }
int16_t y() const { return y_; }
private:
static bool near(int x1, int y1, int x2, int y2, int distance) {
return std::abs(x1 - x2) <= distance && std::abs(y1 - y2) <= distance;
}
int16_t x_{0}, y_{0}, candidate_x_{0}, candidate_y_{0};
uint8_t candidate_count_{0}, release_count_{0};
bool active_{false};
};
} // namespace esphome::xpt2046
+117
View File
@@ -0,0 +1,117 @@
#include "xpt2046.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <algorithm>
namespace esphome::xpt2046 {
static const char *const TAG = "xpt2046";
void XPT2046Component::setup() {
if (this->irq_pin_ != nullptr) {
// The pin reports a touch with a falling edge. Unfortunately the pin goes also changes state
// while the channels are read and wiring it as an interrupt is not straightforward and would
// need careful masking. A GPIO poll is cheap so we'll just use that.
this->irq_pin_->setup(); // INPUT
this->irq_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
this->irq_pin_->setup();
this->attach_interrupt_(this->irq_pin_, gpio::INTERRUPT_FALLING_EDGE);
}
this->spi_setup();
this->read_adc_(0xD0); // ADC powerdown, enable PENIRQ pin
}
void XPT2046Component::update_touches() {
int16_t data[6], x_raw, y_raw, z_raw;
bool touch = false;
enable();
int16_t touch_pressure_1 = this->read_adc_(0xB1 /* touch_pressure_1 */);
int16_t touch_pressure_2 = this->read_adc_(0xC1 /* touch_pressure_2 */);
z_raw = touch_pressure_1 + 0xfff - touch_pressure_2;
ESP_LOGVV(TAG, "Touchscreen Update z = %d", z_raw);
touch = (z_raw >= this->threshold_);
if (touch) {
read_adc_(0xD1 /* X */); // dummy Y measure, 1st is always noisy
data[0] = this->read_adc_(0x91 /* Y */);
data[1] = this->read_adc_(0xD1 /* X */); // make 3 x-y measurements
data[2] = this->read_adc_(0x91 /* Y */);
data[3] = this->read_adc_(0xD1 /* X */);
data[4] = this->read_adc_(0x91 /* Y */);
}
data[5] = this->read_adc_(0xD0 /* X */); // Last X touch power down
disable();
if (touch) {
x_raw = best_two_avg(data[1], data[3], data[5]);
y_raw = best_two_avg(data[0], data[2], data[4]);
ESP_LOGD(TAG, "Touchscreen Update [%d, %d], z = %d", x_raw, y_raw, z_raw);
if (this->filter_.sample(true, x_raw, y_raw) == TouchFilter::POSITION) {
this->add_raw_touch_position_(0,
this->correction_.x(this->filter_.x(), this->filter_.y()),
this->correction_.y(this->filter_.x(), this->filter_.y()), z_raw);
} else {
this->skip_update_ = true;
}
} else if (this->filter_.sample(false, 0, 0) == TouchFilter::HOLD) {
this->skip_update_ = true;
}
}
void XPT2046Component::dump_config() {
ESP_LOGCONFIG(TAG,
"XPT2046:\n"
" X min: %d\n"
" X max: %d\n"
" Y min: %d\n"
" Y max: %d\n"
" Swap X/Y: %s\n"
" Invert X: %s\n"
" Invert Y: %s\n"
" threshold: %d",
this->x_raw_min_, this->x_raw_max_, this->y_raw_min_, this->y_raw_max_, YESNO(this->swap_x_y_),
YESNO(this->invert_x_), YESNO(this->invert_y_), this->threshold_);
LOG_PIN(" IRQ Pin: ", this->irq_pin_);
LOG_UPDATE_INTERVAL(this);
}
// float XPT2046Component::get_setup_priority() const { return setup_priority::DATA; }
int16_t XPT2046Component::best_two_avg(int16_t value1, int16_t value2, int16_t value3) {
int16_t delta_a, delta_b, delta_c;
int16_t reta = 0;
delta_a = (value1 > value2) ? value1 - value2 : value2 - value1;
delta_b = (value1 > value3) ? value1 - value3 : value3 - value1;
delta_c = (value3 > value2) ? value3 - value2 : value2 - value3;
if (delta_a <= delta_b && delta_a <= delta_c) {
reta = (value1 + value2) >> 1;
} else if (delta_b <= delta_a && delta_b <= delta_c) {
reta = (value1 + value3) >> 1;
} else {
reta = (value2 + value3) >> 1;
}
return reta;
}
int16_t XPT2046Component::read_adc_(uint8_t ctrl) { // NOLINT
uint8_t data[2];
this->write_byte(ctrl);
delay(1);
data[0] = this->read_byte();
data[1] = this->read_byte();
return ((data[0] << 8) | data[1]) >> 3;
}
} // namespace esphome::xpt2046
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include "touch_filter.h"
#include "esphome/components/spi/spi.h"
#include "esphome/components/touchscreen/touchscreen.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::xpt2046 {
using namespace touchscreen;
class XPT2046Component : public Touchscreen,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_2MHZ> {
public:
void set_raw_correction(float xx, float xy, float xc, float yx, float yy, float yc) {
correction_.set(xx, xy, xc, yx, yy, yc);
}
int16_t filtered_raw_x() const { return filter_.x(); }
int16_t filtered_raw_y() const { return filter_.y(); }
/// Set the threshold for the touch detection.
void set_threshold(int16_t threshold) { this->threshold_ = threshold; }
/// Set the pin used to detect the touch.
void set_irq_pin(InternalGPIOPin *pin) { this->irq_pin_ = pin; }
void setup() override;
void dump_config() override;
// float get_setup_priority() const override;
protected:
TouchFilter filter_;
RawCorrection correction_;
static int16_t best_two_avg(int16_t value1, int16_t value2, int16_t value3);
int16_t read_adc_(uint8_t ctrl);
void update_touches() override;
int16_t threshold_;
InternalGPIOPin *irq_pin_{nullptr};
};
} // namespace esphome::xpt2046