| Project14 Home | |

Monthly Themes | ||

Monthly Theme Poll |

# 1 Introduction

As a follow-up to Learning Verilog with the Digilent Cmod S7 blog post, and as part of the Project14 | Vision Thing: Beaglebone AI Your Vision Thing Project! I decided to use an FPGA to design a GPU to draw on a vector display [1]. Vector displays in contrast to raster displays can have the electron beam deflected in any arbitrary form through the control of their X and Y coordinates. One example of this type of display is the Tektronix 4051 [2], [3]:

[Dalby Datormuseum]

Another example of displays that use this technology are cathode ray tube (CRT) oscilloscopes:

As you may guess, that's the analogue oscilloscope that I'm going to use as a vector display, it's very old, so I hope it will make it until the end of the contest. You may think at this point that this technology is dead, and in the form of electron beams it somewhat is, but not in the form of lasers yet! Laser scanners [4] use galvanometers to tilt mirrors and deflect lasers:

[Wikipedia]

Laser scanners are used in laser shows, laser engravers, 3D printers and LIDARs among other things.

In this Project14 I'll experiment with vector display rendering. I'll mostly use an FPGA to perform low level graphics as their timing is very predictable and its clock frequency very fast.

# 2 Fixed Point Numbers

One of the first things I needed for the project was a real number representation. Verilog does not support real numbers, so they must be implemented by the designer. The most common ways to represent real numbers are floating-point [5] and fixed-point numbers [6]. Floating-point numbers are what floating-point units (FPU) use, these numbers contain a sign bit, mantissa bits and exponent bits. There are standards like the IEE754 [7] that have standardized floating-point number formats such as the single precision (32 bit) and double precision (64 bit), but their implementation in hardware is quite complex and slow so I discarded them. Fixed-point numbers are on the other hand relatively easy to implement. Fixed-point numbers are treated internally as integers, except that they represent the numerator of a defined power of 2 denominator. Let’s take a closer look at how fixed-point numbers represent real numbers.

There are many notations to specify fixed-point numbers, but I’ll use the Q-notation [8]. A Q5.10 fixed-point number represents a 16-bit number that contains an implicit sign bit, 5 integer bits and 10 fractional bits. Q15 represents a 16-bit number with an implicit sign bit, no integer bits and 15 fractional bits. A “U” prefix indicates that there is no implicit sign bit, such as in the 16-bit fixed-point UQ6.10 and UQ16. UQ6.10 represents a 6 integer bits and 10 fractional bits fixed-point number, while UQ16 represents a 0 integer bits and 16 fractional bits fixed-point number.

It might be easier to understand how to work with these numbers with some examples:

The Q3.4 number “0011.0111” represents the number 55 / (2 ** 4) or 55 / 16 (or 110111 / 10000 in binary) or 3.4375. One way to think of fixed-point numbers is as the integer numerator of a fixed denominator.

To perform an addition, you would just add the numbers, it doesn’t matter where the fractional digits begin, the result is would be the same. For the FPGA (or a CPU) its just an integer addition. Here is an example:

0011.0111 + 0001.0110 = 0100.1101 would be in decimal 3.4375 + 1.375 = 4.8125 or (55 + 22) / 16 = 77 / 16. It’s the same when working with signed values as it’s a two’s complement [wiki].

Multiplications are a bit different:

0000.1000 * 0010.1100 = (8 / 16) * (44 / 16) = (8 * 44) / (16 * 16) = (8 * 44 / 16) / 16 = 22 / 16 = 1.375

As we can see, now it matters the number of fractional bits. So, to perform multiplications you treat the fixed-point numbers as integers (8 and 44 in the previous example) but after multiplying you must divide by 2 to the power the number of fractional bits (2**4 in the previous example).

It may look as if divisions and multiplications by powers of 2 are computationally expensive, but they are not, you can perform them using logical [9] or arithmetic shifts [10], which are bitwise operations that shifts the bits of the operand.

Subtractions and divisions are not much different to additions and multiplications, but other operations, such as logarithms, powers or trigonometric functions are much harder to compute.

# 3 FPGA DAC

To draw in the oscilloscope display, I need 2 analog signals (or 3 if I want to use the “Z-axis”). Since the FPGA does not come with a DAC (although it comes with an ADC) I needed an external DAC. Probably the simplest solution would have been to use a DAC IC, but that would've been boring, so I decided to build my own DAC. There are a couple of ways to do this, but I decided to build a ΔΣ DAC [11], [12] . The principles of ΔΣ ADCs and DACs are the same. A continuous signal is converted (usually) to a 1-bit signal at a much higher sampling rate. The oversampled digital signal contains the original signal and a lot of high frequency noise, as result of noise shaping [13]. This digital signal signal can then be low-pass filtered to remove the noise and recover the original signal.

I implemented the modulator like this:

Through algorithms I generate two fixed-point values per clock tick, these values correspond to the X and Y axis of the electron beam position. Each of the values is fed into their own ΔΣ DAC and the output into the oscilloscope.Most of the ΔΣ DAC components run in the FPGA fabric, with the exception being the low-pass filter (ΔΣ ADCs are the other way around).

ΔΣ modulation can be implemented in many ways, but it’s a good practice to start simple and increase complexity as needed. I decided to use a first order ΔΣ modulator with 1-bit output and an RC low-pass filter. The sampling frequency and RC values will be tuned later as needed and depending on the FPGA design.

The Verilog ΔΣ implementation is very simple:

module DeltaSigma(clk, in, out); parameter bits = 16; input wire clk; input wire [bits - 1 : 0] in; output reg out; reg [bits : 0] sum = 0; always @(posedge clk) begin sum = sum + in; if (sum[bits]) begin sum[bits] = 0; out = 1; end else out = 0; end endmodule

To test it, I built a triangle wave generator:

module Triangle(clk, phase, wave); parameter phaseBits = 12; parameter waveBits = 12; input wire clk; input wire [phaseBits - 1 : 0] phase; output reg [waveBits - 1 : 0] wave = 0; always @(posedge clk) begin if (phaseBits <= waveBits + 1) wave = phase[phaseBits - 3 : 0] << (waveBits - phaseBits + 1); else wave = phase[phaseBits - 3 : 0] >> (phaseBits - waveBits - 1); case (phase[phaseBits - 1 : phaseBits - 2]) 2'b00: wave = wave | (1 << (waveBits - 1)); 2'b01: wave = wave ^ {(waveBits - 1){1'b1}} | (1 << (waveBits - 1)); 2'b10: wave = wave ^ {(waveBits - 1){1'b1}}; endcase end endmodule

The code (made available in [14]) produced the following 1 kHz waveform in my scope:

Note that the waveform spikes are a bit round, this occurs because the low-pass filter bandwidth is not high enough and reduces the high frequency harmonics of the waveform.

# 4 FPGA ODE solving

[TODO]

# 5 References

[2] Dalby Datormuseum: Tektronix 4051

[5] Wikipedia: Floating-point Arithmetic

[6] Wikipedia: Fixed-point Arithmetic

[8] Wikipedia: Q (number format)

[10] Wikipedia: Arithmetic Shift

[11] Wikipedia: Delta-sigma modulation

## Comments