VGA output via FlexIO (Teensy4)

I'm laying out a prototype board and thought I'd make sure.
I don't want to dissuade you, but I may be working on something more "modern" than VGA...
Code:
06:08:52.641 -> FL2000<0x20004c80>: HDMI VendorID: 4954 DeviceID: 1612
06:08:53.782 -> FL2000<0x20004c80>: ITE chip powered up
06:08:53.782 -> FL2000<0x20004c80>: Monitor is not connected
06:08:53.782 -> FL2000<0x20004c80>: Ignoring MonitorPlugout event
06:08:54.334 -> FL2000<0x20004c80>: Interrupt result: 1
06:08:54.334 -> FL2000<0x20004c80>: Interrupt!
06:08:54.334 -> FL2000<0x20004c80>: Monitor is connected
06:08:54.334 -> FL2000<0x20004c80>: External monitor status: 1
06:08:54.334 -> FL2000<0x20004c80>: EDID status: 0
06:08:57.971 -> EDID DUMP:
06:08:57.971 -> 00 FF FF FF FF FF FF 00 4C 2D 1A 0C 55 46 5A 5A
06:08:57.971 -> 05 1C 01 03 80 34 1D 78 2A 97 91 A5 56 54 9D 25
06:08:57.971 -> 0E 50 54 BF EF 80 71 4F 81 C0 81 00 81 80 95 00
06:08:57.971 -> A9 C0 B3 00 01 01 02 3A 80 18 71 38 2D 40 58 2C
06:08:57.971 -> 45 00 09 25 21 00 00 1E 01 1D 00 72 51 D0 1E 20
06:08:57.971 -> 6E 28 55 00 09 25 21 00 00 1E 00 00 00 FD 00 32
06:08:57.971 -> 4B 1E 51 11 00 0A 20 20 20 20 20 20 00 00 00 FC
06:08:57.971 -> 00 53 32 34 45 33 39 30 0A 20 20 20 20 20 01 C5
06:09:01.638 -> 02 03 1A F1 46 90 04 1F 13 12 03 23 09 07 07 83
06:09:01.638 -> 01 00 00 66 03 0C 00 10 00 80 01 1D 00 BC 52 D0
06:09:01.638 -> 1E 20 B8 28 55 40 09 25 21 00 00 1E 8C 0A D0 90
06:09:01.638 -> 20 40 31 20 0C 40 55 00 09 25 21 00 00 18 8C 0A
06:09:01.638 -> D0 8A 20 E0 2D 10 10 3E 96 00 09 25 21 00 00 18
06:09:01.638 -> 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
06:09:01.638 -> 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
06:09:01.638 -> 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 C9
06:09:01.638 -> FL2000<0x20004c80>: 1 EDID extensions found
 
Well the target is an emulated Z80, so resources are (artificially) tight. And, lol, somewhat moot, as the board is at JLCPCB. But I assume there will be revisions. I'm doing the VGA work on the Teensy "side" of the emulation, not in the Z80 address space. But oooh HDMI! What's the hardware look like?

Yeah VGA is nearly vestigial now. Beats a teletype though. HDMI would be win, if I can get rez low enough!
 
It's a Fresco Logic 2000DX HDMI dongle. They're designed for USB3 and only 640x480 or 800x600 are usable over USB2 due to bandwidth limitations. Unfortunately, even though the dongle uses an ITE 66121 HDMI encoder there isn't any way to send audio to it through the Fresco Logic chip...
 
Oh my. I put my USB2 into a USB-C jack, it was meant for keyboard only but will test it and see what else I need to do for USB3. I will look at that device! Thanks!
 
Progress:
IMG_20250114_132049599.jpg


800x600, 8-bit (256 color customizable palette of RGB24) @ 60Hz over HDMI.
 
Fantastic work @jmarsh!!
Will it support 16 or 24 bit color depth as well as similar or smaller resolutions?
16bpp is possible at 640x480 but compression is required which can degrade the quality.
The limit is the USB bandwidth which maxes out at roughly 30MB/s. 60 frames a second of 640x480 using 16bpp = 60*640*480*2 = ~37MB/s. That's why these adapters are intended to be used with USB3 hosts with higher bandwidth.
Memory is also an issue, a 16bpp framebuffer for 640x480 is ~600KB which is too large for the default Teensy4 memory. EXTMEM/PSRAM cannot be used because it's too slow for USB streaming.

