• Система автоматизации с открытым исходным кодом на базе esp8266/esp32 микроконтроллеров и приложения IoT Manager. Наша группа в Telegram

нужна помощь! Как увеличить TCP_WND в ESP8666/Arduino?

pvvx

Активный участник сообщества
Вот спасибо, давно я в ArduinoIDE не заглядывал, пришлось даже установить) но, к сожалению, что-то пошло не так: при выборе этого пункта получаю ошибку:
"exec: "make": executable file not found in %PATH%"
для пересборки Lwip в Arduino для windows необходимо - иметь make.exe в path
т.е. make.exe все-таки какойто нужен дополнительно, в самом Arduino IDE его нет. Словом, вернулись к началу)
Т.е. итоги ардуино-головного-мозгу уже сказались? А говорили что "Однако, для образовательных поделок на Ардуине, ESP все еще вне конкуренции. ".
Достаточно mingw32-make в малом пакете mingw32 и приписать пару букв в одной строке файла "C:\Users\ИмяПользователя\AppData\Local\Arduino15\packages\esp8266\hardware\esp8266\2.7.4\boards.txt"

Тоже самое о "lwipopts.h" - с начала времен, с 2014 года, в SDK Espressif часть опций LwIP всегда находилась в регистрах RTC, так и сидят там.

И ещё к сведению - работать с esp-idf сложнее, а итоги не приносят плоды, особенно по теме обучения и изучения современных методов и типов коммуникаций, алгоритмов и методик программирования и прочего около-элеткронного барахла.
Т.е. бесполезное устаревшее времяпровождение, а всё обучение сводится к поиску готового решения или вопроса в форуме. По типу ЕГЭ.
Итог тут и наблюдаем.
 

pvvx

Активный участник сообщества
По сообщениям на форумах, собственная сборка LwIP1.4 для ардуин возможна пол Линухом, где установлен make. Хотя в makeEspArduino есть такая фраза в readme: "The makefile can be used on Linux, Mac OS and Microsoft Windows (Cygwin or WSL)." Вот только жаль, что ни сам этот пакет, ни тулчейн в Ардуине под виндос их не включает(
Это тоже решается вписыванием четырех символов "wsl " в файл <..8266\hardware\esp8266\2.7.4\boards.txt>
 

думка

New member
Это тоже решается вписыванием четырех символов "wsl " в файл <..8266\hardware\esp8266\2.7.4\boards.txt>
Еще раз спасибо, но я, к сожалению, не понял, куда эти буквы нужно вписывать. Был бы благодарен, если бы Вы высказались более точно и подробно. А так, получается, Вы напрасно потратили свое время, вряд ли это замечание мне поможет.

Пока что ставлю Cygwin, буду и дальше разбираться сам.
 

думка

New member
Всем привет, решил поделиться некоторыми результатами по этой теме.
Во-первых, под Windows собрать библиотеку lwip, которая идет с Ардуиной, не удалось никак. Ругается make, в котором я плохо разбираюсь, судя по всему не находит инклюды. Получилось собрать только на машине с Ubuntu, да и то не сразу, пришлось потанцевать.
Во-вторых, при тестах выяснилось, что более правильное решение проблемы с переполнением TCP_WND - не увеличение окна, а ускорение его очистки. Т.е. нужно читать оттуда данные как можно быстрее после получения, чтобы оно не заполнялось. И в этом направлении мне удалось добиться серьезного прогресса, после существенной оптимизации всех процедур по работе с буфером MP3-чипа и других задач, которые отнимали процессорное время. Теперь даже небольшого стандартного окна 4*TCP_MSS стало хватать, случаи переполнения практически прекратились.
Ситуация с затыками радикально улучшилась, при хорошем WiFi теперь можно более-менее нормально слушать станции, которые раньше слушать было невозможно.
И теперь осталась одна проблема, которую нужно решить для получения действительно хорошего результата: потери пакетов.
Поскольку стрим передается по TCP, то потери пакетов приводят к перезапросам и перезапуску стрима, на что уходит очень много времени. В результате, на станциях с низкой скоростью отклика происходит исчерпание буфера воспроизведения, и те самые "затыки".
Насколько я понял, потери пакетов происходят не только и не столько из-за плохого приема WiFi, они наблюдаются и при стабильном соединении. Я пришел к выводу, что они существенно связаны с загрузкой процессора. Я отключал некоторые вспомогательные задачи (сканирование serial и клавиатуры, вывод на дисплей и т.п.), и потери пакетов заметно снижались. Похоже, часть получаемых пакетов процессор просто не успевает обработать в стеке, если занят в другой ветке. Логично, ведь это NON_OS SDK.
Что делать? Вижу 2 возможных пути: а)попробовать внедрить библиотеку AsyncTCP. На тестовых примерах она показала очень хорошие результаты под нагрузкой, потерь пакетов практически не замечено. Проблема: библиотека заточена под использование в WEB-сервере, и там крайне слабый клиент. Точнее, его практически нет( и для радио многое нужно дописывать. Не уверен что справлюсь. Ну и обидно будет в конце, если результат окажется такойже)
Более толковым кажется вариант с переходом от Ардуины на RTOS_SDK. Более того, на ESP-IDF, чтобы какую-то преемственность с ESP32 обеспечить. Но это еще более трудоемкая задача, и я пока отложил ее на будущее. А пока переключил силы на новый проект радио на ESP32, возможно потом и перенесу его на ESP8266)

