A few months ago I started experimenting with new methods to get VGA output from a Teensy 4.1 (rather than the VGA_t4 library), the first attempt was using the LCDIF module. I was only going for 4-bit output (the classic 16-color PC palette) so wasn't too fussed about the lack of output pins.
It turned out to be pretty simple; you just have to program the video PLL for the pixel clock, plug the timing values into the LCDIF registers and it takes care of generating VSYNC, HSYNC and outputting pixel values. The sketch ended up looking like this:
It's a bit dirty because I ended up abandoning the idea of using LCDIF; it has too many limitations such as not allowing arbitrary strides for the framebuffers, and 8bpp is the smallest input length which limits the maximum resolution (due to memory restrictions*). But since I had to go to the trouble of typing out all the LCDIF_ definitions, I figure it might save someone else some time. Note that there are 5 possible resolutions selectable by #defining timing to one of the vga_timing structs - the 800x600x100Hz mode probably won't work on most LCDs but does work on my VGA CRTs.
(* using EXTMEM/PSRAM for framebuffers does not work, it is too slow to keep up with the pixel clock.)
It turned out to be pretty simple; you just have to program the video PLL for the pixel clock, plug the timing values into the LCDIF registers and it takes care of generating VSYNC, HSYNC and outputting pixel values. The sketch ended up looking like this:
Code:
#define LCDIF_CTRL_SFTRST ((uint32_t)1 << 31)
#define LCDIF_CTRL_CLKGATE ((uint32_t)1 << 30)
#define LCDIF_CTRL_BYPASS_COUNT ((uint32_t)1 << 19)
#define LCDIF_CTRL_DOTCLK_MODE ((uint32_t)1 << 17)
#define LCDIF_CTRL_LCD_DATABUS_WIDTH(n) ((uint32_t)(((n) & 0x3) << 10))
#define LCDIF_CTRL_WORD_LENGTH(n) ((uint32_t)(((n) & 0x3) << 8))
#define LCDIF_CTRL_MASTER ((uint32_t)1 << 5)
#define LCDIF_CTRL_RUN ((uint32_t)1 << 0)
#define LCDIF_CTRL1_IMAGE_DATA_SELECT ((uint32_t)1 << 31)
#define LCDIF_CTRL1_CS_OUT_SELECT ((uint32_t)1 << 30)
#define LCDIF_CTRL1_BM_ERROR_IRQ_EN ((uint32_t)1 << 26)
#define LCDIF_CTRL1_BM_ERROR_IRQ ((uint32_t)1 << 25)
#define LCDIF_CTRL1_RECOVER_ON_UNDERFLOW ((uint32_t)1 << 24)
#define LCDIF_CTRL1_INTERLACE_FIELDS ((uint32_t)1 << 23)
#define LCDIF_CTRL1_START_INTERLACE_FROM_SECOND_FIELD ((uint32_t)1 << 22)
#define LCDIF_CTRL1_FIFO_CLEAR ((uint32_t)1 << 21)
#define LCDIF_CTRL1_IRQ_ON_ALTERNATE_FIELDS ((uint32_t)1 << 20)
#define LCDIF_CTRL1_BYTE_PACKING_FORMAT(n) ((uint32_t)(((n) & 0xF) << 16))
#define LCDIF_CTRL1_OVERFLOW_IRQ_EN ((uint32_t)1 << 15)
#define LCDIF_CTRL1_UNDERFLOW_IRQ_EN ((uint32_t)1 << 14)
#define LCDIF_CTRL1_CUR_FRAME_DONE_IRQ_EN ((uint32_t)1 << 13)
#define LCDIF_CTRL1_VSYNC_EDGE_IRQ_EN ((uint32_t)1 << 12)
#define LCDIF_CTRL1_OVERFLOW_IRQ ((uint32_t)1 << 11)
#define LCDIF_CTRL1_UNDERFLOW_IRQ ((uint32_t)1 << 10)
#define LCDIF_CTRL1_CUR_FRAME_DONE_IRQ ((uint32_t)1 << 9)
#define LCDIF_CTRL1_VSYNC_EDGE_IRQ ((uint32_t)1 << 8)
#define LCDIF_TRANSFER_COUNT_V_COUNT(n) ((uint32_t)(((n) & 0xFFFF) << 16))
#define LCDIF_TRANSFER_COUNT_H_COUNT(n) ((uint32_t)(((n) & 0xFFFF) << 0))
#define LCDIF_VDCTRL0_ENABLE_PRESENT ((uint32_t)1 << 28)
#define LCDIF_VDCTRL0_VSYNC_POL ((uint32_t)1 << 27)
#define LCDIF_VDCTRL0_HSYNC_POL ((uint32_t)1 << 26)
#define LCDIF_VDCTRL0_DOTCLK_POL ((uint32_t)1 << 25)
#define LCDIF_VDCTRL0_ENABLE_POL ((uint32_t)1 << 24)
#define LCDIF_VDCTRL0_VSYNC_PERIOD_UNIT ((uint32_t)1 << 21)
#define LCDIF_VDCTRL0_VSYNC_PULSE_WIDTH_UNIT ((uint32_t)1 << 20)
#define LCDIF_VDCTRL0_HALF_LINE ((uint32_t)1 << 19)
#define LCDIF_VDCTRL0_HALF_LINE_MODE ((uint32_t)1 << 18)
#define LCDIF_VDCTRL0_VSYNC_PULSE_WIDTH(n) ((uint32_t)(((n) & 0x3FFFF) << 0))
#define LCDIF_VDCTRL2_HSYNC_PULSE_WIDTH(n) ((uint32_t)(((n) & 0x3FFF) << 18))
#define LCDIF_VDCTRL2_HSYNC_PERIOD(n) ((uint32_t)(((n) & 0x3FFFF) << 0))
#define LCDIF_VDCTRL3_MUX_SYNC_SIGNALS ((uint32_t)1 << 29)
#define LCDIF_VDCTRL3_VSYNC_ONLY ((uint32_t)1 << 28)
#define LCDIF_VDCTRL3_HORIZONTAL_WAIT_CNT(n) ((uint32_t)(((n) & 0xFFF) << 16))
#define LCDIF_VDCTRL3_VERTICAL_WAIT_CNT(n) ((uint32_t)(((n) & 0xFFFF) << 0))
#define LCDIF_VDCTRL4_DOTCLK_DLY_SEL(n) ((uint32_t)(((n) & 0x7) << 29))
#define LCDIF_VDCTRL4_SYNC_SIGNALS_ON ((uint32_t)1 << 18)
#define LCDIF_VDCTRL4_DOTCLK_H_VALID_DATA_CNT(n) ((uint32_t)(((n) & 0xFFFF) << 0))
#define IMXRT_LCDIF_LUT (*(IMXRT_REGISTER32_t *)(IMXRT_LCDIF_ADDRESS+0xb00))
#define LCDIF_LUT_CTRL (IMXRT_LCDIF_LUT.offset000)
#define LCDIF_LUT0_ADDR (IMXRT_LCDIF_LUT.offset010)
#define LCDIF_LUT0_DATA (IMXRT_LCDIF_LUT.offset020)
#define LCDIF_LUT1_ADDR (IMXRT_LCDIF_LUT.offset030)
#define LCDIF_LUT1_DATA (IMXRT_LCDIF_LUT.offset040)
typedef struct {
uint32_t height;
uint32_t vfp; // vertical front porch
uint32_t vsw; // vertical sync width
uint32_t vbp; // vertical back porch
uint32_t width;
uint32_t hfp; // horizontal front porch
uint32_t hsw; // horizontal sync width
uint32_t hbp; // horizontal back porch
// clk_num * 24MHz / clk_den = pixel clock
uint32_t clk_num; // pix_clk numerator
uint32_t clk_den; // pix_clk denominator
uint32_t vpolarity; // 0 (active low vsync/negative) or LCDIF_VDCTRL0_VSYNC_POL (active high/positive)
uint32_t hpolarity; // 0 (active low hsync/negative) or LCDIF_VDCTRL0_HSYNC_POL (active high/positive)
} vga_timing;
const vga_timing t800x600x100 = {600, 1, 3, 32, 800, 48, 88, 136, 6818, 2400, LCDIF_VDCTRL0_VSYNC_POL, 0};
const vga_timing t800x600x60 = {600, 1, 4, 23, 800, 40, 128, 88, 40, 24, LCDIF_VDCTRL0_VSYNC_POL, LCDIF_VDCTRL0_HSYNC_POL};
const vga_timing t640x480x60 = {480, 10, 2, 33, 640, 16, 96, 48, 150, 143, 0, 0};
const vga_timing t640x400x70 = {400, 12, 2, 35, 640, 16, 96, 48, 150, 143, LCDIF_VDCTRL0_VSYNC_POL, 0};
const vga_timing t640x350x70 = {350, 37, 2, 60, 640, 16, 96, 48, 150, 143, 0, LCDIF_VDCTRL0_HSYNC_POL};
// select desired mode here
#define timing t640x480x60
// PIN OUTPUTS FOR TEENSY 4.1
// red/green/blue: two-thirds of 0.7V
#define VGA_RED 6
#define VGA_BLUE 9
#define VGA_GREEN 32
// intensity: one-third of 0.7V
#define VGA_INTENSITY_RB 7
#define VGA_INTENSITY_G 8
// sync signals = TTL
#define VGA_HSYNC 11 // connect sync signals to VGA via 68R
#define VGA_VSYNC 13
/* R2R ladder:
*
* GROUND <------------- 536R ----*---- 270R ---*-----------> VGA PIN (1/2/3)
* | |
* INTENSITY (7/8/7) <---536R ----/ |
* |
* COLOR (6/32/9) <-----536R-------------------/
*/
// defined using max dimensions due to laziness
// LCDIF framebuffers must be 64-byte aligned
__attribute__((aligned(64))) static uint8_t frameBuffer0[800*600];
// not enough space to have both buffers in RAM1 - second buffer is in RAM2, cache must be flushed after writing it
DMAMEM __attribute__((aligned(64))) static uint8_t frameBuffer1[800*600];
static uint8_t* s_frameBuffer[2] = {frameBuffer0, frameBuffer1};
static volatile bool s_frameDone = false;
static void LCDIF_ISR(void) {
uint32_t intStatus = LCDIF_CTRL1 & (LCDIF_CTRL1_BM_ERROR_IRQ | LCDIF_CTRL1_OVERFLOW_IRQ | LCDIF_CTRL1_UNDERFLOW_IRQ | LCDIF_CTRL1_CUR_FRAME_DONE_IRQ | LCDIF_CTRL1_VSYNC_EDGE_IRQ);
// clear all pending LCD interrupts
LCDIF_CTRL1_CLR = intStatus;
if (intStatus & (LCDIF_CTRL1_CUR_FRAME_DONE_IRQ | LCDIF_CTRL1_VSYNC_EDGE_IRQ)) {
s_frameDone = true;
}
asm volatile("dsb");
}
// num,den = desired pix_clk as a ratio of 24MHz
FLASHMEM static void set_vid_clk(int num, int den) {
int post_divide = 0;
while (num < 27*den) num <<= 1, ++post_divide;
int div_select = num / den;
num -= div_select * den;
// div_select valid range: 27-54
float freq = ((float)num / den + div_select) * 24.0f / (1 << post_divide);
Serial.print("VID_PLL: ");
Serial.print(freq);
Serial.print("Mhz, div_select: ");
Serial.println(div_select);
// switch video PLL to bypass, enable, set div_select
CCM_ANALOG_PLL_VIDEO = CCM_ANALOG_PLL_VIDEO_BYPASS | CCM_ANALOG_PLL_VIDEO_ENABLE | CCM_ANALOG_PLL_VIDEO_DIV_SELECT(div_select);
// clear misc2 vid post-divider
CCM_ANALOG_MISC2_CLR = CCM_ANALOG_MISC2_VIDEO_DIV(3);
switch (post_divide) {
case 0: // div by 1
CCM_ANALOG_PLL_VIDEO_SET = CCM_ANALOG_PLL_VIDEO_POST_DIV_SELECT(2);
break;
case 1: // div by 2
CCM_ANALOG_PLL_VIDEO_SET = CCM_ANALOG_PLL_VIDEO_POST_DIV_SELECT(1);
break;
// div by 4
// case 2: PLL_VIDEO pos_div_select already set to 0
case 3: // div by 8 (4*2)
CCM_ANALOG_MISC2_SET = CCM_ANALOG_MISC2_VIDEO_DIV(1);
break;
case 4: // div by 16 (4*4)
CCM_ANALOG_MISC2_SET = CCM_ANALOG_MISC2_VIDEO_DIV(3);
break;
}
CCM_ANALOG_PLL_VIDEO_NUM = num;
CCM_ANALOG_PLL_VIDEO_DENOM = den;
// ensure PLL is powered
CCM_ANALOG_PLL_VIDEO_CLR = CCM_ANALOG_PLL_VIDEO_POWERDOWN;
// wait for lock
Serial.print("Waiting for PLL Lock...");
while (!(CCM_ANALOG_PLL_VIDEO & CCM_ANALOG_PLL_VIDEO_LOCK));
// deactivate bypass
CCM_ANALOG_PLL_VIDEO_CLR = CCM_ANALOG_PLL_VIDEO_BYPASS;
Serial.println("done.");
Serial.print("Configuring LCD pix_clk source...");
// gate clocks from lcd
CCM_CCGR2 &= ~CCM_CCGR2_LCD(CCM_CCGR_ON);
CCM_CCGR3 &= ~CCM_CCGR3_LCDIF_PIX(CCM_CCGR_ON);
// set LCDIF source to PLL5, pre-divide by 4
uint32_t r = CCM_CSCDR2;
r &= ~(CCM_CSCDR2_LCDIF_PRE_CLK_SEL(7) | CCM_CSCDR2_LCDIF_PRED(7));
r |= CCM_CSCDR2_LCDIF_PRE_CLK_SEL(2) | CCM_CSCDR2_LCDIF_PRED(3);
CCM_CSCDR2 = r;
// set LCDIF post-divide to 1
CCM_CBCMR &= ~CCM_CBCMR_LCDIF_PODF(7);
CCM_CCGR2 |= CCM_CCGR2_LCD(CCM_CCGR_ON);
CCM_CCGR3 |= CCM_CCGR3_LCDIF_PIX(CCM_CCGR_ON);
Serial.println("done.");
}
FLASHMEM static void init_lcd(const vga_timing* vid) {
// mux pins for LCD module. We don't care about ENABLE or DOTCLK for VGA.
Serial.println("Setting pins");
*(portConfigRegister(VGA_RED)) = 0;
*(portConfigRegister(VGA_GREEN)) = 0;
*(portConfigRegister(VGA_BLUE)) = 0;
*(portConfigRegister(VGA_VSYNC)) = 0;
*(portConfigRegister(VGA_HSYNC)) = 0;
*(portConfigRegister(VGA_INTENSITY_RB)) = 0;
*(portConfigRegister(VGA_INTENSITY_G)) = 0;
Serial.print("Resetting LCDIF...");
// reset LCDIF
// ungate clock and wait for it to clear
LCDIF_CTRL_CLR = LCDIF_CTRL_CLKGATE;
while (LCDIF_CTRL & LCDIF_CTRL_CLKGATE);
Serial.print("poking reset...");
/* trigger reset, wait for clock gate to enable - this is what the manual says to do...
* but it doesn't work; the clock gate never re-activates, at least not in the register
* so the best we can do is to make sure the reset flag is reflected and assume it's done the job
*/
LCDIF_CTRL_SET = LCDIF_CTRL_SFTRST;
while (!(LCDIF_CTRL & LCDIF_CTRL_SFTRST));
Serial.print("re-enabling clock...");
// clear reset and ungate clock again
LCDIF_CTRL_CLR = LCDIF_CTRL_SFTRST | LCDIF_CTRL_CLKGATE;
Serial.println("done.");
Serial.print("Initializing LCDIF registers...");
// 8 bits in, using LUT
LCDIF_CTRL = LCDIF_CTRL_WORD_LENGTH(1) | LCDIF_CTRL_LCD_DATABUS_WIDTH(1) | LCDIF_CTRL_DOTCLK_MODE | LCDIF_CTRL_BYPASS_COUNT | LCDIF_CTRL_MASTER;
// recover on underflow = garbage will be displayed if memory is too slow, but at least it keeps running instead of aborting
LCDIF_CTRL1 = LCDIF_CTRL1_RECOVER_ON_UNDERFLOW | LCDIF_CTRL1_BYTE_PACKING_FORMAT(15);
LCDIF_TRANSFER_COUNT = LCDIF_TRANSFER_COUNT_V_COUNT(vid->height) | LCDIF_TRANSFER_COUNT_H_COUNT(vid->width);
// set vsync and hsync signal polarity (depends on mode/resolution), vsync length
LCDIF_VDCTRL0 = LCDIF_VDCTRL0_ENABLE_PRESENT | LCDIF_VDCTRL0_VSYNC_PERIOD_UNIT | LCDIF_VDCTRL0_VSYNC_PULSE_WIDTH_UNIT | LCDIF_VDCTRL0_VSYNC_PULSE_WIDTH(vid->vsw) | vid->vpolarity | vid->hpolarity;
// total lines
LCDIF_VDCTRL1 = vid->height+vid->vfp+vid->vsw+vid->vbp;
// hsync length, line = width+HBP+HSW+HFP
LCDIF_VDCTRL2 = LCDIF_VDCTRL2_HSYNC_PULSE_WIDTH(vid->hsw) | LCDIF_VDCTRL2_HSYNC_PERIOD(vid->width+vid->hfp+vid->hsw+vid->hbp);
// horizontal wait = back porch + sync, vertical wait = back porch + sync
LCDIF_VDCTRL3 = LCDIF_VDCTRL3_HORIZONTAL_WAIT_CNT(vid->hsw+vid->hbp) | LCDIF_VDCTRL3_VERTICAL_WAIT_CNT(vid->vsw+vid->vbp);
LCDIF_VDCTRL4 = LCDIF_VDCTRL4_SYNC_SIGNALS_ON | LCDIF_VDCTRL4_DOTCLK_H_VALID_DATA_CNT(vid->width);
Serial.println("done.");
}
static void InitLUT(void) {
/* index 6 in the "classic" 16-color palette should be brown instead of dark yellow.
* The values of green and green-intensity are swapped for this entry to accomplish this
*/
// bits in palette entries match the LCD_DATA* signals
static const uint32_t red = 1 << 6; // red = pin 6
static const uint32_t green = 1 << 8; // green = pin 32
static const uint32_t blue = 1 << 7; // blue = pin 9
static const uint32_t intensity_g = 1 << 12; // intensity_g = pin 8
static const uint32_t intensity = (1 << 13) | intensity_g; // intensity_rb = pin 7
/* index 0 is output during blanking period!
* The CRT monitor uses that time to measure black levels, if color
* lines are active their voltage levels will be sampled as the
* new ground/black reference.
* tl,dr: index 0 should ALWAYS be black.
*/
static const uint32_t fgColorTable[16] = {0, blue, green, blue|green, red, red|blue, red|intensity_g, red|green|blue,
intensity, intensity|blue, intensity|green, intensity|blue|green, intensity|red, intensity|red|blue, intensity|red|green, intensity|red|green|blue};
// clear LUT1, just to ensure we don't accidentally end up using it
// (LUT1 is used when the lowest bit of the framebuffer address is set)
LCDIF_LUT0_ADDR = 0;
LCDIF_LUT1_ADDR = 0;
for (size_t i=0; i < (sizeof(fgColorTable)/sizeof(fgColorTable[0])); i++) {
LCDIF_LUT0_DATA = fgColorTable[i];
LCDIF_LUT1_DATA = 0;
}
// activate LUT
LCDIF_LUT_CTRL = 0;
}
// draws scrolling colorbars
static void FillFrameBuffer(uint8_t *fb) {
static uint16_t xoff=0;
static uint16_t yoff=0;
uint32_t x,y;
for (y = 0; y < timing.height; y++) {
for (x = 0; x < timing.width; x++) {
fb[y*timing.width+x] = (((x+xoff) / (timing.width / 8)) & 7) | ((((y+yoff) / (timing.height / 2)) << 3) & 8);
}
}
if (++xoff >= timing.width)
xoff = 0;
if (++yoff >= timing.height)
yoff = 0;
}
void setup() {
Serial.begin(9600);
set_vid_clk(4*timing.clk_num,timing.clk_den);
init_lcd(&timing);
LCDIF_CUR_BUF = (uint32_t)s_frameBuffer[1];
LCDIF_NEXT_BUF = (uint32_t)s_frameBuffer[0];
Serial.println("Enabling LCDIF interrupt");
attachInterruptVector(IRQ_LCDIF, LCDIF_ISR);
NVIC_SET_PRIORITY(IRQ_LCDIF, 32);
NVIC_ENABLE_IRQ(IRQ_LCDIF);
InitLUT();
FillFrameBuffer(s_frameBuffer[0]);
FillFrameBuffer(s_frameBuffer[1]);
arm_dcache_flush_delete(s_frameBuffer[1], timing.height*timing.width);
Serial.println("Unmasking frame interrupt");
// unmask CUR_FRAME_DONE interrupt
LCDIF_CTRL1_SET = LCDIF_CTRL1_CUR_FRAME_DONE_IRQ_EN;
// VSYNC_EDGE interrupt also available to notify beginning of raster
//LCDIF_CTRL1_SET = LCDIF_CTRL1_VSYNC_EDGE_IRQ_EN;
Serial.println("Running LCD");
// start LCD
LCDIF_CTRL_SET = LCDIF_CTRL_RUN | LCDIF_CTRL_DOTCLK_MODE;
}
void loop() {
static uint32_t nextBufferIndex;
if (s_frameDone) {
s_frameDone = false;
nextBufferIndex ^= 1;
// this is the frame that just finished, redraw it
FillFrameBuffer(s_frameBuffer[nextBufferIndex]);
if (nextBufferIndex)
arm_dcache_flush_delete(s_frameBuffer[1], timing.height*timing.width);
// queue for display
LCDIF_NEXT_BUF = (uint32_t)s_frameBuffer[nextBufferIndex];
}
}
It's a bit dirty because I ended up abandoning the idea of using LCDIF; it has too many limitations such as not allowing arbitrary strides for the framebuffers, and 8bpp is the smallest input length which limits the maximum resolution (due to memory restrictions*). But since I had to go to the trouble of typing out all the LCDIF_ definitions, I figure it might save someone else some time. Note that there are 5 possible resolutions selectable by #defining timing to one of the vga_timing structs - the 800x600x100Hz mode probably won't work on most LCDs but does work on my VGA CRTs.
(* using EXTMEM/PSRAM for framebuffers does not work, it is too slow to keep up with the pixel clock.)