8bpp paletted mode uses 24bpp colors, you're just limited to only using 256 individual colors in each frame.
 
16bpp is possible at 640x480 but compression is required which can degrade the quality.
The limit is the USB bandwidth which maxes out at roughly 30MB/s. 60 frames a second of 640x480 using 16bpp = 60*640*480*2 = ~37MB/s. That's why these adapters are intended to be used with USB3 hosts with higher bandwidth.
Memory is also an issue, a 16bpp framebuffer for 640x480 is ~600KB which is too large for the default Teensy4 memory. EXTMEM/PSRAM cannot be used because it's too slow for USB streaming.

8bpp paletted mode uses 24bpp colors, you're just limited to only using 256 individual colors in each frame.
Awesome work jmarsh! What about SDRAM at 227MHz for framebuffer, still to slow? It would be pretty wild to take this concept and make a board. With the USB to HDMI "thingy" built onto the board. And use USB Host instead so that the ordinary USB is free for flashing and such.
 
Awesome work jmarsh! What about SDRAM at 227MHz for framebuffer, still to slow? It would be pretty wild to take this concept and make a board. With the USB to HDMI "thingy" built onto the board. And use USB Host instead so that the ordinary USB is free for flashing and such.
SDRAM just means you can have spare render buffers besides the active one, it doesn't really enable more resolutions or bitdepths because the USB bandwidth is still the limiting factor.

If you wanted to integrate something like this on a board it would be better to interface the IMXRT with an ITE 66121 directly (the FL2000DX chip is just the middleman, reading pixel data over USB2/3 and passing it to the ITE HDMI encoder) then you could drive it with eLCDIF and also push I2S/SPDIF audio over the HDMI cable. Maybe you could find some stock of FL2000DX chips if you really wanted to do it, but the company (Fresco Logic) was acquired by someone else and they don't seem to support the product any more, despite there still being hundreds of units available on ebay and amazon for less than $10 and lots of people trying to track them down to use as SDR transmitters.

That would also be a lot cleaner because the FL2000 seems to have a hardware bug that requires swapping each pair of uint32_ts before sending them over USB, so if you want decent performance you have to take that into account when writing to the framebuffer (target pixel[x^4][y] instead of pixel[x][y]).

I am thinking of cracking mine open, because supposedly there's some unpopulated pads on the board for an SPI flash that becomes accessible as a USB mass storage device on an alternate interface when present. It would be handy to have a small flash drive available without needing to plug in a hub, and I'm pretty sure I know which registers to poke to make it writable - by default it's read-only because it's intended to hold the windows drivers for the device.
 
Last edited:
Even though it took using all 8 timer registers, it turns out it is possible to use one FlexIO module (FlexIO2 in this case) to generate VSYNC, HSYNC and drive 4 data lines to get 16-color VGA output. All the framebuffer data is fetched using DMA (one channel for regular mode, three channels for line-doubling) leaving the CPU free for other tasks. Mono/1bpp mode is also supported if low memory usage is required, using two shift buffers as state registers to broadcast the single pixel bit to all four output pins.

Code:
#include <DMAChannel.h>

/* R2R ladder:
 *
 * GROUND <------------- 536R ----*---- 270R ---*-----------> VGA PIN: R=1/G=2/B=3
 *                                |             |
 * INTENSITY (13) <---536R -------/             |
 *                                              |
 * COLOR: R=11/G=12/B=10  <-----536R------------/
 *
 * VSYNC (34) <---------------68R---------------------------> VGA PIN 14
 *
 * HSYNC (35) <---------------68R---------------------------> VGA PIN 13
 */

// horizontal values must be divisible by 8 for correct operation
typedef struct {
  uint32_t height;
  uint32_t vfp;
  uint32_t vsw;
  uint32_t vbp;
  uint32_t width;
  uint32_t hfp;
  uint32_t hsw;
  uint32_t hbp;
  uint32_t clk_num;
  uint32_t clk_den;
  // sync polarities: 0 = active high, 1 = active low
  uint32_t vsync_pol;
  uint32_t hsync_pol;
} vga_timing;

class FlexIO2VGA {
public:
  FlexIO2VGA(const vga_timing& mode, bool half_height=false, bool half_width=false, unsigned int bpp=4);
  void stop(void);

  // wait parameter:
  // TRUE =  wait until previous frame ends and source is "current"
  // FALSE = queue it as the next framebuffer to draw, return immediately
  void set_next_buffer(const void* source, size_t pitch, bool wait);