Всем успехов в ваших проектах, и не прощаюсь)
 

pvvx

Активный участник сообщества
Ситуация с затыками радикально улучшилась, при хорошем WiFi теперь можно более-менее нормально слушать станции, которые раньше слушать было невозможно.
Я что-то не понимаю. У вас внешняя микросхема занимается декодированием, а памяти под буфер вам не хвататет? :eek:
Без внешнего декодера, когда ESP8266 сама декодирует MP3 буфер уже достаточен и "щелчки/затыки" наблюдаются крайне редко на некоторых станциях... Был когда-то проект https://github.com/pvvx/mp3_decode (> 6 years ago), в нем доп. буфер > 20 Kbytes.
 

pvvx

Активный участник сообщества
Еще раз спасибо, но я, к сожалению, не понял, куда эти буквы нужно вписывать. Был бы благодарен, если бы Вы высказались более точно и подробно. А так, получается, Вы напрасно потратили свое время, вряд ли это замечание мне поможет.
Я не напрасно потратил время - у меня теперь собирается то, что мне нужно.
А плеер MP3, WAV, WMA, FLAC, AAC, APE c SD или BT или USB стоит ~100+ руб https://aliexpress.ru/item/33062927386.html и смысла такое собирать на ESP нет.
Он так-же имеет возможность программироваться вашим кодом.
Чуть дороже варианты с FM приемником и индикаторами...
 

думка

New member
Я что-то не понимаю. У вас внешняя микросхема занимается декодированием, а памяти под буфер вам не хвататет? :eek:
Без внешнего декодера, когда ESP8266 сама декодирует MP3 буфер уже достаточен и "щелчки/затыки" наблюдаются крайне редко на некоторых станциях... Был когда-то проект https://github.com/pvvx/mp3_decode (> 6 years ago), в нем доп. буфер > 20 Kbytes.
Спасибо, проект Ваш видел, но в деле не пробовал.
У меня действительно внешний MP3 декодер, подключенный через SPI. Сейчас удалось выделить около 40кБ на кольцевой буфер для аудиоданных. При стабильном потоке с быстрого сервера этого более чем достаточно для качественного приема без всяких затыков. Я поправил код таким образом, что полученные пакеты из TCP буфера достаточно быстро перетаскиваются в этот кольцевой буфер, и окно TCP переполняется только тогда, когда сам кольцевой буфер заполняется. Но проблема в том, что на некоторых стримах этот кольцевой буфер не успевает заполняться. Т.е. MP3 декодер вынимает данные быстрее, чем они поступают из стрима, с учетом потерь пакетов и т.п. Так что этот кольцевой буфер может быть любого размера - он оказывается бесполезен.
Да, корень проблемы в том, что некоторые стримы транслируют поток со скоростью на нижней границе, впритык, и буфер не успевает наполняться. А при наличии даже небольших потерь (порядка 1 -2 перезапроса пакета на 5 секунд), быстро исчерпается даже заранее заполненный буфер.
Посему, для работы с такими стримами я вижу только один выход - уменьшать потери времени на перезапрос пакетов.
 

enjoynering

