Skip to content

Commit

Permalink
[Hacky Holidays] The Pixel Clock (#1396)
Browse files Browse the repository at this point in the history
* add initial files

* update gerbers

* final gerbers and price

* add usb cc resistors

* add cart

* add manufacturing files

* use economic ws2812bs

* fix designators
  • Loading branch information
grimsteel authored Jan 27, 2025
1 parent fcf0d41 commit 4c58b4b
Show file tree
Hide file tree
Showing 27 changed files with 52,282 additions and 0 deletions.
674 changes: 674 additions & 0 deletions projects/the-pixel-clock/LICENSE

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions projects/the-pixel-clock/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
name: "Siddhant Kameswar"
slack_handle: "@grimsteel"
github_handle: "@grimsteel"
tutorial: # Link to the tutorial if you used one
wokwi: # Wokwi doesn't support the CH552 (and my firmware is highly dependent on CH552-specific registers and 8051 ASM.)
---

# The Pixel Clock

<!-- Describe your board in 2-3 sentences. What are you making? What will it do? -->

This is a clock-shaped holiday ornament (which unfortunately cannot function as a clock). It's powered by 2 LIR2032 batteries, and runs on an 8051 CH552 MCU. It has a buzzer which can be used to play songs, with the 8 neopixels flashing in sync.

<!-- How much is it going to cost? -->

## Cost

$31.72 + $18.15 shipping - $9.00 coupon = **$40.87**

<!-- Tell us a little bit about your design process. What were some challenges? What helped? ***Totally optional*** -->

## Design Process

There's not much information on the CH552 online, so figuring out what its voltage/current requirements and capabilities were as well as figuring out how to program it was difficult.
Binary file added projects/the-pixel-clock/cart.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions projects/the-pixel-clock/firmware/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
*.asm
*.lst
*.mem
*.rel
*.rst
*.sym
*.adb
*.lk
*.map
*.mem
*.ihx
*.hex
*.bin

.cache/
compile_commands.json
72 changes: 72 additions & 0 deletions projects/the-pixel-clock/firmware/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# This Makefile is adapted from https://github.com/Blinkinlabs/ch554_sdcc

TARGET=pixel-clock
C_FILES := $(wildcard *.c) $(CH554_SDCC_PATH)/debug.c


#######################################################

# toolchain
CC = sdcc
OBJCOPY = objcopy
PACK_HEX = packihx
WCHISP ?= wchisptool -g -f

#######################################################

FREQ_SYS ?= 6000000 # 6 MHz

XRAM_SIZE ?= 0x0400

XRAM_LOC ?= 0x0000

CODE_SIZE ?= 0x3800

CFLAGS := -V -mmcs51 --model-small \
--xram-size $(XRAM_SIZE) --xram-loc $(XRAM_LOC) \
--code-size $(CODE_SIZE) \
-I$(CH554_SDCC_PATH) -DFREQ_SYS=$(FREQ_SYS) \
$(EXTRA_FLAGS)

LFLAGS := $(CFLAGS)

RELS := $(C_FILES:.c=.rel)

print-% : ; @echo $* = $($*)

%.rel : %.c
$(CC) -c $(CFLAGS) $<

# Note: SDCC will dump all of the temporary files into this one, so strip the paths from RELS
# For now, get around this by stripping the paths off of the RELS list.

$(TARGET).ihx: $(RELS)
$(CC) $(notdir $(RELS)) $(LFLAGS) -o $(TARGET).ihx

$(TARGET).hex: $(TARGET).ihx
$(PACK_HEX) $(TARGET).ihx > $(TARGET).hex

$(TARGET).bin: $(TARGET).ihx
$(OBJCOPY) -I ihex -O binary $(TARGET).ihx $(TARGET).bin

flash: $(TARGET).bin pre-flash
$(WCHISP) $(TARGET).bin

.DEFAULT_GOAL := all
all: $(TARGET).bin $(TARGET).hex

clean:
rm -f \
$(notdir $(RELS:.rel=.asm)) \
$(notdir $(RELS:.rel=.lst)) \
$(notdir $(RELS:.rel=.mem)) \
$(notdir $(RELS:.rel=.rel)) \
$(notdir $(RELS:.rel=.rst)) \
$(notdir $(RELS:.rel=.sym)) \
$(notdir $(RELS:.rel=.adb)) \
$(TARGET).lk \
$(TARGET).map \
$(TARGET).mem \
$(TARGET).ihx \
$(TARGET).hex \
$(TARGET).bin
270 changes: 270 additions & 0 deletions projects/the-pixel-clock/firmware/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
#ifndef bool
#define bool int
#endif

#include "ch554.h"
#include "debug.h"
#include "programs.h"

#define P1 0x90
#define P3 0xB0

// setup buttons

// BTN1: P3.1
// BTN2: P1.4
#define BTN1_PIN 1
#define BTN2_PIN 4
SBIT(BTN1, P3, BTN1_PIN);
SBIT(BTN2, P1, BTN2_PIN);

// BUZ: P1.5
#define BUZ_PIN 5
SBIT(BUZ, P1, BUZ_PIN);

// WS2812B: P1.1
#define WS2812B_PIN 1
#define NUM_WS2812B 8
SBIT(WS2812B, P1, WS2812B_PIN);

// [0, 256)
#define WS2812B_BRIGHTNESS 51

// GRB, in order
uint8_t ws2812b_data[NUM_WS2812B * 3];

// Timer frequency in hertz (6 MHz / 12)
#define TIMER_FREQUENCY 500000
#define HALF_TIMER_FREQUENCY 250000

uint16_t timer_ticks_start;
uint32_t toggle_count;
// this serves as the current time.
// 1 "unit" of timer1_overflows is 0.5ms
volatile uint32_t timer1_overflows = 0;
uint32_t last_button1_press = 0;
uint32_t last_button2_press = 0;
volatile uint8_t current_idx = 0;

void next_program_item();

// PWM timer interrupt
void timer0_interrupt(void) __interrupt(INT_NO_TMR0) {
// one-half cycle has elapsed
TL0 = timer_ticks_start & 0xff;
TH0 = timer_ticks_start >> 8;

if (toggle_count-- > 0) {
// toggle pin
BUZ = !BUZ;
} else {
// turn off
BUZ = 0;
// stop the timer, disable interrupt
TMOD &= ~bT0_M0;
TR0 = 0;

// wait 100ms before next item
mDelaymS(100);
next_program_item();
}
}

// Main timer interrupt
void timer1_interrupt(void) __interrupt(INT_NO_TMR1) { timer1_overflows++; }

inline void btn1_press() {}
inline void btn2_press() {}

// GPIO interurpt
void gpio_interrupt(void) __interrupt(INT_NO_GPIO) {
if (!BTN1) {
// if last_button1_press < timer1_overflows, the program has been running for 25 days, and we overflowed the unsigned int
if (last_button1_press < timer1_overflows && timer1_overflows - last_button1_press >= 20) {
btn1_press();
}
last_button1_press = timer1_overflows;
}

if (!BTN2) {
if (last_button2_press < timer1_overflows && timer1_overflows - last_button2_press >= 20) {
btn2_press();
}
last_button2_press = timer1_overflows;
}
}

// PWM - 50% duty cycle
// Builtin PWM is too fast for audible tones
// Duration is measured in 16th notes (250ms)
void tone(uint16_t frequency_hz, uint8_t duration) {
// already playing a tone
if (TR0 == 1) return;

// TODO: Higher frequencies may be able to use a faster clock (bT0_CLK, bTMR_CLK)

ET0 = 1; // timer0 interrupt
TMOD |= bT0_M0; // 16 bit timer0

// Adapted from arduino tone() code
// TIMER_FREQUENCY / frequency_hz returns number of ticks for a full cycle
// Because this is 50% duty cycle, divide by 2 for number of ticks for each "high" or each "low"
// We will start counting at this value, and go to 0xffff
// idk what the +1 is for
timer_ticks_start = 0xffff - (uint16_t) ((uint32_t) (HALF_TIMER_FREQUENCY) / frequency_hz) + 1;
// frequency * duration = number of full cycles
// * 2 = number of toggles
// / 1000 because ms
// toggle_count = (uint32_t) (frequency_hz * duration_ms) / 1000 * 2;
// optimized version: measure duration as 1/4 second
toggle_count = frequency_hz * duration >> 1;

// Set the timer
TL0 = timer_ticks_start & 0xff;
TH0 = timer_ticks_start >> 8;

TR0 = 1; // begin timer
}

void ws2812b_write() {
// disable interrupts (time sensitive!)
E_DIS = 1;
for (uint8_t i = 0; i < NUM_WS2812B * 3; i++) {
// TIMINGS AT 6MHz (tolerance 150 ns aka 1 clock cycle)
// T1H = 0.8 us --> 4-5 clock cycles
// T1L = 0.45 us --> 2-3 clock cycles
// T0H = 0.4 us -> 2-3 clock cycles
// T0L = 0.85 us --> 5-6 clock cycles
// The low times don't _really_ matter - only the high times matter for distinguishing between 0s and 1s
// RESET TIME: > 50 us --> 300 clock cycles
// Total time: 7/8 clock cycles
// TODO: Tune with a logic analyzer
__asm
mov a, r7 ; i is stored in r7
add a, #_ws2812b_data ; ws2812b_data[i]
mov r6, #8 ; eight bits
01$: ; --- loop
rlc a ; 1 cycle
setb _WS2812B ; 1 cycle - T1H begin + T0H begin + T0L end (2 + 1 + 2 + 1) + T1L end (1 + 2 + 1)
nop ; 1 cycle
mov _WS2812B, c ; 2 cycles - T0H end (1 + 1) + T0L begin
clr _WS2812B ; 1 cycle - T1H end (1 + 1 + 2) + T1L begin
djnz r6, 01$ ; 2 cycles
__endasm;
}
// re-enable interrupts
E_DIS = 0;
}

// Hues is an array of length NUM_WS2812B
void ws2812b_set_hues(uint16_t *hues) {
for (int i = 0; i < NUM_WS2812B; i++) {
uint16_t hue = hues[i];
uint8_t r = 0, g = 0, b = 0;
uint8_t sector = hue / 60;
switch (sector) {
case 5: // [300, 360)
r = WS2812B_BRIGHTNESS;
b = WS2812B_BRIGHTNESS * 6 - WS2812B_BRIGHTNESS * hue / 60;
break;
case 0: // [0, 60)
r = WS2812B_BRIGHTNESS;
g = WS2812B_BRIGHTNESS * hue / 60;
break;

case 1: // [60, 120)
g = WS2812B_BRIGHTNESS;
r = WS2812B_BRIGHTNESS * 2 - WS2812B_BRIGHTNESS * hue / 60;
break;
case 2:
g = WS2812B_BRIGHTNESS;
b = WS2812B_BRIGHTNESS * hue / 60 - WS2812B_BRIGHTNESS * 2;
break;

case 3:
b = WS2812B_BRIGHTNESS;
g = WS2812B_BRIGHTNESS * 4 - WS2812B_BRIGHTNESS * hue / 60;
break;
case 4:
b = WS2812B_BRIGHTNESS;
r = WS2812B_BRIGHTNESS * hue / 60 - WS2812B_BRIGHTNESS * 4;
break;
}
ws2812b_data[i * 3] = g;
ws2812b_data[i * 3 + 1] = r;
ws2812b_data[i * 3 + 2] = b;
}
}

void next_program_item() {
if (current_idx == PROGRAM1_LENGTH) {
// wait 1 second before repeating
current_idx = 0;
mDelaymS(1000);
}
uint16_t hues[NUM_WS2812B];
for (int j = 0; j < NUM_WS2812B; j++) {
hues[j] = program_1_hues[current_idx];
}
ws2812b_set_hues(hues);
ws2812b_write();

// buzzer
tone(program_1_notes[current_idx], program_1_durations[current_idx]);
current_idx++;
}

void main() {
// frequency is default (6MHz)
// CfgFsys(); no need for this
mDelaymS(5);

/*// enter safe mode
SAFE_MOD = 0x55;
SAFE_MOD = 0xAA;
// setup sleep wake on buttons
WAKE_CTRL |= bWAK_P1_4_LO | bWAK_P3_2E_3L; // 3.2 is disconnected
// exit safe mode
SAFE_MOD = 0x00;*/

mInitSTDIO();
UART1Setup();

// disable all pullups except for 1.2 and 1.3 (clock)
P1_DIR_PU &= 0x0C;
// disable all except for 3.6 and 3.7 (usb)
P3_DIR_PU &= 0xC0;

// push pull mode
P1_MOD_OC &= ~(1 << WS2812B_PIN);
// enable pullup
P1_DIR_PU |= 1 << WS2812B_PIN;


P1_MOD_OC &= ~(1 << BUZ_PIN);
P1_DIR_PU |= 1 << BUZ_PIN;
BUZ = 0;

// configure buttons as high impedance with pullup
P1_MOD_OC |= 1 << BTN2_PIN;
P1_DIR_PU |= 1 << BTN2_PIN;

P3_MOD_OC |= 1 << BTN1_PIN;
P3_DIR_PU |= 1 << BTN1_PIN;

// enable interrupts
EA = 1;
// gpio interrupts
IE_GPIO = 1;
// gpio edge interrupt mode, enable on 1.4 and 3.1
GPIO_IE |= bIE_IO_EDGE | bIE_P1_4_LO | bIE_P3_1_LO;

ET1 = 1; // timer0 interrupt
TMOD |= bT1_M1;// 8 bit timer1 (0.5 ms for each overflow - 256 / (6 MHz / 12) = 512 us)
TR1 = 1;

// start program
next_program_item();
}
Loading

0 comments on commit 4c58b4b

Please sign in to comment.