echoplayer: drive LCD parallel bus using STM32 LTDC

This is much, much faster than using SPI and is able to
offload the CPU completely.

Change-Id: Ia4c0289775296fe41d594ba849bd057e8482306e
This commit is contained in:
Aidan MacDonald 2025-12-27 20:33:43 +00:00 committed by Solomon Peachy
parent e98dc7936c
commit 4ceb9e22d6
4 changed files with 172 additions and 38 deletions

View file

@ -33,6 +33,7 @@ enum stm_clock
STM_CLOCK_SPI4_KER,
STM_CLOCK_SPI5_KER,
STM_CLOCK_SPI6_KER,
STM_CLOCK_LTDC_KER,
STM_NUM_CLOCKS,
};

View file

@ -19,6 +19,7 @@
*
****************************************************************************/
#include "clock-stm32h7.h"
#include "lcd-echoplayer.h"
#include "panic.h"
#include "regs/stm32h743/flash.h"
#include "regs/stm32h743/fmc.h"
@ -38,14 +39,23 @@ static void init_hse(void)
static void init_pll(void)
{
/* Select HSE/4 input for PLL1 (6 MHz) */
/* For simplicity, PLL parameters are hardcoded */
_Static_assert(STM32_HSE_FREQ == 24000000,
"HSE frequency not correct");
_Static_assert(LCD_DOTCLOCK_FREQ == 6199200,
"PLL3 parameters not correct for dot clock");
/*
* Use HSE/4 input for PLL1
* Use HSE/16 input for PLL3
*/
reg_writef(RCC_PLLCKSELR,
PLLSRC_V(HSE),
DIVM1(4),
DIVM2(0),
DIVM3(0));
DIVM3(16));
/* Enable PLL1P and PLL1Q */
/* Enable PLL1P, PLL1Q, PLL3R */
reg_writef(RCC_PLLCFGR,
DIVP1EN(1),
DIVQ1EN(1),
@ -55,9 +65,13 @@ static void init_pll(void)
DIVR2EN(0),
DIVP3EN(0),
DIVQ3EN(0),
DIVR3EN(0),
DIVR3EN(1),
PLL1RGE_V(4_8MHZ),
PLL1VCOSEL_V(WIDE));
PLL1VCOSEL_V(WIDE),
PLL1FRACEN(0),
PLL3RGE_V(1_2MHZ),
PLL3VCOSEL_V(MEDIUM),
PLL3FRACEN(0));
reg_writef(RCC_PLL1DIVR,
DIVN(80 - 1), /* 6 * 80 = 480 MHz */
@ -65,8 +79,18 @@ static void init_pll(void)
DIVQ(8 - 1), /* 480 / 8 = 60 MHz */
DIVR(1 - 1));
reg_writef(RCC_CR, PLL1ON(1));
reg_writef(RCC_PLL3FRACR, FRACN(1468));
reg_writef(RCC_PLL3DIVR,
DIVN(161 - 1), /* approx 241.768 MHz */
DIVP(1 - 1),
DIVQ(1 - 1),
DIVR(39 - 1)); /* approx 6.1992 MHz */
reg_writef(RCC_PLLCFGR, PLL3FRACEN(1));
reg_writef(RCC_CR, PLL1ON(1), PLL3ON(1));
while (!reg_readf(RCC_CR, PLL1RDY));
while (!reg_readf(RCC_CR, PLL3RDY));
}
static void init_vos(void)
@ -136,6 +160,9 @@ static void init_lse(void)
static void init_periph_clock(void)
{
reg_writef(RCC_D2CCIP1R, SPI45SEL_V(HSE));
/* Enable AXI SRAM in sleep mode to allow DMA'ing out of it */
reg_writef(RCC_AHB3LPENR, AXISRAMEN(1));
}
void stm_target_clock_init(void)
@ -157,6 +184,11 @@ void stm_target_clock_enable(enum stm_clock clock, bool enable)
reg_writef(RCC_APB2LPENR, SPI5EN(enable));
break;
case STM_CLOCK_LTDC_KER:
reg_writef(RCC_APB3ENR, LTDCEN(enable));
reg_writef(RCC_APB3LPENR, LTDCEN(enable));
break;
default:
panicf("%s: unsupported clock %d", __func__, (int)clock);
break;

