Bare-Metal Ada on the ESP32-S3 A step-by-step guide to running Ada on the ESP32-S3 with no ESP-IDF, no FreeRTOS, and no Python.

Step 19 of 56

LCD: two very different display modes

One controller, two personalities — a command-driven 8-bit i8080 bus that streams a buffer on demand, and a continuously-refreshed RGB panel that never stops.

The i8080 mode

ESP32S3.LCD drives the LCD half of the LCD_CAM controller as an 8-bit Intel-8080 parallel master: a byte buffer is streamed out the data bus, one byte per pixel clock, over GDMA. It suits any 8-bit parallel sink, not only displays.

type Data_Pins is array (0 .. 7) of ESP32S3.GPIO.Optional_Pin;

procedure Acquire
  (S       : in out Session;
   Pclk_Hz : Positive  := 1_000_000;
   Data    : Data_Pins := (others => No_Pin);
   Pclk    : ESP32S3.GPIO.Optional_Pin := No_Pin);

The pixel clock is quantised: you get 20 MHz / round (20 MHz / Pclk_Hz), so ask for a divisor of 20 MHz if the exact rate matters. Unlike I2S, bringing the controller up does not tie up a GDMA channel — Transmit claims one only for the duration of a transfer, so the channel is available to other peripherals between frames.

procedure Transmit (S : Session; Tx : ESP32S3.GDMA.DMA_Buffer;
                    Length : Natural; Ok : out Boolean);

Blocking, 1 .. 4095 bytes (the single-descriptor limit again), buffer in internal SRAM, with the usual DMA_Buffer alignment and whole-cache-line size preconditions. Enable_Clock_Out free-runs the pixel clock on a pad with no data transaction, which is how you check the clock divider on a scope before trusting a panel.

The RGB mode

A TFT panel driven by continuous HSYNC / VSYNC / DE / PCLK timing, rather than by commands. This is a different discipline: the panel must be refreshed forever, so the framebuffer streams from a chained GDMA descriptor ring (step 16), which is exactly why that ring exists.

type RGB_Data_Pins  is array (0 .. 15) of ESP32S3.GPIO.Optional_Pin;
type RGB_Signal_Map is array (0 .. 15) of Natural;

type RGB_Config is record
   H_Sync, V_Sync : Positive;          --  sync pulse widths
   Two_Byte       : Boolean := True;   --  True: 16-bit RGB565; False: 8-bit
   DE_Idle_High   : Boolean := False;  --  DE is usually active-high, so idle low
   --  ... plus the porches, from the panel datasheet
end record;

procedure Acquire_RGB (S : in out Session; Config : RGB_Config; Pins : RGB_Pins);

Horizontal widths and porches are in pixel clocks; vertical ones in lines — copy them from the panel's datasheet. RGB_Signal_Map says which LCD_DATA_OUT signal drives each panel data line, for boards whose wiring is not in the obvious order.

Acquire_RGB initialises the peripheral — enables the controller, sets the timing and routes the pins. Starting the continuous refresh from a framebuffer is a separate call afterwards. Splitting the two lets you get the timing right on a scope before committing a framebuffer to it.

The camera-receive half of LCD_CAM is not covered by this driver.

LCD: two very different display modes · Bare-Metal Ada on the ESP32-S3
Bare-Metal Ada on the ESP32-S3 A step-by-step guide to running Ada on the ESP32-S3 with no ESP-IDF, no FreeRTOS, and no Python.

Step 19 of 56

LCD: two very different display modes

One controller, two personalities — a command-driven 8-bit i8080 bus that streams a buffer on demand, and a continuously-refreshed RGB panel that never stops.

The i8080 mode

ESP32S3.LCD drives the LCD half of the LCD_CAM controller as an 8-bit Intel-8080 parallel master: a byte buffer is streamed out the data bus, one byte per pixel clock, over GDMA. It suits any 8-bit parallel sink, not only displays.

