Программирование ARM ESP-IDF FAQ Tue, September 16 2025  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.


ESP-IDF FAQ Печать
Добавил(а) microsin   

Здесь описаны решения проблем, которые связаны с программированием ESP32 в среде разработки ESP-IDF.

[Общие замечания]

Хорошая новость - среда разработки ESP-IDF [4] бесплатная, и к тому же кроссплатформенная, т. е. будет работать одинаково (почти) и на Windows, и на Linux, и на MacOS. Плохая новость - к сожалению, среда основана на Eclipse, и процесс компиляции плотно завязан на скрипты компиляции Python, поэтому приготовьтесь к тому, что будете много времени тратить на компиляцию проекта, особенно после изменения его опций компиляции. На Linux компиляция работает быстрее, чем на Windows. Поэтому компьютер обязательно должен быть современный, памяти не меньше 16 гигабайт, и обязательно рабочий диск должен быть SSD.

Известная проблема, связанная с созданием Launch Target, когда при создании нового проекта или его импорте не была подключена к COM-порту отладочная плата. В этом случае после процесса компиляции проекта в окне сообщений сборки (CDT Build Console) может появиться сообщение:

No esp launch target found. Please create/select the correct 'Launch Target'

Проблема была в том, процесс сборки требует указания наличия цели запуска (Launch Target). Цель запуска это обычно отладочная плата, подключенная через USB, и она видна на компьютере как виртуальный COM-порт.

Если Launch Target для проекта не создана или не выбрана, то сборка не завершится. Launch Target создается в среде ESP-IDF по стандартной процедуре, описанной в [1]:

1. Подключите плату ESP32 к порту USB. На компьютере должен появиться последовательный порт (COM-порт на Windows).
2. В среде ESP-IDF разверните третий выпадающий список, выберите New Launch Target.
3. В поле Name укажите любое имя. В поле IDF Target выберите esp32. В поле Serial Port выберите порт подключения платы. Кликните на кнопку Finish для сохранения конфигурации.

Перед запуском сборки выберите эту конфигурацию.

[Глюк ESP-IDF, связанный с созданием Launch Target]

Если проект был создан без подключенной платы, то будет наблюдаться глюк, когда нельзя создать цель запуска. Т. е. если пройти все описанные выше шаги New Launch Target, то цель запуска не будет создана, и компиляция будет неизменно заканчиваться ошибкой "No esp launch target found. Please create/select the correct 'Launch Target'".

Решить эту проблему можно следующим образом: создайте/импортируйте проект заново. После этого будет нормально работать New Launch Target. Мало того, в выпадающем списке магически появятся все цели, которые были до этого созданы неудачными попытками.

Даже на современном компьютере (64-разрядная операционная система Windows 10, процессор x64 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz, диск SSD, память 16 гигабайт) компиляция самого простого проекта blink может занимать 5 минут и больше. Что можно сделать с этим?

1. Отключите на время компиляции антивирус и Windows Defender.

2. Убедитесь, что среда ESP-IDF и компилируемый проект находится на диске SSD.

3. Посмотрите, чем загружена система в момент компиляции (с помощью Диспетчера задач) и закройте все ненужные сервисы и программы, которые больше всего отнимают процессорное время.

4. Настройте утилиту make на использование нескольких ядер процессора (Make -jN [2]).

5. Попробуйте перенесите мало изменяемый код в двоичные библиотеки.

6. Если совсем ничего не помогает, то перейдите с Windows на Linux.

1. Обязательно (!) подключить плату ESP32-C3-DevKitC-02v1.1 через USB к компьютеру. На компьютере должен появиться COM-порт.

2. Сконфигурировать New Launch Target (третий выпадающий список на панели инструментов) для появившегося COM-порта и используемого процессора ESP32-C3.

3. File -> New -> Espressif IDF Project -> blink.

4. idf.py set-target ESP32-C3, идем пить кофе.

5. F5, Двойной клик на sdkconfig, Example Configuration -> RMT - Addressable LED, RMT Channel 0, Blink GPIO number 8.

6. Serial flasher config -> Flash size 4 MB.

7. Component config -> FreeRTOS -> Run FreeRTOS only on first core.

8. Ctrl+S, меню Project -> Build Project, снова идем пить кофе.

9. idf.py flash

После этого на платке должен начать мигать RGB-светодиод WS2812.

Модули исходного кода добавляются в проект путем редактирования файлов CMakeLists.txt. Обычно такой файл есть в корневом каталоге проекта, а также в подкаталогах main и components проекта.

Как добавить файл с исходным кодом в проект, процесс по шагам:

1. Скопируйте *.c и его заголовочный файл *.h в подкаталог main. Предположим, это был модуль OneButton.c и заголовок OneButton.h.

2. Откройте в подкаталоге main файл CMakeLists.txt, добавьте модуль OneButton.c в проект следующим образом:

idf_component_register(SRCS "ledc_basic_example_main.c" "OneButton.c"
                    INCLUDE_DIRS ".")

Список добавляемых исходных файлов нужно вставлять через пробел, каждое имя файла нужно заключать в двойные кавычки. Можно также добавлять каждый файл на отдельной строке:

idf_component_register(SRCS "ledc_basic_example_main.c"
                            "OneButton.c"
                    INCLUDE_DIRS ".")

3. Перекомпилируйте проект командой idf.py build. Система сборки обнаружит изменение в CMakeLists.txt, и в процесс компиляции будет добавлен новый модуль.

Подробнее про систему сборки и файлы CMakeLists см. [6]. Пример добавления дополнительного пути поиска подключаемых файлов в секцию INCLUDE_DIRS см. "Q035. Как в проекте ESP-IDF добавить путь подключаемых файлов?".

Предположим, у нас есть плата ESP32-С3-WROOM-02, у которой трехцветный RGB-светодиод WS2812 подключен к ножке порта GPIO18. Как добавить поддержку драйвера RGB и управлять этим светодиодом, процесс по шагам:

1. Создайте в подкаталоге main проекта файл Kconfig.projbuild со следующим содержимым (в этом файле текст должен быть только английским):

menu "Example control RGB-LED WS2812"
 
    choice BLINK_LED
        prompt "Blink LED type"
        default BLINK_LED_GPIO if IDF_TARGET_ESP32
        default BLINK_LED_RMT
        help
            Defines the default peripheral for blink example
 
        config BLINK_LED_GPIO
            bool "GPIO"
        config BLINK_LED_RMT
            bool "RMT - Addressable LED"
    endchoice
 
    config BLINK_LED_RMT_CHANNEL
        depends on BLINK_LED_RMT
        int "RMT Channel"
        range 0 7
        default 0
        help
            Set the RMT peripheral channel.
            ESP32 RMT channel from 0 to 7
            ESP32-S2 RMT channel from 0 to 3
            ESP32-S3 RMT channel from 0 to 3
            ESP32-C3 RMT channel from 0 to 1
 
    config BLINK_GPIO
        int "Blink GPIO number"
        range 0 48
        default 8 if IDF_TARGET_ESP32C3 || IDF_TARGET_ESP32H2
        default 18 if IDF_TARGET_ESP32S2
        default 48 if IDF_TARGET_ESP32S3
        default 5
        help
            GPIO number (IOxx) to blink on and off or the RMT signal for the addressable LED.
            Some GPIOs are used for other purposes (flash connections, etc.) and cannot be used to blink.
 
    config BLINK_PERIOD
        int "Blink period in ms"
        range 10 3600000
        default 1000
        help
            Define the blinking period in milliseconds.
endmenu

2. Запустите команду idf.py menuconfig, и в разделе настроек "Example control RGB-LED WS2812" поменяйте следующие опции:

Blink LED type -> RMT - Addressable LED
Blink GPIO number 18

Сохраните настройки (S) и выйдите из menuconfig (Esc).

3. Откройте файл CMakeLists.txt, который находится в корневом каталоге проекта. Перед директивой include добавьте директиву set, указывающую на дополнительный компонент led_strip:

cmake_minimum_required(VERSION 3.5)
 
set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/led_strip)
 
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
get_filename_component(ProjectId ${CMAKE_CURRENT_LIST_DIR} NAME)
string(REPLACE " " "_" ProjectId ${ProjectId})
project(${ProjectId})

4. Добавьте в основной модуль проекта (где находится функция app_main) следующий код:

#include "led_strip.h"
 
static uint8_t s_led_state = 0;
static led_strip_t *pStrip_a;
 
