Professor Mark Csele

Laser Light Show

Presented is a laser light show utilizing a ds-PIC controller as an arbitrary waveform generator (ARB). The controller allows vector patterns to be either stored in flash (program) memory or uploaded from a PC in a vector format. Analog output, via two 14-bit DACs, is fed to a commercial analog driver board and two high-speed galvo scanners.

The scanner currently resides in the laser lab at Niagara College (in V15) where it is used for both student labs as well as lab demonstrations. A portable scanner, packaged with a HeNe laser, was also created for ‘road show’ use at promotional engagements – both use the same hardware and software.

The splash logo from the system. This image, composed of multiple, unretouched, photos of actual scanner output is made possible by resizing the image in the horizontal direction only. To mirror the image, X coordinates are simply multiplied by a negative number. All of this is made possible by the high-speed multiplier built into the dsPIC processor chip.

The prototype controller is built onto a Microchip dsPICDEM Starter demonstration board featuring a 30F6012 processor chip. The board has a prototyping area which is populated with the DACs, two connectors (one for power and one for analog output), and voltage regulators. The board already contains basic I/O circuitry including an RS-232 interface, which simplifies the construction of the system.


The best background on laser scanning in general is the ELM Homebuilt Laser Projector Page which describes the theory of laser scanners as well as presents a homebuilt design. This site also features vector design software and example files.

In a basic light show, a beam from a laser is deflected by two galvanometer scanner heads, each of which moves a mirror to steer the beam in the horizontal and vertical direction independently. Given the high speeds at which these galvos are required to operate, they are quite pricey.

As well as the actual galvanometer movements (in my case two Cambridge 6350 moving coil closed-loop units) a driver is required which usually has feedback from the sensors. The entire system is usually a PI or PID (proportional-integral-derivative) control loop with the output fed to the galvos and feedback supplied via a capacitive sensor inside the galvo scanners themselves – you can probably appreciate the expense of the scanners owing to the fact that they contain a variable-vane capacitor which changes at most a few pF during rotation. The driver/control-loop usually accepts analog signals: in my case the GSI-Lumonics scanner board (show to the left) employed gives full deflection in one axis with an applied signal varying from -3V to +3V (it is bipolar). This board contains a plethora of high-end AD OP-Amps as well as high-current drivers. Small plug-in daughterboards are included as well with notch filters to control resonances in the scanners.

Up to this point, what is basically described is a slow X/Y oscilloscope in which two analog inputs are used to direct the beam in either direction. In my specific case, I was fortunate to have the scanners and a matching driver board available. Next, a control system is required to generate waveforms. 

The earliest laser light shows, in the 1970’s, used analog technology. Signals were generated using analog generators and often stored on multi-track magnetic tape. Consider a simple Lissajous pattern generated with two sine waves (supplied by commercial lab-type signal generators) which are out-of-phase by 90 degrees:

This pattern, a circle, may be made to rotate by simply setting the frequency of one of the sine waves to be slightly ahead of the other axis – for example using a 100Hz signal for the X-axis and a 100.1Hz for the Y-axis. Many other patterns are possible by setting the frequencies of the axes to be multiples of one another.

Add colour, the ability to offset an image (by adding a DC offset to the signal) and variable gain amplifiers, and fairly complex shows can be made as seen in this open-shutter photograph of the classic light show Laser U2, as shot by the professor in the MacMillan Planetarium, Vancouver BC, in 1988:

While Lissajous patterns are interesting and smooth, the drawing of complex graphics (including text) requires digital technology to generate the complex waveforms required.

Complex patterns are generated by vectoring: breaking long line segments into smaller segments which are scanned out via DACs at fixed speeds, e.g. 8000 points per second – with each ‘point’ consisting of an (X,Y) pair. In this manner, a straight line can be drawn by sending successive (X,Y) values to the DACs which generate corresponding voltage levels to position the mirrors.

Here, the beam from an air-cooled argon laser is deflected by two galvanometer movements, one for each direction (X and Y). An ISOMET AO modulator (the gold box in the center of the photo) is used as a blanker allowing the beam to be switched on and off rapidly (it is not properly aligned in this photo since ‘hidden’ lines between graphics elements are still visible). The vectors, produced as analog signals from two DACs on the DSP board, direct the position of the galvanometer movements.