type Data_Pins is array (0 .. 7) of ESP32S3.GPIO.Optional_Pin;

procedure Acquire
  (S       : in out Session;
   Pclk_Hz : Positive  := 1_000_000;
   Data    : Data_Pins := (others => No_Pin);
   Pclk    : ESP32S3.GPIO.Optional_Pin := No_Pin);

The pixel clock is quantised: you get 20 MHz / round (20 MHz / Pclk_Hz), so ask for a divisor of 20 MHz if the exact rate matters. Unlike I2S, bringing the controller up does not tie up a GDMA channel — Transmit claims one only for the duration of a transfer, so the channel is available to other peripherals between frames.

procedure Transmit (S : Session; Tx : ESP32S3.GDMA.DMA_Buffer;
                    Length : Natural; Ok : out Boolean);

Blocking, 1 .. 4095 bytes (the single-descriptor limit again), buffer in internal SRAM, with the usual DMA_Buffer alignment and whole-cache-line size preconditions. Enable_Clock_Out free-runs the pixel clock on a pad with no data transaction, which is how you check the clock divider on a scope before trusting a panel.

The RGB mode

A TFT panel driven by continuous HSYNC / VSYNC / DE / PCLK timing, rather than by commands. This is a different discipline: the panel must be refreshed forever, so the framebuffer streams from a chained GDMA descriptor ring (step 16), which is exactly why that ring exists.

type RGB_Data_Pins  is array (0 .. 15) of ESP32S3.GPIO.Optional_Pin;
type RGB_Signal_Map is array (0 .. 15) of Natural;

type RGB_Config is record
   H_Sync, V_Sync : Positive;          --  sync pulse widths
   Two_Byte       : Boolean := True;   --  True: 16-bit RGB565; False: 8-bit
   DE_Idle_High   : Boolean := False;  --  DE is usually active-high, so idle low
   --  ... plus the porches, from the panel datasheet
end record;

procedure Acquire_RGB (S : in out Session; Config : RGB_Config; Pins : RGB_Pins);

Horizontal widths and porches are in pixel clocks; vertical ones in lines — copy them from the panel's datasheet. RGB_Signal_Map says which LCD_DATA_OUT signal drives each panel data line, for boards whose wiring is not in the obvious order.

Acquire_RGB initialises the peripheral — enables the controller, sets the timing and routes the pins. Starting the continuous refresh from a framebuffer is a separate call afterwards. Splitting the two lets you get the timing right on a scope before committing a framebuffer to it.

The camera-receive half of LCD_CAM is not covered by this driver.

LCD: two very different display modes · Bare-Metal Ada on the ESP32-S3
Bare-Metal Ada on the ESP32-S3 A step-by-step guide to running Ada on the ESP32-S3 with no ESP-IDF, no FreeRTOS, and no Python.

Step 19 of 56

LCD: two very different display modes

One controller, two personalities — a command-driven 8-bit i8080 bus that streams a buffer on demand, and a continuously-refreshed RGB panel that never stops.

The i8080 mode

ESP32S3.LCD drives the LCD half of the LCD_CAM controller as an 8-bit Intel-8080 parallel master: a byte buffer is streamed out the data bus, one byte per pixel clock, over GDMA. It suits any 8-bit parallel sink, not only displays.

type Data_Pins is array (0 .. 7) of ESP32S3.GPIO.Optional_Pin;

procedure Acquire
  (S       : in out Session;
   Pclk_Hz : Positive  := 1_000_000;
   Data    : Data_Pins := (others => No_Pin);
   Pclk    : ESP32S3.GPIO.Optional_Pin := No_Pin);

The pixel clock is quantised: you get 20 MHz / round (20 MHz / Pclk_Hz), so ask for a divisor of 20 MHz if the exact rate matters. Unlike I2S, bringing the controller up does not tie up a GDMA channel — Transmit claims one only for the duration of a transfer, so the channel is available to other peripherals between frames.