static void configure_led(void)
{
    ESP_LOGI("configure_led", "Example configured to blink addressable LED!");
    /* LED strip initialization with the GPIO and pixels number*/
    pStrip_a = led_strip_init(CONFIG_BLINK_LED_RMT_CHANNEL, CONFIG_BLINK_GPIO, 1);
    /* Set all LED off to clear all pixels */
    pStrip_a->clear(pStrip_a, 50);
}
 
static void blink_led(void)
{
    /* If the addressable LED is enabled */
    if (s_led_state) {
        /* Set the LED pixel using RGB from 0 (0%) to 255 (100%) for each color */
        pStrip_a->set_pixel(pStrip_a, 0, 16, 16, 16);
        /* Refresh the strip to send data */
        pStrip_a->refresh(pStrip_a, 100);
    } else {
        /* Set all LED off to clear all pixels */
        pStrip_a->clear(pStrip_a, 50);
    }
}
 
void app_main(void)
{
    configure_led();
    while(1)
    {
        /* Toggle the LED state */
        blink_led();
        s_led_state = !s_led_state;
        vTaskDelay(CONFIG_BLINK_PERIOD / portTICK_PERIOD_MS);
    }
}

5. Скомпилируйте проект командой idf.py build.

Наиболее часто встречающиеся причины:

• Проблемы с питанием - уровень меньше 3.3V, просадки.
• Проблемы с сигналами подключения SPI flash.
• Проблемы с сигналами приема и передачи UART, через который происходит взаимодействие с загрузчиком.

Подробно решение таких проблем описано в статье [7].

После компиляции проектов примеров, поставляемых вместе со средой программирования ESP-IDF, в корневой папке проекта создается каталог esp_idf_components с кучей подкаталогов, причем они все пустые.

ESP IDF components empty folder

Структура каталогов этой папки esp_idf_components полностью совпадает со структурой каталогов папки c:\Espressif\frameworks\esp-idf-v4.4\components\, но эти каталоги уже не пустые, там находится исходный код компонентов, которые могут использоваться в проекте.

Возникает законный вопрос - зачем нужен каталог esp_idf_components с пустыми папками?

Оказывается, эти пустые папки каталоги нужны для того, чтобы можно было по желанию менять поведение некоторых компонентов, путем правки его исходного кода, и не затрагивая при этом исходный код оригинала. Для этого модуль исходного кода, который нужно исправить, копируется в соответствующий пустой подкаталог папки esp_idf_components, и в файл CMakeLists.txt проекта добавляется ссылка на копируемый файл. После этого система сборки будет брать для компиляции этот модуль вместо исходного, который находился в папке c:\Espressif\frameworks\esp-idf-v4.4\components\.

Покажу на примере проекта Console Example. Предположим, что нужно поменять интерфейс команд консоли. Процесс по шагам:

1. Скопируйте модуль commands.c из папки c:\Espressif\frameworks\esp-idf-v4.4\components\console\ в папку c:\Espressif\frameworks\esp-idf-v4.4\workspace\console_basic\esp_idf_components\console\.

2. Откройте на редактирование файл main\CMakeLists.txt проекта, добавьте в него ссылку на скопированный модуль:

idf_component_register(SRCS "cmd_wifi.c"
                            "console_example_main.c"
                            "../esp_idf_components/console/commands.c"
                    INCLUDE_DIRS ".")

3. Внесите изменения в скопированный модуль commands.c и перекомпилируйте проект.

Windows -> Preferences -> General -> Workspace -> Text file encoding -> в выпадающем списке выберите кодировку.

ESP IDF text editor UTF8

Для вывода русского текста в консоли монитора (idf.py monitor) выберите кодировку UTF-8.

1. Утилита командной строки bin2c [8] (входит в пакет hxtools).

2. Функция линковщика ld, превращающая любой файл в объект [9]:

ld -r -b binary -o binary.o foo.bar  # затем линковка binary.o

3. На некоторых SDK для этого есть специальные функции, например макрос EMBED_FILES системы сборки ESP-IDF (см. врезку "Пример CMakeLists компонента" [6]).

Способ 1:

~/esp/esp-idf$ git status
Отсоединённый указатель HEAD указывает на v4.4.1

Способ 2 (должно быть установлено окружение ESP-IDF, см. Q026):

$ idf.py --version
ESP-IDF v5.4.1-dirty

idf.py menuconfig → Component config → Log output → Default log verbosity

( ) No output
( ) Error
( ) Warning
(X) Info
( ) Debug
( ) Verbose

Для этого можно использовать команду idf.py size, например:

каталогпроекта$ idf.py size
...
Total sizes:
Used stat D/IRAM:  155732 bytes ( 171948 remain, 47.5% used)
      .data size:   23580 bytes
      .bss  size:   49864 bytes
      .text size:   82288 bytes
Used Flash size : 1458700 bytes
      .text     : 1034218 bytes
      .rodata   :  424226 bytes
Total image size: 1564568 bytes (.bin may be padded larger)

В устройстве понадобилось использовать вывод GPIO2 и как вход для ADC1, и как вход для вывода устройства из режима низкого энергопотребления deep sleep. Конечно, не одновременно, а в разные моменты времени жизни приложения.

Однако при этом столкнулся с проблемой: если при пробуждении настроить GPIO2 как вход АЦП, а потом при уходе в сон попытаться перенастроить этот вывод на пробуждение из deep sleep, то функция пробуждения перестает работать. Если точнее, то устройство пробуждается сразу после ухода в сон, как будто на входе появился пробуждающий уровень лог. 0, хотя на самом деле ничего такого нет.

Код, который настраивает вход GPIO2 как вход АЦП:

ESP_ERROR_CHECK(adc1_config_width(ADC_WIDTH_BIT_12));
// ADC1_CHANNEL_2 соответствует выводу GPIO_NUM_2, т. е. GPIO2:
ESP_ERROR_CHECK(adc1_config_channel_atten(ADC1_CHANNEL_2, ADC_ATTEN_DB_11));

Код, который делает попытку настроить этот же вывод на пробуждение:

uint64_t wakeup_mask = BIT(GPIO_NUM_2);
ESP_ERROR_CHECK(esp_deep_sleep_enable_gpio_wakeup(wakeup_mask, ESP_GPIO_WAKEUP_GPIO_LOW));
ESP_LOGI(__FUNCTION__, "Entering deep sleep...");
esp_sleep_pd_config(ESP_PD_DOMAIN_VDDSDIO, ESP_PD_OPTION_OFF);
esp_deep_sleep_start();

Решение проблемы - перед настройкой глубокого пробуждения снова переключить этот вывод на работу как GPIO с помощью функции gpio_config:

gpio_config_t pincfg =
{  .pin_bit_mask = BIT(GPIO_NUM_2),
   .mode         = GPIO_MODE_INPUT,
   .pull_up_en   = GPIO_PULLUP_ENABLE };
ESP_ERROR_CHECK(gpio_config(&pincfg));
// Далее настройка пробуждения вызовами esp_deep_sleep_enable_gpio_wakeup
// и esp_sleep_pd_config, и затем уход в сон через esp_deep_sleep_start.

Интересно, что вызов gpio_reset_pin не восстанавливает функционал вывода из deep sleep, хотя по описанию вызов gpio_reset_pin должен возвратить вывод GPIO в состояние по умолчанию (выбор функции порта ввод/вывода общего назначения, разрешение pullup и запрет работы на вход и выход).

Неожиданно столкнулся в VS Code с ошибкой:

Squiggles are disabled for this translation unit (/home/username/myproject/src/main.c).

Решение проблемы:

1. Добавьте явное определение "compilerPath" в конфигурацию VS Code. Для этого в файле c_cpp_properties.json, который находится в папке .vscode корневого каталога проекта, путь до используемого компилятора в свойстве compilerPath, например "compilerPath": "/usr/bin/gcc":

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${default}",
                "./include"
            ],
            "defines": [],
            "compilerPath": "/usr/bin/gcc",
            "cStandard": "c17",
            "cppStandard": "gnu++14",
            "intelliSenseMode": "linux-gcc-x64"
        }
    ],
    "version": 4
}

2. В файле settings.json (который находится в той же папке .vscode) поменяйте настройку C_Cpp.errorSquiggles на "Enabled", например:

{
    "idf.adapterTargetName": "esp32c3",
    "C_Cpp.errorSquiggles": "enabled"
}

В некоторых случаях бывает нужно подавить предупреждение компилятора, что переменная была определена и ей присвоено значение, но она после этого не используется. Например, это может случиться при использовании макроса ESP_GOTO_ON_ERROR, когда переменная ret, куда макрос записывает возвращаемое значение, не используется. Пример:

