emalliab.wordpress.com
Simplified Pico VGA
The Raspberry Pi Pico Hardware Design Manual has an example board that shows how an RP2040 can be used to create a VGA signal using a 3-way 5-bit resistor DAC. It is the basis of the Pimoroni Pico VGA Demo Base which makes messing around with it pretty easy.
But the downside is that it requires 5 GPIO pins per colour plus H and V sync pins, so that is 17 GPIO pins in total just for the VGA connection. There is a connection diagram in the pico-playground here: https://github.com/raspberrypi/pico-playground/tree/master/scanvideo
To use the code requires using the following:
* pico_scanvideo library from pico_extras.
* Examples from the pico_playground.
As you might imagine it uses the Pico’s PIO and I believe DMA, to transfer pixel data over to the GPIO pins.
But that is a lot of GPIO for video data, so are there any alternatives?
This post looks in varying levels of detail at the following options:
* Pico(Q)VGA and DispHSTX
* MCEBlaster
* pico-zxspectrum
And finally then a proper dive into the Scanvideo library provided as part of the Pico Extras and Pico Playground repositories from Raspberry pi.
### Pico(Q)VGA and DispHSTX
One option is the really comprehensive PicoVGA project here: https://www.breatharian.eu/hw/picovga/index_en.html (with a fork that is designed to use the standard SDK build process and also supports the RP2350 here and a more minimal PicoQVGA version here).
This has a demo circuit that just uses 9 GPIO pins – 3 for red, 3 for green, 2 for blue plus one for Sync, which is a composite sync (CSYNC) for H and V sync combined.
This is because it only implements 8-bit resolution VGA. From the website:
_“The RP2040 processor contains 264 KB of RAM. This is not much for higher resolution image output and therefore RAM needs to be very sparing. In technical practice and for retro games (the processor does not have the power for more advanced games), 8-bit graphics output in R3G3B2 format (i.e. red 3 bits, green 3 bits and blue 2 bits) is fully sufficient. Output in 16 or 24 bits is not meaningful in normal practice, as the Raspberry Pico does not have enough memory or power to provide such large amounts of data in areas other than short demos. Using the dithering technique, interesting display results can be achieved even with 8-bit output.”_
I suspect the lowest level VGA output part of this library is probably relatively understandable, but the library itself is so comprehensive, it is probably a lot more than I am interested in right now. It boasts a large range of resolutions, various frame buffer modes, colour palettes, transparency, and so on (just browse over the “PicoVGA library features” section).
The author has declared this now superseded by DispHSTX, which apparently supports HDMI (DVI) and VGA using the RP2350’s HSPX – the high speed transmit peripheral. There is also a RP2350 specific official HSTX example for driving DVI: https://github.com/raspberrypi/pico-examples/tree/master/hstx. Note: there are several versions of a “DVI sock” for the Pico that hooks up HDMI/DVI to the HSTX compatible GPIO pins (GPIO 12-19).
### MCE Blaster
Another really interesting project is the MCE Blaster: _“A versatile standalone MCA/CGA/EGA TTL to VGA adapter based on a raspberry pi Pico.”_
It is a reimplementation of other similar projects (for example this mce adaptor) that tend to use PLDs or programmable logic.
This uses a Pico to read video TTL data from MDA, CGA, EGA sources and output an equivalent VGA signal. But as it is just limited to the lower, legacy, resolutions and colour depth, it can do this using just two GPIO per colour, plus H and V, so that is 8 GPIO pins in total.
That has the potential to be really useful for me, so digging a bit deeper into how it works, we can see the relevant parts for the VGA output in the schematic:
The output works on the same principles as the Pico VGA demo, but instead of using 5:5:5 colour values, there are only 2 bits per colour, or 4 individual levels for each of R, G and B.
Here is a video that describes how this works really well: https://www.youtube.com/watch?v=kgDOiGoxKvE. The video shows how the EGA and CGA signals differ:
But the end result is a much smaller pallet of colours to be shown on a VGA display.
The other thing to note of course is that the TTL signals are digital signals, with up to 2 bits per signal, whereas the VGA output is an analog signal with each of R, G ,B requiring between 0 and 0.7V each.
A high resolution colour pallet is created by the Pico when using 5 bits per colour, as it splits the voltage range into 32 different levels. With the 2-bit input range there are only four levels (including 0) corresponding to the digital bits: 00, 01, 10, 11 where 11 will equal 0.7V.
The video goes into detail how to use a two-input resistor circuit and which resistor values to choose. The calculations turn out to require R to be 419Ω. The circuit has ultimately chosen to use 390Ω and 780Ω for the two values.
Note that horizontal (H) and vertical sync (V) are digital signals, so either HIGH (TTL 5V) or LOW).
There is a lot of neat code in the MCE-Blaster to handle synchronisation between the digital and analog signals, especially when it comes to sample the incoming TTL pixel signal. It also has a comprehensive range of adjustments and looks like a really useful tool for anyone wishing to plug in an old graphics card into a VGA monitor. But I’m not going to need any of this if I’m just doing video output.
#### MCA Blaster VGA Output Code
All the firmware and PCB designs are available on GitHub: https://github.com/scrapcomputing/MCEBlaster
There is a lot in the firmware folder, including some algorithmically generated PIO code to support the full range of input options, as well as the use of C++ templates.
The key items handling the VGA output side of things are:
* firmware/src/Pio/VGAOut4x1Pixels.pio
* firmware/src/Pio/VGAOut8x1MDA.pio
* firmware/src/VGAWriter.h
* firmware/src/VGAWriter.cpp
As far as I can make out, the main code is essentially split into two. There is the TTLReader, which runs on core 1, and the VGAWriter which runs on core 0. There is a display buffer (as described by the DisplayBuffer class) shared between them.
I’m actually not interested in the MDA support, which supports monochrome, higher resolution, output so will just focus on the EGA/CGA side of things.
The VGAOut4x1Pixels.pio code, as you might imagine, outputs four pixels of VGA data to the three 2-bit DACs, with options to set the H or V sync lines if required.
The format of the data is four sets of 8-bit values of the form: VHRRGGBB. Note that this is different to that suggested by the comments in the PIO programme, which has V and H swapped over.
There are a number of const definitions in the common.h file that define various bit patterns for the colours and H and V signals. Many of these have a _4 variant which creates a 4-pixel version, for example:
static constexpr const uint32_t HMask = 0b01000000;
static constexpr const uint32_t VMask = 0b10000000;
static constexpr const uint32_t HMask_4 =
HMask | HMask << 8 | HMask << 16 | HMask << 24;
static constexpr const uint32_t VMask_4 =
VMask | VMask << 8 | VMask << 16 | VMask << 24;
Each of the colours has a definition too:
static constexpr const uint32_t Black = 0b000000;
static constexpr const uint32_t White = 0b111111;
static constexpr const uint32_t Red = 0b110000;
...etc...
There is a special set of definitions for use with black which has a special use when outputting the borders:
static constexpr const uint32_t Black_4 =
Black | Black << 8 | Black << 16 | Black << 24;
static constexpr const uint32_t Black4_HV = Black_4 | HVMask_4;
static constexpr const uint32_t Black4_V = Black_4 | VMask_4;
Given all this pre-amble, I can start to piece together what the code is doing.
#### VGAPIO
The main PIO programme is managed from VGAWriter, using the following variables:
* VGAPio – the PIO instance – pio1 is used for the output.
* VGASM – the free statemachine allocated to the VGA output.
* VGAOffset – the PIO code offset.
There are three other PIO programmes running related to the input: NoInputSignalPio (pio1), VSyncPolarityPio (pio0), and HSyncPolarityPio (pio0). These are part of the input monitoring and so I’m largely ignoring those.
There is a PIOProgramLoader helper class that is used to initialise all of the TTL reader PIO routines and the main VGA output PIO (it isn’t used for the three additional PIOs mentioned above).
The VGA PIO initialisation is performed in tryChangePIOMode() which will check if a mode switch between EGA/CGA and MDA is required and reinitilise the PIO as required.
#### VGA Writer
The main functions in the code that I’m interested in are as follows:
DrawBlackLineWithMask4x1()
Output a black line with appropriate H and V signals
DrawLineVSyncHigh4x1()
Output a line from the DisplayBuffer with H and V
tryChangePIOMode()
Check input display mode
Select between CGA/EGA and MDA if required
Initialise VGA output PIO
VGAWriter()
Initialise DisplayBuffer
Claim unused SM for VGASM
Initiliase NoInputSignalPio, VSyncPolarityPio, HSyncPolarityPio
(re)start TTLReader on core 1
tryChangePIOMode()
drawFrame4x1()
Draw back porch with DrawBlackLineWithMask4x1()
Draw lines with DrawLineVSyncHigh4x1()
Draw front porch with DrawBlackLineWithMask4x1()
runForEver()
drawFrame4x1()
tryChangePIOMode()
I would like to dig further into this code at some point, but actually I’m going to pause here for a moment, as I’ve found another simplification that is highly relevant for my eventual application of all of this.
### The pico_zxspectrum Approach
One thing that was driving the investigation so far was seeing that the palette for CGA was very similar to that of the ZX Spectrum which is actually what I’d really like to be able to recreate.
When I found the pico-zxspectrum project and more specifically the section on VGA and the Spectrum, I noted it has this GPIO configuration for VGA compatible output with ZX Spectrum characteristics (Image from the pico-zxspectrum repository here):
This format is described as RGBY1111 and the key things to note are:
* Only 5 GPIO pins are required.
* It isn’t using VSYNC.
* There is a single pin that effectively implements the “brightness” control, which reflects one of the limitations of the ZX Spectrum’s attribute arrangement – brightness affects all colour states within an attribute block.
The pico-spectrum project also describes video options for RGB332 and RGB222:
_“So far, there are three supported VGA configurations, which can be found in the various build targets. They have all been designed with a combination of plagiarism and guesswork, so please let me know if you have better versions and I will update this document.”_
There is no mention of the use of PIO in this video driver directly. There is a “zxspectrum_pio_all” definition that lists pio programs for audio i2s, ear, pwm, and st7789 video, but nothing that seems tailored to VGA output, but that is because it is using the scan_video code from the pico_extras repository. There are some references to “video_24mhz_composable” which appears in the extras repo.
There is a full discussion of how to use the code here: https://github.com/raspberrypi/pico-extras/tree/master/src/common/pico_scanvideo
There are many different configurations (including the aforementioned RGBY1111, RGB332, RGB222 modes) in src/vga/CMakeLists.txt which has many of the definitions for the scanvideo code, for example, for RGBY1111, I can see:
set(ZxSpectrumI2SAudioVga1111Ps2_defines
# VGA 1111
VGA_ENC_RGBY_1111
PICO_SCANVIDEO_COLOR_PIN_COUNT=4u
PICO_SCANVIDEO_DPI_PIXEL_RCOUNT=2u
PICO_SCANVIDEO_DPI_PIXEL_GCOUNT=1u
PICO_SCANVIDEO_DPI_PIXEL_BCOUNT=1u
PICO_SCANVIDEO_DPI_PIXEL_RSHIFT=2u
PICO_SCANVIDEO_DPI_PIXEL_GSHIFT=1u
PICO_SCANVIDEO_DPI_PIXEL_BSHIFT=0u
PICO_SCANVIDEO_COLOR_PIN_BASE=12u
I think what is happening is that there are single bit resolutions for blue and green, but red is configured for two bits. Mapping this up with the schematic we can see that the PIN_BASE of 12 with a PIN_COUNT of 4 means using GPIO 12, 13, 14, 15 which maps onto BGR and Y in the schematic. GPIO 16 is the HSYNC.
These definitions will override the default values for the scanvideo driver which can be found in:
* pico_extras:src/rp2_common/pico_scanvideo_dpi/include/pico/scanvideo.h
* pico_extras:src/common/pico_scanvideo/include/pico/scanvideo/scanvideo_base.h
The main code for preparing and handing off data to the scan library can be found in:
* src/ZxRgb332RenderLoop.cpp
* src/ZxScanlineVgaRenderLoop.cpp
* src/ZxSpectrumPrepareRgbScanline.cpp
* src/ZxSpectrumPrepareScanvideoScanline.cpp
* src/ZxSpectrumVideoDriver.cpp
The timings for the different modes for RGBY1111 are detailed in src/ZxScanlineVgaRenderLoop.cpp, for example for 640×480 at 60Hz:
const scanvideo_timing_t vga_timing_640x480_60 =
{
.clock_freq = 25000000,
.h_active = 640,
.v_active = 480,
.h_front_porch = 16,
.h_pulse = 96,
.h_total = 800,
.h_sync_polarity = VGA_H_SYNC_POLARITY, // 1=HSYNC & VSYNC, 5=CSYNC
.v_front_porch = 10,
.v_pulse = 2,
.v_total = 525,
.v_sync_polarity = 1,
.enable_clock = 0,
.clock_polarity = 0,
.enable_den = 0
};
This is also where the scanvideo library is initialised and where the key rendering pipeline happens.
void __not_in_flash_func(ZxScanlineVgaRenderLoop)(
ZxSpectrum &zxSpectrum,
volatile uint &frames,
bool &showMenu,
volatile bool &toggleMenu,
ZxSpectrumMenu& picoRootWin
) {
if (!_scanvideo_mode) return;
scanvideo_setup(_scanvideo_mode);
scanvideo_timing_enable(true);
unsigned char* screenPtr = zxSpectrum.screenPtr();
unsigned char* attrPtr = screenPtr + (32 * 24 * 8);
while (true) {
//
// Display rendering infinite loop
//
}
__builtin_unreachable();
}
These functions are set up in the various structures in src/ZxSpectrumVideoDriver.cpp depending on the build settings. The default is determined by:
#if defined(CVBS_12MHZ) || defined(CVBS_13_5MHZ) || defined(VGA_MODE)
#include "ZxScanlineVgaRenderLoop.h"
#ifndef DEFAULT_VIDEO_DRIVER
#define DEFAULT_VIDEO_DRIVER zx_spectrum_video_driver_vga_index
#endif
#else
#define ZxScanlineVgaRenderLoopInit NULL
#define ZxScanlineVgaRenderLoop NULL
#define ZxScanlineVgaRenderLoopAudioDefault NULL
#endif
The value of DEFAULT_VIDEO_DRIVER can be overwritten in the makefiles (as it is for the st7789 LCD display for example).
The main rendering loop uses the following functions:
* scanvideo_begin_scanline_generation()
* zx_prepare_scanvideo_blankline()
* pcw_prepare_scanvideo_scanline_80()
* x_prepare_scanvideo_scanline()
* scanvideo_end_scanline_generation()
With a couple of callbacks called at appropriate times too. Key to processing the video are two pointers: screenPtr and attrPtr which are set to point to the appropriate locations in the Spectrum video memory and then passed into zx_prepare_scanvideo_scanline(). It also requires two other key variables for each rendering scan: frame_num and y:
uint32_t frame_num = scanvideo_frame_number(scanline_buffer->scanline_id);
uint32_t y = scanvideo_scanline_number(scanline_buffer->scanline_id);
Moving over to src/ZxSpectrumPrepareScanvideoScanline.cpp I can see how the data is processed for each scan line:
void __not_in_flash_func(zx_prepare_scanvideo_scanline)(
struct scanvideo_scanline_buffer *scanline_buffer,
uint32_t y,
uint32_t frame,
uint8_t* screenPtr,
uint8_t* attrPtr,
uint8_t borderColor
) {
uint32_t* buf = scanline_buffer->data;
//
// fill in *buff from the data provided by screenPtr and attrPtr
// including the top/bottom and side borders where necessary
//
}
There is a lot of bit and number manipulation going on in this function to translate between the Spectrum’s screen data format and the data stream required by the scanvideo library. I’m not going to go into that here (its complicated) but I might come back to this at some point. In places this uses an internal function single_color_run() to fill in a block of the same colour.
Note, with regards to some of the other video modes, I can see that src/ZxSpectrumPrepareRgbScanline.cpp is used for the 332 mode, called from src/ZxRgb332RenderLoop.cpp.
But although this gets a mention in src/CMakeLists.txt, I can’t see where it would be enabled.
set(zxspectrum_rgb332_src
${CMAKE_CURRENT_LIST_DIR}/ZxRgb332RenderLoop.cpp
)
set(zxspectrum_rgb_src
${CMAKE_CURRENT_LIST_DIR}/ZxSpectrumPrepareRgbScanline.cpp
)
set(zxspectrum_scanvideo_src
${CMAKE_CURRENT_LIST_DIR}/ZxSpectrumPrepareScanvideoScanline.cpp
${CMAKE_CURRENT_LIST_DIR}/ZxScanlineVgaRenderLoop.cpp
)
In summary it appears that the main way the VGA video modes link together is something like:
src/ZxSpectrumVideoDriver.cpp
Setup _zx_spectrum_video_drivers[]
Include VGA:
{
ZxScanlineVgaRenderLoopInit,
ZxScanlineVgaRenderLoop,
ZxScanlineVgaRenderLoopAudioDefault,
"VGA"
},
Include DVI
Include ST7789 LCD
Set DEFAULT_VIDEO_DRIVER unless overridden by CMakeLists.txt
VGA is the default if VGA_MODE is set by CMakeLists.txt
src/ZxScanlineVgaRenderLoop.cpp
ZxScanlineVgaRenderLoop():
CALL zx_prepare_scanvideo_scanline()
(using src/ZxSpectrumPrepareScanvideoScanline.cpp)
src/ZxRgb332RenderLoop.cpp:
ZxRgb332RenderLoop():
CALL zx_prepare_rgb_scanline()
(using src/ZxSpectrumPrepareRgbScanline.cpp)
At this point so far, I’ve not seen any way for the 332 VGA mode to be enabled or called other than that one mention in the main CMakeLists.txt file. But the main readme does list the following files as example configurations for each of the three video modes:
* RGBY 1111: src/vga/CMakeLists.txt
* RGB 222: src/picomputer/picomputer_vga_zx/CMakeLists.txt
* RGB 332: src/picomputer/picomputer_vga/CMakeLists.txt
These certainly do seem to have GPIO pin definitions for 222 and 332 modes, but as far as I can tell it is still using the vga/scanline line rendering code, rather than the 332/vga line rendering code, via all the mechanisms listed above.
Whilst on the topic of video modes, these are defined in pico_extras:src/common/pico_scanvideo/include/pico/scanvideo/scanvideo_base.h.
The following appear to be options used by the pico_zxspectrum:
vga_mode_640x240_60
vga_mode_720x288_50
cvbs_mode_50
As already mentioned the various pin configurations are provided as part of the pico_zxspectrum CMakeLists.txt files and will override the defaults in the scanvideo driver header and source files using compiler directives.
Even with all this though, I still can’t figure out how ZxRgb332RenderLoop is ever used 🙂
### Using the Scanvideo Library
In principle I could have started this post with this library and ignored most of the above. But when this is first encountered it isn’t obvious (at least to me) that something like the RGBY1111 mode would even be a possibility. It took seeing how pico_zxspectrum used it to get this far.
But knowing that, I can now take a look at how this is possible and hopefully come up with a small test case to show it in action.
The official provided examples for using scanvideo can be found here: https://github.com/raspberrypi/pico-playground/tree/master/scanvideo
Although there is a “minimal” example, actually I think the “test_pattern” example is perhaps the easiest to understand. The pseudo code is something like the following:
Set the vga_mode to vga_mode_320x240_60
main():
Launches video processing on core 1
while (TRUE):
IF Space bar pressed THEN:
invert colours
draw_color_bar():
Use scanvideo_scanline_number() to get line number
FOREACH bar of colour:
Fill display buffer based on line and x position
core1_func():
scanvideo_setup(&vga_mode);
scanvideo_timing_enable(true);
while (TRUE):
scanvideo_scanline_buffer_t *scanline_buffer =
scanvideo_begin_scanline_generation(true);
draw_color_bar(scanline_buffer);
scanvideo_end_scanline_generation(scanline_buffer);
The buffer is defined by scanvideo_scanline_buffer as follows (details here):
typedef struct scanvideo_scanline_buffer {
uint32_t scanline_id;
uint32_t *data;
uint16_t data_used;
uint16_t data_max;
void *user_data;
uint8_t status;
} scanvideo_scanline_buffer_t;
This example uses the following definitions as part of constructing the display pixel data:
PICO_SCANVIDEO_PIXEL_FROM_RGB5 - Convert from RGB555 to pixel data.
COMPOSABLE_COLOR_RUN - For 3 or more pixels of the same colour.
COMPOSABLE_RAW_1P - For a single pixel with colour.
COMPOSABLE_EOL_SKIP_ALIGN - End marker (no alignment reqd)
The PICO_SCANVIDEO_PIXEL_FROM_RGB5 macro converts from three 5-bit R, G and B values into the required scanvideo pixel data format. There is also a RGB8 version too.
This is defined in two places. Firstly in src/rp2_common/pico_scanvideo_dpi/include/pico/scanvideo.h:
#define PICO_SCANVIDEO_PIXEL_RSHIFT 0
#define PICO_SCANVIDEO_PIXEL_GSHIFT 6
#define PICO_SCANVIDEO_PIXEL_BSHIFT 11
#define PICO_SCANVIDEO_PIXEL_FROM_RGB8(r, g, b) ( \
(((b)>>3)<<PICO_SCANVIDEO_PIXEL_BSHIFT)| \
(((g)>>3)<<PICO_SCANVIDEO_PIXEL_GSHIFT)| \
(((r)>>3)<<PICO_SCANVIDEO_PIXEL_RSHIFT))
#define PICO_SCANVIDEO_PIXEL_FROM_RGB5(r, g, b) ( \
((b)<<PICO_SCANVIDEO_PIXEL_BSHIFT)| \
((g)<<PICO_SCANVIDEO_PIXEL_GSHIFT)| \
((r)<<PICO_SCANVIDEO_PIXEL_RSHIFT))
Or if no other definition exists, then the version from src/common/pico_scanvideo/include/pico/scanvideo/scanvideo_base.h is used instead which is slightly different but similar.
Note that many of the scanvideo definitions map over onto scanvideo_dpi definitions, for example:
#define PICO_SCANVIDEO_DPI_PIXEL_BCOUNT 5
#define PICO_SCANVIDEO_PIXEL_BCOUNT PICO_SCANVIDEO_DPI_PIXEL_BCOUNT
Pulling all the above together, I think this ends up with a buffer format for pixels as follows:
R = 5-bit value
G = 5-bit value
B = 5-bit value
11111 1
54321 09876 5 43210
Scanvideo Pixel = bbbbb ggggg 0 rrrrr
So what is going on with that bit 5 of the pixel data? Looking at the shift definitions, I think this is the fourth pin value for the “ALPHA_PIN” as shown below:
#define PICO_SCANVIDEO_DPI_ALPHA_PIN 5u
#define PICO_SCANVIDEO_DPI_PIXEL_RSHIFT 0u
#define PICO_SCANVIDEO_DPI_PIXEL_GSHIFT 6u
#define PICO_SCANVIDEO_DPI_PIXEL_BSHIFT 11u
There are more details on the basic mechanism in the main scanvideo readme here: https://github.com/raspberrypi/pico-extras/tree/master/src/common/pico_scanvideo
#### The scanvideo PIO Program
One thing I can’t quite figure out is how winding the mapping down to a single GPIO per colour works with the VGA pio programs. There are two pio programs:
* src/common/pico_scanvideo/scanvideo.pio
* src/rp2_common/pico_scanvideo_dpi/timing.pio
And I can’t see anything that limits the number of bits of pixel data for them to stream out. The scanvideo readme states:
_“The default scanline program deals with 16 bit pixels, generally assumed to be 5, 5, 5 of RGB and 1 optional transparency pin (for multiple video planes – see below). The pin numbers are configurable via`PICO_SCANVIDEO_PIXEL_RSHIFT` etc. and `PICO_SCANVIDEO_COLOR_PIN_BASE`“_
The function scanvideo_setup_with_timing() configures GPIO and PIO so I assume is handling the varying GPIO pin definitions.
The “Hardware Design with RP2040” guide states the following when describing the example VGA circuit:
_“To this end, we can free up a pin by limiting the green channel to five bits, which will make all the channels the same resolution, by removing the green LSB (least significant bit). It is still desirable for RP2040 to process RGB-565 format data, so PIO will still output six bits of green data to the GPIOs; but we can choose not to use the green LSB in the function select register of that particular GPIO, instead letting RP2040 use it for something else”_
Taking a closer look at the scanvideo.pio program, I can see there are two instances of the PIO code. I’m going to look at program “video_24mhz_composable_default”.
Some observations right at the start:
;
; Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
;
; SPDX-License-Identifier: BSD-3-Clause
;
; Default scanline program (|| means aligned word boundary, | means hword boundary)
.program video_24mhz_composable_default
.origin 0 ; must load at zero (offsets are hardcoded in instruction stream)
.define extra0 0 ; set later by code based on xscale
.define extra1 0 ; set later by code (1 more than extra0)
; note bpp must be a factor of 32
.define bpp 16
;.define bpp 8
That origin statement is very important. From what I can see this code uses direct manipulation of the state machine’s PC register to jump around the code in response to the data stream it is processing. This means that positions of labels in the code are assumed to be relative to instruction position 0.
The two parameters extra0 and extra1 appear as [wait] instructions to control the timing. Recall that VGA is an analogue signal, so to get a stream of pixels on the display simply means holding a particular colour signal for a certain length of time. Double the time it is held and the “line” drawn on the display will be twice as long. These parameters are set by the initialising code and I believe will serve to stretch each “pixel” to change the scaling in the horizontal direction.
public end_of_scanline_skip_ALIGN: ; || jmp end_of_scanline_skip_ALIGN | ignored ||
; was 16 but we just discard the reset of the OSR
; so as to also support 8 bit grayscale
out null, 32;
public end_of_scanline_ALIGN: ; | jmp end_of_scanline_ALIGN ||
This shows a principle that is used through the code. There are a number of instruction positions marked by public labels which can be referred to either from within the code as part of a JMP instruction, but more often than not get pulled out of the data stream and fed into an OUT instruction to change the PC register.
This is detailed in the “how it works” section of the src/common/pico_scanvideo readme. Each “token” (for example “RAW1P” or “END_OF_SCANLINE_ALIGN”) maps onto a public label within the PIO code, so that when the token gets read from the bit stream, the OUT instruction uses that as an equivalent for the label to a JMP by setting the PC directly to the instruction position matching the label.
public entry_point:
wait irq, 4 ; todo perhaps change this to out exec, 16... so that we can do multiple things (including setting black pixel)
public nop_raw:
out pc, bpp
So after waiting on IRQ 4 – presumably for timing synchronisation, we see out first “out pc, bpp”. bpp is 16 so this will take 16 bits from the Output Shift Register (OSR), itself filled from the TX FIFO, and use that to set the PC to the first token instruction.
After this come a series of different instruction blocks corresponding to each of the token instructions, which are:
* RAW1P
* RAW2P
* RAW_RUN
* COLOR_RUN
* END_OF_SCANLINE_ALIGN
* END_OF_SCANLINE_SKIP_ALIGN
* RAW1P_SKIP_ALIGN
Each of these has its own PIO block with the instruction to jump to referenced by a public label. The last instruction in each PIO block is then another “out pc, bpp” instruction to grab the next token instruction. Here is the code for COLOR_RUN:
public delay_h_0:
public color_run: ; | jmp color_run | color | count-3 |
out pins, bpp
out x, bpp
color_loop:
public delay_a_1:
jmp x-- color_loop [extra1]
public nop_extra1:
public delay_b_1:
out pc, bpp [extra1]
We can see 16-bits get written out from the OSR to the pins, then the next 16-bit value is loaded from the OSR in X. What follows is then a delay loop that will keep the value on the pins whilst X decreases down to 0. This has the effect, as previously mentioned, of extending the analogue output by the value read into X, thus creating a “run of coloured pixels” of the same colour. Once the loop is complete the “out pc,bpp” instruction will read the next token for processing.
All other tokens are processed in a similar way. The exception is the RAW1P and RAW2P blocks which have a slightly more complex setup:
.wrap_target
public raw_1p: ; | jmp raw_1p | color |
public delay_e_0:
out pins, bpp [extra0]
out pc, bpp
public raw_2p: ; | jmp raw_2p | color | color |
public delay_f_1:
out pins, bpp [extra1]
.wrap
Jumping into RAW_1P is fairly easy enough – output 16-bits to pins then grab the next token with the “out pc, bpp”. But notice that if the code jumps into RAW_2P then after the outputting of 16-bits to the pins, the .wrap/.wrap_target will need the code loops back to the RAW_1P section to output a second 16-bit value and then do the “out pc, bpp”.
Eventually the token reading will encounter one of the END_OF_SCANLINE_ tokens and execution will jump back to the top and wait on the next IRQ 4 to kick of the whole process once more.
The code to initialise the PIO can be found in: src/rp2_common/pico_scanvideo_dpi/scanvideo.c in the function setup_sm(). This is used to set up both the timing and scanline PIO programs. It is called from scanvideo_setup_with_timing() which also initialises buffers, GPIO, timing frequencies, interrupts, DMA, and run-time adjusts the stored PIO code (e.g. sorting out the “extra” parameters – follow what is going on with “modified_program” for the details) prior to calling pio_add_program twice prior to setting up the PIO itself.
The actual PIO configuration is performed in scanvideo_default_configure_pio() which sets the pin directions, outvpins, and shift and FIFO configuration. This relies on the following configuration definitions which, as previously mentioned, can be overriden by setting compiler directives in the makefiles:
PICO_SCANVIDEO_COLOR_PIN_BASE
PICO_SCANVIDEO_COLOR_PIN_COUNT
PICO_SCANVIDEO_ALPHA_PIN
On the topic of that run-time modification, I think this is all happening in video_24mhz_composable_adapt_for_mode(). This is referenced by a call to pio_program->adapt_for_mode() having previously set this up in the scanvideo_pio_program_t strucutre for the mode.
This uses a series of writes to the PIO code referencing the various “public delay_f_0” type statements. These correspond to equivalent [extra0] statements in the PIO code so adjusting the instructions at that point will change the [extra0] instructions.
Here is an example:
~~PIO code~~
public delay_b_1:
out pc, bpp [extra1]
~~Corresponding adjustment lines~~
int delay0 = 2 * mode->xscale - 2;
int delay1 = delay0 + 1;
modifiable_instructions[video_24mhz_composable_program_extern(delay_b_1)] |= (unsigned) delay1 << 8u;
Two other points to note about the PIO configuration:
* Autopull is enabled.
* It is configured for DMA.
* The FIFOs are joined – presumably this is to give the DMA more buffer headroom.
From the PIO end, then I have to conclude:
* It only accepts TOKENised streams as mentioned in the scanvideo readme.
* 16-bit TOKENs representing colours are passed directly out to the contiguous block of GPIO pins configured for the required video mode.
* This implies that for a RGBY1111 mode, each 16-bit TOKEN would only have four significant bits mapped onto the GPIO.
At this point I have to return to the pico-zxspectrum code and dig into that pixel scanline creation code to see what is going on.
#### How pico-zxspectrum Prepares the Scanvideo Scanline
So far I know there are DPI specific compiler definitions detailing 1 GPIO pin for G, B and 2 for R and the main rendering loop was found in ZxScanlineVgaRenderLoop().
This in turn calls zx_prepare_scanvideo_scanline() at various points to translate between ZX Spectrum screen memory and the format required by scanvideo. This can be found in src/ZxSpectrumPrepareScanvideoScanline.cpp.
I don’t plan to go through this in detail, just the parts that give a clue as to how the scanvideo buffer is formatted for RGBY1111 use. This has to cope with a number of use cases depending on the X and Y coordinates:
* Top and bottom borders are solid colours.
* Side borders likewise are special with solid blocks.
* It has to keep track of the foreground and background colours for each attribute block.
* It has to map onto the Spectrum’s 256 x 192 pixels and 32 x 24 colour resolution.
The main colour rendering section is below:
for (int i = 0; i < 32; ++i) {
uint8_t c = *a++; // Fetch the attribute for the character
uint8_t p = *s++ ^ zx_invert_masks[(c >> 7) & m]; // fetch a byte of pixel data
uint8_t bci = (c >> 3) & 0xf; // The background colour index
uint8_t fci = (c & 7) | (bci & 0x8); // The foreground colour index
uint32_t cw[2];
cw[0] = zxd_colour_words[bci]; // The background colour word
cw[1] = zxd_colour_words[fci]; // The foreground colour word
uint32_t cwb = cw[(p >> 7) & 1];
*buf++ = COMPOSABLE_RAW_RUN | (cwb << 16);
*buf++ = (16 - 3) | (cwb << 16);
*buf++ = cw[(p >> 6) & 1];
*buf++ = cw[(p >> 5) & 1];
*buf++ = cw[(p >> 4) & 1];
*buf++ = cw[(p >> 3) & 1];
*buf++ = cw[(p >> 2) & 1];
*buf++ = cw[(p >> 1) & 1];
*buf++ = cw[(p >> 0) & 1];
}
This loops through the 32 colour attribute blocks on the line and does the following for each block:
* Determine the background and foreground colours relevant to the block.
* Map the colours over to RGBY1111 values using zxd_colour_words.
* Grab a bytes worth of pixel data from the appropriate position in the screen memory.
* Start building a COMPOSABLE_RAW_RUN scanvideo line.
* For each pixel in p map it either onto the background (cw[0]) or foreground (cd[1]) colour.
COMPOSABLE_RAW_RUN is used for three or more VGA screen pixels and has the following format:
COMPOSABLE_RAW_RUN | COL1 | N-3 | COL2 | COL3 |...| COLN |
Recall that these are 16-bit tokens. We also need to process pixels from right to left as this is the direction of the VGA scan, hence starting with pixel 7.
The zxd_colour_words[] is a set of 16 values defined as follows:
#define VGA_RGBY_1111(r,g,b,y) \
((y##UL<<3)|(r##UL<<2)|(g##UL<<1)|b##UL)
#define X2(a) ((a) | (a << 16))
static uint32_t zxd_colour_words[16] = {
X2(VGA_RGBY_1111(0,0,0,0)), // Black
X2(VGA_RGBY_1111(0,0,1,0)), // Blue
X2(VGA_RGBY_1111(1,0,0,0)), // Red
...
X2(VGA_RGBY_1111(1,1,0,1)), // Bright Yellow
X2(VGA_RGBY_1111(1,1,1,1)) // Bright White
};
This defines each of the 16 Spectrum colours (8 colours, but with normal and bright of all apart from black, so really 15 colours) in terms of 16-bit tokens, but each token will be in the range 0 to 15 (b0000 to b1111). The X2 macro doubles it up so that each 32-bit value contains the same repeated 16-bit token.
Note that we can see how the extra R bit definition is used for the Y (or brightness) as the output order will be YRGB which will ultimately map onto the four GPIO pins. The one remaining quirk is the bit ordering of the GPIO pins though. We saw originally in the makefile definitions for VGA_END_RGBY_1111 that the bit-shifts are R:2, G:1, B:0 meaning that from the GPIO base of 12, when the PIO program outputs these to the mapped GPIO, then B=12, G=13, R=14, and thus Y=15.
Here is a fully expanded example of the macros working for bright yellow:
X2(VGA_RGBY_1111(1,1,0,1)), // Bright Yellow
= X2((1##UL<<3)|(1##UL<<2)|(1##UL<<1)|0##UL)
= X2(b0000 0000 0000 1110)
= 0x000E000E
On first glance of the code the ordering of the first two items in the buffer seems a little odd, but the Scanvideo readme states: “The state machines consumes these low half word first, followed by high half-word (little endian)”
This explains the first few lines that fill up the buffer:
// Set cwb to foregroud or background depending on state of p bit 7
uint32_t cwb = cw[(p >> 7) & 1];
// reduce cwb to just the HIGH 16-bits and combine
*buf++ = COMPOSABLE_RAW_RUN | (cwb << 16);
// reduce cwb to HIGH 16-bits again and combine
*buf++ = (16 - 3) | (cwb << 16);
This is filling the buffer with two scanvideo pixel TOKENs per ZX Spectrum pixel. The resultant buffer will look like the following:
[0] = COL1 | RAW_RUN
[1] = COL1 | 13
[2] = COL2 | COL2
[3] = COL3 | COL3
[4] = COL4 | COL4
[5] = COL5 | COL5
[6] = COL6 | COL6
[7] = COL7 | COL7
[8] = COL8 | COL8
Why the duplication? The ZX Spectrum resolution is 256×192 pixels wide. But this is just the main display area. There is also a border which takes the full horizontal resolution (in display terms, even if not usable for graphics) to closer to 384 for a TV output.
The actual values used are defined in src/ZxSpectrumDisplay.h. Here are the relevant ones:
#define ZX_SPECTRUM_SCREEN_WIDTH 256
#define DISPLAY_WIDTH_PIXELS 640
#define DISPLAY_BORDER_PIXELS (DISPLAY_WIDTH_PIXELS >> 1)
#define DISPLAY_BORDER_PIXELS_LEFT \
((DISPLAY_BORDER_PIXELS - ZX_SPECTRUM_SCREEN_WIDTH) >> 1)
Which translates to: Left border pixels = (640 / 2 – 256) / 2 = 32. So the total number of “spectrum” pixels in a VGA horizontal screen line is 32 + 256 + 32 = 320 which is half the VGA resolution. Hence the doubling up of each pixel when creating the Scanvideo scan line.
It is perhaps clear from this, that a ZX Spectrum video display on a VGA monitor will have slightly smaller borders compared to the original TV (320 vs 384) but the main display will be quite usable with pixel-doubling.
I’m not entirely sure what is happening with the vertical resolution, but suspect something similar is going on. It seems to involve either DISPLAY_BLANK_LINES, which I think comes out as 0 as it appears to be using VGA 480 / 2 less 240 for the Spectrum resolution – but elsewhere in the code there are hardcoded references to 192 which is the number I’d expect to see for the Spectrum. So I’m not sure what is going on here, but don’t need to at this stage.
### In summary – Displaying a Spectrum Screen with Scanvideo
I think they key points from all the above are:
* VGA at the ZX Spectrum resolutions is provided using just four GPIO pins: RGB and Y for brightness.
* Scanvideo PIO allows the configuration of a range of video modes.
* pico-zxspectrum uses makefile defines to set up RGBY1111 as a video mode.
* ZX Spectrum display is parsed to create a series of Scanvideo TOKEN scanlines each containing values for pixel/colour data between 0 and 15.
* The Scanvideo TOKEN pixel lines are doubled up to emulate a 320 pixel resolution on a 640 resolution display.
* When the PIO outputs the data from the tokens, the 0..15 values translate over to the four GPIO pins used in the order YRGB for GPIO 15 to 12.
* I don’t know how the vertical resolution is handled, but suspect very similar things are going on.
At this point, I’m going to conclude all the theory (not that anyone will read this far I’m sure) and in the next post I’ll actually attempt to do something with it all!
Kevin
### Share this:
* Share on Mastodon (Opens in new window) Mastodon
* Share on Tumblr (Opens in new window) Tumblr
* Share on Facebook (Opens in new window) Facebook
* Share on Bluesky (Opens in new window) Bluesky
* More
*
* Share on Reddit (Opens in new window) Reddit
* Share on LinkedIn (Opens in new window) LinkedIn
*
Like Loading...
### _Related_