Well-known member
Привет, думка. тоже пришел к таким выводам - "процессор просто не успевает обработать в стеке, если занят в другой ветке". Вот мои потуги на эту тему. Кстати в Arduino IDE lwip v2.0 вроде как пересобирается. Главное скопировать себе в проект файл lwipopts.h и из Arduino ESP8266 и включить его в проект самым ПЕРВЫМ. Если расскажешь как пользоваться WireShark я проверю дальше. Да кроме TCP_WND надо еще менять TCP_WND_UPDATE_THRESHOLD. Я сделал так:

Код:
#define TCP_WND (10 * TCP_MSS)

#define TCP_WND_UPDATE_THRESHOLD        LWIP_MIN((TCP_WND / 10), (TCP_MSS * 10))
проверяю в setup():
Код:
  Serial.printf_P(PSTR("RADIOLA82...............free heap %u-bytes\n"), ESP.getFreeHeap());
  Serial.printf_P(PSTR("RADIOLA82...............heap fragmentation %u%%\n"), ESP.getHeapFragmentation());
  Serial.printf_P(PSTR("RADIOLA82...............ring buffer size %u-bytes\n"), ringBuffer.getBufferSize());
  Serial.printf_P(PSTR("RADIOLA82...............TCP_MSS %u-bytes\n"), TCP_MSS);
  Serial.printf_P(PSTR("RADIOLA82...............TCP_WND %u-bytes\n"), TCP_WND);
  Serial.printf_P(PSTR("RADIOLA82...............TCP_WND_UPDATE_THRESHOLD %u-bytes\n"), TCP_WND_UPDATE_THRESHOLD);
 

думка

New member
Привет, думка. тоже пришел к таким выводам - "процессор просто не успевает обработать в стеке, если занят в другой ветке". Вот мои потуги на эту тему. Кстати в Arduino IDE lwip v2.0 вроде как пересобирается. Главное скопировать себе в проект файл и из Arduino ESP8266 и включить его в проект самым ПЕРВЫМ. Если расскажешь как пользоваться WireShark я проверю дальше. Да кроме TCP_WND надо еще менять TCP_WND_UPDATE_THRESHOLD. Я сделал так:

Код:
#define TCP_WND (10 * TCP_MSS)

#define TCP_WND_UPDATE_THRESHOLD        LWIP_MIN((TCP_WND / 10), (TCP_MSS * 10))
проверяю в setup():
Код:
  Serial.printf_P(PSTR("RADIOLA82...............free heap %u-bytes\n"), ESP.getFreeHeap());
  Serial.printf_P(PSTR("RADIOLA82...............heap fragmentation %u%%\n"), ESP.getHeapFragmentation());
  Serial.printf_P(PSTR("RADIOLA82...............ring buffer size %u-bytes\n"), ringBuffer.getBufferSize());
  Serial.printf_P(PSTR("RADIOLA82...............TCP_MSS %u-bytes\n"), TCP_MSS);
  Serial.printf_P(PSTR("RADIOLA82...............TCP_WND %u-bytes\n"), TCP_WND);
  Serial.printf_P(PSTR("RADIOLA82...............TCP_WND_UPDATE_THRESHOLD %u-bytes\n"), TCP_WND_UPDATE_THRESHOLD);

Спасибо за инфу, коллега)
Однако, будь бдителен!
Те значения TCP_WND и других макро, которые ты переопределяешь в своем lwipopts.h, совсем не обязательно используются в lwip библиотеке, которая линкуется к твоему проекту) Так что терминал тебе покажет одно, а по факту в коде будет совсем другое. Я поначалу тоже на это купился)
Дело в том, что в Ардуине lwip (и 1.4 и 2) используются в виде уже скомпиллированных объектников, которые просто линкуются к твоему коду. Это сделано для сокращения времени компилляции проекта. Когда я разобрался, как их компиллировать, то разницу заметил)
Так вот, в этих объектниках все IP параметры захардкожены на этапе компилляции, а исходники представлены только для справки, чтобы мы могли посмотреть что там было собрано.
Но вот в Ардуине под Linux есть опция, при которой вместе с твоим проектом перекомпиллируется lwip 1.4 уже из исходников, и в этом раскладе можно подсунуть хедер со своими параметрами, которые будут использованы при компилляции. По крайней мере, в теории, поскольку мне не удалось это сделать, по факту параметры всеравно оставались старые. Возможно, не в том месте правил.
Под Wind такой опции нет, поскольку make не собирает исходники lwip правильно - выдает ошибку.

