blob: f45405790360524134a763589290a4fe8f4f8968 [file] [log] [blame]
#include <cstdlib>
#include <math.h>
#include "display.hpp"
#include "display_commands.hpp"
#include "hardware/clocks.h"
#include "hardware/dma.h"
#include "hardware/irq.h"
#include "hardware/pwm.h"
#include "pico/binary_info.h"
#include "pico/time.h"
#include "config.h"
#ifdef DBI_8BIT
#include "dbi-8bit.pio.h"
#else
#include "dbi-spi.pio.h"
#endif
using namespace blit;
// double buffering for lores
static volatile int buf_index = 0;
static volatile bool do_render = true;
static bool have_vsync = false;
static bool backlight_enabled = false;
static uint32_t last_render = 0;
static const uint8_t rotations[]{
0, // 0
MADCTL::HORIZ_ORDER | MADCTL::SWAP_XY | MADCTL::COL_ORDER, // 90
MADCTL::HORIZ_ORDER | MADCTL::SCAN_ORDER | MADCTL::COL_ORDER | MADCTL::ROW_ORDER, // 180
MADCTL::SCAN_ORDER | MADCTL::SWAP_XY | MADCTL::ROW_ORDER // 270
};
enum ST7789Reg
{
RAMCTRL = 0xB0,
PORCTRL = 0xB2,
GCTRL = 0xB7,
VCOMS = 0xBB,
LCMCTRL = 0xC0,
VDVVRHEN = 0xC2,
VRHS = 0xC3,
VDVS = 0xC4,
FRCTRL2 = 0xC6,
PWCTRL1 = 0xD0,
PVGAMCTRL = 0xE0,
NVGAMCTRL = 0xE1,
};
static PIO pio = pio0;
static uint pio_sm = 0;
static uint pio_offset = 0, pio_double_offset = 0;
static uint32_t dma_channel = 0;
static uint16_t win_w, win_h; // window size
static bool write_mode = false; // in RAMWR
static bool pixel_double = false;
static uint16_t *upd_frame_buffer = nullptr;
// frame buffer where pixel data is stored
static uint16_t *frame_buffer = nullptr;
// used for transpose, two scanlines
static uint32_t temp_buffer[DISPLAY_WIDTH];
// pixel double scanline counter
static volatile int cur_scanline = DISPLAY_HEIGHT;
static irq_handler_t cur_irq_handler = nullptr;
// PIO helpers
static void pio_put_byte(PIO pio, uint sm, uint8_t b) {
while (pio_sm_is_tx_fifo_full(pio, sm));
*(volatile uint8_t*)&pio->txf[sm] = b;
}
static void pio_wait(PIO pio, uint sm) {
uint32_t stall_mask = 1u << (PIO_FDEBUG_TXSTALL_LSB + sm);
pio->fdebug |= stall_mask;
while(!(pio->fdebug & stall_mask));
}
static inline void transpose_data(const uint16_t *in, uint16_t *out, int count, int stride) {
auto end = out + count;
do {
*out++ = *in;
in += stride;
} while(out != end);
}
// used for pixel doubling
static void __isr double_dma_irq_handler() {
if(dma_channel_get_irq0_status(dma_channel)) {
dma_channel_acknowledge_irq0(dma_channel);
if(++cur_scanline > win_h / 2)
return;
auto count = cur_scanline == (win_h + 1) / 2 ? win_w / 4 : win_w / 2;
dma_channel_set_trans_count(dma_channel, count, false);
dma_channel_set_read_addr(dma_channel, upd_frame_buffer + (cur_scanline - 1) * (win_w / 2), true);
}
}
static void __isr double_transposed_dma_irq_handler() {
if(dma_channel_get_irq0_status(dma_channel)) {
dma_channel_acknowledge_irq0(dma_channel);
if(cur_scanline > win_h / 2)
return;
// num pixels
auto count = cur_scanline == (win_h + 1) / 2 ? win_w / 2 : win_w;
// start from buffer 0 (to repeat first line)
int temp_buf_index = cur_scanline & 1;
dma_channel_set_trans_count(dma_channel, count / 2, false);
dma_channel_set_read_addr(dma_channel, temp_buffer + (win_w / 2) * (temp_buf_index ^ 1), true);
// prepare next lines
if(++cur_scanline > win_h / 2)
return;
auto in = upd_frame_buffer + (cur_scanline - 1);
auto out = (uint16_t *)temp_buffer + temp_buf_index * win_w;
transpose_data(in, out, win_w / 2, win_h / 2);
if(cur_scanline != (win_h + 1) / 2)
transpose_data(in + 1, out + win_w / 2, win_w / 2, win_h / 2);
}
}
static void __isr transposed_dma_irq_handler() {
if(dma_channel_get_irq0_status(dma_channel)) {
dma_channel_acknowledge_irq0(dma_channel);
if(cur_scanline >= win_h)
return;
// start from buffer 1
int temp_buf_index = cur_scanline & 1;
dma_channel_set_trans_count(dma_channel, win_w, false);
dma_channel_set_read_addr(dma_channel, temp_buffer + (win_w / 2) * temp_buf_index, true);
// prepare next line
if(++cur_scanline >= win_h)
return;
auto in = upd_frame_buffer + cur_scanline;
auto out = (uint16_t *)temp_buffer + (temp_buf_index ^ 1) * win_w;
transpose_data(in, out, win_w, win_h);
}
}
// set DMA irq handler for pixel doubling/palette lookup
static void update_irq_handler() {
irq_handler_t new_handler;
if(LCD_TRANSPOSE)
new_handler = pixel_double ? double_transposed_dma_irq_handler : transposed_dma_irq_handler;
else
new_handler = double_dma_irq_handler;
if(cur_irq_handler == new_handler)
return;
if(cur_irq_handler)
irq_remove_handler(DMA_IRQ_0, cur_irq_handler);
irq_add_shared_handler(DMA_IRQ_0, new_handler, PICO_SHARED_IRQ_HANDLER_DEFAULT_ORDER_PRIORITY);
cur_irq_handler = new_handler;
cur_scanline = win_h; // probably switching mode, set to max so that dma_is_busy thinks we're finished
}
static void command(uint8_t command, size_t len = 0, const char *data = nullptr) {
pio_wait(pio, pio_sm);
if(write_mode) {
// reconfigure to 8 bits
pio_sm_set_enabled(pio, pio_sm, false);
pio->sm[pio_sm].shiftctrl &= ~PIO_SM0_SHIFTCTRL_PULL_THRESH_BITS;
pio->sm[pio_sm].shiftctrl |= (8 << PIO_SM0_SHIFTCTRL_PULL_THRESH_LSB) | PIO_SM0_SHIFTCTRL_AUTOPULL_BITS;
// switch back to raw
pio_sm_restart(pio, pio_sm);
pio_sm_set_wrap(pio, pio_sm, pio_offset + dbi_raw_wrap_target, pio_offset + dbi_raw_wrap);
pio_sm_exec(pio, pio_sm, pio_encode_jmp(pio_offset));
pio_sm_set_enabled(pio, pio_sm, true);
write_mode = false;
}
gpio_put(LCD_CS_PIN, 0);
gpio_put(LCD_DC_PIN, 0); // command mode
pio_put_byte(pio, pio_sm, command);
if(data) {
pio_wait(pio, pio_sm);
gpio_put(LCD_DC_PIN, 1); // data mode
for(size_t i = 0; i < len; i++)
pio_put_byte(pio, pio_sm, data[i]);
}
pio_wait(pio, pio_sm);
gpio_put(LCD_CS_PIN, 1);
}
static void set_window(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
if(LCD_TRANSPOSE) {
std::swap(x, y);
std::swap(w, h);
}
uint32_t cols = __builtin_bswap32((x << 16) | (x + w - 1));
uint32_t rows = __builtin_bswap32((y << 16) | (y + h - 1));
command(MIPIDCS::SetColumnAddress, 4, (const char *)&cols);
command(MIPIDCS::SetRowAddress, 4, (const char *)&rows);
win_w = w;
win_h = h;
}
static void send_init_sequence() {
command(MIPIDCS::SoftReset);
sleep_ms(150);
#ifdef LCD_VSYNC_PIN
command(MIPIDCS::SetTearOn, 1, "\x00"); // enable frame sync signal if used
#endif
command(MIPIDCS::SetPixelFormat, 1, "\x05"); // 16 bits per pixel
int window_x = 0, window_y = 0;
if(DISPLAY_WIDTH == 240 && DISPLAY_HEIGHT == 240) {
command(ST7789Reg::PORCTRL, 5, "\x0c\x0c\x00\x33\x33");
command(ST7789Reg::GCTRL, 1, "\x14");
command(ST7789Reg::VCOMS, 1, "\x37");
command(ST7789Reg::LCMCTRL, 1, "\x2c");
command(ST7789Reg::VDVVRHEN, 1, "\x01");
command(ST7789Reg::VRHS, 1, "\x12");
command(ST7789Reg::VDVS, 1, "\x20");
command(ST7789Reg::PWCTRL1, 2, "\xa4\xa1");
command(ST7789Reg::PVGAMCTRL, 14, "\xD0\x08\x11\x08\x0c\x15\x39\x33\x50\x36\x13\x14\x29\x2d");
command(ST7789Reg::NVGAMCTRL, 14, "\xD0\x08\x10\x08\x06\x06\x39\x44\x51\x0b\x16\x14\x2f\x31");
// trigger "vsync" slightly earlier to avoid tearing while pixel-doubling
// (this is still outside of the visible part of the screen)
command(MIPIDCS::SetTearScanline, 2, "\x01\x2C");
}
if(DISPLAY_WIDTH == 320 && DISPLAY_HEIGHT == 240) {
command(ST7789Reg::PORCTRL, 5, "\x0c\x0c\x00\x33\x33");
command(ST7789Reg::GCTRL, 1, "\x35");
command(ST7789Reg::VCOMS, 1, "\x1f");
command(ST7789Reg::LCMCTRL, 1, "\x2c");
command(ST7789Reg::VDVVRHEN, 1, "\x01");
command(ST7789Reg::VRHS, 1, "\x12");
command(ST7789Reg::VDVS, 1, "\x20");
command(ST7789Reg::PWCTRL1, 2, "\xa4\xa1");
command(0xd6, 1, "\xa1"); // ???
command(ST7789Reg::PVGAMCTRL, 14, "\xD0\x08\x11\x08\x0C\x15\x39\x33\x50\x36\x13\x14\x29\x2D");
command(ST7789Reg::NVGAMCTRL, 14, "\xD0\x08\x10\x08\x06\x06\x39\x44\x51\x0B\x16\x14\x2F\x31");
}
command(ST7789Reg::FRCTRL2, 1, "\x15"); // 50Hz
command(MIPIDCS::EnterInvertMode); // set inversion mode
command(MIPIDCS::ExitSleepMode); // leave sleep mode
command(MIPIDCS::DisplayOn); // turn display on
sleep_ms(100);
uint8_t madctl = MADCTL::RGB | rotations[LCD_ROTATION / 90];
if(LCD_TRANSPOSE)
madctl ^= MADCTL::SWAP_XY;
command(MIPIDCS::SetAddressMode, 1, (char *)&madctl);
// setup correct addressing window
if(DISPLAY_WIDTH == 240 && DISPLAY_HEIGHT == 135) {
window_x = 40;
window_y = 53;
}
set_window(window_x, window_y, DISPLAY_WIDTH, DISPLAY_HEIGHT);
}
static void prepare_write() {
pio_wait(pio, pio_sm);
// setup for writing
uint8_t r = MIPIDCS::WriteMemoryStart;
gpio_put(LCD_CS_PIN, 0);
gpio_put(LCD_DC_PIN, 0); // command mode
pio_put_byte(pio, pio_sm, r);
pio_wait(pio, pio_sm);
gpio_put(LCD_DC_PIN, 1); // data mode
pio_sm_set_enabled(pio, pio_sm, false);
pio_sm_restart(pio, pio_sm);
if(pixel_double) {
// switch program
pio_sm_set_wrap(pio, pio_sm, pio_double_offset + dbi_pixel_double_wrap_target, pio_double_offset + dbi_pixel_double_wrap);
// 32 bits, no autopull
pio->sm[pio_sm].shiftctrl &= ~(PIO_SM0_SHIFTCTRL_PULL_THRESH_BITS | PIO_SM0_SHIFTCTRL_AUTOPULL_BITS);
pio_sm_exec(pio, pio_sm, pio_encode_jmp(pio_double_offset));
// reconfigure dma size
dma_channel_hw_addr(dma_channel)->al1_ctrl &= ~DMA_CH0_CTRL_TRIG_DATA_SIZE_BITS;
dma_channel_hw_addr(dma_channel)->al1_ctrl |= DMA_SIZE_32 << DMA_CH0_CTRL_TRIG_DATA_SIZE_LSB;
} else {
// 16 bits, autopull
pio->sm[pio_sm].shiftctrl &= ~PIO_SM0_SHIFTCTRL_PULL_THRESH_BITS;
pio->sm[pio_sm].shiftctrl |= (16 << PIO_SM0_SHIFTCTRL_PULL_THRESH_LSB) | PIO_SM0_SHIFTCTRL_AUTOPULL_BITS;
dma_channel_hw_addr(dma_channel)->al1_ctrl &= ~DMA_CH0_CTRL_TRIG_DATA_SIZE_BITS;
dma_channel_hw_addr(dma_channel)->al1_ctrl |= DMA_SIZE_16 << DMA_CH0_CTRL_TRIG_DATA_SIZE_LSB;
}
pio_sm_set_enabled(pio, pio_sm, true);
write_mode = true;
}
static void update() {
dma_channel_wait_for_finish_blocking(dma_channel);
if(!frame_buffer)
return;
// update window if needed
auto expected_win = cur_surf_info.bounds * (pixel_double ? 2 : 1);
if(expected_win.w != win_w || expected_win.h != win_h)
set_window((DISPLAY_WIDTH - expected_win.w) / 2, (DISPLAY_HEIGHT - expected_win.h) / 2, expected_win.w, expected_win.h);
if(!write_mode)
prepare_write();
upd_frame_buffer = frame_buffer;
if(LCD_TRANSPOSE) {
// setup first two lines
auto stride = pixel_double ? win_h / 2 : win_h;
auto count = pixel_double ? win_w / 2 : win_w;
transpose_data(frame_buffer, (uint16_t *)temp_buffer, count, stride);
transpose_data(frame_buffer + 1, (uint16_t *)temp_buffer + count, count, stride);
cur_scanline = 1;
int dma_count = pixel_double ? win_w / 4 : win_w;
dma_channel_set_trans_count(dma_channel, dma_count, false);
dma_channel_set_read_addr(dma_channel, temp_buffer, true);
return;
}
if(pixel_double) {
cur_scanline = 0;
dma_channel_set_trans_count(dma_channel, win_w / 4, false);
} else
dma_channel_set_trans_count(dma_channel, win_w * win_h, false);
dma_channel_set_read_addr(dma_channel, frame_buffer, true);
}
static void set_backlight(uint8_t brightness) {
#ifdef LCD_BACKLIGHT_PIN
// gamma correct the provided 0-255 brightness value onto a
// 0-65535 range for the pwm counter
float gamma = 2.8;
uint16_t value = (uint16_t)(pow((float)(brightness) / 255.0f, gamma) * 65535.0f + 0.5f);
pwm_set_gpio_level(LCD_BACKLIGHT_PIN, value);
#endif
}
static void set_pixel_double(bool pd) {
pixel_double = pd;
// nop to reconfigure PIO
if(write_mode)
command(0);
dma_channel_set_irq0_enabled(dma_channel, false);
update_irq_handler();
if(pixel_double || LCD_TRANSPOSE) {
dma_channel_acknowledge_irq0(dma_channel);
dma_channel_set_irq0_enabled(dma_channel, true);
} else
dma_channel_set_irq0_enabled(dma_channel, false);
}
static void clear() {
if(!write_mode)
prepare_write();
for(int i = 0; i < win_w * win_h; i++)
pio_sm_put_blocking(pio, pio_sm,0);
}
static bool dma_is_busy() {
if(pixel_double && cur_scanline <= win_h / 2)
return true;
else if(LCD_TRANSPOSE && !pixel_double && cur_scanline < win_h)
return true;
return dma_channel_is_busy(dma_channel);
}
#ifdef LCD_VSYNC_PIN
static void vsync_callback(uint gpio, uint32_t events) {
if(!do_render && !dma_is_busy()) {
::update();
do_render = true;
}
}
#endif
void init_display() {
frame_buffer = screen_fb;
// configure pins
gpio_set_function(LCD_DC_PIN, GPIO_FUNC_SIO);
gpio_set_dir(LCD_DC_PIN, GPIO_OUT);
gpio_set_function(LCD_CS_PIN, GPIO_FUNC_SIO);
gpio_set_dir(LCD_CS_PIN, GPIO_OUT);
gpio_put(LCD_CS_PIN, 1);
bi_decl_if_func_used(bi_1pin_with_name(LCD_DC_PIN, "Display D/C"));
bi_decl_if_func_used(bi_1pin_with_name(LCD_CS_PIN, "Display CS"));
// if supported by the display then the vsync pin is
// toggled high during vertical blanking period
#ifdef LCD_VSYNC_PIN
gpio_set_function(LCD_VSYNC_PIN, GPIO_FUNC_SIO);
gpio_set_dir(LCD_VSYNC_PIN, GPIO_IN);
gpio_set_pulls(LCD_VSYNC_PIN, false, true);
bi_decl_if_func_used(bi_1pin_with_name(LCD_VSYNC_PIN, "Display TE/VSync"));
#endif
// if a backlight pin is provided then set it up for
// pwm control
#ifdef LCD_BACKLIGHT_PIN
pwm_config pwm_cfg = pwm_get_default_config();
pwm_set_wrap(pwm_gpio_to_slice_num(LCD_BACKLIGHT_PIN), 65535);
pwm_init(pwm_gpio_to_slice_num(LCD_BACKLIGHT_PIN), &pwm_cfg, true);
gpio_set_function(LCD_BACKLIGHT_PIN, GPIO_FUNC_PWM);
bi_decl_if_func_used(bi_1pin_with_name(LCD_BACKLIGHT_PIN, "Display Backlight"));
#endif
#ifdef DBI_8BIT
// init RD
gpio_init(LCD_RD_PIN);
gpio_set_dir(LCD_RD_PIN, GPIO_OUT);
gpio_put(LCD_RD_PIN, 1);
bi_decl_if_func_used(bi_1pin_with_name(LCD_RD_PIN, "Display RD"));
#endif
#ifdef LCD_RESET_PIN
gpio_set_function(LCD_RESET_PIN, GPIO_FUNC_SIO);
gpio_set_dir(LCD_RESET_PIN, GPIO_OUT);
gpio_put(LCD_RESET_PIN, 0);
sleep_ms(100);
gpio_put(LCD_RESET_PIN, 1);
bi_decl_if_func_used(bi_1pin_with_name(LCD_RESET_PIN, "Display Reset"));
#endif
// setup PIO
#if LCD_MOSI_PIN >= 32 || LCD_SCK_PIN >= 32
// assumes anything else using this PIO can also deal with the base
static_assert(LCD_MOSI_PIN >= 16 && LCD_SCK_PIN >= 16);
pio_set_gpio_base(pio, 16);
#endif
pio_offset = pio_add_program(pio, &dbi_raw_program);
pio_double_offset = pio_add_program(pio, &dbi_pixel_double_program);
pio_sm = pio_claim_unused_sm(pio, true);
pio_sm_config cfg = dbi_raw_program_get_default_config(pio_offset);
#ifdef DBI_8BIT
const int out_width = 8;
#else // SPI
const int out_width = 1;
#endif
const int clkdiv = std::ceil(clock_get_hz(clk_sys) / float(LCD_MAX_CLOCK * 2));
sm_config_set_clkdiv(&cfg, clkdiv);
sm_config_set_out_shift(&cfg, false, true, 8);
sm_config_set_out_pins(&cfg, LCD_MOSI_PIN, out_width);
sm_config_set_fifo_join(&cfg, PIO_FIFO_JOIN_TX);
sm_config_set_sideset_pins(&cfg, LCD_SCK_PIN);
// init pins
for(int i = 0; i < out_width; i++)
pio_gpio_init(pio, LCD_MOSI_PIN + i);
pio_gpio_init(pio, LCD_SCK_PIN);
pio_sm_set_consecutive_pindirs(pio, pio_sm, LCD_MOSI_PIN, out_width, true);
pio_sm_set_consecutive_pindirs(pio, pio_sm, LCD_SCK_PIN, 1, true);
pio_sm_init(pio, pio_sm, pio_offset, &cfg);
pio_sm_set_enabled(pio, pio_sm, true);
#ifdef DBI_8BIT
// these are really D0/WR
bi_decl_if_func_used(bi_pin_mask_with_name(0xFFull << LCD_MOSI_PIN, "Display Data"));
bi_decl_if_func_used(bi_1pin_with_name(LCD_SCK_PIN, "Display WR"));
#else
bi_decl_if_func_used(bi_1pin_with_name(LCD_MOSI_PIN, "Display TX"));
bi_decl_if_func_used(bi_1pin_with_name(LCD_SCK_PIN, "Display SCK"));
#endif
// send initialisation sequence for our standard displays based on the width and height
send_init_sequence();
// initialise dma channel for transmitting pixel data to screen
dma_channel = dma_claim_unused_channel(true);
dma_channel_config config = dma_channel_get_default_config(dma_channel);
channel_config_set_transfer_data_size(&config, DMA_SIZE_16);
channel_config_set_dreq(&config, pio_get_dreq(pio, pio_sm, true));
dma_channel_configure(
dma_channel, &config, &pio->txf[pio_sm], frame_buffer, DISPLAY_WIDTH * DISPLAY_HEIGHT, false);
update_irq_handler();
irq_set_enabled(DMA_IRQ_0, true);
clear();
#ifdef LCD_VSYNC_PIN
gpio_set_irq_enabled_with_callback(LCD_VSYNC_PIN, GPIO_IRQ_EDGE_RISE, true, vsync_callback);
have_vsync = true;
#endif
}
void update_display(uint32_t time) {
if((do_render || (!have_vsync && time - last_render >= 20)) && (fb_double_buffer || !dma_is_busy())) {
if(fb_double_buffer) {
buf_index ^= 1;
screen.data = (uint8_t *)screen_fb + (buf_index) * get_display_page_size();
frame_buffer = (uint16_t *)screen.data;
}
::render(time);
if(!have_vsync) {
while(dma_is_busy()) {} // may need to wait for lores.
::update();
}
if(last_render && !backlight_enabled) {
// the first render should have made it to the screen at this point
set_backlight(255);
backlight_enabled = true;
}
last_render = time;
do_render = false;
}
}
void init_display_core1() {
}
void update_display_core1() {
}
bool display_render_needed() {
return do_render;
}
bool display_mode_supported(blit::ScreenMode new_mode, const blit::SurfaceTemplate &new_surf_template) {
if(new_surf_template.format != blit::PixelFormat::RGB565)
return false;
// allow any size that will fit
blit::Size expected_bounds(DISPLAY_WIDTH, DISPLAY_HEIGHT);
if(new_surf_template.bounds.w <= expected_bounds.w && new_surf_template.bounds.h <= expected_bounds.h)
return true;
return false;
}
void display_mode_changed(blit::ScreenMode new_mode, blit::SurfaceTemplate &new_surf_template) {
if(have_vsync)
do_render = true; // prevent starting an update during switch
set_pixel_double(new_mode == ScreenMode::lores);
if(new_mode == ScreenMode::hires)
frame_buffer = screen_fb;
}