  void wait_for_frame(void) {
    unsigned int count = frameCount;
    while (count == frameCount)
      yield();
  }
private:
  void set_clk(int num, int den);
  static void ISR(void);
  void TimerInterrupt(void);

  uint8_t dma_chans[2];
  DMAChannel dma1,dma2,dmaswitcher;
  DMASetting dma_params;

  bool double_height;
  int32_t widthxbpp;
 
  volatile unsigned int frameCount;
};

FLASHMEM FlexIO2VGA::FlexIO2VGA(const vga_timing& mode, bool half_height, bool half_width, unsigned int bpp) {
  frameCount = 0;
  *(portConfigRegister(11)) = 4; // FLEXIO2_D2    RED
  *(portConfigRegister(12)) = 4; // FLEXIO2_D1    GREEN
  *(portConfigRegister(10)) = 4; // FLEXIO2_D0    BLUE
  *(portConfigRegister(13)) = 4; // FLEXIO2_D3    INTENSITY
  *(portConfigRegister(34)) = 4; // FLEXIO2_D29   VSYNC
  *(portConfigRegister(35)) = 4; // FLEXIO2_D30   HSYNC

  dma_chans[0] = dma2.channel;
  dma_chans[1] = dma1.channel;

  memset(dma_params.TCD, 0, sizeof(*dma_params.TCD));
  dma_params.TCD->DOFF = 4;
  dma_params.TCD->ATTR = DMA_TCD_ATTR_DMOD(3) | DMA_TCD_ATTR_DSIZE(2);
  dma_params.TCD->NBYTES = 8;
  dma_params.TCD->DADDR = &FLEXIO2_SHIFTBUF0;
  dma1.triggerAtHardwareEvent(DMAMUX_SOURCE_FLEXIO2_REQUEST0);
  dma2.triggerAtHardwareEvent(DMAMUX_SOURCE_FLEXIO2_REQUEST0);

  dmaswitcher.TCD->SADDR = dma_chans;
  dmaswitcher.TCD->SOFF = 1;
  dmaswitcher.TCD->DADDR = &DMA_SERQ;
  dmaswitcher.TCD->DOFF = 0;
  dmaswitcher.TCD->ATTR = DMA_TCD_ATTR_SMOD(1);
  dmaswitcher.TCD->NBYTES = 1;
  dmaswitcher.TCD->BITER = dmaswitcher.TCD->CITER = 1;

  double_height = half_height;
  widthxbpp = (mode.width * bpp) / (half_width ? 2 : 1);

  set_clk(4*mode.clk_num, mode.clk_den);

  FLEXIO2_CTRL = FLEXIO_CTRL_SWRST;
  asm volatile("dsb");
  FLEXIO2_CTRL = FLEXIO_CTRL_FASTACC | FLEXIO_CTRL_FLEXEN;
  // wait for reset to clear
  while (FLEXIO2_CTRL & FLEXIO_CTRL_SWRST);

  // timer 0: divide pixel clock by 8
  FLEXIO2_TIMCFG0 = 0;
  FLEXIO2_TIMCMP0 = (4*8)-1;
 
  // timer 1: generate HSYNC
  FLEXIO2_TIMCFG1 = FLEXIO_TIMCFG_TIMDEC(1);
  // on = HSW, off = rest of line
  FLEXIO2_TIMCMP1 = ((((mode.width+mode.hbp+mode.hfp)/8)-1)<<8) | ((mode.hsw/8)-1);
  // trigger = timer0, HSYNC=D28
  FLEXIO2_TIMCTL1 = FLEXIO_TIMCTL_TRGSEL(4*0+3) | FLEXIO_TIMCTL_TRGSRC | FLEXIO_TIMCTL_PINCFG(3) | FLEXIO_TIMCTL_PINSEL(28) | FLEXIO_TIMCTL_TIMOD(2) | (mode.hsync_pol*FLEXIO_TIMCTL_PINPOL);

  // timer 2: frame counter
  // tick on HSYNC
  FLEXIO2_TIMCFG2 = FLEXIO_TIMCFG_TIMDEC(1);
  FLEXIO2_TIMCMP2 = ((mode.height+mode.vbp+mode.vfp+mode.vsw)*2)-1;
  // trigger = HYSNC pin
  FLEXIO2_TIMCTL2 = FLEXIO_TIMCTL_TRGSEL(2*28) | (mode.hsync_pol * FLEXIO_TIMCTL_TRGPOL) | FLEXIO_TIMCTL_TRGSRC | FLEXIO_TIMCTL_TIMOD(3);

  // timer 3: generate VSYNC
  FLEXIO2_TIMCFG3 = FLEXIO_TIMCFG_TIMDIS(2) | FLEXIO_TIMCFG_TIMENA(7);
  // active for VSW lines. 4*total horizontal pixels*vertical sync loength must be <= 65536 to not overflow this timer
  FLEXIO2_TIMCMP3 = (4*mode.vsw*(mode.width+mode.hbp+mode.hsw+mode.hfp))-1;
  // trigger = frame counter, VSYNC=D29
  FLEXIO2_TIMCTL3 = FLEXIO_TIMCTL_TRGSEL(4*2+3) | FLEXIO_TIMCTL_TRGSRC | FLEXIO_TIMCTL_PINCFG(3) | FLEXIO_TIMCTL_PINSEL(29) | FLEXIO_TIMCTL_TIMOD(3) | (mode.vsync_pol*FLEXIO_TIMCTL_PINPOL);

  // timer4: count VSYNC and back porch
  // enable on VSYNC start, disable after (VSW+VBP)*2 edges of HSYNC
  FLEXIO2_TIMCFG4 = FLEXIO_TIMCFG_TIMDEC(2) | FLEXIO_TIMCFG_TIMDIS(2) | FLEXIO_TIMCFG_TIMENA(6);
  FLEXIO2_TIMCMP4 = ((mode.vsw+mode.vbp)*2)-1;
  // trigger = VSYNC pin, pin = HSYNC
  FLEXIO2_TIMCTL4 = FLEXIO_TIMCTL_TRGSEL(2*29) | FLEXIO_TIMCTL_TRGSRC | FLEXIO_TIMCTL_PINSEL(28) | FLEXIO_TIMCTL_TIMOD(3) | (mode.vsync_pol*FLEXIO_TIMCTL_TRGPOL) | (mode.hsync_pol*FLEXIO_TIMCTL_PINPOL);

  // timer 5: vertical active region
  // enable when previous timer finishes, disable after height*2 edges of HSYNC
  FLEXIO2_TIMCFG5 = FLEXIO_TIMCFG_TIMDEC(2) | FLEXIO_TIMCFG_TIMDIS(2) | FLEXIO_TIMCFG_TIMENA(6);
  FLEXIO2_TIMCMP5 = (mode.height*2)-1;
  // trigger = timer4 negative, pin = HSYNC
  FLEXIO2_TIMCTL5 = FLEXIO_TIMCTL_TRGSEL(4*4+3) | FLEXIO_TIMCTL_TRGPOL | FLEXIO_TIMCTL_TRGSRC | FLEXIO_TIMCTL_PINSEL(28) | FLEXIO_TIMCTL_TIMOD(3) | (mode.vsync_pol*FLEXIO_TIMCTL_PINPOL);

  // timer 6: horizontal active region
  // configured as PWM: OFF for HSYNC+HBP, ON for active region, reset (to off state) when HSYNC occurs (off state covers HFP then resets)
  FLEXIO2_TIMCFG6 = FLEXIO_TIMCFG_TIMOUT(1) | FLEXIO_TIMCFG_TIMDEC(1) | FLEXIO_TIMCFG_TIMRST(4) | FLEXIO_TIMCFG_TIMDIS(1) | FLEXIO_TIMCFG_TIMENA(1);
  FLEXIO2_TIMCMP6 = ((((mode.hsw+mode.hbp)/8)-1)<<8) | ((mode.width/8)-1);
  // trigger = timer0, pin = HSYNC
  FLEXIO2_TIMCTL6 = FLEXIO_TIMCTL_TRGSEL(4*0+3) | FLEXIO_TIMCTL_TRGSRC | FLEXIO_TIMCTL_PINSEL(28) | FLEXIO_TIMCTL_TIMOD(2) | (mode.hsync_pol*FLEXIO_TIMCTL_PINPOL);

  // timer 7: output pixels from shifter, runs only when trigger is ON
  FLEXIO2_TIMCFG7 = FLEXIO_TIMCFG_TIMDIS(6) | FLEXIO_TIMCFG_TIMENA(6) | FLEXIO_TIMCFG_TSTOP(2);
  FLEXIO2_TIMCMP7 = ((((64/bpp)*2)-1)<<8) | ((half_width ? 4:2)-1);
  // trigger = timer 6
  FLEXIO2_TIMCTL7 = FLEXIO_TIMCTL_TRGSEL(4*6+3) | FLEXIO_TIMCTL_TRGSRC | FLEXIO_TIMCTL_TIMOD(1);

  // start blank
  FLEXIO2_SHIFTBUF1 = FLEXIO2_SHIFTBUF2 = 0;
  if (bpp == 4) {
    FLEXIO2_SHIFTCFG1 = FLEXIO_SHIFTCFG_PWIDTH(3);
    FLEXIO2_SHIFTCTL1 = FLEXIO_SHIFTCTL_TIMSEL(7) | FLEXIO_SHIFTCTL_SMOD(2);
    // output stop bit when timer disables - ensures black output/zero outside active window
    FLEXIO2_SHIFTCFG0 = FLEXIO_SHIFTCFG_PWIDTH(3) | FLEXIO_SHIFTCFG_INSRC | FLEXIO_SHIFTCFG_SSTOP(2);
    FLEXIO2_SHIFTCTL0 = FLEXIO_SHIFTCTL_TIMSEL(7) | FLEXIO_SHIFTCTL_PINCFG(3) | FLEXIO_SHIFTCTL_PINSEL(0) | FLEXIO_SHIFTCTL_SMOD(2);
   
    FLEXIO2_SHIFTSTATE = 0;
  } else { // bpp==1
    FLEXIO2_SHIFTCFG1 = FLEXIO_SHIFTCFG_PWIDTH(0);
    FLEXIO2_SHIFTCTL1 = FLEXIO_SHIFTCTL_TIMSEL(7) | FLEXIO_SHIFTCTL_SMOD(2);
    FLEXIO2_SHIFTCFG0 = FLEXIO_SHIFTCFG_PWIDTH(0) | FLEXIO_SHIFTCFG_INSRC | FLEXIO_SHIFTCFG_SSTOP(2);
    FLEXIO2_SHIFTCTL0 = FLEXIO_SHIFTCTL_TIMSEL(7)  | FLEXIO_SHIFTCTL_PINCFG(3) | FLEXIO_SHIFTCTL_PINSEL(8) | FLEXIO_SHIFTCTL_SMOD(2);

    // D8 clear = use state 2, D8 set = use state 3
    // note that PWIDTH does not seem to mask D4-7 outputs as documented!
    FLEXIO2_SHIFTBUF2 = 0x0069A69A;
    FLEXIO2_SHIFTCFG2 = FLEXIO_SHIFTCFG_PWIDTH(15);
    FLEXIO2_SHIFTCTL2 = FLEXIO_SHIFTCTL_TIMSEL(7) | FLEXIO_SHIFTCTL_PINCFG(3) | FLEXIO_SHIFTCTL_PINSEL(8) | FLEXIO_SHIFTCTL_SMOD(6) | FLEXIO_SHIFTCTL_TIMPOL;
    FLEXIO2_SHIFTBUF3 = 0x0F69A69A;
    FLEXIO2_SHIFTCFG3 = FLEXIO_SHIFTCFG_PWIDTH(15);
    FLEXIO2_SHIFTCTL3 = FLEXIO_SHIFTCTL_TIMSEL(7) | FLEXIO_SHIFTCTL_PINCFG(3) | FLEXIO_SHIFTCTL_PINSEL(8) | FLEXIO_SHIFTCTL_SMOD(6) | FLEXIO_SHIFTCTL_TIMPOL;

    FLEXIO2_SHIFTSTATE = 2;
  }

  // clear timer 5 status
  FLEXIO2_TIMSTAT = 1<<5;
  // make sure no other FlexIO interrupts are enabled
  FLEXIO2_SHIFTSIEN = 0;
  FLEXIO2_SHIFTEIEN = 0;
  // enable timer 5 interrupt
  FLEXIO2_TIMIEN = 1<<5;

  attachInterruptVector(IRQ_FLEXIO2, ISR);
  NVIC_SET_PRIORITY(IRQ_FLEXIO2, 32);
  NVIC_ENABLE_IRQ(IRQ_FLEXIO2);

  // start everything!
  FLEXIO2_TIMCTL0 = FLEXIO_TIMCTL_TIMOD(3);
}