procedure Transmit (S : Session; Tx : ESP32S3.GDMA.DMA_Buffer;
                    Length : Natural; Ok : out Boolean);

Blocking, 1 .. 4095 bytes (the single-descriptor limit again), buffer in internal SRAM, with the usual DMA_Buffer alignment and whole-cache-line size preconditions. Enable_Clock_Out free-runs the pixel clock on a pad with no data transaction, which is how you check the clock divider on a scope before trusting a panel.

The RGB mode

A TFT panel driven by continuous HSYNC / VSYNC / DE / PCLK timing, rather than by commands. This is a different discipline: the panel must be refreshed forever, so the framebuffer streams from a chained GDMA descriptor ring (step 16), which is exactly why that ring exists.

type RGB_Data_Pins  is array (0 .. 15) of ESP32S3.GPIO.Optional_Pin;
type RGB_Signal_Map is array (0 .. 15) of Natural;

type RGB_Config is record
   H_Sync, V_Sync : Positive;          --  sync pulse widths
   Two_Byte       : Boolean := True;   --  True: 16-bit RGB565; False: 8-bit
   DE_Idle_High   : Boolean := False;  --  DE is usually active-high, so idle low
   --  ... plus the porches, from the panel datasheet
end record;

procedure Acquire_RGB (S : in out Session; Config : RGB_Config; Pins : RGB_Pins);

Horizontal widths and porches are in pixel clocks; vertical ones in lines — copy them from the panel's datasheet. RGB_Signal_Map says which LCD_DATA_OUT signal drives each panel data line, for boards whose wiring is not in the obvious order.

Acquire_RGB initialises the peripheral — enables the controller, sets the timing and routes the pins. Starting the continuous refresh from a framebuffer is a separate call afterwards. Splitting the two lets you get the timing right on a scope before committing a framebuffer to it.

The camera-receive half of LCD_CAM is not covered by this driver.

LCD: two very different display modes · Bare-Metal Ada on the ESP32-S3
Bare-Metal Ada on the ESP32-S3 A step-by-step guide to running Ada on the ESP32-S3 with no ESP-IDF, no FreeRTOS, and no Python.

Step 19 of 56

LCD: two very different display modes

One controller, two personalities — a command-driven 8-bit i8080 bus that streams a buffer on demand, and a continuously-refreshed RGB panel that never stops.

The i8080 mode

ESP32S3.LCD drives the LCD half of the LCD_CAM controller as an 8-bit Intel-8080 parallel master: a byte buffer is streamed out the data bus, one byte per pixel clock, over GDMA. It suits any 8-bit parallel sink, not only displays.

type Data_Pins is array (0 .. 7) of ESP32S3.GPIO.Optional_Pin;

procedure Acquire
  (S       : in out Session;
   Pclk_Hz : Positive  := 1_000_000;
   Data    : Data_Pins := (others => No_Pin);
   Pclk    : ESP32S3.GPIO.Optional_Pin := No_Pin);

The pixel clock is quantised: you get 20 MHz / round (20 MHz / Pclk_Hz), so ask for a divisor of 20 MHz if the exact rate matters. Unlike I2S, bringing the controller up does not tie up a GDMA channel — Transmit claims one only for the duration of a transfer, so the channel is available to other peripherals between frames.

procedure Transmit (S : Session; Tx : ESP32S3.GDMA.DMA_Buffer;
                    Length : Natural; Ok : out Boolean);

Blocking, 1 .. 4095 bytes (the single-descriptor limit again), buffer in internal SRAM, with the usual DMA_Buffer alignment and whole-cache-line size preconditions. Enable_Clock_Out free-runs the pixel clock on a pad with no data transaction, which is how you check the clock divider on a scope before trusting a panel.

The RGB mode

A TFT panel driven by continuous HSYNC / VSYNC / DE / PCLK timing, rather than by commands. This is a different discipline: the panel must be refreshed forever, so the framebuffer streams from a chained GDMA descriptor ring (step 16), which is exactly why that ring exists.

