Программирование ARM ESP32-C3: выделение памяти из кучи Tue, October 08 2024  

Поделиться

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

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

ESP32-C3: выделение памяти из кучи Печать
Добавил(а) microsin   

[Стек и куча]

Приложения ESP-IDF используют общие понятия, принятые в архитектуре компьютеров - стек (stack, динамическое выделение памяти под локальные переменные и параметры во время выполнения кода программы) и куча (heap, динамически выделяемая память путем вызова специальных функций), а также статически выделяемая память (которая выделяется в момент компиляции программы).

Система программирования ESP-IDF [2] использует многопоточную среду выполнения (порт FreeRTOS), и у каждой задачи (RTOS task) имеется своя собственная область памяти для стека. По умолчанию каждая из этих областей стека выделяется из кучи в момент создания задачи (см. xTaskCreateStatic() для альтернативы, когда стеки выделяются статически).

Из-за того, что ESP32-C3 использует несколько типов оперативной памяти (RAM), у программы на этом чипе также имеется несколько куч с разными свойствами. Распределитель памяти, учитывающий возможности куч, позволяет приложениям выделять память кучи для определенных целей.

Для большинства таких целей для выделения памяти из кучи, без учета особенностей типов памяти, могут использоваться стандартные libc-функции malloc() и free().

Однако для полного контроля над всеми типами памяти и их характеристиками ESP-IDF также реализует специальный распределитель памяти, учитывающий характеристики памяти (capabilities-based heap memory allocator). Если вам нужна память со специальными возможностями, например оперативная память, которая совместима с прямым доступом (DMA-Capable Memory) или оперативная память для исполняемого кода (executable-memory), то вы можете создать OR-маску для определенных возможностей и передать её в функцию heap_caps_malloc().

[Возможности памяти]

Микроконтроллер ESP32-C3 содержит несколько типов RAM:

DRAM (Data RAM). Это память, используемая для хранения данных. Это обычный тип оперативной памяти, к которой доступ осуществляется как к куче.

IRAM (Instruction RAM). Эта память обычно хранит только данные исполняемого кода. К ней осуществляется доступ как к generic-памяти, все операции должны быть выровнены на 32-битные ячейки памяти (адрес должен нацело делиться на 4).

D/IRAM. Это RAM, которая может использоваться или как оперативная память инструкций (Instruction RAM), или как оперативная память данных (Data RAM).

Подробнее об этих типах памяти см. [3].

DRAM позволяет выделять память с побайтным доступом MALLOC_CAP_8BIT (память можно читать и записывать по байтовому адресу). При вызове функции malloc() её реализация в ESP-IDF вызывает внутри себя heap_caps_malloc(size, MALLOC_CAP_8BIT), чтобы выделить DRAM с возможностью побайтной адресации. Чтобы узнать свободную область кучи DRAM во время выполнения кода, вызовите heap_caps_get_free_size(MALLOC_CAP_8BIT).

Поскольку функция malloc внутренне использует систему выделения памяти, учитывающую свойства типов памяти (capabilities-based allocation system), поэтому память, выделенная heap_caps_malloc(), может быть освобождена вызовом стандартной функции free().

[Доступное пространство в куче]

DRAM. В момент запуска (startup), куча DRAM содержит всю память, которая не была выделена статически в приложении. Если уменьшить размер статически выделенных массивов и буферов в программе, то это увеличит объем свободной памяти кучи.

Чтобы узнать, сколько статически выделенной памяти используется в программе, используйте команду 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)

В этом примере показано, что в момент компиляции вычисленная оставшаяся не использованной область DRAM составляет 171948 байт, и это теоретический максимум, который может быть доступен для кучи DRAM.

Замечание: во время выполнения кода доступное пространство кучи может быть меньше, чем вычисленное пространство в момент компиляции (командой idf.py size). Причина в том, что из кучи выделяется память кодом startup перед тем, как запустится планировщик FreeRTOS (включая память, выделяемую для стеков запускаемых задач FreeRTOS).

IRAM. В момент запуска (startup) куча IRAM содержит всю память инструкций, которая не использовалась исполняемым кодом приложения. Команда idf.py size может использоваться, чтобы определить количество памяти IRAM, используемой приложением.