FLASHMEM void FlexIO2VGA::stop(void) {
  NVIC_DISABLE_IRQ(IRQ_FLEXIO2);
  // FlexIO2 registers don't work if they have no clock
  if (CCM_CCGR3 & CCM_CCGR3_FLEXIO2(CCM_CCGR_ON)) {
    FLEXIO2_CTRL &= ~FLEXIO_CTRL_FLEXEN;
    FLEXIO2_TIMIEN = 0;
    FLEXIO2_SHIFTSDEN = 0;
  }
  dma1.disable();
  dma2.disable();
  asm volatile("dsb");
}

FLASHMEM void FlexIO2VGA::set_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;

  // valid range for div_select: 27-54

  // 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
  while (!(CCM_ANALOG_PLL_VIDEO & CCM_ANALOG_PLL_VIDEO_LOCK));
  // deactivate bypass
  CCM_ANALOG_PLL_VIDEO_CLR = CCM_ANALOG_PLL_VIDEO_BYPASS;

  // gate clock
  CCM_CCGR3 &= ~CCM_CCGR3_FLEXIO2(CCM_CCGR_ON);
  // FlexIO2 use vid clock (PLL5)
  uint32_t t = CCM_CSCMR2;
  t &= ~CCM_CSCMR2_FLEXIO2_CLK_SEL(3);
  t |= CCM_CSCMR2_FLEXIO2_CLK_SEL(2);
  CCM_CSCMR2 = t;
  // flex gets 1:1 clock, no dividing
  CCM_CS1CDR &= ~(CCM_CS1CDR_FLEXIO2_CLK_PODF(7) | CCM_CS1CDR_FLEXIO2_CLK_PRED(7));
  asm volatile("dsb");
  // disable clock gate
  CCM_CCGR3 |= CCM_CCGR3_FLEXIO2(CCM_CCGR_ON);
}