TEvent evt;
for(uint8_t i=0; i < 3; i++)
{
   esp_err_t ret = ESP_OK;  // компилятор выдаст предупреждение на
                            // не используемую переменную ret...
   ESP_GOTO_ON_ERROR(pdTRUE == xQueueReceive(queue, &evt, 100) ? ESP_OK : ESP_FAIL,
                     err_measure, __FUNCTION__, "Failed to read voltage");
   ESP_LOGI(__FUNCTION__, "%.3fV", voltage);err_measure:
   vTaskDelay(100/portTICK_PERIOD_MS);
}

Устранить выдачу предупреждения можно следующим способом:

   esp_err_t ret = ESP_OK;
   (void)ret;  // подавляет warning на не используемую переменную ret

#include "esp_idf_version.h"
 
#if (ESP_IDF_VERSION == ESP_IDF_VERSION_VAL(4, 4, 1))
# include "driver/gpio.h"
# include "driver/uart.h"
#elif (ESP_IDF_VERSION == ESP_IDF_VERSION_VAL(5, 0, 1))
# include "rom/gpio.h"
# include "rom/uart.h"
#endif

# register ESP IDF component
if("v4.4.1-dirty" STREQUAL ${IDF_VER})
idf_component_register( REQUIRES        "driver"
                                        "esp_adc_cal")
elseif("v5.0.1" STREQUAL ${IDF_VER})
idf_component_register( REQUIRES        "driver"
                                        "esp_adc")
endif()

error: format '%u' expects argument of type 'unsigned int', but argument 6
 has type 'uint32_t' {aka 'long unsigned int'} [-Werror=format=]

Эту ошибку выдает код:

uint32_t phaseincrement = (uint32_t)((uint64_t)4294967296*freq/samplerate);
ESP_LOGI("", "phaseincrement %u", phaseincrement);

Как исправить:

ESP_LOGI("", "phaseincrement %" PRIu32, phaseincrement);

Ошибка:

error: expected ')' before 'PRIu32'
   ESP_LOGI("", "phaseincrement %" PRIu32, phaseincrement);
                                   ^~~~~~

Проблема в том, что не подключен заголовок inttypes.h [10].

error: missing binary operator before token "(" #if (ESP_IDF_VERSION == ESP_IDF_VERSION_VAL(4, 4, 1))

Проблема в том, что не подключен заголовочный файл esp_idf_version.h.

Возможно дважды применен атрибут IRAM_ATTR к одной и той же функции - один раз в объявлении заголовка (или в static-объявлении функции), и второй раз в реализации. Например:

// Здесь не надо указывать атрибут IRAM_ATTR:
void IRAM_ATTR ledc_timer_overflow_isr(void *arg);
 
// А вот здесь как раз IRAM_ATTR нужен:
void IRAM_ATTR ledc_timer_overflow_isr(void *arg)
{
  // Очистка бит прерываний:
  uint32_t intstatus = READ_PERI_REG(LEDC_INT_ST_REG);
  WRITE_PERI_REG(LEDC_INT_CLR_REG, intstatus);
 
  // Здесь код обработчика прерывания:
  ...
}

__FILE__: макрос, который компилятор переводит в текстовое имя модуля исходного кода, где встретился этот макрос.

__FUNCTION__: макрос, который компилятор переводит в текстовое имя функции, где встретился этот макрос.

__LINE__: макрос, который компилятор переводит в текстовое имя функции, где встретился этот макрос.

Благодаря этим макросам появляется удобная возможность в логе отследить ход выполнения программы:

ESP_LOGW(__FUNCTION__, "%d", __LINE__);

В результате в логе будут появляться вот такие строки:

I (662) phy_init: phy_version 909,156dee4,Apr  7 2022,20:27:09
I (842) wifi:mode : sta (a0:76:4e:14:5a:3c)
I (842) wifi:enable tsf
W (842) off_task: 475
W (842) off_task: 483
 
assert failed: 0x4038aaea
Core  0 register dump:
MEPC    : 0x4038085e  RA      : 0x4038a710  SP      : 0x3fcae140  GP      : 0x3fc94a00
TP      : 0x3fc4563c  T0      : 0x37363534  T1      : 0x7271706f  T2      : 0x33323130
S0/FP   : 0x00000000  S1      : 0x00000000  A0      : 0x3fcae154  A1      : 0x00000061
A2      : 0x00000003  A3      : 0x3fcae168  A4      : 0x00000001  A5      : 0x3fca3000
A6      : 0x7a797877  A7      : 0x76757473  S2      : 0x00000000  S3      : 0x00000000
S4      : 0x00000000  S5      : 0x00000000  S6      : 0x00000000  S7      : 0x00000000
S8      : 0x00000000  S9      : 0x00000000  S10     : 0x00000000  S11     : 0x00000000
T3      : 0x6e6d6c6b  T4      : 0x6a696867  T5      : 0x66656463  T6      : 0x62613938
MSTATUS : 0x00001801  MTVEC   : 0x40380001  MCAUSE  : 0x00000007  MTVAL   : 0x00000000
MHARTID : 0x00000000
..

В результате становится намного проще разобраться в реальной последовательности выполнения кода и найти место ошибки.

Макрос __FILE__ может пригодиться в больших проектах, где содержится множество отдельных файлов исходного кода.

Вызов vTaskDelete(NULL) используется, чтобы задача могла удалить саму себя. Особенность такого вызова в том, что он должен быть обязательно размещен в конце функции задачи. Только при выполнении этого условия состояние задачи перейдет в eDeleted.

Если же разместить vTaskDelete(NULL) в любом другом месте тела функции задачи, то её состояние навсегда останется eRunning.

1. Ctrl+Shift+t, cdдиректория_проекта.

2. $ . ~/esp/v5.4.1/esp-idf/export.sh

3. Далее используйте idf.py команда (clean, build, flash, monitor, ...).

Функция xTaskGetTickCount возвратит глобальное время в тиках системы начиная от момента её запуска. Частота тиков в Гц определяется настройками menuconfig, см. Component config → FreeRTOS → Kernel → configTICK_RATE_HZ (частота тиков, по умолчанию 100). Соответственно длительность одного тика в миллисекундах равна константе portTICK_PERIOD_MS.

Если необходимо получить метку времени в формате Unix, то можно воспользоваться примерно такой функцией:

int64_t xx_time_get_time() {
   struct timeval tv;
   gettimeofday(&tv, NULL);
   return (tv.tv_sec * 1000LL + (tv.tv_usec / 1000LL));
}

// Функция, запускающая поток с заданным количеством тестов. Количество тестов
// передается в поток (функцию задачи) через параметр num_tests:
void run_test (int num_tests) { xTaskCreate(test_task, "test_task", 3072, &num_tests, PRIORITY_USER_CONSOLE, NULL); }