D/IRAM. Некоторая память ESP32-C3 доступна как DRAM или IRAM. Если была выделена память из региона D/IRAM, то уменьшится свободная память кучи, доступная для обоих типов памяти.

В момент запуска ESP-IDF выводит суммарную информацию всех адресов куч (и их размеров and sizes) на уровне лога Info:

I (252) heap_init: Initializing. RAM available for dynamic allocation:
I (259) heap_init: At 3FFAE6E0 len 00001920 (6 KiB): DRAM
I (265) heap_init: At 3FFB2EC8 len 0002D138 (180 KiB): DRAM
I (272) heap_init: At 3FFE0440 len 00003AE0 (14 KiB): D/IRAM
I (278) heap_init: At 3FFE4350 len 0001BCB0 (111 KiB): D/IRAM
I (284) heap_init: At 4008944C len 00016BB4 (90 KiB): IRAM

Как определить свободную область кучи, см. раздел "Heap Information" документации [4].

[Специальные возможности областей памяти]

DMA-Capable Memory. Используйте флаг MALLOC_CAP_DMA для выделения памяти, которая подходит для использования вместе с DMA-совместимой аппаратурой чипа (например SPI и I2S). Этот capability-флаг исключает любую внешнюю PSRAM.

Память, доступная с выравниванием 32 бита. Если определенные структуры памяти должны быть адресованы исключительно по адресу, нацело делящемуся на 4 (32-bit access), например должен быть выделен массив для чисел int или для указателей, то может быть полезным выделение памяти в куче с флагом MALLOC_CAP_32BIT. Это также позволяет аллокатору предоставлять память IRAM, чего он не мог бы делать при использовании обычного вызова malloc(). Это может помочь для исопльзования всей доступной памяти в ESP32-C3.

К памяти, выделенной с флагом MALLOC_CAP_32BIT, можно обращаться только 32-битными операциями чтения и записи, любой другой тип доступа будет генерировать фатальную ошибку (LoadStoreError exception).

[Справочник API - Heap Allocation]

Заголовочный файл heap/include/esp_heap_caps.h. Ниже в таблице приведен общий справочник по назначению API-функций. Подробное описание параметров этих функций, возвращаемых ими значений см. в документации [1].