void FlexIO2VGA::TimerInterrupt(void) {
  if (dma_params.TCD->SADDR) {
    dma1 = dma_params;
    if (double_height) {
      dma1.disableOnCompletion();
      dmaswitcher.triggerAtCompletionOf(dma1);

      dma2 = dma_params;
      dma2.disableOnCompletion();
      dmaswitcher.triggerAtCompletionOf(dma2);
    }
    dma1.enable();
    // push first pixels into shiftbuf registers
    dma1.triggerManual();
    FLEXIO2_SHIFTSDEN = 1<<0;
  }
  frameCount++;
}

void FlexIO2VGA::set_next_buffer(const void* source, size_t pitch, bool wait) {
  // find worst alignment combo of source and pitch
  size_t log_read;
  switch (((size_t)source | pitch) & 7) {
    case 0: // 8 byte alignment
      log_read = 3;
      break;
    case 2: // 2 byte alignment
    case 6:
      log_read = 1;
      break;
    case 4: // 4 byte alignment
      log_read = 2;
      break;
    default: // 1 byte alignment, this will be slow...
      log_read = 0;
  }
  uint16_t major = (widthxbpp+63)/64;
  dma_params.TCD->SOFF = 1 << log_read;
  dma_params.TCD->ATTR_SRC = log_read;
  dma_params.TCD->SADDR = source;
  dma_params.TCD->SLAST = pitch - (major*8);
  dma_params.TCD->CITER = dma_params.TCD->BITER = major;
  if (wait)
    wait_for_frame();
}