// Функция задачи потока. Количество запускаемых тестов извлекается
// из параметра pvParameters:
static void test_task(void *pvParameters) { vTaskSuspend(xConsoleTask); int num_tests = *(int*)pvParameters; printf("Run %i tests\n", num_tests); memset(tstinfo, 0, MAX_TESTS * sizeof(TTestInfo)); int curr_test = 0; while(curr_test < num_tests) { printf("\r%i", curr_test+1); if (curr_test + 1 == num_tests) printf("\n"); // .. здесь находится код выполняемого теста .. curr_test++; } vTaskResume(xConsoleTask); vTaskDelete(NULL); }

Клиент терминала Linux устроен таким образом, что реализовать в одной строке индикатор прогресса не так просто. Например, следующий код печати в помощью printf не будет выводить проценты прогресса в консоль терминала, несмотря на вызовы flush:

   int progress = (curr_test * 100) / (last_test + 1);
   printf("%d%%\r", progress);
   fflush(stdout);

Причина в том, что клиент терминала (в Ubuntu это программа x-terminal-emulator) буферизирует вывод, и не будет отображать текст, пока не встретит символ перевода строки CR ('\n'), либо пока не произойдет переполнение буфера вывода.

Решить проблему можно с помощью escape-кодов управления (см. ANSI escape code site:wikipedia.org). Последовательность "\x1b[A" позволяет вернуть позицию печати на предыдущую строку, так что "\n\x1b[A\r" позволяет и обновить строку вывода, и вернуть позицию печати обратно:

   int progress = (curr_test * 100) / (last_test + 1);
   printf("%d%%\n\x1b[A\r", progress);
   fflush(stdout);

См. также printf не выводит текст при выходе из программы.

Существует несколько способов управлять поведением задач FreeRTOS и её планировщика: vTaskSuspendAll(), xTaskResumeAll(), назначение приоритетов для задач. Однако для некоторых задач этого может оказаться недостаточно. Может потребоваться программно управлять аппаратными ресурсами с жестко заданными интервалами времени, которые не может обеспечить RTOS. Хороший пример - опрос АЦП HX711.

В заголовочном файле rv_utils.h (он находится в components/riscv/include/riscv каталога установки ESP-IDF) приведены две функции, которые позволяют глобально запрещать и разрешать прерывания: rv_utils_intr_global_enable() и rv_utils_intr_global_disable(). Эти функции следует применять с осторожностью, потому что они оказывают влияние на поведение всей системы целиком.

Для разрешения и запрета определенных аппаратных прерываний смотрите описание API-функций соответствующего драйвера устройства. Например, для разрешения и запрета прерывания по изменению уровня на выводе GPIO можно использовать функцию gpio_set_intr_type().

Это специальное преобразование имен функций в уникальные имена, подходящие для работы компилятора. Такое преобразование особенно важно для языка C++, в котором могут быть несколько функций с одинаковыми именами, но с разными сигнатурами (т. е. с отличающимися наборами параметров).

Для компилятора GCC принята следующая система преобразования имен. Каждому имени функции присваивается префикс, состоящий из __Zn, где n это длина исходного имени функции. Например, для функции foo(void) операцией mangling будет поставлено в соответствие имя __Z3foo.

Если функция имеет параметры, по добавляется суффикс, в котором буквами закодированы типы параметров. Например, для функции foo (int param1, double pafam2) будет поставлено в соответствие имя __Z3fooid.

Программа c++filt позволяет осуществлять обратное преобразование mangling-имени в сигнатуру функции. Например:

$ c++filt -_ __Z3fooid
foo(int, double)

Файл dependencies.lock автоматически генерируется системой Version Solver (см. Version Solver site:docs.espressif.com), и в нем содержатся точные версии компонентов, выбранных в процессе разрешения зависимостей (dependency resolution). Этот файл обеспечивает воспроизводимые сборки, блокируя версии компонентов, чтобы будущие сборки использовали те же версии.

Если ваш проект включает только ESP Component Registry Dependencies или Git Dependencies, то рекомендуется добавить файл dependencies.lock под контроль системы управления версиями. Это гарантирует, что все разработчики используют непротиворечивые версии компонентов при локальной сборке проекта.

Если ваш проект включает Kconfig Options, то файл dependencies.lock не должен контролироваться системой контроля версий. Это связано с тем, что он может содержать локальные пути, характерные для вашей среды, которые не будут доступны другим разработчикам.

Предупреждение: файл dependencies.lock не следует редактировать вручную. Он должен модифицироваться только системой Version Solver.

[Root Attributes (корневые атрибуты) файла dependencies.lock]

dependencies

Атрибут dependencies это словарь, включающий все развернутые зависимости проекта. Каждый ключ представляет имя зависимости, а соответствующее значение содержит атрибуты, описанные далее в разделе "Dependency Attributes".

direct_dependencies

Атрибут direct_dependencies это список прямых зависимостей проекта, указанных непосредственно файлах манифеста проекта. Этот список используется для обеспечения повторного запуска системы сборки при перемещении компонента из ESP Component Registry Dependencies в Kconfig Options.

manifest_hash

Атрибут manifest_hash это хэш всех файлов манифеста, отслеживаемых в проекте. Этот хэш гарантирует, что файл dependencies.lock был сгенерирован на основе последних файлов манифеста.

target

Атрибут target указывает, для какого целевого процессора (esp32, esp32c3, и т. п.) был сгенерирован этот файл dependencies.lock. Это помогает Conditional Dependencies корректно работать в зависимости от выбранного target.

version

Атрибут version показывает версию файла dependencies.lock. Это обеспечивает соответствие его версии с версией IDF Component Manager.

Например IDF Component Manager 1.x использует версию 1.0.0 для файла dependencies.lock, в то время как IDF Component Manager 2.x использует версию 2.0.0.

[Dependency Attributes]

Каждая запись словаря dependencies включает следующие атрибуты:

component_hash

Атрибут component_hash представляет собой хэш компонента, используемый для проверки целостности компонента. Это гарантирует, что компонент не был изменен с момента последней генерации файла dependencies.lock.

dependencies

Атрибут dependencies это словарь, содержащий прямые зависимости компонента.

source

Атрибут source указывает тип компонента. Для дополнительной информации см. секцию "Component Dependencies" документации по файлу манифеста (idf_component.yml Manifest File site:docs.espressif.com).

version

Атрибут version показывает версию компонента, выбранную Version Solver.

targets

Атрибут targets перечисляет все варианты целевых процессоров (esp32, esp32c3, и т. п.), совместимых с этим компонентом. Это поле может быть опущено, если компонент совместим со всеми target.

При запуске проект выдает предупреждающее сообщение наподобие:

W (87) spi_flash: Detected size(4096k) larger than the size in the binary image header(2048k). Using the size in the binary image header.

spi flash detected size fig01

Это означает, что скомпилированное firmware предполагает объем FLASH-памяти 2 мегабайта (определяется настройкой menuconfig-опции Serial Flasher), но на самом деле у вашего чипа в наличии 4 мегабайта. Таким образом, программа будет работать нормально, но ваш код не сможет воспользоваться памятью свыше 2 мегабайт.

Устранить это предупреждение можно, если изменить настройки Serial flasher и перекомпилировать проект. Запустите команду idf.py menuconfig, выберите пункт настроек Serial flasher config и поменяйте опцию Flash size, чтобы она соответствовала реальному объему памяти FLASH:

spi flash detected size fig02

Файл idf_component.yml проекта (он обычно находится в папке main вместе с файлами исходного кода проекта) содержит список зависимостей проекта от компонентов. Чтобы удалить зависимость от определенного компонента:

1. Отредактируйте в файле idf_component.yml список после метки dependencies:, удалив из этого списка компонент.
2. Из папки managed_components проекта удалите папку удаляемого компонента.
3. Выполните очистку и перекомпиляцию проекта (idf.py clean, idf.py build).

Если ваш проект зависел только от одного компонента, зависимость от которого вы хотите удалить, то просто удалите файл idf_component.yml, папку managed_components и файл dependencies.lock, и выполните очистку и компиляцию проекта.

См. также [11].

Например, есть каталог:

c:\Espressif\frameworks\esp-idf-lib\components\esp_idf_lib_helpers\

И в этом каталоге есть файл esp_idf_lib_helpers.h, который мы хотим подключать директивой #include < esp_idf_lib_helpers.h>. Как добавить в проект дополнительный путь подключаемых файлов?

Для этого нужно отредактировать содержимое файла CMakeLists.txt, который находится в папке main проекта, таким образом, чтобы в секции INCLUDE_DIRS появился нужный дополнительный путь. Для нашего примера (добавленный подключаемый путь показан жирным шрифтом):

idf_component_register(SRCS "main.c"
"cmd_console.c"
"i2capp.c"
"ds18b20app.c"
"si5351a_synth.c"
"nvsapp.c"
"debug.c"
"radiochannels.c"
"ifsktx.c"
"ifskcrc.c"
"OneButton.c"
"vars.c"
"rs_code157.c"
"sleep.c"
"blinkapp.c"
#"aht10.c"
"aht.c"
"i2cdev.c"
PRIV_REQUIRES
fatfs
esp_driver_i2c
INCLUDE_DIRS "."
"${IDF_PATH}/../esp-idf-lib/components/esp_idf_lib_helpers")

Обратите внимание, что в строке добавленного пути поиска заголовков используется переменная окружения IDF_PATH, которая для Windows может содержать значение наподобие C:\Espressif\frameworks\esp-idf-v5.4.1:

> set
ALLUSERSPROFILE=C:\ProgramData
...
IDF_PATH=C:\Espressif\frameworks\esp-idf-v5.4.1 IDF_PYTHON=C:/Espressif/python_env/idf5.4_py3.11_env/Scripts/python.exe IDF_PYTHON_DIR=C:\Espressif\python_env\idf5.4_py3.11_env\Scripts\ IDF_PYTHON_ENV_PATH=C:\Espressif\python_env\idf5.4_py3.11_env IDF_TOOLS_EXPORT_CMD=C:\Espressif\frameworks\esp-idf-v5.4.1\export.bat IDF_TOOLS_INSTALL_CMD=C:\Espressif\frameworks\esp-idf-v5.4.1\install.bat IDF_TOOLS_JSON_PATH=C:\Espressif\frameworks\esp-idf-v5.4.1\tools\tools.json IDF_TOOLS_PATH=C:\Espressif IDF_TOOLS_PY_PATH=C:\Espressif\frameworks\esp-idf-v5.4.1\tools\idf_tools.py ... ProgramData=C:\ProgramData ProgramFiles=C:\Program Files ProgramFiles(x86)=C:\Program Files (x86) ProgramW6432=C:\Program Files windir=C:\Windows

См. также "Q005. Как добавить в проект файл исходного кода".

Размер стека консоли по умолчанию создается макросом ESP_CONSOLE_REPL_CONFIG_DEFAULT() (по умолчанию 4096 байт):

    esp_console_repl_t *repl = NULL;
esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT();

#if CONFIG_EXAMPLE_STORE_HISTORY
initialize_filesystem();
repl_config.history_save_path = HISTORY_PATH;
#endif
repl_config.prompt = ">"; // приглашение командной строки консоли
// Инсталляция консоли (console REPL environment), запуск задачи console_repl:
#if CONFIG_ESP_CONSOLE_UART
esp_console_dev_uart_config_t uart_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_console_new_repl_uart(&uart_config, &repl_config, &repl));
#elif CONFIG_ESP_CONSOLE_USB_CDC
esp_console_dev_usb_cdc_config_t cdc_config = ESP_CONSOLE_DEV_CDC_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_console_new_repl_usb_cdc(&cdc_config, &repl_config, &repl));
#elif CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG
esp_console_dev_usb_serial_jtag_config_t usbjtag_config = ESP_CONSOLE_DEV_USB_SERIAL_JTAG_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_console_new_repl_usb_serial_jtag(&usbjtag_config, &repl_config, &repl));
#endif