Функция Описание
heap_caps_register_failed_alloc_callback Регистрирует callback-функцию, которая будет вызываться, когда попытка выделения памяти потерпела неудачу.
heap_caps_malloc Выделит непрерывный кусок памяти с указанными возможностями (capabilities). Семантика у функции эквивалентна libc-аналогу malloc(), но с добавлением учета запрашиваемых возможностей. В ESP-IDF вызов malloc(p) эквивалентен heap_caps_malloc(p, MALLOC_CAP_8BIT).
heap_caps_free Освободит память, ранее выделенную вызовом heap_caps_malloc() или heap_caps_realloc(). Семантика у функции эквивалентна libc-аналогу free(), но с добавлением поддержки запрошенных возможностей памяти. В ESP-IDF вызов free(p) эквивалентен вызову heap_caps_free(p).
heap_caps_realloc Изменяет выделение памяти, которое было ранее выделено вызовом heap_caps_malloc() или heap_caps_realloc(). Семантика у функции эквивалентна libc-аналогу realloc(), но с добавлением учета запрашиваемых возможностей. В ESP-IDF вызов realloc(p, s) эквивалентен heap_caps_realloc(p, s, MALLOC_CAP_8BIT). Третий параметр 'caps' может отличаться от соответствующего параметра, с которым был связан оригинальный указатель на ранее выделенную память. Таким образом heap_caps_realloc может переместить буфер в память с другим необходимым набором возможностей.
heap_caps_aligned_alloc Выделит выровненный кусок памяти с указанными возможностями. Семантика у функции эквивалентна libc-аналогу aligned_alloc(), но с добавлением учета запрашиваемых возможностей.
heap_caps_aligned_free Используется для освобождения памяти, ранее выделенной вызовом heap_caps_aligned_alloc. Замечание: эта функция устарела, используйте вместо неё heap_caps_free().
heap_caps_aligned_calloc Выделит выровненный кусок памяти с указанными возможностями. Начальное значение выделенной памяти будет заполнено нулями.
heap_caps_calloc Выделит кусок памяти с указанными возможностями. Начальное значение выделенной памяти будет заполнено нулями (соответствует libc-функции calloc).
heap_caps_get_total_size Возвратит общий размер всех регионов кучи с указанными возможностями. Эта функция использует все регионы памяти, обладающие указанными возможностями, добавляя их область в общий результат.
heap_caps_get_free_size Возвратит общий свободный размер всех регионов кучи с указанными возможностями. Эта функция использует все регионы памяти, обладающие указанными возможностями, добавляя их свободную область в общий результат.
heap_caps_get_minimum_free_size Возвратит общую минимальную свободную память всех регионов с указанными возможностями. Функция складывает самые маленькие свободные области всех регионов, которые были обнаружены во время жизни приложения на момент вызова функции. Имейте в виду, что результат может быть меньше, чем глобальный минимум доступной кучи этого вида, поскольку "самые маленькие свободные области" отслеживаются индивидуально для каждого региона в течение времени жизни приложения. Отдельные регионы куч могут достигнуть своей "минимальной свободной области" в разные моменты времени. Однако этот результат по-прежнему дает индикацию "наихудшего случая" для постоянной минимальной свободной кучи.
heap_caps_get_largest_free_block Извлечет самый большой свободный блок памяти, который можно было бы выделить с указанными возможностями. Возвратит самое большое значение s, которое было бы передано в успешный вызов heap_caps_malloc(s, caps).
heap_caps_get_info Извлечет информацию кучи для всех регионов с указанными возможностями. Вызовет multi_heap_info() на всех кучах, которые поддерживают указанные возможности. Возвращенная информация представляет собой агрегат по всем подходящим кучам. Смысл полей тот же самый, как определено для multi_heap_info_t, кроме minimum_free_bytes, для чего существуют те же предостережения, что и для heap_caps_get_minimum_free_size().
heap_caps_print_heap_info Напечатает суммарную информацию по всем видам памяти с указанными возможностями. Вызовет multi_heap_info на всех кучах, которые поддерживают указанные возможности, и печатается по 2 строки на для каждого вида памяти, и в конце общая информация.
heap_caps_check_integrity_all Проверит целостность всех куч памяти системы. Вызовет multi_heap_check для каждой кучи. Опционально напечатает ошибки, если обнаружатся поврежденные кучи. Вызов этой функции эквивалентен вызову heap_caps_check_integrity с аргументом caps, установленным в MALLOC_CAP_INVALID.
heap_caps_check_integrity Проверит целостность всех куч с указанными возможностями. Вызовет multi_heap_check на всех кучах, которые поддерживают указанные возможности. Опционально напечатает ошибки, если проверка показала повреждение кучи. См. также heap_caps_check_integrity_all для проверки всех куч системы, и heap_caps_check_integrity_addr для проверки памяти возле одного адреса.
heap_caps_check_integrity_addr Проверка целостности кучи возле указанного адреса. Эта функция может использоваться для проверки целостности одного региона памяти кучи, который содержит указанный адрес. Это может быть полезно для отладки целостности кучи при повреждении по известному адресу, при этом накладные расходы на проверку меньше, чем на проверку всех регионов кучи. Обратите внимание, что если поврежденный адрес переместится между запусками (из-зв тайминга или других факторов), то этот способ не сработает, и вы вместо этого должны вызвать heap_caps_check_integrity или heap_caps_check_integrity_all. Замечание: проверяется весь регион возле указанного адреса, а не только соседние блоки кучи.
heap_caps_malloc_extmem_enable Разрешает malloc() на внешней памяти, и устанавливает лимит, ниже которого malloc() пытается поместить выделяемый блок памяти во внутреннюю память. Когда используется внешняя память, стратегия выделения сначала пытается удовлетворить запросы выделения малых блоков на внутренней памяти, и запросы выделения больших блоков на внешней памяти. Эта функция устанавливает предел между этими выделениями, а также разрешает использовать для выделения блоков внешнюю память.
heap_caps_malloc_prefer Выделит кусок памяти в порядке убывания. Внимание: переменный список параметров (...) это флаги OR-операции для флагов MALLOC_CAP_*, показывающих типы памяти. Эта API-функция предпочтительно выделяет память с первым параметром. Если это не получилось, то берется следующий параметр в списке. Эти попытки будут продолжаться в таком порядке до тех пор, пока не будет успешно выделен блок памяти или не получится выделить память ни с одним из параметров в списке.
heap_caps_realloc_prefer Заново выделит кусок памяти в порядке убывания.
heap_caps_calloc_prefer Выделит кусок памяти в порядке убывания и заполнит его нулями.
heap_caps_dump Дамп полной структуры всех куч с совпавшими возможностями. Напечатает много чего в последовательный порт (из-за ограничений блокировки вывод пропускает stdout/stderr). Для каждого блока (переменного размера) в каждой совпавшей куче на одной строке выводится следующая информация: адрес блока (буфер данных, возвращенный malloc, это 4 байта, если отладка кучи установлена в Basic, или иначе 8 байт), размер данных (размер данных может быть больше, чем запрашивал malloc, либо из-за фрагментации кучи, либо из-за уровня отладки), адрес следующего блока в куче. Если блок свободен, то также печатается следующий свободный блок.
heap_caps_dump_all Дамп полной структуры всех куч. Относится ко всем зарегистрированным кучам. Напечатает в последовательный порт много информации, вывод такой же как и у heap_caps_dump.
heap_caps_get_allocated_size Возвратит размер блока, который был назначен определенному указателю. Замечание: произойдет сбой приложения со срабатыванием assert, если указатель был неправильный.

