Files
cardboy/Firmware/components/backend-esp/src/display.cpp
2025-10-11 16:44:48 +02:00

156 lines
5.7 KiB
C++

// Double-buffered display implementation with async memcpy ---------------------------------
#include "cardboy/backend/esp/display.hpp"
#include <cassert>
#include <cstring>
#include <driver/gpio.h>
#include "driver/spi_master.h"
#include "esp_async_memcpy.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
DMA_ATTR static uint8_t s_dma_buffer0[SMD::kLineDataBytes]{};
DMA_ATTR static uint8_t s_dma_buffer1[SMD::kLineDataBytes]{};
static uint8_t* s_dma_buffers[2] = {s_dma_buffer0, s_dma_buffer1};
DMA_ATTR static uint8_t dma_buf_template[SMD::kLineDataBytes]{};
uint8_t* SMD::dma_buf = s_dma_buffers[0];
spi_device_handle_t SMD::_spi;
static spi_transaction_t _tx{};
static SemaphoreHandle_t _txSem = nullptr;
static bool _vcom = false;
static TaskHandle_t s_clearTaskHandle = nullptr;
static SemaphoreHandle_t s_clearReqSem = nullptr;
static SemaphoreHandle_t s_bufferSem[2] = {nullptr, nullptr};
static bool s_clearPending[2] = {true, true};
static async_memcpy_config_t config = ASYNC_MEMCPY_DEFAULT_CONFIG();
// update the maximum data stream supported by underlying DMA engine
static async_memcpy_handle_t driver = nullptr;
static volatile int s_drawBufIdx = 0;
static unsigned char reverse_bits3(unsigned char b) { return (b * 0x0202020202ULL & 0x010884422010ULL) % 0x3ff; }
static bool IRAM_ATTR my_async_memcpy_cb(async_memcpy_handle_t /*mcp_hdl*/, async_memcpy_event_t* /*event*/,
void* cb_args) {
BaseType_t high_task_wakeup = pdFALSE;
auto sem = static_cast<SemaphoreHandle_t>(cb_args);
xSemaphoreGiveFromISR(sem, &high_task_wakeup);
return high_task_wakeup == pdTRUE;
}
extern "C" void IRAM_ATTR s_spi_post_cb(spi_transaction_t* /*t*/) {
BaseType_t hpw = pdFALSE;
xSemaphoreGiveFromISR(s_clearReqSem, &hpw);
if (hpw)
portYIELD_FROM_ISR();
}
static void clear_task(void*) {
for (;;) {
if (xSemaphoreTake(s_clearReqSem, portMAX_DELAY) == pdTRUE) {
spi_transaction_t* r = nullptr;
ESP_ERROR_CHECK(spi_device_get_trans_result(SMD::_spi, &r, 0));
int bufIdx = (int) r->user;
xSemaphoreGive(_txSem);
const bool shouldClear = s_clearPending[bufIdx];
s_clearPending[bufIdx] = true;
if (shouldClear) {
constexpr unsigned alignedSize = SMD::kLineDataBytes - (SMD::kLineDataBytes % 4);
static_assert(SMD::kLineDataBytes - alignedSize < 8); // Last byte is zero anyway
ESP_ERROR_CHECK(esp_async_memcpy(driver, s_dma_buffers[bufIdx], dma_buf_template, alignedSize,
my_async_memcpy_cb, static_cast<void*>(s_bufferSem[bufIdx])));
} else {
if (!xSemaphoreGive(s_bufferSem[bufIdx]))
assert(false);
}
}
}
}
void SMD::init() {
spi_bus_add_device(SPI_BUS, &_devcfg, &_spi);
ESP_ERROR_CHECK(gpio_reset_pin(SPI_DISP_DISP));
ESP_ERROR_CHECK(gpio_set_direction(SPI_DISP_DISP, GPIO_MODE_OUTPUT));
ESP_ERROR_CHECK(gpio_set_level(SPI_DISP_DISP, 1));
ESP_ERROR_CHECK(gpio_hold_en(SPI_DISP_DISP));
for (int buf = 0; buf < 2; ++buf) {
auto* fb = s_dma_buffers[buf];
for (uint8_t i = 0; i < DISP_HEIGHT; ++i) {
fb[kLineMultiSingle * i + 1] = reverse_bits3(i + 1);
fb[2 + kLineMultiSingle * i + kLineBytes] = 0;
}
fb[kLineDataBytes - 1] = 0;
}
s_drawBufIdx = 0;
dma_buf = s_dma_buffers[s_drawBufIdx];
for (int y = 0; y < DISP_HEIGHT; ++y)
for (int x = 0; x < DISP_WIDTH; ++x)
set_pixel(x, y, false);
std::memcpy(dma_buf_template, dma_buf, sizeof(dma_buf_template));
std::memcpy(s_dma_buffers[1], dma_buf_template, sizeof(dma_buf_template));
ESP_ERROR_CHECK(esp_async_memcpy_install(&config, &driver)); // install driver with default DMA engine
s_clearReqSem = xSemaphoreCreateBinary();
for (int i = 0; i < 2; ++i) {
s_bufferSem[i] = xSemaphoreCreateBinary();
xSemaphoreGive(s_bufferSem[i]);
}
_txSem = xSemaphoreCreateBinary();
xSemaphoreGive(_txSem);
xTaskCreate(clear_task, "fbclr", 1536, nullptr, tskIDLE_PRIORITY + 1, &s_clearTaskHandle);
}
bool SMD::frame_transfer_in_flight() { return uxSemaphoreGetCount(s_bufferSem[s_drawBufIdx]) == 0; }
void SMD::send_frame(bool clear_after_send) {
assert(driver != nullptr);
if (!xSemaphoreTake(_txSem, portMAX_DELAY))
assert(false);
const int sendIdx = s_drawBufIdx;
assert(sendIdx >= 0 && sendIdx < 2);
SemaphoreHandle_t sem = s_bufferSem[sendIdx];
if (!xSemaphoreTake(sem, 0))
assert(false);
const int nextDrawIdx = sendIdx ^ 1;
s_clearPending[sendIdx] = clear_after_send;
_vcom = !_vcom;
_tx = {};
_tx.tx_buffer = s_dma_buffers[sendIdx];
_tx.length = SMD::kLineDataBytes * 8;
_tx.user = (void*) (sendIdx);
s_dma_buffers[sendIdx][0] = 0b10000000 | (_vcom << 6);
ESP_ERROR_CHECK(spi_device_queue_trans(_spi, &_tx, 0));
s_drawBufIdx = nextDrawIdx;
dma_buf = s_dma_buffers[nextDrawIdx];
}
void SMD::frame_ready() {
SemaphoreHandle_t sem = s_bufferSem[s_drawBufIdx];
// uint64_t waitedUs = 0;
if (!uxSemaphoreGetCount(sem)) {
// uint64_t start = esp_timer_get_time();
if (!xSemaphoreTake(sem, portMAX_DELAY))
assert(false);
if (!xSemaphoreGive(sem))
assert(false);
// waitedUs = esp_timer_get_time() - start;
}
// if (waitedUs)
// printf("Waited %" PRIu64 " us\n", waitedUs);
}