По умолчанию размер стека задачи консоли устанавливается на 4096 байт. Чтобы поменять этот размер стека, перед инсталляцией задачи консоли поменяйте значение поля task_stack_size структуры repl_config:

    ...
repl_config.prompt = ">"; // приглашение командной строки консоли
repl_config.task_stack_size = 8196;
// Инсталляция консоли (console REPL environment), запуск задачи console_repl:
...

Существует несколько способов узнать информацию текущей задачи FreeRTOS ESP-IDF.

[Способ 1: использование API-функций FreeRTOS]

Получение базовой информации по дескриптору текущей задачи:

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

void print_current_task_info() {
TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
char *task_name = pcTaskGetName(current_task);
UBaseType_t priority = uxTaskPriorityGet(current_task);

printf("Current Task: %s\n", task_name);
printf("Priority: %d\n", priority);
printf("Stack High Water Mark: %d\n", uxTaskGetStackHighWaterMark(current_task)); }

Получение более подробной информации о состоянии задачи:

void print_detailed_task_info() {
TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
TaskStatus_t task_status;

vTaskGetInfo(current_task, &task_status, pdTRUE, eInvalid);

printf("Имя задачи: %s\n", task_status.pcTaskName);
printf("Номер задачи: %d\n", (int)task_status.xTaskNumber);
printf("Текущее состояние: %d\n", (int)task_status.eCurrentState);
printf("Текущий приоритет: %d\n", (int)task_status.uxCurrentPriority);
printf("Базовый приоритет: %d\n", (int)task_status.uxBasePriority);
printf("Счетчик реального времени: %lu\n", task_status.ulRunTimeCounter);
printf("Сколько осталось неиспользованного места в стеке: %d\n",
uxTaskGetStackHighWaterMark(current_task)); }

[Способ 2: использование встроенных команд]

1. Разрешите с помощью menuconfig фичи мониторинга задачи (task monitoring features):

$ idf.py menuconfig

Перейдите в раздел Component config → FreeRTOS и разрешите опции:

Enable FreeRTOS trace utility
Enable FreeRTOS stats formatting functions

Пример функции, печатающей информацию о всех задачах:

void print_all_tasks() {
// Выделите достаточный размер буфера, чтобы в нем
// поместилась информация, которую выдаст vTaskList:
char *buffer = malloc(1024);
if (buffer != NULL) {
vTaskList(buffer);
printf("Task Name\tState\tPriority\tStack\tTask#\n");
printf("------------------------------------------------\n");
printf("%s\n", buffer); free(buffer);
} }

Пример функции, которая выводит статистику времени выполнения FreeRTOS с помощью вызова vTaskGetRunTimeStats():

void print_runtime_stats() {
char *buffer = malloc(1024);
if (buffer != NULL) {
vTaskGetRunTimeStats(buffer);
printf("Runtime-статистика:\n");
printf("%s\n", buffer);
free(buffer);
} }

[Способ 3: использование команд консоли ESP]

Добавьте следующие команды консоли в свое приложение:

#include "esp_console.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

static int task_list_cmd(int argc, char **argv) {
char *buffer = malloc(1024);
if (buffer) {
vTaskList(buffer);
printf("Task Name\tState\tPriority\tStack\tTask#\n");
printf("------------------------------------------------\n");
printf("%s\n", buffer);
free(buffer);
}
return 0; }

static int task_stats_cmd(int argc, char **argv) {
char *buffer = malloc(1024);
if (buffer) {
vTaskGetRunTimeStats(buffer);
printf("Runtime Statistics:\n");
printf("%s\n", buffer);
free(buffer);
}
return 0; }

void register_task_commands() {
const esp_console_cmd_t cmd_list = {
.command = "tasklist",
.help = "List all FreeRTOS tasks",
.hint = NULL,
.func = &task_list_cmd,
};
const esp_console_cmd_t cmd_stats = {
.command = "taskstats",
.help = "Show task runtime statistics",
.hint = NULL,
.func = &task_stats_cmd,
};

esp_console_cmd_register(&cmd_list);
esp_console_cmd_register(&cmd_stats); }

[Полный пример]

#include < stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

void task_monitor(void *pvParameters) {
while (1) {
printf("\n=== Current Task Info ===\n");

// Информация текущей задачи:
TaskHandle_t current = xTaskGetCurrentTaskHandle();
printf("Current: %s (Priority: %d)\n",
pcTaskGetName(current),
uxTaskPriorityGet(current));

// Периодически выводится список всех задач:
char *buffer = malloc(1024);
if (buffer) {
vTaskList(buffer);
printf("\nAll Tasks:\n");
printf("Name\tState\tPri\tStack\t#\n");
printf("------------------------\n");
printf("%s\n", buffer);
free(buffer);
}

vTaskDelay(pdMS_TO_TICKS(5000)); // Информация выводится каждые 5 секунд
} }

void app_main() {
// Создание задачи мониторинга:
xTaskCreate(task_monitor, "task_monitor", 4096, NULL, 1, NULL);

// Далее код вашего приложения:
... }

Важные замечания:

1. Выделение памяти: для вызова функций vTaskList() и vTaskGetRunTimeStats() необходимо предварительно выделить достаточный размер буфера. Подберите этот размер экспериментально, значение для malloc выбирайте такое, чтобы оно нацело делилось на 8.

2. Конфигурация FreeRTOS: чтобы работал сбор runtime-статистики, нужно разрешить опции в файле FreeRTOSConfig.h:

#define configGENERATE_RUN_TIME_STATS 1
#define configUSE_STATS_FORMATTING_FUNCTIONS 1

3. Использование стека: с помощью uxTaskGetStackHighWaterMark() можно отслеживать состояние стека и определять наличие потенциальных проблем. Если функция uxTaskGetStackHighWaterMark() возвратит значение меньше 100, то это повод задуматься об увеличении стека задачи.

4. Текущее состояние задач (Task States) в выводе функции print_all_tasks обозначается буквами:

   - `R`: Running
   - `B`: Blocked
   - `S`: Suspended
   - `D`: Deleted
   - `X`: Ready

ESP-IDF FreeRTOS параметр размера стека usStackDepth функции xTaskCreate() указывается в байтах, а не в словах или каких-либо других единицах.

BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode,
const char * const pcName,
const uint32_t usStackDepth, // ← Это значение задается в БАЙТАХ
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *pxCreatedTask );

Сравните с официальной документацией FreeRTOS по функции xTaskCreate, она указывает ровно обратное:

"uxStackDepth

The number of words (not bytes!) to allocate for use as the task's stack. For example, if the stack is 16-bits wide and uxStackDepth is 100, then 200 bytes will be allocated for use as the task's stack. As another example, if the stack is 32-bits wide and uxStackDepth is 400 then 1600 bytes will be allocated for use as the task's stack. The stack depth multiplied by the stack width must not exceed the maximum value that can be contained in a variable of type size_t. See the FAQ How big should the stack be?."

Практический пример:

