~shiny/gbcap

Gameboy LCD frame capture using an FPGA
Tweak readme to avoid bad formatting
Add a bit more info to the readme
Delete some now unused code in compress.py

refs

main
browse  log 

clone

read-only
https://git.sr.ht/~shiny/gbcap
read/write
git@git.sr.ht:~shiny/gbcap

You can also use your local clone with git send-email.

This is a hardware design to capture the LCD output of a Gameboy by connecting an FPGA to some of the pins on the ribbon cable between the main and LCD boards of a Gameboy.

#Quickstart

Make sure git submodules are initialised (this repo includes migen as a submodule as the latest released version doesn't include Spartan 6 DDR Input).

Make sure you've got the papilio-prog bitfile loader from here.

If using a Papilio Pro board

 ./run gbcap build
 ./papilio-prog/papilio-prog -f build/top.bit
 ./recv.py -b 2000000 /dev/ttyUSB1 -o frames.cap

Captured raw data can be decompressed into images using decomp_stream.py:

./decomp_stream.py --output-gif frames.gif frames.cap

The GIFs output from this will be pretty big and unoptimised - try gifsicle to optimise the size, or use gif2webp to convert to webp. More annoyingly they will run too slow (at 50fps rather than 60fps) due to the low precision of the GIF frame duration.

There is also an option to output a set of pgm images, one per frame.

./decomp_stream.py --output-pgm frames/ frames.cap

#Pin assignment

This code (in gbcap.py) sets up which pins on the FPGA are expected to be connected to which pins on the Gameboy:

plat.add_extension([
    ('gb_lcd', 0,
        Subsignal('vsync', Pins('B:5')),
        Subsignal('hsync', Pins('B:4')),
        Subsignal('cpl', Pins('B:6')),
        Subsignal('clk', Pins('B:2')),
        Subsignal('pixel', Pins('B:0', 'B:1')))])

#Compressed video format

Compression is line based. Each line starts with a config byte. If the byte is 0xFF then the line is not compressed and the next 40 bytes in the stream are the raw line pixel data. Otherwise the byte describes two things: the most-significant bit is set if the line is an exact match, not otherwise. The lower 7 bits encode a pixel offset in x & y in the range -4..4, in the form ((x + 4) * 9) + (y + 4) (i.e. x/y offsets are biased by 4 to make them in the range 0..9). The offset selects the line in the previous frame (relative to the current line's y coordinate) and an x shift (in pixels, i.e. multiples of 2 bits). 0s are always shifted in to fill the whole line when shifting in x. This shifted line is the reference line. For an exact match there is no more data in the compressed stream for the current line - the output line is just the reference line verbatim. For a non-exact match the next 5 bytes in the compressed stream are a bitmask of which reference line bytes to output. For any 0 in the bitmask a byte is read from the compressed stream and output instead of using the byte from the reference line. For any 1 there is no data in the compressed stream for that byte, the byte from the reference line is output.

See compress_ref.py for a reference implementation of the compressor/decompressor.

#UART testing

 ./run uart_echo build
 ./papilio-prog/papilio-prog -f build/top.bit
 ./com.py -b 2000000 /dev/ttyUSB1 abcde

Should output the second argument 'abcde' in the example back. If it doesn't then something is wrong!

#Verilog testbench

Build the verilator testbench:

./run gbcap verilator

Binary will be at obj_dir/gbcap_tb. Pipe raw binary into the testbench, it will output the bytes from the compressor, e.g.

./dump_pgm.py *.pgm | ./obj_dir/gbcap_tb > tb.cap