ESP32 Урок 44. Сопроцессор ULP. Первое знакомство



Сопроцессор ULP (ultra-low-power processor или процессор со сверхнизким потреблением) — это процессор, который остаётся в работе в режиме пониженного энергопотреблении Deep Sleep. В данном режиме основные два ядра контроллера не работают. Также данный процессор является программируемым автоматом конечного состояния (Finite State Machine или FSM).

Данный сопроцессор работает с памятью RTC_SLOW_MEM, в нём есть только 4 регистра и очень ограниченный набор инструкций, которых, как мы убедимся в дальнейшем, нам вполне будет достаточно для решения многих задач.

Вот некоторые характеристики сопроцессора

  • содержит до 8 КБ SRAM для инструкций и данных;
  • работает на частоте 8 мегагерц;
  • работает как в обычном режиме, так и в режиме глубокого сна;
  • способен разбудить цифровое ядро или послать прерывание ЦП;
  • может получать доступ к периферийным устройствам, внутренним датчикам и регистрам RTC;
  • содержит четыре 16-битных регистра общего назначения (R0, R1, R2, R3) для управления данными и доступа к ним;
  • включает также один 8-битный регистр Stage_cnt, которым может манипулировать ALU и использовать в инструкциях JUMP.

Хоть инструкций и не очень много, знакомиться с ними, как я считаю, лучше всего по мере написания кода, так как практика позволяет лучше усваивать материал. Тем более первоначальное знакомство с сопроцессором и третьим ядром у нас было уже в самом первом уроке данного курса (вспоминаем синие ножки GPIO контроллера, которые остаются работать в спящем режиме). Также мы можем при помощи ULP управлять и отслеживать аппаратно не только уровни данных ножек, но также мы можем использовать ADC, датчик температуры контроллера и шину I2C. Конечно, в данном уроке мы этим не будем пользоваться. Для того чтобы хоть как-то начать работу с командами ULP FSM, мы попробуем для начала ввести процессор в режим DEEP SLEEP и через определённое время при помощи инструкций ULP его оттуда вывести, также немного поработать с циклами подсчёта количества входов в спящий режим.

Схема нашего урока будет простейшая — отладочная плата с контроллером ESP32, подключенная к шине USB компьютера

 

 

Проект для работы с ULP FSM будет настраиваться также по-особенному. Тем не менее с чистого листа мы его создавать не будем, а создадим его из проекта урока 4 по работе с кнопкой с именем BUTTON01 и назовём его ULP_FSM_DELAY.

Также на данном уроке мы попробуем немного другой способ работы с конфигуратором проекта. Для этого мы для начала удалим файл sdkconfig из каталога проекта, тем самым мы заставим данный файл сгенерироваться с настройками по умолчанию. Также в каталоге main удалим файл Kconfig.projbuild. Для того, чтобы добавить свои настройки и сделать это не вручную, нам надо будет создать файл с именем sdkconfig.defaults следующего содержания

 

 

Я думаю, что по содержанию здесь всё понято, что мы включаем и какие режимы используем. Также мы настраиваем размер флеш-памяти. Если у вас другой размер, то вы указываете свой размер, так как по умолчанию он составляет всего 2 мегабайта. Конечно же, вы можете настроить размер стандартно в самом проекте.

И вот в случае, когда сборщик проекта видит такой файл, то данные настройки применяются.

Также давайте создадим ещё один конфигурационный файл с именем sdkconfig.defaults.esp32 следующего содержания

 

 

Файл с таким именем заставит сборщик проекта применить данные установки, если будет использоваться именно контроллер ESP32, а не в любом случае. Это делается на случай использования другого контроллера — ESP32C2, ESP32S2 и т.д.

Теперь открываем наш проект в ESpressif IDE, откроем файл main.c и полностью удалим весь код из тела функции app_main

 

 

Попытаемся собрать проект.

 Если всё нормально, то можно проверить наши кастомные настройки конфига

 

 

 