// Все эти значения задаются в байтах. Обратите внимание, что целесообразно
// для размера стека указывать значения, нацело делящиеся на 4, поскольку
// 32-битная платформа для хранения в стеке адресов использует тип uint32_t.
#define SMALL_TASK_STACK 2048 // 2KB
#define MEDIUM_TASK_STACK 4096 // 4KB
#define LARGE_TASK_STACK 8192 // 8KB
#define VERY_LARGE_STACK 16384 // 16KB

// Создание задач, размер стека указан в байтах: xTaskCreate(my_task, "task1", SMALL_TASK_STACK, NULL, 1, NULL); xTaskCreate(another_task, "task2", MEDIUM_TASK_STACK, NULL, 1, NULL);

Важные замечания:

1. Абсолютный минимум для очень простых задач:

#define MINIMUM_STACK 768   // 768 байт

2. Рекомендованный минимум для большинства задач:

#define SAFE_MINIMUM 1024   // 1 килобайт

3. Часто используемый размер для стека задачи в ESP-IDF:

#define COMMON_STACK 4096   // 4 килобайта

4. Типовой диапазон размеров стека для большинства задач: 1 .. 16 килобайт.

5. Для безопасности оставляйте свободный размер стека задачи не менее 256 .. 512 байт (можно определить runtime вызовом функции uxTaskGetStackHighWaterMark)

6. Размер стека 32-битной архитектуры ESP указывается в байтах, и оно должно нацело делиться на 4 (размер типа uint32_t).

Типовые размеры стека для различных задач:

// Задачи, работающие с сетью, требуют повышенного размера стека:
#define WIFI_TASK_STACK 6144 // 6KB
#define TCP_TASK_STACK 4096 // 4KB
#define HTTP_SERVER_STACK 8192 // 8KB

// Задачи периферийных устройств:
#define SPI_TASK_STACK 2048 // 2KB
#define I2C_TASK_STACK 2048 // 2KB
#define UART_TASK_STACK 2048 // 2KB

// Задачи приложения:
#define UI_TASK_STACK 4096 // 4KB
#define SENSOR_TASK_STACK 3072 // 3KB
#define LOGGING_TASK_STACK 2048 // 2KB

Рекомендации по размеру стека задачи из документации ESP-IDF:

• Главная задача приложения (app_main): как минимум 3584 байт.
• IDLE task: 1536 байт (конфигурируется автоматически).
• Задача обработчика службы таймера (Timer service task): рекомендуется 4096 байт.

[Детектирование переполнения стека задачи]

В ESP-IDF есть встроенная фича детектирования переполнения стека (Stack Overflow Detection). Разрешите её в menuconfig:

Component config → FreeRTOS → Enable FreeRTOS stack overflow detection

Возможные опции: None, Print warning, Trigger breakpoint.

[Проверка реального использования стека]

void my_task(void *pvParameters) {
// Проверка, сколько стека свободно (в байтах) в момент запуска задачи:
UBaseType_t high_water_mark = uxTaskGetStackHighWaterMark(NULL);
printf("Выделено для стека задачи: %d байт\n", configSTACK_SIZE);

// Далее ваш код задачи, который выполняет какие-либо действия,
// приводящие к утилизации стека задачи:
...

// Проверка текущего состояния стека задачи:
high_water_mark = uxTaskGetStackHighWaterMark(NULL);
printf("Минимальное свободное пространство стека за все время: %d bytes\n", high_water_mark);
printf("Максимальное использованное пространство стека: %d bytes\n", configSTACK_SIZE - high_water_mark);

if (high_water_mark < 256) {
printf("WARNING: мало места в стеке! Свободно только %d байт\n", high_water_mark);
} }

Размер стека задачи в байтах определяется при её создании через третий параметр uxStackDepth вызова xTaskCreate. Если вы не знаете, в каком месте кода создается интересующая вас задача, то сначала узнайте её имя с помощью pcTaskGetName (см. для примера тело функции print_current_task_info в Q038), а потом ищите в коде по этому имени место создания задачи.

Альтернативный способ, но не очень точный (покажет размер несколько меньший, чем реальный размер стека задачи): в теле задачи сразу вызвать функцию uxTaskGetStackHighWaterMark, она покажет свободное место в стеке задачи до того, как задача начнет выполнять какие-либо действия.

При выводе на печать через printf в проекте ESP-IDF однобайтных русских символов в кодировке CP1251 (Windows-1251, коды этих символов находятся в диапазоне 0x80 .. 0xFF) монитор idf.py выводит сообщение:

--- Warning: Failed to decode multiple lines in a row. Try 
 checking the baud rate and XTAL frequency setting in menuconfig

Проблему можно решить несколькими способами.

[Способ1: использовать библиотеку iconv (рекомендуется)]

Этот способ универсальный, но довольно затратный по ресурсам из-за богатого функционала iconv.

Разрешите iconv в menuconfig:

$ idf.py menuconfig

В разделе Component config → Newlib разрешите опцию "Enable ICONV library"

Пример кода:

#include < stdio.h>
#include < string.h>
#include < iconv.h>
#include < esp_log.h>

static const char* TAG = "CP1251_TO_UTF8";
void cp1251_to_utf8(const char* cp1251_str, char* utf8_buffer, size_t buffer_size) {
iconv_t cd = iconv_open("UTF-8", "CP1251");
if (cd == (iconv_t)-1) {
ESP_LOGE(TAG, "Ошибка инициализации iconv");
strncpy(utf8_buffer, cp1251_str, buffer_size);
return;
}
size_t in_bytes_left = strlen(cp1251_str);
size_t out_bytes_left = buffer_size - 1; // резервирование пространства под null-терминатор

char *in_ptr = (char*)cp1251_str; char *out_ptr = utf8_buffer;
size_t result = iconv(cd, &in_ptr, &in_bytes_left, &out_ptr, &out_bytes_left);

if (result == (size_t)-1) {
ESP_LOGE(TAG, "Ошибка преобразования");
}

*out_ptr = '\0'; // завершение Null-терминатором выходной строки

iconv_close(cd); }

void app_main() {
// Строка текста, закодированная в CP1251 (русский текст "Привет, мир!")
unsigned char cp1251_text[] = {0xCF, 0xF0, 0xE8, 0xE2, 0xE5, 0xF2, 0x2C,
0x20, 0xEC, 0xE8, 0xF0, 0x21, 0x00};

char utf8_buffer[256];

cp1251_to_utf8((char*)cp1251_text, utf8_buffer, sizeof(utf8_buffer));

printf("Original CP1251: %s\n", cp1251_text);
printf("Converted UTF-8: %s\n", utf8_buffer);
ESP_LOGI(TAG, "UTF-8: %s", utf8_buffer); }

[Способ2: ручная перекодировка по таблице]

Способ2 требует меньше ресурсов, чем Способ1.

#include < stdio.h>
#include < string.h>
#include < esp_log.h>

static const char* TAG = "CP1251_TO_UTF8";

