blob: 124108ab466cf2d9487de8ca6c77c9ca557e1618 [file] [log] [blame] [edit]
#include "st7789.hpp"
#include <cstdlib>
#include <math.h>
#include "hardware/dma.h"
#include "hardware/irq.h"
#include "hardware/pwm.h"
#include "pico/time.h"
#include "config.h"
#include "st7789.pio.h"
namespace pimoroni {
enum MADCTL : uint8_t {
ROW_ORDER = 0b10000000,
COL_ORDER = 0b01000000,
SWAP_XY = 0b00100000, // AKA "MV"
SCAN_ORDER = 0b00010000,
RGB = 0b00001000,
HORIZ_ORDER = 0b00000100
};
#define ROT_240_240_0 0
#define ROT_240_240_90 MADCTL::SWAP_XY | MADCTL::HORIZ_ORDER | MADCTL::COL_ORDER
#define ROT_240_240_180 MADCTL::SCAN_ORDER | MADCTL::HORIZ_ORDER | MADCTL::COL_ORDER | MADCTL::ROW_ORDER
#define ROT_240_240_270 MADCTL::SWAP_XY | MADCTL::HORIZ_ORDER | MADCTL::ROW_ORDER
enum reg {
SWRESET = 0x01,
TEOFF = 0x34,
TEON = 0x35,
MADCTL = 0x36,
COLMOD = 0x3A,
GCTRL = 0xB7,
VCOMS = 0xBB,
LCMCTRL = 0xC0,
VDVVRHEN = 0xC2,
VRHS = 0xC3,
VDVS = 0xC4,
FRCTRL2 = 0xC6,
PWCTRL1 = 0xD0,
PORCTRL = 0xB2,
PVGAMCTRL = 0xE0,
NVGAMCTRL = 0xE1,
INVOFF = 0x20,
SLPOUT = 0x11,
DISPON = 0x29,
GAMSET = 0x26,
DISPOFF = 0x28,
RAMWR = 0x2C,
INVON = 0x21,
CASET = 0x2A,
RASET = 0x2B,
STE = 0x44
};
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));
}
// used for pixel doubling
// only handles one instance
static ST7789 *st7789_ptr = nullptr;
static volatile int cur_scanline = 240;
void __isr st7789_dma_irq_handler() {
auto channel = st7789_ptr->dma_channel;
if(dma_channel_get_irq0_status(channel)) {
dma_channel_acknowledge_irq0(channel);
if(++cur_scanline > st7789_ptr->win_h / 2)
return;
auto count = cur_scanline == st7789_ptr->win_h / 2 ? st7789_ptr->win_w / 4 : st7789_ptr->win_w / 2;
dma_channel_set_trans_count(channel, count, false);
dma_channel_set_read_addr(channel, st7789_ptr->upd_frame_buffer + (cur_scanline - 1) * (st7789_ptr->win_w / 2), true);
}
}
void ST7789::init(bool auto_init_sequence) {
// configure pins
gpio_set_function(dc, GPIO_FUNC_SIO);
gpio_set_dir(dc, GPIO_OUT);
gpio_set_function(cs, GPIO_FUNC_SIO);
gpio_set_dir(cs, GPIO_OUT);
// if supported by the display then the vsync pin is
// toggled high during vertical blanking period
if(vsync != -1) {
gpio_set_function(vsync, GPIO_FUNC_SIO);
gpio_set_dir(vsync, GPIO_IN);
gpio_set_pulls(vsync, false, true);
}
// if a backlight pin is provided then set it up for
// pwm control
if(bl != -1) {
pwm_config cfg = pwm_get_default_config();
pwm_set_wrap(pwm_gpio_to_slice_num(bl), 65535);
pwm_init(pwm_gpio_to_slice_num(bl), &cfg, true);
gpio_set_function(bl, GPIO_FUNC_PWM);
}
if(reset != -1) {
gpio_set_function(reset, GPIO_FUNC_SIO);
gpio_set_dir(reset, GPIO_OUT);
gpio_put(reset, 0);
sleep_ms(100);
gpio_put(reset, 1);
}
// setup PIO
pio_offset = pio_add_program(pio, &st7789_raw_program);
pio_double_offset = pio_add_program(pio, &st7789_pixel_double_program);
pio_sm_config cfg = st7789_raw_program_get_default_config(pio_offset);
#if OVERCLOCK_250
sm_config_set_clkdiv(&cfg, 2); // back to 62.5MHz from overclock
#endif
sm_config_set_out_shift(&cfg, false, true, 8);
sm_config_set_out_pins(&cfg, mosi, 1);
sm_config_set_fifo_join(&cfg, PIO_FIFO_JOIN_TX);
sm_config_set_sideset_pins(&cfg, sck);
pio_gpio_init(pio, mosi);
pio_gpio_init(pio, sck);
pio_sm_set_consecutive_pindirs(pio, pio_sm, mosi, 1, true);
pio_sm_set_consecutive_pindirs(pio, pio_sm, sck, 1, true);
pio_sm_init(pio, pio_sm, pio_offset, &cfg);
pio_sm_set_enabled(pio, pio_sm, true);
// if auto_init_sequence then send initialisation sequence
// for our standard displays based on the width and height
if(auto_init_sequence) {
command(reg::SWRESET);
sleep_ms(150);
command(reg::TEON, 1, "\x00"); // enable frame sync signal if used
command(reg::COLMOD, 1, "\x05"); // 16 bits per pixel
if(width == 240 && height == 240) {
command(reg::PORCTRL, 5, "\x0c\x0c\x00\x33\x33");
command(reg::GCTRL, 1, "\x14");
command(reg::VCOMS, 1, "\x37");
command(reg::LCMCTRL, 1, "\x2c");
command(reg::VDVVRHEN, 1, "\x01");
command(reg::VRHS, 1, "\x12");
command(reg::VDVS, 1, "\x20");
command(reg::PWCTRL1, 2, "\xa4\xa1");
command(reg::PVGAMCTRL, 14, "\xD0\x08\x11\x08\x0c\x15\x39\x33\x50\x36\x13\x14\x29\x2d");
command(reg::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(reg::STE, 2, "\x01\x2C");
}
command(reg::FRCTRL2, 1, "\x15"); // 50Hz
command(reg::INVON); // set inversion mode
command(reg::SLPOUT); // leave sleep mode
command(reg::DISPON); // turn display on
sleep_ms(100);
// setup correct addressing window
uint8_t madctl = MADCTL::RGB;
if(width == 240 && height == 240) {
madctl |= MADCTL::HORIZ_ORDER;
set_window(0, 0, 240, 240);
}
if(width == 240 && height == 135) {
madctl |= MADCTL::COL_ORDER | MADCTL::SWAP_XY | MADCTL::SCAN_ORDER;
set_window(40, 53, 240, 135);
}
command(reg::MADCTL, 1, (char *)&madctl);
}
// 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, width * height, false);
irq_add_shared_handler(DMA_IRQ_0, st7789_dma_irq_handler, PICO_SHARED_IRQ_HANDLER_DEFAULT_ORDER_PRIORITY);
irq_set_enabled(DMA_IRQ_0, true);
st7789_ptr = this;
}
void ST7789::command(uint8_t command, size_t len, const char *data) {
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 + st7789_raw_wrap_target, pio_offset + st7789_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(cs, 0);
gpio_put(dc, 0); // command mode
pio_put_byte(pio, pio_sm, command);
if(data) {
pio_wait(pio, pio_sm);
gpio_put(dc, 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(cs, 1);
}
void ST7789::update(bool dont_block) {
if(dma_channel_is_busy(dma_channel) && dont_block) {
return;
}
dma_channel_wait_for_finish_blocking(dma_channel);
if(!write_mode)
prepare_write();
if(pixel_double) {
cur_scanline = 0;
upd_frame_buffer = frame_buffer;
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);
}
void ST7789::set_backlight(uint8_t brightness) {
// 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(bl, value);
}
bool ST7789::vsync_callback(gpio_irq_callback_t callback) {
if(vsync == -1)
return false;
gpio_set_irq_enabled_with_callback(vsync, GPIO_IRQ_EDGE_RISE, true, callback);
return true;
}
void ST7789::set_window(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
uint32_t cols = __builtin_bswap32((x << 16) | (x + w - 1));
uint32_t rows = __builtin_bswap32((y << 16) | (y + h - 1));
command(reg::CASET, 4, (const char *)&cols);
command(reg::RASET, 4, (const char *)&rows);
win_w = w;
win_h = h;
}
void ST7789::set_pixel_double(bool pd) {
pixel_double = pd;
// nop to reconfigure PIO
if(write_mode)
command(0);
if(pixel_double) {
dma_channel_acknowledge_irq0(dma_channel);
dma_channel_set_irq0_enabled(dma_channel, true);
} else
dma_channel_set_irq0_enabled(dma_channel, false);
}
void ST7789::clear() {
if(!write_mode)
prepare_write();
for(int i = 0; i < win_w * win_h; i++)
pio_sm_put_blocking(pio, pio_sm, 0);
}
bool ST7789::dma_is_busy() {
if(pixel_double && cur_scanline <= win_h / 2)
return true;
return dma_channel_is_busy(dma_channel);
}
void ST7789::prepare_write() {
pio_wait(pio, pio_sm);
// setup for writing
uint8_t r = reg::RAMWR;
gpio_put(cs, 0);
gpio_put(dc, 0); // command mode
pio_put_byte(pio, pio_sm, r);
pio_wait(pio, pio_sm);
gpio_put(dc, 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 + st7789_pixel_double_wrap_target, pio_double_offset + st7789_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;
}
}