View file

@ -21,15 +21,22 @@
#include "system.h"
#include "kernel.h"
#include "lcd.h"
#include "lcd-echoplayer.h"
#include "nvic-arm.h"
#include "spi-stm32h7.h"
#include "gpio-stm32h7.h"
#include "clock-stm32h7.h"
#include "regs/stm32h743/rcc.h"
#include "regs/stm32h743/spi.h"
#include "regs/stm32h743/ltdc.h"
/* ILI9342C specifies 10 MHz max */
#define LCD_SPI_FREQ 10000000
/*
* ILI9342C specifies 10 MHz max
*
* Use 12MHz for now -- for some reason using 6 MHz doesn't work
* to enable RGB mode, but works fine to send graphics in SPI mode?
*/
#define LCD_SPI_FREQ 12000000
struct stm_spi_config spi_cfg = {
.instance = ITA_SPI5,
@ -51,10 +58,46 @@ struct stm_spi spi;
stm_spi_transmit(&spi, arr, sizeof(arr)); \
} while (0)
static void set_row_column_address(int x, int y, int w, int h)
static void init_ltdc(void)
{
ili_cmd(0x2a, x >> 8, x & 0xff, (w-1) >> 8, (w-1) & 0xff);
ili_cmd(0x2b, y >> 8, y & 0xff, (h-1) >> 8, (h-1) & 0xff);
/* Enable LTDC clock */
stm_clock_enable(STM_CLOCK_LTDC_KER);
/* Set timing parameters */
const uint32_t hsw = LCD_HSW - 1;
const uint32_t ahbp = hsw + LCD_HBP;
const uint32_t aaw = ahbp + LCD_HAW;
const uint32_t totw = aaw + LCD_HFP;
const uint32_t vsh = LCD_VSH - 1;
const uint32_t avbp = vsh + LCD_VBP;
const uint32_t aah = avbp + LCD_VAH;
const uint32_t toth = aah + LCD_VFP;
reg_writef(LTDC_SSCR, HSW(hsw), VSH(vsh));
reg_writef(LTDC_BPCR, AHBP(ahbp), AVBP(avbp));
reg_writef(LTDC_AWCR, AAW(aaw), AAH(aah));
reg_writef(LTDC_TWCR, TOTALW(totw), TOTALH(toth));
/* Set interface polarity */
reg_writef(LTDC_GCR, HSPOL(0), VSPOL(0), DEPOL(0), PCPOL(0), DEN(0));
/* Enable layer 1 to blit framebuffer to whole screen */
const uint32_t row_bytes = LCD_WIDTH * FB_DATA_SZ;
reg_assignf(LTDC_LAYER_WHPCR(0), WHSPPOS(ahbp + LCD_HAW), WHSTPOS(ahbp + 1));
reg_assignf(LTDC_LAYER_WVPCR(0), WVSPPOS(avbp + LCD_VAH), WVSTPOS(avbp + 1));
reg_assignf(LTDC_LAYER_PFCR(0), PF(BV_LTDC_LAYER_PFCR_PF_RGB565));
reg_var(LTDC_LAYER_CFBAR(0)) = (uintptr_t)FBADDR(0, 0);
reg_assignf(LTDC_LAYER_CFBLR(0), CFBP(row_bytes), CFBLL(row_bytes + 7));
reg_assignf(LTDC_LAYER_CFBLNR(0), CFBLNBR(LCD_HEIGHT));
reg_assignf(LTDC_LAYER_CR(0), LEN(1));
/* Reload shadow registers to enable layer 1 */
reg_writef(LTDC_SRCR, IMR(1));
/* Enable LTDC output */
reg_writef(LTDC_GCR, LTDCEN(1));
}
void lcd_init_device(void)
@ -63,6 +106,9 @@ void lcd_init_device(void)
stm_spi_init(&spi, &spi_cfg);
nvic_enable_irq(NVIC_IRQN_SPI5);
/* Enable LCD controller */
init_ltdc();
/* Ensure controller is reset */
gpio_set_level(GPIO_LCD_RESET, 0);
sleep(12);
@ -77,8 +123,23 @@ void lcd_init_device(void)
/* memory access control (X/Y invert, BGR panel) */
ili_cmd(0x36, 0xc8);
/* pixel format set (16bpp) */
ili_cmd(0x3a, 0x55);
/* pixel format set (18bpp for RGB bus, 16bpp for SPI bus) */
ili_cmd(0x3a, 0x65);
/* send set EXTC command to allow configuring RGB interface */
ili_cmd(0xc8, 0xff, 0x93, 0x42);
/*
* Enable RGB interface transferring to internal GRAM.
*
* Direct to shift register mode doesn't work; for one, the
* framebuffer doesn't get transferred properly which might
* just be timing issues. Two, the display is horizontally
* flipped and there doesn't seem to be a way to change it
* in the shift register mode.
*/
ili_cmd(0xb0, 0xc0);
ili_cmd(0xf6, 0x01, 0x00, 0x06);
/* display ON */
ili_cmd(0x29);
@ -91,14 +152,11 @@ bool lcd_active(void)
void lcd_update(void)
{
lcd_update_rect(0, 0, LCD_WIDTH, LCD_HEIGHT);
commit_dcache();
}
void lcd_update_rect(int x, int y, int width, int height)
{
/* row buffer to minimize time wasted in post-transaction delay */
static uint16_t row[LCD_WIDTH * 2];
if (x < 0)
x = 0;
else if (x >= LCD_WIDTH)
@ -115,27 +173,8 @@ void lcd_update_rect(int x, int y, int width, int height)
if (height > LCD_HEIGHT - y)
height = LCD_HEIGHT - y;
set_row_column_address(x, y, width, height);
ili_cmd(0x2c);
for (int py = y; py < height; ++py)
{
for (int px = x; px < width; ++px)
{
fb_data *fb = FBADDR(px, py);
uint16_t *data = &row[px * 2];
data[0] = 0x100;
data[0] |= (FB_UNPACK_RED(*fb) >> 3) << 3;
data[0] |= (FB_UNPACK_GREEN(*fb) >> 5);
data[1] = 0x100;
data[1] |= ((FB_UNPACK_GREEN(*fb) >> 2) & 0x7) << 5;
data[1] |= (FB_UNPACK_BLUE(*fb) >> 3);
}
stm_spi_transmit(&spi, &row[x * 2], width * sizeof(*row) * 2);
}
for (int dy = y; dy < height; ++dy)
commit_dcache_range(FBADDR(x, dy), FB_DATA_SZ * width);
}
void spi5_irq_handler(void)