Что касается WireShark - я наконецто нашел способ, как локально мониторить трафик ESPшки. Для этого нужно WiFi-адаптер на своем ПК перевести в режим точки доступа. Не все, но многие адаптеры так умеют. Причем это можно сделать штатными средствами ОС как под Win, так и под Linux (погугли как). И к этой AP подключаем испытуемый ESP. Ну и нужен второй сетевой адаптер на ПК, который подключаем к интернету. Можно использовать проводной или поставить WiFi донгл. В настройках там нужно включить опцию "предоставить доступ для всех пользователей", както так. Там же, где про настройку AP, как правило написано об этом.
Потом запускаеш WireShark и снимаешь лог на интерфейсе, который подключен к ESP. А там все прекрасно будет видно, и размер пакетов, TCP_WND, и много чего. Можно почитать мануалы на wireshark, но если будут конкретные вопросы - пиши, постараемся ответить)
 

думка

New member
Привет, думка. тоже пришел к таким выводам - "процессор просто не успевает обработать в стеке, если занят в другой ветке". Вот мои потуги на эту тему. Кстати в Arduino IDE lwip v2.0 вроде как пересобирается.
Кстати, по моим наблюдениям, с lwip1.4 потерь заметно меньше, а памяти заметно больше остается) я пока на этой версии остановился, из пакета Ардуины.
 

думка

New member
Я не напрасно потратил время - у меня теперь собирается то, что мне нужно.
А плеер MP3, WAV, WMA, FLAC, AAC, APE c SD или BT или USB стоит ~100+ руб https://aliexpress.ru/item/33062927386.html и смысла такое собирать на ESP нет.
Он так-же имеет возможность программироваться вашим кодом.
Чуть дороже варианты с FM приемником и индикаторами...
Занятная микруха, хорошо бы на ней еще SPI нашелся для подключения к ESP вместо VS1053. Все-таки, я себе WiFi радио собираю, и мне выход в интернет нужен в первую очередь) Блютуз это тоже неплохо, но с этим как раз проблем нет, копеешных ресиверов как грязи.
Кстати, в этом смысле ESP32 хорош еще тем, что можно чутка поправить настройки a2dp стека, и заметно улучшить битрейт SBC. В комплекте с PCM5102 получается почти HiFi)
 

enjoynering

Well-known member
Спасибо за информацию по Wireshark. Usb свиток есть, на лептопе стоит древняя intel она умеет прикидываться точкой доступа. Пробую на выходных и отпишусь.

Про использование скомпилированных исходников lwip. Тогда как объяснить, то что в ардуино можно выбрать глубину TCP_WND перед загрузкой. Завется у них "Higher Bandwidth" (окно получается 1460 байт) и "Lower Bandwidth" (окно 536 байт)?
 

думка

New member
Спасибо за информацию по Wireshark. Usb свиток есть, на лептопе стоит древняя intel она умеет прикидываться точкой доступа. Пробую на выходных и отпишусь.

