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.
pull/181/head
Phil Howard 2021-07-16 17:01:55 +01:00
rodzic bcde0b6784
commit 815e784625
20 zmienionych plików z 657 dodań i 3 usunięć

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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)

Wyświetl plik

@ -0,0 +1,2 @@
include(plasma2040_rotary.cmake)
include(plasma2040_rainbow.cmake)

Wyświetl plik

@ -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)

Wyświetl plik

@ -0,0 +1,66 @@
#include <stdio.h>
#include <math.h>
#include <cstdint>
#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);
}
}

Wyświetl plik

@ -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)

Wyświetl plik

@ -0,0 +1,152 @@
#include <stdio.h>
#include <math.h>
#include <cstdint>
#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);
}
}

Wyświetl plik

@ -22,3 +22,4 @@ add_subdirectory(pico_scroll)
add_subdirectory(pico_explorer)
add_subdirectory(pico_rgb_keypad)
add_subdirectory(pico_wireless)
add_subdirectory(plasma2040)

Wyświetl plik

@ -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)

Wyświetl plik

@ -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();
};
}

Wyświetl plik

@ -0,0 +1 @@
include(plasma2040.cmake)

Wyświetl plik

@ -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);
}
}
}

Wyświetl plik

@ -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 <math.h>
#include <cstdint>
#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;
};
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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)

Wyświetl plik

@ -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
}

Wyświetl plik

@ -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
}
}

Wyświetl plik

@ -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 <math.h>
#include <cstdint>
#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;
};
}

Wyświetl plik

@ -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