A flexible, scriptable RGB controller that can be reprogrammed over a serial connection.
The main goal of this project is to provide a more customizable LED controller than off-the-shelf ones, with modular support for different types of LED chips. However, I didn't want to have to flash new firmware on the Arduino each time I wanted to change the output; therefore, I've implemented RGB control in a virtual machine with program memory that persists in EEPROM. The VM program can be reflashed via a simple serial protocol without reflashing the Arduino itself.
This codebase should work on any AVR based Arduino, though it has only been tested on the Uno, as well as the 16MHz "Pro Micro" boards which are widely available online.
Features:
- A virtual machine that interprets bytecode which manipulates the state of RGB values.
- Support for taking analog inputs into the VM program.
- EEPROM bytecode storage that the MCU reads back into memory at startup.
- Serial bytecode updater with persistence in EEPROM.
- An assembler that can translate mnemonics to bytecode.
- A simple PWM based driver for controlling an analog RGB LED.
- Drivers for WS281x and APA102/SK9822 addressable LEDs.
TODO:
- Implement a GUI for easily programming and flashing the bytecode.
More information on this project is available on my blog.
RGB control is implemented in assembly for the rgbvm
virtual machine.
To produce output, an rgbvm
program must:
- Initialize a driver on a channel (
init
instruction) - Write as many pixels of data to that channel as desired (
write
instruction) - Instruct the driver to send the buffered data (
send
instruction)
Bytecode can be generated with assemble.py. The program takes two arguments: the input assembler script, and the output bytecode filename.
python tools/assemble.py input.txt output.bin
The assembler depends on the package bitstruct.
The output from the assembler can be flashed to the Arduino with flash.py. The program takes two arguments: the input bytecode file, and the serial port the Arduino is on.
python tools/flash.py output.bin /dev/ttyACM0
The flasher depends on the package pyserial.
If there is existing content in EEPROM, the firmware may malfunction due to attempting to interpret it as bytecode. It is recommended to run the builtin EEPROM Clear sample from the Arduino SDK prior to flashing this software. Technically, only the first two bytes of EEPROM need to be zeroed to clear the VM's program memory.
The analog
driver runs RGB on pins 9, 10, and 11 respectively through PWM. It is intended for debugging purposes only.
This driver can be activated by passing 0
as the first argument to the init
instruction.
The ws2811
driver should work well with all WS281x class LEDs (WS2811, WS2812, WS2812B), though it has only been tested with the WS2811.
This driver can be activated by passing 1
as the first argument to the init
instruction.
WS2811 is driven by a single data line; the third argument passed to the init
instruction chooses the digital pin this output is sent to.
Since the WS2811 control protocol requires specific timings, the send
instruction for this driver has been implemented in AVR assembly. This
code is tuned for a 16MHz Arduino, and thus will not work on an 8MHz chip.
The apa102
driver should work well with APA102 and SL9822 LEDs.
This driver can be activated by passing 2
as the first argument to the init
instruction.
APA102 is driven by a data line and a clock line; the third argument passed to the init
instruction specifies the digital pin for the clock
signal, while the next pin in numeric order is implicitly chosen as the data pin.
APA102 chips support specifying a brightness value in addition to the RGB intensity, however this has not been implemented in this driver.
This project is based on arduino-cmake and follows the standard CMake build process:
mkdir build
cd build
cmake ..
make
The framework conveniently provides make upload
to flash the Arduino firmware.
-
Base requirements:
CMake
- http://www.cmake.org/cmake/resources/software.htmlArduino SDK
- http://www.arduino.cc/en/Main/Software
-
Linux requirements:
gcc-avr
- AVR GNU GCC compilerbinutils-avr
- AVR binary toolsavr-libc
- AVR C libraryavrdude
- Firmware uploader
rgb-ctrl is based on an 8-bit virtual machine called rgbvm
. It implements an application-specific instruction set, having 15 general purpose registers,
a flag register, an instruction pointer register, a few basic arithmetic operations, color space conversion operations, branching operations, and output controls,
and an instruction that reads analog inputs.
Execution starts at instruction offset 0, resetting to 0 if the end of the program is reached.
All values are treated as unsigned, though the assembler itself supports specifying signed immediate operands.
The assembler currently has a very basic syntax:
- A line starting with
#
is a comment - A line starting with
:
is a label - Every other non-empty line is expected to describe an instruction
- Each instruction is a sequence of space-separated tokens
- The first token is an opcode, and subsequent tokens are operands
- If an operand starts with
r
, it is a register; if it starts with a:
, it is a label; otherwise, it is an immediate
The assembler runs two passes over the source: first, it determines the instruction addresses of all labels; then, it generates code.
Take a look at the example scripts to get an idea of how to use the VM:
- hue_cycle.rgbvm - loops through all hues
- value_pulse.rgbvm - repeatedly increases the value, then reduces it again
- ws2811_test.rgbvm - provides an example of controlling a chain with multiple LEDs
Mnemonic | Operands | Description |
---|---|---|
nop | [imm] | no operation -- if imm specified and nonzero, sleep 2^(imm-1) ms |
set | rdst (rsrc|imm) | load register rdst from rsrc or immediate |
add | rdst (rsrc|imm) | add to rdst from rsrc or immediate |
mul | rdst (rsrc|imm) | multiply rdst by rsrc or immediate |
div | rdst (rsrc|imm) | divide rdst by rsrc or immediate |
mod | rdst (rsrc|imm) | modulo rdst by rsrc or immediate |
cmp | r0 (r1|imm) | compare r0 to r1 or imm and store the result in flags |
goto | address | move the instruction pointer to address |
brne | address | move the ip to address if last comparison was not equal |
breq | address | move the ip to address if last comparison was equal |
brlt | address | move the ip to address if last comparison was less than |
brle | address | move the ip to address if last comparison was less or equal |
brgt | address | move the ip to address if last comparison was greater than |
brge | address | move the ip to address if last comparison was greater or equal |
hsv2rgb | rh rs rv | convert hsv values in registers to rgb (in place), each of hsv in [0, 255] |
init | immd immc [immf] | initialize output channel immc with driver number immd and optional flags immf |
write | rr rg rb immc | buffer rgb value from registers on output channel immc |
send | immc | activate the buffered output of immc |
input | rdst immpin | load rdst with the value from the analog pin numbered by the immediate |
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.