type RGB_Data_Pins  is array (0 .. 15) of ESP32S3.GPIO.Optional_Pin;
type RGB_Signal_Map is array (0 .. 15) of Natural;

type RGB_Config is record
   H_Sync, V_Sync : Positive;          --  sync pulse widths
   Two_Byte       : Boolean := True;   --  True: 16-bit RGB565; False: 8-bit
   DE_Idle_High   : Boolean := False;  --  DE is usually active-high, so idle low
   --  ... plus the porches, from the panel datasheet
end record;

procedure Acquire_RGB (S : in out Session; Config : RGB_Config; Pins : RGB_Pins);

Horizontal widths and porches are in pixel clocks; vertical ones in lines — copy them from the panel's datasheet. RGB_Signal_Map says which LCD_DATA_OUT signal drives each panel data line, for boards whose wiring is not in the obvious order.

Acquire_RGB initialises the peripheral — enables the controller, sets the timing and routes the pins. Starting the continuous refresh from a framebuffer is a separate call afterwards. Splitting the two lets you get the timing right on a scope before committing a framebuffer to it.

The camera-receive half of LCD_CAM is not covered by this driver.

LCD: two very different display modes · Bare-Metal Ada on the ESP32-S3
Bare-Metal Ada on the ESP32-S3 A step-by-step guide to running Ada on the ESP32-S3 with no ESP-IDF, no FreeRTOS, and no Python.

Step 19 of 56

LCD: two very different display modes

One controller, two personalities — a command-driven 8-bit i8080 bus that streams a buffer on demand, and a continuously-refreshed RGB panel that never stops.

The i8080 mode

ESP32S3.LCD drives the LCD half of the LCD_CAM controller as an 8-bit Intel-8080 parallel master: a byte buffer is streamed out the data bus, one byte per pixel clock, over GDMA. It suits any 8-bit parallel sink, not only displays.

type Data_Pins is array (0 .. 7) of ESP32S3.GPIO.Optional_Pin;

procedure Acquire
  (S       : in out Session;
   Pclk_Hz : Positive  := 1_000_000;
   Data    : Data_Pins := (others => No_Pin);
   Pclk    : ESP32S3.GPIO.Optional_Pin := No_Pin);

The pixel clock is quantised: you get 20 MHz / round (20 MHz / Pclk_Hz), so ask for a divisor of 20 MHz if the exact rate matters. Unlike I2S, bringing the controller up does not tie up a GDMA channel — Transmit claims one only for the duration of a transfer, so the channel is available to other peripherals between frames.

procedure Transmit (S : Session; Tx : ESP32S3.GDMA.DMA_Buffer;
                    Length : Natural; Ok : out Boolean);

Blocking, 1 .. 4095 bytes (the single-descriptor limit again), buffer in internal SRAM, with the usual DMA_Buffer alignment and whole-cache-line size preconditions. Enable_Clock_Out free-runs the pixel clock on a pad with no data transaction, which is how you check the clock divider on a scope before trusting a panel.

The RGB mode

A TFT panel driven by continuous HSYNC / VSYNC / DE / PCLK timing, rather than by commands. This is a different discipline: the panel must be refreshed forever, so the framebuffer streams from a chained GDMA descriptor ring (step 16), which is exactly why that ring exists.

type RGB_Data_Pins  is array (0 .. 15) of ESP32S3.GPIO.Optional_Pin;
type RGB_Signal_Map is array (0 .. 15) of Natural;

type RGB_Config is record
   H_Sync, V_Sync : Positive;          --  sync pulse widths
   Two_Byte       : Boolean := True;   --  True: 16-bit RGB565; False: 8-bit
   DE_Idle_High   : Boolean := False;  --  DE is usually active-high, so idle low
   --  ... plus the porches, from the panel datasheet
end record;

