Drop Day Octahedron

I was digging through the archives and I found some documentation for the CCFL Octahedron project from Drop Day that was never published. We don't have photos of the build process, just a text description and the source code, as well as pictures of the finished project in action. This should be good for inspiration and perhaps source code for a similar project, if not total duplication.

Physical construction

Each edge of the octahedron structure is a 2-foot piece of wooden dowel rod. A hole is drilled perpendicular to each rod at each end. The rods at each vertex are held together with a double loop of fishing wire through these holes. Rods and wire are spraypainted black.

The lights are cold cathode flourescent bulbs, as sold for lighting computer cases. A pair of bulbs are attached to the outside of each edge with cable ties. Each pair is powered through an inverter attached to the inside of the edge. I've left some spare bulbs in the elevator closet in alley 3. A pair of bulbs together with inverter can be purchased online for about $8.

A power cable runs from each inverter to a common vertex where they are bundled together and exit through one side. Each of the twelve cables is dual-conductor 18-gauge (?) speaker wire. One end terminates in a connector accepted by the inverter; the other is permanently soldered to the control board.

The control board is powered through a long three-conductor ribbon cable terminating in a Molex 8981 male connector, which will interface with a standard ATX computer power supply. (To make an ATX power supply turn on without a motherboard present, one must connect the ~PS_ON signal on the motherboard connector, normally green, to ground.)


The control board is built around an ATtiny2313 processor, two CD4094BC 8-bit shift registers, and three ULN2068 quad-channel Darlington switch arrays. The processor is clocked by an external 16 MHz crystal. The fuse extended and high bytes are left at factory default. The fuse low byte is programmed to 0xFF: no prescalar, no clock output, external crystal, slow start-up. The shift registers are cascaded to act as a single 16-bit serial-in, parallel-out register. The processor has three outputs to the shift register: Data (pin PB3), Clock (pin PB2), and Latch (pin PB4). A single bit is loaded by setting Data high or low, setting Clock high for 4 processor cycles (250 ns), then bringing Clock low again. This is repeated 16 times, once per bit of a 16-bit word, with the least-significant bit first. After all 16 bits are loaded, the Latch pin is strobed high for 4 cycles, at which point the shift registers will present the 16 bits on their output pins.

12 of these 16 bits are wired to inputs of the Darlington switch arrays. When a switch-array input is at logic high, the corresponding output will sink current to ground, illuminating the corresponding lights. The unused bits of the shift register are (from LSB = 0) bits 6, 7, B, and F.

The power supply to the control board consists of ground, +5 V, and +12 V. The 12 V supply is wired directly to the positive side of each inverter's DC input. The negative side of each inverter's input sinks through a Darlington switch. When all bulbs are on, the system will draw about 5 amps on the 12 V line. It has an inline fuse holder loaded with a 10 amp fuse. The 5 V line is used for powering the logic chips only and should draw minimal current.

.. you see how much effort I'm putting here : screen shot of the monospace text figure

hdr2 is used to program the processor via a standard AVR programmer, such as the AVR ISP mkII. From top to bottom as oriented above, the pins are:


hdr1 is currently unused but is connected to the circuit. From top to bottom:

VCC, GND, not connected, PD4, PD5

This could be used to communicate with another board via a 2-wire serial protocol.

The push buttons will short PB5 or PB6 to ground when pressed. The switch will short PD6. Neither is used by the current software.


See code at the end of this post. The software implements a few modes and switches between them. It includes a very simplistic random number generator (which is not random at all because the seed is hard-coded).

Bugs and quirks

Sometimes, especially when starting, the system will go into a bad state such as freezing or strobing synchronously in an unappealing fashion. I seem to have fixed this by adding another filtering capacitor on the 12 V rail, but the problem may return.

Sometimes after switching off the ATX power supply it will not turn back on for a number of minutes. Perhaps a large resistor between power and ground would fix this problem by acting as a stabilization load.

Many aspects of the circuit, such as the use of shift registers, an external crystal, and extra buttons and switches, are historical or arbitrary. Where the speaker wire meets the board it can short against unintended pins of the ULN2068 in ways that are not visually obvious. This can result in all four channels on that chip turning on when only one is activated in software.

You can pulse-width modulate a light channel in software, but it will not dim the light. Instead, it will light up part of the tube from one end, which looks cool and is probably very bad for the bulb.

Replacing the control board

The control board is a crufty prototype and it may be desired to replace it with something nicer: either a better-produced custom board or a totally off-the-shelf controller. Essentially all that is required is to switch on and off with some reasonable speed the sinking of ~ 500 mA current on each of 12 channels. This could be accomplished with any number of devices: Darlington pairs, power FETs, solid-state relays, etc. Optical isolation of the control signal may improve robustness. Many microcontrollers can provide the 12 I/O pins directly, so external shift registers are unnecessary.


Here is the source code associated with the Octahedron. First, build-and-upload.sh, the bash commands used to compile and upload the program to the AVR.

#!/bin/sh -e

# build with avr-gcc suite
avr-g++ -o octahedron.bin -mmcu=attiny2313 -Wall -Winline -save-temps -fverbose-asm -Os octahedron.cpp
avr-size octahedron.bin
avr-objcopy -j .text -j .data -O ihex octahedron.bin octahedron.hex

# upload using AVR ISP mkII
avrdude -p t2313 -P usb -c avrispmkII -U flash:w:octahedron.hex