Создадим директорию

 

 

 

Создадим два ассемблерных файла с именами ulp_assembly_source_file.S и wake_up.S пока пустые в данной директории

 

 

 

 

 

Теперь надо подключить данные файлы к сборке. Откроем в каталоге main файл CMakeLists.txt и добавим в него следующие строки

 

 

Думаю, тут тоже всё ясно.

Попробуем теперь собрать проект. Пока всё соберётся, так как ассемблерные файлы ещё нигде не используются.

Создадим также для удобства заголовочный файл main.h следующего содержания

 

 

Подключим его в main.c, удалив перед этим подключение всех других заголовочных файлов

 

 

Проект опять нормально соберётся.

Подключим константы для использования точек начала и окончания ассемблерного кода

 

 

В функции app_main узнаем, каким образом у нас запустился контроллер — первоначальный старт или выход из сна, а также выведем это значение в терминал

 

 

Выше добавим функцию инициализации ULP, в которую мы будем попадать в случае первоначального старта контроллера

 

 

Вызовем её в app_main

 

 

Вернёмся в функцию init_ulp_program и загрузим ассемблерную программу в память RTC

 

 

Изолируем 12 и 15 ножки для лучшего энергосбережения, а то вдруг к ним что-то подключено

 

 

Подавим загрузочные сообщения

 

 

Мы можем запрограммировать 5 периодов пробуждения — от 0 до 4. Делается это до входа в режим глубокого сна.

Запрограммируем период на 20 милисекунд

 

 

Запустим нашу программу

 

 

Вот теперь проект точно не соберётся, так как пока что нет глобальной точки входа в нашу программу.

Любые глобальные метки, также переменные объявляются в ассемблерном файле и автоматически добавляются в заголовочный файл ulp_main.h. К данным меткам и переменным добавляется префикс ulp_. Так как мы используем указатель на метку ulp_entry, то значит в ассемблерном файле мы должны объявить метку entry и с данной метки и будет стартовать программа.

Поэтому перейдём в ассемблерный файл ulp_assembly_source_file.S и для начала подключим в нём необходимые заголовочные файлы

 

 

Далее объявим секцию bss

 

 

Затем секцию text и глобальную метку entry

 

 

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

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

Занесём в регистр r0 число 10000 (так как у нас регистры работают с 16-разрядными величинами, то больше, чем 65535 мы число туда не запишем)

 

 

Поздравляю! Мы добавили в код первую ассемблерную инструкцию.

Инструкция move перемещает значение из исходного регистра или 16-разрядное значение со знаком в регистр назначения.

 

 

Далее организуем цикл. Для этого добавим обычную локальную метку

 

 

Затем организуем обратный отсчёт. Для этого будем использовать инструкцию sub (вычитание), которая вычитает значение исходного регистра из значения другого исходного регистра или вычитает 16-битное значение со знаком из значения исходного регистра и сохраняет результат в целевой регистр.

 

 

 

Таким образом, мы вычли 1 из значения регистра r0 и в него же результат и записали.

Затем при помощи следующей инструкции wait мы подождём примерно одну микросекунду. Так как у нас сопроцессор работает на частоте 8 МГц, то нам потребуется примерно 8000 тысяч циклов для одной милисекунды. И так как на запуск самой инструкции тоже требуется какое то время, и также на следующую инструкцию перехода на метку по условию тоже, то подождём чуть меньше

 

 

Вот краткое описание команды

 

 

Затем нам нужно узнать, досчитал ли наш обратный счётчик до нуля или хотя бы до единицы, если ещё не досчитал, то перейти назад на метку, а если досчитал, то продолжить программу далее.

Для этого у нас есть несколько команд типа jump (переходов по условию). Бывает переход по абсолютному адресу jump, а есть также команда jumpr, которая осуществляет переход по относительному смещению. Также данная команда учитывает число, хранящееся в регистре r0. Вот эти условия

  • EQ (equal) – переход, если значение в R0 равно пороговому значению
  • LT (less than) – переход, если значение в R0 меньше порогового значения
  • LE (less or equal) – переход, если значение в R0 меньше или равно пороговому значению
  • GT (greater than) – переход, если значение в R0 больше порогового значения
  • GE (greater or equal) – переход, если значение в R0 больше или равно пороговому значению

 