extern FlexIO2VGA FLEXIOVGA;
void FlexIO2VGA::ISR(void) {
  uint32_t timStatus = FLEXIO2_TIMSTAT & 0xFF;
  FLEXIO2_TIMSTAT = timStatus;

  if (timStatus & (1<<5)) {
    FLEXIOVGA.TimerInterrupt();
  }

  asm volatile("dsb");
}

/* END VGA driver code */

static void FillFrameBuffer(uint8_t *fb, int height, int width, int bpp, size_t pitch) {
  const int radius = height/6 - 10;
  static int xoff = radius;
  static int yoff = radius;
  static int xdir = 4;
  static int ydir = 2;
  static uint8_t bg = 8;
  static uint8_t fg = 8;
  const int limit = radius*radius;

  bool hit = false;
  if (xoff >= (int)width-radius) {
    hit = true;
    xdir = -xdir;
    xoff = (int)width-radius;
  }
  if (xoff <= radius) {
    hit = true;
    xdir = -xdir;
    xoff = radius;
  }
  if (yoff >= (int)height-radius) {
    hit = true;
    ydir = -ydir;
    yoff = (int)height-radius;
  }
  if (yoff <= radius) {
    hit = true;
    ydir = -ydir;
    yoff = radius;
  }

  if (hit) {
    if (++fg == 16)
      fg = 8;
    if (++bg == 24)
      bg = 0;
  }

  for (int y=0; y < height; y++) {
    uint8_t *p = fb+y*pitch;
    for (int x=0; x < width;)  {
      uint8_t c=0;
      if (bpp == 1) {
        for (int i=0; i < 8; i++) {
          int xdiff = x-xoff+i;
          int ydiff = y-yoff;
          if ((xdiff*xdiff + ydiff*ydiff) <= limit)
            c |= 1 << i;
        }
        x += 8;
      } else { // bpp=4
        for (int i=1; i >= 0; i--) {
          c <<= 4;
          int xdiff = x-xoff+i;
          int ydiff = y-yoff;
          c |= ((xdiff*xdiff + ydiff*ydiff) <= limit) ? fg : (bg/3);
        }
        x += 2;
      }
      *p++ = c;
    }
  }

  xoff += xdir;
  yoff += ydir;
}

