element14 Community
element14 Community
    Register Log In
  • Site
  • Search
  • Log In Register
  • About Us
  • Community Hub
    Community Hub
    • What's New on element14
    • Feedback and Support
    • Benefits of Membership
    • Personal Blogs
    • Members Area
    • Achievement Levels
  • Learn
    Learn
    • Ask an Expert
    • eBooks
    • element14 presents
    • Learning Center
    • Tech Spotlight
    • STEM Academy
    • Webinars, Training and Events
    • Learning Groups
  • Technologies
    Technologies
    • 3D Printing
    • FPGA
    • Industrial Automation
    • Internet of Things
    • Power & Energy
    • Sensors
    • Technology Groups
  • Challenges & Projects
    Challenges & Projects
    • Design Challenges
    • element14 presents Projects
    • Project14
    • Arduino Projects
    • Raspberry Pi Projects
    • Project Groups
  • Products
    Products
    • Arduino
    • Avnet Boards Community
    • Dev Tools
    • Manufacturers
    • Multicomp Pro
    • Product Groups
    • Raspberry Pi
    • RoadTests & Reviews
  • Store
    Store
    • Visit Your Store
    • Choose another store...
      • Europe
      •  Austria (German)
      •  Belgium (Dutch, French)
      •  Bulgaria (Bulgarian)
      •  Czech Republic (Czech)
      •  Denmark (Danish)
      •  Estonia (Estonian)
      •  Finland (Finnish)
      •  France (French)
      •  Germany (German)
      •  Hungary (Hungarian)
      •  Ireland
      •  Israel
      •  Italy (Italian)
      •  Latvia (Latvian)
      •  
      •  Lithuania (Lithuanian)
      •  Netherlands (Dutch)
      •  Norway (Norwegian)
      •  Poland (Polish)
      •  Portugal (Portuguese)
      •  Romania (Romanian)
      •  Russia (Russian)
      •  Slovakia (Slovak)
      •  Slovenia (Slovenian)
      •  Spain (Spanish)
      •  Sweden (Swedish)
      •  Switzerland(German, French)
      •  Turkey (Turkish)
      •  United Kingdom
      • Asia Pacific
      •  Australia
      •  China
      •  Hong Kong
      •  India
      •  Korea (Korean)
      •  Malaysia
      •  New Zealand
      •  Philippines
      •  Singapore
      •  Taiwan
      •  Thailand (Thai)
      • Americas
      •  Brazil (Portuguese)
      •  Canada
      •  Mexico (Spanish)
      •  United States
      Can't find the country/region you're looking for? Visit our export site or find a local distributor.
  • Translate
  • Profile
  • Settings
Vision Thing
  • Challenges & Projects
  • Project14
  • Vision Thing
  • More
  • Cancel
Vision Thing
Blog Vector Display GPU Project
  • Blog
  • Forum
  • Documents
  • Events
  • Polls
  • Files
  • Members
  • Mentions
  • Sub-Groups
  • Tags
  • More
  • Cancel
  • New
Join Vision Thing to participate - click to join for free!
  • Share
  • More
  • Cancel
Group Actions
  • Group RSS
  • More
  • Cancel
Engagement
  • Author Author: neuromodulator
  • Date Created: 29 Sep 2019 4:51 AM Date Created
  • Views 4841 views
  • Likes 20 likes
  • Comments 21 comments
  • visionthingch
Related
Recommended

Vector Display GPU Project

neuromodulator
neuromodulator
29 Sep 2019
image

Vision Thing

Enter Your Project for a chance to win an Oscilloscope Grand Prize Package for the Most Creative Vision Thing Project!

Back to The Project14 homepage image

Project14 Home
Monthly Themes
Monthly Theme Poll

  • 1 Introduction
  • 2 Fixed Point Numbers
  • 3 FPGA DAC
  • 4 FPGA ODE Solving
    • 4.1 An Introduction to the Lorenz System
    • 4.2 2D Lorenz System FPGA Implementation
    • 4.3 3D Lorenz System FPGA Implementation
    • 4.4 Bonus Track
  • 5 Final Words
  • 6 References

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]:

 

image

[Dalby Datormuseum]

 

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

 