procedure Acquire_RGB (S : in out Session; Config : RGB_Config; Pins : RGB_Pins);

Horizontal widths and porches are in pixel clocks; vertical ones in lines — copy them from the panel's datasheet. RGB_Signal_Map says which LCD_DATA_OUT signal drives each panel data line, for boards whose wiring is not in the obvious order.

Acquire_RGB initialises the peripheral — enables the controller, sets the timing and routes the pins. Starting the continuous refresh from a framebuffer is a separate call afterwards. Splitting the two lets you get the timing right on a scope before committing a framebuffer to it.

The camera-receive half of LCD_CAM is not covered by this driver.

LCD: two very different display modes · Bare-Metal Ada on the ESP32-S3
Bare-Metal Ada on the ESP32-S3 A step-by-step guide to running Ada on the ESP32-S3 with no ESP-IDF, no FreeRTOS, and no Python.

Step 19 of 56

LCD: two very different display modes

One controller, two personalities — a command-driven 8-bit i8080 bus that streams a buffer on demand, and a continuously-refreshed RGB panel that never stops.

The i8080 mode

ESP32S3.LCD drives the LCD half of the LCD_CAM controller as an 8-bit Intel-8080 parallel master: a byte buffer is streamed out the data bus, one byte per pixel clock, over GDMA. It suits any 8-bit parallel sink, not only displays.

type Data_Pins is array (0 .. 7) of ESP32S3.GPIO.Optional_Pin;

procedure Acquire
  (S       : in out Session;
   Pclk_Hz : Positive  := 1_000_000;
   Data    : Data_Pins := (others => No_Pin);
   Pclk    : ESP32S3.GPIO.Optional_Pin := No_Pin);

The pixel clock is quantised: you get 20 MHz / round (20 MHz / Pclk_Hz), so ask for a divisor of 20 MHz if the exact rate matters. Unlike I2S, bringing the controller up does not tie up a GDMA channel — Transmit claims one only for the duration of a transfer, so the channel is available to other peripherals between frames.

procedure Transmit (S : Session; Tx : ESP32S3.GDMA.DMA_Buffer;
                    Length : Natural; Ok : out Boolean);

Blocking, 1 .. 4095 bytes (the single-descriptor limit again), buffer in internal SRAM, with the usual DMA_Buffer alignment and whole-cache-line size preconditions. Enable_Clock_Out free-runs the pixel clock on a pad with no data transaction, which is how you check the clock divider on a scope before trusting a panel.

The RGB mode

A TFT panel driven by continuous HSYNC / VSYNC / DE / PCLK timing, rather than by commands. This is a different discipline: the panel must be refreshed forever, so the framebuffer streams from a chained GDMA descriptor ring (step 16), which is exactly why that ring exists.

type RGB_Data_Pins  is array (0 .. 15) of ESP32S3.GPIO.Optional_Pin;
type RGB_Signal_Map is array (0 .. 15) of Natural;

type RGB_Config is record
   H_Sync, V_Sync : Positive;          --  sync pulse widths
   Two_Byte       : Boolean := True;   --  True: 16-bit RGB565; False: 8-bit
   DE_Idle_High   : Boolean := False;  --  DE is usually active-high, so idle low
   --  ... plus the porches, from the panel datasheet
end record;

procedure Acquire_RGB (S : in out Session; Config : RGB_Config; Pins : RGB_Pins);

Horizontal widths and porches are in pixel clocks; vertical ones in lines — copy them from the panel's datasheet. RGB_Signal_Map says which LCD_DATA_OUT signal drives each panel data line, for boards whose wiring is not in the obvious order.

Acquire_RGB initialises the peripheral — enables the controller, sets the timing and routes the pins. Starting the continuous refresh from a framebuffer is a separate call afterwards. Splitting the two lets you get the timing right on a scope before committing a framebuffer to it.

The camera-receive half of LCD_CAM is not covered by this driver.