// Таблица преобразования CP1251 в UTF-8 для символов кириллицы (коды 0x80-0xFF):
static const char* cp1251_to_utf8_table[128] = {
/* 0x80 */ "\u0402", /* 0x81 */ "\u0403", /* 0x82 */ "\u201A", /* 0x83 */ "\u0453",
/* 0x84 */ "\u201E", /* 0x85 */ "\u2026", /* 0x86 */ "\u2020", /* 0x87 */ "\u2021",
/* 0x88 */ "\u20AC", /* 0x89 */ "\u2030", /* 0x8A */ "\u0409", /* 0x8B */ "\u2039",
/* 0x8C */ "\u040A", /* 0x8D */ "\u040C", /* 0x8E */ "\u040B", /* 0x8F */ "\u040F",
/* 0x90 */ "\u0452", /* 0x91 */ "\u2018", /* 0x92 */ "\u2019", /* 0x93 */ "\u201C",
/* 0x94 */ "\u201D", /* 0x95 */ "\u2022", /* 0x96 */ "\u2013", /* 0x97 */ "\u2014",
/* 0x98 */ " ", /* 0x99 */ "\u2122", /* 0x9A */ "\u0459", /* 0x9B */ "\u203A",
/* 0x9C */ "\u045A", /* 0x9D */ "\u045C", /* 0x9E */ "\u045B", /* 0x9F */ "\u045F",
/* 0xA0 */ "\u00A0", /* 0xA1 */ "\u040E", /* 0xA2 */ "\u045E", /* 0xA3 */ "\u0408",
/* 0xA4 */ "\u00A4", /* 0xA5 */ "\u0490", /* 0xA6 */ "\u00A6", /* 0xA7 */ "\u00A7",
/* 0xA8 */ "\u0401", /* 0xA9 */ "\u00A9", /* 0xAA */ "\u0404", /* 0xAB */ "\u00AB",
/* 0xAC */ "\u00AC", /* 0xAD */ "\u00AD", /* 0xAE */ "\u00AE", /* 0xAF */ "\u0407",
/* 0xB0 */ "\u00B0", /* 0xB1 */ "\u00B1", /* 0xB2 */ "\u0406", /* 0xB3 */ "\u0456",
/* 0xB4 */ "\u0491", /* 0xB5 */ "\u00B5", /* 0xB6 */ "\u00B6", /* 0xB7 */ "\u00B7",
/* 0xB8 */ "\u0451", /* 0xB9 */ "\u2116", /* 0xBA */ "\u0454", /* 0xBB */ "\u00BB",
/* 0xBC */ "\u0458", /* 0xBD */ "\u0405", /* 0xBE */ "\u0455", /* 0xBF */ "\u0457",
/* 0xC0 */ "\u0410", /* 0xC1 */ "\u0411", /* 0xC2 */ "\u0412", /* 0xC3 */ "\u0413",
/* 0xC4 */ "\u0414", /* 0xC5 */ "\u0415", /* 0xC6 */ "\u0416", /* 0xC7 */ "\u0417",
/* 0xC8 */ "\u0418", /* 0xC9 */ "\u0419", /* 0xCA */ "\u041A", /* 0xCB */ "\u041B",
/* 0xCC */ "\u041C", /* 0xCD */ "\u041D", /* 0xCE */ "\u041E", /* 0xCF */ "\u041F",
/* 0xD0 */ "\u0420", /* 0xD1 */ "\u0421", /* 0xD2 */ "\u0422", /* 0xD3 */ "\u0423",
/* 0xD4 */ "\u0424", /* 0xD5 */ "\u0425", /* 0xD6 */ "\u0426", /* 0xD7 */ "\u0427",
/* 0xD8 */ "\u0428", /* 0xD9 */ "\u0429", /* 0xDA */ "\u042A", /* 0xDB */ "\u042B",
/* 0xDC */ "\u042C", /* 0xDD */ "\u042D", /* 0xDE */ "\u042E", /* 0xDF */ "\u042F",
/* 0xE0 */ "\u0430", /* 0xE1 */ "\u0431", /* 0xE2 */ "\u0432", /* 0xE3 */ "\u0433",
/* 0xE4 */ "\u0434", /* 0xE5 */ "\u0435", /* 0xE6 */ "\u0436", /* 0xE7 */ "\u0437",
/* 0xE8 */ "\u0438", /* 0xE9 */ "\u0439", /* 0xEA */ "\u043A", /* 0xEB */ "\u043B",
/* 0xEC */ "\u043C", /* 0xED */ "\u043D", /* 0xEE */ "\u043E", /* 0xEF */ "\u043F",
/* 0xF0 */ "\u0440", /* 0xF1 */ "\u0441", /* 0xF2 */ "\u0442", /* 0xF3 */ "\u0443",
/* 0xF4 */ "\u0444", /* 0xF5 */ "\u0445", /* 0xF6 */ "\u0446", /* 0xF7 */ "\u0447",
/* 0xF8 */ "\u0448", /* 0xF9 */ "\u0449", /* 0xFA */ "\u044A", /* 0xFB */ "\u044B",
/* 0xFC */ "\u044C", /* 0xFD */ "\u044D", /* 0xFE */ "\u044E", /* 0xFF */ "\u044F" };

void cp1251_to_utf8_manual(const unsigned char* cp1251_str, char* utf8_buffer, size_t buffer_size) {
size_t i = 0, j = 0;

while (cp1251_str[i] != '\0' && j < buffer_size - 1) {
unsigned char c = cp1251_str[i++];

if (c < 0x80) {
// Символ ASCII - копируем как есть
if (j < buffer_size - 1) {
utf8_buffer[j++] = c;
}
} else {
// Символ CP1251 - перекодируем по таблице
const char* utf8_char = cp1251_to_utf8_table[c - 0x80];
size_t len = strlen(utf8_char);

if (j + len < buffer_size - 1) {
strcpy(&utf8_buffer[j], utf8_char);
j += len;
}
}
}

utf8_buffer[j] = '\0'; }

void app_main() {
// Русский текст в кодировке CP1251:
unsigned char cp1251_text[] = {0xCF, 0xF0, 0xE8, 0xE2, 0xE5, 0xF2, 0x2C,
0x20, 0xEC, 0xE8, 0xF0, 0x21, 0x00};

char utf8_buffer[256];

cp1251_to_utf8_manual(cp1251_text, utf8_buffer, sizeof(utf8_buffer));

printf("CP1251: %s\n", cp1251_text);
printf("UTF-8: %s\n", utf8_buffer);
ESP_LOGI(TAG, "Converted: %s", utf8_buffer); }

[Способ3: таблица перекодировки с HEX-значениями]

Более эффективная ручная перекодировка:

#include < stdio.h>
#include < string.h>

typedef struct {
unsigned char cp1251;
const char* utf8; } cp1251_utf8_map_t;