Вот описание команды

 

 

Применим данную команду в нашем коде

 

 

Если значение в регистре r0 больше 1, то переходим на метку, если нет, то идём дальше

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

Для этого будем использовать подпрограмму, которую напишем в отдельном файле wake_up.S.

Перейдём в данный файл и для начала подключим нужные заголовочные файлы

 

 

Добавим глобальную метку

 

 

Нам надо прочитать бит RTC_CNTL_RDY_FOR_WAKEUP регистра RTC_CNTL_LOW_POWER_ST_REG для того, чтобы убедиться, что контроллер готов к пробуждению, так как возможно в данный момент выполняется та или иная инструкция.

 

 

Поэтому читаем регистр при помощи специальной функции

 

 

Затем при помощи логической команды AND узнаем, установлен ли у нас нужный нам бит

 

 

Инструкция AND выполняет побитовое логическое И между исходным регистром и другим исходным регистром или 16-битным значением со знаком и сохраняет результат в регистре назначения.

 

 

Далее применим команду jump — переход по абсолютному адресу

 

 

Вот описание команды

 

 

В нашем случае данная команда проверит равенство нулю содержимого регистра r0 и в случае успеха перейдёт назад на метку. И таким образом мы крутимся в цикле, пока не дождёмся установки нужного нами бита в регистре.

Немного подождём

 

 

Мы можем заметить что в команде wait можно использовать скобки, а можно и не использовать.

Вызовем инструкцию пробуждения

 

 

Инструкция простая, вот её описание

 

 

А далее применяем инструкцию остановки сопроцессора, которая также перезапускает таймер пробуждения ULP (wake up timer), если он разрешен.

 

 

Данная инструкция также простейшая, без параметров

 

 

Вернёмся в файл ulp_assembly_source_file.S и вызовем нашу подпрограмму пробуждения и добавим комментарий места, где мы заканчиваем нашу программу

 

 

Теперь вернёмся в нашу основную программу в файл main.c и выше функции app_main добавим функцию, в которую будем попадать в случае пробуждения

 

 

Вызовем данную функцию в app_main, если у нас будет именно пробуждение, а не первоначальный старт

 

 

Выведем в терминал сообщение, что мы будем сейчас уходить в спящий режим, задействуем его, и, соответственно в него уходим

 

 

Вернёмся в функцию update_count и объявим два указателя на символьные массивы

 

 

Для чего нам эти указатели? Мы будем считать количество пробуждений и записывать его в энергонезависимую память NVS, чтобы данное количество сохранялось и при отключении контроллера.

Прочитаем и отобразим данное число

 

 

Увеличим число и запишем его заново в энергонезависимую память

 

 

Давайте проверим, как работает наша программа.

Только перед этим давайте очистим пространство, в котором расположена память NVS, а именно участок, расположенный с адреса 0x9000 размером 0x6000, при помощи утилиты esptool. Такую операцию мы делали в уроке по NVS

 

 

Соберём код, прошьём контроллер и посмотрим результат в терминале

 

 

Затем каждые 10 секунд мы увидим сообщение в терминале о выходе из режима глубокого сна и наращивающийся счётчик

 

 

Теперь давайте также посчитаем количество входов в режим глубокого сна. Хоть мы, конечно, и так уже определили это, но давайте попробуем наращивать счётчик прямо в режиме глубокого сна, а именно в программе, выполняющейся в сопроцессоре ULP. Тут возникает вопрос, а как передать это значение в основную программу с целью дальнейшего отображения в терминале. Для этого существуют глобальные переменные. Перейдём в файл ulp_assembly_source_file.S и объявим такую переменную в секции bss

 

 

