Rp2040 pio ledstrip (#4818)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
Daniel Mahaney 2023-05-21 18:31:27 -04:00 committed by GitHub
parent 784cc3bc29
commit a15ac06771
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 610 additions and 3 deletions

View file

@ -220,6 +220,7 @@ esphome/components/restart/* @esphome/core
esphome/components/rf_bridge/* @jesserockz esphome/components/rf_bridge/* @jesserockz
esphome/components/rgbct/* @jesserockz esphome/components/rgbct/* @jesserockz
esphome/components/rp2040/* @jesserockz esphome/components/rp2040/* @jesserockz
esphome/components/rp2040_pio_led_strip/* @Papa-DMan
esphome/components/rp2040_pwm/* @jesserockz esphome/components/rp2040_pwm/* @jesserockz
esphome/components/rtttl/* @glmnet esphome/components/rtttl/* @glmnet
esphome/components/safe_mode/* @jsuanet @paulmonigatti esphome/components/safe_mode/* @jsuanet @paulmonigatti

View file

@ -1,4 +1,7 @@
import logging import logging
import os
from string import ascii_letters, digits
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
@ -12,9 +15,11 @@ from esphome.const import (
KEY_TARGET_FRAMEWORK, KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM, KEY_TARGET_PLATFORM,
) )
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, coroutine_with_priority, EsphomeError
from esphome.helpers import mkdir_p, write_file
import esphome.platformio_api as api
from .const import KEY_BOARD, KEY_RP2040, rp2040_ns from .const import KEY_BOARD, KEY_PIO_FILES, KEY_RP2040, rp2040_ns
# force import gpio to register pin schema # force import gpio to register pin schema
from .gpio import rp2040_pin_to_code # noqa from .gpio import rp2040_pin_to_code # noqa
@ -33,6 +38,8 @@ def set_core_data(config):
) )
CORE.data[KEY_RP2040][KEY_BOARD] = config[CONF_BOARD] CORE.data[KEY_RP2040][KEY_BOARD] = config[CONF_BOARD]
CORE.data[KEY_RP2040][KEY_PIO_FILES] = {}
return config return config
@ -148,7 +155,10 @@ async def to_code(config):
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION]) cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
cg.add_platformio_option( cg.add_platformio_option(
"platform_packages", "platform_packages",
[f"earlephilhower/framework-arduinopico@{conf[CONF_SOURCE]}"], [
f"earlephilhower/framework-arduinopico@{conf[CONF_SOURCE]}",
"earlephilhower/tool-pioasm-rp2040-earlephilhower",
],
) )
cg.add_platformio_option("board_build.core", "earlephilhower") cg.add_platformio_option("board_build.core", "earlephilhower")
@ -159,3 +169,53 @@ async def to_code(config):
"USE_ARDUINO_VERSION_CODE", "USE_ARDUINO_VERSION_CODE",
cg.RawExpression(f"VERSION_CODE({ver.major}, {ver.minor}, {ver.patch})"), cg.RawExpression(f"VERSION_CODE({ver.major}, {ver.minor}, {ver.patch})"),
) )
def add_pio_file(component: str, key: str, data: str):
try:
cv.validate_id_name(key)
except cv.Invalid as e:
raise EsphomeError(
f"[{component}] Invalid PIO key: {key}. Allowed characters: [{ascii_letters}{digits}_]\nPlease report an issue https://github.com/esphome/issues"
) from e
CORE.data[KEY_RP2040][KEY_PIO_FILES][key] = data
def generate_pio_files() -> bool:
import shutil
shutil.rmtree(CORE.relative_build_path("src/pio"), ignore_errors=True)
includes: list[str] = []
files = CORE.data[KEY_RP2040][KEY_PIO_FILES]
if not files:
return False
for key, data in files.items():
pio_path = CORE.relative_build_path(f"src/pio/{key}.pio")
mkdir_p(os.path.dirname(pio_path))
write_file(pio_path, data)
_LOGGER.info("Assembling PIO assembly code")
retval = api.run_platformio_cli(
"pkg",
"exec",
"--package",
"earlephilhower/tool-pioasm-rp2040-earlephilhower",
"--",
"pioasm",
pio_path,
pio_path + ".h",
)
includes.append(f"pio/{key}.pio.h")
if retval != 0:
raise EsphomeError("PIO assembly failed")
write_file(
CORE.relative_build_path("src/pio_includes.h"),
"#pragma once\n" + "\n".join([f'#include "{include}"' for include in includes]),
)
return True
# Called by writer.py
def copy_files() -> bool:
return generate_pio_files()

View file

@ -2,5 +2,6 @@ import esphome.codegen as cg
KEY_BOARD = "board" KEY_BOARD = "board"
KEY_RP2040 = "rp2040" KEY_RP2040 = "rp2040"
KEY_PIO_FILES = "pio_files"
rp2040_ns = cg.esphome_ns.namespace("rp2040") rp2040_ns = cg.esphome_ns.namespace("rp2040")

View file

@ -0,0 +1 @@
CODEOWNERS = ["@Papa-DMan"]

View file

@ -0,0 +1,139 @@
#include "led_strip.h"
#ifdef USE_RP2040
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <hardware/clocks.h>
#include <hardware/pio.h>
#include <pico/stdlib.h>
namespace esphome {
namespace rp2040_pio_led_strip {
static const char *TAG = "rp2040_pio_led_strip";
void RP2040PIOLEDStripLightOutput::setup() {
ESP_LOGCONFIG(TAG, "Setting up RP2040 LED Strip...");
size_t buffer_size = this->get_buffer_size_();
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
this->buf_ = allocator.allocate(buffer_size);
if (this->buf_ == nullptr) {
ESP_LOGE(TAG, "Failed to allocate buffer of size %u", buffer_size);
this->mark_failed();
return;
}
this->effect_data_ = allocator.allocate(this->num_leds_);
if (this->effect_data_ == nullptr) {
ESP_LOGE(TAG, "Failed to allocate effect data of size %u", this->num_leds_);
this->mark_failed();
return;
}
// Select PIO instance to use (0 or 1)
this->pio_ = pio0;
if (this->pio_ == nullptr) {
ESP_LOGE(TAG, "Failed to claim PIO instance");
this->mark_failed();
return;
}
// Load the assembled program into the PIO and get its location in the PIO's instruction memory
uint offset = pio_add_program(this->pio_, this->program_);
// Configure the state machine's PIO, and start it
this->sm_ = pio_claim_unused_sm(this->pio_, true);
if (this->sm_ < 0) {
ESP_LOGE(TAG, "Failed to claim PIO state machine");
this->mark_failed();
return;
}
this->init_(this->pio_, this->sm_, offset, this->pin_, this->max_refresh_rate_);
}
void RP2040PIOLEDStripLightOutput::write_state(light::LightState *state) {
ESP_LOGVV(TAG, "Writing state...");
if (this->is_failed()) {
ESP_LOGW(TAG, "Light is in failed state, not writing state.");
return;
}
if (this->buf_ == nullptr) {
ESP_LOGW(TAG, "Buffer is null, not writing state.");
return;
}
// assemble bits in buffer to 32 bit words with ex for GBR: 0bGGGGGGGGRRRRRRRRBBBBBBBB00000000
for (int i = 0; i < this->num_leds_; i++) {
uint8_t c1 = this->buf_[(i * 3) + 0];
uint8_t c2 = this->buf_[(i * 3) + 1];
uint8_t c3 = this->buf_[(i * 3) + 2];
uint8_t w = this->is_rgbw_ ? this->buf_[(i * 4) + 3] : 0;
uint32_t color = encode_uint32(c1, c2, c3, w);
pio_sm_put_blocking(this->pio_, this->sm_, color);
}
}
light::ESPColorView RP2040PIOLEDStripLightOutput::get_view_internal(int32_t index) const {
int32_t r = 0, g = 0, b = 0, w = 0;
switch (this->rgb_order_) {
case ORDER_RGB:
r = 0;
g = 1;
b = 2;
break;
case ORDER_RBG:
r = 0;
g = 2;
b = 1;
break;
case ORDER_GRB:
r = 1;
g = 0;
b = 2;
break;
case ORDER_GBR:
r = 2;
g = 0;
b = 1;
break;
case ORDER_BGR:
r = 2;
g = 1;
b = 0;
break;
case ORDER_BRG:
r = 1;
g = 2;
b = 0;
break;
}
uint8_t multiplier = this->is_rgbw_ ? 4 : 3;
return {this->buf_ + (index * multiplier) + r,
this->buf_ + (index * multiplier) + g,
this->buf_ + (index * multiplier) + b,
this->is_rgbw_ ? this->buf_ + (index * multiplier) + 3 : nullptr,
&this->effect_data_[index],
&this->correction_};
}
void RP2040PIOLEDStripLightOutput::dump_config() {
ESP_LOGCONFIG(TAG, "RP2040 PIO LED Strip Light Output:");
ESP_LOGCONFIG(TAG, " Pin: GPIO%d", this->pin_);
ESP_LOGCONFIG(TAG, " Number of LEDs: %d", this->num_leds_);
ESP_LOGCONFIG(TAG, " RGBW: %s", YESNO(this->is_rgbw_));
ESP_LOGCONFIG(TAG, " RGB Order: %s", rgb_order_to_string(this->rgb_order_));
ESP_LOGCONFIG(TAG, " Max Refresh Rate: %f Hz", this->max_refresh_rate_);
}
float RP2040PIOLEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; }
} // namespace rp2040_pio_led_strip
} // namespace esphome
#endif

View file

@ -0,0 +1,108 @@
#pragma once
#ifdef USE_RP2040
#include "esphome/core/color.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/components/light/addressable_light.h"
#include "esphome/components/light/light_output.h"
#include <hardware/pio.h>
#include <hardware/structs/pio.h>
#include <pico/stdio.h>
namespace esphome {
namespace rp2040_pio_led_strip {
enum RGBOrder : uint8_t {
ORDER_RGB,
ORDER_RBG,
ORDER_GRB,
ORDER_GBR,
ORDER_BGR,
ORDER_BRG,
};
inline const char *rgb_order_to_string(RGBOrder order) {
switch (order) {
case ORDER_RGB:
return "RGB";
case ORDER_RBG:
return "RBG";
case ORDER_GRB:
return "GRB";
case ORDER_GBR:
return "GBR";
case ORDER_BGR:
return "BGR";
case ORDER_BRG:
return "BRG";
default:
return "UNKNOWN";
}
}
using init_fn = void (*)(PIO pio, uint sm, uint offset, uint pin, float freq);
class RP2040PIOLEDStripLightOutput : public light::AddressableLight {
public:
void setup() override;
void write_state(light::LightState *state) override;
float get_setup_priority() const override;
int32_t size() const override { return this->num_leds_; }
light::LightTraits get_traits() override {
auto traits = light::LightTraits();
this->is_rgbw_ ? traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::RGB_WHITE})
: traits.set_supported_color_modes({light::ColorMode::RGB});
return traits;
}
void set_pin(uint8_t pin) { this->pin_ = pin; }
void set_num_leds(uint32_t num_leds) { this->num_leds_ = num_leds; }
void set_is_rgbw(bool is_rgbw) { this->is_rgbw_ = is_rgbw; }
void set_max_refresh_rate(float interval_us) { this->max_refresh_rate_ = interval_us; }
void set_pio(int pio_num) { pio_num ? this->pio_ = pio1 : this->pio_ = pio0; }
void set_program(const pio_program_t *program) { this->program_ = program; }
void set_init_function(init_fn init) { this->init_ = init; }
void set_rgb_order(RGBOrder rgb_order) { this->rgb_order_ = rgb_order; }
void clear_effect_data() override {
for (int i = 0; i < this->size(); i++) {
this->effect_data_[i] = 0;
}
}
void dump_config() override;
protected:
light::ESPColorView get_view_internal(int32_t index) const override;
size_t get_buffer_size_() const { return this->num_leds_ * (3 + this->is_rgbw_); }
uint8_t *buf_{nullptr};
uint8_t *effect_data_{nullptr};
uint8_t pin_;
uint32_t num_leds_;
bool is_rgbw_;
pio_hw_t *pio_;
uint sm_;
RGBOrder rgb_order_{ORDER_RGB};
uint32_t last_refresh_{0};
float max_refresh_rate_;
const pio_program_t *program_;
init_fn init_;
};
} // namespace rp2040_pio_led_strip
} // namespace esphome
#endif // USE_RP2040

View file

@ -0,0 +1,267 @@
from dataclasses import dataclass
from esphome import pins
from esphome.components import light, rp2040
from esphome.const import (
CONF_CHIPSET,
CONF_ID,
CONF_NUM_LEDS,
CONF_OUTPUT_ID,
CONF_PIN,
CONF_RGB_ORDER,
)
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.util import _LOGGER
def get_nops(timing):
"""
Calculate the number of NOP instructions required to wait for a given amount of time.
"""
time_remaining = timing
nops = []
if time_remaining < 32:
nops.append(time_remaining - 1)
return nops
nops.append(31)
time_remaining -= 32
while time_remaining > 0:
if time_remaining >= 32:
nops.append("nop [31]")
time_remaining -= 32
else:
nops.append("nop [" + str(time_remaining) + " - 1 ]")
time_remaining = 0
return nops
def generate_assembly_code(id, rgbw, t0h, t0l, t1h, t1l):
"""
Generate assembly code with the given timing values.
"""
nops_t0h = get_nops(t0h)
nops_t0l = get_nops(t0l)
nops_t1h = get_nops(t1h)
nops_t1l = get_nops(t1l)
t0h = nops_t0h.pop(0)
t0l = nops_t0l.pop(0)
t1h = nops_t1h.pop(0)
t1l = nops_t1l.pop(0)
nops_t0h = "\n".join(" " * 4 + nop for nop in nops_t0h)
nops_t0l = "\n".join(" " * 4 + nop for nop in nops_t0l)
nops_t1h = "\n".join(" " * 4 + nop for nop in nops_t1h)
nops_t1l = "\n".join(" " * 4 + nop for nop in nops_t1l)
const_csdk_code = f"""
% c-sdk {{
#include "hardware/clocks.h"
static inline void rp2040_pio_led_strip_driver_{id}_init(PIO pio, uint sm, uint offset, uint pin, float freq) {{
pio_gpio_init(pio, pin);
pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);
pio_sm_config c = rp2040_pio_led_strip_{id}_program_get_default_config(offset);
sm_config_set_set_pins(&c, pin, 1);
sm_config_set_out_shift(&c, false, true, {32 if rgbw else 24});
sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX);
int cycles_per_bit = 69;
float div = 2.409;
sm_config_set_clkdiv(&c, div);
pio_sm_init(pio, sm, offset, &c);
pio_sm_set_enabled(pio, sm, true);
}}
%}}"""
assembly_template = f""".program rp2040_pio_led_strip_{id}
.wrap_target
awaiting_data:
; Wait for data in FIFO queue
pull block ; this will block until there is data in the FIFO queue and then it will pull it into the shift register
set y, {31 if rgbw else 23} ; set y to the number of bits to write counting 0, (23 if RGB, 31 if RGBW)
mainloop:
; go through each bit in the shift register and jump to the appropriate label
; depending on the value of the bit
out x, 1
jmp !x, writezero
jmp writeone
writezero:
; Write T0H and T0L bits to the output pin
set pins, 1 [{t0h}]
{nops_t0h}
set pins, 0 [{t0l}]
{nops_t0l}
jmp y--, mainloop
jmp awaiting_data
writeone:
; Write T1H and T1L bits to the output pin
set pins, 1 [{t1h}]
{nops_t1h}
set pins, 0 [{t1l}]
{nops_t1l}
jmp y--, mainloop
jmp awaiting_data
.wrap"""
return assembly_template + const_csdk_code
def time_to_cycles(time_us):
cycles_per_us = 57.5
cycles = round(float(time_us) * cycles_per_us)
return cycles
CONF_PIO = "pio"
CODEOWNERS = ["@Papa-DMan"]
DEPENDENCIES = ["rp2040"]
rp2040_pio_led_strip_ns = cg.esphome_ns.namespace("rp2040_pio_led_strip")
RP2040PIOLEDStripLightOutput = rp2040_pio_led_strip_ns.class_(
"RP2040PIOLEDStripLightOutput", light.AddressableLight
)
RGBOrder = rp2040_pio_led_strip_ns.enum("RGBOrder")
Chipsets = rp2040_pio_led_strip_ns.enum("Chipset")
@dataclass
class LEDStripTimings:
T0H: int
T0L: int
T1H: int
T1L: int
RGB_ORDERS = {
"RGB": RGBOrder.ORDER_RGB,
"RBG": RGBOrder.ORDER_RBG,
"GRB": RGBOrder.ORDER_GRB,
"GBR": RGBOrder.ORDER_GBR,
"BGR": RGBOrder.ORDER_BGR,
"BRG": RGBOrder.ORDER_BRG,
}
CHIPSETS = {
"WS2812": LEDStripTimings(20, 43, 41, 31),
"WS2812B": LEDStripTimings(23, 46, 46, 23),
"SK6812": LEDStripTimings(17, 52, 31, 31),
"SM16703": LEDStripTimings(17, 52, 52, 17),
}
CONF_IS_RGBW = "is_rgbw"
CONF_BIT0_HIGH = "bit0_high"
CONF_BIT0_LOW = "bit0_low"
CONF_BIT1_HIGH = "bit1_high"
CONF_BIT1_LOW = "bit1_low"
def _validate_timing(value):
# if doesn't end with us, raise error
if not value.endswith("us"):
raise cv.Invalid("Timing must be in microseconds (us)")
value = float(value[:-2])
nops = get_nops(value)
nops.pop(0)
if len(nops) > 3:
raise cv.Invalid("Timing is too long, please try again.")
return value
CONFIG_SCHEMA = cv.All(
light.ADDRESSABLE_LIGHT_SCHEMA.extend(
{
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(RP2040PIOLEDStripLightOutput),
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int,
cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True),
cv.Required(CONF_PIO): cv.one_of(0, 1, int=True),
cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True),
cv.Optional(CONF_IS_RGBW, default=False): cv.boolean,
cv.Inclusive(
CONF_BIT0_HIGH,
"custom",
): _validate_timing,
cv.Inclusive(
CONF_BIT0_LOW,
"custom",
): _validate_timing,
cv.Inclusive(
CONF_BIT1_HIGH,
"custom",
): _validate_timing,
cv.Inclusive(
CONF_BIT1_LOW,
"custom",
): _validate_timing,
}
),
cv.has_exactly_one_key(CONF_CHIPSET, CONF_BIT0_HIGH),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
id = config[CONF_ID].id
await light.register_light(var, config)
await cg.register_component(var, config)
cg.add(var.set_num_leds(config[CONF_NUM_LEDS]))
cg.add(var.set_pin(config[CONF_PIN]))
cg.add(var.set_rgb_order(config[CONF_RGB_ORDER]))
cg.add(var.set_is_rgbw(config[CONF_IS_RGBW]))
cg.add(var.set_pio(config[CONF_PIO]))
cg.add(var.set_program(cg.RawExpression(f"&rp2040_pio_led_strip_{id}_program")))
cg.add(
var.set_init_function(
cg.RawExpression(f"rp2040_pio_led_strip_driver_{id}_init")
)
)
key = f"led_strip_{id}"
if CONF_CHIPSET in config:
_LOGGER.info("Generating PIO assembly code")
rp2040.add_pio_file(
__name__,
key,
generate_assembly_code(
id,
config[CONF_IS_RGBW],
CHIPSETS[config[CONF_CHIPSET]].T0H,
CHIPSETS[config[CONF_CHIPSET]].T0L,
CHIPSETS[config[CONF_CHIPSET]].T1H,
CHIPSETS[config[CONF_CHIPSET]].T1L,
),
)
else:
_LOGGER.info("Generating custom PIO assembly code")
rp2040.add_pio_file(
__name__,
key,
generate_assembly_code(
id,
config[CONF_IS_RGBW],
time_to_cycles(config[CONF_BIT0_HIGH]),
time_to_cycles(config[CONF_BIT0_LOW]),
time_to_cycles(config[CONF_BIT1_HIGH]),
time_to_cycles(config[CONF_BIT1_LOW]),
),
)

View file

@ -299,6 +299,16 @@ def copy_src_tree():
copy_files() copy_files()
elif CORE.is_rp2040:
from esphome.components.rp2040 import copy_files
(pio) = copy_files()
if pio:
write_file_if_changed(
CORE.relative_src_path("esphome.h"),
ESPHOME_H_FORMAT.format(include_s + '\n#include "pio_includes.h"'),
)
def generate_defines_h(): def generate_defines_h():
define_content_l = [x.as_macro for x in CORE.defines] define_content_l = [x.as_macro for x in CORE.defines]

View file

@ -37,6 +37,26 @@ switch:
output: pin_4 output: pin_4
id: pin_4_switch id: pin_4_switch
light:
- platform: rp2040_pio_led_strip
id: led_strip
pin: GPIO13
num_leds: 60
pio: 0
rgb_order: GRB
chipset: WS2812
- platform: rp2040_pio_led_strip
id: led_strip_custom_timings
pin: GPIO13
num_leds: 60
pio: 1
rgb_order: GRB
bit0_high: .1us
bit0_low: 1.2us
bit1_high: .69us
bit1_low: .4us
sensor: sensor:
- platform: internal_temperature - platform: internal_temperature
name: Internal Temperature name: Internal Temperature