This is a project for playing with images and nmigen / cool FPGA tools and learning as I go. Here is a summary of roughly what is going on
You need to install yosys and nmigen. For handling image data numpy and pillow are also used. You probably also want GTKWave for viewing simulation waveforms.
Run python -m pixtolic.top
. This should build the design and flash
it to an attached iCEBreaker board (currently the only target.
Some modules include simulations, you can run these with e.g. python -m pixtolic.sources.still
. This will produce a still.vcd
waveform
file which you can view with gtkwave still.vcd
.
The pixtolic/top.py
file gives the top-level structure of the
design. It assumes you are using a 12-bit color VGA output pmod as
sold by
Digilent. You
can also use a 12-bit color "DVI pmod" with an HDMI connector as made
by
1bitsquared
(requires different pin assignments -- will add shortly). Whenever the
24-bit color DDR DVI pmod becomes available I will support that also,
everything is designed to be generic in the color depth. Assuming
we've told the compiler with the different types of IO resources
available on the board, we can get groups of pins by name with
platform.request
. Cat
is an nmigen function for concatenating
signals together.
clk12 = platform.request('clk12', dir='-')
vga_pads = platform.request('vga')
uart_pads = platform.request('uart')
leds = Cat([
platform.request('led_r'),
platform.request('led_g'),
])
button = platform.request('button')
The first thing is to define the clock domains and use the iCE40's PLL to synthesize a pixel clock (36 MHz for 800x600p @ 56Hz) from the crystal oscillator clock on the iCEbreaker board (12 MHz). I've defined some objects for storing the VGA timing data. Available resolutions are basically limited by which pixel clock frequencies you can generate.
# this resolution uses a 36 MHz pixel clock, which the PLL can
# match exactly
res = resolutions[ResolutionName.SVGA_800_600p_56hz]
# define clock domains called 'pixel' and 'sync'
# 'sync' will run at 12 MHz and can be used for
# other logic
m.domains.pixel = ClockDomain()
m.domains.sync = sync = ClockDomain()
# this class sets parameters for a special PLL tile
# that generates a higher frequency clock, as well as a
# buffered copy of its input clock as `buf_clkin`. we set the
# high frequency output clock as the clock for the 'pixel' domain
# and the `buf_clkin` signal as the clock for the 'sync' domain
m.submodules.pll = pll = iCE40PLL(
freq_in_mhz=12,
freq_out_mhz=res.pixclk_freq / 1e6,
domain_name='pixel',
)
m.d.comb += [
pll.clk_pin.eq(clk12),
sync.clk.eq(pll.buf_clkin),
]
# we also need to tell the compiler this clock is 36 MHz
# so it can check if the design meets timing constraints
platform.add_clock_constraint(pll.clk_pin, res.pixclk_freq)
Next we add some submodules to the design. The VgaTiming
module
generates hsync / vsync / active signals for the given resolution.
The TestPattern
module generates a simple pattern of color swatches.
For the Still
module we can pass in a sufficiently small PIL.Image
object and it will load it into the FPGA's block RAM when we reflash
the chip, then read out the image contents repeatedly as the pixel
clock runs.
m.submodules.vga_timing = vga_timing = VgaTiming(res)
m.submodules.test_pattern = test_pattern = TestPattern(vga_timing, color_depth=self.color_depth)
fname = path.join(
path.dirname(__file__),
'../resources/RGB_12bits_parrot.png',
)
image = Image.open(fname).resize((100, 75))
m.submodules.still = still = Still(
timing=vga_timing,
color_depth=self.color_depth,
image=image,
)
Then we just wire up stuff to the IO/pins for the VGA and for the button on the iCEbreaker board itself (labeled "Button" on the silkscreen). We'll show the loaded still when the button is held and show the color bars / swatches otherwise.
m.d.comb += [
vga_pads.hsync.eq(vga_timing.hsync),
vga_pads.vsync.eq(vga_timing.vsync),
]
with m.If(button):
m.d.comb += [
vga_pads.red.eq(still.red),
vga_pads.green.eq(still.green),
vga_pads.blue.eq(still.blue),
]
with m.Else():
m.d.comb += [
vga_pads.red.eq(test_pattern.red),
vga_pads.green.eq(test_pattern.green),
vga_pads.blue.eq(test_pattern.blue),
]
Finally there is a UART module included which is just set in loopback
mode right now, because that seems like it could also be useful. You
could take it out and reclaim some more FPGA space for larger still
storage or something. Internally it calculates the UART baud rate by
just dividing down the 12 MHz clock with registers, since there's only
one PLL on the iCE40. When the design is running, you can access this
UART over USB by running screen /dev/ttyUSB0 115200
(you might get a
different ttyUSB*
number). Then anything you type will get echoed
back by the FPGA and the red and green lights on the board will flash
-- use ctrl+a k y
to exit screen
.
m.submodules.uart = uart = UARTLoopback(
uart_pads,
clk_freq=12e6,
baud_rate=115200,
)
m.d.comb += [
leds.eq(uart.rx_data[0:2]),
]
Everything else that's going on right now is pretty much the actual
pixel generation logic in the pixtolic/sources/patterns.py
and
pixtolic/sources/still.py
modules.