POV Light Stick

POV stands for Persistence of Vision and it is the reason why we perceive fast changing snapshots as moving images. A Light Stick is…exactly what it describes, a stick with a set of lights attached to it. But putting the two together means that by carefully timing which lights turn on and off, it is possible to “paint” images, whether on slow human eyes or long exposure photographs such as this one.

Since the Digirule has 16 LEDs and is stick-like, it was inevitable not to try to turn it into a POV light stick.

This section outlines the simplest, un-optimised program that implements a light stick on the Digirule 2. Yes, it is possible to optimise the code further but for the moment we are aiming for the simplest straightforward option.

The reason this example is in the more “advanced” programs section is because it is using the indirect copy version of COPYRR, the details of which can be found in section Advanced Digirule 2 Programming.

The main idea

POV displays are extremely simple. They usually involve storing the “image” as a bitmap in memory which is progressively scanned and its data used to turn the stick’s LEDs on and off.

At their simplest form, POV displays simply scan the bitmap at a fixed rate, based on practical experiments of what seems to look right. At their most complex forms, light sticks synchronise the scanning of the bitmap to the movement of the stick so that no matter how fast or slow, forwards or backwards, faster or slower it moves, the bitmap will display exactly as intended. In addition, the more complex POV light sticks might add bitmap upload functionality, storing multiple bitmaps, displaying rolling image sequences and others.

For the Digirule2 POV light stick, we take the simplest approach (for the moment):

  1. Define a bitmap in memory.

  2. Fetch a line from memory, turn the LEDs on

  3. Wait for at least \(\frac{1}{10}\) of a second, for the pixels to register in the brain.

  4. Turn the LEDs off

  5. Wait for the same time

  6. Repeat from step 2 until all bitmap lines have been read. Once you run out lines, simply start from the beginning.

Defining the bitmap

The “bitmap” can be defined as a set of 8 bytes but since the assembler (dgasm.py) understands binary number literal values, defining that memory area in the editor also provides a sort of preview for it.

Here for example is the letter A:

 1pov_data:
 2.DB 0b00111100
 3.DB 0b01000010
 4.DB 0b10000001
 5.DB 0b11111111
 6.DB 0b10000001
 7.DB 0b10000001
 8.DB 0b10000001
 9.DB 0b10000001
10pov_data_len:
11.DB 8

This is part of ../../dg_asm_examples/advanced/pov.dsf

There is no reason for this bitmap to strictly be an 8x8 bitmap but to let the code know of its length we also define the variable pov_data_len with a static value of 8, for this bitmap.

Scanning the bitmap

Probably the easiest thing to do: Set a counter to the start of the bitmap memory region and advance it by one to fetch the next line. This is a very common “pattern” when dealing with “arrays” (to the extent an array data type can be defined within the Digirule’s capabilities) and proceeds like this:

1reset:
2COPYLR pov_data pov_counter

pov_data is a label and dgasm.py will “translate” it to its literal value. By executing this COPYLR, the content of variable pov_counter at the start of the program now points to the beginning of pov_data.

Wasting time

Steps 3 and 5, from our extremely simple routine outline earlier, include a very small delay. Delays are painfully simple for CPUs and are based on a very simple trick: Let the CPU do nothing else but count down from a large number.

On the Digirule2 there are two ways to control the speed of execution of a program:

  1. Use the SPEED command (which controls the speed that a program is executed globally).

  2. Use one or more counters to keep the CPU busy.

Since the second way of introducing delays depends on how large a number the CPU can count down from and that the most straightforward counter is 1 byte long, both ways of controlling the speed will be used here. The Digirule might not be the fastest CPU but it will count down from 255 incredibly fast.

The delay routine is as simmple as:

 1delay:
 2COPYLR delay_count r1
 3delay_loop:
 4NOP
 5NOP
 6NOP
 7NOP
 8DECRJZ r1
 9JUMP delay_loop
10RETURN

This is part of ../../dg_asm_examples/advanced/pov.dsf

Putting it all together

There is really not much else to this program, except a few cosmetic additions with a few .EQU definitions to change constants without having to recompile the program.

The main loop for the light stick is available below:

 1CBR 2 status_reg
 2COPYLR led_reg f_to
 3reset:
 4COPYLR pov_data pov_counter
 5COPYRR pov_data_len r0
 6start:
 7COPYRR pov_counter f_from
 8CALL f_copy
 9CALL delay
10COPYLR 0 led_reg
11CALL delay
12INCR pov_counter
13DECRJZ r0
14JUMP start
15JUMP reset
16
17f_copy:
18.DB 7
19f_from:
20.DB 0
21f_to:
22.DB 0
23RETURN
24
25delay:
26COPYLR delay_count r1
27delay_loop:
28DECRJZ r1
29JUMP delay_loop
30RETURN
31pov_data:
32.DB 0b00111100
33.DB 0b01000010
34.DB 0b10000001
35.DB 0b11111111
36.DB 0b10000001
37.DB 0b10000001
38.DB 0b10000001
39.DB 0b10000001
40pov_data_len:
41.DB 8
42pov_counter:
43.DB 0
44r0:
45.DB 0
46r1:
47.DB 0

This is part of ../../dg_asm_examples/advanced/pov.dsf

One thing to notice here is that whenever a variable was required it was simply allocated. It is however possible to write a more optimised version of this code that uses less memory. However, even at this sloppy level the program ends up being very very small, leaving as much memory free as possible for……user defined bitmaps.

Conclusion

This example, shows a way of “drawing” images using one row of LEDs on the Digirule2. However, the “output device”, the “screen”, of the Digirule2 has two rows of 8 pixels.

Therefore, it would be possible to reduce the flickering rate by display two rows at the time or even create a “scrolling image” through a “window” that is just 2x7 wide.