4-channel PWM Tree Emulator

Despite Christmas being over and the New Year almost upon us, I decided to write an emulator for the PWM Christmas Tree Controller virtual machine. The original animation script I wrote for it was very rushed, and made without the benefit of watching it execute on the real hardware.

The Emulator

This little JS web application will help me improve it for next year. It loads up with the script currently in the real device's firmware.

It uses YUI 3 for some basic DOM access sugar, but really doesn't need it. Ironically leaving Yahoo has given me the time to explore this library and I find that I really like it. Long ago Marxy recommended I try it out - yep now I get it mate, great stuff! I have a half-working drag-and-drop light array placement and animate application that I hope to finish soon that uses YUI much more extensively.

The assembler is very fragile! I am yet to add any sanity checking. You can write programs with it that make no sense and will crash terribly. Neither does the simulated virtual machine do any bounds checking, the IP will happly take off into no where land or the stack overflow (or underflow). User beware! For my purposes that is all just fine.

The simulation runs almost full speed in Firefox 3.5. Because it uses opacity, getting it working properly in IE (even IE8) required using the alpha filter hack. This makes it perform fairly poorly in IE (and look a bit different too). I have not tried it in other browsers, it probably won't even work in them. If it works for you, or you fix it so it does please tell me!

Virtual Machine Overview

The virtual machine has a ~30 ms cycle per tick. Instructions are only executed when the INT register equals or exceeds the IPI register. Fade/Ramp logic executes each tick regardless. All instructions (except NOP and WAIT) do not surrender the current tick, but call the next instruction immediately. NOP and WAIT reset INT and wait for the next IPI ticks before proceeding.

The Registers

ID Name Type Description
CH Channel State u8[4] Corresponding to the PWM channels. 0 is off, 0xFF is full-on.
RFM Ramp/Fade Mask u4[2] Nibble packed, each bit in the nibbles corresponds to a PWM channel, if a bit is set the corresponding channel value will be incremented/decremented by the BCR register value each cycle until either fully on or off, at which point the bit is automatically cleared. The upper nibble controls fading, the lower ramping. Having a ramp and a fade set for the same channel is not disallowed but makes no sense.
IPI Interrupts per Instruction u8 How frequently to execute an instruction. The virtual machine waits until the INT register equals or exceeds this value before processing the next instruction. Any current ramp/fade executes in the background at each cycle regardless.
INT Interrupt Counter u8 Used with IPI to control rate of code execution.
BCR Brightness Change Rate u8 The value added/subtracted from the CH values each cycle when the associated bits are set in the RFM register. Larger values mean the channels will ramp/fade faster. Zero disables change in brightness and will stall WAIT/DO-WHILE instructions.
IP Instruction Pointer u8 Points to the current instruction.
SP Stack Pointer u2 Points to the current stack frame.
STACK The Stack u8[2][3] Each stack frame is a return address (push of the IP) and a counter variable for the frame's loop counter.

The Instruction Set

The ASSIGN, TOGGLE, RAMP, and FADE instructions have immediate values packed into their high nibble. Mnemonics for each of the 16 variants are available, e.g. ASSIGN_0000 through ASSIGN_1111. The FOR, SPEED and RATE instructions take an immediate byte value from IP+1 and skip that byte. Hopefully my C-like notation in this table is fairly straight forward? Take a look at the controller.js and instruction-set.js files otherwise.

Mnemonic Name Description
NOP No Operation INT := 0; IP++
WAIT Wait for Ramp/Fade INT := 0; IP += (RFM)?(0):(1);
ASSIGN_xxxx Assign PWM Channels CH[j] := (*IP & (1 << j+4))?(0xFF):(0); IP++
TOGGLE_xxxx Assign PWM Channels CH[j] := (*IP & (1 << j+4))?((CH[j])?(0):(0xFF)):(CH[j]); IP++
RAMP_xxxx Assign Ramp Mask RFM[j] := (*IP & (1 << j+4))?(1):(0); IP++
FADE_xxxx Assign FADE Mask RFM[4+j] := (*IP & (1 << j+4))?(1):(0); IP++
RATE Assign BCR IP++; BCR := *IP; IP++
SPEED Assign IPI IP++; IPI := *IP; IP++
FOR Begin Counter Loop IP++; STACK[SP].CNTR := *IP; IP++; STACK[SP].ADDR := IP; SP++
NEXT Loop or Exit Counter Loop IP := (--STACK[SP - 1].CNTR)?(STACK[SP - 1].ADDR):((SP--)?(++IP):(++IP))
DO Begin Ramp/Fade Wait Loop IP++; STACK[SP].ADDR := IP; SP++
WHILE Loop or Exit Ramp/Fade Loop IP := (RFM)?(STACK[SP - 1].ADDR):((SP--)?(++IP):(++IP))
RESET Reset Machine IP := 0; SP := 0; INT := 0;

Try It Out

The real device uses two strings of 2-channel lights, 60 LEDs per channel, alternating along the string. The outline of the tree is the two LSB(ytes)s of the CH register (and LSB(its) of the RFM). The inner zig-zag is the MSBs. So ASSIGN_0011 would turn on just the outline, while ASSIGN_1100 would turn on only the zig-zag. An ASSIGN should be followed by a NOP if you want to see the result, and you should set IPI with a SPEED instruction - larger IPI values mean longer times before the next instruction. Why do you need NOPs at all? So you can chain things like ASSIGN and RAMP/FADE for different effects. Always end with a RESET to restart the program.

Have fun, make your own programs!

I might add a way to load and store programs. If I ever finish the D&D application it will need it.


Parent article: 240 LED Animated Christmas Tree.