Continuing (off the course of the syllabus) from where Path to Programmable Blog 7 - Trying out a PL-only VGA design left off, I decided to try generating a HDMI/DVI video signal.
VGA is an analog signal, where different voltages (from 0 to 0.7V) correspond to different intensities of color. Three wires (channels) are used to transmit R,G & B, and two 'sync' (HSYNC & VSYNC) are level triggered signals that control the scanning i.e. help the receiver determine the end of a row and the end of a frame (and in turn the resolution).
HDMI and DVI are digital, and in fact, HDMI & DVI are compatible both electrically & protocol-wise (since HDMI is basically DVI (video) bundled with some other stuff).
Unlike VGA which relied on voltage levels to differentiate color intensities, HDMI serializes a byte of data, applies 8b/10b encoding & transmits this data using TMDS (Transition-Minimized Differential Signaling).
Coming to the wiring: each color channel (R,G & B) uses 2 wires (since the signalling is differential), for a total of 3*2 = 6. Another differential pair for the clock is added, which comes to a total of 8 wires.
Diagrams of the connector are as follows (from hdmi.org & alrico.org):
Each TMDS channel has an associated shielding cable, and depending on whether it's HDMI or DVI, a couple of extras are included (CEC, ARC etc.). The DDC (I2C) is used to allow the transmitter to query the receiver so that it knows what resolutions are supported (EDID), and to allow adjustment of video settings. DVI supports analog video and an extra channel of digital video (dual link) which is why it has a couple of extra pins.
TMDS uses a current drive on the transmitter side, and pull-ups on the receiver side. For HDMI/DVI, the receiver has a 50 ohm resistor pulled up to 3.3V
The TMDS protocol encodes 8 bits of data to 10 bits - this is done to minimize transitions and maintain a equal number of '1's and '0's. The algorithm is simple and involves a couple of XOR & XNOR operations. Tim Ansell gave a nice presentation on this. After the data conversion is done, the resultant data is serialized and sent to the receiver, which performs everything in reverse to decode the data.
Building a HDMI/DVI Transmitter
In Path to Programmable Blog 7 - Trying out a PL-only VGA design, a VGA sync generator counted pixels row by row and generated the HSYNC & VSYNC at the appropriate time. The test pattern (actual color data) was a fixed image that simply assigned a different color to the pixels based on the horizontal address. All of this worked as a video generator, generating a RGB value for every pixel in a frame. When I transmitted it using VGA, the MSB of each pixel color channel was mapped to an I/O pin, which when connected to a resistor formed a 1-bit DAC (for a total of 3 bits).
To transmit this using HDMI/DVI, we can use the same data, except instead of sending it to the I/O pin directly, we encode the entire byte of data using 8b/10b encoding and serialize it.
Fortunately, Digilent has published a rgb2dvi IP, which takes a pixel clock, a serial clock (5x the pixel clock which is used for serialization), the 24-bit RGB data, HSYNC, VSYNC & a data valid signal - most of which our VGA test pattern generator already generates!
rgb2dvi is 'IP', so it needs to be added to a block design. The output signals from the VGA generator get mapped directly to the inputs of the rgb2dvi IP using the HDL wrapper (which I've already discussed last time). The serial clock that is used by the serializer needs to be 5x the pixel clock - this is because for every pixel (one pixel clock), we encode 10 bits of data (since we're using 8b/10b encoding), and data is clocked out of the serializer in 'DDR' mode i.e. on the rising & falling edges of the serial clock. Therefore, for every pixel generated (8-bits), we encode the pixel to 10 bits and clock it each bit out twice per clock period of a clock that is 5x the pixel clock - transmitting 10 bits.
The pVDE signal is simply a 'data valid' signal that is high whenever the RGB values generated are within the active area, since a frame is actually rendered with porches that don't contain actual data. Computing pVDE is easy: pVDE <= (CounterX < 640) && (CounterY < 480) for a 640x480 frame.
I started this design using a Digilent Zybo Z7-20 board (which contains a higher end Zynq part), since that board has HDMI Tx port. Note that the design is entirely in the PL, and the PS is not required at all. The screenshots that I'm posting are after the project was ported over to the MiniZed, where the PS (Zynq7) is only required to boot up so that the PL has a clock source to work with (the Zybo has a 125Mhz clock connected to the PL, so the PS is not needed at all).
This is what the elaborated design looks like:
The existing VGA generator feeds an output to the block design wrapper, since the rgb2dvi IP is inside the wrapper. The rgb2dvi IP also outputs the HDMI signals, so those (TMDS) signals come out of the wrapper and are mapped to pins. I also added an ILA for debugging.
The Zynq PS7 outputs a 25 Mhz clock, which is fed to the clocking wizard that generates a 25 Mhz pixel clock, and 125 Mhz serial clock. The rgb2dvi can generate the 5x serial clock internally, but couldn't get that to work - I kept getting timing violations during implementation.
After synthesis, the rgb2dvi IP looks like this: each channel is first encoded in the TMDS_Encoder block, and then serialized using the SERDES.
The TMDS Encoder block looks like this:
The Serilizer is interesting, since Digilent used the OSERDES primitives.
Documented in Xilinx: UG471 7 Series SelectIO Resources, the OSERDES blocks are hard IP that take parallel data and clock it out serially. Each OSERDES has a 8-bit input, and 2 can be cascaded in a master-slave mode to offer a wider input - which is what Digilent did.
The OSERDES (and the corresponding ISERDES for input) offer configurability: tri-states, SDR & DDR modes (clocking on rising, or both edges - Digilent uses DDR to output 10 bits at a time using a 5x pixel clock as explained earlier).
When it comes to actual implementation, the 2 small orange blocks on the left are the OSERDES blocks, and the pins are on the right. The 2 SERDES blocks operate in the master-slave mode, and the output from the master is routed to the positive differential pin (white trace). Since the negative differential pin gets the complementary signal, the positive signal goes through the inverter in the IO block, and the output (complementary) is routed to the negative differential pair. In short, this means that when using a master-slave serializer OSERDES' with differential outputs, double the number of OSERDES' don't need to be used, since the complementary differential signal can be generated from the signal itself and routed between the I/O blocks.
After generating the bitstream, I programmed the Zybo and connected the HDMI cable - and the pattern showed up on the display!
Making the MiniZed generate HDMI/DVI
Digging into the Digilent ZYBO Z7 Reference Manual, the HDMI Tx port doesn't require much external circuitry. Digilent includes a multiplexer between the port & the Zynq package, but that's simply to allow the Zynq to be reset with the HDMI cable connected. The schematics also confirm this, which would mean that the Zynq (or 7 series FPGAs for that matter) do not need any transceiver chip since the TMDS signals are generated by the Zynq IC itself.
Going back to the Xilinx: UG471 7 Series SelectIO Resources, I looked at the subsection for "Supported I/O Standards and Terminations", and sure enough, I found TMDS:
If the The Z7-20 supported it and generated the TMDS signals for HDMI & DVI without any external circuitry, there was a possibility of getting this to work on the MiniZed as well.
I referred to Xilinx UG865 : Zynq-7000 SoC Packaging and Pinout Product Specification, which had diagrams and a link to this file which listed the 'Pins' & the corresponding 'Pin names' (though I didn't actually need this. I also referred to the .xdc file of the Zybo Z7-20 to get a better idea of how the differential pairs were mapped: each differential pair (positive & netgative) for a given channel was mapped to the corresponding "IO_xxxP" & "IO_xxxN" pins. Diving deeper into the Xilinx documentation: " Each user I/O bank has 50 single-ended I/Os or 24 differential pairs (48 differential
I/Os). The top and bottom I/O pin are always single ended."
When selecting the pins on for the TMDS channels of the MiniZed (since they weren't already assigned like they were on the Zybo), I would need to ensure that corresponding differential signals get mapped to the correct pair of pins - this might be because the 2 pins have the complementary signal interconnection between them, which I wrote about after the cascaded SERDES. This also probably means that the paired pins are optimized to reduce skew.
I selected the pins in pairs according to the package pin in the schematic. Look at how the Arduino pin numbers don't always correspond to the same differential pair: IO0 & IO1 are not pairs, but IO0 and IO3 are - you need to refer to the schematic to be able to select the correct sets. I don't know whether selecting non-paired pins will cause issues, but I suspect that even if it works, the max frequency will be limited compared to paired-up pins.
The IO pins are LVCMOS33 by default, so I made changes to the .xdc file to make them TMDS_33. For some reason, Vivado didn't detect this and threw an error because the differential pins were being used with single-ended signalling (LVCMOS33). I opened the I/O planning view in the synthesized design, and manually changed every one to TMDS_33.
I wasn't sure whether this would actually work, so I tried it out on the Zybo. HDMI output worked on the Zybo, so I created another instance of the rgb2dvi IP in the block diagram, and mapped these outputs to a PMOD connector (the original were mapped to the HDMI port). First I connected the HDMI output port to the TV to verify that it was working, and then tested out the signals routed through the duplicate instance to the PMOD by connecting male-to-male jumper cables between the PMOD port and the female DVI receiver port on a monitor, which worked.
I went back to the MiniZed design, generated the bitsream and launched an empty application in SDK (to enable the PS clock), I followed the same procedure - jumper wires from the Arduino header to the female DVI port of a monitor (since HDMI pins are too small to use without a breakout/proper cable, but DVI is bigger and wires can be forced into the socket).
I connected 8 cables: 3 color channel pairs (3*2 = 6) and the clock pair (2). The differential shield wasn't connected (since there wasn't any) and the differential pairs weren't twisted.
It worked, and the MiniZed generated HDMI (DVI) without any external components!
While having not much in common with Path to Programmable (except for the MiniZed, since the design doesn't make use of the 'Zynq' capabilities at all), working on this was certainly very interesting. I learnt quite a bit by reading about the 7 Series I/O (voltages, signalling) and hardware blocks (MMCM, PLL, SERDES). Doing all this reading has made the documentation more familiar, but at the same time I've found so much more that I hadn't even known about.
- HDMI Rx. Digilent has dvi2rgb IP, which I plan on using. I'll need to implement the hotplug detect & DDC (for EDID) so that the computer detects the FPGA are a HDMI receiver. This will probably be tested as HDMI pass-through using the HDMI Rx & Tx on the Zybo.
- Driving a laptop LCD panel using LVDS. Later merge this with HDMI Rx to build a HDMI to LVDS converter, since I could always use an extra display. This is a little more complicated since the panel expects LVDS at a lower voltage (not TMDS), but I think I've found a way around this.
- HDMI to VGA converter - should be straightforward once I figure out HDMI Rx
- VGA to HDMI converter - will be interesting, since I'll need to use the XADC to digitize the VGA data first. If this doesn't work, I will resort to a 1 or 2 bit-ADC built using op-amps as comparators to build a flash ADC.