PROGMEM static const vga_timing t1280x720x60 = {
  .height=720, .vfp=13, .vsw=5, .vbp=12,
  .width=1280, .hfp=80, .hsw=40, .hbp=248,
  .clk_num=7425, .clk_den=2400, .vsync_pol=0, .hsync_pol=0
};

PROGMEM static const vga_timing t1024x768x60 = {
  .height=768, .vfp=3, .vsw=6, .vbp=29,
  .width=1024, .hfp=24, .hsw=136, .hbp=160,
  .clk_num=65, .clk_den=24, .vsync_pol=1, .hsync_pol=1
};

PROGMEM static const vga_timing t800x600x100 = {
  .height=600, .vfp=1, .vsw=3, .vbp=32,
  .width=800, .hfp=48, .hsw=88, .hbp=136,
  .clk_num=6818, .clk_den=2400, .vsync_pol=0, .hsync_pol=1
};

PROGMEM static const vga_timing t800x600x60 = {
  .height=600, .vfp=1, .vsw=4, .vbp=23,
  .width=800, .hfp=40, .hsw=128, .hbp=88,
  .clk_num=40, .clk_den=24, .vsync_pol=0, .hsync_pol=0
};

PROGMEM static const vga_timing t640x480x60 = {
  .height=480, .vfp=10, .vsw=2, .vbp=33,
  .width=640, .hfp=16, .hsw=96, .hbp=48,
  .clk_num=150, .clk_den=143, .vsync_pol=1, .hsync_pol=1
};

PROGMEM static const vga_timing t640x400x70 = {
  .height=400, .vfp=12, .vsw=2, .vbp=35,
  .width=640, .hfp=16, .hsw=96, .hbp=48,
  .clk_num=150, .clk_den=143, .vsync_pol=0, .hsync_pol=1
};

PROGMEM static const vga_timing t640x350x70 = {
  .height=350, .vfp=37, .vsw=2, .vbp=60,
  .width=640, .hfp=16, .hsw=96, .hbp=48,
  .clk_num=150, .clk_den=143, .vsync_pol=1, .hsync_pol=0
};

// memory restrictions
#define MAX_WIDTH (1280/2)
#define MAX_HEIGHT 720
#define STRIDE_PADDING 16

static uint8_t frameBuffer0[(MAX_HEIGHT+1)*(MAX_WIDTH+STRIDE_PADDING)];
DMAMEM static uint8_t frameBuffer1[(MAX_HEIGHT+1)*(MAX_WIDTH+STRIDE_PADDING)];
static uint8_t* const s_frameBuffer[2] = {frameBuffer0, frameBuffer1};

const vga_timing *timing = &t640x400x70;
FlexIO2VGA FLEXIOVGA(*timing);

void setup() {
  Serial.begin(9600);
}

