Продолжаем работу с сопроцессором ULP (ultra-low-power processor или процессор со сверхнизким потреблением).
На данном уроке мы попытаемся организовать управление ножкой GPIO. Пока мы поработаем с портом GPIO на выход, для чего подключим к нашей схеме светодиод через ограничивающий резистор к ножке GPIO4, но только подключим мы к ней катодом, а анод подключим к питанию 3,3 вольта
Проект мы создадим на основе проекта прошлого урока с именем ULP_FSM_DELAY и присвоим ему имя ULP_FSM_BLINK.
Откроем наш проект в Espressif IDE, откроем файл main.c и удалим полностью функцию update_count.
В функции app_main удалим вызов данной функции
update_count();
В предыдущей строке удалим лишний текст
printf("ULP wakeup
, saving pulse count\n");
В функции init_ulp_program настроим наш светодиод на выход
1 2 3 4 5 6 |
ESP_ERROR_CHECK(err); gpio_num_t gpio_num = GPIO_NUM_4; rtc_gpio_init(gpio_num); rtc_gpio_set_direction(gpio_num, RTC_GPIO_MODE_OUTPUT_ONLY); |
А следующая переменная нам также будет не нужна
ulp_cnt = 0;
Следующую строку тоже немного подправим, там надо учитывать размер ячейки памяти
1 2 |
ulp_set_wakeup_period(0, 20000); err = ulp_run((&ulp_entry - RTC_SLOW_MEM) / sizeof(uint32_t)); //Start the program |
Затем идём в файл ulp_assembly_source_file.S и удалим объявление переменной
.global cntcnt: .long 0
Задержка в начале программы у нас будет 1 секунда, поэтому вносим исправления в коде
//delay(1 sec)
move r0, 1000
Но, так как задержка у нас будет не одна, а несколько, то хотелось бы процедуру задержки организовать именно как процедуру с выходом из неё. А такой возможности у ассемблера, который понимает сопроцессор ULP, как-таковой нет. Поэтому давайте как-то организуем такую возможность самостоятельно, организовав стек и выход из процедуры.
Для начала удалим весь код ,начиная с того места, где мы внесли исправления до перехода к метке пробуждения
delay_ms_loop:
sub r0, r0, 1
wait(7990) /* 1millsecond = 1000 microsecond */
jumpr delay_ms_loop, 1, GE
//cnt += 1
move r1, cnt //r1 = mem[cnt]
ld r2, r1, 0 //r2 = cnt
add r2, r2, 1 //r2 += 1
st r2, r1, 0 //cnt = r2
Организуем небольшой стек, зарезервировав несколько байт в секции bss и добавив метки начала и конца данного участка памяти
1 2 3 4 5 6 7 8 9 |
.bss .global stack stack: .skip 24 .global stackEnd stackEnd: .long 0 |
В самом начале программы объявим макрос сохранения значения в стек с последующим декрементированием вершины стека
1 2 3 4 5 6 |
#include "soc/sens_reg.h" .macro push rx st \rx,r3,0 sub r3,r3,1 .endm |
Затем добавим макрос возврата значения из стека с предварительным инкрементированием вершины стека
1 2 3 4 5 6 7 |
sub r3,r3,1 .endm .macro pop rx add r3,r3,1 ld \rx,r3,0 .endm |
Следующий макрос — запись в стек адреса выполнения (адрес, куда после выполнения процедуры нужно будет возвращаться
1 2 3 4 5 6 7 8 |
ld \rx,r3,0 .endm .macro psr sr=r1 pos=. .set _next2,(\pos+16) move \sr,_next2 push \sr .endm |
В основном тут всё понятно, стандартное написание макросов. Заинтересовать нас может команда .set. Это директива, задающая временный символьный эквивалент выражению.
В 3 строке макроса мы используем другой макрос — push.
Ну и объявим ещё один макрос, который будет нас возвращать в то место, из которого мы попали в процедуру по метке
1 2 3 4 5 6 7 |
push \sr .endm .macro ret sr=r1 pop \sr jump \sr .endm |
И также добавим макросы, которые будут устанавливать уровни на ножке GPIO
1 2 3 4 5 6 7 8 9 10 11 |
jump \sr .endm .macro GPIO_LOW WRITE_RTC_REG(RTC_GPIO_OUT_W1TC_REG, RTC_GPIO_OUT_DATA_W1TC_S + 10, 1, 1) .endm /* Output mode */ .macro GPIO_HIGH WRITE_RTC_REG(RTC_GPIO_OUT_W1TS_REG, RTC_GPIO_OUT_DATA_W1TS_S + 10, 1, 1) .endm |
В данных макросах мы используем стандартные макрокоманды, которые в регистровое поле регистра заносят число.
Мы используем регистры RTC_GPIO_OUT_W1TC_REG и RTC_GPIO_OUT_W1TS_REG, которые отвечают соответственно за установку и сброс битов, соответствующим уровню ножек GPIO. Тем самым мы можем установить или высокий или низкий уровень ножки. Поля RTC_GPIO_OUT_DATA_W1TC_S и RTC_GPIO_OUT_DATA_W1TS_S — это примерно то же самое, но только это не сам регистр, а определённая его часть.
В технической документации это всё отображено
Следующий параметр — ширина поля. мы берём только 1 бит. Последний параметр — число, которое мы заносим в поле. В нашем случае 1, так как если в первый регистр в соответствующий бит определённой ножке мы заносим 1, то уровень устанавливается высокий, а если мы 1 заносим в соответствующий бит в поле другого регистра, то уровень устанавливается низкий.
Также есть вопрос, почему мы смещаемся от 0 ножки на 10, а не на 4, ведь у нас 4 ножка. А это потому, что для RTC нумерация ножек другая, вот таблица
В самом начале программы сохраним в регистр R3 адрес вершины стека
1 2 |
entry: move r3, stackEnd |
А в самом конце файла, после команды перехода к процедуре пробуждения добавим процедуру нашей задержки, практически без изменений кода
1 2 3 4 5 6 7 8 9 10 |
jump wake_up /* End program */ //delay delay_ms: delay_ms_loop: sub r0, r0, 1 wait(7990) /* 1millsecond = 1000 microsecond */ jumpr delay_ms_loop, 1, GE ret |
Перед переходом к данной процедуре мы заносим в регистр R0 значение задержки. Также не забываем сохранить адрес места в коде, куда нам надо будет вернуться.
Число для задержки в регистре R0, как мы помним, у нас уже в начале нашей программы занесено, поэтому сохраняем счётчик программы и переходим к задержке
1 2 3 |
move r0, 1000 psr jump delay_ms |
При помощи макроса устанавливаем низкий уровень на ножке GPIO. По подключению светодиода мы понимаем, что мы его сейчас зажигаем
1 2 3 |
jump delay_ms GPIO_LOW |
Добавляем ещё задержку на 1 секунду
1 2 3 4 5 6 |
GPIO_LOW //delay(1 sec) move r0, 1000 psr jump delay_ms |
Гасим наш светодиод, устанавливая высокий уровень на ножке
1 2 3 |
jump delay_ms GPIO_HIGH |
И затем перед пробуждением ждём ещё 4 секунды
1 2 3 4 5 6 |
GPIO_HIGH //delay(4 sec) move r0, 4000 psr jump delay_ms |
Мы видим, что у кода появилась читабельность за счёт наших макросов. Только единственный минус — два регистра — r1 и r3 у нас задействованы для работы макросов, поэтому мы должны это учитывать и не использовать их для работы с другими величинами во время работы с макросами.
Вот и вся программа. Собираем код, прошиваем наш контроллер и смотрим, что нам выдаст терминал
Через секунду после старта светодиод зажигается
Через секунду светодиод гаснет, затем через 4 секунды мы видим информацию в терминале о пробуждении основного процессора
И так по кругу.
Итак, на данном уроке нам удалось при помощи сопроцессора ULP управлять уровнем на ножке GPIO. Также мы научились использовать макросы, что позволило нам организовать выход из процедур, что в дальнейшем, думаю принесёт очень большую пользу читабельности нашего кода и его размеру.
Предыдущий урок Программирование МК ESP32 Следующий урок
Недорогие отладочные платы ESP32 можно купить здесь Недорогие отладочные платы ESP32
Недорогие отладочные платы ESP32/ESP32-C3/ESP32-S3 можно купить здесь Недорогие отладочные платы ESP32
Логический анализатор 16 каналов можно приобрести здесь
Смотреть ВИДЕОУРОК в Rhttps://rutube.ru/video/346951ed90b23634d2c2e7587771b360/uTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в Дзен (нажмите на картинку)
Добавить комментарий