Про использование скомпилированных исходников lwip. Тогда как объяснить, то что в ардуино можно выбрать глубину TCP_WND перед загрузкой. Завется у них "Higher Bandwidth" (окно получается 1460 байт) и "Lower Bandwidth" (окно 536 байт)?
Тем, что у них есть несколько вариантов собранных библиотек, которые линкуются в зависимости от выбранного варианта.
смотри ...\AppData\Local\Arduino15\packages\esp8266\hardware\esp8266\2.5.2\tools\sdk\lib они все там
А вот здесь ...\Arduino15\packages\esp8266\hardware\esp8266\2.5.2\tools лежит boards.txt.py
где все варианты расписаны:
Код:
  ####################### lwip

    'lwip2': collections.OrderedDict([
        ( '.menu.ip.lm2f', 'v2 Lower Memory' ),
        ( '.menu.ip.lm2f.build.lwip_include', 'lwip2/include' ),
        ( '.menu.ip.lm2f.build.lwip_lib', '-llwip2-536-feat' ),
        ( '.menu.ip.lm2f.build.lwip_flags', '-DLWIP_OPEN_SRC -DTCP_MSS=536 -DLWIP_FEATURES=1 -DLWIP_IPV6=0' ),
        ( '.menu.ip.hb2f', 'v2 Higher Bandwidth' ),
        ( '.menu.ip.hb2f.build.lwip_include', 'lwip2/include' ),
        ( '.menu.ip.hb2f.build.lwip_lib', '-llwip2-1460-feat' ),
        ( '.menu.ip.hb2f.build.lwip_flags', '-DLWIP_OPEN_SRC -DTCP_MSS=1460 -DLWIP_FEATURES=1 -DLWIP_IPV6=0' ),
        ( '.menu.ip.lm2n', 'v2 Lower Memory (no features)' ),
        ( '.menu.ip.lm2n.build.lwip_include', 'lwip2/include' ),
        ( '.menu.ip.lm2n.build.lwip_lib', '-llwip2-536' ),
        ( '.menu.ip.lm2n.build.lwip_flags', '-DLWIP_OPEN_SRC -DTCP_MSS=536 -DLWIP_FEATURES=0 -DLWIP_IPV6=0' ),
        ( '.menu.ip.hb2n', 'v2 Higher Bandwidth (no features)' ),
        ( '.menu.ip.hb2n.build.lwip_include', 'lwip2/include' ),
        ( '.menu.ip.hb2n.build.lwip_lib', '-llwip2-1460' ),
        ( '.menu.ip.hb2n.build.lwip_flags', '-DLWIP_OPEN_SRC -DTCP_MSS=1460 -DLWIP_FEATURES=0 -DLWIP_IPV6=0' ),
        ( '.menu.ip.lm6f', 'v2 IPv6 Lower Memory' ),
        ( '.menu.ip.lm6f.build.lwip_include', 'lwip2/include' ),
        ( '.menu.ip.lm6f.build.lwip_lib', '-llwip6-536-feat' ),
        ( '.menu.ip.lm6f.build.lwip_flags', '-DLWIP_OPEN_SRC -DTCP_MSS=536 -DLWIP_FEATURES=1 -DLWIP_IPV6=1' ),
        ( '.menu.ip.hb6f', 'v2 IPv6 Higher Bandwidth' ),
        ( '.menu.ip.hb6f.build.lwip_include', 'lwip2/include' ),
        ( '.menu.ip.hb6f.build.lwip_lib', '-llwip6-1460-feat' ),
        ( '.menu.ip.hb6f.build.lwip_flags', '-DLWIP_OPEN_SRC -DTCP_MSS=1460 -DLWIP_FEATURES=1 -DLWIP_IPV6=1' ),
        ]),

    'lwip': collections.OrderedDict([
        ( '.menu.ip.hb1', 'v1.4 Higher Bandwidth' ),
        ( '.menu.ip.hb1.build.lwip_lib', '-llwip_gcc' ),
        ( '.menu.ip.hb1.build.lwip_flags', '-DLWIP_OPEN_SRC' ),
        #( '.menu.ip.Espressif', 'v1.4 Espressif (xcc)' ),
        #( '.menu.ip.Espressif.build.lwip_lib', '-llwip' ),
        #( '.menu.ip.Espressif.build.lwip_flags', '-DLWIP_MAYBE_XCC' ),
        ( '.menu.ip.src', 'v1.4 Compile from source' ),
        ( '.menu.ip.src.build.lwip_lib', '-llwip_src' ),
        ( '.menu.ip.src.build.lwip_flags', '-DLWIP_OPEN_SRC' ),
        ( '.menu.ip.src.recipe.hooks.sketch.prebuild.1.pattern', 'make -C "{runtime.platform.path}/tools/sdk/lwip/src" install TOOLS_PATH="{runtime.tools.xtensa-lx106-elf-gcc.path}/bin/xtensa-lx106-elf-"' ),
        ]),
 

pvvx

