diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ab5c51..9d908a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1 +1,6 @@ -idf_component_register(SRCS "main.c" INCLUDE_DIRS "include") \ No newline at end of file +if(${IDF_VERSION_MAJOR} STREQUAL 6) + set(requires esp_driver_rmt) +else() + set(requires driver) +endif() +idf_component_register(SRCS "zh_tm1637.c" INCLUDE_DIRS "include" REQUIRES ${requires}) \ No newline at end of file diff --git a/README.md b/README.md index 3df3e4d..f7f7736 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,107 @@ -# esp_component_template +# ESP32 ESP-IDF component for TM1637 led drive (via RMT driver) -esp_component_template \ No newline at end of file +## Tested on + +1. [ESP32 ESP-IDF v6.0.1](https://docs.espressif.com/projects/esp-idf/en/v6.0.1/esp32/index.html) + +## SAST Tools + +[PVS-Studio](https://pvs-studio.com/pvs-studio/?utm_source=website&utm_medium=github&utm_campaign=open_source) - static analyzer for C, C++, C#, and Java code. + +## Features + +1. Support up to 4 led drives on one device (depends on the used ESP32 chip). + +## Note + +1. Just simple driver without any additives. + +## Attention + +1. Original ESP32, ESP32-S2 and ESP32-C2 not supported. +2. For correct operation, please enable the following settings in the menuconfig: + +```text +CONFIG_GPIO_CTRL_FUNC_IN_IRAM +CONFIG_RMT_ENCODER_FUNC_IN_IRAM +CONFIG_RMT_TX_ISR_HANDLER_IN_IRAM +CONFIG_RMT_RX_ISR_HANDLER_IN_IRAM +CONFIG_RMT_RECV_FUNC_IN_IRAM +CONFIG_RMT_TX_ISR_CACHE_SAFE +CONFIG_RMT_RX_ISR_CACHE_SAFE +``` + +## Using + +In an existing project, run the following command to install the components: + +```text +cd ../your_project/components +git clone https://github.com/aZholtikov/zh_tm1637 +``` + +In the application, add the component: + +```c +#include "zh_tm1637.h" +``` + +## Examples + +One 4 segment led drive on device: + +```c +// Numerical symbols without dot (with dot). +// 0x3F (0xBF) - 0 (0.). +// 0x06 (0x86) - 1 (1.). +// 0x5B (0xDB) - 2 (2.). +// 0x4F (0xCF) - 3 (3.). +// 0x66 (0xE6) - 4 (4.). +// 0x6D (0xED) - 5 (5.). +// 0x7D (0xFD) - 6 (6.). +// 0x07 (0x87) - 7 (7.). +// 0x7F (0xFF) - 8 (8.). +// 0x6F (0xEF) - 9 (9.). +// 0x40 - Minus. +// 0x00 - Empty. +// 0x80 - Dot. + +#include "zh_tm1637.h" + +zh_tm1637_handle_t tm1637_handle = {0}; + +void app_main(void) +{ + esp_log_level_set("zh_tm1637", ESP_LOG_ERROR); + zh_tm1637_init_config_t config = ZH_TM1637_INIT_CONFIG_DEFAULT(); + config.clk_gpio = GPIO_NUM_25; + config.dio_gpio = GPIO_NUM_26; + zh_tm1637_init(&config, &tm1637_handle); + zh_tm1637_set_brightness(&tm1637_handle, 7); // Set brightness. + uint8_t clear_symbols[4] = {0x00}; + zh_tm1637_print(&tm1637_handle, 0, clear_symbols, sizeof(clear_symbols)); // Clear LCD. + vTaskDelay(1000 / portTICK_PERIOD_MS); + for (;;) + { + uint8_t symbol = 0x3F; + zh_tm1637_print(&tm1637_handle, 0, &symbol, sizeof(symbol)); // 0 on 0 position. + vTaskDelay(1000 / portTICK_PERIOD_MS); + zh_tm1637_print(&tm1637_handle, 1, &symbol, sizeof(symbol)); // 0 on 1 position. + vTaskDelay(1000 / portTICK_PERIOD_MS); + zh_tm1637_print(&tm1637_handle, 0, clear_symbols, sizeof(clear_symbols)); // Clear LCD. + vTaskDelay(1000 / portTICK_PERIOD_MS); + uint8_t symbols[] = {0x40, 0x06, 0x5B, 0x4F}; + zh_tm1637_print(&tm1637_handle, 0, symbols, sizeof(symbols)); // -123 start from 0 position. + vTaskDelay(1000 / portTICK_PERIOD_MS); + zh_tm1637_print(&tm1637_handle, 0, clear_symbols, sizeof(clear_symbols)); // Clear LCD. + vTaskDelay(1000 / portTICK_PERIOD_MS); + zh_tm1637_print(&tm1637_handle, 1, symbols, sizeof(symbols)); // -123 start from 1 position. + vTaskDelay(1000 / portTICK_PERIOD_MS); + zh_tm1637_print(&tm1637_handle, 0, clear_symbols, sizeof(clear_symbols)); // Clear LCD. + vTaskDelay(1000 / portTICK_PERIOD_MS); + const zh_tm1637_stats_t *stats = zh_tm1637_get_stats(); + printf("Number of RMT driver error: %ld.\n", stats->rmt_driver_error); + vTaskDelay(1000 / portTICK_PERIOD_MS); + } +} +``` diff --git a/TM1637 datasheet.pdf b/TM1637 datasheet.pdf new file mode 100644 index 0000000..eda7559 Binary files /dev/null and b/TM1637 datasheet.pdf differ diff --git a/include/main.h b/include/main.h deleted file mode 100644 index e69de29..0000000 diff --git a/include/zh_tm1637.h b/include/zh_tm1637.h new file mode 100644 index 0000000..d62b4ad --- /dev/null +++ b/include/zh_tm1637.h @@ -0,0 +1,115 @@ +/** + * @file zh_tm1637.h + */ + +#pragma once + +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "driver/rmt_tx.h" + +/** + * @brief TM1637 led drive initial default values. + */ +#define ZH_TM1637_INIT_CONFIG_DEFAULT() \ + { \ + .clk_gpio = GPIO_NUM_MAX, \ + .dio_gpio = GPIO_NUM_MAX} + +#ifdef __cplusplus +extern "C" +{ +#endif + + /** + * @brief Structure for initial initialization of TM1637 led drive. + */ + typedef struct + { + gpio_num_t clk_gpio; /*!< CLK GPIO. */ + gpio_num_t dio_gpio; /*!< DIO GPIO. */ + } zh_tm1637_init_config_t; + + /** + * @brief TM1637 led drive handle. + */ + typedef struct + { + bool is_initialized; /*!< Led drive initialization flag. */ + int8_t brightness; /*!< Led drive brightness. */ + rmt_sync_manager_handle_t synchro; /*!< Unique RMT TX device sync manager handle. */ + rmt_channel_handle_t tx_channel[2]; /*!< Unique RMT TX device handles. */ + rmt_encoder_handle_t copy_encoder; /*!< Unique copy encoder handle. */ + } zh_tm1637_handle_t; + + /** + * @brief Structure for error statistics storage. + */ + typedef struct + { + uint32_t rmt_driver_error; /*!< Number of RMT driver error. */ + } zh_tm1637_stats_t; + + /** + * @brief Initialize TM1637 led drive. + * + * @param[in] config Pointer to TM1637 initialized configuration structure. Can point to a temporary variable. + * @param[out] handle Pointer to unique TM1637 handle. + * + * @attention I2C driver must be initialized first. + * + * @note Before initialize the expander recommend initialize zh_tm1637_init_config_t structure with default values. + * + * @code zh_tm1637_init_config_t config = ZH_TM1637_INIT_CONFIG_DEFAULT() @endcode + * + * @return ESP_OK if success or an error code otherwise. + */ + esp_err_t zh_tm1637_init(const zh_tm1637_init_config_t *config, zh_tm1637_handle_t *handle); + + /** + * @brief Deinitialize TM1637 led drive. + * + * @param[in] handle Pointer to unique TM1637 handle. + * + * @return ESP_OK if success or an error code otherwise. + */ + esp_err_t zh_tm1637_deinit(zh_tm1637_handle_t *handle); + + /** + * @brief Prints an symbols to the LED. + * + * @param[in] handle Pointer to unique TM1637 handle. + * @param[in] position Initial print position (0-5). + * @param[in] symbols Pointer to symbols to be displayed. + * @param[in] size The symbols quantity to be displayed. + * + * @return ESP_OK if success or an error code otherwise. + */ + esp_err_t zh_tm1637_print(zh_tm1637_handle_t *handle, uint8_t position, uint8_t *symbols, uint8_t size); + + /** + * @brief Set brightness of the LED. + * + * @param[in] handle Pointer to unique TM1637 handle. + * @param[in] brightness Brightness (0-7). -1 - LED OFF. + * + * @return ESP_OK if success or an error code otherwise. + */ + esp_err_t zh_tm1637_set_brightness(zh_tm1637_handle_t *handle, int8_t brightness); + + /** + * @brief Get error statistics. + * + * @return Pointer to the statistics structure. + */ + const zh_tm1637_stats_t *zh_tm1637_get_stats(void); + + /** + * @brief Reset error statistics. + */ + void zh_tm1637_reset_stats(void); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/main.c b/main.c deleted file mode 100644 index e69de29..0000000 diff --git a/version.txt b/version.txt index e69de29..afaf360 100644 --- a/version.txt +++ b/version.txt @@ -0,0 +1 @@ +1.0.0 \ No newline at end of file diff --git a/zh_tm1637.c b/zh_tm1637.c new file mode 100644 index 0000000..0847eb1 --- /dev/null +++ b/zh_tm1637.c @@ -0,0 +1,173 @@ +#include "zh_tm1637.h" + +static const char *TAG = "zh_tm1637"; + +#define ZH_LOGI(msg, ...) ESP_LOGI(TAG, msg, ##__VA_ARGS__) +#define ZH_LOGE(msg, err, ...) ESP_LOGE(TAG, "[%s:%d:%s] " msg, __FILE__, __LINE__, esp_err_to_name(err), ##__VA_ARGS__) + +#define ZH_ERROR_CHECK(cond, err, cleanup, msg, ...) \ + if (!(cond)) \ + { \ + ZH_LOGE(msg, err, ##__VA_ARGS__); \ + cleanup; \ + return err; \ + } + +static zh_tm1637_stats_t _stats = {0}; + +static esp_err_t _zh_tm1637_validate_config(const zh_tm1637_init_config_t *config, zh_tm1637_handle_t *handle); +static esp_err_t _zh_tm1637_rmt_init(const zh_tm1637_init_config_t *config, zh_tm1637_handle_t *handle); +static esp_err_t _zh_tm1637_send_data(zh_tm1637_handle_t *handle, uint8_t *symbols, uint8_t size); + +esp_err_t zh_tm1637_init(const zh_tm1637_init_config_t *config, zh_tm1637_handle_t *handle) // -V2008 +{ + ZH_LOGI("Led drive initialization started."); +#if defined CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32C2 + ZH_LOGE("Led drive initialization failed. Unsupported device.", ESP_ERR_NOT_ALLOWED); + return ESP_ERR_NOT_ALLOWED; +#endif + ZH_ERROR_CHECK(config != NULL && handle != NULL, ESP_ERR_INVALID_ARG, NULL, "Led drive initialization failed. Invalid argument."); + ZH_ERROR_CHECK(handle->is_initialized == false, ESP_ERR_INVALID_STATE, NULL, "Led drive initialization failed. Led drive is already initialized."); + ZH_ERROR_CHECK(_zh_tm1637_validate_config(config, handle) == ESP_OK, ESP_FAIL, NULL, "Led drive initialization failed. Initial configuration check failed."); + ZH_ERROR_CHECK(_zh_tm1637_rmt_init(config, handle) == ESP_OK, ESP_FAIL, , "Led drive initialization failed. RMT initialization failed."); + handle->brightness = 7; + handle->is_initialized = true; + ZH_LOGI("Led drive initialization completed successfully."); + return ESP_OK; +} + +esp_err_t zh_tm1637_deinit(zh_tm1637_handle_t *handle) // -V2008 +{ + ZH_LOGI("Led drive deinitialization started."); + ZH_ERROR_CHECK(handle != NULL, ESP_ERR_INVALID_ARG, NULL, "Led drive deinitialization failed. Invalid argument."); + ZH_ERROR_CHECK(handle->is_initialized == true, ESP_FAIL, NULL, "Led drive deinitialization failed. Led drive not initialized."); + ZH_ERROR_CHECK(rmt_disable(handle->tx_channel[0]) == ESP_OK, ESP_FAIL, NULL, "Led drive deinitialization failed. TX channel disable failed."); + ZH_ERROR_CHECK(rmt_disable(handle->tx_channel[1]) == ESP_OK, ESP_FAIL, NULL, "Led drive deinitialization failed. RX channel disable failed."); + ZH_ERROR_CHECK(rmt_del_encoder(handle->copy_encoder) == ESP_OK, ESP_FAIL, NULL, "Led drive deinitialization failed. Delete encoder failed."); + ZH_ERROR_CHECK(rmt_del_channel(handle->tx_channel[0]) == ESP_OK, ESP_FAIL, NULL, "Led drive deinitialization failed. Delete TX channel failed."); + ZH_ERROR_CHECK(rmt_del_channel(handle->tx_channel[1]) == ESP_OK, ESP_FAIL, NULL, "Led drive deinitialization failed. Delete TX channel failed."); + ZH_LOGI("Led drive deinitialization completed successfully."); + return ESP_OK; +} + +esp_err_t zh_tm1637_print(zh_tm1637_handle_t *handle, uint8_t position, uint8_t *symbols, uint8_t size) // -V2008 +{ + ZH_LOGI("Led drive printing started."); + ZH_ERROR_CHECK(handle != NULL && symbols != NULL && position <= 5 && size > 0, ESP_ERR_INVALID_ARG, NULL, "Led drive printing failed. Invalid argument."); + ZH_ERROR_CHECK(handle->is_initialized == true, ESP_FAIL, NULL, "Led drive printing failed. Led drive not initialized."); + uint8_t set_data_command = 0x40; + ZH_ERROR_CHECK(_zh_tm1637_send_data(handle, &set_data_command, 1) == ESP_OK, ESP_FAIL, NULL, "Led drive printing failed. RMT driver error."); + uint8_t print_symbols_command[size + 1]; + print_symbols_command[0] = position | 0xC0; + memcpy(&print_symbols_command[1], symbols, size); + ZH_ERROR_CHECK(_zh_tm1637_send_data(handle, print_symbols_command, size + 1) == ESP_OK, ESP_FAIL, NULL, "Led drive printing failed. RMT driver error."); + uint8_t control_display_command = (handle->brightness == -1) ? 0x80 : handle->brightness | 0x88; + ZH_ERROR_CHECK(_zh_tm1637_send_data(handle, &control_display_command, 1) == ESP_OK, ESP_FAIL, NULL, "Led drive printing failed. RMT driver error."); + ZH_LOGI("Led drive printing completed successfully."); + return ESP_OK; +} + +esp_err_t zh_tm1637_set_brightness(zh_tm1637_handle_t *handle, int8_t brightness) +{ + ZH_LOGI("Led drive setup brightness started."); + ZH_ERROR_CHECK(handle != NULL && brightness <= 7 && brightness >= -1, ESP_ERR_INVALID_ARG, NULL, "Led drive setup brightness failed. Invalid argument."); + ZH_ERROR_CHECK(handle->is_initialized == true, ESP_FAIL, NULL, "Led drive setup brightness failed. Led drive not initialized."); + handle->brightness = brightness; + ZH_LOGI("Led drive setup brightness completed successfully."); + return ESP_OK; +} + +const zh_tm1637_stats_t *zh_tm1637_get_stats(void) +{ + return &_stats; +} + +void zh_tm1637_reset_stats(void) +{ + ZH_LOGI("Error statistic reset started."); + _stats.rmt_driver_error = 0; + ZH_LOGI("Error statistic reset successfully."); +} + +static esp_err_t _zh_tm1637_validate_config(const zh_tm1637_init_config_t *config, zh_tm1637_handle_t *handle) +{ + ZH_ERROR_CHECK(config->clk_gpio < GPIO_NUM_MAX && config->dio_gpio < GPIO_NUM_MAX, ESP_ERR_INVALID_ARG, NULL, "Invalid GPIO number.") + ZH_ERROR_CHECK(config->clk_gpio != config->dio_gpio, ESP_ERR_INVALID_ARG, NULL, "CLK GPIO and DIO GPIO is same.") + return ESP_OK; +} + +static esp_err_t _zh_tm1637_rmt_init(const zh_tm1637_init_config_t *config, zh_tm1637_handle_t *handle) // -V2008 +{ + rmt_tx_channel_config_t tx_chan_config = { + .clk_src = RMT_CLK_SRC_DEFAULT, + .mem_block_symbols = 48, + .resolution_hz = 1000000, + .trans_queue_depth = 4, + .flags.init_level = 1, + }; + tx_chan_config.gpio_num = config->clk_gpio; + ZH_ERROR_CHECK(rmt_new_tx_channel(&tx_chan_config, &handle->tx_channel[0]) == ESP_OK, ESP_FAIL, NULL, "TX channel creation failed."); + tx_chan_config.gpio_num = config->dio_gpio; + ZH_ERROR_CHECK(rmt_new_tx_channel(&tx_chan_config, &handle->tx_channel[1]) == ESP_OK, ESP_FAIL, NULL, "TX channel creation failed."); + rmt_copy_encoder_config_t copy_encoder_config = {}; + ZH_ERROR_CHECK(rmt_new_copy_encoder(©_encoder_config, &handle->copy_encoder) == ESP_OK, ESP_FAIL, + ZH_ERROR_CHECK(rmt_del_channel(handle->tx_channel[0]) == ESP_OK, ESP_FAIL, NULL, "Delete channel failed."); + ZH_ERROR_CHECK(rmt_del_channel(handle->tx_channel[1]) == ESP_OK, ESP_FAIL, NULL, "Delete channel failed."), "Copy encoder creation failed."); + ZH_ERROR_CHECK(rmt_enable(handle->tx_channel[0]) == ESP_OK, ESP_FAIL, + ZH_ERROR_CHECK(rmt_del_encoder(handle->copy_encoder) == ESP_OK, ESP_FAIL, NULL, "Delete encoder failed."); + ZH_ERROR_CHECK(rmt_del_channel(handle->tx_channel[0]) == ESP_OK, ESP_FAIL, NULL, "Delete channel failed."); + ZH_ERROR_CHECK(rmt_del_channel(handle->tx_channel[1]) == ESP_OK, ESP_FAIL, NULL, "Delete channel failed."), "Enable TX channel failed."); + ZH_ERROR_CHECK(rmt_enable(handle->tx_channel[1]) == ESP_OK, ESP_FAIL, + ZH_ERROR_CHECK(rmt_del_encoder(handle->copy_encoder) == ESP_OK, ESP_FAIL, NULL, "Delete encoder failed."); + ZH_ERROR_CHECK(rmt_del_channel(handle->tx_channel[0]) == ESP_OK, ESP_FAIL, NULL, "Delete channel failed."); + ZH_ERROR_CHECK(rmt_del_channel(handle->tx_channel[1]) == ESP_OK, ESP_FAIL, NULL, "Delete channel failed."), "Enable TX channel failed."); + rmt_sync_manager_config_t synchro_config = { + .tx_channel_array = handle->tx_channel, + .array_size = sizeof(handle->tx_channel) / sizeof(handle->tx_channel[0]), + }; + ZH_ERROR_CHECK(rmt_new_sync_manager(&synchro_config, &handle->synchro) == ESP_OK, ESP_FAIL, + ZH_ERROR_CHECK(rmt_disable(handle->tx_channel[0]) == ESP_OK, ESP_FAIL, NULL, "TX channel disable failed."); + ZH_ERROR_CHECK(rmt_disable(handle->tx_channel[1]) == ESP_OK, ESP_FAIL, NULL, "TX channel disable failed."); + ZH_ERROR_CHECK(rmt_del_encoder(handle->copy_encoder) == ESP_OK, ESP_FAIL, NULL, "Delete encoder failed."); + ZH_ERROR_CHECK(rmt_del_channel(handle->tx_channel[0]) == ESP_OK, ESP_FAIL, NULL, "Delete TX channel failed."); + ZH_ERROR_CHECK(rmt_del_channel(handle->tx_channel[1]) == ESP_OK, ESP_FAIL, NULL, "Delete TX channel failed."), "Sync manager creation failed."); + rmt_transmit_config_t tx_transmit_config = {.loop_count = 0, .flags.eot_level = 1}; + rmt_symbol_word_t init_symbols = {.duration0 = 10, .level0 = 1, .duration1 = 10, .level1 = 1}; + rmt_sync_reset(handle->synchro); + ZH_ERROR_CHECK(rmt_transmit(handle->tx_channel[0], handle->copy_encoder, &init_symbols, sizeof(init_symbols), &tx_transmit_config) == ESP_OK, ESP_FAIL, ++_stats.rmt_driver_error, "RMT driver error."); + ZH_ERROR_CHECK(rmt_transmit(handle->tx_channel[1], handle->copy_encoder, &init_symbols, sizeof(init_symbols), &tx_transmit_config) == ESP_OK, ESP_FAIL, ++_stats.rmt_driver_error, "RMT driver error."); + ZH_ERROR_CHECK(rmt_tx_wait_all_done(handle->tx_channel[0], 10) == ESP_OK, ESP_FAIL, ++_stats.rmt_driver_error, "RMT driver error."); + ZH_ERROR_CHECK(rmt_tx_wait_all_done(handle->tx_channel[1], 10) == ESP_OK, ESP_FAIL, ++_stats.rmt_driver_error, "RMT driver error."); + return ESP_OK; +} + +static esp_err_t _zh_tm1637_send_data(zh_tm1637_handle_t *handle, uint8_t *symbols, uint8_t size) +{ + uint8_t clk_symbols_counter = 0; + uint8_t dio_symbols_counter = 0; + uint8_t temp_symbols[size]; + memcpy(&temp_symbols, symbols, size); + rmt_symbol_word_t clk_symbols[2 + (size * 9)]; + rmt_symbol_word_t dio_symbols[2 + (size * 9)]; + clk_symbols[clk_symbols_counter++] = (rmt_symbol_word_t){.duration0 = 2, .level0 = 1, .duration1 = 2, .level1 = 1}; // Start. + dio_symbols[dio_symbols_counter++] = (rmt_symbol_word_t){.duration0 = 2, .level0 = 0, .duration1 = 3, .level1 = 0}; // Start. + for (uint8_t i = 0; i < size; ++i) + { + for (uint8_t j = 0; j < 8; ++j) + { + clk_symbols[clk_symbols_counter++] = (rmt_symbol_word_t){.duration0 = 2, .level0 = 0, .duration1 = 2, .level1 = 1}; // Data. + dio_symbols[dio_symbols_counter++] = (rmt_symbol_word_t){.duration0 = 2, .level0 = (temp_symbols[i] >> j) & 0x01, .duration1 = 2, .level1 = (temp_symbols[i] >> j) & 0x01}; // Data. + } + clk_symbols[clk_symbols_counter++] = (rmt_symbol_word_t){.duration0 = 2, .level0 = 0, .duration1 = 2, .level1 = 1}; // Ask. + dio_symbols[dio_symbols_counter++] = (rmt_symbol_word_t){.duration0 = 2, .level0 = 0, .duration1 = 2, .level1 = 0}; // Ask. + } + clk_symbols[clk_symbols_counter] = (rmt_symbol_word_t){.duration0 = 2, .level0 = 0, .duration1 = 2, .level1 = 0}; // Stop. + dio_symbols[dio_symbols_counter] = (rmt_symbol_word_t){.duration0 = 2, .level0 = 1, .duration1 = 2, .level1 = 0}; // Stop. + rmt_transmit_config_t tx_transmit_config = {.loop_count = 0, .flags.eot_level = 1}; + rmt_sync_reset(handle->synchro); + ZH_ERROR_CHECK(rmt_transmit(handle->tx_channel[0], handle->copy_encoder, &clk_symbols, sizeof(clk_symbols), &tx_transmit_config) == ESP_OK, ESP_FAIL, ++_stats.rmt_driver_error, "RMT driver error."); + ZH_ERROR_CHECK(rmt_transmit(handle->tx_channel[1], handle->copy_encoder, &dio_symbols, sizeof(dio_symbols), &tx_transmit_config) == ESP_OK, ESP_FAIL, ++_stats.rmt_driver_error, "RMT driver error."); + ZH_ERROR_CHECK(rmt_tx_wait_all_done(handle->tx_channel[0], 10) == ESP_OK, ESP_FAIL, ++_stats.rmt_driver_error, "RMT driver error."); + ZH_ERROR_CHECK(rmt_tx_wait_all_done(handle->tx_channel[1], 10) == ESP_OK, ESP_FAIL, ++_stats.rmt_driver_error, "RMT driver error."); + return ESP_OK; +} \ No newline at end of file