View file

@ -0,0 +1,62 @@
/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
* $Id$
*
* Copyright (C) 2025 Aidan MacDonald
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
****************************************************************************/
#ifndef __LCD_ECHOPLAYER_H__
#define __LCD_ECHOPLAYER_H__
#include "config.h"
/* Hsync pulse width in units of dot clocks */
#define LCD_HSW 10
/* Hsync back porch in units of dot clocks */
#define LCD_HBP 20
/* Horizontal active width in units of dot clocks */
#define LCD_HAW LCD_WIDTH
/* Hsync front porch in units of dot clocks */
#define LCD_HFP 10
/* Vsync pulse height in units of horizontal lines */
#define LCD_VSH 2
/* Vsync back porch in units of horizontal lines */
#define LCD_VBP 2
/* Vertical active height in units of horizontal lines */
#define LCD_VAH LCD_HEIGHT
/* Vsync front porch in units of horizontal lines */
#define LCD_VFP 2
/* Total horizontal width in dots */
#define LCD_HWIDTH (LCD_HSW + LCD_HBP + LCD_HAW + LCD_HFP)
/* Total vertical height in lines */
#define LCD_VHEIGHT (LCD_VSH + LCD_VBP + LCD_VAH + LCD_VFP)
/* Target frame rate */
#define LCD_FPS 70
/* Dot clock frequency */
#define LCD_DOTCLOCK_FREQ (LCD_FPS * LCD_HWIDTH * LCD_VHEIGHT)
#endif /* __LCD_ECHOPLAYER_H__ */