You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
294 lines
9.1 KiB
294 lines
9.1 KiB
#include "display.hpp"
|
|
#include <atomic>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include "assert.h"
|
|
#include "display-init.hpp"
|
|
#include "driver/gpio.h"
|
|
#include "driver/spi_master.h"
|
|
#include "esp_attr.h"
|
|
#include "esp_heap_caps.h"
|
|
#include "freertos/portable.h"
|
|
#include "freertos/portmacro.h"
|
|
#include "freertos/projdefs.h"
|
|
#include "hal/gpio_types.h"
|
|
#include "hal/spi_types.h"
|
|
#include "lvgl/lvgl.h"
|
|
|
|
static const char* kTag = "DISPLAY";
|
|
static const gpio_num_t kCommandOrDataPin = GPIO_NUM_21;
|
|
static const gpio_num_t kLedPin = GPIO_NUM_22;
|
|
|
|
static const uint8_t kDisplayWidth = 128;
|
|
static const uint8_t kDisplayHeight = 160;
|
|
static const uint8_t kTransactionQueueSize = 10;
|
|
|
|
/*
|
|
* The size of each of our two display buffers. This is fundamentally a balance
|
|
* between performance and memory usage. LVGL docs recommend a buffer 1/10th the
|
|
* size of the screen is the best tradeoff.
|
|
* We use two buffers so that one can be flushed to the screen at the same time
|
|
* as the other is being drawn.
|
|
*/
|
|
static const int kDisplayBufferSize = (kDisplayWidth * kDisplayHeight) / 10;
|
|
|
|
// Allocate both buffers in static memory to ensure that they're in DRAM, with
|
|
// minimal fragmentation. We most cases we always need these buffers anyway, so
|
|
// it's not a memory hit we can avoid anyway.
|
|
// Note: 128 * 160 / 10 * 2 bpp * 2 buffers = 8 KiB
|
|
DMA_ATTR static lv_color_t sBuffer1[kDisplayBufferSize];
|
|
DMA_ATTR static lv_color_t sBuffer2[kDisplayBufferSize];
|
|
|
|
namespace gay_ipod {
|
|
|
|
// Static functions for interrop with the LVGL display driver API, which
|
|
// requires a function pointer.
|
|
namespace callback {
|
|
static std::atomic<Display*> instance = nullptr;
|
|
|
|
static void flush_cb(lv_disp_drv_t* disp_drv,
|
|
const lv_area_t* area,
|
|
lv_color_t* color_map) {
|
|
auto instance_unwrapped = instance.load();
|
|
if (instance_unwrapped == nullptr) {
|
|
ESP_LOGW(kTag, "uncaught flush callback");
|
|
return;
|
|
}
|
|
// TODO: what if a transaction comes in right now?
|
|
instance_unwrapped->Flush(disp_drv, area, color_map);
|
|
}
|
|
|
|
static void IRAM_ATTR post_cb(spi_transaction_t* transaction) {
|
|
auto instance_unwrapped = instance.load();
|
|
if (instance_unwrapped == nullptr) {
|
|
// Can't log in ISR.
|
|
return;
|
|
}
|
|
instance_unwrapped->PostTransaction(*transaction);
|
|
}
|
|
} // namespace callback
|
|
|
|
auto Display::create(GpioExpander* expander,
|
|
const displays::InitialisationData& init_data)
|
|
-> cpp::result<std::unique_ptr<Display>, Error> {
|
|
// First, set up our GPIOs
|
|
gpio_config_t gpio_cfg = {
|
|
.pin_bit_mask = GPIO_SEL_22 | GPIO_SEL_21,
|
|
.mode = GPIO_MODE_OUTPUT,
|
|
.pull_up_en = GPIO_PULLUP_DISABLE,
|
|
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
|
.intr_type = GPIO_INTR_DISABLE,
|
|
};
|
|
gpio_config(&gpio_cfg);
|
|
|
|
gpio_set_level(kLedPin, 1);
|
|
gpio_set_level(kCommandOrDataPin, 0);
|
|
|
|
// Next, init the SPI device
|
|
spi_device_interface_config_t spi_cfg = {
|
|
.command_bits = 0, // No command phase
|
|
.address_bits = 0, // No address phase
|
|
.dummy_bits = 0,
|
|
// For ST7789, mode should be 2
|
|
.mode = 0,
|
|
.duty_cycle_pos = 0, // Unused
|
|
.cs_ena_pretrans = 0,
|
|
.cs_ena_posttrans = 0,
|
|
.clock_speed_hz = SPI_MASTER_FREQ_8M,
|
|
.input_delay_ns = 0, // TODO: tune?
|
|
.spics_io_num = -1, // TODO: change for R2
|
|
.flags = 0,
|
|
.queue_size = kTransactionQueueSize,
|
|
.pre_cb = NULL,
|
|
.post_cb = &callback::post_cb,
|
|
};
|
|
spi_device_handle_t handle;
|
|
spi_bus_add_device(VSPI_HOST, &spi_cfg, &handle);
|
|
|
|
// TODO: ideally create this later? a bit awkward rn.
|
|
auto display = std::make_unique<Display>(expander, handle);
|
|
|
|
// Now we reset the display into a known state, then configure it
|
|
// TODO: set rotatoin
|
|
ESP_LOGI(kTag, "Sending init sequences");
|
|
for (int i = 0; i < init_data.num_sequences; i++) {
|
|
display->SendInitialisationSequence(init_data.sequences[i]);
|
|
}
|
|
|
|
// The hardware is now configured correctly. Next, initialise the LVGL display
|
|
// driver.
|
|
ESP_LOGI(kTag, "Init buffers");
|
|
lv_disp_draw_buf_init(&display->buffers_, sBuffer1, sBuffer2,
|
|
kDisplayBufferSize);
|
|
lv_disp_drv_init(&display->driver_);
|
|
display->driver_.draw_buf = &display->buffers_;
|
|
display->driver_.hor_res = kDisplayWidth;
|
|
display->driver_.ver_res = kDisplayHeight;
|
|
display->driver_.flush_cb = &callback::flush_cb;
|
|
|
|
ESP_LOGI(kTag, "Registering driver");
|
|
display->display_ = lv_disp_drv_register(&display->driver_);
|
|
|
|
return std::move(display);
|
|
}
|
|
|
|
Display::Display(GpioExpander* gpio, spi_device_handle_t handle)
|
|
: gpio_(gpio), handle_(handle) {
|
|
callback::instance = this;
|
|
}
|
|
|
|
Display::~Display() {
|
|
callback::instance = nullptr;
|
|
// TODO.
|
|
}
|
|
|
|
void Display::SendInitialisationSequence(const uint8_t* data) {
|
|
uint8_t command, num_args;
|
|
uint16_t sleep_duration_ms;
|
|
|
|
// First byte of the data is the number of commands.
|
|
for (int i = *(data++); i > 0; i--) {
|
|
command = *(data++);
|
|
num_args = *(data++);
|
|
bool has_delay = (num_args & displays::kDelayBit) > 0;
|
|
num_args &= ~displays::kDelayBit;
|
|
|
|
SendCommandWithData(command, data, num_args);
|
|
|
|
data += num_args;
|
|
if (has_delay) {
|
|
sleep_duration_ms = *(data++);
|
|
if (sleep_duration_ms == 0xFF) {
|
|
sleep_duration_ms = 500;
|
|
}
|
|
vTaskDelay(pdMS_TO_TICKS(sleep_duration_ms));
|
|
}
|
|
}
|
|
}
|
|
|
|
void Display::SendCommandWithData(uint8_t command,
|
|
const uint8_t* data,
|
|
size_t length,
|
|
uintptr_t flags) {
|
|
SendCmd(&command, 1, flags);
|
|
SendData(data, length, flags);
|
|
}
|
|
|
|
void Display::SendCmd(const uint8_t* data, size_t length, uintptr_t flags) {
|
|
SendTransaction(data, length, flags | SEND_COMMAND);
|
|
}
|
|
|
|
void Display::SendData(const uint8_t* data, size_t length, uintptr_t flags) {
|
|
SendTransaction(data, length, flags | SEND_DATA);
|
|
}
|
|
|
|
void Display::SendTransaction(const uint8_t* data,
|
|
size_t length,
|
|
uintptr_t flags) {
|
|
if (length == 0) {
|
|
return;
|
|
}
|
|
|
|
// TODO: Use a memory pool for these.
|
|
spi_transaction_t* transaction = (spi_transaction_t*)heap_caps_calloc(
|
|
1, sizeof(spi_transaction_t), MALLOC_CAP_DMA);
|
|
|
|
transaction->rx_buffer = NULL;
|
|
// Length is in bits, so multiply by 8.
|
|
transaction->length = length * 8;
|
|
transaction->rxlength = 0; // Match `length` value.
|
|
|
|
transaction->user = nullptr; // TODO.
|
|
|
|
// If the data to transmit is very short, then we can fit it directly
|
|
// inside the transaction struct.
|
|
if (length * 8 <= 32) {
|
|
transaction->flags = SPI_TRANS_USE_TXDATA;
|
|
std::memcpy(&transaction->tx_data, data, length);
|
|
} else {
|
|
assert((flags & SMALL) == 0);
|
|
// TODO: copy data to a DMA-capable transaction buffer
|
|
transaction->tx_buffer = const_cast<uint8_t*>(data);
|
|
}
|
|
|
|
transaction->user = reinterpret_cast<void*>(flags);
|
|
|
|
// TODO: acquire the bus first? Or in an outer scope?
|
|
// TODO: fail gracefully
|
|
// ESP_ERROR_CHECK(spi_device_queue_trans(handle_, transaction,
|
|
// portMAX_DELAY));
|
|
//
|
|
|
|
ServiceTransactions();
|
|
if (flags & SEND_COMMAND) {
|
|
gpio_set_level(kCommandOrDataPin, 0);
|
|
} else if (flags & SEND_DATA) {
|
|
gpio_set_level(kCommandOrDataPin, 1);
|
|
}
|
|
|
|
gpio_->with([&](auto& gpio_) {
|
|
gpio_.set_pin(GpioExpander::DISPLAY_CHIP_SELECT, 0);
|
|
});
|
|
{
|
|
// auto lock = gpio_->AcquireSpiBus(GpioExpander::DISPLAY);
|
|
ESP_ERROR_CHECK(spi_device_polling_transmit(handle_, transaction));
|
|
}
|
|
|
|
free(transaction);
|
|
}
|
|
|
|
void Display::Flush(lv_disp_drv_t* disp_drv,
|
|
const lv_area_t* area,
|
|
lv_color_t* color_map) {
|
|
// TODO: constants? based on init sequence?
|
|
uint16_t col_start = 2;
|
|
uint16_t row_start = 1;
|
|
uint16_t data[2] = {0, 0};
|
|
|
|
data[0] = SPI_SWAP_DATA_TX(area->x1 + row_start, 16);
|
|
data[1] = SPI_SWAP_DATA_TX(area->x2 + row_start, 16);
|
|
SendCommandWithData(displays::ST77XX_CASET, (uint8_t*)data, 4, SMALL);
|
|
|
|
data[0] = SPI_SWAP_DATA_TX(area->y1 + col_start, 16);
|
|
data[1] = SPI_SWAP_DATA_TX(area->y2 + col_start, 16);
|
|
SendCommandWithData(displays::ST77XX_RASET, (uint8_t*)data, 4, SMALL);
|
|
|
|
uint32_t size = lv_area_get_width(area) * lv_area_get_height(area);
|
|
SendCommandWithData(displays::ST77XX_RAMWR, (uint8_t*)color_map, size * 2,
|
|
FLUSH_BUFFER);
|
|
|
|
// ESP_LOGI(kTag, "finished flush.");
|
|
// lv_disp_flush_ready(&driver_);
|
|
}
|
|
|
|
void IRAM_ATTR Display::PostTransaction(const spi_transaction_t& transaction) {
|
|
if (reinterpret_cast<uintptr_t>(transaction.user) & FLUSH_BUFFER) {
|
|
lv_disp_flush_ready(&driver_);
|
|
}
|
|
}
|
|
|
|
void Display::ServiceTransactions() {
|
|
// todo
|
|
if (1)
|
|
return;
|
|
spi_transaction_t* transaction = nullptr;
|
|
// TODO: just wait '1' here, provide mechanism to wait for sure (poll?)
|
|
while (spi_device_get_trans_result(handle_, &transaction, pdMS_TO_TICKS(1)) !=
|
|
ESP_ERR_TIMEOUT) {
|
|
ESP_LOGI(kTag, "cleaning up finished transaction");
|
|
|
|
// TODO: a bit dodge lmao
|
|
// TODO: also this should happen in the post callback instead i guess?
|
|
if (transaction->length > 1000) {
|
|
ESP_LOGI(kTag, "finished flush.");
|
|
lv_disp_flush_ready(&driver_);
|
|
}
|
|
|
|
// TODO: place back into pool.
|
|
free(transaction);
|
|
}
|
|
}
|
|
|
|
} // namespace gay_ipod
|
|
|