From 815e7846258b78b9a7227937a9f090b405140344 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Fri, 16 Jul 2021 17:01:55 +0100 Subject: [PATCH] Plasma 2040 library & examples Library: Includes classes for driving WS2812 and APA102 LEDs and defines for Plasma features. Encoder Example: Supports connecting a Rotary Encoder via the Qw'St connector. Works with APA102 or WS281X pixels. Pressing A will cycle between: 1. Colour change 2. Brightness change 3. Cycle delay Pressing B will switch back into auto-cycle mode. Turning the encoder at any time will switch out of auto cycle mode into parameter adjust mode. Also includes a bugfix to Rotary Encoder for getting the interrupt correctly. Rainbow Example: Basic rainbow cycle, press B to speed up and A to slow down. --- drivers/ioexpander/ioexpander.cpp | 7 + drivers/ioexpander/ioexpander.hpp | 1 + examples/CMakeLists.txt | 6 +- examples/plasma_2040/CMakeLists.txt | 2 + examples/plasma_2040/plasma2040_rainbow.cmake | 10 ++ examples/plasma_2040/plasma2040_rainbow.cpp | 66 ++++++++ examples/plasma_2040/plasma2040_rotary.cmake | 12 ++ examples/plasma_2040/plasma2040_rotary.cpp | 152 ++++++++++++++++++ libraries/CMakeLists.txt | 1 + .../breakout_encoder/breakout_encoder.cpp | 4 + .../breakout_encoder/breakout_encoder.hpp | 2 +- libraries/plasma2040/CMakeLists.txt | 1 + libraries/plasma2040/apa102.cpp | 90 +++++++++++ libraries/plasma2040/apa102.hpp | 75 +++++++++ libraries/plasma2040/apa102.pio | 10 ++ libraries/plasma2040/plasma2040.cmake | 17 ++ libraries/plasma2040/plasma2040.hpp | 17 ++ libraries/plasma2040/ws2812.cpp | 86 ++++++++++ libraries/plasma2040/ws2812.hpp | 75 +++++++++ libraries/plasma2040/ws2812.pio | 26 +++ 20 files changed, 657 insertions(+), 3 deletions(-) create mode 100644 examples/plasma_2040/CMakeLists.txt create mode 100644 examples/plasma_2040/plasma2040_rainbow.cmake create mode 100644 examples/plasma_2040/plasma2040_rainbow.cpp create mode 100644 examples/plasma_2040/plasma2040_rotary.cmake create mode 100644 examples/plasma_2040/plasma2040_rotary.cpp create mode 100644 libraries/plasma2040/CMakeLists.txt create mode 100644 libraries/plasma2040/apa102.cpp create mode 100644 libraries/plasma2040/apa102.hpp create mode 100644 libraries/plasma2040/apa102.pio create mode 100644 libraries/plasma2040/plasma2040.cmake create mode 100644 libraries/plasma2040/plasma2040.hpp create mode 100644 libraries/plasma2040/ws2812.cpp create mode 100644 libraries/plasma2040/ws2812.hpp create mode 100644 libraries/plasma2040/ws2812.pio diff --git a/drivers/ioexpander/ioexpander.cpp b/drivers/ioexpander/ioexpander.cpp index 01389503..dec135a5 100644 --- a/drivers/ioexpander/ioexpander.cpp +++ b/drivers/ioexpander/ioexpander.cpp @@ -743,6 +743,13 @@ namespace pimoroni { return encoder_offset[channel] + value; } + void IOExpander::clear_rotary_encoder(uint8_t channel) { + channel -= 1; + encoder_last[channel] = 0; + uint8_t reg = ENC_COUNT[channel]; + i2c->reg_write_uint8(address, reg, 0); + } + uint8_t IOExpander::get_bit(uint8_t reg, uint8_t bit) { // Returns the specified bit (nth position from right) from a register return i2c->reg_read_uint8(address, reg) & (1 << bit); diff --git a/drivers/ioexpander/ioexpander.hpp b/drivers/ioexpander/ioexpander.hpp index 66138c0c..f7292545 100644 --- a/drivers/ioexpander/ioexpander.hpp +++ b/drivers/ioexpander/ioexpander.hpp @@ -214,6 +214,7 @@ namespace pimoroni { void setup_rotary_encoder(uint8_t channel, uint8_t pin_a, uint8_t pin_b, uint8_t pin_c = 0, bool count_microsteps = false); int16_t read_rotary_encoder(uint8_t channel); + void clear_rotary_encoder(uint8_t channel); private: uint8_t i2c_reg_read_uint8(uint8_t reg); diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index f37fbe9d..9b4ad759 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -16,6 +16,8 @@ add_subdirectory(breakout_msa301) add_subdirectory(breakout_bme688) add_subdirectory(breakout_bmp280) add_subdirectory(breakout_bme280) +add_subdirectory(breakout_as7262) +add_subdirectory(breakout_bh1745) add_subdirectory(pico_display) add_subdirectory(pico_unicorn) @@ -30,5 +32,5 @@ add_subdirectory(pico_tof_display) add_subdirectory(pico_trackball_display) add_subdirectory(pico_audio) add_subdirectory(pico_wireless) -add_subdirectory(breakout_as7262) -add_subdirectory(breakout_bh1745) + +add_subdirectory(plasma_2040) diff --git a/examples/plasma_2040/CMakeLists.txt b/examples/plasma_2040/CMakeLists.txt new file mode 100644 index 00000000..9e749f5a --- /dev/null +++ b/examples/plasma_2040/CMakeLists.txt @@ -0,0 +1,2 @@ +include(plasma2040_rotary.cmake) +include(plasma2040_rainbow.cmake) \ No newline at end of file diff --git a/examples/plasma_2040/plasma2040_rainbow.cmake b/examples/plasma_2040/plasma2040_rainbow.cmake new file mode 100644 index 00000000..c0558e65 --- /dev/null +++ b/examples/plasma_2040/plasma2040_rainbow.cmake @@ -0,0 +1,10 @@ +add_executable(plasma2040_rainbow plasma2040_rainbow.cpp) + +target_link_libraries(plasma2040_rainbow + pico_stdlib + plasma2040 + rgbled + button + ) + +pico_add_extra_outputs(plasma2040_rainbow) diff --git a/examples/plasma_2040/plasma2040_rainbow.cpp b/examples/plasma_2040/plasma2040_rainbow.cpp new file mode 100644 index 00000000..6ec11403 --- /dev/null +++ b/examples/plasma_2040/plasma2040_rainbow.cpp @@ -0,0 +1,66 @@ +#include +#include +#include + +#include "pico/stdlib.h" + +#include "plasma2040.hpp" + +#include "common/pimoroni_common.hpp" +#include "rgbled.hpp" +#include "button.hpp" + +/* +Press "B" to speed up the LED cycling effect. +Press "A" to slow it down again. +*/ + +using namespace pimoroni; + +// Set how many LEDs you have +const uint N_LEDS = 30; + +// Pick *one* LED type by uncommenting the relevant line below: + +// APA102-style LEDs with Data/Clock lines. AKA DotStar +//plasma::APA102 led_strip(N_LEDS, pio0, 0, plasma::PIN_DAT, plasma::PIN_CLK); + +// WS28X-style LEDs with a single signal line. AKA NeoPixel +plasma::WS2812 led_strip(N_LEDS, pio0, 0, plasma::PIN_DAT); + + +Button button_a(plasma::BUTTON_A, Polarity::ACTIVE_LOW, 50); +Button button_b(plasma::BUTTON_B, Polarity::ACTIVE_LOW, 50); +RGBLED led(plasma::LED_R, plasma::LED_G, plasma::LED_B); + + +int main() { + stdio_init_all(); + + led_strip.start(60); + + int speed = 10; + float offset = 0.0f; + + while (true) { + bool a = button_a.read(); + bool b = button_b.read(); + + if(a) speed--; + if(b) speed++; + speed = std::min((int)255, std::max((int)1, speed)); + + offset += float(speed) / 2000.0f; + + for(auto i = 0u; i < led_strip.num_leds; ++i) { + float hue = float(i) / led_strip.num_leds; + led_strip.set_hsv(i, hue + offset, 1.0f, 1.0f); + } + + led.set_rgb(speed, 0, 255 - speed); + + // Sleep time controls the rate at which the LED buffer is updated + // but *not* the actual framerate at which the buffer is sent to the LEDs + sleep_ms(1000 / 60); + } +} diff --git a/examples/plasma_2040/plasma2040_rotary.cmake b/examples/plasma_2040/plasma2040_rotary.cmake new file mode 100644 index 00000000..5dfc7087 --- /dev/null +++ b/examples/plasma_2040/plasma2040_rotary.cmake @@ -0,0 +1,12 @@ +add_executable(plasma2040_rotary plasma2040_rotary.cpp) + +target_link_libraries(plasma2040_rotary + pico_stdlib + plasma2040 + breakout_encoder + pimoroni_i2c + rgbled + button + ) + +pico_add_extra_outputs(plasma2040_rotary) diff --git a/examples/plasma_2040/plasma2040_rotary.cpp b/examples/plasma_2040/plasma2040_rotary.cpp new file mode 100644 index 00000000..650491d4 --- /dev/null +++ b/examples/plasma_2040/plasma2040_rotary.cpp @@ -0,0 +1,152 @@ +#include +#include +#include + +#include "pico/stdlib.h" + +#include "plasma2040.hpp" + +#include "common/pimoroni_common.hpp" +#include "breakout_encoder.hpp" +#include "rgbled.hpp" +#include "button.hpp" + +using namespace pimoroni; + +// Set how many LEDs you have +const uint N_LEDS = 30; + +// Pick *one* LED type by uncommenting the relevant line below: + +// APA102-style LEDs with Data/Clock lines. AKA DotStar +//plasma::APA102 led_strip(N_LEDS, pio0, 0, plasma::PIN_DAT, plasma::PIN_CLK); + +// WS28X-style LEDs with a single signal line. AKA NeoPixel +plasma::WS2812 led_strip(N_LEDS, pio0, 0, plasma::PIN_DAT); + + + +Button button_a(plasma::BUTTON_A); +Button button_b(plasma::BUTTON_B); + +RGBLED led(plasma::LED_R, plasma::LED_G, plasma::LED_B); + +I2C i2c(BOARD::PICO_EXPLORER); +BreakoutEncoder enc(&i2c); + +enum ENCODER_MODE { + COLOUR, + ANGLE, + BRIGHTNESS, + TIME +}; + + +void colour_cycle(float hue, float t, float angle) { + t /= 200.0f; + + for (auto i = 0u; i < led_strip.num_leds; ++i) { + float offset = (M_PI * i) / led_strip.num_leds; + offset = sinf(offset + t) * angle; + led_strip.set_hsv(i, (hue + offset) / 360.0f, 1.0f, 1.0f); + } +} + +void gauge(uint v, uint vmax = 100) { + uint light_pixels = led_strip.num_leds * v / vmax; + + for (auto i = 0u; i < led_strip.num_leds; ++i) { + if(i < light_pixels) { + led_strip.set_rgb(i, 0, 255, 0); + } else { + led_strip.set_rgb(i, 255, 0, 0); + } + } +} + +int main() { + stdio_init_all(); + + led_strip.start(60); + + bool encoder_detected = enc.init(); + enc.clear_interrupt_flag(); + + int speed = 50; + float hue = 0; + int angle = 120; + int8_t brightness = 16; + bool cycle = true; + ENCODER_MODE mode = ENCODER_MODE::COLOUR; + while (true) { + uint32_t t = millis(); + if(encoder_detected) { + if(enc.get_interrupt_flag()) { + int count = enc.read(); + enc.clear_interrupt_flag(); + enc.clear(); + + cycle = false; + switch(mode) { + case ENCODER_MODE::COLOUR: + hue += count; + brightness = std::min((int8_t)359, brightness); + brightness = std::max((int8_t)0, brightness); + colour_cycle(hue, 0, (float)angle); + break; + case ENCODER_MODE::ANGLE: + angle += count; + angle = std::min((int)359, angle); + angle = std::max((int)0, angle); + colour_cycle(hue, 0, (float)angle); + break; + case ENCODER_MODE::BRIGHTNESS: + brightness += count; + brightness = std::min((int8_t)31, brightness); + brightness = std::max((int8_t)0, brightness); + led_strip.set_brightness(brightness); + gauge(brightness, 31); + break; + case ENCODER_MODE::TIME: + speed += count; + speed = std::min((int)100, speed); + speed = std::max((int)0, speed); + gauge(speed, 100); + break; + } + } + } + bool a_pressed = button_a.read(); + bool b_pressed = button_b.read(); + + if(b_pressed) cycle = true; + + switch(mode) { + case ENCODER_MODE::COLOUR: + led.set_rgb(255, 0, 0); + if(a_pressed) mode = ENCODER_MODE::ANGLE; + break; + case ENCODER_MODE::ANGLE: + led.set_rgb(255, 255, 0); + if(a_pressed) mode = ENCODER_MODE::BRIGHTNESS; + break; + case ENCODER_MODE::BRIGHTNESS: + led.set_rgb(0, 255, 0); + if(a_pressed) mode = ENCODER_MODE::TIME; + break; + case ENCODER_MODE::TIME: + led.set_rgb(0, 0, 255); + if(a_pressed) mode = ENCODER_MODE::COLOUR; + break; + } + + if(cycle) colour_cycle(hue, t * speed / 100, (float)angle); + + auto first_led = led_strip.get(0); + enc.set_led(first_led.r, first_led.g, first_led.b); + + // Sleep time controls the rate at which the LED buffer is updated + // but *not* the actual framerate at which the buffer is sent to the LEDs + sleep_ms(1000 / 60); + } +} diff --git a/libraries/CMakeLists.txt b/libraries/CMakeLists.txt index 0ff54c42..0aec3bdf 100644 --- a/libraries/CMakeLists.txt +++ b/libraries/CMakeLists.txt @@ -22,3 +22,4 @@ add_subdirectory(pico_scroll) add_subdirectory(pico_explorer) add_subdirectory(pico_rgb_keypad) add_subdirectory(pico_wireless) +add_subdirectory(plasma2040) diff --git a/libraries/breakout_encoder/breakout_encoder.cpp b/libraries/breakout_encoder/breakout_encoder.cpp index afefab48..551a6baa 100644 --- a/libraries/breakout_encoder/breakout_encoder.cpp +++ b/libraries/breakout_encoder/breakout_encoder.cpp @@ -87,6 +87,10 @@ namespace pimoroni { return (ioe.get_interrupt_flag() > 0); } + void BreakoutEncoder::clear() { + ioe.clear_rotary_encoder(ENC_CHANNEL); + } + int16_t BreakoutEncoder::read() { int16_t count = ioe.read_rotary_encoder(ENC_CHANNEL); if(direction != DIRECTION_CW) diff --git a/libraries/breakout_encoder/breakout_encoder.hpp b/libraries/breakout_encoder/breakout_encoder.hpp index 6eeae4f3..ddeca544 100644 --- a/libraries/breakout_encoder/breakout_encoder.hpp +++ b/libraries/breakout_encoder/breakout_encoder.hpp @@ -23,7 +23,6 @@ namespace pimoroni { static const uint8_t DEFAULT_I2C_ADDRESS = 0x0F; static constexpr float DEFAULT_BRIGHTNESS = 1.0f; //Effectively the maximum fraction of the period that the LED will be on static const Direction DEFAULT_DIRECTION = DIRECTION_CW; - static const uint8_t PIN_UNUSED = UINT8_MAX; static const uint32_t DEFAULT_TIMEOUT = 1; private: @@ -90,6 +89,7 @@ namespace pimoroni { bool available(); int16_t read(); + void clear(); }; } \ No newline at end of file diff --git a/libraries/plasma2040/CMakeLists.txt b/libraries/plasma2040/CMakeLists.txt new file mode 100644 index 00000000..36d1bb41 --- /dev/null +++ b/libraries/plasma2040/CMakeLists.txt @@ -0,0 +1 @@ +include(plasma2040.cmake) \ No newline at end of file diff --git a/libraries/plasma2040/apa102.cpp b/libraries/plasma2040/apa102.cpp new file mode 100644 index 00000000..da015dc1 --- /dev/null +++ b/libraries/plasma2040/apa102.cpp @@ -0,0 +1,90 @@ +#include "apa102.hpp" + +namespace plasma { + +APA102::APA102(uint num_leds, PIO pio, uint sm, uint pin_dat, uint pin_clk, uint freq) : num_leds(num_leds), pio(pio), sm(sm) { + uint offset = pio_add_program(pio, &apa102_program); + + pio_sm_set_pins_with_mask(pio, sm, 0, (1u << pin_clk) | (1u << pin_dat)); + pio_sm_set_pindirs_with_mask(pio, sm, ~0u, (1u << pin_clk) | (1u << pin_dat)); + pio_gpio_init(pio, pin_clk); + pio_gpio_init(pio, pin_dat); + + pio_sm_config c = apa102_program_get_default_config(offset); + sm_config_set_out_pins(&c, pin_dat, 1); + sm_config_set_sideset_pins(&c, pin_clk); + + sm_config_set_out_shift(&c, false, true, 32); + sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX); + + // The PIO program transmits 1 bit every 2 execution cycles + float div = (float)clock_get_hz(clk_sys) / (2 * freq); + sm_config_set_clkdiv(&c, div); + + pio_sm_init(pio, sm, offset, &c); + pio_sm_set_enabled(pio, sm, true); + + dma_channel = dma_claim_unused_channel(true); + dma_channel_config config = dma_channel_get_default_config(dma_channel); + channel_config_set_bswap(&config, true); + channel_config_set_dreq(&config, pio_get_dreq(pio, sm, true)); + channel_config_set_transfer_data_size(&config, DMA_SIZE_32); + channel_config_set_read_increment(&config, true); + dma_channel_configure(dma_channel, &config, &pio->txf[sm], NULL, 0, false); + + buffer = new RGB[num_leds]; +} + +bool APA102::dma_timer_callback(struct repeating_timer *t) { + ((APA102*)t->user_data)->update(); + return true; +} + +void APA102::update(bool blocking) { + while(dma_channel_is_busy(dma_channel)) {}; // Block waiting for DMA finish + pio->txf[sm] = 0x00000000; // Output the APA102 start-of-frame bytes + dma_channel_set_trans_count(dma_channel, num_leds, false); + dma_channel_set_read_addr(dma_channel, buffer, true); + if (!blocking) return; + while(dma_channel_is_busy(dma_channel)) {}; // Block waiting for DMA finish +} + +bool APA102::start(uint fps) { + add_repeating_timer_ms(-(1000 / fps), dma_timer_callback, (void*)this, &timer); + return true; +} + +bool APA102::stop() { + dma_channel_unclaim(dma_channel); + return cancel_repeating_timer(&timer); +} + +void APA102::set_hsv(uint32_t index, float h, float s, float v) { + float i = floor(h * 6.0f); + float f = h * 6.0f - i; + v *= 255.0f; + uint8_t p = v * (1.0f - s); + uint8_t q = v * (1.0f - f * s); + uint8_t t = v * (1.0f - (1.0f - f) * s); + + switch (int(i) % 6) { + case 0: buffer[index].rgb(v, t, p); break; + case 1: buffer[index].rgb(q, v, p); break; + case 2: buffer[index].rgb(p, v, t); break; + case 3: buffer[index].rgb(p, q, v); break; + case 4: buffer[index].rgb(t, p, v); break; + case 5: buffer[index].rgb(v, p, q); break; + } +} + +void APA102::set_rgb(uint32_t index, uint8_t r, uint8_t g, uint8_t b) { + buffer[index].rgb(r, g, b); +} + +void APA102::set_brightness(uint8_t b) { + for (auto i = 0u; i < num_leds; ++i) { + buffer[i].brightness(b); + } +} + +} \ No newline at end of file diff --git a/libraries/plasma2040/apa102.hpp b/libraries/plasma2040/apa102.hpp new file mode 100644 index 00000000..54ddec23 --- /dev/null +++ b/libraries/plasma2040/apa102.hpp @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2020 Raspberry Pi (Trading) Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +/* +This code is significantly modified from the PIO apa102 example +found here: https://github.com/raspberrypi/pico-examples/tree/master/pio/apa102 +*/ + +#pragma once + +#include +#include + +#include "apa102.pio.h" + +#include "pico/stdlib.h" +#include "hardware/pio.h" +#include "hardware/dma.h" +#include "hardware/irq.h" +#include "hardware/clocks.h" +#include "hardware/timer.h" + +namespace plasma { + + class APA102 { + public: + static const uint DEFAULT_SERIAL_FREQ = 20 * 1000 * 1000; // 20MHz +#pragma pack(push, 1) + union alignas(4) RGB { + struct { + uint8_t sof = 0b11101111; + uint8_t b; + uint8_t g; + uint8_t r; + } ; + uint32_t srgb; + void operator=(uint32_t v) { + srgb = v; + }; + void brightness(uint8_t b) { + sof = 0b11100000 | b; + }; + void rgb(uint8_t r, uint8_t g, uint8_t b) { + this->r = r; + this->g = g; + this->b = b; + } + RGB() {} + }; +#pragma pack(pop) + RGB *buffer; + uint32_t num_leds; + + APA102(uint num_leds, PIO pio, uint sm, uint pin_dat, uint pin_clk, uint freq=DEFAULT_SERIAL_FREQ); + bool start(uint fps=60); + bool stop(); + void update(bool blocking=false); + void set_hsv(uint32_t index, float h, float s, float v); + void set_rgb(uint32_t index, uint8_t r, uint8_t g, uint8_t b); + void set_brightness(uint8_t b); + RGB get(uint32_t index) {return buffer[index];}; + + static bool dma_timer_callback(struct repeating_timer *t); + + private: + uint32_t fps; + PIO pio; + uint sm; + int dma_channel; + struct repeating_timer timer; + }; +} \ No newline at end of file diff --git a/libraries/plasma2040/apa102.pio b/libraries/plasma2040/apa102.pio new file mode 100644 index 00000000..139a1543 --- /dev/null +++ b/libraries/plasma2040/apa102.pio @@ -0,0 +1,10 @@ +; +; Copyright (c) 2020 Raspberry Pi (Trading) Ltd. +; +; SPDX-License-Identifier: BSD-3-Clause +; + +.program apa102 +.side_set 1 + out pins, 1 side 0 ; Stall here when no data (still asserts clock low) + nop side 1 \ No newline at end of file diff --git a/libraries/plasma2040/plasma2040.cmake b/libraries/plasma2040/plasma2040.cmake new file mode 100644 index 00000000..55abb9b0 --- /dev/null +++ b/libraries/plasma2040/plasma2040.cmake @@ -0,0 +1,17 @@ +add_library(plasma2040 INTERFACE) + +target_sources(plasma2040 INTERFACE + ${CMAKE_CURRENT_LIST_DIR}/apa102.cpp + ${CMAKE_CURRENT_LIST_DIR}/ws2812.cpp +) + +target_include_directories(plasma2040 INTERFACE ${CMAKE_CURRENT_LIST_DIR}) + +target_link_libraries(plasma2040 INTERFACE + pico_stdlib + hardware_pio + hardware_dma + ) + +pico_generate_pio_header(plasma2040 ${CMAKE_CURRENT_LIST_DIR}/apa102.pio) +pico_generate_pio_header(plasma2040 ${CMAKE_CURRENT_LIST_DIR}/ws2812.pio) \ No newline at end of file diff --git a/libraries/plasma2040/plasma2040.hpp b/libraries/plasma2040/plasma2040.hpp new file mode 100644 index 00000000..d57a66e8 --- /dev/null +++ b/libraries/plasma2040/plasma2040.hpp @@ -0,0 +1,17 @@ +#pragma once +#include "pico/stdlib.h" + +#include "apa102.hpp" +#include "ws2812.hpp" + +namespace plasma { +const uint LED_R = 16; +const uint LED_G = 17; +const uint LED_B = 18; + +const uint BUTTON_A = 12; +const uint BUTTON_B = 13; + +const uint PIN_CLK = 14; // Used only for APA102 +const uint PIN_DAT = 15; // Used for both APA102 and WS2812 +} \ No newline at end of file diff --git a/libraries/plasma2040/ws2812.cpp b/libraries/plasma2040/ws2812.cpp new file mode 100644 index 00000000..bcb50e07 --- /dev/null +++ b/libraries/plasma2040/ws2812.cpp @@ -0,0 +1,86 @@ +#include "ws2812.hpp" + +namespace plasma { + +WS2812::WS2812(uint num_leds, PIO pio, uint sm, uint pin, uint freq) : num_leds(num_leds), pio(pio), sm(sm) { + uint offset = pio_add_program(pio, &ws2812_program); + + pio_gpio_init(pio, pin); + pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true); + + pio_sm_config c = ws2812_program_get_default_config(offset); + sm_config_set_sideset_pins(&c, pin); + + sm_config_set_out_shift(&c, false, true, 24); // Discard first (APA102 global brightness) byte. TODO support RGBW WS281X LEDs + sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX); + + int cycles_per_bit = ws2812_T1 + ws2812_T2 + ws2812_T3; + float div = clock_get_hz(clk_sys) / (freq * cycles_per_bit); + sm_config_set_clkdiv(&c, div); + + pio_sm_init(pio, sm, offset, &c); + pio_sm_set_enabled(pio, sm, true); + + dma_channel = dma_claim_unused_channel(true); + dma_channel_config config = dma_channel_get_default_config(dma_channel); + channel_config_set_bswap(&config, true); + channel_config_set_dreq(&config, pio_get_dreq(pio, sm, true)); + channel_config_set_transfer_data_size(&config, DMA_SIZE_32); + channel_config_set_read_increment(&config, true); + dma_channel_set_trans_count(dma_channel, num_leds, false); + dma_channel_set_read_addr(dma_channel, (uint32_t *)buffer, false); + dma_channel_configure(dma_channel, &config, &pio->txf[sm], NULL, 0, false); + + buffer = new RGB[num_leds]; +} + +bool WS2812::dma_timer_callback(struct repeating_timer *t) { + ((WS2812*)t->user_data)->update(); + return true; +} + +void WS2812::update(bool blocking) { + while(dma_channel_is_busy(dma_channel)) {}; // Block waiting for DMA finish + dma_channel_set_trans_count(dma_channel, num_leds, false); + dma_channel_set_read_addr(dma_channel, buffer, true); + if (!blocking) return; + while(dma_channel_is_busy(dma_channel)) {}; // Block waiting for DMA finish +} + +bool WS2812::start(uint fps) { + add_repeating_timer_ms(-(1000 / fps), dma_timer_callback, (void*)this, &timer); + return true; +} + +bool WS2812::stop() { + dma_channel_unclaim(dma_channel); + return cancel_repeating_timer(&timer); +} + +void WS2812::set_hsv(uint32_t index, float h, float s, float v) { + float i = floor(h * 6.0f); + float f = h * 6.0f - i; + v *= 255.0f; + uint8_t p = v * (1.0f - s); + uint8_t q = v * (1.0f - f * s); + uint8_t t = v * (1.0f - (1.0f - f) * s); + + switch (int(i) % 6) { + case 0: buffer[index].rgb(v, t, p); break; + case 1: buffer[index].rgb(q, v, p); break; + case 2: buffer[index].rgb(p, v, t); break; + case 3: buffer[index].rgb(p, q, v); break; + case 4: buffer[index].rgb(t, p, v); break; + case 5: buffer[index].rgb(v, p, q); break; + } +} + +void WS2812::set_rgb(uint32_t index, uint8_t r, uint8_t g, uint8_t b) { + buffer[index].rgb(r, g, b); +} + +void WS2812::set_brightness(uint8_t b) { + // WS2812 LEDs have no global brightness +} + +} \ No newline at end of file diff --git a/libraries/plasma2040/ws2812.hpp b/libraries/plasma2040/ws2812.hpp new file mode 100644 index 00000000..f3e6341b --- /dev/null +++ b/libraries/plasma2040/ws2812.hpp @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2020 Raspberry Pi (Trading) Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +/* +This code is significantly modified from the PIO apa102 example +found here: https://github.com/raspberrypi/pico-examples/tree/master/pio/ws2812 +*/ + +#pragma once + +#include +#include + +#include "ws2812.pio.h" + +#include "pico/stdlib.h" +#include "hardware/pio.h" +#include "hardware/dma.h" +#include "hardware/irq.h" +#include "hardware/clocks.h" +#include "hardware/timer.h" + +namespace plasma { + + class WS2812 { + public: + static const uint SERIAL_FREQ_400KHZ = 400000; + static const uint SERIAL_FREQ_800KHZ = 800000; + static const uint DEFAULT_SERIAL_FREQ = SERIAL_FREQ_400KHZ; +#pragma pack(push, 1) + union alignas(4) RGB { + struct { + uint8_t r; + uint8_t g; + uint8_t b; + uint8_t w = 0b00000000; + } ; + uint32_t srgb; + void operator=(uint32_t v) { + srgb = v; + }; + void brightness(uint8_t b) {};; + void rgb(uint8_t r, uint8_t g, uint8_t b) { + this->r = r; + this->g = g; + this->b = b; + } + RGB() {}; + }; +#pragma pack(pop) + RGB *buffer; + uint32_t num_leds; + + WS2812(uint num_leds, PIO pio, uint sm, uint pin, uint freq=DEFAULT_SERIAL_FREQ); + bool start(uint fps=60); + bool stop(); + void update(bool blocking=false); + void set_hsv(uint32_t index, float h, float s, float v); + void set_rgb(uint32_t index, uint8_t r, uint8_t g, uint8_t b); + void set_brightness(uint8_t b); + RGB get(uint32_t index) {return buffer[index];}; + + static bool dma_timer_callback(struct repeating_timer *t); + + private: + uint32_t fps; + PIO pio; + uint sm; + int dma_channel; + struct repeating_timer timer; + }; +} \ No newline at end of file diff --git a/libraries/plasma2040/ws2812.pio b/libraries/plasma2040/ws2812.pio new file mode 100644 index 00000000..79e603cf --- /dev/null +++ b/libraries/plasma2040/ws2812.pio @@ -0,0 +1,26 @@ +; +; Copyright (c) 2020 Raspberry Pi (Trading) Ltd. +; +; SPDX-License-Identifier: BSD-3-Clause +; + +.program ws2812 +.side_set 1 + +.define public T1 2 +.define public T2 5 +.define public T3 3 + +.lang_opt python sideset_init = pico.PIO.OUT_HIGH +.lang_opt python out_init = pico.PIO.OUT_HIGH +.lang_opt python out_shiftdir = 1 + +.wrap_target +bitloop: + out x, 1 side 0 [T3 - 1] ; Side-set still takes place when instruction stalls + jmp !x do_zero side 1 [T1 - 1] ; Branch on the bit we shifted out. Positive pulse +do_one: + jmp bitloop side 1 [T2 - 1] ; Continue driving high, for a long pulse +do_zero: + nop side 0 [T2 - 1] ; Or drive low, for a short pulse +.wrap \ No newline at end of file