The vector nature of the output becomes evident in this shot of the output as seen on an analog oscilloscope. When in X/Y mode, an analog scope, as opposed to a digital, shows areas where the beam stays longer as bright dots – in this case the endpoints of each vector. On the scope screen, movement between endpoints is so fast that the beam is not clearly visible between endpoints (at 8000pps, the beam dwells at individual points for 125ms). Some points visible here would not be visible in the projected image since the blanker would turn off the beam between these points.


This laser light show controller is built on a dsPICDEM Starter demonstration board which features a 30F6012 MCU and a connector for an ICD-2 debugger module. Additional required circuitry includes two 14-bit DACs – Analog Devices AD7840 parallel DACs were chosen – which are mounted on the prototyping area on the board. These DACs require bipolar (-5V and +5V) power supplies and have a full-range output swing from -3V to +3V which is a good match to the GSI-Lumonics driver board used for the galvanometers.

In order to simplify the wiring of the DACs, the fourteen data lines (D0 through D13) are wired to pins RB2 through RB15 on the 30F6012. RB0 and RB1 were unavailable since they are pre-wired on the board to the ICD-II debug connector (this was also the reasoning to use a 14-bit DAC … aside from the fact that 14 bits of resolution are more than enough for the task). Data lines on both DACs were wired in parallel with the control wiring for each DAC (/CS, /WR, and /LDAC) wired to unique I/O pins on the chip. Independent control lines allow both DACs to be updated simultaneously if required.

The DACs also require two supply voltages. +5V is derived from the on-board 7805 regulator while -5V is provided via a 7905 regulator added to the prototyping area of the demo board. The DACs themselves provide an internal 3V reference which is used for both DACs. The reference voltage is divided via a potentiometer which serves as a gain control to allow adjustment of the image size.

Aside from the DAC wiring, additional lines from the dsPIC chip are made available for blanking as well as a PCAOM (Poly-Chromatic AOM) used as a colour selector.

Two dual-inline ten-pin headers connect the board to the outside world. One is used for power – to supply the board with +12V and -12V as required for the DACs and the DSP chip – and the second connector is used for I/O. I/O includes two analog outputs from the DACs, and several digital outputs (connected to I/O pins on the chip) for blanking and PCAOM use. Grounds were separated into analog (AGND) and digital (DGND) to enhance noise immunity.


First and foremost, a storage format is required for vectors in memory. With a 16-bit word size, a single (X,Y) vector can easily be stored in two consecutive words. The format chosen was designed to allow easy playback of the stored vector via the DACs, which accept data in a two’s complement signed form – in other words the most-significant bit is the sign bit. Since the stored number is only 14-bits (13 plus the sign bit), two “unused” bits, D14 and D13, are used to store blanking and PCAOM data. To convert a 14-bit number for storage bit D14 and D13 are first cleared: the number now consists of the sign bit in D15 and the mantissa in bits D0 through D12. The two remaining bits assigned as follows: For the X value (the first word) D14 is used to indicate BLANKING (active high), while D13 (X only) and D14 and D13 (Y only) are used for PCAOM selection bits.

Most vector formats use 16-bit values (between -32768 and +32767) which are divided by four to convert to a 14-bit value. The original format chosen is a generic CSV (comma separated value) format. Utilities available from the ELM site (and a few other sites) allow conversion of ILDA frames to this format. The format is as follows:

Frame Name 

The first line, starting with the keyword VECTOR, is the header. In the header, is the scan rate in PPS – it is ignored here and is set by the system – only the keyword VECTOR is used to start a vector upload. The frame name is a text string (max 16 chars) to identify the frame – it is also ignored. Point data is in the form [v],[x],[y] where [v] is the visibility flag (1=visible, 0=blank), and [x] and [y] are X-Y vectors as 16-bit signed numbers. When converting data to a format suitable for program memory (i.e. storage in a table), only point data is used.