Соберём код и увидим, что объявление данной переменной появилось у нас в файле ulp_main.h

extern uint32_t ulp_cnt;

Вернёмся в файл main.c и обнулим данную переменную в функции init_ulp_program

 

 

Хотя она и так у нас инициализируется нулём в объявлении, но тем не менее полезно обнулить её физически.

Теперь нам надо будет увеличить её на единицу в файле ulp_assembly_source_file.S. Идём в данный файл и для начала занесём адрес данной переменной в регистр

 

 

Инструкцией move мы уже пользовались, только в данном случае мы уже мы заносим в регистр не просто число, а адрес переменной. Делается это занесением во второй параметр имени метки данной переменной.

Далее мы используем следующую похожую инструкцию ld, при помощи которой мы занесём значение переменной в регистр

 

 

Инструкция ld загружает из памяти младшее 16-битное полуслово с адресом [Rsrc+offset/4] в регистр назначения Rdst:

Rdst[15:0] = Mem[Rsrc + offset / 4][15:0]

 

Вот описание инструкции

 

 

В нашем случае мы загрузили младшую часть числа, находящегося по адресу, находящемуся в регистре r1 без смещения (со смещением 0), а это именно адрес метки cnt, в регистр r2.

Далее мы увеличим число, находящееся в регистре r2, на 1

 

 

В данном случае мы используем инструкцию add. Она работает практически также, как и использованная нами ранее инструкция sub, только здесь происходит операция не вычитания, а сложения. То есть инструкция add добавляет исходный регистр к другому исходному регистру или к 16-битному значению со знаком и сохраняет результат в целевом регистре.

Вот краткое описание инструкции

 

 

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

Осталось нам занести данное число обратно в переменную cnt. А делается это с помощью следующей инструкции

 

 

Инструкция st, в отличие от инструкции ld, обладает обратным эффектом. Она сохраняет 16-битное значение Rsrc в младшем полуслове памяти с адресом Rdst+offset. Верхнее полуслово записывается текущим программным счетчиком (PC) (выражено в словах со сдвигом влево на 5 бит) ИЛИ с помощью Rdst (0..3):

Mem[Rdst + offset / 4]{31:0} = {PC[10:0], 3'b0, Rdst, Rsrc[15:0]}

 

Вот описание данной инструкции

 

 

В нашем случае мы младшее полуслово значения, находящегося в регистре r2 занесли в память по адресу, находящемуся в регистре r1, а именно по адресу метки cnt. Нам сейчас не важно, что находится в старшем полуслове переменной, мы его в дальнейшем обнулим. Можно конечно организовать и 32-разрядный счётчик, но это для нас пока лишние заморочки.

Вернёмся в файл main.c в функцию update_count и отобразим значение младшего полуслова нашего счётчика в терминале

 

 

Соберём код, прошьём контроллер и посмотрим результат работы нашей программы в терминале

 

 

Всё отлично! У нас наращиваются оба счётчика — и тот, который не сбрасывается при отключении и перезагрузки, и тот, который наращивается в режиме пониженного энергопотребления Deep Sleep.

Итак, на данном занятии мы познакомились с работой сопроцессора ULP, работающего в глубоком спящем режиме, познакомились также с многими инструкциями данного сопроцессора, что позволило нам организовать переключение в глубокий спящий режим и выход из него в нормальный режим работы контроллера через определённое время.

Всем спасибо за внимание!

 

 

Предыдущий урок Программирование МК ESP32 Следующий урок

 

Исходный код

 

 

Недорогие отладочные платы ESP32 можно купить здесь:

На AliExpress Недорогие отладочные платы ESP32

На Яндекс.Маркет Недорогие отладочные платы ESP32

Логический анализатор 16 каналов можно приобрести (AliExpress) здесь

 

 

Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)

ESP32 Сопроцессор ULP. Первое знакомство

 

Смотреть ВИДЕОУРОК в Дзен (нажмите на картинку)

ESP32 Name

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

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*