image

 

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:

 

image

[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:

 

image

 

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:

 

image

 

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

 

4.1 An Introduction to the Lorenz System

 

Having the ΔΣ DAC properly working I decided to go for something more challenging. Lissajous curves [15] have already been shown too many times on oscilloscopes, so I decided to solve ordinary differential equations [16] in the FPGA. One interesting ODE system is the Lorenz system [17], developed by Edward Lorenz while studying atmospheric convection, which can displays chaotic behaviour [18] with certain parameter values. The ODE system is defined as:

 

 

Where σ, β and ρ are the system parameters. Before writing Verilog code, I empirically explored how the system behaves. I set the system parameter to values that are known to produce chaotic behaviour (σ = 10, β = 8/3, ρ = 28), and recorded for a long time span the solution trajectories to empirically measure the position and radius of the smallest sphere that encloses them. The sphere was found to be approximately at (0, 0, 24.5) with an approximate radius of 34. I wrote a small Python program to plot the attractor from 3 different viewpoints and get a rough idea on what to expect from the FPGA implementation:

 

import sys
import numpy
import scipy.integrate as integrate
import matplotlib.pyplot as pyplot

sigma = 10.
rho   = 28.
beta  = 8./3.

dt = 0.00001
arraySize = 100000
loopIterations = 50

resolution = 1920
gamma = 0.25


def lorenz(xyz, t):
    x, y, z = xyz
    x_dot = sigma * (y - x)
    y_dot = x * rho - x * z - y
    z_dot = x * y - beta * z
    return [x_dot, y_dot, z_dot]


imageXY = numpy.zeros((resolution, resolution))
imageYZ = numpy.zeros((resolution, resolution))
imageZX = numpy.zeros((resolution, resolution))

t = numpy.arange(arraySize) * dt

initial = [8, 8, 23]
solution = integrate.odeint(lorenz, initial, t)
initial = list(solution[-1, :])

for i in range(loopIterations):
    print('.', end = '')
    sys.stdout.flush()

    solution = integrate.odeint(lorenz, initial, t)[1:]
    initial = list(solution[-1, :])

    x = (( solution[:, 0]         * 0.015 + 0.5) * resolution)
    y = (( solution[:, 1]         * 0.015 + 0.5) * resolution)
    z = (((solution[:, 2] - 23.5) * 0.015 + 0.5) * resolution)

    for i in range(arraySize - 1):
        xi = int(x[i])
        yi = int(y[i])
        zi = int(z[i])

        imageXY[yi, xi] = imageXY[yi, xi] + 1;
        imageYZ[zi, yi] = imageYZ[zi, yi] + 1;
        imageZX[xi, zi] = imageZX[xi, zi] + 1;

imageXY = imageXY ** gamma
imageYZ = imageYZ ** gamma
imageZX = imageZX ** gamma

colorMap = pyplot.cm.afmhot

normalizeXY = pyplot.Normalize(vmin = imageXY.min(), vmax = imageXY.max())
normalizeYZ = pyplot.Normalize(vmin = imageYZ.min(), vmax = imageYZ.max())
normalizeZX = pyplot.Normalize(vmin = imageZX.min(), vmax = imageZX.max())

rgbImageXY = colorMap(normalizeXY(imageXY))
rgbImageYZ = colorMap(normalizeYZ(imageYZ))
rgbImageZX = colorMap(normalizeZX(imageZX))

pyplot.imsave('LorenzXY.png', rgbImageXY, origin = 'lower')
pyplot.imsave('LorenzYZ.png', rgbImageYZ, origin = 'lower')
pyplot.imsave('LorenzZX.png', rgbImageZX, origin = 'lower')

 

 

And the code output is:

 

imageimageimage

 

 

4.2 2D Lorenz System FPGA Implementation

 

The Verilog implemention uses 4 modules: Lorenz, Translate2, Scale2 and DeltaSigma, as it can be seen in the schematic:

 

image

 

I used the 12 MHz clock in the whole design. The Lorenz module uses Euler's method [19] to solve the ODE. It takes a dt parameter and outputs a 3D vector. Internally the Lorenz modules uses Q6.25 fixed point numbers and the parameters σ = 10, β = 8/3 and ρ = 28. I only take the X and Z components of the 3D vector and linearly transform them in 3 steps. First the 2D (X, Z) trajectories are centred to the origin (0,0) (translate1), then they are scaled (scale) so that the components of the vector fall within -0.5 and 0.5, and finally translated (translate2) so that they fall within 0 and 1. The linear transformations were implemented with a focus on simplicity and tweaking, if FPGA resources were limited, the transformations could have been implemented in a single step or with transformation matrices [20]. Finally the output of of the linear transformations is fed into two ΔΣ modulators which are connected to two RC low-pass filters.

 

This is the code (also made available in [21]) :

module Waveform(clk, btn, led, rgb, pinX, pinY);
    localparam integerBits    = 6;
    localparam fractionBits   = 25;
    localparam totalBits      = 1 + integerBits + fractionBits;
    localparam dtBits         = 20;
    localparam dtShift        = 32;
    localparam deltaSigmaBits = 16;
    localparam sigma = $rtoi(       10.0 * (2.0 ** fractionBits));
    localparam beta  = $rtoi((8.0 / 3.0) * (2.0 ** fractionBits));
    localparam rho   = $rtoi(       28.0 * (2.0 ** fractionBits));
   
    input wire clk;
    input wire [1 : 0] btn;
    output reg [3 : 0] led = 4'b0000;
    output reg [2 : 0] rgb = 3'b111;
    output wire pinX;
    output wire pinY;
   
    reg  signed [dtBits - 1 : 0] dt = 0.0001 * (2.0 ** dtShift);
   
    wire signed [totalBits - 1 : 0] x0;
    wire signed [totalBits - 1 : 0] y0;
    wire signed [totalBits - 1 : 0] x1;
    wire signed [totalBits - 1 : 0] y1;
    wire signed [totalBits - 1 : 0] x2;
    wire signed [totalBits - 1 : 0] y2;
    wire signed [totalBits - 1 : 0] x3;
    wire signed [totalBits - 1 : 0] y3;
   
    Lorenz #
    (
        .integerBits(integerBits),
        .fractionBits(fractionBits),
        .dtBits(dtBits),
        .dtShift(dtShift),
        .sigma(sigma),
        .beta(beta),
        .rho(rho)
    ) lorenz(.clk(clk), .dt(dt), .x(x0), .y(), .z(y0));

    Translate2 #(.bits(totalBits)) translate1(clk, x0, y0, 0, $rtoi(-23.5 * (2.0 ** fractionBits)), x1, y1);
    Scale2 #(.integerBits(integerBits), .fractionBits(fractionBits)) scale(clk, x1, y1, $rtoi(0.015 * (2.0 ** fractionBits)), $rtoi(0.015 * (2.0 ** fractionBits)), x2, y2);
    Translate2 #(.bits(totalBits)) translate2(clk, x2, y2, $rtoi(0.5 * (2.0 ** fractionBits)), $rtoi(0.5 * (2.0 ** fractionBits)), x3, y3);

    DeltaSigma #(.bits(deltaSigmaBits)) deltaSigmaX(clk, x3[totalBits - 1 : fractionBits - deltaSigmaBits], pinX);
    DeltaSigma #(.bits(deltaSigmaBits)) deltaSigmaY(clk, y3[totalBits - 1 : fractionBits - deltaSigmaBits], pinY);
endmodule


module Lorenz(clk, dt, x, y, z);
    parameter integerBits  = 6;
    parameter fractionBits = 25;
    parameter dtBits       = 16;
    parameter dtShift      = 32;
    parameter signed [integerBits + fractionBits : 0] sigma  =        10.0 * (2.0 ** fractionBits);
    parameter signed [integerBits + fractionBits : 0] beta   = (8.0 / 3.0) * (2.0 ** fractionBits);
    parameter signed [integerBits + fractionBits : 0] rho    =        28.0 * (2.0 ** fractionBits);
   
    localparam totalBits = 1 + integerBits + fractionBits;
   
    input  wire clk;
    input  wire signed [dtBits    - 1 : 0] dt;
    output reg  signed [totalBits - 1 : 0] x =  8.00 * (2.0 ** fractionBits);
    output reg  signed [totalBits - 1 : 0] y =  8.00 * (2.0 ** fractionBits);
    output reg  signed [totalBits - 1 : 0] z = 27.00 * (2.0 ** fractionBits);
   
    reg signed [totalBits * 2 - 1 : 0] dxdt = 0;
    reg signed [totalBits * 2 - 1 : 0] dydt = 0;
    reg signed [totalBits * 2 - 1 : 0] dzdt = 0;

    always @(posedge clk)
    begin
        dxdt = (sigma * (y - x)) >>> fractionBits;
        dydt = ((x * (rho - z)) >>> fractionBits) - y;
        dzdt = (x * y - beta * z) >>> fractionBits;

        x = x + ((dxdt * dt) >>> dtShift);
        y = y + ((dydt * dt) >>> dtShift);
        z = z + ((dzdt * dt) >>> dtShift);
    end
endmodule


module Scale2(clk, xIn, yIn, xScale, yScale, xOut, yOut);
    parameter integerBits   = 6;
    parameter fractionBits  = 25;

    localparam totalBits = 1 + integerBits + fractionBits;
    localparam multiplicationBits = totalBits + fractionBits;
   
    input  wire clk;
    input  wire signed [totalBits - 1 : 0] xIn;
    input  wire signed [totalBits - 1 : 0] yIn;
    input  wire signed [totalBits - 1 : 0] xScale;
    input  wire signed [totalBits - 1 : 0] yScale;
    output reg  signed [totalBits - 1 : 0] xOut = 0;
    output reg  signed [totalBits - 1 : 0] yOut = 0;
   
    wire signed [multiplicationBits - 1 : 0] x = (xIn * xScale) >>> fractionBits;
    wire signed [multiplicationBits - 1 : 0] y = (yIn * yScale) >>> fractionBits;

    always @(posedge clk)
    begin
        xOut <= x;
        yOut <= y;
    end
endmodule


module Translate2(clk, xIn, yIn, xTranslation, yTranslation, xOut, yOut);
    parameter bits = 32;
   
    input  wire clk;
    input  wire signed [bits - 1 : 0] xIn;
    input  wire signed [bits - 1 : 0] yIn;
    input  wire signed [bits - 1 : 0] xTranslation;
    input  wire signed [bits - 1 : 0] yTranslation;
    output reg  signed [bits - 1 : 0] xOut = 0;
    output reg  signed [bits - 1 : 0] yOut = 0;

    always @(posedge clk)
    begin
        xOut <= xIn + xTranslation;
        yOut <= yIn + yTranslation;
    end
endmodule


module DeltaSigma(clk, in, out);
    parameter bits = 32;

    input wire clk;
    input wire [bits - 1 : 0] in;
    output reg out = 0;
   
    reg [bits : 0] sum = 0;
   
    always @(posedge clk)
    begin
        sum = sum + in;
        out = sum[bits];
        sum[bits] = 0;
    end
endmodule

 

And here is the video of the output:

 

You don't have permission to edit metadata of this video.
Edit media
x
image
Upload Preview
image

 

 

4.3 3D Lorenz System FPGA Implementation

 

The previous design shows the Lorenz system trajectory, but the system evolves too fast to actually let us to appreciate its evolution. Moreover, using a fixed viewpoint it is not possible to appreciate the 3D shape of the trajectories. These two issues are what I tried to solve next. A naive approach to solve the first issue would be to reduce the dt, so that the system evolves slower, the problem of this approach is that it would make trajectories shorter or even reduce them to a single point, as there would be no phosphor persistence [22] or persistence of vision [23]. I solved this by redrawing the trajectory multiple times and slowly advancing the initial position of the trajectory on each redraw. The second issue was solved by continuously 3D rotating the trajectories.

 

The design uses the modules ClockWizard (by Xilinx), Lorenz (an updated version), Translate3, Scale3, SinCos, Rotate2, Translate2 and DeltaSigma:

 

image

 

 

ClockWizard is used to generate two clock signals, one at 25 MHz (clkSlow), and another at 250 MHz (clkFast), the slow signal is used everywhere except in the delta sigma modulator. The updated version of the Lorenz module redraws the trajectory once every 2^18 clkSlow oscillations, and at every redraw, the trajectory advances . It is also worth noting that this time we use all 3 components of the 3D vector. Analog to the 2D Lorenz system, the 3D vector is first translated (translate1) and then scaled (scale), but before getting to the final translation (translate2) I perform to rotations around 2 axis. The rotations angles vary continuously, depending on the values of the xyPhase and yzPhase accumulators. These values are fed into two SinCos modules, that uses a look-up table and a linear interpolation to compute the sine and cosine of the angle. the sine and cosine values are used by the Rotate2 modules to perform the rotations. Finally, just like in the 2D Lorenz system design, the X, and Y values are fed into delta sigma modulators connected to low-pass filters which are probed by the oscilloscope.

 

Code is available in Github ([24]), and this is the video of the output:

 

You don't have permission to edit metadata of this video.
Edit media
x
image
Upload Preview
image
    
You don't have permission to edit metadata of this video.
Edit media
x
image
Upload Preview
image

 

 

4.4 Bonus Track

 

My initial plan for this project was to use a Zinq to perform high level operations in the programmable system (PS), while leaving linear transformations, interpolations and ΔΣ modulation to the programmable logic (PL). I didn't have a Zinq so I applied to the path II programmable program and to a Minized giveaway at that time, I missed both chances of getting a Zinq so I decided to stick to my Cmod S7 and explore what I could do with it. Next I'll show you what I could not do with the little FPGA, but I could using two different suboptimal approaches. So what did I do? I played a video on the oscilloscope! I didn't turn the oscilloscope into a CRT TV (which has already been done many times), but converted the video to a set of contour lines which the oscilloscope could draw in the XY mode.

 

The process can be separated in two phases, video preprocessing and video display. The preprocessing of the video itself can also be separated into multiple phases. First I conditioned the image for edge detection, then I applied a Canny edge dectector [25] (all this was done using OpenCV). The painful part came next, which was the conversion of the edge raster image into a set of lines which the oscilloscope could draw. The optimal electron gun track that reduces the travelled distance needed to draw all lines is computationally very expensive, so I applied a lot of heuristics to get an acceptable result. The processes begins with the segmentation of the edge image into blocks of contiguous pixels, for each of these blocks I tried to extract the longest line that I could make out of its pixels, and repeated that until no more pixels were left. These lines represent edges of a graph and are used to build a single track for the electron gun to travel.

 

Tracks had on average of ~8k points (or pixels), but varied a lot between frames, so in some way this had to be compensated so that every frame takes the same amount of time to draw. The quickest way to see the result was to generate a stereo audio signal and feed it into the oscilloscope:

 

You don't have permission to edit metadata of this video.
Edit media
x
image
Upload Preview
image

Vectorization of Pink Floyd's "What shall we do now?" played on sound card

 

The sound card DAC has high resolution (usually 24 bit), but its low ~22 KHz bandwidth and inability to pass low frequency make it less than ideal. I found that I need a refresh rate of ~40 Hz to produce a flicker-free (to the eye) image on the oscilloscope, but too reach such refresh rate I need to drop the details of the image, on the other hand keeping them generates a notorious flicker. The inability to pass low frequencies produces a constant jiggling of the image, which was specially notorious at the end of the video.

 

I have an ESP32, which comes with two 8-bit DACs that don't have the bandwidth and band-pass limitations of the sound card, so I decided to give it a try. As the ESP32 can't keep in memory the complete video sequence, I decided to feed each frame to the MCU through its serial port (set to 921600 baud), but this is still is not fast enough for real-time video rendering. The ESP32 program uses double buffering, while one buffer is being displayed, the other is receiving the next "frame" track through the serial port. The accelerated the playback of the video looks like this:

 

You don't have permission to edit metadata of this video.
Edit media
x
image
Upload Preview
image

Vectorization of Pink Floyd's "What shall we do now?" played on ESP32

 

The low 8-bit resolution is quite noticeable, and a low-pass filter can reduce the pixelation of lines, but it also makes long distance electron gun "jumps" more visible. The point glows occur when the ESP32 receives data from the serial port, which I suspect may be avoidable using one core for the DAC and another for serial communication.

 

At this points it may be obvious the reason why the little Cmod S7 could not be used here, and that reason is memory! Which does not mean it can't be solved in some way, like by adding SRAM, but I would have probably missed the deadline if I tried that...

 

 

5 Final Words

 

I showed a couple of techniques that I hope others interested in FPGA would find useful. I showed how to build a low-cost ΔΣ DAC, that just requires a single pin, a ground and a low-pass filter. I also showed how fixed point numbers can be used to perform real number operations, such as computing the sine and cosine, 3D linear transformations such as rotations, translations and scaling, and also ODE solving through Euler's method. And maybe more important, I showed how we can use all these techniques to 3D render a Lorenz attractor on a CRT oscilloscope. As an alternative to the Zinq I used a sound card and an ESP32 to trace the contours of a video, most of the time was spent coding the electron gun track generator, and to a lesser extent coding the ESP32 and PC that fed the ESP32 with the frame tracks.

 

In the FPGA projects most of the time was spent on debugging, so it's worth mentioning how it was perfomed. Simple bugs just required me to simulate a few time steps of the top module as these bugs didn't depend on any particular state of the Lorenz system and could be detected at any moment of the simulation. More complex bugs were related to specific states of the system, to find them I had to simulate a few milliseconds, or in the worst case up to a few seconds (which of course took several minutes in the simulator). Bugs only in very few cases could be found by checking the simulator waveforms, in most cases I had to store them in a (very large) file and then analyze them in Python. I coded many small programs to catch different particular bugs I faced during development, but I also coded a Python version of the Verilog code that I could use to test as much as possible in Python without having to simulate or program the FPGA, and a "software oscilloscope", that is, a program that would read the simulated output and generate the oscilloscope image. As you may guess debugging was very time consuming!

 

During the development there was one particular bug that took me ages to find, and the reason for that was that it worked perfectly fine on the simulator but failed on the FPGA. To find the bug I began removing parts of the code to see if what was left worked as it should, it happened that I ended up removing alsmot everything, and the bug didn't dissapear. I thought maybe it it had to do with some obscure timing issue, but I couldn't find anything to blame for timing bugs. This was of course very frustrating, but at some point I found a minor difference between my buggy code and another Lorenz Attractor I wrote some time ago that just worked. That minor difference caused the simulator to run just fine, but fail on the FPGA. I found that Vivado's simulator and synthesizer Verilog implementation differ! Letme show you what it found with an example:

 

The code:

 

reg [7 : 0] x = 123.456;

 

will set x to 123 on the simulator and synthesizer, but:

 

wire [7 : 0] x;
Increment inc(clk, 123.456, x);

module Increment(clk, xIn, xOut);
    input  wire clk;
    input  wire [7 : 0] xIn;
    output  reg [7 : 0] xOut;

    always @(posedge clk)
    begin
        xOut = xIn + 1;
    end
endmodule

 

will set x to 123 on the simulator, and to 119 on the synthesizer. What the 19 is, I don't know, but its likely some binary real number representation.

 

I have found other differences between the simulator and synthesizer Verilog implementations, for instance, in the last example if we change line two to "Increment (clk, 123.456, x);", the simulator will complain, but the synthesizer won't! So the message to anyone interested in developing on FPGAs is it is hard and time consuming!

 

There many other variations I thought about exploring during the development, such as adding perspective projection, affecting the animation with sound (music) or controlling the animation through a softcore CPU, but what I showed you here is what I managed to do for this Project 14.

 

 

6 References

 

[1] Wikipedia: Vector Monitor

[2] Dalby Datormuseum: Tektronix 4051

[3] Youtube: Tektronix 4051

[4] Wikipedia: Laser scanning

[5] Wikipedia: Floating-point Arithmetic

[6] Wikipedia: Fixed-point Arithmetic

[7] Wikipedia: IEEE_754

[8] Wikipedia: Q (number format)

[9] Wikipedia: Logical Shift

[10] Wikipedia: Arithmetic Shift

[11] Wikipedia: Delta-sigma Modulation

[12] Best of Baker's Best Delta-Sigma ADCs

[13] Wikipedia: Noise shaping

[14] Github: Delta-Sigma Triangle Waveform Generator

[15] Wikipedia: Lissajous Curve

[16] Wikipedia: Ordinary Differential Equation

[17] Wikipedia: Lorenz System

[18] Wikipedia: Chaos Theory

[19] Wikipedia: Euler's Method

[20] Wikipedia: Transformation Matrix

[21] Github: 2D-Lorenz

[22] Wikipedia: Phosphor Persistence

[23] Wikipedia: Persistence of Vision

[24] Github: 3D-Lorenz

[25] Wikipedia: Canny Edge Detector

  • Sign in to reply

Top Comments

  • DAB
    DAB over 5 years ago +4
    Your project brings back many memoires. I first encountered vector displays back in the mid 1970's. We had a lot of fun learning how to control them to put up clear displays. In many ways they were simpler…
  • jc2048
    jc2048 over 5 years ago in reply to clem57 +4
    History. This is the first microprocessor I ever owned.
  • dubbie
    dubbie over 5 years ago in reply to jc2048 +4
    Well, I do not have the first microprocessor I ever used but it was the 8085 in an SDK board. Programmed in machine code as far as I remember, hexadecimal machine code entry, 7 segment LED display output…
Parents
  • thehermit
    thehermit over 4 years ago

    OMG!!! Ended up here after watching your Music Player w/controls video on YT. Can't believe what I've just seen.

    In 1978, I was doing Laserium shows in London, until I went freelance. Throughout the 1980's, I was developing laser projection systems for exhibitions and touring w/bands. My forte was developing the projection systems and shows, while my partner(s) did the analog, 6502, or 8088 electronics for the control desk.

    Initially, we used crude analog quadrature oscillators w/preamp mixers. Then, we wire wrapped 8 bit digital boards, etc. before progressing on to acid etched PCBs. Back then, I was dreaming of animating line graphics via interpolation between sequences, just as you have demonstrated.

    Now, I'm retired and becoming a maker, seriously considering building a vector system with some Teensy MCU quad oscillators, inexpensive RGB diode lasers, and an x/y scanning head. With today's prices and technology, a full color, single head laser projector could be built for <$1000 instead of ~$25,000 back in the day.

    Sure wish I had your vector program when I auditioned for Floyd's Division Bell tour back in 1987. Oh well...

    Congratulations on your awesome work. I'll be back.

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
Comment
  • thehermit
    thehermit over 4 years ago

    OMG!!! Ended up here after watching your Music Player w/controls video on YT. Can't believe what I've just seen.

    In 1978, I was doing Laserium shows in London, until I went freelance. Throughout the 1980's, I was developing laser projection systems for exhibitions and touring w/bands. My forte was developing the projection systems and shows, while my partner(s) did the analog, 6502, or 8088 electronics for the control desk.

    Initially, we used crude analog quadrature oscillators w/preamp mixers. Then, we wire wrapped 8 bit digital boards, etc. before progressing on to acid etched PCBs. Back then, I was dreaming of animating line graphics via interpolation between sequences, just as you have demonstrated.

    Now, I'm retired and becoming a maker, seriously considering building a vector system with some Teensy MCU quad oscillators, inexpensive RGB diode lasers, and an x/y scanning head. With today's prices and technology, a full color, single head laser projector could be built for <$1000 instead of ~$25,000 back in the day.

    Sure wish I had your vector program when I auditioned for Floyd's Division Bell tour back in 1987. Oh well...

    Congratulations on your awesome work. I'll be back.

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
Children
  • neuromodulator
    neuromodulator over 4 years ago in reply to thehermit

    I really appreciate your kind words! And thanks for sharing your story! I imagine you had a great time developing laser shows and touring with bands...

     

    Lasers have definitely dropped their price, and probably galvanometers as well. I've seen a couple of DIY RGB laser and laser show projects in the net, but eventually buying an existing solution might be as effective (although not as fun if you like to build hardware). If you have any blogs or online publications about your laser projects I would love to read them.

     

    Paul Horowitz' build a lorenz attractor -- electronic circuit -- chaos inspired me to try this project. I was learning how to program FPGA's with HDL and thought that implementing an ODE solver would be interesting. I also was interested in testing how good an FPGA DAC (lowpass filtered digital signal) would be to generate a steady image on a scope.

     

    I used the oscilloscope as I didn't have a laser show or the components to build one, but lasers would have been definitely more fun.

     

    There is another project that I did with DVD lasers using a completely different technique, that you may find interesting: Caustic XMAS

    • Cancel
    • Vote Up +1 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • thehermit
    thehermit over 4 years ago in reply to neuromodulator

    Thanks for your quick reply. Yes, doing laser shows were lots of fun, but also lots of work and regulatory hassles (pre-show safety checks with local authorities, who knew nothing about lasers). Class A lasers had to be mounted on 10 ft high scaffolding. No scanning into audiences. Scan fail cut-offs. Dozens of mirrors in 20 ft high lighting trusses and balconies, which needed to be accurately 'focused'. Water cooled krypton and/or argon gas ion lasers required 30 gallons per minute, with pumps and hoses. Hand blown glass tubes could freeze and break during transit, unless the water was completely blown out after each show. They also required 30-60 amps of 3 phase @ 440 VAC. Heavy transformers, cables, & road cases. The overall costs of a show system was ~ $50k-$75K. All of that for a total show time of ~ 5 minutes. Then pack it all up, sleep on a bus overnight (with 15 other road techs), and do it all again 6 days a week. Everything had to happen like clock work. I would describe it as more of a stress filled challenge. Not recommended for whiners nor old farts. lol

    Yes, today is a very different story. Found these RGB 'show' lasers on Amazon https://www.amazon.com/dp/B07195BNXF/?coliid=I13BE695OAIGJQ&colid=2RGBNFS5YN133&psc=1&ref_=lv_ov_lig_dp_it

    also some inexpensive galvanometers, complete with x/y mounts and scan amps. They blow away the $1000 1kHz freq @ 15 deg deflection open feedback scanners that were available back in the day

    https://www.amazon.com/dp/B085P1619X/?coliid=I18KC6XP5S2JVR&colid=2RGBNFS5YN133&psc=1&ref_=lv_ov_lig_dp_it

    Perhaps these links will be helpful for your audience to overcome the price barriers of taking your info to the next level.

    But nowadays, I'm more interested in using these to develop my own controller than playing with someone else's commercial toy. It's all about the technical challenge and creative expression. Although I'm 68 years old, I've never lost the enthusiasm. So, I'm up to my ears in RPi, CM4, ESP32, PICO, uPython, IoT, cameras, & etc.

    Your article(s) will be very helpful in realizing those endeavors.

     

    Thank you for sharing. :-)

    • Cancel
    • Vote Up +1 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • neuromodulator
    neuromodulator over 4 years ago in reply to thehermit

    There is a lot of online material, and many communities to learn how others develop different projects. Element14 is very friendly and they give out all kind of hardware as either, price rewards, to participate in some competitions, or to review the hardware. I'm not aware of other communities were so much hardware is given. Then there is Hackster, with a stronger focus on sharing projects, and some competitions were they give out hardware and winners get very nice prices. Then you have EEVBLOG which has what I would say the biggest forum on electronics, the people there are very technically knowledgeable, but not that friendly. Then there are other pages like hackaday.io which I haven't explored much on how it works, but is full of very interesting projects. So if you haven't already explored those pages, you will have a great time checking all the cool stuff that other people do.

    • Cancel
    • Vote Up +1 Vote Down
    • Sign in to reply
    • More
    • Cancel
element14 Community

element14 is the first online community specifically for engineers. Connect with your peers and get expert answers to your questions.

  • Members
  • Learn
  • Technologies
  • Challenges & Projects
  • Products
  • Store
  • About Us
  • Feedback & Support
  • FAQs
  • Terms of Use
  • Privacy Policy
  • Legal and Copyright Notices
  • Sitemap
  • Cookies

An Avnet Company © 2025 Premier Farnell Limited. All Rights Reserved.

Premier Farnell Ltd, registered in England and Wales (no 00876412), registered office: Farnell House, Forge Lane, Leeds LS12 2NE.

ICP 备案号 10220084.

Follow element14

  • X
  • Facebook
  • linkedin
  • YouTube