/* TasmotaLEDPusherRMT.cpp - Implementation to push Leds via SPI channel Copyright (C) 2024 Stephan Hadinger This library is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifdef ESP32 #include "TasmotaLEDPusher.h" #include "TasmotaLED.h" #if TASMOTALED_HARDWARE_SPI #include //************************************************************************************************************** // enable AddLog support within a C++ library extern void AddLog(uint32_t loglevel, PGM_P formatP, ...); enum LoggingLevels {LOG_LEVEL_NONE, LOG_LEVEL_ERROR, LOG_LEVEL_INFO, LOG_LEVEL_DEBUG, LOG_LEVEL_DEBUG_MORE}; //************************************************************************************************************** /*******************************************************************************************\ * Implementation for TasmotaLEDPusherSPI * \*******************************************************************************************/ #define LED_STRIP_SPI_DEFAULT_RESOLUTION (25 * 100 * 1000) // 2.5MHz resolution #define LED_STRIP_SPI_DEFAULT_TRANS_QUEUE_SIZE 4 #define SPI_BYTES_PER_COLOR_BYTE 3 #define SPI_BITS_PER_COLOR_BYTE (SPI_BYTES_PER_COLOR_BYTE * 8) static void __led_strip_spi_bit(uint8_t data, uint8_t *buf) { // Each color of 1 bit is represented by 3 bits of SPI, low_level:100 ,high_level:110 // So a color byte occupies 3 bytes of SPI. buf[0] = (data & BIT(5) ? BIT(1) | BIT(0) : BIT(1)) | (data & BIT(6) ? BIT(4) | BIT(3) : BIT(4)) | (data & BIT(7) ? BIT(7) | BIT(6) : BIT(7)); buf[1] = (BIT(0)) | (data & BIT(3) ? BIT(3) | BIT(2) : BIT(3)) | (data & BIT(4) ? BIT(6) | BIT(5) : BIT(6)); buf[2] = (data & BIT(0) ? BIT(2) | BIT(1) : BIT(2)) | (data & BIT(1) ? BIT(5) | BIT(4) : BIT(5)) | (data & BIT(2) ? BIT(7) : 0x00); } esp_err_t led_strip_spi_refresh(led_strip_spi_obj * spi_strip) { spi_strip->tx_conf.length = spi_strip->strip_len * spi_strip->bytes_per_pixel * SPI_BITS_PER_COLOR_BYTE; spi_strip->tx_conf.tx_buffer = spi_strip->pixel_buf; spi_strip->tx_conf.rx_buffer = NULL; spi_device_transmit(spi_strip->spi_device, &spi_strip->tx_conf); return ESP_OK; } void led_strip_transmit_buffer(led_strip_spi_obj * spi_strip, uint8_t * buffer_rgbw) { // Timing for 512 pixels (extreme test) // Copying to buffer: 418 us // sending pixels: 16.2 ms uint8_t * buf = buffer_rgbw; uint8_t * pix_buf = spi_strip->pixel_buf; for (int i = 0; i < spi_strip->strip_len; i++) { // LED_PIXEL_FORMAT_GRB takes 72bits(9bytes) __led_strip_spi_bit(*buf++, pix_buf); pix_buf += SPI_BYTES_PER_COLOR_BYTE; __led_strip_spi_bit(*buf++, pix_buf); pix_buf += SPI_BYTES_PER_COLOR_BYTE; __led_strip_spi_bit(*buf++, pix_buf); pix_buf += SPI_BYTES_PER_COLOR_BYTE; if (spi_strip->bytes_per_pixel > 3) { __led_strip_spi_bit(*buf++, pix_buf); pix_buf += SPI_BYTES_PER_COLOR_BYTE; } } /* Refresh the strip to send data */ led_strip_spi_refresh(spi_strip); } TasmotaLEDPusherSPI::~TasmotaLEDPusherSPI() { if (_spi_strip.spi_device) { spi_bus_remove_device(_spi_strip.spi_device); } if (_spi_strip.spi_host) { spi_bus_free(_spi_strip.spi_host); } if (_pin >= 0) { gpio_matrix_out(_pin, 0x100, false, false); pinMode(_pin, INPUT); _pin = -1; } } TasmotaLEDPusherSPI::TasmotaLEDPusherSPI(int8_t pin) : _pin(pin) { spi_host_device_t spi_host = SPI2_HOST; spi_bus_config_t spi_bus_cfg = { .mosi_io_num = _pin, //Only use MOSI to generate the signal, set -1 when other pins are not used. .miso_io_num = -1, .sclk_io_num = -1, .quadwp_io_num = -1, .quadhd_io_num = -1, .max_transfer_sz = 0, // _pixel_count * _pixel_size * SPI_BYTES_PER_COLOR_BYTE, }; _err = spi_bus_initialize(spi_host, &spi_bus_cfg, _with_dma ? SPI_DMA_CH_AUTO : SPI_DMA_DISABLED); if (_err == ESP_OK) { _spi_strip.spi_host = spi_host; // confirmed working, so keep it's value to free it later _initialized = true; } } bool TasmotaLEDPusherSPI::Begin(uint16_t pixel_count, uint16_t pixel_size, const TasmotaLED_Timing * led_timing) { if (!_initialized) { return false; } TasmotaLEDPusher::Begin(pixel_count, pixel_size, led_timing); _spi_strip.bytes_per_pixel = _pixel_size; _spi_strip.strip_len = _pixel_count; uint32_t mem_caps = MALLOC_CAP_DEFAULT; // spi_clock_source_t clk_src = SPI_CLK_SRC_DEFAULT; spi_device_interface_config_t spi_dev_cfg; int clock_resolution_khz = 0; if (_with_dma) { // TODO // DMA buffer must be placed in internal SRAM mem_caps |= MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA; } _spi_strip.pixel_buf = (uint8_t *)heap_caps_calloc(1, _pixel_count * _pixel_size * SPI_BYTES_PER_COLOR_BYTE, mem_caps); if (_spi_strip.pixel_buf == nullptr) { AddLog(LOG_LEVEL_INFO, PSTR("LED: Error no mem for spi strip")); goto err; } spi_dev_cfg = { .command_bits = 0, .address_bits = 0, .dummy_bits = 0, .mode = 0, //set -1 when CS is not used .clock_source = SPI_CLK_SRC_DEFAULT, // clk_src, .clock_speed_hz = LED_STRIP_SPI_DEFAULT_RESOLUTION, .spics_io_num = -1, .queue_size = LED_STRIP_SPI_DEFAULT_TRANS_QUEUE_SIZE, }; _err = spi_bus_add_device(_spi_strip.spi_host, &spi_dev_cfg, &_spi_strip.spi_device); if (_err != ESP_OK) { // AddLog(LOG_LEVEL_INFO, "LED: Error failed to add spi device"); goto err; } _err = spi_device_get_actual_freq(_spi_strip.spi_device, &clock_resolution_khz); if (_err != ESP_OK) { // AddLog(LOG_LEVEL_INFO, "LED: Error failed to get spi frequency"); goto err; } // TODO: ideally we should decide the SPI_BYTES_PER_COLOR_BYTE by the real clock resolution // But now, let's fixed the resolution, the downside is, we don't support a clock source whose frequency is not multiple of LED_STRIP_SPI_DEFAULT_RESOLUTION if (clock_resolution_khz != LED_STRIP_SPI_DEFAULT_RESOLUTION / 1000) { // AddLog(LOG_LEVEL_INFO, "LED: Error unsupported clock resolution: %dKHz", clock_resolution_khz); goto err; } return true; err: if (_spi_strip.spi_device) { spi_bus_remove_device(_spi_strip.spi_device); } if (_spi_strip.spi_host) { spi_bus_free(_spi_strip.spi_host); } _initialized = false; return false; } bool TasmotaLEDPusherSPI::SetPixelCount(uint16_t pixel_count) { if (!_initialized) { return false; } if (pixel_count > 0 && _pixel_count != pixel_count) { _pixel_count = pixel_count; if (_spi_strip.pixel_buf) { heap_caps_free(_spi_strip.pixel_buf); _spi_strip.pixel_buf = nullptr; } _spi_strip.strip_len = _pixel_count; uint32_t mem_caps = MALLOC_CAP_DEFAULT; if (_with_dma) { // TODO // DMA buffer must be placed in internal SRAM mem_caps |= MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA; } _spi_strip.pixel_buf = (uint8_t *)heap_caps_calloc(1, _pixel_count * _pixel_size * SPI_BYTES_PER_COLOR_BYTE, mem_caps); if (_spi_strip.pixel_buf == nullptr) { AddLog(LOG_LEVEL_INFO, PSTR("LED: Error no mem for spi strip")); if (_spi_strip.spi_device) { spi_bus_remove_device(_spi_strip.spi_device); } if (_spi_strip.spi_host) { spi_bus_free(_spi_strip.spi_host); } _initialized = false; return false; } return true; } return true; } bool TasmotaLEDPusherSPI::CanShow(void) { return _initialized; // TODO } bool TasmotaLEDPusherSPI::Push(uint8_t *buf) { if (!_initialized) { return false; } if (CanShow()) { led_strip_transmit_buffer(&_spi_strip, buf); } return true; } #endif // TASMOTALED_HARDWARE_SPI #endif // ESP32