Now, consider the position (3666,995) from the original CSV data file which is converted to the 14-bit numbers (917,-249). The X value would be 0x394. If the vector was a blank, D14 would be set high and so the X value would be stored as 0x4394 in memory (a visible vector would have a value of 0x0394). Similarly, a position of -474 would be stored as 0x9E26 (with the beam being visible since the blanking bit, D14, is clear).

One ‘feature’ of this particular scanner is that the Y-axis scanner is mounted upside-down so that positive numbers cause the beam to deflect downward and negative numbers, upwards. Y-values, therefore, must be inverted by multiplying by -1 to invert the image. Finally, a value of (0xFFFF,0xFFFF) is used to indicate the end of a frame. To be safe, unitinialized memory can be set to this value to ensure an ‘overrun’ pointer will not read an invalid value and produce bizarre results.

A spreadsheet was used to process values. The spreadsheet divides X/Y values by four (converting to 14-bits), multiplies the Y-axis by -1 to invert it, masks the unused blanking bits (D13/14), and generates code in the form of “hword” directives for that code produced by the spreadsheet may be cut and pasted directly into the data segment of a dsPIC program in program flash memory.

For example, consider the original point (from the CSV file for the ‘Radiation’ symbol above):

  • The original CSV data point = (0,3666,995) meaning “move to point (3666,995) and BLANK the beam (the ‘0’ is visibility, or lack thereof in this case). {Put into columns A,B,C}
  • Convert the X value to a 14-bit value (divide by 4) to get 917. Convert the Y value to 14-bit as well however invert it (multiply by -1) to correct for the upside-down scanner to get -249. {E=B/4, F=C/4*-1}
  • Now, set the blanking bit ON on the X-value by adding 16384 and clear bits D13/14 on the Y value so that the point becomes (17301,-24825). {G=IF(E>=0,+E+16384-(16384*A),+E-(16384*A)-8192), H==IF(F>=0,+F,+F-16384-8192)}
  • Now, convert to hexadecimal and add formatting so that this point becomes the assembly-code “.hword 0x4394,0x9F08”.{=RIGHT(DEC2HEX(+G,4),4)}