void loop() {
  static uint32_t frameBufferIndex;

  static int double_height = false;
  static int double_width = false;
  静态int bpp = 4;

  int 高度 = 时间->高度 / (double_height?2:1);
  int 宽度 = 计时->宽度 / (double_width?2:1);
  size_t 间距 = 宽度 * bpp/8 + STRIDE_PADDING;

  int c = 串行.读取();
  如果 (c >= 0) {
    开关(c){
      案例‘0’:
        时间=&t1280x720x60;
        Serial.println("新模式:1280x720x60");
        休息;
      案例‘1’:
        时间=&t1024x768x60;
        Serial.println("新模式:1024x768x60");
        休息;
      案例 ‘2’:
        时间 = &t800x600x100;
        Serial.println("新模式:800x600x100");
        休息;
      案例 ‘3’:
        时间 = &t800x600x60;
        Serial.println("新模式:800x600x60");
        休息;
      案例 ‘4’:
        时间=&t640x480x60;
        Serial.println("新模式:640x480x60");
        休息;
      案例‘5’:
        时间=&t640x400x70;
        Serial.println("新模式:640x400x70");
        休息;
      案例 ‘6’:
        时间=&t640x350x70;
        Serial.println("新模式:640x350x70");
        休息;
      案例‘h’:
      案例‘H’:
        双倍高度 = !双倍高度;
        Serial.print("高度加倍是");
        串行.println(double_height?“开”:“关”);
        休息;
      案例‘w’:
      案例‘W’:
        双倍宽度 = !双倍宽度;
        Serial.print("宽度加倍是");
        串行.println(double_width?“开”:“关”);
        休息;
      案例‘b’:
      案例‘B’:
        bpp = (bpp==1)?4:1;
        串行打印(“使用BPP =”);
        串行.println(bpp);
        休息;
      默认:
        串行.println(“0:1280x720x60”);
        串行.println(“1:1024x768x60”);
        串行.println(“2:800x600x100”);
        串行.println(“3:800x600x60”);
        串行.println(“4:640x480x60”);
        串行.println(“5:640x400x70”);
        串行打印(“6:640x350x70”);
        Serial.println("H:切换高度加倍");
        Serial.println("W:切换宽度加倍");
        Serial.println("B: 切换位深度(4/1)");
      案例‘\n’:
        返回;
    }
    FLEXIOVGA.停止();
    FLEXIOVGA = FlexIO2VGA(*timing,double_height,double_width,bpp);
  }

  FillFrameBuffer(s_frameBuffer[frameBufferIndex], 高度, 宽度, bpp, 间距);
  // DMAMEM 帧缓冲区必须从缓存中刷新
  如果 (帧缓冲区索引)
    arm_dcache_flush_delete(s_frameBuffer[frameBufferIndex],高度*间距);

  FLEXIOVGA.set_next_buffer(s_frameBuffer[frameBufferIndex],pitch,true);
  帧缓冲区索引^= 1;
}
[/代码]
[/QUOTE]
[URL='https://www.zouser.com/']你好,也许这个链接对你有帮助[/URL]
 
FWIW, dropped the library into my new board, with the resistor tree using 180/390 ohms, into a 7 inch VGA monitor from AliExpress, the library from Github, and it umm worked first time. That's not right but I'll take it!

640x480 and 800x600 work great, but 1024x768 makes an artifact, I haven't spent ten seconds diagnosing it, could be LCD setup. Later on that. 640x480 is best for text size on the small screen so that will be it for now, and 800x600 would be just fine.

Characters are sharp! Distortion in the 2nd/right image is the phone, close up.

CPU overhead seems to be 50 to 60 uS per second, nice!

(The Teensy is doing the text, emulator isn't running yet.)
 

Attachments

  • IMG_20250121_125932_734.jpg
    IMG_20250121_125932_734.jpg
    349.2 KB · Views: 15
  • IMG_20250121_125936_845.jpg
    IMG_20250121_125936_845.jpg
    331.1 KB · Views: 15
Ouch -- scrolling busies the machine for 26 mS.
I might have a go at implementing scrolling just by adjusting the DMA source... two possible ideas, either aligning the framebuffer to a power of 2 address and using DMA wrapping or splitting it into 2 transfers, one for the "top" portion of the screen and another for the "low" portion (split where it wraps from the bottom of the allocated framebuffer memory to the top). In theory it could also be used to scroll left and right.

Have you given any further effort on this solution? I haven't yet looked at the code to see how scrolling is done. If it's interruptible it should be OK with slow scroll.
 
I never coded any scrolling, that was wwatson's additions. But if you split the DMA operation that writes pixels into the FlexIO registers into two separate operations, scrolling would be practically free.
 
Back
Top