#!/usr/local/bin/perl -w # # Copyright (c) 2025 Sulev-Madis Silber # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. # # # https://controllerstech.com/ws2812-leds-using-spi/ # # https://github.com/avg-I/ws2812-fbsd-spi # # https://fastled.io/ # # https://github.com/PabloCastellano/awesome-ws2812 # # https://blog.pwkf.org/2024/03/20/drive-ws2812-uart.html # # https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them/ # # https://github.com/bigjosh/NeoUart # # https://blog.rot13.org/2020/03/playing-video-on-ws2812-panel.html # # https://bitlair.nl/Projects/Ohm_led_strip_sleeves # # https://tomeko.net/software/WS2812_test/ # # https://github.com/mikeakohn/ws2812_driver # # https://hackaday.com/2017/01/20/cheating-at-5v-ws2812-control-to-use-a-3-3v-data-line/ # # https://quinled.info/2021/03/10/maximum-length-data-wire-leds-ws2812b-sk6812-ws2815/ # # https://github.com/UrielGuy/raspi_ws2812 # # https://github.com/libdriver/ws2812b # # https://kno.wled.ge/advanced/longdata/ # # https://www.doityourselfchristmas.com/forum/index.php # # https://www.diychristmas.org/ # # https://www.pjrc.com/teensy/td_libs_OctoWS2811.html # # https://zrouter.org/users/4 # # https://github.com/gonzoua # # https://adrianchadd.blogspot.com/?m=1 # # https://freebsdfoundation.org/resource/building-a-physical-freebsd-build-status-dashboard/ # # https://www.vintners.net/~mikel/howto/ledproj/ledary/index.html # # https://github.com/PlummersSoftwareLLC/NightDriverStrip # # https://cpldcpu.com/2016/03/09/the-sk6812-another-intelligent-rgb-led/ # # # ws2812-like protocol on spi # # mosi -> din # # # 2.5mhz spi # 1 / 2.5mbit/s = 400 ns # 1 spi bit = 400 ns # # 1 led bit = 3 spi bits # # 1 led bit = 1.2 us # # 0 -> 100 -> 400 ns high + 800 ns low = 1.2 us # 1 -> 110 -> 800 ns high + 400 ns low = 1.2 us # # # or, perhaps better # # 3mhz spi # 1 / 3mbit/s = 333 ns # 1 spi bit = 333 ns # # 1 led bit = 3 spi bits # # 1 led bit = 999 ns # # 0 -> 100 -> 333 ns high + 666 ns low = 999 ns # 1 -> 110 -> 666 ns high + 333 ns low = 999 ns # # _ # 0 - | |______ # # ____ # 1 - | |___ # # # reset # # 16 bytes = 128 bits # 400 ns * 128 = 51.2 us # # or, reset newer ws2812b ic's, if needed # # 88 bytes = 704 bits # 400 ns * 704 = 281.6 us # # # reset with 3mhz / 3mbit/s spi # # 19 bytes = 152 bits # 333 ns * 152 = 50.61600 us # # or, reset newer ws2812b ic's, if needed # # 106 bytes = 848 bits # 333 ns * 848 = 282.38400 us # # # max fps calc # # max transfer rate = 800kbit/s # 8 * 3 = rgb 3 channel 8 bit # 106 * 8 * 2 = reset bits at beginning & the end # # (800 * 1024) / ((8 * 3 * 1000) + (106 * 8 * 2)) = 31.8804483188045 # (800 * 1024) / ((8 * 3 * 500) + (106 * 8 * 2)) = 59.8130841121495 # # # led data per second # # 1000 leds + begin & end @ 30 fps # # 9212 * 8 * 30 / 3 / 1024 = 719.6875 # use strict; use warnings FATAL => 'all'; use Data::Dumper; use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC sleep); $| = 1; my $argv = join ' ', @ARGV; #my $led_count = 1; #my $led_count = 125; my $led_count = 200; #my $led_count = 250; #my $led_count = 500; #my $led_count = 1000; #my $led_count = 100000; #my $fps = 240; my $fps = 120; #my $fps = 60; #my $fps = 30; #my $apa106; my $apa106 = 1; my $grb = 1; my $reverse = 1; $grb = 0 if $apa106; $reverse = 0 if $apa106; my $bit_0 = '100'; my $bit_1 = '110'; my $reset_bits = 848; my $reset = "\x00" x ($reset_bits / 8); my $pre = $reset; my $production = 1; my ($debug, $one, $loop, $slow, $perf, $max, $bin, $wrap); $debug = 1 if $argv =~ /debug/; $one = 1 if $argv =~ /one/; $loop = 1 if $argv =~ /loop/; $slow = 1 if $argv =~ /slow/; $perf = 1 if $argv =~ /perf/; $max = 1 if $argv =~ /max/; $bin = 1 if $argv =~ /bin/; $wrap = 1 if $argv =~ /wrap/; $led_count = 1 if $debug; $led_count = 1 if $one; $production = 0 if $debug; $production = 0 if $perf; $fps = 1 if $slow; my (@led_data, @led_dec, @led_to_spi_map); #my $toggle; my $frames = 0; my $ticks = 0; #my $anim; my $anim = 1; my @anims = qw(1 2 3 4 5 6 7 8); #my $rand_anim; my $rand_anim = 1; my ($on_r, $on_g, $on_b); my $interval = 1.0; my $deadline; sub monotime { return clock_gettime CLOCK_MONOTONIC; } sub gen_bit_map { foreach my $byte (0 .. 255) { my $bin_bits = sprintf "%08b", $byte; my $spi_bits = ''; $bin_bits = reverse $bin_bits if $reverse; foreach my $bit_pos (0 .. 7) { my $bit = substr $bin_bits, $bit_pos, 1; $spi_bits .= $bit ? $bit_1 : $bit_0; } $led_to_spi_map[$byte] = pack 'B*', $spi_bits; } } sub gen_spi_bytes { #$toggle = $toggle ? 0 : 1; $frames++; $ticks++ unless $frames % $fps; if ($rand_anim) { unless ($frames % ($fps * 60)) { $anim = $anims[rand @anims]; @led_data = (); @led_dec = (); } } my ($rand_init, $rand_dec); my ($fade_r, $fade_g, $fade_b); my $step_up_r = 1; my $step_up_g = 1; my $step_up_b = 1; my $step_down_r = 1; my $step_down_g = 1; my $step_down_b = 1; my ($all_r, $all_g, $all_b); unless ($anim) { } elsif ($anim == 1) { $rand_init = 1; $rand_dec = 1; $fade_r = 1; $fade_g = 1; $fade_b = 1; } elsif ($anim == 2) { $all_r = 255; $all_g = 255; $all_b = 255; } elsif ($anim == 3) { if ($ticks % 2) { $on_r = 1; $on_g = 0; } else { $on_r = 0; $on_g = 1; } $all_r = $on_r ? 255 : 0; $all_g = $on_g ? 255 : 0; } elsif ($anim == 4) { if ($ticks % 2) { $on_r = 1; $on_g = 1; $on_b = 0; } else { $on_r = 0; $on_g = 0; $on_b = 1; } $all_r = $on_r ? 255 : 0; $all_g = $on_g ? 255 : 0; $all_b = $on_b ? 255 : 0; } elsif ($anim == 5) { $fade_r = $ticks % 5 ? 0 : 1; } elsif ($anim == 6) { unless ($frames % 3) { $fade_r = 1; $fade_g = 1; } } elsif ($anim == 7) { $on_b = $frames % 5 ? 0 : 1; $all_b = $on_b ? 255 : 0; } elsif ($anim == 8) { if ($frames % ($fps * 5)) { $all_r = 0; $all_g = 0; $all_b = 0; } else { $all_r = 255; $all_g = 255; $all_b = 255; } } my $bits = $pre; foreach my $led (0 .. $led_count - 1) { #next if int rand 2; unless (defined $led_data[$led]) { $led_data[$led][0] = 0; $led_data[$led][1] = 0; $led_data[$led][2] = 0; if ($rand_init) { if (int rand 2) { $led_data[$led][0] = int rand 256; $led_data[$led][1] = int rand 256; $led_data[$led][2] = int rand 256; } } } unless (defined $led_dec[$led]) { $led_dec[$led][0] = 0; $led_dec[$led][1] = 0; $led_dec[$led][2] = 0; if ($rand_dec) { if (int rand 2) { $led_dec[$led][0] = int rand 2; $led_dec[$led][1] = int rand 2; $led_dec[$led][2] = int rand 2; } } } if ($fade_r) { if ($led_dec[$led][0]) { $led_data[$led][0] -= $step_down_r; } else { $led_data[$led][0] += $step_up_r; } if ($led_data[$led][0] <= 0) { $led_data[$led][0] = 0; $led_dec[$led][0] = 0; } elsif ($led_data[$led][0] >= 255) { $led_data[$led][0] = 255; $led_dec[$led][0] = 1; } } if ($fade_g) { if ($led_dec[$led][1]) { $led_data[$led][1] -= $step_down_g; } else { $led_data[$led][1] += $step_up_g; } if ($led_data[$led][1] <= 0) { $led_data[$led][1] = 0; $led_dec[$led][1] = 0; } elsif ($led_data[$led][1] >= 255) { $led_data[$led][1] = 255; $led_dec[$led][1] = 1; } } if ($fade_b) { if ($led_dec[$led][2]) { $led_data[$led][2] -= $step_down_b; } else { $led_data[$led][2] += $step_up_b; } if ($led_data[$led][2] <= 0) { $led_data[$led][2] = 0; $led_dec[$led][2] = 0; } elsif ($led_data[$led][2] >= 255) { $led_data[$led][2] = 255; $led_dec[$led][2] = 1; } } $led_data[$led][0] = $all_r if defined $all_r; $led_data[$led][1] = $all_g if defined $all_g; $led_data[$led][2] = $all_b if defined $all_b; my $r = $led_data[$led][0]; my $g = $led_data[$led][1]; my $b = $led_data[$led][2]; if ($grb) { $bits .= $led_to_spi_map[$g]; $bits .= $led_to_spi_map[$r]; $bits .= $led_to_spi_map[$b]; } else { $bits .= $led_to_spi_map[$r]; $bits .= $led_to_spi_map[$g]; $bits .= $led_to_spi_map[$b]; } } $bits .= $reset; if ($debug) { my @leds; foreach my $led (@led_data) { push @leds, join '', map { sprintf "%02x", $_ } @$led; } printf "leds = %s\n", join ' ', @leds; } if ($debug) { my @bytes = unpack 'C*', $bits; my @hex_bytes = map { sprintf "%02x", $_ } @bytes; my $hex_byte_string = join ' ', @hex_bytes; my @bit_list; foreach my $byte (@hex_bytes) { my $bin = sprintf "%08b", hex $byte; push @bit_list, $bin; } my $bit_string = join '', @bit_list; my $bit_string_space = join ' ', @bit_list; printf "spi bits = %s\n", $bit_string; printf "spi bytes = %s\n", $bit_string_space; my $bit_string_trimmed = $bit_string; $bit_string_trimmed =~ s/^0{$reset_bits}//; $bit_string_trimmed =~ s/0{$reset_bits}$//; my @seq_list; foreach my $seq ($bit_string_trimmed =~ m/(.{1,3})/gs) { $seq = sprintf "%-3s", $seq; $seq =~ tr/ /0/; push @seq_list, $seq; } printf "spi bits trimmed = %s\n", join ' ', @seq_list; printf "spi bytes = %s\n", $hex_byte_string; } if ($production) { if ($bin) { print $bits; } else { printf "%s\n", join ' ', unpack '(H2)*', $bits; } } } sub monosleep { my $now = monotime; my $sleep = $deadline - $now; if ($sleep > 0) { sleep $sleep; } else { $deadline = $now; } $deadline += $interval; } sub loop { while (1) { gen_spi_bytes foreach 0 .. $fps - 1; monosleep; } } sub perf { my ($stats_collected_before, %times); while (1) { my $time = time; $times{$time} = 0 unless defined $times{$time}; if ($max) { gen_spi_bytes; my $time = time; $times{$time}++; } else { foreach (0 .. $fps - 1) { gen_spi_bytes; my $time = time; $times{$time}++; } monosleep; } if (keys %times > 1) { if ($stats_collected_before) { my ($time) = sort keys %times; printf "@ %s = %s leds, %s fps\n", $time, $led_count, $times{$time}; } else { print "waiting for fps stats to stabilize...\n"; } $stats_collected_before = 1; %times = (); } } } sub wrap { my (undef, $device, $speed, $buffer) = @ARGV; return unless $device; return unless $speed; return unless $buffer; my $open = sprintf "| spi -f %s -A -d w -m 0 -s %s -c %s", $device, $speed, $buffer; while () { open(F, $open) || die $!; print F; close(F) || die $!; } } sub run { $deadline = monotime + $interval; gen_bit_map; if ($loop) { loop; } elsif ($perf) { perf; } elsif ($wrap) { wrap; } else { gen_spi_bytes; } } run;