#include "display.hpp" #include #include #include #include #include #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 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, 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_40M, .input_delay_ns = 0, .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(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(COMMAND, data, length, flags); } void Display::SendData(const uint8_t* data, size_t length, uintptr_t flags) { SendTransaction(DATA, data, length, flags); } void Display::SendTransaction(TransactionType type, const uint8_t* data, size_t length, uint32_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. // 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 { // TODO: copy data to a DMA-capable transaction buffer transaction->tx_buffer = const_cast(data); } transaction->user = reinterpret_cast(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(); gpio_set_level(kCommandOrDataPin, type); 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) { uint16_t data[2] = {0, 0}; data[0] = SPI_SWAP_DATA_TX(area->x1, 16); data[1] = SPI_SWAP_DATA_TX(area->x2, 16); SendCommandWithData(displays::ST77XX_CASET, (uint8_t*)data, 4); data[0] = SPI_SWAP_DATA_TX(area->y1, 16); data[1] = SPI_SWAP_DATA_TX(area->y2, 16); SendCommandWithData(displays::ST77XX_RASET, (uint8_t*)data, 4); uint32_t size = lv_area_get_width(area) * lv_area_get_height(area); SendCommandWithData(displays::ST77XX_RAMWR, (uint8_t*)color_map, size * 2, LVGL_FLUSH); // ESP_LOGI(kTag, "finished flush."); // lv_disp_flush_ready(&driver_); } void IRAM_ATTR Display::PostTransaction(const spi_transaction_t& transaction) { if (reinterpret_cast(transaction.user) & LVGL_FLUSH) { 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