The assembly code may be pasted directly into the data segment of the program where it will be stored in program (flash) memory and may be accessed using table pointers [e.g. mov #tbloffset(LaserRadiationSign),w5]. Several images used as ‘splash’ screens were converted in this manner and are stored permanently in program memory.

Aside from the preprogrammed frames in flash program memory, vector files may be uploaded into chip SRAM via the serial port. Upload begins with the command ‘VECTOR’ (part of the CSV file) followed by (b,x,y) pairs where b=0/1 (blanking), and x & y coordinates as per above. PSV is used to map all frames into the same memory space for easy access – only the initial pointer need be reassigned in order to change the image scanned. From the RAM frame, the data may be copied into the program flash memory using the RTSP feature which allows writes to flash memory under program control (usually used to update a program via a bootloader). Flash memory is divided into 4K blocks, each holding one frame.

The actual code to produce the image is quite simple since the values in memory are already processed. The 16-bit number (representing X/Y position) is processed to make it a true 14-bit number with the LSB in bit position 2 and the sign bit in position 15. The value is now output directly to the DAC via port lines RB2-RB15, data is latched, and the DAC is updated. Other elements of the program (which is completely interrupt-driven) include the command interpreter (which understands a few basic commands such as VECTOR to upload a frame to RAM memory, SPEED to change the scan rate, and RUN to select a frame to display), storage of incoming characters to string memory (essentially a ‘get-string’ function), and a conversion function which converts a string to a binary number (string-to-binary) to parse arguments (including comma-separated X/Y values).

The conversion function ConvertString accepts the argument of a pointer to the end of a string of ASCII numbers and returns a 16-bit number. It uses the dsp ‘mac’ instruction and the 40-bit accumulator to add each digit. The last digit (least-significant) is simply converted from ASCII to binary (subtract 0x30) and stored in the accumulator. The next digit is converted similarly and multiplied by ten before storage. the process continues with each digit multiplied by 100, 1000, etc. until a delimiter is found such as a space, comma, or negative sign prefix. The converted number, now in the accumulator, is returned from the function as a signed 2’s complement 16-bit value. This function is used extensively to convert incoming desimal values into binary numbers for storage and/or argument parsing.

The command-line interpreter accepts user commands via the serial port. Strings for comparison are stored in program memory and compared to the string in the incoming string buffer. When a string matches, a function is called to process the buffer and extract arguments as required using the ConvertString function described above.

An accurate time delay is required for the scanner since patterns are scanned-out at fixed rates of 8000, 10000, or 12000 points-per-second. The Chip is clocked at 4MHz with the PLL (*16) active for an internal clock frequency of 64MHz. For a scan rate of 8000 pps, for example, a delay of 125ms is required between points. By loading the timer with a value of 2000, a delay of 2000 times the internal instruction cycle time of 62.5ns is effected – the scan rate is changed simply by loading a new value into the PR1 register for timer 1. The scan rate may be changed using the ‘SPEED’ command via the serial port.

Enhancements: Image Manipulation

In a traditional 1970’s and 1980’s light show, many images were produced by analog oscillators and computers composed of many op-amps. Op-Amps can be used to accomplish many effects: for example, a simple lissajous pattern can be made to move in any direction by adding a DC offset by using a summing amplifier. Image size can be adjusted by using variable-gain amplifiers or attentuators. Inverting amplifiers can be used to invert an entire image about an axis …. the list is somewhat endless. It was desired to give this laser scanner similar functionality however in the digital domain.

In order to produce “analog-type” effects, several functions were added to allow the manipulation of image size, position, and rotation. Image position is changed simply by adding a number (positive or negative) to the coordinates before output by the DACs.

The basic system operates by first generating the waveform via an arbitrary-waveform generator which simply accesses stored 16-bit values in memory for the X and Y coordinates. These numbers are then multiplied by a size variable – size is multiplied by the coordinates then the final value is divided by 100 so that ‘100’ represents full size. Rotation is then accomplished by using the standard transforms X’=X*cos(Theta)-Y*sin(Theta) and Y’=X*sin(Theta)+Y*cos(Theta). Sine and Cosine coefficents are provided by 16-bit tables generated using a spreadsheet and pasted into program memory. These tables have a resolution of one degree. An offset is then added to the X and Y values to accomplish a position shift. Finally these values are sent to the 14-bit DACs.

Immediate commands (available via the console port) include many of these image manipulations as follows:

VECTOR - to upload a CSV file into the RAM frame
SPEED - to set the scanner speed in pps
RUN - to select a frame to output
SAVE - Write the RAM frame into flash memory
SIZEX / SIZEY - Image scale in percent
RESET - Resets all scaling values to default (100%), halts scripts
ROTATE - Rotates the image, argument is in degrees
POSX / POSY - The image position offset
PHASE - The shift of the Y term ahead of the X term
SCRIPT - To upload a script to the script RAM (see below)
SRUN - To start a script in memory running

Not all modern laser shows support image manipulation in this manner: most require processing of images in a PC and simply store the images as multiple frames which are played-back via DACs without further processing. The scheme used here (allowing manipulation of the images inside the scanner) allows motion to be created with only a single frame saving a large amount of memory – this was necessitated by the fact that all frames are stored in the DSP’s on-chip flash memory which is limited to 144K.


One of the most useful features for displaying a complete show is the ability to run a stored script consisting of commands which allow loading of a specific image and manipulation of that image. The script commands consist of 16-bit values of which six bits are the command (op-code) and ten bits are the argument (with a possible range of -512 to +511).

Scripts are loaded into a reserved area of memory in a manner similar to how the vector patterns are uploaded. To run a script, the ‘SRUN’ (Script RUN) command is issued on the console (serial) port which sets a global flag. The script interpreter then begins to fetch commands from the script memory area and uses, essentially, a large ‘case’ switch to select a function. There are simple functions to set the position and size variables (functions already supported via command-line) as well as support for a time delay and use of variables (four, labelled ‘A’ through ‘D’) which allows loops to be constructed in a script. A typical script looks like this:

FRAME       1
SIZEX       1   Start at size 1%
SIZEY       1	
LOADA      36   Number of iterations
LABEL       1	
DELAY      10   Delay 10ms between frame changes
ROTATEINC  10   Rotate by 10 degrees each time (*36 = 360 degrees)
SIZEXINC    3   Size grows by 3%
INCA       -1   Variable A-=1
BRNZA       1   Branch if A>0 to label 1
DELAY     500	
LOADA      36	
LABEL       2	
DELAY      10	
INCA       -1	
BRNZA       2	

This script loads frame #1 from flash memory and displays it at 1% of it’s size. The size of the image grows at the same time the image rotates. After 360 degrees rotation, the image is 90% of it’s original size. Scripts are written as per above and translated by a script compiler written in Excel. Each instruction is translated into a 16-bit value which is then uploaded to the scan engine.

It should be noted that once invoked, the script interpreter on the dsPIC runs until a DISPLAY, DELAY, or WAIT command is found at which point control is returned to the main scan routine to display a frame (displaying only one frame, after which the script interpreter is called again). The DISPLAY command displays only one frame, DELAY will continue displaying frames until a timer is complete (allowing, for example, an image to be displayed for 500ms), and WAIT will display frames until an external input goes high. WAIT is used to synchronize a script with an external event (for example, a sync signal embedded in a show – one could use the video stream from a DVD player to do this).

The full list of script commands can be found in the script compiler written in Excel. The following is an example of a few script commands – all “X” coordinate commands have a corresponding “Y” command.

FRAME     n  1  Load frame n for display
SIZEX     n  2  Resize the image in the X axis only to n %
SIZEXINC  n  3  Increment the X size variable by n
POSX      n  6  Add n to the coordinate position (allows translation)
POSXINC   n  7  Increment (by n) the X coordinate offset value
ROTATE    n 10  Rotate the image by n degrees
ROTATEINC n 11  Increment the rotation angle by n degrees
DELAY     n 12  Display the current frame for n milliseconds
LOADA     n 16  Load variable A with value n
INCA      n 20  Increment variable A by value n
BRZA      n 24  Branch to label n if variable A is zero
BRNZA     n 28  Branch to label n if variable A is not zero
LABEL     n 32  Label (i.e. address for a GOTO or BRANCH operation)
GOTO      n 33  Unconditional branch to label n
PHASE     n 34  Phase offset for Y coordinates (from X)
PHASEINC  n 35  Increment phase offset variable by n
DISPLAY     36  Display the current frame once

Putting It All Together: The Laser REM Show

At this point, the scanner was used for several years to project LASER REM to tour groups coming through the laser lab. When demonstrating our lab to tour groups, we often talk about high power lasers and show them a metal cutting laser, etc. While MAKING laser light is one part of the equation, CONTROLLING it is the other and nothing demonstrates control of laser light better than a light show like this!

Laser REM, set to the tune of REM’s Stand, was a three minute show with graphics which follow the theme of the video. A 30mW Uniphase argon laser (488nm) was projected onto a standard A/V screen mounted in the lab about 5m away from the scanner. Click on the title shot above to see the intro to the show which features the title rotating and expanding (some artifacts, such as letters which appear to blink, are caused by the shutter speed of the camera employed). It should give an idea of the type of image manipulations that this show can render (the title sequence script was quite simple consisting of SIZE and ROTATE commands within a single loop). The actual show consists of only sixteen graphics frames (all that would fit into the internal memory of the 30F chip used) and all manipulations were accomplished via the script: no true multi-frame aminations were used with this version.

Of course producing an actual show is more about art than science and so planning a show like this begins with a planning the sequence of frames required. The title “Laser REM” was obvious, but the rest?? Completing the frame sequence began with watching the video and producing frames to match the sequence of the song as follows:

Lyrics: Stand in the place where you live, now face north
Frame: A compass rose, which spins

Lyrics: If you are confused check with the sun
Frame: A sun which rotates slowly

Lyrics: Your feet are going to be on the ground
Frame: Two feet, left/right, start at the bottom of the screen and walk upwards

And so, the sequence continues. Keep in mind that only sixteen frames fit into the memory so the show must be carefully designed to take advantage of that … for example the “feet walking” sequence uses one frame starting at a position on the bottom of the screen and incrementing that position to move it forward. The same frame can be mirrored using a “SIZEX -100” command which inverts the X axis making the left foot, a right foot. Lack of animation capability (i.e. huge memory) does make the show repetitive though … something which will be fixed on an upcoming version.


The latest addition to the software is AutoVectoring: a vector-bisection algorithm which will breaks-up long vectors for successive scanning – in this manner it would be unnecessary to divide long straight lines into multiple vectors as the scanning system will do this automatically. It is possible, then, to draw a square simply by defining four points (the corners) … the algorithm will evaluate the length of each vector and, assuming it exceeds a threshold value, begin disecting it into 2,3,4, or more parts as necessary to keep each vector element small enough to scan properly (i.e. without overshoot).

A simple square, drawn with only four points, is seen to the left. While the points may resemble a square on the XY oscilloscope display, on a real laser scanner (with mirrors having mass and inertia), the beam will overshoot massively at each corner. With AutoVector on, each long vector is split into sixteen smaller vectors as seen to the right.

An inadequately vectored square shows overshoot at the corners. Large vectors allow mirrors to achieve uncontrollable velocities such that they cannot stop at the desired position and overshoot, rebounding back, as seen here. 

The use of a DSP processor is appreciated here since the chip has features allowing simple scaling, squaring, and square-root determination. In order to determine the length of a vector, for example, the difference in the x-values (between the current and the target positions) is computed and squared, the difference in the y-values is similarly computed and squared, and these values are summed (using a MAC instruction for efficiency, with the sum in the large accumulator). The length is then determined by taking the square-root of the resulting sum using a simple sucessive-approximation approach in the second DSP accumulator.The resulting vector length is then divided by the target length (arbitrarily, 1000, and dependent on the scanner hardware) to determine the number of small vectors into which the large vector must be divided. Small vectors are then generated and scanned-out at the proper rate (PPS) until the target point is reached.

AutoVectoring overcomes two major limitations in addition to splitting the length of a long vector (which is usually handled by the vectorizing software): one being the need to retrace at the end of a frame to the beginning of the frame and the other being a “glitch” seen when frames are switched. The retrace problem is as much an issue of conversion software as anything else where most software does not draw a blanked vector between the end point of a frame and the beginning of the frame allowing the beam to start to trace the frame again – in other words the frame may begin in the left corner and end in the right corner with no vector drawn to bring the beam back to the beginning. The “glitch” between frames is a similar problem where the display of one frame is complete and the display of a second frame begins the beam is seen to suddenly jump from one area (the end-of-frame position of the first image) to another (the beginning-of-frame position of the second image). The lack of proper vectoring of this ‘frame jump’ trace also causes ‘tearing’ of the second image by allowing the scanner mirrors to reach large velocities – vectorization keeps mirror velocity to a reasonable level.

A multi-frame animation shows why AutoVector is so valuable. On the left, with AutoVectoring turned off, traces are visible when the beam retraces from one frame to the next, especially between the last frame and the first (where the trace swings wildly to the left in a large arc). With AutoVectoring on, interframe traces are blanked and are vectored to avoid allowing the scanner mirrors to achieve uncontrollable velocities allowing the artist to pay no attention to where a frame starts or ends.

The animation above was produced by storing eight individual frames in flash memory and writing a short script which loads a frame, executes a delay of 100ms, then loads the next script. At the end of the last frame, a GOTO command takes control back to the beginning of the script.


This project is a continuing work. The next planned upgrade is an SD-card interface allowing huge amounts of storage.


Download the firmware, version 1.00 in MPLAB 7.5x assembler format. This version is basic and supports only basic output.

Download the firmware, version 1.50 in MPLAB 7.5x assembler format. This version supports scripts and image manipulation commands. It also features the ability to store an entire script into flash memory so upload of an entire show script is not required.

Download the firmware, version 1.71 in MPLAB 8.4x assembler format. This version, the newest release, supports autovectoring and interframe interpolation.

Download the script compiler in Microsoft Excel format.