Макросы

MALLOC_CAP_EXEC Флаг, показывающий возможности разных систем памяти. Память должна быть способной содержать исполняемый код. 

MALLOC_CAP_32BIT Память должна позволять операции доступа по выровненному адресу на 32-разрядные данные (байтовый адрес нацело делится на 4).

MALLOC_CAP_8BIT Память должна позволять операции доступа на 8/16/...-разрядные данные.

MALLOC_CAP_DMA Память должна позволять доступ к ней со стороны DMA.

MALLOC_CAP_PID2 Память должна быть отображена на область памяти PID2 (PID-ы в настоящее время не используются).

MALLOC_CAP_PID3 Память должна быть отображена на область памяти PID3 (PID-ы в настоящее время не используются).

MALLOC_CAP_PID4 Память должна быть отображена на область памяти PID4 (PID-ы в настоящее время не используются).

MALLOC_CAP_PID5 Память должна быть отображена на область памяти PID5 (PID-ы в настоящее время не используются).

MALLOC_CAP_PID6 Память должна быть отображена на область памяти PID6 (PID-ы в настоящее время не используются).

MALLOC_CAP_PID7 Память должна быть отображена на область памяти PID7 (PID-ы в настоящее время не используются).

MALLOC_CAP_SPIRAM Память должна быть SPI RAM.

MALLOC_CAP_INTERNAL Память должна быть внутренней; в частности, она не должна пропадать, когда выключена кэш flash/spiram.

MALLOC_CAP_DEFAULT Память может быть возвращена для выделения, когда не были указаны возможности памяти (т. е. запросы выделения были сделаны стандартными функциями malloc(), calloc()).

MALLOC_CAP_IRAM_8BIT Память должна быть в IRAM, и должна допускать не выровненный доступ.

MALLOC_CAP_RETENTION, MALLOC_CAP_INVALID Память не может использоваться / маркер конца списка.

Определения типа

typedef void (*esp_alloc_failed_hook_t)(size_t size, uint32_t caps, const char *function_name);

Callback-функция, вызываемая при сбое операции выделения, если она была зарегистрирована (см. heap_caps_register_failed_alloc_callback).

Безопасное использование в потоках (Thread Safety). Функции кучи безопасны для использования в потоках (thread safe). Это означает, что они могут быть вызваны из разных задач одновременно без каких-либо ограничений.

