Add epd7in3e display driver component
Ports Waveshare's official EPD_7in3e.c register/refresh sequence (the panel has no public datasheet, so their reference driver is the source of truth) to an ESP-IDF component using spi_master + gpio instead of the bcm2835/RPi hardware abstraction the reference targets. Unlike the reference driver, which toggles CS around every single byte, this holds CS low for each logical command/data phase and DMAs pixel data in 4KB chunks -- sending ~192,000 individual one-byte SPI transactions would make a full refresh impractically slow. Exposes a streaming API (epd_display_stream, pulling chunks from a caller-supplied read_fn) so the eventual HTTP fetch path can feed the panel without holding a full ~192KB frame in RAM, plus an in-memory convenience wrapper (epd_display_buffer) for cases like the upcoming QR code screen where buffering the whole frame is fine. Wired into wifi_provisioning_start() as an init + white-clear for now, to prove the driver builds/links/runs at the right point in the boot sequence ahead of the actual QR code content.
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
idf_component_register(SRCS "epd7in3e.c"
|
||||
INCLUDE_DIRS "include"
|
||||
PRIV_REQUIRES esp_driver_spi esp_driver_gpio)
|
||||
@@ -0,0 +1,39 @@
|
||||
menu "E-Paper Display (epd7in3e) Configuration"
|
||||
|
||||
config EPD_PIN_CLK
|
||||
int "SPI CLK (SCLK) GPIO"
|
||||
default 18
|
||||
help
|
||||
Not physically wired yet -- these are sensible ESP32-C6 devkit
|
||||
defaults (avoiding strapping pins 4/5/8/9/15 and the USB-JTAG
|
||||
pins 12/13). Override to match your actual wiring.
|
||||
|
||||
config EPD_PIN_MOSI
|
||||
int "SPI MOSI (DIN) GPIO"
|
||||
default 19
|
||||
|
||||
config EPD_PIN_CS
|
||||
int "SPI CS GPIO"
|
||||
default 20
|
||||
|
||||
config EPD_PIN_DC
|
||||
int "Data/Command GPIO"
|
||||
default 21
|
||||
|
||||
config EPD_PIN_RST
|
||||
int "Reset GPIO"
|
||||
default 22
|
||||
|
||||
config EPD_PIN_BUSY
|
||||
int "Busy GPIO"
|
||||
default 23
|
||||
|
||||
config EPD_SPI_CLOCK_HZ
|
||||
int "SPI clock speed (Hz)"
|
||||
default 4000000
|
||||
help
|
||||
The panel's SPI interface is rated well above this, but dupont
|
||||
wires/breadboards are often unreliable much past a few MHz.
|
||||
Raise this once the physical wiring is confirmed solid.
|
||||
|
||||
endmenu
|
||||
@@ -0,0 +1,293 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "driver/gpio.h"
|
||||
#include "driver/spi_master.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_check.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "epd7in3e.h"
|
||||
|
||||
/* Command bytes and register values below are a line-for-line transcription
|
||||
* of Waveshare's official EPD_7in3e.c reference driver (RaspberryPi_JetsonNano/c
|
||||
* variant) -- this panel's controller has no public datasheet, so the
|
||||
* upstream driver is the source of truth. Unlike that driver (which toggles
|
||||
* CS around every single byte), this port holds CS low for the duration of
|
||||
* each logical command or data phase and DMAs data in chunks, since 192,000
|
||||
* one-byte SPI transactions would make a full refresh impractically slow. */
|
||||
|
||||
#define EPD_SPI_HOST SPI2_HOST
|
||||
#define EPD_SPI_CHUNK_SIZE 4096
|
||||
|
||||
static const char *TAG = "epd7in3e";
|
||||
|
||||
#define EPD_CHECK(expr) ESP_RETURN_ON_ERROR((expr), TAG, #expr)
|
||||
|
||||
static spi_device_handle_t s_spi;
|
||||
|
||||
static void epd_delay_ms(uint32_t ms)
|
||||
{
|
||||
vTaskDelay(pdMS_TO_TICKS(ms));
|
||||
}
|
||||
|
||||
/* BUSY: LOW = busy, HIGH = idle. */
|
||||
static void epd_wait_busy(void)
|
||||
{
|
||||
while (gpio_get_level((gpio_num_t)CONFIG_EPD_PIN_BUSY) == 0) {
|
||||
epd_delay_ms(1);
|
||||
}
|
||||
}
|
||||
|
||||
static esp_err_t epd_spi_write(const uint8_t *data, size_t len)
|
||||
{
|
||||
while (len > 0) {
|
||||
size_t n = len > EPD_SPI_CHUNK_SIZE ? EPD_SPI_CHUNK_SIZE : len;
|
||||
spi_transaction_t t = {
|
||||
.length = n * 8,
|
||||
.tx_buffer = data,
|
||||
};
|
||||
EPD_CHECK(spi_device_polling_transmit(s_spi, &t));
|
||||
data += n;
|
||||
len -= n;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t epd_send_command(uint8_t cmd)
|
||||
{
|
||||
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_DC, 0);
|
||||
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_CS, 0);
|
||||
esp_err_t err = epd_spi_write(&cmd, 1);
|
||||
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_CS, 1);
|
||||
return err;
|
||||
}
|
||||
|
||||
static esp_err_t epd_send_data(const uint8_t *data, size_t len)
|
||||
{
|
||||
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_DC, 1);
|
||||
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_CS, 0);
|
||||
esp_err_t err = epd_spi_write(data, len);
|
||||
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_CS, 1);
|
||||
return err;
|
||||
}
|
||||
|
||||
static esp_err_t epd_send_data_byte(uint8_t data)
|
||||
{
|
||||
return epd_send_data(&data, 1);
|
||||
}
|
||||
|
||||
static void epd_reset(void)
|
||||
{
|
||||
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_RST, 1);
|
||||
epd_delay_ms(20);
|
||||
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_RST, 0);
|
||||
epd_delay_ms(2);
|
||||
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_RST, 1);
|
||||
epd_delay_ms(20);
|
||||
}
|
||||
|
||||
/* Power on, "second setting" registers, refresh, power off -- mirrors
|
||||
* EPD_7IN3E_TurnOnDisplay() in the reference driver. */
|
||||
static esp_err_t epd_turn_on_display(void)
|
||||
{
|
||||
EPD_CHECK(epd_send_command(0x04)); // POWER_ON
|
||||
epd_wait_busy();
|
||||
|
||||
EPD_CHECK(epd_send_command(0x06));
|
||||
EPD_CHECK(epd_send_data((uint8_t[]){ 0x6F, 0x1F, 0x17, 0x49 }, 4));
|
||||
|
||||
EPD_CHECK(epd_send_command(0x12)); // DISPLAY_REFRESH
|
||||
EPD_CHECK(epd_send_data_byte(0x00));
|
||||
epd_wait_busy();
|
||||
|
||||
EPD_CHECK(epd_send_command(0x02)); // POWER_OFF
|
||||
EPD_CHECK(epd_send_data_byte(0x00));
|
||||
epd_wait_busy();
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t epd_init(void)
|
||||
{
|
||||
gpio_config_t out_cfg = {
|
||||
.pin_bit_mask = (1ULL << CONFIG_EPD_PIN_DC) | (1ULL << CONFIG_EPD_PIN_RST) | (1ULL << CONFIG_EPD_PIN_CS),
|
||||
.mode = GPIO_MODE_OUTPUT,
|
||||
};
|
||||
EPD_CHECK(gpio_config(&out_cfg));
|
||||
|
||||
gpio_config_t busy_cfg = {
|
||||
.pin_bit_mask = (1ULL << CONFIG_EPD_PIN_BUSY),
|
||||
.mode = GPIO_MODE_INPUT,
|
||||
};
|
||||
EPD_CHECK(gpio_config(&busy_cfg));
|
||||
|
||||
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_CS, 1);
|
||||
|
||||
spi_bus_config_t bus_cfg = {
|
||||
.mosi_io_num = CONFIG_EPD_PIN_MOSI,
|
||||
.miso_io_num = -1,
|
||||
.sclk_io_num = CONFIG_EPD_PIN_CLK,
|
||||
.quadwp_io_num = -1,
|
||||
.quadhd_io_num = -1,
|
||||
.max_transfer_sz = EPD_SPI_CHUNK_SIZE,
|
||||
};
|
||||
EPD_CHECK(spi_bus_initialize(EPD_SPI_HOST, &bus_cfg, SPI_DMA_CH_AUTO));
|
||||
|
||||
spi_device_interface_config_t dev_cfg = {
|
||||
.clock_speed_hz = CONFIG_EPD_SPI_CLOCK_HZ,
|
||||
.mode = 0,
|
||||
/* CS is bit-banged around each command/data phase above rather than
|
||||
* hardware-driven, since a phase can span many chunked SPI
|
||||
* transactions and must stay asserted across all of them. */
|
||||
.spics_io_num = -1,
|
||||
.queue_size = 1,
|
||||
};
|
||||
EPD_CHECK(spi_bus_add_device(EPD_SPI_HOST, &dev_cfg, &s_spi));
|
||||
|
||||
epd_reset();
|
||||
epd_wait_busy();
|
||||
epd_delay_ms(30);
|
||||
|
||||
EPD_CHECK(epd_send_command(0xAA)); // CMDH
|
||||
EPD_CHECK(epd_send_data((uint8_t[]){ 0x49, 0x55, 0x20, 0x08, 0x09, 0x18 }, 6));
|
||||
|
||||
EPD_CHECK(epd_send_command(0x01));
|
||||
EPD_CHECK(epd_send_data_byte(0x3F));
|
||||
|
||||
EPD_CHECK(epd_send_command(0x00));
|
||||
EPD_CHECK(epd_send_data((uint8_t[]){ 0x5F, 0x69 }, 2));
|
||||
|
||||
EPD_CHECK(epd_send_command(0x03));
|
||||
EPD_CHECK(epd_send_data((uint8_t[]){ 0x00, 0x54, 0x00, 0x44 }, 4));
|
||||
|
||||
EPD_CHECK(epd_send_command(0x05));
|
||||
EPD_CHECK(epd_send_data((uint8_t[]){ 0x40, 0x1F, 0x1F, 0x2C }, 4));
|
||||
|
||||
EPD_CHECK(epd_send_command(0x06));
|
||||
EPD_CHECK(epd_send_data((uint8_t[]){ 0x6F, 0x1F, 0x17, 0x49 }, 4));
|
||||
|
||||
EPD_CHECK(epd_send_command(0x08));
|
||||
EPD_CHECK(epd_send_data((uint8_t[]){ 0x6F, 0x1F, 0x1F, 0x22 }, 4));
|
||||
|
||||
EPD_CHECK(epd_send_command(0x30));
|
||||
EPD_CHECK(epd_send_data_byte(0x03));
|
||||
|
||||
EPD_CHECK(epd_send_command(0x50));
|
||||
EPD_CHECK(epd_send_data_byte(0x3F));
|
||||
|
||||
EPD_CHECK(epd_send_command(0x60));
|
||||
EPD_CHECK(epd_send_data((uint8_t[]){ 0x02, 0x00 }, 2));
|
||||
|
||||
EPD_CHECK(epd_send_command(0x61));
|
||||
EPD_CHECK(epd_send_data((uint8_t[]){ 0x03, 0x20, 0x01, 0xE0 }, 4));
|
||||
|
||||
EPD_CHECK(epd_send_command(0x84));
|
||||
EPD_CHECK(epd_send_data_byte(0x01));
|
||||
|
||||
EPD_CHECK(epd_send_command(0xE3));
|
||||
EPD_CHECK(epd_send_data_byte(0x2F));
|
||||
|
||||
EPD_CHECK(epd_send_command(0x04)); // POWER_ON; waits for the panel to release the idle signal
|
||||
epd_wait_busy();
|
||||
|
||||
ESP_LOGI(TAG, "EPD initialized (CLK=%d MOSI=%d CS=%d DC=%d RST=%d BUSY=%d)",
|
||||
CONFIG_EPD_PIN_CLK, CONFIG_EPD_PIN_MOSI, CONFIG_EPD_PIN_CS,
|
||||
CONFIG_EPD_PIN_DC, CONFIG_EPD_PIN_RST, CONFIG_EPD_PIN_BUSY);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t epd_display_stream(epd_read_fn_t read_fn, void *ctx)
|
||||
{
|
||||
ESP_RETURN_ON_FALSE(read_fn != NULL, ESP_ERR_INVALID_ARG, TAG, "read_fn required");
|
||||
|
||||
EPD_CHECK(epd_send_command(0x10));
|
||||
|
||||
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_DC, 1);
|
||||
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_CS, 0);
|
||||
|
||||
uint8_t chunk[EPD_SPI_CHUNK_SIZE];
|
||||
size_t total = 0;
|
||||
size_t n;
|
||||
esp_err_t err = ESP_OK;
|
||||
while ((n = read_fn(chunk, sizeof(chunk), ctx)) > 0) {
|
||||
err = epd_spi_write(chunk, n);
|
||||
if (err != ESP_OK) {
|
||||
break;
|
||||
}
|
||||
total += n;
|
||||
}
|
||||
|
||||
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_CS, 1);
|
||||
EPD_CHECK(err);
|
||||
|
||||
if (total != EPD_FRAME_BYTES) {
|
||||
ESP_LOGW(TAG, "Stream supplied %u bytes, expected %u", (unsigned)total, (unsigned)EPD_FRAME_BYTES);
|
||||
}
|
||||
|
||||
return epd_turn_on_display();
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const uint8_t *data;
|
||||
size_t len;
|
||||
size_t pos;
|
||||
} epd_buf_ctx_t;
|
||||
|
||||
static size_t epd_buf_read(uint8_t *chunk, size_t chunk_size, void *ctx_)
|
||||
{
|
||||
epd_buf_ctx_t *c = (epd_buf_ctx_t *)ctx_;
|
||||
size_t remaining = c->len - c->pos;
|
||||
size_t n = remaining < chunk_size ? remaining : chunk_size;
|
||||
if (n == 0) {
|
||||
return 0;
|
||||
}
|
||||
memcpy(chunk, c->data + c->pos, n);
|
||||
c->pos += n;
|
||||
return n;
|
||||
}
|
||||
|
||||
esp_err_t epd_display_buffer(const uint8_t *frame, size_t len)
|
||||
{
|
||||
epd_buf_ctx_t buf_ctx = { .data = frame, .len = len, .pos = 0 };
|
||||
return epd_display_stream(epd_buf_read, &buf_ctx);
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
uint8_t fill_byte;
|
||||
size_t remaining;
|
||||
} epd_fill_ctx_t;
|
||||
|
||||
static size_t epd_fill_read(uint8_t *chunk, size_t chunk_size, void *ctx_)
|
||||
{
|
||||
epd_fill_ctx_t *c = (epd_fill_ctx_t *)ctx_;
|
||||
size_t n = c->remaining < chunk_size ? c->remaining : chunk_size;
|
||||
if (n == 0) {
|
||||
return 0;
|
||||
}
|
||||
memset(chunk, c->fill_byte, n);
|
||||
c->remaining -= n;
|
||||
return n;
|
||||
}
|
||||
|
||||
esp_err_t epd_clear(epd_color_t color)
|
||||
{
|
||||
epd_fill_ctx_t fill_ctx = {
|
||||
.fill_byte = (uint8_t)((color << 4) | color),
|
||||
.remaining = EPD_FRAME_BYTES,
|
||||
};
|
||||
return epd_display_stream(epd_fill_read, &fill_ctx);
|
||||
}
|
||||
|
||||
esp_err_t epd_sleep(void)
|
||||
{
|
||||
EPD_CHECK(epd_send_command(0x02)); // POWER_OFF
|
||||
EPD_CHECK(epd_send_data_byte(0x00));
|
||||
epd_wait_busy();
|
||||
|
||||
EPD_CHECK(epd_send_command(0x07)); // DEEP_SLEEP
|
||||
EPD_CHECK(epd_send_data_byte(0xA5));
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
/* Waveshare 7.3" E Ink Spectra 6 (E6) panel: 800x480, 4 bits/pixel packed
|
||||
* 2-pixels-per-byte, per the official EPD_7in3e.c reference driver. */
|
||||
#define EPD_WIDTH 800
|
||||
#define EPD_HEIGHT 480
|
||||
#define EPD_BYTES_PER_ROW ((EPD_WIDTH + 1) / 2)
|
||||
#define EPD_FRAME_BYTES (EPD_BYTES_PER_ROW * EPD_HEIGHT)
|
||||
|
||||
typedef enum {
|
||||
EPD_COLOR_BLACK = 0x0,
|
||||
EPD_COLOR_WHITE = 0x1,
|
||||
EPD_COLOR_YELLOW = 0x2,
|
||||
EPD_COLOR_RED = 0x3,
|
||||
EPD_COLOR_BLUE = 0x5,
|
||||
EPD_COLOR_GREEN = 0x6,
|
||||
} epd_color_t;
|
||||
|
||||
/** Configures SPI + GPIO and runs the panel's power-on register init sequence. */
|
||||
esp_err_t epd_init(void);
|
||||
|
||||
/** Fills the whole panel with a single color and refreshes. */
|
||||
esp_err_t epd_clear(epd_color_t color);
|
||||
|
||||
/**
|
||||
* Called repeatedly by epd_display_stream() to fill up to chunk_size bytes
|
||||
* into chunk. Must return the number of bytes written, or 0 once exhausted.
|
||||
*/
|
||||
typedef size_t (*epd_read_fn_t)(uint8_t *chunk, size_t chunk_size, void *ctx);
|
||||
|
||||
/**
|
||||
* Streams a full frame (EPD_FRAME_BYTES bytes, packed 2 pixels/byte) to the
|
||||
* panel via read_fn and refreshes. Pulling from a caller-supplied source
|
||||
* instead of a single buffer lets callers feed the panel directly from an
|
||||
* HTTP response without holding the whole ~192KB frame in RAM.
|
||||
*/
|
||||
esp_err_t epd_display_stream(epd_read_fn_t read_fn, void *ctx);
|
||||
|
||||
/** Convenience wrapper around epd_display_stream() for an in-memory frame buffer. */
|
||||
esp_err_t epd_display_buffer(const uint8_t *frame, size_t len);
|
||||
|
||||
/** Puts the panel into deep sleep to minimize power draw between refreshes. */
|
||||
esp_err_t epd_sleep(void);
|
||||
@@ -1,3 +1,3 @@
|
||||
idf_component_register(SRCS main.c wifi_provisioning.c frame_client.c
|
||||
PRIV_REQUIRES esp_event nvs_flash esp_wifi esp_netif esp_http_server dns_server
|
||||
PRIV_REQUIRES esp_event nvs_flash esp_wifi esp_netif esp_http_server dns_server epd7in3e
|
||||
EMBED_FILES root.html)
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "epd7in3e.h"
|
||||
#include "wifi_provisioning.h"
|
||||
|
||||
#define NVS_NAMESPACE "frame_cfg"
|
||||
@@ -370,11 +371,19 @@ void wifi_provisioning_start(void)
|
||||
char ap_password[FRAME_AP_PASSWORD_LEN + 1];
|
||||
ap_identity_get(ap_ssid, sizeof(ap_ssid), ap_password, sizeof(ap_password));
|
||||
|
||||
/* TODO(step 5): draw the WiFi-join QR code + plaintext password on the
|
||||
* e-ink panel here, before the AP goes up, so the join instructions are
|
||||
* always visible by the time the network is joinable. */
|
||||
ESP_LOGI(TAG, "Provisioning AP: SSID='%s' password='%s'", ap_ssid, ap_password);
|
||||
|
||||
/* Bring up the display before the AP so join instructions are on-screen
|
||||
* by the time the network is joinable.
|
||||
* TODO(step 5): replace this clear with the WiFi-join QR code +
|
||||
* plaintext password. */
|
||||
esp_err_t epd_err = epd_init();
|
||||
if (epd_err == ESP_OK) {
|
||||
epd_clear(EPD_COLOR_WHITE);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "EPD init failed (%s), continuing without display", esp_err_to_name(epd_err));
|
||||
}
|
||||
|
||||
esp_netif_create_default_wifi_ap();
|
||||
wifi_init_softap(ap_ssid, ap_password);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user