// Таблица соответствия кодов русских символов cp1251
// символам UTF8 (вы можете расширить эту таблицу):
static const cp1251_utf8_map_t cp1251_utf8_map[] = {
{0xC0, "А"}, {0xC1, "Б"}, {0xC2, "В"}, {0xC3, "Г"}, {0xC4, "Д"},
{0xC5, "Е"}, {0xC6, "Ж"}, {0xC7, "З"}, {0xC8, "И"}, {0xC9, "Й"},
{0xCA, "К"}, {0xCB, "Л"}, {0xCC, "М"}, {0xCD, "Н"}, {0xCE, "О"},
{0xCF, "П"}, {0xD0, "Р"}, {0xD1, "С"}, {0xD2, "Т"}, {0xD3, "У"},
{0xD4, "Ф"}, {0xD5, "Х"}, {0xD6, "Ц"}, {0xD7, "Ч"}, {0xD8, "Ш"},
{0xD9, "Щ"}, {0xDA, "Ъ"}, {0xDB, "Ы"}, {0xDC, "Ь"}, {0xDD, "Э"},
{0xDE, "Ю"}, {0xDF, "Я"}, {0xE0, "а"}, {0xE1, "б"}, {0xE2, "в"},
{0xE3, "г"}, {0xE4, "д"}, {0xE5, "е"}, {0xE6, "ж"}, {0xE7, "з"},
{0xE8, "и"}, {0xE9, "й"}, {0xEA, "к"}, {0xEB, "л"}, {0xEC, "м"},
{0xED, "н"}, {0xEE, "о"}, {0xEF, "п"}, {0xF0, "р"}, {0xF1, "с"},
{0xF2, "т"}, {0xF3, "у"}, {0xF4, "ф"}, {0xF5, "х"}, {0xF6, "ц"},
{0xF7, "ч"}, {0xF8, "ш"}, {0xF9, "щ"}, {0xFA, "ъ"}, {0xFB, "ы"},
{0xFC, "ь"}, {0xFD, "э"}, {0xFE, "ю"}, {0xFF, "я"},
{0x00, NULL} // маркер конца таблицы };

const char* cp1251_char_to_utf8(unsigned char c) {
for (int i = 0; cp1251_utf8_map[i].utf8 != NULL; i++) {
if (cp1251_utf8_map[i].cp1251 == c) {
return cp1251_utf8_map[i].utf8;
}
}
return NULL; }

void cp1251_to_utf8_simple(const unsigned char* cp1251_str, char* utf8_buffer, size_t buffer_size) {
size_t i = 0, j = 0;

while (cp1251_str[i] != '\0' && j < buffer_size - 1) {
unsigned char c = cp1251_str[i++];

if (c < 0x80) {
// ASCII utf8_buffer[j++] = c;
} else {
// CP1251
const char* utf8_char = cp1251_char_to_utf8(c);
if (utf8_char) {
size_t len = strlen(utf8_char);
if (j + len < buffer_size) {
strcpy(&utf8_buffer[j], utf8_char);
j += len;
}
} else {
// Неизвестный символ - скопируйте его как есть или замените
utf8_buffer[j++] = '?';
}
}
}

utf8_buffer[j] = '\0'; }

Важные замечания:

1. Для библиотеки iconv разрешите ICONV в menuconfig.
2. Предоставляйте выходной буфер для текста достаточного размера (символ UTF-8 может иметь длину до 4 байт).
3. Обработка ошибок: проверяйте возвращаемые значения функций iconv на наличие ошибок преобразования.
4. Производительность: для частых преобразований сохраняйте открытым контекст iconv вместо повторяющихся открытий/закрытий.

Метод на основе iconv рекомендуется как стандартный и проверенный, он правильно обработает все возможные случаи. Преобразования с помощью ручных методов (Способ2 и Способ3) позволят вам избежать зависимости от iconv, или выполнить обработку специальных случаев.

См. также "Перекодировка русского текста из UTF-8 в CP1251".

Ошибка появилась на массив вида:

// Таблица преобразования CP1251 в UTF-8 для символов кириллицы (коды 0x80-0xFF):
static const char* cp1251_to_utf8_table[128] = {
/* 0x80 */ "\u0402", /* 0x81 */ "\u0403", /* 0x82 */ "\u201A", /* 0x83 */ "\u0453",
/* 0x84 */ "\u201E", /* 0x85 */ "\u2026", /* 0x86 */ "\u2020", /* 0x87 */ "\u2021",
/* 0x88 */ "\u20AC", /* 0x89 */ "\u2030", /* 0x8A */ "\u0409", /* 0x8B */ "\u2039",
/* 0x8C */ "\u040A", /* 0x8D */ "\u040C", /* 0x8E */ "\u040B", /* 0x8F */ "\u040F",
/* 0x90 */ "\u0452", /* 0x91 */ "\u2018", /* 0x92 */ "\u2019", /* 0x93 */ "\u201C",
/* 0x94 */ "\u201D", /* 0x95 */ "\u2022", /* 0x96 */ "\u2013", /* 0x97 */ "\u2014",
/* 0x98 */ "\u0020", /* 0x99 */ "\u2122", /* 0x9A */ "\u0459", /* 0x9B */ "\u203A",
// ^ здесь ошибка
/* 0x9C */ "\u045A", /* 0x9D */ "\u045C", /* 0x9E */ "\u045B", /* 0x9F */ "\u045F",
/* 0xA0 */ "\u00A0", /* 0xA1 */ "\u040E", /* 0xA2 */ "\u045E", /* 0xA3 */ "\u0408",
...

Исправляется ошибка просто: замените строку "\u0020" на строку " ".

На примере сообщения о потенциально небезопасном доступе к невыровненному полю упакованной структуры:

warning: taking address of packed member of 'struct < anonymous>' may result
 in an unaligned pointer value [-Waddress-of-packed-member]

Подавить это сообщение только в одном нужном месте можно с помощью директивы #pragma GCC diagnostic. Пример:

// Packed структура (выравнивание 1 байт)
struct __attribute__((packed)) network_packet {
uint8_t header;
uint32_t timestamp;
uint16_t checksum; };

void process_packet_safe(void) {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Waddress-of-packed-member"

struct network_packet packet;

// Здесь предупреждение подавлено
uint32_t *ts_ptr = &packet.timestamp;
*ts_ptr = 123456789;

#pragma GCC diagnostic pop

printf("Timestamp: %lu\n", *ts_ptr); }

void process_packet_unsafe(void) {
struct network_packet packet;

// Здесь будет предупреждение
uint32_t *ts_ptr = &packet.timestamp;
*ts_ptr = 987654321;

printf("Timestamp: %lu\n", *ts_ptr); }

void app_main(void) {
process_packet_safe(); // без предупреждения
process_packet_unsafe(); // с предупреждением }

Важные замечания:

1. Безопасность: Предупреждение существует не просто так. Доступ к невыровненной памяти может вызвать следующие проблемы:

• Падение производительности (на некоторых архитектурах)
• Ошибки alignment fault (на ARM, RISC-V)
• Неатомарный доступ к данным

2. Архитектура: на ESP32-C3 (архитектура RISC-V) невыровненный доступ может работать, но с понижением производительности.

3. Лучшая практика: если возможно, используйте memcpy() для безопасного доступа к полям упакованных структур:

void safe_access_example(void)
{
struct network_packet packet;

// Безопасное чтение
uint32_t timestamp;
memcpy(&timestamp, &packet.timestamp, sizeof(timestamp));

// Безопасная запись
uint32_t new_ts = 123456;
memcpy(&packet.timestamp, &new_ts, sizeof(new_ts)); }

Таким образом, используйте #pragma GCC diagnostic для точечного подавления предупреждения только там, где вы уверены в безопасности операции.

Существует несколько способов.

1. Стандартная функция esp_rom_delay_us (рекомендуется)

#include "esp_rom_sys.h"

void delay_microseconds(uint32_t us) {
esp_rom_delay_us(us); }

// Использование: delay_microseconds(1000); // Задержка 1000 микросекунд = 1 мс

2. Функция ets_delay_us

#include "esp32c3/rom/ets_sys.h"

void delay_us(uint32_t microseconds) {
ets_delay_us(microseconds); }

// Использование: delay_us(500); // Задержка 500 микросекунд

3. Использование FreeRTOS vTaskDelay

Поскольку FreeRTOS ESP-IDF не позволяет установить длительность тика меньше 1 мс, этот метод подойдет только для задержек с кратностью не меньше 1 мс, т. е. для задержек с микросекундной точностью этот вариант не подойдет.

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

void delay_ms(uint32_t milliseconds) {
vTaskDelay(milliseconds / portTICK_PERIOD_MS); }

4. Точная задержка с использованием циклов

#include "esp_cpu.h"
#include "soc/rtc.h"

void precise_delay_us(uint32_t us) {
// Вместо 1000000 подберите константу под известную частоту CPU:
uint32_t cycles_per_us = esp_cpu_get_cycle_count() / 1000000;
uint32_t cycles = us * cycles_per_us;

uint32_t start = esp_cpu_get_cycle_count();
while (esp_cpu_get_cycle_count() - start < cycles) {
// busy wait
} }

5. Использование системного таймера

#include "esp_timer.h"

void timer_delay_us(uint64_t us) {
uint64_t start = esp_timer_get_time();
while (esp_timer_get_time() - start < us) {
// busy wait
} }

6. Оптимизированная ассемблерная задержка

#include "esp_attr.h"

void IRAM_ATTR busy_delay_us(uint32_t us) {
uint32_t cycles = us * (CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ);
__asm__ __volatile__ (
"1: \n" " addi %0, %0, -1 \n"
" bnez %0, 1b \n"
: "+r" (cycles)
); }

[Пример программы]

#include < stdio.h>
#include "esp_rom_sys.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

void app_main(void) {
while (1) {
printf("Задержка 1 секунда...\n");
esp_rom_delay_us(1000000); // 1 секунда

printf("Задержка 500 мкс...\n");
esp_rom_delay_us(500); // 500 микросекунд

printf("Задержка 100 мс через FreeRTOS...\n");
vTaskDelay(100 / portTICK_PERIOD_MS); // 100 миллисекунд
} }

Для точных временных интервалов:

#include "driver/gptimer.h"

void precise_timer_delay_us(uint64_t us) {
gptimer_handle_t gptimer = NULL;
gptimer_config_t timer_config = {
.clk_src = GPTIMER_CLK_SRC_DEFAULT,
.direction = GPTIMER_COUNT_UP,
.resolution_hz = 1000000, // 1 МГц = 1 мкс разрешение
};

esp_err_t err = gptimer_new_timer(&timer_config, &gptimer);
if (err == ESP_OK) {
gptimer_start(gptimer);
uint64_t count;
gptimer_get_raw_count(gptimer, &count);
while (gptimer_get_raw_count(gptimer, &count) == ESP_OK && count < us) {}
gptimer_stop(gptimer);
gptimer_del_timer(gptimer);
} }

[Рекомендации]

• Для обычных задержек используйте esp_rom_delay_us().
• Для длительных задержек с точностью до миллисекунд используйте vTaskDelay().
• Для точных временных интервалов используйте таймеры.
• В прерываниях используйте busy-wait циклы.

Не используйте ets_delay_us() в критических секциях или прерываниях, так как она может заблокировать выполнение кода.

[Ссылки]

1. Configuring Launch target.
2. [Advice] Boost ESP-IDF Compile time on Windows 10 site:reddit.com.
3. ESP32 compiling time is way too long site:esp32.com.
4. Установка среды разработки ESP-IDF для ESP32.
5. Конфигурация проекта ESP-IDF.
6. ESP-IDF Build System.
7. ESP32: проблемы с прошивкой SPI Flash и загрузкой.
8. hxtools bin2c site:manpages.debian.org.
9. Embedding binary blobs using gcc mingw site:stackoverflow.com.
10. inttypes.h I/O format of integer types site:ibm.com.
11idf.py: как пропустить проверку зависимостей?

 

Добавить комментарий


Защитный код
Обновить

Top of Page