Есть техническая возможность вызова malloc, free и связанных с ними функций из обработчика прерывания (ISR). Однако это делать не рекомендуется, поскольку функции кучи могут задерживать обработку других прерываний. Настоятельно рекомендуется провести рефакторинг приложений так, чтобы любые буферы, используемые в ISR, были предварительно выделены кодом вне ISR. Поддержка вызова функций кучи из ISR может быть удалена в будущих релизах ESP-IDF.

Трассировка и отладка проблем с кучей. На страничке документации [4] рассматриваются вопросы:

· Heap Information, получение информации о куче (свободное пространство, и т. п.).
· Heap Corruption Detection, определение повреждения кучи.
· Heap Tracing, трассировка (детектирование утечки памяти, мониторинг, и т. п.).

[Справочник API - инициализация]

Заголовочный файл: heap/include/esp_heap_caps_init.h. Ниже в таблице приведен общий справочник по назначению API-функций. Подробное описание параметров этих функций, возвращаемых ими значений см. в документации [1].

Функция Описание
heap_caps_init Инициализирует аллокатор кучи, учитывающий возможности памяти. Эта функция вызывается однократно кодом IDF startup. Еще раз вызывать эту функцию не нужно.
heap_caps_enable_nonos_stack_heaps Разрешает кучу (кучи) в регионах памяти, где находятся стеки. Во время startup ядра pro/app CPU получают определенный регион памяти, который они используют в качестве стека, поэтому мы не можем производить выделения блоков памяти в этих фреймах стека. Когда FreeRTOS полностью запущена, эти фреймы стека больше не используются, и они могут быть задействованы для куч.
heap_caps_add_region Добавит регион памяти в коллекцию куч во время выполнения кода (runtime). Большинство регионов памяти определены в модуле soc_memory_layout.c for чипа SoC, и регистрируются вызовом heap_caps_init(). Некоторые из регионов не могут использоваться немедленно, и разрешаются позже с помощью heap_caps_enable_nonos_stack_heaps(). Вызов этой функции добавит регион памяти в кучу в некоторый более поздний момент времени. Эта функция не учитывает ни одну из "reserved" областей или другие данные в soc_memory_layout, вызывающий код должен это учитывать сам. Таким образом, вся память в указанном регионе через параметры start и end, должна быть не используемой. Возможности новой зарегистрированной памяти будут определяться по начальному адресу (start), как указано в регионах soc_memory_layout.c. Используйте heap_caps_add_region_with_caps() для регистрации региона с пользовательскими возможностями.
heap_caps_add_region_with_caps Добавит регион памяти с пользовательскими возможностями к коллекции куч во время выполнения кода программы (runtime). Работает подобно heap_caps_add_region(), только вызывающий код указывает возможности памяти, задаваемые пользователем.

Замечания по реализации. Знания об областях памяти в микросхеме поступают из компонента "soc", который содержит информацию компоновки памяти для микросхемы, и различные возможности каждой области. Возможности каждого региона приоритезированы, поэтому (для примера) выделенные регионы DRAM и IRAM будут использоваться для распределения блоков памяти перед более универсальными регионами DRAM/IRAM.

Каждый непрерывный регион памяти содержит свою собственную кучу памяти. Кучи создаются с помощью функциональности multi_heap. Эта multi_heap позволяет любой непрерывный регион памяти использовать как кучу.

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

Вызов free() производит поиск определенной кучи, соответствующей указанному адресу освобождаемого блока, и затем будет вызвана multi_heap_free() на этом конкретном экземпляре multi_heap.

[Справочник API - Multi Heap API]

Замечание: multi heap API используется аллокатором, учитывающим возможности памяти кучи. Большинство программ на основе библиотек IDF никогда не будут нуждаться в прямом вызове этих API-функций.

Заголовочный файл: heap/include/multi_heap.h. Ниже в таблице приведен общий справочник по назначению API-функций. Подробное описание параметров этих функций, возвращаемых ими значений см. в документации [1].