Активный участник сообщества
Да, корень проблемы в том, что некоторые стримы транслируют поток со скоростью на нижней границе, впритык, и буфер не успевает наполняться. А при наличии даже небольших потерь (порядка 1 -2 перезапроса пакета на 5 секунд), быстро исчерпается даже заранее заполненный буфер.
Посему, для работы с такими стримами я вижу только один выход - уменьшать потери времени на перезапрос пакетов.
Я вроде уже про это писал - у вас скорость выдачи звука неверная и торопится. При таком подходе большинство каналов будет квакать, т.к. сервер дает строго поток + наличие до сотни килобайт (а иногда и ноль) для начальной буферизации. Так-же ограничено и отставание. При совмещении трансляции, на швах (их для того и делают) у вас будут большие паузы или вообще сервак прервет вам поток при сильном уходе в минус. Он для вас лично буферизирует поток и нафиг ему такие, которые требуют много памяти у него для буферизации...
А местный буфер нужен для сглаживания выпадения пакетов в канале WiFi и вашей местной сети и начальной буферизации чтобы согласовать и варьировать в малых пределах скорость вывода звука.
 

pvvx

Активный участник сообщества
Занятная микруха, хорошо бы на ней еще SPI нашелся для подключения к ESP вместо VS1053.
Есть у данных микрух SPI и всё остальное.
Все-таки, я себе WiFi радио собираю, и мне выход в интернет нужен в первую очередь) Блютуз это тоже неплохо, но с этим как раз проблем нет, копеешных ресиверов как грязи.
Кстати, в этом смысле ESP32 хорош еще тем, что можно чутка поправить настройки a2dp стека, и заметно улучшить битрейт SBC. В комплекте с PCM5102 получается почти HiFi)
Какой HiFi на 16 битах и 48 кГц, при этом получаемых ОДНИМ БИТОМ с передискретизацией в вашей микрухе и отсуствующих фильтрах?
ESP32 не может гнать звук даже по I2S - джиттер ужасный.
Но мне больше интересно - как вы хотите получить с сервера "будущее", если не может регулировать скорость вывода? С помощью машины времени?
 

pvvx

Активный участник сообщества
По проценту заполнения местного буфера определяется дельта скорости и вводятся поправки на скорость вывода. Вставляется или удаляется один из выходных семплов WAV-RAW идущих на DAC через определенный шаг. Это самый простой метод.
 

enjoynering

Well-known member
Проблема в том, что это не I2S и скоростью вывода мы не управляем. Ей управляет внешний кодек с DSP (в нашем случае VS1053). Он сообщает он нам, что может принять как минимум 32 байта (верхняя граница не известна) по состоянию на своём пине под названием DREQ.
 

думка

New member
Да, со скоростью это вопрос. Во-первых, когда потерь пакетов нет, или они минимальные - то даже мой "сложный" поток (Soma.FM кстати, можете сами потестить) воспроизводится стабильно, и даже постепенно буфер заполняет. Правда, как только что-то там теряется, то буфер довольно быстро снова опустошается(
Во-вторых, на других, более "быстрых" потоках (http://radio.mv.ru:8080/Radio_Cafe например, рекомендую)) буфер наполняется почти моментально, и его хватает даже при периодически потерях, тем более что сами потери восполняются гораздо быстрее.
Предполагаю, что на эту картину настройки буферизации сервера влияют. Т.е. если на сервере выделен достаточный буфер передачи, то он его выгружает на клиента, и как раз его содержимое накапливается в моем кольцевом буфере. Т.е. буфер передачи транслируется несколько быстрее стандартной скорости потока. Если же на сервере буфер передачи минимальный, то и выгружать на клиента нечего. К томуже, не известно как сервер потери пакетов отрабатывает, не исключено что потерянные пакеты уже выпадают из буфера, и он их откудато из потока вылавливает.. из-за чего и задержки большие.
Словом, сервер - дело темное. Ему же не скажешь "играй помэдленнее")
Да и на нашей стороне тоже сильно не разгуляешься, как справедливо enjoynering заметил. VS воспроизводит MP3-поток по своим часикам, а можно ли их как-то подстраивать - я пока не нашел как.
Словом, подстройка скорости - оно может и правильно, но в нашем случае непонятно как делать.
 

enjoynering

Well-known member
MP3-поток по своим часикам, а можно ли их как-то подстраивать
можно. у VS1053 есть так называемый режим stream mode. в этом режиме чип следит за тем чтоб его fifo буер был пуст максимум на половину и начинает замедлять скорость воспроизведение до 5%. почему-то рекомендуется только для mp3 и wav с постоянным битрейтом до 160 kbit/s. в этом режиме передавать надо кусками по 512 байт вместо 32.

я его пробовал. мне не понравилось. правда передавал кусками по 32 байта
 
Сверху Снизу