This is the straight up AVR C++. You'll need an in system programmer to upload it.

#define F_CPU 16000000L

#include <stdint.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <util/delay.h>

typedef uint8_t byte;

// a shitty "random" number generator
uint16_t xrand() __attribute__ ((noinline));
uint16_t xrand() {
static uint16_t y = 3;
y ^= (y << 13);
y ^= (y >> 9);
return y ^= (y << 7);

// delay by an amount known at runtime
void delay_ms(uint16_t ms) {
while (ms-- > 0) _delay_ms(1);

// Define pins for the shift registers
#define SREG_LCH (1 << 4) // latch, high to set
#define SREG_DAT (1 << 3) // data
#define SREG_CLK (1 << 2) // clock on rising edge

// Define pins for the buttons and switch
#define UI_DDR DDRB
#define UI_PIN PINB
#define UI_BTN_A (1 << 6)
#define UI_BTN_B (1 << 5)

#define UI_BTN_A_DOWN (!(UI_PIN & UI_BTN_A))
#define UI_BTN_B_DOWN (!(UI_PIN & UI_BTN_B))

#define UI_SW_DDR DDRD
#define UI_SW_PIN PIND
#define UI_SW (1 << 6)

// Init the shift registers
inline void sreg_init() {

// Optional delay when setting pins for the
// shift registers.
// At 5V they will handle at least 12 MHz.
inline void sreg_delay() {
asm volatile ("nop");
asm volatile ("nop");
asm volatile ("nop");
asm volatile ("nop");

// Shift a bit into the shift registers.
void sreg_shift_bit(bool x) {
if (x)


// A full shift-register write.
// Shift 16 bits, then latch.
void sreg_write(uint16_t x) {
for (byte i=0; i<16; i++) {
sreg_shift_bit(x & 1);
x >>= 1;


// The bits corresponding to wired-up channels are:
// FEDC BA98 7654 3210
// This function maps 12 contig. bits onto these.
uint16_t lights_to_channels(uint16_t x) {
return (x & 0x003F) // 0000 0011 1111
| ((x & 0x01C0) << 2) // 0001 1100 0000
| ((x & 0x0E00) << 3); // 1110 0000 0000

// Set which lights are on according to a 12-bit value.
uint16_t lights = 0;
void set_lights(uint16_t new_lights) {
static uint16_t state = 0;
lights = new_lights;
uint16_t new_state = lights_to_channels(lights);
uint16_t mask = 0;

//if (new_state == state) return;

for (byte i=0; i<16; i++) {
mask = (mask << 1) | 1;
sreg_write((state & (~mask)) | (new_state & mask));

state = new_state;

inline void ui_init() {
// pull up both buttons and the switch


inline void ui_debounce() {

#define L_R1 0x400
#define L_G1 0x002
#define L_B1 0x004

#define L_R2 0x008
#define L_G2 0x040
#define L_B2 0x010

#define L_R3 0x100
#define L_G3 0x020
#define L_B3 0x080

#define L_R4 0x001
#define L_G4 0x200
#define L_B4 0x800

/// 0101 0000 1001 = 0x509
/// 0010 0110 0010 = 0x262
/// 1000 1001 0100 = 0x894

#define ALL_R (L_R1 | L_R2 | L_R3 | L_R4)
#define ALL_G (L_G1 | L_G2 | L_G3 | L_G4)
#define ALL_B (L_B1 | L_B2 | L_B3 | L_B4)

#define ALL_LIGHTS (ALL_R | ALL_B | ALL_G)

const uint16_t tris[8] PROGMEM = {
L_R2 | L_G2 | L_B2,
L_R1 | L_G3 | L_B2,
L_R3 | L_G3 | L_B3,
L_R4 | L_G2 | L_B3,
L_R3 | L_G1 | L_B4,
L_R4 | L_G4 | L_B4,
L_R2 | L_G4 | L_B1,
L_R1 | L_G1 | L_B1

const uint16_t squares[3] PROGMEM = {

/// modes

uint16_t rand_mask() {
return (1 << (xrand() % 12));

void noise_fast() {
uint16_t len = 300 + (xrand() % 300);
for (uint16_t t = 0; t < len; t++) {
set_lights(lights ^ rand_mask());

void fade_down() {
while (lights != 0) {
set_lights(lights & (~rand_mask()));

void fade_up() {
while (lights != ALL_LIGHTS) {
set_lights(lights | rand_mask());

void updown_fast() {
uint16_t len = 20 + (xrand() % 20);
for (uint16_t t = 0; t < len; t++) {

void rgb() {
uint16_t len = 150 + (xrand() % 100);
for (uint16_t t = 0; t < len; t++) {
for (byte i=0; i<3; i++) {

void tri_rand() {
uint16_t len = 300 + (xrand() % 700);
for (uint16_t t = 0; t < len; t++) {
set_lights(pgm_read_dword(&(tris[xrand() % 8])));

/* causes reset?
void tri_strobe() {
uint16_t len = 100 + (xrand() % 300);
for (uint16_t t = 0; t < len; t++) {
set_lights(pgm_read_dword(&(tris[xrand() % 8])));
delay_ms(xrand() % 200);

void test() {
for (byte i=0; i<12; i++) {
set_lights(1 << i);

int main() {



for (;;) {


No comments:

Post a Comment