Функция Описание
multi_heap_aligned_alloc Выделит кусок памяти с определенным выравниванием адреса.
multi_heap_malloc Выполнит выделение буфера в указанной куче. Семантика такая же, как у стандартной функции malloc(), отличие только в том, что возвращенный буфер будет выделен в указанной куче.
multi_heap_aligned_free Освободит выровненный буфер, выделенный в указанной куче. Замечание: эта функция устарела, вместо неё используйте multi_heap_free().
multi_heap_free Освободит буфер в указанной куче. Семантика такая же, как у стандартной free(), только аргумент 'p' должен быть NULL, или должен размещаться в указанной куче.
multi_heap_realloc Изменит выделение буфера в указанной куче. Семантика такая же, как у стандартной функции realloc(), только аргумент 'p' должен быть NULL, или должен размещаться в указанной куче.
multi_heap_get_allocated_size Возвратит размер, который был назначен определенному указателю.
multi_heap_register Регистрирует новую кучу для использования. Эта функция инициализирует кучу по указанному адресу и возвратит дескриптор для будущих операций с кучей. Не существует комплементарной функции для отмены регистрации кучи - если все блоки в куче свободны, то вы можете немедленно начать использовать эту память для других целей.
multi_heap_set_lock Ассоциирует приватный указатель блокировки с кучей. Предоставляется аргумент lock для макросов MULTI_HEAP_LOCK() и MULTI_HEAP_UNLOCK(), которые определены в multi_heap_platform.h. Рассматриваемая блокировка должна быть рекурсивной. Когда куча была зарегистрирована впервые, связанная блокировка lock равна NULL.
multi_heap_dump Дамп информации кучи в stdout. Для целей отладки эта функция делает дами информации по каждому блоку в куче.
multi_heap_check Проверит целостность кучи. Просматривает кучу и проверяет корректность всех структур данных кучи. Если была обнаружена какая-нибудь ошибка, то опционально в stderr может быть напечатано сообщение, относящееся к ошибке. Поведение печати может быть изменено во время компиляции путем определения MULTI_CHECK_FAIL_PRINTF в заголовочном файле multi_heap_platform.h.
multi_heap_free_size Возвратит размер свободного пространства в куче. Возвратит количество байт, доступное для выделения в куче. Эквивалент полю total_free_bytes, возвращенному вызовом multi_heap_get_heap_info(). Обратите внимание, что куча может быть фрагментирована, так что реальный максимальный размер для одиночного вызова malloc() может быть меньше. Чтобы узнать этот размер, см. поле largest_free_block, возвращенное вызовом multi_heap_get_heap_info().
multi_heap_minimum_free_size Возвратит минимальный свободный размер кучи, полученный за время жизни приложения. Эквивалент полю minimum_free_bytes, возвращенному вызовом multi_heap_get_info().
multi_heap_get_info Возвратит метаданные по указанной куче. Заполнит структуру multi_heap_info_t информацией по указанной куче.

Структуры

/** @brief Структура для доступа к метаданным кучи с помощью вызова multi_heap_get_info */
typedef struct {
    size_t total_free_bytes;      ///<  Общее свободное пространство кучи в байтах.
                                  //    Эквивалент вызову multi_free_heap_size().
    size_t total_allocated_bytes; ///<  Общее количество байт, выделенное в куче.
    size_t largest_free_block;    ///<  Размер самого большого свободного блока в куче. Это 
                                  //    самый большой блок, который можно однократно выделить.
    size_t minimum_free_bytes;    ///<  Минимальное свободное пространство в куче,
                                  //    зарегистрированное за время жизни приложения.
                                  //    Эквивалент multi_minimum_free_heap_size().
    size_t allocated_blocks;      ///<  Количество (разного размера) блоков, выделенных в куче.
    size_t free_blocks;           ///<  Количество (разного размера) свободных блоков в куче.
    size_t total_blocks;          ///<  Общее количество (разного размера) блоков в куче.
} multi_heap_info_t;

Определения типа

typedef struct multi_heap_info *multi_heap_handle_t

Непрозрачный дескриптор для зарегистрированной кучи.

[Ссылки]

1. ESP-IDF Heap Memory Allocation site:docs.espressif.com.
2. Установка среды разработки ESP-IDF для ESP32.
3. ESP32: типы памяти.
4. ESP-IDF Heap Memory Debugging site:docs.espressif.com.

 

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


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

Top of Page